blob: d0f1e1dd27e8793bafb673df3b1a2c5a25828751 [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,
Yu-Ju Hongaccb2e52014-05-01 11:24:22 -0700154 'engine.autoreload.on': False,
Chris Sosa7c931362010-10-11 19:49:01 -0700155 },
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700156 '/api':
157 {
158 # Gets rid of cherrypy parsing post file for args.
159 'request.process_request_body': False,
160 },
Chris Sosaa1ef0102010-10-21 16:22:35 -0700161 '/build':
162 {
163 'response.timeout': 100000,
164 },
Chris Sosa7c931362010-10-11 19:49:01 -0700165 '/update':
166 {
167 # Gets rid of cherrypy parsing post file for args.
168 'request.process_request_body': False,
Chris Sosaf65f4b92010-10-21 15:57:51 -0700169 'response.timeout': 10000,
Chris Sosa7c931362010-10-11 19:49:01 -0700170 },
171 # Sets up the static dir for file hosting.
172 '/static':
joychened64b222013-06-21 16:39:34 -0700173 { 'tools.staticdir.dir': options.static_dir,
Chris Sosa7c931362010-10-11 19:49:01 -0700174 'tools.staticdir.on': True,
Chris Sosaf65f4b92010-10-21 15:57:51 -0700175 'response.timeout': 10000,
Chris Sosa7c931362010-10-11 19:49:01 -0700176 },
177 }
Chris Sosa5f118ef2012-07-12 11:37:50 -0700178 if options.production:
Alex Miller93beca52013-07-30 19:25:09 -0700179 base_config['global'].update({'server.thread_pool': 150})
Chris Sosa7cd23202013-10-15 17:22:57 -0700180 # TODO(sosa): Do this more cleanly.
181 gsutil_util.GSUTIL_ATTEMPTS = 5
Scott Zawalski1c5e7cd2012-02-27 13:12:52 -0500182
Chris Sosa7c931362010-10-11 19:49:01 -0700183 return base_config
rtc@google.com64244662009-11-12 00:52:08 +0000184
Darin Petkove17164a2010-08-11 13:24:41 -0700185
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700186def _GetRecursiveMemberObject(root, member_list):
187 """Returns an object corresponding to a nested member list.
188
189 Args:
190 root: the root object to search
191 member_list: list of nested members to search
Don Garrettf84631a2014-01-07 18:21:26 -0800192
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700193 Returns:
194 An object corresponding to the member name list; None otherwise.
195 """
196 for member in member_list:
197 next_root = root.__class__.__dict__.get(member)
198 if not next_root:
199 return None
200 root = next_root
201 return root
202
203
204def _IsExposed(name):
205 """Returns True iff |name| has an `exposed' attribute and it is set."""
206 return hasattr(name, 'exposed') and name.exposed
207
208
Gilad Arnold748c8322012-10-12 09:51:35 -0700209def _GetExposedMethod(root, nested_member, ignored=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700210 """Returns a CherryPy-exposed method, if such exists.
211
212 Args:
213 root: the root object for searching
214 nested_member: a slash-joined path to the nested member
215 ignored: method paths to be ignored
Don Garrettf84631a2014-01-07 18:21:26 -0800216
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700217 Returns:
218 A function object corresponding to the path defined by |member_list| from
219 the |root| object, if the function is exposed and not ignored; None
220 otherwise.
221 """
Gilad Arnold748c8322012-10-12 09:51:35 -0700222 method = (not (ignored and nested_member in ignored) and
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700223 _GetRecursiveMemberObject(root, nested_member.split('/')))
224 if (method and type(method) == types.FunctionType and _IsExposed(method)):
225 return method
226
227
Gilad Arnold748c8322012-10-12 09:51:35 -0700228def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700229 """Finds exposed CherryPy methods.
230
231 Args:
232 root: the root object for searching
233 prefix: slash-joined chain of members leading to current object
234 unlisted: URLs to be excluded regardless of their exposed status
Don Garrettf84631a2014-01-07 18:21:26 -0800235
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700236 Returns:
237 List of exposed URLs that are not unlisted.
238 """
239 method_list = []
240 for member in sorted(root.__class__.__dict__.keys()):
241 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 09:51:35 -0700242 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700243 continue
244 member_obj = root.__class__.__dict__[member]
245 if _IsExposed(member_obj):
246 if type(member_obj) == types.FunctionType:
247 method_list.append(prefixed_member)
248 else:
249 method_list += _FindExposedMethods(
250 member_obj, prefixed_member, unlisted)
251 return method_list
252
253
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700254class ApiRoot(object):
255 """RESTful API for Dev Server information."""
256 exposed = True
257
258 @cherrypy.expose
259 def hostinfo(self, ip):
260 """Returns a JSON dictionary containing information about the given ip.
261
Gilad Arnold1b908392012-10-05 11:36:27 -0700262 Args:
263 ip: address of host whose info is requested
Don Garrettf84631a2014-01-07 18:21:26 -0800264
Gilad Arnold1b908392012-10-05 11:36:27 -0700265 Returns:
266 A JSON dictionary containing all or some of the following fields:
267 last_event_type (int): last update event type received
268 last_event_status (int): last update event status received
269 last_known_version (string): last known version reported in update ping
270 forced_update_label (string): update label to force next update ping to
271 use, set by setnextupdate
272 See the OmahaEvent class in update_engine/omaha_request_action.h for
273 event type and status code definitions. If the ip does not exist an empty
274 string is returned.
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700275
Gilad Arnold1b908392012-10-05 11:36:27 -0700276 Example URL:
277 http://myhost/api/hostinfo?ip=192.168.1.5
278 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700279 return updater.HandleHostInfoPing(ip)
280
281 @cherrypy.expose
Gilad Arnold286a0062012-01-12 13:47:02 -0800282 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 11:36:27 -0700283 """Returns a JSON object containing a log of host event.
284
285 Args:
286 ip: address of host whose event log is requested, or `all'
Don Garrettf84631a2014-01-07 18:21:26 -0800287
Gilad Arnold1b908392012-10-05 11:36:27 -0700288 Returns:
289 A JSON encoded list (log) of dictionaries (events), each of which
290 containing a `timestamp' and other event fields, as described under
291 /api/hostinfo.
292
293 Example URL:
294 http://myhost/api/hostlog?ip=192.168.1.5
295 """
Gilad Arnold286a0062012-01-12 13:47:02 -0800296 return updater.HandleHostLogPing(ip)
297
298 @cherrypy.expose
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700299 def setnextupdate(self, ip):
300 """Allows the response to the next update ping from a host to be set.
301
302 Takes the IP of the host and an update label as normally provided to the
Gilad Arnold1b908392012-10-05 11:36:27 -0700303 /update command.
304 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700305 body_length = int(cherrypy.request.headers['Content-Length'])
306 label = cherrypy.request.rfile.read(body_length)
307
308 if label:
309 label = label.strip()
310 if label:
311 return updater.HandleSetUpdatePing(ip, label)
Chris Sosa4b951602014-04-09 20:26:07 -0700312 raise common_util.DevServerHTTPError(400, 'No label provided.')
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700313
314
Gilad Arnold55a2a372012-10-02 09:46:32 -0700315 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800316 def fileinfo(self, *args):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700317 """Returns information about a given staged file.
318
319 Args:
Don Garrettf84631a2014-01-07 18:21:26 -0800320 args: path to the file inside the server's static staging directory
321
Gilad Arnold55a2a372012-10-02 09:46:32 -0700322 Returns:
323 A JSON encoded dictionary with information about the said file, which may
324 contain the following keys/values:
Gilad Arnold1b908392012-10-05 11:36:27 -0700325 size (int): the file size in bytes
326 sha1 (string): a base64 encoded SHA1 hash
327 sha256 (string): a base64 encoded SHA256 hash
328
329 Example URL:
330 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 09:46:32 -0700331 """
Don Garrettf84631a2014-01-07 18:21:26 -0800332 file_path = os.path.join(updater.static_dir, *args)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700333 if not os.path.exists(file_path):
334 raise DevServerError('file not found: %s' % file_path)
335 try:
336 file_size = os.path.getsize(file_path)
337 file_sha1 = common_util.GetFileSha1(file_path)
338 file_sha256 = common_util.GetFileSha256(file_path)
339 except os.error, e:
340 raise DevServerError('failed to get info for file %s: %s' %
Gilad Arnolde74b3812013-04-22 11:27:38 -0700341 (file_path, e))
342
343 is_delta = autoupdate.Autoupdate.IsDeltaFormatFile(file_path)
344
345 return json.dumps({
346 autoupdate.Autoupdate.SIZE_ATTR: file_size,
347 autoupdate.Autoupdate.SHA1_ATTR: file_sha1,
348 autoupdate.Autoupdate.SHA256_ATTR: file_sha256,
349 autoupdate.Autoupdate.ISDELTA_ATTR: is_delta
350 })
Gilad Arnold55a2a372012-10-02 09:46:32 -0700351
Chris Sosa76e44b92013-01-31 12:11:38 -0800352
David Rochberg7c79a812011-01-19 14:24:45 -0500353class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700354 """The Root Class for the Dev Server.
355
356 CherryPy works as follows:
357 For each method in this class, cherrpy interprets root/path
358 as a call to an instance of DevServerRoot->method_name. For example,
359 a call to http://myhost/build will call build. CherryPy automatically
360 parses http args and places them as keyword arguments in each method.
361 For paths http://myhost/update/dir1/dir2, you can use *args so that
362 cherrypy uses the update method and puts the extra paths in args.
363 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700364 # Method names that should not be listed on the index page.
365 _UNLISTED_METHODS = ['index', 'doc']
366
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700367 api = ApiRoot()
Chris Sosa7c931362010-10-11 19:49:01 -0700368
Dan Shi59ae7092013-06-04 14:37:27 -0700369 # Number of threads that devserver is staging images.
370 _staging_thread_count = 0
371 # Lock used to lock increasing/decreasing count.
372 _staging_thread_count_lock = threading.Lock()
373
joychen3cb228e2013-06-12 12:13:13 -0700374 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700375 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800376 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 12:13:13 -0700377 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 14:24:45 -0500378
Chris Sosa6b0c6172013-08-05 17:01:33 -0700379 @staticmethod
380 def _get_artifacts(kwargs):
381 """Returns a tuple of named and file artifacts given the stage rpc kwargs.
382
Don Garrettf84631a2014-01-07 18:21:26 -0800383 Raises:
384 DevserverError if no artifacts would be returned.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700385 """
386 artifacts = kwargs.get('artifacts')
387 files = kwargs.get('files')
388 if not artifacts and not files:
389 raise DevServerError('No artifacts specified.')
390
Chris Sosafa86b482013-09-04 11:30:36 -0700391 # Note we NEED to coerce files to a string as we get raw unicode from
392 # cherrypy and we treat files as strings elsewhere in the code.
393 return (str(artifacts).split(',') if artifacts else [],
394 str(files).split(',') if files else [])
Chris Sosa6b0c6172013-08-05 17:01:33 -0700395
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700396 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500397 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700398 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700399 import builder
400 if self._builder is None:
401 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500402 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700403
Chris Sosacde6bf42012-05-31 18:36:39 -0700404 @staticmethod
405 def _canonicalize_archive_url(archive_url):
406 """Canonicalizes archive_url strings.
407
408 Raises:
409 DevserverError: if archive_url is not set.
410 """
411 if archive_url:
Chris Sosa76e44b92013-01-31 12:11:38 -0800412 if not archive_url.startswith('gs://'):
Don Garrett8ccab732013-08-30 09:13:59 -0700413 raise DevServerError("Archive URL isn't from Google Storage (%s) ." %
414 archive_url)
Chris Sosa76e44b92013-01-31 12:11:38 -0800415
Chris Sosacde6bf42012-05-31 18:36:39 -0700416 return archive_url.rstrip('/')
417 else:
418 raise DevServerError("Must specify an archive_url in the request")
419
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700420 @cherrypy.expose
Dan Shif8eb0d12013-08-01 17:52:06 -0700421 def is_staged(self, **kwargs):
422 """Check if artifacts have been downloaded.
423
Chris Sosa6b0c6172013-08-05 17:01:33 -0700424 async: True to return without waiting for download to complete.
425 artifacts: Comma separated list of named artifacts to download.
426 These are defined in artifact_info and have their implementation
427 in build_artifact.py.
428 files: Comma separated list of file artifacts to stage. These
429 will be available as is in the corresponding static directory with no
430 custom post-processing.
431
432 returns: True of all artifacts are staged.
Dan Shif8eb0d12013-08-01 17:52:06 -0700433
434 Example:
435 To check if autotest and test_suites are staged:
436 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
437 artifacts=autotest,test_suites
438 """
439 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
Chris Sosa6b0c6172013-08-05 17:01:33 -0700440 artifacts, files = self._get_artifacts(kwargs)
Dan Shif8eb0d12013-08-01 17:52:06 -0700441 return str(downloader.Downloader(updater.static_dir, archive_url).IsStaged(
Chris Sosa6b0c6172013-08-05 17:01:33 -0700442 artifacts, files))
Dan Shi59ae7092013-06-04 14:37:27 -0700443
Chris Sosa76e44b92013-01-31 12:11:38 -0800444 @cherrypy.expose
Prashanth Ba06d2d22014-03-07 15:35:19 -0800445 def list_image_dir(self, **kwargs):
446 """Take an archive url and list the contents in its staged directory.
447
448 Args:
449 kwargs:
450 archive_url: Google Storage URL for the build.
451
452 Example:
453 To list the contents of where this devserver should have staged
454 gs://image-archive/<board>-release/<build> call:
455 http://devserver_url:<port>/list_image_dir?archive_url=<gs://..>
456
457 Returns:
458 A string with information about the contents of the image directory.
459 """
460 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
461 download_helper = downloader.Downloader(updater.static_dir, archive_url)
462 try:
463 image_dir_contents = download_helper.ListBuildDir()
464 except build_artifact.ArtifactDownloadError as e:
465 return 'Cannot list the contents of staged artifacts. %s' % e
466 if not image_dir_contents:
467 return '%s has not been staged on this devserver.' % archive_url
468 return image_dir_contents
469
470 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800471 def stage(self, **kwargs):
472 """Downloads and caches the artifacts from Google Storage URL.
473
474 Downloads and caches the artifacts Google Storage URL. Returns once these
475 have been downloaded on the devserver. A call to this will attempt to cache
476 non-specified artifacts in the background for the given from the given URL
477 following the principle of spatial locality. Spatial locality of different
478 artifacts is explicitly defined in the build_artifact module.
479
480 These artifacts will then be available from the static/ sub-directory of
481 the devserver.
482
483 Args:
484 archive_url: Google Storage URL for the build.
Dan Shif8eb0d12013-08-01 17:52:06 -0700485 async: True to return without waiting for download to complete.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700486 artifacts: Comma separated list of named artifacts to download.
487 These are defined in artifact_info and have their implementation
488 in build_artifact.py.
489 files: Comma separated list of files to stage. These
490 will be available as is in the corresponding static directory with no
491 custom post-processing.
Chris Sosa76e44b92013-01-31 12:11:38 -0800492
493 Example:
494 To download the autotest and test suites tarballs:
495 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
496 artifacts=autotest,test_suites
497 To download the full update payload:
498 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
499 artifacts=full_payload
Chris Sosa6b0c6172013-08-05 17:01:33 -0700500 To download just a file called blah.bin:
501 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
502 files=blah.bin
Chris Sosa76e44b92013-01-31 12:11:38 -0800503
504 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 16:39:34 -0700505 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 12:11:38 -0800506
507 Note for this example, relative path is the archive_url stripped of its
508 basename i.e. path/ in the examples above. Specific example:
509
510 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
511
512 Will get staged to:
513
joychened64b222013-06-21 16:39:34 -0700514 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Chris Sosa76e44b92013-01-31 12:11:38 -0800515 """
Chris Sosacde6bf42012-05-31 18:36:39 -0700516 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
Dan Shif8eb0d12013-08-01 17:52:06 -0700517 async = kwargs.get('async', False)
Chris Sosa6b0c6172013-08-05 17:01:33 -0700518 artifacts, files = self._get_artifacts(kwargs)
Dan Shi59ae7092013-06-04 14:37:27 -0700519 with DevServerRoot._staging_thread_count_lock:
520 DevServerRoot._staging_thread_count += 1
521 try:
Chris Sosa6b0c6172013-08-05 17:01:33 -0700522 downloader.Downloader(updater.static_dir, archive_url).Download(
523 artifacts, files, async=async)
Dan Shi59ae7092013-06-04 14:37:27 -0700524 finally:
525 with DevServerRoot._staging_thread_count_lock:
526 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800527 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700528
529 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -0800530 def setup_telemetry(self, **kwargs):
531 """Extracts and sets up telemetry
532
533 This method goes through the telemetry deps packages, and stages them on
534 the devserver to be used by the drones and the telemetry tests.
535
536 Args:
537 archive_url: Google Storage URL for the build.
538
539 Returns:
540 Path to the source folder for the telemetry codebase once it is staged.
541 """
542 archive_url = kwargs.get('archive_url')
543 self.stage(archive_url=archive_url, artifacts='autotest')
544
545 build = '/'.join(downloader.Downloader.ParseUrl(archive_url))
546 build_path = os.path.join(updater.static_dir, build)
547 deps_path = os.path.join(build_path, 'autotest/packages')
548 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
549 src_folder = os.path.join(telemetry_path, 'src')
550
551 with self._telemetry_lock_dict.lock(telemetry_path):
552 if os.path.exists(src_folder):
553 # Telemetry is already fully stage return
554 return src_folder
555
556 common_util.MkDirP(telemetry_path)
557
558 # Copy over the required deps tar balls to the telemetry directory.
559 for dep in TELEMETRY_DEPS:
560 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -0700561 if not os.path.exists(dep_path):
562 # This dep does not exist (could be new), do not extract it.
563 continue
Simran Basi4baad082013-02-14 13:39:18 -0800564 try:
565 common_util.ExtractTarball(dep_path, telemetry_path)
566 except common_util.CommonUtilError as e:
567 shutil.rmtree(telemetry_path)
568 raise DevServerError(str(e))
569
570 # By default all the tarballs extract to test_src but some parts of
571 # the telemetry code specifically hardcoded to exist inside of 'src'.
572 test_src = os.path.join(telemetry_path, 'test_src')
573 try:
574 shutil.move(test_src, src_folder)
575 except shutil.Error:
576 # This can occur if src_folder already exists. Remove and retry move.
577 shutil.rmtree(src_folder)
578 raise DevServerError('Failure in telemetry setup for build %s. Appears'
579 ' that the test_src to src move failed.' % build)
580
581 return src_folder
582
583 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800584 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -0700585 """Symbolicates a minidump using pre-downloaded symbols, returns it.
586
587 Callers will need to POST to this URL with a body of MIME-type
588 "multipart/form-data".
589 The body should include a single argument, 'minidump', containing the
590 binary-formatted minidump to symbolicate.
591
Chris Masone816e38c2012-05-02 12:22:36 -0700592 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -0800593 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -0700594 minidump: The binary minidump file to symbolicate.
595 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800596 # Ensure the symbols have been staged.
597 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
598 if self.stage(archive_url=archive_url, artifacts='symbols') != 'Success':
599 raise DevServerError('Failed to stage symbols for %s' % archive_url)
600
Chris Masone816e38c2012-05-02 12:22:36 -0700601 to_return = ''
602 with tempfile.NamedTemporaryFile() as local:
603 while True:
604 data = minidump.file.read(8192)
605 if not data:
606 break
607 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -0800608
Chris Masone816e38c2012-05-02 12:22:36 -0700609 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -0800610
611 symbols_directory = os.path.join(downloader.Downloader.GetBuildDir(
612 updater.static_dir, archive_url), 'debug', 'breakpad')
613
614 stackwalk = subprocess.Popen(
615 ['minidump_stackwalk', local.name, symbols_directory],
616 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
617
Chris Masone816e38c2012-05-02 12:22:36 -0700618 to_return, error_text = stackwalk.communicate()
619 if stackwalk.returncode != 0:
620 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
621 error_text, stackwalk.returncode))
622
623 return to_return
624
625 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800626 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 15:31:36 -0400627 """Return a string representing the latest build for a given target.
628
629 Args:
630 target: The build target, typically a combination of the board and the
631 type of build e.g. x86-mario-release.
632 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
633 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-07 18:21:26 -0800634
Scott Zawalski16954532012-03-20 15:31:36 -0400635 Returns:
636 A string representation of the latest build if one exists, i.e.
637 R19-1993.0.0-a1-b1480.
638 An empty string if no latest could be found.
639 """
Don Garrettf84631a2014-01-07 18:21:26 -0800640 if not kwargs:
Scott Zawalski16954532012-03-20 15:31:36 -0400641 return _PrintDocStringAsHTML(self.latestbuild)
642
Don Garrettf84631a2014-01-07 18:21:26 -0800643 if 'target' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -0700644 raise common_util.DevServerHTTPError(500, 'Error: target= is required!')
Scott Zawalski16954532012-03-20 15:31:36 -0400645 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700646 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-07 18:21:26 -0800647 updater.static_dir, kwargs['target'],
648 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700649 except common_util.CommonUtilError as errmsg:
Chris Sosa4b951602014-04-09 20:26:07 -0700650 raise common_util.DevServerHTTPError(500, str(errmsg))
Scott Zawalski16954532012-03-20 15:31:36 -0400651
652 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800653 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500654 """Return a control file or a list of all known control files.
655
656 Example URL:
657 To List all control files:
beepsbd337242013-07-09 22:44:06 -0700658 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
659 To List all control files for, say, the bvt suite:
660 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -0500661 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500662 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 -0500663
664 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500665 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500666 control_path: If you want the contents of a control file set this
667 to the path. E.g. client/site_tests/sleeptest/control
668 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -0700669 suite_name: If control_path is not specified but a suite_name is
670 specified, list the control files belonging to that suite instead of
671 all control files. The empty string for suite_name will list all control
672 files for the build.
Don Garrettf84631a2014-01-07 18:21:26 -0800673
Scott Zawalski4647ce62012-01-03 17:17:28 -0500674 Returns:
675 Contents of a control file if control_path is provided.
676 A list of control files if no control_path is provided.
677 """
Don Garrettf84631a2014-01-07 18:21:26 -0800678 if not kwargs:
Scott Zawalski4647ce62012-01-03 17:17:28 -0500679 return _PrintDocStringAsHTML(self.controlfiles)
680
Don Garrettf84631a2014-01-07 18:21:26 -0800681 if 'build' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -0700682 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -0500683
Don Garrettf84631a2014-01-07 18:21:26 -0800684 if 'control_path' not in kwargs:
685 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-09 22:44:06 -0700686 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-07 18:21:26 -0800687 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-09 22:44:06 -0700688 else:
689 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-07 18:21:26 -0800690 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -0500691 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700692 return common_util.GetControlFile(
Don Garrettf84631a2014-01-07 18:21:26 -0800693 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -0800694
695 @cherrypy.expose
Simran Basi99e63c02014-05-20 10:39:52 -0700696 def xbuddy_translate(self, *args, **kwargs):
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700697 """Translates an xBuddy path to a real path to artifact if it exists.
698
699 Args:
Simran Basi99e63c02014-05-20 10:39:52 -0700700 args: An xbuddy path in the form of {local|remote}/build_id/artifact.
701 Local searches the devserver's static directory. Remote searches a
702 Google Storage image archive.
703
704 Kwargs:
705 image_dir: Google Storage image archive to search in if requesting a
706 remote artifact. If none uses the default bucket.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700707
708 Returns:
Simran Basi99e63c02014-05-20 10:39:52 -0700709 String in the format of build_id/artifact as stored on the local server
710 or in Google Storage.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700711 """
Simran Basi99e63c02014-05-20 10:39:52 -0700712 build_id, filename = self._xbuddy.Translate(
713 args, image_dir=kwargs.get('image_dir'))
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700714 response = os.path.join(build_id, filename)
715 _Log('Path translation requested, returning: %s', response)
716 return response
717
718 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -0700719 def xbuddy(self, *args, **kwargs):
720 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -0700721
722 Args:
joycheneaf4cfc2013-07-02 08:38:57 -0700723 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -0700724 components of the path. The path can be understood as
725 "{local|remote}/build_id/artifact" where build_id is composed of
726 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -0700727
joychen121fc9b2013-08-02 14:30:30 -0700728 The first path element is optional, and can be "remote" or "local"
729 If local (the default), devserver will not attempt to access Google
730 Storage, and will only search the static directory for the files.
731 If remote, devserver will try to obtain the artifact off GS if it's
732 not found locally.
733 The board is the familiar board name, optionally suffixed.
734 The version can be the google storage version number, and may also be
735 any of a number of xBuddy defined version aliases that will be
736 translated into the latest built image that fits the description.
737 Defaults to latest.
738 The artifact is one of a number of image or artifact aliases used by
739 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -0700740
741 Kwargs:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800742 for_update: {true|false}
743 if true, pregenerates the update payloads for the image,
744 and returns the update uri to pass to the
745 update_engine_client.
joychen3cb228e2013-06-12 12:13:13 -0700746 return_dir: {true|false}
747 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800748 relative_path: {true|false}
749 if set to true, returns the relative path to the payload
750 directory from static_dir.
joychen3cb228e2013-06-12 12:13:13 -0700751 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -0700752 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -0700753 or
joycheneaf4cfc2013-07-02 08:38:57 -0700754 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -0700755
756 Returns:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800757 If |for_update|, returns a redirect to the image or update file
758 on the devserver. E.g.,
759 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
760 chromium-test-image.bin
761 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
762 http://host:port/static/x86-generic-release/R26-4000.0.0/
763 If |relative_path| is true, return a relative path the folder where the
764 payloads are. E.g.,
765 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 12:13:13 -0700766 """
Chris Sosa75490802013-09-30 17:21:45 -0700767 boolean_string = kwargs.get('for_update')
768 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800769 boolean_string = kwargs.get('return_dir')
770 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
771 boolean_string = kwargs.get('relative_path')
772 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -0700773
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800774 if return_dir and relative_path:
Chris Sosa4b951602014-04-09 20:26:07 -0700775 raise common_util.DevServerHTTPError(
776 500, 'Cannot specify both return_dir and relative_path')
Chris Sosa75490802013-09-30 17:21:45 -0700777
778 # For updates, we optimize downloading of test images.
779 file_name = None
780 build_id = None
781 if for_update:
782 try:
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700783 build_id = self._xbuddy.StageTestArtifactsForUpdate(args)
Chris Sosa75490802013-09-30 17:21:45 -0700784 except build_artifact.ArtifactDownloadError:
785 build_id = None
786
787 if not build_id:
788 build_id, file_name = self._xbuddy.Get(args)
789
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800790 if for_update:
791 _Log('Payload generation triggered by request')
792 # Forces payload to be in cache and symlinked into build_id dir.
Chris Sosa75490802013-09-30 17:21:45 -0700793 updater.GetUpdateForLabel(autoupdate.FORCED_UPDATE, build_id,
794 image_name=file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800795
796 response = None
797 if return_dir:
798 response = os.path.join(cherrypy.request.base, 'static', build_id)
799 _Log('Directory requested, returning: %s', response)
800 elif relative_path:
801 response = build_id
802 _Log('Relative path requested, returning: %s', response)
803 elif for_update:
804 response = os.path.join(cherrypy.request.base, 'update', build_id)
805 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 12:13:13 -0700806 else:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800807 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 14:30:30 -0700808 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800809 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 14:30:30 -0700810 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -0700811
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800812 return response
813
joychen3cb228e2013-06-12 12:13:13 -0700814 @cherrypy.expose
815 def xbuddy_list(self):
816 """Lists the currently available images & time since last access.
817
Gilad Arnold452fd272014-02-04 11:09:28 -0800818 Returns:
819 A string representation of a list of tuples [(build_id, time since last
820 access),...]
joychen3cb228e2013-06-12 12:13:13 -0700821 """
822 return self._xbuddy.List()
823
824 @cherrypy.expose
825 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 11:09:28 -0800826 """Returns the number of images cached by xBuddy."""
joychen3cb228e2013-06-12 12:13:13 -0700827 return self._xbuddy.Capacity()
828
829 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700830 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700831 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700832 return ('Welcome to the Dev Server!<br>\n'
833 '<br>\n'
834 'Here are the available methods, click for documentation:<br>\n'
835 '<br>\n'
836 '%s' %
837 '<br>\n'.join(
838 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700839 for name in _FindExposedMethods(
840 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700841
842 @cherrypy.expose
843 def doc(self, *args):
844 """Shows the documentation for available methods / URLs.
845
846 Example:
847 http://myhost/doc/update
848 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700849 name = '/'.join(args)
850 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700851 if not method:
852 raise DevServerError("No exposed method named `%s'" % name)
853 if not method.__doc__:
854 raise DevServerError("No documentation for exposed method `%s'" % name)
855 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -0700856
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700857 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700858 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700859 """Handles an update check from a Chrome OS client.
860
861 The HTTP request should contain the standard Omaha-style XML blob. The URL
862 line may contain an additional intermediate path to the update payload.
863
joychen121fc9b2013-08-02 14:30:30 -0700864 This request can be handled in one of 4 ways, depending on the devsever
865 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -0700866
joychen121fc9b2013-08-02 14:30:30 -0700867 1. No intermediate path
868 If no intermediate path is given, the default behavior is to generate an
869 update payload from the latest test image locally built for the board
870 specified in the xml. Devserver serves the generated payload.
871
872 2. Path explicitly invokes XBuddy
873 If there is a path given, it can explicitly invoke xbuddy by prefixing it
874 with 'xbuddy'. This path is then used to acquire an image binary for the
875 devserver to generate an update payload from. Devserver then serves this
876 payload.
877
878 3. Path is left for the devserver to interpret.
879 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
880 to generate a payload from the test image in that directory and serve it.
881
882 4. The devserver is in a 'forced' mode. TO BE DEPRECATED
883 This comes from the usage of --forced_payload or --image when starting the
884 devserver. No matter what path (or no path) gets passed in, devserver will
885 serve the update payload (--forced_payload) or generate an update payload
886 from the image (--image).
887
888 Examples:
889 1. No intermediate path
890 update_engine_client --omaha_url=http://myhost/update
891 This generates an update payload from the latest test image locally built
892 for the board specified in the xml.
893
894 2. Explicitly invoke xbuddy
895 update_engine_client --omaha_url=
896 http://myhost/update/xbuddy/remote/board/version/dev
897 This would go to GS to download the dev image for the board, from which
898 the devserver would generate a payload to serve.
899
900 3. Give a path for devserver to interpret
901 update_engine_client --omaha_url=http://myhost/update/some/random/path
902 This would attempt, in order to:
903 a) Generate an update from a test image binary if found in
904 static_dir/some/random/path.
905 b) Serve an update payload found in static_dir/some/random/path.
906 c) Hope that some/random/path takes the form "board/version" and
907 and attempt to download an update payload for that board/version
908 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700909 """
joychen121fc9b2013-08-02 14:30:30 -0700910 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -0800911 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -0700912 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-11 19:49:01 -0700913
joychen121fc9b2013-08-02 14:30:30 -0700914 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700915
Dan Shif5ce2de2013-04-25 16:06:32 -0700916 @cherrypy.expose
917 def check_health(self):
918 """Collect the health status of devserver to see if it's ready for staging.
919
Gilad Arnold452fd272014-02-04 11:09:28 -0800920 Returns:
921 A JSON dictionary containing all or some of the following fields:
922 free_disk (int): free disk space in GB
923 staging_thread_count (int): number of devserver threads currently staging
924 an image
Dan Shif5ce2de2013-04-25 16:06:32 -0700925 """
926 # Get free disk space.
927 stat = os.statvfs(updater.static_dir)
928 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
929
930 return json.dumps({
931 'free_disk': free_disk,
Dan Shi59ae7092013-06-04 14:37:27 -0700932 'staging_thread_count': DevServerRoot._staging_thread_count,
Dan Shif5ce2de2013-04-25 16:06:32 -0700933 })
934
935
Chris Sosadbc20082012-12-10 13:39:11 -0800936def _CleanCache(cache_dir, wipe):
937 """Wipes any excess cached items in the cache_dir.
938
939 Args:
940 cache_dir: the directory we are wiping from.
941 wipe: If True, wipe all the contents -- not just the excess.
942 """
943 if wipe:
944 # Clear the cache and exit on error.
945 cmd = 'rm -rf %s/*' % cache_dir
946 if os.system(cmd) != 0:
947 _Log('Failed to clear the cache with %s' % cmd)
948 sys.exit(1)
949 else:
950 # Clear all but the last N cached updates
951 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
952 (cache_dir, CACHED_ENTRIES))
953 if os.system(cmd) != 0:
954 _Log('Failed to clean up old delta cache files with %s' % cmd)
955 sys.exit(1)
956
957
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700958def _AddTestingOptions(parser):
959 group = optparse.OptionGroup(
960 parser, 'Advanced Testing Options', 'These are used by test scripts and '
961 'developers writing integration tests utilizing the devserver. They are '
962 'not intended to be really used outside the scope of someone '
963 'knowledgable about the test.')
964 group.add_option('--exit',
965 action='store_true',
966 help='do not start the server (yet pregenerate/clear cache)')
967 group.add_option('--host_log',
968 action='store_true', default=False,
969 help='record history of host update events (/api/hostlog)')
970 group.add_option('--max_updates',
971 metavar='NUM', default= -1, type='int',
972 help='maximum number of update checks handled positively '
973 '(default: unlimited)')
974 group.add_option('--private_key',
975 metavar='PATH', default=None,
976 help='path to the private key in pem format. If this is set '
977 'the devserver will generate update payloads that are '
978 'signed with this key.')
David Zeuthen52ccd012013-10-31 12:58:26 -0700979 group.add_option('--private_key_for_metadata_hash_signature',
980 metavar='PATH', default=None,
981 help='path to the private key in pem format. If this is set '
982 'the devserver will sign the metadata hash with the given '
983 'key and transmit in the Omaha-style XML response.')
984 group.add_option('--public_key',
985 metavar='PATH', default=None,
986 help='path to the public key in pem format. If this is set '
987 'the devserver will transmit a base64 encoded version of '
988 'the content in the Omaha-style XML response.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700989 group.add_option('--proxy_port',
990 metavar='PORT', default=None, type='int',
991 help='port to have the client connect to -- basically the '
992 'devserver lies to the update to tell it to get the payload '
993 'from a different port that will proxy the request back to '
994 'the devserver. The proxy must be managed outside the '
995 'devserver.')
996 group.add_option('--remote_payload',
997 action='store_true', default=False,
Chris Sosa4b951602014-04-09 20:26:07 -0700998 help='Payload is being served from a remote machine. With '
999 'this setting enabled, this devserver instance serves as '
1000 'just an Omaha server instance. In this mode, the '
1001 'devserver enforces a few extra components of the Omaha '
Chris Sosafc715442014-04-09 20:45:23 -07001002 'protocol, such as hardware class, being sent.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001003 group.add_option('-u', '--urlbase',
1004 metavar='URL',
1005 help='base URL for update images, other than the '
1006 'devserver. Use in conjunction with remote_payload.')
1007 parser.add_option_group(group)
1008
1009
1010def _AddUpdateOptions(parser):
1011 group = optparse.OptionGroup(
1012 parser, 'Autoupdate Options', 'These options can be used to change '
1013 'how the devserver either generates or serve update payloads. Please '
1014 'note that all of these option affect how a payload is generated and so '
1015 'do not work in archive-only mode.')
1016 group.add_option('--board',
1017 help='By default the devserver will create an update '
1018 'payload from the latest image built for the board '
1019 'a device that is requesting an update has. When we '
1020 'pre-generate an update (see below) and we do not specify '
1021 'another update_type option like image or payload, the '
1022 'devserver needs to know the board to generate the latest '
1023 'image for. This is that board.')
1024 group.add_option('--critical_update',
1025 action='store_true', default=False,
1026 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001027 group.add_option('--image',
1028 metavar='FILE',
1029 help='Generate and serve an update using this image to any '
1030 'device that requests an update.')
1031 group.add_option('--no_patch_kernel',
1032 dest='patch_kernel', action='store_false', default=True,
1033 help='When generating an update payload, do not patch the '
1034 'kernel with kernel verification blob from the stateful '
1035 'partition.')
1036 group.add_option('--payload',
1037 metavar='PATH',
1038 help='use the update payload from specified directory '
1039 '(update.gz).')
1040 group.add_option('-p', '--pregenerate_update',
1041 action='store_true', default=False,
1042 help='pre-generate the update payload before accepting '
1043 'update requests. Useful to help debug payload generation '
1044 'issues quickly. Also if an update payload will take a '
1045 'long time to generate, a client may timeout if you do not'
1046 'pregenerate the update.')
1047 group.add_option('--src_image',
1048 metavar='PATH', default='',
1049 help='If specified, delta updates will be generated using '
1050 'this image as the source image. Delta updates are when '
1051 'you are updating from a "source image" to a another '
1052 'image.')
1053 parser.add_option_group(group)
1054
1055
1056def _AddProductionOptions(parser):
1057 group = optparse.OptionGroup(
1058 parser, 'Advanced Server Options', 'These options can be used to changed '
1059 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001060 group.add_option('--clear_cache',
1061 action='store_true', default=False,
1062 help='At startup, removes all cached entries from the'
1063 'devserver\'s cache.')
1064 group.add_option('--logfile',
1065 metavar='PATH',
1066 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -07001067 group.add_option('--pidfile',
1068 metavar='PATH',
1069 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 11:04:13 -08001070 group.add_option('--portfile',
1071 metavar='PATH',
1072 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001073 group.add_option('--production',
1074 action='store_true', default=False,
1075 help='have the devserver use production values when '
1076 'starting up. This includes using more threads and '
1077 'performing less logging.')
1078 parser.add_option_group(group)
1079
1080
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001081def _MakeLogHandler(logfile):
1082 """Create a LogHandler instance used to log all messages."""
1083 hdlr_cls = handlers.TimedRotatingFileHandler
1084 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
1085 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -07001086 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001087 return hdlr
1088
1089
Chris Sosacde6bf42012-05-31 18:36:39 -07001090def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001091 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -08001092 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -07001093
1094 # get directory that the devserver is run from
1095 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -07001096 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -07001097 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001098 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -07001099 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -07001100 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001101 parser.add_option('--port',
1102 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 13:13:28 -08001103 help=('port for the dev server to use; if zero, binds to '
1104 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001105 parser.add_option('-t', '--test_image',
1106 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -07001107 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -07001108 parser.add_option('-x', '--xbuddy_manage_builds',
1109 action='store_true',
1110 default=False,
1111 help='If set, allow xbuddy to manage images in'
1112 'build/images.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001113 _AddProductionOptions(parser)
1114 _AddUpdateOptions(parser)
1115 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -07001116 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001117
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001118 # Handle options that must be set globally in cherrypy. Do this
1119 # work up front, because calls to _Log() below depend on this
1120 # initialization.
1121 if options.production:
1122 cherrypy.config.update({'environment': 'production'})
1123 if not options.logfile:
1124 cherrypy.config.update({'log.screen': True})
1125 else:
1126 cherrypy.config.update({'log.error_file': '',
1127 'log.access_file': ''})
1128 hdlr = _MakeLogHandler(options.logfile)
1129 # Pylint can't seem to process these two calls properly
1130 # pylint: disable=E1101
1131 cherrypy.log.access_log.addHandler(hdlr)
1132 cherrypy.log.error_log.addHandler(hdlr)
1133 # pylint: enable=E1101
1134
joychened64b222013-06-21 16:39:34 -07001135 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001136 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001137
joychened64b222013-06-21 16:39:34 -07001138 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001139 # If our devserver is only supposed to serve payloads, we shouldn't be
1140 # mucking with the cache at all. If the devserver hadn't previously
1141 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001142 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001143 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001144 else:
1145 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001146
Chris Sosadbc20082012-12-10 13:39:11 -08001147 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 16:39:34 -07001148 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001149
joychen121fc9b2013-08-02 14:30:30 -07001150 _xbuddy = xbuddy.XBuddy(options.xbuddy_manage_builds,
1151 options.board,
joychen121fc9b2013-08-02 14:30:30 -07001152 static_dir=options.static_dir)
Chris Sosa75490802013-09-30 17:21:45 -07001153 if options.clear_cache and options.xbuddy_manage_builds:
1154 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 14:30:30 -07001155
Chris Sosa6a3697f2013-01-29 16:44:43 -08001156 # We allow global use here to share with cherrypy classes.
1157 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001158 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001159 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001160 _xbuddy,
joychened64b222013-06-21 16:39:34 -07001161 static_dir=options.static_dir,
Andrew de los Reyes52620802010-04-12 13:40:07 -07001162 urlbase=options.urlbase,
Chris Sosa5d342a22010-09-28 16:54:41 -07001163 forced_image=options.image,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001164 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001165 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-28 23:42:37 -07001166 src_image=options.src_image,
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001167 patch_kernel=options.patch_kernel,
Chris Sosa08d55a22011-01-19 16:08:02 -08001168 board=options.board,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001169 copy_to_static_root=not options.exit,
1170 private_key=options.private_key,
David Zeuthen52ccd012013-10-31 12:58:26 -07001171 private_key_for_metadata_hash_signature=
1172 options.private_key_for_metadata_hash_signature,
1173 public_key=options.public_key,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001174 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001175 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001176 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001177 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001178 )
Chris Sosa7c931362010-10-11 19:49:01 -07001179
Chris Sosa6a3697f2013-01-29 16:44:43 -08001180 if options.pregenerate_update:
1181 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 15:46:22 -07001182
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001183 if options.exit:
1184 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001185
joychen3cb228e2013-06-12 12:13:13 -07001186 dev_server = DevServerRoot(_xbuddy)
1187
Gilad Arnold11fbef42014-02-10 11:04:13 -08001188 # Patch CherryPy to support binding to any available port (--port=0).
1189 cherrypy_ext.ZeroPortPatcher.DoPatch(cherrypy)
1190
Chris Sosa855b8932013-08-21 13:24:55 -07001191 if options.pidfile:
1192 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1193
Gilad Arnold11fbef42014-02-10 11:04:13 -08001194 if options.portfile:
1195 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1196
joychen3cb228e2013-06-12 12:13:13 -07001197 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001198
1199
1200if __name__ == '__main__':
1201 main()