blob: eb498f4db21db8f8170faf2bd352bda2e2df9747 [file] [log] [blame]
Chris Sosa47a7d4e2012-03-28 11:26:55 -07001# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
Frank Farzan37761d12011-12-01 14:29:08 -08002# 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
Frank Farzan37761d12011-12-01 14:29:08 -080012
Chris Sosa47a7d4e2012-03-28 11:26:55 -070013import downloadable_artifact
14import gsutil_util
Frank Farzan37761d12011-12-01 14:29:08 -080015
16AU_BASE = 'au'
17NTON_DIR_SUFFIX = '_nton'
18MTON_DIR_SUFFIX = '_mton'
Frank Farzan37761d12011-12-01 14:29:08 -080019DEV_BUILD_PREFIX = 'dev'
20
21
22class DevServerUtilError(Exception):
23 """Exception classes used by this module."""
24 pass
25
26
27def ParsePayloadList(payload_list):
28 """Parse and return the full/delta payload URLs.
29
30 Args:
31 payload_list: A list of Google Storage URLs.
32
33 Returns:
34 Tuple of 3 payloads URLs: (full, nton, mton).
35
36 Raises:
37 DevServerUtilError: If payloads missing or invalid.
38 """
39 full_payload_url = None
40 mton_payload_url = None
41 nton_payload_url = None
42 for payload in payload_list:
43 if '_full_' in payload:
44 full_payload_url = payload
45 elif '_delta_' in payload:
46 # e.g. chromeos_{from_version}_{to_version}_x86-generic_delta_dev.bin
47 from_version, to_version = payload.rsplit('/', 1)[1].split('_')[1:3]
48 if from_version == to_version:
49 nton_payload_url = payload
50 else:
51 mton_payload_url = payload
52
Chris Sosa781ba6d2012-04-11 12:44:43 -070053 if not full_payload_url or not nton_payload_url:
Frank Farzan37761d12011-12-01 14:29:08 -080054 raise DevServerUtilError(
55 'Payloads are missing or have unexpected name formats.', payload_list)
56
57 return full_payload_url, nton_payload_url, mton_payload_url
58
59
Scott Zawalski50fe4272012-04-30 18:19:28 -070060def GatherArtifactDownloads(main_staging_dir, archive_url, build, build_dir):
Chris Sosa47a7d4e2012-03-28 11:26:55 -070061 """Generates artifacts that we mean to download and install for autotest.
Frank Farzan37761d12011-12-01 14:29:08 -080062
Chris Sosa47a7d4e2012-03-28 11:26:55 -070063 This method generates the list of artifacts we will need for autotest. These
Scott Zawalski50fe4272012-04-30 18:19:28 -070064 artifacts are instances of downloadable_artifact.DownloadableArtifact.Note,
65 these artifacts can be downloaded asynchronously iff !artifact.Synchronous().
Scott Zawalski51ccf9e2012-03-28 08:16:01 -070066 """
Scott Zawalski51ccf9e2012-03-28 08:16:01 -070067 cmd = 'gsutil ls %s/*.bin' % archive_url
68 msg = 'Failed to get a list of payloads.'
Chris Sosa47a7d4e2012-03-28 11:26:55 -070069 payload_list = gsutil_util.GSUtilRun(cmd, msg).splitlines()
Scott Zawalski51ccf9e2012-03-28 08:16:01 -070070
Chris Sosa47a7d4e2012-03-28 11:26:55 -070071 # First we gather the urls/paths for the update payloads.
72 full_url, nton_url, mton_url = ParsePayloadList(payload_list)
Scott Zawalski51ccf9e2012-03-28 08:16:01 -070073
Chris Sosa47a7d4e2012-03-28 11:26:55 -070074 full_payload = os.path.join(build_dir, downloadable_artifact.ROOT_UPDATE)
75 nton_payload = os.path.join(build_dir, AU_BASE, build + NTON_DIR_SUFFIX,
76 downloadable_artifact.ROOT_UPDATE)
Scott Zawalski51ccf9e2012-03-28 08:16:01 -070077
Chris Sosa47a7d4e2012-03-28 11:26:55 -070078 artifacts = []
79 artifacts.append(downloadable_artifact.DownloadableArtifact(full_url,
80 main_staging_dir, full_payload, synchronous=True))
81 artifacts.append(downloadable_artifact.AUTestPayload(nton_url,
82 main_staging_dir, nton_payload))
Chris Sosa781ba6d2012-04-11 12:44:43 -070083 if mton_url:
84 mton_payload = os.path.join(build_dir, AU_BASE, build + MTON_DIR_SUFFIX,
85 downloadable_artifact.ROOT_UPDATE)
86 artifacts.append(downloadable_artifact.AUTestPayload(
87 mton_url, main_staging_dir, mton_payload))
Chris Sosa47a7d4e2012-03-28 11:26:55 -070088
89 # Next we gather the miscellaneous payloads.
90 stateful_url = archive_url + '/' + downloadable_artifact.STATEFUL_UPDATE
91 autotest_url = archive_url + '/' + downloadable_artifact.AUTOTEST_PACKAGE
92 test_suites_url = (archive_url + '/' +
93 downloadable_artifact.TEST_SUITES_PACKAGE)
94
95 stateful_payload = os.path.join(build_dir,
96 downloadable_artifact.STATEFUL_UPDATE)
97
98 artifacts.append(downloadable_artifact.DownloadableArtifact(
99 stateful_url, main_staging_dir, stateful_payload, synchronous=True))
100 artifacts.append(downloadable_artifact.AutotestTarball(
101 autotest_url, main_staging_dir, build_dir))
102 artifacts.append(downloadable_artifact.Tarball(
103 test_suites_url, main_staging_dir, build_dir, synchronous=True))
104 return artifacts
Scott Zawalski51ccf9e2012-03-28 08:16:01 -0700105
106
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700107def PrepareBuildDirectory(build_dir):
108 """Preliminary staging of installation directory for build.
Scott Zawalski51ccf9e2012-03-28 08:16:01 -0700109
110 Args:
Frank Farzan37761d12011-12-01 14:29:08 -0800111 build_dir: Directory to install build components into.
112 """
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700113 if not os.path.isdir(build_dir):
114 os.path.makedirs(build_dir)
Frank Farzan37761d12011-12-01 14:29:08 -0800115
116 # Create blank chromiumos_test_image.bin. Otherwise the Dev Server will
117 # try to rebuild it unnecessarily.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700118 test_image = os.path.join(build_dir, downloadable_artifact.TEST_IMAGE)
Frank Farzan37761d12011-12-01 14:29:08 -0800119 open(test_image, 'a').close()
120
Frank Farzan37761d12011-12-01 14:29:08 -0800121
122def SafeSandboxAccess(static_dir, path):
123 """Verify that the path is in static_dir.
124
125 Args:
126 static_dir: Directory where builds are served from.
127 path: Path to verify.
128
129 Returns:
130 True if path is in static_dir, False otherwise
131 """
132 static_dir = os.path.realpath(static_dir)
133 path = os.path.realpath(path)
134 return (path.startswith(static_dir) and path != static_dir)
135
136
137def AcquireLock(static_dir, tag):
138 """Acquires a lock for a given tag.
139
140 Creates a directory for the specified tag, telling other
141 components the resource/task represented by the tag is unavailable.
142
143 Args:
144 static_dir: Directory where builds are served from.
145 tag: Unique resource/task identifier. Use '/' for nested tags.
146
147 Returns:
148 Path to the created directory or None if creation failed.
149
150 Raises:
151 DevServerUtilError: If lock can't be acquired.
152 """
153 build_dir = os.path.join(static_dir, tag)
154 if not SafeSandboxAccess(static_dir, build_dir):
Chris Sosa9164ca32012-03-28 11:04:50 -0700155 raise DevServerUtilError('Invalid tag "%s".' % tag)
Frank Farzan37761d12011-12-01 14:29:08 -0800156
157 try:
158 os.makedirs(build_dir)
159 except OSError, e:
160 if e.errno == errno.EEXIST:
161 raise DevServerUtilError(str(e))
162 else:
163 raise
164
165 return build_dir
166
167
168def ReleaseLock(static_dir, tag):
169 """Releases the lock for a given tag. Removes lock directory content.
170
171 Args:
172 static_dir: Directory where builds are served from.
173 tag: Unique resource/task identifier. Use '/' for nested tags.
174
175 Raises:
176 DevServerUtilError: If lock can't be released.
177 """
178 build_dir = os.path.join(static_dir, tag)
179 if not SafeSandboxAccess(static_dir, build_dir):
180 raise DevServerUtilError('Invaid tag "%s".' % tag)
181
182 shutil.rmtree(build_dir)
183
184
185def FindMatchingBoards(static_dir, board):
186 """Returns a list of boards given a partial board name.
187
188 Args:
189 static_dir: Directory where builds are served from.
190 board: Partial board name for this build; e.g. x86-generic.
191
192 Returns:
193 Returns a list of boards given a partial board.
194 """
195 return [brd for brd in os.listdir(static_dir) if board in brd]
196
197
198def FindMatchingBuilds(static_dir, board, build):
199 """Returns a list of matching builds given a board and partial build.
200
201 Args:
202 static_dir: Directory where builds are served from.
203 board: Partial board name for this build; e.g. x86-generic-release.
204 build: Partial build string to look for; e.g. R17-1234.
205
206 Returns:
207 Returns a list of (board, build) tuples given a partial board and build.
208 """
209 matches = []
210 for brd in FindMatchingBoards(static_dir, board):
211 a = [(brd, bld) for bld in
212 os.listdir(os.path.join(static_dir, brd)) if build in bld]
213 matches.extend(a)
214 return matches
215
216
Scott Zawalski16954532012-03-20 15:31:36 -0400217def GetLatestBuildVersion(static_dir, target, milestone=None):
Frank Farzan37761d12011-12-01 14:29:08 -0800218 """Retrieves the latest build version for a given board.
219
220 Args:
221 static_dir: Directory where builds are served from.
Scott Zawalski16954532012-03-20 15:31:36 -0400222 target: The build target, typically a combination of the board and the
223 type of build e.g. x86-mario-release.
224 milestone: For latest build set to None, for builds only in a specific
225 milestone set to a str of format Rxx (e.g. R16). Default: None.
Frank Farzan37761d12011-12-01 14:29:08 -0800226
227 Returns:
Scott Zawalski16954532012-03-20 15:31:36 -0400228 If latest found, a full build string is returned e.g. R17-1234.0.0-a1-b983.
229 If no latest is found for some reason or another a '' string is returned.
Frank Farzan37761d12011-12-01 14:29:08 -0800230
231 Raises:
Scott Zawalski16954532012-03-20 15:31:36 -0400232 DevServerUtilError: If for some reason the latest build cannot be
233 deteremined, this could be due to the dir not existing or no builds
234 being present after filtering on milestone.
Frank Farzan37761d12011-12-01 14:29:08 -0800235 """
Scott Zawalski16954532012-03-20 15:31:36 -0400236 target_path = os.path.join(static_dir, target)
237 if not os.path.isdir(target_path):
238 raise DevServerUtilError('Cannot find path %s' % target_path)
Frank Farzan37761d12011-12-01 14:29:08 -0800239
Scott Zawalski16954532012-03-20 15:31:36 -0400240 builds = [distutils.version.LooseVersion(build) for build in
241 os.listdir(target_path)]
Frank Farzan37761d12011-12-01 14:29:08 -0800242
Scott Zawalski16954532012-03-20 15:31:36 -0400243 if milestone and builds:
244 # Check if milestone Rxx is in the string representation of the build.
245 builds = filter(lambda x: milestone.upper() in str(x), builds)
Frank Farzan37761d12011-12-01 14:29:08 -0800246
Scott Zawalski16954532012-03-20 15:31:36 -0400247 if not builds:
248 raise DevServerUtilError('Could not determine build for %s' % target)
Frank Farzan37761d12011-12-01 14:29:08 -0800249
Scott Zawalski16954532012-03-20 15:31:36 -0400250 return str(max(builds))
Frank Farzan37761d12011-12-01 14:29:08 -0800251
252
253def CloneBuild(static_dir, board, build, tag, force=False):
254 """Clone an official build into the developer sandbox.
255
256 Developer sandbox directory must already exist.
257
258 Args:
259 static_dir: Directory where builds are served from.
260 board: Fully qualified board name; e.g. x86-generic-release.
261 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
262 tag: Unique resource/task identifier. Use '/' for nested tags.
263 force: Force re-creation of build_dir even if it already exists.
264
265 Returns:
266 The path to the new build.
267 """
268 # Create the developer build directory.
269 dev_static_dir = os.path.join(static_dir, DEV_BUILD_PREFIX)
270 dev_build_dir = os.path.join(dev_static_dir, tag)
271 official_build_dir = os.path.join(static_dir, board, build)
272 cherrypy.log('Cloning %s -> %s' % (official_build_dir, dev_build_dir),
273 'DEVSERVER_UTIL')
274 dev_build_exists = False
275 try:
276 AcquireLock(dev_static_dir, tag)
277 except DevServerUtilError:
278 dev_build_exists = True
279 if force:
280 dev_build_exists = False
281 ReleaseLock(dev_static_dir, tag)
282 AcquireLock(dev_static_dir, tag)
283
284 # Make a copy of the official build, only take necessary files.
285 if not dev_build_exists:
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700286 copy_list = [downloadable_artifact.TEST_IMAGE,
287 downloadable_artifact.ROOT_UPDATE,
288 downloadable_artifact.STATEFUL_UPDATE]
Frank Farzan37761d12011-12-01 14:29:08 -0800289 for f in copy_list:
290 shutil.copy(os.path.join(official_build_dir, f), dev_build_dir)
291
292 return dev_build_dir
293
Scott Zawalski84a39c92012-01-13 15:12:42 -0500294
295def GetControlFile(static_dir, build, control_path):
Frank Farzan37761d12011-12-01 14:29:08 -0800296 """Attempts to pull the requested control file from the Dev Server.
297
298 Args:
299 static_dir: Directory where builds are served from.
Frank Farzan37761d12011-12-01 14:29:08 -0800300 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
301 control_path: Path to control file on Dev Server relative to Autotest root.
302
303 Raises:
304 DevServerUtilError: If lock can't be acquired.
305
306 Returns:
307 Content of the requested control file.
308 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500309 # Be forgiving if the user passes in the control_path with a leading /
310 control_path = control_path.lstrip('/')
Scott Zawalski84a39c92012-01-13 15:12:42 -0500311 control_path = os.path.join(static_dir, build, 'autotest',
Scott Zawalski4647ce62012-01-03 17:17:28 -0500312 control_path)
Frank Farzan37761d12011-12-01 14:29:08 -0800313 if not SafeSandboxAccess(static_dir, control_path):
314 raise DevServerUtilError('Invaid control file "%s".' % control_path)
315
Scott Zawalski84a39c92012-01-13 15:12:42 -0500316 if not os.path.exists(control_path):
317 # TODO(scottz): Come up with some sort of error mechanism.
318 # crosbug.com/25040
319 return 'Unknown control path %s' % control_path
320
Frank Farzan37761d12011-12-01 14:29:08 -0800321 with open(control_path, 'r') as control_file:
322 return control_file.read()
323
324
Scott Zawalski84a39c92012-01-13 15:12:42 -0500325def GetControlFileList(static_dir, build):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500326 """List all control|control. files in the specified board/build path.
327
328 Args:
329 static_dir: Directory where builds are served from.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500330 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
331
332 Raises:
333 DevServerUtilError: If path is outside of sandbox.
334
335 Returns:
336 String of each file separated by a newline.
337 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500338 autotest_dir = os.path.join(static_dir, build, 'autotest/')
Scott Zawalski4647ce62012-01-03 17:17:28 -0500339 if not SafeSandboxAccess(static_dir, autotest_dir):
340 raise DevServerUtilError('Autotest dir not in sandbox "%s".' % autotest_dir)
341
342 control_files = set()
Scott Zawalski84a39c92012-01-13 15:12:42 -0500343 if not os.path.exists(autotest_dir):
344 # TODO(scottz): Come up with some sort of error mechanism.
345 # crosbug.com/25040
346 return 'Unknown build path %s' % autotest_dir
347
Scott Zawalski4647ce62012-01-03 17:17:28 -0500348 for entry in os.walk(autotest_dir):
349 dir_path, _, files = entry
350 for file_entry in files:
351 if file_entry.startswith('control.') or file_entry == 'control':
352 control_files.add(os.path.join(dir_path,
Chris Sosaea148d92012-03-06 16:22:04 -0800353 file_entry).replace(autotest_dir, ''))
Scott Zawalski4647ce62012-01-03 17:17:28 -0500354
355 return '\n'.join(control_files)
356
357
Frank Farzan37761d12011-12-01 14:29:08 -0800358def ListAutoupdateTargets(static_dir, board, build):
359 """Returns a list of autoupdate test targets for the given board, build.
360
361 Args:
362 static_dir: Directory where builds are served from.
363 board: Fully qualified board name; e.g. x86-generic-release.
364 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
365
366 Returns:
367 List of autoupdate test targets; e.g. ['0.14.747.0-r2bf8859c-b2927_nton']
368 """
369 return os.listdir(os.path.join(static_dir, board, build, AU_BASE))