blob: 19120e7306c0291b655c04739a23bf1dd0c9e21a [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
Chris Sosa76e44b92013-01-31 12:11:38 -080010import tempfile
Chris Sosa47a7d4e2012-03-28 11:26:55 -070011
Chris Sosa76e44b92013-01-31 12:11:38 -080012import artifact_info
13import common_util
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'
30ROOT_UPDATE_FILE = 'update.gz'
31STATEFUL_UPDATE_FILE = 'stateful.tgz'
32TEST_IMAGE_FILE = 'chromiumos_test_image.bin'
33TEST_SUITES_FILE = 'test_suites.tar.bz2'
34
35_build_artifact_locks = common_util.LockDict()
Chris Sosa47a7d4e2012-03-28 11:26:55 -070036
37
38class ArtifactDownloadError(Exception):
39 """Error used to signify an issue processing an artifact."""
40 pass
41
42
Gilad Arnoldc65330c2012-09-20 15:17:48 -070043class BuildArtifact(log_util.Loggable):
Chris Sosa47a7d4e2012-03-28 11:26:55 -070044 """Wrapper around an artifact to download from gsutil.
45
46 The purpose of this class is to download objects from Google Storage
47 and install them to a local directory. There are two main functions, one to
48 download/prepare the artifacts in to a temporary staging area and the second
49 to stage it into its final destination.
Chris Sosa76e44b92013-01-31 12:11:38 -080050
51 Class members:
52 archive_url = archive_url
53 name: Name given for artifact -- either a regexp or name of the artifact in
54 gs. If a regexp, is modified to actual name before call to _Download.
55 build: The version of the build i.e. R26-2342.0.0.
56 marker_name: Name used to define the lock marker for the artifacts to
57 prevent it from being re-downloaded. By default based on name
58 but can be overriden by children.
59 staging_dir: directory for the artifact reserved for staging. Cleaned
60 up after staging.
61 tmp_stage_path: Path used in staging_dir for placing artifact.
62 install_dir: The final location where the artifact should be staged to.
63 single_name: If True the name given should only match one item. Note, if not
64 True, self.name will become a list of items returned.
Chris Sosa47a7d4e2012-03-28 11:26:55 -070065 """
Chris Sosa76e44b92013-01-31 12:11:38 -080066 def __init__(self, install_dir, archive_url, name, build):
Chris Sosa47a7d4e2012-03-28 11:26:55 -070067 """Args:
Chris Sosa76e44b92013-01-31 12:11:38 -080068 install_dir: Where to install the artifact.
69 archive_url: The Google Storage path to find the artifact.
70 name: Identifying name to be used to find/store the artifact.
71 build: The name of the build e.g. board/release.
Chris Sosa47a7d4e2012-03-28 11:26:55 -070072 """
Chris Sosa6a3697f2013-01-29 16:44:43 -080073 super(BuildArtifact, self).__init__()
Chris Sosa47a7d4e2012-03-28 11:26:55 -070074
Chris Sosa76e44b92013-01-31 12:11:38 -080075 # In-memory lock to keep the devserver from colliding with itself while
76 # attempting to stage the same artifact.
77 self._process_lock = None
Chris Sosa47a7d4e2012-03-28 11:26:55 -070078
Chris Sosa76e44b92013-01-31 12:11:38 -080079 self.archive_url = archive_url
80 self.name = name
81 self.build = build
Chris Sosa47a7d4e2012-03-28 11:26:55 -070082
Chris Sosa76e44b92013-01-31 12:11:38 -080083 self.marker_name = '.' + self._SanitizeName(name)
Chris Sosa47a7d4e2012-03-28 11:26:55 -070084
Chris Sosa76e44b92013-01-31 12:11:38 -080085 self.staging_dir = tempfile.mkdtemp(prefix='Devserver%s' % (
86 type(self).__name__))
87 self.tmp_stage_path = None
Chris Sosa47a7d4e2012-03-28 11:26:55 -070088
Chris Sosa76e44b92013-01-31 12:11:38 -080089 self.install_dir = install_dir
90
91 self.single_name = True
92
93 @staticmethod
94 def _SanitizeName(name):
95 """Sanitizes name to be used for creating a file on the filesystem.
96
97 '.','/' and '*' have special meaning in FS lingo. Replace them with words.
98 """
99 return name.replace('*', 'STAR').replace('.', 'DOT').replace('/', 'SLASH')
100
101 def _ArtifactStaged(self):
102 """Returns True if artifact is already staged."""
103 return os.path.exists(os.path.join(self.install_dir, self.marker_name))
104
105 def _MarkArtifactStaged(self):
106 """Marks the artifact as staged."""
107 with open(os.path.join(self.install_dir, self.marker_name), 'w') as f:
108 f.write('')
109
110 def _WaitForArtifactToExist(self, timeout):
111 """Waits for artifact to exist and sets self.name to appropriate name."""
112 names = gsutil_util.GetGSNamesWithWait(
113 self.name, self.archive_url, str(self), single_item=self.single_name,
114 timeout=timeout)
115 if not names:
116 raise ArtifactDownloadError('Could not find %s in Google Storage' %
117 self.name)
118
119 if self.single_name:
120 if len(names) > 1:
121 raise ArtifactDownloadError('Too many artifacts match %s' % self.name)
122
123 self.name = names[0]
124 else:
125 self.name = names
126
127 def _Download(self):
128 """Downloads artifact from Google Storage to a local staging directory."""
129 self.tmp_stage_path = os.path.join(self.staging_dir, self.name)
130 gs_path = '/'.join([self.archive_url, self.name])
131 gsutil_util.DownloadFromGS(gs_path, self.tmp_stage_path)
132
133 def _Stage(self):
134 """Stages the artifact from the tmp directory to the final path."""
135 install_path = os.path.join(self.install_dir, self.name)
136 shutil.move(self.tmp_stage_path, install_path)
137
138 def Process(self, no_wait):
139 """Main call point to all artifacts. Downloads and Stages artifact.
140
141 Downloads and Stages artifact from Google Storage to the install directory
142 specified in the constructor. It multi-thread safe and does not overwrite
143 the artifact if it's already been downloaded or being downloaded. After
144 processing, leaves behind a marker to indicate to future invocations that
145 the artifact has already been staged based on the name of the artifact.
146
147 Do not override as it modifies important private variables, ensures thread
148 safety, and maintains cache semantics.
149
150 Note: this may be a blocking call when the artifact is already in the
151 process of being staged.
152
153 Args:
154 no_wait: If True, don't block waiting for artifact to exist if we fail to
155 immediately find it.
156
157 Raises:
158 ArtifactDownloadError: If the artifact fails to download from Google
159 Storage for any reason or that the regexp
160 defined by name is not specific enough.
161 """
162 if not self._process_lock:
163 self._process_lock = _build_artifact_locks.lock(
164 os.path.join(self.install_dir, self.name))
165
166 with self._process_lock:
167 common_util.MkDirP(self.install_dir)
168 if not self._ArtifactStaged():
169 # If the artifact should already have been uploaded, don't waste
170 # cycles waiting around for it to exist.
171 timeout = 1 if no_wait else 10
172 self._WaitForArtifactToExist(timeout)
173 self._Download()
174 self._Stage()
175 self._MarkArtifactStaged()
176 else:
177 self._Log('%s is already staged.', self)
178
179 def __del__(self):
180 shutil.rmtree(self.staging_dir)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700181
182 def __str__(self):
183 """String representation for the download."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800184 return '->'.join(['%s/%s' % (self.archive_url, self.name),
185 self.staging_dir, self.install_dir])
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700186
187
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700188class AUTestPayloadBuildArtifact(BuildArtifact):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700189 """Wrapper for AUTest delta payloads which need additional setup."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800190 def _Stage(self):
191 super(AUTestPayloadBuildArtifact, self)._Stage()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700192
Chris Sosa76e44b92013-01-31 12:11:38 -0800193 # Rename to update.gz.
194 install_path = os.path.join(self.install_dir, self.name)
195 new_install_path = os.path.join(self.install_dir, ROOT_UPDATE_FILE)
196 shutil.move(install_path, new_install_path)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700197
198
Chris Sosa76e44b92013-01-31 12:11:38 -0800199# TODO(sosa): Change callers to make this artifact more sane.
200class DeltaPayloadsArtifact(BuildArtifact):
201 """Delta payloads from the archive_url.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700202
Chris Sosa76e44b92013-01-31 12:11:38 -0800203 This artifact is super strange. It custom handles directories and
204 pulls in all delta payloads. We can't specify exactly what we want
205 because unlike other artifacts, this one does not conform to something a
206 client might know. The client doesn't know the version of n-1 or whether it
207 was even generated.
208 """
209 def __init__(self, *args):
210 super(DeltaPayloadsArtifact, self).__init__(*args)
211 self.single_name = False # Expect multiple deltas
212 nton_name = 'chromeos_%s%s' % (self.build, self.name)
213 mton_name = 'chromeos_(?!%s)%s' % (self.build, self.name)
214 nton_install_dir = os.path.join(self.install_dir, _AU_BASE,
215 self.build + _NTON_DIR_SUFFIX)
216 mton_install_dir = os.path.join(self.install_dir, _AU_BASE,
217 self.build + _MTON_DIR_SUFFIX)
218 self._sub_artifacts = [
219 AUTestPayloadBuildArtifact(mton_install_dir, self.archive_url,
220 mton_name, self.build),
221 AUTestPayloadBuildArtifact(nton_install_dir, self.archive_url,
222 nton_name, self.build)]
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700223
Chris Sosa76e44b92013-01-31 12:11:38 -0800224 def _Download(self):
225 """With sub-artifacts we do everything in _Stage()."""
226 pass
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700227
Chris Sosa76e44b92013-01-31 12:11:38 -0800228 def _Stage(self):
229 """Process each sub-artifact. Only error out if none can be found."""
230 for artifact in self._sub_artifacts:
231 try:
232 artifact.Process(no_wait=True)
233 # Setup symlink so that AU will work for this payload.
234 os.symlink(
235 os.path.join(os.pardir, os.pardir, STATEFUL_UPDATE_FILE),
236 os.path.join(artifact.install_dir, STATEFUL_UPDATE_FILE))
237 except ArtifactDownloadError as e:
238 self._Log('Could not process %s: %s', artifact, e)
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700239
Chris Sosa76e44b92013-01-31 12:11:38 -0800240
241class BundledBuildArtifact(BuildArtifact):
242 """A single build artifact bundle e.g. zip file or tar file."""
243 def __init__(self, install_dir, archive_url, name, build,
244 files_to_extract=None, exclude=None):
245 """Takes BuildArtifacts are with two additional args.
246
247 Additional args:
248 files_to_extract: A list of files to extract. If set to None, extract
249 all files.
250 exclude: A list of files to exclude. If None, no files are excluded.
251 """
252 super(BundledBuildArtifact, self).__init__(install_dir, archive_url, name,
253 build)
254 self._files_to_extract = files_to_extract
255 self._exclude = exclude
256
257 # We modify the marker so that it is unique to what was staged.
258 if files_to_extract:
259 self.marker_name = self._SanitizeName(
260 '_'.join(['.' + self.name] + files_to_extract))
261
262 def _Extract(self):
263 """Extracts the bundle into install_dir. Must be overridden.
264
265 If set, uses files_to_extract to only extract those items. If set, use
266 exclude to exclude specific files.
267 """
268 raise NotImplementedError()
269
270 def _Stage(self):
271 self._Extract()
272
273
274class TarballBuildArtifact(BundledBuildArtifact):
275 """Artifact for tar and tarball files."""
276
277 def _Extract(self):
278 """Extracts a tarball using tar.
279
280 Detects whether the tarball is compressed or not based on the file
281 extension and extracts the tarball into the install_path.
282 """
283 # Deal with exclusions.
284 cmd = ['tar', 'xf', self.tmp_stage_path, '--directory', self.install_dir]
285
286 # Determine how to decompress.
287 tarball = os.path.basename(self.tmp_stage_path)
288 if tarball.endswith('.tar.bz2'):
289 cmd.append('--use-compress-prog=pbzip2')
290 elif tarball.endswith('.tgz') or tarball.endswith('.tar.gz'):
291 cmd.append('--gzip')
292
293 if self._exclude:
294 for exclude in self._exclude:
295 cmd.extend(['--exclude', exclude])
296
297 if self._files_to_extract:
298 cmd.extend(self._files_to_extract)
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700299
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700300 try:
Chris Sosa76e44b92013-01-31 12:11:38 -0800301 subprocess.check_call(cmd)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700302 except subprocess.CalledProcessError, e:
Chris Sosa76e44b92013-01-31 12:11:38 -0800303 raise ArtifactDownloadError(
304 'An error occurred when attempting to untar %s:\n%s' %
305 (self.tmp_stage_path, e))
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700306
307
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700308class AutotestTarballBuildArtifact(TarballBuildArtifact):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700309 """Wrapper around the autotest tarball to download from gsutil."""
310
Chris Sosa76e44b92013-01-31 12:11:38 -0800311 def _Stage(self):
312 """Extracts the tarball into the install path excluding test suites."""
313 super(AutotestTarballBuildArtifact, self)._Stage()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700314
Chris Sosa76e44b92013-01-31 12:11:38 -0800315 # Deal with older autotest packages that may not be bundled.
316 autotest_dir = os.path.join(self.install_dir, 'autotest')
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700317 autotest_pkgs_dir = os.path.join(autotest_dir, 'packages')
318 if not os.path.exists(autotest_pkgs_dir):
319 os.makedirs(autotest_pkgs_dir)
320
321 if not os.path.exists(os.path.join(autotest_pkgs_dir, 'packages.checksum')):
Chris Sosa76e44b92013-01-31 12:11:38 -0800322 cmd = ['autotest/utils/packager.py', 'upload', '--repository',
323 autotest_pkgs_dir, '--all']
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700324 try:
Chris Sosa76e44b92013-01-31 12:11:38 -0800325 subprocess.check_call(cmd, cwd=self.staging_dir)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700326 except subprocess.CalledProcessError, e:
Chris Sosa76e44b92013-01-31 12:11:38 -0800327 raise ArtifactDownloadError(
328 'Failed to create autotest packages!:\n%s' % e)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700329 else:
Gilad Arnoldf5843132012-09-25 00:31:20 -0700330 self._Log('Using pre-generated packages from autotest')
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700331
Chris Masone816e38c2012-05-02 12:22:36 -0700332
Chris Sosa76e44b92013-01-31 12:11:38 -0800333class ZipfileBuildArtifact(BundledBuildArtifact):
334 """A downloadable artifact that is a zipfile."""
Chris Masone816e38c2012-05-02 12:22:36 -0700335
Chris Sosa76e44b92013-01-31 12:11:38 -0800336 def _Extract(self):
337 """Extracts files into the install path."""
338 # Unzip is weird. It expects its args before any excepts and expects its
339 # excepts in a list following the -x.
340 cmd = ['unzip', '-o', self.tmp_stage_path, '-d', self.install_dir]
341 if self._files_to_extract:
342 cmd.extend(self._files_to_extract)
Chris Masone816e38c2012-05-02 12:22:36 -0700343
Chris Sosa76e44b92013-01-31 12:11:38 -0800344 if self._exclude:
345 cmd.append('-x')
346 cmd.extend(self._exclude)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700347
348 try:
Chris Sosa76e44b92013-01-31 12:11:38 -0800349 subprocess.check_call(cmd)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700350 except subprocess.CalledProcessError, e:
Chris Sosa76e44b92013-01-31 12:11:38 -0800351 raise ArtifactDownloadError(
352 'An error occurred when attempting to unzip %s:\n%s' %
353 (self.tmp_stage_path, e))
Gilad Arnold6f99b982012-09-12 10:49:40 -0700354
Gilad Arnold6f99b982012-09-12 10:49:40 -0700355
Chris Sosa76e44b92013-01-31 12:11:38 -0800356class ImplDescription(object):
357 """Data wrapper that describes an artifact's implementation."""
358 def __init__(self, artifact_class, name, *additional_args):
359 """Constructor:
360
361 Args:
362 artifact_class: BuildArtifact class to use for the artifact.
363 name: name to use to identify artifact (see BuildArtifact.name)
364 additional_args: If sub-class uses additional args, these are passed
365 through to them.
366 """
367 self.artifact_class = artifact_class
368 self.name = name
369 self.additional_args = additional_args
370
371
372# Maps artifact names to their implementation description.
373# Please note, it is good practice to use constants for these names if you're
374# going to re-use the names ANYWHERE else in the devserver code.
375ARTIFACT_IMPLEMENTATION_MAP = {
376 artifact_info.FULL_PAYLOAD:
377 ImplDescription(AUTestPayloadBuildArtifact, '.*_full_.*'),
378 artifact_info.DELTA_PAYLOADS:
379 ImplDescription(DeltaPayloadsArtifact, '.*_delta_.*'),
380 artifact_info.STATEFUL_PAYLOAD:
381 ImplDescription(BuildArtifact, STATEFUL_UPDATE_FILE),
382
383 artifact_info.BASE_IMAGE:
384 ImplDescription(ZipfileBuildArtifact, IMAGE_FILE,
385 ['chromiumos_base_image.bin']),
386 artifact_info.RECOVERY_IMAGE:
387 ImplDescription(ZipfileBuildArtifact, IMAGE_FILE, ['recovery_image.bin']),
388 artifact_info.TEST_IMAGE:
389 ImplDescription(ZipfileBuildArtifact, IMAGE_FILE, [TEST_IMAGE_FILE]),
390
391 artifact_info.AUTOTEST:
392 ImplDescription(AutotestTarballBuildArtifact, AUTOTEST_FILE, None,
393 ['autotest/test_suites']),
394 artifact_info.TEST_SUITES:
395 ImplDescription(TarballBuildArtifact, TEST_SUITES_FILE),
396 artifact_info.AU_SUITE:
397 ImplDescription(AutotestTarballBuildArtifact, AU_SUITE_FILE),
398
399 artifact_info.FIRMWARE:
400 ImplDescription(BuildArtifact, FIRMWARE_FILE),
401 artifact_info.SYMBOLS:
402 ImplDescription(TarballBuildArtifact, DEBUG_SYMBOLS_FILE,
403 ['debug/breakpad']),
404}
405
406
407class ArtifactFactory(object):
408 """A factory class that generates build artifacts from artifact names."""
409
410 def __init__(self, staging_dir, archive_url, artifact_names, build):
411 """Initalizes the member variables for the factory.
412
413 Args:
414 staging_dir: the dir into which to stage the artifacts.
415 archive_url: the Google Storage url of the bucket where the debug
416 symbols for the desired build are stored.
417 artifact_names: List of artifact names to stage.
418 build: The name of the build.
419 """
420 self.staging_dir = staging_dir
421 self.archive_url = archive_url
422 self.artifact_names = artifact_names
423 self.build = build
424
425 @staticmethod
426 def _GetDescriptionComponents(artifact_name):
427 """Returns a tuple of for BuildArtifact class, name, and additional args."""
428 description = ARTIFACT_IMPLEMENTATION_MAP[artifact_name]
429 return (description.artifact_class, description.name,
430 description.additional_args)
431
432 def _Artifacts(self, artifact_names):
433 """Returns an iterable of BuildArtifacts from |artifact_names|."""
434 artifacts = []
435 for artifact_name in artifact_names:
436 artifact_class, path, args = self._GetDescriptionComponents(
437 artifact_name)
438 artifacts.append(artifact_class(self.staging_dir, self.archive_url, path,
439 self.build, *args))
440
441 return artifacts
442
443 def RequiredArtifacts(self):
444 """Returns an iterable of BuildArtifacts for the factory's artifacts."""
445 return self._Artifacts(self.artifact_names)
446
447 def OptionalArtifacts(self):
448 """Returns an iterable of BuildArtifacts that should be cached."""
449 optional_names = set()
450 for artifact_name, optional_list in (
451 artifact_info.REQUESTED_TO_OPTIONAL_MAP.iteritems()):
452 # We are already downloading it.
453 if artifact_name in self.artifact_names:
454 optional_names = optional_names.union(optional_list)
455
456 return self._Artifacts(optional_names - set(self.artifact_names))