blob: 29cb8f1fa2454f47a034dcd00224db35e6536005 [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
Gilad Arnold55a2a372012-10-02 09:46:32 -07007import base64
8import binascii
Frank Farzan37761d12011-12-01 14:29:08 -08009import distutils.version
10import errno
Gilad Arnold55a2a372012-10-02 09:46:32 -070011import hashlib
Frank Farzan37761d12011-12-01 14:29:08 -080012import os
13import shutil
Chris Sosa76e44b92013-01-31 12:11:38 -080014import threading
Simran Basi4baad082013-02-14 13:39:18 -080015import subprocess
Frank Farzan37761d12011-12-01 14:29:08 -080016
Gilad Arnoldc65330c2012-09-20 15:17:48 -070017import log_util
18
19
20# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080021def _Log(message, *args):
22 return log_util.LogWithTag('UTIL', message, *args)
Gilad Arnoldc65330c2012-09-20 15:17:48 -070023
Frank Farzan37761d12011-12-01 14:29:08 -080024
Gilad Arnold55a2a372012-10-02 09:46:32 -070025_HASH_BLOCK_SIZE = 8192
26
Gilad Arnold6f99b982012-09-12 10:49:40 -070027
Gilad Arnold17fe03d2012-10-02 10:05:01 -070028class CommonUtilError(Exception):
Frank Farzan37761d12011-12-01 14:29:08 -080029 """Exception classes used by this module."""
30 pass
31
32
Chris Sosa76e44b92013-01-31 12:11:38 -080033def MkDirP(directory):
34 """Thread-safely create a directory like mkdir -p."""
Frank Farzan37761d12011-12-01 14:29:08 -080035 try:
Chris Sosa76e44b92013-01-31 12:11:38 -080036 os.makedirs(directory)
Frank Farzan37761d12011-12-01 14:29:08 -080037 except OSError, e:
Chris Sosa76e44b92013-01-31 12:11:38 -080038 if not (e.errno == errno.EEXIST and os.path.isdir(directory)):
Frank Farzan37761d12011-12-01 14:29:08 -080039 raise
40
Frank Farzan37761d12011-12-01 14:29:08 -080041
Scott Zawalski16954532012-03-20 15:31:36 -040042def GetLatestBuildVersion(static_dir, target, milestone=None):
Frank Farzan37761d12011-12-01 14:29:08 -080043 """Retrieves the latest build version for a given board.
44
joychen921e1fb2013-06-28 11:12:20 -070045 Searches the static_dir for builds for target, and returns the highest
46 version number currently available locally.
47
Frank Farzan37761d12011-12-01 14:29:08 -080048 Args:
49 static_dir: Directory where builds are served from.
Scott Zawalski16954532012-03-20 15:31:36 -040050 target: The build target, typically a combination of the board and the
51 type of build e.g. x86-mario-release.
52 milestone: For latest build set to None, for builds only in a specific
53 milestone set to a str of format Rxx (e.g. R16). Default: None.
Frank Farzan37761d12011-12-01 14:29:08 -080054
55 Returns:
Scott Zawalski16954532012-03-20 15:31:36 -040056 If latest found, a full build string is returned e.g. R17-1234.0.0-a1-b983.
57 If no latest is found for some reason or another a '' string is returned.
Frank Farzan37761d12011-12-01 14:29:08 -080058
59 Raises:
Gilad Arnold17fe03d2012-10-02 10:05:01 -070060 CommonUtilError: If for some reason the latest build cannot be
Scott Zawalski16954532012-03-20 15:31:36 -040061 deteremined, this could be due to the dir not existing or no builds
62 being present after filtering on milestone.
Frank Farzan37761d12011-12-01 14:29:08 -080063 """
Scott Zawalski16954532012-03-20 15:31:36 -040064 target_path = os.path.join(static_dir, target)
65 if not os.path.isdir(target_path):
Gilad Arnold17fe03d2012-10-02 10:05:01 -070066 raise CommonUtilError('Cannot find path %s' % target_path)
Frank Farzan37761d12011-12-01 14:29:08 -080067
Scott Zawalski16954532012-03-20 15:31:36 -040068 builds = [distutils.version.LooseVersion(build) for build in
69 os.listdir(target_path)]
Frank Farzan37761d12011-12-01 14:29:08 -080070
Scott Zawalski16954532012-03-20 15:31:36 -040071 if milestone and builds:
72 # Check if milestone Rxx is in the string representation of the build.
73 builds = filter(lambda x: milestone.upper() in str(x), builds)
Frank Farzan37761d12011-12-01 14:29:08 -080074
Scott Zawalski16954532012-03-20 15:31:36 -040075 if not builds:
Gilad Arnold17fe03d2012-10-02 10:05:01 -070076 raise CommonUtilError('Could not determine build for %s' % target)
Frank Farzan37761d12011-12-01 14:29:08 -080077
Scott Zawalski16954532012-03-20 15:31:36 -040078 return str(max(builds))
Frank Farzan37761d12011-12-01 14:29:08 -080079
80
Chris Sosa76e44b92013-01-31 12:11:38 -080081def PathInDir(directory, path):
82 """Returns True if the path is in directory.
83
84 Args:
85 directory: Directory where the path should be in.
86 path: Path to check.
87
88 Returns:
89 True if path is in static_dir, False otherwise
90 """
91 directory = os.path.realpath(directory)
92 path = os.path.realpath(path)
93 return (path.startswith(directory) and len(path) != len(directory))
94
95
Scott Zawalski84a39c92012-01-13 15:12:42 -050096def GetControlFile(static_dir, build, control_path):
Frank Farzan37761d12011-12-01 14:29:08 -080097 """Attempts to pull the requested control file from the Dev Server.
98
99 Args:
100 static_dir: Directory where builds are served from.
Frank Farzan37761d12011-12-01 14:29:08 -0800101 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
102 control_path: Path to control file on Dev Server relative to Autotest root.
103
104 Raises:
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700105 CommonUtilError: If lock can't be acquired.
Frank Farzan37761d12011-12-01 14:29:08 -0800106
107 Returns:
108 Content of the requested control file.
109 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500110 # Be forgiving if the user passes in the control_path with a leading /
111 control_path = control_path.lstrip('/')
Scott Zawalski84a39c92012-01-13 15:12:42 -0500112 control_path = os.path.join(static_dir, build, 'autotest',
Scott Zawalski4647ce62012-01-03 17:17:28 -0500113 control_path)
Chris Sosa76e44b92013-01-31 12:11:38 -0800114 if not PathInDir(static_dir, control_path):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700115 raise CommonUtilError('Invalid control file "%s".' % control_path)
Frank Farzan37761d12011-12-01 14:29:08 -0800116
Scott Zawalski84a39c92012-01-13 15:12:42 -0500117 if not os.path.exists(control_path):
118 # TODO(scottz): Come up with some sort of error mechanism.
119 # crosbug.com/25040
120 return 'Unknown control path %s' % control_path
121
Frank Farzan37761d12011-12-01 14:29:08 -0800122 with open(control_path, 'r') as control_file:
123 return control_file.read()
124
125
Scott Zawalski84a39c92012-01-13 15:12:42 -0500126def GetControlFileList(static_dir, build):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500127 """List all control|control. files in the specified board/build path.
128
129 Args:
130 static_dir: Directory where builds are served from.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500131 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
132
133 Raises:
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700134 CommonUtilError: If path is outside of sandbox.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500135
136 Returns:
137 String of each file separated by a newline.
138 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500139 autotest_dir = os.path.join(static_dir, build, 'autotest/')
Chris Sosa76e44b92013-01-31 12:11:38 -0800140 if not PathInDir(static_dir, autotest_dir):
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700141 raise CommonUtilError('Autotest dir not in sandbox "%s".' % autotest_dir)
Scott Zawalski4647ce62012-01-03 17:17:28 -0500142
143 control_files = set()
Scott Zawalski84a39c92012-01-13 15:12:42 -0500144 if not os.path.exists(autotest_dir):
joychen3d164bd2013-06-24 18:12:23 -0700145 raise CommonUtilError('Could not find this directory.'
146 'Is it staged? %s' % autotest_dir)
Scott Zawalski84a39c92012-01-13 15:12:42 -0500147
Scott Zawalski4647ce62012-01-03 17:17:28 -0500148 for entry in os.walk(autotest_dir):
149 dir_path, _, files = entry
150 for file_entry in files:
151 if file_entry.startswith('control.') or file_entry == 'control':
152 control_files.add(os.path.join(dir_path,
Chris Sosaea148d92012-03-06 16:22:04 -0800153 file_entry).replace(autotest_dir, ''))
Scott Zawalski4647ce62012-01-03 17:17:28 -0500154
155 return '\n'.join(control_files)
156
157
Gilad Arnold55a2a372012-10-02 09:46:32 -0700158def GetFileSize(file_path):
159 """Returns the size in bytes of the file given."""
160 return os.path.getsize(file_path)
161
162
Chris Sosa6a3697f2013-01-29 16:44:43 -0800163# Hashlib is strange and doesn't actually define these in a sane way that
164# pylint can find them. Disable checks for them.
165# pylint: disable=E1101,W0106
Gilad Arnold55a2a372012-10-02 09:46:32 -0700166def GetFileHashes(file_path, do_sha1=False, do_sha256=False, do_md5=False):
167 """Computes and returns a list of requested hashes.
168
169 Args:
170 file_path: path to file to be hashed
171 do_sha1: whether or not to compute a SHA1 hash
172 do_sha256: whether or not to compute a SHA256 hash
173 do_md5: whether or not to compute a MD5 hash
174 Returns:
175 A dictionary containing binary hash values, keyed by 'sha1', 'sha256' and
176 'md5', respectively.
177 """
178 hashes = {}
179 if (do_sha1 or do_sha256 or do_md5):
180 # Initialize hashers.
181 hasher_sha1 = hashlib.sha1() if do_sha1 else None
182 hasher_sha256 = hashlib.sha256() if do_sha256 else None
183 hasher_md5 = hashlib.md5() if do_md5 else None
184
185 # Read blocks from file, update hashes.
186 with open(file_path, 'rb') as fd:
187 while True:
188 block = fd.read(_HASH_BLOCK_SIZE)
189 if not block:
190 break
191 hasher_sha1 and hasher_sha1.update(block)
192 hasher_sha256 and hasher_sha256.update(block)
193 hasher_md5 and hasher_md5.update(block)
194
195 # Update return values.
196 if hasher_sha1:
197 hashes['sha1'] = hasher_sha1.digest()
198 if hasher_sha256:
199 hashes['sha256'] = hasher_sha256.digest()
200 if hasher_md5:
201 hashes['md5'] = hasher_md5.digest()
202
203 return hashes
204
205
206def GetFileSha1(file_path):
207 """Returns the SHA1 checksum of the file given (base64 encoded)."""
208 return base64.b64encode(GetFileHashes(file_path, do_sha1=True)['sha1'])
209
210
211def GetFileSha256(file_path):
212 """Returns the SHA256 checksum of the file given (base64 encoded)."""
213 return base64.b64encode(GetFileHashes(file_path, do_sha256=True)['sha256'])
214
215
216def GetFileMd5(file_path):
217 """Returns the MD5 checksum of the file given (hex encoded)."""
218 return binascii.hexlify(GetFileHashes(file_path, do_md5=True)['md5'])
219
220
221def CopyFile(source, dest):
222 """Copies a file from |source| to |dest|."""
223 _Log('Copy File %s -> %s' % (source, dest))
224 shutil.copy(source, dest)
Chris Sosa76e44b92013-01-31 12:11:38 -0800225
226
227class LockDict(object):
228 """A dictionary of locks.
229
230 This class provides a thread-safe store of threading.Lock objects, which can
231 be used to regulate access to any set of hashable resources. Usage:
232
233 foo_lock_dict = LockDict()
234 ...
235 with foo_lock_dict.lock('bar'):
236 # Critical section for 'bar'
237 """
238 def __init__(self):
239 self._lock = self._new_lock()
240 self._dict = {}
241
242 @staticmethod
243 def _new_lock():
244 return threading.Lock()
245
246 def lock(self, key):
247 with self._lock:
248 lock = self._dict.get(key)
249 if not lock:
250 lock = self._new_lock()
251 self._dict[key] = lock
252 return lock
Simran Basi4baad082013-02-14 13:39:18 -0800253
254
255def ExtractTarball(tarball_path, install_path, files_to_extract=None,
256 excluded_files=None):
257 """Extracts a tarball using tar.
258
259 Detects whether the tarball is compressed or not based on the file
260 extension and extracts the tarball into the install_path.
261
262 Args:
263 tarball_path: Path to the tarball to extract.
264 install_path: Path to extract the tarball to.
265 files_to_extract: String of specific files in the tarball to extract.
266 excluded_files: String of files to not extract.
267 """
268 # Deal with exclusions.
269 cmd = ['tar', 'xf', tarball_path, '--directory', install_path]
270
271 # Determine how to decompress.
272 tarball = os.path.basename(tarball_path)
273 if tarball.endswith('.tar.bz2'):
274 cmd.append('--use-compress-prog=pbzip2')
275 elif tarball.endswith('.tgz') or tarball.endswith('.tar.gz'):
276 cmd.append('--gzip')
277
278 if excluded_files:
279 for exclude in excluded_files:
280 cmd.extend(['--exclude', exclude])
281
282 if files_to_extract:
283 cmd.extend(files_to_extract)
284
285 try:
286 subprocess.check_call(cmd)
287 except subprocess.CalledProcessError, e:
288 raise CommonUtilError(
289 'An error occurred when attempting to untar %s:\n%s' %
joychen3d164bd2013-06-24 18:12:23 -0700290 (tarball_path, e))