blob: a8cfb65cd29e2f1d15b3ad0843bfe808a32a2394 [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
15sys.path.append(constants.SOURCE_ROOT)
16from chromite.lib import cros_build_lib
17
18
19AU_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 """
Frank Farzan37761d12011-12-01 14:29:08 -080083 # Get a list of payloads from Google Storage.
84 cmd = 'gsutil ls %s/*.bin' % archive_url
85 msg = 'Failed to get a list of payloads.'
86 try:
87 result = cros_build_lib.RunCommand(cmd, shell=True, redirect_stdout=True,
88 error_message=msg)
89 except cros_build_lib.RunCommandError, e:
90 raise DevServerUtilError(str(e))
91 payload_list = result.output.splitlines()
92 full_payload_url, nton_payload_url, mton_payload_url = (
93 ParsePayloadList(payload_list))
94
95 # Create temp directories for payloads.
96 nton_payload_dir = os.path.join(staging_dir, AU_BASE, build + NTON_DIR_SUFFIX)
97 os.makedirs(nton_payload_dir)
98 mton_payload_dir = os.path.join(staging_dir, AU_BASE, build + MTON_DIR_SUFFIX)
99 os.mkdir(mton_payload_dir)
100
101 # Download build components into respective directories.
102 src = [full_payload_url,
103 nton_payload_url,
104 mton_payload_url,
105 archive_url + '/' + STATEFUL_UPDATE,
106 archive_url + '/' + AUTOTEST_PACKAGE]
107 dst = [os.path.join(staging_dir, ROOT_UPDATE),
108 os.path.join(nton_payload_dir, ROOT_UPDATE),
109 os.path.join(mton_payload_dir, ROOT_UPDATE),
110 staging_dir,
111 staging_dir]
112 for src, dest in zip(src, dst):
113 cmd = 'gsutil cp %s %s' % (src, dest)
114 msg = 'Failed to download "%s".' % src
115 try:
116 cros_build_lib.RunCommand(cmd, shell=True, error_message=msg)
117 except cros_build_lib.RunCommandError, e:
118 raise DevServerUtilError(str(e))
119
120
121def InstallBuild(staging_dir, build_dir):
122 """Installs various build components from staging directory.
123
124 Specifically, the following components are installed:
125 - update.gz
126 - stateful.tgz
127 - chromiumos_test_image.bin
128 - The entire contents of the au directory. Symlinks are generated for each
129 au payload as well.
130 - Contents of autotest-pkgs directory.
131 - Control files from autotest/server/{tests, site_tests}
132
133 Args:
134 staging_dir: Temp directory containing payloads and autotest packages.
135 build_dir: Directory to install build components into.
136 """
137 install_list = [ROOT_UPDATE, STATEFUL_UPDATE]
138
139 # Create blank chromiumos_test_image.bin. Otherwise the Dev Server will
140 # try to rebuild it unnecessarily.
141 test_image = os.path.join(build_dir, TEST_IMAGE)
142 open(test_image, 'a').close()
143
144 # Install AU payloads.
145 au_path = os.path.join(staging_dir, AU_BASE)
146 install_list.append(AU_BASE)
147 # For each AU payload, setup symlinks to the main payloads.
148 cwd = os.getcwd()
149 for au in os.listdir(au_path):
150 os.chdir(os.path.join(au_path, au))
151 os.symlink(os.path.join(os.pardir, os.pardir, TEST_IMAGE), TEST_IMAGE)
152 os.symlink(os.path.join(os.pardir, os.pardir, STATEFUL_UPDATE),
153 STATEFUL_UPDATE)
154 os.chdir(cwd)
155
156 for component in install_list:
157 shutil.move(os.path.join(staging_dir, component), build_dir)
158
Scott Zawalski4647ce62012-01-03 17:17:28 -0500159 shutil.move(os.path.join(staging_dir, 'autotest'),
Frank Farzan37761d12011-12-01 14:29:08 -0800160 os.path.join(build_dir, 'autotest'))
161
Frank Farzan37761d12011-12-01 14:29:08 -0800162
163def PrepareAutotestPkgs(staging_dir):
164 """Create autotest client packages inside staging_dir.
165
166 Args:
167 staging_dir: Temp directory containing payloads and autotest packages.
168
169 Raises:
170 DevServerUtilError: If any steps in the process fail to complete.
171 """
172 cmd = ('tar xf %s --use-compress-prog=pbzip2 --directory=%s' %
173 (os.path.join(staging_dir, AUTOTEST_PACKAGE), staging_dir))
174 msg = 'Failed to extract autotest.tar.bz2 ! Is pbzip2 installed?'
175 try:
176 cros_build_lib.RunCommand(cmd, shell=True, error_message=msg)
177 except cros_build_lib.RunCommandError, e:
178 raise DevServerUtilError(str(e))
179
Scott Zawalski4647ce62012-01-03 17:17:28 -0500180 # Use the root of Autotest
181 autotest_pkgs_dir = os.path.join(staging_dir, 'autotest', 'packages')
182 if not os.path.exists(autotest_pkgs_dir):
183 os.makedirs(autotest_pkgs_dir)
Frank Farzan37761d12011-12-01 14:29:08 -0800184 cmd_list = ['autotest/utils/packager.py',
Scott Zawalski4647ce62012-01-03 17:17:28 -0500185 'upload', '--repository', autotest_pkgs_dir, '--all']
Frank Farzan37761d12011-12-01 14:29:08 -0800186 msg = 'Failed to create autotest packages!'
187 try:
188 cros_build_lib.RunCommand(' '.join(cmd_list), cwd=staging_dir, shell=True,
189 error_message=msg)
190 except cros_build_lib.RunCommandError, e:
191 raise DevServerUtilError(str(e))
192
193
194def SafeSandboxAccess(static_dir, path):
195 """Verify that the path is in static_dir.
196
197 Args:
198 static_dir: Directory where builds are served from.
199 path: Path to verify.
200
201 Returns:
202 True if path is in static_dir, False otherwise
203 """
204 static_dir = os.path.realpath(static_dir)
205 path = os.path.realpath(path)
206 return (path.startswith(static_dir) and path != static_dir)
207
208
209def AcquireLock(static_dir, tag):
210 """Acquires a lock for a given tag.
211
212 Creates a directory for the specified tag, telling other
213 components the resource/task represented by the tag is unavailable.
214
215 Args:
216 static_dir: Directory where builds are served from.
217 tag: Unique resource/task identifier. Use '/' for nested tags.
218
219 Returns:
220 Path to the created directory or None if creation failed.
221
222 Raises:
223 DevServerUtilError: If lock can't be acquired.
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 try:
230 os.makedirs(build_dir)
231 except OSError, e:
232 if e.errno == errno.EEXIST:
233 raise DevServerUtilError(str(e))
234 else:
235 raise
236
237 return build_dir
238
239
240def ReleaseLock(static_dir, tag):
241 """Releases the lock for a given tag. Removes lock directory content.
242
243 Args:
244 static_dir: Directory where builds are served from.
245 tag: Unique resource/task identifier. Use '/' for nested tags.
246
247 Raises:
248 DevServerUtilError: If lock can't be released.
249 """
250 build_dir = os.path.join(static_dir, tag)
251 if not SafeSandboxAccess(static_dir, build_dir):
252 raise DevServerUtilError('Invaid tag "%s".' % tag)
253
254 shutil.rmtree(build_dir)
255
256
257def FindMatchingBoards(static_dir, board):
258 """Returns a list of boards given a partial board name.
259
260 Args:
261 static_dir: Directory where builds are served from.
262 board: Partial board name for this build; e.g. x86-generic.
263
264 Returns:
265 Returns a list of boards given a partial board.
266 """
267 return [brd for brd in os.listdir(static_dir) if board in brd]
268
269
270def FindMatchingBuilds(static_dir, board, build):
271 """Returns a list of matching builds given a board and partial build.
272
273 Args:
274 static_dir: Directory where builds are served from.
275 board: Partial board name for this build; e.g. x86-generic-release.
276 build: Partial build string to look for; e.g. R17-1234.
277
278 Returns:
279 Returns a list of (board, build) tuples given a partial board and build.
280 """
281 matches = []
282 for brd in FindMatchingBoards(static_dir, board):
283 a = [(brd, bld) for bld in
284 os.listdir(os.path.join(static_dir, brd)) if build in bld]
285 matches.extend(a)
286 return matches
287
288
289def GetLatestBuildVersion(static_dir, board):
290 """Retrieves the latest build version for a given board.
291
292 Args:
293 static_dir: Directory where builds are served from.
294 board: Board name for this build; e.g. x86-generic-release.
295
296 Returns:
297 Full build string; e.g. R17-1234.0.0-a1-b983.
298 """
299 builds = [distutils.version.LooseVersion(build) for build in
300 os.listdir(os.path.join(static_dir, board))]
301 return str(max(builds))
302
303
304def FindBuild(static_dir, board, build):
305 """Given partial build and board ids, figure out the appropriate build.
306
307 Args:
308 static_dir: Directory where builds are served from.
309 board: Partial board name for this build; e.g. x86-generic.
310 build: Partial build string to look for; e.g. R17-1234 or "latest" to
311 return the latest build for for most newest board.
312
313 Returns:
314 Tuple of (board, build):
315 board: Fully qualified board name; e.g. x86-generic-release
316 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983
317
318 Raises:
319 DevServerUtilError: If no boards, no builds, or too many builds
320 are matched.
321 """
322 if build.lower() == 'latest':
323 boards = FindMatchingBoards(static_dir, board)
324 if not boards:
325 raise DevServerUtilError(
326 'No boards matching %s could be found on the Dev Server.' % board)
327
328 if len(boards) > 1:
329 raise DevServerUtilError(
330 'The given board name is ambiguous. Disambiguate by using one of'
331 ' these instead: %s' % ', '.join(boards))
332
333 build = GetLatestBuildVersion(static_dir, board)
334 else:
335 builds = FindMatchingBuilds(static_dir, board, build)
336 if not builds:
337 raise DevServerUtilError(
338 'No builds matching %s could be found for board %s.' % (
339 build, board))
340
341 if len(builds) > 1:
342 raise DevServerUtilError(
343 'The given build id is ambiguous. Disambiguate by using one of'
344 ' these instead: %s' % ', '.join([b[1] for b in builds]))
345
346 board, build = builds[0]
347
348 return board, build
349
350
351def CloneBuild(static_dir, board, build, tag, force=False):
352 """Clone an official build into the developer sandbox.
353
354 Developer sandbox directory must already exist.
355
356 Args:
357 static_dir: Directory where builds are served from.
358 board: Fully qualified board name; e.g. x86-generic-release.
359 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
360 tag: Unique resource/task identifier. Use '/' for nested tags.
361 force: Force re-creation of build_dir even if it already exists.
362
363 Returns:
364 The path to the new build.
365 """
366 # Create the developer build directory.
367 dev_static_dir = os.path.join(static_dir, DEV_BUILD_PREFIX)
368 dev_build_dir = os.path.join(dev_static_dir, tag)
369 official_build_dir = os.path.join(static_dir, board, build)
370 cherrypy.log('Cloning %s -> %s' % (official_build_dir, dev_build_dir),
371 'DEVSERVER_UTIL')
372 dev_build_exists = False
373 try:
374 AcquireLock(dev_static_dir, tag)
375 except DevServerUtilError:
376 dev_build_exists = True
377 if force:
378 dev_build_exists = False
379 ReleaseLock(dev_static_dir, tag)
380 AcquireLock(dev_static_dir, tag)
381
382 # Make a copy of the official build, only take necessary files.
383 if not dev_build_exists:
384 copy_list = [TEST_IMAGE, ROOT_UPDATE, STATEFUL_UPDATE]
385 for f in copy_list:
386 shutil.copy(os.path.join(official_build_dir, f), dev_build_dir)
387
388 return dev_build_dir
389
Frank Farzan37761d12011-12-01 14:29:08 -0800390def GetControlFile(static_dir, board, build, control_path):
391 """Attempts to pull the requested control file from the Dev Server.
392
393 Args:
394 static_dir: Directory where builds are served from.
395 board: Fully qualified board name; e.g. x86-generic-release.
396 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
397 control_path: Path to control file on Dev Server relative to Autotest root.
398
399 Raises:
400 DevServerUtilError: If lock can't be acquired.
401
402 Returns:
403 Content of the requested control file.
404 """
Scott Zawalski4647ce62012-01-03 17:17:28 -0500405 control_path = os.path.join(static_dir, board, build, 'autotest',
406 control_path)
Frank Farzan37761d12011-12-01 14:29:08 -0800407 if not SafeSandboxAccess(static_dir, control_path):
408 raise DevServerUtilError('Invaid control file "%s".' % control_path)
409
410 with open(control_path, 'r') as control_file:
411 return control_file.read()
412
413
Scott Zawalski4647ce62012-01-03 17:17:28 -0500414def GetControlFileList(static_dir, board, build):
415 """List all control|control. files in the specified board/build path.
416
417 Args:
418 static_dir: Directory where builds are served from.
419 board: Fully qualified board name; e.g. x86-generic-release.
420 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
421
422 Raises:
423 DevServerUtilError: If path is outside of sandbox.
424
425 Returns:
426 String of each file separated by a newline.
427 """
428 autotest_dir = os.path.join(static_dir, board, build, 'autotest')
429 if not SafeSandboxAccess(static_dir, autotest_dir):
430 raise DevServerUtilError('Autotest dir not in sandbox "%s".' % autotest_dir)
431
432 control_files = set()
433 control_paths = ['testsuite', 'server/tests', 'server/site_tests',
434 'client/tests', 'client/site_tests']
435 for entry in os.walk(autotest_dir):
436 dir_path, _, files = entry
437 for file_entry in files:
438 if file_entry.startswith('control.') or file_entry == 'control':
439 control_files.add(os.path.join(dir_path,
440 file_entry).replace(static_dir,''))
441
442 return '\n'.join(control_files)
443
444
Frank Farzan37761d12011-12-01 14:29:08 -0800445def ListAutoupdateTargets(static_dir, board, build):
446 """Returns a list of autoupdate test targets for the given board, build.
447
448 Args:
449 static_dir: Directory where builds are served from.
450 board: Fully qualified board name; e.g. x86-generic-release.
451 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
452
453 Returns:
454 List of autoupdate test targets; e.g. ['0.14.747.0-r2bf8859c-b2927_nton']
455 """
456 return os.listdir(os.path.join(static_dir, board, build, AU_BASE))