blob: 16e895e9548effea7d1c2cac5e35bff26c8621cb [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
Simran Basi4243a862014-12-12 12:48:33 -0800449 @staticmethod
450 def _canonicalize_local_path(local_path):
451 """Canonicalizes |local_path| strings.
452
453 Raises:
454 DevserverError: if |local_path| is not set.
455 """
456 # Restrict staging of local content to only files within the static
457 # directory.
458 local_path = os.path.abspath(local_path)
459 if not local_path.startswith(updater.static_dir):
460 raise DevServerError('Local path %s must be a subdirectory of the static'
461 ' directory: %s' % (local_path, updater.static_dir))
462
463 return local_path.rstrip('/')
464
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700465 @cherrypy.expose
Dan Shif8eb0d12013-08-01 17:52:06 -0700466 def is_staged(self, **kwargs):
467 """Check if artifacts have been downloaded.
468
Chris Sosa6b0c6172013-08-05 17:01:33 -0700469 async: True to return without waiting for download to complete.
470 artifacts: Comma separated list of named artifacts to download.
471 These are defined in artifact_info and have their implementation
472 in build_artifact.py.
473 files: Comma separated list of file artifacts to stage. These
474 will be available as is in the corresponding static directory with no
475 custom post-processing.
476
477 returns: True of all artifacts are staged.
Dan Shif8eb0d12013-08-01 17:52:06 -0700478
479 Example:
480 To check if autotest and test_suites are staged:
481 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
482 artifacts=autotest,test_suites
483 """
484 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
Chris Sosa6b0c6172013-08-05 17:01:33 -0700485 artifacts, files = self._get_artifacts(kwargs)
Dan Shif8eb0d12013-08-01 17:52:06 -0700486 return str(downloader.Downloader(updater.static_dir, archive_url).IsStaged(
Chris Sosa6b0c6172013-08-05 17:01:33 -0700487 artifacts, files))
Dan Shi59ae7092013-06-04 14:37:27 -0700488
Chris Sosa76e44b92013-01-31 12:11:38 -0800489 @cherrypy.expose
Prashanth Ba06d2d22014-03-07 15:35:19 -0800490 def list_image_dir(self, **kwargs):
491 """Take an archive url and list the contents in its staged directory.
492
493 Args:
494 kwargs:
495 archive_url: Google Storage URL for the build.
496
497 Example:
498 To list the contents of where this devserver should have staged
499 gs://image-archive/<board>-release/<build> call:
500 http://devserver_url:<port>/list_image_dir?archive_url=<gs://..>
501
502 Returns:
503 A string with information about the contents of the image directory.
504 """
505 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
506 download_helper = downloader.Downloader(updater.static_dir, archive_url)
507 try:
508 image_dir_contents = download_helper.ListBuildDir()
509 except build_artifact.ArtifactDownloadError as e:
510 return 'Cannot list the contents of staged artifacts. %s' % e
511 if not image_dir_contents:
512 return '%s has not been staged on this devserver.' % archive_url
513 return image_dir_contents
514
515 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800516 def stage(self, **kwargs):
517 """Downloads and caches the artifacts from Google Storage URL.
518
519 Downloads and caches the artifacts Google Storage URL. Returns once these
520 have been downloaded on the devserver. A call to this will attempt to cache
521 non-specified artifacts in the background for the given from the given URL
522 following the principle of spatial locality. Spatial locality of different
523 artifacts is explicitly defined in the build_artifact module.
524
525 These artifacts will then be available from the static/ sub-directory of
526 the devserver.
527
528 Args:
529 archive_url: Google Storage URL for the build.
Simran Basi4243a862014-12-12 12:48:33 -0800530 local_path: Local path for the build.
Dan Shif8eb0d12013-08-01 17:52:06 -0700531 async: True to return without waiting for download to complete.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700532 artifacts: Comma separated list of named artifacts to download.
533 These are defined in artifact_info and have their implementation
534 in build_artifact.py.
535 files: Comma separated list of files to stage. These
536 will be available as is in the corresponding static directory with no
537 custom post-processing.
Chris Sosa76e44b92013-01-31 12:11:38 -0800538
539 Example:
540 To download the autotest and test suites tarballs:
541 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
542 artifacts=autotest,test_suites
543 To download the full update payload:
544 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
545 artifacts=full_payload
Chris Sosa6b0c6172013-08-05 17:01:33 -0700546 To download just a file called blah.bin:
547 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
548 files=blah.bin
Chris Sosa76e44b92013-01-31 12:11:38 -0800549
550 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 16:39:34 -0700551 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 12:11:38 -0800552
553 Note for this example, relative path is the archive_url stripped of its
554 basename i.e. path/ in the examples above. Specific example:
555
556 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
557
558 Will get staged to:
559
joychened64b222013-06-21 16:39:34 -0700560 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Chris Sosa76e44b92013-01-31 12:11:38 -0800561 """
Simran Basi4243a862014-12-12 12:48:33 -0800562 archive_url = kwargs.get('archive_url')
563 local_path = kwargs.get('local_path')
564 if not archive_url and not local_path:
565 raise DevServerError('Requires archive_url or local_path to be '
566 'specified.')
567 if archive_url and local_path:
568 raise DevServerError('archive_url and local_path can not both be '
569 'specified.')
570 if archive_url:
571 archive_url = self._canonicalize_archive_url(archive_url)
572 if local_path:
573 local_path = self._canonicalize_local_path(local_path)
Dan Shif8eb0d12013-08-01 17:52:06 -0700574 async = kwargs.get('async', False)
Chris Sosa6b0c6172013-08-05 17:01:33 -0700575 artifacts, files = self._get_artifacts(kwargs)
Dan Shi59ae7092013-06-04 14:37:27 -0700576 with DevServerRoot._staging_thread_count_lock:
577 DevServerRoot._staging_thread_count += 1
578 try:
Simran Basi4243a862014-12-12 12:48:33 -0800579 downloader.Downloader(
580 updater.static_dir, (archive_url or local_path)).Download(
581 artifacts, files, async=async)
Dan Shi59ae7092013-06-04 14:37:27 -0700582 finally:
583 with DevServerRoot._staging_thread_count_lock:
584 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800585 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700586
587 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -0800588 def setup_telemetry(self, **kwargs):
589 """Extracts and sets up telemetry
590
591 This method goes through the telemetry deps packages, and stages them on
592 the devserver to be used by the drones and the telemetry tests.
593
594 Args:
595 archive_url: Google Storage URL for the build.
596
597 Returns:
598 Path to the source folder for the telemetry codebase once it is staged.
599 """
600 archive_url = kwargs.get('archive_url')
Simran Basi4baad082013-02-14 13:39:18 -0800601
602 build = '/'.join(downloader.Downloader.ParseUrl(archive_url))
603 build_path = os.path.join(updater.static_dir, build)
604 deps_path = os.path.join(build_path, 'autotest/packages')
605 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
606 src_folder = os.path.join(telemetry_path, 'src')
607
608 with self._telemetry_lock_dict.lock(telemetry_path):
609 if os.path.exists(src_folder):
610 # Telemetry is already fully stage return
611 return src_folder
612
613 common_util.MkDirP(telemetry_path)
614
615 # Copy over the required deps tar balls to the telemetry directory.
616 for dep in TELEMETRY_DEPS:
617 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -0700618 if not os.path.exists(dep_path):
619 # This dep does not exist (could be new), do not extract it.
620 continue
Simran Basi4baad082013-02-14 13:39:18 -0800621 try:
622 common_util.ExtractTarball(dep_path, telemetry_path)
623 except common_util.CommonUtilError as e:
624 shutil.rmtree(telemetry_path)
625 raise DevServerError(str(e))
626
627 # By default all the tarballs extract to test_src but some parts of
628 # the telemetry code specifically hardcoded to exist inside of 'src'.
629 test_src = os.path.join(telemetry_path, 'test_src')
630 try:
631 shutil.move(test_src, src_folder)
632 except shutil.Error:
633 # This can occur if src_folder already exists. Remove and retry move.
634 shutil.rmtree(src_folder)
635 raise DevServerError('Failure in telemetry setup for build %s. Appears'
636 ' that the test_src to src move failed.' % build)
637
638 return src_folder
639
640 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800641 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -0700642 """Symbolicates a minidump using pre-downloaded symbols, returns it.
643
644 Callers will need to POST to this URL with a body of MIME-type
645 "multipart/form-data".
646 The body should include a single argument, 'minidump', containing the
647 binary-formatted minidump to symbolicate.
648
Chris Masone816e38c2012-05-02 12:22:36 -0700649 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -0800650 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -0700651 minidump: The binary minidump file to symbolicate.
652 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800653 # Ensure the symbols have been staged.
654 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
655 if self.stage(archive_url=archive_url, artifacts='symbols') != 'Success':
656 raise DevServerError('Failed to stage symbols for %s' % archive_url)
657
Chris Masone816e38c2012-05-02 12:22:36 -0700658 to_return = ''
659 with tempfile.NamedTemporaryFile() as local:
660 while True:
661 data = minidump.file.read(8192)
662 if not data:
663 break
664 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -0800665
Chris Masone816e38c2012-05-02 12:22:36 -0700666 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -0800667
668 symbols_directory = os.path.join(downloader.Downloader.GetBuildDir(
669 updater.static_dir, archive_url), 'debug', 'breakpad')
670
671 stackwalk = subprocess.Popen(
672 ['minidump_stackwalk', local.name, symbols_directory],
673 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
674
Chris Masone816e38c2012-05-02 12:22:36 -0700675 to_return, error_text = stackwalk.communicate()
676 if stackwalk.returncode != 0:
677 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
678 error_text, stackwalk.returncode))
679
680 return to_return
681
682 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800683 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 15:31:36 -0400684 """Return a string representing the latest build for a given target.
685
686 Args:
687 target: The build target, typically a combination of the board and the
688 type of build e.g. x86-mario-release.
689 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
690 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-07 18:21:26 -0800691
Scott Zawalski16954532012-03-20 15:31:36 -0400692 Returns:
693 A string representation of the latest build if one exists, i.e.
694 R19-1993.0.0-a1-b1480.
695 An empty string if no latest could be found.
696 """
Don Garrettf84631a2014-01-07 18:21:26 -0800697 if not kwargs:
Scott Zawalski16954532012-03-20 15:31:36 -0400698 return _PrintDocStringAsHTML(self.latestbuild)
699
Don Garrettf84631a2014-01-07 18:21:26 -0800700 if 'target' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -0700701 raise common_util.DevServerHTTPError(500, 'Error: target= is required!')
Scott Zawalski16954532012-03-20 15:31:36 -0400702 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700703 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-07 18:21:26 -0800704 updater.static_dir, kwargs['target'],
705 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700706 except common_util.CommonUtilError as errmsg:
Chris Sosa4b951602014-04-09 20:26:07 -0700707 raise common_util.DevServerHTTPError(500, str(errmsg))
Scott Zawalski16954532012-03-20 15:31:36 -0400708
709 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800710 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500711 """Return a control file or a list of all known control files.
712
713 Example URL:
714 To List all control files:
beepsbd337242013-07-09 22:44:06 -0700715 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
716 To List all control files for, say, the bvt suite:
717 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -0500718 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500719 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 -0500720
721 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500722 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500723 control_path: If you want the contents of a control file set this
724 to the path. E.g. client/site_tests/sleeptest/control
725 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -0700726 suite_name: If control_path is not specified but a suite_name is
727 specified, list the control files belonging to that suite instead of
728 all control files. The empty string for suite_name will list all control
729 files for the build.
Don Garrettf84631a2014-01-07 18:21:26 -0800730
Scott Zawalski4647ce62012-01-03 17:17:28 -0500731 Returns:
732 Contents of a control file if control_path is provided.
733 A list of control files if no control_path is provided.
734 """
Don Garrettf84631a2014-01-07 18:21:26 -0800735 if not kwargs:
Scott Zawalski4647ce62012-01-03 17:17:28 -0500736 return _PrintDocStringAsHTML(self.controlfiles)
737
Don Garrettf84631a2014-01-07 18:21:26 -0800738 if 'build' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -0700739 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -0500740
Don Garrettf84631a2014-01-07 18:21:26 -0800741 if 'control_path' not in kwargs:
742 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-09 22:44:06 -0700743 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-07 18:21:26 -0800744 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-09 22:44:06 -0700745 else:
746 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-07 18:21:26 -0800747 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -0500748 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700749 return common_util.GetControlFile(
Don Garrettf84631a2014-01-07 18:21:26 -0800750 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -0800751
752 @cherrypy.expose
Simran Basi99e63c02014-05-20 10:39:52 -0700753 def xbuddy_translate(self, *args, **kwargs):
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700754 """Translates an xBuddy path to a real path to artifact if it exists.
755
756 Args:
Simran Basi99e63c02014-05-20 10:39:52 -0700757 args: An xbuddy path in the form of {local|remote}/build_id/artifact.
758 Local searches the devserver's static directory. Remote searches a
759 Google Storage image archive.
760
761 Kwargs:
762 image_dir: Google Storage image archive to search in if requesting a
763 remote artifact. If none uses the default bucket.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700764
765 Returns:
Simran Basi99e63c02014-05-20 10:39:52 -0700766 String in the format of build_id/artifact as stored on the local server
767 or in Google Storage.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700768 """
Simran Basi99e63c02014-05-20 10:39:52 -0700769 build_id, filename = self._xbuddy.Translate(
770 args, image_dir=kwargs.get('image_dir'))
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700771 response = os.path.join(build_id, filename)
772 _Log('Path translation requested, returning: %s', response)
773 return response
774
775 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -0700776 def xbuddy(self, *args, **kwargs):
777 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -0700778
779 Args:
joycheneaf4cfc2013-07-02 08:38:57 -0700780 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -0700781 components of the path. The path can be understood as
782 "{local|remote}/build_id/artifact" where build_id is composed of
783 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -0700784
joychen121fc9b2013-08-02 14:30:30 -0700785 The first path element is optional, and can be "remote" or "local"
786 If local (the default), devserver will not attempt to access Google
787 Storage, and will only search the static directory for the files.
788 If remote, devserver will try to obtain the artifact off GS if it's
789 not found locally.
790 The board is the familiar board name, optionally suffixed.
791 The version can be the google storage version number, and may also be
792 any of a number of xBuddy defined version aliases that will be
793 translated into the latest built image that fits the description.
794 Defaults to latest.
795 The artifact is one of a number of image or artifact aliases used by
796 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -0700797
798 Kwargs:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800799 for_update: {true|false}
800 if true, pregenerates the update payloads for the image,
801 and returns the update uri to pass to the
802 update_engine_client.
joychen3cb228e2013-06-12 12:13:13 -0700803 return_dir: {true|false}
804 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800805 relative_path: {true|false}
806 if set to true, returns the relative path to the payload
807 directory from static_dir.
joychen3cb228e2013-06-12 12:13:13 -0700808 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -0700809 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -0700810 or
joycheneaf4cfc2013-07-02 08:38:57 -0700811 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -0700812
813 Returns:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800814 If |for_update|, returns a redirect to the image or update file
815 on the devserver. E.g.,
816 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
817 chromium-test-image.bin
818 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
819 http://host:port/static/x86-generic-release/R26-4000.0.0/
820 If |relative_path| is true, return a relative path the folder where the
821 payloads are. E.g.,
822 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 12:13:13 -0700823 """
Chris Sosa75490802013-09-30 17:21:45 -0700824 boolean_string = kwargs.get('for_update')
825 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800826 boolean_string = kwargs.get('return_dir')
827 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
828 boolean_string = kwargs.get('relative_path')
829 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -0700830
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800831 if return_dir and relative_path:
Chris Sosa4b951602014-04-09 20:26:07 -0700832 raise common_util.DevServerHTTPError(
833 500, 'Cannot specify both return_dir and relative_path')
Chris Sosa75490802013-09-30 17:21:45 -0700834
835 # For updates, we optimize downloading of test images.
836 file_name = None
837 build_id = None
838 if for_update:
839 try:
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700840 build_id = self._xbuddy.StageTestArtifactsForUpdate(args)
Chris Sosa75490802013-09-30 17:21:45 -0700841 except build_artifact.ArtifactDownloadError:
842 build_id = None
843
844 if not build_id:
845 build_id, file_name = self._xbuddy.Get(args)
846
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800847 if for_update:
848 _Log('Payload generation triggered by request')
849 # Forces payload to be in cache and symlinked into build_id dir.
Chris Sosa75490802013-09-30 17:21:45 -0700850 updater.GetUpdateForLabel(autoupdate.FORCED_UPDATE, build_id,
851 image_name=file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800852
853 response = None
854 if return_dir:
855 response = os.path.join(cherrypy.request.base, 'static', build_id)
856 _Log('Directory requested, returning: %s', response)
857 elif relative_path:
858 response = build_id
859 _Log('Relative path requested, returning: %s', response)
860 elif for_update:
861 response = os.path.join(cherrypy.request.base, 'update', build_id)
862 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 12:13:13 -0700863 else:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800864 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 14:30:30 -0700865 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800866 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 14:30:30 -0700867 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -0700868
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800869 return response
870
joychen3cb228e2013-06-12 12:13:13 -0700871 @cherrypy.expose
872 def xbuddy_list(self):
873 """Lists the currently available images & time since last access.
874
Gilad Arnold452fd272014-02-04 11:09:28 -0800875 Returns:
876 A string representation of a list of tuples [(build_id, time since last
877 access),...]
joychen3cb228e2013-06-12 12:13:13 -0700878 """
879 return self._xbuddy.List()
880
881 @cherrypy.expose
882 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 11:09:28 -0800883 """Returns the number of images cached by xBuddy."""
joychen3cb228e2013-06-12 12:13:13 -0700884 return self._xbuddy.Capacity()
885
886 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700887 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700888 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700889 return ('Welcome to the Dev Server!<br>\n'
890 '<br>\n'
891 'Here are the available methods, click for documentation:<br>\n'
892 '<br>\n'
893 '%s' %
894 '<br>\n'.join(
895 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700896 for name in _FindExposedMethods(
897 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700898
899 @cherrypy.expose
900 def doc(self, *args):
901 """Shows the documentation for available methods / URLs.
902
903 Example:
904 http://myhost/doc/update
905 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700906 name = '/'.join(args)
907 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700908 if not method:
909 raise DevServerError("No exposed method named `%s'" % name)
910 if not method.__doc__:
911 raise DevServerError("No documentation for exposed method `%s'" % name)
912 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -0700913
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700914 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700915 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700916 """Handles an update check from a Chrome OS client.
917
918 The HTTP request should contain the standard Omaha-style XML blob. The URL
919 line may contain an additional intermediate path to the update payload.
920
joychen121fc9b2013-08-02 14:30:30 -0700921 This request can be handled in one of 4 ways, depending on the devsever
922 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -0700923
joychen121fc9b2013-08-02 14:30:30 -0700924 1. No intermediate path
925 If no intermediate path is given, the default behavior is to generate an
926 update payload from the latest test image locally built for the board
927 specified in the xml. Devserver serves the generated payload.
928
929 2. Path explicitly invokes XBuddy
930 If there is a path given, it can explicitly invoke xbuddy by prefixing it
931 with 'xbuddy'. This path is then used to acquire an image binary for the
932 devserver to generate an update payload from. Devserver then serves this
933 payload.
934
935 3. Path is left for the devserver to interpret.
936 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
937 to generate a payload from the test image in that directory and serve it.
938
939 4. The devserver is in a 'forced' mode. TO BE DEPRECATED
940 This comes from the usage of --forced_payload or --image when starting the
941 devserver. No matter what path (or no path) gets passed in, devserver will
942 serve the update payload (--forced_payload) or generate an update payload
943 from the image (--image).
944
945 Examples:
946 1. No intermediate path
947 update_engine_client --omaha_url=http://myhost/update
948 This generates an update payload from the latest test image locally built
949 for the board specified in the xml.
950
951 2. Explicitly invoke xbuddy
952 update_engine_client --omaha_url=
953 http://myhost/update/xbuddy/remote/board/version/dev
954 This would go to GS to download the dev image for the board, from which
955 the devserver would generate a payload to serve.
956
957 3. Give a path for devserver to interpret
958 update_engine_client --omaha_url=http://myhost/update/some/random/path
959 This would attempt, in order to:
960 a) Generate an update from a test image binary if found in
961 static_dir/some/random/path.
962 b) Serve an update payload found in static_dir/some/random/path.
963 c) Hope that some/random/path takes the form "board/version" and
964 and attempt to download an update payload for that board/version
965 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700966 """
joychen121fc9b2013-08-02 14:30:30 -0700967 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -0800968 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -0700969 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-11 19:49:01 -0700970
joychen121fc9b2013-08-02 14:30:30 -0700971 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700972
Dan Shif5ce2de2013-04-25 16:06:32 -0700973 @cherrypy.expose
974 def check_health(self):
975 """Collect the health status of devserver to see if it's ready for staging.
976
Gilad Arnold452fd272014-02-04 11:09:28 -0800977 Returns:
978 A JSON dictionary containing all or some of the following fields:
979 free_disk (int): free disk space in GB
980 staging_thread_count (int): number of devserver threads currently staging
981 an image
Dan Shif5ce2de2013-04-25 16:06:32 -0700982 """
983 # Get free disk space.
984 stat = os.statvfs(updater.static_dir)
985 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
986
987 return json.dumps({
988 'free_disk': free_disk,
Dan Shi59ae7092013-06-04 14:37:27 -0700989 'staging_thread_count': DevServerRoot._staging_thread_count,
Dan Shif5ce2de2013-04-25 16:06:32 -0700990 })
991
992
Chris Sosadbc20082012-12-10 13:39:11 -0800993def _CleanCache(cache_dir, wipe):
994 """Wipes any excess cached items in the cache_dir.
995
996 Args:
997 cache_dir: the directory we are wiping from.
998 wipe: If True, wipe all the contents -- not just the excess.
999 """
1000 if wipe:
1001 # Clear the cache and exit on error.
1002 cmd = 'rm -rf %s/*' % cache_dir
1003 if os.system(cmd) != 0:
1004 _Log('Failed to clear the cache with %s' % cmd)
1005 sys.exit(1)
1006 else:
1007 # Clear all but the last N cached updates
1008 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
1009 (cache_dir, CACHED_ENTRIES))
1010 if os.system(cmd) != 0:
1011 _Log('Failed to clean up old delta cache files with %s' % cmd)
1012 sys.exit(1)
1013
1014
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001015def _AddTestingOptions(parser):
1016 group = optparse.OptionGroup(
1017 parser, 'Advanced Testing Options', 'These are used by test scripts and '
1018 'developers writing integration tests utilizing the devserver. They are '
1019 'not intended to be really used outside the scope of someone '
1020 'knowledgable about the test.')
1021 group.add_option('--exit',
1022 action='store_true',
1023 help='do not start the server (yet pregenerate/clear cache)')
1024 group.add_option('--host_log',
1025 action='store_true', default=False,
1026 help='record history of host update events (/api/hostlog)')
1027 group.add_option('--max_updates',
1028 metavar='NUM', default= -1, type='int',
1029 help='maximum number of update checks handled positively '
1030 '(default: unlimited)')
1031 group.add_option('--private_key',
1032 metavar='PATH', default=None,
1033 help='path to the private key in pem format. If this is set '
1034 'the devserver will generate update payloads that are '
1035 'signed with this key.')
David Zeuthen52ccd012013-10-31 12:58:26 -07001036 group.add_option('--private_key_for_metadata_hash_signature',
1037 metavar='PATH', default=None,
1038 help='path to the private key in pem format. If this is set '
1039 'the devserver will sign the metadata hash with the given '
1040 'key and transmit in the Omaha-style XML response.')
1041 group.add_option('--public_key',
1042 metavar='PATH', default=None,
1043 help='path to the public key in pem format. If this is set '
1044 'the devserver will transmit a base64 encoded version of '
1045 'the content in the Omaha-style XML response.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001046 group.add_option('--proxy_port',
1047 metavar='PORT', default=None, type='int',
1048 help='port to have the client connect to -- basically the '
1049 'devserver lies to the update to tell it to get the payload '
1050 'from a different port that will proxy the request back to '
1051 'the devserver. The proxy must be managed outside the '
1052 'devserver.')
1053 group.add_option('--remote_payload',
1054 action='store_true', default=False,
Chris Sosa4b951602014-04-09 20:26:07 -07001055 help='Payload is being served from a remote machine. With '
1056 'this setting enabled, this devserver instance serves as '
1057 'just an Omaha server instance. In this mode, the '
1058 'devserver enforces a few extra components of the Omaha '
Chris Sosafc715442014-04-09 20:45:23 -07001059 'protocol, such as hardware class, being sent.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001060 group.add_option('-u', '--urlbase',
1061 metavar='URL',
1062 help='base URL for update images, other than the '
1063 'devserver. Use in conjunction with remote_payload.')
1064 parser.add_option_group(group)
1065
1066
1067def _AddUpdateOptions(parser):
1068 group = optparse.OptionGroup(
1069 parser, 'Autoupdate Options', 'These options can be used to change '
1070 'how the devserver either generates or serve update payloads. Please '
1071 'note that all of these option affect how a payload is generated and so '
1072 'do not work in archive-only mode.')
1073 group.add_option('--board',
1074 help='By default the devserver will create an update '
1075 'payload from the latest image built for the board '
1076 'a device that is requesting an update has. When we '
1077 'pre-generate an update (see below) and we do not specify '
1078 'another update_type option like image or payload, the '
1079 'devserver needs to know the board to generate the latest '
1080 'image for. This is that board.')
1081 group.add_option('--critical_update',
1082 action='store_true', default=False,
1083 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001084 group.add_option('--image',
1085 metavar='FILE',
1086 help='Generate and serve an update using this image to any '
1087 'device that requests an update.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001088 group.add_option('--payload',
1089 metavar='PATH',
1090 help='use the update payload from specified directory '
1091 '(update.gz).')
1092 group.add_option('-p', '--pregenerate_update',
1093 action='store_true', default=False,
1094 help='pre-generate the update payload before accepting '
1095 'update requests. Useful to help debug payload generation '
1096 'issues quickly. Also if an update payload will take a '
1097 'long time to generate, a client may timeout if you do not'
1098 'pregenerate the update.')
1099 group.add_option('--src_image',
1100 metavar='PATH', default='',
1101 help='If specified, delta updates will be generated using '
1102 'this image as the source image. Delta updates are when '
1103 'you are updating from a "source image" to a another '
1104 'image.')
1105 parser.add_option_group(group)
1106
1107
1108def _AddProductionOptions(parser):
1109 group = optparse.OptionGroup(
1110 parser, 'Advanced Server Options', 'These options can be used to changed '
1111 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001112 group.add_option('--clear_cache',
1113 action='store_true', default=False,
1114 help='At startup, removes all cached entries from the'
1115 'devserver\'s cache.')
1116 group.add_option('--logfile',
1117 metavar='PATH',
1118 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -07001119 group.add_option('--pidfile',
1120 metavar='PATH',
1121 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 11:04:13 -08001122 group.add_option('--portfile',
1123 metavar='PATH',
1124 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001125 group.add_option('--production',
1126 action='store_true', default=False,
1127 help='have the devserver use production values when '
1128 'starting up. This includes using more threads and '
1129 'performing less logging.')
1130 parser.add_option_group(group)
1131
1132
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001133def _MakeLogHandler(logfile):
1134 """Create a LogHandler instance used to log all messages."""
1135 hdlr_cls = handlers.TimedRotatingFileHandler
1136 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
1137 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -07001138 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001139 return hdlr
1140
1141
Chris Sosacde6bf42012-05-31 18:36:39 -07001142def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001143 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -08001144 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -07001145
1146 # get directory that the devserver is run from
1147 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -07001148 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -07001149 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001150 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -07001151 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -07001152 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001153 parser.add_option('--port',
1154 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 13:13:28 -08001155 help=('port for the dev server to use; if zero, binds to '
1156 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001157 parser.add_option('-t', '--test_image',
1158 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -07001159 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -07001160 parser.add_option('-x', '--xbuddy_manage_builds',
1161 action='store_true',
1162 default=False,
1163 help='If set, allow xbuddy to manage images in'
1164 'build/images.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001165 _AddProductionOptions(parser)
1166 _AddUpdateOptions(parser)
1167 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -07001168 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001169
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001170 # Handle options that must be set globally in cherrypy. Do this
1171 # work up front, because calls to _Log() below depend on this
1172 # initialization.
1173 if options.production:
1174 cherrypy.config.update({'environment': 'production'})
1175 if not options.logfile:
1176 cherrypy.config.update({'log.screen': True})
1177 else:
1178 cherrypy.config.update({'log.error_file': '',
1179 'log.access_file': ''})
1180 hdlr = _MakeLogHandler(options.logfile)
1181 # Pylint can't seem to process these two calls properly
1182 # pylint: disable=E1101
1183 cherrypy.log.access_log.addHandler(hdlr)
1184 cherrypy.log.error_log.addHandler(hdlr)
1185 # pylint: enable=E1101
1186
joychened64b222013-06-21 16:39:34 -07001187 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001188 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001189
joychened64b222013-06-21 16:39:34 -07001190 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001191 # If our devserver is only supposed to serve payloads, we shouldn't be
1192 # mucking with the cache at all. If the devserver hadn't previously
1193 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001194 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001195 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001196 else:
1197 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001198
Chris Sosadbc20082012-12-10 13:39:11 -08001199 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 16:39:34 -07001200 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001201
joychen121fc9b2013-08-02 14:30:30 -07001202 _xbuddy = xbuddy.XBuddy(options.xbuddy_manage_builds,
1203 options.board,
joychen121fc9b2013-08-02 14:30:30 -07001204 static_dir=options.static_dir)
Chris Sosa75490802013-09-30 17:21:45 -07001205 if options.clear_cache and options.xbuddy_manage_builds:
1206 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 14:30:30 -07001207
Chris Sosa6a3697f2013-01-29 16:44:43 -08001208 # We allow global use here to share with cherrypy classes.
1209 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001210 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001211 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001212 _xbuddy,
joychened64b222013-06-21 16:39:34 -07001213 static_dir=options.static_dir,
Andrew de los Reyes52620802010-04-12 13:40:07 -07001214 urlbase=options.urlbase,
Chris Sosa5d342a22010-09-28 16:54:41 -07001215 forced_image=options.image,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001216 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001217 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-28 23:42:37 -07001218 src_image=options.src_image,
Chris Sosa08d55a22011-01-19 16:08:02 -08001219 board=options.board,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001220 copy_to_static_root=not options.exit,
1221 private_key=options.private_key,
David Zeuthen52ccd012013-10-31 12:58:26 -07001222 private_key_for_metadata_hash_signature=
1223 options.private_key_for_metadata_hash_signature,
1224 public_key=options.public_key,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001225 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001226 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001227 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001228 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001229 )
Chris Sosa7c931362010-10-11 19:49:01 -07001230
Chris Sosa6a3697f2013-01-29 16:44:43 -08001231 if options.pregenerate_update:
1232 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 15:46:22 -07001233
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001234 if options.exit:
1235 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001236
joychen3cb228e2013-06-12 12:13:13 -07001237 dev_server = DevServerRoot(_xbuddy)
1238
Gilad Arnold11fbef42014-02-10 11:04:13 -08001239 # Patch CherryPy to support binding to any available port (--port=0).
1240 cherrypy_ext.ZeroPortPatcher.DoPatch(cherrypy)
1241
Chris Sosa855b8932013-08-21 13:24:55 -07001242 if options.pidfile:
1243 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1244
Gilad Arnold11fbef42014-02-10 11:04:13 -08001245 if options.portfile:
1246 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1247
joychen3cb228e2013-06-12 12:13:13 -07001248 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001249
1250
1251if __name__ == '__main__':
1252 main()