blob: 4d50fd6332632b51fce523771849517ca16a7fb5 [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):]
xixuan28681fd2016-11-23 19:13:56896 elif result == cros_update_progress.FINISHED:
xixuan28d99072016-10-06 19:24:16897 result_dict['finished'] = True
898 result_dict['status'] = result
xixuan28681fd2016-11-23 19:13:56899 elif not cros_update_progress.IsProcessAlive(pid):
xixuan28d99072016-10-06 19:24:16900 result_dict['detailed_error_msg'] = (
901 'Cros_update process terminated midway due to unknown reason. '
902 'Last update status was %s' % result)
xixuan28681fd2016-11-23 19:13:56903 else:
904 result_dict['status'] = result
905 except IOError as e:
906 if pid and cros_update_progress.IsProcessAlive(pid):
xixuan2a0970a2016-08-10 19:12:44907 os.killpg(int(pid), signal.SIGKILL)
xixuan52c2fba2016-05-21 00:02:48908
xixuan28681fd2016-11-23 19:13:56909 result_dict['detailed_error_msg'] = str(e)
910
911 return json.dumps(result_dict)
xixuan52c2fba2016-05-21 00:02:48912
913 @cherrypy.expose
914 def handler_cleanup(self, **kwargs):
xixuan3bc974e2016-10-19 00:21:43915 """Clean track status log and temp directory for CrOS auto-update process.
xixuan52c2fba2016-05-21 00:02:48916
917 Args:
918 kwargs:
919 host_name: the hostname of the DUT to auto-update.
920 pid: the background process id of cros-update.
921 """
922 if 'host_name' not in kwargs:
923 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
924
925 if 'pid' not in kwargs:
926 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
927
928 host_name = kwargs['host_name']
929 pid = kwargs['pid']
930 cros_update_progress.DelTrackStatusFile(host_name, pid)
xixuan3bc974e2016-10-19 00:21:43931 cros_update_progress.DelAUTempDirectory(host_name, pid)
xixuan52c2fba2016-05-21 00:02:48932
933 @cherrypy.expose
934 def kill_au_proc(self, **kwargs):
935 """Kill CrOS auto-update process using given process id.
936
937 Args:
938 kwargs:
939 host_name: Kill all the CrOS auto-update process of this host.
940
941 Returns:
942 True if all processes are killed properly.
943 """
944 if 'host_name' not in kwargs:
945 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
946
947 host_name = kwargs['host_name']
xixuan3bc974e2016-10-19 00:21:43948 track_log_list = cros_update_progress.GetAllTrackStatusFileByHostName(
949 host_name)
xixuan52c2fba2016-05-21 00:02:48950 for log in track_log_list:
951 # The track log's full path is: path/host_name_pid.log
952 # Use splitext to remove file extension, then parse pid from the
953 # filename.
954 pid = os.path.splitext(os.path.basename(log))[0][len(host_name)+1:]
955 if cros_update_progress.IsProcessAlive(pid):
xixuan2a0970a2016-08-10 19:12:44956 os.killpg(int(pid), signal.SIGKILL)
xixuan52c2fba2016-05-21 00:02:48957
958 cros_update_progress.DelTrackStatusFile(host_name, pid)
xixuan1bbfaba2016-10-14 00:53:22959 cros_update_progress.DelExecuteLogFile(host_name, pid)
xixuan52c2fba2016-05-21 00:02:48960
961 return 'True'
962
963 @cherrypy.expose
964 def collect_cros_au_log(self, **kwargs):
965 """Collect CrOS auto-update log.
966
967 Args:
968 kwargs:
969 host_name: the hostname of the DUT to auto-update.
970 pid: the background process id of cros-update.
971
972 Returns:
973 A string contains the whole content of the execute log file.
974 """
975 if 'host_name' not in kwargs:
976 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
977
978 if 'pid' not in kwargs:
979 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
980
981 host_name = kwargs['host_name']
982 pid = kwargs['pid']
xixuan3bc974e2016-10-19 00:21:43983
984 # Fetch the execute log recorded by cros_update_progress.
xixuan1bbfaba2016-10-14 00:53:22985 au_log = cros_update_progress.ReadExecuteLogFile(host_name, pid)
986 cros_update_progress.DelExecuteLogFile(host_name, pid)
987 return au_log
988
xixuan52c2fba2016-05-21 00:02:48989 @cherrypy.expose
Dan Shi2f136862016-02-11 23:38:38990 def locate_file(self, **kwargs):
991 """Get the path to the given file name.
992
993 This method looks up the given file name inside specified build artifacts.
994 One use case is to help caller to locate an apk file inside a build
995 artifact. The location of the apk file could be different based on the
996 branch and target.
997
998 Args:
999 file_name: Name of the file to look for.
1000 artifacts: A list of artifact names to search for the file.
1001
1002 Returns:
1003 Path to the file with the given name. It's relative to the folder for the
1004 build, e.g., DATA/priv-app/sl4a/sl4a.apk
Dan Shi2f136862016-02-11 23:38:381005 """
1006 dl, _ = _get_downloader_and_factory(kwargs)
1007 try:
1008 file_name = kwargs['file_name'].lower()
1009 artifacts = kwargs['artifacts']
1010 except KeyError:
1011 raise DevServerError('`file_name` and `artifacts` are required to search '
1012 'for a file in build artifacts.')
1013 build_path = dl.GetBuildDir()
1014 for artifact in artifacts:
1015 # Get the unzipped folder of the artifact. If it's not defined in
1016 # ARTIFACT_UNZIP_FOLDER_MAP, assume the files are unzipped to the build
1017 # directory directly.
1018 folder = artifact_info.ARTIFACT_UNZIP_FOLDER_MAP.get(artifact, '')
1019 artifact_path = os.path.join(build_path, folder)
1020 for root, _, filenames in os.walk(artifact_path):
1021 if file_name in set([f.lower() for f in filenames]):
1022 return os.path.relpath(os.path.join(root, file_name), build_path)
1023 raise DevServerError('File `%s` can not be found in artifacts: %s' %
1024 (file_name, artifacts))
1025
1026 @cherrypy.expose
Simran Basi4baad082013-02-14 21:39:181027 def setup_telemetry(self, **kwargs):
1028 """Extracts and sets up telemetry
1029
1030 This method goes through the telemetry deps packages, and stages them on
1031 the devserver to be used by the drones and the telemetry tests.
1032
1033 Args:
1034 archive_url: Google Storage URL for the build.
1035
1036 Returns:
1037 Path to the source folder for the telemetry codebase once it is staged.
1038 """
Gabe Black3b567202015-09-23 21:07:591039 dl = _get_downloader(kwargs)
Simran Basi4baad082013-02-14 21:39:181040
Gabe Black3b567202015-09-23 21:07:591041 build_path = dl.GetBuildDir()
Simran Basi4baad082013-02-14 21:39:181042 deps_path = os.path.join(build_path, 'autotest/packages')
1043 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
1044 src_folder = os.path.join(telemetry_path, 'src')
1045
1046 with self._telemetry_lock_dict.lock(telemetry_path):
1047 if os.path.exists(src_folder):
1048 # Telemetry is already fully stage return
1049 return src_folder
1050
1051 common_util.MkDirP(telemetry_path)
1052
1053 # Copy over the required deps tar balls to the telemetry directory.
1054 for dep in TELEMETRY_DEPS:
1055 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 23:40:041056 if not os.path.exists(dep_path):
1057 # This dep does not exist (could be new), do not extract it.
1058 continue
Simran Basi4baad082013-02-14 21:39:181059 try:
1060 common_util.ExtractTarball(dep_path, telemetry_path)
1061 except common_util.CommonUtilError as e:
1062 shutil.rmtree(telemetry_path)
1063 raise DevServerError(str(e))
1064
1065 # By default all the tarballs extract to test_src but some parts of
1066 # the telemetry code specifically hardcoded to exist inside of 'src'.
1067 test_src = os.path.join(telemetry_path, 'test_src')
1068 try:
1069 shutil.move(test_src, src_folder)
1070 except shutil.Error:
1071 # This can occur if src_folder already exists. Remove and retry move.
1072 shutil.rmtree(src_folder)
Gabe Black3b567202015-09-23 21:07:591073 raise DevServerError(
1074 'Failure in telemetry setup for build %s. Appears that the '
1075 'test_src to src move failed.' % dl.GetBuild())
Simran Basi4baad082013-02-14 21:39:181076
1077 return src_folder
1078
1079 @cherrypy.expose
Chris Sosa76e44b92013-01-31 20:11:381080 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 19:22:361081 """Symbolicates a minidump using pre-downloaded symbols, returns it.
1082
1083 Callers will need to POST to this URL with a body of MIME-type
1084 "multipart/form-data".
1085 The body should include a single argument, 'minidump', containing the
1086 binary-formatted minidump to symbolicate.
1087
Chris Masone816e38c2012-05-02 19:22:361088 Args:
Chris Sosa76e44b92013-01-31 20:11:381089 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 19:22:361090 minidump: The binary minidump file to symbolicate.
1091 """
Chris Sosa76e44b92013-01-31 20:11:381092 # Ensure the symbols have been staged.
Dan Shif08fe492016-10-04 21:39:251093 # Try debug.tar.xz first, then debug.tgz
1094 for artifact in (artifact_info.SYMBOLS_ONLY, artifact_info.SYMBOLS):
1095 kwargs['artifacts'] = artifact
1096 dl = _get_downloader(kwargs)
1097
1098 try:
1099 if self.stage(**kwargs) == 'Success':
1100 break
1101 except build_artifact.ArtifactDownloadError:
1102 continue
1103 else:
Gabe Black3b567202015-09-23 21:07:591104 raise DevServerError('Failed to stage symbols for %s' %
1105 dl.DescribeSource())
Chris Sosa76e44b92013-01-31 20:11:381106
Chris Masone816e38c2012-05-02 19:22:361107 to_return = ''
1108 with tempfile.NamedTemporaryFile() as local:
1109 while True:
1110 data = minidump.file.read(8192)
1111 if not data:
1112 break
1113 local.write(data)
Chris Sosa76e44b92013-01-31 20:11:381114
Chris Masone816e38c2012-05-02 19:22:361115 local.flush()
Chris Sosa76e44b92013-01-31 20:11:381116
Gabe Black3b567202015-09-23 21:07:591117 symbols_directory = os.path.join(dl.GetBuildDir(), 'debug', 'breakpad')
Chris Sosa76e44b92013-01-31 20:11:381118
1119 stackwalk = subprocess.Popen(
1120 ['minidump_stackwalk', local.name, symbols_directory],
1121 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1122
Chris Masone816e38c2012-05-02 19:22:361123 to_return, error_text = stackwalk.communicate()
1124 if stackwalk.returncode != 0:
1125 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
1126 error_text, stackwalk.returncode))
1127
1128 return to_return
1129
1130 @cherrypy.expose
Don Garrettf84631a2014-01-08 02:21:261131 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 19:31:361132 """Return a string representing the latest build for a given target.
1133
1134 Args:
1135 target: The build target, typically a combination of the board and the
1136 type of build e.g. x86-mario-release.
1137 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
1138 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-08 02:21:261139
Scott Zawalski16954532012-03-20 19:31:361140 Returns:
1141 A string representation of the latest build if one exists, i.e.
1142 R19-1993.0.0-a1-b1480.
1143 An empty string if no latest could be found.
1144 """
Don Garrettf84631a2014-01-08 02:21:261145 if not kwargs:
Scott Zawalski16954532012-03-20 19:31:361146 return _PrintDocStringAsHTML(self.latestbuild)
1147
Don Garrettf84631a2014-01-08 02:21:261148 if 'target' not in kwargs:
Chris Sosa4b951602014-04-10 03:26:071149 raise common_util.DevServerHTTPError(500, 'Error: target= is required!')
Dan Shi61305df2015-10-26 23:52:351150
1151 if _is_android_build_request(kwargs):
1152 branch = kwargs.get('branch', None)
1153 target = kwargs.get('target', None)
1154 if not target or not branch:
1155 raise DevServerError(
xixuan52c2fba2016-05-21 00:02:481156 'Both target and branch must be specified to query for the latest '
1157 'Android build.')
Dan Shi61305df2015-10-26 23:52:351158 return android_build.BuildAccessor.GetLatestBuildID(target, branch)
1159
Scott Zawalski16954532012-03-20 19:31:361160 try:
Gilad Arnoldc65330c2012-09-20 22:17:481161 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-08 02:21:261162 updater.static_dir, kwargs['target'],
1163 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 17:05:011164 except common_util.CommonUtilError as errmsg:
Chris Sosa4b951602014-04-10 03:26:071165 raise common_util.DevServerHTTPError(500, str(errmsg))
Scott Zawalski16954532012-03-20 19:31:361166
1167 @cherrypy.expose
xixuan7efd0002016-04-14 22:34:011168 def list_suite_controls(self, **kwargs):
1169 """Return a list of contents of all known control files.
1170
1171 Example URL:
1172 To List all control files' content:
1173 http://dev-server/list_suite_controls?suite_name=bvt&
1174 build=daisy_spring-release/R29-4279.0.0
1175
1176 Args:
1177 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
1178 suite_name: List the control files belonging to that suite.
1179
1180 Returns:
Dan Shia1cd6522016-04-18 23:07:211181 A dictionary of all control files's path to its content for given suite.
xixuan7efd0002016-04-14 22:34:011182 """
1183 if not kwargs:
1184 return _PrintDocStringAsHTML(self.controlfiles)
1185
1186 if 'build' not in kwargs:
1187 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
1188
1189 if 'suite_name' not in kwargs:
Dan Shia1cd6522016-04-18 23:07:211190 raise common_util.DevServerHTTPError(500,
1191 'Error: suite_name= is required!')
xixuan7efd0002016-04-14 22:34:011192
1193 control_file_list = [
1194 line.rstrip() for line in common_util.GetControlFileListForSuite(
1195 updater.static_dir, kwargs['build'],
1196 kwargs['suite_name']).splitlines()]
1197
Dan Shia1cd6522016-04-18 23:07:211198 control_file_content_dict = {}
xixuan7efd0002016-04-14 22:34:011199 for control_path in control_file_list:
Dan Shia1cd6522016-04-18 23:07:211200 control_file_content_dict[control_path] = (common_util.GetControlFile(
xixuan7efd0002016-04-14 22:34:011201 updater.static_dir, kwargs['build'], control_path))
1202
Dan Shia1cd6522016-04-18 23:07:211203 return json.dumps(control_file_content_dict)
xixuan7efd0002016-04-14 22:34:011204
1205 @cherrypy.expose
Don Garrettf84631a2014-01-08 02:21:261206 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 22:17:281207 """Return a control file or a list of all known control files.
1208
1209 Example URL:
1210 To List all control files:
beepsbd337242013-07-10 05:44:061211 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
1212 To List all control files for, say, the bvt suite:
1213 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 22:17:281214 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 20:12:421215 http://dev-server/controlfiles?board=x86-alex-release&build=R18-1514.0.0&control_path=client/sleeptest/control
Scott Zawalski4647ce62012-01-03 22:17:281216
1217 Args:
Scott Zawalski84a39c92012-01-13 20:12:421218 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 22:17:281219 control_path: If you want the contents of a control file set this
1220 to the path. E.g. client/site_tests/sleeptest/control
1221 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-10 05:44:061222 suite_name: If control_path is not specified but a suite_name is
1223 specified, list the control files belonging to that suite instead of
1224 all control files. The empty string for suite_name will list all control
1225 files for the build.
Don Garrettf84631a2014-01-08 02:21:261226
Scott Zawalski4647ce62012-01-03 22:17:281227 Returns:
1228 Contents of a control file if control_path is provided.
1229 A list of control files if no control_path is provided.
1230 """
Don Garrettf84631a2014-01-08 02:21:261231 if not kwargs:
Scott Zawalski4647ce62012-01-03 22:17:281232 return _PrintDocStringAsHTML(self.controlfiles)
1233
Don Garrettf84631a2014-01-08 02:21:261234 if 'build' not in kwargs:
Chris Sosa4b951602014-04-10 03:26:071235 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 22:17:281236
Don Garrettf84631a2014-01-08 02:21:261237 if 'control_path' not in kwargs:
1238 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-10 05:44:061239 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-08 02:21:261240 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-10 05:44:061241 else:
1242 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-08 02:21:261243 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 22:17:281244 else:
Gilad Arnoldc65330c2012-09-20 22:17:481245 return common_util.GetControlFile(
Don Garrettf84631a2014-01-08 02:21:261246 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-13 02:39:181247
1248 @cherrypy.expose
Simran Basi99e63c02014-05-20 17:39:521249 def xbuddy_translate(self, *args, **kwargs):
Yu-Ju Hong1bdb7a92014-04-10 23:02:111250 """Translates an xBuddy path to a real path to artifact if it exists.
1251
1252 Args:
Simran Basi99e63c02014-05-20 17:39:521253 args: An xbuddy path in the form of {local|remote}/build_id/artifact.
1254 Local searches the devserver's static directory. Remote searches a
1255 Google Storage image archive.
1256
1257 Kwargs:
1258 image_dir: Google Storage image archive to search in if requesting a
1259 remote artifact. If none uses the default bucket.
Yu-Ju Hong1bdb7a92014-04-10 23:02:111260
1261 Returns:
Simran Basi99e63c02014-05-20 17:39:521262 String in the format of build_id/artifact as stored on the local server
1263 or in Google Storage.
Yu-Ju Hong1bdb7a92014-04-10 23:02:111264 """
Simran Basi99e63c02014-05-20 17:39:521265 build_id, filename = self._xbuddy.Translate(
Gabe Black3b567202015-09-23 21:07:591266 args, image_dir=kwargs.get('image_dir'))
Yu-Ju Hong1bdb7a92014-04-10 23:02:111267 response = os.path.join(build_id, filename)
1268 _Log('Path translation requested, returning: %s', response)
1269 return response
1270
1271 @cherrypy.expose
joycheneaf4cfc2013-07-02 15:38:571272 def xbuddy(self, *args, **kwargs):
1273 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 19:13:131274
1275 Args:
joycheneaf4cfc2013-07-02 15:38:571276 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 21:30:301277 components of the path. The path can be understood as
1278 "{local|remote}/build_id/artifact" where build_id is composed of
1279 "board/version."
joycheneaf4cfc2013-07-02 15:38:571280
joychen121fc9b2013-08-02 21:30:301281 The first path element is optional, and can be "remote" or "local"
1282 If local (the default), devserver will not attempt to access Google
1283 Storage, and will only search the static directory for the files.
1284 If remote, devserver will try to obtain the artifact off GS if it's
1285 not found locally.
1286 The board is the familiar board name, optionally suffixed.
1287 The version can be the google storage version number, and may also be
1288 any of a number of xBuddy defined version aliases that will be
1289 translated into the latest built image that fits the description.
1290 Defaults to latest.
1291 The artifact is one of a number of image or artifact aliases used by
1292 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 15:38:571293
1294 Kwargs:
Yu-Ju Hong51495eb2013-12-13 01:08:431295 for_update: {true|false}
1296 if true, pregenerates the update payloads for the image,
1297 and returns the update uri to pass to the
1298 update_engine_client.
joychen3cb228e2013-06-12 19:13:131299 return_dir: {true|false}
1300 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-13 01:08:431301 relative_path: {true|false}
1302 if set to true, returns the relative path to the payload
1303 directory from static_dir.
joychen3cb228e2013-06-12 19:13:131304 Example URL:
joycheneaf4cfc2013-07-02 15:38:571305 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 19:13:131306 or
joycheneaf4cfc2013-07-02 15:38:571307 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 19:13:131308
1309 Returns:
Yu-Ju Hong51495eb2013-12-13 01:08:431310 If |for_update|, returns a redirect to the image or update file
1311 on the devserver. E.g.,
1312 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
1313 chromium-test-image.bin
1314 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
1315 http://host:port/static/x86-generic-release/R26-4000.0.0/
1316 If |relative_path| is true, return a relative path the folder where the
1317 payloads are. E.g.,
1318 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 19:13:131319 """
Chris Sosa75490802013-10-01 00:21:451320 boolean_string = kwargs.get('for_update')
1321 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-13 01:08:431322 boolean_string = kwargs.get('return_dir')
1323 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
1324 boolean_string = kwargs.get('relative_path')
1325 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 21:30:301326
Yu-Ju Hong51495eb2013-12-13 01:08:431327 if return_dir and relative_path:
Chris Sosa4b951602014-04-10 03:26:071328 raise common_util.DevServerHTTPError(
1329 500, 'Cannot specify both return_dir and relative_path')
Chris Sosa75490802013-10-01 00:21:451330
1331 # For updates, we optimize downloading of test images.
1332 file_name = None
1333 build_id = None
1334 if for_update:
1335 try:
Yu-Ju Hong1bdb7a92014-04-10 23:02:111336 build_id = self._xbuddy.StageTestArtifactsForUpdate(args)
Chris Sosa75490802013-10-01 00:21:451337 except build_artifact.ArtifactDownloadError:
1338 build_id = None
1339
1340 if not build_id:
1341 build_id, file_name = self._xbuddy.Get(args)
1342
Yu-Ju Hong51495eb2013-12-13 01:08:431343 if for_update:
1344 _Log('Payload generation triggered by request')
1345 # Forces payload to be in cache and symlinked into build_id dir.
Chris Sosa75490802013-10-01 00:21:451346 updater.GetUpdateForLabel(autoupdate.FORCED_UPDATE, build_id,
1347 image_name=file_name)
Yu-Ju Hong51495eb2013-12-13 01:08:431348
1349 response = None
1350 if return_dir:
1351 response = os.path.join(cherrypy.request.base, 'static', build_id)
1352 _Log('Directory requested, returning: %s', response)
1353 elif relative_path:
1354 response = build_id
1355 _Log('Relative path requested, returning: %s', response)
1356 elif for_update:
1357 response = os.path.join(cherrypy.request.base, 'update', build_id)
1358 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 19:13:131359 else:
Yu-Ju Hong51495eb2013-12-13 01:08:431360 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 21:30:301361 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-13 01:08:431362 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 21:30:301363 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 19:13:131364
Yu-Ju Hong51495eb2013-12-13 01:08:431365 return response
1366
joychen3cb228e2013-06-12 19:13:131367 @cherrypy.expose
1368 def xbuddy_list(self):
1369 """Lists the currently available images & time since last access.
1370
Gilad Arnold452fd272014-02-04 19:09:281371 Returns:
1372 A string representation of a list of tuples [(build_id, time since last
1373 access),...]
joychen3cb228e2013-06-12 19:13:131374 """
1375 return self._xbuddy.List()
1376
1377 @cherrypy.expose
1378 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 19:09:281379 """Returns the number of images cached by xBuddy."""
joychen3cb228e2013-06-12 19:13:131380 return self._xbuddy.Capacity()
1381
1382 @cherrypy.expose
Chris Sosa7c931362010-10-12 02:49:011383 def index(self):
Gilad Arnoldf8f769f2012-09-24 15:43:011384 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 15:43:011385 return ('Welcome to the Dev Server!<br>\n'
1386 '<br>\n'
1387 'Here are the available methods, click for documentation:<br>\n'
1388 '<br>\n'
1389 '%s' %
1390 '<br>\n'.join(
1391 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 18:52:381392 for name in _FindExposedMethods(
1393 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 15:43:011394
1395 @cherrypy.expose
1396 def doc(self, *args):
1397 """Shows the documentation for available methods / URLs.
1398
1399 Example:
1400 http://myhost/doc/update
1401 """
Gilad Arnoldd5ebaaa2012-10-02 18:52:381402 name = '/'.join(args)
1403 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 15:43:011404 if not method:
1405 raise DevServerError("No exposed method named `%s'" % name)
1406 if not method.__doc__:
1407 raise DevServerError("No documentation for exposed method `%s'" % name)
1408 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-12 02:49:011409
Dale Curtisc9aaf3a2011-08-09 22:47:401410 @cherrypy.expose
Chris Sosa7c931362010-10-12 02:49:011411 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 15:43:011412 """Handles an update check from a Chrome OS client.
1413
1414 The HTTP request should contain the standard Omaha-style XML blob. The URL
1415 line may contain an additional intermediate path to the update payload.
1416
joychen121fc9b2013-08-02 21:30:301417 This request can be handled in one of 4 ways, depending on the devsever
1418 settings and intermediate path.
joychenb0dfe552013-07-30 17:02:061419
joychen121fc9b2013-08-02 21:30:301420 1. No intermediate path
1421 If no intermediate path is given, the default behavior is to generate an
1422 update payload from the latest test image locally built for the board
1423 specified in the xml. Devserver serves the generated payload.
1424
1425 2. Path explicitly invokes XBuddy
1426 If there is a path given, it can explicitly invoke xbuddy by prefixing it
1427 with 'xbuddy'. This path is then used to acquire an image binary for the
1428 devserver to generate an update payload from. Devserver then serves this
1429 payload.
1430
1431 3. Path is left for the devserver to interpret.
1432 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
1433 to generate a payload from the test image in that directory and serve it.
1434
1435 4. The devserver is in a 'forced' mode. TO BE DEPRECATED
1436 This comes from the usage of --forced_payload or --image when starting the
1437 devserver. No matter what path (or no path) gets passed in, devserver will
1438 serve the update payload (--forced_payload) or generate an update payload
1439 from the image (--image).
1440
1441 Examples:
1442 1. No intermediate path
1443 update_engine_client --omaha_url=http://myhost/update
1444 This generates an update payload from the latest test image locally built
1445 for the board specified in the xml.
1446
1447 2. Explicitly invoke xbuddy
1448 update_engine_client --omaha_url=
1449 http://myhost/update/xbuddy/remote/board/version/dev
1450 This would go to GS to download the dev image for the board, from which
1451 the devserver would generate a payload to serve.
1452
1453 3. Give a path for devserver to interpret
1454 update_engine_client --omaha_url=http://myhost/update/some/random/path
1455 This would attempt, in order to:
1456 a) Generate an update from a test image binary if found in
1457 static_dir/some/random/path.
1458 b) Serve an update payload found in static_dir/some/random/path.
1459 c) Hope that some/random/path takes the form "board/version" and
1460 and attempt to download an update payload for that board/version
1461 from GS.
Gilad Arnoldf8f769f2012-09-24 15:43:011462 """
joychen121fc9b2013-08-02 21:30:301463 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 21:47:021464 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-12 02:49:011465 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-12 02:49:011466
joychen121fc9b2013-08-02 21:30:301467 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 22:46:221468
Dan Shiafd0e492015-05-27 21:23:511469 @require_psutil()
1470 def _get_io_stats(self):
1471 """Get the IO stats as a dictionary.
1472
Gabe Black3b567202015-09-23 21:07:591473 Returns:
1474 A dictionary of IO stats collected by psutil.
Dan Shiafd0e492015-05-27 21:23:511475 """
1476 return {'disk_read_bytes_per_second': self.disk_read_bytes_per_sec,
1477 'disk_write_bytes_per_second': self.disk_write_bytes_per_sec,
1478 'disk_total_bytes_per_second': (self.disk_read_bytes_per_sec +
1479 self.disk_write_bytes_per_sec),
1480 'network_sent_bytes_per_second': self.network_sent_bytes_per_sec,
1481 'network_recv_bytes_per_second': self.network_recv_bytes_per_sec,
1482 'network_total_bytes_per_second': (self.network_sent_bytes_per_sec +
1483 self.network_recv_bytes_per_sec),
1484 'cpu_percent': psutil.cpu_percent(),}
1485
Dan Shi7247f9c2016-06-01 16:19:091486
1487 def _get_process_count(self, process_cmd_pattern):
1488 """Get the count of processes that match the given command pattern.
1489
1490 Args:
1491 process_cmd_pattern: The regex pattern of process command to match.
1492
1493 Returns:
1494 The count of processes that match the given command pattern.
1495 """
1496 try:
1497 return int(subprocess.check_output(
1498 'pgrep -fc "%s"' % process_cmd_pattern, shell=True))
1499 except subprocess.CalledProcessError:
1500 return 0
1501
1502
Dan Shif5ce2de2013-04-25 23:06:321503 @cherrypy.expose
1504 def check_health(self):
1505 """Collect the health status of devserver to see if it's ready for staging.
1506
Gilad Arnold452fd272014-02-04 19:09:281507 Returns:
1508 A JSON dictionary containing all or some of the following fields:
1509 free_disk (int): free disk space in GB
1510 staging_thread_count (int): number of devserver threads currently staging
1511 an image
Dan Shi7247f9c2016-06-01 16:19:091512 apache_client_count (int): count of Apache processes.
1513 telemetry_test_count (int): count of telemetry tests.
1514 gsutil_count (int): count of gsutil processes.
Dan Shif5ce2de2013-04-25 23:06:321515 """
1516 # Get free disk space.
1517 stat = os.statvfs(updater.static_dir)
1518 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
Dan Shi7247f9c2016-06-01 16:19:091519 apache_client_count = self._get_process_count('apache')
1520 telemetry_test_count = self._get_process_count('python.*telemetry')
1521 gsutil_count = self._get_process_count('gsutil')
Dan Shif5ce2de2013-04-25 23:06:321522
Dan Shiafd0e492015-05-27 21:23:511523 health_data = {
Dan Shif5ce2de2013-04-25 23:06:321524 'free_disk': free_disk,
Dan Shid76e6bb2016-01-29 06:28:511525 'staging_thread_count': DevServerRoot._staging_thread_count,
1526 'apache_client_count': apache_client_count,
Dan Shi7247f9c2016-06-01 16:19:091527 'telemetry_test_count': telemetry_test_count,
1528 'gsutil_count': gsutil_count}
Dan Shiafd0e492015-05-27 21:23:511529 health_data.update(self._get_io_stats() or {})
1530
1531 return json.dumps(health_data)
Dan Shif5ce2de2013-04-25 23:06:321532
1533
Chris Sosadbc20082012-12-10 21:39:111534def _CleanCache(cache_dir, wipe):
1535 """Wipes any excess cached items in the cache_dir.
1536
1537 Args:
1538 cache_dir: the directory we are wiping from.
1539 wipe: If True, wipe all the contents -- not just the excess.
1540 """
1541 if wipe:
1542 # Clear the cache and exit on error.
1543 cmd = 'rm -rf %s/*' % cache_dir
1544 if os.system(cmd) != 0:
1545 _Log('Failed to clear the cache with %s' % cmd)
1546 sys.exit(1)
1547 else:
1548 # Clear all but the last N cached updates
1549 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
1550 (cache_dir, CACHED_ENTRIES))
1551 if os.system(cmd) != 0:
1552 _Log('Failed to clean up old delta cache files with %s' % cmd)
1553 sys.exit(1)
1554
1555
Chris Sosa3ae4dc12013-03-29 18:47:001556def _AddTestingOptions(parser):
1557 group = optparse.OptionGroup(
1558 parser, 'Advanced Testing Options', 'These are used by test scripts and '
1559 'developers writing integration tests utilizing the devserver. They are '
1560 'not intended to be really used outside the scope of someone '
1561 'knowledgable about the test.')
1562 group.add_option('--exit',
1563 action='store_true',
1564 help='do not start the server (yet pregenerate/clear cache)')
1565 group.add_option('--host_log',
1566 action='store_true', default=False,
1567 help='record history of host update events (/api/hostlog)')
1568 group.add_option('--max_updates',
Gabe Black3b567202015-09-23 21:07:591569 metavar='NUM', default=-1, type='int',
Chris Sosa3ae4dc12013-03-29 18:47:001570 help='maximum number of update checks handled positively '
1571 '(default: unlimited)')
1572 group.add_option('--private_key',
1573 metavar='PATH', default=None,
1574 help='path to the private key in pem format. If this is set '
1575 'the devserver will generate update payloads that are '
1576 'signed with this key.')
David Zeuthen52ccd012013-10-31 19:58:261577 group.add_option('--private_key_for_metadata_hash_signature',
1578 metavar='PATH', default=None,
1579 help='path to the private key in pem format. If this is set '
1580 'the devserver will sign the metadata hash with the given '
1581 'key and transmit in the Omaha-style XML response.')
1582 group.add_option('--public_key',
1583 metavar='PATH', default=None,
1584 help='path to the public key in pem format. If this is set '
1585 'the devserver will transmit a base64 encoded version of '
1586 'the content in the Omaha-style XML response.')
Chris Sosa3ae4dc12013-03-29 18:47:001587 group.add_option('--proxy_port',
1588 metavar='PORT', default=None, type='int',
1589 help='port to have the client connect to -- basically the '
1590 'devserver lies to the update to tell it to get the payload '
1591 'from a different port that will proxy the request back to '
1592 'the devserver. The proxy must be managed outside the '
1593 'devserver.')
1594 group.add_option('--remote_payload',
1595 action='store_true', default=False,
Chris Sosa4b951602014-04-10 03:26:071596 help='Payload is being served from a remote machine. With '
1597 'this setting enabled, this devserver instance serves as '
1598 'just an Omaha server instance. In this mode, the '
1599 'devserver enforces a few extra components of the Omaha '
Chris Sosafc715442014-04-10 03:45:231600 'protocol, such as hardware class, being sent.')
Chris Sosa3ae4dc12013-03-29 18:47:001601 group.add_option('-u', '--urlbase',
1602 metavar='URL',
Gabe Black3b567202015-09-23 21:07:591603 help='base URL for update images, other than the '
1604 'devserver. Use in conjunction with remote_payload.')
Chris Sosa3ae4dc12013-03-29 18:47:001605 parser.add_option_group(group)
1606
1607
1608def _AddUpdateOptions(parser):
1609 group = optparse.OptionGroup(
1610 parser, 'Autoupdate Options', 'These options can be used to change '
1611 'how the devserver either generates or serve update payloads. Please '
1612 'note that all of these option affect how a payload is generated and so '
1613 'do not work in archive-only mode.')
1614 group.add_option('--board',
1615 help='By default the devserver will create an update '
1616 'payload from the latest image built for the board '
1617 'a device that is requesting an update has. When we '
1618 'pre-generate an update (see below) and we do not specify '
1619 'another update_type option like image or payload, the '
1620 'devserver needs to know the board to generate the latest '
1621 'image for. This is that board.')
1622 group.add_option('--critical_update',
1623 action='store_true', default=False,
1624 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 18:47:001625 group.add_option('--image',
1626 metavar='FILE',
1627 help='Generate and serve an update using this image to any '
1628 'device that requests an update.')
Chris Sosa3ae4dc12013-03-29 18:47:001629 group.add_option('--payload',
1630 metavar='PATH',
1631 help='use the update payload from specified directory '
1632 '(update.gz).')
1633 group.add_option('-p', '--pregenerate_update',
1634 action='store_true', default=False,
1635 help='pre-generate the update payload before accepting '
1636 'update requests. Useful to help debug payload generation '
1637 'issues quickly. Also if an update payload will take a '
1638 'long time to generate, a client may timeout if you do not'
1639 'pregenerate the update.')
1640 group.add_option('--src_image',
1641 metavar='PATH', default='',
1642 help='If specified, delta updates will be generated using '
1643 'this image as the source image. Delta updates are when '
1644 'you are updating from a "source image" to a another '
1645 'image.')
1646 parser.add_option_group(group)
1647
1648
1649def _AddProductionOptions(parser):
1650 group = optparse.OptionGroup(
1651 parser, 'Advanced Server Options', 'These options can be used to changed '
1652 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 18:47:001653 group.add_option('--clear_cache',
1654 action='store_true', default=False,
1655 help='At startup, removes all cached entries from the'
1656 'devserver\'s cache.')
1657 group.add_option('--logfile',
1658 metavar='PATH',
1659 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 20:24:551660 group.add_option('--pidfile',
1661 metavar='PATH',
1662 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 19:04:131663 group.add_option('--portfile',
1664 metavar='PATH',
1665 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 18:47:001666 group.add_option('--production',
1667 action='store_true', default=False,
1668 help='have the devserver use production values when '
1669 'starting up. This includes using more threads and '
1670 'performing less logging.')
1671 parser.add_option_group(group)
1672
1673
Paul Hobbsef4e0702016-06-28 00:01:421674def MakeLogHandler(logfile):
J. Richard Barnette3d977b82013-04-23 18:05:191675 """Create a LogHandler instance used to log all messages."""
1676 hdlr_cls = handlers.TimedRotatingFileHandler
1677 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
1678 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 20:24:551679 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 18:05:191680 return hdlr
1681
1682
Chris Sosacde6bf42012-06-01 01:36:391683def main():
Chris Sosa3ae4dc12013-03-29 18:47:001684 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 21:47:021685 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 23:39:341686
1687 # get directory that the devserver is run from
1688 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 16:17:231689 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 23:39:341690 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 17:09:421691 metavar='PATH',
joychen84d13772013-08-06 16:17:231692 default=default_static_dir,
joychened64b222013-06-21 23:39:341693 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 17:09:421694 parser.add_option('--port',
1695 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 21:13:281696 help=('port for the dev server to use; if zero, binds to '
1697 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 17:09:421698 parser.add_option('-t', '--test_image',
1699 action='store_true',
joychen121fc9b2013-08-02 21:30:301700 help='Deprecated.')
joychen5260b9a2013-07-16 21:48:011701 parser.add_option('-x', '--xbuddy_manage_builds',
1702 action='store_true',
1703 default=False,
1704 help='If set, allow xbuddy to manage images in'
1705 'build/images.')
Dan Shi72b16132015-10-08 19:10:331706 parser.add_option('-a', '--android_build_credential',
1707 default=None,
1708 help='Path to a json file which contains the credential '
1709 'needed to access Android builds.')
Chris Sosa3ae4dc12013-03-29 18:47:001710 _AddProductionOptions(parser)
1711 _AddUpdateOptions(parser)
1712 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-12 02:49:011713 (options, _) = parser.parse_args()
[email protected]21a5ca32009-11-04 18:23:231714
J. Richard Barnette3d977b82013-04-23 18:05:191715 # Handle options that must be set globally in cherrypy. Do this
1716 # work up front, because calls to _Log() below depend on this
1717 # initialization.
1718 if options.production:
1719 cherrypy.config.update({'environment': 'production'})
1720 if not options.logfile:
1721 cherrypy.config.update({'log.screen': True})
1722 else:
1723 cherrypy.config.update({'log.error_file': '',
1724 'log.access_file': ''})
Paul Hobbsef4e0702016-06-28 00:01:421725 hdlr = MakeLogHandler(options.logfile)
J. Richard Barnette3d977b82013-04-23 18:05:191726 # Pylint can't seem to process these two calls properly
1727 # pylint: disable=E1101
1728 cherrypy.log.access_log.addHandler(hdlr)
1729 cherrypy.log.error_log.addHandler(hdlr)
1730 # pylint: enable=E1101
1731
joychened64b222013-06-21 23:39:341732 # set static_dir, from which everything will be served
joychen84d13772013-08-06 16:17:231733 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 22:46:221734
joychened64b222013-06-21 23:39:341735 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 18:05:191736 # If our devserver is only supposed to serve payloads, we shouldn't be
1737 # mucking with the cache at all. If the devserver hadn't previously
1738 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 18:14:071739 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 21:39:111740 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 20:12:171741 else:
1742 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-17 01:36:141743
Chris Sosadbc20082012-12-10 21:39:111744 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 23:39:341745 _Log('Serving from %s' % options.static_dir)
[email protected]21a5ca32009-11-04 18:23:231746
joychen121fc9b2013-08-02 21:30:301747 _xbuddy = xbuddy.XBuddy(options.xbuddy_manage_builds,
1748 options.board,
joychen121fc9b2013-08-02 21:30:301749 static_dir=options.static_dir)
Chris Sosa75490802013-10-01 00:21:451750 if options.clear_cache and options.xbuddy_manage_builds:
1751 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 21:30:301752
Chris Sosa6a3697f2013-01-30 00:44:431753 # We allow global use here to share with cherrypy classes.
1754 # pylint: disable=W0603
Chris Sosacde6bf42012-06-01 01:36:391755 global updater
Andrew de los Reyes52620802010-04-12 20:40:071756 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 21:30:301757 _xbuddy,
joychened64b222013-06-21 23:39:341758 static_dir=options.static_dir,
Andrew de los Reyes52620802010-04-12 20:40:071759 urlbase=options.urlbase,
Chris Sosa5d342a22010-09-28 23:54:411760 forced_image=options.image,
Gilad Arnold0c9c8602012-10-03 06:58:581761 payload_path=options.payload,
Don Garrett0ad09372010-12-07 00:20:301762 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-29 06:42:371763 src_image=options.src_image,
Chris Sosa08d55a22011-01-20 00:08:021764 board=options.board,
Chris Sosa0f1ec842011-02-15 00:33:221765 copy_to_static_root=not options.exit,
1766 private_key=options.private_key,
Gabe Black3b567202015-09-23 21:07:591767 private_key_for_metadata_hash_signature=(
1768 options.private_key_for_metadata_hash_signature),
David Zeuthen52ccd012013-10-31 19:58:261769 public_key=options.public_key,
Satoru Takabayashid733cbe2011-11-15 17:36:321770 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-03 06:58:581771 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 17:32:441772 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 19:52:231773 host_log=options.host_log,
Chris Sosa0f1ec842011-02-15 00:33:221774 )
Chris Sosa7c931362010-10-12 02:49:011775
Chris Sosa6a3697f2013-01-30 00:44:431776 if options.pregenerate_update:
1777 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 22:46:221778
J. Richard Barnette3d977b82013-04-23 18:05:191779 if options.exit:
1780 return
Chris Sosa2f1c41e2012-07-10 21:32:331781
joychen3cb228e2013-06-12 19:13:131782 dev_server = DevServerRoot(_xbuddy)
1783
Gilad Arnold11fbef42014-02-10 19:04:131784 # Patch CherryPy to support binding to any available port (--port=0).
1785 cherrypy_ext.ZeroPortPatcher.DoPatch(cherrypy)
1786
Chris Sosa855b8932013-08-21 20:24:551787 if options.pidfile:
1788 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1789
Gilad Arnold11fbef42014-02-10 19:04:131790 if options.portfile:
1791 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1792
Dan Shiafd5c6c2016-01-07 18:27:031793 if (options.android_build_credential and
1794 os.path.exists(options.android_build_credential)):
1795 try:
1796 with open(options.android_build_credential) as f:
1797 android_build.BuildAccessor.credential_info = json.load(f)
1798 except ValueError as e:
1799 _Log('Failed to load the android build credential: %s. Error: %s.' %
1800 (options.android_build_credential, e))
joychen3cb228e2013-06-12 19:13:131801 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-06-01 01:36:391802
1803
1804if __name__ == '__main__':
1805 main()