blob: e4511531668b31961f9ce53292b2005ea05e19d6 [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'
joychen3cb228e2013-06-12 12:13:13 -070027BASE_IMAGE_FILE = 'chromiumos_base_image.bin'
Chris Sosa76e44b92013-01-31 12:11:38 -080028DEBUG_SYMBOLS_FILE = 'debug.tgz'
29FIRMWARE_FILE = 'firmware_from_source.tar.bz2'
30IMAGE_FILE = 'image.zip'
joychen3cb228e2013-06-12 12:13:13 -070031RECOVERY_IMAGE_FILE = 'recovery_image.bin'
Chris Sosa76e44b92013-01-31 12:11:38 -080032STATEFUL_UPDATE_FILE = 'stateful.tgz'
33TEST_IMAGE_FILE = 'chromiumos_test_image.bin'
34TEST_SUITES_FILE = 'test_suites.tar.bz2'
35
36_build_artifact_locks = common_util.LockDict()
Chris Sosa47a7d4e2012-03-28 11:26:55 -070037
38
39class ArtifactDownloadError(Exception):
40 """Error used to signify an issue processing an artifact."""
41 pass
42
43
Gilad Arnoldc65330c2012-09-20 15:17:48 -070044class BuildArtifact(log_util.Loggable):
Chris Sosa47a7d4e2012-03-28 11:26:55 -070045 """Wrapper around an artifact to download from gsutil.
46
47 The purpose of this class is to download objects from Google Storage
48 and install them to a local directory. There are two main functions, one to
49 download/prepare the artifacts in to a temporary staging area and the second
50 to stage it into its final destination.
Chris Sosa76e44b92013-01-31 12:11:38 -080051
52 Class members:
53 archive_url = archive_url
54 name: Name given for artifact -- either a regexp or name of the artifact in
55 gs. If a regexp, is modified to actual name before call to _Download.
56 build: The version of the build i.e. R26-2342.0.0.
57 marker_name: Name used to define the lock marker for the artifacts to
58 prevent it from being re-downloaded. By default based on name
59 but can be overriden by children.
joychen0a8e34e2013-06-24 17:58:36 -070060 install_path: Path to artifact.
Chris Sosa76e44b92013-01-31 12:11:38 -080061 install_dir: The final location where the artifact should be staged to.
62 single_name: If True the name given should only match one item. Note, if not
63 True, self.name will become a list of items returned.
Chris Sosa47a7d4e2012-03-28 11:26:55 -070064 """
Chris Sosa76e44b92013-01-31 12:11:38 -080065 def __init__(self, install_dir, archive_url, name, build):
Chris Sosa47a7d4e2012-03-28 11:26:55 -070066 """Args:
Chris Sosa76e44b92013-01-31 12:11:38 -080067 install_dir: Where to install the artifact.
68 archive_url: The Google Storage path to find the artifact.
69 name: Identifying name to be used to find/store the artifact.
70 build: The name of the build e.g. board/release.
Chris Sosa47a7d4e2012-03-28 11:26:55 -070071 """
Chris Sosa6a3697f2013-01-29 16:44:43 -080072 super(BuildArtifact, self).__init__()
Chris Sosa47a7d4e2012-03-28 11:26:55 -070073
Chris Sosa76e44b92013-01-31 12:11:38 -080074 # In-memory lock to keep the devserver from colliding with itself while
75 # attempting to stage the same artifact.
76 self._process_lock = None
Chris Sosa47a7d4e2012-03-28 11:26:55 -070077
Chris Sosa76e44b92013-01-31 12:11:38 -080078 self.archive_url = archive_url
79 self.name = name
80 self.build = build
Chris Sosa47a7d4e2012-03-28 11:26:55 -070081
Chris Sosa76e44b92013-01-31 12:11:38 -080082 self.marker_name = '.' + self._SanitizeName(name)
Chris Sosa47a7d4e2012-03-28 11:26:55 -070083
joychen0a8e34e2013-06-24 17:58:36 -070084 self.install_path = None
Chris Sosa47a7d4e2012-03-28 11:26:55 -070085
Chris Sosa76e44b92013-01-31 12:11:38 -080086 self.install_dir = install_dir
87
88 self.single_name = True
89
90 @staticmethod
91 def _SanitizeName(name):
92 """Sanitizes name to be used for creating a file on the filesystem.
93
94 '.','/' and '*' have special meaning in FS lingo. Replace them with words.
95 """
96 return name.replace('*', 'STAR').replace('.', 'DOT').replace('/', 'SLASH')
97
98 def _ArtifactStaged(self):
99 """Returns True if artifact is already staged."""
100 return os.path.exists(os.path.join(self.install_dir, self.marker_name))
101
102 def _MarkArtifactStaged(self):
103 """Marks the artifact as staged."""
104 with open(os.path.join(self.install_dir, self.marker_name), 'w') as f:
105 f.write('')
106
107 def _WaitForArtifactToExist(self, timeout):
108 """Waits for artifact to exist and sets self.name to appropriate name."""
109 names = gsutil_util.GetGSNamesWithWait(
110 self.name, self.archive_url, str(self), single_item=self.single_name,
111 timeout=timeout)
112 if not names:
113 raise ArtifactDownloadError('Could not find %s in Google Storage' %
114 self.name)
115
116 if self.single_name:
117 if len(names) > 1:
118 raise ArtifactDownloadError('Too many artifacts match %s' % self.name)
119
120 self.name = names[0]
121 else:
122 self.name = names
123
124 def _Download(self):
joychen0a8e34e2013-06-24 17:58:36 -0700125 """Downloads artifact from Google Storage to a local directory."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800126 gs_path = '/'.join([self.archive_url, self.name])
joychen0a8e34e2013-06-24 17:58:36 -0700127 self.install_path = os.path.join(self.install_dir, self.name)
128 gsutil_util.DownloadFromGS(gs_path, self.install_path)
Chris Sosa76e44b92013-01-31 12:11:38 -0800129
joychen0a8e34e2013-06-24 17:58:36 -0700130 def _Setup(self):
131 """For tarball like artifacts, extracts and prepares contents."""
132 pass
133
Chris Sosa76e44b92013-01-31 12:11:38 -0800134
135 def Process(self, no_wait):
136 """Main call point to all artifacts. Downloads and Stages artifact.
137
138 Downloads and Stages artifact from Google Storage to the install directory
139 specified in the constructor. It multi-thread safe and does not overwrite
140 the artifact if it's already been downloaded or being downloaded. After
141 processing, leaves behind a marker to indicate to future invocations that
142 the artifact has already been staged based on the name of the artifact.
143
144 Do not override as it modifies important private variables, ensures thread
145 safety, and maintains cache semantics.
146
147 Note: this may be a blocking call when the artifact is already in the
148 process of being staged.
149
150 Args:
151 no_wait: If True, don't block waiting for artifact to exist if we fail to
152 immediately find it.
153
154 Raises:
155 ArtifactDownloadError: If the artifact fails to download from Google
156 Storage for any reason or that the regexp
157 defined by name is not specific enough.
158 """
159 if not self._process_lock:
160 self._process_lock = _build_artifact_locks.lock(
161 os.path.join(self.install_dir, self.name))
162
163 with self._process_lock:
164 common_util.MkDirP(self.install_dir)
165 if not self._ArtifactStaged():
166 # If the artifact should already have been uploaded, don't waste
167 # cycles waiting around for it to exist.
168 timeout = 1 if no_wait else 10
169 self._WaitForArtifactToExist(timeout)
170 self._Download()
joychen0a8e34e2013-06-24 17:58:36 -0700171 self._Setup()
Chris Sosa76e44b92013-01-31 12:11:38 -0800172 self._MarkArtifactStaged()
173 else:
174 self._Log('%s is already staged.', self)
175
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700176 def __str__(self):
177 """String representation for the download."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800178 return '->'.join(['%s/%s' % (self.archive_url, self.name),
joychen0a8e34e2013-06-24 17:58:36 -0700179 self.install_dir])
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700180
181
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700182class AUTestPayloadBuildArtifact(BuildArtifact):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700183 """Wrapper for AUTest delta payloads which need additional setup."""
joychen0a8e34e2013-06-24 17:58:36 -0700184 def _Setup(self):
185 super(AUTestPayloadBuildArtifact, self)._Setup()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700186
Chris Sosa76e44b92013-01-31 12:11:38 -0800187 # Rename to update.gz.
188 install_path = os.path.join(self.install_dir, self.name)
joychen3cb228e2013-06-12 12:13:13 -0700189 new_install_path = os.path.join(self.install_dir,
190 devserver_constants.ROOT_UPDATE_FILE)
Chris Sosa76e44b92013-01-31 12:11:38 -0800191 shutil.move(install_path, new_install_path)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700192
193
Chris Sosa76e44b92013-01-31 12:11:38 -0800194# TODO(sosa): Change callers to make this artifact more sane.
195class DeltaPayloadsArtifact(BuildArtifact):
196 """Delta payloads from the archive_url.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700197
Chris Sosa76e44b92013-01-31 12:11:38 -0800198 This artifact is super strange. It custom handles directories and
199 pulls in all delta payloads. We can't specify exactly what we want
200 because unlike other artifacts, this one does not conform to something a
201 client might know. The client doesn't know the version of n-1 or whether it
202 was even generated.
203 """
204 def __init__(self, *args):
205 super(DeltaPayloadsArtifact, self).__init__(*args)
206 self.single_name = False # Expect multiple deltas
207 nton_name = 'chromeos_%s%s' % (self.build, self.name)
208 mton_name = 'chromeos_(?!%s)%s' % (self.build, self.name)
209 nton_install_dir = os.path.join(self.install_dir, _AU_BASE,
210 self.build + _NTON_DIR_SUFFIX)
211 mton_install_dir = os.path.join(self.install_dir, _AU_BASE,
212 self.build + _MTON_DIR_SUFFIX)
213 self._sub_artifacts = [
214 AUTestPayloadBuildArtifact(mton_install_dir, self.archive_url,
215 mton_name, self.build),
216 AUTestPayloadBuildArtifact(nton_install_dir, self.archive_url,
217 nton_name, self.build)]
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700218
Chris Sosa76e44b92013-01-31 12:11:38 -0800219 def _Download(self):
joychen0a8e34e2013-06-24 17:58:36 -0700220 """With sub-artifacts we do everything in _Setup()."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800221 pass
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700222
joychen0a8e34e2013-06-24 17:58:36 -0700223 def _Setup(self):
Chris Sosa76e44b92013-01-31 12:11:38 -0800224 """Process each sub-artifact. Only error out if none can be found."""
225 for artifact in self._sub_artifacts:
226 try:
227 artifact.Process(no_wait=True)
228 # Setup symlink so that AU will work for this payload.
229 os.symlink(
230 os.path.join(os.pardir, os.pardir, STATEFUL_UPDATE_FILE),
231 os.path.join(artifact.install_dir, STATEFUL_UPDATE_FILE))
232 except ArtifactDownloadError as e:
233 self._Log('Could not process %s: %s', artifact, e)
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700234
Chris Sosa76e44b92013-01-31 12:11:38 -0800235
236class BundledBuildArtifact(BuildArtifact):
237 """A single build artifact bundle e.g. zip file or tar file."""
238 def __init__(self, install_dir, archive_url, name, build,
239 files_to_extract=None, exclude=None):
240 """Takes BuildArtifacts are with two additional args.
241
242 Additional args:
243 files_to_extract: A list of files to extract. If set to None, extract
244 all files.
245 exclude: A list of files to exclude. If None, no files are excluded.
246 """
247 super(BundledBuildArtifact, self).__init__(install_dir, archive_url, name,
248 build)
249 self._files_to_extract = files_to_extract
250 self._exclude = exclude
251
252 # We modify the marker so that it is unique to what was staged.
253 if files_to_extract:
254 self.marker_name = self._SanitizeName(
255 '_'.join(['.' + self.name] + files_to_extract))
256
257 def _Extract(self):
258 """Extracts the bundle into install_dir. Must be overridden.
259
260 If set, uses files_to_extract to only extract those items. If set, use
261 exclude to exclude specific files.
262 """
263 raise NotImplementedError()
264
joychen0a8e34e2013-06-24 17:58:36 -0700265 def _Setup(self):
Chris Sosa76e44b92013-01-31 12:11:38 -0800266 self._Extract()
267
268
269class TarballBuildArtifact(BundledBuildArtifact):
270 """Artifact for tar and tarball files."""
271
272 def _Extract(self):
273 """Extracts a tarball using tar.
274
275 Detects whether the tarball is compressed or not based on the file
276 extension and extracts the tarball into the install_path.
277 """
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700278 try:
joychen0a8e34e2013-06-24 17:58:36 -0700279 common_util.ExtractTarball(self.install_path, self.install_dir,
Simran Basi4baad082013-02-14 13:39:18 -0800280 files_to_extract=self._files_to_extract,
281 excluded_files=self._exclude)
282 except common_util.CommonUtilError as e:
283 raise ArtifactDownloadError(str(e))
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700284
285
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700286class AutotestTarballBuildArtifact(TarballBuildArtifact):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700287 """Wrapper around the autotest tarball to download from gsutil."""
288
joychen0a8e34e2013-06-24 17:58:36 -0700289 def _Setup(self):
Chris Sosa76e44b92013-01-31 12:11:38 -0800290 """Extracts the tarball into the install path excluding test suites."""
joychen0a8e34e2013-06-24 17:58:36 -0700291 super(AutotestTarballBuildArtifact, self)._Setup()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700292
Chris Sosa76e44b92013-01-31 12:11:38 -0800293 # Deal with older autotest packages that may not be bundled.
joychen3cb228e2013-06-12 12:13:13 -0700294 autotest_dir = os.path.join(self.install_dir,
295 devserver_constants.AUTOTEST_DIR)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700296 autotest_pkgs_dir = os.path.join(autotest_dir, 'packages')
297 if not os.path.exists(autotest_pkgs_dir):
298 os.makedirs(autotest_pkgs_dir)
299
300 if not os.path.exists(os.path.join(autotest_pkgs_dir, 'packages.checksum')):
Chris Sosa76e44b92013-01-31 12:11:38 -0800301 cmd = ['autotest/utils/packager.py', 'upload', '--repository',
302 autotest_pkgs_dir, '--all']
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700303 try:
joychen0a8e34e2013-06-24 17:58:36 -0700304 subprocess.check_call(cmd, cwd=self.install_dir)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700305 except subprocess.CalledProcessError, e:
Chris Sosa76e44b92013-01-31 12:11:38 -0800306 raise ArtifactDownloadError(
307 'Failed to create autotest packages!:\n%s' % e)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700308 else:
Gilad Arnoldf5843132012-09-25 00:31:20 -0700309 self._Log('Using pre-generated packages from autotest')
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700310
Chris Masone816e38c2012-05-02 12:22:36 -0700311
Chris Sosa76e44b92013-01-31 12:11:38 -0800312class ZipfileBuildArtifact(BundledBuildArtifact):
313 """A downloadable artifact that is a zipfile."""
Chris Masone816e38c2012-05-02 12:22:36 -0700314
Chris Sosa76e44b92013-01-31 12:11:38 -0800315 def _Extract(self):
316 """Extracts files into the install path."""
317 # Unzip is weird. It expects its args before any excepts and expects its
318 # excepts in a list following the -x.
joychen0a8e34e2013-06-24 17:58:36 -0700319 cmd = ['unzip', '-o', self.install_path, '-d', self.install_dir]
Chris Sosa76e44b92013-01-31 12:11:38 -0800320 if self._files_to_extract:
321 cmd.extend(self._files_to_extract)
Chris Masone816e38c2012-05-02 12:22:36 -0700322
Chris Sosa76e44b92013-01-31 12:11:38 -0800323 if self._exclude:
324 cmd.append('-x')
325 cmd.extend(self._exclude)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700326
327 try:
Chris Sosa76e44b92013-01-31 12:11:38 -0800328 subprocess.check_call(cmd)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700329 except subprocess.CalledProcessError, e:
Chris Sosa76e44b92013-01-31 12:11:38 -0800330 raise ArtifactDownloadError(
331 'An error occurred when attempting to unzip %s:\n%s' %
joychen0a8e34e2013-06-24 17:58:36 -0700332 (self.install_path, e))
Gilad Arnold6f99b982012-09-12 10:49:40 -0700333
Gilad Arnold6f99b982012-09-12 10:49:40 -0700334
Chris Sosa76e44b92013-01-31 12:11:38 -0800335class ImplDescription(object):
336 """Data wrapper that describes an artifact's implementation."""
337 def __init__(self, artifact_class, name, *additional_args):
338 """Constructor:
339
340 Args:
341 artifact_class: BuildArtifact class to use for the artifact.
342 name: name to use to identify artifact (see BuildArtifact.name)
343 additional_args: If sub-class uses additional args, these are passed
344 through to them.
345 """
346 self.artifact_class = artifact_class
347 self.name = name
348 self.additional_args = additional_args
349
350
351# Maps artifact names to their implementation description.
352# Please note, it is good practice to use constants for these names if you're
353# going to re-use the names ANYWHERE else in the devserver code.
354ARTIFACT_IMPLEMENTATION_MAP = {
355 artifact_info.FULL_PAYLOAD:
356 ImplDescription(AUTestPayloadBuildArtifact, '.*_full_.*'),
357 artifact_info.DELTA_PAYLOADS:
358 ImplDescription(DeltaPayloadsArtifact, '.*_delta_.*'),
359 artifact_info.STATEFUL_PAYLOAD:
360 ImplDescription(BuildArtifact, STATEFUL_UPDATE_FILE),
361
362 artifact_info.BASE_IMAGE:
363 ImplDescription(ZipfileBuildArtifact, IMAGE_FILE,
joychen3cb228e2013-06-12 12:13:13 -0700364 [BASE_IMAGE_FILE]),
Chris Sosa76e44b92013-01-31 12:11:38 -0800365 artifact_info.RECOVERY_IMAGE:
joychen3cb228e2013-06-12 12:13:13 -0700366 ImplDescription(ZipfileBuildArtifact, IMAGE_FILE, [RECOVERY_IMAGE_FILE]),
Chris Sosa76e44b92013-01-31 12:11:38 -0800367 artifact_info.TEST_IMAGE:
368 ImplDescription(ZipfileBuildArtifact, IMAGE_FILE, [TEST_IMAGE_FILE]),
369
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 Sosa76e44b92013-01-31 12:11:38 -0800377
378 artifact_info.FIRMWARE:
379 ImplDescription(BuildArtifact, FIRMWARE_FILE),
380 artifact_info.SYMBOLS:
381 ImplDescription(TarballBuildArtifact, DEBUG_SYMBOLS_FILE,
382 ['debug/breakpad']),
383}
384
385
386class ArtifactFactory(object):
387 """A factory class that generates build artifacts from artifact names."""
388
joychen0a8e34e2013-06-24 17:58:36 -0700389 def __init__(self, download_dir, archive_url, artifact_names, build):
Chris Sosa76e44b92013-01-31 12:11:38 -0800390 """Initalizes the member variables for the factory.
391
392 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -0800393 archive_url: the Google Storage url of the bucket where the debug
394 symbols for the desired build are stored.
395 artifact_names: List of artifact names to stage.
396 build: The name of the build.
397 """
joychen0a8e34e2013-06-24 17:58:36 -0700398 self.download_dir = download_dir
Chris Sosa76e44b92013-01-31 12:11:38 -0800399 self.archive_url = archive_url
400 self.artifact_names = artifact_names
401 self.build = build
402
403 @staticmethod
404 def _GetDescriptionComponents(artifact_name):
405 """Returns a tuple of for BuildArtifact class, name, and additional args."""
406 description = ARTIFACT_IMPLEMENTATION_MAP[artifact_name]
407 return (description.artifact_class, description.name,
408 description.additional_args)
409
410 def _Artifacts(self, artifact_names):
411 """Returns an iterable of BuildArtifacts from |artifact_names|."""
412 artifacts = []
413 for artifact_name in artifact_names:
414 artifact_class, path, args = self._GetDescriptionComponents(
415 artifact_name)
joychen0a8e34e2013-06-24 17:58:36 -0700416 artifacts.append(artifact_class(self.download_dir, self.archive_url, path,
Chris Sosa76e44b92013-01-31 12:11:38 -0800417 self.build, *args))
418
419 return artifacts
420
421 def RequiredArtifacts(self):
422 """Returns an iterable of BuildArtifacts for the factory's artifacts."""
423 return self._Artifacts(self.artifact_names)
424
425 def OptionalArtifacts(self):
426 """Returns an iterable of BuildArtifacts that should be cached."""
427 optional_names = set()
428 for artifact_name, optional_list in (
429 artifact_info.REQUESTED_TO_OPTIONAL_MAP.iteritems()):
430 # We are already downloading it.
431 if artifact_name in self.artifact_names:
432 optional_names = optional_names.union(optional_list)
433
434 return self._Artifacts(optional_names - set(self.artifact_names))