blob: 7b3c7b262afa3c4cd3d502c557bf2c4c04da68f5 [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
Frank Farzan37761d12011-12-01 14:29:08 -080010import distutils.version
11import errno
Gilad Arnold55a2a372012-10-02 09:46:32 -070012import hashlib
Frank Farzan37761d12011-12-01 14:29:08 -080013import os
14import shutil
Alex Deymo3e2d4952013-09-03 21:49:41 -070015import tempfile
Chris Sosa76e44b92013-01-31 12:11:38 -080016import threading
Simran Basi4baad082013-02-14 13:39:18 -080017import subprocess
Frank Farzan37761d12011-12-01 14:29:08 -080018
Gilad Arnoldc65330c2012-09-20 15:17:48 -070019import log_util
20
21
22# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080023def _Log(message, *args):
24 return log_util.LogWithTag('UTIL', message, *args)
Gilad Arnoldc65330c2012-09-20 15:17:48 -070025
Frank Farzan37761d12011-12-01 14:29:08 -080026
Gilad Arnold55a2a372012-10-02 09:46:32 -070027_HASH_BLOCK_SIZE = 8192
28
Gilad Arnold6f99b982012-09-12 10:49:40 -070029
Gilad Arnold17fe03d2012-10-02 10:05:01 -070030class CommonUtilError(Exception):
Frank Farzan37761d12011-12-01 14:29:08 -080031 """Exception classes used by this module."""
32 pass
33
34
Chris Sosa76e44b92013-01-31 12:11:38 -080035def MkDirP(directory):
36 """Thread-safely create a directory like mkdir -p."""
Frank Farzan37761d12011-12-01 14:29:08 -080037 try:
Chris Sosa76e44b92013-01-31 12:11:38 -080038 os.makedirs(directory)
Frank Farzan37761d12011-12-01 14:29:08 -080039 except OSError, e:
Chris Sosa76e44b92013-01-31 12:11:38 -080040 if not (e.errno == errno.EEXIST and os.path.isdir(directory)):
Frank Farzan37761d12011-12-01 14:29:08 -080041 raise
42
Frank Farzan37761d12011-12-01 14:29:08 -080043
Scott Zawalski16954532012-03-20 15:31:36 -040044def GetLatestBuildVersion(static_dir, target, milestone=None):
Frank Farzan37761d12011-12-01 14:29:08 -080045 """Retrieves the latest build version for a given board.
46
joychen921e1fb2013-06-28 11:12:20 -070047 Searches the static_dir for builds for target, and returns the highest
48 version number currently available locally.
49
Frank Farzan37761d12011-12-01 14:29:08 -080050 Args:
51 static_dir: Directory where builds are served from.
Scott Zawalski16954532012-03-20 15:31:36 -040052 target: The build target, typically a combination of the board and the
53 type of build e.g. x86-mario-release.
54 milestone: For latest build set to None, for builds only in a specific
55 milestone set to a str of format Rxx (e.g. R16). Default: None.
Frank Farzan37761d12011-12-01 14:29:08 -080056
57 Returns:
Scott Zawalski16954532012-03-20 15:31:36 -040058 If latest found, a full build string is returned e.g. R17-1234.0.0-a1-b983.
59 If no latest is found for some reason or another a '' string is returned.
Frank Farzan37761d12011-12-01 14:29:08 -080060
61 Raises:
Gilad Arnold17fe03d2012-10-02 10:05:01 -070062 CommonUtilError: If for some reason the latest build cannot be
Scott Zawalski16954532012-03-20 15:31:36 -040063 deteremined, this could be due to the dir not existing or no builds
64 being present after filtering on milestone.
Frank Farzan37761d12011-12-01 14:29:08 -080065 """
Scott Zawalski16954532012-03-20 15:31:36 -040066 target_path = os.path.join(static_dir, target)
67 if not os.path.isdir(target_path):
Gilad Arnold17fe03d2012-10-02 10:05:01 -070068 raise CommonUtilError('Cannot find path %s' % target_path)
Frank Farzan37761d12011-12-01 14:29:08 -080069
Scott Zawalski16954532012-03-20 15:31:36 -040070 builds = [distutils.version.LooseVersion(build) for build in
Dan Shi9fa4bde2013-12-02 13:40:07 -080071 os.listdir(target_path) if not build.endswith('.exception')]
Frank Farzan37761d12011-12-01 14:29:08 -080072
Scott Zawalski16954532012-03-20 15:31:36 -040073 if milestone and builds:
74 # Check if milestone Rxx is in the string representation of the build.
75 builds = filter(lambda x: milestone.upper() in str(x), builds)
Frank Farzan37761d12011-12-01 14:29:08 -080076
Scott Zawalski16954532012-03-20 15:31:36 -040077 if not builds:
Gilad Arnold17fe03d2012-10-02 10:05:01 -070078 raise CommonUtilError('Could not determine build for %s' % target)
Frank Farzan37761d12011-12-01 14:29:08 -080079
Scott Zawalski16954532012-03-20 15:31:36 -040080 return str(max(builds))
Frank Farzan37761d12011-12-01 14:29:08 -080081
82
Chris Sosa76e44b92013-01-31 12:11:38 -080083def PathInDir(directory, path):
84 """Returns True if the path is in directory.
85
86 Args:
87 directory: Directory where the path should be in.
88 path: Path to check.
89
90 Returns:
91 True if path is in static_dir, False otherwise
92 """
93 directory = os.path.realpath(directory)
94 path = os.path.realpath(path)
95 return (path.startswith(directory) and len(path) != len(directory))
96
97
Scott Zawalski84a39c92012-01-13 15:12:42 -050098def GetControlFile(static_dir, build, control_path):
Frank Farzan37761d12011-12-01 14:29:08 -080099 """Attempts to pull the requested control file from the Dev Server.
100
101 Args:
102 static_dir: Directory where builds are served from.
Frank Farzan37761d12011-12-01 14:29:08 -0800103 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
104 control_path: Path to control file on Dev Server relative to Autotest root.
105
106 Raises:
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700107 CommonUtilError: If lock can't be acquired.
Frank Farzan37761d12011-12-01 14:29:08 -0800108
109 Returns:
110 Content of the requested control file.
111 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500112 # Be forgiving if the user passes in the control_path with a leading /
113 control_path = control_path.lstrip('/')
Scott Zawalski84a39c92012-01-13 15:12:42 -0500114 control_path = os.path.join(static_dir, build, 'autotest',
Scott Zawalski4647ce62012-01-03 17:17:28 -0500115 control_path)
Chris Sosa76e44b92013-01-31 12:11:38 -0800116 if not PathInDir(static_dir, control_path):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700117 raise CommonUtilError('Invalid control file "%s".' % control_path)
Frank Farzan37761d12011-12-01 14:29:08 -0800118
Scott Zawalski84a39c92012-01-13 15:12:42 -0500119 if not os.path.exists(control_path):
120 # TODO(scottz): Come up with some sort of error mechanism.
121 # crosbug.com/25040
122 return 'Unknown control path %s' % control_path
123
Frank Farzan37761d12011-12-01 14:29:08 -0800124 with open(control_path, 'r') as control_file:
125 return control_file.read()
126
127
beepsbd337242013-07-09 22:44:06 -0700128def GetControlFileListForSuite(static_dir, build, suite_name):
129 """List all control files for a specified build, for the given suite.
130
131 If the specified suite_name isn't found in the suite to control file
132 map, this method will return all control files for the build by calling
133 GetControlFileList.
134
135 Args:
136 static_dir: Directory where builds are served from.
137 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
138 suite_name: Name of the suite for which we require control files.
139
140 Raises:
141 CommonUtilError: If the suite_to_control_file_map isn't found in
142 the specified build's staged directory.
143
144 Returns:
145 String of each control file separated by a newline.
146 """
147 suite_to_control_map = os.path.join(static_dir, build,
148 'autotest', 'test_suites',
149 'suite_to_control_file_map')
150
151 if not PathInDir(static_dir, suite_to_control_map):
152 raise CommonUtilError('suite_to_control_map not in "%s".' %
153 suite_to_control_map)
154
155 if not os.path.exists(suite_to_control_map):
156 raise CommonUtilError('Could not find this file. '
157 'Is it staged? %s' % suite_to_control_map)
158
159 with open(suite_to_control_map, 'r') as fd:
160 try:
161 return '\n'.join(ast.literal_eval(fd.read())[suite_name])
162 except KeyError:
163 return GetControlFileList(static_dir, build)
164
165
Scott Zawalski84a39c92012-01-13 15:12:42 -0500166def GetControlFileList(static_dir, build):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500167 """List all control|control. files in the specified board/build path.
168
169 Args:
170 static_dir: Directory where builds are served from.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500171 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
172
173 Raises:
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700174 CommonUtilError: If path is outside of sandbox.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500175
176 Returns:
177 String of each file separated by a newline.
178 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500179 autotest_dir = os.path.join(static_dir, build, 'autotest/')
Chris Sosa76e44b92013-01-31 12:11:38 -0800180 if not PathInDir(static_dir, autotest_dir):
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700181 raise CommonUtilError('Autotest dir not in sandbox "%s".' % autotest_dir)
Scott Zawalski4647ce62012-01-03 17:17:28 -0500182
183 control_files = set()
Scott Zawalski84a39c92012-01-13 15:12:42 -0500184 if not os.path.exists(autotest_dir):
joychen3d164bd2013-06-24 18:12:23 -0700185 raise CommonUtilError('Could not find this directory.'
186 'Is it staged? %s' % autotest_dir)
Scott Zawalski84a39c92012-01-13 15:12:42 -0500187
Scott Zawalski4647ce62012-01-03 17:17:28 -0500188 for entry in os.walk(autotest_dir):
189 dir_path, _, files = entry
190 for file_entry in files:
191 if file_entry.startswith('control.') or file_entry == 'control':
192 control_files.add(os.path.join(dir_path,
Chris Sosaea148d92012-03-06 16:22:04 -0800193 file_entry).replace(autotest_dir, ''))
Scott Zawalski4647ce62012-01-03 17:17:28 -0500194
195 return '\n'.join(control_files)
196
197
Gilad Arnold55a2a372012-10-02 09:46:32 -0700198def GetFileSize(file_path):
199 """Returns the size in bytes of the file given."""
200 return os.path.getsize(file_path)
201
202
Chris Sosa6a3697f2013-01-29 16:44:43 -0800203# Hashlib is strange and doesn't actually define these in a sane way that
204# pylint can find them. Disable checks for them.
205# pylint: disable=E1101,W0106
Gilad Arnold55a2a372012-10-02 09:46:32 -0700206def GetFileHashes(file_path, do_sha1=False, do_sha256=False, do_md5=False):
207 """Computes and returns a list of requested hashes.
208
209 Args:
210 file_path: path to file to be hashed
211 do_sha1: whether or not to compute a SHA1 hash
212 do_sha256: whether or not to compute a SHA256 hash
213 do_md5: whether or not to compute a MD5 hash
214 Returns:
215 A dictionary containing binary hash values, keyed by 'sha1', 'sha256' and
216 'md5', respectively.
217 """
218 hashes = {}
219 if (do_sha1 or do_sha256 or do_md5):
220 # Initialize hashers.
221 hasher_sha1 = hashlib.sha1() if do_sha1 else None
222 hasher_sha256 = hashlib.sha256() if do_sha256 else None
223 hasher_md5 = hashlib.md5() if do_md5 else None
224
225 # Read blocks from file, update hashes.
226 with open(file_path, 'rb') as fd:
227 while True:
228 block = fd.read(_HASH_BLOCK_SIZE)
229 if not block:
230 break
231 hasher_sha1 and hasher_sha1.update(block)
232 hasher_sha256 and hasher_sha256.update(block)
233 hasher_md5 and hasher_md5.update(block)
234
235 # Update return values.
236 if hasher_sha1:
237 hashes['sha1'] = hasher_sha1.digest()
238 if hasher_sha256:
239 hashes['sha256'] = hasher_sha256.digest()
240 if hasher_md5:
241 hashes['md5'] = hasher_md5.digest()
242
243 return hashes
244
245
246def GetFileSha1(file_path):
247 """Returns the SHA1 checksum of the file given (base64 encoded)."""
248 return base64.b64encode(GetFileHashes(file_path, do_sha1=True)['sha1'])
249
250
251def GetFileSha256(file_path):
252 """Returns the SHA256 checksum of the file given (base64 encoded)."""
253 return base64.b64encode(GetFileHashes(file_path, do_sha256=True)['sha256'])
254
255
256def GetFileMd5(file_path):
257 """Returns the MD5 checksum of the file given (hex encoded)."""
258 return binascii.hexlify(GetFileHashes(file_path, do_md5=True)['md5'])
259
260
261def CopyFile(source, dest):
262 """Copies a file from |source| to |dest|."""
263 _Log('Copy File %s -> %s' % (source, dest))
264 shutil.copy(source, dest)
Chris Sosa76e44b92013-01-31 12:11:38 -0800265
266
Alex Deymo3e2d4952013-09-03 21:49:41 -0700267def SymlinkFile(target, link):
268 """Atomically creates or replaces the symlink |link| pointing to |target|.
269
270 If the specified |link| file already exists it is replaced with the new link
271 atomically.
272 """
273 if not os.path.exists(target):
Chris Sosa75490802013-09-30 17:21:45 -0700274 _Log('Could not find target for symlink: %s', target)
Alex Deymo3e2d4952013-09-03 21:49:41 -0700275 return
Chris Sosa75490802013-09-30 17:21:45 -0700276
Alex Deymo3e2d4952013-09-03 21:49:41 -0700277 _Log('Creating symlink: %s --> %s', link, target)
278
279 # Use the created link_base file to prevent other calls to SymlinkFile() to
280 # pick the same link_base temp file, thanks to mkstemp().
281 with tempfile.NamedTemporaryFile(prefix=os.path.basename(link)) as link_fd:
282 link_base = link_fd.name
283
284 # Use the unique link_base filename to create a symlink, but on the same
285 # directory as the required |link| to ensure the created symlink is in the
286 # same file system as |link|.
287 link_name = os.path.join(os.path.dirname(link),
288 os.path.basename(link_base) + "-link")
289
290 # Create the symlink and then rename it to the final position. This ensures
291 # the symlink creation is atomic.
292 os.symlink(target, link_name)
293 os.rename(link_name, link)
294
295
Chris Sosa76e44b92013-01-31 12:11:38 -0800296class LockDict(object):
297 """A dictionary of locks.
298
299 This class provides a thread-safe store of threading.Lock objects, which can
300 be used to regulate access to any set of hashable resources. Usage:
301
302 foo_lock_dict = LockDict()
303 ...
304 with foo_lock_dict.lock('bar'):
305 # Critical section for 'bar'
306 """
307 def __init__(self):
308 self._lock = self._new_lock()
309 self._dict = {}
310
311 @staticmethod
312 def _new_lock():
313 return threading.Lock()
314
315 def lock(self, key):
316 with self._lock:
317 lock = self._dict.get(key)
318 if not lock:
319 lock = self._new_lock()
320 self._dict[key] = lock
321 return lock
Simran Basi4baad082013-02-14 13:39:18 -0800322
323
324def ExtractTarball(tarball_path, install_path, files_to_extract=None,
Gilad Arnold1638d822013-11-07 23:38:16 -0800325 excluded_files=None, return_extracted_files=False):
Simran Basi4baad082013-02-14 13:39:18 -0800326 """Extracts a tarball using tar.
327
328 Detects whether the tarball is compressed or not based on the file
329 extension and extracts the tarball into the install_path.
330
331 Args:
332 tarball_path: Path to the tarball to extract.
333 install_path: Path to extract the tarball to.
334 files_to_extract: String of specific files in the tarball to extract.
335 excluded_files: String of files to not extract.
Gilad Arnold1638d822013-11-07 23:38:16 -0800336 return_extracted_file: Whether or not the caller expects the list of
337 files extracted; if False, returns an empty list.
338 Returns:
339 List of absolute paths of the files extracted (possibly empty).
Simran Basi4baad082013-02-14 13:39:18 -0800340 """
341 # Deal with exclusions.
342 cmd = ['tar', 'xf', tarball_path, '--directory', install_path]
343
Gilad Arnold1638d822013-11-07 23:38:16 -0800344 # If caller requires the list of extracted files, get verbose.
345 if return_extracted_files:
346 cmd += ['--verbose']
347
Simran Basi4baad082013-02-14 13:39:18 -0800348 # Determine how to decompress.
349 tarball = os.path.basename(tarball_path)
350 if tarball.endswith('.tar.bz2'):
351 cmd.append('--use-compress-prog=pbzip2')
352 elif tarball.endswith('.tgz') or tarball.endswith('.tar.gz'):
353 cmd.append('--gzip')
354
355 if excluded_files:
356 for exclude in excluded_files:
357 cmd.extend(['--exclude', exclude])
358
359 if files_to_extract:
360 cmd.extend(files_to_extract)
361
362 try:
Gilad Arnold1638d822013-11-07 23:38:16 -0800363 cmd_output = subprocess.check_output(cmd)
364 if return_extracted_files:
365 return [os.path.join(install_path, filename)
366 for filename in cmd_output.strip('\n').splitlines()
367 if not filename.endswith('/')]
368 return []
Simran Basi4baad082013-02-14 13:39:18 -0800369 except subprocess.CalledProcessError, e:
370 raise CommonUtilError(
371 'An error occurred when attempting to untar %s:\n%s' %
joychen3d164bd2013-06-24 18:12:23 -0700372 (tarball_path, e))
joychen7c2054a2013-07-25 11:14:07 -0700373
374
375def IsInsideChroot():
376 """Returns True if we are inside chroot."""
377 return os.path.exists('/etc/debian_chroot')