blob: 043e376fe3fc09046078f3bc721fa854a94fe43b [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
Prashanth Ba06d2d22014-03-07 15:35:19 -0800457 def list_image_dir(self, **kwargs):
458 """Take an archive url and list the contents in its staged directory.
459
460 Args:
461 kwargs:
462 archive_url: Google Storage URL for the build.
463
464 Example:
465 To list the contents of where this devserver should have staged
466 gs://image-archive/<board>-release/<build> call:
467 http://devserver_url:<port>/list_image_dir?archive_url=<gs://..>
468
469 Returns:
470 A string with information about the contents of the image directory.
471 """
472 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
473 download_helper = downloader.Downloader(updater.static_dir, archive_url)
474 try:
475 image_dir_contents = download_helper.ListBuildDir()
476 except build_artifact.ArtifactDownloadError as e:
477 return 'Cannot list the contents of staged artifacts. %s' % e
478 if not image_dir_contents:
479 return '%s has not been staged on this devserver.' % archive_url
480 return image_dir_contents
481
482 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800483 def stage(self, **kwargs):
484 """Downloads and caches the artifacts from Google Storage URL.
485
486 Downloads and caches the artifacts Google Storage URL. Returns once these
487 have been downloaded on the devserver. A call to this will attempt to cache
488 non-specified artifacts in the background for the given from the given URL
489 following the principle of spatial locality. Spatial locality of different
490 artifacts is explicitly defined in the build_artifact module.
491
492 These artifacts will then be available from the static/ sub-directory of
493 the devserver.
494
495 Args:
496 archive_url: Google Storage URL for the build.
Dan Shif8eb0d12013-08-01 17:52:06 -0700497 async: True to return without waiting for download to complete.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700498 artifacts: Comma separated list of named artifacts to download.
499 These are defined in artifact_info and have their implementation
500 in build_artifact.py.
501 files: Comma separated list of files to stage. These
502 will be available as is in the corresponding static directory with no
503 custom post-processing.
Chris Sosa76e44b92013-01-31 12:11:38 -0800504
505 Example:
506 To download the autotest and test suites tarballs:
507 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
508 artifacts=autotest,test_suites
509 To download the full update payload:
510 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
511 artifacts=full_payload
Chris Sosa6b0c6172013-08-05 17:01:33 -0700512 To download just a file called blah.bin:
513 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
514 files=blah.bin
Chris Sosa76e44b92013-01-31 12:11:38 -0800515
516 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 16:39:34 -0700517 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 12:11:38 -0800518
519 Note for this example, relative path is the archive_url stripped of its
520 basename i.e. path/ in the examples above. Specific example:
521
522 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
523
524 Will get staged to:
525
joychened64b222013-06-21 16:39:34 -0700526 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Chris Sosa76e44b92013-01-31 12:11:38 -0800527 """
Chris Sosacde6bf42012-05-31 18:36:39 -0700528 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
Dan Shif8eb0d12013-08-01 17:52:06 -0700529 async = kwargs.get('async', False)
Chris Sosa6b0c6172013-08-05 17:01:33 -0700530 artifacts, files = self._get_artifacts(kwargs)
Dan Shi59ae7092013-06-04 14:37:27 -0700531 with DevServerRoot._staging_thread_count_lock:
532 DevServerRoot._staging_thread_count += 1
533 try:
Chris Sosa6b0c6172013-08-05 17:01:33 -0700534 downloader.Downloader(updater.static_dir, archive_url).Download(
535 artifacts, files, async=async)
Dan Shi59ae7092013-06-04 14:37:27 -0700536 finally:
537 with DevServerRoot._staging_thread_count_lock:
538 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800539 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700540
541 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -0800542 def setup_telemetry(self, **kwargs):
543 """Extracts and sets up telemetry
544
545 This method goes through the telemetry deps packages, and stages them on
546 the devserver to be used by the drones and the telemetry tests.
547
548 Args:
549 archive_url: Google Storage URL for the build.
550
551 Returns:
552 Path to the source folder for the telemetry codebase once it is staged.
553 """
554 archive_url = kwargs.get('archive_url')
555 self.stage(archive_url=archive_url, artifacts='autotest')
556
557 build = '/'.join(downloader.Downloader.ParseUrl(archive_url))
558 build_path = os.path.join(updater.static_dir, build)
559 deps_path = os.path.join(build_path, 'autotest/packages')
560 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
561 src_folder = os.path.join(telemetry_path, 'src')
562
563 with self._telemetry_lock_dict.lock(telemetry_path):
564 if os.path.exists(src_folder):
565 # Telemetry is already fully stage return
566 return src_folder
567
568 common_util.MkDirP(telemetry_path)
569
570 # Copy over the required deps tar balls to the telemetry directory.
571 for dep in TELEMETRY_DEPS:
572 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -0700573 if not os.path.exists(dep_path):
574 # This dep does not exist (could be new), do not extract it.
575 continue
Simran Basi4baad082013-02-14 13:39:18 -0800576 try:
577 common_util.ExtractTarball(dep_path, telemetry_path)
578 except common_util.CommonUtilError as e:
579 shutil.rmtree(telemetry_path)
580 raise DevServerError(str(e))
581
582 # By default all the tarballs extract to test_src but some parts of
583 # the telemetry code specifically hardcoded to exist inside of 'src'.
584 test_src = os.path.join(telemetry_path, 'test_src')
585 try:
586 shutil.move(test_src, src_folder)
587 except shutil.Error:
588 # This can occur if src_folder already exists. Remove and retry move.
589 shutil.rmtree(src_folder)
590 raise DevServerError('Failure in telemetry setup for build %s. Appears'
591 ' that the test_src to src move failed.' % build)
592
593 return src_folder
594
595 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800596 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -0700597 """Symbolicates a minidump using pre-downloaded symbols, returns it.
598
599 Callers will need to POST to this URL with a body of MIME-type
600 "multipart/form-data".
601 The body should include a single argument, 'minidump', containing the
602 binary-formatted minidump to symbolicate.
603
Chris Masone816e38c2012-05-02 12:22:36 -0700604 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -0800605 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -0700606 minidump: The binary minidump file to symbolicate.
607 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800608 # Ensure the symbols have been staged.
609 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
610 if self.stage(archive_url=archive_url, artifacts='symbols') != 'Success':
611 raise DevServerError('Failed to stage symbols for %s' % archive_url)
612
Chris Masone816e38c2012-05-02 12:22:36 -0700613 to_return = ''
614 with tempfile.NamedTemporaryFile() as local:
615 while True:
616 data = minidump.file.read(8192)
617 if not data:
618 break
619 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -0800620
Chris Masone816e38c2012-05-02 12:22:36 -0700621 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -0800622
623 symbols_directory = os.path.join(downloader.Downloader.GetBuildDir(
624 updater.static_dir, archive_url), 'debug', 'breakpad')
625
626 stackwalk = subprocess.Popen(
627 ['minidump_stackwalk', local.name, symbols_directory],
628 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
629
Chris Masone816e38c2012-05-02 12:22:36 -0700630 to_return, error_text = stackwalk.communicate()
631 if stackwalk.returncode != 0:
632 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
633 error_text, stackwalk.returncode))
634
635 return to_return
636
637 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800638 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 15:31:36 -0400639 """Return a string representing the latest build for a given target.
640
641 Args:
642 target: The build target, typically a combination of the board and the
643 type of build e.g. x86-mario-release.
644 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
645 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-07 18:21:26 -0800646
Scott Zawalski16954532012-03-20 15:31:36 -0400647 Returns:
648 A string representation of the latest build if one exists, i.e.
649 R19-1993.0.0-a1-b1480.
650 An empty string if no latest could be found.
651 """
Don Garrettf84631a2014-01-07 18:21:26 -0800652 if not kwargs:
Scott Zawalski16954532012-03-20 15:31:36 -0400653 return _PrintDocStringAsHTML(self.latestbuild)
654
Don Garrettf84631a2014-01-07 18:21:26 -0800655 if 'target' not in kwargs:
beepsd76c6092013-08-28 22:23:30 -0700656 raise DevServerHTTPError(500, 'Error: target= is required!')
Scott Zawalski16954532012-03-20 15:31:36 -0400657 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700658 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-07 18:21:26 -0800659 updater.static_dir, kwargs['target'],
660 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700661 except common_util.CommonUtilError as errmsg:
beepsd76c6092013-08-28 22:23:30 -0700662 raise DevServerHTTPError(500, str(errmsg))
Scott Zawalski16954532012-03-20 15:31:36 -0400663
664 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800665 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500666 """Return a control file or a list of all known control files.
667
668 Example URL:
669 To List all control files:
beepsbd337242013-07-09 22:44:06 -0700670 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
671 To List all control files for, say, the bvt suite:
672 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -0500673 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500674 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 -0500675
676 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500677 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500678 control_path: If you want the contents of a control file set this
679 to the path. E.g. client/site_tests/sleeptest/control
680 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -0700681 suite_name: If control_path is not specified but a suite_name is
682 specified, list the control files belonging to that suite instead of
683 all control files. The empty string for suite_name will list all control
684 files for the build.
Don Garrettf84631a2014-01-07 18:21:26 -0800685
Scott Zawalski4647ce62012-01-03 17:17:28 -0500686 Returns:
687 Contents of a control file if control_path is provided.
688 A list of control files if no control_path is provided.
689 """
Don Garrettf84631a2014-01-07 18:21:26 -0800690 if not kwargs:
Scott Zawalski4647ce62012-01-03 17:17:28 -0500691 return _PrintDocStringAsHTML(self.controlfiles)
692
Don Garrettf84631a2014-01-07 18:21:26 -0800693 if 'build' not in kwargs:
beepsd76c6092013-08-28 22:23:30 -0700694 raise DevServerHTTPError(500, 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -0500695
Don Garrettf84631a2014-01-07 18:21:26 -0800696 if 'control_path' not in kwargs:
697 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-09 22:44:06 -0700698 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-07 18:21:26 -0800699 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-09 22:44:06 -0700700 else:
701 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-07 18:21:26 -0800702 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -0500703 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700704 return common_util.GetControlFile(
Don Garrettf84631a2014-01-07 18:21:26 -0800705 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -0800706
707 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -0700708 def xbuddy(self, *args, **kwargs):
709 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -0700710
711 Args:
joycheneaf4cfc2013-07-02 08:38:57 -0700712 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -0700713 components of the path. The path can be understood as
714 "{local|remote}/build_id/artifact" where build_id is composed of
715 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -0700716
joychen121fc9b2013-08-02 14:30:30 -0700717 The first path element is optional, and can be "remote" or "local"
718 If local (the default), devserver will not attempt to access Google
719 Storage, and will only search the static directory for the files.
720 If remote, devserver will try to obtain the artifact off GS if it's
721 not found locally.
722 The board is the familiar board name, optionally suffixed.
723 The version can be the google storage version number, and may also be
724 any of a number of xBuddy defined version aliases that will be
725 translated into the latest built image that fits the description.
726 Defaults to latest.
727 The artifact is one of a number of image or artifact aliases used by
728 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -0700729
730 Kwargs:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800731 for_update: {true|false}
732 if true, pregenerates the update payloads for the image,
733 and returns the update uri to pass to the
734 update_engine_client.
joychen3cb228e2013-06-12 12:13:13 -0700735 return_dir: {true|false}
736 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800737 relative_path: {true|false}
738 if set to true, returns the relative path to the payload
739 directory from static_dir.
joychen3cb228e2013-06-12 12:13:13 -0700740 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -0700741 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -0700742 or
joycheneaf4cfc2013-07-02 08:38:57 -0700743 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -0700744
745 Returns:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800746 If |for_update|, returns a redirect to the image or update file
747 on the devserver. E.g.,
748 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
749 chromium-test-image.bin
750 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
751 http://host:port/static/x86-generic-release/R26-4000.0.0/
752 If |relative_path| is true, return a relative path the folder where the
753 payloads are. E.g.,
754 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 12:13:13 -0700755 """
Chris Sosa75490802013-09-30 17:21:45 -0700756 boolean_string = kwargs.get('for_update')
757 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800758 boolean_string = kwargs.get('return_dir')
759 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
760 boolean_string = kwargs.get('relative_path')
761 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -0700762
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800763 if return_dir and relative_path:
764 raise DevServerHTTPError(500, 'Cannot specify both return_dir and '
765 'relative_path')
Chris Sosa75490802013-09-30 17:21:45 -0700766
767 # For updates, we optimize downloading of test images.
768 file_name = None
769 build_id = None
770 if for_update:
771 try:
772 build_id = self._xbuddy.StageTestAritfactsForUpdate(args)
773 except build_artifact.ArtifactDownloadError:
774 build_id = None
775
776 if not build_id:
777 build_id, file_name = self._xbuddy.Get(args)
778
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800779 if for_update:
780 _Log('Payload generation triggered by request')
781 # Forces payload to be in cache and symlinked into build_id dir.
Chris Sosa75490802013-09-30 17:21:45 -0700782 updater.GetUpdateForLabel(autoupdate.FORCED_UPDATE, build_id,
783 image_name=file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800784
785 response = None
786 if return_dir:
787 response = os.path.join(cherrypy.request.base, 'static', build_id)
788 _Log('Directory requested, returning: %s', response)
789 elif relative_path:
790 response = build_id
791 _Log('Relative path requested, returning: %s', response)
792 elif for_update:
793 response = os.path.join(cherrypy.request.base, 'update', build_id)
794 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 12:13:13 -0700795 else:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800796 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 14:30:30 -0700797 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800798 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 14:30:30 -0700799 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -0700800
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800801 return response
802
joychen3cb228e2013-06-12 12:13:13 -0700803 @cherrypy.expose
804 def xbuddy_list(self):
805 """Lists the currently available images & time since last access.
806
Gilad Arnold452fd272014-02-04 11:09:28 -0800807 Returns:
808 A string representation of a list of tuples [(build_id, time since last
809 access),...]
joychen3cb228e2013-06-12 12:13:13 -0700810 """
811 return self._xbuddy.List()
812
813 @cherrypy.expose
814 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 11:09:28 -0800815 """Returns the number of images cached by xBuddy."""
joychen3cb228e2013-06-12 12:13:13 -0700816 return self._xbuddy.Capacity()
817
818 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700819 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700820 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700821 return ('Welcome to the Dev Server!<br>\n'
822 '<br>\n'
823 'Here are the available methods, click for documentation:<br>\n'
824 '<br>\n'
825 '%s' %
826 '<br>\n'.join(
827 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700828 for name in _FindExposedMethods(
829 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700830
831 @cherrypy.expose
832 def doc(self, *args):
833 """Shows the documentation for available methods / URLs.
834
835 Example:
836 http://myhost/doc/update
837 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700838 name = '/'.join(args)
839 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700840 if not method:
841 raise DevServerError("No exposed method named `%s'" % name)
842 if not method.__doc__:
843 raise DevServerError("No documentation for exposed method `%s'" % name)
844 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -0700845
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700846 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700847 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700848 """Handles an update check from a Chrome OS client.
849
850 The HTTP request should contain the standard Omaha-style XML blob. The URL
851 line may contain an additional intermediate path to the update payload.
852
joychen121fc9b2013-08-02 14:30:30 -0700853 This request can be handled in one of 4 ways, depending on the devsever
854 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -0700855
joychen121fc9b2013-08-02 14:30:30 -0700856 1. No intermediate path
857 If no intermediate path is given, the default behavior is to generate an
858 update payload from the latest test image locally built for the board
859 specified in the xml. Devserver serves the generated payload.
860
861 2. Path explicitly invokes XBuddy
862 If there is a path given, it can explicitly invoke xbuddy by prefixing it
863 with 'xbuddy'. This path is then used to acquire an image binary for the
864 devserver to generate an update payload from. Devserver then serves this
865 payload.
866
867 3. Path is left for the devserver to interpret.
868 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
869 to generate a payload from the test image in that directory and serve it.
870
871 4. The devserver is in a 'forced' mode. TO BE DEPRECATED
872 This comes from the usage of --forced_payload or --image when starting the
873 devserver. No matter what path (or no path) gets passed in, devserver will
874 serve the update payload (--forced_payload) or generate an update payload
875 from the image (--image).
876
877 Examples:
878 1. No intermediate path
879 update_engine_client --omaha_url=http://myhost/update
880 This generates an update payload from the latest test image locally built
881 for the board specified in the xml.
882
883 2. Explicitly invoke xbuddy
884 update_engine_client --omaha_url=
885 http://myhost/update/xbuddy/remote/board/version/dev
886 This would go to GS to download the dev image for the board, from which
887 the devserver would generate a payload to serve.
888
889 3. Give a path for devserver to interpret
890 update_engine_client --omaha_url=http://myhost/update/some/random/path
891 This would attempt, in order to:
892 a) Generate an update from a test image binary if found in
893 static_dir/some/random/path.
894 b) Serve an update payload found in static_dir/some/random/path.
895 c) Hope that some/random/path takes the form "board/version" and
896 and attempt to download an update payload for that board/version
897 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700898 """
joychen121fc9b2013-08-02 14:30:30 -0700899 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -0800900 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -0700901 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-11 19:49:01 -0700902
joychen121fc9b2013-08-02 14:30:30 -0700903 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700904
Dan Shif5ce2de2013-04-25 16:06:32 -0700905 @cherrypy.expose
906 def check_health(self):
907 """Collect the health status of devserver to see if it's ready for staging.
908
Gilad Arnold452fd272014-02-04 11:09:28 -0800909 Returns:
910 A JSON dictionary containing all or some of the following fields:
911 free_disk (int): free disk space in GB
912 staging_thread_count (int): number of devserver threads currently staging
913 an image
Dan Shif5ce2de2013-04-25 16:06:32 -0700914 """
915 # Get free disk space.
916 stat = os.statvfs(updater.static_dir)
917 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
918
919 return json.dumps({
920 'free_disk': free_disk,
Dan Shi59ae7092013-06-04 14:37:27 -0700921 'staging_thread_count': DevServerRoot._staging_thread_count,
Dan Shif5ce2de2013-04-25 16:06:32 -0700922 })
923
924
Chris Sosadbc20082012-12-10 13:39:11 -0800925def _CleanCache(cache_dir, wipe):
926 """Wipes any excess cached items in the cache_dir.
927
928 Args:
929 cache_dir: the directory we are wiping from.
930 wipe: If True, wipe all the contents -- not just the excess.
931 """
932 if wipe:
933 # Clear the cache and exit on error.
934 cmd = 'rm -rf %s/*' % cache_dir
935 if os.system(cmd) != 0:
936 _Log('Failed to clear the cache with %s' % cmd)
937 sys.exit(1)
938 else:
939 # Clear all but the last N cached updates
940 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
941 (cache_dir, CACHED_ENTRIES))
942 if os.system(cmd) != 0:
943 _Log('Failed to clean up old delta cache files with %s' % cmd)
944 sys.exit(1)
945
946
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700947def _AddTestingOptions(parser):
948 group = optparse.OptionGroup(
949 parser, 'Advanced Testing Options', 'These are used by test scripts and '
950 'developers writing integration tests utilizing the devserver. They are '
951 'not intended to be really used outside the scope of someone '
952 'knowledgable about the test.')
953 group.add_option('--exit',
954 action='store_true',
955 help='do not start the server (yet pregenerate/clear cache)')
956 group.add_option('--host_log',
957 action='store_true', default=False,
958 help='record history of host update events (/api/hostlog)')
959 group.add_option('--max_updates',
960 metavar='NUM', default= -1, type='int',
961 help='maximum number of update checks handled positively '
962 '(default: unlimited)')
963 group.add_option('--private_key',
964 metavar='PATH', default=None,
965 help='path to the private key in pem format. If this is set '
966 'the devserver will generate update payloads that are '
967 'signed with this key.')
David Zeuthen52ccd012013-10-31 12:58:26 -0700968 group.add_option('--private_key_for_metadata_hash_signature',
969 metavar='PATH', default=None,
970 help='path to the private key in pem format. If this is set '
971 'the devserver will sign the metadata hash with the given '
972 'key and transmit in the Omaha-style XML response.')
973 group.add_option('--public_key',
974 metavar='PATH', default=None,
975 help='path to the public key in pem format. If this is set '
976 'the devserver will transmit a base64 encoded version of '
977 'the content in the Omaha-style XML response.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700978 group.add_option('--proxy_port',
979 metavar='PORT', default=None, type='int',
980 help='port to have the client connect to -- basically the '
981 'devserver lies to the update to tell it to get the payload '
982 'from a different port that will proxy the request back to '
983 'the devserver. The proxy must be managed outside the '
984 'devserver.')
985 group.add_option('--remote_payload',
986 action='store_true', default=False,
987 help='Payload is being served from a remote machine')
988 group.add_option('-u', '--urlbase',
989 metavar='URL',
990 help='base URL for update images, other than the '
991 'devserver. Use in conjunction with remote_payload.')
992 parser.add_option_group(group)
993
994
995def _AddUpdateOptions(parser):
996 group = optparse.OptionGroup(
997 parser, 'Autoupdate Options', 'These options can be used to change '
998 'how the devserver either generates or serve update payloads. Please '
999 'note that all of these option affect how a payload is generated and so '
1000 'do not work in archive-only mode.')
1001 group.add_option('--board',
1002 help='By default the devserver will create an update '
1003 'payload from the latest image built for the board '
1004 'a device that is requesting an update has. When we '
1005 'pre-generate an update (see below) and we do not specify '
1006 'another update_type option like image or payload, the '
1007 'devserver needs to know the board to generate the latest '
1008 'image for. This is that board.')
1009 group.add_option('--critical_update',
1010 action='store_true', default=False,
1011 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001012 group.add_option('--image',
1013 metavar='FILE',
1014 help='Generate and serve an update using this image to any '
1015 'device that requests an update.')
1016 group.add_option('--no_patch_kernel',
1017 dest='patch_kernel', action='store_false', default=True,
1018 help='When generating an update payload, do not patch the '
1019 'kernel with kernel verification blob from the stateful '
1020 'partition.')
1021 group.add_option('--payload',
1022 metavar='PATH',
1023 help='use the update payload from specified directory '
1024 '(update.gz).')
1025 group.add_option('-p', '--pregenerate_update',
1026 action='store_true', default=False,
1027 help='pre-generate the update payload before accepting '
1028 'update requests. Useful to help debug payload generation '
1029 'issues quickly. Also if an update payload will take a '
1030 'long time to generate, a client may timeout if you do not'
1031 'pregenerate the update.')
1032 group.add_option('--src_image',
1033 metavar='PATH', default='',
1034 help='If specified, delta updates will be generated using '
1035 'this image as the source image. Delta updates are when '
1036 'you are updating from a "source image" to a another '
1037 'image.')
1038 parser.add_option_group(group)
1039
1040
1041def _AddProductionOptions(parser):
1042 group = optparse.OptionGroup(
1043 parser, 'Advanced Server Options', 'These options can be used to changed '
1044 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001045 group.add_option('--clear_cache',
1046 action='store_true', default=False,
1047 help='At startup, removes all cached entries from the'
1048 'devserver\'s cache.')
1049 group.add_option('--logfile',
1050 metavar='PATH',
1051 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -07001052 group.add_option('--pidfile',
1053 metavar='PATH',
1054 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 11:04:13 -08001055 group.add_option('--portfile',
1056 metavar='PATH',
1057 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001058 group.add_option('--production',
1059 action='store_true', default=False,
1060 help='have the devserver use production values when '
1061 'starting up. This includes using more threads and '
1062 'performing less logging.')
1063 parser.add_option_group(group)
1064
1065
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001066def _MakeLogHandler(logfile):
1067 """Create a LogHandler instance used to log all messages."""
1068 hdlr_cls = handlers.TimedRotatingFileHandler
1069 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
1070 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -07001071 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001072 return hdlr
1073
1074
Chris Sosacde6bf42012-05-31 18:36:39 -07001075def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001076 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -08001077 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -07001078
1079 # get directory that the devserver is run from
1080 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -07001081 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -07001082 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001083 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -07001084 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -07001085 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001086 parser.add_option('--port',
1087 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 13:13:28 -08001088 help=('port for the dev server to use; if zero, binds to '
1089 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001090 parser.add_option('-t', '--test_image',
1091 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -07001092 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -07001093 parser.add_option('-x', '--xbuddy_manage_builds',
1094 action='store_true',
1095 default=False,
1096 help='If set, allow xbuddy to manage images in'
1097 'build/images.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001098 _AddProductionOptions(parser)
1099 _AddUpdateOptions(parser)
1100 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -07001101 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001102
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001103 # Handle options that must be set globally in cherrypy. Do this
1104 # work up front, because calls to _Log() below depend on this
1105 # initialization.
1106 if options.production:
1107 cherrypy.config.update({'environment': 'production'})
1108 if not options.logfile:
1109 cherrypy.config.update({'log.screen': True})
1110 else:
1111 cherrypy.config.update({'log.error_file': '',
1112 'log.access_file': ''})
1113 hdlr = _MakeLogHandler(options.logfile)
1114 # Pylint can't seem to process these two calls properly
1115 # pylint: disable=E1101
1116 cherrypy.log.access_log.addHandler(hdlr)
1117 cherrypy.log.error_log.addHandler(hdlr)
1118 # pylint: enable=E1101
1119
joychened64b222013-06-21 16:39:34 -07001120 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001121 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001122
joychened64b222013-06-21 16:39:34 -07001123 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001124 # If our devserver is only supposed to serve payloads, we shouldn't be
1125 # mucking with the cache at all. If the devserver hadn't previously
1126 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001127 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001128 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001129 else:
1130 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001131
Chris Sosadbc20082012-12-10 13:39:11 -08001132 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 16:39:34 -07001133 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001134
joychen121fc9b2013-08-02 14:30:30 -07001135 _xbuddy = xbuddy.XBuddy(options.xbuddy_manage_builds,
1136 options.board,
joychen121fc9b2013-08-02 14:30:30 -07001137 static_dir=options.static_dir)
Chris Sosa75490802013-09-30 17:21:45 -07001138 if options.clear_cache and options.xbuddy_manage_builds:
1139 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 14:30:30 -07001140
Chris Sosa6a3697f2013-01-29 16:44:43 -08001141 # We allow global use here to share with cherrypy classes.
1142 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001143 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001144 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001145 _xbuddy,
joychened64b222013-06-21 16:39:34 -07001146 static_dir=options.static_dir,
Andrew de los Reyes52620802010-04-12 13:40:07 -07001147 urlbase=options.urlbase,
Chris Sosa5d342a22010-09-28 16:54:41 -07001148 forced_image=options.image,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001149 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001150 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-28 23:42:37 -07001151 src_image=options.src_image,
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001152 patch_kernel=options.patch_kernel,
Chris Sosa08d55a22011-01-19 16:08:02 -08001153 board=options.board,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001154 copy_to_static_root=not options.exit,
1155 private_key=options.private_key,
David Zeuthen52ccd012013-10-31 12:58:26 -07001156 private_key_for_metadata_hash_signature=
1157 options.private_key_for_metadata_hash_signature,
1158 public_key=options.public_key,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001159 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001160 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001161 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001162 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001163 )
Chris Sosa7c931362010-10-11 19:49:01 -07001164
Chris Sosa6a3697f2013-01-29 16:44:43 -08001165 if options.pregenerate_update:
1166 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 15:46:22 -07001167
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001168 if options.exit:
1169 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001170
joychen3cb228e2013-06-12 12:13:13 -07001171 dev_server = DevServerRoot(_xbuddy)
1172
Gilad Arnold11fbef42014-02-10 11:04:13 -08001173 # Patch CherryPy to support binding to any available port (--port=0).
1174 cherrypy_ext.ZeroPortPatcher.DoPatch(cherrypy)
1175
Chris Sosa855b8932013-08-21 13:24:55 -07001176 if options.pidfile:
1177 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1178
Gilad Arnold11fbef42014-02-10 11:04:13 -08001179 if options.portfile:
1180 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1181
joychen3cb228e2013-06-12 12:13:13 -07001182 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001183
1184
1185if __name__ == '__main__':
1186 main()