blob: 82f7d40d4c2042149492e8e7b4f153c4a7924831 [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
27and generate a payload that the requested system can autoupdate to. In addition,
28it accepts gmerge requests from devices that will stage the newest version of
joychen84d13772013-08-06 09:17:23 -070029a particular package from a developer's chroot onto a requesting device.
Chris Sosa3ae4dc12013-03-29 11:47:00 -070030
31For example:
32gmerge gmerge -d <devserver_url>
33
34devserver will see if a newer package of gmerge is available. If gmerge is
35cros_work'd on, it will re-build gmerge. After this, gmerge will install that
36version of gmerge that the devserver just created/found.
37
38For autoupdates, there are many more advanced options that can help specify
39how to update and which payload to give to a requester.
40"""
41
Gabe Black3b567202015-09-23 14:07:59 -070042from __future__ import print_function
Chris Sosa7c931362010-10-11 19:49:01 -070043
Gilad Arnold55a2a372012-10-02 09:46:32 -070044import json
David Riley2fcb0122017-11-02 11:25:39 -070045import optparse # pylint: disable=deprecated-module
rtc@google.comded22402009-10-26 22:36:21 +000046import os
Scott Zawalski4647ce62012-01-03 17:17:28 -050047import re
Simran Basi4baad082013-02-14 13:39:18 -080048import shutil
xixuan52c2fba2016-05-20 17:02:48 -070049import signal
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -080050import socket
Chris Masone816e38c2012-05-02 12:22:36 -070051import subprocess
J. Richard Barnette3d977b82013-04-23 11:05:19 -070052import sys
Chris Masone816e38c2012-05-02 12:22:36 -070053import tempfile
Dan Shi59ae7092013-06-04 14:37:27 -070054import threading
Dan Shiafd0e492015-05-27 14:23:51 -070055import time
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -070056import types
J. Richard Barnette3d977b82013-04-23 11:05:19 -070057from logging import handlers
58
59import cherrypy
David Riley2fcb0122017-11-02 11:25:39 -070060# pylint: disable=no-name-in-module
Chris Sosa855b8932013-08-21 13:24:55 -070061from cherrypy import _cplogging as cplogging
David Riley2fcb0122017-11-02 11:25:39 -070062from cherrypy.process import plugins # pylint: disable=import-error
63# pylint: enable=no-name-in-module
rtc@google.comded22402009-10-26 22:36:21 +000064
Richard Barnettedf35c322017-08-18 17:02:13 -070065# This must happen before any local modules get a chance to import
66# anything from chromite. Otherwise, really bad things will happen, and
67# you will _not_ understand why.
68import setup_chromite # pylint: disable=unused-import
69
Chris Sosa0356d3b2010-09-16 15:46:22 -070070import autoupdate
Dan Shi2f136862016-02-11 15:38:38 -080071import artifact_info
Chris Sosa75490802013-09-30 17:21:45 -070072import build_artifact
Gilad Arnold11fbef42014-02-10 11:04:13 -080073import cherrypy_ext
Gilad Arnoldc65330c2012-09-20 15:17:48 -070074import common_util
Simran Basief83d6a2014-08-28 14:32:01 -070075import devserver_constants
Chris Sosa47a7d4e2012-03-28 11:26:55 -070076import downloader
Chris Sosa7cd23202013-10-15 17:22:57 -070077import gsutil_util
Gilad Arnoldc65330c2012-09-20 15:17:48 -070078import log_util
joychen3cb228e2013-06-12 12:13:13 -070079import xbuddy
Gilad Arnoldc65330c2012-09-20 15:17:48 -070080
Gilad Arnoldc65330c2012-09-20 15:17:48 -070081# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080082def _Log(message, *args):
83 return log_util.LogWithTag('DEVSERVER', message, *args)
Chris Sosa0356d3b2010-09-16 15:46:22 -070084
Dan Shiafd0e492015-05-27 14:23:51 -070085try:
86 import psutil
87except ImportError:
88 # Ignore psutil import failure. This is for backwards compatibility, so
89 # "cros flash" can still update duts with build without psutil installed.
90 # The reason is that, during cros flash, local devserver code is copied over
91 # to DUT, and devserver will be running inside DUT to stage the build.
92 _Log('Python module psutil is not installed, devserver load data will not be '
93 'collected')
94 psutil = None
Dan Shi94dcbe82015-06-08 20:51:13 -070095except OSError as e:
96 # Ignore error like following. psutil may not work properly in builder. Ignore
97 # the error as load information of devserver is not used in builder.
98 # OSError: [Errno 2] No such file or directory: '/dev/pts/0'
99 _Log('psutil is failed to be imported, error: %s. devserver load data will '
100 'not be collected.', e)
101 psutil = None
102
xixuanac89ce82016-11-30 16:48:20 -0800103# Use try-except to skip unneccesary import for simple use case, eg. running
104# devserver on host.
105try:
106 import cros_update
xixuanac89ce82016-11-30 16:48:20 -0800107except ImportError as e:
108 _Log('cros_update cannot be imported: %r', e)
109 cros_update = None
xixuana4f4e712017-05-08 15:17:54 -0700110
111try:
112 import cros_update_progress
113except ImportError as e:
114 _Log('cros_update_progress cannot be imported: %r', e)
xixuanac89ce82016-11-30 16:48:20 -0800115 cros_update_progress = None
116
xixuanac89ce82016-11-30 16:48:20 -0800117try:
118 from chromite.lib.paygen import gspaths
119except ImportError as e:
120 _Log('chromite cannot be imported: %r', e)
121 gspaths = None
122
Dan Shi72b16132015-10-08 12:10:33 -0700123try:
124 import android_build
125except ImportError as e:
126 # Ignore android_build import failure. This is to support devserver running
127 # inside a ChromeOS device triggered by cros flash. Most ChromeOS test images
128 # do not have google-api-python-client module and they don't need to support
129 # Android updating, therefore, ignore the import failure here.
Dan Shi72b16132015-10-08 12:10:33 -0700130 android_build = None
Frank Farzan40160872011-12-12 18:39:18 -0800131
Chris Sosa417e55d2011-01-25 16:40:48 -0800132CACHED_ENTRIES = 12
Don Garrettf90edf02010-11-16 17:36:14 -0800133
Simran Basi4baad082013-02-14 13:39:18 -0800134TELEMETRY_FOLDER = 'telemetry_src'
135TELEMETRY_DEPS = ['dep-telemetry_dep.tar.bz2',
136 'dep-page_cycler_dep.tar.bz2',
Simran Basi0d078682013-03-22 16:40:04 -0700137 'dep-chrome_test.tar.bz2',
138 'dep-perf_data_dep.tar.bz2']
Simran Basi4baad082013-02-14 13:39:18 -0800139
Chris Sosa0356d3b2010-09-16 15:46:22 -0700140# Sets up global to share between classes.
rtc@google.com21a5ca32009-11-04 18:23:23 +0000141updater = None
rtc@google.comded22402009-10-26 22:36:21 +0000142
xixuan3d48bff2017-01-30 19:00:09 -0800143# Log rotation parameters. These settings correspond to twice a day once
144# devserver is started, with about two weeks (28 backup files) of old logs
145# kept for backup.
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700146#
xixuan3d48bff2017-01-30 19:00:09 -0800147# For more, see the documentation in standard python library for
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700148# logging.handlers.TimedRotatingFileHandler
xixuan3d48bff2017-01-30 19:00:09 -0800149_LOG_ROTATION_TIME = 'H'
150_LOG_ROTATION_INTERVAL = 12 # hours
151_LOG_ROTATION_BACKUP = 28 # backup counts
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700152
Dan Shiafd0e492015-05-27 14:23:51 -0700153# Number of seconds between the collection of disk and network IO counters.
154STATS_INTERVAL = 10.0
Frank Farzan40160872011-12-12 18:39:18 -0800155
xixuan52c2fba2016-05-20 17:02:48 -0700156# Auto-update parameters
157
158# Error msg for missing key in CrOS auto-update.
Xixuan Wu32af9f12017-11-13 14:11:44 -0800159KEY_ERROR_MSG = 'Key Error in RPC: %s= is required'
xixuan52c2fba2016-05-20 17:02:48 -0700160
161# Command of running auto-update.
162AUTO_UPDATE_CMD = '/usr/bin/python -u %s -d %s -b %s --static_dir %s'
163
164
Chris Sosa9164ca32012-03-28 11:04:50 -0700165class DevServerError(Exception):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700166 """Exception class used by this module."""
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700167
168
Dan Shiafd0e492015-05-27 14:23:51 -0700169def require_psutil():
Gabe Black3b567202015-09-23 14:07:59 -0700170 """Decorator for functions require psutil to run."""
Dan Shiafd0e492015-05-27 14:23:51 -0700171 def deco_require_psutil(func):
172 """Wrapper of the decorator function.
173
Gabe Black3b567202015-09-23 14:07:59 -0700174 Args:
175 func: function to be called.
Dan Shiafd0e492015-05-27 14:23:51 -0700176 """
177 def func_require_psutil(*args, **kwargs):
178 """Decorator for functions require psutil to run.
179
180 If psutil is not installed, skip calling the function.
181
Gabe Black3b567202015-09-23 14:07:59 -0700182 Args:
183 *args: arguments for function to be called.
184 **kwargs: keyword arguments for function to be called.
Dan Shiafd0e492015-05-27 14:23:51 -0700185 """
186 if psutil:
187 return func(*args, **kwargs)
188 else:
189 _Log('Python module psutil is not installed. Function call %s is '
190 'skipped.' % func)
191 return func_require_psutil
192 return deco_require_psutil
193
194
Gabe Black3b567202015-09-23 14:07:59 -0700195def _canonicalize_archive_url(archive_url):
196 """Canonicalizes archive_url strings.
197
198 Raises:
199 DevserverError: if archive_url is not set.
200 """
201 if archive_url:
202 if not archive_url.startswith('gs://'):
203 raise DevServerError("Archive URL isn't from Google Storage (%s) ." %
204 archive_url)
205
206 return archive_url.rstrip('/')
207 else:
208 raise DevServerError("Must specify an archive_url in the request")
209
210
211def _canonicalize_local_path(local_path):
212 """Canonicalizes |local_path| strings.
213
214 Raises:
215 DevserverError: if |local_path| is not set.
216 """
217 # Restrict staging of local content to only files within the static
218 # directory.
219 local_path = os.path.abspath(local_path)
220 if not local_path.startswith(updater.static_dir):
221 raise DevServerError('Local path %s must be a subdirectory of the static'
222 ' directory: %s' % (local_path, updater.static_dir))
223
224 return local_path.rstrip('/')
225
226
227def _get_artifacts(kwargs):
228 """Returns a tuple of named and file artifacts given the stage rpc kwargs.
229
230 Raises:
231 DevserverError if no artifacts would be returned.
232 """
233 artifacts = kwargs.get('artifacts')
234 files = kwargs.get('files')
235 if not artifacts and not files:
236 raise DevServerError('No artifacts specified.')
237
238 # Note we NEED to coerce files to a string as we get raw unicode from
239 # cherrypy and we treat files as strings elsewhere in the code.
240 return (str(artifacts).split(',') if artifacts else [],
241 str(files).split(',') if files else [])
242
243
Dan Shi61305df2015-10-26 16:52:35 -0700244def _is_android_build_request(kwargs):
245 """Check if a devserver call is for Android build, based on the arguments.
246
247 This method exams the request's arguments (os_type) to determine if the
248 request is for Android build. If os_type is set to `android`, returns True.
249 If os_type is not set or has other values, returns False.
250
251 Args:
252 kwargs: Keyword arguments for the request.
253
254 Returns:
255 True if the request is for Android build. False otherwise.
256 """
257 os_type = kwargs.get('os_type', None)
258 return os_type == 'android'
259
260
Gabe Black3b567202015-09-23 14:07:59 -0700261def _get_downloader(kwargs):
262 """Returns the downloader based on passed in arguments.
263
264 Args:
265 kwargs: Keyword arguments for the request.
266 """
267 local_path = kwargs.get('local_path')
268 if local_path:
269 local_path = _canonicalize_local_path(local_path)
270
271 dl = None
272 if local_path:
Prathmesh Prabhu58d08932018-01-19 15:08:19 -0800273 delete_source = _parse_boolean_arg(kwargs, 'delete_source')
274 dl = downloader.LocalDownloader(updater.static_dir, local_path,
275 delete_source=delete_source)
Gabe Black3b567202015-09-23 14:07:59 -0700276
Dan Shi61305df2015-10-26 16:52:35 -0700277 if not _is_android_build_request(kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700278 archive_url = kwargs.get('archive_url')
279 if not archive_url and not local_path:
280 raise DevServerError('Requires archive_url or local_path to be '
281 'specified.')
282 if archive_url and local_path:
283 raise DevServerError('archive_url and local_path can not both be '
284 'specified.')
285 if not dl:
286 archive_url = _canonicalize_archive_url(archive_url)
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700287 dl = downloader.GoogleStorageDownloader(
288 updater.static_dir, archive_url,
289 downloader.GoogleStorageDownloader.GetBuildIdFromArchiveURL(
290 archive_url))
Gabe Black3b567202015-09-23 14:07:59 -0700291 elif not dl:
292 target = kwargs.get('target', None)
Dan Shi72b16132015-10-08 12:10:33 -0700293 branch = kwargs.get('branch', None)
Dan Shi61305df2015-10-26 16:52:35 -0700294 build_id = kwargs.get('build_id', None)
295 if not target or not branch or not build_id:
Dan Shi72b16132015-10-08 12:10:33 -0700296 raise DevServerError(
Dan Shi61305df2015-10-26 16:52:35 -0700297 'target, branch, build ID must all be specified for downloading '
298 'Android build.')
Dan Shi72b16132015-10-08 12:10:33 -0700299 dl = downloader.AndroidBuildDownloader(updater.static_dir, branch, build_id,
300 target)
Gabe Black3b567202015-09-23 14:07:59 -0700301
302 return dl
303
304
305def _get_downloader_and_factory(kwargs):
306 """Returns the downloader and artifact factory based on passed in arguments.
307
308 Args:
309 kwargs: Keyword arguments for the request.
310 """
311 artifacts, files = _get_artifacts(kwargs)
312 dl = _get_downloader(kwargs)
313
314 if (isinstance(dl, downloader.GoogleStorageDownloader) or
315 isinstance(dl, downloader.LocalDownloader)):
316 factory_class = build_artifact.ChromeOSArtifactFactory
Dan Shi72b16132015-10-08 12:10:33 -0700317 elif isinstance(dl, downloader.AndroidBuildDownloader):
Gabe Black3b567202015-09-23 14:07:59 -0700318 factory_class = build_artifact.AndroidArtifactFactory
319 else:
320 raise DevServerError('Unrecognized value for downloader type: %s' %
321 type(dl))
322
323 factory = factory_class(dl.GetBuildDir(), artifacts, files, dl.GetBuild())
324
325 return dl, factory
326
327
Scott Zawalski4647ce62012-01-03 17:17:28 -0500328def _LeadingWhiteSpaceCount(string):
329 """Count the amount of leading whitespace in a string.
330
331 Args:
332 string: The string to count leading whitespace in.
Don Garrettf84631a2014-01-07 18:21:26 -0800333
Scott Zawalski4647ce62012-01-03 17:17:28 -0500334 Returns:
335 number of white space chars before characters start.
336 """
Gabe Black3b567202015-09-23 14:07:59 -0700337 matched = re.match(r'^\s+', string)
Scott Zawalski4647ce62012-01-03 17:17:28 -0500338 if matched:
339 return len(matched.group())
340
341 return 0
342
343
344def _PrintDocStringAsHTML(func):
345 """Make a functions docstring somewhat HTML style.
346
347 Args:
348 func: The function to return the docstring from.
Don Garrettf84631a2014-01-07 18:21:26 -0800349
Scott Zawalski4647ce62012-01-03 17:17:28 -0500350 Returns:
351 A string that is somewhat formated for a web browser.
352 """
353 # TODO(scottz): Make this parse Args/Returns in a prettier way.
354 # Arguments could be bolded and indented etc.
355 html_doc = []
356 for line in func.__doc__.splitlines():
357 leading_space = _LeadingWhiteSpaceCount(line)
358 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700359 line = '&nbsp;' * leading_space + line
Scott Zawalski4647ce62012-01-03 17:17:28 -0500360
361 html_doc.append('<BR>%s' % line)
362
363 return '\n'.join(html_doc)
364
365
Simran Basief83d6a2014-08-28 14:32:01 -0700366def _GetUpdateTimestampHandler(static_dir):
367 """Returns a handler to update directory staged.timestamp.
368
369 This handler resets the stage.timestamp whenever static content is accessed.
370
371 Args:
372 static_dir: Directory from which static content is being staged.
373
374 Returns:
375 A cherrypy handler to update the timestamp of accessed content.
376 """
377 def UpdateTimestampHandler():
378 if not '404' in cherrypy.response.status:
379 build_match = re.match(devserver_constants.STAGED_BUILD_REGEX,
380 cherrypy.request.path_info)
381 if build_match:
382 build_dir = os.path.join(static_dir, build_match.group('build'))
383 downloader.Downloader.TouchTimestampForStaged(build_dir)
384 return UpdateTimestampHandler
385
386
Chris Sosa7c931362010-10-11 19:49:01 -0700387def _GetConfig(options):
388 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800389
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800390 socket_host = '::'
Yu-Ju Hongc8d4af32013-11-12 15:14:26 -0800391 # Fall back to IPv4 when python is not configured with IPv6.
392 if not socket.has_ipv6:
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800393 socket_host = '0.0.0.0'
394
Simran Basief83d6a2014-08-28 14:32:01 -0700395 # Adds the UpdateTimestampHandler to cherrypy's tools. This tools executes
396 # on the on_end_resource hook. This hook is called once processing is
397 # complete and the response is ready to be returned.
398 cherrypy.tools.update_timestamp = cherrypy.Tool(
399 'on_end_resource', _GetUpdateTimestampHandler(options.static_dir))
400
David Riley2fcb0122017-11-02 11:25:39 -0700401 base_config = {
402 'global': {
403 'server.log_request_headers': True,
404 'server.protocol_version': 'HTTP/1.1',
405 'server.socket_host': socket_host,
406 'server.socket_port': int(options.port),
407 'response.timeout': 6000,
408 'request.show_tracebacks': True,
409 'server.socket_timeout': 60,
410 'server.thread_pool': 2,
411 'engine.autoreload.on': False,
412 },
413 '/api': {
414 # Gets rid of cherrypy parsing post file for args.
415 'request.process_request_body': False,
416 },
417 '/build': {
418 'response.timeout': 100000,
419 },
420 '/update': {
421 # Gets rid of cherrypy parsing post file for args.
422 'request.process_request_body': False,
423 'response.timeout': 10000,
424 },
425 # Sets up the static dir for file hosting.
426 '/static': {
427 'tools.staticdir.dir': options.static_dir,
428 'tools.staticdir.on': True,
429 'response.timeout': 10000,
430 'tools.update_timestamp.on': True,
431 },
432 }
Chris Sosa5f118ef2012-07-12 11:37:50 -0700433 if options.production:
Alex Miller93beca52013-07-30 19:25:09 -0700434 base_config['global'].update({'server.thread_pool': 150})
Chris Sosa7cd23202013-10-15 17:22:57 -0700435 # TODO(sosa): Do this more cleanly.
436 gsutil_util.GSUTIL_ATTEMPTS = 5
Scott Zawalski1c5e7cd2012-02-27 13:12:52 -0500437
Chris Sosa7c931362010-10-11 19:49:01 -0700438 return base_config
rtc@google.com64244662009-11-12 00:52:08 +0000439
Darin Petkove17164a2010-08-11 13:24:41 -0700440
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700441def _GetRecursiveMemberObject(root, member_list):
442 """Returns an object corresponding to a nested member list.
443
444 Args:
445 root: the root object to search
446 member_list: list of nested members to search
Don Garrettf84631a2014-01-07 18:21:26 -0800447
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700448 Returns:
449 An object corresponding to the member name list; None otherwise.
450 """
451 for member in member_list:
452 next_root = root.__class__.__dict__.get(member)
453 if not next_root:
454 return None
455 root = next_root
456 return root
457
458
459def _IsExposed(name):
460 """Returns True iff |name| has an `exposed' attribute and it is set."""
461 return hasattr(name, 'exposed') and name.exposed
462
463
Gilad Arnold748c8322012-10-12 09:51:35 -0700464def _GetExposedMethod(root, nested_member, ignored=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700465 """Returns a CherryPy-exposed method, if such exists.
466
467 Args:
468 root: the root object for searching
469 nested_member: a slash-joined path to the nested member
470 ignored: method paths to be ignored
Don Garrettf84631a2014-01-07 18:21:26 -0800471
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700472 Returns:
473 A function object corresponding to the path defined by |member_list| from
474 the |root| object, if the function is exposed and not ignored; None
475 otherwise.
476 """
Gilad Arnold748c8322012-10-12 09:51:35 -0700477 method = (not (ignored and nested_member in ignored) and
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700478 _GetRecursiveMemberObject(root, nested_member.split('/')))
Gabe Black3b567202015-09-23 14:07:59 -0700479 if method and type(method) == types.FunctionType and _IsExposed(method):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700480 return method
481
482
Gilad Arnold748c8322012-10-12 09:51:35 -0700483def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700484 """Finds exposed CherryPy methods.
485
486 Args:
487 root: the root object for searching
488 prefix: slash-joined chain of members leading to current object
489 unlisted: URLs to be excluded regardless of their exposed status
Don Garrettf84631a2014-01-07 18:21:26 -0800490
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700491 Returns:
492 List of exposed URLs that are not unlisted.
493 """
494 method_list = []
495 for member in sorted(root.__class__.__dict__.keys()):
496 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 09:51:35 -0700497 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700498 continue
499 member_obj = root.__class__.__dict__[member]
500 if _IsExposed(member_obj):
501 if type(member_obj) == types.FunctionType:
502 method_list.append(prefixed_member)
503 else:
504 method_list += _FindExposedMethods(
505 member_obj, prefixed_member, unlisted)
506 return method_list
507
508
xixuan52c2fba2016-05-20 17:02:48 -0700509def _check_base_args_for_auto_update(kwargs):
xixuanac89ce82016-11-30 16:48:20 -0800510 """Check basic args required for auto-update.
511
512 Args:
513 kwargs: the parameters to be checked.
514
515 Raises:
516 DevServerHTTPError if required parameters don't exist in kwargs.
517 """
xixuan52c2fba2016-05-20 17:02:48 -0700518 if 'host_name' not in kwargs:
519 raise common_util.DevServerHTTPError(KEY_ERROR_MSG % 'host_name')
520
521 if 'build_name' not in kwargs:
522 raise common_util.DevServerHTTPError(KEY_ERROR_MSG % 'build_name')
523
524
525def _parse_boolean_arg(kwargs, key):
xixuanac89ce82016-11-30 16:48:20 -0800526 """Parse boolean arg from kwargs.
527
528 Args:
529 kwargs: the parameters to be checked.
530 key: the key to be parsed.
531
532 Returns:
533 The boolean value of kwargs[key], or False if key doesn't exist in kwargs.
534
535 Raises:
536 DevServerHTTPError if kwargs[key] is not a boolean variable.
537 """
xixuan52c2fba2016-05-20 17:02:48 -0700538 if key in kwargs:
539 if kwargs[key] == 'True':
540 return True
541 elif kwargs[key] == 'False':
542 return False
543 else:
544 raise common_util.DevServerHTTPError(
545 'The value for key %s is not boolean.' % key)
546 else:
547 return False
548
xixuan447ad9d2017-02-28 14:46:20 -0800549
xixuanac89ce82016-11-30 16:48:20 -0800550def _parse_string_arg(kwargs, key):
551 """Parse string arg from kwargs.
552
553 Args:
554 kwargs: the parameters to be checked.
555 key: the key to be parsed.
556
557 Returns:
558 The string value of kwargs[key], or None if key doesn't exist in kwargs.
559 """
560 if key in kwargs:
561 return kwargs[key]
562 else:
563 return None
564
xixuan447ad9d2017-02-28 14:46:20 -0800565
xixuanac89ce82016-11-30 16:48:20 -0800566def _build_uri_from_build_name(build_name):
567 """Get build url from a given build name.
568
569 Args:
570 build_name: the build name to be parsed, whose format is
571 'board/release_version'.
572
573 Returns:
574 The release_archive_url on Google Storage for this build name.
575 """
576 return gspaths.ChromeosReleases.BuildUri(
577 cros_update.STABLE_BUILD_CHANNEL, build_name.split('/')[0],
578 build_name.split('/')[1])
xixuan52c2fba2016-05-20 17:02:48 -0700579
xixuan447ad9d2017-02-28 14:46:20 -0800580
581def _clear_process(host_name, pid):
582 """Clear AU process for given hostname and pid.
583
584 This clear includes:
585 1. kill process if it's alive.
586 2. delete the track status file of this process.
587 3. delete the executing log file of this process.
588
589 Args:
590 host_name: the host to execute auto-update.
591 pid: the background auto-update process id.
592 """
593 if cros_update_progress.IsProcessAlive(pid):
594 os.killpg(int(pid), signal.SIGKILL)
595
596 cros_update_progress.DelTrackStatusFile(host_name, pid)
597 cros_update_progress.DelExecuteLogFile(host_name, pid)
598
599
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700600class ApiRoot(object):
601 """RESTful API for Dev Server information."""
602 exposed = True
603
604 @cherrypy.expose
605 def hostinfo(self, ip):
606 """Returns a JSON dictionary containing information about the given ip.
607
Gilad Arnold1b908392012-10-05 11:36:27 -0700608 Args:
609 ip: address of host whose info is requested
Don Garrettf84631a2014-01-07 18:21:26 -0800610
Gilad Arnold1b908392012-10-05 11:36:27 -0700611 Returns:
612 A JSON dictionary containing all or some of the following fields:
613 last_event_type (int): last update event type received
614 last_event_status (int): last update event status received
615 last_known_version (string): last known version reported in update ping
616 forced_update_label (string): update label to force next update ping to
617 use, set by setnextupdate
618 See the OmahaEvent class in update_engine/omaha_request_action.h for
619 event type and status code definitions. If the ip does not exist an empty
620 string is returned.
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700621
Gilad Arnold1b908392012-10-05 11:36:27 -0700622 Example URL:
623 http://myhost/api/hostinfo?ip=192.168.1.5
624 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700625 return updater.HandleHostInfoPing(ip)
626
627 @cherrypy.expose
Gilad Arnold286a0062012-01-12 13:47:02 -0800628 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 11:36:27 -0700629 """Returns a JSON object containing a log of host event.
630
631 Args:
632 ip: address of host whose event log is requested, or `all'
Don Garrettf84631a2014-01-07 18:21:26 -0800633
Gilad Arnold1b908392012-10-05 11:36:27 -0700634 Returns:
635 A JSON encoded list (log) of dictionaries (events), each of which
636 containing a `timestamp' and other event fields, as described under
637 /api/hostinfo.
638
639 Example URL:
640 http://myhost/api/hostlog?ip=192.168.1.5
641 """
Gilad Arnold286a0062012-01-12 13:47:02 -0800642 return updater.HandleHostLogPing(ip)
643
644 @cherrypy.expose
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700645 def setnextupdate(self, ip):
646 """Allows the response to the next update ping from a host to be set.
647
648 Takes the IP of the host and an update label as normally provided to the
Gilad Arnold1b908392012-10-05 11:36:27 -0700649 /update command.
650 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700651 body_length = int(cherrypy.request.headers['Content-Length'])
652 label = cherrypy.request.rfile.read(body_length)
653
654 if label:
655 label = label.strip()
656 if label:
657 return updater.HandleSetUpdatePing(ip, label)
Chris Sosa4b951602014-04-09 20:26:07 -0700658 raise common_util.DevServerHTTPError(400, 'No label provided.')
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700659
660
Gilad Arnold55a2a372012-10-02 09:46:32 -0700661 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800662 def fileinfo(self, *args):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700663 """Returns information about a given staged file.
664
665 Args:
Don Garrettf84631a2014-01-07 18:21:26 -0800666 args: path to the file inside the server's static staging directory
667
Gilad Arnold55a2a372012-10-02 09:46:32 -0700668 Returns:
669 A JSON encoded dictionary with information about the said file, which may
670 contain the following keys/values:
Gilad Arnold1b908392012-10-05 11:36:27 -0700671 size (int): the file size in bytes
672 sha1 (string): a base64 encoded SHA1 hash
673 sha256 (string): a base64 encoded SHA256 hash
674
675 Example URL:
676 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 09:46:32 -0700677 """
Don Garrettf84631a2014-01-07 18:21:26 -0800678 file_path = os.path.join(updater.static_dir, *args)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700679 if not os.path.exists(file_path):
680 raise DevServerError('file not found: %s' % file_path)
681 try:
682 file_size = os.path.getsize(file_path)
683 file_sha1 = common_util.GetFileSha1(file_path)
684 file_sha256 = common_util.GetFileSha256(file_path)
685 except os.error, e:
686 raise DevServerError('failed to get info for file %s: %s' %
Gilad Arnolde74b3812013-04-22 11:27:38 -0700687 (file_path, e))
688
689 is_delta = autoupdate.Autoupdate.IsDeltaFormatFile(file_path)
690
691 return json.dumps({
692 autoupdate.Autoupdate.SIZE_ATTR: file_size,
693 autoupdate.Autoupdate.SHA1_ATTR: file_sha1,
694 autoupdate.Autoupdate.SHA256_ATTR: file_sha256,
695 autoupdate.Autoupdate.ISDELTA_ATTR: is_delta
696 })
Gilad Arnold55a2a372012-10-02 09:46:32 -0700697
Chris Sosa76e44b92013-01-31 12:11:38 -0800698
David Rochberg7c79a812011-01-19 14:24:45 -0500699class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700700 """The Root Class for the Dev Server.
701
702 CherryPy works as follows:
703 For each method in this class, cherrpy interprets root/path
704 as a call to an instance of DevServerRoot->method_name. For example,
705 a call to http://myhost/build will call build. CherryPy automatically
706 parses http args and places them as keyword arguments in each method.
707 For paths http://myhost/update/dir1/dir2, you can use *args so that
708 cherrypy uses the update method and puts the extra paths in args.
709 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700710 # Method names that should not be listed on the index page.
711 _UNLISTED_METHODS = ['index', 'doc']
712
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700713 api = ApiRoot()
Chris Sosa7c931362010-10-11 19:49:01 -0700714
Dan Shi59ae7092013-06-04 14:37:27 -0700715 # Number of threads that devserver is staging images.
716 _staging_thread_count = 0
717 # Lock used to lock increasing/decreasing count.
718 _staging_thread_count_lock = threading.Lock()
719
Dan Shiafd0e492015-05-27 14:23:51 -0700720 @require_psutil()
721 def _refresh_io_stats(self):
722 """A call running in a thread to update IO stats periodically."""
723 prev_disk_io_counters = psutil.disk_io_counters()
724 prev_network_io_counters = psutil.net_io_counters()
725 prev_read_time = time.time()
726 while True:
727 time.sleep(STATS_INTERVAL)
728 now = time.time()
729 interval = now - prev_read_time
730 prev_read_time = now
731 # Disk IO is for all disks.
732 disk_io_counters = psutil.disk_io_counters()
733 network_io_counters = psutil.net_io_counters()
734
735 self.disk_read_bytes_per_sec = (
736 disk_io_counters.read_bytes -
737 prev_disk_io_counters.read_bytes)/interval
738 self.disk_write_bytes_per_sec = (
739 disk_io_counters.write_bytes -
740 prev_disk_io_counters.write_bytes)/interval
741 prev_disk_io_counters = disk_io_counters
742
743 self.network_sent_bytes_per_sec = (
744 network_io_counters.bytes_sent -
745 prev_network_io_counters.bytes_sent)/interval
746 self.network_recv_bytes_per_sec = (
747 network_io_counters.bytes_recv -
748 prev_network_io_counters.bytes_recv)/interval
749 prev_network_io_counters = network_io_counters
750
751 @require_psutil()
752 def _start_io_stat_thread(self):
Gabe Black3b567202015-09-23 14:07:59 -0700753 """Start the thread to collect IO stats."""
Dan Shiafd0e492015-05-27 14:23:51 -0700754 thread = threading.Thread(target=self._refresh_io_stats)
755 thread.daemon = True
756 thread.start()
757
joychen3cb228e2013-06-12 12:13:13 -0700758 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700759 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800760 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 12:13:13 -0700761 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 14:24:45 -0500762
Dan Shiafd0e492015-05-27 14:23:51 -0700763 # Cache of disk IO stats, a thread refresh the stats every 10 seconds.
764 # lock is not used for these variables as the only thread writes to these
765 # variables is _refresh_io_stats.
766 self.disk_read_bytes_per_sec = 0
767 self.disk_write_bytes_per_sec = 0
768 # Cache of network IO stats.
769 self.network_sent_bytes_per_sec = 0
770 self.network_recv_bytes_per_sec = 0
771 self._start_io_stat_thread()
772
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700773 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500774 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700775 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700776 import builder
777 if self._builder is None:
778 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500779 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700780
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700781 @cherrypy.expose
Dan Shif8eb0d12013-08-01 17:52:06 -0700782 def is_staged(self, **kwargs):
783 """Check if artifacts have been downloaded.
784
Chris Sosa6b0c6172013-08-05 17:01:33 -0700785 async: True to return without waiting for download to complete.
786 artifacts: Comma separated list of named artifacts to download.
787 These are defined in artifact_info and have their implementation
788 in build_artifact.py.
789 files: Comma separated list of file artifacts to stage. These
790 will be available as is in the corresponding static directory with no
791 custom post-processing.
792
793 returns: True of all artifacts are staged.
Dan Shif8eb0d12013-08-01 17:52:06 -0700794
795 Example:
796 To check if autotest and test_suites are staged:
797 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
798 artifacts=autotest,test_suites
799 """
Gabe Black3b567202015-09-23 14:07:59 -0700800 dl, factory = _get_downloader_and_factory(kwargs)
Aviv Keshet57d18172016-06-18 20:39:09 -0700801 response = str(dl.IsStaged(factory))
802 _Log('Responding to is_staged %s request with %r', kwargs, response)
803 return response
Dan Shi59ae7092013-06-04 14:37:27 -0700804
Chris Sosa76e44b92013-01-31 12:11:38 -0800805 @cherrypy.expose
Prashanth Ba06d2d22014-03-07 15:35:19 -0800806 def list_image_dir(self, **kwargs):
807 """Take an archive url and list the contents in its staged directory.
808
809 Args:
810 kwargs:
811 archive_url: Google Storage URL for the build.
812
813 Example:
814 To list the contents of where this devserver should have staged
815 gs://image-archive/<board>-release/<build> call:
816 http://devserver_url:<port>/list_image_dir?archive_url=<gs://..>
817
818 Returns:
819 A string with information about the contents of the image directory.
820 """
Gabe Black3b567202015-09-23 14:07:59 -0700821 dl = _get_downloader(kwargs)
Prashanth Ba06d2d22014-03-07 15:35:19 -0800822 try:
Gabe Black3b567202015-09-23 14:07:59 -0700823 image_dir_contents = dl.ListBuildDir()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800824 except build_artifact.ArtifactDownloadError as e:
825 return 'Cannot list the contents of staged artifacts. %s' % e
826 if not image_dir_contents:
Gabe Black3b567202015-09-23 14:07:59 -0700827 return '%s has not been staged on this devserver.' % dl.DescribeSource()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800828 return image_dir_contents
829
830 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800831 def stage(self, **kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700832 """Downloads and caches build artifacts.
Chris Sosa76e44b92013-01-31 12:11:38 -0800833
Gabe Black3b567202015-09-23 14:07:59 -0700834 Downloads and caches build artifacts, possibly from a Google Storage URL,
Dan Shi72b16132015-10-08 12:10:33 -0700835 or from Android's build server. Returns once these have been downloaded
Gabe Black3b567202015-09-23 14:07:59 -0700836 on the devserver. A call to this will attempt to cache non-specified
837 artifacts in the background for the given from the given URL following
838 the principle of spatial locality. Spatial locality of different
Chris Sosa76e44b92013-01-31 12:11:38 -0800839 artifacts is explicitly defined in the build_artifact module.
840
841 These artifacts will then be available from the static/ sub-directory of
842 the devserver.
843
844 Args:
845 archive_url: Google Storage URL for the build.
Simran Basi4243a862014-12-12 12:48:33 -0800846 local_path: Local path for the build.
Prathmesh Prabhu58d08932018-01-19 15:08:19 -0800847 delete_source: Only meaningful with local_path. bool to indicate if the
848 source files should be deleted. This is especially useful when staging
849 a file locally in resource constrained environments as it allows us to
850 move the relevant files locally instead of copying them.
Dan Shif8eb0d12013-08-01 17:52:06 -0700851 async: True to return without waiting for download to complete.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700852 artifacts: Comma separated list of named artifacts to download.
853 These are defined in artifact_info and have their implementation
854 in build_artifact.py.
855 files: Comma separated list of files to stage. These
856 will be available as is in the corresponding static directory with no
857 custom post-processing.
Laurence Goodbyf5c958d2016-01-14 18:23:56 -0800858 clean: True to remove any previously staged artifacts first.
Chris Sosa76e44b92013-01-31 12:11:38 -0800859
860 Example:
861 To download the autotest and test suites tarballs:
862 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
863 artifacts=autotest,test_suites
864 To download the full update payload:
865 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
866 artifacts=full_payload
Chris Sosa6b0c6172013-08-05 17:01:33 -0700867 To download just a file called blah.bin:
868 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
869 files=blah.bin
Chris Sosa76e44b92013-01-31 12:11:38 -0800870
871 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 16:39:34 -0700872 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 12:11:38 -0800873
874 Note for this example, relative path is the archive_url stripped of its
875 basename i.e. path/ in the examples above. Specific example:
876
877 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
878
879 Will get staged to:
880
joychened64b222013-06-21 16:39:34 -0700881 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Chris Sosa76e44b92013-01-31 12:11:38 -0800882 """
Gabe Black3b567202015-09-23 14:07:59 -0700883 dl, factory = _get_downloader_and_factory(kwargs)
884
Dan Shi59ae7092013-06-04 14:37:27 -0700885 with DevServerRoot._staging_thread_count_lock:
886 DevServerRoot._staging_thread_count += 1
887 try:
Laurence Goodbyf5c958d2016-01-14 18:23:56 -0800888 boolean_string = kwargs.get('clean')
889 clean = xbuddy.XBuddy.ParseBoolean(boolean_string)
890 if clean and os.path.exists(dl.GetBuildDir()):
891 _Log('Removing %s' % dl.GetBuildDir())
892 shutil.rmtree(dl.GetBuildDir())
Gabe Black3b567202015-09-23 14:07:59 -0700893 async = kwargs.get('async', False)
894 dl.Download(factory, async=async)
Dan Shi59ae7092013-06-04 14:37:27 -0700895 finally:
896 with DevServerRoot._staging_thread_count_lock:
897 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800898 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700899
900 @cherrypy.expose
xixuan52c2fba2016-05-20 17:02:48 -0700901 def cros_au(self, **kwargs):
902 """Auto-update a CrOS DUT.
903
904 Args:
905 kwargs:
906 host_name: the hostname of the DUT to auto-update.
907 build_name: the build name for update the DUT.
908 force_update: Force an update even if the version installed is the
909 same. Default: False.
910 full_update: If True, do not run stateful update, directly force a full
911 reimage. If False, try stateful update first if the dut is already
912 installed with the same version.
913 async: Whether the auto_update function is ran in the background.
David Rileyee75de22017-11-02 10:48:15 -0700914 quick_provision: Whether the quick provision path is attempted first.
xixuan52c2fba2016-05-20 17:02:48 -0700915
916 Returns:
917 A tuple includes two elements:
918 a boolean variable represents whether the auto-update process is
919 successfully started.
920 an integer represents the background auto-update process id.
921 """
922 _check_base_args_for_auto_update(kwargs)
923
924 host_name = kwargs['host_name']
925 build_name = kwargs['build_name']
926 force_update = _parse_boolean_arg(kwargs, 'force_update')
927 full_update = _parse_boolean_arg(kwargs, 'full_update')
928 async = _parse_boolean_arg(kwargs, 'async')
xixuanac89ce82016-11-30 16:48:20 -0800929 original_build = _parse_string_arg(kwargs, 'original_build')
David Haddock90e49442017-04-07 19:14:09 -0700930 payload_filename = _parse_string_arg(kwargs, 'payload_filename')
David Haddock20559612017-06-28 22:15:08 -0700931 clobber_stateful = _parse_boolean_arg(kwargs, 'clobber_stateful')
David Rileyee75de22017-11-02 10:48:15 -0700932 quick_provision = _parse_boolean_arg(kwargs, 'quick_provision')
933
934 devserver_url = updater.GetDevserverUrl()
935 static_url = updater.GetStaticUrl()
xixuan52c2fba2016-05-20 17:02:48 -0700936
937 if async:
938 path = os.path.dirname(os.path.abspath(__file__))
939 execute_file = os.path.join(path, 'cros_update.py')
940 args = (AUTO_UPDATE_CMD % (execute_file, host_name, build_name,
941 updater.static_dir))
xixuanac89ce82016-11-30 16:48:20 -0800942
943 # The original_build's format is like: link/3428.210.0
944 # The corresponding release_archive_url's format is like:
945 # gs://chromeos-releases/stable-channel/link/3428.210.0
946 if original_build:
947 release_archive_url = _build_uri_from_build_name(original_build)
948 # First staging the stateful.tgz synchronousely.
949 self.stage(files='stateful.tgz', async=False,
950 archive_url=release_archive_url)
951 args = ('%s --original_build %s' % (args, original_build))
952
xixuan52c2fba2016-05-20 17:02:48 -0700953 if force_update:
954 args = ('%s --force_update' % args)
955
956 if full_update:
957 args = ('%s --full_update' % args)
958
David Haddock90e49442017-04-07 19:14:09 -0700959 if payload_filename:
960 args = ('%s --payload_filename %s' % (args, payload_filename))
961
David Haddock20559612017-06-28 22:15:08 -0700962 if clobber_stateful:
963 args = ('%s --clobber_stateful' % args)
964
David Rileyee75de22017-11-02 10:48:15 -0700965 if quick_provision:
966 args = ('%s --quick_provision' % args)
967
968 if devserver_url:
969 args = ('%s --devserver_url %s' % (args, devserver_url))
970
971 if static_url:
972 args = ('%s --static_url %s' % (args, static_url))
973
xixuan2a0970a2016-08-10 12:12:44 -0700974 p = subprocess.Popen([args], shell=True, preexec_fn=os.setsid)
975 pid = os.getpgid(p.pid)
xixuan52c2fba2016-05-20 17:02:48 -0700976
977 # Pre-write status in the track_status_file before the first call of
978 # 'get_au_status' to make sure that the track_status_file exists.
xixuan2a0970a2016-08-10 12:12:44 -0700979 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -0700980 progress_tracker.WriteStatus('CrOS update is just started.')
981
xixuan2a0970a2016-08-10 12:12:44 -0700982 return json.dumps((True, pid))
xixuan52c2fba2016-05-20 17:02:48 -0700983 else:
984 cros_update_trigger = cros_update.CrOSUpdateTrigger(
xixuanac89ce82016-11-30 16:48:20 -0800985 host_name, build_name, updater.static_dir, force_update=force_update,
David Rileyee75de22017-11-02 10:48:15 -0700986 full_update=full_update, original_build=original_build,
987 quick_provision=quick_provision, devserver_url=devserver_url,
988 static_url=static_url)
xixuan52c2fba2016-05-20 17:02:48 -0700989 cros_update_trigger.TriggerAU()
xixuan27d50442017-08-09 10:38:25 -0700990 return json.dumps((True, -1))
xixuan52c2fba2016-05-20 17:02:48 -0700991
992 @cherrypy.expose
993 def get_au_status(self, **kwargs):
994 """Check if the auto-update task is finished.
995
996 It handles 4 cases:
997 1. If an error exists in the track_status_file, delete the track file and
998 raise it.
999 2. If cros-update process is finished, delete the file and return the
1000 success result.
1001 3. If the process is not running, delete the track file and raise an error
1002 about 'the process is terminated due to unknown reason'.
1003 4. If the track_status_file does not exist, kill the process if it exists,
1004 and raise the IOError.
1005
1006 Args:
1007 kwargs:
1008 host_name: the hostname of the DUT to auto-update.
1009 pid: the background process id of cros-update.
1010
1011 Returns:
xixuan28d99072016-10-06 12:24:16 -07001012 A dict with three elements:
xixuan52c2fba2016-05-20 17:02:48 -07001013 a boolean variable represents whether the auto-update process is
1014 finished.
1015 a string represents the current auto-update process status.
1016 For example, 'Transfer Devserver/Stateful Update Package'.
xixuan28d99072016-10-06 12:24:16 -07001017 a detailed error message paragraph if there exists an Auto-Update
1018 error, in which the last line shows the main exception. Empty
1019 string otherwise.
xixuan52c2fba2016-05-20 17:02:48 -07001020 """
1021 if 'host_name' not in kwargs:
1022 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
1023
1024 if 'pid' not in kwargs:
1025 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
1026
1027 host_name = kwargs['host_name']
1028 pid = kwargs['pid']
1029 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
1030
xixuan28d99072016-10-06 12:24:16 -07001031 result_dict = {'finished': False, 'status': '', 'detailed_error_msg': ''}
xixuan52c2fba2016-05-20 17:02:48 -07001032 try:
1033 result = progress_tracker.ReadStatus()
1034 if result.startswith(cros_update_progress.ERROR_TAG):
xixuan28d99072016-10-06 12:24:16 -07001035 result_dict['detailed_error_msg'] = result[len(
1036 cros_update_progress.ERROR_TAG):]
xixuan28681fd2016-11-23 11:13:56 -08001037 elif result == cros_update_progress.FINISHED:
xixuan28d99072016-10-06 12:24:16 -07001038 result_dict['finished'] = True
1039 result_dict['status'] = result
xixuan28681fd2016-11-23 11:13:56 -08001040 elif not cros_update_progress.IsProcessAlive(pid):
xixuan28d99072016-10-06 12:24:16 -07001041 result_dict['detailed_error_msg'] = (
1042 'Cros_update process terminated midway due to unknown reason. '
1043 'Last update status was %s' % result)
xixuan28681fd2016-11-23 11:13:56 -08001044 else:
1045 result_dict['status'] = result
1046 except IOError as e:
1047 if pid and cros_update_progress.IsProcessAlive(pid):
xixuan2a0970a2016-08-10 12:12:44 -07001048 os.killpg(int(pid), signal.SIGKILL)
xixuan52c2fba2016-05-20 17:02:48 -07001049
xixuan28681fd2016-11-23 11:13:56 -08001050 result_dict['detailed_error_msg'] = str(e)
1051
1052 return json.dumps(result_dict)
xixuan52c2fba2016-05-20 17:02:48 -07001053
1054 @cherrypy.expose
David Riley6d5fca02017-10-31 10:35:47 -07001055 def post_au_status(self, status, **kwargs):
1056 """Updates the status of an auto-update task.
1057
1058 Callers will need to POST to this URL with a body of MIME-type
1059 "multipart/form-data".
1060 The body should include a single argument, 'status', containing the
1061 AU status to record.
1062
1063 Args:
1064 status: The updated status.
1065 kwargs:
1066 host_name: the hostname of the DUT to auto-update.
1067 pid: the background process id of cros-update.
1068 """
1069 if 'host_name' not in kwargs:
1070 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
1071
1072 if 'pid' not in kwargs:
1073 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
1074
1075 host_name = kwargs['host_name']
1076 pid = kwargs['pid']
David Riley3cea2582017-11-24 22:03:01 -08001077 status = status.rstrip()
1078 _Log('Recording status for %s (%s): %s' % (host_name, pid, status))
David Riley6d5fca02017-10-31 10:35:47 -07001079 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
1080
David Riley3cea2582017-11-24 22:03:01 -08001081 progress_tracker.WriteStatus(status)
David Riley6d5fca02017-10-31 10:35:47 -07001082
1083 return 'True'
1084
1085 @cherrypy.expose
xixuan52c2fba2016-05-20 17:02:48 -07001086 def handler_cleanup(self, **kwargs):
xixuan3bc974e2016-10-18 17:21:43 -07001087 """Clean track status log and temp directory for CrOS auto-update process.
xixuan52c2fba2016-05-20 17:02:48 -07001088
1089 Args:
1090 kwargs:
1091 host_name: the hostname of the DUT to auto-update.
1092 pid: the background process id of cros-update.
1093 """
1094 if 'host_name' not in kwargs:
1095 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
1096
1097 if 'pid' not in kwargs:
1098 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
1099
1100 host_name = kwargs['host_name']
1101 pid = kwargs['pid']
1102 cros_update_progress.DelTrackStatusFile(host_name, pid)
xixuan3bc974e2016-10-18 17:21:43 -07001103 cros_update_progress.DelAUTempDirectory(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -07001104
1105 @cherrypy.expose
1106 def kill_au_proc(self, **kwargs):
1107 """Kill CrOS auto-update process using given process id.
1108
1109 Args:
1110 kwargs:
1111 host_name: Kill all the CrOS auto-update process of this host.
1112
1113 Returns:
1114 True if all processes are killed properly.
1115 """
1116 if 'host_name' not in kwargs:
1117 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
1118
xixuan447ad9d2017-02-28 14:46:20 -08001119 cur_pid = kwargs.get('pid')
1120
xixuan52c2fba2016-05-20 17:02:48 -07001121 host_name = kwargs['host_name']
xixuan3bc974e2016-10-18 17:21:43 -07001122 track_log_list = cros_update_progress.GetAllTrackStatusFileByHostName(
1123 host_name)
xixuan52c2fba2016-05-20 17:02:48 -07001124 for log in track_log_list:
1125 # The track log's full path is: path/host_name_pid.log
1126 # Use splitext to remove file extension, then parse pid from the
1127 # filename.
1128 pid = os.path.splitext(os.path.basename(log))[0][len(host_name)+1:]
xixuan447ad9d2017-02-28 14:46:20 -08001129 _clear_process(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -07001130
xixuan447ad9d2017-02-28 14:46:20 -08001131 if cur_pid:
1132 _clear_process(host_name, cur_pid)
xixuan52c2fba2016-05-20 17:02:48 -07001133
1134 return 'True'
1135
1136 @cherrypy.expose
1137 def collect_cros_au_log(self, **kwargs):
1138 """Collect CrOS auto-update log.
1139
1140 Args:
1141 kwargs:
1142 host_name: the hostname of the DUT to auto-update.
1143 pid: the background process id of cros-update.
1144
1145 Returns:
David Haddock9f459632017-05-11 14:45:46 -07001146 A dictionary containing the execute log file and any hostlog files.
xixuan52c2fba2016-05-20 17:02:48 -07001147 """
1148 if 'host_name' not in kwargs:
1149 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
1150
1151 if 'pid' not in kwargs:
1152 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
1153
1154 host_name = kwargs['host_name']
1155 pid = kwargs['pid']
xixuan3bc974e2016-10-18 17:21:43 -07001156
1157 # Fetch the execute log recorded by cros_update_progress.
xixuan1bbfaba2016-10-13 17:53:22 -07001158 au_log = cros_update_progress.ReadExecuteLogFile(host_name, pid)
1159 cros_update_progress.DelExecuteLogFile(host_name, pid)
David Haddock9f459632017-05-11 14:45:46 -07001160 # Fetch the cros_au host_logs if they exist
1161 au_hostlogs = cros_update_progress.ReadAUHostLogFiles(host_name, pid)
1162 return json.dumps({'cros_au_log': au_log, 'host_logs': au_hostlogs})
xixuan1bbfaba2016-10-13 17:53:22 -07001163
xixuan52c2fba2016-05-20 17:02:48 -07001164 @cherrypy.expose
Dan Shi2f136862016-02-11 15:38:38 -08001165 def locate_file(self, **kwargs):
1166 """Get the path to the given file name.
1167
1168 This method looks up the given file name inside specified build artifacts.
1169 One use case is to help caller to locate an apk file inside a build
1170 artifact. The location of the apk file could be different based on the
1171 branch and target.
1172
1173 Args:
1174 file_name: Name of the file to look for.
1175 artifacts: A list of artifact names to search for the file.
1176
1177 Returns:
1178 Path to the file with the given name. It's relative to the folder for the
1179 build, e.g., DATA/priv-app/sl4a/sl4a.apk
Dan Shi2f136862016-02-11 15:38:38 -08001180 """
1181 dl, _ = _get_downloader_and_factory(kwargs)
1182 try:
Joe Brennan1691f8e2017-03-15 15:53:36 -07001183 file_name = kwargs['file_name']
Dan Shi2f136862016-02-11 15:38:38 -08001184 artifacts = kwargs['artifacts']
1185 except KeyError:
1186 raise DevServerError('`file_name` and `artifacts` are required to search '
1187 'for a file in build artifacts.')
1188 build_path = dl.GetBuildDir()
1189 for artifact in artifacts:
1190 # Get the unzipped folder of the artifact. If it's not defined in
1191 # ARTIFACT_UNZIP_FOLDER_MAP, assume the files are unzipped to the build
1192 # directory directly.
1193 folder = artifact_info.ARTIFACT_UNZIP_FOLDER_MAP.get(artifact, '')
1194 artifact_path = os.path.join(build_path, folder)
1195 for root, _, filenames in os.walk(artifact_path):
Joe Brennan1691f8e2017-03-15 15:53:36 -07001196 if file_name in set([f for f in filenames]):
Dan Shi2f136862016-02-11 15:38:38 -08001197 return os.path.relpath(os.path.join(root, file_name), build_path)
1198 raise DevServerError('File `%s` can not be found in artifacts: %s' %
1199 (file_name, artifacts))
1200
1201 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -08001202 def setup_telemetry(self, **kwargs):
1203 """Extracts and sets up telemetry
1204
1205 This method goes through the telemetry deps packages, and stages them on
1206 the devserver to be used by the drones and the telemetry tests.
1207
1208 Args:
1209 archive_url: Google Storage URL for the build.
1210
1211 Returns:
1212 Path to the source folder for the telemetry codebase once it is staged.
1213 """
Gabe Black3b567202015-09-23 14:07:59 -07001214 dl = _get_downloader(kwargs)
Simran Basi4baad082013-02-14 13:39:18 -08001215
Gabe Black3b567202015-09-23 14:07:59 -07001216 build_path = dl.GetBuildDir()
Simran Basi4baad082013-02-14 13:39:18 -08001217 deps_path = os.path.join(build_path, 'autotest/packages')
1218 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
1219 src_folder = os.path.join(telemetry_path, 'src')
1220
1221 with self._telemetry_lock_dict.lock(telemetry_path):
1222 if os.path.exists(src_folder):
1223 # Telemetry is already fully stage return
1224 return src_folder
1225
1226 common_util.MkDirP(telemetry_path)
1227
1228 # Copy over the required deps tar balls to the telemetry directory.
1229 for dep in TELEMETRY_DEPS:
1230 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -07001231 if not os.path.exists(dep_path):
1232 # This dep does not exist (could be new), do not extract it.
1233 continue
Simran Basi4baad082013-02-14 13:39:18 -08001234 try:
1235 common_util.ExtractTarball(dep_path, telemetry_path)
1236 except common_util.CommonUtilError as e:
1237 shutil.rmtree(telemetry_path)
1238 raise DevServerError(str(e))
1239
1240 # By default all the tarballs extract to test_src but some parts of
1241 # the telemetry code specifically hardcoded to exist inside of 'src'.
1242 test_src = os.path.join(telemetry_path, 'test_src')
1243 try:
1244 shutil.move(test_src, src_folder)
1245 except shutil.Error:
1246 # This can occur if src_folder already exists. Remove and retry move.
1247 shutil.rmtree(src_folder)
Gabe Black3b567202015-09-23 14:07:59 -07001248 raise DevServerError(
1249 'Failure in telemetry setup for build %s. Appears that the '
1250 'test_src to src move failed.' % dl.GetBuild())
Simran Basi4baad082013-02-14 13:39:18 -08001251
1252 return src_folder
1253
1254 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -08001255 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -07001256 """Symbolicates a minidump using pre-downloaded symbols, returns it.
1257
1258 Callers will need to POST to this URL with a body of MIME-type
1259 "multipart/form-data".
1260 The body should include a single argument, 'minidump', containing the
1261 binary-formatted minidump to symbolicate.
1262
Chris Masone816e38c2012-05-02 12:22:36 -07001263 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -08001264 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -07001265 minidump: The binary minidump file to symbolicate.
1266 """
Chris Sosa76e44b92013-01-31 12:11:38 -08001267 # Ensure the symbols have been staged.
Dan Shif08fe492016-10-04 14:39:25 -07001268 # Try debug.tar.xz first, then debug.tgz
1269 for artifact in (artifact_info.SYMBOLS_ONLY, artifact_info.SYMBOLS):
1270 kwargs['artifacts'] = artifact
1271 dl = _get_downloader(kwargs)
1272
1273 try:
1274 if self.stage(**kwargs) == 'Success':
1275 break
1276 except build_artifact.ArtifactDownloadError:
1277 continue
1278 else:
Gabe Black3b567202015-09-23 14:07:59 -07001279 raise DevServerError('Failed to stage symbols for %s' %
1280 dl.DescribeSource())
Chris Sosa76e44b92013-01-31 12:11:38 -08001281
Chris Masone816e38c2012-05-02 12:22:36 -07001282 to_return = ''
1283 with tempfile.NamedTemporaryFile() as local:
1284 while True:
1285 data = minidump.file.read(8192)
1286 if not data:
1287 break
1288 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -08001289
Chris Masone816e38c2012-05-02 12:22:36 -07001290 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -08001291
Gabe Black3b567202015-09-23 14:07:59 -07001292 symbols_directory = os.path.join(dl.GetBuildDir(), 'debug', 'breakpad')
Chris Sosa76e44b92013-01-31 12:11:38 -08001293
xixuanab744382017-04-27 10:41:27 -07001294 # The location of minidump_stackwalk is defined in chromeos-admin.
Chris Sosa76e44b92013-01-31 12:11:38 -08001295 stackwalk = subprocess.Popen(
xixuanab744382017-04-27 10:41:27 -07001296 ['/usr/local/bin/minidump_stackwalk', local.name, symbols_directory],
Chris Sosa76e44b92013-01-31 12:11:38 -08001297 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1298
Chris Masone816e38c2012-05-02 12:22:36 -07001299 to_return, error_text = stackwalk.communicate()
1300 if stackwalk.returncode != 0:
1301 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
1302 error_text, stackwalk.returncode))
1303
1304 return to_return
1305
1306 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -08001307 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 15:31:36 -04001308 """Return a string representing the latest build for a given target.
1309
1310 Args:
1311 target: The build target, typically a combination of the board and the
1312 type of build e.g. x86-mario-release.
1313 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
1314 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-07 18:21:26 -08001315
Scott Zawalski16954532012-03-20 15:31:36 -04001316 Returns:
1317 A string representation of the latest build if one exists, i.e.
1318 R19-1993.0.0-a1-b1480.
1319 An empty string if no latest could be found.
1320 """
Don Garrettf84631a2014-01-07 18:21:26 -08001321 if not kwargs:
Scott Zawalski16954532012-03-20 15:31:36 -04001322 return _PrintDocStringAsHTML(self.latestbuild)
1323
Don Garrettf84631a2014-01-07 18:21:26 -08001324 if 'target' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -07001325 raise common_util.DevServerHTTPError(500, 'Error: target= is required!')
Dan Shi61305df2015-10-26 16:52:35 -07001326
1327 if _is_android_build_request(kwargs):
1328 branch = kwargs.get('branch', None)
1329 target = kwargs.get('target', None)
1330 if not target or not branch:
1331 raise DevServerError(
xixuan52c2fba2016-05-20 17:02:48 -07001332 'Both target and branch must be specified to query for the latest '
1333 'Android build.')
Dan Shi61305df2015-10-26 16:52:35 -07001334 return android_build.BuildAccessor.GetLatestBuildID(target, branch)
1335
Scott Zawalski16954532012-03-20 15:31:36 -04001336 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001337 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-07 18:21:26 -08001338 updater.static_dir, kwargs['target'],
1339 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -07001340 except common_util.CommonUtilError as errmsg:
Chris Sosa4b951602014-04-09 20:26:07 -07001341 raise common_util.DevServerHTTPError(500, 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:
1363 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
1364
1365 if 'suite_name' not in kwargs:
Dan Shia1cd6522016-04-18 16:07:21 -07001366 raise common_util.DevServerHTTPError(500,
1367 'Error: suite_name= is required!')
xixuan7efd0002016-04-14 15:34:01 -07001368
1369 control_file_list = [
1370 line.rstrip() for line in common_util.GetControlFileListForSuite(
1371 updater.static_dir, kwargs['build'],
1372 kwargs['suite_name']).splitlines()]
1373
Dan Shia1cd6522016-04-18 16:07:21 -07001374 control_file_content_dict = {}
xixuan7efd0002016-04-14 15:34:01 -07001375 for control_path in control_file_list:
Dan Shia1cd6522016-04-18 16:07:21 -07001376 control_file_content_dict[control_path] = (common_util.GetControlFile(
xixuan7efd0002016-04-14 15:34:01 -07001377 updater.static_dir, kwargs['build'], control_path))
1378
Dan Shia1cd6522016-04-18 16:07:21 -07001379 return json.dumps(control_file_content_dict)
xixuan7efd0002016-04-14 15:34:01 -07001380
1381 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -08001382 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 17:17:28 -05001383 """Return a control file or a list of all known control files.
1384
1385 Example URL:
1386 To List all control files:
beepsbd337242013-07-09 22:44:06 -07001387 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
1388 To List all control files for, say, the bvt suite:
1389 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -05001390 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -05001391 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 -05001392
1393 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -05001394 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -05001395 control_path: If you want the contents of a control file set this
1396 to the path. E.g. client/site_tests/sleeptest/control
1397 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -07001398 suite_name: If control_path is not specified but a suite_name is
1399 specified, list the control files belonging to that suite instead of
1400 all control files. The empty string for suite_name will list all control
1401 files for the build.
Don Garrettf84631a2014-01-07 18:21:26 -08001402
Scott Zawalski4647ce62012-01-03 17:17:28 -05001403 Returns:
1404 Contents of a control file if control_path is provided.
1405 A list of control files if no control_path is provided.
1406 """
Don Garrettf84631a2014-01-07 18:21:26 -08001407 if not kwargs:
Scott Zawalski4647ce62012-01-03 17:17:28 -05001408 return _PrintDocStringAsHTML(self.controlfiles)
1409
Don Garrettf84631a2014-01-07 18:21:26 -08001410 if 'build' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -07001411 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -05001412
Don Garrettf84631a2014-01-07 18:21:26 -08001413 if 'control_path' not in kwargs:
1414 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-09 22:44:06 -07001415 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-07 18:21:26 -08001416 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-09 22:44:06 -07001417 else:
1418 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-07 18:21:26 -08001419 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -05001420 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001421 return common_util.GetControlFile(
Don Garrettf84631a2014-01-07 18:21:26 -08001422 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -08001423
1424 @cherrypy.expose
Simran Basi99e63c02014-05-20 10:39:52 -07001425 def xbuddy_translate(self, *args, **kwargs):
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001426 """Translates an xBuddy path to a real path to artifact if it exists.
1427
1428 Args:
Simran Basi99e63c02014-05-20 10:39:52 -07001429 args: An xbuddy path in the form of {local|remote}/build_id/artifact.
1430 Local searches the devserver's static directory. Remote searches a
1431 Google Storage image archive.
1432
1433 Kwargs:
1434 image_dir: Google Storage image archive to search in if requesting a
1435 remote artifact. If none uses the default bucket.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001436
1437 Returns:
Simran Basi99e63c02014-05-20 10:39:52 -07001438 String in the format of build_id/artifact as stored on the local server
1439 or in Google Storage.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001440 """
Simran Basi99e63c02014-05-20 10:39:52 -07001441 build_id, filename = self._xbuddy.Translate(
Gabe Black3b567202015-09-23 14:07:59 -07001442 args, image_dir=kwargs.get('image_dir'))
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001443 response = os.path.join(build_id, filename)
1444 _Log('Path translation requested, returning: %s', response)
1445 return response
1446
1447 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -07001448 def xbuddy(self, *args, **kwargs):
1449 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -07001450
1451 Args:
joycheneaf4cfc2013-07-02 08:38:57 -07001452 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -07001453 components of the path. The path can be understood as
1454 "{local|remote}/build_id/artifact" where build_id is composed of
1455 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -07001456
joychen121fc9b2013-08-02 14:30:30 -07001457 The first path element is optional, and can be "remote" or "local"
1458 If local (the default), devserver will not attempt to access Google
1459 Storage, and will only search the static directory for the files.
1460 If remote, devserver will try to obtain the artifact off GS if it's
1461 not found locally.
1462 The board is the familiar board name, optionally suffixed.
1463 The version can be the google storage version number, and may also be
1464 any of a number of xBuddy defined version aliases that will be
1465 translated into the latest built image that fits the description.
1466 Defaults to latest.
1467 The artifact is one of a number of image or artifact aliases used by
1468 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -07001469
1470 Kwargs:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001471 for_update: {true|false}
1472 if true, pregenerates the update payloads for the image,
1473 and returns the update uri to pass to the
1474 update_engine_client.
joychen3cb228e2013-06-12 12:13:13 -07001475 return_dir: {true|false}
1476 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001477 relative_path: {true|false}
1478 if set to true, returns the relative path to the payload
1479 directory from static_dir.
joychen3cb228e2013-06-12 12:13:13 -07001480 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -07001481 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -07001482 or
joycheneaf4cfc2013-07-02 08:38:57 -07001483 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -07001484
1485 Returns:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001486 If |for_update|, returns a redirect to the image or update file
1487 on the devserver. E.g.,
1488 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
1489 chromium-test-image.bin
1490 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
1491 http://host:port/static/x86-generic-release/R26-4000.0.0/
1492 If |relative_path| is true, return a relative path the folder where the
1493 payloads are. E.g.,
1494 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 12:13:13 -07001495 """
Chris Sosa75490802013-09-30 17:21:45 -07001496 boolean_string = kwargs.get('for_update')
1497 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001498 boolean_string = kwargs.get('return_dir')
1499 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
1500 boolean_string = kwargs.get('relative_path')
1501 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -07001502
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001503 if return_dir and relative_path:
Chris Sosa4b951602014-04-09 20:26:07 -07001504 raise common_util.DevServerHTTPError(
1505 500, 'Cannot specify both return_dir and relative_path')
Chris Sosa75490802013-09-30 17:21:45 -07001506
1507 # For updates, we optimize downloading of test images.
1508 file_name = None
1509 build_id = None
1510 if for_update:
1511 try:
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001512 build_id = self._xbuddy.StageTestArtifactsForUpdate(args)
Chris Sosa75490802013-09-30 17:21:45 -07001513 except build_artifact.ArtifactDownloadError:
1514 build_id = None
1515
1516 if not build_id:
1517 build_id, file_name = self._xbuddy.Get(args)
1518
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001519 if for_update:
1520 _Log('Payload generation triggered by request')
1521 # Forces payload to be in cache and symlinked into build_id dir.
Chris Sosa75490802013-09-30 17:21:45 -07001522 updater.GetUpdateForLabel(autoupdate.FORCED_UPDATE, build_id,
1523 image_name=file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001524
1525 response = None
1526 if return_dir:
1527 response = os.path.join(cherrypy.request.base, 'static', build_id)
1528 _Log('Directory requested, returning: %s', response)
1529 elif relative_path:
1530 response = build_id
1531 _Log('Relative path requested, returning: %s', response)
1532 elif for_update:
1533 response = os.path.join(cherrypy.request.base, 'update', build_id)
1534 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 12:13:13 -07001535 else:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001536 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 14:30:30 -07001537 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001538 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 14:30:30 -07001539 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -07001540
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001541 return response
1542
joychen3cb228e2013-06-12 12:13:13 -07001543 @cherrypy.expose
1544 def xbuddy_list(self):
1545 """Lists the currently available images & time since last access.
1546
Gilad Arnold452fd272014-02-04 11:09:28 -08001547 Returns:
1548 A string representation of a list of tuples [(build_id, time since last
1549 access),...]
joychen3cb228e2013-06-12 12:13:13 -07001550 """
1551 return self._xbuddy.List()
1552
1553 @cherrypy.expose
1554 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 11:09:28 -08001555 """Returns the number of images cached by xBuddy."""
joychen3cb228e2013-06-12 12:13:13 -07001556 return self._xbuddy.Capacity()
1557
1558 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001559 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001560 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001561 return ('Welcome to the Dev Server!<br>\n'
1562 '<br>\n'
1563 'Here are the available methods, click for documentation:<br>\n'
1564 '<br>\n'
1565 '%s' %
1566 '<br>\n'.join(
1567 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001568 for name in _FindExposedMethods(
1569 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001570
1571 @cherrypy.expose
1572 def doc(self, *args):
1573 """Shows the documentation for available methods / URLs.
1574
1575 Example:
1576 http://myhost/doc/update
1577 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001578 name = '/'.join(args)
1579 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001580 if not method:
1581 raise DevServerError("No exposed method named `%s'" % name)
1582 if not method.__doc__:
1583 raise DevServerError("No documentation for exposed method `%s'" % name)
1584 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -07001585
Dale Curtisc9aaf3a2011-08-09 15:47:40 -07001586 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001587 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001588 """Handles an update check from a Chrome OS client.
1589
1590 The HTTP request should contain the standard Omaha-style XML blob. The URL
1591 line may contain an additional intermediate path to the update payload.
1592
joychen121fc9b2013-08-02 14:30:30 -07001593 This request can be handled in one of 4 ways, depending on the devsever
1594 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -07001595
joychen121fc9b2013-08-02 14:30:30 -07001596 1. No intermediate path
1597 If no intermediate path is given, the default behavior is to generate an
1598 update payload from the latest test image locally built for the board
1599 specified in the xml. Devserver serves the generated payload.
1600
1601 2. Path explicitly invokes XBuddy
1602 If there is a path given, it can explicitly invoke xbuddy by prefixing it
1603 with 'xbuddy'. This path is then used to acquire an image binary for the
1604 devserver to generate an update payload from. Devserver then serves this
1605 payload.
1606
1607 3. Path is left for the devserver to interpret.
1608 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
1609 to generate a payload from the test image in that directory and serve it.
1610
1611 4. The devserver is in a 'forced' mode. TO BE DEPRECATED
1612 This comes from the usage of --forced_payload or --image when starting the
1613 devserver. No matter what path (or no path) gets passed in, devserver will
1614 serve the update payload (--forced_payload) or generate an update payload
1615 from the image (--image).
1616
1617 Examples:
1618 1. No intermediate path
1619 update_engine_client --omaha_url=http://myhost/update
1620 This generates an update payload from the latest test image locally built
1621 for the board specified in the xml.
1622
1623 2. Explicitly invoke xbuddy
1624 update_engine_client --omaha_url=
1625 http://myhost/update/xbuddy/remote/board/version/dev
1626 This would go to GS to download the dev image for the board, from which
1627 the devserver would generate a payload to serve.
1628
1629 3. Give a path for devserver to interpret
1630 update_engine_client --omaha_url=http://myhost/update/some/random/path
1631 This would attempt, in order to:
1632 a) Generate an update from a test image binary if found in
1633 static_dir/some/random/path.
1634 b) Serve an update payload found in static_dir/some/random/path.
1635 c) Hope that some/random/path takes the form "board/version" and
1636 and attempt to download an update payload for that board/version
1637 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001638 """
joychen121fc9b2013-08-02 14:30:30 -07001639 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -08001640 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -07001641 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-11 19:49:01 -07001642
joychen121fc9b2013-08-02 14:30:30 -07001643 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001644
Dan Shiafd0e492015-05-27 14:23:51 -07001645 @require_psutil()
1646 def _get_io_stats(self):
1647 """Get the IO stats as a dictionary.
1648
Gabe Black3b567202015-09-23 14:07:59 -07001649 Returns:
1650 A dictionary of IO stats collected by psutil.
Dan Shiafd0e492015-05-27 14:23:51 -07001651 """
1652 return {'disk_read_bytes_per_second': self.disk_read_bytes_per_sec,
1653 'disk_write_bytes_per_second': self.disk_write_bytes_per_sec,
1654 'disk_total_bytes_per_second': (self.disk_read_bytes_per_sec +
1655 self.disk_write_bytes_per_sec),
1656 'network_sent_bytes_per_second': self.network_sent_bytes_per_sec,
1657 'network_recv_bytes_per_second': self.network_recv_bytes_per_sec,
1658 'network_total_bytes_per_second': (self.network_sent_bytes_per_sec +
1659 self.network_recv_bytes_per_sec),
1660 'cpu_percent': psutil.cpu_percent(),}
1661
Dan Shi7247f9c2016-06-01 09:19:09 -07001662
1663 def _get_process_count(self, process_cmd_pattern):
1664 """Get the count of processes that match the given command pattern.
1665
1666 Args:
1667 process_cmd_pattern: The regex pattern of process command to match.
1668
1669 Returns:
1670 The count of processes that match the given command pattern.
1671 """
1672 try:
xixuanac89ce82016-11-30 16:48:20 -08001673 # Use Popen instead of check_output since the latter cannot run with old
1674 # python version (less than 2.7)
1675 proc = subprocess.Popen(
1676 'pgrep -fc "%s"' % process_cmd_pattern,
1677 stdout=subprocess.PIPE,
1678 stderr=subprocess.PIPE,
1679 shell=True)
1680 cmd_output, cmd_error = proc.communicate()
1681 if cmd_error:
1682 _Log('Error happened when getting process count: %s' % cmd_error)
1683
1684 return int(cmd_output)
Dan Shi7247f9c2016-06-01 09:19:09 -07001685 except subprocess.CalledProcessError:
1686 return 0
1687
1688
Dan Shif5ce2de2013-04-25 16:06:32 -07001689 @cherrypy.expose
1690 def check_health(self):
1691 """Collect the health status of devserver to see if it's ready for staging.
1692
Gilad Arnold452fd272014-02-04 11:09:28 -08001693 Returns:
1694 A JSON dictionary containing all or some of the following fields:
1695 free_disk (int): free disk space in GB
1696 staging_thread_count (int): number of devserver threads currently staging
1697 an image
Dan Shi7247f9c2016-06-01 09:19:09 -07001698 apache_client_count (int): count of Apache processes.
1699 telemetry_test_count (int): count of telemetry tests.
1700 gsutil_count (int): count of gsutil processes.
Dan Shif5ce2de2013-04-25 16:06:32 -07001701 """
1702 # Get free disk space.
1703 stat = os.statvfs(updater.static_dir)
1704 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
Dan Shi7247f9c2016-06-01 09:19:09 -07001705 apache_client_count = self._get_process_count('apache')
1706 telemetry_test_count = self._get_process_count('python.*telemetry')
1707 gsutil_count = self._get_process_count('gsutil')
xixuan447ad9d2017-02-28 14:46:20 -08001708 au_process_count = len(cros_update_progress.GetAllRunningAUProcess())
Dan Shif5ce2de2013-04-25 16:06:32 -07001709
Dan Shiafd0e492015-05-27 14:23:51 -07001710 health_data = {
Dan Shif5ce2de2013-04-25 16:06:32 -07001711 'free_disk': free_disk,
Dan Shid76e6bb2016-01-28 22:28:51 -08001712 'staging_thread_count': DevServerRoot._staging_thread_count,
1713 'apache_client_count': apache_client_count,
Dan Shi7247f9c2016-06-01 09:19:09 -07001714 'telemetry_test_count': telemetry_test_count,
xixuan447ad9d2017-02-28 14:46:20 -08001715 'gsutil_count': gsutil_count,
1716 'au_process_count': au_process_count,
1717 }
Dan Shiafd0e492015-05-27 14:23:51 -07001718 health_data.update(self._get_io_stats() or {})
1719
1720 return json.dumps(health_data)
Dan Shif5ce2de2013-04-25 16:06:32 -07001721
1722
Chris Sosadbc20082012-12-10 13:39:11 -08001723def _CleanCache(cache_dir, wipe):
1724 """Wipes any excess cached items in the cache_dir.
1725
1726 Args:
1727 cache_dir: the directory we are wiping from.
1728 wipe: If True, wipe all the contents -- not just the excess.
1729 """
1730 if wipe:
1731 # Clear the cache and exit on error.
1732 cmd = 'rm -rf %s/*' % cache_dir
1733 if os.system(cmd) != 0:
1734 _Log('Failed to clear the cache with %s' % cmd)
1735 sys.exit(1)
1736 else:
1737 # Clear all but the last N cached updates
1738 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
1739 (cache_dir, CACHED_ENTRIES))
1740 if os.system(cmd) != 0:
1741 _Log('Failed to clean up old delta cache files with %s' % cmd)
1742 sys.exit(1)
1743
1744
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001745def _AddTestingOptions(parser):
1746 group = optparse.OptionGroup(
1747 parser, 'Advanced Testing Options', 'These are used by test scripts and '
1748 'developers writing integration tests utilizing the devserver. They are '
1749 'not intended to be really used outside the scope of someone '
1750 'knowledgable about the test.')
1751 group.add_option('--exit',
1752 action='store_true',
1753 help='do not start the server (yet pregenerate/clear cache)')
1754 group.add_option('--host_log',
1755 action='store_true', default=False,
1756 help='record history of host update events (/api/hostlog)')
1757 group.add_option('--max_updates',
Gabe Black3b567202015-09-23 14:07:59 -07001758 metavar='NUM', default=-1, type='int',
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001759 help='maximum number of update checks handled positively '
1760 '(default: unlimited)')
1761 group.add_option('--private_key',
1762 metavar='PATH', default=None,
1763 help='path to the private key in pem format. If this is set '
1764 'the devserver will generate update payloads that are '
1765 'signed with this key.')
David Zeuthen52ccd012013-10-31 12:58:26 -07001766 group.add_option('--private_key_for_metadata_hash_signature',
1767 metavar='PATH', default=None,
1768 help='path to the private key in pem format. If this is set '
1769 'the devserver will sign the metadata hash with the given '
1770 'key and transmit in the Omaha-style XML response.')
1771 group.add_option('--public_key',
1772 metavar='PATH', default=None,
1773 help='path to the public key in pem format. If this is set '
1774 'the devserver will transmit a base64 encoded version of '
1775 'the content in the Omaha-style XML response.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001776 group.add_option('--proxy_port',
1777 metavar='PORT', default=None, type='int',
1778 help='port to have the client connect to -- basically the '
1779 'devserver lies to the update to tell it to get the payload '
1780 'from a different port that will proxy the request back to '
1781 'the devserver. The proxy must be managed outside the '
1782 'devserver.')
1783 group.add_option('--remote_payload',
1784 action='store_true', default=False,
Chris Sosa4b951602014-04-09 20:26:07 -07001785 help='Payload is being served from a remote machine. With '
1786 'this setting enabled, this devserver instance serves as '
1787 'just an Omaha server instance. In this mode, the '
1788 'devserver enforces a few extra components of the Omaha '
Chris Sosafc715442014-04-09 20:45:23 -07001789 'protocol, such as hardware class, being sent.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001790 group.add_option('-u', '--urlbase',
1791 metavar='URL',
Gabe Black3b567202015-09-23 14:07:59 -07001792 help='base URL for update images, other than the '
1793 'devserver. Use in conjunction with remote_payload.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001794 parser.add_option_group(group)
1795
1796
1797def _AddUpdateOptions(parser):
1798 group = optparse.OptionGroup(
1799 parser, 'Autoupdate Options', 'These options can be used to change '
1800 'how the devserver either generates or serve update payloads. Please '
1801 'note that all of these option affect how a payload is generated and so '
1802 'do not work in archive-only mode.')
1803 group.add_option('--board',
1804 help='By default the devserver will create an update '
1805 'payload from the latest image built for the board '
1806 'a device that is requesting an update has. When we '
1807 'pre-generate an update (see below) and we do not specify '
1808 'another update_type option like image or payload, the '
1809 'devserver needs to know the board to generate the latest '
1810 'image for. This is that board.')
1811 group.add_option('--critical_update',
1812 action='store_true', default=False,
1813 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001814 group.add_option('--image',
1815 metavar='FILE',
1816 help='Generate and serve an update using this image to any '
1817 'device that requests an update.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001818 group.add_option('--payload',
1819 metavar='PATH',
1820 help='use the update payload from specified directory '
1821 '(update.gz).')
1822 group.add_option('-p', '--pregenerate_update',
1823 action='store_true', default=False,
1824 help='pre-generate the update payload before accepting '
1825 'update requests. Useful to help debug payload generation '
1826 'issues quickly. Also if an update payload will take a '
1827 'long time to generate, a client may timeout if you do not'
1828 'pregenerate the update.')
1829 group.add_option('--src_image',
1830 metavar='PATH', default='',
1831 help='If specified, delta updates will be generated using '
1832 'this image as the source image. Delta updates are when '
1833 'you are updating from a "source image" to a another '
1834 'image.')
1835 parser.add_option_group(group)
1836
1837
1838def _AddProductionOptions(parser):
1839 group = optparse.OptionGroup(
1840 parser, 'Advanced Server Options', 'These options can be used to changed '
1841 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001842 group.add_option('--clear_cache',
1843 action='store_true', default=False,
1844 help='At startup, removes all cached entries from the'
1845 'devserver\'s cache.')
1846 group.add_option('--logfile',
1847 metavar='PATH',
1848 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -07001849 group.add_option('--pidfile',
1850 metavar='PATH',
1851 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 11:04:13 -08001852 group.add_option('--portfile',
1853 metavar='PATH',
1854 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001855 group.add_option('--production',
1856 action='store_true', default=False,
1857 help='have the devserver use production values when '
1858 'starting up. This includes using more threads and '
1859 'performing less logging.')
1860 parser.add_option_group(group)
1861
1862
Paul Hobbsef4e0702016-06-27 17:01:42 -07001863def MakeLogHandler(logfile):
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001864 """Create a LogHandler instance used to log all messages."""
1865 hdlr_cls = handlers.TimedRotatingFileHandler
1866 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
xixuan3d48bff2017-01-30 19:00:09 -08001867 interval=_LOG_ROTATION_INTERVAL,
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001868 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -07001869 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001870 return hdlr
1871
1872
Chris Sosacde6bf42012-05-31 18:36:39 -07001873def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001874 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -08001875 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -07001876
1877 # get directory that the devserver is run from
1878 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -07001879 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -07001880 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001881 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -07001882 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -07001883 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001884 parser.add_option('--port',
1885 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 13:13:28 -08001886 help=('port for the dev server to use; if zero, binds to '
1887 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001888 parser.add_option('-t', '--test_image',
1889 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -07001890 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -07001891 parser.add_option('-x', '--xbuddy_manage_builds',
1892 action='store_true',
1893 default=False,
1894 help='If set, allow xbuddy to manage images in'
1895 'build/images.')
Dan Shi72b16132015-10-08 12:10:33 -07001896 parser.add_option('-a', '--android_build_credential',
1897 default=None,
1898 help='Path to a json file which contains the credential '
1899 'needed to access Android builds.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001900 _AddProductionOptions(parser)
1901 _AddUpdateOptions(parser)
1902 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -07001903 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001904
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001905 # Handle options that must be set globally in cherrypy. Do this
1906 # work up front, because calls to _Log() below depend on this
1907 # initialization.
1908 if options.production:
1909 cherrypy.config.update({'environment': 'production'})
1910 if not options.logfile:
1911 cherrypy.config.update({'log.screen': True})
1912 else:
1913 cherrypy.config.update({'log.error_file': '',
1914 'log.access_file': ''})
Paul Hobbsef4e0702016-06-27 17:01:42 -07001915 hdlr = MakeLogHandler(options.logfile)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001916 # Pylint can't seem to process these two calls properly
1917 # pylint: disable=E1101
1918 cherrypy.log.access_log.addHandler(hdlr)
1919 cherrypy.log.error_log.addHandler(hdlr)
1920 # pylint: enable=E1101
1921
joychened64b222013-06-21 16:39:34 -07001922 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001923 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001924
joychened64b222013-06-21 16:39:34 -07001925 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001926 # If our devserver is only supposed to serve payloads, we shouldn't be
1927 # mucking with the cache at all. If the devserver hadn't previously
1928 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001929 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001930 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001931 else:
1932 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001933
Chris Sosadbc20082012-12-10 13:39:11 -08001934 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 16:39:34 -07001935 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001936
joychen121fc9b2013-08-02 14:30:30 -07001937 _xbuddy = xbuddy.XBuddy(options.xbuddy_manage_builds,
1938 options.board,
joychen121fc9b2013-08-02 14:30:30 -07001939 static_dir=options.static_dir)
Chris Sosa75490802013-09-30 17:21:45 -07001940 if options.clear_cache and options.xbuddy_manage_builds:
1941 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 14:30:30 -07001942
Chris Sosa6a3697f2013-01-29 16:44:43 -08001943 # We allow global use here to share with cherrypy classes.
1944 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001945 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001946 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001947 _xbuddy,
joychened64b222013-06-21 16:39:34 -07001948 static_dir=options.static_dir,
Andrew de los Reyes52620802010-04-12 13:40:07 -07001949 urlbase=options.urlbase,
Chris Sosa5d342a22010-09-28 16:54:41 -07001950 forced_image=options.image,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001951 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001952 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-28 23:42:37 -07001953 src_image=options.src_image,
Chris Sosa08d55a22011-01-19 16:08:02 -08001954 board=options.board,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001955 copy_to_static_root=not options.exit,
1956 private_key=options.private_key,
Gabe Black3b567202015-09-23 14:07:59 -07001957 private_key_for_metadata_hash_signature=(
1958 options.private_key_for_metadata_hash_signature),
David Zeuthen52ccd012013-10-31 12:58:26 -07001959 public_key=options.public_key,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001960 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001961 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001962 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001963 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001964 )
Chris Sosa7c931362010-10-11 19:49:01 -07001965
Chris Sosa6a3697f2013-01-29 16:44:43 -08001966 if options.pregenerate_update:
1967 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 15:46:22 -07001968
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001969 if options.exit:
1970 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001971
joychen3cb228e2013-06-12 12:13:13 -07001972 dev_server = DevServerRoot(_xbuddy)
1973
Gilad Arnold11fbef42014-02-10 11:04:13 -08001974 # Patch CherryPy to support binding to any available port (--port=0).
1975 cherrypy_ext.ZeroPortPatcher.DoPatch(cherrypy)
1976
Chris Sosa855b8932013-08-21 13:24:55 -07001977 if options.pidfile:
1978 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1979
Gilad Arnold11fbef42014-02-10 11:04:13 -08001980 if options.portfile:
1981 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1982
Dan Shiafd5c6c2016-01-07 10:27:03 -08001983 if (options.android_build_credential and
1984 os.path.exists(options.android_build_credential)):
1985 try:
1986 with open(options.android_build_credential) as f:
1987 android_build.BuildAccessor.credential_info = json.load(f)
1988 except ValueError as e:
1989 _Log('Failed to load the android build credential: %s. Error: %s.' %
1990 (options.android_build_credential, e))
joychen3cb228e2013-06-12 12:13:13 -07001991 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001992
1993
1994if __name__ == '__main__':
1995 main()