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