blob: a3511a56d2e07897f179c084fc0cd882e5e71ca1 [file] [log] [blame]
Gabe Black3b567202015-09-23 14:07:59 -07001#!/usr/bin/python2
Chris Sosa7c931362010-10-11 19:49:01 -07002
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
Gabe Black3b567202015-09-23 14:07:59 -070042from __future__ import print_function
Chris Sosa7c931362010-10-11 19:49:01 -070043
Gilad Arnold55a2a372012-10-02 09:46:32 -070044import json
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070045import optparse
rtc@google.comded22402009-10-26 22:36:21 +000046import os
Scott Zawalski4647ce62012-01-03 17:17:28 -050047import re
Simran Basi4baad082013-02-14 13:39:18 -080048import shutil
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -080049import socket
Chris Masone816e38c2012-05-02 12:22:36 -070050import subprocess
J. Richard Barnette3d977b82013-04-23 11:05:19 -070051import sys
Chris Masone816e38c2012-05-02 12:22:36 -070052import tempfile
Dan Shi59ae7092013-06-04 14:37:27 -070053import threading
Dan Shiafd0e492015-05-27 14:23:51 -070054import time
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -070055import types
J. Richard Barnette3d977b82013-04-23 11:05:19 -070056from logging import handlers
57
58import cherrypy
Chris Sosa855b8932013-08-21 13:24:55 -070059from cherrypy import _cplogging as cplogging
60from cherrypy.process import plugins
rtc@google.comded22402009-10-26 22:36:21 +000061
Chris Sosa0356d3b2010-09-16 15:46:22 -070062import autoupdate
Dan Shi2f136862016-02-11 15:38:38 -080063import artifact_info
Chris Sosa75490802013-09-30 17:21:45 -070064import build_artifact
Gilad Arnold11fbef42014-02-10 11:04:13 -080065import cherrypy_ext
Gilad Arnoldc65330c2012-09-20 15:17:48 -070066import common_util
Simran Basief83d6a2014-08-28 14:32:01 -070067import devserver_constants
Chris Sosa47a7d4e2012-03-28 11:26:55 -070068import downloader
Chris Sosa7cd23202013-10-15 17:22:57 -070069import gsutil_util
Gilad Arnoldc65330c2012-09-20 15:17:48 -070070import log_util
joychen3cb228e2013-06-12 12:13:13 -070071import xbuddy
Gilad Arnoldc65330c2012-09-20 15:17:48 -070072
Gilad Arnoldc65330c2012-09-20 15:17:48 -070073# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080074def _Log(message, *args):
75 return log_util.LogWithTag('DEVSERVER', message, *args)
Chris Sosa0356d3b2010-09-16 15:46:22 -070076
Dan Shiafd0e492015-05-27 14:23:51 -070077try:
78 import psutil
79except ImportError:
80 # Ignore psutil import failure. This is for backwards compatibility, so
81 # "cros flash" can still update duts with build without psutil installed.
82 # The reason is that, during cros flash, local devserver code is copied over
83 # to DUT, and devserver will be running inside DUT to stage the build.
84 _Log('Python module psutil is not installed, devserver load data will not be '
85 'collected')
86 psutil = None
Dan Shi94dcbe82015-06-08 20:51:13 -070087except OSError as e:
88 # Ignore error like following. psutil may not work properly in builder. Ignore
89 # the error as load information of devserver is not used in builder.
90 # OSError: [Errno 2] No such file or directory: '/dev/pts/0'
91 _Log('psutil is failed to be imported, error: %s. devserver load data will '
92 'not be collected.', e)
93 psutil = None
94
Dan Shi72b16132015-10-08 12:10:33 -070095try:
96 import android_build
97except ImportError as e:
98 # Ignore android_build import failure. This is to support devserver running
99 # inside a ChromeOS device triggered by cros flash. Most ChromeOS test images
100 # do not have google-api-python-client module and they don't need to support
101 # Android updating, therefore, ignore the import failure here.
102 _Log('Import module android_build failed with error: %s', e)
103 android_build = None
Frank Farzan40160872011-12-12 18:39:18 -0800104
Chris Sosa417e55d2011-01-25 16:40:48 -0800105CACHED_ENTRIES = 12
Don Garrettf90edf02010-11-16 17:36:14 -0800106
Simran Basi4baad082013-02-14 13:39:18 -0800107TELEMETRY_FOLDER = 'telemetry_src'
108TELEMETRY_DEPS = ['dep-telemetry_dep.tar.bz2',
109 'dep-page_cycler_dep.tar.bz2',
Simran Basi0d078682013-03-22 16:40:04 -0700110 'dep-chrome_test.tar.bz2',
111 'dep-perf_data_dep.tar.bz2']
Simran Basi4baad082013-02-14 13:39:18 -0800112
Chris Sosa0356d3b2010-09-16 15:46:22 -0700113# Sets up global to share between classes.
rtc@google.com21a5ca32009-11-04 18:23:23 +0000114updater = None
rtc@google.comded22402009-10-26 22:36:21 +0000115
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700116# Log rotation parameters. These settings correspond to once a week
J. Richard Barnette6dfa5342013-06-04 11:48:56 -0700117# at midnight between Friday and Saturday, with about three months
118# of old logs kept for backup.
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700119#
120# For more, see the documentation for
121# logging.handlers.TimedRotatingFileHandler
J. Richard Barnette6dfa5342013-06-04 11:48:56 -0700122_LOG_ROTATION_TIME = 'W4'
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700123_LOG_ROTATION_BACKUP = 13
124
Dan Shiafd0e492015-05-27 14:23:51 -0700125# Number of seconds between the collection of disk and network IO counters.
126STATS_INTERVAL = 10.0
Frank Farzan40160872011-12-12 18:39:18 -0800127
Chris Sosa9164ca32012-03-28 11:04:50 -0700128class DevServerError(Exception):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700129 """Exception class used by this module."""
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700130
131
Dan Shiafd0e492015-05-27 14:23:51 -0700132def require_psutil():
Gabe Black3b567202015-09-23 14:07:59 -0700133 """Decorator for functions require psutil to run."""
Dan Shiafd0e492015-05-27 14:23:51 -0700134 def deco_require_psutil(func):
135 """Wrapper of the decorator function.
136
Gabe Black3b567202015-09-23 14:07:59 -0700137 Args:
138 func: function to be called.
Dan Shiafd0e492015-05-27 14:23:51 -0700139 """
140 def func_require_psutil(*args, **kwargs):
141 """Decorator for functions require psutil to run.
142
143 If psutil is not installed, skip calling the function.
144
Gabe Black3b567202015-09-23 14:07:59 -0700145 Args:
146 *args: arguments for function to be called.
147 **kwargs: keyword arguments for function to be called.
Dan Shiafd0e492015-05-27 14:23:51 -0700148 """
149 if psutil:
150 return func(*args, **kwargs)
151 else:
152 _Log('Python module psutil is not installed. Function call %s is '
153 'skipped.' % func)
154 return func_require_psutil
155 return deco_require_psutil
156
157
Gabe Black3b567202015-09-23 14:07:59 -0700158def _canonicalize_archive_url(archive_url):
159 """Canonicalizes archive_url strings.
160
161 Raises:
162 DevserverError: if archive_url is not set.
163 """
164 if archive_url:
165 if not archive_url.startswith('gs://'):
166 raise DevServerError("Archive URL isn't from Google Storage (%s) ." %
167 archive_url)
168
169 return archive_url.rstrip('/')
170 else:
171 raise DevServerError("Must specify an archive_url in the request")
172
173
174def _canonicalize_local_path(local_path):
175 """Canonicalizes |local_path| strings.
176
177 Raises:
178 DevserverError: if |local_path| is not set.
179 """
180 # Restrict staging of local content to only files within the static
181 # directory.
182 local_path = os.path.abspath(local_path)
183 if not local_path.startswith(updater.static_dir):
184 raise DevServerError('Local path %s must be a subdirectory of the static'
185 ' directory: %s' % (local_path, updater.static_dir))
186
187 return local_path.rstrip('/')
188
189
190def _get_artifacts(kwargs):
191 """Returns a tuple of named and file artifacts given the stage rpc kwargs.
192
193 Raises:
194 DevserverError if no artifacts would be returned.
195 """
196 artifacts = kwargs.get('artifacts')
197 files = kwargs.get('files')
198 if not artifacts and not files:
199 raise DevServerError('No artifacts specified.')
200
201 # Note we NEED to coerce files to a string as we get raw unicode from
202 # cherrypy and we treat files as strings elsewhere in the code.
203 return (str(artifacts).split(',') if artifacts else [],
204 str(files).split(',') if files else [])
205
206
Dan Shi61305df2015-10-26 16:52:35 -0700207def _is_android_build_request(kwargs):
208 """Check if a devserver call is for Android build, based on the arguments.
209
210 This method exams the request's arguments (os_type) to determine if the
211 request is for Android build. If os_type is set to `android`, returns True.
212 If os_type is not set or has other values, returns False.
213
214 Args:
215 kwargs: Keyword arguments for the request.
216
217 Returns:
218 True if the request is for Android build. False otherwise.
219 """
220 os_type = kwargs.get('os_type', None)
221 return os_type == 'android'
222
223
Gabe Black3b567202015-09-23 14:07:59 -0700224def _get_downloader(kwargs):
225 """Returns the downloader based on passed in arguments.
226
227 Args:
228 kwargs: Keyword arguments for the request.
229 """
230 local_path = kwargs.get('local_path')
231 if local_path:
232 local_path = _canonicalize_local_path(local_path)
233
234 dl = None
235 if local_path:
236 dl = downloader.LocalDownloader(updater.static_dir, local_path)
237
Dan Shi61305df2015-10-26 16:52:35 -0700238 if not _is_android_build_request(kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700239 archive_url = kwargs.get('archive_url')
240 if not archive_url and not local_path:
241 raise DevServerError('Requires archive_url or local_path to be '
242 'specified.')
243 if archive_url and local_path:
244 raise DevServerError('archive_url and local_path can not both be '
245 'specified.')
246 if not dl:
247 archive_url = _canonicalize_archive_url(archive_url)
248 dl = downloader.GoogleStorageDownloader(updater.static_dir, archive_url)
249 elif not dl:
250 target = kwargs.get('target', None)
Dan Shi72b16132015-10-08 12:10:33 -0700251 branch = kwargs.get('branch', None)
Dan Shi61305df2015-10-26 16:52:35 -0700252 build_id = kwargs.get('build_id', None)
253 if not target or not branch or not build_id:
Dan Shi72b16132015-10-08 12:10:33 -0700254 raise DevServerError(
Dan Shi61305df2015-10-26 16:52:35 -0700255 'target, branch, build ID must all be specified for downloading '
256 'Android build.')
Dan Shi72b16132015-10-08 12:10:33 -0700257 dl = downloader.AndroidBuildDownloader(updater.static_dir, branch, build_id,
258 target)
Gabe Black3b567202015-09-23 14:07:59 -0700259
260 return dl
261
262
263def _get_downloader_and_factory(kwargs):
264 """Returns the downloader and artifact factory based on passed in arguments.
265
266 Args:
267 kwargs: Keyword arguments for the request.
268 """
269 artifacts, files = _get_artifacts(kwargs)
270 dl = _get_downloader(kwargs)
271
272 if (isinstance(dl, downloader.GoogleStorageDownloader) or
273 isinstance(dl, downloader.LocalDownloader)):
274 factory_class = build_artifact.ChromeOSArtifactFactory
Dan Shi72b16132015-10-08 12:10:33 -0700275 elif isinstance(dl, downloader.AndroidBuildDownloader):
Gabe Black3b567202015-09-23 14:07:59 -0700276 factory_class = build_artifact.AndroidArtifactFactory
277 else:
278 raise DevServerError('Unrecognized value for downloader type: %s' %
279 type(dl))
280
281 factory = factory_class(dl.GetBuildDir(), artifacts, files, dl.GetBuild())
282
283 return dl, factory
284
285
Scott Zawalski4647ce62012-01-03 17:17:28 -0500286def _LeadingWhiteSpaceCount(string):
287 """Count the amount of leading whitespace in a string.
288
289 Args:
290 string: The string to count leading whitespace in.
Don Garrettf84631a2014-01-07 18:21:26 -0800291
Scott Zawalski4647ce62012-01-03 17:17:28 -0500292 Returns:
293 number of white space chars before characters start.
294 """
Gabe Black3b567202015-09-23 14:07:59 -0700295 matched = re.match(r'^\s+', string)
Scott Zawalski4647ce62012-01-03 17:17:28 -0500296 if matched:
297 return len(matched.group())
298
299 return 0
300
301
302def _PrintDocStringAsHTML(func):
303 """Make a functions docstring somewhat HTML style.
304
305 Args:
306 func: The function to return the docstring from.
Don Garrettf84631a2014-01-07 18:21:26 -0800307
Scott Zawalski4647ce62012-01-03 17:17:28 -0500308 Returns:
309 A string that is somewhat formated for a web browser.
310 """
311 # TODO(scottz): Make this parse Args/Returns in a prettier way.
312 # Arguments could be bolded and indented etc.
313 html_doc = []
314 for line in func.__doc__.splitlines():
315 leading_space = _LeadingWhiteSpaceCount(line)
316 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700317 line = '&nbsp;' * leading_space + line
Scott Zawalski4647ce62012-01-03 17:17:28 -0500318
319 html_doc.append('<BR>%s' % line)
320
321 return '\n'.join(html_doc)
322
323
Simran Basief83d6a2014-08-28 14:32:01 -0700324def _GetUpdateTimestampHandler(static_dir):
325 """Returns a handler to update directory staged.timestamp.
326
327 This handler resets the stage.timestamp whenever static content is accessed.
328
329 Args:
330 static_dir: Directory from which static content is being staged.
331
332 Returns:
333 A cherrypy handler to update the timestamp of accessed content.
334 """
335 def UpdateTimestampHandler():
336 if not '404' in cherrypy.response.status:
337 build_match = re.match(devserver_constants.STAGED_BUILD_REGEX,
338 cherrypy.request.path_info)
339 if build_match:
340 build_dir = os.path.join(static_dir, build_match.group('build'))
341 downloader.Downloader.TouchTimestampForStaged(build_dir)
342 return UpdateTimestampHandler
343
344
Chris Sosa7c931362010-10-11 19:49:01 -0700345def _GetConfig(options):
346 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800347
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800348 socket_host = '::'
Yu-Ju Hongc8d4af32013-11-12 15:14:26 -0800349 # Fall back to IPv4 when python is not configured with IPv6.
350 if not socket.has_ipv6:
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800351 socket_host = '0.0.0.0'
352
Simran Basief83d6a2014-08-28 14:32:01 -0700353 # Adds the UpdateTimestampHandler to cherrypy's tools. This tools executes
354 # on the on_end_resource hook. This hook is called once processing is
355 # complete and the response is ready to be returned.
356 cherrypy.tools.update_timestamp = cherrypy.Tool(
357 'on_end_resource', _GetUpdateTimestampHandler(options.static_dir))
358
Gabe Black3b567202015-09-23 14:07:59 -0700359 base_config = {'global':
360 {'server.log_request_headers': True,
361 'server.protocol_version': 'HTTP/1.1',
362 'server.socket_host': socket_host,
363 'server.socket_port': int(options.port),
364 'response.timeout': 6000,
365 'request.show_tracebacks': True,
366 'server.socket_timeout': 60,
367 'server.thread_pool': 2,
368 'engine.autoreload.on': False,
369 },
370 '/api':
371 {
372 # Gets rid of cherrypy parsing post file for args.
373 'request.process_request_body': False,
374 },
375 '/build':
376 {'response.timeout': 100000,
377 },
378 '/update':
379 {
380 # Gets rid of cherrypy parsing post file for args.
381 'request.process_request_body': False,
382 'response.timeout': 10000,
383 },
384 # Sets up the static dir for file hosting.
385 '/static':
386 {'tools.staticdir.dir': options.static_dir,
387 'tools.staticdir.on': True,
388 'response.timeout': 10000,
389 'tools.update_timestamp.on': True,
390 },
391 }
Chris Sosa5f118ef2012-07-12 11:37:50 -0700392 if options.production:
Alex Miller93beca52013-07-30 19:25:09 -0700393 base_config['global'].update({'server.thread_pool': 150})
Chris Sosa7cd23202013-10-15 17:22:57 -0700394 # TODO(sosa): Do this more cleanly.
395 gsutil_util.GSUTIL_ATTEMPTS = 5
Scott Zawalski1c5e7cd2012-02-27 13:12:52 -0500396
Chris Sosa7c931362010-10-11 19:49:01 -0700397 return base_config
rtc@google.com64244662009-11-12 00:52:08 +0000398
Darin Petkove17164a2010-08-11 13:24:41 -0700399
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700400def _GetRecursiveMemberObject(root, member_list):
401 """Returns an object corresponding to a nested member list.
402
403 Args:
404 root: the root object to search
405 member_list: list of nested members to search
Don Garrettf84631a2014-01-07 18:21:26 -0800406
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700407 Returns:
408 An object corresponding to the member name list; None otherwise.
409 """
410 for member in member_list:
411 next_root = root.__class__.__dict__.get(member)
412 if not next_root:
413 return None
414 root = next_root
415 return root
416
417
418def _IsExposed(name):
419 """Returns True iff |name| has an `exposed' attribute and it is set."""
420 return hasattr(name, 'exposed') and name.exposed
421
422
Gilad Arnold748c8322012-10-12 09:51:35 -0700423def _GetExposedMethod(root, nested_member, ignored=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700424 """Returns a CherryPy-exposed method, if such exists.
425
426 Args:
427 root: the root object for searching
428 nested_member: a slash-joined path to the nested member
429 ignored: method paths to be ignored
Don Garrettf84631a2014-01-07 18:21:26 -0800430
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700431 Returns:
432 A function object corresponding to the path defined by |member_list| from
433 the |root| object, if the function is exposed and not ignored; None
434 otherwise.
435 """
Gilad Arnold748c8322012-10-12 09:51:35 -0700436 method = (not (ignored and nested_member in ignored) and
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700437 _GetRecursiveMemberObject(root, nested_member.split('/')))
Gabe Black3b567202015-09-23 14:07:59 -0700438 if method and type(method) == types.FunctionType and _IsExposed(method):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700439 return method
440
441
Gilad Arnold748c8322012-10-12 09:51:35 -0700442def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700443 """Finds exposed CherryPy methods.
444
445 Args:
446 root: the root object for searching
447 prefix: slash-joined chain of members leading to current object
448 unlisted: URLs to be excluded regardless of their exposed status
Don Garrettf84631a2014-01-07 18:21:26 -0800449
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700450 Returns:
451 List of exposed URLs that are not unlisted.
452 """
453 method_list = []
454 for member in sorted(root.__class__.__dict__.keys()):
455 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 09:51:35 -0700456 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700457 continue
458 member_obj = root.__class__.__dict__[member]
459 if _IsExposed(member_obj):
460 if type(member_obj) == types.FunctionType:
461 method_list.append(prefixed_member)
462 else:
463 method_list += _FindExposedMethods(
464 member_obj, prefixed_member, unlisted)
465 return method_list
466
467
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700468class ApiRoot(object):
469 """RESTful API for Dev Server information."""
470 exposed = True
471
472 @cherrypy.expose
473 def hostinfo(self, ip):
474 """Returns a JSON dictionary containing information about the given ip.
475
Gilad Arnold1b908392012-10-05 11:36:27 -0700476 Args:
477 ip: address of host whose info is requested
Don Garrettf84631a2014-01-07 18:21:26 -0800478
Gilad Arnold1b908392012-10-05 11:36:27 -0700479 Returns:
480 A JSON dictionary containing all or some of the following fields:
481 last_event_type (int): last update event type received
482 last_event_status (int): last update event status received
483 last_known_version (string): last known version reported in update ping
484 forced_update_label (string): update label to force next update ping to
485 use, set by setnextupdate
486 See the OmahaEvent class in update_engine/omaha_request_action.h for
487 event type and status code definitions. If the ip does not exist an empty
488 string is returned.
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700489
Gilad Arnold1b908392012-10-05 11:36:27 -0700490 Example URL:
491 http://myhost/api/hostinfo?ip=192.168.1.5
492 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700493 return updater.HandleHostInfoPing(ip)
494
495 @cherrypy.expose
Gilad Arnold286a0062012-01-12 13:47:02 -0800496 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 11:36:27 -0700497 """Returns a JSON object containing a log of host event.
498
499 Args:
500 ip: address of host whose event log is requested, or `all'
Don Garrettf84631a2014-01-07 18:21:26 -0800501
Gilad Arnold1b908392012-10-05 11:36:27 -0700502 Returns:
503 A JSON encoded list (log) of dictionaries (events), each of which
504 containing a `timestamp' and other event fields, as described under
505 /api/hostinfo.
506
507 Example URL:
508 http://myhost/api/hostlog?ip=192.168.1.5
509 """
Gilad Arnold286a0062012-01-12 13:47:02 -0800510 return updater.HandleHostLogPing(ip)
511
512 @cherrypy.expose
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700513 def setnextupdate(self, ip):
514 """Allows the response to the next update ping from a host to be set.
515
516 Takes the IP of the host and an update label as normally provided to the
Gilad Arnold1b908392012-10-05 11:36:27 -0700517 /update command.
518 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700519 body_length = int(cherrypy.request.headers['Content-Length'])
520 label = cherrypy.request.rfile.read(body_length)
521
522 if label:
523 label = label.strip()
524 if label:
525 return updater.HandleSetUpdatePing(ip, label)
Chris Sosa4b951602014-04-09 20:26:07 -0700526 raise common_util.DevServerHTTPError(400, 'No label provided.')
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700527
528
Gilad Arnold55a2a372012-10-02 09:46:32 -0700529 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800530 def fileinfo(self, *args):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700531 """Returns information about a given staged file.
532
533 Args:
Don Garrettf84631a2014-01-07 18:21:26 -0800534 args: path to the file inside the server's static staging directory
535
Gilad Arnold55a2a372012-10-02 09:46:32 -0700536 Returns:
537 A JSON encoded dictionary with information about the said file, which may
538 contain the following keys/values:
Gilad Arnold1b908392012-10-05 11:36:27 -0700539 size (int): the file size in bytes
540 sha1 (string): a base64 encoded SHA1 hash
541 sha256 (string): a base64 encoded SHA256 hash
542
543 Example URL:
544 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 09:46:32 -0700545 """
Don Garrettf84631a2014-01-07 18:21:26 -0800546 file_path = os.path.join(updater.static_dir, *args)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700547 if not os.path.exists(file_path):
548 raise DevServerError('file not found: %s' % file_path)
549 try:
550 file_size = os.path.getsize(file_path)
551 file_sha1 = common_util.GetFileSha1(file_path)
552 file_sha256 = common_util.GetFileSha256(file_path)
553 except os.error, e:
554 raise DevServerError('failed to get info for file %s: %s' %
Gilad Arnolde74b3812013-04-22 11:27:38 -0700555 (file_path, e))
556
557 is_delta = autoupdate.Autoupdate.IsDeltaFormatFile(file_path)
558
559 return json.dumps({
560 autoupdate.Autoupdate.SIZE_ATTR: file_size,
561 autoupdate.Autoupdate.SHA1_ATTR: file_sha1,
562 autoupdate.Autoupdate.SHA256_ATTR: file_sha256,
563 autoupdate.Autoupdate.ISDELTA_ATTR: is_delta
564 })
Gilad Arnold55a2a372012-10-02 09:46:32 -0700565
Chris Sosa76e44b92013-01-31 12:11:38 -0800566
David Rochberg7c79a812011-01-19 14:24:45 -0500567class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700568 """The Root Class for the Dev Server.
569
570 CherryPy works as follows:
571 For each method in this class, cherrpy interprets root/path
572 as a call to an instance of DevServerRoot->method_name. For example,
573 a call to http://myhost/build will call build. CherryPy automatically
574 parses http args and places them as keyword arguments in each method.
575 For paths http://myhost/update/dir1/dir2, you can use *args so that
576 cherrypy uses the update method and puts the extra paths in args.
577 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700578 # Method names that should not be listed on the index page.
579 _UNLISTED_METHODS = ['index', 'doc']
580
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700581 api = ApiRoot()
Chris Sosa7c931362010-10-11 19:49:01 -0700582
Dan Shi59ae7092013-06-04 14:37:27 -0700583 # Number of threads that devserver is staging images.
584 _staging_thread_count = 0
585 # Lock used to lock increasing/decreasing count.
586 _staging_thread_count_lock = threading.Lock()
587
Dan Shiafd0e492015-05-27 14:23:51 -0700588 @require_psutil()
589 def _refresh_io_stats(self):
590 """A call running in a thread to update IO stats periodically."""
591 prev_disk_io_counters = psutil.disk_io_counters()
592 prev_network_io_counters = psutil.net_io_counters()
593 prev_read_time = time.time()
594 while True:
595 time.sleep(STATS_INTERVAL)
596 now = time.time()
597 interval = now - prev_read_time
598 prev_read_time = now
599 # Disk IO is for all disks.
600 disk_io_counters = psutil.disk_io_counters()
601 network_io_counters = psutil.net_io_counters()
602
603 self.disk_read_bytes_per_sec = (
604 disk_io_counters.read_bytes -
605 prev_disk_io_counters.read_bytes)/interval
606 self.disk_write_bytes_per_sec = (
607 disk_io_counters.write_bytes -
608 prev_disk_io_counters.write_bytes)/interval
609 prev_disk_io_counters = disk_io_counters
610
611 self.network_sent_bytes_per_sec = (
612 network_io_counters.bytes_sent -
613 prev_network_io_counters.bytes_sent)/interval
614 self.network_recv_bytes_per_sec = (
615 network_io_counters.bytes_recv -
616 prev_network_io_counters.bytes_recv)/interval
617 prev_network_io_counters = network_io_counters
618
619 @require_psutil()
620 def _start_io_stat_thread(self):
Gabe Black3b567202015-09-23 14:07:59 -0700621 """Start the thread to collect IO stats."""
Dan Shiafd0e492015-05-27 14:23:51 -0700622 thread = threading.Thread(target=self._refresh_io_stats)
623 thread.daemon = True
624 thread.start()
625
joychen3cb228e2013-06-12 12:13:13 -0700626 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700627 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800628 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 12:13:13 -0700629 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 14:24:45 -0500630
Dan Shiafd0e492015-05-27 14:23:51 -0700631 # Cache of disk IO stats, a thread refresh the stats every 10 seconds.
632 # lock is not used for these variables as the only thread writes to these
633 # variables is _refresh_io_stats.
634 self.disk_read_bytes_per_sec = 0
635 self.disk_write_bytes_per_sec = 0
636 # Cache of network IO stats.
637 self.network_sent_bytes_per_sec = 0
638 self.network_recv_bytes_per_sec = 0
639 self._start_io_stat_thread()
640
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700641 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500642 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700643 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700644 import builder
645 if self._builder is None:
646 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500647 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700648
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700649 @cherrypy.expose
Dan Shif8eb0d12013-08-01 17:52:06 -0700650 def is_staged(self, **kwargs):
651 """Check if artifacts have been downloaded.
652
Chris Sosa6b0c6172013-08-05 17:01:33 -0700653 async: True to return without waiting for download to complete.
654 artifacts: Comma separated list of named artifacts to download.
655 These are defined in artifact_info and have their implementation
656 in build_artifact.py.
657 files: Comma separated list of file artifacts to stage. These
658 will be available as is in the corresponding static directory with no
659 custom post-processing.
660
661 returns: True of all artifacts are staged.
Dan Shif8eb0d12013-08-01 17:52:06 -0700662
663 Example:
664 To check if autotest and test_suites are staged:
665 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
666 artifacts=autotest,test_suites
667 """
Gabe Black3b567202015-09-23 14:07:59 -0700668 dl, factory = _get_downloader_and_factory(kwargs)
Aviv Keshet57d18172016-06-18 20:39:09 -0700669 response = str(dl.IsStaged(factory))
670 _Log('Responding to is_staged %s request with %r', kwargs, response)
671 return response
Dan Shi59ae7092013-06-04 14:37:27 -0700672
Chris Sosa76e44b92013-01-31 12:11:38 -0800673 @cherrypy.expose
Prashanth Ba06d2d22014-03-07 15:35:19 -0800674 def list_image_dir(self, **kwargs):
675 """Take an archive url and list the contents in its staged directory.
676
677 Args:
678 kwargs:
679 archive_url: Google Storage URL for the build.
680
681 Example:
682 To list the contents of where this devserver should have staged
683 gs://image-archive/<board>-release/<build> call:
684 http://devserver_url:<port>/list_image_dir?archive_url=<gs://..>
685
686 Returns:
687 A string with information about the contents of the image directory.
688 """
Gabe Black3b567202015-09-23 14:07:59 -0700689 dl = _get_downloader(kwargs)
Prashanth Ba06d2d22014-03-07 15:35:19 -0800690 try:
Gabe Black3b567202015-09-23 14:07:59 -0700691 image_dir_contents = dl.ListBuildDir()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800692 except build_artifact.ArtifactDownloadError as e:
693 return 'Cannot list the contents of staged artifacts. %s' % e
694 if not image_dir_contents:
Gabe Black3b567202015-09-23 14:07:59 -0700695 return '%s has not been staged on this devserver.' % dl.DescribeSource()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800696 return image_dir_contents
697
698 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800699 def stage(self, **kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700700 """Downloads and caches build artifacts.
Chris Sosa76e44b92013-01-31 12:11:38 -0800701
Gabe Black3b567202015-09-23 14:07:59 -0700702 Downloads and caches build artifacts, possibly from a Google Storage URL,
Dan Shi72b16132015-10-08 12:10:33 -0700703 or from Android's build server. Returns once these have been downloaded
Gabe Black3b567202015-09-23 14:07:59 -0700704 on the devserver. A call to this will attempt to cache non-specified
705 artifacts in the background for the given from the given URL following
706 the principle of spatial locality. Spatial locality of different
Chris Sosa76e44b92013-01-31 12:11:38 -0800707 artifacts is explicitly defined in the build_artifact module.
708
709 These artifacts will then be available from the static/ sub-directory of
710 the devserver.
711
712 Args:
713 archive_url: Google Storage URL for the build.
Simran Basi4243a862014-12-12 12:48:33 -0800714 local_path: Local path for the build.
Dan Shif8eb0d12013-08-01 17:52:06 -0700715 async: True to return without waiting for download to complete.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700716 artifacts: Comma separated list of named artifacts to download.
717 These are defined in artifact_info and have their implementation
718 in build_artifact.py.
719 files: Comma separated list of files to stage. These
720 will be available as is in the corresponding static directory with no
721 custom post-processing.
Laurence Goodbyf5c958d2016-01-14 18:23:56 -0800722 clean: True to remove any previously staged artifacts first.
Chris Sosa76e44b92013-01-31 12:11:38 -0800723
724 Example:
725 To download the autotest and test suites tarballs:
726 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
727 artifacts=autotest,test_suites
728 To download the full update payload:
729 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
730 artifacts=full_payload
Chris Sosa6b0c6172013-08-05 17:01:33 -0700731 To download just a file called blah.bin:
732 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
733 files=blah.bin
Chris Sosa76e44b92013-01-31 12:11:38 -0800734
735 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 16:39:34 -0700736 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 12:11:38 -0800737
738 Note for this example, relative path is the archive_url stripped of its
739 basename i.e. path/ in the examples above. Specific example:
740
741 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
742
743 Will get staged to:
744
joychened64b222013-06-21 16:39:34 -0700745 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Chris Sosa76e44b92013-01-31 12:11:38 -0800746 """
Gabe Black3b567202015-09-23 14:07:59 -0700747 dl, factory = _get_downloader_and_factory(kwargs)
748
Dan Shi59ae7092013-06-04 14:37:27 -0700749 with DevServerRoot._staging_thread_count_lock:
750 DevServerRoot._staging_thread_count += 1
751 try:
Laurence Goodbyf5c958d2016-01-14 18:23:56 -0800752 boolean_string = kwargs.get('clean')
753 clean = xbuddy.XBuddy.ParseBoolean(boolean_string)
754 if clean and os.path.exists(dl.GetBuildDir()):
755 _Log('Removing %s' % dl.GetBuildDir())
756 shutil.rmtree(dl.GetBuildDir())
Gabe Black3b567202015-09-23 14:07:59 -0700757 async = kwargs.get('async', False)
758 dl.Download(factory, async=async)
Dan Shi59ae7092013-06-04 14:37:27 -0700759 finally:
760 with DevServerRoot._staging_thread_count_lock:
761 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800762 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700763
764 @cherrypy.expose
Dan Shi2f136862016-02-11 15:38:38 -0800765 def locate_file(self, **kwargs):
766 """Get the path to the given file name.
767
768 This method looks up the given file name inside specified build artifacts.
769 One use case is to help caller to locate an apk file inside a build
770 artifact. The location of the apk file could be different based on the
771 branch and target.
772
773 Args:
774 file_name: Name of the file to look for.
775 artifacts: A list of artifact names to search for the file.
776
777 Returns:
778 Path to the file with the given name. It's relative to the folder for the
779 build, e.g., DATA/priv-app/sl4a/sl4a.apk
780
781 """
782 dl, _ = _get_downloader_and_factory(kwargs)
783 try:
784 file_name = kwargs['file_name'].lower()
785 artifacts = kwargs['artifacts']
786 except KeyError:
787 raise DevServerError('`file_name` and `artifacts` are required to search '
788 'for a file in build artifacts.')
789 build_path = dl.GetBuildDir()
790 for artifact in artifacts:
791 # Get the unzipped folder of the artifact. If it's not defined in
792 # ARTIFACT_UNZIP_FOLDER_MAP, assume the files are unzipped to the build
793 # directory directly.
794 folder = artifact_info.ARTIFACT_UNZIP_FOLDER_MAP.get(artifact, '')
795 artifact_path = os.path.join(build_path, folder)
796 for root, _, filenames in os.walk(artifact_path):
797 if file_name in set([f.lower() for f in filenames]):
798 return os.path.relpath(os.path.join(root, file_name), build_path)
799 raise DevServerError('File `%s` can not be found in artifacts: %s' %
800 (file_name, artifacts))
801
802 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -0800803 def setup_telemetry(self, **kwargs):
804 """Extracts and sets up telemetry
805
806 This method goes through the telemetry deps packages, and stages them on
807 the devserver to be used by the drones and the telemetry tests.
808
809 Args:
810 archive_url: Google Storage URL for the build.
811
812 Returns:
813 Path to the source folder for the telemetry codebase once it is staged.
814 """
Gabe Black3b567202015-09-23 14:07:59 -0700815 dl = _get_downloader(kwargs)
Simran Basi4baad082013-02-14 13:39:18 -0800816
Gabe Black3b567202015-09-23 14:07:59 -0700817 build_path = dl.GetBuildDir()
Simran Basi4baad082013-02-14 13:39:18 -0800818 deps_path = os.path.join(build_path, 'autotest/packages')
819 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
820 src_folder = os.path.join(telemetry_path, 'src')
821
822 with self._telemetry_lock_dict.lock(telemetry_path):
823 if os.path.exists(src_folder):
824 # Telemetry is already fully stage return
825 return src_folder
826
827 common_util.MkDirP(telemetry_path)
828
829 # Copy over the required deps tar balls to the telemetry directory.
830 for dep in TELEMETRY_DEPS:
831 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -0700832 if not os.path.exists(dep_path):
833 # This dep does not exist (could be new), do not extract it.
834 continue
Simran Basi4baad082013-02-14 13:39:18 -0800835 try:
836 common_util.ExtractTarball(dep_path, telemetry_path)
837 except common_util.CommonUtilError as e:
838 shutil.rmtree(telemetry_path)
839 raise DevServerError(str(e))
840
841 # By default all the tarballs extract to test_src but some parts of
842 # the telemetry code specifically hardcoded to exist inside of 'src'.
843 test_src = os.path.join(telemetry_path, 'test_src')
844 try:
845 shutil.move(test_src, src_folder)
846 except shutil.Error:
847 # This can occur if src_folder already exists. Remove and retry move.
848 shutil.rmtree(src_folder)
Gabe Black3b567202015-09-23 14:07:59 -0700849 raise DevServerError(
850 'Failure in telemetry setup for build %s. Appears that the '
851 'test_src to src move failed.' % dl.GetBuild())
Simran Basi4baad082013-02-14 13:39:18 -0800852
853 return src_folder
854
855 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800856 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -0700857 """Symbolicates a minidump using pre-downloaded symbols, returns it.
858
859 Callers will need to POST to this URL with a body of MIME-type
860 "multipart/form-data".
861 The body should include a single argument, 'minidump', containing the
862 binary-formatted minidump to symbolicate.
863
Chris Masone816e38c2012-05-02 12:22:36 -0700864 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -0800865 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -0700866 minidump: The binary minidump file to symbolicate.
867 """
Gabe Black3b567202015-09-23 14:07:59 -0700868 kwargs['artifacts'] = 'symbols'
869 dl = _get_downloader(kwargs)
870
Chris Sosa76e44b92013-01-31 12:11:38 -0800871 # Ensure the symbols have been staged.
Gabe Black3b567202015-09-23 14:07:59 -0700872 if self.stage(**kwargs) != 'Success':
873 raise DevServerError('Failed to stage symbols for %s' %
874 dl.DescribeSource())
Chris Sosa76e44b92013-01-31 12:11:38 -0800875
Chris Masone816e38c2012-05-02 12:22:36 -0700876 to_return = ''
877 with tempfile.NamedTemporaryFile() as local:
878 while True:
879 data = minidump.file.read(8192)
880 if not data:
881 break
882 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -0800883
Chris Masone816e38c2012-05-02 12:22:36 -0700884 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -0800885
Gabe Black3b567202015-09-23 14:07:59 -0700886 symbols_directory = os.path.join(dl.GetBuildDir(), 'debug', 'breakpad')
Chris Sosa76e44b92013-01-31 12:11:38 -0800887
888 stackwalk = subprocess.Popen(
889 ['minidump_stackwalk', local.name, symbols_directory],
890 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
891
Chris Masone816e38c2012-05-02 12:22:36 -0700892 to_return, error_text = stackwalk.communicate()
893 if stackwalk.returncode != 0:
894 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
895 error_text, stackwalk.returncode))
896
897 return to_return
898
899 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800900 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 15:31:36 -0400901 """Return a string representing the latest build for a given target.
902
903 Args:
904 target: The build target, typically a combination of the board and the
905 type of build e.g. x86-mario-release.
906 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
907 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-07 18:21:26 -0800908
Scott Zawalski16954532012-03-20 15:31:36 -0400909 Returns:
910 A string representation of the latest build if one exists, i.e.
911 R19-1993.0.0-a1-b1480.
912 An empty string if no latest could be found.
913 """
Don Garrettf84631a2014-01-07 18:21:26 -0800914 if not kwargs:
Scott Zawalski16954532012-03-20 15:31:36 -0400915 return _PrintDocStringAsHTML(self.latestbuild)
916
Don Garrettf84631a2014-01-07 18:21:26 -0800917 if 'target' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -0700918 raise common_util.DevServerHTTPError(500, 'Error: target= is required!')
Dan Shi61305df2015-10-26 16:52:35 -0700919
920 if _is_android_build_request(kwargs):
921 branch = kwargs.get('branch', None)
922 target = kwargs.get('target', None)
923 if not target or not branch:
924 raise DevServerError(
925 'Both target and branch must be specified to query for the latest '
926 'Android build.')
927 return android_build.BuildAccessor.GetLatestBuildID(target, branch)
928
Scott Zawalski16954532012-03-20 15:31:36 -0400929 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700930 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-07 18:21:26 -0800931 updater.static_dir, kwargs['target'],
932 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700933 except common_util.CommonUtilError as errmsg:
Chris Sosa4b951602014-04-09 20:26:07 -0700934 raise common_util.DevServerHTTPError(500, str(errmsg))
Scott Zawalski16954532012-03-20 15:31:36 -0400935
936 @cherrypy.expose
xixuan7efd0002016-04-14 15:34:01 -0700937 def list_suite_controls(self, **kwargs):
938 """Return a list of contents of all known control files.
939
940 Example URL:
941 To List all control files' content:
942 http://dev-server/list_suite_controls?suite_name=bvt&
943 build=daisy_spring-release/R29-4279.0.0
944
945 Args:
946 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
947 suite_name: List the control files belonging to that suite.
948
949 Returns:
Dan Shia1cd6522016-04-18 16:07:21 -0700950 A dictionary of all control files's path to its content for given suite.
xixuan7efd0002016-04-14 15:34:01 -0700951 """
952 if not kwargs:
953 return _PrintDocStringAsHTML(self.controlfiles)
954
955 if 'build' not in kwargs:
956 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
957
958 if 'suite_name' not in kwargs:
Dan Shia1cd6522016-04-18 16:07:21 -0700959 raise common_util.DevServerHTTPError(500,
960 'Error: suite_name= is required!')
xixuan7efd0002016-04-14 15:34:01 -0700961
962 control_file_list = [
963 line.rstrip() for line in common_util.GetControlFileListForSuite(
964 updater.static_dir, kwargs['build'],
965 kwargs['suite_name']).splitlines()]
966
Dan Shia1cd6522016-04-18 16:07:21 -0700967 control_file_content_dict = {}
xixuan7efd0002016-04-14 15:34:01 -0700968 for control_path in control_file_list:
Dan Shia1cd6522016-04-18 16:07:21 -0700969 control_file_content_dict[control_path] = (common_util.GetControlFile(
xixuan7efd0002016-04-14 15:34:01 -0700970 updater.static_dir, kwargs['build'], control_path))
971
Dan Shia1cd6522016-04-18 16:07:21 -0700972 return json.dumps(control_file_content_dict)
xixuan7efd0002016-04-14 15:34:01 -0700973
974 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800975 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500976 """Return a control file or a list of all known control files.
977
978 Example URL:
979 To List all control files:
beepsbd337242013-07-09 22:44:06 -0700980 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
981 To List all control files for, say, the bvt suite:
982 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -0500983 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500984 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 -0500985
986 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500987 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500988 control_path: If you want the contents of a control file set this
989 to the path. E.g. client/site_tests/sleeptest/control
990 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -0700991 suite_name: If control_path is not specified but a suite_name is
992 specified, list the control files belonging to that suite instead of
993 all control files. The empty string for suite_name will list all control
994 files for the build.
Don Garrettf84631a2014-01-07 18:21:26 -0800995
Scott Zawalski4647ce62012-01-03 17:17:28 -0500996 Returns:
997 Contents of a control file if control_path is provided.
998 A list of control files if no control_path is provided.
999 """
Don Garrettf84631a2014-01-07 18:21:26 -08001000 if not kwargs:
Scott Zawalski4647ce62012-01-03 17:17:28 -05001001 return _PrintDocStringAsHTML(self.controlfiles)
1002
Don Garrettf84631a2014-01-07 18:21:26 -08001003 if 'build' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -07001004 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -05001005
Don Garrettf84631a2014-01-07 18:21:26 -08001006 if 'control_path' not in kwargs:
1007 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-09 22:44:06 -07001008 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-07 18:21:26 -08001009 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-09 22:44:06 -07001010 else:
1011 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-07 18:21:26 -08001012 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -05001013 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001014 return common_util.GetControlFile(
Don Garrettf84631a2014-01-07 18:21:26 -08001015 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -08001016
1017 @cherrypy.expose
Simran Basi99e63c02014-05-20 10:39:52 -07001018 def xbuddy_translate(self, *args, **kwargs):
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001019 """Translates an xBuddy path to a real path to artifact if it exists.
1020
1021 Args:
Simran Basi99e63c02014-05-20 10:39:52 -07001022 args: An xbuddy path in the form of {local|remote}/build_id/artifact.
1023 Local searches the devserver's static directory. Remote searches a
1024 Google Storage image archive.
1025
1026 Kwargs:
1027 image_dir: Google Storage image archive to search in if requesting a
1028 remote artifact. If none uses the default bucket.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001029
1030 Returns:
Simran Basi99e63c02014-05-20 10:39:52 -07001031 String in the format of build_id/artifact as stored on the local server
1032 or in Google Storage.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001033 """
Simran Basi99e63c02014-05-20 10:39:52 -07001034 build_id, filename = self._xbuddy.Translate(
Gabe Black3b567202015-09-23 14:07:59 -07001035 args, image_dir=kwargs.get('image_dir'))
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001036 response = os.path.join(build_id, filename)
1037 _Log('Path translation requested, returning: %s', response)
1038 return response
1039
1040 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -07001041 def xbuddy(self, *args, **kwargs):
1042 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -07001043
1044 Args:
joycheneaf4cfc2013-07-02 08:38:57 -07001045 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -07001046 components of the path. The path can be understood as
1047 "{local|remote}/build_id/artifact" where build_id is composed of
1048 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -07001049
joychen121fc9b2013-08-02 14:30:30 -07001050 The first path element is optional, and can be "remote" or "local"
1051 If local (the default), devserver will not attempt to access Google
1052 Storage, and will only search the static directory for the files.
1053 If remote, devserver will try to obtain the artifact off GS if it's
1054 not found locally.
1055 The board is the familiar board name, optionally suffixed.
1056 The version can be the google storage version number, and may also be
1057 any of a number of xBuddy defined version aliases that will be
1058 translated into the latest built image that fits the description.
1059 Defaults to latest.
1060 The artifact is one of a number of image or artifact aliases used by
1061 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -07001062
1063 Kwargs:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001064 for_update: {true|false}
1065 if true, pregenerates the update payloads for the image,
1066 and returns the update uri to pass to the
1067 update_engine_client.
joychen3cb228e2013-06-12 12:13:13 -07001068 return_dir: {true|false}
1069 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001070 relative_path: {true|false}
1071 if set to true, returns the relative path to the payload
1072 directory from static_dir.
joychen3cb228e2013-06-12 12:13:13 -07001073 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -07001074 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -07001075 or
joycheneaf4cfc2013-07-02 08:38:57 -07001076 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -07001077
1078 Returns:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001079 If |for_update|, returns a redirect to the image or update file
1080 on the devserver. E.g.,
1081 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
1082 chromium-test-image.bin
1083 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
1084 http://host:port/static/x86-generic-release/R26-4000.0.0/
1085 If |relative_path| is true, return a relative path the folder where the
1086 payloads are. E.g.,
1087 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 12:13:13 -07001088 """
Chris Sosa75490802013-09-30 17:21:45 -07001089 boolean_string = kwargs.get('for_update')
1090 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001091 boolean_string = kwargs.get('return_dir')
1092 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
1093 boolean_string = kwargs.get('relative_path')
1094 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -07001095
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001096 if return_dir and relative_path:
Chris Sosa4b951602014-04-09 20:26:07 -07001097 raise common_util.DevServerHTTPError(
1098 500, 'Cannot specify both return_dir and relative_path')
Chris Sosa75490802013-09-30 17:21:45 -07001099
1100 # For updates, we optimize downloading of test images.
1101 file_name = None
1102 build_id = None
1103 if for_update:
1104 try:
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001105 build_id = self._xbuddy.StageTestArtifactsForUpdate(args)
Chris Sosa75490802013-09-30 17:21:45 -07001106 except build_artifact.ArtifactDownloadError:
1107 build_id = None
1108
1109 if not build_id:
1110 build_id, file_name = self._xbuddy.Get(args)
1111
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001112 if for_update:
1113 _Log('Payload generation triggered by request')
1114 # Forces payload to be in cache and symlinked into build_id dir.
Chris Sosa75490802013-09-30 17:21:45 -07001115 updater.GetUpdateForLabel(autoupdate.FORCED_UPDATE, build_id,
1116 image_name=file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001117
1118 response = None
1119 if return_dir:
1120 response = os.path.join(cherrypy.request.base, 'static', build_id)
1121 _Log('Directory requested, returning: %s', response)
1122 elif relative_path:
1123 response = build_id
1124 _Log('Relative path requested, returning: %s', response)
1125 elif for_update:
1126 response = os.path.join(cherrypy.request.base, 'update', build_id)
1127 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 12:13:13 -07001128 else:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001129 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 14:30:30 -07001130 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001131 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 14:30:30 -07001132 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -07001133
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001134 return response
1135
joychen3cb228e2013-06-12 12:13:13 -07001136 @cherrypy.expose
1137 def xbuddy_list(self):
1138 """Lists the currently available images & time since last access.
1139
Gilad Arnold452fd272014-02-04 11:09:28 -08001140 Returns:
1141 A string representation of a list of tuples [(build_id, time since last
1142 access),...]
joychen3cb228e2013-06-12 12:13:13 -07001143 """
1144 return self._xbuddy.List()
1145
1146 @cherrypy.expose
1147 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 11:09:28 -08001148 """Returns the number of images cached by xBuddy."""
joychen3cb228e2013-06-12 12:13:13 -07001149 return self._xbuddy.Capacity()
1150
1151 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001152 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001153 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001154 return ('Welcome to the Dev Server!<br>\n'
1155 '<br>\n'
1156 'Here are the available methods, click for documentation:<br>\n'
1157 '<br>\n'
1158 '%s' %
1159 '<br>\n'.join(
1160 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001161 for name in _FindExposedMethods(
1162 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001163
1164 @cherrypy.expose
1165 def doc(self, *args):
1166 """Shows the documentation for available methods / URLs.
1167
1168 Example:
1169 http://myhost/doc/update
1170 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001171 name = '/'.join(args)
1172 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001173 if not method:
1174 raise DevServerError("No exposed method named `%s'" % name)
1175 if not method.__doc__:
1176 raise DevServerError("No documentation for exposed method `%s'" % name)
1177 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -07001178
Dale Curtisc9aaf3a2011-08-09 15:47:40 -07001179 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001180 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001181 """Handles an update check from a Chrome OS client.
1182
1183 The HTTP request should contain the standard Omaha-style XML blob. The URL
1184 line may contain an additional intermediate path to the update payload.
1185
joychen121fc9b2013-08-02 14:30:30 -07001186 This request can be handled in one of 4 ways, depending on the devsever
1187 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -07001188
joychen121fc9b2013-08-02 14:30:30 -07001189 1. No intermediate path
1190 If no intermediate path is given, the default behavior is to generate an
1191 update payload from the latest test image locally built for the board
1192 specified in the xml. Devserver serves the generated payload.
1193
1194 2. Path explicitly invokes XBuddy
1195 If there is a path given, it can explicitly invoke xbuddy by prefixing it
1196 with 'xbuddy'. This path is then used to acquire an image binary for the
1197 devserver to generate an update payload from. Devserver then serves this
1198 payload.
1199
1200 3. Path is left for the devserver to interpret.
1201 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
1202 to generate a payload from the test image in that directory and serve it.
1203
1204 4. The devserver is in a 'forced' mode. TO BE DEPRECATED
1205 This comes from the usage of --forced_payload or --image when starting the
1206 devserver. No matter what path (or no path) gets passed in, devserver will
1207 serve the update payload (--forced_payload) or generate an update payload
1208 from the image (--image).
1209
1210 Examples:
1211 1. No intermediate path
1212 update_engine_client --omaha_url=http://myhost/update
1213 This generates an update payload from the latest test image locally built
1214 for the board specified in the xml.
1215
1216 2. Explicitly invoke xbuddy
1217 update_engine_client --omaha_url=
1218 http://myhost/update/xbuddy/remote/board/version/dev
1219 This would go to GS to download the dev image for the board, from which
1220 the devserver would generate a payload to serve.
1221
1222 3. Give a path for devserver to interpret
1223 update_engine_client --omaha_url=http://myhost/update/some/random/path
1224 This would attempt, in order to:
1225 a) Generate an update from a test image binary if found in
1226 static_dir/some/random/path.
1227 b) Serve an update payload found in static_dir/some/random/path.
1228 c) Hope that some/random/path takes the form "board/version" and
1229 and attempt to download an update payload for that board/version
1230 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001231 """
joychen121fc9b2013-08-02 14:30:30 -07001232 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -08001233 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -07001234 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-11 19:49:01 -07001235
joychen121fc9b2013-08-02 14:30:30 -07001236 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001237
Dan Shiafd0e492015-05-27 14:23:51 -07001238 @require_psutil()
1239 def _get_io_stats(self):
1240 """Get the IO stats as a dictionary.
1241
Gabe Black3b567202015-09-23 14:07:59 -07001242 Returns:
1243 A dictionary of IO stats collected by psutil.
Dan Shiafd0e492015-05-27 14:23:51 -07001244
1245 """
1246 return {'disk_read_bytes_per_second': self.disk_read_bytes_per_sec,
1247 'disk_write_bytes_per_second': self.disk_write_bytes_per_sec,
1248 'disk_total_bytes_per_second': (self.disk_read_bytes_per_sec +
1249 self.disk_write_bytes_per_sec),
1250 'network_sent_bytes_per_second': self.network_sent_bytes_per_sec,
1251 'network_recv_bytes_per_second': self.network_recv_bytes_per_sec,
1252 'network_total_bytes_per_second': (self.network_sent_bytes_per_sec +
1253 self.network_recv_bytes_per_sec),
1254 'cpu_percent': psutil.cpu_percent(),}
1255
Dan Shi7247f9c2016-06-01 09:19:09 -07001256
1257 def _get_process_count(self, process_cmd_pattern):
1258 """Get the count of processes that match the given command pattern.
1259
1260 Args:
1261 process_cmd_pattern: The regex pattern of process command to match.
1262
1263 Returns:
1264 The count of processes that match the given command pattern.
1265 """
1266 try:
1267 return int(subprocess.check_output(
1268 'pgrep -fc "%s"' % process_cmd_pattern, shell=True))
1269 except subprocess.CalledProcessError:
1270 return 0
1271
1272
Dan Shif5ce2de2013-04-25 16:06:32 -07001273 @cherrypy.expose
1274 def check_health(self):
1275 """Collect the health status of devserver to see if it's ready for staging.
1276
Gilad Arnold452fd272014-02-04 11:09:28 -08001277 Returns:
1278 A JSON dictionary containing all or some of the following fields:
1279 free_disk (int): free disk space in GB
1280 staging_thread_count (int): number of devserver threads currently staging
1281 an image
Dan Shi7247f9c2016-06-01 09:19:09 -07001282 apache_client_count (int): count of Apache processes.
1283 telemetry_test_count (int): count of telemetry tests.
1284 gsutil_count (int): count of gsutil processes.
Dan Shif5ce2de2013-04-25 16:06:32 -07001285 """
1286 # Get free disk space.
1287 stat = os.statvfs(updater.static_dir)
1288 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
Dan Shi7247f9c2016-06-01 09:19:09 -07001289 apache_client_count = self._get_process_count('apache')
1290 telemetry_test_count = self._get_process_count('python.*telemetry')
1291 gsutil_count = self._get_process_count('gsutil')
Dan Shif5ce2de2013-04-25 16:06:32 -07001292
Dan Shiafd0e492015-05-27 14:23:51 -07001293 health_data = {
Dan Shif5ce2de2013-04-25 16:06:32 -07001294 'free_disk': free_disk,
Dan Shid76e6bb2016-01-28 22:28:51 -08001295 'staging_thread_count': DevServerRoot._staging_thread_count,
1296 'apache_client_count': apache_client_count,
Dan Shi7247f9c2016-06-01 09:19:09 -07001297 'telemetry_test_count': telemetry_test_count,
1298 'gsutil_count': gsutil_count}
Dan Shiafd0e492015-05-27 14:23:51 -07001299 health_data.update(self._get_io_stats() or {})
1300
1301 return json.dumps(health_data)
Dan Shif5ce2de2013-04-25 16:06:32 -07001302
1303
Chris Sosadbc20082012-12-10 13:39:11 -08001304def _CleanCache(cache_dir, wipe):
1305 """Wipes any excess cached items in the cache_dir.
1306
1307 Args:
1308 cache_dir: the directory we are wiping from.
1309 wipe: If True, wipe all the contents -- not just the excess.
1310 """
1311 if wipe:
1312 # Clear the cache and exit on error.
1313 cmd = 'rm -rf %s/*' % cache_dir
1314 if os.system(cmd) != 0:
1315 _Log('Failed to clear the cache with %s' % cmd)
1316 sys.exit(1)
1317 else:
1318 # Clear all but the last N cached updates
1319 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
1320 (cache_dir, CACHED_ENTRIES))
1321 if os.system(cmd) != 0:
1322 _Log('Failed to clean up old delta cache files with %s' % cmd)
1323 sys.exit(1)
1324
1325
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001326def _AddTestingOptions(parser):
1327 group = optparse.OptionGroup(
1328 parser, 'Advanced Testing Options', 'These are used by test scripts and '
1329 'developers writing integration tests utilizing the devserver. They are '
1330 'not intended to be really used outside the scope of someone '
1331 'knowledgable about the test.')
1332 group.add_option('--exit',
1333 action='store_true',
1334 help='do not start the server (yet pregenerate/clear cache)')
1335 group.add_option('--host_log',
1336 action='store_true', default=False,
1337 help='record history of host update events (/api/hostlog)')
1338 group.add_option('--max_updates',
Gabe Black3b567202015-09-23 14:07:59 -07001339 metavar='NUM', default=-1, type='int',
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001340 help='maximum number of update checks handled positively '
1341 '(default: unlimited)')
1342 group.add_option('--private_key',
1343 metavar='PATH', default=None,
1344 help='path to the private key in pem format. If this is set '
1345 'the devserver will generate update payloads that are '
1346 'signed with this key.')
David Zeuthen52ccd012013-10-31 12:58:26 -07001347 group.add_option('--private_key_for_metadata_hash_signature',
1348 metavar='PATH', default=None,
1349 help='path to the private key in pem format. If this is set '
1350 'the devserver will sign the metadata hash with the given '
1351 'key and transmit in the Omaha-style XML response.')
1352 group.add_option('--public_key',
1353 metavar='PATH', default=None,
1354 help='path to the public key in pem format. If this is set '
1355 'the devserver will transmit a base64 encoded version of '
1356 'the content in the Omaha-style XML response.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001357 group.add_option('--proxy_port',
1358 metavar='PORT', default=None, type='int',
1359 help='port to have the client connect to -- basically the '
1360 'devserver lies to the update to tell it to get the payload '
1361 'from a different port that will proxy the request back to '
1362 'the devserver. The proxy must be managed outside the '
1363 'devserver.')
1364 group.add_option('--remote_payload',
1365 action='store_true', default=False,
Chris Sosa4b951602014-04-09 20:26:07 -07001366 help='Payload is being served from a remote machine. With '
1367 'this setting enabled, this devserver instance serves as '
1368 'just an Omaha server instance. In this mode, the '
1369 'devserver enforces a few extra components of the Omaha '
Chris Sosafc715442014-04-09 20:45:23 -07001370 'protocol, such as hardware class, being sent.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001371 group.add_option('-u', '--urlbase',
1372 metavar='URL',
Gabe Black3b567202015-09-23 14:07:59 -07001373 help='base URL for update images, other than the '
1374 'devserver. Use in conjunction with remote_payload.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001375 parser.add_option_group(group)
1376
1377
1378def _AddUpdateOptions(parser):
1379 group = optparse.OptionGroup(
1380 parser, 'Autoupdate Options', 'These options can be used to change '
1381 'how the devserver either generates or serve update payloads. Please '
1382 'note that all of these option affect how a payload is generated and so '
1383 'do not work in archive-only mode.')
1384 group.add_option('--board',
1385 help='By default the devserver will create an update '
1386 'payload from the latest image built for the board '
1387 'a device that is requesting an update has. When we '
1388 'pre-generate an update (see below) and we do not specify '
1389 'another update_type option like image or payload, the '
1390 'devserver needs to know the board to generate the latest '
1391 'image for. This is that board.')
1392 group.add_option('--critical_update',
1393 action='store_true', default=False,
1394 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001395 group.add_option('--image',
1396 metavar='FILE',
1397 help='Generate and serve an update using this image to any '
1398 'device that requests an update.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001399 group.add_option('--payload',
1400 metavar='PATH',
1401 help='use the update payload from specified directory '
1402 '(update.gz).')
1403 group.add_option('-p', '--pregenerate_update',
1404 action='store_true', default=False,
1405 help='pre-generate the update payload before accepting '
1406 'update requests. Useful to help debug payload generation '
1407 'issues quickly. Also if an update payload will take a '
1408 'long time to generate, a client may timeout if you do not'
1409 'pregenerate the update.')
1410 group.add_option('--src_image',
1411 metavar='PATH', default='',
1412 help='If specified, delta updates will be generated using '
1413 'this image as the source image. Delta updates are when '
1414 'you are updating from a "source image" to a another '
1415 'image.')
1416 parser.add_option_group(group)
1417
1418
1419def _AddProductionOptions(parser):
1420 group = optparse.OptionGroup(
1421 parser, 'Advanced Server Options', 'These options can be used to changed '
1422 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001423 group.add_option('--clear_cache',
1424 action='store_true', default=False,
1425 help='At startup, removes all cached entries from the'
1426 'devserver\'s cache.')
1427 group.add_option('--logfile',
1428 metavar='PATH',
1429 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -07001430 group.add_option('--pidfile',
1431 metavar='PATH',
1432 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 11:04:13 -08001433 group.add_option('--portfile',
1434 metavar='PATH',
1435 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001436 group.add_option('--production',
1437 action='store_true', default=False,
1438 help='have the devserver use production values when '
1439 'starting up. This includes using more threads and '
1440 'performing less logging.')
1441 parser.add_option_group(group)
1442
1443
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001444def _MakeLogHandler(logfile):
1445 """Create a LogHandler instance used to log all messages."""
1446 hdlr_cls = handlers.TimedRotatingFileHandler
1447 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
1448 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -07001449 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001450 return hdlr
1451
1452
Chris Sosacde6bf42012-05-31 18:36:39 -07001453def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001454 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -08001455 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -07001456
1457 # get directory that the devserver is run from
1458 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -07001459 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -07001460 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001461 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -07001462 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -07001463 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001464 parser.add_option('--port',
1465 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 13:13:28 -08001466 help=('port for the dev server to use; if zero, binds to '
1467 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001468 parser.add_option('-t', '--test_image',
1469 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -07001470 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -07001471 parser.add_option('-x', '--xbuddy_manage_builds',
1472 action='store_true',
1473 default=False,
1474 help='If set, allow xbuddy to manage images in'
1475 'build/images.')
Dan Shi72b16132015-10-08 12:10:33 -07001476 parser.add_option('-a', '--android_build_credential',
1477 default=None,
1478 help='Path to a json file which contains the credential '
1479 'needed to access Android builds.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001480 _AddProductionOptions(parser)
1481 _AddUpdateOptions(parser)
1482 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -07001483 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001484
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001485 # Handle options that must be set globally in cherrypy. Do this
1486 # work up front, because calls to _Log() below depend on this
1487 # initialization.
1488 if options.production:
1489 cherrypy.config.update({'environment': 'production'})
1490 if not options.logfile:
1491 cherrypy.config.update({'log.screen': True})
1492 else:
1493 cherrypy.config.update({'log.error_file': '',
1494 'log.access_file': ''})
1495 hdlr = _MakeLogHandler(options.logfile)
1496 # Pylint can't seem to process these two calls properly
1497 # pylint: disable=E1101
1498 cherrypy.log.access_log.addHandler(hdlr)
1499 cherrypy.log.error_log.addHandler(hdlr)
1500 # pylint: enable=E1101
1501
joychened64b222013-06-21 16:39:34 -07001502 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001503 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001504
joychened64b222013-06-21 16:39:34 -07001505 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001506 # If our devserver is only supposed to serve payloads, we shouldn't be
1507 # mucking with the cache at all. If the devserver hadn't previously
1508 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001509 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001510 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001511 else:
1512 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001513
Chris Sosadbc20082012-12-10 13:39:11 -08001514 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 16:39:34 -07001515 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001516
joychen121fc9b2013-08-02 14:30:30 -07001517 _xbuddy = xbuddy.XBuddy(options.xbuddy_manage_builds,
1518 options.board,
joychen121fc9b2013-08-02 14:30:30 -07001519 static_dir=options.static_dir)
Chris Sosa75490802013-09-30 17:21:45 -07001520 if options.clear_cache and options.xbuddy_manage_builds:
1521 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 14:30:30 -07001522
Chris Sosa6a3697f2013-01-29 16:44:43 -08001523 # We allow global use here to share with cherrypy classes.
1524 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001525 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001526 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001527 _xbuddy,
joychened64b222013-06-21 16:39:34 -07001528 static_dir=options.static_dir,
Andrew de los Reyes52620802010-04-12 13:40:07 -07001529 urlbase=options.urlbase,
Chris Sosa5d342a22010-09-28 16:54:41 -07001530 forced_image=options.image,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001531 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001532 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-28 23:42:37 -07001533 src_image=options.src_image,
Chris Sosa08d55a22011-01-19 16:08:02 -08001534 board=options.board,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001535 copy_to_static_root=not options.exit,
1536 private_key=options.private_key,
Gabe Black3b567202015-09-23 14:07:59 -07001537 private_key_for_metadata_hash_signature=(
1538 options.private_key_for_metadata_hash_signature),
David Zeuthen52ccd012013-10-31 12:58:26 -07001539 public_key=options.public_key,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001540 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001541 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001542 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001543 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001544 )
Chris Sosa7c931362010-10-11 19:49:01 -07001545
Chris Sosa6a3697f2013-01-29 16:44:43 -08001546 if options.pregenerate_update:
1547 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 15:46:22 -07001548
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001549 if options.exit:
1550 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001551
joychen3cb228e2013-06-12 12:13:13 -07001552 dev_server = DevServerRoot(_xbuddy)
1553
Gilad Arnold11fbef42014-02-10 11:04:13 -08001554 # Patch CherryPy to support binding to any available port (--port=0).
1555 cherrypy_ext.ZeroPortPatcher.DoPatch(cherrypy)
1556
Chris Sosa855b8932013-08-21 13:24:55 -07001557 if options.pidfile:
1558 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1559
Gilad Arnold11fbef42014-02-10 11:04:13 -08001560 if options.portfile:
1561 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1562
Dan Shiafd5c6c2016-01-07 10:27:03 -08001563 if (options.android_build_credential and
1564 os.path.exists(options.android_build_credential)):
1565 try:
1566 with open(options.android_build_credential) as f:
1567 android_build.BuildAccessor.credential_info = json.load(f)
1568 except ValueError as e:
1569 _Log('Failed to load the android build credential: %s. Error: %s.' %
1570 (options.android_build_credential, e))
joychen3cb228e2013-06-12 12:13:13 -07001571 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001572
1573
1574if __name__ == '__main__':
1575 main()