blob: fe66deea6f36a818341f3939f764d83410ada029 [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 Sosa47a7d4e2012-03-28 11:26:55 -070019GSUTIL_ATTEMPTS = 5
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)):
Chris Sosa6317e9a2013-09-06 16:43:44 -0700129 get_methods.append(('gsutil du %s/%s' % (archive_url, pattern),
130 'Failed to du on the artifact file.',
Gilad Arnoldc703c8c2013-08-29 10:41:27 -0700131 pattern))
132
Gilad Arnold950569b2013-08-27 14:38:01 -0700133 # The default method is to check the manifest file in the archive directory.
134 get_methods.append(('gsutil cat %s/%s' % (archive_url, UPLOADED_LIST),
Gilad Arnoldc703c8c2013-08-29 10:41:27 -0700135 'Failed to get a list of uploaded files.',
136 None))
Gilad Arnold950569b2013-08-27 14:38:01 -0700137 # For backward compatibility, we fall back to using "gsutil ls" when the
138 # manifest file is not present.
139 get_methods.append(('gsutil ls %s/*' % archive_url,
Gilad Arnoldc703c8c2013-08-29 10:41:27 -0700140 'Failed to list archive directory contents.',
141 None))
Gilad Arnold950569b2013-08-27 14:38:01 -0700142
Chris Sosa76e44b92013-01-31 12:11:38 -0800143 deadline = time.time() + timeout
Chris Sosa6de29302013-03-14 15:27:36 -0700144 while True:
Chris Sosa76e44b92013-01-31 12:11:38 -0800145 uploaded_list = []
Gilad Arnoldc703c8c2013-08-29 10:41:27 -0700146 for cmd, msg, override_result in get_methods:
Gilad Arnold950569b2013-08-27 14:38:01 -0700147 try:
148 result = GSUtilRun(cmd, msg)
149 except GSUtilError:
150 continue # It didn't work, try the next method.
Chris Sosa76e44b92013-01-31 12:11:38 -0800151
Gilad Arnoldc703c8c2013-08-29 10:41:27 -0700152 if override_result:
153 result = override_result
154
Gilad Arnold950569b2013-08-27 14:38:01 -0700155 # Make sure we're dealing with artifact base names only.
156 uploaded_list = [os.path.basename(p) for p in result.splitlines()]
157 break
Chris Sosa76e44b92013-01-31 12:11:38 -0800158
Gilad Arnold950569b2013-08-27 14:38:01 -0700159 # Only keep files matching the target artifact name/pattern.
160 if is_regex_pattern:
161 filter_re = re.compile(pattern)
162 matching_names = [f for f in uploaded_list
163 if filter_re.search(f) is not None]
164 else:
165 matching_names = fnmatch.filter(uploaded_list, pattern)
166
167 if matching_names:
168 return matching_names
Chris Sosa76e44b92013-01-31 12:11:38 -0800169
Chris Sosa6de29302013-03-14 15:27:36 -0700170 # Don't delay past deadline.
171 to_delay = random.uniform(1.5 * delay, 2.5 * delay)
172 if to_delay < (deadline - time.time()):
173 _Log('Retrying in %f seconds...%s', to_delay, err_str)
174 time.sleep(to_delay)
175 else:
176 return None
joychenf8f07e22013-07-12 17:45:51 -0700177
178
joychen562699a2013-08-13 15:22:14 -0700179def GetLatestVersionFromGSDir(gsutil_dir, with_release=True):
joychenf8f07e22013-07-12 17:45:51 -0700180 """Returns most recent version number found in a GS directory.
181
182 This lists out the contents of the given GS bucket or regex to GS buckets,
183 and tries to grab the newest version found in the directory names.
Gilad Arnold950569b2013-08-27 14:38:01 -0700184
185 Args:
186 gsutil_dir: directory location on GS to check.
187 with_release: whether versions include a release milestone (e.g. R12).
188 Returns:
189 The most recent version number found.
190
joychenf8f07e22013-07-12 17:45:51 -0700191 """
192 cmd = 'gsutil ls %s' % gsutil_dir
193 msg = 'Failed to find most recent builds at %s' % gsutil_dir
194 dir_names = [p.split('/')[-2] for p in GSUtilRun(cmd, msg).splitlines()]
195 try:
Gilad Arnold950569b2013-08-27 14:38:01 -0700196 filter_re = re.compile(devserver_constants.VERSION_RE if with_release
197 else devserver_constants.VERSION)
198 versions = filter(filter_re.match, dir_names)
joychenf8f07e22013-07-12 17:45:51 -0700199 latest_version = max(versions, key=distutils.version.LooseVersion)
200 except ValueError:
201 raise GSUtilError(msg)
202
203 return latest_version