blob: be8352de097ba52aa7cf18a65d0886423b332947 [file] [log] [blame]
Chris Sosa7c931362010-10-11 19:49:01 -07001#!/usr/bin/python
2
Chris Sosa781ba6d2012-04-11 12:44:43 -07003# Copyright (c) 2009-2012 The Chromium OS Authors. All rights reserved.
rtc@google.comded22402009-10-26 22:36:21 +00004# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
Chris Sosa3ae4dc12013-03-29 11:47:00 -07007"""Chromium OS development server that can be used for all forms of update.
8
9This devserver can be used to perform system-wide autoupdate and update
10of specific portage packages on devices running Chromium OS derived operating
11systems. It mainly operates in two modes:
12
131) archive mode: In this mode, the devserver is configured to stage and
14serve artifacts from Google Storage using the credentials provided to it before
15it is run. The easiest way to understand this is that the devserver is
16functioning as a local cache for artifacts produced and uploaded by build
17servers. Users of this form of devserver can either download the artifacts
18from the devservers static directory OR use the update RPC to perform a
19system-wide autoupdate. Archive mode is always active.
20
212) artifact-generation mode: in this mode, the devserver will attempt to
22generate update payloads and build artifacts when requested. This mode only
23works in the Chromium OS chroot as it uses build tools only present in the
24chroot (emerge, cros_generate_update_payload, etc.). By default, when a device
25requests an update from this form of devserver, the devserver will attempt to
26discover if a more recent build of the board has been built by the developer
27and generate a payload that the requested system can autoupdate to. In addition,
28it accepts gmerge requests from devices that will stage the newest version of
joychen84d13772013-08-06 09:17:23 -070029a particular package from a developer's chroot onto a requesting device.
Chris Sosa3ae4dc12013-03-29 11:47:00 -070030
31For example:
32gmerge gmerge -d <devserver_url>
33
34devserver will see if a newer package of gmerge is available. If gmerge is
35cros_work'd on, it will re-build gmerge. After this, gmerge will install that
36version of gmerge that the devserver just created/found.
37
38For autoupdates, there are many more advanced options that can help specify
39how to update and which payload to give to a requester.
40"""
41
Chris Sosa7c931362010-10-11 19:49:01 -070042
Gilad Arnold55a2a372012-10-02 09:46:32 -070043import json
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070044import optparse
rtc@google.comded22402009-10-26 22:36:21 +000045import os
Scott Zawalski4647ce62012-01-03 17:17:28 -050046import re
Simran Basi4baad082013-02-14 13:39:18 -080047import shutil
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -080048import socket
Chris Masone816e38c2012-05-02 12:22:36 -070049import subprocess
J. Richard Barnette3d977b82013-04-23 11:05:19 -070050import sys
Chris Masone816e38c2012-05-02 12:22:36 -070051import tempfile
Dan Shi59ae7092013-06-04 14:37:27 -070052import threading
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -070053import types
J. Richard Barnette3d977b82013-04-23 11:05:19 -070054from logging import handlers
55
56import cherrypy
Chris Sosa855b8932013-08-21 13:24:55 -070057from cherrypy import _cplogging as cplogging
58from cherrypy.process import plugins
rtc@google.comded22402009-10-26 22:36:21 +000059
Chris Sosa0356d3b2010-09-16 15:46:22 -070060import autoupdate
Chris Sosa75490802013-09-30 17:21:45 -070061import build_artifact
Gilad Arnoldc65330c2012-09-20 15:17:48 -070062import common_util
Chris Sosa47a7d4e2012-03-28 11:26:55 -070063import downloader
Gilad Arnoldc65330c2012-09-20 15:17:48 -070064import log_util
joychen3cb228e2013-06-12 12:13:13 -070065import xbuddy
Gilad Arnoldc65330c2012-09-20 15:17:48 -070066
Gilad Arnoldc65330c2012-09-20 15:17:48 -070067# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080068def _Log(message, *args):
69 return log_util.LogWithTag('DEVSERVER', message, *args)
Chris Sosa0356d3b2010-09-16 15:46:22 -070070
Frank Farzan40160872011-12-12 18:39:18 -080071
Chris Sosa417e55d2011-01-25 16:40:48 -080072CACHED_ENTRIES = 12
Don Garrettf90edf02010-11-16 17:36:14 -080073
Simran Basi4baad082013-02-14 13:39:18 -080074TELEMETRY_FOLDER = 'telemetry_src'
75TELEMETRY_DEPS = ['dep-telemetry_dep.tar.bz2',
76 'dep-page_cycler_dep.tar.bz2',
Simran Basi0d078682013-03-22 16:40:04 -070077 'dep-chrome_test.tar.bz2',
78 'dep-perf_data_dep.tar.bz2']
Simran Basi4baad082013-02-14 13:39:18 -080079
Chris Sosa0356d3b2010-09-16 15:46:22 -070080# Sets up global to share between classes.
rtc@google.com21a5ca32009-11-04 18:23:23 +000081updater = None
rtc@google.comded22402009-10-26 22:36:21 +000082
J. Richard Barnette3d977b82013-04-23 11:05:19 -070083# Log rotation parameters. These settings correspond to once a week
J. Richard Barnette6dfa5342013-06-04 11:48:56 -070084# at midnight between Friday and Saturday, with about three months
85# of old logs kept for backup.
J. Richard Barnette3d977b82013-04-23 11:05:19 -070086#
87# For more, see the documentation for
88# logging.handlers.TimedRotatingFileHandler
J. Richard Barnette6dfa5342013-06-04 11:48:56 -070089_LOG_ROTATION_TIME = 'W4'
J. Richard Barnette3d977b82013-04-23 11:05:19 -070090_LOG_ROTATION_BACKUP = 13
91
Frank Farzan40160872011-12-12 18:39:18 -080092
Chris Sosa9164ca32012-03-28 11:04:50 -070093class DevServerError(Exception):
Chris Sosa47a7d4e2012-03-28 11:26:55 -070094 """Exception class used by this module."""
Chris Sosa47a7d4e2012-03-28 11:26:55 -070095
96
Don Garrett8ccab732013-08-30 09:13:59 -070097class DevServerHTTPError(cherrypy.HTTPError):
beepsd76c6092013-08-28 22:23:30 -070098 """Exception class to log the HTTPResponse before routing it to cherrypy."""
99 def __init__(self, status, message):
100 """
101 @param status: HTTPResponse status.
102 @param message: Message associated with the response.
103 """
Don Garrett8ccab732013-08-30 09:13:59 -0700104 cherrypy.HTTPError.__init__(self, status, message)
beepsd76c6092013-08-28 22:23:30 -0700105 _Log('HTTPError status: %s message: %s', status, message)
beepsd76c6092013-08-28 22:23:30 -0700106
107
Scott Zawalski4647ce62012-01-03 17:17:28 -0500108def _LeadingWhiteSpaceCount(string):
109 """Count the amount of leading whitespace in a string.
110
111 Args:
112 string: The string to count leading whitespace in.
113 Returns:
114 number of white space chars before characters start.
115 """
116 matched = re.match('^\s+', string)
117 if matched:
118 return len(matched.group())
119
120 return 0
121
122
123def _PrintDocStringAsHTML(func):
124 """Make a functions docstring somewhat HTML style.
125
126 Args:
127 func: The function to return the docstring from.
128 Returns:
129 A string that is somewhat formated for a web browser.
130 """
131 # TODO(scottz): Make this parse Args/Returns in a prettier way.
132 # Arguments could be bolded and indented etc.
133 html_doc = []
134 for line in func.__doc__.splitlines():
135 leading_space = _LeadingWhiteSpaceCount(line)
136 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700137 line = '&nbsp;' * leading_space + line
Scott Zawalski4647ce62012-01-03 17:17:28 -0500138
139 html_doc.append('<BR>%s' % line)
140
141 return '\n'.join(html_doc)
142
143
Chris Sosa7c931362010-10-11 19:49:01 -0700144def _GetConfig(options):
145 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800146
147 # On a system with IPv6 not compiled into the kernel,
148 # AF_INET6 sockets will return a socket.error exception.
149 # On such systems, fall-back to IPv4.
150 socket_host = '::'
151 try:
152 socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
153 except socket.error:
154 socket_host = '0.0.0.0'
155
Chris Sosa7c931362010-10-11 19:49:01 -0700156 base_config = { 'global':
157 { 'server.log_request_headers': True,
158 'server.protocol_version': 'HTTP/1.1',
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800159 'server.socket_host': socket_host,
Chris Sosa7c931362010-10-11 19:49:01 -0700160 'server.socket_port': int(options.port),
Chris Sosa374c62d2010-10-14 09:13:54 -0700161 'response.timeout': 6000,
Chris Sosa6fe23942012-07-02 15:44:46 -0700162 'request.show_tracebacks': True,
Chris Sosa72333d12012-06-13 11:28:05 -0700163 'server.socket_timeout': 60,
joychenecc02aa2013-07-17 18:27:35 -0700164 'server.thread_pool': 2,
Chris Sosa7c931362010-10-11 19:49:01 -0700165 },
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700166 '/api':
167 {
168 # Gets rid of cherrypy parsing post file for args.
169 'request.process_request_body': False,
170 },
Chris Sosaa1ef0102010-10-21 16:22:35 -0700171 '/build':
172 {
173 'response.timeout': 100000,
174 },
Chris Sosa7c931362010-10-11 19:49:01 -0700175 '/update':
176 {
177 # Gets rid of cherrypy parsing post file for args.
178 'request.process_request_body': False,
Chris Sosaf65f4b92010-10-21 15:57:51 -0700179 'response.timeout': 10000,
Chris Sosa7c931362010-10-11 19:49:01 -0700180 },
181 # Sets up the static dir for file hosting.
182 '/static':
joychened64b222013-06-21 16:39:34 -0700183 { 'tools.staticdir.dir': options.static_dir,
Chris Sosa7c931362010-10-11 19:49:01 -0700184 'tools.staticdir.on': True,
Chris Sosaf65f4b92010-10-21 15:57:51 -0700185 'response.timeout': 10000,
Chris Sosa7c931362010-10-11 19:49:01 -0700186 },
187 }
Chris Sosa5f118ef2012-07-12 11:37:50 -0700188 if options.production:
Alex Miller93beca52013-07-30 19:25:09 -0700189 base_config['global'].update({'server.thread_pool': 150})
Scott Zawalski1c5e7cd2012-02-27 13:12:52 -0500190
Chris Sosa7c931362010-10-11 19:49:01 -0700191 return base_config
rtc@google.com64244662009-11-12 00:52:08 +0000192
Darin Petkove17164a2010-08-11 13:24:41 -0700193
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700194def _GetRecursiveMemberObject(root, member_list):
195 """Returns an object corresponding to a nested member list.
196
197 Args:
198 root: the root object to search
199 member_list: list of nested members to search
200 Returns:
201 An object corresponding to the member name list; None otherwise.
202 """
203 for member in member_list:
204 next_root = root.__class__.__dict__.get(member)
205 if not next_root:
206 return None
207 root = next_root
208 return root
209
210
211def _IsExposed(name):
212 """Returns True iff |name| has an `exposed' attribute and it is set."""
213 return hasattr(name, 'exposed') and name.exposed
214
215
Gilad Arnold748c8322012-10-12 09:51:35 -0700216def _GetExposedMethod(root, nested_member, ignored=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700217 """Returns a CherryPy-exposed method, if such exists.
218
219 Args:
220 root: the root object for searching
221 nested_member: a slash-joined path to the nested member
222 ignored: method paths to be ignored
223 Returns:
224 A function object corresponding to the path defined by |member_list| from
225 the |root| object, if the function is exposed and not ignored; None
226 otherwise.
227 """
Gilad Arnold748c8322012-10-12 09:51:35 -0700228 method = (not (ignored and nested_member in ignored) and
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700229 _GetRecursiveMemberObject(root, nested_member.split('/')))
230 if (method and type(method) == types.FunctionType and _IsExposed(method)):
231 return method
232
233
Gilad Arnold748c8322012-10-12 09:51:35 -0700234def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700235 """Finds exposed CherryPy methods.
236
237 Args:
238 root: the root object for searching
239 prefix: slash-joined chain of members leading to current object
240 unlisted: URLs to be excluded regardless of their exposed status
241 Returns:
242 List of exposed URLs that are not unlisted.
243 """
244 method_list = []
245 for member in sorted(root.__class__.__dict__.keys()):
246 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 09:51:35 -0700247 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700248 continue
249 member_obj = root.__class__.__dict__[member]
250 if _IsExposed(member_obj):
251 if type(member_obj) == types.FunctionType:
252 method_list.append(prefixed_member)
253 else:
254 method_list += _FindExposedMethods(
255 member_obj, prefixed_member, unlisted)
256 return method_list
257
258
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700259class ApiRoot(object):
260 """RESTful API for Dev Server information."""
261 exposed = True
262
263 @cherrypy.expose
264 def hostinfo(self, ip):
265 """Returns a JSON dictionary containing information about the given ip.
266
Gilad Arnold1b908392012-10-05 11:36:27 -0700267 Args:
268 ip: address of host whose info is requested
269 Returns:
270 A JSON dictionary containing all or some of the following fields:
271 last_event_type (int): last update event type received
272 last_event_status (int): last update event status received
273 last_known_version (string): last known version reported in update ping
274 forced_update_label (string): update label to force next update ping to
275 use, set by setnextupdate
276 See the OmahaEvent class in update_engine/omaha_request_action.h for
277 event type and status code definitions. If the ip does not exist an empty
278 string is returned.
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700279
Gilad Arnold1b908392012-10-05 11:36:27 -0700280 Example URL:
281 http://myhost/api/hostinfo?ip=192.168.1.5
282 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700283 return updater.HandleHostInfoPing(ip)
284
285 @cherrypy.expose
Gilad Arnold286a0062012-01-12 13:47:02 -0800286 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 11:36:27 -0700287 """Returns a JSON object containing a log of host event.
288
289 Args:
290 ip: address of host whose event log is requested, or `all'
291 Returns:
292 A JSON encoded list (log) of dictionaries (events), each of which
293 containing a `timestamp' and other event fields, as described under
294 /api/hostinfo.
295
296 Example URL:
297 http://myhost/api/hostlog?ip=192.168.1.5
298 """
Gilad Arnold286a0062012-01-12 13:47:02 -0800299 return updater.HandleHostLogPing(ip)
300
301 @cherrypy.expose
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700302 def setnextupdate(self, ip):
303 """Allows the response to the next update ping from a host to be set.
304
305 Takes the IP of the host and an update label as normally provided to the
Gilad Arnold1b908392012-10-05 11:36:27 -0700306 /update command.
307 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700308 body_length = int(cherrypy.request.headers['Content-Length'])
309 label = cherrypy.request.rfile.read(body_length)
310
311 if label:
312 label = label.strip()
313 if label:
314 return updater.HandleSetUpdatePing(ip, label)
beepsd76c6092013-08-28 22:23:30 -0700315 raise DevServerHTTPError(400, 'No label provided.')
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700316
317
Gilad Arnold55a2a372012-10-02 09:46:32 -0700318 @cherrypy.expose
319 def fileinfo(self, *path_args):
320 """Returns information about a given staged file.
321
322 Args:
323 path_args: path to the file inside the server's static staging directory
324 Returns:
325 A JSON encoded dictionary with information about the said file, which may
326 contain the following keys/values:
Gilad Arnold1b908392012-10-05 11:36:27 -0700327 size (int): the file size in bytes
328 sha1 (string): a base64 encoded SHA1 hash
329 sha256 (string): a base64 encoded SHA256 hash
330
331 Example URL:
332 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 09:46:32 -0700333 """
334 file_path = os.path.join(updater.static_dir, *path_args)
335 if not os.path.exists(file_path):
336 raise DevServerError('file not found: %s' % file_path)
337 try:
338 file_size = os.path.getsize(file_path)
339 file_sha1 = common_util.GetFileSha1(file_path)
340 file_sha256 = common_util.GetFileSha256(file_path)
341 except os.error, e:
342 raise DevServerError('failed to get info for file %s: %s' %
Gilad Arnolde74b3812013-04-22 11:27:38 -0700343 (file_path, e))
344
345 is_delta = autoupdate.Autoupdate.IsDeltaFormatFile(file_path)
346
347 return json.dumps({
348 autoupdate.Autoupdate.SIZE_ATTR: file_size,
349 autoupdate.Autoupdate.SHA1_ATTR: file_sha1,
350 autoupdate.Autoupdate.SHA256_ATTR: file_sha256,
351 autoupdate.Autoupdate.ISDELTA_ATTR: is_delta
352 })
Gilad Arnold55a2a372012-10-02 09:46:32 -0700353
Chris Sosa76e44b92013-01-31 12:11:38 -0800354
David Rochberg7c79a812011-01-19 14:24:45 -0500355class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700356 """The Root Class for the Dev Server.
357
358 CherryPy works as follows:
359 For each method in this class, cherrpy interprets root/path
360 as a call to an instance of DevServerRoot->method_name. For example,
361 a call to http://myhost/build will call build. CherryPy automatically
362 parses http args and places them as keyword arguments in each method.
363 For paths http://myhost/update/dir1/dir2, you can use *args so that
364 cherrypy uses the update method and puts the extra paths in args.
365 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700366 # Method names that should not be listed on the index page.
367 _UNLISTED_METHODS = ['index', 'doc']
368
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700369 api = ApiRoot()
Chris Sosa7c931362010-10-11 19:49:01 -0700370
Dan Shi59ae7092013-06-04 14:37:27 -0700371 # Number of threads that devserver is staging images.
372 _staging_thread_count = 0
373 # Lock used to lock increasing/decreasing count.
374 _staging_thread_count_lock = threading.Lock()
375
joychen3cb228e2013-06-12 12:13:13 -0700376 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700377 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800378 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 12:13:13 -0700379 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 14:24:45 -0500380
Chris Sosa6b0c6172013-08-05 17:01:33 -0700381 @staticmethod
382 def _get_artifacts(kwargs):
383 """Returns a tuple of named and file artifacts given the stage rpc kwargs.
384
385 Raises: DevserverError if no artifacts would be returned.
386 """
387 artifacts = kwargs.get('artifacts')
388 files = kwargs.get('files')
389 if not artifacts and not files:
390 raise DevServerError('No artifacts specified.')
391
Chris Sosafa86b482013-09-04 11:30:36 -0700392 # Note we NEED to coerce files to a string as we get raw unicode from
393 # cherrypy and we treat files as strings elsewhere in the code.
394 return (str(artifacts).split(',') if artifacts else [],
395 str(files).split(',') if files else [])
Chris Sosa6b0c6172013-08-05 17:01:33 -0700396
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700397 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500398 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700399 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700400 import builder
401 if self._builder is None:
402 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500403 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700404
Chris Sosacde6bf42012-05-31 18:36:39 -0700405 @staticmethod
406 def _canonicalize_archive_url(archive_url):
407 """Canonicalizes archive_url strings.
408
409 Raises:
410 DevserverError: if archive_url is not set.
411 """
412 if archive_url:
Chris Sosa76e44b92013-01-31 12:11:38 -0800413 if not archive_url.startswith('gs://'):
Don Garrett8ccab732013-08-30 09:13:59 -0700414 raise DevServerError("Archive URL isn't from Google Storage (%s) ." %
415 archive_url)
Chris Sosa76e44b92013-01-31 12:11:38 -0800416
Chris Sosacde6bf42012-05-31 18:36:39 -0700417 return archive_url.rstrip('/')
418 else:
419 raise DevServerError("Must specify an archive_url in the request")
420
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700421 @cherrypy.expose
Dan Shif8eb0d12013-08-01 17:52:06 -0700422 def is_staged(self, **kwargs):
423 """Check if artifacts have been downloaded.
424
Chris Sosa6b0c6172013-08-05 17:01:33 -0700425 async: True to return without waiting for download to complete.
426 artifacts: Comma separated list of named artifacts to download.
427 These are defined in artifact_info and have their implementation
428 in build_artifact.py.
429 files: Comma separated list of file artifacts to stage. These
430 will be available as is in the corresponding static directory with no
431 custom post-processing.
432
433 returns: True of all artifacts are staged.
Dan Shif8eb0d12013-08-01 17:52:06 -0700434
435 Example:
436 To check if autotest and test_suites are staged:
437 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
438 artifacts=autotest,test_suites
439 """
440 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
Chris Sosa6b0c6172013-08-05 17:01:33 -0700441 artifacts, files = self._get_artifacts(kwargs)
Dan Shif8eb0d12013-08-01 17:52:06 -0700442 return str(downloader.Downloader(updater.static_dir, archive_url).IsStaged(
Chris Sosa6b0c6172013-08-05 17:01:33 -0700443 artifacts, files))
Dan Shi59ae7092013-06-04 14:37:27 -0700444
Chris Sosa76e44b92013-01-31 12:11:38 -0800445 @cherrypy.expose
446 def stage(self, **kwargs):
447 """Downloads and caches the artifacts from Google Storage URL.
448
449 Downloads and caches the artifacts Google Storage URL. Returns once these
450 have been downloaded on the devserver. A call to this will attempt to cache
451 non-specified artifacts in the background for the given from the given URL
452 following the principle of spatial locality. Spatial locality of different
453 artifacts is explicitly defined in the build_artifact module.
454
455 These artifacts will then be available from the static/ sub-directory of
456 the devserver.
457
458 Args:
459 archive_url: Google Storage URL for the build.
Dan Shif8eb0d12013-08-01 17:52:06 -0700460 async: True to return without waiting for download to complete.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700461 artifacts: Comma separated list of named artifacts to download.
462 These are defined in artifact_info and have their implementation
463 in build_artifact.py.
464 files: Comma separated list of files to stage. These
465 will be available as is in the corresponding static directory with no
466 custom post-processing.
Chris Sosa76e44b92013-01-31 12:11:38 -0800467
468 Example:
469 To download the autotest and test suites tarballs:
470 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
471 artifacts=autotest,test_suites
472 To download the full update payload:
473 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
474 artifacts=full_payload
Chris Sosa6b0c6172013-08-05 17:01:33 -0700475 To download just a file called blah.bin:
476 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
477 files=blah.bin
Chris Sosa76e44b92013-01-31 12:11:38 -0800478
479 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 16:39:34 -0700480 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 12:11:38 -0800481
482 Note for this example, relative path is the archive_url stripped of its
483 basename i.e. path/ in the examples above. Specific example:
484
485 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
486
487 Will get staged to:
488
joychened64b222013-06-21 16:39:34 -0700489 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Chris Sosa76e44b92013-01-31 12:11:38 -0800490 """
Chris Sosacde6bf42012-05-31 18:36:39 -0700491 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
Dan Shif8eb0d12013-08-01 17:52:06 -0700492 async = kwargs.get('async', False)
Chris Sosa6b0c6172013-08-05 17:01:33 -0700493 artifacts, files = self._get_artifacts(kwargs)
Dan Shi59ae7092013-06-04 14:37:27 -0700494 with DevServerRoot._staging_thread_count_lock:
495 DevServerRoot._staging_thread_count += 1
496 try:
Chris Sosa6b0c6172013-08-05 17:01:33 -0700497 downloader.Downloader(updater.static_dir, archive_url).Download(
498 artifacts, files, async=async)
Dan Shi59ae7092013-06-04 14:37:27 -0700499 finally:
500 with DevServerRoot._staging_thread_count_lock:
501 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800502 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700503
504 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -0800505 def setup_telemetry(self, **kwargs):
506 """Extracts and sets up telemetry
507
508 This method goes through the telemetry deps packages, and stages them on
509 the devserver to be used by the drones and the telemetry tests.
510
511 Args:
512 archive_url: Google Storage URL for the build.
513
514 Returns:
515 Path to the source folder for the telemetry codebase once it is staged.
516 """
517 archive_url = kwargs.get('archive_url')
518 self.stage(archive_url=archive_url, artifacts='autotest')
519
520 build = '/'.join(downloader.Downloader.ParseUrl(archive_url))
521 build_path = os.path.join(updater.static_dir, build)
522 deps_path = os.path.join(build_path, 'autotest/packages')
523 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
524 src_folder = os.path.join(telemetry_path, 'src')
525
526 with self._telemetry_lock_dict.lock(telemetry_path):
527 if os.path.exists(src_folder):
528 # Telemetry is already fully stage return
529 return src_folder
530
531 common_util.MkDirP(telemetry_path)
532
533 # Copy over the required deps tar balls to the telemetry directory.
534 for dep in TELEMETRY_DEPS:
535 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -0700536 if not os.path.exists(dep_path):
537 # This dep does not exist (could be new), do not extract it.
538 continue
Simran Basi4baad082013-02-14 13:39:18 -0800539 try:
540 common_util.ExtractTarball(dep_path, telemetry_path)
541 except common_util.CommonUtilError as e:
542 shutil.rmtree(telemetry_path)
543 raise DevServerError(str(e))
544
545 # By default all the tarballs extract to test_src but some parts of
546 # the telemetry code specifically hardcoded to exist inside of 'src'.
547 test_src = os.path.join(telemetry_path, 'test_src')
548 try:
549 shutil.move(test_src, src_folder)
550 except shutil.Error:
551 # This can occur if src_folder already exists. Remove and retry move.
552 shutil.rmtree(src_folder)
553 raise DevServerError('Failure in telemetry setup for build %s. Appears'
554 ' that the test_src to src move failed.' % build)
555
556 return src_folder
557
558 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800559 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -0700560 """Symbolicates a minidump using pre-downloaded symbols, returns it.
561
562 Callers will need to POST to this URL with a body of MIME-type
563 "multipart/form-data".
564 The body should include a single argument, 'minidump', containing the
565 binary-formatted minidump to symbolicate.
566
Chris Masone816e38c2012-05-02 12:22:36 -0700567 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -0800568 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -0700569 minidump: The binary minidump file to symbolicate.
570 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800571 # Ensure the symbols have been staged.
572 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
573 if self.stage(archive_url=archive_url, artifacts='symbols') != 'Success':
574 raise DevServerError('Failed to stage symbols for %s' % archive_url)
575
Chris Masone816e38c2012-05-02 12:22:36 -0700576 to_return = ''
577 with tempfile.NamedTemporaryFile() as local:
578 while True:
579 data = minidump.file.read(8192)
580 if not data:
581 break
582 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -0800583
Chris Masone816e38c2012-05-02 12:22:36 -0700584 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -0800585
586 symbols_directory = os.path.join(downloader.Downloader.GetBuildDir(
587 updater.static_dir, archive_url), 'debug', 'breakpad')
588
589 stackwalk = subprocess.Popen(
590 ['minidump_stackwalk', local.name, symbols_directory],
591 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
592
Chris Masone816e38c2012-05-02 12:22:36 -0700593 to_return, error_text = stackwalk.communicate()
594 if stackwalk.returncode != 0:
595 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
596 error_text, stackwalk.returncode))
597
598 return to_return
599
600 @cherrypy.expose
Scott Zawalski16954532012-03-20 15:31:36 -0400601 def latestbuild(self, **params):
602 """Return a string representing the latest build for a given target.
603
604 Args:
605 target: The build target, typically a combination of the board and the
606 type of build e.g. x86-mario-release.
607 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
608 provided the latest RXX build will be returned.
609 Returns:
610 A string representation of the latest build if one exists, i.e.
611 R19-1993.0.0-a1-b1480.
612 An empty string if no latest could be found.
613 """
614 if not params:
615 return _PrintDocStringAsHTML(self.latestbuild)
616
617 if 'target' not in params:
beepsd76c6092013-08-28 22:23:30 -0700618 raise DevServerHTTPError(500, 'Error: target= is required!')
Scott Zawalski16954532012-03-20 15:31:36 -0400619 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700620 return common_util.GetLatestBuildVersion(
Scott Zawalski16954532012-03-20 15:31:36 -0400621 updater.static_dir, params['target'],
622 milestone=params.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700623 except common_util.CommonUtilError as errmsg:
beepsd76c6092013-08-28 22:23:30 -0700624 raise DevServerHTTPError(500, str(errmsg))
Scott Zawalski16954532012-03-20 15:31:36 -0400625
626 @cherrypy.expose
Scott Zawalski84a39c92012-01-13 15:12:42 -0500627 def controlfiles(self, **params):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500628 """Return a control file or a list of all known control files.
629
630 Example URL:
631 To List all control files:
beepsbd337242013-07-09 22:44:06 -0700632 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
633 To List all control files for, say, the bvt suite:
634 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -0500635 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500636 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 -0500637
638 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500639 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500640 control_path: If you want the contents of a control file set this
641 to the path. E.g. client/site_tests/sleeptest/control
642 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -0700643 suite_name: If control_path is not specified but a suite_name is
644 specified, list the control files belonging to that suite instead of
645 all control files. The empty string for suite_name will list all control
646 files for the build.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500647 Returns:
648 Contents of a control file if control_path is provided.
649 A list of control files if no control_path is provided.
650 """
Scott Zawalski4647ce62012-01-03 17:17:28 -0500651 if not params:
652 return _PrintDocStringAsHTML(self.controlfiles)
653
Scott Zawalski84a39c92012-01-13 15:12:42 -0500654 if 'build' not in params:
beepsd76c6092013-08-28 22:23:30 -0700655 raise DevServerHTTPError(500, 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -0500656
657 if 'control_path' not in params:
beepsbd337242013-07-09 22:44:06 -0700658 if 'suite_name' in params and params['suite_name']:
659 return common_util.GetControlFileListForSuite(
660 updater.static_dir, params['build'], params['suite_name'])
661 else:
662 return common_util.GetControlFileList(
663 updater.static_dir, params['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -0500664 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700665 return common_util.GetControlFile(
666 updater.static_dir, params['build'], params['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -0800667
668 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -0700669 def xbuddy(self, *args, **kwargs):
670 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -0700671
672 Args:
joycheneaf4cfc2013-07-02 08:38:57 -0700673 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -0700674 components of the path. The path can be understood as
675 "{local|remote}/build_id/artifact" where build_id is composed of
676 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -0700677
joychen121fc9b2013-08-02 14:30:30 -0700678 The first path element is optional, and can be "remote" or "local"
679 If local (the default), devserver will not attempt to access Google
680 Storage, and will only search the static directory for the files.
681 If remote, devserver will try to obtain the artifact off GS if it's
682 not found locally.
683 The board is the familiar board name, optionally suffixed.
684 The version can be the google storage version number, and may also be
685 any of a number of xBuddy defined version aliases that will be
686 translated into the latest built image that fits the description.
687 Defaults to latest.
688 The artifact is one of a number of image or artifact aliases used by
689 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -0700690
691 Kwargs:
joychen3cb228e2013-06-12 12:13:13 -0700692 return_dir: {true|false}
693 if set to true, returns the url to the update.gz
694 instead.
Chris Sosa75490802013-09-30 17:21:45 -0700695 for_update: {true|false}
696 if for_update, pre-generates the update payload for the image
697 and returns the update path to pass to the
698 update_engine_client.
joychen3cb228e2013-06-12 12:13:13 -0700699
700 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -0700701 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -0700702 or
joycheneaf4cfc2013-07-02 08:38:57 -0700703 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -0700704
705 Returns:
706 A redirect to the image or update file on the devserver.
707 e.g. http://host:port/static/archive/x86-generic-release/
708 R26-4000.0.0/chromium-test-image.bin
709 or if return_dir is True, return path to the folder where
joychen121fc9b2013-08-02 14:30:30 -0700710 the artifact is.
joychen3cb228e2013-06-12 12:13:13 -0700711 http://host:port/static/x86-generic-release/R26-4000.0.0/
712 """
713 boolean_string = kwargs.get('return_dir')
714 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
Chris Sosa75490802013-09-30 17:21:45 -0700715 boolean_string = kwargs.get('for_update')
716 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -0700717
Chris Sosa75490802013-09-30 17:21:45 -0700718 if for_update and return_dir:
719 raise DevServerHTTPError(500, 'Cannot specify both update and return_dir')
720
721 # For updates, we optimize downloading of test images.
722 file_name = None
723 build_id = None
724 if for_update:
725 try:
726 build_id = self._xbuddy.StageTestAritfactsForUpdate(args)
727 except build_artifact.ArtifactDownloadError:
728 build_id = None
729
730 if not build_id:
731 build_id, file_name = self._xbuddy.Get(args)
732
joychen3cb228e2013-06-12 12:13:13 -0700733 if return_dir:
Chris Sosa855b8932013-08-21 13:24:55 -0700734 directory = os.path.join(cherrypy.request.base, 'static', build_id)
joycheneaf4cfc2013-07-02 08:38:57 -0700735 _Log("Directory requested, returning: %s", directory)
736 return directory
Chris Sosa75490802013-09-30 17:21:45 -0700737 elif for_update:
738 # Forces paylaod to be in cache and symlinked into build_id dir.
739 updater.GetUpdateForLabel(autoupdate.FORCED_UPDATE, build_id,
740 image_name=file_name)
741 update_uri = os.path.join(cherrypy.request.base, 'update', build_id)
742 _Log("Update requested, returning: %s", update_uri)
743 return update_uri
joychen3cb228e2013-06-12 12:13:13 -0700744 else:
joychen121fc9b2013-08-02 14:30:30 -0700745 build_id = '/' + os.path.join('static', build_id, file_name)
746 _Log("Payload requested, returning: %s", build_id)
747 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -0700748
749 @cherrypy.expose
750 def xbuddy_list(self):
751 """Lists the currently available images & time since last access.
752
753 @return: A string representation of a list of tuples
754 [(build_id, time since last access),...]
755 """
756 return self._xbuddy.List()
757
758 @cherrypy.expose
759 def xbuddy_capacity(self):
760 """Returns the number of images cached by xBuddy.
761
762 @return: Capacity of this devserver.
763 """
764 return self._xbuddy.Capacity()
765
766 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700767 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700768 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700769 return ('Welcome to the Dev Server!<br>\n'
770 '<br>\n'
771 'Here are the available methods, click for documentation:<br>\n'
772 '<br>\n'
773 '%s' %
774 '<br>\n'.join(
775 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700776 for name in _FindExposedMethods(
777 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700778
779 @cherrypy.expose
780 def doc(self, *args):
781 """Shows the documentation for available methods / URLs.
782
783 Example:
784 http://myhost/doc/update
785 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700786 name = '/'.join(args)
787 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700788 if not method:
789 raise DevServerError("No exposed method named `%s'" % name)
790 if not method.__doc__:
791 raise DevServerError("No documentation for exposed method `%s'" % name)
792 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -0700793
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700794 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700795 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700796 """Handles an update check from a Chrome OS client.
797
798 The HTTP request should contain the standard Omaha-style XML blob. The URL
799 line may contain an additional intermediate path to the update payload.
800
joychen121fc9b2013-08-02 14:30:30 -0700801 This request can be handled in one of 4 ways, depending on the devsever
802 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -0700803
joychen121fc9b2013-08-02 14:30:30 -0700804 1. No intermediate path
805 If no intermediate path is given, the default behavior is to generate an
806 update payload from the latest test image locally built for the board
807 specified in the xml. Devserver serves the generated payload.
808
809 2. Path explicitly invokes XBuddy
810 If there is a path given, it can explicitly invoke xbuddy by prefixing it
811 with 'xbuddy'. This path is then used to acquire an image binary for the
812 devserver to generate an update payload from. Devserver then serves this
813 payload.
814
815 3. Path is left for the devserver to interpret.
816 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
817 to generate a payload from the test image in that directory and serve it.
818
819 4. The devserver is in a 'forced' mode. TO BE DEPRECATED
820 This comes from the usage of --forced_payload or --image when starting the
821 devserver. No matter what path (or no path) gets passed in, devserver will
822 serve the update payload (--forced_payload) or generate an update payload
823 from the image (--image).
824
825 Examples:
826 1. No intermediate path
827 update_engine_client --omaha_url=http://myhost/update
828 This generates an update payload from the latest test image locally built
829 for the board specified in the xml.
830
831 2. Explicitly invoke xbuddy
832 update_engine_client --omaha_url=
833 http://myhost/update/xbuddy/remote/board/version/dev
834 This would go to GS to download the dev image for the board, from which
835 the devserver would generate a payload to serve.
836
837 3. Give a path for devserver to interpret
838 update_engine_client --omaha_url=http://myhost/update/some/random/path
839 This would attempt, in order to:
840 a) Generate an update from a test image binary if found in
841 static_dir/some/random/path.
842 b) Serve an update payload found in static_dir/some/random/path.
843 c) Hope that some/random/path takes the form "board/version" and
844 and attempt to download an update payload for that board/version
845 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700846 """
joychen121fc9b2013-08-02 14:30:30 -0700847 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -0800848 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -0700849 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-11 19:49:01 -0700850
joychen121fc9b2013-08-02 14:30:30 -0700851 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700852
Dan Shif5ce2de2013-04-25 16:06:32 -0700853 @cherrypy.expose
854 def check_health(self):
855 """Collect the health status of devserver to see if it's ready for staging.
856
857 @return: A JSON dictionary containing all or some of the following fields:
Dan Shi59ae7092013-06-04 14:37:27 -0700858 free_disk (int): free disk space in GB
859 staging_thread_count (int): number of devserver threads currently
860 staging an image
Dan Shif5ce2de2013-04-25 16:06:32 -0700861 """
862 # Get free disk space.
863 stat = os.statvfs(updater.static_dir)
864 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
865
866 return json.dumps({
867 'free_disk': free_disk,
Dan Shi59ae7092013-06-04 14:37:27 -0700868 'staging_thread_count': DevServerRoot._staging_thread_count,
Dan Shif5ce2de2013-04-25 16:06:32 -0700869 })
870
871
Chris Sosadbc20082012-12-10 13:39:11 -0800872def _CleanCache(cache_dir, wipe):
873 """Wipes any excess cached items in the cache_dir.
874
875 Args:
876 cache_dir: the directory we are wiping from.
877 wipe: If True, wipe all the contents -- not just the excess.
878 """
879 if wipe:
880 # Clear the cache and exit on error.
881 cmd = 'rm -rf %s/*' % cache_dir
882 if os.system(cmd) != 0:
883 _Log('Failed to clear the cache with %s' % cmd)
884 sys.exit(1)
885 else:
886 # Clear all but the last N cached updates
887 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
888 (cache_dir, CACHED_ENTRIES))
889 if os.system(cmd) != 0:
890 _Log('Failed to clean up old delta cache files with %s' % cmd)
891 sys.exit(1)
892
893
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700894def _AddTestingOptions(parser):
895 group = optparse.OptionGroup(
896 parser, 'Advanced Testing Options', 'These are used by test scripts and '
897 'developers writing integration tests utilizing the devserver. They are '
898 'not intended to be really used outside the scope of someone '
899 'knowledgable about the test.')
900 group.add_option('--exit',
901 action='store_true',
902 help='do not start the server (yet pregenerate/clear cache)')
903 group.add_option('--host_log',
904 action='store_true', default=False,
905 help='record history of host update events (/api/hostlog)')
906 group.add_option('--max_updates',
907 metavar='NUM', default= -1, type='int',
908 help='maximum number of update checks handled positively '
909 '(default: unlimited)')
910 group.add_option('--private_key',
911 metavar='PATH', default=None,
912 help='path to the private key in pem format. If this is set '
913 'the devserver will generate update payloads that are '
914 'signed with this key.')
915 group.add_option('--proxy_port',
916 metavar='PORT', default=None, type='int',
917 help='port to have the client connect to -- basically the '
918 'devserver lies to the update to tell it to get the payload '
919 'from a different port that will proxy the request back to '
920 'the devserver. The proxy must be managed outside the '
921 'devserver.')
922 group.add_option('--remote_payload',
923 action='store_true', default=False,
924 help='Payload is being served from a remote machine')
925 group.add_option('-u', '--urlbase',
926 metavar='URL',
927 help='base URL for update images, other than the '
928 'devserver. Use in conjunction with remote_payload.')
929 parser.add_option_group(group)
930
931
932def _AddUpdateOptions(parser):
933 group = optparse.OptionGroup(
934 parser, 'Autoupdate Options', 'These options can be used to change '
935 'how the devserver either generates or serve update payloads. Please '
936 'note that all of these option affect how a payload is generated and so '
937 'do not work in archive-only mode.')
938 group.add_option('--board',
939 help='By default the devserver will create an update '
940 'payload from the latest image built for the board '
941 'a device that is requesting an update has. When we '
942 'pre-generate an update (see below) and we do not specify '
943 'another update_type option like image or payload, the '
944 'devserver needs to know the board to generate the latest '
945 'image for. This is that board.')
946 group.add_option('--critical_update',
947 action='store_true', default=False,
948 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700949 group.add_option('--image',
950 metavar='FILE',
951 help='Generate and serve an update using this image to any '
952 'device that requests an update.')
953 group.add_option('--no_patch_kernel',
954 dest='patch_kernel', action='store_false', default=True,
955 help='When generating an update payload, do not patch the '
956 'kernel with kernel verification blob from the stateful '
957 'partition.')
958 group.add_option('--payload',
959 metavar='PATH',
960 help='use the update payload from specified directory '
961 '(update.gz).')
962 group.add_option('-p', '--pregenerate_update',
963 action='store_true', default=False,
964 help='pre-generate the update payload before accepting '
965 'update requests. Useful to help debug payload generation '
966 'issues quickly. Also if an update payload will take a '
967 'long time to generate, a client may timeout if you do not'
968 'pregenerate the update.')
969 group.add_option('--src_image',
970 metavar='PATH', default='',
971 help='If specified, delta updates will be generated using '
972 'this image as the source image. Delta updates are when '
973 'you are updating from a "source image" to a another '
974 'image.')
975 parser.add_option_group(group)
976
977
978def _AddProductionOptions(parser):
979 group = optparse.OptionGroup(
980 parser, 'Advanced Server Options', 'These options can be used to changed '
981 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700982 group.add_option('--clear_cache',
983 action='store_true', default=False,
984 help='At startup, removes all cached entries from the'
985 'devserver\'s cache.')
986 group.add_option('--logfile',
987 metavar='PATH',
988 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -0700989 group.add_option('--pidfile',
990 metavar='PATH',
991 help='path to output a pid file for the server.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700992 group.add_option('--production',
993 action='store_true', default=False,
994 help='have the devserver use production values when '
995 'starting up. This includes using more threads and '
996 'performing less logging.')
997 parser.add_option_group(group)
998
999
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001000def _MakeLogHandler(logfile):
1001 """Create a LogHandler instance used to log all messages."""
1002 hdlr_cls = handlers.TimedRotatingFileHandler
1003 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
1004 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -07001005 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001006 return hdlr
1007
1008
Chris Sosacde6bf42012-05-31 18:36:39 -07001009def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001010 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -08001011 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -07001012
1013 # get directory that the devserver is run from
1014 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -07001015 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -07001016 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001017 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -07001018 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -07001019 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001020 parser.add_option('--port',
1021 default=8080, type='int',
1022 help='port for the dev server to use (default: 8080)')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001023 parser.add_option('-t', '--test_image',
1024 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -07001025 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -07001026 parser.add_option('-x', '--xbuddy_manage_builds',
1027 action='store_true',
1028 default=False,
1029 help='If set, allow xbuddy to manage images in'
1030 'build/images.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001031 _AddProductionOptions(parser)
1032 _AddUpdateOptions(parser)
1033 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -07001034 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001035
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001036 # Handle options that must be set globally in cherrypy. Do this
1037 # work up front, because calls to _Log() below depend on this
1038 # initialization.
1039 if options.production:
1040 cherrypy.config.update({'environment': 'production'})
1041 if not options.logfile:
1042 cherrypy.config.update({'log.screen': True})
1043 else:
1044 cherrypy.config.update({'log.error_file': '',
1045 'log.access_file': ''})
1046 hdlr = _MakeLogHandler(options.logfile)
1047 # Pylint can't seem to process these two calls properly
1048 # pylint: disable=E1101
1049 cherrypy.log.access_log.addHandler(hdlr)
1050 cherrypy.log.error_log.addHandler(hdlr)
1051 # pylint: enable=E1101
1052
Chris Sosa7c931362010-10-11 19:49:01 -07001053 root_dir = os.path.realpath('%s/../..' % devserver_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001054
joychened64b222013-06-21 16:39:34 -07001055 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001056 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001057
joychened64b222013-06-21 16:39:34 -07001058 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001059 # If our devserver is only supposed to serve payloads, we shouldn't be
1060 # mucking with the cache at all. If the devserver hadn't previously
1061 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001062 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001063 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001064 else:
1065 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001066
Chris Sosadbc20082012-12-10 13:39:11 -08001067 _Log('Using cache directory %s' % cache_dir)
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001068 _Log('Source root is %s' % root_dir)
joychened64b222013-06-21 16:39:34 -07001069 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001070
joychen121fc9b2013-08-02 14:30:30 -07001071 _xbuddy = xbuddy.XBuddy(options.xbuddy_manage_builds,
1072 options.board,
1073 root_dir=root_dir,
1074 static_dir=options.static_dir)
Chris Sosa75490802013-09-30 17:21:45 -07001075 if options.clear_cache and options.xbuddy_manage_builds:
1076 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 14:30:30 -07001077
Chris Sosa6a3697f2013-01-29 16:44:43 -08001078 # We allow global use here to share with cherrypy classes.
1079 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001080 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001081 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001082 _xbuddy,
Andrew de los Reyes52620802010-04-12 13:40:07 -07001083 root_dir=root_dir,
joychened64b222013-06-21 16:39:34 -07001084 static_dir=options.static_dir,
Andrew de los Reyes52620802010-04-12 13:40:07 -07001085 urlbase=options.urlbase,
Chris Sosa5d342a22010-09-28 16:54:41 -07001086 forced_image=options.image,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001087 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001088 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-28 23:42:37 -07001089 src_image=options.src_image,
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001090 patch_kernel=options.patch_kernel,
Chris Sosa08d55a22011-01-19 16:08:02 -08001091 board=options.board,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001092 copy_to_static_root=not options.exit,
1093 private_key=options.private_key,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001094 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001095 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001096 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001097 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001098 )
Chris Sosa7c931362010-10-11 19:49:01 -07001099
Chris Sosa6a3697f2013-01-29 16:44:43 -08001100 if options.pregenerate_update:
1101 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 15:46:22 -07001102
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001103 if options.exit:
1104 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001105
joychen3cb228e2013-06-12 12:13:13 -07001106 dev_server = DevServerRoot(_xbuddy)
1107
Chris Sosa855b8932013-08-21 13:24:55 -07001108 if options.pidfile:
1109 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1110
joychen3cb228e2013-06-12 12:13:13 -07001111 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001112
1113
1114if __name__ == '__main__':
1115 main()