blob: ced16b52a5e469e22f715222c9b4d2489804b24d [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
Richard Barnettedf35c322017-08-18 17:02:13 -070063# This must happen before any local modules get a chance to import
64# anything from chromite. Otherwise, really bad things will happen, and
65# you will _not_ understand why.
66import setup_chromite # pylint: disable=unused-import
67
Chris Sosa0356d3b2010-09-16 15:46:22 -070068import autoupdate
Dan Shi2f136862016-02-11 15:38:38 -080069import artifact_info
Chris Sosa75490802013-09-30 17:21:45 -070070import build_artifact
Gilad Arnold11fbef42014-02-10 11:04:13 -080071import cherrypy_ext
Gilad Arnoldc65330c2012-09-20 15:17:48 -070072import common_util
Simran Basief83d6a2014-08-28 14:32:01 -070073import devserver_constants
Chris Sosa47a7d4e2012-03-28 11:26:55 -070074import downloader
Chris Sosa7cd23202013-10-15 17:22:57 -070075import gsutil_util
Gilad Arnoldc65330c2012-09-20 15:17:48 -070076import log_util
joychen3cb228e2013-06-12 12:13:13 -070077import xbuddy
Gilad Arnoldc65330c2012-09-20 15:17:48 -070078
Gilad Arnoldc65330c2012-09-20 15:17:48 -070079# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080080def _Log(message, *args):
81 return log_util.LogWithTag('DEVSERVER', message, *args)
Chris Sosa0356d3b2010-09-16 15:46:22 -070082
Dan Shiafd0e492015-05-27 14:23:51 -070083try:
84 import psutil
85except ImportError:
86 # Ignore psutil import failure. This is for backwards compatibility, so
87 # "cros flash" can still update duts with build without psutil installed.
88 # The reason is that, during cros flash, local devserver code is copied over
89 # to DUT, and devserver will be running inside DUT to stage the build.
90 _Log('Python module psutil is not installed, devserver load data will not be '
91 'collected')
92 psutil = None
Dan Shi94dcbe82015-06-08 20:51:13 -070093except OSError as e:
94 # Ignore error like following. psutil may not work properly in builder. Ignore
95 # the error as load information of devserver is not used in builder.
96 # OSError: [Errno 2] No such file or directory: '/dev/pts/0'
97 _Log('psutil is failed to be imported, error: %s. devserver load data will '
98 'not be collected.', e)
99 psutil = None
100
xixuanac89ce82016-11-30 16:48:20 -0800101# Use try-except to skip unneccesary import for simple use case, eg. running
102# devserver on host.
103try:
104 import cros_update
xixuanac89ce82016-11-30 16:48:20 -0800105except ImportError as e:
106 _Log('cros_update cannot be imported: %r', e)
107 cros_update = None
xixuana4f4e712017-05-08 15:17:54 -0700108
109try:
110 import cros_update_progress
111except ImportError as e:
112 _Log('cros_update_progress cannot be imported: %r', e)
xixuanac89ce82016-11-30 16:48:20 -0800113 cros_update_progress = None
114
xixuanac89ce82016-11-30 16:48:20 -0800115try:
116 from chromite.lib.paygen import gspaths
117except ImportError as e:
118 _Log('chromite cannot be imported: %r', e)
119 gspaths = None
120
Dan Shi72b16132015-10-08 12:10:33 -0700121try:
122 import android_build
123except ImportError as e:
124 # Ignore android_build import failure. This is to support devserver running
125 # inside a ChromeOS device triggered by cros flash. Most ChromeOS test images
126 # do not have google-api-python-client module and they don't need to support
127 # Android updating, therefore, ignore the import failure here.
Dan Shi72b16132015-10-08 12:10:33 -0700128 android_build = None
Frank Farzan40160872011-12-12 18:39:18 -0800129
Chris Sosa417e55d2011-01-25 16:40:48 -0800130CACHED_ENTRIES = 12
Don Garrettf90edf02010-11-16 17:36:14 -0800131
Simran Basi4baad082013-02-14 13:39:18 -0800132TELEMETRY_FOLDER = 'telemetry_src'
133TELEMETRY_DEPS = ['dep-telemetry_dep.tar.bz2',
134 'dep-page_cycler_dep.tar.bz2',
Simran Basi0d078682013-03-22 16:40:04 -0700135 'dep-chrome_test.tar.bz2',
136 'dep-perf_data_dep.tar.bz2']
Simran Basi4baad082013-02-14 13:39:18 -0800137
Chris Sosa0356d3b2010-09-16 15:46:22 -0700138# Sets up global to share between classes.
rtc@google.com21a5ca32009-11-04 18:23:23 +0000139updater = None
rtc@google.comded22402009-10-26 22:36:21 +0000140
xixuan3d48bff2017-01-30 19:00:09 -0800141# Log rotation parameters. These settings correspond to twice a day once
142# devserver is started, with about two weeks (28 backup files) of old logs
143# kept for backup.
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700144#
xixuan3d48bff2017-01-30 19:00:09 -0800145# For more, see the documentation in standard python library for
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700146# logging.handlers.TimedRotatingFileHandler
xixuan3d48bff2017-01-30 19:00:09 -0800147_LOG_ROTATION_TIME = 'H'
148_LOG_ROTATION_INTERVAL = 12 # hours
149_LOG_ROTATION_BACKUP = 28 # backup counts
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700150
Dan Shiafd0e492015-05-27 14:23:51 -0700151# Number of seconds between the collection of disk and network IO counters.
152STATS_INTERVAL = 10.0
Frank Farzan40160872011-12-12 18:39:18 -0800153
xixuan52c2fba2016-05-20 17:02:48 -0700154# Auto-update parameters
155
156# Error msg for missing key in CrOS auto-update.
157KEY_ERROR_MSG = 'Key Error in cmd %s: %s= is required'
158
159# Command of running auto-update.
160AUTO_UPDATE_CMD = '/usr/bin/python -u %s -d %s -b %s --static_dir %s'
161
162
Chris Sosa9164ca32012-03-28 11:04:50 -0700163class DevServerError(Exception):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700164 """Exception class used by this module."""
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700165
166
Dan Shiafd0e492015-05-27 14:23:51 -0700167def require_psutil():
Gabe Black3b567202015-09-23 14:07:59 -0700168 """Decorator for functions require psutil to run."""
Dan Shiafd0e492015-05-27 14:23:51 -0700169 def deco_require_psutil(func):
170 """Wrapper of the decorator function.
171
Gabe Black3b567202015-09-23 14:07:59 -0700172 Args:
173 func: function to be called.
Dan Shiafd0e492015-05-27 14:23:51 -0700174 """
175 def func_require_psutil(*args, **kwargs):
176 """Decorator for functions require psutil to run.
177
178 If psutil is not installed, skip calling the function.
179
Gabe Black3b567202015-09-23 14:07:59 -0700180 Args:
181 *args: arguments for function to be called.
182 **kwargs: keyword arguments for function to be called.
Dan Shiafd0e492015-05-27 14:23:51 -0700183 """
184 if psutil:
185 return func(*args, **kwargs)
186 else:
187 _Log('Python module psutil is not installed. Function call %s is '
188 'skipped.' % func)
189 return func_require_psutil
190 return deco_require_psutil
191
192
Gabe Black3b567202015-09-23 14:07:59 -0700193def _canonicalize_archive_url(archive_url):
194 """Canonicalizes archive_url strings.
195
196 Raises:
197 DevserverError: if archive_url is not set.
198 """
199 if archive_url:
200 if not archive_url.startswith('gs://'):
201 raise DevServerError("Archive URL isn't from Google Storage (%s) ." %
202 archive_url)
203
204 return archive_url.rstrip('/')
205 else:
206 raise DevServerError("Must specify an archive_url in the request")
207
208
209def _canonicalize_local_path(local_path):
210 """Canonicalizes |local_path| strings.
211
212 Raises:
213 DevserverError: if |local_path| is not set.
214 """
215 # Restrict staging of local content to only files within the static
216 # directory.
217 local_path = os.path.abspath(local_path)
218 if not local_path.startswith(updater.static_dir):
219 raise DevServerError('Local path %s must be a subdirectory of the static'
220 ' directory: %s' % (local_path, updater.static_dir))
221
222 return local_path.rstrip('/')
223
224
225def _get_artifacts(kwargs):
226 """Returns a tuple of named and file artifacts given the stage rpc kwargs.
227
228 Raises:
229 DevserverError if no artifacts would be returned.
230 """
231 artifacts = kwargs.get('artifacts')
232 files = kwargs.get('files')
233 if not artifacts and not files:
234 raise DevServerError('No artifacts specified.')
235
236 # Note we NEED to coerce files to a string as we get raw unicode from
237 # cherrypy and we treat files as strings elsewhere in the code.
238 return (str(artifacts).split(',') if artifacts else [],
239 str(files).split(',') if files else [])
240
241
Dan Shi61305df2015-10-26 16:52:35 -0700242def _is_android_build_request(kwargs):
243 """Check if a devserver call is for Android build, based on the arguments.
244
245 This method exams the request's arguments (os_type) to determine if the
246 request is for Android build. If os_type is set to `android`, returns True.
247 If os_type is not set or has other values, returns False.
248
249 Args:
250 kwargs: Keyword arguments for the request.
251
252 Returns:
253 True if the request is for Android build. False otherwise.
254 """
255 os_type = kwargs.get('os_type', None)
256 return os_type == 'android'
257
258
Gabe Black3b567202015-09-23 14:07:59 -0700259def _get_downloader(kwargs):
260 """Returns the downloader based on passed in arguments.
261
262 Args:
263 kwargs: Keyword arguments for the request.
264 """
265 local_path = kwargs.get('local_path')
266 if local_path:
267 local_path = _canonicalize_local_path(local_path)
268
269 dl = None
270 if local_path:
271 dl = downloader.LocalDownloader(updater.static_dir, local_path)
272
Dan Shi61305df2015-10-26 16:52:35 -0700273 if not _is_android_build_request(kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700274 archive_url = kwargs.get('archive_url')
275 if not archive_url and not local_path:
276 raise DevServerError('Requires archive_url or local_path to be '
277 'specified.')
278 if archive_url and local_path:
279 raise DevServerError('archive_url and local_path can not both be '
280 'specified.')
281 if not dl:
282 archive_url = _canonicalize_archive_url(archive_url)
283 dl = downloader.GoogleStorageDownloader(updater.static_dir, archive_url)
284 elif not dl:
285 target = kwargs.get('target', None)
Dan Shi72b16132015-10-08 12:10:33 -0700286 branch = kwargs.get('branch', None)
Dan Shi61305df2015-10-26 16:52:35 -0700287 build_id = kwargs.get('build_id', None)
288 if not target or not branch or not build_id:
Dan Shi72b16132015-10-08 12:10:33 -0700289 raise DevServerError(
Dan Shi61305df2015-10-26 16:52:35 -0700290 'target, branch, build ID must all be specified for downloading '
291 'Android build.')
Dan Shi72b16132015-10-08 12:10:33 -0700292 dl = downloader.AndroidBuildDownloader(updater.static_dir, branch, build_id,
293 target)
Gabe Black3b567202015-09-23 14:07:59 -0700294
295 return dl
296
297
298def _get_downloader_and_factory(kwargs):
299 """Returns the downloader and artifact factory based on passed in arguments.
300
301 Args:
302 kwargs: Keyword arguments for the request.
303 """
304 artifacts, files = _get_artifacts(kwargs)
305 dl = _get_downloader(kwargs)
306
307 if (isinstance(dl, downloader.GoogleStorageDownloader) or
308 isinstance(dl, downloader.LocalDownloader)):
309 factory_class = build_artifact.ChromeOSArtifactFactory
Dan Shi72b16132015-10-08 12:10:33 -0700310 elif isinstance(dl, downloader.AndroidBuildDownloader):
Gabe Black3b567202015-09-23 14:07:59 -0700311 factory_class = build_artifact.AndroidArtifactFactory
312 else:
313 raise DevServerError('Unrecognized value for downloader type: %s' %
314 type(dl))
315
316 factory = factory_class(dl.GetBuildDir(), artifacts, files, dl.GetBuild())
317
318 return dl, factory
319
320
Scott Zawalski4647ce62012-01-03 17:17:28 -0500321def _LeadingWhiteSpaceCount(string):
322 """Count the amount of leading whitespace in a string.
323
324 Args:
325 string: The string to count leading whitespace in.
Don Garrettf84631a2014-01-07 18:21:26 -0800326
Scott Zawalski4647ce62012-01-03 17:17:28 -0500327 Returns:
328 number of white space chars before characters start.
329 """
Gabe Black3b567202015-09-23 14:07:59 -0700330 matched = re.match(r'^\s+', string)
Scott Zawalski4647ce62012-01-03 17:17:28 -0500331 if matched:
332 return len(matched.group())
333
334 return 0
335
336
337def _PrintDocStringAsHTML(func):
338 """Make a functions docstring somewhat HTML style.
339
340 Args:
341 func: The function to return the docstring from.
Don Garrettf84631a2014-01-07 18:21:26 -0800342
Scott Zawalski4647ce62012-01-03 17:17:28 -0500343 Returns:
344 A string that is somewhat formated for a web browser.
345 """
346 # TODO(scottz): Make this parse Args/Returns in a prettier way.
347 # Arguments could be bolded and indented etc.
348 html_doc = []
349 for line in func.__doc__.splitlines():
350 leading_space = _LeadingWhiteSpaceCount(line)
351 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700352 line = '&nbsp;' * leading_space + line
Scott Zawalski4647ce62012-01-03 17:17:28 -0500353
354 html_doc.append('<BR>%s' % line)
355
356 return '\n'.join(html_doc)
357
358
Simran Basief83d6a2014-08-28 14:32:01 -0700359def _GetUpdateTimestampHandler(static_dir):
360 """Returns a handler to update directory staged.timestamp.
361
362 This handler resets the stage.timestamp whenever static content is accessed.
363
364 Args:
365 static_dir: Directory from which static content is being staged.
366
367 Returns:
368 A cherrypy handler to update the timestamp of accessed content.
369 """
370 def UpdateTimestampHandler():
371 if not '404' in cherrypy.response.status:
372 build_match = re.match(devserver_constants.STAGED_BUILD_REGEX,
373 cherrypy.request.path_info)
374 if build_match:
375 build_dir = os.path.join(static_dir, build_match.group('build'))
376 downloader.Downloader.TouchTimestampForStaged(build_dir)
377 return UpdateTimestampHandler
378
379
Chris Sosa7c931362010-10-11 19:49:01 -0700380def _GetConfig(options):
381 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800382
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800383 socket_host = '::'
Yu-Ju Hongc8d4af32013-11-12 15:14:26 -0800384 # Fall back to IPv4 when python is not configured with IPv6.
385 if not socket.has_ipv6:
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800386 socket_host = '0.0.0.0'
387
Simran Basief83d6a2014-08-28 14:32:01 -0700388 # Adds the UpdateTimestampHandler to cherrypy's tools. This tools executes
389 # on the on_end_resource hook. This hook is called once processing is
390 # complete and the response is ready to be returned.
391 cherrypy.tools.update_timestamp = cherrypy.Tool(
392 'on_end_resource', _GetUpdateTimestampHandler(options.static_dir))
393
Gabe Black3b567202015-09-23 14:07:59 -0700394 base_config = {'global':
395 {'server.log_request_headers': True,
396 'server.protocol_version': 'HTTP/1.1',
397 'server.socket_host': socket_host,
398 'server.socket_port': int(options.port),
399 'response.timeout': 6000,
400 'request.show_tracebacks': True,
401 'server.socket_timeout': 60,
402 'server.thread_pool': 2,
403 'engine.autoreload.on': False,
404 },
405 '/api':
406 {
407 # Gets rid of cherrypy parsing post file for args.
408 'request.process_request_body': False,
409 },
410 '/build':
411 {'response.timeout': 100000,
412 },
413 '/update':
414 {
415 # Gets rid of cherrypy parsing post file for args.
416 'request.process_request_body': False,
417 'response.timeout': 10000,
418 },
419 # Sets up the static dir for file hosting.
420 '/static':
421 {'tools.staticdir.dir': options.static_dir,
422 'tools.staticdir.on': True,
423 'response.timeout': 10000,
424 'tools.update_timestamp.on': True,
425 },
426 }
Chris Sosa5f118ef2012-07-12 11:37:50 -0700427 if options.production:
Alex Miller93beca52013-07-30 19:25:09 -0700428 base_config['global'].update({'server.thread_pool': 150})
Chris Sosa7cd23202013-10-15 17:22:57 -0700429 # TODO(sosa): Do this more cleanly.
430 gsutil_util.GSUTIL_ATTEMPTS = 5
Scott Zawalski1c5e7cd2012-02-27 13:12:52 -0500431
Chris Sosa7c931362010-10-11 19:49:01 -0700432 return base_config
rtc@google.com64244662009-11-12 00:52:08 +0000433
Darin Petkove17164a2010-08-11 13:24:41 -0700434
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700435def _GetRecursiveMemberObject(root, member_list):
436 """Returns an object corresponding to a nested member list.
437
438 Args:
439 root: the root object to search
440 member_list: list of nested members to search
Don Garrettf84631a2014-01-07 18:21:26 -0800441
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700442 Returns:
443 An object corresponding to the member name list; None otherwise.
444 """
445 for member in member_list:
446 next_root = root.__class__.__dict__.get(member)
447 if not next_root:
448 return None
449 root = next_root
450 return root
451
452
453def _IsExposed(name):
454 """Returns True iff |name| has an `exposed' attribute and it is set."""
455 return hasattr(name, 'exposed') and name.exposed
456
457
Gilad Arnold748c8322012-10-12 09:51:35 -0700458def _GetExposedMethod(root, nested_member, ignored=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700459 """Returns a CherryPy-exposed method, if such exists.
460
461 Args:
462 root: the root object for searching
463 nested_member: a slash-joined path to the nested member
464 ignored: method paths to be ignored
Don Garrettf84631a2014-01-07 18:21:26 -0800465
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700466 Returns:
467 A function object corresponding to the path defined by |member_list| from
468 the |root| object, if the function is exposed and not ignored; None
469 otherwise.
470 """
Gilad Arnold748c8322012-10-12 09:51:35 -0700471 method = (not (ignored and nested_member in ignored) and
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700472 _GetRecursiveMemberObject(root, nested_member.split('/')))
Gabe Black3b567202015-09-23 14:07:59 -0700473 if method and type(method) == types.FunctionType and _IsExposed(method):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700474 return method
475
476
Gilad Arnold748c8322012-10-12 09:51:35 -0700477def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700478 """Finds exposed CherryPy methods.
479
480 Args:
481 root: the root object for searching
482 prefix: slash-joined chain of members leading to current object
483 unlisted: URLs to be excluded regardless of their exposed status
Don Garrettf84631a2014-01-07 18:21:26 -0800484
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700485 Returns:
486 List of exposed URLs that are not unlisted.
487 """
488 method_list = []
489 for member in sorted(root.__class__.__dict__.keys()):
490 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 09:51:35 -0700491 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700492 continue
493 member_obj = root.__class__.__dict__[member]
494 if _IsExposed(member_obj):
495 if type(member_obj) == types.FunctionType:
496 method_list.append(prefixed_member)
497 else:
498 method_list += _FindExposedMethods(
499 member_obj, prefixed_member, unlisted)
500 return method_list
501
502
xixuan52c2fba2016-05-20 17:02:48 -0700503def _check_base_args_for_auto_update(kwargs):
xixuanac89ce82016-11-30 16:48:20 -0800504 """Check basic args required for auto-update.
505
506 Args:
507 kwargs: the parameters to be checked.
508
509 Raises:
510 DevServerHTTPError if required parameters don't exist in kwargs.
511 """
xixuan52c2fba2016-05-20 17:02:48 -0700512 if 'host_name' not in kwargs:
513 raise common_util.DevServerHTTPError(KEY_ERROR_MSG % 'host_name')
514
515 if 'build_name' not in kwargs:
516 raise common_util.DevServerHTTPError(KEY_ERROR_MSG % 'build_name')
517
518
519def _parse_boolean_arg(kwargs, key):
xixuanac89ce82016-11-30 16:48:20 -0800520 """Parse boolean arg from kwargs.
521
522 Args:
523 kwargs: the parameters to be checked.
524 key: the key to be parsed.
525
526 Returns:
527 The boolean value of kwargs[key], or False if key doesn't exist in kwargs.
528
529 Raises:
530 DevServerHTTPError if kwargs[key] is not a boolean variable.
531 """
xixuan52c2fba2016-05-20 17:02:48 -0700532 if key in kwargs:
533 if kwargs[key] == 'True':
534 return True
535 elif kwargs[key] == 'False':
536 return False
537 else:
538 raise common_util.DevServerHTTPError(
539 'The value for key %s is not boolean.' % key)
540 else:
541 return False
542
xixuan447ad9d2017-02-28 14:46:20 -0800543
xixuanac89ce82016-11-30 16:48:20 -0800544def _parse_string_arg(kwargs, key):
545 """Parse string arg from kwargs.
546
547 Args:
548 kwargs: the parameters to be checked.
549 key: the key to be parsed.
550
551 Returns:
552 The string value of kwargs[key], or None if key doesn't exist in kwargs.
553 """
554 if key in kwargs:
555 return kwargs[key]
556 else:
557 return None
558
xixuan447ad9d2017-02-28 14:46:20 -0800559
xixuanac89ce82016-11-30 16:48:20 -0800560def _build_uri_from_build_name(build_name):
561 """Get build url from a given build name.
562
563 Args:
564 build_name: the build name to be parsed, whose format is
565 'board/release_version'.
566
567 Returns:
568 The release_archive_url on Google Storage for this build name.
569 """
570 return gspaths.ChromeosReleases.BuildUri(
571 cros_update.STABLE_BUILD_CHANNEL, build_name.split('/')[0],
572 build_name.split('/')[1])
xixuan52c2fba2016-05-20 17:02:48 -0700573
xixuan447ad9d2017-02-28 14:46:20 -0800574
575def _clear_process(host_name, pid):
576 """Clear AU process for given hostname and pid.
577
578 This clear includes:
579 1. kill process if it's alive.
580 2. delete the track status file of this process.
581 3. delete the executing log file of this process.
582
583 Args:
584 host_name: the host to execute auto-update.
585 pid: the background auto-update process id.
586 """
587 if cros_update_progress.IsProcessAlive(pid):
588 os.killpg(int(pid), signal.SIGKILL)
589
590 cros_update_progress.DelTrackStatusFile(host_name, pid)
591 cros_update_progress.DelExecuteLogFile(host_name, pid)
592
593
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700594class ApiRoot(object):
595 """RESTful API for Dev Server information."""
596 exposed = True
597
598 @cherrypy.expose
599 def hostinfo(self, ip):
600 """Returns a JSON dictionary containing information about the given ip.
601
Gilad Arnold1b908392012-10-05 11:36:27 -0700602 Args:
603 ip: address of host whose info is requested
Don Garrettf84631a2014-01-07 18:21:26 -0800604
Gilad Arnold1b908392012-10-05 11:36:27 -0700605 Returns:
606 A JSON dictionary containing all or some of the following fields:
607 last_event_type (int): last update event type received
608 last_event_status (int): last update event status received
609 last_known_version (string): last known version reported in update ping
610 forced_update_label (string): update label to force next update ping to
611 use, set by setnextupdate
612 See the OmahaEvent class in update_engine/omaha_request_action.h for
613 event type and status code definitions. If the ip does not exist an empty
614 string is returned.
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700615
Gilad Arnold1b908392012-10-05 11:36:27 -0700616 Example URL:
617 http://myhost/api/hostinfo?ip=192.168.1.5
618 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700619 return updater.HandleHostInfoPing(ip)
620
621 @cherrypy.expose
Gilad Arnold286a0062012-01-12 13:47:02 -0800622 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 11:36:27 -0700623 """Returns a JSON object containing a log of host event.
624
625 Args:
626 ip: address of host whose event log is requested, or `all'
Don Garrettf84631a2014-01-07 18:21:26 -0800627
Gilad Arnold1b908392012-10-05 11:36:27 -0700628 Returns:
629 A JSON encoded list (log) of dictionaries (events), each of which
630 containing a `timestamp' and other event fields, as described under
631 /api/hostinfo.
632
633 Example URL:
634 http://myhost/api/hostlog?ip=192.168.1.5
635 """
Gilad Arnold286a0062012-01-12 13:47:02 -0800636 return updater.HandleHostLogPing(ip)
637
638 @cherrypy.expose
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700639 def setnextupdate(self, ip):
640 """Allows the response to the next update ping from a host to be set.
641
642 Takes the IP of the host and an update label as normally provided to the
Gilad Arnold1b908392012-10-05 11:36:27 -0700643 /update command.
644 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700645 body_length = int(cherrypy.request.headers['Content-Length'])
646 label = cherrypy.request.rfile.read(body_length)
647
648 if label:
649 label = label.strip()
650 if label:
651 return updater.HandleSetUpdatePing(ip, label)
Chris Sosa4b951602014-04-09 20:26:07 -0700652 raise common_util.DevServerHTTPError(400, 'No label provided.')
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700653
654
Gilad Arnold55a2a372012-10-02 09:46:32 -0700655 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800656 def fileinfo(self, *args):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700657 """Returns information about a given staged file.
658
659 Args:
Don Garrettf84631a2014-01-07 18:21:26 -0800660 args: path to the file inside the server's static staging directory
661
Gilad Arnold55a2a372012-10-02 09:46:32 -0700662 Returns:
663 A JSON encoded dictionary with information about the said file, which may
664 contain the following keys/values:
Gilad Arnold1b908392012-10-05 11:36:27 -0700665 size (int): the file size in bytes
666 sha1 (string): a base64 encoded SHA1 hash
667 sha256 (string): a base64 encoded SHA256 hash
668
669 Example URL:
670 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 09:46:32 -0700671 """
Don Garrettf84631a2014-01-07 18:21:26 -0800672 file_path = os.path.join(updater.static_dir, *args)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700673 if not os.path.exists(file_path):
674 raise DevServerError('file not found: %s' % file_path)
675 try:
676 file_size = os.path.getsize(file_path)
677 file_sha1 = common_util.GetFileSha1(file_path)
678 file_sha256 = common_util.GetFileSha256(file_path)
679 except os.error, e:
680 raise DevServerError('failed to get info for file %s: %s' %
Gilad Arnolde74b3812013-04-22 11:27:38 -0700681 (file_path, e))
682
683 is_delta = autoupdate.Autoupdate.IsDeltaFormatFile(file_path)
684
685 return json.dumps({
686 autoupdate.Autoupdate.SIZE_ATTR: file_size,
687 autoupdate.Autoupdate.SHA1_ATTR: file_sha1,
688 autoupdate.Autoupdate.SHA256_ATTR: file_sha256,
689 autoupdate.Autoupdate.ISDELTA_ATTR: is_delta
690 })
Gilad Arnold55a2a372012-10-02 09:46:32 -0700691
Chris Sosa76e44b92013-01-31 12:11:38 -0800692
David Rochberg7c79a812011-01-19 14:24:45 -0500693class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700694 """The Root Class for the Dev Server.
695
696 CherryPy works as follows:
697 For each method in this class, cherrpy interprets root/path
698 as a call to an instance of DevServerRoot->method_name. For example,
699 a call to http://myhost/build will call build. CherryPy automatically
700 parses http args and places them as keyword arguments in each method.
701 For paths http://myhost/update/dir1/dir2, you can use *args so that
702 cherrypy uses the update method and puts the extra paths in args.
703 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700704 # Method names that should not be listed on the index page.
705 _UNLISTED_METHODS = ['index', 'doc']
706
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700707 api = ApiRoot()
Chris Sosa7c931362010-10-11 19:49:01 -0700708
Dan Shi59ae7092013-06-04 14:37:27 -0700709 # Number of threads that devserver is staging images.
710 _staging_thread_count = 0
711 # Lock used to lock increasing/decreasing count.
712 _staging_thread_count_lock = threading.Lock()
713
Dan Shiafd0e492015-05-27 14:23:51 -0700714 @require_psutil()
715 def _refresh_io_stats(self):
716 """A call running in a thread to update IO stats periodically."""
717 prev_disk_io_counters = psutil.disk_io_counters()
718 prev_network_io_counters = psutil.net_io_counters()
719 prev_read_time = time.time()
720 while True:
721 time.sleep(STATS_INTERVAL)
722 now = time.time()
723 interval = now - prev_read_time
724 prev_read_time = now
725 # Disk IO is for all disks.
726 disk_io_counters = psutil.disk_io_counters()
727 network_io_counters = psutil.net_io_counters()
728
729 self.disk_read_bytes_per_sec = (
730 disk_io_counters.read_bytes -
731 prev_disk_io_counters.read_bytes)/interval
732 self.disk_write_bytes_per_sec = (
733 disk_io_counters.write_bytes -
734 prev_disk_io_counters.write_bytes)/interval
735 prev_disk_io_counters = disk_io_counters
736
737 self.network_sent_bytes_per_sec = (
738 network_io_counters.bytes_sent -
739 prev_network_io_counters.bytes_sent)/interval
740 self.network_recv_bytes_per_sec = (
741 network_io_counters.bytes_recv -
742 prev_network_io_counters.bytes_recv)/interval
743 prev_network_io_counters = network_io_counters
744
745 @require_psutil()
746 def _start_io_stat_thread(self):
Gabe Black3b567202015-09-23 14:07:59 -0700747 """Start the thread to collect IO stats."""
Dan Shiafd0e492015-05-27 14:23:51 -0700748 thread = threading.Thread(target=self._refresh_io_stats)
749 thread.daemon = True
750 thread.start()
751
joychen3cb228e2013-06-12 12:13:13 -0700752 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700753 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800754 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 12:13:13 -0700755 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 14:24:45 -0500756
Dan Shiafd0e492015-05-27 14:23:51 -0700757 # Cache of disk IO stats, a thread refresh the stats every 10 seconds.
758 # lock is not used for these variables as the only thread writes to these
759 # variables is _refresh_io_stats.
760 self.disk_read_bytes_per_sec = 0
761 self.disk_write_bytes_per_sec = 0
762 # Cache of network IO stats.
763 self.network_sent_bytes_per_sec = 0
764 self.network_recv_bytes_per_sec = 0
765 self._start_io_stat_thread()
766
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700767 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500768 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700769 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700770 import builder
771 if self._builder is None:
772 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500773 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700774
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700775 @cherrypy.expose
Dan Shif8eb0d12013-08-01 17:52:06 -0700776 def is_staged(self, **kwargs):
777 """Check if artifacts have been downloaded.
778
Chris Sosa6b0c6172013-08-05 17:01:33 -0700779 async: True to return without waiting for download to complete.
780 artifacts: Comma separated list of named artifacts to download.
781 These are defined in artifact_info and have their implementation
782 in build_artifact.py.
783 files: Comma separated list of file artifacts to stage. These
784 will be available as is in the corresponding static directory with no
785 custom post-processing.
786
787 returns: True of all artifacts are staged.
Dan Shif8eb0d12013-08-01 17:52:06 -0700788
789 Example:
790 To check if autotest and test_suites are staged:
791 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
792 artifacts=autotest,test_suites
793 """
Gabe Black3b567202015-09-23 14:07:59 -0700794 dl, factory = _get_downloader_and_factory(kwargs)
Aviv Keshet57d18172016-06-18 20:39:09 -0700795 response = str(dl.IsStaged(factory))
796 _Log('Responding to is_staged %s request with %r', kwargs, response)
797 return response
Dan Shi59ae7092013-06-04 14:37:27 -0700798
Chris Sosa76e44b92013-01-31 12:11:38 -0800799 @cherrypy.expose
Prashanth Ba06d2d22014-03-07 15:35:19 -0800800 def list_image_dir(self, **kwargs):
801 """Take an archive url and list the contents in its staged directory.
802
803 Args:
804 kwargs:
805 archive_url: Google Storage URL for the build.
806
807 Example:
808 To list the contents of where this devserver should have staged
809 gs://image-archive/<board>-release/<build> call:
810 http://devserver_url:<port>/list_image_dir?archive_url=<gs://..>
811
812 Returns:
813 A string with information about the contents of the image directory.
814 """
Gabe Black3b567202015-09-23 14:07:59 -0700815 dl = _get_downloader(kwargs)
Prashanth Ba06d2d22014-03-07 15:35:19 -0800816 try:
Gabe Black3b567202015-09-23 14:07:59 -0700817 image_dir_contents = dl.ListBuildDir()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800818 except build_artifact.ArtifactDownloadError as e:
819 return 'Cannot list the contents of staged artifacts. %s' % e
820 if not image_dir_contents:
Gabe Black3b567202015-09-23 14:07:59 -0700821 return '%s has not been staged on this devserver.' % dl.DescribeSource()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800822 return image_dir_contents
823
824 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800825 def stage(self, **kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700826 """Downloads and caches build artifacts.
Chris Sosa76e44b92013-01-31 12:11:38 -0800827
Gabe Black3b567202015-09-23 14:07:59 -0700828 Downloads and caches build artifacts, possibly from a Google Storage URL,
Dan Shi72b16132015-10-08 12:10:33 -0700829 or from Android's build server. Returns once these have been downloaded
Gabe Black3b567202015-09-23 14:07:59 -0700830 on the devserver. A call to this will attempt to cache non-specified
831 artifacts in the background for the given from the given URL following
832 the principle of spatial locality. Spatial locality of different
Chris Sosa76e44b92013-01-31 12:11:38 -0800833 artifacts is explicitly defined in the build_artifact module.
834
835 These artifacts will then be available from the static/ sub-directory of
836 the devserver.
837
838 Args:
839 archive_url: Google Storage URL for the build.
Simran Basi4243a862014-12-12 12:48:33 -0800840 local_path: Local path for the build.
Dan Shif8eb0d12013-08-01 17:52:06 -0700841 async: True to return without waiting for download to complete.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700842 artifacts: Comma separated list of named artifacts to download.
843 These are defined in artifact_info and have their implementation
844 in build_artifact.py.
845 files: Comma separated list of files to stage. These
846 will be available as is in the corresponding static directory with no
847 custom post-processing.
Laurence Goodbyf5c958d2016-01-14 18:23:56 -0800848 clean: True to remove any previously staged artifacts first.
Chris Sosa76e44b92013-01-31 12:11:38 -0800849
850 Example:
851 To download the autotest and test suites tarballs:
852 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
853 artifacts=autotest,test_suites
854 To download the full update payload:
855 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
856 artifacts=full_payload
Chris Sosa6b0c6172013-08-05 17:01:33 -0700857 To download just a file called blah.bin:
858 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
859 files=blah.bin
Chris Sosa76e44b92013-01-31 12:11:38 -0800860
861 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 16:39:34 -0700862 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 12:11:38 -0800863
864 Note for this example, relative path is the archive_url stripped of its
865 basename i.e. path/ in the examples above. Specific example:
866
867 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
868
869 Will get staged to:
870
joychened64b222013-06-21 16:39:34 -0700871 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Chris Sosa76e44b92013-01-31 12:11:38 -0800872 """
Gabe Black3b567202015-09-23 14:07:59 -0700873 dl, factory = _get_downloader_and_factory(kwargs)
874
Dan Shi59ae7092013-06-04 14:37:27 -0700875 with DevServerRoot._staging_thread_count_lock:
876 DevServerRoot._staging_thread_count += 1
877 try:
Laurence Goodbyf5c958d2016-01-14 18:23:56 -0800878 boolean_string = kwargs.get('clean')
879 clean = xbuddy.XBuddy.ParseBoolean(boolean_string)
880 if clean and os.path.exists(dl.GetBuildDir()):
881 _Log('Removing %s' % dl.GetBuildDir())
882 shutil.rmtree(dl.GetBuildDir())
Gabe Black3b567202015-09-23 14:07:59 -0700883 async = kwargs.get('async', False)
884 dl.Download(factory, async=async)
Dan Shi59ae7092013-06-04 14:37:27 -0700885 finally:
886 with DevServerRoot._staging_thread_count_lock:
887 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800888 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700889
890 @cherrypy.expose
xixuan52c2fba2016-05-20 17:02:48 -0700891 def cros_au(self, **kwargs):
892 """Auto-update a CrOS DUT.
893
894 Args:
895 kwargs:
896 host_name: the hostname of the DUT to auto-update.
897 build_name: the build name for update the DUT.
898 force_update: Force an update even if the version installed is the
899 same. Default: False.
900 full_update: If True, do not run stateful update, directly force a full
901 reimage. If False, try stateful update first if the dut is already
902 installed with the same version.
903 async: Whether the auto_update function is ran in the background.
904
905 Returns:
906 A tuple includes two elements:
907 a boolean variable represents whether the auto-update process is
908 successfully started.
909 an integer represents the background auto-update process id.
910 """
911 _check_base_args_for_auto_update(kwargs)
912
913 host_name = kwargs['host_name']
914 build_name = kwargs['build_name']
915 force_update = _parse_boolean_arg(kwargs, 'force_update')
916 full_update = _parse_boolean_arg(kwargs, 'full_update')
917 async = _parse_boolean_arg(kwargs, 'async')
xixuanac89ce82016-11-30 16:48:20 -0800918 original_build = _parse_string_arg(kwargs, 'original_build')
David Haddock90e49442017-04-07 19:14:09 -0700919 payload_filename = _parse_string_arg(kwargs, 'payload_filename')
David Haddock20559612017-06-28 22:15:08 -0700920 clobber_stateful = _parse_boolean_arg(kwargs, 'clobber_stateful')
xixuan52c2fba2016-05-20 17:02:48 -0700921
922 if async:
923 path = os.path.dirname(os.path.abspath(__file__))
924 execute_file = os.path.join(path, 'cros_update.py')
925 args = (AUTO_UPDATE_CMD % (execute_file, host_name, build_name,
926 updater.static_dir))
xixuanac89ce82016-11-30 16:48:20 -0800927
928 # The original_build's format is like: link/3428.210.0
929 # The corresponding release_archive_url's format is like:
930 # gs://chromeos-releases/stable-channel/link/3428.210.0
931 if original_build:
932 release_archive_url = _build_uri_from_build_name(original_build)
933 # First staging the stateful.tgz synchronousely.
934 self.stage(files='stateful.tgz', async=False,
935 archive_url=release_archive_url)
936 args = ('%s --original_build %s' % (args, original_build))
937
xixuan52c2fba2016-05-20 17:02:48 -0700938 if force_update:
939 args = ('%s --force_update' % args)
940
941 if full_update:
942 args = ('%s --full_update' % args)
943
David Haddock90e49442017-04-07 19:14:09 -0700944 if payload_filename:
945 args = ('%s --payload_filename %s' % (args, payload_filename))
946
David Haddock20559612017-06-28 22:15:08 -0700947 if clobber_stateful:
948 args = ('%s --clobber_stateful' % args)
949
xixuan2a0970a2016-08-10 12:12:44 -0700950 p = subprocess.Popen([args], shell=True, preexec_fn=os.setsid)
951 pid = os.getpgid(p.pid)
xixuan52c2fba2016-05-20 17:02:48 -0700952
953 # Pre-write status in the track_status_file before the first call of
954 # 'get_au_status' to make sure that the track_status_file exists.
xixuan2a0970a2016-08-10 12:12:44 -0700955 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -0700956 progress_tracker.WriteStatus('CrOS update is just started.')
957
xixuan2a0970a2016-08-10 12:12:44 -0700958 return json.dumps((True, pid))
xixuan52c2fba2016-05-20 17:02:48 -0700959 else:
960 cros_update_trigger = cros_update.CrOSUpdateTrigger(
xixuanac89ce82016-11-30 16:48:20 -0800961 host_name, build_name, updater.static_dir, force_update=force_update,
962 full_update=full_update, original_build=original_build)
xixuan52c2fba2016-05-20 17:02:48 -0700963 cros_update_trigger.TriggerAU()
xixuan27d50442017-08-09 10:38:25 -0700964 return json.dumps((True, -1))
xixuan52c2fba2016-05-20 17:02:48 -0700965
966 @cherrypy.expose
967 def get_au_status(self, **kwargs):
968 """Check if the auto-update task is finished.
969
970 It handles 4 cases:
971 1. If an error exists in the track_status_file, delete the track file and
972 raise it.
973 2. If cros-update process is finished, delete the file and return the
974 success result.
975 3. If the process is not running, delete the track file and raise an error
976 about 'the process is terminated due to unknown reason'.
977 4. If the track_status_file does not exist, kill the process if it exists,
978 and raise the IOError.
979
980 Args:
981 kwargs:
982 host_name: the hostname of the DUT to auto-update.
983 pid: the background process id of cros-update.
984
985 Returns:
xixuan28d99072016-10-06 12:24:16 -0700986 A dict with three elements:
xixuan52c2fba2016-05-20 17:02:48 -0700987 a boolean variable represents whether the auto-update process is
988 finished.
989 a string represents the current auto-update process status.
990 For example, 'Transfer Devserver/Stateful Update Package'.
xixuan28d99072016-10-06 12:24:16 -0700991 a detailed error message paragraph if there exists an Auto-Update
992 error, in which the last line shows the main exception. Empty
993 string otherwise.
xixuan52c2fba2016-05-20 17:02:48 -0700994 """
995 if 'host_name' not in kwargs:
996 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
997
998 if 'pid' not in kwargs:
999 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
1000
1001 host_name = kwargs['host_name']
1002 pid = kwargs['pid']
1003 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
1004
xixuan28d99072016-10-06 12:24:16 -07001005 result_dict = {'finished': False, 'status': '', 'detailed_error_msg': ''}
xixuan52c2fba2016-05-20 17:02:48 -07001006 try:
1007 result = progress_tracker.ReadStatus()
1008 if result.startswith(cros_update_progress.ERROR_TAG):
xixuan28d99072016-10-06 12:24:16 -07001009 result_dict['detailed_error_msg'] = result[len(
1010 cros_update_progress.ERROR_TAG):]
xixuan28681fd2016-11-23 11:13:56 -08001011 elif result == cros_update_progress.FINISHED:
xixuan28d99072016-10-06 12:24:16 -07001012 result_dict['finished'] = True
1013 result_dict['status'] = result
xixuan28681fd2016-11-23 11:13:56 -08001014 elif not cros_update_progress.IsProcessAlive(pid):
xixuan28d99072016-10-06 12:24:16 -07001015 result_dict['detailed_error_msg'] = (
1016 'Cros_update process terminated midway due to unknown reason. '
1017 'Last update status was %s' % result)
xixuan28681fd2016-11-23 11:13:56 -08001018 else:
1019 result_dict['status'] = result
1020 except IOError as e:
1021 if pid and cros_update_progress.IsProcessAlive(pid):
xixuan2a0970a2016-08-10 12:12:44 -07001022 os.killpg(int(pid), signal.SIGKILL)
xixuan52c2fba2016-05-20 17:02:48 -07001023
xixuan28681fd2016-11-23 11:13:56 -08001024 result_dict['detailed_error_msg'] = str(e)
1025
1026 return json.dumps(result_dict)
xixuan52c2fba2016-05-20 17:02:48 -07001027
1028 @cherrypy.expose
1029 def handler_cleanup(self, **kwargs):
xixuan3bc974e2016-10-18 17:21:43 -07001030 """Clean track status log and temp directory for CrOS auto-update process.
xixuan52c2fba2016-05-20 17:02:48 -07001031
1032 Args:
1033 kwargs:
1034 host_name: the hostname of the DUT to auto-update.
1035 pid: the background process id of cros-update.
1036 """
1037 if 'host_name' not in kwargs:
1038 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
1039
1040 if 'pid' not in kwargs:
1041 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
1042
1043 host_name = kwargs['host_name']
1044 pid = kwargs['pid']
1045 cros_update_progress.DelTrackStatusFile(host_name, pid)
xixuan3bc974e2016-10-18 17:21:43 -07001046 cros_update_progress.DelAUTempDirectory(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -07001047
1048 @cherrypy.expose
1049 def kill_au_proc(self, **kwargs):
1050 """Kill CrOS auto-update process using given process id.
1051
1052 Args:
1053 kwargs:
1054 host_name: Kill all the CrOS auto-update process of this host.
1055
1056 Returns:
1057 True if all processes are killed properly.
1058 """
1059 if 'host_name' not in kwargs:
1060 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
1061
xixuan447ad9d2017-02-28 14:46:20 -08001062 cur_pid = kwargs.get('pid')
1063
xixuan52c2fba2016-05-20 17:02:48 -07001064 host_name = kwargs['host_name']
xixuan3bc974e2016-10-18 17:21:43 -07001065 track_log_list = cros_update_progress.GetAllTrackStatusFileByHostName(
1066 host_name)
xixuan52c2fba2016-05-20 17:02:48 -07001067 for log in track_log_list:
1068 # The track log's full path is: path/host_name_pid.log
1069 # Use splitext to remove file extension, then parse pid from the
1070 # filename.
1071 pid = os.path.splitext(os.path.basename(log))[0][len(host_name)+1:]
xixuan447ad9d2017-02-28 14:46:20 -08001072 _clear_process(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -07001073
xixuan447ad9d2017-02-28 14:46:20 -08001074 if cur_pid:
1075 _clear_process(host_name, cur_pid)
xixuan52c2fba2016-05-20 17:02:48 -07001076
1077 return 'True'
1078
1079 @cherrypy.expose
1080 def collect_cros_au_log(self, **kwargs):
1081 """Collect CrOS auto-update log.
1082
1083 Args:
1084 kwargs:
1085 host_name: the hostname of the DUT to auto-update.
1086 pid: the background process id of cros-update.
1087
1088 Returns:
David Haddock9f459632017-05-11 14:45:46 -07001089 A dictionary containing the execute log file and any hostlog files.
xixuan52c2fba2016-05-20 17:02:48 -07001090 """
1091 if 'host_name' not in kwargs:
1092 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
1093
1094 if 'pid' not in kwargs:
1095 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
1096
1097 host_name = kwargs['host_name']
1098 pid = kwargs['pid']
xixuan3bc974e2016-10-18 17:21:43 -07001099
1100 # Fetch the execute log recorded by cros_update_progress.
xixuan1bbfaba2016-10-13 17:53:22 -07001101 au_log = cros_update_progress.ReadExecuteLogFile(host_name, pid)
1102 cros_update_progress.DelExecuteLogFile(host_name, pid)
David Haddock9f459632017-05-11 14:45:46 -07001103 # Fetch the cros_au host_logs if they exist
1104 au_hostlogs = cros_update_progress.ReadAUHostLogFiles(host_name, pid)
1105 return json.dumps({'cros_au_log': au_log, 'host_logs': au_hostlogs})
xixuan1bbfaba2016-10-13 17:53:22 -07001106
xixuan52c2fba2016-05-20 17:02:48 -07001107 @cherrypy.expose
Dan Shi2f136862016-02-11 15:38:38 -08001108 def locate_file(self, **kwargs):
1109 """Get the path to the given file name.
1110
1111 This method looks up the given file name inside specified build artifacts.
1112 One use case is to help caller to locate an apk file inside a build
1113 artifact. The location of the apk file could be different based on the
1114 branch and target.
1115
1116 Args:
1117 file_name: Name of the file to look for.
1118 artifacts: A list of artifact names to search for the file.
1119
1120 Returns:
1121 Path to the file with the given name. It's relative to the folder for the
1122 build, e.g., DATA/priv-app/sl4a/sl4a.apk
Dan Shi2f136862016-02-11 15:38:38 -08001123 """
1124 dl, _ = _get_downloader_and_factory(kwargs)
1125 try:
Joe Brennan1691f8e2017-03-15 15:53:36 -07001126 file_name = kwargs['file_name']
Dan Shi2f136862016-02-11 15:38:38 -08001127 artifacts = kwargs['artifacts']
1128 except KeyError:
1129 raise DevServerError('`file_name` and `artifacts` are required to search '
1130 'for a file in build artifacts.')
1131 build_path = dl.GetBuildDir()
1132 for artifact in artifacts:
1133 # Get the unzipped folder of the artifact. If it's not defined in
1134 # ARTIFACT_UNZIP_FOLDER_MAP, assume the files are unzipped to the build
1135 # directory directly.
1136 folder = artifact_info.ARTIFACT_UNZIP_FOLDER_MAP.get(artifact, '')
1137 artifact_path = os.path.join(build_path, folder)
1138 for root, _, filenames in os.walk(artifact_path):
Joe Brennan1691f8e2017-03-15 15:53:36 -07001139 if file_name in set([f for f in filenames]):
Dan Shi2f136862016-02-11 15:38:38 -08001140 return os.path.relpath(os.path.join(root, file_name), build_path)
1141 raise DevServerError('File `%s` can not be found in artifacts: %s' %
1142 (file_name, artifacts))
1143
1144 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -08001145 def setup_telemetry(self, **kwargs):
1146 """Extracts and sets up telemetry
1147
1148 This method goes through the telemetry deps packages, and stages them on
1149 the devserver to be used by the drones and the telemetry tests.
1150
1151 Args:
1152 archive_url: Google Storage URL for the build.
1153
1154 Returns:
1155 Path to the source folder for the telemetry codebase once it is staged.
1156 """
Gabe Black3b567202015-09-23 14:07:59 -07001157 dl = _get_downloader(kwargs)
Simran Basi4baad082013-02-14 13:39:18 -08001158
Gabe Black3b567202015-09-23 14:07:59 -07001159 build_path = dl.GetBuildDir()
Simran Basi4baad082013-02-14 13:39:18 -08001160 deps_path = os.path.join(build_path, 'autotest/packages')
1161 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
1162 src_folder = os.path.join(telemetry_path, 'src')
1163
1164 with self._telemetry_lock_dict.lock(telemetry_path):
1165 if os.path.exists(src_folder):
1166 # Telemetry is already fully stage return
1167 return src_folder
1168
1169 common_util.MkDirP(telemetry_path)
1170
1171 # Copy over the required deps tar balls to the telemetry directory.
1172 for dep in TELEMETRY_DEPS:
1173 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -07001174 if not os.path.exists(dep_path):
1175 # This dep does not exist (could be new), do not extract it.
1176 continue
Simran Basi4baad082013-02-14 13:39:18 -08001177 try:
1178 common_util.ExtractTarball(dep_path, telemetry_path)
1179 except common_util.CommonUtilError as e:
1180 shutil.rmtree(telemetry_path)
1181 raise DevServerError(str(e))
1182
1183 # By default all the tarballs extract to test_src but some parts of
1184 # the telemetry code specifically hardcoded to exist inside of 'src'.
1185 test_src = os.path.join(telemetry_path, 'test_src')
1186 try:
1187 shutil.move(test_src, src_folder)
1188 except shutil.Error:
1189 # This can occur if src_folder already exists. Remove and retry move.
1190 shutil.rmtree(src_folder)
Gabe Black3b567202015-09-23 14:07:59 -07001191 raise DevServerError(
1192 'Failure in telemetry setup for build %s. Appears that the '
1193 'test_src to src move failed.' % dl.GetBuild())
Simran Basi4baad082013-02-14 13:39:18 -08001194
1195 return src_folder
1196
1197 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -08001198 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -07001199 """Symbolicates a minidump using pre-downloaded symbols, returns it.
1200
1201 Callers will need to POST to this URL with a body of MIME-type
1202 "multipart/form-data".
1203 The body should include a single argument, 'minidump', containing the
1204 binary-formatted minidump to symbolicate.
1205
Chris Masone816e38c2012-05-02 12:22:36 -07001206 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -08001207 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -07001208 minidump: The binary minidump file to symbolicate.
1209 """
Chris Sosa76e44b92013-01-31 12:11:38 -08001210 # Ensure the symbols have been staged.
Dan Shif08fe492016-10-04 14:39:25 -07001211 # Try debug.tar.xz first, then debug.tgz
1212 for artifact in (artifact_info.SYMBOLS_ONLY, artifact_info.SYMBOLS):
1213 kwargs['artifacts'] = artifact
1214 dl = _get_downloader(kwargs)
1215
1216 try:
1217 if self.stage(**kwargs) == 'Success':
1218 break
1219 except build_artifact.ArtifactDownloadError:
1220 continue
1221 else:
Gabe Black3b567202015-09-23 14:07:59 -07001222 raise DevServerError('Failed to stage symbols for %s' %
1223 dl.DescribeSource())
Chris Sosa76e44b92013-01-31 12:11:38 -08001224
Chris Masone816e38c2012-05-02 12:22:36 -07001225 to_return = ''
1226 with tempfile.NamedTemporaryFile() as local:
1227 while True:
1228 data = minidump.file.read(8192)
1229 if not data:
1230 break
1231 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -08001232
Chris Masone816e38c2012-05-02 12:22:36 -07001233 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -08001234
Gabe Black3b567202015-09-23 14:07:59 -07001235 symbols_directory = os.path.join(dl.GetBuildDir(), 'debug', 'breakpad')
Chris Sosa76e44b92013-01-31 12:11:38 -08001236
xixuanab744382017-04-27 10:41:27 -07001237 # The location of minidump_stackwalk is defined in chromeos-admin.
Chris Sosa76e44b92013-01-31 12:11:38 -08001238 stackwalk = subprocess.Popen(
xixuanab744382017-04-27 10:41:27 -07001239 ['/usr/local/bin/minidump_stackwalk', local.name, symbols_directory],
Chris Sosa76e44b92013-01-31 12:11:38 -08001240 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1241
Chris Masone816e38c2012-05-02 12:22:36 -07001242 to_return, error_text = stackwalk.communicate()
1243 if stackwalk.returncode != 0:
1244 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
1245 error_text, stackwalk.returncode))
1246
1247 return to_return
1248
1249 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -08001250 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 15:31:36 -04001251 """Return a string representing the latest build for a given target.
1252
1253 Args:
1254 target: The build target, typically a combination of the board and the
1255 type of build e.g. x86-mario-release.
1256 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
1257 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-07 18:21:26 -08001258
Scott Zawalski16954532012-03-20 15:31:36 -04001259 Returns:
1260 A string representation of the latest build if one exists, i.e.
1261 R19-1993.0.0-a1-b1480.
1262 An empty string if no latest could be found.
1263 """
Don Garrettf84631a2014-01-07 18:21:26 -08001264 if not kwargs:
Scott Zawalski16954532012-03-20 15:31:36 -04001265 return _PrintDocStringAsHTML(self.latestbuild)
1266
Don Garrettf84631a2014-01-07 18:21:26 -08001267 if 'target' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -07001268 raise common_util.DevServerHTTPError(500, 'Error: target= is required!')
Dan Shi61305df2015-10-26 16:52:35 -07001269
1270 if _is_android_build_request(kwargs):
1271 branch = kwargs.get('branch', None)
1272 target = kwargs.get('target', None)
1273 if not target or not branch:
1274 raise DevServerError(
xixuan52c2fba2016-05-20 17:02:48 -07001275 'Both target and branch must be specified to query for the latest '
1276 'Android build.')
Dan Shi61305df2015-10-26 16:52:35 -07001277 return android_build.BuildAccessor.GetLatestBuildID(target, branch)
1278
Scott Zawalski16954532012-03-20 15:31:36 -04001279 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001280 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-07 18:21:26 -08001281 updater.static_dir, kwargs['target'],
1282 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -07001283 except common_util.CommonUtilError as errmsg:
Chris Sosa4b951602014-04-09 20:26:07 -07001284 raise common_util.DevServerHTTPError(500, str(errmsg))
Scott Zawalski16954532012-03-20 15:31:36 -04001285
1286 @cherrypy.expose
xixuan7efd0002016-04-14 15:34:01 -07001287 def list_suite_controls(self, **kwargs):
1288 """Return a list of contents of all known control files.
1289
1290 Example URL:
1291 To List all control files' content:
1292 http://dev-server/list_suite_controls?suite_name=bvt&
1293 build=daisy_spring-release/R29-4279.0.0
1294
1295 Args:
1296 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
1297 suite_name: List the control files belonging to that suite.
1298
1299 Returns:
Dan Shia1cd6522016-04-18 16:07:21 -07001300 A dictionary of all control files's path to its content for given suite.
xixuan7efd0002016-04-14 15:34:01 -07001301 """
1302 if not kwargs:
1303 return _PrintDocStringAsHTML(self.controlfiles)
1304
1305 if 'build' not in kwargs:
1306 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
1307
1308 if 'suite_name' not in kwargs:
Dan Shia1cd6522016-04-18 16:07:21 -07001309 raise common_util.DevServerHTTPError(500,
1310 'Error: suite_name= is required!')
xixuan7efd0002016-04-14 15:34:01 -07001311
1312 control_file_list = [
1313 line.rstrip() for line in common_util.GetControlFileListForSuite(
1314 updater.static_dir, kwargs['build'],
1315 kwargs['suite_name']).splitlines()]
1316
Dan Shia1cd6522016-04-18 16:07:21 -07001317 control_file_content_dict = {}
xixuan7efd0002016-04-14 15:34:01 -07001318 for control_path in control_file_list:
Dan Shia1cd6522016-04-18 16:07:21 -07001319 control_file_content_dict[control_path] = (common_util.GetControlFile(
xixuan7efd0002016-04-14 15:34:01 -07001320 updater.static_dir, kwargs['build'], control_path))
1321
Dan Shia1cd6522016-04-18 16:07:21 -07001322 return json.dumps(control_file_content_dict)
xixuan7efd0002016-04-14 15:34:01 -07001323
1324 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -08001325 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 17:17:28 -05001326 """Return a control file or a list of all known control files.
1327
1328 Example URL:
1329 To List all control files:
beepsbd337242013-07-09 22:44:06 -07001330 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
1331 To List all control files for, say, the bvt suite:
1332 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -05001333 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -05001334 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 -05001335
1336 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -05001337 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -05001338 control_path: If you want the contents of a control file set this
1339 to the path. E.g. client/site_tests/sleeptest/control
1340 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -07001341 suite_name: If control_path is not specified but a suite_name is
1342 specified, list the control files belonging to that suite instead of
1343 all control files. The empty string for suite_name will list all control
1344 files for the build.
Don Garrettf84631a2014-01-07 18:21:26 -08001345
Scott Zawalski4647ce62012-01-03 17:17:28 -05001346 Returns:
1347 Contents of a control file if control_path is provided.
1348 A list of control files if no control_path is provided.
1349 """
Don Garrettf84631a2014-01-07 18:21:26 -08001350 if not kwargs:
Scott Zawalski4647ce62012-01-03 17:17:28 -05001351 return _PrintDocStringAsHTML(self.controlfiles)
1352
Don Garrettf84631a2014-01-07 18:21:26 -08001353 if 'build' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -07001354 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -05001355
Don Garrettf84631a2014-01-07 18:21:26 -08001356 if 'control_path' not in kwargs:
1357 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-09 22:44:06 -07001358 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-07 18:21:26 -08001359 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-09 22:44:06 -07001360 else:
1361 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-07 18:21:26 -08001362 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -05001363 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001364 return common_util.GetControlFile(
Don Garrettf84631a2014-01-07 18:21:26 -08001365 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -08001366
1367 @cherrypy.expose
Simran Basi99e63c02014-05-20 10:39:52 -07001368 def xbuddy_translate(self, *args, **kwargs):
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001369 """Translates an xBuddy path to a real path to artifact if it exists.
1370
1371 Args:
Simran Basi99e63c02014-05-20 10:39:52 -07001372 args: An xbuddy path in the form of {local|remote}/build_id/artifact.
1373 Local searches the devserver's static directory. Remote searches a
1374 Google Storage image archive.
1375
1376 Kwargs:
1377 image_dir: Google Storage image archive to search in if requesting a
1378 remote artifact. If none uses the default bucket.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001379
1380 Returns:
Simran Basi99e63c02014-05-20 10:39:52 -07001381 String in the format of build_id/artifact as stored on the local server
1382 or in Google Storage.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001383 """
Simran Basi99e63c02014-05-20 10:39:52 -07001384 build_id, filename = self._xbuddy.Translate(
Gabe Black3b567202015-09-23 14:07:59 -07001385 args, image_dir=kwargs.get('image_dir'))
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001386 response = os.path.join(build_id, filename)
1387 _Log('Path translation requested, returning: %s', response)
1388 return response
1389
1390 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -07001391 def xbuddy(self, *args, **kwargs):
1392 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -07001393
1394 Args:
joycheneaf4cfc2013-07-02 08:38:57 -07001395 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -07001396 components of the path. The path can be understood as
1397 "{local|remote}/build_id/artifact" where build_id is composed of
1398 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -07001399
joychen121fc9b2013-08-02 14:30:30 -07001400 The first path element is optional, and can be "remote" or "local"
1401 If local (the default), devserver will not attempt to access Google
1402 Storage, and will only search the static directory for the files.
1403 If remote, devserver will try to obtain the artifact off GS if it's
1404 not found locally.
1405 The board is the familiar board name, optionally suffixed.
1406 The version can be the google storage version number, and may also be
1407 any of a number of xBuddy defined version aliases that will be
1408 translated into the latest built image that fits the description.
1409 Defaults to latest.
1410 The artifact is one of a number of image or artifact aliases used by
1411 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -07001412
1413 Kwargs:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001414 for_update: {true|false}
1415 if true, pregenerates the update payloads for the image,
1416 and returns the update uri to pass to the
1417 update_engine_client.
joychen3cb228e2013-06-12 12:13:13 -07001418 return_dir: {true|false}
1419 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001420 relative_path: {true|false}
1421 if set to true, returns the relative path to the payload
1422 directory from static_dir.
joychen3cb228e2013-06-12 12:13:13 -07001423 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -07001424 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -07001425 or
joycheneaf4cfc2013-07-02 08:38:57 -07001426 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -07001427
1428 Returns:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001429 If |for_update|, returns a redirect to the image or update file
1430 on the devserver. E.g.,
1431 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
1432 chromium-test-image.bin
1433 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
1434 http://host:port/static/x86-generic-release/R26-4000.0.0/
1435 If |relative_path| is true, return a relative path the folder where the
1436 payloads are. E.g.,
1437 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 12:13:13 -07001438 """
Chris Sosa75490802013-09-30 17:21:45 -07001439 boolean_string = kwargs.get('for_update')
1440 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001441 boolean_string = kwargs.get('return_dir')
1442 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
1443 boolean_string = kwargs.get('relative_path')
1444 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -07001445
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001446 if return_dir and relative_path:
Chris Sosa4b951602014-04-09 20:26:07 -07001447 raise common_util.DevServerHTTPError(
1448 500, 'Cannot specify both return_dir and relative_path')
Chris Sosa75490802013-09-30 17:21:45 -07001449
1450 # For updates, we optimize downloading of test images.
1451 file_name = None
1452 build_id = None
1453 if for_update:
1454 try:
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001455 build_id = self._xbuddy.StageTestArtifactsForUpdate(args)
Chris Sosa75490802013-09-30 17:21:45 -07001456 except build_artifact.ArtifactDownloadError:
1457 build_id = None
1458
1459 if not build_id:
1460 build_id, file_name = self._xbuddy.Get(args)
1461
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001462 if for_update:
1463 _Log('Payload generation triggered by request')
1464 # Forces payload to be in cache and symlinked into build_id dir.
Chris Sosa75490802013-09-30 17:21:45 -07001465 updater.GetUpdateForLabel(autoupdate.FORCED_UPDATE, build_id,
1466 image_name=file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001467
1468 response = None
1469 if return_dir:
1470 response = os.path.join(cherrypy.request.base, 'static', build_id)
1471 _Log('Directory requested, returning: %s', response)
1472 elif relative_path:
1473 response = build_id
1474 _Log('Relative path requested, returning: %s', response)
1475 elif for_update:
1476 response = os.path.join(cherrypy.request.base, 'update', build_id)
1477 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 12:13:13 -07001478 else:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001479 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 14:30:30 -07001480 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001481 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 14:30:30 -07001482 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -07001483
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001484 return response
1485
joychen3cb228e2013-06-12 12:13:13 -07001486 @cherrypy.expose
1487 def xbuddy_list(self):
1488 """Lists the currently available images & time since last access.
1489
Gilad Arnold452fd272014-02-04 11:09:28 -08001490 Returns:
1491 A string representation of a list of tuples [(build_id, time since last
1492 access),...]
joychen3cb228e2013-06-12 12:13:13 -07001493 """
1494 return self._xbuddy.List()
1495
1496 @cherrypy.expose
1497 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 11:09:28 -08001498 """Returns the number of images cached by xBuddy."""
joychen3cb228e2013-06-12 12:13:13 -07001499 return self._xbuddy.Capacity()
1500
1501 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001502 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001503 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001504 return ('Welcome to the Dev Server!<br>\n'
1505 '<br>\n'
1506 'Here are the available methods, click for documentation:<br>\n'
1507 '<br>\n'
1508 '%s' %
1509 '<br>\n'.join(
1510 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001511 for name in _FindExposedMethods(
1512 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001513
1514 @cherrypy.expose
1515 def doc(self, *args):
1516 """Shows the documentation for available methods / URLs.
1517
1518 Example:
1519 http://myhost/doc/update
1520 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001521 name = '/'.join(args)
1522 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001523 if not method:
1524 raise DevServerError("No exposed method named `%s'" % name)
1525 if not method.__doc__:
1526 raise DevServerError("No documentation for exposed method `%s'" % name)
1527 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -07001528
Dale Curtisc9aaf3a2011-08-09 15:47:40 -07001529 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001530 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001531 """Handles an update check from a Chrome OS client.
1532
1533 The HTTP request should contain the standard Omaha-style XML blob. The URL
1534 line may contain an additional intermediate path to the update payload.
1535
joychen121fc9b2013-08-02 14:30:30 -07001536 This request can be handled in one of 4 ways, depending on the devsever
1537 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -07001538
joychen121fc9b2013-08-02 14:30:30 -07001539 1. No intermediate path
1540 If no intermediate path is given, the default behavior is to generate an
1541 update payload from the latest test image locally built for the board
1542 specified in the xml. Devserver serves the generated payload.
1543
1544 2. Path explicitly invokes XBuddy
1545 If there is a path given, it can explicitly invoke xbuddy by prefixing it
1546 with 'xbuddy'. This path is then used to acquire an image binary for the
1547 devserver to generate an update payload from. Devserver then serves this
1548 payload.
1549
1550 3. Path is left for the devserver to interpret.
1551 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
1552 to generate a payload from the test image in that directory and serve it.
1553
1554 4. The devserver is in a 'forced' mode. TO BE DEPRECATED
1555 This comes from the usage of --forced_payload or --image when starting the
1556 devserver. No matter what path (or no path) gets passed in, devserver will
1557 serve the update payload (--forced_payload) or generate an update payload
1558 from the image (--image).
1559
1560 Examples:
1561 1. No intermediate path
1562 update_engine_client --omaha_url=http://myhost/update
1563 This generates an update payload from the latest test image locally built
1564 for the board specified in the xml.
1565
1566 2. Explicitly invoke xbuddy
1567 update_engine_client --omaha_url=
1568 http://myhost/update/xbuddy/remote/board/version/dev
1569 This would go to GS to download the dev image for the board, from which
1570 the devserver would generate a payload to serve.
1571
1572 3. Give a path for devserver to interpret
1573 update_engine_client --omaha_url=http://myhost/update/some/random/path
1574 This would attempt, in order to:
1575 a) Generate an update from a test image binary if found in
1576 static_dir/some/random/path.
1577 b) Serve an update payload found in static_dir/some/random/path.
1578 c) Hope that some/random/path takes the form "board/version" and
1579 and attempt to download an update payload for that board/version
1580 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001581 """
joychen121fc9b2013-08-02 14:30:30 -07001582 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -08001583 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -07001584 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-11 19:49:01 -07001585
joychen121fc9b2013-08-02 14:30:30 -07001586 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001587
Dan Shiafd0e492015-05-27 14:23:51 -07001588 @require_psutil()
1589 def _get_io_stats(self):
1590 """Get the IO stats as a dictionary.
1591
Gabe Black3b567202015-09-23 14:07:59 -07001592 Returns:
1593 A dictionary of IO stats collected by psutil.
Dan Shiafd0e492015-05-27 14:23:51 -07001594 """
1595 return {'disk_read_bytes_per_second': self.disk_read_bytes_per_sec,
1596 'disk_write_bytes_per_second': self.disk_write_bytes_per_sec,
1597 'disk_total_bytes_per_second': (self.disk_read_bytes_per_sec +
1598 self.disk_write_bytes_per_sec),
1599 'network_sent_bytes_per_second': self.network_sent_bytes_per_sec,
1600 'network_recv_bytes_per_second': self.network_recv_bytes_per_sec,
1601 'network_total_bytes_per_second': (self.network_sent_bytes_per_sec +
1602 self.network_recv_bytes_per_sec),
1603 'cpu_percent': psutil.cpu_percent(),}
1604
Dan Shi7247f9c2016-06-01 09:19:09 -07001605
1606 def _get_process_count(self, process_cmd_pattern):
1607 """Get the count of processes that match the given command pattern.
1608
1609 Args:
1610 process_cmd_pattern: The regex pattern of process command to match.
1611
1612 Returns:
1613 The count of processes that match the given command pattern.
1614 """
1615 try:
xixuanac89ce82016-11-30 16:48:20 -08001616 # Use Popen instead of check_output since the latter cannot run with old
1617 # python version (less than 2.7)
1618 proc = subprocess.Popen(
1619 'pgrep -fc "%s"' % process_cmd_pattern,
1620 stdout=subprocess.PIPE,
1621 stderr=subprocess.PIPE,
1622 shell=True)
1623 cmd_output, cmd_error = proc.communicate()
1624 if cmd_error:
1625 _Log('Error happened when getting process count: %s' % cmd_error)
1626
1627 return int(cmd_output)
Dan Shi7247f9c2016-06-01 09:19:09 -07001628 except subprocess.CalledProcessError:
1629 return 0
1630
1631
Dan Shif5ce2de2013-04-25 16:06:32 -07001632 @cherrypy.expose
1633 def check_health(self):
1634 """Collect the health status of devserver to see if it's ready for staging.
1635
Gilad Arnold452fd272014-02-04 11:09:28 -08001636 Returns:
1637 A JSON dictionary containing all or some of the following fields:
1638 free_disk (int): free disk space in GB
1639 staging_thread_count (int): number of devserver threads currently staging
1640 an image
Dan Shi7247f9c2016-06-01 09:19:09 -07001641 apache_client_count (int): count of Apache processes.
1642 telemetry_test_count (int): count of telemetry tests.
1643 gsutil_count (int): count of gsutil processes.
Dan Shif5ce2de2013-04-25 16:06:32 -07001644 """
1645 # Get free disk space.
1646 stat = os.statvfs(updater.static_dir)
1647 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
Dan Shi7247f9c2016-06-01 09:19:09 -07001648 apache_client_count = self._get_process_count('apache')
1649 telemetry_test_count = self._get_process_count('python.*telemetry')
1650 gsutil_count = self._get_process_count('gsutil')
xixuan447ad9d2017-02-28 14:46:20 -08001651 au_process_count = len(cros_update_progress.GetAllRunningAUProcess())
Dan Shif5ce2de2013-04-25 16:06:32 -07001652
Dan Shiafd0e492015-05-27 14:23:51 -07001653 health_data = {
Dan Shif5ce2de2013-04-25 16:06:32 -07001654 'free_disk': free_disk,
Dan Shid76e6bb2016-01-28 22:28:51 -08001655 'staging_thread_count': DevServerRoot._staging_thread_count,
1656 'apache_client_count': apache_client_count,
Dan Shi7247f9c2016-06-01 09:19:09 -07001657 'telemetry_test_count': telemetry_test_count,
xixuan447ad9d2017-02-28 14:46:20 -08001658 'gsutil_count': gsutil_count,
1659 'au_process_count': au_process_count,
1660 }
Dan Shiafd0e492015-05-27 14:23:51 -07001661 health_data.update(self._get_io_stats() or {})
1662
1663 return json.dumps(health_data)
Dan Shif5ce2de2013-04-25 16:06:32 -07001664
1665
Chris Sosadbc20082012-12-10 13:39:11 -08001666def _CleanCache(cache_dir, wipe):
1667 """Wipes any excess cached items in the cache_dir.
1668
1669 Args:
1670 cache_dir: the directory we are wiping from.
1671 wipe: If True, wipe all the contents -- not just the excess.
1672 """
1673 if wipe:
1674 # Clear the cache and exit on error.
1675 cmd = 'rm -rf %s/*' % cache_dir
1676 if os.system(cmd) != 0:
1677 _Log('Failed to clear the cache with %s' % cmd)
1678 sys.exit(1)
1679 else:
1680 # Clear all but the last N cached updates
1681 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
1682 (cache_dir, CACHED_ENTRIES))
1683 if os.system(cmd) != 0:
1684 _Log('Failed to clean up old delta cache files with %s' % cmd)
1685 sys.exit(1)
1686
1687
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001688def _AddTestingOptions(parser):
1689 group = optparse.OptionGroup(
1690 parser, 'Advanced Testing Options', 'These are used by test scripts and '
1691 'developers writing integration tests utilizing the devserver. They are '
1692 'not intended to be really used outside the scope of someone '
1693 'knowledgable about the test.')
1694 group.add_option('--exit',
1695 action='store_true',
1696 help='do not start the server (yet pregenerate/clear cache)')
1697 group.add_option('--host_log',
1698 action='store_true', default=False,
1699 help='record history of host update events (/api/hostlog)')
1700 group.add_option('--max_updates',
Gabe Black3b567202015-09-23 14:07:59 -07001701 metavar='NUM', default=-1, type='int',
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001702 help='maximum number of update checks handled positively '
1703 '(default: unlimited)')
1704 group.add_option('--private_key',
1705 metavar='PATH', default=None,
1706 help='path to the private key in pem format. If this is set '
1707 'the devserver will generate update payloads that are '
1708 'signed with this key.')
David Zeuthen52ccd012013-10-31 12:58:26 -07001709 group.add_option('--private_key_for_metadata_hash_signature',
1710 metavar='PATH', default=None,
1711 help='path to the private key in pem format. If this is set '
1712 'the devserver will sign the metadata hash with the given '
1713 'key and transmit in the Omaha-style XML response.')
1714 group.add_option('--public_key',
1715 metavar='PATH', default=None,
1716 help='path to the public key in pem format. If this is set '
1717 'the devserver will transmit a base64 encoded version of '
1718 'the content in the Omaha-style XML response.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001719 group.add_option('--proxy_port',
1720 metavar='PORT', default=None, type='int',
1721 help='port to have the client connect to -- basically the '
1722 'devserver lies to the update to tell it to get the payload '
1723 'from a different port that will proxy the request back to '
1724 'the devserver. The proxy must be managed outside the '
1725 'devserver.')
1726 group.add_option('--remote_payload',
1727 action='store_true', default=False,
Chris Sosa4b951602014-04-09 20:26:07 -07001728 help='Payload is being served from a remote machine. With '
1729 'this setting enabled, this devserver instance serves as '
1730 'just an Omaha server instance. In this mode, the '
1731 'devserver enforces a few extra components of the Omaha '
Chris Sosafc715442014-04-09 20:45:23 -07001732 'protocol, such as hardware class, being sent.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001733 group.add_option('-u', '--urlbase',
1734 metavar='URL',
Gabe Black3b567202015-09-23 14:07:59 -07001735 help='base URL for update images, other than the '
1736 'devserver. Use in conjunction with remote_payload.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001737 parser.add_option_group(group)
1738
1739
1740def _AddUpdateOptions(parser):
1741 group = optparse.OptionGroup(
1742 parser, 'Autoupdate Options', 'These options can be used to change '
1743 'how the devserver either generates or serve update payloads. Please '
1744 'note that all of these option affect how a payload is generated and so '
1745 'do not work in archive-only mode.')
1746 group.add_option('--board',
1747 help='By default the devserver will create an update '
1748 'payload from the latest image built for the board '
1749 'a device that is requesting an update has. When we '
1750 'pre-generate an update (see below) and we do not specify '
1751 'another update_type option like image or payload, the '
1752 'devserver needs to know the board to generate the latest '
1753 'image for. This is that board.')
1754 group.add_option('--critical_update',
1755 action='store_true', default=False,
1756 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001757 group.add_option('--image',
1758 metavar='FILE',
1759 help='Generate and serve an update using this image to any '
1760 'device that requests an update.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001761 group.add_option('--payload',
1762 metavar='PATH',
1763 help='use the update payload from specified directory '
1764 '(update.gz).')
1765 group.add_option('-p', '--pregenerate_update',
1766 action='store_true', default=False,
1767 help='pre-generate the update payload before accepting '
1768 'update requests. Useful to help debug payload generation '
1769 'issues quickly. Also if an update payload will take a '
1770 'long time to generate, a client may timeout if you do not'
1771 'pregenerate the update.')
1772 group.add_option('--src_image',
1773 metavar='PATH', default='',
1774 help='If specified, delta updates will be generated using '
1775 'this image as the source image. Delta updates are when '
1776 'you are updating from a "source image" to a another '
1777 'image.')
1778 parser.add_option_group(group)
1779
1780
1781def _AddProductionOptions(parser):
1782 group = optparse.OptionGroup(
1783 parser, 'Advanced Server Options', 'These options can be used to changed '
1784 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001785 group.add_option('--clear_cache',
1786 action='store_true', default=False,
1787 help='At startup, removes all cached entries from the'
1788 'devserver\'s cache.')
1789 group.add_option('--logfile',
1790 metavar='PATH',
1791 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -07001792 group.add_option('--pidfile',
1793 metavar='PATH',
1794 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 11:04:13 -08001795 group.add_option('--portfile',
1796 metavar='PATH',
1797 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001798 group.add_option('--production',
1799 action='store_true', default=False,
1800 help='have the devserver use production values when '
1801 'starting up. This includes using more threads and '
1802 'performing less logging.')
1803 parser.add_option_group(group)
1804
1805
Paul Hobbsef4e0702016-06-27 17:01:42 -07001806def MakeLogHandler(logfile):
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001807 """Create a LogHandler instance used to log all messages."""
1808 hdlr_cls = handlers.TimedRotatingFileHandler
1809 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
xixuan3d48bff2017-01-30 19:00:09 -08001810 interval=_LOG_ROTATION_INTERVAL,
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001811 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -07001812 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001813 return hdlr
1814
1815
Chris Sosacde6bf42012-05-31 18:36:39 -07001816def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001817 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -08001818 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -07001819
1820 # get directory that the devserver is run from
1821 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -07001822 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -07001823 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001824 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -07001825 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -07001826 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001827 parser.add_option('--port',
1828 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 13:13:28 -08001829 help=('port for the dev server to use; if zero, binds to '
1830 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001831 parser.add_option('-t', '--test_image',
1832 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -07001833 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -07001834 parser.add_option('-x', '--xbuddy_manage_builds',
1835 action='store_true',
1836 default=False,
1837 help='If set, allow xbuddy to manage images in'
1838 'build/images.')
Dan Shi72b16132015-10-08 12:10:33 -07001839 parser.add_option('-a', '--android_build_credential',
1840 default=None,
1841 help='Path to a json file which contains the credential '
1842 'needed to access Android builds.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001843 _AddProductionOptions(parser)
1844 _AddUpdateOptions(parser)
1845 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -07001846 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001847
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001848 # Handle options that must be set globally in cherrypy. Do this
1849 # work up front, because calls to _Log() below depend on this
1850 # initialization.
1851 if options.production:
1852 cherrypy.config.update({'environment': 'production'})
1853 if not options.logfile:
1854 cherrypy.config.update({'log.screen': True})
1855 else:
1856 cherrypy.config.update({'log.error_file': '',
1857 'log.access_file': ''})
Paul Hobbsef4e0702016-06-27 17:01:42 -07001858 hdlr = MakeLogHandler(options.logfile)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001859 # Pylint can't seem to process these two calls properly
1860 # pylint: disable=E1101
1861 cherrypy.log.access_log.addHandler(hdlr)
1862 cherrypy.log.error_log.addHandler(hdlr)
1863 # pylint: enable=E1101
1864
joychened64b222013-06-21 16:39:34 -07001865 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001866 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001867
joychened64b222013-06-21 16:39:34 -07001868 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001869 # If our devserver is only supposed to serve payloads, we shouldn't be
1870 # mucking with the cache at all. If the devserver hadn't previously
1871 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001872 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001873 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001874 else:
1875 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001876
Chris Sosadbc20082012-12-10 13:39:11 -08001877 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 16:39:34 -07001878 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001879
joychen121fc9b2013-08-02 14:30:30 -07001880 _xbuddy = xbuddy.XBuddy(options.xbuddy_manage_builds,
1881 options.board,
joychen121fc9b2013-08-02 14:30:30 -07001882 static_dir=options.static_dir)
Chris Sosa75490802013-09-30 17:21:45 -07001883 if options.clear_cache and options.xbuddy_manage_builds:
1884 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 14:30:30 -07001885
Chris Sosa6a3697f2013-01-29 16:44:43 -08001886 # We allow global use here to share with cherrypy classes.
1887 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001888 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001889 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001890 _xbuddy,
joychened64b222013-06-21 16:39:34 -07001891 static_dir=options.static_dir,
Andrew de los Reyes52620802010-04-12 13:40:07 -07001892 urlbase=options.urlbase,
Chris Sosa5d342a22010-09-28 16:54:41 -07001893 forced_image=options.image,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001894 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001895 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-28 23:42:37 -07001896 src_image=options.src_image,
Chris Sosa08d55a22011-01-19 16:08:02 -08001897 board=options.board,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001898 copy_to_static_root=not options.exit,
1899 private_key=options.private_key,
Gabe Black3b567202015-09-23 14:07:59 -07001900 private_key_for_metadata_hash_signature=(
1901 options.private_key_for_metadata_hash_signature),
David Zeuthen52ccd012013-10-31 12:58:26 -07001902 public_key=options.public_key,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001903 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001904 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001905 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001906 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001907 )
Chris Sosa7c931362010-10-11 19:49:01 -07001908
Chris Sosa6a3697f2013-01-29 16:44:43 -08001909 if options.pregenerate_update:
1910 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 15:46:22 -07001911
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001912 if options.exit:
1913 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001914
joychen3cb228e2013-06-12 12:13:13 -07001915 dev_server = DevServerRoot(_xbuddy)
1916
Gilad Arnold11fbef42014-02-10 11:04:13 -08001917 # Patch CherryPy to support binding to any available port (--port=0).
1918 cherrypy_ext.ZeroPortPatcher.DoPatch(cherrypy)
1919
Chris Sosa855b8932013-08-21 13:24:55 -07001920 if options.pidfile:
1921 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1922
Gilad Arnold11fbef42014-02-10 11:04:13 -08001923 if options.portfile:
1924 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1925
Dan Shiafd5c6c2016-01-07 10:27:03 -08001926 if (options.android_build_credential and
1927 os.path.exists(options.android_build_credential)):
1928 try:
1929 with open(options.android_build_credential) as f:
1930 android_build.BuildAccessor.credential_info = json.load(f)
1931 except ValueError as e:
1932 _Log('Failed to load the android build credential: %s. Error: %s.' %
1933 (options.android_build_credential, e))
joychen3cb228e2013-06-12 12:13:13 -07001934 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001935
1936
1937if __name__ == '__main__':
1938 main()