blob: 9c34f28c4a9f85040281dbc3b1dc3523354654b2 [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
57import cherrypy._cplogging
rtc@google.comded22402009-10-26 22:36:21 +000058
Chris Sosa0356d3b2010-09-16 15:46:22 -070059import autoupdate
Gilad Arnoldc65330c2012-09-20 15:17:48 -070060import common_util
Chris Sosa47a7d4e2012-03-28 11:26:55 -070061import downloader
Gilad Arnoldc65330c2012-09-20 15:17:48 -070062import log_util
joychen3cb228e2013-06-12 12:13:13 -070063import xbuddy
Gilad Arnoldc65330c2012-09-20 15:17:48 -070064
Gilad Arnoldc65330c2012-09-20 15:17:48 -070065# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080066def _Log(message, *args):
67 return log_util.LogWithTag('DEVSERVER', message, *args)
Chris Sosa0356d3b2010-09-16 15:46:22 -070068
Frank Farzan40160872011-12-12 18:39:18 -080069
Chris Sosa417e55d2011-01-25 16:40:48 -080070CACHED_ENTRIES = 12
Don Garrettf90edf02010-11-16 17:36:14 -080071
Simran Basi4baad082013-02-14 13:39:18 -080072TELEMETRY_FOLDER = 'telemetry_src'
73TELEMETRY_DEPS = ['dep-telemetry_dep.tar.bz2',
74 'dep-page_cycler_dep.tar.bz2',
Simran Basi0d078682013-03-22 16:40:04 -070075 'dep-chrome_test.tar.bz2',
76 'dep-perf_data_dep.tar.bz2']
Simran Basi4baad082013-02-14 13:39:18 -080077
Chris Sosa0356d3b2010-09-16 15:46:22 -070078# Sets up global to share between classes.
rtc@google.com21a5ca32009-11-04 18:23:23 +000079updater = None
rtc@google.comded22402009-10-26 22:36:21 +000080
J. Richard Barnette3d977b82013-04-23 11:05:19 -070081# Log rotation parameters. These settings correspond to once a week
J. Richard Barnette6dfa5342013-06-04 11:48:56 -070082# at midnight between Friday and Saturday, with about three months
83# of old logs kept for backup.
J. Richard Barnette3d977b82013-04-23 11:05:19 -070084#
85# For more, see the documentation for
86# logging.handlers.TimedRotatingFileHandler
J. Richard Barnette6dfa5342013-06-04 11:48:56 -070087_LOG_ROTATION_TIME = 'W4'
J. Richard Barnette3d977b82013-04-23 11:05:19 -070088_LOG_ROTATION_BACKUP = 13
89
Frank Farzan40160872011-12-12 18:39:18 -080090
Chris Sosa9164ca32012-03-28 11:04:50 -070091class DevServerError(Exception):
Chris Sosa47a7d4e2012-03-28 11:26:55 -070092 """Exception class used by this module."""
93 pass
94
95
Scott Zawalski4647ce62012-01-03 17:17:28 -050096def _LeadingWhiteSpaceCount(string):
97 """Count the amount of leading whitespace in a string.
98
99 Args:
100 string: The string to count leading whitespace in.
101 Returns:
102 number of white space chars before characters start.
103 """
104 matched = re.match('^\s+', string)
105 if matched:
106 return len(matched.group())
107
108 return 0
109
110
111def _PrintDocStringAsHTML(func):
112 """Make a functions docstring somewhat HTML style.
113
114 Args:
115 func: The function to return the docstring from.
116 Returns:
117 A string that is somewhat formated for a web browser.
118 """
119 # TODO(scottz): Make this parse Args/Returns in a prettier way.
120 # Arguments could be bolded and indented etc.
121 html_doc = []
122 for line in func.__doc__.splitlines():
123 leading_space = _LeadingWhiteSpaceCount(line)
124 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700125 line = '&nbsp;' * leading_space + line
Scott Zawalski4647ce62012-01-03 17:17:28 -0500126
127 html_doc.append('<BR>%s' % line)
128
129 return '\n'.join(html_doc)
130
131
Chris Sosa7c931362010-10-11 19:49:01 -0700132def _GetConfig(options):
133 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800134
135 # On a system with IPv6 not compiled into the kernel,
136 # AF_INET6 sockets will return a socket.error exception.
137 # On such systems, fall-back to IPv4.
138 socket_host = '::'
139 try:
140 socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
141 except socket.error:
142 socket_host = '0.0.0.0'
143
Chris Sosa7c931362010-10-11 19:49:01 -0700144 base_config = { 'global':
145 { 'server.log_request_headers': True,
146 'server.protocol_version': 'HTTP/1.1',
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800147 'server.socket_host': socket_host,
Chris Sosa7c931362010-10-11 19:49:01 -0700148 'server.socket_port': int(options.port),
Chris Sosa374c62d2010-10-14 09:13:54 -0700149 'response.timeout': 6000,
Chris Sosa6fe23942012-07-02 15:44:46 -0700150 'request.show_tracebacks': True,
Chris Sosa72333d12012-06-13 11:28:05 -0700151 'server.socket_timeout': 60,
joychenecc02aa2013-07-17 18:27:35 -0700152 'server.thread_pool': 2,
Chris Sosa7c931362010-10-11 19:49:01 -0700153 },
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700154 '/api':
155 {
156 # Gets rid of cherrypy parsing post file for args.
157 'request.process_request_body': False,
158 },
Chris Sosaa1ef0102010-10-21 16:22:35 -0700159 '/build':
160 {
161 'response.timeout': 100000,
162 },
Chris Sosa7c931362010-10-11 19:49:01 -0700163 '/update':
164 {
165 # Gets rid of cherrypy parsing post file for args.
166 'request.process_request_body': False,
Chris Sosaf65f4b92010-10-21 15:57:51 -0700167 'response.timeout': 10000,
Chris Sosa7c931362010-10-11 19:49:01 -0700168 },
169 # Sets up the static dir for file hosting.
170 '/static':
joychened64b222013-06-21 16:39:34 -0700171 { 'tools.staticdir.dir': options.static_dir,
Chris Sosa7c931362010-10-11 19:49:01 -0700172 'tools.staticdir.on': True,
Chris Sosaf65f4b92010-10-21 15:57:51 -0700173 'response.timeout': 10000,
Chris Sosa7c931362010-10-11 19:49:01 -0700174 },
175 }
Chris Sosa5f118ef2012-07-12 11:37:50 -0700176 if options.production:
Alex Miller93beca52013-07-30 19:25:09 -0700177 base_config['global'].update({'server.thread_pool': 150})
Scott Zawalski1c5e7cd2012-02-27 13:12:52 -0500178
Chris Sosa7c931362010-10-11 19:49:01 -0700179 return base_config
rtc@google.com64244662009-11-12 00:52:08 +0000180
Darin Petkove17164a2010-08-11 13:24:41 -0700181
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700182def _GetRecursiveMemberObject(root, member_list):
183 """Returns an object corresponding to a nested member list.
184
185 Args:
186 root: the root object to search
187 member_list: list of nested members to search
188 Returns:
189 An object corresponding to the member name list; None otherwise.
190 """
191 for member in member_list:
192 next_root = root.__class__.__dict__.get(member)
193 if not next_root:
194 return None
195 root = next_root
196 return root
197
198
199def _IsExposed(name):
200 """Returns True iff |name| has an `exposed' attribute and it is set."""
201 return hasattr(name, 'exposed') and name.exposed
202
203
Gilad Arnold748c8322012-10-12 09:51:35 -0700204def _GetExposedMethod(root, nested_member, ignored=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700205 """Returns a CherryPy-exposed method, if such exists.
206
207 Args:
208 root: the root object for searching
209 nested_member: a slash-joined path to the nested member
210 ignored: method paths to be ignored
211 Returns:
212 A function object corresponding to the path defined by |member_list| from
213 the |root| object, if the function is exposed and not ignored; None
214 otherwise.
215 """
Gilad Arnold748c8322012-10-12 09:51:35 -0700216 method = (not (ignored and nested_member in ignored) and
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700217 _GetRecursiveMemberObject(root, nested_member.split('/')))
218 if (method and type(method) == types.FunctionType and _IsExposed(method)):
219 return method
220
221
Gilad Arnold748c8322012-10-12 09:51:35 -0700222def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700223 """Finds exposed CherryPy methods.
224
225 Args:
226 root: the root object for searching
227 prefix: slash-joined chain of members leading to current object
228 unlisted: URLs to be excluded regardless of their exposed status
229 Returns:
230 List of exposed URLs that are not unlisted.
231 """
232 method_list = []
233 for member in sorted(root.__class__.__dict__.keys()):
234 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 09:51:35 -0700235 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700236 continue
237 member_obj = root.__class__.__dict__[member]
238 if _IsExposed(member_obj):
239 if type(member_obj) == types.FunctionType:
240 method_list.append(prefixed_member)
241 else:
242 method_list += _FindExposedMethods(
243 member_obj, prefixed_member, unlisted)
244 return method_list
245
246
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700247class ApiRoot(object):
248 """RESTful API for Dev Server information."""
249 exposed = True
250
251 @cherrypy.expose
252 def hostinfo(self, ip):
253 """Returns a JSON dictionary containing information about the given ip.
254
Gilad Arnold1b908392012-10-05 11:36:27 -0700255 Args:
256 ip: address of host whose info is requested
257 Returns:
258 A JSON dictionary containing all or some of the following fields:
259 last_event_type (int): last update event type received
260 last_event_status (int): last update event status received
261 last_known_version (string): last known version reported in update ping
262 forced_update_label (string): update label to force next update ping to
263 use, set by setnextupdate
264 See the OmahaEvent class in update_engine/omaha_request_action.h for
265 event type and status code definitions. If the ip does not exist an empty
266 string is returned.
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700267
Gilad Arnold1b908392012-10-05 11:36:27 -0700268 Example URL:
269 http://myhost/api/hostinfo?ip=192.168.1.5
270 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700271 return updater.HandleHostInfoPing(ip)
272
273 @cherrypy.expose
Gilad Arnold286a0062012-01-12 13:47:02 -0800274 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 11:36:27 -0700275 """Returns a JSON object containing a log of host event.
276
277 Args:
278 ip: address of host whose event log is requested, or `all'
279 Returns:
280 A JSON encoded list (log) of dictionaries (events), each of which
281 containing a `timestamp' and other event fields, as described under
282 /api/hostinfo.
283
284 Example URL:
285 http://myhost/api/hostlog?ip=192.168.1.5
286 """
Gilad Arnold286a0062012-01-12 13:47:02 -0800287 return updater.HandleHostLogPing(ip)
288
289 @cherrypy.expose
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700290 def setnextupdate(self, ip):
291 """Allows the response to the next update ping from a host to be set.
292
293 Takes the IP of the host and an update label as normally provided to the
Gilad Arnold1b908392012-10-05 11:36:27 -0700294 /update command.
295 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700296 body_length = int(cherrypy.request.headers['Content-Length'])
297 label = cherrypy.request.rfile.read(body_length)
298
299 if label:
300 label = label.strip()
301 if label:
302 return updater.HandleSetUpdatePing(ip, label)
303 raise cherrypy.HTTPError(400, 'No label provided.')
304
305
Gilad Arnold55a2a372012-10-02 09:46:32 -0700306 @cherrypy.expose
307 def fileinfo(self, *path_args):
308 """Returns information about a given staged file.
309
310 Args:
311 path_args: path to the file inside the server's static staging directory
312 Returns:
313 A JSON encoded dictionary with information about the said file, which may
314 contain the following keys/values:
Gilad Arnold1b908392012-10-05 11:36:27 -0700315 size (int): the file size in bytes
316 sha1 (string): a base64 encoded SHA1 hash
317 sha256 (string): a base64 encoded SHA256 hash
318
319 Example URL:
320 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 09:46:32 -0700321 """
322 file_path = os.path.join(updater.static_dir, *path_args)
323 if not os.path.exists(file_path):
324 raise DevServerError('file not found: %s' % file_path)
325 try:
326 file_size = os.path.getsize(file_path)
327 file_sha1 = common_util.GetFileSha1(file_path)
328 file_sha256 = common_util.GetFileSha256(file_path)
329 except os.error, e:
330 raise DevServerError('failed to get info for file %s: %s' %
Gilad Arnolde74b3812013-04-22 11:27:38 -0700331 (file_path, e))
332
333 is_delta = autoupdate.Autoupdate.IsDeltaFormatFile(file_path)
334
335 return json.dumps({
336 autoupdate.Autoupdate.SIZE_ATTR: file_size,
337 autoupdate.Autoupdate.SHA1_ATTR: file_sha1,
338 autoupdate.Autoupdate.SHA256_ATTR: file_sha256,
339 autoupdate.Autoupdate.ISDELTA_ATTR: is_delta
340 })
Gilad Arnold55a2a372012-10-02 09:46:32 -0700341
Chris Sosa76e44b92013-01-31 12:11:38 -0800342
David Rochberg7c79a812011-01-19 14:24:45 -0500343class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700344 """The Root Class for the Dev Server.
345
346 CherryPy works as follows:
347 For each method in this class, cherrpy interprets root/path
348 as a call to an instance of DevServerRoot->method_name. For example,
349 a call to http://myhost/build will call build. CherryPy automatically
350 parses http args and places them as keyword arguments in each method.
351 For paths http://myhost/update/dir1/dir2, you can use *args so that
352 cherrypy uses the update method and puts the extra paths in args.
353 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700354 # Method names that should not be listed on the index page.
355 _UNLISTED_METHODS = ['index', 'doc']
356
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700357 api = ApiRoot()
Chris Sosa7c931362010-10-11 19:49:01 -0700358
Dan Shi59ae7092013-06-04 14:37:27 -0700359 # Number of threads that devserver is staging images.
360 _staging_thread_count = 0
361 # Lock used to lock increasing/decreasing count.
362 _staging_thread_count_lock = threading.Lock()
363
joychen3cb228e2013-06-12 12:13:13 -0700364 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700365 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800366 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 12:13:13 -0700367 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 14:24:45 -0500368
Chris Sosa6b0c6172013-08-05 17:01:33 -0700369 @staticmethod
370 def _get_artifacts(kwargs):
371 """Returns a tuple of named and file artifacts given the stage rpc kwargs.
372
373 Raises: DevserverError if no artifacts would be returned.
374 """
375 artifacts = kwargs.get('artifacts')
376 files = kwargs.get('files')
377 if not artifacts and not files:
378 raise DevServerError('No artifacts specified.')
379
380 return (artifacts.split(',') if artifacts else [],
381 files.split(',') if files else [])
382
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700383 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500384 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700385 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700386 import builder
387 if self._builder is None:
388 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500389 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700390
Chris Sosacde6bf42012-05-31 18:36:39 -0700391 @staticmethod
392 def _canonicalize_archive_url(archive_url):
393 """Canonicalizes archive_url strings.
394
395 Raises:
396 DevserverError: if archive_url is not set.
397 """
398 if archive_url:
Chris Sosa76e44b92013-01-31 12:11:38 -0800399 if not archive_url.startswith('gs://'):
400 raise DevServerError("Archive URL isn't from Google Storage.")
401
Chris Sosacde6bf42012-05-31 18:36:39 -0700402 return archive_url.rstrip('/')
403 else:
404 raise DevServerError("Must specify an archive_url in the request")
405
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700406 @cherrypy.expose
Dan Shif8eb0d12013-08-01 17:52:06 -0700407 def is_staged(self, **kwargs):
408 """Check if artifacts have been downloaded.
409
Chris Sosa6b0c6172013-08-05 17:01:33 -0700410 async: True to return without waiting for download to complete.
411 artifacts: Comma separated list of named artifacts to download.
412 These are defined in artifact_info and have their implementation
413 in build_artifact.py.
414 files: Comma separated list of file artifacts to stage. These
415 will be available as is in the corresponding static directory with no
416 custom post-processing.
417
418 returns: True of all artifacts are staged.
Dan Shif8eb0d12013-08-01 17:52:06 -0700419
420 Example:
421 To check if autotest and test_suites are staged:
422 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
423 artifacts=autotest,test_suites
424 """
425 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
Chris Sosa6b0c6172013-08-05 17:01:33 -0700426 artifacts, files = self._get_artifacts(kwargs)
Dan Shif8eb0d12013-08-01 17:52:06 -0700427 return str(downloader.Downloader(updater.static_dir, archive_url).IsStaged(
Chris Sosa6b0c6172013-08-05 17:01:33 -0700428 artifacts, files))
Dan Shi59ae7092013-06-04 14:37:27 -0700429
Chris Sosa76e44b92013-01-31 12:11:38 -0800430 @cherrypy.expose
431 def stage(self, **kwargs):
432 """Downloads and caches the artifacts from Google Storage URL.
433
434 Downloads and caches the artifacts Google Storage URL. Returns once these
435 have been downloaded on the devserver. A call to this will attempt to cache
436 non-specified artifacts in the background for the given from the given URL
437 following the principle of spatial locality. Spatial locality of different
438 artifacts is explicitly defined in the build_artifact module.
439
440 These artifacts will then be available from the static/ sub-directory of
441 the devserver.
442
443 Args:
444 archive_url: Google Storage URL for the build.
Dan Shif8eb0d12013-08-01 17:52:06 -0700445 async: True to return without waiting for download to complete.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700446 artifacts: Comma separated list of named artifacts to download.
447 These are defined in artifact_info and have their implementation
448 in build_artifact.py.
449 files: Comma separated list of files to stage. These
450 will be available as is in the corresponding static directory with no
451 custom post-processing.
Chris Sosa76e44b92013-01-31 12:11:38 -0800452
453 Example:
454 To download the autotest and test suites tarballs:
455 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
456 artifacts=autotest,test_suites
457 To download the full update payload:
458 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
459 artifacts=full_payload
Chris Sosa6b0c6172013-08-05 17:01:33 -0700460 To download just a file called blah.bin:
461 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
462 files=blah.bin
Chris Sosa76e44b92013-01-31 12:11:38 -0800463
464 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 16:39:34 -0700465 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 12:11:38 -0800466
467 Note for this example, relative path is the archive_url stripped of its
468 basename i.e. path/ in the examples above. Specific example:
469
470 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
471
472 Will get staged to:
473
joychened64b222013-06-21 16:39:34 -0700474 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Chris Sosa76e44b92013-01-31 12:11:38 -0800475 """
Chris Sosacde6bf42012-05-31 18:36:39 -0700476 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
Dan Shif8eb0d12013-08-01 17:52:06 -0700477 async = kwargs.get('async', False)
Chris Sosa6b0c6172013-08-05 17:01:33 -0700478 artifacts, files = self._get_artifacts(kwargs)
Dan Shi59ae7092013-06-04 14:37:27 -0700479 with DevServerRoot._staging_thread_count_lock:
480 DevServerRoot._staging_thread_count += 1
481 try:
Chris Sosa6b0c6172013-08-05 17:01:33 -0700482 downloader.Downloader(updater.static_dir, archive_url).Download(
483 artifacts, files, async=async)
Dan Shi59ae7092013-06-04 14:37:27 -0700484 finally:
485 with DevServerRoot._staging_thread_count_lock:
486 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800487 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700488
489 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -0800490 def setup_telemetry(self, **kwargs):
491 """Extracts and sets up telemetry
492
493 This method goes through the telemetry deps packages, and stages them on
494 the devserver to be used by the drones and the telemetry tests.
495
496 Args:
497 archive_url: Google Storage URL for the build.
498
499 Returns:
500 Path to the source folder for the telemetry codebase once it is staged.
501 """
502 archive_url = kwargs.get('archive_url')
503 self.stage(archive_url=archive_url, artifacts='autotest')
504
505 build = '/'.join(downloader.Downloader.ParseUrl(archive_url))
506 build_path = os.path.join(updater.static_dir, build)
507 deps_path = os.path.join(build_path, 'autotest/packages')
508 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
509 src_folder = os.path.join(telemetry_path, 'src')
510
511 with self._telemetry_lock_dict.lock(telemetry_path):
512 if os.path.exists(src_folder):
513 # Telemetry is already fully stage return
514 return src_folder
515
516 common_util.MkDirP(telemetry_path)
517
518 # Copy over the required deps tar balls to the telemetry directory.
519 for dep in TELEMETRY_DEPS:
520 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -0700521 if not os.path.exists(dep_path):
522 # This dep does not exist (could be new), do not extract it.
523 continue
Simran Basi4baad082013-02-14 13:39:18 -0800524 try:
525 common_util.ExtractTarball(dep_path, telemetry_path)
526 except common_util.CommonUtilError as e:
527 shutil.rmtree(telemetry_path)
528 raise DevServerError(str(e))
529
530 # By default all the tarballs extract to test_src but some parts of
531 # the telemetry code specifically hardcoded to exist inside of 'src'.
532 test_src = os.path.join(telemetry_path, 'test_src')
533 try:
534 shutil.move(test_src, src_folder)
535 except shutil.Error:
536 # This can occur if src_folder already exists. Remove and retry move.
537 shutil.rmtree(src_folder)
538 raise DevServerError('Failure in telemetry setup for build %s. Appears'
539 ' that the test_src to src move failed.' % build)
540
541 return src_folder
542
543 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800544 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -0700545 """Symbolicates a minidump using pre-downloaded symbols, returns it.
546
547 Callers will need to POST to this URL with a body of MIME-type
548 "multipart/form-data".
549 The body should include a single argument, 'minidump', containing the
550 binary-formatted minidump to symbolicate.
551
Chris Masone816e38c2012-05-02 12:22:36 -0700552 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -0800553 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -0700554 minidump: The binary minidump file to symbolicate.
555 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800556 # Ensure the symbols have been staged.
557 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
558 if self.stage(archive_url=archive_url, artifacts='symbols') != 'Success':
559 raise DevServerError('Failed to stage symbols for %s' % archive_url)
560
Chris Masone816e38c2012-05-02 12:22:36 -0700561 to_return = ''
562 with tempfile.NamedTemporaryFile() as local:
563 while True:
564 data = minidump.file.read(8192)
565 if not data:
566 break
567 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -0800568
Chris Masone816e38c2012-05-02 12:22:36 -0700569 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -0800570
571 symbols_directory = os.path.join(downloader.Downloader.GetBuildDir(
572 updater.static_dir, archive_url), 'debug', 'breakpad')
573
574 stackwalk = subprocess.Popen(
575 ['minidump_stackwalk', local.name, symbols_directory],
576 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
577
Chris Masone816e38c2012-05-02 12:22:36 -0700578 to_return, error_text = stackwalk.communicate()
579 if stackwalk.returncode != 0:
580 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
581 error_text, stackwalk.returncode))
582
583 return to_return
584
585 @cherrypy.expose
Scott Zawalski16954532012-03-20 15:31:36 -0400586 def latestbuild(self, **params):
587 """Return a string representing the latest build for a given target.
588
589 Args:
590 target: The build target, typically a combination of the board and the
591 type of build e.g. x86-mario-release.
592 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
593 provided the latest RXX build will be returned.
594 Returns:
595 A string representation of the latest build if one exists, i.e.
596 R19-1993.0.0-a1-b1480.
597 An empty string if no latest could be found.
598 """
599 if not params:
600 return _PrintDocStringAsHTML(self.latestbuild)
601
602 if 'target' not in params:
603 raise cherrypy.HTTPError('500 Internal Server Error',
604 'Error: target= is required!')
605 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700606 return common_util.GetLatestBuildVersion(
Scott Zawalski16954532012-03-20 15:31:36 -0400607 updater.static_dir, params['target'],
608 milestone=params.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700609 except common_util.CommonUtilError as errmsg:
Scott Zawalski16954532012-03-20 15:31:36 -0400610 raise cherrypy.HTTPError('500 Internal Server Error', str(errmsg))
611
612 @cherrypy.expose
Scott Zawalski84a39c92012-01-13 15:12:42 -0500613 def controlfiles(self, **params):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500614 """Return a control file or a list of all known control files.
615
616 Example URL:
617 To List all control files:
beepsbd337242013-07-09 22:44:06 -0700618 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
619 To List all control files for, say, the bvt suite:
620 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -0500621 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500622 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 -0500623
624 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500625 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500626 control_path: If you want the contents of a control file set this
627 to the path. E.g. client/site_tests/sleeptest/control
628 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -0700629 suite_name: If control_path is not specified but a suite_name is
630 specified, list the control files belonging to that suite instead of
631 all control files. The empty string for suite_name will list all control
632 files for the build.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500633 Returns:
634 Contents of a control file if control_path is provided.
635 A list of control files if no control_path is provided.
636 """
Scott Zawalski4647ce62012-01-03 17:17:28 -0500637 if not params:
638 return _PrintDocStringAsHTML(self.controlfiles)
639
Scott Zawalski84a39c92012-01-13 15:12:42 -0500640 if 'build' not in params:
Scott Zawalski4647ce62012-01-03 17:17:28 -0500641 raise cherrypy.HTTPError('500 Internal Server Error',
Scott Zawalski84a39c92012-01-13 15:12:42 -0500642 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -0500643
644 if 'control_path' not in params:
beepsbd337242013-07-09 22:44:06 -0700645 if 'suite_name' in params and params['suite_name']:
646 return common_util.GetControlFileListForSuite(
647 updater.static_dir, params['build'], params['suite_name'])
648 else:
649 return common_util.GetControlFileList(
650 updater.static_dir, params['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -0500651 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700652 return common_util.GetControlFile(
653 updater.static_dir, params['build'], params['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -0800654
655 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -0700656 def xbuddy(self, *args, **kwargs):
657 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -0700658
659 Args:
joycheneaf4cfc2013-07-02 08:38:57 -0700660 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -0700661 components of the path. The path can be understood as
662 "{local|remote}/build_id/artifact" where build_id is composed of
663 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -0700664
joychen121fc9b2013-08-02 14:30:30 -0700665 The first path element is optional, and can be "remote" or "local"
666 If local (the default), devserver will not attempt to access Google
667 Storage, and will only search the static directory for the files.
668 If remote, devserver will try to obtain the artifact off GS if it's
669 not found locally.
670 The board is the familiar board name, optionally suffixed.
671 The version can be the google storage version number, and may also be
672 any of a number of xBuddy defined version aliases that will be
673 translated into the latest built image that fits the description.
674 Defaults to latest.
675 The artifact is one of a number of image or artifact aliases used by
676 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -0700677
678 Kwargs:
joychen3cb228e2013-06-12 12:13:13 -0700679 return_dir: {true|false}
680 if set to true, returns the url to the update.gz
681 instead.
682
683 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -0700684 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -0700685 or
joycheneaf4cfc2013-07-02 08:38:57 -0700686 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -0700687
688 Returns:
689 A redirect to the image or update file on the devserver.
690 e.g. http://host:port/static/archive/x86-generic-release/
691 R26-4000.0.0/chromium-test-image.bin
692 or if return_dir is True, return path to the folder where
joychen121fc9b2013-08-02 14:30:30 -0700693 the artifact is.
joychen3cb228e2013-06-12 12:13:13 -0700694 http://host:port/static/x86-generic-release/R26-4000.0.0/
695 """
696 boolean_string = kwargs.get('return_dir')
697 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -0700698
699 build_id, file_name = self._xbuddy.Get(args)
joychen3cb228e2013-06-12 12:13:13 -0700700 if return_dir:
joychen121fc9b2013-08-02 14:30:30 -0700701 directory = os.path.join(cherrypy.request.base, 'static', build_id)
joycheneaf4cfc2013-07-02 08:38:57 -0700702 _Log("Directory requested, returning: %s", directory)
703 return directory
joychen3cb228e2013-06-12 12:13:13 -0700704 else:
joychen121fc9b2013-08-02 14:30:30 -0700705 build_id = '/' + os.path.join('static', build_id, file_name)
706 _Log("Payload requested, returning: %s", build_id)
707 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -0700708
709 @cherrypy.expose
710 def xbuddy_list(self):
711 """Lists the currently available images & time since last access.
712
713 @return: A string representation of a list of tuples
714 [(build_id, time since last access),...]
715 """
716 return self._xbuddy.List()
717
718 @cherrypy.expose
719 def xbuddy_capacity(self):
720 """Returns the number of images cached by xBuddy.
721
722 @return: Capacity of this devserver.
723 """
724 return self._xbuddy.Capacity()
725
726 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700727 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700728 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700729 return ('Welcome to the Dev Server!<br>\n'
730 '<br>\n'
731 'Here are the available methods, click for documentation:<br>\n'
732 '<br>\n'
733 '%s' %
734 '<br>\n'.join(
735 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700736 for name in _FindExposedMethods(
737 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700738
739 @cherrypy.expose
740 def doc(self, *args):
741 """Shows the documentation for available methods / URLs.
742
743 Example:
744 http://myhost/doc/update
745 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700746 name = '/'.join(args)
747 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700748 if not method:
749 raise DevServerError("No exposed method named `%s'" % name)
750 if not method.__doc__:
751 raise DevServerError("No documentation for exposed method `%s'" % name)
752 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -0700753
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700754 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700755 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700756 """Handles an update check from a Chrome OS client.
757
758 The HTTP request should contain the standard Omaha-style XML blob. The URL
759 line may contain an additional intermediate path to the update payload.
760
joychen121fc9b2013-08-02 14:30:30 -0700761 This request can be handled in one of 4 ways, depending on the devsever
762 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -0700763
joychen121fc9b2013-08-02 14:30:30 -0700764 1. No intermediate path
765 If no intermediate path is given, the default behavior is to generate an
766 update payload from the latest test image locally built for the board
767 specified in the xml. Devserver serves the generated payload.
768
769 2. Path explicitly invokes XBuddy
770 If there is a path given, it can explicitly invoke xbuddy by prefixing it
771 with 'xbuddy'. This path is then used to acquire an image binary for the
772 devserver to generate an update payload from. Devserver then serves this
773 payload.
774
775 3. Path is left for the devserver to interpret.
776 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
777 to generate a payload from the test image in that directory and serve it.
778
779 4. The devserver is in a 'forced' mode. TO BE DEPRECATED
780 This comes from the usage of --forced_payload or --image when starting the
781 devserver. No matter what path (or no path) gets passed in, devserver will
782 serve the update payload (--forced_payload) or generate an update payload
783 from the image (--image).
784
785 Examples:
786 1. No intermediate path
787 update_engine_client --omaha_url=http://myhost/update
788 This generates an update payload from the latest test image locally built
789 for the board specified in the xml.
790
791 2. Explicitly invoke xbuddy
792 update_engine_client --omaha_url=
793 http://myhost/update/xbuddy/remote/board/version/dev
794 This would go to GS to download the dev image for the board, from which
795 the devserver would generate a payload to serve.
796
797 3. Give a path for devserver to interpret
798 update_engine_client --omaha_url=http://myhost/update/some/random/path
799 This would attempt, in order to:
800 a) Generate an update from a test image binary if found in
801 static_dir/some/random/path.
802 b) Serve an update payload found in static_dir/some/random/path.
803 c) Hope that some/random/path takes the form "board/version" and
804 and attempt to download an update payload for that board/version
805 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700806 """
joychen121fc9b2013-08-02 14:30:30 -0700807 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -0800808 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -0700809 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-11 19:49:01 -0700810
joychen121fc9b2013-08-02 14:30:30 -0700811 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700812
Dan Shif5ce2de2013-04-25 16:06:32 -0700813 @cherrypy.expose
814 def check_health(self):
815 """Collect the health status of devserver to see if it's ready for staging.
816
817 @return: A JSON dictionary containing all or some of the following fields:
Dan Shi59ae7092013-06-04 14:37:27 -0700818 free_disk (int): free disk space in GB
819 staging_thread_count (int): number of devserver threads currently
820 staging an image
Dan Shif5ce2de2013-04-25 16:06:32 -0700821 """
822 # Get free disk space.
823 stat = os.statvfs(updater.static_dir)
824 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
825
826 return json.dumps({
827 'free_disk': free_disk,
Dan Shi59ae7092013-06-04 14:37:27 -0700828 'staging_thread_count': DevServerRoot._staging_thread_count,
Dan Shif5ce2de2013-04-25 16:06:32 -0700829 })
830
831
Chris Sosadbc20082012-12-10 13:39:11 -0800832def _CleanCache(cache_dir, wipe):
833 """Wipes any excess cached items in the cache_dir.
834
835 Args:
836 cache_dir: the directory we are wiping from.
837 wipe: If True, wipe all the contents -- not just the excess.
838 """
839 if wipe:
840 # Clear the cache and exit on error.
841 cmd = 'rm -rf %s/*' % cache_dir
842 if os.system(cmd) != 0:
843 _Log('Failed to clear the cache with %s' % cmd)
844 sys.exit(1)
845 else:
846 # Clear all but the last N cached updates
847 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
848 (cache_dir, CACHED_ENTRIES))
849 if os.system(cmd) != 0:
850 _Log('Failed to clean up old delta cache files with %s' % cmd)
851 sys.exit(1)
852
853
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700854def _AddTestingOptions(parser):
855 group = optparse.OptionGroup(
856 parser, 'Advanced Testing Options', 'These are used by test scripts and '
857 'developers writing integration tests utilizing the devserver. They are '
858 'not intended to be really used outside the scope of someone '
859 'knowledgable about the test.')
860 group.add_option('--exit',
861 action='store_true',
862 help='do not start the server (yet pregenerate/clear cache)')
863 group.add_option('--host_log',
864 action='store_true', default=False,
865 help='record history of host update events (/api/hostlog)')
866 group.add_option('--max_updates',
867 metavar='NUM', default= -1, type='int',
868 help='maximum number of update checks handled positively '
869 '(default: unlimited)')
870 group.add_option('--private_key',
871 metavar='PATH', default=None,
872 help='path to the private key in pem format. If this is set '
873 'the devserver will generate update payloads that are '
874 'signed with this key.')
875 group.add_option('--proxy_port',
876 metavar='PORT', default=None, type='int',
877 help='port to have the client connect to -- basically the '
878 'devserver lies to the update to tell it to get the payload '
879 'from a different port that will proxy the request back to '
880 'the devserver. The proxy must be managed outside the '
881 'devserver.')
882 group.add_option('--remote_payload',
883 action='store_true', default=False,
884 help='Payload is being served from a remote machine')
885 group.add_option('-u', '--urlbase',
886 metavar='URL',
887 help='base URL for update images, other than the '
888 'devserver. Use in conjunction with remote_payload.')
889 parser.add_option_group(group)
890
891
892def _AddUpdateOptions(parser):
893 group = optparse.OptionGroup(
894 parser, 'Autoupdate Options', 'These options can be used to change '
895 'how the devserver either generates or serve update payloads. Please '
896 'note that all of these option affect how a payload is generated and so '
897 'do not work in archive-only mode.')
898 group.add_option('--board',
899 help='By default the devserver will create an update '
900 'payload from the latest image built for the board '
901 'a device that is requesting an update has. When we '
902 'pre-generate an update (see below) and we do not specify '
903 'another update_type option like image or payload, the '
904 'devserver needs to know the board to generate the latest '
905 'image for. This is that board.')
906 group.add_option('--critical_update',
907 action='store_true', default=False,
908 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700909 group.add_option('--image',
910 metavar='FILE',
911 help='Generate and serve an update using this image to any '
912 'device that requests an update.')
913 group.add_option('--no_patch_kernel',
914 dest='patch_kernel', action='store_false', default=True,
915 help='When generating an update payload, do not patch the '
916 'kernel with kernel verification blob from the stateful '
917 'partition.')
918 group.add_option('--payload',
919 metavar='PATH',
920 help='use the update payload from specified directory '
921 '(update.gz).')
922 group.add_option('-p', '--pregenerate_update',
923 action='store_true', default=False,
924 help='pre-generate the update payload before accepting '
925 'update requests. Useful to help debug payload generation '
926 'issues quickly. Also if an update payload will take a '
927 'long time to generate, a client may timeout if you do not'
928 'pregenerate the update.')
929 group.add_option('--src_image',
930 metavar='PATH', default='',
931 help='If specified, delta updates will be generated using '
932 'this image as the source image. Delta updates are when '
933 'you are updating from a "source image" to a another '
934 'image.')
935 parser.add_option_group(group)
936
937
938def _AddProductionOptions(parser):
939 group = optparse.OptionGroup(
940 parser, 'Advanced Server Options', 'These options can be used to changed '
941 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700942 group.add_option('--clear_cache',
943 action='store_true', default=False,
944 help='At startup, removes all cached entries from the'
945 'devserver\'s cache.')
946 group.add_option('--logfile',
947 metavar='PATH',
948 help='log output to this file instead of stdout')
949 group.add_option('--production',
950 action='store_true', default=False,
951 help='have the devserver use production values when '
952 'starting up. This includes using more threads and '
953 'performing less logging.')
954 parser.add_option_group(group)
955
956
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700957def _MakeLogHandler(logfile):
958 """Create a LogHandler instance used to log all messages."""
959 hdlr_cls = handlers.TimedRotatingFileHandler
960 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
961 backupCount=_LOG_ROTATION_BACKUP)
962 # The cherrypy documentation says to use the _cplogging module for
963 # this, even though it's named as a private module.
964 # pylint: disable=W0212
965 hdlr.setFormatter(cherrypy._cplogging.logfmt)
966 return hdlr
967
968
Chris Sosacde6bf42012-05-31 18:36:39 -0700969def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700970 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -0800971 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -0700972
973 # get directory that the devserver is run from
974 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -0700975 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -0700976 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700977 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -0700978 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -0700979 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700980 parser.add_option('--port',
981 default=8080, type='int',
982 help='port for the dev server to use (default: 8080)')
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700983 parser.add_option('-t', '--test_image',
984 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -0700985 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -0700986 parser.add_option('-x', '--xbuddy_manage_builds',
987 action='store_true',
988 default=False,
989 help='If set, allow xbuddy to manage images in'
990 'build/images.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700991 _AddProductionOptions(parser)
992 _AddUpdateOptions(parser)
993 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -0700994 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +0000995
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700996 # Handle options that must be set globally in cherrypy. Do this
997 # work up front, because calls to _Log() below depend on this
998 # initialization.
999 if options.production:
1000 cherrypy.config.update({'environment': 'production'})
1001 if not options.logfile:
1002 cherrypy.config.update({'log.screen': True})
1003 else:
1004 cherrypy.config.update({'log.error_file': '',
1005 'log.access_file': ''})
1006 hdlr = _MakeLogHandler(options.logfile)
1007 # Pylint can't seem to process these two calls properly
1008 # pylint: disable=E1101
1009 cherrypy.log.access_log.addHandler(hdlr)
1010 cherrypy.log.error_log.addHandler(hdlr)
1011 # pylint: enable=E1101
1012
Chris Sosa7c931362010-10-11 19:49:01 -07001013 root_dir = os.path.realpath('%s/../..' % devserver_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001014
joychened64b222013-06-21 16:39:34 -07001015 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001016 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001017
joychened64b222013-06-21 16:39:34 -07001018 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001019 # If our devserver is only supposed to serve payloads, we shouldn't be
1020 # mucking with the cache at all. If the devserver hadn't previously
1021 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001022 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001023 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001024 else:
1025 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001026
Chris Sosadbc20082012-12-10 13:39:11 -08001027 _Log('Using cache directory %s' % cache_dir)
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001028 _Log('Source root is %s' % root_dir)
joychened64b222013-06-21 16:39:34 -07001029 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001030
joychen121fc9b2013-08-02 14:30:30 -07001031 _xbuddy = xbuddy.XBuddy(options.xbuddy_manage_builds,
1032 options.board,
1033 root_dir=root_dir,
1034 static_dir=options.static_dir)
1035
Chris Sosa6a3697f2013-01-29 16:44:43 -08001036 # We allow global use here to share with cherrypy classes.
1037 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001038 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001039 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001040 _xbuddy,
Andrew de los Reyes52620802010-04-12 13:40:07 -07001041 root_dir=root_dir,
joychened64b222013-06-21 16:39:34 -07001042 static_dir=options.static_dir,
Andrew de los Reyes52620802010-04-12 13:40:07 -07001043 urlbase=options.urlbase,
Chris Sosa5d342a22010-09-28 16:54:41 -07001044 forced_image=options.image,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001045 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001046 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-28 23:42:37 -07001047 src_image=options.src_image,
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001048 patch_kernel=options.patch_kernel,
Chris Sosa08d55a22011-01-19 16:08:02 -08001049 board=options.board,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001050 copy_to_static_root=not options.exit,
1051 private_key=options.private_key,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001052 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001053 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001054 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001055 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001056 )
Chris Sosa7c931362010-10-11 19:49:01 -07001057
Chris Sosa6a3697f2013-01-29 16:44:43 -08001058 if options.pregenerate_update:
1059 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 15:46:22 -07001060
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001061 if options.exit:
1062 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001063
joychen3cb228e2013-06-12 12:13:13 -07001064 dev_server = DevServerRoot(_xbuddy)
1065
1066 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001067
1068
1069if __name__ == '__main__':
1070 main()