blob: ded50ba96108a91b7476a323ba0cf2035d26b0ad [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
Gilad Arnoldc65330c2012-09-20 15:17:48 -070061import common_util
Chris Sosa47a7d4e2012-03-28 11:26:55 -070062import downloader
Gilad Arnoldc65330c2012-09-20 15:17:48 -070063import log_util
joychen3cb228e2013-06-12 12:13:13 -070064import xbuddy
Gilad Arnoldc65330c2012-09-20 15:17:48 -070065
Gilad Arnoldc65330c2012-09-20 15:17:48 -070066# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080067def _Log(message, *args):
68 return log_util.LogWithTag('DEVSERVER', message, *args)
Chris Sosa0356d3b2010-09-16 15:46:22 -070069
Frank Farzan40160872011-12-12 18:39:18 -080070
Chris Sosa417e55d2011-01-25 16:40:48 -080071CACHED_ENTRIES = 12
Don Garrettf90edf02010-11-16 17:36:14 -080072
Simran Basi4baad082013-02-14 13:39:18 -080073TELEMETRY_FOLDER = 'telemetry_src'
74TELEMETRY_DEPS = ['dep-telemetry_dep.tar.bz2',
75 'dep-page_cycler_dep.tar.bz2',
Simran Basi0d078682013-03-22 16:40:04 -070076 'dep-chrome_test.tar.bz2',
77 'dep-perf_data_dep.tar.bz2']
Simran Basi4baad082013-02-14 13:39:18 -080078
Chris Sosa0356d3b2010-09-16 15:46:22 -070079# Sets up global to share between classes.
rtc@google.com21a5ca32009-11-04 18:23:23 +000080updater = None
rtc@google.comded22402009-10-26 22:36:21 +000081
J. Richard Barnette3d977b82013-04-23 11:05:19 -070082# Log rotation parameters. These settings correspond to once a week
J. Richard Barnette6dfa5342013-06-04 11:48:56 -070083# at midnight between Friday and Saturday, with about three months
84# of old logs kept for backup.
J. Richard Barnette3d977b82013-04-23 11:05:19 -070085#
86# For more, see the documentation for
87# logging.handlers.TimedRotatingFileHandler
J. Richard Barnette6dfa5342013-06-04 11:48:56 -070088_LOG_ROTATION_TIME = 'W4'
J. Richard Barnette3d977b82013-04-23 11:05:19 -070089_LOG_ROTATION_BACKUP = 13
90
Frank Farzan40160872011-12-12 18:39:18 -080091
Chris Sosa9164ca32012-03-28 11:04:50 -070092class DevServerError(Exception):
Chris Sosa47a7d4e2012-03-28 11:26:55 -070093 """Exception class used by this module."""
94 pass
95
96
beepsd76c6092013-08-28 22:23:30 -070097class DevServerHTTPError(Exception):
98 """Exception class to log the HTTPResponse before routing it to cherrypy."""
99 def __init__(self, status, message):
100 """
101 @param status: HTTPResponse status.
102 @param message: Message associated with the response.
103 """
104 _Log('HTTPError status: %s message: %s', status, message)
105 raise cherrypy.HTTPError(status, message)
106
107
Scott Zawalski4647ce62012-01-03 17:17:28 -0500108def _LeadingWhiteSpaceCount(string):
109 """Count the amount of leading whitespace in a string.
110
111 Args:
112 string: The string to count leading whitespace in.
113 Returns:
114 number of white space chars before characters start.
115 """
116 matched = re.match('^\s+', string)
117 if matched:
118 return len(matched.group())
119
120 return 0
121
122
123def _PrintDocStringAsHTML(func):
124 """Make a functions docstring somewhat HTML style.
125
126 Args:
127 func: The function to return the docstring from.
128 Returns:
129 A string that is somewhat formated for a web browser.
130 """
131 # TODO(scottz): Make this parse Args/Returns in a prettier way.
132 # Arguments could be bolded and indented etc.
133 html_doc = []
134 for line in func.__doc__.splitlines():
135 leading_space = _LeadingWhiteSpaceCount(line)
136 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700137 line = '&nbsp;' * leading_space + line
Scott Zawalski4647ce62012-01-03 17:17:28 -0500138
139 html_doc.append('<BR>%s' % line)
140
141 return '\n'.join(html_doc)
142
143
Chris Sosa7c931362010-10-11 19:49:01 -0700144def _GetConfig(options):
145 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800146
147 # On a system with IPv6 not compiled into the kernel,
148 # AF_INET6 sockets will return a socket.error exception.
149 # On such systems, fall-back to IPv4.
150 socket_host = '::'
151 try:
152 socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
153 except socket.error:
154 socket_host = '0.0.0.0'
155
Chris Sosa7c931362010-10-11 19:49:01 -0700156 base_config = { 'global':
157 { 'server.log_request_headers': True,
158 'server.protocol_version': 'HTTP/1.1',
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800159 'server.socket_host': socket_host,
Chris Sosa7c931362010-10-11 19:49:01 -0700160 'server.socket_port': int(options.port),
Chris Sosa374c62d2010-10-14 09:13:54 -0700161 'response.timeout': 6000,
Chris Sosa6fe23942012-07-02 15:44:46 -0700162 'request.show_tracebacks': True,
Chris Sosa72333d12012-06-13 11:28:05 -0700163 'server.socket_timeout': 60,
joychenecc02aa2013-07-17 18:27:35 -0700164 'server.thread_pool': 2,
Chris Sosa7c931362010-10-11 19:49:01 -0700165 },
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700166 '/api':
167 {
168 # Gets rid of cherrypy parsing post file for args.
169 'request.process_request_body': False,
170 },
Chris Sosaa1ef0102010-10-21 16:22:35 -0700171 '/build':
172 {
173 'response.timeout': 100000,
174 },
Chris Sosa7c931362010-10-11 19:49:01 -0700175 '/update':
176 {
177 # Gets rid of cherrypy parsing post file for args.
178 'request.process_request_body': False,
Chris Sosaf65f4b92010-10-21 15:57:51 -0700179 'response.timeout': 10000,
Chris Sosa7c931362010-10-11 19:49:01 -0700180 },
181 # Sets up the static dir for file hosting.
182 '/static':
joychened64b222013-06-21 16:39:34 -0700183 { 'tools.staticdir.dir': options.static_dir,
Chris Sosa7c931362010-10-11 19:49:01 -0700184 'tools.staticdir.on': True,
Chris Sosaf65f4b92010-10-21 15:57:51 -0700185 'response.timeout': 10000,
Chris Sosa7c931362010-10-11 19:49:01 -0700186 },
187 }
Chris Sosa5f118ef2012-07-12 11:37:50 -0700188 if options.production:
Alex Miller93beca52013-07-30 19:25:09 -0700189 base_config['global'].update({'server.thread_pool': 150})
Scott Zawalski1c5e7cd2012-02-27 13:12:52 -0500190
Chris Sosa7c931362010-10-11 19:49:01 -0700191 return base_config
rtc@google.com64244662009-11-12 00:52:08 +0000192
Darin Petkove17164a2010-08-11 13:24:41 -0700193
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700194def _GetRecursiveMemberObject(root, member_list):
195 """Returns an object corresponding to a nested member list.
196
197 Args:
198 root: the root object to search
199 member_list: list of nested members to search
200 Returns:
201 An object corresponding to the member name list; None otherwise.
202 """
203 for member in member_list:
204 next_root = root.__class__.__dict__.get(member)
205 if not next_root:
206 return None
207 root = next_root
208 return root
209
210
211def _IsExposed(name):
212 """Returns True iff |name| has an `exposed' attribute and it is set."""
213 return hasattr(name, 'exposed') and name.exposed
214
215
Gilad Arnold748c8322012-10-12 09:51:35 -0700216def _GetExposedMethod(root, nested_member, ignored=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700217 """Returns a CherryPy-exposed method, if such exists.
218
219 Args:
220 root: the root object for searching
221 nested_member: a slash-joined path to the nested member
222 ignored: method paths to be ignored
223 Returns:
224 A function object corresponding to the path defined by |member_list| from
225 the |root| object, if the function is exposed and not ignored; None
226 otherwise.
227 """
Gilad Arnold748c8322012-10-12 09:51:35 -0700228 method = (not (ignored and nested_member in ignored) and
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700229 _GetRecursiveMemberObject(root, nested_member.split('/')))
230 if (method and type(method) == types.FunctionType and _IsExposed(method)):
231 return method
232
233
Gilad Arnold748c8322012-10-12 09:51:35 -0700234def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700235 """Finds exposed CherryPy methods.
236
237 Args:
238 root: the root object for searching
239 prefix: slash-joined chain of members leading to current object
240 unlisted: URLs to be excluded regardless of their exposed status
241 Returns:
242 List of exposed URLs that are not unlisted.
243 """
244 method_list = []
245 for member in sorted(root.__class__.__dict__.keys()):
246 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 09:51:35 -0700247 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700248 continue
249 member_obj = root.__class__.__dict__[member]
250 if _IsExposed(member_obj):
251 if type(member_obj) == types.FunctionType:
252 method_list.append(prefixed_member)
253 else:
254 method_list += _FindExposedMethods(
255 member_obj, prefixed_member, unlisted)
256 return method_list
257
258
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700259class ApiRoot(object):
260 """RESTful API for Dev Server information."""
261 exposed = True
262
263 @cherrypy.expose
264 def hostinfo(self, ip):
265 """Returns a JSON dictionary containing information about the given ip.
266
Gilad Arnold1b908392012-10-05 11:36:27 -0700267 Args:
268 ip: address of host whose info is requested
269 Returns:
270 A JSON dictionary containing all or some of the following fields:
271 last_event_type (int): last update event type received
272 last_event_status (int): last update event status received
273 last_known_version (string): last known version reported in update ping
274 forced_update_label (string): update label to force next update ping to
275 use, set by setnextupdate
276 See the OmahaEvent class in update_engine/omaha_request_action.h for
277 event type and status code definitions. If the ip does not exist an empty
278 string is returned.
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700279
Gilad Arnold1b908392012-10-05 11:36:27 -0700280 Example URL:
281 http://myhost/api/hostinfo?ip=192.168.1.5
282 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700283 return updater.HandleHostInfoPing(ip)
284
285 @cherrypy.expose
Gilad Arnold286a0062012-01-12 13:47:02 -0800286 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 11:36:27 -0700287 """Returns a JSON object containing a log of host event.
288
289 Args:
290 ip: address of host whose event log is requested, or `all'
291 Returns:
292 A JSON encoded list (log) of dictionaries (events), each of which
293 containing a `timestamp' and other event fields, as described under
294 /api/hostinfo.
295
296 Example URL:
297 http://myhost/api/hostlog?ip=192.168.1.5
298 """
Gilad Arnold286a0062012-01-12 13:47:02 -0800299 return updater.HandleHostLogPing(ip)
300
301 @cherrypy.expose
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700302 def setnextupdate(self, ip):
303 """Allows the response to the next update ping from a host to be set.
304
305 Takes the IP of the host and an update label as normally provided to the
Gilad Arnold1b908392012-10-05 11:36:27 -0700306 /update command.
307 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700308 body_length = int(cherrypy.request.headers['Content-Length'])
309 label = cherrypy.request.rfile.read(body_length)
310
311 if label:
312 label = label.strip()
313 if label:
314 return updater.HandleSetUpdatePing(ip, label)
beepsd76c6092013-08-28 22:23:30 -0700315 raise DevServerHTTPError(400, 'No label provided.')
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700316
317
Gilad Arnold55a2a372012-10-02 09:46:32 -0700318 @cherrypy.expose
319 def fileinfo(self, *path_args):
320 """Returns information about a given staged file.
321
322 Args:
323 path_args: path to the file inside the server's static staging directory
324 Returns:
325 A JSON encoded dictionary with information about the said file, which may
326 contain the following keys/values:
Gilad Arnold1b908392012-10-05 11:36:27 -0700327 size (int): the file size in bytes
328 sha1 (string): a base64 encoded SHA1 hash
329 sha256 (string): a base64 encoded SHA256 hash
330
331 Example URL:
332 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 09:46:32 -0700333 """
334 file_path = os.path.join(updater.static_dir, *path_args)
335 if not os.path.exists(file_path):
336 raise DevServerError('file not found: %s' % file_path)
337 try:
338 file_size = os.path.getsize(file_path)
339 file_sha1 = common_util.GetFileSha1(file_path)
340 file_sha256 = common_util.GetFileSha256(file_path)
341 except os.error, e:
342 raise DevServerError('failed to get info for file %s: %s' %
Gilad Arnolde74b3812013-04-22 11:27:38 -0700343 (file_path, e))
344
345 is_delta = autoupdate.Autoupdate.IsDeltaFormatFile(file_path)
346
347 return json.dumps({
348 autoupdate.Autoupdate.SIZE_ATTR: file_size,
349 autoupdate.Autoupdate.SHA1_ATTR: file_sha1,
350 autoupdate.Autoupdate.SHA256_ATTR: file_sha256,
351 autoupdate.Autoupdate.ISDELTA_ATTR: is_delta
352 })
Gilad Arnold55a2a372012-10-02 09:46:32 -0700353
Chris Sosa76e44b92013-01-31 12:11:38 -0800354
David Rochberg7c79a812011-01-19 14:24:45 -0500355class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700356 """The Root Class for the Dev Server.
357
358 CherryPy works as follows:
359 For each method in this class, cherrpy interprets root/path
360 as a call to an instance of DevServerRoot->method_name. For example,
361 a call to http://myhost/build will call build. CherryPy automatically
362 parses http args and places them as keyword arguments in each method.
363 For paths http://myhost/update/dir1/dir2, you can use *args so that
364 cherrypy uses the update method and puts the extra paths in args.
365 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700366 # Method names that should not be listed on the index page.
367 _UNLISTED_METHODS = ['index', 'doc']
368
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700369 api = ApiRoot()
Chris Sosa7c931362010-10-11 19:49:01 -0700370
Dan Shi59ae7092013-06-04 14:37:27 -0700371 # Number of threads that devserver is staging images.
372 _staging_thread_count = 0
373 # Lock used to lock increasing/decreasing count.
374 _staging_thread_count_lock = threading.Lock()
375
joychen3cb228e2013-06-12 12:13:13 -0700376 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700377 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800378 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 12:13:13 -0700379 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 14:24:45 -0500380
Chris Sosa6b0c6172013-08-05 17:01:33 -0700381 @staticmethod
382 def _get_artifacts(kwargs):
383 """Returns a tuple of named and file artifacts given the stage rpc kwargs.
384
385 Raises: DevserverError if no artifacts would be returned.
386 """
387 artifacts = kwargs.get('artifacts')
388 files = kwargs.get('files')
389 if not artifacts and not files:
390 raise DevServerError('No artifacts specified.')
391
392 return (artifacts.split(',') if artifacts else [],
393 files.split(',') if files else [])
394
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700395 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500396 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700397 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700398 import builder
399 if self._builder is None:
400 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500401 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700402
Chris Sosacde6bf42012-05-31 18:36:39 -0700403 @staticmethod
404 def _canonicalize_archive_url(archive_url):
405 """Canonicalizes archive_url strings.
406
407 Raises:
408 DevserverError: if archive_url is not set.
409 """
410 if archive_url:
Chris Sosa76e44b92013-01-31 12:11:38 -0800411 if not archive_url.startswith('gs://'):
412 raise DevServerError("Archive URL isn't from Google Storage.")
413
Chris Sosacde6bf42012-05-31 18:36:39 -0700414 return archive_url.rstrip('/')
415 else:
416 raise DevServerError("Must specify an archive_url in the request")
417
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700418 @cherrypy.expose
Dan Shif8eb0d12013-08-01 17:52:06 -0700419 def is_staged(self, **kwargs):
420 """Check if artifacts have been downloaded.
421
Chris Sosa6b0c6172013-08-05 17:01:33 -0700422 async: True to return without waiting for download to complete.
423 artifacts: Comma separated list of named artifacts to download.
424 These are defined in artifact_info and have their implementation
425 in build_artifact.py.
426 files: Comma separated list of file artifacts to stage. These
427 will be available as is in the corresponding static directory with no
428 custom post-processing.
429
430 returns: True of all artifacts are staged.
Dan Shif8eb0d12013-08-01 17:52:06 -0700431
432 Example:
433 To check if autotest and test_suites are staged:
434 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
435 artifacts=autotest,test_suites
436 """
437 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
Chris Sosa6b0c6172013-08-05 17:01:33 -0700438 artifacts, files = self._get_artifacts(kwargs)
Dan Shif8eb0d12013-08-01 17:52:06 -0700439 return str(downloader.Downloader(updater.static_dir, archive_url).IsStaged(
Chris Sosa6b0c6172013-08-05 17:01:33 -0700440 artifacts, files))
Dan Shi59ae7092013-06-04 14:37:27 -0700441
Chris Sosa76e44b92013-01-31 12:11:38 -0800442 @cherrypy.expose
443 def stage(self, **kwargs):
444 """Downloads and caches the artifacts from Google Storage URL.
445
446 Downloads and caches the artifacts Google Storage URL. Returns once these
447 have been downloaded on the devserver. A call to this will attempt to cache
448 non-specified artifacts in the background for the given from the given URL
449 following the principle of spatial locality. Spatial locality of different
450 artifacts is explicitly defined in the build_artifact module.
451
452 These artifacts will then be available from the static/ sub-directory of
453 the devserver.
454
455 Args:
456 archive_url: Google Storage URL for the build.
Dan Shif8eb0d12013-08-01 17:52:06 -0700457 async: True to return without waiting for download to complete.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700458 artifacts: Comma separated list of named artifacts to download.
459 These are defined in artifact_info and have their implementation
460 in build_artifact.py.
461 files: Comma separated list of files to stage. These
462 will be available as is in the corresponding static directory with no
463 custom post-processing.
Chris Sosa76e44b92013-01-31 12:11:38 -0800464
465 Example:
466 To download the autotest and test suites tarballs:
467 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
468 artifacts=autotest,test_suites
469 To download the full update payload:
470 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
471 artifacts=full_payload
Chris Sosa6b0c6172013-08-05 17:01:33 -0700472 To download just a file called blah.bin:
473 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
474 files=blah.bin
Chris Sosa76e44b92013-01-31 12:11:38 -0800475
476 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 16:39:34 -0700477 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 12:11:38 -0800478
479 Note for this example, relative path is the archive_url stripped of its
480 basename i.e. path/ in the examples above. Specific example:
481
482 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
483
484 Will get staged to:
485
joychened64b222013-06-21 16:39:34 -0700486 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Chris Sosa76e44b92013-01-31 12:11:38 -0800487 """
Chris Sosacde6bf42012-05-31 18:36:39 -0700488 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
Dan Shif8eb0d12013-08-01 17:52:06 -0700489 async = kwargs.get('async', False)
Chris Sosa6b0c6172013-08-05 17:01:33 -0700490 artifacts, files = self._get_artifacts(kwargs)
Dan Shi59ae7092013-06-04 14:37:27 -0700491 with DevServerRoot._staging_thread_count_lock:
492 DevServerRoot._staging_thread_count += 1
493 try:
Chris Sosa6b0c6172013-08-05 17:01:33 -0700494 downloader.Downloader(updater.static_dir, archive_url).Download(
495 artifacts, files, async=async)
Dan Shi59ae7092013-06-04 14:37:27 -0700496 finally:
497 with DevServerRoot._staging_thread_count_lock:
498 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800499 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700500
501 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -0800502 def setup_telemetry(self, **kwargs):
503 """Extracts and sets up telemetry
504
505 This method goes through the telemetry deps packages, and stages them on
506 the devserver to be used by the drones and the telemetry tests.
507
508 Args:
509 archive_url: Google Storage URL for the build.
510
511 Returns:
512 Path to the source folder for the telemetry codebase once it is staged.
513 """
514 archive_url = kwargs.get('archive_url')
515 self.stage(archive_url=archive_url, artifacts='autotest')
516
517 build = '/'.join(downloader.Downloader.ParseUrl(archive_url))
518 build_path = os.path.join(updater.static_dir, build)
519 deps_path = os.path.join(build_path, 'autotest/packages')
520 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
521 src_folder = os.path.join(telemetry_path, 'src')
522
523 with self._telemetry_lock_dict.lock(telemetry_path):
524 if os.path.exists(src_folder):
525 # Telemetry is already fully stage return
526 return src_folder
527
528 common_util.MkDirP(telemetry_path)
529
530 # Copy over the required deps tar balls to the telemetry directory.
531 for dep in TELEMETRY_DEPS:
532 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -0700533 if not os.path.exists(dep_path):
534 # This dep does not exist (could be new), do not extract it.
535 continue
Simran Basi4baad082013-02-14 13:39:18 -0800536 try:
537 common_util.ExtractTarball(dep_path, telemetry_path)
538 except common_util.CommonUtilError as e:
539 shutil.rmtree(telemetry_path)
540 raise DevServerError(str(e))
541
542 # By default all the tarballs extract to test_src but some parts of
543 # the telemetry code specifically hardcoded to exist inside of 'src'.
544 test_src = os.path.join(telemetry_path, 'test_src')
545 try:
546 shutil.move(test_src, src_folder)
547 except shutil.Error:
548 # This can occur if src_folder already exists. Remove and retry move.
549 shutil.rmtree(src_folder)
550 raise DevServerError('Failure in telemetry setup for build %s. Appears'
551 ' that the test_src to src move failed.' % build)
552
553 return src_folder
554
555 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800556 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -0700557 """Symbolicates a minidump using pre-downloaded symbols, returns it.
558
559 Callers will need to POST to this URL with a body of MIME-type
560 "multipart/form-data".
561 The body should include a single argument, 'minidump', containing the
562 binary-formatted minidump to symbolicate.
563
Chris Masone816e38c2012-05-02 12:22:36 -0700564 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -0800565 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -0700566 minidump: The binary minidump file to symbolicate.
567 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800568 # Ensure the symbols have been staged.
569 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
570 if self.stage(archive_url=archive_url, artifacts='symbols') != 'Success':
571 raise DevServerError('Failed to stage symbols for %s' % archive_url)
572
Chris Masone816e38c2012-05-02 12:22:36 -0700573 to_return = ''
574 with tempfile.NamedTemporaryFile() as local:
575 while True:
576 data = minidump.file.read(8192)
577 if not data:
578 break
579 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -0800580
Chris Masone816e38c2012-05-02 12:22:36 -0700581 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -0800582
583 symbols_directory = os.path.join(downloader.Downloader.GetBuildDir(
584 updater.static_dir, archive_url), 'debug', 'breakpad')
585
586 stackwalk = subprocess.Popen(
587 ['minidump_stackwalk', local.name, symbols_directory],
588 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
589
Chris Masone816e38c2012-05-02 12:22:36 -0700590 to_return, error_text = stackwalk.communicate()
591 if stackwalk.returncode != 0:
592 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
593 error_text, stackwalk.returncode))
594
595 return to_return
596
597 @cherrypy.expose
Scott Zawalski16954532012-03-20 15:31:36 -0400598 def latestbuild(self, **params):
599 """Return a string representing the latest build for a given target.
600
601 Args:
602 target: The build target, typically a combination of the board and the
603 type of build e.g. x86-mario-release.
604 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
605 provided the latest RXX build will be returned.
606 Returns:
607 A string representation of the latest build if one exists, i.e.
608 R19-1993.0.0-a1-b1480.
609 An empty string if no latest could be found.
610 """
611 if not params:
612 return _PrintDocStringAsHTML(self.latestbuild)
613
614 if 'target' not in params:
beepsd76c6092013-08-28 22:23:30 -0700615 raise DevServerHTTPError(500, 'Error: target= is required!')
Scott Zawalski16954532012-03-20 15:31:36 -0400616 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700617 return common_util.GetLatestBuildVersion(
Scott Zawalski16954532012-03-20 15:31:36 -0400618 updater.static_dir, params['target'],
619 milestone=params.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700620 except common_util.CommonUtilError as errmsg:
beepsd76c6092013-08-28 22:23:30 -0700621 raise DevServerHTTPError(500, str(errmsg))
Scott Zawalski16954532012-03-20 15:31:36 -0400622
623 @cherrypy.expose
Scott Zawalski84a39c92012-01-13 15:12:42 -0500624 def controlfiles(self, **params):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500625 """Return a control file or a list of all known control files.
626
627 Example URL:
628 To List all control files:
beepsbd337242013-07-09 22:44:06 -0700629 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
630 To List all control files for, say, the bvt suite:
631 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -0500632 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500633 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 -0500634
635 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500636 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500637 control_path: If you want the contents of a control file set this
638 to the path. E.g. client/site_tests/sleeptest/control
639 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -0700640 suite_name: If control_path is not specified but a suite_name is
641 specified, list the control files belonging to that suite instead of
642 all control files. The empty string for suite_name will list all control
643 files for the build.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500644 Returns:
645 Contents of a control file if control_path is provided.
646 A list of control files if no control_path is provided.
647 """
Scott Zawalski4647ce62012-01-03 17:17:28 -0500648 if not params:
649 return _PrintDocStringAsHTML(self.controlfiles)
650
Scott Zawalski84a39c92012-01-13 15:12:42 -0500651 if 'build' not in params:
beepsd76c6092013-08-28 22:23:30 -0700652 raise DevServerHTTPError(500, 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -0500653
654 if 'control_path' not in params:
beepsbd337242013-07-09 22:44:06 -0700655 if 'suite_name' in params and params['suite_name']:
656 return common_util.GetControlFileListForSuite(
657 updater.static_dir, params['build'], params['suite_name'])
658 else:
659 return common_util.GetControlFileList(
660 updater.static_dir, params['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -0500661 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700662 return common_util.GetControlFile(
663 updater.static_dir, params['build'], params['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -0800664
665 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -0700666 def xbuddy(self, *args, **kwargs):
667 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -0700668
669 Args:
joycheneaf4cfc2013-07-02 08:38:57 -0700670 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -0700671 components of the path. The path can be understood as
672 "{local|remote}/build_id/artifact" where build_id is composed of
673 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -0700674
joychen121fc9b2013-08-02 14:30:30 -0700675 The first path element is optional, and can be "remote" or "local"
676 If local (the default), devserver will not attempt to access Google
677 Storage, and will only search the static directory for the files.
678 If remote, devserver will try to obtain the artifact off GS if it's
679 not found locally.
680 The board is the familiar board name, optionally suffixed.
681 The version can be the google storage version number, and may also be
682 any of a number of xBuddy defined version aliases that will be
683 translated into the latest built image that fits the description.
684 Defaults to latest.
685 The artifact is one of a number of image or artifact aliases used by
686 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -0700687
688 Kwargs:
joychen3cb228e2013-06-12 12:13:13 -0700689 return_dir: {true|false}
690 if set to true, returns the url to the update.gz
691 instead.
692
693 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -0700694 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -0700695 or
joycheneaf4cfc2013-07-02 08:38:57 -0700696 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -0700697
698 Returns:
699 A redirect to the image or update file on the devserver.
700 e.g. http://host:port/static/archive/x86-generic-release/
701 R26-4000.0.0/chromium-test-image.bin
702 or if return_dir is True, return path to the folder where
joychen121fc9b2013-08-02 14:30:30 -0700703 the artifact is.
joychen3cb228e2013-06-12 12:13:13 -0700704 http://host:port/static/x86-generic-release/R26-4000.0.0/
705 """
706 boolean_string = kwargs.get('return_dir')
707 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -0700708
709 build_id, file_name = self._xbuddy.Get(args)
joychen3cb228e2013-06-12 12:13:13 -0700710 if return_dir:
Chris Sosa855b8932013-08-21 13:24:55 -0700711 directory = os.path.join(cherrypy.request.base, 'static', build_id)
joycheneaf4cfc2013-07-02 08:38:57 -0700712 _Log("Directory requested, returning: %s", directory)
713 return directory
joychen3cb228e2013-06-12 12:13:13 -0700714 else:
joychen121fc9b2013-08-02 14:30:30 -0700715 build_id = '/' + os.path.join('static', build_id, file_name)
716 _Log("Payload requested, returning: %s", build_id)
717 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -0700718
719 @cherrypy.expose
720 def xbuddy_list(self):
721 """Lists the currently available images & time since last access.
722
723 @return: A string representation of a list of tuples
724 [(build_id, time since last access),...]
725 """
726 return self._xbuddy.List()
727
728 @cherrypy.expose
729 def xbuddy_capacity(self):
730 """Returns the number of images cached by xBuddy.
731
732 @return: Capacity of this devserver.
733 """
734 return self._xbuddy.Capacity()
735
736 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700737 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700738 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700739 return ('Welcome to the Dev Server!<br>\n'
740 '<br>\n'
741 'Here are the available methods, click for documentation:<br>\n'
742 '<br>\n'
743 '%s' %
744 '<br>\n'.join(
745 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700746 for name in _FindExposedMethods(
747 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700748
749 @cherrypy.expose
750 def doc(self, *args):
751 """Shows the documentation for available methods / URLs.
752
753 Example:
754 http://myhost/doc/update
755 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700756 name = '/'.join(args)
757 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700758 if not method:
759 raise DevServerError("No exposed method named `%s'" % name)
760 if not method.__doc__:
761 raise DevServerError("No documentation for exposed method `%s'" % name)
762 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -0700763
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700764 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700765 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700766 """Handles an update check from a Chrome OS client.
767
768 The HTTP request should contain the standard Omaha-style XML blob. The URL
769 line may contain an additional intermediate path to the update payload.
770
joychen121fc9b2013-08-02 14:30:30 -0700771 This request can be handled in one of 4 ways, depending on the devsever
772 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -0700773
joychen121fc9b2013-08-02 14:30:30 -0700774 1. No intermediate path
775 If no intermediate path is given, the default behavior is to generate an
776 update payload from the latest test image locally built for the board
777 specified in the xml. Devserver serves the generated payload.
778
779 2. Path explicitly invokes XBuddy
780 If there is a path given, it can explicitly invoke xbuddy by prefixing it
781 with 'xbuddy'. This path is then used to acquire an image binary for the
782 devserver to generate an update payload from. Devserver then serves this
783 payload.
784
785 3. Path is left for the devserver to interpret.
786 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
787 to generate a payload from the test image in that directory and serve it.
788
789 4. The devserver is in a 'forced' mode. TO BE DEPRECATED
790 This comes from the usage of --forced_payload or --image when starting the
791 devserver. No matter what path (or no path) gets passed in, devserver will
792 serve the update payload (--forced_payload) or generate an update payload
793 from the image (--image).
794
795 Examples:
796 1. No intermediate path
797 update_engine_client --omaha_url=http://myhost/update
798 This generates an update payload from the latest test image locally built
799 for the board specified in the xml.
800
801 2. Explicitly invoke xbuddy
802 update_engine_client --omaha_url=
803 http://myhost/update/xbuddy/remote/board/version/dev
804 This would go to GS to download the dev image for the board, from which
805 the devserver would generate a payload to serve.
806
807 3. Give a path for devserver to interpret
808 update_engine_client --omaha_url=http://myhost/update/some/random/path
809 This would attempt, in order to:
810 a) Generate an update from a test image binary if found in
811 static_dir/some/random/path.
812 b) Serve an update payload found in static_dir/some/random/path.
813 c) Hope that some/random/path takes the form "board/version" and
814 and attempt to download an update payload for that board/version
815 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700816 """
joychen121fc9b2013-08-02 14:30:30 -0700817 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -0800818 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -0700819 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-11 19:49:01 -0700820
joychen121fc9b2013-08-02 14:30:30 -0700821 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700822
Dan Shif5ce2de2013-04-25 16:06:32 -0700823 @cherrypy.expose
824 def check_health(self):
825 """Collect the health status of devserver to see if it's ready for staging.
826
827 @return: A JSON dictionary containing all or some of the following fields:
Dan Shi59ae7092013-06-04 14:37:27 -0700828 free_disk (int): free disk space in GB
829 staging_thread_count (int): number of devserver threads currently
830 staging an image
Dan Shif5ce2de2013-04-25 16:06:32 -0700831 """
832 # Get free disk space.
833 stat = os.statvfs(updater.static_dir)
834 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
835
836 return json.dumps({
837 'free_disk': free_disk,
Dan Shi59ae7092013-06-04 14:37:27 -0700838 'staging_thread_count': DevServerRoot._staging_thread_count,
Dan Shif5ce2de2013-04-25 16:06:32 -0700839 })
840
841
Chris Sosadbc20082012-12-10 13:39:11 -0800842def _CleanCache(cache_dir, wipe):
843 """Wipes any excess cached items in the cache_dir.
844
845 Args:
846 cache_dir: the directory we are wiping from.
847 wipe: If True, wipe all the contents -- not just the excess.
848 """
849 if wipe:
850 # Clear the cache and exit on error.
851 cmd = 'rm -rf %s/*' % cache_dir
852 if os.system(cmd) != 0:
853 _Log('Failed to clear the cache with %s' % cmd)
854 sys.exit(1)
855 else:
856 # Clear all but the last N cached updates
857 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
858 (cache_dir, CACHED_ENTRIES))
859 if os.system(cmd) != 0:
860 _Log('Failed to clean up old delta cache files with %s' % cmd)
861 sys.exit(1)
862
863
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700864def _AddTestingOptions(parser):
865 group = optparse.OptionGroup(
866 parser, 'Advanced Testing Options', 'These are used by test scripts and '
867 'developers writing integration tests utilizing the devserver. They are '
868 'not intended to be really used outside the scope of someone '
869 'knowledgable about the test.')
870 group.add_option('--exit',
871 action='store_true',
872 help='do not start the server (yet pregenerate/clear cache)')
873 group.add_option('--host_log',
874 action='store_true', default=False,
875 help='record history of host update events (/api/hostlog)')
876 group.add_option('--max_updates',
877 metavar='NUM', default= -1, type='int',
878 help='maximum number of update checks handled positively '
879 '(default: unlimited)')
880 group.add_option('--private_key',
881 metavar='PATH', default=None,
882 help='path to the private key in pem format. If this is set '
883 'the devserver will generate update payloads that are '
884 'signed with this key.')
885 group.add_option('--proxy_port',
886 metavar='PORT', default=None, type='int',
887 help='port to have the client connect to -- basically the '
888 'devserver lies to the update to tell it to get the payload '
889 'from a different port that will proxy the request back to '
890 'the devserver. The proxy must be managed outside the '
891 'devserver.')
892 group.add_option('--remote_payload',
893 action='store_true', default=False,
894 help='Payload is being served from a remote machine')
895 group.add_option('-u', '--urlbase',
896 metavar='URL',
897 help='base URL for update images, other than the '
898 'devserver. Use in conjunction with remote_payload.')
899 parser.add_option_group(group)
900
901
902def _AddUpdateOptions(parser):
903 group = optparse.OptionGroup(
904 parser, 'Autoupdate Options', 'These options can be used to change '
905 'how the devserver either generates or serve update payloads. Please '
906 'note that all of these option affect how a payload is generated and so '
907 'do not work in archive-only mode.')
908 group.add_option('--board',
909 help='By default the devserver will create an update '
910 'payload from the latest image built for the board '
911 'a device that is requesting an update has. When we '
912 'pre-generate an update (see below) and we do not specify '
913 'another update_type option like image or payload, the '
914 'devserver needs to know the board to generate the latest '
915 'image for. This is that board.')
916 group.add_option('--critical_update',
917 action='store_true', default=False,
918 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700919 group.add_option('--image',
920 metavar='FILE',
921 help='Generate and serve an update using this image to any '
922 'device that requests an update.')
923 group.add_option('--no_patch_kernel',
924 dest='patch_kernel', action='store_false', default=True,
925 help='When generating an update payload, do not patch the '
926 'kernel with kernel verification blob from the stateful '
927 'partition.')
928 group.add_option('--payload',
929 metavar='PATH',
930 help='use the update payload from specified directory '
931 '(update.gz).')
932 group.add_option('-p', '--pregenerate_update',
933 action='store_true', default=False,
934 help='pre-generate the update payload before accepting '
935 'update requests. Useful to help debug payload generation '
936 'issues quickly. Also if an update payload will take a '
937 'long time to generate, a client may timeout if you do not'
938 'pregenerate the update.')
939 group.add_option('--src_image',
940 metavar='PATH', default='',
941 help='If specified, delta updates will be generated using '
942 'this image as the source image. Delta updates are when '
943 'you are updating from a "source image" to a another '
944 'image.')
945 parser.add_option_group(group)
946
947
948def _AddProductionOptions(parser):
949 group = optparse.OptionGroup(
950 parser, 'Advanced Server Options', 'These options can be used to changed '
951 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700952 group.add_option('--clear_cache',
953 action='store_true', default=False,
954 help='At startup, removes all cached entries from the'
955 'devserver\'s cache.')
956 group.add_option('--logfile',
957 metavar='PATH',
958 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -0700959 group.add_option('--pidfile',
960 metavar='PATH',
961 help='path to output a pid file for the server.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700962 group.add_option('--production',
963 action='store_true', default=False,
964 help='have the devserver use production values when '
965 'starting up. This includes using more threads and '
966 'performing less logging.')
967 parser.add_option_group(group)
968
969
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700970def _MakeLogHandler(logfile):
971 """Create a LogHandler instance used to log all messages."""
972 hdlr_cls = handlers.TimedRotatingFileHandler
973 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
974 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -0700975 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700976 return hdlr
977
978
Chris Sosacde6bf42012-05-31 18:36:39 -0700979def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700980 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -0800981 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -0700982
983 # get directory that the devserver is run from
984 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -0700985 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -0700986 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700987 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -0700988 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -0700989 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700990 parser.add_option('--port',
991 default=8080, type='int',
992 help='port for the dev server to use (default: 8080)')
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700993 parser.add_option('-t', '--test_image',
994 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -0700995 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -0700996 parser.add_option('-x', '--xbuddy_manage_builds',
997 action='store_true',
998 default=False,
999 help='If set, allow xbuddy to manage images in'
1000 'build/images.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001001 _AddProductionOptions(parser)
1002 _AddUpdateOptions(parser)
1003 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -07001004 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001005
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001006 # Handle options that must be set globally in cherrypy. Do this
1007 # work up front, because calls to _Log() below depend on this
1008 # initialization.
1009 if options.production:
1010 cherrypy.config.update({'environment': 'production'})
1011 if not options.logfile:
1012 cherrypy.config.update({'log.screen': True})
1013 else:
1014 cherrypy.config.update({'log.error_file': '',
1015 'log.access_file': ''})
1016 hdlr = _MakeLogHandler(options.logfile)
1017 # Pylint can't seem to process these two calls properly
1018 # pylint: disable=E1101
1019 cherrypy.log.access_log.addHandler(hdlr)
1020 cherrypy.log.error_log.addHandler(hdlr)
1021 # pylint: enable=E1101
1022
Chris Sosa7c931362010-10-11 19:49:01 -07001023 root_dir = os.path.realpath('%s/../..' % devserver_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001024
joychened64b222013-06-21 16:39:34 -07001025 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001026 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001027
joychened64b222013-06-21 16:39:34 -07001028 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001029 # If our devserver is only supposed to serve payloads, we shouldn't be
1030 # mucking with the cache at all. If the devserver hadn't previously
1031 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001032 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001033 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001034 else:
1035 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001036
Chris Sosadbc20082012-12-10 13:39:11 -08001037 _Log('Using cache directory %s' % cache_dir)
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001038 _Log('Source root is %s' % root_dir)
joychened64b222013-06-21 16:39:34 -07001039 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001040
joychen121fc9b2013-08-02 14:30:30 -07001041 _xbuddy = xbuddy.XBuddy(options.xbuddy_manage_builds,
1042 options.board,
1043 root_dir=root_dir,
1044 static_dir=options.static_dir)
1045
Chris Sosa6a3697f2013-01-29 16:44:43 -08001046 # We allow global use here to share with cherrypy classes.
1047 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001048 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001049 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001050 _xbuddy,
Andrew de los Reyes52620802010-04-12 13:40:07 -07001051 root_dir=root_dir,
joychened64b222013-06-21 16:39:34 -07001052 static_dir=options.static_dir,
Andrew de los Reyes52620802010-04-12 13:40:07 -07001053 urlbase=options.urlbase,
Chris Sosa5d342a22010-09-28 16:54:41 -07001054 forced_image=options.image,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001055 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001056 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-28 23:42:37 -07001057 src_image=options.src_image,
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001058 patch_kernel=options.patch_kernel,
Chris Sosa08d55a22011-01-19 16:08:02 -08001059 board=options.board,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001060 copy_to_static_root=not options.exit,
1061 private_key=options.private_key,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001062 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001063 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001064 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001065 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001066 )
Chris Sosa7c931362010-10-11 19:49:01 -07001067
Chris Sosa6a3697f2013-01-29 16:44:43 -08001068 if options.pregenerate_update:
1069 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 15:46:22 -07001070
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001071 if options.exit:
1072 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001073
joychen3cb228e2013-06-12 12:13:13 -07001074 dev_server = DevServerRoot(_xbuddy)
1075
Chris Sosa855b8932013-08-21 13:24:55 -07001076 if options.pidfile:
1077 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1078
joychen3cb228e2013-06-12 12:13:13 -07001079 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001080
1081
1082if __name__ == '__main__':
1083 main()