blob: dc22ad5fe79c91c85953534a0cc63728cdda7b8d [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
27 def __init__(self, static_dir):
28 self._static_dir = static_dir
Chris Sosa47a7d4e2012-03-28 11:26:55 -070029 self._build_dir = None
30 self._staging_dir = None
31 self._status_queue = multiprocessing.Queue()
32 self._lock_tag = None
Chris Masone816e38c2012-05-02 12:22:36 -070033
34 @staticmethod
35 def CanonicalizeAndParse(archive_url):
36 """Canonicalize archive_url and parse it into its component parts.
37
38 @param archive_url: a URL at which build artifacts are archived.
39 @return a tuple of (canonicalized URL, build target, short build name)
40 """
41 archive_url = archive_url.rstrip('/')
42 target, short_build = archive_url.rsplit('/', 2)[-2:]
43 return archive_url, target, short_build
44
45 @staticmethod
46 def GenerateLockTag(target, short_build):
47 """Generate a name for a lock scoped to this target/build pair.
48
49 @param target: the target the build was for.
50 @param short_build: short build name
51 @return a name to use with AcquireLock that will scope the lock.
52 """
53 return '/'.join([target, short_build])
Frank Farzan37761d12011-12-01 14:29:08 -080054
Chris Sosa9164ca32012-03-28 11:04:50 -070055 @staticmethod
56 def BuildStaged(archive_url, static_dir):
57 """Returns True if the build is already staged."""
Chris Masone816e38c2012-05-02 12:22:36 -070058 _, target, short_build = Downloader.CanonicalizeAndParse(archive_url)
59 sub_directory = Downloader.GenerateLockTag(target, short_build)
Chris Sosa9164ca32012-03-28 11:04:50 -070060 return os.path.isdir(os.path.join(static_dir, sub_directory))
61
Chris Sosa47a7d4e2012-03-28 11:26:55 -070062 def Download(self, archive_url, background=False):
63 """Downloads the given build artifacts defined by the |archive_url|.
64
65 If background is set to True, will return back early before all artifacts
66 have been downloaded. The artifacts that can be backgrounded are all those
67 that are not set as synchronous.
Chris Masone816e38c2012-05-02 12:22:36 -070068
69 TODO: refactor this into a common Download method, once unit tests are
70 fixed up to make iterating on the code easier.
Chris Sosa47a7d4e2012-03-28 11:26:55 -070071 """
Chris Sosa9164ca32012-03-28 11:04:50 -070072 # Parse archive_url into target and short_build.
73 # e.g. gs://chromeos-image-archive/{target}/{short_build}
Chris Masone816e38c2012-05-02 12:22:36 -070074 archive_url, target, short_build = self.CanonicalizeAndParse(archive_url)
Frank Farzan37761d12011-12-01 14:29:08 -080075
76 # Bind build_dir and staging_dir here so we can tell if we need to do any
77 # cleanup after an exception occurs before build_dir is set.
Chris Masone816e38c2012-05-02 12:22:36 -070078 self._lock_tag = self.GenerateLockTag(target, short_build)
79
80 if Downloader.BuildStaged(archive_url, self._static_dir):
81 cherrypy.log('Build %s has already been processed.' % self._lock_tag,
82 'DOWNLOAD')
83 self._status_queue.put('Success')
84 return 'Success'
85
Frank Farzan37761d12011-12-01 14:29:08 -080086 try:
87 # Create Dev Server directory for this build and tell other Downloader
88 # instances we have processed this build.
Chris Masone816e38c2012-05-02 12:22:36 -070089 self._build_dir = devserver_util.AcquireLock(
90 static_dir=self._static_dir, tag=self._lock_tag)
Frank Farzan37761d12011-12-01 14:29:08 -080091
Chris Sosa9164ca32012-03-28 11:04:50 -070092 self._staging_dir = tempfile.mkdtemp(suffix='_'.join([target,
93 short_build]))
Chris Masone816e38c2012-05-02 12:22:36 -070094 cherrypy.log('Gathering download requirements %s' % archive_url,
Chris Sosa47a7d4e2012-03-28 11:26:55 -070095 'DOWNLOAD')
Chris Masone816e38c2012-05-02 12:22:36 -070096 artifacts = self.GatherArtifactDownloads(
97 self._staging_dir, archive_url, short_build, self._build_dir)
Chris Sosa47a7d4e2012-03-28 11:26:55 -070098 devserver_util.PrepareBuildDirectory(self._build_dir)
Frank Farzan37761d12011-12-01 14:29:08 -080099
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700100 cherrypy.log('Downloading foreground artifacts from %s' % archive_url,
101 'DOWNLOAD')
102 background_artifacts = []
103 for artifact in artifacts:
104 if artifact.Synchronous():
105 artifact.Download()
106 artifact.Stage()
107 else:
108 background_artifacts.append(artifact)
Frank Farzan37761d12011-12-01 14:29:08 -0800109
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700110 if background:
Chris Masone816e38c2012-05-02 12:22:36 -0700111 self._DownloadArtifactsInBackground(background_artifacts, archive_url)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700112 else:
113 self._DownloadArtifactsSerially(background_artifacts)
114
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700115 except Exception, e:
Frank Farzan37761d12011-12-01 14:29:08 -0800116 # Release processing lock, which will remove build components directory
117 # so future runs can retry.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700118 if self._build_dir:
119 devserver_util.ReleaseLock(static_dir=self._static_dir,
120 tag=self._lock_tag)
121
122 self._status_queue.put(e)
123 self._Cleanup()
Frank Farzan37761d12011-12-01 14:29:08 -0800124 raise
Frank Farzan37761d12011-12-01 14:29:08 -0800125
126 return 'Success'
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700127
128 def _Cleanup(self):
129 """Cleans up the staging dir for this downloader instanfce."""
130 if self._staging_dir:
131 cherrypy.log('Cleaning up staging directory %s' % self._staging_dir,
132 'DOWNLOAD')
133 shutil.rmtree(self._staging_dir)
134
135 self._staging_dir = None
136
137 def _DownloadArtifactsSerially(self, artifacts):
138 """Simple function to download all the given artifacts serially."""
Chris Masone816e38c2012-05-02 12:22:36 -0700139 cherrypy.log('Downloading background artifacts serially.', 'DOWNLOAD')
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700140 try:
141 for artifact in artifacts:
142 artifact.Download()
143 artifact.Stage()
144 except Exception, e:
145 self._status_queue.put(e)
146
147 # Release processing lock, which will remove build components directory
148 # so future runs can retry.
149 if self._build_dir:
150 devserver_util.ReleaseLock(static_dir=self._static_dir,
151 tag=self._lock_tag)
152 else:
153 self._status_queue.put('Success')
154 finally:
155 self._Cleanup()
156
Chris Masone816e38c2012-05-02 12:22:36 -0700157 def _DownloadArtifactsInBackground(self, artifacts, archive_url):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700158 """Downloads |artifacts| in the background and signals when complete."""
159 proc = multiprocessing.Process(target=self._DownloadArtifactsSerially,
160 args=(artifacts,))
Chris Sosab65973e2012-03-29 18:31:02 -0700161 proc.start()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700162
Chris Masone816e38c2012-05-02 12:22:36 -0700163 def GatherArtifactDownloads(self, main_staging_dir, archive_url, short_build,
164 build_dir):
165 """Wrapper around devserver_util.GatherArtifactDownloads().
166
167 The wrapper allows mocking and overriding in derived classes.
168 """
169 return devserver_util.GatherArtifactDownloads(main_staging_dir, archive_url,
170 short_build, build_dir)
171
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700172 def GetStatusOfBackgroundDownloads(self):
173 """Returns the status of the background downloads.
174
175 This commands returns the status of the background downloads and blocks
176 until a status is returned.
177 """
178 status = self._status_queue.get()
179 # In case anyone else is calling.
180 self._status_queue.put(status)
181 # It's possible we received an exception, if so, re-raise it here.
182 if isinstance(status, Exception):
183 raise status
184
185 return status
Chris Masone816e38c2012-05-02 12:22:36 -0700186
187
188class SymbolDownloader(Downloader):
189 """Download and stage debug symbols for a build on the devsever.
190
191 Given a URL to a build on the archive server:
192
193 - Determine if the build already exists.
194 - Download and extract the debug symbols to a staging directory.
195 - Install symbols to static dir.
196 """
197
198 _DONE_FLAG = 'done'
199
200 @staticmethod
201 def GenerateLockTag(target, short_build):
202 return '/'.join([target, short_build, 'symbols'])
203
204 def Download(self, archive_url):
205 """Downloads debug symbols for the build defined by the |archive_url|.
206
207 The symbols will be downloaded synchronously
208 """
209 # Parse archive_url into target and short_build.
210 # e.g. gs://chromeos-image-archive/{target}/{short_build}
211 archive_url, target, short_build = self.CanonicalizeAndParse(archive_url)
212
213 # Bind build_dir and staging_dir here so we can tell if we need to do any
214 # cleanup after an exception occurs before build_dir is set.
215 self._lock_tag = self.GenerateLockTag(target, short_build)
216 if self.SymbolsStaged(archive_url, self._static_dir):
217 cherrypy.log(
218 'Symbols for build %s have already been staged.' % self._lock_tag,
219 'SYMBOL_DOWNLOAD')
220 return 'Success'
221
222 try:
223 # Create Dev Server directory for this build and tell other Downloader
224 # instances we have processed this build.
225 self._build_dir = devserver_util.AcquireLock(
226 static_dir=self._static_dir, tag=self._lock_tag)
227
228 self._staging_dir = tempfile.mkdtemp(suffix='_'.join([target,
229 short_build]))
230 cherrypy.log('Downloading debug symbols from %s' % archive_url,
231 'SYMBOL_DOWNLOAD')
232
233 [symbol_artifact] = self.GatherArtifactDownloads(
234 self._staging_dir, archive_url, '', self._static_dir)
235 symbol_artifact.Download()
236 symbol_artifact.Stage()
237
238 except Exception:
239 # Release processing "lock", which will indicate to future runs that we
240 # did not succeed, and so they should try again.
241 if self._build_dir:
242 devserver_util.ReleaseLock(static_dir=self._static_dir,
243 tag=self._lock_tag)
244 self._Cleanup()
245 raise
246
247 self.MarkSymbolsStaged()
248 return 'Success'
249
250 def GatherArtifactDownloads(self, temp_download_dir, archive_url, short_build,
251 static_dir):
252 """Call SymbolDownloader-appropriate artifact gathering method.
253
254 @param temp_download_dir: the tempdir into which we're downloading artifacts
255 prior to staging them.
256 @param archive_url: the google storage url of the bucket where the debug
257 symbols for the desired build are stored.
258 @param short_build: IGNORED
259 @param staging_dir: the dir into which to stage the symbols
260
261 @return an iterable of one DebugTarball pointing to the right debug symbols.
262 This is an iterable so that it's similar to GatherArtifactDownloads.
263 Also, it's possible that someday we might have more than one.
264 """
265 return devserver_util.GatherSymbolArtifactDownloads(temp_download_dir,
266 archive_url,
267 static_dir)
268
269 def MarkSymbolsStaged(self):
270 """Puts a flag file on disk to signal that symbols are staged."""
271 with open(os.path.join(self._build_dir, self._DONE_FLAG), 'w') as flag:
272 flag.write(self._DONE_FLAG)
273
274 def SymbolsStaged(self, archive_url, static_dir):
275 """Returns True if the build is already staged."""
276 _, target, short_build = self.CanonicalizeAndParse(archive_url)
277 sub_directory = self.GenerateLockTag(target, short_build)
278 return os.path.isfile(os.path.join(static_dir,
279 sub_directory,
280 self._DONE_FLAG))