blob: 9174f106e73debc3e06569946824b86da889b7e7 [file] [log] [blame]
Chris Sosa76e44b92013-01-31 12:11:38 -08001# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
Frank Farzan37761d12011-12-01 14:29:08 -08002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
Prashanth Ba06d2d22014-03-07 15:35:19 -08005import collections
Chris Sosa9164ca32012-03-28 11:04:50 -07006import os
Gilad Arnold0b8c3f32012-09-19 14:35:44 -07007import threading
Prashanth Ba06d2d22014-03-07 15:35:19 -08008from datetime import datetime
Frank Farzan37761d12011-12-01 14:29:08 -08009
Chris Sosa76e44b92013-01-31 12:11:38 -080010import build_artifact
Gilad Arnoldc65330c2012-09-20 15:17:48 -070011import common_util
12import log_util
Frank Farzan37761d12011-12-01 14:29:08 -080013
14
Dan Shi6e50c722013-08-19 15:05:06 -070015class DownloaderException(Exception):
16 """Exception that aggregates all exceptions raised during async download.
17
18 Exceptions could be raised in artifact.Process method, and saved to files.
19 When caller calls IsStaged to check the downloading progress, devserver can
20 retrieve the persisted exceptions from the files, wrap them into a
21 DownloaderException, and raise it.
22 """
23 def __init__(self, exceptions):
24 """Initialize a DownloaderException instance with a list of exceptions.
25
26 @param exceptions: Exceptions raised when downloading artifacts.
27 """
28 message = 'Exceptions were raised when downloading artifacts.'
29 Exception.__init__(self, message)
30 self.exceptions = exceptions
31
32 def __repr__(self):
33 return self.__str__()
34
35 def __str__(self):
36 """Return a custom exception message with all exceptions merged."""
37 return '--------\n'.join([str(exception) for exception in self.exceptions])
38
Gilad Arnoldc65330c2012-09-20 15:17:48 -070039class Downloader(log_util.Loggable):
Chris Sosa76e44b92013-01-31 12:11:38 -080040 """Downloader of images to the devsever.
Frank Farzan37761d12011-12-01 14:29:08 -080041
42 Given a URL to a build on the archive server:
Chris Sosa76e44b92013-01-31 12:11:38 -080043 - Caches that build and the given artifacts onto the devserver.
44 - May also initiate caching of related artifacts in the background.
Frank Farzan37761d12011-12-01 14:29:08 -080045
Chris Sosa76e44b92013-01-31 12:11:38 -080046 Private class members:
47 archive_url: a URL where to download build artifacts from.
48 static_dir: local filesystem directory to store all artifacts.
49 build_dir: the local filesystem directory to store artifacts for the given
50 build defined by the archive_url.
Frank Farzan37761d12011-12-01 14:29:08 -080051 """
52
Alex Millera44d5022012-07-27 11:34:16 -070053 # This filename must be kept in sync with clean_staged_images.py
54 _TIMESTAMP_FILENAME = 'staged.timestamp'
Chris Masonea22d9382012-05-18 12:38:51 -070055
Chris Sosa76e44b92013-01-31 12:11:38 -080056 def __init__(self, static_dir, archive_url):
57 super(Downloader, self).__init__()
58 self._archive_url = archive_url
Frank Farzan37761d12011-12-01 14:29:08 -080059 self._static_dir = static_dir
Chris Sosa76e44b92013-01-31 12:11:38 -080060 self._build_dir = Downloader.GetBuildDir(static_dir, archive_url)
Chris Masone816e38c2012-05-02 12:22:36 -070061
62 @staticmethod
Chris Sosacde6bf42012-05-31 18:36:39 -070063 def ParseUrl(archive_url):
Chris Sosa76e44b92013-01-31 12:11:38 -080064 """Parses archive_url into rel_path and build.
Chris Masone816e38c2012-05-02 12:22:36 -070065
Chris Sosa76e44b92013-01-31 12:11:38 -080066 Parses archive_url into rel_path and build e.g.
67 gs://chromeos-image-archive/{rel_path}/{build}.
68
69 Args:
70 archive_url: a URL at which build artifacts are archived.
71
72 Returns:
73 A tuple of (build relative path, short build name)
Chris Masone816e38c2012-05-02 12:22:36 -070074 """
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -070075 # The archive_url is of the form gs://server/[some_path/target]/...]/build
76 # This function discards 'gs://server/' and extracts the [some_path/target]
Chris Sosa76e44b92013-01-31 12:11:38 -080077 # as rel_path and the build as build.
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -070078 sub_url = archive_url.partition('://')[2]
79 split_sub_url = sub_url.split('/')
80 rel_path = '/'.join(split_sub_url[1:-1])
Chris Sosa76e44b92013-01-31 12:11:38 -080081 build = split_sub_url[-1]
82 return rel_path, build
Chris Masone816e38c2012-05-02 12:22:36 -070083
84 @staticmethod
Chris Sosa76e44b92013-01-31 12:11:38 -080085 def GetBuildDir(static_dir, archive_url):
86 """Returns the path to where the artifacts will be staged.
Chris Masone816e38c2012-05-02 12:22:36 -070087
Chris Sosa76e44b92013-01-31 12:11:38 -080088 Args:
89 static_dir: The base static dir that will be used.
90 archive_url: The gs path to the archive url.
Chris Masone816e38c2012-05-02 12:22:36 -070091 """
Chris Sosa76e44b92013-01-31 12:11:38 -080092 # Parse archive_url into rel_path (contains the build target) and
93 # build e.g. gs://chromeos-image-archive/{rel_path}/{build}.
94 rel_path, build = Downloader.ParseUrl(archive_url)
95 return os.path.join(static_dir, rel_path, 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
112 @param directory_path: directory used to stage the image.
113
114 """
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(
142 'ls', ['name', 'accessed', 'modified', 'size'])
143 output_format = ('Name: %(name)s Accessed: %(accessed)s '
144 'Modified: %(modified)s Size: %(size)s bytes.\n')
145
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
Chris Sosa6b0c6172013-08-05 17:01:33 -0700157 def Download(self, artifacts, files, async=False):
Chris Sosa76e44b92013-01-31 12:11:38 -0800158 """Downloads and caches the |artifacts|.
Chris Sosa9164ca32012-03-28 11:04:50 -0700159
Chris Sosa76e44b92013-01-31 12:11:38 -0800160 Downloads and caches the |artifacts|. Returns once these
161 are present on the devserver. A call to this will attempt to cache
162 non-specified artifacts in the background following the principle of
163 spatial locality.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700164
Chris Sosa75490802013-09-30 17:21:45 -0700165 Args:
166 artifacts: A list of artifact names that correspond to
167 artifacts defined in artifact_info.py to stage.
168 files: A list of filenames to stage from an archive_url.
169 async: If True, return without waiting for download to complete.
170
171 Raises:
Gilad Arnold02dc6552013-11-14 11:27:54 -0800172 build_artifact.ArtifactDownloadError: If failed to download the artifact.
Dan Shif8eb0d12013-08-01 17:52:06 -0700173
Gilad Arnold6f99b982012-09-12 10:49:40 -0700174 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800175 common_util.MkDirP(self._build_dir)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700176
Chris Sosa76e44b92013-01-31 12:11:38 -0800177 # We are doing some work on this build -- let's touch it to indicate that
178 # we shouldn't be cleaning it up anytime soon.
Simran Basief83d6a2014-08-28 14:32:01 -0700179 Downloader.TouchTimestampForStaged(self._build_dir)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700180
Chris Sosa76e44b92013-01-31 12:11:38 -0800181 # Create factory to create build_artifacts from artifact names.
182 build = self.ParseUrl(self._archive_url)[1]
Chris Sosa6b0c6172013-08-05 17:01:33 -0700183 factory = build_artifact.ArtifactFactory(
184 self._build_dir, self._archive_url, artifacts, files,
185 build)
Chris Sosa76e44b92013-01-31 12:11:38 -0800186 background_artifacts = factory.OptionalArtifacts()
187 if background_artifacts:
188 self._DownloadArtifactsInBackground(background_artifacts)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700189
Chris Sosa76e44b92013-01-31 12:11:38 -0800190 required_artifacts = factory.RequiredArtifacts()
191 str_repr = [str(a) for a in required_artifacts]
192 self._Log('Downloading artifacts %s.', ' '.join(str_repr))
Dan Shie37f8fe2013-08-09 16:10:29 -0700193
Dan Shi6e50c722013-08-19 15:05:06 -0700194 if async:
195 self._DownloadArtifactsInBackground(required_artifacts)
196 else:
197 self._DownloadArtifactsSerially(required_artifacts, no_wait=True)
Chris Sosa76e44b92013-01-31 12:11:38 -0800198
Chris Sosa6b0c6172013-08-05 17:01:33 -0700199 def IsStaged(self, artifacts, files):
Dan Shif8eb0d12013-08-01 17:52:06 -0700200 """Check if all artifacts have been downloaded.
201
Chris Sosa6b0c6172013-08-05 17:01:33 -0700202 artifacts: A list of artifact names that correspond to
203 artifacts defined in artifact_info.py to stage.
204 files: A list of filenames to stage from an archive_url.
Dan Shif8eb0d12013-08-01 17:52:06 -0700205 @returns: True if all artifacts are staged.
Dan Shi6e50c722013-08-19 15:05:06 -0700206 @raise exception: that was raised by any artifact when calling Process.
Dan Shif8eb0d12013-08-01 17:52:06 -0700207
208 """
209 # Create factory to create build_artifacts from artifact names.
210 build = self.ParseUrl(self._archive_url)[1]
Chris Sosa6b0c6172013-08-05 17:01:33 -0700211 factory = build_artifact.ArtifactFactory(
212 self._build_dir, self._archive_url, artifacts, files, build)
Dan Shif8eb0d12013-08-01 17:52:06 -0700213 required_artifacts = factory.RequiredArtifacts()
Dan Shi6e50c722013-08-19 15:05:06 -0700214 exceptions = [artifact.GetException() for artifact in required_artifacts if
215 artifact.GetException()]
216 if exceptions:
217 raise DownloaderException(exceptions)
218
Dan Shif8eb0d12013-08-01 17:52:06 -0700219 return all([artifact.ArtifactStaged() for artifact in required_artifacts])
220
Chris Sosa76e44b92013-01-31 12:11:38 -0800221 def _DownloadArtifactsSerially(self, artifacts, no_wait):
222 """Simple function to download all the given artifacts serially.
223
Chris Sosa75490802013-09-30 17:21:45 -0700224 Args:
225 artifacts: A list of build_artifact.BuildArtifact instances to
226 download.
227 no_wait: If True, don't block waiting for artifact to exist if we
228 fail to immediately find it.
229
230 Raises:
231 build_artifact.ArtifactDownloadError: If we failed to download the
232 artifact.
Dan Shif8eb0d12013-08-01 17:52:06 -0700233
Gilad Arnold6f99b982012-09-12 10:49:40 -0700234 """
Dan Shi6e50c722013-08-19 15:05:06 -0700235 try:
236 for artifact in artifacts:
237 artifact.Process(no_wait)
Gilad Arnold02dc6552013-11-14 11:27:54 -0800238 except build_artifact.ArtifactDownloadError:
Dan Shi6e50c722013-08-19 15:05:06 -0700239 Downloader._TryRemoveStageDir(self._build_dir)
240 raise
Gilad Arnold6f99b982012-09-12 10:49:40 -0700241
Chris Sosa76e44b92013-01-31 12:11:38 -0800242 def _DownloadArtifactsInBackground(self, artifacts):
243 """Downloads |artifacts| in the background.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700244
Chris Sosa76e44b92013-01-31 12:11:38 -0800245 Downloads |artifacts| in the background. As these are backgrounded
246 artifacts, they are done best effort and may not exist.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700247
Chris Sosa76e44b92013-01-31 12:11:38 -0800248 Args:
249 artifacts: List of build_artifact.BuildArtifact instances to download.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700250 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800251 self._Log('Invoking background download of artifacts for %r', artifacts)
252 thread = threading.Thread(target=self._DownloadArtifactsSerially,
253 args=(artifacts, False))
254 thread.start()