blob: 9cdb3bb28ce3445920333036d954fd6c447c0363 [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
159 # Install autotest-pkgs.
160 shutil.move(os.path.join(staging_dir, 'autotest-pkgs'),
161 os.path.join(build_dir, 'autotest'))
162
163 # Install autotest/server/{tests,site_tests}.
164 server_dir = os.path.join(build_dir, 'server')
165 os.mkdir(server_dir)
166 tests = os.path.join(staging_dir, 'autotest', 'server', 'tests')
167 site_tests = os.path.join(staging_dir, 'autotest', 'server', 'site_tests')
168 shutil.move(tests, server_dir)
169 shutil.move(site_tests, server_dir)
170
171
172def PrepareAutotestPkgs(staging_dir):
173 """Create autotest client packages inside staging_dir.
174
175 Args:
176 staging_dir: Temp directory containing payloads and autotest packages.
177
178 Raises:
179 DevServerUtilError: If any steps in the process fail to complete.
180 """
181 cmd = ('tar xf %s --use-compress-prog=pbzip2 --directory=%s' %
182 (os.path.join(staging_dir, AUTOTEST_PACKAGE), staging_dir))
183 msg = 'Failed to extract autotest.tar.bz2 ! Is pbzip2 installed?'
184 try:
185 cros_build_lib.RunCommand(cmd, shell=True, error_message=msg)
186 except cros_build_lib.RunCommandError, e:
187 raise DevServerUtilError(str(e))
188
189 os.mkdir(os.path.join(staging_dir, 'autotest-pkgs'))
190
191 cmd_list = ['autotest/utils/packager.py',
192 'upload', '--repository autotest-pkgs', '--all']
193 msg = 'Failed to create autotest packages!'
194 try:
195 cros_build_lib.RunCommand(' '.join(cmd_list), cwd=staging_dir, shell=True,
196 error_message=msg)
197 except cros_build_lib.RunCommandError, e:
198 raise DevServerUtilError(str(e))
199
200
201def SafeSandboxAccess(static_dir, path):
202 """Verify that the path is in static_dir.
203
204 Args:
205 static_dir: Directory where builds are served from.
206 path: Path to verify.
207
208 Returns:
209 True if path is in static_dir, False otherwise
210 """
211 static_dir = os.path.realpath(static_dir)
212 path = os.path.realpath(path)
213 return (path.startswith(static_dir) and path != static_dir)
214
215
216def AcquireLock(static_dir, tag):
217 """Acquires a lock for a given tag.
218
219 Creates a directory for the specified tag, telling other
220 components the resource/task represented by the tag is unavailable.
221
222 Args:
223 static_dir: Directory where builds are served from.
224 tag: Unique resource/task identifier. Use '/' for nested tags.
225
226 Returns:
227 Path to the created directory or None if creation failed.
228
229 Raises:
230 DevServerUtilError: If lock can't be acquired.
231 """
232 build_dir = os.path.join(static_dir, tag)
233 if not SafeSandboxAccess(static_dir, build_dir):
234 raise DevServerUtilError('Invaid tag "%s".' % tag)
235
236 try:
237 os.makedirs(build_dir)
238 except OSError, e:
239 if e.errno == errno.EEXIST:
240 raise DevServerUtilError(str(e))
241 else:
242 raise
243
244 return build_dir
245
246
247def ReleaseLock(static_dir, tag):
248 """Releases the lock for a given tag. Removes lock directory content.
249
250 Args:
251 static_dir: Directory where builds are served from.
252 tag: Unique resource/task identifier. Use '/' for nested tags.
253
254 Raises:
255 DevServerUtilError: If lock can't be released.
256 """
257 build_dir = os.path.join(static_dir, tag)
258 if not SafeSandboxAccess(static_dir, build_dir):
259 raise DevServerUtilError('Invaid tag "%s".' % tag)
260
261 shutil.rmtree(build_dir)
262
263
264def FindMatchingBoards(static_dir, board):
265 """Returns a list of boards given a partial board name.
266
267 Args:
268 static_dir: Directory where builds are served from.
269 board: Partial board name for this build; e.g. x86-generic.
270
271 Returns:
272 Returns a list of boards given a partial board.
273 """
274 return [brd for brd in os.listdir(static_dir) if board in brd]
275
276
277def FindMatchingBuilds(static_dir, board, build):
278 """Returns a list of matching builds given a board and partial build.
279
280 Args:
281 static_dir: Directory where builds are served from.
282 board: Partial board name for this build; e.g. x86-generic-release.
283 build: Partial build string to look for; e.g. R17-1234.
284
285 Returns:
286 Returns a list of (board, build) tuples given a partial board and build.
287 """
288 matches = []
289 for brd in FindMatchingBoards(static_dir, board):
290 a = [(brd, bld) for bld in
291 os.listdir(os.path.join(static_dir, brd)) if build in bld]
292 matches.extend(a)
293 return matches
294
295
296def GetLatestBuildVersion(static_dir, board):
297 """Retrieves the latest build version for a given board.
298
299 Args:
300 static_dir: Directory where builds are served from.
301 board: Board name for this build; e.g. x86-generic-release.
302
303 Returns:
304 Full build string; e.g. R17-1234.0.0-a1-b983.
305 """
306 builds = [distutils.version.LooseVersion(build) for build in
307 os.listdir(os.path.join(static_dir, board))]
308 return str(max(builds))
309
310
311def FindBuild(static_dir, board, build):
312 """Given partial build and board ids, figure out the appropriate build.
313
314 Args:
315 static_dir: Directory where builds are served from.
316 board: Partial board name for this build; e.g. x86-generic.
317 build: Partial build string to look for; e.g. R17-1234 or "latest" to
318 return the latest build for for most newest board.
319
320 Returns:
321 Tuple of (board, build):
322 board: Fully qualified board name; e.g. x86-generic-release
323 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983
324
325 Raises:
326 DevServerUtilError: If no boards, no builds, or too many builds
327 are matched.
328 """
329 if build.lower() == 'latest':
330 boards = FindMatchingBoards(static_dir, board)
331 if not boards:
332 raise DevServerUtilError(
333 'No boards matching %s could be found on the Dev Server.' % board)
334
335 if len(boards) > 1:
336 raise DevServerUtilError(
337 'The given board name is ambiguous. Disambiguate by using one of'
338 ' these instead: %s' % ', '.join(boards))
339
340 build = GetLatestBuildVersion(static_dir, board)
341 else:
342 builds = FindMatchingBuilds(static_dir, board, build)
343 if not builds:
344 raise DevServerUtilError(
345 'No builds matching %s could be found for board %s.' % (
346 build, board))
347
348 if len(builds) > 1:
349 raise DevServerUtilError(
350 'The given build id is ambiguous. Disambiguate by using one of'
351 ' these instead: %s' % ', '.join([b[1] for b in builds]))
352
353 board, build = builds[0]
354
355 return board, build
356
357
358def CloneBuild(static_dir, board, build, tag, force=False):
359 """Clone an official build into the developer sandbox.
360
361 Developer sandbox directory must already exist.
362
363 Args:
364 static_dir: Directory where builds are served from.
365 board: Fully qualified board name; e.g. x86-generic-release.
366 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
367 tag: Unique resource/task identifier. Use '/' for nested tags.
368 force: Force re-creation of build_dir even if it already exists.
369
370 Returns:
371 The path to the new build.
372 """
373 # Create the developer build directory.
374 dev_static_dir = os.path.join(static_dir, DEV_BUILD_PREFIX)
375 dev_build_dir = os.path.join(dev_static_dir, tag)
376 official_build_dir = os.path.join(static_dir, board, build)
377 cherrypy.log('Cloning %s -> %s' % (official_build_dir, dev_build_dir),
378 'DEVSERVER_UTIL')
379 dev_build_exists = False
380 try:
381 AcquireLock(dev_static_dir, tag)
382 except DevServerUtilError:
383 dev_build_exists = True
384 if force:
385 dev_build_exists = False
386 ReleaseLock(dev_static_dir, tag)
387 AcquireLock(dev_static_dir, tag)
388
389 # Make a copy of the official build, only take necessary files.
390 if not dev_build_exists:
391 copy_list = [TEST_IMAGE, ROOT_UPDATE, STATEFUL_UPDATE]
392 for f in copy_list:
393 shutil.copy(os.path.join(official_build_dir, f), dev_build_dir)
394
395 return dev_build_dir
396
397
398def GetControlFile(static_dir, board, build, control_path):
399 """Attempts to pull the requested control file from the Dev Server.
400
401 Args:
402 static_dir: Directory where builds are served from.
403 board: Fully qualified board name; e.g. x86-generic-release.
404 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
405 control_path: Path to control file on Dev Server relative to Autotest root.
406
407 Raises:
408 DevServerUtilError: If lock can't be acquired.
409
410 Returns:
411 Content of the requested control file.
412 """
413 control_path = os.path.join(static_dir, board, build, control_path)
414 if not SafeSandboxAccess(static_dir, control_path):
415 raise DevServerUtilError('Invaid control file "%s".' % control_path)
416
417 with open(control_path, 'r') as control_file:
418 return control_file.read()
419
420
421def ListAutoupdateTargets(static_dir, board, build):
422 """Returns a list of autoupdate test targets for the given board, build.
423
424 Args:
425 static_dir: Directory where builds are served from.
426 board: Fully qualified board name; e.g. x86-generic-release.
427 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
428
429 Returns:
430 List of autoupdate test targets; e.g. ['0.14.747.0-r2bf8859c-b2927_nton']
431 """
432 return os.listdir(os.path.join(static_dir, board, build, AU_BASE))