blob: ac7f093ac0d498f493469180255fb090888568ea [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
Frank Farzan37761d12011-12-01 14:29:08 -080015
Gilad Arnoldc65330c2012-09-20 15:17:48 -070016import log_util
17
18
19# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080020def _Log(message, *args):
21 return log_util.LogWithTag('UTIL', message, *args)
Gilad Arnoldc65330c2012-09-20 15:17:48 -070022
Frank Farzan37761d12011-12-01 14:29:08 -080023
Gilad Arnold55a2a372012-10-02 09:46:32 -070024_HASH_BLOCK_SIZE = 8192
25
Gilad Arnold6f99b982012-09-12 10:49:40 -070026
Gilad Arnold17fe03d2012-10-02 10:05:01 -070027class CommonUtilError(Exception):
Frank Farzan37761d12011-12-01 14:29:08 -080028 """Exception classes used by this module."""
29 pass
30
31
Chris Sosa76e44b92013-01-31 12:11:38 -080032def MkDirP(directory):
33 """Thread-safely create a directory like mkdir -p."""
Frank Farzan37761d12011-12-01 14:29:08 -080034 try:
Chris Sosa76e44b92013-01-31 12:11:38 -080035 os.makedirs(directory)
Frank Farzan37761d12011-12-01 14:29:08 -080036 except OSError, e:
Chris Sosa76e44b92013-01-31 12:11:38 -080037 if not (e.errno == errno.EEXIST and os.path.isdir(directory)):
Frank Farzan37761d12011-12-01 14:29:08 -080038 raise
39
Frank Farzan37761d12011-12-01 14:29:08 -080040
Scott Zawalski16954532012-03-20 15:31:36 -040041def GetLatestBuildVersion(static_dir, target, milestone=None):
Frank Farzan37761d12011-12-01 14:29:08 -080042 """Retrieves the latest build version for a given board.
43
44 Args:
45 static_dir: Directory where builds are served from.
Scott Zawalski16954532012-03-20 15:31:36 -040046 target: The build target, typically a combination of the board and the
47 type of build e.g. x86-mario-release.
48 milestone: For latest build set to None, for builds only in a specific
49 milestone set to a str of format Rxx (e.g. R16). Default: None.
Frank Farzan37761d12011-12-01 14:29:08 -080050
51 Returns:
Scott Zawalski16954532012-03-20 15:31:36 -040052 If latest found, a full build string is returned e.g. R17-1234.0.0-a1-b983.
53 If no latest is found for some reason or another a '' string is returned.
Frank Farzan37761d12011-12-01 14:29:08 -080054
55 Raises:
Gilad Arnold17fe03d2012-10-02 10:05:01 -070056 CommonUtilError: If for some reason the latest build cannot be
Scott Zawalski16954532012-03-20 15:31:36 -040057 deteremined, this could be due to the dir not existing or no builds
58 being present after filtering on milestone.
Frank Farzan37761d12011-12-01 14:29:08 -080059 """
Scott Zawalski16954532012-03-20 15:31:36 -040060 target_path = os.path.join(static_dir, target)
61 if not os.path.isdir(target_path):
Gilad Arnold17fe03d2012-10-02 10:05:01 -070062 raise CommonUtilError('Cannot find path %s' % target_path)
Frank Farzan37761d12011-12-01 14:29:08 -080063
Scott Zawalski16954532012-03-20 15:31:36 -040064 builds = [distutils.version.LooseVersion(build) for build in
65 os.listdir(target_path)]
Frank Farzan37761d12011-12-01 14:29:08 -080066
Scott Zawalski16954532012-03-20 15:31:36 -040067 if milestone and builds:
68 # Check if milestone Rxx is in the string representation of the build.
69 builds = filter(lambda x: milestone.upper() in str(x), builds)
Frank Farzan37761d12011-12-01 14:29:08 -080070
Scott Zawalski16954532012-03-20 15:31:36 -040071 if not builds:
Gilad Arnold17fe03d2012-10-02 10:05:01 -070072 raise CommonUtilError('Could not determine build for %s' % target)
Frank Farzan37761d12011-12-01 14:29:08 -080073
Scott Zawalski16954532012-03-20 15:31:36 -040074 return str(max(builds))
Frank Farzan37761d12011-12-01 14:29:08 -080075
76
Chris Sosa76e44b92013-01-31 12:11:38 -080077def PathInDir(directory, path):
78 """Returns True if the path is in directory.
79
80 Args:
81 directory: Directory where the path should be in.
82 path: Path to check.
83
84 Returns:
85 True if path is in static_dir, False otherwise
86 """
87 directory = os.path.realpath(directory)
88 path = os.path.realpath(path)
89 return (path.startswith(directory) and len(path) != len(directory))
90
91
Scott Zawalski84a39c92012-01-13 15:12:42 -050092def GetControlFile(static_dir, build, control_path):
Frank Farzan37761d12011-12-01 14:29:08 -080093 """Attempts to pull the requested control file from the Dev Server.
94
95 Args:
96 static_dir: Directory where builds are served from.
Frank Farzan37761d12011-12-01 14:29:08 -080097 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
98 control_path: Path to control file on Dev Server relative to Autotest root.
99
100 Raises:
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700101 CommonUtilError: If lock can't be acquired.
Frank Farzan37761d12011-12-01 14:29:08 -0800102
103 Returns:
104 Content of the requested control file.
105 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500106 # Be forgiving if the user passes in the control_path with a leading /
107 control_path = control_path.lstrip('/')
Scott Zawalski84a39c92012-01-13 15:12:42 -0500108 control_path = os.path.join(static_dir, build, 'autotest',
Scott Zawalski4647ce62012-01-03 17:17:28 -0500109 control_path)
Chris Sosa76e44b92013-01-31 12:11:38 -0800110 if not PathInDir(static_dir, control_path):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700111 raise CommonUtilError('Invalid control file "%s".' % control_path)
Frank Farzan37761d12011-12-01 14:29:08 -0800112
Scott Zawalski84a39c92012-01-13 15:12:42 -0500113 if not os.path.exists(control_path):
114 # TODO(scottz): Come up with some sort of error mechanism.
115 # crosbug.com/25040
116 return 'Unknown control path %s' % control_path
117
Frank Farzan37761d12011-12-01 14:29:08 -0800118 with open(control_path, 'r') as control_file:
119 return control_file.read()
120
121
Scott Zawalski84a39c92012-01-13 15:12:42 -0500122def GetControlFileList(static_dir, build):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500123 """List all control|control. files in the specified board/build path.
124
125 Args:
126 static_dir: Directory where builds are served from.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500127 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
128
129 Raises:
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700130 CommonUtilError: If path is outside of sandbox.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500131
132 Returns:
133 String of each file separated by a newline.
134 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500135 autotest_dir = os.path.join(static_dir, build, 'autotest/')
Chris Sosa76e44b92013-01-31 12:11:38 -0800136 if not PathInDir(static_dir, autotest_dir):
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700137 raise CommonUtilError('Autotest dir not in sandbox "%s".' % autotest_dir)
Scott Zawalski4647ce62012-01-03 17:17:28 -0500138
139 control_files = set()
Scott Zawalski84a39c92012-01-13 15:12:42 -0500140 if not os.path.exists(autotest_dir):
141 # TODO(scottz): Come up with some sort of error mechanism.
142 # crosbug.com/25040
143 return 'Unknown build path %s' % autotest_dir
144
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