blob: 71f14b133ec294b349b87f55195ea84a179fcd26 [file] [log] [blame]
David Riley2fcb0122017-11-02 18:25:391#!/usr/bin/env python2
Chris Sosa7c931362010-10-12 02:49:012
Chris Sosa781ba6d2012-04-11 19:44:433# Copyright (c) 2009-2012 The Chromium OS Authors. All rights reserved.
[email protected]ded22402009-10-26 22:36:214# 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 18:47:007"""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 16:17:2329a particular package from a developer's chroot onto a requesting device.
Chris Sosa3ae4dc12013-03-29 18:47:0030
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 21:07:5942from __future__ import print_function
Chris Sosa7c931362010-10-12 02:49:0143
Gilad Arnold55a2a372012-10-02 16:46:3244import json
David Riley2fcb0122017-11-02 18:25:3945import optparse # pylint: disable=deprecated-module
[email protected]ded22402009-10-26 22:36:2146import os
Scott Zawalski4647ce62012-01-03 22:17:2847import re
Simran Basi4baad082013-02-14 21:39:1848import shutil
xixuan52c2fba2016-05-21 00:02:4849import signal
Mandeep Singh Baines38dcdda2012-12-08 01:55:3350import socket
Chris Masone816e38c2012-05-02 19:22:3651import subprocess
J. Richard Barnette3d977b82013-04-23 18:05:1952import sys
Chris Masone816e38c2012-05-02 19:22:3653import tempfile
Dan Shi59ae7092013-06-04 21:37:2754import threading
Dan Shiafd0e492015-05-27 21:23:5155import time
Gilad Arnoldd5ebaaa2012-10-02 18:52:3856import types
J. Richard Barnette3d977b82013-04-23 18:05:1957from logging import handlers
58
59import cherrypy
David Riley2fcb0122017-11-02 18:25:3960# pylint: disable=no-name-in-module
Chris Sosa855b8932013-08-21 20:24:5561from cherrypy import _cplogging as cplogging
David Riley2fcb0122017-11-02 18:25:3962from cherrypy.process import plugins # pylint: disable=import-error
63# pylint: enable=no-name-in-module
[email protected]ded22402009-10-26 22:36:2164
Richard Barnettedf35c322017-08-19 00:02:1365# This must happen before any local modules get a chance to import
66# anything from chromite. Otherwise, really bad things will happen, and
67# you will _not_ understand why.
68import setup_chromite # pylint: disable=unused-import
69
Chris Sosa0356d3b2010-09-16 22:46:2270import autoupdate
Dan Shi2f136862016-02-11 23:38:3871import artifact_info
Chris Sosa75490802013-10-01 00:21:4572import build_artifact
Gilad Arnold11fbef42014-02-10 19:04:1373import cherrypy_ext
Gilad Arnoldc65330c2012-09-20 22:17:4874import common_util
Simran Basief83d6a2014-08-28 21:32:0175import devserver_constants
Chris Sosa47a7d4e2012-03-28 18:26:5576import downloader
Chris Sosa7cd23202013-10-16 00:22:5777import gsutil_util
Gilad Arnoldc65330c2012-09-20 22:17:4878import log_util
joychen3cb228e2013-06-12 19:13:1379import xbuddy
Gilad Arnoldc65330c2012-09-20 22:17:4880
Gilad Arnoldc65330c2012-09-20 22:17:4881# Module-local log function.
Chris Sosa6a3697f2013-01-30 00:44:4382def _Log(message, *args):
83 return log_util.LogWithTag('DEVSERVER', message, *args)
Chris Sosa0356d3b2010-09-16 22:46:2284
Dan Shiafd0e492015-05-27 21:23:5185try:
86 import psutil
87except ImportError:
88 # Ignore psutil import failure. This is for backwards compatibility, so
89 # "cros flash" can still update duts with build without psutil installed.
90 # The reason is that, during cros flash, local devserver code is copied over
91 # to DUT, and devserver will be running inside DUT to stage the build.
92 _Log('Python module psutil is not installed, devserver load data will not be '
93 'collected')
94 psutil = None
Dan Shi94dcbe82015-06-09 03:51:1395except OSError as e:
96 # Ignore error like following. psutil may not work properly in builder. Ignore
97 # the error as load information of devserver is not used in builder.
98 # OSError: [Errno 2] No such file or directory: '/dev/pts/0'
99 _Log('psutil is failed to be imported, error: %s. devserver load data will '
100 'not be collected.', e)
101 psutil = None
102
xixuanac89ce82016-12-01 00:48:20103# Use try-except to skip unneccesary import for simple use case, eg. running
104# devserver on host.
105try:
106 import cros_update
xixuanac89ce82016-12-01 00:48:20107except ImportError as e:
108 _Log('cros_update cannot be imported: %r', e)
109 cros_update = None
xixuana4f4e712017-05-08 22:17:54110
111try:
112 import cros_update_progress
113except ImportError as e:
114 _Log('cros_update_progress cannot be imported: %r', e)
xixuanac89ce82016-12-01 00:48:20115 cros_update_progress = None
116
xixuanac89ce82016-12-01 00:48:20117try:
118 from chromite.lib.paygen import gspaths
119except ImportError as e:
120 _Log('chromite cannot be imported: %r', e)
121 gspaths = None
122
Dan Shi72b16132015-10-08 19:10:33123try:
124 import android_build
125except ImportError as e:
126 # Ignore android_build import failure. This is to support devserver running
127 # inside a ChromeOS device triggered by cros flash. Most ChromeOS test images
128 # do not have google-api-python-client module and they don't need to support
129 # Android updating, therefore, ignore the import failure here.
Dan Shi72b16132015-10-08 19:10:33130 android_build = None
Frank Farzan40160872011-12-13 02:39:18131
Chris Sosa417e55d2011-01-26 00:40:48132CACHED_ENTRIES = 12
Don Garrettf90edf02010-11-17 01:36:14133
Simran Basi4baad082013-02-14 21:39:18134TELEMETRY_FOLDER = 'telemetry_src'
135TELEMETRY_DEPS = ['dep-telemetry_dep.tar.bz2',
136 'dep-page_cycler_dep.tar.bz2',
Simran Basi0d078682013-03-22 23:40:04137 'dep-chrome_test.tar.bz2',
138 'dep-perf_data_dep.tar.bz2']
Simran Basi4baad082013-02-14 21:39:18139
Chris Sosa0356d3b2010-09-16 22:46:22140# Sets up global to share between classes.
[email protected]21a5ca32009-11-04 18:23:23141updater = None
[email protected]ded22402009-10-26 22:36:21142
xixuan3d48bff2017-01-31 03:00:09143# Log rotation parameters. These settings correspond to twice a day once
144# devserver is started, with about two weeks (28 backup files) of old logs
145# kept for backup.
J. Richard Barnette3d977b82013-04-23 18:05:19146#
xixuan3d48bff2017-01-31 03:00:09147# For more, see the documentation in standard python library for
J. Richard Barnette3d977b82013-04-23 18:05:19148# logging.handlers.TimedRotatingFileHandler
xixuan3d48bff2017-01-31 03:00:09149_LOG_ROTATION_TIME = 'H'
150_LOG_ROTATION_INTERVAL = 12 # hours
151_LOG_ROTATION_BACKUP = 28 # backup counts
J. Richard Barnette3d977b82013-04-23 18:05:19152
Dan Shiafd0e492015-05-27 21:23:51153# Number of seconds between the collection of disk and network IO counters.
154STATS_INTERVAL = 10.0
Frank Farzan40160872011-12-13 02:39:18155
xixuan52c2fba2016-05-21 00:02:48156# Auto-update parameters
157
158# Error msg for missing key in CrOS auto-update.
Xixuan Wu32af9f12017-11-13 22:11:44159KEY_ERROR_MSG = 'Key Error in RPC: %s= is required'
xixuan52c2fba2016-05-21 00:02:48160
161# Command of running auto-update.
162AUTO_UPDATE_CMD = '/usr/bin/python -u %s -d %s -b %s --static_dir %s'
163
164
Chris Sosa9164ca32012-03-28 18:04:50165class DevServerError(Exception):
Chris Sosa47a7d4e2012-03-28 18:26:55166 """Exception class used by this module."""
Chris Sosa47a7d4e2012-03-28 18:26:55167
168
Dan Shiafd0e492015-05-27 21:23:51169def require_psutil():
Gabe Black3b567202015-09-23 21:07:59170 """Decorator for functions require psutil to run."""
Dan Shiafd0e492015-05-27 21:23:51171 def deco_require_psutil(func):
172 """Wrapper of the decorator function.
173
Gabe Black3b567202015-09-23 21:07:59174 Args:
175 func: function to be called.
Dan Shiafd0e492015-05-27 21:23:51176 """
177 def func_require_psutil(*args, **kwargs):
178 """Decorator for functions require psutil to run.
179
180 If psutil is not installed, skip calling the function.
181
Gabe Black3b567202015-09-23 21:07:59182 Args:
183 *args: arguments for function to be called.
184 **kwargs: keyword arguments for function to be called.
Dan Shiafd0e492015-05-27 21:23:51185 """
186 if psutil:
187 return func(*args, **kwargs)
188 else:
189 _Log('Python module psutil is not installed. Function call %s is '
190 'skipped.' % func)
191 return func_require_psutil
192 return deco_require_psutil
193
194
Gabe Black3b567202015-09-23 21:07:59195def _canonicalize_archive_url(archive_url):
196 """Canonicalizes archive_url strings.
197
198 Raises:
199 DevserverError: if archive_url is not set.
200 """
201 if archive_url:
202 if not archive_url.startswith('gs://'):
203 raise DevServerError("Archive URL isn't from Google Storage (%s) ." %
204 archive_url)
205
206 return archive_url.rstrip('/')
207 else:
208 raise DevServerError("Must specify an archive_url in the request")
209
210
211def _canonicalize_local_path(local_path):
212 """Canonicalizes |local_path| strings.
213
214 Raises:
215 DevserverError: if |local_path| is not set.
216 """
217 # Restrict staging of local content to only files within the static
218 # directory.
219 local_path = os.path.abspath(local_path)
220 if not local_path.startswith(updater.static_dir):
221 raise DevServerError('Local path %s must be a subdirectory of the static'
222 ' directory: %s' % (local_path, updater.static_dir))
223
224 return local_path.rstrip('/')
225
226
227def _get_artifacts(kwargs):
228 """Returns a tuple of named and file artifacts given the stage rpc kwargs.
229
230 Raises:
231 DevserverError if no artifacts would be returned.
232 """
233 artifacts = kwargs.get('artifacts')
234 files = kwargs.get('files')
235 if not artifacts and not files:
236 raise DevServerError('No artifacts specified.')
237
238 # Note we NEED to coerce files to a string as we get raw unicode from
239 # cherrypy and we treat files as strings elsewhere in the code.
240 return (str(artifacts).split(',') if artifacts else [],
241 str(files).split(',') if files else [])
242
243
Dan Shi61305df2015-10-26 23:52:35244def _is_android_build_request(kwargs):
245 """Check if a devserver call is for Android build, based on the arguments.
246
247 This method exams the request's arguments (os_type) to determine if the
248 request is for Android build. If os_type is set to `android`, returns True.
249 If os_type is not set or has other values, returns False.
250
251 Args:
252 kwargs: Keyword arguments for the request.
253
254 Returns:
255 True if the request is for Android build. False otherwise.
256 """
257 os_type = kwargs.get('os_type', None)
258 return os_type == 'android'
259
260
Gabe Black3b567202015-09-23 21:07:59261def _get_downloader(kwargs):
262 """Returns the downloader based on passed in arguments.
263
264 Args:
265 kwargs: Keyword arguments for the request.
266 """
267 local_path = kwargs.get('local_path')
268 if local_path:
269 local_path = _canonicalize_local_path(local_path)
270
271 dl = None
272 if local_path:
Prathmesh Prabhu58d08932018-01-19 23:08:19273 delete_source = _parse_boolean_arg(kwargs, 'delete_source')
274 dl = downloader.LocalDownloader(updater.static_dir, local_path,
275 delete_source=delete_source)
Gabe Black3b567202015-09-23 21:07:59276
Dan Shi61305df2015-10-26 23:52:35277 if not _is_android_build_request(kwargs):
Gabe Black3b567202015-09-23 21:07:59278 archive_url = kwargs.get('archive_url')
279 if not archive_url and not local_path:
280 raise DevServerError('Requires archive_url or local_path to be '
281 'specified.')
282 if archive_url and local_path:
283 raise DevServerError('archive_url and local_path can not both be '
284 'specified.')
285 if not dl:
286 archive_url = _canonicalize_archive_url(archive_url)
287 dl = downloader.GoogleStorageDownloader(updater.static_dir, archive_url)
288 elif not dl:
289 target = kwargs.get('target', None)
Dan Shi72b16132015-10-08 19:10:33290 branch = kwargs.get('branch', None)
Dan Shi61305df2015-10-26 23:52:35291 build_id = kwargs.get('build_id', None)
292 if not target or not branch or not build_id:
Dan Shi72b16132015-10-08 19:10:33293 raise DevServerError(
Dan Shi61305df2015-10-26 23:52:35294 'target, branch, build ID must all be specified for downloading '
295 'Android build.')
Dan Shi72b16132015-10-08 19:10:33296 dl = downloader.AndroidBuildDownloader(updater.static_dir, branch, build_id,
297 target)
Gabe Black3b567202015-09-23 21:07:59298
299 return dl
300
301
302def _get_downloader_and_factory(kwargs):
303 """Returns the downloader and artifact factory based on passed in arguments.
304
305 Args:
306 kwargs: Keyword arguments for the request.
307 """
308 artifacts, files = _get_artifacts(kwargs)
309 dl = _get_downloader(kwargs)
310
311 if (isinstance(dl, downloader.GoogleStorageDownloader) or
312 isinstance(dl, downloader.LocalDownloader)):
313 factory_class = build_artifact.ChromeOSArtifactFactory
Dan Shi72b16132015-10-08 19:10:33314 elif isinstance(dl, downloader.AndroidBuildDownloader):
Gabe Black3b567202015-09-23 21:07:59315 factory_class = build_artifact.AndroidArtifactFactory
316 else:
317 raise DevServerError('Unrecognized value for downloader type: %s' %
318 type(dl))
319
320 factory = factory_class(dl.GetBuildDir(), artifacts, files, dl.GetBuild())
321
322 return dl, factory
323
324
Scott Zawalski4647ce62012-01-03 22:17:28325def _LeadingWhiteSpaceCount(string):
326 """Count the amount of leading whitespace in a string.
327
328 Args:
329 string: The string to count leading whitespace in.
Don Garrettf84631a2014-01-08 02:21:26330
Scott Zawalski4647ce62012-01-03 22:17:28331 Returns:
332 number of white space chars before characters start.
333 """
Gabe Black3b567202015-09-23 21:07:59334 matched = re.match(r'^\s+', string)
Scott Zawalski4647ce62012-01-03 22:17:28335 if matched:
336 return len(matched.group())
337
338 return 0
339
340
341def _PrintDocStringAsHTML(func):
342 """Make a functions docstring somewhat HTML style.
343
344 Args:
345 func: The function to return the docstring from.
Don Garrettf84631a2014-01-08 02:21:26346
Scott Zawalski4647ce62012-01-03 22:17:28347 Returns:
348 A string that is somewhat formated for a web browser.
349 """
350 # TODO(scottz): Make this parse Args/Returns in a prettier way.
351 # Arguments could be bolded and indented etc.
352 html_doc = []
353 for line in func.__doc__.splitlines():
354 leading_space = _LeadingWhiteSpaceCount(line)
355 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 18:26:55356 line = '&nbsp;' * leading_space + line
Scott Zawalski4647ce62012-01-03 22:17:28357
358 html_doc.append('<BR>%s' % line)
359
360 return '\n'.join(html_doc)
361
362
Simran Basief83d6a2014-08-28 21:32:01363def _GetUpdateTimestampHandler(static_dir):
364 """Returns a handler to update directory staged.timestamp.
365
366 This handler resets the stage.timestamp whenever static content is accessed.
367
368 Args:
369 static_dir: Directory from which static content is being staged.
370
371 Returns:
372 A cherrypy handler to update the timestamp of accessed content.
373 """
374 def UpdateTimestampHandler():
375 if not '404' in cherrypy.response.status:
376 build_match = re.match(devserver_constants.STAGED_BUILD_REGEX,
377 cherrypy.request.path_info)
378 if build_match:
379 build_dir = os.path.join(static_dir, build_match.group('build'))
380 downloader.Downloader.TouchTimestampForStaged(build_dir)
381 return UpdateTimestampHandler
382
383
Chris Sosa7c931362010-10-12 02:49:01384def _GetConfig(options):
385 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-08 01:55:33386
Mandeep Singh Baines38dcdda2012-12-08 01:55:33387 socket_host = '::'
Yu-Ju Hongc8d4af32013-11-12 23:14:26388 # Fall back to IPv4 when python is not configured with IPv6.
389 if not socket.has_ipv6:
Mandeep Singh Baines38dcdda2012-12-08 01:55:33390 socket_host = '0.0.0.0'
391
Simran Basief83d6a2014-08-28 21:32:01392 # Adds the UpdateTimestampHandler to cherrypy's tools. This tools executes
393 # on the on_end_resource hook. This hook is called once processing is
394 # complete and the response is ready to be returned.
395 cherrypy.tools.update_timestamp = cherrypy.Tool(
396 'on_end_resource', _GetUpdateTimestampHandler(options.static_dir))
397
David Riley2fcb0122017-11-02 18:25:39398 base_config = {
399 'global': {
400 'server.log_request_headers': True,
401 'server.protocol_version': 'HTTP/1.1',
402 'server.socket_host': socket_host,
403 'server.socket_port': int(options.port),
404 'response.timeout': 6000,
405 'request.show_tracebacks': True,
406 'server.socket_timeout': 60,
407 'server.thread_pool': 2,
408 'engine.autoreload.on': False,
409 },
410 '/api': {
411 # Gets rid of cherrypy parsing post file for args.
412 'request.process_request_body': False,
413 },
414 '/build': {
415 'response.timeout': 100000,
416 },
417 '/update': {
418 # Gets rid of cherrypy parsing post file for args.
419 'request.process_request_body': False,
420 'response.timeout': 10000,
421 },
422 # Sets up the static dir for file hosting.
423 '/static': {
424 'tools.staticdir.dir': options.static_dir,
425 'tools.staticdir.on': True,
426 'response.timeout': 10000,
427 'tools.update_timestamp.on': True,
428 },
429 }
Chris Sosa5f118ef2012-07-12 18:37:50430 if options.production:
Alex Miller93beca52013-07-31 02:25:09431 base_config['global'].update({'server.thread_pool': 150})
Chris Sosa7cd23202013-10-16 00:22:57432 # TODO(sosa): Do this more cleanly.
433 gsutil_util.GSUTIL_ATTEMPTS = 5
Scott Zawalski1c5e7cd2012-02-27 18:12:52434
Chris Sosa7c931362010-10-12 02:49:01435 return base_config
[email protected]64244662009-11-12 00:52:08436
Darin Petkove17164a2010-08-11 20:24:41437
Gilad Arnoldd5ebaaa2012-10-02 18:52:38438def _GetRecursiveMemberObject(root, member_list):
439 """Returns an object corresponding to a nested member list.
440
441 Args:
442 root: the root object to search
443 member_list: list of nested members to search
Don Garrettf84631a2014-01-08 02:21:26444
Gilad Arnoldd5ebaaa2012-10-02 18:52:38445 Returns:
446 An object corresponding to the member name list; None otherwise.
447 """
448 for member in member_list:
449 next_root = root.__class__.__dict__.get(member)
450 if not next_root:
451 return None
452 root = next_root
453 return root
454
455
456def _IsExposed(name):
457 """Returns True iff |name| has an `exposed' attribute and it is set."""
458 return hasattr(name, 'exposed') and name.exposed
459
460
Gilad Arnold748c8322012-10-12 16:51:35461def _GetExposedMethod(root, nested_member, ignored=None):
Gilad Arnoldd5ebaaa2012-10-02 18:52:38462 """Returns a CherryPy-exposed method, if such exists.
463
464 Args:
465 root: the root object for searching
466 nested_member: a slash-joined path to the nested member
467 ignored: method paths to be ignored
Don Garrettf84631a2014-01-08 02:21:26468
Gilad Arnoldd5ebaaa2012-10-02 18:52:38469 Returns:
470 A function object corresponding to the path defined by |member_list| from
471 the |root| object, if the function is exposed and not ignored; None
472 otherwise.
473 """
Gilad Arnold748c8322012-10-12 16:51:35474 method = (not (ignored and nested_member in ignored) and
Gilad Arnoldd5ebaaa2012-10-02 18:52:38475 _GetRecursiveMemberObject(root, nested_member.split('/')))
Gabe Black3b567202015-09-23 21:07:59476 if method and type(method) == types.FunctionType and _IsExposed(method):
Gilad Arnoldd5ebaaa2012-10-02 18:52:38477 return method
478
479
Gilad Arnold748c8322012-10-12 16:51:35480def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 18:52:38481 """Finds exposed CherryPy methods.
482
483 Args:
484 root: the root object for searching
485 prefix: slash-joined chain of members leading to current object
486 unlisted: URLs to be excluded regardless of their exposed status
Don Garrettf84631a2014-01-08 02:21:26487
Gilad Arnoldd5ebaaa2012-10-02 18:52:38488 Returns:
489 List of exposed URLs that are not unlisted.
490 """
491 method_list = []
492 for member in sorted(root.__class__.__dict__.keys()):
493 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 16:51:35494 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 18:52:38495 continue
496 member_obj = root.__class__.__dict__[member]
497 if _IsExposed(member_obj):
498 if type(member_obj) == types.FunctionType:
499 method_list.append(prefixed_member)
500 else:
501 method_list += _FindExposedMethods(
502 member_obj, prefixed_member, unlisted)
503 return method_list
504
505
xixuan52c2fba2016-05-21 00:02:48506def _check_base_args_for_auto_update(kwargs):
xixuanac89ce82016-12-01 00:48:20507 """Check basic args required for auto-update.
508
509 Args:
510 kwargs: the parameters to be checked.
511
512 Raises:
513 DevServerHTTPError if required parameters don't exist in kwargs.
514 """
xixuan52c2fba2016-05-21 00:02:48515 if 'host_name' not in kwargs:
516 raise common_util.DevServerHTTPError(KEY_ERROR_MSG % 'host_name')
517
518 if 'build_name' not in kwargs:
519 raise common_util.DevServerHTTPError(KEY_ERROR_MSG % 'build_name')
520
521
522def _parse_boolean_arg(kwargs, key):
xixuanac89ce82016-12-01 00:48:20523 """Parse boolean arg from kwargs.
524
525 Args:
526 kwargs: the parameters to be checked.
527 key: the key to be parsed.
528
529 Returns:
530 The boolean value of kwargs[key], or False if key doesn't exist in kwargs.
531
532 Raises:
533 DevServerHTTPError if kwargs[key] is not a boolean variable.
534 """
xixuan52c2fba2016-05-21 00:02:48535 if key in kwargs:
536 if kwargs[key] == 'True':
537 return True
538 elif kwargs[key] == 'False':
539 return False
540 else:
541 raise common_util.DevServerHTTPError(
542 'The value for key %s is not boolean.' % key)
543 else:
544 return False
545
xixuan447ad9d2017-02-28 22:46:20546
xixuanac89ce82016-12-01 00:48:20547def _parse_string_arg(kwargs, key):
548 """Parse string arg from kwargs.
549
550 Args:
551 kwargs: the parameters to be checked.
552 key: the key to be parsed.
553
554 Returns:
555 The string value of kwargs[key], or None if key doesn't exist in kwargs.
556 """
557 if key in kwargs:
558 return kwargs[key]
559 else:
560 return None
561
xixuan447ad9d2017-02-28 22:46:20562
xixuanac89ce82016-12-01 00:48:20563def _build_uri_from_build_name(build_name):
564 """Get build url from a given build name.
565
566 Args:
567 build_name: the build name to be parsed, whose format is
568 'board/release_version'.
569
570 Returns:
571 The release_archive_url on Google Storage for this build name.
572 """
573 return gspaths.ChromeosReleases.BuildUri(
574 cros_update.STABLE_BUILD_CHANNEL, build_name.split('/')[0],
575 build_name.split('/')[1])
xixuan52c2fba2016-05-21 00:02:48576
xixuan447ad9d2017-02-28 22:46:20577
578def _clear_process(host_name, pid):
579 """Clear AU process for given hostname and pid.
580
581 This clear includes:
582 1. kill process if it's alive.
583 2. delete the track status file of this process.
584 3. delete the executing log file of this process.
585
586 Args:
587 host_name: the host to execute auto-update.
588 pid: the background auto-update process id.
589 """
590 if cros_update_progress.IsProcessAlive(pid):
591 os.killpg(int(pid), signal.SIGKILL)
592
593 cros_update_progress.DelTrackStatusFile(host_name, pid)
594 cros_update_progress.DelExecuteLogFile(host_name, pid)
595
596
Dale Curtisc9aaf3a2011-08-09 22:47:40597class ApiRoot(object):
598 """RESTful API for Dev Server information."""
599 exposed = True
600
601 @cherrypy.expose
602 def hostinfo(self, ip):
603 """Returns a JSON dictionary containing information about the given ip.
604
Gilad Arnold1b908392012-10-05 18:36:27605 Args:
606 ip: address of host whose info is requested
Don Garrettf84631a2014-01-08 02:21:26607
Gilad Arnold1b908392012-10-05 18:36:27608 Returns:
609 A JSON dictionary containing all or some of the following fields:
610 last_event_type (int): last update event type received
611 last_event_status (int): last update event status received
612 last_known_version (string): last known version reported in update ping
613 forced_update_label (string): update label to force next update ping to
614 use, set by setnextupdate
615 See the OmahaEvent class in update_engine/omaha_request_action.h for
616 event type and status code definitions. If the ip does not exist an empty
617 string is returned.
Dale Curtisc9aaf3a2011-08-09 22:47:40618
Gilad Arnold1b908392012-10-05 18:36:27619 Example URL:
620 http://myhost/api/hostinfo?ip=192.168.1.5
621 """
Dale Curtisc9aaf3a2011-08-09 22:47:40622 return updater.HandleHostInfoPing(ip)
623
624 @cherrypy.expose
Gilad Arnold286a0062012-01-12 21:47:02625 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 18:36:27626 """Returns a JSON object containing a log of host event.
627
628 Args:
629 ip: address of host whose event log is requested, or `all'
Don Garrettf84631a2014-01-08 02:21:26630
Gilad Arnold1b908392012-10-05 18:36:27631 Returns:
632 A JSON encoded list (log) of dictionaries (events), each of which
633 containing a `timestamp' and other event fields, as described under
634 /api/hostinfo.
635
636 Example URL:
637 http://myhost/api/hostlog?ip=192.168.1.5
638 """
Gilad Arnold286a0062012-01-12 21:47:02639 return updater.HandleHostLogPing(ip)
640
641 @cherrypy.expose
Dale Curtisc9aaf3a2011-08-09 22:47:40642 def setnextupdate(self, ip):
643 """Allows the response to the next update ping from a host to be set.
644
645 Takes the IP of the host and an update label as normally provided to the
Gilad Arnold1b908392012-10-05 18:36:27646 /update command.
647 """
Dale Curtisc9aaf3a2011-08-09 22:47:40648 body_length = int(cherrypy.request.headers['Content-Length'])
649 label = cherrypy.request.rfile.read(body_length)
650
651 if label:
652 label = label.strip()
653 if label:
654 return updater.HandleSetUpdatePing(ip, label)
Chris Sosa4b951602014-04-10 03:26:07655 raise common_util.DevServerHTTPError(400, 'No label provided.')
Dale Curtisc9aaf3a2011-08-09 22:47:40656
657
Gilad Arnold55a2a372012-10-02 16:46:32658 @cherrypy.expose
Don Garrettf84631a2014-01-08 02:21:26659 def fileinfo(self, *args):
Gilad Arnold55a2a372012-10-02 16:46:32660 """Returns information about a given staged file.
661
662 Args:
Don Garrettf84631a2014-01-08 02:21:26663 args: path to the file inside the server's static staging directory
664
Gilad Arnold55a2a372012-10-02 16:46:32665 Returns:
666 A JSON encoded dictionary with information about the said file, which may
667 contain the following keys/values:
Gilad Arnold1b908392012-10-05 18:36:27668 size (int): the file size in bytes
669 sha1 (string): a base64 encoded SHA1 hash
670 sha256 (string): a base64 encoded SHA256 hash
671
672 Example URL:
673 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 16:46:32674 """
Don Garrettf84631a2014-01-08 02:21:26675 file_path = os.path.join(updater.static_dir, *args)
Gilad Arnold55a2a372012-10-02 16:46:32676 if not os.path.exists(file_path):
677 raise DevServerError('file not found: %s' % file_path)
678 try:
679 file_size = os.path.getsize(file_path)
680 file_sha1 = common_util.GetFileSha1(file_path)
681 file_sha256 = common_util.GetFileSha256(file_path)
682 except os.error, e:
683 raise DevServerError('failed to get info for file %s: %s' %
Gilad Arnolde74b3812013-04-22 18:27:38684 (file_path, e))
685
686 is_delta = autoupdate.Autoupdate.IsDeltaFormatFile(file_path)
687
688 return json.dumps({
689 autoupdate.Autoupdate.SIZE_ATTR: file_size,
690 autoupdate.Autoupdate.SHA1_ATTR: file_sha1,
691 autoupdate.Autoupdate.SHA256_ATTR: file_sha256,
692 autoupdate.Autoupdate.ISDELTA_ATTR: is_delta
693 })
Gilad Arnold55a2a372012-10-02 16:46:32694
Chris Sosa76e44b92013-01-31 20:11:38695
David Rochberg7c79a812011-01-19 19:24:45696class DevServerRoot(object):
Chris Sosa7c931362010-10-12 02:49:01697 """The Root Class for the Dev Server.
698
699 CherryPy works as follows:
700 For each method in this class, cherrpy interprets root/path
701 as a call to an instance of DevServerRoot->method_name. For example,
702 a call to http://myhost/build will call build. CherryPy automatically
703 parses http args and places them as keyword arguments in each method.
704 For paths http://myhost/update/dir1/dir2, you can use *args so that
705 cherrypy uses the update method and puts the extra paths in args.
706 """
Gilad Arnoldf8f769f2012-09-24 15:43:01707 # Method names that should not be listed on the index page.
708 _UNLISTED_METHODS = ['index', 'doc']
709
Dale Curtisc9aaf3a2011-08-09 22:47:40710 api = ApiRoot()
Chris Sosa7c931362010-10-12 02:49:01711
Dan Shi59ae7092013-06-04 21:37:27712 # Number of threads that devserver is staging images.
713 _staging_thread_count = 0
714 # Lock used to lock increasing/decreasing count.
715 _staging_thread_count_lock = threading.Lock()
716
Dan Shiafd0e492015-05-27 21:23:51717 @require_psutil()
718 def _refresh_io_stats(self):
719 """A call running in a thread to update IO stats periodically."""
720 prev_disk_io_counters = psutil.disk_io_counters()
721 prev_network_io_counters = psutil.net_io_counters()
722 prev_read_time = time.time()
723 while True:
724 time.sleep(STATS_INTERVAL)
725 now = time.time()
726 interval = now - prev_read_time
727 prev_read_time = now
728 # Disk IO is for all disks.
729 disk_io_counters = psutil.disk_io_counters()
730 network_io_counters = psutil.net_io_counters()
731
732 self.disk_read_bytes_per_sec = (
733 disk_io_counters.read_bytes -
734 prev_disk_io_counters.read_bytes)/interval
735 self.disk_write_bytes_per_sec = (
736 disk_io_counters.write_bytes -
737 prev_disk_io_counters.write_bytes)/interval
738 prev_disk_io_counters = disk_io_counters
739
740 self.network_sent_bytes_per_sec = (
741 network_io_counters.bytes_sent -
742 prev_network_io_counters.bytes_sent)/interval
743 self.network_recv_bytes_per_sec = (
744 network_io_counters.bytes_recv -
745 prev_network_io_counters.bytes_recv)/interval
746 prev_network_io_counters = network_io_counters
747
748 @require_psutil()
749 def _start_io_stat_thread(self):
Gabe Black3b567202015-09-23 21:07:59750 """Start the thread to collect IO stats."""
Dan Shiafd0e492015-05-27 21:23:51751 thread = threading.Thread(target=self._refresh_io_stats)
752 thread.daemon = True
753 thread.start()
754
joychen3cb228e2013-06-12 19:13:13755 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 22:20:41756 self._builder = None
Simran Basi4baad082013-02-14 21:39:18757 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 19:13:13758 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 19:24:45759
Dan Shiafd0e492015-05-27 21:23:51760 # Cache of disk IO stats, a thread refresh the stats every 10 seconds.
761 # lock is not used for these variables as the only thread writes to these
762 # variables is _refresh_io_stats.
763 self.disk_read_bytes_per_sec = 0
764 self.disk_write_bytes_per_sec = 0
765 # Cache of network IO stats.
766 self.network_sent_bytes_per_sec = 0
767 self.network_recv_bytes_per_sec = 0
768 self._start_io_stat_thread()
769
Dale Curtisc9aaf3a2011-08-09 22:47:40770 @cherrypy.expose
David Rochberg7c79a812011-01-19 19:24:45771 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-12 02:49:01772 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 22:20:41773 import builder
774 if self._builder is None:
775 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 19:24:45776 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-12 02:49:01777
Dale Curtisc9aaf3a2011-08-09 22:47:40778 @cherrypy.expose
Dan Shif8eb0d12013-08-02 00:52:06779 def is_staged(self, **kwargs):
780 """Check if artifacts have been downloaded.
781
Chris Sosa6b0c6172013-08-06 00:01:33782 async: True to return without waiting for download to complete.
783 artifacts: Comma separated list of named artifacts to download.
784 These are defined in artifact_info and have their implementation
785 in build_artifact.py.
786 files: Comma separated list of file artifacts to stage. These
787 will be available as is in the corresponding static directory with no
788 custom post-processing.
789
790 returns: True of all artifacts are staged.
Dan Shif8eb0d12013-08-02 00:52:06791
792 Example:
793 To check if autotest and test_suites are staged:
794 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
795 artifacts=autotest,test_suites
796 """
Gabe Black3b567202015-09-23 21:07:59797 dl, factory = _get_downloader_and_factory(kwargs)
Aviv Keshet57d18172016-06-19 03:39:09798 response = str(dl.IsStaged(factory))
799 _Log('Responding to is_staged %s request with %r', kwargs, response)
800 return response
Dan Shi59ae7092013-06-04 21:37:27801
Chris Sosa76e44b92013-01-31 20:11:38802 @cherrypy.expose
Prashanth Ba06d2d22014-03-07 23:35:19803 def list_image_dir(self, **kwargs):
804 """Take an archive url and list the contents in its staged directory.
805
806 Args:
807 kwargs:
808 archive_url: Google Storage URL for the build.
809
810 Example:
811 To list the contents of where this devserver should have staged
812 gs://image-archive/<board>-release/<build> call:
813 http://devserver_url:<port>/list_image_dir?archive_url=<gs://..>
814
815 Returns:
816 A string with information about the contents of the image directory.
817 """
Gabe Black3b567202015-09-23 21:07:59818 dl = _get_downloader(kwargs)
Prashanth Ba06d2d22014-03-07 23:35:19819 try:
Gabe Black3b567202015-09-23 21:07:59820 image_dir_contents = dl.ListBuildDir()
Prashanth Ba06d2d22014-03-07 23:35:19821 except build_artifact.ArtifactDownloadError as e:
822 return 'Cannot list the contents of staged artifacts. %s' % e
823 if not image_dir_contents:
Gabe Black3b567202015-09-23 21:07:59824 return '%s has not been staged on this devserver.' % dl.DescribeSource()
Prashanth Ba06d2d22014-03-07 23:35:19825 return image_dir_contents
826
827 @cherrypy.expose
Chris Sosa76e44b92013-01-31 20:11:38828 def stage(self, **kwargs):
Gabe Black3b567202015-09-23 21:07:59829 """Downloads and caches build artifacts.
Chris Sosa76e44b92013-01-31 20:11:38830
Gabe Black3b567202015-09-23 21:07:59831 Downloads and caches build artifacts, possibly from a Google Storage URL,
Dan Shi72b16132015-10-08 19:10:33832 or from Android's build server. Returns once these have been downloaded
Gabe Black3b567202015-09-23 21:07:59833 on the devserver. A call to this will attempt to cache non-specified
834 artifacts in the background for the given from the given URL following
835 the principle of spatial locality. Spatial locality of different
Chris Sosa76e44b92013-01-31 20:11:38836 artifacts is explicitly defined in the build_artifact module.
837
838 These artifacts will then be available from the static/ sub-directory of
839 the devserver.
840
841 Args:
842 archive_url: Google Storage URL for the build.
Simran Basi4243a862014-12-12 20:48:33843 local_path: Local path for the build.
Prathmesh Prabhu58d08932018-01-19 23:08:19844 delete_source: Only meaningful with local_path. bool to indicate if the
845 source files should be deleted. This is especially useful when staging
846 a file locally in resource constrained environments as it allows us to
847 move the relevant files locally instead of copying them.
Dan Shif8eb0d12013-08-02 00:52:06848 async: True to return without waiting for download to complete.
Chris Sosa6b0c6172013-08-06 00:01:33849 artifacts: Comma separated list of named artifacts to download.
850 These are defined in artifact_info and have their implementation
851 in build_artifact.py.
852 files: Comma separated list of files to stage. These
853 will be available as is in the corresponding static directory with no
854 custom post-processing.
Laurence Goodbyf5c958d2016-01-15 02:23:56855 clean: True to remove any previously staged artifacts first.
Chris Sosa76e44b92013-01-31 20:11:38856
857 Example:
858 To download the autotest and test suites tarballs:
859 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
860 artifacts=autotest,test_suites
861 To download the full update payload:
862 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
863 artifacts=full_payload
Chris Sosa6b0c6172013-08-06 00:01:33864 To download just a file called blah.bin:
865 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
866 files=blah.bin
Chris Sosa76e44b92013-01-31 20:11:38867
868 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 23:39:34869 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 20:11:38870
871 Note for this example, relative path is the archive_url stripped of its
872 basename i.e. path/ in the examples above. Specific example:
873
874 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
875
876 Will get staged to:
877
joychened64b222013-06-21 23:39:34878 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Chris Sosa76e44b92013-01-31 20:11:38879 """
Gabe Black3b567202015-09-23 21:07:59880 dl, factory = _get_downloader_and_factory(kwargs)
881
Dan Shi59ae7092013-06-04 21:37:27882 with DevServerRoot._staging_thread_count_lock:
883 DevServerRoot._staging_thread_count += 1
884 try:
Laurence Goodbyf5c958d2016-01-15 02:23:56885 boolean_string = kwargs.get('clean')
886 clean = xbuddy.XBuddy.ParseBoolean(boolean_string)
887 if clean and os.path.exists(dl.GetBuildDir()):
888 _Log('Removing %s' % dl.GetBuildDir())
889 shutil.rmtree(dl.GetBuildDir())
Gabe Black3b567202015-09-23 21:07:59890 async = kwargs.get('async', False)
891 dl.Download(factory, async=async)
Dan Shi59ae7092013-06-04 21:37:27892 finally:
893 with DevServerRoot._staging_thread_count_lock:
894 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 20:11:38895 return 'Success'
Chris Sosacde6bf42012-06-01 01:36:39896
897 @cherrypy.expose
xixuan52c2fba2016-05-21 00:02:48898 def cros_au(self, **kwargs):
899 """Auto-update a CrOS DUT.
900
901 Args:
902 kwargs:
903 host_name: the hostname of the DUT to auto-update.
904 build_name: the build name for update the DUT.
905 force_update: Force an update even if the version installed is the
906 same. Default: False.
907 full_update: If True, do not run stateful update, directly force a full
908 reimage. If False, try stateful update first if the dut is already
909 installed with the same version.
910 async: Whether the auto_update function is ran in the background.
David Rileyee75de22017-11-02 17:48:15911 quick_provision: Whether the quick provision path is attempted first.
xixuan52c2fba2016-05-21 00:02:48912
913 Returns:
914 A tuple includes two elements:
915 a boolean variable represents whether the auto-update process is
916 successfully started.
917 an integer represents the background auto-update process id.
918 """
919 _check_base_args_for_auto_update(kwargs)
920
921 host_name = kwargs['host_name']
922 build_name = kwargs['build_name']
923 force_update = _parse_boolean_arg(kwargs, 'force_update')
924 full_update = _parse_boolean_arg(kwargs, 'full_update')
925 async = _parse_boolean_arg(kwargs, 'async')
xixuanac89ce82016-12-01 00:48:20926 original_build = _parse_string_arg(kwargs, 'original_build')
David Haddock90e49442017-04-08 02:14:09927 payload_filename = _parse_string_arg(kwargs, 'payload_filename')
David Haddock20559612017-06-29 05:15:08928 clobber_stateful = _parse_boolean_arg(kwargs, 'clobber_stateful')
David Rileyee75de22017-11-02 17:48:15929 quick_provision = _parse_boolean_arg(kwargs, 'quick_provision')
930
931 devserver_url = updater.GetDevserverUrl()
932 static_url = updater.GetStaticUrl()
xixuan52c2fba2016-05-21 00:02:48933
934 if async:
935 path = os.path.dirname(os.path.abspath(__file__))
936 execute_file = os.path.join(path, 'cros_update.py')
937 args = (AUTO_UPDATE_CMD % (execute_file, host_name, build_name,
938 updater.static_dir))
xixuanac89ce82016-12-01 00:48:20939
940 # The original_build's format is like: link/3428.210.0
941 # The corresponding release_archive_url's format is like:
942 # gs://chromeos-releases/stable-channel/link/3428.210.0
943 if original_build:
944 release_archive_url = _build_uri_from_build_name(original_build)
945 # First staging the stateful.tgz synchronousely.
946 self.stage(files='stateful.tgz', async=False,
947 archive_url=release_archive_url)
948 args = ('%s --original_build %s' % (args, original_build))
949
xixuan52c2fba2016-05-21 00:02:48950 if force_update:
951 args = ('%s --force_update' % args)
952
953 if full_update:
954 args = ('%s --full_update' % args)
955
David Haddock90e49442017-04-08 02:14:09956 if payload_filename:
957 args = ('%s --payload_filename %s' % (args, payload_filename))
958
David Haddock20559612017-06-29 05:15:08959 if clobber_stateful:
960 args = ('%s --clobber_stateful' % args)
961
David Rileyee75de22017-11-02 17:48:15962 if quick_provision:
963 args = ('%s --quick_provision' % args)
964
965 if devserver_url:
966 args = ('%s --devserver_url %s' % (args, devserver_url))
967
968 if static_url:
969 args = ('%s --static_url %s' % (args, static_url))
970
xixuan2a0970a2016-08-10 19:12:44971 p = subprocess.Popen([args], shell=True, preexec_fn=os.setsid)
972 pid = os.getpgid(p.pid)
xixuan52c2fba2016-05-21 00:02:48973
974 # Pre-write status in the track_status_file before the first call of
975 # 'get_au_status' to make sure that the track_status_file exists.
xixuan2a0970a2016-08-10 19:12:44976 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
xixuan52c2fba2016-05-21 00:02:48977 progress_tracker.WriteStatus('CrOS update is just started.')
978
xixuan2a0970a2016-08-10 19:12:44979 return json.dumps((True, pid))
xixuan52c2fba2016-05-21 00:02:48980 else:
981 cros_update_trigger = cros_update.CrOSUpdateTrigger(
xixuanac89ce82016-12-01 00:48:20982 host_name, build_name, updater.static_dir, force_update=force_update,
David Rileyee75de22017-11-02 17:48:15983 full_update=full_update, original_build=original_build,
984 quick_provision=quick_provision, devserver_url=devserver_url,
985 static_url=static_url)
xixuan52c2fba2016-05-21 00:02:48986 cros_update_trigger.TriggerAU()
xixuan27d50442017-08-09 17:38:25987 return json.dumps((True, -1))
xixuan52c2fba2016-05-21 00:02:48988
989 @cherrypy.expose
990 def get_au_status(self, **kwargs):
991 """Check if the auto-update task is finished.
992
993 It handles 4 cases:
994 1. If an error exists in the track_status_file, delete the track file and
995 raise it.
996 2. If cros-update process is finished, delete the file and return the
997 success result.
998 3. If the process is not running, delete the track file and raise an error
999 about 'the process is terminated due to unknown reason'.
1000 4. If the track_status_file does not exist, kill the process if it exists,
1001 and raise the IOError.
1002
1003 Args:
1004 kwargs:
1005 host_name: the hostname of the DUT to auto-update.
1006 pid: the background process id of cros-update.
1007
1008 Returns:
xixuan28d99072016-10-06 19:24:161009 A dict with three elements:
xixuan52c2fba2016-05-21 00:02:481010 a boolean variable represents whether the auto-update process is
1011 finished.
1012 a string represents the current auto-update process status.
1013 For example, 'Transfer Devserver/Stateful Update Package'.
xixuan28d99072016-10-06 19:24:161014 a detailed error message paragraph if there exists an Auto-Update
1015 error, in which the last line shows the main exception. Empty
1016 string otherwise.
xixuan52c2fba2016-05-21 00:02:481017 """
1018 if 'host_name' not in kwargs:
1019 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
1020
1021 if 'pid' not in kwargs:
1022 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
1023
1024 host_name = kwargs['host_name']
1025 pid = kwargs['pid']
1026 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
1027
xixuan28d99072016-10-06 19:24:161028 result_dict = {'finished': False, 'status': '', 'detailed_error_msg': ''}
xixuan52c2fba2016-05-21 00:02:481029 try:
1030 result = progress_tracker.ReadStatus()
1031 if result.startswith(cros_update_progress.ERROR_TAG):
xixuan28d99072016-10-06 19:24:161032 result_dict['detailed_error_msg'] = result[len(
1033 cros_update_progress.ERROR_TAG):]
xixuan28681fd2016-11-23 19:13:561034 elif result == cros_update_progress.FINISHED:
xixuan28d99072016-10-06 19:24:161035 result_dict['finished'] = True
1036 result_dict['status'] = result
xixuan28681fd2016-11-23 19:13:561037 elif not cros_update_progress.IsProcessAlive(pid):
xixuan28d99072016-10-06 19:24:161038 result_dict['detailed_error_msg'] = (
1039 'Cros_update process terminated midway due to unknown reason. '
1040 'Last update status was %s' % result)
xixuan28681fd2016-11-23 19:13:561041 else:
1042 result_dict['status'] = result
1043 except IOError as e:
1044 if pid and cros_update_progress.IsProcessAlive(pid):
xixuan2a0970a2016-08-10 19:12:441045 os.killpg(int(pid), signal.SIGKILL)
xixuan52c2fba2016-05-21 00:02:481046
xixuan28681fd2016-11-23 19:13:561047 result_dict['detailed_error_msg'] = str(e)
1048
1049 return json.dumps(result_dict)
xixuan52c2fba2016-05-21 00:02:481050
1051 @cherrypy.expose
David Riley6d5fca02017-10-31 17:35:471052 def post_au_status(self, status, **kwargs):
1053 """Updates the status of an auto-update task.
1054
1055 Callers will need to POST to this URL with a body of MIME-type
1056 "multipart/form-data".
1057 The body should include a single argument, 'status', containing the
1058 AU status to record.
1059
1060 Args:
1061 status: The updated status.
1062 kwargs:
1063 host_name: the hostname of the DUT to auto-update.
1064 pid: the background process id of cros-update.
1065 """
1066 if 'host_name' not in kwargs:
1067 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
1068
1069 if 'pid' not in kwargs:
1070 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
1071
1072 host_name = kwargs['host_name']
1073 pid = kwargs['pid']
David Riley3cea2582017-11-25 06:03:011074 status = status.rstrip()
1075 _Log('Recording status for %s (%s): %s' % (host_name, pid, status))
David Riley6d5fca02017-10-31 17:35:471076 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
1077
David Riley3cea2582017-11-25 06:03:011078 progress_tracker.WriteStatus(status)
David Riley6d5fca02017-10-31 17:35:471079
1080 return 'True'
1081
1082 @cherrypy.expose
xixuan52c2fba2016-05-21 00:02:481083 def handler_cleanup(self, **kwargs):
xixuan3bc974e2016-10-19 00:21:431084 """Clean track status log and temp directory for CrOS auto-update process.
xixuan52c2fba2016-05-21 00:02:481085
1086 Args:
1087 kwargs:
1088 host_name: the hostname of the DUT to auto-update.
1089 pid: the background process id of cros-update.
1090 """
1091 if 'host_name' not in kwargs:
1092 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
1093
1094 if 'pid' not in kwargs:
1095 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
1096
1097 host_name = kwargs['host_name']
1098 pid = kwargs['pid']
1099 cros_update_progress.DelTrackStatusFile(host_name, pid)
xixuan3bc974e2016-10-19 00:21:431100 cros_update_progress.DelAUTempDirectory(host_name, pid)
xixuan52c2fba2016-05-21 00:02:481101
1102 @cherrypy.expose
1103 def kill_au_proc(self, **kwargs):
1104 """Kill CrOS auto-update process using given process id.
1105
1106 Args:
1107 kwargs:
1108 host_name: Kill all the CrOS auto-update process of this host.
1109
1110 Returns:
1111 True if all processes are killed properly.
1112 """
1113 if 'host_name' not in kwargs:
1114 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
1115
xixuan447ad9d2017-02-28 22:46:201116 cur_pid = kwargs.get('pid')
1117
xixuan52c2fba2016-05-21 00:02:481118 host_name = kwargs['host_name']
xixuan3bc974e2016-10-19 00:21:431119 track_log_list = cros_update_progress.GetAllTrackStatusFileByHostName(
1120 host_name)
xixuan52c2fba2016-05-21 00:02:481121 for log in track_log_list:
1122 # The track log's full path is: path/host_name_pid.log
1123 # Use splitext to remove file extension, then parse pid from the
1124 # filename.
1125 pid = os.path.splitext(os.path.basename(log))[0][len(host_name)+1:]
xixuan447ad9d2017-02-28 22:46:201126 _clear_process(host_name, pid)
xixuan52c2fba2016-05-21 00:02:481127
xixuan447ad9d2017-02-28 22:46:201128 if cur_pid:
1129 _clear_process(host_name, cur_pid)
xixuan52c2fba2016-05-21 00:02:481130
1131 return 'True'
1132
1133 @cherrypy.expose
1134 def collect_cros_au_log(self, **kwargs):
1135 """Collect CrOS auto-update log.
1136
1137 Args:
1138 kwargs:
1139 host_name: the hostname of the DUT to auto-update.
1140 pid: the background process id of cros-update.
1141
1142 Returns:
David Haddock9f459632017-05-11 21:45:461143 A dictionary containing the execute log file and any hostlog files.
xixuan52c2fba2016-05-21 00:02:481144 """
1145 if 'host_name' not in kwargs:
1146 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
1147
1148 if 'pid' not in kwargs:
1149 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
1150
1151 host_name = kwargs['host_name']
1152 pid = kwargs['pid']
xixuan3bc974e2016-10-19 00:21:431153
1154 # Fetch the execute log recorded by cros_update_progress.
xixuan1bbfaba2016-10-14 00:53:221155 au_log = cros_update_progress.ReadExecuteLogFile(host_name, pid)
1156 cros_update_progress.DelExecuteLogFile(host_name, pid)
David Haddock9f459632017-05-11 21:45:461157 # Fetch the cros_au host_logs if they exist
1158 au_hostlogs = cros_update_progress.ReadAUHostLogFiles(host_name, pid)
1159 return json.dumps({'cros_au_log': au_log, 'host_logs': au_hostlogs})
xixuan1bbfaba2016-10-14 00:53:221160
xixuan52c2fba2016-05-21 00:02:481161 @cherrypy.expose
Dan Shi2f136862016-02-11 23:38:381162 def locate_file(self, **kwargs):
1163 """Get the path to the given file name.
1164
1165 This method looks up the given file name inside specified build artifacts.
1166 One use case is to help caller to locate an apk file inside a build
1167 artifact. The location of the apk file could be different based on the
1168 branch and target.
1169
1170 Args:
1171 file_name: Name of the file to look for.
1172 artifacts: A list of artifact names to search for the file.
1173
1174 Returns:
1175 Path to the file with the given name. It's relative to the folder for the
1176 build, e.g., DATA/priv-app/sl4a/sl4a.apk
Dan Shi2f136862016-02-11 23:38:381177 """
1178 dl, _ = _get_downloader_and_factory(kwargs)
1179 try:
Joe Brennan1691f8e2017-03-15 22:53:361180 file_name = kwargs['file_name']
Dan Shi2f136862016-02-11 23:38:381181 artifacts = kwargs['artifacts']
1182 except KeyError:
1183 raise DevServerError('`file_name` and `artifacts` are required to search '
1184 'for a file in build artifacts.')
1185 build_path = dl.GetBuildDir()
1186 for artifact in artifacts:
1187 # Get the unzipped folder of the artifact. If it's not defined in
1188 # ARTIFACT_UNZIP_FOLDER_MAP, assume the files are unzipped to the build
1189 # directory directly.
1190 folder = artifact_info.ARTIFACT_UNZIP_FOLDER_MAP.get(artifact, '')
1191 artifact_path = os.path.join(build_path, folder)
1192 for root, _, filenames in os.walk(artifact_path):
Joe Brennan1691f8e2017-03-15 22:53:361193 if file_name in set([f for f in filenames]):
Dan Shi2f136862016-02-11 23:38:381194 return os.path.relpath(os.path.join(root, file_name), build_path)
1195 raise DevServerError('File `%s` can not be found in artifacts: %s' %
1196 (file_name, artifacts))
1197
1198 @cherrypy.expose
Simran Basi4baad082013-02-14 21:39:181199 def setup_telemetry(self, **kwargs):
1200 """Extracts and sets up telemetry
1201
1202 This method goes through the telemetry deps packages, and stages them on
1203 the devserver to be used by the drones and the telemetry tests.
1204
1205 Args:
1206 archive_url: Google Storage URL for the build.
1207
1208 Returns:
1209 Path to the source folder for the telemetry codebase once it is staged.
1210 """
Gabe Black3b567202015-09-23 21:07:591211 dl = _get_downloader(kwargs)
Simran Basi4baad082013-02-14 21:39:181212
Gabe Black3b567202015-09-23 21:07:591213 build_path = dl.GetBuildDir()
Simran Basi4baad082013-02-14 21:39:181214 deps_path = os.path.join(build_path, 'autotest/packages')
1215 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
1216 src_folder = os.path.join(telemetry_path, 'src')
1217
1218 with self._telemetry_lock_dict.lock(telemetry_path):
1219 if os.path.exists(src_folder):
1220 # Telemetry is already fully stage return
1221 return src_folder
1222
1223 common_util.MkDirP(telemetry_path)
1224
1225 # Copy over the required deps tar balls to the telemetry directory.
1226 for dep in TELEMETRY_DEPS:
1227 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 23:40:041228 if not os.path.exists(dep_path):
1229 # This dep does not exist (could be new), do not extract it.
1230 continue
Simran Basi4baad082013-02-14 21:39:181231 try:
1232 common_util.ExtractTarball(dep_path, telemetry_path)
1233 except common_util.CommonUtilError as e:
1234 shutil.rmtree(telemetry_path)
1235 raise DevServerError(str(e))
1236
1237 # By default all the tarballs extract to test_src but some parts of
1238 # the telemetry code specifically hardcoded to exist inside of 'src'.
1239 test_src = os.path.join(telemetry_path, 'test_src')
1240 try:
1241 shutil.move(test_src, src_folder)
1242 except shutil.Error:
1243 # This can occur if src_folder already exists. Remove and retry move.
1244 shutil.rmtree(src_folder)
Gabe Black3b567202015-09-23 21:07:591245 raise DevServerError(
1246 'Failure in telemetry setup for build %s. Appears that the '
1247 'test_src to src move failed.' % dl.GetBuild())
Simran Basi4baad082013-02-14 21:39:181248
1249 return src_folder
1250
1251 @cherrypy.expose
Chris Sosa76e44b92013-01-31 20:11:381252 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 19:22:361253 """Symbolicates a minidump using pre-downloaded symbols, returns it.
1254
1255 Callers will need to POST to this URL with a body of MIME-type
1256 "multipart/form-data".
1257 The body should include a single argument, 'minidump', containing the
1258 binary-formatted minidump to symbolicate.
1259
Chris Masone816e38c2012-05-02 19:22:361260 Args:
Chris Sosa76e44b92013-01-31 20:11:381261 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 19:22:361262 minidump: The binary minidump file to symbolicate.
1263 """
Chris Sosa76e44b92013-01-31 20:11:381264 # Ensure the symbols have been staged.
Dan Shif08fe492016-10-04 21:39:251265 # Try debug.tar.xz first, then debug.tgz
1266 for artifact in (artifact_info.SYMBOLS_ONLY, artifact_info.SYMBOLS):
1267 kwargs['artifacts'] = artifact
1268 dl = _get_downloader(kwargs)
1269
1270 try:
1271 if self.stage(**kwargs) == 'Success':
1272 break
1273 except build_artifact.ArtifactDownloadError:
1274 continue
1275 else:
Gabe Black3b567202015-09-23 21:07:591276 raise DevServerError('Failed to stage symbols for %s' %
1277 dl.DescribeSource())
Chris Sosa76e44b92013-01-31 20:11:381278
Chris Masone816e38c2012-05-02 19:22:361279 to_return = ''
1280 with tempfile.NamedTemporaryFile() as local:
1281 while True:
1282 data = minidump.file.read(8192)
1283 if not data:
1284 break
1285 local.write(data)
Chris Sosa76e44b92013-01-31 20:11:381286
Chris Masone816e38c2012-05-02 19:22:361287 local.flush()
Chris Sosa76e44b92013-01-31 20:11:381288
Gabe Black3b567202015-09-23 21:07:591289 symbols_directory = os.path.join(dl.GetBuildDir(), 'debug', 'breakpad')
Chris Sosa76e44b92013-01-31 20:11:381290
xixuanab744382017-04-27 17:41:271291 # The location of minidump_stackwalk is defined in chromeos-admin.
Chris Sosa76e44b92013-01-31 20:11:381292 stackwalk = subprocess.Popen(
xixuanab744382017-04-27 17:41:271293 ['/usr/local/bin/minidump_stackwalk', local.name, symbols_directory],
Chris Sosa76e44b92013-01-31 20:11:381294 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1295
Chris Masone816e38c2012-05-02 19:22:361296 to_return, error_text = stackwalk.communicate()
1297 if stackwalk.returncode != 0:
1298 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
1299 error_text, stackwalk.returncode))
1300
1301 return to_return
1302
1303 @cherrypy.expose
Don Garrettf84631a2014-01-08 02:21:261304 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 19:31:361305 """Return a string representing the latest build for a given target.
1306
1307 Args:
1308 target: The build target, typically a combination of the board and the
1309 type of build e.g. x86-mario-release.
1310 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
1311 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-08 02:21:261312
Scott Zawalski16954532012-03-20 19:31:361313 Returns:
1314 A string representation of the latest build if one exists, i.e.
1315 R19-1993.0.0-a1-b1480.
1316 An empty string if no latest could be found.
1317 """
Don Garrettf84631a2014-01-08 02:21:261318 if not kwargs:
Scott Zawalski16954532012-03-20 19:31:361319 return _PrintDocStringAsHTML(self.latestbuild)
1320
Don Garrettf84631a2014-01-08 02:21:261321 if 'target' not in kwargs:
Chris Sosa4b951602014-04-10 03:26:071322 raise common_util.DevServerHTTPError(500, 'Error: target= is required!')
Dan Shi61305df2015-10-26 23:52:351323
1324 if _is_android_build_request(kwargs):
1325 branch = kwargs.get('branch', None)
1326 target = kwargs.get('target', None)
1327 if not target or not branch:
1328 raise DevServerError(
xixuan52c2fba2016-05-21 00:02:481329 'Both target and branch must be specified to query for the latest '
1330 'Android build.')
Dan Shi61305df2015-10-26 23:52:351331 return android_build.BuildAccessor.GetLatestBuildID(target, branch)
1332
Scott Zawalski16954532012-03-20 19:31:361333 try:
Gilad Arnoldc65330c2012-09-20 22:17:481334 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-08 02:21:261335 updater.static_dir, kwargs['target'],
1336 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 17:05:011337 except common_util.CommonUtilError as errmsg:
Chris Sosa4b951602014-04-10 03:26:071338 raise common_util.DevServerHTTPError(500, str(errmsg))
Scott Zawalski16954532012-03-20 19:31:361339
1340 @cherrypy.expose
xixuan7efd0002016-04-14 22:34:011341 def list_suite_controls(self, **kwargs):
1342 """Return a list of contents of all known control files.
1343
1344 Example URL:
1345 To List all control files' content:
1346 http://dev-server/list_suite_controls?suite_name=bvt&
1347 build=daisy_spring-release/R29-4279.0.0
1348
1349 Args:
1350 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
1351 suite_name: List the control files belonging to that suite.
1352
1353 Returns:
Dan Shia1cd6522016-04-18 23:07:211354 A dictionary of all control files's path to its content for given suite.
xixuan7efd0002016-04-14 22:34:011355 """
1356 if not kwargs:
1357 return _PrintDocStringAsHTML(self.controlfiles)
1358
1359 if 'build' not in kwargs:
1360 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
1361
1362 if 'suite_name' not in kwargs:
Dan Shia1cd6522016-04-18 23:07:211363 raise common_util.DevServerHTTPError(500,
1364 'Error: suite_name= is required!')
xixuan7efd0002016-04-14 22:34:011365
1366 control_file_list = [
1367 line.rstrip() for line in common_util.GetControlFileListForSuite(
1368 updater.static_dir, kwargs['build'],
1369 kwargs['suite_name']).splitlines()]
1370
Dan Shia1cd6522016-04-18 23:07:211371 control_file_content_dict = {}
xixuan7efd0002016-04-14 22:34:011372 for control_path in control_file_list:
Dan Shia1cd6522016-04-18 23:07:211373 control_file_content_dict[control_path] = (common_util.GetControlFile(
xixuan7efd0002016-04-14 22:34:011374 updater.static_dir, kwargs['build'], control_path))
1375
Dan Shia1cd6522016-04-18 23:07:211376 return json.dumps(control_file_content_dict)
xixuan7efd0002016-04-14 22:34:011377
1378 @cherrypy.expose
Don Garrettf84631a2014-01-08 02:21:261379 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 22:17:281380 """Return a control file or a list of all known control files.
1381
1382 Example URL:
1383 To List all control files:
beepsbd337242013-07-10 05:44:061384 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
1385 To List all control files for, say, the bvt suite:
1386 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 22:17:281387 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 20:12:421388 http://dev-server/controlfiles?board=x86-alex-release&build=R18-1514.0.0&control_path=client/sleeptest/control
Scott Zawalski4647ce62012-01-03 22:17:281389
1390 Args:
Scott Zawalski84a39c92012-01-13 20:12:421391 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 22:17:281392 control_path: If you want the contents of a control file set this
1393 to the path. E.g. client/site_tests/sleeptest/control
1394 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-10 05:44:061395 suite_name: If control_path is not specified but a suite_name is
1396 specified, list the control files belonging to that suite instead of
1397 all control files. The empty string for suite_name will list all control
1398 files for the build.
Don Garrettf84631a2014-01-08 02:21:261399
Scott Zawalski4647ce62012-01-03 22:17:281400 Returns:
1401 Contents of a control file if control_path is provided.
1402 A list of control files if no control_path is provided.
1403 """
Don Garrettf84631a2014-01-08 02:21:261404 if not kwargs:
Scott Zawalski4647ce62012-01-03 22:17:281405 return _PrintDocStringAsHTML(self.controlfiles)
1406
Don Garrettf84631a2014-01-08 02:21:261407 if 'build' not in kwargs:
Chris Sosa4b951602014-04-10 03:26:071408 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 22:17:281409
Don Garrettf84631a2014-01-08 02:21:261410 if 'control_path' not in kwargs:
1411 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-10 05:44:061412 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-08 02:21:261413 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-10 05:44:061414 else:
1415 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-08 02:21:261416 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 22:17:281417 else:
Gilad Arnoldc65330c2012-09-20 22:17:481418 return common_util.GetControlFile(
Don Garrettf84631a2014-01-08 02:21:261419 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-13 02:39:181420
1421 @cherrypy.expose
Simran Basi99e63c02014-05-20 17:39:521422 def xbuddy_translate(self, *args, **kwargs):
Yu-Ju Hong1bdb7a92014-04-10 23:02:111423 """Translates an xBuddy path to a real path to artifact if it exists.
1424
1425 Args:
Simran Basi99e63c02014-05-20 17:39:521426 args: An xbuddy path in the form of {local|remote}/build_id/artifact.
1427 Local searches the devserver's static directory. Remote searches a
1428 Google Storage image archive.
1429
1430 Kwargs:
1431 image_dir: Google Storage image archive to search in if requesting a
1432 remote artifact. If none uses the default bucket.
Yu-Ju Hong1bdb7a92014-04-10 23:02:111433
1434 Returns:
Simran Basi99e63c02014-05-20 17:39:521435 String in the format of build_id/artifact as stored on the local server
1436 or in Google Storage.
Yu-Ju Hong1bdb7a92014-04-10 23:02:111437 """
Simran Basi99e63c02014-05-20 17:39:521438 build_id, filename = self._xbuddy.Translate(
Gabe Black3b567202015-09-23 21:07:591439 args, image_dir=kwargs.get('image_dir'))
Yu-Ju Hong1bdb7a92014-04-10 23:02:111440 response = os.path.join(build_id, filename)
1441 _Log('Path translation requested, returning: %s', response)
1442 return response
1443
1444 @cherrypy.expose
joycheneaf4cfc2013-07-02 15:38:571445 def xbuddy(self, *args, **kwargs):
1446 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 19:13:131447
1448 Args:
joycheneaf4cfc2013-07-02 15:38:571449 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 21:30:301450 components of the path. The path can be understood as
1451 "{local|remote}/build_id/artifact" where build_id is composed of
1452 "board/version."
joycheneaf4cfc2013-07-02 15:38:571453
joychen121fc9b2013-08-02 21:30:301454 The first path element is optional, and can be "remote" or "local"
1455 If local (the default), devserver will not attempt to access Google
1456 Storage, and will only search the static directory for the files.
1457 If remote, devserver will try to obtain the artifact off GS if it's
1458 not found locally.
1459 The board is the familiar board name, optionally suffixed.
1460 The version can be the google storage version number, and may also be
1461 any of a number of xBuddy defined version aliases that will be
1462 translated into the latest built image that fits the description.
1463 Defaults to latest.
1464 The artifact is one of a number of image or artifact aliases used by
1465 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 15:38:571466
1467 Kwargs:
Yu-Ju Hong51495eb2013-12-13 01:08:431468 for_update: {true|false}
1469 if true, pregenerates the update payloads for the image,
1470 and returns the update uri to pass to the
1471 update_engine_client.
joychen3cb228e2013-06-12 19:13:131472 return_dir: {true|false}
1473 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-13 01:08:431474 relative_path: {true|false}
1475 if set to true, returns the relative path to the payload
1476 directory from static_dir.
joychen3cb228e2013-06-12 19:13:131477 Example URL:
joycheneaf4cfc2013-07-02 15:38:571478 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 19:13:131479 or
joycheneaf4cfc2013-07-02 15:38:571480 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 19:13:131481
1482 Returns:
Yu-Ju Hong51495eb2013-12-13 01:08:431483 If |for_update|, returns a redirect to the image or update file
1484 on the devserver. E.g.,
1485 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
1486 chromium-test-image.bin
1487 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
1488 http://host:port/static/x86-generic-release/R26-4000.0.0/
1489 If |relative_path| is true, return a relative path the folder where the
1490 payloads are. E.g.,
1491 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 19:13:131492 """
Chris Sosa75490802013-10-01 00:21:451493 boolean_string = kwargs.get('for_update')
1494 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-13 01:08:431495 boolean_string = kwargs.get('return_dir')
1496 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
1497 boolean_string = kwargs.get('relative_path')
1498 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 21:30:301499
Yu-Ju Hong51495eb2013-12-13 01:08:431500 if return_dir and relative_path:
Chris Sosa4b951602014-04-10 03:26:071501 raise common_util.DevServerHTTPError(
1502 500, 'Cannot specify both return_dir and relative_path')
Chris Sosa75490802013-10-01 00:21:451503
1504 # For updates, we optimize downloading of test images.
1505 file_name = None
1506 build_id = None
1507 if for_update:
1508 try:
Yu-Ju Hong1bdb7a92014-04-10 23:02:111509 build_id = self._xbuddy.StageTestArtifactsForUpdate(args)
Chris Sosa75490802013-10-01 00:21:451510 except build_artifact.ArtifactDownloadError:
1511 build_id = None
1512
1513 if not build_id:
1514 build_id, file_name = self._xbuddy.Get(args)
1515
Yu-Ju Hong51495eb2013-12-13 01:08:431516 if for_update:
1517 _Log('Payload generation triggered by request')
1518 # Forces payload to be in cache and symlinked into build_id dir.
Chris Sosa75490802013-10-01 00:21:451519 updater.GetUpdateForLabel(autoupdate.FORCED_UPDATE, build_id,
1520 image_name=file_name)
Yu-Ju Hong51495eb2013-12-13 01:08:431521
1522 response = None
1523 if return_dir:
1524 response = os.path.join(cherrypy.request.base, 'static', build_id)
1525 _Log('Directory requested, returning: %s', response)
1526 elif relative_path:
1527 response = build_id
1528 _Log('Relative path requested, returning: %s', response)
1529 elif for_update:
1530 response = os.path.join(cherrypy.request.base, 'update', build_id)
1531 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 19:13:131532 else:
Yu-Ju Hong51495eb2013-12-13 01:08:431533 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 21:30:301534 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-13 01:08:431535 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 21:30:301536 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 19:13:131537
Yu-Ju Hong51495eb2013-12-13 01:08:431538 return response
1539
joychen3cb228e2013-06-12 19:13:131540 @cherrypy.expose
1541 def xbuddy_list(self):
1542 """Lists the currently available images & time since last access.
1543
Gilad Arnold452fd272014-02-04 19:09:281544 Returns:
1545 A string representation of a list of tuples [(build_id, time since last
1546 access),...]
joychen3cb228e2013-06-12 19:13:131547 """
1548 return self._xbuddy.List()
1549
1550 @cherrypy.expose
1551 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 19:09:281552 """Returns the number of images cached by xBuddy."""
joychen3cb228e2013-06-12 19:13:131553 return self._xbuddy.Capacity()
1554
1555 @cherrypy.expose
Chris Sosa7c931362010-10-12 02:49:011556 def index(self):
Gilad Arnoldf8f769f2012-09-24 15:43:011557 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 15:43:011558 return ('Welcome to the Dev Server!<br>\n'
1559 '<br>\n'
1560 'Here are the available methods, click for documentation:<br>\n'
1561 '<br>\n'
1562 '%s' %
1563 '<br>\n'.join(
1564 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 18:52:381565 for name in _FindExposedMethods(
1566 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 15:43:011567
1568 @cherrypy.expose
1569 def doc(self, *args):
1570 """Shows the documentation for available methods / URLs.
1571
1572 Example:
1573 http://myhost/doc/update
1574 """
Gilad Arnoldd5ebaaa2012-10-02 18:52:381575 name = '/'.join(args)
1576 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 15:43:011577 if not method:
1578 raise DevServerError("No exposed method named `%s'" % name)
1579 if not method.__doc__:
1580 raise DevServerError("No documentation for exposed method `%s'" % name)
1581 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-12 02:49:011582
Dale Curtisc9aaf3a2011-08-09 22:47:401583 @cherrypy.expose
Chris Sosa7c931362010-10-12 02:49:011584 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 15:43:011585 """Handles an update check from a Chrome OS client.
1586
1587 The HTTP request should contain the standard Omaha-style XML blob. The URL
1588 line may contain an additional intermediate path to the update payload.
1589
joychen121fc9b2013-08-02 21:30:301590 This request can be handled in one of 4 ways, depending on the devsever
1591 settings and intermediate path.
joychenb0dfe552013-07-30 17:02:061592
joychen121fc9b2013-08-02 21:30:301593 1. No intermediate path
1594 If no intermediate path is given, the default behavior is to generate an
1595 update payload from the latest test image locally built for the board
1596 specified in the xml. Devserver serves the generated payload.
1597
1598 2. Path explicitly invokes XBuddy
1599 If there is a path given, it can explicitly invoke xbuddy by prefixing it
1600 with 'xbuddy'. This path is then used to acquire an image binary for the
1601 devserver to generate an update payload from. Devserver then serves this
1602 payload.
1603
1604 3. Path is left for the devserver to interpret.
1605 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
1606 to generate a payload from the test image in that directory and serve it.
1607
1608 4. The devserver is in a 'forced' mode. TO BE DEPRECATED
1609 This comes from the usage of --forced_payload or --image when starting the
1610 devserver. No matter what path (or no path) gets passed in, devserver will
1611 serve the update payload (--forced_payload) or generate an update payload
1612 from the image (--image).
1613
1614 Examples:
1615 1. No intermediate path
1616 update_engine_client --omaha_url=http://myhost/update
1617 This generates an update payload from the latest test image locally built
1618 for the board specified in the xml.
1619
1620 2. Explicitly invoke xbuddy
1621 update_engine_client --omaha_url=
1622 http://myhost/update/xbuddy/remote/board/version/dev
1623 This would go to GS to download the dev image for the board, from which
1624 the devserver would generate a payload to serve.
1625
1626 3. Give a path for devserver to interpret
1627 update_engine_client --omaha_url=http://myhost/update/some/random/path
1628 This would attempt, in order to:
1629 a) Generate an update from a test image binary if found in
1630 static_dir/some/random/path.
1631 b) Serve an update payload found in static_dir/some/random/path.
1632 c) Hope that some/random/path takes the form "board/version" and
1633 and attempt to download an update payload for that board/version
1634 from GS.
Gilad Arnoldf8f769f2012-09-24 15:43:011635 """
joychen121fc9b2013-08-02 21:30:301636 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 21:47:021637 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-12 02:49:011638 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-12 02:49:011639
joychen121fc9b2013-08-02 21:30:301640 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 22:46:221641
Dan Shiafd0e492015-05-27 21:23:511642 @require_psutil()
1643 def _get_io_stats(self):
1644 """Get the IO stats as a dictionary.
1645
Gabe Black3b567202015-09-23 21:07:591646 Returns:
1647 A dictionary of IO stats collected by psutil.
Dan Shiafd0e492015-05-27 21:23:511648 """
1649 return {'disk_read_bytes_per_second': self.disk_read_bytes_per_sec,
1650 'disk_write_bytes_per_second': self.disk_write_bytes_per_sec,
1651 'disk_total_bytes_per_second': (self.disk_read_bytes_per_sec +
1652 self.disk_write_bytes_per_sec),
1653 'network_sent_bytes_per_second': self.network_sent_bytes_per_sec,
1654 'network_recv_bytes_per_second': self.network_recv_bytes_per_sec,
1655 'network_total_bytes_per_second': (self.network_sent_bytes_per_sec +
1656 self.network_recv_bytes_per_sec),
1657 'cpu_percent': psutil.cpu_percent(),}
1658
Dan Shi7247f9c2016-06-01 16:19:091659
1660 def _get_process_count(self, process_cmd_pattern):
1661 """Get the count of processes that match the given command pattern.
1662
1663 Args:
1664 process_cmd_pattern: The regex pattern of process command to match.
1665
1666 Returns:
1667 The count of processes that match the given command pattern.
1668 """
1669 try:
xixuanac89ce82016-12-01 00:48:201670 # Use Popen instead of check_output since the latter cannot run with old
1671 # python version (less than 2.7)
1672 proc = subprocess.Popen(
1673 'pgrep -fc "%s"' % process_cmd_pattern,
1674 stdout=subprocess.PIPE,
1675 stderr=subprocess.PIPE,
1676 shell=True)
1677 cmd_output, cmd_error = proc.communicate()
1678 if cmd_error:
1679 _Log('Error happened when getting process count: %s' % cmd_error)
1680
1681 return int(cmd_output)
Dan Shi7247f9c2016-06-01 16:19:091682 except subprocess.CalledProcessError:
1683 return 0
1684
1685
Dan Shif5ce2de2013-04-25 23:06:321686 @cherrypy.expose
1687 def check_health(self):
1688 """Collect the health status of devserver to see if it's ready for staging.
1689
Gilad Arnold452fd272014-02-04 19:09:281690 Returns:
1691 A JSON dictionary containing all or some of the following fields:
1692 free_disk (int): free disk space in GB
1693 staging_thread_count (int): number of devserver threads currently staging
1694 an image
Dan Shi7247f9c2016-06-01 16:19:091695 apache_client_count (int): count of Apache processes.
1696 telemetry_test_count (int): count of telemetry tests.
1697 gsutil_count (int): count of gsutil processes.
Dan Shif5ce2de2013-04-25 23:06:321698 """
1699 # Get free disk space.
1700 stat = os.statvfs(updater.static_dir)
1701 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
Dan Shi7247f9c2016-06-01 16:19:091702 apache_client_count = self._get_process_count('apache')
1703 telemetry_test_count = self._get_process_count('python.*telemetry')
1704 gsutil_count = self._get_process_count('gsutil')
xixuan447ad9d2017-02-28 22:46:201705 au_process_count = len(cros_update_progress.GetAllRunningAUProcess())
Dan Shif5ce2de2013-04-25 23:06:321706
Dan Shiafd0e492015-05-27 21:23:511707 health_data = {
Dan Shif5ce2de2013-04-25 23:06:321708 'free_disk': free_disk,
Dan Shid76e6bb2016-01-29 06:28:511709 'staging_thread_count': DevServerRoot._staging_thread_count,
1710 'apache_client_count': apache_client_count,
Dan Shi7247f9c2016-06-01 16:19:091711 'telemetry_test_count': telemetry_test_count,
xixuan447ad9d2017-02-28 22:46:201712 'gsutil_count': gsutil_count,
1713 'au_process_count': au_process_count,
1714 }
Dan Shiafd0e492015-05-27 21:23:511715 health_data.update(self._get_io_stats() or {})
1716
1717 return json.dumps(health_data)
Dan Shif5ce2de2013-04-25 23:06:321718
1719
Chris Sosadbc20082012-12-10 21:39:111720def _CleanCache(cache_dir, wipe):
1721 """Wipes any excess cached items in the cache_dir.
1722
1723 Args:
1724 cache_dir: the directory we are wiping from.
1725 wipe: If True, wipe all the contents -- not just the excess.
1726 """
1727 if wipe:
1728 # Clear the cache and exit on error.
1729 cmd = 'rm -rf %s/*' % cache_dir
1730 if os.system(cmd) != 0:
1731 _Log('Failed to clear the cache with %s' % cmd)
1732 sys.exit(1)
1733 else:
1734 # Clear all but the last N cached updates
1735 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
1736 (cache_dir, CACHED_ENTRIES))
1737 if os.system(cmd) != 0:
1738 _Log('Failed to clean up old delta cache files with %s' % cmd)
1739 sys.exit(1)
1740
1741
Chris Sosa3ae4dc12013-03-29 18:47:001742def _AddTestingOptions(parser):
1743 group = optparse.OptionGroup(
1744 parser, 'Advanced Testing Options', 'These are used by test scripts and '
1745 'developers writing integration tests utilizing the devserver. They are '
1746 'not intended to be really used outside the scope of someone '
1747 'knowledgable about the test.')
1748 group.add_option('--exit',
1749 action='store_true',
1750 help='do not start the server (yet pregenerate/clear cache)')
1751 group.add_option('--host_log',
1752 action='store_true', default=False,
1753 help='record history of host update events (/api/hostlog)')
1754 group.add_option('--max_updates',
Gabe Black3b567202015-09-23 21:07:591755 metavar='NUM', default=-1, type='int',
Chris Sosa3ae4dc12013-03-29 18:47:001756 help='maximum number of update checks handled positively '
1757 '(default: unlimited)')
1758 group.add_option('--private_key',
1759 metavar='PATH', default=None,
1760 help='path to the private key in pem format. If this is set '
1761 'the devserver will generate update payloads that are '
1762 'signed with this key.')
David Zeuthen52ccd012013-10-31 19:58:261763 group.add_option('--private_key_for_metadata_hash_signature',
1764 metavar='PATH', default=None,
1765 help='path to the private key in pem format. If this is set '
1766 'the devserver will sign the metadata hash with the given '
1767 'key and transmit in the Omaha-style XML response.')
1768 group.add_option('--public_key',
1769 metavar='PATH', default=None,
1770 help='path to the public key in pem format. If this is set '
1771 'the devserver will transmit a base64 encoded version of '
1772 'the content in the Omaha-style XML response.')
Chris Sosa3ae4dc12013-03-29 18:47:001773 group.add_option('--proxy_port',
1774 metavar='PORT', default=None, type='int',
1775 help='port to have the client connect to -- basically the '
1776 'devserver lies to the update to tell it to get the payload '
1777 'from a different port that will proxy the request back to '
1778 'the devserver. The proxy must be managed outside the '
1779 'devserver.')
1780 group.add_option('--remote_payload',
1781 action='store_true', default=False,
Chris Sosa4b951602014-04-10 03:26:071782 help='Payload is being served from a remote machine. With '
1783 'this setting enabled, this devserver instance serves as '
1784 'just an Omaha server instance. In this mode, the '
1785 'devserver enforces a few extra components of the Omaha '
Chris Sosafc715442014-04-10 03:45:231786 'protocol, such as hardware class, being sent.')
Chris Sosa3ae4dc12013-03-29 18:47:001787 group.add_option('-u', '--urlbase',
1788 metavar='URL',
Gabe Black3b567202015-09-23 21:07:591789 help='base URL for update images, other than the '
1790 'devserver. Use in conjunction with remote_payload.')
Chris Sosa3ae4dc12013-03-29 18:47:001791 parser.add_option_group(group)
1792
1793
1794def _AddUpdateOptions(parser):
1795 group = optparse.OptionGroup(
1796 parser, 'Autoupdate Options', 'These options can be used to change '
1797 'how the devserver either generates or serve update payloads. Please '
1798 'note that all of these option affect how a payload is generated and so '
1799 'do not work in archive-only mode.')
1800 group.add_option('--board',
1801 help='By default the devserver will create an update '
1802 'payload from the latest image built for the board '
1803 'a device that is requesting an update has. When we '
1804 'pre-generate an update (see below) and we do not specify '
1805 'another update_type option like image or payload, the '
1806 'devserver needs to know the board to generate the latest '
1807 'image for. This is that board.')
1808 group.add_option('--critical_update',
1809 action='store_true', default=False,
1810 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 18:47:001811 group.add_option('--image',
1812 metavar='FILE',
1813 help='Generate and serve an update using this image to any '
1814 'device that requests an update.')
Chris Sosa3ae4dc12013-03-29 18:47:001815 group.add_option('--payload',
1816 metavar='PATH',
1817 help='use the update payload from specified directory '
1818 '(update.gz).')
1819 group.add_option('-p', '--pregenerate_update',
1820 action='store_true', default=False,
1821 help='pre-generate the update payload before accepting '
1822 'update requests. Useful to help debug payload generation '
1823 'issues quickly. Also if an update payload will take a '
1824 'long time to generate, a client may timeout if you do not'
1825 'pregenerate the update.')
1826 group.add_option('--src_image',
1827 metavar='PATH', default='',
1828 help='If specified, delta updates will be generated using '
1829 'this image as the source image. Delta updates are when '
1830 'you are updating from a "source image" to a another '
1831 'image.')
1832 parser.add_option_group(group)
1833
1834
1835def _AddProductionOptions(parser):
1836 group = optparse.OptionGroup(
1837 parser, 'Advanced Server Options', 'These options can be used to changed '
1838 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 18:47:001839 group.add_option('--clear_cache',
1840 action='store_true', default=False,
1841 help='At startup, removes all cached entries from the'
1842 'devserver\'s cache.')
1843 group.add_option('--logfile',
1844 metavar='PATH',
1845 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 20:24:551846 group.add_option('--pidfile',
1847 metavar='PATH',
1848 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 19:04:131849 group.add_option('--portfile',
1850 metavar='PATH',
1851 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 18:47:001852 group.add_option('--production',
1853 action='store_true', default=False,
1854 help='have the devserver use production values when '
1855 'starting up. This includes using more threads and '
1856 'performing less logging.')
1857 parser.add_option_group(group)
1858
1859
Paul Hobbsef4e0702016-06-28 00:01:421860def MakeLogHandler(logfile):
J. Richard Barnette3d977b82013-04-23 18:05:191861 """Create a LogHandler instance used to log all messages."""
1862 hdlr_cls = handlers.TimedRotatingFileHandler
1863 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
xixuan3d48bff2017-01-31 03:00:091864 interval=_LOG_ROTATION_INTERVAL,
J. Richard Barnette3d977b82013-04-23 18:05:191865 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 20:24:551866 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 18:05:191867 return hdlr
1868
1869
Chris Sosacde6bf42012-06-01 01:36:391870def main():
Chris Sosa3ae4dc12013-03-29 18:47:001871 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 21:47:021872 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 23:39:341873
1874 # get directory that the devserver is run from
1875 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 16:17:231876 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 23:39:341877 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 17:09:421878 metavar='PATH',
joychen84d13772013-08-06 16:17:231879 default=default_static_dir,
joychened64b222013-06-21 23:39:341880 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 17:09:421881 parser.add_option('--port',
1882 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 21:13:281883 help=('port for the dev server to use; if zero, binds to '
1884 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 17:09:421885 parser.add_option('-t', '--test_image',
1886 action='store_true',
joychen121fc9b2013-08-02 21:30:301887 help='Deprecated.')
joychen5260b9a2013-07-16 21:48:011888 parser.add_option('-x', '--xbuddy_manage_builds',
1889 action='store_true',
1890 default=False,
1891 help='If set, allow xbuddy to manage images in'
1892 'build/images.')
Dan Shi72b16132015-10-08 19:10:331893 parser.add_option('-a', '--android_build_credential',
1894 default=None,
1895 help='Path to a json file which contains the credential '
1896 'needed to access Android builds.')
Chris Sosa3ae4dc12013-03-29 18:47:001897 _AddProductionOptions(parser)
1898 _AddUpdateOptions(parser)
1899 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-12 02:49:011900 (options, _) = parser.parse_args()
[email protected]21a5ca32009-11-04 18:23:231901
J. Richard Barnette3d977b82013-04-23 18:05:191902 # Handle options that must be set globally in cherrypy. Do this
1903 # work up front, because calls to _Log() below depend on this
1904 # initialization.
1905 if options.production:
1906 cherrypy.config.update({'environment': 'production'})
1907 if not options.logfile:
1908 cherrypy.config.update({'log.screen': True})
1909 else:
1910 cherrypy.config.update({'log.error_file': '',
1911 'log.access_file': ''})
Paul Hobbsef4e0702016-06-28 00:01:421912 hdlr = MakeLogHandler(options.logfile)
J. Richard Barnette3d977b82013-04-23 18:05:191913 # Pylint can't seem to process these two calls properly
1914 # pylint: disable=E1101
1915 cherrypy.log.access_log.addHandler(hdlr)
1916 cherrypy.log.error_log.addHandler(hdlr)
1917 # pylint: enable=E1101
1918
joychened64b222013-06-21 23:39:341919 # set static_dir, from which everything will be served
joychen84d13772013-08-06 16:17:231920 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 22:46:221921
joychened64b222013-06-21 23:39:341922 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 18:05:191923 # If our devserver is only supposed to serve payloads, we shouldn't be
1924 # mucking with the cache at all. If the devserver hadn't previously
1925 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 18:14:071926 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 21:39:111927 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 20:12:171928 else:
1929 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-17 01:36:141930
Chris Sosadbc20082012-12-10 21:39:111931 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 23:39:341932 _Log('Serving from %s' % options.static_dir)
[email protected]21a5ca32009-11-04 18:23:231933
joychen121fc9b2013-08-02 21:30:301934 _xbuddy = xbuddy.XBuddy(options.xbuddy_manage_builds,
1935 options.board,
joychen121fc9b2013-08-02 21:30:301936 static_dir=options.static_dir)
Chris Sosa75490802013-10-01 00:21:451937 if options.clear_cache and options.xbuddy_manage_builds:
1938 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 21:30:301939
Chris Sosa6a3697f2013-01-30 00:44:431940 # We allow global use here to share with cherrypy classes.
1941 # pylint: disable=W0603
Chris Sosacde6bf42012-06-01 01:36:391942 global updater
Andrew de los Reyes52620802010-04-12 20:40:071943 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 21:30:301944 _xbuddy,
joychened64b222013-06-21 23:39:341945 static_dir=options.static_dir,
Andrew de los Reyes52620802010-04-12 20:40:071946 urlbase=options.urlbase,
Chris Sosa5d342a22010-09-28 23:54:411947 forced_image=options.image,
Gilad Arnold0c9c8602012-10-03 06:58:581948 payload_path=options.payload,
Don Garrett0ad09372010-12-07 00:20:301949 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-29 06:42:371950 src_image=options.src_image,
Chris Sosa08d55a22011-01-20 00:08:021951 board=options.board,
Chris Sosa0f1ec842011-02-15 00:33:221952 copy_to_static_root=not options.exit,
1953 private_key=options.private_key,
Gabe Black3b567202015-09-23 21:07:591954 private_key_for_metadata_hash_signature=(
1955 options.private_key_for_metadata_hash_signature),
David Zeuthen52ccd012013-10-31 19:58:261956 public_key=options.public_key,
Satoru Takabayashid733cbe2011-11-15 17:36:321957 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-03 06:58:581958 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 17:32:441959 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 19:52:231960 host_log=options.host_log,
Chris Sosa0f1ec842011-02-15 00:33:221961 )
Chris Sosa7c931362010-10-12 02:49:011962
Chris Sosa6a3697f2013-01-30 00:44:431963 if options.pregenerate_update:
1964 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 22:46:221965
J. Richard Barnette3d977b82013-04-23 18:05:191966 if options.exit:
1967 return
Chris Sosa2f1c41e2012-07-10 21:32:331968
joychen3cb228e2013-06-12 19:13:131969 dev_server = DevServerRoot(_xbuddy)
1970
Gilad Arnold11fbef42014-02-10 19:04:131971 # Patch CherryPy to support binding to any available port (--port=0).
1972 cherrypy_ext.ZeroPortPatcher.DoPatch(cherrypy)
1973
Chris Sosa855b8932013-08-21 20:24:551974 if options.pidfile:
1975 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1976
Gilad Arnold11fbef42014-02-10 19:04:131977 if options.portfile:
1978 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1979
Dan Shiafd5c6c2016-01-07 18:27:031980 if (options.android_build_credential and
1981 os.path.exists(options.android_build_credential)):
1982 try:
1983 with open(options.android_build_credential) as f:
1984 android_build.BuildAccessor.credential_info = json.load(f)
1985 except ValueError as e:
1986 _Log('Failed to load the android build credential: %s. Error: %s.' %
1987 (options.android_build_credential, e))
joychen3cb228e2013-06-12 19:13:131988 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-06-01 01:36:391989
1990
1991if __name__ == '__main__':
1992 main()