blob: f5aaac839ddd69eb0195b087d0be0490dc1c1c85 [file] [log] [blame]
David Riley2fcb0122017-11-02 11:25:39 -07001#!/usr/bin/env python2
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -07002# -*- coding: utf-8 -*-
Chris Sosa781ba6d2012-04-11 12:44:43 -07003# Copyright (c) 2009-2012 The Chromium OS Authors. All rights reserved.
rtc@google.comded22402009-10-26 22:36:21 +00004# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
Chris Sosa3ae4dc12013-03-29 11:47:00 -07007"""Chromium OS development server that can be used for all forms of update.
8
9This devserver can be used to perform system-wide autoupdate and update
10of specific portage packages on devices running Chromium OS derived operating
Amin Hassanie9ffb862019-09-25 17:10:40 -070011systems.
Chris Sosa3ae4dc12013-03-29 11:47:00 -070012
Amin Hassanie9ffb862019-09-25 17:10:40 -070013The devserver is configured to stage and
Chris Sosa3ae4dc12013-03-29 11:47:00 -070014serve 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 11:47:00 -070021For 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 14:07:59 -070025from __future__ import print_function
Chris Sosa7c931362010-10-11 19:49:01 -070026
Gilad Arnold55a2a372012-10-02 09:46:32 -070027import json
David Riley2fcb0122017-11-02 11:25:39 -070028import optparse # pylint: disable=deprecated-module
rtc@google.comded22402009-10-26 22:36:21 +000029import os
Scott Zawalski4647ce62012-01-03 17:17:28 -050030import re
Simran Basi4baad082013-02-14 13:39:18 -080031import shutil
xixuan52c2fba2016-05-20 17:02:48 -070032import signal
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -080033import socket
Chris Masone816e38c2012-05-02 12:22:36 -070034import subprocess
J. Richard Barnette3d977b82013-04-23 11:05:19 -070035import sys
Chris Masone816e38c2012-05-02 12:22:36 -070036import tempfile
Dan Shi59ae7092013-06-04 14:37:27 -070037import threading
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -070038import types
J. Richard Barnette3d977b82013-04-23 11:05:19 -070039from logging import handlers
40
Amin Hassanid4e35392019-10-03 11:02:44 -070041from six.moves import http_client
42
43# pylint: disable=no-name-in-module, import-error
J. Richard Barnette3d977b82013-04-23 11:05:19 -070044import cherrypy
Chris Sosa855b8932013-08-21 13:24:55 -070045from cherrypy import _cplogging as cplogging
Amin Hassanid4e35392019-10-03 11:02:44 -070046from cherrypy.process import plugins
47# pylint: enable=no-name-in-module, import-error
rtc@google.comded22402009-10-26 22:36:21 +000048
Achuith Bhandarkar662fb722019-10-31 16:12:49 -070049import autoupdate
50import cherrypy_ext
Achuith Bhandarkar662fb722019-10-31 16:12:49 -070051import health_checker
52
Richard Barnettedf35c322017-08-18 17:02:13 -070053# 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 16:29:42 -070056import setup_chromite # pylint: disable=unused-import
Amin Hassanie427e212019-10-28 11:04:27 -070057from chromite.lib import cros_update_progress
Achuith Bhandarkar662fb722019-10-31 16:12:49 -070058from 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 15:17:48 -070066
Gilad Arnoldc65330c2012-09-20 15:17:48 -070067# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080068def _Log(message, *args):
Achuith Bhandarkar662fb722019-10-31 16:12:49 -070069 return cherrypy_log_util.LogWithTag('DEVSERVER', message, *args)
Frank Farzan40160872011-12-12 18:39:18 -080070
Chris Sosa417e55d2011-01-25 16:40:48 -080071CACHED_ENTRIES = 12
Don Garrettf90edf02010-11-16 17:36:14 -080072
Simran Basi4baad082013-02-14 13:39:18 -080073TELEMETRY_FOLDER = 'telemetry_src'
74TELEMETRY_DEPS = ['dep-telemetry_dep.tar.bz2',
75 'dep-page_cycler_dep.tar.bz2',
Simran Basi0d078682013-03-22 16:40:04 -070076 'dep-chrome_test.tar.bz2',
77 'dep-perf_data_dep.tar.bz2']
Simran Basi4baad082013-02-14 13:39:18 -080078
Chris Sosa0356d3b2010-09-16 15:46:22 -070079# Sets up global to share between classes.
rtc@google.com21a5ca32009-11-04 18:23:23 +000080updater = None
rtc@google.comded22402009-10-26 22:36:21 +000081
xixuan3d48bff2017-01-30 19:00:09 -080082# 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 11:05:19 -070085#
xixuan3d48bff2017-01-30 19:00:09 -080086# For more, see the documentation in standard python library for
J. Richard Barnette3d977b82013-04-23 11:05:19 -070087# logging.handlers.TimedRotatingFileHandler
xixuan3d48bff2017-01-30 19:00:09 -080088_LOG_ROTATION_TIME = 'H'
Congbin Guo3afae6c2019-08-13 16:29:42 -070089_LOG_ROTATION_INTERVAL = 12 # hours
90_LOG_ROTATION_BACKUP = 28 # backup counts
Frank Farzan40160872011-12-12 18:39:18 -080091
xixuan52c2fba2016-05-20 17:02:48 -070092# Auto-update parameters
93
94# Error msg for missing key in CrOS auto-update.
Xixuan Wu32af9f12017-11-13 14:11:44 -080095KEY_ERROR_MSG = 'Key Error in RPC: %s= is required'
xixuan52c2fba2016-05-20 17:02:48 -070096
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -080097# 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-20 17:02:48 -0700102
Amin Hassanid4e35392019-10-03 11:02:44 -0700103class DevServerError(Exception):
104 """Exception class used by DevServer."""
105
106
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -0800107class 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 """
115 super(DeprecatedRPCError, self).__init__(DEPRECATED_RPC_ERROR_MSG % rpc_name)
116 self.rpc_name = rpc_name
117
118
Amin Hassani722e0962019-11-15 15:45:31 -0800119class DevServerHTTPError(cherrypy.HTTPError):
120 """Exception class to log the HTTPResponse before routing it to cherrypy."""
121 def __init__(self, status, message):
122 """CherryPy error with logging.
123
124 Args:
125 status: HTTPResponse status.
126 message: Message associated with the response.
127 """
128 cherrypy.HTTPError.__init__(self, status, message)
129 _Log('HTTPError status: %s message: %s', status, message)
130
131
Gabe Black3b567202015-09-23 14:07:59 -0700132def _canonicalize_archive_url(archive_url):
133 """Canonicalizes archive_url strings.
134
135 Raises:
136 DevserverError: if archive_url is not set.
137 """
138 if archive_url:
139 if not archive_url.startswith('gs://'):
Amin Hassanid4e35392019-10-03 11:02:44 -0700140 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700141 "Archive URL isn't from Google Storage (%s) ." % archive_url)
Gabe Black3b567202015-09-23 14:07:59 -0700142
143 return archive_url.rstrip('/')
144 else:
Amin Hassanid4e35392019-10-03 11:02:44 -0700145 raise DevServerError('Must specify an archive_url in the request')
Gabe Black3b567202015-09-23 14:07:59 -0700146
147
148def _canonicalize_local_path(local_path):
149 """Canonicalizes |local_path| strings.
150
151 Raises:
152 DevserverError: if |local_path| is not set.
153 """
154 # Restrict staging of local content to only files within the static
155 # directory.
156 local_path = os.path.abspath(local_path)
157 if not local_path.startswith(updater.static_dir):
Amin Hassanid4e35392019-10-03 11:02:44 -0700158 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700159 'Local path %s must be a subdirectory of the static'
160 ' directory: %s' % (local_path, updater.static_dir))
Gabe Black3b567202015-09-23 14:07:59 -0700161
162 return local_path.rstrip('/')
163
164
165def _get_artifacts(kwargs):
166 """Returns a tuple of named and file artifacts given the stage rpc kwargs.
167
168 Raises:
169 DevserverError if no artifacts would be returned.
170 """
171 artifacts = kwargs.get('artifacts')
172 files = kwargs.get('files')
173 if not artifacts and not files:
Amin Hassanid4e35392019-10-03 11:02:44 -0700174 raise DevServerError('No artifacts specified.')
Gabe Black3b567202015-09-23 14:07:59 -0700175
176 # Note we NEED to coerce files to a string as we get raw unicode from
177 # cherrypy and we treat files as strings elsewhere in the code.
178 return (str(artifacts).split(',') if artifacts else [],
179 str(files).split(',') if files else [])
180
181
Dan Shi61305df2015-10-26 16:52:35 -0700182def _is_android_build_request(kwargs):
183 """Check if a devserver call is for Android build, based on the arguments.
184
185 This method exams the request's arguments (os_type) to determine if the
186 request is for Android build. If os_type is set to `android`, returns True.
187 If os_type is not set or has other values, returns False.
188
189 Args:
190 kwargs: Keyword arguments for the request.
191
192 Returns:
193 True if the request is for Android build. False otherwise.
194 """
195 os_type = kwargs.get('os_type', None)
196 return os_type == 'android'
197
198
Gabe Black3b567202015-09-23 14:07:59 -0700199def _get_downloader(kwargs):
200 """Returns the downloader based on passed in arguments.
201
202 Args:
Amin Hassani08e42d22019-06-03 00:31:30 -0700203 kwargs: Keyword arguments for the request.
Gabe Black3b567202015-09-23 14:07:59 -0700204 """
205 local_path = kwargs.get('local_path')
206 if local_path:
207 local_path = _canonicalize_local_path(local_path)
208
209 dl = None
210 if local_path:
Prathmesh Prabhu58d08932018-01-19 15:08:19 -0800211 delete_source = _parse_boolean_arg(kwargs, 'delete_source')
212 dl = downloader.LocalDownloader(updater.static_dir, local_path,
213 delete_source=delete_source)
Gabe Black3b567202015-09-23 14:07:59 -0700214
Dan Shi61305df2015-10-26 16:52:35 -0700215 if not _is_android_build_request(kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700216 archive_url = kwargs.get('archive_url')
217 if not archive_url and not local_path:
Amin Hassanid4e35392019-10-03 11:02:44 -0700218 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700219 'Requires archive_url or local_path to be specified.')
Gabe Black3b567202015-09-23 14:07:59 -0700220 if archive_url and local_path:
Amin Hassanid4e35392019-10-03 11:02:44 -0700221 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700222 'archive_url and local_path can not both be specified.')
Gabe Black3b567202015-09-23 14:07:59 -0700223 if not dl:
224 archive_url = _canonicalize_archive_url(archive_url)
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700225 dl = downloader.GoogleStorageDownloader(
226 updater.static_dir, archive_url,
227 downloader.GoogleStorageDownloader.GetBuildIdFromArchiveURL(
228 archive_url))
Gabe Black3b567202015-09-23 14:07:59 -0700229 elif not dl:
230 target = kwargs.get('target', None)
Dan Shi72b16132015-10-08 12:10:33 -0700231 branch = kwargs.get('branch', None)
Dan Shi61305df2015-10-26 16:52:35 -0700232 build_id = kwargs.get('build_id', None)
233 if not target or not branch or not build_id:
Amin Hassanid4e35392019-10-03 11:02:44 -0700234 raise DevServerError('target, branch, build ID must all be specified for '
235 'downloading Android build.')
Dan Shi72b16132015-10-08 12:10:33 -0700236 dl = downloader.AndroidBuildDownloader(updater.static_dir, branch, build_id,
237 target)
Gabe Black3b567202015-09-23 14:07:59 -0700238
239 return dl
240
241
242def _get_downloader_and_factory(kwargs):
243 """Returns the downloader and artifact factory based on passed in arguments.
244
245 Args:
Amin Hassani08e42d22019-06-03 00:31:30 -0700246 kwargs: Keyword arguments for the request.
Gabe Black3b567202015-09-23 14:07:59 -0700247 """
248 artifacts, files = _get_artifacts(kwargs)
249 dl = _get_downloader(kwargs)
250
Achuith Bhandarkar2a1fcd82019-10-17 17:45:58 -0700251 if (isinstance(dl, (downloader.GoogleStorageDownloader,
252 downloader.LocalDownloader))):
Gabe Black3b567202015-09-23 14:07:59 -0700253 factory_class = build_artifact.ChromeOSArtifactFactory
Dan Shi72b16132015-10-08 12:10:33 -0700254 elif isinstance(dl, downloader.AndroidBuildDownloader):
Gabe Black3b567202015-09-23 14:07:59 -0700255 factory_class = build_artifact.AndroidArtifactFactory
256 else:
Amin Hassanid4e35392019-10-03 11:02:44 -0700257 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700258 'Unrecognized value for downloader type: %s' % type(dl))
Gabe Black3b567202015-09-23 14:07:59 -0700259
260 factory = factory_class(dl.GetBuildDir(), artifacts, files, dl.GetBuild())
261
262 return dl, factory
263
264
Scott Zawalski4647ce62012-01-03 17:17:28 -0500265def _LeadingWhiteSpaceCount(string):
266 """Count the amount of leading whitespace in a string.
267
268 Args:
269 string: The string to count leading whitespace in.
Don Garrettf84631a2014-01-07 18:21:26 -0800270
Scott Zawalski4647ce62012-01-03 17:17:28 -0500271 Returns:
272 number of white space chars before characters start.
273 """
Gabe Black3b567202015-09-23 14:07:59 -0700274 matched = re.match(r'^\s+', string)
Scott Zawalski4647ce62012-01-03 17:17:28 -0500275 if matched:
276 return len(matched.group())
277
278 return 0
279
280
281def _PrintDocStringAsHTML(func):
282 """Make a functions docstring somewhat HTML style.
283
284 Args:
285 func: The function to return the docstring from.
Don Garrettf84631a2014-01-07 18:21:26 -0800286
Scott Zawalski4647ce62012-01-03 17:17:28 -0500287 Returns:
288 A string that is somewhat formated for a web browser.
289 """
290 # TODO(scottz): Make this parse Args/Returns in a prettier way.
291 # Arguments could be bolded and indented etc.
292 html_doc = []
293 for line in func.__doc__.splitlines():
294 leading_space = _LeadingWhiteSpaceCount(line)
295 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700296 line = ' ' * leading_space + line
Scott Zawalski4647ce62012-01-03 17:17:28 -0500297
298 html_doc.append('<BR>%s' % line)
299
300 return '\n'.join(html_doc)
301
302
Simran Basief83d6a2014-08-28 14:32:01 -0700303def _GetUpdateTimestampHandler(static_dir):
304 """Returns a handler to update directory staged.timestamp.
305
306 This handler resets the stage.timestamp whenever static content is accessed.
307
308 Args:
309 static_dir: Directory from which static content is being staged.
310
311 Returns:
Amin Hassani08e42d22019-06-03 00:31:30 -0700312 A cherrypy handler to update the timestamp of accessed content.
Simran Basief83d6a2014-08-28 14:32:01 -0700313 """
314 def UpdateTimestampHandler():
315 if not '404' in cherrypy.response.status:
316 build_match = re.match(devserver_constants.STAGED_BUILD_REGEX,
317 cherrypy.request.path_info)
318 if build_match:
319 build_dir = os.path.join(static_dir, build_match.group('build'))
320 downloader.Downloader.TouchTimestampForStaged(build_dir)
321 return UpdateTimestampHandler
322
323
Chris Sosa7c931362010-10-11 19:49:01 -0700324def _GetConfig(options):
325 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800326
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800327 socket_host = '::'
Yu-Ju Hongc8d4af32013-11-12 15:14:26 -0800328 # Fall back to IPv4 when python is not configured with IPv6.
329 if not socket.has_ipv6:
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800330 socket_host = '0.0.0.0'
331
Simran Basief83d6a2014-08-28 14:32:01 -0700332 # Adds the UpdateTimestampHandler to cherrypy's tools. This tools executes
333 # on the on_end_resource hook. This hook is called once processing is
334 # complete and the response is ready to be returned.
335 cherrypy.tools.update_timestamp = cherrypy.Tool(
336 'on_end_resource', _GetUpdateTimestampHandler(options.static_dir))
337
David Riley2fcb0122017-11-02 11:25:39 -0700338 base_config = {
339 'global': {
340 'server.log_request_headers': True,
341 'server.protocol_version': 'HTTP/1.1',
342 'server.socket_host': socket_host,
343 'server.socket_port': int(options.port),
344 'response.timeout': 6000,
345 'request.show_tracebacks': True,
346 'server.socket_timeout': 60,
347 'server.thread_pool': 2,
348 'engine.autoreload.on': False,
349 },
350 '/api': {
351 # Gets rid of cherrypy parsing post file for args.
352 'request.process_request_body': False,
353 },
354 '/build': {
355 'response.timeout': 100000,
356 },
357 '/update': {
358 # Gets rid of cherrypy parsing post file for args.
359 'request.process_request_body': False,
360 'response.timeout': 10000,
361 },
362 # Sets up the static dir for file hosting.
363 '/static': {
364 'tools.staticdir.dir': options.static_dir,
365 'tools.staticdir.on': True,
366 'response.timeout': 10000,
367 'tools.update_timestamp.on': True,
368 },
369 }
Chris Sosa5f118ef2012-07-12 11:37:50 -0700370 if options.production:
Alex Miller93beca52013-07-30 19:25:09 -0700371 base_config['global'].update({'server.thread_pool': 150})
Scott Zawalski1c5e7cd2012-02-27 13:12:52 -0500372
Chris Sosa7c931362010-10-11 19:49:01 -0700373 return base_config
rtc@google.com64244662009-11-12 00:52:08 +0000374
Darin Petkove17164a2010-08-11 13:24:41 -0700375
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700376def _GetRecursiveMemberObject(root, member_list):
377 """Returns an object corresponding to a nested member list.
378
379 Args:
380 root: the root object to search
381 member_list: list of nested members to search
Don Garrettf84631a2014-01-07 18:21:26 -0800382
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700383 Returns:
384 An object corresponding to the member name list; None otherwise.
385 """
386 for member in member_list:
387 next_root = root.__class__.__dict__.get(member)
388 if not next_root:
389 return None
390 root = next_root
391 return root
392
393
394def _IsExposed(name):
395 """Returns True iff |name| has an `exposed' attribute and it is set."""
396 return hasattr(name, 'exposed') and name.exposed
397
398
Congbin Guo6bc32182019-08-20 17:54:30 -0700399def _GetExposedMethod(nested_member):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700400 """Returns a CherryPy-exposed method, if such exists.
401
402 Args:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700403 nested_member: a slash-joined path to the nested member
Don Garrettf84631a2014-01-07 18:21:26 -0800404
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700405 Returns:
Congbin Guo6bc32182019-08-20 17:54:30 -0700406 A function object corresponding to the path defined by |nested_member| from
407 the app root object registered, if the function is exposed; None otherwise.
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700408 """
Congbin Guo6bc32182019-08-20 17:54:30 -0700409 for app in cherrypy.tree.apps.values():
410 # Use the 'index' function doc as the doc of the app.
411 if nested_member == app.script_name.lstrip('/'):
412 nested_member = 'index'
413
414 method = _GetRecursiveMemberObject(app.root, nested_member.split('/'))
415 if method and isinstance(method, types.FunctionType) and _IsExposed(method):
416 return method
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700417
418
Gilad Arnold748c8322012-10-12 09:51:35 -0700419def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700420 """Finds exposed CherryPy methods.
421
422 Args:
423 root: the root object for searching
424 prefix: slash-joined chain of members leading to current object
425 unlisted: URLs to be excluded regardless of their exposed status
Don Garrettf84631a2014-01-07 18:21:26 -0800426
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700427 Returns:
428 List of exposed URLs that are not unlisted.
429 """
430 method_list = []
Congbin Guo6bc32182019-08-20 17:54:30 -0700431 for member in root.__class__.__dict__.keys():
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700432 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 09:51:35 -0700433 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700434 continue
435 member_obj = root.__class__.__dict__[member]
436 if _IsExposed(member_obj):
Amin Hassani08e42d22019-06-03 00:31:30 -0700437 if isinstance(member_obj, types.FunctionType):
Congbin Guo6bc32182019-08-20 17:54:30 -0700438 # Regard the app name as exposed "method" name if it exposed 'index'
439 # function.
440 if prefix and member == 'index':
441 method_list.append(prefix)
442 else:
443 method_list.append(prefixed_member)
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700444 else:
445 method_list += _FindExposedMethods(
446 member_obj, prefixed_member, unlisted)
447 return method_list
448
449
xixuan52c2fba2016-05-20 17:02:48 -0700450def _check_base_args_for_auto_update(kwargs):
xixuanac89ce82016-11-30 16:48:20 -0800451 """Check basic args required for auto-update.
452
453 Args:
454 kwargs: the parameters to be checked.
455
456 Raises:
457 DevServerHTTPError if required parameters don't exist in kwargs.
458 """
xixuan52c2fba2016-05-20 17:02:48 -0700459 if 'host_name' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -0800460 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
461 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -0700462
463 if 'build_name' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -0800464 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
465 KEY_ERROR_MSG % 'build_name')
xixuan52c2fba2016-05-20 17:02:48 -0700466
467
468def _parse_boolean_arg(kwargs, key):
xixuanac89ce82016-11-30 16:48:20 -0800469 """Parse boolean arg from kwargs.
470
471 Args:
472 kwargs: the parameters to be checked.
473 key: the key to be parsed.
474
475 Returns:
476 The boolean value of kwargs[key], or False if key doesn't exist in kwargs.
477
478 Raises:
479 DevServerHTTPError if kwargs[key] is not a boolean variable.
480 """
xixuan52c2fba2016-05-20 17:02:48 -0700481 if key in kwargs:
482 if kwargs[key] == 'True':
483 return True
484 elif kwargs[key] == 'False':
485 return False
486 else:
Amin Hassani722e0962019-11-15 15:45:31 -0800487 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
488 'The value for key %s is not boolean.' % key)
xixuan52c2fba2016-05-20 17:02:48 -0700489 else:
490 return False
491
xixuan447ad9d2017-02-28 14:46:20 -0800492
xixuanac89ce82016-11-30 16:48:20 -0800493def _parse_string_arg(kwargs, key):
494 """Parse string arg from kwargs.
495
496 Args:
497 kwargs: the parameters to be checked.
498 key: the key to be parsed.
499
500 Returns:
501 The string value of kwargs[key], or None if key doesn't exist in kwargs.
502 """
503 if key in kwargs:
504 return kwargs[key]
505 else:
506 return None
507
xixuan447ad9d2017-02-28 14:46:20 -0800508
xixuanac89ce82016-11-30 16:48:20 -0800509def _build_uri_from_build_name(build_name):
510 """Get build url from a given build name.
511
512 Args:
513 build_name: the build name to be parsed, whose format is
514 'board/release_version'.
515
516 Returns:
517 The release_archive_url on Google Storage for this build name.
518 """
Amin Hassani08e42d22019-06-03 00:31:30 -0700519 # TODO(ahassani): This function doesn't seem to be used anywhere since its
520 # previous use of lib.paygen.gspath was broken and it doesn't seem to be
521 # causing any runtime issues. So deprecate this in the future.
522 tokens = build_name.split('/')
523 return 'gs://chromeos-releases/stable-channel/%s/%s' % (tokens[0], tokens[1])
xixuan52c2fba2016-05-20 17:02:48 -0700524
xixuan447ad9d2017-02-28 14:46:20 -0800525
526def _clear_process(host_name, pid):
527 """Clear AU process for given hostname and pid.
528
529 This clear includes:
530 1. kill process if it's alive.
531 2. delete the track status file of this process.
532 3. delete the executing log file of this process.
533
534 Args:
535 host_name: the host to execute auto-update.
536 pid: the background auto-update process id.
537 """
538 if cros_update_progress.IsProcessAlive(pid):
539 os.killpg(int(pid), signal.SIGKILL)
540
541 cros_update_progress.DelTrackStatusFile(host_name, pid)
542 cros_update_progress.DelExecuteLogFile(host_name, pid)
543
544
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -0800545def is_deprecated_server():
546 """Gets whether the devserver has deprecated RPCs."""
547 return cherrypy.config.get('infra_removal', False)
548
549
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700550class ApiRoot(object):
551 """RESTful API for Dev Server information."""
552 exposed = True
553
554 @cherrypy.expose
Gilad Arnold286a0062012-01-12 13:47:02 -0800555 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 11:36:27 -0700556 """Returns a JSON object containing a log of host event.
557
558 Args:
559 ip: address of host whose event log is requested, or `all'
Don Garrettf84631a2014-01-07 18:21:26 -0800560
Gilad Arnold1b908392012-10-05 11:36:27 -0700561 Returns:
Amin Hassani7c447852019-09-26 15:01:48 -0700562 A JSON dictionary containing all or some of the following fields:
Amin Hassanie7ead902019-10-11 16:42:43 -0700563 version: The Chromium OS version the device is running.
564 track: The channel the device is running on.
565 board: The device's board.
566 event_result: The event result of Omaha request.
567 event_type: The event type of Omaha request.
568 previous_version: The Chromium OS version we updated and rebooted from.
569 timestamp: The timestamp the event was received.
Amin Hassani7c447852019-09-26 15:01:48 -0700570 See the OmahaEvent class in update_engine/omaha_request_action.h for
571 event type and status code definitions. If the ip does not exist an empty
572 string is returned.
Gilad Arnold1b908392012-10-05 11:36:27 -0700573
574 Example URL:
575 http://myhost/api/hostlog?ip=192.168.1.5
576 """
Gilad Arnold286a0062012-01-12 13:47:02 -0800577 return updater.HandleHostLogPing(ip)
578
579 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800580 def fileinfo(self, *args):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700581 """Returns information about a given staged file.
582
583 Args:
Don Garrettf84631a2014-01-07 18:21:26 -0800584 args: path to the file inside the server's static staging directory
585
Gilad Arnold55a2a372012-10-02 09:46:32 -0700586 Returns:
587 A JSON encoded dictionary with information about the said file, which may
588 contain the following keys/values:
Gilad Arnold1b908392012-10-05 11:36:27 -0700589 size (int): the file size in bytes
Gilad Arnold1b908392012-10-05 11:36:27 -0700590 sha256 (string): a base64 encoded SHA256 hash
591
592 Example URL:
593 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 09:46:32 -0700594 """
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -0800595 if is_deprecated_server():
596 raise DeprecatedRPCError('fileinfo')
597
Amin Hassani28df4212019-10-28 10:16:50 -0700598 # TODO(ahassani): A better way of doing this is to just return the the
599 # content of the payloads' property file instead. That has all this info
600 # except that the key for sha256 is 'sha256_hex', but still base64 encdoed.
601
Don Garrettf84631a2014-01-07 18:21:26 -0800602 file_path = os.path.join(updater.static_dir, *args)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700603 if not os.path.exists(file_path):
Amin Hassanid4e35392019-10-03 11:02:44 -0700604 raise DevServerError('file not found: %s' % file_path)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700605 try:
606 file_size = os.path.getsize(file_path)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700607 file_sha256 = common_util.GetFileSha256(file_path)
Amin Hassani469f5702019-10-21 15:35:06 -0700608 except os.error as e:
Amin Hassanid4e35392019-10-03 11:02:44 -0700609 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700610 'failed to get info for file %s: %s' % (file_path, e))
Gilad Arnolde74b3812013-04-22 11:27:38 -0700611
Gilad Arnolde74b3812013-04-22 11:27:38 -0700612 return json.dumps({
Amin Hassani28df4212019-10-28 10:16:50 -0700613 'size': file_size,
614 'sha256': file_sha256,
615 }, sort_keys=True)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700616
Chris Sosa76e44b92013-01-31 12:11:38 -0800617
David Rochberg7c79a812011-01-19 14:24:45 -0500618class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700619 """The Root Class for the Dev Server.
620
621 CherryPy works as follows:
622 For each method in this class, cherrpy interprets root/path
623 as a call to an instance of DevServerRoot->method_name. For example,
624 a call to http://myhost/build will call build. CherryPy automatically
625 parses http args and places them as keyword arguments in each method.
626 For paths http://myhost/update/dir1/dir2, you can use *args so that
627 cherrypy uses the update method and puts the extra paths in args.
628 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700629 # Method names that should not be listed on the index page.
630 _UNLISTED_METHODS = ['index', 'doc']
631
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700632 api = ApiRoot()
Chris Sosa7c931362010-10-11 19:49:01 -0700633
Dan Shi59ae7092013-06-04 14:37:27 -0700634 # Number of threads that devserver is staging images.
635 _staging_thread_count = 0
636 # Lock used to lock increasing/decreasing count.
637 _staging_thread_count_lock = threading.Lock()
638
joychen3cb228e2013-06-12 12:13:13 -0700639 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700640 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800641 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 12:13:13 -0700642 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 14:24:45 -0500643
Congbin Guo3afae6c2019-08-13 16:29:42 -0700644 @property
645 def staging_thread_count(self):
646 """Get the staging thread count."""
647 return self._staging_thread_count
Dan Shiafd0e492015-05-27 14:23:51 -0700648
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700649 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500650 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700651 """Builds the package specified."""
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -0800652 if is_deprecated_server():
653 raise DeprecatedRPCError('build')
654
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700655 import builder
656 if self._builder is None:
657 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500658 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700659
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700660 @cherrypy.expose
Dan Shif8eb0d12013-08-01 17:52:06 -0700661 def is_staged(self, **kwargs):
662 """Check if artifacts have been downloaded.
663
Congbin Guo3afae6c2019-08-13 16:29:42 -0700664 Examples:
665 To check if autotest and test_suites are staged:
666 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
667 artifacts=autotest,test_suites
668
Amin Hassani08e42d22019-06-03 00:31:30 -0700669 Args:
Chris Sosa6b0c6172013-08-05 17:01:33 -0700670 async: True to return without waiting for download to complete.
671 artifacts: Comma separated list of named artifacts to download.
672 These are defined in artifact_info and have their implementation
673 in build_artifact.py.
674 files: Comma separated list of file artifacts to stage. These
675 will be available as is in the corresponding static directory with no
676 custom post-processing.
677
Congbin Guo3afae6c2019-08-13 16:29:42 -0700678 Returns:
679 True of all artifacts are staged.
Dan Shif8eb0d12013-08-01 17:52:06 -0700680 """
Gabe Black3b567202015-09-23 14:07:59 -0700681 dl, factory = _get_downloader_and_factory(kwargs)
Aviv Keshet57d18172016-06-18 20:39:09 -0700682 response = str(dl.IsStaged(factory))
683 _Log('Responding to is_staged %s request with %r', kwargs, response)
684 return response
Dan Shi59ae7092013-06-04 14:37:27 -0700685
Chris Sosa76e44b92013-01-31 12:11:38 -0800686 @cherrypy.expose
Prashanth Ba06d2d22014-03-07 15:35:19 -0800687 def list_image_dir(self, **kwargs):
688 """Take an archive url and list the contents in its staged directory.
689
Amin Hassani08e42d22019-06-03 00:31:30 -0700690 Examples:
Prashanth Ba06d2d22014-03-07 15:35:19 -0800691 To list the contents of where this devserver should have staged
692 gs://image-archive/<board>-release/<build> call:
693 http://devserver_url:<port>/list_image_dir?archive_url=<gs://..>
694
Congbin Guo3afae6c2019-08-13 16:29:42 -0700695 Args:
696 archive_url: Google Storage URL for the build.
697
Prashanth Ba06d2d22014-03-07 15:35:19 -0800698 Returns:
699 A string with information about the contents of the image directory.
700 """
Gabe Black3b567202015-09-23 14:07:59 -0700701 dl = _get_downloader(kwargs)
Prashanth Ba06d2d22014-03-07 15:35:19 -0800702 try:
Gabe Black3b567202015-09-23 14:07:59 -0700703 image_dir_contents = dl.ListBuildDir()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800704 except build_artifact.ArtifactDownloadError as e:
705 return 'Cannot list the contents of staged artifacts. %s' % e
706 if not image_dir_contents:
Gabe Black3b567202015-09-23 14:07:59 -0700707 return '%s has not been staged on this devserver.' % dl.DescribeSource()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800708 return image_dir_contents
709
710 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800711 def stage(self, **kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700712 """Downloads and caches build artifacts.
Chris Sosa76e44b92013-01-31 12:11:38 -0800713
Gabe Black3b567202015-09-23 14:07:59 -0700714 Downloads and caches build artifacts, possibly from a Google Storage URL,
Dan Shi72b16132015-10-08 12:10:33 -0700715 or from Android's build server. Returns once these have been downloaded
Gabe Black3b567202015-09-23 14:07:59 -0700716 on the devserver. A call to this will attempt to cache non-specified
717 artifacts in the background for the given from the given URL following
718 the principle of spatial locality. Spatial locality of different
Chris Sosa76e44b92013-01-31 12:11:38 -0800719 artifacts is explicitly defined in the build_artifact module.
720
721 These artifacts will then be available from the static/ sub-directory of
722 the devserver.
723
Amin Hassani08e42d22019-06-03 00:31:30 -0700724 Examples:
Chris Sosa76e44b92013-01-31 12:11:38 -0800725 To download the autotest and test suites tarballs:
726 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
727 artifacts=autotest,test_suites
728 To download the full update payload:
729 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
730 artifacts=full_payload
Chris Sosa6b0c6172013-08-05 17:01:33 -0700731 To download just a file called blah.bin:
732 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
733 files=blah.bin
Chris Sosa76e44b92013-01-31 12:11:38 -0800734
735 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 16:39:34 -0700736 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 12:11:38 -0800737
738 Note for this example, relative path is the archive_url stripped of its
739 basename i.e. path/ in the examples above. Specific example:
740
741 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
742
743 Will get staged to:
744
joychened64b222013-06-21 16:39:34 -0700745 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Congbin Guo3afae6c2019-08-13 16:29:42 -0700746
747 Args:
748 archive_url: Google Storage URL for the build.
749 local_path: Local path for the build.
750 delete_source: Only meaningful with local_path. bool to indicate if the
751 source files should be deleted. This is especially useful when staging
752 a file locally in resource constrained environments as it allows us to
753 move the relevant files locally instead of copying them.
754 async: True to return without waiting for download to complete.
755 artifacts: Comma separated list of named artifacts to download.
756 These are defined in artifact_info and have their implementation
757 in build_artifact.py.
758 files: Comma separated list of files to stage. These
759 will be available as is in the corresponding static directory with no
760 custom post-processing.
761 clean: True to remove any previously staged artifacts first.
Chris Sosa76e44b92013-01-31 12:11:38 -0800762 """
Gabe Black3b567202015-09-23 14:07:59 -0700763 dl, factory = _get_downloader_and_factory(kwargs)
764
Dan Shi59ae7092013-06-04 14:37:27 -0700765 with DevServerRoot._staging_thread_count_lock:
766 DevServerRoot._staging_thread_count += 1
767 try:
Laurence Goodbyf5c958d2016-01-14 18:23:56 -0800768 boolean_string = kwargs.get('clean')
769 clean = xbuddy.XBuddy.ParseBoolean(boolean_string)
770 if clean and os.path.exists(dl.GetBuildDir()):
771 _Log('Removing %s' % dl.GetBuildDir())
772 shutil.rmtree(dl.GetBuildDir())
Achuith Bhandarkar2a1fcd82019-10-17 17:45:58 -0700773 is_async = kwargs.get('async', False)
774 dl.Download(factory, is_async=is_async)
Dan Shi59ae7092013-06-04 14:37:27 -0700775 finally:
776 with DevServerRoot._staging_thread_count_lock:
777 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800778 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700779
780 @cherrypy.expose
xixuan52c2fba2016-05-20 17:02:48 -0700781 def cros_au(self, **kwargs):
782 """Auto-update a CrOS DUT.
783
784 Args:
785 kwargs:
786 host_name: the hostname of the DUT to auto-update.
787 build_name: the build name for update the DUT.
788 force_update: Force an update even if the version installed is the
789 same. Default: False.
790 full_update: If True, do not run stateful update, directly force a full
791 reimage. If False, try stateful update first if the dut is already
792 installed with the same version.
793 async: Whether the auto_update function is ran in the background.
David Rileyee75de22017-11-02 10:48:15 -0700794 quick_provision: Whether the quick provision path is attempted first.
xixuan52c2fba2016-05-20 17:02:48 -0700795
796 Returns:
797 A tuple includes two elements:
798 a boolean variable represents whether the auto-update process is
799 successfully started.
800 an integer represents the background auto-update process id.
801 """
802 _check_base_args_for_auto_update(kwargs)
803
804 host_name = kwargs['host_name']
805 build_name = kwargs['build_name']
806 force_update = _parse_boolean_arg(kwargs, 'force_update')
807 full_update = _parse_boolean_arg(kwargs, 'full_update')
Achuith Bhandarkar2a1fcd82019-10-17 17:45:58 -0700808 is_async = _parse_boolean_arg(kwargs, 'async')
xixuanac89ce82016-11-30 16:48:20 -0800809 original_build = _parse_string_arg(kwargs, 'original_build')
David Haddock90e49442017-04-07 19:14:09 -0700810 payload_filename = _parse_string_arg(kwargs, 'payload_filename')
David Haddock20559612017-06-28 22:15:08 -0700811 clobber_stateful = _parse_boolean_arg(kwargs, 'clobber_stateful')
David Rileyee75de22017-11-02 10:48:15 -0700812 quick_provision = _parse_boolean_arg(kwargs, 'quick_provision')
813
814 devserver_url = updater.GetDevserverUrl()
815 static_url = updater.GetStaticUrl()
xixuan52c2fba2016-05-20 17:02:48 -0700816
Mike Frysingera777cfc2020-03-02 17:20:46 -0500817 # Command of running auto-update.
818 cmd = ['cros_update', '--hostname', host_name, '-b', build_name,
819 '--static_dir', updater.static_dir]
820
821 # The original_build's format is like: link/3428.210.0
822 # The corresponding release_archive_url's format is like:
823 # gs://chromeos-releases/stable-channel/link/3428.210.0
824 if original_build:
825 release_archive_url = _build_uri_from_build_name(original_build)
826 # First staging the stateful.tgz synchronousely.
827 self.stage(files='stateful.tgz', is_async=False,
828 archive_url=release_archive_url)
829 cmd += ['--original_build', original_build]
830
831 if force_update:
832 cmd += ['--force_update']
833
834 if full_update:
835 cmd += ['--full_update']
836
837 if payload_filename:
838 cmd += ['--payload_filename', payload_filename]
839
840 if clobber_stateful:
841 cmd += ['--clobber_stateful']
842
843 if quick_provision:
844 cmd += ['--quick_provision']
845
846 if devserver_url:
847 cmd += ['--devserver_url', devserver_url]
848
849 if static_url:
850 cmd += ['--static_url', static_url]
851
Achuith Bhandarkar2a1fcd82019-10-17 17:45:58 -0700852 if is_async:
Amin Hassani78520ae2019-10-29 13:26:51 -0700853 p = subprocess.Popen(cmd, preexec_fn=os.setsid)
xixuan2a0970a2016-08-10 12:12:44 -0700854 pid = os.getpgid(p.pid)
xixuan52c2fba2016-05-20 17:02:48 -0700855
856 # Pre-write status in the track_status_file before the first call of
857 # 'get_au_status' to make sure that the track_status_file exists.
xixuan2a0970a2016-08-10 12:12:44 -0700858 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -0700859 progress_tracker.WriteStatus('CrOS update is just started.')
860
xixuan2a0970a2016-08-10 12:12:44 -0700861 return json.dumps((True, pid))
xixuan52c2fba2016-05-20 17:02:48 -0700862 else:
Mike Frysingera777cfc2020-03-02 17:20:46 -0500863 subprocess.check_call(cmd)
xixuan27d50442017-08-09 10:38:25 -0700864 return json.dumps((True, -1))
xixuan52c2fba2016-05-20 17:02:48 -0700865
866 @cherrypy.expose
867 def get_au_status(self, **kwargs):
868 """Check if the auto-update task is finished.
869
870 It handles 4 cases:
871 1. If an error exists in the track_status_file, delete the track file and
872 raise it.
873 2. If cros-update process is finished, delete the file and return the
874 success result.
875 3. If the process is not running, delete the track file and raise an error
876 about 'the process is terminated due to unknown reason'.
877 4. If the track_status_file does not exist, kill the process if it exists,
878 and raise the IOError.
879
880 Args:
881 kwargs:
882 host_name: the hostname of the DUT to auto-update.
883 pid: the background process id of cros-update.
884
885 Returns:
xixuan28d99072016-10-06 12:24:16 -0700886 A dict with three elements:
xixuan52c2fba2016-05-20 17:02:48 -0700887 a boolean variable represents whether the auto-update process is
888 finished.
889 a string represents the current auto-update process status.
890 For example, 'Transfer Devserver/Stateful Update Package'.
xixuan28d99072016-10-06 12:24:16 -0700891 a detailed error message paragraph if there exists an Auto-Update
892 error, in which the last line shows the main exception. Empty
893 string otherwise.
xixuan52c2fba2016-05-20 17:02:48 -0700894 """
895 if 'host_name' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -0800896 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
897 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -0700898
899 if 'pid' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -0800900 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
901 KEY_ERROR_MSG % 'pid')
xixuan52c2fba2016-05-20 17:02:48 -0700902
903 host_name = kwargs['host_name']
904 pid = kwargs['pid']
905 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
906
xixuan28d99072016-10-06 12:24:16 -0700907 result_dict = {'finished': False, 'status': '', 'detailed_error_msg': ''}
xixuan52c2fba2016-05-20 17:02:48 -0700908 try:
909 result = progress_tracker.ReadStatus()
910 if result.startswith(cros_update_progress.ERROR_TAG):
xixuan28d99072016-10-06 12:24:16 -0700911 result_dict['detailed_error_msg'] = result[len(
912 cros_update_progress.ERROR_TAG):]
xixuan28681fd2016-11-23 11:13:56 -0800913 elif result == cros_update_progress.FINISHED:
xixuan28d99072016-10-06 12:24:16 -0700914 result_dict['finished'] = True
915 result_dict['status'] = result
xixuan28681fd2016-11-23 11:13:56 -0800916 elif not cros_update_progress.IsProcessAlive(pid):
xixuan28d99072016-10-06 12:24:16 -0700917 result_dict['detailed_error_msg'] = (
918 'Cros_update process terminated midway due to unknown reason. '
919 'Last update status was %s' % result)
xixuan28681fd2016-11-23 11:13:56 -0800920 else:
921 result_dict['status'] = result
922 except IOError as e:
923 if pid and cros_update_progress.IsProcessAlive(pid):
xixuan2a0970a2016-08-10 12:12:44 -0700924 os.killpg(int(pid), signal.SIGKILL)
xixuan52c2fba2016-05-20 17:02:48 -0700925
xixuan28681fd2016-11-23 11:13:56 -0800926 result_dict['detailed_error_msg'] = str(e)
927
928 return json.dumps(result_dict)
xixuan52c2fba2016-05-20 17:02:48 -0700929
930 @cherrypy.expose
David Riley6d5fca02017-10-31 10:35:47 -0700931 def post_au_status(self, status, **kwargs):
932 """Updates the status of an auto-update task.
933
934 Callers will need to POST to this URL with a body of MIME-type
935 "multipart/form-data".
936 The body should include a single argument, 'status', containing the
937 AU status to record.
938
939 Args:
940 status: The updated status.
941 kwargs:
942 host_name: the hostname of the DUT to auto-update.
943 pid: the background process id of cros-update.
944 """
945 if 'host_name' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -0800946 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
947 KEY_ERROR_MSG % 'host_name')
David Riley6d5fca02017-10-31 10:35:47 -0700948
949 if 'pid' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -0800950 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
951 KEY_ERROR_MSG % 'pid')
David Riley6d5fca02017-10-31 10:35:47 -0700952
953 host_name = kwargs['host_name']
954 pid = kwargs['pid']
David Riley3cea2582017-11-24 22:03:01 -0800955 status = status.rstrip()
956 _Log('Recording status for %s (%s): %s' % (host_name, pid, status))
David Riley6d5fca02017-10-31 10:35:47 -0700957 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
958
David Riley3cea2582017-11-24 22:03:01 -0800959 progress_tracker.WriteStatus(status)
David Riley6d5fca02017-10-31 10:35:47 -0700960
961 return 'True'
962
963 @cherrypy.expose
xixuan52c2fba2016-05-20 17:02:48 -0700964 def handler_cleanup(self, **kwargs):
xixuan3bc974e2016-10-18 17:21:43 -0700965 """Clean track status log and temp directory for CrOS auto-update process.
xixuan52c2fba2016-05-20 17:02:48 -0700966
967 Args:
968 kwargs:
969 host_name: the hostname of the DUT to auto-update.
970 pid: the background process id of cros-update.
971 """
972 if 'host_name' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -0800973 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
974 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -0700975
976 if 'pid' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -0800977 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
978 KEY_ERROR_MSG % 'pid')
xixuan52c2fba2016-05-20 17:02:48 -0700979
980 host_name = kwargs['host_name']
981 pid = kwargs['pid']
982 cros_update_progress.DelTrackStatusFile(host_name, pid)
xixuan3bc974e2016-10-18 17:21:43 -0700983 cros_update_progress.DelAUTempDirectory(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -0700984
985 @cherrypy.expose
986 def kill_au_proc(self, **kwargs):
987 """Kill CrOS auto-update process using given process id.
988
989 Args:
990 kwargs:
991 host_name: Kill all the CrOS auto-update process of this host.
992
993 Returns:
994 True if all processes are killed properly.
995 """
996 if 'host_name' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -0800997 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
998 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -0700999
xixuan447ad9d2017-02-28 14:46:20 -08001000 cur_pid = kwargs.get('pid')
1001
xixuan52c2fba2016-05-20 17:02:48 -07001002 host_name = kwargs['host_name']
xixuan3bc974e2016-10-18 17:21:43 -07001003 track_log_list = cros_update_progress.GetAllTrackStatusFileByHostName(
1004 host_name)
xixuan52c2fba2016-05-20 17:02:48 -07001005 for log in track_log_list:
1006 # The track log's full path is: path/host_name_pid.log
1007 # Use splitext to remove file extension, then parse pid from the
1008 # filename.
Congbin Guo3afae6c2019-08-13 16:29:42 -07001009 pid = os.path.splitext(os.path.basename(log))[0][len(host_name) + 1:]
xixuan447ad9d2017-02-28 14:46:20 -08001010 _clear_process(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -07001011
xixuan447ad9d2017-02-28 14:46:20 -08001012 if cur_pid:
1013 _clear_process(host_name, cur_pid)
xixuan52c2fba2016-05-20 17:02:48 -07001014
1015 return 'True'
1016
1017 @cherrypy.expose
1018 def collect_cros_au_log(self, **kwargs):
1019 """Collect CrOS auto-update log.
1020
1021 Args:
1022 kwargs:
1023 host_name: the hostname of the DUT to auto-update.
1024 pid: the background process id of cros-update.
1025
1026 Returns:
David Haddock9f459632017-05-11 14:45:46 -07001027 A dictionary containing the execute log file and any hostlog files.
xixuan52c2fba2016-05-20 17:02:48 -07001028 """
1029 if 'host_name' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -08001030 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
1031 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -07001032
1033 if 'pid' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -08001034 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
1035 KEY_ERROR_MSG % 'pid')
xixuan52c2fba2016-05-20 17:02:48 -07001036
1037 host_name = kwargs['host_name']
1038 pid = kwargs['pid']
xixuan3bc974e2016-10-18 17:21:43 -07001039
1040 # Fetch the execute log recorded by cros_update_progress.
xixuan1bbfaba2016-10-13 17:53:22 -07001041 au_log = cros_update_progress.ReadExecuteLogFile(host_name, pid)
1042 cros_update_progress.DelExecuteLogFile(host_name, pid)
David Haddock9f459632017-05-11 14:45:46 -07001043 # Fetch the cros_au host_logs if they exist
1044 au_hostlogs = cros_update_progress.ReadAUHostLogFiles(host_name, pid)
1045 return json.dumps({'cros_au_log': au_log, 'host_logs': au_hostlogs})
xixuan1bbfaba2016-10-13 17:53:22 -07001046
xixuan52c2fba2016-05-20 17:02:48 -07001047 @cherrypy.expose
Dan Shi2f136862016-02-11 15:38:38 -08001048 def locate_file(self, **kwargs):
1049 """Get the path to the given file name.
1050
1051 This method looks up the given file name inside specified build artifacts.
1052 One use case is to help caller to locate an apk file inside a build
1053 artifact. The location of the apk file could be different based on the
1054 branch and target.
1055
1056 Args:
1057 file_name: Name of the file to look for.
1058 artifacts: A list of artifact names to search for the file.
1059
1060 Returns:
1061 Path to the file with the given name. It's relative to the folder for the
1062 build, e.g., DATA/priv-app/sl4a/sl4a.apk
Dan Shi2f136862016-02-11 15:38:38 -08001063 """
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -08001064 if is_deprecated_server():
1065 raise DeprecatedRPCError('locate_file')
1066
Dan Shi2f136862016-02-11 15:38:38 -08001067 dl, _ = _get_downloader_and_factory(kwargs)
1068 try:
Joe Brennan1691f8e2017-03-15 15:53:36 -07001069 file_name = kwargs['file_name']
Dan Shi2f136862016-02-11 15:38:38 -08001070 artifacts = kwargs['artifacts']
1071 except KeyError:
Amin Hassanid4e35392019-10-03 11:02:44 -07001072 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -07001073 '`file_name` and `artifacts` are required to search '
1074 'for a file in build artifacts.')
Dan Shi2f136862016-02-11 15:38:38 -08001075 build_path = dl.GetBuildDir()
1076 for artifact in artifacts:
1077 # Get the unzipped folder of the artifact. If it's not defined in
1078 # ARTIFACT_UNZIP_FOLDER_MAP, assume the files are unzipped to the build
1079 # directory directly.
1080 folder = artifact_info.ARTIFACT_UNZIP_FOLDER_MAP.get(artifact, '')
1081 artifact_path = os.path.join(build_path, folder)
1082 for root, _, filenames in os.walk(artifact_path):
Joe Brennan1691f8e2017-03-15 15:53:36 -07001083 if file_name in set([f for f in filenames]):
Dan Shi2f136862016-02-11 15:38:38 -08001084 return os.path.relpath(os.path.join(root, file_name), build_path)
Amin Hassanid4e35392019-10-03 11:02:44 -07001085 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -07001086 'File `%s` can not be found in artifacts: %s' % (file_name, artifacts))
Dan Shi2f136862016-02-11 15:38:38 -08001087
1088 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -08001089 def setup_telemetry(self, **kwargs):
1090 """Extracts and sets up telemetry
1091
1092 This method goes through the telemetry deps packages, and stages them on
1093 the devserver to be used by the drones and the telemetry tests.
1094
1095 Args:
1096 archive_url: Google Storage URL for the build.
1097
1098 Returns:
1099 Path to the source folder for the telemetry codebase once it is staged.
1100 """
Gabe Black3b567202015-09-23 14:07:59 -07001101 dl = _get_downloader(kwargs)
Simran Basi4baad082013-02-14 13:39:18 -08001102
Gabe Black3b567202015-09-23 14:07:59 -07001103 build_path = dl.GetBuildDir()
Simran Basi4baad082013-02-14 13:39:18 -08001104 deps_path = os.path.join(build_path, 'autotest/packages')
1105 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
1106 src_folder = os.path.join(telemetry_path, 'src')
1107
1108 with self._telemetry_lock_dict.lock(telemetry_path):
1109 if os.path.exists(src_folder):
1110 # Telemetry is already fully stage return
1111 return src_folder
1112
1113 common_util.MkDirP(telemetry_path)
1114
1115 # Copy over the required deps tar balls to the telemetry directory.
1116 for dep in TELEMETRY_DEPS:
1117 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -07001118 if not os.path.exists(dep_path):
1119 # This dep does not exist (could be new), do not extract it.
1120 continue
Simran Basi4baad082013-02-14 13:39:18 -08001121 try:
1122 common_util.ExtractTarball(dep_path, telemetry_path)
1123 except common_util.CommonUtilError as e:
1124 shutil.rmtree(telemetry_path)
Amin Hassanid4e35392019-10-03 11:02:44 -07001125 raise DevServerError(str(e))
Simran Basi4baad082013-02-14 13:39:18 -08001126
1127 # By default all the tarballs extract to test_src but some parts of
1128 # the telemetry code specifically hardcoded to exist inside of 'src'.
1129 test_src = os.path.join(telemetry_path, 'test_src')
1130 try:
1131 shutil.move(test_src, src_folder)
1132 except shutil.Error:
1133 # This can occur if src_folder already exists. Remove and retry move.
1134 shutil.rmtree(src_folder)
Amin Hassanid4e35392019-10-03 11:02:44 -07001135 raise DevServerError(
Gabe Black3b567202015-09-23 14:07:59 -07001136 'Failure in telemetry setup for build %s. Appears that the '
1137 'test_src to src move failed.' % dl.GetBuild())
Simran Basi4baad082013-02-14 13:39:18 -08001138
1139 return src_folder
1140
1141 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -08001142 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -07001143 """Symbolicates a minidump using pre-downloaded symbols, returns it.
1144
1145 Callers will need to POST to this URL with a body of MIME-type
1146 "multipart/form-data".
1147 The body should include a single argument, 'minidump', containing the
1148 binary-formatted minidump to symbolicate.
1149
Chris Masone816e38c2012-05-02 12:22:36 -07001150 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -08001151 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -07001152 minidump: The binary minidump file to symbolicate.
1153 """
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -08001154 if is_deprecated_server():
1155 raise DeprecatedRPCError('symbolicate_dump')
1156
Chris Sosa76e44b92013-01-31 12:11:38 -08001157 # Ensure the symbols have been staged.
Dan Shif08fe492016-10-04 14:39:25 -07001158 # Try debug.tar.xz first, then debug.tgz
1159 for artifact in (artifact_info.SYMBOLS_ONLY, artifact_info.SYMBOLS):
1160 kwargs['artifacts'] = artifact
1161 dl = _get_downloader(kwargs)
1162
1163 try:
1164 if self.stage(**kwargs) == 'Success':
1165 break
1166 except build_artifact.ArtifactDownloadError:
1167 continue
1168 else:
Amin Hassanid4e35392019-10-03 11:02:44 -07001169 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -07001170 'Failed to stage symbols for %s' % dl.DescribeSource())
Chris Sosa76e44b92013-01-31 12:11:38 -08001171
Chris Masone816e38c2012-05-02 12:22:36 -07001172 to_return = ''
1173 with tempfile.NamedTemporaryFile() as local:
1174 while True:
1175 data = minidump.file.read(8192)
1176 if not data:
1177 break
1178 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -08001179
Chris Masone816e38c2012-05-02 12:22:36 -07001180 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -08001181
Gabe Black3b567202015-09-23 14:07:59 -07001182 symbols_directory = os.path.join(dl.GetBuildDir(), 'debug', 'breakpad')
Chris Sosa76e44b92013-01-31 12:11:38 -08001183
xixuanab744382017-04-27 10:41:27 -07001184 # The location of minidump_stackwalk is defined in chromeos-admin.
Chris Sosa76e44b92013-01-31 12:11:38 -08001185 stackwalk = subprocess.Popen(
xixuanab744382017-04-27 10:41:27 -07001186 ['/usr/local/bin/minidump_stackwalk', local.name, symbols_directory],
Chris Sosa76e44b92013-01-31 12:11:38 -08001187 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1188
Chris Masone816e38c2012-05-02 12:22:36 -07001189 to_return, error_text = stackwalk.communicate()
1190 if stackwalk.returncode != 0:
Amin Hassanid4e35392019-10-03 11:02:44 -07001191 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -07001192 "Can't generate stack trace: %s (rc=%d)" % (error_text,
1193 stackwalk.returncode))
Chris Masone816e38c2012-05-02 12:22:36 -07001194
1195 return to_return
1196
1197 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -08001198 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 15:31:36 -04001199 """Return a string representing the latest build for a given target.
1200
1201 Args:
1202 target: The build target, typically a combination of the board and the
1203 type of build e.g. x86-mario-release.
1204 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
1205 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-07 18:21:26 -08001206
Scott Zawalski16954532012-03-20 15:31:36 -04001207 Returns:
1208 A string representation of the latest build if one exists, i.e.
1209 R19-1993.0.0-a1-b1480.
1210 An empty string if no latest could be found.
1211 """
Don Garrettf84631a2014-01-07 18:21:26 -08001212 if not kwargs:
Scott Zawalski16954532012-03-20 15:31:36 -04001213 return _PrintDocStringAsHTML(self.latestbuild)
1214
Don Garrettf84631a2014-01-07 18:21:26 -08001215 if 'target' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -08001216 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
1217 'Error: target= is required!')
Dan Shi61305df2015-10-26 16:52:35 -07001218
1219 if _is_android_build_request(kwargs):
1220 branch = kwargs.get('branch', None)
1221 target = kwargs.get('target', None)
1222 if not target or not branch:
Amin Hassanid4e35392019-10-03 11:02:44 -07001223 raise DevServerError('Both target and branch must be specified to query'
1224 ' for the latest Android build.')
Dan Shi61305df2015-10-26 16:52:35 -07001225 return android_build.BuildAccessor.GetLatestBuildID(target, branch)
1226
Scott Zawalski16954532012-03-20 15:31:36 -04001227 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001228 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-07 18:21:26 -08001229 updater.static_dir, kwargs['target'],
1230 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -07001231 except common_util.CommonUtilError as errmsg:
Amin Hassani722e0962019-11-15 15:45:31 -08001232 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
1233 str(errmsg))
Scott Zawalski16954532012-03-20 15:31:36 -04001234
1235 @cherrypy.expose
xixuan7efd0002016-04-14 15:34:01 -07001236 def list_suite_controls(self, **kwargs):
1237 """Return a list of contents of all known control files.
1238
1239 Example URL:
1240 To List all control files' content:
1241 http://dev-server/list_suite_controls?suite_name=bvt&
1242 build=daisy_spring-release/R29-4279.0.0
1243
1244 Args:
1245 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
1246 suite_name: List the control files belonging to that suite.
1247
1248 Returns:
Dan Shia1cd6522016-04-18 16:07:21 -07001249 A dictionary of all control files's path to its content for given suite.
xixuan7efd0002016-04-14 15:34:01 -07001250 """
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -08001251 if is_deprecated_server():
1252 raise DeprecatedRPCError('list_suite_controls')
1253
xixuan7efd0002016-04-14 15:34:01 -07001254 if not kwargs:
1255 return _PrintDocStringAsHTML(self.controlfiles)
1256
1257 if 'build' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -08001258 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
1259 'Error: build= is required!')
xixuan7efd0002016-04-14 15:34:01 -07001260
1261 if 'suite_name' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -08001262 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
1263 'Error: suite_name= is required!')
xixuan7efd0002016-04-14 15:34:01 -07001264
1265 control_file_list = [
1266 line.rstrip() for line in common_util.GetControlFileListForSuite(
1267 updater.static_dir, kwargs['build'],
1268 kwargs['suite_name']).splitlines()]
1269
Dan Shia1cd6522016-04-18 16:07:21 -07001270 control_file_content_dict = {}
xixuan7efd0002016-04-14 15:34:01 -07001271 for control_path in control_file_list:
Dan Shia1cd6522016-04-18 16:07:21 -07001272 control_file_content_dict[control_path] = (common_util.GetControlFile(
xixuan7efd0002016-04-14 15:34:01 -07001273 updater.static_dir, kwargs['build'], control_path))
1274
Dan Shia1cd6522016-04-18 16:07:21 -07001275 return json.dumps(control_file_content_dict)
xixuan7efd0002016-04-14 15:34:01 -07001276
1277 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -08001278 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 17:17:28 -05001279 """Return a control file or a list of all known control files.
1280
1281 Example URL:
1282 To List all control files:
beepsbd337242013-07-09 22:44:06 -07001283 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
1284 To List all control files for, say, the bvt suite:
1285 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -05001286 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -05001287 http://dev-server/controlfiles?board=x86-alex-release&build=R18-1514.0.0&control_path=client/sleeptest/control
Scott Zawalski4647ce62012-01-03 17:17:28 -05001288
1289 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -05001290 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -05001291 control_path: If you want the contents of a control file set this
1292 to the path. E.g. client/site_tests/sleeptest/control
1293 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -07001294 suite_name: If control_path is not specified but a suite_name is
1295 specified, list the control files belonging to that suite instead of
1296 all control files. The empty string for suite_name will list all control
1297 files for the build.
Don Garrettf84631a2014-01-07 18:21:26 -08001298
Scott Zawalski4647ce62012-01-03 17:17:28 -05001299 Returns:
1300 Contents of a control file if control_path is provided.
1301 A list of control files if no control_path is provided.
1302 """
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -08001303 if is_deprecated_server():
1304 raise DeprecatedRPCError('controlfiles')
1305
Don Garrettf84631a2014-01-07 18:21:26 -08001306 if not kwargs:
Scott Zawalski4647ce62012-01-03 17:17:28 -05001307 return _PrintDocStringAsHTML(self.controlfiles)
1308
Don Garrettf84631a2014-01-07 18:21:26 -08001309 if 'build' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -08001310 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
1311 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -05001312
Don Garrettf84631a2014-01-07 18:21:26 -08001313 if 'control_path' not in kwargs:
1314 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-09 22:44:06 -07001315 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-07 18:21:26 -08001316 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-09 22:44:06 -07001317 else:
1318 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-07 18:21:26 -08001319 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -05001320 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001321 return common_util.GetControlFile(
Don Garrettf84631a2014-01-07 18:21:26 -08001322 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -08001323
1324 @cherrypy.expose
Simran Basi99e63c02014-05-20 10:39:52 -07001325 def xbuddy_translate(self, *args, **kwargs):
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001326 """Translates an xBuddy path to a real path to artifact if it exists.
1327
1328 Args:
Simran Basi99e63c02014-05-20 10:39:52 -07001329 args: An xbuddy path in the form of {local|remote}/build_id/artifact.
1330 Local searches the devserver's static directory. Remote searches a
1331 Google Storage image archive.
1332
1333 Kwargs:
1334 image_dir: Google Storage image archive to search in if requesting a
1335 remote artifact. If none uses the default bucket.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001336
1337 Returns:
Simran Basi99e63c02014-05-20 10:39:52 -07001338 String in the format of build_id/artifact as stored on the local server
1339 or in Google Storage.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001340 """
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -08001341 if is_deprecated_server():
1342 raise DeprecatedRPCError('xbuddy_translate')
1343
Simran Basi99e63c02014-05-20 10:39:52 -07001344 build_id, filename = self._xbuddy.Translate(
Gabe Black3b567202015-09-23 14:07:59 -07001345 args, image_dir=kwargs.get('image_dir'))
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001346 response = os.path.join(build_id, filename)
1347 _Log('Path translation requested, returning: %s', response)
1348 return response
1349
1350 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -07001351 def xbuddy(self, *args, **kwargs):
1352 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -07001353
1354 Args:
joycheneaf4cfc2013-07-02 08:38:57 -07001355 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -07001356 components of the path. The path can be understood as
1357 "{local|remote}/build_id/artifact" where build_id is composed of
1358 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -07001359
joychen121fc9b2013-08-02 14:30:30 -07001360 The first path element is optional, and can be "remote" or "local"
1361 If local (the default), devserver will not attempt to access Google
1362 Storage, and will only search the static directory for the files.
1363 If remote, devserver will try to obtain the artifact off GS if it's
1364 not found locally.
1365 The board is the familiar board name, optionally suffixed.
1366 The version can be the google storage version number, and may also be
1367 any of a number of xBuddy defined version aliases that will be
1368 translated into the latest built image that fits the description.
1369 Defaults to latest.
1370 The artifact is one of a number of image or artifact aliases used by
1371 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -07001372
1373 Kwargs:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001374 for_update: {true|false}
Amin Hassanie9ffb862019-09-25 17:10:40 -07001375 if true, prepares the update payloads for the image,
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001376 and returns the update uri to pass to the
1377 update_engine_client.
joychen3cb228e2013-06-12 12:13:13 -07001378 return_dir: {true|false}
1379 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001380 relative_path: {true|false}
1381 if set to true, returns the relative path to the payload
1382 directory from static_dir.
joychen3cb228e2013-06-12 12:13:13 -07001383 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -07001384 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -07001385 or
joycheneaf4cfc2013-07-02 08:38:57 -07001386 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -07001387
1388 Returns:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001389 If |for_update|, returns a redirect to the image or update file
1390 on the devserver. E.g.,
1391 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
1392 chromium-test-image.bin
1393 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
1394 http://host:port/static/x86-generic-release/R26-4000.0.0/
1395 If |relative_path| is true, return a relative path the folder where the
1396 payloads are. E.g.,
1397 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 12:13:13 -07001398 """
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -08001399 if is_deprecated_server():
1400 raise DeprecatedRPCError('xbuddy')
1401
Chris Sosa75490802013-09-30 17:21:45 -07001402 boolean_string = kwargs.get('for_update')
1403 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001404 boolean_string = kwargs.get('return_dir')
1405 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
1406 boolean_string = kwargs.get('relative_path')
1407 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -07001408
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001409 if return_dir and relative_path:
Amin Hassani722e0962019-11-15 15:45:31 -08001410 raise DevServerHTTPError(
Amin Hassanid4e35392019-10-03 11:02:44 -07001411 http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -07001412 'Cannot specify both return_dir and relative_path')
Chris Sosa75490802013-09-30 17:21:45 -07001413
1414 # For updates, we optimize downloading of test images.
1415 file_name = None
1416 build_id = None
1417 if for_update:
1418 try:
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001419 build_id = self._xbuddy.StageTestArtifactsForUpdate(args)
Chris Sosa75490802013-09-30 17:21:45 -07001420 except build_artifact.ArtifactDownloadError:
1421 build_id = None
1422
1423 if not build_id:
1424 build_id, file_name = self._xbuddy.Get(args)
1425
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001426 if for_update:
Amin Hassanie9ffb862019-09-25 17:10:40 -07001427 _Log('Payloads requested.')
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001428 # Forces payload to be in cache and symlinked into build_id dir.
Amin Hassanie9ffb862019-09-25 17:10:40 -07001429 updater.GetUpdateForLabel(build_id)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001430
1431 response = None
1432 if return_dir:
1433 response = os.path.join(cherrypy.request.base, 'static', build_id)
1434 _Log('Directory requested, returning: %s', response)
1435 elif relative_path:
1436 response = build_id
1437 _Log('Relative path requested, returning: %s', response)
1438 elif for_update:
1439 response = os.path.join(cherrypy.request.base, 'update', build_id)
1440 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 12:13:13 -07001441 else:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001442 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 14:30:30 -07001443 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001444 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 14:30:30 -07001445 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -07001446
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001447 return response
1448
joychen3cb228e2013-06-12 12:13:13 -07001449 @cherrypy.expose
1450 def xbuddy_list(self):
1451 """Lists the currently available images & time since last access.
1452
Gilad Arnold452fd272014-02-04 11:09:28 -08001453 Returns:
1454 A string representation of a list of tuples [(build_id, time since last
1455 access),...]
joychen3cb228e2013-06-12 12:13:13 -07001456 """
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -08001457 if is_deprecated_server():
1458 raise DeprecatedRPCError('xbuddy')
1459
joychen3cb228e2013-06-12 12:13:13 -07001460 return self._xbuddy.List()
1461
1462 @cherrypy.expose
1463 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 11:09:28 -08001464 """Returns the number of images cached by xBuddy."""
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -08001465 if is_deprecated_server():
1466 raise DeprecatedRPCError('xbuddy_capacity')
1467
joychen3cb228e2013-06-12 12:13:13 -07001468 return self._xbuddy.Capacity()
1469
1470 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001471 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001472 """Presents a welcome message and documentation links."""
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -08001473 if is_deprecated_server():
1474 raise DeprecatedRPCError('index')
1475
Congbin Guo6bc32182019-08-20 17:54:30 -07001476 html_template = (
1477 'Welcome to the Dev Server!<br>\n'
1478 '<br>\n'
1479 'Here are the available methods, click for documentation:<br>\n'
1480 '<br>\n'
1481 '%s')
1482
1483 exposed_methods = []
1484 for app in cherrypy.tree.apps.values():
1485 exposed_methods += _FindExposedMethods(
1486 app.root, app.script_name.lstrip('/'),
1487 unlisted=self._UNLISTED_METHODS)
1488
1489 return html_template % '<br>\n'.join(
1490 ['<a href=doc/%s>%s</a>' % (name, name)
1491 for name in sorted(exposed_methods)])
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001492
1493 @cherrypy.expose
1494 def doc(self, *args):
1495 """Shows the documentation for available methods / URLs.
1496
Amin Hassani08e42d22019-06-03 00:31:30 -07001497 Examples:
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001498 http://myhost/doc/update
1499 """
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -08001500 if is_deprecated_server():
1501 raise DeprecatedRPCError('doc')
1502
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001503 name = '/'.join(args)
Congbin Guo6bc32182019-08-20 17:54:30 -07001504 method = _GetExposedMethod(name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001505 if not method:
Amin Hassanid4e35392019-10-03 11:02:44 -07001506 raise DevServerError("No exposed method named `%s'" % name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001507 if not method.__doc__:
Amin Hassanid4e35392019-10-03 11:02:44 -07001508 raise DevServerError("No documentation for exposed method `%s'" % name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001509 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -07001510
Dale Curtisc9aaf3a2011-08-09 15:47:40 -07001511 @cherrypy.expose
Amin Hassani6eec8792020-01-09 14:06:48 -08001512 def update(self, *args, **kwargs):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001513 """Handles an update check from a Chrome OS client.
1514
1515 The HTTP request should contain the standard Omaha-style XML blob. The URL
1516 line may contain an additional intermediate path to the update payload.
1517
joychen121fc9b2013-08-02 14:30:30 -07001518 This request can be handled in one of 4 ways, depending on the devsever
1519 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -07001520
Amin Hassanie9ffb862019-09-25 17:10:40 -07001521 1. No intermediate path. DEPRECATED
joychen121fc9b2013-08-02 14:30:30 -07001522
1523 2. Path explicitly invokes XBuddy
1524 If there is a path given, it can explicitly invoke xbuddy by prefixing it
1525 with 'xbuddy'. This path is then used to acquire an image binary for the
1526 devserver to generate an update payload from. Devserver then serves this
1527 payload.
1528
1529 3. Path is left for the devserver to interpret.
1530 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
1531 to generate a payload from the test image in that directory and serve it.
1532
joychen121fc9b2013-08-02 14:30:30 -07001533 Examples:
joychen121fc9b2013-08-02 14:30:30 -07001534 2. Explicitly invoke xbuddy
1535 update_engine_client --omaha_url=
1536 http://myhost/update/xbuddy/remote/board/version/dev
1537 This would go to GS to download the dev image for the board, from which
1538 the devserver would generate a payload to serve.
1539
1540 3. Give a path for devserver to interpret
1541 update_engine_client --omaha_url=http://myhost/update/some/random/path
1542 This would attempt, in order to:
1543 a) Generate an update from a test image binary if found in
1544 static_dir/some/random/path.
1545 b) Serve an update payload found in static_dir/some/random/path.
1546 c) Hope that some/random/path takes the form "board/version" and
1547 and attempt to download an update payload for that board/version
1548 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001549 """
joychen121fc9b2013-08-02 14:30:30 -07001550 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -08001551 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -07001552 data = cherrypy.request.rfile.read(body_length)
Amin Hassani6aa075c2020-02-21 18:36:44 +00001553
Amin Hassani6eec8792020-01-09 14:06:48 -08001554 return updater.HandleUpdatePing(data, label, **kwargs)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001555
Dan Shif5ce2de2013-04-25 16:06:32 -07001556
Chris Sosadbc20082012-12-10 13:39:11 -08001557def _CleanCache(cache_dir, wipe):
1558 """Wipes any excess cached items in the cache_dir.
1559
1560 Args:
1561 cache_dir: the directory we are wiping from.
1562 wipe: If True, wipe all the contents -- not just the excess.
1563 """
1564 if wipe:
1565 # Clear the cache and exit on error.
1566 cmd = 'rm -rf %s/*' % cache_dir
1567 if os.system(cmd) != 0:
1568 _Log('Failed to clear the cache with %s' % cmd)
1569 sys.exit(1)
1570 else:
1571 # Clear all but the last N cached updates
1572 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
1573 (cache_dir, CACHED_ENTRIES))
1574 if os.system(cmd) != 0:
1575 _Log('Failed to clean up old delta cache files with %s' % cmd)
1576 sys.exit(1)
1577
1578
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001579def _AddTestingOptions(parser):
1580 group = optparse.OptionGroup(
1581 parser, 'Advanced Testing Options', 'These are used by test scripts and '
1582 'developers writing integration tests utilizing the devserver. They are '
1583 'not intended to be really used outside the scope of someone '
1584 'knowledgable about the test.')
1585 group.add_option('--exit',
1586 action='store_true',
Amin Hassanie9ffb862019-09-25 17:10:40 -07001587 help='do not start the server (yet clear cache)')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001588 group.add_option('--host_log',
1589 action='store_true', default=False,
1590 help='record history of host update events (/api/hostlog)')
1591 group.add_option('--max_updates',
Gabe Black3b567202015-09-23 14:07:59 -07001592 metavar='NUM', default=-1, type='int',
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001593 help='maximum number of update checks handled positively '
1594 '(default: unlimited)')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001595 group.add_option('--proxy_port',
1596 metavar='PORT', default=None, type='int',
1597 help='port to have the client connect to -- basically the '
1598 'devserver lies to the update to tell it to get the payload '
1599 'from a different port that will proxy the request back to '
1600 'the devserver. The proxy must be managed outside the '
1601 'devserver.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001602 parser.add_option_group(group)
1603
1604
1605def _AddUpdateOptions(parser):
1606 group = optparse.OptionGroup(
1607 parser, 'Autoupdate Options', 'These options can be used to change '
Amin Hassanie9ffb862019-09-25 17:10:40 -07001608 'how the devserver serve update payloads. Please '
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001609 'note that all of these option affect how a payload is generated and so '
1610 'do not work in archive-only mode.')
Amin Hassani6eec8792020-01-09 14:06:48 -08001611 # TODO(crbug/1004487): Deprecate critical_update.
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001612 group.add_option('--critical_update',
1613 action='store_true', default=False,
1614 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001615 group.add_option('--payload',
1616 metavar='PATH',
1617 help='use the update payload from specified directory '
1618 '(update.gz).')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001619 parser.add_option_group(group)
1620
1621
1622def _AddProductionOptions(parser):
1623 group = optparse.OptionGroup(
1624 parser, 'Advanced Server Options', 'These options can be used to changed '
1625 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001626 group.add_option('--clear_cache',
1627 action='store_true', default=False,
1628 help='At startup, removes all cached entries from the'
Amin Hassanid4e35392019-10-03 11:02:44 -07001629 "devserver's cache.")
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001630 group.add_option('--logfile',
1631 metavar='PATH',
1632 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -07001633 group.add_option('--pidfile',
1634 metavar='PATH',
1635 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 11:04:13 -08001636 group.add_option('--portfile',
1637 metavar='PATH',
1638 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001639 group.add_option('--production',
1640 action='store_true', default=False,
1641 help='have the devserver use production values when '
1642 'starting up. This includes using more threads and '
1643 'performing less logging.')
1644 parser.add_option_group(group)
1645
1646
Paul Hobbsef4e0702016-06-27 17:01:42 -07001647def MakeLogHandler(logfile):
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001648 """Create a LogHandler instance used to log all messages."""
1649 hdlr_cls = handlers.TimedRotatingFileHandler
1650 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
xixuan3d48bff2017-01-30 19:00:09 -08001651 interval=_LOG_ROTATION_INTERVAL,
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001652 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -07001653 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001654 return hdlr
1655
1656
Chris Sosacde6bf42012-05-31 18:36:39 -07001657def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001658 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -08001659 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -07001660
1661 # get directory that the devserver is run from
1662 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -07001663 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -07001664 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001665 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -07001666 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -07001667 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001668 parser.add_option('--port',
1669 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 13:13:28 -08001670 help=('port for the dev server to use; if zero, binds to '
1671 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001672 parser.add_option('-t', '--test_image',
1673 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -07001674 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -07001675 parser.add_option('-x', '--xbuddy_manage_builds',
1676 action='store_true',
1677 default=False,
1678 help='If set, allow xbuddy to manage images in'
1679 'build/images.')
Dan Shi72b16132015-10-08 12:10:33 -07001680 parser.add_option('-a', '--android_build_credential',
1681 default=None,
1682 help='Path to a json file which contains the credential '
1683 'needed to access Android builds.')
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -08001684 parser.add_option('--infra_removal',
1685 action='store_true', default=False,
1686 help='If option is present, some RPCs will be disabled to '
1687 'help with infra removal efforts. See '
1688 'go/devserver-deprecation')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001689 _AddProductionOptions(parser)
1690 _AddUpdateOptions(parser)
1691 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -07001692 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001693
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001694 # Handle options that must be set globally in cherrypy. Do this
1695 # work up front, because calls to _Log() below depend on this
1696 # initialization.
1697 if options.production:
1698 cherrypy.config.update({'environment': 'production'})
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -08001699 cherrypy.config.update({'infra_removal': options.infra_removal})
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001700 if not options.logfile:
1701 cherrypy.config.update({'log.screen': True})
1702 else:
1703 cherrypy.config.update({'log.error_file': '',
1704 'log.access_file': ''})
Paul Hobbsef4e0702016-06-27 17:01:42 -07001705 hdlr = MakeLogHandler(options.logfile)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001706 # Pylint can't seem to process these two calls properly
1707 # pylint: disable=E1101
1708 cherrypy.log.access_log.addHandler(hdlr)
1709 cherrypy.log.error_log.addHandler(hdlr)
1710 # pylint: enable=E1101
1711
joychened64b222013-06-21 16:39:34 -07001712 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001713 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001714
joychened64b222013-06-21 16:39:34 -07001715 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001716 # If our devserver is only supposed to serve payloads, we shouldn't be
1717 # mucking with the cache at all. If the devserver hadn't previously
1718 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001719 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001720 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001721 else:
1722 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001723
Chris Sosadbc20082012-12-10 13:39:11 -08001724 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 16:39:34 -07001725 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001726
Amin Hassanie9ffb862019-09-25 17:10:40 -07001727 _xbuddy = xbuddy.XBuddy(manage_builds=options.xbuddy_manage_builds,
joychen121fc9b2013-08-02 14:30:30 -07001728 static_dir=options.static_dir)
Chris Sosa75490802013-09-30 17:21:45 -07001729 if options.clear_cache and options.xbuddy_manage_builds:
1730 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 14:30:30 -07001731
Chris Sosa6a3697f2013-01-29 16:44:43 -08001732 # We allow global use here to share with cherrypy classes.
1733 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001734 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001735 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001736 _xbuddy,
joychened64b222013-06-21 16:39:34 -07001737 static_dir=options.static_dir,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001738 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001739 proxy_port=options.proxy_port,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001740 critical_update=options.critical_update,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001741 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001742 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001743 )
Chris Sosa7c931362010-10-11 19:49:01 -07001744
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001745 if options.exit:
1746 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001747
joychen3cb228e2013-06-12 12:13:13 -07001748 dev_server = DevServerRoot(_xbuddy)
Congbin Guo3afae6c2019-08-13 16:29:42 -07001749 health_checker_app = health_checker.Root(dev_server, options.static_dir)
joychen3cb228e2013-06-12 12:13:13 -07001750
Chris Sosa855b8932013-08-21 13:24:55 -07001751 if options.pidfile:
1752 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1753
Gilad Arnold11fbef42014-02-10 11:04:13 -08001754 if options.portfile:
1755 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1756
Dan Shiafd5c6c2016-01-07 10:27:03 -08001757 if (options.android_build_credential and
1758 os.path.exists(options.android_build_credential)):
1759 try:
1760 with open(options.android_build_credential) as f:
1761 android_build.BuildAccessor.credential_info = json.load(f)
1762 except ValueError as e:
1763 _Log('Failed to load the android build credential: %s. Error: %s.' %
1764 (options.android_build_credential, e))
Congbin Guo3afae6c2019-08-13 16:29:42 -07001765
1766 cherrypy.tree.mount(health_checker_app, '/check_health',
1767 config=health_checker.get_config())
joychen3cb228e2013-06-12 12:13:13 -07001768 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001769
1770
1771if __name__ == '__main__':
1772 main()