blob: c2a659bbdb7654f910c7dd9b9a4d47e18e86f08c [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 Sosa781ba6d2012-04-11 12:44:43 -070055 if not full_payload_url or not nton_payload_url:
Frank Farzan37761d12011-12-01 14:29:08 -080056 raise DevServerUtilError(
57 'Payloads are missing or have unexpected name formats.', payload_list)
58
59 return full_payload_url, nton_payload_url, mton_payload_url
60
61
Chris Masone3fe44db2012-05-02 10:50:21 -070062def GatherArtifactDownloads(main_staging_dir, archive_url, build, build_dir):
Chris Sosa47a7d4e2012-03-28 11:26:55 -070063 """Generates artifacts that we mean to download and install for autotest.
Frank Farzan37761d12011-12-01 14:29:08 -080064
Chris Sosa47a7d4e2012-03-28 11:26:55 -070065 This method generates the list of artifacts we will need for autotest. These
Chris Masone816e38c2012-05-02 12:22:36 -070066 artifacts are instances of downloadable_artifact.DownloadableArtifact.
67
68 Note, these artifacts can be downloaded asynchronously iff
69 !artifact.Synchronous().
Scott Zawalski51ccf9e2012-03-28 08:16:01 -070070 """
Scott Zawalski51ccf9e2012-03-28 08:16:01 -070071 cmd = 'gsutil ls %s/*.bin' % archive_url
72 msg = 'Failed to get a list of payloads.'
Chris Sosa47a7d4e2012-03-28 11:26:55 -070073 payload_list = gsutil_util.GSUtilRun(cmd, msg).splitlines()
Scott Zawalski51ccf9e2012-03-28 08:16:01 -070074
Chris Sosa47a7d4e2012-03-28 11:26:55 -070075 # First we gather the urls/paths for the update payloads.
76 full_url, nton_url, mton_url = ParsePayloadList(payload_list)
Scott Zawalski51ccf9e2012-03-28 08:16:01 -070077
Chris Sosa47a7d4e2012-03-28 11:26:55 -070078 full_payload = os.path.join(build_dir, downloadable_artifact.ROOT_UPDATE)
79 nton_payload = os.path.join(build_dir, AU_BASE, build + NTON_DIR_SUFFIX,
80 downloadable_artifact.ROOT_UPDATE)
Scott Zawalski51ccf9e2012-03-28 08:16:01 -070081
Chris Sosa47a7d4e2012-03-28 11:26:55 -070082 artifacts = []
83 artifacts.append(downloadable_artifact.DownloadableArtifact(full_url,
84 main_staging_dir, full_payload, synchronous=True))
85 artifacts.append(downloadable_artifact.AUTestPayload(nton_url,
86 main_staging_dir, nton_payload))
Chris Sosa781ba6d2012-04-11 12:44:43 -070087 if mton_url:
88 mton_payload = os.path.join(build_dir, AU_BASE, build + MTON_DIR_SUFFIX,
89 downloadable_artifact.ROOT_UPDATE)
90 artifacts.append(downloadable_artifact.AUTestPayload(
91 mton_url, main_staging_dir, mton_payload))
Chris Sosa47a7d4e2012-03-28 11:26:55 -070092
93 # Next we gather the miscellaneous payloads.
94 stateful_url = archive_url + '/' + downloadable_artifact.STATEFUL_UPDATE
95 autotest_url = archive_url + '/' + downloadable_artifact.AUTOTEST_PACKAGE
96 test_suites_url = (archive_url + '/' +
97 downloadable_artifact.TEST_SUITES_PACKAGE)
98
99 stateful_payload = os.path.join(build_dir,
100 downloadable_artifact.STATEFUL_UPDATE)
101
102 artifacts.append(downloadable_artifact.DownloadableArtifact(
103 stateful_url, main_staging_dir, stateful_payload, synchronous=True))
104 artifacts.append(downloadable_artifact.AutotestTarball(
105 autotest_url, main_staging_dir, build_dir))
106 artifacts.append(downloadable_artifact.Tarball(
107 test_suites_url, main_staging_dir, build_dir, synchronous=True))
108 return artifacts
Scott Zawalski51ccf9e2012-03-28 08:16:01 -0700109
110
Chris Masone816e38c2012-05-02 12:22:36 -0700111def GatherSymbolArtifactDownloads(temp_download_dir, archive_url, staging_dir,
112 timeout=600, delay=10):
113 """Generates debug symbol artifacts that we mean to download and stage.
114
115 This method generates the list of artifacts we will need to
116 symbolicate crash dumps that occur during autotest runs. These
117 artifacts are instances of downloadable_artifact.DownloadableArtifact.
118
119 This will poll google storage until the debug symbol artifact becomes
120 available, or until the 10 minute timeout is up.
121
122 @param temp_download_dir: the tempdir into which we're downloading artifacts
123 prior to staging them.
124 @param archive_url: the google storage url of the bucket where the debug
125 symbols for the desired build are stored.
126 @param staging_dir: the dir into which to stage the symbols
127
128 @return an iterable of one DebugTarball pointing to the right debug symbols.
129 This is an iterable so that it's similar to GatherArtifactDownloads.
130 Also, it's possible that someday we might have more than one.
131 """
132 symbol_url = archive_url + '/' + downloadable_artifact.DEBUG_SYMBOLS
133 cmd = 'gsutil ls %s' % symbol_url
134 msg = 'Debug symbols for %s not archived.' % archive_url
135
136 deadline = time.time() + timeout
137 while time.time() < deadline:
138 to_delay = delay + random.choice([-1, 1]) * random.random() * .5 * delay
139 try:
140 gsutil_util.GSUtilRun(cmd, msg)
141 break
142 except gsutil_util.GSUtilError as e:
143 cherrypy.log('%s, Retrying in %f seconds...' % (e, to_delay),
144 'SYMBOL_DOWNLOAD')
145 time.sleep(to_delay)
146 else:
147 # On the last try, run and allow exceptions to escape.
148 gsutil_util.GSUtilRun(cmd, msg)
149
150 return [downloadable_artifact.DebugTarball(symbol_url, temp_download_dir,
151 staging_dir)]
152
153
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700154def PrepareBuildDirectory(build_dir):
155 """Preliminary staging of installation directory for build.
Scott Zawalski51ccf9e2012-03-28 08:16:01 -0700156
157 Args:
Frank Farzan37761d12011-12-01 14:29:08 -0800158 build_dir: Directory to install build components into.
159 """
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700160 if not os.path.isdir(build_dir):
161 os.path.makedirs(build_dir)
Frank Farzan37761d12011-12-01 14:29:08 -0800162
163 # Create blank chromiumos_test_image.bin. Otherwise the Dev Server will
164 # try to rebuild it unnecessarily.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700165 test_image = os.path.join(build_dir, downloadable_artifact.TEST_IMAGE)
Frank Farzan37761d12011-12-01 14:29:08 -0800166 open(test_image, 'a').close()
167
Frank Farzan37761d12011-12-01 14:29:08 -0800168
169def SafeSandboxAccess(static_dir, path):
170 """Verify that the path is in static_dir.
171
172 Args:
173 static_dir: Directory where builds are served from.
174 path: Path to verify.
175
176 Returns:
177 True if path is in static_dir, False otherwise
178 """
179 static_dir = os.path.realpath(static_dir)
180 path = os.path.realpath(path)
181 return (path.startswith(static_dir) and path != static_dir)
182
183
184def AcquireLock(static_dir, tag):
185 """Acquires a lock for a given tag.
186
187 Creates a directory for the specified tag, telling other
188 components the resource/task represented by the tag is unavailable.
189
190 Args:
191 static_dir: Directory where builds are served from.
192 tag: Unique resource/task identifier. Use '/' for nested tags.
193
194 Returns:
195 Path to the created directory or None if creation failed.
196
197 Raises:
198 DevServerUtilError: If lock can't be acquired.
199 """
200 build_dir = os.path.join(static_dir, tag)
201 if not SafeSandboxAccess(static_dir, build_dir):
Chris Sosa9164ca32012-03-28 11:04:50 -0700202 raise DevServerUtilError('Invalid tag "%s".' % tag)
Frank Farzan37761d12011-12-01 14:29:08 -0800203
204 try:
205 os.makedirs(build_dir)
206 except OSError, e:
207 if e.errno == errno.EEXIST:
208 raise DevServerUtilError(str(e))
209 else:
210 raise
211
212 return build_dir
213
214
215def ReleaseLock(static_dir, tag):
216 """Releases the lock for a given tag. Removes lock directory content.
217
218 Args:
219 static_dir: Directory where builds are served from.
220 tag: Unique resource/task identifier. Use '/' for nested tags.
221
222 Raises:
223 DevServerUtilError: If lock can't be released.
224 """
225 build_dir = os.path.join(static_dir, tag)
226 if not SafeSandboxAccess(static_dir, build_dir):
227 raise DevServerUtilError('Invaid tag "%s".' % tag)
228
229 shutil.rmtree(build_dir)
230
231
232def FindMatchingBoards(static_dir, board):
233 """Returns a list of boards given a partial board name.
234
235 Args:
236 static_dir: Directory where builds are served from.
237 board: Partial board name for this build; e.g. x86-generic.
238
239 Returns:
240 Returns a list of boards given a partial board.
241 """
242 return [brd for brd in os.listdir(static_dir) if board in brd]
243
244
245def FindMatchingBuilds(static_dir, board, build):
246 """Returns a list of matching builds given a board and partial build.
247
248 Args:
249 static_dir: Directory where builds are served from.
250 board: Partial board name for this build; e.g. x86-generic-release.
251 build: Partial build string to look for; e.g. R17-1234.
252
253 Returns:
254 Returns a list of (board, build) tuples given a partial board and build.
255 """
256 matches = []
257 for brd in FindMatchingBoards(static_dir, board):
258 a = [(brd, bld) for bld in
259 os.listdir(os.path.join(static_dir, brd)) if build in bld]
260 matches.extend(a)
261 return matches
262
263
Scott Zawalski16954532012-03-20 15:31:36 -0400264def GetLatestBuildVersion(static_dir, target, milestone=None):
Frank Farzan37761d12011-12-01 14:29:08 -0800265 """Retrieves the latest build version for a given board.
266
267 Args:
268 static_dir: Directory where builds are served from.
Scott Zawalski16954532012-03-20 15:31:36 -0400269 target: The build target, typically a combination of the board and the
270 type of build e.g. x86-mario-release.
271 milestone: For latest build set to None, for builds only in a specific
272 milestone set to a str of format Rxx (e.g. R16). Default: None.
Frank Farzan37761d12011-12-01 14:29:08 -0800273
274 Returns:
Scott Zawalski16954532012-03-20 15:31:36 -0400275 If latest found, a full build string is returned e.g. R17-1234.0.0-a1-b983.
276 If no latest is found for some reason or another a '' string is returned.
Frank Farzan37761d12011-12-01 14:29:08 -0800277
278 Raises:
Scott Zawalski16954532012-03-20 15:31:36 -0400279 DevServerUtilError: If for some reason the latest build cannot be
280 deteremined, this could be due to the dir not existing or no builds
281 being present after filtering on milestone.
Frank Farzan37761d12011-12-01 14:29:08 -0800282 """
Scott Zawalski16954532012-03-20 15:31:36 -0400283 target_path = os.path.join(static_dir, target)
284 if not os.path.isdir(target_path):
285 raise DevServerUtilError('Cannot find path %s' % target_path)
Frank Farzan37761d12011-12-01 14:29:08 -0800286
Scott Zawalski16954532012-03-20 15:31:36 -0400287 builds = [distutils.version.LooseVersion(build) for build in
288 os.listdir(target_path)]
Frank Farzan37761d12011-12-01 14:29:08 -0800289
Scott Zawalski16954532012-03-20 15:31:36 -0400290 if milestone and builds:
291 # Check if milestone Rxx is in the string representation of the build.
292 builds = filter(lambda x: milestone.upper() in str(x), builds)
Frank Farzan37761d12011-12-01 14:29:08 -0800293
Scott Zawalski16954532012-03-20 15:31:36 -0400294 if not builds:
295 raise DevServerUtilError('Could not determine build for %s' % target)
Frank Farzan37761d12011-12-01 14:29:08 -0800296
Scott Zawalski16954532012-03-20 15:31:36 -0400297 return str(max(builds))
Frank Farzan37761d12011-12-01 14:29:08 -0800298
299
300def CloneBuild(static_dir, board, build, tag, force=False):
301 """Clone an official build into the developer sandbox.
302
303 Developer sandbox directory must already exist.
304
305 Args:
306 static_dir: Directory where builds are served from.
307 board: Fully qualified board name; e.g. x86-generic-release.
308 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
309 tag: Unique resource/task identifier. Use '/' for nested tags.
310 force: Force re-creation of build_dir even if it already exists.
311
312 Returns:
313 The path to the new build.
314 """
315 # Create the developer build directory.
316 dev_static_dir = os.path.join(static_dir, DEV_BUILD_PREFIX)
317 dev_build_dir = os.path.join(dev_static_dir, tag)
318 official_build_dir = os.path.join(static_dir, board, build)
319 cherrypy.log('Cloning %s -> %s' % (official_build_dir, dev_build_dir),
320 'DEVSERVER_UTIL')
321 dev_build_exists = False
322 try:
323 AcquireLock(dev_static_dir, tag)
324 except DevServerUtilError:
325 dev_build_exists = True
326 if force:
327 dev_build_exists = False
328 ReleaseLock(dev_static_dir, tag)
329 AcquireLock(dev_static_dir, tag)
330
331 # Make a copy of the official build, only take necessary files.
332 if not dev_build_exists:
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700333 copy_list = [downloadable_artifact.TEST_IMAGE,
334 downloadable_artifact.ROOT_UPDATE,
335 downloadable_artifact.STATEFUL_UPDATE]
Frank Farzan37761d12011-12-01 14:29:08 -0800336 for f in copy_list:
337 shutil.copy(os.path.join(official_build_dir, f), dev_build_dir)
338
339 return dev_build_dir
340
Scott Zawalski84a39c92012-01-13 15:12:42 -0500341
342def GetControlFile(static_dir, build, control_path):
Frank Farzan37761d12011-12-01 14:29:08 -0800343 """Attempts to pull the requested control file from the Dev Server.
344
345 Args:
346 static_dir: Directory where builds are served from.
Frank Farzan37761d12011-12-01 14:29:08 -0800347 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
348 control_path: Path to control file on Dev Server relative to Autotest root.
349
350 Raises:
351 DevServerUtilError: If lock can't be acquired.
352
353 Returns:
354 Content of the requested control file.
355 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500356 # Be forgiving if the user passes in the control_path with a leading /
357 control_path = control_path.lstrip('/')
Scott Zawalski84a39c92012-01-13 15:12:42 -0500358 control_path = os.path.join(static_dir, build, 'autotest',
Scott Zawalski4647ce62012-01-03 17:17:28 -0500359 control_path)
Frank Farzan37761d12011-12-01 14:29:08 -0800360 if not SafeSandboxAccess(static_dir, control_path):
361 raise DevServerUtilError('Invaid control file "%s".' % control_path)
362
Scott Zawalski84a39c92012-01-13 15:12:42 -0500363 if not os.path.exists(control_path):
364 # TODO(scottz): Come up with some sort of error mechanism.
365 # crosbug.com/25040
366 return 'Unknown control path %s' % control_path
367
Frank Farzan37761d12011-12-01 14:29:08 -0800368 with open(control_path, 'r') as control_file:
369 return control_file.read()
370
371
Scott Zawalski84a39c92012-01-13 15:12:42 -0500372def GetControlFileList(static_dir, build):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500373 """List all control|control. files in the specified board/build path.
374
375 Args:
376 static_dir: Directory where builds are served from.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500377 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
378
379 Raises:
380 DevServerUtilError: If path is outside of sandbox.
381
382 Returns:
383 String of each file separated by a newline.
384 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500385 autotest_dir = os.path.join(static_dir, build, 'autotest/')
Scott Zawalski4647ce62012-01-03 17:17:28 -0500386 if not SafeSandboxAccess(static_dir, autotest_dir):
387 raise DevServerUtilError('Autotest dir not in sandbox "%s".' % autotest_dir)
388
389 control_files = set()
Scott Zawalski84a39c92012-01-13 15:12:42 -0500390 if not os.path.exists(autotest_dir):
391 # TODO(scottz): Come up with some sort of error mechanism.
392 # crosbug.com/25040
393 return 'Unknown build path %s' % autotest_dir
394
Scott Zawalski4647ce62012-01-03 17:17:28 -0500395 for entry in os.walk(autotest_dir):
396 dir_path, _, files = entry
397 for file_entry in files:
398 if file_entry.startswith('control.') or file_entry == 'control':
399 control_files.add(os.path.join(dir_path,
Chris Sosaea148d92012-03-06 16:22:04 -0800400 file_entry).replace(autotest_dir, ''))
Scott Zawalski4647ce62012-01-03 17:17:28 -0500401
402 return '\n'.join(control_files)
403
404
Frank Farzan37761d12011-12-01 14:29:08 -0800405def ListAutoupdateTargets(static_dir, board, build):
406 """Returns a list of autoupdate test targets for the given board, build.
407
408 Args:
409 static_dir: Directory where builds are served from.
410 board: Fully qualified board name; e.g. x86-generic-release.
411 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
412
413 Returns:
414 List of autoupdate test targets; e.g. ['0.14.747.0-r2bf8859c-b2927_nton']
415 """
416 return os.listdir(os.path.join(static_dir, board, build, AU_BASE))