| # Copyright (c) 2012 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Module containing gsutil helper methods.""" |
| |
| import distutils.version |
| import fnmatch |
| import os |
| import random |
| import re |
| import subprocess |
| import time |
| |
| import devserver_constants |
| import log_util |
| |
| |
| GSUTIL_ATTEMPTS = 1 |
| UPLOADED_LIST = 'UPLOADED' |
| |
| |
| # Module-local log function. |
| def _Log(message, *args): |
| return log_util.LogWithTag('GSUTIL_UTIL', message, *args) |
| |
| |
| class GSUtilError(Exception): |
| """Exception raised when we run into an error running gsutil.""" |
| pass |
| |
| |
| class PatternNotSpecific(Exception): |
| """Raised when unexpectedly more than one item is returned for a pattern.""" |
| pass |
| |
| |
| def GSUtilRun(cmd, err_msg): |
| """Runs a GSUTIL command up to GSUTIL_ATTEMPTS number of times. |
| |
| Attempts are tried with exponential backoff. |
| |
| Args: |
| cmd: a string containing the gsutil command to run. |
| err_msg: string prepended to the exception thrown in case of a failure. |
| Returns: |
| stdout of the called gsutil command. |
| Raises: |
| GSUtilError: if all attempts to run gsutil have failed. |
| """ |
| proc = None |
| sleep_timeout = 1 |
| stderr = None |
| for _ in range(GSUTIL_ATTEMPTS): |
| proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE) |
| stdout, stderr = proc.communicate() |
| if proc.returncode == 0: |
| return stdout |
| |
| not_exist_messages = ('matched no objects', 'non-existent object', |
| 'no urls matched') |
| if (stderr and any(x in stderr.lower() for x in not_exist_messages) or |
| stdout and any(x in stdout.lower() for x in not_exist_messages)): |
| # If the object does not exist, exit now instead of wasting time |
| # on retrying. Note that `gsutil stat` prints error message to |
| # stdout instead (b/16020252), so we check both stdout and |
| # stderr. |
| break |
| |
| if proc.returncode == 127: |
| raise GSUtilError('gsutil tool not found in your path.') |
| |
| time.sleep(sleep_timeout) |
| sleep_timeout *= 2 |
| |
| raise GSUtilError('%s GSUTIL cmd %s failed with return code %d:\n\n%s' % ( |
| err_msg, cmd, proc.returncode, stderr)) |
| |
| |
| def DownloadFromGS(src, dst): |
| """Downloads object from gs_url |src| to |dst|. |
| |
| Args: |
| src: source file on GS that needs to be downloaded. |
| dst: file to copy the source file to. |
| Raises: |
| GSUtilError: if an error occurs during the download. |
| """ |
| cmd = 'gsutil cp %s %s' % (src, dst) |
| msg = 'Failed to download "%s".' % src |
| GSUtilRun(cmd, msg) |
| |
| |
| def _GlobHasWildcards(pattern): |
| """Returns True if a glob pattern contains any wildcards.""" |
| return len(pattern) > len(pattern.translate(None, '*?[]')) |
| |
| |
| def GetGSNamesWithWait(pattern, archive_url, err_str, timeout=600, delay=10, |
| is_regex_pattern=False): |
| """Returns the google storage names specified by the given pattern. |
| |
| This method polls Google Storage until the target artifacts specified by the |
| pattern is available or until the timeout occurs. Because we may not know the |
| exact name of the target artifacts, the method accepts a filename pattern, |
| to identify whether an artifact whose name matches the pattern exists (e.g. |
| use pattern '*_full_*' to search for the full payload |
| 'chromeos_R17-1413.0.0-a1_x86-mario_full_dev.bin'). Returns the name only if |
| found before the timeout. |
| |
| Args: |
| pattern: a path pattern (glob or regex) identifying the files we need. |
| archive_url: URL of the Google Storage bucket. |
| err_str: String to display in the error message on error. |
| timeout: how long are we allowed to keep trying. |
| delay: how long to wait between attempts. |
| is_regex_pattern: Whether the pattern is a regex (otherwise a glob). |
| Returns: |
| The list of artifacts matching the pattern in Google Storage bucket or None |
| if not found. |
| |
| """ |
| # Define the different methods used for obtaining the list of files on the |
| # archive directory, in the order in which they are attempted. Each method is |
| # defined by a tuple consisting of (i) the gsutil command-line to be |
| # executed; (ii) the error message to use in case of a failure (returned in |
| # the corresponding exception); (iii) the desired return value to use in case |
| # of success, or None if the actual command output should be used. |
| get_methods = [] |
| # If the pattern is a glob and contains no wildcards, we'll first attempt to |
| # stat the file via du. |
| if not (is_regex_pattern or _GlobHasWildcards(pattern)): |
| get_methods.append(('gsutil stat %s/%s' % (archive_url, pattern), |
| 'Failed to stat on the artifact file.', pattern)) |
| |
| # The default method is to check the manifest file in the archive directory. |
| get_methods.append(('gsutil cat %s/%s' % (archive_url, UPLOADED_LIST), |
| 'Failed to get a list of uploaded files.', |
| None)) |
| # For backward compatibility, we fall back to using "gsutil ls" when the |
| # manifest file is not present. |
| get_methods.append(('gsutil ls %s/*' % archive_url, |
| 'Failed to list archive directory contents.', |
| None)) |
| |
| deadline = time.time() + timeout |
| while True: |
| uploaded_list = [] |
| for cmd, msg, override_result in get_methods: |
| try: |
| result = GSUtilRun(cmd, msg) |
| except GSUtilError: |
| continue # It didn't work, try the next method. |
| |
| if override_result: |
| result = override_result |
| |
| # Make sure we're dealing with artifact base names only. |
| uploaded_list = [os.path.basename(p) for p in result.splitlines()] |
| break |
| |
| # Only keep files matching the target artifact name/pattern. |
| if is_regex_pattern: |
| filter_re = re.compile(pattern) |
| matching_names = [f for f in uploaded_list |
| if filter_re.search(f) is not None] |
| else: |
| matching_names = fnmatch.filter(uploaded_list, pattern) |
| |
| if matching_names: |
| return matching_names |
| |
| # Don't delay past deadline. |
| to_delay = random.uniform(1.5 * delay, 2.5 * delay) |
| if to_delay < (deadline - time.time()): |
| _Log('Retrying in %f seconds...%s', to_delay, err_str) |
| time.sleep(to_delay) |
| else: |
| return None |
| |
| |
| def GetLatestVersionFromGSDir(gsutil_dir, with_release=True): |
| """Returns most recent version number found in a GS directory. |
| |
| This lists out the contents of the given GS bucket or regex to GS buckets, |
| and tries to grab the newest version found in the directory names. |
| |
| Args: |
| gsutil_dir: directory location on GS to check. |
| with_release: whether versions include a release milestone (e.g. R12). |
| Returns: |
| The most recent version number found. |
| |
| """ |
| cmd = 'gsutil ls %s' % gsutil_dir |
| msg = 'Failed to find most recent builds at %s' % gsutil_dir |
| dir_names = [p.split('/')[-2] for p in GSUtilRun(cmd, msg).splitlines()] |
| try: |
| filter_re = re.compile(devserver_constants.VERSION_RE if with_release |
| else devserver_constants.VERSION) |
| versions = filter(filter_re.match, dir_names) |
| latest_version = max(versions, key=distutils.version.LooseVersion) |
| except ValueError: |
| raise GSUtilError(msg) |
| |
| return latest_version |