blob: 1971ee6c3f330ef285f93d16a96dd8aa1404d77d [file] [log] [blame]
Chris Sosa7c931362010-10-12 02:49:011#!/usr/bin/python
2
Chris Sosa781ba6d2012-04-11 19:44:433# Copyright (c) 2009-2012 The Chromium OS Authors. All rights reserved.
[email protected]ded22402009-10-26 22:36:214# 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 18:47:007"""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 16:17:2329a particular package from a developer's chroot onto a requesting device.
Chris Sosa3ae4dc12013-03-29 18:47:0030
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-12 02:49:0142
Gilad Arnold55a2a372012-10-02 16:46:3243import json
Sean O'Connor14b6a0a2010-03-21 06:23:4844import optparse
[email protected]ded22402009-10-26 22:36:2145import os
Scott Zawalski4647ce62012-01-03 22:17:2846import re
Simran Basi4baad082013-02-14 21:39:1847import shutil
Mandeep Singh Baines38dcdda2012-12-08 01:55:3348import socket
Chris Masone816e38c2012-05-02 19:22:3649import subprocess
J. Richard Barnette3d977b82013-04-23 18:05:1950import sys
Chris Masone816e38c2012-05-02 19:22:3651import tempfile
Dan Shi59ae7092013-06-04 21:37:2752import threading
Gilad Arnoldd5ebaaa2012-10-02 18:52:3853import types
J. Richard Barnette3d977b82013-04-23 18:05:1954from logging import handlers
55
56import cherrypy
Chris Sosa855b8932013-08-21 20:24:5557from cherrypy import _cplogging as cplogging
58from cherrypy.process import plugins
[email protected]ded22402009-10-26 22:36:2159
Chris Sosa0356d3b2010-09-16 22:46:2260import autoupdate
Chris Sosa75490802013-10-01 00:21:4561import build_artifact
Gilad Arnoldc65330c2012-09-20 22:17:4862import common_util
Chris Sosa47a7d4e2012-03-28 18:26:5563import downloader
Chris Sosa7cd23202013-10-16 00:22:5764import gsutil_util
Gilad Arnoldc65330c2012-09-20 22:17:4865import log_util
joychen3cb228e2013-06-12 19:13:1366import xbuddy
Gilad Arnoldc65330c2012-09-20 22:17:4867
Gilad Arnoldc65330c2012-09-20 22:17:4868# Module-local log function.
Chris Sosa6a3697f2013-01-30 00:44:4369def _Log(message, *args):
70 return log_util.LogWithTag('DEVSERVER', message, *args)
Chris Sosa0356d3b2010-09-16 22:46:2271
Frank Farzan40160872011-12-13 02:39:1872
Chris Sosa417e55d2011-01-26 00:40:4873CACHED_ENTRIES = 12
Don Garrettf90edf02010-11-17 01:36:1474
Simran Basi4baad082013-02-14 21:39:1875TELEMETRY_FOLDER = 'telemetry_src'
76TELEMETRY_DEPS = ['dep-telemetry_dep.tar.bz2',
77 'dep-page_cycler_dep.tar.bz2',
Simran Basi0d078682013-03-22 23:40:0478 'dep-chrome_test.tar.bz2',
79 'dep-perf_data_dep.tar.bz2']
Simran Basi4baad082013-02-14 21:39:1880
Chris Sosa0356d3b2010-09-16 22:46:2281# Sets up global to share between classes.
[email protected]21a5ca32009-11-04 18:23:2382updater = None
[email protected]ded22402009-10-26 22:36:2183
J. Richard Barnette3d977b82013-04-23 18:05:1984# Log rotation parameters. These settings correspond to once a week
J. Richard Barnette6dfa5342013-06-04 18:48:5685# at midnight between Friday and Saturday, with about three months
86# of old logs kept for backup.
J. Richard Barnette3d977b82013-04-23 18:05:1987#
88# For more, see the documentation for
89# logging.handlers.TimedRotatingFileHandler
J. Richard Barnette6dfa5342013-06-04 18:48:5690_LOG_ROTATION_TIME = 'W4'
J. Richard Barnette3d977b82013-04-23 18:05:1991_LOG_ROTATION_BACKUP = 13
92
Frank Farzan40160872011-12-13 02:39:1893
Chris Sosa9164ca32012-03-28 18:04:5094class DevServerError(Exception):
Chris Sosa47a7d4e2012-03-28 18:26:5595 """Exception class used by this module."""
Chris Sosa47a7d4e2012-03-28 18:26:5596
97
Don Garrett8ccab732013-08-30 16:13:5998class DevServerHTTPError(cherrypy.HTTPError):
beepsd76c6092013-08-29 05:23:3099 """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 16:13:59105 cherrypy.HTTPError.__init__(self, status, message)
beepsd76c6092013-08-29 05:23:30106 _Log('HTTPError status: %s message: %s', status, message)
beepsd76c6092013-08-29 05:23:30107
108
Scott Zawalski4647ce62012-01-03 22:17:28109def _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 18:26:55138 line = '&nbsp;' * leading_space + line
Scott Zawalski4647ce62012-01-03 22:17:28139
140 html_doc.append('<BR>%s' % line)
141
142 return '\n'.join(html_doc)
143
144
Chris Sosa7c931362010-10-12 02:49:01145def _GetConfig(options):
146 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-08 01:55:33147
148 # On a system with IPv6 not compiled into the kernel,
149 # AF_INET6 sockets will return a socket.error exception.
150 # On such systems, fall-back to IPv4.
151 socket_host = '::'
152 try:
153 socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
154 except socket.error:
155 socket_host = '0.0.0.0'
156
Chris Sosa7c931362010-10-12 02:49:01157 base_config = { 'global':
158 { 'server.log_request_headers': True,
159 'server.protocol_version': 'HTTP/1.1',
Mandeep Singh Baines38dcdda2012-12-08 01:55:33160 'server.socket_host': socket_host,
Chris Sosa7c931362010-10-12 02:49:01161 'server.socket_port': int(options.port),
Chris Sosa374c62d2010-10-14 16:13:54162 'response.timeout': 6000,
Chris Sosa6fe23942012-07-02 22:44:46163 'request.show_tracebacks': True,
Chris Sosa72333d12012-06-13 18:28:05164 'server.socket_timeout': 60,
joychenecc02aa2013-07-18 01:27:35165 'server.thread_pool': 2,
Chris Sosa7c931362010-10-12 02:49:01166 },
Dale Curtisc9aaf3a2011-08-09 22:47:40167 '/api':
168 {
169 # Gets rid of cherrypy parsing post file for args.
170 'request.process_request_body': False,
171 },
Chris Sosaa1ef0102010-10-21 23:22:35172 '/build':
173 {
174 'response.timeout': 100000,
175 },
Chris Sosa7c931362010-10-12 02:49:01176 '/update':
177 {
178 # Gets rid of cherrypy parsing post file for args.
179 'request.process_request_body': False,
Chris Sosaf65f4b92010-10-21 22:57:51180 'response.timeout': 10000,
Chris Sosa7c931362010-10-12 02:49:01181 },
182 # Sets up the static dir for file hosting.
183 '/static':
joychened64b222013-06-21 23:39:34184 { 'tools.staticdir.dir': options.static_dir,
Chris Sosa7c931362010-10-12 02:49:01185 'tools.staticdir.on': True,
Chris Sosaf65f4b92010-10-21 22:57:51186 'response.timeout': 10000,
Chris Sosa7c931362010-10-12 02:49:01187 },
188 }
Chris Sosa5f118ef2012-07-12 18:37:50189 if options.production:
Alex Miller93beca52013-07-31 02:25:09190 base_config['global'].update({'server.thread_pool': 150})
Chris Sosa7cd23202013-10-16 00:22:57191 # TODO(sosa): Do this more cleanly.
192 gsutil_util.GSUTIL_ATTEMPTS = 5
Scott Zawalski1c5e7cd2012-02-27 18:12:52193
Chris Sosa7c931362010-10-12 02:49:01194 return base_config
[email protected]64244662009-11-12 00:52:08195
Darin Petkove17164a2010-08-11 20:24:41196
Gilad Arnoldd5ebaaa2012-10-02 18:52:38197def _GetRecursiveMemberObject(root, member_list):
198 """Returns an object corresponding to a nested member list.
199
200 Args:
201 root: the root object to search
202 member_list: list of nested members to search
203 Returns:
204 An object corresponding to the member name list; None otherwise.
205 """
206 for member in member_list:
207 next_root = root.__class__.__dict__.get(member)
208 if not next_root:
209 return None
210 root = next_root
211 return root
212
213
214def _IsExposed(name):
215 """Returns True iff |name| has an `exposed' attribute and it is set."""
216 return hasattr(name, 'exposed') and name.exposed
217
218
Gilad Arnold748c8322012-10-12 16:51:35219def _GetExposedMethod(root, nested_member, ignored=None):
Gilad Arnoldd5ebaaa2012-10-02 18:52:38220 """Returns a CherryPy-exposed method, if such exists.
221
222 Args:
223 root: the root object for searching
224 nested_member: a slash-joined path to the nested member
225 ignored: method paths to be ignored
226 Returns:
227 A function object corresponding to the path defined by |member_list| from
228 the |root| object, if the function is exposed and not ignored; None
229 otherwise.
230 """
Gilad Arnold748c8322012-10-12 16:51:35231 method = (not (ignored and nested_member in ignored) and
Gilad Arnoldd5ebaaa2012-10-02 18:52:38232 _GetRecursiveMemberObject(root, nested_member.split('/')))
233 if (method and type(method) == types.FunctionType and _IsExposed(method)):
234 return method
235
236
Gilad Arnold748c8322012-10-12 16:51:35237def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 18:52:38238 """Finds exposed CherryPy methods.
239
240 Args:
241 root: the root object for searching
242 prefix: slash-joined chain of members leading to current object
243 unlisted: URLs to be excluded regardless of their exposed status
244 Returns:
245 List of exposed URLs that are not unlisted.
246 """
247 method_list = []
248 for member in sorted(root.__class__.__dict__.keys()):
249 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 16:51:35250 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 18:52:38251 continue
252 member_obj = root.__class__.__dict__[member]
253 if _IsExposed(member_obj):
254 if type(member_obj) == types.FunctionType:
255 method_list.append(prefixed_member)
256 else:
257 method_list += _FindExposedMethods(
258 member_obj, prefixed_member, unlisted)
259 return method_list
260
261
Dale Curtisc9aaf3a2011-08-09 22:47:40262class ApiRoot(object):
263 """RESTful API for Dev Server information."""
264 exposed = True
265
266 @cherrypy.expose
267 def hostinfo(self, ip):
268 """Returns a JSON dictionary containing information about the given ip.
269
Gilad Arnold1b908392012-10-05 18:36:27270 Args:
271 ip: address of host whose info is requested
272 Returns:
273 A JSON dictionary containing all or some of the following fields:
274 last_event_type (int): last update event type received
275 last_event_status (int): last update event status received
276 last_known_version (string): last known version reported in update ping
277 forced_update_label (string): update label to force next update ping to
278 use, set by setnextupdate
279 See the OmahaEvent class in update_engine/omaha_request_action.h for
280 event type and status code definitions. If the ip does not exist an empty
281 string is returned.
Dale Curtisc9aaf3a2011-08-09 22:47:40282
Gilad Arnold1b908392012-10-05 18:36:27283 Example URL:
284 http://myhost/api/hostinfo?ip=192.168.1.5
285 """
Dale Curtisc9aaf3a2011-08-09 22:47:40286 return updater.HandleHostInfoPing(ip)
287
288 @cherrypy.expose
Gilad Arnold286a0062012-01-12 21:47:02289 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 18:36:27290 """Returns a JSON object containing a log of host event.
291
292 Args:
293 ip: address of host whose event log is requested, or `all'
294 Returns:
295 A JSON encoded list (log) of dictionaries (events), each of which
296 containing a `timestamp' and other event fields, as described under
297 /api/hostinfo.
298
299 Example URL:
300 http://myhost/api/hostlog?ip=192.168.1.5
301 """
Gilad Arnold286a0062012-01-12 21:47:02302 return updater.HandleHostLogPing(ip)
303
304 @cherrypy.expose
Dale Curtisc9aaf3a2011-08-09 22:47:40305 def setnextupdate(self, ip):
306 """Allows the response to the next update ping from a host to be set.
307
308 Takes the IP of the host and an update label as normally provided to the
Gilad Arnold1b908392012-10-05 18:36:27309 /update command.
310 """
Dale Curtisc9aaf3a2011-08-09 22:47:40311 body_length = int(cherrypy.request.headers['Content-Length'])
312 label = cherrypy.request.rfile.read(body_length)
313
314 if label:
315 label = label.strip()
316 if label:
317 return updater.HandleSetUpdatePing(ip, label)
beepsd76c6092013-08-29 05:23:30318 raise DevServerHTTPError(400, 'No label provided.')
Dale Curtisc9aaf3a2011-08-09 22:47:40319
320
Gilad Arnold55a2a372012-10-02 16:46:32321 @cherrypy.expose
322 def fileinfo(self, *path_args):
323 """Returns information about a given staged file.
324
325 Args:
326 path_args: path to the file inside the server's static staging directory
327 Returns:
328 A JSON encoded dictionary with information about the said file, which may
329 contain the following keys/values:
Gilad Arnold1b908392012-10-05 18:36:27330 size (int): the file size in bytes
331 sha1 (string): a base64 encoded SHA1 hash
332 sha256 (string): a base64 encoded SHA256 hash
333
334 Example URL:
335 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 16:46:32336 """
337 file_path = os.path.join(updater.static_dir, *path_args)
338 if not os.path.exists(file_path):
339 raise DevServerError('file not found: %s' % file_path)
340 try:
341 file_size = os.path.getsize(file_path)
342 file_sha1 = common_util.GetFileSha1(file_path)
343 file_sha256 = common_util.GetFileSha256(file_path)
344 except os.error, e:
345 raise DevServerError('failed to get info for file %s: %s' %
Gilad Arnolde74b3812013-04-22 18:27:38346 (file_path, e))
347
348 is_delta = autoupdate.Autoupdate.IsDeltaFormatFile(file_path)
349
350 return json.dumps({
351 autoupdate.Autoupdate.SIZE_ATTR: file_size,
352 autoupdate.Autoupdate.SHA1_ATTR: file_sha1,
353 autoupdate.Autoupdate.SHA256_ATTR: file_sha256,
354 autoupdate.Autoupdate.ISDELTA_ATTR: is_delta
355 })
Gilad Arnold55a2a372012-10-02 16:46:32356
Chris Sosa76e44b92013-01-31 20:11:38357
David Rochberg7c79a812011-01-19 19:24:45358class DevServerRoot(object):
Chris Sosa7c931362010-10-12 02:49:01359 """The Root Class for the Dev Server.
360
361 CherryPy works as follows:
362 For each method in this class, cherrpy interprets root/path
363 as a call to an instance of DevServerRoot->method_name. For example,
364 a call to http://myhost/build will call build. CherryPy automatically
365 parses http args and places them as keyword arguments in each method.
366 For paths http://myhost/update/dir1/dir2, you can use *args so that
367 cherrypy uses the update method and puts the extra paths in args.
368 """
Gilad Arnoldf8f769f2012-09-24 15:43:01369 # Method names that should not be listed on the index page.
370 _UNLISTED_METHODS = ['index', 'doc']
371
Dale Curtisc9aaf3a2011-08-09 22:47:40372 api = ApiRoot()
Chris Sosa7c931362010-10-12 02:49:01373
Dan Shi59ae7092013-06-04 21:37:27374 # Number of threads that devserver is staging images.
375 _staging_thread_count = 0
376 # Lock used to lock increasing/decreasing count.
377 _staging_thread_count_lock = threading.Lock()
378
joychen3cb228e2013-06-12 19:13:13379 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 22:20:41380 self._builder = None
Simran Basi4baad082013-02-14 21:39:18381 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 19:13:13382 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 19:24:45383
Chris Sosa6b0c6172013-08-06 00:01:33384 @staticmethod
385 def _get_artifacts(kwargs):
386 """Returns a tuple of named and file artifacts given the stage rpc kwargs.
387
388 Raises: DevserverError if no artifacts would be returned.
389 """
390 artifacts = kwargs.get('artifacts')
391 files = kwargs.get('files')
392 if not artifacts and not files:
393 raise DevServerError('No artifacts specified.')
394
Chris Sosafa86b482013-09-04 18:30:36395 # Note we NEED to coerce files to a string as we get raw unicode from
396 # cherrypy and we treat files as strings elsewhere in the code.
397 return (str(artifacts).split(',') if artifacts else [],
398 str(files).split(',') if files else [])
Chris Sosa6b0c6172013-08-06 00:01:33399
Dale Curtisc9aaf3a2011-08-09 22:47:40400 @cherrypy.expose
David Rochberg7c79a812011-01-19 19:24:45401 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-12 02:49:01402 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 22:20:41403 import builder
404 if self._builder is None:
405 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 19:24:45406 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-12 02:49:01407
Chris Sosacde6bf42012-06-01 01:36:39408 @staticmethod
409 def _canonicalize_archive_url(archive_url):
410 """Canonicalizes archive_url strings.
411
412 Raises:
413 DevserverError: if archive_url is not set.
414 """
415 if archive_url:
Chris Sosa76e44b92013-01-31 20:11:38416 if not archive_url.startswith('gs://'):
Don Garrett8ccab732013-08-30 16:13:59417 raise DevServerError("Archive URL isn't from Google Storage (%s) ." %
418 archive_url)
Chris Sosa76e44b92013-01-31 20:11:38419
Chris Sosacde6bf42012-06-01 01:36:39420 return archive_url.rstrip('/')
421 else:
422 raise DevServerError("Must specify an archive_url in the request")
423
Dale Curtisc9aaf3a2011-08-09 22:47:40424 @cherrypy.expose
Dan Shif8eb0d12013-08-02 00:52:06425 def is_staged(self, **kwargs):
426 """Check if artifacts have been downloaded.
427
Chris Sosa6b0c6172013-08-06 00:01:33428 async: True to return without waiting for download to complete.
429 artifacts: Comma separated list of named artifacts to download.
430 These are defined in artifact_info and have their implementation
431 in build_artifact.py.
432 files: Comma separated list of file artifacts to stage. These
433 will be available as is in the corresponding static directory with no
434 custom post-processing.
435
436 returns: True of all artifacts are staged.
Dan Shif8eb0d12013-08-02 00:52:06437
438 Example:
439 To check if autotest and test_suites are staged:
440 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
441 artifacts=autotest,test_suites
442 """
443 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
Chris Sosa6b0c6172013-08-06 00:01:33444 artifacts, files = self._get_artifacts(kwargs)
Dan Shif8eb0d12013-08-02 00:52:06445 return str(downloader.Downloader(updater.static_dir, archive_url).IsStaged(
Chris Sosa6b0c6172013-08-06 00:01:33446 artifacts, files))
Dan Shi59ae7092013-06-04 21:37:27447
Chris Sosa76e44b92013-01-31 20:11:38448 @cherrypy.expose
449 def stage(self, **kwargs):
450 """Downloads and caches the artifacts from Google Storage URL.
451
452 Downloads and caches the artifacts Google Storage URL. Returns once these
453 have been downloaded on the devserver. A call to this will attempt to cache
454 non-specified artifacts in the background for the given from the given URL
455 following the principle of spatial locality. Spatial locality of different
456 artifacts is explicitly defined in the build_artifact module.
457
458 These artifacts will then be available from the static/ sub-directory of
459 the devserver.
460
461 Args:
462 archive_url: Google Storage URL for the build.
Dan Shif8eb0d12013-08-02 00:52:06463 async: True to return without waiting for download to complete.
Chris Sosa6b0c6172013-08-06 00:01:33464 artifacts: Comma separated list of named artifacts to download.
465 These are defined in artifact_info and have their implementation
466 in build_artifact.py.
467 files: Comma separated list of files to stage. These
468 will be available as is in the corresponding static directory with no
469 custom post-processing.
Chris Sosa76e44b92013-01-31 20:11:38470
471 Example:
472 To download the autotest and test suites tarballs:
473 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
474 artifacts=autotest,test_suites
475 To download the full update payload:
476 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
477 artifacts=full_payload
Chris Sosa6b0c6172013-08-06 00:01:33478 To download just a file called blah.bin:
479 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
480 files=blah.bin
Chris Sosa76e44b92013-01-31 20:11:38481
482 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 23:39:34483 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 20:11:38484
485 Note for this example, relative path is the archive_url stripped of its
486 basename i.e. path/ in the examples above. Specific example:
487
488 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
489
490 Will get staged to:
491
joychened64b222013-06-21 23:39:34492 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Chris Sosa76e44b92013-01-31 20:11:38493 """
Chris Sosacde6bf42012-06-01 01:36:39494 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
Dan Shif8eb0d12013-08-02 00:52:06495 async = kwargs.get('async', False)
Chris Sosa6b0c6172013-08-06 00:01:33496 artifacts, files = self._get_artifacts(kwargs)
Dan Shi59ae7092013-06-04 21:37:27497 with DevServerRoot._staging_thread_count_lock:
498 DevServerRoot._staging_thread_count += 1
499 try:
Chris Sosa6b0c6172013-08-06 00:01:33500 downloader.Downloader(updater.static_dir, archive_url).Download(
501 artifacts, files, async=async)
Dan Shi59ae7092013-06-04 21:37:27502 finally:
503 with DevServerRoot._staging_thread_count_lock:
504 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 20:11:38505 return 'Success'
Chris Sosacde6bf42012-06-01 01:36:39506
507 @cherrypy.expose
Simran Basi4baad082013-02-14 21:39:18508 def setup_telemetry(self, **kwargs):
509 """Extracts and sets up telemetry
510
511 This method goes through the telemetry deps packages, and stages them on
512 the devserver to be used by the drones and the telemetry tests.
513
514 Args:
515 archive_url: Google Storage URL for the build.
516
517 Returns:
518 Path to the source folder for the telemetry codebase once it is staged.
519 """
520 archive_url = kwargs.get('archive_url')
521 self.stage(archive_url=archive_url, artifacts='autotest')
522
523 build = '/'.join(downloader.Downloader.ParseUrl(archive_url))
524 build_path = os.path.join(updater.static_dir, build)
525 deps_path = os.path.join(build_path, 'autotest/packages')
526 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
527 src_folder = os.path.join(telemetry_path, 'src')
528
529 with self._telemetry_lock_dict.lock(telemetry_path):
530 if os.path.exists(src_folder):
531 # Telemetry is already fully stage return
532 return src_folder
533
534 common_util.MkDirP(telemetry_path)
535
536 # Copy over the required deps tar balls to the telemetry directory.
537 for dep in TELEMETRY_DEPS:
538 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 23:40:04539 if not os.path.exists(dep_path):
540 # This dep does not exist (could be new), do not extract it.
541 continue
Simran Basi4baad082013-02-14 21:39:18542 try:
543 common_util.ExtractTarball(dep_path, telemetry_path)
544 except common_util.CommonUtilError as e:
545 shutil.rmtree(telemetry_path)
546 raise DevServerError(str(e))
547
548 # By default all the tarballs extract to test_src but some parts of
549 # the telemetry code specifically hardcoded to exist inside of 'src'.
550 test_src = os.path.join(telemetry_path, 'test_src')
551 try:
552 shutil.move(test_src, src_folder)
553 except shutil.Error:
554 # This can occur if src_folder already exists. Remove and retry move.
555 shutil.rmtree(src_folder)
556 raise DevServerError('Failure in telemetry setup for build %s. Appears'
557 ' that the test_src to src move failed.' % build)
558
559 return src_folder
560
561 @cherrypy.expose
Chris Sosa76e44b92013-01-31 20:11:38562 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 19:22:36563 """Symbolicates a minidump using pre-downloaded symbols, returns it.
564
565 Callers will need to POST to this URL with a body of MIME-type
566 "multipart/form-data".
567 The body should include a single argument, 'minidump', containing the
568 binary-formatted minidump to symbolicate.
569
Chris Masone816e38c2012-05-02 19:22:36570 Args:
Chris Sosa76e44b92013-01-31 20:11:38571 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 19:22:36572 minidump: The binary minidump file to symbolicate.
573 """
Chris Sosa76e44b92013-01-31 20:11:38574 # Ensure the symbols have been staged.
575 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
576 if self.stage(archive_url=archive_url, artifacts='symbols') != 'Success':
577 raise DevServerError('Failed to stage symbols for %s' % archive_url)
578
Chris Masone816e38c2012-05-02 19:22:36579 to_return = ''
580 with tempfile.NamedTemporaryFile() as local:
581 while True:
582 data = minidump.file.read(8192)
583 if not data:
584 break
585 local.write(data)
Chris Sosa76e44b92013-01-31 20:11:38586
Chris Masone816e38c2012-05-02 19:22:36587 local.flush()
Chris Sosa76e44b92013-01-31 20:11:38588
589 symbols_directory = os.path.join(downloader.Downloader.GetBuildDir(
590 updater.static_dir, archive_url), 'debug', 'breakpad')
591
592 stackwalk = subprocess.Popen(
593 ['minidump_stackwalk', local.name, symbols_directory],
594 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
595
Chris Masone816e38c2012-05-02 19:22:36596 to_return, error_text = stackwalk.communicate()
597 if stackwalk.returncode != 0:
598 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
599 error_text, stackwalk.returncode))
600
601 return to_return
602
603 @cherrypy.expose
Scott Zawalski16954532012-03-20 19:31:36604 def latestbuild(self, **params):
605 """Return a string representing the latest build for a given target.
606
607 Args:
608 target: The build target, typically a combination of the board and the
609 type of build e.g. x86-mario-release.
610 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
611 provided the latest RXX build will be returned.
612 Returns:
613 A string representation of the latest build if one exists, i.e.
614 R19-1993.0.0-a1-b1480.
615 An empty string if no latest could be found.
616 """
617 if not params:
618 return _PrintDocStringAsHTML(self.latestbuild)
619
620 if 'target' not in params:
beepsd76c6092013-08-29 05:23:30621 raise DevServerHTTPError(500, 'Error: target= is required!')
Scott Zawalski16954532012-03-20 19:31:36622 try:
Gilad Arnoldc65330c2012-09-20 22:17:48623 return common_util.GetLatestBuildVersion(
Scott Zawalski16954532012-03-20 19:31:36624 updater.static_dir, params['target'],
625 milestone=params.get('milestone'))
Gilad Arnold17fe03d2012-10-02 17:05:01626 except common_util.CommonUtilError as errmsg:
beepsd76c6092013-08-29 05:23:30627 raise DevServerHTTPError(500, str(errmsg))
Scott Zawalski16954532012-03-20 19:31:36628
629 @cherrypy.expose
Scott Zawalski84a39c92012-01-13 20:12:42630 def controlfiles(self, **params):
Scott Zawalski4647ce62012-01-03 22:17:28631 """Return a control file or a list of all known control files.
632
633 Example URL:
634 To List all control files:
beepsbd337242013-07-10 05:44:06635 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
636 To List all control files for, say, the bvt suite:
637 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 22:17:28638 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 20:12:42639 http://dev-server/controlfiles?board=x86-alex-release&build=R18-1514.0.0&control_path=client/sleeptest/control
Scott Zawalski4647ce62012-01-03 22:17:28640
641 Args:
Scott Zawalski84a39c92012-01-13 20:12:42642 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 22:17:28643 control_path: If you want the contents of a control file set this
644 to the path. E.g. client/site_tests/sleeptest/control
645 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-10 05:44:06646 suite_name: If control_path is not specified but a suite_name is
647 specified, list the control files belonging to that suite instead of
648 all control files. The empty string for suite_name will list all control
649 files for the build.
Scott Zawalski4647ce62012-01-03 22:17:28650 Returns:
651 Contents of a control file if control_path is provided.
652 A list of control files if no control_path is provided.
653 """
Scott Zawalski4647ce62012-01-03 22:17:28654 if not params:
655 return _PrintDocStringAsHTML(self.controlfiles)
656
Scott Zawalski84a39c92012-01-13 20:12:42657 if 'build' not in params:
beepsd76c6092013-08-29 05:23:30658 raise DevServerHTTPError(500, 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 22:17:28659
660 if 'control_path' not in params:
beepsbd337242013-07-10 05:44:06661 if 'suite_name' in params and params['suite_name']:
662 return common_util.GetControlFileListForSuite(
663 updater.static_dir, params['build'], params['suite_name'])
664 else:
665 return common_util.GetControlFileList(
666 updater.static_dir, params['build'])
Scott Zawalski4647ce62012-01-03 22:17:28667 else:
Gilad Arnoldc65330c2012-09-20 22:17:48668 return common_util.GetControlFile(
669 updater.static_dir, params['build'], params['control_path'])
Frank Farzan40160872011-12-13 02:39:18670
671 @cherrypy.expose
joycheneaf4cfc2013-07-02 15:38:57672 def xbuddy(self, *args, **kwargs):
673 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 19:13:13674
675 Args:
joycheneaf4cfc2013-07-02 15:38:57676 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 21:30:30677 components of the path. The path can be understood as
678 "{local|remote}/build_id/artifact" where build_id is composed of
679 "board/version."
joycheneaf4cfc2013-07-02 15:38:57680
joychen121fc9b2013-08-02 21:30:30681 The first path element is optional, and can be "remote" or "local"
682 If local (the default), devserver will not attempt to access Google
683 Storage, and will only search the static directory for the files.
684 If remote, devserver will try to obtain the artifact off GS if it's
685 not found locally.
686 The board is the familiar board name, optionally suffixed.
687 The version can be the google storage version number, and may also be
688 any of a number of xBuddy defined version aliases that will be
689 translated into the latest built image that fits the description.
690 Defaults to latest.
691 The artifact is one of a number of image or artifact aliases used by
692 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 15:38:57693
694 Kwargs:
joychen3cb228e2013-06-12 19:13:13695 return_dir: {true|false}
696 if set to true, returns the url to the update.gz
697 instead.
Chris Sosa75490802013-10-01 00:21:45698 for_update: {true|false}
699 if for_update, pre-generates the update payload for the image
700 and returns the update path to pass to the
701 update_engine_client.
joychen3cb228e2013-06-12 19:13:13702
703 Example URL:
joycheneaf4cfc2013-07-02 15:38:57704 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 19:13:13705 or
joycheneaf4cfc2013-07-02 15:38:57706 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 19:13:13707
708 Returns:
709 A redirect to the image or update file on the devserver.
710 e.g. http://host:port/static/archive/x86-generic-release/
711 R26-4000.0.0/chromium-test-image.bin
712 or if return_dir is True, return path to the folder where
joychen121fc9b2013-08-02 21:30:30713 the artifact is.
joychen3cb228e2013-06-12 19:13:13714 http://host:port/static/x86-generic-release/R26-4000.0.0/
715 """
716 boolean_string = kwargs.get('return_dir')
717 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
Chris Sosa75490802013-10-01 00:21:45718 boolean_string = kwargs.get('for_update')
719 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 21:30:30720
Chris Sosa75490802013-10-01 00:21:45721 if for_update and return_dir:
722 raise DevServerHTTPError(500, 'Cannot specify both update and return_dir')
723
724 # For updates, we optimize downloading of test images.
725 file_name = None
726 build_id = None
727 if for_update:
728 try:
729 build_id = self._xbuddy.StageTestAritfactsForUpdate(args)
730 except build_artifact.ArtifactDownloadError:
731 build_id = None
732
733 if not build_id:
734 build_id, file_name = self._xbuddy.Get(args)
735
joychen3cb228e2013-06-12 19:13:13736 if return_dir:
Chris Sosa855b8932013-08-21 20:24:55737 directory = os.path.join(cherrypy.request.base, 'static', build_id)
joycheneaf4cfc2013-07-02 15:38:57738 _Log("Directory requested, returning: %s", directory)
739 return directory
Chris Sosa75490802013-10-01 00:21:45740 elif for_update:
741 # Forces paylaod to be in cache and symlinked into build_id dir.
742 updater.GetUpdateForLabel(autoupdate.FORCED_UPDATE, build_id,
743 image_name=file_name)
744 update_uri = os.path.join(cherrypy.request.base, 'update', build_id)
745 _Log("Update requested, returning: %s", update_uri)
746 return update_uri
joychen3cb228e2013-06-12 19:13:13747 else:
joychen121fc9b2013-08-02 21:30:30748 build_id = '/' + os.path.join('static', build_id, file_name)
749 _Log("Payload requested, returning: %s", build_id)
750 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 19:13:13751
752 @cherrypy.expose
753 def xbuddy_list(self):
754 """Lists the currently available images & time since last access.
755
756 @return: A string representation of a list of tuples
757 [(build_id, time since last access),...]
758 """
759 return self._xbuddy.List()
760
761 @cherrypy.expose
762 def xbuddy_capacity(self):
763 """Returns the number of images cached by xBuddy.
764
765 @return: Capacity of this devserver.
766 """
767 return self._xbuddy.Capacity()
768
769 @cherrypy.expose
Chris Sosa7c931362010-10-12 02:49:01770 def index(self):
Gilad Arnoldf8f769f2012-09-24 15:43:01771 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 15:43:01772 return ('Welcome to the Dev Server!<br>\n'
773 '<br>\n'
774 'Here are the available methods, click for documentation:<br>\n'
775 '<br>\n'
776 '%s' %
777 '<br>\n'.join(
778 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 18:52:38779 for name in _FindExposedMethods(
780 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 15:43:01781
782 @cherrypy.expose
783 def doc(self, *args):
784 """Shows the documentation for available methods / URLs.
785
786 Example:
787 http://myhost/doc/update
788 """
Gilad Arnoldd5ebaaa2012-10-02 18:52:38789 name = '/'.join(args)
790 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 15:43:01791 if not method:
792 raise DevServerError("No exposed method named `%s'" % name)
793 if not method.__doc__:
794 raise DevServerError("No documentation for exposed method `%s'" % name)
795 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-12 02:49:01796
Dale Curtisc9aaf3a2011-08-09 22:47:40797 @cherrypy.expose
Chris Sosa7c931362010-10-12 02:49:01798 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 15:43:01799 """Handles an update check from a Chrome OS client.
800
801 The HTTP request should contain the standard Omaha-style XML blob. The URL
802 line may contain an additional intermediate path to the update payload.
803
joychen121fc9b2013-08-02 21:30:30804 This request can be handled in one of 4 ways, depending on the devsever
805 settings and intermediate path.
joychenb0dfe552013-07-30 17:02:06806
joychen121fc9b2013-08-02 21:30:30807 1. No intermediate path
808 If no intermediate path is given, the default behavior is to generate an
809 update payload from the latest test image locally built for the board
810 specified in the xml. Devserver serves the generated payload.
811
812 2. Path explicitly invokes XBuddy
813 If there is a path given, it can explicitly invoke xbuddy by prefixing it
814 with 'xbuddy'. This path is then used to acquire an image binary for the
815 devserver to generate an update payload from. Devserver then serves this
816 payload.
817
818 3. Path is left for the devserver to interpret.
819 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
820 to generate a payload from the test image in that directory and serve it.
821
822 4. The devserver is in a 'forced' mode. TO BE DEPRECATED
823 This comes from the usage of --forced_payload or --image when starting the
824 devserver. No matter what path (or no path) gets passed in, devserver will
825 serve the update payload (--forced_payload) or generate an update payload
826 from the image (--image).
827
828 Examples:
829 1. No intermediate path
830 update_engine_client --omaha_url=http://myhost/update
831 This generates an update payload from the latest test image locally built
832 for the board specified in the xml.
833
834 2. Explicitly invoke xbuddy
835 update_engine_client --omaha_url=
836 http://myhost/update/xbuddy/remote/board/version/dev
837 This would go to GS to download the dev image for the board, from which
838 the devserver would generate a payload to serve.
839
840 3. Give a path for devserver to interpret
841 update_engine_client --omaha_url=http://myhost/update/some/random/path
842 This would attempt, in order to:
843 a) Generate an update from a test image binary if found in
844 static_dir/some/random/path.
845 b) Serve an update payload found in static_dir/some/random/path.
846 c) Hope that some/random/path takes the form "board/version" and
847 and attempt to download an update payload for that board/version
848 from GS.
Gilad Arnoldf8f769f2012-09-24 15:43:01849 """
joychen121fc9b2013-08-02 21:30:30850 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 21:47:02851 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-12 02:49:01852 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-12 02:49:01853
joychen121fc9b2013-08-02 21:30:30854 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 22:46:22855
Dan Shif5ce2de2013-04-25 23:06:32856 @cherrypy.expose
857 def check_health(self):
858 """Collect the health status of devserver to see if it's ready for staging.
859
860 @return: A JSON dictionary containing all or some of the following fields:
Dan Shi59ae7092013-06-04 21:37:27861 free_disk (int): free disk space in GB
862 staging_thread_count (int): number of devserver threads currently
863 staging an image
Dan Shif5ce2de2013-04-25 23:06:32864 """
865 # Get free disk space.
866 stat = os.statvfs(updater.static_dir)
867 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
868
869 return json.dumps({
870 'free_disk': free_disk,
Dan Shi59ae7092013-06-04 21:37:27871 'staging_thread_count': DevServerRoot._staging_thread_count,
Dan Shif5ce2de2013-04-25 23:06:32872 })
873
874
Chris Sosadbc20082012-12-10 21:39:11875def _CleanCache(cache_dir, wipe):
876 """Wipes any excess cached items in the cache_dir.
877
878 Args:
879 cache_dir: the directory we are wiping from.
880 wipe: If True, wipe all the contents -- not just the excess.
881 """
882 if wipe:
883 # Clear the cache and exit on error.
884 cmd = 'rm -rf %s/*' % cache_dir
885 if os.system(cmd) != 0:
886 _Log('Failed to clear the cache with %s' % cmd)
887 sys.exit(1)
888 else:
889 # Clear all but the last N cached updates
890 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
891 (cache_dir, CACHED_ENTRIES))
892 if os.system(cmd) != 0:
893 _Log('Failed to clean up old delta cache files with %s' % cmd)
894 sys.exit(1)
895
896
Chris Sosa3ae4dc12013-03-29 18:47:00897def _AddTestingOptions(parser):
898 group = optparse.OptionGroup(
899 parser, 'Advanced Testing Options', 'These are used by test scripts and '
900 'developers writing integration tests utilizing the devserver. They are '
901 'not intended to be really used outside the scope of someone '
902 'knowledgable about the test.')
903 group.add_option('--exit',
904 action='store_true',
905 help='do not start the server (yet pregenerate/clear cache)')
906 group.add_option('--host_log',
907 action='store_true', default=False,
908 help='record history of host update events (/api/hostlog)')
909 group.add_option('--max_updates',
910 metavar='NUM', default= -1, type='int',
911 help='maximum number of update checks handled positively '
912 '(default: unlimited)')
913 group.add_option('--private_key',
914 metavar='PATH', default=None,
915 help='path to the private key in pem format. If this is set '
916 'the devserver will generate update payloads that are '
917 'signed with this key.')
918 group.add_option('--proxy_port',
919 metavar='PORT', default=None, type='int',
920 help='port to have the client connect to -- basically the '
921 'devserver lies to the update to tell it to get the payload '
922 'from a different port that will proxy the request back to '
923 'the devserver. The proxy must be managed outside the '
924 'devserver.')
925 group.add_option('--remote_payload',
926 action='store_true', default=False,
927 help='Payload is being served from a remote machine')
928 group.add_option('-u', '--urlbase',
929 metavar='URL',
930 help='base URL for update images, other than the '
931 'devserver. Use in conjunction with remote_payload.')
932 parser.add_option_group(group)
933
934
935def _AddUpdateOptions(parser):
936 group = optparse.OptionGroup(
937 parser, 'Autoupdate Options', 'These options can be used to change '
938 'how the devserver either generates or serve update payloads. Please '
939 'note that all of these option affect how a payload is generated and so '
940 'do not work in archive-only mode.')
941 group.add_option('--board',
942 help='By default the devserver will create an update '
943 'payload from the latest image built for the board '
944 'a device that is requesting an update has. When we '
945 'pre-generate an update (see below) and we do not specify '
946 'another update_type option like image or payload, the '
947 'devserver needs to know the board to generate the latest '
948 'image for. This is that board.')
949 group.add_option('--critical_update',
950 action='store_true', default=False,
951 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 18:47:00952 group.add_option('--image',
953 metavar='FILE',
954 help='Generate and serve an update using this image to any '
955 'device that requests an update.')
956 group.add_option('--no_patch_kernel',
957 dest='patch_kernel', action='store_false', default=True,
958 help='When generating an update payload, do not patch the '
959 'kernel with kernel verification blob from the stateful '
960 'partition.')
961 group.add_option('--payload',
962 metavar='PATH',
963 help='use the update payload from specified directory '
964 '(update.gz).')
965 group.add_option('-p', '--pregenerate_update',
966 action='store_true', default=False,
967 help='pre-generate the update payload before accepting '
968 'update requests. Useful to help debug payload generation '
969 'issues quickly. Also if an update payload will take a '
970 'long time to generate, a client may timeout if you do not'
971 'pregenerate the update.')
972 group.add_option('--src_image',
973 metavar='PATH', default='',
974 help='If specified, delta updates will be generated using '
975 'this image as the source image. Delta updates are when '
976 'you are updating from a "source image" to a another '
977 'image.')
978 parser.add_option_group(group)
979
980
981def _AddProductionOptions(parser):
982 group = optparse.OptionGroup(
983 parser, 'Advanced Server Options', 'These options can be used to changed '
984 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 18:47:00985 group.add_option('--clear_cache',
986 action='store_true', default=False,
987 help='At startup, removes all cached entries from the'
988 'devserver\'s cache.')
989 group.add_option('--logfile',
990 metavar='PATH',
991 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 20:24:55992 group.add_option('--pidfile',
993 metavar='PATH',
994 help='path to output a pid file for the server.')
Chris Sosa3ae4dc12013-03-29 18:47:00995 group.add_option('--production',
996 action='store_true', default=False,
997 help='have the devserver use production values when '
998 'starting up. This includes using more threads and '
999 'performing less logging.')
1000 parser.add_option_group(group)
1001
1002
J. Richard Barnette3d977b82013-04-23 18:05:191003def _MakeLogHandler(logfile):
1004 """Create a LogHandler instance used to log all messages."""
1005 hdlr_cls = handlers.TimedRotatingFileHandler
1006 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
1007 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 20:24:551008 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 18:05:191009 return hdlr
1010
1011
Chris Sosacde6bf42012-06-01 01:36:391012def main():
Chris Sosa3ae4dc12013-03-29 18:47:001013 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 21:47:021014 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 23:39:341015
1016 # get directory that the devserver is run from
1017 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 16:17:231018 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 23:39:341019 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 17:09:421020 metavar='PATH',
joychen84d13772013-08-06 16:17:231021 default=default_static_dir,
joychened64b222013-06-21 23:39:341022 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 17:09:421023 parser.add_option('--port',
1024 default=8080, type='int',
1025 help='port for the dev server to use (default: 8080)')
Gilad Arnold9714d9b2012-10-04 17:09:421026 parser.add_option('-t', '--test_image',
1027 action='store_true',
joychen121fc9b2013-08-02 21:30:301028 help='Deprecated.')
joychen5260b9a2013-07-16 21:48:011029 parser.add_option('-x', '--xbuddy_manage_builds',
1030 action='store_true',
1031 default=False,
1032 help='If set, allow xbuddy to manage images in'
1033 'build/images.')
Chris Sosa3ae4dc12013-03-29 18:47:001034 _AddProductionOptions(parser)
1035 _AddUpdateOptions(parser)
1036 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-12 02:49:011037 (options, _) = parser.parse_args()
[email protected]21a5ca32009-11-04 18:23:231038
J. Richard Barnette3d977b82013-04-23 18:05:191039 # Handle options that must be set globally in cherrypy. Do this
1040 # work up front, because calls to _Log() below depend on this
1041 # initialization.
1042 if options.production:
1043 cherrypy.config.update({'environment': 'production'})
1044 if not options.logfile:
1045 cherrypy.config.update({'log.screen': True})
1046 else:
1047 cherrypy.config.update({'log.error_file': '',
1048 'log.access_file': ''})
1049 hdlr = _MakeLogHandler(options.logfile)
1050 # Pylint can't seem to process these two calls properly
1051 # pylint: disable=E1101
1052 cherrypy.log.access_log.addHandler(hdlr)
1053 cherrypy.log.error_log.addHandler(hdlr)
1054 # pylint: enable=E1101
1055
joychened64b222013-06-21 23:39:341056 # set static_dir, from which everything will be served
joychen84d13772013-08-06 16:17:231057 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 22:46:221058
joychened64b222013-06-21 23:39:341059 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 18:05:191060 # If our devserver is only supposed to serve payloads, we shouldn't be
1061 # mucking with the cache at all. If the devserver hadn't previously
1062 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 18:14:071063 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 21:39:111064 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 20:12:171065 else:
1066 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-17 01:36:141067
Chris Sosadbc20082012-12-10 21:39:111068 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 23:39:341069 _Log('Serving from %s' % options.static_dir)
[email protected]21a5ca32009-11-04 18:23:231070
joychen121fc9b2013-08-02 21:30:301071 _xbuddy = xbuddy.XBuddy(options.xbuddy_manage_builds,
1072 options.board,
joychen121fc9b2013-08-02 21:30:301073 static_dir=options.static_dir)
Chris Sosa75490802013-10-01 00:21:451074 if options.clear_cache and options.xbuddy_manage_builds:
1075 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 21:30:301076
Chris Sosa6a3697f2013-01-30 00:44:431077 # We allow global use here to share with cherrypy classes.
1078 # pylint: disable=W0603
Chris Sosacde6bf42012-06-01 01:36:391079 global updater
Andrew de los Reyes52620802010-04-12 20:40:071080 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 21:30:301081 _xbuddy,
joychened64b222013-06-21 23:39:341082 static_dir=options.static_dir,
Andrew de los Reyes52620802010-04-12 20:40:071083 urlbase=options.urlbase,
Chris Sosa5d342a22010-09-28 23:54:411084 forced_image=options.image,
Gilad Arnold0c9c8602012-10-03 06:58:581085 payload_path=options.payload,
Don Garrett0ad09372010-12-07 00:20:301086 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-29 06:42:371087 src_image=options.src_image,
Chris Sosa3ae4dc12013-03-29 18:47:001088 patch_kernel=options.patch_kernel,
Chris Sosa08d55a22011-01-20 00:08:021089 board=options.board,
Chris Sosa0f1ec842011-02-15 00:33:221090 copy_to_static_root=not options.exit,
1091 private_key=options.private_key,
Satoru Takabayashid733cbe2011-11-15 17:36:321092 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-03 06:58:581093 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 17:32:441094 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 19:52:231095 host_log=options.host_log,
Chris Sosa0f1ec842011-02-15 00:33:221096 )
Chris Sosa7c931362010-10-12 02:49:011097
Chris Sosa6a3697f2013-01-30 00:44:431098 if options.pregenerate_update:
1099 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 22:46:221100
J. Richard Barnette3d977b82013-04-23 18:05:191101 if options.exit:
1102 return
Chris Sosa2f1c41e2012-07-10 21:32:331103
joychen3cb228e2013-06-12 19:13:131104 dev_server = DevServerRoot(_xbuddy)
1105
Chris Sosa855b8932013-08-21 20:24:551106 if options.pidfile:
1107 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1108
joychen3cb228e2013-06-12 19:13:131109 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-06-01 01:36:391110
1111
1112if __name__ == '__main__':
1113 main()