blob: 2a319d6b84a7b07ff490ae62f3e06f793ba905b2 [file] [log] [blame]
Chris Sosa76e44b92013-01-31 20:11:381# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
Frank Farzan37761d12011-12-01 22:29:082# 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 23:35:195import collections
Chris Sosa9164ca32012-03-28 18:04:506import os
Gilad Arnold0b8c3f32012-09-19 21:35:447import threading
Prashanth Ba06d2d22014-03-07 23:35:198from datetime import datetime
Frank Farzan37761d12011-12-01 22:29:089
Chris Sosa76e44b92013-01-31 20:11:3810import build_artifact
Gilad Arnoldc65330c2012-09-20 22:17:4811import common_util
12import log_util
Frank Farzan37761d12011-12-01 22:29:0813
14
Dan Shi6e50c722013-08-19 22:05:0615class 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 22:17:4839class Downloader(log_util.Loggable):
Chris Sosa76e44b92013-01-31 20:11:3840 """Downloader of images to the devsever.
Frank Farzan37761d12011-12-01 22:29:0841
42 Given a URL to a build on the archive server:
Chris Sosa76e44b92013-01-31 20:11:3843 - 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 22:29:0845
Chris Sosa76e44b92013-01-31 20:11:3846 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 22:29:0851 """
52
Alex Millera44d5022012-07-27 18:34:1653 # This filename must be kept in sync with clean_staged_images.py
54 _TIMESTAMP_FILENAME = 'staged.timestamp'
Chris Masonea22d9382012-05-18 19:38:5155
Chris Sosa76e44b92013-01-31 20:11:3856 def __init__(self, static_dir, archive_url):
57 super(Downloader, self).__init__()
58 self._archive_url = archive_url
Frank Farzan37761d12011-12-01 22:29:0859 self._static_dir = static_dir
Chris Sosa76e44b92013-01-31 20:11:3860 self._build_dir = Downloader.GetBuildDir(static_dir, archive_url)
Chris Masone816e38c2012-05-02 19:22:3661
62 @staticmethod
Simran Basi4243a862014-12-12 20:48:3363 def ParseUrl(path_or_url):
64 """Parses |path_or_url| into build relative path and the shorter build name.
65
66 Args:
67 path_or_url: a local path or URL at which build artifacts are archived.
68
69 Returns:
70 A tuple of (build relative path, short build name)
71 """
72 if path_or_url.startswith('gs://'):
73 return Downloader.ParseGSUrl(path_or_url)
74 return Downloader.ParseLocalPath(path_or_url)
75
76 @staticmethod
77 def ParseGSUrl(archive_url):
78 """Parses |path_or_url| into build relative path and the shorter build name.
Chris Masone816e38c2012-05-02 19:22:3679
Chris Sosa76e44b92013-01-31 20:11:3880 Parses archive_url into rel_path and build e.g.
81 gs://chromeos-image-archive/{rel_path}/{build}.
82
83 Args:
84 archive_url: a URL at which build artifacts are archived.
85
86 Returns:
87 A tuple of (build relative path, short build name)
Chris Masone816e38c2012-05-02 19:22:3688 """
Yu-Ju Hongd49d7f42012-06-25 19:23:1189 # The archive_url is of the form gs://server/[some_path/target]/...]/build
90 # This function discards 'gs://server/' and extracts the [some_path/target]
Chris Sosa76e44b92013-01-31 20:11:3891 # as rel_path and the build as build.
Yu-Ju Hongd49d7f42012-06-25 19:23:1192 sub_url = archive_url.partition('://')[2]
93 split_sub_url = sub_url.split('/')
94 rel_path = '/'.join(split_sub_url[1:-1])
Chris Sosa76e44b92013-01-31 20:11:3895 build = split_sub_url[-1]
96 return rel_path, build
Chris Masone816e38c2012-05-02 19:22:3697
98 @staticmethod
Simran Basi4243a862014-12-12 20:48:3399 def ParseLocalPath(local_path):
100 """Parses local_path into rel_path and build.
101
102 Parses a local path into rel_path and build e.g.
103 /{path to static dir}/{rel_path}/{build}.
104
105 Args:
106 local_path: a local path that the build artifacts are stored. Must be a
107 subpath of the static directory.
108
109 Returns:
110 A tuple of (build relative path, short build name)
111 """
112 rel_path = os.path.basename(os.path.dirname(local_path))
113 build = os.path.basename(local_path)
114 return rel_path, build
115
116 @staticmethod
Chris Sosa76e44b92013-01-31 20:11:38117 def GetBuildDir(static_dir, archive_url):
118 """Returns the path to where the artifacts will be staged.
Chris Masone816e38c2012-05-02 19:22:36119
Chris Sosa76e44b92013-01-31 20:11:38120 Args:
121 static_dir: The base static dir that will be used.
122 archive_url: The gs path to the archive url.
Chris Masone816e38c2012-05-02 19:22:36123 """
Chris Sosa76e44b92013-01-31 20:11:38124 # Parse archive_url into rel_path (contains the build target) and
125 # build e.g. gs://chromeos-image-archive/{rel_path}/{build}.
126 rel_path, build = Downloader.ParseUrl(archive_url)
127 return os.path.join(static_dir, rel_path, build)
Frank Farzan37761d12011-12-01 22:29:08128
Chris Sosa9164ca32012-03-28 18:04:50129 @staticmethod
Simran Basief83d6a2014-08-28 21:32:01130 def TouchTimestampForStaged(directory_path):
Alex Millera44d5022012-07-27 18:34:16131 file_name = os.path.join(directory_path, Downloader._TIMESTAMP_FILENAME)
132 # Easiest python version of |touch file_name|
133 with file(file_name, 'a'):
134 os.utime(file_name, None)
135
Dan Shiba0e6742013-06-27 00:39:05136 @staticmethod
137 def _TryRemoveStageDir(directory_path):
Gilad Arnold02dc6552013-11-14 19:27:54138 """If download failed, try to remove the stage dir.
Dan Shiba0e6742013-06-27 00:39:05139
Gilad Arnold02dc6552013-11-14 19:27:54140 If the download attempt failed (ArtifactDownloadError) and staged.timestamp
141 is the only file in that directory. The build could be non-existing, and
142 the directory should be removed.
Dan Shiba0e6742013-06-27 00:39:05143
144 @param directory_path: directory used to stage the image.
145
146 """
147 file_name = os.path.join(directory_path, Downloader._TIMESTAMP_FILENAME)
148 if os.path.exists(file_name) and len(os.listdir(directory_path)) == 1:
149 os.remove(file_name)
150 os.rmdir(directory_path)
151
Prashanth Ba06d2d22014-03-07 23:35:19152 def ListBuildDir(self):
153 """List the files in the build directory.
154
155 Only lists files a single level into the build directory. Includes
156 timestamp information in the listing.
157
158 Returns:
159 A string with information about the files in the build directory.
160 None if the build directory doesn't exist.
161
162 Raises:
163 build_artifact.ArtifactDownloadError: If the build_dir path exists
164 but is not a directory.
165 """
166 if not os.path.exists(self._build_dir):
167 return None
168 if not os.path.isdir(self._build_dir):
169 raise build_artifact.ArtifactDownloadError(
170 'Artifacts %s improperly staged to build_dir path %s. The path is '
171 'not a directory.' % (self._archive_url, self._build_dir))
172
173 ls_format = collections.namedtuple(
174 'ls', ['name', 'accessed', 'modified', 'size'])
175 output_format = ('Name: %(name)s Accessed: %(accessed)s '
176 'Modified: %(modified)s Size: %(size)s bytes.\n')
177
178 build_dir_info = 'Listing contents of :%s \n' % self._build_dir
179 for file_name in os.listdir(self._build_dir):
180 file_path = os.path.join(self._build_dir, file_name)
181 file_info = os.stat(file_path)
182 ls_info = ls_format(file_path,
183 datetime.fromtimestamp(file_info.st_atime),
184 datetime.fromtimestamp(file_info.st_mtime),
185 file_info.st_size)
186 build_dir_info += output_format % ls_info._asdict()
187 return build_dir_info
188
Chris Sosa6b0c6172013-08-06 00:01:33189 def Download(self, artifacts, files, async=False):
Chris Sosa76e44b92013-01-31 20:11:38190 """Downloads and caches the |artifacts|.
Chris Sosa9164ca32012-03-28 18:04:50191
Chris Sosa76e44b92013-01-31 20:11:38192 Downloads and caches the |artifacts|. Returns once these
193 are present on the devserver. A call to this will attempt to cache
194 non-specified artifacts in the background following the principle of
195 spatial locality.
Gilad Arnold6f99b982012-09-12 17:49:40196
Chris Sosa75490802013-10-01 00:21:45197 Args:
198 artifacts: A list of artifact names that correspond to
199 artifacts defined in artifact_info.py to stage.
200 files: A list of filenames to stage from an archive_url.
201 async: If True, return without waiting for download to complete.
202
203 Raises:
Gilad Arnold02dc6552013-11-14 19:27:54204 build_artifact.ArtifactDownloadError: If failed to download the artifact.
Dan Shif8eb0d12013-08-02 00:52:06205
Gilad Arnold6f99b982012-09-12 17:49:40206 """
Chris Sosa76e44b92013-01-31 20:11:38207 common_util.MkDirP(self._build_dir)
Gilad Arnold6f99b982012-09-12 17:49:40208
Chris Sosa76e44b92013-01-31 20:11:38209 # We are doing some work on this build -- let's touch it to indicate that
210 # we shouldn't be cleaning it up anytime soon.
Simran Basief83d6a2014-08-28 21:32:01211 Downloader.TouchTimestampForStaged(self._build_dir)
Gilad Arnold6f99b982012-09-12 17:49:40212
Chris Sosa76e44b92013-01-31 20:11:38213 # Create factory to create build_artifacts from artifact names.
214 build = self.ParseUrl(self._archive_url)[1]
Chris Sosa6b0c6172013-08-06 00:01:33215 factory = build_artifact.ArtifactFactory(
216 self._build_dir, self._archive_url, artifacts, files,
217 build)
Chris Sosa76e44b92013-01-31 20:11:38218 background_artifacts = factory.OptionalArtifacts()
219 if background_artifacts:
220 self._DownloadArtifactsInBackground(background_artifacts)
Gilad Arnold6f99b982012-09-12 17:49:40221
Chris Sosa76e44b92013-01-31 20:11:38222 required_artifacts = factory.RequiredArtifacts()
223 str_repr = [str(a) for a in required_artifacts]
224 self._Log('Downloading artifacts %s.', ' '.join(str_repr))
Dan Shie37f8fe2013-08-09 23:10:29225
Dan Shi6e50c722013-08-19 22:05:06226 if async:
227 self._DownloadArtifactsInBackground(required_artifacts)
228 else:
229 self._DownloadArtifactsSerially(required_artifacts, no_wait=True)
Chris Sosa76e44b92013-01-31 20:11:38230
Chris Sosa6b0c6172013-08-06 00:01:33231 def IsStaged(self, artifacts, files):
Dan Shif8eb0d12013-08-02 00:52:06232 """Check if all artifacts have been downloaded.
233
Chris Sosa6b0c6172013-08-06 00:01:33234 artifacts: A list of artifact names that correspond to
235 artifacts defined in artifact_info.py to stage.
236 files: A list of filenames to stage from an archive_url.
Dan Shif8eb0d12013-08-02 00:52:06237 @returns: True if all artifacts are staged.
Dan Shi6e50c722013-08-19 22:05:06238 @raise exception: that was raised by any artifact when calling Process.
Dan Shif8eb0d12013-08-02 00:52:06239
240 """
241 # Create factory to create build_artifacts from artifact names.
242 build = self.ParseUrl(self._archive_url)[1]
Chris Sosa6b0c6172013-08-06 00:01:33243 factory = build_artifact.ArtifactFactory(
244 self._build_dir, self._archive_url, artifacts, files, build)
Dan Shif8eb0d12013-08-02 00:52:06245 required_artifacts = factory.RequiredArtifacts()
Dan Shi6e50c722013-08-19 22:05:06246 exceptions = [artifact.GetException() for artifact in required_artifacts if
247 artifact.GetException()]
248 if exceptions:
249 raise DownloaderException(exceptions)
250
Dan Shif8eb0d12013-08-02 00:52:06251 return all([artifact.ArtifactStaged() for artifact in required_artifacts])
252
Chris Sosa76e44b92013-01-31 20:11:38253 def _DownloadArtifactsSerially(self, artifacts, no_wait):
254 """Simple function to download all the given artifacts serially.
255
Chris Sosa75490802013-10-01 00:21:45256 Args:
257 artifacts: A list of build_artifact.BuildArtifact instances to
258 download.
259 no_wait: If True, don't block waiting for artifact to exist if we
260 fail to immediately find it.
261
262 Raises:
263 build_artifact.ArtifactDownloadError: If we failed to download the
264 artifact.
Dan Shif8eb0d12013-08-02 00:52:06265
Gilad Arnold6f99b982012-09-12 17:49:40266 """
Dan Shi6e50c722013-08-19 22:05:06267 try:
268 for artifact in artifacts:
269 artifact.Process(no_wait)
Gilad Arnold02dc6552013-11-14 19:27:54270 except build_artifact.ArtifactDownloadError:
Dan Shi6e50c722013-08-19 22:05:06271 Downloader._TryRemoveStageDir(self._build_dir)
272 raise
Gilad Arnold6f99b982012-09-12 17:49:40273
Chris Sosa76e44b92013-01-31 20:11:38274 def _DownloadArtifactsInBackground(self, artifacts):
275 """Downloads |artifacts| in the background.
Gilad Arnold6f99b982012-09-12 17:49:40276
Chris Sosa76e44b92013-01-31 20:11:38277 Downloads |artifacts| in the background. As these are backgrounded
278 artifacts, they are done best effort and may not exist.
Gilad Arnold6f99b982012-09-12 17:49:40279
Chris Sosa76e44b92013-01-31 20:11:38280 Args:
281 artifacts: List of build_artifact.BuildArtifact instances to download.
Gilad Arnold6f99b982012-09-12 17:49:40282 """
Chris Sosa76e44b92013-01-31 20:11:38283 self._Log('Invoking background download of artifacts for %r', artifacts)
284 thread = threading.Thread(target=self._DownloadArtifactsSerially,
285 args=(artifacts, False))
286 thread.start()