blob: 6b5e15b3a937576d73ff4739fbdc371c38475957 [file] [log] [blame]
Achuith Bhandarkar1a8a7f92019-09-26 15:35:23 +02001# -*- coding: utf-8 -*-
Chris Sosa47a7d4e2012-03-28 11:26:55 -07002# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
Frank Farzan37761d12011-12-01 14:29:08 -08003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Helper class for interacting with the Dev Server."""
7
xixuan8a132922016-11-01 09:51:11 -07008from __future__ import print_function
9
beepsbd337242013-07-09 22:44:06 -070010import ast
Gilad Arnold55a2a372012-10-02 09:46:32 -070011import binascii
Achuith Bhandarkar1a8a7f92019-09-26 15:35:23 +020012import distutils.version # pylint: disable=no-name-in-module,import-error
Frank Farzan37761d12011-12-01 14:29:08 -080013import errno
Gilad Arnold55a2a372012-10-02 09:46:32 -070014import hashlib
Frank Farzan37761d12011-12-01 14:29:08 -080015import os
Keith Haddow9caf04b2018-05-21 14:02:06 -070016import re
Frank Farzan37761d12011-12-01 14:29:08 -080017import 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
Achuith Bhandarkar1a8a7f92019-09-26 15:35:23 +020022import cherrypy # pylint: disable=import-error
23
Gilad Arnoldc65330c2012-09-20 15:17:48 -070024import log_util
25
26
27# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080028def _Log(message, *args):
29 return log_util.LogWithTag('UTIL', message, *args)
Gilad Arnoldc65330c2012-09-20 15:17:48 -070030
Frank Farzan37761d12011-12-01 14:29:08 -080031
Gilad Arnold55a2a372012-10-02 09:46:32 -070032_HASH_BLOCK_SIZE = 8192
33
Gilad Arnold6f99b982012-09-12 10:49:40 -070034
Gilad Arnold17fe03d2012-10-02 10:05:01 -070035class CommonUtilError(Exception):
Frank Farzan37761d12011-12-01 14:29:08 -080036 """Exception classes used by this module."""
37 pass
38
39
Chris Sosa4b951602014-04-09 20:26:07 -070040class DevServerHTTPError(cherrypy.HTTPError):
41 """Exception class to log the HTTPResponse before routing it to cherrypy."""
42 def __init__(self, status, message):
43 """CherryPy error with logging.
44
Chris Sosafc715442014-04-09 20:45:23 -070045 Args:
46 status: HTTPResponse status.
47 message: Message associated with the response.
Chris Sosa4b951602014-04-09 20:26:07 -070048 """
49 cherrypy.HTTPError.__init__(self, status, message)
50 _Log('HTTPError status: %s message: %s', status, message)
51
52
Chris Sosa76e44b92013-01-31 12:11:38 -080053def MkDirP(directory):
Yiming Chen4e3741f2014-12-01 16:38:17 -080054 """Thread-safely create a directory like mkdir -p.
55
56 If the directory already exists, call chown on the directory and its subfiles
57 recursively with current user and group to make sure current process has full
58 access to the directory.
59 """
Frank Farzan37761d12011-12-01 14:29:08 -080060 try:
Chris Sosa76e44b92013-01-31 12:11:38 -080061 os.makedirs(directory)
Frank Farzan37761d12011-12-01 14:29:08 -080062 except OSError, e:
Yiming Chen4e3741f2014-12-01 16:38:17 -080063 if e.errno == errno.EEXIST and os.path.isdir(directory):
64 # Fix permissions and ownership of the directory and its subfiles by
65 # calling chown recursively with current user and group.
66 chown_command = [
67 'sudo', 'chown', '-R', '%s:%s' % (os.getuid(), os.getgid()), directory
68 ]
69 subprocess.Popen(chown_command).wait()
70 else:
Frank Farzan37761d12011-12-01 14:29:08 -080071 raise
72
Frank Farzan37761d12011-12-01 14:29:08 -080073
Scott Zawalski16954532012-03-20 15:31:36 -040074def GetLatestBuildVersion(static_dir, target, milestone=None):
Frank Farzan37761d12011-12-01 14:29:08 -080075 """Retrieves the latest build version for a given board.
76
joychen921e1fb2013-06-28 11:12:20 -070077 Searches the static_dir for builds for target, and returns the highest
78 version number currently available locally.
79
Frank Farzan37761d12011-12-01 14:29:08 -080080 Args:
81 static_dir: Directory where builds are served from.
Scott Zawalski16954532012-03-20 15:31:36 -040082 target: The build target, typically a combination of the board and the
83 type of build e.g. x86-mario-release.
84 milestone: For latest build set to None, for builds only in a specific
85 milestone set to a str of format Rxx (e.g. R16). Default: None.
Frank Farzan37761d12011-12-01 14:29:08 -080086
87 Returns:
Scott Zawalski16954532012-03-20 15:31:36 -040088 If latest found, a full build string is returned e.g. R17-1234.0.0-a1-b983.
89 If no latest is found for some reason or another a '' string is returned.
Frank Farzan37761d12011-12-01 14:29:08 -080090
91 Raises:
Gilad Arnold17fe03d2012-10-02 10:05:01 -070092 CommonUtilError: If for some reason the latest build cannot be
Scott Zawalski16954532012-03-20 15:31:36 -040093 deteremined, this could be due to the dir not existing or no builds
94 being present after filtering on milestone.
Frank Farzan37761d12011-12-01 14:29:08 -080095 """
Scott Zawalski16954532012-03-20 15:31:36 -040096 target_path = os.path.join(static_dir, target)
97 if not os.path.isdir(target_path):
Gilad Arnold17fe03d2012-10-02 10:05:01 -070098 raise CommonUtilError('Cannot find path %s' % target_path)
Frank Farzan37761d12011-12-01 14:29:08 -080099
Scott Zawalski16954532012-03-20 15:31:36 -0400100 builds = [distutils.version.LooseVersion(build) for build in
Dan Shi9fa4bde2013-12-02 13:40:07 -0800101 os.listdir(target_path) if not build.endswith('.exception')]
Frank Farzan37761d12011-12-01 14:29:08 -0800102
Scott Zawalski16954532012-03-20 15:31:36 -0400103 if milestone and builds:
104 # Check if milestone Rxx is in the string representation of the build.
Achuith Bhandarkar1a8a7f92019-09-26 15:35:23 +0200105 builds = [build for build in builds if milestone.upper() in str(build)]
Frank Farzan37761d12011-12-01 14:29:08 -0800106
Scott Zawalski16954532012-03-20 15:31:36 -0400107 if not builds:
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700108 raise CommonUtilError('Could not determine build for %s' % target)
Frank Farzan37761d12011-12-01 14:29:08 -0800109
Scott Zawalski16954532012-03-20 15:31:36 -0400110 return str(max(builds))
Frank Farzan37761d12011-12-01 14:29:08 -0800111
112
Chris Sosa76e44b92013-01-31 12:11:38 -0800113def PathInDir(directory, path):
114 """Returns True if the path is in directory.
115
116 Args:
117 directory: Directory where the path should be in.
118 path: Path to check.
119
120 Returns:
121 True if path is in static_dir, False otherwise
122 """
123 directory = os.path.realpath(directory)
124 path = os.path.realpath(path)
xixuan8a132922016-11-01 09:51:11 -0700125 return path.startswith(directory) and len(path) != len(directory)
Chris Sosa76e44b92013-01-31 12:11:38 -0800126
127
Scott Zawalski84a39c92012-01-13 15:12:42 -0500128def GetControlFile(static_dir, build, control_path):
Frank Farzan37761d12011-12-01 14:29:08 -0800129 """Attempts to pull the requested control file from the Dev Server.
130
131 Args:
132 static_dir: Directory where builds are served from.
Frank Farzan37761d12011-12-01 14:29:08 -0800133 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
134 control_path: Path to control file on Dev Server relative to Autotest root.
135
Frank Farzan37761d12011-12-01 14:29:08 -0800136 Returns:
137 Content of the requested control file.
Chris Sosafc715442014-04-09 20:45:23 -0700138
139 Raises:
140 CommonUtilError: If lock can't be acquired.
Frank Farzan37761d12011-12-01 14:29:08 -0800141 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500142 # Be forgiving if the user passes in the control_path with a leading /
143 control_path = control_path.lstrip('/')
Scott Zawalski84a39c92012-01-13 15:12:42 -0500144 control_path = os.path.join(static_dir, build, 'autotest',
Scott Zawalski4647ce62012-01-03 17:17:28 -0500145 control_path)
Chris Sosa76e44b92013-01-31 12:11:38 -0800146 if not PathInDir(static_dir, control_path):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700147 raise CommonUtilError('Invalid control file "%s".' % control_path)
Frank Farzan37761d12011-12-01 14:29:08 -0800148
Scott Zawalski84a39c92012-01-13 15:12:42 -0500149 if not os.path.exists(control_path):
150 # TODO(scottz): Come up with some sort of error mechanism.
151 # crosbug.com/25040
152 return 'Unknown control path %s' % control_path
153
Frank Farzan37761d12011-12-01 14:29:08 -0800154 with open(control_path, 'r') as control_file:
155 return control_file.read()
156
157
beepsbd337242013-07-09 22:44:06 -0700158def GetControlFileListForSuite(static_dir, build, suite_name):
159 """List all control files for a specified build, for the given suite.
160
161 If the specified suite_name isn't found in the suite to control file
162 map, this method will return all control files for the build by calling
163 GetControlFileList.
164
165 Args:
166 static_dir: Directory where builds are served from.
167 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
168 suite_name: Name of the suite for which we require control files.
169
Chris Sosafc715442014-04-09 20:45:23 -0700170 Returns:
171 String of each control file separated by a newline.
172
beepsbd337242013-07-09 22:44:06 -0700173 Raises:
174 CommonUtilError: If the suite_to_control_file_map isn't found in
175 the specified build's staged directory.
beepsbd337242013-07-09 22:44:06 -0700176 """
177 suite_to_control_map = os.path.join(static_dir, build,
178 'autotest', 'test_suites',
179 'suite_to_control_file_map')
180
181 if not PathInDir(static_dir, suite_to_control_map):
182 raise CommonUtilError('suite_to_control_map not in "%s".' %
183 suite_to_control_map)
184
185 if not os.path.exists(suite_to_control_map):
186 raise CommonUtilError('Could not find this file. '
187 'Is it staged? %s' % suite_to_control_map)
188
189 with open(suite_to_control_map, 'r') as fd:
190 try:
191 return '\n'.join(ast.literal_eval(fd.read())[suite_name])
192 except KeyError:
193 return GetControlFileList(static_dir, build)
194
195
Scott Zawalski84a39c92012-01-13 15:12:42 -0500196def GetControlFileList(static_dir, build):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500197 """List all control|control. files in the specified board/build path.
198
199 Args:
200 static_dir: Directory where builds are served from.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500201 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
202
Scott Zawalski4647ce62012-01-03 17:17:28 -0500203 Returns:
204 String of each file separated by a newline.
Chris Sosafc715442014-04-09 20:45:23 -0700205
206 Raises:
207 CommonUtilError: If path is outside of sandbox.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500208 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500209 autotest_dir = os.path.join(static_dir, build, 'autotest/')
Chris Sosa76e44b92013-01-31 12:11:38 -0800210 if not PathInDir(static_dir, autotest_dir):
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700211 raise CommonUtilError('Autotest dir not in sandbox "%s".' % autotest_dir)
Scott Zawalski4647ce62012-01-03 17:17:28 -0500212
213 control_files = set()
Scott Zawalski84a39c92012-01-13 15:12:42 -0500214 if not os.path.exists(autotest_dir):
joychen3d164bd2013-06-24 18:12:23 -0700215 raise CommonUtilError('Could not find this directory.'
216 'Is it staged? %s' % autotest_dir)
Scott Zawalski84a39c92012-01-13 15:12:42 -0500217
Scott Zawalski4647ce62012-01-03 17:17:28 -0500218 for entry in os.walk(autotest_dir):
219 dir_path, _, files = entry
220 for file_entry in files:
221 if file_entry.startswith('control.') or file_entry == 'control':
222 control_files.add(os.path.join(dir_path,
Chris Sosaea148d92012-03-06 16:22:04 -0800223 file_entry).replace(autotest_dir, ''))
Scott Zawalski4647ce62012-01-03 17:17:28 -0500224
225 return '\n'.join(control_files)
226
227
Chris Sosa6a3697f2013-01-29 16:44:43 -0800228# Hashlib is strange and doesn't actually define these in a sane way that
229# pylint can find them. Disable checks for them.
230# pylint: disable=E1101,W0106
Amin Hassani8d718d12019-06-02 21:28:39 -0700231def GetFileHashes(file_path, do_sha256=False, do_md5=False):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700232 """Computes and returns a list of requested hashes.
233
234 Args:
235 file_path: path to file to be hashed
Gilad Arnold55a2a372012-10-02 09:46:32 -0700236 do_sha256: whether or not to compute a SHA256 hash
Chris Sosafc715442014-04-09 20:45:23 -0700237 do_md5: whether or not to compute a MD5 hash
238
Gilad Arnold55a2a372012-10-02 09:46:32 -0700239 Returns:
240 A dictionary containing binary hash values, keyed by 'sha1', 'sha256' and
241 'md5', respectively.
242 """
243 hashes = {}
Amin Hassani8d718d12019-06-02 21:28:39 -0700244 if any((do_sha256, do_md5)):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700245 # Initialize hashers.
Gilad Arnold55a2a372012-10-02 09:46:32 -0700246 hasher_sha256 = hashlib.sha256() if do_sha256 else None
247 hasher_md5 = hashlib.md5() if do_md5 else None
248
249 # Read blocks from file, update hashes.
250 with open(file_path, 'rb') as fd:
251 while True:
252 block = fd.read(_HASH_BLOCK_SIZE)
253 if not block:
254 break
Gilad Arnold55a2a372012-10-02 09:46:32 -0700255 hasher_sha256 and hasher_sha256.update(block)
256 hasher_md5 and hasher_md5.update(block)
257
258 # Update return values.
Gilad Arnold55a2a372012-10-02 09:46:32 -0700259 if hasher_sha256:
260 hashes['sha256'] = hasher_sha256.digest()
261 if hasher_md5:
262 hashes['md5'] = hasher_md5.digest()
263
264 return hashes
265
266
Gilad Arnold55a2a372012-10-02 09:46:32 -0700267def GetFileMd5(file_path):
268 """Returns the MD5 checksum of the file given (hex encoded)."""
269 return binascii.hexlify(GetFileHashes(file_path, do_md5=True)['md5'])
270
271
272def CopyFile(source, dest):
273 """Copies a file from |source| to |dest|."""
274 _Log('Copy File %s -> %s' % (source, dest))
275 shutil.copy(source, dest)
Chris Sosa76e44b92013-01-31 12:11:38 -0800276
277
Alex Deymo3e2d4952013-09-03 21:49:41 -0700278def SymlinkFile(target, link):
279 """Atomically creates or replaces the symlink |link| pointing to |target|.
280
281 If the specified |link| file already exists it is replaced with the new link
282 atomically.
283 """
284 if not os.path.exists(target):
Chris Sosa75490802013-09-30 17:21:45 -0700285 _Log('Could not find target for symlink: %s', target)
Alex Deymo3e2d4952013-09-03 21:49:41 -0700286 return
Chris Sosa75490802013-09-30 17:21:45 -0700287
Alex Deymo3e2d4952013-09-03 21:49:41 -0700288 _Log('Creating symlink: %s --> %s', link, target)
289
290 # Use the created link_base file to prevent other calls to SymlinkFile() to
291 # pick the same link_base temp file, thanks to mkstemp().
292 with tempfile.NamedTemporaryFile(prefix=os.path.basename(link)) as link_fd:
293 link_base = link_fd.name
294
295 # Use the unique link_base filename to create a symlink, but on the same
296 # directory as the required |link| to ensure the created symlink is in the
297 # same file system as |link|.
298 link_name = os.path.join(os.path.dirname(link),
Achuith Bhandarkar1a8a7f92019-09-26 15:35:23 +0200299 os.path.basename(link_base) + '-link')
Alex Deymo3e2d4952013-09-03 21:49:41 -0700300
301 # Create the symlink and then rename it to the final position. This ensures
302 # the symlink creation is atomic.
303 os.symlink(target, link_name)
304 os.rename(link_name, link)
305
306
Chris Sosa76e44b92013-01-31 12:11:38 -0800307class LockDict(object):
308 """A dictionary of locks.
309
310 This class provides a thread-safe store of threading.Lock objects, which can
311 be used to regulate access to any set of hashable resources. Usage:
312
313 foo_lock_dict = LockDict()
314 ...
315 with foo_lock_dict.lock('bar'):
316 # Critical section for 'bar'
317 """
318 def __init__(self):
319 self._lock = self._new_lock()
320 self._dict = {}
321
322 @staticmethod
323 def _new_lock():
324 return threading.Lock()
325
326 def lock(self, key):
327 with self._lock:
328 lock = self._dict.get(key)
329 if not lock:
330 lock = self._new_lock()
331 self._dict[key] = lock
332 return lock
Simran Basi4baad082013-02-14 13:39:18 -0800333
334
335def ExtractTarball(tarball_path, install_path, files_to_extract=None,
Gilad Arnold1638d822013-11-07 23:38:16 -0800336 excluded_files=None, return_extracted_files=False):
Simran Basi4baad082013-02-14 13:39:18 -0800337 """Extracts a tarball using tar.
338
339 Detects whether the tarball is compressed or not based on the file
340 extension and extracts the tarball into the install_path.
341
342 Args:
343 tarball_path: Path to the tarball to extract.
344 install_path: Path to extract the tarball to.
345 files_to_extract: String of specific files in the tarball to extract.
346 excluded_files: String of files to not extract.
Chris Sosafc715442014-04-09 20:45:23 -0700347 return_extracted_files: whether or not the caller expects the list of
Gilad Arnold1638d822013-11-07 23:38:16 -0800348 files extracted; if False, returns an empty list.
Chris Sosafc715442014-04-09 20:45:23 -0700349
Gilad Arnold1638d822013-11-07 23:38:16 -0800350 Returns:
351 List of absolute paths of the files extracted (possibly empty).
Simran Basi4baad082013-02-14 13:39:18 -0800352 """
353 # Deal with exclusions.
xixuan8a132922016-11-01 09:51:11 -0700354 # Add 'm' for not extracting file's modified time. All extracted files are
355 # marked with current system time.
xixuanc316d992016-11-17 16:19:00 -0800356 cmd = ['tar', 'xf', tarball_path, '--directory', install_path]
Simran Basi4baad082013-02-14 13:39:18 -0800357
Gilad Arnold1638d822013-11-07 23:38:16 -0800358 # If caller requires the list of extracted files, get verbose.
359 if return_extracted_files:
360 cmd += ['--verbose']
361
Simran Basi4baad082013-02-14 13:39:18 -0800362 # Determine how to decompress.
363 tarball = os.path.basename(tarball_path)
364 if tarball.endswith('.tar.bz2'):
365 cmd.append('--use-compress-prog=pbzip2')
366 elif tarball.endswith('.tgz') or tarball.endswith('.tar.gz'):
367 cmd.append('--gzip')
368
369 if excluded_files:
370 for exclude in excluded_files:
371 cmd.extend(['--exclude', exclude])
372
373 if files_to_extract:
374 cmd.extend(files_to_extract)
375
Aseda Aboagyeb6ba5492016-10-14 13:20:09 -0700376 cmd_output = ''
Simran Basi4baad082013-02-14 13:39:18 -0800377 try:
xixuanc316d992016-11-17 16:19:00 -0800378 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
379 stderr=subprocess.PIPE)
380 cmd_output, cmd_error = proc.communicate()
381 if cmd_error:
382 _Log('Error happened while in extracting tarball: %s',
383 cmd_error.rstrip())
384
Gilad Arnold1638d822013-11-07 23:38:16 -0800385 if return_extracted_files:
386 return [os.path.join(install_path, filename)
387 for filename in cmd_output.strip('\n').splitlines()
388 if not filename.endswith('/')]
389 return []
Simran Basi4baad082013-02-14 13:39:18 -0800390 except subprocess.CalledProcessError, e:
391 raise CommonUtilError(
Aseda Aboagyeb6ba5492016-10-14 13:20:09 -0700392 'An error occurred when attempting to untar %s:\n%s\n%s' %
393 (tarball_path, e, e.output))
joychen7c2054a2013-07-25 11:14:07 -0700394
395
396def IsInsideChroot():
397 """Returns True if we are inside chroot."""
398 return os.path.exists('/etc/debian_chroot')
xixuan178263c2017-03-22 09:10:25 -0700399
400
401def IsRunningOnMoblab():
402 """Returns True if this code is running on a chromiumOS DUT."""
403 cmd = ['cat', '/etc/lsb-release']
404 try:
405 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
406 stderr=subprocess.PIPE)
407 cmd_output, cmd_error = proc.communicate()
408
409 if cmd_error:
410 _Log('Error happened while reading lsb-release file: %s',
411 cmd_error.rstrip())
412 return False
413
Achuith Bhandarkar1a8a7f92019-09-26 15:35:23 +0200414 return bool(re.search(r'[_-]+moblab', cmd_output))
xixuan178263c2017-03-22 09:10:25 -0700415 except subprocess.CalledProcessError as e:
416 _Log('Error happened while checking whether devserver package is running '
417 'on a DUT: %s\n%s', e, e.output)
418 return False