blob: 68fc72b3707281e2a5c34d577d85d36085204871 [file] [log] [blame]
Gilad Arnold950569b2013-08-27 14:38:01 -07001#!/usr/bin/python
Chris Sosa968a1062013-08-02 17:42:50 -07002
Chris Sosa47a7d4e2012-03-28 11:26:55 -07003# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""Module containing classes that wrap artifact downloads."""
8
Chris Sosa47a7d4e2012-03-28 11:26:55 -07009import os
Dan Shi6e50c722013-08-19 15:05:06 -070010import pickle
Gilad Arnold950569b2013-08-27 14:38:01 -070011import re
Chris Sosa47a7d4e2012-03-28 11:26:55 -070012import shutil
13import subprocess
14
Chris Sosa76e44b92013-01-31 12:11:38 -080015import artifact_info
16import common_util
joychen3cb228e2013-06-12 12:13:13 -070017import devserver_constants
Chris Sosa47a7d4e2012-03-28 11:26:55 -070018import gsutil_util
Gilad Arnoldc65330c2012-09-20 15:17:48 -070019import log_util
Chris Sosa47a7d4e2012-03-28 11:26:55 -070020
21
Chris Sosa76e44b92013-01-31 12:11:38 -080022_AU_BASE = 'au'
23_NTON_DIR_SUFFIX = '_nton'
24_MTON_DIR_SUFFIX = '_mton'
25
26############ Actual filenames of artifacts in Google Storage ############
27
28AU_SUITE_FILE = 'au_control.tar.bz2'
Chris Sosa968a1062013-08-02 17:42:50 -070029PAYGEN_AU_SUITE_FILE_TEMPLATE = 'paygen_au_%(channel)s_control.tar.bz2'
Chris Sosa76e44b92013-01-31 12:11:38 -080030AUTOTEST_FILE = 'autotest.tar'
31AUTOTEST_COMPRESSED_FILE = 'autotest.tar.bz2'
32DEBUG_SYMBOLS_FILE = 'debug.tgz'
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -080033FACTORY_FILE = 'ChromeOS-factory*.zip'
Chris Sosa76e44b92013-01-31 12:11:38 -080034FIRMWARE_FILE = 'firmware_from_source.tar.bz2'
35IMAGE_FILE = 'image.zip'
Chris Sosa76e44b92013-01-31 12:11:38 -080036TEST_SUITES_FILE = 'test_suites.tar.bz2'
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -080037BASE_IMAGE_FILE = 'chromiumos_base_image.tar.xz'
38TEST_IMAGE_FILE = 'chromiumos_test_image.tar.xz'
39RECOVERY_IMAGE_FILE = 'recovery_image.tar.xz'
40
Chris Sosa76e44b92013-01-31 12:11:38 -080041
42_build_artifact_locks = common_util.LockDict()
Chris Sosa47a7d4e2012-03-28 11:26:55 -070043
44
45class ArtifactDownloadError(Exception):
46 """Error used to signify an issue processing an artifact."""
47 pass
48
49
Gilad Arnoldc65330c2012-09-20 15:17:48 -070050class BuildArtifact(log_util.Loggable):
Chris Sosa47a7d4e2012-03-28 11:26:55 -070051 """Wrapper around an artifact to download from gsutil.
52
53 The purpose of this class is to download objects from Google Storage
54 and install them to a local directory. There are two main functions, one to
55 download/prepare the artifacts in to a temporary staging area and the second
56 to stage it into its final destination.
Chris Sosa76e44b92013-01-31 12:11:38 -080057
Gilad Arnold950569b2013-08-27 14:38:01 -070058 IMPORTANT! (i) `name' is a glob expression by default (and not a regex), be
59 attentive when adding new artifacts; (ii) name matching semantics differ
60 between a glob (full name string match) and a regex (partial match).
61
Chris Sosa76e44b92013-01-31 12:11:38 -080062 Class members:
Gilad Arnold950569b2013-08-27 14:38:01 -070063 archive_url: An archive URL.
64 name: Name given for artifact; in fact, it is a pattern that captures the
65 names of files contained in the artifact. This can either be an
66 ordinary shell-style glob (the default), or a regular expression (if
67 is_regex_name is True).
68 is_regex_name: Whether the name value is a regex (default: glob).
Chris Sosa76e44b92013-01-31 12:11:38 -080069 build: The version of the build i.e. R26-2342.0.0.
70 marker_name: Name used to define the lock marker for the artifacts to
71 prevent it from being re-downloaded. By default based on name
72 but can be overriden by children.
Dan Shi6e50c722013-08-19 15:05:06 -070073 exception_file_path: Path to a file containing the serialized exception,
74 which was raised in Process method. The file is located
75 in the parent folder of install_dir, since the
76 install_dir will be deleted if the build does not
77 existed.
joychen0a8e34e2013-06-24 17:58:36 -070078 install_path: Path to artifact.
Chris Sosa76e44b92013-01-31 12:11:38 -080079 install_dir: The final location where the artifact should be staged to.
80 single_name: If True the name given should only match one item. Note, if not
81 True, self.name will become a list of items returned.
Gilad Arnold1638d822013-11-07 23:38:16 -080082 installed_files: A list of files that were the final result of downloading
83 and setting up the artifact.
84 store_installed_files: Whether the list of installed files is stored in the
85 marker file.
Chris Sosa47a7d4e2012-03-28 11:26:55 -070086 """
Gilad Arnold950569b2013-08-27 14:38:01 -070087
88 def __init__(self, install_dir, archive_url, name, build,
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -080089 is_regex_name=False, optional_name=None):
Gilad Arnold950569b2013-08-27 14:38:01 -070090 """Constructor.
91
92 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -080093 install_dir: Where to install the artifact.
94 archive_url: The Google Storage path to find the artifact.
95 name: Identifying name to be used to find/store the artifact.
96 build: The name of the build e.g. board/release.
Gilad Arnold950569b2013-08-27 14:38:01 -070097 is_regex_name: Whether the name pattern is a regex (default: glob).
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -080098 optional_name: An alternative name to find the artifact, which can lead
99 to faster download. Unlike |name|, there is no guarantee that an
100 artifact named |optional_name| is/will be on Google Storage. If it
101 exists, we download it. Otherwise, we fall back to wait for |name|.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700102 """
Chris Sosa6a3697f2013-01-29 16:44:43 -0800103 super(BuildArtifact, self).__init__()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700104
Chris Sosa76e44b92013-01-31 12:11:38 -0800105 # In-memory lock to keep the devserver from colliding with itself while
106 # attempting to stage the same artifact.
107 self._process_lock = None
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700108
Chris Sosa76e44b92013-01-31 12:11:38 -0800109 self.archive_url = archive_url
110 self.name = name
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800111 self.optional_name = optional_name
Gilad Arnold950569b2013-08-27 14:38:01 -0700112 self.is_regex_name = is_regex_name
Chris Sosa76e44b92013-01-31 12:11:38 -0800113 self.build = build
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700114
Chris Sosa76e44b92013-01-31 12:11:38 -0800115 self.marker_name = '.' + self._SanitizeName(name)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700116
Dan Shi6e50c722013-08-19 15:05:06 -0700117 exception_file_name = ('.' + self._SanitizeName(build) + self.marker_name +
118 '.exception')
119 # The exception file needs to be located in parent folder, since the
120 # install_dir will be deleted is the build does not exist.
121 self.exception_file_path = os.path.join(os.path.dirname(install_dir),
Gilad Arnold950569b2013-08-27 14:38:01 -0700122 exception_file_name)
Dan Shi6e50c722013-08-19 15:05:06 -0700123
joychen0a8e34e2013-06-24 17:58:36 -0700124 self.install_path = None
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700125
Chris Sosa76e44b92013-01-31 12:11:38 -0800126 self.install_dir = install_dir
127
128 self.single_name = True
129
Gilad Arnold1638d822013-11-07 23:38:16 -0800130 self.installed_files = []
131 self.store_installed_files = True
132
Chris Sosa76e44b92013-01-31 12:11:38 -0800133 @staticmethod
134 def _SanitizeName(name):
135 """Sanitizes name to be used for creating a file on the filesystem.
136
137 '.','/' and '*' have special meaning in FS lingo. Replace them with words.
Gilad Arnold950569b2013-08-27 14:38:01 -0700138
139 Args:
140 name: A file name/path.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800141
Gilad Arnold950569b2013-08-27 14:38:01 -0700142 Returns:
143 The sanitized name/path.
Chris Sosa76e44b92013-01-31 12:11:38 -0800144 """
145 return name.replace('*', 'STAR').replace('.', 'DOT').replace('/', 'SLASH')
146
Dan Shif8eb0d12013-08-01 17:52:06 -0700147 def ArtifactStaged(self):
Gilad Arnold1638d822013-11-07 23:38:16 -0800148 """Returns True if artifact is already staged.
149
150 This checks for (1) presence of the artifact marker file, and (2) the
151 presence of each installed file listed in this marker. Both must hold for
152 the artifact to be considered staged. Note that this method is safe for use
153 even if the artifacts were not stageed by this instance, as it is assumed
154 that any BuildArtifact instance that did the staging wrote the list of
155 files actually installed into the marker.
156 """
157 marker_file = os.path.join(self.install_dir, self.marker_name)
158
159 # If the marker is missing, it's definitely not staged.
160 if not os.path.exists(marker_file):
161 return False
162
163 # We want to ensure that every file listed in the marker is actually there.
164 if self.store_installed_files:
165 with open(marker_file) as f:
166 files = [line.strip() for line in f]
167
168 # Check to see if any of the purportedly installed files are missing, in
169 # which case the marker is outdated and should be removed.
170 missing_files = [fname for fname in files if not os.path.exists(fname)]
171 if missing_files:
172 self._Log('***ATTENTION*** %s files listed in %s are missing:\n%s',
173 'All' if len(files) == len(missing_files) else 'Some',
174 marker_file, '\n'.join(missing_files))
175 os.remove(marker_file)
176 return False
177
178 return True
Chris Sosa76e44b92013-01-31 12:11:38 -0800179
180 def _MarkArtifactStaged(self):
181 """Marks the artifact as staged."""
182 with open(os.path.join(self.install_dir, self.marker_name), 'w') as f:
Gilad Arnold1638d822013-11-07 23:38:16 -0800183 f.write('\n'.join(self.installed_files))
Chris Sosa76e44b92013-01-31 12:11:38 -0800184
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800185 def _WaitForArtifactToExist(self, name, timeout):
Chris Sosac4e87842013-08-16 18:04:14 -0700186 """Waits for artifact to exist and sets self.name to appropriate name.
187
188 Args:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800189 name: Name to look at.
Gilad Arnold950569b2013-08-27 14:38:01 -0700190 timeout: How long to wait for artifact to become available.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800191
Gilad Arnold950569b2013-08-27 14:38:01 -0700192 Raises:
193 ArtifactDownloadError: An error occurred when obtaining artifact.
Chris Sosac4e87842013-08-16 18:04:14 -0700194 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800195 names = gsutil_util.GetGSNamesWithWait(
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800196 name, self.archive_url, str(self), timeout=timeout,
Gilad Arnold950569b2013-08-27 14:38:01 -0700197 is_regex_pattern=self.is_regex_name)
Chris Sosa76e44b92013-01-31 12:11:38 -0800198 if not names:
Don Garrettef484fb2013-11-22 16:56:18 -0800199 raise ArtifactDownloadError('Could not find %s in Google Storage at %s' %
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800200 (name, self.archive_url))
201 return names
Chris Sosa76e44b92013-01-31 12:11:38 -0800202
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800203 def _UpdateName(self, names):
204 if self.single_name and len(names) > 1:
205 raise ArtifactDownloadError('Too many artifacts match %s' % self.name)
Chris Sosa76e44b92013-01-31 12:11:38 -0800206
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800207 self.name = names[0]
Chris Sosa76e44b92013-01-31 12:11:38 -0800208
209 def _Download(self):
joychen0a8e34e2013-06-24 17:58:36 -0700210 """Downloads artifact from Google Storage to a local directory."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800211 gs_path = '/'.join([self.archive_url, self.name])
joychen0a8e34e2013-06-24 17:58:36 -0700212 self.install_path = os.path.join(self.install_dir, self.name)
213 gsutil_util.DownloadFromGS(gs_path, self.install_path)
Chris Sosa76e44b92013-01-31 12:11:38 -0800214
joychen0a8e34e2013-06-24 17:58:36 -0700215 def _Setup(self):
Gilad Arnold1638d822013-11-07 23:38:16 -0800216 """Process the downloaded content, update the list of installed files."""
217 # In this primitive case, what was downloaded (has to be a single file) is
218 # what's installed.
219 self.installed_files = [self.install_path]
joychen0a8e34e2013-06-24 17:58:36 -0700220
Dan Shi6e50c722013-08-19 15:05:06 -0700221 def _ClearException(self):
222 """Delete any existing exception saved for this artifact."""
223 if os.path.exists(self.exception_file_path):
224 os.remove(self.exception_file_path)
225
226 def _SaveException(self, e):
227 """Save the exception to a file for downloader.IsStaged to retrieve.
228
Gilad Arnold950569b2013-08-27 14:38:01 -0700229 Args:
230 e: Exception object to be saved.
Dan Shi6e50c722013-08-19 15:05:06 -0700231 """
232 with open(self.exception_file_path, 'w') as f:
233 pickle.dump(e, f)
234
235 def GetException(self):
236 """Retrieve any exception that was raised in Process method.
237
Gilad Arnold950569b2013-08-27 14:38:01 -0700238 Returns:
239 An Exception object that was raised when trying to process the artifact.
240 Return None if no exception was found.
Dan Shi6e50c722013-08-19 15:05:06 -0700241 """
242 if not os.path.exists(self.exception_file_path):
243 return None
244 with open(self.exception_file_path, 'r') as f:
245 return pickle.load(f)
Chris Sosa76e44b92013-01-31 12:11:38 -0800246
247 def Process(self, no_wait):
248 """Main call point to all artifacts. Downloads and Stages artifact.
249
250 Downloads and Stages artifact from Google Storage to the install directory
251 specified in the constructor. It multi-thread safe and does not overwrite
252 the artifact if it's already been downloaded or being downloaded. After
253 processing, leaves behind a marker to indicate to future invocations that
254 the artifact has already been staged based on the name of the artifact.
255
256 Do not override as it modifies important private variables, ensures thread
257 safety, and maintains cache semantics.
258
259 Note: this may be a blocking call when the artifact is already in the
260 process of being staged.
261
262 Args:
263 no_wait: If True, don't block waiting for artifact to exist if we fail to
264 immediately find it.
265
266 Raises:
267 ArtifactDownloadError: If the artifact fails to download from Google
268 Storage for any reason or that the regexp
269 defined by name is not specific enough.
270 """
271 if not self._process_lock:
272 self._process_lock = _build_artifact_locks.lock(
273 os.path.join(self.install_dir, self.name))
274
275 with self._process_lock:
276 common_util.MkDirP(self.install_dir)
Dan Shif8eb0d12013-08-01 17:52:06 -0700277 if not self.ArtifactStaged():
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800278 # Delete any existing exception saved for this artifact.
279 self._ClearException()
280 found_artifact = False
281 if self.optional_name:
282 try:
283 # Check if the artifact named |optional_name| exists on GS.
284 # Because this artifact may not always exist, don't bother
285 # to wait for it (set timeout=1).
286 new_names = self._WaitForArtifactToExist(
287 self.optional_name, timeout=1)
288 self._UpdateName(new_names)
289
290 except ArtifactDownloadError:
291 self._Log('Unable to download %s; fall back to download %s',
292 self.optional_name, self.name)
293 else:
294 found_artifact = True
295
Dan Shi6e50c722013-08-19 15:05:06 -0700296 try:
Dan Shi6e50c722013-08-19 15:05:06 -0700297 # If the artifact should already have been uploaded, don't waste
298 # cycles waiting around for it to exist.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800299 if not found_artifact:
300 timeout = 1 if no_wait else 10
301 new_names = self._WaitForArtifactToExist(self.name, timeout)
302 self._UpdateName(new_names)
303
304 self._Log('Downloading file %s', self.name)
Dan Shi6e50c722013-08-19 15:05:06 -0700305 self._Download()
306 self._Setup()
307 self._MarkArtifactStaged()
308 except Exception as e:
309 # Save the exception to a file for downloader.IsStaged to retrieve.
310 self._SaveException(e)
Gilad Arnold02dc6552013-11-14 11:27:54 -0800311
312 # Convert an unknown exception into an ArtifactDownloadError.
313 if type(e) is ArtifactDownloadError:
314 raise
315 else:
316 raise ArtifactDownloadError('An error occurred: %s' % e)
Chris Sosa76e44b92013-01-31 12:11:38 -0800317 else:
318 self._Log('%s is already staged.', self)
319
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700320 def __str__(self):
321 """String representation for the download."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800322 return '->'.join(['%s/%s' % (self.archive_url, self.name),
Gilad Arnold950569b2013-08-27 14:38:01 -0700323 self.install_dir])
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700324
Chris Sosab26b1202013-08-16 16:40:55 -0700325 def __repr__(self):
326 return str(self)
327
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700328
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700329class AUTestPayloadBuildArtifact(BuildArtifact):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700330 """Wrapper for AUTest delta payloads which need additional setup."""
Gilad Arnold950569b2013-08-27 14:38:01 -0700331
joychen0a8e34e2013-06-24 17:58:36 -0700332 def _Setup(self):
333 super(AUTestPayloadBuildArtifact, self)._Setup()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700334
Chris Sosa76e44b92013-01-31 12:11:38 -0800335 # Rename to update.gz.
336 install_path = os.path.join(self.install_dir, self.name)
joychen3cb228e2013-06-12 12:13:13 -0700337 new_install_path = os.path.join(self.install_dir,
joychen7c2054a2013-07-25 11:14:07 -0700338 devserver_constants.UPDATE_FILE)
Chris Sosa76e44b92013-01-31 12:11:38 -0800339 shutil.move(install_path, new_install_path)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700340
Gilad Arnold1638d822013-11-07 23:38:16 -0800341 # Reflect the rename in the list of installed files.
342 self.installed_files.remove(install_path)
343 self.installed_files = [new_install_path]
344
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700345
Chris Sosa76e44b92013-01-31 12:11:38 -0800346# TODO(sosa): Change callers to make this artifact more sane.
347class DeltaPayloadsArtifact(BuildArtifact):
348 """Delta payloads from the archive_url.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700349
Chris Sosa76e44b92013-01-31 12:11:38 -0800350 This artifact is super strange. It custom handles directories and
351 pulls in all delta payloads. We can't specify exactly what we want
352 because unlike other artifacts, this one does not conform to something a
353 client might know. The client doesn't know the version of n-1 or whether it
354 was even generated.
Gilad Arnold950569b2013-08-27 14:38:01 -0700355
356 IMPORTANT! Note that this artifact simply ignores the `name' argument because
357 that name is derived internally in accordance with sub-artifacts. Also note
358 the different types of names (in fact, file name patterns) used for the
359 different sub-artifacts.
Chris Sosa76e44b92013-01-31 12:11:38 -0800360 """
Gilad Arnold950569b2013-08-27 14:38:01 -0700361
Chris Sosa76e44b92013-01-31 12:11:38 -0800362 def __init__(self, *args):
363 super(DeltaPayloadsArtifact, self).__init__(*args)
Gilad Arnold950569b2013-08-27 14:38:01 -0700364 # Override the name field, we know what it should be.
365 self.name = '*_delta_*'
366 self.is_regex_name = False
367 self.single_name = False # Expect multiple deltas
368
369 # We use a regular glob for the N-to-N delta payload.
370 nton_name = 'chromeos_%s*_delta_*' % self.build
371 # We use a regular expression for the M-to-N delta payload.
372 mton_name = ('chromeos_(?!%s).*_delta_.*' % re.escape(self.build))
373
Chris Sosa76e44b92013-01-31 12:11:38 -0800374 nton_install_dir = os.path.join(self.install_dir, _AU_BASE,
375 self.build + _NTON_DIR_SUFFIX)
376 mton_install_dir = os.path.join(self.install_dir, _AU_BASE,
Gilad Arnold950569b2013-08-27 14:38:01 -0700377 self.build + _MTON_DIR_SUFFIX)
Chris Sosa76e44b92013-01-31 12:11:38 -0800378 self._sub_artifacts = [
379 AUTestPayloadBuildArtifact(mton_install_dir, self.archive_url,
Gilad Arnold950569b2013-08-27 14:38:01 -0700380 mton_name, self.build, is_regex_name=True),
Chris Sosa76e44b92013-01-31 12:11:38 -0800381 AUTestPayloadBuildArtifact(nton_install_dir, self.archive_url,
382 nton_name, self.build)]
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700383
Chris Sosa76e44b92013-01-31 12:11:38 -0800384 def _Download(self):
joychen0a8e34e2013-06-24 17:58:36 -0700385 """With sub-artifacts we do everything in _Setup()."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800386 pass
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700387
joychen0a8e34e2013-06-24 17:58:36 -0700388 def _Setup(self):
Chris Sosa76e44b92013-01-31 12:11:38 -0800389 """Process each sub-artifact. Only error out if none can be found."""
390 for artifact in self._sub_artifacts:
391 try:
392 artifact.Process(no_wait=True)
393 # Setup symlink so that AU will work for this payload.
Gilad Arnold1638d822013-11-07 23:38:16 -0800394 stateful_update_symlink = os.path.join(
395 artifact.install_dir, devserver_constants.STATEFUL_FILE)
Chris Sosa76e44b92013-01-31 12:11:38 -0800396 os.symlink(
joychen25d25972013-07-30 14:54:16 -0700397 os.path.join(os.pardir, os.pardir,
joychen121fc9b2013-08-02 14:30:30 -0700398 devserver_constants.STATEFUL_FILE),
Gilad Arnold1638d822013-11-07 23:38:16 -0800399 stateful_update_symlink)
400
401 # Aggregate sub-artifact file lists, including stateful symlink.
402 self.installed_files += artifact.installed_files
403 self.installed_files.append(stateful_update_symlink)
Chris Sosa76e44b92013-01-31 12:11:38 -0800404 except ArtifactDownloadError as e:
405 self._Log('Could not process %s: %s', artifact, e)
Gilad Arnold02dc6552013-11-14 11:27:54 -0800406 raise
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700407
Chris Sosa76e44b92013-01-31 12:11:38 -0800408
409class BundledBuildArtifact(BuildArtifact):
410 """A single build artifact bundle e.g. zip file or tar file."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800411
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800412 def __init__(self, *args, **kwargs):
Gilad Arnold950569b2013-08-27 14:38:01 -0700413 """Takes BuildArtifact args with some additional ones.
414
415 Args:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800416 *args: See BuildArtifact documentation.
417 **kwargs: See BuildArtifact documentation.
Gilad Arnold950569b2013-08-27 14:38:01 -0700418 files_to_extract: A list of files to extract. If set to None, extract
419 all files.
420 exclude: A list of files to exclude. If None, no files are excluded.
Chris Sosa76e44b92013-01-31 12:11:38 -0800421 """
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800422 self._files_to_extract = kwargs.pop('files_to_extract', None)
423 self._exclude = kwargs.pop('exclude', None)
424 super(BundledBuildArtifact, self).__init__(*args, **kwargs)
Chris Sosa76e44b92013-01-31 12:11:38 -0800425
426 # We modify the marker so that it is unique to what was staged.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800427 if self._files_to_extract:
Chris Sosa76e44b92013-01-31 12:11:38 -0800428 self.marker_name = self._SanitizeName(
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800429 '_'.join(['.' + self.name] + self._files_to_extract))
Chris Sosa76e44b92013-01-31 12:11:38 -0800430
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800431 def _RunUnzip(self, list_only):
432 # Unzip is weird. It expects its args before any excludes and expects its
433 # excludes in a list following the -x.
434 cmd = ['unzip', '-qql' if list_only else '-o', self.install_path]
435 if not list_only:
436 cmd += ['-d', self.install_dir]
Chris Sosa76e44b92013-01-31 12:11:38 -0800437
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800438 if self._files_to_extract:
439 cmd.extend(self._files_to_extract)
440
441 if self._exclude:
442 cmd.append('-x')
443 cmd.extend(self._exclude)
444
445 try:
446 return subprocess.check_output(cmd).strip('\n').splitlines()
447 except subprocess.CalledProcessError, e:
448 raise ArtifactDownloadError(
449 'An error occurred when attempting to unzip %s:\n%s' %
450 (self.install_path, e))
Chris Sosa76e44b92013-01-31 12:11:38 -0800451
joychen0a8e34e2013-06-24 17:58:36 -0700452 def _Setup(self):
Gilad Arnold1638d822013-11-07 23:38:16 -0800453 extract_result = self._Extract()
454 if self.store_installed_files:
455 # List both the archive and the extracted files.
456 self.installed_files.append(self.install_path)
457 self.installed_files.extend(extract_result)
Chris Sosa76e44b92013-01-31 12:11:38 -0800458
Chris Sosa76e44b92013-01-31 12:11:38 -0800459 def _Extract(self):
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800460 """Extracts files into the install path."""
461 if self.name.endswith('.zip'):
462 return self._ExtractZipfile()
463 else:
464 return self._ExtractTarball()
465
466 def _ExtractZipfile(self):
467 """Extracts a zip file using unzip."""
468 file_list = [os.path.join(self.install_dir, line[30:].strip())
469 for line in self._RunUnzip(True)
470 if not line.endswith('/')]
471 if file_list:
472 self._RunUnzip(False)
473
474 return file_list
475
476 def _ExtractTarball(self):
Chris Sosa76e44b92013-01-31 12:11:38 -0800477 """Extracts a tarball using tar.
478
479 Detects whether the tarball is compressed or not based on the file
480 extension and extracts the tarball into the install_path.
481 """
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700482 try:
Gilad Arnold1638d822013-11-07 23:38:16 -0800483 return common_util.ExtractTarball(self.install_path, self.install_dir,
484 files_to_extract=self._files_to_extract,
485 excluded_files=self._exclude,
486 return_extracted_files=True)
Simran Basi4baad082013-02-14 13:39:18 -0800487 except common_util.CommonUtilError as e:
488 raise ArtifactDownloadError(str(e))
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700489
490
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800491class AutotestTarballBuildArtifact(BundledBuildArtifact):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700492 """Wrapper around the autotest tarball to download from gsutil."""
493
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800494 def __init__(self, *args, **kwargs):
495 super(AutotestTarballBuildArtifact, self).__init__(*args, **kwargs)
Gilad Arnold1638d822013-11-07 23:38:16 -0800496 # We don't store/check explicit file lists in Autotest tarball markers;
497 # this can get huge and unwieldy, and generally make little sense.
498 self.store_installed_files = False
499
joychen0a8e34e2013-06-24 17:58:36 -0700500 def _Setup(self):
Chris Sosa76e44b92013-01-31 12:11:38 -0800501 """Extracts the tarball into the install path excluding test suites."""
joychen0a8e34e2013-06-24 17:58:36 -0700502 super(AutotestTarballBuildArtifact, self)._Setup()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700503
Chris Sosa76e44b92013-01-31 12:11:38 -0800504 # Deal with older autotest packages that may not be bundled.
joychen3cb228e2013-06-12 12:13:13 -0700505 autotest_dir = os.path.join(self.install_dir,
506 devserver_constants.AUTOTEST_DIR)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700507 autotest_pkgs_dir = os.path.join(autotest_dir, 'packages')
508 if not os.path.exists(autotest_pkgs_dir):
509 os.makedirs(autotest_pkgs_dir)
510
511 if not os.path.exists(os.path.join(autotest_pkgs_dir, 'packages.checksum')):
Chris Sosa76e44b92013-01-31 12:11:38 -0800512 cmd = ['autotest/utils/packager.py', 'upload', '--repository',
513 autotest_pkgs_dir, '--all']
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700514 try:
joychen0a8e34e2013-06-24 17:58:36 -0700515 subprocess.check_call(cmd, cwd=self.install_dir)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700516 except subprocess.CalledProcessError, e:
Chris Sosa76e44b92013-01-31 12:11:38 -0800517 raise ArtifactDownloadError(
518 'Failed to create autotest packages!:\n%s' % e)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700519 else:
Gilad Arnoldf5843132012-09-25 00:31:20 -0700520 self._Log('Using pre-generated packages from autotest')
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700521
Chris Masone816e38c2012-05-02 12:22:36 -0700522
Chris Sosa76e44b92013-01-31 12:11:38 -0800523class ImplDescription(object):
524 """Data wrapper that describes an artifact's implementation."""
Gilad Arnold950569b2013-08-27 14:38:01 -0700525
526 def __init__(self, artifact_class, name, *additional_args,
527 **additional_dargs):
528 """Constructor.
Chris Sosa76e44b92013-01-31 12:11:38 -0800529
530 Args:
531 artifact_class: BuildArtifact class to use for the artifact.
532 name: name to use to identify artifact (see BuildArtifact.name)
Gilad Arnold950569b2013-08-27 14:38:01 -0700533 *additional_args: Additional arguments to pass to artifact_class.
534 **additional_dargs: Additional named arguments to pass to artifact_class.
Chris Sosa76e44b92013-01-31 12:11:38 -0800535 """
536 self.artifact_class = artifact_class
537 self.name = name
538 self.additional_args = additional_args
Gilad Arnold950569b2013-08-27 14:38:01 -0700539 self.additional_dargs = additional_dargs
Chris Sosa76e44b92013-01-31 12:11:38 -0800540
Chris Sosa968a1062013-08-02 17:42:50 -0700541 def __repr__(self):
542 return '%s_%s' % (self.artifact_class, self.name)
543
Chris Sosa76e44b92013-01-31 12:11:38 -0800544
545# Maps artifact names to their implementation description.
546# Please note, it is good practice to use constants for these names if you're
547# going to re-use the names ANYWHERE else in the devserver code.
548ARTIFACT_IMPLEMENTATION_MAP = {
Gilad Arnold950569b2013-08-27 14:38:01 -0700549 artifact_info.FULL_PAYLOAD:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800550 ImplDescription(AUTestPayloadBuildArtifact, ('*_full_*')),
Gilad Arnold950569b2013-08-27 14:38:01 -0700551 artifact_info.DELTA_PAYLOADS:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800552 ImplDescription(DeltaPayloadsArtifact, ('DONTCARE')),
Gilad Arnold950569b2013-08-27 14:38:01 -0700553 artifact_info.STATEFUL_PAYLOAD:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800554 ImplDescription(BuildArtifact, (devserver_constants.STATEFUL_FILE)),
Chris Sosa76e44b92013-01-31 12:11:38 -0800555
Gilad Arnold950569b2013-08-27 14:38:01 -0700556 artifact_info.BASE_IMAGE:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800557 ImplDescription(BundledBuildArtifact, IMAGE_FILE,
558 optional_name=BASE_IMAGE_FILE,
Gilad Arnold69878b42013-09-18 13:39:22 -0700559 files_to_extract=[devserver_constants.BASE_IMAGE_FILE]),
Gilad Arnold950569b2013-08-27 14:38:01 -0700560 artifact_info.RECOVERY_IMAGE:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800561 ImplDescription(BundledBuildArtifact, IMAGE_FILE,
562 optional_name=RECOVERY_IMAGE_FILE,
Gilad Arnold69878b42013-09-18 13:39:22 -0700563 files_to_extract=[devserver_constants.RECOVERY_IMAGE_FILE]),
Chris Sosa75490802013-09-30 17:21:45 -0700564 artifact_info.DEV_IMAGE:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800565 ImplDescription(BundledBuildArtifact, IMAGE_FILE,
Chris Sosa75490802013-09-30 17:21:45 -0700566 files_to_extract=[devserver_constants.IMAGE_FILE]),
Gilad Arnold950569b2013-08-27 14:38:01 -0700567 artifact_info.TEST_IMAGE:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800568 ImplDescription(BundledBuildArtifact, IMAGE_FILE,
569 optional_name=TEST_IMAGE_FILE,
Gilad Arnold69878b42013-09-18 13:39:22 -0700570 files_to_extract=[devserver_constants.TEST_IMAGE_FILE]),
Chris Sosa76e44b92013-01-31 12:11:38 -0800571
Gilad Arnold950569b2013-08-27 14:38:01 -0700572 artifact_info.AUTOTEST:
573 ImplDescription(AutotestTarballBuildArtifact, AUTOTEST_FILE,
574 files_to_extract=None,
575 exclude=['autotest/test_suites']),
576 artifact_info.TEST_SUITES:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800577 ImplDescription(BundledBuildArtifact, TEST_SUITES_FILE),
Gilad Arnold950569b2013-08-27 14:38:01 -0700578 artifact_info.AU_SUITE:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800579 ImplDescription(BundledBuildArtifact, AU_SUITE_FILE),
Chris Sosa76e44b92013-01-31 12:11:38 -0800580
Gilad Arnold950569b2013-08-27 14:38:01 -0700581 artifact_info.FIRMWARE:
582 ImplDescription(BuildArtifact, FIRMWARE_FILE),
583 artifact_info.SYMBOLS:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800584 ImplDescription(BundledBuildArtifact, DEBUG_SYMBOLS_FILE,
Gilad Arnold950569b2013-08-27 14:38:01 -0700585 files_to_extract=['debug/breakpad']),
beepsc3d0f872013-07-31 21:50:40 -0700586
Gilad Arnold950569b2013-08-27 14:38:01 -0700587 artifact_info.FACTORY_IMAGE:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800588 ImplDescription(BundledBuildArtifact, FACTORY_FILE,
Gilad Arnold69878b42013-09-18 13:39:22 -0700589 files_to_extract=[devserver_constants.FACTORY_IMAGE_FILE])
Chris Sosa76e44b92013-01-31 12:11:38 -0800590}
591
Chris Sosa968a1062013-08-02 17:42:50 -0700592# Add all the paygen_au artifacts in one go.
593ARTIFACT_IMPLEMENTATION_MAP.update({
Gilad Arnold950569b2013-08-27 14:38:01 -0700594 artifact_info.PAYGEN_AU_SUITE_TEMPLATE % {'channel': c}:
595 ImplDescription(
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800596 BundledBuildArtifact, PAYGEN_AU_SUITE_FILE_TEMPLATE % {'channel': c})
Gilad Arnold950569b2013-08-27 14:38:01 -0700597 for c in devserver_constants.CHANNELS
Chris Sosa968a1062013-08-02 17:42:50 -0700598})
599
Chris Sosa76e44b92013-01-31 12:11:38 -0800600
601class ArtifactFactory(object):
602 """A factory class that generates build artifacts from artifact names."""
603
Chris Sosa6b0c6172013-08-05 17:01:33 -0700604 def __init__(self, download_dir, archive_url, artifacts, files,
605 build):
Chris Sosa76e44b92013-01-31 12:11:38 -0800606 """Initalizes the member variables for the factory.
607
608 Args:
Gilad Arnold950569b2013-08-27 14:38:01 -0700609 download_dir: A directory to which artifacts are downloaded.
Chris Sosa76e44b92013-01-31 12:11:38 -0800610 archive_url: the Google Storage url of the bucket where the debug
611 symbols for the desired build are stored.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700612 artifacts: List of artifacts to stage. These artifacts must be
613 defined in artifact_info.py and have a mapping in the
614 ARTIFACT_IMPLEMENTATION_MAP.
615 files: List of files to stage. These files are just downloaded and staged
616 as files into the download_dir.
Chris Sosa76e44b92013-01-31 12:11:38 -0800617 build: The name of the build.
618 """
joychen0a8e34e2013-06-24 17:58:36 -0700619 self.download_dir = download_dir
Chris Sosa76e44b92013-01-31 12:11:38 -0800620 self.archive_url = archive_url
Chris Sosa6b0c6172013-08-05 17:01:33 -0700621 self.artifacts = artifacts
622 self.files = files
Chris Sosa76e44b92013-01-31 12:11:38 -0800623 self.build = build
624
625 @staticmethod
Chris Sosa6b0c6172013-08-05 17:01:33 -0700626 def _GetDescriptionComponents(name, is_artifact):
Gilad Arnold950569b2013-08-27 14:38:01 -0700627 """Returns components for constructing a BuildArtifact.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700628
Gilad Arnold950569b2013-08-27 14:38:01 -0700629 Args:
630 name: The artifact name / file pattern.
631 is_artifact: Whether this is a named (True) or file (False) artifact.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800632
Gilad Arnold950569b2013-08-27 14:38:01 -0700633 Returns:
634 A tuple consisting of the BuildArtifact subclass, name, and additional
635 list- and named-arguments.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800636
Gilad Arnold950569b2013-08-27 14:38:01 -0700637 Raises:
638 KeyError: if artifact doesn't exist in ARTIFACT_IMPLEMENTATION_MAP.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700639 """
640
641 if is_artifact:
642 description = ARTIFACT_IMPLEMENTATION_MAP[name]
643 else:
644 description = ImplDescription(BuildArtifact, name)
645
Chris Sosa76e44b92013-01-31 12:11:38 -0800646 return (description.artifact_class, description.name,
Gilad Arnold950569b2013-08-27 14:38:01 -0700647 description.additional_args, description.additional_dargs)
Chris Sosa76e44b92013-01-31 12:11:38 -0800648
Chris Sosa6b0c6172013-08-05 17:01:33 -0700649 def _Artifacts(self, names, is_artifact):
Gilad Arnold950569b2013-08-27 14:38:01 -0700650 """Returns the BuildArtifacts from |names|.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700651
652 If is_artifact is true, then these names define artifacts that must exist in
653 the ARTIFACT_IMPLEMENTATION_MAP. Otherwise, treat as filenames to stage as
654 basic BuildArtifacts.
655
Gilad Arnold950569b2013-08-27 14:38:01 -0700656 Args:
657 names: A sequence of artifact names.
658 is_artifact: Whether this is a named (True) or file (False) artifact.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800659
Gilad Arnold950569b2013-08-27 14:38:01 -0700660 Returns:
661 An iterable of BuildArtifacts.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800662
Gilad Arnold950569b2013-08-27 14:38:01 -0700663 Raises:
664 KeyError: if artifact doesn't exist in ARTIFACT_IMPLEMENTATION_MAP.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700665 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800666 artifacts = []
Chris Sosa6b0c6172013-08-05 17:01:33 -0700667 for name in names:
Gilad Arnold950569b2013-08-27 14:38:01 -0700668 artifact_class, path, args, dargs = self._GetDescriptionComponents(
Chris Sosa6b0c6172013-08-05 17:01:33 -0700669 name, is_artifact)
joychen0a8e34e2013-06-24 17:58:36 -0700670 artifacts.append(artifact_class(self.download_dir, self.archive_url, path,
Gilad Arnold950569b2013-08-27 14:38:01 -0700671 self.build, *args, **dargs))
Chris Sosa76e44b92013-01-31 12:11:38 -0800672
673 return artifacts
674
675 def RequiredArtifacts(self):
Gilad Arnold950569b2013-08-27 14:38:01 -0700676 """Returns BuildArtifacts for the factory's artifacts.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700677
Gilad Arnold950569b2013-08-27 14:38:01 -0700678 Returns:
679 An iterable of BuildArtifacts.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800680
Gilad Arnold950569b2013-08-27 14:38:01 -0700681 Raises:
682 KeyError: if artifact doesn't exist in ARTIFACT_IMPLEMENTATION_MAP.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700683 """
684 artifacts = []
685 if self.artifacts:
686 artifacts.extend(self._Artifacts(self.artifacts, True))
687 if self.files:
688 artifacts.extend(self._Artifacts(self.files, False))
689
690 return artifacts
Chris Sosa76e44b92013-01-31 12:11:38 -0800691
692 def OptionalArtifacts(self):
Gilad Arnold950569b2013-08-27 14:38:01 -0700693 """Returns BuildArtifacts that should be cached.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700694
Gilad Arnold950569b2013-08-27 14:38:01 -0700695 Returns:
696 An iterable of BuildArtifacts.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800697
Gilad Arnold950569b2013-08-27 14:38:01 -0700698 Raises:
699 KeyError: if an optional artifact doesn't exist in
700 ARTIFACT_IMPLEMENTATION_MAP yet defined in
701 artifact_info.REQUESTED_TO_OPTIONAL_MAP.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700702 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800703 optional_names = set()
704 for artifact_name, optional_list in (
705 artifact_info.REQUESTED_TO_OPTIONAL_MAP.iteritems()):
706 # We are already downloading it.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700707 if artifact_name in self.artifacts:
Chris Sosa76e44b92013-01-31 12:11:38 -0800708 optional_names = optional_names.union(optional_list)
709
Chris Sosa6b0c6172013-08-05 17:01:33 -0700710 return self._Artifacts(optional_names - set(self.artifacts), True)
Chris Sosa968a1062013-08-02 17:42:50 -0700711
712
713# A simple main to verify correctness of the artifact map when making simple
714# name changes.
715if __name__ == '__main__':
716 print 'ARTIFACT IMPLEMENTATION MAP (for debugging)'
717 print 'FORMAT: ARTIFACT -> IMPLEMENTATION (<class>_file)'
718 for key, value in sorted(ARTIFACT_IMPLEMENTATION_MAP.items()):
719 print '%s -> %s' % (key, value)