blob: 3bd0d037dcc1040537baf2fe2a34b6d2a897b0ee [file] [log] [blame]
Gabe Black3b567202015-09-23 14:07:59 -07001#!/usr/bin/python2
2#
Chris Sosa76e44b92013-01-31 12:11:38 -08003# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
Frank Farzan37761d12011-12-01 14:29:08 -08004# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
Gabe Black3b567202015-09-23 14:07:59 -07007"""Downloaders used to download artifacts and files from a given source."""
8
9from __future__ import print_function
10
Prashanth Ba06d2d22014-03-07 15:35:19 -080011import collections
Gabe Black3b567202015-09-23 14:07:59 -070012import glob
Chris Sosa9164ca32012-03-28 11:04:50 -070013import os
Gabe Black3b567202015-09-23 14:07:59 -070014import re
15import shutil
Gilad Arnold0b8c3f32012-09-19 14:35:44 -070016import threading
Prashanth Ba06d2d22014-03-07 15:35:19 -080017from datetime import datetime
Frank Farzan37761d12011-12-01 14:29:08 -080018
Chris Sosa76e44b92013-01-31 12:11:38 -080019import build_artifact
Gilad Arnoldc65330c2012-09-20 15:17:48 -070020import common_util
Gabe Black3b567202015-09-23 14:07:59 -070021import gsutil_util
Gilad Arnoldc65330c2012-09-20 15:17:48 -070022import log_util
Frank Farzan37761d12011-12-01 14:29:08 -080023
Dan Shi72b16132015-10-08 12:10:33 -070024try:
25 import android_build
26except ImportError as e:
27 # Ignore android_build import failure. This is to support devserver running
28 # inside a ChromeOS device triggered by cros flash. Most ChromeOS test images
29 # do not have google-api-python-client module and they don't need to support
30 # Android updating, therefore, ignore the import failure here.
31 android_build = None
32
Frank Farzan37761d12011-12-01 14:29:08 -080033
Dan Shi6e50c722013-08-19 15:05:06 -070034class DownloaderException(Exception):
35 """Exception that aggregates all exceptions raised during async download.
36
37 Exceptions could be raised in artifact.Process method, and saved to files.
38 When caller calls IsStaged to check the downloading progress, devserver can
39 retrieve the persisted exceptions from the files, wrap them into a
40 DownloaderException, and raise it.
41 """
42 def __init__(self, exceptions):
43 """Initialize a DownloaderException instance with a list of exceptions.
44
Gabe Black3b567202015-09-23 14:07:59 -070045 Args:
46 exceptions: Exceptions raised when downloading artifacts.
Dan Shi6e50c722013-08-19 15:05:06 -070047 """
48 message = 'Exceptions were raised when downloading artifacts.'
49 Exception.__init__(self, message)
50 self.exceptions = exceptions
51
52 def __repr__(self):
53 return self.__str__()
54
55 def __str__(self):
56 """Return a custom exception message with all exceptions merged."""
57 return '--------\n'.join([str(exception) for exception in self.exceptions])
58
Gilad Arnoldc65330c2012-09-20 15:17:48 -070059class Downloader(log_util.Loggable):
Chris Sosa76e44b92013-01-31 12:11:38 -080060 """Downloader of images to the devsever.
Frank Farzan37761d12011-12-01 14:29:08 -080061
Gabe Black3b567202015-09-23 14:07:59 -070062 This is the base class for different types of downloaders, including
Dan Shi72b16132015-10-08 12:10:33 -070063 GoogleStorageDownloader, LocalDownloader and AndroidBuildDownloader.
Gabe Black3b567202015-09-23 14:07:59 -070064
Frank Farzan37761d12011-12-01 14:29:08 -080065 Given a URL to a build on the archive server:
Chris Sosa76e44b92013-01-31 12:11:38 -080066 - Caches that build and the given artifacts onto the devserver.
67 - May also initiate caching of related artifacts in the background.
Frank Farzan37761d12011-12-01 14:29:08 -080068
Chris Sosa76e44b92013-01-31 12:11:38 -080069 Private class members:
Chris Sosa76e44b92013-01-31 12:11:38 -080070 static_dir: local filesystem directory to store all artifacts.
71 build_dir: the local filesystem directory to store artifacts for the given
Gabe Black3b567202015-09-23 14:07:59 -070072 build based on the remote source.
73
74 Public methods must be overridden:
75 Wait: Verifies the local artifact exists and returns the appropriate names.
76 Fetch: Downloads artifact from given source to a local directory.
77 DescribeSource: Gets the source of the download, e.g., a url to GS.
Frank Farzan37761d12011-12-01 14:29:08 -080078 """
79
Alex Millera44d5022012-07-27 11:34:16 -070080 # This filename must be kept in sync with clean_staged_images.py
81 _TIMESTAMP_FILENAME = 'staged.timestamp'
Chris Masonea22d9382012-05-18 12:38:51 -070082
Gabe Black3b567202015-09-23 14:07:59 -070083 def __init__(self, static_dir, build_dir, build):
Chris Sosa76e44b92013-01-31 12:11:38 -080084 super(Downloader, self).__init__()
Frank Farzan37761d12011-12-01 14:29:08 -080085 self._static_dir = static_dir
Gabe Black3b567202015-09-23 14:07:59 -070086 self._build_dir = build_dir
87 self._build = build
Chris Masone816e38c2012-05-02 12:22:36 -070088
Gabe Black3b567202015-09-23 14:07:59 -070089 def GetBuildDir(self):
90 """Returns the path to where the artifacts will be staged."""
91 return self._build_dir
Simran Basi4243a862014-12-12 12:48:33 -080092
Gabe Black3b567202015-09-23 14:07:59 -070093 def GetBuild(self):
94 """Returns the path to where the artifacts will be staged."""
95 return self._build
Frank Farzan37761d12011-12-01 14:29:08 -080096
Chris Sosa9164ca32012-03-28 11:04:50 -070097 @staticmethod
Simran Basief83d6a2014-08-28 14:32:01 -070098 def TouchTimestampForStaged(directory_path):
Alex Millera44d5022012-07-27 11:34:16 -070099 file_name = os.path.join(directory_path, Downloader._TIMESTAMP_FILENAME)
100 # Easiest python version of |touch file_name|
101 with file(file_name, 'a'):
102 os.utime(file_name, None)
103
Dan Shiba0e6742013-06-26 17:39:05 -0700104 @staticmethod
105 def _TryRemoveStageDir(directory_path):
Gilad Arnold02dc6552013-11-14 11:27:54 -0800106 """If download failed, try to remove the stage dir.
Dan Shiba0e6742013-06-26 17:39:05 -0700107
Gilad Arnold02dc6552013-11-14 11:27:54 -0800108 If the download attempt failed (ArtifactDownloadError) and staged.timestamp
109 is the only file in that directory. The build could be non-existing, and
110 the directory should be removed.
Dan Shiba0e6742013-06-26 17:39:05 -0700111
Gabe Black3b567202015-09-23 14:07:59 -0700112 Args:
113 directory_path: directory used to stage the image.
Dan Shiba0e6742013-06-26 17:39:05 -0700114 """
115 file_name = os.path.join(directory_path, Downloader._TIMESTAMP_FILENAME)
116 if os.path.exists(file_name) and len(os.listdir(directory_path)) == 1:
117 os.remove(file_name)
118 os.rmdir(directory_path)
119
Prashanth Ba06d2d22014-03-07 15:35:19 -0800120 def ListBuildDir(self):
121 """List the files in the build directory.
122
123 Only lists files a single level into the build directory. Includes
124 timestamp information in the listing.
125
126 Returns:
127 A string with information about the files in the build directory.
128 None if the build directory doesn't exist.
129
130 Raises:
131 build_artifact.ArtifactDownloadError: If the build_dir path exists
132 but is not a directory.
133 """
134 if not os.path.exists(self._build_dir):
135 return None
136 if not os.path.isdir(self._build_dir):
137 raise build_artifact.ArtifactDownloadError(
138 'Artifacts %s improperly staged to build_dir path %s. The path is '
139 'not a directory.' % (self._archive_url, self._build_dir))
140
141 ls_format = collections.namedtuple(
Gabe Black3b567202015-09-23 14:07:59 -0700142 'ls', ['name', 'accessed', 'modified', 'size'])
Prashanth Ba06d2d22014-03-07 15:35:19 -0800143 output_format = ('Name: %(name)s Accessed: %(accessed)s '
Gabe Black3b567202015-09-23 14:07:59 -0700144 'Modified: %(modified)s Size: %(size)s bytes.\n')
Prashanth Ba06d2d22014-03-07 15:35:19 -0800145
146 build_dir_info = 'Listing contents of :%s \n' % self._build_dir
147 for file_name in os.listdir(self._build_dir):
148 file_path = os.path.join(self._build_dir, file_name)
149 file_info = os.stat(file_path)
150 ls_info = ls_format(file_path,
151 datetime.fromtimestamp(file_info.st_atime),
152 datetime.fromtimestamp(file_info.st_mtime),
153 file_info.st_size)
154 build_dir_info += output_format % ls_info._asdict()
155 return build_dir_info
156
Gabe Black3b567202015-09-23 14:07:59 -0700157 def Download(self, factory, async=False):
Chris Sosa76e44b92013-01-31 12:11:38 -0800158 """Downloads and caches the |artifacts|.
Chris Sosa9164ca32012-03-28 11:04:50 -0700159
Gabe Black3b567202015-09-23 14:07:59 -0700160 Downloads and caches the |artifacts|. Returns once these are present on the
161 devserver. A call to this will attempt to cache non-specified artifacts in
162 the background following the principle of spatial locality.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700163
Chris Sosa75490802013-09-30 17:21:45 -0700164 Args:
Gabe Black3b567202015-09-23 14:07:59 -0700165 factory: The artifact factory.
Chris Sosa75490802013-09-30 17:21:45 -0700166 async: If True, return without waiting for download to complete.
167
168 Raises:
Gilad Arnold02dc6552013-11-14 11:27:54 -0800169 build_artifact.ArtifactDownloadError: If failed to download the artifact.
Dan Shif8eb0d12013-08-01 17:52:06 -0700170
Gilad Arnold6f99b982012-09-12 10:49:40 -0700171 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800172 common_util.MkDirP(self._build_dir)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700173
Chris Sosa76e44b92013-01-31 12:11:38 -0800174 # We are doing some work on this build -- let's touch it to indicate that
175 # we shouldn't be cleaning it up anytime soon.
Simran Basief83d6a2014-08-28 14:32:01 -0700176 Downloader.TouchTimestampForStaged(self._build_dir)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700177
Chris Sosa76e44b92013-01-31 12:11:38 -0800178 # Create factory to create build_artifacts from artifact names.
Chris Sosa76e44b92013-01-31 12:11:38 -0800179 background_artifacts = factory.OptionalArtifacts()
180 if background_artifacts:
181 self._DownloadArtifactsInBackground(background_artifacts)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700182
Chris Sosa76e44b92013-01-31 12:11:38 -0800183 required_artifacts = factory.RequiredArtifacts()
184 str_repr = [str(a) for a in required_artifacts]
185 self._Log('Downloading artifacts %s.', ' '.join(str_repr))
Dan Shie37f8fe2013-08-09 16:10:29 -0700186
Dan Shi6e50c722013-08-19 15:05:06 -0700187 if async:
188 self._DownloadArtifactsInBackground(required_artifacts)
189 else:
190 self._DownloadArtifactsSerially(required_artifacts, no_wait=True)
Chris Sosa76e44b92013-01-31 12:11:38 -0800191
Gabe Black3b567202015-09-23 14:07:59 -0700192 def IsStaged(self, factory):
Dan Shif8eb0d12013-08-01 17:52:06 -0700193 """Check if all artifacts have been downloaded.
194
Gabe Black3b567202015-09-23 14:07:59 -0700195 Args:
196 factory: An instance of BaseArtifactFactory to be used to check if desired
197 artifacts or files are staged.
198
199 Returns:
200 True if all artifacts are staged.
201
202 Raises:
203 DownloaderException: A wrapper for exceptions raised by any artifact when
204 calling Process.
Dan Shif8eb0d12013-08-01 17:52:06 -0700205
206 """
Dan Shif8eb0d12013-08-01 17:52:06 -0700207 required_artifacts = factory.RequiredArtifacts()
Dan Shi6e50c722013-08-19 15:05:06 -0700208 exceptions = [artifact.GetException() for artifact in required_artifacts if
209 artifact.GetException()]
210 if exceptions:
211 raise DownloaderException(exceptions)
212
Dan Shif8eb0d12013-08-01 17:52:06 -0700213 return all([artifact.ArtifactStaged() for artifact in required_artifacts])
214
Chris Sosa76e44b92013-01-31 12:11:38 -0800215 def _DownloadArtifactsSerially(self, artifacts, no_wait):
216 """Simple function to download all the given artifacts serially.
217
Chris Sosa75490802013-09-30 17:21:45 -0700218 Args:
219 artifacts: A list of build_artifact.BuildArtifact instances to
220 download.
221 no_wait: If True, don't block waiting for artifact to exist if we
222 fail to immediately find it.
223
224 Raises:
225 build_artifact.ArtifactDownloadError: If we failed to download the
226 artifact.
Dan Shif8eb0d12013-08-01 17:52:06 -0700227
Gilad Arnold6f99b982012-09-12 10:49:40 -0700228 """
Dan Shi6e50c722013-08-19 15:05:06 -0700229 try:
230 for artifact in artifacts:
Gabe Black3b567202015-09-23 14:07:59 -0700231 artifact.Process(self, no_wait)
Gilad Arnold02dc6552013-11-14 11:27:54 -0800232 except build_artifact.ArtifactDownloadError:
Dan Shi6e50c722013-08-19 15:05:06 -0700233 Downloader._TryRemoveStageDir(self._build_dir)
234 raise
Gilad Arnold6f99b982012-09-12 10:49:40 -0700235
Chris Sosa76e44b92013-01-31 12:11:38 -0800236 def _DownloadArtifactsInBackground(self, artifacts):
237 """Downloads |artifacts| in the background.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700238
Chris Sosa76e44b92013-01-31 12:11:38 -0800239 Downloads |artifacts| in the background. As these are backgrounded
240 artifacts, they are done best effort and may not exist.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700241
Chris Sosa76e44b92013-01-31 12:11:38 -0800242 Args:
243 artifacts: List of build_artifact.BuildArtifact instances to download.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700244 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800245 self._Log('Invoking background download of artifacts for %r', artifacts)
246 thread = threading.Thread(target=self._DownloadArtifactsSerially,
247 args=(artifacts, False))
248 thread.start()
Gabe Black3b567202015-09-23 14:07:59 -0700249
250 def Wait(self, name, is_regex_name, timeout):
251 """Waits for artifact to exist and returns the appropriate names.
252
253 Args:
254 name: Name to look at.
255 is_regex_name: True if the name is a regex pattern.
256 timeout: How long to wait for the artifact to become available.
257
258 Returns:
259 A list of names that match.
260 """
261 raise NotImplementedError()
262
263 def Fetch(self, remote_name, local_path):
264 """Downloads artifact from given source to a local directory.
265
266 Args:
267 remote_name: Remote name of the file to fetch.
268 local_path: Local path to the folder to store fetched file.
269
270 Returns:
271 The path to fetched file.
272 """
273 raise NotImplementedError()
274
275 def DescribeSource(self):
276 """Gets the source of the download, e.g., a url to GS."""
277 raise NotImplementedError()
278
279
280class GoogleStorageDownloader(Downloader):
281 """Downloader of images to the devserver from Google Storage.
282
283 Given a URL to a build on the archive server:
284 - Caches that build and the given artifacts onto the devserver.
285 - May also initiate caching of related artifacts in the background.
286
287 This is intended to be used with ChromeOS.
288
289 Private class members:
290 archive_url: Google Storage URL to download build artifacts from.
291 """
292
293 def __init__(self, static_dir, archive_url):
294 # The archive_url is of the form gs://server/[some_path/target]/...]/build
295 # This function discards 'gs://server/' and extracts the [some_path/target]
296 # as rel_path and the build as build.
297 sub_url = archive_url.partition('://')[2]
298 split_sub_url = sub_url.split('/')
299 rel_path = '/'.join(split_sub_url[1:-1])
300 build = split_sub_url[-1]
301 build_dir = os.path.join(static_dir, rel_path, build)
302
303 super(GoogleStorageDownloader, self).__init__(static_dir, build_dir, build)
304
305 self._archive_url = archive_url
306
307 def Wait(self, name, is_regex_name, timeout):
308 """Waits for artifact to exist and returns the appropriate names.
309
310 Args:
311 name: Name to look at.
312 is_regex_name: True if the name is a regex pattern.
313 timeout: How long to wait for the artifact to become available.
314
315 Returns:
316 A list of names that match.
317
318 Raises:
319 ArtifactDownloadError: An error occurred when obtaining artifact.
320 """
321 names = gsutil_util.GetGSNamesWithWait(
322 name, self._archive_url, str(self), timeout=timeout,
323 is_regex_pattern=is_regex_name)
324 if not names:
325 raise build_artifact.ArtifactDownloadError(
326 'Could not find %s in Google Storage at %s' %
327 (name, self._archive_url))
328 return names
329
330 def Fetch(self, remote_name, local_path):
331 """Downloads artifact from Google Storage to a local directory."""
332 install_path = os.path.join(local_path, remote_name)
333 gs_path = '/'.join([self._archive_url, remote_name])
334 gsutil_util.DownloadFromGS(gs_path, local_path)
335 return install_path
336
337 def DescribeSource(self):
338 return self._archive_url
339
340
341class LocalDownloader(Downloader):
342 """Downloader of images to the devserver from local storage.
343
344 Given a local path:
345 - Caches that build and the given artifacts onto the devserver.
346 - May also initiate caching of related artifacts in the background.
347
348 Private class members:
349 archive_params: parameters for where to download build artifacts from.
350 """
351
352 def __init__(self, static_dir, source_path):
353 # The local path is of the form /{path to static dir}/{rel_path}/{build}.
354 # local_path must be a subpath of the static directory.
355 self.source_path = source_path
356 rel_path = os.path.basename(os.path.dirname(source_path))
357 build = os.path.basename(source_path)
358 build_dir = os.path.join(static_dir, rel_path, build)
359
360 super(LocalDownloader, self).__init__(static_dir, build_dir, build)
361
362 def Wait(self, name, is_regex_name, timeout):
363 """Verifies the local artifact exists and returns the appropriate names.
364
365 Args:
366 name: Name to look at.
367 is_regex_name: True if the name is a regex pattern.
368 timeout: How long to wait for the artifact to become available.
369
370 Returns:
371 A list of names that match.
372
373 Raises:
374 ArtifactDownloadError: An error occurred when obtaining artifact.
375 """
376 local_path = os.path.join(self.source_path, name)
377 if is_regex_name:
378 filter_re = re.compile(name)
379 for filename in os.listdir(self.source_path):
380 if filter_re.match(filename):
381 return [filename]
382 else:
383 glob_search = glob.glob(local_path)
384 if glob_search and len(glob_search) == 1:
385 return [os.path.basename(glob_search[0])]
386 raise build_artifact.ArtifactDownloadError('Artifact not found.')
387
388 def Fetch(self, remote_name, local_path):
389 """Downloads artifact from Google Storage to a local directory."""
390 install_path = os.path.join(local_path, remote_name)
391 # It's a local path so just copy it into the staged directory.
392 shutil.copyfile(os.path.join(self.source_path, remote_name),
393 install_path)
394 return install_path
395
396 def DescribeSource(self):
397 return self.source_path
398
399
Dan Shi72b16132015-10-08 12:10:33 -0700400class AndroidBuildDownloader(Downloader):
401 """Downloader of images to the devserver from Android's build server."""
Gabe Black3b567202015-09-23 14:07:59 -0700402
Dan Shi72b16132015-10-08 12:10:33 -0700403 def __init__(self, static_dir, branch, build_id, target):
404 """Initialize AndroidBuildDownloader.
Gabe Black3b567202015-09-23 14:07:59 -0700405
406 Args:
407 static_dir: Root directory to store the build.
Dan Shi72b16132015-10-08 12:10:33 -0700408 branch: Branch for the build. Download will always verify if the given
409 build id is for the branch.
Gabe Black3b567202015-09-23 14:07:59 -0700410 build_id: Build id of the Android build, e.g., 2155602.
411 target: Target of the Android build, e.g., shamu-userdebug.
412 """
Dan Shi72b16132015-10-08 12:10:33 -0700413 build = '%s/%s/%s' % (branch, target, build_id)
Gabe Black3b567202015-09-23 14:07:59 -0700414 build_dir = os.path.join(static_dir, '', build)
415
Dan Shi72b16132015-10-08 12:10:33 -0700416 self.branch = branch
Gabe Black3b567202015-09-23 14:07:59 -0700417 self.build_id = build_id
418 self.target = target
419
Dan Shi72b16132015-10-08 12:10:33 -0700420 super(AndroidBuildDownloader, self).__init__(static_dir, build_dir, build)
Gabe Black3b567202015-09-23 14:07:59 -0700421
422 def Wait(self, name, is_regex_name, timeout):
423 """Verifies the local artifact exists and returns the appropriate names.
424
425 Args:
426 name: Name to look at.
427 is_regex_name: True if the name is a regex pattern.
428 timeout: How long to wait for the artifact to become available.
429
430 Returns:
431 A list of names that match.
432
433 Raises:
434 ArtifactDownloadError: An error occurred when obtaining artifact.
435 """
Dan Shi72b16132015-10-08 12:10:33 -0700436 artifacts = android_build.BuildAccessor.GetArtifacts(
437 branch=self.branch, build_id=self.build_id, target=self.target)
438
439 names = []
440 for artifact_name in [a['name'] for a in artifacts]:
441 match = (re.match(name, artifact_name) if is_regex_name
442 else name == artifact_name)
443 if match:
444 names.append(artifact_name)
445
446 if not names:
447 raise build_artifact.ArtifactDownloadError(
448 'No artifact found with given name: %s for %s-%s' %
449 (name, self.target, self.build_id))
450
451 return names
Gabe Black3b567202015-09-23 14:07:59 -0700452
453 def Fetch(self, remote_name, local_path):
Dan Shi72b16132015-10-08 12:10:33 -0700454 """Downloads artifact from Android's build server to a local directory."""
455 dest_file = os.path.join(local_path, remote_name)
456 android_build.BuildAccessor.Download(
457 branch=self.branch, build_id=self.build_id, target=self.target,
458 resource_id=remote_name, dest_file=dest_file)
459 return dest_file
Gabe Black3b567202015-09-23 14:07:59 -0700460
461 def DescribeSource(self):
Dan Shi72b16132015-10-08 12:10:33 -0700462 return '%s/%s/%s/%s' % (android_build.DEFAULT_BUILDER, self.branch,
463 self.target, self.build_id)