blob: 8478a44d22ab4b9b37cddc71e61f8b9a4e1fd3c2 [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
Gabe Black3b567202015-09-23 14:07:59 -0700104def _canonicalize_archive_url(archive_url):
105 """Canonicalizes archive_url strings.
106
107 Raises:
108 DevserverError: if archive_url is not set.
109 """
110 if archive_url:
111 if not archive_url.startswith('gs://'):
Amin Hassanid4e35392019-10-03 11:02:44 -0700112 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700113 "Archive URL isn't from Google Storage (%s) ." % archive_url)
Gabe Black3b567202015-09-23 14:07:59 -0700114
115 return archive_url.rstrip('/')
116 else:
Amin Hassanid4e35392019-10-03 11:02:44 -0700117 raise DevServerError('Must specify an archive_url in the request')
Gabe Black3b567202015-09-23 14:07:59 -0700118
119
120def _canonicalize_local_path(local_path):
121 """Canonicalizes |local_path| strings.
122
123 Raises:
124 DevserverError: if |local_path| is not set.
125 """
126 # Restrict staging of local content to only files within the static
127 # directory.
128 local_path = os.path.abspath(local_path)
129 if not local_path.startswith(updater.static_dir):
Amin Hassanid4e35392019-10-03 11:02:44 -0700130 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700131 'Local path %s must be a subdirectory of the static'
132 ' directory: %s' % (local_path, updater.static_dir))
Gabe Black3b567202015-09-23 14:07:59 -0700133
134 return local_path.rstrip('/')
135
136
137def _get_artifacts(kwargs):
138 """Returns a tuple of named and file artifacts given the stage rpc kwargs.
139
140 Raises:
141 DevserverError if no artifacts would be returned.
142 """
143 artifacts = kwargs.get('artifacts')
144 files = kwargs.get('files')
145 if not artifacts and not files:
Amin Hassanid4e35392019-10-03 11:02:44 -0700146 raise DevServerError('No artifacts specified.')
Gabe Black3b567202015-09-23 14:07:59 -0700147
148 # Note we NEED to coerce files to a string as we get raw unicode from
149 # cherrypy and we treat files as strings elsewhere in the code.
150 return (str(artifacts).split(',') if artifacts else [],
151 str(files).split(',') if files else [])
152
153
Dan Shi61305df2015-10-26 16:52:35 -0700154def _is_android_build_request(kwargs):
155 """Check if a devserver call is for Android build, based on the arguments.
156
157 This method exams the request's arguments (os_type) to determine if the
158 request is for Android build. If os_type is set to `android`, returns True.
159 If os_type is not set or has other values, returns False.
160
161 Args:
162 kwargs: Keyword arguments for the request.
163
164 Returns:
165 True if the request is for Android build. False otherwise.
166 """
167 os_type = kwargs.get('os_type', None)
168 return os_type == 'android'
169
170
Gabe Black3b567202015-09-23 14:07:59 -0700171def _get_downloader(kwargs):
172 """Returns the downloader based on passed in arguments.
173
174 Args:
Amin Hassani08e42d22019-06-03 00:31:30 -0700175 kwargs: Keyword arguments for the request.
Gabe Black3b567202015-09-23 14:07:59 -0700176 """
177 local_path = kwargs.get('local_path')
178 if local_path:
179 local_path = _canonicalize_local_path(local_path)
180
181 dl = None
182 if local_path:
Prathmesh Prabhu58d08932018-01-19 15:08:19 -0800183 delete_source = _parse_boolean_arg(kwargs, 'delete_source')
184 dl = downloader.LocalDownloader(updater.static_dir, local_path,
185 delete_source=delete_source)
Gabe Black3b567202015-09-23 14:07:59 -0700186
Dan Shi61305df2015-10-26 16:52:35 -0700187 if not _is_android_build_request(kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700188 archive_url = kwargs.get('archive_url')
189 if not archive_url and not local_path:
Amin Hassanid4e35392019-10-03 11:02:44 -0700190 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700191 'Requires archive_url or local_path to be specified.')
Gabe Black3b567202015-09-23 14:07:59 -0700192 if archive_url and local_path:
Amin Hassanid4e35392019-10-03 11:02:44 -0700193 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700194 'archive_url and local_path can not both be specified.')
Gabe Black3b567202015-09-23 14:07:59 -0700195 if not dl:
196 archive_url = _canonicalize_archive_url(archive_url)
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700197 dl = downloader.GoogleStorageDownloader(
198 updater.static_dir, archive_url,
199 downloader.GoogleStorageDownloader.GetBuildIdFromArchiveURL(
200 archive_url))
Gabe Black3b567202015-09-23 14:07:59 -0700201 elif not dl:
202 target = kwargs.get('target', None)
Dan Shi72b16132015-10-08 12:10:33 -0700203 branch = kwargs.get('branch', None)
Dan Shi61305df2015-10-26 16:52:35 -0700204 build_id = kwargs.get('build_id', None)
205 if not target or not branch or not build_id:
Amin Hassanid4e35392019-10-03 11:02:44 -0700206 raise DevServerError('target, branch, build ID must all be specified for '
207 'downloading Android build.')
Dan Shi72b16132015-10-08 12:10:33 -0700208 dl = downloader.AndroidBuildDownloader(updater.static_dir, branch, build_id,
209 target)
Gabe Black3b567202015-09-23 14:07:59 -0700210
211 return dl
212
213
214def _get_downloader_and_factory(kwargs):
215 """Returns the downloader and artifact factory based on passed in arguments.
216
217 Args:
Amin Hassani08e42d22019-06-03 00:31:30 -0700218 kwargs: Keyword arguments for the request.
Gabe Black3b567202015-09-23 14:07:59 -0700219 """
220 artifacts, files = _get_artifacts(kwargs)
221 dl = _get_downloader(kwargs)
222
Achuith Bhandarkar2a1fcd82019-10-17 17:45:58 -0700223 if (isinstance(dl, (downloader.GoogleStorageDownloader,
224 downloader.LocalDownloader))):
Gabe Black3b567202015-09-23 14:07:59 -0700225 factory_class = build_artifact.ChromeOSArtifactFactory
Dan Shi72b16132015-10-08 12:10:33 -0700226 elif isinstance(dl, downloader.AndroidBuildDownloader):
Gabe Black3b567202015-09-23 14:07:59 -0700227 factory_class = build_artifact.AndroidArtifactFactory
228 else:
Amin Hassanid4e35392019-10-03 11:02:44 -0700229 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700230 'Unrecognized value for downloader type: %s' % type(dl))
Gabe Black3b567202015-09-23 14:07:59 -0700231
232 factory = factory_class(dl.GetBuildDir(), artifacts, files, dl.GetBuild())
233
234 return dl, factory
235
236
Scott Zawalski4647ce62012-01-03 17:17:28 -0500237def _LeadingWhiteSpaceCount(string):
238 """Count the amount of leading whitespace in a string.
239
240 Args:
241 string: The string to count leading whitespace in.
Don Garrettf84631a2014-01-07 18:21:26 -0800242
Scott Zawalski4647ce62012-01-03 17:17:28 -0500243 Returns:
244 number of white space chars before characters start.
245 """
Gabe Black3b567202015-09-23 14:07:59 -0700246 matched = re.match(r'^\s+', string)
Scott Zawalski4647ce62012-01-03 17:17:28 -0500247 if matched:
248 return len(matched.group())
249
250 return 0
251
252
253def _PrintDocStringAsHTML(func):
254 """Make a functions docstring somewhat HTML style.
255
256 Args:
257 func: The function to return the docstring from.
Don Garrettf84631a2014-01-07 18:21:26 -0800258
Scott Zawalski4647ce62012-01-03 17:17:28 -0500259 Returns:
260 A string that is somewhat formated for a web browser.
261 """
262 # TODO(scottz): Make this parse Args/Returns in a prettier way.
263 # Arguments could be bolded and indented etc.
264 html_doc = []
265 for line in func.__doc__.splitlines():
266 leading_space = _LeadingWhiteSpaceCount(line)
267 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700268 line = ' ' * leading_space + line
Scott Zawalski4647ce62012-01-03 17:17:28 -0500269
270 html_doc.append('<BR>%s' % line)
271
272 return '\n'.join(html_doc)
273
274
Simran Basief83d6a2014-08-28 14:32:01 -0700275def _GetUpdateTimestampHandler(static_dir):
276 """Returns a handler to update directory staged.timestamp.
277
278 This handler resets the stage.timestamp whenever static content is accessed.
279
280 Args:
281 static_dir: Directory from which static content is being staged.
282
283 Returns:
Amin Hassani08e42d22019-06-03 00:31:30 -0700284 A cherrypy handler to update the timestamp of accessed content.
Simran Basief83d6a2014-08-28 14:32:01 -0700285 """
286 def UpdateTimestampHandler():
287 if not '404' in cherrypy.response.status:
288 build_match = re.match(devserver_constants.STAGED_BUILD_REGEX,
289 cherrypy.request.path_info)
290 if build_match:
291 build_dir = os.path.join(static_dir, build_match.group('build'))
292 downloader.Downloader.TouchTimestampForStaged(build_dir)
293 return UpdateTimestampHandler
294
295
Chris Sosa7c931362010-10-11 19:49:01 -0700296def _GetConfig(options):
297 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800298
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800299 socket_host = '::'
Yu-Ju Hongc8d4af32013-11-12 15:14:26 -0800300 # Fall back to IPv4 when python is not configured with IPv6.
301 if not socket.has_ipv6:
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800302 socket_host = '0.0.0.0'
303
Simran Basief83d6a2014-08-28 14:32:01 -0700304 # Adds the UpdateTimestampHandler to cherrypy's tools. This tools executes
305 # on the on_end_resource hook. This hook is called once processing is
306 # complete and the response is ready to be returned.
307 cherrypy.tools.update_timestamp = cherrypy.Tool(
308 'on_end_resource', _GetUpdateTimestampHandler(options.static_dir))
309
David Riley2fcb0122017-11-02 11:25:39 -0700310 base_config = {
311 'global': {
312 'server.log_request_headers': True,
313 'server.protocol_version': 'HTTP/1.1',
314 'server.socket_host': socket_host,
315 'server.socket_port': int(options.port),
316 'response.timeout': 6000,
317 'request.show_tracebacks': True,
318 'server.socket_timeout': 60,
319 'server.thread_pool': 2,
320 'engine.autoreload.on': False,
321 },
322 '/api': {
323 # Gets rid of cherrypy parsing post file for args.
324 'request.process_request_body': False,
325 },
326 '/build': {
327 'response.timeout': 100000,
328 },
329 '/update': {
330 # Gets rid of cherrypy parsing post file for args.
331 'request.process_request_body': False,
332 'response.timeout': 10000,
333 },
334 # Sets up the static dir for file hosting.
335 '/static': {
336 'tools.staticdir.dir': options.static_dir,
337 'tools.staticdir.on': True,
338 'response.timeout': 10000,
339 'tools.update_timestamp.on': True,
340 },
341 }
Chris Sosa5f118ef2012-07-12 11:37:50 -0700342 if options.production:
Alex Miller93beca52013-07-30 19:25:09 -0700343 base_config['global'].update({'server.thread_pool': 150})
Scott Zawalski1c5e7cd2012-02-27 13:12:52 -0500344
Chris Sosa7c931362010-10-11 19:49:01 -0700345 return base_config
rtc@google.com64244662009-11-12 00:52:08 +0000346
Darin Petkove17164a2010-08-11 13:24:41 -0700347
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700348def _GetRecursiveMemberObject(root, member_list):
349 """Returns an object corresponding to a nested member list.
350
351 Args:
352 root: the root object to search
353 member_list: list of nested members to search
Don Garrettf84631a2014-01-07 18:21:26 -0800354
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700355 Returns:
356 An object corresponding to the member name list; None otherwise.
357 """
358 for member in member_list:
359 next_root = root.__class__.__dict__.get(member)
360 if not next_root:
361 return None
362 root = next_root
363 return root
364
365
366def _IsExposed(name):
367 """Returns True iff |name| has an `exposed' attribute and it is set."""
368 return hasattr(name, 'exposed') and name.exposed
369
370
Congbin Guo6bc32182019-08-20 17:54:30 -0700371def _GetExposedMethod(nested_member):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700372 """Returns a CherryPy-exposed method, if such exists.
373
374 Args:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700375 nested_member: a slash-joined path to the nested member
Don Garrettf84631a2014-01-07 18:21:26 -0800376
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700377 Returns:
Congbin Guo6bc32182019-08-20 17:54:30 -0700378 A function object corresponding to the path defined by |nested_member| from
379 the app root object registered, if the function is exposed; None otherwise.
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700380 """
Congbin Guo6bc32182019-08-20 17:54:30 -0700381 for app in cherrypy.tree.apps.values():
382 # Use the 'index' function doc as the doc of the app.
383 if nested_member == app.script_name.lstrip('/'):
384 nested_member = 'index'
385
386 method = _GetRecursiveMemberObject(app.root, nested_member.split('/'))
387 if method and isinstance(method, types.FunctionType) and _IsExposed(method):
388 return method
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700389
390
Gilad Arnold748c8322012-10-12 09:51:35 -0700391def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700392 """Finds exposed CherryPy methods.
393
394 Args:
395 root: the root object for searching
396 prefix: slash-joined chain of members leading to current object
397 unlisted: URLs to be excluded regardless of their exposed status
Don Garrettf84631a2014-01-07 18:21:26 -0800398
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700399 Returns:
400 List of exposed URLs that are not unlisted.
401 """
402 method_list = []
Congbin Guo6bc32182019-08-20 17:54:30 -0700403 for member in root.__class__.__dict__.keys():
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700404 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 09:51:35 -0700405 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700406 continue
407 member_obj = root.__class__.__dict__[member]
408 if _IsExposed(member_obj):
Amin Hassani08e42d22019-06-03 00:31:30 -0700409 if isinstance(member_obj, types.FunctionType):
Congbin Guo6bc32182019-08-20 17:54:30 -0700410 # Regard the app name as exposed "method" name if it exposed 'index'
411 # function.
412 if prefix and member == 'index':
413 method_list.append(prefix)
414 else:
415 method_list.append(prefixed_member)
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700416 else:
417 method_list += _FindExposedMethods(
418 member_obj, prefixed_member, unlisted)
419 return method_list
420
421
xixuan52c2fba2016-05-20 17:02:48 -0700422def _check_base_args_for_auto_update(kwargs):
xixuanac89ce82016-11-30 16:48:20 -0800423 """Check basic args required for auto-update.
424
425 Args:
426 kwargs: the parameters to be checked.
427
428 Raises:
429 DevServerHTTPError if required parameters don't exist in kwargs.
430 """
xixuan52c2fba2016-05-20 17:02:48 -0700431 if 'host_name' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -0700432 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -0700433 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -0700434
435 if 'build_name' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -0700436 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -0700437 KEY_ERROR_MSG % 'build_name')
xixuan52c2fba2016-05-20 17:02:48 -0700438
439
440def _parse_boolean_arg(kwargs, key):
xixuanac89ce82016-11-30 16:48:20 -0800441 """Parse boolean arg from kwargs.
442
443 Args:
444 kwargs: the parameters to be checked.
445 key: the key to be parsed.
446
447 Returns:
448 The boolean value of kwargs[key], or False if key doesn't exist in kwargs.
449
450 Raises:
451 DevServerHTTPError if kwargs[key] is not a boolean variable.
452 """
xixuan52c2fba2016-05-20 17:02:48 -0700453 if key in kwargs:
454 if kwargs[key] == 'True':
455 return True
456 elif kwargs[key] == 'False':
457 return False
458 else:
459 raise common_util.DevServerHTTPError(
Amin Hassanid4e35392019-10-03 11:02:44 -0700460 http_client.INTERNAL_SERVER_ERROR,
xixuan52c2fba2016-05-20 17:02:48 -0700461 'The value for key %s is not boolean.' % key)
462 else:
463 return False
464
xixuan447ad9d2017-02-28 14:46:20 -0800465
xixuanac89ce82016-11-30 16:48:20 -0800466def _parse_string_arg(kwargs, key):
467 """Parse string arg from kwargs.
468
469 Args:
470 kwargs: the parameters to be checked.
471 key: the key to be parsed.
472
473 Returns:
474 The string value of kwargs[key], or None if key doesn't exist in kwargs.
475 """
476 if key in kwargs:
477 return kwargs[key]
478 else:
479 return None
480
xixuan447ad9d2017-02-28 14:46:20 -0800481
xixuanac89ce82016-11-30 16:48:20 -0800482def _build_uri_from_build_name(build_name):
483 """Get build url from a given build name.
484
485 Args:
486 build_name: the build name to be parsed, whose format is
487 'board/release_version'.
488
489 Returns:
490 The release_archive_url on Google Storage for this build name.
491 """
Amin Hassani08e42d22019-06-03 00:31:30 -0700492 # TODO(ahassani): This function doesn't seem to be used anywhere since its
493 # previous use of lib.paygen.gspath was broken and it doesn't seem to be
494 # causing any runtime issues. So deprecate this in the future.
495 tokens = build_name.split('/')
496 return 'gs://chromeos-releases/stable-channel/%s/%s' % (tokens[0], tokens[1])
xixuan52c2fba2016-05-20 17:02:48 -0700497
xixuan447ad9d2017-02-28 14:46:20 -0800498
499def _clear_process(host_name, pid):
500 """Clear AU process for given hostname and pid.
501
502 This clear includes:
503 1. kill process if it's alive.
504 2. delete the track status file of this process.
505 3. delete the executing log file of this process.
506
507 Args:
508 host_name: the host to execute auto-update.
509 pid: the background auto-update process id.
510 """
511 if cros_update_progress.IsProcessAlive(pid):
512 os.killpg(int(pid), signal.SIGKILL)
513
514 cros_update_progress.DelTrackStatusFile(host_name, pid)
515 cros_update_progress.DelExecuteLogFile(host_name, pid)
516
517
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700518class ApiRoot(object):
519 """RESTful API for Dev Server information."""
520 exposed = True
521
522 @cherrypy.expose
Gilad Arnold286a0062012-01-12 13:47:02 -0800523 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 11:36:27 -0700524 """Returns a JSON object containing a log of host event.
525
526 Args:
527 ip: address of host whose event log is requested, or `all'
Don Garrettf84631a2014-01-07 18:21:26 -0800528
Gilad Arnold1b908392012-10-05 11:36:27 -0700529 Returns:
Amin Hassani7c447852019-09-26 15:01:48 -0700530 A JSON dictionary containing all or some of the following fields:
Amin Hassanie7ead902019-10-11 16:42:43 -0700531 version: The Chromium OS version the device is running.
532 track: The channel the device is running on.
533 board: The device's board.
534 event_result: The event result of Omaha request.
535 event_type: The event type of Omaha request.
536 previous_version: The Chromium OS version we updated and rebooted from.
537 timestamp: The timestamp the event was received.
Amin Hassani7c447852019-09-26 15:01:48 -0700538 See the OmahaEvent class in update_engine/omaha_request_action.h for
539 event type and status code definitions. If the ip does not exist an empty
540 string is returned.
Gilad Arnold1b908392012-10-05 11:36:27 -0700541
542 Example URL:
543 http://myhost/api/hostlog?ip=192.168.1.5
544 """
Gilad Arnold286a0062012-01-12 13:47:02 -0800545 return updater.HandleHostLogPing(ip)
546
547 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800548 def fileinfo(self, *args):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700549 """Returns information about a given staged file.
550
551 Args:
Don Garrettf84631a2014-01-07 18:21:26 -0800552 args: path to the file inside the server's static staging directory
553
Gilad Arnold55a2a372012-10-02 09:46:32 -0700554 Returns:
555 A JSON encoded dictionary with information about the said file, which may
556 contain the following keys/values:
Gilad Arnold1b908392012-10-05 11:36:27 -0700557 size (int): the file size in bytes
Gilad Arnold1b908392012-10-05 11:36:27 -0700558 sha256 (string): a base64 encoded SHA256 hash
559
560 Example URL:
561 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 09:46:32 -0700562 """
Amin Hassani28df4212019-10-28 10:16:50 -0700563 # TODO(ahassani): A better way of doing this is to just return the the
564 # content of the payloads' property file instead. That has all this info
565 # except that the key for sha256 is 'sha256_hex', but still base64 encdoed.
566
Don Garrettf84631a2014-01-07 18:21:26 -0800567 file_path = os.path.join(updater.static_dir, *args)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700568 if not os.path.exists(file_path):
Amin Hassanid4e35392019-10-03 11:02:44 -0700569 raise DevServerError('file not found: %s' % file_path)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700570 try:
571 file_size = os.path.getsize(file_path)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700572 file_sha256 = common_util.GetFileSha256(file_path)
Amin Hassani469f5702019-10-21 15:35:06 -0700573 except os.error as e:
Amin Hassanid4e35392019-10-03 11:02:44 -0700574 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700575 'failed to get info for file %s: %s' % (file_path, e))
Gilad Arnolde74b3812013-04-22 11:27:38 -0700576
Gilad Arnolde74b3812013-04-22 11:27:38 -0700577 return json.dumps({
Amin Hassani28df4212019-10-28 10:16:50 -0700578 'size': file_size,
579 'sha256': file_sha256,
580 }, sort_keys=True)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700581
Chris Sosa76e44b92013-01-31 12:11:38 -0800582
David Rochberg7c79a812011-01-19 14:24:45 -0500583class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700584 """The Root Class for the Dev Server.
585
586 CherryPy works as follows:
587 For each method in this class, cherrpy interprets root/path
588 as a call to an instance of DevServerRoot->method_name. For example,
589 a call to http://myhost/build will call build. CherryPy automatically
590 parses http args and places them as keyword arguments in each method.
591 For paths http://myhost/update/dir1/dir2, you can use *args so that
592 cherrypy uses the update method and puts the extra paths in args.
593 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700594 # Method names that should not be listed on the index page.
595 _UNLISTED_METHODS = ['index', 'doc']
596
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700597 api = ApiRoot()
Chris Sosa7c931362010-10-11 19:49:01 -0700598
Dan Shi59ae7092013-06-04 14:37:27 -0700599 # Number of threads that devserver is staging images.
600 _staging_thread_count = 0
601 # Lock used to lock increasing/decreasing count.
602 _staging_thread_count_lock = threading.Lock()
603
joychen3cb228e2013-06-12 12:13:13 -0700604 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700605 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800606 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 12:13:13 -0700607 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 14:24:45 -0500608
Congbin Guo3afae6c2019-08-13 16:29:42 -0700609 @property
610 def staging_thread_count(self):
611 """Get the staging thread count."""
612 return self._staging_thread_count
Dan Shiafd0e492015-05-27 14:23:51 -0700613
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700614 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500615 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700616 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700617 import builder
618 if self._builder is None:
619 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500620 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700621
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700622 @cherrypy.expose
Dan Shif8eb0d12013-08-01 17:52:06 -0700623 def is_staged(self, **kwargs):
624 """Check if artifacts have been downloaded.
625
Congbin Guo3afae6c2019-08-13 16:29:42 -0700626 Examples:
627 To check if autotest and test_suites are staged:
628 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
629 artifacts=autotest,test_suites
630
Amin Hassani08e42d22019-06-03 00:31:30 -0700631 Args:
Chris Sosa6b0c6172013-08-05 17:01:33 -0700632 async: True to return without waiting for download to complete.
633 artifacts: Comma separated list of named artifacts to download.
634 These are defined in artifact_info and have their implementation
635 in build_artifact.py.
636 files: Comma separated list of file artifacts to stage. These
637 will be available as is in the corresponding static directory with no
638 custom post-processing.
639
Congbin Guo3afae6c2019-08-13 16:29:42 -0700640 Returns:
641 True of all artifacts are staged.
Dan Shif8eb0d12013-08-01 17:52:06 -0700642 """
Gabe Black3b567202015-09-23 14:07:59 -0700643 dl, factory = _get_downloader_and_factory(kwargs)
Aviv Keshet57d18172016-06-18 20:39:09 -0700644 response = str(dl.IsStaged(factory))
645 _Log('Responding to is_staged %s request with %r', kwargs, response)
646 return response
Dan Shi59ae7092013-06-04 14:37:27 -0700647
Chris Sosa76e44b92013-01-31 12:11:38 -0800648 @cherrypy.expose
Prashanth Ba06d2d22014-03-07 15:35:19 -0800649 def list_image_dir(self, **kwargs):
650 """Take an archive url and list the contents in its staged directory.
651
Amin Hassani08e42d22019-06-03 00:31:30 -0700652 Examples:
Prashanth Ba06d2d22014-03-07 15:35:19 -0800653 To list the contents of where this devserver should have staged
654 gs://image-archive/<board>-release/<build> call:
655 http://devserver_url:<port>/list_image_dir?archive_url=<gs://..>
656
Congbin Guo3afae6c2019-08-13 16:29:42 -0700657 Args:
658 archive_url: Google Storage URL for the build.
659
Prashanth Ba06d2d22014-03-07 15:35:19 -0800660 Returns:
661 A string with information about the contents of the image directory.
662 """
Gabe Black3b567202015-09-23 14:07:59 -0700663 dl = _get_downloader(kwargs)
Prashanth Ba06d2d22014-03-07 15:35:19 -0800664 try:
Gabe Black3b567202015-09-23 14:07:59 -0700665 image_dir_contents = dl.ListBuildDir()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800666 except build_artifact.ArtifactDownloadError as e:
667 return 'Cannot list the contents of staged artifacts. %s' % e
668 if not image_dir_contents:
Gabe Black3b567202015-09-23 14:07:59 -0700669 return '%s has not been staged on this devserver.' % dl.DescribeSource()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800670 return image_dir_contents
671
672 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800673 def stage(self, **kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700674 """Downloads and caches build artifacts.
Chris Sosa76e44b92013-01-31 12:11:38 -0800675
Gabe Black3b567202015-09-23 14:07:59 -0700676 Downloads and caches build artifacts, possibly from a Google Storage URL,
Dan Shi72b16132015-10-08 12:10:33 -0700677 or from Android's build server. Returns once these have been downloaded
Gabe Black3b567202015-09-23 14:07:59 -0700678 on the devserver. A call to this will attempt to cache non-specified
679 artifacts in the background for the given from the given URL following
680 the principle of spatial locality. Spatial locality of different
Chris Sosa76e44b92013-01-31 12:11:38 -0800681 artifacts is explicitly defined in the build_artifact module.
682
683 These artifacts will then be available from the static/ sub-directory of
684 the devserver.
685
Amin Hassani08e42d22019-06-03 00:31:30 -0700686 Examples:
Chris Sosa76e44b92013-01-31 12:11:38 -0800687 To download the autotest and test suites tarballs:
688 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
689 artifacts=autotest,test_suites
690 To download the full update payload:
691 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
692 artifacts=full_payload
Chris Sosa6b0c6172013-08-05 17:01:33 -0700693 To download just a file called blah.bin:
694 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
695 files=blah.bin
Chris Sosa76e44b92013-01-31 12:11:38 -0800696
697 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 16:39:34 -0700698 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 12:11:38 -0800699
700 Note for this example, relative path is the archive_url stripped of its
701 basename i.e. path/ in the examples above. Specific example:
702
703 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
704
705 Will get staged to:
706
joychened64b222013-06-21 16:39:34 -0700707 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Congbin Guo3afae6c2019-08-13 16:29:42 -0700708
709 Args:
710 archive_url: Google Storage URL for the build.
711 local_path: Local path for the build.
712 delete_source: Only meaningful with local_path. bool to indicate if the
713 source files should be deleted. This is especially useful when staging
714 a file locally in resource constrained environments as it allows us to
715 move the relevant files locally instead of copying them.
716 async: True to return without waiting for download to complete.
717 artifacts: Comma separated list of named artifacts to download.
718 These are defined in artifact_info and have their implementation
719 in build_artifact.py.
720 files: Comma separated list of files to stage. These
721 will be available as is in the corresponding static directory with no
722 custom post-processing.
723 clean: True to remove any previously staged artifacts first.
Chris Sosa76e44b92013-01-31 12:11:38 -0800724 """
Gabe Black3b567202015-09-23 14:07:59 -0700725 dl, factory = _get_downloader_and_factory(kwargs)
726
Dan Shi59ae7092013-06-04 14:37:27 -0700727 with DevServerRoot._staging_thread_count_lock:
728 DevServerRoot._staging_thread_count += 1
729 try:
Laurence Goodbyf5c958d2016-01-14 18:23:56 -0800730 boolean_string = kwargs.get('clean')
731 clean = xbuddy.XBuddy.ParseBoolean(boolean_string)
732 if clean and os.path.exists(dl.GetBuildDir()):
733 _Log('Removing %s' % dl.GetBuildDir())
734 shutil.rmtree(dl.GetBuildDir())
Achuith Bhandarkar2a1fcd82019-10-17 17:45:58 -0700735 is_async = kwargs.get('async', False)
736 dl.Download(factory, is_async=is_async)
Dan Shi59ae7092013-06-04 14:37:27 -0700737 finally:
738 with DevServerRoot._staging_thread_count_lock:
739 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800740 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700741
742 @cherrypy.expose
xixuan52c2fba2016-05-20 17:02:48 -0700743 def cros_au(self, **kwargs):
744 """Auto-update a CrOS DUT.
745
746 Args:
747 kwargs:
748 host_name: the hostname of the DUT to auto-update.
749 build_name: the build name for update the DUT.
750 force_update: Force an update even if the version installed is the
751 same. Default: False.
752 full_update: If True, do not run stateful update, directly force a full
753 reimage. If False, try stateful update first if the dut is already
754 installed with the same version.
755 async: Whether the auto_update function is ran in the background.
David Rileyee75de22017-11-02 10:48:15 -0700756 quick_provision: Whether the quick provision path is attempted first.
xixuan52c2fba2016-05-20 17:02:48 -0700757
758 Returns:
759 A tuple includes two elements:
760 a boolean variable represents whether the auto-update process is
761 successfully started.
762 an integer represents the background auto-update process id.
763 """
764 _check_base_args_for_auto_update(kwargs)
765
766 host_name = kwargs['host_name']
767 build_name = kwargs['build_name']
768 force_update = _parse_boolean_arg(kwargs, 'force_update')
769 full_update = _parse_boolean_arg(kwargs, 'full_update')
Achuith Bhandarkar2a1fcd82019-10-17 17:45:58 -0700770 is_async = _parse_boolean_arg(kwargs, 'async')
xixuanac89ce82016-11-30 16:48:20 -0800771 original_build = _parse_string_arg(kwargs, 'original_build')
David Haddock90e49442017-04-07 19:14:09 -0700772 payload_filename = _parse_string_arg(kwargs, 'payload_filename')
David Haddock20559612017-06-28 22:15:08 -0700773 clobber_stateful = _parse_boolean_arg(kwargs, 'clobber_stateful')
David Rileyee75de22017-11-02 10:48:15 -0700774 quick_provision = _parse_boolean_arg(kwargs, 'quick_provision')
775
776 devserver_url = updater.GetDevserverUrl()
777 static_url = updater.GetStaticUrl()
xixuan52c2fba2016-05-20 17:02:48 -0700778
Achuith Bhandarkar2a1fcd82019-10-17 17:45:58 -0700779 if is_async:
Amin Hassani469f5702019-10-21 15:35:06 -0700780 # Command of running auto-update.
Amin Hassanie427e212019-10-28 11:04:27 -0700781 cmd = ['cros_update', '--hostname', host_name, '-b', build_name,
782 '--static_dir', updater.static_dir]
xixuanac89ce82016-11-30 16:48:20 -0800783
784 # The original_build's format is like: link/3428.210.0
785 # The corresponding release_archive_url's format is like:
786 # gs://chromeos-releases/stable-channel/link/3428.210.0
787 if original_build:
788 release_archive_url = _build_uri_from_build_name(original_build)
789 # First staging the stateful.tgz synchronousely.
Amin Hassani469f5702019-10-21 15:35:06 -0700790 self.stage(files='stateful.tgz', is_async=False,
xixuanac89ce82016-11-30 16:48:20 -0800791 archive_url=release_archive_url)
Amin Hassani469f5702019-10-21 15:35:06 -0700792 cmd += ['--original_build', original_build]
xixuanac89ce82016-11-30 16:48:20 -0800793
xixuan52c2fba2016-05-20 17:02:48 -0700794 if force_update:
Amin Hassani469f5702019-10-21 15:35:06 -0700795 cmd += ['--force_update']
xixuan52c2fba2016-05-20 17:02:48 -0700796
797 if full_update:
Amin Hassani469f5702019-10-21 15:35:06 -0700798 cmd += ['--full_update']
xixuan52c2fba2016-05-20 17:02:48 -0700799
David Haddock90e49442017-04-07 19:14:09 -0700800 if payload_filename:
Amin Hassani469f5702019-10-21 15:35:06 -0700801 cmd += ['--payload_filename', payload_filename]
David Haddock90e49442017-04-07 19:14:09 -0700802
David Haddock20559612017-06-28 22:15:08 -0700803 if clobber_stateful:
Amin Hassani469f5702019-10-21 15:35:06 -0700804 cmd += ['--clobber_stateful']
David Haddock20559612017-06-28 22:15:08 -0700805
David Rileyee75de22017-11-02 10:48:15 -0700806 if quick_provision:
Amin Hassani469f5702019-10-21 15:35:06 -0700807 cmd += ['--quick_provision']
David Rileyee75de22017-11-02 10:48:15 -0700808
809 if devserver_url:
Amin Hassani469f5702019-10-21 15:35:06 -0700810 cmd += ['--devserver_url', devserver_url]
David Rileyee75de22017-11-02 10:48:15 -0700811
812 if static_url:
Amin Hassani469f5702019-10-21 15:35:06 -0700813 cmd += ['--static_url', static_url]
David Rileyee75de22017-11-02 10:48:15 -0700814
Amin Hassani78520ae2019-10-29 13:26:51 -0700815 p = subprocess.Popen(cmd, preexec_fn=os.setsid)
xixuan2a0970a2016-08-10 12:12:44 -0700816 pid = os.getpgid(p.pid)
xixuan52c2fba2016-05-20 17:02:48 -0700817
818 # Pre-write status in the track_status_file before the first call of
819 # 'get_au_status' to make sure that the track_status_file exists.
xixuan2a0970a2016-08-10 12:12:44 -0700820 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -0700821 progress_tracker.WriteStatus('CrOS update is just started.')
822
xixuan2a0970a2016-08-10 12:12:44 -0700823 return json.dumps((True, pid))
xixuan52c2fba2016-05-20 17:02:48 -0700824 else:
825 cros_update_trigger = cros_update.CrOSUpdateTrigger(
xixuanac89ce82016-11-30 16:48:20 -0800826 host_name, build_name, updater.static_dir, force_update=force_update,
David Rileyee75de22017-11-02 10:48:15 -0700827 full_update=full_update, original_build=original_build,
Amin Hassani78520ae2019-10-29 13:26:51 -0700828 payload_filename=payload_filename, quick_provision=quick_provision,
829 devserver_url=devserver_url, static_url=static_url)
xixuan52c2fba2016-05-20 17:02:48 -0700830 cros_update_trigger.TriggerAU()
xixuan27d50442017-08-09 10:38:25 -0700831 return json.dumps((True, -1))
xixuan52c2fba2016-05-20 17:02:48 -0700832
833 @cherrypy.expose
834 def get_au_status(self, **kwargs):
835 """Check if the auto-update task is finished.
836
837 It handles 4 cases:
838 1. If an error exists in the track_status_file, delete the track file and
839 raise it.
840 2. If cros-update process is finished, delete the file and return the
841 success result.
842 3. If the process is not running, delete the track file and raise an error
843 about 'the process is terminated due to unknown reason'.
844 4. If the track_status_file does not exist, kill the process if it exists,
845 and raise the IOError.
846
847 Args:
848 kwargs:
849 host_name: the hostname of the DUT to auto-update.
850 pid: the background process id of cros-update.
851
852 Returns:
xixuan28d99072016-10-06 12:24:16 -0700853 A dict with three elements:
xixuan52c2fba2016-05-20 17:02:48 -0700854 a boolean variable represents whether the auto-update process is
855 finished.
856 a string represents the current auto-update process status.
857 For example, 'Transfer Devserver/Stateful Update Package'.
xixuan28d99072016-10-06 12:24:16 -0700858 a detailed error message paragraph if there exists an Auto-Update
859 error, in which the last line shows the main exception. Empty
860 string otherwise.
xixuan52c2fba2016-05-20 17:02:48 -0700861 """
862 if 'host_name' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -0700863 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -0700864 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -0700865
866 if 'pid' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -0700867 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -0700868 KEY_ERROR_MSG % 'pid')
xixuan52c2fba2016-05-20 17:02:48 -0700869
870 host_name = kwargs['host_name']
871 pid = kwargs['pid']
872 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
873
xixuan28d99072016-10-06 12:24:16 -0700874 result_dict = {'finished': False, 'status': '', 'detailed_error_msg': ''}
xixuan52c2fba2016-05-20 17:02:48 -0700875 try:
876 result = progress_tracker.ReadStatus()
877 if result.startswith(cros_update_progress.ERROR_TAG):
xixuan28d99072016-10-06 12:24:16 -0700878 result_dict['detailed_error_msg'] = result[len(
879 cros_update_progress.ERROR_TAG):]
xixuan28681fd2016-11-23 11:13:56 -0800880 elif result == cros_update_progress.FINISHED:
xixuan28d99072016-10-06 12:24:16 -0700881 result_dict['finished'] = True
882 result_dict['status'] = result
xixuan28681fd2016-11-23 11:13:56 -0800883 elif not cros_update_progress.IsProcessAlive(pid):
xixuan28d99072016-10-06 12:24:16 -0700884 result_dict['detailed_error_msg'] = (
885 'Cros_update process terminated midway due to unknown reason. '
886 'Last update status was %s' % result)
xixuan28681fd2016-11-23 11:13:56 -0800887 else:
888 result_dict['status'] = result
889 except IOError as e:
890 if pid and cros_update_progress.IsProcessAlive(pid):
xixuan2a0970a2016-08-10 12:12:44 -0700891 os.killpg(int(pid), signal.SIGKILL)
xixuan52c2fba2016-05-20 17:02:48 -0700892
xixuan28681fd2016-11-23 11:13:56 -0800893 result_dict['detailed_error_msg'] = str(e)
894
895 return json.dumps(result_dict)
xixuan52c2fba2016-05-20 17:02:48 -0700896
897 @cherrypy.expose
David Riley6d5fca02017-10-31 10:35:47 -0700898 def post_au_status(self, status, **kwargs):
899 """Updates the status of an auto-update task.
900
901 Callers will need to POST to this URL with a body of MIME-type
902 "multipart/form-data".
903 The body should include a single argument, 'status', containing the
904 AU status to record.
905
906 Args:
907 status: The updated status.
908 kwargs:
909 host_name: the hostname of the DUT to auto-update.
910 pid: the background process id of cros-update.
911 """
912 if 'host_name' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -0700913 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -0700914 KEY_ERROR_MSG % 'host_name')
David Riley6d5fca02017-10-31 10:35:47 -0700915
916 if 'pid' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -0700917 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -0700918 KEY_ERROR_MSG % 'pid')
David Riley6d5fca02017-10-31 10:35:47 -0700919
920 host_name = kwargs['host_name']
921 pid = kwargs['pid']
David Riley3cea2582017-11-24 22:03:01 -0800922 status = status.rstrip()
923 _Log('Recording status for %s (%s): %s' % (host_name, pid, status))
David Riley6d5fca02017-10-31 10:35:47 -0700924 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
925
David Riley3cea2582017-11-24 22:03:01 -0800926 progress_tracker.WriteStatus(status)
David Riley6d5fca02017-10-31 10:35:47 -0700927
928 return 'True'
929
930 @cherrypy.expose
xixuan52c2fba2016-05-20 17:02:48 -0700931 def handler_cleanup(self, **kwargs):
xixuan3bc974e2016-10-18 17:21:43 -0700932 """Clean track status log and temp directory for CrOS auto-update process.
xixuan52c2fba2016-05-20 17:02:48 -0700933
934 Args:
935 kwargs:
936 host_name: the hostname of the DUT to auto-update.
937 pid: the background process id of cros-update.
938 """
939 if 'host_name' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -0700940 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -0700941 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -0700942
943 if 'pid' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -0700944 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -0700945 KEY_ERROR_MSG % 'pid')
xixuan52c2fba2016-05-20 17:02:48 -0700946
947 host_name = kwargs['host_name']
948 pid = kwargs['pid']
949 cros_update_progress.DelTrackStatusFile(host_name, pid)
xixuan3bc974e2016-10-18 17:21:43 -0700950 cros_update_progress.DelAUTempDirectory(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -0700951
952 @cherrypy.expose
953 def kill_au_proc(self, **kwargs):
954 """Kill CrOS auto-update process using given process id.
955
956 Args:
957 kwargs:
958 host_name: Kill all the CrOS auto-update process of this host.
959
960 Returns:
961 True if all processes are killed properly.
962 """
963 if 'host_name' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -0700964 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -0700965 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -0700966
xixuan447ad9d2017-02-28 14:46:20 -0800967 cur_pid = kwargs.get('pid')
968
xixuan52c2fba2016-05-20 17:02:48 -0700969 host_name = kwargs['host_name']
xixuan3bc974e2016-10-18 17:21:43 -0700970 track_log_list = cros_update_progress.GetAllTrackStatusFileByHostName(
971 host_name)
xixuan52c2fba2016-05-20 17:02:48 -0700972 for log in track_log_list:
973 # The track log's full path is: path/host_name_pid.log
974 # Use splitext to remove file extension, then parse pid from the
975 # filename.
Congbin Guo3afae6c2019-08-13 16:29:42 -0700976 pid = os.path.splitext(os.path.basename(log))[0][len(host_name) + 1:]
xixuan447ad9d2017-02-28 14:46:20 -0800977 _clear_process(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -0700978
xixuan447ad9d2017-02-28 14:46:20 -0800979 if cur_pid:
980 _clear_process(host_name, cur_pid)
xixuan52c2fba2016-05-20 17:02:48 -0700981
982 return 'True'
983
984 @cherrypy.expose
985 def collect_cros_au_log(self, **kwargs):
986 """Collect CrOS auto-update log.
987
988 Args:
989 kwargs:
990 host_name: the hostname of the DUT to auto-update.
991 pid: the background process id of cros-update.
992
993 Returns:
David Haddock9f459632017-05-11 14:45:46 -0700994 A dictionary containing the execute log file and any hostlog files.
xixuan52c2fba2016-05-20 17:02:48 -0700995 """
996 if 'host_name' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -0700997 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -0700998 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -0700999
1000 if 'pid' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -07001001 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -07001002 KEY_ERROR_MSG % 'pid')
xixuan52c2fba2016-05-20 17:02:48 -07001003
1004 host_name = kwargs['host_name']
1005 pid = kwargs['pid']
xixuan3bc974e2016-10-18 17:21:43 -07001006
1007 # Fetch the execute log recorded by cros_update_progress.
xixuan1bbfaba2016-10-13 17:53:22 -07001008 au_log = cros_update_progress.ReadExecuteLogFile(host_name, pid)
1009 cros_update_progress.DelExecuteLogFile(host_name, pid)
David Haddock9f459632017-05-11 14:45:46 -07001010 # Fetch the cros_au host_logs if they exist
1011 au_hostlogs = cros_update_progress.ReadAUHostLogFiles(host_name, pid)
1012 return json.dumps({'cros_au_log': au_log, 'host_logs': au_hostlogs})
xixuan1bbfaba2016-10-13 17:53:22 -07001013
xixuan52c2fba2016-05-20 17:02:48 -07001014 @cherrypy.expose
Dan Shi2f136862016-02-11 15:38:38 -08001015 def locate_file(self, **kwargs):
1016 """Get the path to the given file name.
1017
1018 This method looks up the given file name inside specified build artifacts.
1019 One use case is to help caller to locate an apk file inside a build
1020 artifact. The location of the apk file could be different based on the
1021 branch and target.
1022
1023 Args:
1024 file_name: Name of the file to look for.
1025 artifacts: A list of artifact names to search for the file.
1026
1027 Returns:
1028 Path to the file with the given name. It's relative to the folder for the
1029 build, e.g., DATA/priv-app/sl4a/sl4a.apk
Dan Shi2f136862016-02-11 15:38:38 -08001030 """
1031 dl, _ = _get_downloader_and_factory(kwargs)
1032 try:
Joe Brennan1691f8e2017-03-15 15:53:36 -07001033 file_name = kwargs['file_name']
Dan Shi2f136862016-02-11 15:38:38 -08001034 artifacts = kwargs['artifacts']
1035 except KeyError:
Amin Hassanid4e35392019-10-03 11:02:44 -07001036 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -07001037 '`file_name` and `artifacts` are required to search '
1038 'for a file in build artifacts.')
Dan Shi2f136862016-02-11 15:38:38 -08001039 build_path = dl.GetBuildDir()
1040 for artifact in artifacts:
1041 # Get the unzipped folder of the artifact. If it's not defined in
1042 # ARTIFACT_UNZIP_FOLDER_MAP, assume the files are unzipped to the build
1043 # directory directly.
1044 folder = artifact_info.ARTIFACT_UNZIP_FOLDER_MAP.get(artifact, '')
1045 artifact_path = os.path.join(build_path, folder)
1046 for root, _, filenames in os.walk(artifact_path):
Joe Brennan1691f8e2017-03-15 15:53:36 -07001047 if file_name in set([f for f in filenames]):
Dan Shi2f136862016-02-11 15:38:38 -08001048 return os.path.relpath(os.path.join(root, file_name), build_path)
Amin Hassanid4e35392019-10-03 11:02:44 -07001049 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -07001050 'File `%s` can not be found in artifacts: %s' % (file_name, artifacts))
Dan Shi2f136862016-02-11 15:38:38 -08001051
1052 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -08001053 def setup_telemetry(self, **kwargs):
1054 """Extracts and sets up telemetry
1055
1056 This method goes through the telemetry deps packages, and stages them on
1057 the devserver to be used by the drones and the telemetry tests.
1058
1059 Args:
1060 archive_url: Google Storage URL for the build.
1061
1062 Returns:
1063 Path to the source folder for the telemetry codebase once it is staged.
1064 """
Gabe Black3b567202015-09-23 14:07:59 -07001065 dl = _get_downloader(kwargs)
Simran Basi4baad082013-02-14 13:39:18 -08001066
Gabe Black3b567202015-09-23 14:07:59 -07001067 build_path = dl.GetBuildDir()
Simran Basi4baad082013-02-14 13:39:18 -08001068 deps_path = os.path.join(build_path, 'autotest/packages')
1069 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
1070 src_folder = os.path.join(telemetry_path, 'src')
1071
1072 with self._telemetry_lock_dict.lock(telemetry_path):
1073 if os.path.exists(src_folder):
1074 # Telemetry is already fully stage return
1075 return src_folder
1076
1077 common_util.MkDirP(telemetry_path)
1078
1079 # Copy over the required deps tar balls to the telemetry directory.
1080 for dep in TELEMETRY_DEPS:
1081 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -07001082 if not os.path.exists(dep_path):
1083 # This dep does not exist (could be new), do not extract it.
1084 continue
Simran Basi4baad082013-02-14 13:39:18 -08001085 try:
1086 common_util.ExtractTarball(dep_path, telemetry_path)
1087 except common_util.CommonUtilError as e:
1088 shutil.rmtree(telemetry_path)
Amin Hassanid4e35392019-10-03 11:02:44 -07001089 raise DevServerError(str(e))
Simran Basi4baad082013-02-14 13:39:18 -08001090
1091 # By default all the tarballs extract to test_src but some parts of
1092 # the telemetry code specifically hardcoded to exist inside of 'src'.
1093 test_src = os.path.join(telemetry_path, 'test_src')
1094 try:
1095 shutil.move(test_src, src_folder)
1096 except shutil.Error:
1097 # This can occur if src_folder already exists. Remove and retry move.
1098 shutil.rmtree(src_folder)
Amin Hassanid4e35392019-10-03 11:02:44 -07001099 raise DevServerError(
Gabe Black3b567202015-09-23 14:07:59 -07001100 'Failure in telemetry setup for build %s. Appears that the '
1101 'test_src to src move failed.' % dl.GetBuild())
Simran Basi4baad082013-02-14 13:39:18 -08001102
1103 return src_folder
1104
1105 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -08001106 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -07001107 """Symbolicates a minidump using pre-downloaded symbols, returns it.
1108
1109 Callers will need to POST to this URL with a body of MIME-type
1110 "multipart/form-data".
1111 The body should include a single argument, 'minidump', containing the
1112 binary-formatted minidump to symbolicate.
1113
Chris Masone816e38c2012-05-02 12:22:36 -07001114 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -08001115 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -07001116 minidump: The binary minidump file to symbolicate.
1117 """
Chris Sosa76e44b92013-01-31 12:11:38 -08001118 # Ensure the symbols have been staged.
Dan Shif08fe492016-10-04 14:39:25 -07001119 # Try debug.tar.xz first, then debug.tgz
1120 for artifact in (artifact_info.SYMBOLS_ONLY, artifact_info.SYMBOLS):
1121 kwargs['artifacts'] = artifact
1122 dl = _get_downloader(kwargs)
1123
1124 try:
1125 if self.stage(**kwargs) == 'Success':
1126 break
1127 except build_artifact.ArtifactDownloadError:
1128 continue
1129 else:
Amin Hassanid4e35392019-10-03 11:02:44 -07001130 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -07001131 'Failed to stage symbols for %s' % dl.DescribeSource())
Chris Sosa76e44b92013-01-31 12:11:38 -08001132
Chris Masone816e38c2012-05-02 12:22:36 -07001133 to_return = ''
1134 with tempfile.NamedTemporaryFile() as local:
1135 while True:
1136 data = minidump.file.read(8192)
1137 if not data:
1138 break
1139 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -08001140
Chris Masone816e38c2012-05-02 12:22:36 -07001141 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -08001142
Gabe Black3b567202015-09-23 14:07:59 -07001143 symbols_directory = os.path.join(dl.GetBuildDir(), 'debug', 'breakpad')
Chris Sosa76e44b92013-01-31 12:11:38 -08001144
xixuanab744382017-04-27 10:41:27 -07001145 # The location of minidump_stackwalk is defined in chromeos-admin.
Chris Sosa76e44b92013-01-31 12:11:38 -08001146 stackwalk = subprocess.Popen(
xixuanab744382017-04-27 10:41:27 -07001147 ['/usr/local/bin/minidump_stackwalk', local.name, symbols_directory],
Chris Sosa76e44b92013-01-31 12:11:38 -08001148 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1149
Chris Masone816e38c2012-05-02 12:22:36 -07001150 to_return, error_text = stackwalk.communicate()
1151 if stackwalk.returncode != 0:
Amin Hassanid4e35392019-10-03 11:02:44 -07001152 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -07001153 "Can't generate stack trace: %s (rc=%d)" % (error_text,
1154 stackwalk.returncode))
Chris Masone816e38c2012-05-02 12:22:36 -07001155
1156 return to_return
1157
1158 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -08001159 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 15:31:36 -04001160 """Return a string representing the latest build for a given target.
1161
1162 Args:
1163 target: The build target, typically a combination of the board and the
1164 type of build e.g. x86-mario-release.
1165 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
1166 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-07 18:21:26 -08001167
Scott Zawalski16954532012-03-20 15:31:36 -04001168 Returns:
1169 A string representation of the latest build if one exists, i.e.
1170 R19-1993.0.0-a1-b1480.
1171 An empty string if no latest could be found.
1172 """
Don Garrettf84631a2014-01-07 18:21:26 -08001173 if not kwargs:
Scott Zawalski16954532012-03-20 15:31:36 -04001174 return _PrintDocStringAsHTML(self.latestbuild)
1175
Don Garrettf84631a2014-01-07 18:21:26 -08001176 if 'target' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -07001177 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -07001178 'Error: target= is required!')
Dan Shi61305df2015-10-26 16:52:35 -07001179
1180 if _is_android_build_request(kwargs):
1181 branch = kwargs.get('branch', None)
1182 target = kwargs.get('target', None)
1183 if not target or not branch:
Amin Hassanid4e35392019-10-03 11:02:44 -07001184 raise DevServerError('Both target and branch must be specified to query'
1185 ' for the latest Android build.')
Dan Shi61305df2015-10-26 16:52:35 -07001186 return android_build.BuildAccessor.GetLatestBuildID(target, branch)
1187
Scott Zawalski16954532012-03-20 15:31:36 -04001188 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001189 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-07 18:21:26 -08001190 updater.static_dir, kwargs['target'],
1191 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -07001192 except common_util.CommonUtilError as errmsg:
Amin Hassanid4e35392019-10-03 11:02:44 -07001193 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -07001194 str(errmsg))
Scott Zawalski16954532012-03-20 15:31:36 -04001195
1196 @cherrypy.expose
xixuan7efd0002016-04-14 15:34:01 -07001197 def list_suite_controls(self, **kwargs):
1198 """Return a list of contents of all known control files.
1199
1200 Example URL:
1201 To List all control files' content:
1202 http://dev-server/list_suite_controls?suite_name=bvt&
1203 build=daisy_spring-release/R29-4279.0.0
1204
1205 Args:
1206 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
1207 suite_name: List the control files belonging to that suite.
1208
1209 Returns:
Dan Shia1cd6522016-04-18 16:07:21 -07001210 A dictionary of all control files's path to its content for given suite.
xixuan7efd0002016-04-14 15:34:01 -07001211 """
1212 if not kwargs:
1213 return _PrintDocStringAsHTML(self.controlfiles)
1214
1215 if 'build' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -07001216 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -07001217 'Error: build= is required!')
xixuan7efd0002016-04-14 15:34:01 -07001218
1219 if 'suite_name' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -07001220 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Dan Shia1cd6522016-04-18 16:07:21 -07001221 'Error: suite_name= is required!')
xixuan7efd0002016-04-14 15:34:01 -07001222
1223 control_file_list = [
1224 line.rstrip() for line in common_util.GetControlFileListForSuite(
1225 updater.static_dir, kwargs['build'],
1226 kwargs['suite_name']).splitlines()]
1227
Dan Shia1cd6522016-04-18 16:07:21 -07001228 control_file_content_dict = {}
xixuan7efd0002016-04-14 15:34:01 -07001229 for control_path in control_file_list:
Dan Shia1cd6522016-04-18 16:07:21 -07001230 control_file_content_dict[control_path] = (common_util.GetControlFile(
xixuan7efd0002016-04-14 15:34:01 -07001231 updater.static_dir, kwargs['build'], control_path))
1232
Dan Shia1cd6522016-04-18 16:07:21 -07001233 return json.dumps(control_file_content_dict)
xixuan7efd0002016-04-14 15:34:01 -07001234
1235 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -08001236 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 17:17:28 -05001237 """Return a control file or a list of all known control files.
1238
1239 Example URL:
1240 To List all control files:
beepsbd337242013-07-09 22:44:06 -07001241 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
1242 To List all control files for, say, the bvt suite:
1243 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -05001244 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -05001245 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 -05001246
1247 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -05001248 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -05001249 control_path: If you want the contents of a control file set this
1250 to the path. E.g. client/site_tests/sleeptest/control
1251 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -07001252 suite_name: If control_path is not specified but a suite_name is
1253 specified, list the control files belonging to that suite instead of
1254 all control files. The empty string for suite_name will list all control
1255 files for the build.
Don Garrettf84631a2014-01-07 18:21:26 -08001256
Scott Zawalski4647ce62012-01-03 17:17:28 -05001257 Returns:
1258 Contents of a control file if control_path is provided.
1259 A list of control files if no control_path is provided.
1260 """
Don Garrettf84631a2014-01-07 18:21:26 -08001261 if not kwargs:
Scott Zawalski4647ce62012-01-03 17:17:28 -05001262 return _PrintDocStringAsHTML(self.controlfiles)
1263
Don Garrettf84631a2014-01-07 18:21:26 -08001264 if 'build' not in kwargs:
Amin Hassanid4e35392019-10-03 11:02:44 -07001265 raise common_util.DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -07001266 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -05001267
Don Garrettf84631a2014-01-07 18:21:26 -08001268 if 'control_path' not in kwargs:
1269 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-09 22:44:06 -07001270 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-07 18:21:26 -08001271 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-09 22:44:06 -07001272 else:
1273 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-07 18:21:26 -08001274 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -05001275 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001276 return common_util.GetControlFile(
Don Garrettf84631a2014-01-07 18:21:26 -08001277 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -08001278
1279 @cherrypy.expose
Simran Basi99e63c02014-05-20 10:39:52 -07001280 def xbuddy_translate(self, *args, **kwargs):
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001281 """Translates an xBuddy path to a real path to artifact if it exists.
1282
1283 Args:
Simran Basi99e63c02014-05-20 10:39:52 -07001284 args: An xbuddy path in the form of {local|remote}/build_id/artifact.
1285 Local searches the devserver's static directory. Remote searches a
1286 Google Storage image archive.
1287
1288 Kwargs:
1289 image_dir: Google Storage image archive to search in if requesting a
1290 remote artifact. If none uses the default bucket.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001291
1292 Returns:
Simran Basi99e63c02014-05-20 10:39:52 -07001293 String in the format of build_id/artifact as stored on the local server
1294 or in Google Storage.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001295 """
Simran Basi99e63c02014-05-20 10:39:52 -07001296 build_id, filename = self._xbuddy.Translate(
Gabe Black3b567202015-09-23 14:07:59 -07001297 args, image_dir=kwargs.get('image_dir'))
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001298 response = os.path.join(build_id, filename)
1299 _Log('Path translation requested, returning: %s', response)
1300 return response
1301
1302 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -07001303 def xbuddy(self, *args, **kwargs):
1304 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -07001305
1306 Args:
joycheneaf4cfc2013-07-02 08:38:57 -07001307 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -07001308 components of the path. The path can be understood as
1309 "{local|remote}/build_id/artifact" where build_id is composed of
1310 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -07001311
joychen121fc9b2013-08-02 14:30:30 -07001312 The first path element is optional, and can be "remote" or "local"
1313 If local (the default), devserver will not attempt to access Google
1314 Storage, and will only search the static directory for the files.
1315 If remote, devserver will try to obtain the artifact off GS if it's
1316 not found locally.
1317 The board is the familiar board name, optionally suffixed.
1318 The version can be the google storage version number, and may also be
1319 any of a number of xBuddy defined version aliases that will be
1320 translated into the latest built image that fits the description.
1321 Defaults to latest.
1322 The artifact is one of a number of image or artifact aliases used by
1323 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -07001324
1325 Kwargs:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001326 for_update: {true|false}
Amin Hassanie9ffb862019-09-25 17:10:40 -07001327 if true, prepares the update payloads for the image,
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001328 and returns the update uri to pass to the
1329 update_engine_client.
joychen3cb228e2013-06-12 12:13:13 -07001330 return_dir: {true|false}
1331 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001332 relative_path: {true|false}
1333 if set to true, returns the relative path to the payload
1334 directory from static_dir.
joychen3cb228e2013-06-12 12:13:13 -07001335 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -07001336 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -07001337 or
joycheneaf4cfc2013-07-02 08:38:57 -07001338 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -07001339
1340 Returns:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001341 If |for_update|, returns a redirect to the image or update file
1342 on the devserver. E.g.,
1343 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
1344 chromium-test-image.bin
1345 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
1346 http://host:port/static/x86-generic-release/R26-4000.0.0/
1347 If |relative_path| is true, return a relative path the folder where the
1348 payloads are. E.g.,
1349 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 12:13:13 -07001350 """
Chris Sosa75490802013-09-30 17:21:45 -07001351 boolean_string = kwargs.get('for_update')
1352 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001353 boolean_string = kwargs.get('return_dir')
1354 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
1355 boolean_string = kwargs.get('relative_path')
1356 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -07001357
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001358 if return_dir and relative_path:
Chris Sosa4b951602014-04-09 20:26:07 -07001359 raise common_util.DevServerHTTPError(
Amin Hassanid4e35392019-10-03 11:02:44 -07001360 http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -07001361 'Cannot specify both return_dir and relative_path')
Chris Sosa75490802013-09-30 17:21:45 -07001362
1363 # For updates, we optimize downloading of test images.
1364 file_name = None
1365 build_id = None
1366 if for_update:
1367 try:
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001368 build_id = self._xbuddy.StageTestArtifactsForUpdate(args)
Chris Sosa75490802013-09-30 17:21:45 -07001369 except build_artifact.ArtifactDownloadError:
1370 build_id = None
1371
1372 if not build_id:
1373 build_id, file_name = self._xbuddy.Get(args)
1374
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001375 if for_update:
Amin Hassanie9ffb862019-09-25 17:10:40 -07001376 _Log('Payloads requested.')
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001377 # Forces payload to be in cache and symlinked into build_id dir.
Amin Hassanie9ffb862019-09-25 17:10:40 -07001378 updater.GetUpdateForLabel(build_id)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001379
1380 response = None
1381 if return_dir:
1382 response = os.path.join(cherrypy.request.base, 'static', build_id)
1383 _Log('Directory requested, returning: %s', response)
1384 elif relative_path:
1385 response = build_id
1386 _Log('Relative path requested, returning: %s', response)
1387 elif for_update:
1388 response = os.path.join(cherrypy.request.base, 'update', build_id)
1389 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 12:13:13 -07001390 else:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001391 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 14:30:30 -07001392 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001393 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 14:30:30 -07001394 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -07001395
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001396 return response
1397
joychen3cb228e2013-06-12 12:13:13 -07001398 @cherrypy.expose
1399 def xbuddy_list(self):
1400 """Lists the currently available images & time since last access.
1401
Gilad Arnold452fd272014-02-04 11:09:28 -08001402 Returns:
1403 A string representation of a list of tuples [(build_id, time since last
1404 access),...]
joychen3cb228e2013-06-12 12:13:13 -07001405 """
1406 return self._xbuddy.List()
1407
1408 @cherrypy.expose
1409 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 11:09:28 -08001410 """Returns the number of images cached by xBuddy."""
joychen3cb228e2013-06-12 12:13:13 -07001411 return self._xbuddy.Capacity()
1412
1413 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001414 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001415 """Presents a welcome message and documentation links."""
Congbin Guo6bc32182019-08-20 17:54:30 -07001416 html_template = (
1417 'Welcome to the Dev Server!<br>\n'
1418 '<br>\n'
1419 'Here are the available methods, click for documentation:<br>\n'
1420 '<br>\n'
1421 '%s')
1422
1423 exposed_methods = []
1424 for app in cherrypy.tree.apps.values():
1425 exposed_methods += _FindExposedMethods(
1426 app.root, app.script_name.lstrip('/'),
1427 unlisted=self._UNLISTED_METHODS)
1428
1429 return html_template % '<br>\n'.join(
1430 ['<a href=doc/%s>%s</a>' % (name, name)
1431 for name in sorted(exposed_methods)])
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001432
1433 @cherrypy.expose
1434 def doc(self, *args):
1435 """Shows the documentation for available methods / URLs.
1436
Amin Hassani08e42d22019-06-03 00:31:30 -07001437 Examples:
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001438 http://myhost/doc/update
1439 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001440 name = '/'.join(args)
Congbin Guo6bc32182019-08-20 17:54:30 -07001441 method = _GetExposedMethod(name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001442 if not method:
Amin Hassanid4e35392019-10-03 11:02:44 -07001443 raise DevServerError("No exposed method named `%s'" % name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001444 if not method.__doc__:
Amin Hassanid4e35392019-10-03 11:02:44 -07001445 raise DevServerError("No documentation for exposed method `%s'" % name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001446 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -07001447
Dale Curtisc9aaf3a2011-08-09 15:47:40 -07001448 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001449 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001450 """Handles an update check from a Chrome OS client.
1451
1452 The HTTP request should contain the standard Omaha-style XML blob. The URL
1453 line may contain an additional intermediate path to the update payload.
1454
joychen121fc9b2013-08-02 14:30:30 -07001455 This request can be handled in one of 4 ways, depending on the devsever
1456 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -07001457
Amin Hassanie9ffb862019-09-25 17:10:40 -07001458 1. No intermediate path. DEPRECATED
joychen121fc9b2013-08-02 14:30:30 -07001459
1460 2. Path explicitly invokes XBuddy
1461 If there is a path given, it can explicitly invoke xbuddy by prefixing it
1462 with 'xbuddy'. This path is then used to acquire an image binary for the
1463 devserver to generate an update payload from. Devserver then serves this
1464 payload.
1465
1466 3. Path is left for the devserver to interpret.
1467 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
1468 to generate a payload from the test image in that directory and serve it.
1469
joychen121fc9b2013-08-02 14:30:30 -07001470 Examples:
joychen121fc9b2013-08-02 14:30:30 -07001471 2. Explicitly invoke xbuddy
1472 update_engine_client --omaha_url=
1473 http://myhost/update/xbuddy/remote/board/version/dev
1474 This would go to GS to download the dev image for the board, from which
1475 the devserver would generate a payload to serve.
1476
1477 3. Give a path for devserver to interpret
1478 update_engine_client --omaha_url=http://myhost/update/some/random/path
1479 This would attempt, in order to:
1480 a) Generate an update from a test image binary if found in
1481 static_dir/some/random/path.
1482 b) Serve an update payload found in static_dir/some/random/path.
1483 c) Hope that some/random/path takes the form "board/version" and
1484 and attempt to download an update payload for that board/version
1485 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001486 """
joychen121fc9b2013-08-02 14:30:30 -07001487 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -08001488 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -07001489 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-11 19:49:01 -07001490
joychen121fc9b2013-08-02 14:30:30 -07001491 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001492
Dan Shif5ce2de2013-04-25 16:06:32 -07001493
Chris Sosadbc20082012-12-10 13:39:11 -08001494def _CleanCache(cache_dir, wipe):
1495 """Wipes any excess cached items in the cache_dir.
1496
1497 Args:
1498 cache_dir: the directory we are wiping from.
1499 wipe: If True, wipe all the contents -- not just the excess.
1500 """
1501 if wipe:
1502 # Clear the cache and exit on error.
1503 cmd = 'rm -rf %s/*' % cache_dir
1504 if os.system(cmd) != 0:
1505 _Log('Failed to clear the cache with %s' % cmd)
1506 sys.exit(1)
1507 else:
1508 # Clear all but the last N cached updates
1509 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
1510 (cache_dir, CACHED_ENTRIES))
1511 if os.system(cmd) != 0:
1512 _Log('Failed to clean up old delta cache files with %s' % cmd)
1513 sys.exit(1)
1514
1515
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001516def _AddTestingOptions(parser):
1517 group = optparse.OptionGroup(
1518 parser, 'Advanced Testing Options', 'These are used by test scripts and '
1519 'developers writing integration tests utilizing the devserver. They are '
1520 'not intended to be really used outside the scope of someone '
1521 'knowledgable about the test.')
1522 group.add_option('--exit',
1523 action='store_true',
Amin Hassanie9ffb862019-09-25 17:10:40 -07001524 help='do not start the server (yet clear cache)')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001525 group.add_option('--host_log',
1526 action='store_true', default=False,
1527 help='record history of host update events (/api/hostlog)')
1528 group.add_option('--max_updates',
Gabe Black3b567202015-09-23 14:07:59 -07001529 metavar='NUM', default=-1, type='int',
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001530 help='maximum number of update checks handled positively '
1531 '(default: unlimited)')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001532 group.add_option('--proxy_port',
1533 metavar='PORT', default=None, type='int',
1534 help='port to have the client connect to -- basically the '
1535 'devserver lies to the update to tell it to get the payload '
1536 'from a different port that will proxy the request back to '
1537 'the devserver. The proxy must be managed outside the '
1538 'devserver.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001539 parser.add_option_group(group)
1540
1541
1542def _AddUpdateOptions(parser):
1543 group = optparse.OptionGroup(
1544 parser, 'Autoupdate Options', 'These options can be used to change '
Amin Hassanie9ffb862019-09-25 17:10:40 -07001545 'how the devserver serve update payloads. Please '
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001546 'note that all of these option affect how a payload is generated and so '
1547 'do not work in archive-only mode.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001548 group.add_option('--critical_update',
1549 action='store_true', default=False,
1550 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001551 group.add_option('--payload',
1552 metavar='PATH',
1553 help='use the update payload from specified directory '
1554 '(update.gz).')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001555 parser.add_option_group(group)
1556
1557
1558def _AddProductionOptions(parser):
1559 group = optparse.OptionGroup(
1560 parser, 'Advanced Server Options', 'These options can be used to changed '
1561 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001562 group.add_option('--clear_cache',
1563 action='store_true', default=False,
1564 help='At startup, removes all cached entries from the'
Amin Hassanid4e35392019-10-03 11:02:44 -07001565 "devserver's cache.")
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001566 group.add_option('--logfile',
1567 metavar='PATH',
1568 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -07001569 group.add_option('--pidfile',
1570 metavar='PATH',
1571 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 11:04:13 -08001572 group.add_option('--portfile',
1573 metavar='PATH',
1574 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001575 group.add_option('--production',
1576 action='store_true', default=False,
1577 help='have the devserver use production values when '
1578 'starting up. This includes using more threads and '
1579 'performing less logging.')
1580 parser.add_option_group(group)
1581
1582
Paul Hobbsef4e0702016-06-27 17:01:42 -07001583def MakeLogHandler(logfile):
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001584 """Create a LogHandler instance used to log all messages."""
1585 hdlr_cls = handlers.TimedRotatingFileHandler
1586 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
xixuan3d48bff2017-01-30 19:00:09 -08001587 interval=_LOG_ROTATION_INTERVAL,
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001588 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -07001589 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001590 return hdlr
1591
1592
Chris Sosacde6bf42012-05-31 18:36:39 -07001593def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001594 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -08001595 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -07001596
1597 # get directory that the devserver is run from
1598 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -07001599 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -07001600 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001601 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -07001602 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -07001603 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001604 parser.add_option('--port',
1605 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 13:13:28 -08001606 help=('port for the dev server to use; if zero, binds to '
1607 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001608 parser.add_option('-t', '--test_image',
1609 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -07001610 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -07001611 parser.add_option('-x', '--xbuddy_manage_builds',
1612 action='store_true',
1613 default=False,
1614 help='If set, allow xbuddy to manage images in'
1615 'build/images.')
Dan Shi72b16132015-10-08 12:10:33 -07001616 parser.add_option('-a', '--android_build_credential',
1617 default=None,
1618 help='Path to a json file which contains the credential '
1619 'needed to access Android builds.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001620 _AddProductionOptions(parser)
1621 _AddUpdateOptions(parser)
1622 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -07001623 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001624
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001625 # Handle options that must be set globally in cherrypy. Do this
1626 # work up front, because calls to _Log() below depend on this
1627 # initialization.
1628 if options.production:
1629 cherrypy.config.update({'environment': 'production'})
1630 if not options.logfile:
1631 cherrypy.config.update({'log.screen': True})
1632 else:
1633 cherrypy.config.update({'log.error_file': '',
1634 'log.access_file': ''})
Paul Hobbsef4e0702016-06-27 17:01:42 -07001635 hdlr = MakeLogHandler(options.logfile)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001636 # Pylint can't seem to process these two calls properly
1637 # pylint: disable=E1101
1638 cherrypy.log.access_log.addHandler(hdlr)
1639 cherrypy.log.error_log.addHandler(hdlr)
1640 # pylint: enable=E1101
1641
joychened64b222013-06-21 16:39:34 -07001642 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001643 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001644
joychened64b222013-06-21 16:39:34 -07001645 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001646 # If our devserver is only supposed to serve payloads, we shouldn't be
1647 # mucking with the cache at all. If the devserver hadn't previously
1648 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001649 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001650 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001651 else:
1652 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001653
Chris Sosadbc20082012-12-10 13:39:11 -08001654 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 16:39:34 -07001655 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001656
Amin Hassanie9ffb862019-09-25 17:10:40 -07001657 _xbuddy = xbuddy.XBuddy(manage_builds=options.xbuddy_manage_builds,
joychen121fc9b2013-08-02 14:30:30 -07001658 static_dir=options.static_dir)
Chris Sosa75490802013-09-30 17:21:45 -07001659 if options.clear_cache and options.xbuddy_manage_builds:
1660 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 14:30:30 -07001661
Chris Sosa6a3697f2013-01-29 16:44:43 -08001662 # We allow global use here to share with cherrypy classes.
1663 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001664 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001665 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001666 _xbuddy,
joychened64b222013-06-21 16:39:34 -07001667 static_dir=options.static_dir,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001668 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001669 proxy_port=options.proxy_port,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001670 critical_update=options.critical_update,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001671 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001672 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001673 )
Chris Sosa7c931362010-10-11 19:49:01 -07001674
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001675 if options.exit:
1676 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001677
joychen3cb228e2013-06-12 12:13:13 -07001678 dev_server = DevServerRoot(_xbuddy)
Congbin Guo3afae6c2019-08-13 16:29:42 -07001679 health_checker_app = health_checker.Root(dev_server, options.static_dir)
joychen3cb228e2013-06-12 12:13:13 -07001680
Amin Hassanic5af4262019-11-13 13:37:20 -08001681 # Patch CherryPy to support binding to any available port (--port=0) only for
1682 # cherrypy versions smaller or equal to 3.2.2.
1683 #
1684 # TODO(crbug/1006305): Remove this once we have deprecated omaha_devserver.py
1685 # in the autotests as that is the only use case.
1686 #
1687 # pylint: disable=no-member
1688 if (distutils.version.StrictVersion(cherrypy.__version__) <=
1689 distutils.version.StrictVersion('3.2.2')):
1690 cherrypy_ext.ZeroPortPatcher.DoPatch(cherrypy)
1691 # pylint: enable=no-member
1692
Chris Sosa855b8932013-08-21 13:24:55 -07001693 if options.pidfile:
1694 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1695
Gilad Arnold11fbef42014-02-10 11:04:13 -08001696 if options.portfile:
1697 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1698
Dan Shiafd5c6c2016-01-07 10:27:03 -08001699 if (options.android_build_credential and
1700 os.path.exists(options.android_build_credential)):
1701 try:
1702 with open(options.android_build_credential) as f:
1703 android_build.BuildAccessor.credential_info = json.load(f)
1704 except ValueError as e:
1705 _Log('Failed to load the android build credential: %s. Error: %s.' %
1706 (options.android_build_credential, e))
Congbin Guo3afae6c2019-08-13 16:29:42 -07001707
1708 cherrypy.tree.mount(health_checker_app, '/check_health',
1709 config=health_checker.get_config())
joychen3cb228e2013-06-12 12:13:13 -07001710 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001711
1712
1713if __name__ == '__main__':
1714 main()