blob: acd4cdf323bbf5bf58c7cb551000b543b3ff2e9c [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
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -080032import socket
Chris Masone816e38c2012-05-02 12:22:36 -070033import subprocess
J. Richard Barnette3d977b82013-04-23 11:05:19 -070034import sys
Chris Masone816e38c2012-05-02 12:22:36 -070035import tempfile
Dan Shi59ae7092013-06-04 14:37:27 -070036import threading
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -070037import types
J. Richard Barnette3d977b82013-04-23 11:05:19 -070038from logging import handlers
39
Amin Hassanid4e35392019-10-03 11:02:44 -070040from six.moves import http_client
41
42# pylint: disable=no-name-in-module, import-error
J. Richard Barnette3d977b82013-04-23 11:05:19 -070043import cherrypy
Chris Sosa855b8932013-08-21 13:24:55 -070044from cherrypy import _cplogging as cplogging
Amin Hassanid4e35392019-10-03 11:02:44 -070045from cherrypy.process import plugins
46# pylint: enable=no-name-in-module, import-error
rtc@google.comded22402009-10-26 22:36:21 +000047
Achuith Bhandarkar662fb722019-10-31 16:12:49 -070048import autoupdate
49import cherrypy_ext
Achuith Bhandarkar662fb722019-10-31 16:12:49 -070050import health_checker
51
Richard Barnettedf35c322017-08-18 17:02:13 -070052# This must happen before any local modules get a chance to import
53# anything from chromite. Otherwise, really bad things will happen, and
54# you will _not_ understand why.
Congbin Guo3afae6c2019-08-13 16:29:42 -070055import setup_chromite # pylint: disable=unused-import
Achuith Bhandarkar662fb722019-10-31 16:12:49 -070056from chromite.lib.xbuddy import android_build
57from chromite.lib.xbuddy import artifact_info
58from chromite.lib.xbuddy import build_artifact
59from chromite.lib.xbuddy import cherrypy_log_util
60from chromite.lib.xbuddy import common_util
61from chromite.lib.xbuddy import devserver_constants
62from chromite.lib.xbuddy import downloader
63from chromite.lib.xbuddy import xbuddy
Gilad Arnoldc65330c2012-09-20 15:17:48 -070064
Gilad Arnoldc65330c2012-09-20 15:17:48 -070065# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080066def _Log(message, *args):
Achuith Bhandarkar662fb722019-10-31 16:12:49 -070067 return cherrypy_log_util.LogWithTag('DEVSERVER', message, *args)
Frank Farzan40160872011-12-12 18:39:18 -080068
Chris Sosa417e55d2011-01-25 16:40:48 -080069CACHED_ENTRIES = 12
Don Garrettf90edf02010-11-16 17:36:14 -080070
Simran Basi4baad082013-02-14 13:39:18 -080071TELEMETRY_FOLDER = 'telemetry_src'
72TELEMETRY_DEPS = ['dep-telemetry_dep.tar.bz2',
73 'dep-page_cycler_dep.tar.bz2',
Simran Basi0d078682013-03-22 16:40:04 -070074 'dep-chrome_test.tar.bz2',
75 'dep-perf_data_dep.tar.bz2']
Simran Basi4baad082013-02-14 13:39:18 -080076
Chris Sosa0356d3b2010-09-16 15:46:22 -070077# Sets up global to share between classes.
rtc@google.com21a5ca32009-11-04 18:23:23 +000078updater = None
rtc@google.comded22402009-10-26 22:36:21 +000079
xixuan3d48bff2017-01-30 19:00:09 -080080# Log rotation parameters. These settings correspond to twice a day once
81# devserver is started, with about two weeks (28 backup files) of old logs
82# kept for backup.
J. Richard Barnette3d977b82013-04-23 11:05:19 -070083#
xixuan3d48bff2017-01-30 19:00:09 -080084# For more, see the documentation in standard python library for
J. Richard Barnette3d977b82013-04-23 11:05:19 -070085# logging.handlers.TimedRotatingFileHandler
xixuan3d48bff2017-01-30 19:00:09 -080086_LOG_ROTATION_TIME = 'H'
Congbin Guo3afae6c2019-08-13 16:29:42 -070087_LOG_ROTATION_INTERVAL = 12 # hours
88_LOG_ROTATION_BACKUP = 28 # backup counts
Frank Farzan40160872011-12-12 18:39:18 -080089
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -080090# Error msg for deprecated RPC usage.
91DEPRECATED_RPC_ERROR_MSG = ('The %s RPC has been deprecated. Usage of this '
92 'RPC is discouraged. Please go to '
93 'go/devserver-deprecation for more information.')
94
xixuan52c2fba2016-05-20 17:02:48 -070095
Amin Hassanid4e35392019-10-03 11:02:44 -070096class DevServerError(Exception):
97 """Exception class used by DevServer."""
98
99
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -0800100class DeprecatedRPCError(DevServerError):
101 """Exception class used when an RPC is deprecated but is still being used."""
102
103 def __init__(self, rpc_name):
104 """Constructor for DeprecatedRPCError class.
105
106 :param rpc_name: (str) name of the RPC that has been deprecated.
107 """
Amin Hassani6ecda232020-03-09 19:03:23 -0700108 super(DeprecatedRPCError, self).__init__(
109 DEPRECATED_RPC_ERROR_MSG % rpc_name)
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -0800110 self.rpc_name = rpc_name
111
112
Amin Hassani722e0962019-11-15 15:45:31 -0800113class DevServerHTTPError(cherrypy.HTTPError):
114 """Exception class to log the HTTPResponse before routing it to cherrypy."""
115 def __init__(self, status, message):
116 """CherryPy error with logging.
117
118 Args:
119 status: HTTPResponse status.
120 message: Message associated with the response.
121 """
122 cherrypy.HTTPError.__init__(self, status, message)
123 _Log('HTTPError status: %s message: %s', status, message)
124
125
Gabe Black3b567202015-09-23 14:07:59 -0700126def _canonicalize_archive_url(archive_url):
127 """Canonicalizes archive_url strings.
128
129 Raises:
130 DevserverError: if archive_url is not set.
131 """
132 if archive_url:
133 if not archive_url.startswith('gs://'):
Amin Hassanid4e35392019-10-03 11:02:44 -0700134 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700135 "Archive URL isn't from Google Storage (%s) ." % archive_url)
Gabe Black3b567202015-09-23 14:07:59 -0700136
137 return archive_url.rstrip('/')
138 else:
Amin Hassanid4e35392019-10-03 11:02:44 -0700139 raise DevServerError('Must specify an archive_url in the request')
Gabe Black3b567202015-09-23 14:07:59 -0700140
141
142def _canonicalize_local_path(local_path):
143 """Canonicalizes |local_path| strings.
144
145 Raises:
146 DevserverError: if |local_path| is not set.
147 """
148 # Restrict staging of local content to only files within the static
149 # directory.
150 local_path = os.path.abspath(local_path)
151 if not local_path.startswith(updater.static_dir):
Amin Hassanid4e35392019-10-03 11:02:44 -0700152 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700153 'Local path %s must be a subdirectory of the static'
154 ' directory: %s' % (local_path, updater.static_dir))
Gabe Black3b567202015-09-23 14:07:59 -0700155
156 return local_path.rstrip('/')
157
158
159def _get_artifacts(kwargs):
160 """Returns a tuple of named and file artifacts given the stage rpc kwargs.
161
162 Raises:
163 DevserverError if no artifacts would be returned.
164 """
165 artifacts = kwargs.get('artifacts')
166 files = kwargs.get('files')
167 if not artifacts and not files:
Amin Hassanid4e35392019-10-03 11:02:44 -0700168 raise DevServerError('No artifacts specified.')
Gabe Black3b567202015-09-23 14:07:59 -0700169
170 # Note we NEED to coerce files to a string as we get raw unicode from
171 # cherrypy and we treat files as strings elsewhere in the code.
172 return (str(artifacts).split(',') if artifacts else [],
173 str(files).split(',') if files else [])
174
175
Dan Shi61305df2015-10-26 16:52:35 -0700176def _is_android_build_request(kwargs):
177 """Check if a devserver call is for Android build, based on the arguments.
178
179 This method exams the request's arguments (os_type) to determine if the
180 request is for Android build. If os_type is set to `android`, returns True.
181 If os_type is not set or has other values, returns False.
182
183 Args:
184 kwargs: Keyword arguments for the request.
185
186 Returns:
187 True if the request is for Android build. False otherwise.
188 """
189 os_type = kwargs.get('os_type', None)
190 return os_type == 'android'
191
192
Gabe Black3b567202015-09-23 14:07:59 -0700193def _get_downloader(kwargs):
194 """Returns the downloader based on passed in arguments.
195
196 Args:
Amin Hassani08e42d22019-06-03 00:31:30 -0700197 kwargs: Keyword arguments for the request.
Gabe Black3b567202015-09-23 14:07:59 -0700198 """
199 local_path = kwargs.get('local_path')
200 if local_path:
201 local_path = _canonicalize_local_path(local_path)
202
203 dl = None
204 if local_path:
Prathmesh Prabhu58d08932018-01-19 15:08:19 -0800205 delete_source = _parse_boolean_arg(kwargs, 'delete_source')
206 dl = downloader.LocalDownloader(updater.static_dir, local_path,
207 delete_source=delete_source)
Gabe Black3b567202015-09-23 14:07:59 -0700208
Dan Shi61305df2015-10-26 16:52:35 -0700209 if not _is_android_build_request(kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700210 archive_url = kwargs.get('archive_url')
211 if not archive_url and not local_path:
Amin Hassanid4e35392019-10-03 11:02:44 -0700212 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700213 'Requires archive_url or local_path to be specified.')
Gabe Black3b567202015-09-23 14:07:59 -0700214 if archive_url and local_path:
Amin Hassanid4e35392019-10-03 11:02:44 -0700215 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700216 'archive_url and local_path can not both be specified.')
Gabe Black3b567202015-09-23 14:07:59 -0700217 if not dl:
218 archive_url = _canonicalize_archive_url(archive_url)
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700219 dl = downloader.GoogleStorageDownloader(
220 updater.static_dir, archive_url,
221 downloader.GoogleStorageDownloader.GetBuildIdFromArchiveURL(
222 archive_url))
Gabe Black3b567202015-09-23 14:07:59 -0700223 elif not dl:
224 target = kwargs.get('target', None)
Dan Shi72b16132015-10-08 12:10:33 -0700225 branch = kwargs.get('branch', None)
Dan Shi61305df2015-10-26 16:52:35 -0700226 build_id = kwargs.get('build_id', None)
227 if not target or not branch or not build_id:
Amin Hassanid4e35392019-10-03 11:02:44 -0700228 raise DevServerError('target, branch, build ID must all be specified for '
229 'downloading Android build.')
Dan Shi72b16132015-10-08 12:10:33 -0700230 dl = downloader.AndroidBuildDownloader(updater.static_dir, branch, build_id,
231 target)
Gabe Black3b567202015-09-23 14:07:59 -0700232
233 return dl
234
235
236def _get_downloader_and_factory(kwargs):
237 """Returns the downloader and artifact factory based on passed in arguments.
238
239 Args:
Amin Hassani08e42d22019-06-03 00:31:30 -0700240 kwargs: Keyword arguments for the request.
Gabe Black3b567202015-09-23 14:07:59 -0700241 """
242 artifacts, files = _get_artifacts(kwargs)
243 dl = _get_downloader(kwargs)
244
Achuith Bhandarkar2a1fcd82019-10-17 17:45:58 -0700245 if (isinstance(dl, (downloader.GoogleStorageDownloader,
246 downloader.LocalDownloader))):
Gabe Black3b567202015-09-23 14:07:59 -0700247 factory_class = build_artifact.ChromeOSArtifactFactory
Dan Shi72b16132015-10-08 12:10:33 -0700248 elif isinstance(dl, downloader.AndroidBuildDownloader):
Gabe Black3b567202015-09-23 14:07:59 -0700249 factory_class = build_artifact.AndroidArtifactFactory
250 else:
Amin Hassanid4e35392019-10-03 11:02:44 -0700251 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700252 'Unrecognized value for downloader type: %s' % type(dl))
Gabe Black3b567202015-09-23 14:07:59 -0700253
254 factory = factory_class(dl.GetBuildDir(), artifacts, files, dl.GetBuild())
255
256 return dl, factory
257
258
Scott Zawalski4647ce62012-01-03 17:17:28 -0500259def _LeadingWhiteSpaceCount(string):
260 """Count the amount of leading whitespace in a string.
261
262 Args:
263 string: The string to count leading whitespace in.
Don Garrettf84631a2014-01-07 18:21:26 -0800264
Scott Zawalski4647ce62012-01-03 17:17:28 -0500265 Returns:
266 number of white space chars before characters start.
267 """
Gabe Black3b567202015-09-23 14:07:59 -0700268 matched = re.match(r'^\s+', string)
Scott Zawalski4647ce62012-01-03 17:17:28 -0500269 if matched:
270 return len(matched.group())
271
272 return 0
273
274
275def _PrintDocStringAsHTML(func):
276 """Make a functions docstring somewhat HTML style.
277
278 Args:
279 func: The function to return the docstring from.
Don Garrettf84631a2014-01-07 18:21:26 -0800280
Scott Zawalski4647ce62012-01-03 17:17:28 -0500281 Returns:
282 A string that is somewhat formated for a web browser.
283 """
284 # TODO(scottz): Make this parse Args/Returns in a prettier way.
285 # Arguments could be bolded and indented etc.
286 html_doc = []
287 for line in func.__doc__.splitlines():
288 leading_space = _LeadingWhiteSpaceCount(line)
289 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700290 line = ' ' * leading_space + line
Scott Zawalski4647ce62012-01-03 17:17:28 -0500291
292 html_doc.append('<BR>%s' % line)
293
294 return '\n'.join(html_doc)
295
296
Simran Basief83d6a2014-08-28 14:32:01 -0700297def _GetUpdateTimestampHandler(static_dir):
298 """Returns a handler to update directory staged.timestamp.
299
300 This handler resets the stage.timestamp whenever static content is accessed.
301
302 Args:
303 static_dir: Directory from which static content is being staged.
304
305 Returns:
Amin Hassani08e42d22019-06-03 00:31:30 -0700306 A cherrypy handler to update the timestamp of accessed content.
Simran Basief83d6a2014-08-28 14:32:01 -0700307 """
308 def UpdateTimestampHandler():
309 if not '404' in cherrypy.response.status:
310 build_match = re.match(devserver_constants.STAGED_BUILD_REGEX,
311 cherrypy.request.path_info)
312 if build_match:
313 build_dir = os.path.join(static_dir, build_match.group('build'))
314 downloader.Downloader.TouchTimestampForStaged(build_dir)
315 return UpdateTimestampHandler
316
317
Chris Sosa7c931362010-10-11 19:49:01 -0700318def _GetConfig(options):
319 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800320
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800321 socket_host = '::'
Yu-Ju Hongc8d4af32013-11-12 15:14:26 -0800322 # Fall back to IPv4 when python is not configured with IPv6.
323 if not socket.has_ipv6:
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800324 socket_host = '0.0.0.0'
325
Simran Basief83d6a2014-08-28 14:32:01 -0700326 # Adds the UpdateTimestampHandler to cherrypy's tools. This tools executes
327 # on the on_end_resource hook. This hook is called once processing is
328 # complete and the response is ready to be returned.
329 cherrypy.tools.update_timestamp = cherrypy.Tool(
330 'on_end_resource', _GetUpdateTimestampHandler(options.static_dir))
331
David Riley2fcb0122017-11-02 11:25:39 -0700332 base_config = {
333 'global': {
334 'server.log_request_headers': True,
335 'server.protocol_version': 'HTTP/1.1',
336 'server.socket_host': socket_host,
337 'server.socket_port': int(options.port),
338 'response.timeout': 6000,
339 'request.show_tracebacks': True,
340 'server.socket_timeout': 60,
341 'server.thread_pool': 2,
342 'engine.autoreload.on': False,
343 },
David Riley2fcb0122017-11-02 11:25:39 -0700344 '/build': {
345 'response.timeout': 100000,
346 },
347 '/update': {
348 # Gets rid of cherrypy parsing post file for args.
349 'request.process_request_body': False,
350 'response.timeout': 10000,
351 },
352 # Sets up the static dir for file hosting.
353 '/static': {
354 'tools.staticdir.dir': options.static_dir,
355 'tools.staticdir.on': True,
356 'response.timeout': 10000,
357 'tools.update_timestamp.on': True,
358 },
359 }
Chris Sosa5f118ef2012-07-12 11:37:50 -0700360 if options.production:
Alex Miller93beca52013-07-30 19:25:09 -0700361 base_config['global'].update({'server.thread_pool': 150})
Scott Zawalski1c5e7cd2012-02-27 13:12:52 -0500362
Chris Sosa7c931362010-10-11 19:49:01 -0700363 return base_config
rtc@google.com64244662009-11-12 00:52:08 +0000364
Darin Petkove17164a2010-08-11 13:24:41 -0700365
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700366def _GetRecursiveMemberObject(root, member_list):
367 """Returns an object corresponding to a nested member list.
368
369 Args:
370 root: the root object to search
371 member_list: list of nested members to search
Don Garrettf84631a2014-01-07 18:21:26 -0800372
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700373 Returns:
374 An object corresponding to the member name list; None otherwise.
375 """
376 for member in member_list:
377 next_root = root.__class__.__dict__.get(member)
378 if not next_root:
379 return None
380 root = next_root
381 return root
382
383
384def _IsExposed(name):
385 """Returns True iff |name| has an `exposed' attribute and it is set."""
386 return hasattr(name, 'exposed') and name.exposed
387
388
Congbin Guo6bc32182019-08-20 17:54:30 -0700389def _GetExposedMethod(nested_member):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700390 """Returns a CherryPy-exposed method, if such exists.
391
392 Args:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700393 nested_member: a slash-joined path to the nested member
Don Garrettf84631a2014-01-07 18:21:26 -0800394
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700395 Returns:
Congbin Guo6bc32182019-08-20 17:54:30 -0700396 A function object corresponding to the path defined by |nested_member| from
397 the app root object registered, if the function is exposed; None otherwise.
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700398 """
Congbin Guo6bc32182019-08-20 17:54:30 -0700399 for app in cherrypy.tree.apps.values():
400 # Use the 'index' function doc as the doc of the app.
401 if nested_member == app.script_name.lstrip('/'):
402 nested_member = 'index'
403
404 method = _GetRecursiveMemberObject(app.root, nested_member.split('/'))
405 if method and isinstance(method, types.FunctionType) and _IsExposed(method):
406 return method
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700407
408
Gilad Arnold748c8322012-10-12 09:51:35 -0700409def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700410 """Finds exposed CherryPy methods.
411
412 Args:
413 root: the root object for searching
414 prefix: slash-joined chain of members leading to current object
415 unlisted: URLs to be excluded regardless of their exposed status
Don Garrettf84631a2014-01-07 18:21:26 -0800416
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700417 Returns:
418 List of exposed URLs that are not unlisted.
419 """
420 method_list = []
Congbin Guo6bc32182019-08-20 17:54:30 -0700421 for member in root.__class__.__dict__.keys():
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700422 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 09:51:35 -0700423 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700424 continue
425 member_obj = root.__class__.__dict__[member]
426 if _IsExposed(member_obj):
Amin Hassani08e42d22019-06-03 00:31:30 -0700427 if isinstance(member_obj, types.FunctionType):
Congbin Guo6bc32182019-08-20 17:54:30 -0700428 # Regard the app name as exposed "method" name if it exposed 'index'
429 # function.
430 if prefix and member == 'index':
431 method_list.append(prefix)
432 else:
433 method_list.append(prefixed_member)
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700434 else:
435 method_list += _FindExposedMethods(
436 member_obj, prefixed_member, unlisted)
437 return method_list
438
439
xixuan52c2fba2016-05-20 17:02:48 -0700440def _parse_boolean_arg(kwargs, key):
xixuanac89ce82016-11-30 16:48:20 -0800441 """Parse boolean arg from kwargs.
442
443 Args:
444 kwargs: the parameters to be checked.
445 key: the key to be parsed.
446
447 Returns:
448 The boolean value of kwargs[key], or False if key doesn't exist in kwargs.
449
450 Raises:
451 DevServerHTTPError if kwargs[key] is not a boolean variable.
452 """
xixuan52c2fba2016-05-20 17:02:48 -0700453 if key in kwargs:
454 if kwargs[key] == 'True':
455 return True
456 elif kwargs[key] == 'False':
457 return False
458 else:
Amin Hassani722e0962019-11-15 15:45:31 -0800459 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
460 'The value for key %s is not boolean.' % key)
xixuan52c2fba2016-05-20 17:02:48 -0700461 else:
462 return False
463
xixuan447ad9d2017-02-28 14:46:20 -0800464
xixuanac89ce82016-11-30 16:48:20 -0800465def _parse_string_arg(kwargs, key):
466 """Parse string arg from kwargs.
467
468 Args:
469 kwargs: the parameters to be checked.
470 key: the key to be parsed.
471
472 Returns:
473 The string value of kwargs[key], or None if key doesn't exist in kwargs.
474 """
475 if key in kwargs:
476 return kwargs[key]
477 else:
478 return None
479
xixuan447ad9d2017-02-28 14:46:20 -0800480
xixuanac89ce82016-11-30 16:48:20 -0800481def _build_uri_from_build_name(build_name):
482 """Get build url from a given build name.
483
484 Args:
485 build_name: the build name to be parsed, whose format is
486 'board/release_version'.
487
488 Returns:
489 The release_archive_url on Google Storage for this build name.
490 """
Amin Hassani08e42d22019-06-03 00:31:30 -0700491 # TODO(ahassani): This function doesn't seem to be used anywhere since its
492 # previous use of lib.paygen.gspath was broken and it doesn't seem to be
493 # causing any runtime issues. So deprecate this in the future.
494 tokens = build_name.split('/')
495 return 'gs://chromeos-releases/stable-channel/%s/%s' % (tokens[0], tokens[1])
xixuan52c2fba2016-05-20 17:02:48 -0700496
xixuan447ad9d2017-02-28 14:46:20 -0800497
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -0800498def is_deprecated_server():
499 """Gets whether the devserver has deprecated RPCs."""
500 return cherrypy.config.get('infra_removal', False)
501
502
David Rochberg7c79a812011-01-19 14:24:45 -0500503class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700504 """The Root Class for the Dev Server.
505
506 CherryPy works as follows:
507 For each method in this class, cherrpy interprets root/path
508 as a call to an instance of DevServerRoot->method_name. For example,
509 a call to http://myhost/build will call build. CherryPy automatically
510 parses http args and places them as keyword arguments in each method.
511 For paths http://myhost/update/dir1/dir2, you can use *args so that
512 cherrypy uses the update method and puts the extra paths in args.
513 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700514 # Method names that should not be listed on the index page.
515 _UNLISTED_METHODS = ['index', 'doc']
516
Dan Shi59ae7092013-06-04 14:37:27 -0700517 # Number of threads that devserver is staging images.
518 _staging_thread_count = 0
519 # Lock used to lock increasing/decreasing count.
520 _staging_thread_count_lock = threading.Lock()
521
joychen3cb228e2013-06-12 12:13:13 -0700522 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700523 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800524 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 12:13:13 -0700525 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 14:24:45 -0500526
Congbin Guo3afae6c2019-08-13 16:29:42 -0700527 @property
528 def staging_thread_count(self):
529 """Get the staging thread count."""
530 return self._staging_thread_count
Dan Shiafd0e492015-05-27 14:23:51 -0700531
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700532 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500533 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700534 """Builds the package specified."""
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -0800535 if is_deprecated_server():
536 raise DeprecatedRPCError('build')
537
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700538 import builder
539 if self._builder is None:
540 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500541 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700542
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700543 @cherrypy.expose
Dan Shif8eb0d12013-08-01 17:52:06 -0700544 def is_staged(self, **kwargs):
545 """Check if artifacts have been downloaded.
546
Congbin Guo3afae6c2019-08-13 16:29:42 -0700547 Examples:
548 To check if autotest and test_suites are staged:
549 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
550 artifacts=autotest,test_suites
551
Amin Hassani08e42d22019-06-03 00:31:30 -0700552 Args:
Chris Sosa6b0c6172013-08-05 17:01:33 -0700553 async: True to return without waiting for download to complete.
554 artifacts: Comma separated list of named artifacts to download.
555 These are defined in artifact_info and have their implementation
556 in build_artifact.py.
557 files: Comma separated list of file artifacts to stage. These
558 will be available as is in the corresponding static directory with no
559 custom post-processing.
560
Congbin Guo3afae6c2019-08-13 16:29:42 -0700561 Returns:
562 True of all artifacts are staged.
Dan Shif8eb0d12013-08-01 17:52:06 -0700563 """
Gabe Black3b567202015-09-23 14:07:59 -0700564 dl, factory = _get_downloader_and_factory(kwargs)
Aviv Keshet57d18172016-06-18 20:39:09 -0700565 response = str(dl.IsStaged(factory))
566 _Log('Responding to is_staged %s request with %r', kwargs, response)
567 return response
Dan Shi59ae7092013-06-04 14:37:27 -0700568
Chris Sosa76e44b92013-01-31 12:11:38 -0800569 @cherrypy.expose
Prashanth Ba06d2d22014-03-07 15:35:19 -0800570 def list_image_dir(self, **kwargs):
571 """Take an archive url and list the contents in its staged directory.
572
Amin Hassani08e42d22019-06-03 00:31:30 -0700573 Examples:
Prashanth Ba06d2d22014-03-07 15:35:19 -0800574 To list the contents of where this devserver should have staged
575 gs://image-archive/<board>-release/<build> call:
576 http://devserver_url:<port>/list_image_dir?archive_url=<gs://..>
577
Congbin Guo3afae6c2019-08-13 16:29:42 -0700578 Args:
579 archive_url: Google Storage URL for the build.
580
Prashanth Ba06d2d22014-03-07 15:35:19 -0800581 Returns:
582 A string with information about the contents of the image directory.
583 """
Gabe Black3b567202015-09-23 14:07:59 -0700584 dl = _get_downloader(kwargs)
Prashanth Ba06d2d22014-03-07 15:35:19 -0800585 try:
Gabe Black3b567202015-09-23 14:07:59 -0700586 image_dir_contents = dl.ListBuildDir()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800587 except build_artifact.ArtifactDownloadError as e:
588 return 'Cannot list the contents of staged artifacts. %s' % e
589 if not image_dir_contents:
Gabe Black3b567202015-09-23 14:07:59 -0700590 return '%s has not been staged on this devserver.' % dl.DescribeSource()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800591 return image_dir_contents
592
593 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800594 def stage(self, **kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700595 """Downloads and caches build artifacts.
Chris Sosa76e44b92013-01-31 12:11:38 -0800596
Gabe Black3b567202015-09-23 14:07:59 -0700597 Downloads and caches build artifacts, possibly from a Google Storage URL,
Dan Shi72b16132015-10-08 12:10:33 -0700598 or from Android's build server. Returns once these have been downloaded
Gabe Black3b567202015-09-23 14:07:59 -0700599 on the devserver. A call to this will attempt to cache non-specified
600 artifacts in the background for the given from the given URL following
601 the principle of spatial locality. Spatial locality of different
Chris Sosa76e44b92013-01-31 12:11:38 -0800602 artifacts is explicitly defined in the build_artifact module.
603
604 These artifacts will then be available from the static/ sub-directory of
605 the devserver.
606
Amin Hassani08e42d22019-06-03 00:31:30 -0700607 Examples:
Chris Sosa76e44b92013-01-31 12:11:38 -0800608 To download the autotest and test suites tarballs:
609 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
610 artifacts=autotest,test_suites
611 To download the full update payload:
612 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
613 artifacts=full_payload
Chris Sosa6b0c6172013-08-05 17:01:33 -0700614 To download just a file called blah.bin:
615 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
616 files=blah.bin
Chris Sosa76e44b92013-01-31 12:11:38 -0800617
618 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 16:39:34 -0700619 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 12:11:38 -0800620
621 Note for this example, relative path is the archive_url stripped of its
622 basename i.e. path/ in the examples above. Specific example:
623
624 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
625
626 Will get staged to:
627
joychened64b222013-06-21 16:39:34 -0700628 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Congbin Guo3afae6c2019-08-13 16:29:42 -0700629
630 Args:
631 archive_url: Google Storage URL for the build.
632 local_path: Local path for the build.
633 delete_source: Only meaningful with local_path. bool to indicate if the
634 source files should be deleted. This is especially useful when staging
635 a file locally in resource constrained environments as it allows us to
636 move the relevant files locally instead of copying them.
637 async: True to return without waiting for download to complete.
638 artifacts: Comma separated list of named artifacts to download.
639 These are defined in artifact_info and have their implementation
640 in build_artifact.py.
641 files: Comma separated list of files to stage. These
642 will be available as is in the corresponding static directory with no
643 custom post-processing.
644 clean: True to remove any previously staged artifacts first.
Chris Sosa76e44b92013-01-31 12:11:38 -0800645 """
Gabe Black3b567202015-09-23 14:07:59 -0700646 dl, factory = _get_downloader_and_factory(kwargs)
647
Dan Shi59ae7092013-06-04 14:37:27 -0700648 with DevServerRoot._staging_thread_count_lock:
649 DevServerRoot._staging_thread_count += 1
650 try:
Laurence Goodbyf5c958d2016-01-14 18:23:56 -0800651 boolean_string = kwargs.get('clean')
652 clean = xbuddy.XBuddy.ParseBoolean(boolean_string)
653 if clean and os.path.exists(dl.GetBuildDir()):
654 _Log('Removing %s' % dl.GetBuildDir())
655 shutil.rmtree(dl.GetBuildDir())
Achuith Bhandarkar2a1fcd82019-10-17 17:45:58 -0700656 is_async = kwargs.get('async', False)
657 dl.Download(factory, is_async=is_async)
Dan Shi59ae7092013-06-04 14:37:27 -0700658 finally:
659 with DevServerRoot._staging_thread_count_lock:
660 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800661 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700662
663 @cherrypy.expose
Dan Shi2f136862016-02-11 15:38:38 -0800664 def locate_file(self, **kwargs):
665 """Get the path to the given file name.
666
667 This method looks up the given file name inside specified build artifacts.
668 One use case is to help caller to locate an apk file inside a build
669 artifact. The location of the apk file could be different based on the
670 branch and target.
671
672 Args:
673 file_name: Name of the file to look for.
674 artifacts: A list of artifact names to search for the file.
675
676 Returns:
677 Path to the file with the given name. It's relative to the folder for the
678 build, e.g., DATA/priv-app/sl4a/sl4a.apk
Dan Shi2f136862016-02-11 15:38:38 -0800679 """
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -0800680 if is_deprecated_server():
681 raise DeprecatedRPCError('locate_file')
682
Dan Shi2f136862016-02-11 15:38:38 -0800683 dl, _ = _get_downloader_and_factory(kwargs)
684 try:
Joe Brennan1691f8e2017-03-15 15:53:36 -0700685 file_name = kwargs['file_name']
Dan Shi2f136862016-02-11 15:38:38 -0800686 artifacts = kwargs['artifacts']
687 except KeyError:
Amin Hassanid4e35392019-10-03 11:02:44 -0700688 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700689 '`file_name` and `artifacts` are required to search '
690 'for a file in build artifacts.')
Dan Shi2f136862016-02-11 15:38:38 -0800691 build_path = dl.GetBuildDir()
692 for artifact in artifacts:
693 # Get the unzipped folder of the artifact. If it's not defined in
694 # ARTIFACT_UNZIP_FOLDER_MAP, assume the files are unzipped to the build
695 # directory directly.
696 folder = artifact_info.ARTIFACT_UNZIP_FOLDER_MAP.get(artifact, '')
697 artifact_path = os.path.join(build_path, folder)
698 for root, _, filenames in os.walk(artifact_path):
Joe Brennan1691f8e2017-03-15 15:53:36 -0700699 if file_name in set([f for f in filenames]):
Dan Shi2f136862016-02-11 15:38:38 -0800700 return os.path.relpath(os.path.join(root, file_name), build_path)
Amin Hassanid4e35392019-10-03 11:02:44 -0700701 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700702 'File `%s` can not be found in artifacts: %s' % (file_name, artifacts))
Dan Shi2f136862016-02-11 15:38:38 -0800703
704 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -0800705 def setup_telemetry(self, **kwargs):
706 """Extracts and sets up telemetry
707
708 This method goes through the telemetry deps packages, and stages them on
709 the devserver to be used by the drones and the telemetry tests.
710
711 Args:
712 archive_url: Google Storage URL for the build.
713
714 Returns:
715 Path to the source folder for the telemetry codebase once it is staged.
716 """
Gabe Black3b567202015-09-23 14:07:59 -0700717 dl = _get_downloader(kwargs)
Simran Basi4baad082013-02-14 13:39:18 -0800718
Gabe Black3b567202015-09-23 14:07:59 -0700719 build_path = dl.GetBuildDir()
Simran Basi4baad082013-02-14 13:39:18 -0800720 deps_path = os.path.join(build_path, 'autotest/packages')
721 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
722 src_folder = os.path.join(telemetry_path, 'src')
723
724 with self._telemetry_lock_dict.lock(telemetry_path):
725 if os.path.exists(src_folder):
726 # Telemetry is already fully stage return
727 return src_folder
728
729 common_util.MkDirP(telemetry_path)
730
731 # Copy over the required deps tar balls to the telemetry directory.
732 for dep in TELEMETRY_DEPS:
733 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -0700734 if not os.path.exists(dep_path):
735 # This dep does not exist (could be new), do not extract it.
736 continue
Simran Basi4baad082013-02-14 13:39:18 -0800737 try:
738 common_util.ExtractTarball(dep_path, telemetry_path)
739 except common_util.CommonUtilError as e:
740 shutil.rmtree(telemetry_path)
Amin Hassanid4e35392019-10-03 11:02:44 -0700741 raise DevServerError(str(e))
Simran Basi4baad082013-02-14 13:39:18 -0800742
743 # By default all the tarballs extract to test_src but some parts of
744 # the telemetry code specifically hardcoded to exist inside of 'src'.
745 test_src = os.path.join(telemetry_path, 'test_src')
746 try:
747 shutil.move(test_src, src_folder)
748 except shutil.Error:
749 # This can occur if src_folder already exists. Remove and retry move.
750 shutil.rmtree(src_folder)
Amin Hassanid4e35392019-10-03 11:02:44 -0700751 raise DevServerError(
Gabe Black3b567202015-09-23 14:07:59 -0700752 'Failure in telemetry setup for build %s. Appears that the '
753 'test_src to src move failed.' % dl.GetBuild())
Simran Basi4baad082013-02-14 13:39:18 -0800754
755 return src_folder
756
757 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800758 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -0700759 """Symbolicates a minidump using pre-downloaded symbols, returns it.
760
761 Callers will need to POST to this URL with a body of MIME-type
762 "multipart/form-data".
763 The body should include a single argument, 'minidump', containing the
764 binary-formatted minidump to symbolicate.
765
Chris Masone816e38c2012-05-02 12:22:36 -0700766 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -0800767 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -0700768 minidump: The binary minidump file to symbolicate.
769 """
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -0800770 if is_deprecated_server():
771 raise DeprecatedRPCError('symbolicate_dump')
772
Chris Sosa76e44b92013-01-31 12:11:38 -0800773 # Ensure the symbols have been staged.
Dan Shif08fe492016-10-04 14:39:25 -0700774 # Try debug.tar.xz first, then debug.tgz
775 for artifact in (artifact_info.SYMBOLS_ONLY, artifact_info.SYMBOLS):
776 kwargs['artifacts'] = artifact
777 dl = _get_downloader(kwargs)
778
779 try:
780 if self.stage(**kwargs) == 'Success':
781 break
782 except build_artifact.ArtifactDownloadError:
783 continue
784 else:
Amin Hassanid4e35392019-10-03 11:02:44 -0700785 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700786 'Failed to stage symbols for %s' % dl.DescribeSource())
Chris Sosa76e44b92013-01-31 12:11:38 -0800787
Chris Masone816e38c2012-05-02 12:22:36 -0700788 to_return = ''
789 with tempfile.NamedTemporaryFile() as local:
790 while True:
791 data = minidump.file.read(8192)
792 if not data:
793 break
794 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -0800795
Chris Masone816e38c2012-05-02 12:22:36 -0700796 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -0800797
Gabe Black3b567202015-09-23 14:07:59 -0700798 symbols_directory = os.path.join(dl.GetBuildDir(), 'debug', 'breakpad')
Chris Sosa76e44b92013-01-31 12:11:38 -0800799
xixuanab744382017-04-27 10:41:27 -0700800 # The location of minidump_stackwalk is defined in chromeos-admin.
Chris Sosa76e44b92013-01-31 12:11:38 -0800801 stackwalk = subprocess.Popen(
xixuanab744382017-04-27 10:41:27 -0700802 ['/usr/local/bin/minidump_stackwalk', local.name, symbols_directory],
Chris Sosa76e44b92013-01-31 12:11:38 -0800803 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
804
Chris Masone816e38c2012-05-02 12:22:36 -0700805 to_return, error_text = stackwalk.communicate()
806 if stackwalk.returncode != 0:
Amin Hassanid4e35392019-10-03 11:02:44 -0700807 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700808 "Can't generate stack trace: %s (rc=%d)" % (error_text,
809 stackwalk.returncode))
Chris Masone816e38c2012-05-02 12:22:36 -0700810
811 return to_return
812
813 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800814 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 15:31:36 -0400815 """Return a string representing the latest build for a given target.
816
817 Args:
818 target: The build target, typically a combination of the board and the
819 type of build e.g. x86-mario-release.
820 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
821 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-07 18:21:26 -0800822
Scott Zawalski16954532012-03-20 15:31:36 -0400823 Returns:
824 A string representation of the latest build if one exists, i.e.
825 R19-1993.0.0-a1-b1480.
826 An empty string if no latest could be found.
827 """
Sanika Kulkarni07d47ed2020-08-06 14:56:46 -0700828 if is_deprecated_server():
829 raise DeprecatedRPCError('latestbuild')
830
Don Garrettf84631a2014-01-07 18:21:26 -0800831 if not kwargs:
Scott Zawalski16954532012-03-20 15:31:36 -0400832 return _PrintDocStringAsHTML(self.latestbuild)
833
Don Garrettf84631a2014-01-07 18:21:26 -0800834 if 'target' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -0800835 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
836 'Error: target= is required!')
Dan Shi61305df2015-10-26 16:52:35 -0700837
838 if _is_android_build_request(kwargs):
839 branch = kwargs.get('branch', None)
840 target = kwargs.get('target', None)
841 if not target or not branch:
Amin Hassanid4e35392019-10-03 11:02:44 -0700842 raise DevServerError('Both target and branch must be specified to query'
843 ' for the latest Android build.')
Dan Shi61305df2015-10-26 16:52:35 -0700844 return android_build.BuildAccessor.GetLatestBuildID(target, branch)
845
Scott Zawalski16954532012-03-20 15:31:36 -0400846 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700847 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-07 18:21:26 -0800848 updater.static_dir, kwargs['target'],
849 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700850 except common_util.CommonUtilError as errmsg:
Amin Hassani722e0962019-11-15 15:45:31 -0800851 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
852 str(errmsg))
Scott Zawalski16954532012-03-20 15:31:36 -0400853
854 @cherrypy.expose
xixuan7efd0002016-04-14 15:34:01 -0700855 def list_suite_controls(self, **kwargs):
856 """Return a list of contents of all known control files.
857
858 Example URL:
859 To List all control files' content:
860 http://dev-server/list_suite_controls?suite_name=bvt&
861 build=daisy_spring-release/R29-4279.0.0
862
863 Args:
864 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
865 suite_name: List the control files belonging to that suite.
866
867 Returns:
Dan Shia1cd6522016-04-18 16:07:21 -0700868 A dictionary of all control files's path to its content for given suite.
xixuan7efd0002016-04-14 15:34:01 -0700869 """
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -0800870 if is_deprecated_server():
871 raise DeprecatedRPCError('list_suite_controls')
872
xixuan7efd0002016-04-14 15:34:01 -0700873 if not kwargs:
874 return _PrintDocStringAsHTML(self.controlfiles)
875
876 if 'build' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -0800877 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
878 'Error: build= is required!')
xixuan7efd0002016-04-14 15:34:01 -0700879
880 if 'suite_name' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -0800881 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
882 'Error: suite_name= is required!')
xixuan7efd0002016-04-14 15:34:01 -0700883
884 control_file_list = [
885 line.rstrip() for line in common_util.GetControlFileListForSuite(
886 updater.static_dir, kwargs['build'],
887 kwargs['suite_name']).splitlines()]
888
Dan Shia1cd6522016-04-18 16:07:21 -0700889 control_file_content_dict = {}
xixuan7efd0002016-04-14 15:34:01 -0700890 for control_path in control_file_list:
Dan Shia1cd6522016-04-18 16:07:21 -0700891 control_file_content_dict[control_path] = (common_util.GetControlFile(
xixuan7efd0002016-04-14 15:34:01 -0700892 updater.static_dir, kwargs['build'], control_path))
893
Dan Shia1cd6522016-04-18 16:07:21 -0700894 return json.dumps(control_file_content_dict)
xixuan7efd0002016-04-14 15:34:01 -0700895
896 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800897 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500898 """Return a control file or a list of all known control files.
899
900 Example URL:
901 To List all control files:
beepsbd337242013-07-09 22:44:06 -0700902 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
903 To List all control files for, say, the bvt suite:
904 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -0500905 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500906 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 -0500907
908 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500909 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500910 control_path: If you want the contents of a control file set this
911 to the path. E.g. client/site_tests/sleeptest/control
912 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -0700913 suite_name: If control_path is not specified but a suite_name is
914 specified, list the control files belonging to that suite instead of
915 all control files. The empty string for suite_name will list all control
916 files for the build.
Don Garrettf84631a2014-01-07 18:21:26 -0800917
Scott Zawalski4647ce62012-01-03 17:17:28 -0500918 Returns:
919 Contents of a control file if control_path is provided.
920 A list of control files if no control_path is provided.
921 """
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -0800922 if is_deprecated_server():
923 raise DeprecatedRPCError('controlfiles')
924
Don Garrettf84631a2014-01-07 18:21:26 -0800925 if not kwargs:
Scott Zawalski4647ce62012-01-03 17:17:28 -0500926 return _PrintDocStringAsHTML(self.controlfiles)
927
Don Garrettf84631a2014-01-07 18:21:26 -0800928 if 'build' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -0800929 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
930 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -0500931
Don Garrettf84631a2014-01-07 18:21:26 -0800932 if 'control_path' not in kwargs:
933 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-09 22:44:06 -0700934 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-07 18:21:26 -0800935 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-09 22:44:06 -0700936 else:
937 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-07 18:21:26 -0800938 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -0500939 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700940 return common_util.GetControlFile(
Don Garrettf84631a2014-01-07 18:21:26 -0800941 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -0800942
943 @cherrypy.expose
Simran Basi99e63c02014-05-20 10:39:52 -0700944 def xbuddy_translate(self, *args, **kwargs):
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700945 """Translates an xBuddy path to a real path to artifact if it exists.
946
947 Args:
Simran Basi99e63c02014-05-20 10:39:52 -0700948 args: An xbuddy path in the form of {local|remote}/build_id/artifact.
949 Local searches the devserver's static directory. Remote searches a
950 Google Storage image archive.
951
952 Kwargs:
953 image_dir: Google Storage image archive to search in if requesting a
954 remote artifact. If none uses the default bucket.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700955
956 Returns:
Simran Basi99e63c02014-05-20 10:39:52 -0700957 String in the format of build_id/artifact as stored on the local server
958 or in Google Storage.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700959 """
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -0800960 if is_deprecated_server():
961 raise DeprecatedRPCError('xbuddy_translate')
962
Simran Basi99e63c02014-05-20 10:39:52 -0700963 build_id, filename = self._xbuddy.Translate(
Gabe Black3b567202015-09-23 14:07:59 -0700964 args, image_dir=kwargs.get('image_dir'))
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700965 response = os.path.join(build_id, filename)
966 _Log('Path translation requested, returning: %s', response)
967 return response
968
969 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -0700970 def xbuddy(self, *args, **kwargs):
971 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -0700972
973 Args:
joycheneaf4cfc2013-07-02 08:38:57 -0700974 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -0700975 components of the path. The path can be understood as
976 "{local|remote}/build_id/artifact" where build_id is composed of
977 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -0700978
joychen121fc9b2013-08-02 14:30:30 -0700979 The first path element is optional, and can be "remote" or "local"
980 If local (the default), devserver will not attempt to access Google
981 Storage, and will only search the static directory for the files.
982 If remote, devserver will try to obtain the artifact off GS if it's
983 not found locally.
984 The board is the familiar board name, optionally suffixed.
985 The version can be the google storage version number, and may also be
986 any of a number of xBuddy defined version aliases that will be
987 translated into the latest built image that fits the description.
988 Defaults to latest.
989 The artifact is one of a number of image or artifact aliases used by
990 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -0700991
992 Kwargs:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800993 for_update: {true|false}
Amin Hassanie9ffb862019-09-25 17:10:40 -0700994 if true, prepares the update payloads for the image,
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800995 and returns the update uri to pass to the
996 update_engine_client.
joychen3cb228e2013-06-12 12:13:13 -0700997 return_dir: {true|false}
998 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800999 relative_path: {true|false}
1000 if set to true, returns the relative path to the payload
1001 directory from static_dir.
joychen3cb228e2013-06-12 12:13:13 -07001002 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -07001003 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -07001004 or
joycheneaf4cfc2013-07-02 08:38:57 -07001005 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -07001006
1007 Returns:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001008 If |for_update|, returns a redirect to the image or update file
1009 on the devserver. E.g.,
1010 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
1011 chromium-test-image.bin
1012 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
1013 http://host:port/static/x86-generic-release/R26-4000.0.0/
1014 If |relative_path| is true, return a relative path the folder where the
1015 payloads are. E.g.,
1016 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 12:13:13 -07001017 """
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -08001018 if is_deprecated_server():
1019 raise DeprecatedRPCError('xbuddy')
1020
Chris Sosa75490802013-09-30 17:21:45 -07001021 boolean_string = kwargs.get('for_update')
1022 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001023 boolean_string = kwargs.get('return_dir')
1024 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
1025 boolean_string = kwargs.get('relative_path')
1026 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -07001027
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001028 if return_dir and relative_path:
Amin Hassani722e0962019-11-15 15:45:31 -08001029 raise DevServerHTTPError(
Amin Hassanid4e35392019-10-03 11:02:44 -07001030 http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -07001031 'Cannot specify both return_dir and relative_path')
Chris Sosa75490802013-09-30 17:21:45 -07001032
1033 # For updates, we optimize downloading of test images.
1034 file_name = None
1035 build_id = None
1036 if for_update:
1037 try:
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001038 build_id = self._xbuddy.StageTestArtifactsForUpdate(args)
Chris Sosa75490802013-09-30 17:21:45 -07001039 except build_artifact.ArtifactDownloadError:
1040 build_id = None
1041
1042 if not build_id:
1043 build_id, file_name = self._xbuddy.Get(args)
1044
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001045 if for_update:
Amin Hassanie9ffb862019-09-25 17:10:40 -07001046 _Log('Payloads requested.')
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001047 # Forces payload to be in cache and symlinked into build_id dir.
Amin Hassanie9ffb862019-09-25 17:10:40 -07001048 updater.GetUpdateForLabel(build_id)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001049
1050 response = None
1051 if return_dir:
1052 response = os.path.join(cherrypy.request.base, 'static', build_id)
1053 _Log('Directory requested, returning: %s', response)
1054 elif relative_path:
1055 response = build_id
1056 _Log('Relative path requested, returning: %s', response)
1057 elif for_update:
1058 response = os.path.join(cherrypy.request.base, 'update', build_id)
1059 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 12:13:13 -07001060 else:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001061 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 14:30:30 -07001062 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001063 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 14:30:30 -07001064 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -07001065
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001066 return response
1067
joychen3cb228e2013-06-12 12:13:13 -07001068 @cherrypy.expose
1069 def xbuddy_list(self):
1070 """Lists the currently available images & time since last access.
1071
Gilad Arnold452fd272014-02-04 11:09:28 -08001072 Returns:
1073 A string representation of a list of tuples [(build_id, time since last
1074 access),...]
joychen3cb228e2013-06-12 12:13:13 -07001075 """
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -08001076 if is_deprecated_server():
1077 raise DeprecatedRPCError('xbuddy')
1078
joychen3cb228e2013-06-12 12:13:13 -07001079 return self._xbuddy.List()
1080
1081 @cherrypy.expose
1082 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 11:09:28 -08001083 """Returns the number of images cached by xBuddy."""
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -08001084 if is_deprecated_server():
1085 raise DeprecatedRPCError('xbuddy_capacity')
1086
joychen3cb228e2013-06-12 12:13:13 -07001087 return self._xbuddy.Capacity()
1088
1089 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001090 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001091 """Presents a welcome message and documentation links."""
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -08001092 if is_deprecated_server():
1093 raise DeprecatedRPCError('index')
1094
Congbin Guo6bc32182019-08-20 17:54:30 -07001095 html_template = (
1096 'Welcome to the Dev Server!<br>\n'
1097 '<br>\n'
1098 'Here are the available methods, click for documentation:<br>\n'
1099 '<br>\n'
1100 '%s')
1101
1102 exposed_methods = []
1103 for app in cherrypy.tree.apps.values():
1104 exposed_methods += _FindExposedMethods(
1105 app.root, app.script_name.lstrip('/'),
1106 unlisted=self._UNLISTED_METHODS)
1107
1108 return html_template % '<br>\n'.join(
1109 ['<a href=doc/%s>%s</a>' % (name, name)
1110 for name in sorted(exposed_methods)])
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001111
1112 @cherrypy.expose
1113 def doc(self, *args):
1114 """Shows the documentation for available methods / URLs.
1115
Amin Hassani08e42d22019-06-03 00:31:30 -07001116 Examples:
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001117 http://myhost/doc/update
1118 """
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -08001119 if is_deprecated_server():
1120 raise DeprecatedRPCError('doc')
1121
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001122 name = '/'.join(args)
Congbin Guo6bc32182019-08-20 17:54:30 -07001123 method = _GetExposedMethod(name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001124 if not method:
Amin Hassanid4e35392019-10-03 11:02:44 -07001125 raise DevServerError("No exposed method named `%s'" % name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001126 if not method.__doc__:
Amin Hassanid4e35392019-10-03 11:02:44 -07001127 raise DevServerError("No documentation for exposed method `%s'" % name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001128 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -07001129
Dale Curtisc9aaf3a2011-08-09 15:47:40 -07001130 @cherrypy.expose
Amin Hassani6eec8792020-01-09 14:06:48 -08001131 def update(self, *args, **kwargs):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001132 """Handles an update check from a Chrome OS client.
1133
1134 The HTTP request should contain the standard Omaha-style XML blob. The URL
1135 line may contain an additional intermediate path to the update payload.
1136
joychen121fc9b2013-08-02 14:30:30 -07001137 This request can be handled in one of 4 ways, depending on the devsever
1138 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -07001139
Amin Hassanie9ffb862019-09-25 17:10:40 -07001140 1. No intermediate path. DEPRECATED
joychen121fc9b2013-08-02 14:30:30 -07001141
1142 2. Path explicitly invokes XBuddy
1143 If there is a path given, it can explicitly invoke xbuddy by prefixing it
1144 with 'xbuddy'. This path is then used to acquire an image binary for the
1145 devserver to generate an update payload from. Devserver then serves this
1146 payload.
1147
1148 3. Path is left for the devserver to interpret.
1149 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
1150 to generate a payload from the test image in that directory and serve it.
1151
joychen121fc9b2013-08-02 14:30:30 -07001152 Examples:
joychen121fc9b2013-08-02 14:30:30 -07001153 2. Explicitly invoke xbuddy
1154 update_engine_client --omaha_url=
1155 http://myhost/update/xbuddy/remote/board/version/dev
1156 This would go to GS to download the dev image for the board, from which
1157 the devserver would generate a payload to serve.
1158
1159 3. Give a path for devserver to interpret
1160 update_engine_client --omaha_url=http://myhost/update/some/random/path
1161 This would attempt, in order to:
1162 a) Generate an update from a test image binary if found in
1163 static_dir/some/random/path.
1164 b) Serve an update payload found in static_dir/some/random/path.
1165 c) Hope that some/random/path takes the form "board/version" and
1166 and attempt to download an update payload for that board/version
1167 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001168 """
joychen121fc9b2013-08-02 14:30:30 -07001169 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -08001170 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -07001171 data = cherrypy.request.rfile.read(body_length)
Amin Hassani6aa075c2020-02-21 18:36:44 +00001172
Amin Hassani6eec8792020-01-09 14:06:48 -08001173 return updater.HandleUpdatePing(data, label, **kwargs)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001174
Dan Shif5ce2de2013-04-25 16:06:32 -07001175
Chris Sosadbc20082012-12-10 13:39:11 -08001176def _CleanCache(cache_dir, wipe):
1177 """Wipes any excess cached items in the cache_dir.
1178
1179 Args:
1180 cache_dir: the directory we are wiping from.
1181 wipe: If True, wipe all the contents -- not just the excess.
1182 """
1183 if wipe:
1184 # Clear the cache and exit on error.
1185 cmd = 'rm -rf %s/*' % cache_dir
1186 if os.system(cmd) != 0:
1187 _Log('Failed to clear the cache with %s' % cmd)
1188 sys.exit(1)
1189 else:
1190 # Clear all but the last N cached updates
1191 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
1192 (cache_dir, CACHED_ENTRIES))
1193 if os.system(cmd) != 0:
1194 _Log('Failed to clean up old delta cache files with %s' % cmd)
1195 sys.exit(1)
1196
1197
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001198def _AddTestingOptions(parser):
1199 group = optparse.OptionGroup(
1200 parser, 'Advanced Testing Options', 'These are used by test scripts and '
1201 'developers writing integration tests utilizing the devserver. They are '
1202 'not intended to be really used outside the scope of someone '
1203 'knowledgable about the test.')
1204 group.add_option('--exit',
1205 action='store_true',
Amin Hassanie9ffb862019-09-25 17:10:40 -07001206 help='do not start the server (yet clear cache)')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001207 parser.add_option_group(group)
1208
1209
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001210def _AddProductionOptions(parser):
1211 group = optparse.OptionGroup(
1212 parser, 'Advanced Server Options', 'These options can be used to changed '
1213 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001214 group.add_option('--clear_cache',
1215 action='store_true', default=False,
1216 help='At startup, removes all cached entries from the'
Amin Hassanid4e35392019-10-03 11:02:44 -07001217 "devserver's cache.")
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001218 group.add_option('--logfile',
1219 metavar='PATH',
1220 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -07001221 group.add_option('--pidfile',
1222 metavar='PATH',
1223 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 11:04:13 -08001224 group.add_option('--portfile',
1225 metavar='PATH',
1226 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001227 group.add_option('--production',
1228 action='store_true', default=False,
1229 help='have the devserver use production values when '
1230 'starting up. This includes using more threads and '
1231 'performing less logging.')
1232 parser.add_option_group(group)
1233
1234
Paul Hobbsef4e0702016-06-27 17:01:42 -07001235def MakeLogHandler(logfile):
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001236 """Create a LogHandler instance used to log all messages."""
1237 hdlr_cls = handlers.TimedRotatingFileHandler
1238 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
xixuan3d48bff2017-01-30 19:00:09 -08001239 interval=_LOG_ROTATION_INTERVAL,
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001240 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -07001241 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001242 return hdlr
1243
1244
Chris Sosacde6bf42012-05-31 18:36:39 -07001245def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001246 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -08001247 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -07001248
1249 # get directory that the devserver is run from
1250 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -07001251 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -07001252 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001253 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -07001254 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -07001255 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001256 parser.add_option('--port',
1257 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 13:13:28 -08001258 help=('port for the dev server to use; if zero, binds to '
1259 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001260 parser.add_option('-t', '--test_image',
1261 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -07001262 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -07001263 parser.add_option('-x', '--xbuddy_manage_builds',
1264 action='store_true',
1265 default=False,
1266 help='If set, allow xbuddy to manage images in'
1267 'build/images.')
Dan Shi72b16132015-10-08 12:10:33 -07001268 parser.add_option('-a', '--android_build_credential',
1269 default=None,
1270 help='Path to a json file which contains the credential '
1271 'needed to access Android builds.')
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -08001272 parser.add_option('--infra_removal',
1273 action='store_true', default=False,
1274 help='If option is present, some RPCs will be disabled to '
1275 'help with infra removal efforts. See '
1276 'go/devserver-deprecation')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001277 _AddProductionOptions(parser)
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001278 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -07001279 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001280
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001281 # Handle options that must be set globally in cherrypy. Do this
1282 # work up front, because calls to _Log() below depend on this
1283 # initialization.
1284 if options.production:
1285 cherrypy.config.update({'environment': 'production'})
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -08001286 cherrypy.config.update({'infra_removal': options.infra_removal})
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001287 if not options.logfile:
1288 cherrypy.config.update({'log.screen': True})
1289 else:
1290 cherrypy.config.update({'log.error_file': '',
1291 'log.access_file': ''})
Paul Hobbsef4e0702016-06-27 17:01:42 -07001292 hdlr = MakeLogHandler(options.logfile)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001293 # Pylint can't seem to process these two calls properly
1294 # pylint: disable=E1101
1295 cherrypy.log.access_log.addHandler(hdlr)
1296 cherrypy.log.error_log.addHandler(hdlr)
1297 # pylint: enable=E1101
1298
joychened64b222013-06-21 16:39:34 -07001299 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001300 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001301
joychened64b222013-06-21 16:39:34 -07001302 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001303 # If our devserver is only supposed to serve payloads, we shouldn't be
1304 # mucking with the cache at all. If the devserver hadn't previously
1305 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001306 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001307 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001308 else:
1309 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001310
Amin Hassanief523622020-07-06 12:09:23 -07001311 pkgroot_dir = os.path.join(options.static_dir, 'pkgroot')
1312 common_util.SymlinkFile('/build', pkgroot_dir)
1313
Chris Sosadbc20082012-12-10 13:39:11 -08001314 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 16:39:34 -07001315 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001316
Amin Hassanie9ffb862019-09-25 17:10:40 -07001317 _xbuddy = xbuddy.XBuddy(manage_builds=options.xbuddy_manage_builds,
joychen121fc9b2013-08-02 14:30:30 -07001318 static_dir=options.static_dir)
Chris Sosa75490802013-09-30 17:21:45 -07001319 if options.clear_cache and options.xbuddy_manage_builds:
1320 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 14:30:30 -07001321
Chris Sosa6a3697f2013-01-29 16:44:43 -08001322 # We allow global use here to share with cherrypy classes.
1323 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001324 global updater
Amin Hassanie5612032020-03-25 14:50:42 -07001325 updater = autoupdate.Autoupdate(_xbuddy, static_dir=options.static_dir)
Chris Sosa7c931362010-10-11 19:49:01 -07001326
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001327 if options.exit:
1328 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001329
joychen3cb228e2013-06-12 12:13:13 -07001330 dev_server = DevServerRoot(_xbuddy)
Congbin Guo3afae6c2019-08-13 16:29:42 -07001331 health_checker_app = health_checker.Root(dev_server, options.static_dir)
joychen3cb228e2013-06-12 12:13:13 -07001332
Chris Sosa855b8932013-08-21 13:24:55 -07001333 if options.pidfile:
1334 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1335
Gilad Arnold11fbef42014-02-10 11:04:13 -08001336 if options.portfile:
1337 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1338
Dan Shiafd5c6c2016-01-07 10:27:03 -08001339 if (options.android_build_credential and
1340 os.path.exists(options.android_build_credential)):
1341 try:
1342 with open(options.android_build_credential) as f:
1343 android_build.BuildAccessor.credential_info = json.load(f)
1344 except ValueError as e:
1345 _Log('Failed to load the android build credential: %s. Error: %s.' %
1346 (options.android_build_credential, e))
Congbin Guo3afae6c2019-08-13 16:29:42 -07001347
1348 cherrypy.tree.mount(health_checker_app, '/check_health',
1349 config=health_checker.get_config())
joychen3cb228e2013-06-12 12:13:13 -07001350 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001351
1352
1353if __name__ == '__main__':
1354 main()