blob: 7cfd024613afc745c1aa93e8ae1fc438e4ea3bca [file] [log] [blame]
Frank Farzan37761d12011-12-01 14:29:08 -08001# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
2# 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
11import shutil
Chris Sosaea148d92012-03-06 16:22:04 -080012import subprocess
Frank Farzan37761d12011-12-01 14:29:08 -080013
14
Chris Sosaea148d92012-03-06 16:22:04 -080015GSUTIL_ATTEMPTS = 5
Frank Farzan37761d12011-12-01 14:29:08 -080016AU_BASE = 'au'
17NTON_DIR_SUFFIX = '_nton'
18MTON_DIR_SUFFIX = '_mton'
19ROOT_UPDATE = 'update.gz'
20STATEFUL_UPDATE = 'stateful.tgz'
21TEST_IMAGE = 'chromiumos_test_image.bin'
22AUTOTEST_PACKAGE = 'autotest.tar.bz2'
23DEV_BUILD_PREFIX = 'dev'
24
25
26class DevServerUtilError(Exception):
27 """Exception classes used by this module."""
28 pass
29
30
31def ParsePayloadList(payload_list):
32 """Parse and return the full/delta payload URLs.
33
34 Args:
35 payload_list: A list of Google Storage URLs.
36
37 Returns:
38 Tuple of 3 payloads URLs: (full, nton, mton).
39
40 Raises:
41 DevServerUtilError: If payloads missing or invalid.
42 """
43 full_payload_url = None
44 mton_payload_url = None
45 nton_payload_url = None
46 for payload in payload_list:
47 if '_full_' in payload:
48 full_payload_url = payload
49 elif '_delta_' in payload:
50 # e.g. chromeos_{from_version}_{to_version}_x86-generic_delta_dev.bin
51 from_version, to_version = payload.rsplit('/', 1)[1].split('_')[1:3]
52 if from_version == to_version:
53 nton_payload_url = payload
54 else:
55 mton_payload_url = payload
56
57 if not full_payload_url or not nton_payload_url or not mton_payload_url:
58 raise DevServerUtilError(
59 'Payloads are missing or have unexpected name formats.', payload_list)
60
61 return full_payload_url, nton_payload_url, mton_payload_url
62
63
64def DownloadBuildFromGS(staging_dir, archive_url, build):
65 """Downloads the specified build from Google Storage into a temp directory.
66
67 The archive is expected to contain stateful.tgz, autotest.tar.bz2, and three
68 payloads: full, N-1->N, and N->N. gsutil is used to download the file.
69 gsutil must be in the path and should have required credentials.
70
71 Args:
72 staging_dir: Temp directory containing payloads and autotest packages.
73 archive_url: Google Storage path to the build directory.
Frank Farzanbcb571e2012-01-03 11:48:17 -080074 e.g. gs://chromeos-image-archive/x86-generic/R17-1208.0.0-a1-b338.
Frank Farzan37761d12011-12-01 14:29:08 -080075 build: Full build string to look for; e.g. R17-1208.0.0-a1-b338.
76
77 Raises:
78 DevServerUtilError: If any steps in the process fail to complete.
79 """
Chris Sosaea148d92012-03-06 16:22:04 -080080 def GSUtilRun(cmd, err_msg):
81 """Runs a GSUTIL command up to GSUTIL_ATTEMPTS number of times.
82
83 Raises:
84 subprocess.CalledProcessError if all attempt to run gsutil cmd fails.
85 """
86 proc = None
87 for _attempt in range(GSUTIL_ATTEMPTS):
88 proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
89 stdout, _stderr = proc.communicate()
90 if proc.returncode == 0:
91 return stdout
92 else:
93 raise DevServerUtilError('%s GSUTIL cmd %s failed with return code %d' % (
94 err_msg, cmd, proc.returncode))
95
Frank Farzan37761d12011-12-01 14:29:08 -080096 # Get a list of payloads from Google Storage.
97 cmd = 'gsutil ls %s/*.bin' % archive_url
98 msg = 'Failed to get a list of payloads.'
Chris Sosaea148d92012-03-06 16:22:04 -080099 stdout = GSUtilRun(cmd, msg)
Brian Harringa99a3f62012-02-24 10:38:22 -0800100
101 payload_list = stdout.splitlines()
Frank Farzan37761d12011-12-01 14:29:08 -0800102 full_payload_url, nton_payload_url, mton_payload_url = (
103 ParsePayloadList(payload_list))
104
105 # Create temp directories for payloads.
106 nton_payload_dir = os.path.join(staging_dir, AU_BASE, build + NTON_DIR_SUFFIX)
107 os.makedirs(nton_payload_dir)
108 mton_payload_dir = os.path.join(staging_dir, AU_BASE, build + MTON_DIR_SUFFIX)
109 os.mkdir(mton_payload_dir)
110
111 # Download build components into respective directories.
112 src = [full_payload_url,
113 nton_payload_url,
114 mton_payload_url,
115 archive_url + '/' + STATEFUL_UPDATE,
116 archive_url + '/' + AUTOTEST_PACKAGE]
117 dst = [os.path.join(staging_dir, ROOT_UPDATE),
118 os.path.join(nton_payload_dir, ROOT_UPDATE),
119 os.path.join(mton_payload_dir, ROOT_UPDATE),
120 staging_dir,
121 staging_dir]
122 for src, dest in zip(src, dst):
123 cmd = 'gsutil cp %s %s' % (src, dest)
124 msg = 'Failed to download "%s".' % src
Chris Sosaea148d92012-03-06 16:22:04 -0800125 GSUtilRun(cmd, msg)
Frank Farzan37761d12011-12-01 14:29:08 -0800126
127
128def InstallBuild(staging_dir, build_dir):
129 """Installs various build components from staging directory.
130
131 Specifically, the following components are installed:
132 - update.gz
133 - stateful.tgz
134 - chromiumos_test_image.bin
135 - The entire contents of the au directory. Symlinks are generated for each
136 au payload as well.
137 - Contents of autotest-pkgs directory.
138 - Control files from autotest/server/{tests, site_tests}
139
140 Args:
141 staging_dir: Temp directory containing payloads and autotest packages.
142 build_dir: Directory to install build components into.
143 """
144 install_list = [ROOT_UPDATE, STATEFUL_UPDATE]
145
146 # Create blank chromiumos_test_image.bin. Otherwise the Dev Server will
147 # try to rebuild it unnecessarily.
148 test_image = os.path.join(build_dir, TEST_IMAGE)
149 open(test_image, 'a').close()
150
151 # Install AU payloads.
152 au_path = os.path.join(staging_dir, AU_BASE)
153 install_list.append(AU_BASE)
154 # For each AU payload, setup symlinks to the main payloads.
155 cwd = os.getcwd()
156 for au in os.listdir(au_path):
157 os.chdir(os.path.join(au_path, au))
158 os.symlink(os.path.join(os.pardir, os.pardir, TEST_IMAGE), TEST_IMAGE)
159 os.symlink(os.path.join(os.pardir, os.pardir, STATEFUL_UPDATE),
160 STATEFUL_UPDATE)
161 os.chdir(cwd)
162
163 for component in install_list:
164 shutil.move(os.path.join(staging_dir, component), build_dir)
165
Scott Zawalski4647ce62012-01-03 17:17:28 -0500166 shutil.move(os.path.join(staging_dir, 'autotest'),
Frank Farzan37761d12011-12-01 14:29:08 -0800167 os.path.join(build_dir, 'autotest'))
168
Frank Farzan37761d12011-12-01 14:29:08 -0800169
170def PrepareAutotestPkgs(staging_dir):
171 """Create autotest client packages inside staging_dir.
172
173 Args:
174 staging_dir: Temp directory containing payloads and autotest packages.
175
176 Raises:
177 DevServerUtilError: If any steps in the process fail to complete.
178 """
179 cmd = ('tar xf %s --use-compress-prog=pbzip2 --directory=%s' %
180 (os.path.join(staging_dir, AUTOTEST_PACKAGE), staging_dir))
181 msg = 'Failed to extract autotest.tar.bz2 ! Is pbzip2 installed?'
182 try:
Brian Harringa99a3f62012-02-24 10:38:22 -0800183 subprocess.check_call(cmd, shell=True)
184 except subprocess.CalledProcessError, e:
185 raise DevServerUtilError('%s %s' % (msg, e))
Frank Farzan37761d12011-12-01 14:29:08 -0800186
Scott Zawalski4647ce62012-01-03 17:17:28 -0500187 # Use the root of Autotest
188 autotest_pkgs_dir = os.path.join(staging_dir, 'autotest', 'packages')
189 if not os.path.exists(autotest_pkgs_dir):
190 os.makedirs(autotest_pkgs_dir)
Chris Sosae1525992012-03-12 16:55:56 -0700191
192 if not os.path.exists(os.path.join(autotest_pkgs_dir, 'packages.checksum')):
193 cmd_list = ['autotest/utils/packager.py',
194 'upload', '--repository', autotest_pkgs_dir, '--all']
195 msg = 'Failed to create autotest packages!'
196 try:
197 subprocess.check_call(' '.join(cmd_list), cwd=staging_dir, shell=True)
198 except subprocess.CalledProcessError, e:
199 raise DevServerUtilError('%s %s' % (msg, e))
200 else:
201 cherrypy.log('Using pre-generated packages from autotest', 'DEVSERVER_UTIL')
Frank Farzan37761d12011-12-01 14:29:08 -0800202
203
204def SafeSandboxAccess(static_dir, path):
205 """Verify that the path is in static_dir.
206
207 Args:
208 static_dir: Directory where builds are served from.
209 path: Path to verify.
210
211 Returns:
212 True if path is in static_dir, False otherwise
213 """
214 static_dir = os.path.realpath(static_dir)
215 path = os.path.realpath(path)
216 return (path.startswith(static_dir) and path != static_dir)
217
218
219def AcquireLock(static_dir, tag):
220 """Acquires a lock for a given tag.
221
222 Creates a directory for the specified tag, telling other
223 components the resource/task represented by the tag is unavailable.
224
225 Args:
226 static_dir: Directory where builds are served from.
227 tag: Unique resource/task identifier. Use '/' for nested tags.
228
229 Returns:
230 Path to the created directory or None if creation failed.
231
232 Raises:
233 DevServerUtilError: If lock can't be acquired.
234 """
235 build_dir = os.path.join(static_dir, tag)
236 if not SafeSandboxAccess(static_dir, build_dir):
237 raise DevServerUtilError('Invaid tag "%s".' % tag)
238
239 try:
240 os.makedirs(build_dir)
241 except OSError, e:
242 if e.errno == errno.EEXIST:
243 raise DevServerUtilError(str(e))
244 else:
245 raise
246
247 return build_dir
248
249
250def ReleaseLock(static_dir, tag):
251 """Releases the lock for a given tag. Removes lock directory content.
252
253 Args:
254 static_dir: Directory where builds are served from.
255 tag: Unique resource/task identifier. Use '/' for nested tags.
256
257 Raises:
258 DevServerUtilError: If lock can't be released.
259 """
260 build_dir = os.path.join(static_dir, tag)
261 if not SafeSandboxAccess(static_dir, build_dir):
262 raise DevServerUtilError('Invaid tag "%s".' % tag)
263
264 shutil.rmtree(build_dir)
265
266
267def FindMatchingBoards(static_dir, board):
268 """Returns a list of boards given a partial board name.
269
270 Args:
271 static_dir: Directory where builds are served from.
272 board: Partial board name for this build; e.g. x86-generic.
273
274 Returns:
275 Returns a list of boards given a partial board.
276 """
277 return [brd for brd in os.listdir(static_dir) if board in brd]
278
279
280def FindMatchingBuilds(static_dir, board, build):
281 """Returns a list of matching builds given a board and partial build.
282
283 Args:
284 static_dir: Directory where builds are served from.
285 board: Partial board name for this build; e.g. x86-generic-release.
286 build: Partial build string to look for; e.g. R17-1234.
287
288 Returns:
289 Returns a list of (board, build) tuples given a partial board and build.
290 """
291 matches = []
292 for brd in FindMatchingBoards(static_dir, board):
293 a = [(brd, bld) for bld in
294 os.listdir(os.path.join(static_dir, brd)) if build in bld]
295 matches.extend(a)
296 return matches
297
298
Scott Zawalski16954532012-03-20 15:31:36 -0400299def GetLatestBuildVersion(static_dir, target, milestone=None):
Frank Farzan37761d12011-12-01 14:29:08 -0800300 """Retrieves the latest build version for a given board.
301
302 Args:
303 static_dir: Directory where builds are served from.
Scott Zawalski16954532012-03-20 15:31:36 -0400304 target: The build target, typically a combination of the board and the
305 type of build e.g. x86-mario-release.
306 milestone: For latest build set to None, for builds only in a specific
307 milestone set to a str of format Rxx (e.g. R16). Default: None.
Frank Farzan37761d12011-12-01 14:29:08 -0800308
309 Returns:
Scott Zawalski16954532012-03-20 15:31:36 -0400310 If latest found, a full build string is returned e.g. R17-1234.0.0-a1-b983.
311 If no latest is found for some reason or another a '' string is returned.
Frank Farzan37761d12011-12-01 14:29:08 -0800312
313 Raises:
Scott Zawalski16954532012-03-20 15:31:36 -0400314 DevServerUtilError: If for some reason the latest build cannot be
315 deteremined, this could be due to the dir not existing or no builds
316 being present after filtering on milestone.
Frank Farzan37761d12011-12-01 14:29:08 -0800317 """
Scott Zawalski16954532012-03-20 15:31:36 -0400318 target_path = os.path.join(static_dir, target)
319 if not os.path.isdir(target_path):
320 raise DevServerUtilError('Cannot find path %s' % target_path)
Frank Farzan37761d12011-12-01 14:29:08 -0800321
Scott Zawalski16954532012-03-20 15:31:36 -0400322 builds = [distutils.version.LooseVersion(build) for build in
323 os.listdir(target_path)]
Frank Farzan37761d12011-12-01 14:29:08 -0800324
Scott Zawalski16954532012-03-20 15:31:36 -0400325 if milestone and builds:
326 # Check if milestone Rxx is in the string representation of the build.
327 builds = filter(lambda x: milestone.upper() in str(x), builds)
Frank Farzan37761d12011-12-01 14:29:08 -0800328
Scott Zawalski16954532012-03-20 15:31:36 -0400329 if not builds:
330 raise DevServerUtilError('Could not determine build for %s' % target)
Frank Farzan37761d12011-12-01 14:29:08 -0800331
Scott Zawalski16954532012-03-20 15:31:36 -0400332 return str(max(builds))
Frank Farzan37761d12011-12-01 14:29:08 -0800333
334
335def CloneBuild(static_dir, board, build, tag, force=False):
336 """Clone an official build into the developer sandbox.
337
338 Developer sandbox directory must already exist.
339
340 Args:
341 static_dir: Directory where builds are served from.
342 board: Fully qualified board name; e.g. x86-generic-release.
343 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
344 tag: Unique resource/task identifier. Use '/' for nested tags.
345 force: Force re-creation of build_dir even if it already exists.
346
347 Returns:
348 The path to the new build.
349 """
350 # Create the developer build directory.
351 dev_static_dir = os.path.join(static_dir, DEV_BUILD_PREFIX)
352 dev_build_dir = os.path.join(dev_static_dir, tag)
353 official_build_dir = os.path.join(static_dir, board, build)
354 cherrypy.log('Cloning %s -> %s' % (official_build_dir, dev_build_dir),
355 'DEVSERVER_UTIL')
356 dev_build_exists = False
357 try:
358 AcquireLock(dev_static_dir, tag)
359 except DevServerUtilError:
360 dev_build_exists = True
361 if force:
362 dev_build_exists = False
363 ReleaseLock(dev_static_dir, tag)
364 AcquireLock(dev_static_dir, tag)
365
366 # Make a copy of the official build, only take necessary files.
367 if not dev_build_exists:
368 copy_list = [TEST_IMAGE, ROOT_UPDATE, STATEFUL_UPDATE]
369 for f in copy_list:
370 shutil.copy(os.path.join(official_build_dir, f), dev_build_dir)
371
372 return dev_build_dir
373
Scott Zawalski84a39c92012-01-13 15:12:42 -0500374
375def GetControlFile(static_dir, build, control_path):
Frank Farzan37761d12011-12-01 14:29:08 -0800376 """Attempts to pull the requested control file from the Dev Server.
377
378 Args:
379 static_dir: Directory where builds are served from.
Frank Farzan37761d12011-12-01 14:29:08 -0800380 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
381 control_path: Path to control file on Dev Server relative to Autotest root.
382
383 Raises:
384 DevServerUtilError: If lock can't be acquired.
385
386 Returns:
387 Content of the requested control file.
388 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500389 # Be forgiving if the user passes in the control_path with a leading /
390 control_path = control_path.lstrip('/')
Scott Zawalski84a39c92012-01-13 15:12:42 -0500391 control_path = os.path.join(static_dir, build, 'autotest',
Scott Zawalski4647ce62012-01-03 17:17:28 -0500392 control_path)
Frank Farzan37761d12011-12-01 14:29:08 -0800393 if not SafeSandboxAccess(static_dir, control_path):
394 raise DevServerUtilError('Invaid control file "%s".' % control_path)
395
Scott Zawalski84a39c92012-01-13 15:12:42 -0500396 if not os.path.exists(control_path):
397 # TODO(scottz): Come up with some sort of error mechanism.
398 # crosbug.com/25040
399 return 'Unknown control path %s' % control_path
400
Frank Farzan37761d12011-12-01 14:29:08 -0800401 with open(control_path, 'r') as control_file:
402 return control_file.read()
403
404
Scott Zawalski84a39c92012-01-13 15:12:42 -0500405def GetControlFileList(static_dir, build):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500406 """List all control|control. files in the specified board/build path.
407
408 Args:
409 static_dir: Directory where builds are served from.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500410 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
411
412 Raises:
413 DevServerUtilError: If path is outside of sandbox.
414
415 Returns:
416 String of each file separated by a newline.
417 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500418 autotest_dir = os.path.join(static_dir, build, 'autotest/')
Scott Zawalski4647ce62012-01-03 17:17:28 -0500419 if not SafeSandboxAccess(static_dir, autotest_dir):
420 raise DevServerUtilError('Autotest dir not in sandbox "%s".' % autotest_dir)
421
422 control_files = set()
Scott Zawalski84a39c92012-01-13 15:12:42 -0500423 if not os.path.exists(autotest_dir):
424 # TODO(scottz): Come up with some sort of error mechanism.
425 # crosbug.com/25040
426 return 'Unknown build path %s' % autotest_dir
427
Scott Zawalski4647ce62012-01-03 17:17:28 -0500428 for entry in os.walk(autotest_dir):
429 dir_path, _, files = entry
430 for file_entry in files:
431 if file_entry.startswith('control.') or file_entry == 'control':
432 control_files.add(os.path.join(dir_path,
Chris Sosaea148d92012-03-06 16:22:04 -0800433 file_entry).replace(autotest_dir, ''))
Scott Zawalski4647ce62012-01-03 17:17:28 -0500434
435 return '\n'.join(control_files)
436
437
Frank Farzan37761d12011-12-01 14:29:08 -0800438def ListAutoupdateTargets(static_dir, board, build):
439 """Returns a list of autoupdate test targets for the given board, build.
440
441 Args:
442 static_dir: Directory where builds are served from.
443 board: Fully qualified board name; e.g. x86-generic-release.
444 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
445
446 Returns:
447 List of autoupdate test targets; e.g. ['0.14.747.0-r2bf8859c-b2927_nton']
448 """
449 return os.listdir(os.path.join(static_dir, board, build, AU_BASE))