blob: 75fc286666e0aac00c2436d0a9791b393c7b5b9b [file] [log] [blame]
Chris Sosa7c931362010-10-11 19:49:01 -07001#!/usr/bin/python
2
Chris Sosa781ba6d2012-04-11 12:44:43 -07003# Copyright (c) 2009-2012 The Chromium OS Authors. All rights reserved.
rtc@google.comded22402009-10-26 22:36:21 +00004# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
Chris Sosa3ae4dc12013-03-29 11:47:00 -07007"""Chromium OS development server that can be used for all forms of update.
8
9This devserver can be used to perform system-wide autoupdate and update
10of specific portage packages on devices running Chromium OS derived operating
11systems. It mainly operates in two modes:
12
131) archive mode: In this mode, the devserver is configured to stage and
14serve artifacts from Google Storage using the credentials provided to it before
15it is run. The easiest way to understand this is that the devserver is
16functioning as a local cache for artifacts produced and uploaded by build
17servers. Users of this form of devserver can either download the artifacts
18from the devservers static directory OR use the update RPC to perform a
19system-wide autoupdate. Archive mode is always active.
20
212) artifact-generation mode: in this mode, the devserver will attempt to
22generate update payloads and build artifacts when requested. This mode only
23works in the Chromium OS chroot as it uses build tools only present in the
24chroot (emerge, cros_generate_update_payload, etc.). By default, when a device
25requests an update from this form of devserver, the devserver will attempt to
26discover if a more recent build of the board has been built by the developer
27and generate a payload that the requested system can autoupdate to. In addition,
28it accepts gmerge requests from devices that will stage the newest version of
joychen84d13772013-08-06 09:17:23 -070029a particular package from a developer's chroot onto a requesting device.
Chris Sosa3ae4dc12013-03-29 11:47:00 -070030
31For example:
32gmerge gmerge -d <devserver_url>
33
34devserver will see if a newer package of gmerge is available. If gmerge is
35cros_work'd on, it will re-build gmerge. After this, gmerge will install that
36version of gmerge that the devserver just created/found.
37
38For autoupdates, there are many more advanced options that can help specify
39how to update and which payload to give to a requester.
40"""
41
Chris Sosa7c931362010-10-11 19:49:01 -070042
Gilad Arnold55a2a372012-10-02 09:46:32 -070043import json
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070044import optparse
rtc@google.comded22402009-10-26 22:36:21 +000045import os
Scott Zawalski4647ce62012-01-03 17:17:28 -050046import re
Simran Basi4baad082013-02-14 13:39:18 -080047import shutil
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -080048import socket
Chris Masone816e38c2012-05-02 12:22:36 -070049import subprocess
J. Richard Barnette3d977b82013-04-23 11:05:19 -070050import sys
Chris Masone816e38c2012-05-02 12:22:36 -070051import tempfile
Dan Shi59ae7092013-06-04 14:37:27 -070052import threading
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -070053import types
J. Richard Barnette3d977b82013-04-23 11:05:19 -070054from logging import handlers
55
56import cherrypy
Chris Sosa855b8932013-08-21 13:24:55 -070057from cherrypy import _cplogging as cplogging
58from cherrypy.process import plugins
rtc@google.comded22402009-10-26 22:36:21 +000059
Chris Sosa0356d3b2010-09-16 15:46:22 -070060import autoupdate
Chris Sosa75490802013-09-30 17:21:45 -070061import build_artifact
Gilad Arnoldc65330c2012-09-20 15:17:48 -070062import common_util
Chris Sosa47a7d4e2012-03-28 11:26:55 -070063import downloader
Chris Sosa7cd23202013-10-15 17:22:57 -070064import gsutil_util
Gilad Arnoldc65330c2012-09-20 15:17:48 -070065import log_util
joychen3cb228e2013-06-12 12:13:13 -070066import xbuddy
Gilad Arnoldc65330c2012-09-20 15:17:48 -070067
Gilad Arnoldc65330c2012-09-20 15:17:48 -070068# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080069def _Log(message, *args):
70 return log_util.LogWithTag('DEVSERVER', message, *args)
Chris Sosa0356d3b2010-09-16 15:46:22 -070071
Frank Farzan40160872011-12-12 18:39:18 -080072
Chris Sosa417e55d2011-01-25 16:40:48 -080073CACHED_ENTRIES = 12
Don Garrettf90edf02010-11-16 17:36:14 -080074
Simran Basi4baad082013-02-14 13:39:18 -080075TELEMETRY_FOLDER = 'telemetry_src'
76TELEMETRY_DEPS = ['dep-telemetry_dep.tar.bz2',
77 'dep-page_cycler_dep.tar.bz2',
Simran Basi0d078682013-03-22 16:40:04 -070078 'dep-chrome_test.tar.bz2',
79 'dep-perf_data_dep.tar.bz2']
Simran Basi4baad082013-02-14 13:39:18 -080080
Chris Sosa0356d3b2010-09-16 15:46:22 -070081# Sets up global to share between classes.
rtc@google.com21a5ca32009-11-04 18:23:23 +000082updater = None
rtc@google.comded22402009-10-26 22:36:21 +000083
J. Richard Barnette3d977b82013-04-23 11:05:19 -070084# Log rotation parameters. These settings correspond to once a week
J. Richard Barnette6dfa5342013-06-04 11:48:56 -070085# at midnight between Friday and Saturday, with about three months
86# of old logs kept for backup.
J. Richard Barnette3d977b82013-04-23 11:05:19 -070087#
88# For more, see the documentation for
89# logging.handlers.TimedRotatingFileHandler
J. Richard Barnette6dfa5342013-06-04 11:48:56 -070090_LOG_ROTATION_TIME = 'W4'
J. Richard Barnette3d977b82013-04-23 11:05:19 -070091_LOG_ROTATION_BACKUP = 13
92
Frank Farzan40160872011-12-12 18:39:18 -080093
Chris Sosa9164ca32012-03-28 11:04:50 -070094class DevServerError(Exception):
Chris Sosa47a7d4e2012-03-28 11:26:55 -070095 """Exception class used by this module."""
Chris Sosa47a7d4e2012-03-28 11:26:55 -070096
97
Don Garrett8ccab732013-08-30 09:13:59 -070098class DevServerHTTPError(cherrypy.HTTPError):
beepsd76c6092013-08-28 22:23:30 -070099 """Exception class to log the HTTPResponse before routing it to cherrypy."""
100 def __init__(self, status, message):
Don Garrettf84631a2014-01-07 18:21:26 -0800101 """CherryPy error with logging.
102
103 Args:
104 status: HTTPResponse status.
105 message: Message associated with the response.
beepsd76c6092013-08-28 22:23:30 -0700106 """
Don Garrett8ccab732013-08-30 09:13:59 -0700107 cherrypy.HTTPError.__init__(self, status, message)
beepsd76c6092013-08-28 22:23:30 -0700108 _Log('HTTPError status: %s message: %s', status, message)
beepsd76c6092013-08-28 22:23:30 -0700109
110
Scott Zawalski4647ce62012-01-03 17:17:28 -0500111def _LeadingWhiteSpaceCount(string):
112 """Count the amount of leading whitespace in a string.
113
114 Args:
115 string: The string to count leading whitespace in.
Don Garrettf84631a2014-01-07 18:21:26 -0800116
Scott Zawalski4647ce62012-01-03 17:17:28 -0500117 Returns:
118 number of white space chars before characters start.
119 """
120 matched = re.match('^\s+', string)
121 if matched:
122 return len(matched.group())
123
124 return 0
125
126
127def _PrintDocStringAsHTML(func):
128 """Make a functions docstring somewhat HTML style.
129
130 Args:
131 func: The function to return the docstring from.
Don Garrettf84631a2014-01-07 18:21:26 -0800132
Scott Zawalski4647ce62012-01-03 17:17:28 -0500133 Returns:
134 A string that is somewhat formated for a web browser.
135 """
136 # TODO(scottz): Make this parse Args/Returns in a prettier way.
137 # Arguments could be bolded and indented etc.
138 html_doc = []
139 for line in func.__doc__.splitlines():
140 leading_space = _LeadingWhiteSpaceCount(line)
141 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700142 line = '&nbsp;' * leading_space + line
Scott Zawalski4647ce62012-01-03 17:17:28 -0500143
144 html_doc.append('<BR>%s' % line)
145
146 return '\n'.join(html_doc)
147
148
Chris Sosa7c931362010-10-11 19:49:01 -0700149def _GetConfig(options):
150 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800151
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800152 socket_host = '::'
Yu-Ju Hongc8d4af32013-11-12 15:14:26 -0800153 # Fall back to IPv4 when python is not configured with IPv6.
154 if not socket.has_ipv6:
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800155 socket_host = '0.0.0.0'
156
Chris Sosa7c931362010-10-11 19:49:01 -0700157 base_config = { 'global':
158 { 'server.log_request_headers': True,
159 'server.protocol_version': 'HTTP/1.1',
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800160 'server.socket_host': socket_host,
Chris Sosa7c931362010-10-11 19:49:01 -0700161 'server.socket_port': int(options.port),
Chris Sosa374c62d2010-10-14 09:13:54 -0700162 'response.timeout': 6000,
Chris Sosa6fe23942012-07-02 15:44:46 -0700163 'request.show_tracebacks': True,
Chris Sosa72333d12012-06-13 11:28:05 -0700164 'server.socket_timeout': 60,
joychenecc02aa2013-07-17 18:27:35 -0700165 'server.thread_pool': 2,
Chris Sosa7c931362010-10-11 19:49:01 -0700166 },
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700167 '/api':
168 {
169 # Gets rid of cherrypy parsing post file for args.
170 'request.process_request_body': False,
171 },
Chris Sosaa1ef0102010-10-21 16:22:35 -0700172 '/build':
173 {
174 'response.timeout': 100000,
175 },
Chris Sosa7c931362010-10-11 19:49:01 -0700176 '/update':
177 {
178 # Gets rid of cherrypy parsing post file for args.
179 'request.process_request_body': False,
Chris Sosaf65f4b92010-10-21 15:57:51 -0700180 'response.timeout': 10000,
Chris Sosa7c931362010-10-11 19:49:01 -0700181 },
182 # Sets up the static dir for file hosting.
183 '/static':
joychened64b222013-06-21 16:39:34 -0700184 { 'tools.staticdir.dir': options.static_dir,
Chris Sosa7c931362010-10-11 19:49:01 -0700185 'tools.staticdir.on': True,
Chris Sosaf65f4b92010-10-21 15:57:51 -0700186 'response.timeout': 10000,
Chris Sosa7c931362010-10-11 19:49:01 -0700187 },
188 }
Chris Sosa5f118ef2012-07-12 11:37:50 -0700189 if options.production:
Alex Miller93beca52013-07-30 19:25:09 -0700190 base_config['global'].update({'server.thread_pool': 150})
Chris Sosa7cd23202013-10-15 17:22:57 -0700191 # TODO(sosa): Do this more cleanly.
192 gsutil_util.GSUTIL_ATTEMPTS = 5
Scott Zawalski1c5e7cd2012-02-27 13:12:52 -0500193
Chris Sosa7c931362010-10-11 19:49:01 -0700194 return base_config
rtc@google.com64244662009-11-12 00:52:08 +0000195
Darin Petkove17164a2010-08-11 13:24:41 -0700196
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700197def _GetRecursiveMemberObject(root, member_list):
198 """Returns an object corresponding to a nested member list.
199
200 Args:
201 root: the root object to search
202 member_list: list of nested members to search
Don Garrettf84631a2014-01-07 18:21:26 -0800203
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700204 Returns:
205 An object corresponding to the member name list; None otherwise.
206 """
207 for member in member_list:
208 next_root = root.__class__.__dict__.get(member)
209 if not next_root:
210 return None
211 root = next_root
212 return root
213
214
215def _IsExposed(name):
216 """Returns True iff |name| has an `exposed' attribute and it is set."""
217 return hasattr(name, 'exposed') and name.exposed
218
219
Gilad Arnold748c8322012-10-12 09:51:35 -0700220def _GetExposedMethod(root, nested_member, ignored=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700221 """Returns a CherryPy-exposed method, if such exists.
222
223 Args:
224 root: the root object for searching
225 nested_member: a slash-joined path to the nested member
226 ignored: method paths to be ignored
Don Garrettf84631a2014-01-07 18:21:26 -0800227
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700228 Returns:
229 A function object corresponding to the path defined by |member_list| from
230 the |root| object, if the function is exposed and not ignored; None
231 otherwise.
232 """
Gilad Arnold748c8322012-10-12 09:51:35 -0700233 method = (not (ignored and nested_member in ignored) and
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700234 _GetRecursiveMemberObject(root, nested_member.split('/')))
235 if (method and type(method) == types.FunctionType and _IsExposed(method)):
236 return method
237
238
Gilad Arnold748c8322012-10-12 09:51:35 -0700239def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700240 """Finds exposed CherryPy methods.
241
242 Args:
243 root: the root object for searching
244 prefix: slash-joined chain of members leading to current object
245 unlisted: URLs to be excluded regardless of their exposed status
Don Garrettf84631a2014-01-07 18:21:26 -0800246
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700247 Returns:
248 List of exposed URLs that are not unlisted.
249 """
250 method_list = []
251 for member in sorted(root.__class__.__dict__.keys()):
252 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 09:51:35 -0700253 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700254 continue
255 member_obj = root.__class__.__dict__[member]
256 if _IsExposed(member_obj):
257 if type(member_obj) == types.FunctionType:
258 method_list.append(prefixed_member)
259 else:
260 method_list += _FindExposedMethods(
261 member_obj, prefixed_member, unlisted)
262 return method_list
263
264
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700265class ApiRoot(object):
266 """RESTful API for Dev Server information."""
267 exposed = True
268
269 @cherrypy.expose
270 def hostinfo(self, ip):
271 """Returns a JSON dictionary containing information about the given ip.
272
Gilad Arnold1b908392012-10-05 11:36:27 -0700273 Args:
274 ip: address of host whose info is requested
Don Garrettf84631a2014-01-07 18:21:26 -0800275
Gilad Arnold1b908392012-10-05 11:36:27 -0700276 Returns:
277 A JSON dictionary containing all or some of the following fields:
278 last_event_type (int): last update event type received
279 last_event_status (int): last update event status received
280 last_known_version (string): last known version reported in update ping
281 forced_update_label (string): update label to force next update ping to
282 use, set by setnextupdate
283 See the OmahaEvent class in update_engine/omaha_request_action.h for
284 event type and status code definitions. If the ip does not exist an empty
285 string is returned.
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700286
Gilad Arnold1b908392012-10-05 11:36:27 -0700287 Example URL:
288 http://myhost/api/hostinfo?ip=192.168.1.5
289 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700290 return updater.HandleHostInfoPing(ip)
291
292 @cherrypy.expose
Gilad Arnold286a0062012-01-12 13:47:02 -0800293 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 11:36:27 -0700294 """Returns a JSON object containing a log of host event.
295
296 Args:
297 ip: address of host whose event log is requested, or `all'
Don Garrettf84631a2014-01-07 18:21:26 -0800298
Gilad Arnold1b908392012-10-05 11:36:27 -0700299 Returns:
300 A JSON encoded list (log) of dictionaries (events), each of which
301 containing a `timestamp' and other event fields, as described under
302 /api/hostinfo.
303
304 Example URL:
305 http://myhost/api/hostlog?ip=192.168.1.5
306 """
Gilad Arnold286a0062012-01-12 13:47:02 -0800307 return updater.HandleHostLogPing(ip)
308
309 @cherrypy.expose
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700310 def setnextupdate(self, ip):
311 """Allows the response to the next update ping from a host to be set.
312
313 Takes the IP of the host and an update label as normally provided to the
Gilad Arnold1b908392012-10-05 11:36:27 -0700314 /update command.
315 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700316 body_length = int(cherrypy.request.headers['Content-Length'])
317 label = cherrypy.request.rfile.read(body_length)
318
319 if label:
320 label = label.strip()
321 if label:
322 return updater.HandleSetUpdatePing(ip, label)
beepsd76c6092013-08-28 22:23:30 -0700323 raise DevServerHTTPError(400, 'No label provided.')
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700324
325
Gilad Arnold55a2a372012-10-02 09:46:32 -0700326 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800327 def fileinfo(self, *args):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700328 """Returns information about a given staged file.
329
330 Args:
Don Garrettf84631a2014-01-07 18:21:26 -0800331 args: path to the file inside the server's static staging directory
332
Gilad Arnold55a2a372012-10-02 09:46:32 -0700333 Returns:
334 A JSON encoded dictionary with information about the said file, which may
335 contain the following keys/values:
Gilad Arnold1b908392012-10-05 11:36:27 -0700336 size (int): the file size in bytes
337 sha1 (string): a base64 encoded SHA1 hash
338 sha256 (string): a base64 encoded SHA256 hash
339
340 Example URL:
341 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 09:46:32 -0700342 """
Don Garrettf84631a2014-01-07 18:21:26 -0800343 file_path = os.path.join(updater.static_dir, *args)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700344 if not os.path.exists(file_path):
345 raise DevServerError('file not found: %s' % file_path)
346 try:
347 file_size = os.path.getsize(file_path)
348 file_sha1 = common_util.GetFileSha1(file_path)
349 file_sha256 = common_util.GetFileSha256(file_path)
350 except os.error, e:
351 raise DevServerError('failed to get info for file %s: %s' %
Gilad Arnolde74b3812013-04-22 11:27:38 -0700352 (file_path, e))
353
354 is_delta = autoupdate.Autoupdate.IsDeltaFormatFile(file_path)
355
356 return json.dumps({
357 autoupdate.Autoupdate.SIZE_ATTR: file_size,
358 autoupdate.Autoupdate.SHA1_ATTR: file_sha1,
359 autoupdate.Autoupdate.SHA256_ATTR: file_sha256,
360 autoupdate.Autoupdate.ISDELTA_ATTR: is_delta
361 })
Gilad Arnold55a2a372012-10-02 09:46:32 -0700362
Chris Sosa76e44b92013-01-31 12:11:38 -0800363
David Rochberg7c79a812011-01-19 14:24:45 -0500364class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700365 """The Root Class for the Dev Server.
366
367 CherryPy works as follows:
368 For each method in this class, cherrpy interprets root/path
369 as a call to an instance of DevServerRoot->method_name. For example,
370 a call to http://myhost/build will call build. CherryPy automatically
371 parses http args and places them as keyword arguments in each method.
372 For paths http://myhost/update/dir1/dir2, you can use *args so that
373 cherrypy uses the update method and puts the extra paths in args.
374 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700375 # Method names that should not be listed on the index page.
376 _UNLISTED_METHODS = ['index', 'doc']
377
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700378 api = ApiRoot()
Chris Sosa7c931362010-10-11 19:49:01 -0700379
Dan Shi59ae7092013-06-04 14:37:27 -0700380 # Number of threads that devserver is staging images.
381 _staging_thread_count = 0
382 # Lock used to lock increasing/decreasing count.
383 _staging_thread_count_lock = threading.Lock()
384
joychen3cb228e2013-06-12 12:13:13 -0700385 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700386 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800387 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 12:13:13 -0700388 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 14:24:45 -0500389
Chris Sosa6b0c6172013-08-05 17:01:33 -0700390 @staticmethod
391 def _get_artifacts(kwargs):
392 """Returns a tuple of named and file artifacts given the stage rpc kwargs.
393
Don Garrettf84631a2014-01-07 18:21:26 -0800394 Raises:
395 DevserverError if no artifacts would be returned.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700396 """
397 artifacts = kwargs.get('artifacts')
398 files = kwargs.get('files')
399 if not artifacts and not files:
400 raise DevServerError('No artifacts specified.')
401
Chris Sosafa86b482013-09-04 11:30:36 -0700402 # Note we NEED to coerce files to a string as we get raw unicode from
403 # cherrypy and we treat files as strings elsewhere in the code.
404 return (str(artifacts).split(',') if artifacts else [],
405 str(files).split(',') if files else [])
Chris Sosa6b0c6172013-08-05 17:01:33 -0700406
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700407 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500408 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700409 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700410 import builder
411 if self._builder is None:
412 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500413 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700414
Chris Sosacde6bf42012-05-31 18:36:39 -0700415 @staticmethod
416 def _canonicalize_archive_url(archive_url):
417 """Canonicalizes archive_url strings.
418
419 Raises:
420 DevserverError: if archive_url is not set.
421 """
422 if archive_url:
Chris Sosa76e44b92013-01-31 12:11:38 -0800423 if not archive_url.startswith('gs://'):
Don Garrett8ccab732013-08-30 09:13:59 -0700424 raise DevServerError("Archive URL isn't from Google Storage (%s) ." %
425 archive_url)
Chris Sosa76e44b92013-01-31 12:11:38 -0800426
Chris Sosacde6bf42012-05-31 18:36:39 -0700427 return archive_url.rstrip('/')
428 else:
429 raise DevServerError("Must specify an archive_url in the request")
430
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700431 @cherrypy.expose
Dan Shif8eb0d12013-08-01 17:52:06 -0700432 def is_staged(self, **kwargs):
433 """Check if artifacts have been downloaded.
434
Chris Sosa6b0c6172013-08-05 17:01:33 -0700435 async: True to return without waiting for download to complete.
436 artifacts: Comma separated list of named artifacts to download.
437 These are defined in artifact_info and have their implementation
438 in build_artifact.py.
439 files: Comma separated list of file artifacts to stage. These
440 will be available as is in the corresponding static directory with no
441 custom post-processing.
442
443 returns: True of all artifacts are staged.
Dan Shif8eb0d12013-08-01 17:52:06 -0700444
445 Example:
446 To check if autotest and test_suites are staged:
447 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
448 artifacts=autotest,test_suites
449 """
450 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
Chris Sosa6b0c6172013-08-05 17:01:33 -0700451 artifacts, files = self._get_artifacts(kwargs)
Dan Shif8eb0d12013-08-01 17:52:06 -0700452 return str(downloader.Downloader(updater.static_dir, archive_url).IsStaged(
Chris Sosa6b0c6172013-08-05 17:01:33 -0700453 artifacts, files))
Dan Shi59ae7092013-06-04 14:37:27 -0700454
Chris Sosa76e44b92013-01-31 12:11:38 -0800455 @cherrypy.expose
456 def stage(self, **kwargs):
457 """Downloads and caches the artifacts from Google Storage URL.
458
459 Downloads and caches the artifacts Google Storage URL. Returns once these
460 have been downloaded on the devserver. A call to this will attempt to cache
461 non-specified artifacts in the background for the given from the given URL
462 following the principle of spatial locality. Spatial locality of different
463 artifacts is explicitly defined in the build_artifact module.
464
465 These artifacts will then be available from the static/ sub-directory of
466 the devserver.
467
468 Args:
469 archive_url: Google Storage URL for the build.
Dan Shif8eb0d12013-08-01 17:52:06 -0700470 async: True to return without waiting for download to complete.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700471 artifacts: Comma separated list of named artifacts to download.
472 These are defined in artifact_info and have their implementation
473 in build_artifact.py.
474 files: Comma separated list of files to stage. These
475 will be available as is in the corresponding static directory with no
476 custom post-processing.
Chris Sosa76e44b92013-01-31 12:11:38 -0800477
478 Example:
479 To download the autotest and test suites tarballs:
480 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
481 artifacts=autotest,test_suites
482 To download the full update payload:
483 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
484 artifacts=full_payload
Chris Sosa6b0c6172013-08-05 17:01:33 -0700485 To download just a file called blah.bin:
486 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
487 files=blah.bin
Chris Sosa76e44b92013-01-31 12:11:38 -0800488
489 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 16:39:34 -0700490 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 12:11:38 -0800491
492 Note for this example, relative path is the archive_url stripped of its
493 basename i.e. path/ in the examples above. Specific example:
494
495 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
496
497 Will get staged to:
498
joychened64b222013-06-21 16:39:34 -0700499 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Chris Sosa76e44b92013-01-31 12:11:38 -0800500 """
Chris Sosacde6bf42012-05-31 18:36:39 -0700501 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
Dan Shif8eb0d12013-08-01 17:52:06 -0700502 async = kwargs.get('async', False)
Chris Sosa6b0c6172013-08-05 17:01:33 -0700503 artifacts, files = self._get_artifacts(kwargs)
Dan Shi59ae7092013-06-04 14:37:27 -0700504 with DevServerRoot._staging_thread_count_lock:
505 DevServerRoot._staging_thread_count += 1
506 try:
Chris Sosa6b0c6172013-08-05 17:01:33 -0700507 downloader.Downloader(updater.static_dir, archive_url).Download(
508 artifacts, files, async=async)
Dan Shi59ae7092013-06-04 14:37:27 -0700509 finally:
510 with DevServerRoot._staging_thread_count_lock:
511 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800512 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700513
514 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -0800515 def setup_telemetry(self, **kwargs):
516 """Extracts and sets up telemetry
517
518 This method goes through the telemetry deps packages, and stages them on
519 the devserver to be used by the drones and the telemetry tests.
520
521 Args:
522 archive_url: Google Storage URL for the build.
523
524 Returns:
525 Path to the source folder for the telemetry codebase once it is staged.
526 """
527 archive_url = kwargs.get('archive_url')
528 self.stage(archive_url=archive_url, artifacts='autotest')
529
530 build = '/'.join(downloader.Downloader.ParseUrl(archive_url))
531 build_path = os.path.join(updater.static_dir, build)
532 deps_path = os.path.join(build_path, 'autotest/packages')
533 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
534 src_folder = os.path.join(telemetry_path, 'src')
535
536 with self._telemetry_lock_dict.lock(telemetry_path):
537 if os.path.exists(src_folder):
538 # Telemetry is already fully stage return
539 return src_folder
540
541 common_util.MkDirP(telemetry_path)
542
543 # Copy over the required deps tar balls to the telemetry directory.
544 for dep in TELEMETRY_DEPS:
545 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -0700546 if not os.path.exists(dep_path):
547 # This dep does not exist (could be new), do not extract it.
548 continue
Simran Basi4baad082013-02-14 13:39:18 -0800549 try:
550 common_util.ExtractTarball(dep_path, telemetry_path)
551 except common_util.CommonUtilError as e:
552 shutil.rmtree(telemetry_path)
553 raise DevServerError(str(e))
554
555 # By default all the tarballs extract to test_src but some parts of
556 # the telemetry code specifically hardcoded to exist inside of 'src'.
557 test_src = os.path.join(telemetry_path, 'test_src')
558 try:
559 shutil.move(test_src, src_folder)
560 except shutil.Error:
561 # This can occur if src_folder already exists. Remove and retry move.
562 shutil.rmtree(src_folder)
563 raise DevServerError('Failure in telemetry setup for build %s. Appears'
564 ' that the test_src to src move failed.' % build)
565
566 return src_folder
567
568 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800569 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -0700570 """Symbolicates a minidump using pre-downloaded symbols, returns it.
571
572 Callers will need to POST to this URL with a body of MIME-type
573 "multipart/form-data".
574 The body should include a single argument, 'minidump', containing the
575 binary-formatted minidump to symbolicate.
576
Chris Masone816e38c2012-05-02 12:22:36 -0700577 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -0800578 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -0700579 minidump: The binary minidump file to symbolicate.
580 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800581 # Ensure the symbols have been staged.
582 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
583 if self.stage(archive_url=archive_url, artifacts='symbols') != 'Success':
584 raise DevServerError('Failed to stage symbols for %s' % archive_url)
585
Chris Masone816e38c2012-05-02 12:22:36 -0700586 to_return = ''
587 with tempfile.NamedTemporaryFile() as local:
588 while True:
589 data = minidump.file.read(8192)
590 if not data:
591 break
592 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -0800593
Chris Masone816e38c2012-05-02 12:22:36 -0700594 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -0800595
596 symbols_directory = os.path.join(downloader.Downloader.GetBuildDir(
597 updater.static_dir, archive_url), 'debug', 'breakpad')
598
599 stackwalk = subprocess.Popen(
600 ['minidump_stackwalk', local.name, symbols_directory],
601 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
602
Chris Masone816e38c2012-05-02 12:22:36 -0700603 to_return, error_text = stackwalk.communicate()
604 if stackwalk.returncode != 0:
605 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
606 error_text, stackwalk.returncode))
607
608 return to_return
609
610 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800611 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 15:31:36 -0400612 """Return a string representing the latest build for a given target.
613
614 Args:
615 target: The build target, typically a combination of the board and the
616 type of build e.g. x86-mario-release.
617 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
618 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-07 18:21:26 -0800619
Scott Zawalski16954532012-03-20 15:31:36 -0400620 Returns:
621 A string representation of the latest build if one exists, i.e.
622 R19-1993.0.0-a1-b1480.
623 An empty string if no latest could be found.
624 """
Don Garrettf84631a2014-01-07 18:21:26 -0800625 if not kwargs:
Scott Zawalski16954532012-03-20 15:31:36 -0400626 return _PrintDocStringAsHTML(self.latestbuild)
627
Don Garrettf84631a2014-01-07 18:21:26 -0800628 if 'target' not in kwargs:
beepsd76c6092013-08-28 22:23:30 -0700629 raise DevServerHTTPError(500, 'Error: target= is required!')
Scott Zawalski16954532012-03-20 15:31:36 -0400630 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700631 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-07 18:21:26 -0800632 updater.static_dir, kwargs['target'],
633 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700634 except common_util.CommonUtilError as errmsg:
beepsd76c6092013-08-28 22:23:30 -0700635 raise DevServerHTTPError(500, str(errmsg))
Scott Zawalski16954532012-03-20 15:31:36 -0400636
637 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800638 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500639 """Return a control file or a list of all known control files.
640
641 Example URL:
642 To List all control files:
beepsbd337242013-07-09 22:44:06 -0700643 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
644 To List all control files for, say, the bvt suite:
645 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -0500646 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500647 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 -0500648
649 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500650 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500651 control_path: If you want the contents of a control file set this
652 to the path. E.g. client/site_tests/sleeptest/control
653 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -0700654 suite_name: If control_path is not specified but a suite_name is
655 specified, list the control files belonging to that suite instead of
656 all control files. The empty string for suite_name will list all control
657 files for the build.
Don Garrettf84631a2014-01-07 18:21:26 -0800658
Scott Zawalski4647ce62012-01-03 17:17:28 -0500659 Returns:
660 Contents of a control file if control_path is provided.
661 A list of control files if no control_path is provided.
662 """
Don Garrettf84631a2014-01-07 18:21:26 -0800663 if not kwargs:
Scott Zawalski4647ce62012-01-03 17:17:28 -0500664 return _PrintDocStringAsHTML(self.controlfiles)
665
Don Garrettf84631a2014-01-07 18:21:26 -0800666 if 'build' not in kwargs:
beepsd76c6092013-08-28 22:23:30 -0700667 raise DevServerHTTPError(500, 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -0500668
Don Garrettf84631a2014-01-07 18:21:26 -0800669 if 'control_path' not in kwargs:
670 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-09 22:44:06 -0700671 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-07 18:21:26 -0800672 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-09 22:44:06 -0700673 else:
674 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-07 18:21:26 -0800675 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -0500676 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700677 return common_util.GetControlFile(
Don Garrettf84631a2014-01-07 18:21:26 -0800678 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -0800679
680 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -0700681 def xbuddy(self, *args, **kwargs):
682 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -0700683
684 Args:
joycheneaf4cfc2013-07-02 08:38:57 -0700685 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -0700686 components of the path. The path can be understood as
687 "{local|remote}/build_id/artifact" where build_id is composed of
688 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -0700689
joychen121fc9b2013-08-02 14:30:30 -0700690 The first path element is optional, and can be "remote" or "local"
691 If local (the default), devserver will not attempt to access Google
692 Storage, and will only search the static directory for the files.
693 If remote, devserver will try to obtain the artifact off GS if it's
694 not found locally.
695 The board is the familiar board name, optionally suffixed.
696 The version can be the google storage version number, and may also be
697 any of a number of xBuddy defined version aliases that will be
698 translated into the latest built image that fits the description.
699 Defaults to latest.
700 The artifact is one of a number of image or artifact aliases used by
701 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -0700702
703 Kwargs:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800704 for_update: {true|false}
705 if true, pregenerates the update payloads for the image,
706 and returns the update uri to pass to the
707 update_engine_client.
joychen3cb228e2013-06-12 12:13:13 -0700708 return_dir: {true|false}
709 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800710 relative_path: {true|false}
711 if set to true, returns the relative path to the payload
712 directory from static_dir.
joychen3cb228e2013-06-12 12:13:13 -0700713 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -0700714 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -0700715 or
joycheneaf4cfc2013-07-02 08:38:57 -0700716 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -0700717
718 Returns:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800719 If |for_update|, returns a redirect to the image or update file
720 on the devserver. E.g.,
721 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
722 chromium-test-image.bin
723 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
724 http://host:port/static/x86-generic-release/R26-4000.0.0/
725 If |relative_path| is true, return a relative path the folder where the
726 payloads are. E.g.,
727 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 12:13:13 -0700728 """
Chris Sosa75490802013-09-30 17:21:45 -0700729 boolean_string = kwargs.get('for_update')
730 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800731 boolean_string = kwargs.get('return_dir')
732 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
733 boolean_string = kwargs.get('relative_path')
734 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -0700735
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800736 if return_dir and relative_path:
737 raise DevServerHTTPError(500, 'Cannot specify both return_dir and '
738 'relative_path')
Chris Sosa75490802013-09-30 17:21:45 -0700739
740 # For updates, we optimize downloading of test images.
741 file_name = None
742 build_id = None
743 if for_update:
744 try:
745 build_id = self._xbuddy.StageTestAritfactsForUpdate(args)
746 except build_artifact.ArtifactDownloadError:
747 build_id = None
748
749 if not build_id:
750 build_id, file_name = self._xbuddy.Get(args)
751
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800752 if for_update:
753 _Log('Payload generation triggered by request')
754 # Forces payload to be in cache and symlinked into build_id dir.
Chris Sosa75490802013-09-30 17:21:45 -0700755 updater.GetUpdateForLabel(autoupdate.FORCED_UPDATE, build_id,
756 image_name=file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800757
758 response = None
759 if return_dir:
760 response = os.path.join(cherrypy.request.base, 'static', build_id)
761 _Log('Directory requested, returning: %s', response)
762 elif relative_path:
763 response = build_id
764 _Log('Relative path requested, returning: %s', response)
765 elif for_update:
766 response = os.path.join(cherrypy.request.base, 'update', build_id)
767 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 12:13:13 -0700768 else:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800769 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 14:30:30 -0700770 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800771 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 14:30:30 -0700772 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -0700773
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800774 return response
775
joychen3cb228e2013-06-12 12:13:13 -0700776 @cherrypy.expose
777 def xbuddy_list(self):
778 """Lists the currently available images & time since last access.
779
780 @return: A string representation of a list of tuples
781 [(build_id, time since last access),...]
782 """
783 return self._xbuddy.List()
784
785 @cherrypy.expose
786 def xbuddy_capacity(self):
787 """Returns the number of images cached by xBuddy.
788
789 @return: Capacity of this devserver.
790 """
791 return self._xbuddy.Capacity()
792
793 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700794 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700795 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700796 return ('Welcome to the Dev Server!<br>\n'
797 '<br>\n'
798 'Here are the available methods, click for documentation:<br>\n'
799 '<br>\n'
800 '%s' %
801 '<br>\n'.join(
802 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700803 for name in _FindExposedMethods(
804 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700805
806 @cherrypy.expose
807 def doc(self, *args):
808 """Shows the documentation for available methods / URLs.
809
810 Example:
811 http://myhost/doc/update
812 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700813 name = '/'.join(args)
814 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700815 if not method:
816 raise DevServerError("No exposed method named `%s'" % name)
817 if not method.__doc__:
818 raise DevServerError("No documentation for exposed method `%s'" % name)
819 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -0700820
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700821 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700822 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700823 """Handles an update check from a Chrome OS client.
824
825 The HTTP request should contain the standard Omaha-style XML blob. The URL
826 line may contain an additional intermediate path to the update payload.
827
joychen121fc9b2013-08-02 14:30:30 -0700828 This request can be handled in one of 4 ways, depending on the devsever
829 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -0700830
joychen121fc9b2013-08-02 14:30:30 -0700831 1. No intermediate path
832 If no intermediate path is given, the default behavior is to generate an
833 update payload from the latest test image locally built for the board
834 specified in the xml. Devserver serves the generated payload.
835
836 2. Path explicitly invokes XBuddy
837 If there is a path given, it can explicitly invoke xbuddy by prefixing it
838 with 'xbuddy'. This path is then used to acquire an image binary for the
839 devserver to generate an update payload from. Devserver then serves this
840 payload.
841
842 3. Path is left for the devserver to interpret.
843 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
844 to generate a payload from the test image in that directory and serve it.
845
846 4. The devserver is in a 'forced' mode. TO BE DEPRECATED
847 This comes from the usage of --forced_payload or --image when starting the
848 devserver. No matter what path (or no path) gets passed in, devserver will
849 serve the update payload (--forced_payload) or generate an update payload
850 from the image (--image).
851
852 Examples:
853 1. No intermediate path
854 update_engine_client --omaha_url=http://myhost/update
855 This generates an update payload from the latest test image locally built
856 for the board specified in the xml.
857
858 2. Explicitly invoke xbuddy
859 update_engine_client --omaha_url=
860 http://myhost/update/xbuddy/remote/board/version/dev
861 This would go to GS to download the dev image for the board, from which
862 the devserver would generate a payload to serve.
863
864 3. Give a path for devserver to interpret
865 update_engine_client --omaha_url=http://myhost/update/some/random/path
866 This would attempt, in order to:
867 a) Generate an update from a test image binary if found in
868 static_dir/some/random/path.
869 b) Serve an update payload found in static_dir/some/random/path.
870 c) Hope that some/random/path takes the form "board/version" and
871 and attempt to download an update payload for that board/version
872 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700873 """
joychen121fc9b2013-08-02 14:30:30 -0700874 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -0800875 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -0700876 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-11 19:49:01 -0700877
joychen121fc9b2013-08-02 14:30:30 -0700878 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700879
Dan Shif5ce2de2013-04-25 16:06:32 -0700880 @cherrypy.expose
881 def check_health(self):
882 """Collect the health status of devserver to see if it's ready for staging.
883
884 @return: A JSON dictionary containing all or some of the following fields:
Dan Shi59ae7092013-06-04 14:37:27 -0700885 free_disk (int): free disk space in GB
886 staging_thread_count (int): number of devserver threads currently
887 staging an image
Dan Shif5ce2de2013-04-25 16:06:32 -0700888 """
889 # Get free disk space.
890 stat = os.statvfs(updater.static_dir)
891 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
892
893 return json.dumps({
894 'free_disk': free_disk,
Dan Shi59ae7092013-06-04 14:37:27 -0700895 'staging_thread_count': DevServerRoot._staging_thread_count,
Dan Shif5ce2de2013-04-25 16:06:32 -0700896 })
897
898
Chris Sosadbc20082012-12-10 13:39:11 -0800899def _CleanCache(cache_dir, wipe):
900 """Wipes any excess cached items in the cache_dir.
901
902 Args:
903 cache_dir: the directory we are wiping from.
904 wipe: If True, wipe all the contents -- not just the excess.
905 """
906 if wipe:
907 # Clear the cache and exit on error.
908 cmd = 'rm -rf %s/*' % cache_dir
909 if os.system(cmd) != 0:
910 _Log('Failed to clear the cache with %s' % cmd)
911 sys.exit(1)
912 else:
913 # Clear all but the last N cached updates
914 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
915 (cache_dir, CACHED_ENTRIES))
916 if os.system(cmd) != 0:
917 _Log('Failed to clean up old delta cache files with %s' % cmd)
918 sys.exit(1)
919
920
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700921def _AddTestingOptions(parser):
922 group = optparse.OptionGroup(
923 parser, 'Advanced Testing Options', 'These are used by test scripts and '
924 'developers writing integration tests utilizing the devserver. They are '
925 'not intended to be really used outside the scope of someone '
926 'knowledgable about the test.')
927 group.add_option('--exit',
928 action='store_true',
929 help='do not start the server (yet pregenerate/clear cache)')
930 group.add_option('--host_log',
931 action='store_true', default=False,
932 help='record history of host update events (/api/hostlog)')
933 group.add_option('--max_updates',
934 metavar='NUM', default= -1, type='int',
935 help='maximum number of update checks handled positively '
936 '(default: unlimited)')
937 group.add_option('--private_key',
938 metavar='PATH', default=None,
939 help='path to the private key in pem format. If this is set '
940 'the devserver will generate update payloads that are '
941 'signed with this key.')
David Zeuthen52ccd012013-10-31 12:58:26 -0700942 group.add_option('--private_key_for_metadata_hash_signature',
943 metavar='PATH', default=None,
944 help='path to the private key in pem format. If this is set '
945 'the devserver will sign the metadata hash with the given '
946 'key and transmit in the Omaha-style XML response.')
947 group.add_option('--public_key',
948 metavar='PATH', default=None,
949 help='path to the public key in pem format. If this is set '
950 'the devserver will transmit a base64 encoded version of '
951 'the content in the Omaha-style XML response.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700952 group.add_option('--proxy_port',
953 metavar='PORT', default=None, type='int',
954 help='port to have the client connect to -- basically the '
955 'devserver lies to the update to tell it to get the payload '
956 'from a different port that will proxy the request back to '
957 'the devserver. The proxy must be managed outside the '
958 'devserver.')
959 group.add_option('--remote_payload',
960 action='store_true', default=False,
961 help='Payload is being served from a remote machine')
962 group.add_option('-u', '--urlbase',
963 metavar='URL',
964 help='base URL for update images, other than the '
965 'devserver. Use in conjunction with remote_payload.')
966 parser.add_option_group(group)
967
968
969def _AddUpdateOptions(parser):
970 group = optparse.OptionGroup(
971 parser, 'Autoupdate Options', 'These options can be used to change '
972 'how the devserver either generates or serve update payloads. Please '
973 'note that all of these option affect how a payload is generated and so '
974 'do not work in archive-only mode.')
975 group.add_option('--board',
976 help='By default the devserver will create an update '
977 'payload from the latest image built for the board '
978 'a device that is requesting an update has. When we '
979 'pre-generate an update (see below) and we do not specify '
980 'another update_type option like image or payload, the '
981 'devserver needs to know the board to generate the latest '
982 'image for. This is that board.')
983 group.add_option('--critical_update',
984 action='store_true', default=False,
985 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700986 group.add_option('--image',
987 metavar='FILE',
988 help='Generate and serve an update using this image to any '
989 'device that requests an update.')
990 group.add_option('--no_patch_kernel',
991 dest='patch_kernel', action='store_false', default=True,
992 help='When generating an update payload, do not patch the '
993 'kernel with kernel verification blob from the stateful '
994 'partition.')
995 group.add_option('--payload',
996 metavar='PATH',
997 help='use the update payload from specified directory '
998 '(update.gz).')
999 group.add_option('-p', '--pregenerate_update',
1000 action='store_true', default=False,
1001 help='pre-generate the update payload before accepting '
1002 'update requests. Useful to help debug payload generation '
1003 'issues quickly. Also if an update payload will take a '
1004 'long time to generate, a client may timeout if you do not'
1005 'pregenerate the update.')
1006 group.add_option('--src_image',
1007 metavar='PATH', default='',
1008 help='If specified, delta updates will be generated using '
1009 'this image as the source image. Delta updates are when '
1010 'you are updating from a "source image" to a another '
1011 'image.')
1012 parser.add_option_group(group)
1013
1014
1015def _AddProductionOptions(parser):
1016 group = optparse.OptionGroup(
1017 parser, 'Advanced Server Options', 'These options can be used to changed '
1018 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001019 group.add_option('--clear_cache',
1020 action='store_true', default=False,
1021 help='At startup, removes all cached entries from the'
1022 'devserver\'s cache.')
1023 group.add_option('--logfile',
1024 metavar='PATH',
1025 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -07001026 group.add_option('--pidfile',
1027 metavar='PATH',
1028 help='path to output a pid file for the server.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001029 group.add_option('--production',
1030 action='store_true', default=False,
1031 help='have the devserver use production values when '
1032 'starting up. This includes using more threads and '
1033 'performing less logging.')
1034 parser.add_option_group(group)
1035
1036
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001037def _MakeLogHandler(logfile):
1038 """Create a LogHandler instance used to log all messages."""
1039 hdlr_cls = handlers.TimedRotatingFileHandler
1040 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
1041 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -07001042 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001043 return hdlr
1044
1045
Chris Sosacde6bf42012-05-31 18:36:39 -07001046def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001047 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -08001048 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -07001049
1050 # get directory that the devserver is run from
1051 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -07001052 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -07001053 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001054 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -07001055 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -07001056 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001057 parser.add_option('--port',
1058 default=8080, type='int',
1059 help='port for the dev server to use (default: 8080)')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001060 parser.add_option('-t', '--test_image',
1061 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -07001062 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -07001063 parser.add_option('-x', '--xbuddy_manage_builds',
1064 action='store_true',
1065 default=False,
1066 help='If set, allow xbuddy to manage images in'
1067 'build/images.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001068 _AddProductionOptions(parser)
1069 _AddUpdateOptions(parser)
1070 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -07001071 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001072
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001073 # Handle options that must be set globally in cherrypy. Do this
1074 # work up front, because calls to _Log() below depend on this
1075 # initialization.
1076 if options.production:
1077 cherrypy.config.update({'environment': 'production'})
1078 if not options.logfile:
1079 cherrypy.config.update({'log.screen': True})
1080 else:
1081 cherrypy.config.update({'log.error_file': '',
1082 'log.access_file': ''})
1083 hdlr = _MakeLogHandler(options.logfile)
1084 # Pylint can't seem to process these two calls properly
1085 # pylint: disable=E1101
1086 cherrypy.log.access_log.addHandler(hdlr)
1087 cherrypy.log.error_log.addHandler(hdlr)
1088 # pylint: enable=E1101
1089
joychened64b222013-06-21 16:39:34 -07001090 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001091 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001092
joychened64b222013-06-21 16:39:34 -07001093 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001094 # If our devserver is only supposed to serve payloads, we shouldn't be
1095 # mucking with the cache at all. If the devserver hadn't previously
1096 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001097 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001098 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001099 else:
1100 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001101
Chris Sosadbc20082012-12-10 13:39:11 -08001102 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 16:39:34 -07001103 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001104
joychen121fc9b2013-08-02 14:30:30 -07001105 _xbuddy = xbuddy.XBuddy(options.xbuddy_manage_builds,
1106 options.board,
joychen121fc9b2013-08-02 14:30:30 -07001107 static_dir=options.static_dir)
Chris Sosa75490802013-09-30 17:21:45 -07001108 if options.clear_cache and options.xbuddy_manage_builds:
1109 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 14:30:30 -07001110
Chris Sosa6a3697f2013-01-29 16:44:43 -08001111 # We allow global use here to share with cherrypy classes.
1112 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001113 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001114 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001115 _xbuddy,
joychened64b222013-06-21 16:39:34 -07001116 static_dir=options.static_dir,
Andrew de los Reyes52620802010-04-12 13:40:07 -07001117 urlbase=options.urlbase,
Chris Sosa5d342a22010-09-28 16:54:41 -07001118 forced_image=options.image,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001119 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001120 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-28 23:42:37 -07001121 src_image=options.src_image,
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001122 patch_kernel=options.patch_kernel,
Chris Sosa08d55a22011-01-19 16:08:02 -08001123 board=options.board,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001124 copy_to_static_root=not options.exit,
1125 private_key=options.private_key,
David Zeuthen52ccd012013-10-31 12:58:26 -07001126 private_key_for_metadata_hash_signature=
1127 options.private_key_for_metadata_hash_signature,
1128 public_key=options.public_key,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001129 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001130 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001131 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001132 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001133 )
Chris Sosa7c931362010-10-11 19:49:01 -07001134
Chris Sosa6a3697f2013-01-29 16:44:43 -08001135 if options.pregenerate_update:
1136 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 15:46:22 -07001137
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001138 if options.exit:
1139 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001140
joychen3cb228e2013-06-12 12:13:13 -07001141 dev_server = DevServerRoot(_xbuddy)
1142
Chris Sosa855b8932013-08-21 13:24:55 -07001143 if options.pidfile:
1144 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1145
joychen3cb228e2013-06-12 12:13:13 -07001146 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001147
1148
1149if __name__ == '__main__':
1150 main()