blob: fa92a97d0f950c0dfc03a760a5ab6f24d8c41c52 [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
Scott Zawalski90afd6f2012-04-11 07:05:36 -070053 if not full_payload_url or not nton_payload_url or not mton_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
Chris Sosa47a7d4e2012-03-28 11:26:55 -070060def GatherArtifactDownloads(main_staging_dir, archive_url, build, build_dir):
61 """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
64 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 Zawalski90afd6f2012-04-11 07:05:36 -070077 mton_payload = os.path.join(build_dir, AU_BASE, build + MTON_DIR_SUFFIX,
78 downloadable_artifact.ROOT_UPDATE)
Scott Zawalski51ccf9e2012-03-28 08:16:01 -070079
Chris Sosa47a7d4e2012-03-28 11:26:55 -070080 artifacts = []
81 artifacts.append(downloadable_artifact.DownloadableArtifact(full_url,
82 main_staging_dir, full_payload, synchronous=True))
83 artifacts.append(downloadable_artifact.AUTestPayload(nton_url,
84 main_staging_dir, nton_payload))
Scott Zawalski90afd6f2012-04-11 07:05:36 -070085 artifacts.append(downloadable_artifact.AUTestPayload(mton_url,
86 main_staging_dir, mton_payload))
Chris Sosa47a7d4e2012-03-28 11:26:55 -070087
88 # Next we gather the miscellaneous payloads.
89 stateful_url = archive_url + '/' + downloadable_artifact.STATEFUL_UPDATE
90 autotest_url = archive_url + '/' + downloadable_artifact.AUTOTEST_PACKAGE
91 test_suites_url = (archive_url + '/' +
92 downloadable_artifact.TEST_SUITES_PACKAGE)
93
94 stateful_payload = os.path.join(build_dir,
95 downloadable_artifact.STATEFUL_UPDATE)
96
97 artifacts.append(downloadable_artifact.DownloadableArtifact(
98 stateful_url, main_staging_dir, stateful_payload, synchronous=True))
99 artifacts.append(downloadable_artifact.AutotestTarball(
100 autotest_url, main_staging_dir, build_dir))
101 artifacts.append(downloadable_artifact.Tarball(
102 test_suites_url, main_staging_dir, build_dir, synchronous=True))
103 return artifacts
Scott Zawalski51ccf9e2012-03-28 08:16:01 -0700104
105
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700106def PrepareBuildDirectory(build_dir):
107 """Preliminary staging of installation directory for build.
Scott Zawalski51ccf9e2012-03-28 08:16:01 -0700108
109 Args:
Frank Farzan37761d12011-12-01 14:29:08 -0800110 build_dir: Directory to install build components into.
111 """
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700112 if not os.path.isdir(build_dir):
113 os.path.makedirs(build_dir)
Frank Farzan37761d12011-12-01 14:29:08 -0800114
115 # Create blank chromiumos_test_image.bin. Otherwise the Dev Server will
116 # try to rebuild it unnecessarily.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700117 test_image = os.path.join(build_dir, downloadable_artifact.TEST_IMAGE)
Frank Farzan37761d12011-12-01 14:29:08 -0800118 open(test_image, 'a').close()
119
Frank Farzan37761d12011-12-01 14:29:08 -0800120
121def SafeSandboxAccess(static_dir, path):
122 """Verify that the path is in static_dir.
123
124 Args:
125 static_dir: Directory where builds are served from.
126 path: Path to verify.
127
128 Returns:
129 True if path is in static_dir, False otherwise
130 """
131 static_dir = os.path.realpath(static_dir)
132 path = os.path.realpath(path)
133 return (path.startswith(static_dir) and path != static_dir)
134
135
136def AcquireLock(static_dir, tag):
137 """Acquires a lock for a given tag.
138
139 Creates a directory for the specified tag, telling other
140 components the resource/task represented by the tag is unavailable.
141
142 Args:
143 static_dir: Directory where builds are served from.
144 tag: Unique resource/task identifier. Use '/' for nested tags.
145
146 Returns:
147 Path to the created directory or None if creation failed.
148
149 Raises:
150 DevServerUtilError: If lock can't be acquired.
151 """
152 build_dir = os.path.join(static_dir, tag)
153 if not SafeSandboxAccess(static_dir, build_dir):
Chris Sosa9164ca32012-03-28 11:04:50 -0700154 raise DevServerUtilError('Invalid tag "%s".' % tag)
Frank Farzan37761d12011-12-01 14:29:08 -0800155
156 try:
157 os.makedirs(build_dir)
158 except OSError, e:
159 if e.errno == errno.EEXIST:
160 raise DevServerUtilError(str(e))
161 else:
162 raise
163
164 return build_dir
165
166
167def ReleaseLock(static_dir, tag):
168 """Releases the lock for a given tag. Removes lock directory content.
169
170 Args:
171 static_dir: Directory where builds are served from.
172 tag: Unique resource/task identifier. Use '/' for nested tags.
173
174 Raises:
175 DevServerUtilError: If lock can't be released.
176 """
177 build_dir = os.path.join(static_dir, tag)
178 if not SafeSandboxAccess(static_dir, build_dir):
179 raise DevServerUtilError('Invaid tag "%s".' % tag)
180
181 shutil.rmtree(build_dir)
182
183
184def FindMatchingBoards(static_dir, board):
185 """Returns a list of boards given a partial board name.
186
187 Args:
188 static_dir: Directory where builds are served from.
189 board: Partial board name for this build; e.g. x86-generic.
190
191 Returns:
192 Returns a list of boards given a partial board.
193 """
194 return [brd for brd in os.listdir(static_dir) if board in brd]
195
196
197def FindMatchingBuilds(static_dir, board, build):
198 """Returns a list of matching builds given a board and partial build.
199
200 Args:
201 static_dir: Directory where builds are served from.
202 board: Partial board name for this build; e.g. x86-generic-release.
203 build: Partial build string to look for; e.g. R17-1234.
204
205 Returns:
206 Returns a list of (board, build) tuples given a partial board and build.
207 """
208 matches = []
209 for brd in FindMatchingBoards(static_dir, board):
210 a = [(brd, bld) for bld in
211 os.listdir(os.path.join(static_dir, brd)) if build in bld]
212 matches.extend(a)
213 return matches
214
215
Scott Zawalski16954532012-03-20 15:31:36 -0400216def GetLatestBuildVersion(static_dir, target, milestone=None):
Frank Farzan37761d12011-12-01 14:29:08 -0800217 """Retrieves the latest build version for a given board.
218
219 Args:
220 static_dir: Directory where builds are served from.
Scott Zawalski16954532012-03-20 15:31:36 -0400221 target: The build target, typically a combination of the board and the
222 type of build e.g. x86-mario-release.
223 milestone: For latest build set to None, for builds only in a specific
224 milestone set to a str of format Rxx (e.g. R16). Default: None.
Frank Farzan37761d12011-12-01 14:29:08 -0800225
226 Returns:
Scott Zawalski16954532012-03-20 15:31:36 -0400227 If latest found, a full build string is returned e.g. R17-1234.0.0-a1-b983.
228 If no latest is found for some reason or another a '' string is returned.
Frank Farzan37761d12011-12-01 14:29:08 -0800229
230 Raises:
Scott Zawalski16954532012-03-20 15:31:36 -0400231 DevServerUtilError: If for some reason the latest build cannot be
232 deteremined, this could be due to the dir not existing or no builds
233 being present after filtering on milestone.
Frank Farzan37761d12011-12-01 14:29:08 -0800234 """
Scott Zawalski16954532012-03-20 15:31:36 -0400235 target_path = os.path.join(static_dir, target)
236 if not os.path.isdir(target_path):
237 raise DevServerUtilError('Cannot find path %s' % target_path)
Frank Farzan37761d12011-12-01 14:29:08 -0800238
Scott Zawalski16954532012-03-20 15:31:36 -0400239 builds = [distutils.version.LooseVersion(build) for build in
240 os.listdir(target_path)]
Frank Farzan37761d12011-12-01 14:29:08 -0800241
Scott Zawalski16954532012-03-20 15:31:36 -0400242 if milestone and builds:
243 # Check if milestone Rxx is in the string representation of the build.
244 builds = filter(lambda x: milestone.upper() in str(x), builds)
Frank Farzan37761d12011-12-01 14:29:08 -0800245
Scott Zawalski16954532012-03-20 15:31:36 -0400246 if not builds:
247 raise DevServerUtilError('Could not determine build for %s' % target)
Frank Farzan37761d12011-12-01 14:29:08 -0800248
Scott Zawalski16954532012-03-20 15:31:36 -0400249 return str(max(builds))
Frank Farzan37761d12011-12-01 14:29:08 -0800250
251
252def CloneBuild(static_dir, board, build, tag, force=False):
253 """Clone an official build into the developer sandbox.
254
255 Developer sandbox directory must already exist.
256
257 Args:
258 static_dir: Directory where builds are served from.
259 board: Fully qualified board name; e.g. x86-generic-release.
260 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
261 tag: Unique resource/task identifier. Use '/' for nested tags.
262 force: Force re-creation of build_dir even if it already exists.
263
264 Returns:
265 The path to the new build.
266 """
267 # Create the developer build directory.
268 dev_static_dir = os.path.join(static_dir, DEV_BUILD_PREFIX)
269 dev_build_dir = os.path.join(dev_static_dir, tag)
270 official_build_dir = os.path.join(static_dir, board, build)
271 cherrypy.log('Cloning %s -> %s' % (official_build_dir, dev_build_dir),
272 'DEVSERVER_UTIL')
273 dev_build_exists = False
274 try:
275 AcquireLock(dev_static_dir, tag)
276 except DevServerUtilError:
277 dev_build_exists = True
278 if force:
279 dev_build_exists = False
280 ReleaseLock(dev_static_dir, tag)
281 AcquireLock(dev_static_dir, tag)
282
283 # Make a copy of the official build, only take necessary files.
284 if not dev_build_exists:
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700285 copy_list = [downloadable_artifact.TEST_IMAGE,
286 downloadable_artifact.ROOT_UPDATE,
287 downloadable_artifact.STATEFUL_UPDATE]
Frank Farzan37761d12011-12-01 14:29:08 -0800288 for f in copy_list:
289 shutil.copy(os.path.join(official_build_dir, f), dev_build_dir)
290
291 return dev_build_dir
292
Scott Zawalski84a39c92012-01-13 15:12:42 -0500293
294def GetControlFile(static_dir, build, control_path):
Frank Farzan37761d12011-12-01 14:29:08 -0800295 """Attempts to pull the requested control file from the Dev Server.
296
297 Args:
298 static_dir: Directory where builds are served from.
Frank Farzan37761d12011-12-01 14:29:08 -0800299 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
300 control_path: Path to control file on Dev Server relative to Autotest root.
301
302 Raises:
303 DevServerUtilError: If lock can't be acquired.
304
305 Returns:
306 Content of the requested control file.
307 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500308 # Be forgiving if the user passes in the control_path with a leading /
309 control_path = control_path.lstrip('/')
Scott Zawalski84a39c92012-01-13 15:12:42 -0500310 control_path = os.path.join(static_dir, build, 'autotest',
Scott Zawalski4647ce62012-01-03 17:17:28 -0500311 control_path)
Frank Farzan37761d12011-12-01 14:29:08 -0800312 if not SafeSandboxAccess(static_dir, control_path):
313 raise DevServerUtilError('Invaid control file "%s".' % control_path)
314
Scott Zawalski84a39c92012-01-13 15:12:42 -0500315 if not os.path.exists(control_path):
316 # TODO(scottz): Come up with some sort of error mechanism.
317 # crosbug.com/25040
318 return 'Unknown control path %s' % control_path
319
Frank Farzan37761d12011-12-01 14:29:08 -0800320 with open(control_path, 'r') as control_file:
321 return control_file.read()
322
323
Scott Zawalski84a39c92012-01-13 15:12:42 -0500324def GetControlFileList(static_dir, build):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500325 """List all control|control. files in the specified board/build path.
326
327 Args:
328 static_dir: Directory where builds are served from.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500329 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
330
331 Raises:
332 DevServerUtilError: If path is outside of sandbox.
333
334 Returns:
335 String of each file separated by a newline.
336 """
Scott Zawalski1572d152012-01-16 14:36:02 -0500337 autotest_dir = os.path.join(static_dir, build, 'autotest/')
Scott Zawalski4647ce62012-01-03 17:17:28 -0500338 if not SafeSandboxAccess(static_dir, autotest_dir):
339 raise DevServerUtilError('Autotest dir not in sandbox "%s".' % autotest_dir)
340
341 control_files = set()
Scott Zawalski84a39c92012-01-13 15:12:42 -0500342 if not os.path.exists(autotest_dir):
343 # TODO(scottz): Come up with some sort of error mechanism.
344 # crosbug.com/25040
345 return 'Unknown build path %s' % autotest_dir
346
Scott Zawalski4647ce62012-01-03 17:17:28 -0500347 for entry in os.walk(autotest_dir):
348 dir_path, _, files = entry
349 for file_entry in files:
350 if file_entry.startswith('control.') or file_entry == 'control':
351 control_files.add(os.path.join(dir_path,
Chris Sosaea148d92012-03-06 16:22:04 -0800352 file_entry).replace(autotest_dir, ''))
Scott Zawalski4647ce62012-01-03 17:17:28 -0500353
354 return '\n'.join(control_files)
355
356
Frank Farzan37761d12011-12-01 14:29:08 -0800357def ListAutoupdateTargets(static_dir, board, build):
358 """Returns a list of autoupdate test targets for the given board, build.
359
360 Args:
361 static_dir: Directory where builds are served from.
362 board: Fully qualified board name; e.g. x86-generic-release.
363 build: Fully qualified build string; e.g. R17-1234.0.0-a1-b983.
364
365 Returns:
366 List of autoupdate test targets; e.g. ['0.14.747.0-r2bf8859c-b2927_nton']
367 """
368 return os.listdir(os.path.join(static_dir, board, build, AU_BASE))