blob: d9f95f8892562111af26d88c0e6aa9372f1638a6 [file] [log] [blame]
Gabe Black3b567202015-09-23 14:07:59 -07001#!/usr/bin/python2
Chris Sosa7c931362010-10-11 19:49:01 -07002
Chris Sosa781ba6d2012-04-11 12:44:43 -07003# Copyright (c) 2009-2012 The Chromium OS Authors. All rights reserved.
rtc@google.comded22402009-10-26 22:36:21 +00004# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
Chris Sosa3ae4dc12013-03-29 11:47:00 -07007"""Chromium OS development server that can be used for all forms of update.
8
9This devserver can be used to perform system-wide autoupdate and update
10of specific portage packages on devices running Chromium OS derived operating
11systems. It mainly operates in two modes:
12
131) archive mode: In this mode, the devserver is configured to stage and
14serve artifacts from Google Storage using the credentials provided to it before
15it is run. The easiest way to understand this is that the devserver is
16functioning as a local cache for artifacts produced and uploaded by build
17servers. Users of this form of devserver can either download the artifacts
18from the devservers static directory OR use the update RPC to perform a
19system-wide autoupdate. Archive mode is always active.
20
212) artifact-generation mode: in this mode, the devserver will attempt to
22generate update payloads and build artifacts when requested. This mode only
23works in the Chromium OS chroot as it uses build tools only present in the
24chroot (emerge, cros_generate_update_payload, etc.). By default, when a device
25requests an update from this form of devserver, the devserver will attempt to
26discover if a more recent build of the board has been built by the developer
27and generate a payload that the requested system can autoupdate to. In addition,
28it accepts gmerge requests from devices that will stage the newest version of
joychen84d13772013-08-06 09:17:23 -070029a particular package from a developer's chroot onto a requesting device.
Chris Sosa3ae4dc12013-03-29 11:47:00 -070030
31For example:
32gmerge gmerge -d <devserver_url>
33
34devserver will see if a newer package of gmerge is available. If gmerge is
35cros_work'd on, it will re-build gmerge. After this, gmerge will install that
36version of gmerge that the devserver just created/found.
37
38For autoupdates, there are many more advanced options that can help specify
39how to update and which payload to give to a requester.
40"""
41
Gabe Black3b567202015-09-23 14:07:59 -070042from __future__ import print_function
Chris Sosa7c931362010-10-11 19:49:01 -070043
Gilad Arnold55a2a372012-10-02 09:46:32 -070044import json
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070045import optparse
rtc@google.comded22402009-10-26 22:36:21 +000046import os
Scott Zawalski4647ce62012-01-03 17:17:28 -050047import re
Simran Basi4baad082013-02-14 13:39:18 -080048import shutil
xixuan52c2fba2016-05-20 17:02:48 -070049import signal
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -080050import socket
Chris Masone816e38c2012-05-02 12:22:36 -070051import subprocess
J. Richard Barnette3d977b82013-04-23 11:05:19 -070052import sys
Chris Masone816e38c2012-05-02 12:22:36 -070053import tempfile
Dan Shi59ae7092013-06-04 14:37:27 -070054import threading
Dan Shiafd0e492015-05-27 14:23:51 -070055import time
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -070056import types
J. Richard Barnette3d977b82013-04-23 11:05:19 -070057from logging import handlers
58
59import cherrypy
Chris Sosa855b8932013-08-21 13:24:55 -070060from cherrypy import _cplogging as cplogging
61from cherrypy.process import plugins
rtc@google.comded22402009-10-26 22:36:21 +000062
Chris Sosa0356d3b2010-09-16 15:46:22 -070063import autoupdate
Dan Shi2f136862016-02-11 15:38:38 -080064import artifact_info
Chris Sosa75490802013-09-30 17:21:45 -070065import build_artifact
Gilad Arnold11fbef42014-02-10 11:04:13 -080066import cherrypy_ext
xixuan52c2fba2016-05-20 17:02:48 -070067import cros_update
68import cros_update_progress
Gilad Arnoldc65330c2012-09-20 15:17:48 -070069import common_util
Simran Basief83d6a2014-08-28 14:32:01 -070070import devserver_constants
Chris Sosa47a7d4e2012-03-28 11:26:55 -070071import downloader
Chris Sosa7cd23202013-10-15 17:22:57 -070072import gsutil_util
Gilad Arnoldc65330c2012-09-20 15:17:48 -070073import log_util
joychen3cb228e2013-06-12 12:13:13 -070074import xbuddy
Gilad Arnoldc65330c2012-09-20 15:17:48 -070075
Gilad Arnoldc65330c2012-09-20 15:17:48 -070076# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080077def _Log(message, *args):
78 return log_util.LogWithTag('DEVSERVER', message, *args)
Chris Sosa0356d3b2010-09-16 15:46:22 -070079
Dan Shiafd0e492015-05-27 14:23:51 -070080try:
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-08 20:51:13 -070090except 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 12:10:33 -070098try:
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.
Dan Shi72b16132015-10-08 12:10:33 -0700105 android_build = None
Frank Farzan40160872011-12-12 18:39:18 -0800106
Chris Sosa417e55d2011-01-25 16:40:48 -0800107CACHED_ENTRIES = 12
Don Garrettf90edf02010-11-16 17:36:14 -0800108
Simran Basi4baad082013-02-14 13:39:18 -0800109TELEMETRY_FOLDER = 'telemetry_src'
110TELEMETRY_DEPS = ['dep-telemetry_dep.tar.bz2',
111 'dep-page_cycler_dep.tar.bz2',
Simran Basi0d078682013-03-22 16:40:04 -0700112 'dep-chrome_test.tar.bz2',
113 'dep-perf_data_dep.tar.bz2']
Simran Basi4baad082013-02-14 13:39:18 -0800114
Chris Sosa0356d3b2010-09-16 15:46:22 -0700115# Sets up global to share between classes.
rtc@google.com21a5ca32009-11-04 18:23:23 +0000116updater = None
rtc@google.comded22402009-10-26 22:36:21 +0000117
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700118# Log rotation parameters. These settings correspond to once a week
J. Richard Barnette6dfa5342013-06-04 11:48:56 -0700119# at midnight between Friday and Saturday, with about three months
120# of old logs kept for backup.
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700121#
122# For more, see the documentation for
123# logging.handlers.TimedRotatingFileHandler
J. Richard Barnette6dfa5342013-06-04 11:48:56 -0700124_LOG_ROTATION_TIME = 'W4'
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700125_LOG_ROTATION_BACKUP = 13
126
Dan Shiafd0e492015-05-27 14:23:51 -0700127# Number of seconds between the collection of disk and network IO counters.
128STATS_INTERVAL = 10.0
Frank Farzan40160872011-12-12 18:39:18 -0800129
xixuan52c2fba2016-05-20 17:02:48 -0700130# Auto-update parameters
131
132# Error msg for missing key in CrOS auto-update.
133KEY_ERROR_MSG = 'Key Error in cmd %s: %s= is required'
134
135# Command of running auto-update.
136AUTO_UPDATE_CMD = '/usr/bin/python -u %s -d %s -b %s --static_dir %s'
137
138
Chris Sosa9164ca32012-03-28 11:04:50 -0700139class DevServerError(Exception):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700140 """Exception class used by this module."""
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700141
142
Dan Shiafd0e492015-05-27 14:23:51 -0700143def require_psutil():
Gabe Black3b567202015-09-23 14:07:59 -0700144 """Decorator for functions require psutil to run."""
Dan Shiafd0e492015-05-27 14:23:51 -0700145 def deco_require_psutil(func):
146 """Wrapper of the decorator function.
147
Gabe Black3b567202015-09-23 14:07:59 -0700148 Args:
149 func: function to be called.
Dan Shiafd0e492015-05-27 14:23:51 -0700150 """
151 def func_require_psutil(*args, **kwargs):
152 """Decorator for functions require psutil to run.
153
154 If psutil is not installed, skip calling the function.
155
Gabe Black3b567202015-09-23 14:07:59 -0700156 Args:
157 *args: arguments for function to be called.
158 **kwargs: keyword arguments for function to be called.
Dan Shiafd0e492015-05-27 14:23:51 -0700159 """
160 if psutil:
161 return func(*args, **kwargs)
162 else:
163 _Log('Python module psutil is not installed. Function call %s is '
164 'skipped.' % func)
165 return func_require_psutil
166 return deco_require_psutil
167
168
Gabe Black3b567202015-09-23 14:07:59 -0700169def _canonicalize_archive_url(archive_url):
170 """Canonicalizes archive_url strings.
171
172 Raises:
173 DevserverError: if archive_url is not set.
174 """
175 if archive_url:
176 if not archive_url.startswith('gs://'):
177 raise DevServerError("Archive URL isn't from Google Storage (%s) ." %
178 archive_url)
179
180 return archive_url.rstrip('/')
181 else:
182 raise DevServerError("Must specify an archive_url in the request")
183
184
185def _canonicalize_local_path(local_path):
186 """Canonicalizes |local_path| strings.
187
188 Raises:
189 DevserverError: if |local_path| is not set.
190 """
191 # Restrict staging of local content to only files within the static
192 # directory.
193 local_path = os.path.abspath(local_path)
194 if not local_path.startswith(updater.static_dir):
195 raise DevServerError('Local path %s must be a subdirectory of the static'
196 ' directory: %s' % (local_path, updater.static_dir))
197
198 return local_path.rstrip('/')
199
200
201def _get_artifacts(kwargs):
202 """Returns a tuple of named and file artifacts given the stage rpc kwargs.
203
204 Raises:
205 DevserverError if no artifacts would be returned.
206 """
207 artifacts = kwargs.get('artifacts')
208 files = kwargs.get('files')
209 if not artifacts and not files:
210 raise DevServerError('No artifacts specified.')
211
212 # Note we NEED to coerce files to a string as we get raw unicode from
213 # cherrypy and we treat files as strings elsewhere in the code.
214 return (str(artifacts).split(',') if artifacts else [],
215 str(files).split(',') if files else [])
216
217
Dan Shi61305df2015-10-26 16:52:35 -0700218def _is_android_build_request(kwargs):
219 """Check if a devserver call is for Android build, based on the arguments.
220
221 This method exams the request's arguments (os_type) to determine if the
222 request is for Android build. If os_type is set to `android`, returns True.
223 If os_type is not set or has other values, returns False.
224
225 Args:
226 kwargs: Keyword arguments for the request.
227
228 Returns:
229 True if the request is for Android build. False otherwise.
230 """
231 os_type = kwargs.get('os_type', None)
232 return os_type == 'android'
233
234
Gabe Black3b567202015-09-23 14:07:59 -0700235def _get_downloader(kwargs):
236 """Returns the downloader based on passed in arguments.
237
238 Args:
239 kwargs: Keyword arguments for the request.
240 """
241 local_path = kwargs.get('local_path')
242 if local_path:
243 local_path = _canonicalize_local_path(local_path)
244
245 dl = None
246 if local_path:
247 dl = downloader.LocalDownloader(updater.static_dir, local_path)
248
Dan Shi61305df2015-10-26 16:52:35 -0700249 if not _is_android_build_request(kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700250 archive_url = kwargs.get('archive_url')
251 if not archive_url and not local_path:
252 raise DevServerError('Requires archive_url or local_path to be '
253 'specified.')
254 if archive_url and local_path:
255 raise DevServerError('archive_url and local_path can not both be '
256 'specified.')
257 if not dl:
258 archive_url = _canonicalize_archive_url(archive_url)
259 dl = downloader.GoogleStorageDownloader(updater.static_dir, archive_url)
260 elif not dl:
261 target = kwargs.get('target', None)
Dan Shi72b16132015-10-08 12:10:33 -0700262 branch = kwargs.get('branch', None)
Dan Shi61305df2015-10-26 16:52:35 -0700263 build_id = kwargs.get('build_id', None)
264 if not target or not branch or not build_id:
Dan Shi72b16132015-10-08 12:10:33 -0700265 raise DevServerError(
Dan Shi61305df2015-10-26 16:52:35 -0700266 'target, branch, build ID must all be specified for downloading '
267 'Android build.')
Dan Shi72b16132015-10-08 12:10:33 -0700268 dl = downloader.AndroidBuildDownloader(updater.static_dir, branch, build_id,
269 target)
Gabe Black3b567202015-09-23 14:07:59 -0700270
271 return dl
272
273
274def _get_downloader_and_factory(kwargs):
275 """Returns the downloader and artifact factory based on passed in arguments.
276
277 Args:
278 kwargs: Keyword arguments for the request.
279 """
280 artifacts, files = _get_artifacts(kwargs)
281 dl = _get_downloader(kwargs)
282
283 if (isinstance(dl, downloader.GoogleStorageDownloader) or
284 isinstance(dl, downloader.LocalDownloader)):
285 factory_class = build_artifact.ChromeOSArtifactFactory
Dan Shi72b16132015-10-08 12:10:33 -0700286 elif isinstance(dl, downloader.AndroidBuildDownloader):
Gabe Black3b567202015-09-23 14:07:59 -0700287 factory_class = build_artifact.AndroidArtifactFactory
288 else:
289 raise DevServerError('Unrecognized value for downloader type: %s' %
290 type(dl))
291
292 factory = factory_class(dl.GetBuildDir(), artifacts, files, dl.GetBuild())
293
294 return dl, factory
295
296
Scott Zawalski4647ce62012-01-03 17:17:28 -0500297def _LeadingWhiteSpaceCount(string):
298 """Count the amount of leading whitespace in a string.
299
300 Args:
301 string: The string to count leading whitespace in.
Don Garrettf84631a2014-01-07 18:21:26 -0800302
Scott Zawalski4647ce62012-01-03 17:17:28 -0500303 Returns:
304 number of white space chars before characters start.
305 """
Gabe Black3b567202015-09-23 14:07:59 -0700306 matched = re.match(r'^\s+', string)
Scott Zawalski4647ce62012-01-03 17:17:28 -0500307 if matched:
308 return len(matched.group())
309
310 return 0
311
312
313def _PrintDocStringAsHTML(func):
314 """Make a functions docstring somewhat HTML style.
315
316 Args:
317 func: The function to return the docstring from.
Don Garrettf84631a2014-01-07 18:21:26 -0800318
Scott Zawalski4647ce62012-01-03 17:17:28 -0500319 Returns:
320 A string that is somewhat formated for a web browser.
321 """
322 # TODO(scottz): Make this parse Args/Returns in a prettier way.
323 # Arguments could be bolded and indented etc.
324 html_doc = []
325 for line in func.__doc__.splitlines():
326 leading_space = _LeadingWhiteSpaceCount(line)
327 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700328 line = '&nbsp;' * leading_space + line
Scott Zawalski4647ce62012-01-03 17:17:28 -0500329
330 html_doc.append('<BR>%s' % line)
331
332 return '\n'.join(html_doc)
333
334
Simran Basief83d6a2014-08-28 14:32:01 -0700335def _GetUpdateTimestampHandler(static_dir):
336 """Returns a handler to update directory staged.timestamp.
337
338 This handler resets the stage.timestamp whenever static content is accessed.
339
340 Args:
341 static_dir: Directory from which static content is being staged.
342
343 Returns:
344 A cherrypy handler to update the timestamp of accessed content.
345 """
346 def UpdateTimestampHandler():
347 if not '404' in cherrypy.response.status:
348 build_match = re.match(devserver_constants.STAGED_BUILD_REGEX,
349 cherrypy.request.path_info)
350 if build_match:
351 build_dir = os.path.join(static_dir, build_match.group('build'))
352 downloader.Downloader.TouchTimestampForStaged(build_dir)
353 return UpdateTimestampHandler
354
355
Chris Sosa7c931362010-10-11 19:49:01 -0700356def _GetConfig(options):
357 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800358
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800359 socket_host = '::'
Yu-Ju Hongc8d4af32013-11-12 15:14:26 -0800360 # Fall back to IPv4 when python is not configured with IPv6.
361 if not socket.has_ipv6:
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800362 socket_host = '0.0.0.0'
363
Simran Basief83d6a2014-08-28 14:32:01 -0700364 # Adds the UpdateTimestampHandler to cherrypy's tools. This tools executes
365 # on the on_end_resource hook. This hook is called once processing is
366 # complete and the response is ready to be returned.
367 cherrypy.tools.update_timestamp = cherrypy.Tool(
368 'on_end_resource', _GetUpdateTimestampHandler(options.static_dir))
369
Gabe Black3b567202015-09-23 14:07:59 -0700370 base_config = {'global':
371 {'server.log_request_headers': True,
372 'server.protocol_version': 'HTTP/1.1',
373 'server.socket_host': socket_host,
374 'server.socket_port': int(options.port),
375 'response.timeout': 6000,
376 'request.show_tracebacks': True,
377 'server.socket_timeout': 60,
378 'server.thread_pool': 2,
379 'engine.autoreload.on': False,
380 },
381 '/api':
382 {
383 # Gets rid of cherrypy parsing post file for args.
384 'request.process_request_body': False,
385 },
386 '/build':
387 {'response.timeout': 100000,
388 },
389 '/update':
390 {
391 # Gets rid of cherrypy parsing post file for args.
392 'request.process_request_body': False,
393 'response.timeout': 10000,
394 },
395 # Sets up the static dir for file hosting.
396 '/static':
397 {'tools.staticdir.dir': options.static_dir,
398 'tools.staticdir.on': True,
399 'response.timeout': 10000,
400 'tools.update_timestamp.on': True,
401 },
402 }
Chris Sosa5f118ef2012-07-12 11:37:50 -0700403 if options.production:
Alex Miller93beca52013-07-30 19:25:09 -0700404 base_config['global'].update({'server.thread_pool': 150})
Chris Sosa7cd23202013-10-15 17:22:57 -0700405 # TODO(sosa): Do this more cleanly.
406 gsutil_util.GSUTIL_ATTEMPTS = 5
Scott Zawalski1c5e7cd2012-02-27 13:12:52 -0500407
Chris Sosa7c931362010-10-11 19:49:01 -0700408 return base_config
rtc@google.com64244662009-11-12 00:52:08 +0000409
Darin Petkove17164a2010-08-11 13:24:41 -0700410
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700411def _GetRecursiveMemberObject(root, member_list):
412 """Returns an object corresponding to a nested member list.
413
414 Args:
415 root: the root object to search
416 member_list: list of nested members to search
Don Garrettf84631a2014-01-07 18:21:26 -0800417
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700418 Returns:
419 An object corresponding to the member name list; None otherwise.
420 """
421 for member in member_list:
422 next_root = root.__class__.__dict__.get(member)
423 if not next_root:
424 return None
425 root = next_root
426 return root
427
428
429def _IsExposed(name):
430 """Returns True iff |name| has an `exposed' attribute and it is set."""
431 return hasattr(name, 'exposed') and name.exposed
432
433
Gilad Arnold748c8322012-10-12 09:51:35 -0700434def _GetExposedMethod(root, nested_member, ignored=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700435 """Returns a CherryPy-exposed method, if such exists.
436
437 Args:
438 root: the root object for searching
439 nested_member: a slash-joined path to the nested member
440 ignored: method paths to be ignored
Don Garrettf84631a2014-01-07 18:21:26 -0800441
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700442 Returns:
443 A function object corresponding to the path defined by |member_list| from
444 the |root| object, if the function is exposed and not ignored; None
445 otherwise.
446 """
Gilad Arnold748c8322012-10-12 09:51:35 -0700447 method = (not (ignored and nested_member in ignored) and
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700448 _GetRecursiveMemberObject(root, nested_member.split('/')))
Gabe Black3b567202015-09-23 14:07:59 -0700449 if method and type(method) == types.FunctionType and _IsExposed(method):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700450 return method
451
452
Gilad Arnold748c8322012-10-12 09:51:35 -0700453def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700454 """Finds exposed CherryPy methods.
455
456 Args:
457 root: the root object for searching
458 prefix: slash-joined chain of members leading to current object
459 unlisted: URLs to be excluded regardless of their exposed status
Don Garrettf84631a2014-01-07 18:21:26 -0800460
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700461 Returns:
462 List of exposed URLs that are not unlisted.
463 """
464 method_list = []
465 for member in sorted(root.__class__.__dict__.keys()):
466 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 09:51:35 -0700467 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700468 continue
469 member_obj = root.__class__.__dict__[member]
470 if _IsExposed(member_obj):
471 if type(member_obj) == types.FunctionType:
472 method_list.append(prefixed_member)
473 else:
474 method_list += _FindExposedMethods(
475 member_obj, prefixed_member, unlisted)
476 return method_list
477
478
xixuan52c2fba2016-05-20 17:02:48 -0700479def _check_base_args_for_auto_update(kwargs):
480 if 'host_name' not in kwargs:
481 raise common_util.DevServerHTTPError(KEY_ERROR_MSG % 'host_name')
482
483 if 'build_name' not in kwargs:
484 raise common_util.DevServerHTTPError(KEY_ERROR_MSG % 'build_name')
485
486
487def _parse_boolean_arg(kwargs, key):
488 if key in kwargs:
489 if kwargs[key] == 'True':
490 return True
491 elif kwargs[key] == 'False':
492 return False
493 else:
494 raise common_util.DevServerHTTPError(
495 'The value for key %s is not boolean.' % key)
496 else:
497 return False
498
499
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700500class ApiRoot(object):
501 """RESTful API for Dev Server information."""
502 exposed = True
503
504 @cherrypy.expose
505 def hostinfo(self, ip):
506 """Returns a JSON dictionary containing information about the given ip.
507
Gilad Arnold1b908392012-10-05 11:36:27 -0700508 Args:
509 ip: address of host whose info is requested
Don Garrettf84631a2014-01-07 18:21:26 -0800510
Gilad Arnold1b908392012-10-05 11:36:27 -0700511 Returns:
512 A JSON dictionary containing all or some of the following fields:
513 last_event_type (int): last update event type received
514 last_event_status (int): last update event status received
515 last_known_version (string): last known version reported in update ping
516 forced_update_label (string): update label to force next update ping to
517 use, set by setnextupdate
518 See the OmahaEvent class in update_engine/omaha_request_action.h for
519 event type and status code definitions. If the ip does not exist an empty
520 string is returned.
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700521
Gilad Arnold1b908392012-10-05 11:36:27 -0700522 Example URL:
523 http://myhost/api/hostinfo?ip=192.168.1.5
524 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700525 return updater.HandleHostInfoPing(ip)
526
527 @cherrypy.expose
Gilad Arnold286a0062012-01-12 13:47:02 -0800528 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 11:36:27 -0700529 """Returns a JSON object containing a log of host event.
530
531 Args:
532 ip: address of host whose event log is requested, or `all'
Don Garrettf84631a2014-01-07 18:21:26 -0800533
Gilad Arnold1b908392012-10-05 11:36:27 -0700534 Returns:
535 A JSON encoded list (log) of dictionaries (events), each of which
536 containing a `timestamp' and other event fields, as described under
537 /api/hostinfo.
538
539 Example URL:
540 http://myhost/api/hostlog?ip=192.168.1.5
541 """
Gilad Arnold286a0062012-01-12 13:47:02 -0800542 return updater.HandleHostLogPing(ip)
543
544 @cherrypy.expose
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700545 def setnextupdate(self, ip):
546 """Allows the response to the next update ping from a host to be set.
547
548 Takes the IP of the host and an update label as normally provided to the
Gilad Arnold1b908392012-10-05 11:36:27 -0700549 /update command.
550 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700551 body_length = int(cherrypy.request.headers['Content-Length'])
552 label = cherrypy.request.rfile.read(body_length)
553
554 if label:
555 label = label.strip()
556 if label:
557 return updater.HandleSetUpdatePing(ip, label)
Chris Sosa4b951602014-04-09 20:26:07 -0700558 raise common_util.DevServerHTTPError(400, 'No label provided.')
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700559
560
Gilad Arnold55a2a372012-10-02 09:46:32 -0700561 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800562 def fileinfo(self, *args):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700563 """Returns information about a given staged file.
564
565 Args:
Don Garrettf84631a2014-01-07 18:21:26 -0800566 args: path to the file inside the server's static staging directory
567
Gilad Arnold55a2a372012-10-02 09:46:32 -0700568 Returns:
569 A JSON encoded dictionary with information about the said file, which may
570 contain the following keys/values:
Gilad Arnold1b908392012-10-05 11:36:27 -0700571 size (int): the file size in bytes
572 sha1 (string): a base64 encoded SHA1 hash
573 sha256 (string): a base64 encoded SHA256 hash
574
575 Example URL:
576 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 09:46:32 -0700577 """
Don Garrettf84631a2014-01-07 18:21:26 -0800578 file_path = os.path.join(updater.static_dir, *args)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700579 if not os.path.exists(file_path):
580 raise DevServerError('file not found: %s' % file_path)
581 try:
582 file_size = os.path.getsize(file_path)
583 file_sha1 = common_util.GetFileSha1(file_path)
584 file_sha256 = common_util.GetFileSha256(file_path)
585 except os.error, e:
586 raise DevServerError('failed to get info for file %s: %s' %
Gilad Arnolde74b3812013-04-22 11:27:38 -0700587 (file_path, e))
588
589 is_delta = autoupdate.Autoupdate.IsDeltaFormatFile(file_path)
590
591 return json.dumps({
592 autoupdate.Autoupdate.SIZE_ATTR: file_size,
593 autoupdate.Autoupdate.SHA1_ATTR: file_sha1,
594 autoupdate.Autoupdate.SHA256_ATTR: file_sha256,
595 autoupdate.Autoupdate.ISDELTA_ATTR: is_delta
596 })
Gilad Arnold55a2a372012-10-02 09:46:32 -0700597
Chris Sosa76e44b92013-01-31 12:11:38 -0800598
David Rochberg7c79a812011-01-19 14:24:45 -0500599class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700600 """The Root Class for the Dev Server.
601
602 CherryPy works as follows:
603 For each method in this class, cherrpy interprets root/path
604 as a call to an instance of DevServerRoot->method_name. For example,
605 a call to http://myhost/build will call build. CherryPy automatically
606 parses http args and places them as keyword arguments in each method.
607 For paths http://myhost/update/dir1/dir2, you can use *args so that
608 cherrypy uses the update method and puts the extra paths in args.
609 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700610 # Method names that should not be listed on the index page.
611 _UNLISTED_METHODS = ['index', 'doc']
612
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700613 api = ApiRoot()
Chris Sosa7c931362010-10-11 19:49:01 -0700614
Dan Shi59ae7092013-06-04 14:37:27 -0700615 # Number of threads that devserver is staging images.
616 _staging_thread_count = 0
617 # Lock used to lock increasing/decreasing count.
618 _staging_thread_count_lock = threading.Lock()
619
Dan Shiafd0e492015-05-27 14:23:51 -0700620 @require_psutil()
621 def _refresh_io_stats(self):
622 """A call running in a thread to update IO stats periodically."""
623 prev_disk_io_counters = psutil.disk_io_counters()
624 prev_network_io_counters = psutil.net_io_counters()
625 prev_read_time = time.time()
626 while True:
627 time.sleep(STATS_INTERVAL)
628 now = time.time()
629 interval = now - prev_read_time
630 prev_read_time = now
631 # Disk IO is for all disks.
632 disk_io_counters = psutil.disk_io_counters()
633 network_io_counters = psutil.net_io_counters()
634
635 self.disk_read_bytes_per_sec = (
636 disk_io_counters.read_bytes -
637 prev_disk_io_counters.read_bytes)/interval
638 self.disk_write_bytes_per_sec = (
639 disk_io_counters.write_bytes -
640 prev_disk_io_counters.write_bytes)/interval
641 prev_disk_io_counters = disk_io_counters
642
643 self.network_sent_bytes_per_sec = (
644 network_io_counters.bytes_sent -
645 prev_network_io_counters.bytes_sent)/interval
646 self.network_recv_bytes_per_sec = (
647 network_io_counters.bytes_recv -
648 prev_network_io_counters.bytes_recv)/interval
649 prev_network_io_counters = network_io_counters
650
651 @require_psutil()
652 def _start_io_stat_thread(self):
Gabe Black3b567202015-09-23 14:07:59 -0700653 """Start the thread to collect IO stats."""
Dan Shiafd0e492015-05-27 14:23:51 -0700654 thread = threading.Thread(target=self._refresh_io_stats)
655 thread.daemon = True
656 thread.start()
657
joychen3cb228e2013-06-12 12:13:13 -0700658 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700659 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800660 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 12:13:13 -0700661 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 14:24:45 -0500662
Dan Shiafd0e492015-05-27 14:23:51 -0700663 # Cache of disk IO stats, a thread refresh the stats every 10 seconds.
664 # lock is not used for these variables as the only thread writes to these
665 # variables is _refresh_io_stats.
666 self.disk_read_bytes_per_sec = 0
667 self.disk_write_bytes_per_sec = 0
668 # Cache of network IO stats.
669 self.network_sent_bytes_per_sec = 0
670 self.network_recv_bytes_per_sec = 0
671 self._start_io_stat_thread()
672
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700673 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500674 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700675 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700676 import builder
677 if self._builder is None:
678 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500679 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700680
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700681 @cherrypy.expose
Dan Shif8eb0d12013-08-01 17:52:06 -0700682 def is_staged(self, **kwargs):
683 """Check if artifacts have been downloaded.
684
Chris Sosa6b0c6172013-08-05 17:01:33 -0700685 async: True to return without waiting for download to complete.
686 artifacts: Comma separated list of named artifacts to download.
687 These are defined in artifact_info and have their implementation
688 in build_artifact.py.
689 files: Comma separated list of file artifacts to stage. These
690 will be available as is in the corresponding static directory with no
691 custom post-processing.
692
693 returns: True of all artifacts are staged.
Dan Shif8eb0d12013-08-01 17:52:06 -0700694
695 Example:
696 To check if autotest and test_suites are staged:
697 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
698 artifacts=autotest,test_suites
699 """
Gabe Black3b567202015-09-23 14:07:59 -0700700 dl, factory = _get_downloader_and_factory(kwargs)
Aviv Keshet57d18172016-06-18 20:39:09 -0700701 response = str(dl.IsStaged(factory))
702 _Log('Responding to is_staged %s request with %r', kwargs, response)
703 return response
Dan Shi59ae7092013-06-04 14:37:27 -0700704
Chris Sosa76e44b92013-01-31 12:11:38 -0800705 @cherrypy.expose
Prashanth Ba06d2d22014-03-07 15:35:19 -0800706 def list_image_dir(self, **kwargs):
707 """Take an archive url and list the contents in its staged directory.
708
709 Args:
710 kwargs:
711 archive_url: Google Storage URL for the build.
712
713 Example:
714 To list the contents of where this devserver should have staged
715 gs://image-archive/<board>-release/<build> call:
716 http://devserver_url:<port>/list_image_dir?archive_url=<gs://..>
717
718 Returns:
719 A string with information about the contents of the image directory.
720 """
Gabe Black3b567202015-09-23 14:07:59 -0700721 dl = _get_downloader(kwargs)
Prashanth Ba06d2d22014-03-07 15:35:19 -0800722 try:
Gabe Black3b567202015-09-23 14:07:59 -0700723 image_dir_contents = dl.ListBuildDir()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800724 except build_artifact.ArtifactDownloadError as e:
725 return 'Cannot list the contents of staged artifacts. %s' % e
726 if not image_dir_contents:
Gabe Black3b567202015-09-23 14:07:59 -0700727 return '%s has not been staged on this devserver.' % dl.DescribeSource()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800728 return image_dir_contents
729
730 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800731 def stage(self, **kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700732 """Downloads and caches build artifacts.
Chris Sosa76e44b92013-01-31 12:11:38 -0800733
Gabe Black3b567202015-09-23 14:07:59 -0700734 Downloads and caches build artifacts, possibly from a Google Storage URL,
Dan Shi72b16132015-10-08 12:10:33 -0700735 or from Android's build server. Returns once these have been downloaded
Gabe Black3b567202015-09-23 14:07:59 -0700736 on the devserver. A call to this will attempt to cache non-specified
737 artifacts in the background for the given from the given URL following
738 the principle of spatial locality. Spatial locality of different
Chris Sosa76e44b92013-01-31 12:11:38 -0800739 artifacts is explicitly defined in the build_artifact module.
740
741 These artifacts will then be available from the static/ sub-directory of
742 the devserver.
743
744 Args:
745 archive_url: Google Storage URL for the build.
Simran Basi4243a862014-12-12 12:48:33 -0800746 local_path: Local path for the build.
Dan Shif8eb0d12013-08-01 17:52:06 -0700747 async: True to return without waiting for download to complete.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700748 artifacts: Comma separated list of named artifacts to download.
749 These are defined in artifact_info and have their implementation
750 in build_artifact.py.
751 files: Comma separated list of files to stage. These
752 will be available as is in the corresponding static directory with no
753 custom post-processing.
Laurence Goodbyf5c958d2016-01-14 18:23:56 -0800754 clean: True to remove any previously staged artifacts first.
Chris Sosa76e44b92013-01-31 12:11:38 -0800755
756 Example:
757 To download the autotest and test suites tarballs:
758 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
759 artifacts=autotest,test_suites
760 To download the full update payload:
761 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
762 artifacts=full_payload
Chris Sosa6b0c6172013-08-05 17:01:33 -0700763 To download just a file called blah.bin:
764 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
765 files=blah.bin
Chris Sosa76e44b92013-01-31 12:11:38 -0800766
767 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 16:39:34 -0700768 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 12:11:38 -0800769
770 Note for this example, relative path is the archive_url stripped of its
771 basename i.e. path/ in the examples above. Specific example:
772
773 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
774
775 Will get staged to:
776
joychened64b222013-06-21 16:39:34 -0700777 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Chris Sosa76e44b92013-01-31 12:11:38 -0800778 """
Gabe Black3b567202015-09-23 14:07:59 -0700779 dl, factory = _get_downloader_and_factory(kwargs)
780
Dan Shi59ae7092013-06-04 14:37:27 -0700781 with DevServerRoot._staging_thread_count_lock:
782 DevServerRoot._staging_thread_count += 1
783 try:
Laurence Goodbyf5c958d2016-01-14 18:23:56 -0800784 boolean_string = kwargs.get('clean')
785 clean = xbuddy.XBuddy.ParseBoolean(boolean_string)
786 if clean and os.path.exists(dl.GetBuildDir()):
787 _Log('Removing %s' % dl.GetBuildDir())
788 shutil.rmtree(dl.GetBuildDir())
Gabe Black3b567202015-09-23 14:07:59 -0700789 async = kwargs.get('async', False)
790 dl.Download(factory, async=async)
Dan Shi59ae7092013-06-04 14:37:27 -0700791 finally:
792 with DevServerRoot._staging_thread_count_lock:
793 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800794 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700795
796 @cherrypy.expose
xixuan52c2fba2016-05-20 17:02:48 -0700797 def cros_au(self, **kwargs):
798 """Auto-update a CrOS DUT.
799
800 Args:
801 kwargs:
802 host_name: the hostname of the DUT to auto-update.
803 build_name: the build name for update the DUT.
804 force_update: Force an update even if the version installed is the
805 same. Default: False.
806 full_update: If True, do not run stateful update, directly force a full
807 reimage. If False, try stateful update first if the dut is already
808 installed with the same version.
809 async: Whether the auto_update function is ran in the background.
810
811 Returns:
812 A tuple includes two elements:
813 a boolean variable represents whether the auto-update process is
814 successfully started.
815 an integer represents the background auto-update process id.
816 """
817 _check_base_args_for_auto_update(kwargs)
818
819 host_name = kwargs['host_name']
820 build_name = kwargs['build_name']
821 force_update = _parse_boolean_arg(kwargs, 'force_update')
822 full_update = _parse_boolean_arg(kwargs, 'full_update')
823 async = _parse_boolean_arg(kwargs, 'async')
824
825 if async:
826 path = os.path.dirname(os.path.abspath(__file__))
827 execute_file = os.path.join(path, 'cros_update.py')
828 args = (AUTO_UPDATE_CMD % (execute_file, host_name, build_name,
829 updater.static_dir))
830 if force_update:
831 args = ('%s --force_update' % args)
832
833 if full_update:
834 args = ('%s --full_update' % args)
835
xixuan2a0970a2016-08-10 12:12:44 -0700836 p = subprocess.Popen([args], shell=True, preexec_fn=os.setsid)
837 pid = os.getpgid(p.pid)
xixuan52c2fba2016-05-20 17:02:48 -0700838
839 # Pre-write status in the track_status_file before the first call of
840 # 'get_au_status' to make sure that the track_status_file exists.
xixuan2a0970a2016-08-10 12:12:44 -0700841 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -0700842 progress_tracker.WriteStatus('CrOS update is just started.')
843
xixuan2a0970a2016-08-10 12:12:44 -0700844 return json.dumps((True, pid))
xixuan52c2fba2016-05-20 17:02:48 -0700845 else:
846 cros_update_trigger = cros_update.CrOSUpdateTrigger(
847 host_name, build_name, updater.static_dir)
848 cros_update_trigger.TriggerAU()
849
850 @cherrypy.expose
851 def get_au_status(self, **kwargs):
852 """Check if the auto-update task is finished.
853
854 It handles 4 cases:
855 1. If an error exists in the track_status_file, delete the track file and
856 raise it.
857 2. If cros-update process is finished, delete the file and return the
858 success result.
859 3. If the process is not running, delete the track file and raise an error
860 about 'the process is terminated due to unknown reason'.
861 4. If the track_status_file does not exist, kill the process if it exists,
862 and raise the IOError.
863
864 Args:
865 kwargs:
866 host_name: the hostname of the DUT to auto-update.
867 pid: the background process id of cros-update.
868
869 Returns:
xixuan28d99072016-10-06 12:24:16 -0700870 A dict with three elements:
xixuan52c2fba2016-05-20 17:02:48 -0700871 a boolean variable represents whether the auto-update process is
872 finished.
873 a string represents the current auto-update process status.
874 For example, 'Transfer Devserver/Stateful Update Package'.
xixuan28d99072016-10-06 12:24:16 -0700875 a detailed error message paragraph if there exists an Auto-Update
876 error, in which the last line shows the main exception. Empty
877 string otherwise.
xixuan52c2fba2016-05-20 17:02:48 -0700878 """
879 if 'host_name' not in kwargs:
880 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
881
882 if 'pid' not in kwargs:
883 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
884
885 host_name = kwargs['host_name']
886 pid = kwargs['pid']
887 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
888
xixuan28d99072016-10-06 12:24:16 -0700889 result_dict = {'finished': False, 'status': '', 'detailed_error_msg': ''}
xixuan52c2fba2016-05-20 17:02:48 -0700890 try:
891 result = progress_tracker.ReadStatus()
892 if result.startswith(cros_update_progress.ERROR_TAG):
xixuan28d99072016-10-06 12:24:16 -0700893 result_dict['detailed_error_msg'] = result[len(
894 cros_update_progress.ERROR_TAG):]
xixuan28681fd2016-11-23 11:13:56 -0800895 elif result == cros_update_progress.FINISHED:
xixuan28d99072016-10-06 12:24:16 -0700896 result_dict['finished'] = True
897 result_dict['status'] = result
xixuan28681fd2016-11-23 11:13:56 -0800898 elif not cros_update_progress.IsProcessAlive(pid):
xixuan28d99072016-10-06 12:24:16 -0700899 result_dict['detailed_error_msg'] = (
900 'Cros_update process terminated midway due to unknown reason. '
901 'Last update status was %s' % result)
xixuan28681fd2016-11-23 11:13:56 -0800902 else:
903 result_dict['status'] = result
904 except IOError as e:
905 if pid and cros_update_progress.IsProcessAlive(pid):
xixuan2a0970a2016-08-10 12:12:44 -0700906 os.killpg(int(pid), signal.SIGKILL)
xixuan52c2fba2016-05-20 17:02:48 -0700907
xixuan28681fd2016-11-23 11:13:56 -0800908 result_dict['detailed_error_msg'] = str(e)
909
910 return json.dumps(result_dict)
xixuan52c2fba2016-05-20 17:02:48 -0700911
912 @cherrypy.expose
913 def handler_cleanup(self, **kwargs):
xixuan3bc974e2016-10-18 17:21:43 -0700914 """Clean track status log and temp directory for CrOS auto-update process.
xixuan52c2fba2016-05-20 17:02:48 -0700915
916 Args:
917 kwargs:
918 host_name: the hostname of the DUT to auto-update.
919 pid: the background process id of cros-update.
920 """
921 if 'host_name' not in kwargs:
922 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
923
924 if 'pid' not in kwargs:
925 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
926
927 host_name = kwargs['host_name']
928 pid = kwargs['pid']
929 cros_update_progress.DelTrackStatusFile(host_name, pid)
xixuan3bc974e2016-10-18 17:21:43 -0700930 cros_update_progress.DelAUTempDirectory(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -0700931
932 @cherrypy.expose
933 def kill_au_proc(self, **kwargs):
934 """Kill CrOS auto-update process using given process id.
935
936 Args:
937 kwargs:
938 host_name: Kill all the CrOS auto-update process of this host.
939
940 Returns:
941 True if all processes are killed properly.
942 """
943 if 'host_name' not in kwargs:
944 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
945
946 host_name = kwargs['host_name']
xixuan3bc974e2016-10-18 17:21:43 -0700947 track_log_list = cros_update_progress.GetAllTrackStatusFileByHostName(
948 host_name)
xixuan52c2fba2016-05-20 17:02:48 -0700949 for log in track_log_list:
950 # The track log's full path is: path/host_name_pid.log
951 # Use splitext to remove file extension, then parse pid from the
952 # filename.
953 pid = os.path.splitext(os.path.basename(log))[0][len(host_name)+1:]
954 if cros_update_progress.IsProcessAlive(pid):
xixuan2a0970a2016-08-10 12:12:44 -0700955 os.killpg(int(pid), signal.SIGKILL)
xixuan52c2fba2016-05-20 17:02:48 -0700956
957 cros_update_progress.DelTrackStatusFile(host_name, pid)
xixuan1bbfaba2016-10-13 17:53:22 -0700958 cros_update_progress.DelExecuteLogFile(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -0700959
960 return 'True'
961
962 @cherrypy.expose
963 def collect_cros_au_log(self, **kwargs):
964 """Collect CrOS auto-update log.
965
966 Args:
967 kwargs:
968 host_name: the hostname of the DUT to auto-update.
969 pid: the background process id of cros-update.
970
971 Returns:
972 A string contains the whole content of the execute log file.
973 """
974 if 'host_name' not in kwargs:
975 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
976
977 if 'pid' not in kwargs:
978 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
979
980 host_name = kwargs['host_name']
981 pid = kwargs['pid']
xixuan3bc974e2016-10-18 17:21:43 -0700982
983 # Fetch the execute log recorded by cros_update_progress.
xixuan1bbfaba2016-10-13 17:53:22 -0700984 au_log = cros_update_progress.ReadExecuteLogFile(host_name, pid)
985 cros_update_progress.DelExecuteLogFile(host_name, pid)
986 return au_log
987
xixuan52c2fba2016-05-20 17:02:48 -0700988 @cherrypy.expose
Dan Shi2f136862016-02-11 15:38:38 -0800989 def locate_file(self, **kwargs):
990 """Get the path to the given file name.
991
992 This method looks up the given file name inside specified build artifacts.
993 One use case is to help caller to locate an apk file inside a build
994 artifact. The location of the apk file could be different based on the
995 branch and target.
996
997 Args:
998 file_name: Name of the file to look for.
999 artifacts: A list of artifact names to search for the file.
1000
1001 Returns:
1002 Path to the file with the given name. It's relative to the folder for the
1003 build, e.g., DATA/priv-app/sl4a/sl4a.apk
Dan Shi2f136862016-02-11 15:38:38 -08001004 """
1005 dl, _ = _get_downloader_and_factory(kwargs)
1006 try:
1007 file_name = kwargs['file_name'].lower()
1008 artifacts = kwargs['artifacts']
1009 except KeyError:
1010 raise DevServerError('`file_name` and `artifacts` are required to search '
1011 'for a file in build artifacts.')
1012 build_path = dl.GetBuildDir()
1013 for artifact in artifacts:
1014 # Get the unzipped folder of the artifact. If it's not defined in
1015 # ARTIFACT_UNZIP_FOLDER_MAP, assume the files are unzipped to the build
1016 # directory directly.
1017 folder = artifact_info.ARTIFACT_UNZIP_FOLDER_MAP.get(artifact, '')
1018 artifact_path = os.path.join(build_path, folder)
1019 for root, _, filenames in os.walk(artifact_path):
1020 if file_name in set([f.lower() for f in filenames]):
1021 return os.path.relpath(os.path.join(root, file_name), build_path)
1022 raise DevServerError('File `%s` can not be found in artifacts: %s' %
1023 (file_name, artifacts))
1024
1025 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -08001026 def setup_telemetry(self, **kwargs):
1027 """Extracts and sets up telemetry
1028
1029 This method goes through the telemetry deps packages, and stages them on
1030 the devserver to be used by the drones and the telemetry tests.
1031
1032 Args:
1033 archive_url: Google Storage URL for the build.
1034
1035 Returns:
1036 Path to the source folder for the telemetry codebase once it is staged.
1037 """
Gabe Black3b567202015-09-23 14:07:59 -07001038 dl = _get_downloader(kwargs)
Simran Basi4baad082013-02-14 13:39:18 -08001039
Gabe Black3b567202015-09-23 14:07:59 -07001040 build_path = dl.GetBuildDir()
Simran Basi4baad082013-02-14 13:39:18 -08001041 deps_path = os.path.join(build_path, 'autotest/packages')
1042 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
1043 src_folder = os.path.join(telemetry_path, 'src')
1044
1045 with self._telemetry_lock_dict.lock(telemetry_path):
1046 if os.path.exists(src_folder):
1047 # Telemetry is already fully stage return
1048 return src_folder
1049
1050 common_util.MkDirP(telemetry_path)
1051
1052 # Copy over the required deps tar balls to the telemetry directory.
1053 for dep in TELEMETRY_DEPS:
1054 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -07001055 if not os.path.exists(dep_path):
1056 # This dep does not exist (could be new), do not extract it.
1057 continue
Simran Basi4baad082013-02-14 13:39:18 -08001058 try:
1059 common_util.ExtractTarball(dep_path, telemetry_path)
1060 except common_util.CommonUtilError as e:
1061 shutil.rmtree(telemetry_path)
1062 raise DevServerError(str(e))
1063
1064 # By default all the tarballs extract to test_src but some parts of
1065 # the telemetry code specifically hardcoded to exist inside of 'src'.
1066 test_src = os.path.join(telemetry_path, 'test_src')
1067 try:
1068 shutil.move(test_src, src_folder)
1069 except shutil.Error:
1070 # This can occur if src_folder already exists. Remove and retry move.
1071 shutil.rmtree(src_folder)
Gabe Black3b567202015-09-23 14:07:59 -07001072 raise DevServerError(
1073 'Failure in telemetry setup for build %s. Appears that the '
1074 'test_src to src move failed.' % dl.GetBuild())
Simran Basi4baad082013-02-14 13:39:18 -08001075
1076 return src_folder
1077
1078 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -08001079 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -07001080 """Symbolicates a minidump using pre-downloaded symbols, returns it.
1081
1082 Callers will need to POST to this URL with a body of MIME-type
1083 "multipart/form-data".
1084 The body should include a single argument, 'minidump', containing the
1085 binary-formatted minidump to symbolicate.
1086
Chris Masone816e38c2012-05-02 12:22:36 -07001087 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -08001088 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -07001089 minidump: The binary minidump file to symbolicate.
1090 """
Chris Sosa76e44b92013-01-31 12:11:38 -08001091 # Ensure the symbols have been staged.
Dan Shif08fe492016-10-04 14:39:25 -07001092 # Try debug.tar.xz first, then debug.tgz
1093 for artifact in (artifact_info.SYMBOLS_ONLY, artifact_info.SYMBOLS):
1094 kwargs['artifacts'] = artifact
1095 dl = _get_downloader(kwargs)
1096
1097 try:
1098 if self.stage(**kwargs) == 'Success':
1099 break
1100 except build_artifact.ArtifactDownloadError:
1101 continue
1102 else:
Gabe Black3b567202015-09-23 14:07:59 -07001103 raise DevServerError('Failed to stage symbols for %s' %
1104 dl.DescribeSource())
Chris Sosa76e44b92013-01-31 12:11:38 -08001105
Chris Masone816e38c2012-05-02 12:22:36 -07001106 to_return = ''
1107 with tempfile.NamedTemporaryFile() as local:
1108 while True:
1109 data = minidump.file.read(8192)
1110 if not data:
1111 break
1112 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -08001113
Chris Masone816e38c2012-05-02 12:22:36 -07001114 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -08001115
Gabe Black3b567202015-09-23 14:07:59 -07001116 symbols_directory = os.path.join(dl.GetBuildDir(), 'debug', 'breakpad')
Chris Sosa76e44b92013-01-31 12:11:38 -08001117
1118 stackwalk = subprocess.Popen(
1119 ['minidump_stackwalk', local.name, symbols_directory],
1120 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1121
Chris Masone816e38c2012-05-02 12:22:36 -07001122 to_return, error_text = stackwalk.communicate()
1123 if stackwalk.returncode != 0:
1124 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
1125 error_text, stackwalk.returncode))
1126
1127 return to_return
1128
1129 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -08001130 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 15:31:36 -04001131 """Return a string representing the latest build for a given target.
1132
1133 Args:
1134 target: The build target, typically a combination of the board and the
1135 type of build e.g. x86-mario-release.
1136 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
1137 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-07 18:21:26 -08001138
Scott Zawalski16954532012-03-20 15:31:36 -04001139 Returns:
1140 A string representation of the latest build if one exists, i.e.
1141 R19-1993.0.0-a1-b1480.
1142 An empty string if no latest could be found.
1143 """
Don Garrettf84631a2014-01-07 18:21:26 -08001144 if not kwargs:
Scott Zawalski16954532012-03-20 15:31:36 -04001145 return _PrintDocStringAsHTML(self.latestbuild)
1146
Don Garrettf84631a2014-01-07 18:21:26 -08001147 if 'target' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -07001148 raise common_util.DevServerHTTPError(500, 'Error: target= is required!')
Dan Shi61305df2015-10-26 16:52:35 -07001149
1150 if _is_android_build_request(kwargs):
1151 branch = kwargs.get('branch', None)
1152 target = kwargs.get('target', None)
1153 if not target or not branch:
1154 raise DevServerError(
xixuan52c2fba2016-05-20 17:02:48 -07001155 'Both target and branch must be specified to query for the latest '
1156 'Android build.')
Dan Shi61305df2015-10-26 16:52:35 -07001157 return android_build.BuildAccessor.GetLatestBuildID(target, branch)
1158
Scott Zawalski16954532012-03-20 15:31:36 -04001159 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001160 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-07 18:21:26 -08001161 updater.static_dir, kwargs['target'],
1162 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -07001163 except common_util.CommonUtilError as errmsg:
Chris Sosa4b951602014-04-09 20:26:07 -07001164 raise common_util.DevServerHTTPError(500, str(errmsg))
Scott Zawalski16954532012-03-20 15:31:36 -04001165
1166 @cherrypy.expose
xixuan7efd0002016-04-14 15:34:01 -07001167 def list_suite_controls(self, **kwargs):
1168 """Return a list of contents of all known control files.
1169
1170 Example URL:
1171 To List all control files' content:
1172 http://dev-server/list_suite_controls?suite_name=bvt&
1173 build=daisy_spring-release/R29-4279.0.0
1174
1175 Args:
1176 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
1177 suite_name: List the control files belonging to that suite.
1178
1179 Returns:
Dan Shia1cd6522016-04-18 16:07:21 -07001180 A dictionary of all control files's path to its content for given suite.
xixuan7efd0002016-04-14 15:34:01 -07001181 """
1182 if not kwargs:
1183 return _PrintDocStringAsHTML(self.controlfiles)
1184
1185 if 'build' not in kwargs:
1186 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
1187
1188 if 'suite_name' not in kwargs:
Dan Shia1cd6522016-04-18 16:07:21 -07001189 raise common_util.DevServerHTTPError(500,
1190 'Error: suite_name= is required!')
xixuan7efd0002016-04-14 15:34:01 -07001191
1192 control_file_list = [
1193 line.rstrip() for line in common_util.GetControlFileListForSuite(
1194 updater.static_dir, kwargs['build'],
1195 kwargs['suite_name']).splitlines()]
1196
Dan Shia1cd6522016-04-18 16:07:21 -07001197 control_file_content_dict = {}
xixuan7efd0002016-04-14 15:34:01 -07001198 for control_path in control_file_list:
Dan Shia1cd6522016-04-18 16:07:21 -07001199 control_file_content_dict[control_path] = (common_util.GetControlFile(
xixuan7efd0002016-04-14 15:34:01 -07001200 updater.static_dir, kwargs['build'], control_path))
1201
Dan Shia1cd6522016-04-18 16:07:21 -07001202 return json.dumps(control_file_content_dict)
xixuan7efd0002016-04-14 15:34:01 -07001203
1204 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -08001205 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 17:17:28 -05001206 """Return a control file or a list of all known control files.
1207
1208 Example URL:
1209 To List all control files:
beepsbd337242013-07-09 22:44:06 -07001210 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
1211 To List all control files for, say, the bvt suite:
1212 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -05001213 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -05001214 http://dev-server/controlfiles?board=x86-alex-release&build=R18-1514.0.0&control_path=client/sleeptest/control
Scott Zawalski4647ce62012-01-03 17:17:28 -05001215
1216 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -05001217 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -05001218 control_path: If you want the contents of a control file set this
1219 to the path. E.g. client/site_tests/sleeptest/control
1220 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -07001221 suite_name: If control_path is not specified but a suite_name is
1222 specified, list the control files belonging to that suite instead of
1223 all control files. The empty string for suite_name will list all control
1224 files for the build.
Don Garrettf84631a2014-01-07 18:21:26 -08001225
Scott Zawalski4647ce62012-01-03 17:17:28 -05001226 Returns:
1227 Contents of a control file if control_path is provided.
1228 A list of control files if no control_path is provided.
1229 """
Don Garrettf84631a2014-01-07 18:21:26 -08001230 if not kwargs:
Scott Zawalski4647ce62012-01-03 17:17:28 -05001231 return _PrintDocStringAsHTML(self.controlfiles)
1232
Don Garrettf84631a2014-01-07 18:21:26 -08001233 if 'build' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -07001234 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -05001235
Don Garrettf84631a2014-01-07 18:21:26 -08001236 if 'control_path' not in kwargs:
1237 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-09 22:44:06 -07001238 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-07 18:21:26 -08001239 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-09 22:44:06 -07001240 else:
1241 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-07 18:21:26 -08001242 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -05001243 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001244 return common_util.GetControlFile(
Don Garrettf84631a2014-01-07 18:21:26 -08001245 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -08001246
1247 @cherrypy.expose
Simran Basi99e63c02014-05-20 10:39:52 -07001248 def xbuddy_translate(self, *args, **kwargs):
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001249 """Translates an xBuddy path to a real path to artifact if it exists.
1250
1251 Args:
Simran Basi99e63c02014-05-20 10:39:52 -07001252 args: An xbuddy path in the form of {local|remote}/build_id/artifact.
1253 Local searches the devserver's static directory. Remote searches a
1254 Google Storage image archive.
1255
1256 Kwargs:
1257 image_dir: Google Storage image archive to search in if requesting a
1258 remote artifact. If none uses the default bucket.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001259
1260 Returns:
Simran Basi99e63c02014-05-20 10:39:52 -07001261 String in the format of build_id/artifact as stored on the local server
1262 or in Google Storage.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001263 """
Simran Basi99e63c02014-05-20 10:39:52 -07001264 build_id, filename = self._xbuddy.Translate(
Gabe Black3b567202015-09-23 14:07:59 -07001265 args, image_dir=kwargs.get('image_dir'))
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001266 response = os.path.join(build_id, filename)
1267 _Log('Path translation requested, returning: %s', response)
1268 return response
1269
1270 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -07001271 def xbuddy(self, *args, **kwargs):
1272 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -07001273
1274 Args:
joycheneaf4cfc2013-07-02 08:38:57 -07001275 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -07001276 components of the path. The path can be understood as
1277 "{local|remote}/build_id/artifact" where build_id is composed of
1278 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -07001279
joychen121fc9b2013-08-02 14:30:30 -07001280 The first path element is optional, and can be "remote" or "local"
1281 If local (the default), devserver will not attempt to access Google
1282 Storage, and will only search the static directory for the files.
1283 If remote, devserver will try to obtain the artifact off GS if it's
1284 not found locally.
1285 The board is the familiar board name, optionally suffixed.
1286 The version can be the google storage version number, and may also be
1287 any of a number of xBuddy defined version aliases that will be
1288 translated into the latest built image that fits the description.
1289 Defaults to latest.
1290 The artifact is one of a number of image or artifact aliases used by
1291 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -07001292
1293 Kwargs:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001294 for_update: {true|false}
1295 if true, pregenerates the update payloads for the image,
1296 and returns the update uri to pass to the
1297 update_engine_client.
joychen3cb228e2013-06-12 12:13:13 -07001298 return_dir: {true|false}
1299 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001300 relative_path: {true|false}
1301 if set to true, returns the relative path to the payload
1302 directory from static_dir.
joychen3cb228e2013-06-12 12:13:13 -07001303 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -07001304 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -07001305 or
joycheneaf4cfc2013-07-02 08:38:57 -07001306 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -07001307
1308 Returns:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001309 If |for_update|, returns a redirect to the image or update file
1310 on the devserver. E.g.,
1311 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
1312 chromium-test-image.bin
1313 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
1314 http://host:port/static/x86-generic-release/R26-4000.0.0/
1315 If |relative_path| is true, return a relative path the folder where the
1316 payloads are. E.g.,
1317 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 12:13:13 -07001318 """
Chris Sosa75490802013-09-30 17:21:45 -07001319 boolean_string = kwargs.get('for_update')
1320 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001321 boolean_string = kwargs.get('return_dir')
1322 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
1323 boolean_string = kwargs.get('relative_path')
1324 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -07001325
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001326 if return_dir and relative_path:
Chris Sosa4b951602014-04-09 20:26:07 -07001327 raise common_util.DevServerHTTPError(
1328 500, 'Cannot specify both return_dir and relative_path')
Chris Sosa75490802013-09-30 17:21:45 -07001329
1330 # For updates, we optimize downloading of test images.
1331 file_name = None
1332 build_id = None
1333 if for_update:
1334 try:
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001335 build_id = self._xbuddy.StageTestArtifactsForUpdate(args)
Chris Sosa75490802013-09-30 17:21:45 -07001336 except build_artifact.ArtifactDownloadError:
1337 build_id = None
1338
1339 if not build_id:
1340 build_id, file_name = self._xbuddy.Get(args)
1341
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001342 if for_update:
1343 _Log('Payload generation triggered by request')
1344 # Forces payload to be in cache and symlinked into build_id dir.
Chris Sosa75490802013-09-30 17:21:45 -07001345 updater.GetUpdateForLabel(autoupdate.FORCED_UPDATE, build_id,
1346 image_name=file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001347
1348 response = None
1349 if return_dir:
1350 response = os.path.join(cherrypy.request.base, 'static', build_id)
1351 _Log('Directory requested, returning: %s', response)
1352 elif relative_path:
1353 response = build_id
1354 _Log('Relative path requested, returning: %s', response)
1355 elif for_update:
1356 response = os.path.join(cherrypy.request.base, 'update', build_id)
1357 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 12:13:13 -07001358 else:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001359 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 14:30:30 -07001360 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001361 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 14:30:30 -07001362 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -07001363
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001364 return response
1365
joychen3cb228e2013-06-12 12:13:13 -07001366 @cherrypy.expose
1367 def xbuddy_list(self):
1368 """Lists the currently available images & time since last access.
1369
Gilad Arnold452fd272014-02-04 11:09:28 -08001370 Returns:
1371 A string representation of a list of tuples [(build_id, time since last
1372 access),...]
joychen3cb228e2013-06-12 12:13:13 -07001373 """
1374 return self._xbuddy.List()
1375
1376 @cherrypy.expose
1377 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 11:09:28 -08001378 """Returns the number of images cached by xBuddy."""
joychen3cb228e2013-06-12 12:13:13 -07001379 return self._xbuddy.Capacity()
1380
1381 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001382 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001383 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001384 return ('Welcome to the Dev Server!<br>\n'
1385 '<br>\n'
1386 'Here are the available methods, click for documentation:<br>\n'
1387 '<br>\n'
1388 '%s' %
1389 '<br>\n'.join(
1390 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001391 for name in _FindExposedMethods(
1392 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001393
1394 @cherrypy.expose
1395 def doc(self, *args):
1396 """Shows the documentation for available methods / URLs.
1397
1398 Example:
1399 http://myhost/doc/update
1400 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001401 name = '/'.join(args)
1402 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001403 if not method:
1404 raise DevServerError("No exposed method named `%s'" % name)
1405 if not method.__doc__:
1406 raise DevServerError("No documentation for exposed method `%s'" % name)
1407 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -07001408
Dale Curtisc9aaf3a2011-08-09 15:47:40 -07001409 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001410 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001411 """Handles an update check from a Chrome OS client.
1412
1413 The HTTP request should contain the standard Omaha-style XML blob. The URL
1414 line may contain an additional intermediate path to the update payload.
1415
joychen121fc9b2013-08-02 14:30:30 -07001416 This request can be handled in one of 4 ways, depending on the devsever
1417 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -07001418
joychen121fc9b2013-08-02 14:30:30 -07001419 1. No intermediate path
1420 If no intermediate path is given, the default behavior is to generate an
1421 update payload from the latest test image locally built for the board
1422 specified in the xml. Devserver serves the generated payload.
1423
1424 2. Path explicitly invokes XBuddy
1425 If there is a path given, it can explicitly invoke xbuddy by prefixing it
1426 with 'xbuddy'. This path is then used to acquire an image binary for the
1427 devserver to generate an update payload from. Devserver then serves this
1428 payload.
1429
1430 3. Path is left for the devserver to interpret.
1431 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
1432 to generate a payload from the test image in that directory and serve it.
1433
1434 4. The devserver is in a 'forced' mode. TO BE DEPRECATED
1435 This comes from the usage of --forced_payload or --image when starting the
1436 devserver. No matter what path (or no path) gets passed in, devserver will
1437 serve the update payload (--forced_payload) or generate an update payload
1438 from the image (--image).
1439
1440 Examples:
1441 1. No intermediate path
1442 update_engine_client --omaha_url=http://myhost/update
1443 This generates an update payload from the latest test image locally built
1444 for the board specified in the xml.
1445
1446 2. Explicitly invoke xbuddy
1447 update_engine_client --omaha_url=
1448 http://myhost/update/xbuddy/remote/board/version/dev
1449 This would go to GS to download the dev image for the board, from which
1450 the devserver would generate a payload to serve.
1451
1452 3. Give a path for devserver to interpret
1453 update_engine_client --omaha_url=http://myhost/update/some/random/path
1454 This would attempt, in order to:
1455 a) Generate an update from a test image binary if found in
1456 static_dir/some/random/path.
1457 b) Serve an update payload found in static_dir/some/random/path.
1458 c) Hope that some/random/path takes the form "board/version" and
1459 and attempt to download an update payload for that board/version
1460 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001461 """
joychen121fc9b2013-08-02 14:30:30 -07001462 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -08001463 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -07001464 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-11 19:49:01 -07001465
joychen121fc9b2013-08-02 14:30:30 -07001466 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001467
Dan Shiafd0e492015-05-27 14:23:51 -07001468 @require_psutil()
1469 def _get_io_stats(self):
1470 """Get the IO stats as a dictionary.
1471
Gabe Black3b567202015-09-23 14:07:59 -07001472 Returns:
1473 A dictionary of IO stats collected by psutil.
Dan Shiafd0e492015-05-27 14:23:51 -07001474 """
1475 return {'disk_read_bytes_per_second': self.disk_read_bytes_per_sec,
1476 'disk_write_bytes_per_second': self.disk_write_bytes_per_sec,
1477 'disk_total_bytes_per_second': (self.disk_read_bytes_per_sec +
1478 self.disk_write_bytes_per_sec),
1479 'network_sent_bytes_per_second': self.network_sent_bytes_per_sec,
1480 'network_recv_bytes_per_second': self.network_recv_bytes_per_sec,
1481 'network_total_bytes_per_second': (self.network_sent_bytes_per_sec +
1482 self.network_recv_bytes_per_sec),
1483 'cpu_percent': psutil.cpu_percent(),}
1484
Dan Shi7247f9c2016-06-01 09:19:09 -07001485
1486 def _get_process_count(self, process_cmd_pattern):
1487 """Get the count of processes that match the given command pattern.
1488
1489 Args:
1490 process_cmd_pattern: The regex pattern of process command to match.
1491
1492 Returns:
1493 The count of processes that match the given command pattern.
1494 """
1495 try:
1496 return int(subprocess.check_output(
1497 'pgrep -fc "%s"' % process_cmd_pattern, shell=True))
1498 except subprocess.CalledProcessError:
1499 return 0
1500
1501
Dan Shif5ce2de2013-04-25 16:06:32 -07001502 @cherrypy.expose
1503 def check_health(self):
1504 """Collect the health status of devserver to see if it's ready for staging.
1505
Gilad Arnold452fd272014-02-04 11:09:28 -08001506 Returns:
1507 A JSON dictionary containing all or some of the following fields:
1508 free_disk (int): free disk space in GB
1509 staging_thread_count (int): number of devserver threads currently staging
1510 an image
Dan Shi7247f9c2016-06-01 09:19:09 -07001511 apache_client_count (int): count of Apache processes.
1512 telemetry_test_count (int): count of telemetry tests.
1513 gsutil_count (int): count of gsutil processes.
Dan Shif5ce2de2013-04-25 16:06:32 -07001514 """
1515 # Get free disk space.
1516 stat = os.statvfs(updater.static_dir)
1517 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
Dan Shi7247f9c2016-06-01 09:19:09 -07001518 apache_client_count = self._get_process_count('apache')
1519 telemetry_test_count = self._get_process_count('python.*telemetry')
1520 gsutil_count = self._get_process_count('gsutil')
Dan Shif5ce2de2013-04-25 16:06:32 -07001521
Dan Shiafd0e492015-05-27 14:23:51 -07001522 health_data = {
Dan Shif5ce2de2013-04-25 16:06:32 -07001523 'free_disk': free_disk,
Dan Shid76e6bb2016-01-28 22:28:51 -08001524 'staging_thread_count': DevServerRoot._staging_thread_count,
1525 'apache_client_count': apache_client_count,
Dan Shi7247f9c2016-06-01 09:19:09 -07001526 'telemetry_test_count': telemetry_test_count,
1527 'gsutil_count': gsutil_count}
Dan Shiafd0e492015-05-27 14:23:51 -07001528 health_data.update(self._get_io_stats() or {})
1529
1530 return json.dumps(health_data)
Dan Shif5ce2de2013-04-25 16:06:32 -07001531
1532
Chris Sosadbc20082012-12-10 13:39:11 -08001533def _CleanCache(cache_dir, wipe):
1534 """Wipes any excess cached items in the cache_dir.
1535
1536 Args:
1537 cache_dir: the directory we are wiping from.
1538 wipe: If True, wipe all the contents -- not just the excess.
1539 """
1540 if wipe:
1541 # Clear the cache and exit on error.
1542 cmd = 'rm -rf %s/*' % cache_dir
1543 if os.system(cmd) != 0:
1544 _Log('Failed to clear the cache with %s' % cmd)
1545 sys.exit(1)
1546 else:
1547 # Clear all but the last N cached updates
1548 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
1549 (cache_dir, CACHED_ENTRIES))
1550 if os.system(cmd) != 0:
1551 _Log('Failed to clean up old delta cache files with %s' % cmd)
1552 sys.exit(1)
1553
1554
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001555def _AddTestingOptions(parser):
1556 group = optparse.OptionGroup(
1557 parser, 'Advanced Testing Options', 'These are used by test scripts and '
1558 'developers writing integration tests utilizing the devserver. They are '
1559 'not intended to be really used outside the scope of someone '
1560 'knowledgable about the test.')
1561 group.add_option('--exit',
1562 action='store_true',
1563 help='do not start the server (yet pregenerate/clear cache)')
1564 group.add_option('--host_log',
1565 action='store_true', default=False,
1566 help='record history of host update events (/api/hostlog)')
1567 group.add_option('--max_updates',
Gabe Black3b567202015-09-23 14:07:59 -07001568 metavar='NUM', default=-1, type='int',
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001569 help='maximum number of update checks handled positively '
1570 '(default: unlimited)')
1571 group.add_option('--private_key',
1572 metavar='PATH', default=None,
1573 help='path to the private key in pem format. If this is set '
1574 'the devserver will generate update payloads that are '
1575 'signed with this key.')
David Zeuthen52ccd012013-10-31 12:58:26 -07001576 group.add_option('--private_key_for_metadata_hash_signature',
1577 metavar='PATH', default=None,
1578 help='path to the private key in pem format. If this is set '
1579 'the devserver will sign the metadata hash with the given '
1580 'key and transmit in the Omaha-style XML response.')
1581 group.add_option('--public_key',
1582 metavar='PATH', default=None,
1583 help='path to the public key in pem format. If this is set '
1584 'the devserver will transmit a base64 encoded version of '
1585 'the content in the Omaha-style XML response.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001586 group.add_option('--proxy_port',
1587 metavar='PORT', default=None, type='int',
1588 help='port to have the client connect to -- basically the '
1589 'devserver lies to the update to tell it to get the payload '
1590 'from a different port that will proxy the request back to '
1591 'the devserver. The proxy must be managed outside the '
1592 'devserver.')
1593 group.add_option('--remote_payload',
1594 action='store_true', default=False,
Chris Sosa4b951602014-04-09 20:26:07 -07001595 help='Payload is being served from a remote machine. With '
1596 'this setting enabled, this devserver instance serves as '
1597 'just an Omaha server instance. In this mode, the '
1598 'devserver enforces a few extra components of the Omaha '
Chris Sosafc715442014-04-09 20:45:23 -07001599 'protocol, such as hardware class, being sent.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001600 group.add_option('-u', '--urlbase',
1601 metavar='URL',
Gabe Black3b567202015-09-23 14:07:59 -07001602 help='base URL for update images, other than the '
1603 'devserver. Use in conjunction with remote_payload.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001604 parser.add_option_group(group)
1605
1606
1607def _AddUpdateOptions(parser):
1608 group = optparse.OptionGroup(
1609 parser, 'Autoupdate Options', 'These options can be used to change '
1610 'how the devserver either generates or serve update payloads. Please '
1611 'note that all of these option affect how a payload is generated and so '
1612 'do not work in archive-only mode.')
1613 group.add_option('--board',
1614 help='By default the devserver will create an update '
1615 'payload from the latest image built for the board '
1616 'a device that is requesting an update has. When we '
1617 'pre-generate an update (see below) and we do not specify '
1618 'another update_type option like image or payload, the '
1619 'devserver needs to know the board to generate the latest '
1620 'image for. This is that board.')
1621 group.add_option('--critical_update',
1622 action='store_true', default=False,
1623 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001624 group.add_option('--image',
1625 metavar='FILE',
1626 help='Generate and serve an update using this image to any '
1627 'device that requests an update.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001628 group.add_option('--payload',
1629 metavar='PATH',
1630 help='use the update payload from specified directory '
1631 '(update.gz).')
1632 group.add_option('-p', '--pregenerate_update',
1633 action='store_true', default=False,
1634 help='pre-generate the update payload before accepting '
1635 'update requests. Useful to help debug payload generation '
1636 'issues quickly. Also if an update payload will take a '
1637 'long time to generate, a client may timeout if you do not'
1638 'pregenerate the update.')
1639 group.add_option('--src_image',
1640 metavar='PATH', default='',
1641 help='If specified, delta updates will be generated using '
1642 'this image as the source image. Delta updates are when '
1643 'you are updating from a "source image" to a another '
1644 'image.')
1645 parser.add_option_group(group)
1646
1647
1648def _AddProductionOptions(parser):
1649 group = optparse.OptionGroup(
1650 parser, 'Advanced Server Options', 'These options can be used to changed '
1651 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001652 group.add_option('--clear_cache',
1653 action='store_true', default=False,
1654 help='At startup, removes all cached entries from the'
1655 'devserver\'s cache.')
1656 group.add_option('--logfile',
1657 metavar='PATH',
1658 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -07001659 group.add_option('--pidfile',
1660 metavar='PATH',
1661 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 11:04:13 -08001662 group.add_option('--portfile',
1663 metavar='PATH',
1664 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001665 group.add_option('--production',
1666 action='store_true', default=False,
1667 help='have the devserver use production values when '
1668 'starting up. This includes using more threads and '
1669 'performing less logging.')
1670 parser.add_option_group(group)
1671
1672
Paul Hobbsef4e0702016-06-27 17:01:42 -07001673def MakeLogHandler(logfile):
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001674 """Create a LogHandler instance used to log all messages."""
1675 hdlr_cls = handlers.TimedRotatingFileHandler
1676 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
1677 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -07001678 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001679 return hdlr
1680
1681
Chris Sosacde6bf42012-05-31 18:36:39 -07001682def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001683 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -08001684 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -07001685
1686 # get directory that the devserver is run from
1687 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -07001688 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -07001689 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001690 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -07001691 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -07001692 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001693 parser.add_option('--port',
1694 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 13:13:28 -08001695 help=('port for the dev server to use; if zero, binds to '
1696 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001697 parser.add_option('-t', '--test_image',
1698 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -07001699 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -07001700 parser.add_option('-x', '--xbuddy_manage_builds',
1701 action='store_true',
1702 default=False,
1703 help='If set, allow xbuddy to manage images in'
1704 'build/images.')
Dan Shi72b16132015-10-08 12:10:33 -07001705 parser.add_option('-a', '--android_build_credential',
1706 default=None,
1707 help='Path to a json file which contains the credential '
1708 'needed to access Android builds.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001709 _AddProductionOptions(parser)
1710 _AddUpdateOptions(parser)
1711 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -07001712 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001713
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001714 # Handle options that must be set globally in cherrypy. Do this
1715 # work up front, because calls to _Log() below depend on this
1716 # initialization.
1717 if options.production:
1718 cherrypy.config.update({'environment': 'production'})
1719 if not options.logfile:
1720 cherrypy.config.update({'log.screen': True})
1721 else:
1722 cherrypy.config.update({'log.error_file': '',
1723 'log.access_file': ''})
Paul Hobbsef4e0702016-06-27 17:01:42 -07001724 hdlr = MakeLogHandler(options.logfile)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001725 # Pylint can't seem to process these two calls properly
1726 # pylint: disable=E1101
1727 cherrypy.log.access_log.addHandler(hdlr)
1728 cherrypy.log.error_log.addHandler(hdlr)
1729 # pylint: enable=E1101
1730
joychened64b222013-06-21 16:39:34 -07001731 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001732 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001733
joychened64b222013-06-21 16:39:34 -07001734 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001735 # If our devserver is only supposed to serve payloads, we shouldn't be
1736 # mucking with the cache at all. If the devserver hadn't previously
1737 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001738 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001739 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001740 else:
1741 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001742
Chris Sosadbc20082012-12-10 13:39:11 -08001743 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 16:39:34 -07001744 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001745
joychen121fc9b2013-08-02 14:30:30 -07001746 _xbuddy = xbuddy.XBuddy(options.xbuddy_manage_builds,
1747 options.board,
joychen121fc9b2013-08-02 14:30:30 -07001748 static_dir=options.static_dir)
Chris Sosa75490802013-09-30 17:21:45 -07001749 if options.clear_cache and options.xbuddy_manage_builds:
1750 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 14:30:30 -07001751
Chris Sosa6a3697f2013-01-29 16:44:43 -08001752 # We allow global use here to share with cherrypy classes.
1753 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001754 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001755 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001756 _xbuddy,
joychened64b222013-06-21 16:39:34 -07001757 static_dir=options.static_dir,
Andrew de los Reyes52620802010-04-12 13:40:07 -07001758 urlbase=options.urlbase,
Chris Sosa5d342a22010-09-28 16:54:41 -07001759 forced_image=options.image,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001760 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001761 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-28 23:42:37 -07001762 src_image=options.src_image,
Chris Sosa08d55a22011-01-19 16:08:02 -08001763 board=options.board,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001764 copy_to_static_root=not options.exit,
1765 private_key=options.private_key,
Gabe Black3b567202015-09-23 14:07:59 -07001766 private_key_for_metadata_hash_signature=(
1767 options.private_key_for_metadata_hash_signature),
David Zeuthen52ccd012013-10-31 12:58:26 -07001768 public_key=options.public_key,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001769 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001770 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001771 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001772 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001773 )
Chris Sosa7c931362010-10-11 19:49:01 -07001774
Chris Sosa6a3697f2013-01-29 16:44:43 -08001775 if options.pregenerate_update:
1776 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 15:46:22 -07001777
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001778 if options.exit:
1779 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001780
joychen3cb228e2013-06-12 12:13:13 -07001781 dev_server = DevServerRoot(_xbuddy)
1782
Gilad Arnold11fbef42014-02-10 11:04:13 -08001783 # Patch CherryPy to support binding to any available port (--port=0).
1784 cherrypy_ext.ZeroPortPatcher.DoPatch(cherrypy)
1785
Chris Sosa855b8932013-08-21 13:24:55 -07001786 if options.pidfile:
1787 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1788
Gilad Arnold11fbef42014-02-10 11:04:13 -08001789 if options.portfile:
1790 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1791
Dan Shiafd5c6c2016-01-07 10:27:03 -08001792 if (options.android_build_credential and
1793 os.path.exists(options.android_build_credential)):
1794 try:
1795 with open(options.android_build_credential) as f:
1796 android_build.BuildAccessor.credential_info = json.load(f)
1797 except ValueError as e:
1798 _Log('Failed to load the android build credential: %s. Error: %s.' %
1799 (options.android_build_credential, e))
joychen3cb228e2013-06-12 12:13:13 -07001800 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001801
1802
1803if __name__ == '__main__':
1804 main()