blob: 3fa64b8e1611d23c777a21790210842f6830c460 [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 """
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700283 try:
Simran Basi4baad082013-02-14 13:39:18 -0800284 common_util.ExtractTarball(self.tmp_stage_path, self.install_dir,
285 files_to_extract=self._files_to_extract,
286 excluded_files=self._exclude)
287 except common_util.CommonUtilError as e:
288 raise ArtifactDownloadError(str(e))
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700289
290
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700291class AutotestTarballBuildArtifact(TarballBuildArtifact):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700292 """Wrapper around the autotest tarball to download from gsutil."""
293
Chris Sosa76e44b92013-01-31 12:11:38 -0800294 def _Stage(self):
295 """Extracts the tarball into the install path excluding test suites."""
296 super(AutotestTarballBuildArtifact, self)._Stage()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700297
Chris Sosa76e44b92013-01-31 12:11:38 -0800298 # Deal with older autotest packages that may not be bundled.
299 autotest_dir = os.path.join(self.install_dir, 'autotest')
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700300 autotest_pkgs_dir = os.path.join(autotest_dir, 'packages')
301 if not os.path.exists(autotest_pkgs_dir):
302 os.makedirs(autotest_pkgs_dir)
303
304 if not os.path.exists(os.path.join(autotest_pkgs_dir, 'packages.checksum')):
Chris Sosa76e44b92013-01-31 12:11:38 -0800305 cmd = ['autotest/utils/packager.py', 'upload', '--repository',
306 autotest_pkgs_dir, '--all']
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700307 try:
Chris Sosa76e44b92013-01-31 12:11:38 -0800308 subprocess.check_call(cmd, cwd=self.staging_dir)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700309 except subprocess.CalledProcessError, e:
Chris Sosa76e44b92013-01-31 12:11:38 -0800310 raise ArtifactDownloadError(
311 'Failed to create autotest packages!:\n%s' % e)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700312 else:
Gilad Arnoldf5843132012-09-25 00:31:20 -0700313 self._Log('Using pre-generated packages from autotest')
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700314
Chris Masone816e38c2012-05-02 12:22:36 -0700315
Chris Sosa76e44b92013-01-31 12:11:38 -0800316class ZipfileBuildArtifact(BundledBuildArtifact):
317 """A downloadable artifact that is a zipfile."""
Chris Masone816e38c2012-05-02 12:22:36 -0700318
Chris Sosa76e44b92013-01-31 12:11:38 -0800319 def _Extract(self):
320 """Extracts files into the install path."""
321 # Unzip is weird. It expects its args before any excepts and expects its
322 # excepts in a list following the -x.
323 cmd = ['unzip', '-o', self.tmp_stage_path, '-d', self.install_dir]
324 if self._files_to_extract:
325 cmd.extend(self._files_to_extract)
Chris Masone816e38c2012-05-02 12:22:36 -0700326
Chris Sosa76e44b92013-01-31 12:11:38 -0800327 if self._exclude:
328 cmd.append('-x')
329 cmd.extend(self._exclude)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700330
331 try:
Chris Sosa76e44b92013-01-31 12:11:38 -0800332 subprocess.check_call(cmd)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700333 except subprocess.CalledProcessError, e:
Chris Sosa76e44b92013-01-31 12:11:38 -0800334 raise ArtifactDownloadError(
335 'An error occurred when attempting to unzip %s:\n%s' %
336 (self.tmp_stage_path, e))
Gilad Arnold6f99b982012-09-12 10:49:40 -0700337
Gilad Arnold6f99b982012-09-12 10:49:40 -0700338
Chris Sosa76e44b92013-01-31 12:11:38 -0800339class ImplDescription(object):
340 """Data wrapper that describes an artifact's implementation."""
341 def __init__(self, artifact_class, name, *additional_args):
342 """Constructor:
343
344 Args:
345 artifact_class: BuildArtifact class to use for the artifact.
346 name: name to use to identify artifact (see BuildArtifact.name)
347 additional_args: If sub-class uses additional args, these are passed
348 through to them.
349 """
350 self.artifact_class = artifact_class
351 self.name = name
352 self.additional_args = additional_args
353
354
355# Maps artifact names to their implementation description.
356# Please note, it is good practice to use constants for these names if you're
357# going to re-use the names ANYWHERE else in the devserver code.
358ARTIFACT_IMPLEMENTATION_MAP = {
359 artifact_info.FULL_PAYLOAD:
360 ImplDescription(AUTestPayloadBuildArtifact, '.*_full_.*'),
361 artifact_info.DELTA_PAYLOADS:
362 ImplDescription(DeltaPayloadsArtifact, '.*_delta_.*'),
363 artifact_info.STATEFUL_PAYLOAD:
364 ImplDescription(BuildArtifact, STATEFUL_UPDATE_FILE),
365
366 artifact_info.BASE_IMAGE:
367 ImplDescription(ZipfileBuildArtifact, IMAGE_FILE,
368 ['chromiumos_base_image.bin']),
369 artifact_info.RECOVERY_IMAGE:
370 ImplDescription(ZipfileBuildArtifact, IMAGE_FILE, ['recovery_image.bin']),
371 artifact_info.TEST_IMAGE:
372 ImplDescription(ZipfileBuildArtifact, IMAGE_FILE, [TEST_IMAGE_FILE]),
373
374 artifact_info.AUTOTEST:
375 ImplDescription(AutotestTarballBuildArtifact, AUTOTEST_FILE, None,
376 ['autotest/test_suites']),
377 artifact_info.TEST_SUITES:
378 ImplDescription(TarballBuildArtifact, TEST_SUITES_FILE),
379 artifact_info.AU_SUITE:
Chris Sosaa56c4032013-03-17 21:59:54 -0700380 ImplDescription(TarballBuildArtifact, AU_SUITE_FILE),
Chris Sosa76e44b92013-01-31 12:11:38 -0800381
382 artifact_info.FIRMWARE:
383 ImplDescription(BuildArtifact, FIRMWARE_FILE),
384 artifact_info.SYMBOLS:
385 ImplDescription(TarballBuildArtifact, DEBUG_SYMBOLS_FILE,
386 ['debug/breakpad']),
387}
388
389
390class ArtifactFactory(object):
391 """A factory class that generates build artifacts from artifact names."""
392
393 def __init__(self, staging_dir, archive_url, artifact_names, build):
394 """Initalizes the member variables for the factory.
395
396 Args:
397 staging_dir: the dir into which to stage the artifacts.
398 archive_url: the Google Storage url of the bucket where the debug
399 symbols for the desired build are stored.
400 artifact_names: List of artifact names to stage.
401 build: The name of the build.
402 """
403 self.staging_dir = staging_dir
404 self.archive_url = archive_url
405 self.artifact_names = artifact_names
406 self.build = build
407
408 @staticmethod
409 def _GetDescriptionComponents(artifact_name):
410 """Returns a tuple of for BuildArtifact class, name, and additional args."""
411 description = ARTIFACT_IMPLEMENTATION_MAP[artifact_name]
412 return (description.artifact_class, description.name,
413 description.additional_args)
414
415 def _Artifacts(self, artifact_names):
416 """Returns an iterable of BuildArtifacts from |artifact_names|."""
417 artifacts = []
418 for artifact_name in artifact_names:
419 artifact_class, path, args = self._GetDescriptionComponents(
420 artifact_name)
421 artifacts.append(artifact_class(self.staging_dir, self.archive_url, path,
422 self.build, *args))
423
424 return artifacts
425
426 def RequiredArtifacts(self):
427 """Returns an iterable of BuildArtifacts for the factory's artifacts."""
428 return self._Artifacts(self.artifact_names)
429
430 def OptionalArtifacts(self):
431 """Returns an iterable of BuildArtifacts that should be cached."""
432 optional_names = set()
433 for artifact_name, optional_list in (
434 artifact_info.REQUESTED_TO_OPTIONAL_MAP.iteritems()):
435 # We are already downloading it.
436 if artifact_name in self.artifact_names:
437 optional_names = optional_names.union(optional_list)
438
439 return self._Artifacts(optional_names - set(self.artifact_names))