123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376 |
- #!/usr/bin/python
- #
- # This Source Code Form is subject to the terms of the Mozilla Public
- # License, v. 2.0. If a copy of the MPL was not distributed with this
- # file, You can obtain one at http://mozilla.org/MPL/2.0/.
- #
- # When run directly, this script expects the following environment variables
- # to be set:
- # UPLOAD_HOST : host to upload files to
- # UPLOAD_USER : username on that host
- # and one of the following:
- # UPLOAD_PATH : path on that host to put the files in
- # UPLOAD_TO_TEMP : upload files to a new temporary directory
- #
- # If UPLOAD_HOST and UPLOAD_USER are not set, this script will simply write out
- # the properties file.
- #
- # If UPLOAD_HOST is "localhost", then files are simply copied to UPLOAD_PATH.
- # In this case, UPLOAD_TO_TEMP and POST_UPLOAD_CMD are not supported, and no
- # properties are written out.
- #
- # And will use the following optional environment variables if set:
- # UPLOAD_SSH_KEY : path to a ssh private key to use
- # UPLOAD_PORT : port to use for ssh
- # POST_UPLOAD_CMD: a commandline to run on the remote host after uploading.
- # UPLOAD_PATH and the full paths of all files uploaded will
- # be appended to the commandline.
- #
- # All files to be uploaded should be passed as commandline arguments to this
- # script. The script takes one other parameter, --base-path, which you can use
- # to indicate that files should be uploaded including their paths relative
- # to the base path.
- import sys, os
- import re
- import json
- import errno
- import hashlib
- import shutil
- from optparse import OptionParser
- from subprocess import (
- check_call,
- check_output,
- STDOUT,
- CalledProcessError,
- )
- import concurrent.futures as futures
- import redo
- def OptionalEnvironmentVariable(v):
- """Return the value of the environment variable named v, or None
- if it's unset (or empty)."""
- if v in os.environ and os.environ[v] != "":
- return os.environ[v]
- return None
- def FixupMsysPath(path):
- """MSYS helpfully translates absolute pathnames in environment variables
- and commandline arguments into Windows native paths. This sucks if you're
- trying to pass an absolute path on a remote server. This function attempts
- to un-mangle such paths."""
- if 'OSTYPE' in os.environ and os.environ['OSTYPE'] == 'msys':
- # sort of awful, find out where our shell is (should be in msys/bin)
- # and strip the first part of that path out of the other path
- if 'SHELL' in os.environ:
- sh = os.environ['SHELL']
- msys = sh[:sh.find('/bin')]
- if path.startswith(msys):
- path = path[len(msys):]
- return path
- def WindowsPathToMsysPath(path):
- """Translate a Windows pathname to an MSYS pathname.
- Necessary because we call out to ssh/scp, which are MSYS binaries
- and expect MSYS paths."""
- # If we're not on Windows, or if we already have an MSYS path (starting
- # with '/' instead of 'c:' or something), then just return.
- if sys.platform != 'win32' or path.startswith('/'):
- return path
- (drive, path) = os.path.splitdrive(os.path.abspath(path))
- return "/" + drive[0] + path.replace('\\','/')
- def AppendOptionalArgsToSSHCommandline(cmdline, port, ssh_key):
- """Given optional port and ssh key values, append valid OpenSSH
- commandline arguments to the list cmdline if the values are not None."""
- if port is not None:
- cmdline.append("-P%d" % port)
- if ssh_key is not None:
- # Don't interpret ~ paths - ssh can handle that on its own
- if not ssh_key.startswith('~'):
- ssh_key = WindowsPathToMsysPath(ssh_key)
- cmdline.extend(["-o", "IdentityFile=%s" % ssh_key])
- # In case of an issue here we don't want to hang on a password prompt.
- cmdline.extend(["-o", "BatchMode=yes"])
- def DoSSHCommand(command, user, host, port=None, ssh_key=None):
- """Execute command on user@host using ssh. Optionally use
- port and ssh_key, if provided."""
- cmdline = ["ssh"]
- AppendOptionalArgsToSSHCommandline(cmdline, port, ssh_key)
- cmdline.extend(["%s@%s" % (user, host), command])
- with redo.retrying(check_output, sleeptime=10) as f:
- try:
- output = f(cmdline, stderr=STDOUT).strip()
- except CalledProcessError as e:
- print "failed ssh command output:"
- print '=' * 20
- print e.output
- print '=' * 20
- raise
- return output
- raise Exception("Command %s returned non-zero exit code" % cmdline)
- def DoSCPFile(file, remote_path, user, host, port=None, ssh_key=None,
- log=False):
- """Upload file to user@host:remote_path using scp. Optionally use
- port and ssh_key, if provided."""
- if log:
- print 'Uploading %s' % file
- cmdline = ["scp"]
- AppendOptionalArgsToSSHCommandline(cmdline, port, ssh_key)
- cmdline.extend([WindowsPathToMsysPath(file),
- "%s@%s:%s" % (user, host, remote_path)])
- with redo.retrying(check_call, sleeptime=10) as f:
- f(cmdline)
- return
- raise Exception("Command %s returned non-zero exit code" % cmdline)
- def GetBaseRelativePath(path, local_file, base_path):
- """Given a remote path to upload to, a full path to a local file, and an
- optional full path that is a base path of the local file, construct the
- full remote path to place the file in. If base_path is not None, include
- the relative path from base_path to file."""
- if base_path is None or not local_file.startswith(base_path):
- # Hack to work around OSX uploading the i386 SDK from i386/dist. Both
- # the i386 SDK and x86-64 SDK end up in the same directory this way.
- if base_path.endswith('/x86_64/dist'):
- return GetBaseRelativePath(path, local_file, base_path.replace('/x86_64/', '/i386/'))
- return path
- dir = os.path.dirname(local_file)
- # strip base_path + extra slash and make it unixy
- dir = dir[len(base_path)+1:].replace('\\','/')
- return path + dir
- def GetFileHashAndSize(filename):
- sha512Hash = 'UNKNOWN'
- size = 'UNKNOWN'
- try:
- # open in binary mode to make sure we get consistent results
- # across all platforms
- with open(filename, "rb") as f:
- shaObj = hashlib.sha512(f.read())
- sha512Hash = shaObj.hexdigest()
- size = os.path.getsize(filename)
- except:
- raise Exception("Unable to get filesize/hash from file: %s" % filename)
- return (sha512Hash, size)
- def GetMarProperties(filename):
- if not os.path.exists(filename):
- return {}
- (mar_hash, mar_size) = GetFileHashAndSize(filename)
- return {
- 'completeMarFilename': os.path.basename(filename),
- 'completeMarSize': mar_size,
- 'completeMarHash': mar_hash,
- }
- def GetUrlProperties(output, package):
- # let's create a switch case using name-spaces/dict
- # rather than a long if/else with duplicate code
- property_conditions = [
- # key: property name, value: condition
- ('symbolsUrl', lambda m: m.endswith('crashreporter-symbols.zip') or
- m.endswith('crashreporter-symbols-full.zip')),
- ('testsUrl', lambda m: m.endswith(('tests.tar.bz2', 'tests.zip'))),
- ('robocopApkUrl', lambda m: m.endswith('apk') and 'robocop' in m),
- ('jsshellUrl', lambda m: 'jsshell-' in m and m.endswith('.zip')),
- ('completeMarUrl', lambda m: m.endswith('.complete.mar')),
- ('partialMarUrl', lambda m: m.endswith('.mar') and '.partial.' in m),
- ('codeCoverageURL', lambda m: m.endswith('code-coverage-gcno.zip')),
- ('sdkUrl', lambda m: m.endswith(('sdk.tar.bz2', 'sdk.zip'))),
- ('testPackagesUrl', lambda m: m.endswith('test_packages.json')),
- ('packageUrl', lambda m: m.endswith(package)),
- ]
- url_re = re.compile(r'''^(https?://.*?\.(?:tar\.bz2|dmg|zip|apk|rpm|mar|tar\.gz|json))$''')
- properties = {}
- try:
- for line in output.splitlines():
- m = url_re.match(line.strip())
- if m:
- m = m.group(1)
- for prop, condition in property_conditions:
- if condition(m):
- properties.update({prop: m})
- break
- except IOError as e:
- if e.errno != errno.ENOENT:
- raise
- properties = {prop: 'UNKNOWN' for prop, condition in property_conditions}
- return properties
- def UploadFiles(user, host, path, files, verbose=False, port=None, ssh_key=None, base_path=None, upload_to_temp_dir=False, post_upload_command=None, package=None):
- """Upload each file in the list files to user@host:path. Optionally pass
- port and ssh_key to the ssh commands. If base_path is not None, upload
- files including their path relative to base_path. If upload_to_temp_dir is
- True files will be uploaded to a temporary directory on the remote server.
- Generally, you should have a post upload command specified in these cases
- that can move them around to their correct location(s).
- If post_upload_command is not None, execute that command on the remote host
- after uploading all files, passing it the upload path, and the full paths to
- all files uploaded.
- If verbose is True, print status updates while working."""
- if not host or not user:
- return {}
- if (not path and not upload_to_temp_dir) or (path and upload_to_temp_dir):
- print "One (and only one of UPLOAD_PATH or UPLOAD_TO_TEMP must be " + \
- "defined."
- sys.exit(1)
- if upload_to_temp_dir:
- path = DoSSHCommand("mktemp -d", user, host, port=port, ssh_key=ssh_key)
- if not path.endswith("/"):
- path += "/"
- if base_path is not None:
- base_path = os.path.abspath(base_path)
- remote_files = []
- properties = {}
- def get_remote_path(p):
- return GetBaseRelativePath(path, os.path.abspath(p), base_path)
- try:
- # Do a pass to find remote directories so we don't perform excessive
- # scp calls.
- remote_paths = set()
- for file in files:
- if not os.path.isfile(file):
- raise IOError("File not found: %s" % file)
- remote_paths.add(get_remote_path(file))
- # If we wanted to, we could reduce the remote paths if they are a parent
- # of any entry.
- for p in sorted(remote_paths):
- DoSSHCommand("mkdir -p " + p, user, host, port=port, ssh_key=ssh_key)
- with futures.ThreadPoolExecutor(4) as e:
- fs = []
- # Since we're uploading in parallel, the largest file should take
- # the longest to upload. So start it first.
- for file in sorted(files, key=os.path.getsize, reverse=True):
- remote_path = get_remote_path(file)
- fs.append(e.submit(DoSCPFile, file, remote_path, user, host,
- port=port, ssh_key=ssh_key, log=verbose))
- remote_files.append(remote_path + '/' + os.path.basename(file))
- # We need to call result() on the future otherwise exceptions could
- # get swallowed.
- for f in futures.as_completed(fs):
- f.result()
- if post_upload_command is not None:
- if verbose:
- print "Running post-upload command: " + post_upload_command
- file_list = '"' + '" "'.join(remote_files) + '"'
- output = DoSSHCommand('%s "%s" %s' % (post_upload_command, path, file_list), user, host, port=port, ssh_key=ssh_key)
- # We print since mozharness may parse URLs from the output stream.
- print output
- properties = GetUrlProperties(output, package)
- finally:
- if upload_to_temp_dir:
- DoSSHCommand("rm -rf %s" % path, user, host, port=port,
- ssh_key=ssh_key)
- if verbose:
- print "Upload complete"
- return properties
- def CopyFilesLocally(path, files, verbose=False, base_path=None, package=None):
- """Copy each file in the list of files to `path`. The `base_path` argument is treated
- as it is by UploadFiles."""
- if not path.endswith("/"):
- path += "/"
- if base_path is not None:
- base_path = os.path.abspath(base_path)
- for file in files:
- file = os.path.abspath(file)
- if not os.path.isfile(file):
- raise IOError("File not found: %s" % file)
- # first ensure that path exists remotely
- target_path = GetBaseRelativePath(path, file, base_path)
- if not os.path.exists(target_path):
- os.makedirs(target_path)
- if verbose:
- print "Copying " + file + " to " + target_path
- shutil.copy(file, target_path)
- def WriteProperties(files, properties_file, url_properties, package):
- properties = url_properties
- for file in files:
- if file.endswith('.complete.mar'):
- properties.update(GetMarProperties(file))
- with open(properties_file, 'w') as outfile:
- properties['packageFilename'] = package
- properties['uploadFiles'] = [os.path.abspath(f) for f in files]
- json.dump(properties, outfile, indent=4)
- if __name__ == '__main__':
- host = OptionalEnvironmentVariable('UPLOAD_HOST')
- user = OptionalEnvironmentVariable('UPLOAD_USER')
- path = OptionalEnvironmentVariable('UPLOAD_PATH')
- upload_to_temp_dir = OptionalEnvironmentVariable('UPLOAD_TO_TEMP')
- port = OptionalEnvironmentVariable('UPLOAD_PORT')
- if port is not None:
- port = int(port)
- key = OptionalEnvironmentVariable('UPLOAD_SSH_KEY')
- post_upload_command = OptionalEnvironmentVariable('POST_UPLOAD_CMD')
- if sys.platform == 'win32':
- if path is not None:
- path = FixupMsysPath(path)
- if post_upload_command is not None:
- post_upload_command = FixupMsysPath(post_upload_command)
- parser = OptionParser(usage="usage: %prog [options] <files>")
- parser.add_option("-b", "--base-path",
- action="store",
- help="Preserve file paths relative to this path when uploading. If unset, all files will be uploaded directly to UPLOAD_PATH.")
- parser.add_option("--properties-file",
- action="store",
- help="Path to the properties file to store the upload properties.")
- parser.add_option("--package",
- action="store",
- help="Name of the main package.")
- (options, args) = parser.parse_args()
- if len(args) < 1:
- print "You must specify at least one file to upload"
- sys.exit(1)
- if not options.properties_file:
- print "You must specify a --properties-file"
- sys.exit(1)
- if host == "localhost":
- if upload_to_temp_dir:
- print "Cannot use UPLOAD_TO_TEMP with UPLOAD_HOST=localhost"
- sys.exit(1)
- if post_upload_command:
- # POST_UPLOAD_COMMAND is difficult to extract from the mozharness
- # scripts, so just ignore it until it's no longer used anywhere
- print "Ignoring POST_UPLOAD_COMMAND with UPLOAD_HOST=localhost"
- try:
- if host == "localhost":
- CopyFilesLocally(path, args, base_path=options.base_path,
- package=options.package,
- verbose=True)
- else:
- url_properties = UploadFiles(user, host, path, args,
- base_path=options.base_path, port=port, ssh_key=key,
- upload_to_temp_dir=upload_to_temp_dir,
- post_upload_command=post_upload_command,
- package=options.package, verbose=True)
- WriteProperties(args, options.properties_file, url_properties, options.package)
- except IOError, (strerror):
- print strerror
- sys.exit(1)
|