blob: fbaa771760deed8114f408bf109d3a6883c5ab62 [file] [log] [blame]
David Riley2fcb0122017-11-02 18:25:391#!/usr/bin/env python2
Luis Hector Chavezdca9dd72018-06-12 19:56:302# -*- coding: utf-8 -*-
Chris Sosa781ba6d2012-04-11 19:44:433# Copyright (c) 2009-2012 The Chromium OS Authors. All rights reserved.
[email protected]ded22402009-10-26 22:36:214# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
Chris Sosa3ae4dc12013-03-29 18:47:007"""Chromium OS development server that can be used for all forms of update.
8
9This devserver can be used to perform system-wide autoupdate and update
10of specific portage packages on devices running Chromium OS derived operating
Amin Hassanie9ffb862019-09-26 00:10:4011systems.
Chris Sosa3ae4dc12013-03-29 18:47:0012
Amin Hassanie9ffb862019-09-26 00:10:4013The devserver is configured to stage and
Chris Sosa3ae4dc12013-03-29 18:47:0014serve 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
Chris Sosa3ae4dc12013-03-29 18:47:0021For autoupdates, there are many more advanced options that can help specify
22how to update and which payload to give to a requester.
23"""
24
Gabe Black3b567202015-09-23 21:07:5925from __future__ import print_function
Chris Sosa7c931362010-10-12 02:49:0126
Gilad Arnold55a2a372012-10-02 16:46:3227import json
David Riley2fcb0122017-11-02 18:25:3928import optparse # pylint: disable=deprecated-module
[email protected]ded22402009-10-26 22:36:2129import os
Scott Zawalski4647ce62012-01-03 22:17:2830import re
Simran Basi4baad082013-02-14 21:39:1831import shutil
xixuan52c2fba2016-05-21 00:02:4832import signal
Mandeep Singh Baines38dcdda2012-12-08 01:55:3333import socket
Chris Masone816e38c2012-05-02 19:22:3634import subprocess
J. Richard Barnette3d977b82013-04-23 18:05:1935import sys
Chris Masone816e38c2012-05-02 19:22:3636import tempfile
Dan Shi59ae7092013-06-04 21:37:2737import threading
Gilad Arnoldd5ebaaa2012-10-02 18:52:3838import types
J. Richard Barnette3d977b82013-04-23 18:05:1939from logging import handlers
40
Amin Hassanid4e35392019-10-03 18:02:4441from six.moves import http_client
42
43# pylint: disable=no-name-in-module, import-error
J. Richard Barnette3d977b82013-04-23 18:05:1944import cherrypy
Chris Sosa855b8932013-08-21 20:24:5545from cherrypy import _cplogging as cplogging
Amin Hassanid4e35392019-10-03 18:02:4446from cherrypy.process import plugins
47# pylint: enable=no-name-in-module, import-error
[email protected]ded22402009-10-26 22:36:2148
Achuith Bhandarkar662fb722019-10-31 23:12:4949import autoupdate
50import cherrypy_ext
Achuith Bhandarkar662fb722019-10-31 23:12:4951import health_checker
52
Richard Barnettedf35c322017-08-19 00:02:1353# This must happen before any local modules get a chance to import
54# anything from chromite. Otherwise, really bad things will happen, and
55# you will _not_ understand why.
Congbin Guo3afae6c2019-08-13 23:29:4256import setup_chromite # pylint: disable=unused-import
Amin Hassanie427e212019-10-28 18:04:2757from chromite.lib import cros_update_progress
Achuith Bhandarkar662fb722019-10-31 23:12:4958from chromite.lib.xbuddy import android_build
59from chromite.lib.xbuddy import artifact_info
60from chromite.lib.xbuddy import build_artifact
61from chromite.lib.xbuddy import cherrypy_log_util
62from chromite.lib.xbuddy import common_util
63from chromite.lib.xbuddy import devserver_constants
64from chromite.lib.xbuddy import downloader
65from chromite.lib.xbuddy import xbuddy
Gilad Arnoldc65330c2012-09-20 22:17:4866
Gilad Arnoldc65330c2012-09-20 22:17:4867# Module-local log function.
Chris Sosa6a3697f2013-01-30 00:44:4368def _Log(message, *args):
Achuith Bhandarkar662fb722019-10-31 23:12:4969 return cherrypy_log_util.LogWithTag('DEVSERVER', message, *args)
Frank Farzan40160872011-12-13 02:39:1870
Chris Sosa417e55d2011-01-26 00:40:4871CACHED_ENTRIES = 12
Don Garrettf90edf02010-11-17 01:36:1472
Simran Basi4baad082013-02-14 21:39:1873TELEMETRY_FOLDER = 'telemetry_src'
74TELEMETRY_DEPS = ['dep-telemetry_dep.tar.bz2',
75 'dep-page_cycler_dep.tar.bz2',
Simran Basi0d078682013-03-22 23:40:0476 'dep-chrome_test.tar.bz2',
77 'dep-perf_data_dep.tar.bz2']
Simran Basi4baad082013-02-14 21:39:1878
Chris Sosa0356d3b2010-09-16 22:46:2279# Sets up global to share between classes.
[email protected]21a5ca32009-11-04 18:23:2380updater = None
[email protected]ded22402009-10-26 22:36:2181
xixuan3d48bff2017-01-31 03:00:0982# Log rotation parameters. These settings correspond to twice a day once
83# devserver is started, with about two weeks (28 backup files) of old logs
84# kept for backup.
J. Richard Barnette3d977b82013-04-23 18:05:1985#
xixuan3d48bff2017-01-31 03:00:0986# For more, see the documentation in standard python library for
J. Richard Barnette3d977b82013-04-23 18:05:1987# logging.handlers.TimedRotatingFileHandler
xixuan3d48bff2017-01-31 03:00:0988_LOG_ROTATION_TIME = 'H'
Congbin Guo3afae6c2019-08-13 23:29:4289_LOG_ROTATION_INTERVAL = 12 # hours
90_LOG_ROTATION_BACKUP = 28 # backup counts
Frank Farzan40160872011-12-13 02:39:1891
xixuan52c2fba2016-05-21 00:02:4892# Auto-update parameters
93
94# Error msg for missing key in CrOS auto-update.
Xixuan Wu32af9f12017-11-13 22:11:4495KEY_ERROR_MSG = 'Key Error in RPC: %s= is required'
xixuan52c2fba2016-05-21 00:02:4896
Sanika Kulkarnid4496fd2020-02-05 01:26:2597# Error msg for deprecated RPC usage.
98DEPRECATED_RPC_ERROR_MSG = ('The %s RPC has been deprecated. Usage of this '
99 'RPC is discouraged. Please go to '
100 'go/devserver-deprecation for more information.')
101
xixuan52c2fba2016-05-21 00:02:48102
Amin Hassanid4e35392019-10-03 18:02:44103class DevServerError(Exception):
104 """Exception class used by DevServer."""
105
106
Sanika Kulkarnid4496fd2020-02-05 01:26:25107class DeprecatedRPCError(DevServerError):
108 """Exception class used when an RPC is deprecated but is still being used."""
109
110 def __init__(self, rpc_name):
111 """Constructor for DeprecatedRPCError class.
112
113 :param rpc_name: (str) name of the RPC that has been deprecated.
114 """
Amin Hassani6ecda232020-03-10 02:03:23115 super(DeprecatedRPCError, self).__init__(
116 DEPRECATED_RPC_ERROR_MSG % rpc_name)
Sanika Kulkarnid4496fd2020-02-05 01:26:25117 self.rpc_name = rpc_name
118
119
Amin Hassani722e0962019-11-15 23:45:31120class DevServerHTTPError(cherrypy.HTTPError):
121 """Exception class to log the HTTPResponse before routing it to cherrypy."""
122 def __init__(self, status, message):
123 """CherryPy error with logging.
124
125 Args:
126 status: HTTPResponse status.
127 message: Message associated with the response.
128 """
129 cherrypy.HTTPError.__init__(self, status, message)
130 _Log('HTTPError status: %s message: %s', status, message)
131
132
Gabe Black3b567202015-09-23 21:07:59133def _canonicalize_archive_url(archive_url):
134 """Canonicalizes archive_url strings.
135
136 Raises:
137 DevserverError: if archive_url is not set.
138 """
139 if archive_url:
140 if not archive_url.startswith('gs://'):
Amin Hassanid4e35392019-10-03 18:02:44141 raise DevServerError(
Congbin Guo4132a272019-08-20 19:32:14142 "Archive URL isn't from Google Storage (%s) ." % archive_url)
Gabe Black3b567202015-09-23 21:07:59143
144 return archive_url.rstrip('/')
145 else:
Amin Hassanid4e35392019-10-03 18:02:44146 raise DevServerError('Must specify an archive_url in the request')
Gabe Black3b567202015-09-23 21:07:59147
148
149def _canonicalize_local_path(local_path):
150 """Canonicalizes |local_path| strings.
151
152 Raises:
153 DevserverError: if |local_path| is not set.
154 """
155 # Restrict staging of local content to only files within the static
156 # directory.
157 local_path = os.path.abspath(local_path)
158 if not local_path.startswith(updater.static_dir):
Amin Hassanid4e35392019-10-03 18:02:44159 raise DevServerError(
Congbin Guo4132a272019-08-20 19:32:14160 'Local path %s must be a subdirectory of the static'
161 ' directory: %s' % (local_path, updater.static_dir))
Gabe Black3b567202015-09-23 21:07:59162
163 return local_path.rstrip('/')
164
165
166def _get_artifacts(kwargs):
167 """Returns a tuple of named and file artifacts given the stage rpc kwargs.
168
169 Raises:
170 DevserverError if no artifacts would be returned.
171 """
172 artifacts = kwargs.get('artifacts')
173 files = kwargs.get('files')
174 if not artifacts and not files:
Amin Hassanid4e35392019-10-03 18:02:44175 raise DevServerError('No artifacts specified.')
Gabe Black3b567202015-09-23 21:07:59176
177 # Note we NEED to coerce files to a string as we get raw unicode from
178 # cherrypy and we treat files as strings elsewhere in the code.
179 return (str(artifacts).split(',') if artifacts else [],
180 str(files).split(',') if files else [])
181
182
Dan Shi61305df2015-10-26 23:52:35183def _is_android_build_request(kwargs):
184 """Check if a devserver call is for Android build, based on the arguments.
185
186 This method exams the request's arguments (os_type) to determine if the
187 request is for Android build. If os_type is set to `android`, returns True.
188 If os_type is not set or has other values, returns False.
189
190 Args:
191 kwargs: Keyword arguments for the request.
192
193 Returns:
194 True if the request is for Android build. False otherwise.
195 """
196 os_type = kwargs.get('os_type', None)
197 return os_type == 'android'
198
199
Gabe Black3b567202015-09-23 21:07:59200def _get_downloader(kwargs):
201 """Returns the downloader based on passed in arguments.
202
203 Args:
Amin Hassani08e42d22019-06-03 07:31:30204 kwargs: Keyword arguments for the request.
Gabe Black3b567202015-09-23 21:07:59205 """
206 local_path = kwargs.get('local_path')
207 if local_path:
208 local_path = _canonicalize_local_path(local_path)
209
210 dl = None
211 if local_path:
Prathmesh Prabhu58d08932018-01-19 23:08:19212 delete_source = _parse_boolean_arg(kwargs, 'delete_source')
213 dl = downloader.LocalDownloader(updater.static_dir, local_path,
214 delete_source=delete_source)
Gabe Black3b567202015-09-23 21:07:59215
Dan Shi61305df2015-10-26 23:52:35216 if not _is_android_build_request(kwargs):
Gabe Black3b567202015-09-23 21:07:59217 archive_url = kwargs.get('archive_url')
218 if not archive_url and not local_path:
Amin Hassanid4e35392019-10-03 18:02:44219 raise DevServerError(
Congbin Guo4132a272019-08-20 19:32:14220 'Requires archive_url or local_path to be specified.')
Gabe Black3b567202015-09-23 21:07:59221 if archive_url and local_path:
Amin Hassanid4e35392019-10-03 18:02:44222 raise DevServerError(
Congbin Guo4132a272019-08-20 19:32:14223 'archive_url and local_path can not both be specified.')
Gabe Black3b567202015-09-23 21:07:59224 if not dl:
225 archive_url = _canonicalize_archive_url(archive_url)
Luis Hector Chavezdca9dd72018-06-12 19:56:30226 dl = downloader.GoogleStorageDownloader(
227 updater.static_dir, archive_url,
228 downloader.GoogleStorageDownloader.GetBuildIdFromArchiveURL(
229 archive_url))
Gabe Black3b567202015-09-23 21:07:59230 elif not dl:
231 target = kwargs.get('target', None)
Dan Shi72b16132015-10-08 19:10:33232 branch = kwargs.get('branch', None)
Dan Shi61305df2015-10-26 23:52:35233 build_id = kwargs.get('build_id', None)
234 if not target or not branch or not build_id:
Amin Hassanid4e35392019-10-03 18:02:44235 raise DevServerError('target, branch, build ID must all be specified for '
236 'downloading Android build.')
Dan Shi72b16132015-10-08 19:10:33237 dl = downloader.AndroidBuildDownloader(updater.static_dir, branch, build_id,
238 target)
Gabe Black3b567202015-09-23 21:07:59239
240 return dl
241
242
243def _get_downloader_and_factory(kwargs):
244 """Returns the downloader and artifact factory based on passed in arguments.
245
246 Args:
Amin Hassani08e42d22019-06-03 07:31:30247 kwargs: Keyword arguments for the request.
Gabe Black3b567202015-09-23 21:07:59248 """
249 artifacts, files = _get_artifacts(kwargs)
250 dl = _get_downloader(kwargs)
251
Achuith Bhandarkar2a1fcd82019-10-18 00:45:58252 if (isinstance(dl, (downloader.GoogleStorageDownloader,
253 downloader.LocalDownloader))):
Gabe Black3b567202015-09-23 21:07:59254 factory_class = build_artifact.ChromeOSArtifactFactory
Dan Shi72b16132015-10-08 19:10:33255 elif isinstance(dl, downloader.AndroidBuildDownloader):
Gabe Black3b567202015-09-23 21:07:59256 factory_class = build_artifact.AndroidArtifactFactory
257 else:
Amin Hassanid4e35392019-10-03 18:02:44258 raise DevServerError(
Congbin Guo4132a272019-08-20 19:32:14259 'Unrecognized value for downloader type: %s' % type(dl))
Gabe Black3b567202015-09-23 21:07:59260
261 factory = factory_class(dl.GetBuildDir(), artifacts, files, dl.GetBuild())
262
263 return dl, factory
264
265
Scott Zawalski4647ce62012-01-03 22:17:28266def _LeadingWhiteSpaceCount(string):
267 """Count the amount of leading whitespace in a string.
268
269 Args:
270 string: The string to count leading whitespace in.
Don Garrettf84631a2014-01-08 02:21:26271
Scott Zawalski4647ce62012-01-03 22:17:28272 Returns:
273 number of white space chars before characters start.
274 """
Gabe Black3b567202015-09-23 21:07:59275 matched = re.match(r'^\s+', string)
Scott Zawalski4647ce62012-01-03 22:17:28276 if matched:
277 return len(matched.group())
278
279 return 0
280
281
282def _PrintDocStringAsHTML(func):
283 """Make a functions docstring somewhat HTML style.
284
285 Args:
286 func: The function to return the docstring from.
Don Garrettf84631a2014-01-08 02:21:26287
Scott Zawalski4647ce62012-01-03 22:17:28288 Returns:
289 A string that is somewhat formated for a web browser.
290 """
291 # TODO(scottz): Make this parse Args/Returns in a prettier way.
292 # Arguments could be bolded and indented etc.
293 html_doc = []
294 for line in func.__doc__.splitlines():
295 leading_space = _LeadingWhiteSpaceCount(line)
296 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 18:26:55297 line = ' ' * leading_space + line
Scott Zawalski4647ce62012-01-03 22:17:28298
299 html_doc.append('<BR>%s' % line)
300
301 return '\n'.join(html_doc)
302
303
Simran Basief83d6a2014-08-28 21:32:01304def _GetUpdateTimestampHandler(static_dir):
305 """Returns a handler to update directory staged.timestamp.
306
307 This handler resets the stage.timestamp whenever static content is accessed.
308
309 Args:
310 static_dir: Directory from which static content is being staged.
311
312 Returns:
Amin Hassani08e42d22019-06-03 07:31:30313 A cherrypy handler to update the timestamp of accessed content.
Simran Basief83d6a2014-08-28 21:32:01314 """
315 def UpdateTimestampHandler():
316 if not '404' in cherrypy.response.status:
317 build_match = re.match(devserver_constants.STAGED_BUILD_REGEX,
318 cherrypy.request.path_info)
319 if build_match:
320 build_dir = os.path.join(static_dir, build_match.group('build'))
321 downloader.Downloader.TouchTimestampForStaged(build_dir)
322 return UpdateTimestampHandler
323
324
Chris Sosa7c931362010-10-12 02:49:01325def _GetConfig(options):
326 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-08 01:55:33327
Mandeep Singh Baines38dcdda2012-12-08 01:55:33328 socket_host = '::'
Yu-Ju Hongc8d4af32013-11-12 23:14:26329 # Fall back to IPv4 when python is not configured with IPv6.
330 if not socket.has_ipv6:
Mandeep Singh Baines38dcdda2012-12-08 01:55:33331 socket_host = '0.0.0.0'
332
Simran Basief83d6a2014-08-28 21:32:01333 # Adds the UpdateTimestampHandler to cherrypy's tools. This tools executes
334 # on the on_end_resource hook. This hook is called once processing is
335 # complete and the response is ready to be returned.
336 cherrypy.tools.update_timestamp = cherrypy.Tool(
337 'on_end_resource', _GetUpdateTimestampHandler(options.static_dir))
338
David Riley2fcb0122017-11-02 18:25:39339 base_config = {
340 'global': {
341 'server.log_request_headers': True,
342 'server.protocol_version': 'HTTP/1.1',
343 'server.socket_host': socket_host,
344 'server.socket_port': int(options.port),
345 'response.timeout': 6000,
346 'request.show_tracebacks': True,
347 'server.socket_timeout': 60,
348 'server.thread_pool': 2,
349 'engine.autoreload.on': False,
350 },
351 '/api': {
352 # Gets rid of cherrypy parsing post file for args.
353 'request.process_request_body': False,
354 },
355 '/build': {
356 'response.timeout': 100000,
357 },
358 '/update': {
359 # Gets rid of cherrypy parsing post file for args.
360 'request.process_request_body': False,
361 'response.timeout': 10000,
362 },
363 # Sets up the static dir for file hosting.
364 '/static': {
365 'tools.staticdir.dir': options.static_dir,
366 'tools.staticdir.on': True,
367 'response.timeout': 10000,
368 'tools.update_timestamp.on': True,
369 },
370 }
Chris Sosa5f118ef2012-07-12 18:37:50371 if options.production:
Alex Miller93beca52013-07-31 02:25:09372 base_config['global'].update({'server.thread_pool': 150})
Scott Zawalski1c5e7cd2012-02-27 18:12:52373
Chris Sosa7c931362010-10-12 02:49:01374 return base_config
[email protected]64244662009-11-12 00:52:08375
Darin Petkove17164a2010-08-11 20:24:41376
Gilad Arnoldd5ebaaa2012-10-02 18:52:38377def _GetRecursiveMemberObject(root, member_list):
378 """Returns an object corresponding to a nested member list.
379
380 Args:
381 root: the root object to search
382 member_list: list of nested members to search
Don Garrettf84631a2014-01-08 02:21:26383
Gilad Arnoldd5ebaaa2012-10-02 18:52:38384 Returns:
385 An object corresponding to the member name list; None otherwise.
386 """
387 for member in member_list:
388 next_root = root.__class__.__dict__.get(member)
389 if not next_root:
390 return None
391 root = next_root
392 return root
393
394
395def _IsExposed(name):
396 """Returns True iff |name| has an `exposed' attribute and it is set."""
397 return hasattr(name, 'exposed') and name.exposed
398
399
Congbin Guo6bc32182019-08-21 00:54:30400def _GetExposedMethod(nested_member):
Gilad Arnoldd5ebaaa2012-10-02 18:52:38401 """Returns a CherryPy-exposed method, if such exists.
402
403 Args:
Gilad Arnoldd5ebaaa2012-10-02 18:52:38404 nested_member: a slash-joined path to the nested member
Don Garrettf84631a2014-01-08 02:21:26405
Gilad Arnoldd5ebaaa2012-10-02 18:52:38406 Returns:
Congbin Guo6bc32182019-08-21 00:54:30407 A function object corresponding to the path defined by |nested_member| from
408 the app root object registered, if the function is exposed; None otherwise.
Gilad Arnoldd5ebaaa2012-10-02 18:52:38409 """
Congbin Guo6bc32182019-08-21 00:54:30410 for app in cherrypy.tree.apps.values():
411 # Use the 'index' function doc as the doc of the app.
412 if nested_member == app.script_name.lstrip('/'):
413 nested_member = 'index'
414
415 method = _GetRecursiveMemberObject(app.root, nested_member.split('/'))
416 if method and isinstance(method, types.FunctionType) and _IsExposed(method):
417 return method
Gilad Arnoldd5ebaaa2012-10-02 18:52:38418
419
Gilad Arnold748c8322012-10-12 16:51:35420def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 18:52:38421 """Finds exposed CherryPy methods.
422
423 Args:
424 root: the root object for searching
425 prefix: slash-joined chain of members leading to current object
426 unlisted: URLs to be excluded regardless of their exposed status
Don Garrettf84631a2014-01-08 02:21:26427
Gilad Arnoldd5ebaaa2012-10-02 18:52:38428 Returns:
429 List of exposed URLs that are not unlisted.
430 """
431 method_list = []
Congbin Guo6bc32182019-08-21 00:54:30432 for member in root.__class__.__dict__.keys():
Gilad Arnoldd5ebaaa2012-10-02 18:52:38433 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 16:51:35434 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 18:52:38435 continue
436 member_obj = root.__class__.__dict__[member]
437 if _IsExposed(member_obj):
Amin Hassani08e42d22019-06-03 07:31:30438 if isinstance(member_obj, types.FunctionType):
Congbin Guo6bc32182019-08-21 00:54:30439 # Regard the app name as exposed "method" name if it exposed 'index'
440 # function.
441 if prefix and member == 'index':
442 method_list.append(prefix)
443 else:
444 method_list.append(prefixed_member)
Gilad Arnoldd5ebaaa2012-10-02 18:52:38445 else:
446 method_list += _FindExposedMethods(
447 member_obj, prefixed_member, unlisted)
448 return method_list
449
450
xixuan52c2fba2016-05-21 00:02:48451def _check_base_args_for_auto_update(kwargs):
xixuanac89ce82016-12-01 00:48:20452 """Check basic args required for auto-update.
453
454 Args:
455 kwargs: the parameters to be checked.
456
457 Raises:
458 DevServerHTTPError if required parameters don't exist in kwargs.
459 """
xixuan52c2fba2016-05-21 00:02:48460 if 'host_name' not in kwargs:
Amin Hassani722e0962019-11-15 23:45:31461 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
462 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-21 00:02:48463
464 if 'build_name' not in kwargs:
Amin Hassani722e0962019-11-15 23:45:31465 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
466 KEY_ERROR_MSG % 'build_name')
xixuan52c2fba2016-05-21 00:02:48467
468
469def _parse_boolean_arg(kwargs, key):
xixuanac89ce82016-12-01 00:48:20470 """Parse boolean arg from kwargs.
471
472 Args:
473 kwargs: the parameters to be checked.
474 key: the key to be parsed.
475
476 Returns:
477 The boolean value of kwargs[key], or False if key doesn't exist in kwargs.
478
479 Raises:
480 DevServerHTTPError if kwargs[key] is not a boolean variable.
481 """
xixuan52c2fba2016-05-21 00:02:48482 if key in kwargs:
483 if kwargs[key] == 'True':
484 return True
485 elif kwargs[key] == 'False':
486 return False
487 else:
Amin Hassani722e0962019-11-15 23:45:31488 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
489 'The value for key %s is not boolean.' % key)
xixuan52c2fba2016-05-21 00:02:48490 else:
491 return False
492
xixuan447ad9d2017-02-28 22:46:20493
xixuanac89ce82016-12-01 00:48:20494def _parse_string_arg(kwargs, key):
495 """Parse string arg from kwargs.
496
497 Args:
498 kwargs: the parameters to be checked.
499 key: the key to be parsed.
500
501 Returns:
502 The string value of kwargs[key], or None if key doesn't exist in kwargs.
503 """
504 if key in kwargs:
505 return kwargs[key]
506 else:
507 return None
508
xixuan447ad9d2017-02-28 22:46:20509
xixuanac89ce82016-12-01 00:48:20510def _build_uri_from_build_name(build_name):
511 """Get build url from a given build name.
512
513 Args:
514 build_name: the build name to be parsed, whose format is
515 'board/release_version'.
516
517 Returns:
518 The release_archive_url on Google Storage for this build name.
519 """
Amin Hassani08e42d22019-06-03 07:31:30520 # TODO(ahassani): This function doesn't seem to be used anywhere since its
521 # previous use of lib.paygen.gspath was broken and it doesn't seem to be
522 # causing any runtime issues. So deprecate this in the future.
523 tokens = build_name.split('/')
524 return 'gs://chromeos-releases/stable-channel/%s/%s' % (tokens[0], tokens[1])
xixuan52c2fba2016-05-21 00:02:48525
xixuan447ad9d2017-02-28 22:46:20526
527def _clear_process(host_name, pid):
528 """Clear AU process for given hostname and pid.
529
530 This clear includes:
531 1. kill process if it's alive.
532 2. delete the track status file of this process.
533 3. delete the executing log file of this process.
534
535 Args:
536 host_name: the host to execute auto-update.
537 pid: the background auto-update process id.
538 """
539 if cros_update_progress.IsProcessAlive(pid):
540 os.killpg(int(pid), signal.SIGKILL)
541
542 cros_update_progress.DelTrackStatusFile(host_name, pid)
543 cros_update_progress.DelExecuteLogFile(host_name, pid)
544
545
Sanika Kulkarnid4496fd2020-02-05 01:26:25546def is_deprecated_server():
547 """Gets whether the devserver has deprecated RPCs."""
548 return cherrypy.config.get('infra_removal', False)
549
550
Dale Curtisc9aaf3a2011-08-09 22:47:40551class ApiRoot(object):
552 """RESTful API for Dev Server information."""
553 exposed = True
554
555 @cherrypy.expose
Gilad Arnold286a0062012-01-12 21:47:02556 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 18:36:27557 """Returns a JSON object containing a log of host event.
558
559 Args:
560 ip: address of host whose event log is requested, or `all'
Don Garrettf84631a2014-01-08 02:21:26561
Gilad Arnold1b908392012-10-05 18:36:27562 Returns:
Amin Hassani7c447852019-09-26 22:01:48563 A JSON dictionary containing all or some of the following fields:
Amin Hassanie7ead902019-10-11 23:42:43564 version: The Chromium OS version the device is running.
565 track: The channel the device is running on.
566 board: The device's board.
567 event_result: The event result of Omaha request.
568 event_type: The event type of Omaha request.
569 previous_version: The Chromium OS version we updated and rebooted from.
570 timestamp: The timestamp the event was received.
Amin Hassani7c447852019-09-26 22:01:48571 See the OmahaEvent class in update_engine/omaha_request_action.h for
572 event type and status code definitions. If the ip does not exist an empty
573 string is returned.
Gilad Arnold1b908392012-10-05 18:36:27574
575 Example URL:
576 http://myhost/api/hostlog?ip=192.168.1.5
577 """
Gilad Arnold286a0062012-01-12 21:47:02578 return updater.HandleHostLogPing(ip)
579
David Rochberg7c79a812011-01-19 19:24:45580class DevServerRoot(object):
Chris Sosa7c931362010-10-12 02:49:01581 """The Root Class for the Dev Server.
582
583 CherryPy works as follows:
584 For each method in this class, cherrpy interprets root/path
585 as a call to an instance of DevServerRoot->method_name. For example,
586 a call to http://myhost/build will call build. CherryPy automatically
587 parses http args and places them as keyword arguments in each method.
588 For paths http://myhost/update/dir1/dir2, you can use *args so that
589 cherrypy uses the update method and puts the extra paths in args.
590 """
Gilad Arnoldf8f769f2012-09-24 15:43:01591 # Method names that should not be listed on the index page.
592 _UNLISTED_METHODS = ['index', 'doc']
593
Dale Curtisc9aaf3a2011-08-09 22:47:40594 api = ApiRoot()
Chris Sosa7c931362010-10-12 02:49:01595
Dan Shi59ae7092013-06-04 21:37:27596 # Number of threads that devserver is staging images.
597 _staging_thread_count = 0
598 # Lock used to lock increasing/decreasing count.
599 _staging_thread_count_lock = threading.Lock()
600
joychen3cb228e2013-06-12 19:13:13601 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 22:20:41602 self._builder = None
Simran Basi4baad082013-02-14 21:39:18603 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 19:13:13604 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 19:24:45605
Congbin Guo3afae6c2019-08-13 23:29:42606 @property
607 def staging_thread_count(self):
608 """Get the staging thread count."""
609 return self._staging_thread_count
Dan Shiafd0e492015-05-27 21:23:51610
Dale Curtisc9aaf3a2011-08-09 22:47:40611 @cherrypy.expose
David Rochberg7c79a812011-01-19 19:24:45612 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-12 02:49:01613 """Builds the package specified."""
Sanika Kulkarnid4496fd2020-02-05 01:26:25614 if is_deprecated_server():
615 raise DeprecatedRPCError('build')
616
Nick Sanders7dcaa2e2011-08-04 22:20:41617 import builder
618 if self._builder is None:
619 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 19:24:45620 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-12 02:49:01621
Dale Curtisc9aaf3a2011-08-09 22:47:40622 @cherrypy.expose
Dan Shif8eb0d12013-08-02 00:52:06623 def is_staged(self, **kwargs):
624 """Check if artifacts have been downloaded.
625
Congbin Guo3afae6c2019-08-13 23:29:42626 Examples:
627 To check if autotest and test_suites are staged:
628 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
629 artifacts=autotest,test_suites
630
Amin Hassani08e42d22019-06-03 07:31:30631 Args:
Chris Sosa6b0c6172013-08-06 00:01:33632 async: True to return without waiting for download to complete.
633 artifacts: Comma separated list of named artifacts to download.
634 These are defined in artifact_info and have their implementation
635 in build_artifact.py.
636 files: Comma separated list of file artifacts to stage. These
637 will be available as is in the corresponding static directory with no
638 custom post-processing.
639
Congbin Guo3afae6c2019-08-13 23:29:42640 Returns:
641 True of all artifacts are staged.
Dan Shif8eb0d12013-08-02 00:52:06642 """
Gabe Black3b567202015-09-23 21:07:59643 dl, factory = _get_downloader_and_factory(kwargs)
Aviv Keshet57d18172016-06-19 03:39:09644 response = str(dl.IsStaged(factory))
645 _Log('Responding to is_staged %s request with %r', kwargs, response)
646 return response
Dan Shi59ae7092013-06-04 21:37:27647
Chris Sosa76e44b92013-01-31 20:11:38648 @cherrypy.expose
Prashanth Ba06d2d22014-03-07 23:35:19649 def list_image_dir(self, **kwargs):
650 """Take an archive url and list the contents in its staged directory.
651
Amin Hassani08e42d22019-06-03 07:31:30652 Examples:
Prashanth Ba06d2d22014-03-07 23:35:19653 To list the contents of where this devserver should have staged
654 gs://image-archive/<board>-release/<build> call:
655 http://devserver_url:<port>/list_image_dir?archive_url=<gs://..>
656
Congbin Guo3afae6c2019-08-13 23:29:42657 Args:
658 archive_url: Google Storage URL for the build.
659
Prashanth Ba06d2d22014-03-07 23:35:19660 Returns:
661 A string with information about the contents of the image directory.
662 """
Gabe Black3b567202015-09-23 21:07:59663 dl = _get_downloader(kwargs)
Prashanth Ba06d2d22014-03-07 23:35:19664 try:
Gabe Black3b567202015-09-23 21:07:59665 image_dir_contents = dl.ListBuildDir()
Prashanth Ba06d2d22014-03-07 23:35:19666 except build_artifact.ArtifactDownloadError as e:
667 return 'Cannot list the contents of staged artifacts. %s' % e
668 if not image_dir_contents:
Gabe Black3b567202015-09-23 21:07:59669 return '%s has not been staged on this devserver.' % dl.DescribeSource()
Prashanth Ba06d2d22014-03-07 23:35:19670 return image_dir_contents
671
672 @cherrypy.expose
Chris Sosa76e44b92013-01-31 20:11:38673 def stage(self, **kwargs):
Gabe Black3b567202015-09-23 21:07:59674 """Downloads and caches build artifacts.
Chris Sosa76e44b92013-01-31 20:11:38675
Gabe Black3b567202015-09-23 21:07:59676 Downloads and caches build artifacts, possibly from a Google Storage URL,
Dan Shi72b16132015-10-08 19:10:33677 or from Android's build server. Returns once these have been downloaded
Gabe Black3b567202015-09-23 21:07:59678 on the devserver. A call to this will attempt to cache non-specified
679 artifacts in the background for the given from the given URL following
680 the principle of spatial locality. Spatial locality of different
Chris Sosa76e44b92013-01-31 20:11:38681 artifacts is explicitly defined in the build_artifact module.
682
683 These artifacts will then be available from the static/ sub-directory of
684 the devserver.
685
Amin Hassani08e42d22019-06-03 07:31:30686 Examples:
Chris Sosa76e44b92013-01-31 20:11:38687 To download the autotest and test suites tarballs:
688 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
689 artifacts=autotest,test_suites
690 To download the full update payload:
691 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
692 artifacts=full_payload
Chris Sosa6b0c6172013-08-06 00:01:33693 To download just a file called blah.bin:
694 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
695 files=blah.bin
Chris Sosa76e44b92013-01-31 20:11:38696
697 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 23:39:34698 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 20:11:38699
700 Note for this example, relative path is the archive_url stripped of its
701 basename i.e. path/ in the examples above. Specific example:
702
703 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
704
705 Will get staged to:
706
joychened64b222013-06-21 23:39:34707 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Congbin Guo3afae6c2019-08-13 23:29:42708
709 Args:
710 archive_url: Google Storage URL for the build.
711 local_path: Local path for the build.
712 delete_source: Only meaningful with local_path. bool to indicate if the
713 source files should be deleted. This is especially useful when staging
714 a file locally in resource constrained environments as it allows us to
715 move the relevant files locally instead of copying them.
716 async: True to return without waiting for download to complete.
717 artifacts: Comma separated list of named artifacts to download.
718 These are defined in artifact_info and have their implementation
719 in build_artifact.py.
720 files: Comma separated list of files to stage. These
721 will be available as is in the corresponding static directory with no
722 custom post-processing.
723 clean: True to remove any previously staged artifacts first.
Chris Sosa76e44b92013-01-31 20:11:38724 """
Gabe Black3b567202015-09-23 21:07:59725 dl, factory = _get_downloader_and_factory(kwargs)
726
Dan Shi59ae7092013-06-04 21:37:27727 with DevServerRoot._staging_thread_count_lock:
728 DevServerRoot._staging_thread_count += 1
729 try:
Laurence Goodbyf5c958d2016-01-15 02:23:56730 boolean_string = kwargs.get('clean')
731 clean = xbuddy.XBuddy.ParseBoolean(boolean_string)
732 if clean and os.path.exists(dl.GetBuildDir()):
733 _Log('Removing %s' % dl.GetBuildDir())
734 shutil.rmtree(dl.GetBuildDir())
Achuith Bhandarkar2a1fcd82019-10-18 00:45:58735 is_async = kwargs.get('async', False)
736 dl.Download(factory, is_async=is_async)
Dan Shi59ae7092013-06-04 21:37:27737 finally:
738 with DevServerRoot._staging_thread_count_lock:
739 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 20:11:38740 return 'Success'
Chris Sosacde6bf42012-06-01 01:36:39741
742 @cherrypy.expose
xixuan52c2fba2016-05-21 00:02:48743 def cros_au(self, **kwargs):
744 """Auto-update a CrOS DUT.
745
746 Args:
747 kwargs:
748 host_name: the hostname of the DUT to auto-update.
749 build_name: the build name for update the DUT.
750 force_update: Force an update even if the version installed is the
751 same. Default: False.
752 full_update: If True, do not run stateful update, directly force a full
753 reimage. If False, try stateful update first if the dut is already
754 installed with the same version.
755 async: Whether the auto_update function is ran in the background.
David Rileyee75de22017-11-02 17:48:15756 quick_provision: Whether the quick provision path is attempted first.
xixuan52c2fba2016-05-21 00:02:48757
758 Returns:
759 A tuple includes two elements:
760 a boolean variable represents whether the auto-update process is
761 successfully started.
762 an integer represents the background auto-update process id.
763 """
764 _check_base_args_for_auto_update(kwargs)
765
766 host_name = kwargs['host_name']
767 build_name = kwargs['build_name']
768 force_update = _parse_boolean_arg(kwargs, 'force_update')
769 full_update = _parse_boolean_arg(kwargs, 'full_update')
Achuith Bhandarkar2a1fcd82019-10-18 00:45:58770 is_async = _parse_boolean_arg(kwargs, 'async')
xixuanac89ce82016-12-01 00:48:20771 original_build = _parse_string_arg(kwargs, 'original_build')
David Haddock90e49442017-04-08 02:14:09772 payload_filename = _parse_string_arg(kwargs, 'payload_filename')
David Haddock20559612017-06-29 05:15:08773 clobber_stateful = _parse_boolean_arg(kwargs, 'clobber_stateful')
David Rileyee75de22017-11-02 17:48:15774 quick_provision = _parse_boolean_arg(kwargs, 'quick_provision')
775
776 devserver_url = updater.GetDevserverUrl()
777 static_url = updater.GetStaticUrl()
xixuan52c2fba2016-05-21 00:02:48778
Mike Frysingera777cfc2020-03-02 22:20:46779 # Command of running auto-update.
780 cmd = ['cros_update', '--hostname', host_name, '-b', build_name,
781 '--static_dir', updater.static_dir]
782
783 # The original_build's format is like: link/3428.210.0
784 # The corresponding release_archive_url's format is like:
785 # gs://chromeos-releases/stable-channel/link/3428.210.0
786 if original_build:
787 release_archive_url = _build_uri_from_build_name(original_build)
788 # First staging the stateful.tgz synchronousely.
789 self.stage(files='stateful.tgz', is_async=False,
790 archive_url=release_archive_url)
791 cmd += ['--original_build', original_build]
792
793 if force_update:
794 cmd += ['--force_update']
795
796 if full_update:
797 cmd += ['--full_update']
798
799 if payload_filename:
800 cmd += ['--payload_filename', payload_filename]
801
802 if clobber_stateful:
803 cmd += ['--clobber_stateful']
804
805 if quick_provision:
806 cmd += ['--quick_provision']
807
808 if devserver_url:
809 cmd += ['--devserver_url', devserver_url]
810
811 if static_url:
812 cmd += ['--static_url', static_url]
813
Achuith Bhandarkar2a1fcd82019-10-18 00:45:58814 if is_async:
Amin Hassani78520ae2019-10-29 20:26:51815 p = subprocess.Popen(cmd, preexec_fn=os.setsid)
xixuan2a0970a2016-08-10 19:12:44816 pid = os.getpgid(p.pid)
xixuan52c2fba2016-05-21 00:02:48817
818 # Pre-write status in the track_status_file before the first call of
819 # 'get_au_status' to make sure that the track_status_file exists.
xixuan2a0970a2016-08-10 19:12:44820 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
xixuan52c2fba2016-05-21 00:02:48821 progress_tracker.WriteStatus('CrOS update is just started.')
822
xixuan2a0970a2016-08-10 19:12:44823 return json.dumps((True, pid))
xixuan52c2fba2016-05-21 00:02:48824 else:
Mike Frysingera777cfc2020-03-02 22:20:46825 subprocess.check_call(cmd)
xixuan27d50442017-08-09 17:38:25826 return json.dumps((True, -1))
xixuan52c2fba2016-05-21 00:02:48827
828 @cherrypy.expose
829 def get_au_status(self, **kwargs):
830 """Check if the auto-update task is finished.
831
832 It handles 4 cases:
833 1. If an error exists in the track_status_file, delete the track file and
834 raise it.
835 2. If cros-update process is finished, delete the file and return the
836 success result.
837 3. If the process is not running, delete the track file and raise an error
838 about 'the process is terminated due to unknown reason'.
839 4. If the track_status_file does not exist, kill the process if it exists,
840 and raise the IOError.
841
842 Args:
843 kwargs:
844 host_name: the hostname of the DUT to auto-update.
845 pid: the background process id of cros-update.
846
847 Returns:
xixuan28d99072016-10-06 19:24:16848 A dict with three elements:
xixuan52c2fba2016-05-21 00:02:48849 a boolean variable represents whether the auto-update process is
850 finished.
851 a string represents the current auto-update process status.
852 For example, 'Transfer Devserver/Stateful Update Package'.
xixuan28d99072016-10-06 19:24:16853 a detailed error message paragraph if there exists an Auto-Update
854 error, in which the last line shows the main exception. Empty
855 string otherwise.
xixuan52c2fba2016-05-21 00:02:48856 """
857 if 'host_name' not in kwargs:
Amin Hassani722e0962019-11-15 23:45:31858 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
859 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-21 00:02:48860
861 if 'pid' not in kwargs:
Amin Hassani722e0962019-11-15 23:45:31862 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
863 KEY_ERROR_MSG % 'pid')
xixuan52c2fba2016-05-21 00:02:48864
865 host_name = kwargs['host_name']
866 pid = kwargs['pid']
867 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
868
xixuan28d99072016-10-06 19:24:16869 result_dict = {'finished': False, 'status': '', 'detailed_error_msg': ''}
xixuan52c2fba2016-05-21 00:02:48870 try:
871 result = progress_tracker.ReadStatus()
872 if result.startswith(cros_update_progress.ERROR_TAG):
xixuan28d99072016-10-06 19:24:16873 result_dict['detailed_error_msg'] = result[len(
874 cros_update_progress.ERROR_TAG):]
xixuan28681fd2016-11-23 19:13:56875 elif result == cros_update_progress.FINISHED:
xixuan28d99072016-10-06 19:24:16876 result_dict['finished'] = True
877 result_dict['status'] = result
xixuan28681fd2016-11-23 19:13:56878 elif not cros_update_progress.IsProcessAlive(pid):
xixuan28d99072016-10-06 19:24:16879 result_dict['detailed_error_msg'] = (
880 'Cros_update process terminated midway due to unknown reason. '
881 'Last update status was %s' % result)
xixuan28681fd2016-11-23 19:13:56882 else:
883 result_dict['status'] = result
884 except IOError as e:
885 if pid and cros_update_progress.IsProcessAlive(pid):
xixuan2a0970a2016-08-10 19:12:44886 os.killpg(int(pid), signal.SIGKILL)
xixuan52c2fba2016-05-21 00:02:48887
xixuan28681fd2016-11-23 19:13:56888 result_dict['detailed_error_msg'] = str(e)
889
890 return json.dumps(result_dict)
xixuan52c2fba2016-05-21 00:02:48891
892 @cherrypy.expose
David Riley6d5fca02017-10-31 17:35:47893 def post_au_status(self, status, **kwargs):
894 """Updates the status of an auto-update task.
895
896 Callers will need to POST to this URL with a body of MIME-type
897 "multipart/form-data".
898 The body should include a single argument, 'status', containing the
899 AU status to record.
900
901 Args:
902 status: The updated status.
903 kwargs:
904 host_name: the hostname of the DUT to auto-update.
905 pid: the background process id of cros-update.
906 """
907 if 'host_name' not in kwargs:
Amin Hassani722e0962019-11-15 23:45:31908 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
909 KEY_ERROR_MSG % 'host_name')
David Riley6d5fca02017-10-31 17:35:47910
911 if 'pid' not in kwargs:
Amin Hassani722e0962019-11-15 23:45:31912 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
913 KEY_ERROR_MSG % 'pid')
David Riley6d5fca02017-10-31 17:35:47914
915 host_name = kwargs['host_name']
916 pid = kwargs['pid']
David Riley3cea2582017-11-25 06:03:01917 status = status.rstrip()
918 _Log('Recording status for %s (%s): %s' % (host_name, pid, status))
David Riley6d5fca02017-10-31 17:35:47919 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
920
David Riley3cea2582017-11-25 06:03:01921 progress_tracker.WriteStatus(status)
David Riley6d5fca02017-10-31 17:35:47922
923 return 'True'
924
925 @cherrypy.expose
xixuan52c2fba2016-05-21 00:02:48926 def handler_cleanup(self, **kwargs):
xixuan3bc974e2016-10-19 00:21:43927 """Clean track status log and temp directory for CrOS auto-update process.
xixuan52c2fba2016-05-21 00:02:48928
929 Args:
930 kwargs:
931 host_name: the hostname of the DUT to auto-update.
932 pid: the background process id of cros-update.
933 """
934 if 'host_name' not in kwargs:
Amin Hassani722e0962019-11-15 23:45:31935 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
936 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-21 00:02:48937
938 if 'pid' not in kwargs:
Amin Hassani722e0962019-11-15 23:45:31939 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
940 KEY_ERROR_MSG % 'pid')
xixuan52c2fba2016-05-21 00:02:48941
942 host_name = kwargs['host_name']
943 pid = kwargs['pid']
944 cros_update_progress.DelTrackStatusFile(host_name, pid)
xixuan3bc974e2016-10-19 00:21:43945 cros_update_progress.DelAUTempDirectory(host_name, pid)
xixuan52c2fba2016-05-21 00:02:48946
947 @cherrypy.expose
948 def kill_au_proc(self, **kwargs):
949 """Kill CrOS auto-update process using given process id.
950
951 Args:
952 kwargs:
953 host_name: Kill all the CrOS auto-update process of this host.
954
955 Returns:
956 True if all processes are killed properly.
957 """
958 if 'host_name' not in kwargs:
Amin Hassani722e0962019-11-15 23:45:31959 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
960 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-21 00:02:48961
xixuan447ad9d2017-02-28 22:46:20962 cur_pid = kwargs.get('pid')
963
xixuan52c2fba2016-05-21 00:02:48964 host_name = kwargs['host_name']
xixuan3bc974e2016-10-19 00:21:43965 track_log_list = cros_update_progress.GetAllTrackStatusFileByHostName(
966 host_name)
xixuan52c2fba2016-05-21 00:02:48967 for log in track_log_list:
968 # The track log's full path is: path/host_name_pid.log
969 # Use splitext to remove file extension, then parse pid from the
970 # filename.
Congbin Guo3afae6c2019-08-13 23:29:42971 pid = os.path.splitext(os.path.basename(log))[0][len(host_name) + 1:]
xixuan447ad9d2017-02-28 22:46:20972 _clear_process(host_name, pid)
xixuan52c2fba2016-05-21 00:02:48973
xixuan447ad9d2017-02-28 22:46:20974 if cur_pid:
975 _clear_process(host_name, cur_pid)
xixuan52c2fba2016-05-21 00:02:48976
977 return 'True'
978
979 @cherrypy.expose
980 def collect_cros_au_log(self, **kwargs):
981 """Collect CrOS auto-update log.
982
983 Args:
984 kwargs:
985 host_name: the hostname of the DUT to auto-update.
986 pid: the background process id of cros-update.
987
988 Returns:
David Haddock9f459632017-05-11 21:45:46989 A dictionary containing the execute log file and any hostlog files.
xixuan52c2fba2016-05-21 00:02:48990 """
991 if 'host_name' not in kwargs:
Amin Hassani722e0962019-11-15 23:45:31992 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
993 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-21 00:02:48994
995 if 'pid' not in kwargs:
Amin Hassani722e0962019-11-15 23:45:31996 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
997 KEY_ERROR_MSG % 'pid')
xixuan52c2fba2016-05-21 00:02:48998
999 host_name = kwargs['host_name']
1000 pid = kwargs['pid']
xixuan3bc974e2016-10-19 00:21:431001
1002 # Fetch the execute log recorded by cros_update_progress.
xixuan1bbfaba2016-10-14 00:53:221003 au_log = cros_update_progress.ReadExecuteLogFile(host_name, pid)
1004 cros_update_progress.DelExecuteLogFile(host_name, pid)
David Haddock9f459632017-05-11 21:45:461005 # Fetch the cros_au host_logs if they exist
1006 au_hostlogs = cros_update_progress.ReadAUHostLogFiles(host_name, pid)
1007 return json.dumps({'cros_au_log': au_log, 'host_logs': au_hostlogs})
xixuan1bbfaba2016-10-14 00:53:221008
xixuan52c2fba2016-05-21 00:02:481009 @cherrypy.expose
Dan Shi2f136862016-02-11 23:38:381010 def locate_file(self, **kwargs):
1011 """Get the path to the given file name.
1012
1013 This method looks up the given file name inside specified build artifacts.
1014 One use case is to help caller to locate an apk file inside a build
1015 artifact. The location of the apk file could be different based on the
1016 branch and target.
1017
1018 Args:
1019 file_name: Name of the file to look for.
1020 artifacts: A list of artifact names to search for the file.
1021
1022 Returns:
1023 Path to the file with the given name. It's relative to the folder for the
1024 build, e.g., DATA/priv-app/sl4a/sl4a.apk
Dan Shi2f136862016-02-11 23:38:381025 """
Sanika Kulkarnid4496fd2020-02-05 01:26:251026 if is_deprecated_server():
1027 raise DeprecatedRPCError('locate_file')
1028
Dan Shi2f136862016-02-11 23:38:381029 dl, _ = _get_downloader_and_factory(kwargs)
1030 try:
Joe Brennan1691f8e2017-03-15 22:53:361031 file_name = kwargs['file_name']
Dan Shi2f136862016-02-11 23:38:381032 artifacts = kwargs['artifacts']
1033 except KeyError:
Amin Hassanid4e35392019-10-03 18:02:441034 raise DevServerError(
Congbin Guo4132a272019-08-20 19:32:141035 '`file_name` and `artifacts` are required to search '
1036 'for a file in build artifacts.')
Dan Shi2f136862016-02-11 23:38:381037 build_path = dl.GetBuildDir()
1038 for artifact in artifacts:
1039 # Get the unzipped folder of the artifact. If it's not defined in
1040 # ARTIFACT_UNZIP_FOLDER_MAP, assume the files are unzipped to the build
1041 # directory directly.
1042 folder = artifact_info.ARTIFACT_UNZIP_FOLDER_MAP.get(artifact, '')
1043 artifact_path = os.path.join(build_path, folder)
1044 for root, _, filenames in os.walk(artifact_path):
Joe Brennan1691f8e2017-03-15 22:53:361045 if file_name in set([f for f in filenames]):
Dan Shi2f136862016-02-11 23:38:381046 return os.path.relpath(os.path.join(root, file_name), build_path)
Amin Hassanid4e35392019-10-03 18:02:441047 raise DevServerError(
Congbin Guo4132a272019-08-20 19:32:141048 'File `%s` can not be found in artifacts: %s' % (file_name, artifacts))
Dan Shi2f136862016-02-11 23:38:381049
1050 @cherrypy.expose
Simran Basi4baad082013-02-14 21:39:181051 def setup_telemetry(self, **kwargs):
1052 """Extracts and sets up telemetry
1053
1054 This method goes through the telemetry deps packages, and stages them on
1055 the devserver to be used by the drones and the telemetry tests.
1056
1057 Args:
1058 archive_url: Google Storage URL for the build.
1059
1060 Returns:
1061 Path to the source folder for the telemetry codebase once it is staged.
1062 """
Gabe Black3b567202015-09-23 21:07:591063 dl = _get_downloader(kwargs)
Simran Basi4baad082013-02-14 21:39:181064
Gabe Black3b567202015-09-23 21:07:591065 build_path = dl.GetBuildDir()
Simran Basi4baad082013-02-14 21:39:181066 deps_path = os.path.join(build_path, 'autotest/packages')
1067 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
1068 src_folder = os.path.join(telemetry_path, 'src')
1069
1070 with self._telemetry_lock_dict.lock(telemetry_path):
1071 if os.path.exists(src_folder):
1072 # Telemetry is already fully stage return
1073 return src_folder
1074
1075 common_util.MkDirP(telemetry_path)
1076
1077 # Copy over the required deps tar balls to the telemetry directory.
1078 for dep in TELEMETRY_DEPS:
1079 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 23:40:041080 if not os.path.exists(dep_path):
1081 # This dep does not exist (could be new), do not extract it.
1082 continue
Simran Basi4baad082013-02-14 21:39:181083 try:
1084 common_util.ExtractTarball(dep_path, telemetry_path)
1085 except common_util.CommonUtilError as e:
1086 shutil.rmtree(telemetry_path)
Amin Hassanid4e35392019-10-03 18:02:441087 raise DevServerError(str(e))
Simran Basi4baad082013-02-14 21:39:181088
1089 # By default all the tarballs extract to test_src but some parts of
1090 # the telemetry code specifically hardcoded to exist inside of 'src'.
1091 test_src = os.path.join(telemetry_path, 'test_src')
1092 try:
1093 shutil.move(test_src, src_folder)
1094 except shutil.Error:
1095 # This can occur if src_folder already exists. Remove and retry move.
1096 shutil.rmtree(src_folder)
Amin Hassanid4e35392019-10-03 18:02:441097 raise DevServerError(
Gabe Black3b567202015-09-23 21:07:591098 'Failure in telemetry setup for build %s. Appears that the '
1099 'test_src to src move failed.' % dl.GetBuild())
Simran Basi4baad082013-02-14 21:39:181100
1101 return src_folder
1102
1103 @cherrypy.expose
Chris Sosa76e44b92013-01-31 20:11:381104 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 19:22:361105 """Symbolicates a minidump using pre-downloaded symbols, returns it.
1106
1107 Callers will need to POST to this URL with a body of MIME-type
1108 "multipart/form-data".
1109 The body should include a single argument, 'minidump', containing the
1110 binary-formatted minidump to symbolicate.
1111
Chris Masone816e38c2012-05-02 19:22:361112 Args:
Chris Sosa76e44b92013-01-31 20:11:381113 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 19:22:361114 minidump: The binary minidump file to symbolicate.
1115 """
Sanika Kulkarnid4496fd2020-02-05 01:26:251116 if is_deprecated_server():
1117 raise DeprecatedRPCError('symbolicate_dump')
1118
Chris Sosa76e44b92013-01-31 20:11:381119 # Ensure the symbols have been staged.
Dan Shif08fe492016-10-04 21:39:251120 # Try debug.tar.xz first, then debug.tgz
1121 for artifact in (artifact_info.SYMBOLS_ONLY, artifact_info.SYMBOLS):
1122 kwargs['artifacts'] = artifact
1123 dl = _get_downloader(kwargs)
1124
1125 try:
1126 if self.stage(**kwargs) == 'Success':
1127 break
1128 except build_artifact.ArtifactDownloadError:
1129 continue
1130 else:
Amin Hassanid4e35392019-10-03 18:02:441131 raise DevServerError(
Congbin Guo4132a272019-08-20 19:32:141132 'Failed to stage symbols for %s' % dl.DescribeSource())
Chris Sosa76e44b92013-01-31 20:11:381133
Chris Masone816e38c2012-05-02 19:22:361134 to_return = ''
1135 with tempfile.NamedTemporaryFile() as local:
1136 while True:
1137 data = minidump.file.read(8192)
1138 if not data:
1139 break
1140 local.write(data)
Chris Sosa76e44b92013-01-31 20:11:381141
Chris Masone816e38c2012-05-02 19:22:361142 local.flush()
Chris Sosa76e44b92013-01-31 20:11:381143
Gabe Black3b567202015-09-23 21:07:591144 symbols_directory = os.path.join(dl.GetBuildDir(), 'debug', 'breakpad')
Chris Sosa76e44b92013-01-31 20:11:381145
xixuanab744382017-04-27 17:41:271146 # The location of minidump_stackwalk is defined in chromeos-admin.
Chris Sosa76e44b92013-01-31 20:11:381147 stackwalk = subprocess.Popen(
xixuanab744382017-04-27 17:41:271148 ['/usr/local/bin/minidump_stackwalk', local.name, symbols_directory],
Chris Sosa76e44b92013-01-31 20:11:381149 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1150
Chris Masone816e38c2012-05-02 19:22:361151 to_return, error_text = stackwalk.communicate()
1152 if stackwalk.returncode != 0:
Amin Hassanid4e35392019-10-03 18:02:441153 raise DevServerError(
Congbin Guo4132a272019-08-20 19:32:141154 "Can't generate stack trace: %s (rc=%d)" % (error_text,
1155 stackwalk.returncode))
Chris Masone816e38c2012-05-02 19:22:361156
1157 return to_return
1158
1159 @cherrypy.expose
Don Garrettf84631a2014-01-08 02:21:261160 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 19:31:361161 """Return a string representing the latest build for a given target.
1162
1163 Args:
1164 target: The build target, typically a combination of the board and the
1165 type of build e.g. x86-mario-release.
1166 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
1167 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-08 02:21:261168
Scott Zawalski16954532012-03-20 19:31:361169 Returns:
1170 A string representation of the latest build if one exists, i.e.
1171 R19-1993.0.0-a1-b1480.
1172 An empty string if no latest could be found.
1173 """
Don Garrettf84631a2014-01-08 02:21:261174 if not kwargs:
Scott Zawalski16954532012-03-20 19:31:361175 return _PrintDocStringAsHTML(self.latestbuild)
1176
Don Garrettf84631a2014-01-08 02:21:261177 if 'target' not in kwargs:
Amin Hassani722e0962019-11-15 23:45:311178 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
1179 'Error: target= is required!')
Dan Shi61305df2015-10-26 23:52:351180
1181 if _is_android_build_request(kwargs):
1182 branch = kwargs.get('branch', None)
1183 target = kwargs.get('target', None)
1184 if not target or not branch:
Amin Hassanid4e35392019-10-03 18:02:441185 raise DevServerError('Both target and branch must be specified to query'
1186 ' for the latest Android build.')
Dan Shi61305df2015-10-26 23:52:351187 return android_build.BuildAccessor.GetLatestBuildID(target, branch)
1188
Scott Zawalski16954532012-03-20 19:31:361189 try:
Gilad Arnoldc65330c2012-09-20 22:17:481190 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-08 02:21:261191 updater.static_dir, kwargs['target'],
1192 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 17:05:011193 except common_util.CommonUtilError as errmsg:
Amin Hassani722e0962019-11-15 23:45:311194 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
1195 str(errmsg))
Scott Zawalski16954532012-03-20 19:31:361196
1197 @cherrypy.expose
xixuan7efd0002016-04-14 22:34:011198 def list_suite_controls(self, **kwargs):
1199 """Return a list of contents of all known control files.
1200
1201 Example URL:
1202 To List all control files' content:
1203 http://dev-server/list_suite_controls?suite_name=bvt&
1204 build=daisy_spring-release/R29-4279.0.0
1205
1206 Args:
1207 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
1208 suite_name: List the control files belonging to that suite.
1209
1210 Returns:
Dan Shia1cd6522016-04-18 23:07:211211 A dictionary of all control files's path to its content for given suite.
xixuan7efd0002016-04-14 22:34:011212 """
Sanika Kulkarnid4496fd2020-02-05 01:26:251213 if is_deprecated_server():
1214 raise DeprecatedRPCError('list_suite_controls')
1215
xixuan7efd0002016-04-14 22:34:011216 if not kwargs:
1217 return _PrintDocStringAsHTML(self.controlfiles)
1218
1219 if 'build' not in kwargs:
Amin Hassani722e0962019-11-15 23:45:311220 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
1221 'Error: build= is required!')
xixuan7efd0002016-04-14 22:34:011222
1223 if 'suite_name' not in kwargs:
Amin Hassani722e0962019-11-15 23:45:311224 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
1225 'Error: suite_name= is required!')
xixuan7efd0002016-04-14 22:34:011226
1227 control_file_list = [
1228 line.rstrip() for line in common_util.GetControlFileListForSuite(
1229 updater.static_dir, kwargs['build'],
1230 kwargs['suite_name']).splitlines()]
1231
Dan Shia1cd6522016-04-18 23:07:211232 control_file_content_dict = {}
xixuan7efd0002016-04-14 22:34:011233 for control_path in control_file_list:
Dan Shia1cd6522016-04-18 23:07:211234 control_file_content_dict[control_path] = (common_util.GetControlFile(
xixuan7efd0002016-04-14 22:34:011235 updater.static_dir, kwargs['build'], control_path))
1236
Dan Shia1cd6522016-04-18 23:07:211237 return json.dumps(control_file_content_dict)
xixuan7efd0002016-04-14 22:34:011238
1239 @cherrypy.expose
Don Garrettf84631a2014-01-08 02:21:261240 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 22:17:281241 """Return a control file or a list of all known control files.
1242
1243 Example URL:
1244 To List all control files:
beepsbd337242013-07-10 05:44:061245 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
1246 To List all control files for, say, the bvt suite:
1247 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 22:17:281248 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 20:12:421249 http://dev-server/controlfiles?board=x86-alex-release&build=R18-1514.0.0&control_path=client/sleeptest/control
Scott Zawalski4647ce62012-01-03 22:17:281250
1251 Args:
Scott Zawalski84a39c92012-01-13 20:12:421252 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 22:17:281253 control_path: If you want the contents of a control file set this
1254 to the path. E.g. client/site_tests/sleeptest/control
1255 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-10 05:44:061256 suite_name: If control_path is not specified but a suite_name is
1257 specified, list the control files belonging to that suite instead of
1258 all control files. The empty string for suite_name will list all control
1259 files for the build.
Don Garrettf84631a2014-01-08 02:21:261260
Scott Zawalski4647ce62012-01-03 22:17:281261 Returns:
1262 Contents of a control file if control_path is provided.
1263 A list of control files if no control_path is provided.
1264 """
Sanika Kulkarnid4496fd2020-02-05 01:26:251265 if is_deprecated_server():
1266 raise DeprecatedRPCError('controlfiles')
1267
Don Garrettf84631a2014-01-08 02:21:261268 if not kwargs:
Scott Zawalski4647ce62012-01-03 22:17:281269 return _PrintDocStringAsHTML(self.controlfiles)
1270
Don Garrettf84631a2014-01-08 02:21:261271 if 'build' not in kwargs:
Amin Hassani722e0962019-11-15 23:45:311272 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
1273 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 22:17:281274
Don Garrettf84631a2014-01-08 02:21:261275 if 'control_path' not in kwargs:
1276 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-10 05:44:061277 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-08 02:21:261278 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-10 05:44:061279 else:
1280 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-08 02:21:261281 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 22:17:281282 else:
Gilad Arnoldc65330c2012-09-20 22:17:481283 return common_util.GetControlFile(
Don Garrettf84631a2014-01-08 02:21:261284 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-13 02:39:181285
1286 @cherrypy.expose
Simran Basi99e63c02014-05-20 17:39:521287 def xbuddy_translate(self, *args, **kwargs):
Yu-Ju Hong1bdb7a92014-04-10 23:02:111288 """Translates an xBuddy path to a real path to artifact if it exists.
1289
1290 Args:
Simran Basi99e63c02014-05-20 17:39:521291 args: An xbuddy path in the form of {local|remote}/build_id/artifact.
1292 Local searches the devserver's static directory. Remote searches a
1293 Google Storage image archive.
1294
1295 Kwargs:
1296 image_dir: Google Storage image archive to search in if requesting a
1297 remote artifact. If none uses the default bucket.
Yu-Ju Hong1bdb7a92014-04-10 23:02:111298
1299 Returns:
Simran Basi99e63c02014-05-20 17:39:521300 String in the format of build_id/artifact as stored on the local server
1301 or in Google Storage.
Yu-Ju Hong1bdb7a92014-04-10 23:02:111302 """
Sanika Kulkarnid4496fd2020-02-05 01:26:251303 if is_deprecated_server():
1304 raise DeprecatedRPCError('xbuddy_translate')
1305
Simran Basi99e63c02014-05-20 17:39:521306 build_id, filename = self._xbuddy.Translate(
Gabe Black3b567202015-09-23 21:07:591307 args, image_dir=kwargs.get('image_dir'))
Yu-Ju Hong1bdb7a92014-04-10 23:02:111308 response = os.path.join(build_id, filename)
1309 _Log('Path translation requested, returning: %s', response)
1310 return response
1311
1312 @cherrypy.expose
joycheneaf4cfc2013-07-02 15:38:571313 def xbuddy(self, *args, **kwargs):
1314 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 19:13:131315
1316 Args:
joycheneaf4cfc2013-07-02 15:38:571317 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 21:30:301318 components of the path. The path can be understood as
1319 "{local|remote}/build_id/artifact" where build_id is composed of
1320 "board/version."
joycheneaf4cfc2013-07-02 15:38:571321
joychen121fc9b2013-08-02 21:30:301322 The first path element is optional, and can be "remote" or "local"
1323 If local (the default), devserver will not attempt to access Google
1324 Storage, and will only search the static directory for the files.
1325 If remote, devserver will try to obtain the artifact off GS if it's
1326 not found locally.
1327 The board is the familiar board name, optionally suffixed.
1328 The version can be the google storage version number, and may also be
1329 any of a number of xBuddy defined version aliases that will be
1330 translated into the latest built image that fits the description.
1331 Defaults to latest.
1332 The artifact is one of a number of image or artifact aliases used by
1333 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 15:38:571334
1335 Kwargs:
Yu-Ju Hong51495eb2013-12-13 01:08:431336 for_update: {true|false}
Amin Hassanie9ffb862019-09-26 00:10:401337 if true, prepares the update payloads for the image,
Yu-Ju Hong51495eb2013-12-13 01:08:431338 and returns the update uri to pass to the
1339 update_engine_client.
joychen3cb228e2013-06-12 19:13:131340 return_dir: {true|false}
1341 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-13 01:08:431342 relative_path: {true|false}
1343 if set to true, returns the relative path to the payload
1344 directory from static_dir.
joychen3cb228e2013-06-12 19:13:131345 Example URL:
joycheneaf4cfc2013-07-02 15:38:571346 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 19:13:131347 or
joycheneaf4cfc2013-07-02 15:38:571348 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 19:13:131349
1350 Returns:
Yu-Ju Hong51495eb2013-12-13 01:08:431351 If |for_update|, returns a redirect to the image or update file
1352 on the devserver. E.g.,
1353 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
1354 chromium-test-image.bin
1355 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
1356 http://host:port/static/x86-generic-release/R26-4000.0.0/
1357 If |relative_path| is true, return a relative path the folder where the
1358 payloads are. E.g.,
1359 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 19:13:131360 """
Sanika Kulkarnid4496fd2020-02-05 01:26:251361 if is_deprecated_server():
1362 raise DeprecatedRPCError('xbuddy')
1363
Chris Sosa75490802013-10-01 00:21:451364 boolean_string = kwargs.get('for_update')
1365 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-13 01:08:431366 boolean_string = kwargs.get('return_dir')
1367 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
1368 boolean_string = kwargs.get('relative_path')
1369 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 21:30:301370
Yu-Ju Hong51495eb2013-12-13 01:08:431371 if return_dir and relative_path:
Amin Hassani722e0962019-11-15 23:45:311372 raise DevServerHTTPError(
Amin Hassanid4e35392019-10-03 18:02:441373 http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 07:31:301374 'Cannot specify both return_dir and relative_path')
Chris Sosa75490802013-10-01 00:21:451375
1376 # For updates, we optimize downloading of test images.
1377 file_name = None
1378 build_id = None
1379 if for_update:
1380 try:
Yu-Ju Hong1bdb7a92014-04-10 23:02:111381 build_id = self._xbuddy.StageTestArtifactsForUpdate(args)
Chris Sosa75490802013-10-01 00:21:451382 except build_artifact.ArtifactDownloadError:
1383 build_id = None
1384
1385 if not build_id:
1386 build_id, file_name = self._xbuddy.Get(args)
1387
Yu-Ju Hong51495eb2013-12-13 01:08:431388 if for_update:
Amin Hassanie9ffb862019-09-26 00:10:401389 _Log('Payloads requested.')
Yu-Ju Hong51495eb2013-12-13 01:08:431390 # Forces payload to be in cache and symlinked into build_id dir.
Amin Hassanie9ffb862019-09-26 00:10:401391 updater.GetUpdateForLabel(build_id)
Yu-Ju Hong51495eb2013-12-13 01:08:431392
1393 response = None
1394 if return_dir:
1395 response = os.path.join(cherrypy.request.base, 'static', build_id)
1396 _Log('Directory requested, returning: %s', response)
1397 elif relative_path:
1398 response = build_id
1399 _Log('Relative path requested, returning: %s', response)
1400 elif for_update:
1401 response = os.path.join(cherrypy.request.base, 'update', build_id)
1402 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 19:13:131403 else:
Yu-Ju Hong51495eb2013-12-13 01:08:431404 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 21:30:301405 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-13 01:08:431406 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 21:30:301407 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 19:13:131408
Yu-Ju Hong51495eb2013-12-13 01:08:431409 return response
1410
joychen3cb228e2013-06-12 19:13:131411 @cherrypy.expose
1412 def xbuddy_list(self):
1413 """Lists the currently available images & time since last access.
1414
Gilad Arnold452fd272014-02-04 19:09:281415 Returns:
1416 A string representation of a list of tuples [(build_id, time since last
1417 access),...]
joychen3cb228e2013-06-12 19:13:131418 """
Sanika Kulkarnid4496fd2020-02-05 01:26:251419 if is_deprecated_server():
1420 raise DeprecatedRPCError('xbuddy')
1421
joychen3cb228e2013-06-12 19:13:131422 return self._xbuddy.List()
1423
1424 @cherrypy.expose
1425 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 19:09:281426 """Returns the number of images cached by xBuddy."""
Sanika Kulkarnid4496fd2020-02-05 01:26:251427 if is_deprecated_server():
1428 raise DeprecatedRPCError('xbuddy_capacity')
1429
joychen3cb228e2013-06-12 19:13:131430 return self._xbuddy.Capacity()
1431
1432 @cherrypy.expose
Chris Sosa7c931362010-10-12 02:49:011433 def index(self):
Gilad Arnoldf8f769f2012-09-24 15:43:011434 """Presents a welcome message and documentation links."""
Sanika Kulkarnid4496fd2020-02-05 01:26:251435 if is_deprecated_server():
1436 raise DeprecatedRPCError('index')
1437
Congbin Guo6bc32182019-08-21 00:54:301438 html_template = (
1439 'Welcome to the Dev Server!<br>\n'
1440 '<br>\n'
1441 'Here are the available methods, click for documentation:<br>\n'
1442 '<br>\n'
1443 '%s')
1444
1445 exposed_methods = []
1446 for app in cherrypy.tree.apps.values():
1447 exposed_methods += _FindExposedMethods(
1448 app.root, app.script_name.lstrip('/'),
1449 unlisted=self._UNLISTED_METHODS)
1450
1451 return html_template % '<br>\n'.join(
1452 ['<a href=doc/%s>%s</a>' % (name, name)
1453 for name in sorted(exposed_methods)])
Gilad Arnoldf8f769f2012-09-24 15:43:011454
1455 @cherrypy.expose
1456 def doc(self, *args):
1457 """Shows the documentation for available methods / URLs.
1458
Amin Hassani08e42d22019-06-03 07:31:301459 Examples:
Gilad Arnoldf8f769f2012-09-24 15:43:011460 http://myhost/doc/update
1461 """
Sanika Kulkarnid4496fd2020-02-05 01:26:251462 if is_deprecated_server():
1463 raise DeprecatedRPCError('doc')
1464
Gilad Arnoldd5ebaaa2012-10-02 18:52:381465 name = '/'.join(args)
Congbin Guo6bc32182019-08-21 00:54:301466 method = _GetExposedMethod(name)
Gilad Arnoldf8f769f2012-09-24 15:43:011467 if not method:
Amin Hassanid4e35392019-10-03 18:02:441468 raise DevServerError("No exposed method named `%s'" % name)
Gilad Arnoldf8f769f2012-09-24 15:43:011469 if not method.__doc__:
Amin Hassanid4e35392019-10-03 18:02:441470 raise DevServerError("No documentation for exposed method `%s'" % name)
Gilad Arnoldf8f769f2012-09-24 15:43:011471 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-12 02:49:011472
Dale Curtisc9aaf3a2011-08-09 22:47:401473 @cherrypy.expose
Amin Hassani6eec8792020-01-09 22:06:481474 def update(self, *args, **kwargs):
Gilad Arnoldf8f769f2012-09-24 15:43:011475 """Handles an update check from a Chrome OS client.
1476
1477 The HTTP request should contain the standard Omaha-style XML blob. The URL
1478 line may contain an additional intermediate path to the update payload.
1479
joychen121fc9b2013-08-02 21:30:301480 This request can be handled in one of 4 ways, depending on the devsever
1481 settings and intermediate path.
joychenb0dfe552013-07-30 17:02:061482
Amin Hassanie9ffb862019-09-26 00:10:401483 1. No intermediate path. DEPRECATED
joychen121fc9b2013-08-02 21:30:301484
1485 2. Path explicitly invokes XBuddy
1486 If there is a path given, it can explicitly invoke xbuddy by prefixing it
1487 with 'xbuddy'. This path is then used to acquire an image binary for the
1488 devserver to generate an update payload from. Devserver then serves this
1489 payload.
1490
1491 3. Path is left for the devserver to interpret.
1492 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
1493 to generate a payload from the test image in that directory and serve it.
1494
joychen121fc9b2013-08-02 21:30:301495 Examples:
joychen121fc9b2013-08-02 21:30:301496 2. Explicitly invoke xbuddy
1497 update_engine_client --omaha_url=
1498 http://myhost/update/xbuddy/remote/board/version/dev
1499 This would go to GS to download the dev image for the board, from which
1500 the devserver would generate a payload to serve.
1501
1502 3. Give a path for devserver to interpret
1503 update_engine_client --omaha_url=http://myhost/update/some/random/path
1504 This would attempt, in order to:
1505 a) Generate an update from a test image binary if found in
1506 static_dir/some/random/path.
1507 b) Serve an update payload found in static_dir/some/random/path.
1508 c) Hope that some/random/path takes the form "board/version" and
1509 and attempt to download an update payload for that board/version
1510 from GS.
Gilad Arnoldf8f769f2012-09-24 15:43:011511 """
joychen121fc9b2013-08-02 21:30:301512 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 21:47:021513 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-12 02:49:011514 data = cherrypy.request.rfile.read(body_length)
Amin Hassani6aa075c2020-02-21 18:36:441515
Amin Hassani6eec8792020-01-09 22:06:481516 return updater.HandleUpdatePing(data, label, **kwargs)
Chris Sosa0356d3b2010-09-16 22:46:221517
Dan Shif5ce2de2013-04-25 23:06:321518
Chris Sosadbc20082012-12-10 21:39:111519def _CleanCache(cache_dir, wipe):
1520 """Wipes any excess cached items in the cache_dir.
1521
1522 Args:
1523 cache_dir: the directory we are wiping from.
1524 wipe: If True, wipe all the contents -- not just the excess.
1525 """
1526 if wipe:
1527 # Clear the cache and exit on error.
1528 cmd = 'rm -rf %s/*' % cache_dir
1529 if os.system(cmd) != 0:
1530 _Log('Failed to clear the cache with %s' % cmd)
1531 sys.exit(1)
1532 else:
1533 # Clear all but the last N cached updates
1534 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
1535 (cache_dir, CACHED_ENTRIES))
1536 if os.system(cmd) != 0:
1537 _Log('Failed to clean up old delta cache files with %s' % cmd)
1538 sys.exit(1)
1539
1540
Chris Sosa3ae4dc12013-03-29 18:47:001541def _AddTestingOptions(parser):
1542 group = optparse.OptionGroup(
1543 parser, 'Advanced Testing Options', 'These are used by test scripts and '
1544 'developers writing integration tests utilizing the devserver. They are '
1545 'not intended to be really used outside the scope of someone '
1546 'knowledgable about the test.')
1547 group.add_option('--exit',
1548 action='store_true',
Amin Hassanie9ffb862019-09-26 00:10:401549 help='do not start the server (yet clear cache)')
Chris Sosa3ae4dc12013-03-29 18:47:001550 group.add_option('--host_log',
1551 action='store_true', default=False,
1552 help='record history of host update events (/api/hostlog)')
1553 group.add_option('--max_updates',
Gabe Black3b567202015-09-23 21:07:591554 metavar='NUM', default=-1, type='int',
Chris Sosa3ae4dc12013-03-29 18:47:001555 help='maximum number of update checks handled positively '
1556 '(default: unlimited)')
Chris Sosa3ae4dc12013-03-29 18:47:001557 group.add_option('--proxy_port',
1558 metavar='PORT', default=None, type='int',
1559 help='port to have the client connect to -- basically the '
1560 'devserver lies to the update to tell it to get the payload '
1561 'from a different port that will proxy the request back to '
1562 'the devserver. The proxy must be managed outside the '
1563 'devserver.')
Chris Sosa3ae4dc12013-03-29 18:47:001564 parser.add_option_group(group)
1565
1566
1567def _AddUpdateOptions(parser):
1568 group = optparse.OptionGroup(
1569 parser, 'Autoupdate Options', 'These options can be used to change '
Amin Hassanie9ffb862019-09-26 00:10:401570 'how the devserver serve update payloads. Please '
Chris Sosa3ae4dc12013-03-29 18:47:001571 'note that all of these option affect how a payload is generated and so '
1572 'do not work in archive-only mode.')
Amin Hassani6eec8792020-01-09 22:06:481573 # TODO(crbug/1004487): Deprecate critical_update.
Chris Sosa3ae4dc12013-03-29 18:47:001574 group.add_option('--critical_update',
1575 action='store_true', default=False,
1576 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 18:47:001577 group.add_option('--payload',
1578 metavar='PATH',
1579 help='use the update payload from specified directory '
1580 '(update.gz).')
Chris Sosa3ae4dc12013-03-29 18:47:001581 parser.add_option_group(group)
1582
1583
1584def _AddProductionOptions(parser):
1585 group = optparse.OptionGroup(
1586 parser, 'Advanced Server Options', 'These options can be used to changed '
1587 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 18:47:001588 group.add_option('--clear_cache',
1589 action='store_true', default=False,
1590 help='At startup, removes all cached entries from the'
Amin Hassanid4e35392019-10-03 18:02:441591 "devserver's cache.")
Chris Sosa3ae4dc12013-03-29 18:47:001592 group.add_option('--logfile',
1593 metavar='PATH',
1594 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 20:24:551595 group.add_option('--pidfile',
1596 metavar='PATH',
1597 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 19:04:131598 group.add_option('--portfile',
1599 metavar='PATH',
1600 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 18:47:001601 group.add_option('--production',
1602 action='store_true', default=False,
1603 help='have the devserver use production values when '
1604 'starting up. This includes using more threads and '
1605 'performing less logging.')
1606 parser.add_option_group(group)
1607
1608
Paul Hobbsef4e0702016-06-28 00:01:421609def MakeLogHandler(logfile):
J. Richard Barnette3d977b82013-04-23 18:05:191610 """Create a LogHandler instance used to log all messages."""
1611 hdlr_cls = handlers.TimedRotatingFileHandler
1612 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
xixuan3d48bff2017-01-31 03:00:091613 interval=_LOG_ROTATION_INTERVAL,
J. Richard Barnette3d977b82013-04-23 18:05:191614 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 20:24:551615 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 18:05:191616 return hdlr
1617
1618
Chris Sosacde6bf42012-06-01 01:36:391619def main():
Chris Sosa3ae4dc12013-03-29 18:47:001620 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 21:47:021621 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 23:39:341622
1623 # get directory that the devserver is run from
1624 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 16:17:231625 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 23:39:341626 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 17:09:421627 metavar='PATH',
joychen84d13772013-08-06 16:17:231628 default=default_static_dir,
joychened64b222013-06-21 23:39:341629 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 17:09:421630 parser.add_option('--port',
1631 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 21:13:281632 help=('port for the dev server to use; if zero, binds to '
1633 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 17:09:421634 parser.add_option('-t', '--test_image',
1635 action='store_true',
joychen121fc9b2013-08-02 21:30:301636 help='Deprecated.')
joychen5260b9a2013-07-16 21:48:011637 parser.add_option('-x', '--xbuddy_manage_builds',
1638 action='store_true',
1639 default=False,
1640 help='If set, allow xbuddy to manage images in'
1641 'build/images.')
Dan Shi72b16132015-10-08 19:10:331642 parser.add_option('-a', '--android_build_credential',
1643 default=None,
1644 help='Path to a json file which contains the credential '
1645 'needed to access Android builds.')
Sanika Kulkarnid4496fd2020-02-05 01:26:251646 parser.add_option('--infra_removal',
1647 action='store_true', default=False,
1648 help='If option is present, some RPCs will be disabled to '
1649 'help with infra removal efforts. See '
1650 'go/devserver-deprecation')
Chris Sosa3ae4dc12013-03-29 18:47:001651 _AddProductionOptions(parser)
1652 _AddUpdateOptions(parser)
1653 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-12 02:49:011654 (options, _) = parser.parse_args()
[email protected]21a5ca32009-11-04 18:23:231655
J. Richard Barnette3d977b82013-04-23 18:05:191656 # Handle options that must be set globally in cherrypy. Do this
1657 # work up front, because calls to _Log() below depend on this
1658 # initialization.
1659 if options.production:
1660 cherrypy.config.update({'environment': 'production'})
Sanika Kulkarnid4496fd2020-02-05 01:26:251661 cherrypy.config.update({'infra_removal': options.infra_removal})
J. Richard Barnette3d977b82013-04-23 18:05:191662 if not options.logfile:
1663 cherrypy.config.update({'log.screen': True})
1664 else:
1665 cherrypy.config.update({'log.error_file': '',
1666 'log.access_file': ''})
Paul Hobbsef4e0702016-06-28 00:01:421667 hdlr = MakeLogHandler(options.logfile)
J. Richard Barnette3d977b82013-04-23 18:05:191668 # Pylint can't seem to process these two calls properly
1669 # pylint: disable=E1101
1670 cherrypy.log.access_log.addHandler(hdlr)
1671 cherrypy.log.error_log.addHandler(hdlr)
1672 # pylint: enable=E1101
1673
joychened64b222013-06-21 23:39:341674 # set static_dir, from which everything will be served
joychen84d13772013-08-06 16:17:231675 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 22:46:221676
joychened64b222013-06-21 23:39:341677 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 18:05:191678 # If our devserver is only supposed to serve payloads, we shouldn't be
1679 # mucking with the cache at all. If the devserver hadn't previously
1680 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 18:14:071681 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 21:39:111682 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 20:12:171683 else:
1684 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-17 01:36:141685
Chris Sosadbc20082012-12-10 21:39:111686 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 23:39:341687 _Log('Serving from %s' % options.static_dir)
[email protected]21a5ca32009-11-04 18:23:231688
Amin Hassanie9ffb862019-09-26 00:10:401689 _xbuddy = xbuddy.XBuddy(manage_builds=options.xbuddy_manage_builds,
joychen121fc9b2013-08-02 21:30:301690 static_dir=options.static_dir)
Chris Sosa75490802013-10-01 00:21:451691 if options.clear_cache and options.xbuddy_manage_builds:
1692 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 21:30:301693
Chris Sosa6a3697f2013-01-30 00:44:431694 # We allow global use here to share with cherrypy classes.
1695 # pylint: disable=W0603
Chris Sosacde6bf42012-06-01 01:36:391696 global updater
Andrew de los Reyes52620802010-04-12 20:40:071697 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 21:30:301698 _xbuddy,
joychened64b222013-06-21 23:39:341699 static_dir=options.static_dir,
Gilad Arnold0c9c8602012-10-03 06:58:581700 payload_path=options.payload,
Don Garrett0ad09372010-12-07 00:20:301701 proxy_port=options.proxy_port,
Satoru Takabayashid733cbe2011-11-15 17:36:321702 critical_update=options.critical_update,
Gilad Arnolda564b4b2012-10-04 17:32:441703 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 19:52:231704 host_log=options.host_log,
Chris Sosa0f1ec842011-02-15 00:33:221705 )
Chris Sosa7c931362010-10-12 02:49:011706
J. Richard Barnette3d977b82013-04-23 18:05:191707 if options.exit:
1708 return
Chris Sosa2f1c41e2012-07-10 21:32:331709
joychen3cb228e2013-06-12 19:13:131710 dev_server = DevServerRoot(_xbuddy)
Congbin Guo3afae6c2019-08-13 23:29:421711 health_checker_app = health_checker.Root(dev_server, options.static_dir)
joychen3cb228e2013-06-12 19:13:131712
Chris Sosa855b8932013-08-21 20:24:551713 if options.pidfile:
1714 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1715
Gilad Arnold11fbef42014-02-10 19:04:131716 if options.portfile:
1717 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1718
Dan Shiafd5c6c2016-01-07 18:27:031719 if (options.android_build_credential and
1720 os.path.exists(options.android_build_credential)):
1721 try:
1722 with open(options.android_build_credential) as f:
1723 android_build.BuildAccessor.credential_info = json.load(f)
1724 except ValueError as e:
1725 _Log('Failed to load the android build credential: %s. Error: %s.' %
1726 (options.android_build_credential, e))
Congbin Guo3afae6c2019-08-13 23:29:421727
1728 cherrypy.tree.mount(health_checker_app, '/check_health',
1729 config=health_checker.get_config())
joychen3cb228e2013-06-12 19:13:131730 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-06-01 01:36:391731
1732
1733if __name__ == '__main__':
1734 main()