blob: 130ce1099a73931b453302611a6d8b559eda12af [file] [log] [blame]
David Riley2fcb0122017-11-02 11:25:39 -07001#!/usr/bin/env python2
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -07002# -*- coding: utf-8 -*-
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
Mike Frysingeraa0cb102019-02-25 01:09:19 -050027and generate a payload that the requested system can autoupdate to.
Chris Sosa3ae4dc12013-03-29 11:47:00 -070028
29For autoupdates, there are many more advanced options that can help specify
30how to update and which payload to give to a requester.
31"""
32
Gabe Black3b567202015-09-23 14:07:59 -070033from __future__ import print_function
Chris Sosa7c931362010-10-11 19:49:01 -070034
Amin Hassani08e42d22019-06-03 00:31:30 -070035import httplib
Gilad Arnold55a2a372012-10-02 09:46:32 -070036import json
David Riley2fcb0122017-11-02 11:25:39 -070037import optparse # pylint: disable=deprecated-module
rtc@google.comded22402009-10-26 22:36:21 +000038import os
Scott Zawalski4647ce62012-01-03 17:17:28 -050039import re
Simran Basi4baad082013-02-14 13:39:18 -080040import shutil
xixuan52c2fba2016-05-20 17:02:48 -070041import signal
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -080042import socket
Chris Masone816e38c2012-05-02 12:22:36 -070043import subprocess
J. Richard Barnette3d977b82013-04-23 11:05:19 -070044import sys
Chris Masone816e38c2012-05-02 12:22:36 -070045import tempfile
Dan Shi59ae7092013-06-04 14:37:27 -070046import threading
Dan Shiafd0e492015-05-27 14:23:51 -070047import time
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -070048import types
J. Richard Barnette3d977b82013-04-23 11:05:19 -070049from logging import handlers
50
51import cherrypy
David Riley2fcb0122017-11-02 11:25:39 -070052# pylint: disable=no-name-in-module
Chris Sosa855b8932013-08-21 13:24:55 -070053from cherrypy import _cplogging as cplogging
David Riley2fcb0122017-11-02 11:25:39 -070054from cherrypy.process import plugins # pylint: disable=import-error
55# pylint: enable=no-name-in-module
rtc@google.comded22402009-10-26 22:36:21 +000056
Richard Barnettedf35c322017-08-18 17:02:13 -070057# This must happen before any local modules get a chance to import
58# anything from chromite. Otherwise, really bad things will happen, and
59# you will _not_ understand why.
60import setup_chromite # pylint: disable=unused-import
61
Chris Sosa0356d3b2010-09-16 15:46:22 -070062import autoupdate
Dan Shi2f136862016-02-11 15:38:38 -080063import artifact_info
Chris Sosa75490802013-09-30 17:21:45 -070064import build_artifact
Gilad Arnold11fbef42014-02-10 11:04:13 -080065import cherrypy_ext
Gilad Arnoldc65330c2012-09-20 15:17:48 -070066import common_util
Simran Basief83d6a2014-08-28 14:32:01 -070067import devserver_constants
Chris Sosa47a7d4e2012-03-28 11:26:55 -070068import downloader
Gilad Arnoldc65330c2012-09-20 15:17:48 -070069import log_util
joychen3cb228e2013-06-12 12:13:13 -070070import xbuddy
Gilad Arnoldc65330c2012-09-20 15:17:48 -070071
Gilad Arnoldc65330c2012-09-20 15:17:48 -070072# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080073def _Log(message, *args):
74 return log_util.LogWithTag('DEVSERVER', message, *args)
Chris Sosa0356d3b2010-09-16 15:46:22 -070075
Dan Shiafd0e492015-05-27 14:23:51 -070076try:
77 import psutil
78except ImportError:
79 # Ignore psutil import failure. This is for backwards compatibility, so
80 # "cros flash" can still update duts with build without psutil installed.
81 # The reason is that, during cros flash, local devserver code is copied over
82 # to DUT, and devserver will be running inside DUT to stage the build.
83 _Log('Python module psutil is not installed, devserver load data will not be '
84 'collected')
85 psutil = None
Dan Shi94dcbe82015-06-08 20:51:13 -070086except OSError as e:
87 # Ignore error like following. psutil may not work properly in builder. Ignore
88 # the error as load information of devserver is not used in builder.
89 # OSError: [Errno 2] No such file or directory: '/dev/pts/0'
90 _Log('psutil is failed to be imported, error: %s. devserver load data will '
91 'not be collected.', e)
92 psutil = None
93
xixuanac89ce82016-11-30 16:48:20 -080094# Use try-except to skip unneccesary import for simple use case, eg. running
95# devserver on host.
96try:
97 import cros_update
xixuanac89ce82016-11-30 16:48:20 -080098except ImportError as e:
99 _Log('cros_update cannot be imported: %r', e)
100 cros_update = None
xixuana4f4e712017-05-08 15:17:54 -0700101
102try:
103 import cros_update_progress
104except ImportError as e:
105 _Log('cros_update_progress cannot be imported: %r', e)
xixuanac89ce82016-11-30 16:48:20 -0800106 cros_update_progress = None
107
xixuanac89ce82016-11-30 16:48:20 -0800108try:
Dan Shi72b16132015-10-08 12:10:33 -0700109 import android_build
110except ImportError as e:
111 # Ignore android_build import failure. This is to support devserver running
112 # inside a ChromeOS device triggered by cros flash. Most ChromeOS test images
113 # do not have google-api-python-client module and they don't need to support
114 # Android updating, therefore, ignore the import failure here.
Dan Shi72b16132015-10-08 12:10:33 -0700115 android_build = None
Frank Farzan40160872011-12-12 18:39:18 -0800116
Chris Sosa417e55d2011-01-25 16:40:48 -0800117CACHED_ENTRIES = 12
Don Garrettf90edf02010-11-16 17:36:14 -0800118
Simran Basi4baad082013-02-14 13:39:18 -0800119TELEMETRY_FOLDER = 'telemetry_src'
120TELEMETRY_DEPS = ['dep-telemetry_dep.tar.bz2',
121 'dep-page_cycler_dep.tar.bz2',
Simran Basi0d078682013-03-22 16:40:04 -0700122 'dep-chrome_test.tar.bz2',
123 'dep-perf_data_dep.tar.bz2']
Simran Basi4baad082013-02-14 13:39:18 -0800124
Chris Sosa0356d3b2010-09-16 15:46:22 -0700125# Sets up global to share between classes.
rtc@google.com21a5ca32009-11-04 18:23:23 +0000126updater = None
rtc@google.comded22402009-10-26 22:36:21 +0000127
xixuan3d48bff2017-01-30 19:00:09 -0800128# Log rotation parameters. These settings correspond to twice a day once
129# devserver is started, with about two weeks (28 backup files) of old logs
130# kept for backup.
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700131#
xixuan3d48bff2017-01-30 19:00:09 -0800132# For more, see the documentation in standard python library for
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700133# logging.handlers.TimedRotatingFileHandler
xixuan3d48bff2017-01-30 19:00:09 -0800134_LOG_ROTATION_TIME = 'H'
135_LOG_ROTATION_INTERVAL = 12 # hours
136_LOG_ROTATION_BACKUP = 28 # backup counts
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700137
Dan Shiafd0e492015-05-27 14:23:51 -0700138# Number of seconds between the collection of disk and network IO counters.
139STATS_INTERVAL = 10.0
Frank Farzan40160872011-12-12 18:39:18 -0800140
xixuan52c2fba2016-05-20 17:02:48 -0700141# Auto-update parameters
142
143# Error msg for missing key in CrOS auto-update.
Xixuan Wu32af9f12017-11-13 14:11:44 -0800144KEY_ERROR_MSG = 'Key Error in RPC: %s= is required'
xixuan52c2fba2016-05-20 17:02:48 -0700145
146# Command of running auto-update.
147AUTO_UPDATE_CMD = '/usr/bin/python -u %s -d %s -b %s --static_dir %s'
148
149
Chris Sosa9164ca32012-03-28 11:04:50 -0700150class DevServerError(Exception):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700151 """Exception class used by this module."""
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700152
153
Dan Shiafd0e492015-05-27 14:23:51 -0700154def require_psutil():
Gabe Black3b567202015-09-23 14:07:59 -0700155 """Decorator for functions require psutil to run."""
Dan Shiafd0e492015-05-27 14:23:51 -0700156 def deco_require_psutil(func):
157 """Wrapper of the decorator function.
158
Gabe Black3b567202015-09-23 14:07:59 -0700159 Args:
160 func: function to be called.
Dan Shiafd0e492015-05-27 14:23:51 -0700161 """
162 def func_require_psutil(*args, **kwargs):
163 """Decorator for functions require psutil to run.
164
165 If psutil is not installed, skip calling the function.
166
Gabe Black3b567202015-09-23 14:07:59 -0700167 Args:
168 *args: arguments for function to be called.
169 **kwargs: keyword arguments for function to be called.
Dan Shiafd0e492015-05-27 14:23:51 -0700170 """
171 if psutil:
172 return func(*args, **kwargs)
173 else:
174 _Log('Python module psutil is not installed. Function call %s is '
175 'skipped.' % func)
176 return func_require_psutil
177 return deco_require_psutil
178
179
Gabe Black3b567202015-09-23 14:07:59 -0700180def _canonicalize_archive_url(archive_url):
181 """Canonicalizes archive_url strings.
182
183 Raises:
184 DevserverError: if archive_url is not set.
185 """
186 if archive_url:
187 if not archive_url.startswith('gs://'):
188 raise DevServerError("Archive URL isn't from Google Storage (%s) ." %
189 archive_url)
190
191 return archive_url.rstrip('/')
192 else:
193 raise DevServerError("Must specify an archive_url in the request")
194
195
196def _canonicalize_local_path(local_path):
197 """Canonicalizes |local_path| strings.
198
199 Raises:
200 DevserverError: if |local_path| is not set.
201 """
202 # Restrict staging of local content to only files within the static
203 # directory.
204 local_path = os.path.abspath(local_path)
205 if not local_path.startswith(updater.static_dir):
206 raise DevServerError('Local path %s must be a subdirectory of the static'
207 ' directory: %s' % (local_path, updater.static_dir))
208
209 return local_path.rstrip('/')
210
211
212def _get_artifacts(kwargs):
213 """Returns a tuple of named and file artifacts given the stage rpc kwargs.
214
215 Raises:
216 DevserverError if no artifacts would be returned.
217 """
218 artifacts = kwargs.get('artifacts')
219 files = kwargs.get('files')
220 if not artifacts and not files:
221 raise DevServerError('No artifacts specified.')
222
223 # Note we NEED to coerce files to a string as we get raw unicode from
224 # cherrypy and we treat files as strings elsewhere in the code.
225 return (str(artifacts).split(',') if artifacts else [],
226 str(files).split(',') if files else [])
227
228
Dan Shi61305df2015-10-26 16:52:35 -0700229def _is_android_build_request(kwargs):
230 """Check if a devserver call is for Android build, based on the arguments.
231
232 This method exams the request's arguments (os_type) to determine if the
233 request is for Android build. If os_type is set to `android`, returns True.
234 If os_type is not set or has other values, returns False.
235
236 Args:
237 kwargs: Keyword arguments for the request.
238
239 Returns:
240 True if the request is for Android build. False otherwise.
241 """
242 os_type = kwargs.get('os_type', None)
243 return os_type == 'android'
244
245
Gabe Black3b567202015-09-23 14:07:59 -0700246def _get_downloader(kwargs):
247 """Returns the downloader based on passed in arguments.
248
249 Args:
Amin Hassani08e42d22019-06-03 00:31:30 -0700250 kwargs: Keyword arguments for the request.
Gabe Black3b567202015-09-23 14:07:59 -0700251 """
252 local_path = kwargs.get('local_path')
253 if local_path:
254 local_path = _canonicalize_local_path(local_path)
255
256 dl = None
257 if local_path:
Prathmesh Prabhu58d08932018-01-19 15:08:19 -0800258 delete_source = _parse_boolean_arg(kwargs, 'delete_source')
259 dl = downloader.LocalDownloader(updater.static_dir, local_path,
260 delete_source=delete_source)
Gabe Black3b567202015-09-23 14:07:59 -0700261
Dan Shi61305df2015-10-26 16:52:35 -0700262 if not _is_android_build_request(kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700263 archive_url = kwargs.get('archive_url')
264 if not archive_url and not local_path:
265 raise DevServerError('Requires archive_url or local_path to be '
266 'specified.')
267 if archive_url and local_path:
268 raise DevServerError('archive_url and local_path can not both be '
269 'specified.')
270 if not dl:
271 archive_url = _canonicalize_archive_url(archive_url)
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700272 dl = downloader.GoogleStorageDownloader(
273 updater.static_dir, archive_url,
274 downloader.GoogleStorageDownloader.GetBuildIdFromArchiveURL(
275 archive_url))
Gabe Black3b567202015-09-23 14:07:59 -0700276 elif not dl:
277 target = kwargs.get('target', None)
Dan Shi72b16132015-10-08 12:10:33 -0700278 branch = kwargs.get('branch', None)
Dan Shi61305df2015-10-26 16:52:35 -0700279 build_id = kwargs.get('build_id', None)
280 if not target or not branch or not build_id:
Dan Shi72b16132015-10-08 12:10:33 -0700281 raise DevServerError(
Dan Shi61305df2015-10-26 16:52:35 -0700282 'target, branch, build ID must all be specified for downloading '
283 'Android build.')
Dan Shi72b16132015-10-08 12:10:33 -0700284 dl = downloader.AndroidBuildDownloader(updater.static_dir, branch, build_id,
285 target)
Gabe Black3b567202015-09-23 14:07:59 -0700286
287 return dl
288
289
290def _get_downloader_and_factory(kwargs):
291 """Returns the downloader and artifact factory based on passed in arguments.
292
293 Args:
Amin Hassani08e42d22019-06-03 00:31:30 -0700294 kwargs: Keyword arguments for the request.
Gabe Black3b567202015-09-23 14:07:59 -0700295 """
296 artifacts, files = _get_artifacts(kwargs)
297 dl = _get_downloader(kwargs)
298
299 if (isinstance(dl, downloader.GoogleStorageDownloader) or
300 isinstance(dl, downloader.LocalDownloader)):
301 factory_class = build_artifact.ChromeOSArtifactFactory
Dan Shi72b16132015-10-08 12:10:33 -0700302 elif isinstance(dl, downloader.AndroidBuildDownloader):
Gabe Black3b567202015-09-23 14:07:59 -0700303 factory_class = build_artifact.AndroidArtifactFactory
304 else:
305 raise DevServerError('Unrecognized value for downloader type: %s' %
306 type(dl))
307
308 factory = factory_class(dl.GetBuildDir(), artifacts, files, dl.GetBuild())
309
310 return dl, factory
311
312
Scott Zawalski4647ce62012-01-03 17:17:28 -0500313def _LeadingWhiteSpaceCount(string):
314 """Count the amount of leading whitespace in a string.
315
316 Args:
317 string: The string to count leading whitespace in.
Don Garrettf84631a2014-01-07 18:21:26 -0800318
Scott Zawalski4647ce62012-01-03 17:17:28 -0500319 Returns:
320 number of white space chars before characters start.
321 """
Gabe Black3b567202015-09-23 14:07:59 -0700322 matched = re.match(r'^\s+', string)
Scott Zawalski4647ce62012-01-03 17:17:28 -0500323 if matched:
324 return len(matched.group())
325
326 return 0
327
328
329def _PrintDocStringAsHTML(func):
330 """Make a functions docstring somewhat HTML style.
331
332 Args:
333 func: The function to return the docstring from.
Don Garrettf84631a2014-01-07 18:21:26 -0800334
Scott Zawalski4647ce62012-01-03 17:17:28 -0500335 Returns:
336 A string that is somewhat formated for a web browser.
337 """
338 # TODO(scottz): Make this parse Args/Returns in a prettier way.
339 # Arguments could be bolded and indented etc.
340 html_doc = []
341 for line in func.__doc__.splitlines():
342 leading_space = _LeadingWhiteSpaceCount(line)
343 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700344 line = ' ' * leading_space + line
Scott Zawalski4647ce62012-01-03 17:17:28 -0500345
346 html_doc.append('<BR>%s' % line)
347
348 return '\n'.join(html_doc)
349
350
Simran Basief83d6a2014-08-28 14:32:01 -0700351def _GetUpdateTimestampHandler(static_dir):
352 """Returns a handler to update directory staged.timestamp.
353
354 This handler resets the stage.timestamp whenever static content is accessed.
355
356 Args:
357 static_dir: Directory from which static content is being staged.
358
359 Returns:
Amin Hassani08e42d22019-06-03 00:31:30 -0700360 A cherrypy handler to update the timestamp of accessed content.
Simran Basief83d6a2014-08-28 14:32:01 -0700361 """
362 def UpdateTimestampHandler():
363 if not '404' in cherrypy.response.status:
364 build_match = re.match(devserver_constants.STAGED_BUILD_REGEX,
365 cherrypy.request.path_info)
366 if build_match:
367 build_dir = os.path.join(static_dir, build_match.group('build'))
368 downloader.Downloader.TouchTimestampForStaged(build_dir)
369 return UpdateTimestampHandler
370
371
Chris Sosa7c931362010-10-11 19:49:01 -0700372def _GetConfig(options):
373 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800374
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800375 socket_host = '::'
Yu-Ju Hongc8d4af32013-11-12 15:14:26 -0800376 # Fall back to IPv4 when python is not configured with IPv6.
377 if not socket.has_ipv6:
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800378 socket_host = '0.0.0.0'
379
Simran Basief83d6a2014-08-28 14:32:01 -0700380 # Adds the UpdateTimestampHandler to cherrypy's tools. This tools executes
381 # on the on_end_resource hook. This hook is called once processing is
382 # complete and the response is ready to be returned.
383 cherrypy.tools.update_timestamp = cherrypy.Tool(
384 'on_end_resource', _GetUpdateTimestampHandler(options.static_dir))
385
David Riley2fcb0122017-11-02 11:25:39 -0700386 base_config = {
387 'global': {
388 'server.log_request_headers': True,
389 'server.protocol_version': 'HTTP/1.1',
390 'server.socket_host': socket_host,
391 'server.socket_port': int(options.port),
392 'response.timeout': 6000,
393 'request.show_tracebacks': True,
394 'server.socket_timeout': 60,
395 'server.thread_pool': 2,
396 'engine.autoreload.on': False,
397 },
398 '/api': {
399 # Gets rid of cherrypy parsing post file for args.
400 'request.process_request_body': False,
401 },
402 '/build': {
403 'response.timeout': 100000,
404 },
405 '/update': {
406 # Gets rid of cherrypy parsing post file for args.
407 'request.process_request_body': False,
408 'response.timeout': 10000,
409 },
410 # Sets up the static dir for file hosting.
411 '/static': {
412 'tools.staticdir.dir': options.static_dir,
413 'tools.staticdir.on': True,
414 'response.timeout': 10000,
415 'tools.update_timestamp.on': True,
416 },
417 }
Chris Sosa5f118ef2012-07-12 11:37:50 -0700418 if options.production:
Alex Miller93beca52013-07-30 19:25:09 -0700419 base_config['global'].update({'server.thread_pool': 150})
Scott Zawalski1c5e7cd2012-02-27 13:12:52 -0500420
Chris Sosa7c931362010-10-11 19:49:01 -0700421 return base_config
rtc@google.com64244662009-11-12 00:52:08 +0000422
Darin Petkove17164a2010-08-11 13:24:41 -0700423
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700424def _GetRecursiveMemberObject(root, member_list):
425 """Returns an object corresponding to a nested member list.
426
427 Args:
428 root: the root object to search
429 member_list: list of nested members to search
Don Garrettf84631a2014-01-07 18:21:26 -0800430
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700431 Returns:
432 An object corresponding to the member name list; None otherwise.
433 """
434 for member in member_list:
435 next_root = root.__class__.__dict__.get(member)
436 if not next_root:
437 return None
438 root = next_root
439 return root
440
441
442def _IsExposed(name):
443 """Returns True iff |name| has an `exposed' attribute and it is set."""
444 return hasattr(name, 'exposed') and name.exposed
445
446
Gilad Arnold748c8322012-10-12 09:51:35 -0700447def _GetExposedMethod(root, nested_member, ignored=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700448 """Returns a CherryPy-exposed method, if such exists.
449
450 Args:
451 root: the root object for searching
452 nested_member: a slash-joined path to the nested member
453 ignored: method paths to be ignored
Don Garrettf84631a2014-01-07 18:21:26 -0800454
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700455 Returns:
456 A function object corresponding to the path defined by |member_list| from
457 the |root| object, if the function is exposed and not ignored; None
458 otherwise.
459 """
Gilad Arnold748c8322012-10-12 09:51:35 -0700460 method = (not (ignored and nested_member in ignored) and
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700461 _GetRecursiveMemberObject(root, nested_member.split('/')))
Amin Hassani08e42d22019-06-03 00:31:30 -0700462 if method and isinstance(method, types.FunctionType) and _IsExposed(method):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700463 return method
464
465
Gilad Arnold748c8322012-10-12 09:51:35 -0700466def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700467 """Finds exposed CherryPy methods.
468
469 Args:
470 root: the root object for searching
471 prefix: slash-joined chain of members leading to current object
472 unlisted: URLs to be excluded regardless of their exposed status
Don Garrettf84631a2014-01-07 18:21:26 -0800473
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700474 Returns:
475 List of exposed URLs that are not unlisted.
476 """
477 method_list = []
478 for member in sorted(root.__class__.__dict__.keys()):
479 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 09:51:35 -0700480 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700481 continue
482 member_obj = root.__class__.__dict__[member]
483 if _IsExposed(member_obj):
Amin Hassani08e42d22019-06-03 00:31:30 -0700484 if isinstance(member_obj, types.FunctionType):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700485 method_list.append(prefixed_member)
486 else:
487 method_list += _FindExposedMethods(
488 member_obj, prefixed_member, unlisted)
489 return method_list
490
491
xixuan52c2fba2016-05-20 17:02:48 -0700492def _check_base_args_for_auto_update(kwargs):
xixuanac89ce82016-11-30 16:48:20 -0800493 """Check basic args required for auto-update.
494
495 Args:
496 kwargs: the parameters to be checked.
497
498 Raises:
499 DevServerHTTPError if required parameters don't exist in kwargs.
500 """
xixuan52c2fba2016-05-20 17:02:48 -0700501 if 'host_name' not in kwargs:
Amin Hassani08e42d22019-06-03 00:31:30 -0700502 raise common_util.DevServerHTTPError(httplib.INTERNAL_SERVER_ERROR,
503 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -0700504
505 if 'build_name' not in kwargs:
Amin Hassani08e42d22019-06-03 00:31:30 -0700506 raise common_util.DevServerHTTPError(httplib.INTERNAL_SERVER_ERROR,
507 KEY_ERROR_MSG % 'build_name')
xixuan52c2fba2016-05-20 17:02:48 -0700508
509
510def _parse_boolean_arg(kwargs, key):
xixuanac89ce82016-11-30 16:48:20 -0800511 """Parse boolean arg from kwargs.
512
513 Args:
514 kwargs: the parameters to be checked.
515 key: the key to be parsed.
516
517 Returns:
518 The boolean value of kwargs[key], or False if key doesn't exist in kwargs.
519
520 Raises:
521 DevServerHTTPError if kwargs[key] is not a boolean variable.
522 """
xixuan52c2fba2016-05-20 17:02:48 -0700523 if key in kwargs:
524 if kwargs[key] == 'True':
525 return True
526 elif kwargs[key] == 'False':
527 return False
528 else:
529 raise common_util.DevServerHTTPError(
Amin Hassani08e42d22019-06-03 00:31:30 -0700530 httplib.INTERNAL_SERVER_ERROR,
xixuan52c2fba2016-05-20 17:02:48 -0700531 'The value for key %s is not boolean.' % key)
532 else:
533 return False
534
xixuan447ad9d2017-02-28 14:46:20 -0800535
xixuanac89ce82016-11-30 16:48:20 -0800536def _parse_string_arg(kwargs, key):
537 """Parse string arg from kwargs.
538
539 Args:
540 kwargs: the parameters to be checked.
541 key: the key to be parsed.
542
543 Returns:
544 The string value of kwargs[key], or None if key doesn't exist in kwargs.
545 """
546 if key in kwargs:
547 return kwargs[key]
548 else:
549 return None
550
xixuan447ad9d2017-02-28 14:46:20 -0800551
xixuanac89ce82016-11-30 16:48:20 -0800552def _build_uri_from_build_name(build_name):
553 """Get build url from a given build name.
554
555 Args:
556 build_name: the build name to be parsed, whose format is
557 'board/release_version'.
558
559 Returns:
560 The release_archive_url on Google Storage for this build name.
561 """
Amin Hassani08e42d22019-06-03 00:31:30 -0700562 # TODO(ahassani): This function doesn't seem to be used anywhere since its
563 # previous use of lib.paygen.gspath was broken and it doesn't seem to be
564 # causing any runtime issues. So deprecate this in the future.
565 tokens = build_name.split('/')
566 return 'gs://chromeos-releases/stable-channel/%s/%s' % (tokens[0], tokens[1])
xixuan52c2fba2016-05-20 17:02:48 -0700567
xixuan447ad9d2017-02-28 14:46:20 -0800568
569def _clear_process(host_name, pid):
570 """Clear AU process for given hostname and pid.
571
572 This clear includes:
573 1. kill process if it's alive.
574 2. delete the track status file of this process.
575 3. delete the executing log file of this process.
576
577 Args:
578 host_name: the host to execute auto-update.
579 pid: the background auto-update process id.
580 """
581 if cros_update_progress.IsProcessAlive(pid):
582 os.killpg(int(pid), signal.SIGKILL)
583
584 cros_update_progress.DelTrackStatusFile(host_name, pid)
585 cros_update_progress.DelExecuteLogFile(host_name, pid)
586
587
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700588class ApiRoot(object):
589 """RESTful API for Dev Server information."""
590 exposed = True
591
592 @cherrypy.expose
593 def hostinfo(self, ip):
594 """Returns a JSON dictionary containing information about the given ip.
595
Gilad Arnold1b908392012-10-05 11:36:27 -0700596 Args:
597 ip: address of host whose info is requested
Don Garrettf84631a2014-01-07 18:21:26 -0800598
Gilad Arnold1b908392012-10-05 11:36:27 -0700599 Returns:
600 A JSON dictionary containing all or some of the following fields:
601 last_event_type (int): last update event type received
602 last_event_status (int): last update event status received
603 last_known_version (string): last known version reported in update ping
604 forced_update_label (string): update label to force next update ping to
605 use, set by setnextupdate
606 See the OmahaEvent class in update_engine/omaha_request_action.h for
607 event type and status code definitions. If the ip does not exist an empty
608 string is returned.
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700609
Gilad Arnold1b908392012-10-05 11:36:27 -0700610 Example URL:
611 http://myhost/api/hostinfo?ip=192.168.1.5
612 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700613 return updater.HandleHostInfoPing(ip)
614
615 @cherrypy.expose
Gilad Arnold286a0062012-01-12 13:47:02 -0800616 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 11:36:27 -0700617 """Returns a JSON object containing a log of host event.
618
619 Args:
620 ip: address of host whose event log is requested, or `all'
Don Garrettf84631a2014-01-07 18:21:26 -0800621
Gilad Arnold1b908392012-10-05 11:36:27 -0700622 Returns:
623 A JSON encoded list (log) of dictionaries (events), each of which
624 containing a `timestamp' and other event fields, as described under
625 /api/hostinfo.
626
627 Example URL:
628 http://myhost/api/hostlog?ip=192.168.1.5
629 """
Gilad Arnold286a0062012-01-12 13:47:02 -0800630 return updater.HandleHostLogPing(ip)
631
632 @cherrypy.expose
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700633 def setnextupdate(self, ip):
634 """Allows the response to the next update ping from a host to be set.
635
636 Takes the IP of the host and an update label as normally provided to the
Gilad Arnold1b908392012-10-05 11:36:27 -0700637 /update command.
638 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700639 body_length = int(cherrypy.request.headers['Content-Length'])
640 label = cherrypy.request.rfile.read(body_length)
641
642 if label:
643 label = label.strip()
644 if label:
645 return updater.HandleSetUpdatePing(ip, label)
Amin Hassani08e42d22019-06-03 00:31:30 -0700646 raise common_util.DevServerHTTPError(httplib.BAD_REQUEST,
647 'No label provided.')
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700648
649
Gilad Arnold55a2a372012-10-02 09:46:32 -0700650 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800651 def fileinfo(self, *args):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700652 """Returns information about a given staged file.
653
654 Args:
Don Garrettf84631a2014-01-07 18:21:26 -0800655 args: path to the file inside the server's static staging directory
656
Gilad Arnold55a2a372012-10-02 09:46:32 -0700657 Returns:
658 A JSON encoded dictionary with information about the said file, which may
659 contain the following keys/values:
Gilad Arnold1b908392012-10-05 11:36:27 -0700660 size (int): the file size in bytes
Gilad Arnold1b908392012-10-05 11:36:27 -0700661 sha256 (string): a base64 encoded SHA256 hash
662
663 Example URL:
664 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 09:46:32 -0700665 """
Don Garrettf84631a2014-01-07 18:21:26 -0800666 file_path = os.path.join(updater.static_dir, *args)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700667 if not os.path.exists(file_path):
668 raise DevServerError('file not found: %s' % file_path)
669 try:
670 file_size = os.path.getsize(file_path)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700671 file_sha256 = common_util.GetFileSha256(file_path)
672 except os.error, e:
673 raise DevServerError('failed to get info for file %s: %s' %
Gilad Arnolde74b3812013-04-22 11:27:38 -0700674 (file_path, e))
675
Gilad Arnolde74b3812013-04-22 11:27:38 -0700676 return json.dumps({
677 autoupdate.Autoupdate.SIZE_ATTR: file_size,
Gilad Arnolde74b3812013-04-22 11:27:38 -0700678 autoupdate.Autoupdate.SHA256_ATTR: file_sha256,
Gilad Arnolde74b3812013-04-22 11:27:38 -0700679 })
Gilad Arnold55a2a372012-10-02 09:46:32 -0700680
Chris Sosa76e44b92013-01-31 12:11:38 -0800681
David Rochberg7c79a812011-01-19 14:24:45 -0500682class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700683 """The Root Class for the Dev Server.
684
685 CherryPy works as follows:
686 For each method in this class, cherrpy interprets root/path
687 as a call to an instance of DevServerRoot->method_name. For example,
688 a call to http://myhost/build will call build. CherryPy automatically
689 parses http args and places them as keyword arguments in each method.
690 For paths http://myhost/update/dir1/dir2, you can use *args so that
691 cherrypy uses the update method and puts the extra paths in args.
692 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700693 # Method names that should not be listed on the index page.
694 _UNLISTED_METHODS = ['index', 'doc']
695
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700696 api = ApiRoot()
Chris Sosa7c931362010-10-11 19:49:01 -0700697
Dan Shi59ae7092013-06-04 14:37:27 -0700698 # Number of threads that devserver is staging images.
699 _staging_thread_count = 0
700 # Lock used to lock increasing/decreasing count.
701 _staging_thread_count_lock = threading.Lock()
702
Dan Shiafd0e492015-05-27 14:23:51 -0700703 @require_psutil()
704 def _refresh_io_stats(self):
705 """A call running in a thread to update IO stats periodically."""
706 prev_disk_io_counters = psutil.disk_io_counters()
707 prev_network_io_counters = psutil.net_io_counters()
708 prev_read_time = time.time()
709 while True:
710 time.sleep(STATS_INTERVAL)
711 now = time.time()
712 interval = now - prev_read_time
713 prev_read_time = now
714 # Disk IO is for all disks.
715 disk_io_counters = psutil.disk_io_counters()
716 network_io_counters = psutil.net_io_counters()
717
718 self.disk_read_bytes_per_sec = (
719 disk_io_counters.read_bytes -
720 prev_disk_io_counters.read_bytes)/interval
721 self.disk_write_bytes_per_sec = (
722 disk_io_counters.write_bytes -
723 prev_disk_io_counters.write_bytes)/interval
724 prev_disk_io_counters = disk_io_counters
725
726 self.network_sent_bytes_per_sec = (
727 network_io_counters.bytes_sent -
728 prev_network_io_counters.bytes_sent)/interval
729 self.network_recv_bytes_per_sec = (
730 network_io_counters.bytes_recv -
731 prev_network_io_counters.bytes_recv)/interval
732 prev_network_io_counters = network_io_counters
733
734 @require_psutil()
735 def _start_io_stat_thread(self):
Gabe Black3b567202015-09-23 14:07:59 -0700736 """Start the thread to collect IO stats."""
Dan Shiafd0e492015-05-27 14:23:51 -0700737 thread = threading.Thread(target=self._refresh_io_stats)
738 thread.daemon = True
739 thread.start()
740
joychen3cb228e2013-06-12 12:13:13 -0700741 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700742 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800743 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 12:13:13 -0700744 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 14:24:45 -0500745
Dan Shiafd0e492015-05-27 14:23:51 -0700746 # Cache of disk IO stats, a thread refresh the stats every 10 seconds.
747 # lock is not used for these variables as the only thread writes to these
748 # variables is _refresh_io_stats.
749 self.disk_read_bytes_per_sec = 0
750 self.disk_write_bytes_per_sec = 0
751 # Cache of network IO stats.
752 self.network_sent_bytes_per_sec = 0
753 self.network_recv_bytes_per_sec = 0
754 self._start_io_stat_thread()
755
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700756 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500757 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700758 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700759 import builder
760 if self._builder is None:
761 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500762 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700763
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700764 @cherrypy.expose
Dan Shif8eb0d12013-08-01 17:52:06 -0700765 def is_staged(self, **kwargs):
766 """Check if artifacts have been downloaded.
767
Amin Hassani08e42d22019-06-03 00:31:30 -0700768 Args:
Chris Sosa6b0c6172013-08-05 17:01:33 -0700769 async: True to return without waiting for download to complete.
770 artifacts: Comma separated list of named artifacts to download.
771 These are defined in artifact_info and have their implementation
772 in build_artifact.py.
773 files: Comma separated list of file artifacts to stage. These
774 will be available as is in the corresponding static directory with no
775 custom post-processing.
776
Amin Hassani08e42d22019-06-03 00:31:30 -0700777 Returns: True of all artifacts are staged.
Dan Shif8eb0d12013-08-01 17:52:06 -0700778
Amin Hassani08e42d22019-06-03 00:31:30 -0700779 Examples:
Dan Shif8eb0d12013-08-01 17:52:06 -0700780 To check if autotest and test_suites are staged:
781 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
782 artifacts=autotest,test_suites
783 """
Gabe Black3b567202015-09-23 14:07:59 -0700784 dl, factory = _get_downloader_and_factory(kwargs)
Aviv Keshet57d18172016-06-18 20:39:09 -0700785 response = str(dl.IsStaged(factory))
786 _Log('Responding to is_staged %s request with %r', kwargs, response)
787 return response
Dan Shi59ae7092013-06-04 14:37:27 -0700788
Chris Sosa76e44b92013-01-31 12:11:38 -0800789 @cherrypy.expose
Prashanth Ba06d2d22014-03-07 15:35:19 -0800790 def list_image_dir(self, **kwargs):
791 """Take an archive url and list the contents in its staged directory.
792
793 Args:
Amin Hassani08e42d22019-06-03 00:31:30 -0700794 archive_url: Google Storage URL for the build.
Prashanth Ba06d2d22014-03-07 15:35:19 -0800795
Amin Hassani08e42d22019-06-03 00:31:30 -0700796 Examples:
Prashanth Ba06d2d22014-03-07 15:35:19 -0800797 To list the contents of where this devserver should have staged
798 gs://image-archive/<board>-release/<build> call:
799 http://devserver_url:<port>/list_image_dir?archive_url=<gs://..>
800
801 Returns:
802 A string with information about the contents of the image directory.
803 """
Gabe Black3b567202015-09-23 14:07:59 -0700804 dl = _get_downloader(kwargs)
Prashanth Ba06d2d22014-03-07 15:35:19 -0800805 try:
Gabe Black3b567202015-09-23 14:07:59 -0700806 image_dir_contents = dl.ListBuildDir()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800807 except build_artifact.ArtifactDownloadError as e:
808 return 'Cannot list the contents of staged artifacts. %s' % e
809 if not image_dir_contents:
Gabe Black3b567202015-09-23 14:07:59 -0700810 return '%s has not been staged on this devserver.' % dl.DescribeSource()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800811 return image_dir_contents
812
813 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800814 def stage(self, **kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700815 """Downloads and caches build artifacts.
Chris Sosa76e44b92013-01-31 12:11:38 -0800816
Gabe Black3b567202015-09-23 14:07:59 -0700817 Downloads and caches build artifacts, possibly from a Google Storage URL,
Dan Shi72b16132015-10-08 12:10:33 -0700818 or from Android's build server. Returns once these have been downloaded
Gabe Black3b567202015-09-23 14:07:59 -0700819 on the devserver. A call to this will attempt to cache non-specified
820 artifacts in the background for the given from the given URL following
821 the principle of spatial locality. Spatial locality of different
Chris Sosa76e44b92013-01-31 12:11:38 -0800822 artifacts is explicitly defined in the build_artifact module.
823
824 These artifacts will then be available from the static/ sub-directory of
825 the devserver.
826
827 Args:
828 archive_url: Google Storage URL for the build.
Simran Basi4243a862014-12-12 12:48:33 -0800829 local_path: Local path for the build.
Prathmesh Prabhu58d08932018-01-19 15:08:19 -0800830 delete_source: Only meaningful with local_path. bool to indicate if the
831 source files should be deleted. This is especially useful when staging
832 a file locally in resource constrained environments as it allows us to
833 move the relevant files locally instead of copying them.
Dan Shif8eb0d12013-08-01 17:52:06 -0700834 async: True to return without waiting for download to complete.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700835 artifacts: Comma separated list of named artifacts to download.
836 These are defined in artifact_info and have their implementation
837 in build_artifact.py.
838 files: Comma separated list of files to stage. These
839 will be available as is in the corresponding static directory with no
840 custom post-processing.
Laurence Goodbyf5c958d2016-01-14 18:23:56 -0800841 clean: True to remove any previously staged artifacts first.
Chris Sosa76e44b92013-01-31 12:11:38 -0800842
Amin Hassani08e42d22019-06-03 00:31:30 -0700843 Examples:
Chris Sosa76e44b92013-01-31 12:11:38 -0800844 To download the autotest and test suites tarballs:
845 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
846 artifacts=autotest,test_suites
847 To download the full update payload:
848 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
849 artifacts=full_payload
Chris Sosa6b0c6172013-08-05 17:01:33 -0700850 To download just a file called blah.bin:
851 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
852 files=blah.bin
Chris Sosa76e44b92013-01-31 12:11:38 -0800853
854 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 16:39:34 -0700855 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 12:11:38 -0800856
857 Note for this example, relative path is the archive_url stripped of its
858 basename i.e. path/ in the examples above. Specific example:
859
860 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
861
862 Will get staged to:
863
joychened64b222013-06-21 16:39:34 -0700864 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Chris Sosa76e44b92013-01-31 12:11:38 -0800865 """
Gabe Black3b567202015-09-23 14:07:59 -0700866 dl, factory = _get_downloader_and_factory(kwargs)
867
Dan Shi59ae7092013-06-04 14:37:27 -0700868 with DevServerRoot._staging_thread_count_lock:
869 DevServerRoot._staging_thread_count += 1
870 try:
Laurence Goodbyf5c958d2016-01-14 18:23:56 -0800871 boolean_string = kwargs.get('clean')
872 clean = xbuddy.XBuddy.ParseBoolean(boolean_string)
873 if clean and os.path.exists(dl.GetBuildDir()):
874 _Log('Removing %s' % dl.GetBuildDir())
875 shutil.rmtree(dl.GetBuildDir())
Gabe Black3b567202015-09-23 14:07:59 -0700876 async = kwargs.get('async', False)
877 dl.Download(factory, async=async)
Dan Shi59ae7092013-06-04 14:37:27 -0700878 finally:
879 with DevServerRoot._staging_thread_count_lock:
880 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800881 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700882
883 @cherrypy.expose
xixuan52c2fba2016-05-20 17:02:48 -0700884 def cros_au(self, **kwargs):
885 """Auto-update a CrOS DUT.
886
887 Args:
888 kwargs:
889 host_name: the hostname of the DUT to auto-update.
890 build_name: the build name for update the DUT.
891 force_update: Force an update even if the version installed is the
892 same. Default: False.
893 full_update: If True, do not run stateful update, directly force a full
894 reimage. If False, try stateful update first if the dut is already
895 installed with the same version.
896 async: Whether the auto_update function is ran in the background.
David Rileyee75de22017-11-02 10:48:15 -0700897 quick_provision: Whether the quick provision path is attempted first.
xixuan52c2fba2016-05-20 17:02:48 -0700898
899 Returns:
900 A tuple includes two elements:
901 a boolean variable represents whether the auto-update process is
902 successfully started.
903 an integer represents the background auto-update process id.
904 """
905 _check_base_args_for_auto_update(kwargs)
906
907 host_name = kwargs['host_name']
908 build_name = kwargs['build_name']
909 force_update = _parse_boolean_arg(kwargs, 'force_update')
910 full_update = _parse_boolean_arg(kwargs, 'full_update')
911 async = _parse_boolean_arg(kwargs, 'async')
xixuanac89ce82016-11-30 16:48:20 -0800912 original_build = _parse_string_arg(kwargs, 'original_build')
David Haddock90e49442017-04-07 19:14:09 -0700913 payload_filename = _parse_string_arg(kwargs, 'payload_filename')
David Haddock20559612017-06-28 22:15:08 -0700914 clobber_stateful = _parse_boolean_arg(kwargs, 'clobber_stateful')
David Rileyee75de22017-11-02 10:48:15 -0700915 quick_provision = _parse_boolean_arg(kwargs, 'quick_provision')
916
917 devserver_url = updater.GetDevserverUrl()
918 static_url = updater.GetStaticUrl()
xixuan52c2fba2016-05-20 17:02:48 -0700919
920 if async:
921 path = os.path.dirname(os.path.abspath(__file__))
922 execute_file = os.path.join(path, 'cros_update.py')
923 args = (AUTO_UPDATE_CMD % (execute_file, host_name, build_name,
924 updater.static_dir))
xixuanac89ce82016-11-30 16:48:20 -0800925
926 # The original_build's format is like: link/3428.210.0
927 # The corresponding release_archive_url's format is like:
928 # gs://chromeos-releases/stable-channel/link/3428.210.0
929 if original_build:
930 release_archive_url = _build_uri_from_build_name(original_build)
931 # First staging the stateful.tgz synchronousely.
932 self.stage(files='stateful.tgz', async=False,
933 archive_url=release_archive_url)
934 args = ('%s --original_build %s' % (args, original_build))
935
xixuan52c2fba2016-05-20 17:02:48 -0700936 if force_update:
937 args = ('%s --force_update' % args)
938
939 if full_update:
940 args = ('%s --full_update' % args)
941
David Haddock90e49442017-04-07 19:14:09 -0700942 if payload_filename:
943 args = ('%s --payload_filename %s' % (args, payload_filename))
944
David Haddock20559612017-06-28 22:15:08 -0700945 if clobber_stateful:
946 args = ('%s --clobber_stateful' % args)
947
David Rileyee75de22017-11-02 10:48:15 -0700948 if quick_provision:
949 args = ('%s --quick_provision' % args)
950
951 if devserver_url:
952 args = ('%s --devserver_url %s' % (args, devserver_url))
953
954 if static_url:
955 args = ('%s --static_url %s' % (args, static_url))
956
xixuan2a0970a2016-08-10 12:12:44 -0700957 p = subprocess.Popen([args], shell=True, preexec_fn=os.setsid)
958 pid = os.getpgid(p.pid)
xixuan52c2fba2016-05-20 17:02:48 -0700959
960 # Pre-write status in the track_status_file before the first call of
961 # 'get_au_status' to make sure that the track_status_file exists.
xixuan2a0970a2016-08-10 12:12:44 -0700962 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -0700963 progress_tracker.WriteStatus('CrOS update is just started.')
964
xixuan2a0970a2016-08-10 12:12:44 -0700965 return json.dumps((True, pid))
xixuan52c2fba2016-05-20 17:02:48 -0700966 else:
967 cros_update_trigger = cros_update.CrOSUpdateTrigger(
xixuanac89ce82016-11-30 16:48:20 -0800968 host_name, build_name, updater.static_dir, force_update=force_update,
David Rileyee75de22017-11-02 10:48:15 -0700969 full_update=full_update, original_build=original_build,
970 quick_provision=quick_provision, devserver_url=devserver_url,
971 static_url=static_url)
xixuan52c2fba2016-05-20 17:02:48 -0700972 cros_update_trigger.TriggerAU()
xixuan27d50442017-08-09 10:38:25 -0700973 return json.dumps((True, -1))
xixuan52c2fba2016-05-20 17:02:48 -0700974
975 @cherrypy.expose
976 def get_au_status(self, **kwargs):
977 """Check if the auto-update task is finished.
978
979 It handles 4 cases:
980 1. If an error exists in the track_status_file, delete the track file and
981 raise it.
982 2. If cros-update process is finished, delete the file and return the
983 success result.
984 3. If the process is not running, delete the track file and raise an error
985 about 'the process is terminated due to unknown reason'.
986 4. If the track_status_file does not exist, kill the process if it exists,
987 and raise the IOError.
988
989 Args:
990 kwargs:
991 host_name: the hostname of the DUT to auto-update.
992 pid: the background process id of cros-update.
993
994 Returns:
xixuan28d99072016-10-06 12:24:16 -0700995 A dict with three elements:
xixuan52c2fba2016-05-20 17:02:48 -0700996 a boolean variable represents whether the auto-update process is
997 finished.
998 a string represents the current auto-update process status.
999 For example, 'Transfer Devserver/Stateful Update Package'.
xixuan28d99072016-10-06 12:24:16 -07001000 a detailed error message paragraph if there exists an Auto-Update
1001 error, in which the last line shows the main exception. Empty
1002 string otherwise.
xixuan52c2fba2016-05-20 17:02:48 -07001003 """
1004 if 'host_name' not in kwargs:
Amin Hassani08e42d22019-06-03 00:31:30 -07001005 raise common_util.DevServerHTTPError(httplib.INTERNAL_SERVER_ERROR,
1006 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -07001007
1008 if 'pid' not in kwargs:
Amin Hassani08e42d22019-06-03 00:31:30 -07001009 raise common_util.DevServerHTTPError(httplib.INTERNAL_SERVER_ERROR,
1010 KEY_ERROR_MSG % 'pid')
xixuan52c2fba2016-05-20 17:02:48 -07001011
1012 host_name = kwargs['host_name']
1013 pid = kwargs['pid']
1014 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
1015
xixuan28d99072016-10-06 12:24:16 -07001016 result_dict = {'finished': False, 'status': '', 'detailed_error_msg': ''}
xixuan52c2fba2016-05-20 17:02:48 -07001017 try:
1018 result = progress_tracker.ReadStatus()
1019 if result.startswith(cros_update_progress.ERROR_TAG):
xixuan28d99072016-10-06 12:24:16 -07001020 result_dict['detailed_error_msg'] = result[len(
1021 cros_update_progress.ERROR_TAG):]
xixuan28681fd2016-11-23 11:13:56 -08001022 elif result == cros_update_progress.FINISHED:
xixuan28d99072016-10-06 12:24:16 -07001023 result_dict['finished'] = True
1024 result_dict['status'] = result
xixuan28681fd2016-11-23 11:13:56 -08001025 elif not cros_update_progress.IsProcessAlive(pid):
xixuan28d99072016-10-06 12:24:16 -07001026 result_dict['detailed_error_msg'] = (
1027 'Cros_update process terminated midway due to unknown reason. '
1028 'Last update status was %s' % result)
xixuan28681fd2016-11-23 11:13:56 -08001029 else:
1030 result_dict['status'] = result
1031 except IOError as e:
1032 if pid and cros_update_progress.IsProcessAlive(pid):
xixuan2a0970a2016-08-10 12:12:44 -07001033 os.killpg(int(pid), signal.SIGKILL)
xixuan52c2fba2016-05-20 17:02:48 -07001034
xixuan28681fd2016-11-23 11:13:56 -08001035 result_dict['detailed_error_msg'] = str(e)
1036
1037 return json.dumps(result_dict)
xixuan52c2fba2016-05-20 17:02:48 -07001038
1039 @cherrypy.expose
David Riley6d5fca02017-10-31 10:35:47 -07001040 def post_au_status(self, status, **kwargs):
1041 """Updates the status of an auto-update task.
1042
1043 Callers will need to POST to this URL with a body of MIME-type
1044 "multipart/form-data".
1045 The body should include a single argument, 'status', containing the
1046 AU status to record.
1047
1048 Args:
1049 status: The updated status.
1050 kwargs:
1051 host_name: the hostname of the DUT to auto-update.
1052 pid: the background process id of cros-update.
1053 """
1054 if 'host_name' not in kwargs:
Amin Hassani08e42d22019-06-03 00:31:30 -07001055 raise common_util.DevServerHTTPError(httplib.INTERNAL_SERVER_ERROR,
1056 KEY_ERROR_MSG % 'host_name')
David Riley6d5fca02017-10-31 10:35:47 -07001057
1058 if 'pid' not in kwargs:
Amin Hassani08e42d22019-06-03 00:31:30 -07001059 raise common_util.DevServerHTTPError(httplib.INTERNAL_SERVER_ERROR,
1060 KEY_ERROR_MSG % 'pid')
David Riley6d5fca02017-10-31 10:35:47 -07001061
1062 host_name = kwargs['host_name']
1063 pid = kwargs['pid']
David Riley3cea2582017-11-24 22:03:01 -08001064 status = status.rstrip()
1065 _Log('Recording status for %s (%s): %s' % (host_name, pid, status))
David Riley6d5fca02017-10-31 10:35:47 -07001066 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
1067
David Riley3cea2582017-11-24 22:03:01 -08001068 progress_tracker.WriteStatus(status)
David Riley6d5fca02017-10-31 10:35:47 -07001069
1070 return 'True'
1071
1072 @cherrypy.expose
xixuan52c2fba2016-05-20 17:02:48 -07001073 def handler_cleanup(self, **kwargs):
xixuan3bc974e2016-10-18 17:21:43 -07001074 """Clean track status log and temp directory for CrOS auto-update process.
xixuan52c2fba2016-05-20 17:02:48 -07001075
1076 Args:
1077 kwargs:
1078 host_name: the hostname of the DUT to auto-update.
1079 pid: the background process id of cros-update.
1080 """
1081 if 'host_name' not in kwargs:
Amin Hassani08e42d22019-06-03 00:31:30 -07001082 raise common_util.DevServerHTTPError(httplib.INTERNAL_SERVER_ERROR,
1083 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -07001084
1085 if 'pid' not in kwargs:
Amin Hassani08e42d22019-06-03 00:31:30 -07001086 raise common_util.DevServerHTTPError(httplib.INTERNAL_SERVER_ERROR,
1087 KEY_ERROR_MSG % 'pid')
xixuan52c2fba2016-05-20 17:02:48 -07001088
1089 host_name = kwargs['host_name']
1090 pid = kwargs['pid']
1091 cros_update_progress.DelTrackStatusFile(host_name, pid)
xixuan3bc974e2016-10-18 17:21:43 -07001092 cros_update_progress.DelAUTempDirectory(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -07001093
1094 @cherrypy.expose
1095 def kill_au_proc(self, **kwargs):
1096 """Kill CrOS auto-update process using given process id.
1097
1098 Args:
1099 kwargs:
1100 host_name: Kill all the CrOS auto-update process of this host.
1101
1102 Returns:
1103 True if all processes are killed properly.
1104 """
1105 if 'host_name' not in kwargs:
Amin Hassani08e42d22019-06-03 00:31:30 -07001106 raise common_util.DevServerHTTPError(httplib.INTERNAL_SERVER_ERROR,
1107 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -07001108
xixuan447ad9d2017-02-28 14:46:20 -08001109 cur_pid = kwargs.get('pid')
1110
xixuan52c2fba2016-05-20 17:02:48 -07001111 host_name = kwargs['host_name']
xixuan3bc974e2016-10-18 17:21:43 -07001112 track_log_list = cros_update_progress.GetAllTrackStatusFileByHostName(
1113 host_name)
xixuan52c2fba2016-05-20 17:02:48 -07001114 for log in track_log_list:
1115 # The track log's full path is: path/host_name_pid.log
1116 # Use splitext to remove file extension, then parse pid from the
1117 # filename.
1118 pid = os.path.splitext(os.path.basename(log))[0][len(host_name)+1:]
xixuan447ad9d2017-02-28 14:46:20 -08001119 _clear_process(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -07001120
xixuan447ad9d2017-02-28 14:46:20 -08001121 if cur_pid:
1122 _clear_process(host_name, cur_pid)
xixuan52c2fba2016-05-20 17:02:48 -07001123
1124 return 'True'
1125
1126 @cherrypy.expose
1127 def collect_cros_au_log(self, **kwargs):
1128 """Collect CrOS auto-update log.
1129
1130 Args:
1131 kwargs:
1132 host_name: the hostname of the DUT to auto-update.
1133 pid: the background process id of cros-update.
1134
1135 Returns:
David Haddock9f459632017-05-11 14:45:46 -07001136 A dictionary containing the execute log file and any hostlog files.
xixuan52c2fba2016-05-20 17:02:48 -07001137 """
1138 if 'host_name' not in kwargs:
Amin Hassani08e42d22019-06-03 00:31:30 -07001139 raise common_util.DevServerHTTPError(httplib.INTERNAL_SERVER_ERROR,
1140 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -07001141
1142 if 'pid' not in kwargs:
Amin Hassani08e42d22019-06-03 00:31:30 -07001143 raise common_util.DevServerHTTPError(httplib.INTERNAL_SERVER_ERROR,
1144 KEY_ERROR_MSG % 'pid')
xixuan52c2fba2016-05-20 17:02:48 -07001145
1146 host_name = kwargs['host_name']
1147 pid = kwargs['pid']
xixuan3bc974e2016-10-18 17:21:43 -07001148
1149 # Fetch the execute log recorded by cros_update_progress.
xixuan1bbfaba2016-10-13 17:53:22 -07001150 au_log = cros_update_progress.ReadExecuteLogFile(host_name, pid)
1151 cros_update_progress.DelExecuteLogFile(host_name, pid)
David Haddock9f459632017-05-11 14:45:46 -07001152 # Fetch the cros_au host_logs if they exist
1153 au_hostlogs = cros_update_progress.ReadAUHostLogFiles(host_name, pid)
1154 return json.dumps({'cros_au_log': au_log, 'host_logs': au_hostlogs})
xixuan1bbfaba2016-10-13 17:53:22 -07001155
xixuan52c2fba2016-05-20 17:02:48 -07001156 @cherrypy.expose
Dan Shi2f136862016-02-11 15:38:38 -08001157 def locate_file(self, **kwargs):
1158 """Get the path to the given file name.
1159
1160 This method looks up the given file name inside specified build artifacts.
1161 One use case is to help caller to locate an apk file inside a build
1162 artifact. The location of the apk file could be different based on the
1163 branch and target.
1164
1165 Args:
1166 file_name: Name of the file to look for.
1167 artifacts: A list of artifact names to search for the file.
1168
1169 Returns:
1170 Path to the file with the given name. It's relative to the folder for the
1171 build, e.g., DATA/priv-app/sl4a/sl4a.apk
Dan Shi2f136862016-02-11 15:38:38 -08001172 """
1173 dl, _ = _get_downloader_and_factory(kwargs)
1174 try:
Joe Brennan1691f8e2017-03-15 15:53:36 -07001175 file_name = kwargs['file_name']
Dan Shi2f136862016-02-11 15:38:38 -08001176 artifacts = kwargs['artifacts']
1177 except KeyError:
1178 raise DevServerError('`file_name` and `artifacts` are required to search '
1179 'for a file in build artifacts.')
1180 build_path = dl.GetBuildDir()
1181 for artifact in artifacts:
1182 # Get the unzipped folder of the artifact. If it's not defined in
1183 # ARTIFACT_UNZIP_FOLDER_MAP, assume the files are unzipped to the build
1184 # directory directly.
1185 folder = artifact_info.ARTIFACT_UNZIP_FOLDER_MAP.get(artifact, '')
1186 artifact_path = os.path.join(build_path, folder)
1187 for root, _, filenames in os.walk(artifact_path):
Joe Brennan1691f8e2017-03-15 15:53:36 -07001188 if file_name in set([f for f in filenames]):
Dan Shi2f136862016-02-11 15:38:38 -08001189 return os.path.relpath(os.path.join(root, file_name), build_path)
1190 raise DevServerError('File `%s` can not be found in artifacts: %s' %
1191 (file_name, artifacts))
1192
1193 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -08001194 def setup_telemetry(self, **kwargs):
1195 """Extracts and sets up telemetry
1196
1197 This method goes through the telemetry deps packages, and stages them on
1198 the devserver to be used by the drones and the telemetry tests.
1199
1200 Args:
1201 archive_url: Google Storage URL for the build.
1202
1203 Returns:
1204 Path to the source folder for the telemetry codebase once it is staged.
1205 """
Gabe Black3b567202015-09-23 14:07:59 -07001206 dl = _get_downloader(kwargs)
Simran Basi4baad082013-02-14 13:39:18 -08001207
Gabe Black3b567202015-09-23 14:07:59 -07001208 build_path = dl.GetBuildDir()
Simran Basi4baad082013-02-14 13:39:18 -08001209 deps_path = os.path.join(build_path, 'autotest/packages')
1210 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
1211 src_folder = os.path.join(telemetry_path, 'src')
1212
1213 with self._telemetry_lock_dict.lock(telemetry_path):
1214 if os.path.exists(src_folder):
1215 # Telemetry is already fully stage return
1216 return src_folder
1217
1218 common_util.MkDirP(telemetry_path)
1219
1220 # Copy over the required deps tar balls to the telemetry directory.
1221 for dep in TELEMETRY_DEPS:
1222 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -07001223 if not os.path.exists(dep_path):
1224 # This dep does not exist (could be new), do not extract it.
1225 continue
Simran Basi4baad082013-02-14 13:39:18 -08001226 try:
1227 common_util.ExtractTarball(dep_path, telemetry_path)
1228 except common_util.CommonUtilError as e:
1229 shutil.rmtree(telemetry_path)
1230 raise DevServerError(str(e))
1231
1232 # By default all the tarballs extract to test_src but some parts of
1233 # the telemetry code specifically hardcoded to exist inside of 'src'.
1234 test_src = os.path.join(telemetry_path, 'test_src')
1235 try:
1236 shutil.move(test_src, src_folder)
1237 except shutil.Error:
1238 # This can occur if src_folder already exists. Remove and retry move.
1239 shutil.rmtree(src_folder)
Gabe Black3b567202015-09-23 14:07:59 -07001240 raise DevServerError(
1241 'Failure in telemetry setup for build %s. Appears that the '
1242 'test_src to src move failed.' % dl.GetBuild())
Simran Basi4baad082013-02-14 13:39:18 -08001243
1244 return src_folder
1245
1246 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -08001247 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -07001248 """Symbolicates a minidump using pre-downloaded symbols, returns it.
1249
1250 Callers will need to POST to this URL with a body of MIME-type
1251 "multipart/form-data".
1252 The body should include a single argument, 'minidump', containing the
1253 binary-formatted minidump to symbolicate.
1254
Chris Masone816e38c2012-05-02 12:22:36 -07001255 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -08001256 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -07001257 minidump: The binary minidump file to symbolicate.
1258 """
Chris Sosa76e44b92013-01-31 12:11:38 -08001259 # Ensure the symbols have been staged.
Dan Shif08fe492016-10-04 14:39:25 -07001260 # Try debug.tar.xz first, then debug.tgz
1261 for artifact in (artifact_info.SYMBOLS_ONLY, artifact_info.SYMBOLS):
1262 kwargs['artifacts'] = artifact
1263 dl = _get_downloader(kwargs)
1264
1265 try:
1266 if self.stage(**kwargs) == 'Success':
1267 break
1268 except build_artifact.ArtifactDownloadError:
1269 continue
1270 else:
Gabe Black3b567202015-09-23 14:07:59 -07001271 raise DevServerError('Failed to stage symbols for %s' %
1272 dl.DescribeSource())
Chris Sosa76e44b92013-01-31 12:11:38 -08001273
Chris Masone816e38c2012-05-02 12:22:36 -07001274 to_return = ''
1275 with tempfile.NamedTemporaryFile() as local:
1276 while True:
1277 data = minidump.file.read(8192)
1278 if not data:
1279 break
1280 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -08001281
Chris Masone816e38c2012-05-02 12:22:36 -07001282 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -08001283
Gabe Black3b567202015-09-23 14:07:59 -07001284 symbols_directory = os.path.join(dl.GetBuildDir(), 'debug', 'breakpad')
Chris Sosa76e44b92013-01-31 12:11:38 -08001285
xixuanab744382017-04-27 10:41:27 -07001286 # The location of minidump_stackwalk is defined in chromeos-admin.
Chris Sosa76e44b92013-01-31 12:11:38 -08001287 stackwalk = subprocess.Popen(
xixuanab744382017-04-27 10:41:27 -07001288 ['/usr/local/bin/minidump_stackwalk', local.name, symbols_directory],
Chris Sosa76e44b92013-01-31 12:11:38 -08001289 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1290
Chris Masone816e38c2012-05-02 12:22:36 -07001291 to_return, error_text = stackwalk.communicate()
1292 if stackwalk.returncode != 0:
1293 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
1294 error_text, stackwalk.returncode))
1295
1296 return to_return
1297
1298 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -08001299 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 15:31:36 -04001300 """Return a string representing the latest build for a given target.
1301
1302 Args:
1303 target: The build target, typically a combination of the board and the
1304 type of build e.g. x86-mario-release.
1305 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
1306 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-07 18:21:26 -08001307
Scott Zawalski16954532012-03-20 15:31:36 -04001308 Returns:
1309 A string representation of the latest build if one exists, i.e.
1310 R19-1993.0.0-a1-b1480.
1311 An empty string if no latest could be found.
1312 """
Don Garrettf84631a2014-01-07 18:21:26 -08001313 if not kwargs:
Scott Zawalski16954532012-03-20 15:31:36 -04001314 return _PrintDocStringAsHTML(self.latestbuild)
1315
Don Garrettf84631a2014-01-07 18:21:26 -08001316 if 'target' not in kwargs:
Amin Hassani08e42d22019-06-03 00:31:30 -07001317 raise common_util.DevServerHTTPError(httplib.INTERNAL_SERVER_ERROR,
1318 'Error: target= is required!')
Dan Shi61305df2015-10-26 16:52:35 -07001319
1320 if _is_android_build_request(kwargs):
1321 branch = kwargs.get('branch', None)
1322 target = kwargs.get('target', None)
1323 if not target or not branch:
1324 raise DevServerError(
xixuan52c2fba2016-05-20 17:02:48 -07001325 'Both target and branch must be specified to query for the latest '
1326 'Android build.')
Dan Shi61305df2015-10-26 16:52:35 -07001327 return android_build.BuildAccessor.GetLatestBuildID(target, branch)
1328
Scott Zawalski16954532012-03-20 15:31:36 -04001329 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001330 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-07 18:21:26 -08001331 updater.static_dir, kwargs['target'],
1332 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -07001333 except common_util.CommonUtilError as errmsg:
Amin Hassani08e42d22019-06-03 00:31:30 -07001334 raise common_util.DevServerHTTPError(httplib.INTERNAL_SERVER_ERROR,
1335 str(errmsg))
Scott Zawalski16954532012-03-20 15:31:36 -04001336
1337 @cherrypy.expose
xixuan7efd0002016-04-14 15:34:01 -07001338 def list_suite_controls(self, **kwargs):
1339 """Return a list of contents of all known control files.
1340
1341 Example URL:
1342 To List all control files' content:
1343 http://dev-server/list_suite_controls?suite_name=bvt&
1344 build=daisy_spring-release/R29-4279.0.0
1345
1346 Args:
1347 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
1348 suite_name: List the control files belonging to that suite.
1349
1350 Returns:
Dan Shia1cd6522016-04-18 16:07:21 -07001351 A dictionary of all control files's path to its content for given suite.
xixuan7efd0002016-04-14 15:34:01 -07001352 """
1353 if not kwargs:
1354 return _PrintDocStringAsHTML(self.controlfiles)
1355
1356 if 'build' not in kwargs:
Amin Hassani08e42d22019-06-03 00:31:30 -07001357 raise common_util.DevServerHTTPError(httplib.INTERNAL_SERVER_ERROR,
1358 'Error: build= is required!')
xixuan7efd0002016-04-14 15:34:01 -07001359
1360 if 'suite_name' not in kwargs:
Amin Hassani08e42d22019-06-03 00:31:30 -07001361 raise common_util.DevServerHTTPError(httplib.INTERNAL_SERVER_ERROR,
Dan Shia1cd6522016-04-18 16:07:21 -07001362 'Error: suite_name= is required!')
xixuan7efd0002016-04-14 15:34:01 -07001363
1364 control_file_list = [
1365 line.rstrip() for line in common_util.GetControlFileListForSuite(
1366 updater.static_dir, kwargs['build'],
1367 kwargs['suite_name']).splitlines()]
1368
Dan Shia1cd6522016-04-18 16:07:21 -07001369 control_file_content_dict = {}
xixuan7efd0002016-04-14 15:34:01 -07001370 for control_path in control_file_list:
Dan Shia1cd6522016-04-18 16:07:21 -07001371 control_file_content_dict[control_path] = (common_util.GetControlFile(
xixuan7efd0002016-04-14 15:34:01 -07001372 updater.static_dir, kwargs['build'], control_path))
1373
Dan Shia1cd6522016-04-18 16:07:21 -07001374 return json.dumps(control_file_content_dict)
xixuan7efd0002016-04-14 15:34:01 -07001375
1376 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -08001377 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 17:17:28 -05001378 """Return a control file or a list of all known control files.
1379
1380 Example URL:
1381 To List all control files:
beepsbd337242013-07-09 22:44:06 -07001382 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
1383 To List all control files for, say, the bvt suite:
1384 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -05001385 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -05001386 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 -05001387
1388 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -05001389 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -05001390 control_path: If you want the contents of a control file set this
1391 to the path. E.g. client/site_tests/sleeptest/control
1392 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -07001393 suite_name: If control_path is not specified but a suite_name is
1394 specified, list the control files belonging to that suite instead of
1395 all control files. The empty string for suite_name will list all control
1396 files for the build.
Don Garrettf84631a2014-01-07 18:21:26 -08001397
Scott Zawalski4647ce62012-01-03 17:17:28 -05001398 Returns:
1399 Contents of a control file if control_path is provided.
1400 A list of control files if no control_path is provided.
1401 """
Don Garrettf84631a2014-01-07 18:21:26 -08001402 if not kwargs:
Scott Zawalski4647ce62012-01-03 17:17:28 -05001403 return _PrintDocStringAsHTML(self.controlfiles)
1404
Don Garrettf84631a2014-01-07 18:21:26 -08001405 if 'build' not in kwargs:
Amin Hassani08e42d22019-06-03 00:31:30 -07001406 raise common_util.DevServerHTTPError(httplib.INTERNAL_SERVER_ERROR,
1407 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -05001408
Don Garrettf84631a2014-01-07 18:21:26 -08001409 if 'control_path' not in kwargs:
1410 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-09 22:44:06 -07001411 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-07 18:21:26 -08001412 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-09 22:44:06 -07001413 else:
1414 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-07 18:21:26 -08001415 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -05001416 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001417 return common_util.GetControlFile(
Don Garrettf84631a2014-01-07 18:21:26 -08001418 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -08001419
1420 @cherrypy.expose
Simran Basi99e63c02014-05-20 10:39:52 -07001421 def xbuddy_translate(self, *args, **kwargs):
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001422 """Translates an xBuddy path to a real path to artifact if it exists.
1423
1424 Args:
Simran Basi99e63c02014-05-20 10:39:52 -07001425 args: An xbuddy path in the form of {local|remote}/build_id/artifact.
1426 Local searches the devserver's static directory. Remote searches a
1427 Google Storage image archive.
1428
1429 Kwargs:
1430 image_dir: Google Storage image archive to search in if requesting a
1431 remote artifact. If none uses the default bucket.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001432
1433 Returns:
Simran Basi99e63c02014-05-20 10:39:52 -07001434 String in the format of build_id/artifact as stored on the local server
1435 or in Google Storage.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001436 """
Simran Basi99e63c02014-05-20 10:39:52 -07001437 build_id, filename = self._xbuddy.Translate(
Gabe Black3b567202015-09-23 14:07:59 -07001438 args, image_dir=kwargs.get('image_dir'))
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001439 response = os.path.join(build_id, filename)
1440 _Log('Path translation requested, returning: %s', response)
1441 return response
1442
1443 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -07001444 def xbuddy(self, *args, **kwargs):
1445 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -07001446
1447 Args:
joycheneaf4cfc2013-07-02 08:38:57 -07001448 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -07001449 components of the path. The path can be understood as
1450 "{local|remote}/build_id/artifact" where build_id is composed of
1451 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -07001452
joychen121fc9b2013-08-02 14:30:30 -07001453 The first path element is optional, and can be "remote" or "local"
1454 If local (the default), devserver will not attempt to access Google
1455 Storage, and will only search the static directory for the files.
1456 If remote, devserver will try to obtain the artifact off GS if it's
1457 not found locally.
1458 The board is the familiar board name, optionally suffixed.
1459 The version can be the google storage version number, and may also be
1460 any of a number of xBuddy defined version aliases that will be
1461 translated into the latest built image that fits the description.
1462 Defaults to latest.
1463 The artifact is one of a number of image or artifact aliases used by
1464 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -07001465
1466 Kwargs:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001467 for_update: {true|false}
1468 if true, pregenerates the update payloads for the image,
1469 and returns the update uri to pass to the
1470 update_engine_client.
joychen3cb228e2013-06-12 12:13:13 -07001471 return_dir: {true|false}
1472 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001473 relative_path: {true|false}
1474 if set to true, returns the relative path to the payload
1475 directory from static_dir.
joychen3cb228e2013-06-12 12:13:13 -07001476 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -07001477 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -07001478 or
joycheneaf4cfc2013-07-02 08:38:57 -07001479 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -07001480
1481 Returns:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001482 If |for_update|, returns a redirect to the image or update file
1483 on the devserver. E.g.,
1484 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
1485 chromium-test-image.bin
1486 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
1487 http://host:port/static/x86-generic-release/R26-4000.0.0/
1488 If |relative_path| is true, return a relative path the folder where the
1489 payloads are. E.g.,
1490 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 12:13:13 -07001491 """
Chris Sosa75490802013-09-30 17:21:45 -07001492 boolean_string = kwargs.get('for_update')
1493 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001494 boolean_string = kwargs.get('return_dir')
1495 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
1496 boolean_string = kwargs.get('relative_path')
1497 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -07001498
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001499 if return_dir and relative_path:
Chris Sosa4b951602014-04-09 20:26:07 -07001500 raise common_util.DevServerHTTPError(
Amin Hassani08e42d22019-06-03 00:31:30 -07001501 httplib.INTERNAL_SERVER_ERROR,
1502 'Cannot specify both return_dir and relative_path')
Chris Sosa75490802013-09-30 17:21:45 -07001503
1504 # For updates, we optimize downloading of test images.
1505 file_name = None
1506 build_id = None
1507 if for_update:
1508 try:
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001509 build_id = self._xbuddy.StageTestArtifactsForUpdate(args)
Chris Sosa75490802013-09-30 17:21:45 -07001510 except build_artifact.ArtifactDownloadError:
1511 build_id = None
1512
1513 if not build_id:
1514 build_id, file_name = self._xbuddy.Get(args)
1515
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001516 if for_update:
1517 _Log('Payload generation triggered by request')
1518 # Forces payload to be in cache and symlinked into build_id dir.
Chris Sosa75490802013-09-30 17:21:45 -07001519 updater.GetUpdateForLabel(autoupdate.FORCED_UPDATE, build_id,
1520 image_name=file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001521
1522 response = None
1523 if return_dir:
1524 response = os.path.join(cherrypy.request.base, 'static', build_id)
1525 _Log('Directory requested, returning: %s', response)
1526 elif relative_path:
1527 response = build_id
1528 _Log('Relative path requested, returning: %s', response)
1529 elif for_update:
1530 response = os.path.join(cherrypy.request.base, 'update', build_id)
1531 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 12:13:13 -07001532 else:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001533 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 14:30:30 -07001534 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001535 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 14:30:30 -07001536 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -07001537
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001538 return response
1539
joychen3cb228e2013-06-12 12:13:13 -07001540 @cherrypy.expose
1541 def xbuddy_list(self):
1542 """Lists the currently available images & time since last access.
1543
Gilad Arnold452fd272014-02-04 11:09:28 -08001544 Returns:
1545 A string representation of a list of tuples [(build_id, time since last
1546 access),...]
joychen3cb228e2013-06-12 12:13:13 -07001547 """
1548 return self._xbuddy.List()
1549
1550 @cherrypy.expose
1551 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 11:09:28 -08001552 """Returns the number of images cached by xBuddy."""
joychen3cb228e2013-06-12 12:13:13 -07001553 return self._xbuddy.Capacity()
1554
1555 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001556 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001557 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001558 return ('Welcome to the Dev Server!<br>\n'
1559 '<br>\n'
1560 'Here are the available methods, click for documentation:<br>\n'
1561 '<br>\n'
1562 '%s' %
1563 '<br>\n'.join(
1564 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001565 for name in _FindExposedMethods(
1566 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001567
1568 @cherrypy.expose
1569 def doc(self, *args):
1570 """Shows the documentation for available methods / URLs.
1571
Amin Hassani08e42d22019-06-03 00:31:30 -07001572 Examples:
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001573 http://myhost/doc/update
1574 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001575 name = '/'.join(args)
1576 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001577 if not method:
1578 raise DevServerError("No exposed method named `%s'" % name)
1579 if not method.__doc__:
1580 raise DevServerError("No documentation for exposed method `%s'" % name)
1581 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -07001582
Dale Curtisc9aaf3a2011-08-09 15:47:40 -07001583 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001584 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001585 """Handles an update check from a Chrome OS client.
1586
1587 The HTTP request should contain the standard Omaha-style XML blob. The URL
1588 line may contain an additional intermediate path to the update payload.
1589
joychen121fc9b2013-08-02 14:30:30 -07001590 This request can be handled in one of 4 ways, depending on the devsever
1591 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -07001592
joychen121fc9b2013-08-02 14:30:30 -07001593 1. No intermediate path
1594 If no intermediate path is given, the default behavior is to generate an
1595 update payload from the latest test image locally built for the board
1596 specified in the xml. Devserver serves the generated payload.
1597
1598 2. Path explicitly invokes XBuddy
1599 If there is a path given, it can explicitly invoke xbuddy by prefixing it
1600 with 'xbuddy'. This path is then used to acquire an image binary for the
1601 devserver to generate an update payload from. Devserver then serves this
1602 payload.
1603
1604 3. Path is left for the devserver to interpret.
1605 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
1606 to generate a payload from the test image in that directory and serve it.
1607
1608 4. The devserver is in a 'forced' mode. TO BE DEPRECATED
1609 This comes from the usage of --forced_payload or --image when starting the
1610 devserver. No matter what path (or no path) gets passed in, devserver will
1611 serve the update payload (--forced_payload) or generate an update payload
1612 from the image (--image).
1613
1614 Examples:
1615 1. No intermediate path
1616 update_engine_client --omaha_url=http://myhost/update
1617 This generates an update payload from the latest test image locally built
1618 for the board specified in the xml.
1619
1620 2. Explicitly invoke xbuddy
1621 update_engine_client --omaha_url=
1622 http://myhost/update/xbuddy/remote/board/version/dev
1623 This would go to GS to download the dev image for the board, from which
1624 the devserver would generate a payload to serve.
1625
1626 3. Give a path for devserver to interpret
1627 update_engine_client --omaha_url=http://myhost/update/some/random/path
1628 This would attempt, in order to:
1629 a) Generate an update from a test image binary if found in
1630 static_dir/some/random/path.
1631 b) Serve an update payload found in static_dir/some/random/path.
1632 c) Hope that some/random/path takes the form "board/version" and
1633 and attempt to download an update payload for that board/version
1634 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001635 """
joychen121fc9b2013-08-02 14:30:30 -07001636 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -08001637 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -07001638 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-11 19:49:01 -07001639
joychen121fc9b2013-08-02 14:30:30 -07001640 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001641
Dan Shiafd0e492015-05-27 14:23:51 -07001642 @require_psutil()
1643 def _get_io_stats(self):
1644 """Get the IO stats as a dictionary.
1645
Gabe Black3b567202015-09-23 14:07:59 -07001646 Returns:
1647 A dictionary of IO stats collected by psutil.
Dan Shiafd0e492015-05-27 14:23:51 -07001648 """
1649 return {'disk_read_bytes_per_second': self.disk_read_bytes_per_sec,
1650 'disk_write_bytes_per_second': self.disk_write_bytes_per_sec,
1651 'disk_total_bytes_per_second': (self.disk_read_bytes_per_sec +
1652 self.disk_write_bytes_per_sec),
1653 'network_sent_bytes_per_second': self.network_sent_bytes_per_sec,
1654 'network_recv_bytes_per_second': self.network_recv_bytes_per_sec,
1655 'network_total_bytes_per_second': (self.network_sent_bytes_per_sec +
1656 self.network_recv_bytes_per_sec),
1657 'cpu_percent': psutil.cpu_percent(),}
1658
Dan Shi7247f9c2016-06-01 09:19:09 -07001659
1660 def _get_process_count(self, process_cmd_pattern):
1661 """Get the count of processes that match the given command pattern.
1662
1663 Args:
1664 process_cmd_pattern: The regex pattern of process command to match.
1665
1666 Returns:
1667 The count of processes that match the given command pattern.
1668 """
1669 try:
xixuanac89ce82016-11-30 16:48:20 -08001670 # Use Popen instead of check_output since the latter cannot run with old
1671 # python version (less than 2.7)
1672 proc = subprocess.Popen(
Congbin Guoe527bec2019-05-10 16:14:26 -07001673 ['pgrep', '-fc', process_cmd_pattern],
xixuanac89ce82016-11-30 16:48:20 -08001674 stdout=subprocess.PIPE,
1675 stderr=subprocess.PIPE,
Congbin Guoe527bec2019-05-10 16:14:26 -07001676 )
xixuanac89ce82016-11-30 16:48:20 -08001677 cmd_output, cmd_error = proc.communicate()
1678 if cmd_error:
1679 _Log('Error happened when getting process count: %s' % cmd_error)
1680
1681 return int(cmd_output)
Dan Shi7247f9c2016-06-01 09:19:09 -07001682 except subprocess.CalledProcessError:
1683 return 0
1684
1685
Dan Shif5ce2de2013-04-25 16:06:32 -07001686 @cherrypy.expose
1687 def check_health(self):
1688 """Collect the health status of devserver to see if it's ready for staging.
1689
Gilad Arnold452fd272014-02-04 11:09:28 -08001690 Returns:
1691 A JSON dictionary containing all or some of the following fields:
1692 free_disk (int): free disk space in GB
1693 staging_thread_count (int): number of devserver threads currently staging
1694 an image
Dan Shi7247f9c2016-06-01 09:19:09 -07001695 apache_client_count (int): count of Apache processes.
1696 telemetry_test_count (int): count of telemetry tests.
1697 gsutil_count (int): count of gsutil processes.
Dan Shif5ce2de2013-04-25 16:06:32 -07001698 """
1699 # Get free disk space.
1700 stat = os.statvfs(updater.static_dir)
1701 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
Congbin Guo9ffd6ff2019-05-09 15:54:17 -07001702 apache_client_count = self._get_process_count('bin/apache2? -k start')
Dan Shi7247f9c2016-06-01 09:19:09 -07001703 telemetry_test_count = self._get_process_count('python.*telemetry')
1704 gsutil_count = self._get_process_count('gsutil')
xixuan447ad9d2017-02-28 14:46:20 -08001705 au_process_count = len(cros_update_progress.GetAllRunningAUProcess())
Dan Shif5ce2de2013-04-25 16:06:32 -07001706
Dan Shiafd0e492015-05-27 14:23:51 -07001707 health_data = {
Dan Shif5ce2de2013-04-25 16:06:32 -07001708 'free_disk': free_disk,
Dan Shid76e6bb2016-01-28 22:28:51 -08001709 'staging_thread_count': DevServerRoot._staging_thread_count,
1710 'apache_client_count': apache_client_count,
Dan Shi7247f9c2016-06-01 09:19:09 -07001711 'telemetry_test_count': telemetry_test_count,
xixuan447ad9d2017-02-28 14:46:20 -08001712 'gsutil_count': gsutil_count,
1713 'au_process_count': au_process_count,
1714 }
Dan Shiafd0e492015-05-27 14:23:51 -07001715 health_data.update(self._get_io_stats() or {})
1716
1717 return json.dumps(health_data)
Dan Shif5ce2de2013-04-25 16:06:32 -07001718
1719
Chris Sosadbc20082012-12-10 13:39:11 -08001720def _CleanCache(cache_dir, wipe):
1721 """Wipes any excess cached items in the cache_dir.
1722
1723 Args:
1724 cache_dir: the directory we are wiping from.
1725 wipe: If True, wipe all the contents -- not just the excess.
1726 """
1727 if wipe:
1728 # Clear the cache and exit on error.
1729 cmd = 'rm -rf %s/*' % cache_dir
1730 if os.system(cmd) != 0:
1731 _Log('Failed to clear the cache with %s' % cmd)
1732 sys.exit(1)
1733 else:
1734 # Clear all but the last N cached updates
1735 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
1736 (cache_dir, CACHED_ENTRIES))
1737 if os.system(cmd) != 0:
1738 _Log('Failed to clean up old delta cache files with %s' % cmd)
1739 sys.exit(1)
1740
1741
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001742def _AddTestingOptions(parser):
1743 group = optparse.OptionGroup(
1744 parser, 'Advanced Testing Options', 'These are used by test scripts and '
1745 'developers writing integration tests utilizing the devserver. They are '
1746 'not intended to be really used outside the scope of someone '
1747 'knowledgable about the test.')
1748 group.add_option('--exit',
1749 action='store_true',
1750 help='do not start the server (yet pregenerate/clear cache)')
1751 group.add_option('--host_log',
1752 action='store_true', default=False,
1753 help='record history of host update events (/api/hostlog)')
1754 group.add_option('--max_updates',
Gabe Black3b567202015-09-23 14:07:59 -07001755 metavar='NUM', default=-1, type='int',
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001756 help='maximum number of update checks handled positively '
1757 '(default: unlimited)')
David Zeuthen52ccd012013-10-31 12:58:26 -07001758 group.add_option('--public_key',
1759 metavar='PATH', default=None,
1760 help='path to the public key in pem format. If this is set '
1761 'the devserver will transmit a base64 encoded version of '
1762 'the content in the Omaha-style XML response.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001763 group.add_option('--proxy_port',
1764 metavar='PORT', default=None, type='int',
1765 help='port to have the client connect to -- basically the '
1766 'devserver lies to the update to tell it to get the payload '
1767 'from a different port that will proxy the request back to '
1768 'the devserver. The proxy must be managed outside the '
1769 'devserver.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001770 parser.add_option_group(group)
1771
1772
1773def _AddUpdateOptions(parser):
1774 group = optparse.OptionGroup(
1775 parser, 'Autoupdate Options', 'These options can be used to change '
1776 'how the devserver either generates or serve update payloads. Please '
1777 'note that all of these option affect how a payload is generated and so '
1778 'do not work in archive-only mode.')
1779 group.add_option('--board',
1780 help='By default the devserver will create an update '
1781 'payload from the latest image built for the board '
1782 'a device that is requesting an update has. When we '
1783 'pre-generate an update (see below) and we do not specify '
1784 'another update_type option like image or payload, the '
1785 'devserver needs to know the board to generate the latest '
1786 'image for. This is that board.')
1787 group.add_option('--critical_update',
1788 action='store_true', default=False,
1789 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001790 group.add_option('--image',
1791 metavar='FILE',
1792 help='Generate and serve an update using this image to any '
1793 'device that requests an update.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001794 group.add_option('--payload',
1795 metavar='PATH',
1796 help='use the update payload from specified directory '
1797 '(update.gz).')
1798 group.add_option('-p', '--pregenerate_update',
1799 action='store_true', default=False,
1800 help='pre-generate the update payload before accepting '
1801 'update requests. Useful to help debug payload generation '
1802 'issues quickly. Also if an update payload will take a '
1803 'long time to generate, a client may timeout if you do not'
1804 'pregenerate the update.')
1805 group.add_option('--src_image',
1806 metavar='PATH', default='',
1807 help='If specified, delta updates will be generated using '
1808 'this image as the source image. Delta updates are when '
1809 'you are updating from a "source image" to a another '
1810 'image.')
1811 parser.add_option_group(group)
1812
1813
1814def _AddProductionOptions(parser):
1815 group = optparse.OptionGroup(
1816 parser, 'Advanced Server Options', 'These options can be used to changed '
1817 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001818 group.add_option('--clear_cache',
1819 action='store_true', default=False,
1820 help='At startup, removes all cached entries from the'
1821 'devserver\'s cache.')
1822 group.add_option('--logfile',
1823 metavar='PATH',
1824 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -07001825 group.add_option('--pidfile',
1826 metavar='PATH',
1827 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 11:04:13 -08001828 group.add_option('--portfile',
1829 metavar='PATH',
1830 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001831 group.add_option('--production',
1832 action='store_true', default=False,
1833 help='have the devserver use production values when '
1834 'starting up. This includes using more threads and '
1835 'performing less logging.')
1836 parser.add_option_group(group)
1837
1838
Paul Hobbsef4e0702016-06-27 17:01:42 -07001839def MakeLogHandler(logfile):
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001840 """Create a LogHandler instance used to log all messages."""
1841 hdlr_cls = handlers.TimedRotatingFileHandler
1842 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
xixuan3d48bff2017-01-30 19:00:09 -08001843 interval=_LOG_ROTATION_INTERVAL,
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001844 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -07001845 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001846 return hdlr
1847
1848
Chris Sosacde6bf42012-05-31 18:36:39 -07001849def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001850 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -08001851 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -07001852
1853 # get directory that the devserver is run from
1854 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -07001855 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -07001856 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001857 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -07001858 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -07001859 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001860 parser.add_option('--port',
1861 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 13:13:28 -08001862 help=('port for the dev server to use; if zero, binds to '
1863 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001864 parser.add_option('-t', '--test_image',
1865 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -07001866 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -07001867 parser.add_option('-x', '--xbuddy_manage_builds',
1868 action='store_true',
1869 default=False,
1870 help='If set, allow xbuddy to manage images in'
1871 'build/images.')
Dan Shi72b16132015-10-08 12:10:33 -07001872 parser.add_option('-a', '--android_build_credential',
1873 default=None,
1874 help='Path to a json file which contains the credential '
1875 'needed to access Android builds.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001876 _AddProductionOptions(parser)
1877 _AddUpdateOptions(parser)
1878 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -07001879 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001880
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001881 # Handle options that must be set globally in cherrypy. Do this
1882 # work up front, because calls to _Log() below depend on this
1883 # initialization.
1884 if options.production:
1885 cherrypy.config.update({'environment': 'production'})
1886 if not options.logfile:
1887 cherrypy.config.update({'log.screen': True})
1888 else:
1889 cherrypy.config.update({'log.error_file': '',
1890 'log.access_file': ''})
Paul Hobbsef4e0702016-06-27 17:01:42 -07001891 hdlr = MakeLogHandler(options.logfile)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001892 # Pylint can't seem to process these two calls properly
1893 # pylint: disable=E1101
1894 cherrypy.log.access_log.addHandler(hdlr)
1895 cherrypy.log.error_log.addHandler(hdlr)
1896 # pylint: enable=E1101
1897
joychened64b222013-06-21 16:39:34 -07001898 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001899 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001900
joychened64b222013-06-21 16:39:34 -07001901 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001902 # If our devserver is only supposed to serve payloads, we shouldn't be
1903 # mucking with the cache at all. If the devserver hadn't previously
1904 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001905 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001906 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001907 else:
1908 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001909
Chris Sosadbc20082012-12-10 13:39:11 -08001910 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 16:39:34 -07001911 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001912
joychen121fc9b2013-08-02 14:30:30 -07001913 _xbuddy = xbuddy.XBuddy(options.xbuddy_manage_builds,
1914 options.board,
joychen121fc9b2013-08-02 14:30:30 -07001915 static_dir=options.static_dir)
Chris Sosa75490802013-09-30 17:21:45 -07001916 if options.clear_cache and options.xbuddy_manage_builds:
1917 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 14:30:30 -07001918
Chris Sosa6a3697f2013-01-29 16:44:43 -08001919 # We allow global use here to share with cherrypy classes.
1920 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001921 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001922 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001923 _xbuddy,
joychened64b222013-06-21 16:39:34 -07001924 static_dir=options.static_dir,
Chris Sosa5d342a22010-09-28 16:54:41 -07001925 forced_image=options.image,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001926 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001927 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-28 23:42:37 -07001928 src_image=options.src_image,
Chris Sosa08d55a22011-01-19 16:08:02 -08001929 board=options.board,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001930 copy_to_static_root=not options.exit,
David Zeuthen52ccd012013-10-31 12:58:26 -07001931 public_key=options.public_key,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001932 critical_update=options.critical_update,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001933 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001934 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001935 )
Chris Sosa7c931362010-10-11 19:49:01 -07001936
Chris Sosa6a3697f2013-01-29 16:44:43 -08001937 if options.pregenerate_update:
1938 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 15:46:22 -07001939
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001940 if options.exit:
1941 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001942
joychen3cb228e2013-06-12 12:13:13 -07001943 dev_server = DevServerRoot(_xbuddy)
1944
Gilad Arnold11fbef42014-02-10 11:04:13 -08001945 # Patch CherryPy to support binding to any available port (--port=0).
1946 cherrypy_ext.ZeroPortPatcher.DoPatch(cherrypy)
1947
Chris Sosa855b8932013-08-21 13:24:55 -07001948 if options.pidfile:
1949 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1950
Gilad Arnold11fbef42014-02-10 11:04:13 -08001951 if options.portfile:
1952 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1953
Dan Shiafd5c6c2016-01-07 10:27:03 -08001954 if (options.android_build_credential and
1955 os.path.exists(options.android_build_credential)):
1956 try:
1957 with open(options.android_build_credential) as f:
1958 android_build.BuildAccessor.credential_info = json.load(f)
1959 except ValueError as e:
1960 _Log('Failed to load the android build credential: %s. Error: %s.' %
1961 (options.android_build_credential, e))
joychen3cb228e2013-06-12 12:13:13 -07001962 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001963
1964
1965if __name__ == '__main__':
1966 main()