blob: f60014c2e836472b36978dd94fa70db2dea82066 [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
41 Args:
42 status: HTTPResponse status.
43 message: Message associated with the response.
44 """
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
120 Raises:
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700121 CommonUtilError: If lock can't be acquired.
Frank Farzan37761d12011-12-01 14:29:08 -0800122
123 Returns:
124 Content of the requested control file.
125 """
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
154 Raises:
155 CommonUtilError: If the suite_to_control_file_map isn't found in
156 the specified build's staged directory.
157
158 Returns:
159 String of each control file separated by a newline.
160 """
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
187 Raises:
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700188 CommonUtilError: If path is outside of sandbox.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500189
190 Returns:
191 String of each file separated by a newline.
192 """
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
225 do_sha1: whether or not to compute a SHA1 hash
226 do_sha256: whether or not to compute a SHA256 hash
227 do_md5: whether or not to compute a MD5 hash
228 Returns:
229 A dictionary containing binary hash values, keyed by 'sha1', 'sha256' and
230 'md5', respectively.
231 """
232 hashes = {}
233 if (do_sha1 or do_sha256 or do_md5):
234 # Initialize hashers.
235 hasher_sha1 = hashlib.sha1() if do_sha1 else None
236 hasher_sha256 = hashlib.sha256() if do_sha256 else None
237 hasher_md5 = hashlib.md5() if do_md5 else None
238
239 # Read blocks from file, update hashes.
240 with open(file_path, 'rb') as fd:
241 while True:
242 block = fd.read(_HASH_BLOCK_SIZE)
243 if not block:
244 break
245 hasher_sha1 and hasher_sha1.update(block)
246 hasher_sha256 and hasher_sha256.update(block)
247 hasher_md5 and hasher_md5.update(block)
248
249 # Update return values.
250 if hasher_sha1:
251 hashes['sha1'] = hasher_sha1.digest()
252 if hasher_sha256:
253 hashes['sha256'] = hasher_sha256.digest()
254 if hasher_md5:
255 hashes['md5'] = hasher_md5.digest()
256
257 return hashes
258
259
260def GetFileSha1(file_path):
261 """Returns the SHA1 checksum of the file given (base64 encoded)."""
262 return base64.b64encode(GetFileHashes(file_path, do_sha1=True)['sha1'])
263
264
265def GetFileSha256(file_path):
266 """Returns the SHA256 checksum of the file given (base64 encoded)."""
267 return base64.b64encode(GetFileHashes(file_path, do_sha256=True)['sha256'])
268
269
270def GetFileMd5(file_path):
271 """Returns the MD5 checksum of the file given (hex encoded)."""
272 return binascii.hexlify(GetFileHashes(file_path, do_md5=True)['md5'])
273
274
275def CopyFile(source, dest):
276 """Copies a file from |source| to |dest|."""
277 _Log('Copy File %s -> %s' % (source, dest))
278 shutil.copy(source, dest)
Chris Sosa76e44b92013-01-31 12:11:38 -0800279
280
Alex Deymo3e2d4952013-09-03 21:49:41 -0700281def SymlinkFile(target, link):
282 """Atomically creates or replaces the symlink |link| pointing to |target|.
283
284 If the specified |link| file already exists it is replaced with the new link
285 atomically.
286 """
287 if not os.path.exists(target):
Chris Sosa75490802013-09-30 17:21:45 -0700288 _Log('Could not find target for symlink: %s', target)
Alex Deymo3e2d4952013-09-03 21:49:41 -0700289 return
Chris Sosa75490802013-09-30 17:21:45 -0700290
Alex Deymo3e2d4952013-09-03 21:49:41 -0700291 _Log('Creating symlink: %s --> %s', link, target)
292
293 # Use the created link_base file to prevent other calls to SymlinkFile() to
294 # pick the same link_base temp file, thanks to mkstemp().
295 with tempfile.NamedTemporaryFile(prefix=os.path.basename(link)) as link_fd:
296 link_base = link_fd.name
297
298 # Use the unique link_base filename to create a symlink, but on the same
299 # directory as the required |link| to ensure the created symlink is in the
300 # same file system as |link|.
301 link_name = os.path.join(os.path.dirname(link),
302 os.path.basename(link_base) + "-link")
303
304 # Create the symlink and then rename it to the final position. This ensures
305 # the symlink creation is atomic.
306 os.symlink(target, link_name)
307 os.rename(link_name, link)
308
309
Chris Sosa76e44b92013-01-31 12:11:38 -0800310class LockDict(object):
311 """A dictionary of locks.
312
313 This class provides a thread-safe store of threading.Lock objects, which can
314 be used to regulate access to any set of hashable resources. Usage:
315
316 foo_lock_dict = LockDict()
317 ...
318 with foo_lock_dict.lock('bar'):
319 # Critical section for 'bar'
320 """
321 def __init__(self):
322 self._lock = self._new_lock()
323 self._dict = {}
324
325 @staticmethod
326 def _new_lock():
327 return threading.Lock()
328
329 def lock(self, key):
330 with self._lock:
331 lock = self._dict.get(key)
332 if not lock:
333 lock = self._new_lock()
334 self._dict[key] = lock
335 return lock
Simran Basi4baad082013-02-14 13:39:18 -0800336
337
338def ExtractTarball(tarball_path, install_path, files_to_extract=None,
Gilad Arnold1638d822013-11-07 23:38:16 -0800339 excluded_files=None, return_extracted_files=False):
Simran Basi4baad082013-02-14 13:39:18 -0800340 """Extracts a tarball using tar.
341
342 Detects whether the tarball is compressed or not based on the file
343 extension and extracts the tarball into the install_path.
344
345 Args:
346 tarball_path: Path to the tarball to extract.
347 install_path: Path to extract the tarball to.
348 files_to_extract: String of specific files in the tarball to extract.
349 excluded_files: String of files to not extract.
Gilad Arnold1638d822013-11-07 23:38:16 -0800350 return_extracted_file: Whether or not the caller expects the list of
351 files extracted; if False, returns an empty list.
352 Returns:
353 List of absolute paths of the files extracted (possibly empty).
Simran Basi4baad082013-02-14 13:39:18 -0800354 """
355 # Deal with exclusions.
356 cmd = ['tar', 'xf', tarball_path, '--directory', install_path]
357
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
376 try:
Gilad Arnold1638d822013-11-07 23:38:16 -0800377 cmd_output = subprocess.check_output(cmd)
378 if return_extracted_files:
379 return [os.path.join(install_path, filename)
380 for filename in cmd_output.strip('\n').splitlines()
381 if not filename.endswith('/')]
382 return []
Simran Basi4baad082013-02-14 13:39:18 -0800383 except subprocess.CalledProcessError, e:
384 raise CommonUtilError(
385 'An error occurred when attempting to untar %s:\n%s' %
joychen3d164bd2013-06-24 18:12:23 -0700386 (tarball_path, e))
joychen7c2054a2013-07-25 11:14:07 -0700387
388
389def IsInsideChroot():
390 """Returns True if we are inside chroot."""
391 return os.path.exists('/etc/debian_chroot')