blob: c2a9287d9cf846c039adc27f579c072d7fda409d [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
Keith Haddow9caf04b2018-05-21 14:02:06 -070017import re
Frank Farzan37761d12011-12-01 14:29:08 -080018import shutil
Alex Deymo3e2d4952013-09-03 21:49:41 -070019import tempfile
Chris Sosa76e44b92013-01-31 12:11:38 -080020import threading
Simran Basi4baad082013-02-14 13:39:18 -080021import subprocess
Frank Farzan37761d12011-12-01 14:29:08 -080022
Gilad Arnoldc65330c2012-09-20 15:17:48 -070023import log_util
24
25
26# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080027def _Log(message, *args):
28 return log_util.LogWithTag('UTIL', message, *args)
Gilad Arnoldc65330c2012-09-20 15:17:48 -070029
Frank Farzan37761d12011-12-01 14:29:08 -080030
Gilad Arnold55a2a372012-10-02 09:46:32 -070031_HASH_BLOCK_SIZE = 8192
32
Gilad Arnold6f99b982012-09-12 10:49:40 -070033
Gilad Arnold17fe03d2012-10-02 10:05:01 -070034class CommonUtilError(Exception):
Frank Farzan37761d12011-12-01 14:29:08 -080035 """Exception classes used by this module."""
36 pass
37
38
Chris Sosa4b951602014-04-09 20:26:07 -070039class DevServerHTTPError(cherrypy.HTTPError):
40 """Exception class to log the HTTPResponse before routing it to cherrypy."""
41 def __init__(self, status, message):
42 """CherryPy error with logging.
43
Chris Sosafc715442014-04-09 20:45:23 -070044 Args:
45 status: HTTPResponse status.
46 message: Message associated with the response.
Chris Sosa4b951602014-04-09 20:26:07 -070047 """
48 cherrypy.HTTPError.__init__(self, status, message)
49 _Log('HTTPError status: %s message: %s', status, message)
50
51
Chris Sosa76e44b92013-01-31 12:11:38 -080052def MkDirP(directory):
Yiming Chen4e3741f2014-12-01 16:38:17 -080053 """Thread-safely create a directory like mkdir -p.
54
55 If the directory already exists, call chown on the directory and its subfiles
56 recursively with current user and group to make sure current process has full
57 access to the directory.
58 """
Frank Farzan37761d12011-12-01 14:29:08 -080059 try:
Chris Sosa76e44b92013-01-31 12:11:38 -080060 os.makedirs(directory)
Frank Farzan37761d12011-12-01 14:29:08 -080061 except OSError, e:
Yiming Chen4e3741f2014-12-01 16:38:17 -080062 if e.errno == errno.EEXIST and os.path.isdir(directory):
63 # Fix permissions and ownership of the directory and its subfiles by
64 # calling chown recursively with current user and group.
65 chown_command = [
66 'sudo', 'chown', '-R', '%s:%s' % (os.getuid(), os.getgid()), directory
67 ]
68 subprocess.Popen(chown_command).wait()
69 else:
Frank Farzan37761d12011-12-01 14:29:08 -080070 raise
71
Frank Farzan37761d12011-12-01 14:29:08 -080072
Scott Zawalski16954532012-03-20 15:31:36 -040073def GetLatestBuildVersion(static_dir, target, milestone=None):
Frank Farzan37761d12011-12-01 14:29:08 -080074 """Retrieves the latest build version for a given board.
75
joychen921e1fb2013-06-28 11:12:20 -070076 Searches the static_dir for builds for target, and returns the highest
77 version number currently available locally.
78
Frank Farzan37761d12011-12-01 14:29:08 -080079 Args:
80 static_dir: Directory where builds are served from.
Scott Zawalski16954532012-03-20 15:31:36 -040081 target: The build target, typically a combination of the board and the
82 type of build e.g. x86-mario-release.
83 milestone: For latest build set to None, for builds only in a specific
84 milestone set to a str of format Rxx (e.g. R16). Default: None.
Frank Farzan37761d12011-12-01 14:29:08 -080085
86 Returns:
Scott Zawalski16954532012-03-20 15:31:36 -040087 If latest found, a full build string is returned e.g. R17-1234.0.0-a1-b983.
88 If no latest is found for some reason or another a '' string is returned.
Frank Farzan37761d12011-12-01 14:29:08 -080089
90 Raises:
Gilad Arnold17fe03d2012-10-02 10:05:01 -070091 CommonUtilError: If for some reason the latest build cannot be
Scott Zawalski16954532012-03-20 15:31:36 -040092 deteremined, this could be due to the dir not existing or no builds
93 being present after filtering on milestone.
Frank Farzan37761d12011-12-01 14:29:08 -080094 """
Scott Zawalski16954532012-03-20 15:31:36 -040095 target_path = os.path.join(static_dir, target)
96 if not os.path.isdir(target_path):
Gilad Arnold17fe03d2012-10-02 10:05:01 -070097 raise CommonUtilError('Cannot find path %s' % target_path)
Frank Farzan37761d12011-12-01 14:29:08 -080098
Scott Zawalski16954532012-03-20 15:31:36 -040099 builds = [distutils.version.LooseVersion(build) for build in
Dan Shi9fa4bde2013-12-02 13:40:07 -0800100 os.listdir(target_path) if not build.endswith('.exception')]
Frank Farzan37761d12011-12-01 14:29:08 -0800101
Scott Zawalski16954532012-03-20 15:31:36 -0400102 if milestone and builds:
103 # Check if milestone Rxx is in the string representation of the build.
104 builds = filter(lambda x: milestone.upper() in str(x), builds)
Frank Farzan37761d12011-12-01 14:29:08 -0800105
Scott Zawalski16954532012-03-20 15:31:36 -0400106 if not builds:
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700107 raise CommonUtilError('Could not determine build for %s' % target)
Frank Farzan37761d12011-12-01 14:29:08 -0800108
Scott Zawalski16954532012-03-20 15:31:36 -0400109 return str(max(builds))
Frank Farzan37761d12011-12-01 14:29:08 -0800110
111
Chris Sosa76e44b92013-01-31 12:11:38 -0800112def PathInDir(directory, path):
113 """Returns True if the path is in directory.
114
115 Args:
116 directory: Directory where the path should be in.
117 path: Path to check.
118
119 Returns:
120 True if path is in static_dir, False otherwise
121 """
122 directory = os.path.realpath(directory)
123 path = os.path.realpath(path)
xixuan8a132922016-11-01 09:51:11 -0700124 return path.startswith(directory) and len(path) != len(directory)
Chris Sosa76e44b92013-01-31 12:11:38 -0800125
126
Scott Zawalski84a39c92012-01-13 15:12:42 -0500127def GetControlFile(static_dir, build, control_path):
Frank Farzan37761d12011-12-01 14:29:08 -0800128 """Attempts to pull the requested control file from the Dev Server.
129
130 Args:
131 static_dir: Directory where builds are served from.
Frank Farzan37761d12011-12-01 14:29:08 -0800132 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
133 control_path: Path to control file on Dev Server relative to Autotest root.
134
Frank Farzan37761d12011-12-01 14:29:08 -0800135 Returns:
136 Content of the requested control file.
Chris Sosafc715442014-04-09 20:45:23 -0700137
138 Raises:
139 CommonUtilError: If lock can't be acquired.
Frank Farzan37761d12011-12-01 14:29:08 -0800140 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500141 # Be forgiving if the user passes in the control_path with a leading /
142 control_path = control_path.lstrip('/')
Scott Zawalski84a39c92012-01-13 15:12:42 -0500143 control_path = os.path.join(static_dir, build, 'autotest',
Scott Zawalski4647ce62012-01-03 17:17:28 -0500144 control_path)
Chris Sosa76e44b92013-01-31 12:11:38 -0800145 if not PathInDir(static_dir, control_path):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700146 raise CommonUtilError('Invalid control file "%s".' % control_path)
Frank Farzan37761d12011-12-01 14:29:08 -0800147
Scott Zawalski84a39c92012-01-13 15:12:42 -0500148 if not os.path.exists(control_path):
149 # TODO(scottz): Come up with some sort of error mechanism.
150 # crosbug.com/25040
151 return 'Unknown control path %s' % control_path
152
Frank Farzan37761d12011-12-01 14:29:08 -0800153 with open(control_path, 'r') as control_file:
154 return control_file.read()
155
156
beepsbd337242013-07-09 22:44:06 -0700157def GetControlFileListForSuite(static_dir, build, suite_name):
158 """List all control files for a specified build, for the given suite.
159
160 If the specified suite_name isn't found in the suite to control file
161 map, this method will return all control files for the build by calling
162 GetControlFileList.
163
164 Args:
165 static_dir: Directory where builds are served from.
166 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
167 suite_name: Name of the suite for which we require control files.
168
Chris Sosafc715442014-04-09 20:45:23 -0700169 Returns:
170 String of each control file separated by a newline.
171
beepsbd337242013-07-09 22:44:06 -0700172 Raises:
173 CommonUtilError: If the suite_to_control_file_map isn't found in
174 the specified build's staged directory.
beepsbd337242013-07-09 22:44:06 -0700175 """
176 suite_to_control_map = os.path.join(static_dir, build,
177 'autotest', 'test_suites',
178 'suite_to_control_file_map')
179
180 if not PathInDir(static_dir, suite_to_control_map):
181 raise CommonUtilError('suite_to_control_map not in "%s".' %
182 suite_to_control_map)
183
184 if not os.path.exists(suite_to_control_map):
185 raise CommonUtilError('Could not find this file. '
186 'Is it staged? %s' % suite_to_control_map)
187
188 with open(suite_to_control_map, 'r') as fd:
189 try:
190 return '\n'.join(ast.literal_eval(fd.read())[suite_name])
191 except KeyError:
192 return GetControlFileList(static_dir, build)
193
194
Scott Zawalski84a39c92012-01-13 15:12:42 -0500195def GetControlFileList(static_dir, build):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500196 """List all control|control. files in the specified board/build path.
197
198 Args:
199 static_dir: Directory where builds are served from.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500200 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
201
Scott Zawalski4647ce62012-01-03 17:17:28 -0500202 Returns:
203 String of each file separated by a newline.
Chris Sosafc715442014-04-09 20:45:23 -0700204
205 Raises:
206 CommonUtilError: If path is outside of sandbox.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500207 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500208 autotest_dir = os.path.join(static_dir, build, 'autotest/')
Chris Sosa76e44b92013-01-31 12:11:38 -0800209 if not PathInDir(static_dir, autotest_dir):
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700210 raise CommonUtilError('Autotest dir not in sandbox "%s".' % autotest_dir)
Scott Zawalski4647ce62012-01-03 17:17:28 -0500211
212 control_files = set()
Scott Zawalski84a39c92012-01-13 15:12:42 -0500213 if not os.path.exists(autotest_dir):
joychen3d164bd2013-06-24 18:12:23 -0700214 raise CommonUtilError('Could not find this directory.'
215 'Is it staged? %s' % autotest_dir)
Scott Zawalski84a39c92012-01-13 15:12:42 -0500216
Scott Zawalski4647ce62012-01-03 17:17:28 -0500217 for entry in os.walk(autotest_dir):
218 dir_path, _, files = entry
219 for file_entry in files:
220 if file_entry.startswith('control.') or file_entry == 'control':
221 control_files.add(os.path.join(dir_path,
Chris Sosaea148d92012-03-06 16:22:04 -0800222 file_entry).replace(autotest_dir, ''))
Scott Zawalski4647ce62012-01-03 17:17:28 -0500223
224 return '\n'.join(control_files)
225
226
Gilad Arnold55a2a372012-10-02 09:46:32 -0700227def GetFileSize(file_path):
228 """Returns the size in bytes of the file given."""
229 return os.path.getsize(file_path)
230
231
Chris Sosa6a3697f2013-01-29 16:44:43 -0800232# Hashlib is strange and doesn't actually define these in a sane way that
233# pylint can find them. Disable checks for them.
234# pylint: disable=E1101,W0106
Gilad Arnold55a2a372012-10-02 09:46:32 -0700235def GetFileHashes(file_path, do_sha1=False, do_sha256=False, do_md5=False):
236 """Computes and returns a list of requested hashes.
237
238 Args:
239 file_path: path to file to be hashed
Chris Sosafc715442014-04-09 20:45:23 -0700240 do_sha1: whether or not to compute a SHA1 hash
Gilad Arnold55a2a372012-10-02 09:46:32 -0700241 do_sha256: whether or not to compute a SHA256 hash
Chris Sosafc715442014-04-09 20:45:23 -0700242 do_md5: whether or not to compute a MD5 hash
243
Gilad Arnold55a2a372012-10-02 09:46:32 -0700244 Returns:
245 A dictionary containing binary hash values, keyed by 'sha1', 'sha256' and
246 'md5', respectively.
247 """
248 hashes = {}
xixuan8a132922016-11-01 09:51:11 -0700249 if any((do_sha1, do_sha256, do_md5)):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700250 # Initialize hashers.
251 hasher_sha1 = hashlib.sha1() if do_sha1 else None
252 hasher_sha256 = hashlib.sha256() if do_sha256 else None
253 hasher_md5 = hashlib.md5() if do_md5 else None
254
255 # Read blocks from file, update hashes.
256 with open(file_path, 'rb') as fd:
257 while True:
258 block = fd.read(_HASH_BLOCK_SIZE)
259 if not block:
260 break
261 hasher_sha1 and hasher_sha1.update(block)
262 hasher_sha256 and hasher_sha256.update(block)
263 hasher_md5 and hasher_md5.update(block)
264
265 # Update return values.
266 if hasher_sha1:
267 hashes['sha1'] = hasher_sha1.digest()
268 if hasher_sha256:
269 hashes['sha256'] = hasher_sha256.digest()
270 if hasher_md5:
271 hashes['md5'] = hasher_md5.digest()
272
273 return hashes
274
275
276def GetFileSha1(file_path):
277 """Returns the SHA1 checksum of the file given (base64 encoded)."""
278 return base64.b64encode(GetFileHashes(file_path, do_sha1=True)['sha1'])
279
280
281def GetFileSha256(file_path):
282 """Returns the SHA256 checksum of the file given (base64 encoded)."""
283 return base64.b64encode(GetFileHashes(file_path, do_sha256=True)['sha256'])
284
285
286def GetFileMd5(file_path):
287 """Returns the MD5 checksum of the file given (hex encoded)."""
288 return binascii.hexlify(GetFileHashes(file_path, do_md5=True)['md5'])
289
290
291def CopyFile(source, dest):
292 """Copies a file from |source| to |dest|."""
293 _Log('Copy File %s -> %s' % (source, dest))
294 shutil.copy(source, dest)
Chris Sosa76e44b92013-01-31 12:11:38 -0800295
296
Alex Deymo3e2d4952013-09-03 21:49:41 -0700297def SymlinkFile(target, link):
298 """Atomically creates or replaces the symlink |link| pointing to |target|.
299
300 If the specified |link| file already exists it is replaced with the new link
301 atomically.
302 """
303 if not os.path.exists(target):
Chris Sosa75490802013-09-30 17:21:45 -0700304 _Log('Could not find target for symlink: %s', target)
Alex Deymo3e2d4952013-09-03 21:49:41 -0700305 return
Chris Sosa75490802013-09-30 17:21:45 -0700306
Alex Deymo3e2d4952013-09-03 21:49:41 -0700307 _Log('Creating symlink: %s --> %s', link, target)
308
309 # Use the created link_base file to prevent other calls to SymlinkFile() to
310 # pick the same link_base temp file, thanks to mkstemp().
311 with tempfile.NamedTemporaryFile(prefix=os.path.basename(link)) as link_fd:
312 link_base = link_fd.name
313
314 # Use the unique link_base filename to create a symlink, but on the same
315 # directory as the required |link| to ensure the created symlink is in the
316 # same file system as |link|.
317 link_name = os.path.join(os.path.dirname(link),
318 os.path.basename(link_base) + "-link")
319
320 # Create the symlink and then rename it to the final position. This ensures
321 # the symlink creation is atomic.
322 os.symlink(target, link_name)
323 os.rename(link_name, link)
324
325
Chris Sosa76e44b92013-01-31 12:11:38 -0800326class LockDict(object):
327 """A dictionary of locks.
328
329 This class provides a thread-safe store of threading.Lock objects, which can
330 be used to regulate access to any set of hashable resources. Usage:
331
332 foo_lock_dict = LockDict()
333 ...
334 with foo_lock_dict.lock('bar'):
335 # Critical section for 'bar'
336 """
337 def __init__(self):
338 self._lock = self._new_lock()
339 self._dict = {}
340
341 @staticmethod
342 def _new_lock():
343 return threading.Lock()
344
345 def lock(self, key):
346 with self._lock:
347 lock = self._dict.get(key)
348 if not lock:
349 lock = self._new_lock()
350 self._dict[key] = lock
351 return lock
Simran Basi4baad082013-02-14 13:39:18 -0800352
353
354def ExtractTarball(tarball_path, install_path, files_to_extract=None,
Gilad Arnold1638d822013-11-07 23:38:16 -0800355 excluded_files=None, return_extracted_files=False):
Simran Basi4baad082013-02-14 13:39:18 -0800356 """Extracts a tarball using tar.
357
358 Detects whether the tarball is compressed or not based on the file
359 extension and extracts the tarball into the install_path.
360
361 Args:
362 tarball_path: Path to the tarball to extract.
363 install_path: Path to extract the tarball to.
364 files_to_extract: String of specific files in the tarball to extract.
365 excluded_files: String of files to not extract.
Chris Sosafc715442014-04-09 20:45:23 -0700366 return_extracted_files: whether or not the caller expects the list of
Gilad Arnold1638d822013-11-07 23:38:16 -0800367 files extracted; if False, returns an empty list.
Chris Sosafc715442014-04-09 20:45:23 -0700368
Gilad Arnold1638d822013-11-07 23:38:16 -0800369 Returns:
370 List of absolute paths of the files extracted (possibly empty).
Simran Basi4baad082013-02-14 13:39:18 -0800371 """
372 # Deal with exclusions.
xixuan8a132922016-11-01 09:51:11 -0700373 # Add 'm' for not extracting file's modified time. All extracted files are
374 # marked with current system time.
xixuanc316d992016-11-17 16:19:00 -0800375 cmd = ['tar', 'xf', tarball_path, '--directory', install_path]
Simran Basi4baad082013-02-14 13:39:18 -0800376
Gilad Arnold1638d822013-11-07 23:38:16 -0800377 # If caller requires the list of extracted files, get verbose.
378 if return_extracted_files:
379 cmd += ['--verbose']
380
Simran Basi4baad082013-02-14 13:39:18 -0800381 # Determine how to decompress.
382 tarball = os.path.basename(tarball_path)
383 if tarball.endswith('.tar.bz2'):
384 cmd.append('--use-compress-prog=pbzip2')
385 elif tarball.endswith('.tgz') or tarball.endswith('.tar.gz'):
386 cmd.append('--gzip')
387
388 if excluded_files:
389 for exclude in excluded_files:
390 cmd.extend(['--exclude', exclude])
391
392 if files_to_extract:
393 cmd.extend(files_to_extract)
394
Aseda Aboagyeb6ba5492016-10-14 13:20:09 -0700395 cmd_output = ''
Simran Basi4baad082013-02-14 13:39:18 -0800396 try:
xixuanc316d992016-11-17 16:19:00 -0800397 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
398 stderr=subprocess.PIPE)
399 cmd_output, cmd_error = proc.communicate()
400 if cmd_error:
401 _Log('Error happened while in extracting tarball: %s',
402 cmd_error.rstrip())
403
Gilad Arnold1638d822013-11-07 23:38:16 -0800404 if return_extracted_files:
405 return [os.path.join(install_path, filename)
406 for filename in cmd_output.strip('\n').splitlines()
407 if not filename.endswith('/')]
408 return []
Simran Basi4baad082013-02-14 13:39:18 -0800409 except subprocess.CalledProcessError, e:
410 raise CommonUtilError(
Aseda Aboagyeb6ba5492016-10-14 13:20:09 -0700411 'An error occurred when attempting to untar %s:\n%s\n%s' %
412 (tarball_path, e, e.output))
joychen7c2054a2013-07-25 11:14:07 -0700413
414
415def IsInsideChroot():
416 """Returns True if we are inside chroot."""
417 return os.path.exists('/etc/debian_chroot')
xixuan178263c2017-03-22 09:10:25 -0700418
419
420def IsRunningOnMoblab():
421 """Returns True if this code is running on a chromiumOS DUT."""
422 cmd = ['cat', '/etc/lsb-release']
423 try:
424 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
425 stderr=subprocess.PIPE)
426 cmd_output, cmd_error = proc.communicate()
427
428 if cmd_error:
429 _Log('Error happened while reading lsb-release file: %s',
430 cmd_error.rstrip())
431 return False
432
Keith Haddow87850ed2018-05-30 17:31:53 -0700433 if re.search(r"[_-]+moblab", cmd_output):
xixuan178263c2017-03-22 09:10:25 -0700434 return True
435 else:
436 return False
437 except subprocess.CalledProcessError as e:
438 _Log('Error happened while checking whether devserver package is running '
439 'on a DUT: %s\n%s', e, e.output)
440 return False