blob: 0cf7937b913649f12373201f06c28cd963a5de63 [file] [log] [blame]
Chris Sosa47a7d4e2012-03-28 18:26:551# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
Frank Farzan37761d12011-12-01 22:29:082# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Helper class for interacting with the Dev Server."""
6
beepsbd337242013-07-10 05:44:067import ast
Gilad Arnold55a2a372012-10-02 16:46:328import base64
9import binascii
Chris Sosa4b951602014-04-10 03:26:0710import cherrypy
Frank Farzan37761d12011-12-01 22:29:0811import distutils.version
12import errno
Gilad Arnold55a2a372012-10-02 16:46:3213import hashlib
Frank Farzan37761d12011-12-01 22:29:0814import os
15import shutil
Alex Deymo3e2d4952013-09-04 04:49:4116import tempfile
Chris Sosa76e44b92013-01-31 20:11:3817import threading
Simran Basi4baad082013-02-14 21:39:1818import subprocess
Frank Farzan37761d12011-12-01 22:29:0819
Gilad Arnoldc65330c2012-09-20 22:17:4820import log_util
21
22
23# Module-local log function.
Chris Sosa6a3697f2013-01-30 00:44:4324def _Log(message, *args):
25 return log_util.LogWithTag('UTIL', message, *args)
Gilad Arnoldc65330c2012-09-20 22:17:4826
Frank Farzan37761d12011-12-01 22:29:0827
Gilad Arnold55a2a372012-10-02 16:46:3228_HASH_BLOCK_SIZE = 8192
29
Gilad Arnold6f99b982012-09-12 17:49:4030
Gilad Arnold17fe03d2012-10-02 17:05:0131class CommonUtilError(Exception):
Frank Farzan37761d12011-12-01 22:29:0832 """Exception classes used by this module."""
33 pass
34
35
Chris Sosa4b951602014-04-10 03:26:0736class DevServerHTTPError(cherrypy.HTTPError):
37 """Exception class to log the HTTPResponse before routing it to cherrypy."""
38 def __init__(self, status, message):
39 """CherryPy error with logging.
40
Chris Sosafc715442014-04-10 03:45:2341 Args:
42 status: HTTPResponse status.
43 message: Message associated with the response.
Chris Sosa4b951602014-04-10 03:26:0744 """
45 cherrypy.HTTPError.__init__(self, status, message)
46 _Log('HTTPError status: %s message: %s', status, message)
47
48
Chris Sosa76e44b92013-01-31 20:11:3849def MkDirP(directory):
Yiming Chen4e3741f2014-12-02 00:38:1750 """Thread-safely create a directory like mkdir -p.
51
52 If the directory already exists, call chown on the directory and its subfiles
53 recursively with current user and group to make sure current process has full
54 access to the directory.
55 """
Frank Farzan37761d12011-12-01 22:29:0856 try:
Chris Sosa76e44b92013-01-31 20:11:3857 os.makedirs(directory)
Frank Farzan37761d12011-12-01 22:29:0858 except OSError, e:
Yiming Chen4e3741f2014-12-02 00:38:1759 if e.errno == errno.EEXIST and os.path.isdir(directory):
60 # Fix permissions and ownership of the directory and its subfiles by
61 # calling chown recursively with current user and group.
62 chown_command = [
63 'sudo', 'chown', '-R', '%s:%s' % (os.getuid(), os.getgid()), directory
64 ]
65 subprocess.Popen(chown_command).wait()
66 else:
Frank Farzan37761d12011-12-01 22:29:0867 raise
68
Frank Farzan37761d12011-12-01 22:29:0869
Scott Zawalski16954532012-03-20 19:31:3670def GetLatestBuildVersion(static_dir, target, milestone=None):
Frank Farzan37761d12011-12-01 22:29:0871 """Retrieves the latest build version for a given board.
72
joychen921e1fb2013-06-28 18:12:2073 Searches the static_dir for builds for target, and returns the highest
74 version number currently available locally.
75
Frank Farzan37761d12011-12-01 22:29:0876 Args:
77 static_dir: Directory where builds are served from.
Scott Zawalski16954532012-03-20 19:31:3678 target: The build target, typically a combination of the board and the
79 type of build e.g. x86-mario-release.
80 milestone: For latest build set to None, for builds only in a specific
81 milestone set to a str of format Rxx (e.g. R16). Default: None.
Frank Farzan37761d12011-12-01 22:29:0882
83 Returns:
Scott Zawalski16954532012-03-20 19:31:3684 If latest found, a full build string is returned e.g. R17-1234.0.0-a1-b983.
85 If no latest is found for some reason or another a '' string is returned.
Frank Farzan37761d12011-12-01 22:29:0886
87 Raises:
Gilad Arnold17fe03d2012-10-02 17:05:0188 CommonUtilError: If for some reason the latest build cannot be
Scott Zawalski16954532012-03-20 19:31:3689 deteremined, this could be due to the dir not existing or no builds
90 being present after filtering on milestone.
Frank Farzan37761d12011-12-01 22:29:0891 """
Scott Zawalski16954532012-03-20 19:31:3692 target_path = os.path.join(static_dir, target)
93 if not os.path.isdir(target_path):
Gilad Arnold17fe03d2012-10-02 17:05:0194 raise CommonUtilError('Cannot find path %s' % target_path)
Frank Farzan37761d12011-12-01 22:29:0895
Scott Zawalski16954532012-03-20 19:31:3696 builds = [distutils.version.LooseVersion(build) for build in
Dan Shi9fa4bde2013-12-02 21:40:0797 os.listdir(target_path) if not build.endswith('.exception')]
Frank Farzan37761d12011-12-01 22:29:0898
Scott Zawalski16954532012-03-20 19:31:3699 if milestone and builds:
100 # Check if milestone Rxx is in the string representation of the build.
101 builds = filter(lambda x: milestone.upper() in str(x), builds)
Frank Farzan37761d12011-12-01 22:29:08102
Scott Zawalski16954532012-03-20 19:31:36103 if not builds:
Gilad Arnold17fe03d2012-10-02 17:05:01104 raise CommonUtilError('Could not determine build for %s' % target)
Frank Farzan37761d12011-12-01 22:29:08105
Scott Zawalski16954532012-03-20 19:31:36106 return str(max(builds))
Frank Farzan37761d12011-12-01 22:29:08107
108
Chris Sosa76e44b92013-01-31 20:11:38109def PathInDir(directory, path):
110 """Returns True if the path is in directory.
111
112 Args:
113 directory: Directory where the path should be in.
114 path: Path to check.
115
116 Returns:
117 True if path is in static_dir, False otherwise
118 """
119 directory = os.path.realpath(directory)
120 path = os.path.realpath(path)
121 return (path.startswith(directory) and len(path) != len(directory))
122
123
Scott Zawalski84a39c92012-01-13 20:12:42124def GetControlFile(static_dir, build, control_path):
Frank Farzan37761d12011-12-01 22:29:08125 """Attempts to pull the requested control file from the Dev Server.
126
127 Args:
128 static_dir: Directory where builds are served from.
Frank Farzan37761d12011-12-01 22:29:08129 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
130 control_path: Path to control file on Dev Server relative to Autotest root.
131
Frank Farzan37761d12011-12-01 22:29:08132 Returns:
133 Content of the requested control file.
Chris Sosafc715442014-04-10 03:45:23134
135 Raises:
136 CommonUtilError: If lock can't be acquired.
Frank Farzan37761d12011-12-01 22:29:08137 """
Scott Zawalski1572d152012-01-16 19:36:02138 # Be forgiving if the user passes in the control_path with a leading /
139 control_path = control_path.lstrip('/')
Scott Zawalski84a39c92012-01-13 20:12:42140 control_path = os.path.join(static_dir, build, 'autotest',
Scott Zawalski4647ce62012-01-03 22:17:28141 control_path)
Chris Sosa76e44b92013-01-31 20:11:38142 if not PathInDir(static_dir, control_path):
Gilad Arnold55a2a372012-10-02 16:46:32143 raise CommonUtilError('Invalid control file "%s".' % control_path)
Frank Farzan37761d12011-12-01 22:29:08144
Scott Zawalski84a39c92012-01-13 20:12:42145 if not os.path.exists(control_path):
146 # TODO(scottz): Come up with some sort of error mechanism.
147 # crosbug.com/25040
148 return 'Unknown control path %s' % control_path
149
Frank Farzan37761d12011-12-01 22:29:08150 with open(control_path, 'r') as control_file:
151 return control_file.read()
152
153
beepsbd337242013-07-10 05:44:06154def GetControlFileListForSuite(static_dir, build, suite_name):
155 """List all control files for a specified build, for the given suite.
156
157 If the specified suite_name isn't found in the suite to control file
158 map, this method will return all control files for the build by calling
159 GetControlFileList.
160
161 Args:
162 static_dir: Directory where builds are served from.
163 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
164 suite_name: Name of the suite for which we require control files.
165
Chris Sosafc715442014-04-10 03:45:23166 Returns:
167 String of each control file separated by a newline.
168
beepsbd337242013-07-10 05:44:06169 Raises:
170 CommonUtilError: If the suite_to_control_file_map isn't found in
171 the specified build's staged directory.
beepsbd337242013-07-10 05:44:06172 """
173 suite_to_control_map = os.path.join(static_dir, build,
174 'autotest', 'test_suites',
175 'suite_to_control_file_map')
176
177 if not PathInDir(static_dir, suite_to_control_map):
178 raise CommonUtilError('suite_to_control_map not in "%s".' %
179 suite_to_control_map)
180
181 if not os.path.exists(suite_to_control_map):
182 raise CommonUtilError('Could not find this file. '
183 'Is it staged? %s' % suite_to_control_map)
184
185 with open(suite_to_control_map, 'r') as fd:
186 try:
187 return '\n'.join(ast.literal_eval(fd.read())[suite_name])
188 except KeyError:
189 return GetControlFileList(static_dir, build)
190
191
Scott Zawalski84a39c92012-01-13 20:12:42192def GetControlFileList(static_dir, build):
Scott Zawalski4647ce62012-01-03 22:17:28193 """List all control|control. files in the specified board/build path.
194
195 Args:
196 static_dir: Directory where builds are served from.
Scott Zawalski4647ce62012-01-03 22:17:28197 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
198
Scott Zawalski4647ce62012-01-03 22:17:28199 Returns:
200 String of each file separated by a newline.
Chris Sosafc715442014-04-10 03:45:23201
202 Raises:
203 CommonUtilError: If path is outside of sandbox.
Scott Zawalski4647ce62012-01-03 22:17:28204 """
Scott Zawalski1572d152012-01-16 19:36:02205 autotest_dir = os.path.join(static_dir, build, 'autotest/')
Chris Sosa76e44b92013-01-31 20:11:38206 if not PathInDir(static_dir, autotest_dir):
Gilad Arnold17fe03d2012-10-02 17:05:01207 raise CommonUtilError('Autotest dir not in sandbox "%s".' % autotest_dir)
Scott Zawalski4647ce62012-01-03 22:17:28208
209 control_files = set()
Scott Zawalski84a39c92012-01-13 20:12:42210 if not os.path.exists(autotest_dir):
joychen3d164bd2013-06-25 01:12:23211 raise CommonUtilError('Could not find this directory.'
212 'Is it staged? %s' % autotest_dir)
Scott Zawalski84a39c92012-01-13 20:12:42213
Scott Zawalski4647ce62012-01-03 22:17:28214 for entry in os.walk(autotest_dir):
215 dir_path, _, files = entry
216 for file_entry in files:
217 if file_entry.startswith('control.') or file_entry == 'control':
218 control_files.add(os.path.join(dir_path,
Chris Sosaea148d92012-03-07 00:22:04219 file_entry).replace(autotest_dir, ''))
Scott Zawalski4647ce62012-01-03 22:17:28220
221 return '\n'.join(control_files)
222
223
Gilad Arnold55a2a372012-10-02 16:46:32224def GetFileSize(file_path):
225 """Returns the size in bytes of the file given."""
226 return os.path.getsize(file_path)
227
228
Chris Sosa6a3697f2013-01-30 00:44:43229# Hashlib is strange and doesn't actually define these in a sane way that
230# pylint can find them. Disable checks for them.
231# pylint: disable=E1101,W0106
Gilad Arnold55a2a372012-10-02 16:46:32232def GetFileHashes(file_path, do_sha1=False, do_sha256=False, do_md5=False):
233 """Computes and returns a list of requested hashes.
234
235 Args:
236 file_path: path to file to be hashed
Chris Sosafc715442014-04-10 03:45:23237 do_sha1: whether or not to compute a SHA1 hash
Gilad Arnold55a2a372012-10-02 16:46:32238 do_sha256: whether or not to compute a SHA256 hash
Chris Sosafc715442014-04-10 03:45:23239 do_md5: whether or not to compute a MD5 hash
240
Gilad Arnold55a2a372012-10-02 16:46:32241 Returns:
242 A dictionary containing binary hash values, keyed by 'sha1', 'sha256' and
243 'md5', respectively.
244 """
245 hashes = {}
246 if (do_sha1 or do_sha256 or do_md5):
247 # Initialize hashers.
248 hasher_sha1 = hashlib.sha1() if do_sha1 else None
249 hasher_sha256 = hashlib.sha256() if do_sha256 else None
250 hasher_md5 = hashlib.md5() if do_md5 else None
251
252 # Read blocks from file, update hashes.
253 with open(file_path, 'rb') as fd:
254 while True:
255 block = fd.read(_HASH_BLOCK_SIZE)
256 if not block:
257 break
258 hasher_sha1 and hasher_sha1.update(block)
259 hasher_sha256 and hasher_sha256.update(block)
260 hasher_md5 and hasher_md5.update(block)
261
262 # Update return values.
263 if hasher_sha1:
264 hashes['sha1'] = hasher_sha1.digest()
265 if hasher_sha256:
266 hashes['sha256'] = hasher_sha256.digest()
267 if hasher_md5:
268 hashes['md5'] = hasher_md5.digest()
269
270 return hashes
271
272
273def GetFileSha1(file_path):
274 """Returns the SHA1 checksum of the file given (base64 encoded)."""
275 return base64.b64encode(GetFileHashes(file_path, do_sha1=True)['sha1'])
276
277
278def GetFileSha256(file_path):
279 """Returns the SHA256 checksum of the file given (base64 encoded)."""
280 return base64.b64encode(GetFileHashes(file_path, do_sha256=True)['sha256'])
281
282
283def GetFileMd5(file_path):
284 """Returns the MD5 checksum of the file given (hex encoded)."""
285 return binascii.hexlify(GetFileHashes(file_path, do_md5=True)['md5'])
286
287
288def CopyFile(source, dest):
289 """Copies a file from |source| to |dest|."""
290 _Log('Copy File %s -> %s' % (source, dest))
291 shutil.copy(source, dest)
Chris Sosa76e44b92013-01-31 20:11:38292
293
Alex Deymo3e2d4952013-09-04 04:49:41294def SymlinkFile(target, link):
295 """Atomically creates or replaces the symlink |link| pointing to |target|.
296
297 If the specified |link| file already exists it is replaced with the new link
298 atomically.
299 """
300 if not os.path.exists(target):
Chris Sosa75490802013-10-01 00:21:45301 _Log('Could not find target for symlink: %s', target)
Alex Deymo3e2d4952013-09-04 04:49:41302 return
Chris Sosa75490802013-10-01 00:21:45303
Alex Deymo3e2d4952013-09-04 04:49:41304 _Log('Creating symlink: %s --> %s', link, target)
305
306 # Use the created link_base file to prevent other calls to SymlinkFile() to
307 # pick the same link_base temp file, thanks to mkstemp().
308 with tempfile.NamedTemporaryFile(prefix=os.path.basename(link)) as link_fd:
309 link_base = link_fd.name
310
311 # Use the unique link_base filename to create a symlink, but on the same
312 # directory as the required |link| to ensure the created symlink is in the
313 # same file system as |link|.
314 link_name = os.path.join(os.path.dirname(link),
315 os.path.basename(link_base) + "-link")
316
317 # Create the symlink and then rename it to the final position. This ensures
318 # the symlink creation is atomic.
319 os.symlink(target, link_name)
320 os.rename(link_name, link)
321
322
Chris Sosa76e44b92013-01-31 20:11:38323class LockDict(object):
324 """A dictionary of locks.
325
326 This class provides a thread-safe store of threading.Lock objects, which can
327 be used to regulate access to any set of hashable resources. Usage:
328
329 foo_lock_dict = LockDict()
330 ...
331 with foo_lock_dict.lock('bar'):
332 # Critical section for 'bar'
333 """
334 def __init__(self):
335 self._lock = self._new_lock()
336 self._dict = {}
337
338 @staticmethod
339 def _new_lock():
340 return threading.Lock()
341
342 def lock(self, key):
343 with self._lock:
344 lock = self._dict.get(key)
345 if not lock:
346 lock = self._new_lock()
347 self._dict[key] = lock
348 return lock
Simran Basi4baad082013-02-14 21:39:18349
350
351def ExtractTarball(tarball_path, install_path, files_to_extract=None,
Gilad Arnold1638d822013-11-08 07:38:16352 excluded_files=None, return_extracted_files=False):
Simran Basi4baad082013-02-14 21:39:18353 """Extracts a tarball using tar.
354
355 Detects whether the tarball is compressed or not based on the file
356 extension and extracts the tarball into the install_path.
357
358 Args:
359 tarball_path: Path to the tarball to extract.
360 install_path: Path to extract the tarball to.
361 files_to_extract: String of specific files in the tarball to extract.
362 excluded_files: String of files to not extract.
Chris Sosafc715442014-04-10 03:45:23363 return_extracted_files: whether or not the caller expects the list of
Gilad Arnold1638d822013-11-08 07:38:16364 files extracted; if False, returns an empty list.
Chris Sosafc715442014-04-10 03:45:23365
Gilad Arnold1638d822013-11-08 07:38:16366 Returns:
367 List of absolute paths of the files extracted (possibly empty).
Simran Basi4baad082013-02-14 21:39:18368 """
369 # Deal with exclusions.
370 cmd = ['tar', 'xf', tarball_path, '--directory', install_path]
371
Gilad Arnold1638d822013-11-08 07:38:16372 # If caller requires the list of extracted files, get verbose.
373 if return_extracted_files:
374 cmd += ['--verbose']
375
Simran Basi4baad082013-02-14 21:39:18376 # Determine how to decompress.
377 tarball = os.path.basename(tarball_path)
378 if tarball.endswith('.tar.bz2'):
379 cmd.append('--use-compress-prog=pbzip2')
380 elif tarball.endswith('.tgz') or tarball.endswith('.tar.gz'):
381 cmd.append('--gzip')
382
383 if excluded_files:
384 for exclude in excluded_files:
385 cmd.extend(['--exclude', exclude])
386
387 if files_to_extract:
388 cmd.extend(files_to_extract)
389
390 try:
Gilad Arnold1638d822013-11-08 07:38:16391 cmd_output = subprocess.check_output(cmd)
392 if return_extracted_files:
393 return [os.path.join(install_path, filename)
394 for filename in cmd_output.strip('\n').splitlines()
395 if not filename.endswith('/')]
396 return []
Simran Basi4baad082013-02-14 21:39:18397 except subprocess.CalledProcessError, e:
398 raise CommonUtilError(
399 'An error occurred when attempting to untar %s:\n%s' %
joychen3d164bd2013-06-25 01:12:23400 (tarball_path, e))
joychen7c2054a2013-07-25 18:14:07401
402
403def IsInsideChroot():
404 """Returns True if we are inside chroot."""
405 return os.path.exists('/etc/debian_chroot')