blob: 7bb2dd0f3fa5e7e4f08c255cb2868abca1804beb [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
Chris Sosa9164ca32012-03-28 11:04:50 -07005import os
Gilad Arnold0b8c3f32012-09-19 14:35:44 -07006import threading
Frank Farzan37761d12011-12-01 14:29:08 -08007
Chris Sosa76e44b92013-01-31 12:11:38 -08008import build_artifact
Gilad Arnoldc65330c2012-09-20 15:17:48 -07009import common_util
Dan Shiba0e6742013-06-26 17:39:05 -070010import gsutil_util
Gilad Arnoldc65330c2012-09-20 15:17:48 -070011import log_util
Frank Farzan37761d12011-12-01 14:29:08 -080012
13
Dan Shi6e50c722013-08-19 15:05:06 -070014class DownloaderException(Exception):
15 """Exception that aggregates all exceptions raised during async download.
16
17 Exceptions could be raised in artifact.Process method, and saved to files.
18 When caller calls IsStaged to check the downloading progress, devserver can
19 retrieve the persisted exceptions from the files, wrap them into a
20 DownloaderException, and raise it.
21 """
22 def __init__(self, exceptions):
23 """Initialize a DownloaderException instance with a list of exceptions.
24
25 @param exceptions: Exceptions raised when downloading artifacts.
26 """
27 message = 'Exceptions were raised when downloading artifacts.'
28 Exception.__init__(self, message)
29 self.exceptions = exceptions
30
31 def __repr__(self):
32 return self.__str__()
33
34 def __str__(self):
35 """Return a custom exception message with all exceptions merged."""
36 return '--------\n'.join([str(exception) for exception in self.exceptions])
37
Gilad Arnoldc65330c2012-09-20 15:17:48 -070038class Downloader(log_util.Loggable):
Chris Sosa76e44b92013-01-31 12:11:38 -080039 """Downloader of images to the devsever.
Frank Farzan37761d12011-12-01 14:29:08 -080040
41 Given a URL to a build on the archive server:
Chris Sosa76e44b92013-01-31 12:11:38 -080042 - Caches that build and the given artifacts onto the devserver.
43 - May also initiate caching of related artifacts in the background.
Frank Farzan37761d12011-12-01 14:29:08 -080044
Chris Sosa76e44b92013-01-31 12:11:38 -080045 Private class members:
46 archive_url: a URL where to download build artifacts from.
47 static_dir: local filesystem directory to store all artifacts.
48 build_dir: the local filesystem directory to store artifacts for the given
49 build defined by the archive_url.
Frank Farzan37761d12011-12-01 14:29:08 -080050 """
51
Alex Millera44d5022012-07-27 11:34:16 -070052 # This filename must be kept in sync with clean_staged_images.py
53 _TIMESTAMP_FILENAME = 'staged.timestamp'
Chris Masonea22d9382012-05-18 12:38:51 -070054
Chris Sosa76e44b92013-01-31 12:11:38 -080055 def __init__(self, static_dir, archive_url):
56 super(Downloader, self).__init__()
57 self._archive_url = archive_url
Frank Farzan37761d12011-12-01 14:29:08 -080058 self._static_dir = static_dir
Chris Sosa76e44b92013-01-31 12:11:38 -080059 self._build_dir = Downloader.GetBuildDir(static_dir, archive_url)
Chris Masone816e38c2012-05-02 12:22:36 -070060
61 @staticmethod
Chris Sosacde6bf42012-05-31 18:36:39 -070062 def ParseUrl(archive_url):
Chris Sosa76e44b92013-01-31 12:11:38 -080063 """Parses archive_url into rel_path and build.
Chris Masone816e38c2012-05-02 12:22:36 -070064
Chris Sosa76e44b92013-01-31 12:11:38 -080065 Parses archive_url into rel_path and build e.g.
66 gs://chromeos-image-archive/{rel_path}/{build}.
67
68 Args:
69 archive_url: a URL at which build artifacts are archived.
70
71 Returns:
72 A tuple of (build relative path, short build name)
Chris Masone816e38c2012-05-02 12:22:36 -070073 """
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -070074 # The archive_url is of the form gs://server/[some_path/target]/...]/build
75 # This function discards 'gs://server/' and extracts the [some_path/target]
Chris Sosa76e44b92013-01-31 12:11:38 -080076 # as rel_path and the build as build.
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -070077 sub_url = archive_url.partition('://')[2]
78 split_sub_url = sub_url.split('/')
79 rel_path = '/'.join(split_sub_url[1:-1])
Chris Sosa76e44b92013-01-31 12:11:38 -080080 build = split_sub_url[-1]
81 return rel_path, build
Chris Masone816e38c2012-05-02 12:22:36 -070082
83 @staticmethod
Chris Sosa76e44b92013-01-31 12:11:38 -080084 def GetBuildDir(static_dir, archive_url):
85 """Returns the path to where the artifacts will be staged.
Chris Masone816e38c2012-05-02 12:22:36 -070086
Chris Sosa76e44b92013-01-31 12:11:38 -080087 Args:
88 static_dir: The base static dir that will be used.
89 archive_url: The gs path to the archive url.
Chris Masone816e38c2012-05-02 12:22:36 -070090 """
Chris Sosa76e44b92013-01-31 12:11:38 -080091 # Parse archive_url into rel_path (contains the build target) and
92 # build e.g. gs://chromeos-image-archive/{rel_path}/{build}.
93 rel_path, build = Downloader.ParseUrl(archive_url)
94 return os.path.join(static_dir, rel_path, build)
Frank Farzan37761d12011-12-01 14:29:08 -080095
Chris Sosa9164ca32012-03-28 11:04:50 -070096 @staticmethod
Alex Millera44d5022012-07-27 11:34:16 -070097 def _TouchTimestampForStaged(directory_path):
98 file_name = os.path.join(directory_path, Downloader._TIMESTAMP_FILENAME)
99 # Easiest python version of |touch file_name|
100 with file(file_name, 'a'):
101 os.utime(file_name, None)
102
Dan Shiba0e6742013-06-26 17:39:05 -0700103 @staticmethod
104 def _TryRemoveStageDir(directory_path):
105 """If download failed with GSUtilError, try to remove the stage dir.
106
107 If the download attempt failed with GSUtilError and staged.timestamp is the
108 only file in that directory. The build could be non-existing, and the
109 directory should be removed.
110
111 @param directory_path: directory used to stage the image.
112
113 """
114 file_name = os.path.join(directory_path, Downloader._TIMESTAMP_FILENAME)
115 if os.path.exists(file_name) and len(os.listdir(directory_path)) == 1:
116 os.remove(file_name)
117 os.rmdir(directory_path)
118
Chris Sosa6b0c6172013-08-05 17:01:33 -0700119 def Download(self, artifacts, files, async=False):
Chris Sosa76e44b92013-01-31 12:11:38 -0800120 """Downloads and caches the |artifacts|.
Chris Sosa9164ca32012-03-28 11:04:50 -0700121
Chris Sosa76e44b92013-01-31 12:11:38 -0800122 Downloads and caches the |artifacts|. Returns once these
123 are present on the devserver. A call to this will attempt to cache
124 non-specified artifacts in the background following the principle of
125 spatial locality.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700126
Chris Sosa6b0c6172013-08-05 17:01:33 -0700127 artifacts: A list of artifact names that correspond to
128 artifacts defined in artifact_info.py to stage.
129 files: A list of filenames to stage from an archive_url.
130 async: If True, return without waiting for download to complete.
Dan Shif8eb0d12013-08-01 17:52:06 -0700131
Gilad Arnold6f99b982012-09-12 10:49:40 -0700132 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800133 common_util.MkDirP(self._build_dir)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700134
Chris Sosa76e44b92013-01-31 12:11:38 -0800135 # We are doing some work on this build -- let's touch it to indicate that
136 # we shouldn't be cleaning it up anytime soon.
137 Downloader._TouchTimestampForStaged(self._build_dir)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700138
Chris Sosa76e44b92013-01-31 12:11:38 -0800139 # Create factory to create build_artifacts from artifact names.
140 build = self.ParseUrl(self._archive_url)[1]
Chris Sosa6b0c6172013-08-05 17:01:33 -0700141 factory = build_artifact.ArtifactFactory(
142 self._build_dir, self._archive_url, artifacts, files,
143 build)
Chris Sosa76e44b92013-01-31 12:11:38 -0800144 background_artifacts = factory.OptionalArtifacts()
145 if background_artifacts:
146 self._DownloadArtifactsInBackground(background_artifacts)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700147
Chris Sosa76e44b92013-01-31 12:11:38 -0800148 required_artifacts = factory.RequiredArtifacts()
149 str_repr = [str(a) for a in required_artifacts]
150 self._Log('Downloading artifacts %s.', ' '.join(str_repr))
Dan Shie37f8fe2013-08-09 16:10:29 -0700151
Dan Shi6e50c722013-08-19 15:05:06 -0700152 if async:
153 self._DownloadArtifactsInBackground(required_artifacts)
154 else:
155 self._DownloadArtifactsSerially(required_artifacts, no_wait=True)
Chris Sosa76e44b92013-01-31 12:11:38 -0800156
Chris Sosa6b0c6172013-08-05 17:01:33 -0700157 def IsStaged(self, artifacts, files):
Dan Shif8eb0d12013-08-01 17:52:06 -0700158 """Check if all artifacts have been downloaded.
159
Chris Sosa6b0c6172013-08-05 17:01:33 -0700160 artifacts: A list of artifact names that correspond to
161 artifacts defined in artifact_info.py to stage.
162 files: A list of filenames to stage from an archive_url.
Dan Shif8eb0d12013-08-01 17:52:06 -0700163 @returns: True if all artifacts are staged.
Dan Shi6e50c722013-08-19 15:05:06 -0700164 @raise exception: that was raised by any artifact when calling Process.
Dan Shif8eb0d12013-08-01 17:52:06 -0700165
166 """
167 # Create factory to create build_artifacts from artifact names.
168 build = self.ParseUrl(self._archive_url)[1]
Chris Sosa6b0c6172013-08-05 17:01:33 -0700169 factory = build_artifact.ArtifactFactory(
170 self._build_dir, self._archive_url, artifacts, files, build)
Dan Shif8eb0d12013-08-01 17:52:06 -0700171 required_artifacts = factory.RequiredArtifacts()
Dan Shi6e50c722013-08-19 15:05:06 -0700172 exceptions = [artifact.GetException() for artifact in required_artifacts if
173 artifact.GetException()]
174 if exceptions:
175 raise DownloaderException(exceptions)
176
Dan Shif8eb0d12013-08-01 17:52:06 -0700177 return all([artifact.ArtifactStaged() for artifact in required_artifacts])
178
Chris Sosa76e44b92013-01-31 12:11:38 -0800179 def _DownloadArtifactsSerially(self, artifacts, no_wait):
180 """Simple function to download all the given artifacts serially.
181
Dan Shif8eb0d12013-08-01 17:52:06 -0700182 @param artifacts: A list of build_artifact.BuildArtifact instances to
183 download.
184 @param no_wait: If True, don't block waiting for artifact to exist if we
185 fail to immediately find it.
186
Gilad Arnold6f99b982012-09-12 10:49:40 -0700187 """
Dan Shi6e50c722013-08-19 15:05:06 -0700188 try:
189 for artifact in artifacts:
190 artifact.Process(no_wait)
191 except gsutil_util.GSUtilError:
192 Downloader._TryRemoveStageDir(self._build_dir)
193 raise
Gilad Arnold6f99b982012-09-12 10:49:40 -0700194
Chris Sosa76e44b92013-01-31 12:11:38 -0800195 def _DownloadArtifactsInBackground(self, artifacts):
196 """Downloads |artifacts| in the background.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700197
Chris Sosa76e44b92013-01-31 12:11:38 -0800198 Downloads |artifacts| in the background. As these are backgrounded
199 artifacts, they are done best effort and may not exist.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700200
Chris Sosa76e44b92013-01-31 12:11:38 -0800201 Args:
202 artifacts: List of build_artifact.BuildArtifact instances to download.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700203 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800204 self._Log('Invoking background download of artifacts for %r', artifacts)
205 thread = threading.Thread(target=self._DownloadArtifactsSerially,
206 args=(artifacts, False))
207 thread.start()