blob: eae74113c064dd7b7f8c87744d87b846bfa763b4 [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):
50 """Thread-safely create a directory like mkdir -p."""
Frank Farzan37761d12011-12-01 14:29:08 -080051 try:
Chris Sosa76e44b92013-01-31 12:11:38 -080052 os.makedirs(directory)
Frank Farzan37761d12011-12-01 14:29:08 -080053 except OSError, e:
Chris Sosa76e44b92013-01-31 12:11:38 -080054 if not (e.errno == errno.EEXIST and os.path.isdir(directory)):
Frank Farzan37761d12011-12-01 14:29:08 -080055 raise
56
Frank Farzan37761d12011-12-01 14:29:08 -080057
Scott Zawalski16954532012-03-20 15:31:36 -040058def GetLatestBuildVersion(static_dir, target, milestone=None):
Frank Farzan37761d12011-12-01 14:29:08 -080059 """Retrieves the latest build version for a given board.
60
joychen921e1fb2013-06-28 11:12:20 -070061 Searches the static_dir for builds for target, and returns the highest
62 version number currently available locally.
63
Frank Farzan37761d12011-12-01 14:29:08 -080064 Args:
65 static_dir: Directory where builds are served from.
Scott Zawalski16954532012-03-20 15:31:36 -040066 target: The build target, typically a combination of the board and the
67 type of build e.g. x86-mario-release.
68 milestone: For latest build set to None, for builds only in a specific
69 milestone set to a str of format Rxx (e.g. R16). Default: None.
Frank Farzan37761d12011-12-01 14:29:08 -080070
71 Returns:
Scott Zawalski16954532012-03-20 15:31:36 -040072 If latest found, a full build string is returned e.g. R17-1234.0.0-a1-b983.
73 If no latest is found for some reason or another a '' string is returned.
Frank Farzan37761d12011-12-01 14:29:08 -080074
75 Raises:
Gilad Arnold17fe03d2012-10-02 10:05:01 -070076 CommonUtilError: If for some reason the latest build cannot be
Scott Zawalski16954532012-03-20 15:31:36 -040077 deteremined, this could be due to the dir not existing or no builds
78 being present after filtering on milestone.
Frank Farzan37761d12011-12-01 14:29:08 -080079 """
Scott Zawalski16954532012-03-20 15:31:36 -040080 target_path = os.path.join(static_dir, target)
81 if not os.path.isdir(target_path):
Gilad Arnold17fe03d2012-10-02 10:05:01 -070082 raise CommonUtilError('Cannot find path %s' % target_path)
Frank Farzan37761d12011-12-01 14:29:08 -080083
Scott Zawalski16954532012-03-20 15:31:36 -040084 builds = [distutils.version.LooseVersion(build) for build in
Dan Shi9fa4bde2013-12-02 13:40:07 -080085 os.listdir(target_path) if not build.endswith('.exception')]
Frank Farzan37761d12011-12-01 14:29:08 -080086
Scott Zawalski16954532012-03-20 15:31:36 -040087 if milestone and builds:
88 # Check if milestone Rxx is in the string representation of the build.
89 builds = filter(lambda x: milestone.upper() in str(x), builds)
Frank Farzan37761d12011-12-01 14:29:08 -080090
Scott Zawalski16954532012-03-20 15:31:36 -040091 if not builds:
Gilad Arnold17fe03d2012-10-02 10:05:01 -070092 raise CommonUtilError('Could not determine build for %s' % target)
Frank Farzan37761d12011-12-01 14:29:08 -080093
Scott Zawalski16954532012-03-20 15:31:36 -040094 return str(max(builds))
Frank Farzan37761d12011-12-01 14:29:08 -080095
96
Chris Sosa76e44b92013-01-31 12:11:38 -080097def PathInDir(directory, path):
98 """Returns True if the path is in directory.
99
100 Args:
101 directory: Directory where the path should be in.
102 path: Path to check.
103
104 Returns:
105 True if path is in static_dir, False otherwise
106 """
107 directory = os.path.realpath(directory)
108 path = os.path.realpath(path)
109 return (path.startswith(directory) and len(path) != len(directory))
110
111
Scott Zawalski84a39c92012-01-13 15:12:42 -0500112def GetControlFile(static_dir, build, control_path):
Frank Farzan37761d12011-12-01 14:29:08 -0800113 """Attempts to pull the requested control file from the Dev Server.
114
115 Args:
116 static_dir: Directory where builds are served from.
Frank Farzan37761d12011-12-01 14:29:08 -0800117 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
118 control_path: Path to control file on Dev Server relative to Autotest root.
119
Frank Farzan37761d12011-12-01 14:29:08 -0800120 Returns:
121 Content of the requested control file.
Chris Sosafc715442014-04-09 20:45:23 -0700122
123 Raises:
124 CommonUtilError: If lock can't be acquired.
Frank Farzan37761d12011-12-01 14:29:08 -0800125 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500126 # Be forgiving if the user passes in the control_path with a leading /
127 control_path = control_path.lstrip('/')
Scott Zawalski84a39c92012-01-13 15:12:42 -0500128 control_path = os.path.join(static_dir, build, 'autotest',
Scott Zawalski4647ce62012-01-03 17:17:28 -0500129 control_path)
Chris Sosa76e44b92013-01-31 12:11:38 -0800130 if not PathInDir(static_dir, control_path):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700131 raise CommonUtilError('Invalid control file "%s".' % control_path)
Frank Farzan37761d12011-12-01 14:29:08 -0800132
Scott Zawalski84a39c92012-01-13 15:12:42 -0500133 if not os.path.exists(control_path):
134 # TODO(scottz): Come up with some sort of error mechanism.
135 # crosbug.com/25040
136 return 'Unknown control path %s' % control_path
137
Frank Farzan37761d12011-12-01 14:29:08 -0800138 with open(control_path, 'r') as control_file:
139 return control_file.read()
140
141
beepsbd337242013-07-09 22:44:06 -0700142def GetControlFileListForSuite(static_dir, build, suite_name):
143 """List all control files for a specified build, for the given suite.
144
145 If the specified suite_name isn't found in the suite to control file
146 map, this method will return all control files for the build by calling
147 GetControlFileList.
148
149 Args:
150 static_dir: Directory where builds are served from.
151 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
152 suite_name: Name of the suite for which we require control files.
153
Chris Sosafc715442014-04-09 20:45:23 -0700154 Returns:
155 String of each control file separated by a newline.
156
beepsbd337242013-07-09 22:44:06 -0700157 Raises:
158 CommonUtilError: If the suite_to_control_file_map isn't found in
159 the specified build's staged directory.
beepsbd337242013-07-09 22:44:06 -0700160 """
161 suite_to_control_map = os.path.join(static_dir, build,
162 'autotest', 'test_suites',
163 'suite_to_control_file_map')
164
165 if not PathInDir(static_dir, suite_to_control_map):
166 raise CommonUtilError('suite_to_control_map not in "%s".' %
167 suite_to_control_map)
168
169 if not os.path.exists(suite_to_control_map):
170 raise CommonUtilError('Could not find this file. '
171 'Is it staged? %s' % suite_to_control_map)
172
173 with open(suite_to_control_map, 'r') as fd:
174 try:
175 return '\n'.join(ast.literal_eval(fd.read())[suite_name])
176 except KeyError:
177 return GetControlFileList(static_dir, build)
178
179
Scott Zawalski84a39c92012-01-13 15:12:42 -0500180def GetControlFileList(static_dir, build):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500181 """List all control|control. files in the specified board/build path.
182
183 Args:
184 static_dir: Directory where builds are served from.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500185 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
186
Scott Zawalski4647ce62012-01-03 17:17:28 -0500187 Returns:
188 String of each file separated by a newline.
Chris Sosafc715442014-04-09 20:45:23 -0700189
190 Raises:
191 CommonUtilError: If path is outside of sandbox.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500192 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500193 autotest_dir = os.path.join(static_dir, build, 'autotest/')
Chris Sosa76e44b92013-01-31 12:11:38 -0800194 if not PathInDir(static_dir, autotest_dir):
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700195 raise CommonUtilError('Autotest dir not in sandbox "%s".' % autotest_dir)
Scott Zawalski4647ce62012-01-03 17:17:28 -0500196
197 control_files = set()
Scott Zawalski84a39c92012-01-13 15:12:42 -0500198 if not os.path.exists(autotest_dir):
joychen3d164bd2013-06-24 18:12:23 -0700199 raise CommonUtilError('Could not find this directory.'
200 'Is it staged? %s' % autotest_dir)
Scott Zawalski84a39c92012-01-13 15:12:42 -0500201
Scott Zawalski4647ce62012-01-03 17:17:28 -0500202 for entry in os.walk(autotest_dir):
203 dir_path, _, files = entry
204 for file_entry in files:
205 if file_entry.startswith('control.') or file_entry == 'control':
206 control_files.add(os.path.join(dir_path,
Chris Sosaea148d92012-03-06 16:22:04 -0800207 file_entry).replace(autotest_dir, ''))
Scott Zawalski4647ce62012-01-03 17:17:28 -0500208
209 return '\n'.join(control_files)
210
211
Gilad Arnold55a2a372012-10-02 09:46:32 -0700212def GetFileSize(file_path):
213 """Returns the size in bytes of the file given."""
214 return os.path.getsize(file_path)
215
216
Chris Sosa6a3697f2013-01-29 16:44:43 -0800217# Hashlib is strange and doesn't actually define these in a sane way that
218# pylint can find them. Disable checks for them.
219# pylint: disable=E1101,W0106
Gilad Arnold55a2a372012-10-02 09:46:32 -0700220def GetFileHashes(file_path, do_sha1=False, do_sha256=False, do_md5=False):
221 """Computes and returns a list of requested hashes.
222
223 Args:
224 file_path: path to file to be hashed
Chris Sosafc715442014-04-09 20:45:23 -0700225 do_sha1: whether or not to compute a SHA1 hash
Gilad Arnold55a2a372012-10-02 09:46:32 -0700226 do_sha256: whether or not to compute a SHA256 hash
Chris Sosafc715442014-04-09 20:45:23 -0700227 do_md5: whether or not to compute a MD5 hash
228
Gilad Arnold55a2a372012-10-02 09:46:32 -0700229 Returns:
230 A dictionary containing binary hash values, keyed by 'sha1', 'sha256' and
231 'md5', respectively.
232 """
233 hashes = {}
234 if (do_sha1 or do_sha256 or do_md5):
235 # Initialize hashers.
236 hasher_sha1 = hashlib.sha1() if do_sha1 else None
237 hasher_sha256 = hashlib.sha256() if do_sha256 else None
238 hasher_md5 = hashlib.md5() if do_md5 else None
239
240 # Read blocks from file, update hashes.
241 with open(file_path, 'rb') as fd:
242 while True:
243 block = fd.read(_HASH_BLOCK_SIZE)
244 if not block:
245 break
246 hasher_sha1 and hasher_sha1.update(block)
247 hasher_sha256 and hasher_sha256.update(block)
248 hasher_md5 and hasher_md5.update(block)
249
250 # Update return values.
251 if hasher_sha1:
252 hashes['sha1'] = hasher_sha1.digest()
253 if hasher_sha256:
254 hashes['sha256'] = hasher_sha256.digest()
255 if hasher_md5:
256 hashes['md5'] = hasher_md5.digest()
257
258 return hashes
259
260
261def GetFileSha1(file_path):
262 """Returns the SHA1 checksum of the file given (base64 encoded)."""
263 return base64.b64encode(GetFileHashes(file_path, do_sha1=True)['sha1'])
264
265
266def GetFileSha256(file_path):
267 """Returns the SHA256 checksum of the file given (base64 encoded)."""
268 return base64.b64encode(GetFileHashes(file_path, do_sha256=True)['sha256'])
269
270
271def GetFileMd5(file_path):
272 """Returns the MD5 checksum of the file given (hex encoded)."""
273 return binascii.hexlify(GetFileHashes(file_path, do_md5=True)['md5'])
274
275
276def CopyFile(source, dest):
277 """Copies a file from |source| to |dest|."""
278 _Log('Copy File %s -> %s' % (source, dest))
279 shutil.copy(source, dest)
Chris Sosa76e44b92013-01-31 12:11:38 -0800280
281
Alex Deymo3e2d4952013-09-03 21:49:41 -0700282def SymlinkFile(target, link):
283 """Atomically creates or replaces the symlink |link| pointing to |target|.
284
285 If the specified |link| file already exists it is replaced with the new link
286 atomically.
287 """
288 if not os.path.exists(target):
Chris Sosa75490802013-09-30 17:21:45 -0700289 _Log('Could not find target for symlink: %s', target)
Alex Deymo3e2d4952013-09-03 21:49:41 -0700290 return
Chris Sosa75490802013-09-30 17:21:45 -0700291
Alex Deymo3e2d4952013-09-03 21:49:41 -0700292 _Log('Creating symlink: %s --> %s', link, target)
293
294 # Use the created link_base file to prevent other calls to SymlinkFile() to
295 # pick the same link_base temp file, thanks to mkstemp().
296 with tempfile.NamedTemporaryFile(prefix=os.path.basename(link)) as link_fd:
297 link_base = link_fd.name
298
299 # Use the unique link_base filename to create a symlink, but on the same
300 # directory as the required |link| to ensure the created symlink is in the
301 # same file system as |link|.
302 link_name = os.path.join(os.path.dirname(link),
303 os.path.basename(link_base) + "-link")
304
305 # Create the symlink and then rename it to the final position. This ensures
306 # the symlink creation is atomic.
307 os.symlink(target, link_name)
308 os.rename(link_name, link)
309
310
Chris Sosa76e44b92013-01-31 12:11:38 -0800311class LockDict(object):
312 """A dictionary of locks.
313
314 This class provides a thread-safe store of threading.Lock objects, which can
315 be used to regulate access to any set of hashable resources. Usage:
316
317 foo_lock_dict = LockDict()
318 ...
319 with foo_lock_dict.lock('bar'):
320 # Critical section for 'bar'
321 """
322 def __init__(self):
323 self._lock = self._new_lock()
324 self._dict = {}
325
326 @staticmethod
327 def _new_lock():
328 return threading.Lock()
329
330 def lock(self, key):
331 with self._lock:
332 lock = self._dict.get(key)
333 if not lock:
334 lock = self._new_lock()
335 self._dict[key] = lock
336 return lock
Simran Basi4baad082013-02-14 13:39:18 -0800337
338
339def ExtractTarball(tarball_path, install_path, files_to_extract=None,
Gilad Arnold1638d822013-11-07 23:38:16 -0800340 excluded_files=None, return_extracted_files=False):
Simran Basi4baad082013-02-14 13:39:18 -0800341 """Extracts a tarball using tar.
342
343 Detects whether the tarball is compressed or not based on the file
344 extension and extracts the tarball into the install_path.
345
346 Args:
347 tarball_path: Path to the tarball to extract.
348 install_path: Path to extract the tarball to.
349 files_to_extract: String of specific files in the tarball to extract.
350 excluded_files: String of files to not extract.
Chris Sosafc715442014-04-09 20:45:23 -0700351 return_extracted_files: whether or not the caller expects the list of
Gilad Arnold1638d822013-11-07 23:38:16 -0800352 files extracted; if False, returns an empty list.
Chris Sosafc715442014-04-09 20:45:23 -0700353
Gilad Arnold1638d822013-11-07 23:38:16 -0800354 Returns:
355 List of absolute paths of the files extracted (possibly empty).
Simran Basi4baad082013-02-14 13:39:18 -0800356 """
357 # Deal with exclusions.
358 cmd = ['tar', 'xf', tarball_path, '--directory', install_path]
359
Gilad Arnold1638d822013-11-07 23:38:16 -0800360 # If caller requires the list of extracted files, get verbose.
361 if return_extracted_files:
362 cmd += ['--verbose']
363
Simran Basi4baad082013-02-14 13:39:18 -0800364 # Determine how to decompress.
365 tarball = os.path.basename(tarball_path)
366 if tarball.endswith('.tar.bz2'):
367 cmd.append('--use-compress-prog=pbzip2')
368 elif tarball.endswith('.tgz') or tarball.endswith('.tar.gz'):
369 cmd.append('--gzip')
370
371 if excluded_files:
372 for exclude in excluded_files:
373 cmd.extend(['--exclude', exclude])
374
375 if files_to_extract:
376 cmd.extend(files_to_extract)
377
378 try:
Gilad Arnold1638d822013-11-07 23:38:16 -0800379 cmd_output = subprocess.check_output(cmd)
380 if return_extracted_files:
381 return [os.path.join(install_path, filename)
382 for filename in cmd_output.strip('\n').splitlines()
383 if not filename.endswith('/')]
384 return []
Simran Basi4baad082013-02-14 13:39:18 -0800385 except subprocess.CalledProcessError, e:
386 raise CommonUtilError(
387 'An error occurred when attempting to untar %s:\n%s' %
joychen3d164bd2013-06-24 18:12:23 -0700388 (tarball_path, e))
joychen7c2054a2013-07-25 11:14:07 -0700389
390
391def IsInsideChroot():
392 """Returns True if we are inside chroot."""
393 return os.path.exists('/etc/debian_chroot')