blob: ae55848a7200e2567627b000e4b6c24d51255e8e [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'
Simran Basiea0590d2014-10-29 11:31:26 -070031CONTROL_FILES_FILE = 'control_files.tar'
32AUTOTEST_PACKAGES_FILE = 'autotest_packages.tar'
Chris Sosa76e44b92013-01-31 12:11:38 -080033AUTOTEST_COMPRESSED_FILE = 'autotest.tar.bz2'
34DEBUG_SYMBOLS_FILE = 'debug.tgz'
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -080035FACTORY_FILE = 'ChromeOS-factory*.zip'
Chris Sosa76e44b92013-01-31 12:11:38 -080036FIRMWARE_FILE = 'firmware_from_source.tar.bz2'
37IMAGE_FILE = 'image.zip'
Chris Sosa76e44b92013-01-31 12:11:38 -080038TEST_SUITES_FILE = 'test_suites.tar.bz2'
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -080039BASE_IMAGE_FILE = 'chromiumos_base_image.tar.xz'
40TEST_IMAGE_FILE = 'chromiumos_test_image.tar.xz'
41RECOVERY_IMAGE_FILE = 'recovery_image.tar.xz'
42
Chris Sosa76e44b92013-01-31 12:11:38 -080043
44_build_artifact_locks = common_util.LockDict()
Chris Sosa47a7d4e2012-03-28 11:26:55 -070045
46
47class ArtifactDownloadError(Exception):
48 """Error used to signify an issue processing an artifact."""
49 pass
50
51
Gilad Arnoldc65330c2012-09-20 15:17:48 -070052class BuildArtifact(log_util.Loggable):
Chris Sosa47a7d4e2012-03-28 11:26:55 -070053 """Wrapper around an artifact to download from gsutil.
54
55 The purpose of this class is to download objects from Google Storage
56 and install them to a local directory. There are two main functions, one to
57 download/prepare the artifacts in to a temporary staging area and the second
58 to stage it into its final destination.
Chris Sosa76e44b92013-01-31 12:11:38 -080059
Gilad Arnold950569b2013-08-27 14:38:01 -070060 IMPORTANT! (i) `name' is a glob expression by default (and not a regex), be
61 attentive when adding new artifacts; (ii) name matching semantics differ
62 between a glob (full name string match) and a regex (partial match).
63
Chris Sosa76e44b92013-01-31 12:11:38 -080064 Class members:
Gilad Arnold950569b2013-08-27 14:38:01 -070065 archive_url: An archive URL.
66 name: Name given for artifact; in fact, it is a pattern that captures the
67 names of files contained in the artifact. This can either be an
68 ordinary shell-style glob (the default), or a regular expression (if
69 is_regex_name is True).
70 is_regex_name: Whether the name value is a regex (default: glob).
Chris Sosa76e44b92013-01-31 12:11:38 -080071 build: The version of the build i.e. R26-2342.0.0.
72 marker_name: Name used to define the lock marker for the artifacts to
73 prevent it from being re-downloaded. By default based on name
74 but can be overriden by children.
Dan Shi6e50c722013-08-19 15:05:06 -070075 exception_file_path: Path to a file containing the serialized exception,
76 which was raised in Process method. The file is located
77 in the parent folder of install_dir, since the
78 install_dir will be deleted if the build does not
79 existed.
joychen0a8e34e2013-06-24 17:58:36 -070080 install_path: Path to artifact.
Chris Sosa76e44b92013-01-31 12:11:38 -080081 install_dir: The final location where the artifact should be staged to.
82 single_name: If True the name given should only match one item. Note, if not
83 True, self.name will become a list of items returned.
Gilad Arnold1638d822013-11-07 23:38:16 -080084 installed_files: A list of files that were the final result of downloading
85 and setting up the artifact.
86 store_installed_files: Whether the list of installed files is stored in the
87 marker file.
Chris Sosa47a7d4e2012-03-28 11:26:55 -070088 """
Gilad Arnold950569b2013-08-27 14:38:01 -070089
90 def __init__(self, install_dir, archive_url, name, build,
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -080091 is_regex_name=False, optional_name=None):
Gilad Arnold950569b2013-08-27 14:38:01 -070092 """Constructor.
93
94 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -080095 install_dir: Where to install the artifact.
96 archive_url: The Google Storage path to find the artifact.
97 name: Identifying name to be used to find/store the artifact.
98 build: The name of the build e.g. board/release.
Gilad Arnold950569b2013-08-27 14:38:01 -070099 is_regex_name: Whether the name pattern is a regex (default: glob).
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800100 optional_name: An alternative name to find the artifact, which can lead
101 to faster download. Unlike |name|, there is no guarantee that an
102 artifact named |optional_name| is/will be on Google Storage. If it
103 exists, we download it. Otherwise, we fall back to wait for |name|.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700104 """
Chris Sosa6a3697f2013-01-29 16:44:43 -0800105 super(BuildArtifact, self).__init__()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700106
Chris Sosa76e44b92013-01-31 12:11:38 -0800107 # In-memory lock to keep the devserver from colliding with itself while
108 # attempting to stage the same artifact.
109 self._process_lock = None
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700110
Chris Sosa76e44b92013-01-31 12:11:38 -0800111 self.archive_url = archive_url
112 self.name = name
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800113 self.optional_name = optional_name
Gilad Arnold950569b2013-08-27 14:38:01 -0700114 self.is_regex_name = is_regex_name
Chris Sosa76e44b92013-01-31 12:11:38 -0800115 self.build = build
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700116
Chris Sosa76e44b92013-01-31 12:11:38 -0800117 self.marker_name = '.' + self._SanitizeName(name)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700118
Dan Shi6e50c722013-08-19 15:05:06 -0700119 exception_file_name = ('.' + self._SanitizeName(build) + self.marker_name +
120 '.exception')
121 # The exception file needs to be located in parent folder, since the
122 # install_dir will be deleted is the build does not exist.
123 self.exception_file_path = os.path.join(os.path.dirname(install_dir),
Gilad Arnold950569b2013-08-27 14:38:01 -0700124 exception_file_name)
Dan Shi6e50c722013-08-19 15:05:06 -0700125
joychen0a8e34e2013-06-24 17:58:36 -0700126 self.install_path = None
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700127
Chris Sosa76e44b92013-01-31 12:11:38 -0800128 self.install_dir = install_dir
129
130 self.single_name = True
131
Gilad Arnold1638d822013-11-07 23:38:16 -0800132 self.installed_files = []
133 self.store_installed_files = True
134
Chris Sosa76e44b92013-01-31 12:11:38 -0800135 @staticmethod
136 def _SanitizeName(name):
137 """Sanitizes name to be used for creating a file on the filesystem.
138
139 '.','/' and '*' have special meaning in FS lingo. Replace them with words.
Gilad Arnold950569b2013-08-27 14:38:01 -0700140
141 Args:
142 name: A file name/path.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800143
Gilad Arnold950569b2013-08-27 14:38:01 -0700144 Returns:
145 The sanitized name/path.
Chris Sosa76e44b92013-01-31 12:11:38 -0800146 """
147 return name.replace('*', 'STAR').replace('.', 'DOT').replace('/', 'SLASH')
148
Dan Shif8eb0d12013-08-01 17:52:06 -0700149 def ArtifactStaged(self):
Gilad Arnold1638d822013-11-07 23:38:16 -0800150 """Returns True if artifact is already staged.
151
152 This checks for (1) presence of the artifact marker file, and (2) the
153 presence of each installed file listed in this marker. Both must hold for
154 the artifact to be considered staged. Note that this method is safe for use
155 even if the artifacts were not stageed by this instance, as it is assumed
156 that any BuildArtifact instance that did the staging wrote the list of
157 files actually installed into the marker.
158 """
159 marker_file = os.path.join(self.install_dir, self.marker_name)
160
161 # If the marker is missing, it's definitely not staged.
162 if not os.path.exists(marker_file):
163 return False
164
165 # We want to ensure that every file listed in the marker is actually there.
166 if self.store_installed_files:
167 with open(marker_file) as f:
168 files = [line.strip() for line in f]
169
170 # Check to see if any of the purportedly installed files are missing, in
171 # which case the marker is outdated and should be removed.
172 missing_files = [fname for fname in files if not os.path.exists(fname)]
173 if missing_files:
174 self._Log('***ATTENTION*** %s files listed in %s are missing:\n%s',
175 'All' if len(files) == len(missing_files) else 'Some',
176 marker_file, '\n'.join(missing_files))
177 os.remove(marker_file)
178 return False
179
180 return True
Chris Sosa76e44b92013-01-31 12:11:38 -0800181
182 def _MarkArtifactStaged(self):
183 """Marks the artifact as staged."""
184 with open(os.path.join(self.install_dir, self.marker_name), 'w') as f:
Gilad Arnold1638d822013-11-07 23:38:16 -0800185 f.write('\n'.join(self.installed_files))
Chris Sosa76e44b92013-01-31 12:11:38 -0800186
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800187 def _WaitForArtifactToExist(self, name, timeout):
Chris Sosac4e87842013-08-16 18:04:14 -0700188 """Waits for artifact to exist and sets self.name to appropriate name.
189
190 Args:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800191 name: Name to look at.
Gilad Arnold950569b2013-08-27 14:38:01 -0700192 timeout: How long to wait for artifact to become available.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800193
Gilad Arnold950569b2013-08-27 14:38:01 -0700194 Raises:
195 ArtifactDownloadError: An error occurred when obtaining artifact.
Chris Sosac4e87842013-08-16 18:04:14 -0700196 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800197 names = gsutil_util.GetGSNamesWithWait(
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800198 name, self.archive_url, str(self), timeout=timeout,
Gilad Arnold950569b2013-08-27 14:38:01 -0700199 is_regex_pattern=self.is_regex_name)
Chris Sosa76e44b92013-01-31 12:11:38 -0800200 if not names:
Don Garrettef484fb2013-11-22 16:56:18 -0800201 raise ArtifactDownloadError('Could not find %s in Google Storage at %s' %
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800202 (name, self.archive_url))
203 return names
Chris Sosa76e44b92013-01-31 12:11:38 -0800204
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800205 def _UpdateName(self, names):
206 if self.single_name and len(names) > 1:
207 raise ArtifactDownloadError('Too many artifacts match %s' % self.name)
Chris Sosa76e44b92013-01-31 12:11:38 -0800208
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800209 self.name = names[0]
Chris Sosa76e44b92013-01-31 12:11:38 -0800210
211 def _Download(self):
joychen0a8e34e2013-06-24 17:58:36 -0700212 """Downloads artifact from Google Storage to a local directory."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800213 gs_path = '/'.join([self.archive_url, self.name])
joychen0a8e34e2013-06-24 17:58:36 -0700214 self.install_path = os.path.join(self.install_dir, self.name)
215 gsutil_util.DownloadFromGS(gs_path, self.install_path)
Chris Sosa76e44b92013-01-31 12:11:38 -0800216
joychen0a8e34e2013-06-24 17:58:36 -0700217 def _Setup(self):
Gilad Arnold1638d822013-11-07 23:38:16 -0800218 """Process the downloaded content, update the list of installed files."""
219 # In this primitive case, what was downloaded (has to be a single file) is
220 # what's installed.
221 self.installed_files = [self.install_path]
joychen0a8e34e2013-06-24 17:58:36 -0700222
Dan Shi6e50c722013-08-19 15:05:06 -0700223 def _ClearException(self):
224 """Delete any existing exception saved for this artifact."""
225 if os.path.exists(self.exception_file_path):
226 os.remove(self.exception_file_path)
227
228 def _SaveException(self, e):
229 """Save the exception to a file for downloader.IsStaged to retrieve.
230
Gilad Arnold950569b2013-08-27 14:38:01 -0700231 Args:
232 e: Exception object to be saved.
Dan Shi6e50c722013-08-19 15:05:06 -0700233 """
234 with open(self.exception_file_path, 'w') as f:
235 pickle.dump(e, f)
236
237 def GetException(self):
238 """Retrieve any exception that was raised in Process method.
239
Gilad Arnold950569b2013-08-27 14:38:01 -0700240 Returns:
241 An Exception object that was raised when trying to process the artifact.
242 Return None if no exception was found.
Dan Shi6e50c722013-08-19 15:05:06 -0700243 """
244 if not os.path.exists(self.exception_file_path):
245 return None
246 with open(self.exception_file_path, 'r') as f:
247 return pickle.load(f)
Chris Sosa76e44b92013-01-31 12:11:38 -0800248
249 def Process(self, no_wait):
250 """Main call point to all artifacts. Downloads and Stages artifact.
251
252 Downloads and Stages artifact from Google Storage to the install directory
253 specified in the constructor. It multi-thread safe and does not overwrite
254 the artifact if it's already been downloaded or being downloaded. After
255 processing, leaves behind a marker to indicate to future invocations that
256 the artifact has already been staged based on the name of the artifact.
257
258 Do not override as it modifies important private variables, ensures thread
259 safety, and maintains cache semantics.
260
261 Note: this may be a blocking call when the artifact is already in the
262 process of being staged.
263
264 Args:
265 no_wait: If True, don't block waiting for artifact to exist if we fail to
266 immediately find it.
267
268 Raises:
269 ArtifactDownloadError: If the artifact fails to download from Google
270 Storage for any reason or that the regexp
271 defined by name is not specific enough.
272 """
273 if not self._process_lock:
274 self._process_lock = _build_artifact_locks.lock(
275 os.path.join(self.install_dir, self.name))
276
277 with self._process_lock:
278 common_util.MkDirP(self.install_dir)
Dan Shif8eb0d12013-08-01 17:52:06 -0700279 if not self.ArtifactStaged():
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800280 # Delete any existing exception saved for this artifact.
281 self._ClearException()
282 found_artifact = False
283 if self.optional_name:
284 try:
285 # Check if the artifact named |optional_name| exists on GS.
286 # Because this artifact may not always exist, don't bother
287 # to wait for it (set timeout=1).
288 new_names = self._WaitForArtifactToExist(
289 self.optional_name, timeout=1)
290 self._UpdateName(new_names)
291
292 except ArtifactDownloadError:
293 self._Log('Unable to download %s; fall back to download %s',
294 self.optional_name, self.name)
295 else:
296 found_artifact = True
297
Dan Shi6e50c722013-08-19 15:05:06 -0700298 try:
Dan Shi6e50c722013-08-19 15:05:06 -0700299 # If the artifact should already have been uploaded, don't waste
300 # cycles waiting around for it to exist.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800301 if not found_artifact:
302 timeout = 1 if no_wait else 10
303 new_names = self._WaitForArtifactToExist(self.name, timeout)
304 self._UpdateName(new_names)
305
306 self._Log('Downloading file %s', self.name)
Dan Shi6e50c722013-08-19 15:05:06 -0700307 self._Download()
308 self._Setup()
309 self._MarkArtifactStaged()
310 except Exception as e:
311 # Save the exception to a file for downloader.IsStaged to retrieve.
312 self._SaveException(e)
Gilad Arnold02dc6552013-11-14 11:27:54 -0800313
314 # Convert an unknown exception into an ArtifactDownloadError.
315 if type(e) is ArtifactDownloadError:
316 raise
317 else:
318 raise ArtifactDownloadError('An error occurred: %s' % e)
Chris Sosa76e44b92013-01-31 12:11:38 -0800319 else:
320 self._Log('%s is already staged.', self)
321
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700322 def __str__(self):
323 """String representation for the download."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800324 return '->'.join(['%s/%s' % (self.archive_url, self.name),
Gilad Arnold950569b2013-08-27 14:38:01 -0700325 self.install_dir])
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700326
Chris Sosab26b1202013-08-16 16:40:55 -0700327 def __repr__(self):
328 return str(self)
329
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700330
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700331class AUTestPayloadBuildArtifact(BuildArtifact):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700332 """Wrapper for AUTest delta payloads which need additional setup."""
Gilad Arnold950569b2013-08-27 14:38:01 -0700333
joychen0a8e34e2013-06-24 17:58:36 -0700334 def _Setup(self):
335 super(AUTestPayloadBuildArtifact, self)._Setup()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700336
Chris Sosa76e44b92013-01-31 12:11:38 -0800337 # Rename to update.gz.
338 install_path = os.path.join(self.install_dir, self.name)
joychen3cb228e2013-06-12 12:13:13 -0700339 new_install_path = os.path.join(self.install_dir,
joychen7c2054a2013-07-25 11:14:07 -0700340 devserver_constants.UPDATE_FILE)
Chris Sosa76e44b92013-01-31 12:11:38 -0800341 shutil.move(install_path, new_install_path)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700342
Gilad Arnold1638d822013-11-07 23:38:16 -0800343 # Reflect the rename in the list of installed files.
344 self.installed_files.remove(install_path)
345 self.installed_files = [new_install_path]
346
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700347
Chris Sosa76e44b92013-01-31 12:11:38 -0800348# TODO(sosa): Change callers to make this artifact more sane.
349class DeltaPayloadsArtifact(BuildArtifact):
350 """Delta payloads from the archive_url.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700351
Chris Sosa76e44b92013-01-31 12:11:38 -0800352 This artifact is super strange. It custom handles directories and
353 pulls in all delta payloads. We can't specify exactly what we want
354 because unlike other artifacts, this one does not conform to something a
355 client might know. The client doesn't know the version of n-1 or whether it
356 was even generated.
Gilad Arnold950569b2013-08-27 14:38:01 -0700357
358 IMPORTANT! Note that this artifact simply ignores the `name' argument because
359 that name is derived internally in accordance with sub-artifacts. Also note
360 the different types of names (in fact, file name patterns) used for the
361 different sub-artifacts.
Chris Sosa76e44b92013-01-31 12:11:38 -0800362 """
Gilad Arnold950569b2013-08-27 14:38:01 -0700363
Chris Sosa76e44b92013-01-31 12:11:38 -0800364 def __init__(self, *args):
365 super(DeltaPayloadsArtifact, self).__init__(*args)
Gilad Arnold950569b2013-08-27 14:38:01 -0700366 # Override the name field, we know what it should be.
367 self.name = '*_delta_*'
368 self.is_regex_name = False
369 self.single_name = False # Expect multiple deltas
370
371 # We use a regular glob for the N-to-N delta payload.
372 nton_name = 'chromeos_%s*_delta_*' % self.build
373 # We use a regular expression for the M-to-N delta payload.
374 mton_name = ('chromeos_(?!%s).*_delta_.*' % re.escape(self.build))
375
Chris Sosa76e44b92013-01-31 12:11:38 -0800376 nton_install_dir = os.path.join(self.install_dir, _AU_BASE,
377 self.build + _NTON_DIR_SUFFIX)
378 mton_install_dir = os.path.join(self.install_dir, _AU_BASE,
Gilad Arnold950569b2013-08-27 14:38:01 -0700379 self.build + _MTON_DIR_SUFFIX)
Chris Sosa76e44b92013-01-31 12:11:38 -0800380 self._sub_artifacts = [
381 AUTestPayloadBuildArtifact(mton_install_dir, self.archive_url,
Gilad Arnold950569b2013-08-27 14:38:01 -0700382 mton_name, self.build, is_regex_name=True),
Chris Sosa76e44b92013-01-31 12:11:38 -0800383 AUTestPayloadBuildArtifact(nton_install_dir, self.archive_url,
384 nton_name, self.build)]
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700385
Chris Sosa76e44b92013-01-31 12:11:38 -0800386 def _Download(self):
joychen0a8e34e2013-06-24 17:58:36 -0700387 """With sub-artifacts we do everything in _Setup()."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800388 pass
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700389
joychen0a8e34e2013-06-24 17:58:36 -0700390 def _Setup(self):
Chris Sosa76e44b92013-01-31 12:11:38 -0800391 """Process each sub-artifact. Only error out if none can be found."""
392 for artifact in self._sub_artifacts:
393 try:
394 artifact.Process(no_wait=True)
395 # Setup symlink so that AU will work for this payload.
Gilad Arnold1638d822013-11-07 23:38:16 -0800396 stateful_update_symlink = os.path.join(
397 artifact.install_dir, devserver_constants.STATEFUL_FILE)
Chris Sosa76e44b92013-01-31 12:11:38 -0800398 os.symlink(
joychen25d25972013-07-30 14:54:16 -0700399 os.path.join(os.pardir, os.pardir,
joychen121fc9b2013-08-02 14:30:30 -0700400 devserver_constants.STATEFUL_FILE),
Gilad Arnold1638d822013-11-07 23:38:16 -0800401 stateful_update_symlink)
402
403 # Aggregate sub-artifact file lists, including stateful symlink.
404 self.installed_files += artifact.installed_files
405 self.installed_files.append(stateful_update_symlink)
Chris Sosa76e44b92013-01-31 12:11:38 -0800406 except ArtifactDownloadError as e:
407 self._Log('Could not process %s: %s', artifact, e)
Gilad Arnold02dc6552013-11-14 11:27:54 -0800408 raise
Yu-Ju Honge61cbe92012-07-10 14:10:26 -0700409
Chris Sosa76e44b92013-01-31 12:11:38 -0800410
411class BundledBuildArtifact(BuildArtifact):
412 """A single build artifact bundle e.g. zip file or tar file."""
Chris Sosa76e44b92013-01-31 12:11:38 -0800413
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800414 def __init__(self, *args, **kwargs):
Gilad Arnold950569b2013-08-27 14:38:01 -0700415 """Takes BuildArtifact args with some additional ones.
416
417 Args:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800418 *args: See BuildArtifact documentation.
419 **kwargs: See BuildArtifact documentation.
Gilad Arnold950569b2013-08-27 14:38:01 -0700420 files_to_extract: A list of files to extract. If set to None, extract
421 all files.
422 exclude: A list of files to exclude. If None, no files are excluded.
Chris Sosa76e44b92013-01-31 12:11:38 -0800423 """
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800424 self._files_to_extract = kwargs.pop('files_to_extract', None)
425 self._exclude = kwargs.pop('exclude', None)
426 super(BundledBuildArtifact, self).__init__(*args, **kwargs)
Chris Sosa76e44b92013-01-31 12:11:38 -0800427
428 # We modify the marker so that it is unique to what was staged.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800429 if self._files_to_extract:
Chris Sosa76e44b92013-01-31 12:11:38 -0800430 self.marker_name = self._SanitizeName(
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800431 '_'.join(['.' + self.name] + self._files_to_extract))
Chris Sosa76e44b92013-01-31 12:11:38 -0800432
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800433 def _RunUnzip(self, list_only):
434 # Unzip is weird. It expects its args before any excludes and expects its
435 # excludes in a list following the -x.
436 cmd = ['unzip', '-qql' if list_only else '-o', self.install_path]
437 if not list_only:
438 cmd += ['-d', self.install_dir]
Chris Sosa76e44b92013-01-31 12:11:38 -0800439
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800440 if self._files_to_extract:
441 cmd.extend(self._files_to_extract)
442
443 if self._exclude:
444 cmd.append('-x')
445 cmd.extend(self._exclude)
446
447 try:
448 return subprocess.check_output(cmd).strip('\n').splitlines()
449 except subprocess.CalledProcessError, e:
450 raise ArtifactDownloadError(
451 'An error occurred when attempting to unzip %s:\n%s' %
452 (self.install_path, e))
Chris Sosa76e44b92013-01-31 12:11:38 -0800453
joychen0a8e34e2013-06-24 17:58:36 -0700454 def _Setup(self):
Gilad Arnold1638d822013-11-07 23:38:16 -0800455 extract_result = self._Extract()
456 if self.store_installed_files:
457 # List both the archive and the extracted files.
458 self.installed_files.append(self.install_path)
459 self.installed_files.extend(extract_result)
Chris Sosa76e44b92013-01-31 12:11:38 -0800460
Chris Sosa76e44b92013-01-31 12:11:38 -0800461 def _Extract(self):
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800462 """Extracts files into the install path."""
463 if self.name.endswith('.zip'):
464 return self._ExtractZipfile()
465 else:
466 return self._ExtractTarball()
467
468 def _ExtractZipfile(self):
469 """Extracts a zip file using unzip."""
470 file_list = [os.path.join(self.install_dir, line[30:].strip())
471 for line in self._RunUnzip(True)
472 if not line.endswith('/')]
473 if file_list:
474 self._RunUnzip(False)
475
476 return file_list
477
478 def _ExtractTarball(self):
Chris Sosa76e44b92013-01-31 12:11:38 -0800479 """Extracts a tarball using tar.
480
481 Detects whether the tarball is compressed or not based on the file
482 extension and extracts the tarball into the install_path.
483 """
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700484 try:
Gilad Arnold1638d822013-11-07 23:38:16 -0800485 return common_util.ExtractTarball(self.install_path, self.install_dir,
486 files_to_extract=self._files_to_extract,
487 excluded_files=self._exclude,
488 return_extracted_files=True)
Simran Basi4baad082013-02-14 13:39:18 -0800489 except common_util.CommonUtilError as e:
490 raise ArtifactDownloadError(str(e))
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700491
492
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800493class AutotestTarballBuildArtifact(BundledBuildArtifact):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700494 """Wrapper around the autotest tarball to download from gsutil."""
495
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800496 def __init__(self, *args, **kwargs):
497 super(AutotestTarballBuildArtifact, self).__init__(*args, **kwargs)
Gilad Arnold1638d822013-11-07 23:38:16 -0800498 # We don't store/check explicit file lists in Autotest tarball markers;
499 # this can get huge and unwieldy, and generally make little sense.
500 self.store_installed_files = False
501
joychen0a8e34e2013-06-24 17:58:36 -0700502 def _Setup(self):
Chris Sosa76e44b92013-01-31 12:11:38 -0800503 """Extracts the tarball into the install path excluding test suites."""
joychen0a8e34e2013-06-24 17:58:36 -0700504 super(AutotestTarballBuildArtifact, self)._Setup()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700505
Chris Sosa76e44b92013-01-31 12:11:38 -0800506 # Deal with older autotest packages that may not be bundled.
joychen3cb228e2013-06-12 12:13:13 -0700507 autotest_dir = os.path.join(self.install_dir,
508 devserver_constants.AUTOTEST_DIR)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700509 autotest_pkgs_dir = os.path.join(autotest_dir, 'packages')
510 if not os.path.exists(autotest_pkgs_dir):
511 os.makedirs(autotest_pkgs_dir)
512
513 if not os.path.exists(os.path.join(autotest_pkgs_dir, 'packages.checksum')):
Chris Sosa76e44b92013-01-31 12:11:38 -0800514 cmd = ['autotest/utils/packager.py', 'upload', '--repository',
515 autotest_pkgs_dir, '--all']
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700516 try:
joychen0a8e34e2013-06-24 17:58:36 -0700517 subprocess.check_call(cmd, cwd=self.install_dir)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700518 except subprocess.CalledProcessError, e:
Chris Sosa76e44b92013-01-31 12:11:38 -0800519 raise ArtifactDownloadError(
520 'Failed to create autotest packages!:\n%s' % e)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700521 else:
Gilad Arnoldf5843132012-09-25 00:31:20 -0700522 self._Log('Using pre-generated packages from autotest')
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700523
Chris Masone816e38c2012-05-02 12:22:36 -0700524
Chris Sosa76e44b92013-01-31 12:11:38 -0800525class ImplDescription(object):
526 """Data wrapper that describes an artifact's implementation."""
Gilad Arnold950569b2013-08-27 14:38:01 -0700527
528 def __init__(self, artifact_class, name, *additional_args,
529 **additional_dargs):
530 """Constructor.
Chris Sosa76e44b92013-01-31 12:11:38 -0800531
532 Args:
533 artifact_class: BuildArtifact class to use for the artifact.
534 name: name to use to identify artifact (see BuildArtifact.name)
Gilad Arnold950569b2013-08-27 14:38:01 -0700535 *additional_args: Additional arguments to pass to artifact_class.
536 **additional_dargs: Additional named arguments to pass to artifact_class.
Chris Sosa76e44b92013-01-31 12:11:38 -0800537 """
538 self.artifact_class = artifact_class
539 self.name = name
540 self.additional_args = additional_args
Gilad Arnold950569b2013-08-27 14:38:01 -0700541 self.additional_dargs = additional_dargs
Chris Sosa76e44b92013-01-31 12:11:38 -0800542
Chris Sosa968a1062013-08-02 17:42:50 -0700543 def __repr__(self):
544 return '%s_%s' % (self.artifact_class, self.name)
545
Chris Sosa76e44b92013-01-31 12:11:38 -0800546
547# Maps artifact names to their implementation description.
548# Please note, it is good practice to use constants for these names if you're
549# going to re-use the names ANYWHERE else in the devserver code.
550ARTIFACT_IMPLEMENTATION_MAP = {
Gilad Arnold950569b2013-08-27 14:38:01 -0700551 artifact_info.FULL_PAYLOAD:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800552 ImplDescription(AUTestPayloadBuildArtifact, ('*_full_*')),
Gilad Arnold950569b2013-08-27 14:38:01 -0700553 artifact_info.DELTA_PAYLOADS:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800554 ImplDescription(DeltaPayloadsArtifact, ('DONTCARE')),
Gilad Arnold950569b2013-08-27 14:38:01 -0700555 artifact_info.STATEFUL_PAYLOAD:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800556 ImplDescription(BuildArtifact, (devserver_constants.STATEFUL_FILE)),
Chris Sosa76e44b92013-01-31 12:11:38 -0800557
Gilad Arnold950569b2013-08-27 14:38:01 -0700558 artifact_info.BASE_IMAGE:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800559 ImplDescription(BundledBuildArtifact, IMAGE_FILE,
560 optional_name=BASE_IMAGE_FILE,
Gilad Arnold69878b42013-09-18 13:39:22 -0700561 files_to_extract=[devserver_constants.BASE_IMAGE_FILE]),
Gilad Arnold950569b2013-08-27 14:38:01 -0700562 artifact_info.RECOVERY_IMAGE:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800563 ImplDescription(BundledBuildArtifact, IMAGE_FILE,
564 optional_name=RECOVERY_IMAGE_FILE,
Gilad Arnold69878b42013-09-18 13:39:22 -0700565 files_to_extract=[devserver_constants.RECOVERY_IMAGE_FILE]),
Chris Sosa75490802013-09-30 17:21:45 -0700566 artifact_info.DEV_IMAGE:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800567 ImplDescription(BundledBuildArtifact, IMAGE_FILE,
Chris Sosa75490802013-09-30 17:21:45 -0700568 files_to_extract=[devserver_constants.IMAGE_FILE]),
Gilad Arnold950569b2013-08-27 14:38:01 -0700569 artifact_info.TEST_IMAGE:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800570 ImplDescription(BundledBuildArtifact, IMAGE_FILE,
571 optional_name=TEST_IMAGE_FILE,
Gilad Arnold69878b42013-09-18 13:39:22 -0700572 files_to_extract=[devserver_constants.TEST_IMAGE_FILE]),
Chris Sosa76e44b92013-01-31 12:11:38 -0800573
Gilad Arnold950569b2013-08-27 14:38:01 -0700574 artifact_info.AUTOTEST:
575 ImplDescription(AutotestTarballBuildArtifact, AUTOTEST_FILE,
576 files_to_extract=None,
577 exclude=['autotest/test_suites']),
Simran Basiea0590d2014-10-29 11:31:26 -0700578 artifact_info.CONTROL_FILES:
579 ImplDescription(BundledBuildArtifact, CONTROL_FILES_FILE),
580 artifact_info.AUTOTEST_PACKAGES:
581 ImplDescription(AutotestTarballBuildArtifact, AUTOTEST_PACKAGES_FILE),
Gilad Arnold950569b2013-08-27 14:38:01 -0700582 artifact_info.TEST_SUITES:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800583 ImplDescription(BundledBuildArtifact, TEST_SUITES_FILE),
Gilad Arnold950569b2013-08-27 14:38:01 -0700584 artifact_info.AU_SUITE:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800585 ImplDescription(BundledBuildArtifact, AU_SUITE_FILE),
Chris Sosa76e44b92013-01-31 12:11:38 -0800586
Gilad Arnold950569b2013-08-27 14:38:01 -0700587 artifact_info.FIRMWARE:
588 ImplDescription(BuildArtifact, FIRMWARE_FILE),
589 artifact_info.SYMBOLS:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800590 ImplDescription(BundledBuildArtifact, DEBUG_SYMBOLS_FILE,
Gilad Arnold950569b2013-08-27 14:38:01 -0700591 files_to_extract=['debug/breakpad']),
beepsc3d0f872013-07-31 21:50:40 -0700592
Gilad Arnold950569b2013-08-27 14:38:01 -0700593 artifact_info.FACTORY_IMAGE:
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800594 ImplDescription(BundledBuildArtifact, FACTORY_FILE,
Gilad Arnold69878b42013-09-18 13:39:22 -0700595 files_to_extract=[devserver_constants.FACTORY_IMAGE_FILE])
Chris Sosa76e44b92013-01-31 12:11:38 -0800596}
597
Chris Sosa968a1062013-08-02 17:42:50 -0700598# Add all the paygen_au artifacts in one go.
599ARTIFACT_IMPLEMENTATION_MAP.update({
Gilad Arnold950569b2013-08-27 14:38:01 -0700600 artifact_info.PAYGEN_AU_SUITE_TEMPLATE % {'channel': c}:
601 ImplDescription(
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800602 BundledBuildArtifact, PAYGEN_AU_SUITE_FILE_TEMPLATE % {'channel': c})
Gilad Arnold950569b2013-08-27 14:38:01 -0700603 for c in devserver_constants.CHANNELS
Chris Sosa968a1062013-08-02 17:42:50 -0700604})
605
Chris Sosa76e44b92013-01-31 12:11:38 -0800606
607class ArtifactFactory(object):
608 """A factory class that generates build artifacts from artifact names."""
609
Chris Sosa6b0c6172013-08-05 17:01:33 -0700610 def __init__(self, download_dir, archive_url, artifacts, files,
611 build):
Chris Sosa76e44b92013-01-31 12:11:38 -0800612 """Initalizes the member variables for the factory.
613
614 Args:
Gilad Arnold950569b2013-08-27 14:38:01 -0700615 download_dir: A directory to which artifacts are downloaded.
Chris Sosa76e44b92013-01-31 12:11:38 -0800616 archive_url: the Google Storage url of the bucket where the debug
617 symbols for the desired build are stored.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700618 artifacts: List of artifacts to stage. These artifacts must be
619 defined in artifact_info.py and have a mapping in the
620 ARTIFACT_IMPLEMENTATION_MAP.
621 files: List of files to stage. These files are just downloaded and staged
622 as files into the download_dir.
Chris Sosa76e44b92013-01-31 12:11:38 -0800623 build: The name of the build.
624 """
joychen0a8e34e2013-06-24 17:58:36 -0700625 self.download_dir = download_dir
Chris Sosa76e44b92013-01-31 12:11:38 -0800626 self.archive_url = archive_url
Chris Sosa6b0c6172013-08-05 17:01:33 -0700627 self.artifacts = artifacts
628 self.files = files
Chris Sosa76e44b92013-01-31 12:11:38 -0800629 self.build = build
630
631 @staticmethod
Chris Sosa6b0c6172013-08-05 17:01:33 -0700632 def _GetDescriptionComponents(name, is_artifact):
Gilad Arnold950569b2013-08-27 14:38:01 -0700633 """Returns components for constructing a BuildArtifact.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700634
Gilad Arnold950569b2013-08-27 14:38:01 -0700635 Args:
636 name: The artifact name / file pattern.
637 is_artifact: Whether this is a named (True) or file (False) artifact.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800638
Gilad Arnold950569b2013-08-27 14:38:01 -0700639 Returns:
640 A tuple consisting of the BuildArtifact subclass, name, and additional
641 list- and named-arguments.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800642
Gilad Arnold950569b2013-08-27 14:38:01 -0700643 Raises:
644 KeyError: if artifact doesn't exist in ARTIFACT_IMPLEMENTATION_MAP.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700645 """
646
647 if is_artifact:
648 description = ARTIFACT_IMPLEMENTATION_MAP[name]
649 else:
650 description = ImplDescription(BuildArtifact, name)
651
Chris Sosa76e44b92013-01-31 12:11:38 -0800652 return (description.artifact_class, description.name,
Gilad Arnold950569b2013-08-27 14:38:01 -0700653 description.additional_args, description.additional_dargs)
Chris Sosa76e44b92013-01-31 12:11:38 -0800654
Chris Sosa6b0c6172013-08-05 17:01:33 -0700655 def _Artifacts(self, names, is_artifact):
Gilad Arnold950569b2013-08-27 14:38:01 -0700656 """Returns the BuildArtifacts from |names|.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700657
658 If is_artifact is true, then these names define artifacts that must exist in
659 the ARTIFACT_IMPLEMENTATION_MAP. Otherwise, treat as filenames to stage as
660 basic BuildArtifacts.
661
Gilad Arnold950569b2013-08-27 14:38:01 -0700662 Args:
663 names: A sequence of artifact names.
664 is_artifact: Whether this is a named (True) or file (False) artifact.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800665
Gilad Arnold950569b2013-08-27 14:38:01 -0700666 Returns:
667 An iterable of BuildArtifacts.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800668
Gilad Arnold950569b2013-08-27 14:38:01 -0700669 Raises:
670 KeyError: if artifact doesn't exist in ARTIFACT_IMPLEMENTATION_MAP.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700671 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800672 artifacts = []
Chris Sosa6b0c6172013-08-05 17:01:33 -0700673 for name in names:
Gilad Arnold950569b2013-08-27 14:38:01 -0700674 artifact_class, path, args, dargs = self._GetDescriptionComponents(
Chris Sosa6b0c6172013-08-05 17:01:33 -0700675 name, is_artifact)
joychen0a8e34e2013-06-24 17:58:36 -0700676 artifacts.append(artifact_class(self.download_dir, self.archive_url, path,
Gilad Arnold950569b2013-08-27 14:38:01 -0700677 self.build, *args, **dargs))
Chris Sosa76e44b92013-01-31 12:11:38 -0800678
679 return artifacts
680
681 def RequiredArtifacts(self):
Gilad Arnold950569b2013-08-27 14:38:01 -0700682 """Returns BuildArtifacts for the factory's artifacts.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700683
Gilad Arnold950569b2013-08-27 14:38:01 -0700684 Returns:
685 An iterable of BuildArtifacts.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800686
Gilad Arnold950569b2013-08-27 14:38:01 -0700687 Raises:
688 KeyError: if artifact doesn't exist in ARTIFACT_IMPLEMENTATION_MAP.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700689 """
690 artifacts = []
691 if self.artifacts:
692 artifacts.extend(self._Artifacts(self.artifacts, True))
693 if self.files:
694 artifacts.extend(self._Artifacts(self.files, False))
695
696 return artifacts
Chris Sosa76e44b92013-01-31 12:11:38 -0800697
698 def OptionalArtifacts(self):
Gilad Arnold950569b2013-08-27 14:38:01 -0700699 """Returns BuildArtifacts that should be cached.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700700
Gilad Arnold950569b2013-08-27 14:38:01 -0700701 Returns:
702 An iterable of BuildArtifacts.
Yu-Ju Hong5d5bf0d2014-02-11 21:38:20 -0800703
Gilad Arnold950569b2013-08-27 14:38:01 -0700704 Raises:
705 KeyError: if an optional artifact doesn't exist in
706 ARTIFACT_IMPLEMENTATION_MAP yet defined in
707 artifact_info.REQUESTED_TO_OPTIONAL_MAP.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700708 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800709 optional_names = set()
710 for artifact_name, optional_list in (
711 artifact_info.REQUESTED_TO_OPTIONAL_MAP.iteritems()):
712 # We are already downloading it.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700713 if artifact_name in self.artifacts:
Chris Sosa76e44b92013-01-31 12:11:38 -0800714 optional_names = optional_names.union(optional_list)
715
Chris Sosa6b0c6172013-08-05 17:01:33 -0700716 return self._Artifacts(optional_names - set(self.artifacts), True)
Chris Sosa968a1062013-08-02 17:42:50 -0700717
718
719# A simple main to verify correctness of the artifact map when making simple
720# name changes.
721if __name__ == '__main__':
722 print 'ARTIFACT IMPLEMENTATION MAP (for debugging)'
723 print 'FORMAT: ARTIFACT -> IMPLEMENTATION (<class>_file)'
724 for key, value in sorted(ARTIFACT_IMPLEMENTATION_MAP.items()):
725 print '%s -> %s' % (key, value)