blob: ea0d70b349e8d4d6cda5bde4872edd42d4c1c2f9 [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
xixuan52c2fba2016-05-20 17:02:48 -070044import glob
Gilad Arnold55a2a372012-10-02 09:46:32 -070045import json
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070046import optparse
rtc@google.comded22402009-10-26 22:36:21 +000047import os
Scott Zawalski4647ce62012-01-03 17:17:28 -050048import re
Simran Basi4baad082013-02-14 13:39:18 -080049import shutil
xixuan52c2fba2016-05-20 17:02:48 -070050import signal
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -080051import socket
Chris Masone816e38c2012-05-02 12:22:36 -070052import subprocess
J. Richard Barnette3d977b82013-04-23 11:05:19 -070053import sys
Chris Masone816e38c2012-05-02 12:22:36 -070054import tempfile
Dan Shi59ae7092013-06-04 14:37:27 -070055import threading
Dan Shiafd0e492015-05-27 14:23:51 -070056import time
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -070057import types
J. Richard Barnette3d977b82013-04-23 11:05:19 -070058from logging import handlers
59
60import cherrypy
Chris Sosa855b8932013-08-21 13:24:55 -070061from cherrypy import _cplogging as cplogging
62from cherrypy.process import plugins
rtc@google.comded22402009-10-26 22:36:21 +000063
Chris Sosa0356d3b2010-09-16 15:46:22 -070064import autoupdate
Dan Shi2f136862016-02-11 15:38:38 -080065import artifact_info
Chris Sosa75490802013-09-30 17:21:45 -070066import build_artifact
Gilad Arnold11fbef42014-02-10 11:04:13 -080067import cherrypy_ext
xixuan52c2fba2016-05-20 17:02:48 -070068import cros_update
69import cros_update_progress
Gilad Arnoldc65330c2012-09-20 15:17:48 -070070import common_util
Simran Basief83d6a2014-08-28 14:32:01 -070071import devserver_constants
Chris Sosa47a7d4e2012-03-28 11:26:55 -070072import downloader
Chris Sosa7cd23202013-10-15 17:22:57 -070073import gsutil_util
Gilad Arnoldc65330c2012-09-20 15:17:48 -070074import log_util
joychen3cb228e2013-06-12 12:13:13 -070075import xbuddy
Gilad Arnoldc65330c2012-09-20 15:17:48 -070076
Gilad Arnoldc65330c2012-09-20 15:17:48 -070077# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080078def _Log(message, *args):
79 return log_util.LogWithTag('DEVSERVER', message, *args)
Chris Sosa0356d3b2010-09-16 15:46:22 -070080
Dan Shiafd0e492015-05-27 14:23:51 -070081try:
82 import psutil
83except ImportError:
84 # Ignore psutil import failure. This is for backwards compatibility, so
85 # "cros flash" can still update duts with build without psutil installed.
86 # The reason is that, during cros flash, local devserver code is copied over
87 # to DUT, and devserver will be running inside DUT to stage the build.
88 _Log('Python module psutil is not installed, devserver load data will not be '
89 'collected')
90 psutil = None
Dan Shi94dcbe82015-06-08 20:51:13 -070091except OSError as e:
92 # Ignore error like following. psutil may not work properly in builder. Ignore
93 # the error as load information of devserver is not used in builder.
94 # OSError: [Errno 2] No such file or directory: '/dev/pts/0'
95 _Log('psutil is failed to be imported, error: %s. devserver load data will '
96 'not be collected.', e)
97 psutil = None
98
Dan Shi72b16132015-10-08 12:10:33 -070099try:
100 import android_build
101except ImportError as e:
102 # Ignore android_build import failure. This is to support devserver running
103 # inside a ChromeOS device triggered by cros flash. Most ChromeOS test images
104 # do not have google-api-python-client module and they don't need to support
105 # Android updating, therefore, ignore the import failure here.
106 _Log('Import module android_build failed with error: %s', e)
107 android_build = None
Frank Farzan40160872011-12-12 18:39:18 -0800108
Chris Sosa417e55d2011-01-25 16:40:48 -0800109CACHED_ENTRIES = 12
Don Garrettf90edf02010-11-16 17:36:14 -0800110
Simran Basi4baad082013-02-14 13:39:18 -0800111TELEMETRY_FOLDER = 'telemetry_src'
112TELEMETRY_DEPS = ['dep-telemetry_dep.tar.bz2',
113 'dep-page_cycler_dep.tar.bz2',
Simran Basi0d078682013-03-22 16:40:04 -0700114 'dep-chrome_test.tar.bz2',
115 'dep-perf_data_dep.tar.bz2']
Simran Basi4baad082013-02-14 13:39:18 -0800116
Chris Sosa0356d3b2010-09-16 15:46:22 -0700117# Sets up global to share between classes.
rtc@google.com21a5ca32009-11-04 18:23:23 +0000118updater = None
rtc@google.comded22402009-10-26 22:36:21 +0000119
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700120# Log rotation parameters. These settings correspond to once a week
J. Richard Barnette6dfa5342013-06-04 11:48:56 -0700121# at midnight between Friday and Saturday, with about three months
122# of old logs kept for backup.
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700123#
124# For more, see the documentation for
125# logging.handlers.TimedRotatingFileHandler
J. Richard Barnette6dfa5342013-06-04 11:48:56 -0700126_LOG_ROTATION_TIME = 'W4'
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700127_LOG_ROTATION_BACKUP = 13
128
Dan Shiafd0e492015-05-27 14:23:51 -0700129# Number of seconds between the collection of disk and network IO counters.
130STATS_INTERVAL = 10.0
Frank Farzan40160872011-12-12 18:39:18 -0800131
xixuan52c2fba2016-05-20 17:02:48 -0700132# Auto-update parameters
133
134# Error msg for missing key in CrOS auto-update.
135KEY_ERROR_MSG = 'Key Error in cmd %s: %s= is required'
136
137# Command of running auto-update.
138AUTO_UPDATE_CMD = '/usr/bin/python -u %s -d %s -b %s --static_dir %s'
139
140
Chris Sosa9164ca32012-03-28 11:04:50 -0700141class DevServerError(Exception):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700142 """Exception class used by this module."""
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700143
144
Dan Shiafd0e492015-05-27 14:23:51 -0700145def require_psutil():
Gabe Black3b567202015-09-23 14:07:59 -0700146 """Decorator for functions require psutil to run."""
Dan Shiafd0e492015-05-27 14:23:51 -0700147 def deco_require_psutil(func):
148 """Wrapper of the decorator function.
149
Gabe Black3b567202015-09-23 14:07:59 -0700150 Args:
151 func: function to be called.
Dan Shiafd0e492015-05-27 14:23:51 -0700152 """
153 def func_require_psutil(*args, **kwargs):
154 """Decorator for functions require psutil to run.
155
156 If psutil is not installed, skip calling the function.
157
Gabe Black3b567202015-09-23 14:07:59 -0700158 Args:
159 *args: arguments for function to be called.
160 **kwargs: keyword arguments for function to be called.
Dan Shiafd0e492015-05-27 14:23:51 -0700161 """
162 if psutil:
163 return func(*args, **kwargs)
164 else:
165 _Log('Python module psutil is not installed. Function call %s is '
166 'skipped.' % func)
167 return func_require_psutil
168 return deco_require_psutil
169
170
Gabe Black3b567202015-09-23 14:07:59 -0700171def _canonicalize_archive_url(archive_url):
172 """Canonicalizes archive_url strings.
173
174 Raises:
175 DevserverError: if archive_url is not set.
176 """
177 if archive_url:
178 if not archive_url.startswith('gs://'):
179 raise DevServerError("Archive URL isn't from Google Storage (%s) ." %
180 archive_url)
181
182 return archive_url.rstrip('/')
183 else:
184 raise DevServerError("Must specify an archive_url in the request")
185
186
187def _canonicalize_local_path(local_path):
188 """Canonicalizes |local_path| strings.
189
190 Raises:
191 DevserverError: if |local_path| is not set.
192 """
193 # Restrict staging of local content to only files within the static
194 # directory.
195 local_path = os.path.abspath(local_path)
196 if not local_path.startswith(updater.static_dir):
197 raise DevServerError('Local path %s must be a subdirectory of the static'
198 ' directory: %s' % (local_path, updater.static_dir))
199
200 return local_path.rstrip('/')
201
202
203def _get_artifacts(kwargs):
204 """Returns a tuple of named and file artifacts given the stage rpc kwargs.
205
206 Raises:
207 DevserverError if no artifacts would be returned.
208 """
209 artifacts = kwargs.get('artifacts')
210 files = kwargs.get('files')
211 if not artifacts and not files:
212 raise DevServerError('No artifacts specified.')
213
214 # Note we NEED to coerce files to a string as we get raw unicode from
215 # cherrypy and we treat files as strings elsewhere in the code.
216 return (str(artifacts).split(',') if artifacts else [],
217 str(files).split(',') if files else [])
218
219
Dan Shi61305df2015-10-26 16:52:35 -0700220def _is_android_build_request(kwargs):
221 """Check if a devserver call is for Android build, based on the arguments.
222
223 This method exams the request's arguments (os_type) to determine if the
224 request is for Android build. If os_type is set to `android`, returns True.
225 If os_type is not set or has other values, returns False.
226
227 Args:
228 kwargs: Keyword arguments for the request.
229
230 Returns:
231 True if the request is for Android build. False otherwise.
232 """
233 os_type = kwargs.get('os_type', None)
234 return os_type == 'android'
235
236
Gabe Black3b567202015-09-23 14:07:59 -0700237def _get_downloader(kwargs):
238 """Returns the downloader based on passed in arguments.
239
240 Args:
241 kwargs: Keyword arguments for the request.
242 """
243 local_path = kwargs.get('local_path')
244 if local_path:
245 local_path = _canonicalize_local_path(local_path)
246
247 dl = None
248 if local_path:
249 dl = downloader.LocalDownloader(updater.static_dir, local_path)
250
Dan Shi61305df2015-10-26 16:52:35 -0700251 if not _is_android_build_request(kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700252 archive_url = kwargs.get('archive_url')
253 if not archive_url and not local_path:
254 raise DevServerError('Requires archive_url or local_path to be '
255 'specified.')
256 if archive_url and local_path:
257 raise DevServerError('archive_url and local_path can not both be '
258 'specified.')
259 if not dl:
260 archive_url = _canonicalize_archive_url(archive_url)
261 dl = downloader.GoogleStorageDownloader(updater.static_dir, archive_url)
262 elif not dl:
263 target = kwargs.get('target', None)
Dan Shi72b16132015-10-08 12:10:33 -0700264 branch = kwargs.get('branch', None)
Dan Shi61305df2015-10-26 16:52:35 -0700265 build_id = kwargs.get('build_id', None)
266 if not target or not branch or not build_id:
Dan Shi72b16132015-10-08 12:10:33 -0700267 raise DevServerError(
Dan Shi61305df2015-10-26 16:52:35 -0700268 'target, branch, build ID must all be specified for downloading '
269 'Android build.')
Dan Shi72b16132015-10-08 12:10:33 -0700270 dl = downloader.AndroidBuildDownloader(updater.static_dir, branch, build_id,
271 target)
Gabe Black3b567202015-09-23 14:07:59 -0700272
273 return dl
274
275
276def _get_downloader_and_factory(kwargs):
277 """Returns the downloader and artifact factory based on passed in arguments.
278
279 Args:
280 kwargs: Keyword arguments for the request.
281 """
282 artifacts, files = _get_artifacts(kwargs)
283 dl = _get_downloader(kwargs)
284
285 if (isinstance(dl, downloader.GoogleStorageDownloader) or
286 isinstance(dl, downloader.LocalDownloader)):
287 factory_class = build_artifact.ChromeOSArtifactFactory
Dan Shi72b16132015-10-08 12:10:33 -0700288 elif isinstance(dl, downloader.AndroidBuildDownloader):
Gabe Black3b567202015-09-23 14:07:59 -0700289 factory_class = build_artifact.AndroidArtifactFactory
290 else:
291 raise DevServerError('Unrecognized value for downloader type: %s' %
292 type(dl))
293
294 factory = factory_class(dl.GetBuildDir(), artifacts, files, dl.GetBuild())
295
296 return dl, factory
297
298
Scott Zawalski4647ce62012-01-03 17:17:28 -0500299def _LeadingWhiteSpaceCount(string):
300 """Count the amount of leading whitespace in a string.
301
302 Args:
303 string: The string to count leading whitespace in.
Don Garrettf84631a2014-01-07 18:21:26 -0800304
Scott Zawalski4647ce62012-01-03 17:17:28 -0500305 Returns:
306 number of white space chars before characters start.
307 """
Gabe Black3b567202015-09-23 14:07:59 -0700308 matched = re.match(r'^\s+', string)
Scott Zawalski4647ce62012-01-03 17:17:28 -0500309 if matched:
310 return len(matched.group())
311
312 return 0
313
314
315def _PrintDocStringAsHTML(func):
316 """Make a functions docstring somewhat HTML style.
317
318 Args:
319 func: The function to return the docstring from.
Don Garrettf84631a2014-01-07 18:21:26 -0800320
Scott Zawalski4647ce62012-01-03 17:17:28 -0500321 Returns:
322 A string that is somewhat formated for a web browser.
323 """
324 # TODO(scottz): Make this parse Args/Returns in a prettier way.
325 # Arguments could be bolded and indented etc.
326 html_doc = []
327 for line in func.__doc__.splitlines():
328 leading_space = _LeadingWhiteSpaceCount(line)
329 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700330 line = '&nbsp;' * leading_space + line
Scott Zawalski4647ce62012-01-03 17:17:28 -0500331
332 html_doc.append('<BR>%s' % line)
333
334 return '\n'.join(html_doc)
335
336
Simran Basief83d6a2014-08-28 14:32:01 -0700337def _GetUpdateTimestampHandler(static_dir):
338 """Returns a handler to update directory staged.timestamp.
339
340 This handler resets the stage.timestamp whenever static content is accessed.
341
342 Args:
343 static_dir: Directory from which static content is being staged.
344
345 Returns:
346 A cherrypy handler to update the timestamp of accessed content.
347 """
348 def UpdateTimestampHandler():
349 if not '404' in cherrypy.response.status:
350 build_match = re.match(devserver_constants.STAGED_BUILD_REGEX,
351 cherrypy.request.path_info)
352 if build_match:
353 build_dir = os.path.join(static_dir, build_match.group('build'))
354 downloader.Downloader.TouchTimestampForStaged(build_dir)
355 return UpdateTimestampHandler
356
357
Chris Sosa7c931362010-10-11 19:49:01 -0700358def _GetConfig(options):
359 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800360
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800361 socket_host = '::'
Yu-Ju Hongc8d4af32013-11-12 15:14:26 -0800362 # Fall back to IPv4 when python is not configured with IPv6.
363 if not socket.has_ipv6:
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800364 socket_host = '0.0.0.0'
365
Simran Basief83d6a2014-08-28 14:32:01 -0700366 # Adds the UpdateTimestampHandler to cherrypy's tools. This tools executes
367 # on the on_end_resource hook. This hook is called once processing is
368 # complete and the response is ready to be returned.
369 cherrypy.tools.update_timestamp = cherrypy.Tool(
370 'on_end_resource', _GetUpdateTimestampHandler(options.static_dir))
371
Gabe Black3b567202015-09-23 14:07:59 -0700372 base_config = {'global':
373 {'server.log_request_headers': True,
374 'server.protocol_version': 'HTTP/1.1',
375 'server.socket_host': socket_host,
376 'server.socket_port': int(options.port),
377 'response.timeout': 6000,
378 'request.show_tracebacks': True,
379 'server.socket_timeout': 60,
380 'server.thread_pool': 2,
381 'engine.autoreload.on': False,
382 },
383 '/api':
384 {
385 # Gets rid of cherrypy parsing post file for args.
386 'request.process_request_body': False,
387 },
388 '/build':
389 {'response.timeout': 100000,
390 },
391 '/update':
392 {
393 # Gets rid of cherrypy parsing post file for args.
394 'request.process_request_body': False,
395 'response.timeout': 10000,
396 },
397 # Sets up the static dir for file hosting.
398 '/static':
399 {'tools.staticdir.dir': options.static_dir,
400 'tools.staticdir.on': True,
401 'response.timeout': 10000,
402 'tools.update_timestamp.on': True,
403 },
404 }
Chris Sosa5f118ef2012-07-12 11:37:50 -0700405 if options.production:
Alex Miller93beca52013-07-30 19:25:09 -0700406 base_config['global'].update({'server.thread_pool': 150})
Chris Sosa7cd23202013-10-15 17:22:57 -0700407 # TODO(sosa): Do this more cleanly.
408 gsutil_util.GSUTIL_ATTEMPTS = 5
Scott Zawalski1c5e7cd2012-02-27 13:12:52 -0500409
Chris Sosa7c931362010-10-11 19:49:01 -0700410 return base_config
rtc@google.com64244662009-11-12 00:52:08 +0000411
Darin Petkove17164a2010-08-11 13:24:41 -0700412
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700413def _GetRecursiveMemberObject(root, member_list):
414 """Returns an object corresponding to a nested member list.
415
416 Args:
417 root: the root object to search
418 member_list: list of nested members to search
Don Garrettf84631a2014-01-07 18:21:26 -0800419
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700420 Returns:
421 An object corresponding to the member name list; None otherwise.
422 """
423 for member in member_list:
424 next_root = root.__class__.__dict__.get(member)
425 if not next_root:
426 return None
427 root = next_root
428 return root
429
430
431def _IsExposed(name):
432 """Returns True iff |name| has an `exposed' attribute and it is set."""
433 return hasattr(name, 'exposed') and name.exposed
434
435
Gilad Arnold748c8322012-10-12 09:51:35 -0700436def _GetExposedMethod(root, nested_member, ignored=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700437 """Returns a CherryPy-exposed method, if such exists.
438
439 Args:
440 root: the root object for searching
441 nested_member: a slash-joined path to the nested member
442 ignored: method paths to be ignored
Don Garrettf84631a2014-01-07 18:21:26 -0800443
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700444 Returns:
445 A function object corresponding to the path defined by |member_list| from
446 the |root| object, if the function is exposed and not ignored; None
447 otherwise.
448 """
Gilad Arnold748c8322012-10-12 09:51:35 -0700449 method = (not (ignored and nested_member in ignored) and
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700450 _GetRecursiveMemberObject(root, nested_member.split('/')))
Gabe Black3b567202015-09-23 14:07:59 -0700451 if method and type(method) == types.FunctionType and _IsExposed(method):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700452 return method
453
454
Gilad Arnold748c8322012-10-12 09:51:35 -0700455def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700456 """Finds exposed CherryPy methods.
457
458 Args:
459 root: the root object for searching
460 prefix: slash-joined chain of members leading to current object
461 unlisted: URLs to be excluded regardless of their exposed status
Don Garrettf84631a2014-01-07 18:21:26 -0800462
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700463 Returns:
464 List of exposed URLs that are not unlisted.
465 """
466 method_list = []
467 for member in sorted(root.__class__.__dict__.keys()):
468 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 09:51:35 -0700469 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700470 continue
471 member_obj = root.__class__.__dict__[member]
472 if _IsExposed(member_obj):
473 if type(member_obj) == types.FunctionType:
474 method_list.append(prefixed_member)
475 else:
476 method_list += _FindExposedMethods(
477 member_obj, prefixed_member, unlisted)
478 return method_list
479
480
xixuan52c2fba2016-05-20 17:02:48 -0700481def _check_base_args_for_auto_update(kwargs):
482 if 'host_name' not in kwargs:
483 raise common_util.DevServerHTTPError(KEY_ERROR_MSG % 'host_name')
484
485 if 'build_name' not in kwargs:
486 raise common_util.DevServerHTTPError(KEY_ERROR_MSG % 'build_name')
487
488
489def _parse_boolean_arg(kwargs, key):
490 if key in kwargs:
491 if kwargs[key] == 'True':
492 return True
493 elif kwargs[key] == 'False':
494 return False
495 else:
496 raise common_util.DevServerHTTPError(
497 'The value for key %s is not boolean.' % key)
498 else:
499 return False
500
501
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700502class ApiRoot(object):
503 """RESTful API for Dev Server information."""
504 exposed = True
505
506 @cherrypy.expose
507 def hostinfo(self, ip):
508 """Returns a JSON dictionary containing information about the given ip.
509
Gilad Arnold1b908392012-10-05 11:36:27 -0700510 Args:
511 ip: address of host whose info is requested
Don Garrettf84631a2014-01-07 18:21:26 -0800512
Gilad Arnold1b908392012-10-05 11:36:27 -0700513 Returns:
514 A JSON dictionary containing all or some of the following fields:
515 last_event_type (int): last update event type received
516 last_event_status (int): last update event status received
517 last_known_version (string): last known version reported in update ping
518 forced_update_label (string): update label to force next update ping to
519 use, set by setnextupdate
520 See the OmahaEvent class in update_engine/omaha_request_action.h for
521 event type and status code definitions. If the ip does not exist an empty
522 string is returned.
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700523
Gilad Arnold1b908392012-10-05 11:36:27 -0700524 Example URL:
525 http://myhost/api/hostinfo?ip=192.168.1.5
526 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700527 return updater.HandleHostInfoPing(ip)
528
529 @cherrypy.expose
Gilad Arnold286a0062012-01-12 13:47:02 -0800530 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 11:36:27 -0700531 """Returns a JSON object containing a log of host event.
532
533 Args:
534 ip: address of host whose event log is requested, or `all'
Don Garrettf84631a2014-01-07 18:21:26 -0800535
Gilad Arnold1b908392012-10-05 11:36:27 -0700536 Returns:
537 A JSON encoded list (log) of dictionaries (events), each of which
538 containing a `timestamp' and other event fields, as described under
539 /api/hostinfo.
540
541 Example URL:
542 http://myhost/api/hostlog?ip=192.168.1.5
543 """
Gilad Arnold286a0062012-01-12 13:47:02 -0800544 return updater.HandleHostLogPing(ip)
545
546 @cherrypy.expose
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700547 def setnextupdate(self, ip):
548 """Allows the response to the next update ping from a host to be set.
549
550 Takes the IP of the host and an update label as normally provided to the
Gilad Arnold1b908392012-10-05 11:36:27 -0700551 /update command.
552 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700553 body_length = int(cherrypy.request.headers['Content-Length'])
554 label = cherrypy.request.rfile.read(body_length)
555
556 if label:
557 label = label.strip()
558 if label:
559 return updater.HandleSetUpdatePing(ip, label)
Chris Sosa4b951602014-04-09 20:26:07 -0700560 raise common_util.DevServerHTTPError(400, 'No label provided.')
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700561
562
Gilad Arnold55a2a372012-10-02 09:46:32 -0700563 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800564 def fileinfo(self, *args):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700565 """Returns information about a given staged file.
566
567 Args:
Don Garrettf84631a2014-01-07 18:21:26 -0800568 args: path to the file inside the server's static staging directory
569
Gilad Arnold55a2a372012-10-02 09:46:32 -0700570 Returns:
571 A JSON encoded dictionary with information about the said file, which may
572 contain the following keys/values:
Gilad Arnold1b908392012-10-05 11:36:27 -0700573 size (int): the file size in bytes
574 sha1 (string): a base64 encoded SHA1 hash
575 sha256 (string): a base64 encoded SHA256 hash
576
577 Example URL:
578 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 09:46:32 -0700579 """
Don Garrettf84631a2014-01-07 18:21:26 -0800580 file_path = os.path.join(updater.static_dir, *args)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700581 if not os.path.exists(file_path):
582 raise DevServerError('file not found: %s' % file_path)
583 try:
584 file_size = os.path.getsize(file_path)
585 file_sha1 = common_util.GetFileSha1(file_path)
586 file_sha256 = common_util.GetFileSha256(file_path)
587 except os.error, e:
588 raise DevServerError('failed to get info for file %s: %s' %
Gilad Arnolde74b3812013-04-22 11:27:38 -0700589 (file_path, e))
590
591 is_delta = autoupdate.Autoupdate.IsDeltaFormatFile(file_path)
592
593 return json.dumps({
594 autoupdate.Autoupdate.SIZE_ATTR: file_size,
595 autoupdate.Autoupdate.SHA1_ATTR: file_sha1,
596 autoupdate.Autoupdate.SHA256_ATTR: file_sha256,
597 autoupdate.Autoupdate.ISDELTA_ATTR: is_delta
598 })
Gilad Arnold55a2a372012-10-02 09:46:32 -0700599
Chris Sosa76e44b92013-01-31 12:11:38 -0800600
David Rochberg7c79a812011-01-19 14:24:45 -0500601class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700602 """The Root Class for the Dev Server.
603
604 CherryPy works as follows:
605 For each method in this class, cherrpy interprets root/path
606 as a call to an instance of DevServerRoot->method_name. For example,
607 a call to http://myhost/build will call build. CherryPy automatically
608 parses http args and places them as keyword arguments in each method.
609 For paths http://myhost/update/dir1/dir2, you can use *args so that
610 cherrypy uses the update method and puts the extra paths in args.
611 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700612 # Method names that should not be listed on the index page.
613 _UNLISTED_METHODS = ['index', 'doc']
614
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700615 api = ApiRoot()
Chris Sosa7c931362010-10-11 19:49:01 -0700616
Dan Shi59ae7092013-06-04 14:37:27 -0700617 # Number of threads that devserver is staging images.
618 _staging_thread_count = 0
619 # Lock used to lock increasing/decreasing count.
620 _staging_thread_count_lock = threading.Lock()
621
Dan Shiafd0e492015-05-27 14:23:51 -0700622 @require_psutil()
623 def _refresh_io_stats(self):
624 """A call running in a thread to update IO stats periodically."""
625 prev_disk_io_counters = psutil.disk_io_counters()
626 prev_network_io_counters = psutil.net_io_counters()
627 prev_read_time = time.time()
628 while True:
629 time.sleep(STATS_INTERVAL)
630 now = time.time()
631 interval = now - prev_read_time
632 prev_read_time = now
633 # Disk IO is for all disks.
634 disk_io_counters = psutil.disk_io_counters()
635 network_io_counters = psutil.net_io_counters()
636
637 self.disk_read_bytes_per_sec = (
638 disk_io_counters.read_bytes -
639 prev_disk_io_counters.read_bytes)/interval
640 self.disk_write_bytes_per_sec = (
641 disk_io_counters.write_bytes -
642 prev_disk_io_counters.write_bytes)/interval
643 prev_disk_io_counters = disk_io_counters
644
645 self.network_sent_bytes_per_sec = (
646 network_io_counters.bytes_sent -
647 prev_network_io_counters.bytes_sent)/interval
648 self.network_recv_bytes_per_sec = (
649 network_io_counters.bytes_recv -
650 prev_network_io_counters.bytes_recv)/interval
651 prev_network_io_counters = network_io_counters
652
653 @require_psutil()
654 def _start_io_stat_thread(self):
Gabe Black3b567202015-09-23 14:07:59 -0700655 """Start the thread to collect IO stats."""
Dan Shiafd0e492015-05-27 14:23:51 -0700656 thread = threading.Thread(target=self._refresh_io_stats)
657 thread.daemon = True
658 thread.start()
659
joychen3cb228e2013-06-12 12:13:13 -0700660 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700661 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800662 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 12:13:13 -0700663 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 14:24:45 -0500664
Dan Shiafd0e492015-05-27 14:23:51 -0700665 # Cache of disk IO stats, a thread refresh the stats every 10 seconds.
666 # lock is not used for these variables as the only thread writes to these
667 # variables is _refresh_io_stats.
668 self.disk_read_bytes_per_sec = 0
669 self.disk_write_bytes_per_sec = 0
670 # Cache of network IO stats.
671 self.network_sent_bytes_per_sec = 0
672 self.network_recv_bytes_per_sec = 0
673 self._start_io_stat_thread()
674
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700675 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500676 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700677 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700678 import builder
679 if self._builder is None:
680 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500681 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700682
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700683 @cherrypy.expose
Dan Shif8eb0d12013-08-01 17:52:06 -0700684 def is_staged(self, **kwargs):
685 """Check if artifacts have been downloaded.
686
Chris Sosa6b0c6172013-08-05 17:01:33 -0700687 async: True to return without waiting for download to complete.
688 artifacts: Comma separated list of named artifacts to download.
689 These are defined in artifact_info and have their implementation
690 in build_artifact.py.
691 files: Comma separated list of file artifacts to stage. These
692 will be available as is in the corresponding static directory with no
693 custom post-processing.
694
695 returns: True of all artifacts are staged.
Dan Shif8eb0d12013-08-01 17:52:06 -0700696
697 Example:
698 To check if autotest and test_suites are staged:
699 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
700 artifacts=autotest,test_suites
701 """
Gabe Black3b567202015-09-23 14:07:59 -0700702 dl, factory = _get_downloader_and_factory(kwargs)
Aviv Keshet57d18172016-06-18 20:39:09 -0700703 response = str(dl.IsStaged(factory))
704 _Log('Responding to is_staged %s request with %r', kwargs, response)
705 return response
Dan Shi59ae7092013-06-04 14:37:27 -0700706
Chris Sosa76e44b92013-01-31 12:11:38 -0800707 @cherrypy.expose
Prashanth Ba06d2d22014-03-07 15:35:19 -0800708 def list_image_dir(self, **kwargs):
709 """Take an archive url and list the contents in its staged directory.
710
711 Args:
712 kwargs:
713 archive_url: Google Storage URL for the build.
714
715 Example:
716 To list the contents of where this devserver should have staged
717 gs://image-archive/<board>-release/<build> call:
718 http://devserver_url:<port>/list_image_dir?archive_url=<gs://..>
719
720 Returns:
721 A string with information about the contents of the image directory.
722 """
Gabe Black3b567202015-09-23 14:07:59 -0700723 dl = _get_downloader(kwargs)
Prashanth Ba06d2d22014-03-07 15:35:19 -0800724 try:
Gabe Black3b567202015-09-23 14:07:59 -0700725 image_dir_contents = dl.ListBuildDir()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800726 except build_artifact.ArtifactDownloadError as e:
727 return 'Cannot list the contents of staged artifacts. %s' % e
728 if not image_dir_contents:
Gabe Black3b567202015-09-23 14:07:59 -0700729 return '%s has not been staged on this devserver.' % dl.DescribeSource()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800730 return image_dir_contents
731
732 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800733 def stage(self, **kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700734 """Downloads and caches build artifacts.
Chris Sosa76e44b92013-01-31 12:11:38 -0800735
Gabe Black3b567202015-09-23 14:07:59 -0700736 Downloads and caches build artifacts, possibly from a Google Storage URL,
Dan Shi72b16132015-10-08 12:10:33 -0700737 or from Android's build server. Returns once these have been downloaded
Gabe Black3b567202015-09-23 14:07:59 -0700738 on the devserver. A call to this will attempt to cache non-specified
739 artifacts in the background for the given from the given URL following
740 the principle of spatial locality. Spatial locality of different
Chris Sosa76e44b92013-01-31 12:11:38 -0800741 artifacts is explicitly defined in the build_artifact module.
742
743 These artifacts will then be available from the static/ sub-directory of
744 the devserver.
745
746 Args:
747 archive_url: Google Storage URL for the build.
Simran Basi4243a862014-12-12 12:48:33 -0800748 local_path: Local path for the build.
Dan Shif8eb0d12013-08-01 17:52:06 -0700749 async: True to return without waiting for download to complete.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700750 artifacts: Comma separated list of named artifacts to download.
751 These are defined in artifact_info and have their implementation
752 in build_artifact.py.
753 files: Comma separated list of files to stage. These
754 will be available as is in the corresponding static directory with no
755 custom post-processing.
Laurence Goodbyf5c958d2016-01-14 18:23:56 -0800756 clean: True to remove any previously staged artifacts first.
Chris Sosa76e44b92013-01-31 12:11:38 -0800757
758 Example:
759 To download the autotest and test suites tarballs:
760 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
761 artifacts=autotest,test_suites
762 To download the full update payload:
763 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
764 artifacts=full_payload
Chris Sosa6b0c6172013-08-05 17:01:33 -0700765 To download just a file called blah.bin:
766 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
767 files=blah.bin
Chris Sosa76e44b92013-01-31 12:11:38 -0800768
769 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 16:39:34 -0700770 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 12:11:38 -0800771
772 Note for this example, relative path is the archive_url stripped of its
773 basename i.e. path/ in the examples above. Specific example:
774
775 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
776
777 Will get staged to:
778
joychened64b222013-06-21 16:39:34 -0700779 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Chris Sosa76e44b92013-01-31 12:11:38 -0800780 """
Gabe Black3b567202015-09-23 14:07:59 -0700781 dl, factory = _get_downloader_and_factory(kwargs)
782
Dan Shi59ae7092013-06-04 14:37:27 -0700783 with DevServerRoot._staging_thread_count_lock:
784 DevServerRoot._staging_thread_count += 1
785 try:
Laurence Goodbyf5c958d2016-01-14 18:23:56 -0800786 boolean_string = kwargs.get('clean')
787 clean = xbuddy.XBuddy.ParseBoolean(boolean_string)
788 if clean and os.path.exists(dl.GetBuildDir()):
789 _Log('Removing %s' % dl.GetBuildDir())
790 shutil.rmtree(dl.GetBuildDir())
Gabe Black3b567202015-09-23 14:07:59 -0700791 async = kwargs.get('async', False)
792 dl.Download(factory, async=async)
Dan Shi59ae7092013-06-04 14:37:27 -0700793 finally:
794 with DevServerRoot._staging_thread_count_lock:
795 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800796 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700797
798 @cherrypy.expose
xixuan52c2fba2016-05-20 17:02:48 -0700799 def cros_au(self, **kwargs):
800 """Auto-update a CrOS DUT.
801
802 Args:
803 kwargs:
804 host_name: the hostname of the DUT to auto-update.
805 build_name: the build name for update the DUT.
806 force_update: Force an update even if the version installed is the
807 same. Default: False.
808 full_update: If True, do not run stateful update, directly force a full
809 reimage. If False, try stateful update first if the dut is already
810 installed with the same version.
811 async: Whether the auto_update function is ran in the background.
812
813 Returns:
814 A tuple includes two elements:
815 a boolean variable represents whether the auto-update process is
816 successfully started.
817 an integer represents the background auto-update process id.
818 """
819 _check_base_args_for_auto_update(kwargs)
820
821 host_name = kwargs['host_name']
822 build_name = kwargs['build_name']
823 force_update = _parse_boolean_arg(kwargs, 'force_update')
824 full_update = _parse_boolean_arg(kwargs, 'full_update')
825 async = _parse_boolean_arg(kwargs, 'async')
826
827 if async:
828 path = os.path.dirname(os.path.abspath(__file__))
829 execute_file = os.path.join(path, 'cros_update.py')
830 args = (AUTO_UPDATE_CMD % (execute_file, host_name, build_name,
831 updater.static_dir))
832 if force_update:
833 args = ('%s --force_update' % args)
834
835 if full_update:
836 args = ('%s --full_update' % args)
837
xixuan2a0970a2016-08-10 12:12:44 -0700838 p = subprocess.Popen([args], shell=True, preexec_fn=os.setsid)
839 pid = os.getpgid(p.pid)
xixuan52c2fba2016-05-20 17:02:48 -0700840
841 # Pre-write status in the track_status_file before the first call of
842 # 'get_au_status' to make sure that the track_status_file exists.
xixuan2a0970a2016-08-10 12:12:44 -0700843 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -0700844 progress_tracker.WriteStatus('CrOS update is just started.')
845
xixuan2a0970a2016-08-10 12:12:44 -0700846 return json.dumps((True, pid))
xixuan52c2fba2016-05-20 17:02:48 -0700847 else:
848 cros_update_trigger = cros_update.CrOSUpdateTrigger(
849 host_name, build_name, updater.static_dir)
850 cros_update_trigger.TriggerAU()
851
852 @cherrypy.expose
853 def get_au_status(self, **kwargs):
854 """Check if the auto-update task is finished.
855
856 It handles 4 cases:
857 1. If an error exists in the track_status_file, delete the track file and
858 raise it.
859 2. If cros-update process is finished, delete the file and return the
860 success result.
861 3. If the process is not running, delete the track file and raise an error
862 about 'the process is terminated due to unknown reason'.
863 4. If the track_status_file does not exist, kill the process if it exists,
864 and raise the IOError.
865
866 Args:
867 kwargs:
868 host_name: the hostname of the DUT to auto-update.
869 pid: the background process id of cros-update.
870
871 Returns:
xixuan28d99072016-10-06 12:24:16 -0700872 A dict with three elements:
xixuan52c2fba2016-05-20 17:02:48 -0700873 a boolean variable represents whether the auto-update process is
874 finished.
875 a string represents the current auto-update process status.
876 For example, 'Transfer Devserver/Stateful Update Package'.
xixuan28d99072016-10-06 12:24:16 -0700877 a detailed error message paragraph if there exists an Auto-Update
878 error, in which the last line shows the main exception. Empty
879 string otherwise.
xixuan52c2fba2016-05-20 17:02:48 -0700880 """
881 if 'host_name' not in kwargs:
882 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
883
884 if 'pid' not in kwargs:
885 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
886
887 host_name = kwargs['host_name']
888 pid = kwargs['pid']
889 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
890
xixuan28d99072016-10-06 12:24:16 -0700891 result_dict = {'finished': False, 'status': '', 'detailed_error_msg': ''}
xixuan52c2fba2016-05-20 17:02:48 -0700892 try:
893 result = progress_tracker.ReadStatus()
894 if result.startswith(cros_update_progress.ERROR_TAG):
xixuan28d99072016-10-06 12:24:16 -0700895 result_dict['detailed_error_msg'] = result[len(
896 cros_update_progress.ERROR_TAG):]
897 return json.dumps(result_dict)
xixuan52c2fba2016-05-20 17:02:48 -0700898
899 if result == cros_update_progress.FINISHED:
xixuan28d99072016-10-06 12:24:16 -0700900 result_dict['finished'] = True
901 result_dict['status'] = result
902 return json.dumps(result_dict)
xixuan52c2fba2016-05-20 17:02:48 -0700903
904 if not cros_update_progress.IsProcessAlive(pid):
xixuan28d99072016-10-06 12:24:16 -0700905 result_dict['detailed_error_msg'] = (
906 'Cros_update process terminated midway due to unknown reason. '
907 'Last update status was %s' % result)
908 return json.dumps(result_dict)
xixuan52c2fba2016-05-20 17:02:48 -0700909
xixuan28d99072016-10-06 12:24:16 -0700910 result_dict['status'] = result
911 return json.dumps(result_dict)
xixuan52c2fba2016-05-20 17:02:48 -0700912 except IOError:
913 if pid:
xixuan2a0970a2016-08-10 12:12:44 -0700914 os.killpg(int(pid), signal.SIGKILL)
xixuan52c2fba2016-05-20 17:02:48 -0700915
916 raise
917
918 @cherrypy.expose
919 def handler_cleanup(self, **kwargs):
920 """Clean track status log for CrOS auto-update process.
921
922 Args:
923 kwargs:
924 host_name: the hostname of the DUT to auto-update.
925 pid: the background process id of cros-update.
926 """
927 if 'host_name' not in kwargs:
928 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
929
930 if 'pid' not in kwargs:
931 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
932
933 host_name = kwargs['host_name']
934 pid = kwargs['pid']
935 cros_update_progress.DelTrackStatusFile(host_name, pid)
936
937 @cherrypy.expose
938 def kill_au_proc(self, **kwargs):
939 """Kill CrOS auto-update process using given process id.
940
941 Args:
942 kwargs:
943 host_name: Kill all the CrOS auto-update process of this host.
944
945 Returns:
946 True if all processes are killed properly.
947 """
948 if 'host_name' not in kwargs:
949 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
950
951 host_name = kwargs['host_name']
952 file_filter = cros_update_progress.TRACK_LOG_FILE_PATH % (host_name, '*')
953 track_log_list = glob.glob(file_filter)
954 for log in track_log_list:
955 # The track log's full path is: path/host_name_pid.log
956 # Use splitext to remove file extension, then parse pid from the
957 # filename.
958 pid = os.path.splitext(os.path.basename(log))[0][len(host_name)+1:]
959 if cros_update_progress.IsProcessAlive(pid):
xixuan2a0970a2016-08-10 12:12:44 -0700960 os.killpg(int(pid), signal.SIGKILL)
xixuan52c2fba2016-05-20 17:02:48 -0700961
962 cros_update_progress.DelTrackStatusFile(host_name, pid)
xixuan1bbfaba2016-10-13 17:53:22 -0700963 cros_update_progress.DelExecuteLogFile(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -0700964
965 return 'True'
966
967 @cherrypy.expose
968 def collect_cros_au_log(self, **kwargs):
969 """Collect CrOS auto-update log.
970
971 Args:
972 kwargs:
973 host_name: the hostname of the DUT to auto-update.
974 pid: the background process id of cros-update.
975
976 Returns:
977 A string contains the whole content of the execute log file.
978 """
979 if 'host_name' not in kwargs:
980 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
981
982 if 'pid' not in kwargs:
983 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
984
985 host_name = kwargs['host_name']
986 pid = kwargs['pid']
xixuan1bbfaba2016-10-13 17:53:22 -0700987 au_log = cros_update_progress.ReadExecuteLogFile(host_name, pid)
988 cros_update_progress.DelExecuteLogFile(host_name, pid)
989 return au_log
990
xixuan52c2fba2016-05-20 17:02:48 -0700991
992 @cherrypy.expose
Dan Shi2f136862016-02-11 15:38:38 -0800993 def locate_file(self, **kwargs):
994 """Get the path to the given file name.
995
996 This method looks up the given file name inside specified build artifacts.
997 One use case is to help caller to locate an apk file inside a build
998 artifact. The location of the apk file could be different based on the
999 branch and target.
1000
1001 Args:
1002 file_name: Name of the file to look for.
1003 artifacts: A list of artifact names to search for the file.
1004
1005 Returns:
1006 Path to the file with the given name. It's relative to the folder for the
1007 build, e.g., DATA/priv-app/sl4a/sl4a.apk
Dan Shi2f136862016-02-11 15:38:38 -08001008 """
1009 dl, _ = _get_downloader_and_factory(kwargs)
1010 try:
1011 file_name = kwargs['file_name'].lower()
1012 artifacts = kwargs['artifacts']
1013 except KeyError:
1014 raise DevServerError('`file_name` and `artifacts` are required to search '
1015 'for a file in build artifacts.')
1016 build_path = dl.GetBuildDir()
1017 for artifact in artifacts:
1018 # Get the unzipped folder of the artifact. If it's not defined in
1019 # ARTIFACT_UNZIP_FOLDER_MAP, assume the files are unzipped to the build
1020 # directory directly.
1021 folder = artifact_info.ARTIFACT_UNZIP_FOLDER_MAP.get(artifact, '')
1022 artifact_path = os.path.join(build_path, folder)
1023 for root, _, filenames in os.walk(artifact_path):
1024 if file_name in set([f.lower() for f in filenames]):
1025 return os.path.relpath(os.path.join(root, file_name), build_path)
1026 raise DevServerError('File `%s` can not be found in artifacts: %s' %
1027 (file_name, artifacts))
1028
1029 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -08001030 def setup_telemetry(self, **kwargs):
1031 """Extracts and sets up telemetry
1032
1033 This method goes through the telemetry deps packages, and stages them on
1034 the devserver to be used by the drones and the telemetry tests.
1035
1036 Args:
1037 archive_url: Google Storage URL for the build.
1038
1039 Returns:
1040 Path to the source folder for the telemetry codebase once it is staged.
1041 """
Gabe Black3b567202015-09-23 14:07:59 -07001042 dl = _get_downloader(kwargs)
Simran Basi4baad082013-02-14 13:39:18 -08001043
Gabe Black3b567202015-09-23 14:07:59 -07001044 build_path = dl.GetBuildDir()
Simran Basi4baad082013-02-14 13:39:18 -08001045 deps_path = os.path.join(build_path, 'autotest/packages')
1046 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
1047 src_folder = os.path.join(telemetry_path, 'src')
1048
1049 with self._telemetry_lock_dict.lock(telemetry_path):
1050 if os.path.exists(src_folder):
1051 # Telemetry is already fully stage return
1052 return src_folder
1053
1054 common_util.MkDirP(telemetry_path)
1055
1056 # Copy over the required deps tar balls to the telemetry directory.
1057 for dep in TELEMETRY_DEPS:
1058 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -07001059 if not os.path.exists(dep_path):
1060 # This dep does not exist (could be new), do not extract it.
1061 continue
Simran Basi4baad082013-02-14 13:39:18 -08001062 try:
1063 common_util.ExtractTarball(dep_path, telemetry_path)
1064 except common_util.CommonUtilError as e:
1065 shutil.rmtree(telemetry_path)
1066 raise DevServerError(str(e))
1067
1068 # By default all the tarballs extract to test_src but some parts of
1069 # the telemetry code specifically hardcoded to exist inside of 'src'.
1070 test_src = os.path.join(telemetry_path, 'test_src')
1071 try:
1072 shutil.move(test_src, src_folder)
1073 except shutil.Error:
1074 # This can occur if src_folder already exists. Remove and retry move.
1075 shutil.rmtree(src_folder)
Gabe Black3b567202015-09-23 14:07:59 -07001076 raise DevServerError(
1077 'Failure in telemetry setup for build %s. Appears that the '
1078 'test_src to src move failed.' % dl.GetBuild())
Simran Basi4baad082013-02-14 13:39:18 -08001079
1080 return src_folder
1081
1082 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -08001083 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -07001084 """Symbolicates a minidump using pre-downloaded symbols, returns it.
1085
1086 Callers will need to POST to this URL with a body of MIME-type
1087 "multipart/form-data".
1088 The body should include a single argument, 'minidump', containing the
1089 binary-formatted minidump to symbolicate.
1090
Chris Masone816e38c2012-05-02 12:22:36 -07001091 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -08001092 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -07001093 minidump: The binary minidump file to symbolicate.
1094 """
Chris Sosa76e44b92013-01-31 12:11:38 -08001095 # Ensure the symbols have been staged.
Dan Shif08fe492016-10-04 14:39:25 -07001096 # Try debug.tar.xz first, then debug.tgz
1097 for artifact in (artifact_info.SYMBOLS_ONLY, artifact_info.SYMBOLS):
1098 kwargs['artifacts'] = artifact
1099 dl = _get_downloader(kwargs)
1100
1101 try:
1102 if self.stage(**kwargs) == 'Success':
1103 break
1104 except build_artifact.ArtifactDownloadError:
1105 continue
1106 else:
Gabe Black3b567202015-09-23 14:07:59 -07001107 raise DevServerError('Failed to stage symbols for %s' %
1108 dl.DescribeSource())
Chris Sosa76e44b92013-01-31 12:11:38 -08001109
Chris Masone816e38c2012-05-02 12:22:36 -07001110 to_return = ''
1111 with tempfile.NamedTemporaryFile() as local:
1112 while True:
1113 data = minidump.file.read(8192)
1114 if not data:
1115 break
1116 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -08001117
Chris Masone816e38c2012-05-02 12:22:36 -07001118 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -08001119
Gabe Black3b567202015-09-23 14:07:59 -07001120 symbols_directory = os.path.join(dl.GetBuildDir(), 'debug', 'breakpad')
Chris Sosa76e44b92013-01-31 12:11:38 -08001121
1122 stackwalk = subprocess.Popen(
1123 ['minidump_stackwalk', local.name, symbols_directory],
1124 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1125
Chris Masone816e38c2012-05-02 12:22:36 -07001126 to_return, error_text = stackwalk.communicate()
1127 if stackwalk.returncode != 0:
1128 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
1129 error_text, stackwalk.returncode))
1130
1131 return to_return
1132
1133 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -08001134 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 15:31:36 -04001135 """Return a string representing the latest build for a given target.
1136
1137 Args:
1138 target: The build target, typically a combination of the board and the
1139 type of build e.g. x86-mario-release.
1140 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
1141 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-07 18:21:26 -08001142
Scott Zawalski16954532012-03-20 15:31:36 -04001143 Returns:
1144 A string representation of the latest build if one exists, i.e.
1145 R19-1993.0.0-a1-b1480.
1146 An empty string if no latest could be found.
1147 """
Don Garrettf84631a2014-01-07 18:21:26 -08001148 if not kwargs:
Scott Zawalski16954532012-03-20 15:31:36 -04001149 return _PrintDocStringAsHTML(self.latestbuild)
1150
Don Garrettf84631a2014-01-07 18:21:26 -08001151 if 'target' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -07001152 raise common_util.DevServerHTTPError(500, 'Error: target= is required!')
Dan Shi61305df2015-10-26 16:52:35 -07001153
1154 if _is_android_build_request(kwargs):
1155 branch = kwargs.get('branch', None)
1156 target = kwargs.get('target', None)
1157 if not target or not branch:
1158 raise DevServerError(
xixuan52c2fba2016-05-20 17:02:48 -07001159 'Both target and branch must be specified to query for the latest '
1160 'Android build.')
Dan Shi61305df2015-10-26 16:52:35 -07001161 return android_build.BuildAccessor.GetLatestBuildID(target, branch)
1162
Scott Zawalski16954532012-03-20 15:31:36 -04001163 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001164 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-07 18:21:26 -08001165 updater.static_dir, kwargs['target'],
1166 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -07001167 except common_util.CommonUtilError as errmsg:
Chris Sosa4b951602014-04-09 20:26:07 -07001168 raise common_util.DevServerHTTPError(500, str(errmsg))
Scott Zawalski16954532012-03-20 15:31:36 -04001169
1170 @cherrypy.expose
xixuan7efd0002016-04-14 15:34:01 -07001171 def list_suite_controls(self, **kwargs):
1172 """Return a list of contents of all known control files.
1173
1174 Example URL:
1175 To List all control files' content:
1176 http://dev-server/list_suite_controls?suite_name=bvt&
1177 build=daisy_spring-release/R29-4279.0.0
1178
1179 Args:
1180 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
1181 suite_name: List the control files belonging to that suite.
1182
1183 Returns:
Dan Shia1cd6522016-04-18 16:07:21 -07001184 A dictionary of all control files's path to its content for given suite.
xixuan7efd0002016-04-14 15:34:01 -07001185 """
1186 if not kwargs:
1187 return _PrintDocStringAsHTML(self.controlfiles)
1188
1189 if 'build' not in kwargs:
1190 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
1191
1192 if 'suite_name' not in kwargs:
Dan Shia1cd6522016-04-18 16:07:21 -07001193 raise common_util.DevServerHTTPError(500,
1194 'Error: suite_name= is required!')
xixuan7efd0002016-04-14 15:34:01 -07001195
1196 control_file_list = [
1197 line.rstrip() for line in common_util.GetControlFileListForSuite(
1198 updater.static_dir, kwargs['build'],
1199 kwargs['suite_name']).splitlines()]
1200
Dan Shia1cd6522016-04-18 16:07:21 -07001201 control_file_content_dict = {}
xixuan7efd0002016-04-14 15:34:01 -07001202 for control_path in control_file_list:
Dan Shia1cd6522016-04-18 16:07:21 -07001203 control_file_content_dict[control_path] = (common_util.GetControlFile(
xixuan7efd0002016-04-14 15:34:01 -07001204 updater.static_dir, kwargs['build'], control_path))
1205
Dan Shia1cd6522016-04-18 16:07:21 -07001206 return json.dumps(control_file_content_dict)
xixuan7efd0002016-04-14 15:34:01 -07001207
1208 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -08001209 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 17:17:28 -05001210 """Return a control file or a list of all known control files.
1211
1212 Example URL:
1213 To List all control files:
beepsbd337242013-07-09 22:44:06 -07001214 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
1215 To List all control files for, say, the bvt suite:
1216 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -05001217 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -05001218 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 -05001219
1220 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -05001221 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -05001222 control_path: If you want the contents of a control file set this
1223 to the path. E.g. client/site_tests/sleeptest/control
1224 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -07001225 suite_name: If control_path is not specified but a suite_name is
1226 specified, list the control files belonging to that suite instead of
1227 all control files. The empty string for suite_name will list all control
1228 files for the build.
Don Garrettf84631a2014-01-07 18:21:26 -08001229
Scott Zawalski4647ce62012-01-03 17:17:28 -05001230 Returns:
1231 Contents of a control file if control_path is provided.
1232 A list of control files if no control_path is provided.
1233 """
Don Garrettf84631a2014-01-07 18:21:26 -08001234 if not kwargs:
Scott Zawalski4647ce62012-01-03 17:17:28 -05001235 return _PrintDocStringAsHTML(self.controlfiles)
1236
Don Garrettf84631a2014-01-07 18:21:26 -08001237 if 'build' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -07001238 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -05001239
Don Garrettf84631a2014-01-07 18:21:26 -08001240 if 'control_path' not in kwargs:
1241 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-09 22:44:06 -07001242 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-07 18:21:26 -08001243 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-09 22:44:06 -07001244 else:
1245 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-07 18:21:26 -08001246 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -05001247 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001248 return common_util.GetControlFile(
Don Garrettf84631a2014-01-07 18:21:26 -08001249 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -08001250
1251 @cherrypy.expose
Simran Basi99e63c02014-05-20 10:39:52 -07001252 def xbuddy_translate(self, *args, **kwargs):
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001253 """Translates an xBuddy path to a real path to artifact if it exists.
1254
1255 Args:
Simran Basi99e63c02014-05-20 10:39:52 -07001256 args: An xbuddy path in the form of {local|remote}/build_id/artifact.
1257 Local searches the devserver's static directory. Remote searches a
1258 Google Storage image archive.
1259
1260 Kwargs:
1261 image_dir: Google Storage image archive to search in if requesting a
1262 remote artifact. If none uses the default bucket.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001263
1264 Returns:
Simran Basi99e63c02014-05-20 10:39:52 -07001265 String in the format of build_id/artifact as stored on the local server
1266 or in Google Storage.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001267 """
Simran Basi99e63c02014-05-20 10:39:52 -07001268 build_id, filename = self._xbuddy.Translate(
Gabe Black3b567202015-09-23 14:07:59 -07001269 args, image_dir=kwargs.get('image_dir'))
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001270 response = os.path.join(build_id, filename)
1271 _Log('Path translation requested, returning: %s', response)
1272 return response
1273
1274 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -07001275 def xbuddy(self, *args, **kwargs):
1276 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -07001277
1278 Args:
joycheneaf4cfc2013-07-02 08:38:57 -07001279 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -07001280 components of the path. The path can be understood as
1281 "{local|remote}/build_id/artifact" where build_id is composed of
1282 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -07001283
joychen121fc9b2013-08-02 14:30:30 -07001284 The first path element is optional, and can be "remote" or "local"
1285 If local (the default), devserver will not attempt to access Google
1286 Storage, and will only search the static directory for the files.
1287 If remote, devserver will try to obtain the artifact off GS if it's
1288 not found locally.
1289 The board is the familiar board name, optionally suffixed.
1290 The version can be the google storage version number, and may also be
1291 any of a number of xBuddy defined version aliases that will be
1292 translated into the latest built image that fits the description.
1293 Defaults to latest.
1294 The artifact is one of a number of image or artifact aliases used by
1295 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -07001296
1297 Kwargs:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001298 for_update: {true|false}
1299 if true, pregenerates the update payloads for the image,
1300 and returns the update uri to pass to the
1301 update_engine_client.
joychen3cb228e2013-06-12 12:13:13 -07001302 return_dir: {true|false}
1303 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001304 relative_path: {true|false}
1305 if set to true, returns the relative path to the payload
1306 directory from static_dir.
joychen3cb228e2013-06-12 12:13:13 -07001307 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -07001308 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -07001309 or
joycheneaf4cfc2013-07-02 08:38:57 -07001310 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -07001311
1312 Returns:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001313 If |for_update|, returns a redirect to the image or update file
1314 on the devserver. E.g.,
1315 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
1316 chromium-test-image.bin
1317 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
1318 http://host:port/static/x86-generic-release/R26-4000.0.0/
1319 If |relative_path| is true, return a relative path the folder where the
1320 payloads are. E.g.,
1321 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 12:13:13 -07001322 """
Chris Sosa75490802013-09-30 17:21:45 -07001323 boolean_string = kwargs.get('for_update')
1324 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001325 boolean_string = kwargs.get('return_dir')
1326 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
1327 boolean_string = kwargs.get('relative_path')
1328 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -07001329
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001330 if return_dir and relative_path:
Chris Sosa4b951602014-04-09 20:26:07 -07001331 raise common_util.DevServerHTTPError(
1332 500, 'Cannot specify both return_dir and relative_path')
Chris Sosa75490802013-09-30 17:21:45 -07001333
1334 # For updates, we optimize downloading of test images.
1335 file_name = None
1336 build_id = None
1337 if for_update:
1338 try:
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001339 build_id = self._xbuddy.StageTestArtifactsForUpdate(args)
Chris Sosa75490802013-09-30 17:21:45 -07001340 except build_artifact.ArtifactDownloadError:
1341 build_id = None
1342
1343 if not build_id:
1344 build_id, file_name = self._xbuddy.Get(args)
1345
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001346 if for_update:
1347 _Log('Payload generation triggered by request')
1348 # Forces payload to be in cache and symlinked into build_id dir.
Chris Sosa75490802013-09-30 17:21:45 -07001349 updater.GetUpdateForLabel(autoupdate.FORCED_UPDATE, build_id,
1350 image_name=file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001351
1352 response = None
1353 if return_dir:
1354 response = os.path.join(cherrypy.request.base, 'static', build_id)
1355 _Log('Directory requested, returning: %s', response)
1356 elif relative_path:
1357 response = build_id
1358 _Log('Relative path requested, returning: %s', response)
1359 elif for_update:
1360 response = os.path.join(cherrypy.request.base, 'update', build_id)
1361 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 12:13:13 -07001362 else:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001363 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 14:30:30 -07001364 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001365 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 14:30:30 -07001366 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -07001367
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001368 return response
1369
joychen3cb228e2013-06-12 12:13:13 -07001370 @cherrypy.expose
1371 def xbuddy_list(self):
1372 """Lists the currently available images & time since last access.
1373
Gilad Arnold452fd272014-02-04 11:09:28 -08001374 Returns:
1375 A string representation of a list of tuples [(build_id, time since last
1376 access),...]
joychen3cb228e2013-06-12 12:13:13 -07001377 """
1378 return self._xbuddy.List()
1379
1380 @cherrypy.expose
1381 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 11:09:28 -08001382 """Returns the number of images cached by xBuddy."""
joychen3cb228e2013-06-12 12:13:13 -07001383 return self._xbuddy.Capacity()
1384
1385 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001386 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001387 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001388 return ('Welcome to the Dev Server!<br>\n'
1389 '<br>\n'
1390 'Here are the available methods, click for documentation:<br>\n'
1391 '<br>\n'
1392 '%s' %
1393 '<br>\n'.join(
1394 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001395 for name in _FindExposedMethods(
1396 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001397
1398 @cherrypy.expose
1399 def doc(self, *args):
1400 """Shows the documentation for available methods / URLs.
1401
1402 Example:
1403 http://myhost/doc/update
1404 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001405 name = '/'.join(args)
1406 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001407 if not method:
1408 raise DevServerError("No exposed method named `%s'" % name)
1409 if not method.__doc__:
1410 raise DevServerError("No documentation for exposed method `%s'" % name)
1411 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -07001412
Dale Curtisc9aaf3a2011-08-09 15:47:40 -07001413 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001414 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001415 """Handles an update check from a Chrome OS client.
1416
1417 The HTTP request should contain the standard Omaha-style XML blob. The URL
1418 line may contain an additional intermediate path to the update payload.
1419
joychen121fc9b2013-08-02 14:30:30 -07001420 This request can be handled in one of 4 ways, depending on the devsever
1421 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -07001422
joychen121fc9b2013-08-02 14:30:30 -07001423 1. No intermediate path
1424 If no intermediate path is given, the default behavior is to generate an
1425 update payload from the latest test image locally built for the board
1426 specified in the xml. Devserver serves the generated payload.
1427
1428 2. Path explicitly invokes XBuddy
1429 If there is a path given, it can explicitly invoke xbuddy by prefixing it
1430 with 'xbuddy'. This path is then used to acquire an image binary for the
1431 devserver to generate an update payload from. Devserver then serves this
1432 payload.
1433
1434 3. Path is left for the devserver to interpret.
1435 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
1436 to generate a payload from the test image in that directory and serve it.
1437
1438 4. The devserver is in a 'forced' mode. TO BE DEPRECATED
1439 This comes from the usage of --forced_payload or --image when starting the
1440 devserver. No matter what path (or no path) gets passed in, devserver will
1441 serve the update payload (--forced_payload) or generate an update payload
1442 from the image (--image).
1443
1444 Examples:
1445 1. No intermediate path
1446 update_engine_client --omaha_url=http://myhost/update
1447 This generates an update payload from the latest test image locally built
1448 for the board specified in the xml.
1449
1450 2. Explicitly invoke xbuddy
1451 update_engine_client --omaha_url=
1452 http://myhost/update/xbuddy/remote/board/version/dev
1453 This would go to GS to download the dev image for the board, from which
1454 the devserver would generate a payload to serve.
1455
1456 3. Give a path for devserver to interpret
1457 update_engine_client --omaha_url=http://myhost/update/some/random/path
1458 This would attempt, in order to:
1459 a) Generate an update from a test image binary if found in
1460 static_dir/some/random/path.
1461 b) Serve an update payload found in static_dir/some/random/path.
1462 c) Hope that some/random/path takes the form "board/version" and
1463 and attempt to download an update payload for that board/version
1464 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001465 """
joychen121fc9b2013-08-02 14:30:30 -07001466 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -08001467 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -07001468 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-11 19:49:01 -07001469
joychen121fc9b2013-08-02 14:30:30 -07001470 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001471
Dan Shiafd0e492015-05-27 14:23:51 -07001472 @require_psutil()
1473 def _get_io_stats(self):
1474 """Get the IO stats as a dictionary.
1475
Gabe Black3b567202015-09-23 14:07:59 -07001476 Returns:
1477 A dictionary of IO stats collected by psutil.
Dan Shiafd0e492015-05-27 14:23:51 -07001478 """
1479 return {'disk_read_bytes_per_second': self.disk_read_bytes_per_sec,
1480 'disk_write_bytes_per_second': self.disk_write_bytes_per_sec,
1481 'disk_total_bytes_per_second': (self.disk_read_bytes_per_sec +
1482 self.disk_write_bytes_per_sec),
1483 'network_sent_bytes_per_second': self.network_sent_bytes_per_sec,
1484 'network_recv_bytes_per_second': self.network_recv_bytes_per_sec,
1485 'network_total_bytes_per_second': (self.network_sent_bytes_per_sec +
1486 self.network_recv_bytes_per_sec),
1487 'cpu_percent': psutil.cpu_percent(),}
1488
Dan Shi7247f9c2016-06-01 09:19:09 -07001489
1490 def _get_process_count(self, process_cmd_pattern):
1491 """Get the count of processes that match the given command pattern.
1492
1493 Args:
1494 process_cmd_pattern: The regex pattern of process command to match.
1495
1496 Returns:
1497 The count of processes that match the given command pattern.
1498 """
1499 try:
1500 return int(subprocess.check_output(
1501 'pgrep -fc "%s"' % process_cmd_pattern, shell=True))
1502 except subprocess.CalledProcessError:
1503 return 0
1504
1505
Dan Shif5ce2de2013-04-25 16:06:32 -07001506 @cherrypy.expose
1507 def check_health(self):
1508 """Collect the health status of devserver to see if it's ready for staging.
1509
Gilad Arnold452fd272014-02-04 11:09:28 -08001510 Returns:
1511 A JSON dictionary containing all or some of the following fields:
1512 free_disk (int): free disk space in GB
1513 staging_thread_count (int): number of devserver threads currently staging
1514 an image
Dan Shi7247f9c2016-06-01 09:19:09 -07001515 apache_client_count (int): count of Apache processes.
1516 telemetry_test_count (int): count of telemetry tests.
1517 gsutil_count (int): count of gsutil processes.
Dan Shif5ce2de2013-04-25 16:06:32 -07001518 """
1519 # Get free disk space.
1520 stat = os.statvfs(updater.static_dir)
1521 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
Dan Shi7247f9c2016-06-01 09:19:09 -07001522 apache_client_count = self._get_process_count('apache')
1523 telemetry_test_count = self._get_process_count('python.*telemetry')
1524 gsutil_count = self._get_process_count('gsutil')
Dan Shif5ce2de2013-04-25 16:06:32 -07001525
Dan Shiafd0e492015-05-27 14:23:51 -07001526 health_data = {
Dan Shif5ce2de2013-04-25 16:06:32 -07001527 'free_disk': free_disk,
Dan Shid76e6bb2016-01-28 22:28:51 -08001528 'staging_thread_count': DevServerRoot._staging_thread_count,
1529 'apache_client_count': apache_client_count,
Dan Shi7247f9c2016-06-01 09:19:09 -07001530 'telemetry_test_count': telemetry_test_count,
1531 'gsutil_count': gsutil_count}
Dan Shiafd0e492015-05-27 14:23:51 -07001532 health_data.update(self._get_io_stats() or {})
1533
1534 return json.dumps(health_data)
Dan Shif5ce2de2013-04-25 16:06:32 -07001535
1536
Chris Sosadbc20082012-12-10 13:39:11 -08001537def _CleanCache(cache_dir, wipe):
1538 """Wipes any excess cached items in the cache_dir.
1539
1540 Args:
1541 cache_dir: the directory we are wiping from.
1542 wipe: If True, wipe all the contents -- not just the excess.
1543 """
1544 if wipe:
1545 # Clear the cache and exit on error.
1546 cmd = 'rm -rf %s/*' % cache_dir
1547 if os.system(cmd) != 0:
1548 _Log('Failed to clear the cache with %s' % cmd)
1549 sys.exit(1)
1550 else:
1551 # Clear all but the last N cached updates
1552 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
1553 (cache_dir, CACHED_ENTRIES))
1554 if os.system(cmd) != 0:
1555 _Log('Failed to clean up old delta cache files with %s' % cmd)
1556 sys.exit(1)
1557
1558
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001559def _AddTestingOptions(parser):
1560 group = optparse.OptionGroup(
1561 parser, 'Advanced Testing Options', 'These are used by test scripts and '
1562 'developers writing integration tests utilizing the devserver. They are '
1563 'not intended to be really used outside the scope of someone '
1564 'knowledgable about the test.')
1565 group.add_option('--exit',
1566 action='store_true',
1567 help='do not start the server (yet pregenerate/clear cache)')
1568 group.add_option('--host_log',
1569 action='store_true', default=False,
1570 help='record history of host update events (/api/hostlog)')
1571 group.add_option('--max_updates',
Gabe Black3b567202015-09-23 14:07:59 -07001572 metavar='NUM', default=-1, type='int',
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001573 help='maximum number of update checks handled positively '
1574 '(default: unlimited)')
1575 group.add_option('--private_key',
1576 metavar='PATH', default=None,
1577 help='path to the private key in pem format. If this is set '
1578 'the devserver will generate update payloads that are '
1579 'signed with this key.')
David Zeuthen52ccd012013-10-31 12:58:26 -07001580 group.add_option('--private_key_for_metadata_hash_signature',
1581 metavar='PATH', default=None,
1582 help='path to the private key in pem format. If this is set '
1583 'the devserver will sign the metadata hash with the given '
1584 'key and transmit in the Omaha-style XML response.')
1585 group.add_option('--public_key',
1586 metavar='PATH', default=None,
1587 help='path to the public key in pem format. If this is set '
1588 'the devserver will transmit a base64 encoded version of '
1589 'the content in the Omaha-style XML response.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001590 group.add_option('--proxy_port',
1591 metavar='PORT', default=None, type='int',
1592 help='port to have the client connect to -- basically the '
1593 'devserver lies to the update to tell it to get the payload '
1594 'from a different port that will proxy the request back to '
1595 'the devserver. The proxy must be managed outside the '
1596 'devserver.')
1597 group.add_option('--remote_payload',
1598 action='store_true', default=False,
Chris Sosa4b951602014-04-09 20:26:07 -07001599 help='Payload is being served from a remote machine. With '
1600 'this setting enabled, this devserver instance serves as '
1601 'just an Omaha server instance. In this mode, the '
1602 'devserver enforces a few extra components of the Omaha '
Chris Sosafc715442014-04-09 20:45:23 -07001603 'protocol, such as hardware class, being sent.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001604 group.add_option('-u', '--urlbase',
1605 metavar='URL',
Gabe Black3b567202015-09-23 14:07:59 -07001606 help='base URL for update images, other than the '
1607 'devserver. Use in conjunction with remote_payload.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001608 parser.add_option_group(group)
1609
1610
1611def _AddUpdateOptions(parser):
1612 group = optparse.OptionGroup(
1613 parser, 'Autoupdate Options', 'These options can be used to change '
1614 'how the devserver either generates or serve update payloads. Please '
1615 'note that all of these option affect how a payload is generated and so '
1616 'do not work in archive-only mode.')
1617 group.add_option('--board',
1618 help='By default the devserver will create an update '
1619 'payload from the latest image built for the board '
1620 'a device that is requesting an update has. When we '
1621 'pre-generate an update (see below) and we do not specify '
1622 'another update_type option like image or payload, the '
1623 'devserver needs to know the board to generate the latest '
1624 'image for. This is that board.')
1625 group.add_option('--critical_update',
1626 action='store_true', default=False,
1627 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001628 group.add_option('--image',
1629 metavar='FILE',
1630 help='Generate and serve an update using this image to any '
1631 'device that requests an update.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001632 group.add_option('--payload',
1633 metavar='PATH',
1634 help='use the update payload from specified directory '
1635 '(update.gz).')
1636 group.add_option('-p', '--pregenerate_update',
1637 action='store_true', default=False,
1638 help='pre-generate the update payload before accepting '
1639 'update requests. Useful to help debug payload generation '
1640 'issues quickly. Also if an update payload will take a '
1641 'long time to generate, a client may timeout if you do not'
1642 'pregenerate the update.')
1643 group.add_option('--src_image',
1644 metavar='PATH', default='',
1645 help='If specified, delta updates will be generated using '
1646 'this image as the source image. Delta updates are when '
1647 'you are updating from a "source image" to a another '
1648 'image.')
1649 parser.add_option_group(group)
1650
1651
1652def _AddProductionOptions(parser):
1653 group = optparse.OptionGroup(
1654 parser, 'Advanced Server Options', 'These options can be used to changed '
1655 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001656 group.add_option('--clear_cache',
1657 action='store_true', default=False,
1658 help='At startup, removes all cached entries from the'
1659 'devserver\'s cache.')
1660 group.add_option('--logfile',
1661 metavar='PATH',
1662 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -07001663 group.add_option('--pidfile',
1664 metavar='PATH',
1665 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 11:04:13 -08001666 group.add_option('--portfile',
1667 metavar='PATH',
1668 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001669 group.add_option('--production',
1670 action='store_true', default=False,
1671 help='have the devserver use production values when '
1672 'starting up. This includes using more threads and '
1673 'performing less logging.')
1674 parser.add_option_group(group)
1675
1676
Paul Hobbsef4e0702016-06-27 17:01:42 -07001677def MakeLogHandler(logfile):
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001678 """Create a LogHandler instance used to log all messages."""
1679 hdlr_cls = handlers.TimedRotatingFileHandler
1680 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
1681 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -07001682 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001683 return hdlr
1684
1685
Chris Sosacde6bf42012-05-31 18:36:39 -07001686def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001687 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -08001688 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -07001689
1690 # get directory that the devserver is run from
1691 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -07001692 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -07001693 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001694 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -07001695 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -07001696 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001697 parser.add_option('--port',
1698 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 13:13:28 -08001699 help=('port for the dev server to use; if zero, binds to '
1700 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001701 parser.add_option('-t', '--test_image',
1702 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -07001703 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -07001704 parser.add_option('-x', '--xbuddy_manage_builds',
1705 action='store_true',
1706 default=False,
1707 help='If set, allow xbuddy to manage images in'
1708 'build/images.')
Dan Shi72b16132015-10-08 12:10:33 -07001709 parser.add_option('-a', '--android_build_credential',
1710 default=None,
1711 help='Path to a json file which contains the credential '
1712 'needed to access Android builds.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001713 _AddProductionOptions(parser)
1714 _AddUpdateOptions(parser)
1715 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -07001716 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001717
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001718 # Handle options that must be set globally in cherrypy. Do this
1719 # work up front, because calls to _Log() below depend on this
1720 # initialization.
1721 if options.production:
1722 cherrypy.config.update({'environment': 'production'})
1723 if not options.logfile:
1724 cherrypy.config.update({'log.screen': True})
1725 else:
1726 cherrypy.config.update({'log.error_file': '',
1727 'log.access_file': ''})
Paul Hobbsef4e0702016-06-27 17:01:42 -07001728 hdlr = MakeLogHandler(options.logfile)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001729 # Pylint can't seem to process these two calls properly
1730 # pylint: disable=E1101
1731 cherrypy.log.access_log.addHandler(hdlr)
1732 cherrypy.log.error_log.addHandler(hdlr)
1733 # pylint: enable=E1101
1734
joychened64b222013-06-21 16:39:34 -07001735 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001736 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001737
joychened64b222013-06-21 16:39:34 -07001738 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001739 # If our devserver is only supposed to serve payloads, we shouldn't be
1740 # mucking with the cache at all. If the devserver hadn't previously
1741 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001742 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001743 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001744 else:
1745 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001746
Chris Sosadbc20082012-12-10 13:39:11 -08001747 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 16:39:34 -07001748 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001749
joychen121fc9b2013-08-02 14:30:30 -07001750 _xbuddy = xbuddy.XBuddy(options.xbuddy_manage_builds,
1751 options.board,
joychen121fc9b2013-08-02 14:30:30 -07001752 static_dir=options.static_dir)
Chris Sosa75490802013-09-30 17:21:45 -07001753 if options.clear_cache and options.xbuddy_manage_builds:
1754 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 14:30:30 -07001755
Chris Sosa6a3697f2013-01-29 16:44:43 -08001756 # We allow global use here to share with cherrypy classes.
1757 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001758 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001759 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001760 _xbuddy,
joychened64b222013-06-21 16:39:34 -07001761 static_dir=options.static_dir,
Andrew de los Reyes52620802010-04-12 13:40:07 -07001762 urlbase=options.urlbase,
Chris Sosa5d342a22010-09-28 16:54:41 -07001763 forced_image=options.image,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001764 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001765 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-28 23:42:37 -07001766 src_image=options.src_image,
Chris Sosa08d55a22011-01-19 16:08:02 -08001767 board=options.board,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001768 copy_to_static_root=not options.exit,
1769 private_key=options.private_key,
Gabe Black3b567202015-09-23 14:07:59 -07001770 private_key_for_metadata_hash_signature=(
1771 options.private_key_for_metadata_hash_signature),
David Zeuthen52ccd012013-10-31 12:58:26 -07001772 public_key=options.public_key,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001773 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001774 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001775 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001776 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001777 )
Chris Sosa7c931362010-10-11 19:49:01 -07001778
Chris Sosa6a3697f2013-01-29 16:44:43 -08001779 if options.pregenerate_update:
1780 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 15:46:22 -07001781
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001782 if options.exit:
1783 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001784
joychen3cb228e2013-06-12 12:13:13 -07001785 dev_server = DevServerRoot(_xbuddy)
1786
Gilad Arnold11fbef42014-02-10 11:04:13 -08001787 # Patch CherryPy to support binding to any available port (--port=0).
1788 cherrypy_ext.ZeroPortPatcher.DoPatch(cherrypy)
1789
Chris Sosa855b8932013-08-21 13:24:55 -07001790 if options.pidfile:
1791 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1792
Gilad Arnold11fbef42014-02-10 11:04:13 -08001793 if options.portfile:
1794 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1795
Dan Shiafd5c6c2016-01-07 10:27:03 -08001796 if (options.android_build_credential and
1797 os.path.exists(options.android_build_credential)):
1798 try:
1799 with open(options.android_build_credential) as f:
1800 android_build.BuildAccessor.credential_info = json.load(f)
1801 except ValueError as e:
1802 _Log('Failed to load the android build credential: %s. Error: %s.' %
1803 (options.android_build_credential, e))
joychen3cb228e2013-06-12 12:13:13 -07001804 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001805
1806
1807if __name__ == '__main__':
1808 main()