blob: 8378b76379605824e502156aa44ac00557062c1b [file] [log] [blame]
Chris Sosa7c931362010-10-11 19:49:01 -07001#!/usr/bin/python
2
Chris Sosa781ba6d2012-04-11 12:44:43 -07003# Copyright (c) 2009-2012 The Chromium OS Authors. All rights reserved.
rtc@google.comded22402009-10-26 22:36:21 +00004# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
Chris Sosa7c931362010-10-11 19:49:01 -07007"""A CherryPy-based webserver to host images and build packages."""
8
Chris Sosadbc20082012-12-10 13:39:11 -08009import cherrypy
Gilad Arnold55a2a372012-10-02 09:46:32 -070010import json
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070011import optparse
rtc@google.comded22402009-10-26 22:36:21 +000012import os
Scott Zawalski4647ce62012-01-03 17:17:28 -050013import re
Simran Basi4baad082013-02-14 13:39:18 -080014import shutil
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -080015import socket
chocobo@google.com4dc25812009-10-27 23:46:26 +000016import sys
Chris Masone816e38c2012-05-02 12:22:36 -070017import subprocess
18import tempfile
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -070019import types
rtc@google.comded22402009-10-26 22:36:21 +000020
Chris Sosa0356d3b2010-09-16 15:46:22 -070021import autoupdate
Gilad Arnoldc65330c2012-09-20 15:17:48 -070022import common_util
Chris Sosa47a7d4e2012-03-28 11:26:55 -070023import downloader
Gilad Arnoldc65330c2012-09-20 15:17:48 -070024import log_util
25
26
27# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080028def _Log(message, *args):
29 return log_util.LogWithTag('DEVSERVER', message, *args)
Chris Sosa0356d3b2010-09-16 15:46:22 -070030
Frank Farzan40160872011-12-12 18:39:18 -080031
Chris Sosa417e55d2011-01-25 16:40:48 -080032CACHED_ENTRIES = 12
Don Garrettf90edf02010-11-16 17:36:14 -080033
Simran Basi4baad082013-02-14 13:39:18 -080034TELEMETRY_FOLDER = 'telemetry_src'
35TELEMETRY_DEPS = ['dep-telemetry_dep.tar.bz2',
36 'dep-page_cycler_dep.tar.bz2',
37 'dep-chrome_test.tar.bz2']
38
Chris Sosa0356d3b2010-09-16 15:46:22 -070039# Sets up global to share between classes.
rtc@google.com21a5ca32009-11-04 18:23:23 +000040updater = None
rtc@google.comded22402009-10-26 22:36:21 +000041
Frank Farzan40160872011-12-12 18:39:18 -080042
Chris Sosa9164ca32012-03-28 11:04:50 -070043class DevServerError(Exception):
Chris Sosa47a7d4e2012-03-28 11:26:55 -070044 """Exception class used by this module."""
45 pass
46
47
Scott Zawalski4647ce62012-01-03 17:17:28 -050048def _LeadingWhiteSpaceCount(string):
49 """Count the amount of leading whitespace in a string.
50
51 Args:
52 string: The string to count leading whitespace in.
53 Returns:
54 number of white space chars before characters start.
55 """
56 matched = re.match('^\s+', string)
57 if matched:
58 return len(matched.group())
59
60 return 0
61
62
63def _PrintDocStringAsHTML(func):
64 """Make a functions docstring somewhat HTML style.
65
66 Args:
67 func: The function to return the docstring from.
68 Returns:
69 A string that is somewhat formated for a web browser.
70 """
71 # TODO(scottz): Make this parse Args/Returns in a prettier way.
72 # Arguments could be bolded and indented etc.
73 html_doc = []
74 for line in func.__doc__.splitlines():
75 leading_space = _LeadingWhiteSpaceCount(line)
76 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 11:26:55 -070077 line = ' ' * leading_space + line
Scott Zawalski4647ce62012-01-03 17:17:28 -050078
79 html_doc.append('<BR>%s' % line)
80
81 return '\n'.join(html_doc)
82
83
Chris Sosa7c931362010-10-11 19:49:01 -070084def _GetConfig(options):
85 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -080086
87 # On a system with IPv6 not compiled into the kernel,
88 # AF_INET6 sockets will return a socket.error exception.
89 # On such systems, fall-back to IPv4.
90 socket_host = '::'
91 try:
92 socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
93 except socket.error:
94 socket_host = '0.0.0.0'
95
Chris Sosa7c931362010-10-11 19:49:01 -070096 base_config = { 'global':
97 { 'server.log_request_headers': True,
98 'server.protocol_version': 'HTTP/1.1',
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -080099 'server.socket_host': socket_host,
Chris Sosa7c931362010-10-11 19:49:01 -0700100 'server.socket_port': int(options.port),
Chris Sosa374c62d2010-10-14 09:13:54 -0700101 'response.timeout': 6000,
Chris Sosa6fe23942012-07-02 15:44:46 -0700102 'request.show_tracebacks': True,
Chris Sosa72333d12012-06-13 11:28:05 -0700103 'server.socket_timeout': 60,
Zdenek Behan1347a312011-02-10 03:59:17 +0100104 'tools.staticdir.root':
105 os.path.dirname(os.path.abspath(sys.argv[0])),
Chris Sosa7c931362010-10-11 19:49:01 -0700106 },
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700107 '/api':
108 {
109 # Gets rid of cherrypy parsing post file for args.
110 'request.process_request_body': False,
111 },
Chris Sosaa1ef0102010-10-21 16:22:35 -0700112 '/build':
113 {
114 'response.timeout': 100000,
115 },
Chris Sosa7c931362010-10-11 19:49:01 -0700116 '/update':
117 {
118 # Gets rid of cherrypy parsing post file for args.
119 'request.process_request_body': False,
Chris Sosaf65f4b92010-10-21 15:57:51 -0700120 'response.timeout': 10000,
Chris Sosa7c931362010-10-11 19:49:01 -0700121 },
122 # Sets up the static dir for file hosting.
123 '/static':
124 { 'tools.staticdir.dir': 'static',
125 'tools.staticdir.on': True,
Chris Sosaf65f4b92010-10-21 15:57:51 -0700126 'response.timeout': 10000,
Chris Sosa7c931362010-10-11 19:49:01 -0700127 },
128 }
Chris Sosa5f118ef2012-07-12 11:37:50 -0700129 if options.production:
Chris Sosad1ea86b2012-07-12 13:35:37 -0700130 base_config['global'].update({'server.thread_pool': 75})
Scott Zawalski1c5e7cd2012-02-27 13:12:52 -0500131
Chris Sosa7c931362010-10-11 19:49:01 -0700132 return base_config
rtc@google.com64244662009-11-12 00:52:08 +0000133
Darin Petkove17164a2010-08-11 13:24:41 -0700134
Zdenek Behan608f46c2011-02-19 00:47:16 +0100135def _PrepareToServeUpdatesOnly(image_dir, static_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700136 """Sets up symlink to image_dir for serving purposes."""
137 assert os.path.exists(image_dir), '%s must exist.' % image_dir
138 # If we're serving out of an archived build dir (e.g. a
139 # buildbot), prepare this webserver's magic 'static/' dir with a
140 # link to the build archive.
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700141 _Log('Preparing autoupdate for "serve updates only" mode.')
Zdenek Behan608f46c2011-02-19 00:47:16 +0100142 if os.path.lexists('%s/archive' % static_dir):
143 if image_dir != os.readlink('%s/archive' % static_dir):
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700144 _Log('removing stale symlink to %s' % image_dir)
Zdenek Behan608f46c2011-02-19 00:47:16 +0100145 os.unlink('%s/archive' % static_dir)
146 os.symlink(image_dir, '%s/archive' % static_dir)
Chris Sosacde6bf42012-05-31 18:36:39 -0700147
Chris Sosa0356d3b2010-09-16 15:46:22 -0700148 else:
Zdenek Behan608f46c2011-02-19 00:47:16 +0100149 os.symlink(image_dir, '%s/archive' % static_dir)
Chris Sosacde6bf42012-05-31 18:36:39 -0700150
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700151 _Log('archive dir: %s ready to be used to serve images.' % image_dir)
Chris Sosa7c931362010-10-11 19:49:01 -0700152
153
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700154def _GetRecursiveMemberObject(root, member_list):
155 """Returns an object corresponding to a nested member list.
156
157 Args:
158 root: the root object to search
159 member_list: list of nested members to search
160 Returns:
161 An object corresponding to the member name list; None otherwise.
162 """
163 for member in member_list:
164 next_root = root.__class__.__dict__.get(member)
165 if not next_root:
166 return None
167 root = next_root
168 return root
169
170
171def _IsExposed(name):
172 """Returns True iff |name| has an `exposed' attribute and it is set."""
173 return hasattr(name, 'exposed') and name.exposed
174
175
Gilad Arnold748c8322012-10-12 09:51:35 -0700176def _GetExposedMethod(root, nested_member, ignored=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700177 """Returns a CherryPy-exposed method, if such exists.
178
179 Args:
180 root: the root object for searching
181 nested_member: a slash-joined path to the nested member
182 ignored: method paths to be ignored
183 Returns:
184 A function object corresponding to the path defined by |member_list| from
185 the |root| object, if the function is exposed and not ignored; None
186 otherwise.
187 """
Gilad Arnold748c8322012-10-12 09:51:35 -0700188 method = (not (ignored and nested_member in ignored) and
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700189 _GetRecursiveMemberObject(root, nested_member.split('/')))
190 if (method and type(method) == types.FunctionType and _IsExposed(method)):
191 return method
192
193
Gilad Arnold748c8322012-10-12 09:51:35 -0700194def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700195 """Finds exposed CherryPy methods.
196
197 Args:
198 root: the root object for searching
199 prefix: slash-joined chain of members leading to current object
200 unlisted: URLs to be excluded regardless of their exposed status
201 Returns:
202 List of exposed URLs that are not unlisted.
203 """
204 method_list = []
205 for member in sorted(root.__class__.__dict__.keys()):
206 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 09:51:35 -0700207 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700208 continue
209 member_obj = root.__class__.__dict__[member]
210 if _IsExposed(member_obj):
211 if type(member_obj) == types.FunctionType:
212 method_list.append(prefixed_member)
213 else:
214 method_list += _FindExposedMethods(
215 member_obj, prefixed_member, unlisted)
216 return method_list
217
218
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700219class ApiRoot(object):
220 """RESTful API for Dev Server information."""
221 exposed = True
222
223 @cherrypy.expose
224 def hostinfo(self, ip):
225 """Returns a JSON dictionary containing information about the given ip.
226
Gilad Arnold1b908392012-10-05 11:36:27 -0700227 Args:
228 ip: address of host whose info is requested
229 Returns:
230 A JSON dictionary containing all or some of the following fields:
231 last_event_type (int): last update event type received
232 last_event_status (int): last update event status received
233 last_known_version (string): last known version reported in update ping
234 forced_update_label (string): update label to force next update ping to
235 use, set by setnextupdate
236 See the OmahaEvent class in update_engine/omaha_request_action.h for
237 event type and status code definitions. If the ip does not exist an empty
238 string is returned.
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700239
Gilad Arnold1b908392012-10-05 11:36:27 -0700240 Example URL:
241 http://myhost/api/hostinfo?ip=192.168.1.5
242 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700243 return updater.HandleHostInfoPing(ip)
244
245 @cherrypy.expose
Gilad Arnold286a0062012-01-12 13:47:02 -0800246 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 11:36:27 -0700247 """Returns a JSON object containing a log of host event.
248
249 Args:
250 ip: address of host whose event log is requested, or `all'
251 Returns:
252 A JSON encoded list (log) of dictionaries (events), each of which
253 containing a `timestamp' and other event fields, as described under
254 /api/hostinfo.
255
256 Example URL:
257 http://myhost/api/hostlog?ip=192.168.1.5
258 """
Gilad Arnold286a0062012-01-12 13:47:02 -0800259 return updater.HandleHostLogPing(ip)
260
261 @cherrypy.expose
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700262 def setnextupdate(self, ip):
263 """Allows the response to the next update ping from a host to be set.
264
265 Takes the IP of the host and an update label as normally provided to the
Gilad Arnold1b908392012-10-05 11:36:27 -0700266 /update command.
267 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700268 body_length = int(cherrypy.request.headers['Content-Length'])
269 label = cherrypy.request.rfile.read(body_length)
270
271 if label:
272 label = label.strip()
273 if label:
274 return updater.HandleSetUpdatePing(ip, label)
275 raise cherrypy.HTTPError(400, 'No label provided.')
276
277
Gilad Arnold55a2a372012-10-02 09:46:32 -0700278 @cherrypy.expose
279 def fileinfo(self, *path_args):
280 """Returns information about a given staged file.
281
282 Args:
283 path_args: path to the file inside the server's static staging directory
284 Returns:
285 A JSON encoded dictionary with information about the said file, which may
286 contain the following keys/values:
Gilad Arnold1b908392012-10-05 11:36:27 -0700287 size (int): the file size in bytes
288 sha1 (string): a base64 encoded SHA1 hash
289 sha256 (string): a base64 encoded SHA256 hash
290
291 Example URL:
292 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 09:46:32 -0700293 """
294 file_path = os.path.join(updater.static_dir, *path_args)
295 if not os.path.exists(file_path):
296 raise DevServerError('file not found: %s' % file_path)
297 try:
298 file_size = os.path.getsize(file_path)
299 file_sha1 = common_util.GetFileSha1(file_path)
300 file_sha256 = common_util.GetFileSha256(file_path)
301 except os.error, e:
302 raise DevServerError('failed to get info for file %s: %s' %
Gilad Arnold29021592013-02-15 16:19:17 -0800303 (file_path, e))
304
305 is_delta = autoupdate.Autoupdate.IsDeltaFormatFile(file_path)
306
307 return json.dumps({
308 autoupdate.Autoupdate.SIZE_ATTR: file_size,
309 autoupdate.Autoupdate.SHA1_ATTR: file_sha1,
310 autoupdate.Autoupdate.SHA256_ATTR: file_sha256,
311 autoupdate.Autoupdate.ISDELTA_ATTR: is_delta
312 })
Gilad Arnold55a2a372012-10-02 09:46:32 -0700313
Chris Sosa76e44b92013-01-31 12:11:38 -0800314
David Rochberg7c79a812011-01-19 14:24:45 -0500315class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700316 """The Root Class for the Dev Server.
317
318 CherryPy works as follows:
319 For each method in this class, cherrpy interprets root/path
320 as a call to an instance of DevServerRoot->method_name. For example,
321 a call to http://myhost/build will call build. CherryPy automatically
322 parses http args and places them as keyword arguments in each method.
323 For paths http://myhost/update/dir1/dir2, you can use *args so that
324 cherrypy uses the update method and puts the extra paths in args.
325 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700326 # Method names that should not be listed on the index page.
327 _UNLISTED_METHODS = ['index', 'doc']
328
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700329 api = ApiRoot()
Chris Sosa7c931362010-10-11 19:49:01 -0700330
David Rochberg7c79a812011-01-19 14:24:45 -0500331 def __init__(self):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700332 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800333 self._telemetry_lock_dict = common_util.LockDict()
David Rochberg7c79a812011-01-19 14:24:45 -0500334
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700335 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500336 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700337 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700338 import builder
339 if self._builder is None:
340 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500341 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700342
Chris Sosacde6bf42012-05-31 18:36:39 -0700343 @staticmethod
344 def _canonicalize_archive_url(archive_url):
345 """Canonicalizes archive_url strings.
346
347 Raises:
348 DevserverError: if archive_url is not set.
349 """
350 if archive_url:
Chris Sosa76e44b92013-01-31 12:11:38 -0800351 if not archive_url.startswith('gs://'):
352 raise DevServerError("Archive URL isn't from Google Storage.")
353
Chris Sosacde6bf42012-05-31 18:36:39 -0700354 return archive_url.rstrip('/')
355 else:
356 raise DevServerError("Must specify an archive_url in the request")
357
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700358 @cherrypy.expose
Frank Farzanbcb571e2012-01-03 11:48:17 -0800359 def download(self, **kwargs):
360 """Downloads and archives full/delta payloads from Google Storage.
361
Chris Sosa76e44b92013-01-31 12:11:38 -0800362 THIS METHOD IS DEPRECATED: use stage(..., artifacts=...) instead.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700363 This methods downloads artifacts. It may download artifacts in the
364 background in which case a caller should call wait_for_status to get
365 the status of the background artifact downloads. They should use the same
366 args passed to download.
367
Frank Farzanbcb571e2012-01-03 11:48:17 -0800368 Args:
369 archive_url: Google Storage URL for the build.
370
371 Example URL:
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700372 http://myhost/download?archive_url=gs://chromeos-image-archive/
373 x86-generic/R17-1208.0.0-a1-b338
Frank Farzanbcb571e2012-01-03 11:48:17 -0800374 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800375 return self.stage(archive_url=kwargs.get('archive_url'),
376 artifacts='full_payload,test_suites,stateful')
377
378 @cherrypy.expose
379 def stage(self, **kwargs):
380 """Downloads and caches the artifacts from Google Storage URL.
381
382 Downloads and caches the artifacts Google Storage URL. Returns once these
383 have been downloaded on the devserver. A call to this will attempt to cache
384 non-specified artifacts in the background for the given from the given URL
385 following the principle of spatial locality. Spatial locality of different
386 artifacts is explicitly defined in the build_artifact module.
387
388 These artifacts will then be available from the static/ sub-directory of
389 the devserver.
390
391 Args:
392 archive_url: Google Storage URL for the build.
393 artifacts: Comma separated list of artifacts to download.
394
395 Example:
396 To download the autotest and test suites tarballs:
397 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
398 artifacts=autotest,test_suites
399 To download the full update payload:
400 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
401 artifacts=full_payload
402
403 For both these examples, one could find these artifacts at:
404 http://devserver_url:<port>/static/archive/<relative_path>*
405
406 Note for this example, relative path is the archive_url stripped of its
407 basename i.e. path/ in the examples above. Specific example:
408
409 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
410
411 Will get staged to:
412
413 http://devserver_url:<port>/static/archive/x86-mario-release/R26-3920.0.0
414 """
Chris Sosacde6bf42012-05-31 18:36:39 -0700415 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
Chris Sosa76e44b92013-01-31 12:11:38 -0800416 artifacts = kwargs.get('artifacts', '')
417 if not artifacts:
418 raise DevServerError('No artifacts specified.')
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700419
Chris Sosa76e44b92013-01-31 12:11:38 -0800420 downloader.Downloader(updater.static_dir, archive_url).Download(
421 artifacts.split(','))
422 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700423
424 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -0800425 def setup_telemetry(self, **kwargs):
426 """Extracts and sets up telemetry
427
428 This method goes through the telemetry deps packages, and stages them on
429 the devserver to be used by the drones and the telemetry tests.
430
431 Args:
432 archive_url: Google Storage URL for the build.
433
434 Returns:
435 Path to the source folder for the telemetry codebase once it is staged.
436 """
437 archive_url = kwargs.get('archive_url')
438 self.stage(archive_url=archive_url, artifacts='autotest')
439
440 build = '/'.join(downloader.Downloader.ParseUrl(archive_url))
441 build_path = os.path.join(updater.static_dir, build)
442 deps_path = os.path.join(build_path, 'autotest/packages')
443 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
444 src_folder = os.path.join(telemetry_path, 'src')
445
446 with self._telemetry_lock_dict.lock(telemetry_path):
447 if os.path.exists(src_folder):
448 # Telemetry is already fully stage return
449 return src_folder
450
451 common_util.MkDirP(telemetry_path)
452
453 # Copy over the required deps tar balls to the telemetry directory.
454 for dep in TELEMETRY_DEPS:
455 dep_path = os.path.join(deps_path, dep)
456 try:
457 common_util.ExtractTarball(dep_path, telemetry_path)
458 except common_util.CommonUtilError as e:
459 shutil.rmtree(telemetry_path)
460 raise DevServerError(str(e))
461
462 # By default all the tarballs extract to test_src but some parts of
463 # the telemetry code specifically hardcoded to exist inside of 'src'.
464 test_src = os.path.join(telemetry_path, 'test_src')
465 try:
466 shutil.move(test_src, src_folder)
467 except shutil.Error:
468 # This can occur if src_folder already exists. Remove and retry move.
469 shutil.rmtree(src_folder)
470 raise DevServerError('Failure in telemetry setup for build %s. Appears'
471 ' that the test_src to src move failed.' % build)
472
473 return src_folder
474
475 @cherrypy.expose
Chris Sosacde6bf42012-05-31 18:36:39 -0700476 def wait_for_status(self, **kwargs):
477 """Waits for background artifacts to be downloaded from Google Storage.
478
Chris Sosa76e44b92013-01-31 12:11:38 -0800479 THIS METHOD IS DEPRECATED: use stage(..., artifacts=...) instead.
Chris Sosacde6bf42012-05-31 18:36:39 -0700480 Args:
481 archive_url: Google Storage URL for the build.
482
483 Example URL:
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700484 http://myhost/wait_for_status?archive_url=gs://chromeos-image-archive/
485 x86-generic/R17-1208.0.0-a1-b338
Chris Sosacde6bf42012-05-31 18:36:39 -0700486 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800487 return self.stage(archive_url=kwargs.get('archive_url'),
488 artifacts='full_payload,test_suites,autotest,stateful')
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700489
490 @cherrypy.expose
Chris Masone816e38c2012-05-02 12:22:36 -0700491 def stage_debug(self, **kwargs):
492 """Downloads and stages debug symbol payloads from Google Storage.
493
Chris Sosa76e44b92013-01-31 12:11:38 -0800494 THIS METHOD IS DEPRECATED: use stage(..., artifacts=...) instead.
495 This methods downloads the debug symbol build artifact
496 synchronously, and then stages it for use by symbolicate_dump.
Chris Masone816e38c2012-05-02 12:22:36 -0700497
498 Args:
499 archive_url: Google Storage URL for the build.
500
501 Example URL:
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700502 http://myhost/stage_debug?archive_url=gs://chromeos-image-archive/
503 x86-generic/R17-1208.0.0-a1-b338
Chris Masone816e38c2012-05-02 12:22:36 -0700504 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800505 return self.stage(archive_url=kwargs.get('archive_url'),
506 artifacts='symbols')
Chris Masone816e38c2012-05-02 12:22:36 -0700507
508 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800509 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -0700510 """Symbolicates a minidump using pre-downloaded symbols, returns it.
511
512 Callers will need to POST to this URL with a body of MIME-type
513 "multipart/form-data".
514 The body should include a single argument, 'minidump', containing the
515 binary-formatted minidump to symbolicate.
516
Chris Masone816e38c2012-05-02 12:22:36 -0700517 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -0800518 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -0700519 minidump: The binary minidump file to symbolicate.
520 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800521 # Ensure the symbols have been staged.
522 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
523 if self.stage(archive_url=archive_url, artifacts='symbols') != 'Success':
524 raise DevServerError('Failed to stage symbols for %s' % archive_url)
525
Chris Masone816e38c2012-05-02 12:22:36 -0700526 to_return = ''
527 with tempfile.NamedTemporaryFile() as local:
528 while True:
529 data = minidump.file.read(8192)
530 if not data:
531 break
532 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -0800533
Chris Masone816e38c2012-05-02 12:22:36 -0700534 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -0800535
536 symbols_directory = os.path.join(downloader.Downloader.GetBuildDir(
537 updater.static_dir, archive_url), 'debug', 'breakpad')
538
539 stackwalk = subprocess.Popen(
540 ['minidump_stackwalk', local.name, symbols_directory],
541 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
542
Chris Masone816e38c2012-05-02 12:22:36 -0700543 to_return, error_text = stackwalk.communicate()
544 if stackwalk.returncode != 0:
545 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
546 error_text, stackwalk.returncode))
547
548 return to_return
549
550 @cherrypy.expose
Scott Zawalski16954532012-03-20 15:31:36 -0400551 def latestbuild(self, **params):
552 """Return a string representing the latest build for a given target.
553
554 Args:
555 target: The build target, typically a combination of the board and the
556 type of build e.g. x86-mario-release.
557 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
558 provided the latest RXX build will be returned.
559 Returns:
560 A string representation of the latest build if one exists, i.e.
561 R19-1993.0.0-a1-b1480.
562 An empty string if no latest could be found.
563 """
564 if not params:
565 return _PrintDocStringAsHTML(self.latestbuild)
566
567 if 'target' not in params:
568 raise cherrypy.HTTPError('500 Internal Server Error',
569 'Error: target= is required!')
570 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700571 return common_util.GetLatestBuildVersion(
Scott Zawalski16954532012-03-20 15:31:36 -0400572 updater.static_dir, params['target'],
573 milestone=params.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700574 except common_util.CommonUtilError as errmsg:
Scott Zawalski16954532012-03-20 15:31:36 -0400575 raise cherrypy.HTTPError('500 Internal Server Error', str(errmsg))
576
577 @cherrypy.expose
Scott Zawalski84a39c92012-01-13 15:12:42 -0500578 def controlfiles(self, **params):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500579 """Return a control file or a list of all known control files.
580
581 Example URL:
582 To List all control files:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500583 http://dev-server/controlfiles?board=x86-alex-release&build=R18-1514.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -0500584 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500585 http://dev-server/controlfiles?board=x86-alex-release&build=R18-1514.0.0&control_path=client/sleeptest/control
Scott Zawalski4647ce62012-01-03 17:17:28 -0500586
587 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500588 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500589 control_path: If you want the contents of a control file set this
590 to the path. E.g. client/site_tests/sleeptest/control
591 Optional, if not provided return a list of control files is returned.
592 Returns:
593 Contents of a control file if control_path is provided.
594 A list of control files if no control_path is provided.
595 """
Scott Zawalski4647ce62012-01-03 17:17:28 -0500596 if not params:
597 return _PrintDocStringAsHTML(self.controlfiles)
598
Scott Zawalski84a39c92012-01-13 15:12:42 -0500599 if 'build' not in params:
Scott Zawalski4647ce62012-01-03 17:17:28 -0500600 raise cherrypy.HTTPError('500 Internal Server Error',
Scott Zawalski84a39c92012-01-13 15:12:42 -0500601 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -0500602
603 if 'control_path' not in params:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700604 return common_util.GetControlFileList(
605 updater.static_dir, params['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -0500606 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700607 return common_util.GetControlFile(
608 updater.static_dir, params['build'], params['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -0800609
610 @cherrypy.expose
Gilad Arnold6f99b982012-09-12 10:49:40 -0700611 def stage_images(self, **kwargs):
612 """Downloads and stages a Chrome OS image from Google Storage.
613
Chris Sosa76e44b92013-01-31 12:11:38 -0800614 THIS METHOD IS DEPRECATED: use stage(..., artifacts=...) instead.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700615 This method downloads a zipped archive from a specified GS location, then
616 extracts and stages the specified list of images and stages them under
Chris Sosa76e44b92013-01-31 12:11:38 -0800617 static/BOARD/BUILD/. Download is synchronous.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700618
619 Args:
620 archive_url: Google Storage URL for the build.
621 image_types: comma-separated list of images to download, may include
622 'test', 'recovery', and 'base'
623
624 Example URL:
625 http://myhost/stage_images?archive_url=gs://chromeos-image-archive/
626 x86-generic/R17-1208.0.0-a1-b338&image_types=test,base
627 """
Gilad Arnold6f99b982012-09-12 10:49:40 -0700628 image_types = kwargs.get('image_types').split(',')
Chris Sosa76e44b92013-01-31 12:11:38 -0800629 image_types_list = [image + '_image' for image in image_types]
630 self.stage(archive_url=kwargs.get('archive_url'), artifacts=','.join(
631 image_types_list))
Gilad Arnold6f99b982012-09-12 10:49:40 -0700632
633 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700634 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700635 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700636 return ('Welcome to the Dev Server!<br>\n'
637 '<br>\n'
638 'Here are the available methods, click for documentation:<br>\n'
639 '<br>\n'
640 '%s' %
641 '<br>\n'.join(
642 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700643 for name in _FindExposedMethods(
644 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700645
646 @cherrypy.expose
647 def doc(self, *args):
648 """Shows the documentation for available methods / URLs.
649
650 Example:
651 http://myhost/doc/update
652 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700653 name = '/'.join(args)
654 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700655 if not method:
656 raise DevServerError("No exposed method named `%s'" % name)
657 if not method.__doc__:
658 raise DevServerError("No documentation for exposed method `%s'" % name)
659 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -0700660
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700661 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700662 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700663 """Handles an update check from a Chrome OS client.
664
665 The HTTP request should contain the standard Omaha-style XML blob. The URL
666 line may contain an additional intermediate path to the update payload.
667
668 Example:
669 http://myhost/update/optional/path/to/payload
670 """
Chris Sosa7c931362010-10-11 19:49:01 -0700671 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -0800672 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -0700673 data = cherrypy.request.rfile.read(body_length)
674 return updater.HandleUpdatePing(data, label)
675
Chris Sosa0356d3b2010-09-16 15:46:22 -0700676
Chris Sosadbc20082012-12-10 13:39:11 -0800677def _CleanCache(cache_dir, wipe):
678 """Wipes any excess cached items in the cache_dir.
679
680 Args:
681 cache_dir: the directory we are wiping from.
682 wipe: If True, wipe all the contents -- not just the excess.
683 """
684 if wipe:
685 # Clear the cache and exit on error.
686 cmd = 'rm -rf %s/*' % cache_dir
687 if os.system(cmd) != 0:
688 _Log('Failed to clear the cache with %s' % cmd)
689 sys.exit(1)
690 else:
691 # Clear all but the last N cached updates
692 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
693 (cache_dir, CACHED_ENTRIES))
694 if os.system(cmd) != 0:
695 _Log('Failed to clean up old delta cache files with %s' % cmd)
696 sys.exit(1)
697
698
Chris Sosacde6bf42012-05-31 18:36:39 -0700699def main():
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700700 usage = 'usage: %prog [options]'
Gilad Arnold286a0062012-01-12 13:47:02 -0800701 parser = optparse.OptionParser(usage=usage)
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700702 parser.add_option('--archive_dir',
703 metavar='PATH',
Chris Sosadbc20082012-12-10 13:39:11 -0800704 help='Enables serve-only mode. Serves archived builds only')
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700705 parser.add_option('--board',
706 help='when pre-generating update, board for latest image')
707 parser.add_option('--clear_cache',
Satoru Takabayashid733cbe2011-11-15 09:36:32 -0800708 action='store_true', default=False,
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700709 help='clear out all cached updates and exit')
710 parser.add_option('--critical_update',
711 action='store_true', default=False,
712 help='present update payload as critical')
713 parser.add_option('--data_dir',
714 metavar='PATH',
715 default=os.path.dirname(os.path.abspath(sys.argv[0])),
716 help='writable directory where static lives')
717 parser.add_option('--exit',
718 action='store_true',
719 help='do not start server (yet pregenerate/clear cache)')
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700720 parser.add_option('--for_vm',
721 dest='vm', action='store_true',
722 help='update is for a vm image')
Gilad Arnold8318eac2012-10-04 12:52:23 -0700723 parser.add_option('--host_log',
724 action='store_true', default=False,
725 help='record history of host update events (/api/hostlog)')
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700726 parser.add_option('--image',
727 metavar='FILE',
Chris Sosadbc20082012-12-10 13:39:11 -0800728 help='Force update using this image. Can only be used when '
729 'not in serve-only mode as it is used to generate a '
730 'payload.')
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700731 parser.add_option('--logfile',
732 metavar='PATH',
733 help='log output to this file instead of stdout')
Gilad Arnolda564b4b2012-10-04 10:32:44 -0700734 parser.add_option('--max_updates',
Chris Sosa76e44b92013-01-31 12:11:38 -0800735 metavar='NUM', default= -1, type='int',
Gilad Arnolda564b4b2012-10-04 10:32:44 -0700736 help='maximum number of update checks handled positively '
737 '(default: unlimited)')
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700738 parser.add_option('-p', '--pregenerate_update',
739 action='store_true', default=False,
Chris Sosadbc20082012-12-10 13:39:11 -0800740 help='pre-generate update payload. Can only be used when '
741 'not in serve-only mode as it is used to generate a '
742 'payload.')
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700743 parser.add_option('--payload',
744 metavar='PATH',
745 help='use update payload from specified directory')
746 parser.add_option('--port',
747 default=8080, type='int',
748 help='port for the dev server to use (default: 8080)')
749 parser.add_option('--private_key',
750 metavar='PATH', default=None,
751 help='path to the private key in pem format')
752 parser.add_option('--production',
753 action='store_true', default=False,
754 help='have the devserver use production values')
755 parser.add_option('--proxy_port',
756 metavar='PORT', default=None, type='int',
757 help='port to have the client connect to (testing support)')
758 parser.add_option('--remote_payload',
759 action='store_true', default=False,
760 help='Payload is being served from a remote machine')
761 parser.add_option('--src_image',
762 metavar='PATH', default='',
763 help='source image for generating delta updates from')
764 parser.add_option('-t', '--test_image',
765 action='store_true',
766 help='whether or not to use test images')
767 parser.add_option('-u', '--urlbase',
768 metavar='URL',
769 help='base URL for update images, other than the devserver')
Chris Sosa7c931362010-10-11 19:49:01 -0700770 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +0000771
Chris Sosa7c931362010-10-11 19:49:01 -0700772 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
773 root_dir = os.path.realpath('%s/../..' % devserver_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700774 serve_only = False
775
Zdenek Behan608f46c2011-02-19 00:47:16 +0100776 static_dir = os.path.realpath('%s/static' % options.data_dir)
777 os.system('mkdir -p %s' % static_dir)
778
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700779 if options.archive_dir:
Zdenek Behan608f46c2011-02-19 00:47:16 +0100780 # TODO(zbehan) Remove legacy support:
781 # archive_dir is the directory where static/archive will point.
782 # If this is an absolute path, all is fine. If someone calls this
783 # using a relative path, that is relative to src/platform/dev/.
784 # That use case is unmaintainable, but since applications use it
785 # with =./static, instead of a boolean flag, we'll make this relative
786 # to devserver_dir to keep these unbroken. For now.
787 archive_dir = options.archive_dir
788 if not os.path.isabs(archive_dir):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700789 archive_dir = os.path.realpath(os.path.join(devserver_dir, archive_dir))
Zdenek Behan608f46c2011-02-19 00:47:16 +0100790 _PrepareToServeUpdatesOnly(archive_dir, static_dir)
Zdenek Behan6d93e552011-03-02 22:35:49 +0100791 static_dir = os.path.realpath(archive_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700792 serve_only = True
Chris Sosa0356d3b2010-09-16 15:46:22 -0700793
Don Garrettf90edf02010-11-16 17:36:14 -0800794 cache_dir = os.path.join(static_dir, 'cache')
Chris Sosadbc20082012-12-10 13:39:11 -0800795 # If our devserver is only supposed to serve payloads, we shouldn't be mucking
796 # with the cache at all. If the devserver hadn't previously generated a cache
797 # and is expected, the caller is using it wrong.
798 if serve_only:
799 # Extra check to make sure we're not being called incorrectly.
800 if (options.clear_cache or options.exit or options.pregenerate_update or
801 options.board or options.image):
802 parser.error('Incompatible flags detected for serve_only mode.')
Don Garrettf90edf02010-11-16 17:36:14 -0800803
Chris Sosadbc20082012-12-10 13:39:11 -0800804 elif os.path.exists(cache_dir):
805 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -0800806 else:
807 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800808
Chris Sosadbc20082012-12-10 13:39:11 -0800809 _Log('Using cache directory %s' % cache_dir)
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700810 _Log('Data dir is %s' % options.data_dir)
811 _Log('Source root is %s' % root_dir)
812 _Log('Serving from %s' % static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +0000813
Chris Sosa6a3697f2013-01-29 16:44:43 -0800814 # We allow global use here to share with cherrypy classes.
815 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -0700816 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -0700817 updater = autoupdate.Autoupdate(
818 root_dir=root_dir,
819 static_dir=static_dir,
Chris Sosa0356d3b2010-09-16 15:46:22 -0700820 serve_only=serve_only,
Andrew de los Reyes52620802010-04-12 13:40:07 -0700821 urlbase=options.urlbase,
822 test_image=options.test_image,
Chris Sosa5d342a22010-09-28 16:54:41 -0700823 forced_image=options.image,
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700824 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -0800825 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-28 23:42:37 -0700826 src_image=options.src_image,
Chris Sosae67b78f2010-11-04 17:33:16 -0700827 vm=options.vm,
Chris Sosa08d55a22011-01-19 16:08:02 -0800828 board=options.board,
Chris Sosa0f1ec842011-02-14 16:33:22 -0800829 copy_to_static_root=not options.exit,
830 private_key=options.private_key,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -0800831 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700832 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 10:32:44 -0700833 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -0700834 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -0800835 )
Chris Sosa7c931362010-10-11 19:49:01 -0700836
Chris Sosa6a3697f2013-01-29 16:44:43 -0800837 if options.pregenerate_update:
838 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 15:46:22 -0700839
Don Garrett0c880e22010-11-17 18:13:37 -0800840 # If the command line requested after setup, it's time to do it.
841 if not options.exit:
Chris Sosa66e2d9c2012-07-11 14:14:14 -0700842 # Handle options that must be set globally in cherrypy.
Chris Sosa2f1c41e2012-07-10 14:32:33 -0700843 if options.production:
Chris Sosa66e2d9c2012-07-11 14:14:14 -0700844 cherrypy.config.update({'environment': 'production'})
845 if not options.logfile:
846 cherrypy.config.update({'log.screen': True})
847 else:
848 cherrypy.config.update({'log.error_file': options.logfile,
849 'log.access_file': options.logfile})
Chris Sosa2f1c41e2012-07-10 14:32:33 -0700850
Don Garrett0c880e22010-11-17 18:13:37 -0800851 cherrypy.quickstart(DevServerRoot(), config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -0700852
853
854if __name__ == '__main__':
855 main()