blob: 2a319d6b84a7b07ff490ae62f3e06f793ba905b2 [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
Simran Basi4243a862014-12-12 12:48:33 -080063 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 12:22:36 -070079
Chris Sosa76e44b92013-01-31 12:11:38 -080080 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 12:22:36 -070088 """
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -070089 # 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 12:11:38 -080091 # as rel_path and the build as build.
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -070092 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 12:11:38 -080095 build = split_sub_url[-1]
96 return rel_path, build
Chris Masone816e38c2012-05-02 12:22:36 -070097
98 @staticmethod
Simran Basi4243a862014-12-12 12:48:33 -080099 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 12:11:38 -0800117 def GetBuildDir(static_dir, archive_url):
118 """Returns the path to where the artifacts will be staged.
Chris Masone816e38c2012-05-02 12:22:36 -0700119
Chris Sosa76e44b92013-01-31 12:11:38 -0800120 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 12:22:36 -0700123 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800124 # 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 14:29:08 -0800128
Chris Sosa9164ca32012-03-28 11:04:50 -0700129 @staticmethod
Simran Basief83d6a2014-08-28 14:32:01 -0700130 def TouchTimestampForStaged(directory_path):
Alex Millera44d5022012-07-27 11:34:16 -0700131 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-26 17:39:05 -0700136 @staticmethod
137 def _TryRemoveStageDir(directory_path):
Gilad Arnold02dc6552013-11-14 11:27:54 -0800138 """If download failed, try to remove the stage dir.
Dan Shiba0e6742013-06-26 17:39:05 -0700139
Gilad Arnold02dc6552013-11-14 11:27:54 -0800140 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-26 17:39:05 -0700143
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 15:35:19 -0800152 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-05 17:01:33 -0700189 def Download(self, artifacts, files, async=False):
Chris Sosa76e44b92013-01-31 12:11:38 -0800190 """Downloads and caches the |artifacts|.
Chris Sosa9164ca32012-03-28 11:04:50 -0700191
Chris Sosa76e44b92013-01-31 12:11:38 -0800192 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 10:49:40 -0700196
Chris Sosa75490802013-09-30 17:21:45 -0700197 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 11:27:54 -0800204 build_artifact.ArtifactDownloadError: If failed to download the artifact.
Dan Shif8eb0d12013-08-01 17:52:06 -0700205
Gilad Arnold6f99b982012-09-12 10:49:40 -0700206 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800207 common_util.MkDirP(self._build_dir)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700208
Chris Sosa76e44b92013-01-31 12:11:38 -0800209 # 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 14:32:01 -0700211 Downloader.TouchTimestampForStaged(self._build_dir)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700212
Chris Sosa76e44b92013-01-31 12:11:38 -0800213 # Create factory to create build_artifacts from artifact names.
214 build = self.ParseUrl(self._archive_url)[1]
Chris Sosa6b0c6172013-08-05 17:01:33 -0700215 factory = build_artifact.ArtifactFactory(
216 self._build_dir, self._archive_url, artifacts, files,
217 build)
Chris Sosa76e44b92013-01-31 12:11:38 -0800218 background_artifacts = factory.OptionalArtifacts()
219 if background_artifacts:
220 self._DownloadArtifactsInBackground(background_artifacts)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700221
Chris Sosa76e44b92013-01-31 12:11:38 -0800222 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 16:10:29 -0700225
Dan Shi6e50c722013-08-19 15:05:06 -0700226 if async:
227 self._DownloadArtifactsInBackground(required_artifacts)
228 else:
229 self._DownloadArtifactsSerially(required_artifacts, no_wait=True)
Chris Sosa76e44b92013-01-31 12:11:38 -0800230
Chris Sosa6b0c6172013-08-05 17:01:33 -0700231 def IsStaged(self, artifacts, files):
Dan Shif8eb0d12013-08-01 17:52:06 -0700232 """Check if all artifacts have been downloaded.
233
Chris Sosa6b0c6172013-08-05 17:01:33 -0700234 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-01 17:52:06 -0700237 @returns: True if all artifacts are staged.
Dan Shi6e50c722013-08-19 15:05:06 -0700238 @raise exception: that was raised by any artifact when calling Process.
Dan Shif8eb0d12013-08-01 17:52:06 -0700239
240 """
241 # Create factory to create build_artifacts from artifact names.
242 build = self.ParseUrl(self._archive_url)[1]
Chris Sosa6b0c6172013-08-05 17:01:33 -0700243 factory = build_artifact.ArtifactFactory(
244 self._build_dir, self._archive_url, artifacts, files, build)
Dan Shif8eb0d12013-08-01 17:52:06 -0700245 required_artifacts = factory.RequiredArtifacts()
Dan Shi6e50c722013-08-19 15:05:06 -0700246 exceptions = [artifact.GetException() for artifact in required_artifacts if
247 artifact.GetException()]
248 if exceptions:
249 raise DownloaderException(exceptions)
250
Dan Shif8eb0d12013-08-01 17:52:06 -0700251 return all([artifact.ArtifactStaged() for artifact in required_artifacts])
252
Chris Sosa76e44b92013-01-31 12:11:38 -0800253 def _DownloadArtifactsSerially(self, artifacts, no_wait):
254 """Simple function to download all the given artifacts serially.
255
Chris Sosa75490802013-09-30 17:21:45 -0700256 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-01 17:52:06 -0700265
Gilad Arnold6f99b982012-09-12 10:49:40 -0700266 """
Dan Shi6e50c722013-08-19 15:05:06 -0700267 try:
268 for artifact in artifacts:
269 artifact.Process(no_wait)
Gilad Arnold02dc6552013-11-14 11:27:54 -0800270 except build_artifact.ArtifactDownloadError:
Dan Shi6e50c722013-08-19 15:05:06 -0700271 Downloader._TryRemoveStageDir(self._build_dir)
272 raise
Gilad Arnold6f99b982012-09-12 10:49:40 -0700273
Chris Sosa76e44b92013-01-31 12:11:38 -0800274 def _DownloadArtifactsInBackground(self, artifacts):
275 """Downloads |artifacts| in the background.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700276
Chris Sosa76e44b92013-01-31 12:11:38 -0800277 Downloads |artifacts| in the background. As these are backgrounded
278 artifacts, they are done best effort and may not exist.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700279
Chris Sosa76e44b92013-01-31 12:11:38 -0800280 Args:
281 artifacts: List of build_artifact.BuildArtifact instances to download.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700282 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800283 self._Log('Invoking background download of artifacts for %r', artifacts)
284 thread = threading.Thread(target=self._DownloadArtifactsSerially,
285 args=(artifacts, False))
286 thread.start()