blob: 2d156f3692d0331ddf5940bfceeb79daac252866 [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
Richard Barnettedf35c322017-08-19 00:02:1349# This must happen before any local modules get a chance to import
50# anything from chromite. Otherwise, really bad things will happen, and
51# you will _not_ understand why.
Congbin Guo3afae6c2019-08-13 23:29:4252import setup_chromite # pylint: disable=unused-import
Richard Barnettedf35c322017-08-19 00:02:1353
Dan Shi2f136862016-02-11 23:38:3854import artifact_info
Congbin Guo3afae6c2019-08-13 23:29:4255import autoupdate
Chris Sosa75490802013-10-01 00:21:4556import build_artifact
Gilad Arnold11fbef42014-02-10 19:04:1357import cherrypy_ext
Gilad Arnoldc65330c2012-09-20 22:17:4858import common_util
Simran Basief83d6a2014-08-28 21:32:0159import devserver_constants
Chris Sosa47a7d4e2012-03-28 18:26:5560import downloader
Congbin Guo3afae6c2019-08-13 23:29:4261import health_checker
Gilad Arnoldc65330c2012-09-20 22:17:4862import log_util
joychen3cb228e2013-06-12 19:13:1363import xbuddy
Gilad Arnoldc65330c2012-09-20 22:17:4864
Gilad Arnoldc65330c2012-09-20 22:17:4865# Module-local log function.
Chris Sosa6a3697f2013-01-30 00:44:4366def _Log(message, *args):
67 return log_util.LogWithTag('DEVSERVER', message, *args)
Chris Sosa0356d3b2010-09-16 22:46:2268
Dan Shi94dcbe82015-06-09 03:51:1369
xixuanac89ce82016-12-01 00:48:2070# Use try-except to skip unneccesary import for simple use case, eg. running
71# devserver on host.
72try:
73 import cros_update
xixuanac89ce82016-12-01 00:48:2074except ImportError as e:
75 _Log('cros_update cannot be imported: %r', e)
76 cros_update = None
xixuana4f4e712017-05-08 22:17:5477
78try:
79 import cros_update_progress
80except ImportError as e:
81 _Log('cros_update_progress cannot be imported: %r', e)
xixuanac89ce82016-12-01 00:48:2082 cros_update_progress = None
83
xixuanac89ce82016-12-01 00:48:2084try:
Dan Shi72b16132015-10-08 19:10:3385 import android_build
Amin Hassanid4e35392019-10-03 18:02:4486except ImportError:
Dan Shi72b16132015-10-08 19:10:3387 # Ignore android_build import failure. This is to support devserver running
88 # inside a ChromeOS device triggered by cros flash. Most ChromeOS test images
89 # do not have google-api-python-client module and they don't need to support
90 # Android updating, therefore, ignore the import failure here.
Dan Shi72b16132015-10-08 19:10:3391 android_build = None
Frank Farzan40160872011-12-13 02:39:1892
Chris Sosa417e55d2011-01-26 00:40:4893CACHED_ENTRIES = 12
Don Garrettf90edf02010-11-17 01:36:1494
Simran Basi4baad082013-02-14 21:39:1895TELEMETRY_FOLDER = 'telemetry_src'
96TELEMETRY_DEPS = ['dep-telemetry_dep.tar.bz2',
97 'dep-page_cycler_dep.tar.bz2',
Simran Basi0d078682013-03-22 23:40:0498 'dep-chrome_test.tar.bz2',
99 'dep-perf_data_dep.tar.bz2']
Simran Basi4baad082013-02-14 21:39:18100
Chris Sosa0356d3b2010-09-16 22:46:22101# Sets up global to share between classes.
[email protected]21a5ca32009-11-04 18:23:23102updater = None
[email protected]ded22402009-10-26 22:36:21103
xixuan3d48bff2017-01-31 03:00:09104# Log rotation parameters. These settings correspond to twice a day once
105# devserver is started, with about two weeks (28 backup files) of old logs
106# kept for backup.
J. Richard Barnette3d977b82013-04-23 18:05:19107#
xixuan3d48bff2017-01-31 03:00:09108# For more, see the documentation in standard python library for
J. Richard Barnette3d977b82013-04-23 18:05:19109# logging.handlers.TimedRotatingFileHandler
xixuan3d48bff2017-01-31 03:00:09110_LOG_ROTATION_TIME = 'H'
Congbin Guo3afae6c2019-08-13 23:29:42111_LOG_ROTATION_INTERVAL = 12 # hours
112_LOG_ROTATION_BACKUP = 28 # backup counts
Frank Farzan40160872011-12-13 02:39:18113
xixuan52c2fba2016-05-21 00:02:48114# Auto-update parameters
115
116# Error msg for missing key in CrOS auto-update.
Xixuan Wu32af9f12017-11-13 22:11:44117KEY_ERROR_MSG = 'Key Error in RPC: %s= is required'
xixuan52c2fba2016-05-21 00:02:48118
xixuan52c2fba2016-05-21 00:02:48119
Amin Hassanid4e35392019-10-03 18:02:44120class DevServerError(Exception):
121 """Exception class used by DevServer."""
122
123
Gabe Black3b567202015-09-23 21:07:59124def _canonicalize_archive_url(archive_url):
125 """Canonicalizes archive_url strings.
126
127 Raises:
128 DevserverError: if archive_url is not set.
129 """
130 if archive_url:
131 if not archive_url.startswith('gs://'):
Amin Hassanid4e35392019-10-03 18:02:44132 raise DevServerError(
Congbin Guo4132a272019-08-20 19:32:14133 "Archive URL isn't from Google Storage (%s) ." % archive_url)
Gabe Black3b567202015-09-23 21:07:59134
135 return archive_url.rstrip('/')
136 else:
Amin Hassanid4e35392019-10-03 18:02:44137 raise DevServerError('Must specify an archive_url in the request')
Gabe Black3b567202015-09-23 21:07:59138
139
140def _canonicalize_local_path(local_path):
141 """Canonicalizes |local_path| strings.
142
143 Raises:
144 DevserverError: if |local_path| is not set.
145 """
146 # Restrict staging of local content to only files within the static
147 # directory.
148 local_path = os.path.abspath(local_path)
149 if not local_path.startswith(updater.static_dir):
Amin Hassanid4e35392019-10-03 18:02:44150 raise DevServerError(
Congbin Guo4132a272019-08-20 19:32:14151 'Local path %s must be a subdirectory of the static'
152 ' directory: %s' % (local_path, updater.static_dir))
Gabe Black3b567202015-09-23 21:07:59153
154 return local_path.rstrip('/')
155
156
157def _get_artifacts(kwargs):
158 """Returns a tuple of named and file artifacts given the stage rpc kwargs.
159
160 Raises:
161 DevserverError if no artifacts would be returned.
162 """
163 artifacts = kwargs.get('artifacts')
164 files = kwargs.get('files')
165 if not artifacts and not files:
Amin Hassanid4e35392019-10-03 18:02:44166 raise DevServerError('No artifacts specified.')
Gabe Black3b567202015-09-23 21:07:59167
168 # Note we NEED to coerce files to a string as we get raw unicode from
169 # cherrypy and we treat files as strings elsewhere in the code.
170 return (str(artifacts).split(',') if artifacts else [],
171 str(files).split(',') if files else [])
172
173
Dan Shi61305df2015-10-26 23:52:35174def _is_android_build_request(kwargs):
175 """Check if a devserver call is for Android build, based on the arguments.
176
177 This method exams the request's arguments (os_type) to determine if the
178 request is for Android build. If os_type is set to `android`, returns True.
179 If os_type is not set or has other values, returns False.
180
181 Args:
182 kwargs: Keyword arguments for the request.
183
184 Returns:
185 True if the request is for Android build. False otherwise.
186 """
187 os_type = kwargs.get('os_type', None)
188 return os_type == 'android'
189
190
Gabe Black3b567202015-09-23 21:07:59191def _get_downloader(kwargs):
192 """Returns the downloader based on passed in arguments.
193
194 Args:
Amin Hassani08e42d22019-06-03 07:31:30195 kwargs: Keyword arguments for the request.
Gabe Black3b567202015-09-23 21:07:59196 """
197 local_path = kwargs.get('local_path')
198 if local_path:
199 local_path = _canonicalize_local_path(local_path)
200
201 dl = None
202 if local_path:
Prathmesh Prabhu58d08932018-01-19 23:08:19203 delete_source = _parse_boolean_arg(kwargs, 'delete_source')
204 dl = downloader.LocalDownloader(updater.static_dir, local_path,
205 delete_source=delete_source)
Gabe Black3b567202015-09-23 21:07:59206
Dan Shi61305df2015-10-26 23:52:35207 if not _is_android_build_request(kwargs):
Gabe Black3b567202015-09-23 21:07:59208 archive_url = kwargs.get('archive_url')
209 if not archive_url and not local_path:
Amin Hassanid4e35392019-10-03 18:02:44210 raise DevServerError(
Congbin Guo4132a272019-08-20 19:32:14211 'Requires archive_url or local_path to be specified.')
Gabe Black3b567202015-09-23 21:07:59212 if archive_url and local_path:
Amin Hassanid4e35392019-10-03 18:02:44213 raise DevServerError(
Congbin Guo4132a272019-08-20 19:32:14214 'archive_url and local_path can not both be specified.')
Gabe Black3b567202015-09-23 21:07:59215 if not dl:
216 archive_url = _canonicalize_archive_url(archive_url)
Luis Hector Chavezdca9dd72018-06-12 19:56:30217 dl = downloader.GoogleStorageDownloader(
218 updater.static_dir, archive_url,
219 downloader.GoogleStorageDownloader.GetBuildIdFromArchiveURL(
220 archive_url))
Gabe Black3b567202015-09-23 21:07:59221 elif not dl:
222 target = kwargs.get('target', None)
Dan Shi72b16132015-10-08 19:10:33223 branch = kwargs.get('branch', None)
Dan Shi61305df2015-10-26 23:52:35224 build_id = kwargs.get('build_id', None)
225 if not target or not branch or not build_id:
Amin Hassanid4e35392019-10-03 18:02:44226 raise DevServerError('target, branch, build ID must all be specified for '
227 'downloading Android build.')
Dan Shi72b16132015-10-08 19:10:33228 dl = downloader.AndroidBuildDownloader(updater.static_dir, branch, build_id,
229 target)
Gabe Black3b567202015-09-23 21:07:59230
231 return dl
232
233
234def _get_downloader_and_factory(kwargs):
235 """Returns the downloader and artifact factory based on passed in arguments.
236
237 Args:
Amin Hassani08e42d22019-06-03 07:31:30238 kwargs: Keyword arguments for the request.
Gabe Black3b567202015-09-23 21:07:59239 """
240 artifacts, files = _get_artifacts(kwargs)
241 dl = _get_downloader(kwargs)
242
Achuith Bhandarkar2a1fcd82019-10-18 00:45:58243 if (isinstance(dl, (downloader.GoogleStorageDownloader,
244 downloader.LocalDownloader))):
Gabe Black3b567202015-09-23 21:07:59245 factory_class = build_artifact.ChromeOSArtifactFactory
Dan Shi72b16132015-10-08 19:10:33246 elif isinstance(dl, downloader.AndroidBuildDownloader):
Gabe Black3b567202015-09-23 21:07:59247 factory_class = build_artifact.AndroidArtifactFactory
248 else:
Amin Hassanid4e35392019-10-03 18:02:44249 raise DevServerError(
Congbin Guo4132a272019-08-20 19:32:14250 'Unrecognized value for downloader type: %s' % type(dl))
Gabe Black3b567202015-09-23 21:07:59251
252 factory = factory_class(dl.GetBuildDir(), artifacts, files, dl.GetBuild())
253
254 return dl, factory
255
256
Scott Zawalski4647ce62012-01-03 22:17:28257def _LeadingWhiteSpaceCount(string):
258 """Count the amount of leading whitespace in a string.
259
260 Args:
261 string: The string to count leading whitespace in.
Don Garrettf84631a2014-01-08 02:21:26262
Scott Zawalski4647ce62012-01-03 22:17:28263 Returns:
264 number of white space chars before characters start.
265 """
Gabe Black3b567202015-09-23 21:07:59266 matched = re.match(r'^\s+', string)
Scott Zawalski4647ce62012-01-03 22:17:28267 if matched:
268 return len(matched.group())
269
270 return 0
271
272
273def _PrintDocStringAsHTML(func):
274 """Make a functions docstring somewhat HTML style.
275
276 Args:
277 func: The function to return the docstring from.
Don Garrettf84631a2014-01-08 02:21:26278
Scott Zawalski4647ce62012-01-03 22:17:28279 Returns:
280 A string that is somewhat formated for a web browser.
281 """
282 # TODO(scottz): Make this parse Args/Returns in a prettier way.
283 # Arguments could be bolded and indented etc.
284 html_doc = []
285 for line in func.__doc__.splitlines():
286 leading_space = _LeadingWhiteSpaceCount(line)
287 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 18:26:55288 line = ' ' * leading_space + line
Scott Zawalski4647ce62012-01-03 22:17:28289
290 html_doc.append('<BR>%s' % line)
291
292 return '\n'.join(html_doc)
293
294
Simran Basief83d6a2014-08-28 21:32:01295def _GetUpdateTimestampHandler(static_dir):
296 """Returns a handler to update directory staged.timestamp.
297
298 This handler resets the stage.timestamp whenever static content is accessed.
299
300 Args:
301 static_dir: Directory from which static content is being staged.
302
303 Returns:
Amin Hassani08e42d22019-06-03 07:31:30304 A cherrypy handler to update the timestamp of accessed content.
Simran Basief83d6a2014-08-28 21:32:01305 """
306 def UpdateTimestampHandler():
307 if not '404' in cherrypy.response.status:
308 build_match = re.match(devserver_constants.STAGED_BUILD_REGEX,
309 cherrypy.request.path_info)
310 if build_match:
311 build_dir = os.path.join(static_dir, build_match.group('build'))
312 downloader.Downloader.TouchTimestampForStaged(build_dir)
313 return UpdateTimestampHandler
314
315
Chris Sosa7c931362010-10-12 02:49:01316def _GetConfig(options):
317 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-08 01:55:33318
Mandeep Singh Baines38dcdda2012-12-08 01:55:33319 socket_host = '::'
Yu-Ju Hongc8d4af32013-11-12 23:14:26320 # Fall back to IPv4 when python is not configured with IPv6.
321 if not socket.has_ipv6:
Mandeep Singh Baines38dcdda2012-12-08 01:55:33322 socket_host = '0.0.0.0'
323
Simran Basief83d6a2014-08-28 21:32:01324 # Adds the UpdateTimestampHandler to cherrypy's tools. This tools executes
325 # on the on_end_resource hook. This hook is called once processing is
326 # complete and the response is ready to be returned.
327 cherrypy.tools.update_timestamp = cherrypy.Tool(
328 'on_end_resource', _GetUpdateTimestampHandler(options.static_dir))
329
David Riley2fcb0122017-11-02 18:25:39330 base_config = {
331 'global': {
332 'server.log_request_headers': True,
333 'server.protocol_version': 'HTTP/1.1',
334 'server.socket_host': socket_host,
335 'server.socket_port': int(options.port),
336 'response.timeout': 6000,
337 'request.show_tracebacks': True,
338 'server.socket_timeout': 60,
339 'server.thread_pool': 2,
340 'engine.autoreload.on': False,
341 },
342 '/api': {
343 # Gets rid of cherrypy parsing post file for args.
344 'request.process_request_body': False,
345 },
346 '/build': {
347 'response.timeout': 100000,
348 },
349 '/update': {
350 # Gets rid of cherrypy parsing post file for args.
351 'request.process_request_body': False,
352 'response.timeout': 10000,
353 },
354 # Sets up the static dir for file hosting.
355 '/static': {
356 'tools.staticdir.dir': options.static_dir,
357 'tools.staticdir.on': True,
358 'response.timeout': 10000,
359 'tools.update_timestamp.on': True,
360 },
361 }
Chris Sosa5f118ef2012-07-12 18:37:50362 if options.production:
Alex Miller93beca52013-07-31 02:25:09363 base_config['global'].update({'server.thread_pool': 150})
Scott Zawalski1c5e7cd2012-02-27 18:12:52364
Chris Sosa7c931362010-10-12 02:49:01365 return base_config
[email protected]64244662009-11-12 00:52:08366
Darin Petkove17164a2010-08-11 20:24:41367
Gilad Arnoldd5ebaaa2012-10-02 18:52:38368def _GetRecursiveMemberObject(root, member_list):
369 """Returns an object corresponding to a nested member list.
370
371 Args:
372 root: the root object to search
373 member_list: list of nested members to search
Don Garrettf84631a2014-01-08 02:21:26374
Gilad Arnoldd5ebaaa2012-10-02 18:52:38375 Returns:
376 An object corresponding to the member name list; None otherwise.
377 """
378 for member in member_list:
379 next_root = root.__class__.__dict__.get(member)
380 if not next_root:
381 return None
382 root = next_root
383 return root
384
385
386def _IsExposed(name):
387 """Returns True iff |name| has an `exposed' attribute and it is set."""
388 return hasattr(name, 'exposed') and name.exposed
389
390
Congbin Guo6bc32182019-08-21 00:54:30391def _GetExposedMethod(nested_member):
Gilad Arnoldd5ebaaa2012-10-02 18:52:38392 """Returns a CherryPy-exposed method, if such exists.
393
394 Args:
Gilad Arnoldd5ebaaa2012-10-02 18:52:38395 nested_member: a slash-joined path to the nested member
Don Garrettf84631a2014-01-08 02:21:26396
Gilad Arnoldd5ebaaa2012-10-02 18:52:38397 Returns:
Congbin Guo6bc32182019-08-21 00:54:30398 A function object corresponding to the path defined by |nested_member| from
399 the app root object registered, if the function is exposed; None otherwise.
Gilad Arnoldd5ebaaa2012-10-02 18:52:38400 """
Congbin Guo6bc32182019-08-21 00:54:30401 for app in cherrypy.tree.apps.values():
402 # Use the 'index' function doc as the doc of the app.
403 if nested_member == app.script_name.lstrip('/'):
404 nested_member = 'index'
405
406 method = _GetRecursiveMemberObject(app.root, nested_member.split('/'))
407 if method and isinstance(method, types.FunctionType) and _IsExposed(method):
408 return method
Gilad Arnoldd5ebaaa2012-10-02 18:52:38409
410
Gilad Arnold748c8322012-10-12 16:51:35411def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 18:52:38412 """Finds exposed CherryPy methods.
413
414 Args:
415 root: the root object for searching
416 prefix: slash-joined chain of members leading to current object
417 unlisted: URLs to be excluded regardless of their exposed status
Don Garrettf84631a2014-01-08 02:21:26418
Gilad Arnoldd5ebaaa2012-10-02 18:52:38419 Returns:
420 List of exposed URLs that are not unlisted.
421 """
422 method_list = []
Congbin Guo6bc32182019-08-21 00:54:30423 for member in root.__class__.__dict__.keys():
Gilad Arnoldd5ebaaa2012-10-02 18:52:38424 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 16:51:35425 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 18:52:38426 continue
427 member_obj = root.__class__.__dict__[member]
428 if _IsExposed(member_obj):
Amin Hassani08e42d22019-06-03 07:31:30429 if isinstance(member_obj, types.FunctionType):
Congbin Guo6bc32182019-08-21 00:54:30430 # Regard the app name as exposed "method" name if it exposed 'index'
431 # function.
432 if prefix and member == 'index':
433 method_list.append(prefix)
434 else:
435 method_list.append(prefixed_member)
Gilad Arnoldd5ebaaa2012-10-02 18:52:38436 else:
437 method_list += _FindExposedMethods(
438 member_obj, prefixed_member, unlisted)
439 return method_list
440
441
xixuan52c2fba2016-05-21 00:02:48442def _check_base_args_for_auto_update(kwargs):
xixuanac89ce82016-12-01 00:48:20443 """Check basic args required for auto-update.
444
445 Args:
446 kwargs: the parameters to be checked.
447
448 Raises:
449 DevServerHTTPError if required parameters don't exist in kwargs.
450 """
xixuan52c2fba2016-05-21 00:02:48451 if 'host_name' not in kwargs:
Amin Hassanid4e35392019-10-03 18:02:44452 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 07:31:30453 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-21 00:02:48454
455 if 'build_name' not in kwargs:
Amin Hassanid4e35392019-10-03 18:02:44456 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 07:31:30457 KEY_ERROR_MSG % 'build_name')
xixuan52c2fba2016-05-21 00:02:48458
459
460def _parse_boolean_arg(kwargs, key):
xixuanac89ce82016-12-01 00:48:20461 """Parse boolean arg from kwargs.
462
463 Args:
464 kwargs: the parameters to be checked.
465 key: the key to be parsed.
466
467 Returns:
468 The boolean value of kwargs[key], or False if key doesn't exist in kwargs.
469
470 Raises:
471 DevServerHTTPError if kwargs[key] is not a boolean variable.
472 """
xixuan52c2fba2016-05-21 00:02:48473 if key in kwargs:
474 if kwargs[key] == 'True':
475 return True
476 elif kwargs[key] == 'False':
477 return False
478 else:
479 raise common_util.DevServerHTTPError(
Amin Hassanid4e35392019-10-03 18:02:44480 http_client.INTERNAL_SERVER_ERROR,
xixuan52c2fba2016-05-21 00:02:48481 'The value for key %s is not boolean.' % key)
482 else:
483 return False
484
xixuan447ad9d2017-02-28 22:46:20485
xixuanac89ce82016-12-01 00:48:20486def _parse_string_arg(kwargs, key):
487 """Parse string arg from kwargs.
488
489 Args:
490 kwargs: the parameters to be checked.
491 key: the key to be parsed.
492
493 Returns:
494 The string value of kwargs[key], or None if key doesn't exist in kwargs.
495 """
496 if key in kwargs:
497 return kwargs[key]
498 else:
499 return None
500
xixuan447ad9d2017-02-28 22:46:20501
xixuanac89ce82016-12-01 00:48:20502def _build_uri_from_build_name(build_name):
503 """Get build url from a given build name.
504
505 Args:
506 build_name: the build name to be parsed, whose format is
507 'board/release_version'.
508
509 Returns:
510 The release_archive_url on Google Storage for this build name.
511 """
Amin Hassani08e42d22019-06-03 07:31:30512 # TODO(ahassani): This function doesn't seem to be used anywhere since its
513 # previous use of lib.paygen.gspath was broken and it doesn't seem to be
514 # causing any runtime issues. So deprecate this in the future.
515 tokens = build_name.split('/')
516 return 'gs://chromeos-releases/stable-channel/%s/%s' % (tokens[0], tokens[1])
xixuan52c2fba2016-05-21 00:02:48517
xixuan447ad9d2017-02-28 22:46:20518
519def _clear_process(host_name, pid):
520 """Clear AU process for given hostname and pid.
521
522 This clear includes:
523 1. kill process if it's alive.
524 2. delete the track status file of this process.
525 3. delete the executing log file of this process.
526
527 Args:
528 host_name: the host to execute auto-update.
529 pid: the background auto-update process id.
530 """
531 if cros_update_progress.IsProcessAlive(pid):
532 os.killpg(int(pid), signal.SIGKILL)
533
534 cros_update_progress.DelTrackStatusFile(host_name, pid)
535 cros_update_progress.DelExecuteLogFile(host_name, pid)
536
537
Dale Curtisc9aaf3a2011-08-09 22:47:40538class ApiRoot(object):
539 """RESTful API for Dev Server information."""
540 exposed = True
541
542 @cherrypy.expose
Gilad Arnold286a0062012-01-12 21:47:02543 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 18:36:27544 """Returns a JSON object containing a log of host event.
545
546 Args:
547 ip: address of host whose event log is requested, or `all'
Don Garrettf84631a2014-01-08 02:21:26548
Gilad Arnold1b908392012-10-05 18:36:27549 Returns:
Amin Hassani7c447852019-09-26 22:01:48550 A JSON dictionary containing all or some of the following fields:
Amin Hassanie7ead902019-10-11 23:42:43551 version: The Chromium OS version the device is running.
552 track: The channel the device is running on.
553 board: The device's board.
554 event_result: The event result of Omaha request.
555 event_type: The event type of Omaha request.
556 previous_version: The Chromium OS version we updated and rebooted from.
557 timestamp: The timestamp the event was received.
Amin Hassani7c447852019-09-26 22:01:48558 See the OmahaEvent class in update_engine/omaha_request_action.h for
559 event type and status code definitions. If the ip does not exist an empty
560 string is returned.
Gilad Arnold1b908392012-10-05 18:36:27561
562 Example URL:
563 http://myhost/api/hostlog?ip=192.168.1.5
564 """
Gilad Arnold286a0062012-01-12 21:47:02565 return updater.HandleHostLogPing(ip)
566
567 @cherrypy.expose
Don Garrettf84631a2014-01-08 02:21:26568 def fileinfo(self, *args):
Gilad Arnold55a2a372012-10-02 16:46:32569 """Returns information about a given staged file.
570
571 Args:
Don Garrettf84631a2014-01-08 02:21:26572 args: path to the file inside the server's static staging directory
573
Gilad Arnold55a2a372012-10-02 16:46:32574 Returns:
575 A JSON encoded dictionary with information about the said file, which may
576 contain the following keys/values:
Gilad Arnold1b908392012-10-05 18:36:27577 size (int): the file size in bytes
Gilad Arnold1b908392012-10-05 18:36:27578 sha256 (string): a base64 encoded SHA256 hash
579
580 Example URL:
581 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 16:46:32582 """
Amin Hassani28df4212019-10-28 17:16:50583 # TODO(ahassani): A better way of doing this is to just return the the
584 # content of the payloads' property file instead. That has all this info
585 # except that the key for sha256 is 'sha256_hex', but still base64 encdoed.
586
Don Garrettf84631a2014-01-08 02:21:26587 file_path = os.path.join(updater.static_dir, *args)
Gilad Arnold55a2a372012-10-02 16:46:32588 if not os.path.exists(file_path):
Amin Hassanid4e35392019-10-03 18:02:44589 raise DevServerError('file not found: %s' % file_path)
Gilad Arnold55a2a372012-10-02 16:46:32590 try:
591 file_size = os.path.getsize(file_path)
Gilad Arnold55a2a372012-10-02 16:46:32592 file_sha256 = common_util.GetFileSha256(file_path)
Amin Hassani469f5702019-10-21 22:35:06593 except os.error as e:
Amin Hassanid4e35392019-10-03 18:02:44594 raise DevServerError(
Congbin Guo4132a272019-08-20 19:32:14595 'failed to get info for file %s: %s' % (file_path, e))
Gilad Arnolde74b3812013-04-22 18:27:38596
Gilad Arnolde74b3812013-04-22 18:27:38597 return json.dumps({
Amin Hassani28df4212019-10-28 17:16:50598 'size': file_size,
599 'sha256': file_sha256,
600 }, sort_keys=True)
Gilad Arnold55a2a372012-10-02 16:46:32601
Chris Sosa76e44b92013-01-31 20:11:38602
David Rochberg7c79a812011-01-19 19:24:45603class DevServerRoot(object):
Chris Sosa7c931362010-10-12 02:49:01604 """The Root Class for the Dev Server.
605
606 CherryPy works as follows:
607 For each method in this class, cherrpy interprets root/path
608 as a call to an instance of DevServerRoot->method_name. For example,
609 a call to http://myhost/build will call build. CherryPy automatically
610 parses http args and places them as keyword arguments in each method.
611 For paths http://myhost/update/dir1/dir2, you can use *args so that
612 cherrypy uses the update method and puts the extra paths in args.
613 """
Gilad Arnoldf8f769f2012-09-24 15:43:01614 # Method names that should not be listed on the index page.
615 _UNLISTED_METHODS = ['index', 'doc']
616
Dale Curtisc9aaf3a2011-08-09 22:47:40617 api = ApiRoot()
Chris Sosa7c931362010-10-12 02:49:01618
Dan Shi59ae7092013-06-04 21:37:27619 # Number of threads that devserver is staging images.
620 _staging_thread_count = 0
621 # Lock used to lock increasing/decreasing count.
622 _staging_thread_count_lock = threading.Lock()
623
joychen3cb228e2013-06-12 19:13:13624 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 22:20:41625 self._builder = None
Simran Basi4baad082013-02-14 21:39:18626 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 19:13:13627 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 19:24:45628
Congbin Guo3afae6c2019-08-13 23:29:42629 @property
630 def staging_thread_count(self):
631 """Get the staging thread count."""
632 return self._staging_thread_count
Dan Shiafd0e492015-05-27 21:23:51633
Dale Curtisc9aaf3a2011-08-09 22:47:40634 @cherrypy.expose
David Rochberg7c79a812011-01-19 19:24:45635 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-12 02:49:01636 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 22:20:41637 import builder
638 if self._builder is None:
639 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 19:24:45640 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-12 02:49:01641
Dale Curtisc9aaf3a2011-08-09 22:47:40642 @cherrypy.expose
Dan Shif8eb0d12013-08-02 00:52:06643 def is_staged(self, **kwargs):
644 """Check if artifacts have been downloaded.
645
Congbin Guo3afae6c2019-08-13 23:29:42646 Examples:
647 To check if autotest and test_suites are staged:
648 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
649 artifacts=autotest,test_suites
650
Amin Hassani08e42d22019-06-03 07:31:30651 Args:
Chris Sosa6b0c6172013-08-06 00:01:33652 async: True to return without waiting for download to complete.
653 artifacts: Comma separated list of named artifacts to download.
654 These are defined in artifact_info and have their implementation
655 in build_artifact.py.
656 files: Comma separated list of file artifacts to stage. These
657 will be available as is in the corresponding static directory with no
658 custom post-processing.
659
Congbin Guo3afae6c2019-08-13 23:29:42660 Returns:
661 True of all artifacts are staged.
Dan Shif8eb0d12013-08-02 00:52:06662 """
Gabe Black3b567202015-09-23 21:07:59663 dl, factory = _get_downloader_and_factory(kwargs)
Aviv Keshet57d18172016-06-19 03:39:09664 response = str(dl.IsStaged(factory))
665 _Log('Responding to is_staged %s request with %r', kwargs, response)
666 return response
Dan Shi59ae7092013-06-04 21:37:27667
Chris Sosa76e44b92013-01-31 20:11:38668 @cherrypy.expose
Prashanth Ba06d2d22014-03-07 23:35:19669 def list_image_dir(self, **kwargs):
670 """Take an archive url and list the contents in its staged directory.
671
Amin Hassani08e42d22019-06-03 07:31:30672 Examples:
Prashanth Ba06d2d22014-03-07 23:35:19673 To list the contents of where this devserver should have staged
674 gs://image-archive/<board>-release/<build> call:
675 http://devserver_url:<port>/list_image_dir?archive_url=<gs://..>
676
Congbin Guo3afae6c2019-08-13 23:29:42677 Args:
678 archive_url: Google Storage URL for the build.
679
Prashanth Ba06d2d22014-03-07 23:35:19680 Returns:
681 A string with information about the contents of the image directory.
682 """
Gabe Black3b567202015-09-23 21:07:59683 dl = _get_downloader(kwargs)
Prashanth Ba06d2d22014-03-07 23:35:19684 try:
Gabe Black3b567202015-09-23 21:07:59685 image_dir_contents = dl.ListBuildDir()
Prashanth Ba06d2d22014-03-07 23:35:19686 except build_artifact.ArtifactDownloadError as e:
687 return 'Cannot list the contents of staged artifacts. %s' % e
688 if not image_dir_contents:
Gabe Black3b567202015-09-23 21:07:59689 return '%s has not been staged on this devserver.' % dl.DescribeSource()
Prashanth Ba06d2d22014-03-07 23:35:19690 return image_dir_contents
691
692 @cherrypy.expose
Chris Sosa76e44b92013-01-31 20:11:38693 def stage(self, **kwargs):
Gabe Black3b567202015-09-23 21:07:59694 """Downloads and caches build artifacts.
Chris Sosa76e44b92013-01-31 20:11:38695
Gabe Black3b567202015-09-23 21:07:59696 Downloads and caches build artifacts, possibly from a Google Storage URL,
Dan Shi72b16132015-10-08 19:10:33697 or from Android's build server. Returns once these have been downloaded
Gabe Black3b567202015-09-23 21:07:59698 on the devserver. A call to this will attempt to cache non-specified
699 artifacts in the background for the given from the given URL following
700 the principle of spatial locality. Spatial locality of different
Chris Sosa76e44b92013-01-31 20:11:38701 artifacts is explicitly defined in the build_artifact module.
702
703 These artifacts will then be available from the static/ sub-directory of
704 the devserver.
705
Amin Hassani08e42d22019-06-03 07:31:30706 Examples:
Chris Sosa76e44b92013-01-31 20:11:38707 To download the autotest and test suites tarballs:
708 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
709 artifacts=autotest,test_suites
710 To download the full update payload:
711 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
712 artifacts=full_payload
Chris Sosa6b0c6172013-08-06 00:01:33713 To download just a file called blah.bin:
714 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
715 files=blah.bin
Chris Sosa76e44b92013-01-31 20:11:38716
717 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 23:39:34718 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 20:11:38719
720 Note for this example, relative path is the archive_url stripped of its
721 basename i.e. path/ in the examples above. Specific example:
722
723 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
724
725 Will get staged to:
726
joychened64b222013-06-21 23:39:34727 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Congbin Guo3afae6c2019-08-13 23:29:42728
729 Args:
730 archive_url: Google Storage URL for the build.
731 local_path: Local path for the build.
732 delete_source: Only meaningful with local_path. bool to indicate if the
733 source files should be deleted. This is especially useful when staging
734 a file locally in resource constrained environments as it allows us to
735 move the relevant files locally instead of copying them.
736 async: True to return without waiting for download to complete.
737 artifacts: Comma separated list of named artifacts to download.
738 These are defined in artifact_info and have their implementation
739 in build_artifact.py.
740 files: Comma separated list of files to stage. These
741 will be available as is in the corresponding static directory with no
742 custom post-processing.
743 clean: True to remove any previously staged artifacts first.
Chris Sosa76e44b92013-01-31 20:11:38744 """
Gabe Black3b567202015-09-23 21:07:59745 dl, factory = _get_downloader_and_factory(kwargs)
746
Dan Shi59ae7092013-06-04 21:37:27747 with DevServerRoot._staging_thread_count_lock:
748 DevServerRoot._staging_thread_count += 1
749 try:
Laurence Goodbyf5c958d2016-01-15 02:23:56750 boolean_string = kwargs.get('clean')
751 clean = xbuddy.XBuddy.ParseBoolean(boolean_string)
752 if clean and os.path.exists(dl.GetBuildDir()):
753 _Log('Removing %s' % dl.GetBuildDir())
754 shutil.rmtree(dl.GetBuildDir())
Achuith Bhandarkar2a1fcd82019-10-18 00:45:58755 is_async = kwargs.get('async', False)
756 dl.Download(factory, is_async=is_async)
Dan Shi59ae7092013-06-04 21:37:27757 finally:
758 with DevServerRoot._staging_thread_count_lock:
759 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 20:11:38760 return 'Success'
Chris Sosacde6bf42012-06-01 01:36:39761
762 @cherrypy.expose
xixuan52c2fba2016-05-21 00:02:48763 def cros_au(self, **kwargs):
764 """Auto-update a CrOS DUT.
765
766 Args:
767 kwargs:
768 host_name: the hostname of the DUT to auto-update.
769 build_name: the build name for update the DUT.
770 force_update: Force an update even if the version installed is the
771 same. Default: False.
772 full_update: If True, do not run stateful update, directly force a full
773 reimage. If False, try stateful update first if the dut is already
774 installed with the same version.
775 async: Whether the auto_update function is ran in the background.
David Rileyee75de22017-11-02 17:48:15776 quick_provision: Whether the quick provision path is attempted first.
xixuan52c2fba2016-05-21 00:02:48777
778 Returns:
779 A tuple includes two elements:
780 a boolean variable represents whether the auto-update process is
781 successfully started.
782 an integer represents the background auto-update process id.
783 """
784 _check_base_args_for_auto_update(kwargs)
785
786 host_name = kwargs['host_name']
787 build_name = kwargs['build_name']
788 force_update = _parse_boolean_arg(kwargs, 'force_update')
789 full_update = _parse_boolean_arg(kwargs, 'full_update')
Achuith Bhandarkar2a1fcd82019-10-18 00:45:58790 is_async = _parse_boolean_arg(kwargs, 'async')
xixuanac89ce82016-12-01 00:48:20791 original_build = _parse_string_arg(kwargs, 'original_build')
David Haddock90e49442017-04-08 02:14:09792 payload_filename = _parse_string_arg(kwargs, 'payload_filename')
David Haddock20559612017-06-29 05:15:08793 clobber_stateful = _parse_boolean_arg(kwargs, 'clobber_stateful')
David Rileyee75de22017-11-02 17:48:15794 quick_provision = _parse_boolean_arg(kwargs, 'quick_provision')
795
796 devserver_url = updater.GetDevserverUrl()
797 static_url = updater.GetStaticUrl()
xixuan52c2fba2016-05-21 00:02:48798
Achuith Bhandarkar2a1fcd82019-10-18 00:45:58799 if is_async:
Amin Hassani469f5702019-10-21 22:35:06800 # Command of running auto-update.
Amin Hassani78520ae2019-10-29 20:26:51801 path = os.path.dirname(os.path.abspath(__file__))
802 execute_file = os.path.join(path, 'cros_update.py')
803 cmd = ['/usr/bin/python', '-u', execute_file, '-d', host_name,
804 '-b', build_name, '--static_dir', updater.static_dir]
xixuanac89ce82016-12-01 00:48:20805
806 # The original_build's format is like: link/3428.210.0
807 # The corresponding release_archive_url's format is like:
808 # gs://chromeos-releases/stable-channel/link/3428.210.0
809 if original_build:
810 release_archive_url = _build_uri_from_build_name(original_build)
811 # First staging the stateful.tgz synchronousely.
Amin Hassani469f5702019-10-21 22:35:06812 self.stage(files='stateful.tgz', is_async=False,
xixuanac89ce82016-12-01 00:48:20813 archive_url=release_archive_url)
Amin Hassani469f5702019-10-21 22:35:06814 cmd += ['--original_build', original_build]
xixuanac89ce82016-12-01 00:48:20815
xixuan52c2fba2016-05-21 00:02:48816 if force_update:
Amin Hassani469f5702019-10-21 22:35:06817 cmd += ['--force_update']
xixuan52c2fba2016-05-21 00:02:48818
819 if full_update:
Amin Hassani469f5702019-10-21 22:35:06820 cmd += ['--full_update']
xixuan52c2fba2016-05-21 00:02:48821
David Haddock90e49442017-04-08 02:14:09822 if payload_filename:
Amin Hassani469f5702019-10-21 22:35:06823 cmd += ['--payload_filename', payload_filename]
David Haddock90e49442017-04-08 02:14:09824
David Haddock20559612017-06-29 05:15:08825 if clobber_stateful:
Amin Hassani469f5702019-10-21 22:35:06826 cmd += ['--clobber_stateful']
David Haddock20559612017-06-29 05:15:08827
David Rileyee75de22017-11-02 17:48:15828 if quick_provision:
Amin Hassani469f5702019-10-21 22:35:06829 cmd += ['--quick_provision']
David Rileyee75de22017-11-02 17:48:15830
831 if devserver_url:
Amin Hassani469f5702019-10-21 22:35:06832 cmd += ['--devserver_url', devserver_url]
David Rileyee75de22017-11-02 17:48:15833
834 if static_url:
Amin Hassani469f5702019-10-21 22:35:06835 cmd += ['--static_url', static_url]
David Rileyee75de22017-11-02 17:48:15836
Amin Hassani78520ae2019-10-29 20:26:51837 p = subprocess.Popen(cmd, preexec_fn=os.setsid)
xixuan2a0970a2016-08-10 19:12:44838 pid = os.getpgid(p.pid)
xixuan52c2fba2016-05-21 00:02:48839
840 # Pre-write status in the track_status_file before the first call of
841 # 'get_au_status' to make sure that the track_status_file exists.
xixuan2a0970a2016-08-10 19:12:44842 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
xixuan52c2fba2016-05-21 00:02:48843 progress_tracker.WriteStatus('CrOS update is just started.')
844
xixuan2a0970a2016-08-10 19:12:44845 return json.dumps((True, pid))
xixuan52c2fba2016-05-21 00:02:48846 else:
847 cros_update_trigger = cros_update.CrOSUpdateTrigger(
xixuanac89ce82016-12-01 00:48:20848 host_name, build_name, updater.static_dir, force_update=force_update,
David Rileyee75de22017-11-02 17:48:15849 full_update=full_update, original_build=original_build,
Amin Hassani78520ae2019-10-29 20:26:51850 payload_filename=payload_filename, quick_provision=quick_provision,
851 devserver_url=devserver_url, static_url=static_url)
xixuan52c2fba2016-05-21 00:02:48852 cros_update_trigger.TriggerAU()
xixuan27d50442017-08-09 17:38:25853 return json.dumps((True, -1))
xixuan52c2fba2016-05-21 00:02:48854
855 @cherrypy.expose
856 def get_au_status(self, **kwargs):
857 """Check if the auto-update task is finished.
858
859 It handles 4 cases:
860 1. If an error exists in the track_status_file, delete the track file and
861 raise it.
862 2. If cros-update process is finished, delete the file and return the
863 success result.
864 3. If the process is not running, delete the track file and raise an error
865 about 'the process is terminated due to unknown reason'.
866 4. If the track_status_file does not exist, kill the process if it exists,
867 and raise the IOError.
868
869 Args:
870 kwargs:
871 host_name: the hostname of the DUT to auto-update.
872 pid: the background process id of cros-update.
873
874 Returns:
xixuan28d99072016-10-06 19:24:16875 A dict with three elements:
xixuan52c2fba2016-05-21 00:02:48876 a boolean variable represents whether the auto-update process is
877 finished.
878 a string represents the current auto-update process status.
879 For example, 'Transfer Devserver/Stateful Update Package'.
xixuan28d99072016-10-06 19:24:16880 a detailed error message paragraph if there exists an Auto-Update
881 error, in which the last line shows the main exception. Empty
882 string otherwise.
xixuan52c2fba2016-05-21 00:02:48883 """
884 if 'host_name' not in kwargs:
Amin Hassanid4e35392019-10-03 18:02:44885 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 07:31:30886 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-21 00:02:48887
888 if 'pid' not in kwargs:
Amin Hassanid4e35392019-10-03 18:02:44889 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 07:31:30890 KEY_ERROR_MSG % 'pid')
xixuan52c2fba2016-05-21 00:02:48891
892 host_name = kwargs['host_name']
893 pid = kwargs['pid']
894 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
895
xixuan28d99072016-10-06 19:24:16896 result_dict = {'finished': False, 'status': '', 'detailed_error_msg': ''}
xixuan52c2fba2016-05-21 00:02:48897 try:
898 result = progress_tracker.ReadStatus()
899 if result.startswith(cros_update_progress.ERROR_TAG):
xixuan28d99072016-10-06 19:24:16900 result_dict['detailed_error_msg'] = result[len(
901 cros_update_progress.ERROR_TAG):]
xixuan28681fd2016-11-23 19:13:56902 elif result == cros_update_progress.FINISHED:
xixuan28d99072016-10-06 19:24:16903 result_dict['finished'] = True
904 result_dict['status'] = result
xixuan28681fd2016-11-23 19:13:56905 elif not cros_update_progress.IsProcessAlive(pid):
xixuan28d99072016-10-06 19:24:16906 result_dict['detailed_error_msg'] = (
907 'Cros_update process terminated midway due to unknown reason. '
908 'Last update status was %s' % result)
xixuan28681fd2016-11-23 19:13:56909 else:
910 result_dict['status'] = result
911 except IOError as e:
912 if pid and cros_update_progress.IsProcessAlive(pid):
xixuan2a0970a2016-08-10 19:12:44913 os.killpg(int(pid), signal.SIGKILL)
xixuan52c2fba2016-05-21 00:02:48914
xixuan28681fd2016-11-23 19:13:56915 result_dict['detailed_error_msg'] = str(e)
916
917 return json.dumps(result_dict)
xixuan52c2fba2016-05-21 00:02:48918
919 @cherrypy.expose
David Riley6d5fca02017-10-31 17:35:47920 def post_au_status(self, status, **kwargs):
921 """Updates the status of an auto-update task.
922
923 Callers will need to POST to this URL with a body of MIME-type
924 "multipart/form-data".
925 The body should include a single argument, 'status', containing the
926 AU status to record.
927
928 Args:
929 status: The updated status.
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 Hassanid4e35392019-10-03 18:02:44935 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 07:31:30936 KEY_ERROR_MSG % 'host_name')
David Riley6d5fca02017-10-31 17:35:47937
938 if 'pid' not in kwargs:
Amin Hassanid4e35392019-10-03 18:02:44939 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 07:31:30940 KEY_ERROR_MSG % 'pid')
David Riley6d5fca02017-10-31 17:35:47941
942 host_name = kwargs['host_name']
943 pid = kwargs['pid']
David Riley3cea2582017-11-25 06:03:01944 status = status.rstrip()
945 _Log('Recording status for %s (%s): %s' % (host_name, pid, status))
David Riley6d5fca02017-10-31 17:35:47946 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
947
David Riley3cea2582017-11-25 06:03:01948 progress_tracker.WriteStatus(status)
David Riley6d5fca02017-10-31 17:35:47949
950 return 'True'
951
952 @cherrypy.expose
xixuan52c2fba2016-05-21 00:02:48953 def handler_cleanup(self, **kwargs):
xixuan3bc974e2016-10-19 00:21:43954 """Clean track status log and temp directory for CrOS auto-update process.
xixuan52c2fba2016-05-21 00:02:48955
956 Args:
957 kwargs:
958 host_name: the hostname of the DUT to auto-update.
959 pid: the background process id of cros-update.
960 """
961 if 'host_name' not in kwargs:
Amin Hassanid4e35392019-10-03 18:02:44962 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 07:31:30963 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-21 00:02:48964
965 if 'pid' not in kwargs:
Amin Hassanid4e35392019-10-03 18:02:44966 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 07:31:30967 KEY_ERROR_MSG % 'pid')
xixuan52c2fba2016-05-21 00:02:48968
969 host_name = kwargs['host_name']
970 pid = kwargs['pid']
971 cros_update_progress.DelTrackStatusFile(host_name, pid)
xixuan3bc974e2016-10-19 00:21:43972 cros_update_progress.DelAUTempDirectory(host_name, pid)
xixuan52c2fba2016-05-21 00:02:48973
974 @cherrypy.expose
975 def kill_au_proc(self, **kwargs):
976 """Kill CrOS auto-update process using given process id.
977
978 Args:
979 kwargs:
980 host_name: Kill all the CrOS auto-update process of this host.
981
982 Returns:
983 True if all processes are killed properly.
984 """
985 if 'host_name' not in kwargs:
Amin Hassanid4e35392019-10-03 18:02:44986 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 07:31:30987 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-21 00:02:48988
xixuan447ad9d2017-02-28 22:46:20989 cur_pid = kwargs.get('pid')
990
xixuan52c2fba2016-05-21 00:02:48991 host_name = kwargs['host_name']
xixuan3bc974e2016-10-19 00:21:43992 track_log_list = cros_update_progress.GetAllTrackStatusFileByHostName(
993 host_name)
xixuan52c2fba2016-05-21 00:02:48994 for log in track_log_list:
995 # The track log's full path is: path/host_name_pid.log
996 # Use splitext to remove file extension, then parse pid from the
997 # filename.
Congbin Guo3afae6c2019-08-13 23:29:42998 pid = os.path.splitext(os.path.basename(log))[0][len(host_name) + 1:]
xixuan447ad9d2017-02-28 22:46:20999 _clear_process(host_name, pid)
xixuan52c2fba2016-05-21 00:02:481000
xixuan447ad9d2017-02-28 22:46:201001 if cur_pid:
1002 _clear_process(host_name, cur_pid)
xixuan52c2fba2016-05-21 00:02:481003
1004 return 'True'
1005
1006 @cherrypy.expose
1007 def collect_cros_au_log(self, **kwargs):
1008 """Collect CrOS auto-update log.
1009
1010 Args:
1011 kwargs:
1012 host_name: the hostname of the DUT to auto-update.
1013 pid: the background process id of cros-update.
1014
1015 Returns:
David Haddock9f459632017-05-11 21:45:461016 A dictionary containing the execute log file and any hostlog files.
xixuan52c2fba2016-05-21 00:02:481017 """
1018 if 'host_name' not in kwargs:
Amin Hassanid4e35392019-10-03 18:02:441019 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 07:31:301020 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-21 00:02:481021
1022 if 'pid' not in kwargs:
Amin Hassanid4e35392019-10-03 18:02:441023 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 07:31:301024 KEY_ERROR_MSG % 'pid')
xixuan52c2fba2016-05-21 00:02:481025
1026 host_name = kwargs['host_name']
1027 pid = kwargs['pid']
xixuan3bc974e2016-10-19 00:21:431028
1029 # Fetch the execute log recorded by cros_update_progress.
xixuan1bbfaba2016-10-14 00:53:221030 au_log = cros_update_progress.ReadExecuteLogFile(host_name, pid)
1031 cros_update_progress.DelExecuteLogFile(host_name, pid)
David Haddock9f459632017-05-11 21:45:461032 # Fetch the cros_au host_logs if they exist
1033 au_hostlogs = cros_update_progress.ReadAUHostLogFiles(host_name, pid)
1034 return json.dumps({'cros_au_log': au_log, 'host_logs': au_hostlogs})
xixuan1bbfaba2016-10-14 00:53:221035
xixuan52c2fba2016-05-21 00:02:481036 @cherrypy.expose
Dan Shi2f136862016-02-11 23:38:381037 def locate_file(self, **kwargs):
1038 """Get the path to the given file name.
1039
1040 This method looks up the given file name inside specified build artifacts.
1041 One use case is to help caller to locate an apk file inside a build
1042 artifact. The location of the apk file could be different based on the
1043 branch and target.
1044
1045 Args:
1046 file_name: Name of the file to look for.
1047 artifacts: A list of artifact names to search for the file.
1048
1049 Returns:
1050 Path to the file with the given name. It's relative to the folder for the
1051 build, e.g., DATA/priv-app/sl4a/sl4a.apk
Dan Shi2f136862016-02-11 23:38:381052 """
1053 dl, _ = _get_downloader_and_factory(kwargs)
1054 try:
Joe Brennan1691f8e2017-03-15 22:53:361055 file_name = kwargs['file_name']
Dan Shi2f136862016-02-11 23:38:381056 artifacts = kwargs['artifacts']
1057 except KeyError:
Amin Hassanid4e35392019-10-03 18:02:441058 raise DevServerError(
Congbin Guo4132a272019-08-20 19:32:141059 '`file_name` and `artifacts` are required to search '
1060 'for a file in build artifacts.')
Dan Shi2f136862016-02-11 23:38:381061 build_path = dl.GetBuildDir()
1062 for artifact in artifacts:
1063 # Get the unzipped folder of the artifact. If it's not defined in
1064 # ARTIFACT_UNZIP_FOLDER_MAP, assume the files are unzipped to the build
1065 # directory directly.
1066 folder = artifact_info.ARTIFACT_UNZIP_FOLDER_MAP.get(artifact, '')
1067 artifact_path = os.path.join(build_path, folder)
1068 for root, _, filenames in os.walk(artifact_path):
Joe Brennan1691f8e2017-03-15 22:53:361069 if file_name in set([f for f in filenames]):
Dan Shi2f136862016-02-11 23:38:381070 return os.path.relpath(os.path.join(root, file_name), build_path)
Amin Hassanid4e35392019-10-03 18:02:441071 raise DevServerError(
Congbin Guo4132a272019-08-20 19:32:141072 'File `%s` can not be found in artifacts: %s' % (file_name, artifacts))
Dan Shi2f136862016-02-11 23:38:381073
1074 @cherrypy.expose
Simran Basi4baad082013-02-14 21:39:181075 def setup_telemetry(self, **kwargs):
1076 """Extracts and sets up telemetry
1077
1078 This method goes through the telemetry deps packages, and stages them on
1079 the devserver to be used by the drones and the telemetry tests.
1080
1081 Args:
1082 archive_url: Google Storage URL for the build.
1083
1084 Returns:
1085 Path to the source folder for the telemetry codebase once it is staged.
1086 """
Gabe Black3b567202015-09-23 21:07:591087 dl = _get_downloader(kwargs)
Simran Basi4baad082013-02-14 21:39:181088
Gabe Black3b567202015-09-23 21:07:591089 build_path = dl.GetBuildDir()
Simran Basi4baad082013-02-14 21:39:181090 deps_path = os.path.join(build_path, 'autotest/packages')
1091 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
1092 src_folder = os.path.join(telemetry_path, 'src')
1093
1094 with self._telemetry_lock_dict.lock(telemetry_path):
1095 if os.path.exists(src_folder):
1096 # Telemetry is already fully stage return
1097 return src_folder
1098
1099 common_util.MkDirP(telemetry_path)
1100
1101 # Copy over the required deps tar balls to the telemetry directory.
1102 for dep in TELEMETRY_DEPS:
1103 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 23:40:041104 if not os.path.exists(dep_path):
1105 # This dep does not exist (could be new), do not extract it.
1106 continue
Simran Basi4baad082013-02-14 21:39:181107 try:
1108 common_util.ExtractTarball(dep_path, telemetry_path)
1109 except common_util.CommonUtilError as e:
1110 shutil.rmtree(telemetry_path)
Amin Hassanid4e35392019-10-03 18:02:441111 raise DevServerError(str(e))
Simran Basi4baad082013-02-14 21:39:181112
1113 # By default all the tarballs extract to test_src but some parts of
1114 # the telemetry code specifically hardcoded to exist inside of 'src'.
1115 test_src = os.path.join(telemetry_path, 'test_src')
1116 try:
1117 shutil.move(test_src, src_folder)
1118 except shutil.Error:
1119 # This can occur if src_folder already exists. Remove and retry move.
1120 shutil.rmtree(src_folder)
Amin Hassanid4e35392019-10-03 18:02:441121 raise DevServerError(
Gabe Black3b567202015-09-23 21:07:591122 'Failure in telemetry setup for build %s. Appears that the '
1123 'test_src to src move failed.' % dl.GetBuild())
Simran Basi4baad082013-02-14 21:39:181124
1125 return src_folder
1126
1127 @cherrypy.expose
Chris Sosa76e44b92013-01-31 20:11:381128 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 19:22:361129 """Symbolicates a minidump using pre-downloaded symbols, returns it.
1130
1131 Callers will need to POST to this URL with a body of MIME-type
1132 "multipart/form-data".
1133 The body should include a single argument, 'minidump', containing the
1134 binary-formatted minidump to symbolicate.
1135
Chris Masone816e38c2012-05-02 19:22:361136 Args:
Chris Sosa76e44b92013-01-31 20:11:381137 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 19:22:361138 minidump: The binary minidump file to symbolicate.
1139 """
Chris Sosa76e44b92013-01-31 20:11:381140 # Ensure the symbols have been staged.
Dan Shif08fe492016-10-04 21:39:251141 # Try debug.tar.xz first, then debug.tgz
1142 for artifact in (artifact_info.SYMBOLS_ONLY, artifact_info.SYMBOLS):
1143 kwargs['artifacts'] = artifact
1144 dl = _get_downloader(kwargs)
1145
1146 try:
1147 if self.stage(**kwargs) == 'Success':
1148 break
1149 except build_artifact.ArtifactDownloadError:
1150 continue
1151 else:
Amin Hassanid4e35392019-10-03 18:02:441152 raise DevServerError(
Congbin Guo4132a272019-08-20 19:32:141153 'Failed to stage symbols for %s' % dl.DescribeSource())
Chris Sosa76e44b92013-01-31 20:11:381154
Chris Masone816e38c2012-05-02 19:22:361155 to_return = ''
1156 with tempfile.NamedTemporaryFile() as local:
1157 while True:
1158 data = minidump.file.read(8192)
1159 if not data:
1160 break
1161 local.write(data)
Chris Sosa76e44b92013-01-31 20:11:381162
Chris Masone816e38c2012-05-02 19:22:361163 local.flush()
Chris Sosa76e44b92013-01-31 20:11:381164
Gabe Black3b567202015-09-23 21:07:591165 symbols_directory = os.path.join(dl.GetBuildDir(), 'debug', 'breakpad')
Chris Sosa76e44b92013-01-31 20:11:381166
xixuanab744382017-04-27 17:41:271167 # The location of minidump_stackwalk is defined in chromeos-admin.
Chris Sosa76e44b92013-01-31 20:11:381168 stackwalk = subprocess.Popen(
xixuanab744382017-04-27 17:41:271169 ['/usr/local/bin/minidump_stackwalk', local.name, symbols_directory],
Chris Sosa76e44b92013-01-31 20:11:381170 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1171
Chris Masone816e38c2012-05-02 19:22:361172 to_return, error_text = stackwalk.communicate()
1173 if stackwalk.returncode != 0:
Amin Hassanid4e35392019-10-03 18:02:441174 raise DevServerError(
Congbin Guo4132a272019-08-20 19:32:141175 "Can't generate stack trace: %s (rc=%d)" % (error_text,
1176 stackwalk.returncode))
Chris Masone816e38c2012-05-02 19:22:361177
1178 return to_return
1179
1180 @cherrypy.expose
Don Garrettf84631a2014-01-08 02:21:261181 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 19:31:361182 """Return a string representing the latest build for a given target.
1183
1184 Args:
1185 target: The build target, typically a combination of the board and the
1186 type of build e.g. x86-mario-release.
1187 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
1188 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-08 02:21:261189
Scott Zawalski16954532012-03-20 19:31:361190 Returns:
1191 A string representation of the latest build if one exists, i.e.
1192 R19-1993.0.0-a1-b1480.
1193 An empty string if no latest could be found.
1194 """
Don Garrettf84631a2014-01-08 02:21:261195 if not kwargs:
Scott Zawalski16954532012-03-20 19:31:361196 return _PrintDocStringAsHTML(self.latestbuild)
1197
Don Garrettf84631a2014-01-08 02:21:261198 if 'target' not in kwargs:
Amin Hassanid4e35392019-10-03 18:02:441199 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 07:31:301200 'Error: target= is required!')
Dan Shi61305df2015-10-26 23:52:351201
1202 if _is_android_build_request(kwargs):
1203 branch = kwargs.get('branch', None)
1204 target = kwargs.get('target', None)
1205 if not target or not branch:
Amin Hassanid4e35392019-10-03 18:02:441206 raise DevServerError('Both target and branch must be specified to query'
1207 ' for the latest Android build.')
Dan Shi61305df2015-10-26 23:52:351208 return android_build.BuildAccessor.GetLatestBuildID(target, branch)
1209
Scott Zawalski16954532012-03-20 19:31:361210 try:
Gilad Arnoldc65330c2012-09-20 22:17:481211 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-08 02:21:261212 updater.static_dir, kwargs['target'],
1213 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 17:05:011214 except common_util.CommonUtilError as errmsg:
Amin Hassanid4e35392019-10-03 18:02:441215 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 07:31:301216 str(errmsg))
Scott Zawalski16954532012-03-20 19:31:361217
1218 @cherrypy.expose
xixuan7efd0002016-04-14 22:34:011219 def list_suite_controls(self, **kwargs):
1220 """Return a list of contents of all known control files.
1221
1222 Example URL:
1223 To List all control files' content:
1224 http://dev-server/list_suite_controls?suite_name=bvt&
1225 build=daisy_spring-release/R29-4279.0.0
1226
1227 Args:
1228 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
1229 suite_name: List the control files belonging to that suite.
1230
1231 Returns:
Dan Shia1cd6522016-04-18 23:07:211232 A dictionary of all control files's path to its content for given suite.
xixuan7efd0002016-04-14 22:34:011233 """
1234 if not kwargs:
1235 return _PrintDocStringAsHTML(self.controlfiles)
1236
1237 if 'build' not in kwargs:
Amin Hassanid4e35392019-10-03 18:02:441238 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 07:31:301239 'Error: build= is required!')
xixuan7efd0002016-04-14 22:34:011240
1241 if 'suite_name' not in kwargs:
Amin Hassanid4e35392019-10-03 18:02:441242 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Dan Shia1cd6522016-04-18 23:07:211243 'Error: suite_name= is required!')
xixuan7efd0002016-04-14 22:34:011244
1245 control_file_list = [
1246 line.rstrip() for line in common_util.GetControlFileListForSuite(
1247 updater.static_dir, kwargs['build'],
1248 kwargs['suite_name']).splitlines()]
1249
Dan Shia1cd6522016-04-18 23:07:211250 control_file_content_dict = {}
xixuan7efd0002016-04-14 22:34:011251 for control_path in control_file_list:
Dan Shia1cd6522016-04-18 23:07:211252 control_file_content_dict[control_path] = (common_util.GetControlFile(
xixuan7efd0002016-04-14 22:34:011253 updater.static_dir, kwargs['build'], control_path))
1254
Dan Shia1cd6522016-04-18 23:07:211255 return json.dumps(control_file_content_dict)
xixuan7efd0002016-04-14 22:34:011256
1257 @cherrypy.expose
Don Garrettf84631a2014-01-08 02:21:261258 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 22:17:281259 """Return a control file or a list of all known control files.
1260
1261 Example URL:
1262 To List all control files:
beepsbd337242013-07-10 05:44:061263 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
1264 To List all control files for, say, the bvt suite:
1265 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 22:17:281266 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 20:12:421267 http://dev-server/controlfiles?board=x86-alex-release&build=R18-1514.0.0&control_path=client/sleeptest/control
Scott Zawalski4647ce62012-01-03 22:17:281268
1269 Args:
Scott Zawalski84a39c92012-01-13 20:12:421270 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 22:17:281271 control_path: If you want the contents of a control file set this
1272 to the path. E.g. client/site_tests/sleeptest/control
1273 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-10 05:44:061274 suite_name: If control_path is not specified but a suite_name is
1275 specified, list the control files belonging to that suite instead of
1276 all control files. The empty string for suite_name will list all control
1277 files for the build.
Don Garrettf84631a2014-01-08 02:21:261278
Scott Zawalski4647ce62012-01-03 22:17:281279 Returns:
1280 Contents of a control file if control_path is provided.
1281 A list of control files if no control_path is provided.
1282 """
Don Garrettf84631a2014-01-08 02:21:261283 if not kwargs:
Scott Zawalski4647ce62012-01-03 22:17:281284 return _PrintDocStringAsHTML(self.controlfiles)
1285
Don Garrettf84631a2014-01-08 02:21:261286 if 'build' not in kwargs:
Amin Hassanid4e35392019-10-03 18:02:441287 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 07:31:301288 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 22:17:281289
Don Garrettf84631a2014-01-08 02:21:261290 if 'control_path' not in kwargs:
1291 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-10 05:44:061292 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-08 02:21:261293 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-10 05:44:061294 else:
1295 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-08 02:21:261296 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 22:17:281297 else:
Gilad Arnoldc65330c2012-09-20 22:17:481298 return common_util.GetControlFile(
Don Garrettf84631a2014-01-08 02:21:261299 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-13 02:39:181300
1301 @cherrypy.expose
Simran Basi99e63c02014-05-20 17:39:521302 def xbuddy_translate(self, *args, **kwargs):
Yu-Ju Hong1bdb7a92014-04-10 23:02:111303 """Translates an xBuddy path to a real path to artifact if it exists.
1304
1305 Args:
Simran Basi99e63c02014-05-20 17:39:521306 args: An xbuddy path in the form of {local|remote}/build_id/artifact.
1307 Local searches the devserver's static directory. Remote searches a
1308 Google Storage image archive.
1309
1310 Kwargs:
1311 image_dir: Google Storage image archive to search in if requesting a
1312 remote artifact. If none uses the default bucket.
Yu-Ju Hong1bdb7a92014-04-10 23:02:111313
1314 Returns:
Simran Basi99e63c02014-05-20 17:39:521315 String in the format of build_id/artifact as stored on the local server
1316 or in Google Storage.
Yu-Ju Hong1bdb7a92014-04-10 23:02:111317 """
Simran Basi99e63c02014-05-20 17:39:521318 build_id, filename = self._xbuddy.Translate(
Gabe Black3b567202015-09-23 21:07:591319 args, image_dir=kwargs.get('image_dir'))
Yu-Ju Hong1bdb7a92014-04-10 23:02:111320 response = os.path.join(build_id, filename)
1321 _Log('Path translation requested, returning: %s', response)
1322 return response
1323
1324 @cherrypy.expose
joycheneaf4cfc2013-07-02 15:38:571325 def xbuddy(self, *args, **kwargs):
1326 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 19:13:131327
1328 Args:
joycheneaf4cfc2013-07-02 15:38:571329 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 21:30:301330 components of the path. The path can be understood as
1331 "{local|remote}/build_id/artifact" where build_id is composed of
1332 "board/version."
joycheneaf4cfc2013-07-02 15:38:571333
joychen121fc9b2013-08-02 21:30:301334 The first path element is optional, and can be "remote" or "local"
1335 If local (the default), devserver will not attempt to access Google
1336 Storage, and will only search the static directory for the files.
1337 If remote, devserver will try to obtain the artifact off GS if it's
1338 not found locally.
1339 The board is the familiar board name, optionally suffixed.
1340 The version can be the google storage version number, and may also be
1341 any of a number of xBuddy defined version aliases that will be
1342 translated into the latest built image that fits the description.
1343 Defaults to latest.
1344 The artifact is one of a number of image or artifact aliases used by
1345 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 15:38:571346
1347 Kwargs:
Yu-Ju Hong51495eb2013-12-13 01:08:431348 for_update: {true|false}
Amin Hassanie9ffb862019-09-26 00:10:401349 if true, prepares the update payloads for the image,
Yu-Ju Hong51495eb2013-12-13 01:08:431350 and returns the update uri to pass to the
1351 update_engine_client.
joychen3cb228e2013-06-12 19:13:131352 return_dir: {true|false}
1353 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-13 01:08:431354 relative_path: {true|false}
1355 if set to true, returns the relative path to the payload
1356 directory from static_dir.
joychen3cb228e2013-06-12 19:13:131357 Example URL:
joycheneaf4cfc2013-07-02 15:38:571358 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 19:13:131359 or
joycheneaf4cfc2013-07-02 15:38:571360 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 19:13:131361
1362 Returns:
Yu-Ju Hong51495eb2013-12-13 01:08:431363 If |for_update|, returns a redirect to the image or update file
1364 on the devserver. E.g.,
1365 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
1366 chromium-test-image.bin
1367 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
1368 http://host:port/static/x86-generic-release/R26-4000.0.0/
1369 If |relative_path| is true, return a relative path the folder where the
1370 payloads are. E.g.,
1371 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 19:13:131372 """
Chris Sosa75490802013-10-01 00:21:451373 boolean_string = kwargs.get('for_update')
1374 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-13 01:08:431375 boolean_string = kwargs.get('return_dir')
1376 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
1377 boolean_string = kwargs.get('relative_path')
1378 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 21:30:301379
Yu-Ju Hong51495eb2013-12-13 01:08:431380 if return_dir and relative_path:
Chris Sosa4b951602014-04-10 03:26:071381 raise common_util.DevServerHTTPError(
Amin Hassanid4e35392019-10-03 18:02:441382 http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 07:31:301383 'Cannot specify both return_dir and relative_path')
Chris Sosa75490802013-10-01 00:21:451384
1385 # For updates, we optimize downloading of test images.
1386 file_name = None
1387 build_id = None
1388 if for_update:
1389 try:
Yu-Ju Hong1bdb7a92014-04-10 23:02:111390 build_id = self._xbuddy.StageTestArtifactsForUpdate(args)
Chris Sosa75490802013-10-01 00:21:451391 except build_artifact.ArtifactDownloadError:
1392 build_id = None
1393
1394 if not build_id:
1395 build_id, file_name = self._xbuddy.Get(args)
1396
Yu-Ju Hong51495eb2013-12-13 01:08:431397 if for_update:
Amin Hassanie9ffb862019-09-26 00:10:401398 _Log('Payloads requested.')
Yu-Ju Hong51495eb2013-12-13 01:08:431399 # Forces payload to be in cache and symlinked into build_id dir.
Amin Hassanie9ffb862019-09-26 00:10:401400 updater.GetUpdateForLabel(build_id)
Yu-Ju Hong51495eb2013-12-13 01:08:431401
1402 response = None
1403 if return_dir:
1404 response = os.path.join(cherrypy.request.base, 'static', build_id)
1405 _Log('Directory requested, returning: %s', response)
1406 elif relative_path:
1407 response = build_id
1408 _Log('Relative path requested, returning: %s', response)
1409 elif for_update:
1410 response = os.path.join(cherrypy.request.base, 'update', build_id)
1411 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 19:13:131412 else:
Yu-Ju Hong51495eb2013-12-13 01:08:431413 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 21:30:301414 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-13 01:08:431415 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 21:30:301416 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 19:13:131417
Yu-Ju Hong51495eb2013-12-13 01:08:431418 return response
1419
joychen3cb228e2013-06-12 19:13:131420 @cherrypy.expose
1421 def xbuddy_list(self):
1422 """Lists the currently available images & time since last access.
1423
Gilad Arnold452fd272014-02-04 19:09:281424 Returns:
1425 A string representation of a list of tuples [(build_id, time since last
1426 access),...]
joychen3cb228e2013-06-12 19:13:131427 """
1428 return self._xbuddy.List()
1429
1430 @cherrypy.expose
1431 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 19:09:281432 """Returns the number of images cached by xBuddy."""
joychen3cb228e2013-06-12 19:13:131433 return self._xbuddy.Capacity()
1434
1435 @cherrypy.expose
Chris Sosa7c931362010-10-12 02:49:011436 def index(self):
Gilad Arnoldf8f769f2012-09-24 15:43:011437 """Presents a welcome message and documentation links."""
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 """
Gilad Arnoldd5ebaaa2012-10-02 18:52:381462 name = '/'.join(args)
Congbin Guo6bc32182019-08-21 00:54:301463 method = _GetExposedMethod(name)
Gilad Arnoldf8f769f2012-09-24 15:43:011464 if not method:
Amin Hassanid4e35392019-10-03 18:02:441465 raise DevServerError("No exposed method named `%s'" % name)
Gilad Arnoldf8f769f2012-09-24 15:43:011466 if not method.__doc__:
Amin Hassanid4e35392019-10-03 18:02:441467 raise DevServerError("No documentation for exposed method `%s'" % name)
Gilad Arnoldf8f769f2012-09-24 15:43:011468 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-12 02:49:011469
Dale Curtisc9aaf3a2011-08-09 22:47:401470 @cherrypy.expose
Chris Sosa7c931362010-10-12 02:49:011471 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 15:43:011472 """Handles an update check from a Chrome OS client.
1473
1474 The HTTP request should contain the standard Omaha-style XML blob. The URL
1475 line may contain an additional intermediate path to the update payload.
1476
joychen121fc9b2013-08-02 21:30:301477 This request can be handled in one of 4 ways, depending on the devsever
1478 settings and intermediate path.
joychenb0dfe552013-07-30 17:02:061479
Amin Hassanie9ffb862019-09-26 00:10:401480 1. No intermediate path. DEPRECATED
joychen121fc9b2013-08-02 21:30:301481
1482 2. Path explicitly invokes XBuddy
1483 If there is a path given, it can explicitly invoke xbuddy by prefixing it
1484 with 'xbuddy'. This path is then used to acquire an image binary for the
1485 devserver to generate an update payload from. Devserver then serves this
1486 payload.
1487
1488 3. Path is left for the devserver to interpret.
1489 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
1490 to generate a payload from the test image in that directory and serve it.
1491
joychen121fc9b2013-08-02 21:30:301492 Examples:
joychen121fc9b2013-08-02 21:30:301493 2. Explicitly invoke xbuddy
1494 update_engine_client --omaha_url=
1495 http://myhost/update/xbuddy/remote/board/version/dev
1496 This would go to GS to download the dev image for the board, from which
1497 the devserver would generate a payload to serve.
1498
1499 3. Give a path for devserver to interpret
1500 update_engine_client --omaha_url=http://myhost/update/some/random/path
1501 This would attempt, in order to:
1502 a) Generate an update from a test image binary if found in
1503 static_dir/some/random/path.
1504 b) Serve an update payload found in static_dir/some/random/path.
1505 c) Hope that some/random/path takes the form "board/version" and
1506 and attempt to download an update payload for that board/version
1507 from GS.
Gilad Arnoldf8f769f2012-09-24 15:43:011508 """
joychen121fc9b2013-08-02 21:30:301509 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 21:47:021510 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-12 02:49:011511 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-12 02:49:011512
joychen121fc9b2013-08-02 21:30:301513 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 22:46:221514
Dan Shif5ce2de2013-04-25 23:06:321515
Chris Sosadbc20082012-12-10 21:39:111516def _CleanCache(cache_dir, wipe):
1517 """Wipes any excess cached items in the cache_dir.
1518
1519 Args:
1520 cache_dir: the directory we are wiping from.
1521 wipe: If True, wipe all the contents -- not just the excess.
1522 """
1523 if wipe:
1524 # Clear the cache and exit on error.
1525 cmd = 'rm -rf %s/*' % cache_dir
1526 if os.system(cmd) != 0:
1527 _Log('Failed to clear the cache with %s' % cmd)
1528 sys.exit(1)
1529 else:
1530 # Clear all but the last N cached updates
1531 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
1532 (cache_dir, CACHED_ENTRIES))
1533 if os.system(cmd) != 0:
1534 _Log('Failed to clean up old delta cache files with %s' % cmd)
1535 sys.exit(1)
1536
1537
Chris Sosa3ae4dc12013-03-29 18:47:001538def _AddTestingOptions(parser):
1539 group = optparse.OptionGroup(
1540 parser, 'Advanced Testing Options', 'These are used by test scripts and '
1541 'developers writing integration tests utilizing the devserver. They are '
1542 'not intended to be really used outside the scope of someone '
1543 'knowledgable about the test.')
1544 group.add_option('--exit',
1545 action='store_true',
Amin Hassanie9ffb862019-09-26 00:10:401546 help='do not start the server (yet clear cache)')
Chris Sosa3ae4dc12013-03-29 18:47:001547 group.add_option('--host_log',
1548 action='store_true', default=False,
1549 help='record history of host update events (/api/hostlog)')
1550 group.add_option('--max_updates',
Gabe Black3b567202015-09-23 21:07:591551 metavar='NUM', default=-1, type='int',
Chris Sosa3ae4dc12013-03-29 18:47:001552 help='maximum number of update checks handled positively '
1553 '(default: unlimited)')
Chris Sosa3ae4dc12013-03-29 18:47:001554 group.add_option('--proxy_port',
1555 metavar='PORT', default=None, type='int',
1556 help='port to have the client connect to -- basically the '
1557 'devserver lies to the update to tell it to get the payload '
1558 'from a different port that will proxy the request back to '
1559 'the devserver. The proxy must be managed outside the '
1560 'devserver.')
Chris Sosa3ae4dc12013-03-29 18:47:001561 parser.add_option_group(group)
1562
1563
1564def _AddUpdateOptions(parser):
1565 group = optparse.OptionGroup(
1566 parser, 'Autoupdate Options', 'These options can be used to change '
Amin Hassanie9ffb862019-09-26 00:10:401567 'how the devserver serve update payloads. Please '
Chris Sosa3ae4dc12013-03-29 18:47:001568 'note that all of these option affect how a payload is generated and so '
1569 'do not work in archive-only mode.')
Chris Sosa3ae4dc12013-03-29 18:47:001570 group.add_option('--critical_update',
1571 action='store_true', default=False,
1572 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 18:47:001573 group.add_option('--payload',
1574 metavar='PATH',
1575 help='use the update payload from specified directory '
1576 '(update.gz).')
Chris Sosa3ae4dc12013-03-29 18:47:001577 parser.add_option_group(group)
1578
1579
1580def _AddProductionOptions(parser):
1581 group = optparse.OptionGroup(
1582 parser, 'Advanced Server Options', 'These options can be used to changed '
1583 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 18:47:001584 group.add_option('--clear_cache',
1585 action='store_true', default=False,
1586 help='At startup, removes all cached entries from the'
Amin Hassanid4e35392019-10-03 18:02:441587 "devserver's cache.")
Chris Sosa3ae4dc12013-03-29 18:47:001588 group.add_option('--logfile',
1589 metavar='PATH',
1590 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 20:24:551591 group.add_option('--pidfile',
1592 metavar='PATH',
1593 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 19:04:131594 group.add_option('--portfile',
1595 metavar='PATH',
1596 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 18:47:001597 group.add_option('--production',
1598 action='store_true', default=False,
1599 help='have the devserver use production values when '
1600 'starting up. This includes using more threads and '
1601 'performing less logging.')
1602 parser.add_option_group(group)
1603
1604
Paul Hobbsef4e0702016-06-28 00:01:421605def MakeLogHandler(logfile):
J. Richard Barnette3d977b82013-04-23 18:05:191606 """Create a LogHandler instance used to log all messages."""
1607 hdlr_cls = handlers.TimedRotatingFileHandler
1608 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
xixuan3d48bff2017-01-31 03:00:091609 interval=_LOG_ROTATION_INTERVAL,
J. Richard Barnette3d977b82013-04-23 18:05:191610 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 20:24:551611 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 18:05:191612 return hdlr
1613
1614
Chris Sosacde6bf42012-06-01 01:36:391615def main():
Chris Sosa3ae4dc12013-03-29 18:47:001616 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 21:47:021617 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 23:39:341618
1619 # get directory that the devserver is run from
1620 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 16:17:231621 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 23:39:341622 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 17:09:421623 metavar='PATH',
joychen84d13772013-08-06 16:17:231624 default=default_static_dir,
joychened64b222013-06-21 23:39:341625 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 17:09:421626 parser.add_option('--port',
1627 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 21:13:281628 help=('port for the dev server to use; if zero, binds to '
1629 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 17:09:421630 parser.add_option('-t', '--test_image',
1631 action='store_true',
joychen121fc9b2013-08-02 21:30:301632 help='Deprecated.')
joychen5260b9a2013-07-16 21:48:011633 parser.add_option('-x', '--xbuddy_manage_builds',
1634 action='store_true',
1635 default=False,
1636 help='If set, allow xbuddy to manage images in'
1637 'build/images.')
Dan Shi72b16132015-10-08 19:10:331638 parser.add_option('-a', '--android_build_credential',
1639 default=None,
1640 help='Path to a json file which contains the credential '
1641 'needed to access Android builds.')
Chris Sosa3ae4dc12013-03-29 18:47:001642 _AddProductionOptions(parser)
1643 _AddUpdateOptions(parser)
1644 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-12 02:49:011645 (options, _) = parser.parse_args()
[email protected]21a5ca32009-11-04 18:23:231646
J. Richard Barnette3d977b82013-04-23 18:05:191647 # Handle options that must be set globally in cherrypy. Do this
1648 # work up front, because calls to _Log() below depend on this
1649 # initialization.
1650 if options.production:
1651 cherrypy.config.update({'environment': 'production'})
1652 if not options.logfile:
1653 cherrypy.config.update({'log.screen': True})
1654 else:
1655 cherrypy.config.update({'log.error_file': '',
1656 'log.access_file': ''})
Paul Hobbsef4e0702016-06-28 00:01:421657 hdlr = MakeLogHandler(options.logfile)
J. Richard Barnette3d977b82013-04-23 18:05:191658 # Pylint can't seem to process these two calls properly
1659 # pylint: disable=E1101
1660 cherrypy.log.access_log.addHandler(hdlr)
1661 cherrypy.log.error_log.addHandler(hdlr)
1662 # pylint: enable=E1101
1663
joychened64b222013-06-21 23:39:341664 # set static_dir, from which everything will be served
joychen84d13772013-08-06 16:17:231665 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 22:46:221666
joychened64b222013-06-21 23:39:341667 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 18:05:191668 # If our devserver is only supposed to serve payloads, we shouldn't be
1669 # mucking with the cache at all. If the devserver hadn't previously
1670 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 18:14:071671 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 21:39:111672 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 20:12:171673 else:
1674 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-17 01:36:141675
Chris Sosadbc20082012-12-10 21:39:111676 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 23:39:341677 _Log('Serving from %s' % options.static_dir)
[email protected]21a5ca32009-11-04 18:23:231678
Amin Hassanie9ffb862019-09-26 00:10:401679 _xbuddy = xbuddy.XBuddy(manage_builds=options.xbuddy_manage_builds,
joychen121fc9b2013-08-02 21:30:301680 static_dir=options.static_dir)
Chris Sosa75490802013-10-01 00:21:451681 if options.clear_cache and options.xbuddy_manage_builds:
1682 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 21:30:301683
Chris Sosa6a3697f2013-01-30 00:44:431684 # We allow global use here to share with cherrypy classes.
1685 # pylint: disable=W0603
Chris Sosacde6bf42012-06-01 01:36:391686 global updater
Andrew de los Reyes52620802010-04-12 20:40:071687 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 21:30:301688 _xbuddy,
joychened64b222013-06-21 23:39:341689 static_dir=options.static_dir,
Gilad Arnold0c9c8602012-10-03 06:58:581690 payload_path=options.payload,
Don Garrett0ad09372010-12-07 00:20:301691 proxy_port=options.proxy_port,
Satoru Takabayashid733cbe2011-11-15 17:36:321692 critical_update=options.critical_update,
Gilad Arnolda564b4b2012-10-04 17:32:441693 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 19:52:231694 host_log=options.host_log,
Chris Sosa0f1ec842011-02-15 00:33:221695 )
Chris Sosa7c931362010-10-12 02:49:011696
J. Richard Barnette3d977b82013-04-23 18:05:191697 if options.exit:
1698 return
Chris Sosa2f1c41e2012-07-10 21:32:331699
joychen3cb228e2013-06-12 19:13:131700 dev_server = DevServerRoot(_xbuddy)
Congbin Guo3afae6c2019-08-13 23:29:421701 health_checker_app = health_checker.Root(dev_server, options.static_dir)
joychen3cb228e2013-06-12 19:13:131702
Chris Sosa855b8932013-08-21 20:24:551703 if options.pidfile:
1704 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1705
Gilad Arnold11fbef42014-02-10 19:04:131706 if options.portfile:
1707 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1708
Dan Shiafd5c6c2016-01-07 18:27:031709 if (options.android_build_credential and
1710 os.path.exists(options.android_build_credential)):
1711 try:
1712 with open(options.android_build_credential) as f:
1713 android_build.BuildAccessor.credential_info = json.load(f)
1714 except ValueError as e:
1715 _Log('Failed to load the android build credential: %s. Error: %s.' %
1716 (options.android_build_credential, e))
Congbin Guo3afae6c2019-08-13 23:29:421717
1718 cherrypy.tree.mount(health_checker_app, '/check_health',
1719 config=health_checker.get_config())
joychen3cb228e2013-06-12 19:13:131720 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-06-01 01:36:391721
1722
1723if __name__ == '__main__':
1724 main()