blob: 554ce33ab2d9166136abf2d3379c95d77c3fbb1f [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
24
Dan Shi6e50c722013-08-19 15:05:06 -070025class DownloaderException(Exception):
26 """Exception that aggregates all exceptions raised during async download.
27
28 Exceptions could be raised in artifact.Process method, and saved to files.
29 When caller calls IsStaged to check the downloading progress, devserver can
30 retrieve the persisted exceptions from the files, wrap them into a
31 DownloaderException, and raise it.
32 """
33 def __init__(self, exceptions):
34 """Initialize a DownloaderException instance with a list of exceptions.
35
Gabe Black3b567202015-09-23 14:07:59 -070036 Args:
37 exceptions: Exceptions raised when downloading artifacts.
Dan Shi6e50c722013-08-19 15:05:06 -070038 """
39 message = 'Exceptions were raised when downloading artifacts.'
40 Exception.__init__(self, message)
41 self.exceptions = exceptions
42
43 def __repr__(self):
44 return self.__str__()
45
46 def __str__(self):
47 """Return a custom exception message with all exceptions merged."""
48 return '--------\n'.join([str(exception) for exception in self.exceptions])
49
Gilad Arnoldc65330c2012-09-20 15:17:48 -070050class Downloader(log_util.Loggable):
Chris Sosa76e44b92013-01-31 12:11:38 -080051 """Downloader of images to the devsever.
Frank Farzan37761d12011-12-01 14:29:08 -080052
Gabe Black3b567202015-09-23 14:07:59 -070053 This is the base class for different types of downloaders, including
54 GoogleStorageDownloader, LocalDownloader and LaunchControlDownloader.
55
Frank Farzan37761d12011-12-01 14:29:08 -080056 Given a URL to a build on the archive server:
Chris Sosa76e44b92013-01-31 12:11:38 -080057 - Caches that build and the given artifacts onto the devserver.
58 - May also initiate caching of related artifacts in the background.
Frank Farzan37761d12011-12-01 14:29:08 -080059
Chris Sosa76e44b92013-01-31 12:11:38 -080060 Private class members:
Chris Sosa76e44b92013-01-31 12:11:38 -080061 static_dir: local filesystem directory to store all artifacts.
62 build_dir: the local filesystem directory to store artifacts for the given
Gabe Black3b567202015-09-23 14:07:59 -070063 build based on the remote source.
64
65 Public methods must be overridden:
66 Wait: Verifies the local artifact exists and returns the appropriate names.
67 Fetch: Downloads artifact from given source to a local directory.
68 DescribeSource: Gets the source of the download, e.g., a url to GS.
Frank Farzan37761d12011-12-01 14:29:08 -080069 """
70
Alex Millera44d5022012-07-27 11:34:16 -070071 # This filename must be kept in sync with clean_staged_images.py
72 _TIMESTAMP_FILENAME = 'staged.timestamp'
Chris Masonea22d9382012-05-18 12:38:51 -070073
Gabe Black3b567202015-09-23 14:07:59 -070074 def __init__(self, static_dir, build_dir, build):
Chris Sosa76e44b92013-01-31 12:11:38 -080075 super(Downloader, self).__init__()
Frank Farzan37761d12011-12-01 14:29:08 -080076 self._static_dir = static_dir
Gabe Black3b567202015-09-23 14:07:59 -070077 self._build_dir = build_dir
78 self._build = build
Chris Masone816e38c2012-05-02 12:22:36 -070079
Gabe Black3b567202015-09-23 14:07:59 -070080 def GetBuildDir(self):
81 """Returns the path to where the artifacts will be staged."""
82 return self._build_dir
Simran Basi4243a862014-12-12 12:48:33 -080083
Gabe Black3b567202015-09-23 14:07:59 -070084 def GetBuild(self):
85 """Returns the path to where the artifacts will be staged."""
86 return self._build
Frank Farzan37761d12011-12-01 14:29:08 -080087
Chris Sosa9164ca32012-03-28 11:04:50 -070088 @staticmethod
Simran Basief83d6a2014-08-28 14:32:01 -070089 def TouchTimestampForStaged(directory_path):
Alex Millera44d5022012-07-27 11:34:16 -070090 file_name = os.path.join(directory_path, Downloader._TIMESTAMP_FILENAME)
91 # Easiest python version of |touch file_name|
92 with file(file_name, 'a'):
93 os.utime(file_name, None)
94
Dan Shiba0e6742013-06-26 17:39:05 -070095 @staticmethod
96 def _TryRemoveStageDir(directory_path):
Gilad Arnold02dc6552013-11-14 11:27:54 -080097 """If download failed, try to remove the stage dir.
Dan Shiba0e6742013-06-26 17:39:05 -070098
Gilad Arnold02dc6552013-11-14 11:27:54 -080099 If the download attempt failed (ArtifactDownloadError) and staged.timestamp
100 is the only file in that directory. The build could be non-existing, and
101 the directory should be removed.
Dan Shiba0e6742013-06-26 17:39:05 -0700102
Gabe Black3b567202015-09-23 14:07:59 -0700103 Args:
104 directory_path: directory used to stage the image.
Dan Shiba0e6742013-06-26 17:39:05 -0700105 """
106 file_name = os.path.join(directory_path, Downloader._TIMESTAMP_FILENAME)
107 if os.path.exists(file_name) and len(os.listdir(directory_path)) == 1:
108 os.remove(file_name)
109 os.rmdir(directory_path)
110
Prashanth Ba06d2d22014-03-07 15:35:19 -0800111 def ListBuildDir(self):
112 """List the files in the build directory.
113
114 Only lists files a single level into the build directory. Includes
115 timestamp information in the listing.
116
117 Returns:
118 A string with information about the files in the build directory.
119 None if the build directory doesn't exist.
120
121 Raises:
122 build_artifact.ArtifactDownloadError: If the build_dir path exists
123 but is not a directory.
124 """
125 if not os.path.exists(self._build_dir):
126 return None
127 if not os.path.isdir(self._build_dir):
128 raise build_artifact.ArtifactDownloadError(
129 'Artifacts %s improperly staged to build_dir path %s. The path is '
130 'not a directory.' % (self._archive_url, self._build_dir))
131
132 ls_format = collections.namedtuple(
Gabe Black3b567202015-09-23 14:07:59 -0700133 'ls', ['name', 'accessed', 'modified', 'size'])
Prashanth Ba06d2d22014-03-07 15:35:19 -0800134 output_format = ('Name: %(name)s Accessed: %(accessed)s '
Gabe Black3b567202015-09-23 14:07:59 -0700135 'Modified: %(modified)s Size: %(size)s bytes.\n')
Prashanth Ba06d2d22014-03-07 15:35:19 -0800136
137 build_dir_info = 'Listing contents of :%s \n' % self._build_dir
138 for file_name in os.listdir(self._build_dir):
139 file_path = os.path.join(self._build_dir, file_name)
140 file_info = os.stat(file_path)
141 ls_info = ls_format(file_path,
142 datetime.fromtimestamp(file_info.st_atime),
143 datetime.fromtimestamp(file_info.st_mtime),
144 file_info.st_size)
145 build_dir_info += output_format % ls_info._asdict()
146 return build_dir_info
147
Gabe Black3b567202015-09-23 14:07:59 -0700148 def Download(self, factory, async=False):
Chris Sosa76e44b92013-01-31 12:11:38 -0800149 """Downloads and caches the |artifacts|.
Chris Sosa9164ca32012-03-28 11:04:50 -0700150
Gabe Black3b567202015-09-23 14:07:59 -0700151 Downloads and caches the |artifacts|. Returns once these are present on the
152 devserver. A call to this will attempt to cache non-specified artifacts in
153 the background following the principle of spatial locality.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700154
Chris Sosa75490802013-09-30 17:21:45 -0700155 Args:
Gabe Black3b567202015-09-23 14:07:59 -0700156 factory: The artifact factory.
Chris Sosa75490802013-09-30 17:21:45 -0700157 async: If True, return without waiting for download to complete.
158
159 Raises:
Gilad Arnold02dc6552013-11-14 11:27:54 -0800160 build_artifact.ArtifactDownloadError: If failed to download the artifact.
Dan Shif8eb0d12013-08-01 17:52:06 -0700161
Gilad Arnold6f99b982012-09-12 10:49:40 -0700162 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800163 common_util.MkDirP(self._build_dir)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700164
Chris Sosa76e44b92013-01-31 12:11:38 -0800165 # We are doing some work on this build -- let's touch it to indicate that
166 # we shouldn't be cleaning it up anytime soon.
Simran Basief83d6a2014-08-28 14:32:01 -0700167 Downloader.TouchTimestampForStaged(self._build_dir)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700168
Chris Sosa76e44b92013-01-31 12:11:38 -0800169 # Create factory to create build_artifacts from artifact names.
Chris Sosa76e44b92013-01-31 12:11:38 -0800170 background_artifacts = factory.OptionalArtifacts()
171 if background_artifacts:
172 self._DownloadArtifactsInBackground(background_artifacts)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700173
Chris Sosa76e44b92013-01-31 12:11:38 -0800174 required_artifacts = factory.RequiredArtifacts()
175 str_repr = [str(a) for a in required_artifacts]
176 self._Log('Downloading artifacts %s.', ' '.join(str_repr))
Dan Shie37f8fe2013-08-09 16:10:29 -0700177
Dan Shi6e50c722013-08-19 15:05:06 -0700178 if async:
179 self._DownloadArtifactsInBackground(required_artifacts)
180 else:
181 self._DownloadArtifactsSerially(required_artifacts, no_wait=True)
Chris Sosa76e44b92013-01-31 12:11:38 -0800182
Gabe Black3b567202015-09-23 14:07:59 -0700183 def IsStaged(self, factory):
Dan Shif8eb0d12013-08-01 17:52:06 -0700184 """Check if all artifacts have been downloaded.
185
Gabe Black3b567202015-09-23 14:07:59 -0700186 Args:
187 factory: An instance of BaseArtifactFactory to be used to check if desired
188 artifacts or files are staged.
189
190 Returns:
191 True if all artifacts are staged.
192
193 Raises:
194 DownloaderException: A wrapper for exceptions raised by any artifact when
195 calling Process.
Dan Shif8eb0d12013-08-01 17:52:06 -0700196
197 """
Dan Shif8eb0d12013-08-01 17:52:06 -0700198 required_artifacts = factory.RequiredArtifacts()
Dan Shi6e50c722013-08-19 15:05:06 -0700199 exceptions = [artifact.GetException() for artifact in required_artifacts if
200 artifact.GetException()]
201 if exceptions:
202 raise DownloaderException(exceptions)
203
Dan Shif8eb0d12013-08-01 17:52:06 -0700204 return all([artifact.ArtifactStaged() for artifact in required_artifacts])
205
Chris Sosa76e44b92013-01-31 12:11:38 -0800206 def _DownloadArtifactsSerially(self, artifacts, no_wait):
207 """Simple function to download all the given artifacts serially.
208
Chris Sosa75490802013-09-30 17:21:45 -0700209 Args:
210 artifacts: A list of build_artifact.BuildArtifact instances to
211 download.
212 no_wait: If True, don't block waiting for artifact to exist if we
213 fail to immediately find it.
214
215 Raises:
216 build_artifact.ArtifactDownloadError: If we failed to download the
217 artifact.
Dan Shif8eb0d12013-08-01 17:52:06 -0700218
Gilad Arnold6f99b982012-09-12 10:49:40 -0700219 """
Dan Shi6e50c722013-08-19 15:05:06 -0700220 try:
221 for artifact in artifacts:
Gabe Black3b567202015-09-23 14:07:59 -0700222 artifact.Process(self, no_wait)
Gilad Arnold02dc6552013-11-14 11:27:54 -0800223 except build_artifact.ArtifactDownloadError:
Dan Shi6e50c722013-08-19 15:05:06 -0700224 Downloader._TryRemoveStageDir(self._build_dir)
225 raise
Gilad Arnold6f99b982012-09-12 10:49:40 -0700226
Chris Sosa76e44b92013-01-31 12:11:38 -0800227 def _DownloadArtifactsInBackground(self, artifacts):
228 """Downloads |artifacts| in the background.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700229
Chris Sosa76e44b92013-01-31 12:11:38 -0800230 Downloads |artifacts| in the background. As these are backgrounded
231 artifacts, they are done best effort and may not exist.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700232
Chris Sosa76e44b92013-01-31 12:11:38 -0800233 Args:
234 artifacts: List of build_artifact.BuildArtifact instances to download.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700235 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800236 self._Log('Invoking background download of artifacts for %r', artifacts)
237 thread = threading.Thread(target=self._DownloadArtifactsSerially,
238 args=(artifacts, False))
239 thread.start()
Gabe Black3b567202015-09-23 14:07:59 -0700240
241 def Wait(self, name, is_regex_name, timeout):
242 """Waits for artifact to exist and returns the appropriate names.
243
244 Args:
245 name: Name to look at.
246 is_regex_name: True if the name is a regex pattern.
247 timeout: How long to wait for the artifact to become available.
248
249 Returns:
250 A list of names that match.
251 """
252 raise NotImplementedError()
253
254 def Fetch(self, remote_name, local_path):
255 """Downloads artifact from given source to a local directory.
256
257 Args:
258 remote_name: Remote name of the file to fetch.
259 local_path: Local path to the folder to store fetched file.
260
261 Returns:
262 The path to fetched file.
263 """
264 raise NotImplementedError()
265
266 def DescribeSource(self):
267 """Gets the source of the download, e.g., a url to GS."""
268 raise NotImplementedError()
269
270
271class GoogleStorageDownloader(Downloader):
272 """Downloader of images to the devserver from Google Storage.
273
274 Given a URL to a build on the archive server:
275 - Caches that build and the given artifacts onto the devserver.
276 - May also initiate caching of related artifacts in the background.
277
278 This is intended to be used with ChromeOS.
279
280 Private class members:
281 archive_url: Google Storage URL to download build artifacts from.
282 """
283
284 def __init__(self, static_dir, archive_url):
285 # The archive_url is of the form gs://server/[some_path/target]/...]/build
286 # This function discards 'gs://server/' and extracts the [some_path/target]
287 # as rel_path and the build as build.
288 sub_url = archive_url.partition('://')[2]
289 split_sub_url = sub_url.split('/')
290 rel_path = '/'.join(split_sub_url[1:-1])
291 build = split_sub_url[-1]
292 build_dir = os.path.join(static_dir, rel_path, build)
293
294 super(GoogleStorageDownloader, self).__init__(static_dir, build_dir, build)
295
296 self._archive_url = archive_url
297
298 def Wait(self, name, is_regex_name, timeout):
299 """Waits for artifact to exist and returns the appropriate names.
300
301 Args:
302 name: Name to look at.
303 is_regex_name: True if the name is a regex pattern.
304 timeout: How long to wait for the artifact to become available.
305
306 Returns:
307 A list of names that match.
308
309 Raises:
310 ArtifactDownloadError: An error occurred when obtaining artifact.
311 """
312 names = gsutil_util.GetGSNamesWithWait(
313 name, self._archive_url, str(self), timeout=timeout,
314 is_regex_pattern=is_regex_name)
315 if not names:
316 raise build_artifact.ArtifactDownloadError(
317 'Could not find %s in Google Storage at %s' %
318 (name, self._archive_url))
319 return names
320
321 def Fetch(self, remote_name, local_path):
322 """Downloads artifact from Google Storage to a local directory."""
323 install_path = os.path.join(local_path, remote_name)
324 gs_path = '/'.join([self._archive_url, remote_name])
325 gsutil_util.DownloadFromGS(gs_path, local_path)
326 return install_path
327
328 def DescribeSource(self):
329 return self._archive_url
330
331
332class LocalDownloader(Downloader):
333 """Downloader of images to the devserver from local storage.
334
335 Given a local path:
336 - Caches that build and the given artifacts onto the devserver.
337 - May also initiate caching of related artifacts in the background.
338
339 Private class members:
340 archive_params: parameters for where to download build artifacts from.
341 """
342
343 def __init__(self, static_dir, source_path):
344 # The local path is of the form /{path to static dir}/{rel_path}/{build}.
345 # local_path must be a subpath of the static directory.
346 self.source_path = source_path
347 rel_path = os.path.basename(os.path.dirname(source_path))
348 build = os.path.basename(source_path)
349 build_dir = os.path.join(static_dir, rel_path, build)
350
351 super(LocalDownloader, self).__init__(static_dir, build_dir, build)
352
353 def Wait(self, name, is_regex_name, timeout):
354 """Verifies the local artifact exists and returns the appropriate names.
355
356 Args:
357 name: Name to look at.
358 is_regex_name: True if the name is a regex pattern.
359 timeout: How long to wait for the artifact to become available.
360
361 Returns:
362 A list of names that match.
363
364 Raises:
365 ArtifactDownloadError: An error occurred when obtaining artifact.
366 """
367 local_path = os.path.join(self.source_path, name)
368 if is_regex_name:
369 filter_re = re.compile(name)
370 for filename in os.listdir(self.source_path):
371 if filter_re.match(filename):
372 return [filename]
373 else:
374 glob_search = glob.glob(local_path)
375 if glob_search and len(glob_search) == 1:
376 return [os.path.basename(glob_search[0])]
377 raise build_artifact.ArtifactDownloadError('Artifact not found.')
378
379 def Fetch(self, remote_name, local_path):
380 """Downloads artifact from Google Storage to a local directory."""
381 install_path = os.path.join(local_path, remote_name)
382 # It's a local path so just copy it into the staged directory.
383 shutil.copyfile(os.path.join(self.source_path, remote_name),
384 install_path)
385 return install_path
386
387 def DescribeSource(self):
388 return self.source_path
389
390
391class LaunchControlDownloader(Downloader):
392 """Downloader of images to the devserver from launch control."""
393
394 def __init__(self, static_dir, build_id, target):
395 """Initialize LaunchControlDownloader.
396
397 Args:
398 static_dir: Root directory to store the build.
399 build_id: Build id of the Android build, e.g., 2155602.
400 target: Target of the Android build, e.g., shamu-userdebug.
401 """
402 build = '%s/%s' % (target, build_id)
403 build_dir = os.path.join(static_dir, '', build)
404
405 self.build_id = build_id
406 self.target = target
407
408 super(LaunchControlDownloader, self).__init__(static_dir, build_dir, build)
409
410 def Wait(self, name, is_regex_name, timeout):
411 """Verifies the local artifact exists and returns the appropriate names.
412
413 Args:
414 name: Name to look at.
415 is_regex_name: True if the name is a regex pattern.
416 timeout: How long to wait for the artifact to become available.
417
418 Returns:
419 A list of names that match.
420
421 Raises:
422 ArtifactDownloadError: An error occurred when obtaining artifact.
423 """
424 raise NotImplementedError()
425
426 def Fetch(self, remote_name, local_path):
427 """Downloads artifact from LaunchControl to a local directory."""
428 install_path = os.path.join(local_path, remote_name)
429 return install_path
430
431 def DescribeSource(self):
432 raise NotImplementedError()