blob: 73fb39accbd1f4bd3929584ff04fa6467f125711 [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 Arnold11fbef42014-02-10 11:04:13 -080062import cherrypy_ext
Gilad Arnoldc65330c2012-09-20 15:17:48 -070063import common_util
Chris Sosa47a7d4e2012-03-28 11:26:55 -070064import downloader
Chris Sosa7cd23202013-10-15 17:22:57 -070065import gsutil_util
Gilad Arnoldc65330c2012-09-20 15:17:48 -070066import log_util
joychen3cb228e2013-06-12 12:13:13 -070067import xbuddy
Gilad Arnoldc65330c2012-09-20 15:17:48 -070068
Gilad Arnoldc65330c2012-09-20 15:17:48 -070069# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080070def _Log(message, *args):
71 return log_util.LogWithTag('DEVSERVER', message, *args)
Chris Sosa0356d3b2010-09-16 15:46:22 -070072
Frank Farzan40160872011-12-12 18:39:18 -080073
Chris Sosa417e55d2011-01-25 16:40:48 -080074CACHED_ENTRIES = 12
Don Garrettf90edf02010-11-16 17:36:14 -080075
Simran Basi4baad082013-02-14 13:39:18 -080076TELEMETRY_FOLDER = 'telemetry_src'
77TELEMETRY_DEPS = ['dep-telemetry_dep.tar.bz2',
78 'dep-page_cycler_dep.tar.bz2',
Simran Basi0d078682013-03-22 16:40:04 -070079 'dep-chrome_test.tar.bz2',
80 'dep-perf_data_dep.tar.bz2']
Simran Basi4baad082013-02-14 13:39:18 -080081
Chris Sosa0356d3b2010-09-16 15:46:22 -070082# Sets up global to share between classes.
rtc@google.com21a5ca32009-11-04 18:23:23 +000083updater = None
rtc@google.comded22402009-10-26 22:36:21 +000084
J. Richard Barnette3d977b82013-04-23 11:05:19 -070085# Log rotation parameters. These settings correspond to once a week
J. Richard Barnette6dfa5342013-06-04 11:48:56 -070086# at midnight between Friday and Saturday, with about three months
87# of old logs kept for backup.
J. Richard Barnette3d977b82013-04-23 11:05:19 -070088#
89# For more, see the documentation for
90# logging.handlers.TimedRotatingFileHandler
J. Richard Barnette6dfa5342013-06-04 11:48:56 -070091_LOG_ROTATION_TIME = 'W4'
J. Richard Barnette3d977b82013-04-23 11:05:19 -070092_LOG_ROTATION_BACKUP = 13
93
Frank Farzan40160872011-12-12 18:39:18 -080094
Chris Sosa9164ca32012-03-28 11:04:50 -070095class DevServerError(Exception):
Chris Sosa47a7d4e2012-03-28 11:26:55 -070096 """Exception class used by this module."""
Chris Sosa47a7d4e2012-03-28 11:26:55 -070097
98
Don Garrett8ccab732013-08-30 09:13:59 -070099class DevServerHTTPError(cherrypy.HTTPError):
beepsd76c6092013-08-28 22:23:30 -0700100 """Exception class to log the HTTPResponse before routing it to cherrypy."""
101 def __init__(self, status, message):
Don Garrettf84631a2014-01-07 18:21:26 -0800102 """CherryPy error with logging.
103
104 Args:
105 status: HTTPResponse status.
106 message: Message associated with the response.
beepsd76c6092013-08-28 22:23:30 -0700107 """
Don Garrett8ccab732013-08-30 09:13:59 -0700108 cherrypy.HTTPError.__init__(self, status, message)
beepsd76c6092013-08-28 22:23:30 -0700109 _Log('HTTPError status: %s message: %s', status, message)
beepsd76c6092013-08-28 22:23:30 -0700110
111
Scott Zawalski4647ce62012-01-03 17:17:28 -0500112def _LeadingWhiteSpaceCount(string):
113 """Count the amount of leading whitespace in a string.
114
115 Args:
116 string: The string to count leading whitespace in.
Don Garrettf84631a2014-01-07 18:21:26 -0800117
Scott Zawalski4647ce62012-01-03 17:17:28 -0500118 Returns:
119 number of white space chars before characters start.
120 """
121 matched = re.match('^\s+', string)
122 if matched:
123 return len(matched.group())
124
125 return 0
126
127
128def _PrintDocStringAsHTML(func):
129 """Make a functions docstring somewhat HTML style.
130
131 Args:
132 func: The function to return the docstring from.
Don Garrettf84631a2014-01-07 18:21:26 -0800133
Scott Zawalski4647ce62012-01-03 17:17:28 -0500134 Returns:
135 A string that is somewhat formated for a web browser.
136 """
137 # TODO(scottz): Make this parse Args/Returns in a prettier way.
138 # Arguments could be bolded and indented etc.
139 html_doc = []
140 for line in func.__doc__.splitlines():
141 leading_space = _LeadingWhiteSpaceCount(line)
142 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700143 line = '&nbsp;' * leading_space + line
Scott Zawalski4647ce62012-01-03 17:17:28 -0500144
145 html_doc.append('<BR>%s' % line)
146
147 return '\n'.join(html_doc)
148
149
Chris Sosa7c931362010-10-11 19:49:01 -0700150def _GetConfig(options):
151 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800152
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800153 socket_host = '::'
Yu-Ju Hongc8d4af32013-11-12 15:14:26 -0800154 # Fall back to IPv4 when python is not configured with IPv6.
155 if not socket.has_ipv6:
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800156 socket_host = '0.0.0.0'
157
Chris Sosa7c931362010-10-11 19:49:01 -0700158 base_config = { 'global':
159 { 'server.log_request_headers': True,
160 'server.protocol_version': 'HTTP/1.1',
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800161 'server.socket_host': socket_host,
Chris Sosa7c931362010-10-11 19:49:01 -0700162 'server.socket_port': int(options.port),
Chris Sosa374c62d2010-10-14 09:13:54 -0700163 'response.timeout': 6000,
Chris Sosa6fe23942012-07-02 15:44:46 -0700164 'request.show_tracebacks': True,
Chris Sosa72333d12012-06-13 11:28:05 -0700165 'server.socket_timeout': 60,
joychenecc02aa2013-07-17 18:27:35 -0700166 'server.thread_pool': 2,
Chris Sosa7c931362010-10-11 19:49:01 -0700167 },
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700168 '/api':
169 {
170 # Gets rid of cherrypy parsing post file for args.
171 'request.process_request_body': False,
172 },
Chris Sosaa1ef0102010-10-21 16:22:35 -0700173 '/build':
174 {
175 'response.timeout': 100000,
176 },
Chris Sosa7c931362010-10-11 19:49:01 -0700177 '/update':
178 {
179 # Gets rid of cherrypy parsing post file for args.
180 'request.process_request_body': False,
Chris Sosaf65f4b92010-10-21 15:57:51 -0700181 'response.timeout': 10000,
Chris Sosa7c931362010-10-11 19:49:01 -0700182 },
183 # Sets up the static dir for file hosting.
184 '/static':
joychened64b222013-06-21 16:39:34 -0700185 { 'tools.staticdir.dir': options.static_dir,
Chris Sosa7c931362010-10-11 19:49:01 -0700186 'tools.staticdir.on': True,
Chris Sosaf65f4b92010-10-21 15:57:51 -0700187 'response.timeout': 10000,
Chris Sosa7c931362010-10-11 19:49:01 -0700188 },
189 }
Chris Sosa5f118ef2012-07-12 11:37:50 -0700190 if options.production:
Alex Miller93beca52013-07-30 19:25:09 -0700191 base_config['global'].update({'server.thread_pool': 150})
Chris Sosa7cd23202013-10-15 17:22:57 -0700192 # TODO(sosa): Do this more cleanly.
193 gsutil_util.GSUTIL_ATTEMPTS = 5
Scott Zawalski1c5e7cd2012-02-27 13:12:52 -0500194
Chris Sosa7c931362010-10-11 19:49:01 -0700195 return base_config
rtc@google.com64244662009-11-12 00:52:08 +0000196
Darin Petkove17164a2010-08-11 13:24:41 -0700197
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700198def _GetRecursiveMemberObject(root, member_list):
199 """Returns an object corresponding to a nested member list.
200
201 Args:
202 root: the root object to search
203 member_list: list of nested members to search
Don Garrettf84631a2014-01-07 18:21:26 -0800204
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700205 Returns:
206 An object corresponding to the member name list; None otherwise.
207 """
208 for member in member_list:
209 next_root = root.__class__.__dict__.get(member)
210 if not next_root:
211 return None
212 root = next_root
213 return root
214
215
216def _IsExposed(name):
217 """Returns True iff |name| has an `exposed' attribute and it is set."""
218 return hasattr(name, 'exposed') and name.exposed
219
220
Gilad Arnold748c8322012-10-12 09:51:35 -0700221def _GetExposedMethod(root, nested_member, ignored=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700222 """Returns a CherryPy-exposed method, if such exists.
223
224 Args:
225 root: the root object for searching
226 nested_member: a slash-joined path to the nested member
227 ignored: method paths to be ignored
Don Garrettf84631a2014-01-07 18:21:26 -0800228
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700229 Returns:
230 A function object corresponding to the path defined by |member_list| from
231 the |root| object, if the function is exposed and not ignored; None
232 otherwise.
233 """
Gilad Arnold748c8322012-10-12 09:51:35 -0700234 method = (not (ignored and nested_member in ignored) and
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700235 _GetRecursiveMemberObject(root, nested_member.split('/')))
236 if (method and type(method) == types.FunctionType and _IsExposed(method)):
237 return method
238
239
Gilad Arnold748c8322012-10-12 09:51:35 -0700240def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700241 """Finds exposed CherryPy methods.
242
243 Args:
244 root: the root object for searching
245 prefix: slash-joined chain of members leading to current object
246 unlisted: URLs to be excluded regardless of their exposed status
Don Garrettf84631a2014-01-07 18:21:26 -0800247
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700248 Returns:
249 List of exposed URLs that are not unlisted.
250 """
251 method_list = []
252 for member in sorted(root.__class__.__dict__.keys()):
253 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 09:51:35 -0700254 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700255 continue
256 member_obj = root.__class__.__dict__[member]
257 if _IsExposed(member_obj):
258 if type(member_obj) == types.FunctionType:
259 method_list.append(prefixed_member)
260 else:
261 method_list += _FindExposedMethods(
262 member_obj, prefixed_member, unlisted)
263 return method_list
264
265
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700266class ApiRoot(object):
267 """RESTful API for Dev Server information."""
268 exposed = True
269
270 @cherrypy.expose
271 def hostinfo(self, ip):
272 """Returns a JSON dictionary containing information about the given ip.
273
Gilad Arnold1b908392012-10-05 11:36:27 -0700274 Args:
275 ip: address of host whose info is requested
Don Garrettf84631a2014-01-07 18:21:26 -0800276
Gilad Arnold1b908392012-10-05 11:36:27 -0700277 Returns:
278 A JSON dictionary containing all or some of the following fields:
279 last_event_type (int): last update event type received
280 last_event_status (int): last update event status received
281 last_known_version (string): last known version reported in update ping
282 forced_update_label (string): update label to force next update ping to
283 use, set by setnextupdate
284 See the OmahaEvent class in update_engine/omaha_request_action.h for
285 event type and status code definitions. If the ip does not exist an empty
286 string is returned.
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700287
Gilad Arnold1b908392012-10-05 11:36:27 -0700288 Example URL:
289 http://myhost/api/hostinfo?ip=192.168.1.5
290 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700291 return updater.HandleHostInfoPing(ip)
292
293 @cherrypy.expose
Gilad Arnold286a0062012-01-12 13:47:02 -0800294 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 11:36:27 -0700295 """Returns a JSON object containing a log of host event.
296
297 Args:
298 ip: address of host whose event log is requested, or `all'
Don Garrettf84631a2014-01-07 18:21:26 -0800299
Gilad Arnold1b908392012-10-05 11:36:27 -0700300 Returns:
301 A JSON encoded list (log) of dictionaries (events), each of which
302 containing a `timestamp' and other event fields, as described under
303 /api/hostinfo.
304
305 Example URL:
306 http://myhost/api/hostlog?ip=192.168.1.5
307 """
Gilad Arnold286a0062012-01-12 13:47:02 -0800308 return updater.HandleHostLogPing(ip)
309
310 @cherrypy.expose
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700311 def setnextupdate(self, ip):
312 """Allows the response to the next update ping from a host to be set.
313
314 Takes the IP of the host and an update label as normally provided to the
Gilad Arnold1b908392012-10-05 11:36:27 -0700315 /update command.
316 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700317 body_length = int(cherrypy.request.headers['Content-Length'])
318 label = cherrypy.request.rfile.read(body_length)
319
320 if label:
321 label = label.strip()
322 if label:
323 return updater.HandleSetUpdatePing(ip, label)
beepsd76c6092013-08-28 22:23:30 -0700324 raise DevServerHTTPError(400, 'No label provided.')
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700325
326
Gilad Arnold55a2a372012-10-02 09:46:32 -0700327 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800328 def fileinfo(self, *args):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700329 """Returns information about a given staged file.
330
331 Args:
Don Garrettf84631a2014-01-07 18:21:26 -0800332 args: path to the file inside the server's static staging directory
333
Gilad Arnold55a2a372012-10-02 09:46:32 -0700334 Returns:
335 A JSON encoded dictionary with information about the said file, which may
336 contain the following keys/values:
Gilad Arnold1b908392012-10-05 11:36:27 -0700337 size (int): the file size in bytes
338 sha1 (string): a base64 encoded SHA1 hash
339 sha256 (string): a base64 encoded SHA256 hash
340
341 Example URL:
342 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 09:46:32 -0700343 """
Don Garrettf84631a2014-01-07 18:21:26 -0800344 file_path = os.path.join(updater.static_dir, *args)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700345 if not os.path.exists(file_path):
346 raise DevServerError('file not found: %s' % file_path)
347 try:
348 file_size = os.path.getsize(file_path)
349 file_sha1 = common_util.GetFileSha1(file_path)
350 file_sha256 = common_util.GetFileSha256(file_path)
351 except os.error, e:
352 raise DevServerError('failed to get info for file %s: %s' %
Gilad Arnolde74b3812013-04-22 11:27:38 -0700353 (file_path, e))
354
355 is_delta = autoupdate.Autoupdate.IsDeltaFormatFile(file_path)
356
357 return json.dumps({
358 autoupdate.Autoupdate.SIZE_ATTR: file_size,
359 autoupdate.Autoupdate.SHA1_ATTR: file_sha1,
360 autoupdate.Autoupdate.SHA256_ATTR: file_sha256,
361 autoupdate.Autoupdate.ISDELTA_ATTR: is_delta
362 })
Gilad Arnold55a2a372012-10-02 09:46:32 -0700363
Chris Sosa76e44b92013-01-31 12:11:38 -0800364
David Rochberg7c79a812011-01-19 14:24:45 -0500365class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700366 """The Root Class for the Dev Server.
367
368 CherryPy works as follows:
369 For each method in this class, cherrpy interprets root/path
370 as a call to an instance of DevServerRoot->method_name. For example,
371 a call to http://myhost/build will call build. CherryPy automatically
372 parses http args and places them as keyword arguments in each method.
373 For paths http://myhost/update/dir1/dir2, you can use *args so that
374 cherrypy uses the update method and puts the extra paths in args.
375 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700376 # Method names that should not be listed on the index page.
377 _UNLISTED_METHODS = ['index', 'doc']
378
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700379 api = ApiRoot()
Chris Sosa7c931362010-10-11 19:49:01 -0700380
Dan Shi59ae7092013-06-04 14:37:27 -0700381 # Number of threads that devserver is staging images.
382 _staging_thread_count = 0
383 # Lock used to lock increasing/decreasing count.
384 _staging_thread_count_lock = threading.Lock()
385
joychen3cb228e2013-06-12 12:13:13 -0700386 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700387 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800388 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 12:13:13 -0700389 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 14:24:45 -0500390
Chris Sosa6b0c6172013-08-05 17:01:33 -0700391 @staticmethod
392 def _get_artifacts(kwargs):
393 """Returns a tuple of named and file artifacts given the stage rpc kwargs.
394
Don Garrettf84631a2014-01-07 18:21:26 -0800395 Raises:
396 DevserverError if no artifacts would be returned.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700397 """
398 artifacts = kwargs.get('artifacts')
399 files = kwargs.get('files')
400 if not artifacts and not files:
401 raise DevServerError('No artifacts specified.')
402
Chris Sosafa86b482013-09-04 11:30:36 -0700403 # Note we NEED to coerce files to a string as we get raw unicode from
404 # cherrypy and we treat files as strings elsewhere in the code.
405 return (str(artifacts).split(',') if artifacts else [],
406 str(files).split(',') if files else [])
Chris Sosa6b0c6172013-08-05 17:01:33 -0700407
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700408 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500409 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700410 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700411 import builder
412 if self._builder is None:
413 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500414 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700415
Chris Sosacde6bf42012-05-31 18:36:39 -0700416 @staticmethod
417 def _canonicalize_archive_url(archive_url):
418 """Canonicalizes archive_url strings.
419
420 Raises:
421 DevserverError: if archive_url is not set.
422 """
423 if archive_url:
Chris Sosa76e44b92013-01-31 12:11:38 -0800424 if not archive_url.startswith('gs://'):
Don Garrett8ccab732013-08-30 09:13:59 -0700425 raise DevServerError("Archive URL isn't from Google Storage (%s) ." %
426 archive_url)
Chris Sosa76e44b92013-01-31 12:11:38 -0800427
Chris Sosacde6bf42012-05-31 18:36:39 -0700428 return archive_url.rstrip('/')
429 else:
430 raise DevServerError("Must specify an archive_url in the request")
431
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700432 @cherrypy.expose
Dan Shif8eb0d12013-08-01 17:52:06 -0700433 def is_staged(self, **kwargs):
434 """Check if artifacts have been downloaded.
435
Chris Sosa6b0c6172013-08-05 17:01:33 -0700436 async: True to return without waiting for download to complete.
437 artifacts: Comma separated list of named artifacts to download.
438 These are defined in artifact_info and have their implementation
439 in build_artifact.py.
440 files: Comma separated list of file artifacts to stage. These
441 will be available as is in the corresponding static directory with no
442 custom post-processing.
443
444 returns: True of all artifacts are staged.
Dan Shif8eb0d12013-08-01 17:52:06 -0700445
446 Example:
447 To check if autotest and test_suites are staged:
448 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
449 artifacts=autotest,test_suites
450 """
451 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
Chris Sosa6b0c6172013-08-05 17:01:33 -0700452 artifacts, files = self._get_artifacts(kwargs)
Dan Shif8eb0d12013-08-01 17:52:06 -0700453 return str(downloader.Downloader(updater.static_dir, archive_url).IsStaged(
Chris Sosa6b0c6172013-08-05 17:01:33 -0700454 artifacts, files))
Dan Shi59ae7092013-06-04 14:37:27 -0700455
Chris Sosa76e44b92013-01-31 12:11:38 -0800456 @cherrypy.expose
457 def stage(self, **kwargs):
458 """Downloads and caches the artifacts from Google Storage URL.
459
460 Downloads and caches the artifacts Google Storage URL. Returns once these
461 have been downloaded on the devserver. A call to this will attempt to cache
462 non-specified artifacts in the background for the given from the given URL
463 following the principle of spatial locality. Spatial locality of different
464 artifacts is explicitly defined in the build_artifact module.
465
466 These artifacts will then be available from the static/ sub-directory of
467 the devserver.
468
469 Args:
470 archive_url: Google Storage URL for the build.
Dan Shif8eb0d12013-08-01 17:52:06 -0700471 async: True to return without waiting for download to complete.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700472 artifacts: Comma separated list of named artifacts to download.
473 These are defined in artifact_info and have their implementation
474 in build_artifact.py.
475 files: Comma separated list of files to stage. These
476 will be available as is in the corresponding static directory with no
477 custom post-processing.
Chris Sosa76e44b92013-01-31 12:11:38 -0800478
479 Example:
480 To download the autotest and test suites tarballs:
481 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
482 artifacts=autotest,test_suites
483 To download the full update payload:
484 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
485 artifacts=full_payload
Chris Sosa6b0c6172013-08-05 17:01:33 -0700486 To download just a file called blah.bin:
487 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
488 files=blah.bin
Chris Sosa76e44b92013-01-31 12:11:38 -0800489
490 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 16:39:34 -0700491 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 12:11:38 -0800492
493 Note for this example, relative path is the archive_url stripped of its
494 basename i.e. path/ in the examples above. Specific example:
495
496 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
497
498 Will get staged to:
499
joychened64b222013-06-21 16:39:34 -0700500 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Chris Sosa76e44b92013-01-31 12:11:38 -0800501 """
Chris Sosacde6bf42012-05-31 18:36:39 -0700502 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
Dan Shif8eb0d12013-08-01 17:52:06 -0700503 async = kwargs.get('async', False)
Chris Sosa6b0c6172013-08-05 17:01:33 -0700504 artifacts, files = self._get_artifacts(kwargs)
Dan Shi59ae7092013-06-04 14:37:27 -0700505 with DevServerRoot._staging_thread_count_lock:
506 DevServerRoot._staging_thread_count += 1
507 try:
Chris Sosa6b0c6172013-08-05 17:01:33 -0700508 downloader.Downloader(updater.static_dir, archive_url).Download(
509 artifacts, files, async=async)
Dan Shi59ae7092013-06-04 14:37:27 -0700510 finally:
511 with DevServerRoot._staging_thread_count_lock:
512 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800513 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700514
515 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -0800516 def setup_telemetry(self, **kwargs):
517 """Extracts and sets up telemetry
518
519 This method goes through the telemetry deps packages, and stages them on
520 the devserver to be used by the drones and the telemetry tests.
521
522 Args:
523 archive_url: Google Storage URL for the build.
524
525 Returns:
526 Path to the source folder for the telemetry codebase once it is staged.
527 """
528 archive_url = kwargs.get('archive_url')
529 self.stage(archive_url=archive_url, artifacts='autotest')
530
531 build = '/'.join(downloader.Downloader.ParseUrl(archive_url))
532 build_path = os.path.join(updater.static_dir, build)
533 deps_path = os.path.join(build_path, 'autotest/packages')
534 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
535 src_folder = os.path.join(telemetry_path, 'src')
536
537 with self._telemetry_lock_dict.lock(telemetry_path):
538 if os.path.exists(src_folder):
539 # Telemetry is already fully stage return
540 return src_folder
541
542 common_util.MkDirP(telemetry_path)
543
544 # Copy over the required deps tar balls to the telemetry directory.
545 for dep in TELEMETRY_DEPS:
546 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -0700547 if not os.path.exists(dep_path):
548 # This dep does not exist (could be new), do not extract it.
549 continue
Simran Basi4baad082013-02-14 13:39:18 -0800550 try:
551 common_util.ExtractTarball(dep_path, telemetry_path)
552 except common_util.CommonUtilError as e:
553 shutil.rmtree(telemetry_path)
554 raise DevServerError(str(e))
555
556 # By default all the tarballs extract to test_src but some parts of
557 # the telemetry code specifically hardcoded to exist inside of 'src'.
558 test_src = os.path.join(telemetry_path, 'test_src')
559 try:
560 shutil.move(test_src, src_folder)
561 except shutil.Error:
562 # This can occur if src_folder already exists. Remove and retry move.
563 shutil.rmtree(src_folder)
564 raise DevServerError('Failure in telemetry setup for build %s. Appears'
565 ' that the test_src to src move failed.' % build)
566
567 return src_folder
568
569 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800570 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -0700571 """Symbolicates a minidump using pre-downloaded symbols, returns it.
572
573 Callers will need to POST to this URL with a body of MIME-type
574 "multipart/form-data".
575 The body should include a single argument, 'minidump', containing the
576 binary-formatted minidump to symbolicate.
577
Chris Masone816e38c2012-05-02 12:22:36 -0700578 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -0800579 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -0700580 minidump: The binary minidump file to symbolicate.
581 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800582 # Ensure the symbols have been staged.
583 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
584 if self.stage(archive_url=archive_url, artifacts='symbols') != 'Success':
585 raise DevServerError('Failed to stage symbols for %s' % archive_url)
586
Chris Masone816e38c2012-05-02 12:22:36 -0700587 to_return = ''
588 with tempfile.NamedTemporaryFile() as local:
589 while True:
590 data = minidump.file.read(8192)
591 if not data:
592 break
593 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -0800594
Chris Masone816e38c2012-05-02 12:22:36 -0700595 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -0800596
597 symbols_directory = os.path.join(downloader.Downloader.GetBuildDir(
598 updater.static_dir, archive_url), 'debug', 'breakpad')
599
600 stackwalk = subprocess.Popen(
601 ['minidump_stackwalk', local.name, symbols_directory],
602 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
603
Chris Masone816e38c2012-05-02 12:22:36 -0700604 to_return, error_text = stackwalk.communicate()
605 if stackwalk.returncode != 0:
606 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
607 error_text, stackwalk.returncode))
608
609 return to_return
610
611 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800612 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 15:31:36 -0400613 """Return a string representing the latest build for a given target.
614
615 Args:
616 target: The build target, typically a combination of the board and the
617 type of build e.g. x86-mario-release.
618 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
619 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-07 18:21:26 -0800620
Scott Zawalski16954532012-03-20 15:31:36 -0400621 Returns:
622 A string representation of the latest build if one exists, i.e.
623 R19-1993.0.0-a1-b1480.
624 An empty string if no latest could be found.
625 """
Don Garrettf84631a2014-01-07 18:21:26 -0800626 if not kwargs:
Scott Zawalski16954532012-03-20 15:31:36 -0400627 return _PrintDocStringAsHTML(self.latestbuild)
628
Don Garrettf84631a2014-01-07 18:21:26 -0800629 if 'target' not in kwargs:
beepsd76c6092013-08-28 22:23:30 -0700630 raise DevServerHTTPError(500, 'Error: target= is required!')
Scott Zawalski16954532012-03-20 15:31:36 -0400631 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700632 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-07 18:21:26 -0800633 updater.static_dir, kwargs['target'],
634 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700635 except common_util.CommonUtilError as errmsg:
beepsd76c6092013-08-28 22:23:30 -0700636 raise DevServerHTTPError(500, str(errmsg))
Scott Zawalski16954532012-03-20 15:31:36 -0400637
638 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800639 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500640 """Return a control file or a list of all known control files.
641
642 Example URL:
643 To List all control files:
beepsbd337242013-07-09 22:44:06 -0700644 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
645 To List all control files for, say, the bvt suite:
646 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -0500647 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500648 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 -0500649
650 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500651 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500652 control_path: If you want the contents of a control file set this
653 to the path. E.g. client/site_tests/sleeptest/control
654 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -0700655 suite_name: If control_path is not specified but a suite_name is
656 specified, list the control files belonging to that suite instead of
657 all control files. The empty string for suite_name will list all control
658 files for the build.
Don Garrettf84631a2014-01-07 18:21:26 -0800659
Scott Zawalski4647ce62012-01-03 17:17:28 -0500660 Returns:
661 Contents of a control file if control_path is provided.
662 A list of control files if no control_path is provided.
663 """
Don Garrettf84631a2014-01-07 18:21:26 -0800664 if not kwargs:
Scott Zawalski4647ce62012-01-03 17:17:28 -0500665 return _PrintDocStringAsHTML(self.controlfiles)
666
Don Garrettf84631a2014-01-07 18:21:26 -0800667 if 'build' not in kwargs:
beepsd76c6092013-08-28 22:23:30 -0700668 raise DevServerHTTPError(500, 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -0500669
Don Garrettf84631a2014-01-07 18:21:26 -0800670 if 'control_path' not in kwargs:
671 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-09 22:44:06 -0700672 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-07 18:21:26 -0800673 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-09 22:44:06 -0700674 else:
675 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-07 18:21:26 -0800676 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -0500677 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700678 return common_util.GetControlFile(
Don Garrettf84631a2014-01-07 18:21:26 -0800679 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -0800680
681 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -0700682 def xbuddy(self, *args, **kwargs):
683 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -0700684
685 Args:
joycheneaf4cfc2013-07-02 08:38:57 -0700686 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -0700687 components of the path. The path can be understood as
688 "{local|remote}/build_id/artifact" where build_id is composed of
689 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -0700690
joychen121fc9b2013-08-02 14:30:30 -0700691 The first path element is optional, and can be "remote" or "local"
692 If local (the default), devserver will not attempt to access Google
693 Storage, and will only search the static directory for the files.
694 If remote, devserver will try to obtain the artifact off GS if it's
695 not found locally.
696 The board is the familiar board name, optionally suffixed.
697 The version can be the google storage version number, and may also be
698 any of a number of xBuddy defined version aliases that will be
699 translated into the latest built image that fits the description.
700 Defaults to latest.
701 The artifact is one of a number of image or artifact aliases used by
702 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -0700703
704 Kwargs:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800705 for_update: {true|false}
706 if true, pregenerates the update payloads for the image,
707 and returns the update uri to pass to the
708 update_engine_client.
joychen3cb228e2013-06-12 12:13:13 -0700709 return_dir: {true|false}
710 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800711 relative_path: {true|false}
712 if set to true, returns the relative path to the payload
713 directory from static_dir.
joychen3cb228e2013-06-12 12:13:13 -0700714 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -0700715 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -0700716 or
joycheneaf4cfc2013-07-02 08:38:57 -0700717 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -0700718
719 Returns:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800720 If |for_update|, returns a redirect to the image or update file
721 on the devserver. E.g.,
722 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
723 chromium-test-image.bin
724 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
725 http://host:port/static/x86-generic-release/R26-4000.0.0/
726 If |relative_path| is true, return a relative path the folder where the
727 payloads are. E.g.,
728 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 12:13:13 -0700729 """
Chris Sosa75490802013-09-30 17:21:45 -0700730 boolean_string = kwargs.get('for_update')
731 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800732 boolean_string = kwargs.get('return_dir')
733 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
734 boolean_string = kwargs.get('relative_path')
735 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -0700736
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800737 if return_dir and relative_path:
738 raise DevServerHTTPError(500, 'Cannot specify both return_dir and '
739 'relative_path')
Chris Sosa75490802013-09-30 17:21:45 -0700740
741 # For updates, we optimize downloading of test images.
742 file_name = None
743 build_id = None
744 if for_update:
745 try:
746 build_id = self._xbuddy.StageTestAritfactsForUpdate(args)
747 except build_artifact.ArtifactDownloadError:
748 build_id = None
749
750 if not build_id:
751 build_id, file_name = self._xbuddy.Get(args)
752
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800753 if for_update:
754 _Log('Payload generation triggered by request')
755 # Forces payload to be in cache and symlinked into build_id dir.
Chris Sosa75490802013-09-30 17:21:45 -0700756 updater.GetUpdateForLabel(autoupdate.FORCED_UPDATE, build_id,
757 image_name=file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800758
759 response = None
760 if return_dir:
761 response = os.path.join(cherrypy.request.base, 'static', build_id)
762 _Log('Directory requested, returning: %s', response)
763 elif relative_path:
764 response = build_id
765 _Log('Relative path requested, returning: %s', response)
766 elif for_update:
767 response = os.path.join(cherrypy.request.base, 'update', build_id)
768 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 12:13:13 -0700769 else:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800770 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 14:30:30 -0700771 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800772 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 14:30:30 -0700773 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -0700774
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800775 return response
776
joychen3cb228e2013-06-12 12:13:13 -0700777 @cherrypy.expose
778 def xbuddy_list(self):
779 """Lists the currently available images & time since last access.
780
Gilad Arnold452fd272014-02-04 11:09:28 -0800781 Returns:
782 A string representation of a list of tuples [(build_id, time since last
783 access),...]
joychen3cb228e2013-06-12 12:13:13 -0700784 """
785 return self._xbuddy.List()
786
787 @cherrypy.expose
788 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 11:09:28 -0800789 """Returns the number of images cached by xBuddy."""
joychen3cb228e2013-06-12 12:13:13 -0700790 return self._xbuddy.Capacity()
791
792 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700793 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700794 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700795 return ('Welcome to the Dev Server!<br>\n'
796 '<br>\n'
797 'Here are the available methods, click for documentation:<br>\n'
798 '<br>\n'
799 '%s' %
800 '<br>\n'.join(
801 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700802 for name in _FindExposedMethods(
803 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700804
805 @cherrypy.expose
806 def doc(self, *args):
807 """Shows the documentation for available methods / URLs.
808
809 Example:
810 http://myhost/doc/update
811 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700812 name = '/'.join(args)
813 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700814 if not method:
815 raise DevServerError("No exposed method named `%s'" % name)
816 if not method.__doc__:
817 raise DevServerError("No documentation for exposed method `%s'" % name)
818 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -0700819
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700820 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700821 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700822 """Handles an update check from a Chrome OS client.
823
824 The HTTP request should contain the standard Omaha-style XML blob. The URL
825 line may contain an additional intermediate path to the update payload.
826
joychen121fc9b2013-08-02 14:30:30 -0700827 This request can be handled in one of 4 ways, depending on the devsever
828 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -0700829
joychen121fc9b2013-08-02 14:30:30 -0700830 1. No intermediate path
831 If no intermediate path is given, the default behavior is to generate an
832 update payload from the latest test image locally built for the board
833 specified in the xml. Devserver serves the generated payload.
834
835 2. Path explicitly invokes XBuddy
836 If there is a path given, it can explicitly invoke xbuddy by prefixing it
837 with 'xbuddy'. This path is then used to acquire an image binary for the
838 devserver to generate an update payload from. Devserver then serves this
839 payload.
840
841 3. Path is left for the devserver to interpret.
842 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
843 to generate a payload from the test image in that directory and serve it.
844
845 4. The devserver is in a 'forced' mode. TO BE DEPRECATED
846 This comes from the usage of --forced_payload or --image when starting the
847 devserver. No matter what path (or no path) gets passed in, devserver will
848 serve the update payload (--forced_payload) or generate an update payload
849 from the image (--image).
850
851 Examples:
852 1. No intermediate path
853 update_engine_client --omaha_url=http://myhost/update
854 This generates an update payload from the latest test image locally built
855 for the board specified in the xml.
856
857 2. Explicitly invoke xbuddy
858 update_engine_client --omaha_url=
859 http://myhost/update/xbuddy/remote/board/version/dev
860 This would go to GS to download the dev image for the board, from which
861 the devserver would generate a payload to serve.
862
863 3. Give a path for devserver to interpret
864 update_engine_client --omaha_url=http://myhost/update/some/random/path
865 This would attempt, in order to:
866 a) Generate an update from a test image binary if found in
867 static_dir/some/random/path.
868 b) Serve an update payload found in static_dir/some/random/path.
869 c) Hope that some/random/path takes the form "board/version" and
870 and attempt to download an update payload for that board/version
871 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700872 """
joychen121fc9b2013-08-02 14:30:30 -0700873 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -0800874 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -0700875 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-11 19:49:01 -0700876
joychen121fc9b2013-08-02 14:30:30 -0700877 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700878
Dan Shif5ce2de2013-04-25 16:06:32 -0700879 @cherrypy.expose
880 def check_health(self):
881 """Collect the health status of devserver to see if it's ready for staging.
882
Gilad Arnold452fd272014-02-04 11:09:28 -0800883 Returns:
884 A JSON dictionary containing all or some of the following fields:
885 free_disk (int): free disk space in GB
886 staging_thread_count (int): number of devserver threads currently staging
887 an image
Dan Shif5ce2de2013-04-25 16:06:32 -0700888 """
889 # Get free disk space.
890 stat = os.statvfs(updater.static_dir)
891 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
892
893 return json.dumps({
894 'free_disk': free_disk,
Dan Shi59ae7092013-06-04 14:37:27 -0700895 'staging_thread_count': DevServerRoot._staging_thread_count,
Dan Shif5ce2de2013-04-25 16:06:32 -0700896 })
897
898
Chris Sosadbc20082012-12-10 13:39:11 -0800899def _CleanCache(cache_dir, wipe):
900 """Wipes any excess cached items in the cache_dir.
901
902 Args:
903 cache_dir: the directory we are wiping from.
904 wipe: If True, wipe all the contents -- not just the excess.
905 """
906 if wipe:
907 # Clear the cache and exit on error.
908 cmd = 'rm -rf %s/*' % cache_dir
909 if os.system(cmd) != 0:
910 _Log('Failed to clear the cache with %s' % cmd)
911 sys.exit(1)
912 else:
913 # Clear all but the last N cached updates
914 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
915 (cache_dir, CACHED_ENTRIES))
916 if os.system(cmd) != 0:
917 _Log('Failed to clean up old delta cache files with %s' % cmd)
918 sys.exit(1)
919
920
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700921def _AddTestingOptions(parser):
922 group = optparse.OptionGroup(
923 parser, 'Advanced Testing Options', 'These are used by test scripts and '
924 'developers writing integration tests utilizing the devserver. They are '
925 'not intended to be really used outside the scope of someone '
926 'knowledgable about the test.')
927 group.add_option('--exit',
928 action='store_true',
929 help='do not start the server (yet pregenerate/clear cache)')
930 group.add_option('--host_log',
931 action='store_true', default=False,
932 help='record history of host update events (/api/hostlog)')
933 group.add_option('--max_updates',
934 metavar='NUM', default= -1, type='int',
935 help='maximum number of update checks handled positively '
936 '(default: unlimited)')
937 group.add_option('--private_key',
938 metavar='PATH', default=None,
939 help='path to the private key in pem format. If this is set '
940 'the devserver will generate update payloads that are '
941 'signed with this key.')
David Zeuthen52ccd012013-10-31 12:58:26 -0700942 group.add_option('--private_key_for_metadata_hash_signature',
943 metavar='PATH', default=None,
944 help='path to the private key in pem format. If this is set '
945 'the devserver will sign the metadata hash with the given '
946 'key and transmit in the Omaha-style XML response.')
947 group.add_option('--public_key',
948 metavar='PATH', default=None,
949 help='path to the public key in pem format. If this is set '
950 'the devserver will transmit a base64 encoded version of '
951 'the content in the Omaha-style XML response.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700952 group.add_option('--proxy_port',
953 metavar='PORT', default=None, type='int',
954 help='port to have the client connect to -- basically the '
955 'devserver lies to the update to tell it to get the payload '
956 'from a different port that will proxy the request back to '
957 'the devserver. The proxy must be managed outside the '
958 'devserver.')
959 group.add_option('--remote_payload',
960 action='store_true', default=False,
961 help='Payload is being served from a remote machine')
962 group.add_option('-u', '--urlbase',
963 metavar='URL',
964 help='base URL for update images, other than the '
965 'devserver. Use in conjunction with remote_payload.')
966 parser.add_option_group(group)
967
968
969def _AddUpdateOptions(parser):
970 group = optparse.OptionGroup(
971 parser, 'Autoupdate Options', 'These options can be used to change '
972 'how the devserver either generates or serve update payloads. Please '
973 'note that all of these option affect how a payload is generated and so '
974 'do not work in archive-only mode.')
975 group.add_option('--board',
976 help='By default the devserver will create an update '
977 'payload from the latest image built for the board '
978 'a device that is requesting an update has. When we '
979 'pre-generate an update (see below) and we do not specify '
980 'another update_type option like image or payload, the '
981 'devserver needs to know the board to generate the latest '
982 'image for. This is that board.')
983 group.add_option('--critical_update',
984 action='store_true', default=False,
985 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700986 group.add_option('--image',
987 metavar='FILE',
988 help='Generate and serve an update using this image to any '
989 'device that requests an update.')
990 group.add_option('--no_patch_kernel',
991 dest='patch_kernel', action='store_false', default=True,
992 help='When generating an update payload, do not patch the '
993 'kernel with kernel verification blob from the stateful '
994 'partition.')
995 group.add_option('--payload',
996 metavar='PATH',
997 help='use the update payload from specified directory '
998 '(update.gz).')
999 group.add_option('-p', '--pregenerate_update',
1000 action='store_true', default=False,
1001 help='pre-generate the update payload before accepting '
1002 'update requests. Useful to help debug payload generation '
1003 'issues quickly. Also if an update payload will take a '
1004 'long time to generate, a client may timeout if you do not'
1005 'pregenerate the update.')
1006 group.add_option('--src_image',
1007 metavar='PATH', default='',
1008 help='If specified, delta updates will be generated using '
1009 'this image as the source image. Delta updates are when '
1010 'you are updating from a "source image" to a another '
1011 'image.')
1012 parser.add_option_group(group)
1013
1014
1015def _AddProductionOptions(parser):
1016 group = optparse.OptionGroup(
1017 parser, 'Advanced Server Options', 'These options can be used to changed '
1018 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001019 group.add_option('--clear_cache',
1020 action='store_true', default=False,
1021 help='At startup, removes all cached entries from the'
1022 'devserver\'s cache.')
1023 group.add_option('--logfile',
1024 metavar='PATH',
1025 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -07001026 group.add_option('--pidfile',
1027 metavar='PATH',
1028 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 11:04:13 -08001029 group.add_option('--portfile',
1030 metavar='PATH',
1031 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001032 group.add_option('--production',
1033 action='store_true', default=False,
1034 help='have the devserver use production values when '
1035 'starting up. This includes using more threads and '
1036 'performing less logging.')
1037 parser.add_option_group(group)
1038
1039
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001040def _MakeLogHandler(logfile):
1041 """Create a LogHandler instance used to log all messages."""
1042 hdlr_cls = handlers.TimedRotatingFileHandler
1043 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
1044 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -07001045 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001046 return hdlr
1047
1048
Chris Sosacde6bf42012-05-31 18:36:39 -07001049def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001050 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -08001051 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -07001052
1053 # get directory that the devserver is run from
1054 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -07001055 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -07001056 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001057 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -07001058 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -07001059 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001060 parser.add_option('--port',
1061 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 13:13:28 -08001062 help=('port for the dev server to use; if zero, binds to '
1063 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001064 parser.add_option('-t', '--test_image',
1065 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -07001066 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -07001067 parser.add_option('-x', '--xbuddy_manage_builds',
1068 action='store_true',
1069 default=False,
1070 help='If set, allow xbuddy to manage images in'
1071 'build/images.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001072 _AddProductionOptions(parser)
1073 _AddUpdateOptions(parser)
1074 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -07001075 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001076
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001077 # Handle options that must be set globally in cherrypy. Do this
1078 # work up front, because calls to _Log() below depend on this
1079 # initialization.
1080 if options.production:
1081 cherrypy.config.update({'environment': 'production'})
1082 if not options.logfile:
1083 cherrypy.config.update({'log.screen': True})
1084 else:
1085 cherrypy.config.update({'log.error_file': '',
1086 'log.access_file': ''})
1087 hdlr = _MakeLogHandler(options.logfile)
1088 # Pylint can't seem to process these two calls properly
1089 # pylint: disable=E1101
1090 cherrypy.log.access_log.addHandler(hdlr)
1091 cherrypy.log.error_log.addHandler(hdlr)
1092 # pylint: enable=E1101
1093
joychened64b222013-06-21 16:39:34 -07001094 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001095 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001096
joychened64b222013-06-21 16:39:34 -07001097 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001098 # If our devserver is only supposed to serve payloads, we shouldn't be
1099 # mucking with the cache at all. If the devserver hadn't previously
1100 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001101 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001102 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001103 else:
1104 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001105
Chris Sosadbc20082012-12-10 13:39:11 -08001106 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 16:39:34 -07001107 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001108
joychen121fc9b2013-08-02 14:30:30 -07001109 _xbuddy = xbuddy.XBuddy(options.xbuddy_manage_builds,
1110 options.board,
joychen121fc9b2013-08-02 14:30:30 -07001111 static_dir=options.static_dir)
Chris Sosa75490802013-09-30 17:21:45 -07001112 if options.clear_cache and options.xbuddy_manage_builds:
1113 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 14:30:30 -07001114
Chris Sosa6a3697f2013-01-29 16:44:43 -08001115 # We allow global use here to share with cherrypy classes.
1116 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001117 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001118 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001119 _xbuddy,
joychened64b222013-06-21 16:39:34 -07001120 static_dir=options.static_dir,
Andrew de los Reyes52620802010-04-12 13:40:07 -07001121 urlbase=options.urlbase,
Chris Sosa5d342a22010-09-28 16:54:41 -07001122 forced_image=options.image,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001123 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001124 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-28 23:42:37 -07001125 src_image=options.src_image,
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001126 patch_kernel=options.patch_kernel,
Chris Sosa08d55a22011-01-19 16:08:02 -08001127 board=options.board,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001128 copy_to_static_root=not options.exit,
1129 private_key=options.private_key,
David Zeuthen52ccd012013-10-31 12:58:26 -07001130 private_key_for_metadata_hash_signature=
1131 options.private_key_for_metadata_hash_signature,
1132 public_key=options.public_key,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001133 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001134 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001135 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001136 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001137 )
Chris Sosa7c931362010-10-11 19:49:01 -07001138
Chris Sosa6a3697f2013-01-29 16:44:43 -08001139 if options.pregenerate_update:
1140 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 15:46:22 -07001141
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001142 if options.exit:
1143 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001144
joychen3cb228e2013-06-12 12:13:13 -07001145 dev_server = DevServerRoot(_xbuddy)
1146
Gilad Arnold11fbef42014-02-10 11:04:13 -08001147 # Patch CherryPy to support binding to any available port (--port=0).
1148 cherrypy_ext.ZeroPortPatcher.DoPatch(cherrypy)
1149
Chris Sosa855b8932013-08-21 13:24:55 -07001150 if options.pidfile:
1151 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1152
Gilad Arnold11fbef42014-02-10 11:04:13 -08001153 if options.portfile:
1154 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1155
joychen3cb228e2013-06-12 12:13:13 -07001156 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001157
1158
1159if __name__ == '__main__':
1160 main()