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."""
14 from telemetry
.core
import util
15 from telemetry
import decorators
16 from telemetry
.internal
.util
import path
19 PUBLIC_BUCKET
= 'chromium-telemetry'
20 PARTNER_BUCKET
= 'chrome-partner-telemetry'
21 INTERNAL_BUCKET
= 'chrome-telemetry'
24 # Uses ordered dict to make sure that bucket's key-value items are ordered from
25 # the most open to the most restrictive.
26 BUCKET_ALIASES
= collections
.OrderedDict((
27 ('public', PUBLIC_BUCKET
),
28 ('partner', PARTNER_BUCKET
),
29 ('internal', INTERNAL_BUCKET
),
33 _GSUTIL_PATH
= os
.path
.join(path
.GetTelemetryDir(), 'third_party', 'gsutilz',
36 # TODO(tbarzic): A workaround for http://crbug.com/386416 and
37 # http://crbug.com/359293. See |_RunCommand|.
38 _CROS_GSUTIL_HOME_WAR
= '/home/chromeos-test/'
41 class CloudStorageError(Exception):
43 def _GetConfigInstructions():
44 command
= _GSUTIL_PATH
45 if util
.IsRunningOnCrosDevice():
46 command
= 'HOME=%s %s' % (_CROS_GSUTIL_HOME_WAR
, _GSUTIL_PATH
)
47 return ('To configure your credentials:\n'
48 ' 1. Run "%s config" and follow its instructions.\n'
49 ' 2. If you have a @google.com account, use that account.\n'
50 ' 3. For the project-id, just enter 0.' % command
)
53 class PermissionError(CloudStorageError
):
55 super(PermissionError
, self
).__init
__(
56 'Attempted to access a file from Cloud Storage but you don\'t '
57 'have permission. ' + self
._GetConfigInstructions
())
60 class CredentialsError(CloudStorageError
):
62 super(CredentialsError
, self
).__init
__(
63 'Attempted to access a file from Cloud Storage but you have no '
64 'configured credentials. ' + self
._GetConfigInstructions
())
67 class NotFoundError(CloudStorageError
):
71 class ServerError(CloudStorageError
):
75 # TODO(tonyg/dtu): Can this be replaced with distutils.spawn.find_executable()?
76 def _FindExecutableInPath(relative_executable_path
, *extra_search_paths
):
77 search_paths
= list(extra_search_paths
) + os
.environ
['PATH'].split(os
.pathsep
)
78 for search_path
in search_paths
:
79 executable_path
= os
.path
.join(search_path
, relative_executable_path
)
80 if path
.IsExecutable(executable_path
):
81 return executable_path
85 def _RunCommand(args
):
86 # On cros device, as telemetry is running as root, home will be set to /root/,
87 # which is not writable. gsutil will attempt to create a download tracker dir
88 # in home dir and fail. To avoid this, override HOME dir to something writable
89 # when running on cros device.
91 # TODO(tbarzic): Figure out a better way to handle gsutil on cros.
92 # http://crbug.com/386416, http://crbug.com/359293.
94 if util
.IsRunningOnCrosDevice():
95 gsutil_env
= os
.environ
.copy()
96 gsutil_env
['HOME'] = _CROS_GSUTIL_HOME_WAR
99 # If Windows, prepend python. Python scripts aren't directly executable.
100 args
= [sys
.executable
, _GSUTIL_PATH
] + args
102 # Don't do it on POSIX, in case someone is using a shell script to redirect.
103 args
= [_GSUTIL_PATH
] + args
105 gsutil
= subprocess
.Popen(args
, stdout
=subprocess
.PIPE
,
106 stderr
=subprocess
.PIPE
, env
=gsutil_env
)
107 stdout
, stderr
= gsutil
.communicate()
109 if gsutil
.returncode
:
110 if stderr
.startswith((
111 'You are attempting to access protected data with no configured',
112 'Failure: No handler was ready to authenticate.')):
113 raise CredentialsError()
114 if ('status=403' in stderr
or 'status 403' in stderr
or
115 '403 Forbidden' in stderr
):
116 raise PermissionError()
117 if (stderr
.startswith('InvalidUriError') or 'No such object' in stderr
or
118 'No URLs matched' in stderr
or 'One or more URLs matched no' in stderr
):
119 raise NotFoundError(stderr
)
120 if '500 Internal Server Error' in stderr
:
121 raise ServerError(stderr
)
122 raise CloudStorageError(stderr
)
128 query
= 'gs://%s/' % bucket
129 stdout
= _RunCommand(['ls', query
])
130 return [url
[len(query
):] for url
in stdout
.splitlines()]
133 def Exists(bucket
, remote_path
):
135 _RunCommand(['ls', 'gs://%s/%s' % (bucket
, remote_path
)])
137 except NotFoundError
:
141 def Move(bucket1
, bucket2
, remote_path
):
142 url1
= 'gs://%s/%s' % (bucket1
, remote_path
)
143 url2
= 'gs://%s/%s' % (bucket2
, remote_path
)
144 logging
.info('Moving %s to %s' % (url1
, url2
))
145 _RunCommand(['mv', url1
, url2
])
148 def Copy(bucket_from
, bucket_to
, remote_path_from
, remote_path_to
):
149 """Copy a file from one location in CloudStorage to another.
152 bucket_from: The cloud storage bucket where the file is currently located.
153 bucket_to: The cloud storage bucket it is being copied to.
154 remote_path_from: The file path where the file is located in bucket_from.
155 remote_path_to: The file path it is being copied to in bucket_to.
157 It should: cause no changes locally or to the starting file, and will
158 overwrite any existing files in the destination location.
160 url1
= 'gs://%s/%s' % (bucket_from
, remote_path_from
)
161 url2
= 'gs://%s/%s' % (bucket_to
, remote_path_to
)
162 logging
.info('Copying %s to %s' % (url1
, url2
))
163 _RunCommand(['cp', url1
, url2
])
166 def Delete(bucket
, remote_path
):
167 url
= 'gs://%s/%s' % (bucket
, remote_path
)
168 logging
.info('Deleting %s' % url
)
169 _RunCommand(['rm', url
])
172 def Get(bucket
, remote_path
, local_path
):
173 url
= 'gs://%s/%s' % (bucket
, remote_path
)
174 logging
.info('Downloading %s to %s' % (url
, local_path
))
176 _RunCommand(['cp', url
, local_path
])
178 logging
.info('Cloud Storage server error, retrying download')
179 _RunCommand(['cp', url
, local_path
])
182 def Insert(bucket
, remote_path
, local_path
, publicly_readable
=False):
183 """ Upload file in |local_path| to cloud storage.
185 bucket: the google cloud storage bucket name.
186 remote_path: the remote file path in |bucket|.
187 local_path: path of the local file to be uploaded.
188 publicly_readable: whether the uploaded file has publicly readable
192 The url where the file is uploaded to.
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
)
203 return 'https://console.developers.google.com/m/cloudstorage/b/%s/o/%s' % (
207 def GetIfChanged(file_path
, bucket
):
208 """Gets the file at file_path if it has a hash file that doesn't match or
209 if there is no local copy of file_path, but there is a hash file for it.
212 True if the binary was changed.
214 CredentialsError if the user has no configured credentials.
215 PermissionError if the user does not have permission to access the bucket.
216 NotFoundError if the file is not in the given bucket in cloud_storage.
218 hash_path
= file_path
+ '.sha1'
219 if not os
.path
.exists(hash_path
):
220 logging
.warning('Hash file not found: %s' % hash_path
)
223 expected_hash
= ReadHash(hash_path
)
224 if os
.path
.exists(file_path
) and CalculateHash(file_path
) == expected_hash
:
227 Get(bucket
, expected_hash
, file_path
)
230 # TODO(aiolos): remove @decorators.Cache for http://crbug.com/459787
232 def GetFilesInDirectoryIfChanged(directory
, bucket
):
233 """ Scan the directory for .sha1 files, and download them from the given
234 bucket in cloud storage if the local and remote hash don't match or
235 there is no local copy.
237 if not os
.path
.isdir(directory
):
238 raise ValueError('Must provide a valid directory.')
239 # Don't allow the root directory to be a serving_dir.
240 if directory
== os
.path
.abspath(os
.sep
):
241 raise ValueError('Trying to serve root directory from HTTP server.')
242 for dirpath
, _
, filenames
in os
.walk(directory
):
243 for filename
in filenames
:
244 path_name
, extension
= os
.path
.splitext(
245 os
.path
.join(dirpath
, filename
))
246 if extension
!= '.sha1':
248 GetIfChanged(path_name
, bucket
)
250 def CalculateHash(file_path
):
251 """Calculates and returns the hash of the file at file_path."""
252 sha1
= hashlib
.sha1()
253 with
open(file_path
, 'rb') as f
:
255 # Read in 1mb chunks, so it doesn't all have to be loaded into memory.
256 chunk
= f
.read(1024*1024)
260 return sha1
.hexdigest()
263 def ReadHash(hash_path
):
264 with
open(hash_path
, 'rb') as f
:
265 return f
.read(1024).rstrip()