1 # Copyright 2014 The Chromium 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.
5 """Wrappers for gsutil, for basic interaction with Google Cloud Storage."""
17 from telemetry.core import platform
18 from telemetry.util import path
21 PUBLIC_BUCKET = 'chromium-telemetry'
22 PARTNER_BUCKET = 'chrome-partner-telemetry'
23 INTERNAL_BUCKET = 'chrome-telemetry'
27 'public': PUBLIC_BUCKET,
28 'partner': PARTNER_BUCKET,
29 'internal': INTERNAL_BUCKET,
33 _GSUTIL_URL = 'http://storage.googleapis.com/pub/gsutil.tar.gz'
34 _DOWNLOAD_PATH = os.path.join(path.GetTelemetryDir(), 'third_party', 'gsutil')
35 # TODO(tbarzic): A workaround for http://crbug.com/386416 and
36 # http://crbug.com/359293. See |_RunCommand|.
37 _CROS_GSUTIL_HOME_WAR = '/home/chromeos-test/'
40 class CloudStorageError(Exception):
42 def _GetConfigInstructions(gsutil_path):
43 if SupportsProdaccess(gsutil_path) and _FindExecutableInPath('prodaccess'):
44 return 'Run prodaccess to authenticate.'
46 if platform.GetHostPlatform().GetOSName() == 'chromeos':
47 gsutil_path = ('HOME=%s %s' % (_CROS_GSUTIL_HOME_WAR, gsutil_path))
48 return ('To configure your credentials:\n'
49 ' 1. Run "%s config" and follow its instructions.\n'
50 ' 2. If you have a @google.com account, use that account.\n'
51 ' 3. For the project-id, just enter 0.' % gsutil_path)
54 class PermissionError(CloudStorageError):
55 def __init__(self, gsutil_path):
56 super(PermissionError, self).__init__(
57 'Attempted to access a file from Cloud Storage but you don\'t '
58 'have permission. ' + self._GetConfigInstructions(gsutil_path))
61 class CredentialsError(CloudStorageError):
62 def __init__(self, gsutil_path):
63 super(CredentialsError, self).__init__(
64 'Attempted to access a file from Cloud Storage but you have no '
65 'configured credentials. ' + self._GetConfigInstructions(gsutil_path))
68 class NotFoundError(CloudStorageError):
72 class ServerError(CloudStorageError):
76 # TODO(tonyg/dtu): Can this be replaced with distutils.spawn.find_executable()?
77 def _FindExecutableInPath(relative_executable_path, *extra_search_paths):
78 search_paths = list(extra_search_paths) + os.environ['PATH'].split(os.pathsep)
79 for search_path in search_paths:
80 executable_path = os.path.join(search_path, relative_executable_path)
81 if path.IsExecutable(executable_path):
82 return executable_path
86 def _DownloadGsutil():
87 logging.info('Downloading gsutil')
88 with contextlib.closing(urllib2.urlopen(_GSUTIL_URL, timeout=60)) as response:
89 with tarfile.open(fileobj=cStringIO.StringIO(response.read())) as tar_file:
90 tar_file.extractall(os.path.dirname(_DOWNLOAD_PATH))
91 logging.info('Downloaded gsutil to %s' % _DOWNLOAD_PATH)
93 return os.path.join(_DOWNLOAD_PATH, 'gsutil')
97 """Return the gsutil executable path. If we can't find it, download it."""
98 # Look for a depot_tools installation.
99 # FIXME: gsutil in depot_tools is not working correctly. crbug.com/413414
100 #gsutil_path = _FindExecutableInPath(
101 # os.path.join('third_party', 'gsutil', 'gsutil'), _DOWNLOAD_PATH)
105 # Look for a gsutil installation.
106 gsutil_path = _FindExecutableInPath('gsutil', _DOWNLOAD_PATH)
110 # Failed to find it. Download it!
111 return _DownloadGsutil()
114 def SupportsProdaccess(gsutil_path):
115 with open(gsutil_path, 'r') as gsutil:
116 return 'prodaccess' in gsutil.read()
119 def _RunCommand(args):
120 gsutil_path = FindGsutil()
122 # On cros device, as telemetry is running as root, home will be set to /root/,
123 # which is not writable. gsutil will attempt to create a download tracker dir
124 # in home dir and fail. To avoid this, override HOME dir to something writable
125 # when running on cros device.
127 # TODO(tbarzic): Figure out a better way to handle gsutil on cros.
128 # http://crbug.com/386416, http://crbug.com/359293.
130 if platform.GetHostPlatform().GetOSName() == 'chromeos':
131 gsutil_env = os.environ.copy()
132 gsutil_env['HOME'] = _CROS_GSUTIL_HOME_WAR
134 gsutil = subprocess.Popen([sys.executable, gsutil_path] + args,
135 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
137 stdout, stderr = gsutil.communicate()
139 if gsutil.returncode:
140 if stderr.startswith((
141 'You are attempting to access protected data with no configured',
142 'Failure: No handler was ready to authenticate.')):
143 raise CredentialsError(gsutil_path)
144 if 'status=403' in stderr or 'status 403' in stderr:
145 raise PermissionError(gsutil_path)
146 if (stderr.startswith('InvalidUriError') or 'No such object' in stderr or
147 'No URLs matched' in stderr):
148 raise NotFoundError(stderr)
149 if '500 Internal Server Error' in stderr:
150 raise ServerError(stderr)
151 raise CloudStorageError(stderr)
157 query = 'gs://%s/' % bucket
158 stdout = _RunCommand(['ls', query])
159 return [url[len(query):] for url in stdout.splitlines()]
162 def Exists(bucket, remote_path):
164 _RunCommand(['ls', 'gs://%s/%s' % (bucket, remote_path)])
166 except NotFoundError:
170 def Move(bucket1, bucket2, remote_path):
171 url1 = 'gs://%s/%s' % (bucket1, remote_path)
172 url2 = 'gs://%s/%s' % (bucket2, remote_path)
173 logging.info('Moving %s to %s' % (url1, url2))
174 _RunCommand(['mv', url1, url2])
177 def Delete(bucket, remote_path):
178 url = 'gs://%s/%s' % (bucket, remote_path)
179 logging.info('Deleting %s' % url)
180 _RunCommand(['rm', url])
183 def Get(bucket, remote_path, local_path):
184 url = 'gs://%s/%s' % (bucket, remote_path)
185 logging.info('Downloading %s to %s' % (url, local_path))
187 _RunCommand(['cp', url, local_path])
189 logging.info('Cloud Storage server error, retrying download')
190 _RunCommand(['cp', url, local_path])
193 def Insert(bucket, remote_path, local_path, publicly_readable=False):
194 url = 'gs://%s/%s' % (bucket, remote_path)
195 command_and_args = ['cp']
197 if publicly_readable:
198 command_and_args += ['-a', 'public-read']
199 extra_info = ' (publicly readable)'
200 command_and_args += [local_path, url]
201 logging.info('Uploading %s to %s%s' % (local_path, url, extra_info))
202 _RunCommand(command_and_args)
205 def GetIfChanged(file_path, bucket=None):
206 """Gets the file at file_path if it has a hash file that doesn't match.
208 If the file is not in Cloud Storage, log a warning instead of raising an
209 exception. We assume that the user just hasn't uploaded the file yet.
212 True if the binary was changed.
214 hash_path = file_path + '.sha1'
215 if not os.path.exists(hash_path):
216 logging.warning('Hash file not found: %s' % hash_path)
219 expected_hash = ReadHash(hash_path)
220 if os.path.exists(file_path) and CalculateHash(file_path) == expected_hash:
226 buckets = [PUBLIC_BUCKET, PARTNER_BUCKET, INTERNAL_BUCKET]
228 for bucket in buckets:
230 Get(bucket, expected_hash, file_path)
232 except NotFoundError:
235 logging.warning('Unable to find file in Cloud Storage: %s', file_path)
239 def CalculateHash(file_path):
240 """Calculates and returns the hash of the file at file_path."""
241 sha1 = hashlib.sha1()
242 with open(file_path, 'rb') as f:
244 # Read in 1mb chunks, so it doesn't all have to be loaded into memory.
245 chunk = f.read(1024*1024)
249 return sha1.hexdigest()
252 def ReadHash(hash_path):
253 with open(hash_path, 'rb') as f:
254 return f.read(1024).rstrip()