blob: dd0243079e213bf0e99b64e5094385e3326c1265 [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
Scott Zawalski4647ce62012-01-03 17:17:28 -050097def _LeadingWhiteSpaceCount(string):
98 """Count the amount of leading whitespace in a string.
99
100 Args:
101 string: The string to count leading whitespace in.
102 Returns:
103 number of white space chars before characters start.
104 """
105 matched = re.match('^\s+', string)
106 if matched:
107 return len(matched.group())
108
109 return 0
110
111
112def _PrintDocStringAsHTML(func):
113 """Make a functions docstring somewhat HTML style.
114
115 Args:
116 func: The function to return the docstring from.
117 Returns:
118 A string that is somewhat formated for a web browser.
119 """
120 # TODO(scottz): Make this parse Args/Returns in a prettier way.
121 # Arguments could be bolded and indented etc.
122 html_doc = []
123 for line in func.__doc__.splitlines():
124 leading_space = _LeadingWhiteSpaceCount(line)
125 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700126 line = '&nbsp;' * leading_space + line
Scott Zawalski4647ce62012-01-03 17:17:28 -0500127
128 html_doc.append('<BR>%s' % line)
129
130 return '\n'.join(html_doc)
131
132
Chris Sosa7c931362010-10-11 19:49:01 -0700133def _GetConfig(options):
134 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800135
136 # On a system with IPv6 not compiled into the kernel,
137 # AF_INET6 sockets will return a socket.error exception.
138 # On such systems, fall-back to IPv4.
139 socket_host = '::'
140 try:
141 socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
142 except socket.error:
143 socket_host = '0.0.0.0'
144
Chris Sosa7c931362010-10-11 19:49:01 -0700145 base_config = { 'global':
146 { 'server.log_request_headers': True,
147 'server.protocol_version': 'HTTP/1.1',
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800148 'server.socket_host': socket_host,
Chris Sosa7c931362010-10-11 19:49:01 -0700149 'server.socket_port': int(options.port),
Chris Sosa374c62d2010-10-14 09:13:54 -0700150 'response.timeout': 6000,
Chris Sosa6fe23942012-07-02 15:44:46 -0700151 'request.show_tracebacks': True,
Chris Sosa72333d12012-06-13 11:28:05 -0700152 'server.socket_timeout': 60,
joychenecc02aa2013-07-17 18:27:35 -0700153 'server.thread_pool': 2,
Chris Sosa7c931362010-10-11 19:49:01 -0700154 },
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700155 '/api':
156 {
157 # Gets rid of cherrypy parsing post file for args.
158 'request.process_request_body': False,
159 },
Chris Sosaa1ef0102010-10-21 16:22:35 -0700160 '/build':
161 {
162 'response.timeout': 100000,
163 },
Chris Sosa7c931362010-10-11 19:49:01 -0700164 '/update':
165 {
166 # Gets rid of cherrypy parsing post file for args.
167 'request.process_request_body': False,
Chris Sosaf65f4b92010-10-21 15:57:51 -0700168 'response.timeout': 10000,
Chris Sosa7c931362010-10-11 19:49:01 -0700169 },
170 # Sets up the static dir for file hosting.
171 '/static':
joychened64b222013-06-21 16:39:34 -0700172 { 'tools.staticdir.dir': options.static_dir,
Chris Sosa7c931362010-10-11 19:49:01 -0700173 'tools.staticdir.on': True,
Chris Sosaf65f4b92010-10-21 15:57:51 -0700174 'response.timeout': 10000,
Chris Sosa7c931362010-10-11 19:49:01 -0700175 },
176 }
Chris Sosa5f118ef2012-07-12 11:37:50 -0700177 if options.production:
Alex Miller93beca52013-07-30 19:25:09 -0700178 base_config['global'].update({'server.thread_pool': 150})
Scott Zawalski1c5e7cd2012-02-27 13:12:52 -0500179
Chris Sosa7c931362010-10-11 19:49:01 -0700180 return base_config
rtc@google.com64244662009-11-12 00:52:08 +0000181
Darin Petkove17164a2010-08-11 13:24:41 -0700182
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700183def _GetRecursiveMemberObject(root, member_list):
184 """Returns an object corresponding to a nested member list.
185
186 Args:
187 root: the root object to search
188 member_list: list of nested members to search
189 Returns:
190 An object corresponding to the member name list; None otherwise.
191 """
192 for member in member_list:
193 next_root = root.__class__.__dict__.get(member)
194 if not next_root:
195 return None
196 root = next_root
197 return root
198
199
200def _IsExposed(name):
201 """Returns True iff |name| has an `exposed' attribute and it is set."""
202 return hasattr(name, 'exposed') and name.exposed
203
204
Gilad Arnold748c8322012-10-12 09:51:35 -0700205def _GetExposedMethod(root, nested_member, ignored=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700206 """Returns a CherryPy-exposed method, if such exists.
207
208 Args:
209 root: the root object for searching
210 nested_member: a slash-joined path to the nested member
211 ignored: method paths to be ignored
212 Returns:
213 A function object corresponding to the path defined by |member_list| from
214 the |root| object, if the function is exposed and not ignored; None
215 otherwise.
216 """
Gilad Arnold748c8322012-10-12 09:51:35 -0700217 method = (not (ignored and nested_member in ignored) and
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700218 _GetRecursiveMemberObject(root, nested_member.split('/')))
219 if (method and type(method) == types.FunctionType and _IsExposed(method)):
220 return method
221
222
Gilad Arnold748c8322012-10-12 09:51:35 -0700223def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700224 """Finds exposed CherryPy methods.
225
226 Args:
227 root: the root object for searching
228 prefix: slash-joined chain of members leading to current object
229 unlisted: URLs to be excluded regardless of their exposed status
230 Returns:
231 List of exposed URLs that are not unlisted.
232 """
233 method_list = []
234 for member in sorted(root.__class__.__dict__.keys()):
235 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 09:51:35 -0700236 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700237 continue
238 member_obj = root.__class__.__dict__[member]
239 if _IsExposed(member_obj):
240 if type(member_obj) == types.FunctionType:
241 method_list.append(prefixed_member)
242 else:
243 method_list += _FindExposedMethods(
244 member_obj, prefixed_member, unlisted)
245 return method_list
246
247
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700248class ApiRoot(object):
249 """RESTful API for Dev Server information."""
250 exposed = True
251
252 @cherrypy.expose
253 def hostinfo(self, ip):
254 """Returns a JSON dictionary containing information about the given ip.
255
Gilad Arnold1b908392012-10-05 11:36:27 -0700256 Args:
257 ip: address of host whose info is requested
258 Returns:
259 A JSON dictionary containing all or some of the following fields:
260 last_event_type (int): last update event type received
261 last_event_status (int): last update event status received
262 last_known_version (string): last known version reported in update ping
263 forced_update_label (string): update label to force next update ping to
264 use, set by setnextupdate
265 See the OmahaEvent class in update_engine/omaha_request_action.h for
266 event type and status code definitions. If the ip does not exist an empty
267 string is returned.
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700268
Gilad Arnold1b908392012-10-05 11:36:27 -0700269 Example URL:
270 http://myhost/api/hostinfo?ip=192.168.1.5
271 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700272 return updater.HandleHostInfoPing(ip)
273
274 @cherrypy.expose
Gilad Arnold286a0062012-01-12 13:47:02 -0800275 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 11:36:27 -0700276 """Returns a JSON object containing a log of host event.
277
278 Args:
279 ip: address of host whose event log is requested, or `all'
280 Returns:
281 A JSON encoded list (log) of dictionaries (events), each of which
282 containing a `timestamp' and other event fields, as described under
283 /api/hostinfo.
284
285 Example URL:
286 http://myhost/api/hostlog?ip=192.168.1.5
287 """
Gilad Arnold286a0062012-01-12 13:47:02 -0800288 return updater.HandleHostLogPing(ip)
289
290 @cherrypy.expose
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700291 def setnextupdate(self, ip):
292 """Allows the response to the next update ping from a host to be set.
293
294 Takes the IP of the host and an update label as normally provided to the
Gilad Arnold1b908392012-10-05 11:36:27 -0700295 /update command.
296 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700297 body_length = int(cherrypy.request.headers['Content-Length'])
298 label = cherrypy.request.rfile.read(body_length)
299
300 if label:
301 label = label.strip()
302 if label:
303 return updater.HandleSetUpdatePing(ip, label)
304 raise cherrypy.HTTPError(400, 'No label provided.')
305
306
Gilad Arnold55a2a372012-10-02 09:46:32 -0700307 @cherrypy.expose
308 def fileinfo(self, *path_args):
309 """Returns information about a given staged file.
310
311 Args:
312 path_args: path to the file inside the server's static staging directory
313 Returns:
314 A JSON encoded dictionary with information about the said file, which may
315 contain the following keys/values:
Gilad Arnold1b908392012-10-05 11:36:27 -0700316 size (int): the file size in bytes
317 sha1 (string): a base64 encoded SHA1 hash
318 sha256 (string): a base64 encoded SHA256 hash
319
320 Example URL:
321 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 09:46:32 -0700322 """
323 file_path = os.path.join(updater.static_dir, *path_args)
324 if not os.path.exists(file_path):
325 raise DevServerError('file not found: %s' % file_path)
326 try:
327 file_size = os.path.getsize(file_path)
328 file_sha1 = common_util.GetFileSha1(file_path)
329 file_sha256 = common_util.GetFileSha256(file_path)
330 except os.error, e:
331 raise DevServerError('failed to get info for file %s: %s' %
Gilad Arnolde74b3812013-04-22 11:27:38 -0700332 (file_path, e))
333
334 is_delta = autoupdate.Autoupdate.IsDeltaFormatFile(file_path)
335
336 return json.dumps({
337 autoupdate.Autoupdate.SIZE_ATTR: file_size,
338 autoupdate.Autoupdate.SHA1_ATTR: file_sha1,
339 autoupdate.Autoupdate.SHA256_ATTR: file_sha256,
340 autoupdate.Autoupdate.ISDELTA_ATTR: is_delta
341 })
Gilad Arnold55a2a372012-10-02 09:46:32 -0700342
Chris Sosa76e44b92013-01-31 12:11:38 -0800343
David Rochberg7c79a812011-01-19 14:24:45 -0500344class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700345 """The Root Class for the Dev Server.
346
347 CherryPy works as follows:
348 For each method in this class, cherrpy interprets root/path
349 as a call to an instance of DevServerRoot->method_name. For example,
350 a call to http://myhost/build will call build. CherryPy automatically
351 parses http args and places them as keyword arguments in each method.
352 For paths http://myhost/update/dir1/dir2, you can use *args so that
353 cherrypy uses the update method and puts the extra paths in args.
354 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700355 # Method names that should not be listed on the index page.
356 _UNLISTED_METHODS = ['index', 'doc']
357
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700358 api = ApiRoot()
Chris Sosa7c931362010-10-11 19:49:01 -0700359
Dan Shi59ae7092013-06-04 14:37:27 -0700360 # Number of threads that devserver is staging images.
361 _staging_thread_count = 0
362 # Lock used to lock increasing/decreasing count.
363 _staging_thread_count_lock = threading.Lock()
364
joychen3cb228e2013-06-12 12:13:13 -0700365 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700366 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800367 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 12:13:13 -0700368 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 14:24:45 -0500369
Chris Sosa6b0c6172013-08-05 17:01:33 -0700370 @staticmethod
371 def _get_artifacts(kwargs):
372 """Returns a tuple of named and file artifacts given the stage rpc kwargs.
373
374 Raises: DevserverError if no artifacts would be returned.
375 """
376 artifacts = kwargs.get('artifacts')
377 files = kwargs.get('files')
378 if not artifacts and not files:
379 raise DevServerError('No artifacts specified.')
380
381 return (artifacts.split(',') if artifacts else [],
382 files.split(',') if files else [])
383
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700384 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500385 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700386 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700387 import builder
388 if self._builder is None:
389 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500390 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700391
Chris Sosacde6bf42012-05-31 18:36:39 -0700392 @staticmethod
393 def _canonicalize_archive_url(archive_url):
394 """Canonicalizes archive_url strings.
395
396 Raises:
397 DevserverError: if archive_url is not set.
398 """
399 if archive_url:
Chris Sosa76e44b92013-01-31 12:11:38 -0800400 if not archive_url.startswith('gs://'):
401 raise DevServerError("Archive URL isn't from Google Storage.")
402
Chris Sosacde6bf42012-05-31 18:36:39 -0700403 return archive_url.rstrip('/')
404 else:
405 raise DevServerError("Must specify an archive_url in the request")
406
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700407 @cherrypy.expose
Dan Shif8eb0d12013-08-01 17:52:06 -0700408 def is_staged(self, **kwargs):
409 """Check if artifacts have been downloaded.
410
Chris Sosa6b0c6172013-08-05 17:01:33 -0700411 async: True to return without waiting for download to complete.
412 artifacts: Comma separated list of named artifacts to download.
413 These are defined in artifact_info and have their implementation
414 in build_artifact.py.
415 files: Comma separated list of file artifacts to stage. These
416 will be available as is in the corresponding static directory with no
417 custom post-processing.
418
419 returns: True of all artifacts are staged.
Dan Shif8eb0d12013-08-01 17:52:06 -0700420
421 Example:
422 To check if autotest and test_suites are staged:
423 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
424 artifacts=autotest,test_suites
425 """
426 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
Chris Sosa6b0c6172013-08-05 17:01:33 -0700427 artifacts, files = self._get_artifacts(kwargs)
Dan Shif8eb0d12013-08-01 17:52:06 -0700428 return str(downloader.Downloader(updater.static_dir, archive_url).IsStaged(
Chris Sosa6b0c6172013-08-05 17:01:33 -0700429 artifacts, files))
Dan Shi59ae7092013-06-04 14:37:27 -0700430
Chris Sosa76e44b92013-01-31 12:11:38 -0800431 @cherrypy.expose
432 def stage(self, **kwargs):
433 """Downloads and caches the artifacts from Google Storage URL.
434
435 Downloads and caches the artifacts Google Storage URL. Returns once these
436 have been downloaded on the devserver. A call to this will attempt to cache
437 non-specified artifacts in the background for the given from the given URL
438 following the principle of spatial locality. Spatial locality of different
439 artifacts is explicitly defined in the build_artifact module.
440
441 These artifacts will then be available from the static/ sub-directory of
442 the devserver.
443
444 Args:
445 archive_url: Google Storage URL for the build.
Dan Shif8eb0d12013-08-01 17:52:06 -0700446 async: True to return without waiting for download to complete.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700447 artifacts: Comma separated list of named artifacts to download.
448 These are defined in artifact_info and have their implementation
449 in build_artifact.py.
450 files: Comma separated list of files to stage. These
451 will be available as is in the corresponding static directory with no
452 custom post-processing.
Chris Sosa76e44b92013-01-31 12:11:38 -0800453
454 Example:
455 To download the autotest and test suites tarballs:
456 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
457 artifacts=autotest,test_suites
458 To download the full update payload:
459 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
460 artifacts=full_payload
Chris Sosa6b0c6172013-08-05 17:01:33 -0700461 To download just a file called blah.bin:
462 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
463 files=blah.bin
Chris Sosa76e44b92013-01-31 12:11:38 -0800464
465 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 16:39:34 -0700466 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 12:11:38 -0800467
468 Note for this example, relative path is the archive_url stripped of its
469 basename i.e. path/ in the examples above. Specific example:
470
471 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
472
473 Will get staged to:
474
joychened64b222013-06-21 16:39:34 -0700475 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Chris Sosa76e44b92013-01-31 12:11:38 -0800476 """
Chris Sosacde6bf42012-05-31 18:36:39 -0700477 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
Dan Shif8eb0d12013-08-01 17:52:06 -0700478 async = kwargs.get('async', False)
Chris Sosa6b0c6172013-08-05 17:01:33 -0700479 artifacts, files = self._get_artifacts(kwargs)
Dan Shi59ae7092013-06-04 14:37:27 -0700480 with DevServerRoot._staging_thread_count_lock:
481 DevServerRoot._staging_thread_count += 1
482 try:
Chris Sosa6b0c6172013-08-05 17:01:33 -0700483 downloader.Downloader(updater.static_dir, archive_url).Download(
484 artifacts, files, async=async)
Dan Shi59ae7092013-06-04 14:37:27 -0700485 finally:
486 with DevServerRoot._staging_thread_count_lock:
487 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800488 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700489
490 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -0800491 def setup_telemetry(self, **kwargs):
492 """Extracts and sets up telemetry
493
494 This method goes through the telemetry deps packages, and stages them on
495 the devserver to be used by the drones and the telemetry tests.
496
497 Args:
498 archive_url: Google Storage URL for the build.
499
500 Returns:
501 Path to the source folder for the telemetry codebase once it is staged.
502 """
503 archive_url = kwargs.get('archive_url')
504 self.stage(archive_url=archive_url, artifacts='autotest')
505
506 build = '/'.join(downloader.Downloader.ParseUrl(archive_url))
507 build_path = os.path.join(updater.static_dir, build)
508 deps_path = os.path.join(build_path, 'autotest/packages')
509 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
510 src_folder = os.path.join(telemetry_path, 'src')
511
512 with self._telemetry_lock_dict.lock(telemetry_path):
513 if os.path.exists(src_folder):
514 # Telemetry is already fully stage return
515 return src_folder
516
517 common_util.MkDirP(telemetry_path)
518
519 # Copy over the required deps tar balls to the telemetry directory.
520 for dep in TELEMETRY_DEPS:
521 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -0700522 if not os.path.exists(dep_path):
523 # This dep does not exist (could be new), do not extract it.
524 continue
Simran Basi4baad082013-02-14 13:39:18 -0800525 try:
526 common_util.ExtractTarball(dep_path, telemetry_path)
527 except common_util.CommonUtilError as e:
528 shutil.rmtree(telemetry_path)
529 raise DevServerError(str(e))
530
531 # By default all the tarballs extract to test_src but some parts of
532 # the telemetry code specifically hardcoded to exist inside of 'src'.
533 test_src = os.path.join(telemetry_path, 'test_src')
534 try:
535 shutil.move(test_src, src_folder)
536 except shutil.Error:
537 # This can occur if src_folder already exists. Remove and retry move.
538 shutil.rmtree(src_folder)
539 raise DevServerError('Failure in telemetry setup for build %s. Appears'
540 ' that the test_src to src move failed.' % build)
541
542 return src_folder
543
544 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800545 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -0700546 """Symbolicates a minidump using pre-downloaded symbols, returns it.
547
548 Callers will need to POST to this URL with a body of MIME-type
549 "multipart/form-data".
550 The body should include a single argument, 'minidump', containing the
551 binary-formatted minidump to symbolicate.
552
Chris Masone816e38c2012-05-02 12:22:36 -0700553 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -0800554 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -0700555 minidump: The binary minidump file to symbolicate.
556 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800557 # Ensure the symbols have been staged.
558 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
559 if self.stage(archive_url=archive_url, artifacts='symbols') != 'Success':
560 raise DevServerError('Failed to stage symbols for %s' % archive_url)
561
Chris Masone816e38c2012-05-02 12:22:36 -0700562 to_return = ''
563 with tempfile.NamedTemporaryFile() as local:
564 while True:
565 data = minidump.file.read(8192)
566 if not data:
567 break
568 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -0800569
Chris Masone816e38c2012-05-02 12:22:36 -0700570 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -0800571
572 symbols_directory = os.path.join(downloader.Downloader.GetBuildDir(
573 updater.static_dir, archive_url), 'debug', 'breakpad')
574
575 stackwalk = subprocess.Popen(
576 ['minidump_stackwalk', local.name, symbols_directory],
577 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
578
Chris Masone816e38c2012-05-02 12:22:36 -0700579 to_return, error_text = stackwalk.communicate()
580 if stackwalk.returncode != 0:
581 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
582 error_text, stackwalk.returncode))
583
584 return to_return
585
586 @cherrypy.expose
Scott Zawalski16954532012-03-20 15:31:36 -0400587 def latestbuild(self, **params):
588 """Return a string representing the latest build for a given target.
589
590 Args:
591 target: The build target, typically a combination of the board and the
592 type of build e.g. x86-mario-release.
593 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
594 provided the latest RXX build will be returned.
595 Returns:
596 A string representation of the latest build if one exists, i.e.
597 R19-1993.0.0-a1-b1480.
598 An empty string if no latest could be found.
599 """
600 if not params:
601 return _PrintDocStringAsHTML(self.latestbuild)
602
603 if 'target' not in params:
604 raise cherrypy.HTTPError('500 Internal Server Error',
605 'Error: target= is required!')
606 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700607 return common_util.GetLatestBuildVersion(
Scott Zawalski16954532012-03-20 15:31:36 -0400608 updater.static_dir, params['target'],
609 milestone=params.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700610 except common_util.CommonUtilError as errmsg:
Scott Zawalski16954532012-03-20 15:31:36 -0400611 raise cherrypy.HTTPError('500 Internal Server Error', str(errmsg))
612
613 @cherrypy.expose
Scott Zawalski84a39c92012-01-13 15:12:42 -0500614 def controlfiles(self, **params):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500615 """Return a control file or a list of all known control files.
616
617 Example URL:
618 To List all control files:
beepsbd337242013-07-09 22:44:06 -0700619 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
620 To List all control files for, say, the bvt suite:
621 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -0500622 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500623 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 -0500624
625 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500626 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500627 control_path: If you want the contents of a control file set this
628 to the path. E.g. client/site_tests/sleeptest/control
629 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -0700630 suite_name: If control_path is not specified but a suite_name is
631 specified, list the control files belonging to that suite instead of
632 all control files. The empty string for suite_name will list all control
633 files for the build.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500634 Returns:
635 Contents of a control file if control_path is provided.
636 A list of control files if no control_path is provided.
637 """
Scott Zawalski4647ce62012-01-03 17:17:28 -0500638 if not params:
639 return _PrintDocStringAsHTML(self.controlfiles)
640
Scott Zawalski84a39c92012-01-13 15:12:42 -0500641 if 'build' not in params:
Scott Zawalski4647ce62012-01-03 17:17:28 -0500642 raise cherrypy.HTTPError('500 Internal Server Error',
Scott Zawalski84a39c92012-01-13 15:12:42 -0500643 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -0500644
645 if 'control_path' not in params:
beepsbd337242013-07-09 22:44:06 -0700646 if 'suite_name' in params and params['suite_name']:
647 return common_util.GetControlFileListForSuite(
648 updater.static_dir, params['build'], params['suite_name'])
649 else:
650 return common_util.GetControlFileList(
651 updater.static_dir, params['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -0500652 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700653 return common_util.GetControlFile(
654 updater.static_dir, params['build'], params['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -0800655
656 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -0700657 def xbuddy(self, *args, **kwargs):
658 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -0700659
660 Args:
joycheneaf4cfc2013-07-02 08:38:57 -0700661 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -0700662 components of the path. The path can be understood as
663 "{local|remote}/build_id/artifact" where build_id is composed of
664 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -0700665
joychen121fc9b2013-08-02 14:30:30 -0700666 The first path element is optional, and can be "remote" or "local"
667 If local (the default), devserver will not attempt to access Google
668 Storage, and will only search the static directory for the files.
669 If remote, devserver will try to obtain the artifact off GS if it's
670 not found locally.
671 The board is the familiar board name, optionally suffixed.
672 The version can be the google storage version number, and may also be
673 any of a number of xBuddy defined version aliases that will be
674 translated into the latest built image that fits the description.
675 Defaults to latest.
676 The artifact is one of a number of image or artifact aliases used by
677 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -0700678
679 Kwargs:
joychen3cb228e2013-06-12 12:13:13 -0700680 return_dir: {true|false}
681 if set to true, returns the url to the update.gz
682 instead.
683
684 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -0700685 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -0700686 or
joycheneaf4cfc2013-07-02 08:38:57 -0700687 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -0700688
689 Returns:
690 A redirect to the image or update file on the devserver.
691 e.g. http://host:port/static/archive/x86-generic-release/
692 R26-4000.0.0/chromium-test-image.bin
693 or if return_dir is True, return path to the folder where
joychen121fc9b2013-08-02 14:30:30 -0700694 the artifact is.
joychen3cb228e2013-06-12 12:13:13 -0700695 http://host:port/static/x86-generic-release/R26-4000.0.0/
696 """
697 boolean_string = kwargs.get('return_dir')
698 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -0700699
700 build_id, file_name = self._xbuddy.Get(args)
joychen3cb228e2013-06-12 12:13:13 -0700701 if return_dir:
Chris Sosa855b8932013-08-21 13:24:55 -0700702 directory = os.path.join(cherrypy.request.base, 'static', build_id)
joycheneaf4cfc2013-07-02 08:38:57 -0700703 _Log("Directory requested, returning: %s", directory)
704 return directory
joychen3cb228e2013-06-12 12:13:13 -0700705 else:
joychen121fc9b2013-08-02 14:30:30 -0700706 build_id = '/' + os.path.join('static', build_id, file_name)
707 _Log("Payload requested, returning: %s", build_id)
708 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -0700709
710 @cherrypy.expose
711 def xbuddy_list(self):
712 """Lists the currently available images & time since last access.
713
714 @return: A string representation of a list of tuples
715 [(build_id, time since last access),...]
716 """
717 return self._xbuddy.List()
718
719 @cherrypy.expose
720 def xbuddy_capacity(self):
721 """Returns the number of images cached by xBuddy.
722
723 @return: Capacity of this devserver.
724 """
725 return self._xbuddy.Capacity()
726
727 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700728 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700729 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700730 return ('Welcome to the Dev Server!<br>\n'
731 '<br>\n'
732 'Here are the available methods, click for documentation:<br>\n'
733 '<br>\n'
734 '%s' %
735 '<br>\n'.join(
736 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700737 for name in _FindExposedMethods(
738 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700739
740 @cherrypy.expose
741 def doc(self, *args):
742 """Shows the documentation for available methods / URLs.
743
744 Example:
745 http://myhost/doc/update
746 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700747 name = '/'.join(args)
748 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700749 if not method:
750 raise DevServerError("No exposed method named `%s'" % name)
751 if not method.__doc__:
752 raise DevServerError("No documentation for exposed method `%s'" % name)
753 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -0700754
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700755 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700756 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700757 """Handles an update check from a Chrome OS client.
758
759 The HTTP request should contain the standard Omaha-style XML blob. The URL
760 line may contain an additional intermediate path to the update payload.
761
joychen121fc9b2013-08-02 14:30:30 -0700762 This request can be handled in one of 4 ways, depending on the devsever
763 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -0700764
joychen121fc9b2013-08-02 14:30:30 -0700765 1. No intermediate path
766 If no intermediate path is given, the default behavior is to generate an
767 update payload from the latest test image locally built for the board
768 specified in the xml. Devserver serves the generated payload.
769
770 2. Path explicitly invokes XBuddy
771 If there is a path given, it can explicitly invoke xbuddy by prefixing it
772 with 'xbuddy'. This path is then used to acquire an image binary for the
773 devserver to generate an update payload from. Devserver then serves this
774 payload.
775
776 3. Path is left for the devserver to interpret.
777 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
778 to generate a payload from the test image in that directory and serve it.
779
780 4. The devserver is in a 'forced' mode. TO BE DEPRECATED
781 This comes from the usage of --forced_payload or --image when starting the
782 devserver. No matter what path (or no path) gets passed in, devserver will
783 serve the update payload (--forced_payload) or generate an update payload
784 from the image (--image).
785
786 Examples:
787 1. No intermediate path
788 update_engine_client --omaha_url=http://myhost/update
789 This generates an update payload from the latest test image locally built
790 for the board specified in the xml.
791
792 2. Explicitly invoke xbuddy
793 update_engine_client --omaha_url=
794 http://myhost/update/xbuddy/remote/board/version/dev
795 This would go to GS to download the dev image for the board, from which
796 the devserver would generate a payload to serve.
797
798 3. Give a path for devserver to interpret
799 update_engine_client --omaha_url=http://myhost/update/some/random/path
800 This would attempt, in order to:
801 a) Generate an update from a test image binary if found in
802 static_dir/some/random/path.
803 b) Serve an update payload found in static_dir/some/random/path.
804 c) Hope that some/random/path takes the form "board/version" and
805 and attempt to download an update payload for that board/version
806 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700807 """
joychen121fc9b2013-08-02 14:30:30 -0700808 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -0800809 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -0700810 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-11 19:49:01 -0700811
joychen121fc9b2013-08-02 14:30:30 -0700812 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700813
Dan Shif5ce2de2013-04-25 16:06:32 -0700814 @cherrypy.expose
815 def check_health(self):
816 """Collect the health status of devserver to see if it's ready for staging.
817
818 @return: A JSON dictionary containing all or some of the following fields:
Dan Shi59ae7092013-06-04 14:37:27 -0700819 free_disk (int): free disk space in GB
820 staging_thread_count (int): number of devserver threads currently
821 staging an image
Dan Shif5ce2de2013-04-25 16:06:32 -0700822 """
823 # Get free disk space.
824 stat = os.statvfs(updater.static_dir)
825 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
826
827 return json.dumps({
828 'free_disk': free_disk,
Dan Shi59ae7092013-06-04 14:37:27 -0700829 'staging_thread_count': DevServerRoot._staging_thread_count,
Dan Shif5ce2de2013-04-25 16:06:32 -0700830 })
831
832
Chris Sosadbc20082012-12-10 13:39:11 -0800833def _CleanCache(cache_dir, wipe):
834 """Wipes any excess cached items in the cache_dir.
835
836 Args:
837 cache_dir: the directory we are wiping from.
838 wipe: If True, wipe all the contents -- not just the excess.
839 """
840 if wipe:
841 # Clear the cache and exit on error.
842 cmd = 'rm -rf %s/*' % cache_dir
843 if os.system(cmd) != 0:
844 _Log('Failed to clear the cache with %s' % cmd)
845 sys.exit(1)
846 else:
847 # Clear all but the last N cached updates
848 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
849 (cache_dir, CACHED_ENTRIES))
850 if os.system(cmd) != 0:
851 _Log('Failed to clean up old delta cache files with %s' % cmd)
852 sys.exit(1)
853
854
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700855def _AddTestingOptions(parser):
856 group = optparse.OptionGroup(
857 parser, 'Advanced Testing Options', 'These are used by test scripts and '
858 'developers writing integration tests utilizing the devserver. They are '
859 'not intended to be really used outside the scope of someone '
860 'knowledgable about the test.')
861 group.add_option('--exit',
862 action='store_true',
863 help='do not start the server (yet pregenerate/clear cache)')
864 group.add_option('--host_log',
865 action='store_true', default=False,
866 help='record history of host update events (/api/hostlog)')
867 group.add_option('--max_updates',
868 metavar='NUM', default= -1, type='int',
869 help='maximum number of update checks handled positively '
870 '(default: unlimited)')
871 group.add_option('--private_key',
872 metavar='PATH', default=None,
873 help='path to the private key in pem format. If this is set '
874 'the devserver will generate update payloads that are '
875 'signed with this key.')
876 group.add_option('--proxy_port',
877 metavar='PORT', default=None, type='int',
878 help='port to have the client connect to -- basically the '
879 'devserver lies to the update to tell it to get the payload '
880 'from a different port that will proxy the request back to '
881 'the devserver. The proxy must be managed outside the '
882 'devserver.')
883 group.add_option('--remote_payload',
884 action='store_true', default=False,
885 help='Payload is being served from a remote machine')
886 group.add_option('-u', '--urlbase',
887 metavar='URL',
888 help='base URL for update images, other than the '
889 'devserver. Use in conjunction with remote_payload.')
890 parser.add_option_group(group)
891
892
893def _AddUpdateOptions(parser):
894 group = optparse.OptionGroup(
895 parser, 'Autoupdate Options', 'These options can be used to change '
896 'how the devserver either generates or serve update payloads. Please '
897 'note that all of these option affect how a payload is generated and so '
898 'do not work in archive-only mode.')
899 group.add_option('--board',
900 help='By default the devserver will create an update '
901 'payload from the latest image built for the board '
902 'a device that is requesting an update has. When we '
903 'pre-generate an update (see below) and we do not specify '
904 'another update_type option like image or payload, the '
905 'devserver needs to know the board to generate the latest '
906 'image for. This is that board.')
907 group.add_option('--critical_update',
908 action='store_true', default=False,
909 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700910 group.add_option('--image',
911 metavar='FILE',
912 help='Generate and serve an update using this image to any '
913 'device that requests an update.')
914 group.add_option('--no_patch_kernel',
915 dest='patch_kernel', action='store_false', default=True,
916 help='When generating an update payload, do not patch the '
917 'kernel with kernel verification blob from the stateful '
918 'partition.')
919 group.add_option('--payload',
920 metavar='PATH',
921 help='use the update payload from specified directory '
922 '(update.gz).')
923 group.add_option('-p', '--pregenerate_update',
924 action='store_true', default=False,
925 help='pre-generate the update payload before accepting '
926 'update requests. Useful to help debug payload generation '
927 'issues quickly. Also if an update payload will take a '
928 'long time to generate, a client may timeout if you do not'
929 'pregenerate the update.')
930 group.add_option('--src_image',
931 metavar='PATH', default='',
932 help='If specified, delta updates will be generated using '
933 'this image as the source image. Delta updates are when '
934 'you are updating from a "source image" to a another '
935 'image.')
936 parser.add_option_group(group)
937
938
939def _AddProductionOptions(parser):
940 group = optparse.OptionGroup(
941 parser, 'Advanced Server Options', 'These options can be used to changed '
942 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700943 group.add_option('--clear_cache',
944 action='store_true', default=False,
945 help='At startup, removes all cached entries from the'
946 'devserver\'s cache.')
947 group.add_option('--logfile',
948 metavar='PATH',
949 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -0700950 group.add_option('--pidfile',
951 metavar='PATH',
952 help='path to output a pid file for the server.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700953 group.add_option('--production',
954 action='store_true', default=False,
955 help='have the devserver use production values when '
956 'starting up. This includes using more threads and '
957 'performing less logging.')
958 parser.add_option_group(group)
959
960
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700961def _MakeLogHandler(logfile):
962 """Create a LogHandler instance used to log all messages."""
963 hdlr_cls = handlers.TimedRotatingFileHandler
964 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
965 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -0700966 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700967 return hdlr
968
969
Chris Sosacde6bf42012-05-31 18:36:39 -0700970def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700971 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -0800972 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -0700973
974 # get directory that the devserver is run from
975 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -0700976 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -0700977 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700978 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -0700979 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -0700980 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700981 parser.add_option('--port',
982 default=8080, type='int',
983 help='port for the dev server to use (default: 8080)')
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700984 parser.add_option('-t', '--test_image',
985 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -0700986 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -0700987 parser.add_option('-x', '--xbuddy_manage_builds',
988 action='store_true',
989 default=False,
990 help='If set, allow xbuddy to manage images in'
991 'build/images.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700992 _AddProductionOptions(parser)
993 _AddUpdateOptions(parser)
994 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -0700995 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +0000996
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700997 # Handle options that must be set globally in cherrypy. Do this
998 # work up front, because calls to _Log() below depend on this
999 # initialization.
1000 if options.production:
1001 cherrypy.config.update({'environment': 'production'})
1002 if not options.logfile:
1003 cherrypy.config.update({'log.screen': True})
1004 else:
1005 cherrypy.config.update({'log.error_file': '',
1006 'log.access_file': ''})
1007 hdlr = _MakeLogHandler(options.logfile)
1008 # Pylint can't seem to process these two calls properly
1009 # pylint: disable=E1101
1010 cherrypy.log.access_log.addHandler(hdlr)
1011 cherrypy.log.error_log.addHandler(hdlr)
1012 # pylint: enable=E1101
1013
Chris Sosa7c931362010-10-11 19:49:01 -07001014 root_dir = os.path.realpath('%s/../..' % devserver_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001015
joychened64b222013-06-21 16:39:34 -07001016 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001017 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001018
joychened64b222013-06-21 16:39:34 -07001019 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001020 # If our devserver is only supposed to serve payloads, we shouldn't be
1021 # mucking with the cache at all. If the devserver hadn't previously
1022 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001023 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001024 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001025 else:
1026 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001027
Chris Sosadbc20082012-12-10 13:39:11 -08001028 _Log('Using cache directory %s' % cache_dir)
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001029 _Log('Source root is %s' % root_dir)
joychened64b222013-06-21 16:39:34 -07001030 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001031
joychen121fc9b2013-08-02 14:30:30 -07001032 _xbuddy = xbuddy.XBuddy(options.xbuddy_manage_builds,
1033 options.board,
1034 root_dir=root_dir,
1035 static_dir=options.static_dir)
1036
Chris Sosa6a3697f2013-01-29 16:44:43 -08001037 # We allow global use here to share with cherrypy classes.
1038 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001039 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001040 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001041 _xbuddy,
Andrew de los Reyes52620802010-04-12 13:40:07 -07001042 root_dir=root_dir,
joychened64b222013-06-21 16:39:34 -07001043 static_dir=options.static_dir,
Andrew de los Reyes52620802010-04-12 13:40:07 -07001044 urlbase=options.urlbase,
Chris Sosa5d342a22010-09-28 16:54:41 -07001045 forced_image=options.image,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001046 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001047 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-28 23:42:37 -07001048 src_image=options.src_image,
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001049 patch_kernel=options.patch_kernel,
Chris Sosa08d55a22011-01-19 16:08:02 -08001050 board=options.board,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001051 copy_to_static_root=not options.exit,
1052 private_key=options.private_key,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001053 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001054 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001055 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001056 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001057 )
Chris Sosa7c931362010-10-11 19:49:01 -07001058
Chris Sosa6a3697f2013-01-29 16:44:43 -08001059 if options.pregenerate_update:
1060 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 15:46:22 -07001061
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001062 if options.exit:
1063 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001064
joychen3cb228e2013-06-12 12:13:13 -07001065 dev_server = DevServerRoot(_xbuddy)
1066
Chris Sosa855b8932013-08-21 13:24:55 -07001067 if options.pidfile:
1068 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1069
joychen3cb228e2013-06-12 12:13:13 -07001070 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001071
1072
1073if __name__ == '__main__':
1074 main()