blob: 710a4255256f19607fa61cad8e90b77840eff6ac [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
Gilad Arnold0b8c3f32012-09-19 14:35:44 -07007import Queue
Frank Farzan37761d12011-12-01 14:29:08 -08008import cherrypy
Chris Sosa9164ca32012-03-28 11:04:50 -07009import os
Frank Farzan37761d12011-12-01 14:29:08 -080010import shutil
11import tempfile
Gilad Arnold0b8c3f32012-09-19 14:35:44 -070012import threading
Frank Farzan37761d12011-12-01 14:29:08 -080013
14import devserver_util
15
16
17class Downloader(object):
18 """Download images to the devsever.
19
20 Given a URL to a build on the archive server:
21
22 - Determine if the build already exists.
23 - Download and extract the build to a staging directory.
24 - Package autotest tests.
25 - Install components to static dir.
26 """
27
Chris Masonea22d9382012-05-18 12:38:51 -070028 _LOG_TAG = 'DOWNLOAD'
Alex Millera44d5022012-07-27 11:34:16 -070029 # This filename must be kept in sync with clean_staged_images.py
30 _TIMESTAMP_FILENAME = 'staged.timestamp'
Chris Masonea22d9382012-05-18 12:38:51 -070031
Frank Farzan37761d12011-12-01 14:29:08 -080032 def __init__(self, static_dir):
33 self._static_dir = static_dir
Chris Sosa47a7d4e2012-03-28 11:26:55 -070034 self._build_dir = None
35 self._staging_dir = None
Gilad Arnold0b8c3f32012-09-19 14:35:44 -070036 self._status_queue = Queue.Queue(maxsize=1)
Chris Sosa47a7d4e2012-03-28 11:26:55 -070037 self._lock_tag = None
Chris Masone816e38c2012-05-02 12:22:36 -070038
39 @staticmethod
Chris Sosacde6bf42012-05-31 18:36:39 -070040 def ParseUrl(archive_url):
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -070041 """Parse archive_url into rel_path and short_build
42 e.g. gs://chromeos-image-archive/{rel_path}/{short_build}
Chris Masone816e38c2012-05-02 12:22:36 -070043
44 @param archive_url: a URL at which build artifacts are archived.
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -070045 @return a tuple of (build relative path, short build name)
Chris Masone816e38c2012-05-02 12:22:36 -070046 """
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -070047 # The archive_url is of the form gs://server/[some_path/target]/...]/build
48 # This function discards 'gs://server/' and extracts the [some_path/target]
49 # as rel_path and the build as short_build.
50 sub_url = archive_url.partition('://')[2]
51 split_sub_url = sub_url.split('/')
52 rel_path = '/'.join(split_sub_url[1:-1])
53 short_build = split_sub_url[-1]
54 return rel_path, short_build
Chris Masone816e38c2012-05-02 12:22:36 -070055
56 @staticmethod
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -070057 def GenerateLockTag(rel_path, short_build):
58 """Generate a name for a lock scoped to this rel_path/build pair.
Chris Masone816e38c2012-05-02 12:22:36 -070059
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -070060 @param rel_path: the relative path for the build.
Chris Masone816e38c2012-05-02 12:22:36 -070061 @param short_build: short build name
62 @return a name to use with AcquireLock that will scope the lock.
63 """
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -070064 return '/'.join([rel_path, short_build])
Frank Farzan37761d12011-12-01 14:29:08 -080065
Chris Sosa9164ca32012-03-28 11:04:50 -070066 @staticmethod
Alex Millera44d5022012-07-27 11:34:16 -070067 def _TouchTimestampForStaged(directory_path):
68 file_name = os.path.join(directory_path, Downloader._TIMESTAMP_FILENAME)
69 # Easiest python version of |touch file_name|
70 with file(file_name, 'a'):
71 os.utime(file_name, None)
72
73 @staticmethod
Chris Sosa9164ca32012-03-28 11:04:50 -070074 def BuildStaged(archive_url, static_dir):
75 """Returns True if the build is already staged."""
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -070076 rel_path, short_build = Downloader.ParseUrl(archive_url)
77 sub_directory = Downloader.GenerateLockTag(rel_path, short_build)
Alex Millera44d5022012-07-27 11:34:16 -070078 directory_path = os.path.join(static_dir, sub_directory)
79 exists = os.path.isdir(directory_path)
80 # If the build exists, then touch the timestamp to tell
81 # clean_stages_images.py that we're using this build.
82 if exists:
83 Downloader._TouchTimestampForStaged(directory_path)
84 return exists
Chris Sosa9164ca32012-03-28 11:04:50 -070085
Chris Sosa47a7d4e2012-03-28 11:26:55 -070086 def Download(self, archive_url, background=False):
87 """Downloads the given build artifacts defined by the |archive_url|.
88
89 If background is set to True, will return back early before all artifacts
90 have been downloaded. The artifacts that can be backgrounded are all those
91 that are not set as synchronous.
Chris Masone816e38c2012-05-02 12:22:36 -070092
93 TODO: refactor this into a common Download method, once unit tests are
94 fixed up to make iterating on the code easier.
Chris Sosa47a7d4e2012-03-28 11:26:55 -070095 """
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -070096 # Parse archive_url into rel_path (contains the build target) and
97 # short_build.
98 # e.g. gs://chromeos-image-archive/{rel_path}/{short_build}
99 rel_path, short_build = self.ParseUrl(archive_url)
Chris Sosacde6bf42012-05-31 18:36:39 -0700100 # This should never happen. The Devserver should only try to call this
101 # method if no previous downloads have been staged for this archive_url.
102 assert not Downloader.BuildStaged(archive_url, self._static_dir)
Frank Farzan37761d12011-12-01 14:29:08 -0800103 # Bind build_dir and staging_dir here so we can tell if we need to do any
104 # cleanup after an exception occurs before build_dir is set.
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -0700105 self._lock_tag = self.GenerateLockTag(rel_path, short_build)
Frank Farzan37761d12011-12-01 14:29:08 -0800106 try:
107 # Create Dev Server directory for this build and tell other Downloader
108 # instances we have processed this build.
Chris Masone816e38c2012-05-02 12:22:36 -0700109 self._build_dir = devserver_util.AcquireLock(
110 static_dir=self._static_dir, tag=self._lock_tag)
Frank Farzan37761d12011-12-01 14:29:08 -0800111
Yu-Ju Hong1a83a712012-06-27 09:11:34 -0700112 # Replace '/' with '_' in rel_path because it may contain multiple levels
113 # which would not be qualified as part of the suffix.
114 self._staging_dir = tempfile.mkdtemp(suffix='_'.join(
Chris Sosaf0975642012-06-29 13:53:42 -0700115 [rel_path.replace('/', '_'), short_build]))
Alex Millera44d5022012-07-27 11:34:16 -0700116 Downloader._TouchTimestampForStaged(self._staging_dir)
Chris Masone816e38c2012-05-02 12:22:36 -0700117 cherrypy.log('Gathering download requirements %s' % archive_url,
Chris Masonea22d9382012-05-18 12:38:51 -0700118 self._LOG_TAG)
Chris Masone816e38c2012-05-02 12:22:36 -0700119 artifacts = self.GatherArtifactDownloads(
120 self._staging_dir, archive_url, short_build, self._build_dir)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700121 devserver_util.PrepareBuildDirectory(self._build_dir)
Frank Farzan37761d12011-12-01 14:29:08 -0800122
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700123 cherrypy.log('Downloading foreground artifacts from %s' % archive_url,
Chris Masonea22d9382012-05-18 12:38:51 -0700124 self._LOG_TAG)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700125 background_artifacts = []
126 for artifact in artifacts:
127 if artifact.Synchronous():
128 artifact.Download()
129 artifact.Stage()
130 else:
131 background_artifacts.append(artifact)
Frank Farzan37761d12011-12-01 14:29:08 -0800132
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700133 if background:
Chris Sosacde6bf42012-05-31 18:36:39 -0700134 self._DownloadArtifactsInBackground(background_artifacts)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700135 else:
136 self._DownloadArtifactsSerially(background_artifacts)
137
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700138 except Exception, e:
Frank Farzan37761d12011-12-01 14:29:08 -0800139 # Release processing lock, which will remove build components directory
140 # so future runs can retry.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700141 if self._build_dir:
142 devserver_util.ReleaseLock(static_dir=self._static_dir,
143 tag=self._lock_tag)
144
145 self._status_queue.put(e)
146 self._Cleanup()
Frank Farzan37761d12011-12-01 14:29:08 -0800147 raise
Frank Farzan37761d12011-12-01 14:29:08 -0800148
149 return 'Success'
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700150
151 def _Cleanup(self):
152 """Cleans up the staging dir for this downloader instanfce."""
153 if self._staging_dir:
154 cherrypy.log('Cleaning up staging directory %s' % self._staging_dir,
Chris Masonea22d9382012-05-18 12:38:51 -0700155 self._LOG_TAG)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700156 shutil.rmtree(self._staging_dir)
157
158 self._staging_dir = None
159
160 def _DownloadArtifactsSerially(self, artifacts):
161 """Simple function to download all the given artifacts serially."""
Gilad Arnold0b8c3f32012-09-19 14:35:44 -0700162 cherrypy.log('Downloading artifacts serially.', self._LOG_TAG)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700163 try:
164 for artifact in artifacts:
165 artifact.Download()
166 artifact.Stage()
167 except Exception, e:
168 self._status_queue.put(e)
169
170 # Release processing lock, which will remove build components directory
171 # so future runs can retry.
172 if self._build_dir:
173 devserver_util.ReleaseLock(static_dir=self._static_dir,
174 tag=self._lock_tag)
175 else:
176 self._status_queue.put('Success')
177 finally:
178 self._Cleanup()
179
Chris Sosacde6bf42012-05-31 18:36:39 -0700180 def _DownloadArtifactsInBackground(self, artifacts):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700181 """Downloads |artifacts| in the background and signals when complete."""
Gilad Arnold0b8c3f32012-09-19 14:35:44 -0700182 cherrypy.log('Invoking background download of artifacts', self._LOG_TAG)
183 thread = threading.Thread(target=self._DownloadArtifactsSerially,
184 args=(artifacts,))
185 thread.start()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700186
Chris Masone816e38c2012-05-02 12:22:36 -0700187 def GatherArtifactDownloads(self, main_staging_dir, archive_url, short_build,
188 build_dir):
189 """Wrapper around devserver_util.GatherArtifactDownloads().
190
191 The wrapper allows mocking and overriding in derived classes.
192 """
193 return devserver_util.GatherArtifactDownloads(main_staging_dir, archive_url,
194 short_build, build_dir)
195
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700196 def GetStatusOfBackgroundDownloads(self):
197 """Returns the status of the background downloads.
198
199 This commands returns the status of the background downloads and blocks
200 until a status is returned.
201 """
202 status = self._status_queue.get()
203 # In case anyone else is calling.
204 self._status_queue.put(status)
Alex Miller92ed3592012-08-15 16:27:46 -0700205 # If someone is curious about the status of a build, then we should
206 # probably keep it around for a bit longer.
207 if os.path.exists(self._staging_dir):
208 Downloader._TouchTimestampForStaged(self._staging_dir)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700209 # It's possible we received an exception, if so, re-raise it here.
210 if isinstance(status, Exception):
211 raise status
212
213 return status
Chris Masone816e38c2012-05-02 12:22:36 -0700214
215
216class SymbolDownloader(Downloader):
217 """Download and stage debug symbols for a build on the devsever.
218
219 Given a URL to a build on the archive server:
220
221 - Determine if the build already exists.
222 - Download and extract the debug symbols to a staging directory.
223 - Install symbols to static dir.
224 """
225
226 _DONE_FLAG = 'done'
Chris Masonea22d9382012-05-18 12:38:51 -0700227 _LOG_TAG = 'SYMBOL_DOWNLOAD'
Chris Masone816e38c2012-05-02 12:22:36 -0700228
229 @staticmethod
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -0700230 def GenerateLockTag(rel_path, short_build):
231 return '/'.join([rel_path, short_build, 'symbols'])
Chris Masone816e38c2012-05-02 12:22:36 -0700232
Chris Sosacde6bf42012-05-31 18:36:39 -0700233 def Download(self, archive_url, _background=False):
Chris Masone816e38c2012-05-02 12:22:36 -0700234 """Downloads debug symbols for the build defined by the |archive_url|.
235
236 The symbols will be downloaded synchronously
237 """
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -0700238 # Parse archive_url into rel_path (contains the build target) and
239 # short_build.
240 # e.g. gs://chromeos-image-archive/{rel_path}/{short_build}
241 rel_path, short_build = self.ParseUrl(archive_url)
Chris Masone816e38c2012-05-02 12:22:36 -0700242
243 # Bind build_dir and staging_dir here so we can tell if we need to do any
244 # cleanup after an exception occurs before build_dir is set.
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -0700245 self._lock_tag = self.GenerateLockTag(rel_path, short_build)
Chris Masone816e38c2012-05-02 12:22:36 -0700246 if self.SymbolsStaged(archive_url, self._static_dir):
247 cherrypy.log(
248 'Symbols for build %s have already been staged.' % self._lock_tag,
Chris Masonea22d9382012-05-18 12:38:51 -0700249 self._LOG_TAG)
Chris Masone816e38c2012-05-02 12:22:36 -0700250 return 'Success'
251
252 try:
253 # Create Dev Server directory for this build and tell other Downloader
254 # instances we have processed this build.
255 self._build_dir = devserver_util.AcquireLock(
256 static_dir=self._static_dir, tag=self._lock_tag)
257
Yu-Ju Hong1a83a712012-06-27 09:11:34 -0700258 # Replace '/' with '_' in rel_path because it may contain multiple levels
259 # which would not be qualified as part of the suffix.
260 self._staging_dir = tempfile.mkdtemp(suffix='_'.join(
Chris Sosaf0975642012-06-29 13:53:42 -0700261 [rel_path.replace('/', '_'), short_build]))
Chris Masone816e38c2012-05-02 12:22:36 -0700262 cherrypy.log('Downloading debug symbols from %s' % archive_url,
Chris Masonea22d9382012-05-18 12:38:51 -0700263 self._LOG_TAG)
Chris Masone816e38c2012-05-02 12:22:36 -0700264
265 [symbol_artifact] = self.GatherArtifactDownloads(
266 self._staging_dir, archive_url, '', self._static_dir)
267 symbol_artifact.Download()
268 symbol_artifact.Stage()
Chris Masonea22d9382012-05-18 12:38:51 -0700269 self.MarkSymbolsStaged()
Chris Masone816e38c2012-05-02 12:22:36 -0700270
271 except Exception:
272 # Release processing "lock", which will indicate to future runs that we
273 # did not succeed, and so they should try again.
274 if self._build_dir:
275 devserver_util.ReleaseLock(static_dir=self._static_dir,
276 tag=self._lock_tag)
Chris Masone816e38c2012-05-02 12:22:36 -0700277 raise
Chris Masonea22d9382012-05-18 12:38:51 -0700278 finally:
279 self._Cleanup()
Chris Sosa4d9c4d42012-06-29 15:23:23 -0700280
Chris Masone816e38c2012-05-02 12:22:36 -0700281 return 'Success'
282
283 def GatherArtifactDownloads(self, temp_download_dir, archive_url, short_build,
284 static_dir):
285 """Call SymbolDownloader-appropriate artifact gathering method.
286
287 @param temp_download_dir: the tempdir into which we're downloading artifacts
288 prior to staging them.
289 @param archive_url: the google storage url of the bucket where the debug
290 symbols for the desired build are stored.
291 @param short_build: IGNORED
292 @param staging_dir: the dir into which to stage the symbols
293
294 @return an iterable of one DebugTarball pointing to the right debug symbols.
295 This is an iterable so that it's similar to GatherArtifactDownloads.
296 Also, it's possible that someday we might have more than one.
297 """
298 return devserver_util.GatherSymbolArtifactDownloads(temp_download_dir,
299 archive_url,
300 static_dir)
301
302 def MarkSymbolsStaged(self):
303 """Puts a flag file on disk to signal that symbols are staged."""
304 with open(os.path.join(self._build_dir, self._DONE_FLAG), 'w') as flag:
305 flag.write(self._DONE_FLAG)
306
307 def SymbolsStaged(self, archive_url, static_dir):
308 """Returns True if the build is already staged."""
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -0700309 rel_path, short_build = self.ParseUrl(archive_url)
310 sub_directory = self.GenerateLockTag(rel_path, short_build)
Chris Masone816e38c2012-05-02 12:22:36 -0700311 return os.path.isfile(os.path.join(static_dir,
312 sub_directory,
313 self._DONE_FLAG))