blob: 4d43893299ce2af2a08a895f0f285f51a8f95067 [file] [log] [blame]
Chris Sosa47a7d4e2012-03-28 11:26:55 -07001# Copyright (c) 2012 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"""Module containing classes that wrap artifact downloads."""
6
Chris Sosa47a7d4e2012-03-28 11:26:55 -07007import os
8import shutil
9import subprocess
10
Chris Sosa76e44b92013-01-31 12:11:38 -080011import artifact_info
12import common_util
joychen3cb228e2013-06-12 12:13:13 -070013import devserver_constants
Chris Sosa47a7d4e2012-03-28 11:26:55 -070014import gsutil_util
Gilad Arnoldc65330c2012-09-20 15:17:48 -070015import log_util
Chris Sosa47a7d4e2012-03-28 11:26:55 -070016
17
Chris Sosa76e44b92013-01-31 12:11:38 -080018_AU_BASE = 'au'
19_NTON_DIR_SUFFIX = '_nton'
20_MTON_DIR_SUFFIX = '_mton'
21
22############ Actual filenames of artifacts in Google Storage ############
23
24AU_SUITE_FILE = 'au_control.tar.bz2'
25AUTOTEST_FILE = 'autotest.tar'
26AUTOTEST_COMPRESSED_FILE = 'autotest.tar.bz2'
27DEBUG_SYMBOLS_FILE = 'debug.tgz'
28FIRMWARE_FILE = 'firmware_from_source.tar.bz2'
29IMAGE_FILE = 'image.zip'
Chris Sosa76e44b92013-01-31 12:11:38 -080030STATEFUL_UPDATE_FILE = 'stateful.tgz'
Chris Sosa76e44b92013-01-31 12:11:38 -080031TEST_SUITES_FILE = 'test_suites.tar.bz2'
32
33_build_artifact_locks = common_util.LockDict()
Chris Sosa47a7d4e2012-03-28 11:26:55 -070034
35
36class ArtifactDownloadError(Exception):
37 """Error used to signify an issue processing an artifact."""
38 pass
39
40
Gilad Arnoldc65330c2012-09-20 15:17:48 -070041class BuildArtifact(log_util.Loggable):
Chris Sosa47a7d4e2012-03-28 11:26:55 -070042 """Wrapper around an artifact to download from gsutil.
43
44 The purpose of this class is to download objects from Google Storage
45 and install them to a local directory. There are two main functions, one to
46 download/prepare the artifacts in to a temporary staging area and the second
47 to stage it into its final destination.
Chris Sosa76e44b92013-01-31 12:11:38 -080048
49 Class members:
50 archive_url = archive_url
51 name: Name given for artifact -- either a regexp or name of the artifact in
52 gs. If a regexp, is modified to actual name before call to _Download.
53 build: The version of the build i.e. R26-2342.0.0.
54 marker_name: Name used to define the lock marker for the artifacts to
55 prevent it from being re-downloaded. By default based on name
56 but can be overriden by children.
joychen0a8e34e2013-06-24 17:58:36 -070057 install_path: Path to artifact.
Chris Sosa76e44b92013-01-31 12:11:38 -080058 install_dir: The final location where the artifact should be staged to.
59 single_name: If True the name given should only match one item. Note, if not
60 True, self.name will become a list of items returned.
Chris Sosa47a7d4e2012-03-28 11:26:55 -070061 """
Chris Sosa76e44b92013-01-31 12:11:38 -080062 def __init__(self, install_dir, archive_url, name, build):
Chris Sosa47a7d4e2012-03-28 11:26:55 -070063 """Args:
Chris Sosa76e44b92013-01-31 12:11:38 -080064 install_dir: Where to install the artifact.
65 archive_url: The Google Storage path to find the artifact.
66 name: Identifying name to be used to find/store the artifact.
67 build: The name of the build e.g. board/release.
Chris Sosa47a7d4e2012-03-28 11:26:55 -070068 """
Chris Sosa6a3697f2013-01-29 16:44:43 -080069 super(BuildArtifact, self).__init__()
Chris Sosa47a7d4e2012-03-28 11:26:55 -070070
Chris Sosa76e44b92013-01-31 12:11:38 -080071 # In-memory lock to keep the devserver from colliding with itself while
72 # attempting to stage the same artifact.
73 self._process_lock = None
Chris Sosa47a7d4e2012-03-28 11:26:55 -070074
Chris Sosa76e44b92013-01-31 12:11:38 -080075 self.archive_url = archive_url
76 self.name = name
77 self.build = build
Chris Sosa47a7d4e2012-03-28 11:26:55 -070078
Chris Sosa76e44b92013-01-31 12:11:38 -080079 self.marker_name = '.' + self._SanitizeName(name)
Chris Sosa47a7d4e2012-03-28 11:26:55 -070080
joychen0a8e34e2013-06-24 17:58:36 -070081 self.install_path = None
Chris Sosa47a7d4e2012-03-28 11:26:55 -070082
Chris Sosa76e44b92013-01-31 12:11:38 -080083 self.install_dir = install_dir
84
85 self.single_name = True
86
87 @staticmethod
88 def _SanitizeName(name):
89 """Sanitizes name to be used for creating a file on the filesystem.
90
91 '.','/' and '*' have special meaning in FS lingo. Replace them with words.
92 """
93 return name.replace('*', 'STAR').replace('.', 'DOT').replace('/', 'SLASH')
94
95 def _ArtifactStaged(self):
96 """Returns True if artifact is already staged."""
97 return os.path.exists(os.path.join(self.install_dir, self.marker_name))
98
99 def _MarkArtifactStaged(self):
100 """Marks the artifact as staged."""
101 with open(os.path.join(self.install_dir, self.marker_name), 'w') as f:
102 f.write('')
103
104 def _WaitForArtifactToExist(self, timeout):
105 """Waits for artifact to exist and sets self.name to appropriate name."""
106 names = gsutil_util.GetGSNamesWithWait(
107 self.name, self.archive_url, str(self), single_item=self.single_name,
108 timeout=timeout)
109 if not names:
110 raise ArtifactDownloadError('Could not find %s in Google Storage' %
111 self.name)
112
113 if self.single_name:
114 if len(names) > 1:
115 raise ArtifactDownloadError('Too many artifacts match %s' % self.name)
116
117 self.name = names[0]
118 else:
119 self.name = names
120
121 def _Download(self):
joychen0a8e34e2013-06-24 17:58:36 -0700122 """Downloads artifact from Google Storage to a local directory."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800123 gs_path = '/'.join([self.archive_url, self.name])
joychen0a8e34e2013-06-24 17:58:36 -0700124 self.install_path = os.path.join(self.install_dir, self.name)
125 gsutil_util.DownloadFromGS(gs_path, self.install_path)
Chris Sosa76e44b92013-01-31 12:11:38 -0800126
joychen0a8e34e2013-06-24 17:58:36 -0700127 def _Setup(self):
128 """For tarball like artifacts, extracts and prepares contents."""
129 pass
130
Chris Sosa76e44b92013-01-31 12:11:38 -0800131
132 def Process(self, no_wait):
133 """Main call point to all artifacts. Downloads and Stages artifact.
134
135 Downloads and Stages artifact from Google Storage to the install directory
136 specified in the constructor. It multi-thread safe and does not overwrite
137 the artifact if it's already been downloaded or being downloaded. After
138 processing, leaves behind a marker to indicate to future invocations that
139 the artifact has already been staged based on the name of the artifact.
140
141 Do not override as it modifies important private variables, ensures thread
142 safety, and maintains cache semantics.
143
144 Note: this may be a blocking call when the artifact is already in the
145 process of being staged.
146
147 Args:
148 no_wait: If True, don't block waiting for artifact to exist if we fail to
149 immediately find it.
150
151 Raises:
152 ArtifactDownloadError: If the artifact fails to download from Google
153 Storage for any reason or that the regexp
154 defined by name is not specific enough.
155 """
156 if not self._process_lock:
157 self._process_lock = _build_artifact_locks.lock(
158 os.path.join(self.install_dir, self.name))
159
160 with self._process_lock:
161 common_util.MkDirP(self.install_dir)
162 if not self._ArtifactStaged():
163 # If the artifact should already have been uploaded, don't waste
164 # cycles waiting around for it to exist.
165 timeout = 1 if no_wait else 10
166 self._WaitForArtifactToExist(timeout)
167 self._Download()
joychen0a8e34e2013-06-24 17:58:36 -0700168 self._Setup()
Chris Sosa76e44b92013-01-31 12:11:38 -0800169 self._MarkArtifactStaged()
170 else:
171 self._Log('%s is already staged.', self)
172
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700173 def __str__(self):
174 """String representation for the download."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800175 return '->'.join(['%s/%s' % (self.archive_url, self.name),
joychen0a8e34e2013-06-24 17:58:36 -0700176 self.install_dir])
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700177
178
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700179class AUTestPayloadBuildArtifact(BuildArtifact):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700180 """Wrapper for AUTest delta payloads which need additional setup."""
joychen0a8e34e2013-06-24 17:58:36 -0700181 def _Setup(self):
182 super(AUTestPayloadBuildArtifact, self)._Setup()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700183
Chris Sosa76e44b92013-01-31 12:11:38 -0800184 # Rename to update.gz.
185 install_path = os.path.join(self.install_dir, self.name)
joychen3cb228e2013-06-12 12:13:13 -0700186 new_install_path = os.path.join(self.install_dir,
187 devserver_constants.ROOT_UPDATE_FILE)
Chris Sosa76e44b92013-01-31 12:11:38 -0800188 shutil.move(install_path, new_install_path)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700189
190
Chris Sosa76e44b92013-01-31 12:11:38 -0800191# TODO(sosa): Change callers to make this artifact more sane.
192class DeltaPayloadsArtifact(BuildArtifact):
193 """Delta payloads from the archive_url.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700194
Chris Sosa76e44b92013-01-31 12:11:38 -0800195 This artifact is super strange. It custom handles directories and
196 pulls in all delta payloads. We can't specify exactly what we want
197 because unlike other artifacts, this one does not conform to something a
198 client might know. The client doesn't know the version of n-1 or whether it
199 was even generated.
200 """
201 def __init__(self, *args):
202 super(DeltaPayloadsArtifact, self).__init__(*args)
203 self.single_name = False # Expect multiple deltas
204 nton_name = 'chromeos_%s%s' % (self.build, self.name)
205 mton_name = 'chromeos_(?!%s)%s' % (self.build, self.name)
206 nton_install_dir = os.path.join(self.install_dir, _AU_BASE,
207 self.build + _NTON_DIR_SUFFIX)
208 mton_install_dir = os.path.join(self.install_dir, _AU_BASE,
209 self.build + _MTON_DIR_SUFFIX)
210 self._sub_artifacts = [
211 AUTestPayloadBuildArtifact(mton_install_dir, self.archive_url,
212 mton_name, self.build),
213 AUTestPayloadBuildArtifact(nton_install_dir, self.archive_url,
214 nton_name, self.build)]
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700215
Chris Sosa76e44b92013-01-31 12:11:38 -0800216 def _Download(self):
joychen0a8e34e2013-06-24 17:58:36 -0700217 """With sub-artifacts we do everything in _Setup()."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800218 pass
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700219
joychen0a8e34e2013-06-24 17:58:36 -0700220 def _Setup(self):
Chris Sosa76e44b92013-01-31 12:11:38 -0800221 """Process each sub-artifact. Only error out if none can be found."""
222 for artifact in self._sub_artifacts:
223 try:
224 artifact.Process(no_wait=True)
225 # Setup symlink so that AU will work for this payload.
226 os.symlink(
227 os.path.join(os.pardir, os.pardir, STATEFUL_UPDATE_FILE),
228 os.path.join(artifact.install_dir, STATEFUL_UPDATE_FILE))
229 except ArtifactDownloadError as e:
230 self._Log('Could not process %s: %s', artifact, e)
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700231
Chris Sosa76e44b92013-01-31 12:11:38 -0800232
233class BundledBuildArtifact(BuildArtifact):
234 """A single build artifact bundle e.g. zip file or tar file."""
235 def __init__(self, install_dir, archive_url, name, build,
236 files_to_extract=None, exclude=None):
237 """Takes BuildArtifacts are with two additional args.
238
239 Additional args:
240 files_to_extract: A list of files to extract. If set to None, extract
241 all files.
242 exclude: A list of files to exclude. If None, no files are excluded.
243 """
244 super(BundledBuildArtifact, self).__init__(install_dir, archive_url, name,
245 build)
246 self._files_to_extract = files_to_extract
247 self._exclude = exclude
248
249 # We modify the marker so that it is unique to what was staged.
250 if files_to_extract:
251 self.marker_name = self._SanitizeName(
252 '_'.join(['.' + self.name] + files_to_extract))
253
254 def _Extract(self):
255 """Extracts the bundle into install_dir. Must be overridden.
256
257 If set, uses files_to_extract to only extract those items. If set, use
258 exclude to exclude specific files.
259 """
260 raise NotImplementedError()
261
joychen0a8e34e2013-06-24 17:58:36 -0700262 def _Setup(self):
Chris Sosa76e44b92013-01-31 12:11:38 -0800263 self._Extract()
264
265
266class TarballBuildArtifact(BundledBuildArtifact):
267 """Artifact for tar and tarball files."""
268
269 def _Extract(self):
270 """Extracts a tarball using tar.
271
272 Detects whether the tarball is compressed or not based on the file
273 extension and extracts the tarball into the install_path.
274 """
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700275 try:
joychen0a8e34e2013-06-24 17:58:36 -0700276 common_util.ExtractTarball(self.install_path, self.install_dir,
Simran Basi4baad082013-02-14 13:39:18 -0800277 files_to_extract=self._files_to_extract,
278 excluded_files=self._exclude)
279 except common_util.CommonUtilError as e:
280 raise ArtifactDownloadError(str(e))
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700281
282
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700283class AutotestTarballBuildArtifact(TarballBuildArtifact):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700284 """Wrapper around the autotest tarball to download from gsutil."""
285
joychen0a8e34e2013-06-24 17:58:36 -0700286 def _Setup(self):
Chris Sosa76e44b92013-01-31 12:11:38 -0800287 """Extracts the tarball into the install path excluding test suites."""
joychen0a8e34e2013-06-24 17:58:36 -0700288 super(AutotestTarballBuildArtifact, self)._Setup()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700289
Chris Sosa76e44b92013-01-31 12:11:38 -0800290 # Deal with older autotest packages that may not be bundled.
joychen3cb228e2013-06-12 12:13:13 -0700291 autotest_dir = os.path.join(self.install_dir,
292 devserver_constants.AUTOTEST_DIR)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700293 autotest_pkgs_dir = os.path.join(autotest_dir, 'packages')
294 if not os.path.exists(autotest_pkgs_dir):
295 os.makedirs(autotest_pkgs_dir)
296
297 if not os.path.exists(os.path.join(autotest_pkgs_dir, 'packages.checksum')):
Chris Sosa76e44b92013-01-31 12:11:38 -0800298 cmd = ['autotest/utils/packager.py', 'upload', '--repository',
299 autotest_pkgs_dir, '--all']
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700300 try:
joychen0a8e34e2013-06-24 17:58:36 -0700301 subprocess.check_call(cmd, cwd=self.install_dir)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700302 except subprocess.CalledProcessError, e:
Chris Sosa76e44b92013-01-31 12:11:38 -0800303 raise ArtifactDownloadError(
304 'Failed to create autotest packages!:\n%s' % e)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700305 else:
Gilad Arnoldf5843132012-09-25 00:31:20 -0700306 self._Log('Using pre-generated packages from autotest')
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700307
Chris Masone816e38c2012-05-02 12:22:36 -0700308
Chris Sosa76e44b92013-01-31 12:11:38 -0800309class ZipfileBuildArtifact(BundledBuildArtifact):
310 """A downloadable artifact that is a zipfile."""
Chris Masone816e38c2012-05-02 12:22:36 -0700311
Chris Sosa76e44b92013-01-31 12:11:38 -0800312 def _Extract(self):
313 """Extracts files into the install path."""
314 # Unzip is weird. It expects its args before any excepts and expects its
315 # excepts in a list following the -x.
joychen0a8e34e2013-06-24 17:58:36 -0700316 cmd = ['unzip', '-o', self.install_path, '-d', self.install_dir]
Chris Sosa76e44b92013-01-31 12:11:38 -0800317 if self._files_to_extract:
318 cmd.extend(self._files_to_extract)
Chris Masone816e38c2012-05-02 12:22:36 -0700319
Chris Sosa76e44b92013-01-31 12:11:38 -0800320 if self._exclude:
321 cmd.append('-x')
322 cmd.extend(self._exclude)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700323
324 try:
Chris Sosa76e44b92013-01-31 12:11:38 -0800325 subprocess.check_call(cmd)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700326 except subprocess.CalledProcessError, e:
Chris Sosa76e44b92013-01-31 12:11:38 -0800327 raise ArtifactDownloadError(
328 'An error occurred when attempting to unzip %s:\n%s' %
joychen0a8e34e2013-06-24 17:58:36 -0700329 (self.install_path, e))
Gilad Arnold6f99b982012-09-12 10:49:40 -0700330
Gilad Arnold6f99b982012-09-12 10:49:40 -0700331
Chris Sosa76e44b92013-01-31 12:11:38 -0800332class ImplDescription(object):
333 """Data wrapper that describes an artifact's implementation."""
334 def __init__(self, artifact_class, name, *additional_args):
335 """Constructor:
336
337 Args:
338 artifact_class: BuildArtifact class to use for the artifact.
339 name: name to use to identify artifact (see BuildArtifact.name)
340 additional_args: If sub-class uses additional args, these are passed
341 through to them.
342 """
343 self.artifact_class = artifact_class
344 self.name = name
345 self.additional_args = additional_args
346
347
348# Maps artifact names to their implementation description.
349# Please note, it is good practice to use constants for these names if you're
350# going to re-use the names ANYWHERE else in the devserver code.
351ARTIFACT_IMPLEMENTATION_MAP = {
352 artifact_info.FULL_PAYLOAD:
353 ImplDescription(AUTestPayloadBuildArtifact, '.*_full_.*'),
354 artifact_info.DELTA_PAYLOADS:
355 ImplDescription(DeltaPayloadsArtifact, '.*_delta_.*'),
356 artifact_info.STATEFUL_PAYLOAD:
357 ImplDescription(BuildArtifact, STATEFUL_UPDATE_FILE),
358
359 artifact_info.BASE_IMAGE:
360 ImplDescription(ZipfileBuildArtifact, IMAGE_FILE,
joychen921e1fb2013-06-28 11:12:20 -0700361 [devserver_constants.BASE_IMAGE_FILE]),
Chris Sosa76e44b92013-01-31 12:11:38 -0800362 artifact_info.RECOVERY_IMAGE:
joychen921e1fb2013-06-28 11:12:20 -0700363 ImplDescription(ZipfileBuildArtifact, IMAGE_FILE,
364 [devserver_constants.RECOVERY_IMAGE_FILE]),
Chris Sosa76e44b92013-01-31 12:11:38 -0800365 artifact_info.TEST_IMAGE:
joychen921e1fb2013-06-28 11:12:20 -0700366 ImplDescription(ZipfileBuildArtifact, IMAGE_FILE,
367 [devserver_constants.TEST_IMAGE_FILE]),
Chris Sosa76e44b92013-01-31 12:11:38 -0800368
369 artifact_info.AUTOTEST:
370 ImplDescription(AutotestTarballBuildArtifact, AUTOTEST_FILE, None,
371 ['autotest/test_suites']),
372 artifact_info.TEST_SUITES:
373 ImplDescription(TarballBuildArtifact, TEST_SUITES_FILE),
374 artifact_info.AU_SUITE:
Chris Sosaa56c4032013-03-17 21:59:54 -0700375 ImplDescription(TarballBuildArtifact, AU_SUITE_FILE),
Chris Sosa76e44b92013-01-31 12:11:38 -0800376
377 artifact_info.FIRMWARE:
378 ImplDescription(BuildArtifact, FIRMWARE_FILE),
379 artifact_info.SYMBOLS:
380 ImplDescription(TarballBuildArtifact, DEBUG_SYMBOLS_FILE,
381 ['debug/breakpad']),
382}
383
384
385class ArtifactFactory(object):
386 """A factory class that generates build artifacts from artifact names."""
387
joychen0a8e34e2013-06-24 17:58:36 -0700388 def __init__(self, download_dir, archive_url, artifact_names, build):
Chris Sosa76e44b92013-01-31 12:11:38 -0800389 """Initalizes the member variables for the factory.
390
391 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -0800392 archive_url: the Google Storage url of the bucket where the debug
393 symbols for the desired build are stored.
394 artifact_names: List of artifact names to stage.
395 build: The name of the build.
396 """
joychen0a8e34e2013-06-24 17:58:36 -0700397 self.download_dir = download_dir
Chris Sosa76e44b92013-01-31 12:11:38 -0800398 self.archive_url = archive_url
399 self.artifact_names = artifact_names
400 self.build = build
401
402 @staticmethod
403 def _GetDescriptionComponents(artifact_name):
404 """Returns a tuple of for BuildArtifact class, name, and additional args."""
405 description = ARTIFACT_IMPLEMENTATION_MAP[artifact_name]
406 return (description.artifact_class, description.name,
407 description.additional_args)
408
409 def _Artifacts(self, artifact_names):
410 """Returns an iterable of BuildArtifacts from |artifact_names|."""
411 artifacts = []
412 for artifact_name in artifact_names:
413 artifact_class, path, args = self._GetDescriptionComponents(
414 artifact_name)
joychen0a8e34e2013-06-24 17:58:36 -0700415 artifacts.append(artifact_class(self.download_dir, self.archive_url, path,
Chris Sosa76e44b92013-01-31 12:11:38 -0800416 self.build, *args))
417
418 return artifacts
419
420 def RequiredArtifacts(self):
421 """Returns an iterable of BuildArtifacts for the factory's artifacts."""
422 return self._Artifacts(self.artifact_names)
423
424 def OptionalArtifacts(self):
425 """Returns an iterable of BuildArtifacts that should be cached."""
426 optional_names = set()
427 for artifact_name, optional_list in (
428 artifact_info.REQUESTED_TO_OPTIONAL_MAP.iteritems()):
429 # We are already downloading it.
430 if artifact_name in self.artifact_names:
431 optional_names = optional_names.union(optional_list)
432
433 return self._Artifacts(optional_names - set(self.artifact_names))