blob: 81cce8ed99a06fe9ddf10a988768a14ab24be396 [file] [log] [blame]
David Riley2fcb0122017-11-02 11:25:39 -07001#!/usr/bin/env python2
Chris Sosa7c931362010-10-11 19:49:01 -07002
Chris Sosa781ba6d2012-04-11 12:44:43 -07003# Copyright (c) 2009-2012 The Chromium OS Authors. All rights reserved.
rtc@google.comded22402009-10-26 22:36:21 +00004# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
Chris Sosa3ae4dc12013-03-29 11:47:00 -07007"""Chromium OS development server that can be used for all forms of update.
8
9This devserver can be used to perform system-wide autoupdate and update
10of specific portage packages on devices running Chromium OS derived operating
11systems. It mainly operates in two modes:
12
131) archive mode: In this mode, the devserver is configured to stage and
14serve artifacts from Google Storage using the credentials provided to it before
15it is run. The easiest way to understand this is that the devserver is
16functioning as a local cache for artifacts produced and uploaded by build
17servers. Users of this form of devserver can either download the artifacts
18from the devservers static directory OR use the update RPC to perform a
19system-wide autoupdate. Archive mode is always active.
20
212) artifact-generation mode: in this mode, the devserver will attempt to
22generate update payloads and build artifacts when requested. This mode only
23works in the Chromium OS chroot as it uses build tools only present in the
24chroot (emerge, cros_generate_update_payload, etc.). By default, when a device
25requests an update from this form of devserver, the devserver will attempt to
26discover if a more recent build of the board has been built by the developer
27and generate a payload that the requested system can autoupdate to. In addition,
28it accepts gmerge requests from devices that will stage the newest version of
joychen84d13772013-08-06 09:17:23 -070029a particular package from a developer's chroot onto a requesting device.
Chris Sosa3ae4dc12013-03-29 11:47:00 -070030
31For example:
32gmerge gmerge -d <devserver_url>
33
34devserver will see if a newer package of gmerge is available. If gmerge is
35cros_work'd on, it will re-build gmerge. After this, gmerge will install that
36version of gmerge that the devserver just created/found.
37
38For autoupdates, there are many more advanced options that can help specify
39how to update and which payload to give to a requester.
40"""
41
Gabe Black3b567202015-09-23 14:07:59 -070042from __future__ import print_function
Chris Sosa7c931362010-10-11 19:49:01 -070043
Gilad Arnold55a2a372012-10-02 09:46:32 -070044import json
David Riley2fcb0122017-11-02 11:25:39 -070045import optparse # pylint: disable=deprecated-module
rtc@google.comded22402009-10-26 22:36:21 +000046import os
Scott Zawalski4647ce62012-01-03 17:17:28 -050047import re
Simran Basi4baad082013-02-14 13:39:18 -080048import shutil
xixuan52c2fba2016-05-20 17:02:48 -070049import signal
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -080050import socket
Chris Masone816e38c2012-05-02 12:22:36 -070051import subprocess
J. Richard Barnette3d977b82013-04-23 11:05:19 -070052import sys
Chris Masone816e38c2012-05-02 12:22:36 -070053import tempfile
Dan Shi59ae7092013-06-04 14:37:27 -070054import threading
Dan Shiafd0e492015-05-27 14:23:51 -070055import time
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -070056import types
J. Richard Barnette3d977b82013-04-23 11:05:19 -070057from logging import handlers
58
59import cherrypy
David Riley2fcb0122017-11-02 11:25:39 -070060# pylint: disable=no-name-in-module
Chris Sosa855b8932013-08-21 13:24:55 -070061from cherrypy import _cplogging as cplogging
David Riley2fcb0122017-11-02 11:25:39 -070062from cherrypy.process import plugins # pylint: disable=import-error
63# pylint: enable=no-name-in-module
rtc@google.comded22402009-10-26 22:36:21 +000064
Richard Barnettedf35c322017-08-18 17:02:13 -070065# 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 15:46:22 -070070import autoupdate
Dan Shi2f136862016-02-11 15:38:38 -080071import artifact_info
Chris Sosa75490802013-09-30 17:21:45 -070072import build_artifact
Gilad Arnold11fbef42014-02-10 11:04:13 -080073import cherrypy_ext
Gilad Arnoldc65330c2012-09-20 15:17:48 -070074import common_util
Simran Basief83d6a2014-08-28 14:32:01 -070075import devserver_constants
Chris Sosa47a7d4e2012-03-28 11:26:55 -070076import downloader
Chris Sosa7cd23202013-10-15 17:22:57 -070077import gsutil_util
Gilad Arnoldc65330c2012-09-20 15:17:48 -070078import log_util
joychen3cb228e2013-06-12 12:13:13 -070079import xbuddy
Gilad Arnoldc65330c2012-09-20 15:17:48 -070080
Gilad Arnoldc65330c2012-09-20 15:17:48 -070081# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080082def _Log(message, *args):
83 return log_util.LogWithTag('DEVSERVER', message, *args)
Chris Sosa0356d3b2010-09-16 15:46:22 -070084
Dan Shiafd0e492015-05-27 14:23:51 -070085try:
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-08 20:51:13 -070095except 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-11-30 16:48:20 -0800103# Use try-except to skip unneccesary import for simple use case, eg. running
104# devserver on host.
105try:
106 import cros_update
xixuanac89ce82016-11-30 16:48:20 -0800107except ImportError as e:
108 _Log('cros_update cannot be imported: %r', e)
109 cros_update = None
xixuana4f4e712017-05-08 15:17:54 -0700110
111try:
112 import cros_update_progress
113except ImportError as e:
114 _Log('cros_update_progress cannot be imported: %r', e)
xixuanac89ce82016-11-30 16:48:20 -0800115 cros_update_progress = None
116
xixuanac89ce82016-11-30 16:48:20 -0800117try:
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 12:10:33 -0700123try:
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 12:10:33 -0700130 android_build = None
Frank Farzan40160872011-12-12 18:39:18 -0800131
Chris Sosa417e55d2011-01-25 16:40:48 -0800132CACHED_ENTRIES = 12
Don Garrettf90edf02010-11-16 17:36:14 -0800133
Simran Basi4baad082013-02-14 13:39:18 -0800134TELEMETRY_FOLDER = 'telemetry_src'
135TELEMETRY_DEPS = ['dep-telemetry_dep.tar.bz2',
136 'dep-page_cycler_dep.tar.bz2',
Simran Basi0d078682013-03-22 16:40:04 -0700137 'dep-chrome_test.tar.bz2',
138 'dep-perf_data_dep.tar.bz2']
Simran Basi4baad082013-02-14 13:39:18 -0800139
Chris Sosa0356d3b2010-09-16 15:46:22 -0700140# Sets up global to share between classes.
rtc@google.com21a5ca32009-11-04 18:23:23 +0000141updater = None
rtc@google.comded22402009-10-26 22:36:21 +0000142
xixuan3d48bff2017-01-30 19:00:09 -0800143# 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 11:05:19 -0700146#
xixuan3d48bff2017-01-30 19:00:09 -0800147# For more, see the documentation in standard python library for
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700148# logging.handlers.TimedRotatingFileHandler
xixuan3d48bff2017-01-30 19:00:09 -0800149_LOG_ROTATION_TIME = 'H'
150_LOG_ROTATION_INTERVAL = 12 # hours
151_LOG_ROTATION_BACKUP = 28 # backup counts
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700152
Dan Shiafd0e492015-05-27 14:23:51 -0700153# Number of seconds between the collection of disk and network IO counters.
154STATS_INTERVAL = 10.0
Frank Farzan40160872011-12-12 18:39:18 -0800155
xixuan52c2fba2016-05-20 17:02:48 -0700156# Auto-update parameters
157
158# Error msg for missing key in CrOS auto-update.
Xixuan Wu32af9f12017-11-13 14:11:44 -0800159KEY_ERROR_MSG = 'Key Error in RPC: %s= is required'
xixuan52c2fba2016-05-20 17:02:48 -0700160
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 11:04:50 -0700165class DevServerError(Exception):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700166 """Exception class used by this module."""
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700167
168
Dan Shiafd0e492015-05-27 14:23:51 -0700169def require_psutil():
Gabe Black3b567202015-09-23 14:07:59 -0700170 """Decorator for functions require psutil to run."""
Dan Shiafd0e492015-05-27 14:23:51 -0700171 def deco_require_psutil(func):
172 """Wrapper of the decorator function.
173
Gabe Black3b567202015-09-23 14:07:59 -0700174 Args:
175 func: function to be called.
Dan Shiafd0e492015-05-27 14:23:51 -0700176 """
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 14:07:59 -0700182 Args:
183 *args: arguments for function to be called.
184 **kwargs: keyword arguments for function to be called.
Dan Shiafd0e492015-05-27 14:23:51 -0700185 """
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 14:07:59 -0700195def _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 16:52:35 -0700244def _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 14:07:59 -0700261def _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:
273 dl = downloader.LocalDownloader(updater.static_dir, local_path)
274
Dan Shi61305df2015-10-26 16:52:35 -0700275 if not _is_android_build_request(kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700276 archive_url = kwargs.get('archive_url')
277 if not archive_url and not local_path:
278 raise DevServerError('Requires archive_url or local_path to be '
279 'specified.')
280 if archive_url and local_path:
281 raise DevServerError('archive_url and local_path can not both be '
282 'specified.')
283 if not dl:
284 archive_url = _canonicalize_archive_url(archive_url)
285 dl = downloader.GoogleStorageDownloader(updater.static_dir, archive_url)
286 elif not dl:
287 target = kwargs.get('target', None)
Dan Shi72b16132015-10-08 12:10:33 -0700288 branch = kwargs.get('branch', None)
Dan Shi61305df2015-10-26 16:52:35 -0700289 build_id = kwargs.get('build_id', None)
290 if not target or not branch or not build_id:
Dan Shi72b16132015-10-08 12:10:33 -0700291 raise DevServerError(
Dan Shi61305df2015-10-26 16:52:35 -0700292 'target, branch, build ID must all be specified for downloading '
293 'Android build.')
Dan Shi72b16132015-10-08 12:10:33 -0700294 dl = downloader.AndroidBuildDownloader(updater.static_dir, branch, build_id,
295 target)
Gabe Black3b567202015-09-23 14:07:59 -0700296
297 return dl
298
299
300def _get_downloader_and_factory(kwargs):
301 """Returns the downloader and artifact factory based on passed in arguments.
302
303 Args:
304 kwargs: Keyword arguments for the request.
305 """
306 artifacts, files = _get_artifacts(kwargs)
307 dl = _get_downloader(kwargs)
308
309 if (isinstance(dl, downloader.GoogleStorageDownloader) or
310 isinstance(dl, downloader.LocalDownloader)):
311 factory_class = build_artifact.ChromeOSArtifactFactory
Dan Shi72b16132015-10-08 12:10:33 -0700312 elif isinstance(dl, downloader.AndroidBuildDownloader):
Gabe Black3b567202015-09-23 14:07:59 -0700313 factory_class = build_artifact.AndroidArtifactFactory
314 else:
315 raise DevServerError('Unrecognized value for downloader type: %s' %
316 type(dl))
317
318 factory = factory_class(dl.GetBuildDir(), artifacts, files, dl.GetBuild())
319
320 return dl, factory
321
322
Scott Zawalski4647ce62012-01-03 17:17:28 -0500323def _LeadingWhiteSpaceCount(string):
324 """Count the amount of leading whitespace in a string.
325
326 Args:
327 string: The string to count leading whitespace in.
Don Garrettf84631a2014-01-07 18:21:26 -0800328
Scott Zawalski4647ce62012-01-03 17:17:28 -0500329 Returns:
330 number of white space chars before characters start.
331 """
Gabe Black3b567202015-09-23 14:07:59 -0700332 matched = re.match(r'^\s+', string)
Scott Zawalski4647ce62012-01-03 17:17:28 -0500333 if matched:
334 return len(matched.group())
335
336 return 0
337
338
339def _PrintDocStringAsHTML(func):
340 """Make a functions docstring somewhat HTML style.
341
342 Args:
343 func: The function to return the docstring from.
Don Garrettf84631a2014-01-07 18:21:26 -0800344
Scott Zawalski4647ce62012-01-03 17:17:28 -0500345 Returns:
346 A string that is somewhat formated for a web browser.
347 """
348 # TODO(scottz): Make this parse Args/Returns in a prettier way.
349 # Arguments could be bolded and indented etc.
350 html_doc = []
351 for line in func.__doc__.splitlines():
352 leading_space = _LeadingWhiteSpaceCount(line)
353 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700354 line = '&nbsp;' * leading_space + line
Scott Zawalski4647ce62012-01-03 17:17:28 -0500355
356 html_doc.append('<BR>%s' % line)
357
358 return '\n'.join(html_doc)
359
360
Simran Basief83d6a2014-08-28 14:32:01 -0700361def _GetUpdateTimestampHandler(static_dir):
362 """Returns a handler to update directory staged.timestamp.
363
364 This handler resets the stage.timestamp whenever static content is accessed.
365
366 Args:
367 static_dir: Directory from which static content is being staged.
368
369 Returns:
370 A cherrypy handler to update the timestamp of accessed content.
371 """
372 def UpdateTimestampHandler():
373 if not '404' in cherrypy.response.status:
374 build_match = re.match(devserver_constants.STAGED_BUILD_REGEX,
375 cherrypy.request.path_info)
376 if build_match:
377 build_dir = os.path.join(static_dir, build_match.group('build'))
378 downloader.Downloader.TouchTimestampForStaged(build_dir)
379 return UpdateTimestampHandler
380
381
Chris Sosa7c931362010-10-11 19:49:01 -0700382def _GetConfig(options):
383 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800384
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800385 socket_host = '::'
Yu-Ju Hongc8d4af32013-11-12 15:14:26 -0800386 # Fall back to IPv4 when python is not configured with IPv6.
387 if not socket.has_ipv6:
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800388 socket_host = '0.0.0.0'
389
Simran Basief83d6a2014-08-28 14:32:01 -0700390 # Adds the UpdateTimestampHandler to cherrypy's tools. This tools executes
391 # on the on_end_resource hook. This hook is called once processing is
392 # complete and the response is ready to be returned.
393 cherrypy.tools.update_timestamp = cherrypy.Tool(
394 'on_end_resource', _GetUpdateTimestampHandler(options.static_dir))
395
David Riley2fcb0122017-11-02 11:25:39 -0700396 base_config = {
397 'global': {
398 'server.log_request_headers': True,
399 'server.protocol_version': 'HTTP/1.1',
400 'server.socket_host': socket_host,
401 'server.socket_port': int(options.port),
402 'response.timeout': 6000,
403 'request.show_tracebacks': True,
404 'server.socket_timeout': 60,
405 'server.thread_pool': 2,
406 'engine.autoreload.on': False,
407 },
408 '/api': {
409 # Gets rid of cherrypy parsing post file for args.
410 'request.process_request_body': False,
411 },
412 '/build': {
413 'response.timeout': 100000,
414 },
415 '/update': {
416 # Gets rid of cherrypy parsing post file for args.
417 'request.process_request_body': False,
418 'response.timeout': 10000,
419 },
420 # Sets up the static dir for file hosting.
421 '/static': {
422 'tools.staticdir.dir': options.static_dir,
423 'tools.staticdir.on': True,
424 'response.timeout': 10000,
425 'tools.update_timestamp.on': True,
426 },
427 }
Chris Sosa5f118ef2012-07-12 11:37:50 -0700428 if options.production:
Alex Miller93beca52013-07-30 19:25:09 -0700429 base_config['global'].update({'server.thread_pool': 150})
Chris Sosa7cd23202013-10-15 17:22:57 -0700430 # TODO(sosa): Do this more cleanly.
431 gsutil_util.GSUTIL_ATTEMPTS = 5
Scott Zawalski1c5e7cd2012-02-27 13:12:52 -0500432
Chris Sosa7c931362010-10-11 19:49:01 -0700433 return base_config
rtc@google.com64244662009-11-12 00:52:08 +0000434
Darin Petkove17164a2010-08-11 13:24:41 -0700435
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700436def _GetRecursiveMemberObject(root, member_list):
437 """Returns an object corresponding to a nested member list.
438
439 Args:
440 root: the root object to search
441 member_list: list of nested members to search
Don Garrettf84631a2014-01-07 18:21:26 -0800442
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700443 Returns:
444 An object corresponding to the member name list; None otherwise.
445 """
446 for member in member_list:
447 next_root = root.__class__.__dict__.get(member)
448 if not next_root:
449 return None
450 root = next_root
451 return root
452
453
454def _IsExposed(name):
455 """Returns True iff |name| has an `exposed' attribute and it is set."""
456 return hasattr(name, 'exposed') and name.exposed
457
458
Gilad Arnold748c8322012-10-12 09:51:35 -0700459def _GetExposedMethod(root, nested_member, ignored=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700460 """Returns a CherryPy-exposed method, if such exists.
461
462 Args:
463 root: the root object for searching
464 nested_member: a slash-joined path to the nested member
465 ignored: method paths to be ignored
Don Garrettf84631a2014-01-07 18:21:26 -0800466
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700467 Returns:
468 A function object corresponding to the path defined by |member_list| from
469 the |root| object, if the function is exposed and not ignored; None
470 otherwise.
471 """
Gilad Arnold748c8322012-10-12 09:51:35 -0700472 method = (not (ignored and nested_member in ignored) and
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700473 _GetRecursiveMemberObject(root, nested_member.split('/')))
Gabe Black3b567202015-09-23 14:07:59 -0700474 if method and type(method) == types.FunctionType and _IsExposed(method):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700475 return method
476
477
Gilad Arnold748c8322012-10-12 09:51:35 -0700478def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700479 """Finds exposed CherryPy methods.
480
481 Args:
482 root: the root object for searching
483 prefix: slash-joined chain of members leading to current object
484 unlisted: URLs to be excluded regardless of their exposed status
Don Garrettf84631a2014-01-07 18:21:26 -0800485
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700486 Returns:
487 List of exposed URLs that are not unlisted.
488 """
489 method_list = []
490 for member in sorted(root.__class__.__dict__.keys()):
491 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 09:51:35 -0700492 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700493 continue
494 member_obj = root.__class__.__dict__[member]
495 if _IsExposed(member_obj):
496 if type(member_obj) == types.FunctionType:
497 method_list.append(prefixed_member)
498 else:
499 method_list += _FindExposedMethods(
500 member_obj, prefixed_member, unlisted)
501 return method_list
502
503
xixuan52c2fba2016-05-20 17:02:48 -0700504def _check_base_args_for_auto_update(kwargs):
xixuanac89ce82016-11-30 16:48:20 -0800505 """Check basic args required for auto-update.
506
507 Args:
508 kwargs: the parameters to be checked.
509
510 Raises:
511 DevServerHTTPError if required parameters don't exist in kwargs.
512 """
xixuan52c2fba2016-05-20 17:02:48 -0700513 if 'host_name' not in kwargs:
514 raise common_util.DevServerHTTPError(KEY_ERROR_MSG % 'host_name')
515
516 if 'build_name' not in kwargs:
517 raise common_util.DevServerHTTPError(KEY_ERROR_MSG % 'build_name')
518
519
520def _parse_boolean_arg(kwargs, key):
xixuanac89ce82016-11-30 16:48:20 -0800521 """Parse boolean arg from kwargs.
522
523 Args:
524 kwargs: the parameters to be checked.
525 key: the key to be parsed.
526
527 Returns:
528 The boolean value of kwargs[key], or False if key doesn't exist in kwargs.
529
530 Raises:
531 DevServerHTTPError if kwargs[key] is not a boolean variable.
532 """
xixuan52c2fba2016-05-20 17:02:48 -0700533 if key in kwargs:
534 if kwargs[key] == 'True':
535 return True
536 elif kwargs[key] == 'False':
537 return False
538 else:
539 raise common_util.DevServerHTTPError(
540 'The value for key %s is not boolean.' % key)
541 else:
542 return False
543
xixuan447ad9d2017-02-28 14:46:20 -0800544
xixuanac89ce82016-11-30 16:48:20 -0800545def _parse_string_arg(kwargs, key):
546 """Parse string arg from kwargs.
547
548 Args:
549 kwargs: the parameters to be checked.
550 key: the key to be parsed.
551
552 Returns:
553 The string value of kwargs[key], or None if key doesn't exist in kwargs.
554 """
555 if key in kwargs:
556 return kwargs[key]
557 else:
558 return None
559
xixuan447ad9d2017-02-28 14:46:20 -0800560
xixuanac89ce82016-11-30 16:48:20 -0800561def _build_uri_from_build_name(build_name):
562 """Get build url from a given build name.
563
564 Args:
565 build_name: the build name to be parsed, whose format is
566 'board/release_version'.
567
568 Returns:
569 The release_archive_url on Google Storage for this build name.
570 """
571 return gspaths.ChromeosReleases.BuildUri(
572 cros_update.STABLE_BUILD_CHANNEL, build_name.split('/')[0],
573 build_name.split('/')[1])
xixuan52c2fba2016-05-20 17:02:48 -0700574
xixuan447ad9d2017-02-28 14:46:20 -0800575
576def _clear_process(host_name, pid):
577 """Clear AU process for given hostname and pid.
578
579 This clear includes:
580 1. kill process if it's alive.
581 2. delete the track status file of this process.
582 3. delete the executing log file of this process.
583
584 Args:
585 host_name: the host to execute auto-update.
586 pid: the background auto-update process id.
587 """
588 if cros_update_progress.IsProcessAlive(pid):
589 os.killpg(int(pid), signal.SIGKILL)
590
591 cros_update_progress.DelTrackStatusFile(host_name, pid)
592 cros_update_progress.DelExecuteLogFile(host_name, pid)
593
594
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700595class ApiRoot(object):
596 """RESTful API for Dev Server information."""
597 exposed = True
598
599 @cherrypy.expose
600 def hostinfo(self, ip):
601 """Returns a JSON dictionary containing information about the given ip.
602
Gilad Arnold1b908392012-10-05 11:36:27 -0700603 Args:
604 ip: address of host whose info is requested
Don Garrettf84631a2014-01-07 18:21:26 -0800605
Gilad Arnold1b908392012-10-05 11:36:27 -0700606 Returns:
607 A JSON dictionary containing all or some of the following fields:
608 last_event_type (int): last update event type received
609 last_event_status (int): last update event status received
610 last_known_version (string): last known version reported in update ping
611 forced_update_label (string): update label to force next update ping to
612 use, set by setnextupdate
613 See the OmahaEvent class in update_engine/omaha_request_action.h for
614 event type and status code definitions. If the ip does not exist an empty
615 string is returned.
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700616
Gilad Arnold1b908392012-10-05 11:36:27 -0700617 Example URL:
618 http://myhost/api/hostinfo?ip=192.168.1.5
619 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700620 return updater.HandleHostInfoPing(ip)
621
622 @cherrypy.expose
Gilad Arnold286a0062012-01-12 13:47:02 -0800623 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 11:36:27 -0700624 """Returns a JSON object containing a log of host event.
625
626 Args:
627 ip: address of host whose event log is requested, or `all'
Don Garrettf84631a2014-01-07 18:21:26 -0800628
Gilad Arnold1b908392012-10-05 11:36:27 -0700629 Returns:
630 A JSON encoded list (log) of dictionaries (events), each of which
631 containing a `timestamp' and other event fields, as described under
632 /api/hostinfo.
633
634 Example URL:
635 http://myhost/api/hostlog?ip=192.168.1.5
636 """
Gilad Arnold286a0062012-01-12 13:47:02 -0800637 return updater.HandleHostLogPing(ip)
638
639 @cherrypy.expose
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700640 def setnextupdate(self, ip):
641 """Allows the response to the next update ping from a host to be set.
642
643 Takes the IP of the host and an update label as normally provided to the
Gilad Arnold1b908392012-10-05 11:36:27 -0700644 /update command.
645 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700646 body_length = int(cherrypy.request.headers['Content-Length'])
647 label = cherrypy.request.rfile.read(body_length)
648
649 if label:
650 label = label.strip()
651 if label:
652 return updater.HandleSetUpdatePing(ip, label)
Chris Sosa4b951602014-04-09 20:26:07 -0700653 raise common_util.DevServerHTTPError(400, 'No label provided.')
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700654
655
Gilad Arnold55a2a372012-10-02 09:46:32 -0700656 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800657 def fileinfo(self, *args):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700658 """Returns information about a given staged file.
659
660 Args:
Don Garrettf84631a2014-01-07 18:21:26 -0800661 args: path to the file inside the server's static staging directory
662
Gilad Arnold55a2a372012-10-02 09:46:32 -0700663 Returns:
664 A JSON encoded dictionary with information about the said file, which may
665 contain the following keys/values:
Gilad Arnold1b908392012-10-05 11:36:27 -0700666 size (int): the file size in bytes
667 sha1 (string): a base64 encoded SHA1 hash
668 sha256 (string): a base64 encoded SHA256 hash
669
670 Example URL:
671 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 09:46:32 -0700672 """
Don Garrettf84631a2014-01-07 18:21:26 -0800673 file_path = os.path.join(updater.static_dir, *args)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700674 if not os.path.exists(file_path):
675 raise DevServerError('file not found: %s' % file_path)
676 try:
677 file_size = os.path.getsize(file_path)
678 file_sha1 = common_util.GetFileSha1(file_path)
679 file_sha256 = common_util.GetFileSha256(file_path)
680 except os.error, e:
681 raise DevServerError('failed to get info for file %s: %s' %
Gilad Arnolde74b3812013-04-22 11:27:38 -0700682 (file_path, e))
683
684 is_delta = autoupdate.Autoupdate.IsDeltaFormatFile(file_path)
685
686 return json.dumps({
687 autoupdate.Autoupdate.SIZE_ATTR: file_size,
688 autoupdate.Autoupdate.SHA1_ATTR: file_sha1,
689 autoupdate.Autoupdate.SHA256_ATTR: file_sha256,
690 autoupdate.Autoupdate.ISDELTA_ATTR: is_delta
691 })
Gilad Arnold55a2a372012-10-02 09:46:32 -0700692
Chris Sosa76e44b92013-01-31 12:11:38 -0800693
David Rochberg7c79a812011-01-19 14:24:45 -0500694class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700695 """The Root Class for the Dev Server.
696
697 CherryPy works as follows:
698 For each method in this class, cherrpy interprets root/path
699 as a call to an instance of DevServerRoot->method_name. For example,
700 a call to http://myhost/build will call build. CherryPy automatically
701 parses http args and places them as keyword arguments in each method.
702 For paths http://myhost/update/dir1/dir2, you can use *args so that
703 cherrypy uses the update method and puts the extra paths in args.
704 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700705 # Method names that should not be listed on the index page.
706 _UNLISTED_METHODS = ['index', 'doc']
707
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700708 api = ApiRoot()
Chris Sosa7c931362010-10-11 19:49:01 -0700709
Dan Shi59ae7092013-06-04 14:37:27 -0700710 # Number of threads that devserver is staging images.
711 _staging_thread_count = 0
712 # Lock used to lock increasing/decreasing count.
713 _staging_thread_count_lock = threading.Lock()
714
Dan Shiafd0e492015-05-27 14:23:51 -0700715 @require_psutil()
716 def _refresh_io_stats(self):
717 """A call running in a thread to update IO stats periodically."""
718 prev_disk_io_counters = psutil.disk_io_counters()
719 prev_network_io_counters = psutil.net_io_counters()
720 prev_read_time = time.time()
721 while True:
722 time.sleep(STATS_INTERVAL)
723 now = time.time()
724 interval = now - prev_read_time
725 prev_read_time = now
726 # Disk IO is for all disks.
727 disk_io_counters = psutil.disk_io_counters()
728 network_io_counters = psutil.net_io_counters()
729
730 self.disk_read_bytes_per_sec = (
731 disk_io_counters.read_bytes -
732 prev_disk_io_counters.read_bytes)/interval
733 self.disk_write_bytes_per_sec = (
734 disk_io_counters.write_bytes -
735 prev_disk_io_counters.write_bytes)/interval
736 prev_disk_io_counters = disk_io_counters
737
738 self.network_sent_bytes_per_sec = (
739 network_io_counters.bytes_sent -
740 prev_network_io_counters.bytes_sent)/interval
741 self.network_recv_bytes_per_sec = (
742 network_io_counters.bytes_recv -
743 prev_network_io_counters.bytes_recv)/interval
744 prev_network_io_counters = network_io_counters
745
746 @require_psutil()
747 def _start_io_stat_thread(self):
Gabe Black3b567202015-09-23 14:07:59 -0700748 """Start the thread to collect IO stats."""
Dan Shiafd0e492015-05-27 14:23:51 -0700749 thread = threading.Thread(target=self._refresh_io_stats)
750 thread.daemon = True
751 thread.start()
752
joychen3cb228e2013-06-12 12:13:13 -0700753 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700754 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800755 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 12:13:13 -0700756 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 14:24:45 -0500757
Dan Shiafd0e492015-05-27 14:23:51 -0700758 # Cache of disk IO stats, a thread refresh the stats every 10 seconds.
759 # lock is not used for these variables as the only thread writes to these
760 # variables is _refresh_io_stats.
761 self.disk_read_bytes_per_sec = 0
762 self.disk_write_bytes_per_sec = 0
763 # Cache of network IO stats.
764 self.network_sent_bytes_per_sec = 0
765 self.network_recv_bytes_per_sec = 0
766 self._start_io_stat_thread()
767
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700768 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500769 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700770 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700771 import builder
772 if self._builder is None:
773 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500774 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700775
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700776 @cherrypy.expose
Dan Shif8eb0d12013-08-01 17:52:06 -0700777 def is_staged(self, **kwargs):
778 """Check if artifacts have been downloaded.
779
Chris Sosa6b0c6172013-08-05 17:01:33 -0700780 async: True to return without waiting for download to complete.
781 artifacts: Comma separated list of named artifacts to download.
782 These are defined in artifact_info and have their implementation
783 in build_artifact.py.
784 files: Comma separated list of file artifacts to stage. These
785 will be available as is in the corresponding static directory with no
786 custom post-processing.
787
788 returns: True of all artifacts are staged.
Dan Shif8eb0d12013-08-01 17:52:06 -0700789
790 Example:
791 To check if autotest and test_suites are staged:
792 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
793 artifacts=autotest,test_suites
794 """
Gabe Black3b567202015-09-23 14:07:59 -0700795 dl, factory = _get_downloader_and_factory(kwargs)
Aviv Keshet57d18172016-06-18 20:39:09 -0700796 response = str(dl.IsStaged(factory))
797 _Log('Responding to is_staged %s request with %r', kwargs, response)
798 return response
Dan Shi59ae7092013-06-04 14:37:27 -0700799
Chris Sosa76e44b92013-01-31 12:11:38 -0800800 @cherrypy.expose
Prashanth Ba06d2d22014-03-07 15:35:19 -0800801 def list_image_dir(self, **kwargs):
802 """Take an archive url and list the contents in its staged directory.
803
804 Args:
805 kwargs:
806 archive_url: Google Storage URL for the build.
807
808 Example:
809 To list the contents of where this devserver should have staged
810 gs://image-archive/<board>-release/<build> call:
811 http://devserver_url:<port>/list_image_dir?archive_url=<gs://..>
812
813 Returns:
814 A string with information about the contents of the image directory.
815 """
Gabe Black3b567202015-09-23 14:07:59 -0700816 dl = _get_downloader(kwargs)
Prashanth Ba06d2d22014-03-07 15:35:19 -0800817 try:
Gabe Black3b567202015-09-23 14:07:59 -0700818 image_dir_contents = dl.ListBuildDir()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800819 except build_artifact.ArtifactDownloadError as e:
820 return 'Cannot list the contents of staged artifacts. %s' % e
821 if not image_dir_contents:
Gabe Black3b567202015-09-23 14:07:59 -0700822 return '%s has not been staged on this devserver.' % dl.DescribeSource()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800823 return image_dir_contents
824
825 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800826 def stage(self, **kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700827 """Downloads and caches build artifacts.
Chris Sosa76e44b92013-01-31 12:11:38 -0800828
Gabe Black3b567202015-09-23 14:07:59 -0700829 Downloads and caches build artifacts, possibly from a Google Storage URL,
Dan Shi72b16132015-10-08 12:10:33 -0700830 or from Android's build server. Returns once these have been downloaded
Gabe Black3b567202015-09-23 14:07:59 -0700831 on the devserver. A call to this will attempt to cache non-specified
832 artifacts in the background for the given from the given URL following
833 the principle of spatial locality. Spatial locality of different
Chris Sosa76e44b92013-01-31 12:11:38 -0800834 artifacts is explicitly defined in the build_artifact module.
835
836 These artifacts will then be available from the static/ sub-directory of
837 the devserver.
838
839 Args:
840 archive_url: Google Storage URL for the build.
Simran Basi4243a862014-12-12 12:48:33 -0800841 local_path: Local path for the build.
Dan Shif8eb0d12013-08-01 17:52:06 -0700842 async: True to return without waiting for download to complete.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700843 artifacts: Comma separated list of named artifacts to download.
844 These are defined in artifact_info and have their implementation
845 in build_artifact.py.
846 files: Comma separated list of files to stage. These
847 will be available as is in the corresponding static directory with no
848 custom post-processing.
Laurence Goodbyf5c958d2016-01-14 18:23:56 -0800849 clean: True to remove any previously staged artifacts first.
Chris Sosa76e44b92013-01-31 12:11:38 -0800850
851 Example:
852 To download the autotest and test suites tarballs:
853 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
854 artifacts=autotest,test_suites
855 To download the full update payload:
856 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
857 artifacts=full_payload
Chris Sosa6b0c6172013-08-05 17:01:33 -0700858 To download just a file called blah.bin:
859 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
860 files=blah.bin
Chris Sosa76e44b92013-01-31 12:11:38 -0800861
862 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 16:39:34 -0700863 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 12:11:38 -0800864
865 Note for this example, relative path is the archive_url stripped of its
866 basename i.e. path/ in the examples above. Specific example:
867
868 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
869
870 Will get staged to:
871
joychened64b222013-06-21 16:39:34 -0700872 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Chris Sosa76e44b92013-01-31 12:11:38 -0800873 """
Gabe Black3b567202015-09-23 14:07:59 -0700874 dl, factory = _get_downloader_and_factory(kwargs)
875
Dan Shi59ae7092013-06-04 14:37:27 -0700876 with DevServerRoot._staging_thread_count_lock:
877 DevServerRoot._staging_thread_count += 1
878 try:
Laurence Goodbyf5c958d2016-01-14 18:23:56 -0800879 boolean_string = kwargs.get('clean')
880 clean = xbuddy.XBuddy.ParseBoolean(boolean_string)
881 if clean and os.path.exists(dl.GetBuildDir()):
882 _Log('Removing %s' % dl.GetBuildDir())
883 shutil.rmtree(dl.GetBuildDir())
Gabe Black3b567202015-09-23 14:07:59 -0700884 async = kwargs.get('async', False)
885 dl.Download(factory, async=async)
Dan Shi59ae7092013-06-04 14:37:27 -0700886 finally:
887 with DevServerRoot._staging_thread_count_lock:
888 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800889 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700890
891 @cherrypy.expose
xixuan52c2fba2016-05-20 17:02:48 -0700892 def cros_au(self, **kwargs):
893 """Auto-update a CrOS DUT.
894
895 Args:
896 kwargs:
897 host_name: the hostname of the DUT to auto-update.
898 build_name: the build name for update the DUT.
899 force_update: Force an update even if the version installed is the
900 same. Default: False.
901 full_update: If True, do not run stateful update, directly force a full
902 reimage. If False, try stateful update first if the dut is already
903 installed with the same version.
904 async: Whether the auto_update function is ran in the background.
David Rileyee75de22017-11-02 10:48:15 -0700905 quick_provision: Whether the quick provision path is attempted first.
xixuan52c2fba2016-05-20 17:02:48 -0700906
907 Returns:
908 A tuple includes two elements:
909 a boolean variable represents whether the auto-update process is
910 successfully started.
911 an integer represents the background auto-update process id.
912 """
913 _check_base_args_for_auto_update(kwargs)
914
915 host_name = kwargs['host_name']
916 build_name = kwargs['build_name']
917 force_update = _parse_boolean_arg(kwargs, 'force_update')
918 full_update = _parse_boolean_arg(kwargs, 'full_update')
919 async = _parse_boolean_arg(kwargs, 'async')
xixuanac89ce82016-11-30 16:48:20 -0800920 original_build = _parse_string_arg(kwargs, 'original_build')
David Haddock90e49442017-04-07 19:14:09 -0700921 payload_filename = _parse_string_arg(kwargs, 'payload_filename')
David Haddock20559612017-06-28 22:15:08 -0700922 clobber_stateful = _parse_boolean_arg(kwargs, 'clobber_stateful')
David Rileyee75de22017-11-02 10:48:15 -0700923 quick_provision = _parse_boolean_arg(kwargs, 'quick_provision')
924
925 devserver_url = updater.GetDevserverUrl()
926 static_url = updater.GetStaticUrl()
xixuan52c2fba2016-05-20 17:02:48 -0700927
928 if async:
929 path = os.path.dirname(os.path.abspath(__file__))
930 execute_file = os.path.join(path, 'cros_update.py')
931 args = (AUTO_UPDATE_CMD % (execute_file, host_name, build_name,
932 updater.static_dir))
xixuanac89ce82016-11-30 16:48:20 -0800933
934 # The original_build's format is like: link/3428.210.0
935 # The corresponding release_archive_url's format is like:
936 # gs://chromeos-releases/stable-channel/link/3428.210.0
937 if original_build:
938 release_archive_url = _build_uri_from_build_name(original_build)
939 # First staging the stateful.tgz synchronousely.
940 self.stage(files='stateful.tgz', async=False,
941 archive_url=release_archive_url)
942 args = ('%s --original_build %s' % (args, original_build))
943
xixuan52c2fba2016-05-20 17:02:48 -0700944 if force_update:
945 args = ('%s --force_update' % args)
946
947 if full_update:
948 args = ('%s --full_update' % args)
949
David Haddock90e49442017-04-07 19:14:09 -0700950 if payload_filename:
951 args = ('%s --payload_filename %s' % (args, payload_filename))
952
David Haddock20559612017-06-28 22:15:08 -0700953 if clobber_stateful:
954 args = ('%s --clobber_stateful' % args)
955
David Rileyee75de22017-11-02 10:48:15 -0700956 if quick_provision:
957 args = ('%s --quick_provision' % args)
958
959 if devserver_url:
960 args = ('%s --devserver_url %s' % (args, devserver_url))
961
962 if static_url:
963 args = ('%s --static_url %s' % (args, static_url))
964
xixuan2a0970a2016-08-10 12:12:44 -0700965 p = subprocess.Popen([args], shell=True, preexec_fn=os.setsid)
966 pid = os.getpgid(p.pid)
xixuan52c2fba2016-05-20 17:02:48 -0700967
968 # Pre-write status in the track_status_file before the first call of
969 # 'get_au_status' to make sure that the track_status_file exists.
xixuan2a0970a2016-08-10 12:12:44 -0700970 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -0700971 progress_tracker.WriteStatus('CrOS update is just started.')
972
xixuan2a0970a2016-08-10 12:12:44 -0700973 return json.dumps((True, pid))
xixuan52c2fba2016-05-20 17:02:48 -0700974 else:
975 cros_update_trigger = cros_update.CrOSUpdateTrigger(
xixuanac89ce82016-11-30 16:48:20 -0800976 host_name, build_name, updater.static_dir, force_update=force_update,
David Rileyee75de22017-11-02 10:48:15 -0700977 full_update=full_update, original_build=original_build,
978 quick_provision=quick_provision, devserver_url=devserver_url,
979 static_url=static_url)
xixuan52c2fba2016-05-20 17:02:48 -0700980 cros_update_trigger.TriggerAU()
xixuan27d50442017-08-09 10:38:25 -0700981 return json.dumps((True, -1))
xixuan52c2fba2016-05-20 17:02:48 -0700982
983 @cherrypy.expose
984 def get_au_status(self, **kwargs):
985 """Check if the auto-update task is finished.
986
987 It handles 4 cases:
988 1. If an error exists in the track_status_file, delete the track file and
989 raise it.
990 2. If cros-update process is finished, delete the file and return the
991 success result.
992 3. If the process is not running, delete the track file and raise an error
993 about 'the process is terminated due to unknown reason'.
994 4. If the track_status_file does not exist, kill the process if it exists,
995 and raise the IOError.
996
997 Args:
998 kwargs:
999 host_name: the hostname of the DUT to auto-update.
1000 pid: the background process id of cros-update.
1001
1002 Returns:
xixuan28d99072016-10-06 12:24:16 -07001003 A dict with three elements:
xixuan52c2fba2016-05-20 17:02:48 -07001004 a boolean variable represents whether the auto-update process is
1005 finished.
1006 a string represents the current auto-update process status.
1007 For example, 'Transfer Devserver/Stateful Update Package'.
xixuan28d99072016-10-06 12:24:16 -07001008 a detailed error message paragraph if there exists an Auto-Update
1009 error, in which the last line shows the main exception. Empty
1010 string otherwise.
xixuan52c2fba2016-05-20 17:02:48 -07001011 """
1012 if 'host_name' not in kwargs:
1013 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
1014
1015 if 'pid' not in kwargs:
1016 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
1017
1018 host_name = kwargs['host_name']
1019 pid = kwargs['pid']
1020 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
1021
xixuan28d99072016-10-06 12:24:16 -07001022 result_dict = {'finished': False, 'status': '', 'detailed_error_msg': ''}
xixuan52c2fba2016-05-20 17:02:48 -07001023 try:
1024 result = progress_tracker.ReadStatus()
1025 if result.startswith(cros_update_progress.ERROR_TAG):
xixuan28d99072016-10-06 12:24:16 -07001026 result_dict['detailed_error_msg'] = result[len(
1027 cros_update_progress.ERROR_TAG):]
xixuan28681fd2016-11-23 11:13:56 -08001028 elif result == cros_update_progress.FINISHED:
xixuan28d99072016-10-06 12:24:16 -07001029 result_dict['finished'] = True
1030 result_dict['status'] = result
xixuan28681fd2016-11-23 11:13:56 -08001031 elif not cros_update_progress.IsProcessAlive(pid):
xixuan28d99072016-10-06 12:24:16 -07001032 result_dict['detailed_error_msg'] = (
1033 'Cros_update process terminated midway due to unknown reason. '
1034 'Last update status was %s' % result)
xixuan28681fd2016-11-23 11:13:56 -08001035 else:
1036 result_dict['status'] = result
1037 except IOError as e:
1038 if pid and cros_update_progress.IsProcessAlive(pid):
xixuan2a0970a2016-08-10 12:12:44 -07001039 os.killpg(int(pid), signal.SIGKILL)
xixuan52c2fba2016-05-20 17:02:48 -07001040
xixuan28681fd2016-11-23 11:13:56 -08001041 result_dict['detailed_error_msg'] = str(e)
1042
1043 return json.dumps(result_dict)
xixuan52c2fba2016-05-20 17:02:48 -07001044
1045 @cherrypy.expose
David Riley6d5fca02017-10-31 10:35:47 -07001046 def post_au_status(self, status, **kwargs):
1047 """Updates the status of an auto-update task.
1048
1049 Callers will need to POST to this URL with a body of MIME-type
1050 "multipart/form-data".
1051 The body should include a single argument, 'status', containing the
1052 AU status to record.
1053
1054 Args:
1055 status: The updated status.
1056 kwargs:
1057 host_name: the hostname of the DUT to auto-update.
1058 pid: the background process id of cros-update.
1059 """
1060 if 'host_name' not in kwargs:
1061 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
1062
1063 if 'pid' not in kwargs:
1064 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
1065
1066 host_name = kwargs['host_name']
1067 pid = kwargs['pid']
David Riley3cea2582017-11-24 22:03:01 -08001068 status = status.rstrip()
1069 _Log('Recording status for %s (%s): %s' % (host_name, pid, status))
David Riley6d5fca02017-10-31 10:35:47 -07001070 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
1071
David Riley3cea2582017-11-24 22:03:01 -08001072 progress_tracker.WriteStatus(status)
David Riley6d5fca02017-10-31 10:35:47 -07001073
1074 return 'True'
1075
1076 @cherrypy.expose
xixuan52c2fba2016-05-20 17:02:48 -07001077 def handler_cleanup(self, **kwargs):
xixuan3bc974e2016-10-18 17:21:43 -07001078 """Clean track status log and temp directory for CrOS auto-update process.
xixuan52c2fba2016-05-20 17:02:48 -07001079
1080 Args:
1081 kwargs:
1082 host_name: the hostname of the DUT to auto-update.
1083 pid: the background process id of cros-update.
1084 """
1085 if 'host_name' not in kwargs:
1086 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
1087
1088 if 'pid' not in kwargs:
1089 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
1090
1091 host_name = kwargs['host_name']
1092 pid = kwargs['pid']
1093 cros_update_progress.DelTrackStatusFile(host_name, pid)
xixuan3bc974e2016-10-18 17:21:43 -07001094 cros_update_progress.DelAUTempDirectory(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -07001095
1096 @cherrypy.expose
1097 def kill_au_proc(self, **kwargs):
1098 """Kill CrOS auto-update process using given process id.
1099
1100 Args:
1101 kwargs:
1102 host_name: Kill all the CrOS auto-update process of this host.
1103
1104 Returns:
1105 True if all processes are killed properly.
1106 """
1107 if 'host_name' not in kwargs:
1108 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
1109
xixuan447ad9d2017-02-28 14:46:20 -08001110 cur_pid = kwargs.get('pid')
1111
xixuan52c2fba2016-05-20 17:02:48 -07001112 host_name = kwargs['host_name']
xixuan3bc974e2016-10-18 17:21:43 -07001113 track_log_list = cros_update_progress.GetAllTrackStatusFileByHostName(
1114 host_name)
xixuan52c2fba2016-05-20 17:02:48 -07001115 for log in track_log_list:
1116 # The track log's full path is: path/host_name_pid.log
1117 # Use splitext to remove file extension, then parse pid from the
1118 # filename.
1119 pid = os.path.splitext(os.path.basename(log))[0][len(host_name)+1:]
xixuan447ad9d2017-02-28 14:46:20 -08001120 _clear_process(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -07001121
xixuan447ad9d2017-02-28 14:46:20 -08001122 if cur_pid:
1123 _clear_process(host_name, cur_pid)
xixuan52c2fba2016-05-20 17:02:48 -07001124
1125 return 'True'
1126
1127 @cherrypy.expose
1128 def collect_cros_au_log(self, **kwargs):
1129 """Collect CrOS auto-update log.
1130
1131 Args:
1132 kwargs:
1133 host_name: the hostname of the DUT to auto-update.
1134 pid: the background process id of cros-update.
1135
1136 Returns:
David Haddock9f459632017-05-11 14:45:46 -07001137 A dictionary containing the execute log file and any hostlog files.
xixuan52c2fba2016-05-20 17:02:48 -07001138 """
1139 if 'host_name' not in kwargs:
1140 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
1141
1142 if 'pid' not in kwargs:
1143 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
1144
1145 host_name = kwargs['host_name']
1146 pid = kwargs['pid']
xixuan3bc974e2016-10-18 17:21:43 -07001147
1148 # Fetch the execute log recorded by cros_update_progress.
xixuan1bbfaba2016-10-13 17:53:22 -07001149 au_log = cros_update_progress.ReadExecuteLogFile(host_name, pid)
1150 cros_update_progress.DelExecuteLogFile(host_name, pid)
David Haddock9f459632017-05-11 14:45:46 -07001151 # Fetch the cros_au host_logs if they exist
1152 au_hostlogs = cros_update_progress.ReadAUHostLogFiles(host_name, pid)
1153 return json.dumps({'cros_au_log': au_log, 'host_logs': au_hostlogs})
xixuan1bbfaba2016-10-13 17:53:22 -07001154
xixuan52c2fba2016-05-20 17:02:48 -07001155 @cherrypy.expose
Dan Shi2f136862016-02-11 15:38:38 -08001156 def locate_file(self, **kwargs):
1157 """Get the path to the given file name.
1158
1159 This method looks up the given file name inside specified build artifacts.
1160 One use case is to help caller to locate an apk file inside a build
1161 artifact. The location of the apk file could be different based on the
1162 branch and target.
1163
1164 Args:
1165 file_name: Name of the file to look for.
1166 artifacts: A list of artifact names to search for the file.
1167
1168 Returns:
1169 Path to the file with the given name. It's relative to the folder for the
1170 build, e.g., DATA/priv-app/sl4a/sl4a.apk
Dan Shi2f136862016-02-11 15:38:38 -08001171 """
1172 dl, _ = _get_downloader_and_factory(kwargs)
1173 try:
Joe Brennan1691f8e2017-03-15 15:53:36 -07001174 file_name = kwargs['file_name']
Dan Shi2f136862016-02-11 15:38:38 -08001175 artifacts = kwargs['artifacts']
1176 except KeyError:
1177 raise DevServerError('`file_name` and `artifacts` are required to search '
1178 'for a file in build artifacts.')
1179 build_path = dl.GetBuildDir()
1180 for artifact in artifacts:
1181 # Get the unzipped folder of the artifact. If it's not defined in
1182 # ARTIFACT_UNZIP_FOLDER_MAP, assume the files are unzipped to the build
1183 # directory directly.
1184 folder = artifact_info.ARTIFACT_UNZIP_FOLDER_MAP.get(artifact, '')
1185 artifact_path = os.path.join(build_path, folder)
1186 for root, _, filenames in os.walk(artifact_path):
Joe Brennan1691f8e2017-03-15 15:53:36 -07001187 if file_name in set([f for f in filenames]):
Dan Shi2f136862016-02-11 15:38:38 -08001188 return os.path.relpath(os.path.join(root, file_name), build_path)
1189 raise DevServerError('File `%s` can not be found in artifacts: %s' %
1190 (file_name, artifacts))
1191
1192 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -08001193 def setup_telemetry(self, **kwargs):
1194 """Extracts and sets up telemetry
1195
1196 This method goes through the telemetry deps packages, and stages them on
1197 the devserver to be used by the drones and the telemetry tests.
1198
1199 Args:
1200 archive_url: Google Storage URL for the build.
1201
1202 Returns:
1203 Path to the source folder for the telemetry codebase once it is staged.
1204 """
Gabe Black3b567202015-09-23 14:07:59 -07001205 dl = _get_downloader(kwargs)
Simran Basi4baad082013-02-14 13:39:18 -08001206
Gabe Black3b567202015-09-23 14:07:59 -07001207 build_path = dl.GetBuildDir()
Simran Basi4baad082013-02-14 13:39:18 -08001208 deps_path = os.path.join(build_path, 'autotest/packages')
1209 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
1210 src_folder = os.path.join(telemetry_path, 'src')
1211
1212 with self._telemetry_lock_dict.lock(telemetry_path):
1213 if os.path.exists(src_folder):
1214 # Telemetry is already fully stage return
1215 return src_folder
1216
1217 common_util.MkDirP(telemetry_path)
1218
1219 # Copy over the required deps tar balls to the telemetry directory.
1220 for dep in TELEMETRY_DEPS:
1221 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -07001222 if not os.path.exists(dep_path):
1223 # This dep does not exist (could be new), do not extract it.
1224 continue
Simran Basi4baad082013-02-14 13:39:18 -08001225 try:
1226 common_util.ExtractTarball(dep_path, telemetry_path)
1227 except common_util.CommonUtilError as e:
1228 shutil.rmtree(telemetry_path)
1229 raise DevServerError(str(e))
1230
1231 # By default all the tarballs extract to test_src but some parts of
1232 # the telemetry code specifically hardcoded to exist inside of 'src'.
1233 test_src = os.path.join(telemetry_path, 'test_src')
1234 try:
1235 shutil.move(test_src, src_folder)
1236 except shutil.Error:
1237 # This can occur if src_folder already exists. Remove and retry move.
1238 shutil.rmtree(src_folder)
Gabe Black3b567202015-09-23 14:07:59 -07001239 raise DevServerError(
1240 'Failure in telemetry setup for build %s. Appears that the '
1241 'test_src to src move failed.' % dl.GetBuild())
Simran Basi4baad082013-02-14 13:39:18 -08001242
1243 return src_folder
1244
1245 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -08001246 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -07001247 """Symbolicates a minidump using pre-downloaded symbols, returns it.
1248
1249 Callers will need to POST to this URL with a body of MIME-type
1250 "multipart/form-data".
1251 The body should include a single argument, 'minidump', containing the
1252 binary-formatted minidump to symbolicate.
1253
Chris Masone816e38c2012-05-02 12:22:36 -07001254 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -08001255 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -07001256 minidump: The binary minidump file to symbolicate.
1257 """
Chris Sosa76e44b92013-01-31 12:11:38 -08001258 # Ensure the symbols have been staged.
Dan Shif08fe492016-10-04 14:39:25 -07001259 # Try debug.tar.xz first, then debug.tgz
1260 for artifact in (artifact_info.SYMBOLS_ONLY, artifact_info.SYMBOLS):
1261 kwargs['artifacts'] = artifact
1262 dl = _get_downloader(kwargs)
1263
1264 try:
1265 if self.stage(**kwargs) == 'Success':
1266 break
1267 except build_artifact.ArtifactDownloadError:
1268 continue
1269 else:
Gabe Black3b567202015-09-23 14:07:59 -07001270 raise DevServerError('Failed to stage symbols for %s' %
1271 dl.DescribeSource())
Chris Sosa76e44b92013-01-31 12:11:38 -08001272
Chris Masone816e38c2012-05-02 12:22:36 -07001273 to_return = ''
1274 with tempfile.NamedTemporaryFile() as local:
1275 while True:
1276 data = minidump.file.read(8192)
1277 if not data:
1278 break
1279 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -08001280
Chris Masone816e38c2012-05-02 12:22:36 -07001281 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -08001282
Gabe Black3b567202015-09-23 14:07:59 -07001283 symbols_directory = os.path.join(dl.GetBuildDir(), 'debug', 'breakpad')
Chris Sosa76e44b92013-01-31 12:11:38 -08001284
xixuanab744382017-04-27 10:41:27 -07001285 # The location of minidump_stackwalk is defined in chromeos-admin.
Chris Sosa76e44b92013-01-31 12:11:38 -08001286 stackwalk = subprocess.Popen(
xixuanab744382017-04-27 10:41:27 -07001287 ['/usr/local/bin/minidump_stackwalk', local.name, symbols_directory],
Chris Sosa76e44b92013-01-31 12:11:38 -08001288 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1289
Chris Masone816e38c2012-05-02 12:22:36 -07001290 to_return, error_text = stackwalk.communicate()
1291 if stackwalk.returncode != 0:
1292 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
1293 error_text, stackwalk.returncode))
1294
1295 return to_return
1296
1297 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -08001298 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 15:31:36 -04001299 """Return a string representing the latest build for a given target.
1300
1301 Args:
1302 target: The build target, typically a combination of the board and the
1303 type of build e.g. x86-mario-release.
1304 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
1305 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-07 18:21:26 -08001306
Scott Zawalski16954532012-03-20 15:31:36 -04001307 Returns:
1308 A string representation of the latest build if one exists, i.e.
1309 R19-1993.0.0-a1-b1480.
1310 An empty string if no latest could be found.
1311 """
Don Garrettf84631a2014-01-07 18:21:26 -08001312 if not kwargs:
Scott Zawalski16954532012-03-20 15:31:36 -04001313 return _PrintDocStringAsHTML(self.latestbuild)
1314
Don Garrettf84631a2014-01-07 18:21:26 -08001315 if 'target' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -07001316 raise common_util.DevServerHTTPError(500, 'Error: target= is required!')
Dan Shi61305df2015-10-26 16:52:35 -07001317
1318 if _is_android_build_request(kwargs):
1319 branch = kwargs.get('branch', None)
1320 target = kwargs.get('target', None)
1321 if not target or not branch:
1322 raise DevServerError(
xixuan52c2fba2016-05-20 17:02:48 -07001323 'Both target and branch must be specified to query for the latest '
1324 'Android build.')
Dan Shi61305df2015-10-26 16:52:35 -07001325 return android_build.BuildAccessor.GetLatestBuildID(target, branch)
1326
Scott Zawalski16954532012-03-20 15:31:36 -04001327 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001328 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-07 18:21:26 -08001329 updater.static_dir, kwargs['target'],
1330 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -07001331 except common_util.CommonUtilError as errmsg:
Chris Sosa4b951602014-04-09 20:26:07 -07001332 raise common_util.DevServerHTTPError(500, str(errmsg))
Scott Zawalski16954532012-03-20 15:31:36 -04001333
1334 @cherrypy.expose
xixuan7efd0002016-04-14 15:34:01 -07001335 def list_suite_controls(self, **kwargs):
1336 """Return a list of contents of all known control files.
1337
1338 Example URL:
1339 To List all control files' content:
1340 http://dev-server/list_suite_controls?suite_name=bvt&
1341 build=daisy_spring-release/R29-4279.0.0
1342
1343 Args:
1344 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
1345 suite_name: List the control files belonging to that suite.
1346
1347 Returns:
Dan Shia1cd6522016-04-18 16:07:21 -07001348 A dictionary of all control files's path to its content for given suite.
xixuan7efd0002016-04-14 15:34:01 -07001349 """
1350 if not kwargs:
1351 return _PrintDocStringAsHTML(self.controlfiles)
1352
1353 if 'build' not in kwargs:
1354 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
1355
1356 if 'suite_name' not in kwargs:
Dan Shia1cd6522016-04-18 16:07:21 -07001357 raise common_util.DevServerHTTPError(500,
1358 'Error: suite_name= is required!')
xixuan7efd0002016-04-14 15:34:01 -07001359
1360 control_file_list = [
1361 line.rstrip() for line in common_util.GetControlFileListForSuite(
1362 updater.static_dir, kwargs['build'],
1363 kwargs['suite_name']).splitlines()]
1364
Dan Shia1cd6522016-04-18 16:07:21 -07001365 control_file_content_dict = {}
xixuan7efd0002016-04-14 15:34:01 -07001366 for control_path in control_file_list:
Dan Shia1cd6522016-04-18 16:07:21 -07001367 control_file_content_dict[control_path] = (common_util.GetControlFile(
xixuan7efd0002016-04-14 15:34:01 -07001368 updater.static_dir, kwargs['build'], control_path))
1369
Dan Shia1cd6522016-04-18 16:07:21 -07001370 return json.dumps(control_file_content_dict)
xixuan7efd0002016-04-14 15:34:01 -07001371
1372 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -08001373 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 17:17:28 -05001374 """Return a control file or a list of all known control files.
1375
1376 Example URL:
1377 To List all control files:
beepsbd337242013-07-09 22:44:06 -07001378 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
1379 To List all control files for, say, the bvt suite:
1380 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -05001381 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -05001382 http://dev-server/controlfiles?board=x86-alex-release&build=R18-1514.0.0&control_path=client/sleeptest/control
Scott Zawalski4647ce62012-01-03 17:17:28 -05001383
1384 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -05001385 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -05001386 control_path: If you want the contents of a control file set this
1387 to the path. E.g. client/site_tests/sleeptest/control
1388 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -07001389 suite_name: If control_path is not specified but a suite_name is
1390 specified, list the control files belonging to that suite instead of
1391 all control files. The empty string for suite_name will list all control
1392 files for the build.
Don Garrettf84631a2014-01-07 18:21:26 -08001393
Scott Zawalski4647ce62012-01-03 17:17:28 -05001394 Returns:
1395 Contents of a control file if control_path is provided.
1396 A list of control files if no control_path is provided.
1397 """
Don Garrettf84631a2014-01-07 18:21:26 -08001398 if not kwargs:
Scott Zawalski4647ce62012-01-03 17:17:28 -05001399 return _PrintDocStringAsHTML(self.controlfiles)
1400
Don Garrettf84631a2014-01-07 18:21:26 -08001401 if 'build' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -07001402 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -05001403
Don Garrettf84631a2014-01-07 18:21:26 -08001404 if 'control_path' not in kwargs:
1405 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-09 22:44:06 -07001406 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-07 18:21:26 -08001407 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-09 22:44:06 -07001408 else:
1409 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-07 18:21:26 -08001410 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -05001411 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001412 return common_util.GetControlFile(
Don Garrettf84631a2014-01-07 18:21:26 -08001413 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -08001414
1415 @cherrypy.expose
Simran Basi99e63c02014-05-20 10:39:52 -07001416 def xbuddy_translate(self, *args, **kwargs):
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001417 """Translates an xBuddy path to a real path to artifact if it exists.
1418
1419 Args:
Simran Basi99e63c02014-05-20 10:39:52 -07001420 args: An xbuddy path in the form of {local|remote}/build_id/artifact.
1421 Local searches the devserver's static directory. Remote searches a
1422 Google Storage image archive.
1423
1424 Kwargs:
1425 image_dir: Google Storage image archive to search in if requesting a
1426 remote artifact. If none uses the default bucket.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001427
1428 Returns:
Simran Basi99e63c02014-05-20 10:39:52 -07001429 String in the format of build_id/artifact as stored on the local server
1430 or in Google Storage.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001431 """
Simran Basi99e63c02014-05-20 10:39:52 -07001432 build_id, filename = self._xbuddy.Translate(
Gabe Black3b567202015-09-23 14:07:59 -07001433 args, image_dir=kwargs.get('image_dir'))
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001434 response = os.path.join(build_id, filename)
1435 _Log('Path translation requested, returning: %s', response)
1436 return response
1437
1438 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -07001439 def xbuddy(self, *args, **kwargs):
1440 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -07001441
1442 Args:
joycheneaf4cfc2013-07-02 08:38:57 -07001443 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -07001444 components of the path. The path can be understood as
1445 "{local|remote}/build_id/artifact" where build_id is composed of
1446 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -07001447
joychen121fc9b2013-08-02 14:30:30 -07001448 The first path element is optional, and can be "remote" or "local"
1449 If local (the default), devserver will not attempt to access Google
1450 Storage, and will only search the static directory for the files.
1451 If remote, devserver will try to obtain the artifact off GS if it's
1452 not found locally.
1453 The board is the familiar board name, optionally suffixed.
1454 The version can be the google storage version number, and may also be
1455 any of a number of xBuddy defined version aliases that will be
1456 translated into the latest built image that fits the description.
1457 Defaults to latest.
1458 The artifact is one of a number of image or artifact aliases used by
1459 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -07001460
1461 Kwargs:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001462 for_update: {true|false}
1463 if true, pregenerates the update payloads for the image,
1464 and returns the update uri to pass to the
1465 update_engine_client.
joychen3cb228e2013-06-12 12:13:13 -07001466 return_dir: {true|false}
1467 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001468 relative_path: {true|false}
1469 if set to true, returns the relative path to the payload
1470 directory from static_dir.
joychen3cb228e2013-06-12 12:13:13 -07001471 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -07001472 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -07001473 or
joycheneaf4cfc2013-07-02 08:38:57 -07001474 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -07001475
1476 Returns:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001477 If |for_update|, returns a redirect to the image or update file
1478 on the devserver. E.g.,
1479 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
1480 chromium-test-image.bin
1481 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
1482 http://host:port/static/x86-generic-release/R26-4000.0.0/
1483 If |relative_path| is true, return a relative path the folder where the
1484 payloads are. E.g.,
1485 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 12:13:13 -07001486 """
Chris Sosa75490802013-09-30 17:21:45 -07001487 boolean_string = kwargs.get('for_update')
1488 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001489 boolean_string = kwargs.get('return_dir')
1490 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
1491 boolean_string = kwargs.get('relative_path')
1492 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -07001493
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001494 if return_dir and relative_path:
Chris Sosa4b951602014-04-09 20:26:07 -07001495 raise common_util.DevServerHTTPError(
1496 500, 'Cannot specify both return_dir and relative_path')
Chris Sosa75490802013-09-30 17:21:45 -07001497
1498 # For updates, we optimize downloading of test images.
1499 file_name = None
1500 build_id = None
1501 if for_update:
1502 try:
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001503 build_id = self._xbuddy.StageTestArtifactsForUpdate(args)
Chris Sosa75490802013-09-30 17:21:45 -07001504 except build_artifact.ArtifactDownloadError:
1505 build_id = None
1506
1507 if not build_id:
1508 build_id, file_name = self._xbuddy.Get(args)
1509
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001510 if for_update:
1511 _Log('Payload generation triggered by request')
1512 # Forces payload to be in cache and symlinked into build_id dir.
Chris Sosa75490802013-09-30 17:21:45 -07001513 updater.GetUpdateForLabel(autoupdate.FORCED_UPDATE, build_id,
1514 image_name=file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001515
1516 response = None
1517 if return_dir:
1518 response = os.path.join(cherrypy.request.base, 'static', build_id)
1519 _Log('Directory requested, returning: %s', response)
1520 elif relative_path:
1521 response = build_id
1522 _Log('Relative path requested, returning: %s', response)
1523 elif for_update:
1524 response = os.path.join(cherrypy.request.base, 'update', build_id)
1525 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 12:13:13 -07001526 else:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001527 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 14:30:30 -07001528 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001529 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 14:30:30 -07001530 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -07001531
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001532 return response
1533
joychen3cb228e2013-06-12 12:13:13 -07001534 @cherrypy.expose
1535 def xbuddy_list(self):
1536 """Lists the currently available images & time since last access.
1537
Gilad Arnold452fd272014-02-04 11:09:28 -08001538 Returns:
1539 A string representation of a list of tuples [(build_id, time since last
1540 access),...]
joychen3cb228e2013-06-12 12:13:13 -07001541 """
1542 return self._xbuddy.List()
1543
1544 @cherrypy.expose
1545 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 11:09:28 -08001546 """Returns the number of images cached by xBuddy."""
joychen3cb228e2013-06-12 12:13:13 -07001547 return self._xbuddy.Capacity()
1548
1549 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001550 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001551 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001552 return ('Welcome to the Dev Server!<br>\n'
1553 '<br>\n'
1554 'Here are the available methods, click for documentation:<br>\n'
1555 '<br>\n'
1556 '%s' %
1557 '<br>\n'.join(
1558 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001559 for name in _FindExposedMethods(
1560 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001561
1562 @cherrypy.expose
1563 def doc(self, *args):
1564 """Shows the documentation for available methods / URLs.
1565
1566 Example:
1567 http://myhost/doc/update
1568 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001569 name = '/'.join(args)
1570 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001571 if not method:
1572 raise DevServerError("No exposed method named `%s'" % name)
1573 if not method.__doc__:
1574 raise DevServerError("No documentation for exposed method `%s'" % name)
1575 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -07001576
Dale Curtisc9aaf3a2011-08-09 15:47:40 -07001577 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001578 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001579 """Handles an update check from a Chrome OS client.
1580
1581 The HTTP request should contain the standard Omaha-style XML blob. The URL
1582 line may contain an additional intermediate path to the update payload.
1583
joychen121fc9b2013-08-02 14:30:30 -07001584 This request can be handled in one of 4 ways, depending on the devsever
1585 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -07001586
joychen121fc9b2013-08-02 14:30:30 -07001587 1. No intermediate path
1588 If no intermediate path is given, the default behavior is to generate an
1589 update payload from the latest test image locally built for the board
1590 specified in the xml. Devserver serves the generated payload.
1591
1592 2. Path explicitly invokes XBuddy
1593 If there is a path given, it can explicitly invoke xbuddy by prefixing it
1594 with 'xbuddy'. This path is then used to acquire an image binary for the
1595 devserver to generate an update payload from. Devserver then serves this
1596 payload.
1597
1598 3. Path is left for the devserver to interpret.
1599 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
1600 to generate a payload from the test image in that directory and serve it.
1601
1602 4. The devserver is in a 'forced' mode. TO BE DEPRECATED
1603 This comes from the usage of --forced_payload or --image when starting the
1604 devserver. No matter what path (or no path) gets passed in, devserver will
1605 serve the update payload (--forced_payload) or generate an update payload
1606 from the image (--image).
1607
1608 Examples:
1609 1. No intermediate path
1610 update_engine_client --omaha_url=http://myhost/update
1611 This generates an update payload from the latest test image locally built
1612 for the board specified in the xml.
1613
1614 2. Explicitly invoke xbuddy
1615 update_engine_client --omaha_url=
1616 http://myhost/update/xbuddy/remote/board/version/dev
1617 This would go to GS to download the dev image for the board, from which
1618 the devserver would generate a payload to serve.
1619
1620 3. Give a path for devserver to interpret
1621 update_engine_client --omaha_url=http://myhost/update/some/random/path
1622 This would attempt, in order to:
1623 a) Generate an update from a test image binary if found in
1624 static_dir/some/random/path.
1625 b) Serve an update payload found in static_dir/some/random/path.
1626 c) Hope that some/random/path takes the form "board/version" and
1627 and attempt to download an update payload for that board/version
1628 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001629 """
joychen121fc9b2013-08-02 14:30:30 -07001630 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -08001631 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -07001632 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-11 19:49:01 -07001633
joychen121fc9b2013-08-02 14:30:30 -07001634 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001635
Dan Shiafd0e492015-05-27 14:23:51 -07001636 @require_psutil()
1637 def _get_io_stats(self):
1638 """Get the IO stats as a dictionary.
1639
Gabe Black3b567202015-09-23 14:07:59 -07001640 Returns:
1641 A dictionary of IO stats collected by psutil.
Dan Shiafd0e492015-05-27 14:23:51 -07001642 """
1643 return {'disk_read_bytes_per_second': self.disk_read_bytes_per_sec,
1644 'disk_write_bytes_per_second': self.disk_write_bytes_per_sec,
1645 'disk_total_bytes_per_second': (self.disk_read_bytes_per_sec +
1646 self.disk_write_bytes_per_sec),
1647 'network_sent_bytes_per_second': self.network_sent_bytes_per_sec,
1648 'network_recv_bytes_per_second': self.network_recv_bytes_per_sec,
1649 'network_total_bytes_per_second': (self.network_sent_bytes_per_sec +
1650 self.network_recv_bytes_per_sec),
1651 'cpu_percent': psutil.cpu_percent(),}
1652
Dan Shi7247f9c2016-06-01 09:19:09 -07001653
1654 def _get_process_count(self, process_cmd_pattern):
1655 """Get the count of processes that match the given command pattern.
1656
1657 Args:
1658 process_cmd_pattern: The regex pattern of process command to match.
1659
1660 Returns:
1661 The count of processes that match the given command pattern.
1662 """
1663 try:
xixuanac89ce82016-11-30 16:48:20 -08001664 # Use Popen instead of check_output since the latter cannot run with old
1665 # python version (less than 2.7)
1666 proc = subprocess.Popen(
1667 'pgrep -fc "%s"' % process_cmd_pattern,
1668 stdout=subprocess.PIPE,
1669 stderr=subprocess.PIPE,
1670 shell=True)
1671 cmd_output, cmd_error = proc.communicate()
1672 if cmd_error:
1673 _Log('Error happened when getting process count: %s' % cmd_error)
1674
1675 return int(cmd_output)
Dan Shi7247f9c2016-06-01 09:19:09 -07001676 except subprocess.CalledProcessError:
1677 return 0
1678
1679
Dan Shif5ce2de2013-04-25 16:06:32 -07001680 @cherrypy.expose
1681 def check_health(self):
1682 """Collect the health status of devserver to see if it's ready for staging.
1683
Gilad Arnold452fd272014-02-04 11:09:28 -08001684 Returns:
1685 A JSON dictionary containing all or some of the following fields:
1686 free_disk (int): free disk space in GB
1687 staging_thread_count (int): number of devserver threads currently staging
1688 an image
Dan Shi7247f9c2016-06-01 09:19:09 -07001689 apache_client_count (int): count of Apache processes.
1690 telemetry_test_count (int): count of telemetry tests.
1691 gsutil_count (int): count of gsutil processes.
Dan Shif5ce2de2013-04-25 16:06:32 -07001692 """
1693 # Get free disk space.
1694 stat = os.statvfs(updater.static_dir)
1695 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
Dan Shi7247f9c2016-06-01 09:19:09 -07001696 apache_client_count = self._get_process_count('apache')
1697 telemetry_test_count = self._get_process_count('python.*telemetry')
1698 gsutil_count = self._get_process_count('gsutil')
xixuan447ad9d2017-02-28 14:46:20 -08001699 au_process_count = len(cros_update_progress.GetAllRunningAUProcess())
Dan Shif5ce2de2013-04-25 16:06:32 -07001700
Dan Shiafd0e492015-05-27 14:23:51 -07001701 health_data = {
Dan Shif5ce2de2013-04-25 16:06:32 -07001702 'free_disk': free_disk,
Dan Shid76e6bb2016-01-28 22:28:51 -08001703 'staging_thread_count': DevServerRoot._staging_thread_count,
1704 'apache_client_count': apache_client_count,
Dan Shi7247f9c2016-06-01 09:19:09 -07001705 'telemetry_test_count': telemetry_test_count,
xixuan447ad9d2017-02-28 14:46:20 -08001706 'gsutil_count': gsutil_count,
1707 'au_process_count': au_process_count,
1708 }
Dan Shiafd0e492015-05-27 14:23:51 -07001709 health_data.update(self._get_io_stats() or {})
1710
1711 return json.dumps(health_data)
Dan Shif5ce2de2013-04-25 16:06:32 -07001712
1713
Chris Sosadbc20082012-12-10 13:39:11 -08001714def _CleanCache(cache_dir, wipe):
1715 """Wipes any excess cached items in the cache_dir.
1716
1717 Args:
1718 cache_dir: the directory we are wiping from.
1719 wipe: If True, wipe all the contents -- not just the excess.
1720 """
1721 if wipe:
1722 # Clear the cache and exit on error.
1723 cmd = 'rm -rf %s/*' % cache_dir
1724 if os.system(cmd) != 0:
1725 _Log('Failed to clear the cache with %s' % cmd)
1726 sys.exit(1)
1727 else:
1728 # Clear all but the last N cached updates
1729 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
1730 (cache_dir, CACHED_ENTRIES))
1731 if os.system(cmd) != 0:
1732 _Log('Failed to clean up old delta cache files with %s' % cmd)
1733 sys.exit(1)
1734
1735
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001736def _AddTestingOptions(parser):
1737 group = optparse.OptionGroup(
1738 parser, 'Advanced Testing Options', 'These are used by test scripts and '
1739 'developers writing integration tests utilizing the devserver. They are '
1740 'not intended to be really used outside the scope of someone '
1741 'knowledgable about the test.')
1742 group.add_option('--exit',
1743 action='store_true',
1744 help='do not start the server (yet pregenerate/clear cache)')
1745 group.add_option('--host_log',
1746 action='store_true', default=False,
1747 help='record history of host update events (/api/hostlog)')
1748 group.add_option('--max_updates',
Gabe Black3b567202015-09-23 14:07:59 -07001749 metavar='NUM', default=-1, type='int',
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001750 help='maximum number of update checks handled positively '
1751 '(default: unlimited)')
1752 group.add_option('--private_key',
1753 metavar='PATH', default=None,
1754 help='path to the private key in pem format. If this is set '
1755 'the devserver will generate update payloads that are '
1756 'signed with this key.')
David Zeuthen52ccd012013-10-31 12:58:26 -07001757 group.add_option('--private_key_for_metadata_hash_signature',
1758 metavar='PATH', default=None,
1759 help='path to the private key in pem format. If this is set '
1760 'the devserver will sign the metadata hash with the given '
1761 'key and transmit in the Omaha-style XML response.')
1762 group.add_option('--public_key',
1763 metavar='PATH', default=None,
1764 help='path to the public key in pem format. If this is set '
1765 'the devserver will transmit a base64 encoded version of '
1766 'the content in the Omaha-style XML response.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001767 group.add_option('--proxy_port',
1768 metavar='PORT', default=None, type='int',
1769 help='port to have the client connect to -- basically the '
1770 'devserver lies to the update to tell it to get the payload '
1771 'from a different port that will proxy the request back to '
1772 'the devserver. The proxy must be managed outside the '
1773 'devserver.')
1774 group.add_option('--remote_payload',
1775 action='store_true', default=False,
Chris Sosa4b951602014-04-09 20:26:07 -07001776 help='Payload is being served from a remote machine. With '
1777 'this setting enabled, this devserver instance serves as '
1778 'just an Omaha server instance. In this mode, the '
1779 'devserver enforces a few extra components of the Omaha '
Chris Sosafc715442014-04-09 20:45:23 -07001780 'protocol, such as hardware class, being sent.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001781 group.add_option('-u', '--urlbase',
1782 metavar='URL',
Gabe Black3b567202015-09-23 14:07:59 -07001783 help='base URL for update images, other than the '
1784 'devserver. Use in conjunction with remote_payload.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001785 parser.add_option_group(group)
1786
1787
1788def _AddUpdateOptions(parser):
1789 group = optparse.OptionGroup(
1790 parser, 'Autoupdate Options', 'These options can be used to change '
1791 'how the devserver either generates or serve update payloads. Please '
1792 'note that all of these option affect how a payload is generated and so '
1793 'do not work in archive-only mode.')
1794 group.add_option('--board',
1795 help='By default the devserver will create an update '
1796 'payload from the latest image built for the board '
1797 'a device that is requesting an update has. When we '
1798 'pre-generate an update (see below) and we do not specify '
1799 'another update_type option like image or payload, the '
1800 'devserver needs to know the board to generate the latest '
1801 'image for. This is that board.')
1802 group.add_option('--critical_update',
1803 action='store_true', default=False,
1804 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001805 group.add_option('--image',
1806 metavar='FILE',
1807 help='Generate and serve an update using this image to any '
1808 'device that requests an update.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001809 group.add_option('--payload',
1810 metavar='PATH',
1811 help='use the update payload from specified directory '
1812 '(update.gz).')
1813 group.add_option('-p', '--pregenerate_update',
1814 action='store_true', default=False,
1815 help='pre-generate the update payload before accepting '
1816 'update requests. Useful to help debug payload generation '
1817 'issues quickly. Also if an update payload will take a '
1818 'long time to generate, a client may timeout if you do not'
1819 'pregenerate the update.')
1820 group.add_option('--src_image',
1821 metavar='PATH', default='',
1822 help='If specified, delta updates will be generated using '
1823 'this image as the source image. Delta updates are when '
1824 'you are updating from a "source image" to a another '
1825 'image.')
1826 parser.add_option_group(group)
1827
1828
1829def _AddProductionOptions(parser):
1830 group = optparse.OptionGroup(
1831 parser, 'Advanced Server Options', 'These options can be used to changed '
1832 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001833 group.add_option('--clear_cache',
1834 action='store_true', default=False,
1835 help='At startup, removes all cached entries from the'
1836 'devserver\'s cache.')
1837 group.add_option('--logfile',
1838 metavar='PATH',
1839 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -07001840 group.add_option('--pidfile',
1841 metavar='PATH',
1842 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 11:04:13 -08001843 group.add_option('--portfile',
1844 metavar='PATH',
1845 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001846 group.add_option('--production',
1847 action='store_true', default=False,
1848 help='have the devserver use production values when '
1849 'starting up. This includes using more threads and '
1850 'performing less logging.')
1851 parser.add_option_group(group)
1852
1853
Paul Hobbsef4e0702016-06-27 17:01:42 -07001854def MakeLogHandler(logfile):
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001855 """Create a LogHandler instance used to log all messages."""
1856 hdlr_cls = handlers.TimedRotatingFileHandler
1857 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
xixuan3d48bff2017-01-30 19:00:09 -08001858 interval=_LOG_ROTATION_INTERVAL,
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001859 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -07001860 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001861 return hdlr
1862
1863
Chris Sosacde6bf42012-05-31 18:36:39 -07001864def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001865 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -08001866 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -07001867
1868 # get directory that the devserver is run from
1869 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -07001870 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -07001871 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001872 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -07001873 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -07001874 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001875 parser.add_option('--port',
1876 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 13:13:28 -08001877 help=('port for the dev server to use; if zero, binds to '
1878 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001879 parser.add_option('-t', '--test_image',
1880 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -07001881 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -07001882 parser.add_option('-x', '--xbuddy_manage_builds',
1883 action='store_true',
1884 default=False,
1885 help='If set, allow xbuddy to manage images in'
1886 'build/images.')
Dan Shi72b16132015-10-08 12:10:33 -07001887 parser.add_option('-a', '--android_build_credential',
1888 default=None,
1889 help='Path to a json file which contains the credential '
1890 'needed to access Android builds.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001891 _AddProductionOptions(parser)
1892 _AddUpdateOptions(parser)
1893 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -07001894 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001895
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001896 # Handle options that must be set globally in cherrypy. Do this
1897 # work up front, because calls to _Log() below depend on this
1898 # initialization.
1899 if options.production:
1900 cherrypy.config.update({'environment': 'production'})
1901 if not options.logfile:
1902 cherrypy.config.update({'log.screen': True})
1903 else:
1904 cherrypy.config.update({'log.error_file': '',
1905 'log.access_file': ''})
Paul Hobbsef4e0702016-06-27 17:01:42 -07001906 hdlr = MakeLogHandler(options.logfile)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001907 # Pylint can't seem to process these two calls properly
1908 # pylint: disable=E1101
1909 cherrypy.log.access_log.addHandler(hdlr)
1910 cherrypy.log.error_log.addHandler(hdlr)
1911 # pylint: enable=E1101
1912
joychened64b222013-06-21 16:39:34 -07001913 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001914 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001915
joychened64b222013-06-21 16:39:34 -07001916 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001917 # If our devserver is only supposed to serve payloads, we shouldn't be
1918 # mucking with the cache at all. If the devserver hadn't previously
1919 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001920 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001921 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001922 else:
1923 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001924
Chris Sosadbc20082012-12-10 13:39:11 -08001925 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 16:39:34 -07001926 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001927
joychen121fc9b2013-08-02 14:30:30 -07001928 _xbuddy = xbuddy.XBuddy(options.xbuddy_manage_builds,
1929 options.board,
joychen121fc9b2013-08-02 14:30:30 -07001930 static_dir=options.static_dir)
Chris Sosa75490802013-09-30 17:21:45 -07001931 if options.clear_cache and options.xbuddy_manage_builds:
1932 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 14:30:30 -07001933
Chris Sosa6a3697f2013-01-29 16:44:43 -08001934 # We allow global use here to share with cherrypy classes.
1935 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001936 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001937 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001938 _xbuddy,
joychened64b222013-06-21 16:39:34 -07001939 static_dir=options.static_dir,
Andrew de los Reyes52620802010-04-12 13:40:07 -07001940 urlbase=options.urlbase,
Chris Sosa5d342a22010-09-28 16:54:41 -07001941 forced_image=options.image,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001942 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001943 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-28 23:42:37 -07001944 src_image=options.src_image,
Chris Sosa08d55a22011-01-19 16:08:02 -08001945 board=options.board,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001946 copy_to_static_root=not options.exit,
1947 private_key=options.private_key,
Gabe Black3b567202015-09-23 14:07:59 -07001948 private_key_for_metadata_hash_signature=(
1949 options.private_key_for_metadata_hash_signature),
David Zeuthen52ccd012013-10-31 12:58:26 -07001950 public_key=options.public_key,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001951 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001952 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001953 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001954 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001955 )
Chris Sosa7c931362010-10-11 19:49:01 -07001956
Chris Sosa6a3697f2013-01-29 16:44:43 -08001957 if options.pregenerate_update:
1958 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 15:46:22 -07001959
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001960 if options.exit:
1961 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001962
joychen3cb228e2013-06-12 12:13:13 -07001963 dev_server = DevServerRoot(_xbuddy)
1964
Gilad Arnold11fbef42014-02-10 11:04:13 -08001965 # Patch CherryPy to support binding to any available port (--port=0).
1966 cherrypy_ext.ZeroPortPatcher.DoPatch(cherrypy)
1967
Chris Sosa855b8932013-08-21 13:24:55 -07001968 if options.pidfile:
1969 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1970
Gilad Arnold11fbef42014-02-10 11:04:13 -08001971 if options.portfile:
1972 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1973
Dan Shiafd5c6c2016-01-07 10:27:03 -08001974 if (options.android_build_credential and
1975 os.path.exists(options.android_build_credential)):
1976 try:
1977 with open(options.android_build_credential) as f:
1978 android_build.BuildAccessor.credential_info = json.load(f)
1979 except ValueError as e:
1980 _Log('Failed to load the android build credential: %s. Error: %s.' %
1981 (options.android_build_credential, e))
joychen3cb228e2013-06-12 12:13:13 -07001982 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001983
1984
1985if __name__ == '__main__':
1986 main()