blob: 34e35e74c841633de60ca40cb416caa4a5c01230 [file] [log] [blame]
Chris Sosa47a7d4e2012-03-28 11:26:55 -07001# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
Frank Farzan37761d12011-12-01 14:29:08 -08002# 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-09 22:44:06 -07007import ast
Gilad Arnold55a2a372012-10-02 09:46:32 -07008import base64
9import binascii
Chris Sosa4b951602014-04-09 20:26:07 -070010import cherrypy
Frank Farzan37761d12011-12-01 14:29:08 -080011import distutils.version
12import errno
Gilad Arnold55a2a372012-10-02 09:46:32 -070013import hashlib
Frank Farzan37761d12011-12-01 14:29:08 -080014import os
15import shutil
Alex Deymo3e2d4952013-09-03 21:49:41 -070016import tempfile
Chris Sosa76e44b92013-01-31 12:11:38 -080017import threading
Simran Basi4baad082013-02-14 13:39:18 -080018import subprocess
Frank Farzan37761d12011-12-01 14:29:08 -080019
Gilad Arnoldc65330c2012-09-20 15:17:48 -070020import log_util
21
22
23# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080024def _Log(message, *args):
25 return log_util.LogWithTag('UTIL', message, *args)
Gilad Arnoldc65330c2012-09-20 15:17:48 -070026
Frank Farzan37761d12011-12-01 14:29:08 -080027
Gilad Arnold55a2a372012-10-02 09:46:32 -070028_HASH_BLOCK_SIZE = 8192
29
Gilad Arnold6f99b982012-09-12 10:49:40 -070030
Gilad Arnold17fe03d2012-10-02 10:05:01 -070031class CommonUtilError(Exception):
Frank Farzan37761d12011-12-01 14:29:08 -080032 """Exception classes used by this module."""
33 pass
34
35
Chris Sosa4b951602014-04-09 20:26:07 -070036class 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-09 20:45:23 -070041 Args:
42 status: HTTPResponse status.
43 message: Message associated with the response.
Chris Sosa4b951602014-04-09 20:26:07 -070044 """
45 cherrypy.HTTPError.__init__(self, status, message)
46 _Log('HTTPError status: %s message: %s', status, message)
47
48
Chris Sosa76e44b92013-01-31 12:11:38 -080049def MkDirP(directory):
Yiming Chen4e3741f2014-12-01 16:38:17 -080050 """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 14:29:08 -080056 try:
Chris Sosa76e44b92013-01-31 12:11:38 -080057 os.makedirs(directory)
Frank Farzan37761d12011-12-01 14:29:08 -080058 except OSError, e:
Yiming Chen4e3741f2014-12-01 16:38:17 -080059 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 14:29:08 -080067 raise
68
Frank Farzan37761d12011-12-01 14:29:08 -080069
Scott Zawalski16954532012-03-20 15:31:36 -040070def GetLatestBuildVersion(static_dir, target, milestone=None):
Frank Farzan37761d12011-12-01 14:29:08 -080071 """Retrieves the latest build version for a given board.
72
joychen921e1fb2013-06-28 11:12:20 -070073 Searches the static_dir for builds for target, and returns the highest
74 version number currently available locally.
75
Frank Farzan37761d12011-12-01 14:29:08 -080076 Args:
77 static_dir: Directory where builds are served from.
Scott Zawalski16954532012-03-20 15:31:36 -040078 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 14:29:08 -080082
83 Returns:
Scott Zawalski16954532012-03-20 15:31:36 -040084 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 14:29:08 -080086
87 Raises:
Gilad Arnold17fe03d2012-10-02 10:05:01 -070088 CommonUtilError: If for some reason the latest build cannot be
Scott Zawalski16954532012-03-20 15:31:36 -040089 deteremined, this could be due to the dir not existing or no builds
90 being present after filtering on milestone.
Frank Farzan37761d12011-12-01 14:29:08 -080091 """
Scott Zawalski16954532012-03-20 15:31:36 -040092 target_path = os.path.join(static_dir, target)
93 if not os.path.isdir(target_path):
Gilad Arnold17fe03d2012-10-02 10:05:01 -070094 raise CommonUtilError('Cannot find path %s' % target_path)
Frank Farzan37761d12011-12-01 14:29:08 -080095
Scott Zawalski16954532012-03-20 15:31:36 -040096 builds = [distutils.version.LooseVersion(build) for build in
Dan Shi9fa4bde2013-12-02 13:40:07 -080097 os.listdir(target_path) if not build.endswith('.exception')]
Frank Farzan37761d12011-12-01 14:29:08 -080098
Scott Zawalski16954532012-03-20 15:31:36 -040099 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 14:29:08 -0800102
Scott Zawalski16954532012-03-20 15:31:36 -0400103 if not builds:
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700104 raise CommonUtilError('Could not determine build for %s' % target)
Frank Farzan37761d12011-12-01 14:29:08 -0800105
Scott Zawalski16954532012-03-20 15:31:36 -0400106 return str(max(builds))
Frank Farzan37761d12011-12-01 14:29:08 -0800107
108
Chris Sosa76e44b92013-01-31 12:11:38 -0800109def 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 15:12:42 -0500124def GetControlFile(static_dir, build, control_path):
Frank Farzan37761d12011-12-01 14:29:08 -0800125 """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 14:29:08 -0800129 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 14:29:08 -0800132 Returns:
133 Content of the requested control file.
Chris Sosafc715442014-04-09 20:45:23 -0700134
135 Raises:
136 CommonUtilError: If lock can't be acquired.
Frank Farzan37761d12011-12-01 14:29:08 -0800137 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500138 # Be forgiving if the user passes in the control_path with a leading /
139 control_path = control_path.lstrip('/')
Scott Zawalski84a39c92012-01-13 15:12:42 -0500140 control_path = os.path.join(static_dir, build, 'autotest',
Scott Zawalski4647ce62012-01-03 17:17:28 -0500141 control_path)
Chris Sosa76e44b92013-01-31 12:11:38 -0800142 if not PathInDir(static_dir, control_path):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700143 raise CommonUtilError('Invalid control file "%s".' % control_path)
Frank Farzan37761d12011-12-01 14:29:08 -0800144
Scott Zawalski84a39c92012-01-13 15:12:42 -0500145 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 14:29:08 -0800150 with open(control_path, 'r') as control_file:
151 return control_file.read()
152
153
beepsbd337242013-07-09 22:44:06 -0700154def 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-09 20:45:23 -0700166 Returns:
167 String of each control file separated by a newline.
168
beepsbd337242013-07-09 22:44:06 -0700169 Raises:
170 CommonUtilError: If the suite_to_control_file_map isn't found in
171 the specified build's staged directory.
beepsbd337242013-07-09 22:44:06 -0700172 """
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 15:12:42 -0500192def GetControlFileList(static_dir, build):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500193 """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 17:17:28 -0500197 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
198
Scott Zawalski4647ce62012-01-03 17:17:28 -0500199 Returns:
200 String of each file separated by a newline.
Chris Sosafc715442014-04-09 20:45:23 -0700201
202 Raises:
203 CommonUtilError: If path is outside of sandbox.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500204 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500205 autotest_dir = os.path.join(static_dir, build, 'autotest/')
Chris Sosa76e44b92013-01-31 12:11:38 -0800206 if not PathInDir(static_dir, autotest_dir):
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700207 raise CommonUtilError('Autotest dir not in sandbox "%s".' % autotest_dir)
Scott Zawalski4647ce62012-01-03 17:17:28 -0500208
209 control_files = set()
Scott Zawalski84a39c92012-01-13 15:12:42 -0500210 if not os.path.exists(autotest_dir):
joychen3d164bd2013-06-24 18:12:23 -0700211 raise CommonUtilError('Could not find this directory.'
212 'Is it staged? %s' % autotest_dir)
Scott Zawalski84a39c92012-01-13 15:12:42 -0500213
Scott Zawalski4647ce62012-01-03 17:17:28 -0500214 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-06 16:22:04 -0800219 file_entry).replace(autotest_dir, ''))
Scott Zawalski4647ce62012-01-03 17:17:28 -0500220
221 return '\n'.join(control_files)
222
223
Gilad Arnold55a2a372012-10-02 09:46:32 -0700224def 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-29 16:44:43 -0800229# 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 09:46:32 -0700232def 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-09 20:45:23 -0700237 do_sha1: whether or not to compute a SHA1 hash
Gilad Arnold55a2a372012-10-02 09:46:32 -0700238 do_sha256: whether or not to compute a SHA256 hash
Chris Sosafc715442014-04-09 20:45:23 -0700239 do_md5: whether or not to compute a MD5 hash
240
Gilad Arnold55a2a372012-10-02 09:46:32 -0700241 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 12:11:38 -0800292
293
Alex Deymo3e2d4952013-09-03 21:49:41 -0700294def 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-09-30 17:21:45 -0700301 _Log('Could not find target for symlink: %s', target)
Alex Deymo3e2d4952013-09-03 21:49:41 -0700302 return
Chris Sosa75490802013-09-30 17:21:45 -0700303
Alex Deymo3e2d4952013-09-03 21:49:41 -0700304 _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 12:11:38 -0800323class 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 13:39:18 -0800349
350
351def ExtractTarball(tarball_path, install_path, files_to_extract=None,
Gilad Arnold1638d822013-11-07 23:38:16 -0800352 excluded_files=None, return_extracted_files=False):
Simran Basi4baad082013-02-14 13:39:18 -0800353 """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-09 20:45:23 -0700363 return_extracted_files: whether or not the caller expects the list of
Gilad Arnold1638d822013-11-07 23:38:16 -0800364 files extracted; if False, returns an empty list.
Chris Sosafc715442014-04-09 20:45:23 -0700365
Gilad Arnold1638d822013-11-07 23:38:16 -0800366 Returns:
367 List of absolute paths of the files extracted (possibly empty).
Simran Basi4baad082013-02-14 13:39:18 -0800368 """
369 # Deal with exclusions.
370 cmd = ['tar', 'xf', tarball_path, '--directory', install_path]
371
Gilad Arnold1638d822013-11-07 23:38:16 -0800372 # If caller requires the list of extracted files, get verbose.
373 if return_extracted_files:
374 cmd += ['--verbose']
375
Simran Basi4baad082013-02-14 13:39:18 -0800376 # 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
Aseda Aboagyeb6ba5492016-10-14 13:20:09 -0700390 cmd_output = ''
Simran Basi4baad082013-02-14 13:39:18 -0800391 try:
Aseda Aboagyeb6ba5492016-10-14 13:20:09 -0700392 cmd_output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
Gilad Arnold1638d822013-11-07 23:38:16 -0800393 if return_extracted_files:
394 return [os.path.join(install_path, filename)
395 for filename in cmd_output.strip('\n').splitlines()
396 if not filename.endswith('/')]
397 return []
Simran Basi4baad082013-02-14 13:39:18 -0800398 except subprocess.CalledProcessError, e:
399 raise CommonUtilError(
Aseda Aboagyeb6ba5492016-10-14 13:20:09 -0700400 'An error occurred when attempting to untar %s:\n%s\n%s' %
401 (tarball_path, e, e.output))
joychen7c2054a2013-07-25 11:14:07 -0700402
403
404def IsInsideChroot():
405 """Returns True if we are inside chroot."""
406 return os.path.exists('/etc/debian_chroot')