blob: a4633dba2b51c90d6b73101b12db4505779f61f6 [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
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)
Scott Zawalski51ccf9e2012-03-28 08:16:01 -070079
Chris Sosa47a7d4e2012-03-28 11:26:55 -070080 artifacts = []
81 artifacts.append(downloadable_artifact.DownloadableArtifact(full_url,
82 main_staging_dir, full_payload, synchronous=True))
Chris Sosa1228a1a2012-05-22 17:12:13 -070083
84 if nton_url:
85 nton_payload = os.path.join(build_dir, AU_BASE, build + NTON_DIR_SUFFIX,
86 downloadable_artifact.ROOT_UPDATE)
87 artifacts.append(downloadable_artifact.AUTestPayload(nton_url,
Chris Sosa47a7d4e2012-03-28 11:26:55 -070088 main_staging_dir, nton_payload))
Chris Sosa1228a1a2012-05-22 17:12:13 -070089
Chris Sosa781ba6d2012-04-11 12:44:43 -070090 if mton_url:
91 mton_payload = os.path.join(build_dir, AU_BASE, build + MTON_DIR_SUFFIX,
92 downloadable_artifact.ROOT_UPDATE)
93 artifacts.append(downloadable_artifact.AUTestPayload(
94 mton_url, main_staging_dir, mton_payload))
Chris Sosa47a7d4e2012-03-28 11:26:55 -070095
96 # Next we gather the miscellaneous payloads.
97 stateful_url = archive_url + '/' + downloadable_artifact.STATEFUL_UPDATE
98 autotest_url = archive_url + '/' + downloadable_artifact.AUTOTEST_PACKAGE
99 test_suites_url = (archive_url + '/' +
100 downloadable_artifact.TEST_SUITES_PACKAGE)
101
102 stateful_payload = os.path.join(build_dir,
103 downloadable_artifact.STATEFUL_UPDATE)
104
105 artifacts.append(downloadable_artifact.DownloadableArtifact(
106 stateful_url, main_staging_dir, stateful_payload, synchronous=True))
107 artifacts.append(downloadable_artifact.AutotestTarball(
108 autotest_url, main_staging_dir, build_dir))
109 artifacts.append(downloadable_artifact.Tarball(
110 test_suites_url, main_staging_dir, build_dir, synchronous=True))
111 return artifacts
Scott Zawalski51ccf9e2012-03-28 08:16:01 -0700112
113
Chris Masone816e38c2012-05-02 12:22:36 -0700114def GatherSymbolArtifactDownloads(temp_download_dir, archive_url, staging_dir,
115 timeout=600, delay=10):
116 """Generates debug symbol artifacts that we mean to download and stage.
117
118 This method generates the list of artifacts we will need to
119 symbolicate crash dumps that occur during autotest runs. These
120 artifacts are instances of downloadable_artifact.DownloadableArtifact.
121
122 This will poll google storage until the debug symbol artifact becomes
123 available, or until the 10 minute timeout is up.
124
125 @param temp_download_dir: the tempdir into which we're downloading artifacts
126 prior to staging them.
127 @param archive_url: the google storage url of the bucket where the debug
128 symbols for the desired build are stored.
129 @param staging_dir: the dir into which to stage the symbols
130
131 @return an iterable of one DebugTarball pointing to the right debug symbols.
132 This is an iterable so that it's similar to GatherArtifactDownloads.
133 Also, it's possible that someday we might have more than one.
134 """
135 symbol_url = archive_url + '/' + downloadable_artifact.DEBUG_SYMBOLS
136 cmd = 'gsutil ls %s' % symbol_url
137 msg = 'Debug symbols for %s not archived.' % archive_url
138
139 deadline = time.time() + timeout
140 while time.time() < deadline:
141 to_delay = delay + random.choice([-1, 1]) * random.random() * .5 * delay
142 try:
143 gsutil_util.GSUtilRun(cmd, msg)
144 break
145 except gsutil_util.GSUtilError as e:
146 cherrypy.log('%s, Retrying in %f seconds...' % (e, to_delay),
147 'SYMBOL_DOWNLOAD')
148 time.sleep(to_delay)
149 else:
150 # On the last try, run and allow exceptions to escape.
151 gsutil_util.GSUtilRun(cmd, msg)
152
153 return [downloadable_artifact.DebugTarball(symbol_url, temp_download_dir,
154 staging_dir)]
155
156
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700157def PrepareBuildDirectory(build_dir):
158 """Preliminary staging of installation directory for build.
Scott Zawalski51ccf9e2012-03-28 08:16:01 -0700159
160 Args:
Frank Farzan37761d12011-12-01 14:29:08 -0800161 build_dir: Directory to install build components into.
162 """
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700163 if not os.path.isdir(build_dir):
164 os.path.makedirs(build_dir)
Frank Farzan37761d12011-12-01 14:29:08 -0800165
166 # Create blank chromiumos_test_image.bin. Otherwise the Dev Server will
167 # try to rebuild it unnecessarily.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700168 test_image = os.path.join(build_dir, downloadable_artifact.TEST_IMAGE)
Frank Farzan37761d12011-12-01 14:29:08 -0800169 open(test_image, 'a').close()
170
Frank Farzan37761d12011-12-01 14:29:08 -0800171
172def SafeSandboxAccess(static_dir, path):
173 """Verify that the path is in static_dir.
174
175 Args:
176 static_dir: Directory where builds are served from.
177 path: Path to verify.
178
179 Returns:
180 True if path is in static_dir, False otherwise
181 """
182 static_dir = os.path.realpath(static_dir)
183 path = os.path.realpath(path)
184 return (path.startswith(static_dir) and path != static_dir)
185
186
187def AcquireLock(static_dir, tag):
188 """Acquires a lock for a given tag.
189
190 Creates a directory for the specified tag, telling other
191 components the resource/task represented by the tag is unavailable.
192
193 Args:
194 static_dir: Directory where builds are served from.
195 tag: Unique resource/task identifier. Use '/' for nested tags.
196
197 Returns:
198 Path to the created directory or None if creation failed.
199
200 Raises:
201 DevServerUtilError: If lock can't be acquired.
202 """
203 build_dir = os.path.join(static_dir, tag)
204 if not SafeSandboxAccess(static_dir, build_dir):
Chris Sosa9164ca32012-03-28 11:04:50 -0700205 raise DevServerUtilError('Invalid tag "%s".' % tag)
Frank Farzan37761d12011-12-01 14:29:08 -0800206
207 try:
208 os.makedirs(build_dir)
209 except OSError, e:
210 if e.errno == errno.EEXIST:
211 raise DevServerUtilError(str(e))
212 else:
213 raise
214
215 return build_dir
216
217
218def ReleaseLock(static_dir, tag):
219 """Releases the lock for a given tag. Removes lock directory content.
220
221 Args:
222 static_dir: Directory where builds are served from.
223 tag: Unique resource/task identifier. Use '/' for nested tags.
224
225 Raises:
226 DevServerUtilError: If lock can't be released.
227 """
228 build_dir = os.path.join(static_dir, tag)
229 if not SafeSandboxAccess(static_dir, build_dir):
230 raise DevServerUtilError('Invaid tag "%s".' % tag)
231
232 shutil.rmtree(build_dir)
233
234
235def FindMatchingBoards(static_dir, board):
236 """Returns a list of boards given a partial board name.
237
238 Args:
239 static_dir: Directory where builds are served from.
240 board: Partial board name for this build; e.g. x86-generic.
241
242 Returns:
243 Returns a list of boards given a partial board.
244 """
245 return [brd for brd in os.listdir(static_dir) if board in brd]
246
247
248def FindMatchingBuilds(static_dir, board, build):
249 """Returns a list of matching builds given a board and partial build.
250
251 Args:
252 static_dir: Directory where builds are served from.
253 board: Partial board name for this build; e.g. x86-generic-release.
254 build: Partial build string to look for; e.g. R17-1234.
255
256 Returns:
257 Returns a list of (board, build) tuples given a partial board and build.
258 """
259 matches = []
260 for brd in FindMatchingBoards(static_dir, board):
261 a = [(brd, bld) for bld in
262 os.listdir(os.path.join(static_dir, brd)) if build in bld]
263 matches.extend(a)
264 return matches
265
266
Scott Zawalski16954532012-03-20 15:31:36 -0400267def GetLatestBuildVersion(static_dir, target, milestone=None):
Frank Farzan37761d12011-12-01 14:29:08 -0800268 """Retrieves the latest build version for a given board.
269
270 Args:
271 static_dir: Directory where builds are served from.
Scott Zawalski16954532012-03-20 15:31:36 -0400272 target: The build target, typically a combination of the board and the
273 type of build e.g. x86-mario-release.
274 milestone: For latest build set to None, for builds only in a specific
275 milestone set to a str of format Rxx (e.g. R16). Default: None.
Frank Farzan37761d12011-12-01 14:29:08 -0800276
277 Returns:
Scott Zawalski16954532012-03-20 15:31:36 -0400278 If latest found, a full build string is returned e.g. R17-1234.0.0-a1-b983.
279 If no latest is found for some reason or another a '' string is returned.
Frank Farzan37761d12011-12-01 14:29:08 -0800280
281 Raises:
Scott Zawalski16954532012-03-20 15:31:36 -0400282 DevServerUtilError: If for some reason the latest build cannot be
283 deteremined, this could be due to the dir not existing or no builds
284 being present after filtering on milestone.
Frank Farzan37761d12011-12-01 14:29:08 -0800285 """
Scott Zawalski16954532012-03-20 15:31:36 -0400286 target_path = os.path.join(static_dir, target)
287 if not os.path.isdir(target_path):
288 raise DevServerUtilError('Cannot find path %s' % target_path)
Frank Farzan37761d12011-12-01 14:29:08 -0800289
Scott Zawalski16954532012-03-20 15:31:36 -0400290 builds = [distutils.version.LooseVersion(build) for build in
291 os.listdir(target_path)]
Frank Farzan37761d12011-12-01 14:29:08 -0800292
Scott Zawalski16954532012-03-20 15:31:36 -0400293 if milestone and builds:
294 # Check if milestone Rxx is in the string representation of the build.
295 builds = filter(lambda x: milestone.upper() in str(x), builds)
Frank Farzan37761d12011-12-01 14:29:08 -0800296
Scott Zawalski16954532012-03-20 15:31:36 -0400297 if not builds:
298 raise DevServerUtilError('Could not determine build for %s' % target)
Frank Farzan37761d12011-12-01 14:29:08 -0800299
Scott Zawalski16954532012-03-20 15:31:36 -0400300 return str(max(builds))
Frank Farzan37761d12011-12-01 14:29:08 -0800301
302
303def CloneBuild(static_dir, board, build, tag, force=False):
304 """Clone an official build into the developer sandbox.
305
306 Developer sandbox directory must already exist.
307
308 Args:
309 static_dir: Directory where builds are served from.
310 board: Fully qualified board name; e.g. x86-generic-release.
311 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
312 tag: Unique resource/task identifier. Use '/' for nested tags.
313 force: Force re-creation of build_dir even if it already exists.
314
315 Returns:
316 The path to the new build.
317 """
318 # Create the developer build directory.
319 dev_static_dir = os.path.join(static_dir, DEV_BUILD_PREFIX)
320 dev_build_dir = os.path.join(dev_static_dir, tag)
321 official_build_dir = os.path.join(static_dir, board, build)
322 cherrypy.log('Cloning %s -> %s' % (official_build_dir, dev_build_dir),
323 'DEVSERVER_UTIL')
324 dev_build_exists = False
325 try:
326 AcquireLock(dev_static_dir, tag)
327 except DevServerUtilError:
328 dev_build_exists = True
329 if force:
330 dev_build_exists = False
331 ReleaseLock(dev_static_dir, tag)
332 AcquireLock(dev_static_dir, tag)
333
334 # Make a copy of the official build, only take necessary files.
335 if not dev_build_exists:
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700336 copy_list = [downloadable_artifact.TEST_IMAGE,
337 downloadable_artifact.ROOT_UPDATE,
338 downloadable_artifact.STATEFUL_UPDATE]
Frank Farzan37761d12011-12-01 14:29:08 -0800339 for f in copy_list:
340 shutil.copy(os.path.join(official_build_dir, f), dev_build_dir)
341
342 return dev_build_dir
343
Scott Zawalski84a39c92012-01-13 15:12:42 -0500344
345def GetControlFile(static_dir, build, control_path):
Frank Farzan37761d12011-12-01 14:29:08 -0800346 """Attempts to pull the requested control file from the Dev Server.
347
348 Args:
349 static_dir: Directory where builds are served from.
Frank Farzan37761d12011-12-01 14:29:08 -0800350 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
351 control_path: Path to control file on Dev Server relative to Autotest root.
352
353 Raises:
354 DevServerUtilError: If lock can't be acquired.
355
356 Returns:
357 Content of the requested control file.
358 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500359 # Be forgiving if the user passes in the control_path with a leading /
360 control_path = control_path.lstrip('/')
Scott Zawalski84a39c92012-01-13 15:12:42 -0500361 control_path = os.path.join(static_dir, build, 'autotest',
Scott Zawalski4647ce62012-01-03 17:17:28 -0500362 control_path)
Frank Farzan37761d12011-12-01 14:29:08 -0800363 if not SafeSandboxAccess(static_dir, control_path):
364 raise DevServerUtilError('Invaid control file "%s".' % control_path)
365
Scott Zawalski84a39c92012-01-13 15:12:42 -0500366 if not os.path.exists(control_path):
367 # TODO(scottz): Come up with some sort of error mechanism.
368 # crosbug.com/25040
369 return 'Unknown control path %s' % control_path
370
Frank Farzan37761d12011-12-01 14:29:08 -0800371 with open(control_path, 'r') as control_file:
372 return control_file.read()
373
374
Scott Zawalski84a39c92012-01-13 15:12:42 -0500375def GetControlFileList(static_dir, build):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500376 """List all control|control. files in the specified board/build path.
377
378 Args:
379 static_dir: Directory where builds are served from.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500380 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
381
382 Raises:
383 DevServerUtilError: If path is outside of sandbox.
384
385 Returns:
386 String of each file separated by a newline.
387 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500388 autotest_dir = os.path.join(static_dir, build, 'autotest/')
Scott Zawalski4647ce62012-01-03 17:17:28 -0500389 if not SafeSandboxAccess(static_dir, autotest_dir):
390 raise DevServerUtilError('Autotest dir not in sandbox "%s".' % autotest_dir)
391
392 control_files = set()
Scott Zawalski84a39c92012-01-13 15:12:42 -0500393 if not os.path.exists(autotest_dir):
394 # TODO(scottz): Come up with some sort of error mechanism.
395 # crosbug.com/25040
396 return 'Unknown build path %s' % autotest_dir
397
Scott Zawalski4647ce62012-01-03 17:17:28 -0500398 for entry in os.walk(autotest_dir):
399 dir_path, _, files = entry
400 for file_entry in files:
401 if file_entry.startswith('control.') or file_entry == 'control':
402 control_files.add(os.path.join(dir_path,
Chris Sosaea148d92012-03-06 16:22:04 -0800403 file_entry).replace(autotest_dir, ''))
Scott Zawalski4647ce62012-01-03 17:17:28 -0500404
405 return '\n'.join(control_files)
406
407
Frank Farzan37761d12011-12-01 14:29:08 -0800408def ListAutoupdateTargets(static_dir, board, build):
409 """Returns a list of autoupdate test targets for the given board, build.
410
411 Args:
412 static_dir: Directory where builds are served from.
413 board: Fully qualified board name; e.g. x86-generic-release.
414 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
415
416 Returns:
417 List of autoupdate test targets; e.g. ['0.14.747.0-r2bf8859c-b2927_nton']
418 """
419 return os.listdir(os.path.join(static_dir, board, build, AU_BASE))