blob: a1494a00cc77497e7f64595194f626f5ca3b5427 [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 base64
12import binascii
Achuith Bhandarkar1a8a7f92019-09-26 15:35:23 +020013import distutils.version # pylint: disable=no-name-in-module,import-error
Frank Farzan37761d12011-12-01 14:29:08 -080014import 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
Achuith Bhandarkar1a8a7f92019-09-26 15:35:23 +020023import cherrypy # pylint: disable=import-error
24
Gilad Arnoldc65330c2012-09-20 15:17:48 -070025import log_util
26
27
28# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080029def _Log(message, *args):
30 return log_util.LogWithTag('UTIL', message, *args)
Gilad Arnoldc65330c2012-09-20 15:17:48 -070031
Frank Farzan37761d12011-12-01 14:29:08 -080032
Gilad Arnold55a2a372012-10-02 09:46:32 -070033_HASH_BLOCK_SIZE = 8192
34
Gilad Arnold6f99b982012-09-12 10:49:40 -070035
Gilad Arnold17fe03d2012-10-02 10:05:01 -070036class CommonUtilError(Exception):
Frank Farzan37761d12011-12-01 14:29:08 -080037 """Exception classes used by this module."""
38 pass
39
40
Chris Sosa4b951602014-04-09 20:26:07 -070041class DevServerHTTPError(cherrypy.HTTPError):
42 """Exception class to log the HTTPResponse before routing it to cherrypy."""
43 def __init__(self, status, message):
44 """CherryPy error with logging.
45
Chris Sosafc715442014-04-09 20:45:23 -070046 Args:
47 status: HTTPResponse status.
48 message: Message associated with the response.
Chris Sosa4b951602014-04-09 20:26:07 -070049 """
50 cherrypy.HTTPError.__init__(self, status, message)
51 _Log('HTTPError status: %s message: %s', status, message)
52
53
Chris Sosa76e44b92013-01-31 12:11:38 -080054def MkDirP(directory):
Yiming Chen4e3741f2014-12-01 16:38:17 -080055 """Thread-safely create a directory like mkdir -p.
56
57 If the directory already exists, call chown on the directory and its subfiles
58 recursively with current user and group to make sure current process has full
59 access to the directory.
60 """
Frank Farzan37761d12011-12-01 14:29:08 -080061 try:
Chris Sosa76e44b92013-01-31 12:11:38 -080062 os.makedirs(directory)
Frank Farzan37761d12011-12-01 14:29:08 -080063 except OSError, e:
Yiming Chen4e3741f2014-12-01 16:38:17 -080064 if e.errno == errno.EEXIST and os.path.isdir(directory):
65 # Fix permissions and ownership of the directory and its subfiles by
66 # calling chown recursively with current user and group.
67 chown_command = [
68 'sudo', 'chown', '-R', '%s:%s' % (os.getuid(), os.getgid()), directory
69 ]
70 subprocess.Popen(chown_command).wait()
71 else:
Frank Farzan37761d12011-12-01 14:29:08 -080072 raise
73
Frank Farzan37761d12011-12-01 14:29:08 -080074
Scott Zawalski16954532012-03-20 15:31:36 -040075def GetLatestBuildVersion(static_dir, target, milestone=None):
Frank Farzan37761d12011-12-01 14:29:08 -080076 """Retrieves the latest build version for a given board.
77
joychen921e1fb2013-06-28 11:12:20 -070078 Searches the static_dir for builds for target, and returns the highest
79 version number currently available locally.
80
Frank Farzan37761d12011-12-01 14:29:08 -080081 Args:
82 static_dir: Directory where builds are served from.
Scott Zawalski16954532012-03-20 15:31:36 -040083 target: The build target, typically a combination of the board and the
84 type of build e.g. x86-mario-release.
85 milestone: For latest build set to None, for builds only in a specific
86 milestone set to a str of format Rxx (e.g. R16). Default: None.
Frank Farzan37761d12011-12-01 14:29:08 -080087
88 Returns:
Scott Zawalski16954532012-03-20 15:31:36 -040089 If latest found, a full build string is returned e.g. R17-1234.0.0-a1-b983.
90 If no latest is found for some reason or another a '' string is returned.
Frank Farzan37761d12011-12-01 14:29:08 -080091
92 Raises:
Gilad Arnold17fe03d2012-10-02 10:05:01 -070093 CommonUtilError: If for some reason the latest build cannot be
Scott Zawalski16954532012-03-20 15:31:36 -040094 deteremined, this could be due to the dir not existing or no builds
95 being present after filtering on milestone.
Frank Farzan37761d12011-12-01 14:29:08 -080096 """
Scott Zawalski16954532012-03-20 15:31:36 -040097 target_path = os.path.join(static_dir, target)
98 if not os.path.isdir(target_path):
Gilad Arnold17fe03d2012-10-02 10:05:01 -070099 raise CommonUtilError('Cannot find path %s' % target_path)
Frank Farzan37761d12011-12-01 14:29:08 -0800100
Scott Zawalski16954532012-03-20 15:31:36 -0400101 builds = [distutils.version.LooseVersion(build) for build in
Dan Shi9fa4bde2013-12-02 13:40:07 -0800102 os.listdir(target_path) if not build.endswith('.exception')]
Frank Farzan37761d12011-12-01 14:29:08 -0800103
Scott Zawalski16954532012-03-20 15:31:36 -0400104 if milestone and builds:
105 # Check if milestone Rxx is in the string representation of the build.
Achuith Bhandarkar1a8a7f92019-09-26 15:35:23 +0200106 builds = [build for build in builds if milestone.upper() in str(build)]
Frank Farzan37761d12011-12-01 14:29:08 -0800107
Scott Zawalski16954532012-03-20 15:31:36 -0400108 if not builds:
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700109 raise CommonUtilError('Could not determine build for %s' % target)
Frank Farzan37761d12011-12-01 14:29:08 -0800110
Scott Zawalski16954532012-03-20 15:31:36 -0400111 return str(max(builds))
Frank Farzan37761d12011-12-01 14:29:08 -0800112
113
Chris Sosa76e44b92013-01-31 12:11:38 -0800114def PathInDir(directory, path):
115 """Returns True if the path is in directory.
116
117 Args:
118 directory: Directory where the path should be in.
119 path: Path to check.
120
121 Returns:
122 True if path is in static_dir, False otherwise
123 """
124 directory = os.path.realpath(directory)
125 path = os.path.realpath(path)
xixuan8a132922016-11-01 09:51:11 -0700126 return path.startswith(directory) and len(path) != len(directory)
Chris Sosa76e44b92013-01-31 12:11:38 -0800127
128
Scott Zawalski84a39c92012-01-13 15:12:42 -0500129def GetControlFile(static_dir, build, control_path):
Frank Farzan37761d12011-12-01 14:29:08 -0800130 """Attempts to pull the requested control file from the Dev Server.
131
132 Args:
133 static_dir: Directory where builds are served from.
Frank Farzan37761d12011-12-01 14:29:08 -0800134 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
135 control_path: Path to control file on Dev Server relative to Autotest root.
136
Frank Farzan37761d12011-12-01 14:29:08 -0800137 Returns:
138 Content of the requested control file.
Chris Sosafc715442014-04-09 20:45:23 -0700139
140 Raises:
141 CommonUtilError: If lock can't be acquired.
Frank Farzan37761d12011-12-01 14:29:08 -0800142 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500143 # Be forgiving if the user passes in the control_path with a leading /
144 control_path = control_path.lstrip('/')
Scott Zawalski84a39c92012-01-13 15:12:42 -0500145 control_path = os.path.join(static_dir, build, 'autotest',
Scott Zawalski4647ce62012-01-03 17:17:28 -0500146 control_path)
Chris Sosa76e44b92013-01-31 12:11:38 -0800147 if not PathInDir(static_dir, control_path):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700148 raise CommonUtilError('Invalid control file "%s".' % control_path)
Frank Farzan37761d12011-12-01 14:29:08 -0800149
Scott Zawalski84a39c92012-01-13 15:12:42 -0500150 if not os.path.exists(control_path):
151 # TODO(scottz): Come up with some sort of error mechanism.
152 # crosbug.com/25040
153 return 'Unknown control path %s' % control_path
154
Frank Farzan37761d12011-12-01 14:29:08 -0800155 with open(control_path, 'r') as control_file:
156 return control_file.read()
157
158
beepsbd337242013-07-09 22:44:06 -0700159def GetControlFileListForSuite(static_dir, build, suite_name):
160 """List all control files for a specified build, for the given suite.
161
162 If the specified suite_name isn't found in the suite to control file
163 map, this method will return all control files for the build by calling
164 GetControlFileList.
165
166 Args:
167 static_dir: Directory where builds are served from.
168 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
169 suite_name: Name of the suite for which we require control files.
170
Chris Sosafc715442014-04-09 20:45:23 -0700171 Returns:
172 String of each control file separated by a newline.
173
beepsbd337242013-07-09 22:44:06 -0700174 Raises:
175 CommonUtilError: If the suite_to_control_file_map isn't found in
176 the specified build's staged directory.
beepsbd337242013-07-09 22:44:06 -0700177 """
178 suite_to_control_map = os.path.join(static_dir, build,
179 'autotest', 'test_suites',
180 'suite_to_control_file_map')
181
182 if not PathInDir(static_dir, suite_to_control_map):
183 raise CommonUtilError('suite_to_control_map not in "%s".' %
184 suite_to_control_map)
185
186 if not os.path.exists(suite_to_control_map):
187 raise CommonUtilError('Could not find this file. '
188 'Is it staged? %s' % suite_to_control_map)
189
190 with open(suite_to_control_map, 'r') as fd:
191 try:
192 return '\n'.join(ast.literal_eval(fd.read())[suite_name])
193 except KeyError:
194 return GetControlFileList(static_dir, build)
195
196
Scott Zawalski84a39c92012-01-13 15:12:42 -0500197def GetControlFileList(static_dir, build):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500198 """List all control|control. files in the specified board/build path.
199
200 Args:
201 static_dir: Directory where builds are served from.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500202 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
203
Scott Zawalski4647ce62012-01-03 17:17:28 -0500204 Returns:
205 String of each file separated by a newline.
Chris Sosafc715442014-04-09 20:45:23 -0700206
207 Raises:
208 CommonUtilError: If path is outside of sandbox.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500209 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500210 autotest_dir = os.path.join(static_dir, build, 'autotest/')
Chris Sosa76e44b92013-01-31 12:11:38 -0800211 if not PathInDir(static_dir, autotest_dir):
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700212 raise CommonUtilError('Autotest dir not in sandbox "%s".' % autotest_dir)
Scott Zawalski4647ce62012-01-03 17:17:28 -0500213
214 control_files = set()
Scott Zawalski84a39c92012-01-13 15:12:42 -0500215 if not os.path.exists(autotest_dir):
joychen3d164bd2013-06-24 18:12:23 -0700216 raise CommonUtilError('Could not find this directory.'
217 'Is it staged? %s' % autotest_dir)
Scott Zawalski84a39c92012-01-13 15:12:42 -0500218
Scott Zawalski4647ce62012-01-03 17:17:28 -0500219 for entry in os.walk(autotest_dir):
220 dir_path, _, files = entry
221 for file_entry in files:
222 if file_entry.startswith('control.') or file_entry == 'control':
223 control_files.add(os.path.join(dir_path,
Chris Sosaea148d92012-03-06 16:22:04 -0800224 file_entry).replace(autotest_dir, ''))
Scott Zawalski4647ce62012-01-03 17:17:28 -0500225
226 return '\n'.join(control_files)
227
228
Gilad Arnold55a2a372012-10-02 09:46:32 -0700229def GetFileSize(file_path):
230 """Returns the size in bytes of the file given."""
231 return os.path.getsize(file_path)
232
233
Chris Sosa6a3697f2013-01-29 16:44:43 -0800234# Hashlib is strange and doesn't actually define these in a sane way that
235# pylint can find them. Disable checks for them.
236# pylint: disable=E1101,W0106
Gilad Arnold55a2a372012-10-02 09:46:32 -0700237def GetFileHashes(file_path, do_sha1=False, do_sha256=False, do_md5=False):
238 """Computes and returns a list of requested hashes.
239
240 Args:
241 file_path: path to file to be hashed
Chris Sosafc715442014-04-09 20:45:23 -0700242 do_sha1: whether or not to compute a SHA1 hash
Gilad Arnold55a2a372012-10-02 09:46:32 -0700243 do_sha256: whether or not to compute a SHA256 hash
Chris Sosafc715442014-04-09 20:45:23 -0700244 do_md5: whether or not to compute a MD5 hash
245
Gilad Arnold55a2a372012-10-02 09:46:32 -0700246 Returns:
247 A dictionary containing binary hash values, keyed by 'sha1', 'sha256' and
248 'md5', respectively.
249 """
250 hashes = {}
xixuan8a132922016-11-01 09:51:11 -0700251 if any((do_sha1, do_sha256, do_md5)):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700252 # Initialize hashers.
253 hasher_sha1 = hashlib.sha1() if do_sha1 else None
254 hasher_sha256 = hashlib.sha256() if do_sha256 else None
255 hasher_md5 = hashlib.md5() if do_md5 else None
256
257 # Read blocks from file, update hashes.
258 with open(file_path, 'rb') as fd:
259 while True:
260 block = fd.read(_HASH_BLOCK_SIZE)
261 if not block:
262 break
263 hasher_sha1 and hasher_sha1.update(block)
264 hasher_sha256 and hasher_sha256.update(block)
265 hasher_md5 and hasher_md5.update(block)
266
267 # Update return values.
268 if hasher_sha1:
269 hashes['sha1'] = hasher_sha1.digest()
270 if hasher_sha256:
271 hashes['sha256'] = hasher_sha256.digest()
272 if hasher_md5:
273 hashes['md5'] = hasher_md5.digest()
274
275 return hashes
276
277
278def GetFileSha1(file_path):
279 """Returns the SHA1 checksum of the file given (base64 encoded)."""
280 return base64.b64encode(GetFileHashes(file_path, do_sha1=True)['sha1'])
281
282
283def GetFileSha256(file_path):
284 """Returns the SHA256 checksum of the file given (base64 encoded)."""
285 return base64.b64encode(GetFileHashes(file_path, do_sha256=True)['sha256'])
286
287
288def GetFileMd5(file_path):
289 """Returns the MD5 checksum of the file given (hex encoded)."""
290 return binascii.hexlify(GetFileHashes(file_path, do_md5=True)['md5'])
291
292
293def CopyFile(source, dest):
294 """Copies a file from |source| to |dest|."""
295 _Log('Copy File %s -> %s' % (source, dest))
296 shutil.copy(source, dest)
Chris Sosa76e44b92013-01-31 12:11:38 -0800297
298
Alex Deymo3e2d4952013-09-03 21:49:41 -0700299def SymlinkFile(target, link):
300 """Atomically creates or replaces the symlink |link| pointing to |target|.
301
302 If the specified |link| file already exists it is replaced with the new link
303 atomically.
304 """
305 if not os.path.exists(target):
Chris Sosa75490802013-09-30 17:21:45 -0700306 _Log('Could not find target for symlink: %s', target)
Alex Deymo3e2d4952013-09-03 21:49:41 -0700307 return
Chris Sosa75490802013-09-30 17:21:45 -0700308
Alex Deymo3e2d4952013-09-03 21:49:41 -0700309 _Log('Creating symlink: %s --> %s', link, target)
310
311 # Use the created link_base file to prevent other calls to SymlinkFile() to
312 # pick the same link_base temp file, thanks to mkstemp().
313 with tempfile.NamedTemporaryFile(prefix=os.path.basename(link)) as link_fd:
314 link_base = link_fd.name
315
316 # Use the unique link_base filename to create a symlink, but on the same
317 # directory as the required |link| to ensure the created symlink is in the
318 # same file system as |link|.
319 link_name = os.path.join(os.path.dirname(link),
Achuith Bhandarkar1a8a7f92019-09-26 15:35:23 +0200320 os.path.basename(link_base) + '-link')
Alex Deymo3e2d4952013-09-03 21:49:41 -0700321
322 # Create the symlink and then rename it to the final position. This ensures
323 # the symlink creation is atomic.
324 os.symlink(target, link_name)
325 os.rename(link_name, link)
326
327
Chris Sosa76e44b92013-01-31 12:11:38 -0800328class LockDict(object):
329 """A dictionary of locks.
330
331 This class provides a thread-safe store of threading.Lock objects, which can
332 be used to regulate access to any set of hashable resources. Usage:
333
334 foo_lock_dict = LockDict()
335 ...
336 with foo_lock_dict.lock('bar'):
337 # Critical section for 'bar'
338 """
339 def __init__(self):
340 self._lock = self._new_lock()
341 self._dict = {}
342
343 @staticmethod
344 def _new_lock():
345 return threading.Lock()
346
347 def lock(self, key):
348 with self._lock:
349 lock = self._dict.get(key)
350 if not lock:
351 lock = self._new_lock()
352 self._dict[key] = lock
353 return lock
Simran Basi4baad082013-02-14 13:39:18 -0800354
355
356def ExtractTarball(tarball_path, install_path, files_to_extract=None,
Gilad Arnold1638d822013-11-07 23:38:16 -0800357 excluded_files=None, return_extracted_files=False):
Simran Basi4baad082013-02-14 13:39:18 -0800358 """Extracts a tarball using tar.
359
360 Detects whether the tarball is compressed or not based on the file
361 extension and extracts the tarball into the install_path.
362
363 Args:
364 tarball_path: Path to the tarball to extract.
365 install_path: Path to extract the tarball to.
366 files_to_extract: String of specific files in the tarball to extract.
367 excluded_files: String of files to not extract.
Chris Sosafc715442014-04-09 20:45:23 -0700368 return_extracted_files: whether or not the caller expects the list of
Gilad Arnold1638d822013-11-07 23:38:16 -0800369 files extracted; if False, returns an empty list.
Chris Sosafc715442014-04-09 20:45:23 -0700370
Gilad Arnold1638d822013-11-07 23:38:16 -0800371 Returns:
372 List of absolute paths of the files extracted (possibly empty).
Simran Basi4baad082013-02-14 13:39:18 -0800373 """
374 # Deal with exclusions.
xixuan8a132922016-11-01 09:51:11 -0700375 # Add 'm' for not extracting file's modified time. All extracted files are
376 # marked with current system time.
xixuanc316d992016-11-17 16:19:00 -0800377 cmd = ['tar', 'xf', tarball_path, '--directory', install_path]
Simran Basi4baad082013-02-14 13:39:18 -0800378
Gilad Arnold1638d822013-11-07 23:38:16 -0800379 # If caller requires the list of extracted files, get verbose.
380 if return_extracted_files:
381 cmd += ['--verbose']
382
Simran Basi4baad082013-02-14 13:39:18 -0800383 # Determine how to decompress.
384 tarball = os.path.basename(tarball_path)
385 if tarball.endswith('.tar.bz2'):
386 cmd.append('--use-compress-prog=pbzip2')
387 elif tarball.endswith('.tgz') or tarball.endswith('.tar.gz'):
388 cmd.append('--gzip')
389
390 if excluded_files:
391 for exclude in excluded_files:
392 cmd.extend(['--exclude', exclude])
393
394 if files_to_extract:
395 cmd.extend(files_to_extract)
396
Aseda Aboagyeb6ba5492016-10-14 13:20:09 -0700397 cmd_output = ''
Simran Basi4baad082013-02-14 13:39:18 -0800398 try:
xixuanc316d992016-11-17 16:19:00 -0800399 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
400 stderr=subprocess.PIPE)
401 cmd_output, cmd_error = proc.communicate()
402 if cmd_error:
403 _Log('Error happened while in extracting tarball: %s',
404 cmd_error.rstrip())
405
Gilad Arnold1638d822013-11-07 23:38:16 -0800406 if return_extracted_files:
407 return [os.path.join(install_path, filename)
408 for filename in cmd_output.strip('\n').splitlines()
409 if not filename.endswith('/')]
410 return []
Simran Basi4baad082013-02-14 13:39:18 -0800411 except subprocess.CalledProcessError, e:
412 raise CommonUtilError(
Aseda Aboagyeb6ba5492016-10-14 13:20:09 -0700413 'An error occurred when attempting to untar %s:\n%s\n%s' %
414 (tarball_path, e, e.output))
joychen7c2054a2013-07-25 11:14:07 -0700415
416
417def IsInsideChroot():
418 """Returns True if we are inside chroot."""
419 return os.path.exists('/etc/debian_chroot')
xixuan178263c2017-03-22 09:10:25 -0700420
421
422def IsRunningOnMoblab():
423 """Returns True if this code is running on a chromiumOS DUT."""
424 cmd = ['cat', '/etc/lsb-release']
425 try:
426 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
427 stderr=subprocess.PIPE)
428 cmd_output, cmd_error = proc.communicate()
429
430 if cmd_error:
431 _Log('Error happened while reading lsb-release file: %s',
432 cmd_error.rstrip())
433 return False
434
Achuith Bhandarkar1a8a7f92019-09-26 15:35:23 +0200435 return bool(re.search(r'[_-]+moblab', cmd_output))
xixuan178263c2017-03-22 09:10:25 -0700436 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