blob: adf07b19461ce90790bcc3f7e5454f4d077f9c8c [file] [log] [blame]
David Riley2fcb0122017-11-02 11:25:39 -07001#!/usr/bin/env python2
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -07002# -*- coding: utf-8 -*-
Chris Sosa781ba6d2012-04-11 12:44:43 -07003# Copyright (c) 2009-2012 The Chromium OS Authors. All rights reserved.
rtc@google.comded22402009-10-26 22:36:21 +00004# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
Chris Sosa3ae4dc12013-03-29 11:47:00 -07007"""Chromium OS development server that can be used for all forms of update.
8
9This devserver can be used to perform system-wide autoupdate and update
10of specific portage packages on devices running Chromium OS derived operating
11systems. It mainly operates in two modes:
12
131) archive mode: In this mode, the devserver is configured to stage and
14serve artifacts from Google Storage using the credentials provided to it before
15it is run. The easiest way to understand this is that the devserver is
16functioning as a local cache for artifacts produced and uploaded by build
17servers. Users of this form of devserver can either download the artifacts
18from the devservers static directory OR use the update RPC to perform a
19system-wide autoupdate. Archive mode is always active.
20
212) artifact-generation mode: in this mode, the devserver will attempt to
22generate update payloads and build artifacts when requested. This mode only
23works in the Chromium OS chroot as it uses build tools only present in the
24chroot (emerge, cros_generate_update_payload, etc.). By default, when a device
25requests an update from this form of devserver, the devserver will attempt to
26discover if a more recent build of the board has been built by the developer
Mike Frysingeraa0cb102019-02-25 01:09:19 -050027and generate a payload that the requested system can autoupdate to.
Chris Sosa3ae4dc12013-03-29 11:47:00 -070028
29For autoupdates, there are many more advanced options that can help specify
30how to update and which payload to give to a requester.
31"""
32
Gabe Black3b567202015-09-23 14:07:59 -070033from __future__ import print_function
Chris Sosa7c931362010-10-11 19:49:01 -070034
Gilad Arnold55a2a372012-10-02 09:46:32 -070035import json
David Riley2fcb0122017-11-02 11:25:39 -070036import optparse # pylint: disable=deprecated-module
rtc@google.comded22402009-10-26 22:36:21 +000037import os
Scott Zawalski4647ce62012-01-03 17:17:28 -050038import re
Simran Basi4baad082013-02-14 13:39:18 -080039import shutil
xixuan52c2fba2016-05-20 17:02:48 -070040import signal
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -080041import socket
Chris Masone816e38c2012-05-02 12:22:36 -070042import subprocess
J. Richard Barnette3d977b82013-04-23 11:05:19 -070043import sys
Chris Masone816e38c2012-05-02 12:22:36 -070044import tempfile
Dan Shi59ae7092013-06-04 14:37:27 -070045import threading
Dan Shiafd0e492015-05-27 14:23:51 -070046import time
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -070047import types
J. Richard Barnette3d977b82013-04-23 11:05:19 -070048from logging import handlers
49
50import cherrypy
David Riley2fcb0122017-11-02 11:25:39 -070051# pylint: disable=no-name-in-module
Chris Sosa855b8932013-08-21 13:24:55 -070052from cherrypy import _cplogging as cplogging
David Riley2fcb0122017-11-02 11:25:39 -070053from cherrypy.process import plugins # pylint: disable=import-error
54# pylint: enable=no-name-in-module
rtc@google.comded22402009-10-26 22:36:21 +000055
Richard Barnettedf35c322017-08-18 17:02:13 -070056# This must happen before any local modules get a chance to import
57# anything from chromite. Otherwise, really bad things will happen, and
58# you will _not_ understand why.
59import setup_chromite # pylint: disable=unused-import
60
Chris Sosa0356d3b2010-09-16 15:46:22 -070061import autoupdate
Dan Shi2f136862016-02-11 15:38:38 -080062import artifact_info
Chris Sosa75490802013-09-30 17:21:45 -070063import build_artifact
Gilad Arnold11fbef42014-02-10 11:04:13 -080064import cherrypy_ext
Gilad Arnoldc65330c2012-09-20 15:17:48 -070065import common_util
Simran Basief83d6a2014-08-28 14:32:01 -070066import devserver_constants
Chris Sosa47a7d4e2012-03-28 11:26:55 -070067import downloader
Gilad Arnoldc65330c2012-09-20 15:17:48 -070068import log_util
joychen3cb228e2013-06-12 12:13:13 -070069import xbuddy
Gilad Arnoldc65330c2012-09-20 15:17:48 -070070
Gilad Arnoldc65330c2012-09-20 15:17:48 -070071# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080072def _Log(message, *args):
73 return log_util.LogWithTag('DEVSERVER', message, *args)
Chris Sosa0356d3b2010-09-16 15:46:22 -070074
Dan Shiafd0e492015-05-27 14:23:51 -070075try:
76 import psutil
77except ImportError:
78 # Ignore psutil import failure. This is for backwards compatibility, so
79 # "cros flash" can still update duts with build without psutil installed.
80 # The reason is that, during cros flash, local devserver code is copied over
81 # to DUT, and devserver will be running inside DUT to stage the build.
82 _Log('Python module psutil is not installed, devserver load data will not be '
83 'collected')
84 psutil = None
Dan Shi94dcbe82015-06-08 20:51:13 -070085except OSError as e:
86 # Ignore error like following. psutil may not work properly in builder. Ignore
87 # the error as load information of devserver is not used in builder.
88 # OSError: [Errno 2] No such file or directory: '/dev/pts/0'
89 _Log('psutil is failed to be imported, error: %s. devserver load data will '
90 'not be collected.', e)
91 psutil = None
92
xixuanac89ce82016-11-30 16:48:20 -080093# Use try-except to skip unneccesary import for simple use case, eg. running
94# devserver on host.
95try:
96 import cros_update
xixuanac89ce82016-11-30 16:48:20 -080097except ImportError as e:
98 _Log('cros_update cannot be imported: %r', e)
99 cros_update = None
xixuana4f4e712017-05-08 15:17:54 -0700100
101try:
102 import cros_update_progress
103except ImportError as e:
104 _Log('cros_update_progress cannot be imported: %r', e)
xixuanac89ce82016-11-30 16:48:20 -0800105 cros_update_progress = None
106
xixuanac89ce82016-11-30 16:48:20 -0800107try:
108 from chromite.lib.paygen import gspaths
109except ImportError as e:
110 _Log('chromite cannot be imported: %r', e)
111 gspaths = None
112
Dan Shi72b16132015-10-08 12:10:33 -0700113try:
114 import android_build
115except ImportError as e:
116 # Ignore android_build import failure. This is to support devserver running
117 # inside a ChromeOS device triggered by cros flash. Most ChromeOS test images
118 # do not have google-api-python-client module and they don't need to support
119 # Android updating, therefore, ignore the import failure here.
Dan Shi72b16132015-10-08 12:10:33 -0700120 android_build = None
Frank Farzan40160872011-12-12 18:39:18 -0800121
Chris Sosa417e55d2011-01-25 16:40:48 -0800122CACHED_ENTRIES = 12
Don Garrettf90edf02010-11-16 17:36:14 -0800123
Simran Basi4baad082013-02-14 13:39:18 -0800124TELEMETRY_FOLDER = 'telemetry_src'
125TELEMETRY_DEPS = ['dep-telemetry_dep.tar.bz2',
126 'dep-page_cycler_dep.tar.bz2',
Simran Basi0d078682013-03-22 16:40:04 -0700127 'dep-chrome_test.tar.bz2',
128 'dep-perf_data_dep.tar.bz2']
Simran Basi4baad082013-02-14 13:39:18 -0800129
Chris Sosa0356d3b2010-09-16 15:46:22 -0700130# Sets up global to share between classes.
rtc@google.com21a5ca32009-11-04 18:23:23 +0000131updater = None
rtc@google.comded22402009-10-26 22:36:21 +0000132
xixuan3d48bff2017-01-30 19:00:09 -0800133# Log rotation parameters. These settings correspond to twice a day once
134# devserver is started, with about two weeks (28 backup files) of old logs
135# kept for backup.
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700136#
xixuan3d48bff2017-01-30 19:00:09 -0800137# For more, see the documentation in standard python library for
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700138# logging.handlers.TimedRotatingFileHandler
xixuan3d48bff2017-01-30 19:00:09 -0800139_LOG_ROTATION_TIME = 'H'
140_LOG_ROTATION_INTERVAL = 12 # hours
141_LOG_ROTATION_BACKUP = 28 # backup counts
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700142
Dan Shiafd0e492015-05-27 14:23:51 -0700143# Number of seconds between the collection of disk and network IO counters.
144STATS_INTERVAL = 10.0
Frank Farzan40160872011-12-12 18:39:18 -0800145
xixuan52c2fba2016-05-20 17:02:48 -0700146# Auto-update parameters
147
148# Error msg for missing key in CrOS auto-update.
Xixuan Wu32af9f12017-11-13 14:11:44 -0800149KEY_ERROR_MSG = 'Key Error in RPC: %s= is required'
xixuan52c2fba2016-05-20 17:02:48 -0700150
151# Command of running auto-update.
152AUTO_UPDATE_CMD = '/usr/bin/python -u %s -d %s -b %s --static_dir %s'
153
154
Chris Sosa9164ca32012-03-28 11:04:50 -0700155class DevServerError(Exception):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700156 """Exception class used by this module."""
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700157
158
Dan Shiafd0e492015-05-27 14:23:51 -0700159def require_psutil():
Gabe Black3b567202015-09-23 14:07:59 -0700160 """Decorator for functions require psutil to run."""
Dan Shiafd0e492015-05-27 14:23:51 -0700161 def deco_require_psutil(func):
162 """Wrapper of the decorator function.
163
Gabe Black3b567202015-09-23 14:07:59 -0700164 Args:
165 func: function to be called.
Dan Shiafd0e492015-05-27 14:23:51 -0700166 """
167 def func_require_psutil(*args, **kwargs):
168 """Decorator for functions require psutil to run.
169
170 If psutil is not installed, skip calling the function.
171
Gabe Black3b567202015-09-23 14:07:59 -0700172 Args:
173 *args: arguments for function to be called.
174 **kwargs: keyword arguments for function to be called.
Dan Shiafd0e492015-05-27 14:23:51 -0700175 """
176 if psutil:
177 return func(*args, **kwargs)
178 else:
179 _Log('Python module psutil is not installed. Function call %s is '
180 'skipped.' % func)
181 return func_require_psutil
182 return deco_require_psutil
183
184
Gabe Black3b567202015-09-23 14:07:59 -0700185def _canonicalize_archive_url(archive_url):
186 """Canonicalizes archive_url strings.
187
188 Raises:
189 DevserverError: if archive_url is not set.
190 """
191 if archive_url:
192 if not archive_url.startswith('gs://'):
193 raise DevServerError("Archive URL isn't from Google Storage (%s) ." %
194 archive_url)
195
196 return archive_url.rstrip('/')
197 else:
198 raise DevServerError("Must specify an archive_url in the request")
199
200
201def _canonicalize_local_path(local_path):
202 """Canonicalizes |local_path| strings.
203
204 Raises:
205 DevserverError: if |local_path| is not set.
206 """
207 # Restrict staging of local content to only files within the static
208 # directory.
209 local_path = os.path.abspath(local_path)
210 if not local_path.startswith(updater.static_dir):
211 raise DevServerError('Local path %s must be a subdirectory of the static'
212 ' directory: %s' % (local_path, updater.static_dir))
213
214 return local_path.rstrip('/')
215
216
217def _get_artifacts(kwargs):
218 """Returns a tuple of named and file artifacts given the stage rpc kwargs.
219
220 Raises:
221 DevserverError if no artifacts would be returned.
222 """
223 artifacts = kwargs.get('artifacts')
224 files = kwargs.get('files')
225 if not artifacts and not files:
226 raise DevServerError('No artifacts specified.')
227
228 # Note we NEED to coerce files to a string as we get raw unicode from
229 # cherrypy and we treat files as strings elsewhere in the code.
230 return (str(artifacts).split(',') if artifacts else [],
231 str(files).split(',') if files else [])
232
233
Dan Shi61305df2015-10-26 16:52:35 -0700234def _is_android_build_request(kwargs):
235 """Check if a devserver call is for Android build, based on the arguments.
236
237 This method exams the request's arguments (os_type) to determine if the
238 request is for Android build. If os_type is set to `android`, returns True.
239 If os_type is not set or has other values, returns False.
240
241 Args:
242 kwargs: Keyword arguments for the request.
243
244 Returns:
245 True if the request is for Android build. False otherwise.
246 """
247 os_type = kwargs.get('os_type', None)
248 return os_type == 'android'
249
250
Gabe Black3b567202015-09-23 14:07:59 -0700251def _get_downloader(kwargs):
252 """Returns the downloader based on passed in arguments.
253
254 Args:
255 kwargs: Keyword arguments for the request.
256 """
257 local_path = kwargs.get('local_path')
258 if local_path:
259 local_path = _canonicalize_local_path(local_path)
260
261 dl = None
262 if local_path:
Prathmesh Prabhu58d08932018-01-19 15:08:19 -0800263 delete_source = _parse_boolean_arg(kwargs, 'delete_source')
264 dl = downloader.LocalDownloader(updater.static_dir, local_path,
265 delete_source=delete_source)
Gabe Black3b567202015-09-23 14:07:59 -0700266
Dan Shi61305df2015-10-26 16:52:35 -0700267 if not _is_android_build_request(kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700268 archive_url = kwargs.get('archive_url')
269 if not archive_url and not local_path:
270 raise DevServerError('Requires archive_url or local_path to be '
271 'specified.')
272 if archive_url and local_path:
273 raise DevServerError('archive_url and local_path can not both be '
274 'specified.')
275 if not dl:
276 archive_url = _canonicalize_archive_url(archive_url)
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700277 dl = downloader.GoogleStorageDownloader(
278 updater.static_dir, archive_url,
279 downloader.GoogleStorageDownloader.GetBuildIdFromArchiveURL(
280 archive_url))
Gabe Black3b567202015-09-23 14:07:59 -0700281 elif not dl:
282 target = kwargs.get('target', None)
Dan Shi72b16132015-10-08 12:10:33 -0700283 branch = kwargs.get('branch', None)
Dan Shi61305df2015-10-26 16:52:35 -0700284 build_id = kwargs.get('build_id', None)
285 if not target or not branch or not build_id:
Dan Shi72b16132015-10-08 12:10:33 -0700286 raise DevServerError(
Dan Shi61305df2015-10-26 16:52:35 -0700287 'target, branch, build ID must all be specified for downloading '
288 'Android build.')
Dan Shi72b16132015-10-08 12:10:33 -0700289 dl = downloader.AndroidBuildDownloader(updater.static_dir, branch, build_id,
290 target)
Gabe Black3b567202015-09-23 14:07:59 -0700291
292 return dl
293
294
295def _get_downloader_and_factory(kwargs):
296 """Returns the downloader and artifact factory based on passed in arguments.
297
298 Args:
299 kwargs: Keyword arguments for the request.
300 """
301 artifacts, files = _get_artifacts(kwargs)
302 dl = _get_downloader(kwargs)
303
304 if (isinstance(dl, downloader.GoogleStorageDownloader) or
305 isinstance(dl, downloader.LocalDownloader)):
306 factory_class = build_artifact.ChromeOSArtifactFactory
Dan Shi72b16132015-10-08 12:10:33 -0700307 elif isinstance(dl, downloader.AndroidBuildDownloader):
Gabe Black3b567202015-09-23 14:07:59 -0700308 factory_class = build_artifact.AndroidArtifactFactory
309 else:
310 raise DevServerError('Unrecognized value for downloader type: %s' %
311 type(dl))
312
313 factory = factory_class(dl.GetBuildDir(), artifacts, files, dl.GetBuild())
314
315 return dl, factory
316
317
Scott Zawalski4647ce62012-01-03 17:17:28 -0500318def _LeadingWhiteSpaceCount(string):
319 """Count the amount of leading whitespace in a string.
320
321 Args:
322 string: The string to count leading whitespace in.
Don Garrettf84631a2014-01-07 18:21:26 -0800323
Scott Zawalski4647ce62012-01-03 17:17:28 -0500324 Returns:
325 number of white space chars before characters start.
326 """
Gabe Black3b567202015-09-23 14:07:59 -0700327 matched = re.match(r'^\s+', string)
Scott Zawalski4647ce62012-01-03 17:17:28 -0500328 if matched:
329 return len(matched.group())
330
331 return 0
332
333
334def _PrintDocStringAsHTML(func):
335 """Make a functions docstring somewhat HTML style.
336
337 Args:
338 func: The function to return the docstring from.
Don Garrettf84631a2014-01-07 18:21:26 -0800339
Scott Zawalski4647ce62012-01-03 17:17:28 -0500340 Returns:
341 A string that is somewhat formated for a web browser.
342 """
343 # TODO(scottz): Make this parse Args/Returns in a prettier way.
344 # Arguments could be bolded and indented etc.
345 html_doc = []
346 for line in func.__doc__.splitlines():
347 leading_space = _LeadingWhiteSpaceCount(line)
348 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700349 line = ' ' * leading_space + line
Scott Zawalski4647ce62012-01-03 17:17:28 -0500350
351 html_doc.append('<BR>%s' % line)
352
353 return '\n'.join(html_doc)
354
355
Simran Basief83d6a2014-08-28 14:32:01 -0700356def _GetUpdateTimestampHandler(static_dir):
357 """Returns a handler to update directory staged.timestamp.
358
359 This handler resets the stage.timestamp whenever static content is accessed.
360
361 Args:
362 static_dir: Directory from which static content is being staged.
363
364 Returns:
365 A cherrypy handler to update the timestamp of accessed content.
366 """
367 def UpdateTimestampHandler():
368 if not '404' in cherrypy.response.status:
369 build_match = re.match(devserver_constants.STAGED_BUILD_REGEX,
370 cherrypy.request.path_info)
371 if build_match:
372 build_dir = os.path.join(static_dir, build_match.group('build'))
373 downloader.Downloader.TouchTimestampForStaged(build_dir)
374 return UpdateTimestampHandler
375
376
Chris Sosa7c931362010-10-11 19:49:01 -0700377def _GetConfig(options):
378 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800379
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800380 socket_host = '::'
Yu-Ju Hongc8d4af32013-11-12 15:14:26 -0800381 # Fall back to IPv4 when python is not configured with IPv6.
382 if not socket.has_ipv6:
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800383 socket_host = '0.0.0.0'
384
Simran Basief83d6a2014-08-28 14:32:01 -0700385 # Adds the UpdateTimestampHandler to cherrypy's tools. This tools executes
386 # on the on_end_resource hook. This hook is called once processing is
387 # complete and the response is ready to be returned.
388 cherrypy.tools.update_timestamp = cherrypy.Tool(
389 'on_end_resource', _GetUpdateTimestampHandler(options.static_dir))
390
David Riley2fcb0122017-11-02 11:25:39 -0700391 base_config = {
392 'global': {
393 'server.log_request_headers': True,
394 'server.protocol_version': 'HTTP/1.1',
395 'server.socket_host': socket_host,
396 'server.socket_port': int(options.port),
397 'response.timeout': 6000,
398 'request.show_tracebacks': True,
399 'server.socket_timeout': 60,
400 'server.thread_pool': 2,
401 'engine.autoreload.on': False,
402 },
403 '/api': {
404 # Gets rid of cherrypy parsing post file for args.
405 'request.process_request_body': False,
406 },
407 '/build': {
408 'response.timeout': 100000,
409 },
410 '/update': {
411 # Gets rid of cherrypy parsing post file for args.
412 'request.process_request_body': False,
413 'response.timeout': 10000,
414 },
415 # Sets up the static dir for file hosting.
416 '/static': {
417 'tools.staticdir.dir': options.static_dir,
418 'tools.staticdir.on': True,
419 'response.timeout': 10000,
420 'tools.update_timestamp.on': True,
421 },
422 }
Chris Sosa5f118ef2012-07-12 11:37:50 -0700423 if options.production:
Alex Miller93beca52013-07-30 19:25:09 -0700424 base_config['global'].update({'server.thread_pool': 150})
Scott Zawalski1c5e7cd2012-02-27 13:12:52 -0500425
Chris Sosa7c931362010-10-11 19:49:01 -0700426 return base_config
rtc@google.com64244662009-11-12 00:52:08 +0000427
Darin Petkove17164a2010-08-11 13:24:41 -0700428
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700429def _GetRecursiveMemberObject(root, member_list):
430 """Returns an object corresponding to a nested member list.
431
432 Args:
433 root: the root object to search
434 member_list: list of nested members to search
Don Garrettf84631a2014-01-07 18:21:26 -0800435
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700436 Returns:
437 An object corresponding to the member name list; None otherwise.
438 """
439 for member in member_list:
440 next_root = root.__class__.__dict__.get(member)
441 if not next_root:
442 return None
443 root = next_root
444 return root
445
446
447def _IsExposed(name):
448 """Returns True iff |name| has an `exposed' attribute and it is set."""
449 return hasattr(name, 'exposed') and name.exposed
450
451
Gilad Arnold748c8322012-10-12 09:51:35 -0700452def _GetExposedMethod(root, nested_member, ignored=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700453 """Returns a CherryPy-exposed method, if such exists.
454
455 Args:
456 root: the root object for searching
457 nested_member: a slash-joined path to the nested member
458 ignored: method paths to be ignored
Don Garrettf84631a2014-01-07 18:21:26 -0800459
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700460 Returns:
461 A function object corresponding to the path defined by |member_list| from
462 the |root| object, if the function is exposed and not ignored; None
463 otherwise.
464 """
Gilad Arnold748c8322012-10-12 09:51:35 -0700465 method = (not (ignored and nested_member in ignored) and
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700466 _GetRecursiveMemberObject(root, nested_member.split('/')))
Gabe Black3b567202015-09-23 14:07:59 -0700467 if method and type(method) == types.FunctionType and _IsExposed(method):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700468 return method
469
470
Gilad Arnold748c8322012-10-12 09:51:35 -0700471def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700472 """Finds exposed CherryPy methods.
473
474 Args:
475 root: the root object for searching
476 prefix: slash-joined chain of members leading to current object
477 unlisted: URLs to be excluded regardless of their exposed status
Don Garrettf84631a2014-01-07 18:21:26 -0800478
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700479 Returns:
480 List of exposed URLs that are not unlisted.
481 """
482 method_list = []
483 for member in sorted(root.__class__.__dict__.keys()):
484 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 09:51:35 -0700485 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700486 continue
487 member_obj = root.__class__.__dict__[member]
488 if _IsExposed(member_obj):
489 if type(member_obj) == types.FunctionType:
490 method_list.append(prefixed_member)
491 else:
492 method_list += _FindExposedMethods(
493 member_obj, prefixed_member, unlisted)
494 return method_list
495
496
xixuan52c2fba2016-05-20 17:02:48 -0700497def _check_base_args_for_auto_update(kwargs):
xixuanac89ce82016-11-30 16:48:20 -0800498 """Check basic args required for auto-update.
499
500 Args:
501 kwargs: the parameters to be checked.
502
503 Raises:
504 DevServerHTTPError if required parameters don't exist in kwargs.
505 """
xixuan52c2fba2016-05-20 17:02:48 -0700506 if 'host_name' not in kwargs:
507 raise common_util.DevServerHTTPError(KEY_ERROR_MSG % 'host_name')
508
509 if 'build_name' not in kwargs:
510 raise common_util.DevServerHTTPError(KEY_ERROR_MSG % 'build_name')
511
512
513def _parse_boolean_arg(kwargs, key):
xixuanac89ce82016-11-30 16:48:20 -0800514 """Parse boolean arg from kwargs.
515
516 Args:
517 kwargs: the parameters to be checked.
518 key: the key to be parsed.
519
520 Returns:
521 The boolean value of kwargs[key], or False if key doesn't exist in kwargs.
522
523 Raises:
524 DevServerHTTPError if kwargs[key] is not a boolean variable.
525 """
xixuan52c2fba2016-05-20 17:02:48 -0700526 if key in kwargs:
527 if kwargs[key] == 'True':
528 return True
529 elif kwargs[key] == 'False':
530 return False
531 else:
532 raise common_util.DevServerHTTPError(
533 'The value for key %s is not boolean.' % key)
534 else:
535 return False
536
xixuan447ad9d2017-02-28 14:46:20 -0800537
xixuanac89ce82016-11-30 16:48:20 -0800538def _parse_string_arg(kwargs, key):
539 """Parse string arg from kwargs.
540
541 Args:
542 kwargs: the parameters to be checked.
543 key: the key to be parsed.
544
545 Returns:
546 The string value of kwargs[key], or None if key doesn't exist in kwargs.
547 """
548 if key in kwargs:
549 return kwargs[key]
550 else:
551 return None
552
xixuan447ad9d2017-02-28 14:46:20 -0800553
xixuanac89ce82016-11-30 16:48:20 -0800554def _build_uri_from_build_name(build_name):
555 """Get build url from a given build name.
556
557 Args:
558 build_name: the build name to be parsed, whose format is
559 'board/release_version'.
560
561 Returns:
562 The release_archive_url on Google Storage for this build name.
563 """
564 return gspaths.ChromeosReleases.BuildUri(
565 cros_update.STABLE_BUILD_CHANNEL, build_name.split('/')[0],
566 build_name.split('/')[1])
xixuan52c2fba2016-05-20 17:02:48 -0700567
xixuan447ad9d2017-02-28 14:46:20 -0800568
569def _clear_process(host_name, pid):
570 """Clear AU process for given hostname and pid.
571
572 This clear includes:
573 1. kill process if it's alive.
574 2. delete the track status file of this process.
575 3. delete the executing log file of this process.
576
577 Args:
578 host_name: the host to execute auto-update.
579 pid: the background auto-update process id.
580 """
581 if cros_update_progress.IsProcessAlive(pid):
582 os.killpg(int(pid), signal.SIGKILL)
583
584 cros_update_progress.DelTrackStatusFile(host_name, pid)
585 cros_update_progress.DelExecuteLogFile(host_name, pid)
586
587
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700588class ApiRoot(object):
589 """RESTful API for Dev Server information."""
590 exposed = True
591
592 @cherrypy.expose
593 def hostinfo(self, ip):
594 """Returns a JSON dictionary containing information about the given ip.
595
Gilad Arnold1b908392012-10-05 11:36:27 -0700596 Args:
597 ip: address of host whose info is requested
Don Garrettf84631a2014-01-07 18:21:26 -0800598
Gilad Arnold1b908392012-10-05 11:36:27 -0700599 Returns:
600 A JSON dictionary containing all or some of the following fields:
601 last_event_type (int): last update event type received
602 last_event_status (int): last update event status received
603 last_known_version (string): last known version reported in update ping
604 forced_update_label (string): update label to force next update ping to
605 use, set by setnextupdate
606 See the OmahaEvent class in update_engine/omaha_request_action.h for
607 event type and status code definitions. If the ip does not exist an empty
608 string is returned.
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700609
Gilad Arnold1b908392012-10-05 11:36:27 -0700610 Example URL:
611 http://myhost/api/hostinfo?ip=192.168.1.5
612 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700613 return updater.HandleHostInfoPing(ip)
614
615 @cherrypy.expose
Gilad Arnold286a0062012-01-12 13:47:02 -0800616 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 11:36:27 -0700617 """Returns a JSON object containing a log of host event.
618
619 Args:
620 ip: address of host whose event log is requested, or `all'
Don Garrettf84631a2014-01-07 18:21:26 -0800621
Gilad Arnold1b908392012-10-05 11:36:27 -0700622 Returns:
623 A JSON encoded list (log) of dictionaries (events), each of which
624 containing a `timestamp' and other event fields, as described under
625 /api/hostinfo.
626
627 Example URL:
628 http://myhost/api/hostlog?ip=192.168.1.5
629 """
Gilad Arnold286a0062012-01-12 13:47:02 -0800630 return updater.HandleHostLogPing(ip)
631
632 @cherrypy.expose
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700633 def setnextupdate(self, ip):
634 """Allows the response to the next update ping from a host to be set.
635
636 Takes the IP of the host and an update label as normally provided to the
Gilad Arnold1b908392012-10-05 11:36:27 -0700637 /update command.
638 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700639 body_length = int(cherrypy.request.headers['Content-Length'])
640 label = cherrypy.request.rfile.read(body_length)
641
642 if label:
643 label = label.strip()
644 if label:
645 return updater.HandleSetUpdatePing(ip, label)
Chris Sosa4b951602014-04-09 20:26:07 -0700646 raise common_util.DevServerHTTPError(400, 'No label provided.')
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700647
648
Gilad Arnold55a2a372012-10-02 09:46:32 -0700649 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800650 def fileinfo(self, *args):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700651 """Returns information about a given staged file.
652
653 Args:
Don Garrettf84631a2014-01-07 18:21:26 -0800654 args: path to the file inside the server's static staging directory
655
Gilad Arnold55a2a372012-10-02 09:46:32 -0700656 Returns:
657 A JSON encoded dictionary with information about the said file, which may
658 contain the following keys/values:
Gilad Arnold1b908392012-10-05 11:36:27 -0700659 size (int): the file size in bytes
660 sha1 (string): a base64 encoded SHA1 hash
661 sha256 (string): a base64 encoded SHA256 hash
662
663 Example URL:
664 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 09:46:32 -0700665 """
Don Garrettf84631a2014-01-07 18:21:26 -0800666 file_path = os.path.join(updater.static_dir, *args)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700667 if not os.path.exists(file_path):
668 raise DevServerError('file not found: %s' % file_path)
669 try:
670 file_size = os.path.getsize(file_path)
671 file_sha1 = common_util.GetFileSha1(file_path)
672 file_sha256 = common_util.GetFileSha256(file_path)
673 except os.error, e:
674 raise DevServerError('failed to get info for file %s: %s' %
Gilad Arnolde74b3812013-04-22 11:27:38 -0700675 (file_path, e))
676
677 is_delta = autoupdate.Autoupdate.IsDeltaFormatFile(file_path)
678
679 return json.dumps({
680 autoupdate.Autoupdate.SIZE_ATTR: file_size,
681 autoupdate.Autoupdate.SHA1_ATTR: file_sha1,
682 autoupdate.Autoupdate.SHA256_ATTR: file_sha256,
683 autoupdate.Autoupdate.ISDELTA_ATTR: is_delta
684 })
Gilad Arnold55a2a372012-10-02 09:46:32 -0700685
Chris Sosa76e44b92013-01-31 12:11:38 -0800686
David Rochberg7c79a812011-01-19 14:24:45 -0500687class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700688 """The Root Class for the Dev Server.
689
690 CherryPy works as follows:
691 For each method in this class, cherrpy interprets root/path
692 as a call to an instance of DevServerRoot->method_name. For example,
693 a call to http://myhost/build will call build. CherryPy automatically
694 parses http args and places them as keyword arguments in each method.
695 For paths http://myhost/update/dir1/dir2, you can use *args so that
696 cherrypy uses the update method and puts the extra paths in args.
697 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700698 # Method names that should not be listed on the index page.
699 _UNLISTED_METHODS = ['index', 'doc']
700
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700701 api = ApiRoot()
Chris Sosa7c931362010-10-11 19:49:01 -0700702
Dan Shi59ae7092013-06-04 14:37:27 -0700703 # Number of threads that devserver is staging images.
704 _staging_thread_count = 0
705 # Lock used to lock increasing/decreasing count.
706 _staging_thread_count_lock = threading.Lock()
707
Dan Shiafd0e492015-05-27 14:23:51 -0700708 @require_psutil()
709 def _refresh_io_stats(self):
710 """A call running in a thread to update IO stats periodically."""
711 prev_disk_io_counters = psutil.disk_io_counters()
712 prev_network_io_counters = psutil.net_io_counters()
713 prev_read_time = time.time()
714 while True:
715 time.sleep(STATS_INTERVAL)
716 now = time.time()
717 interval = now - prev_read_time
718 prev_read_time = now
719 # Disk IO is for all disks.
720 disk_io_counters = psutil.disk_io_counters()
721 network_io_counters = psutil.net_io_counters()
722
723 self.disk_read_bytes_per_sec = (
724 disk_io_counters.read_bytes -
725 prev_disk_io_counters.read_bytes)/interval
726 self.disk_write_bytes_per_sec = (
727 disk_io_counters.write_bytes -
728 prev_disk_io_counters.write_bytes)/interval
729 prev_disk_io_counters = disk_io_counters
730
731 self.network_sent_bytes_per_sec = (
732 network_io_counters.bytes_sent -
733 prev_network_io_counters.bytes_sent)/interval
734 self.network_recv_bytes_per_sec = (
735 network_io_counters.bytes_recv -
736 prev_network_io_counters.bytes_recv)/interval
737 prev_network_io_counters = network_io_counters
738
739 @require_psutil()
740 def _start_io_stat_thread(self):
Gabe Black3b567202015-09-23 14:07:59 -0700741 """Start the thread to collect IO stats."""
Dan Shiafd0e492015-05-27 14:23:51 -0700742 thread = threading.Thread(target=self._refresh_io_stats)
743 thread.daemon = True
744 thread.start()
745
joychen3cb228e2013-06-12 12:13:13 -0700746 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700747 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800748 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 12:13:13 -0700749 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 14:24:45 -0500750
Dan Shiafd0e492015-05-27 14:23:51 -0700751 # Cache of disk IO stats, a thread refresh the stats every 10 seconds.
752 # lock is not used for these variables as the only thread writes to these
753 # variables is _refresh_io_stats.
754 self.disk_read_bytes_per_sec = 0
755 self.disk_write_bytes_per_sec = 0
756 # Cache of network IO stats.
757 self.network_sent_bytes_per_sec = 0
758 self.network_recv_bytes_per_sec = 0
759 self._start_io_stat_thread()
760
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700761 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500762 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700763 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700764 import builder
765 if self._builder is None:
766 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500767 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700768
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700769 @cherrypy.expose
Dan Shif8eb0d12013-08-01 17:52:06 -0700770 def is_staged(self, **kwargs):
771 """Check if artifacts have been downloaded.
772
Chris Sosa6b0c6172013-08-05 17:01:33 -0700773 async: True to return without waiting for download to complete.
774 artifacts: Comma separated list of named artifacts to download.
775 These are defined in artifact_info and have their implementation
776 in build_artifact.py.
777 files: Comma separated list of file artifacts to stage. These
778 will be available as is in the corresponding static directory with no
779 custom post-processing.
780
781 returns: True of all artifacts are staged.
Dan Shif8eb0d12013-08-01 17:52:06 -0700782
783 Example:
784 To check if autotest and test_suites are staged:
785 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
786 artifacts=autotest,test_suites
787 """
Gabe Black3b567202015-09-23 14:07:59 -0700788 dl, factory = _get_downloader_and_factory(kwargs)
Aviv Keshet57d18172016-06-18 20:39:09 -0700789 response = str(dl.IsStaged(factory))
790 _Log('Responding to is_staged %s request with %r', kwargs, response)
791 return response
Dan Shi59ae7092013-06-04 14:37:27 -0700792
Chris Sosa76e44b92013-01-31 12:11:38 -0800793 @cherrypy.expose
Prashanth Ba06d2d22014-03-07 15:35:19 -0800794 def list_image_dir(self, **kwargs):
795 """Take an archive url and list the contents in its staged directory.
796
797 Args:
798 kwargs:
799 archive_url: Google Storage URL for the build.
800
801 Example:
802 To list the contents of where this devserver should have staged
803 gs://image-archive/<board>-release/<build> call:
804 http://devserver_url:<port>/list_image_dir?archive_url=<gs://..>
805
806 Returns:
807 A string with information about the contents of the image directory.
808 """
Gabe Black3b567202015-09-23 14:07:59 -0700809 dl = _get_downloader(kwargs)
Prashanth Ba06d2d22014-03-07 15:35:19 -0800810 try:
Gabe Black3b567202015-09-23 14:07:59 -0700811 image_dir_contents = dl.ListBuildDir()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800812 except build_artifact.ArtifactDownloadError as e:
813 return 'Cannot list the contents of staged artifacts. %s' % e
814 if not image_dir_contents:
Gabe Black3b567202015-09-23 14:07:59 -0700815 return '%s has not been staged on this devserver.' % dl.DescribeSource()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800816 return image_dir_contents
817
818 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800819 def stage(self, **kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700820 """Downloads and caches build artifacts.
Chris Sosa76e44b92013-01-31 12:11:38 -0800821
Gabe Black3b567202015-09-23 14:07:59 -0700822 Downloads and caches build artifacts, possibly from a Google Storage URL,
Dan Shi72b16132015-10-08 12:10:33 -0700823 or from Android's build server. Returns once these have been downloaded
Gabe Black3b567202015-09-23 14:07:59 -0700824 on the devserver. A call to this will attempt to cache non-specified
825 artifacts in the background for the given from the given URL following
826 the principle of spatial locality. Spatial locality of different
Chris Sosa76e44b92013-01-31 12:11:38 -0800827 artifacts is explicitly defined in the build_artifact module.
828
829 These artifacts will then be available from the static/ sub-directory of
830 the devserver.
831
832 Args:
833 archive_url: Google Storage URL for the build.
Simran Basi4243a862014-12-12 12:48:33 -0800834 local_path: Local path for the build.
Prathmesh Prabhu58d08932018-01-19 15:08:19 -0800835 delete_source: Only meaningful with local_path. bool to indicate if the
836 source files should be deleted. This is especially useful when staging
837 a file locally in resource constrained environments as it allows us to
838 move the relevant files locally instead of copying them.
Dan Shif8eb0d12013-08-01 17:52:06 -0700839 async: True to return without waiting for download to complete.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700840 artifacts: Comma separated list of named artifacts to download.
841 These are defined in artifact_info and have their implementation
842 in build_artifact.py.
843 files: Comma separated list of files to stage. These
844 will be available as is in the corresponding static directory with no
845 custom post-processing.
Laurence Goodbyf5c958d2016-01-14 18:23:56 -0800846 clean: True to remove any previously staged artifacts first.
Chris Sosa76e44b92013-01-31 12:11:38 -0800847
848 Example:
849 To download the autotest and test suites tarballs:
850 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
851 artifacts=autotest,test_suites
852 To download the full update payload:
853 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
854 artifacts=full_payload
Chris Sosa6b0c6172013-08-05 17:01:33 -0700855 To download just a file called blah.bin:
856 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
857 files=blah.bin
Chris Sosa76e44b92013-01-31 12:11:38 -0800858
859 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 16:39:34 -0700860 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 12:11:38 -0800861
862 Note for this example, relative path is the archive_url stripped of its
863 basename i.e. path/ in the examples above. Specific example:
864
865 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
866
867 Will get staged to:
868
joychened64b222013-06-21 16:39:34 -0700869 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Chris Sosa76e44b92013-01-31 12:11:38 -0800870 """
Gabe Black3b567202015-09-23 14:07:59 -0700871 dl, factory = _get_downloader_and_factory(kwargs)
872
Dan Shi59ae7092013-06-04 14:37:27 -0700873 with DevServerRoot._staging_thread_count_lock:
874 DevServerRoot._staging_thread_count += 1
875 try:
Laurence Goodbyf5c958d2016-01-14 18:23:56 -0800876 boolean_string = kwargs.get('clean')
877 clean = xbuddy.XBuddy.ParseBoolean(boolean_string)
878 if clean and os.path.exists(dl.GetBuildDir()):
879 _Log('Removing %s' % dl.GetBuildDir())
880 shutil.rmtree(dl.GetBuildDir())
Gabe Black3b567202015-09-23 14:07:59 -0700881 async = kwargs.get('async', False)
882 dl.Download(factory, async=async)
Dan Shi59ae7092013-06-04 14:37:27 -0700883 finally:
884 with DevServerRoot._staging_thread_count_lock:
885 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800886 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700887
888 @cherrypy.expose
xixuan52c2fba2016-05-20 17:02:48 -0700889 def cros_au(self, **kwargs):
890 """Auto-update a CrOS DUT.
891
892 Args:
893 kwargs:
894 host_name: the hostname of the DUT to auto-update.
895 build_name: the build name for update the DUT.
896 force_update: Force an update even if the version installed is the
897 same. Default: False.
898 full_update: If True, do not run stateful update, directly force a full
899 reimage. If False, try stateful update first if the dut is already
900 installed with the same version.
901 async: Whether the auto_update function is ran in the background.
David Rileyee75de22017-11-02 10:48:15 -0700902 quick_provision: Whether the quick provision path is attempted first.
xixuan52c2fba2016-05-20 17:02:48 -0700903
904 Returns:
905 A tuple includes two elements:
906 a boolean variable represents whether the auto-update process is
907 successfully started.
908 an integer represents the background auto-update process id.
909 """
910 _check_base_args_for_auto_update(kwargs)
911
912 host_name = kwargs['host_name']
913 build_name = kwargs['build_name']
914 force_update = _parse_boolean_arg(kwargs, 'force_update')
915 full_update = _parse_boolean_arg(kwargs, 'full_update')
916 async = _parse_boolean_arg(kwargs, 'async')
xixuanac89ce82016-11-30 16:48:20 -0800917 original_build = _parse_string_arg(kwargs, 'original_build')
David Haddock90e49442017-04-07 19:14:09 -0700918 payload_filename = _parse_string_arg(kwargs, 'payload_filename')
David Haddock20559612017-06-28 22:15:08 -0700919 clobber_stateful = _parse_boolean_arg(kwargs, 'clobber_stateful')
David Rileyee75de22017-11-02 10:48:15 -0700920 quick_provision = _parse_boolean_arg(kwargs, 'quick_provision')
921
922 devserver_url = updater.GetDevserverUrl()
923 static_url = updater.GetStaticUrl()
xixuan52c2fba2016-05-20 17:02:48 -0700924
925 if async:
926 path = os.path.dirname(os.path.abspath(__file__))
927 execute_file = os.path.join(path, 'cros_update.py')
928 args = (AUTO_UPDATE_CMD % (execute_file, host_name, build_name,
929 updater.static_dir))
xixuanac89ce82016-11-30 16:48:20 -0800930
931 # The original_build's format is like: link/3428.210.0
932 # The corresponding release_archive_url's format is like:
933 # gs://chromeos-releases/stable-channel/link/3428.210.0
934 if original_build:
935 release_archive_url = _build_uri_from_build_name(original_build)
936 # First staging the stateful.tgz synchronousely.
937 self.stage(files='stateful.tgz', async=False,
938 archive_url=release_archive_url)
939 args = ('%s --original_build %s' % (args, original_build))
940
xixuan52c2fba2016-05-20 17:02:48 -0700941 if force_update:
942 args = ('%s --force_update' % args)
943
944 if full_update:
945 args = ('%s --full_update' % args)
946
David Haddock90e49442017-04-07 19:14:09 -0700947 if payload_filename:
948 args = ('%s --payload_filename %s' % (args, payload_filename))
949
David Haddock20559612017-06-28 22:15:08 -0700950 if clobber_stateful:
951 args = ('%s --clobber_stateful' % args)
952
David Rileyee75de22017-11-02 10:48:15 -0700953 if quick_provision:
954 args = ('%s --quick_provision' % args)
955
956 if devserver_url:
957 args = ('%s --devserver_url %s' % (args, devserver_url))
958
959 if static_url:
960 args = ('%s --static_url %s' % (args, static_url))
961
xixuan2a0970a2016-08-10 12:12:44 -0700962 p = subprocess.Popen([args], shell=True, preexec_fn=os.setsid)
963 pid = os.getpgid(p.pid)
xixuan52c2fba2016-05-20 17:02:48 -0700964
965 # Pre-write status in the track_status_file before the first call of
966 # 'get_au_status' to make sure that the track_status_file exists.
xixuan2a0970a2016-08-10 12:12:44 -0700967 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -0700968 progress_tracker.WriteStatus('CrOS update is just started.')
969
xixuan2a0970a2016-08-10 12:12:44 -0700970 return json.dumps((True, pid))
xixuan52c2fba2016-05-20 17:02:48 -0700971 else:
972 cros_update_trigger = cros_update.CrOSUpdateTrigger(
xixuanac89ce82016-11-30 16:48:20 -0800973 host_name, build_name, updater.static_dir, force_update=force_update,
David Rileyee75de22017-11-02 10:48:15 -0700974 full_update=full_update, original_build=original_build,
975 quick_provision=quick_provision, devserver_url=devserver_url,
976 static_url=static_url)
xixuan52c2fba2016-05-20 17:02:48 -0700977 cros_update_trigger.TriggerAU()
xixuan27d50442017-08-09 10:38:25 -0700978 return json.dumps((True, -1))
xixuan52c2fba2016-05-20 17:02:48 -0700979
980 @cherrypy.expose
981 def get_au_status(self, **kwargs):
982 """Check if the auto-update task is finished.
983
984 It handles 4 cases:
985 1. If an error exists in the track_status_file, delete the track file and
986 raise it.
987 2. If cros-update process is finished, delete the file and return the
988 success result.
989 3. If the process is not running, delete the track file and raise an error
990 about 'the process is terminated due to unknown reason'.
991 4. If the track_status_file does not exist, kill the process if it exists,
992 and raise the IOError.
993
994 Args:
995 kwargs:
996 host_name: the hostname of the DUT to auto-update.
997 pid: the background process id of cros-update.
998
999 Returns:
xixuan28d99072016-10-06 12:24:16 -07001000 A dict with three elements:
xixuan52c2fba2016-05-20 17:02:48 -07001001 a boolean variable represents whether the auto-update process is
1002 finished.
1003 a string represents the current auto-update process status.
1004 For example, 'Transfer Devserver/Stateful Update Package'.
xixuan28d99072016-10-06 12:24:16 -07001005 a detailed error message paragraph if there exists an Auto-Update
1006 error, in which the last line shows the main exception. Empty
1007 string otherwise.
xixuan52c2fba2016-05-20 17:02:48 -07001008 """
1009 if 'host_name' not in kwargs:
1010 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
1011
1012 if 'pid' not in kwargs:
1013 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
1014
1015 host_name = kwargs['host_name']
1016 pid = kwargs['pid']
1017 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
1018
xixuan28d99072016-10-06 12:24:16 -07001019 result_dict = {'finished': False, 'status': '', 'detailed_error_msg': ''}
xixuan52c2fba2016-05-20 17:02:48 -07001020 try:
1021 result = progress_tracker.ReadStatus()
1022 if result.startswith(cros_update_progress.ERROR_TAG):
xixuan28d99072016-10-06 12:24:16 -07001023 result_dict['detailed_error_msg'] = result[len(
1024 cros_update_progress.ERROR_TAG):]
xixuan28681fd2016-11-23 11:13:56 -08001025 elif result == cros_update_progress.FINISHED:
xixuan28d99072016-10-06 12:24:16 -07001026 result_dict['finished'] = True
1027 result_dict['status'] = result
xixuan28681fd2016-11-23 11:13:56 -08001028 elif not cros_update_progress.IsProcessAlive(pid):
xixuan28d99072016-10-06 12:24:16 -07001029 result_dict['detailed_error_msg'] = (
1030 'Cros_update process terminated midway due to unknown reason. '
1031 'Last update status was %s' % result)
xixuan28681fd2016-11-23 11:13:56 -08001032 else:
1033 result_dict['status'] = result
1034 except IOError as e:
1035 if pid and cros_update_progress.IsProcessAlive(pid):
xixuan2a0970a2016-08-10 12:12:44 -07001036 os.killpg(int(pid), signal.SIGKILL)
xixuan52c2fba2016-05-20 17:02:48 -07001037
xixuan28681fd2016-11-23 11:13:56 -08001038 result_dict['detailed_error_msg'] = str(e)
1039
1040 return json.dumps(result_dict)
xixuan52c2fba2016-05-20 17:02:48 -07001041
1042 @cherrypy.expose
David Riley6d5fca02017-10-31 10:35:47 -07001043 def post_au_status(self, status, **kwargs):
1044 """Updates the status of an auto-update task.
1045
1046 Callers will need to POST to this URL with a body of MIME-type
1047 "multipart/form-data".
1048 The body should include a single argument, 'status', containing the
1049 AU status to record.
1050
1051 Args:
1052 status: The updated status.
1053 kwargs:
1054 host_name: the hostname of the DUT to auto-update.
1055 pid: the background process id of cros-update.
1056 """
1057 if 'host_name' not in kwargs:
1058 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
1059
1060 if 'pid' not in kwargs:
1061 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
1062
1063 host_name = kwargs['host_name']
1064 pid = kwargs['pid']
David Riley3cea2582017-11-24 22:03:01 -08001065 status = status.rstrip()
1066 _Log('Recording status for %s (%s): %s' % (host_name, pid, status))
David Riley6d5fca02017-10-31 10:35:47 -07001067 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
1068
David Riley3cea2582017-11-24 22:03:01 -08001069 progress_tracker.WriteStatus(status)
David Riley6d5fca02017-10-31 10:35:47 -07001070
1071 return 'True'
1072
1073 @cherrypy.expose
xixuan52c2fba2016-05-20 17:02:48 -07001074 def handler_cleanup(self, **kwargs):
xixuan3bc974e2016-10-18 17:21:43 -07001075 """Clean track status log and temp directory for CrOS auto-update process.
xixuan52c2fba2016-05-20 17:02:48 -07001076
1077 Args:
1078 kwargs:
1079 host_name: the hostname of the DUT to auto-update.
1080 pid: the background process id of cros-update.
1081 """
1082 if 'host_name' not in kwargs:
1083 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
1084
1085 if 'pid' not in kwargs:
1086 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
1087
1088 host_name = kwargs['host_name']
1089 pid = kwargs['pid']
1090 cros_update_progress.DelTrackStatusFile(host_name, pid)
xixuan3bc974e2016-10-18 17:21:43 -07001091 cros_update_progress.DelAUTempDirectory(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -07001092
1093 @cherrypy.expose
1094 def kill_au_proc(self, **kwargs):
1095 """Kill CrOS auto-update process using given process id.
1096
1097 Args:
1098 kwargs:
1099 host_name: Kill all the CrOS auto-update process of this host.
1100
1101 Returns:
1102 True if all processes are killed properly.
1103 """
1104 if 'host_name' not in kwargs:
1105 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
1106
xixuan447ad9d2017-02-28 14:46:20 -08001107 cur_pid = kwargs.get('pid')
1108
xixuan52c2fba2016-05-20 17:02:48 -07001109 host_name = kwargs['host_name']
xixuan3bc974e2016-10-18 17:21:43 -07001110 track_log_list = cros_update_progress.GetAllTrackStatusFileByHostName(
1111 host_name)
xixuan52c2fba2016-05-20 17:02:48 -07001112 for log in track_log_list:
1113 # The track log's full path is: path/host_name_pid.log
1114 # Use splitext to remove file extension, then parse pid from the
1115 # filename.
1116 pid = os.path.splitext(os.path.basename(log))[0][len(host_name)+1:]
xixuan447ad9d2017-02-28 14:46:20 -08001117 _clear_process(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -07001118
xixuan447ad9d2017-02-28 14:46:20 -08001119 if cur_pid:
1120 _clear_process(host_name, cur_pid)
xixuan52c2fba2016-05-20 17:02:48 -07001121
1122 return 'True'
1123
1124 @cherrypy.expose
1125 def collect_cros_au_log(self, **kwargs):
1126 """Collect CrOS auto-update log.
1127
1128 Args:
1129 kwargs:
1130 host_name: the hostname of the DUT to auto-update.
1131 pid: the background process id of cros-update.
1132
1133 Returns:
David Haddock9f459632017-05-11 14:45:46 -07001134 A dictionary containing the execute log file and any hostlog files.
xixuan52c2fba2016-05-20 17:02:48 -07001135 """
1136 if 'host_name' not in kwargs:
1137 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
1138
1139 if 'pid' not in kwargs:
1140 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
1141
1142 host_name = kwargs['host_name']
1143 pid = kwargs['pid']
xixuan3bc974e2016-10-18 17:21:43 -07001144
1145 # Fetch the execute log recorded by cros_update_progress.
xixuan1bbfaba2016-10-13 17:53:22 -07001146 au_log = cros_update_progress.ReadExecuteLogFile(host_name, pid)
1147 cros_update_progress.DelExecuteLogFile(host_name, pid)
David Haddock9f459632017-05-11 14:45:46 -07001148 # Fetch the cros_au host_logs if they exist
1149 au_hostlogs = cros_update_progress.ReadAUHostLogFiles(host_name, pid)
1150 return json.dumps({'cros_au_log': au_log, 'host_logs': au_hostlogs})
xixuan1bbfaba2016-10-13 17:53:22 -07001151
xixuan52c2fba2016-05-20 17:02:48 -07001152 @cherrypy.expose
Dan Shi2f136862016-02-11 15:38:38 -08001153 def locate_file(self, **kwargs):
1154 """Get the path to the given file name.
1155
1156 This method looks up the given file name inside specified build artifacts.
1157 One use case is to help caller to locate an apk file inside a build
1158 artifact. The location of the apk file could be different based on the
1159 branch and target.
1160
1161 Args:
1162 file_name: Name of the file to look for.
1163 artifacts: A list of artifact names to search for the file.
1164
1165 Returns:
1166 Path to the file with the given name. It's relative to the folder for the
1167 build, e.g., DATA/priv-app/sl4a/sl4a.apk
Dan Shi2f136862016-02-11 15:38:38 -08001168 """
1169 dl, _ = _get_downloader_and_factory(kwargs)
1170 try:
Joe Brennan1691f8e2017-03-15 15:53:36 -07001171 file_name = kwargs['file_name']
Dan Shi2f136862016-02-11 15:38:38 -08001172 artifacts = kwargs['artifacts']
1173 except KeyError:
1174 raise DevServerError('`file_name` and `artifacts` are required to search '
1175 'for a file in build artifacts.')
1176 build_path = dl.GetBuildDir()
1177 for artifact in artifacts:
1178 # Get the unzipped folder of the artifact. If it's not defined in
1179 # ARTIFACT_UNZIP_FOLDER_MAP, assume the files are unzipped to the build
1180 # directory directly.
1181 folder = artifact_info.ARTIFACT_UNZIP_FOLDER_MAP.get(artifact, '')
1182 artifact_path = os.path.join(build_path, folder)
1183 for root, _, filenames in os.walk(artifact_path):
Joe Brennan1691f8e2017-03-15 15:53:36 -07001184 if file_name in set([f for f in filenames]):
Dan Shi2f136862016-02-11 15:38:38 -08001185 return os.path.relpath(os.path.join(root, file_name), build_path)
1186 raise DevServerError('File `%s` can not be found in artifacts: %s' %
1187 (file_name, artifacts))
1188
1189 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -08001190 def setup_telemetry(self, **kwargs):
1191 """Extracts and sets up telemetry
1192
1193 This method goes through the telemetry deps packages, and stages them on
1194 the devserver to be used by the drones and the telemetry tests.
1195
1196 Args:
1197 archive_url: Google Storage URL for the build.
1198
1199 Returns:
1200 Path to the source folder for the telemetry codebase once it is staged.
1201 """
Gabe Black3b567202015-09-23 14:07:59 -07001202 dl = _get_downloader(kwargs)
Simran Basi4baad082013-02-14 13:39:18 -08001203
Gabe Black3b567202015-09-23 14:07:59 -07001204 build_path = dl.GetBuildDir()
Simran Basi4baad082013-02-14 13:39:18 -08001205 deps_path = os.path.join(build_path, 'autotest/packages')
1206 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
1207 src_folder = os.path.join(telemetry_path, 'src')
1208
1209 with self._telemetry_lock_dict.lock(telemetry_path):
1210 if os.path.exists(src_folder):
1211 # Telemetry is already fully stage return
1212 return src_folder
1213
1214 common_util.MkDirP(telemetry_path)
1215
1216 # Copy over the required deps tar balls to the telemetry directory.
1217 for dep in TELEMETRY_DEPS:
1218 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -07001219 if not os.path.exists(dep_path):
1220 # This dep does not exist (could be new), do not extract it.
1221 continue
Simran Basi4baad082013-02-14 13:39:18 -08001222 try:
1223 common_util.ExtractTarball(dep_path, telemetry_path)
1224 except common_util.CommonUtilError as e:
1225 shutil.rmtree(telemetry_path)
1226 raise DevServerError(str(e))
1227
1228 # By default all the tarballs extract to test_src but some parts of
1229 # the telemetry code specifically hardcoded to exist inside of 'src'.
1230 test_src = os.path.join(telemetry_path, 'test_src')
1231 try:
1232 shutil.move(test_src, src_folder)
1233 except shutil.Error:
1234 # This can occur if src_folder already exists. Remove and retry move.
1235 shutil.rmtree(src_folder)
Gabe Black3b567202015-09-23 14:07:59 -07001236 raise DevServerError(
1237 'Failure in telemetry setup for build %s. Appears that the '
1238 'test_src to src move failed.' % dl.GetBuild())
Simran Basi4baad082013-02-14 13:39:18 -08001239
1240 return src_folder
1241
1242 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -08001243 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -07001244 """Symbolicates a minidump using pre-downloaded symbols, returns it.
1245
1246 Callers will need to POST to this URL with a body of MIME-type
1247 "multipart/form-data".
1248 The body should include a single argument, 'minidump', containing the
1249 binary-formatted minidump to symbolicate.
1250
Chris Masone816e38c2012-05-02 12:22:36 -07001251 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -08001252 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -07001253 minidump: The binary minidump file to symbolicate.
1254 """
Chris Sosa76e44b92013-01-31 12:11:38 -08001255 # Ensure the symbols have been staged.
Dan Shif08fe492016-10-04 14:39:25 -07001256 # Try debug.tar.xz first, then debug.tgz
1257 for artifact in (artifact_info.SYMBOLS_ONLY, artifact_info.SYMBOLS):
1258 kwargs['artifacts'] = artifact
1259 dl = _get_downloader(kwargs)
1260
1261 try:
1262 if self.stage(**kwargs) == 'Success':
1263 break
1264 except build_artifact.ArtifactDownloadError:
1265 continue
1266 else:
Gabe Black3b567202015-09-23 14:07:59 -07001267 raise DevServerError('Failed to stage symbols for %s' %
1268 dl.DescribeSource())
Chris Sosa76e44b92013-01-31 12:11:38 -08001269
Chris Masone816e38c2012-05-02 12:22:36 -07001270 to_return = ''
1271 with tempfile.NamedTemporaryFile() as local:
1272 while True:
1273 data = minidump.file.read(8192)
1274 if not data:
1275 break
1276 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -08001277
Chris Masone816e38c2012-05-02 12:22:36 -07001278 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -08001279
Gabe Black3b567202015-09-23 14:07:59 -07001280 symbols_directory = os.path.join(dl.GetBuildDir(), 'debug', 'breakpad')
Chris Sosa76e44b92013-01-31 12:11:38 -08001281
xixuanab744382017-04-27 10:41:27 -07001282 # The location of minidump_stackwalk is defined in chromeos-admin.
Chris Sosa76e44b92013-01-31 12:11:38 -08001283 stackwalk = subprocess.Popen(
xixuanab744382017-04-27 10:41:27 -07001284 ['/usr/local/bin/minidump_stackwalk', local.name, symbols_directory],
Chris Sosa76e44b92013-01-31 12:11:38 -08001285 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1286
Chris Masone816e38c2012-05-02 12:22:36 -07001287 to_return, error_text = stackwalk.communicate()
1288 if stackwalk.returncode != 0:
1289 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
1290 error_text, stackwalk.returncode))
1291
1292 return to_return
1293
1294 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -08001295 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 15:31:36 -04001296 """Return a string representing the latest build for a given target.
1297
1298 Args:
1299 target: The build target, typically a combination of the board and the
1300 type of build e.g. x86-mario-release.
1301 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
1302 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-07 18:21:26 -08001303
Scott Zawalski16954532012-03-20 15:31:36 -04001304 Returns:
1305 A string representation of the latest build if one exists, i.e.
1306 R19-1993.0.0-a1-b1480.
1307 An empty string if no latest could be found.
1308 """
Don Garrettf84631a2014-01-07 18:21:26 -08001309 if not kwargs:
Scott Zawalski16954532012-03-20 15:31:36 -04001310 return _PrintDocStringAsHTML(self.latestbuild)
1311
Don Garrettf84631a2014-01-07 18:21:26 -08001312 if 'target' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -07001313 raise common_util.DevServerHTTPError(500, 'Error: target= is required!')
Dan Shi61305df2015-10-26 16:52:35 -07001314
1315 if _is_android_build_request(kwargs):
1316 branch = kwargs.get('branch', None)
1317 target = kwargs.get('target', None)
1318 if not target or not branch:
1319 raise DevServerError(
xixuan52c2fba2016-05-20 17:02:48 -07001320 'Both target and branch must be specified to query for the latest '
1321 'Android build.')
Dan Shi61305df2015-10-26 16:52:35 -07001322 return android_build.BuildAccessor.GetLatestBuildID(target, branch)
1323
Scott Zawalski16954532012-03-20 15:31:36 -04001324 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001325 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-07 18:21:26 -08001326 updater.static_dir, kwargs['target'],
1327 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -07001328 except common_util.CommonUtilError as errmsg:
Chris Sosa4b951602014-04-09 20:26:07 -07001329 raise common_util.DevServerHTTPError(500, str(errmsg))
Scott Zawalski16954532012-03-20 15:31:36 -04001330
1331 @cherrypy.expose
xixuan7efd0002016-04-14 15:34:01 -07001332 def list_suite_controls(self, **kwargs):
1333 """Return a list of contents of all known control files.
1334
1335 Example URL:
1336 To List all control files' content:
1337 http://dev-server/list_suite_controls?suite_name=bvt&
1338 build=daisy_spring-release/R29-4279.0.0
1339
1340 Args:
1341 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
1342 suite_name: List the control files belonging to that suite.
1343
1344 Returns:
Dan Shia1cd6522016-04-18 16:07:21 -07001345 A dictionary of all control files's path to its content for given suite.
xixuan7efd0002016-04-14 15:34:01 -07001346 """
1347 if not kwargs:
1348 return _PrintDocStringAsHTML(self.controlfiles)
1349
1350 if 'build' not in kwargs:
1351 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
1352
1353 if 'suite_name' not in kwargs:
Dan Shia1cd6522016-04-18 16:07:21 -07001354 raise common_util.DevServerHTTPError(500,
1355 'Error: suite_name= is required!')
xixuan7efd0002016-04-14 15:34:01 -07001356
1357 control_file_list = [
1358 line.rstrip() for line in common_util.GetControlFileListForSuite(
1359 updater.static_dir, kwargs['build'],
1360 kwargs['suite_name']).splitlines()]
1361
Dan Shia1cd6522016-04-18 16:07:21 -07001362 control_file_content_dict = {}
xixuan7efd0002016-04-14 15:34:01 -07001363 for control_path in control_file_list:
Dan Shia1cd6522016-04-18 16:07:21 -07001364 control_file_content_dict[control_path] = (common_util.GetControlFile(
xixuan7efd0002016-04-14 15:34:01 -07001365 updater.static_dir, kwargs['build'], control_path))
1366
Dan Shia1cd6522016-04-18 16:07:21 -07001367 return json.dumps(control_file_content_dict)
xixuan7efd0002016-04-14 15:34:01 -07001368
1369 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -08001370 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 17:17:28 -05001371 """Return a control file or a list of all known control files.
1372
1373 Example URL:
1374 To List all control files:
beepsbd337242013-07-09 22:44:06 -07001375 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
1376 To List all control files for, say, the bvt suite:
1377 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -05001378 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -05001379 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 -05001380
1381 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -05001382 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -05001383 control_path: If you want the contents of a control file set this
1384 to the path. E.g. client/site_tests/sleeptest/control
1385 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -07001386 suite_name: If control_path is not specified but a suite_name is
1387 specified, list the control files belonging to that suite instead of
1388 all control files. The empty string for suite_name will list all control
1389 files for the build.
Don Garrettf84631a2014-01-07 18:21:26 -08001390
Scott Zawalski4647ce62012-01-03 17:17:28 -05001391 Returns:
1392 Contents of a control file if control_path is provided.
1393 A list of control files if no control_path is provided.
1394 """
Don Garrettf84631a2014-01-07 18:21:26 -08001395 if not kwargs:
Scott Zawalski4647ce62012-01-03 17:17:28 -05001396 return _PrintDocStringAsHTML(self.controlfiles)
1397
Don Garrettf84631a2014-01-07 18:21:26 -08001398 if 'build' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -07001399 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -05001400
Don Garrettf84631a2014-01-07 18:21:26 -08001401 if 'control_path' not in kwargs:
1402 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-09 22:44:06 -07001403 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-07 18:21:26 -08001404 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-09 22:44:06 -07001405 else:
1406 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-07 18:21:26 -08001407 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -05001408 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001409 return common_util.GetControlFile(
Don Garrettf84631a2014-01-07 18:21:26 -08001410 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -08001411
1412 @cherrypy.expose
Simran Basi99e63c02014-05-20 10:39:52 -07001413 def xbuddy_translate(self, *args, **kwargs):
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001414 """Translates an xBuddy path to a real path to artifact if it exists.
1415
1416 Args:
Simran Basi99e63c02014-05-20 10:39:52 -07001417 args: An xbuddy path in the form of {local|remote}/build_id/artifact.
1418 Local searches the devserver's static directory. Remote searches a
1419 Google Storage image archive.
1420
1421 Kwargs:
1422 image_dir: Google Storage image archive to search in if requesting a
1423 remote artifact. If none uses the default bucket.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001424
1425 Returns:
Simran Basi99e63c02014-05-20 10:39:52 -07001426 String in the format of build_id/artifact as stored on the local server
1427 or in Google Storage.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001428 """
Simran Basi99e63c02014-05-20 10:39:52 -07001429 build_id, filename = self._xbuddy.Translate(
Gabe Black3b567202015-09-23 14:07:59 -07001430 args, image_dir=kwargs.get('image_dir'))
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001431 response = os.path.join(build_id, filename)
1432 _Log('Path translation requested, returning: %s', response)
1433 return response
1434
1435 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -07001436 def xbuddy(self, *args, **kwargs):
1437 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -07001438
1439 Args:
joycheneaf4cfc2013-07-02 08:38:57 -07001440 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -07001441 components of the path. The path can be understood as
1442 "{local|remote}/build_id/artifact" where build_id is composed of
1443 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -07001444
joychen121fc9b2013-08-02 14:30:30 -07001445 The first path element is optional, and can be "remote" or "local"
1446 If local (the default), devserver will not attempt to access Google
1447 Storage, and will only search the static directory for the files.
1448 If remote, devserver will try to obtain the artifact off GS if it's
1449 not found locally.
1450 The board is the familiar board name, optionally suffixed.
1451 The version can be the google storage version number, and may also be
1452 any of a number of xBuddy defined version aliases that will be
1453 translated into the latest built image that fits the description.
1454 Defaults to latest.
1455 The artifact is one of a number of image or artifact aliases used by
1456 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -07001457
1458 Kwargs:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001459 for_update: {true|false}
1460 if true, pregenerates the update payloads for the image,
1461 and returns the update uri to pass to the
1462 update_engine_client.
joychen3cb228e2013-06-12 12:13:13 -07001463 return_dir: {true|false}
1464 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001465 relative_path: {true|false}
1466 if set to true, returns the relative path to the payload
1467 directory from static_dir.
joychen3cb228e2013-06-12 12:13:13 -07001468 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -07001469 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -07001470 or
joycheneaf4cfc2013-07-02 08:38:57 -07001471 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -07001472
1473 Returns:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001474 If |for_update|, returns a redirect to the image or update file
1475 on the devserver. E.g.,
1476 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
1477 chromium-test-image.bin
1478 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
1479 http://host:port/static/x86-generic-release/R26-4000.0.0/
1480 If |relative_path| is true, return a relative path the folder where the
1481 payloads are. E.g.,
1482 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 12:13:13 -07001483 """
Chris Sosa75490802013-09-30 17:21:45 -07001484 boolean_string = kwargs.get('for_update')
1485 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001486 boolean_string = kwargs.get('return_dir')
1487 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
1488 boolean_string = kwargs.get('relative_path')
1489 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -07001490
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001491 if return_dir and relative_path:
Chris Sosa4b951602014-04-09 20:26:07 -07001492 raise common_util.DevServerHTTPError(
1493 500, 'Cannot specify both return_dir and relative_path')
Chris Sosa75490802013-09-30 17:21:45 -07001494
1495 # For updates, we optimize downloading of test images.
1496 file_name = None
1497 build_id = None
1498 if for_update:
1499 try:
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001500 build_id = self._xbuddy.StageTestArtifactsForUpdate(args)
Chris Sosa75490802013-09-30 17:21:45 -07001501 except build_artifact.ArtifactDownloadError:
1502 build_id = None
1503
1504 if not build_id:
1505 build_id, file_name = self._xbuddy.Get(args)
1506
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001507 if for_update:
1508 _Log('Payload generation triggered by request')
1509 # Forces payload to be in cache and symlinked into build_id dir.
Chris Sosa75490802013-09-30 17:21:45 -07001510 updater.GetUpdateForLabel(autoupdate.FORCED_UPDATE, build_id,
1511 image_name=file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001512
1513 response = None
1514 if return_dir:
1515 response = os.path.join(cherrypy.request.base, 'static', build_id)
1516 _Log('Directory requested, returning: %s', response)
1517 elif relative_path:
1518 response = build_id
1519 _Log('Relative path requested, returning: %s', response)
1520 elif for_update:
1521 response = os.path.join(cherrypy.request.base, 'update', build_id)
1522 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 12:13:13 -07001523 else:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001524 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 14:30:30 -07001525 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001526 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 14:30:30 -07001527 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -07001528
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001529 return response
1530
joychen3cb228e2013-06-12 12:13:13 -07001531 @cherrypy.expose
1532 def xbuddy_list(self):
1533 """Lists the currently available images & time since last access.
1534
Gilad Arnold452fd272014-02-04 11:09:28 -08001535 Returns:
1536 A string representation of a list of tuples [(build_id, time since last
1537 access),...]
joychen3cb228e2013-06-12 12:13:13 -07001538 """
1539 return self._xbuddy.List()
1540
1541 @cherrypy.expose
1542 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 11:09:28 -08001543 """Returns the number of images cached by xBuddy."""
joychen3cb228e2013-06-12 12:13:13 -07001544 return self._xbuddy.Capacity()
1545
1546 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001547 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001548 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001549 return ('Welcome to the Dev Server!<br>\n'
1550 '<br>\n'
1551 'Here are the available methods, click for documentation:<br>\n'
1552 '<br>\n'
1553 '%s' %
1554 '<br>\n'.join(
1555 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001556 for name in _FindExposedMethods(
1557 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001558
1559 @cherrypy.expose
1560 def doc(self, *args):
1561 """Shows the documentation for available methods / URLs.
1562
1563 Example:
1564 http://myhost/doc/update
1565 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001566 name = '/'.join(args)
1567 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001568 if not method:
1569 raise DevServerError("No exposed method named `%s'" % name)
1570 if not method.__doc__:
1571 raise DevServerError("No documentation for exposed method `%s'" % name)
1572 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -07001573
Dale Curtisc9aaf3a2011-08-09 15:47:40 -07001574 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001575 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001576 """Handles an update check from a Chrome OS client.
1577
1578 The HTTP request should contain the standard Omaha-style XML blob. The URL
1579 line may contain an additional intermediate path to the update payload.
1580
joychen121fc9b2013-08-02 14:30:30 -07001581 This request can be handled in one of 4 ways, depending on the devsever
1582 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -07001583
joychen121fc9b2013-08-02 14:30:30 -07001584 1. No intermediate path
1585 If no intermediate path is given, the default behavior is to generate an
1586 update payload from the latest test image locally built for the board
1587 specified in the xml. Devserver serves the generated payload.
1588
1589 2. Path explicitly invokes XBuddy
1590 If there is a path given, it can explicitly invoke xbuddy by prefixing it
1591 with 'xbuddy'. This path is then used to acquire an image binary for the
1592 devserver to generate an update payload from. Devserver then serves this
1593 payload.
1594
1595 3. Path is left for the devserver to interpret.
1596 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
1597 to generate a payload from the test image in that directory and serve it.
1598
1599 4. The devserver is in a 'forced' mode. TO BE DEPRECATED
1600 This comes from the usage of --forced_payload or --image when starting the
1601 devserver. No matter what path (or no path) gets passed in, devserver will
1602 serve the update payload (--forced_payload) or generate an update payload
1603 from the image (--image).
1604
1605 Examples:
1606 1. No intermediate path
1607 update_engine_client --omaha_url=http://myhost/update
1608 This generates an update payload from the latest test image locally built
1609 for the board specified in the xml.
1610
1611 2. Explicitly invoke xbuddy
1612 update_engine_client --omaha_url=
1613 http://myhost/update/xbuddy/remote/board/version/dev
1614 This would go to GS to download the dev image for the board, from which
1615 the devserver would generate a payload to serve.
1616
1617 3. Give a path for devserver to interpret
1618 update_engine_client --omaha_url=http://myhost/update/some/random/path
1619 This would attempt, in order to:
1620 a) Generate an update from a test image binary if found in
1621 static_dir/some/random/path.
1622 b) Serve an update payload found in static_dir/some/random/path.
1623 c) Hope that some/random/path takes the form "board/version" and
1624 and attempt to download an update payload for that board/version
1625 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001626 """
joychen121fc9b2013-08-02 14:30:30 -07001627 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -08001628 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -07001629 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-11 19:49:01 -07001630
joychen121fc9b2013-08-02 14:30:30 -07001631 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001632
Dan Shiafd0e492015-05-27 14:23:51 -07001633 @require_psutil()
1634 def _get_io_stats(self):
1635 """Get the IO stats as a dictionary.
1636
Gabe Black3b567202015-09-23 14:07:59 -07001637 Returns:
1638 A dictionary of IO stats collected by psutil.
Dan Shiafd0e492015-05-27 14:23:51 -07001639 """
1640 return {'disk_read_bytes_per_second': self.disk_read_bytes_per_sec,
1641 'disk_write_bytes_per_second': self.disk_write_bytes_per_sec,
1642 'disk_total_bytes_per_second': (self.disk_read_bytes_per_sec +
1643 self.disk_write_bytes_per_sec),
1644 'network_sent_bytes_per_second': self.network_sent_bytes_per_sec,
1645 'network_recv_bytes_per_second': self.network_recv_bytes_per_sec,
1646 'network_total_bytes_per_second': (self.network_sent_bytes_per_sec +
1647 self.network_recv_bytes_per_sec),
1648 'cpu_percent': psutil.cpu_percent(),}
1649
Dan Shi7247f9c2016-06-01 09:19:09 -07001650
1651 def _get_process_count(self, process_cmd_pattern):
1652 """Get the count of processes that match the given command pattern.
1653
1654 Args:
1655 process_cmd_pattern: The regex pattern of process command to match.
1656
1657 Returns:
1658 The count of processes that match the given command pattern.
1659 """
1660 try:
xixuanac89ce82016-11-30 16:48:20 -08001661 # Use Popen instead of check_output since the latter cannot run with old
1662 # python version (less than 2.7)
1663 proc = subprocess.Popen(
1664 'pgrep -fc "%s"' % process_cmd_pattern,
1665 stdout=subprocess.PIPE,
1666 stderr=subprocess.PIPE,
1667 shell=True)
1668 cmd_output, cmd_error = proc.communicate()
1669 if cmd_error:
1670 _Log('Error happened when getting process count: %s' % cmd_error)
1671
1672 return int(cmd_output)
Dan Shi7247f9c2016-06-01 09:19:09 -07001673 except subprocess.CalledProcessError:
1674 return 0
1675
1676
Dan Shif5ce2de2013-04-25 16:06:32 -07001677 @cherrypy.expose
1678 def check_health(self):
1679 """Collect the health status of devserver to see if it's ready for staging.
1680
Gilad Arnold452fd272014-02-04 11:09:28 -08001681 Returns:
1682 A JSON dictionary containing all or some of the following fields:
1683 free_disk (int): free disk space in GB
1684 staging_thread_count (int): number of devserver threads currently staging
1685 an image
Dan Shi7247f9c2016-06-01 09:19:09 -07001686 apache_client_count (int): count of Apache processes.
1687 telemetry_test_count (int): count of telemetry tests.
1688 gsutil_count (int): count of gsutil processes.
Dan Shif5ce2de2013-04-25 16:06:32 -07001689 """
1690 # Get free disk space.
1691 stat = os.statvfs(updater.static_dir)
1692 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
Dan Shi7247f9c2016-06-01 09:19:09 -07001693 apache_client_count = self._get_process_count('apache')
1694 telemetry_test_count = self._get_process_count('python.*telemetry')
1695 gsutil_count = self._get_process_count('gsutil')
xixuan447ad9d2017-02-28 14:46:20 -08001696 au_process_count = len(cros_update_progress.GetAllRunningAUProcess())
Dan Shif5ce2de2013-04-25 16:06:32 -07001697
Dan Shiafd0e492015-05-27 14:23:51 -07001698 health_data = {
Dan Shif5ce2de2013-04-25 16:06:32 -07001699 'free_disk': free_disk,
Dan Shid76e6bb2016-01-28 22:28:51 -08001700 'staging_thread_count': DevServerRoot._staging_thread_count,
1701 'apache_client_count': apache_client_count,
Dan Shi7247f9c2016-06-01 09:19:09 -07001702 'telemetry_test_count': telemetry_test_count,
xixuan447ad9d2017-02-28 14:46:20 -08001703 'gsutil_count': gsutil_count,
1704 'au_process_count': au_process_count,
1705 }
Dan Shiafd0e492015-05-27 14:23:51 -07001706 health_data.update(self._get_io_stats() or {})
1707
1708 return json.dumps(health_data)
Dan Shif5ce2de2013-04-25 16:06:32 -07001709
1710
Chris Sosadbc20082012-12-10 13:39:11 -08001711def _CleanCache(cache_dir, wipe):
1712 """Wipes any excess cached items in the cache_dir.
1713
1714 Args:
1715 cache_dir: the directory we are wiping from.
1716 wipe: If True, wipe all the contents -- not just the excess.
1717 """
1718 if wipe:
1719 # Clear the cache and exit on error.
1720 cmd = 'rm -rf %s/*' % cache_dir
1721 if os.system(cmd) != 0:
1722 _Log('Failed to clear the cache with %s' % cmd)
1723 sys.exit(1)
1724 else:
1725 # Clear all but the last N cached updates
1726 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
1727 (cache_dir, CACHED_ENTRIES))
1728 if os.system(cmd) != 0:
1729 _Log('Failed to clean up old delta cache files with %s' % cmd)
1730 sys.exit(1)
1731
1732
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001733def _AddTestingOptions(parser):
1734 group = optparse.OptionGroup(
1735 parser, 'Advanced Testing Options', 'These are used by test scripts and '
1736 'developers writing integration tests utilizing the devserver. They are '
1737 'not intended to be really used outside the scope of someone '
1738 'knowledgable about the test.')
1739 group.add_option('--exit',
1740 action='store_true',
1741 help='do not start the server (yet pregenerate/clear cache)')
1742 group.add_option('--host_log',
1743 action='store_true', default=False,
1744 help='record history of host update events (/api/hostlog)')
1745 group.add_option('--max_updates',
Gabe Black3b567202015-09-23 14:07:59 -07001746 metavar='NUM', default=-1, type='int',
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001747 help='maximum number of update checks handled positively '
1748 '(default: unlimited)')
1749 group.add_option('--private_key',
1750 metavar='PATH', default=None,
1751 help='path to the private key in pem format. If this is set '
1752 'the devserver will generate update payloads that are '
1753 'signed with this key.')
David Zeuthen52ccd012013-10-31 12:58:26 -07001754 group.add_option('--private_key_for_metadata_hash_signature',
1755 metavar='PATH', default=None,
1756 help='path to the private key in pem format. If this is set '
1757 'the devserver will sign the metadata hash with the given '
1758 'key and transmit in the Omaha-style XML response.')
1759 group.add_option('--public_key',
1760 metavar='PATH', default=None,
1761 help='path to the public key in pem format. If this is set '
1762 'the devserver will transmit a base64 encoded version of '
1763 'the content in the Omaha-style XML response.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001764 group.add_option('--proxy_port',
1765 metavar='PORT', default=None, type='int',
1766 help='port to have the client connect to -- basically the '
1767 'devserver lies to the update to tell it to get the payload '
1768 'from a different port that will proxy the request back to '
1769 'the devserver. The proxy must be managed outside the '
1770 'devserver.')
1771 group.add_option('--remote_payload',
1772 action='store_true', default=False,
Chris Sosa4b951602014-04-09 20:26:07 -07001773 help='Payload is being served from a remote machine. With '
1774 'this setting enabled, this devserver instance serves as '
1775 'just an Omaha server instance. In this mode, the '
1776 'devserver enforces a few extra components of the Omaha '
Chris Sosafc715442014-04-09 20:45:23 -07001777 'protocol, such as hardware class, being sent.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001778 group.add_option('-u', '--urlbase',
1779 metavar='URL',
Gabe Black3b567202015-09-23 14:07:59 -07001780 help='base URL for update images, other than the '
1781 'devserver. Use in conjunction with remote_payload.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001782 parser.add_option_group(group)
1783
1784
1785def _AddUpdateOptions(parser):
1786 group = optparse.OptionGroup(
1787 parser, 'Autoupdate Options', 'These options can be used to change '
1788 'how the devserver either generates or serve update payloads. Please '
1789 'note that all of these option affect how a payload is generated and so '
1790 'do not work in archive-only mode.')
1791 group.add_option('--board',
1792 help='By default the devserver will create an update '
1793 'payload from the latest image built for the board '
1794 'a device that is requesting an update has. When we '
1795 'pre-generate an update (see below) and we do not specify '
1796 'another update_type option like image or payload, the '
1797 'devserver needs to know the board to generate the latest '
1798 'image for. This is that board.')
1799 group.add_option('--critical_update',
1800 action='store_true', default=False,
1801 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001802 group.add_option('--image',
1803 metavar='FILE',
1804 help='Generate and serve an update using this image to any '
1805 'device that requests an update.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001806 group.add_option('--payload',
1807 metavar='PATH',
1808 help='use the update payload from specified directory '
1809 '(update.gz).')
1810 group.add_option('-p', '--pregenerate_update',
1811 action='store_true', default=False,
1812 help='pre-generate the update payload before accepting '
1813 'update requests. Useful to help debug payload generation '
1814 'issues quickly. Also if an update payload will take a '
1815 'long time to generate, a client may timeout if you do not'
1816 'pregenerate the update.')
1817 group.add_option('--src_image',
1818 metavar='PATH', default='',
1819 help='If specified, delta updates will be generated using '
1820 'this image as the source image. Delta updates are when '
1821 'you are updating from a "source image" to a another '
1822 'image.')
1823 parser.add_option_group(group)
1824
1825
1826def _AddProductionOptions(parser):
1827 group = optparse.OptionGroup(
1828 parser, 'Advanced Server Options', 'These options can be used to changed '
1829 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001830 group.add_option('--clear_cache',
1831 action='store_true', default=False,
1832 help='At startup, removes all cached entries from the'
1833 'devserver\'s cache.')
1834 group.add_option('--logfile',
1835 metavar='PATH',
1836 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -07001837 group.add_option('--pidfile',
1838 metavar='PATH',
1839 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 11:04:13 -08001840 group.add_option('--portfile',
1841 metavar='PATH',
1842 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001843 group.add_option('--production',
1844 action='store_true', default=False,
1845 help='have the devserver use production values when '
1846 'starting up. This includes using more threads and '
1847 'performing less logging.')
1848 parser.add_option_group(group)
1849
1850
Paul Hobbsef4e0702016-06-27 17:01:42 -07001851def MakeLogHandler(logfile):
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001852 """Create a LogHandler instance used to log all messages."""
1853 hdlr_cls = handlers.TimedRotatingFileHandler
1854 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
xixuan3d48bff2017-01-30 19:00:09 -08001855 interval=_LOG_ROTATION_INTERVAL,
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001856 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -07001857 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001858 return hdlr
1859
1860
Chris Sosacde6bf42012-05-31 18:36:39 -07001861def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001862 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -08001863 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -07001864
1865 # get directory that the devserver is run from
1866 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -07001867 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -07001868 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001869 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -07001870 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -07001871 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001872 parser.add_option('--port',
1873 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 13:13:28 -08001874 help=('port for the dev server to use; if zero, binds to '
1875 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001876 parser.add_option('-t', '--test_image',
1877 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -07001878 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -07001879 parser.add_option('-x', '--xbuddy_manage_builds',
1880 action='store_true',
1881 default=False,
1882 help='If set, allow xbuddy to manage images in'
1883 'build/images.')
Dan Shi72b16132015-10-08 12:10:33 -07001884 parser.add_option('-a', '--android_build_credential',
1885 default=None,
1886 help='Path to a json file which contains the credential '
1887 'needed to access Android builds.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001888 _AddProductionOptions(parser)
1889 _AddUpdateOptions(parser)
1890 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -07001891 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001892
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001893 # Handle options that must be set globally in cherrypy. Do this
1894 # work up front, because calls to _Log() below depend on this
1895 # initialization.
1896 if options.production:
1897 cherrypy.config.update({'environment': 'production'})
1898 if not options.logfile:
1899 cherrypy.config.update({'log.screen': True})
1900 else:
1901 cherrypy.config.update({'log.error_file': '',
1902 'log.access_file': ''})
Paul Hobbsef4e0702016-06-27 17:01:42 -07001903 hdlr = MakeLogHandler(options.logfile)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001904 # Pylint can't seem to process these two calls properly
1905 # pylint: disable=E1101
1906 cherrypy.log.access_log.addHandler(hdlr)
1907 cherrypy.log.error_log.addHandler(hdlr)
1908 # pylint: enable=E1101
1909
joychened64b222013-06-21 16:39:34 -07001910 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001911 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001912
joychened64b222013-06-21 16:39:34 -07001913 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001914 # If our devserver is only supposed to serve payloads, we shouldn't be
1915 # mucking with the cache at all. If the devserver hadn't previously
1916 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001917 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001918 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001919 else:
1920 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001921
Chris Sosadbc20082012-12-10 13:39:11 -08001922 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 16:39:34 -07001923 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001924
joychen121fc9b2013-08-02 14:30:30 -07001925 _xbuddy = xbuddy.XBuddy(options.xbuddy_manage_builds,
1926 options.board,
joychen121fc9b2013-08-02 14:30:30 -07001927 static_dir=options.static_dir)
Chris Sosa75490802013-09-30 17:21:45 -07001928 if options.clear_cache and options.xbuddy_manage_builds:
1929 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 14:30:30 -07001930
Chris Sosa6a3697f2013-01-29 16:44:43 -08001931 # We allow global use here to share with cherrypy classes.
1932 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001933 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001934 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001935 _xbuddy,
joychened64b222013-06-21 16:39:34 -07001936 static_dir=options.static_dir,
Andrew de los Reyes52620802010-04-12 13:40:07 -07001937 urlbase=options.urlbase,
Chris Sosa5d342a22010-09-28 16:54:41 -07001938 forced_image=options.image,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001939 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001940 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-28 23:42:37 -07001941 src_image=options.src_image,
Chris Sosa08d55a22011-01-19 16:08:02 -08001942 board=options.board,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001943 copy_to_static_root=not options.exit,
1944 private_key=options.private_key,
Gabe Black3b567202015-09-23 14:07:59 -07001945 private_key_for_metadata_hash_signature=(
1946 options.private_key_for_metadata_hash_signature),
David Zeuthen52ccd012013-10-31 12:58:26 -07001947 public_key=options.public_key,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001948 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001949 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001950 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001951 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001952 )
Chris Sosa7c931362010-10-11 19:49:01 -07001953
Chris Sosa6a3697f2013-01-29 16:44:43 -08001954 if options.pregenerate_update:
1955 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 15:46:22 -07001956
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001957 if options.exit:
1958 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001959
joychen3cb228e2013-06-12 12:13:13 -07001960 dev_server = DevServerRoot(_xbuddy)
1961
Gilad Arnold11fbef42014-02-10 11:04:13 -08001962 # Patch CherryPy to support binding to any available port (--port=0).
1963 cherrypy_ext.ZeroPortPatcher.DoPatch(cherrypy)
1964
Chris Sosa855b8932013-08-21 13:24:55 -07001965 if options.pidfile:
1966 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1967
Gilad Arnold11fbef42014-02-10 11:04:13 -08001968 if options.portfile:
1969 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1970
Dan Shiafd5c6c2016-01-07 10:27:03 -08001971 if (options.android_build_credential and
1972 os.path.exists(options.android_build_credential)):
1973 try:
1974 with open(options.android_build_credential) as f:
1975 android_build.BuildAccessor.credential_info = json.load(f)
1976 except ValueError as e:
1977 _Log('Failed to load the android build credential: %s. Error: %s.' %
1978 (options.android_build_credential, e))
joychen3cb228e2013-06-12 12:13:13 -07001979 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001980
1981
1982if __name__ == '__main__':
1983 main()