blob: 96f8b8751f3e806a52dc76a44caefba39f6648ed [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
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700708 def xbuddy_translate(self, *args):
709 """Translates an xBuddy path to a real path to artifact if it exists.
710
711 Args:
712 An xbuddy path in the form of {local|remote}/build_id/artifact.
713
714 Returns:
715 build_id/artifact
716 """
717 build_id, filename = self._xbuddy.Translate(args)
718 response = os.path.join(build_id, filename)
719 _Log('Path translation requested, returning: %s', response)
720 return response
721
722 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -0700723 def xbuddy(self, *args, **kwargs):
724 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -0700725
726 Args:
joycheneaf4cfc2013-07-02 08:38:57 -0700727 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -0700728 components of the path. The path can be understood as
729 "{local|remote}/build_id/artifact" where build_id is composed of
730 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -0700731
joychen121fc9b2013-08-02 14:30:30 -0700732 The first path element is optional, and can be "remote" or "local"
733 If local (the default), devserver will not attempt to access Google
734 Storage, and will only search the static directory for the files.
735 If remote, devserver will try to obtain the artifact off GS if it's
736 not found locally.
737 The board is the familiar board name, optionally suffixed.
738 The version can be the google storage version number, and may also be
739 any of a number of xBuddy defined version aliases that will be
740 translated into the latest built image that fits the description.
741 Defaults to latest.
742 The artifact is one of a number of image or artifact aliases used by
743 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -0700744
745 Kwargs:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800746 for_update: {true|false}
747 if true, pregenerates the update payloads for the image,
748 and returns the update uri to pass to the
749 update_engine_client.
joychen3cb228e2013-06-12 12:13:13 -0700750 return_dir: {true|false}
751 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800752 relative_path: {true|false}
753 if set to true, returns the relative path to the payload
754 directory from static_dir.
joychen3cb228e2013-06-12 12:13:13 -0700755 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -0700756 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -0700757 or
joycheneaf4cfc2013-07-02 08:38:57 -0700758 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -0700759
760 Returns:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800761 If |for_update|, returns a redirect to the image or update file
762 on the devserver. E.g.,
763 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
764 chromium-test-image.bin
765 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
766 http://host:port/static/x86-generic-release/R26-4000.0.0/
767 If |relative_path| is true, return a relative path the folder where the
768 payloads are. E.g.,
769 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 12:13:13 -0700770 """
Chris Sosa75490802013-09-30 17:21:45 -0700771 boolean_string = kwargs.get('for_update')
772 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800773 boolean_string = kwargs.get('return_dir')
774 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
775 boolean_string = kwargs.get('relative_path')
776 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -0700777
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800778 if return_dir and relative_path:
779 raise DevServerHTTPError(500, 'Cannot specify both return_dir and '
780 'relative_path')
Chris Sosa75490802013-09-30 17:21:45 -0700781
782 # For updates, we optimize downloading of test images.
783 file_name = None
784 build_id = None
785 if for_update:
786 try:
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700787 build_id = self._xbuddy.StageTestArtifactsForUpdate(args)
Chris Sosa75490802013-09-30 17:21:45 -0700788 except build_artifact.ArtifactDownloadError:
789 build_id = None
790
791 if not build_id:
792 build_id, file_name = self._xbuddy.Get(args)
793
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800794 if for_update:
795 _Log('Payload generation triggered by request')
796 # Forces payload to be in cache and symlinked into build_id dir.
Chris Sosa75490802013-09-30 17:21:45 -0700797 updater.GetUpdateForLabel(autoupdate.FORCED_UPDATE, build_id,
798 image_name=file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800799
800 response = None
801 if return_dir:
802 response = os.path.join(cherrypy.request.base, 'static', build_id)
803 _Log('Directory requested, returning: %s', response)
804 elif relative_path:
805 response = build_id
806 _Log('Relative path requested, returning: %s', response)
807 elif for_update:
808 response = os.path.join(cherrypy.request.base, 'update', build_id)
809 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 12:13:13 -0700810 else:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800811 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 14:30:30 -0700812 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800813 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 14:30:30 -0700814 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -0700815
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800816 return response
817
joychen3cb228e2013-06-12 12:13:13 -0700818 @cherrypy.expose
819 def xbuddy_list(self):
820 """Lists the currently available images & time since last access.
821
Gilad Arnold452fd272014-02-04 11:09:28 -0800822 Returns:
823 A string representation of a list of tuples [(build_id, time since last
824 access),...]
joychen3cb228e2013-06-12 12:13:13 -0700825 """
826 return self._xbuddy.List()
827
828 @cherrypy.expose
829 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 11:09:28 -0800830 """Returns the number of images cached by xBuddy."""
joychen3cb228e2013-06-12 12:13:13 -0700831 return self._xbuddy.Capacity()
832
833 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700834 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700835 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700836 return ('Welcome to the Dev Server!<br>\n'
837 '<br>\n'
838 'Here are the available methods, click for documentation:<br>\n'
839 '<br>\n'
840 '%s' %
841 '<br>\n'.join(
842 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700843 for name in _FindExposedMethods(
844 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700845
846 @cherrypy.expose
847 def doc(self, *args):
848 """Shows the documentation for available methods / URLs.
849
850 Example:
851 http://myhost/doc/update
852 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700853 name = '/'.join(args)
854 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700855 if not method:
856 raise DevServerError("No exposed method named `%s'" % name)
857 if not method.__doc__:
858 raise DevServerError("No documentation for exposed method `%s'" % name)
859 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -0700860
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700861 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700862 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700863 """Handles an update check from a Chrome OS client.
864
865 The HTTP request should contain the standard Omaha-style XML blob. The URL
866 line may contain an additional intermediate path to the update payload.
867
joychen121fc9b2013-08-02 14:30:30 -0700868 This request can be handled in one of 4 ways, depending on the devsever
869 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -0700870
joychen121fc9b2013-08-02 14:30:30 -0700871 1. No intermediate path
872 If no intermediate path is given, the default behavior is to generate an
873 update payload from the latest test image locally built for the board
874 specified in the xml. Devserver serves the generated payload.
875
876 2. Path explicitly invokes XBuddy
877 If there is a path given, it can explicitly invoke xbuddy by prefixing it
878 with 'xbuddy'. This path is then used to acquire an image binary for the
879 devserver to generate an update payload from. Devserver then serves this
880 payload.
881
882 3. Path is left for the devserver to interpret.
883 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
884 to generate a payload from the test image in that directory and serve it.
885
886 4. The devserver is in a 'forced' mode. TO BE DEPRECATED
887 This comes from the usage of --forced_payload or --image when starting the
888 devserver. No matter what path (or no path) gets passed in, devserver will
889 serve the update payload (--forced_payload) or generate an update payload
890 from the image (--image).
891
892 Examples:
893 1. No intermediate path
894 update_engine_client --omaha_url=http://myhost/update
895 This generates an update payload from the latest test image locally built
896 for the board specified in the xml.
897
898 2. Explicitly invoke xbuddy
899 update_engine_client --omaha_url=
900 http://myhost/update/xbuddy/remote/board/version/dev
901 This would go to GS to download the dev image for the board, from which
902 the devserver would generate a payload to serve.
903
904 3. Give a path for devserver to interpret
905 update_engine_client --omaha_url=http://myhost/update/some/random/path
906 This would attempt, in order to:
907 a) Generate an update from a test image binary if found in
908 static_dir/some/random/path.
909 b) Serve an update payload found in static_dir/some/random/path.
910 c) Hope that some/random/path takes the form "board/version" and
911 and attempt to download an update payload for that board/version
912 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700913 """
joychen121fc9b2013-08-02 14:30:30 -0700914 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -0800915 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -0700916 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-11 19:49:01 -0700917
joychen121fc9b2013-08-02 14:30:30 -0700918 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700919
Dan Shif5ce2de2013-04-25 16:06:32 -0700920 @cherrypy.expose
921 def check_health(self):
922 """Collect the health status of devserver to see if it's ready for staging.
923
Gilad Arnold452fd272014-02-04 11:09:28 -0800924 Returns:
925 A JSON dictionary containing all or some of the following fields:
926 free_disk (int): free disk space in GB
927 staging_thread_count (int): number of devserver threads currently staging
928 an image
Dan Shif5ce2de2013-04-25 16:06:32 -0700929 """
930 # Get free disk space.
931 stat = os.statvfs(updater.static_dir)
932 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
933
934 return json.dumps({
935 'free_disk': free_disk,
Dan Shi59ae7092013-06-04 14:37:27 -0700936 'staging_thread_count': DevServerRoot._staging_thread_count,
Dan Shif5ce2de2013-04-25 16:06:32 -0700937 })
938
939
Chris Sosadbc20082012-12-10 13:39:11 -0800940def _CleanCache(cache_dir, wipe):
941 """Wipes any excess cached items in the cache_dir.
942
943 Args:
944 cache_dir: the directory we are wiping from.
945 wipe: If True, wipe all the contents -- not just the excess.
946 """
947 if wipe:
948 # Clear the cache and exit on error.
949 cmd = 'rm -rf %s/*' % cache_dir
950 if os.system(cmd) != 0:
951 _Log('Failed to clear the cache with %s' % cmd)
952 sys.exit(1)
953 else:
954 # Clear all but the last N cached updates
955 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
956 (cache_dir, CACHED_ENTRIES))
957 if os.system(cmd) != 0:
958 _Log('Failed to clean up old delta cache files with %s' % cmd)
959 sys.exit(1)
960
961
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700962def _AddTestingOptions(parser):
963 group = optparse.OptionGroup(
964 parser, 'Advanced Testing Options', 'These are used by test scripts and '
965 'developers writing integration tests utilizing the devserver. They are '
966 'not intended to be really used outside the scope of someone '
967 'knowledgable about the test.')
968 group.add_option('--exit',
969 action='store_true',
970 help='do not start the server (yet pregenerate/clear cache)')
971 group.add_option('--host_log',
972 action='store_true', default=False,
973 help='record history of host update events (/api/hostlog)')
974 group.add_option('--max_updates',
975 metavar='NUM', default= -1, type='int',
976 help='maximum number of update checks handled positively '
977 '(default: unlimited)')
978 group.add_option('--private_key',
979 metavar='PATH', default=None,
980 help='path to the private key in pem format. If this is set '
981 'the devserver will generate update payloads that are '
982 'signed with this key.')
David Zeuthen52ccd012013-10-31 12:58:26 -0700983 group.add_option('--private_key_for_metadata_hash_signature',
984 metavar='PATH', default=None,
985 help='path to the private key in pem format. If this is set '
986 'the devserver will sign the metadata hash with the given '
987 'key and transmit in the Omaha-style XML response.')
988 group.add_option('--public_key',
989 metavar='PATH', default=None,
990 help='path to the public key in pem format. If this is set '
991 'the devserver will transmit a base64 encoded version of '
992 'the content in the Omaha-style XML response.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700993 group.add_option('--proxy_port',
994 metavar='PORT', default=None, type='int',
995 help='port to have the client connect to -- basically the '
996 'devserver lies to the update to tell it to get the payload '
997 'from a different port that will proxy the request back to '
998 'the devserver. The proxy must be managed outside the '
999 'devserver.')
1000 group.add_option('--remote_payload',
1001 action='store_true', default=False,
1002 help='Payload is being served from a remote machine')
1003 group.add_option('-u', '--urlbase',
1004 metavar='URL',
1005 help='base URL for update images, other than the '
1006 'devserver. Use in conjunction with remote_payload.')
1007 parser.add_option_group(group)
1008
1009
1010def _AddUpdateOptions(parser):
1011 group = optparse.OptionGroup(
1012 parser, 'Autoupdate Options', 'These options can be used to change '
1013 'how the devserver either generates or serve update payloads. Please '
1014 'note that all of these option affect how a payload is generated and so '
1015 'do not work in archive-only mode.')
1016 group.add_option('--board',
1017 help='By default the devserver will create an update '
1018 'payload from the latest image built for the board '
1019 'a device that is requesting an update has. When we '
1020 'pre-generate an update (see below) and we do not specify '
1021 'another update_type option like image or payload, the '
1022 'devserver needs to know the board to generate the latest '
1023 'image for. This is that board.')
1024 group.add_option('--critical_update',
1025 action='store_true', default=False,
1026 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001027 group.add_option('--image',
1028 metavar='FILE',
1029 help='Generate and serve an update using this image to any '
1030 'device that requests an update.')
1031 group.add_option('--no_patch_kernel',
1032 dest='patch_kernel', action='store_false', default=True,
1033 help='When generating an update payload, do not patch the '
1034 'kernel with kernel verification blob from the stateful '
1035 'partition.')
1036 group.add_option('--payload',
1037 metavar='PATH',
1038 help='use the update payload from specified directory '
1039 '(update.gz).')
1040 group.add_option('-p', '--pregenerate_update',
1041 action='store_true', default=False,
1042 help='pre-generate the update payload before accepting '
1043 'update requests. Useful to help debug payload generation '
1044 'issues quickly. Also if an update payload will take a '
1045 'long time to generate, a client may timeout if you do not'
1046 'pregenerate the update.')
1047 group.add_option('--src_image',
1048 metavar='PATH', default='',
1049 help='If specified, delta updates will be generated using '
1050 'this image as the source image. Delta updates are when '
1051 'you are updating from a "source image" to a another '
1052 'image.')
1053 parser.add_option_group(group)
1054
1055
1056def _AddProductionOptions(parser):
1057 group = optparse.OptionGroup(
1058 parser, 'Advanced Server Options', 'These options can be used to changed '
1059 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001060 group.add_option('--clear_cache',
1061 action='store_true', default=False,
1062 help='At startup, removes all cached entries from the'
1063 'devserver\'s cache.')
1064 group.add_option('--logfile',
1065 metavar='PATH',
1066 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -07001067 group.add_option('--pidfile',
1068 metavar='PATH',
1069 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 11:04:13 -08001070 group.add_option('--portfile',
1071 metavar='PATH',
1072 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001073 group.add_option('--production',
1074 action='store_true', default=False,
1075 help='have the devserver use production values when '
1076 'starting up. This includes using more threads and '
1077 'performing less logging.')
1078 parser.add_option_group(group)
1079
1080
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001081def _MakeLogHandler(logfile):
1082 """Create a LogHandler instance used to log all messages."""
1083 hdlr_cls = handlers.TimedRotatingFileHandler
1084 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
1085 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -07001086 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001087 return hdlr
1088
1089
Chris Sosacde6bf42012-05-31 18:36:39 -07001090def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001091 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -08001092 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -07001093
1094 # get directory that the devserver is run from
1095 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -07001096 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -07001097 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001098 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -07001099 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -07001100 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001101 parser.add_option('--port',
1102 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 13:13:28 -08001103 help=('port for the dev server to use; if zero, binds to '
1104 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001105 parser.add_option('-t', '--test_image',
1106 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -07001107 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -07001108 parser.add_option('-x', '--xbuddy_manage_builds',
1109 action='store_true',
1110 default=False,
1111 help='If set, allow xbuddy to manage images in'
1112 'build/images.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001113 _AddProductionOptions(parser)
1114 _AddUpdateOptions(parser)
1115 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -07001116 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001117
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001118 # Handle options that must be set globally in cherrypy. Do this
1119 # work up front, because calls to _Log() below depend on this
1120 # initialization.
1121 if options.production:
1122 cherrypy.config.update({'environment': 'production'})
1123 if not options.logfile:
1124 cherrypy.config.update({'log.screen': True})
1125 else:
1126 cherrypy.config.update({'log.error_file': '',
1127 'log.access_file': ''})
1128 hdlr = _MakeLogHandler(options.logfile)
1129 # Pylint can't seem to process these two calls properly
1130 # pylint: disable=E1101
1131 cherrypy.log.access_log.addHandler(hdlr)
1132 cherrypy.log.error_log.addHandler(hdlr)
1133 # pylint: enable=E1101
1134
joychened64b222013-06-21 16:39:34 -07001135 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001136 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001137
joychened64b222013-06-21 16:39:34 -07001138 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001139 # If our devserver is only supposed to serve payloads, we shouldn't be
1140 # mucking with the cache at all. If the devserver hadn't previously
1141 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001142 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001143 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001144 else:
1145 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001146
Chris Sosadbc20082012-12-10 13:39:11 -08001147 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 16:39:34 -07001148 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001149
joychen121fc9b2013-08-02 14:30:30 -07001150 _xbuddy = xbuddy.XBuddy(options.xbuddy_manage_builds,
1151 options.board,
joychen121fc9b2013-08-02 14:30:30 -07001152 static_dir=options.static_dir)
Chris Sosa75490802013-09-30 17:21:45 -07001153 if options.clear_cache and options.xbuddy_manage_builds:
1154 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 14:30:30 -07001155
Chris Sosa6a3697f2013-01-29 16:44:43 -08001156 # We allow global use here to share with cherrypy classes.
1157 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001158 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001159 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001160 _xbuddy,
joychened64b222013-06-21 16:39:34 -07001161 static_dir=options.static_dir,
Andrew de los Reyes52620802010-04-12 13:40:07 -07001162 urlbase=options.urlbase,
Chris Sosa5d342a22010-09-28 16:54:41 -07001163 forced_image=options.image,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001164 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001165 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-28 23:42:37 -07001166 src_image=options.src_image,
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001167 patch_kernel=options.patch_kernel,
Chris Sosa08d55a22011-01-19 16:08:02 -08001168 board=options.board,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001169 copy_to_static_root=not options.exit,
1170 private_key=options.private_key,
David Zeuthen52ccd012013-10-31 12:58:26 -07001171 private_key_for_metadata_hash_signature=
1172 options.private_key_for_metadata_hash_signature,
1173 public_key=options.public_key,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001174 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001175 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001176 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001177 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001178 )
Chris Sosa7c931362010-10-11 19:49:01 -07001179
Chris Sosa6a3697f2013-01-29 16:44:43 -08001180 if options.pregenerate_update:
1181 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 15:46:22 -07001182
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001183 if options.exit:
1184 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001185
joychen3cb228e2013-06-12 12:13:13 -07001186 dev_server = DevServerRoot(_xbuddy)
1187
Gilad Arnold11fbef42014-02-10 11:04:13 -08001188 # Patch CherryPy to support binding to any available port (--port=0).
1189 cherrypy_ext.ZeroPortPatcher.DoPatch(cherrypy)
1190
Chris Sosa855b8932013-08-21 13:24:55 -07001191 if options.pidfile:
1192 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1193
Gilad Arnold11fbef42014-02-10 11:04:13 -08001194 if options.portfile:
1195 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1196
joychen3cb228e2013-06-12 12:13:13 -07001197 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001198
1199
1200if __name__ == '__main__':
1201 main()