blob: 26a46d38a9719985f46386518a0565eb0f37e31f [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)
669 return str(dl.IsStaged(factory))
Dan Shi59ae7092013-06-04 14:37:27 -0700670
Chris Sosa76e44b92013-01-31 12:11:38 -0800671 @cherrypy.expose
Prashanth Ba06d2d22014-03-07 15:35:19 -0800672 def list_image_dir(self, **kwargs):
673 """Take an archive url and list the contents in its staged directory.
674
675 Args:
676 kwargs:
677 archive_url: Google Storage URL for the build.
678
679 Example:
680 To list the contents of where this devserver should have staged
681 gs://image-archive/<board>-release/<build> call:
682 http://devserver_url:<port>/list_image_dir?archive_url=<gs://..>
683
684 Returns:
685 A string with information about the contents of the image directory.
686 """
Gabe Black3b567202015-09-23 14:07:59 -0700687 dl = _get_downloader(kwargs)
Prashanth Ba06d2d22014-03-07 15:35:19 -0800688 try:
Gabe Black3b567202015-09-23 14:07:59 -0700689 image_dir_contents = dl.ListBuildDir()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800690 except build_artifact.ArtifactDownloadError as e:
691 return 'Cannot list the contents of staged artifacts. %s' % e
692 if not image_dir_contents:
Gabe Black3b567202015-09-23 14:07:59 -0700693 return '%s has not been staged on this devserver.' % dl.DescribeSource()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800694 return image_dir_contents
695
696 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800697 def stage(self, **kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700698 """Downloads and caches build artifacts.
Chris Sosa76e44b92013-01-31 12:11:38 -0800699
Gabe Black3b567202015-09-23 14:07:59 -0700700 Downloads and caches build artifacts, possibly from a Google Storage URL,
Dan Shi72b16132015-10-08 12:10:33 -0700701 or from Android's build server. Returns once these have been downloaded
Gabe Black3b567202015-09-23 14:07:59 -0700702 on the devserver. A call to this will attempt to cache non-specified
703 artifacts in the background for the given from the given URL following
704 the principle of spatial locality. Spatial locality of different
Chris Sosa76e44b92013-01-31 12:11:38 -0800705 artifacts is explicitly defined in the build_artifact module.
706
707 These artifacts will then be available from the static/ sub-directory of
708 the devserver.
709
710 Args:
711 archive_url: Google Storage URL for the build.
Simran Basi4243a862014-12-12 12:48:33 -0800712 local_path: Local path for the build.
Dan Shif8eb0d12013-08-01 17:52:06 -0700713 async: True to return without waiting for download to complete.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700714 artifacts: Comma separated list of named artifacts to download.
715 These are defined in artifact_info and have their implementation
716 in build_artifact.py.
717 files: Comma separated list of files to stage. These
718 will be available as is in the corresponding static directory with no
719 custom post-processing.
Laurence Goodbyf5c958d2016-01-14 18:23:56 -0800720 clean: True to remove any previously staged artifacts first.
Chris Sosa76e44b92013-01-31 12:11:38 -0800721
722 Example:
723 To download the autotest and test suites tarballs:
724 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
725 artifacts=autotest,test_suites
726 To download the full update payload:
727 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
728 artifacts=full_payload
Chris Sosa6b0c6172013-08-05 17:01:33 -0700729 To download just a file called blah.bin:
730 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
731 files=blah.bin
Chris Sosa76e44b92013-01-31 12:11:38 -0800732
733 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 16:39:34 -0700734 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 12:11:38 -0800735
736 Note for this example, relative path is the archive_url stripped of its
737 basename i.e. path/ in the examples above. Specific example:
738
739 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
740
741 Will get staged to:
742
joychened64b222013-06-21 16:39:34 -0700743 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Chris Sosa76e44b92013-01-31 12:11:38 -0800744 """
Gabe Black3b567202015-09-23 14:07:59 -0700745 dl, factory = _get_downloader_and_factory(kwargs)
746
Dan Shi59ae7092013-06-04 14:37:27 -0700747 with DevServerRoot._staging_thread_count_lock:
748 DevServerRoot._staging_thread_count += 1
749 try:
Laurence Goodbyf5c958d2016-01-14 18:23:56 -0800750 boolean_string = kwargs.get('clean')
751 clean = xbuddy.XBuddy.ParseBoolean(boolean_string)
752 if clean and os.path.exists(dl.GetBuildDir()):
753 _Log('Removing %s' % dl.GetBuildDir())
754 shutil.rmtree(dl.GetBuildDir())
Gabe Black3b567202015-09-23 14:07:59 -0700755 async = kwargs.get('async', False)
756 dl.Download(factory, async=async)
Dan Shi59ae7092013-06-04 14:37:27 -0700757 finally:
758 with DevServerRoot._staging_thread_count_lock:
759 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800760 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700761
762 @cherrypy.expose
Dan Shi2f136862016-02-11 15:38:38 -0800763 def locate_file(self, **kwargs):
764 """Get the path to the given file name.
765
766 This method looks up the given file name inside specified build artifacts.
767 One use case is to help caller to locate an apk file inside a build
768 artifact. The location of the apk file could be different based on the
769 branch and target.
770
771 Args:
772 file_name: Name of the file to look for.
773 artifacts: A list of artifact names to search for the file.
774
775 Returns:
776 Path to the file with the given name. It's relative to the folder for the
777 build, e.g., DATA/priv-app/sl4a/sl4a.apk
778
779 """
780 dl, _ = _get_downloader_and_factory(kwargs)
781 try:
782 file_name = kwargs['file_name'].lower()
783 artifacts = kwargs['artifacts']
784 except KeyError:
785 raise DevServerError('`file_name` and `artifacts` are required to search '
786 'for a file in build artifacts.')
787 build_path = dl.GetBuildDir()
788 for artifact in artifacts:
789 # Get the unzipped folder of the artifact. If it's not defined in
790 # ARTIFACT_UNZIP_FOLDER_MAP, assume the files are unzipped to the build
791 # directory directly.
792 folder = artifact_info.ARTIFACT_UNZIP_FOLDER_MAP.get(artifact, '')
793 artifact_path = os.path.join(build_path, folder)
794 for root, _, filenames in os.walk(artifact_path):
795 if file_name in set([f.lower() for f in filenames]):
796 return os.path.relpath(os.path.join(root, file_name), build_path)
797 raise DevServerError('File `%s` can not be found in artifacts: %s' %
798 (file_name, artifacts))
799
800 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -0800801 def setup_telemetry(self, **kwargs):
802 """Extracts and sets up telemetry
803
804 This method goes through the telemetry deps packages, and stages them on
805 the devserver to be used by the drones and the telemetry tests.
806
807 Args:
808 archive_url: Google Storage URL for the build.
809
810 Returns:
811 Path to the source folder for the telemetry codebase once it is staged.
812 """
Gabe Black3b567202015-09-23 14:07:59 -0700813 dl = _get_downloader(kwargs)
Simran Basi4baad082013-02-14 13:39:18 -0800814
Gabe Black3b567202015-09-23 14:07:59 -0700815 build_path = dl.GetBuildDir()
Simran Basi4baad082013-02-14 13:39:18 -0800816 deps_path = os.path.join(build_path, 'autotest/packages')
817 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
818 src_folder = os.path.join(telemetry_path, 'src')
819
820 with self._telemetry_lock_dict.lock(telemetry_path):
821 if os.path.exists(src_folder):
822 # Telemetry is already fully stage return
823 return src_folder
824
825 common_util.MkDirP(telemetry_path)
826
827 # Copy over the required deps tar balls to the telemetry directory.
828 for dep in TELEMETRY_DEPS:
829 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -0700830 if not os.path.exists(dep_path):
831 # This dep does not exist (could be new), do not extract it.
832 continue
Simran Basi4baad082013-02-14 13:39:18 -0800833 try:
834 common_util.ExtractTarball(dep_path, telemetry_path)
835 except common_util.CommonUtilError as e:
836 shutil.rmtree(telemetry_path)
837 raise DevServerError(str(e))
838
839 # By default all the tarballs extract to test_src but some parts of
840 # the telemetry code specifically hardcoded to exist inside of 'src'.
841 test_src = os.path.join(telemetry_path, 'test_src')
842 try:
843 shutil.move(test_src, src_folder)
844 except shutil.Error:
845 # This can occur if src_folder already exists. Remove and retry move.
846 shutil.rmtree(src_folder)
Gabe Black3b567202015-09-23 14:07:59 -0700847 raise DevServerError(
848 'Failure in telemetry setup for build %s. Appears that the '
849 'test_src to src move failed.' % dl.GetBuild())
Simran Basi4baad082013-02-14 13:39:18 -0800850
851 return src_folder
852
853 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800854 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -0700855 """Symbolicates a minidump using pre-downloaded symbols, returns it.
856
857 Callers will need to POST to this URL with a body of MIME-type
858 "multipart/form-data".
859 The body should include a single argument, 'minidump', containing the
860 binary-formatted minidump to symbolicate.
861
Chris Masone816e38c2012-05-02 12:22:36 -0700862 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -0800863 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -0700864 minidump: The binary minidump file to symbolicate.
865 """
Gabe Black3b567202015-09-23 14:07:59 -0700866 kwargs['artifacts'] = 'symbols'
867 dl = _get_downloader(kwargs)
868
Chris Sosa76e44b92013-01-31 12:11:38 -0800869 # Ensure the symbols have been staged.
Gabe Black3b567202015-09-23 14:07:59 -0700870 if self.stage(**kwargs) != 'Success':
871 raise DevServerError('Failed to stage symbols for %s' %
872 dl.DescribeSource())
Chris Sosa76e44b92013-01-31 12:11:38 -0800873
Chris Masone816e38c2012-05-02 12:22:36 -0700874 to_return = ''
875 with tempfile.NamedTemporaryFile() as local:
876 while True:
877 data = minidump.file.read(8192)
878 if not data:
879 break
880 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -0800881
Chris Masone816e38c2012-05-02 12:22:36 -0700882 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -0800883
Gabe Black3b567202015-09-23 14:07:59 -0700884 symbols_directory = os.path.join(dl.GetBuildDir(), 'debug', 'breakpad')
Chris Sosa76e44b92013-01-31 12:11:38 -0800885
886 stackwalk = subprocess.Popen(
887 ['minidump_stackwalk', local.name, symbols_directory],
888 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
889
Chris Masone816e38c2012-05-02 12:22:36 -0700890 to_return, error_text = stackwalk.communicate()
891 if stackwalk.returncode != 0:
892 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
893 error_text, stackwalk.returncode))
894
895 return to_return
896
897 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800898 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 15:31:36 -0400899 """Return a string representing the latest build for a given target.
900
901 Args:
902 target: The build target, typically a combination of the board and the
903 type of build e.g. x86-mario-release.
904 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
905 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-07 18:21:26 -0800906
Scott Zawalski16954532012-03-20 15:31:36 -0400907 Returns:
908 A string representation of the latest build if one exists, i.e.
909 R19-1993.0.0-a1-b1480.
910 An empty string if no latest could be found.
911 """
Don Garrettf84631a2014-01-07 18:21:26 -0800912 if not kwargs:
Scott Zawalski16954532012-03-20 15:31:36 -0400913 return _PrintDocStringAsHTML(self.latestbuild)
914
Don Garrettf84631a2014-01-07 18:21:26 -0800915 if 'target' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -0700916 raise common_util.DevServerHTTPError(500, 'Error: target= is required!')
Dan Shi61305df2015-10-26 16:52:35 -0700917
918 if _is_android_build_request(kwargs):
919 branch = kwargs.get('branch', None)
920 target = kwargs.get('target', None)
921 if not target or not branch:
922 raise DevServerError(
923 'Both target and branch must be specified to query for the latest '
924 'Android build.')
925 return android_build.BuildAccessor.GetLatestBuildID(target, branch)
926
Scott Zawalski16954532012-03-20 15:31:36 -0400927 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700928 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-07 18:21:26 -0800929 updater.static_dir, kwargs['target'],
930 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700931 except common_util.CommonUtilError as errmsg:
Chris Sosa4b951602014-04-09 20:26:07 -0700932 raise common_util.DevServerHTTPError(500, str(errmsg))
Scott Zawalski16954532012-03-20 15:31:36 -0400933
934 @cherrypy.expose
xixuan7efd0002016-04-14 15:34:01 -0700935 def list_suite_controls(self, **kwargs):
936 """Return a list of contents of all known control files.
937
938 Example URL:
939 To List all control files' content:
940 http://dev-server/list_suite_controls?suite_name=bvt&
941 build=daisy_spring-release/R29-4279.0.0
942
943 Args:
944 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
945 suite_name: List the control files belonging to that suite.
946
947 Returns:
948 A list of contents of all control files are provided.
949 """
950 if not kwargs:
951 return _PrintDocStringAsHTML(self.controlfiles)
952
953 if 'build' not in kwargs:
954 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
955
956 if 'suite_name' not in kwargs:
957 raise common_util.DevServerHTTPError(500, 'Error: suite_name= is required!')
958
959 control_file_list = [
960 line.rstrip() for line in common_util.GetControlFileListForSuite(
961 updater.static_dir, kwargs['build'],
962 kwargs['suite_name']).splitlines()]
963
964 control_file_content_list = []
965 for control_path in control_file_list:
966 control_file_content_list.append(common_util.GetControlFile(
967 updater.static_dir, kwargs['build'], control_path))
968
969 return json.dumps(control_file_content_list)
970
971 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800972 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500973 """Return a control file or a list of all known control files.
974
975 Example URL:
976 To List all control files:
beepsbd337242013-07-09 22:44:06 -0700977 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
978 To List all control files for, say, the bvt suite:
979 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -0500980 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500981 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 -0500982
983 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500984 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500985 control_path: If you want the contents of a control file set this
986 to the path. E.g. client/site_tests/sleeptest/control
987 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -0700988 suite_name: If control_path is not specified but a suite_name is
989 specified, list the control files belonging to that suite instead of
990 all control files. The empty string for suite_name will list all control
991 files for the build.
Don Garrettf84631a2014-01-07 18:21:26 -0800992
Scott Zawalski4647ce62012-01-03 17:17:28 -0500993 Returns:
994 Contents of a control file if control_path is provided.
995 A list of control files if no control_path is provided.
996 """
Don Garrettf84631a2014-01-07 18:21:26 -0800997 if not kwargs:
Scott Zawalski4647ce62012-01-03 17:17:28 -0500998 return _PrintDocStringAsHTML(self.controlfiles)
999
Don Garrettf84631a2014-01-07 18:21:26 -08001000 if 'build' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -07001001 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -05001002
Don Garrettf84631a2014-01-07 18:21:26 -08001003 if 'control_path' not in kwargs:
1004 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-09 22:44:06 -07001005 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-07 18:21:26 -08001006 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-09 22:44:06 -07001007 else:
1008 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-07 18:21:26 -08001009 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -05001010 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001011 return common_util.GetControlFile(
Don Garrettf84631a2014-01-07 18:21:26 -08001012 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -08001013
1014 @cherrypy.expose
Simran Basi99e63c02014-05-20 10:39:52 -07001015 def xbuddy_translate(self, *args, **kwargs):
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001016 """Translates an xBuddy path to a real path to artifact if it exists.
1017
1018 Args:
Simran Basi99e63c02014-05-20 10:39:52 -07001019 args: An xbuddy path in the form of {local|remote}/build_id/artifact.
1020 Local searches the devserver's static directory. Remote searches a
1021 Google Storage image archive.
1022
1023 Kwargs:
1024 image_dir: Google Storage image archive to search in if requesting a
1025 remote artifact. If none uses the default bucket.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001026
1027 Returns:
Simran Basi99e63c02014-05-20 10:39:52 -07001028 String in the format of build_id/artifact as stored on the local server
1029 or in Google Storage.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001030 """
Simran Basi99e63c02014-05-20 10:39:52 -07001031 build_id, filename = self._xbuddy.Translate(
Gabe Black3b567202015-09-23 14:07:59 -07001032 args, image_dir=kwargs.get('image_dir'))
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001033 response = os.path.join(build_id, filename)
1034 _Log('Path translation requested, returning: %s', response)
1035 return response
1036
1037 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -07001038 def xbuddy(self, *args, **kwargs):
1039 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -07001040
1041 Args:
joycheneaf4cfc2013-07-02 08:38:57 -07001042 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -07001043 components of the path. The path can be understood as
1044 "{local|remote}/build_id/artifact" where build_id is composed of
1045 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -07001046
joychen121fc9b2013-08-02 14:30:30 -07001047 The first path element is optional, and can be "remote" or "local"
1048 If local (the default), devserver will not attempt to access Google
1049 Storage, and will only search the static directory for the files.
1050 If remote, devserver will try to obtain the artifact off GS if it's
1051 not found locally.
1052 The board is the familiar board name, optionally suffixed.
1053 The version can be the google storage version number, and may also be
1054 any of a number of xBuddy defined version aliases that will be
1055 translated into the latest built image that fits the description.
1056 Defaults to latest.
1057 The artifact is one of a number of image or artifact aliases used by
1058 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -07001059
1060 Kwargs:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001061 for_update: {true|false}
1062 if true, pregenerates the update payloads for the image,
1063 and returns the update uri to pass to the
1064 update_engine_client.
joychen3cb228e2013-06-12 12:13:13 -07001065 return_dir: {true|false}
1066 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001067 relative_path: {true|false}
1068 if set to true, returns the relative path to the payload
1069 directory from static_dir.
joychen3cb228e2013-06-12 12:13:13 -07001070 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -07001071 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -07001072 or
joycheneaf4cfc2013-07-02 08:38:57 -07001073 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -07001074
1075 Returns:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001076 If |for_update|, returns a redirect to the image or update file
1077 on the devserver. E.g.,
1078 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
1079 chromium-test-image.bin
1080 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
1081 http://host:port/static/x86-generic-release/R26-4000.0.0/
1082 If |relative_path| is true, return a relative path the folder where the
1083 payloads are. E.g.,
1084 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 12:13:13 -07001085 """
Chris Sosa75490802013-09-30 17:21:45 -07001086 boolean_string = kwargs.get('for_update')
1087 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001088 boolean_string = kwargs.get('return_dir')
1089 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
1090 boolean_string = kwargs.get('relative_path')
1091 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -07001092
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001093 if return_dir and relative_path:
Chris Sosa4b951602014-04-09 20:26:07 -07001094 raise common_util.DevServerHTTPError(
1095 500, 'Cannot specify both return_dir and relative_path')
Chris Sosa75490802013-09-30 17:21:45 -07001096
1097 # For updates, we optimize downloading of test images.
1098 file_name = None
1099 build_id = None
1100 if for_update:
1101 try:
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001102 build_id = self._xbuddy.StageTestArtifactsForUpdate(args)
Chris Sosa75490802013-09-30 17:21:45 -07001103 except build_artifact.ArtifactDownloadError:
1104 build_id = None
1105
1106 if not build_id:
1107 build_id, file_name = self._xbuddy.Get(args)
1108
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001109 if for_update:
1110 _Log('Payload generation triggered by request')
1111 # Forces payload to be in cache and symlinked into build_id dir.
Chris Sosa75490802013-09-30 17:21:45 -07001112 updater.GetUpdateForLabel(autoupdate.FORCED_UPDATE, build_id,
1113 image_name=file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001114
1115 response = None
1116 if return_dir:
1117 response = os.path.join(cherrypy.request.base, 'static', build_id)
1118 _Log('Directory requested, returning: %s', response)
1119 elif relative_path:
1120 response = build_id
1121 _Log('Relative path requested, returning: %s', response)
1122 elif for_update:
1123 response = os.path.join(cherrypy.request.base, 'update', build_id)
1124 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 12:13:13 -07001125 else:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001126 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 14:30:30 -07001127 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001128 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 14:30:30 -07001129 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -07001130
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001131 return response
1132
joychen3cb228e2013-06-12 12:13:13 -07001133 @cherrypy.expose
1134 def xbuddy_list(self):
1135 """Lists the currently available images & time since last access.
1136
Gilad Arnold452fd272014-02-04 11:09:28 -08001137 Returns:
1138 A string representation of a list of tuples [(build_id, time since last
1139 access),...]
joychen3cb228e2013-06-12 12:13:13 -07001140 """
1141 return self._xbuddy.List()
1142
1143 @cherrypy.expose
1144 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 11:09:28 -08001145 """Returns the number of images cached by xBuddy."""
joychen3cb228e2013-06-12 12:13:13 -07001146 return self._xbuddy.Capacity()
1147
1148 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001149 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001150 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001151 return ('Welcome to the Dev Server!<br>\n'
1152 '<br>\n'
1153 'Here are the available methods, click for documentation:<br>\n'
1154 '<br>\n'
1155 '%s' %
1156 '<br>\n'.join(
1157 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001158 for name in _FindExposedMethods(
1159 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001160
1161 @cherrypy.expose
1162 def doc(self, *args):
1163 """Shows the documentation for available methods / URLs.
1164
1165 Example:
1166 http://myhost/doc/update
1167 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001168 name = '/'.join(args)
1169 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001170 if not method:
1171 raise DevServerError("No exposed method named `%s'" % name)
1172 if not method.__doc__:
1173 raise DevServerError("No documentation for exposed method `%s'" % name)
1174 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -07001175
Dale Curtisc9aaf3a2011-08-09 15:47:40 -07001176 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001177 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001178 """Handles an update check from a Chrome OS client.
1179
1180 The HTTP request should contain the standard Omaha-style XML blob. The URL
1181 line may contain an additional intermediate path to the update payload.
1182
joychen121fc9b2013-08-02 14:30:30 -07001183 This request can be handled in one of 4 ways, depending on the devsever
1184 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -07001185
joychen121fc9b2013-08-02 14:30:30 -07001186 1. No intermediate path
1187 If no intermediate path is given, the default behavior is to generate an
1188 update payload from the latest test image locally built for the board
1189 specified in the xml. Devserver serves the generated payload.
1190
1191 2. Path explicitly invokes XBuddy
1192 If there is a path given, it can explicitly invoke xbuddy by prefixing it
1193 with 'xbuddy'. This path is then used to acquire an image binary for the
1194 devserver to generate an update payload from. Devserver then serves this
1195 payload.
1196
1197 3. Path is left for the devserver to interpret.
1198 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
1199 to generate a payload from the test image in that directory and serve it.
1200
1201 4. The devserver is in a 'forced' mode. TO BE DEPRECATED
1202 This comes from the usage of --forced_payload or --image when starting the
1203 devserver. No matter what path (or no path) gets passed in, devserver will
1204 serve the update payload (--forced_payload) or generate an update payload
1205 from the image (--image).
1206
1207 Examples:
1208 1. No intermediate path
1209 update_engine_client --omaha_url=http://myhost/update
1210 This generates an update payload from the latest test image locally built
1211 for the board specified in the xml.
1212
1213 2. Explicitly invoke xbuddy
1214 update_engine_client --omaha_url=
1215 http://myhost/update/xbuddy/remote/board/version/dev
1216 This would go to GS to download the dev image for the board, from which
1217 the devserver would generate a payload to serve.
1218
1219 3. Give a path for devserver to interpret
1220 update_engine_client --omaha_url=http://myhost/update/some/random/path
1221 This would attempt, in order to:
1222 a) Generate an update from a test image binary if found in
1223 static_dir/some/random/path.
1224 b) Serve an update payload found in static_dir/some/random/path.
1225 c) Hope that some/random/path takes the form "board/version" and
1226 and attempt to download an update payload for that board/version
1227 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001228 """
joychen121fc9b2013-08-02 14:30:30 -07001229 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -08001230 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -07001231 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-11 19:49:01 -07001232
joychen121fc9b2013-08-02 14:30:30 -07001233 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001234
Dan Shiafd0e492015-05-27 14:23:51 -07001235 @require_psutil()
1236 def _get_io_stats(self):
1237 """Get the IO stats as a dictionary.
1238
Gabe Black3b567202015-09-23 14:07:59 -07001239 Returns:
1240 A dictionary of IO stats collected by psutil.
Dan Shiafd0e492015-05-27 14:23:51 -07001241
1242 """
1243 return {'disk_read_bytes_per_second': self.disk_read_bytes_per_sec,
1244 'disk_write_bytes_per_second': self.disk_write_bytes_per_sec,
1245 'disk_total_bytes_per_second': (self.disk_read_bytes_per_sec +
1246 self.disk_write_bytes_per_sec),
1247 'network_sent_bytes_per_second': self.network_sent_bytes_per_sec,
1248 'network_recv_bytes_per_second': self.network_recv_bytes_per_sec,
1249 'network_total_bytes_per_second': (self.network_sent_bytes_per_sec +
1250 self.network_recv_bytes_per_sec),
1251 'cpu_percent': psutil.cpu_percent(),}
1252
Dan Shif5ce2de2013-04-25 16:06:32 -07001253 @cherrypy.expose
1254 def check_health(self):
1255 """Collect the health status of devserver to see if it's ready for staging.
1256
Gilad Arnold452fd272014-02-04 11:09:28 -08001257 Returns:
1258 A JSON dictionary containing all or some of the following fields:
1259 free_disk (int): free disk space in GB
1260 staging_thread_count (int): number of devserver threads currently staging
1261 an image
Dan Shif5ce2de2013-04-25 16:06:32 -07001262 """
1263 # Get free disk space.
1264 stat = os.statvfs(updater.static_dir)
1265 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
Dan Shid76e6bb2016-01-28 22:28:51 -08001266 try:
1267 apache_client_count = int(subprocess.check_output('pgrep -fc apache',
1268 shell=True))
1269 except subprocess.CalledProcessError:
1270 apache_client_count = 0
1271 try:
1272 telemetry_test_count = int(subprocess.check_output(
1273 'pgrep -fc "python.*telemetry"', shell=True))
1274 except subprocess.CalledProcessError:
1275 telemetry_test_count = 0
Dan Shif5ce2de2013-04-25 16:06:32 -07001276
Dan Shiafd0e492015-05-27 14:23:51 -07001277 health_data = {
Dan Shif5ce2de2013-04-25 16:06:32 -07001278 'free_disk': free_disk,
Dan Shid76e6bb2016-01-28 22:28:51 -08001279 'staging_thread_count': DevServerRoot._staging_thread_count,
1280 'apache_client_count': apache_client_count,
1281 'telemetry_test_count': telemetry_test_count}
Dan Shiafd0e492015-05-27 14:23:51 -07001282 health_data.update(self._get_io_stats() or {})
1283
1284 return json.dumps(health_data)
Dan Shif5ce2de2013-04-25 16:06:32 -07001285
1286
Chris Sosadbc20082012-12-10 13:39:11 -08001287def _CleanCache(cache_dir, wipe):
1288 """Wipes any excess cached items in the cache_dir.
1289
1290 Args:
1291 cache_dir: the directory we are wiping from.
1292 wipe: If True, wipe all the contents -- not just the excess.
1293 """
1294 if wipe:
1295 # Clear the cache and exit on error.
1296 cmd = 'rm -rf %s/*' % cache_dir
1297 if os.system(cmd) != 0:
1298 _Log('Failed to clear the cache with %s' % cmd)
1299 sys.exit(1)
1300 else:
1301 # Clear all but the last N cached updates
1302 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
1303 (cache_dir, CACHED_ENTRIES))
1304 if os.system(cmd) != 0:
1305 _Log('Failed to clean up old delta cache files with %s' % cmd)
1306 sys.exit(1)
1307
1308
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001309def _AddTestingOptions(parser):
1310 group = optparse.OptionGroup(
1311 parser, 'Advanced Testing Options', 'These are used by test scripts and '
1312 'developers writing integration tests utilizing the devserver. They are '
1313 'not intended to be really used outside the scope of someone '
1314 'knowledgable about the test.')
1315 group.add_option('--exit',
1316 action='store_true',
1317 help='do not start the server (yet pregenerate/clear cache)')
1318 group.add_option('--host_log',
1319 action='store_true', default=False,
1320 help='record history of host update events (/api/hostlog)')
1321 group.add_option('--max_updates',
Gabe Black3b567202015-09-23 14:07:59 -07001322 metavar='NUM', default=-1, type='int',
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001323 help='maximum number of update checks handled positively '
1324 '(default: unlimited)')
1325 group.add_option('--private_key',
1326 metavar='PATH', default=None,
1327 help='path to the private key in pem format. If this is set '
1328 'the devserver will generate update payloads that are '
1329 'signed with this key.')
David Zeuthen52ccd012013-10-31 12:58:26 -07001330 group.add_option('--private_key_for_metadata_hash_signature',
1331 metavar='PATH', default=None,
1332 help='path to the private key in pem format. If this is set '
1333 'the devserver will sign the metadata hash with the given '
1334 'key and transmit in the Omaha-style XML response.')
1335 group.add_option('--public_key',
1336 metavar='PATH', default=None,
1337 help='path to the public key in pem format. If this is set '
1338 'the devserver will transmit a base64 encoded version of '
1339 'the content in the Omaha-style XML response.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001340 group.add_option('--proxy_port',
1341 metavar='PORT', default=None, type='int',
1342 help='port to have the client connect to -- basically the '
1343 'devserver lies to the update to tell it to get the payload '
1344 'from a different port that will proxy the request back to '
1345 'the devserver. The proxy must be managed outside the '
1346 'devserver.')
1347 group.add_option('--remote_payload',
1348 action='store_true', default=False,
Chris Sosa4b951602014-04-09 20:26:07 -07001349 help='Payload is being served from a remote machine. With '
1350 'this setting enabled, this devserver instance serves as '
1351 'just an Omaha server instance. In this mode, the '
1352 'devserver enforces a few extra components of the Omaha '
Chris Sosafc715442014-04-09 20:45:23 -07001353 'protocol, such as hardware class, being sent.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001354 group.add_option('-u', '--urlbase',
1355 metavar='URL',
Gabe Black3b567202015-09-23 14:07:59 -07001356 help='base URL for update images, other than the '
1357 'devserver. Use in conjunction with remote_payload.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001358 parser.add_option_group(group)
1359
1360
1361def _AddUpdateOptions(parser):
1362 group = optparse.OptionGroup(
1363 parser, 'Autoupdate Options', 'These options can be used to change '
1364 'how the devserver either generates or serve update payloads. Please '
1365 'note that all of these option affect how a payload is generated and so '
1366 'do not work in archive-only mode.')
1367 group.add_option('--board',
1368 help='By default the devserver will create an update '
1369 'payload from the latest image built for the board '
1370 'a device that is requesting an update has. When we '
1371 'pre-generate an update (see below) and we do not specify '
1372 'another update_type option like image or payload, the '
1373 'devserver needs to know the board to generate the latest '
1374 'image for. This is that board.')
1375 group.add_option('--critical_update',
1376 action='store_true', default=False,
1377 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001378 group.add_option('--image',
1379 metavar='FILE',
1380 help='Generate and serve an update using this image to any '
1381 'device that requests an update.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001382 group.add_option('--payload',
1383 metavar='PATH',
1384 help='use the update payload from specified directory '
1385 '(update.gz).')
1386 group.add_option('-p', '--pregenerate_update',
1387 action='store_true', default=False,
1388 help='pre-generate the update payload before accepting '
1389 'update requests. Useful to help debug payload generation '
1390 'issues quickly. Also if an update payload will take a '
1391 'long time to generate, a client may timeout if you do not'
1392 'pregenerate the update.')
1393 group.add_option('--src_image',
1394 metavar='PATH', default='',
1395 help='If specified, delta updates will be generated using '
1396 'this image as the source image. Delta updates are when '
1397 'you are updating from a "source image" to a another '
1398 'image.')
1399 parser.add_option_group(group)
1400
1401
1402def _AddProductionOptions(parser):
1403 group = optparse.OptionGroup(
1404 parser, 'Advanced Server Options', 'These options can be used to changed '
1405 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001406 group.add_option('--clear_cache',
1407 action='store_true', default=False,
1408 help='At startup, removes all cached entries from the'
1409 'devserver\'s cache.')
1410 group.add_option('--logfile',
1411 metavar='PATH',
1412 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -07001413 group.add_option('--pidfile',
1414 metavar='PATH',
1415 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 11:04:13 -08001416 group.add_option('--portfile',
1417 metavar='PATH',
1418 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001419 group.add_option('--production',
1420 action='store_true', default=False,
1421 help='have the devserver use production values when '
1422 'starting up. This includes using more threads and '
1423 'performing less logging.')
1424 parser.add_option_group(group)
1425
1426
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001427def _MakeLogHandler(logfile):
1428 """Create a LogHandler instance used to log all messages."""
1429 hdlr_cls = handlers.TimedRotatingFileHandler
1430 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
1431 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -07001432 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001433 return hdlr
1434
1435
Chris Sosacde6bf42012-05-31 18:36:39 -07001436def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001437 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -08001438 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -07001439
1440 # get directory that the devserver is run from
1441 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -07001442 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -07001443 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001444 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -07001445 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -07001446 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001447 parser.add_option('--port',
1448 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 13:13:28 -08001449 help=('port for the dev server to use; if zero, binds to '
1450 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001451 parser.add_option('-t', '--test_image',
1452 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -07001453 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -07001454 parser.add_option('-x', '--xbuddy_manage_builds',
1455 action='store_true',
1456 default=False,
1457 help='If set, allow xbuddy to manage images in'
1458 'build/images.')
Dan Shi72b16132015-10-08 12:10:33 -07001459 parser.add_option('-a', '--android_build_credential',
1460 default=None,
1461 help='Path to a json file which contains the credential '
1462 'needed to access Android builds.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001463 _AddProductionOptions(parser)
1464 _AddUpdateOptions(parser)
1465 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -07001466 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001467
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001468 # Handle options that must be set globally in cherrypy. Do this
1469 # work up front, because calls to _Log() below depend on this
1470 # initialization.
1471 if options.production:
1472 cherrypy.config.update({'environment': 'production'})
1473 if not options.logfile:
1474 cherrypy.config.update({'log.screen': True})
1475 else:
1476 cherrypy.config.update({'log.error_file': '',
1477 'log.access_file': ''})
1478 hdlr = _MakeLogHandler(options.logfile)
1479 # Pylint can't seem to process these two calls properly
1480 # pylint: disable=E1101
1481 cherrypy.log.access_log.addHandler(hdlr)
1482 cherrypy.log.error_log.addHandler(hdlr)
1483 # pylint: enable=E1101
1484
joychened64b222013-06-21 16:39:34 -07001485 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001486 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001487
joychened64b222013-06-21 16:39:34 -07001488 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001489 # If our devserver is only supposed to serve payloads, we shouldn't be
1490 # mucking with the cache at all. If the devserver hadn't previously
1491 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001492 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001493 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001494 else:
1495 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001496
Chris Sosadbc20082012-12-10 13:39:11 -08001497 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 16:39:34 -07001498 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001499
joychen121fc9b2013-08-02 14:30:30 -07001500 _xbuddy = xbuddy.XBuddy(options.xbuddy_manage_builds,
1501 options.board,
joychen121fc9b2013-08-02 14:30:30 -07001502 static_dir=options.static_dir)
Chris Sosa75490802013-09-30 17:21:45 -07001503 if options.clear_cache and options.xbuddy_manage_builds:
1504 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 14:30:30 -07001505
Chris Sosa6a3697f2013-01-29 16:44:43 -08001506 # We allow global use here to share with cherrypy classes.
1507 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001508 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001509 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001510 _xbuddy,
joychened64b222013-06-21 16:39:34 -07001511 static_dir=options.static_dir,
Andrew de los Reyes52620802010-04-12 13:40:07 -07001512 urlbase=options.urlbase,
Chris Sosa5d342a22010-09-28 16:54:41 -07001513 forced_image=options.image,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001514 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001515 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-28 23:42:37 -07001516 src_image=options.src_image,
Chris Sosa08d55a22011-01-19 16:08:02 -08001517 board=options.board,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001518 copy_to_static_root=not options.exit,
1519 private_key=options.private_key,
Gabe Black3b567202015-09-23 14:07:59 -07001520 private_key_for_metadata_hash_signature=(
1521 options.private_key_for_metadata_hash_signature),
David Zeuthen52ccd012013-10-31 12:58:26 -07001522 public_key=options.public_key,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001523 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001524 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001525 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001526 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001527 )
Chris Sosa7c931362010-10-11 19:49:01 -07001528
Chris Sosa6a3697f2013-01-29 16:44:43 -08001529 if options.pregenerate_update:
1530 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 15:46:22 -07001531
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001532 if options.exit:
1533 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001534
joychen3cb228e2013-06-12 12:13:13 -07001535 dev_server = DevServerRoot(_xbuddy)
1536
Gilad Arnold11fbef42014-02-10 11:04:13 -08001537 # Patch CherryPy to support binding to any available port (--port=0).
1538 cherrypy_ext.ZeroPortPatcher.DoPatch(cherrypy)
1539
Chris Sosa855b8932013-08-21 13:24:55 -07001540 if options.pidfile:
1541 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1542
Gilad Arnold11fbef42014-02-10 11:04:13 -08001543 if options.portfile:
1544 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1545
Dan Shiafd5c6c2016-01-07 10:27:03 -08001546 if (options.android_build_credential and
1547 os.path.exists(options.android_build_credential)):
1548 try:
1549 with open(options.android_build_credential) as f:
1550 android_build.BuildAccessor.credential_info = json.load(f)
1551 except ValueError as e:
1552 _Log('Failed to load the android build credential: %s. Error: %s.' %
1553 (options.android_build_credential, e))
joychen3cb228e2013-06-12 12:13:13 -07001554 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001555
1556
1557if __name__ == '__main__':
1558 main()