blob: e0d6d979dac9f665f38146d669296caeeeffb084 [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'
Gilad Arnold950569b2013-08-27 14:38:01 -070033FACTORY_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'
37
38_build_artifact_locks = common_util.LockDict()
Chris Sosa47a7d4e2012-03-28 11:26:55 -070039
40
41class ArtifactDownloadError(Exception):
42 """Error used to signify an issue processing an artifact."""
43 pass
44
45
Gilad Arnoldc65330c2012-09-20 15:17:48 -070046class BuildArtifact(log_util.Loggable):
Chris Sosa47a7d4e2012-03-28 11:26:55 -070047 """Wrapper around an artifact to download from gsutil.
48
49 The purpose of this class is to download objects from Google Storage
50 and install them to a local directory. There are two main functions, one to
51 download/prepare the artifacts in to a temporary staging area and the second
52 to stage it into its final destination.
Chris Sosa76e44b92013-01-31 12:11:38 -080053
Gilad Arnold950569b2013-08-27 14:38:01 -070054 IMPORTANT! (i) `name' is a glob expression by default (and not a regex), be
55 attentive when adding new artifacts; (ii) name matching semantics differ
56 between a glob (full name string match) and a regex (partial match).
57
Chris Sosa76e44b92013-01-31 12:11:38 -080058 Class members:
Gilad Arnold950569b2013-08-27 14:38:01 -070059 archive_url: An archive URL.
60 name: Name given for artifact; in fact, it is a pattern that captures the
61 names of files contained in the artifact. This can either be an
62 ordinary shell-style glob (the default), or a regular expression (if
63 is_regex_name is True).
64 is_regex_name: Whether the name value is a regex (default: glob).
Chris Sosa76e44b92013-01-31 12:11:38 -080065 build: The version of the build i.e. R26-2342.0.0.
66 marker_name: Name used to define the lock marker for the artifacts to
67 prevent it from being re-downloaded. By default based on name
68 but can be overriden by children.
Dan Shi6e50c722013-08-19 15:05:06 -070069 exception_file_path: Path to a file containing the serialized exception,
70 which was raised in Process method. The file is located
71 in the parent folder of install_dir, since the
72 install_dir will be deleted if the build does not
73 existed.
joychen0a8e34e2013-06-24 17:58:36 -070074 install_path: Path to artifact.
Chris Sosa76e44b92013-01-31 12:11:38 -080075 install_dir: The final location where the artifact should be staged to.
76 single_name: If True the name given should only match one item. Note, if not
77 True, self.name will become a list of items returned.
Gilad Arnold1638d822013-11-07 23:38:16 -080078 installed_files: A list of files that were the final result of downloading
79 and setting up the artifact.
80 store_installed_files: Whether the list of installed files is stored in the
81 marker file.
Chris Sosa47a7d4e2012-03-28 11:26:55 -070082 """
Gilad Arnold950569b2013-08-27 14:38:01 -070083
84 def __init__(self, install_dir, archive_url, name, build,
85 is_regex_name=False):
86 """Constructor.
87
88 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -080089 install_dir: Where to install the artifact.
90 archive_url: The Google Storage path to find the artifact.
91 name: Identifying name to be used to find/store the artifact.
92 build: The name of the build e.g. board/release.
Gilad Arnold950569b2013-08-27 14:38:01 -070093 is_regex_name: Whether the name pattern is a regex (default: glob).
Chris Sosa47a7d4e2012-03-28 11:26:55 -070094 """
Chris Sosa6a3697f2013-01-29 16:44:43 -080095 super(BuildArtifact, self).__init__()
Chris Sosa47a7d4e2012-03-28 11:26:55 -070096
Chris Sosa76e44b92013-01-31 12:11:38 -080097 # In-memory lock to keep the devserver from colliding with itself while
98 # attempting to stage the same artifact.
99 self._process_lock = None
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700100
Chris Sosa76e44b92013-01-31 12:11:38 -0800101 self.archive_url = archive_url
102 self.name = name
Gilad Arnold950569b2013-08-27 14:38:01 -0700103 self.is_regex_name = is_regex_name
Chris Sosa76e44b92013-01-31 12:11:38 -0800104 self.build = build
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700105
Chris Sosa76e44b92013-01-31 12:11:38 -0800106 self.marker_name = '.' + self._SanitizeName(name)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700107
Dan Shi6e50c722013-08-19 15:05:06 -0700108 exception_file_name = ('.' + self._SanitizeName(build) + self.marker_name +
109 '.exception')
110 # The exception file needs to be located in parent folder, since the
111 # install_dir will be deleted is the build does not exist.
112 self.exception_file_path = os.path.join(os.path.dirname(install_dir),
Gilad Arnold950569b2013-08-27 14:38:01 -0700113 exception_file_name)
Dan Shi6e50c722013-08-19 15:05:06 -0700114
joychen0a8e34e2013-06-24 17:58:36 -0700115 self.install_path = None
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700116
Chris Sosa76e44b92013-01-31 12:11:38 -0800117 self.install_dir = install_dir
118
119 self.single_name = True
120
Gilad Arnold1638d822013-11-07 23:38:16 -0800121 self.installed_files = []
122 self.store_installed_files = True
123
Chris Sosa76e44b92013-01-31 12:11:38 -0800124 @staticmethod
125 def _SanitizeName(name):
126 """Sanitizes name to be used for creating a file on the filesystem.
127
128 '.','/' and '*' have special meaning in FS lingo. Replace them with words.
Gilad Arnold950569b2013-08-27 14:38:01 -0700129
130 Args:
131 name: A file name/path.
132 Returns:
133 The sanitized name/path.
Chris Sosa76e44b92013-01-31 12:11:38 -0800134 """
135 return name.replace('*', 'STAR').replace('.', 'DOT').replace('/', 'SLASH')
136
Dan Shif8eb0d12013-08-01 17:52:06 -0700137 def ArtifactStaged(self):
Gilad Arnold1638d822013-11-07 23:38:16 -0800138 """Returns True if artifact is already staged.
139
140 This checks for (1) presence of the artifact marker file, and (2) the
141 presence of each installed file listed in this marker. Both must hold for
142 the artifact to be considered staged. Note that this method is safe for use
143 even if the artifacts were not stageed by this instance, as it is assumed
144 that any BuildArtifact instance that did the staging wrote the list of
145 files actually installed into the marker.
146 """
147 marker_file = os.path.join(self.install_dir, self.marker_name)
148
149 # If the marker is missing, it's definitely not staged.
150 if not os.path.exists(marker_file):
151 return False
152
153 # We want to ensure that every file listed in the marker is actually there.
154 if self.store_installed_files:
155 with open(marker_file) as f:
156 files = [line.strip() for line in f]
157
158 # Check to see if any of the purportedly installed files are missing, in
159 # which case the marker is outdated and should be removed.
160 missing_files = [fname for fname in files if not os.path.exists(fname)]
161 if missing_files:
162 self._Log('***ATTENTION*** %s files listed in %s are missing:\n%s',
163 'All' if len(files) == len(missing_files) else 'Some',
164 marker_file, '\n'.join(missing_files))
165 os.remove(marker_file)
166 return False
167
168 return True
Chris Sosa76e44b92013-01-31 12:11:38 -0800169
170 def _MarkArtifactStaged(self):
171 """Marks the artifact as staged."""
172 with open(os.path.join(self.install_dir, self.marker_name), 'w') as f:
Gilad Arnold1638d822013-11-07 23:38:16 -0800173 f.write('\n'.join(self.installed_files))
Chris Sosa76e44b92013-01-31 12:11:38 -0800174
Gilad Arnold02dc6552013-11-14 11:27:54 -0800175 def _WaitForArtifactToExist(self, timeout, update_name=True):
Chris Sosac4e87842013-08-16 18:04:14 -0700176 """Waits for artifact to exist and sets self.name to appropriate name.
177
178 Args:
Gilad Arnold950569b2013-08-27 14:38:01 -0700179 timeout: How long to wait for artifact to become available.
Chris Sosac4e87842013-08-16 18:04:14 -0700180 update_name: If False, don't actually update self.name.
Gilad Arnold950569b2013-08-27 14:38:01 -0700181 Raises:
182 ArtifactDownloadError: An error occurred when obtaining artifact.
Chris Sosac4e87842013-08-16 18:04:14 -0700183 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800184 names = gsutil_util.GetGSNamesWithWait(
Gilad Arnold950569b2013-08-27 14:38:01 -0700185 self.name, self.archive_url, str(self), timeout=timeout,
186 is_regex_pattern=self.is_regex_name)
Chris Sosa76e44b92013-01-31 12:11:38 -0800187 if not names:
Don Garrettef484fb2013-11-22 16:56:18 -0800188 raise ArtifactDownloadError('Could not find %s in Google Storage at %s' %
189 (self.name, self.archive_url))
Chris Sosa76e44b92013-01-31 12:11:38 -0800190
191 if self.single_name:
192 if len(names) > 1:
193 raise ArtifactDownloadError('Too many artifacts match %s' % self.name)
194
Chris Sosac4e87842013-08-16 18:04:14 -0700195 new_name = names[0]
Chris Sosa76e44b92013-01-31 12:11:38 -0800196 else:
Chris Sosac4e87842013-08-16 18:04:14 -0700197 new_name = names
198
199 if update_name:
200 self.name = new_name
Chris Sosa76e44b92013-01-31 12:11:38 -0800201
202 def _Download(self):
joychen0a8e34e2013-06-24 17:58:36 -0700203 """Downloads artifact from Google Storage to a local directory."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800204 gs_path = '/'.join([self.archive_url, self.name])
joychen0a8e34e2013-06-24 17:58:36 -0700205 self.install_path = os.path.join(self.install_dir, self.name)
206 gsutil_util.DownloadFromGS(gs_path, self.install_path)
Chris Sosa76e44b92013-01-31 12:11:38 -0800207
joychen0a8e34e2013-06-24 17:58:36 -0700208 def _Setup(self):
Gilad Arnold1638d822013-11-07 23:38:16 -0800209 """Process the downloaded content, update the list of installed files."""
210 # In this primitive case, what was downloaded (has to be a single file) is
211 # what's installed.
212 self.installed_files = [self.install_path]
joychen0a8e34e2013-06-24 17:58:36 -0700213
Dan Shi6e50c722013-08-19 15:05:06 -0700214 def _ClearException(self):
215 """Delete any existing exception saved for this artifact."""
216 if os.path.exists(self.exception_file_path):
217 os.remove(self.exception_file_path)
218
219 def _SaveException(self, e):
220 """Save the exception to a file for downloader.IsStaged to retrieve.
221
Gilad Arnold950569b2013-08-27 14:38:01 -0700222 Args:
223 e: Exception object to be saved.
Dan Shi6e50c722013-08-19 15:05:06 -0700224 """
225 with open(self.exception_file_path, 'w') as f:
226 pickle.dump(e, f)
227
228 def GetException(self):
229 """Retrieve any exception that was raised in Process method.
230
Gilad Arnold950569b2013-08-27 14:38:01 -0700231 Returns:
232 An Exception object that was raised when trying to process the artifact.
233 Return None if no exception was found.
Dan Shi6e50c722013-08-19 15:05:06 -0700234 """
235 if not os.path.exists(self.exception_file_path):
236 return None
237 with open(self.exception_file_path, 'r') as f:
238 return pickle.load(f)
Chris Sosa76e44b92013-01-31 12:11:38 -0800239
240 def Process(self, no_wait):
241 """Main call point to all artifacts. Downloads and Stages artifact.
242
243 Downloads and Stages artifact from Google Storage to the install directory
244 specified in the constructor. It multi-thread safe and does not overwrite
245 the artifact if it's already been downloaded or being downloaded. After
246 processing, leaves behind a marker to indicate to future invocations that
247 the artifact has already been staged based on the name of the artifact.
248
249 Do not override as it modifies important private variables, ensures thread
250 safety, and maintains cache semantics.
251
252 Note: this may be a blocking call when the artifact is already in the
253 process of being staged.
254
255 Args:
256 no_wait: If True, don't block waiting for artifact to exist if we fail to
257 immediately find it.
258
259 Raises:
260 ArtifactDownloadError: If the artifact fails to download from Google
261 Storage for any reason or that the regexp
262 defined by name is not specific enough.
263 """
264 if not self._process_lock:
265 self._process_lock = _build_artifact_locks.lock(
266 os.path.join(self.install_dir, self.name))
267
268 with self._process_lock:
269 common_util.MkDirP(self.install_dir)
Dan Shif8eb0d12013-08-01 17:52:06 -0700270 if not self.ArtifactStaged():
Dan Shi6e50c722013-08-19 15:05:06 -0700271 try:
272 # Delete any existing exception saved for this artifact.
273 self._ClearException()
274 # If the artifact should already have been uploaded, don't waste
275 # cycles waiting around for it to exist.
276 timeout = 1 if no_wait else 10
Gilad Arnold02dc6552013-11-14 11:27:54 -0800277 self._WaitForArtifactToExist(timeout)
Dan Shi6e50c722013-08-19 15:05:06 -0700278 self._Download()
279 self._Setup()
280 self._MarkArtifactStaged()
281 except Exception as e:
282 # Save the exception to a file for downloader.IsStaged to retrieve.
283 self._SaveException(e)
Gilad Arnold02dc6552013-11-14 11:27:54 -0800284
285 # Convert an unknown exception into an ArtifactDownloadError.
286 if type(e) is ArtifactDownloadError:
287 raise
288 else:
289 raise ArtifactDownloadError('An error occurred: %s' % e)
Chris Sosa76e44b92013-01-31 12:11:38 -0800290 else:
291 self._Log('%s is already staged.', self)
292
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700293 def __str__(self):
294 """String representation for the download."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800295 return '->'.join(['%s/%s' % (self.archive_url, self.name),
Gilad Arnold950569b2013-08-27 14:38:01 -0700296 self.install_dir])
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700297
Chris Sosab26b1202013-08-16 16:40:55 -0700298 def __repr__(self):
299 return str(self)
300
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700301
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700302class AUTestPayloadBuildArtifact(BuildArtifact):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700303 """Wrapper for AUTest delta payloads which need additional setup."""
Gilad Arnold950569b2013-08-27 14:38:01 -0700304
joychen0a8e34e2013-06-24 17:58:36 -0700305 def _Setup(self):
306 super(AUTestPayloadBuildArtifact, self)._Setup()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700307
Chris Sosa76e44b92013-01-31 12:11:38 -0800308 # Rename to update.gz.
309 install_path = os.path.join(self.install_dir, self.name)
joychen3cb228e2013-06-12 12:13:13 -0700310 new_install_path = os.path.join(self.install_dir,
joychen7c2054a2013-07-25 11:14:07 -0700311 devserver_constants.UPDATE_FILE)
Chris Sosa76e44b92013-01-31 12:11:38 -0800312 shutil.move(install_path, new_install_path)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700313
Gilad Arnold1638d822013-11-07 23:38:16 -0800314 # Reflect the rename in the list of installed files.
315 self.installed_files.remove(install_path)
316 self.installed_files = [new_install_path]
317
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700318
Chris Sosa76e44b92013-01-31 12:11:38 -0800319# TODO(sosa): Change callers to make this artifact more sane.
320class DeltaPayloadsArtifact(BuildArtifact):
321 """Delta payloads from the archive_url.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700322
Chris Sosa76e44b92013-01-31 12:11:38 -0800323 This artifact is super strange. It custom handles directories and
324 pulls in all delta payloads. We can't specify exactly what we want
325 because unlike other artifacts, this one does not conform to something a
326 client might know. The client doesn't know the version of n-1 or whether it
327 was even generated.
Gilad Arnold950569b2013-08-27 14:38:01 -0700328
329 IMPORTANT! Note that this artifact simply ignores the `name' argument because
330 that name is derived internally in accordance with sub-artifacts. Also note
331 the different types of names (in fact, file name patterns) used for the
332 different sub-artifacts.
Chris Sosa76e44b92013-01-31 12:11:38 -0800333 """
Gilad Arnold950569b2013-08-27 14:38:01 -0700334
Chris Sosa76e44b92013-01-31 12:11:38 -0800335 def __init__(self, *args):
336 super(DeltaPayloadsArtifact, self).__init__(*args)
Gilad Arnold950569b2013-08-27 14:38:01 -0700337 # Override the name field, we know what it should be.
338 self.name = '*_delta_*'
339 self.is_regex_name = False
340 self.single_name = False # Expect multiple deltas
341
342 # We use a regular glob for the N-to-N delta payload.
343 nton_name = 'chromeos_%s*_delta_*' % self.build
344 # We use a regular expression for the M-to-N delta payload.
345 mton_name = ('chromeos_(?!%s).*_delta_.*' % re.escape(self.build))
346
Chris Sosa76e44b92013-01-31 12:11:38 -0800347 nton_install_dir = os.path.join(self.install_dir, _AU_BASE,
348 self.build + _NTON_DIR_SUFFIX)
349 mton_install_dir = os.path.join(self.install_dir, _AU_BASE,
Gilad Arnold950569b2013-08-27 14:38:01 -0700350 self.build + _MTON_DIR_SUFFIX)
Chris Sosa76e44b92013-01-31 12:11:38 -0800351 self._sub_artifacts = [
352 AUTestPayloadBuildArtifact(mton_install_dir, self.archive_url,
Gilad Arnold950569b2013-08-27 14:38:01 -0700353 mton_name, self.build, is_regex_name=True),
Chris Sosa76e44b92013-01-31 12:11:38 -0800354 AUTestPayloadBuildArtifact(nton_install_dir, self.archive_url,
355 nton_name, self.build)]
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700356
Chris Sosa76e44b92013-01-31 12:11:38 -0800357 def _Download(self):
joychen0a8e34e2013-06-24 17:58:36 -0700358 """With sub-artifacts we do everything in _Setup()."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800359 pass
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700360
joychen0a8e34e2013-06-24 17:58:36 -0700361 def _Setup(self):
Chris Sosa76e44b92013-01-31 12:11:38 -0800362 """Process each sub-artifact. Only error out if none can be found."""
363 for artifact in self._sub_artifacts:
364 try:
365 artifact.Process(no_wait=True)
366 # Setup symlink so that AU will work for this payload.
Gilad Arnold1638d822013-11-07 23:38:16 -0800367 stateful_update_symlink = os.path.join(
368 artifact.install_dir, devserver_constants.STATEFUL_FILE)
Chris Sosa76e44b92013-01-31 12:11:38 -0800369 os.symlink(
joychen25d25972013-07-30 14:54:16 -0700370 os.path.join(os.pardir, os.pardir,
joychen121fc9b2013-08-02 14:30:30 -0700371 devserver_constants.STATEFUL_FILE),
Gilad Arnold1638d822013-11-07 23:38:16 -0800372 stateful_update_symlink)
373
374 # Aggregate sub-artifact file lists, including stateful symlink.
375 self.installed_files += artifact.installed_files
376 self.installed_files.append(stateful_update_symlink)
Chris Sosa76e44b92013-01-31 12:11:38 -0800377 except ArtifactDownloadError as e:
378 self._Log('Could not process %s: %s', artifact, e)
Gilad Arnold02dc6552013-11-14 11:27:54 -0800379 raise
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700380
Chris Sosa76e44b92013-01-31 12:11:38 -0800381
382class BundledBuildArtifact(BuildArtifact):
383 """A single build artifact bundle e.g. zip file or tar file."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800384
Gilad Arnold950569b2013-08-27 14:38:01 -0700385 def __init__(self, install_dir, archive_url, name, build,
386 is_regex_name=False, files_to_extract=None, exclude=None):
387 """Takes BuildArtifact args with some additional ones.
388
389 Args:
390 install_dir: See superclass.
391 archive_url: See superclass.
392 name: See superclass.
393 build: See superclass.
394 is_regex_name: See superclass.
395 files_to_extract: A list of files to extract. If set to None, extract
396 all files.
397 exclude: A list of files to exclude. If None, no files are excluded.
Chris Sosa76e44b92013-01-31 12:11:38 -0800398 """
Gilad Arnold950569b2013-08-27 14:38:01 -0700399 super(BundledBuildArtifact, self).__init__(
400 install_dir, archive_url, name, build, is_regex_name=is_regex_name)
Chris Sosa76e44b92013-01-31 12:11:38 -0800401 self._files_to_extract = files_to_extract
402 self._exclude = exclude
403
404 # We modify the marker so that it is unique to what was staged.
405 if files_to_extract:
406 self.marker_name = self._SanitizeName(
407 '_'.join(['.' + self.name] + files_to_extract))
408
409 def _Extract(self):
410 """Extracts the bundle into install_dir. Must be overridden.
411
412 If set, uses files_to_extract to only extract those items. If set, use
Gilad Arnold1638d822013-11-07 23:38:16 -0800413 exclude to exclude specific files. In any case, this must return the list
414 of files extracted (absolute paths).
Chris Sosa76e44b92013-01-31 12:11:38 -0800415 """
416 raise NotImplementedError()
417
joychen0a8e34e2013-06-24 17:58:36 -0700418 def _Setup(self):
Gilad Arnold1638d822013-11-07 23:38:16 -0800419 extract_result = self._Extract()
420 if self.store_installed_files:
421 # List both the archive and the extracted files.
422 self.installed_files.append(self.install_path)
423 self.installed_files.extend(extract_result)
Chris Sosa76e44b92013-01-31 12:11:38 -0800424
425
426class TarballBuildArtifact(BundledBuildArtifact):
427 """Artifact for tar and tarball files."""
428
429 def _Extract(self):
430 """Extracts a tarball using tar.
431
432 Detects whether the tarball is compressed or not based on the file
433 extension and extracts the tarball into the install_path.
434 """
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700435 try:
Gilad Arnold1638d822013-11-07 23:38:16 -0800436 return common_util.ExtractTarball(self.install_path, self.install_dir,
437 files_to_extract=self._files_to_extract,
438 excluded_files=self._exclude,
439 return_extracted_files=True)
Simran Basi4baad082013-02-14 13:39:18 -0800440 except common_util.CommonUtilError as e:
441 raise ArtifactDownloadError(str(e))
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700442
443
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700444class AutotestTarballBuildArtifact(TarballBuildArtifact):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700445 """Wrapper around the autotest tarball to download from gsutil."""
446
Gilad Arnolde57e2332013-11-14 17:07:11 -0800447 def __init__(self, *args, **dargs):
448 super(AutotestTarballBuildArtifact, self).__init__(*args, **dargs)
Gilad Arnold1638d822013-11-07 23:38:16 -0800449 # We don't store/check explicit file lists in Autotest tarball markers;
450 # this can get huge and unwieldy, and generally make little sense.
451 self.store_installed_files = False
452
joychen0a8e34e2013-06-24 17:58:36 -0700453 def _Setup(self):
Chris Sosa76e44b92013-01-31 12:11:38 -0800454 """Extracts the tarball into the install path excluding test suites."""
joychen0a8e34e2013-06-24 17:58:36 -0700455 super(AutotestTarballBuildArtifact, self)._Setup()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700456
Chris Sosa76e44b92013-01-31 12:11:38 -0800457 # Deal with older autotest packages that may not be bundled.
joychen3cb228e2013-06-12 12:13:13 -0700458 autotest_dir = os.path.join(self.install_dir,
459 devserver_constants.AUTOTEST_DIR)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700460 autotest_pkgs_dir = os.path.join(autotest_dir, 'packages')
461 if not os.path.exists(autotest_pkgs_dir):
462 os.makedirs(autotest_pkgs_dir)
463
464 if not os.path.exists(os.path.join(autotest_pkgs_dir, 'packages.checksum')):
Chris Sosa76e44b92013-01-31 12:11:38 -0800465 cmd = ['autotest/utils/packager.py', 'upload', '--repository',
466 autotest_pkgs_dir, '--all']
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700467 try:
joychen0a8e34e2013-06-24 17:58:36 -0700468 subprocess.check_call(cmd, cwd=self.install_dir)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700469 except subprocess.CalledProcessError, e:
Chris Sosa76e44b92013-01-31 12:11:38 -0800470 raise ArtifactDownloadError(
471 'Failed to create autotest packages!:\n%s' % e)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700472 else:
Gilad Arnoldf5843132012-09-25 00:31:20 -0700473 self._Log('Using pre-generated packages from autotest')
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700474
Chris Masone816e38c2012-05-02 12:22:36 -0700475
Chris Sosa76e44b92013-01-31 12:11:38 -0800476class ZipfileBuildArtifact(BundledBuildArtifact):
477 """A downloadable artifact that is a zipfile."""
Chris Masone816e38c2012-05-02 12:22:36 -0700478
Gilad Arnold1638d822013-11-07 23:38:16 -0800479 def _RunUnzip(self, list_only):
480 # Unzip is weird. It expects its args before any excludes and expects its
481 # excludes in a list following the -x.
482 cmd = ['unzip', '-qql' if list_only else '-o', self.install_path]
483 if not list_only:
484 cmd += ['-d', self.install_dir]
485
Chris Sosa76e44b92013-01-31 12:11:38 -0800486 if self._files_to_extract:
487 cmd.extend(self._files_to_extract)
Chris Masone816e38c2012-05-02 12:22:36 -0700488
Chris Sosa76e44b92013-01-31 12:11:38 -0800489 if self._exclude:
490 cmd.append('-x')
491 cmd.extend(self._exclude)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700492
493 try:
Gilad Arnold1638d822013-11-07 23:38:16 -0800494 return subprocess.check_output(cmd).strip('\n').splitlines()
Gilad Arnold6f99b982012-09-12 10:49:40 -0700495 except subprocess.CalledProcessError, e:
Chris Sosa76e44b92013-01-31 12:11:38 -0800496 raise ArtifactDownloadError(
497 'An error occurred when attempting to unzip %s:\n%s' %
joychen0a8e34e2013-06-24 17:58:36 -0700498 (self.install_path, e))
Gilad Arnold6f99b982012-09-12 10:49:40 -0700499
Gilad Arnold1638d822013-11-07 23:38:16 -0800500 def _Extract(self):
501 """Extracts files into the install path."""
502 file_list = [os.path.join(self.install_dir, line[30:].strip())
503 for line in self._RunUnzip(True)
504 if not line.endswith('/')]
505 if file_list:
506 self._RunUnzip(False)
507
508 return file_list
509
Gilad Arnold6f99b982012-09-12 10:49:40 -0700510
Chris Sosa76e44b92013-01-31 12:11:38 -0800511class ImplDescription(object):
512 """Data wrapper that describes an artifact's implementation."""
Gilad Arnold950569b2013-08-27 14:38:01 -0700513
514 def __init__(self, artifact_class, name, *additional_args,
515 **additional_dargs):
516 """Constructor.
Chris Sosa76e44b92013-01-31 12:11:38 -0800517
518 Args:
519 artifact_class: BuildArtifact class to use for the artifact.
520 name: name to use to identify artifact (see BuildArtifact.name)
Gilad Arnold950569b2013-08-27 14:38:01 -0700521 *additional_args: Additional arguments to pass to artifact_class.
522 **additional_dargs: Additional named arguments to pass to artifact_class.
Chris Sosa76e44b92013-01-31 12:11:38 -0800523 """
524 self.artifact_class = artifact_class
525 self.name = name
526 self.additional_args = additional_args
Gilad Arnold950569b2013-08-27 14:38:01 -0700527 self.additional_dargs = additional_dargs
Chris Sosa76e44b92013-01-31 12:11:38 -0800528
Chris Sosa968a1062013-08-02 17:42:50 -0700529 def __repr__(self):
530 return '%s_%s' % (self.artifact_class, self.name)
531
Chris Sosa76e44b92013-01-31 12:11:38 -0800532
533# Maps artifact names to their implementation description.
534# Please note, it is good practice to use constants for these names if you're
535# going to re-use the names ANYWHERE else in the devserver code.
536ARTIFACT_IMPLEMENTATION_MAP = {
Gilad Arnold950569b2013-08-27 14:38:01 -0700537 artifact_info.FULL_PAYLOAD:
538 ImplDescription(AUTestPayloadBuildArtifact, '*_full_*'),
539 artifact_info.DELTA_PAYLOADS:
540 ImplDescription(DeltaPayloadsArtifact, 'DONTCARE'),
541 artifact_info.STATEFUL_PAYLOAD:
542 ImplDescription(BuildArtifact, devserver_constants.STATEFUL_FILE),
Chris Sosa76e44b92013-01-31 12:11:38 -0800543
Gilad Arnold950569b2013-08-27 14:38:01 -0700544 artifact_info.BASE_IMAGE:
545 ImplDescription(ZipfileBuildArtifact, IMAGE_FILE,
Gilad Arnold69878b42013-09-18 13:39:22 -0700546 files_to_extract=[devserver_constants.BASE_IMAGE_FILE]),
Gilad Arnold950569b2013-08-27 14:38:01 -0700547 artifact_info.RECOVERY_IMAGE:
548 ImplDescription(ZipfileBuildArtifact, IMAGE_FILE,
Gilad Arnold69878b42013-09-18 13:39:22 -0700549 files_to_extract=[devserver_constants.RECOVERY_IMAGE_FILE]),
Chris Sosa75490802013-09-30 17:21:45 -0700550 artifact_info.DEV_IMAGE:
551 ImplDescription(ZipfileBuildArtifact, IMAGE_FILE,
552 files_to_extract=[devserver_constants.IMAGE_FILE]),
Gilad Arnold950569b2013-08-27 14:38:01 -0700553 artifact_info.TEST_IMAGE:
554 ImplDescription(ZipfileBuildArtifact, IMAGE_FILE,
Gilad Arnold69878b42013-09-18 13:39:22 -0700555 files_to_extract=[devserver_constants.TEST_IMAGE_FILE]),
Chris Sosa76e44b92013-01-31 12:11:38 -0800556
Gilad Arnold950569b2013-08-27 14:38:01 -0700557 artifact_info.AUTOTEST:
558 ImplDescription(AutotestTarballBuildArtifact, AUTOTEST_FILE,
559 files_to_extract=None,
560 exclude=['autotest/test_suites']),
561 artifact_info.TEST_SUITES:
562 ImplDescription(TarballBuildArtifact, TEST_SUITES_FILE),
563 artifact_info.AU_SUITE:
564 ImplDescription(TarballBuildArtifact, AU_SUITE_FILE),
Chris Sosa76e44b92013-01-31 12:11:38 -0800565
Gilad Arnold950569b2013-08-27 14:38:01 -0700566 artifact_info.FIRMWARE:
567 ImplDescription(BuildArtifact, FIRMWARE_FILE),
568 artifact_info.SYMBOLS:
569 ImplDescription(TarballBuildArtifact, DEBUG_SYMBOLS_FILE,
570 files_to_extract=['debug/breakpad']),
beepsc3d0f872013-07-31 21:50:40 -0700571
Gilad Arnold950569b2013-08-27 14:38:01 -0700572 artifact_info.FACTORY_IMAGE:
573 ImplDescription(ZipfileBuildArtifact, FACTORY_FILE,
Gilad Arnold69878b42013-09-18 13:39:22 -0700574 files_to_extract=[devserver_constants.FACTORY_IMAGE_FILE])
Chris Sosa76e44b92013-01-31 12:11:38 -0800575}
576
Chris Sosa968a1062013-08-02 17:42:50 -0700577# Add all the paygen_au artifacts in one go.
578ARTIFACT_IMPLEMENTATION_MAP.update({
Gilad Arnold950569b2013-08-27 14:38:01 -0700579 artifact_info.PAYGEN_AU_SUITE_TEMPLATE % {'channel': c}:
580 ImplDescription(
581 TarballBuildArtifact, PAYGEN_AU_SUITE_FILE_TEMPLATE % {'channel': c})
582 for c in devserver_constants.CHANNELS
Chris Sosa968a1062013-08-02 17:42:50 -0700583})
584
Chris Sosa76e44b92013-01-31 12:11:38 -0800585
586class ArtifactFactory(object):
587 """A factory class that generates build artifacts from artifact names."""
588
Chris Sosa6b0c6172013-08-05 17:01:33 -0700589 def __init__(self, download_dir, archive_url, artifacts, files,
590 build):
Chris Sosa76e44b92013-01-31 12:11:38 -0800591 """Initalizes the member variables for the factory.
592
593 Args:
Gilad Arnold950569b2013-08-27 14:38:01 -0700594 download_dir: A directory to which artifacts are downloaded.
Chris Sosa76e44b92013-01-31 12:11:38 -0800595 archive_url: the Google Storage url of the bucket where the debug
596 symbols for the desired build are stored.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700597 artifacts: List of artifacts to stage. These artifacts must be
598 defined in artifact_info.py and have a mapping in the
599 ARTIFACT_IMPLEMENTATION_MAP.
600 files: List of files to stage. These files are just downloaded and staged
601 as files into the download_dir.
Chris Sosa76e44b92013-01-31 12:11:38 -0800602 build: The name of the build.
603 """
joychen0a8e34e2013-06-24 17:58:36 -0700604 self.download_dir = download_dir
Chris Sosa76e44b92013-01-31 12:11:38 -0800605 self.archive_url = archive_url
Chris Sosa6b0c6172013-08-05 17:01:33 -0700606 self.artifacts = artifacts
607 self.files = files
Chris Sosa76e44b92013-01-31 12:11:38 -0800608 self.build = build
609
610 @staticmethod
Chris Sosa6b0c6172013-08-05 17:01:33 -0700611 def _GetDescriptionComponents(name, is_artifact):
Gilad Arnold950569b2013-08-27 14:38:01 -0700612 """Returns components for constructing a BuildArtifact.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700613
Gilad Arnold950569b2013-08-27 14:38:01 -0700614 Args:
615 name: The artifact name / file pattern.
616 is_artifact: Whether this is a named (True) or file (False) artifact.
617 Returns:
618 A tuple consisting of the BuildArtifact subclass, name, and additional
619 list- and named-arguments.
620 Raises:
621 KeyError: if artifact doesn't exist in ARTIFACT_IMPLEMENTATION_MAP.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700622 """
623
624 if is_artifact:
625 description = ARTIFACT_IMPLEMENTATION_MAP[name]
626 else:
627 description = ImplDescription(BuildArtifact, name)
628
Chris Sosa76e44b92013-01-31 12:11:38 -0800629 return (description.artifact_class, description.name,
Gilad Arnold950569b2013-08-27 14:38:01 -0700630 description.additional_args, description.additional_dargs)
Chris Sosa76e44b92013-01-31 12:11:38 -0800631
Chris Sosa6b0c6172013-08-05 17:01:33 -0700632 def _Artifacts(self, names, is_artifact):
Gilad Arnold950569b2013-08-27 14:38:01 -0700633 """Returns the BuildArtifacts from |names|.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700634
635 If is_artifact is true, then these names define artifacts that must exist in
636 the ARTIFACT_IMPLEMENTATION_MAP. Otherwise, treat as filenames to stage as
637 basic BuildArtifacts.
638
Gilad Arnold950569b2013-08-27 14:38:01 -0700639 Args:
640 names: A sequence of artifact names.
641 is_artifact: Whether this is a named (True) or file (False) artifact.
642 Returns:
643 An iterable of BuildArtifacts.
644 Raises:
645 KeyError: if artifact doesn't exist in ARTIFACT_IMPLEMENTATION_MAP.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700646 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800647 artifacts = []
Chris Sosa6b0c6172013-08-05 17:01:33 -0700648 for name in names:
Gilad Arnold950569b2013-08-27 14:38:01 -0700649 artifact_class, path, args, dargs = self._GetDescriptionComponents(
Chris Sosa6b0c6172013-08-05 17:01:33 -0700650 name, is_artifact)
joychen0a8e34e2013-06-24 17:58:36 -0700651 artifacts.append(artifact_class(self.download_dir, self.archive_url, path,
Gilad Arnold950569b2013-08-27 14:38:01 -0700652 self.build, *args, **dargs))
Chris Sosa76e44b92013-01-31 12:11:38 -0800653
654 return artifacts
655
656 def RequiredArtifacts(self):
Gilad Arnold950569b2013-08-27 14:38:01 -0700657 """Returns BuildArtifacts for the factory's artifacts.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700658
Gilad Arnold950569b2013-08-27 14:38:01 -0700659 Returns:
660 An iterable of BuildArtifacts.
661 Raises:
662 KeyError: if artifact doesn't exist in ARTIFACT_IMPLEMENTATION_MAP.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700663 """
664 artifacts = []
665 if self.artifacts:
666 artifacts.extend(self._Artifacts(self.artifacts, True))
667 if self.files:
668 artifacts.extend(self._Artifacts(self.files, False))
669
670 return artifacts
Chris Sosa76e44b92013-01-31 12:11:38 -0800671
672 def OptionalArtifacts(self):
Gilad Arnold950569b2013-08-27 14:38:01 -0700673 """Returns BuildArtifacts that should be cached.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700674
Gilad Arnold950569b2013-08-27 14:38:01 -0700675 Returns:
676 An iterable of BuildArtifacts.
677 Raises:
678 KeyError: if an optional artifact doesn't exist in
679 ARTIFACT_IMPLEMENTATION_MAP yet defined in
680 artifact_info.REQUESTED_TO_OPTIONAL_MAP.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700681 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800682 optional_names = set()
683 for artifact_name, optional_list in (
684 artifact_info.REQUESTED_TO_OPTIONAL_MAP.iteritems()):
685 # We are already downloading it.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700686 if artifact_name in self.artifacts:
Chris Sosa76e44b92013-01-31 12:11:38 -0800687 optional_names = optional_names.union(optional_list)
688
Chris Sosa6b0c6172013-08-05 17:01:33 -0700689 return self._Artifacts(optional_names - set(self.artifacts), True)
Chris Sosa968a1062013-08-02 17:42:50 -0700690
691
692# A simple main to verify correctness of the artifact map when making simple
693# name changes.
694if __name__ == '__main__':
695 print 'ARTIFACT IMPLEMENTATION MAP (for debugging)'
696 print 'FORMAT: ARTIFACT -> IMPLEMENTATION (<class>_file)'
697 for key, value in sorted(ARTIFACT_IMPLEMENTATION_MAP.items()):
698 print '%s -> %s' % (key, value)