blob: 5aa2f2509997783e127d5620301575ea32fa1f8e [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
661 sha1 (string): a base64 encoded SHA1 hash
662 sha256 (string): a base64 encoded SHA256 hash
663
664 Example URL:
665 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 09:46:32 -0700666 """
Don Garrettf84631a2014-01-07 18:21:26 -0800667 file_path = os.path.join(updater.static_dir, *args)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700668 if not os.path.exists(file_path):
669 raise DevServerError('file not found: %s' % file_path)
670 try:
671 file_size = os.path.getsize(file_path)
672 file_sha1 = common_util.GetFileSha1(file_path)
673 file_sha256 = common_util.GetFileSha256(file_path)
674 except os.error, e:
675 raise DevServerError('failed to get info for file %s: %s' %
Gilad Arnolde74b3812013-04-22 11:27:38 -0700676 (file_path, e))
677
678 is_delta = autoupdate.Autoupdate.IsDeltaFormatFile(file_path)
679
680 return json.dumps({
681 autoupdate.Autoupdate.SIZE_ATTR: file_size,
682 autoupdate.Autoupdate.SHA1_ATTR: file_sha1,
683 autoupdate.Autoupdate.SHA256_ATTR: file_sha256,
684 autoupdate.Autoupdate.ISDELTA_ATTR: is_delta
685 })
Gilad Arnold55a2a372012-10-02 09:46:32 -0700686
Chris Sosa76e44b92013-01-31 12:11:38 -0800687
David Rochberg7c79a812011-01-19 14:24:45 -0500688class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700689 """The Root Class for the Dev Server.
690
691 CherryPy works as follows:
692 For each method in this class, cherrpy interprets root/path
693 as a call to an instance of DevServerRoot->method_name. For example,
694 a call to http://myhost/build will call build. CherryPy automatically
695 parses http args and places them as keyword arguments in each method.
696 For paths http://myhost/update/dir1/dir2, you can use *args so that
697 cherrypy uses the update method and puts the extra paths in args.
698 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700699 # Method names that should not be listed on the index page.
700 _UNLISTED_METHODS = ['index', 'doc']
701
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700702 api = ApiRoot()
Chris Sosa7c931362010-10-11 19:49:01 -0700703
Dan Shi59ae7092013-06-04 14:37:27 -0700704 # Number of threads that devserver is staging images.
705 _staging_thread_count = 0
706 # Lock used to lock increasing/decreasing count.
707 _staging_thread_count_lock = threading.Lock()
708
Dan Shiafd0e492015-05-27 14:23:51 -0700709 @require_psutil()
710 def _refresh_io_stats(self):
711 """A call running in a thread to update IO stats periodically."""
712 prev_disk_io_counters = psutil.disk_io_counters()
713 prev_network_io_counters = psutil.net_io_counters()
714 prev_read_time = time.time()
715 while True:
716 time.sleep(STATS_INTERVAL)
717 now = time.time()
718 interval = now - prev_read_time
719 prev_read_time = now
720 # Disk IO is for all disks.
721 disk_io_counters = psutil.disk_io_counters()
722 network_io_counters = psutil.net_io_counters()
723
724 self.disk_read_bytes_per_sec = (
725 disk_io_counters.read_bytes -
726 prev_disk_io_counters.read_bytes)/interval
727 self.disk_write_bytes_per_sec = (
728 disk_io_counters.write_bytes -
729 prev_disk_io_counters.write_bytes)/interval
730 prev_disk_io_counters = disk_io_counters
731
732 self.network_sent_bytes_per_sec = (
733 network_io_counters.bytes_sent -
734 prev_network_io_counters.bytes_sent)/interval
735 self.network_recv_bytes_per_sec = (
736 network_io_counters.bytes_recv -
737 prev_network_io_counters.bytes_recv)/interval
738 prev_network_io_counters = network_io_counters
739
740 @require_psutil()
741 def _start_io_stat_thread(self):
Gabe Black3b567202015-09-23 14:07:59 -0700742 """Start the thread to collect IO stats."""
Dan Shiafd0e492015-05-27 14:23:51 -0700743 thread = threading.Thread(target=self._refresh_io_stats)
744 thread.daemon = True
745 thread.start()
746
joychen3cb228e2013-06-12 12:13:13 -0700747 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700748 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800749 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 12:13:13 -0700750 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 14:24:45 -0500751
Dan Shiafd0e492015-05-27 14:23:51 -0700752 # Cache of disk IO stats, a thread refresh the stats every 10 seconds.
753 # lock is not used for these variables as the only thread writes to these
754 # variables is _refresh_io_stats.
755 self.disk_read_bytes_per_sec = 0
756 self.disk_write_bytes_per_sec = 0
757 # Cache of network IO stats.
758 self.network_sent_bytes_per_sec = 0
759 self.network_recv_bytes_per_sec = 0
760 self._start_io_stat_thread()
761
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700762 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500763 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700764 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700765 import builder
766 if self._builder is None:
767 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500768 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700769
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700770 @cherrypy.expose
Dan Shif8eb0d12013-08-01 17:52:06 -0700771 def is_staged(self, **kwargs):
772 """Check if artifacts have been downloaded.
773
Amin Hassani08e42d22019-06-03 00:31:30 -0700774 Args:
Chris Sosa6b0c6172013-08-05 17:01:33 -0700775 async: True to return without waiting for download to complete.
776 artifacts: Comma separated list of named artifacts to download.
777 These are defined in artifact_info and have their implementation
778 in build_artifact.py.
779 files: Comma separated list of file artifacts to stage. These
780 will be available as is in the corresponding static directory with no
781 custom post-processing.
782
Amin Hassani08e42d22019-06-03 00:31:30 -0700783 Returns: True of all artifacts are staged.
Dan Shif8eb0d12013-08-01 17:52:06 -0700784
Amin Hassani08e42d22019-06-03 00:31:30 -0700785 Examples:
Dan Shif8eb0d12013-08-01 17:52:06 -0700786 To check if autotest and test_suites are staged:
787 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
788 artifacts=autotest,test_suites
789 """
Gabe Black3b567202015-09-23 14:07:59 -0700790 dl, factory = _get_downloader_and_factory(kwargs)
Aviv Keshet57d18172016-06-18 20:39:09 -0700791 response = str(dl.IsStaged(factory))
792 _Log('Responding to is_staged %s request with %r', kwargs, response)
793 return response
Dan Shi59ae7092013-06-04 14:37:27 -0700794
Chris Sosa76e44b92013-01-31 12:11:38 -0800795 @cherrypy.expose
Prashanth Ba06d2d22014-03-07 15:35:19 -0800796 def list_image_dir(self, **kwargs):
797 """Take an archive url and list the contents in its staged directory.
798
799 Args:
Amin Hassani08e42d22019-06-03 00:31:30 -0700800 archive_url: Google Storage URL for the build.
Prashanth Ba06d2d22014-03-07 15:35:19 -0800801
Amin Hassani08e42d22019-06-03 00:31:30 -0700802 Examples:
Prashanth Ba06d2d22014-03-07 15:35:19 -0800803 To list the contents of where this devserver should have staged
804 gs://image-archive/<board>-release/<build> call:
805 http://devserver_url:<port>/list_image_dir?archive_url=<gs://..>
806
807 Returns:
808 A string with information about the contents of the image directory.
809 """
Gabe Black3b567202015-09-23 14:07:59 -0700810 dl = _get_downloader(kwargs)
Prashanth Ba06d2d22014-03-07 15:35:19 -0800811 try:
Gabe Black3b567202015-09-23 14:07:59 -0700812 image_dir_contents = dl.ListBuildDir()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800813 except build_artifact.ArtifactDownloadError as e:
814 return 'Cannot list the contents of staged artifacts. %s' % e
815 if not image_dir_contents:
Gabe Black3b567202015-09-23 14:07:59 -0700816 return '%s has not been staged on this devserver.' % dl.DescribeSource()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800817 return image_dir_contents
818
819 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800820 def stage(self, **kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700821 """Downloads and caches build artifacts.
Chris Sosa76e44b92013-01-31 12:11:38 -0800822
Gabe Black3b567202015-09-23 14:07:59 -0700823 Downloads and caches build artifacts, possibly from a Google Storage URL,
Dan Shi72b16132015-10-08 12:10:33 -0700824 or from Android's build server. Returns once these have been downloaded
Gabe Black3b567202015-09-23 14:07:59 -0700825 on the devserver. A call to this will attempt to cache non-specified
826 artifacts in the background for the given from the given URL following
827 the principle of spatial locality. Spatial locality of different
Chris Sosa76e44b92013-01-31 12:11:38 -0800828 artifacts is explicitly defined in the build_artifact module.
829
830 These artifacts will then be available from the static/ sub-directory of
831 the devserver.
832
833 Args:
834 archive_url: Google Storage URL for the build.
Simran Basi4243a862014-12-12 12:48:33 -0800835 local_path: Local path for the build.
Prathmesh Prabhu58d08932018-01-19 15:08:19 -0800836 delete_source: Only meaningful with local_path. bool to indicate if the
837 source files should be deleted. This is especially useful when staging
838 a file locally in resource constrained environments as it allows us to
839 move the relevant files locally instead of copying them.
Dan Shif8eb0d12013-08-01 17:52:06 -0700840 async: True to return without waiting for download to complete.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700841 artifacts: Comma separated list of named artifacts to download.
842 These are defined in artifact_info and have their implementation
843 in build_artifact.py.
844 files: Comma separated list of files to stage. These
845 will be available as is in the corresponding static directory with no
846 custom post-processing.
Laurence Goodbyf5c958d2016-01-14 18:23:56 -0800847 clean: True to remove any previously staged artifacts first.
Chris Sosa76e44b92013-01-31 12:11:38 -0800848
Amin Hassani08e42d22019-06-03 00:31:30 -0700849 Examples:
Chris Sosa76e44b92013-01-31 12:11:38 -0800850 To download the autotest and test suites tarballs:
851 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
852 artifacts=autotest,test_suites
853 To download the full update payload:
854 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
855 artifacts=full_payload
Chris Sosa6b0c6172013-08-05 17:01:33 -0700856 To download just a file called blah.bin:
857 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
858 files=blah.bin
Chris Sosa76e44b92013-01-31 12:11:38 -0800859
860 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 16:39:34 -0700861 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 12:11:38 -0800862
863 Note for this example, relative path is the archive_url stripped of its
864 basename i.e. path/ in the examples above. Specific example:
865
866 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
867
868 Will get staged to:
869
joychened64b222013-06-21 16:39:34 -0700870 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Chris Sosa76e44b92013-01-31 12:11:38 -0800871 """
Gabe Black3b567202015-09-23 14:07:59 -0700872 dl, factory = _get_downloader_and_factory(kwargs)
873
Dan Shi59ae7092013-06-04 14:37:27 -0700874 with DevServerRoot._staging_thread_count_lock:
875 DevServerRoot._staging_thread_count += 1
876 try:
Laurence Goodbyf5c958d2016-01-14 18:23:56 -0800877 boolean_string = kwargs.get('clean')
878 clean = xbuddy.XBuddy.ParseBoolean(boolean_string)
879 if clean and os.path.exists(dl.GetBuildDir()):
880 _Log('Removing %s' % dl.GetBuildDir())
881 shutil.rmtree(dl.GetBuildDir())
Gabe Black3b567202015-09-23 14:07:59 -0700882 async = kwargs.get('async', False)
883 dl.Download(factory, async=async)
Dan Shi59ae7092013-06-04 14:37:27 -0700884 finally:
885 with DevServerRoot._staging_thread_count_lock:
886 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800887 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700888
889 @cherrypy.expose
xixuan52c2fba2016-05-20 17:02:48 -0700890 def cros_au(self, **kwargs):
891 """Auto-update a CrOS DUT.
892
893 Args:
894 kwargs:
895 host_name: the hostname of the DUT to auto-update.
896 build_name: the build name for update the DUT.
897 force_update: Force an update even if the version installed is the
898 same. Default: False.
899 full_update: If True, do not run stateful update, directly force a full
900 reimage. If False, try stateful update first if the dut is already
901 installed with the same version.
902 async: Whether the auto_update function is ran in the background.
David Rileyee75de22017-11-02 10:48:15 -0700903 quick_provision: Whether the quick provision path is attempted first.
xixuan52c2fba2016-05-20 17:02:48 -0700904
905 Returns:
906 A tuple includes two elements:
907 a boolean variable represents whether the auto-update process is
908 successfully started.
909 an integer represents the background auto-update process id.
910 """
911 _check_base_args_for_auto_update(kwargs)
912
913 host_name = kwargs['host_name']
914 build_name = kwargs['build_name']
915 force_update = _parse_boolean_arg(kwargs, 'force_update')
916 full_update = _parse_boolean_arg(kwargs, 'full_update')
917 async = _parse_boolean_arg(kwargs, 'async')
xixuanac89ce82016-11-30 16:48:20 -0800918 original_build = _parse_string_arg(kwargs, 'original_build')
David Haddock90e49442017-04-07 19:14:09 -0700919 payload_filename = _parse_string_arg(kwargs, 'payload_filename')
David Haddock20559612017-06-28 22:15:08 -0700920 clobber_stateful = _parse_boolean_arg(kwargs, 'clobber_stateful')
David Rileyee75de22017-11-02 10:48:15 -0700921 quick_provision = _parse_boolean_arg(kwargs, 'quick_provision')
922
923 devserver_url = updater.GetDevserverUrl()
924 static_url = updater.GetStaticUrl()
xixuan52c2fba2016-05-20 17:02:48 -0700925
926 if async:
927 path = os.path.dirname(os.path.abspath(__file__))
928 execute_file = os.path.join(path, 'cros_update.py')
929 args = (AUTO_UPDATE_CMD % (execute_file, host_name, build_name,
930 updater.static_dir))
xixuanac89ce82016-11-30 16:48:20 -0800931
932 # The original_build's format is like: link/3428.210.0
933 # The corresponding release_archive_url's format is like:
934 # gs://chromeos-releases/stable-channel/link/3428.210.0
935 if original_build:
936 release_archive_url = _build_uri_from_build_name(original_build)
937 # First staging the stateful.tgz synchronousely.
938 self.stage(files='stateful.tgz', async=False,
939 archive_url=release_archive_url)
940 args = ('%s --original_build %s' % (args, original_build))
941
xixuan52c2fba2016-05-20 17:02:48 -0700942 if force_update:
943 args = ('%s --force_update' % args)
944
945 if full_update:
946 args = ('%s --full_update' % args)
947
David Haddock90e49442017-04-07 19:14:09 -0700948 if payload_filename:
949 args = ('%s --payload_filename %s' % (args, payload_filename))
950
David Haddock20559612017-06-28 22:15:08 -0700951 if clobber_stateful:
952 args = ('%s --clobber_stateful' % args)
953
David Rileyee75de22017-11-02 10:48:15 -0700954 if quick_provision:
955 args = ('%s --quick_provision' % args)
956
957 if devserver_url:
958 args = ('%s --devserver_url %s' % (args, devserver_url))
959
960 if static_url:
961 args = ('%s --static_url %s' % (args, static_url))
962
xixuan2a0970a2016-08-10 12:12:44 -0700963 p = subprocess.Popen([args], shell=True, preexec_fn=os.setsid)
964 pid = os.getpgid(p.pid)
xixuan52c2fba2016-05-20 17:02:48 -0700965
966 # Pre-write status in the track_status_file before the first call of
967 # 'get_au_status' to make sure that the track_status_file exists.
xixuan2a0970a2016-08-10 12:12:44 -0700968 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -0700969 progress_tracker.WriteStatus('CrOS update is just started.')
970
xixuan2a0970a2016-08-10 12:12:44 -0700971 return json.dumps((True, pid))
xixuan52c2fba2016-05-20 17:02:48 -0700972 else:
973 cros_update_trigger = cros_update.CrOSUpdateTrigger(
xixuanac89ce82016-11-30 16:48:20 -0800974 host_name, build_name, updater.static_dir, force_update=force_update,
David Rileyee75de22017-11-02 10:48:15 -0700975 full_update=full_update, original_build=original_build,
976 quick_provision=quick_provision, devserver_url=devserver_url,
977 static_url=static_url)
xixuan52c2fba2016-05-20 17:02:48 -0700978 cros_update_trigger.TriggerAU()
xixuan27d50442017-08-09 10:38:25 -0700979 return json.dumps((True, -1))
xixuan52c2fba2016-05-20 17:02:48 -0700980
981 @cherrypy.expose
982 def get_au_status(self, **kwargs):
983 """Check if the auto-update task is finished.
984
985 It handles 4 cases:
986 1. If an error exists in the track_status_file, delete the track file and
987 raise it.
988 2. If cros-update process is finished, delete the file and return the
989 success result.
990 3. If the process is not running, delete the track file and raise an error
991 about 'the process is terminated due to unknown reason'.
992 4. If the track_status_file does not exist, kill the process if it exists,
993 and raise the IOError.
994
995 Args:
996 kwargs:
997 host_name: the hostname of the DUT to auto-update.
998 pid: the background process id of cros-update.
999
1000 Returns:
xixuan28d99072016-10-06 12:24:16 -07001001 A dict with three elements:
xixuan52c2fba2016-05-20 17:02:48 -07001002 a boolean variable represents whether the auto-update process is
1003 finished.
1004 a string represents the current auto-update process status.
1005 For example, 'Transfer Devserver/Stateful Update Package'.
xixuan28d99072016-10-06 12:24:16 -07001006 a detailed error message paragraph if there exists an Auto-Update
1007 error, in which the last line shows the main exception. Empty
1008 string otherwise.
xixuan52c2fba2016-05-20 17:02:48 -07001009 """
1010 if 'host_name' not in kwargs:
Amin Hassani08e42d22019-06-03 00:31:30 -07001011 raise common_util.DevServerHTTPError(httplib.INTERNAL_SERVER_ERROR,
1012 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -07001013
1014 if 'pid' not in kwargs:
Amin Hassani08e42d22019-06-03 00:31:30 -07001015 raise common_util.DevServerHTTPError(httplib.INTERNAL_SERVER_ERROR,
1016 KEY_ERROR_MSG % 'pid')
xixuan52c2fba2016-05-20 17:02:48 -07001017
1018 host_name = kwargs['host_name']
1019 pid = kwargs['pid']
1020 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
1021
xixuan28d99072016-10-06 12:24:16 -07001022 result_dict = {'finished': False, 'status': '', 'detailed_error_msg': ''}
xixuan52c2fba2016-05-20 17:02:48 -07001023 try:
1024 result = progress_tracker.ReadStatus()
1025 if result.startswith(cros_update_progress.ERROR_TAG):
xixuan28d99072016-10-06 12:24:16 -07001026 result_dict['detailed_error_msg'] = result[len(
1027 cros_update_progress.ERROR_TAG):]
xixuan28681fd2016-11-23 11:13:56 -08001028 elif result == cros_update_progress.FINISHED:
xixuan28d99072016-10-06 12:24:16 -07001029 result_dict['finished'] = True
1030 result_dict['status'] = result
xixuan28681fd2016-11-23 11:13:56 -08001031 elif not cros_update_progress.IsProcessAlive(pid):
xixuan28d99072016-10-06 12:24:16 -07001032 result_dict['detailed_error_msg'] = (
1033 'Cros_update process terminated midway due to unknown reason. '
1034 'Last update status was %s' % result)
xixuan28681fd2016-11-23 11:13:56 -08001035 else:
1036 result_dict['status'] = result
1037 except IOError as e:
1038 if pid and cros_update_progress.IsProcessAlive(pid):
xixuan2a0970a2016-08-10 12:12:44 -07001039 os.killpg(int(pid), signal.SIGKILL)
xixuan52c2fba2016-05-20 17:02:48 -07001040
xixuan28681fd2016-11-23 11:13:56 -08001041 result_dict['detailed_error_msg'] = str(e)
1042
1043 return json.dumps(result_dict)
xixuan52c2fba2016-05-20 17:02:48 -07001044
1045 @cherrypy.expose
David Riley6d5fca02017-10-31 10:35:47 -07001046 def post_au_status(self, status, **kwargs):
1047 """Updates the status of an auto-update task.
1048
1049 Callers will need to POST to this URL with a body of MIME-type
1050 "multipart/form-data".
1051 The body should include a single argument, 'status', containing the
1052 AU status to record.
1053
1054 Args:
1055 status: The updated status.
1056 kwargs:
1057 host_name: the hostname of the DUT to auto-update.
1058 pid: the background process id of cros-update.
1059 """
1060 if 'host_name' not in kwargs:
Amin Hassani08e42d22019-06-03 00:31:30 -07001061 raise common_util.DevServerHTTPError(httplib.INTERNAL_SERVER_ERROR,
1062 KEY_ERROR_MSG % 'host_name')
David Riley6d5fca02017-10-31 10:35:47 -07001063
1064 if 'pid' not in kwargs:
Amin Hassani08e42d22019-06-03 00:31:30 -07001065 raise common_util.DevServerHTTPError(httplib.INTERNAL_SERVER_ERROR,
1066 KEY_ERROR_MSG % 'pid')
David Riley6d5fca02017-10-31 10:35:47 -07001067
1068 host_name = kwargs['host_name']
1069 pid = kwargs['pid']
David Riley3cea2582017-11-24 22:03:01 -08001070 status = status.rstrip()
1071 _Log('Recording status for %s (%s): %s' % (host_name, pid, status))
David Riley6d5fca02017-10-31 10:35:47 -07001072 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
1073
David Riley3cea2582017-11-24 22:03:01 -08001074 progress_tracker.WriteStatus(status)
David Riley6d5fca02017-10-31 10:35:47 -07001075
1076 return 'True'
1077
1078 @cherrypy.expose
xixuan52c2fba2016-05-20 17:02:48 -07001079 def handler_cleanup(self, **kwargs):
xixuan3bc974e2016-10-18 17:21:43 -07001080 """Clean track status log and temp directory for CrOS auto-update process.
xixuan52c2fba2016-05-20 17:02:48 -07001081
1082 Args:
1083 kwargs:
1084 host_name: the hostname of the DUT to auto-update.
1085 pid: the background process id of cros-update.
1086 """
1087 if 'host_name' not in kwargs:
Amin Hassani08e42d22019-06-03 00:31:30 -07001088 raise common_util.DevServerHTTPError(httplib.INTERNAL_SERVER_ERROR,
1089 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -07001090
1091 if 'pid' not in kwargs:
Amin Hassani08e42d22019-06-03 00:31:30 -07001092 raise common_util.DevServerHTTPError(httplib.INTERNAL_SERVER_ERROR,
1093 KEY_ERROR_MSG % 'pid')
xixuan52c2fba2016-05-20 17:02:48 -07001094
1095 host_name = kwargs['host_name']
1096 pid = kwargs['pid']
1097 cros_update_progress.DelTrackStatusFile(host_name, pid)
xixuan3bc974e2016-10-18 17:21:43 -07001098 cros_update_progress.DelAUTempDirectory(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -07001099
1100 @cherrypy.expose
1101 def kill_au_proc(self, **kwargs):
1102 """Kill CrOS auto-update process using given process id.
1103
1104 Args:
1105 kwargs:
1106 host_name: Kill all the CrOS auto-update process of this host.
1107
1108 Returns:
1109 True if all processes are killed properly.
1110 """
1111 if 'host_name' not in kwargs:
Amin Hassani08e42d22019-06-03 00:31:30 -07001112 raise common_util.DevServerHTTPError(httplib.INTERNAL_SERVER_ERROR,
1113 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -07001114
xixuan447ad9d2017-02-28 14:46:20 -08001115 cur_pid = kwargs.get('pid')
1116
xixuan52c2fba2016-05-20 17:02:48 -07001117 host_name = kwargs['host_name']
xixuan3bc974e2016-10-18 17:21:43 -07001118 track_log_list = cros_update_progress.GetAllTrackStatusFileByHostName(
1119 host_name)
xixuan52c2fba2016-05-20 17:02:48 -07001120 for log in track_log_list:
1121 # The track log's full path is: path/host_name_pid.log
1122 # Use splitext to remove file extension, then parse pid from the
1123 # filename.
1124 pid = os.path.splitext(os.path.basename(log))[0][len(host_name)+1:]
xixuan447ad9d2017-02-28 14:46:20 -08001125 _clear_process(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -07001126
xixuan447ad9d2017-02-28 14:46:20 -08001127 if cur_pid:
1128 _clear_process(host_name, cur_pid)
xixuan52c2fba2016-05-20 17:02:48 -07001129
1130 return 'True'
1131
1132 @cherrypy.expose
1133 def collect_cros_au_log(self, **kwargs):
1134 """Collect CrOS auto-update log.
1135
1136 Args:
1137 kwargs:
1138 host_name: the hostname of the DUT to auto-update.
1139 pid: the background process id of cros-update.
1140
1141 Returns:
David Haddock9f459632017-05-11 14:45:46 -07001142 A dictionary containing the execute log file and any hostlog files.
xixuan52c2fba2016-05-20 17:02:48 -07001143 """
1144 if 'host_name' not in kwargs:
Amin Hassani08e42d22019-06-03 00:31:30 -07001145 raise common_util.DevServerHTTPError(httplib.INTERNAL_SERVER_ERROR,
1146 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -07001147
1148 if 'pid' not in kwargs:
Amin Hassani08e42d22019-06-03 00:31:30 -07001149 raise common_util.DevServerHTTPError(httplib.INTERNAL_SERVER_ERROR,
1150 KEY_ERROR_MSG % 'pid')
xixuan52c2fba2016-05-20 17:02:48 -07001151
1152 host_name = kwargs['host_name']
1153 pid = kwargs['pid']
xixuan3bc974e2016-10-18 17:21:43 -07001154
1155 # Fetch the execute log recorded by cros_update_progress.
xixuan1bbfaba2016-10-13 17:53:22 -07001156 au_log = cros_update_progress.ReadExecuteLogFile(host_name, pid)
1157 cros_update_progress.DelExecuteLogFile(host_name, pid)
David Haddock9f459632017-05-11 14:45:46 -07001158 # Fetch the cros_au host_logs if they exist
1159 au_hostlogs = cros_update_progress.ReadAUHostLogFiles(host_name, pid)
1160 return json.dumps({'cros_au_log': au_log, 'host_logs': au_hostlogs})
xixuan1bbfaba2016-10-13 17:53:22 -07001161
xixuan52c2fba2016-05-20 17:02:48 -07001162 @cherrypy.expose
Dan Shi2f136862016-02-11 15:38:38 -08001163 def locate_file(self, **kwargs):
1164 """Get the path to the given file name.
1165
1166 This method looks up the given file name inside specified build artifacts.
1167 One use case is to help caller to locate an apk file inside a build
1168 artifact. The location of the apk file could be different based on the
1169 branch and target.
1170
1171 Args:
1172 file_name: Name of the file to look for.
1173 artifacts: A list of artifact names to search for the file.
1174
1175 Returns:
1176 Path to the file with the given name. It's relative to the folder for the
1177 build, e.g., DATA/priv-app/sl4a/sl4a.apk
Dan Shi2f136862016-02-11 15:38:38 -08001178 """
1179 dl, _ = _get_downloader_and_factory(kwargs)
1180 try:
Joe Brennan1691f8e2017-03-15 15:53:36 -07001181 file_name = kwargs['file_name']
Dan Shi2f136862016-02-11 15:38:38 -08001182 artifacts = kwargs['artifacts']
1183 except KeyError:
1184 raise DevServerError('`file_name` and `artifacts` are required to search '
1185 'for a file in build artifacts.')
1186 build_path = dl.GetBuildDir()
1187 for artifact in artifacts:
1188 # Get the unzipped folder of the artifact. If it's not defined in
1189 # ARTIFACT_UNZIP_FOLDER_MAP, assume the files are unzipped to the build
1190 # directory directly.
1191 folder = artifact_info.ARTIFACT_UNZIP_FOLDER_MAP.get(artifact, '')
1192 artifact_path = os.path.join(build_path, folder)
1193 for root, _, filenames in os.walk(artifact_path):
Joe Brennan1691f8e2017-03-15 15:53:36 -07001194 if file_name in set([f for f in filenames]):
Dan Shi2f136862016-02-11 15:38:38 -08001195 return os.path.relpath(os.path.join(root, file_name), build_path)
1196 raise DevServerError('File `%s` can not be found in artifacts: %s' %
1197 (file_name, artifacts))
1198
1199 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -08001200 def setup_telemetry(self, **kwargs):
1201 """Extracts and sets up telemetry
1202
1203 This method goes through the telemetry deps packages, and stages them on
1204 the devserver to be used by the drones and the telemetry tests.
1205
1206 Args:
1207 archive_url: Google Storage URL for the build.
1208
1209 Returns:
1210 Path to the source folder for the telemetry codebase once it is staged.
1211 """
Gabe Black3b567202015-09-23 14:07:59 -07001212 dl = _get_downloader(kwargs)
Simran Basi4baad082013-02-14 13:39:18 -08001213
Gabe Black3b567202015-09-23 14:07:59 -07001214 build_path = dl.GetBuildDir()
Simran Basi4baad082013-02-14 13:39:18 -08001215 deps_path = os.path.join(build_path, 'autotest/packages')
1216 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
1217 src_folder = os.path.join(telemetry_path, 'src')
1218
1219 with self._telemetry_lock_dict.lock(telemetry_path):
1220 if os.path.exists(src_folder):
1221 # Telemetry is already fully stage return
1222 return src_folder
1223
1224 common_util.MkDirP(telemetry_path)
1225
1226 # Copy over the required deps tar balls to the telemetry directory.
1227 for dep in TELEMETRY_DEPS:
1228 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -07001229 if not os.path.exists(dep_path):
1230 # This dep does not exist (could be new), do not extract it.
1231 continue
Simran Basi4baad082013-02-14 13:39:18 -08001232 try:
1233 common_util.ExtractTarball(dep_path, telemetry_path)
1234 except common_util.CommonUtilError as e:
1235 shutil.rmtree(telemetry_path)
1236 raise DevServerError(str(e))
1237
1238 # By default all the tarballs extract to test_src but some parts of
1239 # the telemetry code specifically hardcoded to exist inside of 'src'.
1240 test_src = os.path.join(telemetry_path, 'test_src')
1241 try:
1242 shutil.move(test_src, src_folder)
1243 except shutil.Error:
1244 # This can occur if src_folder already exists. Remove and retry move.
1245 shutil.rmtree(src_folder)
Gabe Black3b567202015-09-23 14:07:59 -07001246 raise DevServerError(
1247 'Failure in telemetry setup for build %s. Appears that the '
1248 'test_src to src move failed.' % dl.GetBuild())
Simran Basi4baad082013-02-14 13:39:18 -08001249
1250 return src_folder
1251
1252 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -08001253 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -07001254 """Symbolicates a minidump using pre-downloaded symbols, returns it.
1255
1256 Callers will need to POST to this URL with a body of MIME-type
1257 "multipart/form-data".
1258 The body should include a single argument, 'minidump', containing the
1259 binary-formatted minidump to symbolicate.
1260
Chris Masone816e38c2012-05-02 12:22:36 -07001261 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -08001262 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -07001263 minidump: The binary minidump file to symbolicate.
1264 """
Chris Sosa76e44b92013-01-31 12:11:38 -08001265 # Ensure the symbols have been staged.
Dan Shif08fe492016-10-04 14:39:25 -07001266 # Try debug.tar.xz first, then debug.tgz
1267 for artifact in (artifact_info.SYMBOLS_ONLY, artifact_info.SYMBOLS):
1268 kwargs['artifacts'] = artifact
1269 dl = _get_downloader(kwargs)
1270
1271 try:
1272 if self.stage(**kwargs) == 'Success':
1273 break
1274 except build_artifact.ArtifactDownloadError:
1275 continue
1276 else:
Gabe Black3b567202015-09-23 14:07:59 -07001277 raise DevServerError('Failed to stage symbols for %s' %
1278 dl.DescribeSource())
Chris Sosa76e44b92013-01-31 12:11:38 -08001279
Chris Masone816e38c2012-05-02 12:22:36 -07001280 to_return = ''
1281 with tempfile.NamedTemporaryFile() as local:
1282 while True:
1283 data = minidump.file.read(8192)
1284 if not data:
1285 break
1286 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -08001287
Chris Masone816e38c2012-05-02 12:22:36 -07001288 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -08001289
Gabe Black3b567202015-09-23 14:07:59 -07001290 symbols_directory = os.path.join(dl.GetBuildDir(), 'debug', 'breakpad')
Chris Sosa76e44b92013-01-31 12:11:38 -08001291
xixuanab744382017-04-27 10:41:27 -07001292 # The location of minidump_stackwalk is defined in chromeos-admin.
Chris Sosa76e44b92013-01-31 12:11:38 -08001293 stackwalk = subprocess.Popen(
xixuanab744382017-04-27 10:41:27 -07001294 ['/usr/local/bin/minidump_stackwalk', local.name, symbols_directory],
Chris Sosa76e44b92013-01-31 12:11:38 -08001295 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1296
Chris Masone816e38c2012-05-02 12:22:36 -07001297 to_return, error_text = stackwalk.communicate()
1298 if stackwalk.returncode != 0:
1299 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
1300 error_text, stackwalk.returncode))
1301
1302 return to_return
1303
1304 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -08001305 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 15:31:36 -04001306 """Return a string representing the latest build for a given target.
1307
1308 Args:
1309 target: The build target, typically a combination of the board and the
1310 type of build e.g. x86-mario-release.
1311 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
1312 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-07 18:21:26 -08001313
Scott Zawalski16954532012-03-20 15:31:36 -04001314 Returns:
1315 A string representation of the latest build if one exists, i.e.
1316 R19-1993.0.0-a1-b1480.
1317 An empty string if no latest could be found.
1318 """
Don Garrettf84631a2014-01-07 18:21:26 -08001319 if not kwargs:
Scott Zawalski16954532012-03-20 15:31:36 -04001320 return _PrintDocStringAsHTML(self.latestbuild)
1321
Don Garrettf84631a2014-01-07 18:21:26 -08001322 if 'target' not in kwargs:
Amin Hassani08e42d22019-06-03 00:31:30 -07001323 raise common_util.DevServerHTTPError(httplib.INTERNAL_SERVER_ERROR,
1324 'Error: target= is required!')
Dan Shi61305df2015-10-26 16:52:35 -07001325
1326 if _is_android_build_request(kwargs):
1327 branch = kwargs.get('branch', None)
1328 target = kwargs.get('target', None)
1329 if not target or not branch:
1330 raise DevServerError(
xixuan52c2fba2016-05-20 17:02:48 -07001331 'Both target and branch must be specified to query for the latest '
1332 'Android build.')
Dan Shi61305df2015-10-26 16:52:35 -07001333 return android_build.BuildAccessor.GetLatestBuildID(target, branch)
1334
Scott Zawalski16954532012-03-20 15:31:36 -04001335 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001336 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-07 18:21:26 -08001337 updater.static_dir, kwargs['target'],
1338 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -07001339 except common_util.CommonUtilError as errmsg:
Amin Hassani08e42d22019-06-03 00:31:30 -07001340 raise common_util.DevServerHTTPError(httplib.INTERNAL_SERVER_ERROR,
1341 str(errmsg))
Scott Zawalski16954532012-03-20 15:31:36 -04001342
1343 @cherrypy.expose
xixuan7efd0002016-04-14 15:34:01 -07001344 def list_suite_controls(self, **kwargs):
1345 """Return a list of contents of all known control files.
1346
1347 Example URL:
1348 To List all control files' content:
1349 http://dev-server/list_suite_controls?suite_name=bvt&
1350 build=daisy_spring-release/R29-4279.0.0
1351
1352 Args:
1353 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
1354 suite_name: List the control files belonging to that suite.
1355
1356 Returns:
Dan Shia1cd6522016-04-18 16:07:21 -07001357 A dictionary of all control files's path to its content for given suite.
xixuan7efd0002016-04-14 15:34:01 -07001358 """
1359 if not kwargs:
1360 return _PrintDocStringAsHTML(self.controlfiles)
1361
1362 if 'build' not in kwargs:
Amin Hassani08e42d22019-06-03 00:31:30 -07001363 raise common_util.DevServerHTTPError(httplib.INTERNAL_SERVER_ERROR,
1364 'Error: build= is required!')
xixuan7efd0002016-04-14 15:34:01 -07001365
1366 if 'suite_name' not in kwargs:
Amin Hassani08e42d22019-06-03 00:31:30 -07001367 raise common_util.DevServerHTTPError(httplib.INTERNAL_SERVER_ERROR,
Dan Shia1cd6522016-04-18 16:07:21 -07001368 'Error: suite_name= is required!')
xixuan7efd0002016-04-14 15:34:01 -07001369
1370 control_file_list = [
1371 line.rstrip() for line in common_util.GetControlFileListForSuite(
1372 updater.static_dir, kwargs['build'],
1373 kwargs['suite_name']).splitlines()]
1374
Dan Shia1cd6522016-04-18 16:07:21 -07001375 control_file_content_dict = {}
xixuan7efd0002016-04-14 15:34:01 -07001376 for control_path in control_file_list:
Dan Shia1cd6522016-04-18 16:07:21 -07001377 control_file_content_dict[control_path] = (common_util.GetControlFile(
xixuan7efd0002016-04-14 15:34:01 -07001378 updater.static_dir, kwargs['build'], control_path))
1379
Dan Shia1cd6522016-04-18 16:07:21 -07001380 return json.dumps(control_file_content_dict)
xixuan7efd0002016-04-14 15:34:01 -07001381
1382 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -08001383 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 17:17:28 -05001384 """Return a control file or a list of all known control files.
1385
1386 Example URL:
1387 To List all control files:
beepsbd337242013-07-09 22:44:06 -07001388 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
1389 To List all control files for, say, the bvt suite:
1390 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -05001391 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -05001392 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 -05001393
1394 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -05001395 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -05001396 control_path: If you want the contents of a control file set this
1397 to the path. E.g. client/site_tests/sleeptest/control
1398 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -07001399 suite_name: If control_path is not specified but a suite_name is
1400 specified, list the control files belonging to that suite instead of
1401 all control files. The empty string for suite_name will list all control
1402 files for the build.
Don Garrettf84631a2014-01-07 18:21:26 -08001403
Scott Zawalski4647ce62012-01-03 17:17:28 -05001404 Returns:
1405 Contents of a control file if control_path is provided.
1406 A list of control files if no control_path is provided.
1407 """
Don Garrettf84631a2014-01-07 18:21:26 -08001408 if not kwargs:
Scott Zawalski4647ce62012-01-03 17:17:28 -05001409 return _PrintDocStringAsHTML(self.controlfiles)
1410
Don Garrettf84631a2014-01-07 18:21:26 -08001411 if 'build' not in kwargs:
Amin Hassani08e42d22019-06-03 00:31:30 -07001412 raise common_util.DevServerHTTPError(httplib.INTERNAL_SERVER_ERROR,
1413 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -05001414
Don Garrettf84631a2014-01-07 18:21:26 -08001415 if 'control_path' not in kwargs:
1416 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-09 22:44:06 -07001417 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-07 18:21:26 -08001418 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-09 22:44:06 -07001419 else:
1420 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-07 18:21:26 -08001421 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -05001422 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001423 return common_util.GetControlFile(
Don Garrettf84631a2014-01-07 18:21:26 -08001424 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -08001425
1426 @cherrypy.expose
Simran Basi99e63c02014-05-20 10:39:52 -07001427 def xbuddy_translate(self, *args, **kwargs):
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001428 """Translates an xBuddy path to a real path to artifact if it exists.
1429
1430 Args:
Simran Basi99e63c02014-05-20 10:39:52 -07001431 args: An xbuddy path in the form of {local|remote}/build_id/artifact.
1432 Local searches the devserver's static directory. Remote searches a
1433 Google Storage image archive.
1434
1435 Kwargs:
1436 image_dir: Google Storage image archive to search in if requesting a
1437 remote artifact. If none uses the default bucket.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001438
1439 Returns:
Simran Basi99e63c02014-05-20 10:39:52 -07001440 String in the format of build_id/artifact as stored on the local server
1441 or in Google Storage.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001442 """
Simran Basi99e63c02014-05-20 10:39:52 -07001443 build_id, filename = self._xbuddy.Translate(
Gabe Black3b567202015-09-23 14:07:59 -07001444 args, image_dir=kwargs.get('image_dir'))
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001445 response = os.path.join(build_id, filename)
1446 _Log('Path translation requested, returning: %s', response)
1447 return response
1448
1449 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -07001450 def xbuddy(self, *args, **kwargs):
1451 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -07001452
1453 Args:
joycheneaf4cfc2013-07-02 08:38:57 -07001454 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -07001455 components of the path. The path can be understood as
1456 "{local|remote}/build_id/artifact" where build_id is composed of
1457 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -07001458
joychen121fc9b2013-08-02 14:30:30 -07001459 The first path element is optional, and can be "remote" or "local"
1460 If local (the default), devserver will not attempt to access Google
1461 Storage, and will only search the static directory for the files.
1462 If remote, devserver will try to obtain the artifact off GS if it's
1463 not found locally.
1464 The board is the familiar board name, optionally suffixed.
1465 The version can be the google storage version number, and may also be
1466 any of a number of xBuddy defined version aliases that will be
1467 translated into the latest built image that fits the description.
1468 Defaults to latest.
1469 The artifact is one of a number of image or artifact aliases used by
1470 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -07001471
1472 Kwargs:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001473 for_update: {true|false}
1474 if true, pregenerates the update payloads for the image,
1475 and returns the update uri to pass to the
1476 update_engine_client.
joychen3cb228e2013-06-12 12:13:13 -07001477 return_dir: {true|false}
1478 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001479 relative_path: {true|false}
1480 if set to true, returns the relative path to the payload
1481 directory from static_dir.
joychen3cb228e2013-06-12 12:13:13 -07001482 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -07001483 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -07001484 or
joycheneaf4cfc2013-07-02 08:38:57 -07001485 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -07001486
1487 Returns:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001488 If |for_update|, returns a redirect to the image or update file
1489 on the devserver. E.g.,
1490 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
1491 chromium-test-image.bin
1492 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
1493 http://host:port/static/x86-generic-release/R26-4000.0.0/
1494 If |relative_path| is true, return a relative path the folder where the
1495 payloads are. E.g.,
1496 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 12:13:13 -07001497 """
Chris Sosa75490802013-09-30 17:21:45 -07001498 boolean_string = kwargs.get('for_update')
1499 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001500 boolean_string = kwargs.get('return_dir')
1501 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
1502 boolean_string = kwargs.get('relative_path')
1503 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -07001504
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001505 if return_dir and relative_path:
Chris Sosa4b951602014-04-09 20:26:07 -07001506 raise common_util.DevServerHTTPError(
Amin Hassani08e42d22019-06-03 00:31:30 -07001507 httplib.INTERNAL_SERVER_ERROR,
1508 'Cannot specify both return_dir and relative_path')
Chris Sosa75490802013-09-30 17:21:45 -07001509
1510 # For updates, we optimize downloading of test images.
1511 file_name = None
1512 build_id = None
1513 if for_update:
1514 try:
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001515 build_id = self._xbuddy.StageTestArtifactsForUpdate(args)
Chris Sosa75490802013-09-30 17:21:45 -07001516 except build_artifact.ArtifactDownloadError:
1517 build_id = None
1518
1519 if not build_id:
1520 build_id, file_name = self._xbuddy.Get(args)
1521
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001522 if for_update:
1523 _Log('Payload generation triggered by request')
1524 # Forces payload to be in cache and symlinked into build_id dir.
Chris Sosa75490802013-09-30 17:21:45 -07001525 updater.GetUpdateForLabel(autoupdate.FORCED_UPDATE, build_id,
1526 image_name=file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001527
1528 response = None
1529 if return_dir:
1530 response = os.path.join(cherrypy.request.base, 'static', build_id)
1531 _Log('Directory requested, returning: %s', response)
1532 elif relative_path:
1533 response = build_id
1534 _Log('Relative path requested, returning: %s', response)
1535 elif for_update:
1536 response = os.path.join(cherrypy.request.base, 'update', build_id)
1537 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 12:13:13 -07001538 else:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001539 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 14:30:30 -07001540 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001541 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 14:30:30 -07001542 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -07001543
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001544 return response
1545
joychen3cb228e2013-06-12 12:13:13 -07001546 @cherrypy.expose
1547 def xbuddy_list(self):
1548 """Lists the currently available images & time since last access.
1549
Gilad Arnold452fd272014-02-04 11:09:28 -08001550 Returns:
1551 A string representation of a list of tuples [(build_id, time since last
1552 access),...]
joychen3cb228e2013-06-12 12:13:13 -07001553 """
1554 return self._xbuddy.List()
1555
1556 @cherrypy.expose
1557 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 11:09:28 -08001558 """Returns the number of images cached by xBuddy."""
joychen3cb228e2013-06-12 12:13:13 -07001559 return self._xbuddy.Capacity()
1560
1561 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001562 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001563 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001564 return ('Welcome to the Dev Server!<br>\n'
1565 '<br>\n'
1566 'Here are the available methods, click for documentation:<br>\n'
1567 '<br>\n'
1568 '%s' %
1569 '<br>\n'.join(
1570 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001571 for name in _FindExposedMethods(
1572 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001573
1574 @cherrypy.expose
1575 def doc(self, *args):
1576 """Shows the documentation for available methods / URLs.
1577
Amin Hassani08e42d22019-06-03 00:31:30 -07001578 Examples:
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001579 http://myhost/doc/update
1580 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001581 name = '/'.join(args)
1582 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001583 if not method:
1584 raise DevServerError("No exposed method named `%s'" % name)
1585 if not method.__doc__:
1586 raise DevServerError("No documentation for exposed method `%s'" % name)
1587 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -07001588
Dale Curtisc9aaf3a2011-08-09 15:47:40 -07001589 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001590 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001591 """Handles an update check from a Chrome OS client.
1592
1593 The HTTP request should contain the standard Omaha-style XML blob. The URL
1594 line may contain an additional intermediate path to the update payload.
1595
joychen121fc9b2013-08-02 14:30:30 -07001596 This request can be handled in one of 4 ways, depending on the devsever
1597 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -07001598
joychen121fc9b2013-08-02 14:30:30 -07001599 1. No intermediate path
1600 If no intermediate path is given, the default behavior is to generate an
1601 update payload from the latest test image locally built for the board
1602 specified in the xml. Devserver serves the generated payload.
1603
1604 2. Path explicitly invokes XBuddy
1605 If there is a path given, it can explicitly invoke xbuddy by prefixing it
1606 with 'xbuddy'. This path is then used to acquire an image binary for the
1607 devserver to generate an update payload from. Devserver then serves this
1608 payload.
1609
1610 3. Path is left for the devserver to interpret.
1611 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
1612 to generate a payload from the test image in that directory and serve it.
1613
1614 4. The devserver is in a 'forced' mode. TO BE DEPRECATED
1615 This comes from the usage of --forced_payload or --image when starting the
1616 devserver. No matter what path (or no path) gets passed in, devserver will
1617 serve the update payload (--forced_payload) or generate an update payload
1618 from the image (--image).
1619
1620 Examples:
1621 1. No intermediate path
1622 update_engine_client --omaha_url=http://myhost/update
1623 This generates an update payload from the latest test image locally built
1624 for the board specified in the xml.
1625
1626 2. Explicitly invoke xbuddy
1627 update_engine_client --omaha_url=
1628 http://myhost/update/xbuddy/remote/board/version/dev
1629 This would go to GS to download the dev image for the board, from which
1630 the devserver would generate a payload to serve.
1631
1632 3. Give a path for devserver to interpret
1633 update_engine_client --omaha_url=http://myhost/update/some/random/path
1634 This would attempt, in order to:
1635 a) Generate an update from a test image binary if found in
1636 static_dir/some/random/path.
1637 b) Serve an update payload found in static_dir/some/random/path.
1638 c) Hope that some/random/path takes the form "board/version" and
1639 and attempt to download an update payload for that board/version
1640 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001641 """
joychen121fc9b2013-08-02 14:30:30 -07001642 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -08001643 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -07001644 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-11 19:49:01 -07001645
joychen121fc9b2013-08-02 14:30:30 -07001646 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001647
Dan Shiafd0e492015-05-27 14:23:51 -07001648 @require_psutil()
1649 def _get_io_stats(self):
1650 """Get the IO stats as a dictionary.
1651
Gabe Black3b567202015-09-23 14:07:59 -07001652 Returns:
1653 A dictionary of IO stats collected by psutil.
Dan Shiafd0e492015-05-27 14:23:51 -07001654 """
1655 return {'disk_read_bytes_per_second': self.disk_read_bytes_per_sec,
1656 'disk_write_bytes_per_second': self.disk_write_bytes_per_sec,
1657 'disk_total_bytes_per_second': (self.disk_read_bytes_per_sec +
1658 self.disk_write_bytes_per_sec),
1659 'network_sent_bytes_per_second': self.network_sent_bytes_per_sec,
1660 'network_recv_bytes_per_second': self.network_recv_bytes_per_sec,
1661 'network_total_bytes_per_second': (self.network_sent_bytes_per_sec +
1662 self.network_recv_bytes_per_sec),
1663 'cpu_percent': psutil.cpu_percent(),}
1664
Dan Shi7247f9c2016-06-01 09:19:09 -07001665
1666 def _get_process_count(self, process_cmd_pattern):
1667 """Get the count of processes that match the given command pattern.
1668
1669 Args:
1670 process_cmd_pattern: The regex pattern of process command to match.
1671
1672 Returns:
1673 The count of processes that match the given command pattern.
1674 """
1675 try:
xixuanac89ce82016-11-30 16:48:20 -08001676 # Use Popen instead of check_output since the latter cannot run with old
1677 # python version (less than 2.7)
1678 proc = subprocess.Popen(
Congbin Guoe527bec2019-05-10 16:14:26 -07001679 ['pgrep', '-fc', process_cmd_pattern],
xixuanac89ce82016-11-30 16:48:20 -08001680 stdout=subprocess.PIPE,
1681 stderr=subprocess.PIPE,
Congbin Guoe527bec2019-05-10 16:14:26 -07001682 )
xixuanac89ce82016-11-30 16:48:20 -08001683 cmd_output, cmd_error = proc.communicate()
1684 if cmd_error:
1685 _Log('Error happened when getting process count: %s' % cmd_error)
1686
1687 return int(cmd_output)
Dan Shi7247f9c2016-06-01 09:19:09 -07001688 except subprocess.CalledProcessError:
1689 return 0
1690
1691
Dan Shif5ce2de2013-04-25 16:06:32 -07001692 @cherrypy.expose
1693 def check_health(self):
1694 """Collect the health status of devserver to see if it's ready for staging.
1695
Gilad Arnold452fd272014-02-04 11:09:28 -08001696 Returns:
1697 A JSON dictionary containing all or some of the following fields:
1698 free_disk (int): free disk space in GB
1699 staging_thread_count (int): number of devserver threads currently staging
1700 an image
Dan Shi7247f9c2016-06-01 09:19:09 -07001701 apache_client_count (int): count of Apache processes.
1702 telemetry_test_count (int): count of telemetry tests.
1703 gsutil_count (int): count of gsutil processes.
Dan Shif5ce2de2013-04-25 16:06:32 -07001704 """
1705 # Get free disk space.
1706 stat = os.statvfs(updater.static_dir)
1707 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
Congbin Guo9ffd6ff2019-05-09 15:54:17 -07001708 apache_client_count = self._get_process_count('bin/apache2? -k start')
Dan Shi7247f9c2016-06-01 09:19:09 -07001709 telemetry_test_count = self._get_process_count('python.*telemetry')
1710 gsutil_count = self._get_process_count('gsutil')
xixuan447ad9d2017-02-28 14:46:20 -08001711 au_process_count = len(cros_update_progress.GetAllRunningAUProcess())
Dan Shif5ce2de2013-04-25 16:06:32 -07001712
Dan Shiafd0e492015-05-27 14:23:51 -07001713 health_data = {
Dan Shif5ce2de2013-04-25 16:06:32 -07001714 'free_disk': free_disk,
Dan Shid76e6bb2016-01-28 22:28:51 -08001715 'staging_thread_count': DevServerRoot._staging_thread_count,
1716 'apache_client_count': apache_client_count,
Dan Shi7247f9c2016-06-01 09:19:09 -07001717 'telemetry_test_count': telemetry_test_count,
xixuan447ad9d2017-02-28 14:46:20 -08001718 'gsutil_count': gsutil_count,
1719 'au_process_count': au_process_count,
1720 }
Dan Shiafd0e492015-05-27 14:23:51 -07001721 health_data.update(self._get_io_stats() or {})
1722
1723 return json.dumps(health_data)
Dan Shif5ce2de2013-04-25 16:06:32 -07001724
1725
Chris Sosadbc20082012-12-10 13:39:11 -08001726def _CleanCache(cache_dir, wipe):
1727 """Wipes any excess cached items in the cache_dir.
1728
1729 Args:
1730 cache_dir: the directory we are wiping from.
1731 wipe: If True, wipe all the contents -- not just the excess.
1732 """
1733 if wipe:
1734 # Clear the cache and exit on error.
1735 cmd = 'rm -rf %s/*' % cache_dir
1736 if os.system(cmd) != 0:
1737 _Log('Failed to clear the cache with %s' % cmd)
1738 sys.exit(1)
1739 else:
1740 # Clear all but the last N cached updates
1741 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
1742 (cache_dir, CACHED_ENTRIES))
1743 if os.system(cmd) != 0:
1744 _Log('Failed to clean up old delta cache files with %s' % cmd)
1745 sys.exit(1)
1746
1747
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001748def _AddTestingOptions(parser):
1749 group = optparse.OptionGroup(
1750 parser, 'Advanced Testing Options', 'These are used by test scripts and '
1751 'developers writing integration tests utilizing the devserver. They are '
1752 'not intended to be really used outside the scope of someone '
1753 'knowledgable about the test.')
1754 group.add_option('--exit',
1755 action='store_true',
1756 help='do not start the server (yet pregenerate/clear cache)')
1757 group.add_option('--host_log',
1758 action='store_true', default=False,
1759 help='record history of host update events (/api/hostlog)')
1760 group.add_option('--max_updates',
Gabe Black3b567202015-09-23 14:07:59 -07001761 metavar='NUM', default=-1, type='int',
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001762 help='maximum number of update checks handled positively '
1763 '(default: unlimited)')
David Zeuthen52ccd012013-10-31 12:58:26 -07001764 group.add_option('--public_key',
1765 metavar='PATH', default=None,
1766 help='path to the public key in pem format. If this is set '
1767 'the devserver will transmit a base64 encoded version of '
1768 'the content in the Omaha-style XML response.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001769 group.add_option('--proxy_port',
1770 metavar='PORT', default=None, type='int',
1771 help='port to have the client connect to -- basically the '
1772 'devserver lies to the update to tell it to get the payload '
1773 'from a different port that will proxy the request back to '
1774 'the devserver. The proxy must be managed outside the '
1775 'devserver.')
1776 group.add_option('--remote_payload',
1777 action='store_true', default=False,
Chris Sosa4b951602014-04-09 20:26:07 -07001778 help='Payload is being served from a remote machine. With '
1779 'this setting enabled, this devserver instance serves as '
1780 'just an Omaha server instance. In this mode, the '
1781 'devserver enforces a few extra components of the Omaha '
Chris Sosafc715442014-04-09 20:45:23 -07001782 'protocol, such as hardware class, being sent.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001783 group.add_option('-u', '--urlbase',
1784 metavar='URL',
Gabe Black3b567202015-09-23 14:07:59 -07001785 help='base URL for update images, other than the '
1786 'devserver. Use in conjunction with remote_payload.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001787 parser.add_option_group(group)
1788
1789
1790def _AddUpdateOptions(parser):
1791 group = optparse.OptionGroup(
1792 parser, 'Autoupdate Options', 'These options can be used to change '
1793 'how the devserver either generates or serve update payloads. Please '
1794 'note that all of these option affect how a payload is generated and so '
1795 'do not work in archive-only mode.')
1796 group.add_option('--board',
1797 help='By default the devserver will create an update '
1798 'payload from the latest image built for the board '
1799 'a device that is requesting an update has. When we '
1800 'pre-generate an update (see below) and we do not specify '
1801 'another update_type option like image or payload, the '
1802 'devserver needs to know the board to generate the latest '
1803 'image for. This is that board.')
1804 group.add_option('--critical_update',
1805 action='store_true', default=False,
1806 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001807 group.add_option('--image',
1808 metavar='FILE',
1809 help='Generate and serve an update using this image to any '
1810 'device that requests an update.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001811 group.add_option('--payload',
1812 metavar='PATH',
1813 help='use the update payload from specified directory '
1814 '(update.gz).')
1815 group.add_option('-p', '--pregenerate_update',
1816 action='store_true', default=False,
1817 help='pre-generate the update payload before accepting '
1818 'update requests. Useful to help debug payload generation '
1819 'issues quickly. Also if an update payload will take a '
1820 'long time to generate, a client may timeout if you do not'
1821 'pregenerate the update.')
1822 group.add_option('--src_image',
1823 metavar='PATH', default='',
1824 help='If specified, delta updates will be generated using '
1825 'this image as the source image. Delta updates are when '
1826 'you are updating from a "source image" to a another '
1827 'image.')
1828 parser.add_option_group(group)
1829
1830
1831def _AddProductionOptions(parser):
1832 group = optparse.OptionGroup(
1833 parser, 'Advanced Server Options', 'These options can be used to changed '
1834 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001835 group.add_option('--clear_cache',
1836 action='store_true', default=False,
1837 help='At startup, removes all cached entries from the'
1838 'devserver\'s cache.')
1839 group.add_option('--logfile',
1840 metavar='PATH',
1841 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -07001842 group.add_option('--pidfile',
1843 metavar='PATH',
1844 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 11:04:13 -08001845 group.add_option('--portfile',
1846 metavar='PATH',
1847 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001848 group.add_option('--production',
1849 action='store_true', default=False,
1850 help='have the devserver use production values when '
1851 'starting up. This includes using more threads and '
1852 'performing less logging.')
1853 parser.add_option_group(group)
1854
1855
Paul Hobbsef4e0702016-06-27 17:01:42 -07001856def MakeLogHandler(logfile):
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001857 """Create a LogHandler instance used to log all messages."""
1858 hdlr_cls = handlers.TimedRotatingFileHandler
1859 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
xixuan3d48bff2017-01-30 19:00:09 -08001860 interval=_LOG_ROTATION_INTERVAL,
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001861 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -07001862 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001863 return hdlr
1864
1865
Chris Sosacde6bf42012-05-31 18:36:39 -07001866def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001867 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -08001868 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -07001869
1870 # get directory that the devserver is run from
1871 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -07001872 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -07001873 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001874 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -07001875 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -07001876 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001877 parser.add_option('--port',
1878 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 13:13:28 -08001879 help=('port for the dev server to use; if zero, binds to '
1880 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001881 parser.add_option('-t', '--test_image',
1882 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -07001883 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -07001884 parser.add_option('-x', '--xbuddy_manage_builds',
1885 action='store_true',
1886 default=False,
1887 help='If set, allow xbuddy to manage images in'
1888 'build/images.')
Dan Shi72b16132015-10-08 12:10:33 -07001889 parser.add_option('-a', '--android_build_credential',
1890 default=None,
1891 help='Path to a json file which contains the credential '
1892 'needed to access Android builds.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001893 _AddProductionOptions(parser)
1894 _AddUpdateOptions(parser)
1895 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -07001896 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001897
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001898 # Handle options that must be set globally in cherrypy. Do this
1899 # work up front, because calls to _Log() below depend on this
1900 # initialization.
1901 if options.production:
1902 cherrypy.config.update({'environment': 'production'})
1903 if not options.logfile:
1904 cherrypy.config.update({'log.screen': True})
1905 else:
1906 cherrypy.config.update({'log.error_file': '',
1907 'log.access_file': ''})
Paul Hobbsef4e0702016-06-27 17:01:42 -07001908 hdlr = MakeLogHandler(options.logfile)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001909 # Pylint can't seem to process these two calls properly
1910 # pylint: disable=E1101
1911 cherrypy.log.access_log.addHandler(hdlr)
1912 cherrypy.log.error_log.addHandler(hdlr)
1913 # pylint: enable=E1101
1914
joychened64b222013-06-21 16:39:34 -07001915 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001916 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001917
joychened64b222013-06-21 16:39:34 -07001918 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001919 # If our devserver is only supposed to serve payloads, we shouldn't be
1920 # mucking with the cache at all. If the devserver hadn't previously
1921 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001922 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001923 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001924 else:
1925 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001926
Chris Sosadbc20082012-12-10 13:39:11 -08001927 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 16:39:34 -07001928 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001929
joychen121fc9b2013-08-02 14:30:30 -07001930 _xbuddy = xbuddy.XBuddy(options.xbuddy_manage_builds,
1931 options.board,
joychen121fc9b2013-08-02 14:30:30 -07001932 static_dir=options.static_dir)
Chris Sosa75490802013-09-30 17:21:45 -07001933 if options.clear_cache and options.xbuddy_manage_builds:
1934 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 14:30:30 -07001935
Chris Sosa6a3697f2013-01-29 16:44:43 -08001936 # We allow global use here to share with cherrypy classes.
1937 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001938 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001939 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001940 _xbuddy,
joychened64b222013-06-21 16:39:34 -07001941 static_dir=options.static_dir,
Andrew de los Reyes52620802010-04-12 13:40:07 -07001942 urlbase=options.urlbase,
Chris Sosa5d342a22010-09-28 16:54:41 -07001943 forced_image=options.image,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001944 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001945 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-28 23:42:37 -07001946 src_image=options.src_image,
Chris Sosa08d55a22011-01-19 16:08:02 -08001947 board=options.board,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001948 copy_to_static_root=not options.exit,
David Zeuthen52ccd012013-10-31 12:58:26 -07001949 public_key=options.public_key,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001950 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001951 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001952 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001953 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001954 )
Chris Sosa7c931362010-10-11 19:49:01 -07001955
Chris Sosa6a3697f2013-01-29 16:44:43 -08001956 if options.pregenerate_update:
1957 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 15:46:22 -07001958
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001959 if options.exit:
1960 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001961
joychen3cb228e2013-06-12 12:13:13 -07001962 dev_server = DevServerRoot(_xbuddy)
1963
Gilad Arnold11fbef42014-02-10 11:04:13 -08001964 # Patch CherryPy to support binding to any available port (--port=0).
1965 cherrypy_ext.ZeroPortPatcher.DoPatch(cherrypy)
1966
Chris Sosa855b8932013-08-21 13:24:55 -07001967 if options.pidfile:
1968 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1969
Gilad Arnold11fbef42014-02-10 11:04:13 -08001970 if options.portfile:
1971 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1972
Dan Shiafd5c6c2016-01-07 10:27:03 -08001973 if (options.android_build_credential and
1974 os.path.exists(options.android_build_credential)):
1975 try:
1976 with open(options.android_build_credential) as f:
1977 android_build.BuildAccessor.credential_info = json.load(f)
1978 except ValueError as e:
1979 _Log('Failed to load the android build credential: %s. Error: %s.' %
1980 (options.android_build_credential, e))
joychen3cb228e2013-06-12 12:13:13 -07001981 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001982
1983
1984if __name__ == '__main__':
1985 main()