blob: 4edfafa958e7238eadedc2ca7ca36a343ee34a2f [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
7import cherrypy
8import distutils.version
9import errno
10import os
Chris Masone816e38c2012-05-02 12:22:36 -070011import random
Frank Farzan37761d12011-12-01 14:29:08 -080012import shutil
Chris Masone816e38c2012-05-02 12:22:36 -070013import time
Frank Farzan37761d12011-12-01 14:29:08 -080014
Chris Sosa47a7d4e2012-03-28 11:26:55 -070015import downloadable_artifact
16import gsutil_util
Frank Farzan37761d12011-12-01 14:29:08 -080017
18AU_BASE = 'au'
19NTON_DIR_SUFFIX = '_nton'
20MTON_DIR_SUFFIX = '_mton'
Frank Farzan37761d12011-12-01 14:29:08 -080021DEV_BUILD_PREFIX = 'dev'
22
23
24class DevServerUtilError(Exception):
25 """Exception classes used by this module."""
26 pass
27
28
29def ParsePayloadList(payload_list):
30 """Parse and return the full/delta payload URLs.
31
32 Args:
33 payload_list: A list of Google Storage URLs.
34
35 Returns:
36 Tuple of 3 payloads URLs: (full, nton, mton).
37
38 Raises:
39 DevServerUtilError: If payloads missing or invalid.
40 """
41 full_payload_url = None
42 mton_payload_url = None
43 nton_payload_url = None
44 for payload in payload_list:
45 if '_full_' in payload:
46 full_payload_url = payload
47 elif '_delta_' in payload:
48 # e.g. chromeos_{from_version}_{to_version}_x86-generic_delta_dev.bin
49 from_version, to_version = payload.rsplit('/', 1)[1].split('_')[1:3]
50 if from_version == to_version:
51 nton_payload_url = payload
52 else:
53 mton_payload_url = payload
54
Chris Sosa1228a1a2012-05-22 17:12:13 -070055 if not full_payload_url:
Frank Farzan37761d12011-12-01 14:29:08 -080056 raise DevServerUtilError(
Chris Sosa1228a1a2012-05-22 17:12:13 -070057 'Full payload is missing or has unexpected name format.', payload_list)
Frank Farzan37761d12011-12-01 14:29:08 -080058
59 return full_payload_url, nton_payload_url, mton_payload_url
60
61
Yu-Ju Honge61cbe92012-07-10 14:10:26 -070062def _GetAutotestURL(archive_url):
63 """Find out what type of autotest tarball is available and return the
64 coresponding URL."""
65
66 cmd = 'gsutil ls %s/autotest.*' % archive_url
67 msg = 'Failed to retrieve the list of autotest tarballs.'
68 autotest_tarballs = gsutil_util.GSUtilRun(cmd, msg).splitlines()
69
70 # Use autotest.tar if it is available.
71 for tarball in autotest_tarballs:
72 if os.path.basename(tarball) == downloadable_artifact.AUTOTEST_PACKAGE:
73 return '%s/%s' % (archive_url, downloadable_artifact.AUTOTEST_PACKAGE)
74
75 # Use autotest.tar.bz2 by default for backward compatibility.
76 return '%s/%s' % (archive_url,
77 downloadable_artifact.AUTOTEST_ZIPPED_PACKAGE)
78
Chris Masone3fe44db2012-05-02 10:50:21 -070079def GatherArtifactDownloads(main_staging_dir, archive_url, build, build_dir):
Chris Sosa47a7d4e2012-03-28 11:26:55 -070080 """Generates artifacts that we mean to download and install for autotest.
Frank Farzan37761d12011-12-01 14:29:08 -080081
Chris Sosa47a7d4e2012-03-28 11:26:55 -070082 This method generates the list of artifacts we will need for autotest. These
Chris Masone816e38c2012-05-02 12:22:36 -070083 artifacts are instances of downloadable_artifact.DownloadableArtifact.
84
85 Note, these artifacts can be downloaded asynchronously iff
86 !artifact.Synchronous().
Scott Zawalski51ccf9e2012-03-28 08:16:01 -070087 """
Scott Zawalski51ccf9e2012-03-28 08:16:01 -070088 cmd = 'gsutil ls %s/*.bin' % archive_url
89 msg = 'Failed to get a list of payloads.'
Chris Sosa47a7d4e2012-03-28 11:26:55 -070090 payload_list = gsutil_util.GSUtilRun(cmd, msg).splitlines()
Scott Zawalski51ccf9e2012-03-28 08:16:01 -070091
Chris Sosa47a7d4e2012-03-28 11:26:55 -070092 # First we gather the urls/paths for the update payloads.
93 full_url, nton_url, mton_url = ParsePayloadList(payload_list)
Scott Zawalski51ccf9e2012-03-28 08:16:01 -070094
Chris Sosa47a7d4e2012-03-28 11:26:55 -070095 full_payload = os.path.join(build_dir, downloadable_artifact.ROOT_UPDATE)
Scott Zawalski51ccf9e2012-03-28 08:16:01 -070096
Chris Sosa47a7d4e2012-03-28 11:26:55 -070097 artifacts = []
98 artifacts.append(downloadable_artifact.DownloadableArtifact(full_url,
99 main_staging_dir, full_payload, synchronous=True))
Chris Sosa1228a1a2012-05-22 17:12:13 -0700100
101 if nton_url:
102 nton_payload = os.path.join(build_dir, AU_BASE, build + NTON_DIR_SUFFIX,
103 downloadable_artifact.ROOT_UPDATE)
104 artifacts.append(downloadable_artifact.AUTestPayload(nton_url,
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700105 main_staging_dir, nton_payload))
Chris Sosa1228a1a2012-05-22 17:12:13 -0700106
Chris Sosa781ba6d2012-04-11 12:44:43 -0700107 if mton_url:
108 mton_payload = os.path.join(build_dir, AU_BASE, build + MTON_DIR_SUFFIX,
109 downloadable_artifact.ROOT_UPDATE)
110 artifacts.append(downloadable_artifact.AUTestPayload(
111 mton_url, main_staging_dir, mton_payload))
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700112
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700113
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700114 # Next we gather the miscellaneous payloads.
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700115 autotest_url = _GetAutotestURL(archive_url)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700116 stateful_url = archive_url + '/' + downloadable_artifact.STATEFUL_UPDATE
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700117 test_suites_url = (archive_url + '/' +
118 downloadable_artifact.TEST_SUITES_PACKAGE)
119
120 stateful_payload = os.path.join(build_dir,
121 downloadable_artifact.STATEFUL_UPDATE)
122
123 artifacts.append(downloadable_artifact.DownloadableArtifact(
124 stateful_url, main_staging_dir, stateful_payload, synchronous=True))
125 artifacts.append(downloadable_artifact.AutotestTarball(
126 autotest_url, main_staging_dir, build_dir))
127 artifacts.append(downloadable_artifact.Tarball(
128 test_suites_url, main_staging_dir, build_dir, synchronous=True))
129 return artifacts
Scott Zawalski51ccf9e2012-03-28 08:16:01 -0700130
131
Chris Masone816e38c2012-05-02 12:22:36 -0700132def GatherSymbolArtifactDownloads(temp_download_dir, archive_url, staging_dir,
133 timeout=600, delay=10):
134 """Generates debug symbol artifacts that we mean to download and stage.
135
136 This method generates the list of artifacts we will need to
137 symbolicate crash dumps that occur during autotest runs. These
138 artifacts are instances of downloadable_artifact.DownloadableArtifact.
139
140 This will poll google storage until the debug symbol artifact becomes
141 available, or until the 10 minute timeout is up.
142
143 @param temp_download_dir: the tempdir into which we're downloading artifacts
144 prior to staging them.
145 @param archive_url: the google storage url of the bucket where the debug
146 symbols for the desired build are stored.
147 @param staging_dir: the dir into which to stage the symbols
148
149 @return an iterable of one DebugTarball pointing to the right debug symbols.
150 This is an iterable so that it's similar to GatherArtifactDownloads.
151 Also, it's possible that someday we might have more than one.
152 """
153 symbol_url = archive_url + '/' + downloadable_artifact.DEBUG_SYMBOLS
154 cmd = 'gsutil ls %s' % symbol_url
155 msg = 'Debug symbols for %s not archived.' % archive_url
156
157 deadline = time.time() + timeout
158 while time.time() < deadline:
159 to_delay = delay + random.choice([-1, 1]) * random.random() * .5 * delay
160 try:
161 gsutil_util.GSUtilRun(cmd, msg)
162 break
163 except gsutil_util.GSUtilError as e:
164 cherrypy.log('%s, Retrying in %f seconds...' % (e, to_delay),
165 'SYMBOL_DOWNLOAD')
166 time.sleep(to_delay)
167 else:
168 # On the last try, run and allow exceptions to escape.
169 gsutil_util.GSUtilRun(cmd, msg)
170
171 return [downloadable_artifact.DebugTarball(symbol_url, temp_download_dir,
172 staging_dir)]
173
174
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700175def PrepareBuildDirectory(build_dir):
176 """Preliminary staging of installation directory for build.
Scott Zawalski51ccf9e2012-03-28 08:16:01 -0700177
178 Args:
Frank Farzan37761d12011-12-01 14:29:08 -0800179 build_dir: Directory to install build components into.
180 """
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700181 if not os.path.isdir(build_dir):
182 os.path.makedirs(build_dir)
Frank Farzan37761d12011-12-01 14:29:08 -0800183
184 # Create blank chromiumos_test_image.bin. Otherwise the Dev Server will
185 # try to rebuild it unnecessarily.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700186 test_image = os.path.join(build_dir, downloadable_artifact.TEST_IMAGE)
Frank Farzan37761d12011-12-01 14:29:08 -0800187 open(test_image, 'a').close()
188
Frank Farzan37761d12011-12-01 14:29:08 -0800189
190def SafeSandboxAccess(static_dir, path):
191 """Verify that the path is in static_dir.
192
193 Args:
194 static_dir: Directory where builds are served from.
195 path: Path to verify.
196
197 Returns:
198 True if path is in static_dir, False otherwise
199 """
200 static_dir = os.path.realpath(static_dir)
201 path = os.path.realpath(path)
202 return (path.startswith(static_dir) and path != static_dir)
203
204
205def AcquireLock(static_dir, tag):
206 """Acquires a lock for a given tag.
207
208 Creates a directory for the specified tag, telling other
209 components the resource/task represented by the tag is unavailable.
210
211 Args:
212 static_dir: Directory where builds are served from.
213 tag: Unique resource/task identifier. Use '/' for nested tags.
214
215 Returns:
216 Path to the created directory or None if creation failed.
217
218 Raises:
219 DevServerUtilError: If lock can't be acquired.
220 """
221 build_dir = os.path.join(static_dir, tag)
222 if not SafeSandboxAccess(static_dir, build_dir):
Chris Sosa9164ca32012-03-28 11:04:50 -0700223 raise DevServerUtilError('Invalid tag "%s".' % tag)
Frank Farzan37761d12011-12-01 14:29:08 -0800224
225 try:
226 os.makedirs(build_dir)
227 except OSError, e:
228 if e.errno == errno.EEXIST:
229 raise DevServerUtilError(str(e))
230 else:
231 raise
232
233 return build_dir
234
235
236def ReleaseLock(static_dir, tag):
237 """Releases the lock for a given tag. Removes lock directory content.
238
239 Args:
240 static_dir: Directory where builds are served from.
241 tag: Unique resource/task identifier. Use '/' for nested tags.
242
243 Raises:
244 DevServerUtilError: If lock can't be released.
245 """
246 build_dir = os.path.join(static_dir, tag)
247 if not SafeSandboxAccess(static_dir, build_dir):
248 raise DevServerUtilError('Invaid tag "%s".' % tag)
249
250 shutil.rmtree(build_dir)
251
252
253def FindMatchingBoards(static_dir, board):
254 """Returns a list of boards given a partial board name.
255
256 Args:
257 static_dir: Directory where builds are served from.
258 board: Partial board name for this build; e.g. x86-generic.
259
260 Returns:
261 Returns a list of boards given a partial board.
262 """
263 return [brd for brd in os.listdir(static_dir) if board in brd]
264
265
266def FindMatchingBuilds(static_dir, board, build):
267 """Returns a list of matching builds given a board and partial build.
268
269 Args:
270 static_dir: Directory where builds are served from.
271 board: Partial board name for this build; e.g. x86-generic-release.
272 build: Partial build string to look for; e.g. R17-1234.
273
274 Returns:
275 Returns a list of (board, build) tuples given a partial board and build.
276 """
277 matches = []
278 for brd in FindMatchingBoards(static_dir, board):
279 a = [(brd, bld) for bld in
280 os.listdir(os.path.join(static_dir, brd)) if build in bld]
281 matches.extend(a)
282 return matches
283
284
Scott Zawalski16954532012-03-20 15:31:36 -0400285def GetLatestBuildVersion(static_dir, target, milestone=None):
Frank Farzan37761d12011-12-01 14:29:08 -0800286 """Retrieves the latest build version for a given board.
287
288 Args:
289 static_dir: Directory where builds are served from.
Scott Zawalski16954532012-03-20 15:31:36 -0400290 target: The build target, typically a combination of the board and the
291 type of build e.g. x86-mario-release.
292 milestone: For latest build set to None, for builds only in a specific
293 milestone set to a str of format Rxx (e.g. R16). Default: None.
Frank Farzan37761d12011-12-01 14:29:08 -0800294
295 Returns:
Scott Zawalski16954532012-03-20 15:31:36 -0400296 If latest found, a full build string is returned e.g. R17-1234.0.0-a1-b983.
297 If no latest is found for some reason or another a '' string is returned.
Frank Farzan37761d12011-12-01 14:29:08 -0800298
299 Raises:
Scott Zawalski16954532012-03-20 15:31:36 -0400300 DevServerUtilError: If for some reason the latest build cannot be
301 deteremined, this could be due to the dir not existing or no builds
302 being present after filtering on milestone.
Frank Farzan37761d12011-12-01 14:29:08 -0800303 """
Scott Zawalski16954532012-03-20 15:31:36 -0400304 target_path = os.path.join(static_dir, target)
305 if not os.path.isdir(target_path):
306 raise DevServerUtilError('Cannot find path %s' % target_path)
Frank Farzan37761d12011-12-01 14:29:08 -0800307
Scott Zawalski16954532012-03-20 15:31:36 -0400308 builds = [distutils.version.LooseVersion(build) for build in
309 os.listdir(target_path)]
Frank Farzan37761d12011-12-01 14:29:08 -0800310
Scott Zawalski16954532012-03-20 15:31:36 -0400311 if milestone and builds:
312 # Check if milestone Rxx is in the string representation of the build.
313 builds = filter(lambda x: milestone.upper() in str(x), builds)
Frank Farzan37761d12011-12-01 14:29:08 -0800314
Scott Zawalski16954532012-03-20 15:31:36 -0400315 if not builds:
316 raise DevServerUtilError('Could not determine build for %s' % target)
Frank Farzan37761d12011-12-01 14:29:08 -0800317
Scott Zawalski16954532012-03-20 15:31:36 -0400318 return str(max(builds))
Frank Farzan37761d12011-12-01 14:29:08 -0800319
320
321def CloneBuild(static_dir, board, build, tag, force=False):
322 """Clone an official build into the developer sandbox.
323
324 Developer sandbox directory must already exist.
325
326 Args:
327 static_dir: Directory where builds are served from.
328 board: Fully qualified board name; e.g. x86-generic-release.
329 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
330 tag: Unique resource/task identifier. Use '/' for nested tags.
331 force: Force re-creation of build_dir even if it already exists.
332
333 Returns:
334 The path to the new build.
335 """
336 # Create the developer build directory.
337 dev_static_dir = os.path.join(static_dir, DEV_BUILD_PREFIX)
338 dev_build_dir = os.path.join(dev_static_dir, tag)
339 official_build_dir = os.path.join(static_dir, board, build)
340 cherrypy.log('Cloning %s -> %s' % (official_build_dir, dev_build_dir),
341 'DEVSERVER_UTIL')
342 dev_build_exists = False
343 try:
344 AcquireLock(dev_static_dir, tag)
345 except DevServerUtilError:
346 dev_build_exists = True
347 if force:
348 dev_build_exists = False
349 ReleaseLock(dev_static_dir, tag)
350 AcquireLock(dev_static_dir, tag)
351
352 # Make a copy of the official build, only take necessary files.
353 if not dev_build_exists:
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700354 copy_list = [downloadable_artifact.TEST_IMAGE,
355 downloadable_artifact.ROOT_UPDATE,
356 downloadable_artifact.STATEFUL_UPDATE]
Frank Farzan37761d12011-12-01 14:29:08 -0800357 for f in copy_list:
358 shutil.copy(os.path.join(official_build_dir, f), dev_build_dir)
359
360 return dev_build_dir
361
Scott Zawalski84a39c92012-01-13 15:12:42 -0500362
363def GetControlFile(static_dir, build, control_path):
Frank Farzan37761d12011-12-01 14:29:08 -0800364 """Attempts to pull the requested control file from the Dev Server.
365
366 Args:
367 static_dir: Directory where builds are served from.
Frank Farzan37761d12011-12-01 14:29:08 -0800368 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
369 control_path: Path to control file on Dev Server relative to Autotest root.
370
371 Raises:
372 DevServerUtilError: If lock can't be acquired.
373
374 Returns:
375 Content of the requested control file.
376 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500377 # Be forgiving if the user passes in the control_path with a leading /
378 control_path = control_path.lstrip('/')
Scott Zawalski84a39c92012-01-13 15:12:42 -0500379 control_path = os.path.join(static_dir, build, 'autotest',
Scott Zawalski4647ce62012-01-03 17:17:28 -0500380 control_path)
Frank Farzan37761d12011-12-01 14:29:08 -0800381 if not SafeSandboxAccess(static_dir, control_path):
382 raise DevServerUtilError('Invaid control file "%s".' % control_path)
383
Scott Zawalski84a39c92012-01-13 15:12:42 -0500384 if not os.path.exists(control_path):
385 # TODO(scottz): Come up with some sort of error mechanism.
386 # crosbug.com/25040
387 return 'Unknown control path %s' % control_path
388
Frank Farzan37761d12011-12-01 14:29:08 -0800389 with open(control_path, 'r') as control_file:
390 return control_file.read()
391
392
Scott Zawalski84a39c92012-01-13 15:12:42 -0500393def GetControlFileList(static_dir, build):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500394 """List all control|control. files in the specified board/build path.
395
396 Args:
397 static_dir: Directory where builds are served from.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500398 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
399
400 Raises:
401 DevServerUtilError: If path is outside of sandbox.
402
403 Returns:
404 String of each file separated by a newline.
405 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500406 autotest_dir = os.path.join(static_dir, build, 'autotest/')
Scott Zawalski4647ce62012-01-03 17:17:28 -0500407 if not SafeSandboxAccess(static_dir, autotest_dir):
408 raise DevServerUtilError('Autotest dir not in sandbox "%s".' % autotest_dir)
409
410 control_files = set()
Scott Zawalski84a39c92012-01-13 15:12:42 -0500411 if not os.path.exists(autotest_dir):
412 # TODO(scottz): Come up with some sort of error mechanism.
413 # crosbug.com/25040
414 return 'Unknown build path %s' % autotest_dir
415
Scott Zawalski4647ce62012-01-03 17:17:28 -0500416 for entry in os.walk(autotest_dir):
417 dir_path, _, files = entry
418 for file_entry in files:
419 if file_entry.startswith('control.') or file_entry == 'control':
420 control_files.add(os.path.join(dir_path,
Chris Sosaea148d92012-03-06 16:22:04 -0800421 file_entry).replace(autotest_dir, ''))
Scott Zawalski4647ce62012-01-03 17:17:28 -0500422
423 return '\n'.join(control_files)
424
425
Frank Farzan37761d12011-12-01 14:29:08 -0800426def ListAutoupdateTargets(static_dir, board, build):
427 """Returns a list of autoupdate test targets for the given board, build.
428
429 Args:
430 static_dir: Directory where builds are served from.
431 board: Fully qualified board name; e.g. x86-generic-release.
432 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
433
434 Returns:
435 List of autoupdate test targets; e.g. ['0.14.747.0-r2bf8859c-b2927_nton']
436 """
437 return os.listdir(os.path.join(static_dir, board, build, AU_BASE))