blob: 97cbded58e96c95d4fbf83bcbcf6c503418ad44b [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
Scott Zawalski4647ce62012-01-03 17:17:28 -050099def _LeadingWhiteSpaceCount(string):
100 """Count the amount of leading whitespace in a string.
101
102 Args:
103 string: The string to count leading whitespace in.
Don Garrettf84631a2014-01-07 18:21:26 -0800104
Scott Zawalski4647ce62012-01-03 17:17:28 -0500105 Returns:
106 number of white space chars before characters start.
107 """
108 matched = re.match('^\s+', string)
109 if matched:
110 return len(matched.group())
111
112 return 0
113
114
115def _PrintDocStringAsHTML(func):
116 """Make a functions docstring somewhat HTML style.
117
118 Args:
119 func: The function to return the docstring from.
Don Garrettf84631a2014-01-07 18:21:26 -0800120
Scott Zawalski4647ce62012-01-03 17:17:28 -0500121 Returns:
122 A string that is somewhat formated for a web browser.
123 """
124 # TODO(scottz): Make this parse Args/Returns in a prettier way.
125 # Arguments could be bolded and indented etc.
126 html_doc = []
127 for line in func.__doc__.splitlines():
128 leading_space = _LeadingWhiteSpaceCount(line)
129 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700130 line = '&nbsp;' * leading_space + line
Scott Zawalski4647ce62012-01-03 17:17:28 -0500131
132 html_doc.append('<BR>%s' % line)
133
134 return '\n'.join(html_doc)
135
136
Chris Sosa7c931362010-10-11 19:49:01 -0700137def _GetConfig(options):
138 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800139
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800140 socket_host = '::'
Yu-Ju Hongc8d4af32013-11-12 15:14:26 -0800141 # Fall back to IPv4 when python is not configured with IPv6.
142 if not socket.has_ipv6:
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800143 socket_host = '0.0.0.0'
144
Chris Sosa7c931362010-10-11 19:49:01 -0700145 base_config = { 'global':
146 { 'server.log_request_headers': True,
147 'server.protocol_version': 'HTTP/1.1',
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800148 'server.socket_host': socket_host,
Chris Sosa7c931362010-10-11 19:49:01 -0700149 'server.socket_port': int(options.port),
Chris Sosa374c62d2010-10-14 09:13:54 -0700150 'response.timeout': 6000,
Chris Sosa6fe23942012-07-02 15:44:46 -0700151 'request.show_tracebacks': True,
Chris Sosa72333d12012-06-13 11:28:05 -0700152 'server.socket_timeout': 60,
joychenecc02aa2013-07-17 18:27:35 -0700153 'server.thread_pool': 2,
Chris Sosa7c931362010-10-11 19:49:01 -0700154 },
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700155 '/api':
156 {
157 # Gets rid of cherrypy parsing post file for args.
158 'request.process_request_body': False,
159 },
Chris Sosaa1ef0102010-10-21 16:22:35 -0700160 '/build':
161 {
162 'response.timeout': 100000,
163 },
Chris Sosa7c931362010-10-11 19:49:01 -0700164 '/update':
165 {
166 # Gets rid of cherrypy parsing post file for args.
167 'request.process_request_body': False,
Chris Sosaf65f4b92010-10-21 15:57:51 -0700168 'response.timeout': 10000,
Chris Sosa7c931362010-10-11 19:49:01 -0700169 },
170 # Sets up the static dir for file hosting.
171 '/static':
joychened64b222013-06-21 16:39:34 -0700172 { 'tools.staticdir.dir': options.static_dir,
Chris Sosa7c931362010-10-11 19:49:01 -0700173 'tools.staticdir.on': True,
Chris Sosaf65f4b92010-10-21 15:57:51 -0700174 'response.timeout': 10000,
Chris Sosa7c931362010-10-11 19:49:01 -0700175 },
176 }
Chris Sosa5f118ef2012-07-12 11:37:50 -0700177 if options.production:
Alex Miller93beca52013-07-30 19:25:09 -0700178 base_config['global'].update({'server.thread_pool': 150})
Chris Sosa7cd23202013-10-15 17:22:57 -0700179 # TODO(sosa): Do this more cleanly.
180 gsutil_util.GSUTIL_ATTEMPTS = 5
Scott Zawalski1c5e7cd2012-02-27 13:12:52 -0500181
Chris Sosa7c931362010-10-11 19:49:01 -0700182 return base_config
rtc@google.com64244662009-11-12 00:52:08 +0000183
Darin Petkove17164a2010-08-11 13:24:41 -0700184
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700185def _GetRecursiveMemberObject(root, member_list):
186 """Returns an object corresponding to a nested member list.
187
188 Args:
189 root: the root object to search
190 member_list: list of nested members to search
Don Garrettf84631a2014-01-07 18:21:26 -0800191
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700192 Returns:
193 An object corresponding to the member name list; None otherwise.
194 """
195 for member in member_list:
196 next_root = root.__class__.__dict__.get(member)
197 if not next_root:
198 return None
199 root = next_root
200 return root
201
202
203def _IsExposed(name):
204 """Returns True iff |name| has an `exposed' attribute and it is set."""
205 return hasattr(name, 'exposed') and name.exposed
206
207
Gilad Arnold748c8322012-10-12 09:51:35 -0700208def _GetExposedMethod(root, nested_member, ignored=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700209 """Returns a CherryPy-exposed method, if such exists.
210
211 Args:
212 root: the root object for searching
213 nested_member: a slash-joined path to the nested member
214 ignored: method paths to be ignored
Don Garrettf84631a2014-01-07 18:21:26 -0800215
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700216 Returns:
217 A function object corresponding to the path defined by |member_list| from
218 the |root| object, if the function is exposed and not ignored; None
219 otherwise.
220 """
Gilad Arnold748c8322012-10-12 09:51:35 -0700221 method = (not (ignored and nested_member in ignored) and
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700222 _GetRecursiveMemberObject(root, nested_member.split('/')))
223 if (method and type(method) == types.FunctionType and _IsExposed(method)):
224 return method
225
226
Gilad Arnold748c8322012-10-12 09:51:35 -0700227def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700228 """Finds exposed CherryPy methods.
229
230 Args:
231 root: the root object for searching
232 prefix: slash-joined chain of members leading to current object
233 unlisted: URLs to be excluded regardless of their exposed status
Don Garrettf84631a2014-01-07 18:21:26 -0800234
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700235 Returns:
236 List of exposed URLs that are not unlisted.
237 """
238 method_list = []
239 for member in sorted(root.__class__.__dict__.keys()):
240 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 09:51:35 -0700241 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700242 continue
243 member_obj = root.__class__.__dict__[member]
244 if _IsExposed(member_obj):
245 if type(member_obj) == types.FunctionType:
246 method_list.append(prefixed_member)
247 else:
248 method_list += _FindExposedMethods(
249 member_obj, prefixed_member, unlisted)
250 return method_list
251
252
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700253class ApiRoot(object):
254 """RESTful API for Dev Server information."""
255 exposed = True
256
257 @cherrypy.expose
258 def hostinfo(self, ip):
259 """Returns a JSON dictionary containing information about the given ip.
260
Gilad Arnold1b908392012-10-05 11:36:27 -0700261 Args:
262 ip: address of host whose info is requested
Don Garrettf84631a2014-01-07 18:21:26 -0800263
Gilad Arnold1b908392012-10-05 11:36:27 -0700264 Returns:
265 A JSON dictionary containing all or some of the following fields:
266 last_event_type (int): last update event type received
267 last_event_status (int): last update event status received
268 last_known_version (string): last known version reported in update ping
269 forced_update_label (string): update label to force next update ping to
270 use, set by setnextupdate
271 See the OmahaEvent class in update_engine/omaha_request_action.h for
272 event type and status code definitions. If the ip does not exist an empty
273 string is returned.
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700274
Gilad Arnold1b908392012-10-05 11:36:27 -0700275 Example URL:
276 http://myhost/api/hostinfo?ip=192.168.1.5
277 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700278 return updater.HandleHostInfoPing(ip)
279
280 @cherrypy.expose
Gilad Arnold286a0062012-01-12 13:47:02 -0800281 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 11:36:27 -0700282 """Returns a JSON object containing a log of host event.
283
284 Args:
285 ip: address of host whose event log is requested, or `all'
Don Garrettf84631a2014-01-07 18:21:26 -0800286
Gilad Arnold1b908392012-10-05 11:36:27 -0700287 Returns:
288 A JSON encoded list (log) of dictionaries (events), each of which
289 containing a `timestamp' and other event fields, as described under
290 /api/hostinfo.
291
292 Example URL:
293 http://myhost/api/hostlog?ip=192.168.1.5
294 """
Gilad Arnold286a0062012-01-12 13:47:02 -0800295 return updater.HandleHostLogPing(ip)
296
297 @cherrypy.expose
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700298 def setnextupdate(self, ip):
299 """Allows the response to the next update ping from a host to be set.
300
301 Takes the IP of the host and an update label as normally provided to the
Gilad Arnold1b908392012-10-05 11:36:27 -0700302 /update command.
303 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700304 body_length = int(cherrypy.request.headers['Content-Length'])
305 label = cherrypy.request.rfile.read(body_length)
306
307 if label:
308 label = label.strip()
309 if label:
310 return updater.HandleSetUpdatePing(ip, label)
Chris Sosa4b951602014-04-09 20:26:07 -0700311 raise common_util.DevServerHTTPError(400, 'No label provided.')
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700312
313
Gilad Arnold55a2a372012-10-02 09:46:32 -0700314 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800315 def fileinfo(self, *args):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700316 """Returns information about a given staged file.
317
318 Args:
Don Garrettf84631a2014-01-07 18:21:26 -0800319 args: path to the file inside the server's static staging directory
320
Gilad Arnold55a2a372012-10-02 09:46:32 -0700321 Returns:
322 A JSON encoded dictionary with information about the said file, which may
323 contain the following keys/values:
Gilad Arnold1b908392012-10-05 11:36:27 -0700324 size (int): the file size in bytes
325 sha1 (string): a base64 encoded SHA1 hash
326 sha256 (string): a base64 encoded SHA256 hash
327
328 Example URL:
329 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 09:46:32 -0700330 """
Don Garrettf84631a2014-01-07 18:21:26 -0800331 file_path = os.path.join(updater.static_dir, *args)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700332 if not os.path.exists(file_path):
333 raise DevServerError('file not found: %s' % file_path)
334 try:
335 file_size = os.path.getsize(file_path)
336 file_sha1 = common_util.GetFileSha1(file_path)
337 file_sha256 = common_util.GetFileSha256(file_path)
338 except os.error, e:
339 raise DevServerError('failed to get info for file %s: %s' %
Gilad Arnolde74b3812013-04-22 11:27:38 -0700340 (file_path, e))
341
342 is_delta = autoupdate.Autoupdate.IsDeltaFormatFile(file_path)
343
344 return json.dumps({
345 autoupdate.Autoupdate.SIZE_ATTR: file_size,
346 autoupdate.Autoupdate.SHA1_ATTR: file_sha1,
347 autoupdate.Autoupdate.SHA256_ATTR: file_sha256,
348 autoupdate.Autoupdate.ISDELTA_ATTR: is_delta
349 })
Gilad Arnold55a2a372012-10-02 09:46:32 -0700350
Chris Sosa76e44b92013-01-31 12:11:38 -0800351
David Rochberg7c79a812011-01-19 14:24:45 -0500352class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700353 """The Root Class for the Dev Server.
354
355 CherryPy works as follows:
356 For each method in this class, cherrpy interprets root/path
357 as a call to an instance of DevServerRoot->method_name. For example,
358 a call to http://myhost/build will call build. CherryPy automatically
359 parses http args and places them as keyword arguments in each method.
360 For paths http://myhost/update/dir1/dir2, you can use *args so that
361 cherrypy uses the update method and puts the extra paths in args.
362 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700363 # Method names that should not be listed on the index page.
364 _UNLISTED_METHODS = ['index', 'doc']
365
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700366 api = ApiRoot()
Chris Sosa7c931362010-10-11 19:49:01 -0700367
Dan Shi59ae7092013-06-04 14:37:27 -0700368 # Number of threads that devserver is staging images.
369 _staging_thread_count = 0
370 # Lock used to lock increasing/decreasing count.
371 _staging_thread_count_lock = threading.Lock()
372
joychen3cb228e2013-06-12 12:13:13 -0700373 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700374 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800375 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 12:13:13 -0700376 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 14:24:45 -0500377
Chris Sosa6b0c6172013-08-05 17:01:33 -0700378 @staticmethod
379 def _get_artifacts(kwargs):
380 """Returns a tuple of named and file artifacts given the stage rpc kwargs.
381
Don Garrettf84631a2014-01-07 18:21:26 -0800382 Raises:
383 DevserverError if no artifacts would be returned.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700384 """
385 artifacts = kwargs.get('artifacts')
386 files = kwargs.get('files')
387 if not artifacts and not files:
388 raise DevServerError('No artifacts specified.')
389
Chris Sosafa86b482013-09-04 11:30:36 -0700390 # Note we NEED to coerce files to a string as we get raw unicode from
391 # cherrypy and we treat files as strings elsewhere in the code.
392 return (str(artifacts).split(',') if artifacts else [],
393 str(files).split(',') if files else [])
Chris Sosa6b0c6172013-08-05 17:01:33 -0700394
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700395 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500396 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700397 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700398 import builder
399 if self._builder is None:
400 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500401 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700402
Chris Sosacde6bf42012-05-31 18:36:39 -0700403 @staticmethod
404 def _canonicalize_archive_url(archive_url):
405 """Canonicalizes archive_url strings.
406
407 Raises:
408 DevserverError: if archive_url is not set.
409 """
410 if archive_url:
Chris Sosa76e44b92013-01-31 12:11:38 -0800411 if not archive_url.startswith('gs://'):
Don Garrett8ccab732013-08-30 09:13:59 -0700412 raise DevServerError("Archive URL isn't from Google Storage (%s) ." %
413 archive_url)
Chris Sosa76e44b92013-01-31 12:11:38 -0800414
Chris Sosacde6bf42012-05-31 18:36:39 -0700415 return archive_url.rstrip('/')
416 else:
417 raise DevServerError("Must specify an archive_url in the request")
418
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700419 @cherrypy.expose
Dan Shif8eb0d12013-08-01 17:52:06 -0700420 def is_staged(self, **kwargs):
421 """Check if artifacts have been downloaded.
422
Chris Sosa6b0c6172013-08-05 17:01:33 -0700423 async: True to return without waiting for download to complete.
424 artifacts: Comma separated list of named artifacts to download.
425 These are defined in artifact_info and have their implementation
426 in build_artifact.py.
427 files: Comma separated list of file artifacts to stage. These
428 will be available as is in the corresponding static directory with no
429 custom post-processing.
430
431 returns: True of all artifacts are staged.
Dan Shif8eb0d12013-08-01 17:52:06 -0700432
433 Example:
434 To check if autotest and test_suites are staged:
435 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
436 artifacts=autotest,test_suites
437 """
438 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
Chris Sosa6b0c6172013-08-05 17:01:33 -0700439 artifacts, files = self._get_artifacts(kwargs)
Dan Shif8eb0d12013-08-01 17:52:06 -0700440 return str(downloader.Downloader(updater.static_dir, archive_url).IsStaged(
Chris Sosa6b0c6172013-08-05 17:01:33 -0700441 artifacts, files))
Dan Shi59ae7092013-06-04 14:37:27 -0700442
Chris Sosa76e44b92013-01-31 12:11:38 -0800443 @cherrypy.expose
Prashanth Ba06d2d22014-03-07 15:35:19 -0800444 def list_image_dir(self, **kwargs):
445 """Take an archive url and list the contents in its staged directory.
446
447 Args:
448 kwargs:
449 archive_url: Google Storage URL for the build.
450
451 Example:
452 To list the contents of where this devserver should have staged
453 gs://image-archive/<board>-release/<build> call:
454 http://devserver_url:<port>/list_image_dir?archive_url=<gs://..>
455
456 Returns:
457 A string with information about the contents of the image directory.
458 """
459 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
460 download_helper = downloader.Downloader(updater.static_dir, archive_url)
461 try:
462 image_dir_contents = download_helper.ListBuildDir()
463 except build_artifact.ArtifactDownloadError as e:
464 return 'Cannot list the contents of staged artifacts. %s' % e
465 if not image_dir_contents:
466 return '%s has not been staged on this devserver.' % archive_url
467 return image_dir_contents
468
469 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800470 def stage(self, **kwargs):
471 """Downloads and caches the artifacts from Google Storage URL.
472
473 Downloads and caches the artifacts Google Storage URL. Returns once these
474 have been downloaded on the devserver. A call to this will attempt to cache
475 non-specified artifacts in the background for the given from the given URL
476 following the principle of spatial locality. Spatial locality of different
477 artifacts is explicitly defined in the build_artifact module.
478
479 These artifacts will then be available from the static/ sub-directory of
480 the devserver.
481
482 Args:
483 archive_url: Google Storage URL for the build.
Dan Shif8eb0d12013-08-01 17:52:06 -0700484 async: True to return without waiting for download to complete.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700485 artifacts: Comma separated list of named artifacts to download.
486 These are defined in artifact_info and have their implementation
487 in build_artifact.py.
488 files: Comma separated list of files to stage. These
489 will be available as is in the corresponding static directory with no
490 custom post-processing.
Chris Sosa76e44b92013-01-31 12:11:38 -0800491
492 Example:
493 To download the autotest and test suites tarballs:
494 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
495 artifacts=autotest,test_suites
496 To download the full update payload:
497 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
498 artifacts=full_payload
Chris Sosa6b0c6172013-08-05 17:01:33 -0700499 To download just a file called blah.bin:
500 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
501 files=blah.bin
Chris Sosa76e44b92013-01-31 12:11:38 -0800502
503 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 16:39:34 -0700504 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 12:11:38 -0800505
506 Note for this example, relative path is the archive_url stripped of its
507 basename i.e. path/ in the examples above. Specific example:
508
509 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
510
511 Will get staged to:
512
joychened64b222013-06-21 16:39:34 -0700513 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Chris Sosa76e44b92013-01-31 12:11:38 -0800514 """
Chris Sosacde6bf42012-05-31 18:36:39 -0700515 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
Dan Shif8eb0d12013-08-01 17:52:06 -0700516 async = kwargs.get('async', False)
Chris Sosa6b0c6172013-08-05 17:01:33 -0700517 artifacts, files = self._get_artifacts(kwargs)
Dan Shi59ae7092013-06-04 14:37:27 -0700518 with DevServerRoot._staging_thread_count_lock:
519 DevServerRoot._staging_thread_count += 1
520 try:
Chris Sosa6b0c6172013-08-05 17:01:33 -0700521 downloader.Downloader(updater.static_dir, archive_url).Download(
522 artifacts, files, async=async)
Dan Shi59ae7092013-06-04 14:37:27 -0700523 finally:
524 with DevServerRoot._staging_thread_count_lock:
525 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800526 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700527
528 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -0800529 def setup_telemetry(self, **kwargs):
530 """Extracts and sets up telemetry
531
532 This method goes through the telemetry deps packages, and stages them on
533 the devserver to be used by the drones and the telemetry tests.
534
535 Args:
536 archive_url: Google Storage URL for the build.
537
538 Returns:
539 Path to the source folder for the telemetry codebase once it is staged.
540 """
541 archive_url = kwargs.get('archive_url')
542 self.stage(archive_url=archive_url, artifacts='autotest')
543
544 build = '/'.join(downloader.Downloader.ParseUrl(archive_url))
545 build_path = os.path.join(updater.static_dir, build)
546 deps_path = os.path.join(build_path, 'autotest/packages')
547 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
548 src_folder = os.path.join(telemetry_path, 'src')
549
550 with self._telemetry_lock_dict.lock(telemetry_path):
551 if os.path.exists(src_folder):
552 # Telemetry is already fully stage return
553 return src_folder
554
555 common_util.MkDirP(telemetry_path)
556
557 # Copy over the required deps tar balls to the telemetry directory.
558 for dep in TELEMETRY_DEPS:
559 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -0700560 if not os.path.exists(dep_path):
561 # This dep does not exist (could be new), do not extract it.
562 continue
Simran Basi4baad082013-02-14 13:39:18 -0800563 try:
564 common_util.ExtractTarball(dep_path, telemetry_path)
565 except common_util.CommonUtilError as e:
566 shutil.rmtree(telemetry_path)
567 raise DevServerError(str(e))
568
569 # By default all the tarballs extract to test_src but some parts of
570 # the telemetry code specifically hardcoded to exist inside of 'src'.
571 test_src = os.path.join(telemetry_path, 'test_src')
572 try:
573 shutil.move(test_src, src_folder)
574 except shutil.Error:
575 # This can occur if src_folder already exists. Remove and retry move.
576 shutil.rmtree(src_folder)
577 raise DevServerError('Failure in telemetry setup for build %s. Appears'
578 ' that the test_src to src move failed.' % build)
579
580 return src_folder
581
582 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800583 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -0700584 """Symbolicates a minidump using pre-downloaded symbols, returns it.
585
586 Callers will need to POST to this URL with a body of MIME-type
587 "multipart/form-data".
588 The body should include a single argument, 'minidump', containing the
589 binary-formatted minidump to symbolicate.
590
Chris Masone816e38c2012-05-02 12:22:36 -0700591 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -0800592 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -0700593 minidump: The binary minidump file to symbolicate.
594 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800595 # Ensure the symbols have been staged.
596 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
597 if self.stage(archive_url=archive_url, artifacts='symbols') != 'Success':
598 raise DevServerError('Failed to stage symbols for %s' % archive_url)
599
Chris Masone816e38c2012-05-02 12:22:36 -0700600 to_return = ''
601 with tempfile.NamedTemporaryFile() as local:
602 while True:
603 data = minidump.file.read(8192)
604 if not data:
605 break
606 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -0800607
Chris Masone816e38c2012-05-02 12:22:36 -0700608 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -0800609
610 symbols_directory = os.path.join(downloader.Downloader.GetBuildDir(
611 updater.static_dir, archive_url), 'debug', 'breakpad')
612
613 stackwalk = subprocess.Popen(
614 ['minidump_stackwalk', local.name, symbols_directory],
615 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
616
Chris Masone816e38c2012-05-02 12:22:36 -0700617 to_return, error_text = stackwalk.communicate()
618 if stackwalk.returncode != 0:
619 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
620 error_text, stackwalk.returncode))
621
622 return to_return
623
624 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800625 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 15:31:36 -0400626 """Return a string representing the latest build for a given target.
627
628 Args:
629 target: The build target, typically a combination of the board and the
630 type of build e.g. x86-mario-release.
631 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
632 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-07 18:21:26 -0800633
Scott Zawalski16954532012-03-20 15:31:36 -0400634 Returns:
635 A string representation of the latest build if one exists, i.e.
636 R19-1993.0.0-a1-b1480.
637 An empty string if no latest could be found.
638 """
Don Garrettf84631a2014-01-07 18:21:26 -0800639 if not kwargs:
Scott Zawalski16954532012-03-20 15:31:36 -0400640 return _PrintDocStringAsHTML(self.latestbuild)
641
Don Garrettf84631a2014-01-07 18:21:26 -0800642 if 'target' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -0700643 raise common_util.DevServerHTTPError(500, 'Error: target= is required!')
Scott Zawalski16954532012-03-20 15:31:36 -0400644 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700645 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-07 18:21:26 -0800646 updater.static_dir, kwargs['target'],
647 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700648 except common_util.CommonUtilError as errmsg:
Chris Sosa4b951602014-04-09 20:26:07 -0700649 raise common_util.DevServerHTTPError(500, str(errmsg))
Scott Zawalski16954532012-03-20 15:31:36 -0400650
651 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800652 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500653 """Return a control file or a list of all known control files.
654
655 Example URL:
656 To List all control files:
beepsbd337242013-07-09 22:44:06 -0700657 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
658 To List all control files for, say, the bvt suite:
659 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -0500660 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500661 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 -0500662
663 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500664 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500665 control_path: If you want the contents of a control file set this
666 to the path. E.g. client/site_tests/sleeptest/control
667 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -0700668 suite_name: If control_path is not specified but a suite_name is
669 specified, list the control files belonging to that suite instead of
670 all control files. The empty string for suite_name will list all control
671 files for the build.
Don Garrettf84631a2014-01-07 18:21:26 -0800672
Scott Zawalski4647ce62012-01-03 17:17:28 -0500673 Returns:
674 Contents of a control file if control_path is provided.
675 A list of control files if no control_path is provided.
676 """
Don Garrettf84631a2014-01-07 18:21:26 -0800677 if not kwargs:
Scott Zawalski4647ce62012-01-03 17:17:28 -0500678 return _PrintDocStringAsHTML(self.controlfiles)
679
Don Garrettf84631a2014-01-07 18:21:26 -0800680 if 'build' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -0700681 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -0500682
Don Garrettf84631a2014-01-07 18:21:26 -0800683 if 'control_path' not in kwargs:
684 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-09 22:44:06 -0700685 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-07 18:21:26 -0800686 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-09 22:44:06 -0700687 else:
688 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-07 18:21:26 -0800689 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -0500690 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700691 return common_util.GetControlFile(
Don Garrettf84631a2014-01-07 18:21:26 -0800692 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -0800693
694 @cherrypy.expose
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700695 def xbuddy_translate(self, *args):
696 """Translates an xBuddy path to a real path to artifact if it exists.
697
698 Args:
699 An xbuddy path in the form of {local|remote}/build_id/artifact.
700
701 Returns:
702 build_id/artifact
703 """
704 build_id, filename = self._xbuddy.Translate(args)
705 response = os.path.join(build_id, filename)
706 _Log('Path translation requested, returning: %s', response)
707 return response
708
709 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -0700710 def xbuddy(self, *args, **kwargs):
711 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -0700712
713 Args:
joycheneaf4cfc2013-07-02 08:38:57 -0700714 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -0700715 components of the path. The path can be understood as
716 "{local|remote}/build_id/artifact" where build_id is composed of
717 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -0700718
joychen121fc9b2013-08-02 14:30:30 -0700719 The first path element is optional, and can be "remote" or "local"
720 If local (the default), devserver will not attempt to access Google
721 Storage, and will only search the static directory for the files.
722 If remote, devserver will try to obtain the artifact off GS if it's
723 not found locally.
724 The board is the familiar board name, optionally suffixed.
725 The version can be the google storage version number, and may also be
726 any of a number of xBuddy defined version aliases that will be
727 translated into the latest built image that fits the description.
728 Defaults to latest.
729 The artifact is one of a number of image or artifact aliases used by
730 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -0700731
732 Kwargs:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800733 for_update: {true|false}
734 if true, pregenerates the update payloads for the image,
735 and returns the update uri to pass to the
736 update_engine_client.
joychen3cb228e2013-06-12 12:13:13 -0700737 return_dir: {true|false}
738 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800739 relative_path: {true|false}
740 if set to true, returns the relative path to the payload
741 directory from static_dir.
joychen3cb228e2013-06-12 12:13:13 -0700742 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -0700743 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -0700744 or
joycheneaf4cfc2013-07-02 08:38:57 -0700745 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -0700746
747 Returns:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800748 If |for_update|, returns a redirect to the image or update file
749 on the devserver. E.g.,
750 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
751 chromium-test-image.bin
752 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
753 http://host:port/static/x86-generic-release/R26-4000.0.0/
754 If |relative_path| is true, return a relative path the folder where the
755 payloads are. E.g.,
756 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 12:13:13 -0700757 """
Chris Sosa75490802013-09-30 17:21:45 -0700758 boolean_string = kwargs.get('for_update')
759 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800760 boolean_string = kwargs.get('return_dir')
761 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
762 boolean_string = kwargs.get('relative_path')
763 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -0700764
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800765 if return_dir and relative_path:
Chris Sosa4b951602014-04-09 20:26:07 -0700766 raise common_util.DevServerHTTPError(
767 500, 'Cannot specify both return_dir and relative_path')
Chris Sosa75490802013-09-30 17:21:45 -0700768
769 # For updates, we optimize downloading of test images.
770 file_name = None
771 build_id = None
772 if for_update:
773 try:
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700774 build_id = self._xbuddy.StageTestArtifactsForUpdate(args)
Chris Sosa75490802013-09-30 17:21:45 -0700775 except build_artifact.ArtifactDownloadError:
776 build_id = None
777
778 if not build_id:
779 build_id, file_name = self._xbuddy.Get(args)
780
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800781 if for_update:
782 _Log('Payload generation triggered by request')
783 # Forces payload to be in cache and symlinked into build_id dir.
Chris Sosa75490802013-09-30 17:21:45 -0700784 updater.GetUpdateForLabel(autoupdate.FORCED_UPDATE, build_id,
785 image_name=file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800786
787 response = None
788 if return_dir:
789 response = os.path.join(cherrypy.request.base, 'static', build_id)
790 _Log('Directory requested, returning: %s', response)
791 elif relative_path:
792 response = build_id
793 _Log('Relative path requested, returning: %s', response)
794 elif for_update:
795 response = os.path.join(cherrypy.request.base, 'update', build_id)
796 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 12:13:13 -0700797 else:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800798 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 14:30:30 -0700799 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800800 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 14:30:30 -0700801 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -0700802
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800803 return response
804
joychen3cb228e2013-06-12 12:13:13 -0700805 @cherrypy.expose
806 def xbuddy_list(self):
807 """Lists the currently available images & time since last access.
808
Gilad Arnold452fd272014-02-04 11:09:28 -0800809 Returns:
810 A string representation of a list of tuples [(build_id, time since last
811 access),...]
joychen3cb228e2013-06-12 12:13:13 -0700812 """
813 return self._xbuddy.List()
814
815 @cherrypy.expose
816 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 11:09:28 -0800817 """Returns the number of images cached by xBuddy."""
joychen3cb228e2013-06-12 12:13:13 -0700818 return self._xbuddy.Capacity()
819
820 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700821 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700822 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700823 return ('Welcome to the Dev Server!<br>\n'
824 '<br>\n'
825 'Here are the available methods, click for documentation:<br>\n'
826 '<br>\n'
827 '%s' %
828 '<br>\n'.join(
829 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700830 for name in _FindExposedMethods(
831 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700832
833 @cherrypy.expose
834 def doc(self, *args):
835 """Shows the documentation for available methods / URLs.
836
837 Example:
838 http://myhost/doc/update
839 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700840 name = '/'.join(args)
841 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700842 if not method:
843 raise DevServerError("No exposed method named `%s'" % name)
844 if not method.__doc__:
845 raise DevServerError("No documentation for exposed method `%s'" % name)
846 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -0700847
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700848 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700849 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700850 """Handles an update check from a Chrome OS client.
851
852 The HTTP request should contain the standard Omaha-style XML blob. The URL
853 line may contain an additional intermediate path to the update payload.
854
joychen121fc9b2013-08-02 14:30:30 -0700855 This request can be handled in one of 4 ways, depending on the devsever
856 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -0700857
joychen121fc9b2013-08-02 14:30:30 -0700858 1. No intermediate path
859 If no intermediate path is given, the default behavior is to generate an
860 update payload from the latest test image locally built for the board
861 specified in the xml. Devserver serves the generated payload.
862
863 2. Path explicitly invokes XBuddy
864 If there is a path given, it can explicitly invoke xbuddy by prefixing it
865 with 'xbuddy'. This path is then used to acquire an image binary for the
866 devserver to generate an update payload from. Devserver then serves this
867 payload.
868
869 3. Path is left for the devserver to interpret.
870 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
871 to generate a payload from the test image in that directory and serve it.
872
873 4. The devserver is in a 'forced' mode. TO BE DEPRECATED
874 This comes from the usage of --forced_payload or --image when starting the
875 devserver. No matter what path (or no path) gets passed in, devserver will
876 serve the update payload (--forced_payload) or generate an update payload
877 from the image (--image).
878
879 Examples:
880 1. No intermediate path
881 update_engine_client --omaha_url=http://myhost/update
882 This generates an update payload from the latest test image locally built
883 for the board specified in the xml.
884
885 2. Explicitly invoke xbuddy
886 update_engine_client --omaha_url=
887 http://myhost/update/xbuddy/remote/board/version/dev
888 This would go to GS to download the dev image for the board, from which
889 the devserver would generate a payload to serve.
890
891 3. Give a path for devserver to interpret
892 update_engine_client --omaha_url=http://myhost/update/some/random/path
893 This would attempt, in order to:
894 a) Generate an update from a test image binary if found in
895 static_dir/some/random/path.
896 b) Serve an update payload found in static_dir/some/random/path.
897 c) Hope that some/random/path takes the form "board/version" and
898 and attempt to download an update payload for that board/version
899 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700900 """
joychen121fc9b2013-08-02 14:30:30 -0700901 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -0800902 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -0700903 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-11 19:49:01 -0700904
joychen121fc9b2013-08-02 14:30:30 -0700905 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700906
Dan Shif5ce2de2013-04-25 16:06:32 -0700907 @cherrypy.expose
908 def check_health(self):
909 """Collect the health status of devserver to see if it's ready for staging.
910
Gilad Arnold452fd272014-02-04 11:09:28 -0800911 Returns:
912 A JSON dictionary containing all or some of the following fields:
913 free_disk (int): free disk space in GB
914 staging_thread_count (int): number of devserver threads currently staging
915 an image
Dan Shif5ce2de2013-04-25 16:06:32 -0700916 """
917 # Get free disk space.
918 stat = os.statvfs(updater.static_dir)
919 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
920
921 return json.dumps({
922 'free_disk': free_disk,
Dan Shi59ae7092013-06-04 14:37:27 -0700923 'staging_thread_count': DevServerRoot._staging_thread_count,
Dan Shif5ce2de2013-04-25 16:06:32 -0700924 })
925
926
Chris Sosadbc20082012-12-10 13:39:11 -0800927def _CleanCache(cache_dir, wipe):
928 """Wipes any excess cached items in the cache_dir.
929
930 Args:
931 cache_dir: the directory we are wiping from.
932 wipe: If True, wipe all the contents -- not just the excess.
933 """
934 if wipe:
935 # Clear the cache and exit on error.
936 cmd = 'rm -rf %s/*' % cache_dir
937 if os.system(cmd) != 0:
938 _Log('Failed to clear the cache with %s' % cmd)
939 sys.exit(1)
940 else:
941 # Clear all but the last N cached updates
942 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
943 (cache_dir, CACHED_ENTRIES))
944 if os.system(cmd) != 0:
945 _Log('Failed to clean up old delta cache files with %s' % cmd)
946 sys.exit(1)
947
948
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700949def _AddTestingOptions(parser):
950 group = optparse.OptionGroup(
951 parser, 'Advanced Testing Options', 'These are used by test scripts and '
952 'developers writing integration tests utilizing the devserver. They are '
953 'not intended to be really used outside the scope of someone '
954 'knowledgable about the test.')
955 group.add_option('--exit',
956 action='store_true',
957 help='do not start the server (yet pregenerate/clear cache)')
958 group.add_option('--host_log',
959 action='store_true', default=False,
960 help='record history of host update events (/api/hostlog)')
961 group.add_option('--max_updates',
962 metavar='NUM', default= -1, type='int',
963 help='maximum number of update checks handled positively '
964 '(default: unlimited)')
965 group.add_option('--private_key',
966 metavar='PATH', default=None,
967 help='path to the private key in pem format. If this is set '
968 'the devserver will generate update payloads that are '
969 'signed with this key.')
David Zeuthen52ccd012013-10-31 12:58:26 -0700970 group.add_option('--private_key_for_metadata_hash_signature',
971 metavar='PATH', default=None,
972 help='path to the private key in pem format. If this is set '
973 'the devserver will sign the metadata hash with the given '
974 'key and transmit in the Omaha-style XML response.')
975 group.add_option('--public_key',
976 metavar='PATH', default=None,
977 help='path to the public key in pem format. If this is set '
978 'the devserver will transmit a base64 encoded version of '
979 'the content in the Omaha-style XML response.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700980 group.add_option('--proxy_port',
981 metavar='PORT', default=None, type='int',
982 help='port to have the client connect to -- basically the '
983 'devserver lies to the update to tell it to get the payload '
984 'from a different port that will proxy the request back to '
985 'the devserver. The proxy must be managed outside the '
986 'devserver.')
987 group.add_option('--remote_payload',
988 action='store_true', default=False,
Chris Sosa4b951602014-04-09 20:26:07 -0700989 help='Payload is being served from a remote machine. With '
990 'this setting enabled, this devserver instance serves as '
991 'just an Omaha server instance. In this mode, the '
992 'devserver enforces a few extra components of the Omaha '
Chris Sosafc715442014-04-09 20:45:23 -0700993 'protocol, such as hardware class, being sent.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700994 group.add_option('-u', '--urlbase',
995 metavar='URL',
996 help='base URL for update images, other than the '
997 'devserver. Use in conjunction with remote_payload.')
998 parser.add_option_group(group)
999
1000
1001def _AddUpdateOptions(parser):
1002 group = optparse.OptionGroup(
1003 parser, 'Autoupdate Options', 'These options can be used to change '
1004 'how the devserver either generates or serve update payloads. Please '
1005 'note that all of these option affect how a payload is generated and so '
1006 'do not work in archive-only mode.')
1007 group.add_option('--board',
1008 help='By default the devserver will create an update '
1009 'payload from the latest image built for the board '
1010 'a device that is requesting an update has. When we '
1011 'pre-generate an update (see below) and we do not specify '
1012 'another update_type option like image or payload, the '
1013 'devserver needs to know the board to generate the latest '
1014 'image for. This is that board.')
1015 group.add_option('--critical_update',
1016 action='store_true', default=False,
1017 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001018 group.add_option('--image',
1019 metavar='FILE',
1020 help='Generate and serve an update using this image to any '
1021 'device that requests an update.')
1022 group.add_option('--no_patch_kernel',
1023 dest='patch_kernel', action='store_false', default=True,
1024 help='When generating an update payload, do not patch the '
1025 'kernel with kernel verification blob from the stateful '
1026 'partition.')
1027 group.add_option('--payload',
1028 metavar='PATH',
1029 help='use the update payload from specified directory '
1030 '(update.gz).')
1031 group.add_option('-p', '--pregenerate_update',
1032 action='store_true', default=False,
1033 help='pre-generate the update payload before accepting '
1034 'update requests. Useful to help debug payload generation '
1035 'issues quickly. Also if an update payload will take a '
1036 'long time to generate, a client may timeout if you do not'
1037 'pregenerate the update.')
1038 group.add_option('--src_image',
1039 metavar='PATH', default='',
1040 help='If specified, delta updates will be generated using '
1041 'this image as the source image. Delta updates are when '
1042 'you are updating from a "source image" to a another '
1043 'image.')
1044 parser.add_option_group(group)
1045
1046
1047def _AddProductionOptions(parser):
1048 group = optparse.OptionGroup(
1049 parser, 'Advanced Server Options', 'These options can be used to changed '
1050 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001051 group.add_option('--clear_cache',
1052 action='store_true', default=False,
1053 help='At startup, removes all cached entries from the'
1054 'devserver\'s cache.')
1055 group.add_option('--logfile',
1056 metavar='PATH',
1057 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -07001058 group.add_option('--pidfile',
1059 metavar='PATH',
1060 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 11:04:13 -08001061 group.add_option('--portfile',
1062 metavar='PATH',
1063 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001064 group.add_option('--production',
1065 action='store_true', default=False,
1066 help='have the devserver use production values when '
1067 'starting up. This includes using more threads and '
1068 'performing less logging.')
1069 parser.add_option_group(group)
1070
1071
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001072def _MakeLogHandler(logfile):
1073 """Create a LogHandler instance used to log all messages."""
1074 hdlr_cls = handlers.TimedRotatingFileHandler
1075 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
1076 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -07001077 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001078 return hdlr
1079
1080
Chris Sosacde6bf42012-05-31 18:36:39 -07001081def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001082 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -08001083 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -07001084
1085 # get directory that the devserver is run from
1086 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -07001087 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -07001088 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001089 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -07001090 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -07001091 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001092 parser.add_option('--port',
1093 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 13:13:28 -08001094 help=('port for the dev server to use; if zero, binds to '
1095 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001096 parser.add_option('-t', '--test_image',
1097 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -07001098 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -07001099 parser.add_option('-x', '--xbuddy_manage_builds',
1100 action='store_true',
1101 default=False,
1102 help='If set, allow xbuddy to manage images in'
1103 'build/images.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001104 _AddProductionOptions(parser)
1105 _AddUpdateOptions(parser)
1106 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -07001107 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001108
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001109 # Handle options that must be set globally in cherrypy. Do this
1110 # work up front, because calls to _Log() below depend on this
1111 # initialization.
1112 if options.production:
1113 cherrypy.config.update({'environment': 'production'})
1114 if not options.logfile:
1115 cherrypy.config.update({'log.screen': True})
1116 else:
1117 cherrypy.config.update({'log.error_file': '',
1118 'log.access_file': ''})
1119 hdlr = _MakeLogHandler(options.logfile)
1120 # Pylint can't seem to process these two calls properly
1121 # pylint: disable=E1101
1122 cherrypy.log.access_log.addHandler(hdlr)
1123 cherrypy.log.error_log.addHandler(hdlr)
1124 # pylint: enable=E1101
1125
joychened64b222013-06-21 16:39:34 -07001126 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001127 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001128
joychened64b222013-06-21 16:39:34 -07001129 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001130 # If our devserver is only supposed to serve payloads, we shouldn't be
1131 # mucking with the cache at all. If the devserver hadn't previously
1132 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001133 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001134 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001135 else:
1136 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001137
Chris Sosadbc20082012-12-10 13:39:11 -08001138 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 16:39:34 -07001139 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001140
joychen121fc9b2013-08-02 14:30:30 -07001141 _xbuddy = xbuddy.XBuddy(options.xbuddy_manage_builds,
1142 options.board,
joychen121fc9b2013-08-02 14:30:30 -07001143 static_dir=options.static_dir)
Chris Sosa75490802013-09-30 17:21:45 -07001144 if options.clear_cache and options.xbuddy_manage_builds:
1145 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 14:30:30 -07001146
Chris Sosa6a3697f2013-01-29 16:44:43 -08001147 # We allow global use here to share with cherrypy classes.
1148 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001149 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001150 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001151 _xbuddy,
joychened64b222013-06-21 16:39:34 -07001152 static_dir=options.static_dir,
Andrew de los Reyes52620802010-04-12 13:40:07 -07001153 urlbase=options.urlbase,
Chris Sosa5d342a22010-09-28 16:54:41 -07001154 forced_image=options.image,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001155 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001156 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-28 23:42:37 -07001157 src_image=options.src_image,
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001158 patch_kernel=options.patch_kernel,
Chris Sosa08d55a22011-01-19 16:08:02 -08001159 board=options.board,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001160 copy_to_static_root=not options.exit,
1161 private_key=options.private_key,
David Zeuthen52ccd012013-10-31 12:58:26 -07001162 private_key_for_metadata_hash_signature=
1163 options.private_key_for_metadata_hash_signature,
1164 public_key=options.public_key,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001165 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001166 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001167 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001168 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001169 )
Chris Sosa7c931362010-10-11 19:49:01 -07001170
Chris Sosa6a3697f2013-01-29 16:44:43 -08001171 if options.pregenerate_update:
1172 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 15:46:22 -07001173
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001174 if options.exit:
1175 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001176
joychen3cb228e2013-06-12 12:13:13 -07001177 dev_server = DevServerRoot(_xbuddy)
1178
Gilad Arnold11fbef42014-02-10 11:04:13 -08001179 # Patch CherryPy to support binding to any available port (--port=0).
1180 cherrypy_ext.ZeroPortPatcher.DoPatch(cherrypy)
1181
Chris Sosa855b8932013-08-21 13:24:55 -07001182 if options.pidfile:
1183 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1184
Gilad Arnold11fbef42014-02-10 11:04:13 -08001185 if options.portfile:
1186 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1187
joychen3cb228e2013-06-12 12:13:13 -07001188 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001189
1190
1191if __name__ == '__main__':
1192 main()