blob: 6ee02aa88cf39fb2fd1445bd9719f843c52eebb8 [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.
77 e.g. chromeos-image-archive/x86-generic-release/R17-1208.0.0-a1-b338.
78 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 """
83 archive_url = 'gs://' + archive_url
84
85 # Get a list of payloads from Google Storage.
86 cmd = 'gsutil ls %s/*.bin' % archive_url
87 msg = 'Failed to get a list of payloads.'
88 try:
89 result = cros_build_lib.RunCommand(cmd, shell=True, redirect_stdout=True,
90 error_message=msg)
91 except cros_build_lib.RunCommandError, e:
92 raise DevServerUtilError(str(e))
93 payload_list = result.output.splitlines()
94 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:
118 cros_build_lib.RunCommand(cmd, shell=True, error_message=msg)
119 except cros_build_lib.RunCommandError, e:
120 raise DevServerUtilError(str(e))
121
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
161 # Install autotest-pkgs.
162 shutil.move(os.path.join(staging_dir, 'autotest-pkgs'),
163 os.path.join(build_dir, 'autotest'))
164
165 # Install autotest/server/{tests,site_tests}.
166 server_dir = os.path.join(build_dir, 'server')
167 os.mkdir(server_dir)
168 tests = os.path.join(staging_dir, 'autotest', 'server', 'tests')
169 site_tests = os.path.join(staging_dir, 'autotest', 'server', 'site_tests')
170 shutil.move(tests, server_dir)
171 shutil.move(site_tests, server_dir)
172
173
174def PrepareAutotestPkgs(staging_dir):
175 """Create autotest client packages inside staging_dir.
176
177 Args:
178 staging_dir: Temp directory containing payloads and autotest packages.
179
180 Raises:
181 DevServerUtilError: If any steps in the process fail to complete.
182 """
183 cmd = ('tar xf %s --use-compress-prog=pbzip2 --directory=%s' %
184 (os.path.join(staging_dir, AUTOTEST_PACKAGE), staging_dir))
185 msg = 'Failed to extract autotest.tar.bz2 ! Is pbzip2 installed?'
186 try:
187 cros_build_lib.RunCommand(cmd, shell=True, error_message=msg)
188 except cros_build_lib.RunCommandError, e:
189 raise DevServerUtilError(str(e))
190
191 os.mkdir(os.path.join(staging_dir, 'autotest-pkgs'))
192
193 cmd_list = ['autotest/utils/packager.py',
194 'upload', '--repository autotest-pkgs', '--all']
195 msg = 'Failed to create autotest packages!'
196 try:
197 cros_build_lib.RunCommand(' '.join(cmd_list), cwd=staging_dir, shell=True,
198 error_message=msg)
199 except cros_build_lib.RunCommandError, e:
200 raise DevServerUtilError(str(e))
201
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
399
400def GetControlFile(static_dir, board, build, control_path):
401 """Attempts to pull the requested control file from the Dev Server.
402
403 Args:
404 static_dir: Directory where builds are served from.
405 board: Fully qualified board name; e.g. x86-generic-release.
406 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
407 control_path: Path to control file on Dev Server relative to Autotest root.
408
409 Raises:
410 DevServerUtilError: If lock can't be acquired.
411
412 Returns:
413 Content of the requested control file.
414 """
415 control_path = os.path.join(static_dir, board, build, control_path)
416 if not SafeSandboxAccess(static_dir, control_path):
417 raise DevServerUtilError('Invaid control file "%s".' % control_path)
418
419 with open(control_path, 'r') as control_file:
420 return control_file.read()
421
422
423def ListAutoupdateTargets(static_dir, board, build):
424 """Returns a list of autoupdate test targets for the given board, build.
425
426 Args:
427 static_dir: Directory where builds are served from.
428 board: Fully qualified board name; e.g. x86-generic-release.
429 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
430
431 Returns:
432 List of autoupdate test targets; e.g. ['0.14.747.0-r2bf8859c-b2927_nton']
433 """
434 return os.listdir(os.path.join(static_dir, board, build, AU_BASE))