blob: 0d8aa07707a4619d6b33c38421c630cd7b158523 [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
Richard Barnettedf35c322017-08-18 17:02:13 -070049# This must happen before any local modules get a chance to import
50# anything from chromite. Otherwise, really bad things will happen, and
51# you will _not_ understand why.
Congbin Guo3afae6c2019-08-13 16:29:42 -070052import setup_chromite # pylint: disable=unused-import
Richard Barnettedf35c322017-08-18 17:02:13 -070053
Dan Shi2f136862016-02-11 15:38:38 -080054import artifact_info
Congbin Guo3afae6c2019-08-13 16:29:42 -070055import autoupdate
Chris Sosa75490802013-09-30 17:21:45 -070056import build_artifact
Gilad Arnold11fbef42014-02-10 11:04:13 -080057import cherrypy_ext
Gilad Arnoldc65330c2012-09-20 15:17:48 -070058import common_util
Simran Basief83d6a2014-08-28 14:32:01 -070059import devserver_constants
Chris Sosa47a7d4e2012-03-28 11:26:55 -070060import downloader
Congbin Guo3afae6c2019-08-13 16:29:42 -070061import health_checker
Gilad Arnoldc65330c2012-09-20 15:17:48 -070062import log_util
joychen3cb228e2013-06-12 12:13:13 -070063import 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):
67 return log_util.LogWithTag('DEVSERVER', message, *args)
Chris Sosa0356d3b2010-09-16 15:46:22 -070068
Dan Shi94dcbe82015-06-08 20:51:13 -070069
xixuanac89ce82016-11-30 16:48:20 -080070# Use try-except to skip unneccesary import for simple use case, eg. running
71# devserver on host.
72try:
73 import cros_update
xixuanac89ce82016-11-30 16:48:20 -080074except ImportError as e:
75 _Log('cros_update cannot be imported: %r', e)
76 cros_update = None
xixuana4f4e712017-05-08 15:17:54 -070077
78try:
79 import cros_update_progress
80except ImportError as e:
81 _Log('cros_update_progress cannot be imported: %r', e)
xixuanac89ce82016-11-30 16:48:20 -080082 cros_update_progress = None
83
xixuanac89ce82016-11-30 16:48:20 -080084try:
Dan Shi72b16132015-10-08 12:10:33 -070085 import android_build
Amin Hassanid4e35392019-10-03 11:02:44 -070086except ImportError:
Dan Shi72b16132015-10-08 12:10:33 -070087 # Ignore android_build import failure. This is to support devserver running
88 # inside a ChromeOS device triggered by cros flash. Most ChromeOS test images
89 # do not have google-api-python-client module and they don't need to support
90 # Android updating, therefore, ignore the import failure here.
Dan Shi72b16132015-10-08 12:10:33 -070091 android_build = None
Frank Farzan40160872011-12-12 18:39:18 -080092
Chris Sosa417e55d2011-01-25 16:40:48 -080093CACHED_ENTRIES = 12
Don Garrettf90edf02010-11-16 17:36:14 -080094
Simran Basi4baad082013-02-14 13:39:18 -080095TELEMETRY_FOLDER = 'telemetry_src'
96TELEMETRY_DEPS = ['dep-telemetry_dep.tar.bz2',
97 'dep-page_cycler_dep.tar.bz2',
Simran Basi0d078682013-03-22 16:40:04 -070098 'dep-chrome_test.tar.bz2',
99 'dep-perf_data_dep.tar.bz2']
Simran Basi4baad082013-02-14 13:39:18 -0800100
Chris Sosa0356d3b2010-09-16 15:46:22 -0700101# Sets up global to share between classes.
rtc@google.com21a5ca32009-11-04 18:23:23 +0000102updater = None
rtc@google.comded22402009-10-26 22:36:21 +0000103
xixuan3d48bff2017-01-30 19:00:09 -0800104# Log rotation parameters. These settings correspond to twice a day once
105# devserver is started, with about two weeks (28 backup files) of old logs
106# kept for backup.
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700107#
xixuan3d48bff2017-01-30 19:00:09 -0800108# For more, see the documentation in standard python library for
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700109# logging.handlers.TimedRotatingFileHandler
xixuan3d48bff2017-01-30 19:00:09 -0800110_LOG_ROTATION_TIME = 'H'
Congbin Guo3afae6c2019-08-13 16:29:42 -0700111_LOG_ROTATION_INTERVAL = 12 # hours
112_LOG_ROTATION_BACKUP = 28 # backup counts
Frank Farzan40160872011-12-12 18:39:18 -0800113
xixuan52c2fba2016-05-20 17:02:48 -0700114# Auto-update parameters
115
116# Error msg for missing key in CrOS auto-update.
Xixuan Wu32af9f12017-11-13 14:11:44 -0800117KEY_ERROR_MSG = 'Key Error in RPC: %s= is required'
xixuan52c2fba2016-05-20 17:02:48 -0700118
xixuan52c2fba2016-05-20 17:02:48 -0700119
Amin Hassanid4e35392019-10-03 11:02:44 -0700120class DevServerError(Exception):
121 """Exception class used by DevServer."""
122
123
Gabe Black3b567202015-09-23 14:07:59 -0700124def _canonicalize_archive_url(archive_url):
125 """Canonicalizes archive_url strings.
126
127 Raises:
128 DevserverError: if archive_url is not set.
129 """
130 if archive_url:
131 if not archive_url.startswith('gs://'):
Amin Hassanid4e35392019-10-03 11:02:44 -0700132 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700133 "Archive URL isn't from Google Storage (%s) ." % archive_url)
Gabe Black3b567202015-09-23 14:07:59 -0700134
135 return archive_url.rstrip('/')
136 else:
Amin Hassanid4e35392019-10-03 11:02:44 -0700137 raise DevServerError('Must specify an archive_url in the request')
Gabe Black3b567202015-09-23 14:07:59 -0700138
139
140def _canonicalize_local_path(local_path):
141 """Canonicalizes |local_path| strings.
142
143 Raises:
144 DevserverError: if |local_path| is not set.
145 """
146 # Restrict staging of local content to only files within the static
147 # directory.
148 local_path = os.path.abspath(local_path)
149 if not local_path.startswith(updater.static_dir):
Amin Hassanid4e35392019-10-03 11:02:44 -0700150 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700151 'Local path %s must be a subdirectory of the static'
152 ' directory: %s' % (local_path, updater.static_dir))
Gabe Black3b567202015-09-23 14:07:59 -0700153
154 return local_path.rstrip('/')
155
156
157def _get_artifacts(kwargs):
158 """Returns a tuple of named and file artifacts given the stage rpc kwargs.
159
160 Raises:
161 DevserverError if no artifacts would be returned.
162 """
163 artifacts = kwargs.get('artifacts')
164 files = kwargs.get('files')
165 if not artifacts and not files:
Amin Hassanid4e35392019-10-03 11:02:44 -0700166 raise DevServerError('No artifacts specified.')
Gabe Black3b567202015-09-23 14:07:59 -0700167
168 # Note we NEED to coerce files to a string as we get raw unicode from
169 # cherrypy and we treat files as strings elsewhere in the code.
170 return (str(artifacts).split(',') if artifacts else [],
171 str(files).split(',') if files else [])
172
173
Dan Shi61305df2015-10-26 16:52:35 -0700174def _is_android_build_request(kwargs):
175 """Check if a devserver call is for Android build, based on the arguments.
176
177 This method exams the request's arguments (os_type) to determine if the
178 request is for Android build. If os_type is set to `android`, returns True.
179 If os_type is not set or has other values, returns False.
180
181 Args:
182 kwargs: Keyword arguments for the request.
183
184 Returns:
185 True if the request is for Android build. False otherwise.
186 """
187 os_type = kwargs.get('os_type', None)
188 return os_type == 'android'
189
190
Gabe Black3b567202015-09-23 14:07:59 -0700191def _get_downloader(kwargs):
192 """Returns the downloader based on passed in arguments.
193
194 Args:
Amin Hassani08e42d22019-06-03 00:31:30 -0700195 kwargs: Keyword arguments for the request.
Gabe Black3b567202015-09-23 14:07:59 -0700196 """
197 local_path = kwargs.get('local_path')
198 if local_path:
199 local_path = _canonicalize_local_path(local_path)
200
201 dl = None
202 if local_path:
Prathmesh Prabhu58d08932018-01-19 15:08:19 -0800203 delete_source = _parse_boolean_arg(kwargs, 'delete_source')
204 dl = downloader.LocalDownloader(updater.static_dir, local_path,
205 delete_source=delete_source)
Gabe Black3b567202015-09-23 14:07:59 -0700206
Dan Shi61305df2015-10-26 16:52:35 -0700207 if not _is_android_build_request(kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700208 archive_url = kwargs.get('archive_url')
209 if not archive_url and not local_path:
Amin Hassanid4e35392019-10-03 11:02:44 -0700210 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700211 'Requires archive_url or local_path to be specified.')
Gabe Black3b567202015-09-23 14:07:59 -0700212 if archive_url and local_path:
Amin Hassanid4e35392019-10-03 11:02:44 -0700213 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700214 'archive_url and local_path can not both be specified.')
Gabe Black3b567202015-09-23 14:07:59 -0700215 if not dl:
216 archive_url = _canonicalize_archive_url(archive_url)
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700217 dl = downloader.GoogleStorageDownloader(
218 updater.static_dir, archive_url,
219 downloader.GoogleStorageDownloader.GetBuildIdFromArchiveURL(
220 archive_url))
Gabe Black3b567202015-09-23 14:07:59 -0700221 elif not dl:
222 target = kwargs.get('target', None)
Dan Shi72b16132015-10-08 12:10:33 -0700223 branch = kwargs.get('branch', None)
Dan Shi61305df2015-10-26 16:52:35 -0700224 build_id = kwargs.get('build_id', None)
225 if not target or not branch or not build_id:
Amin Hassanid4e35392019-10-03 11:02:44 -0700226 raise DevServerError('target, branch, build ID must all be specified for '
227 'downloading Android build.')
Dan Shi72b16132015-10-08 12:10:33 -0700228 dl = downloader.AndroidBuildDownloader(updater.static_dir, branch, build_id,
229 target)
Gabe Black3b567202015-09-23 14:07:59 -0700230
231 return dl
232
233
234def _get_downloader_and_factory(kwargs):
235 """Returns the downloader and artifact factory based on passed in arguments.
236
237 Args:
Amin Hassani08e42d22019-06-03 00:31:30 -0700238 kwargs: Keyword arguments for the request.
Gabe Black3b567202015-09-23 14:07:59 -0700239 """
240 artifacts, files = _get_artifacts(kwargs)
241 dl = _get_downloader(kwargs)
242
Achuith Bhandarkar2a1fcd82019-10-17 17:45:58 -0700243 if (isinstance(dl, (downloader.GoogleStorageDownloader,
244 downloader.LocalDownloader))):
Gabe Black3b567202015-09-23 14:07:59 -0700245 factory_class = build_artifact.ChromeOSArtifactFactory
Dan Shi72b16132015-10-08 12:10:33 -0700246 elif isinstance(dl, downloader.AndroidBuildDownloader):
Gabe Black3b567202015-09-23 14:07:59 -0700247 factory_class = build_artifact.AndroidArtifactFactory
248 else:
Amin Hassanid4e35392019-10-03 11:02:44 -0700249 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700250 'Unrecognized value for downloader type: %s' % type(dl))
Gabe Black3b567202015-09-23 14:07:59 -0700251
252 factory = factory_class(dl.GetBuildDir(), artifacts, files, dl.GetBuild())
253
254 return dl, factory
255
256
Scott Zawalski4647ce62012-01-03 17:17:28 -0500257def _LeadingWhiteSpaceCount(string):
258 """Count the amount of leading whitespace in a string.
259
260 Args:
261 string: The string to count leading whitespace in.
Don Garrettf84631a2014-01-07 18:21:26 -0800262
Scott Zawalski4647ce62012-01-03 17:17:28 -0500263 Returns:
264 number of white space chars before characters start.
265 """
Gabe Black3b567202015-09-23 14:07:59 -0700266 matched = re.match(r'^\s+', string)
Scott Zawalski4647ce62012-01-03 17:17:28 -0500267 if matched:
268 return len(matched.group())
269
270 return 0
271
272
273def _PrintDocStringAsHTML(func):
274 """Make a functions docstring somewhat HTML style.
275
276 Args:
277 func: The function to return the docstring from.
Don Garrettf84631a2014-01-07 18:21:26 -0800278
Scott Zawalski4647ce62012-01-03 17:17:28 -0500279 Returns:
280 A string that is somewhat formated for a web browser.
281 """
282 # TODO(scottz): Make this parse Args/Returns in a prettier way.
283 # Arguments could be bolded and indented etc.
284 html_doc = []
285 for line in func.__doc__.splitlines():
286 leading_space = _LeadingWhiteSpaceCount(line)
287 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700288 line = ' ' * leading_space + line
Scott Zawalski4647ce62012-01-03 17:17:28 -0500289
290 html_doc.append('<BR>%s' % line)
291
292 return '\n'.join(html_doc)
293
294
Simran Basief83d6a2014-08-28 14:32:01 -0700295def _GetUpdateTimestampHandler(static_dir):
296 """Returns a handler to update directory staged.timestamp.
297
298 This handler resets the stage.timestamp whenever static content is accessed.
299
300 Args:
301 static_dir: Directory from which static content is being staged.
302
303 Returns:
Amin Hassani08e42d22019-06-03 00:31:30 -0700304 A cherrypy handler to update the timestamp of accessed content.
Simran Basief83d6a2014-08-28 14:32:01 -0700305 """
306 def UpdateTimestampHandler():
307 if not '404' in cherrypy.response.status:
308 build_match = re.match(devserver_constants.STAGED_BUILD_REGEX,
309 cherrypy.request.path_info)
310 if build_match:
311 build_dir = os.path.join(static_dir, build_match.group('build'))
312 downloader.Downloader.TouchTimestampForStaged(build_dir)
313 return UpdateTimestampHandler
314
315
Chris Sosa7c931362010-10-11 19:49:01 -0700316def _GetConfig(options):
317 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800318
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800319 socket_host = '::'
Yu-Ju Hongc8d4af32013-11-12 15:14:26 -0800320 # Fall back to IPv4 when python is not configured with IPv6.
321 if not socket.has_ipv6:
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800322 socket_host = '0.0.0.0'
323
Simran Basief83d6a2014-08-28 14:32:01 -0700324 # Adds the UpdateTimestampHandler to cherrypy's tools. This tools executes
325 # on the on_end_resource hook. This hook is called once processing is
326 # complete and the response is ready to be returned.
327 cherrypy.tools.update_timestamp = cherrypy.Tool(
328 'on_end_resource', _GetUpdateTimestampHandler(options.static_dir))
329
David Riley2fcb0122017-11-02 11:25:39 -0700330 base_config = {
331 'global': {
332 'server.log_request_headers': True,
333 'server.protocol_version': 'HTTP/1.1',
334 'server.socket_host': socket_host,
335 'server.socket_port': int(options.port),
336 'response.timeout': 6000,
337 'request.show_tracebacks': True,
338 'server.socket_timeout': 60,
339 'server.thread_pool': 2,
340 'engine.autoreload.on': False,
341 },
342 '/api': {
343 # Gets rid of cherrypy parsing post file for args.
344 'request.process_request_body': False,
345 },
346 '/build': {
347 'response.timeout': 100000,
348 },
349 '/update': {
350 # Gets rid of cherrypy parsing post file for args.
351 'request.process_request_body': False,
352 'response.timeout': 10000,
353 },
354 # Sets up the static dir for file hosting.
355 '/static': {
356 'tools.staticdir.dir': options.static_dir,
357 'tools.staticdir.on': True,
358 'response.timeout': 10000,
359 'tools.update_timestamp.on': True,
360 },
361 }
Chris Sosa5f118ef2012-07-12 11:37:50 -0700362 if options.production:
Alex Miller93beca52013-07-30 19:25:09 -0700363 base_config['global'].update({'server.thread_pool': 150})
Scott Zawalski1c5e7cd2012-02-27 13:12:52 -0500364
Chris Sosa7c931362010-10-11 19:49:01 -0700365 return base_config
rtc@google.com64244662009-11-12 00:52:08 +0000366
Darin Petkove17164a2010-08-11 13:24:41 -0700367
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700368def _GetRecursiveMemberObject(root, member_list):
369 """Returns an object corresponding to a nested member list.
370
371 Args:
372 root: the root object to search
373 member_list: list of nested members to search
Don Garrettf84631a2014-01-07 18:21:26 -0800374
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700375 Returns:
376 An object corresponding to the member name list; None otherwise.
377 """
378 for member in member_list:
379 next_root = root.__class__.__dict__.get(member)
380 if not next_root:
381 return None
382 root = next_root
383 return root
384
385
386def _IsExposed(name):
387 """Returns True iff |name| has an `exposed' attribute and it is set."""
388 return hasattr(name, 'exposed') and name.exposed
389
390
Congbin Guo6bc32182019-08-20 17:54:30 -0700391def _GetExposedMethod(nested_member):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700392 """Returns a CherryPy-exposed method, if such exists.
393
394 Args:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700395 nested_member: a slash-joined path to the nested member
Don Garrettf84631a2014-01-07 18:21:26 -0800396
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700397 Returns:
Congbin Guo6bc32182019-08-20 17:54:30 -0700398 A function object corresponding to the path defined by |nested_member| from
399 the app root object registered, if the function is exposed; None otherwise.
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700400 """
Congbin Guo6bc32182019-08-20 17:54:30 -0700401 for app in cherrypy.tree.apps.values():
402 # Use the 'index' function doc as the doc of the app.
403 if nested_member == app.script_name.lstrip('/'):
404 nested_member = 'index'
405
406 method = _GetRecursiveMemberObject(app.root, nested_member.split('/'))
407 if method and isinstance(method, types.FunctionType) and _IsExposed(method):
408 return method
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700409
410
Gilad Arnold748c8322012-10-12 09:51:35 -0700411def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700412 """Finds exposed CherryPy methods.
413
414 Args:
415 root: the root object for searching
416 prefix: slash-joined chain of members leading to current object
417 unlisted: URLs to be excluded regardless of their exposed status
Don Garrettf84631a2014-01-07 18:21:26 -0800418
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700419 Returns:
420 List of exposed URLs that are not unlisted.
421 """
422 method_list = []
Congbin Guo6bc32182019-08-20 17:54:30 -0700423 for member in root.__class__.__dict__.keys():
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700424 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 09:51:35 -0700425 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700426 continue
427 member_obj = root.__class__.__dict__[member]
428 if _IsExposed(member_obj):
Amin Hassani08e42d22019-06-03 00:31:30 -0700429 if isinstance(member_obj, types.FunctionType):
Congbin Guo6bc32182019-08-20 17:54:30 -0700430 # Regard the app name as exposed "method" name if it exposed 'index'
431 # function.
432 if prefix and member == 'index':
433 method_list.append(prefix)
434 else:
435 method_list.append(prefixed_member)
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700436 else:
437 method_list += _FindExposedMethods(
438 member_obj, prefixed_member, unlisted)
439 return method_list
440
441
xixuan52c2fba2016-05-20 17:02:48 -0700442def _check_base_args_for_auto_update(kwargs):
xixuanac89ce82016-11-30 16:48:20 -0800443 """Check basic args required for auto-update.
444
445 Args:
446 kwargs: the parameters to be checked.
447
448 Raises:
449 DevServerHTTPError if required parameters don't exist in kwargs.
450 """
xixuan52c2fba2016-05-20 17:02:48 -0700451 if 'host_name' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -0700452 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -0700453 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -0700454
455 if 'build_name' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -0700456 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -0700457 KEY_ERROR_MSG % 'build_name')
xixuan52c2fba2016-05-20 17:02:48 -0700458
459
460def _parse_boolean_arg(kwargs, key):
xixuanac89ce82016-11-30 16:48:20 -0800461 """Parse boolean arg from kwargs.
462
463 Args:
464 kwargs: the parameters to be checked.
465 key: the key to be parsed.
466
467 Returns:
468 The boolean value of kwargs[key], or False if key doesn't exist in kwargs.
469
470 Raises:
471 DevServerHTTPError if kwargs[key] is not a boolean variable.
472 """
xixuan52c2fba2016-05-20 17:02:48 -0700473 if key in kwargs:
474 if kwargs[key] == 'True':
475 return True
476 elif kwargs[key] == 'False':
477 return False
478 else:
479 raise common_util.DevServerHTTPError(
Amin Hassanid4e35392019-10-03 11:02:44 -0700480 http_client.INTERNAL_SERVER_ERROR,
xixuan52c2fba2016-05-20 17:02:48 -0700481 'The value for key %s is not boolean.' % key)
482 else:
483 return False
484
xixuan447ad9d2017-02-28 14:46:20 -0800485
xixuanac89ce82016-11-30 16:48:20 -0800486def _parse_string_arg(kwargs, key):
487 """Parse string arg from kwargs.
488
489 Args:
490 kwargs: the parameters to be checked.
491 key: the key to be parsed.
492
493 Returns:
494 The string value of kwargs[key], or None if key doesn't exist in kwargs.
495 """
496 if key in kwargs:
497 return kwargs[key]
498 else:
499 return None
500
xixuan447ad9d2017-02-28 14:46:20 -0800501
xixuanac89ce82016-11-30 16:48:20 -0800502def _build_uri_from_build_name(build_name):
503 """Get build url from a given build name.
504
505 Args:
506 build_name: the build name to be parsed, whose format is
507 'board/release_version'.
508
509 Returns:
510 The release_archive_url on Google Storage for this build name.
511 """
Amin Hassani08e42d22019-06-03 00:31:30 -0700512 # TODO(ahassani): This function doesn't seem to be used anywhere since its
513 # previous use of lib.paygen.gspath was broken and it doesn't seem to be
514 # causing any runtime issues. So deprecate this in the future.
515 tokens = build_name.split('/')
516 return 'gs://chromeos-releases/stable-channel/%s/%s' % (tokens[0], tokens[1])
xixuan52c2fba2016-05-20 17:02:48 -0700517
xixuan447ad9d2017-02-28 14:46:20 -0800518
519def _clear_process(host_name, pid):
520 """Clear AU process for given hostname and pid.
521
522 This clear includes:
523 1. kill process if it's alive.
524 2. delete the track status file of this process.
525 3. delete the executing log file of this process.
526
527 Args:
528 host_name: the host to execute auto-update.
529 pid: the background auto-update process id.
530 """
531 if cros_update_progress.IsProcessAlive(pid):
532 os.killpg(int(pid), signal.SIGKILL)
533
534 cros_update_progress.DelTrackStatusFile(host_name, pid)
535 cros_update_progress.DelExecuteLogFile(host_name, pid)
536
537
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700538class ApiRoot(object):
539 """RESTful API for Dev Server information."""
540 exposed = True
541
542 @cherrypy.expose
Gilad Arnold286a0062012-01-12 13:47:02 -0800543 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 11:36:27 -0700544 """Returns a JSON object containing a log of host event.
545
546 Args:
547 ip: address of host whose event log is requested, or `all'
Don Garrettf84631a2014-01-07 18:21:26 -0800548
Gilad Arnold1b908392012-10-05 11:36:27 -0700549 Returns:
Amin Hassani7c447852019-09-26 15:01:48 -0700550 A JSON dictionary containing all or some of the following fields:
Amin Hassanie7ead902019-10-11 16:42:43 -0700551 version: The Chromium OS version the device is running.
552 track: The channel the device is running on.
553 board: The device's board.
554 event_result: The event result of Omaha request.
555 event_type: The event type of Omaha request.
556 previous_version: The Chromium OS version we updated and rebooted from.
557 timestamp: The timestamp the event was received.
Amin Hassani7c447852019-09-26 15:01:48 -0700558 See the OmahaEvent class in update_engine/omaha_request_action.h for
559 event type and status code definitions. If the ip does not exist an empty
560 string is returned.
Gilad Arnold1b908392012-10-05 11:36:27 -0700561
562 Example URL:
563 http://myhost/api/hostlog?ip=192.168.1.5
564 """
Gilad Arnold286a0062012-01-12 13:47:02 -0800565 return updater.HandleHostLogPing(ip)
566
567 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800568 def fileinfo(self, *args):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700569 """Returns information about a given staged file.
570
571 Args:
Don Garrettf84631a2014-01-07 18:21:26 -0800572 args: path to the file inside the server's static staging directory
573
Gilad Arnold55a2a372012-10-02 09:46:32 -0700574 Returns:
575 A JSON encoded dictionary with information about the said file, which may
576 contain the following keys/values:
Gilad Arnold1b908392012-10-05 11:36:27 -0700577 size (int): the file size in bytes
Gilad Arnold1b908392012-10-05 11:36:27 -0700578 sha256 (string): a base64 encoded SHA256 hash
579
580 Example URL:
581 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 09:46:32 -0700582 """
Don Garrettf84631a2014-01-07 18:21:26 -0800583 file_path = os.path.join(updater.static_dir, *args)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700584 if not os.path.exists(file_path):
Amin Hassanid4e35392019-10-03 11:02:44 -0700585 raise DevServerError('file not found: %s' % file_path)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700586 try:
587 file_size = os.path.getsize(file_path)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700588 file_sha256 = common_util.GetFileSha256(file_path)
Amin Hassani469f5702019-10-21 15:35:06 -0700589 except os.error as e:
Amin Hassanid4e35392019-10-03 11:02:44 -0700590 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700591 'failed to get info for file %s: %s' % (file_path, e))
Gilad Arnolde74b3812013-04-22 11:27:38 -0700592
Gilad Arnolde74b3812013-04-22 11:27:38 -0700593 return json.dumps({
594 autoupdate.Autoupdate.SIZE_ATTR: file_size,
Gilad Arnolde74b3812013-04-22 11:27:38 -0700595 autoupdate.Autoupdate.SHA256_ATTR: file_sha256,
Gilad Arnolde74b3812013-04-22 11:27:38 -0700596 })
Gilad Arnold55a2a372012-10-02 09:46:32 -0700597
Chris Sosa76e44b92013-01-31 12:11:38 -0800598
David Rochberg7c79a812011-01-19 14:24:45 -0500599class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700600 """The Root Class for the Dev Server.
601
602 CherryPy works as follows:
603 For each method in this class, cherrpy interprets root/path
604 as a call to an instance of DevServerRoot->method_name. For example,
605 a call to http://myhost/build will call build. CherryPy automatically
606 parses http args and places them as keyword arguments in each method.
607 For paths http://myhost/update/dir1/dir2, you can use *args so that
608 cherrypy uses the update method and puts the extra paths in args.
609 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700610 # Method names that should not be listed on the index page.
611 _UNLISTED_METHODS = ['index', 'doc']
612
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700613 api = ApiRoot()
Chris Sosa7c931362010-10-11 19:49:01 -0700614
Dan Shi59ae7092013-06-04 14:37:27 -0700615 # Number of threads that devserver is staging images.
616 _staging_thread_count = 0
617 # Lock used to lock increasing/decreasing count.
618 _staging_thread_count_lock = threading.Lock()
619
joychen3cb228e2013-06-12 12:13:13 -0700620 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700621 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800622 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 12:13:13 -0700623 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 14:24:45 -0500624
Congbin Guo3afae6c2019-08-13 16:29:42 -0700625 @property
626 def staging_thread_count(self):
627 """Get the staging thread count."""
628 return self._staging_thread_count
Dan Shiafd0e492015-05-27 14:23:51 -0700629
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700630 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500631 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700632 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700633 import builder
634 if self._builder is None:
635 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500636 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700637
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700638 @cherrypy.expose
Dan Shif8eb0d12013-08-01 17:52:06 -0700639 def is_staged(self, **kwargs):
640 """Check if artifacts have been downloaded.
641
Congbin Guo3afae6c2019-08-13 16:29:42 -0700642 Examples:
643 To check if autotest and test_suites are staged:
644 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
645 artifacts=autotest,test_suites
646
Amin Hassani08e42d22019-06-03 00:31:30 -0700647 Args:
Chris Sosa6b0c6172013-08-05 17:01:33 -0700648 async: True to return without waiting for download to complete.
649 artifacts: Comma separated list of named artifacts to download.
650 These are defined in artifact_info and have their implementation
651 in build_artifact.py.
652 files: Comma separated list of file artifacts to stage. These
653 will be available as is in the corresponding static directory with no
654 custom post-processing.
655
Congbin Guo3afae6c2019-08-13 16:29:42 -0700656 Returns:
657 True of all artifacts are staged.
Dan Shif8eb0d12013-08-01 17:52:06 -0700658 """
Gabe Black3b567202015-09-23 14:07:59 -0700659 dl, factory = _get_downloader_and_factory(kwargs)
Aviv Keshet57d18172016-06-18 20:39:09 -0700660 response = str(dl.IsStaged(factory))
661 _Log('Responding to is_staged %s request with %r', kwargs, response)
662 return response
Dan Shi59ae7092013-06-04 14:37:27 -0700663
Chris Sosa76e44b92013-01-31 12:11:38 -0800664 @cherrypy.expose
Prashanth Ba06d2d22014-03-07 15:35:19 -0800665 def list_image_dir(self, **kwargs):
666 """Take an archive url and list the contents in its staged directory.
667
Amin Hassani08e42d22019-06-03 00:31:30 -0700668 Examples:
Prashanth Ba06d2d22014-03-07 15:35:19 -0800669 To list the contents of where this devserver should have staged
670 gs://image-archive/<board>-release/<build> call:
671 http://devserver_url:<port>/list_image_dir?archive_url=<gs://..>
672
Congbin Guo3afae6c2019-08-13 16:29:42 -0700673 Args:
674 archive_url: Google Storage URL for the build.
675
Prashanth Ba06d2d22014-03-07 15:35:19 -0800676 Returns:
677 A string with information about the contents of the image directory.
678 """
Gabe Black3b567202015-09-23 14:07:59 -0700679 dl = _get_downloader(kwargs)
Prashanth Ba06d2d22014-03-07 15:35:19 -0800680 try:
Gabe Black3b567202015-09-23 14:07:59 -0700681 image_dir_contents = dl.ListBuildDir()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800682 except build_artifact.ArtifactDownloadError as e:
683 return 'Cannot list the contents of staged artifacts. %s' % e
684 if not image_dir_contents:
Gabe Black3b567202015-09-23 14:07:59 -0700685 return '%s has not been staged on this devserver.' % dl.DescribeSource()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800686 return image_dir_contents
687
688 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800689 def stage(self, **kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700690 """Downloads and caches build artifacts.
Chris Sosa76e44b92013-01-31 12:11:38 -0800691
Gabe Black3b567202015-09-23 14:07:59 -0700692 Downloads and caches build artifacts, possibly from a Google Storage URL,
Dan Shi72b16132015-10-08 12:10:33 -0700693 or from Android's build server. Returns once these have been downloaded
Gabe Black3b567202015-09-23 14:07:59 -0700694 on the devserver. A call to this will attempt to cache non-specified
695 artifacts in the background for the given from the given URL following
696 the principle of spatial locality. Spatial locality of different
Chris Sosa76e44b92013-01-31 12:11:38 -0800697 artifacts is explicitly defined in the build_artifact module.
698
699 These artifacts will then be available from the static/ sub-directory of
700 the devserver.
701
Amin Hassani08e42d22019-06-03 00:31:30 -0700702 Examples:
Chris Sosa76e44b92013-01-31 12:11:38 -0800703 To download the autotest and test suites tarballs:
704 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
705 artifacts=autotest,test_suites
706 To download the full update payload:
707 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
708 artifacts=full_payload
Chris Sosa6b0c6172013-08-05 17:01:33 -0700709 To download just a file called blah.bin:
710 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
711 files=blah.bin
Chris Sosa76e44b92013-01-31 12:11:38 -0800712
713 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 16:39:34 -0700714 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 12:11:38 -0800715
716 Note for this example, relative path is the archive_url stripped of its
717 basename i.e. path/ in the examples above. Specific example:
718
719 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
720
721 Will get staged to:
722
joychened64b222013-06-21 16:39:34 -0700723 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Congbin Guo3afae6c2019-08-13 16:29:42 -0700724
725 Args:
726 archive_url: Google Storage URL for the build.
727 local_path: Local path for the build.
728 delete_source: Only meaningful with local_path. bool to indicate if the
729 source files should be deleted. This is especially useful when staging
730 a file locally in resource constrained environments as it allows us to
731 move the relevant files locally instead of copying them.
732 async: True to return without waiting for download to complete.
733 artifacts: Comma separated list of named artifacts to download.
734 These are defined in artifact_info and have their implementation
735 in build_artifact.py.
736 files: Comma separated list of files to stage. These
737 will be available as is in the corresponding static directory with no
738 custom post-processing.
739 clean: True to remove any previously staged artifacts first.
Chris Sosa76e44b92013-01-31 12:11:38 -0800740 """
Gabe Black3b567202015-09-23 14:07:59 -0700741 dl, factory = _get_downloader_and_factory(kwargs)
742
Dan Shi59ae7092013-06-04 14:37:27 -0700743 with DevServerRoot._staging_thread_count_lock:
744 DevServerRoot._staging_thread_count += 1
745 try:
Laurence Goodbyf5c958d2016-01-14 18:23:56 -0800746 boolean_string = kwargs.get('clean')
747 clean = xbuddy.XBuddy.ParseBoolean(boolean_string)
748 if clean and os.path.exists(dl.GetBuildDir()):
749 _Log('Removing %s' % dl.GetBuildDir())
750 shutil.rmtree(dl.GetBuildDir())
Achuith Bhandarkar2a1fcd82019-10-17 17:45:58 -0700751 is_async = kwargs.get('async', False)
752 dl.Download(factory, is_async=is_async)
Dan Shi59ae7092013-06-04 14:37:27 -0700753 finally:
754 with DevServerRoot._staging_thread_count_lock:
755 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800756 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700757
758 @cherrypy.expose
xixuan52c2fba2016-05-20 17:02:48 -0700759 def cros_au(self, **kwargs):
760 """Auto-update a CrOS DUT.
761
762 Args:
763 kwargs:
764 host_name: the hostname of the DUT to auto-update.
765 build_name: the build name for update the DUT.
766 force_update: Force an update even if the version installed is the
767 same. Default: False.
768 full_update: If True, do not run stateful update, directly force a full
769 reimage. If False, try stateful update first if the dut is already
770 installed with the same version.
771 async: Whether the auto_update function is ran in the background.
David Rileyee75de22017-11-02 10:48:15 -0700772 quick_provision: Whether the quick provision path is attempted first.
xixuan52c2fba2016-05-20 17:02:48 -0700773
774 Returns:
775 A tuple includes two elements:
776 a boolean variable represents whether the auto-update process is
777 successfully started.
778 an integer represents the background auto-update process id.
779 """
780 _check_base_args_for_auto_update(kwargs)
781
782 host_name = kwargs['host_name']
783 build_name = kwargs['build_name']
784 force_update = _parse_boolean_arg(kwargs, 'force_update')
785 full_update = _parse_boolean_arg(kwargs, 'full_update')
Achuith Bhandarkar2a1fcd82019-10-17 17:45:58 -0700786 is_async = _parse_boolean_arg(kwargs, 'async')
xixuanac89ce82016-11-30 16:48:20 -0800787 original_build = _parse_string_arg(kwargs, 'original_build')
David Haddock90e49442017-04-07 19:14:09 -0700788 payload_filename = _parse_string_arg(kwargs, 'payload_filename')
David Haddock20559612017-06-28 22:15:08 -0700789 clobber_stateful = _parse_boolean_arg(kwargs, 'clobber_stateful')
David Rileyee75de22017-11-02 10:48:15 -0700790 quick_provision = _parse_boolean_arg(kwargs, 'quick_provision')
791
792 devserver_url = updater.GetDevserverUrl()
793 static_url = updater.GetStaticUrl()
xixuan52c2fba2016-05-20 17:02:48 -0700794
Achuith Bhandarkar2a1fcd82019-10-17 17:45:58 -0700795 if is_async:
Amin Hassani469f5702019-10-21 15:35:06 -0700796 # Command of running auto-update.
Amin Hassani78520ae2019-10-29 13:26:51 -0700797 path = os.path.dirname(os.path.abspath(__file__))
798 execute_file = os.path.join(path, 'cros_update.py')
799 cmd = ['/usr/bin/python', '-u', execute_file, '-d', host_name,
800 '-b', build_name, '--static_dir', updater.static_dir]
xixuanac89ce82016-11-30 16:48:20 -0800801
802 # The original_build's format is like: link/3428.210.0
803 # The corresponding release_archive_url's format is like:
804 # gs://chromeos-releases/stable-channel/link/3428.210.0
805 if original_build:
806 release_archive_url = _build_uri_from_build_name(original_build)
807 # First staging the stateful.tgz synchronousely.
Amin Hassani469f5702019-10-21 15:35:06 -0700808 self.stage(files='stateful.tgz', is_async=False,
xixuanac89ce82016-11-30 16:48:20 -0800809 archive_url=release_archive_url)
Amin Hassani469f5702019-10-21 15:35:06 -0700810 cmd += ['--original_build', original_build]
xixuanac89ce82016-11-30 16:48:20 -0800811
xixuan52c2fba2016-05-20 17:02:48 -0700812 if force_update:
Amin Hassani469f5702019-10-21 15:35:06 -0700813 cmd += ['--force_update']
xixuan52c2fba2016-05-20 17:02:48 -0700814
815 if full_update:
Amin Hassani469f5702019-10-21 15:35:06 -0700816 cmd += ['--full_update']
xixuan52c2fba2016-05-20 17:02:48 -0700817
David Haddock90e49442017-04-07 19:14:09 -0700818 if payload_filename:
Amin Hassani469f5702019-10-21 15:35:06 -0700819 cmd += ['--payload_filename', payload_filename]
David Haddock90e49442017-04-07 19:14:09 -0700820
David Haddock20559612017-06-28 22:15:08 -0700821 if clobber_stateful:
Amin Hassani469f5702019-10-21 15:35:06 -0700822 cmd += ['--clobber_stateful']
David Haddock20559612017-06-28 22:15:08 -0700823
David Rileyee75de22017-11-02 10:48:15 -0700824 if quick_provision:
Amin Hassani469f5702019-10-21 15:35:06 -0700825 cmd += ['--quick_provision']
David Rileyee75de22017-11-02 10:48:15 -0700826
827 if devserver_url:
Amin Hassani469f5702019-10-21 15:35:06 -0700828 cmd += ['--devserver_url', devserver_url]
David Rileyee75de22017-11-02 10:48:15 -0700829
830 if static_url:
Amin Hassani469f5702019-10-21 15:35:06 -0700831 cmd += ['--static_url', static_url]
David Rileyee75de22017-11-02 10:48:15 -0700832
Amin Hassani78520ae2019-10-29 13:26:51 -0700833 p = subprocess.Popen(cmd, preexec_fn=os.setsid)
xixuan2a0970a2016-08-10 12:12:44 -0700834 pid = os.getpgid(p.pid)
xixuan52c2fba2016-05-20 17:02:48 -0700835
836 # Pre-write status in the track_status_file before the first call of
837 # 'get_au_status' to make sure that the track_status_file exists.
xixuan2a0970a2016-08-10 12:12:44 -0700838 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -0700839 progress_tracker.WriteStatus('CrOS update is just started.')
840
xixuan2a0970a2016-08-10 12:12:44 -0700841 return json.dumps((True, pid))
xixuan52c2fba2016-05-20 17:02:48 -0700842 else:
843 cros_update_trigger = cros_update.CrOSUpdateTrigger(
xixuanac89ce82016-11-30 16:48:20 -0800844 host_name, build_name, updater.static_dir, force_update=force_update,
David Rileyee75de22017-11-02 10:48:15 -0700845 full_update=full_update, original_build=original_build,
Amin Hassani78520ae2019-10-29 13:26:51 -0700846 payload_filename=payload_filename, quick_provision=quick_provision,
847 devserver_url=devserver_url, static_url=static_url)
xixuan52c2fba2016-05-20 17:02:48 -0700848 cros_update_trigger.TriggerAU()
xixuan27d50442017-08-09 10:38:25 -0700849 return json.dumps((True, -1))
xixuan52c2fba2016-05-20 17:02:48 -0700850
851 @cherrypy.expose
852 def get_au_status(self, **kwargs):
853 """Check if the auto-update task is finished.
854
855 It handles 4 cases:
856 1. If an error exists in the track_status_file, delete the track file and
857 raise it.
858 2. If cros-update process is finished, delete the file and return the
859 success result.
860 3. If the process is not running, delete the track file and raise an error
861 about 'the process is terminated due to unknown reason'.
862 4. If the track_status_file does not exist, kill the process if it exists,
863 and raise the IOError.
864
865 Args:
866 kwargs:
867 host_name: the hostname of the DUT to auto-update.
868 pid: the background process id of cros-update.
869
870 Returns:
xixuan28d99072016-10-06 12:24:16 -0700871 A dict with three elements:
xixuan52c2fba2016-05-20 17:02:48 -0700872 a boolean variable represents whether the auto-update process is
873 finished.
874 a string represents the current auto-update process status.
875 For example, 'Transfer Devserver/Stateful Update Package'.
xixuan28d99072016-10-06 12:24:16 -0700876 a detailed error message paragraph if there exists an Auto-Update
877 error, in which the last line shows the main exception. Empty
878 string otherwise.
xixuan52c2fba2016-05-20 17:02:48 -0700879 """
880 if 'host_name' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -0700881 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -0700882 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -0700883
884 if 'pid' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -0700885 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -0700886 KEY_ERROR_MSG % 'pid')
xixuan52c2fba2016-05-20 17:02:48 -0700887
888 host_name = kwargs['host_name']
889 pid = kwargs['pid']
890 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
891
xixuan28d99072016-10-06 12:24:16 -0700892 result_dict = {'finished': False, 'status': '', 'detailed_error_msg': ''}
xixuan52c2fba2016-05-20 17:02:48 -0700893 try:
894 result = progress_tracker.ReadStatus()
895 if result.startswith(cros_update_progress.ERROR_TAG):
xixuan28d99072016-10-06 12:24:16 -0700896 result_dict['detailed_error_msg'] = result[len(
897 cros_update_progress.ERROR_TAG):]
xixuan28681fd2016-11-23 11:13:56 -0800898 elif result == cros_update_progress.FINISHED:
xixuan28d99072016-10-06 12:24:16 -0700899 result_dict['finished'] = True
900 result_dict['status'] = result
xixuan28681fd2016-11-23 11:13:56 -0800901 elif not cros_update_progress.IsProcessAlive(pid):
xixuan28d99072016-10-06 12:24:16 -0700902 result_dict['detailed_error_msg'] = (
903 'Cros_update process terminated midway due to unknown reason. '
904 'Last update status was %s' % result)
xixuan28681fd2016-11-23 11:13:56 -0800905 else:
906 result_dict['status'] = result
907 except IOError as e:
908 if pid and cros_update_progress.IsProcessAlive(pid):
xixuan2a0970a2016-08-10 12:12:44 -0700909 os.killpg(int(pid), signal.SIGKILL)
xixuan52c2fba2016-05-20 17:02:48 -0700910
xixuan28681fd2016-11-23 11:13:56 -0800911 result_dict['detailed_error_msg'] = str(e)
912
913 return json.dumps(result_dict)
xixuan52c2fba2016-05-20 17:02:48 -0700914
915 @cherrypy.expose
David Riley6d5fca02017-10-31 10:35:47 -0700916 def post_au_status(self, status, **kwargs):
917 """Updates the status of an auto-update task.
918
919 Callers will need to POST to this URL with a body of MIME-type
920 "multipart/form-data".
921 The body should include a single argument, 'status', containing the
922 AU status to record.
923
924 Args:
925 status: The updated status.
926 kwargs:
927 host_name: the hostname of the DUT to auto-update.
928 pid: the background process id of cros-update.
929 """
930 if 'host_name' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -0700931 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -0700932 KEY_ERROR_MSG % 'host_name')
David Riley6d5fca02017-10-31 10:35:47 -0700933
934 if 'pid' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -0700935 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -0700936 KEY_ERROR_MSG % 'pid')
David Riley6d5fca02017-10-31 10:35:47 -0700937
938 host_name = kwargs['host_name']
939 pid = kwargs['pid']
David Riley3cea2582017-11-24 22:03:01 -0800940 status = status.rstrip()
941 _Log('Recording status for %s (%s): %s' % (host_name, pid, status))
David Riley6d5fca02017-10-31 10:35:47 -0700942 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
943
David Riley3cea2582017-11-24 22:03:01 -0800944 progress_tracker.WriteStatus(status)
David Riley6d5fca02017-10-31 10:35:47 -0700945
946 return 'True'
947
948 @cherrypy.expose
xixuan52c2fba2016-05-20 17:02:48 -0700949 def handler_cleanup(self, **kwargs):
xixuan3bc974e2016-10-18 17:21:43 -0700950 """Clean track status log and temp directory for CrOS auto-update process.
xixuan52c2fba2016-05-20 17:02:48 -0700951
952 Args:
953 kwargs:
954 host_name: the hostname of the DUT to auto-update.
955 pid: the background process id of cros-update.
956 """
957 if 'host_name' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -0700958 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -0700959 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -0700960
961 if 'pid' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -0700962 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -0700963 KEY_ERROR_MSG % 'pid')
xixuan52c2fba2016-05-20 17:02:48 -0700964
965 host_name = kwargs['host_name']
966 pid = kwargs['pid']
967 cros_update_progress.DelTrackStatusFile(host_name, pid)
xixuan3bc974e2016-10-18 17:21:43 -0700968 cros_update_progress.DelAUTempDirectory(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -0700969
970 @cherrypy.expose
971 def kill_au_proc(self, **kwargs):
972 """Kill CrOS auto-update process using given process id.
973
974 Args:
975 kwargs:
976 host_name: Kill all the CrOS auto-update process of this host.
977
978 Returns:
979 True if all processes are killed properly.
980 """
981 if 'host_name' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -0700982 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -0700983 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -0700984
xixuan447ad9d2017-02-28 14:46:20 -0800985 cur_pid = kwargs.get('pid')
986
xixuan52c2fba2016-05-20 17:02:48 -0700987 host_name = kwargs['host_name']
xixuan3bc974e2016-10-18 17:21:43 -0700988 track_log_list = cros_update_progress.GetAllTrackStatusFileByHostName(
989 host_name)
xixuan52c2fba2016-05-20 17:02:48 -0700990 for log in track_log_list:
991 # The track log's full path is: path/host_name_pid.log
992 # Use splitext to remove file extension, then parse pid from the
993 # filename.
Congbin Guo3afae6c2019-08-13 16:29:42 -0700994 pid = os.path.splitext(os.path.basename(log))[0][len(host_name) + 1:]
xixuan447ad9d2017-02-28 14:46:20 -0800995 _clear_process(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -0700996
xixuan447ad9d2017-02-28 14:46:20 -0800997 if cur_pid:
998 _clear_process(host_name, cur_pid)
xixuan52c2fba2016-05-20 17:02:48 -0700999
1000 return 'True'
1001
1002 @cherrypy.expose
1003 def collect_cros_au_log(self, **kwargs):
1004 """Collect CrOS auto-update log.
1005
1006 Args:
1007 kwargs:
1008 host_name: the hostname of the DUT to auto-update.
1009 pid: the background process id of cros-update.
1010
1011 Returns:
David Haddock9f459632017-05-11 14:45:46 -07001012 A dictionary containing the execute log file and any hostlog files.
xixuan52c2fba2016-05-20 17:02:48 -07001013 """
1014 if 'host_name' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -07001015 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -07001016 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -07001017
1018 if 'pid' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -07001019 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -07001020 KEY_ERROR_MSG % 'pid')
xixuan52c2fba2016-05-20 17:02:48 -07001021
1022 host_name = kwargs['host_name']
1023 pid = kwargs['pid']
xixuan3bc974e2016-10-18 17:21:43 -07001024
1025 # Fetch the execute log recorded by cros_update_progress.
xixuan1bbfaba2016-10-13 17:53:22 -07001026 au_log = cros_update_progress.ReadExecuteLogFile(host_name, pid)
1027 cros_update_progress.DelExecuteLogFile(host_name, pid)
David Haddock9f459632017-05-11 14:45:46 -07001028 # Fetch the cros_au host_logs if they exist
1029 au_hostlogs = cros_update_progress.ReadAUHostLogFiles(host_name, pid)
1030 return json.dumps({'cros_au_log': au_log, 'host_logs': au_hostlogs})
xixuan1bbfaba2016-10-13 17:53:22 -07001031
xixuan52c2fba2016-05-20 17:02:48 -07001032 @cherrypy.expose
Dan Shi2f136862016-02-11 15:38:38 -08001033 def locate_file(self, **kwargs):
1034 """Get the path to the given file name.
1035
1036 This method looks up the given file name inside specified build artifacts.
1037 One use case is to help caller to locate an apk file inside a build
1038 artifact. The location of the apk file could be different based on the
1039 branch and target.
1040
1041 Args:
1042 file_name: Name of the file to look for.
1043 artifacts: A list of artifact names to search for the file.
1044
1045 Returns:
1046 Path to the file with the given name. It's relative to the folder for the
1047 build, e.g., DATA/priv-app/sl4a/sl4a.apk
Dan Shi2f136862016-02-11 15:38:38 -08001048 """
1049 dl, _ = _get_downloader_and_factory(kwargs)
1050 try:
Joe Brennan1691f8e2017-03-15 15:53:36 -07001051 file_name = kwargs['file_name']
Dan Shi2f136862016-02-11 15:38:38 -08001052 artifacts = kwargs['artifacts']
1053 except KeyError:
Amin Hassanid4e35392019-10-03 11:02:44 -07001054 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -07001055 '`file_name` and `artifacts` are required to search '
1056 'for a file in build artifacts.')
Dan Shi2f136862016-02-11 15:38:38 -08001057 build_path = dl.GetBuildDir()
1058 for artifact in artifacts:
1059 # Get the unzipped folder of the artifact. If it's not defined in
1060 # ARTIFACT_UNZIP_FOLDER_MAP, assume the files are unzipped to the build
1061 # directory directly.
1062 folder = artifact_info.ARTIFACT_UNZIP_FOLDER_MAP.get(artifact, '')
1063 artifact_path = os.path.join(build_path, folder)
1064 for root, _, filenames in os.walk(artifact_path):
Joe Brennan1691f8e2017-03-15 15:53:36 -07001065 if file_name in set([f for f in filenames]):
Dan Shi2f136862016-02-11 15:38:38 -08001066 return os.path.relpath(os.path.join(root, file_name), build_path)
Amin Hassanid4e35392019-10-03 11:02:44 -07001067 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -07001068 'File `%s` can not be found in artifacts: %s' % (file_name, artifacts))
Dan Shi2f136862016-02-11 15:38:38 -08001069
1070 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -08001071 def setup_telemetry(self, **kwargs):
1072 """Extracts and sets up telemetry
1073
1074 This method goes through the telemetry deps packages, and stages them on
1075 the devserver to be used by the drones and the telemetry tests.
1076
1077 Args:
1078 archive_url: Google Storage URL for the build.
1079
1080 Returns:
1081 Path to the source folder for the telemetry codebase once it is staged.
1082 """
Gabe Black3b567202015-09-23 14:07:59 -07001083 dl = _get_downloader(kwargs)
Simran Basi4baad082013-02-14 13:39:18 -08001084
Gabe Black3b567202015-09-23 14:07:59 -07001085 build_path = dl.GetBuildDir()
Simran Basi4baad082013-02-14 13:39:18 -08001086 deps_path = os.path.join(build_path, 'autotest/packages')
1087 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
1088 src_folder = os.path.join(telemetry_path, 'src')
1089
1090 with self._telemetry_lock_dict.lock(telemetry_path):
1091 if os.path.exists(src_folder):
1092 # Telemetry is already fully stage return
1093 return src_folder
1094
1095 common_util.MkDirP(telemetry_path)
1096
1097 # Copy over the required deps tar balls to the telemetry directory.
1098 for dep in TELEMETRY_DEPS:
1099 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -07001100 if not os.path.exists(dep_path):
1101 # This dep does not exist (could be new), do not extract it.
1102 continue
Simran Basi4baad082013-02-14 13:39:18 -08001103 try:
1104 common_util.ExtractTarball(dep_path, telemetry_path)
1105 except common_util.CommonUtilError as e:
1106 shutil.rmtree(telemetry_path)
Amin Hassanid4e35392019-10-03 11:02:44 -07001107 raise DevServerError(str(e))
Simran Basi4baad082013-02-14 13:39:18 -08001108
1109 # By default all the tarballs extract to test_src but some parts of
1110 # the telemetry code specifically hardcoded to exist inside of 'src'.
1111 test_src = os.path.join(telemetry_path, 'test_src')
1112 try:
1113 shutil.move(test_src, src_folder)
1114 except shutil.Error:
1115 # This can occur if src_folder already exists. Remove and retry move.
1116 shutil.rmtree(src_folder)
Amin Hassanid4e35392019-10-03 11:02:44 -07001117 raise DevServerError(
Gabe Black3b567202015-09-23 14:07:59 -07001118 'Failure in telemetry setup for build %s. Appears that the '
1119 'test_src to src move failed.' % dl.GetBuild())
Simran Basi4baad082013-02-14 13:39:18 -08001120
1121 return src_folder
1122
1123 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -08001124 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -07001125 """Symbolicates a minidump using pre-downloaded symbols, returns it.
1126
1127 Callers will need to POST to this URL with a body of MIME-type
1128 "multipart/form-data".
1129 The body should include a single argument, 'minidump', containing the
1130 binary-formatted minidump to symbolicate.
1131
Chris Masone816e38c2012-05-02 12:22:36 -07001132 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -08001133 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -07001134 minidump: The binary minidump file to symbolicate.
1135 """
Chris Sosa76e44b92013-01-31 12:11:38 -08001136 # Ensure the symbols have been staged.
Dan Shif08fe492016-10-04 14:39:25 -07001137 # Try debug.tar.xz first, then debug.tgz
1138 for artifact in (artifact_info.SYMBOLS_ONLY, artifact_info.SYMBOLS):
1139 kwargs['artifacts'] = artifact
1140 dl = _get_downloader(kwargs)
1141
1142 try:
1143 if self.stage(**kwargs) == 'Success':
1144 break
1145 except build_artifact.ArtifactDownloadError:
1146 continue
1147 else:
Amin Hassanid4e35392019-10-03 11:02:44 -07001148 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -07001149 'Failed to stage symbols for %s' % dl.DescribeSource())
Chris Sosa76e44b92013-01-31 12:11:38 -08001150
Chris Masone816e38c2012-05-02 12:22:36 -07001151 to_return = ''
1152 with tempfile.NamedTemporaryFile() as local:
1153 while True:
1154 data = minidump.file.read(8192)
1155 if not data:
1156 break
1157 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -08001158
Chris Masone816e38c2012-05-02 12:22:36 -07001159 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -08001160
Gabe Black3b567202015-09-23 14:07:59 -07001161 symbols_directory = os.path.join(dl.GetBuildDir(), 'debug', 'breakpad')
Chris Sosa76e44b92013-01-31 12:11:38 -08001162
xixuanab744382017-04-27 10:41:27 -07001163 # The location of minidump_stackwalk is defined in chromeos-admin.
Chris Sosa76e44b92013-01-31 12:11:38 -08001164 stackwalk = subprocess.Popen(
xixuanab744382017-04-27 10:41:27 -07001165 ['/usr/local/bin/minidump_stackwalk', local.name, symbols_directory],
Chris Sosa76e44b92013-01-31 12:11:38 -08001166 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1167
Chris Masone816e38c2012-05-02 12:22:36 -07001168 to_return, error_text = stackwalk.communicate()
1169 if stackwalk.returncode != 0:
Amin Hassanid4e35392019-10-03 11:02:44 -07001170 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -07001171 "Can't generate stack trace: %s (rc=%d)" % (error_text,
1172 stackwalk.returncode))
Chris Masone816e38c2012-05-02 12:22:36 -07001173
1174 return to_return
1175
1176 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -08001177 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 15:31:36 -04001178 """Return a string representing the latest build for a given target.
1179
1180 Args:
1181 target: The build target, typically a combination of the board and the
1182 type of build e.g. x86-mario-release.
1183 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
1184 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-07 18:21:26 -08001185
Scott Zawalski16954532012-03-20 15:31:36 -04001186 Returns:
1187 A string representation of the latest build if one exists, i.e.
1188 R19-1993.0.0-a1-b1480.
1189 An empty string if no latest could be found.
1190 """
Don Garrettf84631a2014-01-07 18:21:26 -08001191 if not kwargs:
Scott Zawalski16954532012-03-20 15:31:36 -04001192 return _PrintDocStringAsHTML(self.latestbuild)
1193
Don Garrettf84631a2014-01-07 18:21:26 -08001194 if 'target' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -07001195 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -07001196 'Error: target= is required!')
Dan Shi61305df2015-10-26 16:52:35 -07001197
1198 if _is_android_build_request(kwargs):
1199 branch = kwargs.get('branch', None)
1200 target = kwargs.get('target', None)
1201 if not target or not branch:
Amin Hassanid4e35392019-10-03 11:02:44 -07001202 raise DevServerError('Both target and branch must be specified to query'
1203 ' for the latest Android build.')
Dan Shi61305df2015-10-26 16:52:35 -07001204 return android_build.BuildAccessor.GetLatestBuildID(target, branch)
1205
Scott Zawalski16954532012-03-20 15:31:36 -04001206 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001207 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-07 18:21:26 -08001208 updater.static_dir, kwargs['target'],
1209 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -07001210 except common_util.CommonUtilError as errmsg:
Amin Hassanid4e35392019-10-03 11:02:44 -07001211 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -07001212 str(errmsg))
Scott Zawalski16954532012-03-20 15:31:36 -04001213
1214 @cherrypy.expose
xixuan7efd0002016-04-14 15:34:01 -07001215 def list_suite_controls(self, **kwargs):
1216 """Return a list of contents of all known control files.
1217
1218 Example URL:
1219 To List all control files' content:
1220 http://dev-server/list_suite_controls?suite_name=bvt&
1221 build=daisy_spring-release/R29-4279.0.0
1222
1223 Args:
1224 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
1225 suite_name: List the control files belonging to that suite.
1226
1227 Returns:
Dan Shia1cd6522016-04-18 16:07:21 -07001228 A dictionary of all control files's path to its content for given suite.
xixuan7efd0002016-04-14 15:34:01 -07001229 """
1230 if not kwargs:
1231 return _PrintDocStringAsHTML(self.controlfiles)
1232
1233 if 'build' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -07001234 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -07001235 'Error: build= is required!')
xixuan7efd0002016-04-14 15:34:01 -07001236
1237 if 'suite_name' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -07001238 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Dan Shia1cd6522016-04-18 16:07:21 -07001239 'Error: suite_name= is required!')
xixuan7efd0002016-04-14 15:34:01 -07001240
1241 control_file_list = [
1242 line.rstrip() for line in common_util.GetControlFileListForSuite(
1243 updater.static_dir, kwargs['build'],
1244 kwargs['suite_name']).splitlines()]
1245
Dan Shia1cd6522016-04-18 16:07:21 -07001246 control_file_content_dict = {}
xixuan7efd0002016-04-14 15:34:01 -07001247 for control_path in control_file_list:
Dan Shia1cd6522016-04-18 16:07:21 -07001248 control_file_content_dict[control_path] = (common_util.GetControlFile(
xixuan7efd0002016-04-14 15:34:01 -07001249 updater.static_dir, kwargs['build'], control_path))
1250
Dan Shia1cd6522016-04-18 16:07:21 -07001251 return json.dumps(control_file_content_dict)
xixuan7efd0002016-04-14 15:34:01 -07001252
1253 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -08001254 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 17:17:28 -05001255 """Return a control file or a list of all known control files.
1256
1257 Example URL:
1258 To List all control files:
beepsbd337242013-07-09 22:44:06 -07001259 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
1260 To List all control files for, say, the bvt suite:
1261 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -05001262 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -05001263 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 -05001264
1265 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -05001266 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -05001267 control_path: If you want the contents of a control file set this
1268 to the path. E.g. client/site_tests/sleeptest/control
1269 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -07001270 suite_name: If control_path is not specified but a suite_name is
1271 specified, list the control files belonging to that suite instead of
1272 all control files. The empty string for suite_name will list all control
1273 files for the build.
Don Garrettf84631a2014-01-07 18:21:26 -08001274
Scott Zawalski4647ce62012-01-03 17:17:28 -05001275 Returns:
1276 Contents of a control file if control_path is provided.
1277 A list of control files if no control_path is provided.
1278 """
Don Garrettf84631a2014-01-07 18:21:26 -08001279 if not kwargs:
Scott Zawalski4647ce62012-01-03 17:17:28 -05001280 return _PrintDocStringAsHTML(self.controlfiles)
1281
Don Garrettf84631a2014-01-07 18:21:26 -08001282 if 'build' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -07001283 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -07001284 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -05001285
Don Garrettf84631a2014-01-07 18:21:26 -08001286 if 'control_path' not in kwargs:
1287 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-09 22:44:06 -07001288 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-07 18:21:26 -08001289 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-09 22:44:06 -07001290 else:
1291 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-07 18:21:26 -08001292 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -05001293 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001294 return common_util.GetControlFile(
Don Garrettf84631a2014-01-07 18:21:26 -08001295 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -08001296
1297 @cherrypy.expose
Simran Basi99e63c02014-05-20 10:39:52 -07001298 def xbuddy_translate(self, *args, **kwargs):
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001299 """Translates an xBuddy path to a real path to artifact if it exists.
1300
1301 Args:
Simran Basi99e63c02014-05-20 10:39:52 -07001302 args: An xbuddy path in the form of {local|remote}/build_id/artifact.
1303 Local searches the devserver's static directory. Remote searches a
1304 Google Storage image archive.
1305
1306 Kwargs:
1307 image_dir: Google Storage image archive to search in if requesting a
1308 remote artifact. If none uses the default bucket.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001309
1310 Returns:
Simran Basi99e63c02014-05-20 10:39:52 -07001311 String in the format of build_id/artifact as stored on the local server
1312 or in Google Storage.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001313 """
Simran Basi99e63c02014-05-20 10:39:52 -07001314 build_id, filename = self._xbuddy.Translate(
Gabe Black3b567202015-09-23 14:07:59 -07001315 args, image_dir=kwargs.get('image_dir'))
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001316 response = os.path.join(build_id, filename)
1317 _Log('Path translation requested, returning: %s', response)
1318 return response
1319
1320 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -07001321 def xbuddy(self, *args, **kwargs):
1322 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -07001323
1324 Args:
joycheneaf4cfc2013-07-02 08:38:57 -07001325 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -07001326 components of the path. The path can be understood as
1327 "{local|remote}/build_id/artifact" where build_id is composed of
1328 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -07001329
joychen121fc9b2013-08-02 14:30:30 -07001330 The first path element is optional, and can be "remote" or "local"
1331 If local (the default), devserver will not attempt to access Google
1332 Storage, and will only search the static directory for the files.
1333 If remote, devserver will try to obtain the artifact off GS if it's
1334 not found locally.
1335 The board is the familiar board name, optionally suffixed.
1336 The version can be the google storage version number, and may also be
1337 any of a number of xBuddy defined version aliases that will be
1338 translated into the latest built image that fits the description.
1339 Defaults to latest.
1340 The artifact is one of a number of image or artifact aliases used by
1341 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -07001342
1343 Kwargs:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001344 for_update: {true|false}
Amin Hassanie9ffb862019-09-25 17:10:40 -07001345 if true, prepares the update payloads for the image,
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001346 and returns the update uri to pass to the
1347 update_engine_client.
joychen3cb228e2013-06-12 12:13:13 -07001348 return_dir: {true|false}
1349 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001350 relative_path: {true|false}
1351 if set to true, returns the relative path to the payload
1352 directory from static_dir.
joychen3cb228e2013-06-12 12:13:13 -07001353 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -07001354 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -07001355 or
joycheneaf4cfc2013-07-02 08:38:57 -07001356 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -07001357
1358 Returns:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001359 If |for_update|, returns a redirect to the image or update file
1360 on the devserver. E.g.,
1361 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
1362 chromium-test-image.bin
1363 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
1364 http://host:port/static/x86-generic-release/R26-4000.0.0/
1365 If |relative_path| is true, return a relative path the folder where the
1366 payloads are. E.g.,
1367 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 12:13:13 -07001368 """
Chris Sosa75490802013-09-30 17:21:45 -07001369 boolean_string = kwargs.get('for_update')
1370 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001371 boolean_string = kwargs.get('return_dir')
1372 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
1373 boolean_string = kwargs.get('relative_path')
1374 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -07001375
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001376 if return_dir and relative_path:
Chris Sosa4b951602014-04-09 20:26:07 -07001377 raise common_util.DevServerHTTPError(
Amin Hassanid4e35392019-10-03 11:02:44 -07001378 http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -07001379 'Cannot specify both return_dir and relative_path')
Chris Sosa75490802013-09-30 17:21:45 -07001380
1381 # For updates, we optimize downloading of test images.
1382 file_name = None
1383 build_id = None
1384 if for_update:
1385 try:
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001386 build_id = self._xbuddy.StageTestArtifactsForUpdate(args)
Chris Sosa75490802013-09-30 17:21:45 -07001387 except build_artifact.ArtifactDownloadError:
1388 build_id = None
1389
1390 if not build_id:
1391 build_id, file_name = self._xbuddy.Get(args)
1392
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001393 if for_update:
Amin Hassanie9ffb862019-09-25 17:10:40 -07001394 _Log('Payloads requested.')
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001395 # Forces payload to be in cache and symlinked into build_id dir.
Amin Hassanie9ffb862019-09-25 17:10:40 -07001396 updater.GetUpdateForLabel(build_id)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001397
1398 response = None
1399 if return_dir:
1400 response = os.path.join(cherrypy.request.base, 'static', build_id)
1401 _Log('Directory requested, returning: %s', response)
1402 elif relative_path:
1403 response = build_id
1404 _Log('Relative path requested, returning: %s', response)
1405 elif for_update:
1406 response = os.path.join(cherrypy.request.base, 'update', build_id)
1407 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 12:13:13 -07001408 else:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001409 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 14:30:30 -07001410 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001411 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 14:30:30 -07001412 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -07001413
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001414 return response
1415
joychen3cb228e2013-06-12 12:13:13 -07001416 @cherrypy.expose
1417 def xbuddy_list(self):
1418 """Lists the currently available images & time since last access.
1419
Gilad Arnold452fd272014-02-04 11:09:28 -08001420 Returns:
1421 A string representation of a list of tuples [(build_id, time since last
1422 access),...]
joychen3cb228e2013-06-12 12:13:13 -07001423 """
1424 return self._xbuddy.List()
1425
1426 @cherrypy.expose
1427 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 11:09:28 -08001428 """Returns the number of images cached by xBuddy."""
joychen3cb228e2013-06-12 12:13:13 -07001429 return self._xbuddy.Capacity()
1430
1431 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001432 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001433 """Presents a welcome message and documentation links."""
Congbin Guo6bc32182019-08-20 17:54:30 -07001434 html_template = (
1435 'Welcome to the Dev Server!<br>\n'
1436 '<br>\n'
1437 'Here are the available methods, click for documentation:<br>\n'
1438 '<br>\n'
1439 '%s')
1440
1441 exposed_methods = []
1442 for app in cherrypy.tree.apps.values():
1443 exposed_methods += _FindExposedMethods(
1444 app.root, app.script_name.lstrip('/'),
1445 unlisted=self._UNLISTED_METHODS)
1446
1447 return html_template % '<br>\n'.join(
1448 ['<a href=doc/%s>%s</a>' % (name, name)
1449 for name in sorted(exposed_methods)])
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001450
1451 @cherrypy.expose
1452 def doc(self, *args):
1453 """Shows the documentation for available methods / URLs.
1454
Amin Hassani08e42d22019-06-03 00:31:30 -07001455 Examples:
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001456 http://myhost/doc/update
1457 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001458 name = '/'.join(args)
Congbin Guo6bc32182019-08-20 17:54:30 -07001459 method = _GetExposedMethod(name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001460 if not method:
Amin Hassanid4e35392019-10-03 11:02:44 -07001461 raise DevServerError("No exposed method named `%s'" % name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001462 if not method.__doc__:
Amin Hassanid4e35392019-10-03 11:02:44 -07001463 raise DevServerError("No documentation for exposed method `%s'" % name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001464 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -07001465
Dale Curtisc9aaf3a2011-08-09 15:47:40 -07001466 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001467 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001468 """Handles an update check from a Chrome OS client.
1469
1470 The HTTP request should contain the standard Omaha-style XML blob. The URL
1471 line may contain an additional intermediate path to the update payload.
1472
joychen121fc9b2013-08-02 14:30:30 -07001473 This request can be handled in one of 4 ways, depending on the devsever
1474 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -07001475
Amin Hassanie9ffb862019-09-25 17:10:40 -07001476 1. No intermediate path. DEPRECATED
joychen121fc9b2013-08-02 14:30:30 -07001477
1478 2. Path explicitly invokes XBuddy
1479 If there is a path given, it can explicitly invoke xbuddy by prefixing it
1480 with 'xbuddy'. This path is then used to acquire an image binary for the
1481 devserver to generate an update payload from. Devserver then serves this
1482 payload.
1483
1484 3. Path is left for the devserver to interpret.
1485 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
1486 to generate a payload from the test image in that directory and serve it.
1487
joychen121fc9b2013-08-02 14:30:30 -07001488 Examples:
joychen121fc9b2013-08-02 14:30:30 -07001489 2. Explicitly invoke xbuddy
1490 update_engine_client --omaha_url=
1491 http://myhost/update/xbuddy/remote/board/version/dev
1492 This would go to GS to download the dev image for the board, from which
1493 the devserver would generate a payload to serve.
1494
1495 3. Give a path for devserver to interpret
1496 update_engine_client --omaha_url=http://myhost/update/some/random/path
1497 This would attempt, in order to:
1498 a) Generate an update from a test image binary if found in
1499 static_dir/some/random/path.
1500 b) Serve an update payload found in static_dir/some/random/path.
1501 c) Hope that some/random/path takes the form "board/version" and
1502 and attempt to download an update payload for that board/version
1503 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001504 """
joychen121fc9b2013-08-02 14:30:30 -07001505 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -08001506 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -07001507 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-11 19:49:01 -07001508
joychen121fc9b2013-08-02 14:30:30 -07001509 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001510
Dan Shif5ce2de2013-04-25 16:06:32 -07001511
Chris Sosadbc20082012-12-10 13:39:11 -08001512def _CleanCache(cache_dir, wipe):
1513 """Wipes any excess cached items in the cache_dir.
1514
1515 Args:
1516 cache_dir: the directory we are wiping from.
1517 wipe: If True, wipe all the contents -- not just the excess.
1518 """
1519 if wipe:
1520 # Clear the cache and exit on error.
1521 cmd = 'rm -rf %s/*' % cache_dir
1522 if os.system(cmd) != 0:
1523 _Log('Failed to clear the cache with %s' % cmd)
1524 sys.exit(1)
1525 else:
1526 # Clear all but the last N cached updates
1527 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
1528 (cache_dir, CACHED_ENTRIES))
1529 if os.system(cmd) != 0:
1530 _Log('Failed to clean up old delta cache files with %s' % cmd)
1531 sys.exit(1)
1532
1533
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001534def _AddTestingOptions(parser):
1535 group = optparse.OptionGroup(
1536 parser, 'Advanced Testing Options', 'These are used by test scripts and '
1537 'developers writing integration tests utilizing the devserver. They are '
1538 'not intended to be really used outside the scope of someone '
1539 'knowledgable about the test.')
1540 group.add_option('--exit',
1541 action='store_true',
Amin Hassanie9ffb862019-09-25 17:10:40 -07001542 help='do not start the server (yet clear cache)')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001543 group.add_option('--host_log',
1544 action='store_true', default=False,
1545 help='record history of host update events (/api/hostlog)')
1546 group.add_option('--max_updates',
Gabe Black3b567202015-09-23 14:07:59 -07001547 metavar='NUM', default=-1, type='int',
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001548 help='maximum number of update checks handled positively '
1549 '(default: unlimited)')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001550 group.add_option('--proxy_port',
1551 metavar='PORT', default=None, type='int',
1552 help='port to have the client connect to -- basically the '
1553 'devserver lies to the update to tell it to get the payload '
1554 'from a different port that will proxy the request back to '
1555 'the devserver. The proxy must be managed outside the '
1556 'devserver.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001557 parser.add_option_group(group)
1558
1559
1560def _AddUpdateOptions(parser):
1561 group = optparse.OptionGroup(
1562 parser, 'Autoupdate Options', 'These options can be used to change '
Amin Hassanie9ffb862019-09-25 17:10:40 -07001563 'how the devserver serve update payloads. Please '
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001564 'note that all of these option affect how a payload is generated and so '
1565 'do not work in archive-only mode.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001566 group.add_option('--critical_update',
1567 action='store_true', default=False,
1568 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001569 group.add_option('--payload',
1570 metavar='PATH',
1571 help='use the update payload from specified directory '
1572 '(update.gz).')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001573 parser.add_option_group(group)
1574
1575
1576def _AddProductionOptions(parser):
1577 group = optparse.OptionGroup(
1578 parser, 'Advanced Server Options', 'These options can be used to changed '
1579 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001580 group.add_option('--clear_cache',
1581 action='store_true', default=False,
1582 help='At startup, removes all cached entries from the'
Amin Hassanid4e35392019-10-03 11:02:44 -07001583 "devserver's cache.")
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001584 group.add_option('--logfile',
1585 metavar='PATH',
1586 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -07001587 group.add_option('--pidfile',
1588 metavar='PATH',
1589 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 11:04:13 -08001590 group.add_option('--portfile',
1591 metavar='PATH',
1592 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001593 group.add_option('--production',
1594 action='store_true', default=False,
1595 help='have the devserver use production values when '
1596 'starting up. This includes using more threads and '
1597 'performing less logging.')
1598 parser.add_option_group(group)
1599
1600
Paul Hobbsef4e0702016-06-27 17:01:42 -07001601def MakeLogHandler(logfile):
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001602 """Create a LogHandler instance used to log all messages."""
1603 hdlr_cls = handlers.TimedRotatingFileHandler
1604 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
xixuan3d48bff2017-01-30 19:00:09 -08001605 interval=_LOG_ROTATION_INTERVAL,
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001606 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -07001607 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001608 return hdlr
1609
1610
Chris Sosacde6bf42012-05-31 18:36:39 -07001611def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001612 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -08001613 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -07001614
1615 # get directory that the devserver is run from
1616 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -07001617 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -07001618 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001619 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -07001620 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -07001621 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001622 parser.add_option('--port',
1623 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 13:13:28 -08001624 help=('port for the dev server to use; if zero, binds to '
1625 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001626 parser.add_option('-t', '--test_image',
1627 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -07001628 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -07001629 parser.add_option('-x', '--xbuddy_manage_builds',
1630 action='store_true',
1631 default=False,
1632 help='If set, allow xbuddy to manage images in'
1633 'build/images.')
Dan Shi72b16132015-10-08 12:10:33 -07001634 parser.add_option('-a', '--android_build_credential',
1635 default=None,
1636 help='Path to a json file which contains the credential '
1637 'needed to access Android builds.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001638 _AddProductionOptions(parser)
1639 _AddUpdateOptions(parser)
1640 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -07001641 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001642
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001643 # Handle options that must be set globally in cherrypy. Do this
1644 # work up front, because calls to _Log() below depend on this
1645 # initialization.
1646 if options.production:
1647 cherrypy.config.update({'environment': 'production'})
1648 if not options.logfile:
1649 cherrypy.config.update({'log.screen': True})
1650 else:
1651 cherrypy.config.update({'log.error_file': '',
1652 'log.access_file': ''})
Paul Hobbsef4e0702016-06-27 17:01:42 -07001653 hdlr = MakeLogHandler(options.logfile)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001654 # Pylint can't seem to process these two calls properly
1655 # pylint: disable=E1101
1656 cherrypy.log.access_log.addHandler(hdlr)
1657 cherrypy.log.error_log.addHandler(hdlr)
1658 # pylint: enable=E1101
1659
joychened64b222013-06-21 16:39:34 -07001660 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001661 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001662
joychened64b222013-06-21 16:39:34 -07001663 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001664 # If our devserver is only supposed to serve payloads, we shouldn't be
1665 # mucking with the cache at all. If the devserver hadn't previously
1666 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001667 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001668 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001669 else:
1670 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001671
Chris Sosadbc20082012-12-10 13:39:11 -08001672 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 16:39:34 -07001673 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001674
Amin Hassanie9ffb862019-09-25 17:10:40 -07001675 _xbuddy = xbuddy.XBuddy(manage_builds=options.xbuddy_manage_builds,
joychen121fc9b2013-08-02 14:30:30 -07001676 static_dir=options.static_dir)
Chris Sosa75490802013-09-30 17:21:45 -07001677 if options.clear_cache and options.xbuddy_manage_builds:
1678 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 14:30:30 -07001679
Chris Sosa6a3697f2013-01-29 16:44:43 -08001680 # We allow global use here to share with cherrypy classes.
1681 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001682 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001683 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001684 _xbuddy,
joychened64b222013-06-21 16:39:34 -07001685 static_dir=options.static_dir,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001686 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001687 proxy_port=options.proxy_port,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001688 critical_update=options.critical_update,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001689 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001690 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001691 )
Chris Sosa7c931362010-10-11 19:49:01 -07001692
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001693 if options.exit:
1694 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001695
joychen3cb228e2013-06-12 12:13:13 -07001696 dev_server = DevServerRoot(_xbuddy)
Congbin Guo3afae6c2019-08-13 16:29:42 -07001697 health_checker_app = health_checker.Root(dev_server, options.static_dir)
joychen3cb228e2013-06-12 12:13:13 -07001698
Chris Sosa855b8932013-08-21 13:24:55 -07001699 if options.pidfile:
1700 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1701
Gilad Arnold11fbef42014-02-10 11:04:13 -08001702 if options.portfile:
1703 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1704
Dan Shiafd5c6c2016-01-07 10:27:03 -08001705 if (options.android_build_credential and
1706 os.path.exists(options.android_build_credential)):
1707 try:
1708 with open(options.android_build_credential) as f:
1709 android_build.BuildAccessor.credential_info = json.load(f)
1710 except ValueError as e:
1711 _Log('Failed to load the android build credential: %s. Error: %s.' %
1712 (options.android_build_credential, e))
Congbin Guo3afae6c2019-08-13 16:29:42 -07001713
1714 cherrypy.tree.mount(health_checker_app, '/check_health',
1715 config=health_checker.get_config())
joychen3cb228e2013-06-12 12:13:13 -07001716 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001717
1718
1719if __name__ == '__main__':
1720 main()