blob: dff388e68318c8f216c26bc0db22838b04632e78 [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 Sosa3ae4dc12013-03-29 11:47:00 -07007"""Chromium OS development server that can be used for all forms of update.
8
9This devserver can be used to perform system-wide autoupdate and update
10of specific portage packages on devices running Chromium OS derived operating
11systems. It mainly operates in two modes:
12
131) archive mode: In this mode, the devserver is configured to stage and
14serve artifacts from Google Storage using the credentials provided to it before
15it is run. The easiest way to understand this is that the devserver is
16functioning as a local cache for artifacts produced and uploaded by build
17servers. Users of this form of devserver can either download the artifacts
18from the devservers static directory OR use the update RPC to perform a
19system-wide autoupdate. Archive mode is always active.
20
212) artifact-generation mode: in this mode, the devserver will attempt to
22generate update payloads and build artifacts when requested. This mode only
23works in the Chromium OS chroot as it uses build tools only present in the
24chroot (emerge, cros_generate_update_payload, etc.). By default, when a device
25requests an update from this form of devserver, the devserver will attempt to
26discover if a more recent build of the board has been built by the developer
27and generate a payload that the requested system can autoupdate to. In addition,
28it accepts gmerge requests from devices that will stage the newest version of
joychen84d13772013-08-06 09:17:23 -070029a particular package from a developer's chroot onto a requesting device.
Chris Sosa3ae4dc12013-03-29 11:47:00 -070030
31For example:
32gmerge gmerge -d <devserver_url>
33
34devserver will see if a newer package of gmerge is available. If gmerge is
35cros_work'd on, it will re-build gmerge. After this, gmerge will install that
36version of gmerge that the devserver just created/found.
37
38For autoupdates, there are many more advanced options that can help specify
39how to update and which payload to give to a requester.
40"""
41
Chris Sosa7c931362010-10-11 19:49:01 -070042
Gilad Arnold55a2a372012-10-02 09:46:32 -070043import json
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070044import optparse
rtc@google.comded22402009-10-26 22:36:21 +000045import os
Scott Zawalski4647ce62012-01-03 17:17:28 -050046import re
Simran Basi4baad082013-02-14 13:39:18 -080047import shutil
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -080048import socket
Chris Masone816e38c2012-05-02 12:22:36 -070049import subprocess
J. Richard Barnette3d977b82013-04-23 11:05:19 -070050import sys
Chris Masone816e38c2012-05-02 12:22:36 -070051import tempfile
Dan Shi59ae7092013-06-04 14:37:27 -070052import threading
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -070053import types
J. Richard Barnette3d977b82013-04-23 11:05:19 -070054from logging import handlers
55
56import cherrypy
Chris Sosa855b8932013-08-21 13:24:55 -070057from cherrypy import _cplogging as cplogging
58from cherrypy.process import plugins
rtc@google.comded22402009-10-26 22:36:21 +000059
Chris Sosa0356d3b2010-09-16 15:46:22 -070060import autoupdate
Chris Sosa75490802013-09-30 17:21:45 -070061import build_artifact
Gilad Arnoldc65330c2012-09-20 15:17:48 -070062import common_util
Chris Sosa47a7d4e2012-03-28 11:26:55 -070063import downloader
Chris Sosa7cd23202013-10-15 17:22:57 -070064import gsutil_util
Gilad Arnoldc65330c2012-09-20 15:17:48 -070065import log_util
joychen3cb228e2013-06-12 12:13:13 -070066import xbuddy
Gilad Arnoldc65330c2012-09-20 15:17:48 -070067
Gilad Arnoldc65330c2012-09-20 15:17:48 -070068# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080069def _Log(message, *args):
70 return log_util.LogWithTag('DEVSERVER', message, *args)
Chris Sosa0356d3b2010-09-16 15:46:22 -070071
Frank Farzan40160872011-12-12 18:39:18 -080072
Chris Sosa417e55d2011-01-25 16:40:48 -080073CACHED_ENTRIES = 12
Don Garrettf90edf02010-11-16 17:36:14 -080074
Simran Basi4baad082013-02-14 13:39:18 -080075TELEMETRY_FOLDER = 'telemetry_src'
76TELEMETRY_DEPS = ['dep-telemetry_dep.tar.bz2',
77 'dep-page_cycler_dep.tar.bz2',
Simran Basi0d078682013-03-22 16:40:04 -070078 'dep-chrome_test.tar.bz2',
79 'dep-perf_data_dep.tar.bz2']
Simran Basi4baad082013-02-14 13:39:18 -080080
Chris Sosa0356d3b2010-09-16 15:46:22 -070081# Sets up global to share between classes.
rtc@google.com21a5ca32009-11-04 18:23:23 +000082updater = None
rtc@google.comded22402009-10-26 22:36:21 +000083
J. Richard Barnette3d977b82013-04-23 11:05:19 -070084# Log rotation parameters. These settings correspond to once a week
J. Richard Barnette6dfa5342013-06-04 11:48:56 -070085# at midnight between Friday and Saturday, with about three months
86# of old logs kept for backup.
J. Richard Barnette3d977b82013-04-23 11:05:19 -070087#
88# For more, see the documentation for
89# logging.handlers.TimedRotatingFileHandler
J. Richard Barnette6dfa5342013-06-04 11:48:56 -070090_LOG_ROTATION_TIME = 'W4'
J. Richard Barnette3d977b82013-04-23 11:05:19 -070091_LOG_ROTATION_BACKUP = 13
92
Frank Farzan40160872011-12-12 18:39:18 -080093
Chris Sosa9164ca32012-03-28 11:04:50 -070094class DevServerError(Exception):
Chris Sosa47a7d4e2012-03-28 11:26:55 -070095 """Exception class used by this module."""
Chris Sosa47a7d4e2012-03-28 11:26:55 -070096
97
Don Garrett8ccab732013-08-30 09:13:59 -070098class DevServerHTTPError(cherrypy.HTTPError):
beepsd76c6092013-08-28 22:23:30 -070099 """Exception class to log the HTTPResponse before routing it to cherrypy."""
100 def __init__(self, status, message):
101 """
102 @param status: HTTPResponse status.
103 @param message: Message associated with the response.
104 """
Don Garrett8ccab732013-08-30 09:13:59 -0700105 cherrypy.HTTPError.__init__(self, status, message)
beepsd76c6092013-08-28 22:23:30 -0700106 _Log('HTTPError status: %s message: %s', status, message)
beepsd76c6092013-08-28 22:23:30 -0700107
108
Scott Zawalski4647ce62012-01-03 17:17:28 -0500109def _LeadingWhiteSpaceCount(string):
110 """Count the amount of leading whitespace in a string.
111
112 Args:
113 string: The string to count leading whitespace in.
114 Returns:
115 number of white space chars before characters start.
116 """
117 matched = re.match('^\s+', string)
118 if matched:
119 return len(matched.group())
120
121 return 0
122
123
124def _PrintDocStringAsHTML(func):
125 """Make a functions docstring somewhat HTML style.
126
127 Args:
128 func: The function to return the docstring from.
129 Returns:
130 A string that is somewhat formated for a web browser.
131 """
132 # TODO(scottz): Make this parse Args/Returns in a prettier way.
133 # Arguments could be bolded and indented etc.
134 html_doc = []
135 for line in func.__doc__.splitlines():
136 leading_space = _LeadingWhiteSpaceCount(line)
137 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700138 line = '&nbsp;' * leading_space + line
Scott Zawalski4647ce62012-01-03 17:17:28 -0500139
140 html_doc.append('<BR>%s' % line)
141
142 return '\n'.join(html_doc)
143
144
Chris Sosa7c931362010-10-11 19:49:01 -0700145def _GetConfig(options):
146 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800147
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800148 socket_host = '::'
Yu-Ju Hongc8d4af32013-11-12 15:14:26 -0800149 # Fall back to IPv4 when python is not configured with IPv6.
150 if not socket.has_ipv6:
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800151 socket_host = '0.0.0.0'
152
Chris Sosa7c931362010-10-11 19:49:01 -0700153 base_config = { 'global':
154 { 'server.log_request_headers': True,
155 'server.protocol_version': 'HTTP/1.1',
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800156 'server.socket_host': socket_host,
Chris Sosa7c931362010-10-11 19:49:01 -0700157 'server.socket_port': int(options.port),
Chris Sosa374c62d2010-10-14 09:13:54 -0700158 'response.timeout': 6000,
Chris Sosa6fe23942012-07-02 15:44:46 -0700159 'request.show_tracebacks': True,
Chris Sosa72333d12012-06-13 11:28:05 -0700160 'server.socket_timeout': 60,
joychenecc02aa2013-07-17 18:27:35 -0700161 'server.thread_pool': 2,
Chris Sosa7c931362010-10-11 19:49:01 -0700162 },
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700163 '/api':
164 {
165 # Gets rid of cherrypy parsing post file for args.
166 'request.process_request_body': False,
167 },
Chris Sosaa1ef0102010-10-21 16:22:35 -0700168 '/build':
169 {
170 'response.timeout': 100000,
171 },
Chris Sosa7c931362010-10-11 19:49:01 -0700172 '/update':
173 {
174 # Gets rid of cherrypy parsing post file for args.
175 'request.process_request_body': False,
Chris Sosaf65f4b92010-10-21 15:57:51 -0700176 'response.timeout': 10000,
Chris Sosa7c931362010-10-11 19:49:01 -0700177 },
178 # Sets up the static dir for file hosting.
179 '/static':
joychened64b222013-06-21 16:39:34 -0700180 { 'tools.staticdir.dir': options.static_dir,
Chris Sosa7c931362010-10-11 19:49:01 -0700181 'tools.staticdir.on': True,
Chris Sosaf65f4b92010-10-21 15:57:51 -0700182 'response.timeout': 10000,
Chris Sosa7c931362010-10-11 19:49:01 -0700183 },
184 }
Chris Sosa5f118ef2012-07-12 11:37:50 -0700185 if options.production:
Alex Miller93beca52013-07-30 19:25:09 -0700186 base_config['global'].update({'server.thread_pool': 150})
Chris Sosa7cd23202013-10-15 17:22:57 -0700187 # TODO(sosa): Do this more cleanly.
188 gsutil_util.GSUTIL_ATTEMPTS = 5
Scott Zawalski1c5e7cd2012-02-27 13:12:52 -0500189
Chris Sosa7c931362010-10-11 19:49:01 -0700190 return base_config
rtc@google.com64244662009-11-12 00:52:08 +0000191
Darin Petkove17164a2010-08-11 13:24:41 -0700192
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700193def _GetRecursiveMemberObject(root, member_list):
194 """Returns an object corresponding to a nested member list.
195
196 Args:
197 root: the root object to search
198 member_list: list of nested members to search
199 Returns:
200 An object corresponding to the member name list; None otherwise.
201 """
202 for member in member_list:
203 next_root = root.__class__.__dict__.get(member)
204 if not next_root:
205 return None
206 root = next_root
207 return root
208
209
210def _IsExposed(name):
211 """Returns True iff |name| has an `exposed' attribute and it is set."""
212 return hasattr(name, 'exposed') and name.exposed
213
214
Gilad Arnold748c8322012-10-12 09:51:35 -0700215def _GetExposedMethod(root, nested_member, ignored=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700216 """Returns a CherryPy-exposed method, if such exists.
217
218 Args:
219 root: the root object for searching
220 nested_member: a slash-joined path to the nested member
221 ignored: method paths to be ignored
222 Returns:
223 A function object corresponding to the path defined by |member_list| from
224 the |root| object, if the function is exposed and not ignored; None
225 otherwise.
226 """
Gilad Arnold748c8322012-10-12 09:51:35 -0700227 method = (not (ignored and nested_member in ignored) and
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700228 _GetRecursiveMemberObject(root, nested_member.split('/')))
229 if (method and type(method) == types.FunctionType and _IsExposed(method)):
230 return method
231
232
Gilad Arnold748c8322012-10-12 09:51:35 -0700233def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700234 """Finds exposed CherryPy methods.
235
236 Args:
237 root: the root object for searching
238 prefix: slash-joined chain of members leading to current object
239 unlisted: URLs to be excluded regardless of their exposed status
240 Returns:
241 List of exposed URLs that are not unlisted.
242 """
243 method_list = []
244 for member in sorted(root.__class__.__dict__.keys()):
245 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 09:51:35 -0700246 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700247 continue
248 member_obj = root.__class__.__dict__[member]
249 if _IsExposed(member_obj):
250 if type(member_obj) == types.FunctionType:
251 method_list.append(prefixed_member)
252 else:
253 method_list += _FindExposedMethods(
254 member_obj, prefixed_member, unlisted)
255 return method_list
256
257
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700258class ApiRoot(object):
259 """RESTful API for Dev Server information."""
260 exposed = True
261
262 @cherrypy.expose
263 def hostinfo(self, ip):
264 """Returns a JSON dictionary containing information about the given ip.
265
Gilad Arnold1b908392012-10-05 11:36:27 -0700266 Args:
267 ip: address of host whose info is requested
268 Returns:
269 A JSON dictionary containing all or some of the following fields:
270 last_event_type (int): last update event type received
271 last_event_status (int): last update event status received
272 last_known_version (string): last known version reported in update ping
273 forced_update_label (string): update label to force next update ping to
274 use, set by setnextupdate
275 See the OmahaEvent class in update_engine/omaha_request_action.h for
276 event type and status code definitions. If the ip does not exist an empty
277 string is returned.
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700278
Gilad Arnold1b908392012-10-05 11:36:27 -0700279 Example URL:
280 http://myhost/api/hostinfo?ip=192.168.1.5
281 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700282 return updater.HandleHostInfoPing(ip)
283
284 @cherrypy.expose
Gilad Arnold286a0062012-01-12 13:47:02 -0800285 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 11:36:27 -0700286 """Returns a JSON object containing a log of host event.
287
288 Args:
289 ip: address of host whose event log is requested, or `all'
290 Returns:
291 A JSON encoded list (log) of dictionaries (events), each of which
292 containing a `timestamp' and other event fields, as described under
293 /api/hostinfo.
294
295 Example URL:
296 http://myhost/api/hostlog?ip=192.168.1.5
297 """
Gilad Arnold286a0062012-01-12 13:47:02 -0800298 return updater.HandleHostLogPing(ip)
299
300 @cherrypy.expose
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700301 def setnextupdate(self, ip):
302 """Allows the response to the next update ping from a host to be set.
303
304 Takes the IP of the host and an update label as normally provided to the
Gilad Arnold1b908392012-10-05 11:36:27 -0700305 /update command.
306 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700307 body_length = int(cherrypy.request.headers['Content-Length'])
308 label = cherrypy.request.rfile.read(body_length)
309
310 if label:
311 label = label.strip()
312 if label:
313 return updater.HandleSetUpdatePing(ip, label)
beepsd76c6092013-08-28 22:23:30 -0700314 raise DevServerHTTPError(400, 'No label provided.')
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700315
316
Gilad Arnold55a2a372012-10-02 09:46:32 -0700317 @cherrypy.expose
318 def fileinfo(self, *path_args):
319 """Returns information about a given staged file.
320
321 Args:
322 path_args: path to the file inside the server's static staging directory
323 Returns:
324 A JSON encoded dictionary with information about the said file, which may
325 contain the following keys/values:
Gilad Arnold1b908392012-10-05 11:36:27 -0700326 size (int): the file size in bytes
327 sha1 (string): a base64 encoded SHA1 hash
328 sha256 (string): a base64 encoded SHA256 hash
329
330 Example URL:
331 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 09:46:32 -0700332 """
333 file_path = os.path.join(updater.static_dir, *path_args)
334 if not os.path.exists(file_path):
335 raise DevServerError('file not found: %s' % file_path)
336 try:
337 file_size = os.path.getsize(file_path)
338 file_sha1 = common_util.GetFileSha1(file_path)
339 file_sha256 = common_util.GetFileSha256(file_path)
340 except os.error, e:
341 raise DevServerError('failed to get info for file %s: %s' %
Gilad Arnolde74b3812013-04-22 11:27:38 -0700342 (file_path, e))
343
344 is_delta = autoupdate.Autoupdate.IsDeltaFormatFile(file_path)
345
346 return json.dumps({
347 autoupdate.Autoupdate.SIZE_ATTR: file_size,
348 autoupdate.Autoupdate.SHA1_ATTR: file_sha1,
349 autoupdate.Autoupdate.SHA256_ATTR: file_sha256,
350 autoupdate.Autoupdate.ISDELTA_ATTR: is_delta
351 })
Gilad Arnold55a2a372012-10-02 09:46:32 -0700352
Chris Sosa76e44b92013-01-31 12:11:38 -0800353
David Rochberg7c79a812011-01-19 14:24:45 -0500354class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700355 """The Root Class for the Dev Server.
356
357 CherryPy works as follows:
358 For each method in this class, cherrpy interprets root/path
359 as a call to an instance of DevServerRoot->method_name. For example,
360 a call to http://myhost/build will call build. CherryPy automatically
361 parses http args and places them as keyword arguments in each method.
362 For paths http://myhost/update/dir1/dir2, you can use *args so that
363 cherrypy uses the update method and puts the extra paths in args.
364 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700365 # Method names that should not be listed on the index page.
366 _UNLISTED_METHODS = ['index', 'doc']
367
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700368 api = ApiRoot()
Chris Sosa7c931362010-10-11 19:49:01 -0700369
Dan Shi59ae7092013-06-04 14:37:27 -0700370 # Number of threads that devserver is staging images.
371 _staging_thread_count = 0
372 # Lock used to lock increasing/decreasing count.
373 _staging_thread_count_lock = threading.Lock()
374
joychen3cb228e2013-06-12 12:13:13 -0700375 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700376 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800377 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 12:13:13 -0700378 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 14:24:45 -0500379
Chris Sosa6b0c6172013-08-05 17:01:33 -0700380 @staticmethod
381 def _get_artifacts(kwargs):
382 """Returns a tuple of named and file artifacts given the stage rpc kwargs.
383
384 Raises: DevserverError if no artifacts would be returned.
385 """
386 artifacts = kwargs.get('artifacts')
387 files = kwargs.get('files')
388 if not artifacts and not files:
389 raise DevServerError('No artifacts specified.')
390
Chris Sosafa86b482013-09-04 11:30:36 -0700391 # Note we NEED to coerce files to a string as we get raw unicode from
392 # cherrypy and we treat files as strings elsewhere in the code.
393 return (str(artifacts).split(',') if artifacts else [],
394 str(files).split(',') if files else [])
Chris Sosa6b0c6172013-08-05 17:01:33 -0700395
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700396 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500397 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700398 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700399 import builder
400 if self._builder is None:
401 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500402 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700403
Chris Sosacde6bf42012-05-31 18:36:39 -0700404 @staticmethod
405 def _canonicalize_archive_url(archive_url):
406 """Canonicalizes archive_url strings.
407
408 Raises:
409 DevserverError: if archive_url is not set.
410 """
411 if archive_url:
Chris Sosa76e44b92013-01-31 12:11:38 -0800412 if not archive_url.startswith('gs://'):
Don Garrett8ccab732013-08-30 09:13:59 -0700413 raise DevServerError("Archive URL isn't from Google Storage (%s) ." %
414 archive_url)
Chris Sosa76e44b92013-01-31 12:11:38 -0800415
Chris Sosacde6bf42012-05-31 18:36:39 -0700416 return archive_url.rstrip('/')
417 else:
418 raise DevServerError("Must specify an archive_url in the request")
419
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700420 @cherrypy.expose
Dan Shif8eb0d12013-08-01 17:52:06 -0700421 def is_staged(self, **kwargs):
422 """Check if artifacts have been downloaded.
423
Chris Sosa6b0c6172013-08-05 17:01:33 -0700424 async: True to return without waiting for download to complete.
425 artifacts: Comma separated list of named artifacts to download.
426 These are defined in artifact_info and have their implementation
427 in build_artifact.py.
428 files: Comma separated list of file artifacts to stage. These
429 will be available as is in the corresponding static directory with no
430 custom post-processing.
431
432 returns: True of all artifacts are staged.
Dan Shif8eb0d12013-08-01 17:52:06 -0700433
434 Example:
435 To check if autotest and test_suites are staged:
436 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
437 artifacts=autotest,test_suites
438 """
439 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
Chris Sosa6b0c6172013-08-05 17:01:33 -0700440 artifacts, files = self._get_artifacts(kwargs)
Dan Shif8eb0d12013-08-01 17:52:06 -0700441 return str(downloader.Downloader(updater.static_dir, archive_url).IsStaged(
Chris Sosa6b0c6172013-08-05 17:01:33 -0700442 artifacts, files))
Dan Shi59ae7092013-06-04 14:37:27 -0700443
Chris Sosa76e44b92013-01-31 12:11:38 -0800444 @cherrypy.expose
445 def stage(self, **kwargs):
446 """Downloads and caches the artifacts from Google Storage URL.
447
448 Downloads and caches the artifacts Google Storage URL. Returns once these
449 have been downloaded on the devserver. A call to this will attempt to cache
450 non-specified artifacts in the background for the given from the given URL
451 following the principle of spatial locality. Spatial locality of different
452 artifacts is explicitly defined in the build_artifact module.
453
454 These artifacts will then be available from the static/ sub-directory of
455 the devserver.
456
457 Args:
458 archive_url: Google Storage URL for the build.
Dan Shif8eb0d12013-08-01 17:52:06 -0700459 async: True to return without waiting for download to complete.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700460 artifacts: Comma separated list of named artifacts to download.
461 These are defined in artifact_info and have their implementation
462 in build_artifact.py.
463 files: Comma separated list of files to stage. These
464 will be available as is in the corresponding static directory with no
465 custom post-processing.
Chris Sosa76e44b92013-01-31 12:11:38 -0800466
467 Example:
468 To download the autotest and test suites tarballs:
469 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
470 artifacts=autotest,test_suites
471 To download the full update payload:
472 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
473 artifacts=full_payload
Chris Sosa6b0c6172013-08-05 17:01:33 -0700474 To download just a file called blah.bin:
475 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
476 files=blah.bin
Chris Sosa76e44b92013-01-31 12:11:38 -0800477
478 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 16:39:34 -0700479 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 12:11:38 -0800480
481 Note for this example, relative path is the archive_url stripped of its
482 basename i.e. path/ in the examples above. Specific example:
483
484 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
485
486 Will get staged to:
487
joychened64b222013-06-21 16:39:34 -0700488 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Chris Sosa76e44b92013-01-31 12:11:38 -0800489 """
Chris Sosacde6bf42012-05-31 18:36:39 -0700490 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
Dan Shif8eb0d12013-08-01 17:52:06 -0700491 async = kwargs.get('async', False)
Chris Sosa6b0c6172013-08-05 17:01:33 -0700492 artifacts, files = self._get_artifacts(kwargs)
Dan Shi59ae7092013-06-04 14:37:27 -0700493 with DevServerRoot._staging_thread_count_lock:
494 DevServerRoot._staging_thread_count += 1
495 try:
Chris Sosa6b0c6172013-08-05 17:01:33 -0700496 downloader.Downloader(updater.static_dir, archive_url).Download(
497 artifacts, files, async=async)
Dan Shi59ae7092013-06-04 14:37:27 -0700498 finally:
499 with DevServerRoot._staging_thread_count_lock:
500 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800501 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700502
503 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -0800504 def setup_telemetry(self, **kwargs):
505 """Extracts and sets up telemetry
506
507 This method goes through the telemetry deps packages, and stages them on
508 the devserver to be used by the drones and the telemetry tests.
509
510 Args:
511 archive_url: Google Storage URL for the build.
512
513 Returns:
514 Path to the source folder for the telemetry codebase once it is staged.
515 """
516 archive_url = kwargs.get('archive_url')
517 self.stage(archive_url=archive_url, artifacts='autotest')
518
519 build = '/'.join(downloader.Downloader.ParseUrl(archive_url))
520 build_path = os.path.join(updater.static_dir, build)
521 deps_path = os.path.join(build_path, 'autotest/packages')
522 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
523 src_folder = os.path.join(telemetry_path, 'src')
524
525 with self._telemetry_lock_dict.lock(telemetry_path):
526 if os.path.exists(src_folder):
527 # Telemetry is already fully stage return
528 return src_folder
529
530 common_util.MkDirP(telemetry_path)
531
532 # Copy over the required deps tar balls to the telemetry directory.
533 for dep in TELEMETRY_DEPS:
534 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -0700535 if not os.path.exists(dep_path):
536 # This dep does not exist (could be new), do not extract it.
537 continue
Simran Basi4baad082013-02-14 13:39:18 -0800538 try:
539 common_util.ExtractTarball(dep_path, telemetry_path)
540 except common_util.CommonUtilError as e:
541 shutil.rmtree(telemetry_path)
542 raise DevServerError(str(e))
543
544 # By default all the tarballs extract to test_src but some parts of
545 # the telemetry code specifically hardcoded to exist inside of 'src'.
546 test_src = os.path.join(telemetry_path, 'test_src')
547 try:
548 shutil.move(test_src, src_folder)
549 except shutil.Error:
550 # This can occur if src_folder already exists. Remove and retry move.
551 shutil.rmtree(src_folder)
552 raise DevServerError('Failure in telemetry setup for build %s. Appears'
553 ' that the test_src to src move failed.' % build)
554
555 return src_folder
556
557 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800558 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -0700559 """Symbolicates a minidump using pre-downloaded symbols, returns it.
560
561 Callers will need to POST to this URL with a body of MIME-type
562 "multipart/form-data".
563 The body should include a single argument, 'minidump', containing the
564 binary-formatted minidump to symbolicate.
565
Chris Masone816e38c2012-05-02 12:22:36 -0700566 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -0800567 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -0700568 minidump: The binary minidump file to symbolicate.
569 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800570 # Ensure the symbols have been staged.
571 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
572 if self.stage(archive_url=archive_url, artifacts='symbols') != 'Success':
573 raise DevServerError('Failed to stage symbols for %s' % archive_url)
574
Chris Masone816e38c2012-05-02 12:22:36 -0700575 to_return = ''
576 with tempfile.NamedTemporaryFile() as local:
577 while True:
578 data = minidump.file.read(8192)
579 if not data:
580 break
581 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -0800582
Chris Masone816e38c2012-05-02 12:22:36 -0700583 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -0800584
585 symbols_directory = os.path.join(downloader.Downloader.GetBuildDir(
586 updater.static_dir, archive_url), 'debug', 'breakpad')
587
588 stackwalk = subprocess.Popen(
589 ['minidump_stackwalk', local.name, symbols_directory],
590 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
591
Chris Masone816e38c2012-05-02 12:22:36 -0700592 to_return, error_text = stackwalk.communicate()
593 if stackwalk.returncode != 0:
594 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
595 error_text, stackwalk.returncode))
596
597 return to_return
598
599 @cherrypy.expose
Scott Zawalski16954532012-03-20 15:31:36 -0400600 def latestbuild(self, **params):
601 """Return a string representing the latest build for a given target.
602
603 Args:
604 target: The build target, typically a combination of the board and the
605 type of build e.g. x86-mario-release.
606 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
607 provided the latest RXX build will be returned.
608 Returns:
609 A string representation of the latest build if one exists, i.e.
610 R19-1993.0.0-a1-b1480.
611 An empty string if no latest could be found.
612 """
613 if not params:
614 return _PrintDocStringAsHTML(self.latestbuild)
615
616 if 'target' not in params:
beepsd76c6092013-08-28 22:23:30 -0700617 raise DevServerHTTPError(500, 'Error: target= is required!')
Scott Zawalski16954532012-03-20 15:31:36 -0400618 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700619 return common_util.GetLatestBuildVersion(
Scott Zawalski16954532012-03-20 15:31:36 -0400620 updater.static_dir, params['target'],
621 milestone=params.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700622 except common_util.CommonUtilError as errmsg:
beepsd76c6092013-08-28 22:23:30 -0700623 raise DevServerHTTPError(500, str(errmsg))
Scott Zawalski16954532012-03-20 15:31:36 -0400624
625 @cherrypy.expose
Scott Zawalski84a39c92012-01-13 15:12:42 -0500626 def controlfiles(self, **params):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500627 """Return a control file or a list of all known control files.
628
629 Example URL:
630 To List all control files:
beepsbd337242013-07-09 22:44:06 -0700631 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
632 To List all control files for, say, the bvt suite:
633 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -0500634 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500635 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 -0500636
637 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500638 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500639 control_path: If you want the contents of a control file set this
640 to the path. E.g. client/site_tests/sleeptest/control
641 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -0700642 suite_name: If control_path is not specified but a suite_name is
643 specified, list the control files belonging to that suite instead of
644 all control files. The empty string for suite_name will list all control
645 files for the build.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500646 Returns:
647 Contents of a control file if control_path is provided.
648 A list of control files if no control_path is provided.
649 """
Scott Zawalski4647ce62012-01-03 17:17:28 -0500650 if not params:
651 return _PrintDocStringAsHTML(self.controlfiles)
652
Scott Zawalski84a39c92012-01-13 15:12:42 -0500653 if 'build' not in params:
beepsd76c6092013-08-28 22:23:30 -0700654 raise DevServerHTTPError(500, 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -0500655
656 if 'control_path' not in params:
beepsbd337242013-07-09 22:44:06 -0700657 if 'suite_name' in params and params['suite_name']:
658 return common_util.GetControlFileListForSuite(
659 updater.static_dir, params['build'], params['suite_name'])
660 else:
661 return common_util.GetControlFileList(
662 updater.static_dir, params['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -0500663 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700664 return common_util.GetControlFile(
665 updater.static_dir, params['build'], params['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -0800666
667 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -0700668 def xbuddy(self, *args, **kwargs):
669 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -0700670
671 Args:
joycheneaf4cfc2013-07-02 08:38:57 -0700672 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -0700673 components of the path. The path can be understood as
674 "{local|remote}/build_id/artifact" where build_id is composed of
675 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -0700676
joychen121fc9b2013-08-02 14:30:30 -0700677 The first path element is optional, and can be "remote" or "local"
678 If local (the default), devserver will not attempt to access Google
679 Storage, and will only search the static directory for the files.
680 If remote, devserver will try to obtain the artifact off GS if it's
681 not found locally.
682 The board is the familiar board name, optionally suffixed.
683 The version can be the google storage version number, and may also be
684 any of a number of xBuddy defined version aliases that will be
685 translated into the latest built image that fits the description.
686 Defaults to latest.
687 The artifact is one of a number of image or artifact aliases used by
688 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -0700689
690 Kwargs:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800691 for_update: {true|false}
692 if true, pregenerates the update payloads for the image,
693 and returns the update uri to pass to the
694 update_engine_client.
joychen3cb228e2013-06-12 12:13:13 -0700695 return_dir: {true|false}
696 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800697 relative_path: {true|false}
698 if set to true, returns the relative path to the payload
699 directory from static_dir.
joychen3cb228e2013-06-12 12:13:13 -0700700 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -0700701 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -0700702 or
joycheneaf4cfc2013-07-02 08:38:57 -0700703 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -0700704
705 Returns:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800706 If |for_update|, returns a redirect to the image or update file
707 on the devserver. E.g.,
708 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
709 chromium-test-image.bin
710 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
711 http://host:port/static/x86-generic-release/R26-4000.0.0/
712 If |relative_path| is true, return a relative path the folder where the
713 payloads are. E.g.,
714 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 12:13:13 -0700715 """
Chris Sosa75490802013-09-30 17:21:45 -0700716 boolean_string = kwargs.get('for_update')
717 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800718 boolean_string = kwargs.get('return_dir')
719 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
720 boolean_string = kwargs.get('relative_path')
721 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -0700722
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800723 if return_dir and relative_path:
724 raise DevServerHTTPError(500, 'Cannot specify both return_dir and '
725 'relative_path')
Chris Sosa75490802013-09-30 17:21:45 -0700726
727 # For updates, we optimize downloading of test images.
728 file_name = None
729 build_id = None
730 if for_update:
731 try:
732 build_id = self._xbuddy.StageTestAritfactsForUpdate(args)
733 except build_artifact.ArtifactDownloadError:
734 build_id = None
735
736 if not build_id:
737 build_id, file_name = self._xbuddy.Get(args)
738
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800739 if for_update:
740 _Log('Payload generation triggered by request')
741 # Forces payload to be in cache and symlinked into build_id dir.
Chris Sosa75490802013-09-30 17:21:45 -0700742 updater.GetUpdateForLabel(autoupdate.FORCED_UPDATE, build_id,
743 image_name=file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800744
745 response = None
746 if return_dir:
747 response = os.path.join(cherrypy.request.base, 'static', build_id)
748 _Log('Directory requested, returning: %s', response)
749 elif relative_path:
750 response = build_id
751 _Log('Relative path requested, returning: %s', response)
752 elif for_update:
753 response = os.path.join(cherrypy.request.base, 'update', build_id)
754 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 12:13:13 -0700755 else:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800756 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 14:30:30 -0700757 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800758 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 14:30:30 -0700759 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -0700760
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800761 return response
762
joychen3cb228e2013-06-12 12:13:13 -0700763 @cherrypy.expose
764 def xbuddy_list(self):
765 """Lists the currently available images & time since last access.
766
767 @return: A string representation of a list of tuples
768 [(build_id, time since last access),...]
769 """
770 return self._xbuddy.List()
771
772 @cherrypy.expose
773 def xbuddy_capacity(self):
774 """Returns the number of images cached by xBuddy.
775
776 @return: Capacity of this devserver.
777 """
778 return self._xbuddy.Capacity()
779
780 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700781 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700782 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700783 return ('Welcome to the Dev Server!<br>\n'
784 '<br>\n'
785 'Here are the available methods, click for documentation:<br>\n'
786 '<br>\n'
787 '%s' %
788 '<br>\n'.join(
789 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700790 for name in _FindExposedMethods(
791 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700792
793 @cherrypy.expose
794 def doc(self, *args):
795 """Shows the documentation for available methods / URLs.
796
797 Example:
798 http://myhost/doc/update
799 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700800 name = '/'.join(args)
801 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700802 if not method:
803 raise DevServerError("No exposed method named `%s'" % name)
804 if not method.__doc__:
805 raise DevServerError("No documentation for exposed method `%s'" % name)
806 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -0700807
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700808 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700809 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700810 """Handles an update check from a Chrome OS client.
811
812 The HTTP request should contain the standard Omaha-style XML blob. The URL
813 line may contain an additional intermediate path to the update payload.
814
joychen121fc9b2013-08-02 14:30:30 -0700815 This request can be handled in one of 4 ways, depending on the devsever
816 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -0700817
joychen121fc9b2013-08-02 14:30:30 -0700818 1. No intermediate path
819 If no intermediate path is given, the default behavior is to generate an
820 update payload from the latest test image locally built for the board
821 specified in the xml. Devserver serves the generated payload.
822
823 2. Path explicitly invokes XBuddy
824 If there is a path given, it can explicitly invoke xbuddy by prefixing it
825 with 'xbuddy'. This path is then used to acquire an image binary for the
826 devserver to generate an update payload from. Devserver then serves this
827 payload.
828
829 3. Path is left for the devserver to interpret.
830 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
831 to generate a payload from the test image in that directory and serve it.
832
833 4. The devserver is in a 'forced' mode. TO BE DEPRECATED
834 This comes from the usage of --forced_payload or --image when starting the
835 devserver. No matter what path (or no path) gets passed in, devserver will
836 serve the update payload (--forced_payload) or generate an update payload
837 from the image (--image).
838
839 Examples:
840 1. No intermediate path
841 update_engine_client --omaha_url=http://myhost/update
842 This generates an update payload from the latest test image locally built
843 for the board specified in the xml.
844
845 2. Explicitly invoke xbuddy
846 update_engine_client --omaha_url=
847 http://myhost/update/xbuddy/remote/board/version/dev
848 This would go to GS to download the dev image for the board, from which
849 the devserver would generate a payload to serve.
850
851 3. Give a path for devserver to interpret
852 update_engine_client --omaha_url=http://myhost/update/some/random/path
853 This would attempt, in order to:
854 a) Generate an update from a test image binary if found in
855 static_dir/some/random/path.
856 b) Serve an update payload found in static_dir/some/random/path.
857 c) Hope that some/random/path takes the form "board/version" and
858 and attempt to download an update payload for that board/version
859 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700860 """
joychen121fc9b2013-08-02 14:30:30 -0700861 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -0800862 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -0700863 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-11 19:49:01 -0700864
joychen121fc9b2013-08-02 14:30:30 -0700865 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700866
Dan Shif5ce2de2013-04-25 16:06:32 -0700867 @cherrypy.expose
868 def check_health(self):
869 """Collect the health status of devserver to see if it's ready for staging.
870
871 @return: A JSON dictionary containing all or some of the following fields:
Dan Shi59ae7092013-06-04 14:37:27 -0700872 free_disk (int): free disk space in GB
873 staging_thread_count (int): number of devserver threads currently
874 staging an image
Dan Shif5ce2de2013-04-25 16:06:32 -0700875 """
876 # Get free disk space.
877 stat = os.statvfs(updater.static_dir)
878 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
879
880 return json.dumps({
881 'free_disk': free_disk,
Dan Shi59ae7092013-06-04 14:37:27 -0700882 'staging_thread_count': DevServerRoot._staging_thread_count,
Dan Shif5ce2de2013-04-25 16:06:32 -0700883 })
884
885
Chris Sosadbc20082012-12-10 13:39:11 -0800886def _CleanCache(cache_dir, wipe):
887 """Wipes any excess cached items in the cache_dir.
888
889 Args:
890 cache_dir: the directory we are wiping from.
891 wipe: If True, wipe all the contents -- not just the excess.
892 """
893 if wipe:
894 # Clear the cache and exit on error.
895 cmd = 'rm -rf %s/*' % cache_dir
896 if os.system(cmd) != 0:
897 _Log('Failed to clear the cache with %s' % cmd)
898 sys.exit(1)
899 else:
900 # Clear all but the last N cached updates
901 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
902 (cache_dir, CACHED_ENTRIES))
903 if os.system(cmd) != 0:
904 _Log('Failed to clean up old delta cache files with %s' % cmd)
905 sys.exit(1)
906
907
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700908def _AddTestingOptions(parser):
909 group = optparse.OptionGroup(
910 parser, 'Advanced Testing Options', 'These are used by test scripts and '
911 'developers writing integration tests utilizing the devserver. They are '
912 'not intended to be really used outside the scope of someone '
913 'knowledgable about the test.')
914 group.add_option('--exit',
915 action='store_true',
916 help='do not start the server (yet pregenerate/clear cache)')
917 group.add_option('--host_log',
918 action='store_true', default=False,
919 help='record history of host update events (/api/hostlog)')
920 group.add_option('--max_updates',
921 metavar='NUM', default= -1, type='int',
922 help='maximum number of update checks handled positively '
923 '(default: unlimited)')
924 group.add_option('--private_key',
925 metavar='PATH', default=None,
926 help='path to the private key in pem format. If this is set '
927 'the devserver will generate update payloads that are '
928 'signed with this key.')
David Zeuthen52ccd012013-10-31 12:58:26 -0700929 group.add_option('--private_key_for_metadata_hash_signature',
930 metavar='PATH', default=None,
931 help='path to the private key in pem format. If this is set '
932 'the devserver will sign the metadata hash with the given '
933 'key and transmit in the Omaha-style XML response.')
934 group.add_option('--public_key',
935 metavar='PATH', default=None,
936 help='path to the public key in pem format. If this is set '
937 'the devserver will transmit a base64 encoded version of '
938 'the content in the Omaha-style XML response.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700939 group.add_option('--proxy_port',
940 metavar='PORT', default=None, type='int',
941 help='port to have the client connect to -- basically the '
942 'devserver lies to the update to tell it to get the payload '
943 'from a different port that will proxy the request back to '
944 'the devserver. The proxy must be managed outside the '
945 'devserver.')
946 group.add_option('--remote_payload',
947 action='store_true', default=False,
948 help='Payload is being served from a remote machine')
949 group.add_option('-u', '--urlbase',
950 metavar='URL',
951 help='base URL for update images, other than the '
952 'devserver. Use in conjunction with remote_payload.')
953 parser.add_option_group(group)
954
955
956def _AddUpdateOptions(parser):
957 group = optparse.OptionGroup(
958 parser, 'Autoupdate Options', 'These options can be used to change '
959 'how the devserver either generates or serve update payloads. Please '
960 'note that all of these option affect how a payload is generated and so '
961 'do not work in archive-only mode.')
962 group.add_option('--board',
963 help='By default the devserver will create an update '
964 'payload from the latest image built for the board '
965 'a device that is requesting an update has. When we '
966 'pre-generate an update (see below) and we do not specify '
967 'another update_type option like image or payload, the '
968 'devserver needs to know the board to generate the latest '
969 'image for. This is that board.')
970 group.add_option('--critical_update',
971 action='store_true', default=False,
972 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700973 group.add_option('--image',
974 metavar='FILE',
975 help='Generate and serve an update using this image to any '
976 'device that requests an update.')
977 group.add_option('--no_patch_kernel',
978 dest='patch_kernel', action='store_false', default=True,
979 help='When generating an update payload, do not patch the '
980 'kernel with kernel verification blob from the stateful '
981 'partition.')
982 group.add_option('--payload',
983 metavar='PATH',
984 help='use the update payload from specified directory '
985 '(update.gz).')
986 group.add_option('-p', '--pregenerate_update',
987 action='store_true', default=False,
988 help='pre-generate the update payload before accepting '
989 'update requests. Useful to help debug payload generation '
990 'issues quickly. Also if an update payload will take a '
991 'long time to generate, a client may timeout if you do not'
992 'pregenerate the update.')
993 group.add_option('--src_image',
994 metavar='PATH', default='',
995 help='If specified, delta updates will be generated using '
996 'this image as the source image. Delta updates are when '
997 'you are updating from a "source image" to a another '
998 'image.')
999 parser.add_option_group(group)
1000
1001
1002def _AddProductionOptions(parser):
1003 group = optparse.OptionGroup(
1004 parser, 'Advanced Server Options', 'These options can be used to changed '
1005 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001006 group.add_option('--clear_cache',
1007 action='store_true', default=False,
1008 help='At startup, removes all cached entries from the'
1009 'devserver\'s cache.')
1010 group.add_option('--logfile',
1011 metavar='PATH',
1012 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -07001013 group.add_option('--pidfile',
1014 metavar='PATH',
1015 help='path to output a pid file for the server.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001016 group.add_option('--production',
1017 action='store_true', default=False,
1018 help='have the devserver use production values when '
1019 'starting up. This includes using more threads and '
1020 'performing less logging.')
1021 parser.add_option_group(group)
1022
1023
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001024def _MakeLogHandler(logfile):
1025 """Create a LogHandler instance used to log all messages."""
1026 hdlr_cls = handlers.TimedRotatingFileHandler
1027 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
1028 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -07001029 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001030 return hdlr
1031
1032
Chris Sosacde6bf42012-05-31 18:36:39 -07001033def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001034 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -08001035 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -07001036
1037 # get directory that the devserver is run from
1038 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -07001039 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -07001040 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001041 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -07001042 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -07001043 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001044 parser.add_option('--port',
1045 default=8080, type='int',
1046 help='port for the dev server to use (default: 8080)')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001047 parser.add_option('-t', '--test_image',
1048 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -07001049 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -07001050 parser.add_option('-x', '--xbuddy_manage_builds',
1051 action='store_true',
1052 default=False,
1053 help='If set, allow xbuddy to manage images in'
1054 'build/images.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001055 _AddProductionOptions(parser)
1056 _AddUpdateOptions(parser)
1057 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -07001058 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001059
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001060 # Handle options that must be set globally in cherrypy. Do this
1061 # work up front, because calls to _Log() below depend on this
1062 # initialization.
1063 if options.production:
1064 cherrypy.config.update({'environment': 'production'})
1065 if not options.logfile:
1066 cherrypy.config.update({'log.screen': True})
1067 else:
1068 cherrypy.config.update({'log.error_file': '',
1069 'log.access_file': ''})
1070 hdlr = _MakeLogHandler(options.logfile)
1071 # Pylint can't seem to process these two calls properly
1072 # pylint: disable=E1101
1073 cherrypy.log.access_log.addHandler(hdlr)
1074 cherrypy.log.error_log.addHandler(hdlr)
1075 # pylint: enable=E1101
1076
joychened64b222013-06-21 16:39:34 -07001077 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001078 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001079
joychened64b222013-06-21 16:39:34 -07001080 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001081 # If our devserver is only supposed to serve payloads, we shouldn't be
1082 # mucking with the cache at all. If the devserver hadn't previously
1083 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001084 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001085 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001086 else:
1087 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001088
Chris Sosadbc20082012-12-10 13:39:11 -08001089 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 16:39:34 -07001090 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001091
joychen121fc9b2013-08-02 14:30:30 -07001092 _xbuddy = xbuddy.XBuddy(options.xbuddy_manage_builds,
1093 options.board,
joychen121fc9b2013-08-02 14:30:30 -07001094 static_dir=options.static_dir)
Chris Sosa75490802013-09-30 17:21:45 -07001095 if options.clear_cache and options.xbuddy_manage_builds:
1096 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 14:30:30 -07001097
Chris Sosa6a3697f2013-01-29 16:44:43 -08001098 # We allow global use here to share with cherrypy classes.
1099 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001100 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001101 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001102 _xbuddy,
joychened64b222013-06-21 16:39:34 -07001103 static_dir=options.static_dir,
Andrew de los Reyes52620802010-04-12 13:40:07 -07001104 urlbase=options.urlbase,
Chris Sosa5d342a22010-09-28 16:54:41 -07001105 forced_image=options.image,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001106 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001107 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-28 23:42:37 -07001108 src_image=options.src_image,
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001109 patch_kernel=options.patch_kernel,
Chris Sosa08d55a22011-01-19 16:08:02 -08001110 board=options.board,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001111 copy_to_static_root=not options.exit,
1112 private_key=options.private_key,
David Zeuthen52ccd012013-10-31 12:58:26 -07001113 private_key_for_metadata_hash_signature=
1114 options.private_key_for_metadata_hash_signature,
1115 public_key=options.public_key,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001116 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001117 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001118 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001119 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001120 )
Chris Sosa7c931362010-10-11 19:49:01 -07001121
Chris Sosa6a3697f2013-01-29 16:44:43 -08001122 if options.pregenerate_update:
1123 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 15:46:22 -07001124
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001125 if options.exit:
1126 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001127
joychen3cb228e2013-06-12 12:13:13 -07001128 dev_server = DevServerRoot(_xbuddy)
1129
Chris Sosa855b8932013-08-21 13:24:55 -07001130 if options.pidfile:
1131 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1132
joychen3cb228e2013-06-12 12:13:13 -07001133 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001134
1135
1136if __name__ == '__main__':
1137 main()