blob: 3b89260816cb42ce6d43ae58c8675712c3de7841 [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
52import cros_update
53import cros_update_progress
54import health_checker
55
Richard Barnettedf35c322017-08-18 17:02:13 -070056# This must happen before any local modules get a chance to import
57# anything from chromite. Otherwise, really bad things will happen, and
58# you will _not_ understand why.
Congbin Guo3afae6c2019-08-13 16:29:42 -070059import setup_chromite # pylint: disable=unused-import
Achuith Bhandarkar662fb722019-10-31 16:12:49 -070060from chromite.lib.xbuddy import android_build
61from chromite.lib.xbuddy import artifact_info
62from chromite.lib.xbuddy import build_artifact
63from chromite.lib.xbuddy import cherrypy_log_util
64from chromite.lib.xbuddy import common_util
65from chromite.lib.xbuddy import devserver_constants
66from chromite.lib.xbuddy import downloader
67from chromite.lib.xbuddy import xbuddy
Richard Barnettedf35c322017-08-18 17:02:13 -070068
Gilad Arnoldc65330c2012-09-20 15:17:48 -070069
Gilad Arnoldc65330c2012-09-20 15:17:48 -070070# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080071def _Log(message, *args):
Achuith Bhandarkar662fb722019-10-31 16:12:49 -070072 return cherrypy_log_util.LogWithTag('DEVSERVER', message, *args)
Frank Farzan40160872011-12-12 18:39:18 -080073
Chris Sosa417e55d2011-01-25 16:40:48 -080074CACHED_ENTRIES = 12
Don Garrettf90edf02010-11-16 17:36:14 -080075
Simran Basi4baad082013-02-14 13:39:18 -080076TELEMETRY_FOLDER = 'telemetry_src'
77TELEMETRY_DEPS = ['dep-telemetry_dep.tar.bz2',
78 'dep-page_cycler_dep.tar.bz2',
Simran Basi0d078682013-03-22 16:40:04 -070079 'dep-chrome_test.tar.bz2',
80 'dep-perf_data_dep.tar.bz2']
Simran Basi4baad082013-02-14 13:39:18 -080081
Chris Sosa0356d3b2010-09-16 15:46:22 -070082# Sets up global to share between classes.
rtc@google.com21a5ca32009-11-04 18:23:23 +000083updater = None
rtc@google.comded22402009-10-26 22:36:21 +000084
xixuan3d48bff2017-01-30 19:00:09 -080085# Log rotation parameters. These settings correspond to twice a day once
86# devserver is started, with about two weeks (28 backup files) of old logs
87# kept for backup.
J. Richard Barnette3d977b82013-04-23 11:05:19 -070088#
xixuan3d48bff2017-01-30 19:00:09 -080089# For more, see the documentation in standard python library for
J. Richard Barnette3d977b82013-04-23 11:05:19 -070090# logging.handlers.TimedRotatingFileHandler
xixuan3d48bff2017-01-30 19:00:09 -080091_LOG_ROTATION_TIME = 'H'
Congbin Guo3afae6c2019-08-13 16:29:42 -070092_LOG_ROTATION_INTERVAL = 12 # hours
93_LOG_ROTATION_BACKUP = 28 # backup counts
Frank Farzan40160872011-12-12 18:39:18 -080094
xixuan52c2fba2016-05-20 17:02:48 -070095# Auto-update parameters
96
97# Error msg for missing key in CrOS auto-update.
Xixuan Wu32af9f12017-11-13 14:11:44 -080098KEY_ERROR_MSG = 'Key Error in RPC: %s= is required'
xixuan52c2fba2016-05-20 17:02:48 -070099
xixuan52c2fba2016-05-20 17:02:48 -0700100
Amin Hassanid4e35392019-10-03 11:02:44 -0700101class DevServerError(Exception):
102 """Exception class used by DevServer."""
103
104
Gabe Black3b567202015-09-23 14:07:59 -0700105def _canonicalize_archive_url(archive_url):
106 """Canonicalizes archive_url strings.
107
108 Raises:
109 DevserverError: if archive_url is not set.
110 """
111 if archive_url:
112 if not archive_url.startswith('gs://'):
Amin Hassanid4e35392019-10-03 11:02:44 -0700113 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700114 "Archive URL isn't from Google Storage (%s) ." % archive_url)
Gabe Black3b567202015-09-23 14:07:59 -0700115
116 return archive_url.rstrip('/')
117 else:
Amin Hassanid4e35392019-10-03 11:02:44 -0700118 raise DevServerError('Must specify an archive_url in the request')
Gabe Black3b567202015-09-23 14:07:59 -0700119
120
121def _canonicalize_local_path(local_path):
122 """Canonicalizes |local_path| strings.
123
124 Raises:
125 DevserverError: if |local_path| is not set.
126 """
127 # Restrict staging of local content to only files within the static
128 # directory.
129 local_path = os.path.abspath(local_path)
130 if not local_path.startswith(updater.static_dir):
Amin Hassanid4e35392019-10-03 11:02:44 -0700131 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700132 'Local path %s must be a subdirectory of the static'
133 ' directory: %s' % (local_path, updater.static_dir))
Gabe Black3b567202015-09-23 14:07:59 -0700134
135 return local_path.rstrip('/')
136
137
138def _get_artifacts(kwargs):
139 """Returns a tuple of named and file artifacts given the stage rpc kwargs.
140
141 Raises:
142 DevserverError if no artifacts would be returned.
143 """
144 artifacts = kwargs.get('artifacts')
145 files = kwargs.get('files')
146 if not artifacts and not files:
Amin Hassanid4e35392019-10-03 11:02:44 -0700147 raise DevServerError('No artifacts specified.')
Gabe Black3b567202015-09-23 14:07:59 -0700148
149 # Note we NEED to coerce files to a string as we get raw unicode from
150 # cherrypy and we treat files as strings elsewhere in the code.
151 return (str(artifacts).split(',') if artifacts else [],
152 str(files).split(',') if files else [])
153
154
Dan Shi61305df2015-10-26 16:52:35 -0700155def _is_android_build_request(kwargs):
156 """Check if a devserver call is for Android build, based on the arguments.
157
158 This method exams the request's arguments (os_type) to determine if the
159 request is for Android build. If os_type is set to `android`, returns True.
160 If os_type is not set or has other values, returns False.
161
162 Args:
163 kwargs: Keyword arguments for the request.
164
165 Returns:
166 True if the request is for Android build. False otherwise.
167 """
168 os_type = kwargs.get('os_type', None)
169 return os_type == 'android'
170
171
Gabe Black3b567202015-09-23 14:07:59 -0700172def _get_downloader(kwargs):
173 """Returns the downloader based on passed in arguments.
174
175 Args:
Amin Hassani08e42d22019-06-03 00:31:30 -0700176 kwargs: Keyword arguments for the request.
Gabe Black3b567202015-09-23 14:07:59 -0700177 """
178 local_path = kwargs.get('local_path')
179 if local_path:
180 local_path = _canonicalize_local_path(local_path)
181
182 dl = None
183 if local_path:
Prathmesh Prabhu58d08932018-01-19 15:08:19 -0800184 delete_source = _parse_boolean_arg(kwargs, 'delete_source')
185 dl = downloader.LocalDownloader(updater.static_dir, local_path,
186 delete_source=delete_source)
Gabe Black3b567202015-09-23 14:07:59 -0700187
Dan Shi61305df2015-10-26 16:52:35 -0700188 if not _is_android_build_request(kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700189 archive_url = kwargs.get('archive_url')
190 if not archive_url and not local_path:
Amin Hassanid4e35392019-10-03 11:02:44 -0700191 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700192 'Requires archive_url or local_path to be specified.')
Gabe Black3b567202015-09-23 14:07:59 -0700193 if archive_url and local_path:
Amin Hassanid4e35392019-10-03 11:02:44 -0700194 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700195 'archive_url and local_path can not both be specified.')
Gabe Black3b567202015-09-23 14:07:59 -0700196 if not dl:
197 archive_url = _canonicalize_archive_url(archive_url)
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700198 dl = downloader.GoogleStorageDownloader(
199 updater.static_dir, archive_url,
200 downloader.GoogleStorageDownloader.GetBuildIdFromArchiveURL(
201 archive_url))
Gabe Black3b567202015-09-23 14:07:59 -0700202 elif not dl:
203 target = kwargs.get('target', None)
Dan Shi72b16132015-10-08 12:10:33 -0700204 branch = kwargs.get('branch', None)
Dan Shi61305df2015-10-26 16:52:35 -0700205 build_id = kwargs.get('build_id', None)
206 if not target or not branch or not build_id:
Amin Hassanid4e35392019-10-03 11:02:44 -0700207 raise DevServerError('target, branch, build ID must all be specified for '
208 'downloading Android build.')
Dan Shi72b16132015-10-08 12:10:33 -0700209 dl = downloader.AndroidBuildDownloader(updater.static_dir, branch, build_id,
210 target)
Gabe Black3b567202015-09-23 14:07:59 -0700211
212 return dl
213
214
215def _get_downloader_and_factory(kwargs):
216 """Returns the downloader and artifact factory based on passed in arguments.
217
218 Args:
Amin Hassani08e42d22019-06-03 00:31:30 -0700219 kwargs: Keyword arguments for the request.
Gabe Black3b567202015-09-23 14:07:59 -0700220 """
221 artifacts, files = _get_artifacts(kwargs)
222 dl = _get_downloader(kwargs)
223
Achuith Bhandarkar2a1fcd82019-10-17 17:45:58 -0700224 if (isinstance(dl, (downloader.GoogleStorageDownloader,
225 downloader.LocalDownloader))):
Gabe Black3b567202015-09-23 14:07:59 -0700226 factory_class = build_artifact.ChromeOSArtifactFactory
Dan Shi72b16132015-10-08 12:10:33 -0700227 elif isinstance(dl, downloader.AndroidBuildDownloader):
Gabe Black3b567202015-09-23 14:07:59 -0700228 factory_class = build_artifact.AndroidArtifactFactory
229 else:
Amin Hassanid4e35392019-10-03 11:02:44 -0700230 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700231 'Unrecognized value for downloader type: %s' % type(dl))
Gabe Black3b567202015-09-23 14:07:59 -0700232
233 factory = factory_class(dl.GetBuildDir(), artifacts, files, dl.GetBuild())
234
235 return dl, factory
236
237
Scott Zawalski4647ce62012-01-03 17:17:28 -0500238def _LeadingWhiteSpaceCount(string):
239 """Count the amount of leading whitespace in a string.
240
241 Args:
242 string: The string to count leading whitespace in.
Don Garrettf84631a2014-01-07 18:21:26 -0800243
Scott Zawalski4647ce62012-01-03 17:17:28 -0500244 Returns:
245 number of white space chars before characters start.
246 """
Gabe Black3b567202015-09-23 14:07:59 -0700247 matched = re.match(r'^\s+', string)
Scott Zawalski4647ce62012-01-03 17:17:28 -0500248 if matched:
249 return len(matched.group())
250
251 return 0
252
253
254def _PrintDocStringAsHTML(func):
255 """Make a functions docstring somewhat HTML style.
256
257 Args:
258 func: The function to return the docstring from.
Don Garrettf84631a2014-01-07 18:21:26 -0800259
Scott Zawalski4647ce62012-01-03 17:17:28 -0500260 Returns:
261 A string that is somewhat formated for a web browser.
262 """
263 # TODO(scottz): Make this parse Args/Returns in a prettier way.
264 # Arguments could be bolded and indented etc.
265 html_doc = []
266 for line in func.__doc__.splitlines():
267 leading_space = _LeadingWhiteSpaceCount(line)
268 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700269 line = ' ' * leading_space + line
Scott Zawalski4647ce62012-01-03 17:17:28 -0500270
271 html_doc.append('<BR>%s' % line)
272
273 return '\n'.join(html_doc)
274
275
Simran Basief83d6a2014-08-28 14:32:01 -0700276def _GetUpdateTimestampHandler(static_dir):
277 """Returns a handler to update directory staged.timestamp.
278
279 This handler resets the stage.timestamp whenever static content is accessed.
280
281 Args:
282 static_dir: Directory from which static content is being staged.
283
284 Returns:
Amin Hassani08e42d22019-06-03 00:31:30 -0700285 A cherrypy handler to update the timestamp of accessed content.
Simran Basief83d6a2014-08-28 14:32:01 -0700286 """
287 def UpdateTimestampHandler():
288 if not '404' in cherrypy.response.status:
289 build_match = re.match(devserver_constants.STAGED_BUILD_REGEX,
290 cherrypy.request.path_info)
291 if build_match:
292 build_dir = os.path.join(static_dir, build_match.group('build'))
293 downloader.Downloader.TouchTimestampForStaged(build_dir)
294 return UpdateTimestampHandler
295
296
Chris Sosa7c931362010-10-11 19:49:01 -0700297def _GetConfig(options):
298 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800299
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800300 socket_host = '::'
Yu-Ju Hongc8d4af32013-11-12 15:14:26 -0800301 # Fall back to IPv4 when python is not configured with IPv6.
302 if not socket.has_ipv6:
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800303 socket_host = '0.0.0.0'
304
Simran Basief83d6a2014-08-28 14:32:01 -0700305 # Adds the UpdateTimestampHandler to cherrypy's tools. This tools executes
306 # on the on_end_resource hook. This hook is called once processing is
307 # complete and the response is ready to be returned.
308 cherrypy.tools.update_timestamp = cherrypy.Tool(
309 'on_end_resource', _GetUpdateTimestampHandler(options.static_dir))
310
David Riley2fcb0122017-11-02 11:25:39 -0700311 base_config = {
312 'global': {
313 'server.log_request_headers': True,
314 'server.protocol_version': 'HTTP/1.1',
315 'server.socket_host': socket_host,
316 'server.socket_port': int(options.port),
317 'response.timeout': 6000,
318 'request.show_tracebacks': True,
319 'server.socket_timeout': 60,
320 'server.thread_pool': 2,
321 'engine.autoreload.on': False,
322 },
323 '/api': {
324 # Gets rid of cherrypy parsing post file for args.
325 'request.process_request_body': False,
326 },
327 '/build': {
328 'response.timeout': 100000,
329 },
330 '/update': {
331 # Gets rid of cherrypy parsing post file for args.
332 'request.process_request_body': False,
333 'response.timeout': 10000,
334 },
335 # Sets up the static dir for file hosting.
336 '/static': {
337 'tools.staticdir.dir': options.static_dir,
338 'tools.staticdir.on': True,
339 'response.timeout': 10000,
340 'tools.update_timestamp.on': True,
341 },
342 }
Chris Sosa5f118ef2012-07-12 11:37:50 -0700343 if options.production:
Alex Miller93beca52013-07-30 19:25:09 -0700344 base_config['global'].update({'server.thread_pool': 150})
Scott Zawalski1c5e7cd2012-02-27 13:12:52 -0500345
Chris Sosa7c931362010-10-11 19:49:01 -0700346 return base_config
rtc@google.com64244662009-11-12 00:52:08 +0000347
Darin Petkove17164a2010-08-11 13:24:41 -0700348
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700349def _GetRecursiveMemberObject(root, member_list):
350 """Returns an object corresponding to a nested member list.
351
352 Args:
353 root: the root object to search
354 member_list: list of nested members to search
Don Garrettf84631a2014-01-07 18:21:26 -0800355
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700356 Returns:
357 An object corresponding to the member name list; None otherwise.
358 """
359 for member in member_list:
360 next_root = root.__class__.__dict__.get(member)
361 if not next_root:
362 return None
363 root = next_root
364 return root
365
366
367def _IsExposed(name):
368 """Returns True iff |name| has an `exposed' attribute and it is set."""
369 return hasattr(name, 'exposed') and name.exposed
370
371
Congbin Guo6bc32182019-08-20 17:54:30 -0700372def _GetExposedMethod(nested_member):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700373 """Returns a CherryPy-exposed method, if such exists.
374
375 Args:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700376 nested_member: a slash-joined path to the nested member
Don Garrettf84631a2014-01-07 18:21:26 -0800377
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700378 Returns:
Congbin Guo6bc32182019-08-20 17:54:30 -0700379 A function object corresponding to the path defined by |nested_member| from
380 the app root object registered, if the function is exposed; None otherwise.
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700381 """
Congbin Guo6bc32182019-08-20 17:54:30 -0700382 for app in cherrypy.tree.apps.values():
383 # Use the 'index' function doc as the doc of the app.
384 if nested_member == app.script_name.lstrip('/'):
385 nested_member = 'index'
386
387 method = _GetRecursiveMemberObject(app.root, nested_member.split('/'))
388 if method and isinstance(method, types.FunctionType) and _IsExposed(method):
389 return method
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700390
391
Gilad Arnold748c8322012-10-12 09:51:35 -0700392def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700393 """Finds exposed CherryPy methods.
394
395 Args:
396 root: the root object for searching
397 prefix: slash-joined chain of members leading to current object
398 unlisted: URLs to be excluded regardless of their exposed status
Don Garrettf84631a2014-01-07 18:21:26 -0800399
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700400 Returns:
401 List of exposed URLs that are not unlisted.
402 """
403 method_list = []
Congbin Guo6bc32182019-08-20 17:54:30 -0700404 for member in root.__class__.__dict__.keys():
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700405 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 09:51:35 -0700406 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700407 continue
408 member_obj = root.__class__.__dict__[member]
409 if _IsExposed(member_obj):
Amin Hassani08e42d22019-06-03 00:31:30 -0700410 if isinstance(member_obj, types.FunctionType):
Congbin Guo6bc32182019-08-20 17:54:30 -0700411 # Regard the app name as exposed "method" name if it exposed 'index'
412 # function.
413 if prefix and member == 'index':
414 method_list.append(prefix)
415 else:
416 method_list.append(prefixed_member)
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700417 else:
418 method_list += _FindExposedMethods(
419 member_obj, prefixed_member, unlisted)
420 return method_list
421
422
xixuan52c2fba2016-05-20 17:02:48 -0700423def _check_base_args_for_auto_update(kwargs):
xixuanac89ce82016-11-30 16:48:20 -0800424 """Check basic args required for auto-update.
425
426 Args:
427 kwargs: the parameters to be checked.
428
429 Raises:
430 DevServerHTTPError if required parameters don't exist in kwargs.
431 """
xixuan52c2fba2016-05-20 17:02:48 -0700432 if 'host_name' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -0700433 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -0700434 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -0700435
436 if 'build_name' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -0700437 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -0700438 KEY_ERROR_MSG % 'build_name')
xixuan52c2fba2016-05-20 17:02:48 -0700439
440
441def _parse_boolean_arg(kwargs, key):
xixuanac89ce82016-11-30 16:48:20 -0800442 """Parse boolean arg from kwargs.
443
444 Args:
445 kwargs: the parameters to be checked.
446 key: the key to be parsed.
447
448 Returns:
449 The boolean value of kwargs[key], or False if key doesn't exist in kwargs.
450
451 Raises:
452 DevServerHTTPError if kwargs[key] is not a boolean variable.
453 """
xixuan52c2fba2016-05-20 17:02:48 -0700454 if key in kwargs:
455 if kwargs[key] == 'True':
456 return True
457 elif kwargs[key] == 'False':
458 return False
459 else:
460 raise common_util.DevServerHTTPError(
Amin Hassanid4e35392019-10-03 11:02:44 -0700461 http_client.INTERNAL_SERVER_ERROR,
xixuan52c2fba2016-05-20 17:02:48 -0700462 'The value for key %s is not boolean.' % key)
463 else:
464 return False
465
xixuan447ad9d2017-02-28 14:46:20 -0800466
xixuanac89ce82016-11-30 16:48:20 -0800467def _parse_string_arg(kwargs, key):
468 """Parse string arg from kwargs.
469
470 Args:
471 kwargs: the parameters to be checked.
472 key: the key to be parsed.
473
474 Returns:
475 The string value of kwargs[key], or None if key doesn't exist in kwargs.
476 """
477 if key in kwargs:
478 return kwargs[key]
479 else:
480 return None
481
xixuan447ad9d2017-02-28 14:46:20 -0800482
xixuanac89ce82016-11-30 16:48:20 -0800483def _build_uri_from_build_name(build_name):
484 """Get build url from a given build name.
485
486 Args:
487 build_name: the build name to be parsed, whose format is
488 'board/release_version'.
489
490 Returns:
491 The release_archive_url on Google Storage for this build name.
492 """
Amin Hassani08e42d22019-06-03 00:31:30 -0700493 # TODO(ahassani): This function doesn't seem to be used anywhere since its
494 # previous use of lib.paygen.gspath was broken and it doesn't seem to be
495 # causing any runtime issues. So deprecate this in the future.
496 tokens = build_name.split('/')
497 return 'gs://chromeos-releases/stable-channel/%s/%s' % (tokens[0], tokens[1])
xixuan52c2fba2016-05-20 17:02:48 -0700498
xixuan447ad9d2017-02-28 14:46:20 -0800499
500def _clear_process(host_name, pid):
501 """Clear AU process for given hostname and pid.
502
503 This clear includes:
504 1. kill process if it's alive.
505 2. delete the track status file of this process.
506 3. delete the executing log file of this process.
507
508 Args:
509 host_name: the host to execute auto-update.
510 pid: the background auto-update process id.
511 """
512 if cros_update_progress.IsProcessAlive(pid):
513 os.killpg(int(pid), signal.SIGKILL)
514
515 cros_update_progress.DelTrackStatusFile(host_name, pid)
516 cros_update_progress.DelExecuteLogFile(host_name, pid)
517
518
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700519class ApiRoot(object):
520 """RESTful API for Dev Server information."""
521 exposed = True
522
523 @cherrypy.expose
Gilad Arnold286a0062012-01-12 13:47:02 -0800524 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 11:36:27 -0700525 """Returns a JSON object containing a log of host event.
526
527 Args:
528 ip: address of host whose event log is requested, or `all'
Don Garrettf84631a2014-01-07 18:21:26 -0800529
Gilad Arnold1b908392012-10-05 11:36:27 -0700530 Returns:
Amin Hassani7c447852019-09-26 15:01:48 -0700531 A JSON dictionary containing all or some of the following fields:
Amin Hassanie7ead902019-10-11 16:42:43 -0700532 version: The Chromium OS version the device is running.
533 track: The channel the device is running on.
534 board: The device's board.
535 event_result: The event result of Omaha request.
536 event_type: The event type of Omaha request.
537 previous_version: The Chromium OS version we updated and rebooted from.
538 timestamp: The timestamp the event was received.
Amin Hassani7c447852019-09-26 15:01:48 -0700539 See the OmahaEvent class in update_engine/omaha_request_action.h for
540 event type and status code definitions. If the ip does not exist an empty
541 string is returned.
Gilad Arnold1b908392012-10-05 11:36:27 -0700542
543 Example URL:
544 http://myhost/api/hostlog?ip=192.168.1.5
545 """
Gilad Arnold286a0062012-01-12 13:47:02 -0800546 return updater.HandleHostLogPing(ip)
547
548 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800549 def fileinfo(self, *args):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700550 """Returns information about a given staged file.
551
552 Args:
Don Garrettf84631a2014-01-07 18:21:26 -0800553 args: path to the file inside the server's static staging directory
554
Gilad Arnold55a2a372012-10-02 09:46:32 -0700555 Returns:
556 A JSON encoded dictionary with information about the said file, which may
557 contain the following keys/values:
Gilad Arnold1b908392012-10-05 11:36:27 -0700558 size (int): the file size in bytes
Gilad Arnold1b908392012-10-05 11:36:27 -0700559 sha256 (string): a base64 encoded SHA256 hash
560
561 Example URL:
562 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 09:46:32 -0700563 """
Amin Hassani28df4212019-10-28 10:16:50 -0700564 # TODO(ahassani): A better way of doing this is to just return the the
565 # content of the payloads' property file instead. That has all this info
566 # except that the key for sha256 is 'sha256_hex', but still base64 encdoed.
567
Don Garrettf84631a2014-01-07 18:21:26 -0800568 file_path = os.path.join(updater.static_dir, *args)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700569 if not os.path.exists(file_path):
Amin Hassanid4e35392019-10-03 11:02:44 -0700570 raise DevServerError('file not found: %s' % file_path)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700571 try:
572 file_size = os.path.getsize(file_path)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700573 file_sha256 = common_util.GetFileSha256(file_path)
Amin Hassani469f5702019-10-21 15:35:06 -0700574 except os.error as e:
Amin Hassanid4e35392019-10-03 11:02:44 -0700575 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700576 'failed to get info for file %s: %s' % (file_path, e))
Gilad Arnolde74b3812013-04-22 11:27:38 -0700577
Gilad Arnolde74b3812013-04-22 11:27:38 -0700578 return json.dumps({
Amin Hassani28df4212019-10-28 10:16:50 -0700579 'size': file_size,
580 'sha256': file_sha256,
581 }, sort_keys=True)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700582
Chris Sosa76e44b92013-01-31 12:11:38 -0800583
David Rochberg7c79a812011-01-19 14:24:45 -0500584class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700585 """The Root Class for the Dev Server.
586
587 CherryPy works as follows:
588 For each method in this class, cherrpy interprets root/path
589 as a call to an instance of DevServerRoot->method_name. For example,
590 a call to http://myhost/build will call build. CherryPy automatically
591 parses http args and places them as keyword arguments in each method.
592 For paths http://myhost/update/dir1/dir2, you can use *args so that
593 cherrypy uses the update method and puts the extra paths in args.
594 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700595 # Method names that should not be listed on the index page.
596 _UNLISTED_METHODS = ['index', 'doc']
597
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700598 api = ApiRoot()
Chris Sosa7c931362010-10-11 19:49:01 -0700599
Dan Shi59ae7092013-06-04 14:37:27 -0700600 # Number of threads that devserver is staging images.
601 _staging_thread_count = 0
602 # Lock used to lock increasing/decreasing count.
603 _staging_thread_count_lock = threading.Lock()
604
joychen3cb228e2013-06-12 12:13:13 -0700605 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700606 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800607 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 12:13:13 -0700608 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 14:24:45 -0500609
Congbin Guo3afae6c2019-08-13 16:29:42 -0700610 @property
611 def staging_thread_count(self):
612 """Get the staging thread count."""
613 return self._staging_thread_count
Dan Shiafd0e492015-05-27 14:23:51 -0700614
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700615 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500616 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700617 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700618 import builder
619 if self._builder is None:
620 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500621 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700622
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700623 @cherrypy.expose
Dan Shif8eb0d12013-08-01 17:52:06 -0700624 def is_staged(self, **kwargs):
625 """Check if artifacts have been downloaded.
626
Congbin Guo3afae6c2019-08-13 16:29:42 -0700627 Examples:
628 To check if autotest and test_suites are staged:
629 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
630 artifacts=autotest,test_suites
631
Amin Hassani08e42d22019-06-03 00:31:30 -0700632 Args:
Chris Sosa6b0c6172013-08-05 17:01:33 -0700633 async: True to return without waiting for download to complete.
634 artifacts: Comma separated list of named artifacts to download.
635 These are defined in artifact_info and have their implementation
636 in build_artifact.py.
637 files: Comma separated list of file artifacts to stage. These
638 will be available as is in the corresponding static directory with no
639 custom post-processing.
640
Congbin Guo3afae6c2019-08-13 16:29:42 -0700641 Returns:
642 True of all artifacts are staged.
Dan Shif8eb0d12013-08-01 17:52:06 -0700643 """
Gabe Black3b567202015-09-23 14:07:59 -0700644 dl, factory = _get_downloader_and_factory(kwargs)
Aviv Keshet57d18172016-06-18 20:39:09 -0700645 response = str(dl.IsStaged(factory))
646 _Log('Responding to is_staged %s request with %r', kwargs, response)
647 return response
Dan Shi59ae7092013-06-04 14:37:27 -0700648
Chris Sosa76e44b92013-01-31 12:11:38 -0800649 @cherrypy.expose
Prashanth Ba06d2d22014-03-07 15:35:19 -0800650 def list_image_dir(self, **kwargs):
651 """Take an archive url and list the contents in its staged directory.
652
Amin Hassani08e42d22019-06-03 00:31:30 -0700653 Examples:
Prashanth Ba06d2d22014-03-07 15:35:19 -0800654 To list the contents of where this devserver should have staged
655 gs://image-archive/<board>-release/<build> call:
656 http://devserver_url:<port>/list_image_dir?archive_url=<gs://..>
657
Congbin Guo3afae6c2019-08-13 16:29:42 -0700658 Args:
659 archive_url: Google Storage URL for the build.
660
Prashanth Ba06d2d22014-03-07 15:35:19 -0800661 Returns:
662 A string with information about the contents of the image directory.
663 """
Gabe Black3b567202015-09-23 14:07:59 -0700664 dl = _get_downloader(kwargs)
Prashanth Ba06d2d22014-03-07 15:35:19 -0800665 try:
Gabe Black3b567202015-09-23 14:07:59 -0700666 image_dir_contents = dl.ListBuildDir()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800667 except build_artifact.ArtifactDownloadError as e:
668 return 'Cannot list the contents of staged artifacts. %s' % e
669 if not image_dir_contents:
Gabe Black3b567202015-09-23 14:07:59 -0700670 return '%s has not been staged on this devserver.' % dl.DescribeSource()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800671 return image_dir_contents
672
673 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800674 def stage(self, **kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700675 """Downloads and caches build artifacts.
Chris Sosa76e44b92013-01-31 12:11:38 -0800676
Gabe Black3b567202015-09-23 14:07:59 -0700677 Downloads and caches build artifacts, possibly from a Google Storage URL,
Dan Shi72b16132015-10-08 12:10:33 -0700678 or from Android's build server. Returns once these have been downloaded
Gabe Black3b567202015-09-23 14:07:59 -0700679 on the devserver. A call to this will attempt to cache non-specified
680 artifacts in the background for the given from the given URL following
681 the principle of spatial locality. Spatial locality of different
Chris Sosa76e44b92013-01-31 12:11:38 -0800682 artifacts is explicitly defined in the build_artifact module.
683
684 These artifacts will then be available from the static/ sub-directory of
685 the devserver.
686
Amin Hassani08e42d22019-06-03 00:31:30 -0700687 Examples:
Chris Sosa76e44b92013-01-31 12:11:38 -0800688 To download the autotest and test suites tarballs:
689 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
690 artifacts=autotest,test_suites
691 To download the full update payload:
692 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
693 artifacts=full_payload
Chris Sosa6b0c6172013-08-05 17:01:33 -0700694 To download just a file called blah.bin:
695 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
696 files=blah.bin
Chris Sosa76e44b92013-01-31 12:11:38 -0800697
698 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 16:39:34 -0700699 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 12:11:38 -0800700
701 Note for this example, relative path is the archive_url stripped of its
702 basename i.e. path/ in the examples above. Specific example:
703
704 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
705
706 Will get staged to:
707
joychened64b222013-06-21 16:39:34 -0700708 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Congbin Guo3afae6c2019-08-13 16:29:42 -0700709
710 Args:
711 archive_url: Google Storage URL for the build.
712 local_path: Local path for the build.
713 delete_source: Only meaningful with local_path. bool to indicate if the
714 source files should be deleted. This is especially useful when staging
715 a file locally in resource constrained environments as it allows us to
716 move the relevant files locally instead of copying them.
717 async: True to return without waiting for download to complete.
718 artifacts: Comma separated list of named artifacts to download.
719 These are defined in artifact_info and have their implementation
720 in build_artifact.py.
721 files: Comma separated list of files to stage. These
722 will be available as is in the corresponding static directory with no
723 custom post-processing.
724 clean: True to remove any previously staged artifacts first.
Chris Sosa76e44b92013-01-31 12:11:38 -0800725 """
Gabe Black3b567202015-09-23 14:07:59 -0700726 dl, factory = _get_downloader_and_factory(kwargs)
727
Dan Shi59ae7092013-06-04 14:37:27 -0700728 with DevServerRoot._staging_thread_count_lock:
729 DevServerRoot._staging_thread_count += 1
730 try:
Laurence Goodbyf5c958d2016-01-14 18:23:56 -0800731 boolean_string = kwargs.get('clean')
732 clean = xbuddy.XBuddy.ParseBoolean(boolean_string)
733 if clean and os.path.exists(dl.GetBuildDir()):
734 _Log('Removing %s' % dl.GetBuildDir())
735 shutil.rmtree(dl.GetBuildDir())
Achuith Bhandarkar2a1fcd82019-10-17 17:45:58 -0700736 is_async = kwargs.get('async', False)
737 dl.Download(factory, is_async=is_async)
Dan Shi59ae7092013-06-04 14:37:27 -0700738 finally:
739 with DevServerRoot._staging_thread_count_lock:
740 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800741 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700742
743 @cherrypy.expose
xixuan52c2fba2016-05-20 17:02:48 -0700744 def cros_au(self, **kwargs):
745 """Auto-update a CrOS DUT.
746
747 Args:
748 kwargs:
749 host_name: the hostname of the DUT to auto-update.
750 build_name: the build name for update the DUT.
751 force_update: Force an update even if the version installed is the
752 same. Default: False.
753 full_update: If True, do not run stateful update, directly force a full
754 reimage. If False, try stateful update first if the dut is already
755 installed with the same version.
756 async: Whether the auto_update function is ran in the background.
David Rileyee75de22017-11-02 10:48:15 -0700757 quick_provision: Whether the quick provision path is attempted first.
xixuan52c2fba2016-05-20 17:02:48 -0700758
759 Returns:
760 A tuple includes two elements:
761 a boolean variable represents whether the auto-update process is
762 successfully started.
763 an integer represents the background auto-update process id.
764 """
765 _check_base_args_for_auto_update(kwargs)
766
767 host_name = kwargs['host_name']
768 build_name = kwargs['build_name']
769 force_update = _parse_boolean_arg(kwargs, 'force_update')
770 full_update = _parse_boolean_arg(kwargs, 'full_update')
Achuith Bhandarkar2a1fcd82019-10-17 17:45:58 -0700771 is_async = _parse_boolean_arg(kwargs, 'async')
xixuanac89ce82016-11-30 16:48:20 -0800772 original_build = _parse_string_arg(kwargs, 'original_build')
David Haddock90e49442017-04-07 19:14:09 -0700773 payload_filename = _parse_string_arg(kwargs, 'payload_filename')
David Haddock20559612017-06-28 22:15:08 -0700774 clobber_stateful = _parse_boolean_arg(kwargs, 'clobber_stateful')
David Rileyee75de22017-11-02 10:48:15 -0700775 quick_provision = _parse_boolean_arg(kwargs, 'quick_provision')
776
777 devserver_url = updater.GetDevserverUrl()
778 static_url = updater.GetStaticUrl()
xixuan52c2fba2016-05-20 17:02:48 -0700779
Achuith Bhandarkar2a1fcd82019-10-17 17:45:58 -0700780 if is_async:
Amin Hassani469f5702019-10-21 15:35:06 -0700781 # Command of running auto-update.
Amin Hassani78520ae2019-10-29 13:26:51 -0700782 path = os.path.dirname(os.path.abspath(__file__))
783 execute_file = os.path.join(path, 'cros_update.py')
784 cmd = ['/usr/bin/python', '-u', execute_file, '-d', host_name,
785 '-b', build_name, '--static_dir', updater.static_dir]
xixuanac89ce82016-11-30 16:48:20 -0800786
787 # The original_build's format is like: link/3428.210.0
788 # The corresponding release_archive_url's format is like:
789 # gs://chromeos-releases/stable-channel/link/3428.210.0
790 if original_build:
791 release_archive_url = _build_uri_from_build_name(original_build)
792 # First staging the stateful.tgz synchronousely.
Amin Hassani469f5702019-10-21 15:35:06 -0700793 self.stage(files='stateful.tgz', is_async=False,
xixuanac89ce82016-11-30 16:48:20 -0800794 archive_url=release_archive_url)
Amin Hassani469f5702019-10-21 15:35:06 -0700795 cmd += ['--original_build', original_build]
xixuanac89ce82016-11-30 16:48:20 -0800796
xixuan52c2fba2016-05-20 17:02:48 -0700797 if force_update:
Amin Hassani469f5702019-10-21 15:35:06 -0700798 cmd += ['--force_update']
xixuan52c2fba2016-05-20 17:02:48 -0700799
800 if full_update:
Amin Hassani469f5702019-10-21 15:35:06 -0700801 cmd += ['--full_update']
xixuan52c2fba2016-05-20 17:02:48 -0700802
David Haddock90e49442017-04-07 19:14:09 -0700803 if payload_filename:
Amin Hassani469f5702019-10-21 15:35:06 -0700804 cmd += ['--payload_filename', payload_filename]
David Haddock90e49442017-04-07 19:14:09 -0700805
David Haddock20559612017-06-28 22:15:08 -0700806 if clobber_stateful:
Amin Hassani469f5702019-10-21 15:35:06 -0700807 cmd += ['--clobber_stateful']
David Haddock20559612017-06-28 22:15:08 -0700808
David Rileyee75de22017-11-02 10:48:15 -0700809 if quick_provision:
Amin Hassani469f5702019-10-21 15:35:06 -0700810 cmd += ['--quick_provision']
David Rileyee75de22017-11-02 10:48:15 -0700811
812 if devserver_url:
Amin Hassani469f5702019-10-21 15:35:06 -0700813 cmd += ['--devserver_url', devserver_url]
David Rileyee75de22017-11-02 10:48:15 -0700814
815 if static_url:
Amin Hassani469f5702019-10-21 15:35:06 -0700816 cmd += ['--static_url', static_url]
David Rileyee75de22017-11-02 10:48:15 -0700817
Amin Hassani78520ae2019-10-29 13:26:51 -0700818 p = subprocess.Popen(cmd, preexec_fn=os.setsid)
xixuan2a0970a2016-08-10 12:12:44 -0700819 pid = os.getpgid(p.pid)
xixuan52c2fba2016-05-20 17:02:48 -0700820
821 # Pre-write status in the track_status_file before the first call of
822 # 'get_au_status' to make sure that the track_status_file exists.
xixuan2a0970a2016-08-10 12:12:44 -0700823 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -0700824 progress_tracker.WriteStatus('CrOS update is just started.')
825
xixuan2a0970a2016-08-10 12:12:44 -0700826 return json.dumps((True, pid))
xixuan52c2fba2016-05-20 17:02:48 -0700827 else:
828 cros_update_trigger = cros_update.CrOSUpdateTrigger(
xixuanac89ce82016-11-30 16:48:20 -0800829 host_name, build_name, updater.static_dir, force_update=force_update,
David Rileyee75de22017-11-02 10:48:15 -0700830 full_update=full_update, original_build=original_build,
Amin Hassani78520ae2019-10-29 13:26:51 -0700831 payload_filename=payload_filename, quick_provision=quick_provision,
832 devserver_url=devserver_url, static_url=static_url)
xixuan52c2fba2016-05-20 17:02:48 -0700833 cros_update_trigger.TriggerAU()
xixuan27d50442017-08-09 10:38:25 -0700834 return json.dumps((True, -1))
xixuan52c2fba2016-05-20 17:02:48 -0700835
836 @cherrypy.expose
837 def get_au_status(self, **kwargs):
838 """Check if the auto-update task is finished.
839
840 It handles 4 cases:
841 1. If an error exists in the track_status_file, delete the track file and
842 raise it.
843 2. If cros-update process is finished, delete the file and return the
844 success result.
845 3. If the process is not running, delete the track file and raise an error
846 about 'the process is terminated due to unknown reason'.
847 4. If the track_status_file does not exist, kill the process if it exists,
848 and raise the IOError.
849
850 Args:
851 kwargs:
852 host_name: the hostname of the DUT to auto-update.
853 pid: the background process id of cros-update.
854
855 Returns:
xixuan28d99072016-10-06 12:24:16 -0700856 A dict with three elements:
xixuan52c2fba2016-05-20 17:02:48 -0700857 a boolean variable represents whether the auto-update process is
858 finished.
859 a string represents the current auto-update process status.
860 For example, 'Transfer Devserver/Stateful Update Package'.
xixuan28d99072016-10-06 12:24:16 -0700861 a detailed error message paragraph if there exists an Auto-Update
862 error, in which the last line shows the main exception. Empty
863 string otherwise.
xixuan52c2fba2016-05-20 17:02:48 -0700864 """
865 if 'host_name' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -0700866 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -0700867 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -0700868
869 if 'pid' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -0700870 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -0700871 KEY_ERROR_MSG % 'pid')
xixuan52c2fba2016-05-20 17:02:48 -0700872
873 host_name = kwargs['host_name']
874 pid = kwargs['pid']
875 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
876
xixuan28d99072016-10-06 12:24:16 -0700877 result_dict = {'finished': False, 'status': '', 'detailed_error_msg': ''}
xixuan52c2fba2016-05-20 17:02:48 -0700878 try:
879 result = progress_tracker.ReadStatus()
880 if result.startswith(cros_update_progress.ERROR_TAG):
xixuan28d99072016-10-06 12:24:16 -0700881 result_dict['detailed_error_msg'] = result[len(
882 cros_update_progress.ERROR_TAG):]
xixuan28681fd2016-11-23 11:13:56 -0800883 elif result == cros_update_progress.FINISHED:
xixuan28d99072016-10-06 12:24:16 -0700884 result_dict['finished'] = True
885 result_dict['status'] = result
xixuan28681fd2016-11-23 11:13:56 -0800886 elif not cros_update_progress.IsProcessAlive(pid):
xixuan28d99072016-10-06 12:24:16 -0700887 result_dict['detailed_error_msg'] = (
888 'Cros_update process terminated midway due to unknown reason. '
889 'Last update status was %s' % result)
xixuan28681fd2016-11-23 11:13:56 -0800890 else:
891 result_dict['status'] = result
892 except IOError as e:
893 if pid and cros_update_progress.IsProcessAlive(pid):
xixuan2a0970a2016-08-10 12:12:44 -0700894 os.killpg(int(pid), signal.SIGKILL)
xixuan52c2fba2016-05-20 17:02:48 -0700895
xixuan28681fd2016-11-23 11:13:56 -0800896 result_dict['detailed_error_msg'] = str(e)
897
898 return json.dumps(result_dict)
xixuan52c2fba2016-05-20 17:02:48 -0700899
900 @cherrypy.expose
David Riley6d5fca02017-10-31 10:35:47 -0700901 def post_au_status(self, status, **kwargs):
902 """Updates the status of an auto-update task.
903
904 Callers will need to POST to this URL with a body of MIME-type
905 "multipart/form-data".
906 The body should include a single argument, 'status', containing the
907 AU status to record.
908
909 Args:
910 status: The updated status.
911 kwargs:
912 host_name: the hostname of the DUT to auto-update.
913 pid: the background process id of cros-update.
914 """
915 if 'host_name' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -0700916 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -0700917 KEY_ERROR_MSG % 'host_name')
David Riley6d5fca02017-10-31 10:35:47 -0700918
919 if 'pid' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -0700920 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -0700921 KEY_ERROR_MSG % 'pid')
David Riley6d5fca02017-10-31 10:35:47 -0700922
923 host_name = kwargs['host_name']
924 pid = kwargs['pid']
David Riley3cea2582017-11-24 22:03:01 -0800925 status = status.rstrip()
926 _Log('Recording status for %s (%s): %s' % (host_name, pid, status))
David Riley6d5fca02017-10-31 10:35:47 -0700927 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
928
David Riley3cea2582017-11-24 22:03:01 -0800929 progress_tracker.WriteStatus(status)
David Riley6d5fca02017-10-31 10:35:47 -0700930
931 return 'True'
932
933 @cherrypy.expose
xixuan52c2fba2016-05-20 17:02:48 -0700934 def handler_cleanup(self, **kwargs):
xixuan3bc974e2016-10-18 17:21:43 -0700935 """Clean track status log and temp directory for CrOS auto-update process.
xixuan52c2fba2016-05-20 17:02:48 -0700936
937 Args:
938 kwargs:
939 host_name: the hostname of the DUT to auto-update.
940 pid: the background process id of cros-update.
941 """
942 if 'host_name' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -0700943 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -0700944 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -0700945
946 if 'pid' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -0700947 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -0700948 KEY_ERROR_MSG % 'pid')
xixuan52c2fba2016-05-20 17:02:48 -0700949
950 host_name = kwargs['host_name']
951 pid = kwargs['pid']
952 cros_update_progress.DelTrackStatusFile(host_name, pid)
xixuan3bc974e2016-10-18 17:21:43 -0700953 cros_update_progress.DelAUTempDirectory(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -0700954
955 @cherrypy.expose
956 def kill_au_proc(self, **kwargs):
957 """Kill CrOS auto-update process using given process id.
958
959 Args:
960 kwargs:
961 host_name: Kill all the CrOS auto-update process of this host.
962
963 Returns:
964 True if all processes are killed properly.
965 """
966 if 'host_name' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -0700967 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -0700968 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -0700969
xixuan447ad9d2017-02-28 14:46:20 -0800970 cur_pid = kwargs.get('pid')
971
xixuan52c2fba2016-05-20 17:02:48 -0700972 host_name = kwargs['host_name']
xixuan3bc974e2016-10-18 17:21:43 -0700973 track_log_list = cros_update_progress.GetAllTrackStatusFileByHostName(
974 host_name)
xixuan52c2fba2016-05-20 17:02:48 -0700975 for log in track_log_list:
976 # The track log's full path is: path/host_name_pid.log
977 # Use splitext to remove file extension, then parse pid from the
978 # filename.
Congbin Guo3afae6c2019-08-13 16:29:42 -0700979 pid = os.path.splitext(os.path.basename(log))[0][len(host_name) + 1:]
xixuan447ad9d2017-02-28 14:46:20 -0800980 _clear_process(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -0700981
xixuan447ad9d2017-02-28 14:46:20 -0800982 if cur_pid:
983 _clear_process(host_name, cur_pid)
xixuan52c2fba2016-05-20 17:02:48 -0700984
985 return 'True'
986
987 @cherrypy.expose
988 def collect_cros_au_log(self, **kwargs):
989 """Collect CrOS auto-update log.
990
991 Args:
992 kwargs:
993 host_name: the hostname of the DUT to auto-update.
994 pid: the background process id of cros-update.
995
996 Returns:
David Haddock9f459632017-05-11 14:45:46 -0700997 A dictionary containing the execute log file and any hostlog files.
xixuan52c2fba2016-05-20 17:02:48 -0700998 """
999 if 'host_name' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -07001000 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -07001001 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -07001002
1003 if 'pid' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -07001004 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -07001005 KEY_ERROR_MSG % 'pid')
xixuan52c2fba2016-05-20 17:02:48 -07001006
1007 host_name = kwargs['host_name']
1008 pid = kwargs['pid']
xixuan3bc974e2016-10-18 17:21:43 -07001009
1010 # Fetch the execute log recorded by cros_update_progress.
xixuan1bbfaba2016-10-13 17:53:22 -07001011 au_log = cros_update_progress.ReadExecuteLogFile(host_name, pid)
1012 cros_update_progress.DelExecuteLogFile(host_name, pid)
David Haddock9f459632017-05-11 14:45:46 -07001013 # Fetch the cros_au host_logs if they exist
1014 au_hostlogs = cros_update_progress.ReadAUHostLogFiles(host_name, pid)
1015 return json.dumps({'cros_au_log': au_log, 'host_logs': au_hostlogs})
xixuan1bbfaba2016-10-13 17:53:22 -07001016
xixuan52c2fba2016-05-20 17:02:48 -07001017 @cherrypy.expose
Dan Shi2f136862016-02-11 15:38:38 -08001018 def locate_file(self, **kwargs):
1019 """Get the path to the given file name.
1020
1021 This method looks up the given file name inside specified build artifacts.
1022 One use case is to help caller to locate an apk file inside a build
1023 artifact. The location of the apk file could be different based on the
1024 branch and target.
1025
1026 Args:
1027 file_name: Name of the file to look for.
1028 artifacts: A list of artifact names to search for the file.
1029
1030 Returns:
1031 Path to the file with the given name. It's relative to the folder for the
1032 build, e.g., DATA/priv-app/sl4a/sl4a.apk
Dan Shi2f136862016-02-11 15:38:38 -08001033 """
1034 dl, _ = _get_downloader_and_factory(kwargs)
1035 try:
Joe Brennan1691f8e2017-03-15 15:53:36 -07001036 file_name = kwargs['file_name']
Dan Shi2f136862016-02-11 15:38:38 -08001037 artifacts = kwargs['artifacts']
1038 except KeyError:
Amin Hassanid4e35392019-10-03 11:02:44 -07001039 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -07001040 '`file_name` and `artifacts` are required to search '
1041 'for a file in build artifacts.')
Dan Shi2f136862016-02-11 15:38:38 -08001042 build_path = dl.GetBuildDir()
1043 for artifact in artifacts:
1044 # Get the unzipped folder of the artifact. If it's not defined in
1045 # ARTIFACT_UNZIP_FOLDER_MAP, assume the files are unzipped to the build
1046 # directory directly.
1047 folder = artifact_info.ARTIFACT_UNZIP_FOLDER_MAP.get(artifact, '')
1048 artifact_path = os.path.join(build_path, folder)
1049 for root, _, filenames in os.walk(artifact_path):
Joe Brennan1691f8e2017-03-15 15:53:36 -07001050 if file_name in set([f for f in filenames]):
Dan Shi2f136862016-02-11 15:38:38 -08001051 return os.path.relpath(os.path.join(root, file_name), build_path)
Amin Hassanid4e35392019-10-03 11:02:44 -07001052 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -07001053 'File `%s` can not be found in artifacts: %s' % (file_name, artifacts))
Dan Shi2f136862016-02-11 15:38:38 -08001054
1055 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -08001056 def setup_telemetry(self, **kwargs):
1057 """Extracts and sets up telemetry
1058
1059 This method goes through the telemetry deps packages, and stages them on
1060 the devserver to be used by the drones and the telemetry tests.
1061
1062 Args:
1063 archive_url: Google Storage URL for the build.
1064
1065 Returns:
1066 Path to the source folder for the telemetry codebase once it is staged.
1067 """
Gabe Black3b567202015-09-23 14:07:59 -07001068 dl = _get_downloader(kwargs)
Simran Basi4baad082013-02-14 13:39:18 -08001069
Gabe Black3b567202015-09-23 14:07:59 -07001070 build_path = dl.GetBuildDir()
Simran Basi4baad082013-02-14 13:39:18 -08001071 deps_path = os.path.join(build_path, 'autotest/packages')
1072 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
1073 src_folder = os.path.join(telemetry_path, 'src')
1074
1075 with self._telemetry_lock_dict.lock(telemetry_path):
1076 if os.path.exists(src_folder):
1077 # Telemetry is already fully stage return
1078 return src_folder
1079
1080 common_util.MkDirP(telemetry_path)
1081
1082 # Copy over the required deps tar balls to the telemetry directory.
1083 for dep in TELEMETRY_DEPS:
1084 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -07001085 if not os.path.exists(dep_path):
1086 # This dep does not exist (could be new), do not extract it.
1087 continue
Simran Basi4baad082013-02-14 13:39:18 -08001088 try:
1089 common_util.ExtractTarball(dep_path, telemetry_path)
1090 except common_util.CommonUtilError as e:
1091 shutil.rmtree(telemetry_path)
Amin Hassanid4e35392019-10-03 11:02:44 -07001092 raise DevServerError(str(e))
Simran Basi4baad082013-02-14 13:39:18 -08001093
1094 # By default all the tarballs extract to test_src but some parts of
1095 # the telemetry code specifically hardcoded to exist inside of 'src'.
1096 test_src = os.path.join(telemetry_path, 'test_src')
1097 try:
1098 shutil.move(test_src, src_folder)
1099 except shutil.Error:
1100 # This can occur if src_folder already exists. Remove and retry move.
1101 shutil.rmtree(src_folder)
Amin Hassanid4e35392019-10-03 11:02:44 -07001102 raise DevServerError(
Gabe Black3b567202015-09-23 14:07:59 -07001103 'Failure in telemetry setup for build %s. Appears that the '
1104 'test_src to src move failed.' % dl.GetBuild())
Simran Basi4baad082013-02-14 13:39:18 -08001105
1106 return src_folder
1107
1108 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -08001109 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -07001110 """Symbolicates a minidump using pre-downloaded symbols, returns it.
1111
1112 Callers will need to POST to this URL with a body of MIME-type
1113 "multipart/form-data".
1114 The body should include a single argument, 'minidump', containing the
1115 binary-formatted minidump to symbolicate.
1116
Chris Masone816e38c2012-05-02 12:22:36 -07001117 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -08001118 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -07001119 minidump: The binary minidump file to symbolicate.
1120 """
Chris Sosa76e44b92013-01-31 12:11:38 -08001121 # Ensure the symbols have been staged.
Dan Shif08fe492016-10-04 14:39:25 -07001122 # Try debug.tar.xz first, then debug.tgz
1123 for artifact in (artifact_info.SYMBOLS_ONLY, artifact_info.SYMBOLS):
1124 kwargs['artifacts'] = artifact
1125 dl = _get_downloader(kwargs)
1126
1127 try:
1128 if self.stage(**kwargs) == 'Success':
1129 break
1130 except build_artifact.ArtifactDownloadError:
1131 continue
1132 else:
Amin Hassanid4e35392019-10-03 11:02:44 -07001133 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -07001134 'Failed to stage symbols for %s' % dl.DescribeSource())
Chris Sosa76e44b92013-01-31 12:11:38 -08001135
Chris Masone816e38c2012-05-02 12:22:36 -07001136 to_return = ''
1137 with tempfile.NamedTemporaryFile() as local:
1138 while True:
1139 data = minidump.file.read(8192)
1140 if not data:
1141 break
1142 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -08001143
Chris Masone816e38c2012-05-02 12:22:36 -07001144 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -08001145
Gabe Black3b567202015-09-23 14:07:59 -07001146 symbols_directory = os.path.join(dl.GetBuildDir(), 'debug', 'breakpad')
Chris Sosa76e44b92013-01-31 12:11:38 -08001147
xixuanab744382017-04-27 10:41:27 -07001148 # The location of minidump_stackwalk is defined in chromeos-admin.
Chris Sosa76e44b92013-01-31 12:11:38 -08001149 stackwalk = subprocess.Popen(
xixuanab744382017-04-27 10:41:27 -07001150 ['/usr/local/bin/minidump_stackwalk', local.name, symbols_directory],
Chris Sosa76e44b92013-01-31 12:11:38 -08001151 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1152
Chris Masone816e38c2012-05-02 12:22:36 -07001153 to_return, error_text = stackwalk.communicate()
1154 if stackwalk.returncode != 0:
Amin Hassanid4e35392019-10-03 11:02:44 -07001155 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -07001156 "Can't generate stack trace: %s (rc=%d)" % (error_text,
1157 stackwalk.returncode))
Chris Masone816e38c2012-05-02 12:22:36 -07001158
1159 return to_return
1160
1161 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -08001162 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 15:31:36 -04001163 """Return a string representing the latest build for a given target.
1164
1165 Args:
1166 target: The build target, typically a combination of the board and the
1167 type of build e.g. x86-mario-release.
1168 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
1169 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-07 18:21:26 -08001170
Scott Zawalski16954532012-03-20 15:31:36 -04001171 Returns:
1172 A string representation of the latest build if one exists, i.e.
1173 R19-1993.0.0-a1-b1480.
1174 An empty string if no latest could be found.
1175 """
Don Garrettf84631a2014-01-07 18:21:26 -08001176 if not kwargs:
Scott Zawalski16954532012-03-20 15:31:36 -04001177 return _PrintDocStringAsHTML(self.latestbuild)
1178
Don Garrettf84631a2014-01-07 18:21:26 -08001179 if 'target' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -07001180 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -07001181 'Error: target= is required!')
Dan Shi61305df2015-10-26 16:52:35 -07001182
1183 if _is_android_build_request(kwargs):
1184 branch = kwargs.get('branch', None)
1185 target = kwargs.get('target', None)
1186 if not target or not branch:
Amin Hassanid4e35392019-10-03 11:02:44 -07001187 raise DevServerError('Both target and branch must be specified to query'
1188 ' for the latest Android build.')
Dan Shi61305df2015-10-26 16:52:35 -07001189 return android_build.BuildAccessor.GetLatestBuildID(target, branch)
1190
Scott Zawalski16954532012-03-20 15:31:36 -04001191 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001192 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-07 18:21:26 -08001193 updater.static_dir, kwargs['target'],
1194 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -07001195 except common_util.CommonUtilError as errmsg:
Amin Hassanid4e35392019-10-03 11:02:44 -07001196 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -07001197 str(errmsg))
Scott Zawalski16954532012-03-20 15:31:36 -04001198
1199 @cherrypy.expose
xixuan7efd0002016-04-14 15:34:01 -07001200 def list_suite_controls(self, **kwargs):
1201 """Return a list of contents of all known control files.
1202
1203 Example URL:
1204 To List all control files' content:
1205 http://dev-server/list_suite_controls?suite_name=bvt&
1206 build=daisy_spring-release/R29-4279.0.0
1207
1208 Args:
1209 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
1210 suite_name: List the control files belonging to that suite.
1211
1212 Returns:
Dan Shia1cd6522016-04-18 16:07:21 -07001213 A dictionary of all control files's path to its content for given suite.
xixuan7efd0002016-04-14 15:34:01 -07001214 """
1215 if not kwargs:
1216 return _PrintDocStringAsHTML(self.controlfiles)
1217
1218 if 'build' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -07001219 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -07001220 'Error: build= is required!')
xixuan7efd0002016-04-14 15:34:01 -07001221
1222 if 'suite_name' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -07001223 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Dan Shia1cd6522016-04-18 16:07:21 -07001224 'Error: suite_name= is required!')
xixuan7efd0002016-04-14 15:34:01 -07001225
1226 control_file_list = [
1227 line.rstrip() for line in common_util.GetControlFileListForSuite(
1228 updater.static_dir, kwargs['build'],
1229 kwargs['suite_name']).splitlines()]
1230
Dan Shia1cd6522016-04-18 16:07:21 -07001231 control_file_content_dict = {}
xixuan7efd0002016-04-14 15:34:01 -07001232 for control_path in control_file_list:
Dan Shia1cd6522016-04-18 16:07:21 -07001233 control_file_content_dict[control_path] = (common_util.GetControlFile(
xixuan7efd0002016-04-14 15:34:01 -07001234 updater.static_dir, kwargs['build'], control_path))
1235
Dan Shia1cd6522016-04-18 16:07:21 -07001236 return json.dumps(control_file_content_dict)
xixuan7efd0002016-04-14 15:34:01 -07001237
1238 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -08001239 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 17:17:28 -05001240 """Return a control file or a list of all known control files.
1241
1242 Example URL:
1243 To List all control files:
beepsbd337242013-07-09 22:44:06 -07001244 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
1245 To List all control files for, say, the bvt suite:
1246 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -05001247 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -05001248 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 -05001249
1250 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -05001251 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -05001252 control_path: If you want the contents of a control file set this
1253 to the path. E.g. client/site_tests/sleeptest/control
1254 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -07001255 suite_name: If control_path is not specified but a suite_name is
1256 specified, list the control files belonging to that suite instead of
1257 all control files. The empty string for suite_name will list all control
1258 files for the build.
Don Garrettf84631a2014-01-07 18:21:26 -08001259
Scott Zawalski4647ce62012-01-03 17:17:28 -05001260 Returns:
1261 Contents of a control file if control_path is provided.
1262 A list of control files if no control_path is provided.
1263 """
Don Garrettf84631a2014-01-07 18:21:26 -08001264 if not kwargs:
Scott Zawalski4647ce62012-01-03 17:17:28 -05001265 return _PrintDocStringAsHTML(self.controlfiles)
1266
Don Garrettf84631a2014-01-07 18:21:26 -08001267 if 'build' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -07001268 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -07001269 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -05001270
Don Garrettf84631a2014-01-07 18:21:26 -08001271 if 'control_path' not in kwargs:
1272 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-09 22:44:06 -07001273 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-07 18:21:26 -08001274 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-09 22:44:06 -07001275 else:
1276 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-07 18:21:26 -08001277 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -05001278 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001279 return common_util.GetControlFile(
Don Garrettf84631a2014-01-07 18:21:26 -08001280 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -08001281
1282 @cherrypy.expose
Simran Basi99e63c02014-05-20 10:39:52 -07001283 def xbuddy_translate(self, *args, **kwargs):
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001284 """Translates an xBuddy path to a real path to artifact if it exists.
1285
1286 Args:
Simran Basi99e63c02014-05-20 10:39:52 -07001287 args: An xbuddy path in the form of {local|remote}/build_id/artifact.
1288 Local searches the devserver's static directory. Remote searches a
1289 Google Storage image archive.
1290
1291 Kwargs:
1292 image_dir: Google Storage image archive to search in if requesting a
1293 remote artifact. If none uses the default bucket.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001294
1295 Returns:
Simran Basi99e63c02014-05-20 10:39:52 -07001296 String in the format of build_id/artifact as stored on the local server
1297 or in Google Storage.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001298 """
Simran Basi99e63c02014-05-20 10:39:52 -07001299 build_id, filename = self._xbuddy.Translate(
Gabe Black3b567202015-09-23 14:07:59 -07001300 args, image_dir=kwargs.get('image_dir'))
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001301 response = os.path.join(build_id, filename)
1302 _Log('Path translation requested, returning: %s', response)
1303 return response
1304
1305 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -07001306 def xbuddy(self, *args, **kwargs):
1307 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -07001308
1309 Args:
joycheneaf4cfc2013-07-02 08:38:57 -07001310 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -07001311 components of the path. The path can be understood as
1312 "{local|remote}/build_id/artifact" where build_id is composed of
1313 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -07001314
joychen121fc9b2013-08-02 14:30:30 -07001315 The first path element is optional, and can be "remote" or "local"
1316 If local (the default), devserver will not attempt to access Google
1317 Storage, and will only search the static directory for the files.
1318 If remote, devserver will try to obtain the artifact off GS if it's
1319 not found locally.
1320 The board is the familiar board name, optionally suffixed.
1321 The version can be the google storage version number, and may also be
1322 any of a number of xBuddy defined version aliases that will be
1323 translated into the latest built image that fits the description.
1324 Defaults to latest.
1325 The artifact is one of a number of image or artifact aliases used by
1326 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -07001327
1328 Kwargs:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001329 for_update: {true|false}
Amin Hassanie9ffb862019-09-25 17:10:40 -07001330 if true, prepares the update payloads for the image,
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001331 and returns the update uri to pass to the
1332 update_engine_client.
joychen3cb228e2013-06-12 12:13:13 -07001333 return_dir: {true|false}
1334 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001335 relative_path: {true|false}
1336 if set to true, returns the relative path to the payload
1337 directory from static_dir.
joychen3cb228e2013-06-12 12:13:13 -07001338 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -07001339 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -07001340 or
joycheneaf4cfc2013-07-02 08:38:57 -07001341 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -07001342
1343 Returns:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001344 If |for_update|, returns a redirect to the image or update file
1345 on the devserver. E.g.,
1346 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
1347 chromium-test-image.bin
1348 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
1349 http://host:port/static/x86-generic-release/R26-4000.0.0/
1350 If |relative_path| is true, return a relative path the folder where the
1351 payloads are. E.g.,
1352 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 12:13:13 -07001353 """
Chris Sosa75490802013-09-30 17:21:45 -07001354 boolean_string = kwargs.get('for_update')
1355 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001356 boolean_string = kwargs.get('return_dir')
1357 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
1358 boolean_string = kwargs.get('relative_path')
1359 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -07001360
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001361 if return_dir and relative_path:
Chris Sosa4b951602014-04-09 20:26:07 -07001362 raise common_util.DevServerHTTPError(
Amin Hassanid4e35392019-10-03 11:02:44 -07001363 http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -07001364 'Cannot specify both return_dir and relative_path')
Chris Sosa75490802013-09-30 17:21:45 -07001365
1366 # For updates, we optimize downloading of test images.
1367 file_name = None
1368 build_id = None
1369 if for_update:
1370 try:
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001371 build_id = self._xbuddy.StageTestArtifactsForUpdate(args)
Chris Sosa75490802013-09-30 17:21:45 -07001372 except build_artifact.ArtifactDownloadError:
1373 build_id = None
1374
1375 if not build_id:
1376 build_id, file_name = self._xbuddy.Get(args)
1377
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001378 if for_update:
Amin Hassanie9ffb862019-09-25 17:10:40 -07001379 _Log('Payloads requested.')
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001380 # Forces payload to be in cache and symlinked into build_id dir.
Amin Hassanie9ffb862019-09-25 17:10:40 -07001381 updater.GetUpdateForLabel(build_id)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001382
1383 response = None
1384 if return_dir:
1385 response = os.path.join(cherrypy.request.base, 'static', build_id)
1386 _Log('Directory requested, returning: %s', response)
1387 elif relative_path:
1388 response = build_id
1389 _Log('Relative path requested, returning: %s', response)
1390 elif for_update:
1391 response = os.path.join(cherrypy.request.base, 'update', build_id)
1392 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 12:13:13 -07001393 else:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001394 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 14:30:30 -07001395 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001396 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 14:30:30 -07001397 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -07001398
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001399 return response
1400
joychen3cb228e2013-06-12 12:13:13 -07001401 @cherrypy.expose
1402 def xbuddy_list(self):
1403 """Lists the currently available images & time since last access.
1404
Gilad Arnold452fd272014-02-04 11:09:28 -08001405 Returns:
1406 A string representation of a list of tuples [(build_id, time since last
1407 access),...]
joychen3cb228e2013-06-12 12:13:13 -07001408 """
1409 return self._xbuddy.List()
1410
1411 @cherrypy.expose
1412 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 11:09:28 -08001413 """Returns the number of images cached by xBuddy."""
joychen3cb228e2013-06-12 12:13:13 -07001414 return self._xbuddy.Capacity()
1415
1416 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001417 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001418 """Presents a welcome message and documentation links."""
Congbin Guo6bc32182019-08-20 17:54:30 -07001419 html_template = (
1420 'Welcome to the Dev Server!<br>\n'
1421 '<br>\n'
1422 'Here are the available methods, click for documentation:<br>\n'
1423 '<br>\n'
1424 '%s')
1425
1426 exposed_methods = []
1427 for app in cherrypy.tree.apps.values():
1428 exposed_methods += _FindExposedMethods(
1429 app.root, app.script_name.lstrip('/'),
1430 unlisted=self._UNLISTED_METHODS)
1431
1432 return html_template % '<br>\n'.join(
1433 ['<a href=doc/%s>%s</a>' % (name, name)
1434 for name in sorted(exposed_methods)])
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001435
1436 @cherrypy.expose
1437 def doc(self, *args):
1438 """Shows the documentation for available methods / URLs.
1439
Amin Hassani08e42d22019-06-03 00:31:30 -07001440 Examples:
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001441 http://myhost/doc/update
1442 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001443 name = '/'.join(args)
Congbin Guo6bc32182019-08-20 17:54:30 -07001444 method = _GetExposedMethod(name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001445 if not method:
Amin Hassanid4e35392019-10-03 11:02:44 -07001446 raise DevServerError("No exposed method named `%s'" % name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001447 if not method.__doc__:
Amin Hassanid4e35392019-10-03 11:02:44 -07001448 raise DevServerError("No documentation for exposed method `%s'" % name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001449 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -07001450
Dale Curtisc9aaf3a2011-08-09 15:47:40 -07001451 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001452 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001453 """Handles an update check from a Chrome OS client.
1454
1455 The HTTP request should contain the standard Omaha-style XML blob. The URL
1456 line may contain an additional intermediate path to the update payload.
1457
joychen121fc9b2013-08-02 14:30:30 -07001458 This request can be handled in one of 4 ways, depending on the devsever
1459 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -07001460
Amin Hassanie9ffb862019-09-25 17:10:40 -07001461 1. No intermediate path. DEPRECATED
joychen121fc9b2013-08-02 14:30:30 -07001462
1463 2. Path explicitly invokes XBuddy
1464 If there is a path given, it can explicitly invoke xbuddy by prefixing it
1465 with 'xbuddy'. This path is then used to acquire an image binary for the
1466 devserver to generate an update payload from. Devserver then serves this
1467 payload.
1468
1469 3. Path is left for the devserver to interpret.
1470 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
1471 to generate a payload from the test image in that directory and serve it.
1472
joychen121fc9b2013-08-02 14:30:30 -07001473 Examples:
joychen121fc9b2013-08-02 14:30:30 -07001474 2. Explicitly invoke xbuddy
1475 update_engine_client --omaha_url=
1476 http://myhost/update/xbuddy/remote/board/version/dev
1477 This would go to GS to download the dev image for the board, from which
1478 the devserver would generate a payload to serve.
1479
1480 3. Give a path for devserver to interpret
1481 update_engine_client --omaha_url=http://myhost/update/some/random/path
1482 This would attempt, in order to:
1483 a) Generate an update from a test image binary if found in
1484 static_dir/some/random/path.
1485 b) Serve an update payload found in static_dir/some/random/path.
1486 c) Hope that some/random/path takes the form "board/version" and
1487 and attempt to download an update payload for that board/version
1488 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001489 """
joychen121fc9b2013-08-02 14:30:30 -07001490 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -08001491 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -07001492 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-11 19:49:01 -07001493
joychen121fc9b2013-08-02 14:30:30 -07001494 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001495
Dan Shif5ce2de2013-04-25 16:06:32 -07001496
Chris Sosadbc20082012-12-10 13:39:11 -08001497def _CleanCache(cache_dir, wipe):
1498 """Wipes any excess cached items in the cache_dir.
1499
1500 Args:
1501 cache_dir: the directory we are wiping from.
1502 wipe: If True, wipe all the contents -- not just the excess.
1503 """
1504 if wipe:
1505 # Clear the cache and exit on error.
1506 cmd = 'rm -rf %s/*' % cache_dir
1507 if os.system(cmd) != 0:
1508 _Log('Failed to clear the cache with %s' % cmd)
1509 sys.exit(1)
1510 else:
1511 # Clear all but the last N cached updates
1512 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
1513 (cache_dir, CACHED_ENTRIES))
1514 if os.system(cmd) != 0:
1515 _Log('Failed to clean up old delta cache files with %s' % cmd)
1516 sys.exit(1)
1517
1518
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001519def _AddTestingOptions(parser):
1520 group = optparse.OptionGroup(
1521 parser, 'Advanced Testing Options', 'These are used by test scripts and '
1522 'developers writing integration tests utilizing the devserver. They are '
1523 'not intended to be really used outside the scope of someone '
1524 'knowledgable about the test.')
1525 group.add_option('--exit',
1526 action='store_true',
Amin Hassanie9ffb862019-09-25 17:10:40 -07001527 help='do not start the server (yet clear cache)')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001528 group.add_option('--host_log',
1529 action='store_true', default=False,
1530 help='record history of host update events (/api/hostlog)')
1531 group.add_option('--max_updates',
Gabe Black3b567202015-09-23 14:07:59 -07001532 metavar='NUM', default=-1, type='int',
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001533 help='maximum number of update checks handled positively '
1534 '(default: unlimited)')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001535 group.add_option('--proxy_port',
1536 metavar='PORT', default=None, type='int',
1537 help='port to have the client connect to -- basically the '
1538 'devserver lies to the update to tell it to get the payload '
1539 'from a different port that will proxy the request back to '
1540 'the devserver. The proxy must be managed outside the '
1541 'devserver.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001542 parser.add_option_group(group)
1543
1544
1545def _AddUpdateOptions(parser):
1546 group = optparse.OptionGroup(
1547 parser, 'Autoupdate Options', 'These options can be used to change '
Amin Hassanie9ffb862019-09-25 17:10:40 -07001548 'how the devserver serve update payloads. Please '
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001549 'note that all of these option affect how a payload is generated and so '
1550 'do not work in archive-only mode.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001551 group.add_option('--critical_update',
1552 action='store_true', default=False,
1553 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001554 group.add_option('--payload',
1555 metavar='PATH',
1556 help='use the update payload from specified directory '
1557 '(update.gz).')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001558 parser.add_option_group(group)
1559
1560
1561def _AddProductionOptions(parser):
1562 group = optparse.OptionGroup(
1563 parser, 'Advanced Server Options', 'These options can be used to changed '
1564 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001565 group.add_option('--clear_cache',
1566 action='store_true', default=False,
1567 help='At startup, removes all cached entries from the'
Amin Hassanid4e35392019-10-03 11:02:44 -07001568 "devserver's cache.")
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001569 group.add_option('--logfile',
1570 metavar='PATH',
1571 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -07001572 group.add_option('--pidfile',
1573 metavar='PATH',
1574 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 11:04:13 -08001575 group.add_option('--portfile',
1576 metavar='PATH',
1577 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001578 group.add_option('--production',
1579 action='store_true', default=False,
1580 help='have the devserver use production values when '
1581 'starting up. This includes using more threads and '
1582 'performing less logging.')
1583 parser.add_option_group(group)
1584
1585
Paul Hobbsef4e0702016-06-27 17:01:42 -07001586def MakeLogHandler(logfile):
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001587 """Create a LogHandler instance used to log all messages."""
1588 hdlr_cls = handlers.TimedRotatingFileHandler
1589 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
xixuan3d48bff2017-01-30 19:00:09 -08001590 interval=_LOG_ROTATION_INTERVAL,
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001591 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -07001592 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001593 return hdlr
1594
1595
Chris Sosacde6bf42012-05-31 18:36:39 -07001596def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001597 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -08001598 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -07001599
1600 # get directory that the devserver is run from
1601 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -07001602 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -07001603 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001604 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -07001605 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -07001606 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001607 parser.add_option('--port',
1608 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 13:13:28 -08001609 help=('port for the dev server to use; if zero, binds to '
1610 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001611 parser.add_option('-t', '--test_image',
1612 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -07001613 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -07001614 parser.add_option('-x', '--xbuddy_manage_builds',
1615 action='store_true',
1616 default=False,
1617 help='If set, allow xbuddy to manage images in'
1618 'build/images.')
Dan Shi72b16132015-10-08 12:10:33 -07001619 parser.add_option('-a', '--android_build_credential',
1620 default=None,
1621 help='Path to a json file which contains the credential '
1622 'needed to access Android builds.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001623 _AddProductionOptions(parser)
1624 _AddUpdateOptions(parser)
1625 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -07001626 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001627
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001628 # Handle options that must be set globally in cherrypy. Do this
1629 # work up front, because calls to _Log() below depend on this
1630 # initialization.
1631 if options.production:
1632 cherrypy.config.update({'environment': 'production'})
1633 if not options.logfile:
1634 cherrypy.config.update({'log.screen': True})
1635 else:
1636 cherrypy.config.update({'log.error_file': '',
1637 'log.access_file': ''})
Paul Hobbsef4e0702016-06-27 17:01:42 -07001638 hdlr = MakeLogHandler(options.logfile)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001639 # Pylint can't seem to process these two calls properly
1640 # pylint: disable=E1101
1641 cherrypy.log.access_log.addHandler(hdlr)
1642 cherrypy.log.error_log.addHandler(hdlr)
1643 # pylint: enable=E1101
1644
joychened64b222013-06-21 16:39:34 -07001645 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001646 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001647
joychened64b222013-06-21 16:39:34 -07001648 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001649 # If our devserver is only supposed to serve payloads, we shouldn't be
1650 # mucking with the cache at all. If the devserver hadn't previously
1651 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001652 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001653 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001654 else:
1655 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001656
Chris Sosadbc20082012-12-10 13:39:11 -08001657 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 16:39:34 -07001658 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001659
Amin Hassanie9ffb862019-09-25 17:10:40 -07001660 _xbuddy = xbuddy.XBuddy(manage_builds=options.xbuddy_manage_builds,
joychen121fc9b2013-08-02 14:30:30 -07001661 static_dir=options.static_dir)
Chris Sosa75490802013-09-30 17:21:45 -07001662 if options.clear_cache and options.xbuddy_manage_builds:
1663 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 14:30:30 -07001664
Chris Sosa6a3697f2013-01-29 16:44:43 -08001665 # We allow global use here to share with cherrypy classes.
1666 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001667 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001668 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001669 _xbuddy,
joychened64b222013-06-21 16:39:34 -07001670 static_dir=options.static_dir,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001671 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001672 proxy_port=options.proxy_port,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001673 critical_update=options.critical_update,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001674 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001675 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001676 )
Chris Sosa7c931362010-10-11 19:49:01 -07001677
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001678 if options.exit:
1679 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001680
joychen3cb228e2013-06-12 12:13:13 -07001681 dev_server = DevServerRoot(_xbuddy)
Congbin Guo3afae6c2019-08-13 16:29:42 -07001682 health_checker_app = health_checker.Root(dev_server, options.static_dir)
joychen3cb228e2013-06-12 12:13:13 -07001683
Amin Hassanic5af4262019-11-13 13:37:20 -08001684 # Patch CherryPy to support binding to any available port (--port=0) only for
1685 # cherrypy versions smaller or equal to 3.2.2.
1686 #
1687 # TODO(crbug/1006305): Remove this once we have deprecated omaha_devserver.py
1688 # in the autotests as that is the only use case.
1689 #
1690 # pylint: disable=no-member
1691 if (distutils.version.StrictVersion(cherrypy.__version__) <=
1692 distutils.version.StrictVersion('3.2.2')):
1693 cherrypy_ext.ZeroPortPatcher.DoPatch(cherrypy)
1694 # pylint: enable=no-member
1695
Chris Sosa855b8932013-08-21 13:24:55 -07001696 if options.pidfile:
1697 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1698
Gilad Arnold11fbef42014-02-10 11:04:13 -08001699 if options.portfile:
1700 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1701
Dan Shiafd5c6c2016-01-07 10:27:03 -08001702 if (options.android_build_credential and
1703 os.path.exists(options.android_build_credential)):
1704 try:
1705 with open(options.android_build_credential) as f:
1706 android_build.BuildAccessor.credential_info = json.load(f)
1707 except ValueError as e:
1708 _Log('Failed to load the android build credential: %s. Error: %s.' %
1709 (options.android_build_credential, e))
Congbin Guo3afae6c2019-08-13 16:29:42 -07001710
1711 cherrypy.tree.mount(health_checker_app, '/check_health',
1712 config=health_checker.get_config())
joychen3cb228e2013-06-12 12:13:13 -07001713 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001714
1715
1716if __name__ == '__main__':
1717 main()