blob: 955e796fd0da958cb09de3d0d776774d562cb217 [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
33 self._status_queue = multiprocessing.Queue()
34 self._lock_tag = None
Chris Masone816e38c2012-05-02 12:22:36 -070035
36 @staticmethod
37 def CanonicalizeAndParse(archive_url):
38 """Canonicalize archive_url and parse it into its component parts.
39
40 @param archive_url: a URL at which build artifacts are archived.
41 @return a tuple of (canonicalized URL, build target, short build name)
42 """
43 archive_url = archive_url.rstrip('/')
44 target, short_build = archive_url.rsplit('/', 2)[-2:]
45 return archive_url, target, short_build
46
47 @staticmethod
48 def GenerateLockTag(target, short_build):
49 """Generate a name for a lock scoped to this target/build pair.
50
51 @param target: the target the build was for.
52 @param short_build: short build name
53 @return a name to use with AcquireLock that will scope the lock.
54 """
55 return '/'.join([target, short_build])
Frank Farzan37761d12011-12-01 14:29:08 -080056
Chris Sosa9164ca32012-03-28 11:04:50 -070057 @staticmethod
58 def BuildStaged(archive_url, static_dir):
59 """Returns True if the build is already staged."""
Chris Masone816e38c2012-05-02 12:22:36 -070060 _, target, short_build = Downloader.CanonicalizeAndParse(archive_url)
61 sub_directory = Downloader.GenerateLockTag(target, short_build)
Chris Sosa9164ca32012-03-28 11:04:50 -070062 return os.path.isdir(os.path.join(static_dir, sub_directory))
63
Chris Sosa47a7d4e2012-03-28 11:26:55 -070064 def Download(self, archive_url, background=False):
65 """Downloads the given build artifacts defined by the |archive_url|.
66
67 If background is set to True, will return back early before all artifacts
68 have been downloaded. The artifacts that can be backgrounded are all those
69 that are not set as synchronous.
Chris Masone816e38c2012-05-02 12:22:36 -070070
71 TODO: refactor this into a common Download method, once unit tests are
72 fixed up to make iterating on the code easier.
Chris Sosa47a7d4e2012-03-28 11:26:55 -070073 """
Chris Sosa9164ca32012-03-28 11:04:50 -070074 # Parse archive_url into target and short_build.
75 # e.g. gs://chromeos-image-archive/{target}/{short_build}
Chris Masone816e38c2012-05-02 12:22:36 -070076 archive_url, target, short_build = self.CanonicalizeAndParse(archive_url)
Frank Farzan37761d12011-12-01 14:29:08 -080077
78 # Bind build_dir and staging_dir here so we can tell if we need to do any
79 # cleanup after an exception occurs before build_dir is set.
Chris Masone816e38c2012-05-02 12:22:36 -070080 self._lock_tag = self.GenerateLockTag(target, short_build)
81
82 if Downloader.BuildStaged(archive_url, self._static_dir):
83 cherrypy.log('Build %s has already been processed.' % self._lock_tag,
Chris Masonea22d9382012-05-18 12:38:51 -070084 self._LOG_TAG)
Chris Masone816e38c2012-05-02 12:22:36 -070085 self._status_queue.put('Success')
86 return 'Success'
87
Frank Farzan37761d12011-12-01 14:29:08 -080088 try:
89 # Create Dev Server directory for this build and tell other Downloader
90 # instances we have processed this build.
Chris Masone816e38c2012-05-02 12:22:36 -070091 self._build_dir = devserver_util.AcquireLock(
92 static_dir=self._static_dir, tag=self._lock_tag)
Frank Farzan37761d12011-12-01 14:29:08 -080093
Chris Sosa9164ca32012-03-28 11:04:50 -070094 self._staging_dir = tempfile.mkdtemp(suffix='_'.join([target,
95 short_build]))
Chris Masone816e38c2012-05-02 12:22:36 -070096 cherrypy.log('Gathering download requirements %s' % archive_url,
Chris Masonea22d9382012-05-18 12:38:51 -070097 self._LOG_TAG)
Chris Masone816e38c2012-05-02 12:22:36 -070098 artifacts = self.GatherArtifactDownloads(
99 self._staging_dir, archive_url, short_build, self._build_dir)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700100 devserver_util.PrepareBuildDirectory(self._build_dir)
Frank Farzan37761d12011-12-01 14:29:08 -0800101
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700102 cherrypy.log('Downloading foreground artifacts from %s' % archive_url,
Chris Masonea22d9382012-05-18 12:38:51 -0700103 self._LOG_TAG)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700104 background_artifacts = []
105 for artifact in artifacts:
106 if artifact.Synchronous():
107 artifact.Download()
108 artifact.Stage()
109 else:
110 background_artifacts.append(artifact)
Frank Farzan37761d12011-12-01 14:29:08 -0800111
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700112 if background:
Chris Masone816e38c2012-05-02 12:22:36 -0700113 self._DownloadArtifactsInBackground(background_artifacts, archive_url)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700114 else:
115 self._DownloadArtifactsSerially(background_artifacts)
116
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700117 except Exception, e:
Frank Farzan37761d12011-12-01 14:29:08 -0800118 # Release processing lock, which will remove build components directory
119 # so future runs can retry.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700120 if self._build_dir:
121 devserver_util.ReleaseLock(static_dir=self._static_dir,
122 tag=self._lock_tag)
123
124 self._status_queue.put(e)
125 self._Cleanup()
Frank Farzan37761d12011-12-01 14:29:08 -0800126 raise
Frank Farzan37761d12011-12-01 14:29:08 -0800127
128 return 'Success'
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700129
130 def _Cleanup(self):
131 """Cleans up the staging dir for this downloader instanfce."""
132 if self._staging_dir:
133 cherrypy.log('Cleaning up staging directory %s' % self._staging_dir,
Chris Masonea22d9382012-05-18 12:38:51 -0700134 self._LOG_TAG)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700135 shutil.rmtree(self._staging_dir)
136
137 self._staging_dir = None
138
139 def _DownloadArtifactsSerially(self, artifacts):
140 """Simple function to download all the given artifacts serially."""
Chris Masonea22d9382012-05-18 12:38:51 -0700141 cherrypy.log('Downloading background artifacts serially.', self._LOG_TAG)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700142 try:
143 for artifact in artifacts:
144 artifact.Download()
145 artifact.Stage()
146 except Exception, e:
147 self._status_queue.put(e)
148
149 # Release processing lock, which will remove build components directory
150 # so future runs can retry.
151 if self._build_dir:
152 devserver_util.ReleaseLock(static_dir=self._static_dir,
153 tag=self._lock_tag)
154 else:
155 self._status_queue.put('Success')
156 finally:
157 self._Cleanup()
158
Chris Masone816e38c2012-05-02 12:22:36 -0700159 def _DownloadArtifactsInBackground(self, artifacts, archive_url):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700160 """Downloads |artifacts| in the background and signals when complete."""
161 proc = multiprocessing.Process(target=self._DownloadArtifactsSerially,
162 args=(artifacts,))
Chris Sosab65973e2012-03-29 18:31:02 -0700163 proc.start()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700164
Chris Masone816e38c2012-05-02 12:22:36 -0700165 def GatherArtifactDownloads(self, main_staging_dir, archive_url, short_build,
166 build_dir):
167 """Wrapper around devserver_util.GatherArtifactDownloads().
168
169 The wrapper allows mocking and overriding in derived classes.
170 """
171 return devserver_util.GatherArtifactDownloads(main_staging_dir, archive_url,
172 short_build, build_dir)
173
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700174 def GetStatusOfBackgroundDownloads(self):
175 """Returns the status of the background downloads.
176
177 This commands returns the status of the background downloads and blocks
178 until a status is returned.
179 """
180 status = self._status_queue.get()
181 # In case anyone else is calling.
182 self._status_queue.put(status)
183 # It's possible we received an exception, if so, re-raise it here.
184 if isinstance(status, Exception):
185 raise status
186
187 return status
Chris Masone816e38c2012-05-02 12:22:36 -0700188
189
190class SymbolDownloader(Downloader):
191 """Download and stage debug symbols for a build on the devsever.
192
193 Given a URL to a build on the archive server:
194
195 - Determine if the build already exists.
196 - Download and extract the debug symbols to a staging directory.
197 - Install symbols to static dir.
198 """
199
200 _DONE_FLAG = 'done'
Chris Masonea22d9382012-05-18 12:38:51 -0700201 _LOG_TAG = 'SYMBOL_DOWNLOAD'
Chris Masone816e38c2012-05-02 12:22:36 -0700202
203 @staticmethod
204 def GenerateLockTag(target, short_build):
205 return '/'.join([target, short_build, 'symbols'])
206
207 def Download(self, archive_url):
208 """Downloads debug symbols for the build defined by the |archive_url|.
209
210 The symbols will be downloaded synchronously
211 """
212 # Parse archive_url into target and short_build.
213 # e.g. gs://chromeos-image-archive/{target}/{short_build}
214 archive_url, target, short_build = self.CanonicalizeAndParse(archive_url)
215
216 # Bind build_dir and staging_dir here so we can tell if we need to do any
217 # cleanup after an exception occurs before build_dir is set.
218 self._lock_tag = self.GenerateLockTag(target, short_build)
219 if self.SymbolsStaged(archive_url, self._static_dir):
220 cherrypy.log(
221 'Symbols for build %s have already been staged.' % self._lock_tag,
Chris Masonea22d9382012-05-18 12:38:51 -0700222 self._LOG_TAG)
Chris Masone816e38c2012-05-02 12:22:36 -0700223 return 'Success'
224
225 try:
226 # Create Dev Server directory for this build and tell other Downloader
227 # instances we have processed this build.
228 self._build_dir = devserver_util.AcquireLock(
229 static_dir=self._static_dir, tag=self._lock_tag)
230
231 self._staging_dir = tempfile.mkdtemp(suffix='_'.join([target,
232 short_build]))
233 cherrypy.log('Downloading debug symbols from %s' % archive_url,
Chris Masonea22d9382012-05-18 12:38:51 -0700234 self._LOG_TAG)
Chris Masone816e38c2012-05-02 12:22:36 -0700235
236 [symbol_artifact] = self.GatherArtifactDownloads(
237 self._staging_dir, archive_url, '', self._static_dir)
238 symbol_artifact.Download()
239 symbol_artifact.Stage()
Chris Masonea22d9382012-05-18 12:38:51 -0700240 self.MarkSymbolsStaged()
Chris Masone816e38c2012-05-02 12:22:36 -0700241
242 except Exception:
243 # Release processing "lock", which will indicate to future runs that we
244 # did not succeed, and so they should try again.
245 if self._build_dir:
246 devserver_util.ReleaseLock(static_dir=self._static_dir,
247 tag=self._lock_tag)
Chris Masone816e38c2012-05-02 12:22:36 -0700248 raise
Chris Masonea22d9382012-05-18 12:38:51 -0700249 finally:
250 self._Cleanup()
Chris Masone816e38c2012-05-02 12:22:36 -0700251 return 'Success'
252
253 def GatherArtifactDownloads(self, temp_download_dir, archive_url, short_build,
254 static_dir):
255 """Call SymbolDownloader-appropriate artifact gathering method.
256
257 @param temp_download_dir: the tempdir into which we're downloading artifacts
258 prior to staging them.
259 @param archive_url: the google storage url of the bucket where the debug
260 symbols for the desired build are stored.
261 @param short_build: IGNORED
262 @param staging_dir: the dir into which to stage the symbols
263
264 @return an iterable of one DebugTarball pointing to the right debug symbols.
265 This is an iterable so that it's similar to GatherArtifactDownloads.
266 Also, it's possible that someday we might have more than one.
267 """
268 return devserver_util.GatherSymbolArtifactDownloads(temp_download_dir,
269 archive_url,
270 static_dir)
271
272 def MarkSymbolsStaged(self):
273 """Puts a flag file on disk to signal that symbols are staged."""
274 with open(os.path.join(self._build_dir, self._DONE_FLAG), 'w') as flag:
275 flag.write(self._DONE_FLAG)
276
277 def SymbolsStaged(self, archive_url, static_dir):
278 """Returns True if the build is already staged."""
279 _, target, short_build = self.CanonicalizeAndParse(archive_url)
280 sub_directory = self.GenerateLockTag(target, short_build)
281 return os.path.isfile(os.path.join(static_dir,
282 sub_directory,
283 self._DONE_FLAG))