blob: 3ce734481ea70693a81db35cb3a24fd01ffb19c5 [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 -080013
14
Chris Sosaea148d92012-03-06 16:22:04 -080015GSUTIL_ATTEMPTS = 5
Frank Farzan37761d12011-12-01 14:29:08 -080016AU_BASE = 'au'
17NTON_DIR_SUFFIX = '_nton'
18MTON_DIR_SUFFIX = '_mton'
19ROOT_UPDATE = 'update.gz'
20STATEFUL_UPDATE = 'stateful.tgz'
21TEST_IMAGE = 'chromiumos_test_image.bin'
22AUTOTEST_PACKAGE = 'autotest.tar.bz2'
23DEV_BUILD_PREFIX = 'dev'
24
25
26class DevServerUtilError(Exception):
27 """Exception classes used by this module."""
28 pass
29
30
31def ParsePayloadList(payload_list):
32 """Parse and return the full/delta payload URLs.
33
34 Args:
35 payload_list: A list of Google Storage URLs.
36
37 Returns:
38 Tuple of 3 payloads URLs: (full, nton, mton).
39
40 Raises:
41 DevServerUtilError: If payloads missing or invalid.
42 """
43 full_payload_url = None
44 mton_payload_url = None
45 nton_payload_url = None
46 for payload in payload_list:
47 if '_full_' in payload:
48 full_payload_url = payload
49 elif '_delta_' in payload:
50 # e.g. chromeos_{from_version}_{to_version}_x86-generic_delta_dev.bin
51 from_version, to_version = payload.rsplit('/', 1)[1].split('_')[1:3]
52 if from_version == to_version:
53 nton_payload_url = payload
54 else:
55 mton_payload_url = payload
56
57 if not full_payload_url or not nton_payload_url or not mton_payload_url:
58 raise DevServerUtilError(
59 'Payloads are missing or have unexpected name formats.', payload_list)
60
61 return full_payload_url, nton_payload_url, mton_payload_url
62
63
64def DownloadBuildFromGS(staging_dir, archive_url, build):
65 """Downloads the specified build from Google Storage into a temp directory.
66
67 The archive is expected to contain stateful.tgz, autotest.tar.bz2, and three
68 payloads: full, N-1->N, and N->N. gsutil is used to download the file.
69 gsutil must be in the path and should have required credentials.
70
71 Args:
72 staging_dir: Temp directory containing payloads and autotest packages.
73 archive_url: Google Storage path to the build directory.
Frank Farzanbcb571e2012-01-03 11:48:17 -080074 e.g. gs://chromeos-image-archive/x86-generic/R17-1208.0.0-a1-b338.
Frank Farzan37761d12011-12-01 14:29:08 -080075 build: Full build string to look for; e.g. R17-1208.0.0-a1-b338.
76
77 Raises:
78 DevServerUtilError: If any steps in the process fail to complete.
79 """
Chris Sosaea148d92012-03-06 16:22:04 -080080 def GSUtilRun(cmd, err_msg):
81 """Runs a GSUTIL command up to GSUTIL_ATTEMPTS number of times.
82
83 Raises:
84 subprocess.CalledProcessError if all attempt to run gsutil cmd fails.
85 """
86 proc = None
87 for _attempt in range(GSUTIL_ATTEMPTS):
88 proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
89 stdout, _stderr = proc.communicate()
90 if proc.returncode == 0:
91 return stdout
92 else:
93 raise DevServerUtilError('%s GSUTIL cmd %s failed with return code %d' % (
94 err_msg, cmd, proc.returncode))
95
Frank Farzan37761d12011-12-01 14:29:08 -080096 # Get a list of payloads from Google Storage.
97 cmd = 'gsutil ls %s/*.bin' % archive_url
98 msg = 'Failed to get a list of payloads.'
Chris Sosaea148d92012-03-06 16:22:04 -080099 stdout = GSUtilRun(cmd, msg)
Brian Harringa99a3f62012-02-24 10:38:22 -0800100
101 payload_list = stdout.splitlines()
Frank Farzan37761d12011-12-01 14:29:08 -0800102 full_payload_url, nton_payload_url, mton_payload_url = (
103 ParsePayloadList(payload_list))
104
105 # Create temp directories for payloads.
106 nton_payload_dir = os.path.join(staging_dir, AU_BASE, build + NTON_DIR_SUFFIX)
107 os.makedirs(nton_payload_dir)
108 mton_payload_dir = os.path.join(staging_dir, AU_BASE, build + MTON_DIR_SUFFIX)
109 os.mkdir(mton_payload_dir)
110
111 # Download build components into respective directories.
112 src = [full_payload_url,
113 nton_payload_url,
114 mton_payload_url,
115 archive_url + '/' + STATEFUL_UPDATE,
116 archive_url + '/' + AUTOTEST_PACKAGE]
117 dst = [os.path.join(staging_dir, ROOT_UPDATE),
118 os.path.join(nton_payload_dir, ROOT_UPDATE),
119 os.path.join(mton_payload_dir, ROOT_UPDATE),
120 staging_dir,
121 staging_dir]
122 for src, dest in zip(src, dst):
123 cmd = 'gsutil cp %s %s' % (src, dest)
124 msg = 'Failed to download "%s".' % src
Chris Sosaea148d92012-03-06 16:22:04 -0800125 GSUtilRun(cmd, msg)
Frank Farzan37761d12011-12-01 14:29:08 -0800126
127
128def InstallBuild(staging_dir, build_dir):
129 """Installs various build components from staging directory.
130
131 Specifically, the following components are installed:
132 - update.gz
133 - stateful.tgz
134 - chromiumos_test_image.bin
135 - The entire contents of the au directory. Symlinks are generated for each
136 au payload as well.
137 - Contents of autotest-pkgs directory.
138 - Control files from autotest/server/{tests, site_tests}
139
140 Args:
141 staging_dir: Temp directory containing payloads and autotest packages.
142 build_dir: Directory to install build components into.
143 """
144 install_list = [ROOT_UPDATE, STATEFUL_UPDATE]
145
146 # Create blank chromiumos_test_image.bin. Otherwise the Dev Server will
147 # try to rebuild it unnecessarily.
148 test_image = os.path.join(build_dir, TEST_IMAGE)
149 open(test_image, 'a').close()
150
151 # Install AU payloads.
152 au_path = os.path.join(staging_dir, AU_BASE)
153 install_list.append(AU_BASE)
154 # For each AU payload, setup symlinks to the main payloads.
155 cwd = os.getcwd()
156 for au in os.listdir(au_path):
157 os.chdir(os.path.join(au_path, au))
158 os.symlink(os.path.join(os.pardir, os.pardir, TEST_IMAGE), TEST_IMAGE)
159 os.symlink(os.path.join(os.pardir, os.pardir, STATEFUL_UPDATE),
160 STATEFUL_UPDATE)
161 os.chdir(cwd)
162
163 for component in install_list:
164 shutil.move(os.path.join(staging_dir, component), build_dir)
165
Scott Zawalski4647ce62012-01-03 17:17:28 -0500166 shutil.move(os.path.join(staging_dir, 'autotest'),
Frank Farzan37761d12011-12-01 14:29:08 -0800167 os.path.join(build_dir, 'autotest'))
168
Frank Farzan37761d12011-12-01 14:29:08 -0800169
170def PrepareAutotestPkgs(staging_dir):
171 """Create autotest client packages inside staging_dir.
172
173 Args:
174 staging_dir: Temp directory containing payloads and autotest packages.
175
176 Raises:
177 DevServerUtilError: If any steps in the process fail to complete.
178 """
179 cmd = ('tar xf %s --use-compress-prog=pbzip2 --directory=%s' %
180 (os.path.join(staging_dir, AUTOTEST_PACKAGE), staging_dir))
181 msg = 'Failed to extract autotest.tar.bz2 ! Is pbzip2 installed?'
182 try:
Brian Harringa99a3f62012-02-24 10:38:22 -0800183 subprocess.check_call(cmd, shell=True)
184 except subprocess.CalledProcessError, e:
185 raise DevServerUtilError('%s %s' % (msg, e))
Frank Farzan37761d12011-12-01 14:29:08 -0800186
Scott Zawalski4647ce62012-01-03 17:17:28 -0500187 # Use the root of Autotest
188 autotest_pkgs_dir = os.path.join(staging_dir, 'autotest', 'packages')
189 if not os.path.exists(autotest_pkgs_dir):
190 os.makedirs(autotest_pkgs_dir)
Chris Sosae1525992012-03-12 16:55:56 -0700191
192 if not os.path.exists(os.path.join(autotest_pkgs_dir, 'packages.checksum')):
193 cmd_list = ['autotest/utils/packager.py',
194 'upload', '--repository', autotest_pkgs_dir, '--all']
195 msg = 'Failed to create autotest packages!'
196 try:
197 subprocess.check_call(' '.join(cmd_list), cwd=staging_dir, shell=True)
198 except subprocess.CalledProcessError, e:
199 raise DevServerUtilError('%s %s' % (msg, e))
200 else:
201 cherrypy.log('Using pre-generated packages from autotest', 'DEVSERVER_UTIL')
Frank Farzan37761d12011-12-01 14:29:08 -0800202
Scott Zawalskiae569482012-03-23 17:30:12 -0400203 # TODO(scottz): Remove after we have moved away from the old test_scheduler
204 # code.
205 cmd = 'cp %s/* %s' % (autotest_pkgs_dir,
206 os.path.join(staging_dir, 'autotest'))
207 subprocess.check_call(cmd, shell=True)
208
Frank Farzan37761d12011-12-01 14:29:08 -0800209
210def SafeSandboxAccess(static_dir, path):
211 """Verify that the path is in static_dir.
212
213 Args:
214 static_dir: Directory where builds are served from.
215 path: Path to verify.
216
217 Returns:
218 True if path is in static_dir, False otherwise
219 """
220 static_dir = os.path.realpath(static_dir)
221 path = os.path.realpath(path)
222 return (path.startswith(static_dir) and path != static_dir)
223
224
225def AcquireLock(static_dir, tag):
226 """Acquires a lock for a given tag.
227
228 Creates a directory for the specified tag, telling other
229 components the resource/task represented by the tag is unavailable.
230
231 Args:
232 static_dir: Directory where builds are served from.
233 tag: Unique resource/task identifier. Use '/' for nested tags.
234
235 Returns:
236 Path to the created directory or None if creation failed.
237
238 Raises:
239 DevServerUtilError: If lock can't be acquired.
240 """
241 build_dir = os.path.join(static_dir, tag)
242 if not SafeSandboxAccess(static_dir, build_dir):
243 raise DevServerUtilError('Invaid tag "%s".' % tag)
244
245 try:
246 os.makedirs(build_dir)
247 except OSError, e:
248 if e.errno == errno.EEXIST:
249 raise DevServerUtilError(str(e))
250 else:
251 raise
252
253 return build_dir
254
255
256def ReleaseLock(static_dir, tag):
257 """Releases the lock for a given tag. Removes lock directory content.
258
259 Args:
260 static_dir: Directory where builds are served from.
261 tag: Unique resource/task identifier. Use '/' for nested tags.
262
263 Raises:
264 DevServerUtilError: If lock can't be released.
265 """
266 build_dir = os.path.join(static_dir, tag)
267 if not SafeSandboxAccess(static_dir, build_dir):
268 raise DevServerUtilError('Invaid tag "%s".' % tag)
269
270 shutil.rmtree(build_dir)
271
272
273def FindMatchingBoards(static_dir, board):
274 """Returns a list of boards given a partial board name.
275
276 Args:
277 static_dir: Directory where builds are served from.
278 board: Partial board name for this build; e.g. x86-generic.
279
280 Returns:
281 Returns a list of boards given a partial board.
282 """
283 return [brd for brd in os.listdir(static_dir) if board in brd]
284
285
286def FindMatchingBuilds(static_dir, board, build):
287 """Returns a list of matching builds given a board and partial build.
288
289 Args:
290 static_dir: Directory where builds are served from.
291 board: Partial board name for this build; e.g. x86-generic-release.
292 build: Partial build string to look for; e.g. R17-1234.
293
294 Returns:
295 Returns a list of (board, build) tuples given a partial board and build.
296 """
297 matches = []
298 for brd in FindMatchingBoards(static_dir, board):
299 a = [(brd, bld) for bld in
300 os.listdir(os.path.join(static_dir, brd)) if build in bld]
301 matches.extend(a)
302 return matches
303
304
Scott Zawalski16954532012-03-20 15:31:36 -0400305def GetLatestBuildVersion(static_dir, target, milestone=None):
Frank Farzan37761d12011-12-01 14:29:08 -0800306 """Retrieves the latest build version for a given board.
307
308 Args:
309 static_dir: Directory where builds are served from.
Scott Zawalski16954532012-03-20 15:31:36 -0400310 target: The build target, typically a combination of the board and the
311 type of build e.g. x86-mario-release.
312 milestone: For latest build set to None, for builds only in a specific
313 milestone set to a str of format Rxx (e.g. R16). Default: None.
Frank Farzan37761d12011-12-01 14:29:08 -0800314
315 Returns:
Scott Zawalski16954532012-03-20 15:31:36 -0400316 If latest found, a full build string is returned e.g. R17-1234.0.0-a1-b983.
317 If no latest is found for some reason or another a '' string is returned.
Frank Farzan37761d12011-12-01 14:29:08 -0800318
319 Raises:
Scott Zawalski16954532012-03-20 15:31:36 -0400320 DevServerUtilError: If for some reason the latest build cannot be
321 deteremined, this could be due to the dir not existing or no builds
322 being present after filtering on milestone.
Frank Farzan37761d12011-12-01 14:29:08 -0800323 """
Scott Zawalski16954532012-03-20 15:31:36 -0400324 target_path = os.path.join(static_dir, target)
325 if not os.path.isdir(target_path):
326 raise DevServerUtilError('Cannot find path %s' % target_path)
Frank Farzan37761d12011-12-01 14:29:08 -0800327
Scott Zawalski16954532012-03-20 15:31:36 -0400328 builds = [distutils.version.LooseVersion(build) for build in
329 os.listdir(target_path)]
Frank Farzan37761d12011-12-01 14:29:08 -0800330
Scott Zawalski16954532012-03-20 15:31:36 -0400331 if milestone and builds:
332 # Check if milestone Rxx is in the string representation of the build.
333 builds = filter(lambda x: milestone.upper() in str(x), builds)
Frank Farzan37761d12011-12-01 14:29:08 -0800334
Scott Zawalski16954532012-03-20 15:31:36 -0400335 if not builds:
336 raise DevServerUtilError('Could not determine build for %s' % target)
Frank Farzan37761d12011-12-01 14:29:08 -0800337
Scott Zawalski16954532012-03-20 15:31:36 -0400338 return str(max(builds))
Frank Farzan37761d12011-12-01 14:29:08 -0800339
340
341def CloneBuild(static_dir, board, build, tag, force=False):
342 """Clone an official build into the developer sandbox.
343
344 Developer sandbox directory must already exist.
345
346 Args:
347 static_dir: Directory where builds are served from.
348 board: Fully qualified board name; e.g. x86-generic-release.
349 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
350 tag: Unique resource/task identifier. Use '/' for nested tags.
351 force: Force re-creation of build_dir even if it already exists.
352
353 Returns:
354 The path to the new build.
355 """
356 # Create the developer build directory.
357 dev_static_dir = os.path.join(static_dir, DEV_BUILD_PREFIX)
358 dev_build_dir = os.path.join(dev_static_dir, tag)
359 official_build_dir = os.path.join(static_dir, board, build)
360 cherrypy.log('Cloning %s -> %s' % (official_build_dir, dev_build_dir),
361 'DEVSERVER_UTIL')
362 dev_build_exists = False
363 try:
364 AcquireLock(dev_static_dir, tag)
365 except DevServerUtilError:
366 dev_build_exists = True
367 if force:
368 dev_build_exists = False
369 ReleaseLock(dev_static_dir, tag)
370 AcquireLock(dev_static_dir, tag)
371
372 # Make a copy of the official build, only take necessary files.
373 if not dev_build_exists:
374 copy_list = [TEST_IMAGE, ROOT_UPDATE, STATEFUL_UPDATE]
375 for f in copy_list:
376 shutil.copy(os.path.join(official_build_dir, f), dev_build_dir)
377
378 return dev_build_dir
379
Scott Zawalski84a39c92012-01-13 15:12:42 -0500380
381def GetControlFile(static_dir, build, control_path):
Frank Farzan37761d12011-12-01 14:29:08 -0800382 """Attempts to pull the requested control file from the Dev Server.
383
384 Args:
385 static_dir: Directory where builds are served from.
Frank Farzan37761d12011-12-01 14:29:08 -0800386 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
387 control_path: Path to control file on Dev Server relative to Autotest root.
388
389 Raises:
390 DevServerUtilError: If lock can't be acquired.
391
392 Returns:
393 Content of the requested control file.
394 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500395 # Be forgiving if the user passes in the control_path with a leading /
396 control_path = control_path.lstrip('/')
Scott Zawalski84a39c92012-01-13 15:12:42 -0500397 control_path = os.path.join(static_dir, build, 'autotest',
Scott Zawalski4647ce62012-01-03 17:17:28 -0500398 control_path)
Frank Farzan37761d12011-12-01 14:29:08 -0800399 if not SafeSandboxAccess(static_dir, control_path):
400 raise DevServerUtilError('Invaid control file "%s".' % control_path)
401
Scott Zawalski84a39c92012-01-13 15:12:42 -0500402 if not os.path.exists(control_path):
403 # TODO(scottz): Come up with some sort of error mechanism.
404 # crosbug.com/25040
405 return 'Unknown control path %s' % control_path
406
Frank Farzan37761d12011-12-01 14:29:08 -0800407 with open(control_path, 'r') as control_file:
408 return control_file.read()
409
410
Scott Zawalski84a39c92012-01-13 15:12:42 -0500411def GetControlFileList(static_dir, build):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500412 """List all control|control. files in the specified board/build path.
413
414 Args:
415 static_dir: Directory where builds are served from.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500416 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
417
418 Raises:
419 DevServerUtilError: If path is outside of sandbox.
420
421 Returns:
422 String of each file separated by a newline.
423 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500424 autotest_dir = os.path.join(static_dir, build, 'autotest/')
Scott Zawalski4647ce62012-01-03 17:17:28 -0500425 if not SafeSandboxAccess(static_dir, autotest_dir):
426 raise DevServerUtilError('Autotest dir not in sandbox "%s".' % autotest_dir)
427
428 control_files = set()
Scott Zawalski84a39c92012-01-13 15:12:42 -0500429 if not os.path.exists(autotest_dir):
430 # TODO(scottz): Come up with some sort of error mechanism.
431 # crosbug.com/25040
432 return 'Unknown build path %s' % autotest_dir
433
Scott Zawalski4647ce62012-01-03 17:17:28 -0500434 for entry in os.walk(autotest_dir):
435 dir_path, _, files = entry
436 for file_entry in files:
437 if file_entry.startswith('control.') or file_entry == 'control':
438 control_files.add(os.path.join(dir_path,
Chris Sosaea148d92012-03-06 16:22:04 -0800439 file_entry).replace(autotest_dir, ''))
Scott Zawalski4647ce62012-01-03 17:17:28 -0500440
441 return '\n'.join(control_files)
442
443
Frank Farzan37761d12011-12-01 14:29:08 -0800444def ListAutoupdateTargets(static_dir, board, build):
445 """Returns a list of autoupdate test targets for the given board, build.
446
447 Args:
448 static_dir: Directory where builds are served from.
449 board: Fully qualified board name; e.g. x86-generic-release.
450 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
451
452 Returns:
453 List of autoupdate test targets; e.g. ['0.14.747.0-r2bf8859c-b2927_nton']
454 """
455 return os.listdir(os.path.join(static_dir, board, build, AU_BASE))