blob: 17dadbab02ae61123e95d26a432be71ddf578109 [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'
Alex Millera44d5022012-07-27 11:34:16 -070028 # This filename must be kept in sync with clean_staged_images.py
29 _TIMESTAMP_FILENAME = 'staged.timestamp'
Chris Masonea22d9382012-05-18 12:38:51 -070030
Frank Farzan37761d12011-12-01 14:29:08 -080031 def __init__(self, static_dir):
32 self._static_dir = static_dir
Chris Sosa47a7d4e2012-03-28 11:26:55 -070033 self._build_dir = None
34 self._staging_dir = None
Chris Sosacde6bf42012-05-31 18:36:39 -070035 self._status_queue = multiprocessing.Queue(maxsize=1)
Chris Sosa47a7d4e2012-03-28 11:26:55 -070036 self._lock_tag = None
Chris Masone816e38c2012-05-02 12:22:36 -070037
38 @staticmethod
Chris Sosacde6bf42012-05-31 18:36:39 -070039 def ParseUrl(archive_url):
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -070040 """Parse archive_url into rel_path and short_build
41 e.g. gs://chromeos-image-archive/{rel_path}/{short_build}
Chris Masone816e38c2012-05-02 12:22:36 -070042
43 @param archive_url: a URL at which build artifacts are archived.
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -070044 @return a tuple of (build relative path, short build name)
Chris Masone816e38c2012-05-02 12:22:36 -070045 """
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -070046 # The archive_url is of the form gs://server/[some_path/target]/...]/build
47 # This function discards 'gs://server/' and extracts the [some_path/target]
48 # as rel_path and the build as short_build.
49 sub_url = archive_url.partition('://')[2]
50 split_sub_url = sub_url.split('/')
51 rel_path = '/'.join(split_sub_url[1:-1])
52 short_build = split_sub_url[-1]
53 return rel_path, short_build
Chris Masone816e38c2012-05-02 12:22:36 -070054
55 @staticmethod
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -070056 def GenerateLockTag(rel_path, short_build):
57 """Generate a name for a lock scoped to this rel_path/build pair.
Chris Masone816e38c2012-05-02 12:22:36 -070058
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -070059 @param rel_path: the relative path for the build.
Chris Masone816e38c2012-05-02 12:22:36 -070060 @param short_build: short build name
61 @return a name to use with AcquireLock that will scope the lock.
62 """
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -070063 return '/'.join([rel_path, short_build])
Frank Farzan37761d12011-12-01 14:29:08 -080064
Chris Sosa9164ca32012-03-28 11:04:50 -070065 @staticmethod
Alex Millera44d5022012-07-27 11:34:16 -070066 def _TouchTimestampForStaged(directory_path):
67 file_name = os.path.join(directory_path, Downloader._TIMESTAMP_FILENAME)
68 # Easiest python version of |touch file_name|
69 with file(file_name, 'a'):
70 os.utime(file_name, None)
71
72 @staticmethod
Chris Sosa9164ca32012-03-28 11:04:50 -070073 def BuildStaged(archive_url, static_dir):
74 """Returns True if the build is already staged."""
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -070075 rel_path, short_build = Downloader.ParseUrl(archive_url)
76 sub_directory = Downloader.GenerateLockTag(rel_path, short_build)
Alex Millera44d5022012-07-27 11:34:16 -070077 directory_path = os.path.join(static_dir, sub_directory)
78 exists = os.path.isdir(directory_path)
79 # If the build exists, then touch the timestamp to tell
80 # clean_stages_images.py that we're using this build.
81 if exists:
82 Downloader._TouchTimestampForStaged(directory_path)
83 return exists
Chris Sosa9164ca32012-03-28 11:04:50 -070084
Chris Sosa47a7d4e2012-03-28 11:26:55 -070085 def Download(self, archive_url, background=False):
86 """Downloads the given build artifacts defined by the |archive_url|.
87
88 If background is set to True, will return back early before all artifacts
89 have been downloaded. The artifacts that can be backgrounded are all those
90 that are not set as synchronous.
Chris Masone816e38c2012-05-02 12:22:36 -070091
92 TODO: refactor this into a common Download method, once unit tests are
93 fixed up to make iterating on the code easier.
Chris Sosa47a7d4e2012-03-28 11:26:55 -070094 """
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -070095 # Parse archive_url into rel_path (contains the build target) and
96 # short_build.
97 # e.g. gs://chromeos-image-archive/{rel_path}/{short_build}
98 rel_path, short_build = self.ParseUrl(archive_url)
Chris Sosacde6bf42012-05-31 18:36:39 -070099 # This should never happen. The Devserver should only try to call this
100 # method if no previous downloads have been staged for this archive_url.
101 assert not Downloader.BuildStaged(archive_url, self._static_dir)
Frank Farzan37761d12011-12-01 14:29:08 -0800102 # Bind build_dir and staging_dir here so we can tell if we need to do any
103 # cleanup after an exception occurs before build_dir is set.
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -0700104 self._lock_tag = self.GenerateLockTag(rel_path, short_build)
Frank Farzan37761d12011-12-01 14:29:08 -0800105 try:
106 # Create Dev Server directory for this build and tell other Downloader
107 # instances we have processed this build.
Chris Masone816e38c2012-05-02 12:22:36 -0700108 self._build_dir = devserver_util.AcquireLock(
109 static_dir=self._static_dir, tag=self._lock_tag)
Frank Farzan37761d12011-12-01 14:29:08 -0800110
Yu-Ju Hong1a83a712012-06-27 09:11:34 -0700111 # Replace '/' with '_' in rel_path because it may contain multiple levels
112 # which would not be qualified as part of the suffix.
113 self._staging_dir = tempfile.mkdtemp(suffix='_'.join(
Chris Sosaf0975642012-06-29 13:53:42 -0700114 [rel_path.replace('/', '_'), short_build]))
Alex Millera44d5022012-07-27 11:34:16 -0700115 Downloader._TouchTimestampForStaged(self._staging_dir)
Chris Masone816e38c2012-05-02 12:22:36 -0700116 cherrypy.log('Gathering download requirements %s' % archive_url,
Chris Masonea22d9382012-05-18 12:38:51 -0700117 self._LOG_TAG)
Chris Masone816e38c2012-05-02 12:22:36 -0700118 artifacts = self.GatherArtifactDownloads(
119 self._staging_dir, archive_url, short_build, self._build_dir)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700120 devserver_util.PrepareBuildDirectory(self._build_dir)
Frank Farzan37761d12011-12-01 14:29:08 -0800121
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700122 cherrypy.log('Downloading foreground artifacts from %s' % archive_url,
Chris Masonea22d9382012-05-18 12:38:51 -0700123 self._LOG_TAG)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700124 background_artifacts = []
125 for artifact in artifacts:
126 if artifact.Synchronous():
127 artifact.Download()
128 artifact.Stage()
129 else:
130 background_artifacts.append(artifact)
Frank Farzan37761d12011-12-01 14:29:08 -0800131
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700132 if background:
Chris Sosacde6bf42012-05-31 18:36:39 -0700133 self._DownloadArtifactsInBackground(background_artifacts)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700134 else:
135 self._DownloadArtifactsSerially(background_artifacts)
136
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700137 except Exception, e:
Frank Farzan37761d12011-12-01 14:29:08 -0800138 # Release processing lock, which will remove build components directory
139 # so future runs can retry.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700140 if self._build_dir:
141 devserver_util.ReleaseLock(static_dir=self._static_dir,
142 tag=self._lock_tag)
143
144 self._status_queue.put(e)
145 self._Cleanup()
Frank Farzan37761d12011-12-01 14:29:08 -0800146 raise
Frank Farzan37761d12011-12-01 14:29:08 -0800147
148 return 'Success'
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700149
150 def _Cleanup(self):
151 """Cleans up the staging dir for this downloader instanfce."""
152 if self._staging_dir:
153 cherrypy.log('Cleaning up staging directory %s' % self._staging_dir,
Chris Masonea22d9382012-05-18 12:38:51 -0700154 self._LOG_TAG)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700155 shutil.rmtree(self._staging_dir)
156
157 self._staging_dir = None
158
159 def _DownloadArtifactsSerially(self, artifacts):
160 """Simple function to download all the given artifacts serially."""
Chris Masonea22d9382012-05-18 12:38:51 -0700161 cherrypy.log('Downloading background artifacts serially.', self._LOG_TAG)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700162 try:
163 for artifact in artifacts:
164 artifact.Download()
165 artifact.Stage()
166 except Exception, e:
167 self._status_queue.put(e)
168
169 # Release processing lock, which will remove build components directory
170 # so future runs can retry.
171 if self._build_dir:
172 devserver_util.ReleaseLock(static_dir=self._static_dir,
173 tag=self._lock_tag)
174 else:
175 self._status_queue.put('Success')
176 finally:
177 self._Cleanup()
178
Chris Sosacde6bf42012-05-31 18:36:39 -0700179 def _DownloadArtifactsInBackground(self, artifacts):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700180 """Downloads |artifacts| in the background and signals when complete."""
181 proc = multiprocessing.Process(target=self._DownloadArtifactsSerially,
182 args=(artifacts,))
Chris Sosab65973e2012-03-29 18:31:02 -0700183 proc.start()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700184
Chris Masone816e38c2012-05-02 12:22:36 -0700185 def GatherArtifactDownloads(self, main_staging_dir, archive_url, short_build,
186 build_dir):
187 """Wrapper around devserver_util.GatherArtifactDownloads().
188
189 The wrapper allows mocking and overriding in derived classes.
190 """
191 return devserver_util.GatherArtifactDownloads(main_staging_dir, archive_url,
192 short_build, build_dir)
193
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700194 def GetStatusOfBackgroundDownloads(self):
195 """Returns the status of the background downloads.
196
197 This commands returns the status of the background downloads and blocks
198 until a status is returned.
199 """
200 status = self._status_queue.get()
201 # In case anyone else is calling.
202 self._status_queue.put(status)
Alex Miller92ed3592012-08-15 16:27:46 -0700203 # If someone is curious about the status of a build, then we should
204 # probably keep it around for a bit longer.
205 if os.path.exists(self._staging_dir):
206 Downloader._TouchTimestampForStaged(self._staging_dir)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700207 # It's possible we received an exception, if so, re-raise it here.
208 if isinstance(status, Exception):
209 raise status
210
211 return status
Chris Masone816e38c2012-05-02 12:22:36 -0700212
213
214class SymbolDownloader(Downloader):
215 """Download and stage debug symbols for a build on the devsever.
216
217 Given a URL to a build on the archive server:
218
219 - Determine if the build already exists.
220 - Download and extract the debug symbols to a staging directory.
221 - Install symbols to static dir.
222 """
223
224 _DONE_FLAG = 'done'
Chris Masonea22d9382012-05-18 12:38:51 -0700225 _LOG_TAG = 'SYMBOL_DOWNLOAD'
Chris Masone816e38c2012-05-02 12:22:36 -0700226
227 @staticmethod
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -0700228 def GenerateLockTag(rel_path, short_build):
229 return '/'.join([rel_path, short_build, 'symbols'])
Chris Masone816e38c2012-05-02 12:22:36 -0700230
Chris Sosacde6bf42012-05-31 18:36:39 -0700231 def Download(self, archive_url, _background=False):
Chris Masone816e38c2012-05-02 12:22:36 -0700232 """Downloads debug symbols for the build defined by the |archive_url|.
233
234 The symbols will be downloaded synchronously
235 """
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -0700236 # Parse archive_url into rel_path (contains the build target) and
237 # short_build.
238 # e.g. gs://chromeos-image-archive/{rel_path}/{short_build}
239 rel_path, short_build = self.ParseUrl(archive_url)
Chris Masone816e38c2012-05-02 12:22:36 -0700240
241 # Bind build_dir and staging_dir here so we can tell if we need to do any
242 # cleanup after an exception occurs before build_dir is set.
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -0700243 self._lock_tag = self.GenerateLockTag(rel_path, short_build)
Chris Masone816e38c2012-05-02 12:22:36 -0700244 if self.SymbolsStaged(archive_url, self._static_dir):
245 cherrypy.log(
246 'Symbols for build %s have already been staged.' % self._lock_tag,
Chris Masonea22d9382012-05-18 12:38:51 -0700247 self._LOG_TAG)
Chris Masone816e38c2012-05-02 12:22:36 -0700248 return 'Success'
249
250 try:
251 # Create Dev Server directory for this build and tell other Downloader
252 # instances we have processed this build.
253 self._build_dir = devserver_util.AcquireLock(
254 static_dir=self._static_dir, tag=self._lock_tag)
255
Yu-Ju Hong1a83a712012-06-27 09:11:34 -0700256 # Replace '/' with '_' in rel_path because it may contain multiple levels
257 # which would not be qualified as part of the suffix.
258 self._staging_dir = tempfile.mkdtemp(suffix='_'.join(
Chris Sosaf0975642012-06-29 13:53:42 -0700259 [rel_path.replace('/', '_'), short_build]))
Chris Masone816e38c2012-05-02 12:22:36 -0700260 cherrypy.log('Downloading debug symbols from %s' % archive_url,
Chris Masonea22d9382012-05-18 12:38:51 -0700261 self._LOG_TAG)
Chris Masone816e38c2012-05-02 12:22:36 -0700262
263 [symbol_artifact] = self.GatherArtifactDownloads(
264 self._staging_dir, archive_url, '', self._static_dir)
265 symbol_artifact.Download()
266 symbol_artifact.Stage()
Chris Masonea22d9382012-05-18 12:38:51 -0700267 self.MarkSymbolsStaged()
Chris Masone816e38c2012-05-02 12:22:36 -0700268
269 except Exception:
270 # Release processing "lock", which will indicate to future runs that we
271 # did not succeed, and so they should try again.
272 if self._build_dir:
273 devserver_util.ReleaseLock(static_dir=self._static_dir,
274 tag=self._lock_tag)
Chris Masone816e38c2012-05-02 12:22:36 -0700275 raise
Chris Masonea22d9382012-05-18 12:38:51 -0700276 finally:
277 self._Cleanup()
Chris Sosa4d9c4d42012-06-29 15:23:23 -0700278
Chris Masone816e38c2012-05-02 12:22:36 -0700279 return 'Success'
280
281 def GatherArtifactDownloads(self, temp_download_dir, archive_url, short_build,
282 static_dir):
283 """Call SymbolDownloader-appropriate artifact gathering method.
284
285 @param temp_download_dir: the tempdir into which we're downloading artifacts
286 prior to staging them.
287 @param archive_url: the google storage url of the bucket where the debug
288 symbols for the desired build are stored.
289 @param short_build: IGNORED
290 @param staging_dir: the dir into which to stage the symbols
291
292 @return an iterable of one DebugTarball pointing to the right debug symbols.
293 This is an iterable so that it's similar to GatherArtifactDownloads.
294 Also, it's possible that someday we might have more than one.
295 """
296 return devserver_util.GatherSymbolArtifactDownloads(temp_download_dir,
297 archive_url,
298 static_dir)
299
300 def MarkSymbolsStaged(self):
301 """Puts a flag file on disk to signal that symbols are staged."""
302 with open(os.path.join(self._build_dir, self._DONE_FLAG), 'w') as flag:
303 flag.write(self._DONE_FLAG)
304
305 def SymbolsStaged(self, archive_url, static_dir):
306 """Returns True if the build is already staged."""
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -0700307 rel_path, short_build = self.ParseUrl(archive_url)
308 sub_directory = self.GenerateLockTag(rel_path, short_build)
Chris Masone816e38c2012-05-02 12:22:36 -0700309 return os.path.isfile(os.path.join(static_dir,
310 sub_directory,
311 self._DONE_FLAG))