blob: 9b3f5cb9f057a3f6891c92c7120f90136e0f9f57 [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
10import log_util
Frank Farzan37761d12011-12-01 14:29:08 -080011
12
Dan Shi6e50c722013-08-19 15:05:06 -070013class DownloaderException(Exception):
14 """Exception that aggregates all exceptions raised during async download.
15
16 Exceptions could be raised in artifact.Process method, and saved to files.
17 When caller calls IsStaged to check the downloading progress, devserver can
18 retrieve the persisted exceptions from the files, wrap them into a
19 DownloaderException, and raise it.
20 """
21 def __init__(self, exceptions):
22 """Initialize a DownloaderException instance with a list of exceptions.
23
24 @param exceptions: Exceptions raised when downloading artifacts.
25 """
26 message = 'Exceptions were raised when downloading artifacts.'
27 Exception.__init__(self, message)
28 self.exceptions = exceptions
29
30 def __repr__(self):
31 return self.__str__()
32
33 def __str__(self):
34 """Return a custom exception message with all exceptions merged."""
35 return '--------\n'.join([str(exception) for exception in self.exceptions])
36
Gilad Arnoldc65330c2012-09-20 15:17:48 -070037class Downloader(log_util.Loggable):
Chris Sosa76e44b92013-01-31 12:11:38 -080038 """Downloader of images to the devsever.
Frank Farzan37761d12011-12-01 14:29:08 -080039
40 Given a URL to a build on the archive server:
Chris Sosa76e44b92013-01-31 12:11:38 -080041 - Caches that build and the given artifacts onto the devserver.
42 - May also initiate caching of related artifacts in the background.
Frank Farzan37761d12011-12-01 14:29:08 -080043
Chris Sosa76e44b92013-01-31 12:11:38 -080044 Private class members:
45 archive_url: a URL where to download build artifacts from.
46 static_dir: local filesystem directory to store all artifacts.
47 build_dir: the local filesystem directory to store artifacts for the given
48 build defined by the archive_url.
Frank Farzan37761d12011-12-01 14:29:08 -080049 """
50
Alex Millera44d5022012-07-27 11:34:16 -070051 # This filename must be kept in sync with clean_staged_images.py
52 _TIMESTAMP_FILENAME = 'staged.timestamp'
Chris Masonea22d9382012-05-18 12:38:51 -070053
Chris Sosa76e44b92013-01-31 12:11:38 -080054 def __init__(self, static_dir, archive_url):
55 super(Downloader, self).__init__()
56 self._archive_url = archive_url
Frank Farzan37761d12011-12-01 14:29:08 -080057 self._static_dir = static_dir
Chris Sosa76e44b92013-01-31 12:11:38 -080058 self._build_dir = Downloader.GetBuildDir(static_dir, archive_url)
Chris Masone816e38c2012-05-02 12:22:36 -070059
60 @staticmethod
Chris Sosacde6bf42012-05-31 18:36:39 -070061 def ParseUrl(archive_url):
Chris Sosa76e44b92013-01-31 12:11:38 -080062 """Parses archive_url into rel_path and build.
Chris Masone816e38c2012-05-02 12:22:36 -070063
Chris Sosa76e44b92013-01-31 12:11:38 -080064 Parses archive_url into rel_path and build e.g.
65 gs://chromeos-image-archive/{rel_path}/{build}.
66
67 Args:
68 archive_url: a URL at which build artifacts are archived.
69
70 Returns:
71 A tuple of (build relative path, short build name)
Chris Masone816e38c2012-05-02 12:22:36 -070072 """
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -070073 # The archive_url is of the form gs://server/[some_path/target]/...]/build
74 # This function discards 'gs://server/' and extracts the [some_path/target]
Chris Sosa76e44b92013-01-31 12:11:38 -080075 # as rel_path and the build as build.
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -070076 sub_url = archive_url.partition('://')[2]
77 split_sub_url = sub_url.split('/')
78 rel_path = '/'.join(split_sub_url[1:-1])
Chris Sosa76e44b92013-01-31 12:11:38 -080079 build = split_sub_url[-1]
80 return rel_path, build
Chris Masone816e38c2012-05-02 12:22:36 -070081
82 @staticmethod
Chris Sosa76e44b92013-01-31 12:11:38 -080083 def GetBuildDir(static_dir, archive_url):
84 """Returns the path to where the artifacts will be staged.
Chris Masone816e38c2012-05-02 12:22:36 -070085
Chris Sosa76e44b92013-01-31 12:11:38 -080086 Args:
87 static_dir: The base static dir that will be used.
88 archive_url: The gs path to the archive url.
Chris Masone816e38c2012-05-02 12:22:36 -070089 """
Chris Sosa76e44b92013-01-31 12:11:38 -080090 # Parse archive_url into rel_path (contains the build target) and
91 # build e.g. gs://chromeos-image-archive/{rel_path}/{build}.
92 rel_path, build = Downloader.ParseUrl(archive_url)
93 return os.path.join(static_dir, rel_path, build)
Frank Farzan37761d12011-12-01 14:29:08 -080094
Chris Sosa9164ca32012-03-28 11:04:50 -070095 @staticmethod
Alex Millera44d5022012-07-27 11:34:16 -070096 def _TouchTimestampForStaged(directory_path):
97 file_name = os.path.join(directory_path, Downloader._TIMESTAMP_FILENAME)
98 # Easiest python version of |touch file_name|
99 with file(file_name, 'a'):
100 os.utime(file_name, None)
101
Dan Shiba0e6742013-06-26 17:39:05 -0700102 @staticmethod
103 def _TryRemoveStageDir(directory_path):
Gilad Arnold02dc6552013-11-14 11:27:54 -0800104 """If download failed, try to remove the stage dir.
Dan Shiba0e6742013-06-26 17:39:05 -0700105
Gilad Arnold02dc6552013-11-14 11:27:54 -0800106 If the download attempt failed (ArtifactDownloadError) and staged.timestamp
107 is the only file in that directory. The build could be non-existing, and
108 the directory should be removed.
Dan Shiba0e6742013-06-26 17:39:05 -0700109
110 @param directory_path: directory used to stage the image.
111
112 """
113 file_name = os.path.join(directory_path, Downloader._TIMESTAMP_FILENAME)
114 if os.path.exists(file_name) and len(os.listdir(directory_path)) == 1:
115 os.remove(file_name)
116 os.rmdir(directory_path)
117
Chris Sosa6b0c6172013-08-05 17:01:33 -0700118 def Download(self, artifacts, files, async=False):
Chris Sosa76e44b92013-01-31 12:11:38 -0800119 """Downloads and caches the |artifacts|.
Chris Sosa9164ca32012-03-28 11:04:50 -0700120
Chris Sosa76e44b92013-01-31 12:11:38 -0800121 Downloads and caches the |artifacts|. Returns once these
122 are present on the devserver. A call to this will attempt to cache
123 non-specified artifacts in the background following the principle of
124 spatial locality.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700125
Chris Sosa75490802013-09-30 17:21:45 -0700126 Args:
127 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.
131
132 Raises:
Gilad Arnold02dc6552013-11-14 11:27:54 -0800133 build_artifact.ArtifactDownloadError: If failed to download the artifact.
Dan Shif8eb0d12013-08-01 17:52:06 -0700134
Gilad Arnold6f99b982012-09-12 10:49:40 -0700135 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800136 common_util.MkDirP(self._build_dir)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700137
Chris Sosa76e44b92013-01-31 12:11:38 -0800138 # We are doing some work on this build -- let's touch it to indicate that
139 # we shouldn't be cleaning it up anytime soon.
140 Downloader._TouchTimestampForStaged(self._build_dir)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700141
Chris Sosa76e44b92013-01-31 12:11:38 -0800142 # Create factory to create build_artifacts from artifact names.
143 build = self.ParseUrl(self._archive_url)[1]
Chris Sosa6b0c6172013-08-05 17:01:33 -0700144 factory = build_artifact.ArtifactFactory(
145 self._build_dir, self._archive_url, artifacts, files,
146 build)
Chris Sosa76e44b92013-01-31 12:11:38 -0800147 background_artifacts = factory.OptionalArtifacts()
148 if background_artifacts:
149 self._DownloadArtifactsInBackground(background_artifacts)
Gilad Arnold6f99b982012-09-12 10:49:40 -0700150
Chris Sosa76e44b92013-01-31 12:11:38 -0800151 required_artifacts = factory.RequiredArtifacts()
152 str_repr = [str(a) for a in required_artifacts]
153 self._Log('Downloading artifacts %s.', ' '.join(str_repr))
Dan Shie37f8fe2013-08-09 16:10:29 -0700154
Dan Shi6e50c722013-08-19 15:05:06 -0700155 if async:
156 self._DownloadArtifactsInBackground(required_artifacts)
157 else:
158 self._DownloadArtifactsSerially(required_artifacts, no_wait=True)
Chris Sosa76e44b92013-01-31 12:11:38 -0800159
Chris Sosa6b0c6172013-08-05 17:01:33 -0700160 def IsStaged(self, artifacts, files):
Dan Shif8eb0d12013-08-01 17:52:06 -0700161 """Check if all artifacts have been downloaded.
162
Chris Sosa6b0c6172013-08-05 17:01:33 -0700163 artifacts: A list of artifact names that correspond to
164 artifacts defined in artifact_info.py to stage.
165 files: A list of filenames to stage from an archive_url.
Dan Shif8eb0d12013-08-01 17:52:06 -0700166 @returns: True if all artifacts are staged.
Dan Shi6e50c722013-08-19 15:05:06 -0700167 @raise exception: that was raised by any artifact when calling Process.
Dan Shif8eb0d12013-08-01 17:52:06 -0700168
169 """
170 # Create factory to create build_artifacts from artifact names.
171 build = self.ParseUrl(self._archive_url)[1]
Chris Sosa6b0c6172013-08-05 17:01:33 -0700172 factory = build_artifact.ArtifactFactory(
173 self._build_dir, self._archive_url, artifacts, files, build)
Dan Shif8eb0d12013-08-01 17:52:06 -0700174 required_artifacts = factory.RequiredArtifacts()
Dan Shi6e50c722013-08-19 15:05:06 -0700175 exceptions = [artifact.GetException() for artifact in required_artifacts if
176 artifact.GetException()]
177 if exceptions:
178 raise DownloaderException(exceptions)
179
Dan Shif8eb0d12013-08-01 17:52:06 -0700180 return all([artifact.ArtifactStaged() for artifact in required_artifacts])
181
Chris Sosa76e44b92013-01-31 12:11:38 -0800182 def _DownloadArtifactsSerially(self, artifacts, no_wait):
183 """Simple function to download all the given artifacts serially.
184
Chris Sosa75490802013-09-30 17:21:45 -0700185 Args:
186 artifacts: A list of build_artifact.BuildArtifact instances to
187 download.
188 no_wait: If True, don't block waiting for artifact to exist if we
189 fail to immediately find it.
190
191 Raises:
192 build_artifact.ArtifactDownloadError: If we failed to download the
193 artifact.
Dan Shif8eb0d12013-08-01 17:52:06 -0700194
Gilad Arnold6f99b982012-09-12 10:49:40 -0700195 """
Dan Shi6e50c722013-08-19 15:05:06 -0700196 try:
197 for artifact in artifacts:
198 artifact.Process(no_wait)
Gilad Arnold02dc6552013-11-14 11:27:54 -0800199 except build_artifact.ArtifactDownloadError:
Dan Shi6e50c722013-08-19 15:05:06 -0700200 Downloader._TryRemoveStageDir(self._build_dir)
201 raise
Gilad Arnold6f99b982012-09-12 10:49:40 -0700202
Chris Sosa76e44b92013-01-31 12:11:38 -0800203 def _DownloadArtifactsInBackground(self, artifacts):
204 """Downloads |artifacts| in the background.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700205
Chris Sosa76e44b92013-01-31 12:11:38 -0800206 Downloads |artifacts| in the background. As these are backgrounded
207 artifacts, they are done best effort and may not exist.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700208
Chris Sosa76e44b92013-01-31 12:11:38 -0800209 Args:
210 artifacts: List of build_artifact.BuildArtifact instances to download.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700211 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800212 self._Log('Invoking background download of artifacts for %r', artifacts)
213 thread = threading.Thread(target=self._DownloadArtifactsSerially,
214 args=(artifacts, False))
215 thread.start()