blob: a6e2324e88006c43572aa499f236d9ef8016193b [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
Chris Sosab26b1202013-08-16 16:40:55 -07008import logging
Chris Sosa76e44b92013-01-31 12:11:38 -08009import random
10import re
Chris Sosa47a7d4e2012-03-28 11:26:55 -070011import subprocess
Chris Sosa101fd862012-06-12 17:44:53 -070012import time
Chris Sosa47a7d4e2012-03-28 11:26:55 -070013
joychenf8f07e22013-07-12 17:45:51 -070014import devserver_constants
Chris Sosa76e44b92013-01-31 12:11:38 -080015import log_util
16
Gilad Arnoldabb352e2012-09-23 01:24:27 -070017
Chris Sosa47a7d4e2012-03-28 11:26:55 -070018GSUTIL_ATTEMPTS = 5
Chris Sosa76e44b92013-01-31 12:11:38 -080019UPLOADED_LIST = 'UPLOADED'
20
21
22# Module-local log function.
23def _Log(message, *args):
24 return log_util.LogWithTag('GSUTIL_UTIL', message, *args)
Chris Sosa47a7d4e2012-03-28 11:26:55 -070025
26
27class GSUtilError(Exception):
Chris Sosa76e44b92013-01-31 12:11:38 -080028 """Exception raised when we run into an error running gsutil."""
29 pass
30
31
32class PatternNotSpecific(Exception):
33 """Raised when unexpectedly more than one item is returned for a pattern."""
Chris Sosa47a7d4e2012-03-28 11:26:55 -070034 pass
35
36
37def GSUtilRun(cmd, err_msg):
38 """Runs a GSUTIL command up to GSUTIL_ATTEMPTS number of times.
39
Chris Sosa101fd862012-06-12 17:44:53 -070040 Attempts are tried with exponential backoff.
41
Chris Sosa47a7d4e2012-03-28 11:26:55 -070042 Returns:
43 stdout of the called gsutil command.
44 Raises:
45 subprocess.CalledProcessError if all attempt to run gsutil cmd fails.
46 """
47 proc = None
Chris Sosa101fd862012-06-12 17:44:53 -070048 sleep_timeout = 1
Chris Sosa407e8c52013-02-27 15:33:35 -080049 stderr = None
Chris Sosa47a7d4e2012-03-28 11:26:55 -070050 for _attempt in range(GSUTIL_ATTEMPTS):
Chris Sosa407e8c52013-02-27 15:33:35 -080051 proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,
52 stderr=subprocess.PIPE)
53 stdout, stderr = proc.communicate()
Chris Sosa47a7d4e2012-03-28 11:26:55 -070054 if proc.returncode == 0:
55 return stdout
Chris Sosa6de29302013-03-14 15:27:36 -070056 elif stderr and ('matched no objects' in stderr or
57 'non-existent object' in stderr):
Chris Sosa407e8c52013-02-27 15:33:35 -080058 # TODO(sosa): Note this is a heuristic that makes us not re-attempt
59 # unnecessarily. However, if it fails, the worst that can happen is just
60 # waiting longer than necessary.
61 break
Chris Sosa6de29302013-03-14 15:27:36 -070062 elif proc.returncode == 127:
63 raise GSUtilError('gsutil tool not found in your path.')
Chris Sosa101fd862012-06-12 17:44:53 -070064
65 time.sleep(sleep_timeout)
66 sleep_timeout *= 2
67
Chris Sosa407e8c52013-02-27 15:33:35 -080068 raise GSUtilError('%s GSUTIL cmd %s failed with return code %d:\n\n%s' % (
69 err_msg, cmd, proc.returncode, stderr))
Chris Sosa47a7d4e2012-03-28 11:26:55 -070070
71
72def DownloadFromGS(src, dst):
73 """Downloads object from gs_url |src| to |dst|.
74
75 Raises:
76 GSUtilError: if an error occurs during the download.
77 """
78 cmd = 'gsutil cp %s %s' % (src, dst)
79 msg = 'Failed to download "%s".' % src
80 GSUtilRun(cmd, msg)
Chris Sosa76e44b92013-01-31 12:11:38 -080081
82
83def _GetGSNamesFromList(filename_list, pattern):
84 """Given a list of filenames, returns the filenames that match pattern."""
85 matches = []
86 re_pattern = re.compile(pattern)
87 for filename in filename_list:
88 if re_pattern.match(filename):
89 matches.append(filename)
90
91 return matches
92
93
94def GetGSNamesWithWait(pattern, archive_url, err_str, single_item=True,
95 timeout=600, delay=10):
96 """Returns the google storage names specified by the given pattern.
97
98 This method polls Google Storage until the target artifacts specified by the
99 pattern is available or until the timeout occurs. Because we may not know the
100 exact name of the target artifacts, the method accepts a filename pattern,
101 to identify whether an artifact whose name matches the pattern exists (e.g.
102 use pattern '_full_' to search for the full payload
103 'chromeos_R17-1413.0.0-a1_x86-mario_full_dev.bin'). Returns the name only if
104 found before the timeout.
105
106 Args:
107 pattern: Regular expression pattern to identify the target artifact.
108 archive_url: URL of the Google Storage bucket.
109 err_str: String to display in the error message on error.
Chris Sosab26b1202013-08-16 16:40:55 -0700110 single_item: Only a single item should be returned. If more than one item
111 matches the pattern errors out unless pattern matches one
112 exactly.
Chris Sosa76e44b92013-01-31 12:11:38 -0800113 timeout/delay: optional and self-explanatory.
114
115 Returns:
116 The list of artifacts matching the pattern in Google Storage bucket or None
117 if not found.
118
119 Raises:
120 PatternNotSpecific: If caller sets single_item but multiple items match.
121 """
122 deadline = time.time() + timeout
Chris Sosa6de29302013-03-14 15:27:36 -0700123 while True:
Chris Sosa76e44b92013-01-31 12:11:38 -0800124 uploaded_list = []
Chris Sosa76e44b92013-01-31 12:11:38 -0800125 try:
126 cmd = 'gsutil cat %s/%s' % (archive_url, UPLOADED_LIST)
127 msg = 'Failed to get a list of uploaded files.'
128 uploaded_list = GSUtilRun(cmd, msg).splitlines()
129 except GSUtilError:
130 # For backward compatibility, falling back to use "gsutil ls"
131 # when the manifest file is not present.
132 cmd = 'gsutil ls %s/*' % archive_url
133 msg = 'Failed to list payloads.'
134 returned_list = GSUtilRun(cmd, msg).splitlines()
135 for item in returned_list:
beepsc3d0f872013-07-31 21:50:40 -0700136 try:
137 uploaded_list.append(item.rsplit('/', 1)[1])
138 except IndexError:
139 pass
Chris Sosa76e44b92013-01-31 12:11:38 -0800140
141 # Check if all target artifacts are available.
142 found_names = _GetGSNamesFromList(uploaded_list, pattern)
143 if found_names:
144 if single_item and len(found_names) > 1:
Chris Sosab26b1202013-08-16 16:40:55 -0700145 found_names_exact = _GetGSNamesFromList(uploaded_list, '^%s$' % pattern)
146 if not found_names_exact:
147 raise PatternNotSpecific(
Chris Sosa76e44b92013-01-31 12:11:38 -0800148 'Too many items %s returned by pattern %s in %s' % (
149 str(found_names), pattern, archive_url))
Chris Sosab26b1202013-08-16 16:40:55 -0700150 else:
151 logging.info('More than one item returned but one file matched'
152 ' exactly so returning that: %s.', found_names_exact)
153 found_names = found_names_exact
Chris Sosa76e44b92013-01-31 12:11:38 -0800154
155 return found_names
156
Chris Sosa6de29302013-03-14 15:27:36 -0700157 # Don't delay past deadline.
158 to_delay = random.uniform(1.5 * delay, 2.5 * delay)
159 if to_delay < (deadline - time.time()):
160 _Log('Retrying in %f seconds...%s', to_delay, err_str)
161 time.sleep(to_delay)
162 else:
163 return None
joychenf8f07e22013-07-12 17:45:51 -0700164
165
166def GetLatestVersionFromGSDir(gsutil_dir):
167 """Returns most recent version number found in a GS directory.
168
169 This lists out the contents of the given GS bucket or regex to GS buckets,
170 and tries to grab the newest version found in the directory names.
171 """
172 cmd = 'gsutil ls %s' % gsutil_dir
173 msg = 'Failed to find most recent builds at %s' % gsutil_dir
174 dir_names = [p.split('/')[-2] for p in GSUtilRun(cmd, msg).splitlines()]
175 try:
176 versions = filter(lambda x: re.match(devserver_constants.VERSION_RE, x),
177 dir_names)
178 latest_version = max(versions, key=distutils.version.LooseVersion)
179 except ValueError:
180 raise GSUtilError(msg)
181
182 return latest_version