blob: 95b498b4b73a014352e676a82d9c14acfc40bbfc [file] [log] [blame]
Gabe Black3b567202015-09-23 21:07:591#!/usr/bin/python2
Chris Sosa7c931362010-10-12 02:49:012
Chris Sosa781ba6d2012-04-11 19:44:433# Copyright (c) 2009-2012 The Chromium OS Authors. All rights reserved.
[email protected]ded22402009-10-26 22:36:214# 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 18:47:007"""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 16:17:2329a particular package from a developer's chroot onto a requesting device.
Chris Sosa3ae4dc12013-03-29 18:47:0030
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 21:07:5942from __future__ import print_function
Chris Sosa7c931362010-10-12 02:49:0143
Gilad Arnold55a2a372012-10-02 16:46:3244import json
Sean O'Connor14b6a0a2010-03-21 06:23:4845import optparse
[email protected]ded22402009-10-26 22:36:2146import os
Scott Zawalski4647ce62012-01-03 22:17:2847import re
Simran Basi4baad082013-02-14 21:39:1848import shutil
xixuan52c2fba2016-05-21 00:02:4849import signal
Mandeep Singh Baines38dcdda2012-12-08 01:55:3350import socket
Chris Masone816e38c2012-05-02 19:22:3651import subprocess
J. Richard Barnette3d977b82013-04-23 18:05:1952import sys
Chris Masone816e38c2012-05-02 19:22:3653import tempfile
Dan Shi59ae7092013-06-04 21:37:2754import threading
Dan Shiafd0e492015-05-27 21:23:5155import time
Gilad Arnoldd5ebaaa2012-10-02 18:52:3856import types
J. Richard Barnette3d977b82013-04-23 18:05:1957from logging import handlers
58
59import cherrypy
Chris Sosa855b8932013-08-21 20:24:5560from cherrypy import _cplogging as cplogging
61from cherrypy.process import plugins
[email protected]ded22402009-10-26 22:36:2162
Chris Sosa0356d3b2010-09-16 22:46:2263import autoupdate
Dan Shi2f136862016-02-11 23:38:3864import artifact_info
Chris Sosa75490802013-10-01 00:21:4565import build_artifact
Gilad Arnold11fbef42014-02-10 19:04:1366import cherrypy_ext
xixuan52c2fba2016-05-21 00:02:4867import cros_update
68import cros_update_progress
Gilad Arnoldc65330c2012-09-20 22:17:4869import common_util
Simran Basief83d6a2014-08-28 21:32:0170import devserver_constants
Chris Sosa47a7d4e2012-03-28 18:26:5571import downloader
Chris Sosa7cd23202013-10-16 00:22:5772import gsutil_util
Gilad Arnoldc65330c2012-09-20 22:17:4873import log_util
joychen3cb228e2013-06-12 19:13:1374import xbuddy
Gilad Arnoldc65330c2012-09-20 22:17:4875
Gilad Arnoldc65330c2012-09-20 22:17:4876# Module-local log function.
Chris Sosa6a3697f2013-01-30 00:44:4377def _Log(message, *args):
78 return log_util.LogWithTag('DEVSERVER', message, *args)
Chris Sosa0356d3b2010-09-16 22:46:2279
Dan Shiafd0e492015-05-27 21:23:5180try:
81 import psutil
82except ImportError:
83 # Ignore psutil import failure. This is for backwards compatibility, so
84 # "cros flash" can still update duts with build without psutil installed.
85 # The reason is that, during cros flash, local devserver code is copied over
86 # to DUT, and devserver will be running inside DUT to stage the build.
87 _Log('Python module psutil is not installed, devserver load data will not be '
88 'collected')
89 psutil = None
Dan Shi94dcbe82015-06-09 03:51:1390except OSError as e:
91 # Ignore error like following. psutil may not work properly in builder. Ignore
92 # the error as load information of devserver is not used in builder.
93 # OSError: [Errno 2] No such file or directory: '/dev/pts/0'
94 _Log('psutil is failed to be imported, error: %s. devserver load data will '
95 'not be collected.', e)
96 psutil = None
97
Dan Shi72b16132015-10-08 19:10:3398try:
99 import android_build
100except ImportError as e:
101 # Ignore android_build import failure. This is to support devserver running
102 # inside a ChromeOS device triggered by cros flash. Most ChromeOS test images
103 # do not have google-api-python-client module and they don't need to support
104 # Android updating, therefore, ignore the import failure here.
105 _Log('Import module android_build failed with error: %s', e)
106 android_build = None
Frank Farzan40160872011-12-13 02:39:18107
Chris Sosa417e55d2011-01-26 00:40:48108CACHED_ENTRIES = 12
Don Garrettf90edf02010-11-17 01:36:14109
Simran Basi4baad082013-02-14 21:39:18110TELEMETRY_FOLDER = 'telemetry_src'
111TELEMETRY_DEPS = ['dep-telemetry_dep.tar.bz2',
112 'dep-page_cycler_dep.tar.bz2',
Simran Basi0d078682013-03-22 23:40:04113 'dep-chrome_test.tar.bz2',
114 'dep-perf_data_dep.tar.bz2']
Simran Basi4baad082013-02-14 21:39:18115
Chris Sosa0356d3b2010-09-16 22:46:22116# Sets up global to share between classes.
[email protected]21a5ca32009-11-04 18:23:23117updater = None
[email protected]ded22402009-10-26 22:36:21118
J. Richard Barnette3d977b82013-04-23 18:05:19119# Log rotation parameters. These settings correspond to once a week
J. Richard Barnette6dfa5342013-06-04 18:48:56120# at midnight between Friday and Saturday, with about three months
121# of old logs kept for backup.
J. Richard Barnette3d977b82013-04-23 18:05:19122#
123# For more, see the documentation for
124# logging.handlers.TimedRotatingFileHandler
J. Richard Barnette6dfa5342013-06-04 18:48:56125_LOG_ROTATION_TIME = 'W4'
J. Richard Barnette3d977b82013-04-23 18:05:19126_LOG_ROTATION_BACKUP = 13
127
Dan Shiafd0e492015-05-27 21:23:51128# Number of seconds between the collection of disk and network IO counters.
129STATS_INTERVAL = 10.0
Frank Farzan40160872011-12-13 02:39:18130
xixuan52c2fba2016-05-21 00:02:48131# Auto-update parameters
132
133# Error msg for missing key in CrOS auto-update.
134KEY_ERROR_MSG = 'Key Error in cmd %s: %s= is required'
135
136# Command of running auto-update.
137AUTO_UPDATE_CMD = '/usr/bin/python -u %s -d %s -b %s --static_dir %s'
138
139
Chris Sosa9164ca32012-03-28 18:04:50140class DevServerError(Exception):
Chris Sosa47a7d4e2012-03-28 18:26:55141 """Exception class used by this module."""
Chris Sosa47a7d4e2012-03-28 18:26:55142
143
Dan Shiafd0e492015-05-27 21:23:51144def require_psutil():
Gabe Black3b567202015-09-23 21:07:59145 """Decorator for functions require psutil to run."""
Dan Shiafd0e492015-05-27 21:23:51146 def deco_require_psutil(func):
147 """Wrapper of the decorator function.
148
Gabe Black3b567202015-09-23 21:07:59149 Args:
150 func: function to be called.
Dan Shiafd0e492015-05-27 21:23:51151 """
152 def func_require_psutil(*args, **kwargs):
153 """Decorator for functions require psutil to run.
154
155 If psutil is not installed, skip calling the function.
156
Gabe Black3b567202015-09-23 21:07:59157 Args:
158 *args: arguments for function to be called.
159 **kwargs: keyword arguments for function to be called.
Dan Shiafd0e492015-05-27 21:23:51160 """
161 if psutil:
162 return func(*args, **kwargs)
163 else:
164 _Log('Python module psutil is not installed. Function call %s is '
165 'skipped.' % func)
166 return func_require_psutil
167 return deco_require_psutil
168
169
Gabe Black3b567202015-09-23 21:07:59170def _canonicalize_archive_url(archive_url):
171 """Canonicalizes archive_url strings.
172
173 Raises:
174 DevserverError: if archive_url is not set.
175 """
176 if archive_url:
177 if not archive_url.startswith('gs://'):
178 raise DevServerError("Archive URL isn't from Google Storage (%s) ." %
179 archive_url)
180
181 return archive_url.rstrip('/')
182 else:
183 raise DevServerError("Must specify an archive_url in the request")
184
185
186def _canonicalize_local_path(local_path):
187 """Canonicalizes |local_path| strings.
188
189 Raises:
190 DevserverError: if |local_path| is not set.
191 """
192 # Restrict staging of local content to only files within the static
193 # directory.
194 local_path = os.path.abspath(local_path)
195 if not local_path.startswith(updater.static_dir):
196 raise DevServerError('Local path %s must be a subdirectory of the static'
197 ' directory: %s' % (local_path, updater.static_dir))
198
199 return local_path.rstrip('/')
200
201
202def _get_artifacts(kwargs):
203 """Returns a tuple of named and file artifacts given the stage rpc kwargs.
204
205 Raises:
206 DevserverError if no artifacts would be returned.
207 """
208 artifacts = kwargs.get('artifacts')
209 files = kwargs.get('files')
210 if not artifacts and not files:
211 raise DevServerError('No artifacts specified.')
212
213 # Note we NEED to coerce files to a string as we get raw unicode from
214 # cherrypy and we treat files as strings elsewhere in the code.
215 return (str(artifacts).split(',') if artifacts else [],
216 str(files).split(',') if files else [])
217
218
Dan Shi61305df2015-10-26 23:52:35219def _is_android_build_request(kwargs):
220 """Check if a devserver call is for Android build, based on the arguments.
221
222 This method exams the request's arguments (os_type) to determine if the
223 request is for Android build. If os_type is set to `android`, returns True.
224 If os_type is not set or has other values, returns False.
225
226 Args:
227 kwargs: Keyword arguments for the request.
228
229 Returns:
230 True if the request is for Android build. False otherwise.
231 """
232 os_type = kwargs.get('os_type', None)
233 return os_type == 'android'
234
235
Gabe Black3b567202015-09-23 21:07:59236def _get_downloader(kwargs):
237 """Returns the downloader based on passed in arguments.
238
239 Args:
240 kwargs: Keyword arguments for the request.
241 """
242 local_path = kwargs.get('local_path')
243 if local_path:
244 local_path = _canonicalize_local_path(local_path)
245
246 dl = None
247 if local_path:
248 dl = downloader.LocalDownloader(updater.static_dir, local_path)
249
Dan Shi61305df2015-10-26 23:52:35250 if not _is_android_build_request(kwargs):
Gabe Black3b567202015-09-23 21:07:59251 archive_url = kwargs.get('archive_url')
252 if not archive_url and not local_path:
253 raise DevServerError('Requires archive_url or local_path to be '
254 'specified.')
255 if archive_url and local_path:
256 raise DevServerError('archive_url and local_path can not both be '
257 'specified.')
258 if not dl:
259 archive_url = _canonicalize_archive_url(archive_url)
260 dl = downloader.GoogleStorageDownloader(updater.static_dir, archive_url)
261 elif not dl:
262 target = kwargs.get('target', None)
Dan Shi72b16132015-10-08 19:10:33263 branch = kwargs.get('branch', None)
Dan Shi61305df2015-10-26 23:52:35264 build_id = kwargs.get('build_id', None)
265 if not target or not branch or not build_id:
Dan Shi72b16132015-10-08 19:10:33266 raise DevServerError(
Dan Shi61305df2015-10-26 23:52:35267 'target, branch, build ID must all be specified for downloading '
268 'Android build.')
Dan Shi72b16132015-10-08 19:10:33269 dl = downloader.AndroidBuildDownloader(updater.static_dir, branch, build_id,
270 target)
Gabe Black3b567202015-09-23 21:07:59271
272 return dl
273
274
275def _get_downloader_and_factory(kwargs):
276 """Returns the downloader and artifact factory based on passed in arguments.
277
278 Args:
279 kwargs: Keyword arguments for the request.
280 """
281 artifacts, files = _get_artifacts(kwargs)
282 dl = _get_downloader(kwargs)
283
284 if (isinstance(dl, downloader.GoogleStorageDownloader) or
285 isinstance(dl, downloader.LocalDownloader)):
286 factory_class = build_artifact.ChromeOSArtifactFactory
Dan Shi72b16132015-10-08 19:10:33287 elif isinstance(dl, downloader.AndroidBuildDownloader):
Gabe Black3b567202015-09-23 21:07:59288 factory_class = build_artifact.AndroidArtifactFactory
289 else:
290 raise DevServerError('Unrecognized value for downloader type: %s' %
291 type(dl))
292
293 factory = factory_class(dl.GetBuildDir(), artifacts, files, dl.GetBuild())
294
295 return dl, factory
296
297
Scott Zawalski4647ce62012-01-03 22:17:28298def _LeadingWhiteSpaceCount(string):
299 """Count the amount of leading whitespace in a string.
300
301 Args:
302 string: The string to count leading whitespace in.
Don Garrettf84631a2014-01-08 02:21:26303
Scott Zawalski4647ce62012-01-03 22:17:28304 Returns:
305 number of white space chars before characters start.
306 """
Gabe Black3b567202015-09-23 21:07:59307 matched = re.match(r'^\s+', string)
Scott Zawalski4647ce62012-01-03 22:17:28308 if matched:
309 return len(matched.group())
310
311 return 0
312
313
314def _PrintDocStringAsHTML(func):
315 """Make a functions docstring somewhat HTML style.
316
317 Args:
318 func: The function to return the docstring from.
Don Garrettf84631a2014-01-08 02:21:26319
Scott Zawalski4647ce62012-01-03 22:17:28320 Returns:
321 A string that is somewhat formated for a web browser.
322 """
323 # TODO(scottz): Make this parse Args/Returns in a prettier way.
324 # Arguments could be bolded and indented etc.
325 html_doc = []
326 for line in func.__doc__.splitlines():
327 leading_space = _LeadingWhiteSpaceCount(line)
328 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 18:26:55329 line = '&nbsp;' * leading_space + line
Scott Zawalski4647ce62012-01-03 22:17:28330
331 html_doc.append('<BR>%s' % line)
332
333 return '\n'.join(html_doc)
334
335
Simran Basief83d6a2014-08-28 21:32:01336def _GetUpdateTimestampHandler(static_dir):
337 """Returns a handler to update directory staged.timestamp.
338
339 This handler resets the stage.timestamp whenever static content is accessed.
340
341 Args:
342 static_dir: Directory from which static content is being staged.
343
344 Returns:
345 A cherrypy handler to update the timestamp of accessed content.
346 """
347 def UpdateTimestampHandler():
348 if not '404' in cherrypy.response.status:
349 build_match = re.match(devserver_constants.STAGED_BUILD_REGEX,
350 cherrypy.request.path_info)
351 if build_match:
352 build_dir = os.path.join(static_dir, build_match.group('build'))
353 downloader.Downloader.TouchTimestampForStaged(build_dir)
354 return UpdateTimestampHandler
355
356
Chris Sosa7c931362010-10-12 02:49:01357def _GetConfig(options):
358 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-08 01:55:33359
Mandeep Singh Baines38dcdda2012-12-08 01:55:33360 socket_host = '::'
Yu-Ju Hongc8d4af32013-11-12 23:14:26361 # Fall back to IPv4 when python is not configured with IPv6.
362 if not socket.has_ipv6:
Mandeep Singh Baines38dcdda2012-12-08 01:55:33363 socket_host = '0.0.0.0'
364
Simran Basief83d6a2014-08-28 21:32:01365 # Adds the UpdateTimestampHandler to cherrypy's tools. This tools executes
366 # on the on_end_resource hook. This hook is called once processing is
367 # complete and the response is ready to be returned.
368 cherrypy.tools.update_timestamp = cherrypy.Tool(
369 'on_end_resource', _GetUpdateTimestampHandler(options.static_dir))
370
Gabe Black3b567202015-09-23 21:07:59371 base_config = {'global':
372 {'server.log_request_headers': True,
373 'server.protocol_version': 'HTTP/1.1',
374 'server.socket_host': socket_host,
375 'server.socket_port': int(options.port),
376 'response.timeout': 6000,
377 'request.show_tracebacks': True,
378 'server.socket_timeout': 60,
379 'server.thread_pool': 2,
380 'engine.autoreload.on': False,
381 },
382 '/api':
383 {
384 # Gets rid of cherrypy parsing post file for args.
385 'request.process_request_body': False,
386 },
387 '/build':
388 {'response.timeout': 100000,
389 },
390 '/update':
391 {
392 # Gets rid of cherrypy parsing post file for args.
393 'request.process_request_body': False,
394 'response.timeout': 10000,
395 },
396 # Sets up the static dir for file hosting.
397 '/static':
398 {'tools.staticdir.dir': options.static_dir,
399 'tools.staticdir.on': True,
400 'response.timeout': 10000,
401 'tools.update_timestamp.on': True,
402 },
403 }
Chris Sosa5f118ef2012-07-12 18:37:50404 if options.production:
Alex Miller93beca52013-07-31 02:25:09405 base_config['global'].update({'server.thread_pool': 150})
Chris Sosa7cd23202013-10-16 00:22:57406 # TODO(sosa): Do this more cleanly.
407 gsutil_util.GSUTIL_ATTEMPTS = 5
Scott Zawalski1c5e7cd2012-02-27 18:12:52408
Chris Sosa7c931362010-10-12 02:49:01409 return base_config
[email protected]64244662009-11-12 00:52:08410
Darin Petkove17164a2010-08-11 20:24:41411
Gilad Arnoldd5ebaaa2012-10-02 18:52:38412def _GetRecursiveMemberObject(root, member_list):
413 """Returns an object corresponding to a nested member list.
414
415 Args:
416 root: the root object to search
417 member_list: list of nested members to search
Don Garrettf84631a2014-01-08 02:21:26418
Gilad Arnoldd5ebaaa2012-10-02 18:52:38419 Returns:
420 An object corresponding to the member name list; None otherwise.
421 """
422 for member in member_list:
423 next_root = root.__class__.__dict__.get(member)
424 if not next_root:
425 return None
426 root = next_root
427 return root
428
429
430def _IsExposed(name):
431 """Returns True iff |name| has an `exposed' attribute and it is set."""
432 return hasattr(name, 'exposed') and name.exposed
433
434
Gilad Arnold748c8322012-10-12 16:51:35435def _GetExposedMethod(root, nested_member, ignored=None):
Gilad Arnoldd5ebaaa2012-10-02 18:52:38436 """Returns a CherryPy-exposed method, if such exists.
437
438 Args:
439 root: the root object for searching
440 nested_member: a slash-joined path to the nested member
441 ignored: method paths to be ignored
Don Garrettf84631a2014-01-08 02:21:26442
Gilad Arnoldd5ebaaa2012-10-02 18:52:38443 Returns:
444 A function object corresponding to the path defined by |member_list| from
445 the |root| object, if the function is exposed and not ignored; None
446 otherwise.
447 """
Gilad Arnold748c8322012-10-12 16:51:35448 method = (not (ignored and nested_member in ignored) and
Gilad Arnoldd5ebaaa2012-10-02 18:52:38449 _GetRecursiveMemberObject(root, nested_member.split('/')))
Gabe Black3b567202015-09-23 21:07:59450 if method and type(method) == types.FunctionType and _IsExposed(method):
Gilad Arnoldd5ebaaa2012-10-02 18:52:38451 return method
452
453
Gilad Arnold748c8322012-10-12 16:51:35454def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 18:52:38455 """Finds exposed CherryPy methods.
456
457 Args:
458 root: the root object for searching
459 prefix: slash-joined chain of members leading to current object
460 unlisted: URLs to be excluded regardless of their exposed status
Don Garrettf84631a2014-01-08 02:21:26461
Gilad Arnoldd5ebaaa2012-10-02 18:52:38462 Returns:
463 List of exposed URLs that are not unlisted.
464 """
465 method_list = []
466 for member in sorted(root.__class__.__dict__.keys()):
467 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 16:51:35468 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 18:52:38469 continue
470 member_obj = root.__class__.__dict__[member]
471 if _IsExposed(member_obj):
472 if type(member_obj) == types.FunctionType:
473 method_list.append(prefixed_member)
474 else:
475 method_list += _FindExposedMethods(
476 member_obj, prefixed_member, unlisted)
477 return method_list
478
479
xixuan52c2fba2016-05-21 00:02:48480def _check_base_args_for_auto_update(kwargs):
481 if 'host_name' not in kwargs:
482 raise common_util.DevServerHTTPError(KEY_ERROR_MSG % 'host_name')
483
484 if 'build_name' not in kwargs:
485 raise common_util.DevServerHTTPError(KEY_ERROR_MSG % 'build_name')
486
487
488def _parse_boolean_arg(kwargs, key):
489 if key in kwargs:
490 if kwargs[key] == 'True':
491 return True
492 elif kwargs[key] == 'False':
493 return False
494 else:
495 raise common_util.DevServerHTTPError(
496 'The value for key %s is not boolean.' % key)
497 else:
498 return False
499
500
Dale Curtisc9aaf3a2011-08-09 22:47:40501class ApiRoot(object):
502 """RESTful API for Dev Server information."""
503 exposed = True
504
505 @cherrypy.expose
506 def hostinfo(self, ip):
507 """Returns a JSON dictionary containing information about the given ip.
508
Gilad Arnold1b908392012-10-05 18:36:27509 Args:
510 ip: address of host whose info is requested
Don Garrettf84631a2014-01-08 02:21:26511
Gilad Arnold1b908392012-10-05 18:36:27512 Returns:
513 A JSON dictionary containing all or some of the following fields:
514 last_event_type (int): last update event type received
515 last_event_status (int): last update event status received
516 last_known_version (string): last known version reported in update ping
517 forced_update_label (string): update label to force next update ping to
518 use, set by setnextupdate
519 See the OmahaEvent class in update_engine/omaha_request_action.h for
520 event type and status code definitions. If the ip does not exist an empty
521 string is returned.
Dale Curtisc9aaf3a2011-08-09 22:47:40522
Gilad Arnold1b908392012-10-05 18:36:27523 Example URL:
524 http://myhost/api/hostinfo?ip=192.168.1.5
525 """
Dale Curtisc9aaf3a2011-08-09 22:47:40526 return updater.HandleHostInfoPing(ip)
527
528 @cherrypy.expose
Gilad Arnold286a0062012-01-12 21:47:02529 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 18:36:27530 """Returns a JSON object containing a log of host event.
531
532 Args:
533 ip: address of host whose event log is requested, or `all'
Don Garrettf84631a2014-01-08 02:21:26534
Gilad Arnold1b908392012-10-05 18:36:27535 Returns:
536 A JSON encoded list (log) of dictionaries (events), each of which
537 containing a `timestamp' and other event fields, as described under
538 /api/hostinfo.
539
540 Example URL:
541 http://myhost/api/hostlog?ip=192.168.1.5
542 """
Gilad Arnold286a0062012-01-12 21:47:02543 return updater.HandleHostLogPing(ip)
544
545 @cherrypy.expose
Dale Curtisc9aaf3a2011-08-09 22:47:40546 def setnextupdate(self, ip):
547 """Allows the response to the next update ping from a host to be set.
548
549 Takes the IP of the host and an update label as normally provided to the
Gilad Arnold1b908392012-10-05 18:36:27550 /update command.
551 """
Dale Curtisc9aaf3a2011-08-09 22:47:40552 body_length = int(cherrypy.request.headers['Content-Length'])
553 label = cherrypy.request.rfile.read(body_length)
554
555 if label:
556 label = label.strip()
557 if label:
558 return updater.HandleSetUpdatePing(ip, label)
Chris Sosa4b951602014-04-10 03:26:07559 raise common_util.DevServerHTTPError(400, 'No label provided.')
Dale Curtisc9aaf3a2011-08-09 22:47:40560
561
Gilad Arnold55a2a372012-10-02 16:46:32562 @cherrypy.expose
Don Garrettf84631a2014-01-08 02:21:26563 def fileinfo(self, *args):
Gilad Arnold55a2a372012-10-02 16:46:32564 """Returns information about a given staged file.
565
566 Args:
Don Garrettf84631a2014-01-08 02:21:26567 args: path to the file inside the server's static staging directory
568
Gilad Arnold55a2a372012-10-02 16:46:32569 Returns:
570 A JSON encoded dictionary with information about the said file, which may
571 contain the following keys/values:
Gilad Arnold1b908392012-10-05 18:36:27572 size (int): the file size in bytes
573 sha1 (string): a base64 encoded SHA1 hash
574 sha256 (string): a base64 encoded SHA256 hash
575
576 Example URL:
577 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 16:46:32578 """
Don Garrettf84631a2014-01-08 02:21:26579 file_path = os.path.join(updater.static_dir, *args)
Gilad Arnold55a2a372012-10-02 16:46:32580 if not os.path.exists(file_path):
581 raise DevServerError('file not found: %s' % file_path)
582 try:
583 file_size = os.path.getsize(file_path)
584 file_sha1 = common_util.GetFileSha1(file_path)
585 file_sha256 = common_util.GetFileSha256(file_path)
586 except os.error, e:
587 raise DevServerError('failed to get info for file %s: %s' %
Gilad Arnolde74b3812013-04-22 18:27:38588 (file_path, e))
589
590 is_delta = autoupdate.Autoupdate.IsDeltaFormatFile(file_path)
591
592 return json.dumps({
593 autoupdate.Autoupdate.SIZE_ATTR: file_size,
594 autoupdate.Autoupdate.SHA1_ATTR: file_sha1,
595 autoupdate.Autoupdate.SHA256_ATTR: file_sha256,
596 autoupdate.Autoupdate.ISDELTA_ATTR: is_delta
597 })
Gilad Arnold55a2a372012-10-02 16:46:32598
Chris Sosa76e44b92013-01-31 20:11:38599
David Rochberg7c79a812011-01-19 19:24:45600class DevServerRoot(object):
Chris Sosa7c931362010-10-12 02:49:01601 """The Root Class for the Dev Server.
602
603 CherryPy works as follows:
604 For each method in this class, cherrpy interprets root/path
605 as a call to an instance of DevServerRoot->method_name. For example,
606 a call to http://myhost/build will call build. CherryPy automatically
607 parses http args and places them as keyword arguments in each method.
608 For paths http://myhost/update/dir1/dir2, you can use *args so that
609 cherrypy uses the update method and puts the extra paths in args.
610 """
Gilad Arnoldf8f769f2012-09-24 15:43:01611 # Method names that should not be listed on the index page.
612 _UNLISTED_METHODS = ['index', 'doc']
613
Dale Curtisc9aaf3a2011-08-09 22:47:40614 api = ApiRoot()
Chris Sosa7c931362010-10-12 02:49:01615
Dan Shi59ae7092013-06-04 21:37:27616 # Number of threads that devserver is staging images.
617 _staging_thread_count = 0
618 # Lock used to lock increasing/decreasing count.
619 _staging_thread_count_lock = threading.Lock()
620
Dan Shiafd0e492015-05-27 21:23:51621 @require_psutil()
622 def _refresh_io_stats(self):
623 """A call running in a thread to update IO stats periodically."""
624 prev_disk_io_counters = psutil.disk_io_counters()
625 prev_network_io_counters = psutil.net_io_counters()
626 prev_read_time = time.time()
627 while True:
628 time.sleep(STATS_INTERVAL)
629 now = time.time()
630 interval = now - prev_read_time
631 prev_read_time = now
632 # Disk IO is for all disks.
633 disk_io_counters = psutil.disk_io_counters()
634 network_io_counters = psutil.net_io_counters()
635
636 self.disk_read_bytes_per_sec = (
637 disk_io_counters.read_bytes -
638 prev_disk_io_counters.read_bytes)/interval
639 self.disk_write_bytes_per_sec = (
640 disk_io_counters.write_bytes -
641 prev_disk_io_counters.write_bytes)/interval
642 prev_disk_io_counters = disk_io_counters
643
644 self.network_sent_bytes_per_sec = (
645 network_io_counters.bytes_sent -
646 prev_network_io_counters.bytes_sent)/interval
647 self.network_recv_bytes_per_sec = (
648 network_io_counters.bytes_recv -
649 prev_network_io_counters.bytes_recv)/interval
650 prev_network_io_counters = network_io_counters
651
652 @require_psutil()
653 def _start_io_stat_thread(self):
Gabe Black3b567202015-09-23 21:07:59654 """Start the thread to collect IO stats."""
Dan Shiafd0e492015-05-27 21:23:51655 thread = threading.Thread(target=self._refresh_io_stats)
656 thread.daemon = True
657 thread.start()
658
joychen3cb228e2013-06-12 19:13:13659 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 22:20:41660 self._builder = None
Simran Basi4baad082013-02-14 21:39:18661 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 19:13:13662 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 19:24:45663
Dan Shiafd0e492015-05-27 21:23:51664 # Cache of disk IO stats, a thread refresh the stats every 10 seconds.
665 # lock is not used for these variables as the only thread writes to these
666 # variables is _refresh_io_stats.
667 self.disk_read_bytes_per_sec = 0
668 self.disk_write_bytes_per_sec = 0
669 # Cache of network IO stats.
670 self.network_sent_bytes_per_sec = 0
671 self.network_recv_bytes_per_sec = 0
672 self._start_io_stat_thread()
673
Dale Curtisc9aaf3a2011-08-09 22:47:40674 @cherrypy.expose
David Rochberg7c79a812011-01-19 19:24:45675 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-12 02:49:01676 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 22:20:41677 import builder
678 if self._builder is None:
679 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 19:24:45680 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-12 02:49:01681
Dale Curtisc9aaf3a2011-08-09 22:47:40682 @cherrypy.expose
Dan Shif8eb0d12013-08-02 00:52:06683 def is_staged(self, **kwargs):
684 """Check if artifacts have been downloaded.
685
Chris Sosa6b0c6172013-08-06 00:01:33686 async: True to return without waiting for download to complete.
687 artifacts: Comma separated list of named artifacts to download.
688 These are defined in artifact_info and have their implementation
689 in build_artifact.py.
690 files: Comma separated list of file artifacts to stage. These
691 will be available as is in the corresponding static directory with no
692 custom post-processing.
693
694 returns: True of all artifacts are staged.
Dan Shif8eb0d12013-08-02 00:52:06695
696 Example:
697 To check if autotest and test_suites are staged:
698 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
699 artifacts=autotest,test_suites
700 """
Gabe Black3b567202015-09-23 21:07:59701 dl, factory = _get_downloader_and_factory(kwargs)
Aviv Keshet57d18172016-06-19 03:39:09702 response = str(dl.IsStaged(factory))
703 _Log('Responding to is_staged %s request with %r', kwargs, response)
704 return response
Dan Shi59ae7092013-06-04 21:37:27705
Chris Sosa76e44b92013-01-31 20:11:38706 @cherrypy.expose
Prashanth Ba06d2d22014-03-07 23:35:19707 def list_image_dir(self, **kwargs):
708 """Take an archive url and list the contents in its staged directory.
709
710 Args:
711 kwargs:
712 archive_url: Google Storage URL for the build.
713
714 Example:
715 To list the contents of where this devserver should have staged
716 gs://image-archive/<board>-release/<build> call:
717 http://devserver_url:<port>/list_image_dir?archive_url=<gs://..>
718
719 Returns:
720 A string with information about the contents of the image directory.
721 """
Gabe Black3b567202015-09-23 21:07:59722 dl = _get_downloader(kwargs)
Prashanth Ba06d2d22014-03-07 23:35:19723 try:
Gabe Black3b567202015-09-23 21:07:59724 image_dir_contents = dl.ListBuildDir()
Prashanth Ba06d2d22014-03-07 23:35:19725 except build_artifact.ArtifactDownloadError as e:
726 return 'Cannot list the contents of staged artifacts. %s' % e
727 if not image_dir_contents:
Gabe Black3b567202015-09-23 21:07:59728 return '%s has not been staged on this devserver.' % dl.DescribeSource()
Prashanth Ba06d2d22014-03-07 23:35:19729 return image_dir_contents
730
731 @cherrypy.expose
Chris Sosa76e44b92013-01-31 20:11:38732 def stage(self, **kwargs):
Gabe Black3b567202015-09-23 21:07:59733 """Downloads and caches build artifacts.
Chris Sosa76e44b92013-01-31 20:11:38734
Gabe Black3b567202015-09-23 21:07:59735 Downloads and caches build artifacts, possibly from a Google Storage URL,
Dan Shi72b16132015-10-08 19:10:33736 or from Android's build server. Returns once these have been downloaded
Gabe Black3b567202015-09-23 21:07:59737 on the devserver. A call to this will attempt to cache non-specified
738 artifacts in the background for the given from the given URL following
739 the principle of spatial locality. Spatial locality of different
Chris Sosa76e44b92013-01-31 20:11:38740 artifacts is explicitly defined in the build_artifact module.
741
742 These artifacts will then be available from the static/ sub-directory of
743 the devserver.
744
745 Args:
746 archive_url: Google Storage URL for the build.
Simran Basi4243a862014-12-12 20:48:33747 local_path: Local path for the build.
Dan Shif8eb0d12013-08-02 00:52:06748 async: True to return without waiting for download to complete.
Chris Sosa6b0c6172013-08-06 00:01:33749 artifacts: Comma separated list of named artifacts to download.
750 These are defined in artifact_info and have their implementation
751 in build_artifact.py.
752 files: Comma separated list of files to stage. These
753 will be available as is in the corresponding static directory with no
754 custom post-processing.
Laurence Goodbyf5c958d2016-01-15 02:23:56755 clean: True to remove any previously staged artifacts first.
Chris Sosa76e44b92013-01-31 20:11:38756
757 Example:
758 To download the autotest and test suites tarballs:
759 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
760 artifacts=autotest,test_suites
761 To download the full update payload:
762 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
763 artifacts=full_payload
Chris Sosa6b0c6172013-08-06 00:01:33764 To download just a file called blah.bin:
765 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
766 files=blah.bin
Chris Sosa76e44b92013-01-31 20:11:38767
768 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 23:39:34769 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 20:11:38770
771 Note for this example, relative path is the archive_url stripped of its
772 basename i.e. path/ in the examples above. Specific example:
773
774 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
775
776 Will get staged to:
777
joychened64b222013-06-21 23:39:34778 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Chris Sosa76e44b92013-01-31 20:11:38779 """
Gabe Black3b567202015-09-23 21:07:59780 dl, factory = _get_downloader_and_factory(kwargs)
781
Dan Shi59ae7092013-06-04 21:37:27782 with DevServerRoot._staging_thread_count_lock:
783 DevServerRoot._staging_thread_count += 1
784 try:
Laurence Goodbyf5c958d2016-01-15 02:23:56785 boolean_string = kwargs.get('clean')
786 clean = xbuddy.XBuddy.ParseBoolean(boolean_string)
787 if clean and os.path.exists(dl.GetBuildDir()):
788 _Log('Removing %s' % dl.GetBuildDir())
789 shutil.rmtree(dl.GetBuildDir())
Gabe Black3b567202015-09-23 21:07:59790 async = kwargs.get('async', False)
791 dl.Download(factory, async=async)
Dan Shi59ae7092013-06-04 21:37:27792 finally:
793 with DevServerRoot._staging_thread_count_lock:
794 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 20:11:38795 return 'Success'
Chris Sosacde6bf42012-06-01 01:36:39796
797 @cherrypy.expose
xixuan52c2fba2016-05-21 00:02:48798 def cros_au(self, **kwargs):
799 """Auto-update a CrOS DUT.
800
801 Args:
802 kwargs:
803 host_name: the hostname of the DUT to auto-update.
804 build_name: the build name for update the DUT.
805 force_update: Force an update even if the version installed is the
806 same. Default: False.
807 full_update: If True, do not run stateful update, directly force a full
808 reimage. If False, try stateful update first if the dut is already
809 installed with the same version.
810 async: Whether the auto_update function is ran in the background.
811
812 Returns:
813 A tuple includes two elements:
814 a boolean variable represents whether the auto-update process is
815 successfully started.
816 an integer represents the background auto-update process id.
817 """
818 _check_base_args_for_auto_update(kwargs)
819
820 host_name = kwargs['host_name']
821 build_name = kwargs['build_name']
822 force_update = _parse_boolean_arg(kwargs, 'force_update')
823 full_update = _parse_boolean_arg(kwargs, 'full_update')
824 async = _parse_boolean_arg(kwargs, 'async')
825
826 if async:
827 path = os.path.dirname(os.path.abspath(__file__))
828 execute_file = os.path.join(path, 'cros_update.py')
829 args = (AUTO_UPDATE_CMD % (execute_file, host_name, build_name,
830 updater.static_dir))
831 if force_update:
832 args = ('%s --force_update' % args)
833
834 if full_update:
835 args = ('%s --full_update' % args)
836
xixuan2a0970a2016-08-10 19:12:44837 p = subprocess.Popen([args], shell=True, preexec_fn=os.setsid)
838 pid = os.getpgid(p.pid)
xixuan52c2fba2016-05-21 00:02:48839
840 # Pre-write status in the track_status_file before the first call of
841 # 'get_au_status' to make sure that the track_status_file exists.
xixuan2a0970a2016-08-10 19:12:44842 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
xixuan52c2fba2016-05-21 00:02:48843 progress_tracker.WriteStatus('CrOS update is just started.')
844
xixuan2a0970a2016-08-10 19:12:44845 return json.dumps((True, pid))
xixuan52c2fba2016-05-21 00:02:48846 else:
847 cros_update_trigger = cros_update.CrOSUpdateTrigger(
848 host_name, build_name, updater.static_dir)
849 cros_update_trigger.TriggerAU()
850
851 @cherrypy.expose
852 def get_au_status(self, **kwargs):
853 """Check if the auto-update task is finished.
854
855 It handles 4 cases:
856 1. If an error exists in the track_status_file, delete the track file and
857 raise it.
858 2. If cros-update process is finished, delete the file and return the
859 success result.
860 3. If the process is not running, delete the track file and raise an error
861 about 'the process is terminated due to unknown reason'.
862 4. If the track_status_file does not exist, kill the process if it exists,
863 and raise the IOError.
864
865 Args:
866 kwargs:
867 host_name: the hostname of the DUT to auto-update.
868 pid: the background process id of cros-update.
869
870 Returns:
xixuan28d99072016-10-06 19:24:16871 A dict with three elements:
xixuan52c2fba2016-05-21 00:02:48872 a boolean variable represents whether the auto-update process is
873 finished.
874 a string represents the current auto-update process status.
875 For example, 'Transfer Devserver/Stateful Update Package'.
xixuan28d99072016-10-06 19:24:16876 a detailed error message paragraph if there exists an Auto-Update
877 error, in which the last line shows the main exception. Empty
878 string otherwise.
xixuan52c2fba2016-05-21 00:02:48879 """
880 if 'host_name' not in kwargs:
881 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
882
883 if 'pid' not in kwargs:
884 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
885
886 host_name = kwargs['host_name']
887 pid = kwargs['pid']
888 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
889
xixuan28d99072016-10-06 19:24:16890 result_dict = {'finished': False, 'status': '', 'detailed_error_msg': ''}
xixuan52c2fba2016-05-21 00:02:48891 try:
892 result = progress_tracker.ReadStatus()
893 if result.startswith(cros_update_progress.ERROR_TAG):
xixuan28d99072016-10-06 19:24:16894 result_dict['detailed_error_msg'] = result[len(
895 cros_update_progress.ERROR_TAG):]
896 return json.dumps(result_dict)
xixuan52c2fba2016-05-21 00:02:48897
898 if result == cros_update_progress.FINISHED:
xixuan28d99072016-10-06 19:24:16899 result_dict['finished'] = True
900 result_dict['status'] = result
901 return json.dumps(result_dict)
xixuan52c2fba2016-05-21 00:02:48902
903 if not cros_update_progress.IsProcessAlive(pid):
xixuan28d99072016-10-06 19:24:16904 result_dict['detailed_error_msg'] = (
905 'Cros_update process terminated midway due to unknown reason. '
906 'Last update status was %s' % result)
907 return json.dumps(result_dict)
xixuan52c2fba2016-05-21 00:02:48908
xixuan28d99072016-10-06 19:24:16909 result_dict['status'] = result
910 return json.dumps(result_dict)
xixuan52c2fba2016-05-21 00:02:48911 except IOError:
912 if pid:
xixuan2a0970a2016-08-10 19:12:44913 os.killpg(int(pid), signal.SIGKILL)
xixuan52c2fba2016-05-21 00:02:48914
915 raise
916
917 @cherrypy.expose
918 def handler_cleanup(self, **kwargs):
xixuan3bc974e2016-10-19 00:21:43919 """Clean track status log and temp directory for CrOS auto-update process.
xixuan52c2fba2016-05-21 00:02:48920
921 Args:
922 kwargs:
923 host_name: the hostname of the DUT to auto-update.
924 pid: the background process id of cros-update.
925 """
926 if 'host_name' not in kwargs:
927 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
928
929 if 'pid' not in kwargs:
930 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
931
932 host_name = kwargs['host_name']
933 pid = kwargs['pid']
934 cros_update_progress.DelTrackStatusFile(host_name, pid)
xixuan3bc974e2016-10-19 00:21:43935 cros_update_progress.DelAUTempDirectory(host_name, pid)
xixuan52c2fba2016-05-21 00:02:48936
937 @cherrypy.expose
938 def kill_au_proc(self, **kwargs):
939 """Kill CrOS auto-update process using given process id.
940
941 Args:
942 kwargs:
943 host_name: Kill all the CrOS auto-update process of this host.
944
945 Returns:
946 True if all processes are killed properly.
947 """
948 if 'host_name' not in kwargs:
949 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
950
951 host_name = kwargs['host_name']
xixuan3bc974e2016-10-19 00:21:43952 track_log_list = cros_update_progress.GetAllTrackStatusFileByHostName(
953 host_name)
xixuan52c2fba2016-05-21 00:02:48954 for log in track_log_list:
955 # The track log's full path is: path/host_name_pid.log
956 # Use splitext to remove file extension, then parse pid from the
957 # filename.
958 pid = os.path.splitext(os.path.basename(log))[0][len(host_name)+1:]
959 if cros_update_progress.IsProcessAlive(pid):
xixuan2a0970a2016-08-10 19:12:44960 os.killpg(int(pid), signal.SIGKILL)
xixuan52c2fba2016-05-21 00:02:48961
962 cros_update_progress.DelTrackStatusFile(host_name, pid)
xixuan1bbfaba2016-10-14 00:53:22963 cros_update_progress.DelExecuteLogFile(host_name, pid)
xixuan52c2fba2016-05-21 00:02:48964
965 return 'True'
966
967 @cherrypy.expose
968 def collect_cros_au_log(self, **kwargs):
969 """Collect CrOS auto-update log.
970
971 Args:
972 kwargs:
973 host_name: the hostname of the DUT to auto-update.
974 pid: the background process id of cros-update.
975
976 Returns:
977 A string contains the whole content of the execute log file.
978 """
979 if 'host_name' not in kwargs:
980 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
981
982 if 'pid' not in kwargs:
983 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
984
985 host_name = kwargs['host_name']
986 pid = kwargs['pid']
xixuan3bc974e2016-10-19 00:21:43987
988 # Fetch the execute log recorded by cros_update_progress.
xixuan1bbfaba2016-10-14 00:53:22989 au_log = cros_update_progress.ReadExecuteLogFile(host_name, pid)
990 cros_update_progress.DelExecuteLogFile(host_name, pid)
991 return au_log
992
xixuan52c2fba2016-05-21 00:02:48993 @cherrypy.expose
Dan Shi2f136862016-02-11 23:38:38994 def locate_file(self, **kwargs):
995 """Get the path to the given file name.
996
997 This method looks up the given file name inside specified build artifacts.
998 One use case is to help caller to locate an apk file inside a build
999 artifact. The location of the apk file could be different based on the
1000 branch and target.
1001
1002 Args:
1003 file_name: Name of the file to look for.
1004 artifacts: A list of artifact names to search for the file.
1005
1006 Returns:
1007 Path to the file with the given name. It's relative to the folder for the
1008 build, e.g., DATA/priv-app/sl4a/sl4a.apk
Dan Shi2f136862016-02-11 23:38:381009 """
1010 dl, _ = _get_downloader_and_factory(kwargs)
1011 try:
1012 file_name = kwargs['file_name'].lower()
1013 artifacts = kwargs['artifacts']
1014 except KeyError:
1015 raise DevServerError('`file_name` and `artifacts` are required to search '
1016 'for a file in build artifacts.')
1017 build_path = dl.GetBuildDir()
1018 for artifact in artifacts:
1019 # Get the unzipped folder of the artifact. If it's not defined in
1020 # ARTIFACT_UNZIP_FOLDER_MAP, assume the files are unzipped to the build
1021 # directory directly.
1022 folder = artifact_info.ARTIFACT_UNZIP_FOLDER_MAP.get(artifact, '')
1023 artifact_path = os.path.join(build_path, folder)
1024 for root, _, filenames in os.walk(artifact_path):
1025 if file_name in set([f.lower() for f in filenames]):
1026 return os.path.relpath(os.path.join(root, file_name), build_path)
1027 raise DevServerError('File `%s` can not be found in artifacts: %s' %
1028 (file_name, artifacts))
1029
1030 @cherrypy.expose
Simran Basi4baad082013-02-14 21:39:181031 def setup_telemetry(self, **kwargs):
1032 """Extracts and sets up telemetry
1033
1034 This method goes through the telemetry deps packages, and stages them on
1035 the devserver to be used by the drones and the telemetry tests.
1036
1037 Args:
1038 archive_url: Google Storage URL for the build.
1039
1040 Returns:
1041 Path to the source folder for the telemetry codebase once it is staged.
1042 """
Gabe Black3b567202015-09-23 21:07:591043 dl = _get_downloader(kwargs)
Simran Basi4baad082013-02-14 21:39:181044
Gabe Black3b567202015-09-23 21:07:591045 build_path = dl.GetBuildDir()
Simran Basi4baad082013-02-14 21:39:181046 deps_path = os.path.join(build_path, 'autotest/packages')
1047 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
1048 src_folder = os.path.join(telemetry_path, 'src')
1049
1050 with self._telemetry_lock_dict.lock(telemetry_path):
1051 if os.path.exists(src_folder):
1052 # Telemetry is already fully stage return
1053 return src_folder
1054
1055 common_util.MkDirP(telemetry_path)
1056
1057 # Copy over the required deps tar balls to the telemetry directory.
1058 for dep in TELEMETRY_DEPS:
1059 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 23:40:041060 if not os.path.exists(dep_path):
1061 # This dep does not exist (could be new), do not extract it.
1062 continue
Simran Basi4baad082013-02-14 21:39:181063 try:
1064 common_util.ExtractTarball(dep_path, telemetry_path)
1065 except common_util.CommonUtilError as e:
1066 shutil.rmtree(telemetry_path)
1067 raise DevServerError(str(e))
1068
1069 # By default all the tarballs extract to test_src but some parts of
1070 # the telemetry code specifically hardcoded to exist inside of 'src'.
1071 test_src = os.path.join(telemetry_path, 'test_src')
1072 try:
1073 shutil.move(test_src, src_folder)
1074 except shutil.Error:
1075 # This can occur if src_folder already exists. Remove and retry move.
1076 shutil.rmtree(src_folder)
Gabe Black3b567202015-09-23 21:07:591077 raise DevServerError(
1078 'Failure in telemetry setup for build %s. Appears that the '
1079 'test_src to src move failed.' % dl.GetBuild())
Simran Basi4baad082013-02-14 21:39:181080
1081 return src_folder
1082
1083 @cherrypy.expose
Chris Sosa76e44b92013-01-31 20:11:381084 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 19:22:361085 """Symbolicates a minidump using pre-downloaded symbols, returns it.
1086
1087 Callers will need to POST to this URL with a body of MIME-type
1088 "multipart/form-data".
1089 The body should include a single argument, 'minidump', containing the
1090 binary-formatted minidump to symbolicate.
1091
Chris Masone816e38c2012-05-02 19:22:361092 Args:
Chris Sosa76e44b92013-01-31 20:11:381093 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 19:22:361094 minidump: The binary minidump file to symbolicate.
1095 """
Chris Sosa76e44b92013-01-31 20:11:381096 # Ensure the symbols have been staged.
Dan Shif08fe492016-10-04 21:39:251097 # Try debug.tar.xz first, then debug.tgz
1098 for artifact in (artifact_info.SYMBOLS_ONLY, artifact_info.SYMBOLS):
1099 kwargs['artifacts'] = artifact
1100 dl = _get_downloader(kwargs)
1101
1102 try:
1103 if self.stage(**kwargs) == 'Success':
1104 break
1105 except build_artifact.ArtifactDownloadError:
1106 continue
1107 else:
Gabe Black3b567202015-09-23 21:07:591108 raise DevServerError('Failed to stage symbols for %s' %
1109 dl.DescribeSource())
Chris Sosa76e44b92013-01-31 20:11:381110
Chris Masone816e38c2012-05-02 19:22:361111 to_return = ''
1112 with tempfile.NamedTemporaryFile() as local:
1113 while True:
1114 data = minidump.file.read(8192)
1115 if not data:
1116 break
1117 local.write(data)
Chris Sosa76e44b92013-01-31 20:11:381118
Chris Masone816e38c2012-05-02 19:22:361119 local.flush()
Chris Sosa76e44b92013-01-31 20:11:381120
Gabe Black3b567202015-09-23 21:07:591121 symbols_directory = os.path.join(dl.GetBuildDir(), 'debug', 'breakpad')
Chris Sosa76e44b92013-01-31 20:11:381122
1123 stackwalk = subprocess.Popen(
1124 ['minidump_stackwalk', local.name, symbols_directory],
1125 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1126
Chris Masone816e38c2012-05-02 19:22:361127 to_return, error_text = stackwalk.communicate()
1128 if stackwalk.returncode != 0:
1129 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
1130 error_text, stackwalk.returncode))
1131
1132 return to_return
1133
1134 @cherrypy.expose
Don Garrettf84631a2014-01-08 02:21:261135 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 19:31:361136 """Return a string representing the latest build for a given target.
1137
1138 Args:
1139 target: The build target, typically a combination of the board and the
1140 type of build e.g. x86-mario-release.
1141 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
1142 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-08 02:21:261143
Scott Zawalski16954532012-03-20 19:31:361144 Returns:
1145 A string representation of the latest build if one exists, i.e.
1146 R19-1993.0.0-a1-b1480.
1147 An empty string if no latest could be found.
1148 """
Don Garrettf84631a2014-01-08 02:21:261149 if not kwargs:
Scott Zawalski16954532012-03-20 19:31:361150 return _PrintDocStringAsHTML(self.latestbuild)
1151
Don Garrettf84631a2014-01-08 02:21:261152 if 'target' not in kwargs:
Chris Sosa4b951602014-04-10 03:26:071153 raise common_util.DevServerHTTPError(500, 'Error: target= is required!')
Dan Shi61305df2015-10-26 23:52:351154
1155 if _is_android_build_request(kwargs):
1156 branch = kwargs.get('branch', None)
1157 target = kwargs.get('target', None)
1158 if not target or not branch:
1159 raise DevServerError(
xixuan52c2fba2016-05-21 00:02:481160 'Both target and branch must be specified to query for the latest '
1161 'Android build.')
Dan Shi61305df2015-10-26 23:52:351162 return android_build.BuildAccessor.GetLatestBuildID(target, branch)
1163
Scott Zawalski16954532012-03-20 19:31:361164 try:
Gilad Arnoldc65330c2012-09-20 22:17:481165 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-08 02:21:261166 updater.static_dir, kwargs['target'],
1167 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 17:05:011168 except common_util.CommonUtilError as errmsg:
Chris Sosa4b951602014-04-10 03:26:071169 raise common_util.DevServerHTTPError(500, str(errmsg))
Scott Zawalski16954532012-03-20 19:31:361170
1171 @cherrypy.expose
xixuan7efd0002016-04-14 22:34:011172 def list_suite_controls(self, **kwargs):
1173 """Return a list of contents of all known control files.
1174
1175 Example URL:
1176 To List all control files' content:
1177 http://dev-server/list_suite_controls?suite_name=bvt&
1178 build=daisy_spring-release/R29-4279.0.0
1179
1180 Args:
1181 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
1182 suite_name: List the control files belonging to that suite.
1183
1184 Returns:
Dan Shia1cd6522016-04-18 23:07:211185 A dictionary of all control files's path to its content for given suite.
xixuan7efd0002016-04-14 22:34:011186 """
1187 if not kwargs:
1188 return _PrintDocStringAsHTML(self.controlfiles)
1189
1190 if 'build' not in kwargs:
1191 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
1192
1193 if 'suite_name' not in kwargs:
Dan Shia1cd6522016-04-18 23:07:211194 raise common_util.DevServerHTTPError(500,
1195 'Error: suite_name= is required!')
xixuan7efd0002016-04-14 22:34:011196
1197 control_file_list = [
1198 line.rstrip() for line in common_util.GetControlFileListForSuite(
1199 updater.static_dir, kwargs['build'],
1200 kwargs['suite_name']).splitlines()]
1201
Dan Shia1cd6522016-04-18 23:07:211202 control_file_content_dict = {}
xixuan7efd0002016-04-14 22:34:011203 for control_path in control_file_list:
Dan Shia1cd6522016-04-18 23:07:211204 control_file_content_dict[control_path] = (common_util.GetControlFile(
xixuan7efd0002016-04-14 22:34:011205 updater.static_dir, kwargs['build'], control_path))
1206
Dan Shia1cd6522016-04-18 23:07:211207 return json.dumps(control_file_content_dict)
xixuan7efd0002016-04-14 22:34:011208
1209 @cherrypy.expose
Don Garrettf84631a2014-01-08 02:21:261210 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 22:17:281211 """Return a control file or a list of all known control files.
1212
1213 Example URL:
1214 To List all control files:
beepsbd337242013-07-10 05:44:061215 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
1216 To List all control files for, say, the bvt suite:
1217 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 22:17:281218 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 20:12:421219 http://dev-server/controlfiles?board=x86-alex-release&build=R18-1514.0.0&control_path=client/sleeptest/control
Scott Zawalski4647ce62012-01-03 22:17:281220
1221 Args:
Scott Zawalski84a39c92012-01-13 20:12:421222 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 22:17:281223 control_path: If you want the contents of a control file set this
1224 to the path. E.g. client/site_tests/sleeptest/control
1225 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-10 05:44:061226 suite_name: If control_path is not specified but a suite_name is
1227 specified, list the control files belonging to that suite instead of
1228 all control files. The empty string for suite_name will list all control
1229 files for the build.
Don Garrettf84631a2014-01-08 02:21:261230
Scott Zawalski4647ce62012-01-03 22:17:281231 Returns:
1232 Contents of a control file if control_path is provided.
1233 A list of control files if no control_path is provided.
1234 """
Don Garrettf84631a2014-01-08 02:21:261235 if not kwargs:
Scott Zawalski4647ce62012-01-03 22:17:281236 return _PrintDocStringAsHTML(self.controlfiles)
1237
Don Garrettf84631a2014-01-08 02:21:261238 if 'build' not in kwargs:
Chris Sosa4b951602014-04-10 03:26:071239 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 22:17:281240
Don Garrettf84631a2014-01-08 02:21:261241 if 'control_path' not in kwargs:
1242 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-10 05:44:061243 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-08 02:21:261244 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-10 05:44:061245 else:
1246 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-08 02:21:261247 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 22:17:281248 else:
Gilad Arnoldc65330c2012-09-20 22:17:481249 return common_util.GetControlFile(
Don Garrettf84631a2014-01-08 02:21:261250 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-13 02:39:181251
1252 @cherrypy.expose
Simran Basi99e63c02014-05-20 17:39:521253 def xbuddy_translate(self, *args, **kwargs):
Yu-Ju Hong1bdb7a92014-04-10 23:02:111254 """Translates an xBuddy path to a real path to artifact if it exists.
1255
1256 Args:
Simran Basi99e63c02014-05-20 17:39:521257 args: An xbuddy path in the form of {local|remote}/build_id/artifact.
1258 Local searches the devserver's static directory. Remote searches a
1259 Google Storage image archive.
1260
1261 Kwargs:
1262 image_dir: Google Storage image archive to search in if requesting a
1263 remote artifact. If none uses the default bucket.
Yu-Ju Hong1bdb7a92014-04-10 23:02:111264
1265 Returns:
Simran Basi99e63c02014-05-20 17:39:521266 String in the format of build_id/artifact as stored on the local server
1267 or in Google Storage.
Yu-Ju Hong1bdb7a92014-04-10 23:02:111268 """
Simran Basi99e63c02014-05-20 17:39:521269 build_id, filename = self._xbuddy.Translate(
Gabe Black3b567202015-09-23 21:07:591270 args, image_dir=kwargs.get('image_dir'))
Yu-Ju Hong1bdb7a92014-04-10 23:02:111271 response = os.path.join(build_id, filename)
1272 _Log('Path translation requested, returning: %s', response)
1273 return response
1274
1275 @cherrypy.expose
joycheneaf4cfc2013-07-02 15:38:571276 def xbuddy(self, *args, **kwargs):
1277 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 19:13:131278
1279 Args:
joycheneaf4cfc2013-07-02 15:38:571280 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 21:30:301281 components of the path. The path can be understood as
1282 "{local|remote}/build_id/artifact" where build_id is composed of
1283 "board/version."
joycheneaf4cfc2013-07-02 15:38:571284
joychen121fc9b2013-08-02 21:30:301285 The first path element is optional, and can be "remote" or "local"
1286 If local (the default), devserver will not attempt to access Google
1287 Storage, and will only search the static directory for the files.
1288 If remote, devserver will try to obtain the artifact off GS if it's
1289 not found locally.
1290 The board is the familiar board name, optionally suffixed.
1291 The version can be the google storage version number, and may also be
1292 any of a number of xBuddy defined version aliases that will be
1293 translated into the latest built image that fits the description.
1294 Defaults to latest.
1295 The artifact is one of a number of image or artifact aliases used by
1296 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 15:38:571297
1298 Kwargs:
Yu-Ju Hong51495eb2013-12-13 01:08:431299 for_update: {true|false}
1300 if true, pregenerates the update payloads for the image,
1301 and returns the update uri to pass to the
1302 update_engine_client.
joychen3cb228e2013-06-12 19:13:131303 return_dir: {true|false}
1304 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-13 01:08:431305 relative_path: {true|false}
1306 if set to true, returns the relative path to the payload
1307 directory from static_dir.
joychen3cb228e2013-06-12 19:13:131308 Example URL:
joycheneaf4cfc2013-07-02 15:38:571309 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 19:13:131310 or
joycheneaf4cfc2013-07-02 15:38:571311 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 19:13:131312
1313 Returns:
Yu-Ju Hong51495eb2013-12-13 01:08:431314 If |for_update|, returns a redirect to the image or update file
1315 on the devserver. E.g.,
1316 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
1317 chromium-test-image.bin
1318 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
1319 http://host:port/static/x86-generic-release/R26-4000.0.0/
1320 If |relative_path| is true, return a relative path the folder where the
1321 payloads are. E.g.,
1322 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 19:13:131323 """
Chris Sosa75490802013-10-01 00:21:451324 boolean_string = kwargs.get('for_update')
1325 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-13 01:08:431326 boolean_string = kwargs.get('return_dir')
1327 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
1328 boolean_string = kwargs.get('relative_path')
1329 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 21:30:301330
Yu-Ju Hong51495eb2013-12-13 01:08:431331 if return_dir and relative_path:
Chris Sosa4b951602014-04-10 03:26:071332 raise common_util.DevServerHTTPError(
1333 500, 'Cannot specify both return_dir and relative_path')
Chris Sosa75490802013-10-01 00:21:451334
1335 # For updates, we optimize downloading of test images.
1336 file_name = None
1337 build_id = None
1338 if for_update:
1339 try:
Yu-Ju Hong1bdb7a92014-04-10 23:02:111340 build_id = self._xbuddy.StageTestArtifactsForUpdate(args)
Chris Sosa75490802013-10-01 00:21:451341 except build_artifact.ArtifactDownloadError:
1342 build_id = None
1343
1344 if not build_id:
1345 build_id, file_name = self._xbuddy.Get(args)
1346
Yu-Ju Hong51495eb2013-12-13 01:08:431347 if for_update:
1348 _Log('Payload generation triggered by request')
1349 # Forces payload to be in cache and symlinked into build_id dir.
Chris Sosa75490802013-10-01 00:21:451350 updater.GetUpdateForLabel(autoupdate.FORCED_UPDATE, build_id,
1351 image_name=file_name)
Yu-Ju Hong51495eb2013-12-13 01:08:431352
1353 response = None
1354 if return_dir:
1355 response = os.path.join(cherrypy.request.base, 'static', build_id)
1356 _Log('Directory requested, returning: %s', response)
1357 elif relative_path:
1358 response = build_id
1359 _Log('Relative path requested, returning: %s', response)
1360 elif for_update:
1361 response = os.path.join(cherrypy.request.base, 'update', build_id)
1362 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 19:13:131363 else:
Yu-Ju Hong51495eb2013-12-13 01:08:431364 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 21:30:301365 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-13 01:08:431366 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 21:30:301367 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 19:13:131368
Yu-Ju Hong51495eb2013-12-13 01:08:431369 return response
1370
joychen3cb228e2013-06-12 19:13:131371 @cherrypy.expose
1372 def xbuddy_list(self):
1373 """Lists the currently available images & time since last access.
1374
Gilad Arnold452fd272014-02-04 19:09:281375 Returns:
1376 A string representation of a list of tuples [(build_id, time since last
1377 access),...]
joychen3cb228e2013-06-12 19:13:131378 """
1379 return self._xbuddy.List()
1380
1381 @cherrypy.expose
1382 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 19:09:281383 """Returns the number of images cached by xBuddy."""
joychen3cb228e2013-06-12 19:13:131384 return self._xbuddy.Capacity()
1385
1386 @cherrypy.expose
Chris Sosa7c931362010-10-12 02:49:011387 def index(self):
Gilad Arnoldf8f769f2012-09-24 15:43:011388 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 15:43:011389 return ('Welcome to the Dev Server!<br>\n'
1390 '<br>\n'
1391 'Here are the available methods, click for documentation:<br>\n'
1392 '<br>\n'
1393 '%s' %
1394 '<br>\n'.join(
1395 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 18:52:381396 for name in _FindExposedMethods(
1397 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 15:43:011398
1399 @cherrypy.expose
1400 def doc(self, *args):
1401 """Shows the documentation for available methods / URLs.
1402
1403 Example:
1404 http://myhost/doc/update
1405 """
Gilad Arnoldd5ebaaa2012-10-02 18:52:381406 name = '/'.join(args)
1407 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 15:43:011408 if not method:
1409 raise DevServerError("No exposed method named `%s'" % name)
1410 if not method.__doc__:
1411 raise DevServerError("No documentation for exposed method `%s'" % name)
1412 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-12 02:49:011413
Dale Curtisc9aaf3a2011-08-09 22:47:401414 @cherrypy.expose
Chris Sosa7c931362010-10-12 02:49:011415 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 15:43:011416 """Handles an update check from a Chrome OS client.
1417
1418 The HTTP request should contain the standard Omaha-style XML blob. The URL
1419 line may contain an additional intermediate path to the update payload.
1420
joychen121fc9b2013-08-02 21:30:301421 This request can be handled in one of 4 ways, depending on the devsever
1422 settings and intermediate path.
joychenb0dfe552013-07-30 17:02:061423
joychen121fc9b2013-08-02 21:30:301424 1. No intermediate path
1425 If no intermediate path is given, the default behavior is to generate an
1426 update payload from the latest test image locally built for the board
1427 specified in the xml. Devserver serves the generated payload.
1428
1429 2. Path explicitly invokes XBuddy
1430 If there is a path given, it can explicitly invoke xbuddy by prefixing it
1431 with 'xbuddy'. This path is then used to acquire an image binary for the
1432 devserver to generate an update payload from. Devserver then serves this
1433 payload.
1434
1435 3. Path is left for the devserver to interpret.
1436 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
1437 to generate a payload from the test image in that directory and serve it.
1438
1439 4. The devserver is in a 'forced' mode. TO BE DEPRECATED
1440 This comes from the usage of --forced_payload or --image when starting the
1441 devserver. No matter what path (or no path) gets passed in, devserver will
1442 serve the update payload (--forced_payload) or generate an update payload
1443 from the image (--image).
1444
1445 Examples:
1446 1. No intermediate path
1447 update_engine_client --omaha_url=http://myhost/update
1448 This generates an update payload from the latest test image locally built
1449 for the board specified in the xml.
1450
1451 2. Explicitly invoke xbuddy
1452 update_engine_client --omaha_url=
1453 http://myhost/update/xbuddy/remote/board/version/dev
1454 This would go to GS to download the dev image for the board, from which
1455 the devserver would generate a payload to serve.
1456
1457 3. Give a path for devserver to interpret
1458 update_engine_client --omaha_url=http://myhost/update/some/random/path
1459 This would attempt, in order to:
1460 a) Generate an update from a test image binary if found in
1461 static_dir/some/random/path.
1462 b) Serve an update payload found in static_dir/some/random/path.
1463 c) Hope that some/random/path takes the form "board/version" and
1464 and attempt to download an update payload for that board/version
1465 from GS.
Gilad Arnoldf8f769f2012-09-24 15:43:011466 """
joychen121fc9b2013-08-02 21:30:301467 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 21:47:021468 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-12 02:49:011469 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-12 02:49:011470
joychen121fc9b2013-08-02 21:30:301471 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 22:46:221472
Dan Shiafd0e492015-05-27 21:23:511473 @require_psutil()
1474 def _get_io_stats(self):
1475 """Get the IO stats as a dictionary.
1476
Gabe Black3b567202015-09-23 21:07:591477 Returns:
1478 A dictionary of IO stats collected by psutil.
Dan Shiafd0e492015-05-27 21:23:511479 """
1480 return {'disk_read_bytes_per_second': self.disk_read_bytes_per_sec,
1481 'disk_write_bytes_per_second': self.disk_write_bytes_per_sec,
1482 'disk_total_bytes_per_second': (self.disk_read_bytes_per_sec +
1483 self.disk_write_bytes_per_sec),
1484 'network_sent_bytes_per_second': self.network_sent_bytes_per_sec,
1485 'network_recv_bytes_per_second': self.network_recv_bytes_per_sec,
1486 'network_total_bytes_per_second': (self.network_sent_bytes_per_sec +
1487 self.network_recv_bytes_per_sec),
1488 'cpu_percent': psutil.cpu_percent(),}
1489
Dan Shi7247f9c2016-06-01 16:19:091490
1491 def _get_process_count(self, process_cmd_pattern):
1492 """Get the count of processes that match the given command pattern.
1493
1494 Args:
1495 process_cmd_pattern: The regex pattern of process command to match.
1496
1497 Returns:
1498 The count of processes that match the given command pattern.
1499 """
1500 try:
1501 return int(subprocess.check_output(
1502 'pgrep -fc "%s"' % process_cmd_pattern, shell=True))
1503 except subprocess.CalledProcessError:
1504 return 0
1505
1506
Dan Shif5ce2de2013-04-25 23:06:321507 @cherrypy.expose
1508 def check_health(self):
1509 """Collect the health status of devserver to see if it's ready for staging.
1510
Gilad Arnold452fd272014-02-04 19:09:281511 Returns:
1512 A JSON dictionary containing all or some of the following fields:
1513 free_disk (int): free disk space in GB
1514 staging_thread_count (int): number of devserver threads currently staging
1515 an image
Dan Shi7247f9c2016-06-01 16:19:091516 apache_client_count (int): count of Apache processes.
1517 telemetry_test_count (int): count of telemetry tests.
1518 gsutil_count (int): count of gsutil processes.
Dan Shif5ce2de2013-04-25 23:06:321519 """
1520 # Get free disk space.
1521 stat = os.statvfs(updater.static_dir)
1522 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
Dan Shi7247f9c2016-06-01 16:19:091523 apache_client_count = self._get_process_count('apache')
1524 telemetry_test_count = self._get_process_count('python.*telemetry')
1525 gsutil_count = self._get_process_count('gsutil')
Dan Shif5ce2de2013-04-25 23:06:321526
Dan Shiafd0e492015-05-27 21:23:511527 health_data = {
Dan Shif5ce2de2013-04-25 23:06:321528 'free_disk': free_disk,
Dan Shid76e6bb2016-01-29 06:28:511529 'staging_thread_count': DevServerRoot._staging_thread_count,
1530 'apache_client_count': apache_client_count,
Dan Shi7247f9c2016-06-01 16:19:091531 'telemetry_test_count': telemetry_test_count,
1532 'gsutil_count': gsutil_count}
Dan Shiafd0e492015-05-27 21:23:511533 health_data.update(self._get_io_stats() or {})
1534
1535 return json.dumps(health_data)
Dan Shif5ce2de2013-04-25 23:06:321536
1537
Chris Sosadbc20082012-12-10 21:39:111538def _CleanCache(cache_dir, wipe):
1539 """Wipes any excess cached items in the cache_dir.
1540
1541 Args:
1542 cache_dir: the directory we are wiping from.
1543 wipe: If True, wipe all the contents -- not just the excess.
1544 """
1545 if wipe:
1546 # Clear the cache and exit on error.
1547 cmd = 'rm -rf %s/*' % cache_dir
1548 if os.system(cmd) != 0:
1549 _Log('Failed to clear the cache with %s' % cmd)
1550 sys.exit(1)
1551 else:
1552 # Clear all but the last N cached updates
1553 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
1554 (cache_dir, CACHED_ENTRIES))
1555 if os.system(cmd) != 0:
1556 _Log('Failed to clean up old delta cache files with %s' % cmd)
1557 sys.exit(1)
1558
1559
Chris Sosa3ae4dc12013-03-29 18:47:001560def _AddTestingOptions(parser):
1561 group = optparse.OptionGroup(
1562 parser, 'Advanced Testing Options', 'These are used by test scripts and '
1563 'developers writing integration tests utilizing the devserver. They are '
1564 'not intended to be really used outside the scope of someone '
1565 'knowledgable about the test.')
1566 group.add_option('--exit',
1567 action='store_true',
1568 help='do not start the server (yet pregenerate/clear cache)')
1569 group.add_option('--host_log',
1570 action='store_true', default=False,
1571 help='record history of host update events (/api/hostlog)')
1572 group.add_option('--max_updates',
Gabe Black3b567202015-09-23 21:07:591573 metavar='NUM', default=-1, type='int',
Chris Sosa3ae4dc12013-03-29 18:47:001574 help='maximum number of update checks handled positively '
1575 '(default: unlimited)')
1576 group.add_option('--private_key',
1577 metavar='PATH', default=None,
1578 help='path to the private key in pem format. If this is set '
1579 'the devserver will generate update payloads that are '
1580 'signed with this key.')
David Zeuthen52ccd012013-10-31 19:58:261581 group.add_option('--private_key_for_metadata_hash_signature',
1582 metavar='PATH', default=None,
1583 help='path to the private key in pem format. If this is set '
1584 'the devserver will sign the metadata hash with the given '
1585 'key and transmit in the Omaha-style XML response.')
1586 group.add_option('--public_key',
1587 metavar='PATH', default=None,
1588 help='path to the public key in pem format. If this is set '
1589 'the devserver will transmit a base64 encoded version of '
1590 'the content in the Omaha-style XML response.')
Chris Sosa3ae4dc12013-03-29 18:47:001591 group.add_option('--proxy_port',
1592 metavar='PORT', default=None, type='int',
1593 help='port to have the client connect to -- basically the '
1594 'devserver lies to the update to tell it to get the payload '
1595 'from a different port that will proxy the request back to '
1596 'the devserver. The proxy must be managed outside the '
1597 'devserver.')
1598 group.add_option('--remote_payload',
1599 action='store_true', default=False,
Chris Sosa4b951602014-04-10 03:26:071600 help='Payload is being served from a remote machine. With '
1601 'this setting enabled, this devserver instance serves as '
1602 'just an Omaha server instance. In this mode, the '
1603 'devserver enforces a few extra components of the Omaha '
Chris Sosafc715442014-04-10 03:45:231604 'protocol, such as hardware class, being sent.')
Chris Sosa3ae4dc12013-03-29 18:47:001605 group.add_option('-u', '--urlbase',
1606 metavar='URL',
Gabe Black3b567202015-09-23 21:07:591607 help='base URL for update images, other than the '
1608 'devserver. Use in conjunction with remote_payload.')
Chris Sosa3ae4dc12013-03-29 18:47:001609 parser.add_option_group(group)
1610
1611
1612def _AddUpdateOptions(parser):
1613 group = optparse.OptionGroup(
1614 parser, 'Autoupdate Options', 'These options can be used to change '
1615 'how the devserver either generates or serve update payloads. Please '
1616 'note that all of these option affect how a payload is generated and so '
1617 'do not work in archive-only mode.')
1618 group.add_option('--board',
1619 help='By default the devserver will create an update '
1620 'payload from the latest image built for the board '
1621 'a device that is requesting an update has. When we '
1622 'pre-generate an update (see below) and we do not specify '
1623 'another update_type option like image or payload, the '
1624 'devserver needs to know the board to generate the latest '
1625 'image for. This is that board.')
1626 group.add_option('--critical_update',
1627 action='store_true', default=False,
1628 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 18:47:001629 group.add_option('--image',
1630 metavar='FILE',
1631 help='Generate and serve an update using this image to any '
1632 'device that requests an update.')
Chris Sosa3ae4dc12013-03-29 18:47:001633 group.add_option('--payload',
1634 metavar='PATH',
1635 help='use the update payload from specified directory '
1636 '(update.gz).')
1637 group.add_option('-p', '--pregenerate_update',
1638 action='store_true', default=False,
1639 help='pre-generate the update payload before accepting '
1640 'update requests. Useful to help debug payload generation '
1641 'issues quickly. Also if an update payload will take a '
1642 'long time to generate, a client may timeout if you do not'
1643 'pregenerate the update.')
1644 group.add_option('--src_image',
1645 metavar='PATH', default='',
1646 help='If specified, delta updates will be generated using '
1647 'this image as the source image. Delta updates are when '
1648 'you are updating from a "source image" to a another '
1649 'image.')
1650 parser.add_option_group(group)
1651
1652
1653def _AddProductionOptions(parser):
1654 group = optparse.OptionGroup(
1655 parser, 'Advanced Server Options', 'These options can be used to changed '
1656 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 18:47:001657 group.add_option('--clear_cache',
1658 action='store_true', default=False,
1659 help='At startup, removes all cached entries from the'
1660 'devserver\'s cache.')
1661 group.add_option('--logfile',
1662 metavar='PATH',
1663 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 20:24:551664 group.add_option('--pidfile',
1665 metavar='PATH',
1666 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 19:04:131667 group.add_option('--portfile',
1668 metavar='PATH',
1669 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 18:47:001670 group.add_option('--production',
1671 action='store_true', default=False,
1672 help='have the devserver use production values when '
1673 'starting up. This includes using more threads and '
1674 'performing less logging.')
1675 parser.add_option_group(group)
1676
1677
Paul Hobbsef4e0702016-06-28 00:01:421678def MakeLogHandler(logfile):
J. Richard Barnette3d977b82013-04-23 18:05:191679 """Create a LogHandler instance used to log all messages."""
1680 hdlr_cls = handlers.TimedRotatingFileHandler
1681 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
1682 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 20:24:551683 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 18:05:191684 return hdlr
1685
1686
Chris Sosacde6bf42012-06-01 01:36:391687def main():
Chris Sosa3ae4dc12013-03-29 18:47:001688 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 21:47:021689 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 23:39:341690
1691 # get directory that the devserver is run from
1692 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 16:17:231693 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 23:39:341694 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 17:09:421695 metavar='PATH',
joychen84d13772013-08-06 16:17:231696 default=default_static_dir,
joychened64b222013-06-21 23:39:341697 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 17:09:421698 parser.add_option('--port',
1699 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 21:13:281700 help=('port for the dev server to use; if zero, binds to '
1701 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 17:09:421702 parser.add_option('-t', '--test_image',
1703 action='store_true',
joychen121fc9b2013-08-02 21:30:301704 help='Deprecated.')
joychen5260b9a2013-07-16 21:48:011705 parser.add_option('-x', '--xbuddy_manage_builds',
1706 action='store_true',
1707 default=False,
1708 help='If set, allow xbuddy to manage images in'
1709 'build/images.')
Dan Shi72b16132015-10-08 19:10:331710 parser.add_option('-a', '--android_build_credential',
1711 default=None,
1712 help='Path to a json file which contains the credential '
1713 'needed to access Android builds.')
Chris Sosa3ae4dc12013-03-29 18:47:001714 _AddProductionOptions(parser)
1715 _AddUpdateOptions(parser)
1716 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-12 02:49:011717 (options, _) = parser.parse_args()
[email protected]21a5ca32009-11-04 18:23:231718
J. Richard Barnette3d977b82013-04-23 18:05:191719 # Handle options that must be set globally in cherrypy. Do this
1720 # work up front, because calls to _Log() below depend on this
1721 # initialization.
1722 if options.production:
1723 cherrypy.config.update({'environment': 'production'})
1724 if not options.logfile:
1725 cherrypy.config.update({'log.screen': True})
1726 else:
1727 cherrypy.config.update({'log.error_file': '',
1728 'log.access_file': ''})
Paul Hobbsef4e0702016-06-28 00:01:421729 hdlr = MakeLogHandler(options.logfile)
J. Richard Barnette3d977b82013-04-23 18:05:191730 # Pylint can't seem to process these two calls properly
1731 # pylint: disable=E1101
1732 cherrypy.log.access_log.addHandler(hdlr)
1733 cherrypy.log.error_log.addHandler(hdlr)
1734 # pylint: enable=E1101
1735
joychened64b222013-06-21 23:39:341736 # set static_dir, from which everything will be served
joychen84d13772013-08-06 16:17:231737 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 22:46:221738
joychened64b222013-06-21 23:39:341739 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 18:05:191740 # If our devserver is only supposed to serve payloads, we shouldn't be
1741 # mucking with the cache at all. If the devserver hadn't previously
1742 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 18:14:071743 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 21:39:111744 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 20:12:171745 else:
1746 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-17 01:36:141747
Chris Sosadbc20082012-12-10 21:39:111748 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 23:39:341749 _Log('Serving from %s' % options.static_dir)
[email protected]21a5ca32009-11-04 18:23:231750
joychen121fc9b2013-08-02 21:30:301751 _xbuddy = xbuddy.XBuddy(options.xbuddy_manage_builds,
1752 options.board,
joychen121fc9b2013-08-02 21:30:301753 static_dir=options.static_dir)
Chris Sosa75490802013-10-01 00:21:451754 if options.clear_cache and options.xbuddy_manage_builds:
1755 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 21:30:301756
Chris Sosa6a3697f2013-01-30 00:44:431757 # We allow global use here to share with cherrypy classes.
1758 # pylint: disable=W0603
Chris Sosacde6bf42012-06-01 01:36:391759 global updater
Andrew de los Reyes52620802010-04-12 20:40:071760 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 21:30:301761 _xbuddy,
joychened64b222013-06-21 23:39:341762 static_dir=options.static_dir,
Andrew de los Reyes52620802010-04-12 20:40:071763 urlbase=options.urlbase,
Chris Sosa5d342a22010-09-28 23:54:411764 forced_image=options.image,
Gilad Arnold0c9c8602012-10-03 06:58:581765 payload_path=options.payload,
Don Garrett0ad09372010-12-07 00:20:301766 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-29 06:42:371767 src_image=options.src_image,
Chris Sosa08d55a22011-01-20 00:08:021768 board=options.board,
Chris Sosa0f1ec842011-02-15 00:33:221769 copy_to_static_root=not options.exit,
1770 private_key=options.private_key,
Gabe Black3b567202015-09-23 21:07:591771 private_key_for_metadata_hash_signature=(
1772 options.private_key_for_metadata_hash_signature),
David Zeuthen52ccd012013-10-31 19:58:261773 public_key=options.public_key,
Satoru Takabayashid733cbe2011-11-15 17:36:321774 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-03 06:58:581775 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 17:32:441776 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 19:52:231777 host_log=options.host_log,
Chris Sosa0f1ec842011-02-15 00:33:221778 )
Chris Sosa7c931362010-10-12 02:49:011779
Chris Sosa6a3697f2013-01-30 00:44:431780 if options.pregenerate_update:
1781 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 22:46:221782
J. Richard Barnette3d977b82013-04-23 18:05:191783 if options.exit:
1784 return
Chris Sosa2f1c41e2012-07-10 21:32:331785
joychen3cb228e2013-06-12 19:13:131786 dev_server = DevServerRoot(_xbuddy)
1787
Gilad Arnold11fbef42014-02-10 19:04:131788 # Patch CherryPy to support binding to any available port (--port=0).
1789 cherrypy_ext.ZeroPortPatcher.DoPatch(cherrypy)
1790
Chris Sosa855b8932013-08-21 20:24:551791 if options.pidfile:
1792 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1793
Gilad Arnold11fbef42014-02-10 19:04:131794 if options.portfile:
1795 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1796
Dan Shiafd5c6c2016-01-07 18:27:031797 if (options.android_build_credential and
1798 os.path.exists(options.android_build_credential)):
1799 try:
1800 with open(options.android_build_credential) as f:
1801 android_build.BuildAccessor.credential_info = json.load(f)
1802 except ValueError as e:
1803 _Log('Failed to load the android build credential: %s. Error: %s.' %
1804 (options.android_build_credential, e))
joychen3cb228e2013-06-12 19:13:131805 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-06-01 01:36:391806
1807
1808if __name__ == '__main__':
1809 main()