blob: a606d9d797c7a6e3dc694392f209dc8c6220d90b [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
xixuan8a132922016-11-01 09:51:11 -07007from __future__ import print_function
8
beepsbd337242013-07-09 22:44:06 -07009import ast
Gilad Arnold55a2a372012-10-02 09:46:32 -070010import base64
11import binascii
Chris Sosa4b951602014-04-09 20:26:07 -070012import cherrypy
Frank Farzan37761d12011-12-01 14:29:08 -080013import distutils.version
14import errno
Gilad Arnold55a2a372012-10-02 09:46:32 -070015import hashlib
Frank Farzan37761d12011-12-01 14:29:08 -080016import os
17import shutil
Alex Deymo3e2d4952013-09-03 21:49:41 -070018import tempfile
Chris Sosa76e44b92013-01-31 12:11:38 -080019import threading
Simran Basi4baad082013-02-14 13:39:18 -080020import subprocess
Frank Farzan37761d12011-12-01 14:29:08 -080021
Gilad Arnoldc65330c2012-09-20 15:17:48 -070022import log_util
23
24
25# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080026def _Log(message, *args):
27 return log_util.LogWithTag('UTIL', message, *args)
Gilad Arnoldc65330c2012-09-20 15:17:48 -070028
Frank Farzan37761d12011-12-01 14:29:08 -080029
Gilad Arnold55a2a372012-10-02 09:46:32 -070030_HASH_BLOCK_SIZE = 8192
31
Gilad Arnold6f99b982012-09-12 10:49:40 -070032
Gilad Arnold17fe03d2012-10-02 10:05:01 -070033class CommonUtilError(Exception):
Frank Farzan37761d12011-12-01 14:29:08 -080034 """Exception classes used by this module."""
35 pass
36
37
Chris Sosa4b951602014-04-09 20:26:07 -070038class DevServerHTTPError(cherrypy.HTTPError):
39 """Exception class to log the HTTPResponse before routing it to cherrypy."""
40 def __init__(self, status, message):
41 """CherryPy error with logging.
42
Chris Sosafc715442014-04-09 20:45:23 -070043 Args:
44 status: HTTPResponse status.
45 message: Message associated with the response.
Chris Sosa4b951602014-04-09 20:26:07 -070046 """
47 cherrypy.HTTPError.__init__(self, status, message)
48 _Log('HTTPError status: %s message: %s', status, message)
49
50
Chris Sosa76e44b92013-01-31 12:11:38 -080051def MkDirP(directory):
Yiming Chen4e3741f2014-12-01 16:38:17 -080052 """Thread-safely create a directory like mkdir -p.
53
54 If the directory already exists, call chown on the directory and its subfiles
55 recursively with current user and group to make sure current process has full
56 access to the directory.
57 """
Frank Farzan37761d12011-12-01 14:29:08 -080058 try:
Chris Sosa76e44b92013-01-31 12:11:38 -080059 os.makedirs(directory)
Frank Farzan37761d12011-12-01 14:29:08 -080060 except OSError, e:
Yiming Chen4e3741f2014-12-01 16:38:17 -080061 if e.errno == errno.EEXIST and os.path.isdir(directory):
62 # Fix permissions and ownership of the directory and its subfiles by
63 # calling chown recursively with current user and group.
64 chown_command = [
65 'sudo', 'chown', '-R', '%s:%s' % (os.getuid(), os.getgid()), directory
66 ]
67 subprocess.Popen(chown_command).wait()
68 else:
Frank Farzan37761d12011-12-01 14:29:08 -080069 raise
70
Frank Farzan37761d12011-12-01 14:29:08 -080071
Scott Zawalski16954532012-03-20 15:31:36 -040072def GetLatestBuildVersion(static_dir, target, milestone=None):
Frank Farzan37761d12011-12-01 14:29:08 -080073 """Retrieves the latest build version for a given board.
74
joychen921e1fb2013-06-28 11:12:20 -070075 Searches the static_dir for builds for target, and returns the highest
76 version number currently available locally.
77
Frank Farzan37761d12011-12-01 14:29:08 -080078 Args:
79 static_dir: Directory where builds are served from.
Scott Zawalski16954532012-03-20 15:31:36 -040080 target: The build target, typically a combination of the board and the
81 type of build e.g. x86-mario-release.
82 milestone: For latest build set to None, for builds only in a specific
83 milestone set to a str of format Rxx (e.g. R16). Default: None.
Frank Farzan37761d12011-12-01 14:29:08 -080084
85 Returns:
Scott Zawalski16954532012-03-20 15:31:36 -040086 If latest found, a full build string is returned e.g. R17-1234.0.0-a1-b983.
87 If no latest is found for some reason or another a '' string is returned.
Frank Farzan37761d12011-12-01 14:29:08 -080088
89 Raises:
Gilad Arnold17fe03d2012-10-02 10:05:01 -070090 CommonUtilError: If for some reason the latest build cannot be
Scott Zawalski16954532012-03-20 15:31:36 -040091 deteremined, this could be due to the dir not existing or no builds
92 being present after filtering on milestone.
Frank Farzan37761d12011-12-01 14:29:08 -080093 """
Scott Zawalski16954532012-03-20 15:31:36 -040094 target_path = os.path.join(static_dir, target)
95 if not os.path.isdir(target_path):
Gilad Arnold17fe03d2012-10-02 10:05:01 -070096 raise CommonUtilError('Cannot find path %s' % target_path)
Frank Farzan37761d12011-12-01 14:29:08 -080097
Scott Zawalski16954532012-03-20 15:31:36 -040098 builds = [distutils.version.LooseVersion(build) for build in
Dan Shi9fa4bde2013-12-02 13:40:07 -080099 os.listdir(target_path) if not build.endswith('.exception')]
Frank Farzan37761d12011-12-01 14:29:08 -0800100
Scott Zawalski16954532012-03-20 15:31:36 -0400101 if milestone and builds:
102 # Check if milestone Rxx is in the string representation of the build.
103 builds = filter(lambda x: milestone.upper() in str(x), builds)
Frank Farzan37761d12011-12-01 14:29:08 -0800104
Scott Zawalski16954532012-03-20 15:31:36 -0400105 if not builds:
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700106 raise CommonUtilError('Could not determine build for %s' % target)
Frank Farzan37761d12011-12-01 14:29:08 -0800107
Scott Zawalski16954532012-03-20 15:31:36 -0400108 return str(max(builds))
Frank Farzan37761d12011-12-01 14:29:08 -0800109
110
Chris Sosa76e44b92013-01-31 12:11:38 -0800111def PathInDir(directory, path):
112 """Returns True if the path is in directory.
113
114 Args:
115 directory: Directory where the path should be in.
116 path: Path to check.
117
118 Returns:
119 True if path is in static_dir, False otherwise
120 """
121 directory = os.path.realpath(directory)
122 path = os.path.realpath(path)
xixuan8a132922016-11-01 09:51:11 -0700123 return path.startswith(directory) and len(path) != len(directory)
Chris Sosa76e44b92013-01-31 12:11:38 -0800124
125
Scott Zawalski84a39c92012-01-13 15:12:42 -0500126def GetControlFile(static_dir, build, control_path):
Frank Farzan37761d12011-12-01 14:29:08 -0800127 """Attempts to pull the requested control file from the Dev Server.
128
129 Args:
130 static_dir: Directory where builds are served from.
Frank Farzan37761d12011-12-01 14:29:08 -0800131 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
132 control_path: Path to control file on Dev Server relative to Autotest root.
133
Frank Farzan37761d12011-12-01 14:29:08 -0800134 Returns:
135 Content of the requested control file.
Chris Sosafc715442014-04-09 20:45:23 -0700136
137 Raises:
138 CommonUtilError: If lock can't be acquired.
Frank Farzan37761d12011-12-01 14:29:08 -0800139 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500140 # Be forgiving if the user passes in the control_path with a leading /
141 control_path = control_path.lstrip('/')
Scott Zawalski84a39c92012-01-13 15:12:42 -0500142 control_path = os.path.join(static_dir, build, 'autotest',
Scott Zawalski4647ce62012-01-03 17:17:28 -0500143 control_path)
Chris Sosa76e44b92013-01-31 12:11:38 -0800144 if not PathInDir(static_dir, control_path):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700145 raise CommonUtilError('Invalid control file "%s".' % control_path)
Frank Farzan37761d12011-12-01 14:29:08 -0800146
Scott Zawalski84a39c92012-01-13 15:12:42 -0500147 if not os.path.exists(control_path):
148 # TODO(scottz): Come up with some sort of error mechanism.
149 # crosbug.com/25040
150 return 'Unknown control path %s' % control_path
151
Frank Farzan37761d12011-12-01 14:29:08 -0800152 with open(control_path, 'r') as control_file:
153 return control_file.read()
154
155
beepsbd337242013-07-09 22:44:06 -0700156def GetControlFileListForSuite(static_dir, build, suite_name):
157 """List all control files for a specified build, for the given suite.
158
159 If the specified suite_name isn't found in the suite to control file
160 map, this method will return all control files for the build by calling
161 GetControlFileList.
162
163 Args:
164 static_dir: Directory where builds are served from.
165 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
166 suite_name: Name of the suite for which we require control files.
167
Chris Sosafc715442014-04-09 20:45:23 -0700168 Returns:
169 String of each control file separated by a newline.
170
beepsbd337242013-07-09 22:44:06 -0700171 Raises:
172 CommonUtilError: If the suite_to_control_file_map isn't found in
173 the specified build's staged directory.
beepsbd337242013-07-09 22:44:06 -0700174 """
175 suite_to_control_map = os.path.join(static_dir, build,
176 'autotest', 'test_suites',
177 'suite_to_control_file_map')
178
179 if not PathInDir(static_dir, suite_to_control_map):
180 raise CommonUtilError('suite_to_control_map not in "%s".' %
181 suite_to_control_map)
182
183 if not os.path.exists(suite_to_control_map):
184 raise CommonUtilError('Could not find this file. '
185 'Is it staged? %s' % suite_to_control_map)
186
187 with open(suite_to_control_map, 'r') as fd:
188 try:
189 return '\n'.join(ast.literal_eval(fd.read())[suite_name])
190 except KeyError:
191 return GetControlFileList(static_dir, build)
192
193
Scott Zawalski84a39c92012-01-13 15:12:42 -0500194def GetControlFileList(static_dir, build):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500195 """List all control|control. files in the specified board/build path.
196
197 Args:
198 static_dir: Directory where builds are served from.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500199 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
200
Scott Zawalski4647ce62012-01-03 17:17:28 -0500201 Returns:
202 String of each file separated by a newline.
Chris Sosafc715442014-04-09 20:45:23 -0700203
204 Raises:
205 CommonUtilError: If path is outside of sandbox.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500206 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500207 autotest_dir = os.path.join(static_dir, build, 'autotest/')
Chris Sosa76e44b92013-01-31 12:11:38 -0800208 if not PathInDir(static_dir, autotest_dir):
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700209 raise CommonUtilError('Autotest dir not in sandbox "%s".' % autotest_dir)
Scott Zawalski4647ce62012-01-03 17:17:28 -0500210
211 control_files = set()
Scott Zawalski84a39c92012-01-13 15:12:42 -0500212 if not os.path.exists(autotest_dir):
joychen3d164bd2013-06-24 18:12:23 -0700213 raise CommonUtilError('Could not find this directory.'
214 'Is it staged? %s' % autotest_dir)
Scott Zawalski84a39c92012-01-13 15:12:42 -0500215
Scott Zawalski4647ce62012-01-03 17:17:28 -0500216 for entry in os.walk(autotest_dir):
217 dir_path, _, files = entry
218 for file_entry in files:
219 if file_entry.startswith('control.') or file_entry == 'control':
220 control_files.add(os.path.join(dir_path,
Chris Sosaea148d92012-03-06 16:22:04 -0800221 file_entry).replace(autotest_dir, ''))
Scott Zawalski4647ce62012-01-03 17:17:28 -0500222
223 return '\n'.join(control_files)
224
225
Gilad Arnold55a2a372012-10-02 09:46:32 -0700226def GetFileSize(file_path):
227 """Returns the size in bytes of the file given."""
228 return os.path.getsize(file_path)
229
230
Chris Sosa6a3697f2013-01-29 16:44:43 -0800231# Hashlib is strange and doesn't actually define these in a sane way that
232# pylint can find them. Disable checks for them.
233# pylint: disable=E1101,W0106
Gilad Arnold55a2a372012-10-02 09:46:32 -0700234def GetFileHashes(file_path, do_sha1=False, do_sha256=False, do_md5=False):
235 """Computes and returns a list of requested hashes.
236
237 Args:
238 file_path: path to file to be hashed
Chris Sosafc715442014-04-09 20:45:23 -0700239 do_sha1: whether or not to compute a SHA1 hash
Gilad Arnold55a2a372012-10-02 09:46:32 -0700240 do_sha256: whether or not to compute a SHA256 hash
Chris Sosafc715442014-04-09 20:45:23 -0700241 do_md5: whether or not to compute a MD5 hash
242
Gilad Arnold55a2a372012-10-02 09:46:32 -0700243 Returns:
244 A dictionary containing binary hash values, keyed by 'sha1', 'sha256' and
245 'md5', respectively.
246 """
247 hashes = {}
xixuan8a132922016-11-01 09:51:11 -0700248 if any((do_sha1, do_sha256, do_md5)):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700249 # Initialize hashers.
250 hasher_sha1 = hashlib.sha1() if do_sha1 else None
251 hasher_sha256 = hashlib.sha256() if do_sha256 else None
252 hasher_md5 = hashlib.md5() if do_md5 else None
253
254 # Read blocks from file, update hashes.
255 with open(file_path, 'rb') as fd:
256 while True:
257 block = fd.read(_HASH_BLOCK_SIZE)
258 if not block:
259 break
260 hasher_sha1 and hasher_sha1.update(block)
261 hasher_sha256 and hasher_sha256.update(block)
262 hasher_md5 and hasher_md5.update(block)
263
264 # Update return values.
265 if hasher_sha1:
266 hashes['sha1'] = hasher_sha1.digest()
267 if hasher_sha256:
268 hashes['sha256'] = hasher_sha256.digest()
269 if hasher_md5:
270 hashes['md5'] = hasher_md5.digest()
271
272 return hashes
273
274
275def GetFileSha1(file_path):
276 """Returns the SHA1 checksum of the file given (base64 encoded)."""
277 return base64.b64encode(GetFileHashes(file_path, do_sha1=True)['sha1'])
278
279
280def GetFileSha256(file_path):
281 """Returns the SHA256 checksum of the file given (base64 encoded)."""
282 return base64.b64encode(GetFileHashes(file_path, do_sha256=True)['sha256'])
283
284
285def GetFileMd5(file_path):
286 """Returns the MD5 checksum of the file given (hex encoded)."""
287 return binascii.hexlify(GetFileHashes(file_path, do_md5=True)['md5'])
288
289
290def CopyFile(source, dest):
291 """Copies a file from |source| to |dest|."""
292 _Log('Copy File %s -> %s' % (source, dest))
293 shutil.copy(source, dest)
Chris Sosa76e44b92013-01-31 12:11:38 -0800294
295
Alex Deymo3e2d4952013-09-03 21:49:41 -0700296def SymlinkFile(target, link):
297 """Atomically creates or replaces the symlink |link| pointing to |target|.
298
299 If the specified |link| file already exists it is replaced with the new link
300 atomically.
301 """
302 if not os.path.exists(target):
Chris Sosa75490802013-09-30 17:21:45 -0700303 _Log('Could not find target for symlink: %s', target)
Alex Deymo3e2d4952013-09-03 21:49:41 -0700304 return
Chris Sosa75490802013-09-30 17:21:45 -0700305
Alex Deymo3e2d4952013-09-03 21:49:41 -0700306 _Log('Creating symlink: %s --> %s', link, target)
307
308 # Use the created link_base file to prevent other calls to SymlinkFile() to
309 # pick the same link_base temp file, thanks to mkstemp().
310 with tempfile.NamedTemporaryFile(prefix=os.path.basename(link)) as link_fd:
311 link_base = link_fd.name
312
313 # Use the unique link_base filename to create a symlink, but on the same
314 # directory as the required |link| to ensure the created symlink is in the
315 # same file system as |link|.
316 link_name = os.path.join(os.path.dirname(link),
317 os.path.basename(link_base) + "-link")
318
319 # Create the symlink and then rename it to the final position. This ensures
320 # the symlink creation is atomic.
321 os.symlink(target, link_name)
322 os.rename(link_name, link)
323
324
Chris Sosa76e44b92013-01-31 12:11:38 -0800325class LockDict(object):
326 """A dictionary of locks.
327
328 This class provides a thread-safe store of threading.Lock objects, which can
329 be used to regulate access to any set of hashable resources. Usage:
330
331 foo_lock_dict = LockDict()
332 ...
333 with foo_lock_dict.lock('bar'):
334 # Critical section for 'bar'
335 """
336 def __init__(self):
337 self._lock = self._new_lock()
338 self._dict = {}
339
340 @staticmethod
341 def _new_lock():
342 return threading.Lock()
343
344 def lock(self, key):
345 with self._lock:
346 lock = self._dict.get(key)
347 if not lock:
348 lock = self._new_lock()
349 self._dict[key] = lock
350 return lock
Simran Basi4baad082013-02-14 13:39:18 -0800351
352
353def ExtractTarball(tarball_path, install_path, files_to_extract=None,
Gilad Arnold1638d822013-11-07 23:38:16 -0800354 excluded_files=None, return_extracted_files=False):
Simran Basi4baad082013-02-14 13:39:18 -0800355 """Extracts a tarball using tar.
356
357 Detects whether the tarball is compressed or not based on the file
358 extension and extracts the tarball into the install_path.
359
360 Args:
361 tarball_path: Path to the tarball to extract.
362 install_path: Path to extract the tarball to.
363 files_to_extract: String of specific files in the tarball to extract.
364 excluded_files: String of files to not extract.
Chris Sosafc715442014-04-09 20:45:23 -0700365 return_extracted_files: whether or not the caller expects the list of
Gilad Arnold1638d822013-11-07 23:38:16 -0800366 files extracted; if False, returns an empty list.
Chris Sosafc715442014-04-09 20:45:23 -0700367
Gilad Arnold1638d822013-11-07 23:38:16 -0800368 Returns:
369 List of absolute paths of the files extracted (possibly empty).
Simran Basi4baad082013-02-14 13:39:18 -0800370 """
371 # Deal with exclusions.
xixuan8a132922016-11-01 09:51:11 -0700372 # Add 'm' for not extracting file's modified time. All extracted files are
373 # marked with current system time.
xixuanc316d992016-11-17 16:19:00 -0800374 cmd = ['tar', 'xf', tarball_path, '--directory', install_path]
Simran Basi4baad082013-02-14 13:39:18 -0800375
Gilad Arnold1638d822013-11-07 23:38:16 -0800376 # If caller requires the list of extracted files, get verbose.
377 if return_extracted_files:
378 cmd += ['--verbose']
379
Simran Basi4baad082013-02-14 13:39:18 -0800380 # Determine how to decompress.
381 tarball = os.path.basename(tarball_path)
382 if tarball.endswith('.tar.bz2'):
383 cmd.append('--use-compress-prog=pbzip2')
384 elif tarball.endswith('.tgz') or tarball.endswith('.tar.gz'):
385 cmd.append('--gzip')
386
387 if excluded_files:
388 for exclude in excluded_files:
389 cmd.extend(['--exclude', exclude])
390
391 if files_to_extract:
392 cmd.extend(files_to_extract)
393
Aseda Aboagyeb6ba5492016-10-14 13:20:09 -0700394 cmd_output = ''
Simran Basi4baad082013-02-14 13:39:18 -0800395 try:
xixuanc316d992016-11-17 16:19:00 -0800396 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
397 stderr=subprocess.PIPE)
398 cmd_output, cmd_error = proc.communicate()
399 if cmd_error:
400 _Log('Error happened while in extracting tarball: %s',
401 cmd_error.rstrip())
402
Gilad Arnold1638d822013-11-07 23:38:16 -0800403 if return_extracted_files:
404 return [os.path.join(install_path, filename)
405 for filename in cmd_output.strip('\n').splitlines()
406 if not filename.endswith('/')]
407 return []
Simran Basi4baad082013-02-14 13:39:18 -0800408 except subprocess.CalledProcessError, e:
409 raise CommonUtilError(
Aseda Aboagyeb6ba5492016-10-14 13:20:09 -0700410 'An error occurred when attempting to untar %s:\n%s\n%s' %
411 (tarball_path, e, e.output))
joychen7c2054a2013-07-25 11:14:07 -0700412
413
414def IsInsideChroot():
415 """Returns True if we are inside chroot."""
416 return os.path.exists('/etc/debian_chroot')
xixuan60f141e2017-03-22 09:10:25 -0700417
418
419def IsRunningOnMoblab():
420 """Returns True if this code is running on a chromiumOS DUT."""
421 cmd = ['cat', '/etc/lsb-release']
422 try:
423 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
424 stderr=subprocess.PIPE)
425 cmd_output, cmd_error = proc.communicate()
426
427 if cmd_error:
428 _Log('Error happened while reading lsb-release file: %s',
429 cmd_error.rstrip())
430 return False
431
432 if '_moblab' in cmd_output:
433 return True
434 else:
435 return False
436 except subprocess.CalledProcessError as e:
437 _Log('Error happened while checking whether devserver package is running '
438 'on a DUT: %s\n%s', e, e.output)
439 return False