blob: c398c9ecbae51e6dfbae291fb5c61e719a0cef81 [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
Simran Basief83d6a2014-08-28 14:32:01 -070064import devserver_constants
Chris Sosa47a7d4e2012-03-28 11:26:55 -070065import downloader
Chris Sosa7cd23202013-10-15 17:22:57 -070066import gsutil_util
Gilad Arnoldc65330c2012-09-20 15:17:48 -070067import log_util
joychen3cb228e2013-06-12 12:13:13 -070068import xbuddy
Gilad Arnoldc65330c2012-09-20 15:17:48 -070069
Gilad Arnoldc65330c2012-09-20 15:17:48 -070070# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080071def _Log(message, *args):
72 return log_util.LogWithTag('DEVSERVER', message, *args)
Chris Sosa0356d3b2010-09-16 15:46:22 -070073
Frank Farzan40160872011-12-12 18:39:18 -080074
Chris Sosa417e55d2011-01-25 16:40:48 -080075CACHED_ENTRIES = 12
Don Garrettf90edf02010-11-16 17:36:14 -080076
Simran Basi4baad082013-02-14 13:39:18 -080077TELEMETRY_FOLDER = 'telemetry_src'
78TELEMETRY_DEPS = ['dep-telemetry_dep.tar.bz2',
79 'dep-page_cycler_dep.tar.bz2',
Simran Basi0d078682013-03-22 16:40:04 -070080 'dep-chrome_test.tar.bz2',
81 'dep-perf_data_dep.tar.bz2']
Simran Basi4baad082013-02-14 13:39:18 -080082
Chris Sosa0356d3b2010-09-16 15:46:22 -070083# Sets up global to share between classes.
rtc@google.com21a5ca32009-11-04 18:23:23 +000084updater = None
rtc@google.comded22402009-10-26 22:36:21 +000085
J. Richard Barnette3d977b82013-04-23 11:05:19 -070086# Log rotation parameters. These settings correspond to once a week
J. Richard Barnette6dfa5342013-06-04 11:48:56 -070087# at midnight between Friday and Saturday, with about three months
88# of old logs kept for backup.
J. Richard Barnette3d977b82013-04-23 11:05:19 -070089#
90# For more, see the documentation for
91# logging.handlers.TimedRotatingFileHandler
J. Richard Barnette6dfa5342013-06-04 11:48:56 -070092_LOG_ROTATION_TIME = 'W4'
J. Richard Barnette3d977b82013-04-23 11:05:19 -070093_LOG_ROTATION_BACKUP = 13
94
Frank Farzan40160872011-12-12 18:39:18 -080095
Chris Sosa9164ca32012-03-28 11:04:50 -070096class DevServerError(Exception):
Chris Sosa47a7d4e2012-03-28 11:26:55 -070097 """Exception class used by this module."""
Chris Sosa47a7d4e2012-03-28 11:26:55 -070098
99
Scott Zawalski4647ce62012-01-03 17:17:28 -0500100def _LeadingWhiteSpaceCount(string):
101 """Count the amount of leading whitespace in a string.
102
103 Args:
104 string: The string to count leading whitespace in.
Don Garrettf84631a2014-01-07 18:21:26 -0800105
Scott Zawalski4647ce62012-01-03 17:17:28 -0500106 Returns:
107 number of white space chars before characters start.
108 """
109 matched = re.match('^\s+', string)
110 if matched:
111 return len(matched.group())
112
113 return 0
114
115
116def _PrintDocStringAsHTML(func):
117 """Make a functions docstring somewhat HTML style.
118
119 Args:
120 func: The function to return the docstring from.
Don Garrettf84631a2014-01-07 18:21:26 -0800121
Scott Zawalski4647ce62012-01-03 17:17:28 -0500122 Returns:
123 A string that is somewhat formated for a web browser.
124 """
125 # TODO(scottz): Make this parse Args/Returns in a prettier way.
126 # Arguments could be bolded and indented etc.
127 html_doc = []
128 for line in func.__doc__.splitlines():
129 leading_space = _LeadingWhiteSpaceCount(line)
130 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700131 line = '&nbsp;' * leading_space + line
Scott Zawalski4647ce62012-01-03 17:17:28 -0500132
133 html_doc.append('<BR>%s' % line)
134
135 return '\n'.join(html_doc)
136
137
Simran Basief83d6a2014-08-28 14:32:01 -0700138def _GetUpdateTimestampHandler(static_dir):
139 """Returns a handler to update directory staged.timestamp.
140
141 This handler resets the stage.timestamp whenever static content is accessed.
142
143 Args:
144 static_dir: Directory from which static content is being staged.
145
146 Returns:
147 A cherrypy handler to update the timestamp of accessed content.
148 """
149 def UpdateTimestampHandler():
150 if not '404' in cherrypy.response.status:
151 build_match = re.match(devserver_constants.STAGED_BUILD_REGEX,
152 cherrypy.request.path_info)
153 if build_match:
154 build_dir = os.path.join(static_dir, build_match.group('build'))
155 downloader.Downloader.TouchTimestampForStaged(build_dir)
156 return UpdateTimestampHandler
157
158
Chris Sosa7c931362010-10-11 19:49:01 -0700159def _GetConfig(options):
160 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800161
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800162 socket_host = '::'
Yu-Ju Hongc8d4af32013-11-12 15:14:26 -0800163 # Fall back to IPv4 when python is not configured with IPv6.
164 if not socket.has_ipv6:
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800165 socket_host = '0.0.0.0'
166
Simran Basief83d6a2014-08-28 14:32:01 -0700167 # Adds the UpdateTimestampHandler to cherrypy's tools. This tools executes
168 # on the on_end_resource hook. This hook is called once processing is
169 # complete and the response is ready to be returned.
170 cherrypy.tools.update_timestamp = cherrypy.Tool(
171 'on_end_resource', _GetUpdateTimestampHandler(options.static_dir))
172
Chris Sosa7c931362010-10-11 19:49:01 -0700173 base_config = { 'global':
174 { 'server.log_request_headers': True,
175 'server.protocol_version': 'HTTP/1.1',
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800176 'server.socket_host': socket_host,
Chris Sosa7c931362010-10-11 19:49:01 -0700177 'server.socket_port': int(options.port),
Chris Sosa374c62d2010-10-14 09:13:54 -0700178 'response.timeout': 6000,
Chris Sosa6fe23942012-07-02 15:44:46 -0700179 'request.show_tracebacks': True,
Chris Sosa72333d12012-06-13 11:28:05 -0700180 'server.socket_timeout': 60,
joychenecc02aa2013-07-17 18:27:35 -0700181 'server.thread_pool': 2,
Yu-Ju Hongaccb2e52014-05-01 11:24:22 -0700182 'engine.autoreload.on': False,
Chris Sosa7c931362010-10-11 19:49:01 -0700183 },
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700184 '/api':
185 {
186 # Gets rid of cherrypy parsing post file for args.
187 'request.process_request_body': False,
188 },
Chris Sosaa1ef0102010-10-21 16:22:35 -0700189 '/build':
190 {
191 'response.timeout': 100000,
192 },
Chris Sosa7c931362010-10-11 19:49:01 -0700193 '/update':
194 {
195 # Gets rid of cherrypy parsing post file for args.
196 'request.process_request_body': False,
Chris Sosaf65f4b92010-10-21 15:57:51 -0700197 'response.timeout': 10000,
Chris Sosa7c931362010-10-11 19:49:01 -0700198 },
199 # Sets up the static dir for file hosting.
200 '/static':
joychened64b222013-06-21 16:39:34 -0700201 { 'tools.staticdir.dir': options.static_dir,
Chris Sosa7c931362010-10-11 19:49:01 -0700202 'tools.staticdir.on': True,
Chris Sosaf65f4b92010-10-21 15:57:51 -0700203 'response.timeout': 10000,
Simran Basief83d6a2014-08-28 14:32:01 -0700204 'tools.update_timestamp.on': True,
Chris Sosa7c931362010-10-11 19:49:01 -0700205 },
206 }
Chris Sosa5f118ef2012-07-12 11:37:50 -0700207 if options.production:
Alex Miller93beca52013-07-30 19:25:09 -0700208 base_config['global'].update({'server.thread_pool': 150})
Chris Sosa7cd23202013-10-15 17:22:57 -0700209 # TODO(sosa): Do this more cleanly.
210 gsutil_util.GSUTIL_ATTEMPTS = 5
Scott Zawalski1c5e7cd2012-02-27 13:12:52 -0500211
Chris Sosa7c931362010-10-11 19:49:01 -0700212 return base_config
rtc@google.com64244662009-11-12 00:52:08 +0000213
Darin Petkove17164a2010-08-11 13:24:41 -0700214
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700215def _GetRecursiveMemberObject(root, member_list):
216 """Returns an object corresponding to a nested member list.
217
218 Args:
219 root: the root object to search
220 member_list: list of nested members to search
Don Garrettf84631a2014-01-07 18:21:26 -0800221
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700222 Returns:
223 An object corresponding to the member name list; None otherwise.
224 """
225 for member in member_list:
226 next_root = root.__class__.__dict__.get(member)
227 if not next_root:
228 return None
229 root = next_root
230 return root
231
232
233def _IsExposed(name):
234 """Returns True iff |name| has an `exposed' attribute and it is set."""
235 return hasattr(name, 'exposed') and name.exposed
236
237
Gilad Arnold748c8322012-10-12 09:51:35 -0700238def _GetExposedMethod(root, nested_member, ignored=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700239 """Returns a CherryPy-exposed method, if such exists.
240
241 Args:
242 root: the root object for searching
243 nested_member: a slash-joined path to the nested member
244 ignored: method paths to be ignored
Don Garrettf84631a2014-01-07 18:21:26 -0800245
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700246 Returns:
247 A function object corresponding to the path defined by |member_list| from
248 the |root| object, if the function is exposed and not ignored; None
249 otherwise.
250 """
Gilad Arnold748c8322012-10-12 09:51:35 -0700251 method = (not (ignored and nested_member in ignored) and
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700252 _GetRecursiveMemberObject(root, nested_member.split('/')))
253 if (method and type(method) == types.FunctionType and _IsExposed(method)):
254 return method
255
256
Gilad Arnold748c8322012-10-12 09:51:35 -0700257def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700258 """Finds exposed CherryPy methods.
259
260 Args:
261 root: the root object for searching
262 prefix: slash-joined chain of members leading to current object
263 unlisted: URLs to be excluded regardless of their exposed status
Don Garrettf84631a2014-01-07 18:21:26 -0800264
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700265 Returns:
266 List of exposed URLs that are not unlisted.
267 """
268 method_list = []
269 for member in sorted(root.__class__.__dict__.keys()):
270 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 09:51:35 -0700271 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700272 continue
273 member_obj = root.__class__.__dict__[member]
274 if _IsExposed(member_obj):
275 if type(member_obj) == types.FunctionType:
276 method_list.append(prefixed_member)
277 else:
278 method_list += _FindExposedMethods(
279 member_obj, prefixed_member, unlisted)
280 return method_list
281
282
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700283class ApiRoot(object):
284 """RESTful API for Dev Server information."""
285 exposed = True
286
287 @cherrypy.expose
288 def hostinfo(self, ip):
289 """Returns a JSON dictionary containing information about the given ip.
290
Gilad Arnold1b908392012-10-05 11:36:27 -0700291 Args:
292 ip: address of host whose info is requested
Don Garrettf84631a2014-01-07 18:21:26 -0800293
Gilad Arnold1b908392012-10-05 11:36:27 -0700294 Returns:
295 A JSON dictionary containing all or some of the following fields:
296 last_event_type (int): last update event type received
297 last_event_status (int): last update event status received
298 last_known_version (string): last known version reported in update ping
299 forced_update_label (string): update label to force next update ping to
300 use, set by setnextupdate
301 See the OmahaEvent class in update_engine/omaha_request_action.h for
302 event type and status code definitions. If the ip does not exist an empty
303 string is returned.
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700304
Gilad Arnold1b908392012-10-05 11:36:27 -0700305 Example URL:
306 http://myhost/api/hostinfo?ip=192.168.1.5
307 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700308 return updater.HandleHostInfoPing(ip)
309
310 @cherrypy.expose
Gilad Arnold286a0062012-01-12 13:47:02 -0800311 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 11:36:27 -0700312 """Returns a JSON object containing a log of host event.
313
314 Args:
315 ip: address of host whose event log is requested, or `all'
Don Garrettf84631a2014-01-07 18:21:26 -0800316
Gilad Arnold1b908392012-10-05 11:36:27 -0700317 Returns:
318 A JSON encoded list (log) of dictionaries (events), each of which
319 containing a `timestamp' and other event fields, as described under
320 /api/hostinfo.
321
322 Example URL:
323 http://myhost/api/hostlog?ip=192.168.1.5
324 """
Gilad Arnold286a0062012-01-12 13:47:02 -0800325 return updater.HandleHostLogPing(ip)
326
327 @cherrypy.expose
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700328 def setnextupdate(self, ip):
329 """Allows the response to the next update ping from a host to be set.
330
331 Takes the IP of the host and an update label as normally provided to the
Gilad Arnold1b908392012-10-05 11:36:27 -0700332 /update command.
333 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700334 body_length = int(cherrypy.request.headers['Content-Length'])
335 label = cherrypy.request.rfile.read(body_length)
336
337 if label:
338 label = label.strip()
339 if label:
340 return updater.HandleSetUpdatePing(ip, label)
Chris Sosa4b951602014-04-09 20:26:07 -0700341 raise common_util.DevServerHTTPError(400, 'No label provided.')
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700342
343
Gilad Arnold55a2a372012-10-02 09:46:32 -0700344 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800345 def fileinfo(self, *args):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700346 """Returns information about a given staged file.
347
348 Args:
Don Garrettf84631a2014-01-07 18:21:26 -0800349 args: path to the file inside the server's static staging directory
350
Gilad Arnold55a2a372012-10-02 09:46:32 -0700351 Returns:
352 A JSON encoded dictionary with information about the said file, which may
353 contain the following keys/values:
Gilad Arnold1b908392012-10-05 11:36:27 -0700354 size (int): the file size in bytes
355 sha1 (string): a base64 encoded SHA1 hash
356 sha256 (string): a base64 encoded SHA256 hash
357
358 Example URL:
359 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 09:46:32 -0700360 """
Don Garrettf84631a2014-01-07 18:21:26 -0800361 file_path = os.path.join(updater.static_dir, *args)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700362 if not os.path.exists(file_path):
363 raise DevServerError('file not found: %s' % file_path)
364 try:
365 file_size = os.path.getsize(file_path)
366 file_sha1 = common_util.GetFileSha1(file_path)
367 file_sha256 = common_util.GetFileSha256(file_path)
368 except os.error, e:
369 raise DevServerError('failed to get info for file %s: %s' %
Gilad Arnolde74b3812013-04-22 11:27:38 -0700370 (file_path, e))
371
372 is_delta = autoupdate.Autoupdate.IsDeltaFormatFile(file_path)
373
374 return json.dumps({
375 autoupdate.Autoupdate.SIZE_ATTR: file_size,
376 autoupdate.Autoupdate.SHA1_ATTR: file_sha1,
377 autoupdate.Autoupdate.SHA256_ATTR: file_sha256,
378 autoupdate.Autoupdate.ISDELTA_ATTR: is_delta
379 })
Gilad Arnold55a2a372012-10-02 09:46:32 -0700380
Chris Sosa76e44b92013-01-31 12:11:38 -0800381
David Rochberg7c79a812011-01-19 14:24:45 -0500382class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700383 """The Root Class for the Dev Server.
384
385 CherryPy works as follows:
386 For each method in this class, cherrpy interprets root/path
387 as a call to an instance of DevServerRoot->method_name. For example,
388 a call to http://myhost/build will call build. CherryPy automatically
389 parses http args and places them as keyword arguments in each method.
390 For paths http://myhost/update/dir1/dir2, you can use *args so that
391 cherrypy uses the update method and puts the extra paths in args.
392 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700393 # Method names that should not be listed on the index page.
394 _UNLISTED_METHODS = ['index', 'doc']
395
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700396 api = ApiRoot()
Chris Sosa7c931362010-10-11 19:49:01 -0700397
Dan Shi59ae7092013-06-04 14:37:27 -0700398 # Number of threads that devserver is staging images.
399 _staging_thread_count = 0
400 # Lock used to lock increasing/decreasing count.
401 _staging_thread_count_lock = threading.Lock()
402
joychen3cb228e2013-06-12 12:13:13 -0700403 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700404 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800405 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 12:13:13 -0700406 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 14:24:45 -0500407
Chris Sosa6b0c6172013-08-05 17:01:33 -0700408 @staticmethod
409 def _get_artifacts(kwargs):
410 """Returns a tuple of named and file artifacts given the stage rpc kwargs.
411
Don Garrettf84631a2014-01-07 18:21:26 -0800412 Raises:
413 DevserverError if no artifacts would be returned.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700414 """
415 artifacts = kwargs.get('artifacts')
416 files = kwargs.get('files')
417 if not artifacts and not files:
418 raise DevServerError('No artifacts specified.')
419
Chris Sosafa86b482013-09-04 11:30:36 -0700420 # Note we NEED to coerce files to a string as we get raw unicode from
421 # cherrypy and we treat files as strings elsewhere in the code.
422 return (str(artifacts).split(',') if artifacts else [],
423 str(files).split(',') if files else [])
Chris Sosa6b0c6172013-08-05 17:01:33 -0700424
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700425 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500426 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700427 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700428 import builder
429 if self._builder is None:
430 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500431 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700432
Chris Sosacde6bf42012-05-31 18:36:39 -0700433 @staticmethod
434 def _canonicalize_archive_url(archive_url):
435 """Canonicalizes archive_url strings.
436
437 Raises:
438 DevserverError: if archive_url is not set.
439 """
440 if archive_url:
Chris Sosa76e44b92013-01-31 12:11:38 -0800441 if not archive_url.startswith('gs://'):
Don Garrett8ccab732013-08-30 09:13:59 -0700442 raise DevServerError("Archive URL isn't from Google Storage (%s) ." %
443 archive_url)
Chris Sosa76e44b92013-01-31 12:11:38 -0800444
Chris Sosacde6bf42012-05-31 18:36:39 -0700445 return archive_url.rstrip('/')
446 else:
447 raise DevServerError("Must specify an archive_url in the request")
448
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700449 @cherrypy.expose
Dan Shif8eb0d12013-08-01 17:52:06 -0700450 def is_staged(self, **kwargs):
451 """Check if artifacts have been downloaded.
452
Chris Sosa6b0c6172013-08-05 17:01:33 -0700453 async: True to return without waiting for download to complete.
454 artifacts: Comma separated list of named artifacts to download.
455 These are defined in artifact_info and have their implementation
456 in build_artifact.py.
457 files: Comma separated list of file artifacts to stage. These
458 will be available as is in the corresponding static directory with no
459 custom post-processing.
460
461 returns: True of all artifacts are staged.
Dan Shif8eb0d12013-08-01 17:52:06 -0700462
463 Example:
464 To check if autotest and test_suites are staged:
465 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
466 artifacts=autotest,test_suites
467 """
468 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
Chris Sosa6b0c6172013-08-05 17:01:33 -0700469 artifacts, files = self._get_artifacts(kwargs)
Dan Shif8eb0d12013-08-01 17:52:06 -0700470 return str(downloader.Downloader(updater.static_dir, archive_url).IsStaged(
Chris Sosa6b0c6172013-08-05 17:01:33 -0700471 artifacts, files))
Dan Shi59ae7092013-06-04 14:37:27 -0700472
Chris Sosa76e44b92013-01-31 12:11:38 -0800473 @cherrypy.expose
Prashanth Ba06d2d22014-03-07 15:35:19 -0800474 def list_image_dir(self, **kwargs):
475 """Take an archive url and list the contents in its staged directory.
476
477 Args:
478 kwargs:
479 archive_url: Google Storage URL for the build.
480
481 Example:
482 To list the contents of where this devserver should have staged
483 gs://image-archive/<board>-release/<build> call:
484 http://devserver_url:<port>/list_image_dir?archive_url=<gs://..>
485
486 Returns:
487 A string with information about the contents of the image directory.
488 """
489 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
490 download_helper = downloader.Downloader(updater.static_dir, archive_url)
491 try:
492 image_dir_contents = download_helper.ListBuildDir()
493 except build_artifact.ArtifactDownloadError as e:
494 return 'Cannot list the contents of staged artifacts. %s' % e
495 if not image_dir_contents:
496 return '%s has not been staged on this devserver.' % archive_url
497 return image_dir_contents
498
499 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800500 def stage(self, **kwargs):
501 """Downloads and caches the artifacts from Google Storage URL.
502
503 Downloads and caches the artifacts Google Storage URL. Returns once these
504 have been downloaded on the devserver. A call to this will attempt to cache
505 non-specified artifacts in the background for the given from the given URL
506 following the principle of spatial locality. Spatial locality of different
507 artifacts is explicitly defined in the build_artifact module.
508
509 These artifacts will then be available from the static/ sub-directory of
510 the devserver.
511
512 Args:
513 archive_url: Google Storage URL for the build.
Dan Shif8eb0d12013-08-01 17:52:06 -0700514 async: True to return without waiting for download to complete.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700515 artifacts: Comma separated list of named artifacts to download.
516 These are defined in artifact_info and have their implementation
517 in build_artifact.py.
518 files: Comma separated list of files to stage. These
519 will be available as is in the corresponding static directory with no
520 custom post-processing.
Chris Sosa76e44b92013-01-31 12:11:38 -0800521
522 Example:
523 To download the autotest and test suites tarballs:
524 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
525 artifacts=autotest,test_suites
526 To download the full update payload:
527 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
528 artifacts=full_payload
Chris Sosa6b0c6172013-08-05 17:01:33 -0700529 To download just a file called blah.bin:
530 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
531 files=blah.bin
Chris Sosa76e44b92013-01-31 12:11:38 -0800532
533 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 16:39:34 -0700534 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 12:11:38 -0800535
536 Note for this example, relative path is the archive_url stripped of its
537 basename i.e. path/ in the examples above. Specific example:
538
539 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
540
541 Will get staged to:
542
joychened64b222013-06-21 16:39:34 -0700543 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Chris Sosa76e44b92013-01-31 12:11:38 -0800544 """
Chris Sosacde6bf42012-05-31 18:36:39 -0700545 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
Dan Shif8eb0d12013-08-01 17:52:06 -0700546 async = kwargs.get('async', False)
Chris Sosa6b0c6172013-08-05 17:01:33 -0700547 artifacts, files = self._get_artifacts(kwargs)
Dan Shi59ae7092013-06-04 14:37:27 -0700548 with DevServerRoot._staging_thread_count_lock:
549 DevServerRoot._staging_thread_count += 1
550 try:
Chris Sosa6b0c6172013-08-05 17:01:33 -0700551 downloader.Downloader(updater.static_dir, archive_url).Download(
552 artifacts, files, async=async)
Dan Shi59ae7092013-06-04 14:37:27 -0700553 finally:
554 with DevServerRoot._staging_thread_count_lock:
555 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800556 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700557
558 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -0800559 def setup_telemetry(self, **kwargs):
560 """Extracts and sets up telemetry
561
562 This method goes through the telemetry deps packages, and stages them on
563 the devserver to be used by the drones and the telemetry tests.
564
565 Args:
566 archive_url: Google Storage URL for the build.
567
568 Returns:
569 Path to the source folder for the telemetry codebase once it is staged.
570 """
571 archive_url = kwargs.get('archive_url')
Simran Basi4baad082013-02-14 13:39:18 -0800572
573 build = '/'.join(downloader.Downloader.ParseUrl(archive_url))
574 build_path = os.path.join(updater.static_dir, build)
575 deps_path = os.path.join(build_path, 'autotest/packages')
576 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
577 src_folder = os.path.join(telemetry_path, 'src')
578
579 with self._telemetry_lock_dict.lock(telemetry_path):
580 if os.path.exists(src_folder):
581 # Telemetry is already fully stage return
582 return src_folder
583
584 common_util.MkDirP(telemetry_path)
585
586 # Copy over the required deps tar balls to the telemetry directory.
587 for dep in TELEMETRY_DEPS:
588 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -0700589 if not os.path.exists(dep_path):
590 # This dep does not exist (could be new), do not extract it.
591 continue
Simran Basi4baad082013-02-14 13:39:18 -0800592 try:
593 common_util.ExtractTarball(dep_path, telemetry_path)
594 except common_util.CommonUtilError as e:
595 shutil.rmtree(telemetry_path)
596 raise DevServerError(str(e))
597
598 # By default all the tarballs extract to test_src but some parts of
599 # the telemetry code specifically hardcoded to exist inside of 'src'.
600 test_src = os.path.join(telemetry_path, 'test_src')
601 try:
602 shutil.move(test_src, src_folder)
603 except shutil.Error:
604 # This can occur if src_folder already exists. Remove and retry move.
605 shutil.rmtree(src_folder)
606 raise DevServerError('Failure in telemetry setup for build %s. Appears'
607 ' that the test_src to src move failed.' % build)
608
609 return src_folder
610
611 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800612 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -0700613 """Symbolicates a minidump using pre-downloaded symbols, returns it.
614
615 Callers will need to POST to this URL with a body of MIME-type
616 "multipart/form-data".
617 The body should include a single argument, 'minidump', containing the
618 binary-formatted minidump to symbolicate.
619
Chris Masone816e38c2012-05-02 12:22:36 -0700620 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -0800621 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -0700622 minidump: The binary minidump file to symbolicate.
623 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800624 # Ensure the symbols have been staged.
625 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
626 if self.stage(archive_url=archive_url, artifacts='symbols') != 'Success':
627 raise DevServerError('Failed to stage symbols for %s' % archive_url)
628
Chris Masone816e38c2012-05-02 12:22:36 -0700629 to_return = ''
630 with tempfile.NamedTemporaryFile() as local:
631 while True:
632 data = minidump.file.read(8192)
633 if not data:
634 break
635 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -0800636
Chris Masone816e38c2012-05-02 12:22:36 -0700637 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -0800638
639 symbols_directory = os.path.join(downloader.Downloader.GetBuildDir(
640 updater.static_dir, archive_url), 'debug', 'breakpad')
641
642 stackwalk = subprocess.Popen(
643 ['minidump_stackwalk', local.name, symbols_directory],
644 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
645
Chris Masone816e38c2012-05-02 12:22:36 -0700646 to_return, error_text = stackwalk.communicate()
647 if stackwalk.returncode != 0:
648 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
649 error_text, stackwalk.returncode))
650
651 return to_return
652
653 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800654 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 15:31:36 -0400655 """Return a string representing the latest build for a given target.
656
657 Args:
658 target: The build target, typically a combination of the board and the
659 type of build e.g. x86-mario-release.
660 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
661 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-07 18:21:26 -0800662
Scott Zawalski16954532012-03-20 15:31:36 -0400663 Returns:
664 A string representation of the latest build if one exists, i.e.
665 R19-1993.0.0-a1-b1480.
666 An empty string if no latest could be found.
667 """
Don Garrettf84631a2014-01-07 18:21:26 -0800668 if not kwargs:
Scott Zawalski16954532012-03-20 15:31:36 -0400669 return _PrintDocStringAsHTML(self.latestbuild)
670
Don Garrettf84631a2014-01-07 18:21:26 -0800671 if 'target' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -0700672 raise common_util.DevServerHTTPError(500, 'Error: target= is required!')
Scott Zawalski16954532012-03-20 15:31:36 -0400673 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700674 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-07 18:21:26 -0800675 updater.static_dir, kwargs['target'],
676 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700677 except common_util.CommonUtilError as errmsg:
Chris Sosa4b951602014-04-09 20:26:07 -0700678 raise common_util.DevServerHTTPError(500, str(errmsg))
Scott Zawalski16954532012-03-20 15:31:36 -0400679
680 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800681 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500682 """Return a control file or a list of all known control files.
683
684 Example URL:
685 To List all control files:
beepsbd337242013-07-09 22:44:06 -0700686 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
687 To List all control files for, say, the bvt suite:
688 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -0500689 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500690 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 -0500691
692 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500693 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500694 control_path: If you want the contents of a control file set this
695 to the path. E.g. client/site_tests/sleeptest/control
696 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -0700697 suite_name: If control_path is not specified but a suite_name is
698 specified, list the control files belonging to that suite instead of
699 all control files. The empty string for suite_name will list all control
700 files for the build.
Don Garrettf84631a2014-01-07 18:21:26 -0800701
Scott Zawalski4647ce62012-01-03 17:17:28 -0500702 Returns:
703 Contents of a control file if control_path is provided.
704 A list of control files if no control_path is provided.
705 """
Don Garrettf84631a2014-01-07 18:21:26 -0800706 if not kwargs:
Scott Zawalski4647ce62012-01-03 17:17:28 -0500707 return _PrintDocStringAsHTML(self.controlfiles)
708
Don Garrettf84631a2014-01-07 18:21:26 -0800709 if 'build' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -0700710 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -0500711
Don Garrettf84631a2014-01-07 18:21:26 -0800712 if 'control_path' not in kwargs:
713 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-09 22:44:06 -0700714 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-07 18:21:26 -0800715 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-09 22:44:06 -0700716 else:
717 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-07 18:21:26 -0800718 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -0500719 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700720 return common_util.GetControlFile(
Don Garrettf84631a2014-01-07 18:21:26 -0800721 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -0800722
723 @cherrypy.expose
Simran Basi99e63c02014-05-20 10:39:52 -0700724 def xbuddy_translate(self, *args, **kwargs):
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700725 """Translates an xBuddy path to a real path to artifact if it exists.
726
727 Args:
Simran Basi99e63c02014-05-20 10:39:52 -0700728 args: An xbuddy path in the form of {local|remote}/build_id/artifact.
729 Local searches the devserver's static directory. Remote searches a
730 Google Storage image archive.
731
732 Kwargs:
733 image_dir: Google Storage image archive to search in if requesting a
734 remote artifact. If none uses the default bucket.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700735
736 Returns:
Simran Basi99e63c02014-05-20 10:39:52 -0700737 String in the format of build_id/artifact as stored on the local server
738 or in Google Storage.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700739 """
Simran Basi99e63c02014-05-20 10:39:52 -0700740 build_id, filename = self._xbuddy.Translate(
741 args, image_dir=kwargs.get('image_dir'))
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700742 response = os.path.join(build_id, filename)
743 _Log('Path translation requested, returning: %s', response)
744 return response
745
746 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -0700747 def xbuddy(self, *args, **kwargs):
748 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -0700749
750 Args:
joycheneaf4cfc2013-07-02 08:38:57 -0700751 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -0700752 components of the path. The path can be understood as
753 "{local|remote}/build_id/artifact" where build_id is composed of
754 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -0700755
joychen121fc9b2013-08-02 14:30:30 -0700756 The first path element is optional, and can be "remote" or "local"
757 If local (the default), devserver will not attempt to access Google
758 Storage, and will only search the static directory for the files.
759 If remote, devserver will try to obtain the artifact off GS if it's
760 not found locally.
761 The board is the familiar board name, optionally suffixed.
762 The version can be the google storage version number, and may also be
763 any of a number of xBuddy defined version aliases that will be
764 translated into the latest built image that fits the description.
765 Defaults to latest.
766 The artifact is one of a number of image or artifact aliases used by
767 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -0700768
769 Kwargs:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800770 for_update: {true|false}
771 if true, pregenerates the update payloads for the image,
772 and returns the update uri to pass to the
773 update_engine_client.
joychen3cb228e2013-06-12 12:13:13 -0700774 return_dir: {true|false}
775 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800776 relative_path: {true|false}
777 if set to true, returns the relative path to the payload
778 directory from static_dir.
joychen3cb228e2013-06-12 12:13:13 -0700779 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -0700780 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -0700781 or
joycheneaf4cfc2013-07-02 08:38:57 -0700782 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -0700783
784 Returns:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800785 If |for_update|, returns a redirect to the image or update file
786 on the devserver. E.g.,
787 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
788 chromium-test-image.bin
789 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
790 http://host:port/static/x86-generic-release/R26-4000.0.0/
791 If |relative_path| is true, return a relative path the folder where the
792 payloads are. E.g.,
793 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 12:13:13 -0700794 """
Chris Sosa75490802013-09-30 17:21:45 -0700795 boolean_string = kwargs.get('for_update')
796 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800797 boolean_string = kwargs.get('return_dir')
798 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
799 boolean_string = kwargs.get('relative_path')
800 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -0700801
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800802 if return_dir and relative_path:
Chris Sosa4b951602014-04-09 20:26:07 -0700803 raise common_util.DevServerHTTPError(
804 500, 'Cannot specify both return_dir and relative_path')
Chris Sosa75490802013-09-30 17:21:45 -0700805
806 # For updates, we optimize downloading of test images.
807 file_name = None
808 build_id = None
809 if for_update:
810 try:
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700811 build_id = self._xbuddy.StageTestArtifactsForUpdate(args)
Chris Sosa75490802013-09-30 17:21:45 -0700812 except build_artifact.ArtifactDownloadError:
813 build_id = None
814
815 if not build_id:
816 build_id, file_name = self._xbuddy.Get(args)
817
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800818 if for_update:
819 _Log('Payload generation triggered by request')
820 # Forces payload to be in cache and symlinked into build_id dir.
Chris Sosa75490802013-09-30 17:21:45 -0700821 updater.GetUpdateForLabel(autoupdate.FORCED_UPDATE, build_id,
822 image_name=file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800823
824 response = None
825 if return_dir:
826 response = os.path.join(cherrypy.request.base, 'static', build_id)
827 _Log('Directory requested, returning: %s', response)
828 elif relative_path:
829 response = build_id
830 _Log('Relative path requested, returning: %s', response)
831 elif for_update:
832 response = os.path.join(cherrypy.request.base, 'update', build_id)
833 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 12:13:13 -0700834 else:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800835 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 14:30:30 -0700836 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800837 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 14:30:30 -0700838 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -0700839
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800840 return response
841
joychen3cb228e2013-06-12 12:13:13 -0700842 @cherrypy.expose
843 def xbuddy_list(self):
844 """Lists the currently available images & time since last access.
845
Gilad Arnold452fd272014-02-04 11:09:28 -0800846 Returns:
847 A string representation of a list of tuples [(build_id, time since last
848 access),...]
joychen3cb228e2013-06-12 12:13:13 -0700849 """
850 return self._xbuddy.List()
851
852 @cherrypy.expose
853 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 11:09:28 -0800854 """Returns the number of images cached by xBuddy."""
joychen3cb228e2013-06-12 12:13:13 -0700855 return self._xbuddy.Capacity()
856
857 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700858 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700859 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700860 return ('Welcome to the Dev Server!<br>\n'
861 '<br>\n'
862 'Here are the available methods, click for documentation:<br>\n'
863 '<br>\n'
864 '%s' %
865 '<br>\n'.join(
866 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700867 for name in _FindExposedMethods(
868 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700869
870 @cherrypy.expose
871 def doc(self, *args):
872 """Shows the documentation for available methods / URLs.
873
874 Example:
875 http://myhost/doc/update
876 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700877 name = '/'.join(args)
878 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700879 if not method:
880 raise DevServerError("No exposed method named `%s'" % name)
881 if not method.__doc__:
882 raise DevServerError("No documentation for exposed method `%s'" % name)
883 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -0700884
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700885 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700886 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700887 """Handles an update check from a Chrome OS client.
888
889 The HTTP request should contain the standard Omaha-style XML blob. The URL
890 line may contain an additional intermediate path to the update payload.
891
joychen121fc9b2013-08-02 14:30:30 -0700892 This request can be handled in one of 4 ways, depending on the devsever
893 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -0700894
joychen121fc9b2013-08-02 14:30:30 -0700895 1. No intermediate path
896 If no intermediate path is given, the default behavior is to generate an
897 update payload from the latest test image locally built for the board
898 specified in the xml. Devserver serves the generated payload.
899
900 2. Path explicitly invokes XBuddy
901 If there is a path given, it can explicitly invoke xbuddy by prefixing it
902 with 'xbuddy'. This path is then used to acquire an image binary for the
903 devserver to generate an update payload from. Devserver then serves this
904 payload.
905
906 3. Path is left for the devserver to interpret.
907 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
908 to generate a payload from the test image in that directory and serve it.
909
910 4. The devserver is in a 'forced' mode. TO BE DEPRECATED
911 This comes from the usage of --forced_payload or --image when starting the
912 devserver. No matter what path (or no path) gets passed in, devserver will
913 serve the update payload (--forced_payload) or generate an update payload
914 from the image (--image).
915
916 Examples:
917 1. No intermediate path
918 update_engine_client --omaha_url=http://myhost/update
919 This generates an update payload from the latest test image locally built
920 for the board specified in the xml.
921
922 2. Explicitly invoke xbuddy
923 update_engine_client --omaha_url=
924 http://myhost/update/xbuddy/remote/board/version/dev
925 This would go to GS to download the dev image for the board, from which
926 the devserver would generate a payload to serve.
927
928 3. Give a path for devserver to interpret
929 update_engine_client --omaha_url=http://myhost/update/some/random/path
930 This would attempt, in order to:
931 a) Generate an update from a test image binary if found in
932 static_dir/some/random/path.
933 b) Serve an update payload found in static_dir/some/random/path.
934 c) Hope that some/random/path takes the form "board/version" and
935 and attempt to download an update payload for that board/version
936 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700937 """
joychen121fc9b2013-08-02 14:30:30 -0700938 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -0800939 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -0700940 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-11 19:49:01 -0700941
joychen121fc9b2013-08-02 14:30:30 -0700942 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700943
Dan Shif5ce2de2013-04-25 16:06:32 -0700944 @cherrypy.expose
945 def check_health(self):
946 """Collect the health status of devserver to see if it's ready for staging.
947
Gilad Arnold452fd272014-02-04 11:09:28 -0800948 Returns:
949 A JSON dictionary containing all or some of the following fields:
950 free_disk (int): free disk space in GB
951 staging_thread_count (int): number of devserver threads currently staging
952 an image
Dan Shif5ce2de2013-04-25 16:06:32 -0700953 """
954 # Get free disk space.
955 stat = os.statvfs(updater.static_dir)
956 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
957
958 return json.dumps({
959 'free_disk': free_disk,
Dan Shi59ae7092013-06-04 14:37:27 -0700960 'staging_thread_count': DevServerRoot._staging_thread_count,
Dan Shif5ce2de2013-04-25 16:06:32 -0700961 })
962
963
Chris Sosadbc20082012-12-10 13:39:11 -0800964def _CleanCache(cache_dir, wipe):
965 """Wipes any excess cached items in the cache_dir.
966
967 Args:
968 cache_dir: the directory we are wiping from.
969 wipe: If True, wipe all the contents -- not just the excess.
970 """
971 if wipe:
972 # Clear the cache and exit on error.
973 cmd = 'rm -rf %s/*' % cache_dir
974 if os.system(cmd) != 0:
975 _Log('Failed to clear the cache with %s' % cmd)
976 sys.exit(1)
977 else:
978 # Clear all but the last N cached updates
979 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
980 (cache_dir, CACHED_ENTRIES))
981 if os.system(cmd) != 0:
982 _Log('Failed to clean up old delta cache files with %s' % cmd)
983 sys.exit(1)
984
985
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700986def _AddTestingOptions(parser):
987 group = optparse.OptionGroup(
988 parser, 'Advanced Testing Options', 'These are used by test scripts and '
989 'developers writing integration tests utilizing the devserver. They are '
990 'not intended to be really used outside the scope of someone '
991 'knowledgable about the test.')
992 group.add_option('--exit',
993 action='store_true',
994 help='do not start the server (yet pregenerate/clear cache)')
995 group.add_option('--host_log',
996 action='store_true', default=False,
997 help='record history of host update events (/api/hostlog)')
998 group.add_option('--max_updates',
999 metavar='NUM', default= -1, type='int',
1000 help='maximum number of update checks handled positively '
1001 '(default: unlimited)')
1002 group.add_option('--private_key',
1003 metavar='PATH', default=None,
1004 help='path to the private key in pem format. If this is set '
1005 'the devserver will generate update payloads that are '
1006 'signed with this key.')
David Zeuthen52ccd012013-10-31 12:58:26 -07001007 group.add_option('--private_key_for_metadata_hash_signature',
1008 metavar='PATH', default=None,
1009 help='path to the private key in pem format. If this is set '
1010 'the devserver will sign the metadata hash with the given '
1011 'key and transmit in the Omaha-style XML response.')
1012 group.add_option('--public_key',
1013 metavar='PATH', default=None,
1014 help='path to the public key in pem format. If this is set '
1015 'the devserver will transmit a base64 encoded version of '
1016 'the content in the Omaha-style XML response.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001017 group.add_option('--proxy_port',
1018 metavar='PORT', default=None, type='int',
1019 help='port to have the client connect to -- basically the '
1020 'devserver lies to the update to tell it to get the payload '
1021 'from a different port that will proxy the request back to '
1022 'the devserver. The proxy must be managed outside the '
1023 'devserver.')
1024 group.add_option('--remote_payload',
1025 action='store_true', default=False,
Chris Sosa4b951602014-04-09 20:26:07 -07001026 help='Payload is being served from a remote machine. With '
1027 'this setting enabled, this devserver instance serves as '
1028 'just an Omaha server instance. In this mode, the '
1029 'devserver enforces a few extra components of the Omaha '
Chris Sosafc715442014-04-09 20:45:23 -07001030 'protocol, such as hardware class, being sent.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001031 group.add_option('-u', '--urlbase',
1032 metavar='URL',
1033 help='base URL for update images, other than the '
1034 'devserver. Use in conjunction with remote_payload.')
1035 parser.add_option_group(group)
1036
1037
1038def _AddUpdateOptions(parser):
1039 group = optparse.OptionGroup(
1040 parser, 'Autoupdate Options', 'These options can be used to change '
1041 'how the devserver either generates or serve update payloads. Please '
1042 'note that all of these option affect how a payload is generated and so '
1043 'do not work in archive-only mode.')
1044 group.add_option('--board',
1045 help='By default the devserver will create an update '
1046 'payload from the latest image built for the board '
1047 'a device that is requesting an update has. When we '
1048 'pre-generate an update (see below) and we do not specify '
1049 'another update_type option like image or payload, the '
1050 'devserver needs to know the board to generate the latest '
1051 'image for. This is that board.')
1052 group.add_option('--critical_update',
1053 action='store_true', default=False,
1054 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001055 group.add_option('--image',
1056 metavar='FILE',
1057 help='Generate and serve an update using this image to any '
1058 'device that requests an update.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001059 group.add_option('--payload',
1060 metavar='PATH',
1061 help='use the update payload from specified directory '
1062 '(update.gz).')
1063 group.add_option('-p', '--pregenerate_update',
1064 action='store_true', default=False,
1065 help='pre-generate the update payload before accepting '
1066 'update requests. Useful to help debug payload generation '
1067 'issues quickly. Also if an update payload will take a '
1068 'long time to generate, a client may timeout if you do not'
1069 'pregenerate the update.')
1070 group.add_option('--src_image',
1071 metavar='PATH', default='',
1072 help='If specified, delta updates will be generated using '
1073 'this image as the source image. Delta updates are when '
1074 'you are updating from a "source image" to a another '
1075 'image.')
1076 parser.add_option_group(group)
1077
1078
1079def _AddProductionOptions(parser):
1080 group = optparse.OptionGroup(
1081 parser, 'Advanced Server Options', 'These options can be used to changed '
1082 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001083 group.add_option('--clear_cache',
1084 action='store_true', default=False,
1085 help='At startup, removes all cached entries from the'
1086 'devserver\'s cache.')
1087 group.add_option('--logfile',
1088 metavar='PATH',
1089 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -07001090 group.add_option('--pidfile',
1091 metavar='PATH',
1092 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 11:04:13 -08001093 group.add_option('--portfile',
1094 metavar='PATH',
1095 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001096 group.add_option('--production',
1097 action='store_true', default=False,
1098 help='have the devserver use production values when '
1099 'starting up. This includes using more threads and '
1100 'performing less logging.')
1101 parser.add_option_group(group)
1102
1103
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001104def _MakeLogHandler(logfile):
1105 """Create a LogHandler instance used to log all messages."""
1106 hdlr_cls = handlers.TimedRotatingFileHandler
1107 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
1108 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -07001109 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001110 return hdlr
1111
1112
Chris Sosacde6bf42012-05-31 18:36:39 -07001113def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001114 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -08001115 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -07001116
1117 # get directory that the devserver is run from
1118 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -07001119 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -07001120 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001121 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -07001122 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -07001123 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001124 parser.add_option('--port',
1125 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 13:13:28 -08001126 help=('port for the dev server to use; if zero, binds to '
1127 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001128 parser.add_option('-t', '--test_image',
1129 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -07001130 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -07001131 parser.add_option('-x', '--xbuddy_manage_builds',
1132 action='store_true',
1133 default=False,
1134 help='If set, allow xbuddy to manage images in'
1135 'build/images.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001136 _AddProductionOptions(parser)
1137 _AddUpdateOptions(parser)
1138 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -07001139 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001140
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001141 # Handle options that must be set globally in cherrypy. Do this
1142 # work up front, because calls to _Log() below depend on this
1143 # initialization.
1144 if options.production:
1145 cherrypy.config.update({'environment': 'production'})
1146 if not options.logfile:
1147 cherrypy.config.update({'log.screen': True})
1148 else:
1149 cherrypy.config.update({'log.error_file': '',
1150 'log.access_file': ''})
1151 hdlr = _MakeLogHandler(options.logfile)
1152 # Pylint can't seem to process these two calls properly
1153 # pylint: disable=E1101
1154 cherrypy.log.access_log.addHandler(hdlr)
1155 cherrypy.log.error_log.addHandler(hdlr)
1156 # pylint: enable=E1101
1157
joychened64b222013-06-21 16:39:34 -07001158 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001159 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001160
joychened64b222013-06-21 16:39:34 -07001161 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001162 # If our devserver is only supposed to serve payloads, we shouldn't be
1163 # mucking with the cache at all. If the devserver hadn't previously
1164 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001165 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001166 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001167 else:
1168 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001169
Chris Sosadbc20082012-12-10 13:39:11 -08001170 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 16:39:34 -07001171 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001172
joychen121fc9b2013-08-02 14:30:30 -07001173 _xbuddy = xbuddy.XBuddy(options.xbuddy_manage_builds,
1174 options.board,
joychen121fc9b2013-08-02 14:30:30 -07001175 static_dir=options.static_dir)
Chris Sosa75490802013-09-30 17:21:45 -07001176 if options.clear_cache and options.xbuddy_manage_builds:
1177 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 14:30:30 -07001178
Chris Sosa6a3697f2013-01-29 16:44:43 -08001179 # We allow global use here to share with cherrypy classes.
1180 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001181 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001182 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001183 _xbuddy,
joychened64b222013-06-21 16:39:34 -07001184 static_dir=options.static_dir,
Andrew de los Reyes52620802010-04-12 13:40:07 -07001185 urlbase=options.urlbase,
Chris Sosa5d342a22010-09-28 16:54:41 -07001186 forced_image=options.image,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001187 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001188 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-28 23:42:37 -07001189 src_image=options.src_image,
Chris Sosa08d55a22011-01-19 16:08:02 -08001190 board=options.board,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001191 copy_to_static_root=not options.exit,
1192 private_key=options.private_key,
David Zeuthen52ccd012013-10-31 12:58:26 -07001193 private_key_for_metadata_hash_signature=
1194 options.private_key_for_metadata_hash_signature,
1195 public_key=options.public_key,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001196 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001197 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001198 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001199 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001200 )
Chris Sosa7c931362010-10-11 19:49:01 -07001201
Chris Sosa6a3697f2013-01-29 16:44:43 -08001202 if options.pregenerate_update:
1203 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 15:46:22 -07001204
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001205 if options.exit:
1206 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001207
joychen3cb228e2013-06-12 12:13:13 -07001208 dev_server = DevServerRoot(_xbuddy)
1209
Gilad Arnold11fbef42014-02-10 11:04:13 -08001210 # Patch CherryPy to support binding to any available port (--port=0).
1211 cherrypy_ext.ZeroPortPatcher.DoPatch(cherrypy)
1212
Chris Sosa855b8932013-08-21 13:24:55 -07001213 if options.pidfile:
1214 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1215
Gilad Arnold11fbef42014-02-10 11:04:13 -08001216 if options.portfile:
1217 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1218
joychen3cb228e2013-06-12 12:13:13 -07001219 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001220
1221
1222if __name__ == '__main__':
1223 main()