blob: beb57c21b9c188ceaae339dc934150db78bd3000 [file] [log] [blame]
Frank Farzan37761d12011-12-01 14:29:08 -08001#!/usr/bin/python
2#
Chris Sosa47a7d4e2012-03-28 11:26:55 -07003# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
Frank Farzan37761d12011-12-01 14:29:08 -08004# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7import cherrypy
Chris Sosa47a7d4e2012-03-28 11:26:55 -07008import multiprocessing
Chris Sosa9164ca32012-03-28 11:04:50 -07009import os
Frank Farzan37761d12011-12-01 14:29:08 -080010import shutil
11import tempfile
12
13import devserver_util
14
15
16class Downloader(object):
17 """Download images to the devsever.
18
19 Given a URL to a build on the archive server:
20
21 - Determine if the build already exists.
22 - Download and extract the build to a staging directory.
23 - Package autotest tests.
24 - Install components to static dir.
25 """
26
Chris Masonea22d9382012-05-18 12:38:51 -070027 _LOG_TAG = 'DOWNLOAD'
28
Frank Farzan37761d12011-12-01 14:29:08 -080029 def __init__(self, static_dir):
30 self._static_dir = static_dir
Chris Sosa47a7d4e2012-03-28 11:26:55 -070031 self._build_dir = None
32 self._staging_dir = None
Chris Sosacde6bf42012-05-31 18:36:39 -070033 self._status_queue = multiprocessing.Queue(maxsize=1)
Chris Sosa47a7d4e2012-03-28 11:26:55 -070034 self._lock_tag = None
Chris Masone816e38c2012-05-02 12:22:36 -070035
36 @staticmethod
Chris Sosacde6bf42012-05-31 18:36:39 -070037 def ParseUrl(archive_url):
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -070038 """Parse archive_url into rel_path and short_build
39 e.g. gs://chromeos-image-archive/{rel_path}/{short_build}
Chris Masone816e38c2012-05-02 12:22:36 -070040
41 @param archive_url: a URL at which build artifacts are archived.
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -070042 @return a tuple of (build relative path, short build name)
Chris Masone816e38c2012-05-02 12:22:36 -070043 """
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -070044 # The archive_url is of the form gs://server/[some_path/target]/...]/build
45 # This function discards 'gs://server/' and extracts the [some_path/target]
46 # as rel_path and the build as short_build.
47 sub_url = archive_url.partition('://')[2]
48 split_sub_url = sub_url.split('/')
49 rel_path = '/'.join(split_sub_url[1:-1])
50 short_build = split_sub_url[-1]
51 return rel_path, short_build
Chris Masone816e38c2012-05-02 12:22:36 -070052
53 @staticmethod
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -070054 def GenerateLockTag(rel_path, short_build):
55 """Generate a name for a lock scoped to this rel_path/build pair.
Chris Masone816e38c2012-05-02 12:22:36 -070056
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -070057 @param rel_path: the relative path for the build.
Chris Masone816e38c2012-05-02 12:22:36 -070058 @param short_build: short build name
59 @return a name to use with AcquireLock that will scope the lock.
60 """
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -070061 return '/'.join([rel_path, short_build])
Frank Farzan37761d12011-12-01 14:29:08 -080062
Chris Sosa9164ca32012-03-28 11:04:50 -070063 @staticmethod
64 def BuildStaged(archive_url, static_dir):
65 """Returns True if the build is already staged."""
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -070066 rel_path, short_build = Downloader.ParseUrl(archive_url)
67 sub_directory = Downloader.GenerateLockTag(rel_path, short_build)
Chris Sosa9164ca32012-03-28 11:04:50 -070068 return os.path.isdir(os.path.join(static_dir, sub_directory))
69
Chris Sosa47a7d4e2012-03-28 11:26:55 -070070 def Download(self, archive_url, background=False):
71 """Downloads the given build artifacts defined by the |archive_url|.
72
73 If background is set to True, will return back early before all artifacts
74 have been downloaded. The artifacts that can be backgrounded are all those
75 that are not set as synchronous.
Chris Masone816e38c2012-05-02 12:22:36 -070076
77 TODO: refactor this into a common Download method, once unit tests are
78 fixed up to make iterating on the code easier.
Chris Sosa47a7d4e2012-03-28 11:26:55 -070079 """
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -070080 # Parse archive_url into rel_path (contains the build target) and
81 # short_build.
82 # e.g. gs://chromeos-image-archive/{rel_path}/{short_build}
83 rel_path, short_build = self.ParseUrl(archive_url)
Chris Sosacde6bf42012-05-31 18:36:39 -070084 # This should never happen. The Devserver should only try to call this
85 # method if no previous downloads have been staged for this archive_url.
86 assert not Downloader.BuildStaged(archive_url, self._static_dir)
Frank Farzan37761d12011-12-01 14:29:08 -080087 # Bind build_dir and staging_dir here so we can tell if we need to do any
88 # cleanup after an exception occurs before build_dir is set.
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -070089 self._lock_tag = self.GenerateLockTag(rel_path, short_build)
Frank Farzan37761d12011-12-01 14:29:08 -080090 try:
91 # Create Dev Server directory for this build and tell other Downloader
92 # instances we have processed this build.
Chris Masone816e38c2012-05-02 12:22:36 -070093 self._build_dir = devserver_util.AcquireLock(
94 static_dir=self._static_dir, tag=self._lock_tag)
Frank Farzan37761d12011-12-01 14:29:08 -080095
Yu-Ju Hong1a83a712012-06-27 09:11:34 -070096 # Replace '/' with '_' in rel_path because it may contain multiple levels
97 # which would not be qualified as part of the suffix.
98 self._staging_dir = tempfile.mkdtemp(suffix='_'.join(
Chris Sosaf0975642012-06-29 13:53:42 -070099 [rel_path.replace('/', '_'), short_build]))
Chris Masone816e38c2012-05-02 12:22:36 -0700100 cherrypy.log('Gathering download requirements %s' % archive_url,
Chris Masonea22d9382012-05-18 12:38:51 -0700101 self._LOG_TAG)
Chris Masone816e38c2012-05-02 12:22:36 -0700102 artifacts = self.GatherArtifactDownloads(
103 self._staging_dir, archive_url, short_build, self._build_dir)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700104 devserver_util.PrepareBuildDirectory(self._build_dir)
Frank Farzan37761d12011-12-01 14:29:08 -0800105
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700106 cherrypy.log('Downloading foreground artifacts from %s' % archive_url,
Chris Masonea22d9382012-05-18 12:38:51 -0700107 self._LOG_TAG)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700108 background_artifacts = []
109 for artifact in artifacts:
110 if artifact.Synchronous():
111 artifact.Download()
112 artifact.Stage()
113 else:
114 background_artifacts.append(artifact)
Frank Farzan37761d12011-12-01 14:29:08 -0800115
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700116 if background:
Chris Sosacde6bf42012-05-31 18:36:39 -0700117 self._DownloadArtifactsInBackground(background_artifacts)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700118 else:
119 self._DownloadArtifactsSerially(background_artifacts)
120
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700121 except Exception, e:
Frank Farzan37761d12011-12-01 14:29:08 -0800122 # Release processing lock, which will remove build components directory
123 # so future runs can retry.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700124 if self._build_dir:
125 devserver_util.ReleaseLock(static_dir=self._static_dir,
126 tag=self._lock_tag)
127
128 self._status_queue.put(e)
129 self._Cleanup()
Frank Farzan37761d12011-12-01 14:29:08 -0800130 raise
Frank Farzan37761d12011-12-01 14:29:08 -0800131
132 return 'Success'
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700133
134 def _Cleanup(self):
135 """Cleans up the staging dir for this downloader instanfce."""
136 if self._staging_dir:
137 cherrypy.log('Cleaning up staging directory %s' % self._staging_dir,
Chris Masonea22d9382012-05-18 12:38:51 -0700138 self._LOG_TAG)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700139 shutil.rmtree(self._staging_dir)
140
141 self._staging_dir = None
142
143 def _DownloadArtifactsSerially(self, artifacts):
144 """Simple function to download all the given artifacts serially."""
Chris Masonea22d9382012-05-18 12:38:51 -0700145 cherrypy.log('Downloading background artifacts serially.', self._LOG_TAG)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700146 try:
147 for artifact in artifacts:
148 artifact.Download()
149 artifact.Stage()
150 except Exception, e:
151 self._status_queue.put(e)
152
153 # Release processing lock, which will remove build components directory
154 # so future runs can retry.
155 if self._build_dir:
156 devserver_util.ReleaseLock(static_dir=self._static_dir,
157 tag=self._lock_tag)
158 else:
159 self._status_queue.put('Success')
160 finally:
161 self._Cleanup()
162
Chris Sosacde6bf42012-05-31 18:36:39 -0700163 def _DownloadArtifactsInBackground(self, artifacts):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700164 """Downloads |artifacts| in the background and signals when complete."""
165 proc = multiprocessing.Process(target=self._DownloadArtifactsSerially,
166 args=(artifacts,))
Chris Sosab65973e2012-03-29 18:31:02 -0700167 proc.start()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700168
Chris Masone816e38c2012-05-02 12:22:36 -0700169 def GatherArtifactDownloads(self, main_staging_dir, archive_url, short_build,
170 build_dir):
171 """Wrapper around devserver_util.GatherArtifactDownloads().
172
173 The wrapper allows mocking and overriding in derived classes.
174 """
175 return devserver_util.GatherArtifactDownloads(main_staging_dir, archive_url,
176 short_build, build_dir)
177
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700178 def GetStatusOfBackgroundDownloads(self):
179 """Returns the status of the background downloads.
180
181 This commands returns the status of the background downloads and blocks
182 until a status is returned.
183 """
184 status = self._status_queue.get()
185 # In case anyone else is calling.
186 self._status_queue.put(status)
187 # It's possible we received an exception, if so, re-raise it here.
188 if isinstance(status, Exception):
189 raise status
190
191 return status
Chris Masone816e38c2012-05-02 12:22:36 -0700192
193
194class SymbolDownloader(Downloader):
195 """Download and stage debug symbols for a build on the devsever.
196
197 Given a URL to a build on the archive server:
198
199 - Determine if the build already exists.
200 - Download and extract the debug symbols to a staging directory.
201 - Install symbols to static dir.
202 """
203
204 _DONE_FLAG = 'done'
Chris Masonea22d9382012-05-18 12:38:51 -0700205 _LOG_TAG = 'SYMBOL_DOWNLOAD'
Chris Masone816e38c2012-05-02 12:22:36 -0700206
207 @staticmethod
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -0700208 def GenerateLockTag(rel_path, short_build):
209 return '/'.join([rel_path, short_build, 'symbols'])
Chris Masone816e38c2012-05-02 12:22:36 -0700210
Chris Sosacde6bf42012-05-31 18:36:39 -0700211 def Download(self, archive_url, _background=False):
Chris Masone816e38c2012-05-02 12:22:36 -0700212 """Downloads debug symbols for the build defined by the |archive_url|.
213
214 The symbols will be downloaded synchronously
215 """
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -0700216 # Parse archive_url into rel_path (contains the build target) and
217 # short_build.
218 # e.g. gs://chromeos-image-archive/{rel_path}/{short_build}
219 rel_path, short_build = self.ParseUrl(archive_url)
Chris Masone816e38c2012-05-02 12:22:36 -0700220
221 # Bind build_dir and staging_dir here so we can tell if we need to do any
222 # cleanup after an exception occurs before build_dir is set.
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -0700223 self._lock_tag = self.GenerateLockTag(rel_path, short_build)
Chris Masone816e38c2012-05-02 12:22:36 -0700224 if self.SymbolsStaged(archive_url, self._static_dir):
225 cherrypy.log(
226 'Symbols for build %s have already been staged.' % self._lock_tag,
Chris Masonea22d9382012-05-18 12:38:51 -0700227 self._LOG_TAG)
Chris Masone816e38c2012-05-02 12:22:36 -0700228 return 'Success'
229
230 try:
231 # Create Dev Server directory for this build and tell other Downloader
232 # instances we have processed this build.
233 self._build_dir = devserver_util.AcquireLock(
234 static_dir=self._static_dir, tag=self._lock_tag)
235
Yu-Ju Hong1a83a712012-06-27 09:11:34 -0700236 # Replace '/' with '_' in rel_path because it may contain multiple levels
237 # which would not be qualified as part of the suffix.
238 self._staging_dir = tempfile.mkdtemp(suffix='_'.join(
Chris Sosaf0975642012-06-29 13:53:42 -0700239 [rel_path.replace('/', '_'), short_build]))
Chris Masone816e38c2012-05-02 12:22:36 -0700240 cherrypy.log('Downloading debug symbols from %s' % archive_url,
Chris Masonea22d9382012-05-18 12:38:51 -0700241 self._LOG_TAG)
Chris Masone816e38c2012-05-02 12:22:36 -0700242
243 [symbol_artifact] = self.GatherArtifactDownloads(
244 self._staging_dir, archive_url, '', self._static_dir)
245 symbol_artifact.Download()
246 symbol_artifact.Stage()
Chris Masonea22d9382012-05-18 12:38:51 -0700247 self.MarkSymbolsStaged()
Chris Masone816e38c2012-05-02 12:22:36 -0700248
249 except Exception:
250 # Release processing "lock", which will indicate to future runs that we
251 # did not succeed, and so they should try again.
252 if self._build_dir:
253 devserver_util.ReleaseLock(static_dir=self._static_dir,
254 tag=self._lock_tag)
Chris Masone816e38c2012-05-02 12:22:36 -0700255 raise
Chris Masonea22d9382012-05-18 12:38:51 -0700256 finally:
257 self._Cleanup()
Chris Masone816e38c2012-05-02 12:22:36 -0700258 return 'Success'
259
260 def GatherArtifactDownloads(self, temp_download_dir, archive_url, short_build,
261 static_dir):
262 """Call SymbolDownloader-appropriate artifact gathering method.
263
264 @param temp_download_dir: the tempdir into which we're downloading artifacts
265 prior to staging them.
266 @param archive_url: the google storage url of the bucket where the debug
267 symbols for the desired build are stored.
268 @param short_build: IGNORED
269 @param staging_dir: the dir into which to stage the symbols
270
271 @return an iterable of one DebugTarball pointing to the right debug symbols.
272 This is an iterable so that it's similar to GatherArtifactDownloads.
273 Also, it's possible that someday we might have more than one.
274 """
275 return devserver_util.GatherSymbolArtifactDownloads(temp_download_dir,
276 archive_url,
277 static_dir)
278
279 def MarkSymbolsStaged(self):
280 """Puts a flag file on disk to signal that symbols are staged."""
281 with open(os.path.join(self._build_dir, self._DONE_FLAG), 'w') as flag:
282 flag.write(self._DONE_FLAG)
283
284 def SymbolsStaged(self, archive_url, static_dir):
285 """Returns True if the build is already staged."""
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -0700286 rel_path, short_build = self.ParseUrl(archive_url)
287 sub_directory = self.GenerateLockTag(rel_path, short_build)
Chris Masone816e38c2012-05-02 12:22:36 -0700288 return os.path.isfile(os.path.join(static_dir,
289 sub_directory,
290 self._DONE_FLAG))