blob: 20cbed03c8c40c0a0e3bf313abec7ca7327e95e7 [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
45 Args:
46 static_dir: Directory where builds are served from.
Scott Zawalski16954532012-03-20 15:31:36 -040047 target: The build target, typically a combination of the board and the
48 type of build e.g. x86-mario-release.
49 milestone: For latest build set to None, for builds only in a specific
50 milestone set to a str of format Rxx (e.g. R16). Default: None.
Frank Farzan37761d12011-12-01 14:29:08 -080051
52 Returns:
Scott Zawalski16954532012-03-20 15:31:36 -040053 If latest found, a full build string is returned e.g. R17-1234.0.0-a1-b983.
54 If no latest is found for some reason or another a '' string is returned.
Frank Farzan37761d12011-12-01 14:29:08 -080055
56 Raises:
Gilad Arnold17fe03d2012-10-02 10:05:01 -070057 CommonUtilError: If for some reason the latest build cannot be
Scott Zawalski16954532012-03-20 15:31:36 -040058 deteremined, this could be due to the dir not existing or no builds
59 being present after filtering on milestone.
Frank Farzan37761d12011-12-01 14:29:08 -080060 """
Scott Zawalski16954532012-03-20 15:31:36 -040061 target_path = os.path.join(static_dir, target)
62 if not os.path.isdir(target_path):
Gilad Arnold17fe03d2012-10-02 10:05:01 -070063 raise CommonUtilError('Cannot find path %s' % target_path)
Frank Farzan37761d12011-12-01 14:29:08 -080064
Scott Zawalski16954532012-03-20 15:31:36 -040065 builds = [distutils.version.LooseVersion(build) for build in
66 os.listdir(target_path)]
Frank Farzan37761d12011-12-01 14:29:08 -080067
Scott Zawalski16954532012-03-20 15:31:36 -040068 if milestone and builds:
69 # Check if milestone Rxx is in the string representation of the build.
70 builds = filter(lambda x: milestone.upper() in str(x), builds)
Frank Farzan37761d12011-12-01 14:29:08 -080071
Scott Zawalski16954532012-03-20 15:31:36 -040072 if not builds:
Gilad Arnold17fe03d2012-10-02 10:05:01 -070073 raise CommonUtilError('Could not determine build for %s' % target)
Frank Farzan37761d12011-12-01 14:29:08 -080074
Scott Zawalski16954532012-03-20 15:31:36 -040075 return str(max(builds))
Frank Farzan37761d12011-12-01 14:29:08 -080076
77
Chris Sosa76e44b92013-01-31 12:11:38 -080078def PathInDir(directory, path):
79 """Returns True if the path is in directory.
80
81 Args:
82 directory: Directory where the path should be in.
83 path: Path to check.
84
85 Returns:
86 True if path is in static_dir, False otherwise
87 """
88 directory = os.path.realpath(directory)
89 path = os.path.realpath(path)
90 return (path.startswith(directory) and len(path) != len(directory))
91
92
Scott Zawalski84a39c92012-01-13 15:12:42 -050093def GetControlFile(static_dir, build, control_path):
Frank Farzan37761d12011-12-01 14:29:08 -080094 """Attempts to pull the requested control file from the Dev Server.
95
96 Args:
97 static_dir: Directory where builds are served from.
Frank Farzan37761d12011-12-01 14:29:08 -080098 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
99 control_path: Path to control file on Dev Server relative to Autotest root.
100
101 Raises:
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700102 CommonUtilError: If lock can't be acquired.
Frank Farzan37761d12011-12-01 14:29:08 -0800103
104 Returns:
105 Content of the requested control file.
106 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500107 # Be forgiving if the user passes in the control_path with a leading /
108 control_path = control_path.lstrip('/')
Scott Zawalski84a39c92012-01-13 15:12:42 -0500109 control_path = os.path.join(static_dir, build, 'autotest',
Scott Zawalski4647ce62012-01-03 17:17:28 -0500110 control_path)
Chris Sosa76e44b92013-01-31 12:11:38 -0800111 if not PathInDir(static_dir, control_path):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700112 raise CommonUtilError('Invalid control file "%s".' % control_path)
Frank Farzan37761d12011-12-01 14:29:08 -0800113
Scott Zawalski84a39c92012-01-13 15:12:42 -0500114 if not os.path.exists(control_path):
115 # TODO(scottz): Come up with some sort of error mechanism.
116 # crosbug.com/25040
117 return 'Unknown control path %s' % control_path
118
Frank Farzan37761d12011-12-01 14:29:08 -0800119 with open(control_path, 'r') as control_file:
120 return control_file.read()
121
122
Scott Zawalski84a39c92012-01-13 15:12:42 -0500123def GetControlFileList(static_dir, build):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500124 """List all control|control. files in the specified board/build path.
125
126 Args:
127 static_dir: Directory where builds are served from.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500128 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
129
130 Raises:
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700131 CommonUtilError: If path is outside of sandbox.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500132
133 Returns:
134 String of each file separated by a newline.
135 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500136 autotest_dir = os.path.join(static_dir, build, 'autotest/')
Chris Sosa76e44b92013-01-31 12:11:38 -0800137 if not PathInDir(static_dir, autotest_dir):
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700138 raise CommonUtilError('Autotest dir not in sandbox "%s".' % autotest_dir)
Scott Zawalski4647ce62012-01-03 17:17:28 -0500139
140 control_files = set()
Scott Zawalski84a39c92012-01-13 15:12:42 -0500141 if not os.path.exists(autotest_dir):
joychen3d164bd2013-06-24 18:12:23 -0700142 raise CommonUtilError('Could not find this directory.'
143 'Is it staged? %s' % autotest_dir)
Scott Zawalski84a39c92012-01-13 15:12:42 -0500144
Scott Zawalski4647ce62012-01-03 17:17:28 -0500145 for entry in os.walk(autotest_dir):
146 dir_path, _, files = entry
147 for file_entry in files:
148 if file_entry.startswith('control.') or file_entry == 'control':
149 control_files.add(os.path.join(dir_path,
Chris Sosaea148d92012-03-06 16:22:04 -0800150 file_entry).replace(autotest_dir, ''))
Scott Zawalski4647ce62012-01-03 17:17:28 -0500151
152 return '\n'.join(control_files)
153
154
Gilad Arnold55a2a372012-10-02 09:46:32 -0700155def GetFileSize(file_path):
156 """Returns the size in bytes of the file given."""
157 return os.path.getsize(file_path)
158
159
Chris Sosa6a3697f2013-01-29 16:44:43 -0800160# Hashlib is strange and doesn't actually define these in a sane way that
161# pylint can find them. Disable checks for them.
162# pylint: disable=E1101,W0106
Gilad Arnold55a2a372012-10-02 09:46:32 -0700163def GetFileHashes(file_path, do_sha1=False, do_sha256=False, do_md5=False):
164 """Computes and returns a list of requested hashes.
165
166 Args:
167 file_path: path to file to be hashed
168 do_sha1: whether or not to compute a SHA1 hash
169 do_sha256: whether or not to compute a SHA256 hash
170 do_md5: whether or not to compute a MD5 hash
171 Returns:
172 A dictionary containing binary hash values, keyed by 'sha1', 'sha256' and
173 'md5', respectively.
174 """
175 hashes = {}
176 if (do_sha1 or do_sha256 or do_md5):
177 # Initialize hashers.
178 hasher_sha1 = hashlib.sha1() if do_sha1 else None
179 hasher_sha256 = hashlib.sha256() if do_sha256 else None
180 hasher_md5 = hashlib.md5() if do_md5 else None
181
182 # Read blocks from file, update hashes.
183 with open(file_path, 'rb') as fd:
184 while True:
185 block = fd.read(_HASH_BLOCK_SIZE)
186 if not block:
187 break
188 hasher_sha1 and hasher_sha1.update(block)
189 hasher_sha256 and hasher_sha256.update(block)
190 hasher_md5 and hasher_md5.update(block)
191
192 # Update return values.
193 if hasher_sha1:
194 hashes['sha1'] = hasher_sha1.digest()
195 if hasher_sha256:
196 hashes['sha256'] = hasher_sha256.digest()
197 if hasher_md5:
198 hashes['md5'] = hasher_md5.digest()
199
200 return hashes
201
202
203def GetFileSha1(file_path):
204 """Returns the SHA1 checksum of the file given (base64 encoded)."""
205 return base64.b64encode(GetFileHashes(file_path, do_sha1=True)['sha1'])
206
207
208def GetFileSha256(file_path):
209 """Returns the SHA256 checksum of the file given (base64 encoded)."""
210 return base64.b64encode(GetFileHashes(file_path, do_sha256=True)['sha256'])
211
212
213def GetFileMd5(file_path):
214 """Returns the MD5 checksum of the file given (hex encoded)."""
215 return binascii.hexlify(GetFileHashes(file_path, do_md5=True)['md5'])
216
217
218def CopyFile(source, dest):
219 """Copies a file from |source| to |dest|."""
220 _Log('Copy File %s -> %s' % (source, dest))
221 shutil.copy(source, dest)
Chris Sosa76e44b92013-01-31 12:11:38 -0800222
223
224class LockDict(object):
225 """A dictionary of locks.
226
227 This class provides a thread-safe store of threading.Lock objects, which can
228 be used to regulate access to any set of hashable resources. Usage:
229
230 foo_lock_dict = LockDict()
231 ...
232 with foo_lock_dict.lock('bar'):
233 # Critical section for 'bar'
234 """
235 def __init__(self):
236 self._lock = self._new_lock()
237 self._dict = {}
238
239 @staticmethod
240 def _new_lock():
241 return threading.Lock()
242
243 def lock(self, key):
244 with self._lock:
245 lock = self._dict.get(key)
246 if not lock:
247 lock = self._new_lock()
248 self._dict[key] = lock
249 return lock
Simran Basi4baad082013-02-14 13:39:18 -0800250
251
252def ExtractTarball(tarball_path, install_path, files_to_extract=None,
253 excluded_files=None):
254 """Extracts a tarball using tar.
255
256 Detects whether the tarball is compressed or not based on the file
257 extension and extracts the tarball into the install_path.
258
259 Args:
260 tarball_path: Path to the tarball to extract.
261 install_path: Path to extract the tarball to.
262 files_to_extract: String of specific files in the tarball to extract.
263 excluded_files: String of files to not extract.
264 """
265 # Deal with exclusions.
266 cmd = ['tar', 'xf', tarball_path, '--directory', install_path]
267
268 # Determine how to decompress.
269 tarball = os.path.basename(tarball_path)
270 if tarball.endswith('.tar.bz2'):
271 cmd.append('--use-compress-prog=pbzip2')
272 elif tarball.endswith('.tgz') or tarball.endswith('.tar.gz'):
273 cmd.append('--gzip')
274
275 if excluded_files:
276 for exclude in excluded_files:
277 cmd.extend(['--exclude', exclude])
278
279 if files_to_extract:
280 cmd.extend(files_to_extract)
281
282 try:
283 subprocess.check_call(cmd)
284 except subprocess.CalledProcessError, e:
285 raise CommonUtilError(
286 'An error occurred when attempting to untar %s:\n%s' %
joychen3d164bd2013-06-24 18:12:23 -0700287 (tarball_path, e))