blob: 2934467978ac64d7b9b4b39f5a4de3565a4b3be8 [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'
Chris Sosa5d1b0792013-07-31 10:54:52 -070025PAYGEN_AU_SUITE_FILE = 'paygen_au_control.tar.bz2'
Chris Sosa76e44b92013-01-31 12:11:38 -080026AUTOTEST_FILE = 'autotest.tar'
27AUTOTEST_COMPRESSED_FILE = 'autotest.tar.bz2'
28DEBUG_SYMBOLS_FILE = 'debug.tgz'
29FIRMWARE_FILE = 'firmware_from_source.tar.bz2'
30IMAGE_FILE = 'image.zip'
Chris Sosa76e44b92013-01-31 12:11:38 -080031STATEFUL_UPDATE_FILE = 'stateful.tgz'
Chris Sosa76e44b92013-01-31 12:11:38 -080032TEST_SUITES_FILE = 'test_suites.tar.bz2'
33
34_build_artifact_locks = common_util.LockDict()
Chris Sosa47a7d4e2012-03-28 11:26:55 -070035
36
37class ArtifactDownloadError(Exception):
38 """Error used to signify an issue processing an artifact."""
39 pass
40
41
Gilad Arnoldc65330c2012-09-20 15:17:48 -070042class BuildArtifact(log_util.Loggable):
Chris Sosa47a7d4e2012-03-28 11:26:55 -070043 """Wrapper around an artifact to download from gsutil.
44
45 The purpose of this class is to download objects from Google Storage
46 and install them to a local directory. There are two main functions, one to
47 download/prepare the artifacts in to a temporary staging area and the second
48 to stage it into its final destination.
Chris Sosa76e44b92013-01-31 12:11:38 -080049
50 Class members:
51 archive_url = archive_url
52 name: Name given for artifact -- either a regexp or name of the artifact in
53 gs. If a regexp, is modified to actual name before call to _Download.
54 build: The version of the build i.e. R26-2342.0.0.
55 marker_name: Name used to define the lock marker for the artifacts to
56 prevent it from being re-downloaded. By default based on name
57 but can be overriden by children.
joychen0a8e34e2013-06-24 17:58:36 -070058 install_path: Path to artifact.
Chris Sosa76e44b92013-01-31 12:11:38 -080059 install_dir: The final location where the artifact should be staged to.
60 single_name: If True the name given should only match one item. Note, if not
61 True, self.name will become a list of items returned.
Chris Sosa47a7d4e2012-03-28 11:26:55 -070062 """
Chris Sosa76e44b92013-01-31 12:11:38 -080063 def __init__(self, install_dir, archive_url, name, build):
Chris Sosa47a7d4e2012-03-28 11:26:55 -070064 """Args:
Chris Sosa76e44b92013-01-31 12:11:38 -080065 install_dir: Where to install the artifact.
66 archive_url: The Google Storage path to find the artifact.
67 name: Identifying name to be used to find/store the artifact.
68 build: The name of the build e.g. board/release.
Chris Sosa47a7d4e2012-03-28 11:26:55 -070069 """
Chris Sosa6a3697f2013-01-29 16:44:43 -080070 super(BuildArtifact, self).__init__()
Chris Sosa47a7d4e2012-03-28 11:26:55 -070071
Chris Sosa76e44b92013-01-31 12:11:38 -080072 # In-memory lock to keep the devserver from colliding with itself while
73 # attempting to stage the same artifact.
74 self._process_lock = None
Chris Sosa47a7d4e2012-03-28 11:26:55 -070075
Chris Sosa76e44b92013-01-31 12:11:38 -080076 self.archive_url = archive_url
77 self.name = name
78 self.build = build
Chris Sosa47a7d4e2012-03-28 11:26:55 -070079
Chris Sosa76e44b92013-01-31 12:11:38 -080080 self.marker_name = '.' + self._SanitizeName(name)
Chris Sosa47a7d4e2012-03-28 11:26:55 -070081
joychen0a8e34e2013-06-24 17:58:36 -070082 self.install_path = None
Chris Sosa47a7d4e2012-03-28 11:26:55 -070083
Chris Sosa76e44b92013-01-31 12:11:38 -080084 self.install_dir = install_dir
85
86 self.single_name = True
87
88 @staticmethod
89 def _SanitizeName(name):
90 """Sanitizes name to be used for creating a file on the filesystem.
91
92 '.','/' and '*' have special meaning in FS lingo. Replace them with words.
93 """
94 return name.replace('*', 'STAR').replace('.', 'DOT').replace('/', 'SLASH')
95
96 def _ArtifactStaged(self):
97 """Returns True if artifact is already staged."""
98 return os.path.exists(os.path.join(self.install_dir, self.marker_name))
99
100 def _MarkArtifactStaged(self):
101 """Marks the artifact as staged."""
102 with open(os.path.join(self.install_dir, self.marker_name), 'w') as f:
103 f.write('')
104
105 def _WaitForArtifactToExist(self, timeout):
106 """Waits for artifact to exist and sets self.name to appropriate name."""
107 names = gsutil_util.GetGSNamesWithWait(
108 self.name, self.archive_url, str(self), single_item=self.single_name,
109 timeout=timeout)
110 if not names:
111 raise ArtifactDownloadError('Could not find %s in Google Storage' %
112 self.name)
113
114 if self.single_name:
115 if len(names) > 1:
116 raise ArtifactDownloadError('Too many artifacts match %s' % self.name)
117
118 self.name = names[0]
119 else:
120 self.name = names
121
122 def _Download(self):
joychen0a8e34e2013-06-24 17:58:36 -0700123 """Downloads artifact from Google Storage to a local directory."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800124 gs_path = '/'.join([self.archive_url, self.name])
joychen0a8e34e2013-06-24 17:58:36 -0700125 self.install_path = os.path.join(self.install_dir, self.name)
126 gsutil_util.DownloadFromGS(gs_path, self.install_path)
Chris Sosa76e44b92013-01-31 12:11:38 -0800127
joychen0a8e34e2013-06-24 17:58:36 -0700128 def _Setup(self):
129 """For tarball like artifacts, extracts and prepares contents."""
130 pass
131
Chris Sosa76e44b92013-01-31 12:11:38 -0800132
133 def Process(self, no_wait):
134 """Main call point to all artifacts. Downloads and Stages artifact.
135
136 Downloads and Stages artifact from Google Storage to the install directory
137 specified in the constructor. It multi-thread safe and does not overwrite
138 the artifact if it's already been downloaded or being downloaded. After
139 processing, leaves behind a marker to indicate to future invocations that
140 the artifact has already been staged based on the name of the artifact.
141
142 Do not override as it modifies important private variables, ensures thread
143 safety, and maintains cache semantics.
144
145 Note: this may be a blocking call when the artifact is already in the
146 process of being staged.
147
148 Args:
149 no_wait: If True, don't block waiting for artifact to exist if we fail to
150 immediately find it.
151
152 Raises:
153 ArtifactDownloadError: If the artifact fails to download from Google
154 Storage for any reason or that the regexp
155 defined by name is not specific enough.
156 """
157 if not self._process_lock:
158 self._process_lock = _build_artifact_locks.lock(
159 os.path.join(self.install_dir, self.name))
160
161 with self._process_lock:
162 common_util.MkDirP(self.install_dir)
163 if not self._ArtifactStaged():
164 # If the artifact should already have been uploaded, don't waste
165 # cycles waiting around for it to exist.
166 timeout = 1 if no_wait else 10
167 self._WaitForArtifactToExist(timeout)
168 self._Download()
joychen0a8e34e2013-06-24 17:58:36 -0700169 self._Setup()
Chris Sosa76e44b92013-01-31 12:11:38 -0800170 self._MarkArtifactStaged()
171 else:
172 self._Log('%s is already staged.', self)
173
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700174 def __str__(self):
175 """String representation for the download."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800176 return '->'.join(['%s/%s' % (self.archive_url, self.name),
joychen0a8e34e2013-06-24 17:58:36 -0700177 self.install_dir])
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700178
179
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700180class AUTestPayloadBuildArtifact(BuildArtifact):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700181 """Wrapper for AUTest delta payloads which need additional setup."""
joychen0a8e34e2013-06-24 17:58:36 -0700182 def _Setup(self):
183 super(AUTestPayloadBuildArtifact, self)._Setup()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700184
Chris Sosa76e44b92013-01-31 12:11:38 -0800185 # Rename to update.gz.
186 install_path = os.path.join(self.install_dir, self.name)
joychen3cb228e2013-06-12 12:13:13 -0700187 new_install_path = os.path.join(self.install_dir,
joychen7c2054a2013-07-25 11:14:07 -0700188 devserver_constants.UPDATE_FILE)
Chris Sosa76e44b92013-01-31 12:11:38 -0800189 shutil.move(install_path, new_install_path)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700190
191
Chris Sosa76e44b92013-01-31 12:11:38 -0800192# TODO(sosa): Change callers to make this artifact more sane.
193class DeltaPayloadsArtifact(BuildArtifact):
194 """Delta payloads from the archive_url.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700195
Chris Sosa76e44b92013-01-31 12:11:38 -0800196 This artifact is super strange. It custom handles directories and
197 pulls in all delta payloads. We can't specify exactly what we want
198 because unlike other artifacts, this one does not conform to something a
199 client might know. The client doesn't know the version of n-1 or whether it
200 was even generated.
201 """
202 def __init__(self, *args):
203 super(DeltaPayloadsArtifact, self).__init__(*args)
204 self.single_name = False # Expect multiple deltas
205 nton_name = 'chromeos_%s%s' % (self.build, self.name)
206 mton_name = 'chromeos_(?!%s)%s' % (self.build, self.name)
207 nton_install_dir = os.path.join(self.install_dir, _AU_BASE,
208 self.build + _NTON_DIR_SUFFIX)
209 mton_install_dir = os.path.join(self.install_dir, _AU_BASE,
210 self.build + _MTON_DIR_SUFFIX)
211 self._sub_artifacts = [
212 AUTestPayloadBuildArtifact(mton_install_dir, self.archive_url,
213 mton_name, self.build),
214 AUTestPayloadBuildArtifact(nton_install_dir, self.archive_url,
215 nton_name, self.build)]
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700216
Chris Sosa76e44b92013-01-31 12:11:38 -0800217 def _Download(self):
joychen0a8e34e2013-06-24 17:58:36 -0700218 """With sub-artifacts we do everything in _Setup()."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800219 pass
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700220
joychen0a8e34e2013-06-24 17:58:36 -0700221 def _Setup(self):
Chris Sosa76e44b92013-01-31 12:11:38 -0800222 """Process each sub-artifact. Only error out if none can be found."""
223 for artifact in self._sub_artifacts:
224 try:
225 artifact.Process(no_wait=True)
226 # Setup symlink so that AU will work for this payload.
227 os.symlink(
228 os.path.join(os.pardir, os.pardir, STATEFUL_UPDATE_FILE),
229 os.path.join(artifact.install_dir, STATEFUL_UPDATE_FILE))
230 except ArtifactDownloadError as e:
231 self._Log('Could not process %s: %s', artifact, e)
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700232
Chris Sosa76e44b92013-01-31 12:11:38 -0800233
234class BundledBuildArtifact(BuildArtifact):
235 """A single build artifact bundle e.g. zip file or tar file."""
236 def __init__(self, install_dir, archive_url, name, build,
237 files_to_extract=None, exclude=None):
238 """Takes BuildArtifacts are with two additional args.
239
240 Additional args:
241 files_to_extract: A list of files to extract. If set to None, extract
242 all files.
243 exclude: A list of files to exclude. If None, no files are excluded.
244 """
245 super(BundledBuildArtifact, self).__init__(install_dir, archive_url, name,
246 build)
247 self._files_to_extract = files_to_extract
248 self._exclude = exclude
249
250 # We modify the marker so that it is unique to what was staged.
251 if files_to_extract:
252 self.marker_name = self._SanitizeName(
253 '_'.join(['.' + self.name] + files_to_extract))
254
255 def _Extract(self):
256 """Extracts the bundle into install_dir. Must be overridden.
257
258 If set, uses files_to_extract to only extract those items. If set, use
259 exclude to exclude specific files.
260 """
261 raise NotImplementedError()
262
joychen0a8e34e2013-06-24 17:58:36 -0700263 def _Setup(self):
Chris Sosa76e44b92013-01-31 12:11:38 -0800264 self._Extract()
265
266
267class TarballBuildArtifact(BundledBuildArtifact):
268 """Artifact for tar and tarball files."""
269
270 def _Extract(self):
271 """Extracts a tarball using tar.
272
273 Detects whether the tarball is compressed or not based on the file
274 extension and extracts the tarball into the install_path.
275 """
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700276 try:
joychen0a8e34e2013-06-24 17:58:36 -0700277 common_util.ExtractTarball(self.install_path, self.install_dir,
Simran Basi4baad082013-02-14 13:39:18 -0800278 files_to_extract=self._files_to_extract,
279 excluded_files=self._exclude)
280 except common_util.CommonUtilError as e:
281 raise ArtifactDownloadError(str(e))
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700282
283
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700284class AutotestTarballBuildArtifact(TarballBuildArtifact):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700285 """Wrapper around the autotest tarball to download from gsutil."""
286
joychen0a8e34e2013-06-24 17:58:36 -0700287 def _Setup(self):
Chris Sosa76e44b92013-01-31 12:11:38 -0800288 """Extracts the tarball into the install path excluding test suites."""
joychen0a8e34e2013-06-24 17:58:36 -0700289 super(AutotestTarballBuildArtifact, self)._Setup()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700290
Chris Sosa76e44b92013-01-31 12:11:38 -0800291 # Deal with older autotest packages that may not be bundled.
joychen3cb228e2013-06-12 12:13:13 -0700292 autotest_dir = os.path.join(self.install_dir,
293 devserver_constants.AUTOTEST_DIR)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700294 autotest_pkgs_dir = os.path.join(autotest_dir, 'packages')
295 if not os.path.exists(autotest_pkgs_dir):
296 os.makedirs(autotest_pkgs_dir)
297
298 if not os.path.exists(os.path.join(autotest_pkgs_dir, 'packages.checksum')):
Chris Sosa76e44b92013-01-31 12:11:38 -0800299 cmd = ['autotest/utils/packager.py', 'upload', '--repository',
300 autotest_pkgs_dir, '--all']
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700301 try:
joychen0a8e34e2013-06-24 17:58:36 -0700302 subprocess.check_call(cmd, cwd=self.install_dir)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700303 except subprocess.CalledProcessError, e:
Chris Sosa76e44b92013-01-31 12:11:38 -0800304 raise ArtifactDownloadError(
305 'Failed to create autotest packages!:\n%s' % e)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700306 else:
Gilad Arnoldf5843132012-09-25 00:31:20 -0700307 self._Log('Using pre-generated packages from autotest')
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700308
Chris Masone816e38c2012-05-02 12:22:36 -0700309
Chris Sosa76e44b92013-01-31 12:11:38 -0800310class ZipfileBuildArtifact(BundledBuildArtifact):
311 """A downloadable artifact that is a zipfile."""
Chris Masone816e38c2012-05-02 12:22:36 -0700312
Chris Sosa76e44b92013-01-31 12:11:38 -0800313 def _Extract(self):
314 """Extracts files into the install path."""
315 # Unzip is weird. It expects its args before any excepts and expects its
316 # excepts in a list following the -x.
joychen0a8e34e2013-06-24 17:58:36 -0700317 cmd = ['unzip', '-o', self.install_path, '-d', self.install_dir]
Chris Sosa76e44b92013-01-31 12:11:38 -0800318 if self._files_to_extract:
319 cmd.extend(self._files_to_extract)
Chris Masone816e38c2012-05-02 12:22:36 -0700320
Chris Sosa76e44b92013-01-31 12:11:38 -0800321 if self._exclude:
322 cmd.append('-x')
323 cmd.extend(self._exclude)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700324
325 try:
Chris Sosa76e44b92013-01-31 12:11:38 -0800326 subprocess.check_call(cmd)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700327 except subprocess.CalledProcessError, e:
Chris Sosa76e44b92013-01-31 12:11:38 -0800328 raise ArtifactDownloadError(
329 'An error occurred when attempting to unzip %s:\n%s' %
joychen0a8e34e2013-06-24 17:58:36 -0700330 (self.install_path, e))
Gilad Arnold6f99b982012-09-12 10:49:40 -0700331
Gilad Arnold6f99b982012-09-12 10:49:40 -0700332
Chris Sosa76e44b92013-01-31 12:11:38 -0800333class ImplDescription(object):
334 """Data wrapper that describes an artifact's implementation."""
335 def __init__(self, artifact_class, name, *additional_args):
336 """Constructor:
337
338 Args:
339 artifact_class: BuildArtifact class to use for the artifact.
340 name: name to use to identify artifact (see BuildArtifact.name)
341 additional_args: If sub-class uses additional args, these are passed
342 through to them.
343 """
344 self.artifact_class = artifact_class
345 self.name = name
346 self.additional_args = additional_args
347
348
349# Maps artifact names to their implementation description.
350# Please note, it is good practice to use constants for these names if you're
351# going to re-use the names ANYWHERE else in the devserver code.
352ARTIFACT_IMPLEMENTATION_MAP = {
353 artifact_info.FULL_PAYLOAD:
354 ImplDescription(AUTestPayloadBuildArtifact, '.*_full_.*'),
355 artifact_info.DELTA_PAYLOADS:
356 ImplDescription(DeltaPayloadsArtifact, '.*_delta_.*'),
357 artifact_info.STATEFUL_PAYLOAD:
358 ImplDescription(BuildArtifact, STATEFUL_UPDATE_FILE),
359
360 artifact_info.BASE_IMAGE:
361 ImplDescription(ZipfileBuildArtifact, IMAGE_FILE,
joychen921e1fb2013-06-28 11:12:20 -0700362 [devserver_constants.BASE_IMAGE_FILE]),
Chris Sosa76e44b92013-01-31 12:11:38 -0800363 artifact_info.RECOVERY_IMAGE:
joychen921e1fb2013-06-28 11:12:20 -0700364 ImplDescription(ZipfileBuildArtifact, IMAGE_FILE,
365 [devserver_constants.RECOVERY_IMAGE_FILE]),
Chris Sosa76e44b92013-01-31 12:11:38 -0800366 artifact_info.TEST_IMAGE:
joychen921e1fb2013-06-28 11:12:20 -0700367 ImplDescription(ZipfileBuildArtifact, IMAGE_FILE,
368 [devserver_constants.TEST_IMAGE_FILE]),
Chris Sosa76e44b92013-01-31 12:11:38 -0800369
370 artifact_info.AUTOTEST:
371 ImplDescription(AutotestTarballBuildArtifact, AUTOTEST_FILE, None,
372 ['autotest/test_suites']),
373 artifact_info.TEST_SUITES:
374 ImplDescription(TarballBuildArtifact, TEST_SUITES_FILE),
375 artifact_info.AU_SUITE:
Chris Sosaa56c4032013-03-17 21:59:54 -0700376 ImplDescription(TarballBuildArtifact, AU_SUITE_FILE),
Chris Sosa5d1b0792013-07-31 10:54:52 -0700377 artifact_info.PAYGEN_AU_SUITE:
378 ImplDescription(TarballBuildArtifact, PAYGEN_AU_SUITE_FILE),
Chris Sosa76e44b92013-01-31 12:11:38 -0800379
380 artifact_info.FIRMWARE:
381 ImplDescription(BuildArtifact, FIRMWARE_FILE),
382 artifact_info.SYMBOLS:
383 ImplDescription(TarballBuildArtifact, DEBUG_SYMBOLS_FILE,
384 ['debug/breakpad']),
385}
386
387
388class ArtifactFactory(object):
389 """A factory class that generates build artifacts from artifact names."""
390
joychen0a8e34e2013-06-24 17:58:36 -0700391 def __init__(self, download_dir, archive_url, artifact_names, build):
Chris Sosa76e44b92013-01-31 12:11:38 -0800392 """Initalizes the member variables for the factory.
393
394 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -0800395 archive_url: the Google Storage url of the bucket where the debug
396 symbols for the desired build are stored.
397 artifact_names: List of artifact names to stage.
398 build: The name of the build.
399 """
joychen0a8e34e2013-06-24 17:58:36 -0700400 self.download_dir = download_dir
Chris Sosa76e44b92013-01-31 12:11:38 -0800401 self.archive_url = archive_url
402 self.artifact_names = artifact_names
403 self.build = build
404
405 @staticmethod
406 def _GetDescriptionComponents(artifact_name):
407 """Returns a tuple of for BuildArtifact class, name, and additional args."""
408 description = ARTIFACT_IMPLEMENTATION_MAP[artifact_name]
409 return (description.artifact_class, description.name,
410 description.additional_args)
411
412 def _Artifacts(self, artifact_names):
413 """Returns an iterable of BuildArtifacts from |artifact_names|."""
414 artifacts = []
415 for artifact_name in artifact_names:
416 artifact_class, path, args = self._GetDescriptionComponents(
417 artifact_name)
joychen0a8e34e2013-06-24 17:58:36 -0700418 artifacts.append(artifact_class(self.download_dir, self.archive_url, path,
Chris Sosa76e44b92013-01-31 12:11:38 -0800419 self.build, *args))
420
421 return artifacts
422
423 def RequiredArtifacts(self):
424 """Returns an iterable of BuildArtifacts for the factory's artifacts."""
425 return self._Artifacts(self.artifact_names)
426
427 def OptionalArtifacts(self):
428 """Returns an iterable of BuildArtifacts that should be cached."""
429 optional_names = set()
430 for artifact_name, optional_list in (
431 artifact_info.REQUESTED_TO_OPTIONAL_MAP.iteritems()):
432 # We are already downloading it.
433 if artifact_name in self.artifact_names:
434 optional_names = optional_names.union(optional_list)
435
436 return self._Artifacts(optional_names - set(self.artifact_names))