blob: 73cd6f6c9fba0e6a2e44762064f68ae88e32a671 [file] [log] [blame]
Gabe Black3b567202015-09-23 14:07:59 -07001#!/usr/bin/python2
Chris Sosa7c931362010-10-11 19:49:01 -07002
Chris Sosa781ba6d2012-04-11 12:44:43 -07003# Copyright (c) 2009-2012 The Chromium OS Authors. All rights reserved.
rtc@google.comded22402009-10-26 22:36:21 +00004# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
Chris Sosa3ae4dc12013-03-29 11:47:00 -07007"""Chromium OS development server that can be used for all forms of update.
8
9This devserver can be used to perform system-wide autoupdate and update
10of specific portage packages on devices running Chromium OS derived operating
11systems. It mainly operates in two modes:
12
131) archive mode: In this mode, the devserver is configured to stage and
14serve artifacts from Google Storage using the credentials provided to it before
15it is run. The easiest way to understand this is that the devserver is
16functioning as a local cache for artifacts produced and uploaded by build
17servers. Users of this form of devserver can either download the artifacts
18from the devservers static directory OR use the update RPC to perform a
19system-wide autoupdate. Archive mode is always active.
20
212) artifact-generation mode: in this mode, the devserver will attempt to
22generate update payloads and build artifacts when requested. This mode only
23works in the Chromium OS chroot as it uses build tools only present in the
24chroot (emerge, cros_generate_update_payload, etc.). By default, when a device
25requests an update from this form of devserver, the devserver will attempt to
26discover if a more recent build of the board has been built by the developer
27and generate a payload that the requested system can autoupdate to. In addition,
28it accepts gmerge requests from devices that will stage the newest version of
joychen84d13772013-08-06 09:17:23 -070029a particular package from a developer's chroot onto a requesting device.
Chris Sosa3ae4dc12013-03-29 11:47:00 -070030
31For example:
32gmerge gmerge -d <devserver_url>
33
34devserver will see if a newer package of gmerge is available. If gmerge is
35cros_work'd on, it will re-build gmerge. After this, gmerge will install that
36version of gmerge that the devserver just created/found.
37
38For autoupdates, there are many more advanced options that can help specify
39how to update and which payload to give to a requester.
40"""
41
Gabe Black3b567202015-09-23 14:07:59 -070042from __future__ import print_function
Chris Sosa7c931362010-10-11 19:49:01 -070043
Gilad Arnold55a2a372012-10-02 09:46:32 -070044import json
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070045import optparse
rtc@google.comded22402009-10-26 22:36:21 +000046import os
Scott Zawalski4647ce62012-01-03 17:17:28 -050047import re
Simran Basi4baad082013-02-14 13:39:18 -080048import shutil
xixuan52c2fba2016-05-20 17:02:48 -070049import signal
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -080050import socket
Chris Masone816e38c2012-05-02 12:22:36 -070051import subprocess
J. Richard Barnette3d977b82013-04-23 11:05:19 -070052import sys
Chris Masone816e38c2012-05-02 12:22:36 -070053import tempfile
Dan Shi59ae7092013-06-04 14:37:27 -070054import threading
Dan Shiafd0e492015-05-27 14:23:51 -070055import time
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -070056import types
J. Richard Barnette3d977b82013-04-23 11:05:19 -070057from logging import handlers
58
59import cherrypy
Chris Sosa855b8932013-08-21 13:24:55 -070060from cherrypy import _cplogging as cplogging
61from cherrypy.process import plugins
rtc@google.comded22402009-10-26 22:36:21 +000062
Chris Sosa0356d3b2010-09-16 15:46:22 -070063import autoupdate
Dan Shi2f136862016-02-11 15:38:38 -080064import artifact_info
Chris Sosa75490802013-09-30 17:21:45 -070065import build_artifact
Gilad Arnold11fbef42014-02-10 11:04:13 -080066import cherrypy_ext
Gilad Arnoldc65330c2012-09-20 15:17:48 -070067import common_util
Simran Basief83d6a2014-08-28 14:32:01 -070068import devserver_constants
Chris Sosa47a7d4e2012-03-28 11:26:55 -070069import downloader
Chris Sosa7cd23202013-10-15 17:22:57 -070070import gsutil_util
Gilad Arnoldc65330c2012-09-20 15:17:48 -070071import log_util
joychen3cb228e2013-06-12 12:13:13 -070072import xbuddy
Gilad Arnoldc65330c2012-09-20 15:17:48 -070073
Gilad Arnoldc65330c2012-09-20 15:17:48 -070074# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080075def _Log(message, *args):
76 return log_util.LogWithTag('DEVSERVER', message, *args)
Chris Sosa0356d3b2010-09-16 15:46:22 -070077
Dan Shiafd0e492015-05-27 14:23:51 -070078try:
79 import psutil
80except ImportError:
81 # Ignore psutil import failure. This is for backwards compatibility, so
82 # "cros flash" can still update duts with build without psutil installed.
83 # The reason is that, during cros flash, local devserver code is copied over
84 # to DUT, and devserver will be running inside DUT to stage the build.
85 _Log('Python module psutil is not installed, devserver load data will not be '
86 'collected')
87 psutil = None
Dan Shi94dcbe82015-06-08 20:51:13 -070088except OSError as e:
89 # Ignore error like following. psutil may not work properly in builder. Ignore
90 # the error as load information of devserver is not used in builder.
91 # OSError: [Errno 2] No such file or directory: '/dev/pts/0'
92 _Log('psutil is failed to be imported, error: %s. devserver load data will '
93 'not be collected.', e)
94 psutil = None
95
xixuanac89ce82016-11-30 16:48:20 -080096# Use try-except to skip unneccesary import for simple use case, eg. running
97# devserver on host.
98try:
99 import cros_update
xixuanac89ce82016-11-30 16:48:20 -0800100except ImportError as e:
101 _Log('cros_update cannot be imported: %r', e)
102 cros_update = None
xixuana4f4e712017-05-08 15:17:54 -0700103
104try:
105 import cros_update_progress
106except ImportError as e:
107 _Log('cros_update_progress cannot be imported: %r', e)
xixuanac89ce82016-11-30 16:48:20 -0800108 cros_update_progress = None
109
110# only import setup_chromite before chromite import.
111import setup_chromite # pylint: disable=unused-import
112try:
113 from chromite.lib.paygen import gspaths
114except ImportError as e:
115 _Log('chromite cannot be imported: %r', e)
116 gspaths = None
117
Dan Shi72b16132015-10-08 12:10:33 -0700118try:
119 import android_build
120except ImportError as e:
121 # Ignore android_build import failure. This is to support devserver running
122 # inside a ChromeOS device triggered by cros flash. Most ChromeOS test images
123 # do not have google-api-python-client module and they don't need to support
124 # Android updating, therefore, ignore the import failure here.
Dan Shi72b16132015-10-08 12:10:33 -0700125 android_build = None
Frank Farzan40160872011-12-12 18:39:18 -0800126
Chris Sosa417e55d2011-01-25 16:40:48 -0800127CACHED_ENTRIES = 12
Don Garrettf90edf02010-11-16 17:36:14 -0800128
Simran Basi4baad082013-02-14 13:39:18 -0800129TELEMETRY_FOLDER = 'telemetry_src'
130TELEMETRY_DEPS = ['dep-telemetry_dep.tar.bz2',
131 'dep-page_cycler_dep.tar.bz2',
Simran Basi0d078682013-03-22 16:40:04 -0700132 'dep-chrome_test.tar.bz2',
133 'dep-perf_data_dep.tar.bz2']
Simran Basi4baad082013-02-14 13:39:18 -0800134
Chris Sosa0356d3b2010-09-16 15:46:22 -0700135# Sets up global to share between classes.
rtc@google.com21a5ca32009-11-04 18:23:23 +0000136updater = None
rtc@google.comded22402009-10-26 22:36:21 +0000137
xixuan3d48bff2017-01-30 19:00:09 -0800138# Log rotation parameters. These settings correspond to twice a day once
139# devserver is started, with about two weeks (28 backup files) of old logs
140# kept for backup.
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700141#
xixuan3d48bff2017-01-30 19:00:09 -0800142# For more, see the documentation in standard python library for
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700143# logging.handlers.TimedRotatingFileHandler
xixuan3d48bff2017-01-30 19:00:09 -0800144_LOG_ROTATION_TIME = 'H'
145_LOG_ROTATION_INTERVAL = 12 # hours
146_LOG_ROTATION_BACKUP = 28 # backup counts
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700147
Dan Shiafd0e492015-05-27 14:23:51 -0700148# Number of seconds between the collection of disk and network IO counters.
149STATS_INTERVAL = 10.0
Frank Farzan40160872011-12-12 18:39:18 -0800150
xixuan52c2fba2016-05-20 17:02:48 -0700151# Auto-update parameters
152
153# Error msg for missing key in CrOS auto-update.
154KEY_ERROR_MSG = 'Key Error in cmd %s: %s= is required'
155
156# Command of running auto-update.
157AUTO_UPDATE_CMD = '/usr/bin/python -u %s -d %s -b %s --static_dir %s'
158
159
Chris Sosa9164ca32012-03-28 11:04:50 -0700160class DevServerError(Exception):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700161 """Exception class used by this module."""
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700162
163
Dan Shiafd0e492015-05-27 14:23:51 -0700164def require_psutil():
Gabe Black3b567202015-09-23 14:07:59 -0700165 """Decorator for functions require psutil to run."""
Dan Shiafd0e492015-05-27 14:23:51 -0700166 def deco_require_psutil(func):
167 """Wrapper of the decorator function.
168
Gabe Black3b567202015-09-23 14:07:59 -0700169 Args:
170 func: function to be called.
Dan Shiafd0e492015-05-27 14:23:51 -0700171 """
172 def func_require_psutil(*args, **kwargs):
173 """Decorator for functions require psutil to run.
174
175 If psutil is not installed, skip calling the function.
176
Gabe Black3b567202015-09-23 14:07:59 -0700177 Args:
178 *args: arguments for function to be called.
179 **kwargs: keyword arguments for function to be called.
Dan Shiafd0e492015-05-27 14:23:51 -0700180 """
181 if psutil:
182 return func(*args, **kwargs)
183 else:
184 _Log('Python module psutil is not installed. Function call %s is '
185 'skipped.' % func)
186 return func_require_psutil
187 return deco_require_psutil
188
189
Gabe Black3b567202015-09-23 14:07:59 -0700190def _canonicalize_archive_url(archive_url):
191 """Canonicalizes archive_url strings.
192
193 Raises:
194 DevserverError: if archive_url is not set.
195 """
196 if archive_url:
197 if not archive_url.startswith('gs://'):
198 raise DevServerError("Archive URL isn't from Google Storage (%s) ." %
199 archive_url)
200
201 return archive_url.rstrip('/')
202 else:
203 raise DevServerError("Must specify an archive_url in the request")
204
205
206def _canonicalize_local_path(local_path):
207 """Canonicalizes |local_path| strings.
208
209 Raises:
210 DevserverError: if |local_path| is not set.
211 """
212 # Restrict staging of local content to only files within the static
213 # directory.
214 local_path = os.path.abspath(local_path)
215 if not local_path.startswith(updater.static_dir):
216 raise DevServerError('Local path %s must be a subdirectory of the static'
217 ' directory: %s' % (local_path, updater.static_dir))
218
219 return local_path.rstrip('/')
220
221
222def _get_artifacts(kwargs):
223 """Returns a tuple of named and file artifacts given the stage rpc kwargs.
224
225 Raises:
226 DevserverError if no artifacts would be returned.
227 """
228 artifacts = kwargs.get('artifacts')
229 files = kwargs.get('files')
230 if not artifacts and not files:
231 raise DevServerError('No artifacts specified.')
232
233 # Note we NEED to coerce files to a string as we get raw unicode from
234 # cherrypy and we treat files as strings elsewhere in the code.
235 return (str(artifacts).split(',') if artifacts else [],
236 str(files).split(',') if files else [])
237
238
Dan Shi61305df2015-10-26 16:52:35 -0700239def _is_android_build_request(kwargs):
240 """Check if a devserver call is for Android build, based on the arguments.
241
242 This method exams the request's arguments (os_type) to determine if the
243 request is for Android build. If os_type is set to `android`, returns True.
244 If os_type is not set or has other values, returns False.
245
246 Args:
247 kwargs: Keyword arguments for the request.
248
249 Returns:
250 True if the request is for Android build. False otherwise.
251 """
252 os_type = kwargs.get('os_type', None)
253 return os_type == 'android'
254
255
Gabe Black3b567202015-09-23 14:07:59 -0700256def _get_downloader(kwargs):
257 """Returns the downloader based on passed in arguments.
258
259 Args:
260 kwargs: Keyword arguments for the request.
261 """
262 local_path = kwargs.get('local_path')
263 if local_path:
264 local_path = _canonicalize_local_path(local_path)
265
266 dl = None
267 if local_path:
268 dl = downloader.LocalDownloader(updater.static_dir, local_path)
269
Dan Shi61305df2015-10-26 16:52:35 -0700270 if not _is_android_build_request(kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700271 archive_url = kwargs.get('archive_url')
272 if not archive_url and not local_path:
273 raise DevServerError('Requires archive_url or local_path to be '
274 'specified.')
275 if archive_url and local_path:
276 raise DevServerError('archive_url and local_path can not both be '
277 'specified.')
278 if not dl:
279 archive_url = _canonicalize_archive_url(archive_url)
280 dl = downloader.GoogleStorageDownloader(updater.static_dir, archive_url)
281 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 = '&nbsp;' * 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
Gabe Black3b567202015-09-23 14:07:59 -0700391 base_config = {'global':
392 {'server.log_request_headers': True,
393 'server.protocol_version': 'HTTP/1.1',
394 'server.socket_host': socket_host,
395 'server.socket_port': int(options.port),
396 'response.timeout': 6000,
397 'request.show_tracebacks': True,
398 'server.socket_timeout': 60,
399 'server.thread_pool': 2,
400 'engine.autoreload.on': False,
401 },
402 '/api':
403 {
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 {
412 # Gets rid of cherrypy parsing post file for args.
413 'request.process_request_body': False,
414 'response.timeout': 10000,
415 },
416 # Sets up the static dir for file hosting.
417 '/static':
418 {'tools.staticdir.dir': options.static_dir,
419 'tools.staticdir.on': True,
420 'response.timeout': 10000,
421 'tools.update_timestamp.on': True,
422 },
423 }
Chris Sosa5f118ef2012-07-12 11:37:50 -0700424 if options.production:
Alex Miller93beca52013-07-30 19:25:09 -0700425 base_config['global'].update({'server.thread_pool': 150})
Chris Sosa7cd23202013-10-15 17:22:57 -0700426 # TODO(sosa): Do this more cleanly.
427 gsutil_util.GSUTIL_ATTEMPTS = 5
Scott Zawalski1c5e7cd2012-02-27 13:12:52 -0500428
Chris Sosa7c931362010-10-11 19:49:01 -0700429 return base_config
rtc@google.com64244662009-11-12 00:52:08 +0000430
Darin Petkove17164a2010-08-11 13:24:41 -0700431
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700432def _GetRecursiveMemberObject(root, member_list):
433 """Returns an object corresponding to a nested member list.
434
435 Args:
436 root: the root object to search
437 member_list: list of nested members to search
Don Garrettf84631a2014-01-07 18:21:26 -0800438
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700439 Returns:
440 An object corresponding to the member name list; None otherwise.
441 """
442 for member in member_list:
443 next_root = root.__class__.__dict__.get(member)
444 if not next_root:
445 return None
446 root = next_root
447 return root
448
449
450def _IsExposed(name):
451 """Returns True iff |name| has an `exposed' attribute and it is set."""
452 return hasattr(name, 'exposed') and name.exposed
453
454
Gilad Arnold748c8322012-10-12 09:51:35 -0700455def _GetExposedMethod(root, nested_member, ignored=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700456 """Returns a CherryPy-exposed method, if such exists.
457
458 Args:
459 root: the root object for searching
460 nested_member: a slash-joined path to the nested member
461 ignored: method paths to be ignored
Don Garrettf84631a2014-01-07 18:21:26 -0800462
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700463 Returns:
464 A function object corresponding to the path defined by |member_list| from
465 the |root| object, if the function is exposed and not ignored; None
466 otherwise.
467 """
Gilad Arnold748c8322012-10-12 09:51:35 -0700468 method = (not (ignored and nested_member in ignored) and
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700469 _GetRecursiveMemberObject(root, nested_member.split('/')))
Gabe Black3b567202015-09-23 14:07:59 -0700470 if method and type(method) == types.FunctionType and _IsExposed(method):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700471 return method
472
473
Gilad Arnold748c8322012-10-12 09:51:35 -0700474def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700475 """Finds exposed CherryPy methods.
476
477 Args:
478 root: the root object for searching
479 prefix: slash-joined chain of members leading to current object
480 unlisted: URLs to be excluded regardless of their exposed status
Don Garrettf84631a2014-01-07 18:21:26 -0800481
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700482 Returns:
483 List of exposed URLs that are not unlisted.
484 """
485 method_list = []
486 for member in sorted(root.__class__.__dict__.keys()):
487 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 09:51:35 -0700488 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700489 continue
490 member_obj = root.__class__.__dict__[member]
491 if _IsExposed(member_obj):
492 if type(member_obj) == types.FunctionType:
493 method_list.append(prefixed_member)
494 else:
495 method_list += _FindExposedMethods(
496 member_obj, prefixed_member, unlisted)
497 return method_list
498
499
xixuan52c2fba2016-05-20 17:02:48 -0700500def _check_base_args_for_auto_update(kwargs):
xixuanac89ce82016-11-30 16:48:20 -0800501 """Check basic args required for auto-update.
502
503 Args:
504 kwargs: the parameters to be checked.
505
506 Raises:
507 DevServerHTTPError if required parameters don't exist in kwargs.
508 """
xixuan52c2fba2016-05-20 17:02:48 -0700509 if 'host_name' not in kwargs:
510 raise common_util.DevServerHTTPError(KEY_ERROR_MSG % 'host_name')
511
512 if 'build_name' not in kwargs:
513 raise common_util.DevServerHTTPError(KEY_ERROR_MSG % 'build_name')
514
515
516def _parse_boolean_arg(kwargs, key):
xixuanac89ce82016-11-30 16:48:20 -0800517 """Parse boolean arg from kwargs.
518
519 Args:
520 kwargs: the parameters to be checked.
521 key: the key to be parsed.
522
523 Returns:
524 The boolean value of kwargs[key], or False if key doesn't exist in kwargs.
525
526 Raises:
527 DevServerHTTPError if kwargs[key] is not a boolean variable.
528 """
xixuan52c2fba2016-05-20 17:02:48 -0700529 if key in kwargs:
530 if kwargs[key] == 'True':
531 return True
532 elif kwargs[key] == 'False':
533 return False
534 else:
535 raise common_util.DevServerHTTPError(
536 'The value for key %s is not boolean.' % key)
537 else:
538 return False
539
xixuan447ad9d2017-02-28 14:46:20 -0800540
xixuanac89ce82016-11-30 16:48:20 -0800541def _parse_string_arg(kwargs, key):
542 """Parse string arg from kwargs.
543
544 Args:
545 kwargs: the parameters to be checked.
546 key: the key to be parsed.
547
548 Returns:
549 The string value of kwargs[key], or None if key doesn't exist in kwargs.
550 """
551 if key in kwargs:
552 return kwargs[key]
553 else:
554 return None
555
xixuan447ad9d2017-02-28 14:46:20 -0800556
xixuanac89ce82016-11-30 16:48:20 -0800557def _build_uri_from_build_name(build_name):
558 """Get build url from a given build name.
559
560 Args:
561 build_name: the build name to be parsed, whose format is
562 'board/release_version'.
563
564 Returns:
565 The release_archive_url on Google Storage for this build name.
566 """
567 return gspaths.ChromeosReleases.BuildUri(
568 cros_update.STABLE_BUILD_CHANNEL, build_name.split('/')[0],
569 build_name.split('/')[1])
xixuan52c2fba2016-05-20 17:02:48 -0700570
xixuan447ad9d2017-02-28 14:46:20 -0800571
572def _clear_process(host_name, pid):
573 """Clear AU process for given hostname and pid.
574
575 This clear includes:
576 1. kill process if it's alive.
577 2. delete the track status file of this process.
578 3. delete the executing log file of this process.
579
580 Args:
581 host_name: the host to execute auto-update.
582 pid: the background auto-update process id.
583 """
584 if cros_update_progress.IsProcessAlive(pid):
585 os.killpg(int(pid), signal.SIGKILL)
586
587 cros_update_progress.DelTrackStatusFile(host_name, pid)
588 cros_update_progress.DelExecuteLogFile(host_name, pid)
589
590
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700591class ApiRoot(object):
592 """RESTful API for Dev Server information."""
593 exposed = True
594
595 @cherrypy.expose
596 def hostinfo(self, ip):
597 """Returns a JSON dictionary containing information about the given ip.
598
Gilad Arnold1b908392012-10-05 11:36:27 -0700599 Args:
600 ip: address of host whose info is requested
Don Garrettf84631a2014-01-07 18:21:26 -0800601
Gilad Arnold1b908392012-10-05 11:36:27 -0700602 Returns:
603 A JSON dictionary containing all or some of the following fields:
604 last_event_type (int): last update event type received
605 last_event_status (int): last update event status received
606 last_known_version (string): last known version reported in update ping
607 forced_update_label (string): update label to force next update ping to
608 use, set by setnextupdate
609 See the OmahaEvent class in update_engine/omaha_request_action.h for
610 event type and status code definitions. If the ip does not exist an empty
611 string is returned.
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700612
Gilad Arnold1b908392012-10-05 11:36:27 -0700613 Example URL:
614 http://myhost/api/hostinfo?ip=192.168.1.5
615 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700616 return updater.HandleHostInfoPing(ip)
617
618 @cherrypy.expose
Gilad Arnold286a0062012-01-12 13:47:02 -0800619 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 11:36:27 -0700620 """Returns a JSON object containing a log of host event.
621
622 Args:
623 ip: address of host whose event log is requested, or `all'
Don Garrettf84631a2014-01-07 18:21:26 -0800624
Gilad Arnold1b908392012-10-05 11:36:27 -0700625 Returns:
626 A JSON encoded list (log) of dictionaries (events), each of which
627 containing a `timestamp' and other event fields, as described under
628 /api/hostinfo.
629
630 Example URL:
631 http://myhost/api/hostlog?ip=192.168.1.5
632 """
Gilad Arnold286a0062012-01-12 13:47:02 -0800633 return updater.HandleHostLogPing(ip)
634
635 @cherrypy.expose
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700636 def setnextupdate(self, ip):
637 """Allows the response to the next update ping from a host to be set.
638
639 Takes the IP of the host and an update label as normally provided to the
Gilad Arnold1b908392012-10-05 11:36:27 -0700640 /update command.
641 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700642 body_length = int(cherrypy.request.headers['Content-Length'])
643 label = cherrypy.request.rfile.read(body_length)
644
645 if label:
646 label = label.strip()
647 if label:
648 return updater.HandleSetUpdatePing(ip, label)
Chris Sosa4b951602014-04-09 20:26:07 -0700649 raise common_util.DevServerHTTPError(400, 'No label provided.')
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700650
651
Gilad Arnold55a2a372012-10-02 09:46:32 -0700652 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800653 def fileinfo(self, *args):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700654 """Returns information about a given staged file.
655
656 Args:
Don Garrettf84631a2014-01-07 18:21:26 -0800657 args: path to the file inside the server's static staging directory
658
Gilad Arnold55a2a372012-10-02 09:46:32 -0700659 Returns:
660 A JSON encoded dictionary with information about the said file, which may
661 contain the following keys/values:
Gilad Arnold1b908392012-10-05 11:36:27 -0700662 size (int): the file size in bytes
663 sha1 (string): a base64 encoded SHA1 hash
664 sha256 (string): a base64 encoded SHA256 hash
665
666 Example URL:
667 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 09:46:32 -0700668 """
Don Garrettf84631a2014-01-07 18:21:26 -0800669 file_path = os.path.join(updater.static_dir, *args)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700670 if not os.path.exists(file_path):
671 raise DevServerError('file not found: %s' % file_path)
672 try:
673 file_size = os.path.getsize(file_path)
674 file_sha1 = common_util.GetFileSha1(file_path)
675 file_sha256 = common_util.GetFileSha256(file_path)
676 except os.error, e:
677 raise DevServerError('failed to get info for file %s: %s' %
Gilad Arnolde74b3812013-04-22 11:27:38 -0700678 (file_path, e))
679
680 is_delta = autoupdate.Autoupdate.IsDeltaFormatFile(file_path)
681
682 return json.dumps({
683 autoupdate.Autoupdate.SIZE_ATTR: file_size,
684 autoupdate.Autoupdate.SHA1_ATTR: file_sha1,
685 autoupdate.Autoupdate.SHA256_ATTR: file_sha256,
686 autoupdate.Autoupdate.ISDELTA_ATTR: is_delta
687 })
Gilad Arnold55a2a372012-10-02 09:46:32 -0700688
Chris Sosa76e44b92013-01-31 12:11:38 -0800689
David Rochberg7c79a812011-01-19 14:24:45 -0500690class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700691 """The Root Class for the Dev Server.
692
693 CherryPy works as follows:
694 For each method in this class, cherrpy interprets root/path
695 as a call to an instance of DevServerRoot->method_name. For example,
696 a call to http://myhost/build will call build. CherryPy automatically
697 parses http args and places them as keyword arguments in each method.
698 For paths http://myhost/update/dir1/dir2, you can use *args so that
699 cherrypy uses the update method and puts the extra paths in args.
700 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700701 # Method names that should not be listed on the index page.
702 _UNLISTED_METHODS = ['index', 'doc']
703
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700704 api = ApiRoot()
Chris Sosa7c931362010-10-11 19:49:01 -0700705
Dan Shi59ae7092013-06-04 14:37:27 -0700706 # Number of threads that devserver is staging images.
707 _staging_thread_count = 0
708 # Lock used to lock increasing/decreasing count.
709 _staging_thread_count_lock = threading.Lock()
710
Dan Shiafd0e492015-05-27 14:23:51 -0700711 @require_psutil()
712 def _refresh_io_stats(self):
713 """A call running in a thread to update IO stats periodically."""
714 prev_disk_io_counters = psutil.disk_io_counters()
715 prev_network_io_counters = psutil.net_io_counters()
716 prev_read_time = time.time()
717 while True:
718 time.sleep(STATS_INTERVAL)
719 now = time.time()
720 interval = now - prev_read_time
721 prev_read_time = now
722 # Disk IO is for all disks.
723 disk_io_counters = psutil.disk_io_counters()
724 network_io_counters = psutil.net_io_counters()
725
726 self.disk_read_bytes_per_sec = (
727 disk_io_counters.read_bytes -
728 prev_disk_io_counters.read_bytes)/interval
729 self.disk_write_bytes_per_sec = (
730 disk_io_counters.write_bytes -
731 prev_disk_io_counters.write_bytes)/interval
732 prev_disk_io_counters = disk_io_counters
733
734 self.network_sent_bytes_per_sec = (
735 network_io_counters.bytes_sent -
736 prev_network_io_counters.bytes_sent)/interval
737 self.network_recv_bytes_per_sec = (
738 network_io_counters.bytes_recv -
739 prev_network_io_counters.bytes_recv)/interval
740 prev_network_io_counters = network_io_counters
741
742 @require_psutil()
743 def _start_io_stat_thread(self):
Gabe Black3b567202015-09-23 14:07:59 -0700744 """Start the thread to collect IO stats."""
Dan Shiafd0e492015-05-27 14:23:51 -0700745 thread = threading.Thread(target=self._refresh_io_stats)
746 thread.daemon = True
747 thread.start()
748
joychen3cb228e2013-06-12 12:13:13 -0700749 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700750 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800751 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 12:13:13 -0700752 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 14:24:45 -0500753
Dan Shiafd0e492015-05-27 14:23:51 -0700754 # Cache of disk IO stats, a thread refresh the stats every 10 seconds.
755 # lock is not used for these variables as the only thread writes to these
756 # variables is _refresh_io_stats.
757 self.disk_read_bytes_per_sec = 0
758 self.disk_write_bytes_per_sec = 0
759 # Cache of network IO stats.
760 self.network_sent_bytes_per_sec = 0
761 self.network_recv_bytes_per_sec = 0
762 self._start_io_stat_thread()
763
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700764 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500765 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700766 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700767 import builder
768 if self._builder is None:
769 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500770 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700771
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700772 @cherrypy.expose
Dan Shif8eb0d12013-08-01 17:52:06 -0700773 def is_staged(self, **kwargs):
774 """Check if artifacts have been downloaded.
775
Chris Sosa6b0c6172013-08-05 17:01:33 -0700776 async: True to return without waiting for download to complete.
777 artifacts: Comma separated list of named artifacts to download.
778 These are defined in artifact_info and have their implementation
779 in build_artifact.py.
780 files: Comma separated list of file artifacts to stage. These
781 will be available as is in the corresponding static directory with no
782 custom post-processing.
783
784 returns: True of all artifacts are staged.
Dan Shif8eb0d12013-08-01 17:52:06 -0700785
786 Example:
787 To check if autotest and test_suites are staged:
788 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
789 artifacts=autotest,test_suites
790 """
Gabe Black3b567202015-09-23 14:07:59 -0700791 dl, factory = _get_downloader_and_factory(kwargs)
Aviv Keshet57d18172016-06-18 20:39:09 -0700792 response = str(dl.IsStaged(factory))
793 _Log('Responding to is_staged %s request with %r', kwargs, response)
794 return response
Dan Shi59ae7092013-06-04 14:37:27 -0700795
Chris Sosa76e44b92013-01-31 12:11:38 -0800796 @cherrypy.expose
Prashanth Ba06d2d22014-03-07 15:35:19 -0800797 def list_image_dir(self, **kwargs):
798 """Take an archive url and list the contents in its staged directory.
799
800 Args:
801 kwargs:
802 archive_url: Google Storage URL for the build.
803
804 Example:
805 To list the contents of where this devserver should have staged
806 gs://image-archive/<board>-release/<build> call:
807 http://devserver_url:<port>/list_image_dir?archive_url=<gs://..>
808
809 Returns:
810 A string with information about the contents of the image directory.
811 """
Gabe Black3b567202015-09-23 14:07:59 -0700812 dl = _get_downloader(kwargs)
Prashanth Ba06d2d22014-03-07 15:35:19 -0800813 try:
Gabe Black3b567202015-09-23 14:07:59 -0700814 image_dir_contents = dl.ListBuildDir()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800815 except build_artifact.ArtifactDownloadError as e:
816 return 'Cannot list the contents of staged artifacts. %s' % e
817 if not image_dir_contents:
Gabe Black3b567202015-09-23 14:07:59 -0700818 return '%s has not been staged on this devserver.' % dl.DescribeSource()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800819 return image_dir_contents
820
821 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800822 def stage(self, **kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700823 """Downloads and caches build artifacts.
Chris Sosa76e44b92013-01-31 12:11:38 -0800824
Gabe Black3b567202015-09-23 14:07:59 -0700825 Downloads and caches build artifacts, possibly from a Google Storage URL,
Dan Shi72b16132015-10-08 12:10:33 -0700826 or from Android's build server. Returns once these have been downloaded
Gabe Black3b567202015-09-23 14:07:59 -0700827 on the devserver. A call to this will attempt to cache non-specified
828 artifacts in the background for the given from the given URL following
829 the principle of spatial locality. Spatial locality of different
Chris Sosa76e44b92013-01-31 12:11:38 -0800830 artifacts is explicitly defined in the build_artifact module.
831
832 These artifacts will then be available from the static/ sub-directory of
833 the devserver.
834
835 Args:
836 archive_url: Google Storage URL for the build.
Simran Basi4243a862014-12-12 12:48:33 -0800837 local_path: Local path for the build.
Dan Shif8eb0d12013-08-01 17:52:06 -0700838 async: True to return without waiting for download to complete.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700839 artifacts: Comma separated list of named artifacts to download.
840 These are defined in artifact_info and have their implementation
841 in build_artifact.py.
842 files: Comma separated list of files to stage. These
843 will be available as is in the corresponding static directory with no
844 custom post-processing.
Laurence Goodbyf5c958d2016-01-14 18:23:56 -0800845 clean: True to remove any previously staged artifacts first.
Chris Sosa76e44b92013-01-31 12:11:38 -0800846
847 Example:
848 To download the autotest and test suites tarballs:
849 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
850 artifacts=autotest,test_suites
851 To download the full update payload:
852 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
853 artifacts=full_payload
Chris Sosa6b0c6172013-08-05 17:01:33 -0700854 To download just a file called blah.bin:
855 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
856 files=blah.bin
Chris Sosa76e44b92013-01-31 12:11:38 -0800857
858 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 16:39:34 -0700859 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 12:11:38 -0800860
861 Note for this example, relative path is the archive_url stripped of its
862 basename i.e. path/ in the examples above. Specific example:
863
864 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
865
866 Will get staged to:
867
joychened64b222013-06-21 16:39:34 -0700868 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Chris Sosa76e44b92013-01-31 12:11:38 -0800869 """
Gabe Black3b567202015-09-23 14:07:59 -0700870 dl, factory = _get_downloader_and_factory(kwargs)
871
Dan Shi59ae7092013-06-04 14:37:27 -0700872 with DevServerRoot._staging_thread_count_lock:
873 DevServerRoot._staging_thread_count += 1
874 try:
Laurence Goodbyf5c958d2016-01-14 18:23:56 -0800875 boolean_string = kwargs.get('clean')
876 clean = xbuddy.XBuddy.ParseBoolean(boolean_string)
877 if clean and os.path.exists(dl.GetBuildDir()):
878 _Log('Removing %s' % dl.GetBuildDir())
879 shutil.rmtree(dl.GetBuildDir())
Gabe Black3b567202015-09-23 14:07:59 -0700880 async = kwargs.get('async', False)
881 dl.Download(factory, async=async)
Dan Shi59ae7092013-06-04 14:37:27 -0700882 finally:
883 with DevServerRoot._staging_thread_count_lock:
884 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800885 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700886
887 @cherrypy.expose
xixuan52c2fba2016-05-20 17:02:48 -0700888 def cros_au(self, **kwargs):
889 """Auto-update a CrOS DUT.
890
891 Args:
892 kwargs:
893 host_name: the hostname of the DUT to auto-update.
894 build_name: the build name for update the DUT.
895 force_update: Force an update even if the version installed is the
896 same. Default: False.
897 full_update: If True, do not run stateful update, directly force a full
898 reimage. If False, try stateful update first if the dut is already
899 installed with the same version.
900 async: Whether the auto_update function is ran in the background.
901
902 Returns:
903 A tuple includes two elements:
904 a boolean variable represents whether the auto-update process is
905 successfully started.
906 an integer represents the background auto-update process id.
907 """
908 _check_base_args_for_auto_update(kwargs)
909
910 host_name = kwargs['host_name']
911 build_name = kwargs['build_name']
912 force_update = _parse_boolean_arg(kwargs, 'force_update')
913 full_update = _parse_boolean_arg(kwargs, 'full_update')
914 async = _parse_boolean_arg(kwargs, 'async')
xixuanac89ce82016-11-30 16:48:20 -0800915 original_build = _parse_string_arg(kwargs, 'original_build')
David Haddock90e49442017-04-07 19:14:09 -0700916 payload_filename = _parse_string_arg(kwargs, 'payload_filename')
David Haddock20559612017-06-28 22:15:08 -0700917 clobber_stateful = _parse_boolean_arg(kwargs, 'clobber_stateful')
xixuan52c2fba2016-05-20 17:02:48 -0700918
919 if async:
920 path = os.path.dirname(os.path.abspath(__file__))
921 execute_file = os.path.join(path, 'cros_update.py')
922 args = (AUTO_UPDATE_CMD % (execute_file, host_name, build_name,
923 updater.static_dir))
xixuanac89ce82016-11-30 16:48:20 -0800924
925 # The original_build's format is like: link/3428.210.0
926 # The corresponding release_archive_url's format is like:
927 # gs://chromeos-releases/stable-channel/link/3428.210.0
928 if original_build:
929 release_archive_url = _build_uri_from_build_name(original_build)
930 # First staging the stateful.tgz synchronousely.
931 self.stage(files='stateful.tgz', async=False,
932 archive_url=release_archive_url)
933 args = ('%s --original_build %s' % (args, original_build))
934
xixuan52c2fba2016-05-20 17:02:48 -0700935 if force_update:
936 args = ('%s --force_update' % args)
937
938 if full_update:
939 args = ('%s --full_update' % args)
940
David Haddock90e49442017-04-07 19:14:09 -0700941 if payload_filename:
942 args = ('%s --payload_filename %s' % (args, payload_filename))
943
David Haddock20559612017-06-28 22:15:08 -0700944 if clobber_stateful:
945 args = ('%s --clobber_stateful' % args)
946
xixuan2a0970a2016-08-10 12:12:44 -0700947 p = subprocess.Popen([args], shell=True, preexec_fn=os.setsid)
948 pid = os.getpgid(p.pid)
xixuan52c2fba2016-05-20 17:02:48 -0700949
950 # Pre-write status in the track_status_file before the first call of
951 # 'get_au_status' to make sure that the track_status_file exists.
xixuan2a0970a2016-08-10 12:12:44 -0700952 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -0700953 progress_tracker.WriteStatus('CrOS update is just started.')
954
xixuan2a0970a2016-08-10 12:12:44 -0700955 return json.dumps((True, pid))
xixuan52c2fba2016-05-20 17:02:48 -0700956 else:
957 cros_update_trigger = cros_update.CrOSUpdateTrigger(
xixuanac89ce82016-11-30 16:48:20 -0800958 host_name, build_name, updater.static_dir, force_update=force_update,
959 full_update=full_update, original_build=original_build)
xixuan52c2fba2016-05-20 17:02:48 -0700960 cros_update_trigger.TriggerAU()
xixuan27d50442017-08-09 10:38:25 -0700961 return json.dumps((True, -1))
xixuan52c2fba2016-05-20 17:02:48 -0700962
963 @cherrypy.expose
964 def get_au_status(self, **kwargs):
965 """Check if the auto-update task is finished.
966
967 It handles 4 cases:
968 1. If an error exists in the track_status_file, delete the track file and
969 raise it.
970 2. If cros-update process is finished, delete the file and return the
971 success result.
972 3. If the process is not running, delete the track file and raise an error
973 about 'the process is terminated due to unknown reason'.
974 4. If the track_status_file does not exist, kill the process if it exists,
975 and raise the IOError.
976
977 Args:
978 kwargs:
979 host_name: the hostname of the DUT to auto-update.
980 pid: the background process id of cros-update.
981
982 Returns:
xixuan28d99072016-10-06 12:24:16 -0700983 A dict with three elements:
xixuan52c2fba2016-05-20 17:02:48 -0700984 a boolean variable represents whether the auto-update process is
985 finished.
986 a string represents the current auto-update process status.
987 For example, 'Transfer Devserver/Stateful Update Package'.
xixuan28d99072016-10-06 12:24:16 -0700988 a detailed error message paragraph if there exists an Auto-Update
989 error, in which the last line shows the main exception. Empty
990 string otherwise.
xixuan52c2fba2016-05-20 17:02:48 -0700991 """
992 if 'host_name' not in kwargs:
993 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
994
995 if 'pid' not in kwargs:
996 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
997
998 host_name = kwargs['host_name']
999 pid = kwargs['pid']
1000 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
1001
xixuan28d99072016-10-06 12:24:16 -07001002 result_dict = {'finished': False, 'status': '', 'detailed_error_msg': ''}
xixuan52c2fba2016-05-20 17:02:48 -07001003 try:
1004 result = progress_tracker.ReadStatus()
1005 if result.startswith(cros_update_progress.ERROR_TAG):
xixuan28d99072016-10-06 12:24:16 -07001006 result_dict['detailed_error_msg'] = result[len(
1007 cros_update_progress.ERROR_TAG):]
xixuan28681fd2016-11-23 11:13:56 -08001008 elif result == cros_update_progress.FINISHED:
xixuan28d99072016-10-06 12:24:16 -07001009 result_dict['finished'] = True
1010 result_dict['status'] = result
xixuan28681fd2016-11-23 11:13:56 -08001011 elif not cros_update_progress.IsProcessAlive(pid):
xixuan28d99072016-10-06 12:24:16 -07001012 result_dict['detailed_error_msg'] = (
1013 'Cros_update process terminated midway due to unknown reason. '
1014 'Last update status was %s' % result)
xixuan28681fd2016-11-23 11:13:56 -08001015 else:
1016 result_dict['status'] = result
1017 except IOError as e:
1018 if pid and cros_update_progress.IsProcessAlive(pid):
xixuan2a0970a2016-08-10 12:12:44 -07001019 os.killpg(int(pid), signal.SIGKILL)
xixuan52c2fba2016-05-20 17:02:48 -07001020
xixuan28681fd2016-11-23 11:13:56 -08001021 result_dict['detailed_error_msg'] = str(e)
1022
1023 return json.dumps(result_dict)
xixuan52c2fba2016-05-20 17:02:48 -07001024
1025 @cherrypy.expose
1026 def handler_cleanup(self, **kwargs):
xixuan3bc974e2016-10-18 17:21:43 -07001027 """Clean track status log and temp directory for CrOS auto-update process.
xixuan52c2fba2016-05-20 17:02:48 -07001028
1029 Args:
1030 kwargs:
1031 host_name: the hostname of the DUT to auto-update.
1032 pid: the background process id of cros-update.
1033 """
1034 if 'host_name' not in kwargs:
1035 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
1036
1037 if 'pid' not in kwargs:
1038 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
1039
1040 host_name = kwargs['host_name']
1041 pid = kwargs['pid']
1042 cros_update_progress.DelTrackStatusFile(host_name, pid)
xixuan3bc974e2016-10-18 17:21:43 -07001043 cros_update_progress.DelAUTempDirectory(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -07001044
1045 @cherrypy.expose
1046 def kill_au_proc(self, **kwargs):
1047 """Kill CrOS auto-update process using given process id.
1048
1049 Args:
1050 kwargs:
1051 host_name: Kill all the CrOS auto-update process of this host.
1052
1053 Returns:
1054 True if all processes are killed properly.
1055 """
1056 if 'host_name' not in kwargs:
1057 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
1058
xixuan447ad9d2017-02-28 14:46:20 -08001059 cur_pid = kwargs.get('pid')
1060
xixuan52c2fba2016-05-20 17:02:48 -07001061 host_name = kwargs['host_name']
xixuan3bc974e2016-10-18 17:21:43 -07001062 track_log_list = cros_update_progress.GetAllTrackStatusFileByHostName(
1063 host_name)
xixuan52c2fba2016-05-20 17:02:48 -07001064 for log in track_log_list:
1065 # The track log's full path is: path/host_name_pid.log
1066 # Use splitext to remove file extension, then parse pid from the
1067 # filename.
1068 pid = os.path.splitext(os.path.basename(log))[0][len(host_name)+1:]
xixuan447ad9d2017-02-28 14:46:20 -08001069 _clear_process(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -07001070
xixuan447ad9d2017-02-28 14:46:20 -08001071 if cur_pid:
1072 _clear_process(host_name, cur_pid)
xixuan52c2fba2016-05-20 17:02:48 -07001073
1074 return 'True'
1075
1076 @cherrypy.expose
1077 def collect_cros_au_log(self, **kwargs):
1078 """Collect CrOS auto-update log.
1079
1080 Args:
1081 kwargs:
1082 host_name: the hostname of the DUT to auto-update.
1083 pid: the background process id of cros-update.
1084
1085 Returns:
David Haddock9f459632017-05-11 14:45:46 -07001086 A dictionary containing the execute log file and any hostlog files.
xixuan52c2fba2016-05-20 17:02:48 -07001087 """
1088 if 'host_name' not in kwargs:
1089 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
1090
1091 if 'pid' not in kwargs:
1092 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
1093
1094 host_name = kwargs['host_name']
1095 pid = kwargs['pid']
xixuan3bc974e2016-10-18 17:21:43 -07001096
1097 # Fetch the execute log recorded by cros_update_progress.
xixuan1bbfaba2016-10-13 17:53:22 -07001098 au_log = cros_update_progress.ReadExecuteLogFile(host_name, pid)
1099 cros_update_progress.DelExecuteLogFile(host_name, pid)
David Haddock9f459632017-05-11 14:45:46 -07001100 # Fetch the cros_au host_logs if they exist
1101 au_hostlogs = cros_update_progress.ReadAUHostLogFiles(host_name, pid)
1102 return json.dumps({'cros_au_log': au_log, 'host_logs': au_hostlogs})
xixuan1bbfaba2016-10-13 17:53:22 -07001103
xixuan52c2fba2016-05-20 17:02:48 -07001104 @cherrypy.expose
Dan Shi2f136862016-02-11 15:38:38 -08001105 def locate_file(self, **kwargs):
1106 """Get the path to the given file name.
1107
1108 This method looks up the given file name inside specified build artifacts.
1109 One use case is to help caller to locate an apk file inside a build
1110 artifact. The location of the apk file could be different based on the
1111 branch and target.
1112
1113 Args:
1114 file_name: Name of the file to look for.
1115 artifacts: A list of artifact names to search for the file.
1116
1117 Returns:
1118 Path to the file with the given name. It's relative to the folder for the
1119 build, e.g., DATA/priv-app/sl4a/sl4a.apk
Dan Shi2f136862016-02-11 15:38:38 -08001120 """
1121 dl, _ = _get_downloader_and_factory(kwargs)
1122 try:
Joe Brennan1691f8e2017-03-15 15:53:36 -07001123 file_name = kwargs['file_name']
Dan Shi2f136862016-02-11 15:38:38 -08001124 artifacts = kwargs['artifacts']
1125 except KeyError:
1126 raise DevServerError('`file_name` and `artifacts` are required to search '
1127 'for a file in build artifacts.')
1128 build_path = dl.GetBuildDir()
1129 for artifact in artifacts:
1130 # Get the unzipped folder of the artifact. If it's not defined in
1131 # ARTIFACT_UNZIP_FOLDER_MAP, assume the files are unzipped to the build
1132 # directory directly.
1133 folder = artifact_info.ARTIFACT_UNZIP_FOLDER_MAP.get(artifact, '')
1134 artifact_path = os.path.join(build_path, folder)
1135 for root, _, filenames in os.walk(artifact_path):
Joe Brennan1691f8e2017-03-15 15:53:36 -07001136 if file_name in set([f for f in filenames]):
Dan Shi2f136862016-02-11 15:38:38 -08001137 return os.path.relpath(os.path.join(root, file_name), build_path)
1138 raise DevServerError('File `%s` can not be found in artifacts: %s' %
1139 (file_name, artifacts))
1140
1141 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -08001142 def setup_telemetry(self, **kwargs):
1143 """Extracts and sets up telemetry
1144
1145 This method goes through the telemetry deps packages, and stages them on
1146 the devserver to be used by the drones and the telemetry tests.
1147
1148 Args:
1149 archive_url: Google Storage URL for the build.
1150
1151 Returns:
1152 Path to the source folder for the telemetry codebase once it is staged.
1153 """
Gabe Black3b567202015-09-23 14:07:59 -07001154 dl = _get_downloader(kwargs)
Simran Basi4baad082013-02-14 13:39:18 -08001155
Gabe Black3b567202015-09-23 14:07:59 -07001156 build_path = dl.GetBuildDir()
Simran Basi4baad082013-02-14 13:39:18 -08001157 deps_path = os.path.join(build_path, 'autotest/packages')
1158 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
1159 src_folder = os.path.join(telemetry_path, 'src')
1160
1161 with self._telemetry_lock_dict.lock(telemetry_path):
1162 if os.path.exists(src_folder):
1163 # Telemetry is already fully stage return
1164 return src_folder
1165
1166 common_util.MkDirP(telemetry_path)
1167
1168 # Copy over the required deps tar balls to the telemetry directory.
1169 for dep in TELEMETRY_DEPS:
1170 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -07001171 if not os.path.exists(dep_path):
1172 # This dep does not exist (could be new), do not extract it.
1173 continue
Simran Basi4baad082013-02-14 13:39:18 -08001174 try:
1175 common_util.ExtractTarball(dep_path, telemetry_path)
1176 except common_util.CommonUtilError as e:
1177 shutil.rmtree(telemetry_path)
1178 raise DevServerError(str(e))
1179
1180 # By default all the tarballs extract to test_src but some parts of
1181 # the telemetry code specifically hardcoded to exist inside of 'src'.
1182 test_src = os.path.join(telemetry_path, 'test_src')
1183 try:
1184 shutil.move(test_src, src_folder)
1185 except shutil.Error:
1186 # This can occur if src_folder already exists. Remove and retry move.
1187 shutil.rmtree(src_folder)
Gabe Black3b567202015-09-23 14:07:59 -07001188 raise DevServerError(
1189 'Failure in telemetry setup for build %s. Appears that the '
1190 'test_src to src move failed.' % dl.GetBuild())
Simran Basi4baad082013-02-14 13:39:18 -08001191
1192 return src_folder
1193
1194 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -08001195 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -07001196 """Symbolicates a minidump using pre-downloaded symbols, returns it.
1197
1198 Callers will need to POST to this URL with a body of MIME-type
1199 "multipart/form-data".
1200 The body should include a single argument, 'minidump', containing the
1201 binary-formatted minidump to symbolicate.
1202
Chris Masone816e38c2012-05-02 12:22:36 -07001203 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -08001204 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -07001205 minidump: The binary minidump file to symbolicate.
1206 """
Chris Sosa76e44b92013-01-31 12:11:38 -08001207 # Ensure the symbols have been staged.
Dan Shif08fe492016-10-04 14:39:25 -07001208 # Try debug.tar.xz first, then debug.tgz
1209 for artifact in (artifact_info.SYMBOLS_ONLY, artifact_info.SYMBOLS):
1210 kwargs['artifacts'] = artifact
1211 dl = _get_downloader(kwargs)
1212
1213 try:
1214 if self.stage(**kwargs) == 'Success':
1215 break
1216 except build_artifact.ArtifactDownloadError:
1217 continue
1218 else:
Gabe Black3b567202015-09-23 14:07:59 -07001219 raise DevServerError('Failed to stage symbols for %s' %
1220 dl.DescribeSource())
Chris Sosa76e44b92013-01-31 12:11:38 -08001221
Chris Masone816e38c2012-05-02 12:22:36 -07001222 to_return = ''
1223 with tempfile.NamedTemporaryFile() as local:
1224 while True:
1225 data = minidump.file.read(8192)
1226 if not data:
1227 break
1228 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -08001229
Chris Masone816e38c2012-05-02 12:22:36 -07001230 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -08001231
Gabe Black3b567202015-09-23 14:07:59 -07001232 symbols_directory = os.path.join(dl.GetBuildDir(), 'debug', 'breakpad')
Chris Sosa76e44b92013-01-31 12:11:38 -08001233
xixuanab744382017-04-27 10:41:27 -07001234 # The location of minidump_stackwalk is defined in chromeos-admin.
Chris Sosa76e44b92013-01-31 12:11:38 -08001235 stackwalk = subprocess.Popen(
xixuanab744382017-04-27 10:41:27 -07001236 ['/usr/local/bin/minidump_stackwalk', local.name, symbols_directory],
Chris Sosa76e44b92013-01-31 12:11:38 -08001237 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1238
Chris Masone816e38c2012-05-02 12:22:36 -07001239 to_return, error_text = stackwalk.communicate()
1240 if stackwalk.returncode != 0:
1241 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
1242 error_text, stackwalk.returncode))
1243
1244 return to_return
1245
1246 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -08001247 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 15:31:36 -04001248 """Return a string representing the latest build for a given target.
1249
1250 Args:
1251 target: The build target, typically a combination of the board and the
1252 type of build e.g. x86-mario-release.
1253 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
1254 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-07 18:21:26 -08001255
Scott Zawalski16954532012-03-20 15:31:36 -04001256 Returns:
1257 A string representation of the latest build if one exists, i.e.
1258 R19-1993.0.0-a1-b1480.
1259 An empty string if no latest could be found.
1260 """
Don Garrettf84631a2014-01-07 18:21:26 -08001261 if not kwargs:
Scott Zawalski16954532012-03-20 15:31:36 -04001262 return _PrintDocStringAsHTML(self.latestbuild)
1263
Don Garrettf84631a2014-01-07 18:21:26 -08001264 if 'target' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -07001265 raise common_util.DevServerHTTPError(500, 'Error: target= is required!')
Dan Shi61305df2015-10-26 16:52:35 -07001266
1267 if _is_android_build_request(kwargs):
1268 branch = kwargs.get('branch', None)
1269 target = kwargs.get('target', None)
1270 if not target or not branch:
1271 raise DevServerError(
xixuan52c2fba2016-05-20 17:02:48 -07001272 'Both target and branch must be specified to query for the latest '
1273 'Android build.')
Dan Shi61305df2015-10-26 16:52:35 -07001274 return android_build.BuildAccessor.GetLatestBuildID(target, branch)
1275
Scott Zawalski16954532012-03-20 15:31:36 -04001276 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001277 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-07 18:21:26 -08001278 updater.static_dir, kwargs['target'],
1279 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -07001280 except common_util.CommonUtilError as errmsg:
Chris Sosa4b951602014-04-09 20:26:07 -07001281 raise common_util.DevServerHTTPError(500, str(errmsg))
Scott Zawalski16954532012-03-20 15:31:36 -04001282
1283 @cherrypy.expose
xixuan7efd0002016-04-14 15:34:01 -07001284 def list_suite_controls(self, **kwargs):
1285 """Return a list of contents of all known control files.
1286
1287 Example URL:
1288 To List all control files' content:
1289 http://dev-server/list_suite_controls?suite_name=bvt&
1290 build=daisy_spring-release/R29-4279.0.0
1291
1292 Args:
1293 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
1294 suite_name: List the control files belonging to that suite.
1295
1296 Returns:
Dan Shia1cd6522016-04-18 16:07:21 -07001297 A dictionary of all control files's path to its content for given suite.
xixuan7efd0002016-04-14 15:34:01 -07001298 """
1299 if not kwargs:
1300 return _PrintDocStringAsHTML(self.controlfiles)
1301
1302 if 'build' not in kwargs:
1303 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
1304
1305 if 'suite_name' not in kwargs:
Dan Shia1cd6522016-04-18 16:07:21 -07001306 raise common_util.DevServerHTTPError(500,
1307 'Error: suite_name= is required!')
xixuan7efd0002016-04-14 15:34:01 -07001308
1309 control_file_list = [
1310 line.rstrip() for line in common_util.GetControlFileListForSuite(
1311 updater.static_dir, kwargs['build'],
1312 kwargs['suite_name']).splitlines()]
1313
Dan Shia1cd6522016-04-18 16:07:21 -07001314 control_file_content_dict = {}
xixuan7efd0002016-04-14 15:34:01 -07001315 for control_path in control_file_list:
Dan Shia1cd6522016-04-18 16:07:21 -07001316 control_file_content_dict[control_path] = (common_util.GetControlFile(
xixuan7efd0002016-04-14 15:34:01 -07001317 updater.static_dir, kwargs['build'], control_path))
1318
Dan Shia1cd6522016-04-18 16:07:21 -07001319 return json.dumps(control_file_content_dict)
xixuan7efd0002016-04-14 15:34:01 -07001320
1321 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -08001322 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 17:17:28 -05001323 """Return a control file or a list of all known control files.
1324
1325 Example URL:
1326 To List all control files:
beepsbd337242013-07-09 22:44:06 -07001327 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
1328 To List all control files for, say, the bvt suite:
1329 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -05001330 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -05001331 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 -05001332
1333 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -05001334 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -05001335 control_path: If you want the contents of a control file set this
1336 to the path. E.g. client/site_tests/sleeptest/control
1337 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -07001338 suite_name: If control_path is not specified but a suite_name is
1339 specified, list the control files belonging to that suite instead of
1340 all control files. The empty string for suite_name will list all control
1341 files for the build.
Don Garrettf84631a2014-01-07 18:21:26 -08001342
Scott Zawalski4647ce62012-01-03 17:17:28 -05001343 Returns:
1344 Contents of a control file if control_path is provided.
1345 A list of control files if no control_path is provided.
1346 """
Don Garrettf84631a2014-01-07 18:21:26 -08001347 if not kwargs:
Scott Zawalski4647ce62012-01-03 17:17:28 -05001348 return _PrintDocStringAsHTML(self.controlfiles)
1349
Don Garrettf84631a2014-01-07 18:21:26 -08001350 if 'build' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -07001351 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -05001352
Don Garrettf84631a2014-01-07 18:21:26 -08001353 if 'control_path' not in kwargs:
1354 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-09 22:44:06 -07001355 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-07 18:21:26 -08001356 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-09 22:44:06 -07001357 else:
1358 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-07 18:21:26 -08001359 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -05001360 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001361 return common_util.GetControlFile(
Don Garrettf84631a2014-01-07 18:21:26 -08001362 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -08001363
1364 @cherrypy.expose
Simran Basi99e63c02014-05-20 10:39:52 -07001365 def xbuddy_translate(self, *args, **kwargs):
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001366 """Translates an xBuddy path to a real path to artifact if it exists.
1367
1368 Args:
Simran Basi99e63c02014-05-20 10:39:52 -07001369 args: An xbuddy path in the form of {local|remote}/build_id/artifact.
1370 Local searches the devserver's static directory. Remote searches a
1371 Google Storage image archive.
1372
1373 Kwargs:
1374 image_dir: Google Storage image archive to search in if requesting a
1375 remote artifact. If none uses the default bucket.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001376
1377 Returns:
Simran Basi99e63c02014-05-20 10:39:52 -07001378 String in the format of build_id/artifact as stored on the local server
1379 or in Google Storage.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001380 """
Simran Basi99e63c02014-05-20 10:39:52 -07001381 build_id, filename = self._xbuddy.Translate(
Gabe Black3b567202015-09-23 14:07:59 -07001382 args, image_dir=kwargs.get('image_dir'))
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001383 response = os.path.join(build_id, filename)
1384 _Log('Path translation requested, returning: %s', response)
1385 return response
1386
1387 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -07001388 def xbuddy(self, *args, **kwargs):
1389 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -07001390
1391 Args:
joycheneaf4cfc2013-07-02 08:38:57 -07001392 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -07001393 components of the path. The path can be understood as
1394 "{local|remote}/build_id/artifact" where build_id is composed of
1395 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -07001396
joychen121fc9b2013-08-02 14:30:30 -07001397 The first path element is optional, and can be "remote" or "local"
1398 If local (the default), devserver will not attempt to access Google
1399 Storage, and will only search the static directory for the files.
1400 If remote, devserver will try to obtain the artifact off GS if it's
1401 not found locally.
1402 The board is the familiar board name, optionally suffixed.
1403 The version can be the google storage version number, and may also be
1404 any of a number of xBuddy defined version aliases that will be
1405 translated into the latest built image that fits the description.
1406 Defaults to latest.
1407 The artifact is one of a number of image or artifact aliases used by
1408 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -07001409
1410 Kwargs:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001411 for_update: {true|false}
1412 if true, pregenerates the update payloads for the image,
1413 and returns the update uri to pass to the
1414 update_engine_client.
joychen3cb228e2013-06-12 12:13:13 -07001415 return_dir: {true|false}
1416 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001417 relative_path: {true|false}
1418 if set to true, returns the relative path to the payload
1419 directory from static_dir.
joychen3cb228e2013-06-12 12:13:13 -07001420 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -07001421 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -07001422 or
joycheneaf4cfc2013-07-02 08:38:57 -07001423 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -07001424
1425 Returns:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001426 If |for_update|, returns a redirect to the image or update file
1427 on the devserver. E.g.,
1428 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
1429 chromium-test-image.bin
1430 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
1431 http://host:port/static/x86-generic-release/R26-4000.0.0/
1432 If |relative_path| is true, return a relative path the folder where the
1433 payloads are. E.g.,
1434 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 12:13:13 -07001435 """
Chris Sosa75490802013-09-30 17:21:45 -07001436 boolean_string = kwargs.get('for_update')
1437 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001438 boolean_string = kwargs.get('return_dir')
1439 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
1440 boolean_string = kwargs.get('relative_path')
1441 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -07001442
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001443 if return_dir and relative_path:
Chris Sosa4b951602014-04-09 20:26:07 -07001444 raise common_util.DevServerHTTPError(
1445 500, 'Cannot specify both return_dir and relative_path')
Chris Sosa75490802013-09-30 17:21:45 -07001446
1447 # For updates, we optimize downloading of test images.
1448 file_name = None
1449 build_id = None
1450 if for_update:
1451 try:
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001452 build_id = self._xbuddy.StageTestArtifactsForUpdate(args)
Chris Sosa75490802013-09-30 17:21:45 -07001453 except build_artifact.ArtifactDownloadError:
1454 build_id = None
1455
1456 if not build_id:
1457 build_id, file_name = self._xbuddy.Get(args)
1458
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001459 if for_update:
1460 _Log('Payload generation triggered by request')
1461 # Forces payload to be in cache and symlinked into build_id dir.
Chris Sosa75490802013-09-30 17:21:45 -07001462 updater.GetUpdateForLabel(autoupdate.FORCED_UPDATE, build_id,
1463 image_name=file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001464
1465 response = None
1466 if return_dir:
1467 response = os.path.join(cherrypy.request.base, 'static', build_id)
1468 _Log('Directory requested, returning: %s', response)
1469 elif relative_path:
1470 response = build_id
1471 _Log('Relative path requested, returning: %s', response)
1472 elif for_update:
1473 response = os.path.join(cherrypy.request.base, 'update', build_id)
1474 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 12:13:13 -07001475 else:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001476 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 14:30:30 -07001477 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001478 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 14:30:30 -07001479 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -07001480
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001481 return response
1482
joychen3cb228e2013-06-12 12:13:13 -07001483 @cherrypy.expose
1484 def xbuddy_list(self):
1485 """Lists the currently available images & time since last access.
1486
Gilad Arnold452fd272014-02-04 11:09:28 -08001487 Returns:
1488 A string representation of a list of tuples [(build_id, time since last
1489 access),...]
joychen3cb228e2013-06-12 12:13:13 -07001490 """
1491 return self._xbuddy.List()
1492
1493 @cherrypy.expose
1494 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 11:09:28 -08001495 """Returns the number of images cached by xBuddy."""
joychen3cb228e2013-06-12 12:13:13 -07001496 return self._xbuddy.Capacity()
1497
1498 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001499 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001500 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001501 return ('Welcome to the Dev Server!<br>\n'
1502 '<br>\n'
1503 'Here are the available methods, click for documentation:<br>\n'
1504 '<br>\n'
1505 '%s' %
1506 '<br>\n'.join(
1507 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001508 for name in _FindExposedMethods(
1509 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001510
1511 @cherrypy.expose
1512 def doc(self, *args):
1513 """Shows the documentation for available methods / URLs.
1514
1515 Example:
1516 http://myhost/doc/update
1517 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001518 name = '/'.join(args)
1519 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001520 if not method:
1521 raise DevServerError("No exposed method named `%s'" % name)
1522 if not method.__doc__:
1523 raise DevServerError("No documentation for exposed method `%s'" % name)
1524 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -07001525
Dale Curtisc9aaf3a2011-08-09 15:47:40 -07001526 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001527 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001528 """Handles an update check from a Chrome OS client.
1529
1530 The HTTP request should contain the standard Omaha-style XML blob. The URL
1531 line may contain an additional intermediate path to the update payload.
1532
joychen121fc9b2013-08-02 14:30:30 -07001533 This request can be handled in one of 4 ways, depending on the devsever
1534 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -07001535
joychen121fc9b2013-08-02 14:30:30 -07001536 1. No intermediate path
1537 If no intermediate path is given, the default behavior is to generate an
1538 update payload from the latest test image locally built for the board
1539 specified in the xml. Devserver serves the generated payload.
1540
1541 2. Path explicitly invokes XBuddy
1542 If there is a path given, it can explicitly invoke xbuddy by prefixing it
1543 with 'xbuddy'. This path is then used to acquire an image binary for the
1544 devserver to generate an update payload from. Devserver then serves this
1545 payload.
1546
1547 3. Path is left for the devserver to interpret.
1548 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
1549 to generate a payload from the test image in that directory and serve it.
1550
1551 4. The devserver is in a 'forced' mode. TO BE DEPRECATED
1552 This comes from the usage of --forced_payload or --image when starting the
1553 devserver. No matter what path (or no path) gets passed in, devserver will
1554 serve the update payload (--forced_payload) or generate an update payload
1555 from the image (--image).
1556
1557 Examples:
1558 1. No intermediate path
1559 update_engine_client --omaha_url=http://myhost/update
1560 This generates an update payload from the latest test image locally built
1561 for the board specified in the xml.
1562
1563 2. Explicitly invoke xbuddy
1564 update_engine_client --omaha_url=
1565 http://myhost/update/xbuddy/remote/board/version/dev
1566 This would go to GS to download the dev image for the board, from which
1567 the devserver would generate a payload to serve.
1568
1569 3. Give a path for devserver to interpret
1570 update_engine_client --omaha_url=http://myhost/update/some/random/path
1571 This would attempt, in order to:
1572 a) Generate an update from a test image binary if found in
1573 static_dir/some/random/path.
1574 b) Serve an update payload found in static_dir/some/random/path.
1575 c) Hope that some/random/path takes the form "board/version" and
1576 and attempt to download an update payload for that board/version
1577 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001578 """
joychen121fc9b2013-08-02 14:30:30 -07001579 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -08001580 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -07001581 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-11 19:49:01 -07001582
joychen121fc9b2013-08-02 14:30:30 -07001583 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001584
Dan Shiafd0e492015-05-27 14:23:51 -07001585 @require_psutil()
1586 def _get_io_stats(self):
1587 """Get the IO stats as a dictionary.
1588
Gabe Black3b567202015-09-23 14:07:59 -07001589 Returns:
1590 A dictionary of IO stats collected by psutil.
Dan Shiafd0e492015-05-27 14:23:51 -07001591 """
1592 return {'disk_read_bytes_per_second': self.disk_read_bytes_per_sec,
1593 'disk_write_bytes_per_second': self.disk_write_bytes_per_sec,
1594 'disk_total_bytes_per_second': (self.disk_read_bytes_per_sec +
1595 self.disk_write_bytes_per_sec),
1596 'network_sent_bytes_per_second': self.network_sent_bytes_per_sec,
1597 'network_recv_bytes_per_second': self.network_recv_bytes_per_sec,
1598 'network_total_bytes_per_second': (self.network_sent_bytes_per_sec +
1599 self.network_recv_bytes_per_sec),
1600 'cpu_percent': psutil.cpu_percent(),}
1601
Dan Shi7247f9c2016-06-01 09:19:09 -07001602
1603 def _get_process_count(self, process_cmd_pattern):
1604 """Get the count of processes that match the given command pattern.
1605
1606 Args:
1607 process_cmd_pattern: The regex pattern of process command to match.
1608
1609 Returns:
1610 The count of processes that match the given command pattern.
1611 """
1612 try:
xixuanac89ce82016-11-30 16:48:20 -08001613 # Use Popen instead of check_output since the latter cannot run with old
1614 # python version (less than 2.7)
1615 proc = subprocess.Popen(
1616 'pgrep -fc "%s"' % process_cmd_pattern,
1617 stdout=subprocess.PIPE,
1618 stderr=subprocess.PIPE,
1619 shell=True)
1620 cmd_output, cmd_error = proc.communicate()
1621 if cmd_error:
1622 _Log('Error happened when getting process count: %s' % cmd_error)
1623
1624 return int(cmd_output)
Dan Shi7247f9c2016-06-01 09:19:09 -07001625 except subprocess.CalledProcessError:
1626 return 0
1627
1628
Dan Shif5ce2de2013-04-25 16:06:32 -07001629 @cherrypy.expose
1630 def check_health(self):
1631 """Collect the health status of devserver to see if it's ready for staging.
1632
Gilad Arnold452fd272014-02-04 11:09:28 -08001633 Returns:
1634 A JSON dictionary containing all or some of the following fields:
1635 free_disk (int): free disk space in GB
1636 staging_thread_count (int): number of devserver threads currently staging
1637 an image
Dan Shi7247f9c2016-06-01 09:19:09 -07001638 apache_client_count (int): count of Apache processes.
1639 telemetry_test_count (int): count of telemetry tests.
1640 gsutil_count (int): count of gsutil processes.
Dan Shif5ce2de2013-04-25 16:06:32 -07001641 """
1642 # Get free disk space.
1643 stat = os.statvfs(updater.static_dir)
1644 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
Dan Shi7247f9c2016-06-01 09:19:09 -07001645 apache_client_count = self._get_process_count('apache')
1646 telemetry_test_count = self._get_process_count('python.*telemetry')
1647 gsutil_count = self._get_process_count('gsutil')
xixuan447ad9d2017-02-28 14:46:20 -08001648 au_process_count = len(cros_update_progress.GetAllRunningAUProcess())
Dan Shif5ce2de2013-04-25 16:06:32 -07001649
Dan Shiafd0e492015-05-27 14:23:51 -07001650 health_data = {
Dan Shif5ce2de2013-04-25 16:06:32 -07001651 'free_disk': free_disk,
Dan Shid76e6bb2016-01-28 22:28:51 -08001652 'staging_thread_count': DevServerRoot._staging_thread_count,
1653 'apache_client_count': apache_client_count,
Dan Shi7247f9c2016-06-01 09:19:09 -07001654 'telemetry_test_count': telemetry_test_count,
xixuan447ad9d2017-02-28 14:46:20 -08001655 'gsutil_count': gsutil_count,
1656 'au_process_count': au_process_count,
1657 }
Dan Shiafd0e492015-05-27 14:23:51 -07001658 health_data.update(self._get_io_stats() or {})
1659
1660 return json.dumps(health_data)
Dan Shif5ce2de2013-04-25 16:06:32 -07001661
1662
Chris Sosadbc20082012-12-10 13:39:11 -08001663def _CleanCache(cache_dir, wipe):
1664 """Wipes any excess cached items in the cache_dir.
1665
1666 Args:
1667 cache_dir: the directory we are wiping from.
1668 wipe: If True, wipe all the contents -- not just the excess.
1669 """
1670 if wipe:
1671 # Clear the cache and exit on error.
1672 cmd = 'rm -rf %s/*' % cache_dir
1673 if os.system(cmd) != 0:
1674 _Log('Failed to clear the cache with %s' % cmd)
1675 sys.exit(1)
1676 else:
1677 # Clear all but the last N cached updates
1678 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
1679 (cache_dir, CACHED_ENTRIES))
1680 if os.system(cmd) != 0:
1681 _Log('Failed to clean up old delta cache files with %s' % cmd)
1682 sys.exit(1)
1683
1684
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001685def _AddTestingOptions(parser):
1686 group = optparse.OptionGroup(
1687 parser, 'Advanced Testing Options', 'These are used by test scripts and '
1688 'developers writing integration tests utilizing the devserver. They are '
1689 'not intended to be really used outside the scope of someone '
1690 'knowledgable about the test.')
1691 group.add_option('--exit',
1692 action='store_true',
1693 help='do not start the server (yet pregenerate/clear cache)')
1694 group.add_option('--host_log',
1695 action='store_true', default=False,
1696 help='record history of host update events (/api/hostlog)')
1697 group.add_option('--max_updates',
Gabe Black3b567202015-09-23 14:07:59 -07001698 metavar='NUM', default=-1, type='int',
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001699 help='maximum number of update checks handled positively '
1700 '(default: unlimited)')
1701 group.add_option('--private_key',
1702 metavar='PATH', default=None,
1703 help='path to the private key in pem format. If this is set '
1704 'the devserver will generate update payloads that are '
1705 'signed with this key.')
David Zeuthen52ccd012013-10-31 12:58:26 -07001706 group.add_option('--private_key_for_metadata_hash_signature',
1707 metavar='PATH', default=None,
1708 help='path to the private key in pem format. If this is set '
1709 'the devserver will sign the metadata hash with the given '
1710 'key and transmit in the Omaha-style XML response.')
1711 group.add_option('--public_key',
1712 metavar='PATH', default=None,
1713 help='path to the public key in pem format. If this is set '
1714 'the devserver will transmit a base64 encoded version of '
1715 'the content in the Omaha-style XML response.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001716 group.add_option('--proxy_port',
1717 metavar='PORT', default=None, type='int',
1718 help='port to have the client connect to -- basically the '
1719 'devserver lies to the update to tell it to get the payload '
1720 'from a different port that will proxy the request back to '
1721 'the devserver. The proxy must be managed outside the '
1722 'devserver.')
1723 group.add_option('--remote_payload',
1724 action='store_true', default=False,
Chris Sosa4b951602014-04-09 20:26:07 -07001725 help='Payload is being served from a remote machine. With '
1726 'this setting enabled, this devserver instance serves as '
1727 'just an Omaha server instance. In this mode, the '
1728 'devserver enforces a few extra components of the Omaha '
Chris Sosafc715442014-04-09 20:45:23 -07001729 'protocol, such as hardware class, being sent.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001730 group.add_option('-u', '--urlbase',
1731 metavar='URL',
Gabe Black3b567202015-09-23 14:07:59 -07001732 help='base URL for update images, other than the '
1733 'devserver. Use in conjunction with remote_payload.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001734 parser.add_option_group(group)
1735
1736
1737def _AddUpdateOptions(parser):
1738 group = optparse.OptionGroup(
1739 parser, 'Autoupdate Options', 'These options can be used to change '
1740 'how the devserver either generates or serve update payloads. Please '
1741 'note that all of these option affect how a payload is generated and so '
1742 'do not work in archive-only mode.')
1743 group.add_option('--board',
1744 help='By default the devserver will create an update '
1745 'payload from the latest image built for the board '
1746 'a device that is requesting an update has. When we '
1747 'pre-generate an update (see below) and we do not specify '
1748 'another update_type option like image or payload, the '
1749 'devserver needs to know the board to generate the latest '
1750 'image for. This is that board.')
1751 group.add_option('--critical_update',
1752 action='store_true', default=False,
1753 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001754 group.add_option('--image',
1755 metavar='FILE',
1756 help='Generate and serve an update using this image to any '
1757 'device that requests an update.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001758 group.add_option('--payload',
1759 metavar='PATH',
1760 help='use the update payload from specified directory '
1761 '(update.gz).')
1762 group.add_option('-p', '--pregenerate_update',
1763 action='store_true', default=False,
1764 help='pre-generate the update payload before accepting '
1765 'update requests. Useful to help debug payload generation '
1766 'issues quickly. Also if an update payload will take a '
1767 'long time to generate, a client may timeout if you do not'
1768 'pregenerate the update.')
1769 group.add_option('--src_image',
1770 metavar='PATH', default='',
1771 help='If specified, delta updates will be generated using '
1772 'this image as the source image. Delta updates are when '
1773 'you are updating from a "source image" to a another '
1774 'image.')
1775 parser.add_option_group(group)
1776
1777
1778def _AddProductionOptions(parser):
1779 group = optparse.OptionGroup(
1780 parser, 'Advanced Server Options', 'These options can be used to changed '
1781 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001782 group.add_option('--clear_cache',
1783 action='store_true', default=False,
1784 help='At startup, removes all cached entries from the'
1785 'devserver\'s cache.')
1786 group.add_option('--logfile',
1787 metavar='PATH',
1788 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -07001789 group.add_option('--pidfile',
1790 metavar='PATH',
1791 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 11:04:13 -08001792 group.add_option('--portfile',
1793 metavar='PATH',
1794 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001795 group.add_option('--production',
1796 action='store_true', default=False,
1797 help='have the devserver use production values when '
1798 'starting up. This includes using more threads and '
1799 'performing less logging.')
1800 parser.add_option_group(group)
1801
1802
Paul Hobbsef4e0702016-06-27 17:01:42 -07001803def MakeLogHandler(logfile):
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001804 """Create a LogHandler instance used to log all messages."""
1805 hdlr_cls = handlers.TimedRotatingFileHandler
1806 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
xixuan3d48bff2017-01-30 19:00:09 -08001807 interval=_LOG_ROTATION_INTERVAL,
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001808 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -07001809 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001810 return hdlr
1811
1812
Chris Sosacde6bf42012-05-31 18:36:39 -07001813def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001814 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -08001815 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -07001816
1817 # get directory that the devserver is run from
1818 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -07001819 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -07001820 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001821 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -07001822 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -07001823 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001824 parser.add_option('--port',
1825 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 13:13:28 -08001826 help=('port for the dev server to use; if zero, binds to '
1827 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001828 parser.add_option('-t', '--test_image',
1829 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -07001830 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -07001831 parser.add_option('-x', '--xbuddy_manage_builds',
1832 action='store_true',
1833 default=False,
1834 help='If set, allow xbuddy to manage images in'
1835 'build/images.')
Dan Shi72b16132015-10-08 12:10:33 -07001836 parser.add_option('-a', '--android_build_credential',
1837 default=None,
1838 help='Path to a json file which contains the credential '
1839 'needed to access Android builds.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001840 _AddProductionOptions(parser)
1841 _AddUpdateOptions(parser)
1842 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -07001843 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001844
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001845 # Handle options that must be set globally in cherrypy. Do this
1846 # work up front, because calls to _Log() below depend on this
1847 # initialization.
1848 if options.production:
1849 cherrypy.config.update({'environment': 'production'})
1850 if not options.logfile:
1851 cherrypy.config.update({'log.screen': True})
1852 else:
1853 cherrypy.config.update({'log.error_file': '',
1854 'log.access_file': ''})
Paul Hobbsef4e0702016-06-27 17:01:42 -07001855 hdlr = MakeLogHandler(options.logfile)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001856 # Pylint can't seem to process these two calls properly
1857 # pylint: disable=E1101
1858 cherrypy.log.access_log.addHandler(hdlr)
1859 cherrypy.log.error_log.addHandler(hdlr)
1860 # pylint: enable=E1101
1861
joychened64b222013-06-21 16:39:34 -07001862 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001863 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001864
joychened64b222013-06-21 16:39:34 -07001865 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001866 # If our devserver is only supposed to serve payloads, we shouldn't be
1867 # mucking with the cache at all. If the devserver hadn't previously
1868 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001869 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001870 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001871 else:
1872 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001873
Chris Sosadbc20082012-12-10 13:39:11 -08001874 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 16:39:34 -07001875 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001876
joychen121fc9b2013-08-02 14:30:30 -07001877 _xbuddy = xbuddy.XBuddy(options.xbuddy_manage_builds,
1878 options.board,
joychen121fc9b2013-08-02 14:30:30 -07001879 static_dir=options.static_dir)
Chris Sosa75490802013-09-30 17:21:45 -07001880 if options.clear_cache and options.xbuddy_manage_builds:
1881 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 14:30:30 -07001882
Chris Sosa6a3697f2013-01-29 16:44:43 -08001883 # We allow global use here to share with cherrypy classes.
1884 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001885 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001886 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001887 _xbuddy,
joychened64b222013-06-21 16:39:34 -07001888 static_dir=options.static_dir,
Andrew de los Reyes52620802010-04-12 13:40:07 -07001889 urlbase=options.urlbase,
Chris Sosa5d342a22010-09-28 16:54:41 -07001890 forced_image=options.image,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001891 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001892 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-28 23:42:37 -07001893 src_image=options.src_image,
Chris Sosa08d55a22011-01-19 16:08:02 -08001894 board=options.board,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001895 copy_to_static_root=not options.exit,
1896 private_key=options.private_key,
Gabe Black3b567202015-09-23 14:07:59 -07001897 private_key_for_metadata_hash_signature=(
1898 options.private_key_for_metadata_hash_signature),
David Zeuthen52ccd012013-10-31 12:58:26 -07001899 public_key=options.public_key,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001900 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001901 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001902 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001903 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001904 )
Chris Sosa7c931362010-10-11 19:49:01 -07001905
Chris Sosa6a3697f2013-01-29 16:44:43 -08001906 if options.pregenerate_update:
1907 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 15:46:22 -07001908
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001909 if options.exit:
1910 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001911
joychen3cb228e2013-06-12 12:13:13 -07001912 dev_server = DevServerRoot(_xbuddy)
1913
Gilad Arnold11fbef42014-02-10 11:04:13 -08001914 # Patch CherryPy to support binding to any available port (--port=0).
1915 cherrypy_ext.ZeroPortPatcher.DoPatch(cherrypy)
1916
Chris Sosa855b8932013-08-21 13:24:55 -07001917 if options.pidfile:
1918 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1919
Gilad Arnold11fbef42014-02-10 11:04:13 -08001920 if options.portfile:
1921 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1922
Dan Shiafd5c6c2016-01-07 10:27:03 -08001923 if (options.android_build_credential and
1924 os.path.exists(options.android_build_credential)):
1925 try:
1926 with open(options.android_build_credential) as f:
1927 android_build.BuildAccessor.credential_info = json.load(f)
1928 except ValueError as e:
1929 _Log('Failed to load the android build credential: %s. Error: %s.' %
1930 (options.android_build_credential, e))
joychen3cb228e2013-06-12 12:13:13 -07001931 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001932
1933
1934if __name__ == '__main__':
1935 main()