blob: 2ac4b9f6336c0b1068836cd0a175ff63aba43b96 [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')
572 self.stage(archive_url=archive_url, artifacts='autotest')
573
574 build = '/'.join(downloader.Downloader.ParseUrl(archive_url))
575 build_path = os.path.join(updater.static_dir, build)
576 deps_path = os.path.join(build_path, 'autotest/packages')
577 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
578 src_folder = os.path.join(telemetry_path, 'src')
579
580 with self._telemetry_lock_dict.lock(telemetry_path):
581 if os.path.exists(src_folder):
582 # Telemetry is already fully stage return
583 return src_folder
584
585 common_util.MkDirP(telemetry_path)
586
587 # Copy over the required deps tar balls to the telemetry directory.
588 for dep in TELEMETRY_DEPS:
589 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -0700590 if not os.path.exists(dep_path):
591 # This dep does not exist (could be new), do not extract it.
592 continue
Simran Basi4baad082013-02-14 13:39:18 -0800593 try:
594 common_util.ExtractTarball(dep_path, telemetry_path)
595 except common_util.CommonUtilError as e:
596 shutil.rmtree(telemetry_path)
597 raise DevServerError(str(e))
598
599 # By default all the tarballs extract to test_src but some parts of
600 # the telemetry code specifically hardcoded to exist inside of 'src'.
601 test_src = os.path.join(telemetry_path, 'test_src')
602 try:
603 shutil.move(test_src, src_folder)
604 except shutil.Error:
605 # This can occur if src_folder already exists. Remove and retry move.
606 shutil.rmtree(src_folder)
607 raise DevServerError('Failure in telemetry setup for build %s. Appears'
608 ' that the test_src to src move failed.' % build)
609
610 return src_folder
611
612 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800613 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -0700614 """Symbolicates a minidump using pre-downloaded symbols, returns it.
615
616 Callers will need to POST to this URL with a body of MIME-type
617 "multipart/form-data".
618 The body should include a single argument, 'minidump', containing the
619 binary-formatted minidump to symbolicate.
620
Chris Masone816e38c2012-05-02 12:22:36 -0700621 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -0800622 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -0700623 minidump: The binary minidump file to symbolicate.
624 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800625 # Ensure the symbols have been staged.
626 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
627 if self.stage(archive_url=archive_url, artifacts='symbols') != 'Success':
628 raise DevServerError('Failed to stage symbols for %s' % archive_url)
629
Chris Masone816e38c2012-05-02 12:22:36 -0700630 to_return = ''
631 with tempfile.NamedTemporaryFile() as local:
632 while True:
633 data = minidump.file.read(8192)
634 if not data:
635 break
636 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -0800637
Chris Masone816e38c2012-05-02 12:22:36 -0700638 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -0800639
640 symbols_directory = os.path.join(downloader.Downloader.GetBuildDir(
641 updater.static_dir, archive_url), 'debug', 'breakpad')
642
643 stackwalk = subprocess.Popen(
644 ['minidump_stackwalk', local.name, symbols_directory],
645 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
646
Chris Masone816e38c2012-05-02 12:22:36 -0700647 to_return, error_text = stackwalk.communicate()
648 if stackwalk.returncode != 0:
649 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
650 error_text, stackwalk.returncode))
651
652 return to_return
653
654 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800655 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 15:31:36 -0400656 """Return a string representing the latest build for a given target.
657
658 Args:
659 target: The build target, typically a combination of the board and the
660 type of build e.g. x86-mario-release.
661 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
662 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-07 18:21:26 -0800663
Scott Zawalski16954532012-03-20 15:31:36 -0400664 Returns:
665 A string representation of the latest build if one exists, i.e.
666 R19-1993.0.0-a1-b1480.
667 An empty string if no latest could be found.
668 """
Don Garrettf84631a2014-01-07 18:21:26 -0800669 if not kwargs:
Scott Zawalski16954532012-03-20 15:31:36 -0400670 return _PrintDocStringAsHTML(self.latestbuild)
671
Don Garrettf84631a2014-01-07 18:21:26 -0800672 if 'target' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -0700673 raise common_util.DevServerHTTPError(500, 'Error: target= is required!')
Scott Zawalski16954532012-03-20 15:31:36 -0400674 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700675 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-07 18:21:26 -0800676 updater.static_dir, kwargs['target'],
677 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700678 except common_util.CommonUtilError as errmsg:
Chris Sosa4b951602014-04-09 20:26:07 -0700679 raise common_util.DevServerHTTPError(500, str(errmsg))
Scott Zawalski16954532012-03-20 15:31:36 -0400680
681 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800682 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500683 """Return a control file or a list of all known control files.
684
685 Example URL:
686 To List all control files:
beepsbd337242013-07-09 22:44:06 -0700687 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
688 To List all control files for, say, the bvt suite:
689 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -0500690 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500691 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 -0500692
693 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500694 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500695 control_path: If you want the contents of a control file set this
696 to the path. E.g. client/site_tests/sleeptest/control
697 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -0700698 suite_name: If control_path is not specified but a suite_name is
699 specified, list the control files belonging to that suite instead of
700 all control files. The empty string for suite_name will list all control
701 files for the build.
Don Garrettf84631a2014-01-07 18:21:26 -0800702
Scott Zawalski4647ce62012-01-03 17:17:28 -0500703 Returns:
704 Contents of a control file if control_path is provided.
705 A list of control files if no control_path is provided.
706 """
Don Garrettf84631a2014-01-07 18:21:26 -0800707 if not kwargs:
Scott Zawalski4647ce62012-01-03 17:17:28 -0500708 return _PrintDocStringAsHTML(self.controlfiles)
709
Don Garrettf84631a2014-01-07 18:21:26 -0800710 if 'build' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -0700711 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -0500712
Don Garrettf84631a2014-01-07 18:21:26 -0800713 if 'control_path' not in kwargs:
714 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-09 22:44:06 -0700715 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-07 18:21:26 -0800716 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-09 22:44:06 -0700717 else:
718 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-07 18:21:26 -0800719 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -0500720 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700721 return common_util.GetControlFile(
Don Garrettf84631a2014-01-07 18:21:26 -0800722 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -0800723
724 @cherrypy.expose
Simran Basi99e63c02014-05-20 10:39:52 -0700725 def xbuddy_translate(self, *args, **kwargs):
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700726 """Translates an xBuddy path to a real path to artifact if it exists.
727
728 Args:
Simran Basi99e63c02014-05-20 10:39:52 -0700729 args: An xbuddy path in the form of {local|remote}/build_id/artifact.
730 Local searches the devserver's static directory. Remote searches a
731 Google Storage image archive.
732
733 Kwargs:
734 image_dir: Google Storage image archive to search in if requesting a
735 remote artifact. If none uses the default bucket.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700736
737 Returns:
Simran Basi99e63c02014-05-20 10:39:52 -0700738 String in the format of build_id/artifact as stored on the local server
739 or in Google Storage.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700740 """
Simran Basi99e63c02014-05-20 10:39:52 -0700741 build_id, filename = self._xbuddy.Translate(
742 args, image_dir=kwargs.get('image_dir'))
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700743 response = os.path.join(build_id, filename)
744 _Log('Path translation requested, returning: %s', response)
745 return response
746
747 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -0700748 def xbuddy(self, *args, **kwargs):
749 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -0700750
751 Args:
joycheneaf4cfc2013-07-02 08:38:57 -0700752 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -0700753 components of the path. The path can be understood as
754 "{local|remote}/build_id/artifact" where build_id is composed of
755 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -0700756
joychen121fc9b2013-08-02 14:30:30 -0700757 The first path element is optional, and can be "remote" or "local"
758 If local (the default), devserver will not attempt to access Google
759 Storage, and will only search the static directory for the files.
760 If remote, devserver will try to obtain the artifact off GS if it's
761 not found locally.
762 The board is the familiar board name, optionally suffixed.
763 The version can be the google storage version number, and may also be
764 any of a number of xBuddy defined version aliases that will be
765 translated into the latest built image that fits the description.
766 Defaults to latest.
767 The artifact is one of a number of image or artifact aliases used by
768 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -0700769
770 Kwargs:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800771 for_update: {true|false}
772 if true, pregenerates the update payloads for the image,
773 and returns the update uri to pass to the
774 update_engine_client.
joychen3cb228e2013-06-12 12:13:13 -0700775 return_dir: {true|false}
776 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800777 relative_path: {true|false}
778 if set to true, returns the relative path to the payload
779 directory from static_dir.
joychen3cb228e2013-06-12 12:13:13 -0700780 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -0700781 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -0700782 or
joycheneaf4cfc2013-07-02 08:38:57 -0700783 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -0700784
785 Returns:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800786 If |for_update|, returns a redirect to the image or update file
787 on the devserver. E.g.,
788 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
789 chromium-test-image.bin
790 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
791 http://host:port/static/x86-generic-release/R26-4000.0.0/
792 If |relative_path| is true, return a relative path the folder where the
793 payloads are. E.g.,
794 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 12:13:13 -0700795 """
Chris Sosa75490802013-09-30 17:21:45 -0700796 boolean_string = kwargs.get('for_update')
797 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800798 boolean_string = kwargs.get('return_dir')
799 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
800 boolean_string = kwargs.get('relative_path')
801 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -0700802
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800803 if return_dir and relative_path:
Chris Sosa4b951602014-04-09 20:26:07 -0700804 raise common_util.DevServerHTTPError(
805 500, 'Cannot specify both return_dir and relative_path')
Chris Sosa75490802013-09-30 17:21:45 -0700806
807 # For updates, we optimize downloading of test images.
808 file_name = None
809 build_id = None
810 if for_update:
811 try:
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700812 build_id = self._xbuddy.StageTestArtifactsForUpdate(args)
Chris Sosa75490802013-09-30 17:21:45 -0700813 except build_artifact.ArtifactDownloadError:
814 build_id = None
815
816 if not build_id:
817 build_id, file_name = self._xbuddy.Get(args)
818
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800819 if for_update:
820 _Log('Payload generation triggered by request')
821 # Forces payload to be in cache and symlinked into build_id dir.
Chris Sosa75490802013-09-30 17:21:45 -0700822 updater.GetUpdateForLabel(autoupdate.FORCED_UPDATE, build_id,
823 image_name=file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800824
825 response = None
826 if return_dir:
827 response = os.path.join(cherrypy.request.base, 'static', build_id)
828 _Log('Directory requested, returning: %s', response)
829 elif relative_path:
830 response = build_id
831 _Log('Relative path requested, returning: %s', response)
832 elif for_update:
833 response = os.path.join(cherrypy.request.base, 'update', build_id)
834 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 12:13:13 -0700835 else:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800836 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 14:30:30 -0700837 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800838 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 14:30:30 -0700839 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -0700840
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800841 return response
842
joychen3cb228e2013-06-12 12:13:13 -0700843 @cherrypy.expose
844 def xbuddy_list(self):
845 """Lists the currently available images & time since last access.
846
Gilad Arnold452fd272014-02-04 11:09:28 -0800847 Returns:
848 A string representation of a list of tuples [(build_id, time since last
849 access),...]
joychen3cb228e2013-06-12 12:13:13 -0700850 """
851 return self._xbuddy.List()
852
853 @cherrypy.expose
854 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 11:09:28 -0800855 """Returns the number of images cached by xBuddy."""
joychen3cb228e2013-06-12 12:13:13 -0700856 return self._xbuddy.Capacity()
857
858 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700859 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700860 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700861 return ('Welcome to the Dev Server!<br>\n'
862 '<br>\n'
863 'Here are the available methods, click for documentation:<br>\n'
864 '<br>\n'
865 '%s' %
866 '<br>\n'.join(
867 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700868 for name in _FindExposedMethods(
869 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700870
871 @cherrypy.expose
872 def doc(self, *args):
873 """Shows the documentation for available methods / URLs.
874
875 Example:
876 http://myhost/doc/update
877 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700878 name = '/'.join(args)
879 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700880 if not method:
881 raise DevServerError("No exposed method named `%s'" % name)
882 if not method.__doc__:
883 raise DevServerError("No documentation for exposed method `%s'" % name)
884 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -0700885
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700886 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700887 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700888 """Handles an update check from a Chrome OS client.
889
890 The HTTP request should contain the standard Omaha-style XML blob. The URL
891 line may contain an additional intermediate path to the update payload.
892
joychen121fc9b2013-08-02 14:30:30 -0700893 This request can be handled in one of 4 ways, depending on the devsever
894 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -0700895
joychen121fc9b2013-08-02 14:30:30 -0700896 1. No intermediate path
897 If no intermediate path is given, the default behavior is to generate an
898 update payload from the latest test image locally built for the board
899 specified in the xml. Devserver serves the generated payload.
900
901 2. Path explicitly invokes XBuddy
902 If there is a path given, it can explicitly invoke xbuddy by prefixing it
903 with 'xbuddy'. This path is then used to acquire an image binary for the
904 devserver to generate an update payload from. Devserver then serves this
905 payload.
906
907 3. Path is left for the devserver to interpret.
908 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
909 to generate a payload from the test image in that directory and serve it.
910
911 4. The devserver is in a 'forced' mode. TO BE DEPRECATED
912 This comes from the usage of --forced_payload or --image when starting the
913 devserver. No matter what path (or no path) gets passed in, devserver will
914 serve the update payload (--forced_payload) or generate an update payload
915 from the image (--image).
916
917 Examples:
918 1. No intermediate path
919 update_engine_client --omaha_url=http://myhost/update
920 This generates an update payload from the latest test image locally built
921 for the board specified in the xml.
922
923 2. Explicitly invoke xbuddy
924 update_engine_client --omaha_url=
925 http://myhost/update/xbuddy/remote/board/version/dev
926 This would go to GS to download the dev image for the board, from which
927 the devserver would generate a payload to serve.
928
929 3. Give a path for devserver to interpret
930 update_engine_client --omaha_url=http://myhost/update/some/random/path
931 This would attempt, in order to:
932 a) Generate an update from a test image binary if found in
933 static_dir/some/random/path.
934 b) Serve an update payload found in static_dir/some/random/path.
935 c) Hope that some/random/path takes the form "board/version" and
936 and attempt to download an update payload for that board/version
937 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700938 """
joychen121fc9b2013-08-02 14:30:30 -0700939 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -0800940 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -0700941 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-11 19:49:01 -0700942
joychen121fc9b2013-08-02 14:30:30 -0700943 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700944
Dan Shif5ce2de2013-04-25 16:06:32 -0700945 @cherrypy.expose
946 def check_health(self):
947 """Collect the health status of devserver to see if it's ready for staging.
948
Gilad Arnold452fd272014-02-04 11:09:28 -0800949 Returns:
950 A JSON dictionary containing all or some of the following fields:
951 free_disk (int): free disk space in GB
952 staging_thread_count (int): number of devserver threads currently staging
953 an image
Dan Shif5ce2de2013-04-25 16:06:32 -0700954 """
955 # Get free disk space.
956 stat = os.statvfs(updater.static_dir)
957 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
958
959 return json.dumps({
960 'free_disk': free_disk,
Dan Shi59ae7092013-06-04 14:37:27 -0700961 'staging_thread_count': DevServerRoot._staging_thread_count,
Dan Shif5ce2de2013-04-25 16:06:32 -0700962 })
963
964
Chris Sosadbc20082012-12-10 13:39:11 -0800965def _CleanCache(cache_dir, wipe):
966 """Wipes any excess cached items in the cache_dir.
967
968 Args:
969 cache_dir: the directory we are wiping from.
970 wipe: If True, wipe all the contents -- not just the excess.
971 """
972 if wipe:
973 # Clear the cache and exit on error.
974 cmd = 'rm -rf %s/*' % cache_dir
975 if os.system(cmd) != 0:
976 _Log('Failed to clear the cache with %s' % cmd)
977 sys.exit(1)
978 else:
979 # Clear all but the last N cached updates
980 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
981 (cache_dir, CACHED_ENTRIES))
982 if os.system(cmd) != 0:
983 _Log('Failed to clean up old delta cache files with %s' % cmd)
984 sys.exit(1)
985
986
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700987def _AddTestingOptions(parser):
988 group = optparse.OptionGroup(
989 parser, 'Advanced Testing Options', 'These are used by test scripts and '
990 'developers writing integration tests utilizing the devserver. They are '
991 'not intended to be really used outside the scope of someone '
992 'knowledgable about the test.')
993 group.add_option('--exit',
994 action='store_true',
995 help='do not start the server (yet pregenerate/clear cache)')
996 group.add_option('--host_log',
997 action='store_true', default=False,
998 help='record history of host update events (/api/hostlog)')
999 group.add_option('--max_updates',
1000 metavar='NUM', default= -1, type='int',
1001 help='maximum number of update checks handled positively '
1002 '(default: unlimited)')
1003 group.add_option('--private_key',
1004 metavar='PATH', default=None,
1005 help='path to the private key in pem format. If this is set '
1006 'the devserver will generate update payloads that are '
1007 'signed with this key.')
David Zeuthen52ccd012013-10-31 12:58:26 -07001008 group.add_option('--private_key_for_metadata_hash_signature',
1009 metavar='PATH', default=None,
1010 help='path to the private key in pem format. If this is set '
1011 'the devserver will sign the metadata hash with the given '
1012 'key and transmit in the Omaha-style XML response.')
1013 group.add_option('--public_key',
1014 metavar='PATH', default=None,
1015 help='path to the public key in pem format. If this is set '
1016 'the devserver will transmit a base64 encoded version of '
1017 'the content in the Omaha-style XML response.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001018 group.add_option('--proxy_port',
1019 metavar='PORT', default=None, type='int',
1020 help='port to have the client connect to -- basically the '
1021 'devserver lies to the update to tell it to get the payload '
1022 'from a different port that will proxy the request back to '
1023 'the devserver. The proxy must be managed outside the '
1024 'devserver.')
1025 group.add_option('--remote_payload',
1026 action='store_true', default=False,
Chris Sosa4b951602014-04-09 20:26:07 -07001027 help='Payload is being served from a remote machine. With '
1028 'this setting enabled, this devserver instance serves as '
1029 'just an Omaha server instance. In this mode, the '
1030 'devserver enforces a few extra components of the Omaha '
Chris Sosafc715442014-04-09 20:45:23 -07001031 'protocol, such as hardware class, being sent.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001032 group.add_option('-u', '--urlbase',
1033 metavar='URL',
1034 help='base URL for update images, other than the '
1035 'devserver. Use in conjunction with remote_payload.')
1036 parser.add_option_group(group)
1037
1038
1039def _AddUpdateOptions(parser):
1040 group = optparse.OptionGroup(
1041 parser, 'Autoupdate Options', 'These options can be used to change '
1042 'how the devserver either generates or serve update payloads. Please '
1043 'note that all of these option affect how a payload is generated and so '
1044 'do not work in archive-only mode.')
1045 group.add_option('--board',
1046 help='By default the devserver will create an update '
1047 'payload from the latest image built for the board '
1048 'a device that is requesting an update has. When we '
1049 'pre-generate an update (see below) and we do not specify '
1050 'another update_type option like image or payload, the '
1051 'devserver needs to know the board to generate the latest '
1052 'image for. This is that board.')
1053 group.add_option('--critical_update',
1054 action='store_true', default=False,
1055 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001056 group.add_option('--image',
1057 metavar='FILE',
1058 help='Generate and serve an update using this image to any '
1059 'device that requests an update.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001060 group.add_option('--payload',
1061 metavar='PATH',
1062 help='use the update payload from specified directory '
1063 '(update.gz).')
1064 group.add_option('-p', '--pregenerate_update',
1065 action='store_true', default=False,
1066 help='pre-generate the update payload before accepting '
1067 'update requests. Useful to help debug payload generation '
1068 'issues quickly. Also if an update payload will take a '
1069 'long time to generate, a client may timeout if you do not'
1070 'pregenerate the update.')
1071 group.add_option('--src_image',
1072 metavar='PATH', default='',
1073 help='If specified, delta updates will be generated using '
1074 'this image as the source image. Delta updates are when '
1075 'you are updating from a "source image" to a another '
1076 'image.')
1077 parser.add_option_group(group)
1078
1079
1080def _AddProductionOptions(parser):
1081 group = optparse.OptionGroup(
1082 parser, 'Advanced Server Options', 'These options can be used to changed '
1083 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001084 group.add_option('--clear_cache',
1085 action='store_true', default=False,
1086 help='At startup, removes all cached entries from the'
1087 'devserver\'s cache.')
1088 group.add_option('--logfile',
1089 metavar='PATH',
1090 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -07001091 group.add_option('--pidfile',
1092 metavar='PATH',
1093 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 11:04:13 -08001094 group.add_option('--portfile',
1095 metavar='PATH',
1096 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001097 group.add_option('--production',
1098 action='store_true', default=False,
1099 help='have the devserver use production values when '
1100 'starting up. This includes using more threads and '
1101 'performing less logging.')
1102 parser.add_option_group(group)
1103
1104
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001105def _MakeLogHandler(logfile):
1106 """Create a LogHandler instance used to log all messages."""
1107 hdlr_cls = handlers.TimedRotatingFileHandler
1108 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
1109 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -07001110 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001111 return hdlr
1112
1113
Chris Sosacde6bf42012-05-31 18:36:39 -07001114def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001115 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -08001116 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -07001117
1118 # get directory that the devserver is run from
1119 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -07001120 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -07001121 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001122 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -07001123 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -07001124 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001125 parser.add_option('--port',
1126 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 13:13:28 -08001127 help=('port for the dev server to use; if zero, binds to '
1128 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001129 parser.add_option('-t', '--test_image',
1130 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -07001131 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -07001132 parser.add_option('-x', '--xbuddy_manage_builds',
1133 action='store_true',
1134 default=False,
1135 help='If set, allow xbuddy to manage images in'
1136 'build/images.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001137 _AddProductionOptions(parser)
1138 _AddUpdateOptions(parser)
1139 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -07001140 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001141
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001142 # Handle options that must be set globally in cherrypy. Do this
1143 # work up front, because calls to _Log() below depend on this
1144 # initialization.
1145 if options.production:
1146 cherrypy.config.update({'environment': 'production'})
1147 if not options.logfile:
1148 cherrypy.config.update({'log.screen': True})
1149 else:
1150 cherrypy.config.update({'log.error_file': '',
1151 'log.access_file': ''})
1152 hdlr = _MakeLogHandler(options.logfile)
1153 # Pylint can't seem to process these two calls properly
1154 # pylint: disable=E1101
1155 cherrypy.log.access_log.addHandler(hdlr)
1156 cherrypy.log.error_log.addHandler(hdlr)
1157 # pylint: enable=E1101
1158
joychened64b222013-06-21 16:39:34 -07001159 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001160 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001161
joychened64b222013-06-21 16:39:34 -07001162 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001163 # If our devserver is only supposed to serve payloads, we shouldn't be
1164 # mucking with the cache at all. If the devserver hadn't previously
1165 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001166 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001167 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001168 else:
1169 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001170
Chris Sosadbc20082012-12-10 13:39:11 -08001171 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 16:39:34 -07001172 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001173
joychen121fc9b2013-08-02 14:30:30 -07001174 _xbuddy = xbuddy.XBuddy(options.xbuddy_manage_builds,
1175 options.board,
joychen121fc9b2013-08-02 14:30:30 -07001176 static_dir=options.static_dir)
Chris Sosa75490802013-09-30 17:21:45 -07001177 if options.clear_cache and options.xbuddy_manage_builds:
1178 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 14:30:30 -07001179
Chris Sosa6a3697f2013-01-29 16:44:43 -08001180 # We allow global use here to share with cherrypy classes.
1181 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001182 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001183 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001184 _xbuddy,
joychened64b222013-06-21 16:39:34 -07001185 static_dir=options.static_dir,
Andrew de los Reyes52620802010-04-12 13:40:07 -07001186 urlbase=options.urlbase,
Chris Sosa5d342a22010-09-28 16:54:41 -07001187 forced_image=options.image,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001188 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001189 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-28 23:42:37 -07001190 src_image=options.src_image,
Chris Sosa08d55a22011-01-19 16:08:02 -08001191 board=options.board,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001192 copy_to_static_root=not options.exit,
1193 private_key=options.private_key,
David Zeuthen52ccd012013-10-31 12:58:26 -07001194 private_key_for_metadata_hash_signature=
1195 options.private_key_for_metadata_hash_signature,
1196 public_key=options.public_key,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001197 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001198 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001199 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001200 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001201 )
Chris Sosa7c931362010-10-11 19:49:01 -07001202
Chris Sosa6a3697f2013-01-29 16:44:43 -08001203 if options.pregenerate_update:
1204 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 15:46:22 -07001205
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001206 if options.exit:
1207 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001208
joychen3cb228e2013-06-12 12:13:13 -07001209 dev_server = DevServerRoot(_xbuddy)
1210
Gilad Arnold11fbef42014-02-10 11:04:13 -08001211 # Patch CherryPy to support binding to any available port (--port=0).
1212 cherrypy_ext.ZeroPortPatcher.DoPatch(cherrypy)
1213
Chris Sosa855b8932013-08-21 13:24:55 -07001214 if options.pidfile:
1215 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1216
Gilad Arnold11fbef42014-02-10 11:04:13 -08001217 if options.portfile:
1218 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1219
joychen3cb228e2013-06-12 12:13:13 -07001220 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001221
1222
1223if __name__ == '__main__':
1224 main()