blob: 68de871ac23e07fa01c4543549b4960ad6426f80 [file] [log] [blame]
xixuan44b55452016-09-06 15:35:56 -07001#!/usr/bin/env 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
21import log_util
Frank Farzan37761d12011-12-01 14:29:08 -080022
xixuan44b55452016-09-06 15:35:56 -070023# Make sure that chromite is available to import.
24import setup_chromite # pylint: disable=unused-import
25
26try:
27 from chromite.lib import gs
28except ImportError as e:
29 gs = None
30
Dan Shi72b16132015-10-08 12:10:33 -070031try:
32 import android_build
33except ImportError as e:
34 # Ignore android_build import failure. This is to support devserver running
35 # inside a ChromeOS device triggered by cros flash. Most ChromeOS test images
36 # do not have google-api-python-client module and they don't need to support
37 # Android updating, therefore, ignore the import failure here.
38 android_build = None
39
Frank Farzan37761d12011-12-01 14:29:08 -080040
Dan Shi6e50c722013-08-19 15:05:06 -070041class DownloaderException(Exception):
42 """Exception that aggregates all exceptions raised during async download.
43
44 Exceptions could be raised in artifact.Process method, and saved to files.
45 When caller calls IsStaged to check the downloading progress, devserver can
46 retrieve the persisted exceptions from the files, wrap them into a
47 DownloaderException, and raise it.
48 """
49 def __init__(self, exceptions):
50 """Initialize a DownloaderException instance with a list of exceptions.
51
Gabe Black3b567202015-09-23 14:07:59 -070052 Args:
53 exceptions: Exceptions raised when downloading artifacts.
Dan Shi6e50c722013-08-19 15:05:06 -070054 """
55 message = 'Exceptions were raised when downloading artifacts.'
56 Exception.__init__(self, message)
57 self.exceptions = exceptions
58
59 def __repr__(self):
60 return self.__str__()
61
62 def __str__(self):
63 """Return a custom exception message with all exceptions merged."""
64 return '--------\n'.join([str(exception) for exception in self.exceptions])
65
Gilad Arnoldc65330c2012-09-20 15:17:48 -070066class Downloader(log_util.Loggable):
Chris Sosa76e44b92013-01-31 12:11:38 -080067 """Downloader of images to the devsever.
Frank Farzan37761d12011-12-01 14:29:08 -080068
Gabe Black3b567202015-09-23 14:07:59 -070069 This is the base class for different types of downloaders, including
Dan Shi72b16132015-10-08 12:10:33 -070070 GoogleStorageDownloader, LocalDownloader and AndroidBuildDownloader.
Gabe Black3b567202015-09-23 14:07:59 -070071
Frank Farzan37761d12011-12-01 14:29:08 -080072 Given a URL to a build on the archive server:
Chris Sosa76e44b92013-01-31 12:11:38 -080073 - Caches that build and the given artifacts onto the devserver.
74 - May also initiate caching of related artifacts in the background.
Frank Farzan37761d12011-12-01 14:29:08 -080075
Chris Sosa76e44b92013-01-31 12:11:38 -080076 Private class members:
Chris Sosa76e44b92013-01-31 12:11:38 -080077 static_dir: local filesystem directory to store all artifacts.
78 build_dir: the local filesystem directory to store artifacts for the given
Gabe Black3b567202015-09-23 14:07:59 -070079 build based on the remote source.
80
81 Public methods must be overridden:
82 Wait: Verifies the local artifact exists and returns the appropriate names.
83 Fetch: Downloads artifact from given source to a local directory.
84 DescribeSource: Gets the source of the download, e.g., a url to GS.
Frank Farzan37761d12011-12-01 14:29:08 -080085 """
86
Alex Millera44d5022012-07-27 11:34:16 -070087 # This filename must be kept in sync with clean_staged_images.py
88 _TIMESTAMP_FILENAME = 'staged.timestamp'
Chris Masonea22d9382012-05-18 12:38:51 -070089
Gabe Black3b567202015-09-23 14:07:59 -070090 def __init__(self, static_dir, build_dir, build):
Chris Sosa76e44b92013-01-31 12:11:38 -080091 super(Downloader, self).__init__()
Frank Farzan37761d12011-12-01 14:29:08 -080092 self._static_dir = static_dir
Gabe Black3b567202015-09-23 14:07:59 -070093 self._build_dir = build_dir
94 self._build = build
Chris Masone816e38c2012-05-02 12:22:36 -070095
Gabe Black3b567202015-09-23 14:07:59 -070096 def GetBuildDir(self):
97 """Returns the path to where the artifacts will be staged."""
98 return self._build_dir
Simran Basi4243a862014-12-12 12:48:33 -080099
Gabe Black3b567202015-09-23 14:07:59 -0700100 def GetBuild(self):
101 """Returns the path to where the artifacts will be staged."""
102 return self._build
Frank Farzan37761d12011-12-01 14:29:08 -0800103
Chris Sosa9164ca32012-03-28 11:04:50 -0700104 @staticmethod
Simran Basief83d6a2014-08-28 14:32:01 -0700105 def TouchTimestampForStaged(directory_path):
Alex Millera44d5022012-07-27 11:34:16 -0700106 file_name = os.path.join(directory_path, Downloader._TIMESTAMP_FILENAME)
107 # Easiest python version of |touch file_name|
108 with file(file_name, 'a'):
109 os.utime(file_name, None)
110
Dan Shiba0e6742013-06-26 17:39:05 -0700111 @staticmethod
112 def _TryRemoveStageDir(directory_path):
Gilad Arnold02dc6552013-11-14 11:27:54 -0800113 """If download failed, try to remove the stage dir.
Dan Shiba0e6742013-06-26 17:39:05 -0700114
Gilad Arnold02dc6552013-11-14 11:27:54 -0800115 If the download attempt failed (ArtifactDownloadError) and staged.timestamp
116 is the only file in that directory. The build could be non-existing, and
117 the directory should be removed.
Dan Shiba0e6742013-06-26 17:39:05 -0700118
Gabe Black3b567202015-09-23 14:07:59 -0700119 Args:
120 directory_path: directory used to stage the image.
Dan Shiba0e6742013-06-26 17:39:05 -0700121 """
122 file_name = os.path.join(directory_path, Downloader._TIMESTAMP_FILENAME)
123 if os.path.exists(file_name) and len(os.listdir(directory_path)) == 1:
124 os.remove(file_name)
125 os.rmdir(directory_path)
126
Prashanth Ba06d2d22014-03-07 15:35:19 -0800127 def ListBuildDir(self):
128 """List the files in the build directory.
129
130 Only lists files a single level into the build directory. Includes
131 timestamp information in the listing.
132
133 Returns:
134 A string with information about the files in the build directory.
135 None if the build directory doesn't exist.
136
137 Raises:
138 build_artifact.ArtifactDownloadError: If the build_dir path exists
139 but is not a directory.
140 """
141 if not os.path.exists(self._build_dir):
142 return None
143 if not os.path.isdir(self._build_dir):
144 raise build_artifact.ArtifactDownloadError(
145 'Artifacts %s improperly staged to build_dir path %s. The path is '
146 'not a directory.' % (self._archive_url, self._build_dir))
147
148 ls_format = collections.namedtuple(
Gabe Black3b567202015-09-23 14:07:59 -0700149 'ls', ['name', 'accessed', 'modified', 'size'])
Prashanth Ba06d2d22014-03-07 15:35:19 -0800150 output_format = ('Name: %(name)s Accessed: %(accessed)s '
Gabe Black3b567202015-09-23 14:07:59 -0700151 'Modified: %(modified)s Size: %(size)s bytes.\n')
Prashanth Ba06d2d22014-03-07 15:35:19 -0800152
153 build_dir_info = 'Listing contents of :%s \n' % self._build_dir
154 for file_name in os.listdir(self._build_dir):
155 file_path = os.path.join(self._build_dir, file_name)
156 file_info = os.stat(file_path)
157 ls_info = ls_format(file_path,
158 datetime.fromtimestamp(file_info.st_atime),
159 datetime.fromtimestamp(file_info.st_mtime),
160 file_info.st_size)
161 build_dir_info += output_format % ls_info._asdict()
162 return build_dir_info
163
Gabe Black3b567202015-09-23 14:07:59 -0700164 def Download(self, factory, async=False):
Chris Sosa76e44b92013-01-31 12:11:38 -0800165 """Downloads and caches the |artifacts|.
Chris Sosa9164ca32012-03-28 11:04:50 -0700166
Gabe Black3b567202015-09-23 14:07:59 -0700167 Downloads and caches the |artifacts|. Returns once these are present on the
168 devserver. A call to this will attempt to cache non-specified artifacts in
169 the background following the principle of spatial locality.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700170
Chris Sosa75490802013-09-30 17:21:45 -0700171 Args:
Gabe Black3b567202015-09-23 14:07:59 -0700172 factory: The artifact factory.
Chris Sosa75490802013-09-30 17:21:45 -0700173 async: If True, return without waiting for download to complete.
174
175 Raises:
Gilad Arnold02dc6552013-11-14 11:27:54 -0800176 build_artifact.ArtifactDownloadError: If failed to download the artifact.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700177 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800178 common_util.MkDirP(self._build_dir)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700179
Chris Sosa76e44b92013-01-31 12:11:38 -0800180 # We are doing some work on this build -- let's touch it to indicate that
181 # we shouldn't be cleaning it up anytime soon.
Simran Basief83d6a2014-08-28 14:32:01 -0700182 Downloader.TouchTimestampForStaged(self._build_dir)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700183
Chris Sosa76e44b92013-01-31 12:11:38 -0800184 # Create factory to create build_artifacts from artifact names.
Chris Sosa76e44b92013-01-31 12:11:38 -0800185 background_artifacts = factory.OptionalArtifacts()
186 if background_artifacts:
187 self._DownloadArtifactsInBackground(background_artifacts)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700188
Chris Sosa76e44b92013-01-31 12:11:38 -0800189 required_artifacts = factory.RequiredArtifacts()
190 str_repr = [str(a) for a in required_artifacts]
191 self._Log('Downloading artifacts %s.', ' '.join(str_repr))
Dan Shie37f8fe2013-08-09 16:10:29 -0700192
Dan Shi6e50c722013-08-19 15:05:06 -0700193 if async:
194 self._DownloadArtifactsInBackground(required_artifacts)
195 else:
196 self._DownloadArtifactsSerially(required_artifacts, no_wait=True)
Chris Sosa76e44b92013-01-31 12:11:38 -0800197
Gabe Black3b567202015-09-23 14:07:59 -0700198 def IsStaged(self, factory):
Dan Shif8eb0d12013-08-01 17:52:06 -0700199 """Check if all artifacts have been downloaded.
200
Gabe Black3b567202015-09-23 14:07:59 -0700201 Args:
202 factory: An instance of BaseArtifactFactory to be used to check if desired
203 artifacts or files are staged.
204
205 Returns:
206 True if all artifacts are staged.
207
208 Raises:
209 DownloaderException: A wrapper for exceptions raised by any artifact when
210 calling Process.
Dan Shif8eb0d12013-08-01 17:52:06 -0700211 """
Dan Shif8eb0d12013-08-01 17:52:06 -0700212 required_artifacts = factory.RequiredArtifacts()
Dan Shi6e50c722013-08-19 15:05:06 -0700213 exceptions = [artifact.GetException() for artifact in required_artifacts if
214 artifact.GetException()]
215 if exceptions:
216 raise DownloaderException(exceptions)
217
Dan Shif8eb0d12013-08-01 17:52:06 -0700218 return all([artifact.ArtifactStaged() for artifact in required_artifacts])
219
Chris Sosa76e44b92013-01-31 12:11:38 -0800220 def _DownloadArtifactsSerially(self, artifacts, no_wait):
221 """Simple function to download all the given artifacts serially.
222
Chris Sosa75490802013-09-30 17:21:45 -0700223 Args:
224 artifacts: A list of build_artifact.BuildArtifact instances to
225 download.
226 no_wait: If True, don't block waiting for artifact to exist if we
227 fail to immediately find it.
228
229 Raises:
230 build_artifact.ArtifactDownloadError: If we failed to download the
231 artifact.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700232 """
Dan Shi6e50c722013-08-19 15:05:06 -0700233 try:
234 for artifact in artifacts:
Gabe Black3b567202015-09-23 14:07:59 -0700235 artifact.Process(self, no_wait)
Gilad Arnold02dc6552013-11-14 11:27:54 -0800236 except build_artifact.ArtifactDownloadError:
Dan Shi6e50c722013-08-19 15:05:06 -0700237 Downloader._TryRemoveStageDir(self._build_dir)
238 raise
Gilad Arnold6f99b982012-09-12 10:49:40 -0700239
Chris Sosa76e44b92013-01-31 12:11:38 -0800240 def _DownloadArtifactsInBackground(self, artifacts):
241 """Downloads |artifacts| in the background.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700242
Chris Sosa76e44b92013-01-31 12:11:38 -0800243 Downloads |artifacts| in the background. As these are backgrounded
244 artifacts, they are done best effort and may not exist.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700245
Chris Sosa76e44b92013-01-31 12:11:38 -0800246 Args:
247 artifacts: List of build_artifact.BuildArtifact instances to download.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700248 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800249 self._Log('Invoking background download of artifacts for %r', artifacts)
250 thread = threading.Thread(target=self._DownloadArtifactsSerially,
251 args=(artifacts, False))
252 thread.start()
Gabe Black3b567202015-09-23 14:07:59 -0700253
254 def Wait(self, name, is_regex_name, timeout):
255 """Waits for artifact to exist and returns the appropriate names.
256
257 Args:
258 name: Name to look at.
259 is_regex_name: True if the name is a regex pattern.
260 timeout: How long to wait for the artifact to become available.
261
262 Returns:
263 A list of names that match.
264 """
265 raise NotImplementedError()
266
267 def Fetch(self, remote_name, local_path):
268 """Downloads artifact from given source to a local directory.
269
270 Args:
271 remote_name: Remote name of the file to fetch.
272 local_path: Local path to the folder to store fetched file.
273
274 Returns:
275 The path to fetched file.
276 """
277 raise NotImplementedError()
278
279 def DescribeSource(self):
280 """Gets the source of the download, e.g., a url to GS."""
281 raise NotImplementedError()
282
283
284class GoogleStorageDownloader(Downloader):
285 """Downloader of images to the devserver from Google Storage.
286
287 Given a URL to a build on the archive server:
288 - Caches that build and the given artifacts onto the devserver.
289 - May also initiate caching of related artifacts in the background.
290
291 This is intended to be used with ChromeOS.
292
293 Private class members:
294 archive_url: Google Storage URL to download build artifacts from.
295 """
296
297 def __init__(self, static_dir, archive_url):
298 # The archive_url is of the form gs://server/[some_path/target]/...]/build
299 # This function discards 'gs://server/' and extracts the [some_path/target]
300 # as rel_path and the build as build.
301 sub_url = archive_url.partition('://')[2]
302 split_sub_url = sub_url.split('/')
303 rel_path = '/'.join(split_sub_url[1:-1])
304 build = split_sub_url[-1]
305 build_dir = os.path.join(static_dir, rel_path, build)
306
307 super(GoogleStorageDownloader, self).__init__(static_dir, build_dir, build)
308
309 self._archive_url = archive_url
310
xixuan178263c2017-03-22 09:10:25 -0700311 if common_util.IsRunningOnMoblab():
312 self._ctx = gs.GSContext(cache_user='chronos') if gs else None
313 else:
314 self._ctx = gs.GSContext() if gs else None
xixuan44b55452016-09-06 15:35:56 -0700315
Gabe Black3b567202015-09-23 14:07:59 -0700316 def Wait(self, name, is_regex_name, timeout):
317 """Waits for artifact to exist and returns the appropriate names.
318
319 Args:
320 name: Name to look at.
321 is_regex_name: True if the name is a regex pattern.
322 timeout: How long to wait for the artifact to become available.
323
324 Returns:
325 A list of names that match.
326
327 Raises:
328 ArtifactDownloadError: An error occurred when obtaining artifact.
329 """
xixuan44b55452016-09-06 15:35:56 -0700330 names = self._ctx.GetGsNamesWithWait(
331 name, self._archive_url, timeout=timeout,
Gabe Black3b567202015-09-23 14:07:59 -0700332 is_regex_pattern=is_regex_name)
333 if not names:
334 raise build_artifact.ArtifactDownloadError(
335 'Could not find %s in Google Storage at %s' %
336 (name, self._archive_url))
337 return names
338
339 def Fetch(self, remote_name, local_path):
340 """Downloads artifact from Google Storage to a local directory."""
341 install_path = os.path.join(local_path, remote_name)
342 gs_path = '/'.join([self._archive_url, remote_name])
xixuan44b55452016-09-06 15:35:56 -0700343 self._ctx.Copy(gs_path, local_path)
Gabe Black3b567202015-09-23 14:07:59 -0700344 return install_path
345
346 def DescribeSource(self):
347 return self._archive_url
348
349
350class LocalDownloader(Downloader):
351 """Downloader of images to the devserver from local storage.
352
353 Given a local path:
354 - Caches that build and the given artifacts onto the devserver.
355 - May also initiate caching of related artifacts in the background.
356
357 Private class members:
358 archive_params: parameters for where to download build artifacts from.
359 """
360
361 def __init__(self, static_dir, source_path):
362 # The local path is of the form /{path to static dir}/{rel_path}/{build}.
363 # local_path must be a subpath of the static directory.
364 self.source_path = source_path
365 rel_path = os.path.basename(os.path.dirname(source_path))
366 build = os.path.basename(source_path)
367 build_dir = os.path.join(static_dir, rel_path, build)
368
369 super(LocalDownloader, self).__init__(static_dir, build_dir, build)
370
371 def Wait(self, name, is_regex_name, timeout):
372 """Verifies the local artifact exists and returns the appropriate names.
373
374 Args:
375 name: Name to look at.
376 is_regex_name: True if the name is a regex pattern.
377 timeout: How long to wait for the artifact to become available.
378
379 Returns:
380 A list of names that match.
381
382 Raises:
383 ArtifactDownloadError: An error occurred when obtaining artifact.
384 """
385 local_path = os.path.join(self.source_path, name)
386 if is_regex_name:
387 filter_re = re.compile(name)
388 for filename in os.listdir(self.source_path):
389 if filter_re.match(filename):
390 return [filename]
391 else:
392 glob_search = glob.glob(local_path)
393 if glob_search and len(glob_search) == 1:
394 return [os.path.basename(glob_search[0])]
395 raise build_artifact.ArtifactDownloadError('Artifact not found.')
396
397 def Fetch(self, remote_name, local_path):
398 """Downloads artifact from Google Storage to a local directory."""
399 install_path = os.path.join(local_path, remote_name)
400 # It's a local path so just copy it into the staged directory.
401 shutil.copyfile(os.path.join(self.source_path, remote_name),
402 install_path)
403 return install_path
404
405 def DescribeSource(self):
406 return self.source_path
407
408
Dan Shi72b16132015-10-08 12:10:33 -0700409class AndroidBuildDownloader(Downloader):
410 """Downloader of images to the devserver from Android's build server."""
Gabe Black3b567202015-09-23 14:07:59 -0700411
Dan Shi72b16132015-10-08 12:10:33 -0700412 def __init__(self, static_dir, branch, build_id, target):
413 """Initialize AndroidBuildDownloader.
Gabe Black3b567202015-09-23 14:07:59 -0700414
415 Args:
416 static_dir: Root directory to store the build.
Dan Shi72b16132015-10-08 12:10:33 -0700417 branch: Branch for the build. Download will always verify if the given
418 build id is for the branch.
Gabe Black3b567202015-09-23 14:07:59 -0700419 build_id: Build id of the Android build, e.g., 2155602.
420 target: Target of the Android build, e.g., shamu-userdebug.
421 """
Dan Shi72b16132015-10-08 12:10:33 -0700422 build = '%s/%s/%s' % (branch, target, build_id)
Gabe Black3b567202015-09-23 14:07:59 -0700423 build_dir = os.path.join(static_dir, '', build)
424
Dan Shi72b16132015-10-08 12:10:33 -0700425 self.branch = branch
Gabe Black3b567202015-09-23 14:07:59 -0700426 self.build_id = build_id
427 self.target = target
428
Dan Shi72b16132015-10-08 12:10:33 -0700429 super(AndroidBuildDownloader, self).__init__(static_dir, build_dir, build)
Gabe Black3b567202015-09-23 14:07:59 -0700430
431 def Wait(self, name, is_regex_name, timeout):
432 """Verifies the local artifact exists and returns the appropriate names.
433
434 Args:
435 name: Name to look at.
436 is_regex_name: True if the name is a regex pattern.
437 timeout: How long to wait for the artifact to become available.
438
439 Returns:
440 A list of names that match.
441
442 Raises:
443 ArtifactDownloadError: An error occurred when obtaining artifact.
444 """
Dan Shi72b16132015-10-08 12:10:33 -0700445 artifacts = android_build.BuildAccessor.GetArtifacts(
446 branch=self.branch, build_id=self.build_id, target=self.target)
447
448 names = []
449 for artifact_name in [a['name'] for a in artifacts]:
450 match = (re.match(name, artifact_name) if is_regex_name
451 else name == artifact_name)
452 if match:
453 names.append(artifact_name)
454
455 if not names:
456 raise build_artifact.ArtifactDownloadError(
Dan Shi9ee5dc22017-06-27 11:53:07 -0700457 'No artifact found with given name: %s for %s-%s. All available '
458 'artifacts are: %s' %
459 (name, self.target, self.build_id,
460 ','.join([a['name'] for a in artifacts])))
Dan Shi72b16132015-10-08 12:10:33 -0700461
462 return names
Gabe Black3b567202015-09-23 14:07:59 -0700463
464 def Fetch(self, remote_name, local_path):
Dan Shi72b16132015-10-08 12:10:33 -0700465 """Downloads artifact from Android's build server to a local directory."""
466 dest_file = os.path.join(local_path, remote_name)
467 android_build.BuildAccessor.Download(
468 branch=self.branch, build_id=self.build_id, target=self.target,
469 resource_id=remote_name, dest_file=dest_file)
470 return dest_file
Gabe Black3b567202015-09-23 14:07:59 -0700471
472 def DescribeSource(self):
Dan Shi72b16132015-10-08 12:10:33 -0700473 return '%s/%s/%s/%s' % (android_build.DEFAULT_BUILDER, self.branch,
474 self.target, self.build_id)