blob: 3a48d53488ff35aeae729791e35b3d5e22047e85 [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
12import sys
13
14import constants
Frank Farzan37761d12011-12-01 14:29:08 -080015
16
17AU_BASE = 'au'
18NTON_DIR_SUFFIX = '_nton'
19MTON_DIR_SUFFIX = '_mton'
20ROOT_UPDATE = 'update.gz'
21STATEFUL_UPDATE = 'stateful.tgz'
22TEST_IMAGE = 'chromiumos_test_image.bin'
23AUTOTEST_PACKAGE = 'autotest.tar.bz2'
24DEV_BUILD_PREFIX = 'dev'
25
26
27class DevServerUtilError(Exception):
28 """Exception classes used by this module."""
29 pass
30
31
32def ParsePayloadList(payload_list):
33 """Parse and return the full/delta payload URLs.
34
35 Args:
36 payload_list: A list of Google Storage URLs.
37
38 Returns:
39 Tuple of 3 payloads URLs: (full, nton, mton).
40
41 Raises:
42 DevServerUtilError: If payloads missing or invalid.
43 """
44 full_payload_url = None
45 mton_payload_url = None
46 nton_payload_url = None
47 for payload in payload_list:
48 if '_full_' in payload:
49 full_payload_url = payload
50 elif '_delta_' in payload:
51 # e.g. chromeos_{from_version}_{to_version}_x86-generic_delta_dev.bin
52 from_version, to_version = payload.rsplit('/', 1)[1].split('_')[1:3]
53 if from_version == to_version:
54 nton_payload_url = payload
55 else:
56 mton_payload_url = payload
57
58 if not full_payload_url or not nton_payload_url or not mton_payload_url:
59 raise DevServerUtilError(
60 'Payloads are missing or have unexpected name formats.', payload_list)
61
62 return full_payload_url, nton_payload_url, mton_payload_url
63
64
65def DownloadBuildFromGS(staging_dir, archive_url, build):
66 """Downloads the specified build from Google Storage into a temp directory.
67
68 The archive is expected to contain stateful.tgz, autotest.tar.bz2, and three
69 payloads: full, N-1->N, and N->N. gsutil is used to download the file.
70 gsutil must be in the path and should have required credentials.
71
72 Args:
73 staging_dir: Temp directory containing payloads and autotest packages.
74 archive_url: Google Storage path to the build directory.
Frank Farzanbcb571e2012-01-03 11:48:17 -080075 e.g. gs://chromeos-image-archive/x86-generic/R17-1208.0.0-a1-b338.
Frank Farzan37761d12011-12-01 14:29:08 -080076 build: Full build string to look for; e.g. R17-1208.0.0-a1-b338.
77
78 Raises:
79 DevServerUtilError: If any steps in the process fail to complete.
80 """
Frank Farzan37761d12011-12-01 14:29:08 -080081 # Get a list of payloads from Google Storage.
82 cmd = 'gsutil ls %s/*.bin' % archive_url
83 msg = 'Failed to get a list of payloads.'
84 try:
Brian Harringa99a3f62012-02-24 10:38:22 -080085 proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
86 stdout, stderr = proc.communicate()
87 if proc.returncode != 0:
88 raise DevServerUtilError('%s nonzero exit code: %i'
89 % (msg, proc.returncode))
90 except subprocess.CalledProcessError, e:
91 raise DevServerUtilError('%s %s' % (msg, e))
92
93 payload_list = stdout.splitlines()
Frank Farzan37761d12011-12-01 14:29:08 -080094 full_payload_url, nton_payload_url, mton_payload_url = (
95 ParsePayloadList(payload_list))
96
97 # Create temp directories for payloads.
98 nton_payload_dir = os.path.join(staging_dir, AU_BASE, build + NTON_DIR_SUFFIX)
99 os.makedirs(nton_payload_dir)
100 mton_payload_dir = os.path.join(staging_dir, AU_BASE, build + MTON_DIR_SUFFIX)
101 os.mkdir(mton_payload_dir)
102
103 # Download build components into respective directories.
104 src = [full_payload_url,
105 nton_payload_url,
106 mton_payload_url,
107 archive_url + '/' + STATEFUL_UPDATE,
108 archive_url + '/' + AUTOTEST_PACKAGE]
109 dst = [os.path.join(staging_dir, ROOT_UPDATE),
110 os.path.join(nton_payload_dir, ROOT_UPDATE),
111 os.path.join(mton_payload_dir, ROOT_UPDATE),
112 staging_dir,
113 staging_dir]
114 for src, dest in zip(src, dst):
115 cmd = 'gsutil cp %s %s' % (src, dest)
116 msg = 'Failed to download "%s".' % src
117 try:
Brian Harringa99a3f62012-02-24 10:38:22 -0800118 subprocess.check_call(cmd, shell=True)
119 except subprocess.CalledProcessError, e:
120 raise DevServerUtilError('%s %s' % (msg, e))
Frank Farzan37761d12011-12-01 14:29:08 -0800121
122
123def InstallBuild(staging_dir, build_dir):
124 """Installs various build components from staging directory.
125
126 Specifically, the following components are installed:
127 - update.gz
128 - stateful.tgz
129 - chromiumos_test_image.bin
130 - The entire contents of the au directory. Symlinks are generated for each
131 au payload as well.
132 - Contents of autotest-pkgs directory.
133 - Control files from autotest/server/{tests, site_tests}
134
135 Args:
136 staging_dir: Temp directory containing payloads and autotest packages.
137 build_dir: Directory to install build components into.
138 """
139 install_list = [ROOT_UPDATE, STATEFUL_UPDATE]
140
141 # Create blank chromiumos_test_image.bin. Otherwise the Dev Server will
142 # try to rebuild it unnecessarily.
143 test_image = os.path.join(build_dir, TEST_IMAGE)
144 open(test_image, 'a').close()
145
146 # Install AU payloads.
147 au_path = os.path.join(staging_dir, AU_BASE)
148 install_list.append(AU_BASE)
149 # For each AU payload, setup symlinks to the main payloads.
150 cwd = os.getcwd()
151 for au in os.listdir(au_path):
152 os.chdir(os.path.join(au_path, au))
153 os.symlink(os.path.join(os.pardir, os.pardir, TEST_IMAGE), TEST_IMAGE)
154 os.symlink(os.path.join(os.pardir, os.pardir, STATEFUL_UPDATE),
155 STATEFUL_UPDATE)
156 os.chdir(cwd)
157
158 for component in install_list:
159 shutil.move(os.path.join(staging_dir, component), build_dir)
160
Scott Zawalski4647ce62012-01-03 17:17:28 -0500161 shutil.move(os.path.join(staging_dir, 'autotest'),
Frank Farzan37761d12011-12-01 14:29:08 -0800162 os.path.join(build_dir, 'autotest'))
163
Frank Farzan37761d12011-12-01 14:29:08 -0800164
165def PrepareAutotestPkgs(staging_dir):
166 """Create autotest client packages inside staging_dir.
167
168 Args:
169 staging_dir: Temp directory containing payloads and autotest packages.
170
171 Raises:
172 DevServerUtilError: If any steps in the process fail to complete.
173 """
174 cmd = ('tar xf %s --use-compress-prog=pbzip2 --directory=%s' %
175 (os.path.join(staging_dir, AUTOTEST_PACKAGE), staging_dir))
176 msg = 'Failed to extract autotest.tar.bz2 ! Is pbzip2 installed?'
177 try:
Brian Harringa99a3f62012-02-24 10:38:22 -0800178 subprocess.check_call(cmd, shell=True)
179 except subprocess.CalledProcessError, e:
180 raise DevServerUtilError('%s %s' % (msg, e))
Frank Farzan37761d12011-12-01 14:29:08 -0800181
Scott Zawalski4647ce62012-01-03 17:17:28 -0500182 # Use the root of Autotest
183 autotest_pkgs_dir = os.path.join(staging_dir, 'autotest', 'packages')
184 if not os.path.exists(autotest_pkgs_dir):
185 os.makedirs(autotest_pkgs_dir)
Frank Farzan37761d12011-12-01 14:29:08 -0800186 cmd_list = ['autotest/utils/packager.py',
Scott Zawalski4647ce62012-01-03 17:17:28 -0500187 'upload', '--repository', autotest_pkgs_dir, '--all']
Frank Farzan37761d12011-12-01 14:29:08 -0800188 msg = 'Failed to create autotest packages!'
189 try:
Brian Harringa99a3f62012-02-24 10:38:22 -0800190 subprocess.check_call(' '.join(cmd_list), cwd=staging_dir, shell=True)
191 except subprocess.CalledProcessError, e:
192 raise DevServerUtilError('%s %s' % (msg, e))
Frank Farzan37761d12011-12-01 14:29:08 -0800193
194
195def SafeSandboxAccess(static_dir, path):
196 """Verify that the path is in static_dir.
197
198 Args:
199 static_dir: Directory where builds are served from.
200 path: Path to verify.
201
202 Returns:
203 True if path is in static_dir, False otherwise
204 """
205 static_dir = os.path.realpath(static_dir)
206 path = os.path.realpath(path)
207 return (path.startswith(static_dir) and path != static_dir)
208
209
210def AcquireLock(static_dir, tag):
211 """Acquires a lock for a given tag.
212
213 Creates a directory for the specified tag, telling other
214 components the resource/task represented by the tag is unavailable.
215
216 Args:
217 static_dir: Directory where builds are served from.
218 tag: Unique resource/task identifier. Use '/' for nested tags.
219
220 Returns:
221 Path to the created directory or None if creation failed.
222
223 Raises:
224 DevServerUtilError: If lock can't be acquired.
225 """
226 build_dir = os.path.join(static_dir, tag)
227 if not SafeSandboxAccess(static_dir, build_dir):
228 raise DevServerUtilError('Invaid tag "%s".' % tag)
229
230 try:
231 os.makedirs(build_dir)
232 except OSError, e:
233 if e.errno == errno.EEXIST:
234 raise DevServerUtilError(str(e))
235 else:
236 raise
237
238 return build_dir
239
240
241def ReleaseLock(static_dir, tag):
242 """Releases the lock for a given tag. Removes lock directory content.
243
244 Args:
245 static_dir: Directory where builds are served from.
246 tag: Unique resource/task identifier. Use '/' for nested tags.
247
248 Raises:
249 DevServerUtilError: If lock can't be released.
250 """
251 build_dir = os.path.join(static_dir, tag)
252 if not SafeSandboxAccess(static_dir, build_dir):
253 raise DevServerUtilError('Invaid tag "%s".' % tag)
254
255 shutil.rmtree(build_dir)
256
257
258def FindMatchingBoards(static_dir, board):
259 """Returns a list of boards given a partial board name.
260
261 Args:
262 static_dir: Directory where builds are served from.
263 board: Partial board name for this build; e.g. x86-generic.
264
265 Returns:
266 Returns a list of boards given a partial board.
267 """
268 return [brd for brd in os.listdir(static_dir) if board in brd]
269
270
271def FindMatchingBuilds(static_dir, board, build):
272 """Returns a list of matching builds given a board and partial build.
273
274 Args:
275 static_dir: Directory where builds are served from.
276 board: Partial board name for this build; e.g. x86-generic-release.
277 build: Partial build string to look for; e.g. R17-1234.
278
279 Returns:
280 Returns a list of (board, build) tuples given a partial board and build.
281 """
282 matches = []
283 for brd in FindMatchingBoards(static_dir, board):
284 a = [(brd, bld) for bld in
285 os.listdir(os.path.join(static_dir, brd)) if build in bld]
286 matches.extend(a)
287 return matches
288
289
290def GetLatestBuildVersion(static_dir, board):
291 """Retrieves the latest build version for a given board.
292
293 Args:
294 static_dir: Directory where builds are served from.
295 board: Board name for this build; e.g. x86-generic-release.
296
297 Returns:
298 Full build string; e.g. R17-1234.0.0-a1-b983.
299 """
300 builds = [distutils.version.LooseVersion(build) for build in
301 os.listdir(os.path.join(static_dir, board))]
302 return str(max(builds))
303
304
305def FindBuild(static_dir, board, build):
306 """Given partial build and board ids, figure out the appropriate build.
307
308 Args:
309 static_dir: Directory where builds are served from.
310 board: Partial board name for this build; e.g. x86-generic.
311 build: Partial build string to look for; e.g. R17-1234 or "latest" to
312 return the latest build for for most newest board.
313
314 Returns:
315 Tuple of (board, build):
316 board: Fully qualified board name; e.g. x86-generic-release
317 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983
318
319 Raises:
320 DevServerUtilError: If no boards, no builds, or too many builds
321 are matched.
322 """
323 if build.lower() == 'latest':
324 boards = FindMatchingBoards(static_dir, board)
325 if not boards:
326 raise DevServerUtilError(
327 'No boards matching %s could be found on the Dev Server.' % board)
328
329 if len(boards) > 1:
330 raise DevServerUtilError(
331 'The given board name is ambiguous. Disambiguate by using one of'
332 ' these instead: %s' % ', '.join(boards))
333
334 build = GetLatestBuildVersion(static_dir, board)
335 else:
336 builds = FindMatchingBuilds(static_dir, board, build)
337 if not builds:
338 raise DevServerUtilError(
339 'No builds matching %s could be found for board %s.' % (
340 build, board))
341
342 if len(builds) > 1:
343 raise DevServerUtilError(
344 'The given build id is ambiguous. Disambiguate by using one of'
345 ' these instead: %s' % ', '.join([b[1] for b in builds]))
346
347 board, build = builds[0]
348
349 return board, build
350
351
352def CloneBuild(static_dir, board, build, tag, force=False):
353 """Clone an official build into the developer sandbox.
354
355 Developer sandbox directory must already exist.
356
357 Args:
358 static_dir: Directory where builds are served from.
359 board: Fully qualified board name; e.g. x86-generic-release.
360 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
361 tag: Unique resource/task identifier. Use '/' for nested tags.
362 force: Force re-creation of build_dir even if it already exists.
363
364 Returns:
365 The path to the new build.
366 """
367 # Create the developer build directory.
368 dev_static_dir = os.path.join(static_dir, DEV_BUILD_PREFIX)
369 dev_build_dir = os.path.join(dev_static_dir, tag)
370 official_build_dir = os.path.join(static_dir, board, build)
371 cherrypy.log('Cloning %s -> %s' % (official_build_dir, dev_build_dir),
372 'DEVSERVER_UTIL')
373 dev_build_exists = False
374 try:
375 AcquireLock(dev_static_dir, tag)
376 except DevServerUtilError:
377 dev_build_exists = True
378 if force:
379 dev_build_exists = False
380 ReleaseLock(dev_static_dir, tag)
381 AcquireLock(dev_static_dir, tag)
382
383 # Make a copy of the official build, only take necessary files.
384 if not dev_build_exists:
385 copy_list = [TEST_IMAGE, ROOT_UPDATE, STATEFUL_UPDATE]
386 for f in copy_list:
387 shutil.copy(os.path.join(official_build_dir, f), dev_build_dir)
388
389 return dev_build_dir
390
Scott Zawalski84a39c92012-01-13 15:12:42 -0500391
392def GetControlFile(static_dir, build, control_path):
Frank Farzan37761d12011-12-01 14:29:08 -0800393 """Attempts to pull the requested control file from the Dev Server.
394
395 Args:
396 static_dir: Directory where builds are served from.
Frank Farzan37761d12011-12-01 14:29:08 -0800397 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
398 control_path: Path to control file on Dev Server relative to Autotest root.
399
400 Raises:
401 DevServerUtilError: If lock can't be acquired.
402
403 Returns:
404 Content of the requested control file.
405 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500406 # Be forgiving if the user passes in the control_path with a leading /
407 control_path = control_path.lstrip('/')
Scott Zawalski84a39c92012-01-13 15:12:42 -0500408 control_path = os.path.join(static_dir, build, 'autotest',
Scott Zawalski4647ce62012-01-03 17:17:28 -0500409 control_path)
Frank Farzan37761d12011-12-01 14:29:08 -0800410 if not SafeSandboxAccess(static_dir, control_path):
411 raise DevServerUtilError('Invaid control file "%s".' % control_path)
412
Scott Zawalski84a39c92012-01-13 15:12:42 -0500413 if not os.path.exists(control_path):
414 # TODO(scottz): Come up with some sort of error mechanism.
415 # crosbug.com/25040
416 return 'Unknown control path %s' % control_path
417
Frank Farzan37761d12011-12-01 14:29:08 -0800418 with open(control_path, 'r') as control_file:
419 return control_file.read()
420
421
Scott Zawalski84a39c92012-01-13 15:12:42 -0500422def GetControlFileList(static_dir, build):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500423 """List all control|control. files in the specified board/build path.
424
425 Args:
426 static_dir: Directory where builds are served from.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500427 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
428
429 Raises:
430 DevServerUtilError: If path is outside of sandbox.
431
432 Returns:
433 String of each file separated by a newline.
434 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500435 autotest_dir = os.path.join(static_dir, build, 'autotest/')
Scott Zawalski4647ce62012-01-03 17:17:28 -0500436 if not SafeSandboxAccess(static_dir, autotest_dir):
437 raise DevServerUtilError('Autotest dir not in sandbox "%s".' % autotest_dir)
438
439 control_files = set()
Scott Zawalski84a39c92012-01-13 15:12:42 -0500440 if not os.path.exists(autotest_dir):
441 # TODO(scottz): Come up with some sort of error mechanism.
442 # crosbug.com/25040
443 return 'Unknown build path %s' % autotest_dir
444
Scott Zawalski4647ce62012-01-03 17:17:28 -0500445 for entry in os.walk(autotest_dir):
446 dir_path, _, files = entry
447 for file_entry in files:
448 if file_entry.startswith('control.') or file_entry == 'control':
449 control_files.add(os.path.join(dir_path,
Scott Zawalski1572d152012-01-16 14:36:02 -0500450 file_entry).replace(autotest_dir,''))
Scott Zawalski4647ce62012-01-03 17:17:28 -0500451
452 return '\n'.join(control_files)
453
454
Frank Farzan37761d12011-12-01 14:29:08 -0800455def ListAutoupdateTargets(static_dir, board, build):
456 """Returns a list of autoupdate test targets for the given board, build.
457
458 Args:
459 static_dir: Directory where builds are served from.
460 board: Fully qualified board name; e.g. x86-generic-release.
461 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
462
463 Returns:
464 List of autoupdate test targets; e.g. ['0.14.747.0-r2bf8859c-b2927_nton']
465 """
466 return os.listdir(os.path.join(static_dir, board, build, AU_BASE))