blob: 6698ce41e6ed87b5cc4d727034ad7466fb8c5e4f [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 Hongd49d7f42012-06-25 12:23:11 -070096 self._staging_dir = tempfile.mkdtemp(suffix='_'.join([rel_path,
Chris Sosa9164ca32012-03-28 11:04:50 -070097 short_build]))
Chris Masone816e38c2012-05-02 12:22:36 -070098 cherrypy.log('Gathering download requirements %s' % archive_url,
Chris Masonea22d9382012-05-18 12:38:51 -070099 self._LOG_TAG)
Chris Masone816e38c2012-05-02 12:22:36 -0700100 artifacts = self.GatherArtifactDownloads(
101 self._staging_dir, archive_url, short_build, self._build_dir)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700102 devserver_util.PrepareBuildDirectory(self._build_dir)
Frank Farzan37761d12011-12-01 14:29:08 -0800103
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700104 cherrypy.log('Downloading foreground artifacts from %s' % archive_url,
Chris Masonea22d9382012-05-18 12:38:51 -0700105 self._LOG_TAG)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700106 background_artifacts = []
107 for artifact in artifacts:
108 if artifact.Synchronous():
109 artifact.Download()
110 artifact.Stage()
111 else:
112 background_artifacts.append(artifact)
Frank Farzan37761d12011-12-01 14:29:08 -0800113
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700114 if background:
Chris Sosacde6bf42012-05-31 18:36:39 -0700115 self._DownloadArtifactsInBackground(background_artifacts)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700116 else:
117 self._DownloadArtifactsSerially(background_artifacts)
118
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700119 except Exception, e:
Frank Farzan37761d12011-12-01 14:29:08 -0800120 # Release processing lock, which will remove build components directory
121 # so future runs can retry.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700122 if self._build_dir:
123 devserver_util.ReleaseLock(static_dir=self._static_dir,
124 tag=self._lock_tag)
125
126 self._status_queue.put(e)
127 self._Cleanup()
Frank Farzan37761d12011-12-01 14:29:08 -0800128 raise
Frank Farzan37761d12011-12-01 14:29:08 -0800129
130 return 'Success'
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700131
132 def _Cleanup(self):
133 """Cleans up the staging dir for this downloader instanfce."""
134 if self._staging_dir:
135 cherrypy.log('Cleaning up staging directory %s' % self._staging_dir,
Chris Masonea22d9382012-05-18 12:38:51 -0700136 self._LOG_TAG)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700137 shutil.rmtree(self._staging_dir)
138
139 self._staging_dir = None
140
141 def _DownloadArtifactsSerially(self, artifacts):
142 """Simple function to download all the given artifacts serially."""
Chris Masonea22d9382012-05-18 12:38:51 -0700143 cherrypy.log('Downloading background artifacts serially.', self._LOG_TAG)
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700144 try:
145 for artifact in artifacts:
146 artifact.Download()
147 artifact.Stage()
148 except Exception, e:
149 self._status_queue.put(e)
150
151 # Release processing lock, which will remove build components directory
152 # so future runs can retry.
153 if self._build_dir:
154 devserver_util.ReleaseLock(static_dir=self._static_dir,
155 tag=self._lock_tag)
156 else:
157 self._status_queue.put('Success')
158 finally:
159 self._Cleanup()
160
Chris Sosacde6bf42012-05-31 18:36:39 -0700161 def _DownloadArtifactsInBackground(self, artifacts):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700162 """Downloads |artifacts| in the background and signals when complete."""
163 proc = multiprocessing.Process(target=self._DownloadArtifactsSerially,
164 args=(artifacts,))
Chris Sosab65973e2012-03-29 18:31:02 -0700165 proc.start()
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700166
Chris Masone816e38c2012-05-02 12:22:36 -0700167 def GatherArtifactDownloads(self, main_staging_dir, archive_url, short_build,
168 build_dir):
169 """Wrapper around devserver_util.GatherArtifactDownloads().
170
171 The wrapper allows mocking and overriding in derived classes.
172 """
173 return devserver_util.GatherArtifactDownloads(main_staging_dir, archive_url,
174 short_build, build_dir)
175
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700176 def GetStatusOfBackgroundDownloads(self):
177 """Returns the status of the background downloads.
178
179 This commands returns the status of the background downloads and blocks
180 until a status is returned.
181 """
182 status = self._status_queue.get()
183 # In case anyone else is calling.
184 self._status_queue.put(status)
185 # It's possible we received an exception, if so, re-raise it here.
186 if isinstance(status, Exception):
187 raise status
188
189 return status
Chris Masone816e38c2012-05-02 12:22:36 -0700190
191
192class SymbolDownloader(Downloader):
193 """Download and stage debug symbols for a build on the devsever.
194
195 Given a URL to a build on the archive server:
196
197 - Determine if the build already exists.
198 - Download and extract the debug symbols to a staging directory.
199 - Install symbols to static dir.
200 """
201
202 _DONE_FLAG = 'done'
Chris Masonea22d9382012-05-18 12:38:51 -0700203 _LOG_TAG = 'SYMBOL_DOWNLOAD'
Chris Masone816e38c2012-05-02 12:22:36 -0700204
205 @staticmethod
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -0700206 def GenerateLockTag(rel_path, short_build):
207 return '/'.join([rel_path, short_build, 'symbols'])
Chris Masone816e38c2012-05-02 12:22:36 -0700208
Chris Sosacde6bf42012-05-31 18:36:39 -0700209 def Download(self, archive_url, _background=False):
Chris Masone816e38c2012-05-02 12:22:36 -0700210 """Downloads debug symbols for the build defined by the |archive_url|.
211
212 The symbols will be downloaded synchronously
213 """
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -0700214 # Parse archive_url into rel_path (contains the build target) and
215 # short_build.
216 # e.g. gs://chromeos-image-archive/{rel_path}/{short_build}
217 rel_path, short_build = self.ParseUrl(archive_url)
Chris Masone816e38c2012-05-02 12:22:36 -0700218
219 # Bind build_dir and staging_dir here so we can tell if we need to do any
220 # cleanup after an exception occurs before build_dir is set.
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -0700221 self._lock_tag = self.GenerateLockTag(rel_path, short_build)
Chris Masone816e38c2012-05-02 12:22:36 -0700222 if self.SymbolsStaged(archive_url, self._static_dir):
223 cherrypy.log(
224 'Symbols for build %s have already been staged.' % self._lock_tag,
Chris Masonea22d9382012-05-18 12:38:51 -0700225 self._LOG_TAG)
Chris Masone816e38c2012-05-02 12:22:36 -0700226 return 'Success'
227
228 try:
229 # Create Dev Server directory for this build and tell other Downloader
230 # instances we have processed this build.
231 self._build_dir = devserver_util.AcquireLock(
232 static_dir=self._static_dir, tag=self._lock_tag)
233
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -0700234 self._staging_dir = tempfile.mkdtemp(suffix='_'.join([rel_path,
Chris Masone816e38c2012-05-02 12:22:36 -0700235 short_build]))
236 cherrypy.log('Downloading debug symbols from %s' % archive_url,
Chris Masonea22d9382012-05-18 12:38:51 -0700237 self._LOG_TAG)
Chris Masone816e38c2012-05-02 12:22:36 -0700238
239 [symbol_artifact] = self.GatherArtifactDownloads(
240 self._staging_dir, archive_url, '', self._static_dir)
241 symbol_artifact.Download()
242 symbol_artifact.Stage()
Chris Masonea22d9382012-05-18 12:38:51 -0700243 self.MarkSymbolsStaged()
Chris Masone816e38c2012-05-02 12:22:36 -0700244
245 except Exception:
246 # Release processing "lock", which will indicate to future runs that we
247 # did not succeed, and so they should try again.
248 if self._build_dir:
249 devserver_util.ReleaseLock(static_dir=self._static_dir,
250 tag=self._lock_tag)
Chris Masone816e38c2012-05-02 12:22:36 -0700251 raise
Chris Masonea22d9382012-05-18 12:38:51 -0700252 finally:
253 self._Cleanup()
Chris Masone816e38c2012-05-02 12:22:36 -0700254 return 'Success'
255
256 def GatherArtifactDownloads(self, temp_download_dir, archive_url, short_build,
257 static_dir):
258 """Call SymbolDownloader-appropriate artifact gathering method.
259
260 @param temp_download_dir: the tempdir into which we're downloading artifacts
261 prior to staging them.
262 @param archive_url: the google storage url of the bucket where the debug
263 symbols for the desired build are stored.
264 @param short_build: IGNORED
265 @param staging_dir: the dir into which to stage the symbols
266
267 @return an iterable of one DebugTarball pointing to the right debug symbols.
268 This is an iterable so that it's similar to GatherArtifactDownloads.
269 Also, it's possible that someday we might have more than one.
270 """
271 return devserver_util.GatherSymbolArtifactDownloads(temp_download_dir,
272 archive_url,
273 static_dir)
274
275 def MarkSymbolsStaged(self):
276 """Puts a flag file on disk to signal that symbols are staged."""
277 with open(os.path.join(self._build_dir, self._DONE_FLAG), 'w') as flag:
278 flag.write(self._DONE_FLAG)
279
280 def SymbolsStaged(self, archive_url, static_dir):
281 """Returns True if the build is already staged."""
Yu-Ju Hongd49d7f42012-06-25 12:23:11 -0700282 rel_path, short_build = self.ParseUrl(archive_url)
283 sub_directory = self.GenerateLockTag(rel_path, short_build)
Chris Masone816e38c2012-05-02 12:22:36 -0700284 return os.path.isfile(os.path.join(static_dir,
285 sub_directory,
286 self._DONE_FLAG))