blob: dd543085bbbeec812d1ba8cfb633639c3fb89721 [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
Amin Hassanic5af4262019-11-13 13:37:20 -080027import distutils.version # pylint: disable=import-error,no-name-in-module
Gilad Arnold55a2a372012-10-02 09:46:32 -070028import json
David Riley2fcb0122017-11-02 11:25:39 -070029import optparse # pylint: disable=deprecated-module
rtc@google.comded22402009-10-26 22:36:21 +000030import os
Scott Zawalski4647ce62012-01-03 17:17:28 -050031import re
Simran Basi4baad082013-02-14 13:39:18 -080032import shutil
xixuan52c2fba2016-05-20 17:02:48 -070033import signal
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -080034import socket
Chris Masone816e38c2012-05-02 12:22:36 -070035import subprocess
J. Richard Barnette3d977b82013-04-23 11:05:19 -070036import sys
Chris Masone816e38c2012-05-02 12:22:36 -070037import tempfile
Dan Shi59ae7092013-06-04 14:37:27 -070038import threading
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -070039import types
J. Richard Barnette3d977b82013-04-23 11:05:19 -070040from logging import handlers
41
Amin Hassanid4e35392019-10-03 11:02:44 -070042from six.moves import http_client
43
44# pylint: disable=no-name-in-module, import-error
J. Richard Barnette3d977b82013-04-23 11:05:19 -070045import cherrypy
Chris Sosa855b8932013-08-21 13:24:55 -070046from cherrypy import _cplogging as cplogging
Amin Hassanid4e35392019-10-03 11:02:44 -070047from cherrypy.process import plugins
48# pylint: enable=no-name-in-module, import-error
rtc@google.comded22402009-10-26 22:36:21 +000049
Achuith Bhandarkar662fb722019-10-31 16:12:49 -070050import autoupdate
51import cherrypy_ext
Achuith Bhandarkar662fb722019-10-31 16:12:49 -070052import health_checker
53
Richard Barnettedf35c322017-08-18 17:02:13 -070054# This must happen before any local modules get a chance to import
55# anything from chromite. Otherwise, really bad things will happen, and
56# you will _not_ understand why.
Congbin Guo3afae6c2019-08-13 16:29:42 -070057import setup_chromite # pylint: disable=unused-import
Amin Hassanie427e212019-10-28 11:04:27 -070058from chromite.lib import cros_update_progress
Achuith Bhandarkar662fb722019-10-31 16:12:49 -070059from chromite.lib.xbuddy import android_build
60from chromite.lib.xbuddy import artifact_info
61from chromite.lib.xbuddy import build_artifact
62from chromite.lib.xbuddy import cherrypy_log_util
63from chromite.lib.xbuddy import common_util
64from chromite.lib.xbuddy import devserver_constants
65from chromite.lib.xbuddy import downloader
66from chromite.lib.xbuddy import xbuddy
Amin Hassanie427e212019-10-28 11:04:27 -070067from chromite.scripts import cros_update
Gilad Arnoldc65330c2012-09-20 15:17:48 -070068
Gilad Arnoldc65330c2012-09-20 15:17:48 -070069# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080070def _Log(message, *args):
Achuith Bhandarkar662fb722019-10-31 16:12:49 -070071 return cherrypy_log_util.LogWithTag('DEVSERVER', message, *args)
Frank Farzan40160872011-12-12 18:39:18 -080072
Chris Sosa417e55d2011-01-25 16:40:48 -080073CACHED_ENTRIES = 12
Don Garrettf90edf02010-11-16 17:36:14 -080074
Simran Basi4baad082013-02-14 13:39:18 -080075TELEMETRY_FOLDER = 'telemetry_src'
76TELEMETRY_DEPS = ['dep-telemetry_dep.tar.bz2',
77 'dep-page_cycler_dep.tar.bz2',
Simran Basi0d078682013-03-22 16:40:04 -070078 'dep-chrome_test.tar.bz2',
79 'dep-perf_data_dep.tar.bz2']
Simran Basi4baad082013-02-14 13:39:18 -080080
Chris Sosa0356d3b2010-09-16 15:46:22 -070081# Sets up global to share between classes.
rtc@google.com21a5ca32009-11-04 18:23:23 +000082updater = None
rtc@google.comded22402009-10-26 22:36:21 +000083
xixuan3d48bff2017-01-30 19:00:09 -080084# Log rotation parameters. These settings correspond to twice a day once
85# devserver is started, with about two weeks (28 backup files) of old logs
86# kept for backup.
J. Richard Barnette3d977b82013-04-23 11:05:19 -070087#
xixuan3d48bff2017-01-30 19:00:09 -080088# For more, see the documentation in standard python library for
J. Richard Barnette3d977b82013-04-23 11:05:19 -070089# logging.handlers.TimedRotatingFileHandler
xixuan3d48bff2017-01-30 19:00:09 -080090_LOG_ROTATION_TIME = 'H'
Congbin Guo3afae6c2019-08-13 16:29:42 -070091_LOG_ROTATION_INTERVAL = 12 # hours
92_LOG_ROTATION_BACKUP = 28 # backup counts
Frank Farzan40160872011-12-12 18:39:18 -080093
xixuan52c2fba2016-05-20 17:02:48 -070094# Auto-update parameters
95
96# Error msg for missing key in CrOS auto-update.
Xixuan Wu32af9f12017-11-13 14:11:44 -080097KEY_ERROR_MSG = 'Key Error in RPC: %s= is required'
xixuan52c2fba2016-05-20 17:02:48 -070098
xixuan52c2fba2016-05-20 17:02:48 -070099
Amin Hassanid4e35392019-10-03 11:02:44 -0700100class DevServerError(Exception):
101 """Exception class used by DevServer."""
102
103
Amin Hassani722e0962019-11-15 15:45:31 -0800104class DevServerHTTPError(cherrypy.HTTPError):
105 """Exception class to log the HTTPResponse before routing it to cherrypy."""
106 def __init__(self, status, message):
107 """CherryPy error with logging.
108
109 Args:
110 status: HTTPResponse status.
111 message: Message associated with the response.
112 """
113 cherrypy.HTTPError.__init__(self, status, message)
114 _Log('HTTPError status: %s message: %s', status, message)
115
116
Gabe Black3b567202015-09-23 14:07:59 -0700117def _canonicalize_archive_url(archive_url):
118 """Canonicalizes archive_url strings.
119
120 Raises:
121 DevserverError: if archive_url is not set.
122 """
123 if archive_url:
124 if not archive_url.startswith('gs://'):
Amin Hassanid4e35392019-10-03 11:02:44 -0700125 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700126 "Archive URL isn't from Google Storage (%s) ." % archive_url)
Gabe Black3b567202015-09-23 14:07:59 -0700127
128 return archive_url.rstrip('/')
129 else:
Amin Hassanid4e35392019-10-03 11:02:44 -0700130 raise DevServerError('Must specify an archive_url in the request')
Gabe Black3b567202015-09-23 14:07:59 -0700131
132
133def _canonicalize_local_path(local_path):
134 """Canonicalizes |local_path| strings.
135
136 Raises:
137 DevserverError: if |local_path| is not set.
138 """
139 # Restrict staging of local content to only files within the static
140 # directory.
141 local_path = os.path.abspath(local_path)
142 if not local_path.startswith(updater.static_dir):
Amin Hassanid4e35392019-10-03 11:02:44 -0700143 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700144 'Local path %s must be a subdirectory of the static'
145 ' directory: %s' % (local_path, updater.static_dir))
Gabe Black3b567202015-09-23 14:07:59 -0700146
147 return local_path.rstrip('/')
148
149
150def _get_artifacts(kwargs):
151 """Returns a tuple of named and file artifacts given the stage rpc kwargs.
152
153 Raises:
154 DevserverError if no artifacts would be returned.
155 """
156 artifacts = kwargs.get('artifacts')
157 files = kwargs.get('files')
158 if not artifacts and not files:
Amin Hassanid4e35392019-10-03 11:02:44 -0700159 raise DevServerError('No artifacts specified.')
Gabe Black3b567202015-09-23 14:07:59 -0700160
161 # Note we NEED to coerce files to a string as we get raw unicode from
162 # cherrypy and we treat files as strings elsewhere in the code.
163 return (str(artifacts).split(',') if artifacts else [],
164 str(files).split(',') if files else [])
165
166
Dan Shi61305df2015-10-26 16:52:35 -0700167def _is_android_build_request(kwargs):
168 """Check if a devserver call is for Android build, based on the arguments.
169
170 This method exams the request's arguments (os_type) to determine if the
171 request is for Android build. If os_type is set to `android`, returns True.
172 If os_type is not set or has other values, returns False.
173
174 Args:
175 kwargs: Keyword arguments for the request.
176
177 Returns:
178 True if the request is for Android build. False otherwise.
179 """
180 os_type = kwargs.get('os_type', None)
181 return os_type == 'android'
182
183
Gabe Black3b567202015-09-23 14:07:59 -0700184def _get_downloader(kwargs):
185 """Returns the downloader based on passed in arguments.
186
187 Args:
Amin Hassani08e42d22019-06-03 00:31:30 -0700188 kwargs: Keyword arguments for the request.
Gabe Black3b567202015-09-23 14:07:59 -0700189 """
190 local_path = kwargs.get('local_path')
191 if local_path:
192 local_path = _canonicalize_local_path(local_path)
193
194 dl = None
195 if local_path:
Prathmesh Prabhu58d08932018-01-19 15:08:19 -0800196 delete_source = _parse_boolean_arg(kwargs, 'delete_source')
197 dl = downloader.LocalDownloader(updater.static_dir, local_path,
198 delete_source=delete_source)
Gabe Black3b567202015-09-23 14:07:59 -0700199
Dan Shi61305df2015-10-26 16:52:35 -0700200 if not _is_android_build_request(kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700201 archive_url = kwargs.get('archive_url')
202 if not archive_url and not local_path:
Amin Hassanid4e35392019-10-03 11:02:44 -0700203 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700204 'Requires archive_url or local_path to be specified.')
Gabe Black3b567202015-09-23 14:07:59 -0700205 if archive_url and local_path:
Amin Hassanid4e35392019-10-03 11:02:44 -0700206 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700207 'archive_url and local_path can not both be specified.')
Gabe Black3b567202015-09-23 14:07:59 -0700208 if not dl:
209 archive_url = _canonicalize_archive_url(archive_url)
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700210 dl = downloader.GoogleStorageDownloader(
211 updater.static_dir, archive_url,
212 downloader.GoogleStorageDownloader.GetBuildIdFromArchiveURL(
213 archive_url))
Gabe Black3b567202015-09-23 14:07:59 -0700214 elif not dl:
215 target = kwargs.get('target', None)
Dan Shi72b16132015-10-08 12:10:33 -0700216 branch = kwargs.get('branch', None)
Dan Shi61305df2015-10-26 16:52:35 -0700217 build_id = kwargs.get('build_id', None)
218 if not target or not branch or not build_id:
Amin Hassanid4e35392019-10-03 11:02:44 -0700219 raise DevServerError('target, branch, build ID must all be specified for '
220 'downloading Android build.')
Dan Shi72b16132015-10-08 12:10:33 -0700221 dl = downloader.AndroidBuildDownloader(updater.static_dir, branch, build_id,
222 target)
Gabe Black3b567202015-09-23 14:07:59 -0700223
224 return dl
225
226
227def _get_downloader_and_factory(kwargs):
228 """Returns the downloader and artifact factory based on passed in arguments.
229
230 Args:
Amin Hassani08e42d22019-06-03 00:31:30 -0700231 kwargs: Keyword arguments for the request.
Gabe Black3b567202015-09-23 14:07:59 -0700232 """
233 artifacts, files = _get_artifacts(kwargs)
234 dl = _get_downloader(kwargs)
235
Achuith Bhandarkar2a1fcd82019-10-17 17:45:58 -0700236 if (isinstance(dl, (downloader.GoogleStorageDownloader,
237 downloader.LocalDownloader))):
Gabe Black3b567202015-09-23 14:07:59 -0700238 factory_class = build_artifact.ChromeOSArtifactFactory
Dan Shi72b16132015-10-08 12:10:33 -0700239 elif isinstance(dl, downloader.AndroidBuildDownloader):
Gabe Black3b567202015-09-23 14:07:59 -0700240 factory_class = build_artifact.AndroidArtifactFactory
241 else:
Amin Hassanid4e35392019-10-03 11:02:44 -0700242 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700243 'Unrecognized value for downloader type: %s' % type(dl))
Gabe Black3b567202015-09-23 14:07:59 -0700244
245 factory = factory_class(dl.GetBuildDir(), artifacts, files, dl.GetBuild())
246
247 return dl, factory
248
249
Scott Zawalski4647ce62012-01-03 17:17:28 -0500250def _LeadingWhiteSpaceCount(string):
251 """Count the amount of leading whitespace in a string.
252
253 Args:
254 string: The string to count leading whitespace in.
Don Garrettf84631a2014-01-07 18:21:26 -0800255
Scott Zawalski4647ce62012-01-03 17:17:28 -0500256 Returns:
257 number of white space chars before characters start.
258 """
Gabe Black3b567202015-09-23 14:07:59 -0700259 matched = re.match(r'^\s+', string)
Scott Zawalski4647ce62012-01-03 17:17:28 -0500260 if matched:
261 return len(matched.group())
262
263 return 0
264
265
266def _PrintDocStringAsHTML(func):
267 """Make a functions docstring somewhat HTML style.
268
269 Args:
270 func: The function to return the docstring from.
Don Garrettf84631a2014-01-07 18:21:26 -0800271
Scott Zawalski4647ce62012-01-03 17:17:28 -0500272 Returns:
273 A string that is somewhat formated for a web browser.
274 """
275 # TODO(scottz): Make this parse Args/Returns in a prettier way.
276 # Arguments could be bolded and indented etc.
277 html_doc = []
278 for line in func.__doc__.splitlines():
279 leading_space = _LeadingWhiteSpaceCount(line)
280 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700281 line = ' ' * leading_space + line
Scott Zawalski4647ce62012-01-03 17:17:28 -0500282
283 html_doc.append('<BR>%s' % line)
284
285 return '\n'.join(html_doc)
286
287
Simran Basief83d6a2014-08-28 14:32:01 -0700288def _GetUpdateTimestampHandler(static_dir):
289 """Returns a handler to update directory staged.timestamp.
290
291 This handler resets the stage.timestamp whenever static content is accessed.
292
293 Args:
294 static_dir: Directory from which static content is being staged.
295
296 Returns:
Amin Hassani08e42d22019-06-03 00:31:30 -0700297 A cherrypy handler to update the timestamp of accessed content.
Simran Basief83d6a2014-08-28 14:32:01 -0700298 """
299 def UpdateTimestampHandler():
300 if not '404' in cherrypy.response.status:
301 build_match = re.match(devserver_constants.STAGED_BUILD_REGEX,
302 cherrypy.request.path_info)
303 if build_match:
304 build_dir = os.path.join(static_dir, build_match.group('build'))
305 downloader.Downloader.TouchTimestampForStaged(build_dir)
306 return UpdateTimestampHandler
307
308
Chris Sosa7c931362010-10-11 19:49:01 -0700309def _GetConfig(options):
310 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800311
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800312 socket_host = '::'
Yu-Ju Hongc8d4af32013-11-12 15:14:26 -0800313 # Fall back to IPv4 when python is not configured with IPv6.
314 if not socket.has_ipv6:
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800315 socket_host = '0.0.0.0'
316
Simran Basief83d6a2014-08-28 14:32:01 -0700317 # Adds the UpdateTimestampHandler to cherrypy's tools. This tools executes
318 # on the on_end_resource hook. This hook is called once processing is
319 # complete and the response is ready to be returned.
320 cherrypy.tools.update_timestamp = cherrypy.Tool(
321 'on_end_resource', _GetUpdateTimestampHandler(options.static_dir))
322
David Riley2fcb0122017-11-02 11:25:39 -0700323 base_config = {
324 'global': {
325 'server.log_request_headers': True,
326 'server.protocol_version': 'HTTP/1.1',
327 'server.socket_host': socket_host,
328 'server.socket_port': int(options.port),
329 'response.timeout': 6000,
330 'request.show_tracebacks': True,
331 'server.socket_timeout': 60,
332 'server.thread_pool': 2,
333 'engine.autoreload.on': False,
334 },
335 '/api': {
336 # Gets rid of cherrypy parsing post file for args.
337 'request.process_request_body': False,
338 },
339 '/build': {
340 'response.timeout': 100000,
341 },
342 '/update': {
343 # Gets rid of cherrypy parsing post file for args.
344 'request.process_request_body': False,
345 'response.timeout': 10000,
346 },
347 # Sets up the static dir for file hosting.
348 '/static': {
349 'tools.staticdir.dir': options.static_dir,
350 'tools.staticdir.on': True,
351 'response.timeout': 10000,
352 'tools.update_timestamp.on': True,
353 },
354 }
Chris Sosa5f118ef2012-07-12 11:37:50 -0700355 if options.production:
Alex Miller93beca52013-07-30 19:25:09 -0700356 base_config['global'].update({'server.thread_pool': 150})
Scott Zawalski1c5e7cd2012-02-27 13:12:52 -0500357
Chris Sosa7c931362010-10-11 19:49:01 -0700358 return base_config
rtc@google.com64244662009-11-12 00:52:08 +0000359
Darin Petkove17164a2010-08-11 13:24:41 -0700360
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700361def _GetRecursiveMemberObject(root, member_list):
362 """Returns an object corresponding to a nested member list.
363
364 Args:
365 root: the root object to search
366 member_list: list of nested members to search
Don Garrettf84631a2014-01-07 18:21:26 -0800367
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700368 Returns:
369 An object corresponding to the member name list; None otherwise.
370 """
371 for member in member_list:
372 next_root = root.__class__.__dict__.get(member)
373 if not next_root:
374 return None
375 root = next_root
376 return root
377
378
379def _IsExposed(name):
380 """Returns True iff |name| has an `exposed' attribute and it is set."""
381 return hasattr(name, 'exposed') and name.exposed
382
383
Congbin Guo6bc32182019-08-20 17:54:30 -0700384def _GetExposedMethod(nested_member):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700385 """Returns a CherryPy-exposed method, if such exists.
386
387 Args:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700388 nested_member: a slash-joined path to the nested member
Don Garrettf84631a2014-01-07 18:21:26 -0800389
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700390 Returns:
Congbin Guo6bc32182019-08-20 17:54:30 -0700391 A function object corresponding to the path defined by |nested_member| from
392 the app root object registered, if the function is exposed; None otherwise.
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700393 """
Congbin Guo6bc32182019-08-20 17:54:30 -0700394 for app in cherrypy.tree.apps.values():
395 # Use the 'index' function doc as the doc of the app.
396 if nested_member == app.script_name.lstrip('/'):
397 nested_member = 'index'
398
399 method = _GetRecursiveMemberObject(app.root, nested_member.split('/'))
400 if method and isinstance(method, types.FunctionType) and _IsExposed(method):
401 return method
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700402
403
Gilad Arnold748c8322012-10-12 09:51:35 -0700404def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700405 """Finds exposed CherryPy methods.
406
407 Args:
408 root: the root object for searching
409 prefix: slash-joined chain of members leading to current object
410 unlisted: URLs to be excluded regardless of their exposed status
Don Garrettf84631a2014-01-07 18:21:26 -0800411
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700412 Returns:
413 List of exposed URLs that are not unlisted.
414 """
415 method_list = []
Congbin Guo6bc32182019-08-20 17:54:30 -0700416 for member in root.__class__.__dict__.keys():
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700417 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 09:51:35 -0700418 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700419 continue
420 member_obj = root.__class__.__dict__[member]
421 if _IsExposed(member_obj):
Amin Hassani08e42d22019-06-03 00:31:30 -0700422 if isinstance(member_obj, types.FunctionType):
Congbin Guo6bc32182019-08-20 17:54:30 -0700423 # Regard the app name as exposed "method" name if it exposed 'index'
424 # function.
425 if prefix and member == 'index':
426 method_list.append(prefix)
427 else:
428 method_list.append(prefixed_member)
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700429 else:
430 method_list += _FindExposedMethods(
431 member_obj, prefixed_member, unlisted)
432 return method_list
433
434
xixuan52c2fba2016-05-20 17:02:48 -0700435def _check_base_args_for_auto_update(kwargs):
xixuanac89ce82016-11-30 16:48:20 -0800436 """Check basic args required for auto-update.
437
438 Args:
439 kwargs: the parameters to be checked.
440
441 Raises:
442 DevServerHTTPError if required parameters don't exist in kwargs.
443 """
xixuan52c2fba2016-05-20 17:02:48 -0700444 if 'host_name' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -0800445 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
446 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -0700447
448 if 'build_name' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -0800449 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
450 KEY_ERROR_MSG % 'build_name')
xixuan52c2fba2016-05-20 17:02:48 -0700451
452
453def _parse_boolean_arg(kwargs, key):
xixuanac89ce82016-11-30 16:48:20 -0800454 """Parse boolean arg from kwargs.
455
456 Args:
457 kwargs: the parameters to be checked.
458 key: the key to be parsed.
459
460 Returns:
461 The boolean value of kwargs[key], or False if key doesn't exist in kwargs.
462
463 Raises:
464 DevServerHTTPError if kwargs[key] is not a boolean variable.
465 """
xixuan52c2fba2016-05-20 17:02:48 -0700466 if key in kwargs:
467 if kwargs[key] == 'True':
468 return True
469 elif kwargs[key] == 'False':
470 return False
471 else:
Amin Hassani722e0962019-11-15 15:45:31 -0800472 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
473 'The value for key %s is not boolean.' % key)
xixuan52c2fba2016-05-20 17:02:48 -0700474 else:
475 return False
476
xixuan447ad9d2017-02-28 14:46:20 -0800477
xixuanac89ce82016-11-30 16:48:20 -0800478def _parse_string_arg(kwargs, key):
479 """Parse string arg from kwargs.
480
481 Args:
482 kwargs: the parameters to be checked.
483 key: the key to be parsed.
484
485 Returns:
486 The string value of kwargs[key], or None if key doesn't exist in kwargs.
487 """
488 if key in kwargs:
489 return kwargs[key]
490 else:
491 return None
492
xixuan447ad9d2017-02-28 14:46:20 -0800493
xixuanac89ce82016-11-30 16:48:20 -0800494def _build_uri_from_build_name(build_name):
495 """Get build url from a given build name.
496
497 Args:
498 build_name: the build name to be parsed, whose format is
499 'board/release_version'.
500
501 Returns:
502 The release_archive_url on Google Storage for this build name.
503 """
Amin Hassani08e42d22019-06-03 00:31:30 -0700504 # TODO(ahassani): This function doesn't seem to be used anywhere since its
505 # previous use of lib.paygen.gspath was broken and it doesn't seem to be
506 # causing any runtime issues. So deprecate this in the future.
507 tokens = build_name.split('/')
508 return 'gs://chromeos-releases/stable-channel/%s/%s' % (tokens[0], tokens[1])
xixuan52c2fba2016-05-20 17:02:48 -0700509
xixuan447ad9d2017-02-28 14:46:20 -0800510
511def _clear_process(host_name, pid):
512 """Clear AU process for given hostname and pid.
513
514 This clear includes:
515 1. kill process if it's alive.
516 2. delete the track status file of this process.
517 3. delete the executing log file of this process.
518
519 Args:
520 host_name: the host to execute auto-update.
521 pid: the background auto-update process id.
522 """
523 if cros_update_progress.IsProcessAlive(pid):
524 os.killpg(int(pid), signal.SIGKILL)
525
526 cros_update_progress.DelTrackStatusFile(host_name, pid)
527 cros_update_progress.DelExecuteLogFile(host_name, pid)
528
529
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700530class ApiRoot(object):
531 """RESTful API for Dev Server information."""
532 exposed = True
533
534 @cherrypy.expose
Gilad Arnold286a0062012-01-12 13:47:02 -0800535 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 11:36:27 -0700536 """Returns a JSON object containing a log of host event.
537
538 Args:
539 ip: address of host whose event log is requested, or `all'
Don Garrettf84631a2014-01-07 18:21:26 -0800540
Gilad Arnold1b908392012-10-05 11:36:27 -0700541 Returns:
Amin Hassani7c447852019-09-26 15:01:48 -0700542 A JSON dictionary containing all or some of the following fields:
Amin Hassanie7ead902019-10-11 16:42:43 -0700543 version: The Chromium OS version the device is running.
544 track: The channel the device is running on.
545 board: The device's board.
546 event_result: The event result of Omaha request.
547 event_type: The event type of Omaha request.
548 previous_version: The Chromium OS version we updated and rebooted from.
549 timestamp: The timestamp the event was received.
Amin Hassani7c447852019-09-26 15:01:48 -0700550 See the OmahaEvent class in update_engine/omaha_request_action.h for
551 event type and status code definitions. If the ip does not exist an empty
552 string is returned.
Gilad Arnold1b908392012-10-05 11:36:27 -0700553
554 Example URL:
555 http://myhost/api/hostlog?ip=192.168.1.5
556 """
Gilad Arnold286a0062012-01-12 13:47:02 -0800557 return updater.HandleHostLogPing(ip)
558
559 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800560 def fileinfo(self, *args):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700561 """Returns information about a given staged file.
562
563 Args:
Don Garrettf84631a2014-01-07 18:21:26 -0800564 args: path to the file inside the server's static staging directory
565
Gilad Arnold55a2a372012-10-02 09:46:32 -0700566 Returns:
567 A JSON encoded dictionary with information about the said file, which may
568 contain the following keys/values:
Gilad Arnold1b908392012-10-05 11:36:27 -0700569 size (int): the file size in bytes
Gilad Arnold1b908392012-10-05 11:36:27 -0700570 sha256 (string): a base64 encoded SHA256 hash
571
572 Example URL:
573 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 09:46:32 -0700574 """
Amin Hassani28df4212019-10-28 10:16:50 -0700575 # TODO(ahassani): A better way of doing this is to just return the the
576 # content of the payloads' property file instead. That has all this info
577 # except that the key for sha256 is 'sha256_hex', but still base64 encdoed.
578
Don Garrettf84631a2014-01-07 18:21:26 -0800579 file_path = os.path.join(updater.static_dir, *args)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700580 if not os.path.exists(file_path):
Amin Hassanid4e35392019-10-03 11:02:44 -0700581 raise DevServerError('file not found: %s' % file_path)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700582 try:
583 file_size = os.path.getsize(file_path)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700584 file_sha256 = common_util.GetFileSha256(file_path)
Amin Hassani469f5702019-10-21 15:35:06 -0700585 except os.error as e:
Amin Hassanid4e35392019-10-03 11:02:44 -0700586 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700587 'failed to get info for file %s: %s' % (file_path, e))
Gilad Arnolde74b3812013-04-22 11:27:38 -0700588
Gilad Arnolde74b3812013-04-22 11:27:38 -0700589 return json.dumps({
Amin Hassani28df4212019-10-28 10:16:50 -0700590 'size': file_size,
591 'sha256': file_sha256,
592 }, sort_keys=True)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700593
Chris Sosa76e44b92013-01-31 12:11:38 -0800594
David Rochberg7c79a812011-01-19 14:24:45 -0500595class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700596 """The Root Class for the Dev Server.
597
598 CherryPy works as follows:
599 For each method in this class, cherrpy interprets root/path
600 as a call to an instance of DevServerRoot->method_name. For example,
601 a call to http://myhost/build will call build. CherryPy automatically
602 parses http args and places them as keyword arguments in each method.
603 For paths http://myhost/update/dir1/dir2, you can use *args so that
604 cherrypy uses the update method and puts the extra paths in args.
605 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700606 # Method names that should not be listed on the index page.
607 _UNLISTED_METHODS = ['index', 'doc']
608
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700609 api = ApiRoot()
Chris Sosa7c931362010-10-11 19:49:01 -0700610
Dan Shi59ae7092013-06-04 14:37:27 -0700611 # Number of threads that devserver is staging images.
612 _staging_thread_count = 0
613 # Lock used to lock increasing/decreasing count.
614 _staging_thread_count_lock = threading.Lock()
615
joychen3cb228e2013-06-12 12:13:13 -0700616 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700617 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800618 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 12:13:13 -0700619 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 14:24:45 -0500620
Congbin Guo3afae6c2019-08-13 16:29:42 -0700621 @property
622 def staging_thread_count(self):
623 """Get the staging thread count."""
624 return self._staging_thread_count
Dan Shiafd0e492015-05-27 14:23:51 -0700625
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700626 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500627 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700628 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700629 import builder
630 if self._builder is None:
631 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500632 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700633
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700634 @cherrypy.expose
Dan Shif8eb0d12013-08-01 17:52:06 -0700635 def is_staged(self, **kwargs):
636 """Check if artifacts have been downloaded.
637
Congbin Guo3afae6c2019-08-13 16:29:42 -0700638 Examples:
639 To check if autotest and test_suites are staged:
640 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
641 artifacts=autotest,test_suites
642
Amin Hassani08e42d22019-06-03 00:31:30 -0700643 Args:
Chris Sosa6b0c6172013-08-05 17:01:33 -0700644 async: True to return without waiting for download to complete.
645 artifacts: Comma separated list of named artifacts to download.
646 These are defined in artifact_info and have their implementation
647 in build_artifact.py.
648 files: Comma separated list of file artifacts to stage. These
649 will be available as is in the corresponding static directory with no
650 custom post-processing.
651
Congbin Guo3afae6c2019-08-13 16:29:42 -0700652 Returns:
653 True of all artifacts are staged.
Dan Shif8eb0d12013-08-01 17:52:06 -0700654 """
Gabe Black3b567202015-09-23 14:07:59 -0700655 dl, factory = _get_downloader_and_factory(kwargs)
Aviv Keshet57d18172016-06-18 20:39:09 -0700656 response = str(dl.IsStaged(factory))
657 _Log('Responding to is_staged %s request with %r', kwargs, response)
658 return response
Dan Shi59ae7092013-06-04 14:37:27 -0700659
Chris Sosa76e44b92013-01-31 12:11:38 -0800660 @cherrypy.expose
Prashanth Ba06d2d22014-03-07 15:35:19 -0800661 def list_image_dir(self, **kwargs):
662 """Take an archive url and list the contents in its staged directory.
663
Amin Hassani08e42d22019-06-03 00:31:30 -0700664 Examples:
Prashanth Ba06d2d22014-03-07 15:35:19 -0800665 To list the contents of where this devserver should have staged
666 gs://image-archive/<board>-release/<build> call:
667 http://devserver_url:<port>/list_image_dir?archive_url=<gs://..>
668
Congbin Guo3afae6c2019-08-13 16:29:42 -0700669 Args:
670 archive_url: Google Storage URL for the build.
671
Prashanth Ba06d2d22014-03-07 15:35:19 -0800672 Returns:
673 A string with information about the contents of the image directory.
674 """
Gabe Black3b567202015-09-23 14:07:59 -0700675 dl = _get_downloader(kwargs)
Prashanth Ba06d2d22014-03-07 15:35:19 -0800676 try:
Gabe Black3b567202015-09-23 14:07:59 -0700677 image_dir_contents = dl.ListBuildDir()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800678 except build_artifact.ArtifactDownloadError as e:
679 return 'Cannot list the contents of staged artifacts. %s' % e
680 if not image_dir_contents:
Gabe Black3b567202015-09-23 14:07:59 -0700681 return '%s has not been staged on this devserver.' % dl.DescribeSource()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800682 return image_dir_contents
683
684 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800685 def stage(self, **kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700686 """Downloads and caches build artifacts.
Chris Sosa76e44b92013-01-31 12:11:38 -0800687
Gabe Black3b567202015-09-23 14:07:59 -0700688 Downloads and caches build artifacts, possibly from a Google Storage URL,
Dan Shi72b16132015-10-08 12:10:33 -0700689 or from Android's build server. Returns once these have been downloaded
Gabe Black3b567202015-09-23 14:07:59 -0700690 on the devserver. A call to this will attempt to cache non-specified
691 artifacts in the background for the given from the given URL following
692 the principle of spatial locality. Spatial locality of different
Chris Sosa76e44b92013-01-31 12:11:38 -0800693 artifacts is explicitly defined in the build_artifact module.
694
695 These artifacts will then be available from the static/ sub-directory of
696 the devserver.
697
Amin Hassani08e42d22019-06-03 00:31:30 -0700698 Examples:
Chris Sosa76e44b92013-01-31 12:11:38 -0800699 To download the autotest and test suites tarballs:
700 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
701 artifacts=autotest,test_suites
702 To download the full update payload:
703 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
704 artifacts=full_payload
Chris Sosa6b0c6172013-08-05 17:01:33 -0700705 To download just a file called blah.bin:
706 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
707 files=blah.bin
Chris Sosa76e44b92013-01-31 12:11:38 -0800708
709 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 16:39:34 -0700710 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 12:11:38 -0800711
712 Note for this example, relative path is the archive_url stripped of its
713 basename i.e. path/ in the examples above. Specific example:
714
715 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
716
717 Will get staged to:
718
joychened64b222013-06-21 16:39:34 -0700719 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Congbin Guo3afae6c2019-08-13 16:29:42 -0700720
721 Args:
722 archive_url: Google Storage URL for the build.
723 local_path: Local path for the build.
724 delete_source: Only meaningful with local_path. bool to indicate if the
725 source files should be deleted. This is especially useful when staging
726 a file locally in resource constrained environments as it allows us to
727 move the relevant files locally instead of copying them.
728 async: True to return without waiting for download to complete.
729 artifacts: Comma separated list of named artifacts to download.
730 These are defined in artifact_info and have their implementation
731 in build_artifact.py.
732 files: Comma separated list of files to stage. These
733 will be available as is in the corresponding static directory with no
734 custom post-processing.
735 clean: True to remove any previously staged artifacts first.
Chris Sosa76e44b92013-01-31 12:11:38 -0800736 """
Gabe Black3b567202015-09-23 14:07:59 -0700737 dl, factory = _get_downloader_and_factory(kwargs)
738
Dan Shi59ae7092013-06-04 14:37:27 -0700739 with DevServerRoot._staging_thread_count_lock:
740 DevServerRoot._staging_thread_count += 1
741 try:
Laurence Goodbyf5c958d2016-01-14 18:23:56 -0800742 boolean_string = kwargs.get('clean')
743 clean = xbuddy.XBuddy.ParseBoolean(boolean_string)
744 if clean and os.path.exists(dl.GetBuildDir()):
745 _Log('Removing %s' % dl.GetBuildDir())
746 shutil.rmtree(dl.GetBuildDir())
Achuith Bhandarkar2a1fcd82019-10-17 17:45:58 -0700747 is_async = kwargs.get('async', False)
748 dl.Download(factory, is_async=is_async)
Dan Shi59ae7092013-06-04 14:37:27 -0700749 finally:
750 with DevServerRoot._staging_thread_count_lock:
751 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800752 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700753
754 @cherrypy.expose
xixuan52c2fba2016-05-20 17:02:48 -0700755 def cros_au(self, **kwargs):
756 """Auto-update a CrOS DUT.
757
758 Args:
759 kwargs:
760 host_name: the hostname of the DUT to auto-update.
761 build_name: the build name for update the DUT.
762 force_update: Force an update even if the version installed is the
763 same. Default: False.
764 full_update: If True, do not run stateful update, directly force a full
765 reimage. If False, try stateful update first if the dut is already
766 installed with the same version.
767 async: Whether the auto_update function is ran in the background.
David Rileyee75de22017-11-02 10:48:15 -0700768 quick_provision: Whether the quick provision path is attempted first.
xixuan52c2fba2016-05-20 17:02:48 -0700769
770 Returns:
771 A tuple includes two elements:
772 a boolean variable represents whether the auto-update process is
773 successfully started.
774 an integer represents the background auto-update process id.
775 """
776 _check_base_args_for_auto_update(kwargs)
777
778 host_name = kwargs['host_name']
779 build_name = kwargs['build_name']
780 force_update = _parse_boolean_arg(kwargs, 'force_update')
781 full_update = _parse_boolean_arg(kwargs, 'full_update')
Achuith Bhandarkar2a1fcd82019-10-17 17:45:58 -0700782 is_async = _parse_boolean_arg(kwargs, 'async')
xixuanac89ce82016-11-30 16:48:20 -0800783 original_build = _parse_string_arg(kwargs, 'original_build')
David Haddock90e49442017-04-07 19:14:09 -0700784 payload_filename = _parse_string_arg(kwargs, 'payload_filename')
David Haddock20559612017-06-28 22:15:08 -0700785 clobber_stateful = _parse_boolean_arg(kwargs, 'clobber_stateful')
David Rileyee75de22017-11-02 10:48:15 -0700786 quick_provision = _parse_boolean_arg(kwargs, 'quick_provision')
787
788 devserver_url = updater.GetDevserverUrl()
789 static_url = updater.GetStaticUrl()
xixuan52c2fba2016-05-20 17:02:48 -0700790
Achuith Bhandarkar2a1fcd82019-10-17 17:45:58 -0700791 if is_async:
Amin Hassani469f5702019-10-21 15:35:06 -0700792 # Command of running auto-update.
Amin Hassanie427e212019-10-28 11:04:27 -0700793 cmd = ['cros_update', '--hostname', host_name, '-b', build_name,
794 '--static_dir', updater.static_dir]
xixuanac89ce82016-11-30 16:48:20 -0800795
796 # The original_build's format is like: link/3428.210.0
797 # The corresponding release_archive_url's format is like:
798 # gs://chromeos-releases/stable-channel/link/3428.210.0
799 if original_build:
800 release_archive_url = _build_uri_from_build_name(original_build)
801 # First staging the stateful.tgz synchronousely.
Amin Hassani469f5702019-10-21 15:35:06 -0700802 self.stage(files='stateful.tgz', is_async=False,
xixuanac89ce82016-11-30 16:48:20 -0800803 archive_url=release_archive_url)
Amin Hassani469f5702019-10-21 15:35:06 -0700804 cmd += ['--original_build', original_build]
xixuanac89ce82016-11-30 16:48:20 -0800805
xixuan52c2fba2016-05-20 17:02:48 -0700806 if force_update:
Amin Hassani469f5702019-10-21 15:35:06 -0700807 cmd += ['--force_update']
xixuan52c2fba2016-05-20 17:02:48 -0700808
809 if full_update:
Amin Hassani469f5702019-10-21 15:35:06 -0700810 cmd += ['--full_update']
xixuan52c2fba2016-05-20 17:02:48 -0700811
David Haddock90e49442017-04-07 19:14:09 -0700812 if payload_filename:
Amin Hassani469f5702019-10-21 15:35:06 -0700813 cmd += ['--payload_filename', payload_filename]
David Haddock90e49442017-04-07 19:14:09 -0700814
David Haddock20559612017-06-28 22:15:08 -0700815 if clobber_stateful:
Amin Hassani469f5702019-10-21 15:35:06 -0700816 cmd += ['--clobber_stateful']
David Haddock20559612017-06-28 22:15:08 -0700817
David Rileyee75de22017-11-02 10:48:15 -0700818 if quick_provision:
Amin Hassani469f5702019-10-21 15:35:06 -0700819 cmd += ['--quick_provision']
David Rileyee75de22017-11-02 10:48:15 -0700820
821 if devserver_url:
Amin Hassani469f5702019-10-21 15:35:06 -0700822 cmd += ['--devserver_url', devserver_url]
David Rileyee75de22017-11-02 10:48:15 -0700823
824 if static_url:
Amin Hassani469f5702019-10-21 15:35:06 -0700825 cmd += ['--static_url', static_url]
David Rileyee75de22017-11-02 10:48:15 -0700826
Amin Hassani78520ae2019-10-29 13:26:51 -0700827 p = subprocess.Popen(cmd, preexec_fn=os.setsid)
xixuan2a0970a2016-08-10 12:12:44 -0700828 pid = os.getpgid(p.pid)
xixuan52c2fba2016-05-20 17:02:48 -0700829
830 # Pre-write status in the track_status_file before the first call of
831 # 'get_au_status' to make sure that the track_status_file exists.
xixuan2a0970a2016-08-10 12:12:44 -0700832 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -0700833 progress_tracker.WriteStatus('CrOS update is just started.')
834
xixuan2a0970a2016-08-10 12:12:44 -0700835 return json.dumps((True, pid))
xixuan52c2fba2016-05-20 17:02:48 -0700836 else:
837 cros_update_trigger = cros_update.CrOSUpdateTrigger(
xixuanac89ce82016-11-30 16:48:20 -0800838 host_name, build_name, updater.static_dir, force_update=force_update,
David Rileyee75de22017-11-02 10:48:15 -0700839 full_update=full_update, original_build=original_build,
Amin Hassani78520ae2019-10-29 13:26:51 -0700840 payload_filename=payload_filename, quick_provision=quick_provision,
841 devserver_url=devserver_url, static_url=static_url)
xixuan52c2fba2016-05-20 17:02:48 -0700842 cros_update_trigger.TriggerAU()
xixuan27d50442017-08-09 10:38:25 -0700843 return json.dumps((True, -1))
xixuan52c2fba2016-05-20 17:02:48 -0700844
845 @cherrypy.expose
846 def get_au_status(self, **kwargs):
847 """Check if the auto-update task is finished.
848
849 It handles 4 cases:
850 1. If an error exists in the track_status_file, delete the track file and
851 raise it.
852 2. If cros-update process is finished, delete the file and return the
853 success result.
854 3. If the process is not running, delete the track file and raise an error
855 about 'the process is terminated due to unknown reason'.
856 4. If the track_status_file does not exist, kill the process if it exists,
857 and raise the IOError.
858
859 Args:
860 kwargs:
861 host_name: the hostname of the DUT to auto-update.
862 pid: the background process id of cros-update.
863
864 Returns:
xixuan28d99072016-10-06 12:24:16 -0700865 A dict with three elements:
xixuan52c2fba2016-05-20 17:02:48 -0700866 a boolean variable represents whether the auto-update process is
867 finished.
868 a string represents the current auto-update process status.
869 For example, 'Transfer Devserver/Stateful Update Package'.
xixuan28d99072016-10-06 12:24:16 -0700870 a detailed error message paragraph if there exists an Auto-Update
871 error, in which the last line shows the main exception. Empty
872 string otherwise.
xixuan52c2fba2016-05-20 17:02:48 -0700873 """
874 if 'host_name' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -0800875 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
876 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -0700877
878 if 'pid' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -0800879 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
880 KEY_ERROR_MSG % 'pid')
xixuan52c2fba2016-05-20 17:02:48 -0700881
882 host_name = kwargs['host_name']
883 pid = kwargs['pid']
884 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
885
xixuan28d99072016-10-06 12:24:16 -0700886 result_dict = {'finished': False, 'status': '', 'detailed_error_msg': ''}
xixuan52c2fba2016-05-20 17:02:48 -0700887 try:
888 result = progress_tracker.ReadStatus()
889 if result.startswith(cros_update_progress.ERROR_TAG):
xixuan28d99072016-10-06 12:24:16 -0700890 result_dict['detailed_error_msg'] = result[len(
891 cros_update_progress.ERROR_TAG):]
xixuan28681fd2016-11-23 11:13:56 -0800892 elif result == cros_update_progress.FINISHED:
xixuan28d99072016-10-06 12:24:16 -0700893 result_dict['finished'] = True
894 result_dict['status'] = result
xixuan28681fd2016-11-23 11:13:56 -0800895 elif not cros_update_progress.IsProcessAlive(pid):
xixuan28d99072016-10-06 12:24:16 -0700896 result_dict['detailed_error_msg'] = (
897 'Cros_update process terminated midway due to unknown reason. '
898 'Last update status was %s' % result)
xixuan28681fd2016-11-23 11:13:56 -0800899 else:
900 result_dict['status'] = result
901 except IOError as e:
902 if pid and cros_update_progress.IsProcessAlive(pid):
xixuan2a0970a2016-08-10 12:12:44 -0700903 os.killpg(int(pid), signal.SIGKILL)
xixuan52c2fba2016-05-20 17:02:48 -0700904
xixuan28681fd2016-11-23 11:13:56 -0800905 result_dict['detailed_error_msg'] = str(e)
906
907 return json.dumps(result_dict)
xixuan52c2fba2016-05-20 17:02:48 -0700908
909 @cherrypy.expose
David Riley6d5fca02017-10-31 10:35:47 -0700910 def post_au_status(self, status, **kwargs):
911 """Updates the status of an auto-update task.
912
913 Callers will need to POST to this URL with a body of MIME-type
914 "multipart/form-data".
915 The body should include a single argument, 'status', containing the
916 AU status to record.
917
918 Args:
919 status: The updated status.
920 kwargs:
921 host_name: the hostname of the DUT to auto-update.
922 pid: the background process id of cros-update.
923 """
924 if 'host_name' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -0800925 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
926 KEY_ERROR_MSG % 'host_name')
David Riley6d5fca02017-10-31 10:35:47 -0700927
928 if 'pid' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -0800929 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
930 KEY_ERROR_MSG % 'pid')
David Riley6d5fca02017-10-31 10:35:47 -0700931
932 host_name = kwargs['host_name']
933 pid = kwargs['pid']
David Riley3cea2582017-11-24 22:03:01 -0800934 status = status.rstrip()
935 _Log('Recording status for %s (%s): %s' % (host_name, pid, status))
David Riley6d5fca02017-10-31 10:35:47 -0700936 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
937
David Riley3cea2582017-11-24 22:03:01 -0800938 progress_tracker.WriteStatus(status)
David Riley6d5fca02017-10-31 10:35:47 -0700939
940 return 'True'
941
942 @cherrypy.expose
xixuan52c2fba2016-05-20 17:02:48 -0700943 def handler_cleanup(self, **kwargs):
xixuan3bc974e2016-10-18 17:21:43 -0700944 """Clean track status log and temp directory for CrOS auto-update process.
xixuan52c2fba2016-05-20 17:02:48 -0700945
946 Args:
947 kwargs:
948 host_name: the hostname of the DUT to auto-update.
949 pid: the background process id of cros-update.
950 """
951 if 'host_name' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -0800952 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
953 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -0700954
955 if 'pid' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -0800956 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
957 KEY_ERROR_MSG % 'pid')
xixuan52c2fba2016-05-20 17:02:48 -0700958
959 host_name = kwargs['host_name']
960 pid = kwargs['pid']
961 cros_update_progress.DelTrackStatusFile(host_name, pid)
xixuan3bc974e2016-10-18 17:21:43 -0700962 cros_update_progress.DelAUTempDirectory(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -0700963
964 @cherrypy.expose
965 def kill_au_proc(self, **kwargs):
966 """Kill CrOS auto-update process using given process id.
967
968 Args:
969 kwargs:
970 host_name: Kill all the CrOS auto-update process of this host.
971
972 Returns:
973 True if all processes are killed properly.
974 """
975 if 'host_name' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -0800976 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
977 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -0700978
xixuan447ad9d2017-02-28 14:46:20 -0800979 cur_pid = kwargs.get('pid')
980
xixuan52c2fba2016-05-20 17:02:48 -0700981 host_name = kwargs['host_name']
xixuan3bc974e2016-10-18 17:21:43 -0700982 track_log_list = cros_update_progress.GetAllTrackStatusFileByHostName(
983 host_name)
xixuan52c2fba2016-05-20 17:02:48 -0700984 for log in track_log_list:
985 # The track log's full path is: path/host_name_pid.log
986 # Use splitext to remove file extension, then parse pid from the
987 # filename.
Congbin Guo3afae6c2019-08-13 16:29:42 -0700988 pid = os.path.splitext(os.path.basename(log))[0][len(host_name) + 1:]
xixuan447ad9d2017-02-28 14:46:20 -0800989 _clear_process(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -0700990
xixuan447ad9d2017-02-28 14:46:20 -0800991 if cur_pid:
992 _clear_process(host_name, cur_pid)
xixuan52c2fba2016-05-20 17:02:48 -0700993
994 return 'True'
995
996 @cherrypy.expose
997 def collect_cros_au_log(self, **kwargs):
998 """Collect CrOS auto-update log.
999
1000 Args:
1001 kwargs:
1002 host_name: the hostname of the DUT to auto-update.
1003 pid: the background process id of cros-update.
1004
1005 Returns:
David Haddock9f459632017-05-11 14:45:46 -07001006 A dictionary containing the execute log file and any hostlog files.
xixuan52c2fba2016-05-20 17:02:48 -07001007 """
1008 if 'host_name' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -08001009 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
1010 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -07001011
1012 if 'pid' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -08001013 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
1014 KEY_ERROR_MSG % 'pid')
xixuan52c2fba2016-05-20 17:02:48 -07001015
1016 host_name = kwargs['host_name']
1017 pid = kwargs['pid']
xixuan3bc974e2016-10-18 17:21:43 -07001018
1019 # Fetch the execute log recorded by cros_update_progress.
xixuan1bbfaba2016-10-13 17:53:22 -07001020 au_log = cros_update_progress.ReadExecuteLogFile(host_name, pid)
1021 cros_update_progress.DelExecuteLogFile(host_name, pid)
David Haddock9f459632017-05-11 14:45:46 -07001022 # Fetch the cros_au host_logs if they exist
1023 au_hostlogs = cros_update_progress.ReadAUHostLogFiles(host_name, pid)
1024 return json.dumps({'cros_au_log': au_log, 'host_logs': au_hostlogs})
xixuan1bbfaba2016-10-13 17:53:22 -07001025
xixuan52c2fba2016-05-20 17:02:48 -07001026 @cherrypy.expose
Dan Shi2f136862016-02-11 15:38:38 -08001027 def locate_file(self, **kwargs):
1028 """Get the path to the given file name.
1029
1030 This method looks up the given file name inside specified build artifacts.
1031 One use case is to help caller to locate an apk file inside a build
1032 artifact. The location of the apk file could be different based on the
1033 branch and target.
1034
1035 Args:
1036 file_name: Name of the file to look for.
1037 artifacts: A list of artifact names to search for the file.
1038
1039 Returns:
1040 Path to the file with the given name. It's relative to the folder for the
1041 build, e.g., DATA/priv-app/sl4a/sl4a.apk
Dan Shi2f136862016-02-11 15:38:38 -08001042 """
1043 dl, _ = _get_downloader_and_factory(kwargs)
1044 try:
Joe Brennan1691f8e2017-03-15 15:53:36 -07001045 file_name = kwargs['file_name']
Dan Shi2f136862016-02-11 15:38:38 -08001046 artifacts = kwargs['artifacts']
1047 except KeyError:
Amin Hassanid4e35392019-10-03 11:02:44 -07001048 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -07001049 '`file_name` and `artifacts` are required to search '
1050 'for a file in build artifacts.')
Dan Shi2f136862016-02-11 15:38:38 -08001051 build_path = dl.GetBuildDir()
1052 for artifact in artifacts:
1053 # Get the unzipped folder of the artifact. If it's not defined in
1054 # ARTIFACT_UNZIP_FOLDER_MAP, assume the files are unzipped to the build
1055 # directory directly.
1056 folder = artifact_info.ARTIFACT_UNZIP_FOLDER_MAP.get(artifact, '')
1057 artifact_path = os.path.join(build_path, folder)
1058 for root, _, filenames in os.walk(artifact_path):
Joe Brennan1691f8e2017-03-15 15:53:36 -07001059 if file_name in set([f for f in filenames]):
Dan Shi2f136862016-02-11 15:38:38 -08001060 return os.path.relpath(os.path.join(root, file_name), build_path)
Amin Hassanid4e35392019-10-03 11:02:44 -07001061 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -07001062 'File `%s` can not be found in artifacts: %s' % (file_name, artifacts))
Dan Shi2f136862016-02-11 15:38:38 -08001063
1064 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -08001065 def setup_telemetry(self, **kwargs):
1066 """Extracts and sets up telemetry
1067
1068 This method goes through the telemetry deps packages, and stages them on
1069 the devserver to be used by the drones and the telemetry tests.
1070
1071 Args:
1072 archive_url: Google Storage URL for the build.
1073
1074 Returns:
1075 Path to the source folder for the telemetry codebase once it is staged.
1076 """
Gabe Black3b567202015-09-23 14:07:59 -07001077 dl = _get_downloader(kwargs)
Simran Basi4baad082013-02-14 13:39:18 -08001078
Gabe Black3b567202015-09-23 14:07:59 -07001079 build_path = dl.GetBuildDir()
Simran Basi4baad082013-02-14 13:39:18 -08001080 deps_path = os.path.join(build_path, 'autotest/packages')
1081 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
1082 src_folder = os.path.join(telemetry_path, 'src')
1083
1084 with self._telemetry_lock_dict.lock(telemetry_path):
1085 if os.path.exists(src_folder):
1086 # Telemetry is already fully stage return
1087 return src_folder
1088
1089 common_util.MkDirP(telemetry_path)
1090
1091 # Copy over the required deps tar balls to the telemetry directory.
1092 for dep in TELEMETRY_DEPS:
1093 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -07001094 if not os.path.exists(dep_path):
1095 # This dep does not exist (could be new), do not extract it.
1096 continue
Simran Basi4baad082013-02-14 13:39:18 -08001097 try:
1098 common_util.ExtractTarball(dep_path, telemetry_path)
1099 except common_util.CommonUtilError as e:
1100 shutil.rmtree(telemetry_path)
Amin Hassanid4e35392019-10-03 11:02:44 -07001101 raise DevServerError(str(e))
Simran Basi4baad082013-02-14 13:39:18 -08001102
1103 # By default all the tarballs extract to test_src but some parts of
1104 # the telemetry code specifically hardcoded to exist inside of 'src'.
1105 test_src = os.path.join(telemetry_path, 'test_src')
1106 try:
1107 shutil.move(test_src, src_folder)
1108 except shutil.Error:
1109 # This can occur if src_folder already exists. Remove and retry move.
1110 shutil.rmtree(src_folder)
Amin Hassanid4e35392019-10-03 11:02:44 -07001111 raise DevServerError(
Gabe Black3b567202015-09-23 14:07:59 -07001112 'Failure in telemetry setup for build %s. Appears that the '
1113 'test_src to src move failed.' % dl.GetBuild())
Simran Basi4baad082013-02-14 13:39:18 -08001114
1115 return src_folder
1116
1117 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -08001118 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -07001119 """Symbolicates a minidump using pre-downloaded symbols, returns it.
1120
1121 Callers will need to POST to this URL with a body of MIME-type
1122 "multipart/form-data".
1123 The body should include a single argument, 'minidump', containing the
1124 binary-formatted minidump to symbolicate.
1125
Chris Masone816e38c2012-05-02 12:22:36 -07001126 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -08001127 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -07001128 minidump: The binary minidump file to symbolicate.
1129 """
Chris Sosa76e44b92013-01-31 12:11:38 -08001130 # Ensure the symbols have been staged.
Dan Shif08fe492016-10-04 14:39:25 -07001131 # Try debug.tar.xz first, then debug.tgz
1132 for artifact in (artifact_info.SYMBOLS_ONLY, artifact_info.SYMBOLS):
1133 kwargs['artifacts'] = artifact
1134 dl = _get_downloader(kwargs)
1135
1136 try:
1137 if self.stage(**kwargs) == 'Success':
1138 break
1139 except build_artifact.ArtifactDownloadError:
1140 continue
1141 else:
Amin Hassanid4e35392019-10-03 11:02:44 -07001142 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -07001143 'Failed to stage symbols for %s' % dl.DescribeSource())
Chris Sosa76e44b92013-01-31 12:11:38 -08001144
Chris Masone816e38c2012-05-02 12:22:36 -07001145 to_return = ''
1146 with tempfile.NamedTemporaryFile() as local:
1147 while True:
1148 data = minidump.file.read(8192)
1149 if not data:
1150 break
1151 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -08001152
Chris Masone816e38c2012-05-02 12:22:36 -07001153 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -08001154
Gabe Black3b567202015-09-23 14:07:59 -07001155 symbols_directory = os.path.join(dl.GetBuildDir(), 'debug', 'breakpad')
Chris Sosa76e44b92013-01-31 12:11:38 -08001156
xixuanab744382017-04-27 10:41:27 -07001157 # The location of minidump_stackwalk is defined in chromeos-admin.
Chris Sosa76e44b92013-01-31 12:11:38 -08001158 stackwalk = subprocess.Popen(
xixuanab744382017-04-27 10:41:27 -07001159 ['/usr/local/bin/minidump_stackwalk', local.name, symbols_directory],
Chris Sosa76e44b92013-01-31 12:11:38 -08001160 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1161
Chris Masone816e38c2012-05-02 12:22:36 -07001162 to_return, error_text = stackwalk.communicate()
1163 if stackwalk.returncode != 0:
Amin Hassanid4e35392019-10-03 11:02:44 -07001164 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -07001165 "Can't generate stack trace: %s (rc=%d)" % (error_text,
1166 stackwalk.returncode))
Chris Masone816e38c2012-05-02 12:22:36 -07001167
1168 return to_return
1169
1170 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -08001171 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 15:31:36 -04001172 """Return a string representing the latest build for a given target.
1173
1174 Args:
1175 target: The build target, typically a combination of the board and the
1176 type of build e.g. x86-mario-release.
1177 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
1178 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-07 18:21:26 -08001179
Scott Zawalski16954532012-03-20 15:31:36 -04001180 Returns:
1181 A string representation of the latest build if one exists, i.e.
1182 R19-1993.0.0-a1-b1480.
1183 An empty string if no latest could be found.
1184 """
Don Garrettf84631a2014-01-07 18:21:26 -08001185 if not kwargs:
Scott Zawalski16954532012-03-20 15:31:36 -04001186 return _PrintDocStringAsHTML(self.latestbuild)
1187
Don Garrettf84631a2014-01-07 18:21:26 -08001188 if 'target' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -08001189 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
1190 'Error: target= is required!')
Dan Shi61305df2015-10-26 16:52:35 -07001191
1192 if _is_android_build_request(kwargs):
1193 branch = kwargs.get('branch', None)
1194 target = kwargs.get('target', None)
1195 if not target or not branch:
Amin Hassanid4e35392019-10-03 11:02:44 -07001196 raise DevServerError('Both target and branch must be specified to query'
1197 ' for the latest Android build.')
Dan Shi61305df2015-10-26 16:52:35 -07001198 return android_build.BuildAccessor.GetLatestBuildID(target, branch)
1199
Scott Zawalski16954532012-03-20 15:31:36 -04001200 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001201 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-07 18:21:26 -08001202 updater.static_dir, kwargs['target'],
1203 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -07001204 except common_util.CommonUtilError as errmsg:
Amin Hassani722e0962019-11-15 15:45:31 -08001205 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
1206 str(errmsg))
Scott Zawalski16954532012-03-20 15:31:36 -04001207
1208 @cherrypy.expose
xixuan7efd0002016-04-14 15:34:01 -07001209 def list_suite_controls(self, **kwargs):
1210 """Return a list of contents of all known control files.
1211
1212 Example URL:
1213 To List all control files' content:
1214 http://dev-server/list_suite_controls?suite_name=bvt&
1215 build=daisy_spring-release/R29-4279.0.0
1216
1217 Args:
1218 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
1219 suite_name: List the control files belonging to that suite.
1220
1221 Returns:
Dan Shia1cd6522016-04-18 16:07:21 -07001222 A dictionary of all control files's path to its content for given suite.
xixuan7efd0002016-04-14 15:34:01 -07001223 """
1224 if not kwargs:
1225 return _PrintDocStringAsHTML(self.controlfiles)
1226
1227 if 'build' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -08001228 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
1229 'Error: build= is required!')
xixuan7efd0002016-04-14 15:34:01 -07001230
1231 if 'suite_name' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -08001232 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
1233 'Error: suite_name= is required!')
xixuan7efd0002016-04-14 15:34:01 -07001234
1235 control_file_list = [
1236 line.rstrip() for line in common_util.GetControlFileListForSuite(
1237 updater.static_dir, kwargs['build'],
1238 kwargs['suite_name']).splitlines()]
1239
Dan Shia1cd6522016-04-18 16:07:21 -07001240 control_file_content_dict = {}
xixuan7efd0002016-04-14 15:34:01 -07001241 for control_path in control_file_list:
Dan Shia1cd6522016-04-18 16:07:21 -07001242 control_file_content_dict[control_path] = (common_util.GetControlFile(
xixuan7efd0002016-04-14 15:34:01 -07001243 updater.static_dir, kwargs['build'], control_path))
1244
Dan Shia1cd6522016-04-18 16:07:21 -07001245 return json.dumps(control_file_content_dict)
xixuan7efd0002016-04-14 15:34:01 -07001246
1247 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -08001248 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 17:17:28 -05001249 """Return a control file or a list of all known control files.
1250
1251 Example URL:
1252 To List all control files:
beepsbd337242013-07-09 22:44:06 -07001253 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
1254 To List all control files for, say, the bvt suite:
1255 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -05001256 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -05001257 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 -05001258
1259 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -05001260 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -05001261 control_path: If you want the contents of a control file set this
1262 to the path. E.g. client/site_tests/sleeptest/control
1263 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -07001264 suite_name: If control_path is not specified but a suite_name is
1265 specified, list the control files belonging to that suite instead of
1266 all control files. The empty string for suite_name will list all control
1267 files for the build.
Don Garrettf84631a2014-01-07 18:21:26 -08001268
Scott Zawalski4647ce62012-01-03 17:17:28 -05001269 Returns:
1270 Contents of a control file if control_path is provided.
1271 A list of control files if no control_path is provided.
1272 """
Don Garrettf84631a2014-01-07 18:21:26 -08001273 if not kwargs:
Scott Zawalski4647ce62012-01-03 17:17:28 -05001274 return _PrintDocStringAsHTML(self.controlfiles)
1275
Don Garrettf84631a2014-01-07 18:21:26 -08001276 if 'build' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -08001277 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
1278 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -05001279
Don Garrettf84631a2014-01-07 18:21:26 -08001280 if 'control_path' not in kwargs:
1281 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-09 22:44:06 -07001282 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-07 18:21:26 -08001283 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-09 22:44:06 -07001284 else:
1285 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-07 18:21:26 -08001286 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -05001287 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001288 return common_util.GetControlFile(
Don Garrettf84631a2014-01-07 18:21:26 -08001289 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -08001290
1291 @cherrypy.expose
Simran Basi99e63c02014-05-20 10:39:52 -07001292 def xbuddy_translate(self, *args, **kwargs):
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001293 """Translates an xBuddy path to a real path to artifact if it exists.
1294
1295 Args:
Simran Basi99e63c02014-05-20 10:39:52 -07001296 args: An xbuddy path in the form of {local|remote}/build_id/artifact.
1297 Local searches the devserver's static directory. Remote searches a
1298 Google Storage image archive.
1299
1300 Kwargs:
1301 image_dir: Google Storage image archive to search in if requesting a
1302 remote artifact. If none uses the default bucket.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001303
1304 Returns:
Simran Basi99e63c02014-05-20 10:39:52 -07001305 String in the format of build_id/artifact as stored on the local server
1306 or in Google Storage.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001307 """
Simran Basi99e63c02014-05-20 10:39:52 -07001308 build_id, filename = self._xbuddy.Translate(
Gabe Black3b567202015-09-23 14:07:59 -07001309 args, image_dir=kwargs.get('image_dir'))
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001310 response = os.path.join(build_id, filename)
1311 _Log('Path translation requested, returning: %s', response)
1312 return response
1313
1314 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -07001315 def xbuddy(self, *args, **kwargs):
1316 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -07001317
1318 Args:
joycheneaf4cfc2013-07-02 08:38:57 -07001319 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -07001320 components of the path. The path can be understood as
1321 "{local|remote}/build_id/artifact" where build_id is composed of
1322 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -07001323
joychen121fc9b2013-08-02 14:30:30 -07001324 The first path element is optional, and can be "remote" or "local"
1325 If local (the default), devserver will not attempt to access Google
1326 Storage, and will only search the static directory for the files.
1327 If remote, devserver will try to obtain the artifact off GS if it's
1328 not found locally.
1329 The board is the familiar board name, optionally suffixed.
1330 The version can be the google storage version number, and may also be
1331 any of a number of xBuddy defined version aliases that will be
1332 translated into the latest built image that fits the description.
1333 Defaults to latest.
1334 The artifact is one of a number of image or artifact aliases used by
1335 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -07001336
1337 Kwargs:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001338 for_update: {true|false}
Amin Hassanie9ffb862019-09-25 17:10:40 -07001339 if true, prepares the update payloads for the image,
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001340 and returns the update uri to pass to the
1341 update_engine_client.
joychen3cb228e2013-06-12 12:13:13 -07001342 return_dir: {true|false}
1343 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001344 relative_path: {true|false}
1345 if set to true, returns the relative path to the payload
1346 directory from static_dir.
joychen3cb228e2013-06-12 12:13:13 -07001347 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -07001348 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -07001349 or
joycheneaf4cfc2013-07-02 08:38:57 -07001350 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -07001351
1352 Returns:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001353 If |for_update|, returns a redirect to the image or update file
1354 on the devserver. E.g.,
1355 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
1356 chromium-test-image.bin
1357 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
1358 http://host:port/static/x86-generic-release/R26-4000.0.0/
1359 If |relative_path| is true, return a relative path the folder where the
1360 payloads are. E.g.,
1361 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 12:13:13 -07001362 """
Chris Sosa75490802013-09-30 17:21:45 -07001363 boolean_string = kwargs.get('for_update')
1364 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001365 boolean_string = kwargs.get('return_dir')
1366 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
1367 boolean_string = kwargs.get('relative_path')
1368 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -07001369
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001370 if return_dir and relative_path:
Amin Hassani722e0962019-11-15 15:45:31 -08001371 raise DevServerHTTPError(
Amin Hassanid4e35392019-10-03 11:02:44 -07001372 http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -07001373 'Cannot specify both return_dir and relative_path')
Chris Sosa75490802013-09-30 17:21:45 -07001374
1375 # For updates, we optimize downloading of test images.
1376 file_name = None
1377 build_id = None
1378 if for_update:
1379 try:
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001380 build_id = self._xbuddy.StageTestArtifactsForUpdate(args)
Chris Sosa75490802013-09-30 17:21:45 -07001381 except build_artifact.ArtifactDownloadError:
1382 build_id = None
1383
1384 if not build_id:
1385 build_id, file_name = self._xbuddy.Get(args)
1386
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001387 if for_update:
Amin Hassanie9ffb862019-09-25 17:10:40 -07001388 _Log('Payloads requested.')
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001389 # Forces payload to be in cache and symlinked into build_id dir.
Amin Hassanie9ffb862019-09-25 17:10:40 -07001390 updater.GetUpdateForLabel(build_id)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001391
1392 response = None
1393 if return_dir:
1394 response = os.path.join(cherrypy.request.base, 'static', build_id)
1395 _Log('Directory requested, returning: %s', response)
1396 elif relative_path:
1397 response = build_id
1398 _Log('Relative path requested, returning: %s', response)
1399 elif for_update:
1400 response = os.path.join(cherrypy.request.base, 'update', build_id)
1401 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 12:13:13 -07001402 else:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001403 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 14:30:30 -07001404 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001405 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 14:30:30 -07001406 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -07001407
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001408 return response
1409
joychen3cb228e2013-06-12 12:13:13 -07001410 @cherrypy.expose
1411 def xbuddy_list(self):
1412 """Lists the currently available images & time since last access.
1413
Gilad Arnold452fd272014-02-04 11:09:28 -08001414 Returns:
1415 A string representation of a list of tuples [(build_id, time since last
1416 access),...]
joychen3cb228e2013-06-12 12:13:13 -07001417 """
1418 return self._xbuddy.List()
1419
1420 @cherrypy.expose
1421 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 11:09:28 -08001422 """Returns the number of images cached by xBuddy."""
joychen3cb228e2013-06-12 12:13:13 -07001423 return self._xbuddy.Capacity()
1424
1425 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001426 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001427 """Presents a welcome message and documentation links."""
Congbin Guo6bc32182019-08-20 17:54:30 -07001428 html_template = (
1429 'Welcome to the Dev Server!<br>\n'
1430 '<br>\n'
1431 'Here are the available methods, click for documentation:<br>\n'
1432 '<br>\n'
1433 '%s')
1434
1435 exposed_methods = []
1436 for app in cherrypy.tree.apps.values():
1437 exposed_methods += _FindExposedMethods(
1438 app.root, app.script_name.lstrip('/'),
1439 unlisted=self._UNLISTED_METHODS)
1440
1441 return html_template % '<br>\n'.join(
1442 ['<a href=doc/%s>%s</a>' % (name, name)
1443 for name in sorted(exposed_methods)])
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001444
1445 @cherrypy.expose
1446 def doc(self, *args):
1447 """Shows the documentation for available methods / URLs.
1448
Amin Hassani08e42d22019-06-03 00:31:30 -07001449 Examples:
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001450 http://myhost/doc/update
1451 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001452 name = '/'.join(args)
Congbin Guo6bc32182019-08-20 17:54:30 -07001453 method = _GetExposedMethod(name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001454 if not method:
Amin Hassanid4e35392019-10-03 11:02:44 -07001455 raise DevServerError("No exposed method named `%s'" % name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001456 if not method.__doc__:
Amin Hassanid4e35392019-10-03 11:02:44 -07001457 raise DevServerError("No documentation for exposed method `%s'" % name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001458 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -07001459
Dale Curtisc9aaf3a2011-08-09 15:47:40 -07001460 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001461 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001462 """Handles an update check from a Chrome OS client.
1463
1464 The HTTP request should contain the standard Omaha-style XML blob. The URL
1465 line may contain an additional intermediate path to the update payload.
1466
joychen121fc9b2013-08-02 14:30:30 -07001467 This request can be handled in one of 4 ways, depending on the devsever
1468 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -07001469
Amin Hassanie9ffb862019-09-25 17:10:40 -07001470 1. No intermediate path. DEPRECATED
joychen121fc9b2013-08-02 14:30:30 -07001471
1472 2. Path explicitly invokes XBuddy
1473 If there is a path given, it can explicitly invoke xbuddy by prefixing it
1474 with 'xbuddy'. This path is then used to acquire an image binary for the
1475 devserver to generate an update payload from. Devserver then serves this
1476 payload.
1477
1478 3. Path is left for the devserver to interpret.
1479 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
1480 to generate a payload from the test image in that directory and serve it.
1481
joychen121fc9b2013-08-02 14:30:30 -07001482 Examples:
joychen121fc9b2013-08-02 14:30:30 -07001483 2. Explicitly invoke xbuddy
1484 update_engine_client --omaha_url=
1485 http://myhost/update/xbuddy/remote/board/version/dev
1486 This would go to GS to download the dev image for the board, from which
1487 the devserver would generate a payload to serve.
1488
1489 3. Give a path for devserver to interpret
1490 update_engine_client --omaha_url=http://myhost/update/some/random/path
1491 This would attempt, in order to:
1492 a) Generate an update from a test image binary if found in
1493 static_dir/some/random/path.
1494 b) Serve an update payload found in static_dir/some/random/path.
1495 c) Hope that some/random/path takes the form "board/version" and
1496 and attempt to download an update payload for that board/version
1497 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001498 """
joychen121fc9b2013-08-02 14:30:30 -07001499 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -08001500 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -07001501 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-11 19:49:01 -07001502
joychen121fc9b2013-08-02 14:30:30 -07001503 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001504
Dan Shif5ce2de2013-04-25 16:06:32 -07001505
Chris Sosadbc20082012-12-10 13:39:11 -08001506def _CleanCache(cache_dir, wipe):
1507 """Wipes any excess cached items in the cache_dir.
1508
1509 Args:
1510 cache_dir: the directory we are wiping from.
1511 wipe: If True, wipe all the contents -- not just the excess.
1512 """
1513 if wipe:
1514 # Clear the cache and exit on error.
1515 cmd = 'rm -rf %s/*' % cache_dir
1516 if os.system(cmd) != 0:
1517 _Log('Failed to clear the cache with %s' % cmd)
1518 sys.exit(1)
1519 else:
1520 # Clear all but the last N cached updates
1521 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
1522 (cache_dir, CACHED_ENTRIES))
1523 if os.system(cmd) != 0:
1524 _Log('Failed to clean up old delta cache files with %s' % cmd)
1525 sys.exit(1)
1526
1527
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001528def _AddTestingOptions(parser):
1529 group = optparse.OptionGroup(
1530 parser, 'Advanced Testing Options', 'These are used by test scripts and '
1531 'developers writing integration tests utilizing the devserver. They are '
1532 'not intended to be really used outside the scope of someone '
1533 'knowledgable about the test.')
1534 group.add_option('--exit',
1535 action='store_true',
Amin Hassanie9ffb862019-09-25 17:10:40 -07001536 help='do not start the server (yet clear cache)')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001537 group.add_option('--host_log',
1538 action='store_true', default=False,
1539 help='record history of host update events (/api/hostlog)')
1540 group.add_option('--max_updates',
Gabe Black3b567202015-09-23 14:07:59 -07001541 metavar='NUM', default=-1, type='int',
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001542 help='maximum number of update checks handled positively '
1543 '(default: unlimited)')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001544 group.add_option('--proxy_port',
1545 metavar='PORT', default=None, type='int',
1546 help='port to have the client connect to -- basically the '
1547 'devserver lies to the update to tell it to get the payload '
1548 'from a different port that will proxy the request back to '
1549 'the devserver. The proxy must be managed outside the '
1550 'devserver.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001551 parser.add_option_group(group)
1552
1553
1554def _AddUpdateOptions(parser):
1555 group = optparse.OptionGroup(
1556 parser, 'Autoupdate Options', 'These options can be used to change '
Amin Hassanie9ffb862019-09-25 17:10:40 -07001557 'how the devserver serve update payloads. Please '
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001558 'note that all of these option affect how a payload is generated and so '
1559 'do not work in archive-only mode.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001560 group.add_option('--critical_update',
1561 action='store_true', default=False,
1562 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001563 group.add_option('--payload',
1564 metavar='PATH',
1565 help='use the update payload from specified directory '
1566 '(update.gz).')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001567 parser.add_option_group(group)
1568
1569
1570def _AddProductionOptions(parser):
1571 group = optparse.OptionGroup(
1572 parser, 'Advanced Server Options', 'These options can be used to changed '
1573 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001574 group.add_option('--clear_cache',
1575 action='store_true', default=False,
1576 help='At startup, removes all cached entries from the'
Amin Hassanid4e35392019-10-03 11:02:44 -07001577 "devserver's cache.")
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001578 group.add_option('--logfile',
1579 metavar='PATH',
1580 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -07001581 group.add_option('--pidfile',
1582 metavar='PATH',
1583 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 11:04:13 -08001584 group.add_option('--portfile',
1585 metavar='PATH',
1586 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001587 group.add_option('--production',
1588 action='store_true', default=False,
1589 help='have the devserver use production values when '
1590 'starting up. This includes using more threads and '
1591 'performing less logging.')
1592 parser.add_option_group(group)
1593
1594
Paul Hobbsef4e0702016-06-27 17:01:42 -07001595def MakeLogHandler(logfile):
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001596 """Create a LogHandler instance used to log all messages."""
1597 hdlr_cls = handlers.TimedRotatingFileHandler
1598 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
xixuan3d48bff2017-01-30 19:00:09 -08001599 interval=_LOG_ROTATION_INTERVAL,
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001600 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -07001601 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001602 return hdlr
1603
1604
Chris Sosacde6bf42012-05-31 18:36:39 -07001605def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001606 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -08001607 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -07001608
1609 # get directory that the devserver is run from
1610 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -07001611 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -07001612 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001613 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -07001614 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -07001615 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001616 parser.add_option('--port',
1617 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 13:13:28 -08001618 help=('port for the dev server to use; if zero, binds to '
1619 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001620 parser.add_option('-t', '--test_image',
1621 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -07001622 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -07001623 parser.add_option('-x', '--xbuddy_manage_builds',
1624 action='store_true',
1625 default=False,
1626 help='If set, allow xbuddy to manage images in'
1627 'build/images.')
Dan Shi72b16132015-10-08 12:10:33 -07001628 parser.add_option('-a', '--android_build_credential',
1629 default=None,
1630 help='Path to a json file which contains the credential '
1631 'needed to access Android builds.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001632 _AddProductionOptions(parser)
1633 _AddUpdateOptions(parser)
1634 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -07001635 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001636
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001637 # Handle options that must be set globally in cherrypy. Do this
1638 # work up front, because calls to _Log() below depend on this
1639 # initialization.
1640 if options.production:
1641 cherrypy.config.update({'environment': 'production'})
1642 if not options.logfile:
1643 cherrypy.config.update({'log.screen': True})
1644 else:
1645 cherrypy.config.update({'log.error_file': '',
1646 'log.access_file': ''})
Paul Hobbsef4e0702016-06-27 17:01:42 -07001647 hdlr = MakeLogHandler(options.logfile)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001648 # Pylint can't seem to process these two calls properly
1649 # pylint: disable=E1101
1650 cherrypy.log.access_log.addHandler(hdlr)
1651 cherrypy.log.error_log.addHandler(hdlr)
1652 # pylint: enable=E1101
1653
joychened64b222013-06-21 16:39:34 -07001654 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001655 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001656
joychened64b222013-06-21 16:39:34 -07001657 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001658 # If our devserver is only supposed to serve payloads, we shouldn't be
1659 # mucking with the cache at all. If the devserver hadn't previously
1660 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001661 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001662 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001663 else:
1664 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001665
Chris Sosadbc20082012-12-10 13:39:11 -08001666 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 16:39:34 -07001667 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001668
Amin Hassanie9ffb862019-09-25 17:10:40 -07001669 _xbuddy = xbuddy.XBuddy(manage_builds=options.xbuddy_manage_builds,
joychen121fc9b2013-08-02 14:30:30 -07001670 static_dir=options.static_dir)
Chris Sosa75490802013-09-30 17:21:45 -07001671 if options.clear_cache and options.xbuddy_manage_builds:
1672 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 14:30:30 -07001673
Chris Sosa6a3697f2013-01-29 16:44:43 -08001674 # We allow global use here to share with cherrypy classes.
1675 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001676 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001677 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001678 _xbuddy,
joychened64b222013-06-21 16:39:34 -07001679 static_dir=options.static_dir,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001680 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001681 proxy_port=options.proxy_port,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001682 critical_update=options.critical_update,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001683 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001684 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001685 )
Chris Sosa7c931362010-10-11 19:49:01 -07001686
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001687 if options.exit:
1688 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001689
joychen3cb228e2013-06-12 12:13:13 -07001690 dev_server = DevServerRoot(_xbuddy)
Congbin Guo3afae6c2019-08-13 16:29:42 -07001691 health_checker_app = health_checker.Root(dev_server, options.static_dir)
joychen3cb228e2013-06-12 12:13:13 -07001692
Amin Hassanic5af4262019-11-13 13:37:20 -08001693 # Patch CherryPy to support binding to any available port (--port=0) only for
1694 # cherrypy versions smaller or equal to 3.2.2.
1695 #
1696 # TODO(crbug/1006305): Remove this once we have deprecated omaha_devserver.py
1697 # in the autotests as that is the only use case.
1698 #
1699 # pylint: disable=no-member
1700 if (distutils.version.StrictVersion(cherrypy.__version__) <=
1701 distutils.version.StrictVersion('3.2.2')):
1702 cherrypy_ext.ZeroPortPatcher.DoPatch(cherrypy)
1703 # pylint: enable=no-member
1704
Chris Sosa855b8932013-08-21 13:24:55 -07001705 if options.pidfile:
1706 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1707
Gilad Arnold11fbef42014-02-10 11:04:13 -08001708 if options.portfile:
1709 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1710
Dan Shiafd5c6c2016-01-07 10:27:03 -08001711 if (options.android_build_credential and
1712 os.path.exists(options.android_build_credential)):
1713 try:
1714 with open(options.android_build_credential) as f:
1715 android_build.BuildAccessor.credential_info = json.load(f)
1716 except ValueError as e:
1717 _Log('Failed to load the android build credential: %s. Error: %s.' %
1718 (options.android_build_credential, e))
Congbin Guo3afae6c2019-08-13 16:29:42 -07001719
1720 cherrypy.tree.mount(health_checker_app, '/check_health',
1721 config=health_checker.get_config())
joychen3cb228e2013-06-12 12:13:13 -07001722 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001723
1724
1725if __name__ == '__main__':
1726 main()