blob: 1cf72b151b247734f9c14dccf569824b616be650 [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
Chris Sosa76e44b92013-01-31 12:11:38 -080015import threading
Simran Basi4baad082013-02-14 13:39:18 -080016import subprocess
Frank Farzan37761d12011-12-01 14:29:08 -080017
Gilad Arnoldc65330c2012-09-20 15:17:48 -070018import log_util
19
20
21# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080022def _Log(message, *args):
23 return log_util.LogWithTag('UTIL', message, *args)
Gilad Arnoldc65330c2012-09-20 15:17:48 -070024
Frank Farzan37761d12011-12-01 14:29:08 -080025
Gilad Arnold55a2a372012-10-02 09:46:32 -070026_HASH_BLOCK_SIZE = 8192
27
Gilad Arnold6f99b982012-09-12 10:49:40 -070028
Gilad Arnold17fe03d2012-10-02 10:05:01 -070029class CommonUtilError(Exception):
Frank Farzan37761d12011-12-01 14:29:08 -080030 """Exception classes used by this module."""
31 pass
32
33
Chris Sosa76e44b92013-01-31 12:11:38 -080034def MkDirP(directory):
35 """Thread-safely create a directory like mkdir -p."""
Frank Farzan37761d12011-12-01 14:29:08 -080036 try:
Chris Sosa76e44b92013-01-31 12:11:38 -080037 os.makedirs(directory)
Frank Farzan37761d12011-12-01 14:29:08 -080038 except OSError, e:
Chris Sosa76e44b92013-01-31 12:11:38 -080039 if not (e.errno == errno.EEXIST and os.path.isdir(directory)):
Frank Farzan37761d12011-12-01 14:29:08 -080040 raise
41
Frank Farzan37761d12011-12-01 14:29:08 -080042
Scott Zawalski16954532012-03-20 15:31:36 -040043def GetLatestBuildVersion(static_dir, target, milestone=None):
Frank Farzan37761d12011-12-01 14:29:08 -080044 """Retrieves the latest build version for a given board.
45
joychen921e1fb2013-06-28 11:12:20 -070046 Searches the static_dir for builds for target, and returns the highest
47 version number currently available locally.
48
Frank Farzan37761d12011-12-01 14:29:08 -080049 Args:
50 static_dir: Directory where builds are served from.
Scott Zawalski16954532012-03-20 15:31:36 -040051 target: The build target, typically a combination of the board and the
52 type of build e.g. x86-mario-release.
53 milestone: For latest build set to None, for builds only in a specific
54 milestone set to a str of format Rxx (e.g. R16). Default: None.
Frank Farzan37761d12011-12-01 14:29:08 -080055
56 Returns:
Scott Zawalski16954532012-03-20 15:31:36 -040057 If latest found, a full build string is returned e.g. R17-1234.0.0-a1-b983.
58 If no latest is found for some reason or another a '' string is returned.
Frank Farzan37761d12011-12-01 14:29:08 -080059
60 Raises:
Gilad Arnold17fe03d2012-10-02 10:05:01 -070061 CommonUtilError: If for some reason the latest build cannot be
Scott Zawalski16954532012-03-20 15:31:36 -040062 deteremined, this could be due to the dir not existing or no builds
63 being present after filtering on milestone.
Frank Farzan37761d12011-12-01 14:29:08 -080064 """
Scott Zawalski16954532012-03-20 15:31:36 -040065 target_path = os.path.join(static_dir, target)
66 if not os.path.isdir(target_path):
Gilad Arnold17fe03d2012-10-02 10:05:01 -070067 raise CommonUtilError('Cannot find path %s' % target_path)
Frank Farzan37761d12011-12-01 14:29:08 -080068
Scott Zawalski16954532012-03-20 15:31:36 -040069 builds = [distutils.version.LooseVersion(build) for build in
70 os.listdir(target_path)]
Frank Farzan37761d12011-12-01 14:29:08 -080071
Scott Zawalski16954532012-03-20 15:31:36 -040072 if milestone and builds:
73 # Check if milestone Rxx is in the string representation of the build.
74 builds = filter(lambda x: milestone.upper() in str(x), builds)
Frank Farzan37761d12011-12-01 14:29:08 -080075
Scott Zawalski16954532012-03-20 15:31:36 -040076 if not builds:
Gilad Arnold17fe03d2012-10-02 10:05:01 -070077 raise CommonUtilError('Could not determine build for %s' % target)
Frank Farzan37761d12011-12-01 14:29:08 -080078
Scott Zawalski16954532012-03-20 15:31:36 -040079 return str(max(builds))
Frank Farzan37761d12011-12-01 14:29:08 -080080
81
Chris Sosa76e44b92013-01-31 12:11:38 -080082def PathInDir(directory, path):
83 """Returns True if the path is in directory.
84
85 Args:
86 directory: Directory where the path should be in.
87 path: Path to check.
88
89 Returns:
90 True if path is in static_dir, False otherwise
91 """
92 directory = os.path.realpath(directory)
93 path = os.path.realpath(path)
94 return (path.startswith(directory) and len(path) != len(directory))
95
96
Scott Zawalski84a39c92012-01-13 15:12:42 -050097def GetControlFile(static_dir, build, control_path):
Frank Farzan37761d12011-12-01 14:29:08 -080098 """Attempts to pull the requested control file from the Dev Server.
99
100 Args:
101 static_dir: Directory where builds are served from.
Frank Farzan37761d12011-12-01 14:29:08 -0800102 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
103 control_path: Path to control file on Dev Server relative to Autotest root.
104
105 Raises:
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700106 CommonUtilError: If lock can't be acquired.
Frank Farzan37761d12011-12-01 14:29:08 -0800107
108 Returns:
109 Content of the requested control file.
110 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500111 # Be forgiving if the user passes in the control_path with a leading /
112 control_path = control_path.lstrip('/')
Scott Zawalski84a39c92012-01-13 15:12:42 -0500113 control_path = os.path.join(static_dir, build, 'autotest',
Scott Zawalski4647ce62012-01-03 17:17:28 -0500114 control_path)
Chris Sosa76e44b92013-01-31 12:11:38 -0800115 if not PathInDir(static_dir, control_path):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700116 raise CommonUtilError('Invalid control file "%s".' % control_path)
Frank Farzan37761d12011-12-01 14:29:08 -0800117
Scott Zawalski84a39c92012-01-13 15:12:42 -0500118 if not os.path.exists(control_path):
119 # TODO(scottz): Come up with some sort of error mechanism.
120 # crosbug.com/25040
121 return 'Unknown control path %s' % control_path
122
Frank Farzan37761d12011-12-01 14:29:08 -0800123 with open(control_path, 'r') as control_file:
124 return control_file.read()
125
126
beepsbd337242013-07-09 22:44:06 -0700127def GetControlFileListForSuite(static_dir, build, suite_name):
128 """List all control files for a specified build, for the given suite.
129
130 If the specified suite_name isn't found in the suite to control file
131 map, this method will return all control files for the build by calling
132 GetControlFileList.
133
134 Args:
135 static_dir: Directory where builds are served from.
136 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
137 suite_name: Name of the suite for which we require control files.
138
139 Raises:
140 CommonUtilError: If the suite_to_control_file_map isn't found in
141 the specified build's staged directory.
142
143 Returns:
144 String of each control file separated by a newline.
145 """
146 suite_to_control_map = os.path.join(static_dir, build,
147 'autotest', 'test_suites',
148 'suite_to_control_file_map')
149
150 if not PathInDir(static_dir, suite_to_control_map):
151 raise CommonUtilError('suite_to_control_map not in "%s".' %
152 suite_to_control_map)
153
154 if not os.path.exists(suite_to_control_map):
155 raise CommonUtilError('Could not find this file. '
156 'Is it staged? %s' % suite_to_control_map)
157
158 with open(suite_to_control_map, 'r') as fd:
159 try:
160 return '\n'.join(ast.literal_eval(fd.read())[suite_name])
161 except KeyError:
162 return GetControlFileList(static_dir, build)
163
164
Scott Zawalski84a39c92012-01-13 15:12:42 -0500165def GetControlFileList(static_dir, build):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500166 """List all control|control. files in the specified board/build path.
167
168 Args:
169 static_dir: Directory where builds are served from.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500170 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
171
172 Raises:
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700173 CommonUtilError: If path is outside of sandbox.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500174
175 Returns:
176 String of each file separated by a newline.
177 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500178 autotest_dir = os.path.join(static_dir, build, 'autotest/')
Chris Sosa76e44b92013-01-31 12:11:38 -0800179 if not PathInDir(static_dir, autotest_dir):
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700180 raise CommonUtilError('Autotest dir not in sandbox "%s".' % autotest_dir)
Scott Zawalski4647ce62012-01-03 17:17:28 -0500181
182 control_files = set()
Scott Zawalski84a39c92012-01-13 15:12:42 -0500183 if not os.path.exists(autotest_dir):
joychen3d164bd2013-06-24 18:12:23 -0700184 raise CommonUtilError('Could not find this directory.'
185 'Is it staged? %s' % autotest_dir)
Scott Zawalski84a39c92012-01-13 15:12:42 -0500186
Scott Zawalski4647ce62012-01-03 17:17:28 -0500187 for entry in os.walk(autotest_dir):
188 dir_path, _, files = entry
189 for file_entry in files:
190 if file_entry.startswith('control.') or file_entry == 'control':
191 control_files.add(os.path.join(dir_path,
Chris Sosaea148d92012-03-06 16:22:04 -0800192 file_entry).replace(autotest_dir, ''))
Scott Zawalski4647ce62012-01-03 17:17:28 -0500193
194 return '\n'.join(control_files)
195
196
Gilad Arnold55a2a372012-10-02 09:46:32 -0700197def GetFileSize(file_path):
198 """Returns the size in bytes of the file given."""
199 return os.path.getsize(file_path)
200
201
Chris Sosa6a3697f2013-01-29 16:44:43 -0800202# Hashlib is strange and doesn't actually define these in a sane way that
203# pylint can find them. Disable checks for them.
204# pylint: disable=E1101,W0106
Gilad Arnold55a2a372012-10-02 09:46:32 -0700205def GetFileHashes(file_path, do_sha1=False, do_sha256=False, do_md5=False):
206 """Computes and returns a list of requested hashes.
207
208 Args:
209 file_path: path to file to be hashed
210 do_sha1: whether or not to compute a SHA1 hash
211 do_sha256: whether or not to compute a SHA256 hash
212 do_md5: whether or not to compute a MD5 hash
213 Returns:
214 A dictionary containing binary hash values, keyed by 'sha1', 'sha256' and
215 'md5', respectively.
216 """
217 hashes = {}
218 if (do_sha1 or do_sha256 or do_md5):
219 # Initialize hashers.
220 hasher_sha1 = hashlib.sha1() if do_sha1 else None
221 hasher_sha256 = hashlib.sha256() if do_sha256 else None
222 hasher_md5 = hashlib.md5() if do_md5 else None
223
224 # Read blocks from file, update hashes.
225 with open(file_path, 'rb') as fd:
226 while True:
227 block = fd.read(_HASH_BLOCK_SIZE)
228 if not block:
229 break
230 hasher_sha1 and hasher_sha1.update(block)
231 hasher_sha256 and hasher_sha256.update(block)
232 hasher_md5 and hasher_md5.update(block)
233
234 # Update return values.
235 if hasher_sha1:
236 hashes['sha1'] = hasher_sha1.digest()
237 if hasher_sha256:
238 hashes['sha256'] = hasher_sha256.digest()
239 if hasher_md5:
240 hashes['md5'] = hasher_md5.digest()
241
242 return hashes
243
244
245def GetFileSha1(file_path):
246 """Returns the SHA1 checksum of the file given (base64 encoded)."""
247 return base64.b64encode(GetFileHashes(file_path, do_sha1=True)['sha1'])
248
249
250def GetFileSha256(file_path):
251 """Returns the SHA256 checksum of the file given (base64 encoded)."""
252 return base64.b64encode(GetFileHashes(file_path, do_sha256=True)['sha256'])
253
254
255def GetFileMd5(file_path):
256 """Returns the MD5 checksum of the file given (hex encoded)."""
257 return binascii.hexlify(GetFileHashes(file_path, do_md5=True)['md5'])
258
259
260def CopyFile(source, dest):
261 """Copies a file from |source| to |dest|."""
262 _Log('Copy File %s -> %s' % (source, dest))
263 shutil.copy(source, dest)
Chris Sosa76e44b92013-01-31 12:11:38 -0800264
265
266class LockDict(object):
267 """A dictionary of locks.
268
269 This class provides a thread-safe store of threading.Lock objects, which can
270 be used to regulate access to any set of hashable resources. Usage:
271
272 foo_lock_dict = LockDict()
273 ...
274 with foo_lock_dict.lock('bar'):
275 # Critical section for 'bar'
276 """
277 def __init__(self):
278 self._lock = self._new_lock()
279 self._dict = {}
280
281 @staticmethod
282 def _new_lock():
283 return threading.Lock()
284
285 def lock(self, key):
286 with self._lock:
287 lock = self._dict.get(key)
288 if not lock:
289 lock = self._new_lock()
290 self._dict[key] = lock
291 return lock
Simran Basi4baad082013-02-14 13:39:18 -0800292
293
294def ExtractTarball(tarball_path, install_path, files_to_extract=None,
295 excluded_files=None):
296 """Extracts a tarball using tar.
297
298 Detects whether the tarball is compressed or not based on the file
299 extension and extracts the tarball into the install_path.
300
301 Args:
302 tarball_path: Path to the tarball to extract.
303 install_path: Path to extract the tarball to.
304 files_to_extract: String of specific files in the tarball to extract.
305 excluded_files: String of files to not extract.
306 """
307 # Deal with exclusions.
308 cmd = ['tar', 'xf', tarball_path, '--directory', install_path]
309
310 # Determine how to decompress.
311 tarball = os.path.basename(tarball_path)
312 if tarball.endswith('.tar.bz2'):
313 cmd.append('--use-compress-prog=pbzip2')
314 elif tarball.endswith('.tgz') or tarball.endswith('.tar.gz'):
315 cmd.append('--gzip')
316
317 if excluded_files:
318 for exclude in excluded_files:
319 cmd.extend(['--exclude', exclude])
320
321 if files_to_extract:
322 cmd.extend(files_to_extract)
323
324 try:
325 subprocess.check_call(cmd)
326 except subprocess.CalledProcessError, e:
327 raise CommonUtilError(
328 'An error occurred when attempting to untar %s:\n%s' %
joychen3d164bd2013-06-24 18:12:23 -0700329 (tarball_path, e))
joychen7c2054a2013-07-25 11:14:07 -0700330
331
332def IsInsideChroot():
333 """Returns True if we are inside chroot."""
334 return os.path.exists('/etc/debian_chroot')