blob: 2cbd6ae4875f4df04c5bda0bf187651026ccbbb8 [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
joychen3cb228e2013-06-12 12:13:13 -070014import devserver_constants
Chris Sosa47a7d4e2012-03-28 11:26:55 -070015import gsutil_util
Gilad Arnoldc65330c2012-09-20 15:17:48 -070016import log_util
Chris Sosa47a7d4e2012-03-28 11:26:55 -070017
18
Chris Sosa76e44b92013-01-31 12:11:38 -080019_AU_BASE = 'au'
20_NTON_DIR_SUFFIX = '_nton'
21_MTON_DIR_SUFFIX = '_mton'
22
23############ Actual filenames of artifacts in Google Storage ############
24
25AU_SUITE_FILE = 'au_control.tar.bz2'
26AUTOTEST_FILE = 'autotest.tar'
27AUTOTEST_COMPRESSED_FILE = 'autotest.tar.bz2'
joychen3cb228e2013-06-12 12:13:13 -070028BASE_IMAGE_FILE = 'chromiumos_base_image.bin'
Chris Sosa76e44b92013-01-31 12:11:38 -080029DEBUG_SYMBOLS_FILE = 'debug.tgz'
30FIRMWARE_FILE = 'firmware_from_source.tar.bz2'
31IMAGE_FILE = 'image.zip'
joychen3cb228e2013-06-12 12:13:13 -070032RECOVERY_IMAGE_FILE = 'recovery_image.bin'
Chris Sosa76e44b92013-01-31 12:11:38 -080033STATEFUL_UPDATE_FILE = 'stateful.tgz'
34TEST_IMAGE_FILE = 'chromiumos_test_image.bin'
35TEST_SUITES_FILE = 'test_suites.tar.bz2'
36
37_build_artifact_locks = common_util.LockDict()
Chris Sosa47a7d4e2012-03-28 11:26:55 -070038
39
40class ArtifactDownloadError(Exception):
41 """Error used to signify an issue processing an artifact."""
42 pass
43
44
Gilad Arnoldc65330c2012-09-20 15:17:48 -070045class BuildArtifact(log_util.Loggable):
Chris Sosa47a7d4e2012-03-28 11:26:55 -070046 """Wrapper around an artifact to download from gsutil.
47
48 The purpose of this class is to download objects from Google Storage
49 and install them to a local directory. There are two main functions, one to
50 download/prepare the artifacts in to a temporary staging area and the second
51 to stage it into its final destination.
Chris Sosa76e44b92013-01-31 12:11:38 -080052
53 Class members:
54 archive_url = archive_url
55 name: Name given for artifact -- either a regexp or name of the artifact in
56 gs. If a regexp, is modified to actual name before call to _Download.
57 build: The version of the build i.e. R26-2342.0.0.
58 marker_name: Name used to define the lock marker for the artifacts to
59 prevent it from being re-downloaded. By default based on name
60 but can be overriden by children.
61 staging_dir: directory for the artifact reserved for staging. Cleaned
62 up after staging.
63 tmp_stage_path: Path used in staging_dir for placing artifact.
64 install_dir: The final location where the artifact should be staged to.
65 single_name: If True the name given should only match one item. Note, if not
66 True, self.name will become a list of items returned.
Chris Sosa47a7d4e2012-03-28 11:26:55 -070067 """
Chris Sosa76e44b92013-01-31 12:11:38 -080068 def __init__(self, install_dir, archive_url, name, build):
Chris Sosa47a7d4e2012-03-28 11:26:55 -070069 """Args:
Chris Sosa76e44b92013-01-31 12:11:38 -080070 install_dir: Where to install the artifact.
71 archive_url: The Google Storage path to find the artifact.
72 name: Identifying name to be used to find/store the artifact.
73 build: The name of the build e.g. board/release.
Chris Sosa47a7d4e2012-03-28 11:26:55 -070074 """
Chris Sosa6a3697f2013-01-29 16:44:43 -080075 super(BuildArtifact, self).__init__()
Chris Sosa47a7d4e2012-03-28 11:26:55 -070076
Chris Sosa76e44b92013-01-31 12:11:38 -080077 # In-memory lock to keep the devserver from colliding with itself while
78 # attempting to stage the same artifact.
79 self._process_lock = None
Chris Sosa47a7d4e2012-03-28 11:26:55 -070080
Chris Sosa76e44b92013-01-31 12:11:38 -080081 self.archive_url = archive_url
82 self.name = name
83 self.build = build
Chris Sosa47a7d4e2012-03-28 11:26:55 -070084
Chris Sosa76e44b92013-01-31 12:11:38 -080085 self.marker_name = '.' + self._SanitizeName(name)
Chris Sosa47a7d4e2012-03-28 11:26:55 -070086
Chris Sosa76e44b92013-01-31 12:11:38 -080087 self.staging_dir = tempfile.mkdtemp(prefix='Devserver%s' % (
88 type(self).__name__))
89 self.tmp_stage_path = None
Chris Sosa47a7d4e2012-03-28 11:26:55 -070090
Chris Sosa76e44b92013-01-31 12:11:38 -080091 self.install_dir = install_dir
92
93 self.single_name = True
94
95 @staticmethod
96 def _SanitizeName(name):
97 """Sanitizes name to be used for creating a file on the filesystem.
98
99 '.','/' and '*' have special meaning in FS lingo. Replace them with words.
100 """
101 return name.replace('*', 'STAR').replace('.', 'DOT').replace('/', 'SLASH')
102
103 def _ArtifactStaged(self):
104 """Returns True if artifact is already staged."""
105 return os.path.exists(os.path.join(self.install_dir, self.marker_name))
106
107 def _MarkArtifactStaged(self):
108 """Marks the artifact as staged."""
109 with open(os.path.join(self.install_dir, self.marker_name), 'w') as f:
110 f.write('')
111
112 def _WaitForArtifactToExist(self, timeout):
113 """Waits for artifact to exist and sets self.name to appropriate name."""
114 names = gsutil_util.GetGSNamesWithWait(
115 self.name, self.archive_url, str(self), single_item=self.single_name,
116 timeout=timeout)
117 if not names:
118 raise ArtifactDownloadError('Could not find %s in Google Storage' %
119 self.name)
120
121 if self.single_name:
122 if len(names) > 1:
123 raise ArtifactDownloadError('Too many artifacts match %s' % self.name)
124
125 self.name = names[0]
126 else:
127 self.name = names
128
129 def _Download(self):
130 """Downloads artifact from Google Storage to a local staging directory."""
131 self.tmp_stage_path = os.path.join(self.staging_dir, self.name)
132 gs_path = '/'.join([self.archive_url, self.name])
133 gsutil_util.DownloadFromGS(gs_path, self.tmp_stage_path)
134
135 def _Stage(self):
136 """Stages the artifact from the tmp directory to the final path."""
137 install_path = os.path.join(self.install_dir, self.name)
138 shutil.move(self.tmp_stage_path, install_path)
139
140 def Process(self, no_wait):
141 """Main call point to all artifacts. Downloads and Stages artifact.
142
143 Downloads and Stages artifact from Google Storage to the install directory
144 specified in the constructor. It multi-thread safe and does not overwrite
145 the artifact if it's already been downloaded or being downloaded. After
146 processing, leaves behind a marker to indicate to future invocations that
147 the artifact has already been staged based on the name of the artifact.
148
149 Do not override as it modifies important private variables, ensures thread
150 safety, and maintains cache semantics.
151
152 Note: this may be a blocking call when the artifact is already in the
153 process of being staged.
154
155 Args:
156 no_wait: If True, don't block waiting for artifact to exist if we fail to
157 immediately find it.
158
159 Raises:
160 ArtifactDownloadError: If the artifact fails to download from Google
161 Storage for any reason or that the regexp
162 defined by name is not specific enough.
163 """
164 if not self._process_lock:
165 self._process_lock = _build_artifact_locks.lock(
166 os.path.join(self.install_dir, self.name))
167
168 with self._process_lock:
169 common_util.MkDirP(self.install_dir)
170 if not self._ArtifactStaged():
171 # If the artifact should already have been uploaded, don't waste
172 # cycles waiting around for it to exist.
173 timeout = 1 if no_wait else 10
174 self._WaitForArtifactToExist(timeout)
175 self._Download()
176 self._Stage()
177 self._MarkArtifactStaged()
178 else:
179 self._Log('%s is already staged.', self)
180
181 def __del__(self):
182 shutil.rmtree(self.staging_dir)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700183
184 def __str__(self):
185 """String representation for the download."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800186 return '->'.join(['%s/%s' % (self.archive_url, self.name),
187 self.staging_dir, self.install_dir])
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700188
189
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700190class AUTestPayloadBuildArtifact(BuildArtifact):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700191 """Wrapper for AUTest delta payloads which need additional setup."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800192 def _Stage(self):
193 super(AUTestPayloadBuildArtifact, self)._Stage()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700194
Chris Sosa76e44b92013-01-31 12:11:38 -0800195 # Rename to update.gz.
196 install_path = os.path.join(self.install_dir, self.name)
joychen3cb228e2013-06-12 12:13:13 -0700197 new_install_path = os.path.join(self.install_dir,
198 devserver_constants.ROOT_UPDATE_FILE)
Chris Sosa76e44b92013-01-31 12:11:38 -0800199 shutil.move(install_path, new_install_path)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700200
201
Chris Sosa76e44b92013-01-31 12:11:38 -0800202# TODO(sosa): Change callers to make this artifact more sane.
203class DeltaPayloadsArtifact(BuildArtifact):
204 """Delta payloads from the archive_url.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700205
Chris Sosa76e44b92013-01-31 12:11:38 -0800206 This artifact is super strange. It custom handles directories and
207 pulls in all delta payloads. We can't specify exactly what we want
208 because unlike other artifacts, this one does not conform to something a
209 client might know. The client doesn't know the version of n-1 or whether it
210 was even generated.
211 """
212 def __init__(self, *args):
213 super(DeltaPayloadsArtifact, self).__init__(*args)
214 self.single_name = False # Expect multiple deltas
215 nton_name = 'chromeos_%s%s' % (self.build, self.name)
216 mton_name = 'chromeos_(?!%s)%s' % (self.build, self.name)
217 nton_install_dir = os.path.join(self.install_dir, _AU_BASE,
218 self.build + _NTON_DIR_SUFFIX)
219 mton_install_dir = os.path.join(self.install_dir, _AU_BASE,
220 self.build + _MTON_DIR_SUFFIX)
221 self._sub_artifacts = [
222 AUTestPayloadBuildArtifact(mton_install_dir, self.archive_url,
223 mton_name, self.build),
224 AUTestPayloadBuildArtifact(nton_install_dir, self.archive_url,
225 nton_name, self.build)]
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700226
Chris Sosa76e44b92013-01-31 12:11:38 -0800227 def _Download(self):
228 """With sub-artifacts we do everything in _Stage()."""
229 pass
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700230
Chris Sosa76e44b92013-01-31 12:11:38 -0800231 def _Stage(self):
232 """Process each sub-artifact. Only error out if none can be found."""
233 for artifact in self._sub_artifacts:
234 try:
235 artifact.Process(no_wait=True)
236 # Setup symlink so that AU will work for this payload.
237 os.symlink(
238 os.path.join(os.pardir, os.pardir, STATEFUL_UPDATE_FILE),
239 os.path.join(artifact.install_dir, STATEFUL_UPDATE_FILE))
240 except ArtifactDownloadError as e:
241 self._Log('Could not process %s: %s', artifact, e)
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700242
Chris Sosa76e44b92013-01-31 12:11:38 -0800243
244class BundledBuildArtifact(BuildArtifact):
245 """A single build artifact bundle e.g. zip file or tar file."""
246 def __init__(self, install_dir, archive_url, name, build,
247 files_to_extract=None, exclude=None):
248 """Takes BuildArtifacts are with two additional args.
249
250 Additional args:
251 files_to_extract: A list of files to extract. If set to None, extract
252 all files.
253 exclude: A list of files to exclude. If None, no files are excluded.
254 """
255 super(BundledBuildArtifact, self).__init__(install_dir, archive_url, name,
256 build)
257 self._files_to_extract = files_to_extract
258 self._exclude = exclude
259
260 # We modify the marker so that it is unique to what was staged.
261 if files_to_extract:
262 self.marker_name = self._SanitizeName(
263 '_'.join(['.' + self.name] + files_to_extract))
264
265 def _Extract(self):
266 """Extracts the bundle into install_dir. Must be overridden.
267
268 If set, uses files_to_extract to only extract those items. If set, use
269 exclude to exclude specific files.
270 """
271 raise NotImplementedError()
272
273 def _Stage(self):
274 self._Extract()
275
276
277class TarballBuildArtifact(BundledBuildArtifact):
278 """Artifact for tar and tarball files."""
279
280 def _Extract(self):
281 """Extracts a tarball using tar.
282
283 Detects whether the tarball is compressed or not based on the file
284 extension and extracts the tarball into the install_path.
285 """
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700286 try:
Simran Basi4baad082013-02-14 13:39:18 -0800287 common_util.ExtractTarball(self.tmp_stage_path, self.install_dir,
288 files_to_extract=self._files_to_extract,
289 excluded_files=self._exclude)
290 except common_util.CommonUtilError as e:
291 raise ArtifactDownloadError(str(e))
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700292
293
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700294class AutotestTarballBuildArtifact(TarballBuildArtifact):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700295 """Wrapper around the autotest tarball to download from gsutil."""
296
Chris Sosa76e44b92013-01-31 12:11:38 -0800297 def _Stage(self):
298 """Extracts the tarball into the install path excluding test suites."""
299 super(AutotestTarballBuildArtifact, self)._Stage()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700300
Chris Sosa76e44b92013-01-31 12:11:38 -0800301 # Deal with older autotest packages that may not be bundled.
joychen3cb228e2013-06-12 12:13:13 -0700302 autotest_dir = os.path.join(self.install_dir,
303 devserver_constants.AUTOTEST_DIR)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700304 autotest_pkgs_dir = os.path.join(autotest_dir, 'packages')
305 if not os.path.exists(autotest_pkgs_dir):
306 os.makedirs(autotest_pkgs_dir)
307
308 if not os.path.exists(os.path.join(autotest_pkgs_dir, 'packages.checksum')):
Chris Sosa76e44b92013-01-31 12:11:38 -0800309 cmd = ['autotest/utils/packager.py', 'upload', '--repository',
310 autotest_pkgs_dir, '--all']
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700311 try:
Chris Sosa76e44b92013-01-31 12:11:38 -0800312 subprocess.check_call(cmd, cwd=self.staging_dir)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700313 except subprocess.CalledProcessError, e:
Chris Sosa76e44b92013-01-31 12:11:38 -0800314 raise ArtifactDownloadError(
315 'Failed to create autotest packages!:\n%s' % e)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700316 else:
Gilad Arnoldf5843132012-09-25 00:31:20 -0700317 self._Log('Using pre-generated packages from autotest')
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700318
Chris Masone816e38c2012-05-02 12:22:36 -0700319
Chris Sosa76e44b92013-01-31 12:11:38 -0800320class ZipfileBuildArtifact(BundledBuildArtifact):
321 """A downloadable artifact that is a zipfile."""
Chris Masone816e38c2012-05-02 12:22:36 -0700322
Chris Sosa76e44b92013-01-31 12:11:38 -0800323 def _Extract(self):
324 """Extracts files into the install path."""
325 # Unzip is weird. It expects its args before any excepts and expects its
326 # excepts in a list following the -x.
327 cmd = ['unzip', '-o', self.tmp_stage_path, '-d', self.install_dir]
328 if self._files_to_extract:
329 cmd.extend(self._files_to_extract)
Chris Masone816e38c2012-05-02 12:22:36 -0700330
Chris Sosa76e44b92013-01-31 12:11:38 -0800331 if self._exclude:
332 cmd.append('-x')
333 cmd.extend(self._exclude)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700334
335 try:
Chris Sosa76e44b92013-01-31 12:11:38 -0800336 subprocess.check_call(cmd)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700337 except subprocess.CalledProcessError, e:
Chris Sosa76e44b92013-01-31 12:11:38 -0800338 raise ArtifactDownloadError(
339 'An error occurred when attempting to unzip %s:\n%s' %
340 (self.tmp_stage_path, e))
Gilad Arnold6f99b982012-09-12 10:49:40 -0700341
Gilad Arnold6f99b982012-09-12 10:49:40 -0700342
Chris Sosa76e44b92013-01-31 12:11:38 -0800343class ImplDescription(object):
344 """Data wrapper that describes an artifact's implementation."""
345 def __init__(self, artifact_class, name, *additional_args):
346 """Constructor:
347
348 Args:
349 artifact_class: BuildArtifact class to use for the artifact.
350 name: name to use to identify artifact (see BuildArtifact.name)
351 additional_args: If sub-class uses additional args, these are passed
352 through to them.
353 """
354 self.artifact_class = artifact_class
355 self.name = name
356 self.additional_args = additional_args
357
358
359# Maps artifact names to their implementation description.
360# Please note, it is good practice to use constants for these names if you're
361# going to re-use the names ANYWHERE else in the devserver code.
362ARTIFACT_IMPLEMENTATION_MAP = {
363 artifact_info.FULL_PAYLOAD:
364 ImplDescription(AUTestPayloadBuildArtifact, '.*_full_.*'),
365 artifact_info.DELTA_PAYLOADS:
366 ImplDescription(DeltaPayloadsArtifact, '.*_delta_.*'),
367 artifact_info.STATEFUL_PAYLOAD:
368 ImplDescription(BuildArtifact, STATEFUL_UPDATE_FILE),
369
370 artifact_info.BASE_IMAGE:
371 ImplDescription(ZipfileBuildArtifact, IMAGE_FILE,
joychen3cb228e2013-06-12 12:13:13 -0700372 [BASE_IMAGE_FILE]),
Chris Sosa76e44b92013-01-31 12:11:38 -0800373 artifact_info.RECOVERY_IMAGE:
joychen3cb228e2013-06-12 12:13:13 -0700374 ImplDescription(ZipfileBuildArtifact, IMAGE_FILE, [RECOVERY_IMAGE_FILE]),
Chris Sosa76e44b92013-01-31 12:11:38 -0800375 artifact_info.TEST_IMAGE:
376 ImplDescription(ZipfileBuildArtifact, IMAGE_FILE, [TEST_IMAGE_FILE]),
377
378 artifact_info.AUTOTEST:
379 ImplDescription(AutotestTarballBuildArtifact, AUTOTEST_FILE, None,
380 ['autotest/test_suites']),
381 artifact_info.TEST_SUITES:
382 ImplDescription(TarballBuildArtifact, TEST_SUITES_FILE),
383 artifact_info.AU_SUITE:
Chris Sosaa56c4032013-03-17 21:59:54 -0700384 ImplDescription(TarballBuildArtifact, AU_SUITE_FILE),
Chris Sosa76e44b92013-01-31 12:11:38 -0800385
386 artifact_info.FIRMWARE:
387 ImplDescription(BuildArtifact, FIRMWARE_FILE),
388 artifact_info.SYMBOLS:
389 ImplDescription(TarballBuildArtifact, DEBUG_SYMBOLS_FILE,
390 ['debug/breakpad']),
391}
392
393
394class ArtifactFactory(object):
395 """A factory class that generates build artifacts from artifact names."""
396
397 def __init__(self, staging_dir, archive_url, artifact_names, build):
398 """Initalizes the member variables for the factory.
399
400 Args:
401 staging_dir: the dir into which to stage the artifacts.
402 archive_url: the Google Storage url of the bucket where the debug
403 symbols for the desired build are stored.
404 artifact_names: List of artifact names to stage.
405 build: The name of the build.
406 """
407 self.staging_dir = staging_dir
408 self.archive_url = archive_url
409 self.artifact_names = artifact_names
410 self.build = build
411
412 @staticmethod
413 def _GetDescriptionComponents(artifact_name):
414 """Returns a tuple of for BuildArtifact class, name, and additional args."""
415 description = ARTIFACT_IMPLEMENTATION_MAP[artifact_name]
416 return (description.artifact_class, description.name,
417 description.additional_args)
418
419 def _Artifacts(self, artifact_names):
420 """Returns an iterable of BuildArtifacts from |artifact_names|."""
421 artifacts = []
422 for artifact_name in artifact_names:
423 artifact_class, path, args = self._GetDescriptionComponents(
424 artifact_name)
425 artifacts.append(artifact_class(self.staging_dir, self.archive_url, path,
426 self.build, *args))
427
428 return artifacts
429
430 def RequiredArtifacts(self):
431 """Returns an iterable of BuildArtifacts for the factory's artifacts."""
432 return self._Artifacts(self.artifact_names)
433
434 def OptionalArtifacts(self):
435 """Returns an iterable of BuildArtifacts that should be cached."""
436 optional_names = set()
437 for artifact_name, optional_list in (
438 artifact_info.REQUESTED_TO_OPTIONAL_MAP.iteritems()):
439 # We are already downloading it.
440 if artifact_name in self.artifact_names:
441 optional_names = optional_names.union(optional_list)
442
443 return self._Artifacts(optional_names - set(self.artifact_names))