blob: 7aa81c62f2fe387a24ed3354b20e31cccff61f2a [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
xixuan3d48bff2017-01-30 19:00:09 -0800118# Log rotation parameters. These settings correspond to twice a day once
119# devserver is started, with about two weeks (28 backup files) of old logs
120# kept for backup.
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700121#
xixuan3d48bff2017-01-30 19:00:09 -0800122# For more, see the documentation in standard python library for
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700123# logging.handlers.TimedRotatingFileHandler
xixuan3d48bff2017-01-30 19:00:09 -0800124_LOG_ROTATION_TIME = 'H'
125_LOG_ROTATION_INTERVAL = 12 # hours
126_LOG_ROTATION_BACKUP = 28 # backup counts
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700127
Dan Shiafd0e492015-05-27 14:23:51 -0700128# Number of seconds between the collection of disk and network IO counters.
129STATS_INTERVAL = 10.0
Frank Farzan40160872011-12-12 18:39:18 -0800130
xixuan52c2fba2016-05-20 17:02:48 -0700131# Auto-update parameters
132
133# Error msg for missing key in CrOS auto-update.
134KEY_ERROR_MSG = 'Key Error in cmd %s: %s= is required'
135
136# Command of running auto-update.
137AUTO_UPDATE_CMD = '/usr/bin/python -u %s -d %s -b %s --static_dir %s'
138
139
Chris Sosa9164ca32012-03-28 11:04:50 -0700140class DevServerError(Exception):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700141 """Exception class used by this module."""
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700142
143
Dan Shiafd0e492015-05-27 14:23:51 -0700144def require_psutil():
Gabe Black3b567202015-09-23 14:07:59 -0700145 """Decorator for functions require psutil to run."""
Dan Shiafd0e492015-05-27 14:23:51 -0700146 def deco_require_psutil(func):
147 """Wrapper of the decorator function.
148
Gabe Black3b567202015-09-23 14:07:59 -0700149 Args:
150 func: function to be called.
Dan Shiafd0e492015-05-27 14:23:51 -0700151 """
152 def func_require_psutil(*args, **kwargs):
153 """Decorator for functions require psutil to run.
154
155 If psutil is not installed, skip calling the function.
156
Gabe Black3b567202015-09-23 14:07:59 -0700157 Args:
158 *args: arguments for function to be called.
159 **kwargs: keyword arguments for function to be called.
Dan Shiafd0e492015-05-27 14:23:51 -0700160 """
161 if psutil:
162 return func(*args, **kwargs)
163 else:
164 _Log('Python module psutil is not installed. Function call %s is '
165 'skipped.' % func)
166 return func_require_psutil
167 return deco_require_psutil
168
169
Gabe Black3b567202015-09-23 14:07:59 -0700170def _canonicalize_archive_url(archive_url):
171 """Canonicalizes archive_url strings.
172
173 Raises:
174 DevserverError: if archive_url is not set.
175 """
176 if archive_url:
177 if not archive_url.startswith('gs://'):
178 raise DevServerError("Archive URL isn't from Google Storage (%s) ." %
179 archive_url)
180
181 return archive_url.rstrip('/')
182 else:
183 raise DevServerError("Must specify an archive_url in the request")
184
185
186def _canonicalize_local_path(local_path):
187 """Canonicalizes |local_path| strings.
188
189 Raises:
190 DevserverError: if |local_path| is not set.
191 """
192 # Restrict staging of local content to only files within the static
193 # directory.
194 local_path = os.path.abspath(local_path)
195 if not local_path.startswith(updater.static_dir):
196 raise DevServerError('Local path %s must be a subdirectory of the static'
197 ' directory: %s' % (local_path, updater.static_dir))
198
199 return local_path.rstrip('/')
200
201
202def _get_artifacts(kwargs):
203 """Returns a tuple of named and file artifacts given the stage rpc kwargs.
204
205 Raises:
206 DevserverError if no artifacts would be returned.
207 """
208 artifacts = kwargs.get('artifacts')
209 files = kwargs.get('files')
210 if not artifacts and not files:
211 raise DevServerError('No artifacts specified.')
212
213 # Note we NEED to coerce files to a string as we get raw unicode from
214 # cherrypy and we treat files as strings elsewhere in the code.
215 return (str(artifacts).split(',') if artifacts else [],
216 str(files).split(',') if files else [])
217
218
Dan Shi61305df2015-10-26 16:52:35 -0700219def _is_android_build_request(kwargs):
220 """Check if a devserver call is for Android build, based on the arguments.
221
222 This method exams the request's arguments (os_type) to determine if the
223 request is for Android build. If os_type is set to `android`, returns True.
224 If os_type is not set or has other values, returns False.
225
226 Args:
227 kwargs: Keyword arguments for the request.
228
229 Returns:
230 True if the request is for Android build. False otherwise.
231 """
232 os_type = kwargs.get('os_type', None)
233 return os_type == 'android'
234
235
Gabe Black3b567202015-09-23 14:07:59 -0700236def _get_downloader(kwargs):
237 """Returns the downloader based on passed in arguments.
238
239 Args:
240 kwargs: Keyword arguments for the request.
241 """
242 local_path = kwargs.get('local_path')
243 if local_path:
244 local_path = _canonicalize_local_path(local_path)
245
246 dl = None
247 if local_path:
248 dl = downloader.LocalDownloader(updater.static_dir, local_path)
249
Dan Shi61305df2015-10-26 16:52:35 -0700250 if not _is_android_build_request(kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700251 archive_url = kwargs.get('archive_url')
252 if not archive_url and not local_path:
253 raise DevServerError('Requires archive_url or local_path to be '
254 'specified.')
255 if archive_url and local_path:
256 raise DevServerError('archive_url and local_path can not both be '
257 'specified.')
258 if not dl:
259 archive_url = _canonicalize_archive_url(archive_url)
260 dl = downloader.GoogleStorageDownloader(updater.static_dir, archive_url)
261 elif not dl:
262 target = kwargs.get('target', None)
Dan Shi72b16132015-10-08 12:10:33 -0700263 branch = kwargs.get('branch', None)
Dan Shi61305df2015-10-26 16:52:35 -0700264 build_id = kwargs.get('build_id', None)
265 if not target or not branch or not build_id:
Dan Shi72b16132015-10-08 12:10:33 -0700266 raise DevServerError(
Dan Shi61305df2015-10-26 16:52:35 -0700267 'target, branch, build ID must all be specified for downloading '
268 'Android build.')
Dan Shi72b16132015-10-08 12:10:33 -0700269 dl = downloader.AndroidBuildDownloader(updater.static_dir, branch, build_id,
270 target)
Gabe Black3b567202015-09-23 14:07:59 -0700271
272 return dl
273
274
275def _get_downloader_and_factory(kwargs):
276 """Returns the downloader and artifact factory based on passed in arguments.
277
278 Args:
279 kwargs: Keyword arguments for the request.
280 """
281 artifacts, files = _get_artifacts(kwargs)
282 dl = _get_downloader(kwargs)
283
284 if (isinstance(dl, downloader.GoogleStorageDownloader) or
285 isinstance(dl, downloader.LocalDownloader)):
286 factory_class = build_artifact.ChromeOSArtifactFactory
Dan Shi72b16132015-10-08 12:10:33 -0700287 elif isinstance(dl, downloader.AndroidBuildDownloader):
Gabe Black3b567202015-09-23 14:07:59 -0700288 factory_class = build_artifact.AndroidArtifactFactory
289 else:
290 raise DevServerError('Unrecognized value for downloader type: %s' %
291 type(dl))
292
293 factory = factory_class(dl.GetBuildDir(), artifacts, files, dl.GetBuild())
294
295 return dl, factory
296
297
Scott Zawalski4647ce62012-01-03 17:17:28 -0500298def _LeadingWhiteSpaceCount(string):
299 """Count the amount of leading whitespace in a string.
300
301 Args:
302 string: The string to count leading whitespace in.
Don Garrettf84631a2014-01-07 18:21:26 -0800303
Scott Zawalski4647ce62012-01-03 17:17:28 -0500304 Returns:
305 number of white space chars before characters start.
306 """
Gabe Black3b567202015-09-23 14:07:59 -0700307 matched = re.match(r'^\s+', string)
Scott Zawalski4647ce62012-01-03 17:17:28 -0500308 if matched:
309 return len(matched.group())
310
311 return 0
312
313
314def _PrintDocStringAsHTML(func):
315 """Make a functions docstring somewhat HTML style.
316
317 Args:
318 func: The function to return the docstring from.
Don Garrettf84631a2014-01-07 18:21:26 -0800319
Scott Zawalski4647ce62012-01-03 17:17:28 -0500320 Returns:
321 A string that is somewhat formated for a web browser.
322 """
323 # TODO(scottz): Make this parse Args/Returns in a prettier way.
324 # Arguments could be bolded and indented etc.
325 html_doc = []
326 for line in func.__doc__.splitlines():
327 leading_space = _LeadingWhiteSpaceCount(line)
328 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700329 line = '&nbsp;' * leading_space + line
Scott Zawalski4647ce62012-01-03 17:17:28 -0500330
331 html_doc.append('<BR>%s' % line)
332
333 return '\n'.join(html_doc)
334
335
Simran Basief83d6a2014-08-28 14:32:01 -0700336def _GetUpdateTimestampHandler(static_dir):
337 """Returns a handler to update directory staged.timestamp.
338
339 This handler resets the stage.timestamp whenever static content is accessed.
340
341 Args:
342 static_dir: Directory from which static content is being staged.
343
344 Returns:
345 A cherrypy handler to update the timestamp of accessed content.
346 """
347 def UpdateTimestampHandler():
348 if not '404' in cherrypy.response.status:
349 build_match = re.match(devserver_constants.STAGED_BUILD_REGEX,
350 cherrypy.request.path_info)
351 if build_match:
352 build_dir = os.path.join(static_dir, build_match.group('build'))
353 downloader.Downloader.TouchTimestampForStaged(build_dir)
354 return UpdateTimestampHandler
355
356
Chris Sosa7c931362010-10-11 19:49:01 -0700357def _GetConfig(options):
358 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800359
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800360 socket_host = '::'
Yu-Ju Hongc8d4af32013-11-12 15:14:26 -0800361 # Fall back to IPv4 when python is not configured with IPv6.
362 if not socket.has_ipv6:
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800363 socket_host = '0.0.0.0'
364
Simran Basief83d6a2014-08-28 14:32:01 -0700365 # Adds the UpdateTimestampHandler to cherrypy's tools. This tools executes
366 # on the on_end_resource hook. This hook is called once processing is
367 # complete and the response is ready to be returned.
368 cherrypy.tools.update_timestamp = cherrypy.Tool(
369 'on_end_resource', _GetUpdateTimestampHandler(options.static_dir))
370
Gabe Black3b567202015-09-23 14:07:59 -0700371 base_config = {'global':
372 {'server.log_request_headers': True,
373 'server.protocol_version': 'HTTP/1.1',
374 'server.socket_host': socket_host,
375 'server.socket_port': int(options.port),
376 'response.timeout': 6000,
377 'request.show_tracebacks': True,
378 'server.socket_timeout': 60,
379 'server.thread_pool': 2,
380 'engine.autoreload.on': False,
381 },
382 '/api':
383 {
384 # Gets rid of cherrypy parsing post file for args.
385 'request.process_request_body': False,
386 },
387 '/build':
388 {'response.timeout': 100000,
389 },
390 '/update':
391 {
392 # Gets rid of cherrypy parsing post file for args.
393 'request.process_request_body': False,
394 'response.timeout': 10000,
395 },
396 # Sets up the static dir for file hosting.
397 '/static':
398 {'tools.staticdir.dir': options.static_dir,
399 'tools.staticdir.on': True,
400 'response.timeout': 10000,
401 'tools.update_timestamp.on': True,
402 },
403 }
Chris Sosa5f118ef2012-07-12 11:37:50 -0700404 if options.production:
Alex Miller93beca52013-07-30 19:25:09 -0700405 base_config['global'].update({'server.thread_pool': 150})
Chris Sosa7cd23202013-10-15 17:22:57 -0700406 # TODO(sosa): Do this more cleanly.
407 gsutil_util.GSUTIL_ATTEMPTS = 5
Scott Zawalski1c5e7cd2012-02-27 13:12:52 -0500408
Chris Sosa7c931362010-10-11 19:49:01 -0700409 return base_config
rtc@google.com64244662009-11-12 00:52:08 +0000410
Darin Petkove17164a2010-08-11 13:24:41 -0700411
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700412def _GetRecursiveMemberObject(root, member_list):
413 """Returns an object corresponding to a nested member list.
414
415 Args:
416 root: the root object to search
417 member_list: list of nested members to search
Don Garrettf84631a2014-01-07 18:21:26 -0800418
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700419 Returns:
420 An object corresponding to the member name list; None otherwise.
421 """
422 for member in member_list:
423 next_root = root.__class__.__dict__.get(member)
424 if not next_root:
425 return None
426 root = next_root
427 return root
428
429
430def _IsExposed(name):
431 """Returns True iff |name| has an `exposed' attribute and it is set."""
432 return hasattr(name, 'exposed') and name.exposed
433
434
Gilad Arnold748c8322012-10-12 09:51:35 -0700435def _GetExposedMethod(root, nested_member, ignored=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700436 """Returns a CherryPy-exposed method, if such exists.
437
438 Args:
439 root: the root object for searching
440 nested_member: a slash-joined path to the nested member
441 ignored: method paths to be ignored
Don Garrettf84631a2014-01-07 18:21:26 -0800442
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700443 Returns:
444 A function object corresponding to the path defined by |member_list| from
445 the |root| object, if the function is exposed and not ignored; None
446 otherwise.
447 """
Gilad Arnold748c8322012-10-12 09:51:35 -0700448 method = (not (ignored and nested_member in ignored) and
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700449 _GetRecursiveMemberObject(root, nested_member.split('/')))
Gabe Black3b567202015-09-23 14:07:59 -0700450 if method and type(method) == types.FunctionType and _IsExposed(method):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700451 return method
452
453
Gilad Arnold748c8322012-10-12 09:51:35 -0700454def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700455 """Finds exposed CherryPy methods.
456
457 Args:
458 root: the root object for searching
459 prefix: slash-joined chain of members leading to current object
460 unlisted: URLs to be excluded regardless of their exposed status
Don Garrettf84631a2014-01-07 18:21:26 -0800461
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700462 Returns:
463 List of exposed URLs that are not unlisted.
464 """
465 method_list = []
466 for member in sorted(root.__class__.__dict__.keys()):
467 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 09:51:35 -0700468 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700469 continue
470 member_obj = root.__class__.__dict__[member]
471 if _IsExposed(member_obj):
472 if type(member_obj) == types.FunctionType:
473 method_list.append(prefixed_member)
474 else:
475 method_list += _FindExposedMethods(
476 member_obj, prefixed_member, unlisted)
477 return method_list
478
479
xixuan52c2fba2016-05-20 17:02:48 -0700480def _check_base_args_for_auto_update(kwargs):
481 if 'host_name' not in kwargs:
482 raise common_util.DevServerHTTPError(KEY_ERROR_MSG % 'host_name')
483
484 if 'build_name' not in kwargs:
485 raise common_util.DevServerHTTPError(KEY_ERROR_MSG % 'build_name')
486
487
488def _parse_boolean_arg(kwargs, key):
489 if key in kwargs:
490 if kwargs[key] == 'True':
491 return True
492 elif kwargs[key] == 'False':
493 return False
494 else:
495 raise common_util.DevServerHTTPError(
496 'The value for key %s is not boolean.' % key)
497 else:
498 return False
499
500
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700501class ApiRoot(object):
502 """RESTful API for Dev Server information."""
503 exposed = True
504
505 @cherrypy.expose
506 def hostinfo(self, ip):
507 """Returns a JSON dictionary containing information about the given ip.
508
Gilad Arnold1b908392012-10-05 11:36:27 -0700509 Args:
510 ip: address of host whose info is requested
Don Garrettf84631a2014-01-07 18:21:26 -0800511
Gilad Arnold1b908392012-10-05 11:36:27 -0700512 Returns:
513 A JSON dictionary containing all or some of the following fields:
514 last_event_type (int): last update event type received
515 last_event_status (int): last update event status received
516 last_known_version (string): last known version reported in update ping
517 forced_update_label (string): update label to force next update ping to
518 use, set by setnextupdate
519 See the OmahaEvent class in update_engine/omaha_request_action.h for
520 event type and status code definitions. If the ip does not exist an empty
521 string is returned.
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700522
Gilad Arnold1b908392012-10-05 11:36:27 -0700523 Example URL:
524 http://myhost/api/hostinfo?ip=192.168.1.5
525 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700526 return updater.HandleHostInfoPing(ip)
527
528 @cherrypy.expose
Gilad Arnold286a0062012-01-12 13:47:02 -0800529 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 11:36:27 -0700530 """Returns a JSON object containing a log of host event.
531
532 Args:
533 ip: address of host whose event log is requested, or `all'
Don Garrettf84631a2014-01-07 18:21:26 -0800534
Gilad Arnold1b908392012-10-05 11:36:27 -0700535 Returns:
536 A JSON encoded list (log) of dictionaries (events), each of which
537 containing a `timestamp' and other event fields, as described under
538 /api/hostinfo.
539
540 Example URL:
541 http://myhost/api/hostlog?ip=192.168.1.5
542 """
Gilad Arnold286a0062012-01-12 13:47:02 -0800543 return updater.HandleHostLogPing(ip)
544
545 @cherrypy.expose
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700546 def setnextupdate(self, ip):
547 """Allows the response to the next update ping from a host to be set.
548
549 Takes the IP of the host and an update label as normally provided to the
Gilad Arnold1b908392012-10-05 11:36:27 -0700550 /update command.
551 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700552 body_length = int(cherrypy.request.headers['Content-Length'])
553 label = cherrypy.request.rfile.read(body_length)
554
555 if label:
556 label = label.strip()
557 if label:
558 return updater.HandleSetUpdatePing(ip, label)
Chris Sosa4b951602014-04-09 20:26:07 -0700559 raise common_util.DevServerHTTPError(400, 'No label provided.')
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700560
561
Gilad Arnold55a2a372012-10-02 09:46:32 -0700562 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800563 def fileinfo(self, *args):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700564 """Returns information about a given staged file.
565
566 Args:
Don Garrettf84631a2014-01-07 18:21:26 -0800567 args: path to the file inside the server's static staging directory
568
Gilad Arnold55a2a372012-10-02 09:46:32 -0700569 Returns:
570 A JSON encoded dictionary with information about the said file, which may
571 contain the following keys/values:
Gilad Arnold1b908392012-10-05 11:36:27 -0700572 size (int): the file size in bytes
573 sha1 (string): a base64 encoded SHA1 hash
574 sha256 (string): a base64 encoded SHA256 hash
575
576 Example URL:
577 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 09:46:32 -0700578 """
Don Garrettf84631a2014-01-07 18:21:26 -0800579 file_path = os.path.join(updater.static_dir, *args)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700580 if not os.path.exists(file_path):
581 raise DevServerError('file not found: %s' % file_path)
582 try:
583 file_size = os.path.getsize(file_path)
584 file_sha1 = common_util.GetFileSha1(file_path)
585 file_sha256 = common_util.GetFileSha256(file_path)
586 except os.error, e:
587 raise DevServerError('failed to get info for file %s: %s' %
Gilad Arnolde74b3812013-04-22 11:27:38 -0700588 (file_path, e))
589
590 is_delta = autoupdate.Autoupdate.IsDeltaFormatFile(file_path)
591
592 return json.dumps({
593 autoupdate.Autoupdate.SIZE_ATTR: file_size,
594 autoupdate.Autoupdate.SHA1_ATTR: file_sha1,
595 autoupdate.Autoupdate.SHA256_ATTR: file_sha256,
596 autoupdate.Autoupdate.ISDELTA_ATTR: is_delta
597 })
Gilad Arnold55a2a372012-10-02 09:46:32 -0700598
Chris Sosa76e44b92013-01-31 12:11:38 -0800599
David Rochberg7c79a812011-01-19 14:24:45 -0500600class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700601 """The Root Class for the Dev Server.
602
603 CherryPy works as follows:
604 For each method in this class, cherrpy interprets root/path
605 as a call to an instance of DevServerRoot->method_name. For example,
606 a call to http://myhost/build will call build. CherryPy automatically
607 parses http args and places them as keyword arguments in each method.
608 For paths http://myhost/update/dir1/dir2, you can use *args so that
609 cherrypy uses the update method and puts the extra paths in args.
610 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700611 # Method names that should not be listed on the index page.
612 _UNLISTED_METHODS = ['index', 'doc']
613
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700614 api = ApiRoot()
Chris Sosa7c931362010-10-11 19:49:01 -0700615
Dan Shi59ae7092013-06-04 14:37:27 -0700616 # Number of threads that devserver is staging images.
617 _staging_thread_count = 0
618 # Lock used to lock increasing/decreasing count.
619 _staging_thread_count_lock = threading.Lock()
620
Dan Shiafd0e492015-05-27 14:23:51 -0700621 @require_psutil()
622 def _refresh_io_stats(self):
623 """A call running in a thread to update IO stats periodically."""
624 prev_disk_io_counters = psutil.disk_io_counters()
625 prev_network_io_counters = psutil.net_io_counters()
626 prev_read_time = time.time()
627 while True:
628 time.sleep(STATS_INTERVAL)
629 now = time.time()
630 interval = now - prev_read_time
631 prev_read_time = now
632 # Disk IO is for all disks.
633 disk_io_counters = psutil.disk_io_counters()
634 network_io_counters = psutil.net_io_counters()
635
636 self.disk_read_bytes_per_sec = (
637 disk_io_counters.read_bytes -
638 prev_disk_io_counters.read_bytes)/interval
639 self.disk_write_bytes_per_sec = (
640 disk_io_counters.write_bytes -
641 prev_disk_io_counters.write_bytes)/interval
642 prev_disk_io_counters = disk_io_counters
643
644 self.network_sent_bytes_per_sec = (
645 network_io_counters.bytes_sent -
646 prev_network_io_counters.bytes_sent)/interval
647 self.network_recv_bytes_per_sec = (
648 network_io_counters.bytes_recv -
649 prev_network_io_counters.bytes_recv)/interval
650 prev_network_io_counters = network_io_counters
651
652 @require_psutil()
653 def _start_io_stat_thread(self):
Gabe Black3b567202015-09-23 14:07:59 -0700654 """Start the thread to collect IO stats."""
Dan Shiafd0e492015-05-27 14:23:51 -0700655 thread = threading.Thread(target=self._refresh_io_stats)
656 thread.daemon = True
657 thread.start()
658
joychen3cb228e2013-06-12 12:13:13 -0700659 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700660 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800661 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 12:13:13 -0700662 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 14:24:45 -0500663
Dan Shiafd0e492015-05-27 14:23:51 -0700664 # Cache of disk IO stats, a thread refresh the stats every 10 seconds.
665 # lock is not used for these variables as the only thread writes to these
666 # variables is _refresh_io_stats.
667 self.disk_read_bytes_per_sec = 0
668 self.disk_write_bytes_per_sec = 0
669 # Cache of network IO stats.
670 self.network_sent_bytes_per_sec = 0
671 self.network_recv_bytes_per_sec = 0
672 self._start_io_stat_thread()
673
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700674 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500675 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700676 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700677 import builder
678 if self._builder is None:
679 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500680 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700681
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700682 @cherrypy.expose
Dan Shif8eb0d12013-08-01 17:52:06 -0700683 def is_staged(self, **kwargs):
684 """Check if artifacts have been downloaded.
685
Chris Sosa6b0c6172013-08-05 17:01:33 -0700686 async: True to return without waiting for download to complete.
687 artifacts: Comma separated list of named artifacts to download.
688 These are defined in artifact_info and have their implementation
689 in build_artifact.py.
690 files: Comma separated list of file artifacts to stage. These
691 will be available as is in the corresponding static directory with no
692 custom post-processing.
693
694 returns: True of all artifacts are staged.
Dan Shif8eb0d12013-08-01 17:52:06 -0700695
696 Example:
697 To check if autotest and test_suites are staged:
698 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
699 artifacts=autotest,test_suites
700 """
Gabe Black3b567202015-09-23 14:07:59 -0700701 dl, factory = _get_downloader_and_factory(kwargs)
Aviv Keshet57d18172016-06-18 20:39:09 -0700702 response = str(dl.IsStaged(factory))
703 _Log('Responding to is_staged %s request with %r', kwargs, response)
704 return response
Dan Shi59ae7092013-06-04 14:37:27 -0700705
Chris Sosa76e44b92013-01-31 12:11:38 -0800706 @cherrypy.expose
Prashanth Ba06d2d22014-03-07 15:35:19 -0800707 def list_image_dir(self, **kwargs):
708 """Take an archive url and list the contents in its staged directory.
709
710 Args:
711 kwargs:
712 archive_url: Google Storage URL for the build.
713
714 Example:
715 To list the contents of where this devserver should have staged
716 gs://image-archive/<board>-release/<build> call:
717 http://devserver_url:<port>/list_image_dir?archive_url=<gs://..>
718
719 Returns:
720 A string with information about the contents of the image directory.
721 """
Gabe Black3b567202015-09-23 14:07:59 -0700722 dl = _get_downloader(kwargs)
Prashanth Ba06d2d22014-03-07 15:35:19 -0800723 try:
Gabe Black3b567202015-09-23 14:07:59 -0700724 image_dir_contents = dl.ListBuildDir()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800725 except build_artifact.ArtifactDownloadError as e:
726 return 'Cannot list the contents of staged artifacts. %s' % e
727 if not image_dir_contents:
Gabe Black3b567202015-09-23 14:07:59 -0700728 return '%s has not been staged on this devserver.' % dl.DescribeSource()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800729 return image_dir_contents
730
731 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800732 def stage(self, **kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700733 """Downloads and caches build artifacts.
Chris Sosa76e44b92013-01-31 12:11:38 -0800734
Gabe Black3b567202015-09-23 14:07:59 -0700735 Downloads and caches build artifacts, possibly from a Google Storage URL,
Dan Shi72b16132015-10-08 12:10:33 -0700736 or from Android's build server. Returns once these have been downloaded
Gabe Black3b567202015-09-23 14:07:59 -0700737 on the devserver. A call to this will attempt to cache non-specified
738 artifacts in the background for the given from the given URL following
739 the principle of spatial locality. Spatial locality of different
Chris Sosa76e44b92013-01-31 12:11:38 -0800740 artifacts is explicitly defined in the build_artifact module.
741
742 These artifacts will then be available from the static/ sub-directory of
743 the devserver.
744
745 Args:
746 archive_url: Google Storage URL for the build.
Simran Basi4243a862014-12-12 12:48:33 -0800747 local_path: Local path for the build.
Dan Shif8eb0d12013-08-01 17:52:06 -0700748 async: True to return without waiting for download to complete.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700749 artifacts: Comma separated list of named artifacts to download.
750 These are defined in artifact_info and have their implementation
751 in build_artifact.py.
752 files: Comma separated list of files to stage. These
753 will be available as is in the corresponding static directory with no
754 custom post-processing.
Laurence Goodbyf5c958d2016-01-14 18:23:56 -0800755 clean: True to remove any previously staged artifacts first.
Chris Sosa76e44b92013-01-31 12:11:38 -0800756
757 Example:
758 To download the autotest and test suites tarballs:
759 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
760 artifacts=autotest,test_suites
761 To download the full update payload:
762 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
763 artifacts=full_payload
Chris Sosa6b0c6172013-08-05 17:01:33 -0700764 To download just a file called blah.bin:
765 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
766 files=blah.bin
Chris Sosa76e44b92013-01-31 12:11:38 -0800767
768 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 16:39:34 -0700769 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 12:11:38 -0800770
771 Note for this example, relative path is the archive_url stripped of its
772 basename i.e. path/ in the examples above. Specific example:
773
774 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
775
776 Will get staged to:
777
joychened64b222013-06-21 16:39:34 -0700778 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Chris Sosa76e44b92013-01-31 12:11:38 -0800779 """
Gabe Black3b567202015-09-23 14:07:59 -0700780 dl, factory = _get_downloader_and_factory(kwargs)
781
Dan Shi59ae7092013-06-04 14:37:27 -0700782 with DevServerRoot._staging_thread_count_lock:
783 DevServerRoot._staging_thread_count += 1
784 try:
Laurence Goodbyf5c958d2016-01-14 18:23:56 -0800785 boolean_string = kwargs.get('clean')
786 clean = xbuddy.XBuddy.ParseBoolean(boolean_string)
787 if clean and os.path.exists(dl.GetBuildDir()):
788 _Log('Removing %s' % dl.GetBuildDir())
789 shutil.rmtree(dl.GetBuildDir())
Gabe Black3b567202015-09-23 14:07:59 -0700790 async = kwargs.get('async', False)
791 dl.Download(factory, async=async)
Dan Shi59ae7092013-06-04 14:37:27 -0700792 finally:
793 with DevServerRoot._staging_thread_count_lock:
794 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800795 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700796
797 @cherrypy.expose
xixuan52c2fba2016-05-20 17:02:48 -0700798 def cros_au(self, **kwargs):
799 """Auto-update a CrOS DUT.
800
801 Args:
802 kwargs:
803 host_name: the hostname of the DUT to auto-update.
804 build_name: the build name for update the DUT.
805 force_update: Force an update even if the version installed is the
806 same. Default: False.
807 full_update: If True, do not run stateful update, directly force a full
808 reimage. If False, try stateful update first if the dut is already
809 installed with the same version.
810 async: Whether the auto_update function is ran in the background.
811
812 Returns:
813 A tuple includes two elements:
814 a boolean variable represents whether the auto-update process is
815 successfully started.
816 an integer represents the background auto-update process id.
817 """
818 _check_base_args_for_auto_update(kwargs)
819
820 host_name = kwargs['host_name']
821 build_name = kwargs['build_name']
822 force_update = _parse_boolean_arg(kwargs, 'force_update')
823 full_update = _parse_boolean_arg(kwargs, 'full_update')
824 async = _parse_boolean_arg(kwargs, 'async')
825
826 if async:
827 path = os.path.dirname(os.path.abspath(__file__))
828 execute_file = os.path.join(path, 'cros_update.py')
829 args = (AUTO_UPDATE_CMD % (execute_file, host_name, build_name,
830 updater.static_dir))
831 if force_update:
832 args = ('%s --force_update' % args)
833
834 if full_update:
835 args = ('%s --full_update' % args)
836
xixuan2a0970a2016-08-10 12:12:44 -0700837 p = subprocess.Popen([args], shell=True, preexec_fn=os.setsid)
838 pid = os.getpgid(p.pid)
xixuan52c2fba2016-05-20 17:02:48 -0700839
840 # Pre-write status in the track_status_file before the first call of
841 # 'get_au_status' to make sure that the track_status_file exists.
xixuan2a0970a2016-08-10 12:12:44 -0700842 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -0700843 progress_tracker.WriteStatus('CrOS update is just started.')
844
xixuan2a0970a2016-08-10 12:12:44 -0700845 return json.dumps((True, pid))
xixuan52c2fba2016-05-20 17:02:48 -0700846 else:
847 cros_update_trigger = cros_update.CrOSUpdateTrigger(
848 host_name, build_name, updater.static_dir)
849 cros_update_trigger.TriggerAU()
850
851 @cherrypy.expose
852 def get_au_status(self, **kwargs):
853 """Check if the auto-update task is finished.
854
855 It handles 4 cases:
856 1. If an error exists in the track_status_file, delete the track file and
857 raise it.
858 2. If cros-update process is finished, delete the file and return the
859 success result.
860 3. If the process is not running, delete the track file and raise an error
861 about 'the process is terminated due to unknown reason'.
862 4. If the track_status_file does not exist, kill the process if it exists,
863 and raise the IOError.
864
865 Args:
866 kwargs:
867 host_name: the hostname of the DUT to auto-update.
868 pid: the background process id of cros-update.
869
870 Returns:
xixuan28d99072016-10-06 12:24:16 -0700871 A dict with three elements:
xixuan52c2fba2016-05-20 17:02:48 -0700872 a boolean variable represents whether the auto-update process is
873 finished.
874 a string represents the current auto-update process status.
875 For example, 'Transfer Devserver/Stateful Update Package'.
xixuan28d99072016-10-06 12:24:16 -0700876 a detailed error message paragraph if there exists an Auto-Update
877 error, in which the last line shows the main exception. Empty
878 string otherwise.
xixuan52c2fba2016-05-20 17:02:48 -0700879 """
880 if 'host_name' not in kwargs:
881 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
882
883 if 'pid' not in kwargs:
884 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
885
886 host_name = kwargs['host_name']
887 pid = kwargs['pid']
888 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
889
xixuan28d99072016-10-06 12:24:16 -0700890 result_dict = {'finished': False, 'status': '', 'detailed_error_msg': ''}
xixuan52c2fba2016-05-20 17:02:48 -0700891 try:
892 result = progress_tracker.ReadStatus()
893 if result.startswith(cros_update_progress.ERROR_TAG):
xixuan28d99072016-10-06 12:24:16 -0700894 result_dict['detailed_error_msg'] = result[len(
895 cros_update_progress.ERROR_TAG):]
xixuan28681fd2016-11-23 11:13:56 -0800896 elif result == cros_update_progress.FINISHED:
xixuan28d99072016-10-06 12:24:16 -0700897 result_dict['finished'] = True
898 result_dict['status'] = result
xixuan28681fd2016-11-23 11:13:56 -0800899 elif not cros_update_progress.IsProcessAlive(pid):
xixuan28d99072016-10-06 12:24:16 -0700900 result_dict['detailed_error_msg'] = (
901 'Cros_update process terminated midway due to unknown reason. '
902 'Last update status was %s' % result)
xixuan28681fd2016-11-23 11:13:56 -0800903 else:
904 result_dict['status'] = result
905 except IOError as e:
906 if pid and cros_update_progress.IsProcessAlive(pid):
xixuan2a0970a2016-08-10 12:12:44 -0700907 os.killpg(int(pid), signal.SIGKILL)
xixuan52c2fba2016-05-20 17:02:48 -0700908
xixuan28681fd2016-11-23 11:13:56 -0800909 result_dict['detailed_error_msg'] = str(e)
910
911 return json.dumps(result_dict)
xixuan52c2fba2016-05-20 17:02:48 -0700912
913 @cherrypy.expose
914 def handler_cleanup(self, **kwargs):
xixuan3bc974e2016-10-18 17:21:43 -0700915 """Clean track status log and temp directory for CrOS auto-update process.
xixuan52c2fba2016-05-20 17:02:48 -0700916
917 Args:
918 kwargs:
919 host_name: the hostname of the DUT to auto-update.
920 pid: the background process id of cros-update.
921 """
922 if 'host_name' not in kwargs:
923 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
924
925 if 'pid' not in kwargs:
926 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
927
928 host_name = kwargs['host_name']
929 pid = kwargs['pid']
930 cros_update_progress.DelTrackStatusFile(host_name, pid)
xixuan3bc974e2016-10-18 17:21:43 -0700931 cros_update_progress.DelAUTempDirectory(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -0700932
933 @cherrypy.expose
934 def kill_au_proc(self, **kwargs):
935 """Kill CrOS auto-update process using given process id.
936
937 Args:
938 kwargs:
939 host_name: Kill all the CrOS auto-update process of this host.
940
941 Returns:
942 True if all processes are killed properly.
943 """
944 if 'host_name' not in kwargs:
945 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
946
947 host_name = kwargs['host_name']
xixuan3bc974e2016-10-18 17:21:43 -0700948 track_log_list = cros_update_progress.GetAllTrackStatusFileByHostName(
949 host_name)
xixuan52c2fba2016-05-20 17:02:48 -0700950 for log in track_log_list:
951 # The track log's full path is: path/host_name_pid.log
952 # Use splitext to remove file extension, then parse pid from the
953 # filename.
954 pid = os.path.splitext(os.path.basename(log))[0][len(host_name)+1:]
955 if cros_update_progress.IsProcessAlive(pid):
xixuan2a0970a2016-08-10 12:12:44 -0700956 os.killpg(int(pid), signal.SIGKILL)
xixuan52c2fba2016-05-20 17:02:48 -0700957
958 cros_update_progress.DelTrackStatusFile(host_name, pid)
xixuan1bbfaba2016-10-13 17:53:22 -0700959 cros_update_progress.DelExecuteLogFile(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -0700960
961 return 'True'
962
963 @cherrypy.expose
964 def collect_cros_au_log(self, **kwargs):
965 """Collect CrOS auto-update log.
966
967 Args:
968 kwargs:
969 host_name: the hostname of the DUT to auto-update.
970 pid: the background process id of cros-update.
971
972 Returns:
973 A string contains the whole content of the execute log file.
974 """
975 if 'host_name' not in kwargs:
976 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
977
978 if 'pid' not in kwargs:
979 raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
980
981 host_name = kwargs['host_name']
982 pid = kwargs['pid']
xixuan3bc974e2016-10-18 17:21:43 -0700983
984 # Fetch the execute log recorded by cros_update_progress.
xixuan1bbfaba2016-10-13 17:53:22 -0700985 au_log = cros_update_progress.ReadExecuteLogFile(host_name, pid)
986 cros_update_progress.DelExecuteLogFile(host_name, pid)
987 return au_log
988
xixuan52c2fba2016-05-20 17:02:48 -0700989 @cherrypy.expose
Dan Shi2f136862016-02-11 15:38:38 -0800990 def locate_file(self, **kwargs):
991 """Get the path to the given file name.
992
993 This method looks up the given file name inside specified build artifacts.
994 One use case is to help caller to locate an apk file inside a build
995 artifact. The location of the apk file could be different based on the
996 branch and target.
997
998 Args:
999 file_name: Name of the file to look for.
1000 artifacts: A list of artifact names to search for the file.
1001
1002 Returns:
1003 Path to the file with the given name. It's relative to the folder for the
1004 build, e.g., DATA/priv-app/sl4a/sl4a.apk
Dan Shi2f136862016-02-11 15:38:38 -08001005 """
1006 dl, _ = _get_downloader_and_factory(kwargs)
1007 try:
1008 file_name = kwargs['file_name'].lower()
1009 artifacts = kwargs['artifacts']
1010 except KeyError:
1011 raise DevServerError('`file_name` and `artifacts` are required to search '
1012 'for a file in build artifacts.')
1013 build_path = dl.GetBuildDir()
1014 for artifact in artifacts:
1015 # Get the unzipped folder of the artifact. If it's not defined in
1016 # ARTIFACT_UNZIP_FOLDER_MAP, assume the files are unzipped to the build
1017 # directory directly.
1018 folder = artifact_info.ARTIFACT_UNZIP_FOLDER_MAP.get(artifact, '')
1019 artifact_path = os.path.join(build_path, folder)
1020 for root, _, filenames in os.walk(artifact_path):
1021 if file_name in set([f.lower() for f in filenames]):
1022 return os.path.relpath(os.path.join(root, file_name), build_path)
1023 raise DevServerError('File `%s` can not be found in artifacts: %s' %
1024 (file_name, artifacts))
1025
1026 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -08001027 def setup_telemetry(self, **kwargs):
1028 """Extracts and sets up telemetry
1029
1030 This method goes through the telemetry deps packages, and stages them on
1031 the devserver to be used by the drones and the telemetry tests.
1032
1033 Args:
1034 archive_url: Google Storage URL for the build.
1035
1036 Returns:
1037 Path to the source folder for the telemetry codebase once it is staged.
1038 """
Gabe Black3b567202015-09-23 14:07:59 -07001039 dl = _get_downloader(kwargs)
Simran Basi4baad082013-02-14 13:39:18 -08001040
Gabe Black3b567202015-09-23 14:07:59 -07001041 build_path = dl.GetBuildDir()
Simran Basi4baad082013-02-14 13:39:18 -08001042 deps_path = os.path.join(build_path, 'autotest/packages')
1043 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
1044 src_folder = os.path.join(telemetry_path, 'src')
1045
1046 with self._telemetry_lock_dict.lock(telemetry_path):
1047 if os.path.exists(src_folder):
1048 # Telemetry is already fully stage return
1049 return src_folder
1050
1051 common_util.MkDirP(telemetry_path)
1052
1053 # Copy over the required deps tar balls to the telemetry directory.
1054 for dep in TELEMETRY_DEPS:
1055 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -07001056 if not os.path.exists(dep_path):
1057 # This dep does not exist (could be new), do not extract it.
1058 continue
Simran Basi4baad082013-02-14 13:39:18 -08001059 try:
1060 common_util.ExtractTarball(dep_path, telemetry_path)
1061 except common_util.CommonUtilError as e:
1062 shutil.rmtree(telemetry_path)
1063 raise DevServerError(str(e))
1064
1065 # By default all the tarballs extract to test_src but some parts of
1066 # the telemetry code specifically hardcoded to exist inside of 'src'.
1067 test_src = os.path.join(telemetry_path, 'test_src')
1068 try:
1069 shutil.move(test_src, src_folder)
1070 except shutil.Error:
1071 # This can occur if src_folder already exists. Remove and retry move.
1072 shutil.rmtree(src_folder)
Gabe Black3b567202015-09-23 14:07:59 -07001073 raise DevServerError(
1074 'Failure in telemetry setup for build %s. Appears that the '
1075 'test_src to src move failed.' % dl.GetBuild())
Simran Basi4baad082013-02-14 13:39:18 -08001076
1077 return src_folder
1078
1079 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -08001080 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -07001081 """Symbolicates a minidump using pre-downloaded symbols, returns it.
1082
1083 Callers will need to POST to this URL with a body of MIME-type
1084 "multipart/form-data".
1085 The body should include a single argument, 'minidump', containing the
1086 binary-formatted minidump to symbolicate.
1087
Chris Masone816e38c2012-05-02 12:22:36 -07001088 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -08001089 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -07001090 minidump: The binary minidump file to symbolicate.
1091 """
Chris Sosa76e44b92013-01-31 12:11:38 -08001092 # Ensure the symbols have been staged.
Dan Shif08fe492016-10-04 14:39:25 -07001093 # Try debug.tar.xz first, then debug.tgz
1094 for artifact in (artifact_info.SYMBOLS_ONLY, artifact_info.SYMBOLS):
1095 kwargs['artifacts'] = artifact
1096 dl = _get_downloader(kwargs)
1097
1098 try:
1099 if self.stage(**kwargs) == 'Success':
1100 break
1101 except build_artifact.ArtifactDownloadError:
1102 continue
1103 else:
Gabe Black3b567202015-09-23 14:07:59 -07001104 raise DevServerError('Failed to stage symbols for %s' %
1105 dl.DescribeSource())
Chris Sosa76e44b92013-01-31 12:11:38 -08001106
Chris Masone816e38c2012-05-02 12:22:36 -07001107 to_return = ''
1108 with tempfile.NamedTemporaryFile() as local:
1109 while True:
1110 data = minidump.file.read(8192)
1111 if not data:
1112 break
1113 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -08001114
Chris Masone816e38c2012-05-02 12:22:36 -07001115 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -08001116
Gabe Black3b567202015-09-23 14:07:59 -07001117 symbols_directory = os.path.join(dl.GetBuildDir(), 'debug', 'breakpad')
Chris Sosa76e44b92013-01-31 12:11:38 -08001118
1119 stackwalk = subprocess.Popen(
1120 ['minidump_stackwalk', local.name, symbols_directory],
1121 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1122
Chris Masone816e38c2012-05-02 12:22:36 -07001123 to_return, error_text = stackwalk.communicate()
1124 if stackwalk.returncode != 0:
1125 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
1126 error_text, stackwalk.returncode))
1127
1128 return to_return
1129
1130 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -08001131 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 15:31:36 -04001132 """Return a string representing the latest build for a given target.
1133
1134 Args:
1135 target: The build target, typically a combination of the board and the
1136 type of build e.g. x86-mario-release.
1137 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
1138 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-07 18:21:26 -08001139
Scott Zawalski16954532012-03-20 15:31:36 -04001140 Returns:
1141 A string representation of the latest build if one exists, i.e.
1142 R19-1993.0.0-a1-b1480.
1143 An empty string if no latest could be found.
1144 """
Don Garrettf84631a2014-01-07 18:21:26 -08001145 if not kwargs:
Scott Zawalski16954532012-03-20 15:31:36 -04001146 return _PrintDocStringAsHTML(self.latestbuild)
1147
Don Garrettf84631a2014-01-07 18:21:26 -08001148 if 'target' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -07001149 raise common_util.DevServerHTTPError(500, 'Error: target= is required!')
Dan Shi61305df2015-10-26 16:52:35 -07001150
1151 if _is_android_build_request(kwargs):
1152 branch = kwargs.get('branch', None)
1153 target = kwargs.get('target', None)
1154 if not target or not branch:
1155 raise DevServerError(
xixuan52c2fba2016-05-20 17:02:48 -07001156 'Both target and branch must be specified to query for the latest '
1157 'Android build.')
Dan Shi61305df2015-10-26 16:52:35 -07001158 return android_build.BuildAccessor.GetLatestBuildID(target, branch)
1159
Scott Zawalski16954532012-03-20 15:31:36 -04001160 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001161 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-07 18:21:26 -08001162 updater.static_dir, kwargs['target'],
1163 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -07001164 except common_util.CommonUtilError as errmsg:
Chris Sosa4b951602014-04-09 20:26:07 -07001165 raise common_util.DevServerHTTPError(500, str(errmsg))
Scott Zawalski16954532012-03-20 15:31:36 -04001166
1167 @cherrypy.expose
xixuan7efd0002016-04-14 15:34:01 -07001168 def list_suite_controls(self, **kwargs):
1169 """Return a list of contents of all known control files.
1170
1171 Example URL:
1172 To List all control files' content:
1173 http://dev-server/list_suite_controls?suite_name=bvt&
1174 build=daisy_spring-release/R29-4279.0.0
1175
1176 Args:
1177 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
1178 suite_name: List the control files belonging to that suite.
1179
1180 Returns:
Dan Shia1cd6522016-04-18 16:07:21 -07001181 A dictionary of all control files's path to its content for given suite.
xixuan7efd0002016-04-14 15:34:01 -07001182 """
1183 if not kwargs:
1184 return _PrintDocStringAsHTML(self.controlfiles)
1185
1186 if 'build' not in kwargs:
1187 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
1188
1189 if 'suite_name' not in kwargs:
Dan Shia1cd6522016-04-18 16:07:21 -07001190 raise common_util.DevServerHTTPError(500,
1191 'Error: suite_name= is required!')
xixuan7efd0002016-04-14 15:34:01 -07001192
1193 control_file_list = [
1194 line.rstrip() for line in common_util.GetControlFileListForSuite(
1195 updater.static_dir, kwargs['build'],
1196 kwargs['suite_name']).splitlines()]
1197
Dan Shia1cd6522016-04-18 16:07:21 -07001198 control_file_content_dict = {}
xixuan7efd0002016-04-14 15:34:01 -07001199 for control_path in control_file_list:
Dan Shia1cd6522016-04-18 16:07:21 -07001200 control_file_content_dict[control_path] = (common_util.GetControlFile(
xixuan7efd0002016-04-14 15:34:01 -07001201 updater.static_dir, kwargs['build'], control_path))
1202
Dan Shia1cd6522016-04-18 16:07:21 -07001203 return json.dumps(control_file_content_dict)
xixuan7efd0002016-04-14 15:34:01 -07001204
1205 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -08001206 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 17:17:28 -05001207 """Return a control file or a list of all known control files.
1208
1209 Example URL:
1210 To List all control files:
beepsbd337242013-07-09 22:44:06 -07001211 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
1212 To List all control files for, say, the bvt suite:
1213 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -05001214 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -05001215 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 -05001216
1217 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -05001218 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -05001219 control_path: If you want the contents of a control file set this
1220 to the path. E.g. client/site_tests/sleeptest/control
1221 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -07001222 suite_name: If control_path is not specified but a suite_name is
1223 specified, list the control files belonging to that suite instead of
1224 all control files. The empty string for suite_name will list all control
1225 files for the build.
Don Garrettf84631a2014-01-07 18:21:26 -08001226
Scott Zawalski4647ce62012-01-03 17:17:28 -05001227 Returns:
1228 Contents of a control file if control_path is provided.
1229 A list of control files if no control_path is provided.
1230 """
Don Garrettf84631a2014-01-07 18:21:26 -08001231 if not kwargs:
Scott Zawalski4647ce62012-01-03 17:17:28 -05001232 return _PrintDocStringAsHTML(self.controlfiles)
1233
Don Garrettf84631a2014-01-07 18:21:26 -08001234 if 'build' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -07001235 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -05001236
Don Garrettf84631a2014-01-07 18:21:26 -08001237 if 'control_path' not in kwargs:
1238 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-09 22:44:06 -07001239 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-07 18:21:26 -08001240 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-09 22:44:06 -07001241 else:
1242 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-07 18:21:26 -08001243 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -05001244 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001245 return common_util.GetControlFile(
Don Garrettf84631a2014-01-07 18:21:26 -08001246 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -08001247
1248 @cherrypy.expose
Simran Basi99e63c02014-05-20 10:39:52 -07001249 def xbuddy_translate(self, *args, **kwargs):
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001250 """Translates an xBuddy path to a real path to artifact if it exists.
1251
1252 Args:
Simran Basi99e63c02014-05-20 10:39:52 -07001253 args: An xbuddy path in the form of {local|remote}/build_id/artifact.
1254 Local searches the devserver's static directory. Remote searches a
1255 Google Storage image archive.
1256
1257 Kwargs:
1258 image_dir: Google Storage image archive to search in if requesting a
1259 remote artifact. If none uses the default bucket.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001260
1261 Returns:
Simran Basi99e63c02014-05-20 10:39:52 -07001262 String in the format of build_id/artifact as stored on the local server
1263 or in Google Storage.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001264 """
Simran Basi99e63c02014-05-20 10:39:52 -07001265 build_id, filename = self._xbuddy.Translate(
Gabe Black3b567202015-09-23 14:07:59 -07001266 args, image_dir=kwargs.get('image_dir'))
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001267 response = os.path.join(build_id, filename)
1268 _Log('Path translation requested, returning: %s', response)
1269 return response
1270
1271 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -07001272 def xbuddy(self, *args, **kwargs):
1273 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -07001274
1275 Args:
joycheneaf4cfc2013-07-02 08:38:57 -07001276 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -07001277 components of the path. The path can be understood as
1278 "{local|remote}/build_id/artifact" where build_id is composed of
1279 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -07001280
joychen121fc9b2013-08-02 14:30:30 -07001281 The first path element is optional, and can be "remote" or "local"
1282 If local (the default), devserver will not attempt to access Google
1283 Storage, and will only search the static directory for the files.
1284 If remote, devserver will try to obtain the artifact off GS if it's
1285 not found locally.
1286 The board is the familiar board name, optionally suffixed.
1287 The version can be the google storage version number, and may also be
1288 any of a number of xBuddy defined version aliases that will be
1289 translated into the latest built image that fits the description.
1290 Defaults to latest.
1291 The artifact is one of a number of image or artifact aliases used by
1292 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -07001293
1294 Kwargs:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001295 for_update: {true|false}
1296 if true, pregenerates the update payloads for the image,
1297 and returns the update uri to pass to the
1298 update_engine_client.
joychen3cb228e2013-06-12 12:13:13 -07001299 return_dir: {true|false}
1300 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001301 relative_path: {true|false}
1302 if set to true, returns the relative path to the payload
1303 directory from static_dir.
joychen3cb228e2013-06-12 12:13:13 -07001304 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -07001305 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -07001306 or
joycheneaf4cfc2013-07-02 08:38:57 -07001307 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -07001308
1309 Returns:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001310 If |for_update|, returns a redirect to the image or update file
1311 on the devserver. E.g.,
1312 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
1313 chromium-test-image.bin
1314 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
1315 http://host:port/static/x86-generic-release/R26-4000.0.0/
1316 If |relative_path| is true, return a relative path the folder where the
1317 payloads are. E.g.,
1318 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 12:13:13 -07001319 """
Chris Sosa75490802013-09-30 17:21:45 -07001320 boolean_string = kwargs.get('for_update')
1321 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001322 boolean_string = kwargs.get('return_dir')
1323 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
1324 boolean_string = kwargs.get('relative_path')
1325 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -07001326
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001327 if return_dir and relative_path:
Chris Sosa4b951602014-04-09 20:26:07 -07001328 raise common_util.DevServerHTTPError(
1329 500, 'Cannot specify both return_dir and relative_path')
Chris Sosa75490802013-09-30 17:21:45 -07001330
1331 # For updates, we optimize downloading of test images.
1332 file_name = None
1333 build_id = None
1334 if for_update:
1335 try:
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001336 build_id = self._xbuddy.StageTestArtifactsForUpdate(args)
Chris Sosa75490802013-09-30 17:21:45 -07001337 except build_artifact.ArtifactDownloadError:
1338 build_id = None
1339
1340 if not build_id:
1341 build_id, file_name = self._xbuddy.Get(args)
1342
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001343 if for_update:
1344 _Log('Payload generation triggered by request')
1345 # Forces payload to be in cache and symlinked into build_id dir.
Chris Sosa75490802013-09-30 17:21:45 -07001346 updater.GetUpdateForLabel(autoupdate.FORCED_UPDATE, build_id,
1347 image_name=file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001348
1349 response = None
1350 if return_dir:
1351 response = os.path.join(cherrypy.request.base, 'static', build_id)
1352 _Log('Directory requested, returning: %s', response)
1353 elif relative_path:
1354 response = build_id
1355 _Log('Relative path requested, returning: %s', response)
1356 elif for_update:
1357 response = os.path.join(cherrypy.request.base, 'update', build_id)
1358 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 12:13:13 -07001359 else:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001360 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 14:30:30 -07001361 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001362 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 14:30:30 -07001363 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -07001364
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001365 return response
1366
joychen3cb228e2013-06-12 12:13:13 -07001367 @cherrypy.expose
1368 def xbuddy_list(self):
1369 """Lists the currently available images & time since last access.
1370
Gilad Arnold452fd272014-02-04 11:09:28 -08001371 Returns:
1372 A string representation of a list of tuples [(build_id, time since last
1373 access),...]
joychen3cb228e2013-06-12 12:13:13 -07001374 """
1375 return self._xbuddy.List()
1376
1377 @cherrypy.expose
1378 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 11:09:28 -08001379 """Returns the number of images cached by xBuddy."""
joychen3cb228e2013-06-12 12:13:13 -07001380 return self._xbuddy.Capacity()
1381
1382 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001383 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001384 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001385 return ('Welcome to the Dev Server!<br>\n'
1386 '<br>\n'
1387 'Here are the available methods, click for documentation:<br>\n'
1388 '<br>\n'
1389 '%s' %
1390 '<br>\n'.join(
1391 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001392 for name in _FindExposedMethods(
1393 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001394
1395 @cherrypy.expose
1396 def doc(self, *args):
1397 """Shows the documentation for available methods / URLs.
1398
1399 Example:
1400 http://myhost/doc/update
1401 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001402 name = '/'.join(args)
1403 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001404 if not method:
1405 raise DevServerError("No exposed method named `%s'" % name)
1406 if not method.__doc__:
1407 raise DevServerError("No documentation for exposed method `%s'" % name)
1408 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -07001409
Dale Curtisc9aaf3a2011-08-09 15:47:40 -07001410 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001411 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001412 """Handles an update check from a Chrome OS client.
1413
1414 The HTTP request should contain the standard Omaha-style XML blob. The URL
1415 line may contain an additional intermediate path to the update payload.
1416
joychen121fc9b2013-08-02 14:30:30 -07001417 This request can be handled in one of 4 ways, depending on the devsever
1418 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -07001419
joychen121fc9b2013-08-02 14:30:30 -07001420 1. No intermediate path
1421 If no intermediate path is given, the default behavior is to generate an
1422 update payload from the latest test image locally built for the board
1423 specified in the xml. Devserver serves the generated payload.
1424
1425 2. Path explicitly invokes XBuddy
1426 If there is a path given, it can explicitly invoke xbuddy by prefixing it
1427 with 'xbuddy'. This path is then used to acquire an image binary for the
1428 devserver to generate an update payload from. Devserver then serves this
1429 payload.
1430
1431 3. Path is left for the devserver to interpret.
1432 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
1433 to generate a payload from the test image in that directory and serve it.
1434
1435 4. The devserver is in a 'forced' mode. TO BE DEPRECATED
1436 This comes from the usage of --forced_payload or --image when starting the
1437 devserver. No matter what path (or no path) gets passed in, devserver will
1438 serve the update payload (--forced_payload) or generate an update payload
1439 from the image (--image).
1440
1441 Examples:
1442 1. No intermediate path
1443 update_engine_client --omaha_url=http://myhost/update
1444 This generates an update payload from the latest test image locally built
1445 for the board specified in the xml.
1446
1447 2. Explicitly invoke xbuddy
1448 update_engine_client --omaha_url=
1449 http://myhost/update/xbuddy/remote/board/version/dev
1450 This would go to GS to download the dev image for the board, from which
1451 the devserver would generate a payload to serve.
1452
1453 3. Give a path for devserver to interpret
1454 update_engine_client --omaha_url=http://myhost/update/some/random/path
1455 This would attempt, in order to:
1456 a) Generate an update from a test image binary if found in
1457 static_dir/some/random/path.
1458 b) Serve an update payload found in static_dir/some/random/path.
1459 c) Hope that some/random/path takes the form "board/version" and
1460 and attempt to download an update payload for that board/version
1461 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001462 """
joychen121fc9b2013-08-02 14:30:30 -07001463 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -08001464 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -07001465 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-11 19:49:01 -07001466
joychen121fc9b2013-08-02 14:30:30 -07001467 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001468
Dan Shiafd0e492015-05-27 14:23:51 -07001469 @require_psutil()
1470 def _get_io_stats(self):
1471 """Get the IO stats as a dictionary.
1472
Gabe Black3b567202015-09-23 14:07:59 -07001473 Returns:
1474 A dictionary of IO stats collected by psutil.
Dan Shiafd0e492015-05-27 14:23:51 -07001475 """
1476 return {'disk_read_bytes_per_second': self.disk_read_bytes_per_sec,
1477 'disk_write_bytes_per_second': self.disk_write_bytes_per_sec,
1478 'disk_total_bytes_per_second': (self.disk_read_bytes_per_sec +
1479 self.disk_write_bytes_per_sec),
1480 'network_sent_bytes_per_second': self.network_sent_bytes_per_sec,
1481 'network_recv_bytes_per_second': self.network_recv_bytes_per_sec,
1482 'network_total_bytes_per_second': (self.network_sent_bytes_per_sec +
1483 self.network_recv_bytes_per_sec),
1484 'cpu_percent': psutil.cpu_percent(),}
1485
Dan Shi7247f9c2016-06-01 09:19:09 -07001486
1487 def _get_process_count(self, process_cmd_pattern):
1488 """Get the count of processes that match the given command pattern.
1489
1490 Args:
1491 process_cmd_pattern: The regex pattern of process command to match.
1492
1493 Returns:
1494 The count of processes that match the given command pattern.
1495 """
1496 try:
1497 return int(subprocess.check_output(
1498 'pgrep -fc "%s"' % process_cmd_pattern, shell=True))
1499 except subprocess.CalledProcessError:
1500 return 0
1501
1502
Dan Shif5ce2de2013-04-25 16:06:32 -07001503 @cherrypy.expose
1504 def check_health(self):
1505 """Collect the health status of devserver to see if it's ready for staging.
1506
Gilad Arnold452fd272014-02-04 11:09:28 -08001507 Returns:
1508 A JSON dictionary containing all or some of the following fields:
1509 free_disk (int): free disk space in GB
1510 staging_thread_count (int): number of devserver threads currently staging
1511 an image
Dan Shi7247f9c2016-06-01 09:19:09 -07001512 apache_client_count (int): count of Apache processes.
1513 telemetry_test_count (int): count of telemetry tests.
1514 gsutil_count (int): count of gsutil processes.
Dan Shif5ce2de2013-04-25 16:06:32 -07001515 """
1516 # Get free disk space.
1517 stat = os.statvfs(updater.static_dir)
1518 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
Dan Shi7247f9c2016-06-01 09:19:09 -07001519 apache_client_count = self._get_process_count('apache')
1520 telemetry_test_count = self._get_process_count('python.*telemetry')
1521 gsutil_count = self._get_process_count('gsutil')
Dan Shif5ce2de2013-04-25 16:06:32 -07001522
Dan Shiafd0e492015-05-27 14:23:51 -07001523 health_data = {
Dan Shif5ce2de2013-04-25 16:06:32 -07001524 'free_disk': free_disk,
Dan Shid76e6bb2016-01-28 22:28:51 -08001525 'staging_thread_count': DevServerRoot._staging_thread_count,
1526 'apache_client_count': apache_client_count,
Dan Shi7247f9c2016-06-01 09:19:09 -07001527 'telemetry_test_count': telemetry_test_count,
1528 'gsutil_count': gsutil_count}
Dan Shiafd0e492015-05-27 14:23:51 -07001529 health_data.update(self._get_io_stats() or {})
1530
1531 return json.dumps(health_data)
Dan Shif5ce2de2013-04-25 16:06:32 -07001532
1533
Chris Sosadbc20082012-12-10 13:39:11 -08001534def _CleanCache(cache_dir, wipe):
1535 """Wipes any excess cached items in the cache_dir.
1536
1537 Args:
1538 cache_dir: the directory we are wiping from.
1539 wipe: If True, wipe all the contents -- not just the excess.
1540 """
1541 if wipe:
1542 # Clear the cache and exit on error.
1543 cmd = 'rm -rf %s/*' % cache_dir
1544 if os.system(cmd) != 0:
1545 _Log('Failed to clear the cache with %s' % cmd)
1546 sys.exit(1)
1547 else:
1548 # Clear all but the last N cached updates
1549 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
1550 (cache_dir, CACHED_ENTRIES))
1551 if os.system(cmd) != 0:
1552 _Log('Failed to clean up old delta cache files with %s' % cmd)
1553 sys.exit(1)
1554
1555
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001556def _AddTestingOptions(parser):
1557 group = optparse.OptionGroup(
1558 parser, 'Advanced Testing Options', 'These are used by test scripts and '
1559 'developers writing integration tests utilizing the devserver. They are '
1560 'not intended to be really used outside the scope of someone '
1561 'knowledgable about the test.')
1562 group.add_option('--exit',
1563 action='store_true',
1564 help='do not start the server (yet pregenerate/clear cache)')
1565 group.add_option('--host_log',
1566 action='store_true', default=False,
1567 help='record history of host update events (/api/hostlog)')
1568 group.add_option('--max_updates',
Gabe Black3b567202015-09-23 14:07:59 -07001569 metavar='NUM', default=-1, type='int',
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001570 help='maximum number of update checks handled positively '
1571 '(default: unlimited)')
1572 group.add_option('--private_key',
1573 metavar='PATH', default=None,
1574 help='path to the private key in pem format. If this is set '
1575 'the devserver will generate update payloads that are '
1576 'signed with this key.')
David Zeuthen52ccd012013-10-31 12:58:26 -07001577 group.add_option('--private_key_for_metadata_hash_signature',
1578 metavar='PATH', default=None,
1579 help='path to the private key in pem format. If this is set '
1580 'the devserver will sign the metadata hash with the given '
1581 'key and transmit in the Omaha-style XML response.')
1582 group.add_option('--public_key',
1583 metavar='PATH', default=None,
1584 help='path to the public key in pem format. If this is set '
1585 'the devserver will transmit a base64 encoded version of '
1586 'the content in the Omaha-style XML response.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001587 group.add_option('--proxy_port',
1588 metavar='PORT', default=None, type='int',
1589 help='port to have the client connect to -- basically the '
1590 'devserver lies to the update to tell it to get the payload '
1591 'from a different port that will proxy the request back to '
1592 'the devserver. The proxy must be managed outside the '
1593 'devserver.')
1594 group.add_option('--remote_payload',
1595 action='store_true', default=False,
Chris Sosa4b951602014-04-09 20:26:07 -07001596 help='Payload is being served from a remote machine. With '
1597 'this setting enabled, this devserver instance serves as '
1598 'just an Omaha server instance. In this mode, the '
1599 'devserver enforces a few extra components of the Omaha '
Chris Sosafc715442014-04-09 20:45:23 -07001600 'protocol, such as hardware class, being sent.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001601 group.add_option('-u', '--urlbase',
1602 metavar='URL',
Gabe Black3b567202015-09-23 14:07:59 -07001603 help='base URL for update images, other than the '
1604 'devserver. Use in conjunction with remote_payload.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001605 parser.add_option_group(group)
1606
1607
1608def _AddUpdateOptions(parser):
1609 group = optparse.OptionGroup(
1610 parser, 'Autoupdate Options', 'These options can be used to change '
1611 'how the devserver either generates or serve update payloads. Please '
1612 'note that all of these option affect how a payload is generated and so '
1613 'do not work in archive-only mode.')
1614 group.add_option('--board',
1615 help='By default the devserver will create an update '
1616 'payload from the latest image built for the board '
1617 'a device that is requesting an update has. When we '
1618 'pre-generate an update (see below) and we do not specify '
1619 'another update_type option like image or payload, the '
1620 'devserver needs to know the board to generate the latest '
1621 'image for. This is that board.')
1622 group.add_option('--critical_update',
1623 action='store_true', default=False,
1624 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001625 group.add_option('--image',
1626 metavar='FILE',
1627 help='Generate and serve an update using this image to any '
1628 'device that requests an update.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001629 group.add_option('--payload',
1630 metavar='PATH',
1631 help='use the update payload from specified directory '
1632 '(update.gz).')
1633 group.add_option('-p', '--pregenerate_update',
1634 action='store_true', default=False,
1635 help='pre-generate the update payload before accepting '
1636 'update requests. Useful to help debug payload generation '
1637 'issues quickly. Also if an update payload will take a '
1638 'long time to generate, a client may timeout if you do not'
1639 'pregenerate the update.')
1640 group.add_option('--src_image',
1641 metavar='PATH', default='',
1642 help='If specified, delta updates will be generated using '
1643 'this image as the source image. Delta updates are when '
1644 'you are updating from a "source image" to a another '
1645 'image.')
1646 parser.add_option_group(group)
1647
1648
1649def _AddProductionOptions(parser):
1650 group = optparse.OptionGroup(
1651 parser, 'Advanced Server Options', 'These options can be used to changed '
1652 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001653 group.add_option('--clear_cache',
1654 action='store_true', default=False,
1655 help='At startup, removes all cached entries from the'
1656 'devserver\'s cache.')
1657 group.add_option('--logfile',
1658 metavar='PATH',
1659 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -07001660 group.add_option('--pidfile',
1661 metavar='PATH',
1662 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 11:04:13 -08001663 group.add_option('--portfile',
1664 metavar='PATH',
1665 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001666 group.add_option('--production',
1667 action='store_true', default=False,
1668 help='have the devserver use production values when '
1669 'starting up. This includes using more threads and '
1670 'performing less logging.')
1671 parser.add_option_group(group)
1672
1673
Paul Hobbsef4e0702016-06-27 17:01:42 -07001674def MakeLogHandler(logfile):
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001675 """Create a LogHandler instance used to log all messages."""
1676 hdlr_cls = handlers.TimedRotatingFileHandler
1677 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
xixuan3d48bff2017-01-30 19:00:09 -08001678 interval=_LOG_ROTATION_INTERVAL,
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001679 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -07001680 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001681 return hdlr
1682
1683
Chris Sosacde6bf42012-05-31 18:36:39 -07001684def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001685 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -08001686 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -07001687
1688 # get directory that the devserver is run from
1689 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -07001690 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -07001691 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001692 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -07001693 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -07001694 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001695 parser.add_option('--port',
1696 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 13:13:28 -08001697 help=('port for the dev server to use; if zero, binds to '
1698 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001699 parser.add_option('-t', '--test_image',
1700 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -07001701 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -07001702 parser.add_option('-x', '--xbuddy_manage_builds',
1703 action='store_true',
1704 default=False,
1705 help='If set, allow xbuddy to manage images in'
1706 'build/images.')
Dan Shi72b16132015-10-08 12:10:33 -07001707 parser.add_option('-a', '--android_build_credential',
1708 default=None,
1709 help='Path to a json file which contains the credential '
1710 'needed to access Android builds.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001711 _AddProductionOptions(parser)
1712 _AddUpdateOptions(parser)
1713 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -07001714 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001715
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001716 # Handle options that must be set globally in cherrypy. Do this
1717 # work up front, because calls to _Log() below depend on this
1718 # initialization.
1719 if options.production:
1720 cherrypy.config.update({'environment': 'production'})
1721 if not options.logfile:
1722 cherrypy.config.update({'log.screen': True})
1723 else:
1724 cherrypy.config.update({'log.error_file': '',
1725 'log.access_file': ''})
Paul Hobbsef4e0702016-06-27 17:01:42 -07001726 hdlr = MakeLogHandler(options.logfile)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001727 # Pylint can't seem to process these two calls properly
1728 # pylint: disable=E1101
1729 cherrypy.log.access_log.addHandler(hdlr)
1730 cherrypy.log.error_log.addHandler(hdlr)
1731 # pylint: enable=E1101
1732
joychened64b222013-06-21 16:39:34 -07001733 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001734 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001735
joychened64b222013-06-21 16:39:34 -07001736 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001737 # If our devserver is only supposed to serve payloads, we shouldn't be
1738 # mucking with the cache at all. If the devserver hadn't previously
1739 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001740 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001741 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001742 else:
1743 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001744
Chris Sosadbc20082012-12-10 13:39:11 -08001745 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 16:39:34 -07001746 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001747
joychen121fc9b2013-08-02 14:30:30 -07001748 _xbuddy = xbuddy.XBuddy(options.xbuddy_manage_builds,
1749 options.board,
joychen121fc9b2013-08-02 14:30:30 -07001750 static_dir=options.static_dir)
Chris Sosa75490802013-09-30 17:21:45 -07001751 if options.clear_cache and options.xbuddy_manage_builds:
1752 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 14:30:30 -07001753
Chris Sosa6a3697f2013-01-29 16:44:43 -08001754 # We allow global use here to share with cherrypy classes.
1755 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001756 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001757 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001758 _xbuddy,
joychened64b222013-06-21 16:39:34 -07001759 static_dir=options.static_dir,
Andrew de los Reyes52620802010-04-12 13:40:07 -07001760 urlbase=options.urlbase,
Chris Sosa5d342a22010-09-28 16:54:41 -07001761 forced_image=options.image,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001762 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001763 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-28 23:42:37 -07001764 src_image=options.src_image,
Chris Sosa08d55a22011-01-19 16:08:02 -08001765 board=options.board,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001766 copy_to_static_root=not options.exit,
1767 private_key=options.private_key,
Gabe Black3b567202015-09-23 14:07:59 -07001768 private_key_for_metadata_hash_signature=(
1769 options.private_key_for_metadata_hash_signature),
David Zeuthen52ccd012013-10-31 12:58:26 -07001770 public_key=options.public_key,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001771 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001772 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001773 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001774 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001775 )
Chris Sosa7c931362010-10-11 19:49:01 -07001776
Chris Sosa6a3697f2013-01-29 16:44:43 -08001777 if options.pregenerate_update:
1778 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 15:46:22 -07001779
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001780 if options.exit:
1781 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001782
joychen3cb228e2013-06-12 12:13:13 -07001783 dev_server = DevServerRoot(_xbuddy)
1784
Gilad Arnold11fbef42014-02-10 11:04:13 -08001785 # Patch CherryPy to support binding to any available port (--port=0).
1786 cherrypy_ext.ZeroPortPatcher.DoPatch(cherrypy)
1787
Chris Sosa855b8932013-08-21 13:24:55 -07001788 if options.pidfile:
1789 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1790
Gilad Arnold11fbef42014-02-10 11:04:13 -08001791 if options.portfile:
1792 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1793
Dan Shiafd5c6c2016-01-07 10:27:03 -08001794 if (options.android_build_credential and
1795 os.path.exists(options.android_build_credential)):
1796 try:
1797 with open(options.android_build_credential) as f:
1798 android_build.BuildAccessor.credential_info = json.load(f)
1799 except ValueError as e:
1800 _Log('Failed to load the android build credential: %s. Error: %s.' %
1801 (options.android_build_credential, e))
joychen3cb228e2013-06-12 12:13:13 -07001802 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001803
1804
1805if __name__ == '__main__':
1806 main()