blob: bf69c106df9a1a9e9d09e5e3f83b508cd3bae2ef [file] [log] [blame]
Chris Sosa47a7d4e2012-03-28 11:26:55 -07001# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Module containing gsutil helper methods."""
6
joychenf8f07e22013-07-12 17:45:51 -07007import distutils.version
Gilad Arnold950569b2013-08-27 14:38:01 -07008import fnmatch
9import os
Chris Sosa76e44b92013-01-31 12:11:38 -080010import random
11import re
Chris Sosa47a7d4e2012-03-28 11:26:55 -070012import subprocess
Chris Sosa101fd862012-06-12 17:44:53 -070013import time
Chris Sosa47a7d4e2012-03-28 11:26:55 -070014
joychenf8f07e22013-07-12 17:45:51 -070015import devserver_constants
Chris Sosa76e44b92013-01-31 12:11:38 -080016import log_util
17
Gilad Arnoldabb352e2012-09-23 01:24:27 -070018
Chris Sosa7cd23202013-10-15 17:22:57 -070019GSUTIL_ATTEMPTS = 1
Chris Sosa76e44b92013-01-31 12:11:38 -080020UPLOADED_LIST = 'UPLOADED'
21
22
23# Module-local log function.
24def _Log(message, *args):
25 return log_util.LogWithTag('GSUTIL_UTIL', message, *args)
Chris Sosa47a7d4e2012-03-28 11:26:55 -070026
27
28class GSUtilError(Exception):
Chris Sosa76e44b92013-01-31 12:11:38 -080029 """Exception raised when we run into an error running gsutil."""
30 pass
31
32
33class PatternNotSpecific(Exception):
34 """Raised when unexpectedly more than one item is returned for a pattern."""
Chris Sosa47a7d4e2012-03-28 11:26:55 -070035 pass
36
37
38def GSUtilRun(cmd, err_msg):
39 """Runs a GSUTIL command up to GSUTIL_ATTEMPTS number of times.
40
Chris Sosa101fd862012-06-12 17:44:53 -070041 Attempts are tried with exponential backoff.
42
Gilad Arnold950569b2013-08-27 14:38:01 -070043 Args:
44 cmd: a string containing the gsutil command to run.
45 err_msg: string prepended to the exception thrown in case of a failure.
Chris Sosa47a7d4e2012-03-28 11:26:55 -070046 Returns:
47 stdout of the called gsutil command.
48 Raises:
Gilad Arnold950569b2013-08-27 14:38:01 -070049 GSUtilError: if all attempts to run gsutil have failed.
Chris Sosa47a7d4e2012-03-28 11:26:55 -070050 """
51 proc = None
Chris Sosa101fd862012-06-12 17:44:53 -070052 sleep_timeout = 1
Chris Sosa407e8c52013-02-27 15:33:35 -080053 stderr = None
Gilad Arnold950569b2013-08-27 14:38:01 -070054 for _ in range(GSUTIL_ATTEMPTS):
Chris Sosa407e8c52013-02-27 15:33:35 -080055 proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,
56 stderr=subprocess.PIPE)
57 stdout, stderr = proc.communicate()
Chris Sosa47a7d4e2012-03-28 11:26:55 -070058 if proc.returncode == 0:
59 return stdout
Chris Sosa6de29302013-03-14 15:27:36 -070060 elif stderr and ('matched no objects' in stderr or
61 'non-existent object' in stderr):
Chris Sosa407e8c52013-02-27 15:33:35 -080062 # TODO(sosa): Note this is a heuristic that makes us not re-attempt
63 # unnecessarily. However, if it fails, the worst that can happen is just
64 # waiting longer than necessary.
65 break
Chris Sosa6de29302013-03-14 15:27:36 -070066 elif proc.returncode == 127:
67 raise GSUtilError('gsutil tool not found in your path.')
Chris Sosa101fd862012-06-12 17:44:53 -070068
69 time.sleep(sleep_timeout)
70 sleep_timeout *= 2
71
Chris Sosa407e8c52013-02-27 15:33:35 -080072 raise GSUtilError('%s GSUTIL cmd %s failed with return code %d:\n\n%s' % (
73 err_msg, cmd, proc.returncode, stderr))
Chris Sosa47a7d4e2012-03-28 11:26:55 -070074
75
76def DownloadFromGS(src, dst):
77 """Downloads object from gs_url |src| to |dst|.
78
Gilad Arnold950569b2013-08-27 14:38:01 -070079 Args:
80 src: source file on GS that needs to be downloaded.
81 dst: file to copy the source file to.
Chris Sosa47a7d4e2012-03-28 11:26:55 -070082 Raises:
83 GSUtilError: if an error occurs during the download.
84 """
85 cmd = 'gsutil cp %s %s' % (src, dst)
86 msg = 'Failed to download "%s".' % src
87 GSUtilRun(cmd, msg)
Chris Sosa76e44b92013-01-31 12:11:38 -080088
89
Gilad Arnold950569b2013-08-27 14:38:01 -070090def _GlobHasWildcards(pattern):
91 """Returns True if a glob pattern contains any wildcards."""
Gilad Arnoldc703c8c2013-08-29 10:41:27 -070092 return len(pattern) > len(pattern.translate(None, '*?[]'))
Chris Sosa76e44b92013-01-31 12:11:38 -080093
94
Gilad Arnold950569b2013-08-27 14:38:01 -070095def GetGSNamesWithWait(pattern, archive_url, err_str, timeout=600, delay=10,
96 is_regex_pattern=False):
Chris Sosa76e44b92013-01-31 12:11:38 -080097 """Returns the google storage names specified by the given pattern.
98
99 This method polls Google Storage until the target artifacts specified by the
100 pattern is available or until the timeout occurs. Because we may not know the
101 exact name of the target artifacts, the method accepts a filename pattern,
102 to identify whether an artifact whose name matches the pattern exists (e.g.
Gilad Arnold950569b2013-08-27 14:38:01 -0700103 use pattern '*_full_*' to search for the full payload
Chris Sosa76e44b92013-01-31 12:11:38 -0800104 'chromeos_R17-1413.0.0-a1_x86-mario_full_dev.bin'). Returns the name only if
105 found before the timeout.
106
107 Args:
Gilad Arnold950569b2013-08-27 14:38:01 -0700108 pattern: a path pattern (glob or regex) identifying the files we need.
Chris Sosa76e44b92013-01-31 12:11:38 -0800109 archive_url: URL of the Google Storage bucket.
110 err_str: String to display in the error message on error.
Gilad Arnold950569b2013-08-27 14:38:01 -0700111 timeout: how long are we allowed to keep trying.
112 delay: how long to wait between attempts.
Chris Sosa6317e9a2013-09-06 16:43:44 -0700113 is_regex_pattern: Whether the pattern is a regex (otherwise a glob).
Chris Sosa76e44b92013-01-31 12:11:38 -0800114 Returns:
115 The list of artifacts matching the pattern in Google Storage bucket or None
Gilad Arnold950569b2013-08-27 14:38:01 -0700116 if not found.
Chris Sosa76e44b92013-01-31 12:11:38 -0800117
Chris Sosa76e44b92013-01-31 12:11:38 -0800118 """
Gilad Arnold950569b2013-08-27 14:38:01 -0700119 # Define the different methods used for obtaining the list of files on the
Gilad Arnoldc703c8c2013-08-29 10:41:27 -0700120 # archive directory, in the order in which they are attempted. Each method is
121 # defined by a tuple consisting of (i) the gsutil command-line to be
122 # executed; (ii) the error message to use in case of a failure (returned in
123 # the corresponding exception); (iii) the desired return value to use in case
124 # of success, or None if the actual command output should be used.
Gilad Arnold950569b2013-08-27 14:38:01 -0700125 get_methods = []
Gilad Arnoldc703c8c2013-08-29 10:41:27 -0700126 # If the pattern is a glob and contains no wildcards, we'll first attempt to
Chris Sosa6317e9a2013-09-06 16:43:44 -0700127 # stat the file via du.
Gilad Arnoldc703c8c2013-08-29 10:41:27 -0700128 if not (is_regex_pattern or _GlobHasWildcards(pattern)):
Yu-Ju Hongc853b6e2014-11-17 15:04:55 -0800129 get_methods.append(('gsutil stat %s/%s' % (archive_url, pattern),
130 'Failed to stat on the artifact file.', pattern))
Gilad Arnoldc703c8c2013-08-29 10:41:27 -0700131
Gilad Arnold950569b2013-08-27 14:38:01 -0700132 # The default method is to check the manifest file in the archive directory.
133 get_methods.append(('gsutil cat %s/%s' % (archive_url, UPLOADED_LIST),
Gilad Arnoldc703c8c2013-08-29 10:41:27 -0700134 'Failed to get a list of uploaded files.',
135 None))
Gilad Arnold950569b2013-08-27 14:38:01 -0700136 # For backward compatibility, we fall back to using "gsutil ls" when the
137 # manifest file is not present.
138 get_methods.append(('gsutil ls %s/*' % archive_url,
Gilad Arnoldc703c8c2013-08-29 10:41:27 -0700139 'Failed to list archive directory contents.',
140 None))
Gilad Arnold950569b2013-08-27 14:38:01 -0700141
Chris Sosa76e44b92013-01-31 12:11:38 -0800142 deadline = time.time() + timeout
Chris Sosa6de29302013-03-14 15:27:36 -0700143 while True:
Chris Sosa76e44b92013-01-31 12:11:38 -0800144 uploaded_list = []
Gilad Arnoldc703c8c2013-08-29 10:41:27 -0700145 for cmd, msg, override_result in get_methods:
Gilad Arnold950569b2013-08-27 14:38:01 -0700146 try:
147 result = GSUtilRun(cmd, msg)
148 except GSUtilError:
149 continue # It didn't work, try the next method.
Chris Sosa76e44b92013-01-31 12:11:38 -0800150
Gilad Arnoldc703c8c2013-08-29 10:41:27 -0700151 if override_result:
152 result = override_result
153
Gilad Arnold950569b2013-08-27 14:38:01 -0700154 # Make sure we're dealing with artifact base names only.
155 uploaded_list = [os.path.basename(p) for p in result.splitlines()]
156 break
Chris Sosa76e44b92013-01-31 12:11:38 -0800157
Gilad Arnold950569b2013-08-27 14:38:01 -0700158 # Only keep files matching the target artifact name/pattern.
159 if is_regex_pattern:
160 filter_re = re.compile(pattern)
161 matching_names = [f for f in uploaded_list
162 if filter_re.search(f) is not None]
163 else:
164 matching_names = fnmatch.filter(uploaded_list, pattern)
165
166 if matching_names:
167 return matching_names
Chris Sosa76e44b92013-01-31 12:11:38 -0800168
Chris Sosa6de29302013-03-14 15:27:36 -0700169 # Don't delay past deadline.
170 to_delay = random.uniform(1.5 * delay, 2.5 * delay)
171 if to_delay < (deadline - time.time()):
172 _Log('Retrying in %f seconds...%s', to_delay, err_str)
173 time.sleep(to_delay)
174 else:
175 return None
joychenf8f07e22013-07-12 17:45:51 -0700176
177
joychen562699a2013-08-13 15:22:14 -0700178def GetLatestVersionFromGSDir(gsutil_dir, with_release=True):
joychenf8f07e22013-07-12 17:45:51 -0700179 """Returns most recent version number found in a GS directory.
180
181 This lists out the contents of the given GS bucket or regex to GS buckets,
182 and tries to grab the newest version found in the directory names.
Gilad Arnold950569b2013-08-27 14:38:01 -0700183
184 Args:
185 gsutil_dir: directory location on GS to check.
186 with_release: whether versions include a release milestone (e.g. R12).
187 Returns:
188 The most recent version number found.
189
joychenf8f07e22013-07-12 17:45:51 -0700190 """
191 cmd = 'gsutil ls %s' % gsutil_dir
192 msg = 'Failed to find most recent builds at %s' % gsutil_dir
193 dir_names = [p.split('/')[-2] for p in GSUtilRun(cmd, msg).splitlines()]
194 try:
Gilad Arnold950569b2013-08-27 14:38:01 -0700195 filter_re = re.compile(devserver_constants.VERSION_RE if with_release
196 else devserver_constants.VERSION)
197 versions = filter(filter_re.match, dir_names)
joychenf8f07e22013-07-12 17:45:51 -0700198 latest_version = max(versions, key=distutils.version.LooseVersion)
199 except ValueError:
200 raise GSUtilError(msg)
201
202 return latest_version