blob: 6621e53144ef349eced7afe668a513e3f047b9ff [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' %
303 (file_path, str(e)))
304 return json.dumps(
305 {'size': file_size, 'sha1': file_sha1, 'sha256': file_sha256})
306
Chris Sosa76e44b92013-01-31 12:11:38 -0800307
David Rochberg7c79a812011-01-19 14:24:45 -0500308class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700309 """The Root Class for the Dev Server.
310
311 CherryPy works as follows:
312 For each method in this class, cherrpy interprets root/path
313 as a call to an instance of DevServerRoot->method_name. For example,
314 a call to http://myhost/build will call build. CherryPy automatically
315 parses http args and places them as keyword arguments in each method.
316 For paths http://myhost/update/dir1/dir2, you can use *args so that
317 cherrypy uses the update method and puts the extra paths in args.
318 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700319 # Method names that should not be listed on the index page.
320 _UNLISTED_METHODS = ['index', 'doc']
321
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700322 api = ApiRoot()
Chris Sosa7c931362010-10-11 19:49:01 -0700323
David Rochberg7c79a812011-01-19 14:24:45 -0500324 def __init__(self):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700325 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800326 self._telemetry_lock_dict = common_util.LockDict()
David Rochberg7c79a812011-01-19 14:24:45 -0500327
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700328 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500329 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700330 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700331 import builder
332 if self._builder is None:
333 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500334 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700335
Chris Sosacde6bf42012-05-31 18:36:39 -0700336 @staticmethod
337 def _canonicalize_archive_url(archive_url):
338 """Canonicalizes archive_url strings.
339
340 Raises:
341 DevserverError: if archive_url is not set.
342 """
343 if archive_url:
Chris Sosa76e44b92013-01-31 12:11:38 -0800344 if not archive_url.startswith('gs://'):
345 raise DevServerError("Archive URL isn't from Google Storage.")
346
Chris Sosacde6bf42012-05-31 18:36:39 -0700347 return archive_url.rstrip('/')
348 else:
349 raise DevServerError("Must specify an archive_url in the request")
350
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700351 @cherrypy.expose
Frank Farzanbcb571e2012-01-03 11:48:17 -0800352 def download(self, **kwargs):
353 """Downloads and archives full/delta payloads from Google Storage.
354
Chris Sosa76e44b92013-01-31 12:11:38 -0800355 THIS METHOD IS DEPRECATED: use stage(..., artifacts=...) instead.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700356 This methods downloads artifacts. It may download artifacts in the
357 background in which case a caller should call wait_for_status to get
358 the status of the background artifact downloads. They should use the same
359 args passed to download.
360
Frank Farzanbcb571e2012-01-03 11:48:17 -0800361 Args:
362 archive_url: Google Storage URL for the build.
363
364 Example URL:
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700365 http://myhost/download?archive_url=gs://chromeos-image-archive/
366 x86-generic/R17-1208.0.0-a1-b338
Frank Farzanbcb571e2012-01-03 11:48:17 -0800367 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800368 return self.stage(archive_url=kwargs.get('archive_url'),
369 artifacts='full_payload,test_suites,stateful')
370
371 @cherrypy.expose
372 def stage(self, **kwargs):
373 """Downloads and caches the artifacts from Google Storage URL.
374
375 Downloads and caches the artifacts Google Storage URL. Returns once these
376 have been downloaded on the devserver. A call to this will attempt to cache
377 non-specified artifacts in the background for the given from the given URL
378 following the principle of spatial locality. Spatial locality of different
379 artifacts is explicitly defined in the build_artifact module.
380
381 These artifacts will then be available from the static/ sub-directory of
382 the devserver.
383
384 Args:
385 archive_url: Google Storage URL for the build.
386 artifacts: Comma separated list of artifacts to download.
387
388 Example:
389 To download the autotest and test suites tarballs:
390 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
391 artifacts=autotest,test_suites
392 To download the full update payload:
393 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
394 artifacts=full_payload
395
396 For both these examples, one could find these artifacts at:
397 http://devserver_url:<port>/static/archive/<relative_path>*
398
399 Note for this example, relative path is the archive_url stripped of its
400 basename i.e. path/ in the examples above. Specific example:
401
402 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
403
404 Will get staged to:
405
406 http://devserver_url:<port>/static/archive/x86-mario-release/R26-3920.0.0
407 """
Chris Sosacde6bf42012-05-31 18:36:39 -0700408 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
Chris Sosa76e44b92013-01-31 12:11:38 -0800409 artifacts = kwargs.get('artifacts', '')
410 if not artifacts:
411 raise DevServerError('No artifacts specified.')
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700412
Chris Sosa76e44b92013-01-31 12:11:38 -0800413 downloader.Downloader(updater.static_dir, archive_url).Download(
414 artifacts.split(','))
415 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700416
417 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -0800418 def setup_telemetry(self, **kwargs):
419 """Extracts and sets up telemetry
420
421 This method goes through the telemetry deps packages, and stages them on
422 the devserver to be used by the drones and the telemetry tests.
423
424 Args:
425 archive_url: Google Storage URL for the build.
426
427 Returns:
428 Path to the source folder for the telemetry codebase once it is staged.
429 """
430 archive_url = kwargs.get('archive_url')
431 self.stage(archive_url=archive_url, artifacts='autotest')
432
433 build = '/'.join(downloader.Downloader.ParseUrl(archive_url))
434 build_path = os.path.join(updater.static_dir, build)
435 deps_path = os.path.join(build_path, 'autotest/packages')
436 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
437 src_folder = os.path.join(telemetry_path, 'src')
438
439 with self._telemetry_lock_dict.lock(telemetry_path):
440 if os.path.exists(src_folder):
441 # Telemetry is already fully stage return
442 return src_folder
443
444 common_util.MkDirP(telemetry_path)
445
446 # Copy over the required deps tar balls to the telemetry directory.
447 for dep in TELEMETRY_DEPS:
448 dep_path = os.path.join(deps_path, dep)
449 try:
450 common_util.ExtractTarball(dep_path, telemetry_path)
451 except common_util.CommonUtilError as e:
452 shutil.rmtree(telemetry_path)
453 raise DevServerError(str(e))
454
455 # By default all the tarballs extract to test_src but some parts of
456 # the telemetry code specifically hardcoded to exist inside of 'src'.
457 test_src = os.path.join(telemetry_path, 'test_src')
458 try:
459 shutil.move(test_src, src_folder)
460 except shutil.Error:
461 # This can occur if src_folder already exists. Remove and retry move.
462 shutil.rmtree(src_folder)
463 raise DevServerError('Failure in telemetry setup for build %s. Appears'
464 ' that the test_src to src move failed.' % build)
465
466 return src_folder
467
468 @cherrypy.expose
Chris Sosacde6bf42012-05-31 18:36:39 -0700469 def wait_for_status(self, **kwargs):
470 """Waits for background artifacts to be downloaded from Google Storage.
471
Chris Sosa76e44b92013-01-31 12:11:38 -0800472 THIS METHOD IS DEPRECATED: use stage(..., artifacts=...) instead.
Chris Sosacde6bf42012-05-31 18:36:39 -0700473 Args:
474 archive_url: Google Storage URL for the build.
475
476 Example URL:
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700477 http://myhost/wait_for_status?archive_url=gs://chromeos-image-archive/
478 x86-generic/R17-1208.0.0-a1-b338
Chris Sosacde6bf42012-05-31 18:36:39 -0700479 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800480 return self.stage(archive_url=kwargs.get('archive_url'),
481 artifacts='full_payload,test_suites,autotest,stateful')
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700482
483 @cherrypy.expose
Chris Masone816e38c2012-05-02 12:22:36 -0700484 def stage_debug(self, **kwargs):
485 """Downloads and stages debug symbol payloads from Google Storage.
486
Chris Sosa76e44b92013-01-31 12:11:38 -0800487 THIS METHOD IS DEPRECATED: use stage(..., artifacts=...) instead.
488 This methods downloads the debug symbol build artifact
489 synchronously, and then stages it for use by symbolicate_dump.
Chris Masone816e38c2012-05-02 12:22:36 -0700490
491 Args:
492 archive_url: Google Storage URL for the build.
493
494 Example URL:
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700495 http://myhost/stage_debug?archive_url=gs://chromeos-image-archive/
496 x86-generic/R17-1208.0.0-a1-b338
Chris Masone816e38c2012-05-02 12:22:36 -0700497 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800498 return self.stage(archive_url=kwargs.get('archive_url'),
499 artifacts='symbols')
Chris Masone816e38c2012-05-02 12:22:36 -0700500
501 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800502 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -0700503 """Symbolicates a minidump using pre-downloaded symbols, returns it.
504
505 Callers will need to POST to this URL with a body of MIME-type
506 "multipart/form-data".
507 The body should include a single argument, 'minidump', containing the
508 binary-formatted minidump to symbolicate.
509
Chris Masone816e38c2012-05-02 12:22:36 -0700510 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -0800511 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -0700512 minidump: The binary minidump file to symbolicate.
513 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800514 # Ensure the symbols have been staged.
515 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
516 if self.stage(archive_url=archive_url, artifacts='symbols') != 'Success':
517 raise DevServerError('Failed to stage symbols for %s' % archive_url)
518
Chris Masone816e38c2012-05-02 12:22:36 -0700519 to_return = ''
520 with tempfile.NamedTemporaryFile() as local:
521 while True:
522 data = minidump.file.read(8192)
523 if not data:
524 break
525 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -0800526
Chris Masone816e38c2012-05-02 12:22:36 -0700527 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -0800528
529 symbols_directory = os.path.join(downloader.Downloader.GetBuildDir(
530 updater.static_dir, archive_url), 'debug', 'breakpad')
531
532 stackwalk = subprocess.Popen(
533 ['minidump_stackwalk', local.name, symbols_directory],
534 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
535
Chris Masone816e38c2012-05-02 12:22:36 -0700536 to_return, error_text = stackwalk.communicate()
537 if stackwalk.returncode != 0:
538 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
539 error_text, stackwalk.returncode))
540
541 return to_return
542
543 @cherrypy.expose
Scott Zawalski16954532012-03-20 15:31:36 -0400544 def latestbuild(self, **params):
545 """Return a string representing the latest build for a given target.
546
547 Args:
548 target: The build target, typically a combination of the board and the
549 type of build e.g. x86-mario-release.
550 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
551 provided the latest RXX build will be returned.
552 Returns:
553 A string representation of the latest build if one exists, i.e.
554 R19-1993.0.0-a1-b1480.
555 An empty string if no latest could be found.
556 """
557 if not params:
558 return _PrintDocStringAsHTML(self.latestbuild)
559
560 if 'target' not in params:
561 raise cherrypy.HTTPError('500 Internal Server Error',
562 'Error: target= is required!')
563 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700564 return common_util.GetLatestBuildVersion(
Scott Zawalski16954532012-03-20 15:31:36 -0400565 updater.static_dir, params['target'],
566 milestone=params.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700567 except common_util.CommonUtilError as errmsg:
Scott Zawalski16954532012-03-20 15:31:36 -0400568 raise cherrypy.HTTPError('500 Internal Server Error', str(errmsg))
569
570 @cherrypy.expose
Scott Zawalski84a39c92012-01-13 15:12:42 -0500571 def controlfiles(self, **params):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500572 """Return a control file or a list of all known control files.
573
574 Example URL:
575 To List all control files:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500576 http://dev-server/controlfiles?board=x86-alex-release&build=R18-1514.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -0500577 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500578 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 -0500579
580 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500581 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500582 control_path: If you want the contents of a control file set this
583 to the path. E.g. client/site_tests/sleeptest/control
584 Optional, if not provided return a list of control files is returned.
585 Returns:
586 Contents of a control file if control_path is provided.
587 A list of control files if no control_path is provided.
588 """
Scott Zawalski4647ce62012-01-03 17:17:28 -0500589 if not params:
590 return _PrintDocStringAsHTML(self.controlfiles)
591
Scott Zawalski84a39c92012-01-13 15:12:42 -0500592 if 'build' not in params:
Scott Zawalski4647ce62012-01-03 17:17:28 -0500593 raise cherrypy.HTTPError('500 Internal Server Error',
Scott Zawalski84a39c92012-01-13 15:12:42 -0500594 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -0500595
596 if 'control_path' not in params:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700597 return common_util.GetControlFileList(
598 updater.static_dir, params['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -0500599 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700600 return common_util.GetControlFile(
601 updater.static_dir, params['build'], params['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -0800602
603 @cherrypy.expose
Gilad Arnold6f99b982012-09-12 10:49:40 -0700604 def stage_images(self, **kwargs):
605 """Downloads and stages a Chrome OS image from Google Storage.
606
Chris Sosa76e44b92013-01-31 12:11:38 -0800607 THIS METHOD IS DEPRECATED: use stage(..., artifacts=...) instead.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700608 This method downloads a zipped archive from a specified GS location, then
609 extracts and stages the specified list of images and stages them under
Chris Sosa76e44b92013-01-31 12:11:38 -0800610 static/BOARD/BUILD/. Download is synchronous.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700611
612 Args:
613 archive_url: Google Storage URL for the build.
614 image_types: comma-separated list of images to download, may include
615 'test', 'recovery', and 'base'
616
617 Example URL:
618 http://myhost/stage_images?archive_url=gs://chromeos-image-archive/
619 x86-generic/R17-1208.0.0-a1-b338&image_types=test,base
620 """
Gilad Arnold6f99b982012-09-12 10:49:40 -0700621 image_types = kwargs.get('image_types').split(',')
Chris Sosa76e44b92013-01-31 12:11:38 -0800622 image_types_list = [image + '_image' for image in image_types]
623 self.stage(archive_url=kwargs.get('archive_url'), artifacts=','.join(
624 image_types_list))
Gilad Arnold6f99b982012-09-12 10:49:40 -0700625
626 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700627 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700628 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700629 return ('Welcome to the Dev Server!<br>\n'
630 '<br>\n'
631 'Here are the available methods, click for documentation:<br>\n'
632 '<br>\n'
633 '%s' %
634 '<br>\n'.join(
635 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700636 for name in _FindExposedMethods(
637 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700638
639 @cherrypy.expose
640 def doc(self, *args):
641 """Shows the documentation for available methods / URLs.
642
643 Example:
644 http://myhost/doc/update
645 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700646 name = '/'.join(args)
647 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700648 if not method:
649 raise DevServerError("No exposed method named `%s'" % name)
650 if not method.__doc__:
651 raise DevServerError("No documentation for exposed method `%s'" % name)
652 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -0700653
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700654 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700655 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700656 """Handles an update check from a Chrome OS client.
657
658 The HTTP request should contain the standard Omaha-style XML blob. The URL
659 line may contain an additional intermediate path to the update payload.
660
661 Example:
662 http://myhost/update/optional/path/to/payload
663 """
Chris Sosa7c931362010-10-11 19:49:01 -0700664 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -0800665 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -0700666 data = cherrypy.request.rfile.read(body_length)
667 return updater.HandleUpdatePing(data, label)
668
Chris Sosa0356d3b2010-09-16 15:46:22 -0700669
Chris Sosadbc20082012-12-10 13:39:11 -0800670def _CleanCache(cache_dir, wipe):
671 """Wipes any excess cached items in the cache_dir.
672
673 Args:
674 cache_dir: the directory we are wiping from.
675 wipe: If True, wipe all the contents -- not just the excess.
676 """
677 if wipe:
678 # Clear the cache and exit on error.
679 cmd = 'rm -rf %s/*' % cache_dir
680 if os.system(cmd) != 0:
681 _Log('Failed to clear the cache with %s' % cmd)
682 sys.exit(1)
683 else:
684 # Clear all but the last N cached updates
685 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
686 (cache_dir, CACHED_ENTRIES))
687 if os.system(cmd) != 0:
688 _Log('Failed to clean up old delta cache files with %s' % cmd)
689 sys.exit(1)
690
691
Chris Sosacde6bf42012-05-31 18:36:39 -0700692def main():
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700693 usage = 'usage: %prog [options]'
Gilad Arnold286a0062012-01-12 13:47:02 -0800694 parser = optparse.OptionParser(usage=usage)
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700695 parser.add_option('--archive_dir',
696 metavar='PATH',
Chris Sosadbc20082012-12-10 13:39:11 -0800697 help='Enables serve-only mode. Serves archived builds only')
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700698 parser.add_option('--board',
699 help='when pre-generating update, board for latest image')
700 parser.add_option('--clear_cache',
Satoru Takabayashid733cbe2011-11-15 09:36:32 -0800701 action='store_true', default=False,
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700702 help='clear out all cached updates and exit')
703 parser.add_option('--critical_update',
704 action='store_true', default=False,
705 help='present update payload as critical')
706 parser.add_option('--data_dir',
707 metavar='PATH',
708 default=os.path.dirname(os.path.abspath(sys.argv[0])),
709 help='writable directory where static lives')
710 parser.add_option('--exit',
711 action='store_true',
712 help='do not start server (yet pregenerate/clear cache)')
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700713 parser.add_option('--for_vm',
714 dest='vm', action='store_true',
715 help='update is for a vm image')
Gilad Arnold8318eac2012-10-04 12:52:23 -0700716 parser.add_option('--host_log',
717 action='store_true', default=False,
718 help='record history of host update events (/api/hostlog)')
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700719 parser.add_option('--image',
720 metavar='FILE',
Chris Sosadbc20082012-12-10 13:39:11 -0800721 help='Force update using this image. Can only be used when '
722 'not in serve-only mode as it is used to generate a '
723 'payload.')
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700724 parser.add_option('--logfile',
725 metavar='PATH',
726 help='log output to this file instead of stdout')
Gilad Arnolda564b4b2012-10-04 10:32:44 -0700727 parser.add_option('--max_updates',
Chris Sosa76e44b92013-01-31 12:11:38 -0800728 metavar='NUM', default= -1, type='int',
Gilad Arnolda564b4b2012-10-04 10:32:44 -0700729 help='maximum number of update checks handled positively '
730 '(default: unlimited)')
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700731 parser.add_option('-p', '--pregenerate_update',
732 action='store_true', default=False,
Chris Sosadbc20082012-12-10 13:39:11 -0800733 help='pre-generate update payload. Can only be used when '
734 'not in serve-only mode as it is used to generate a '
735 'payload.')
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700736 parser.add_option('--payload',
737 metavar='PATH',
738 help='use update payload from specified directory')
739 parser.add_option('--port',
740 default=8080, type='int',
741 help='port for the dev server to use (default: 8080)')
742 parser.add_option('--private_key',
743 metavar='PATH', default=None,
744 help='path to the private key in pem format')
745 parser.add_option('--production',
746 action='store_true', default=False,
747 help='have the devserver use production values')
748 parser.add_option('--proxy_port',
749 metavar='PORT', default=None, type='int',
750 help='port to have the client connect to (testing support)')
751 parser.add_option('--remote_payload',
752 action='store_true', default=False,
753 help='Payload is being served from a remote machine')
754 parser.add_option('--src_image',
755 metavar='PATH', default='',
756 help='source image for generating delta updates from')
757 parser.add_option('-t', '--test_image',
758 action='store_true',
759 help='whether or not to use test images')
760 parser.add_option('-u', '--urlbase',
761 metavar='URL',
762 help='base URL for update images, other than the devserver')
Chris Sosa7c931362010-10-11 19:49:01 -0700763 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +0000764
Chris Sosa7c931362010-10-11 19:49:01 -0700765 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
766 root_dir = os.path.realpath('%s/../..' % devserver_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700767 serve_only = False
768
Zdenek Behan608f46c2011-02-19 00:47:16 +0100769 static_dir = os.path.realpath('%s/static' % options.data_dir)
770 os.system('mkdir -p %s' % static_dir)
771
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700772 if options.archive_dir:
Zdenek Behan608f46c2011-02-19 00:47:16 +0100773 # TODO(zbehan) Remove legacy support:
774 # archive_dir is the directory where static/archive will point.
775 # If this is an absolute path, all is fine. If someone calls this
776 # using a relative path, that is relative to src/platform/dev/.
777 # That use case is unmaintainable, but since applications use it
778 # with =./static, instead of a boolean flag, we'll make this relative
779 # to devserver_dir to keep these unbroken. For now.
780 archive_dir = options.archive_dir
781 if not os.path.isabs(archive_dir):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700782 archive_dir = os.path.realpath(os.path.join(devserver_dir, archive_dir))
Zdenek Behan608f46c2011-02-19 00:47:16 +0100783 _PrepareToServeUpdatesOnly(archive_dir, static_dir)
Zdenek Behan6d93e552011-03-02 22:35:49 +0100784 static_dir = os.path.realpath(archive_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700785 serve_only = True
Chris Sosa0356d3b2010-09-16 15:46:22 -0700786
Don Garrettf90edf02010-11-16 17:36:14 -0800787 cache_dir = os.path.join(static_dir, 'cache')
Chris Sosadbc20082012-12-10 13:39:11 -0800788 # If our devserver is only supposed to serve payloads, we shouldn't be mucking
789 # with the cache at all. If the devserver hadn't previously generated a cache
790 # and is expected, the caller is using it wrong.
791 if serve_only:
792 # Extra check to make sure we're not being called incorrectly.
793 if (options.clear_cache or options.exit or options.pregenerate_update or
794 options.board or options.image):
795 parser.error('Incompatible flags detected for serve_only mode.')
Don Garrettf90edf02010-11-16 17:36:14 -0800796
Chris Sosadbc20082012-12-10 13:39:11 -0800797 elif os.path.exists(cache_dir):
798 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -0800799 else:
800 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800801
Chris Sosadbc20082012-12-10 13:39:11 -0800802 _Log('Using cache directory %s' % cache_dir)
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700803 _Log('Data dir is %s' % options.data_dir)
804 _Log('Source root is %s' % root_dir)
805 _Log('Serving from %s' % static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +0000806
Chris Sosa6a3697f2013-01-29 16:44:43 -0800807 # We allow global use here to share with cherrypy classes.
808 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -0700809 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -0700810 updater = autoupdate.Autoupdate(
811 root_dir=root_dir,
812 static_dir=static_dir,
Chris Sosa0356d3b2010-09-16 15:46:22 -0700813 serve_only=serve_only,
Andrew de los Reyes52620802010-04-12 13:40:07 -0700814 urlbase=options.urlbase,
815 test_image=options.test_image,
Chris Sosa5d342a22010-09-28 16:54:41 -0700816 forced_image=options.image,
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700817 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -0800818 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-28 23:42:37 -0700819 src_image=options.src_image,
Chris Sosae67b78f2010-11-04 17:33:16 -0700820 vm=options.vm,
Chris Sosa08d55a22011-01-19 16:08:02 -0800821 board=options.board,
Chris Sosa0f1ec842011-02-14 16:33:22 -0800822 copy_to_static_root=not options.exit,
823 private_key=options.private_key,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -0800824 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700825 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 10:32:44 -0700826 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -0700827 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -0800828 )
Chris Sosa7c931362010-10-11 19:49:01 -0700829
Chris Sosa6a3697f2013-01-29 16:44:43 -0800830 if options.pregenerate_update:
831 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 15:46:22 -0700832
Don Garrett0c880e22010-11-17 18:13:37 -0800833 # If the command line requested after setup, it's time to do it.
834 if not options.exit:
Chris Sosa66e2d9c2012-07-11 14:14:14 -0700835 # Handle options that must be set globally in cherrypy.
Chris Sosa2f1c41e2012-07-10 14:32:33 -0700836 if options.production:
Chris Sosa66e2d9c2012-07-11 14:14:14 -0700837 cherrypy.config.update({'environment': 'production'})
838 if not options.logfile:
839 cherrypy.config.update({'log.screen': True})
840 else:
841 cherrypy.config.update({'log.error_file': options.logfile,
842 'log.access_file': options.logfile})
Chris Sosa2f1c41e2012-07-10 14:32:33 -0700843
Don Garrett0c880e22010-11-17 18:13:37 -0800844 cherrypy.quickstart(DevServerRoot(), config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -0700845
846
847if __name__ == '__main__':
848 main()