blob: 34b50435d0976c099e846dd2276e40b2191ecbab [file] [log] [blame]
David Riley2fcb0122017-11-02 11:25:39 -07001#!/usr/bin/env python2
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -07002# -*- coding: utf-8 -*-
Chris Sosa781ba6d2012-04-11 12:44:43 -07003# Copyright (c) 2009-2012 The Chromium OS Authors. All rights reserved.
rtc@google.comded22402009-10-26 22:36:21 +00004# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
Chris Sosa3ae4dc12013-03-29 11:47:00 -07007"""Chromium OS development server that can be used for all forms of update.
8
9This devserver can be used to perform system-wide autoupdate and update
10of specific portage packages on devices running Chromium OS derived operating
Amin Hassanie9ffb862019-09-25 17:10:40 -070011systems.
Chris Sosa3ae4dc12013-03-29 11:47:00 -070012
Amin Hassanie9ffb862019-09-25 17:10:40 -070013The devserver is configured to stage and
Chris Sosa3ae4dc12013-03-29 11:47:00 -070014serve artifacts from Google Storage using the credentials provided to it before
15it is run. The easiest way to understand this is that the devserver is
16functioning as a local cache for artifacts produced and uploaded by build
17servers. Users of this form of devserver can either download the artifacts
18from the devservers static directory OR use the update RPC to perform a
19system-wide autoupdate. Archive mode is always active.
20
Chris Sosa3ae4dc12013-03-29 11:47:00 -070021For autoupdates, there are many more advanced options that can help specify
22how to update and which payload to give to a requester.
23"""
24
Gabe Black3b567202015-09-23 14:07:59 -070025from __future__ import print_function
Chris Sosa7c931362010-10-11 19:49:01 -070026
Gilad Arnold55a2a372012-10-02 09:46:32 -070027import json
David Riley2fcb0122017-11-02 11:25:39 -070028import optparse # pylint: disable=deprecated-module
rtc@google.comded22402009-10-26 22:36:21 +000029import os
Scott Zawalski4647ce62012-01-03 17:17:28 -050030import re
Simran Basi4baad082013-02-14 13:39:18 -080031import shutil
xixuan52c2fba2016-05-20 17:02:48 -070032import signal
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -080033import socket
Chris Masone816e38c2012-05-02 12:22:36 -070034import subprocess
J. Richard Barnette3d977b82013-04-23 11:05:19 -070035import sys
Chris Masone816e38c2012-05-02 12:22:36 -070036import tempfile
Dan Shi59ae7092013-06-04 14:37:27 -070037import threading
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -070038import types
J. Richard Barnette3d977b82013-04-23 11:05:19 -070039from logging import handlers
40
Amin Hassanid4e35392019-10-03 11:02:44 -070041from six.moves import http_client
42
43# pylint: disable=no-name-in-module, import-error
J. Richard Barnette3d977b82013-04-23 11:05:19 -070044import cherrypy
Chris Sosa855b8932013-08-21 13:24:55 -070045from cherrypy import _cplogging as cplogging
Amin Hassanid4e35392019-10-03 11:02:44 -070046from cherrypy.process import plugins
47# pylint: enable=no-name-in-module, import-error
rtc@google.comded22402009-10-26 22:36:21 +000048
Achuith Bhandarkar662fb722019-10-31 16:12:49 -070049import autoupdate
50import cherrypy_ext
Achuith Bhandarkar662fb722019-10-31 16:12:49 -070051import health_checker
52
Richard Barnettedf35c322017-08-18 17:02:13 -070053# This must happen before any local modules get a chance to import
54# anything from chromite. Otherwise, really bad things will happen, and
55# you will _not_ understand why.
Congbin Guo3afae6c2019-08-13 16:29:42 -070056import setup_chromite # pylint: disable=unused-import
Amin Hassanie427e212019-10-28 11:04:27 -070057from chromite.lib import cros_update_progress
Achuith Bhandarkar662fb722019-10-31 16:12:49 -070058from chromite.lib.xbuddy import android_build
59from chromite.lib.xbuddy import artifact_info
60from chromite.lib.xbuddy import build_artifact
61from chromite.lib.xbuddy import cherrypy_log_util
62from chromite.lib.xbuddy import common_util
63from chromite.lib.xbuddy import devserver_constants
64from chromite.lib.xbuddy import downloader
65from chromite.lib.xbuddy import xbuddy
Gilad Arnoldc65330c2012-09-20 15:17:48 -070066
Gilad Arnoldc65330c2012-09-20 15:17:48 -070067# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080068def _Log(message, *args):
Achuith Bhandarkar662fb722019-10-31 16:12:49 -070069 return cherrypy_log_util.LogWithTag('DEVSERVER', message, *args)
Frank Farzan40160872011-12-12 18:39:18 -080070
Chris Sosa417e55d2011-01-25 16:40:48 -080071CACHED_ENTRIES = 12
Don Garrettf90edf02010-11-16 17:36:14 -080072
Simran Basi4baad082013-02-14 13:39:18 -080073TELEMETRY_FOLDER = 'telemetry_src'
74TELEMETRY_DEPS = ['dep-telemetry_dep.tar.bz2',
75 'dep-page_cycler_dep.tar.bz2',
Simran Basi0d078682013-03-22 16:40:04 -070076 'dep-chrome_test.tar.bz2',
77 'dep-perf_data_dep.tar.bz2']
Simran Basi4baad082013-02-14 13:39:18 -080078
Chris Sosa0356d3b2010-09-16 15:46:22 -070079# Sets up global to share between classes.
rtc@google.com21a5ca32009-11-04 18:23:23 +000080updater = None
rtc@google.comded22402009-10-26 22:36:21 +000081
xixuan3d48bff2017-01-30 19:00:09 -080082# Log rotation parameters. These settings correspond to twice a day once
83# devserver is started, with about two weeks (28 backup files) of old logs
84# kept for backup.
J. Richard Barnette3d977b82013-04-23 11:05:19 -070085#
xixuan3d48bff2017-01-30 19:00:09 -080086# For more, see the documentation in standard python library for
J. Richard Barnette3d977b82013-04-23 11:05:19 -070087# logging.handlers.TimedRotatingFileHandler
xixuan3d48bff2017-01-30 19:00:09 -080088_LOG_ROTATION_TIME = 'H'
Congbin Guo3afae6c2019-08-13 16:29:42 -070089_LOG_ROTATION_INTERVAL = 12 # hours
90_LOG_ROTATION_BACKUP = 28 # backup counts
Frank Farzan40160872011-12-12 18:39:18 -080091
xixuan52c2fba2016-05-20 17:02:48 -070092# Auto-update parameters
93
94# Error msg for missing key in CrOS auto-update.
Xixuan Wu32af9f12017-11-13 14:11:44 -080095KEY_ERROR_MSG = 'Key Error in RPC: %s= is required'
xixuan52c2fba2016-05-20 17:02:48 -070096
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -080097# Error msg for deprecated RPC usage.
98DEPRECATED_RPC_ERROR_MSG = ('The %s RPC has been deprecated. Usage of this '
99 'RPC is discouraged. Please go to '
100 'go/devserver-deprecation for more information.')
101
xixuan52c2fba2016-05-20 17:02:48 -0700102
Amin Hassanid4e35392019-10-03 11:02:44 -0700103class DevServerError(Exception):
104 """Exception class used by DevServer."""
105
106
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -0800107class DeprecatedRPCError(DevServerError):
108 """Exception class used when an RPC is deprecated but is still being used."""
109
110 def __init__(self, rpc_name):
111 """Constructor for DeprecatedRPCError class.
112
113 :param rpc_name: (str) name of the RPC that has been deprecated.
114 """
Amin Hassani6ecda232020-03-09 19:03:23 -0700115 super(DeprecatedRPCError, self).__init__(
116 DEPRECATED_RPC_ERROR_MSG % rpc_name)
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -0800117 self.rpc_name = rpc_name
118
119
Amin Hassani722e0962019-11-15 15:45:31 -0800120class DevServerHTTPError(cherrypy.HTTPError):
121 """Exception class to log the HTTPResponse before routing it to cherrypy."""
122 def __init__(self, status, message):
123 """CherryPy error with logging.
124
125 Args:
126 status: HTTPResponse status.
127 message: Message associated with the response.
128 """
129 cherrypy.HTTPError.__init__(self, status, message)
130 _Log('HTTPError status: %s message: %s', status, message)
131
132
Gabe Black3b567202015-09-23 14:07:59 -0700133def _canonicalize_archive_url(archive_url):
134 """Canonicalizes archive_url strings.
135
136 Raises:
137 DevserverError: if archive_url is not set.
138 """
139 if archive_url:
140 if not archive_url.startswith('gs://'):
Amin Hassanid4e35392019-10-03 11:02:44 -0700141 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700142 "Archive URL isn't from Google Storage (%s) ." % archive_url)
Gabe Black3b567202015-09-23 14:07:59 -0700143
144 return archive_url.rstrip('/')
145 else:
Amin Hassanid4e35392019-10-03 11:02:44 -0700146 raise DevServerError('Must specify an archive_url in the request')
Gabe Black3b567202015-09-23 14:07:59 -0700147
148
149def _canonicalize_local_path(local_path):
150 """Canonicalizes |local_path| strings.
151
152 Raises:
153 DevserverError: if |local_path| is not set.
154 """
155 # Restrict staging of local content to only files within the static
156 # directory.
157 local_path = os.path.abspath(local_path)
158 if not local_path.startswith(updater.static_dir):
Amin Hassanid4e35392019-10-03 11:02:44 -0700159 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700160 'Local path %s must be a subdirectory of the static'
161 ' directory: %s' % (local_path, updater.static_dir))
Gabe Black3b567202015-09-23 14:07:59 -0700162
163 return local_path.rstrip('/')
164
165
166def _get_artifacts(kwargs):
167 """Returns a tuple of named and file artifacts given the stage rpc kwargs.
168
169 Raises:
170 DevserverError if no artifacts would be returned.
171 """
172 artifacts = kwargs.get('artifacts')
173 files = kwargs.get('files')
174 if not artifacts and not files:
Amin Hassanid4e35392019-10-03 11:02:44 -0700175 raise DevServerError('No artifacts specified.')
Gabe Black3b567202015-09-23 14:07:59 -0700176
177 # Note we NEED to coerce files to a string as we get raw unicode from
178 # cherrypy and we treat files as strings elsewhere in the code.
179 return (str(artifacts).split(',') if artifacts else [],
180 str(files).split(',') if files else [])
181
182
Dan Shi61305df2015-10-26 16:52:35 -0700183def _is_android_build_request(kwargs):
184 """Check if a devserver call is for Android build, based on the arguments.
185
186 This method exams the request's arguments (os_type) to determine if the
187 request is for Android build. If os_type is set to `android`, returns True.
188 If os_type is not set or has other values, returns False.
189
190 Args:
191 kwargs: Keyword arguments for the request.
192
193 Returns:
194 True if the request is for Android build. False otherwise.
195 """
196 os_type = kwargs.get('os_type', None)
197 return os_type == 'android'
198
199
Gabe Black3b567202015-09-23 14:07:59 -0700200def _get_downloader(kwargs):
201 """Returns the downloader based on passed in arguments.
202
203 Args:
Amin Hassani08e42d22019-06-03 00:31:30 -0700204 kwargs: Keyword arguments for the request.
Gabe Black3b567202015-09-23 14:07:59 -0700205 """
206 local_path = kwargs.get('local_path')
207 if local_path:
208 local_path = _canonicalize_local_path(local_path)
209
210 dl = None
211 if local_path:
Prathmesh Prabhu58d08932018-01-19 15:08:19 -0800212 delete_source = _parse_boolean_arg(kwargs, 'delete_source')
213 dl = downloader.LocalDownloader(updater.static_dir, local_path,
214 delete_source=delete_source)
Gabe Black3b567202015-09-23 14:07:59 -0700215
Dan Shi61305df2015-10-26 16:52:35 -0700216 if not _is_android_build_request(kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700217 archive_url = kwargs.get('archive_url')
218 if not archive_url and not local_path:
Amin Hassanid4e35392019-10-03 11:02:44 -0700219 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700220 'Requires archive_url or local_path to be specified.')
Gabe Black3b567202015-09-23 14:07:59 -0700221 if archive_url and local_path:
Amin Hassanid4e35392019-10-03 11:02:44 -0700222 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700223 'archive_url and local_path can not both be specified.')
Gabe Black3b567202015-09-23 14:07:59 -0700224 if not dl:
225 archive_url = _canonicalize_archive_url(archive_url)
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -0700226 dl = downloader.GoogleStorageDownloader(
227 updater.static_dir, archive_url,
228 downloader.GoogleStorageDownloader.GetBuildIdFromArchiveURL(
229 archive_url))
Gabe Black3b567202015-09-23 14:07:59 -0700230 elif not dl:
231 target = kwargs.get('target', None)
Dan Shi72b16132015-10-08 12:10:33 -0700232 branch = kwargs.get('branch', None)
Dan Shi61305df2015-10-26 16:52:35 -0700233 build_id = kwargs.get('build_id', None)
234 if not target or not branch or not build_id:
Amin Hassanid4e35392019-10-03 11:02:44 -0700235 raise DevServerError('target, branch, build ID must all be specified for '
236 'downloading Android build.')
Dan Shi72b16132015-10-08 12:10:33 -0700237 dl = downloader.AndroidBuildDownloader(updater.static_dir, branch, build_id,
238 target)
Gabe Black3b567202015-09-23 14:07:59 -0700239
240 return dl
241
242
243def _get_downloader_and_factory(kwargs):
244 """Returns the downloader and artifact factory based on passed in arguments.
245
246 Args:
Amin Hassani08e42d22019-06-03 00:31:30 -0700247 kwargs: Keyword arguments for the request.
Gabe Black3b567202015-09-23 14:07:59 -0700248 """
249 artifacts, files = _get_artifacts(kwargs)
250 dl = _get_downloader(kwargs)
251
Achuith Bhandarkar2a1fcd82019-10-17 17:45:58 -0700252 if (isinstance(dl, (downloader.GoogleStorageDownloader,
253 downloader.LocalDownloader))):
Gabe Black3b567202015-09-23 14:07:59 -0700254 factory_class = build_artifact.ChromeOSArtifactFactory
Dan Shi72b16132015-10-08 12:10:33 -0700255 elif isinstance(dl, downloader.AndroidBuildDownloader):
Gabe Black3b567202015-09-23 14:07:59 -0700256 factory_class = build_artifact.AndroidArtifactFactory
257 else:
Amin Hassanid4e35392019-10-03 11:02:44 -0700258 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -0700259 'Unrecognized value for downloader type: %s' % type(dl))
Gabe Black3b567202015-09-23 14:07:59 -0700260
261 factory = factory_class(dl.GetBuildDir(), artifacts, files, dl.GetBuild())
262
263 return dl, factory
264
265
Scott Zawalski4647ce62012-01-03 17:17:28 -0500266def _LeadingWhiteSpaceCount(string):
267 """Count the amount of leading whitespace in a string.
268
269 Args:
270 string: The string to count leading whitespace in.
Don Garrettf84631a2014-01-07 18:21:26 -0800271
Scott Zawalski4647ce62012-01-03 17:17:28 -0500272 Returns:
273 number of white space chars before characters start.
274 """
Gabe Black3b567202015-09-23 14:07:59 -0700275 matched = re.match(r'^\s+', string)
Scott Zawalski4647ce62012-01-03 17:17:28 -0500276 if matched:
277 return len(matched.group())
278
279 return 0
280
281
282def _PrintDocStringAsHTML(func):
283 """Make a functions docstring somewhat HTML style.
284
285 Args:
286 func: The function to return the docstring from.
Don Garrettf84631a2014-01-07 18:21:26 -0800287
Scott Zawalski4647ce62012-01-03 17:17:28 -0500288 Returns:
289 A string that is somewhat formated for a web browser.
290 """
291 # TODO(scottz): Make this parse Args/Returns in a prettier way.
292 # Arguments could be bolded and indented etc.
293 html_doc = []
294 for line in func.__doc__.splitlines():
295 leading_space = _LeadingWhiteSpaceCount(line)
296 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700297 line = ' ' * leading_space + line
Scott Zawalski4647ce62012-01-03 17:17:28 -0500298
299 html_doc.append('<BR>%s' % line)
300
301 return '\n'.join(html_doc)
302
303
Simran Basief83d6a2014-08-28 14:32:01 -0700304def _GetUpdateTimestampHandler(static_dir):
305 """Returns a handler to update directory staged.timestamp.
306
307 This handler resets the stage.timestamp whenever static content is accessed.
308
309 Args:
310 static_dir: Directory from which static content is being staged.
311
312 Returns:
Amin Hassani08e42d22019-06-03 00:31:30 -0700313 A cherrypy handler to update the timestamp of accessed content.
Simran Basief83d6a2014-08-28 14:32:01 -0700314 """
315 def UpdateTimestampHandler():
316 if not '404' in cherrypy.response.status:
317 build_match = re.match(devserver_constants.STAGED_BUILD_REGEX,
318 cherrypy.request.path_info)
319 if build_match:
320 build_dir = os.path.join(static_dir, build_match.group('build'))
321 downloader.Downloader.TouchTimestampForStaged(build_dir)
322 return UpdateTimestampHandler
323
324
Chris Sosa7c931362010-10-11 19:49:01 -0700325def _GetConfig(options):
326 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800327
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800328 socket_host = '::'
Yu-Ju Hongc8d4af32013-11-12 15:14:26 -0800329 # Fall back to IPv4 when python is not configured with IPv6.
330 if not socket.has_ipv6:
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800331 socket_host = '0.0.0.0'
332
Simran Basief83d6a2014-08-28 14:32:01 -0700333 # Adds the UpdateTimestampHandler to cherrypy's tools. This tools executes
334 # on the on_end_resource hook. This hook is called once processing is
335 # complete and the response is ready to be returned.
336 cherrypy.tools.update_timestamp = cherrypy.Tool(
337 'on_end_resource', _GetUpdateTimestampHandler(options.static_dir))
338
David Riley2fcb0122017-11-02 11:25:39 -0700339 base_config = {
340 'global': {
341 'server.log_request_headers': True,
342 'server.protocol_version': 'HTTP/1.1',
343 'server.socket_host': socket_host,
344 'server.socket_port': int(options.port),
345 'response.timeout': 6000,
346 'request.show_tracebacks': True,
347 'server.socket_timeout': 60,
348 'server.thread_pool': 2,
349 'engine.autoreload.on': False,
350 },
David Riley2fcb0122017-11-02 11:25:39 -0700351 '/build': {
352 'response.timeout': 100000,
353 },
354 '/update': {
355 # Gets rid of cherrypy parsing post file for args.
356 'request.process_request_body': False,
357 'response.timeout': 10000,
358 },
359 # Sets up the static dir for file hosting.
360 '/static': {
361 'tools.staticdir.dir': options.static_dir,
362 'tools.staticdir.on': True,
363 'response.timeout': 10000,
364 'tools.update_timestamp.on': True,
365 },
366 }
Chris Sosa5f118ef2012-07-12 11:37:50 -0700367 if options.production:
Alex Miller93beca52013-07-30 19:25:09 -0700368 base_config['global'].update({'server.thread_pool': 150})
Scott Zawalski1c5e7cd2012-02-27 13:12:52 -0500369
Chris Sosa7c931362010-10-11 19:49:01 -0700370 return base_config
rtc@google.com64244662009-11-12 00:52:08 +0000371
Darin Petkove17164a2010-08-11 13:24:41 -0700372
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700373def _GetRecursiveMemberObject(root, member_list):
374 """Returns an object corresponding to a nested member list.
375
376 Args:
377 root: the root object to search
378 member_list: list of nested members to search
Don Garrettf84631a2014-01-07 18:21:26 -0800379
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700380 Returns:
381 An object corresponding to the member name list; None otherwise.
382 """
383 for member in member_list:
384 next_root = root.__class__.__dict__.get(member)
385 if not next_root:
386 return None
387 root = next_root
388 return root
389
390
391def _IsExposed(name):
392 """Returns True iff |name| has an `exposed' attribute and it is set."""
393 return hasattr(name, 'exposed') and name.exposed
394
395
Congbin Guo6bc32182019-08-20 17:54:30 -0700396def _GetExposedMethod(nested_member):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700397 """Returns a CherryPy-exposed method, if such exists.
398
399 Args:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700400 nested_member: a slash-joined path to the nested member
Don Garrettf84631a2014-01-07 18:21:26 -0800401
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700402 Returns:
Congbin Guo6bc32182019-08-20 17:54:30 -0700403 A function object corresponding to the path defined by |nested_member| from
404 the app root object registered, if the function is exposed; None otherwise.
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700405 """
Congbin Guo6bc32182019-08-20 17:54:30 -0700406 for app in cherrypy.tree.apps.values():
407 # Use the 'index' function doc as the doc of the app.
408 if nested_member == app.script_name.lstrip('/'):
409 nested_member = 'index'
410
411 method = _GetRecursiveMemberObject(app.root, nested_member.split('/'))
412 if method and isinstance(method, types.FunctionType) and _IsExposed(method):
413 return method
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700414
415
Gilad Arnold748c8322012-10-12 09:51:35 -0700416def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700417 """Finds exposed CherryPy methods.
418
419 Args:
420 root: the root object for searching
421 prefix: slash-joined chain of members leading to current object
422 unlisted: URLs to be excluded regardless of their exposed status
Don Garrettf84631a2014-01-07 18:21:26 -0800423
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700424 Returns:
425 List of exposed URLs that are not unlisted.
426 """
427 method_list = []
Congbin Guo6bc32182019-08-20 17:54:30 -0700428 for member in root.__class__.__dict__.keys():
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700429 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 09:51:35 -0700430 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700431 continue
432 member_obj = root.__class__.__dict__[member]
433 if _IsExposed(member_obj):
Amin Hassani08e42d22019-06-03 00:31:30 -0700434 if isinstance(member_obj, types.FunctionType):
Congbin Guo6bc32182019-08-20 17:54:30 -0700435 # Regard the app name as exposed "method" name if it exposed 'index'
436 # function.
437 if prefix and member == 'index':
438 method_list.append(prefix)
439 else:
440 method_list.append(prefixed_member)
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700441 else:
442 method_list += _FindExposedMethods(
443 member_obj, prefixed_member, unlisted)
444 return method_list
445
446
xixuan52c2fba2016-05-20 17:02:48 -0700447def _check_base_args_for_auto_update(kwargs):
xixuanac89ce82016-11-30 16:48:20 -0800448 """Check basic args required for auto-update.
449
450 Args:
451 kwargs: the parameters to be checked.
452
453 Raises:
454 DevServerHTTPError if required parameters don't exist in kwargs.
455 """
xixuan52c2fba2016-05-20 17:02:48 -0700456 if 'host_name' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -0800457 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
458 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -0700459
460 if 'build_name' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -0800461 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
462 KEY_ERROR_MSG % 'build_name')
xixuan52c2fba2016-05-20 17:02:48 -0700463
464
465def _parse_boolean_arg(kwargs, key):
xixuanac89ce82016-11-30 16:48:20 -0800466 """Parse boolean arg from kwargs.
467
468 Args:
469 kwargs: the parameters to be checked.
470 key: the key to be parsed.
471
472 Returns:
473 The boolean value of kwargs[key], or False if key doesn't exist in kwargs.
474
475 Raises:
476 DevServerHTTPError if kwargs[key] is not a boolean variable.
477 """
xixuan52c2fba2016-05-20 17:02:48 -0700478 if key in kwargs:
479 if kwargs[key] == 'True':
480 return True
481 elif kwargs[key] == 'False':
482 return False
483 else:
Amin Hassani722e0962019-11-15 15:45:31 -0800484 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
485 'The value for key %s is not boolean.' % key)
xixuan52c2fba2016-05-20 17:02:48 -0700486 else:
487 return False
488
xixuan447ad9d2017-02-28 14:46:20 -0800489
xixuanac89ce82016-11-30 16:48:20 -0800490def _parse_string_arg(kwargs, key):
491 """Parse string arg from kwargs.
492
493 Args:
494 kwargs: the parameters to be checked.
495 key: the key to be parsed.
496
497 Returns:
498 The string value of kwargs[key], or None if key doesn't exist in kwargs.
499 """
500 if key in kwargs:
501 return kwargs[key]
502 else:
503 return None
504
xixuan447ad9d2017-02-28 14:46:20 -0800505
xixuanac89ce82016-11-30 16:48:20 -0800506def _build_uri_from_build_name(build_name):
507 """Get build url from a given build name.
508
509 Args:
510 build_name: the build name to be parsed, whose format is
511 'board/release_version'.
512
513 Returns:
514 The release_archive_url on Google Storage for this build name.
515 """
Amin Hassani08e42d22019-06-03 00:31:30 -0700516 # TODO(ahassani): This function doesn't seem to be used anywhere since its
517 # previous use of lib.paygen.gspath was broken and it doesn't seem to be
518 # causing any runtime issues. So deprecate this in the future.
519 tokens = build_name.split('/')
520 return 'gs://chromeos-releases/stable-channel/%s/%s' % (tokens[0], tokens[1])
xixuan52c2fba2016-05-20 17:02:48 -0700521
xixuan447ad9d2017-02-28 14:46:20 -0800522
523def _clear_process(host_name, pid):
524 """Clear AU process for given hostname and pid.
525
526 This clear includes:
527 1. kill process if it's alive.
528 2. delete the track status file of this process.
529 3. delete the executing log file of this process.
530
531 Args:
532 host_name: the host to execute auto-update.
533 pid: the background auto-update process id.
534 """
535 if cros_update_progress.IsProcessAlive(pid):
536 os.killpg(int(pid), signal.SIGKILL)
537
538 cros_update_progress.DelTrackStatusFile(host_name, pid)
539 cros_update_progress.DelExecuteLogFile(host_name, pid)
540
541
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -0800542def is_deprecated_server():
543 """Gets whether the devserver has deprecated RPCs."""
544 return cherrypy.config.get('infra_removal', False)
545
546
David Rochberg7c79a812011-01-19 14:24:45 -0500547class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700548 """The Root Class for the Dev Server.
549
550 CherryPy works as follows:
551 For each method in this class, cherrpy interprets root/path
552 as a call to an instance of DevServerRoot->method_name. For example,
553 a call to http://myhost/build will call build. CherryPy automatically
554 parses http args and places them as keyword arguments in each method.
555 For paths http://myhost/update/dir1/dir2, you can use *args so that
556 cherrypy uses the update method and puts the extra paths in args.
557 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700558 # Method names that should not be listed on the index page.
559 _UNLISTED_METHODS = ['index', 'doc']
560
Dan Shi59ae7092013-06-04 14:37:27 -0700561 # Number of threads that devserver is staging images.
562 _staging_thread_count = 0
563 # Lock used to lock increasing/decreasing count.
564 _staging_thread_count_lock = threading.Lock()
565
joychen3cb228e2013-06-12 12:13:13 -0700566 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700567 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800568 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 12:13:13 -0700569 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 14:24:45 -0500570
Congbin Guo3afae6c2019-08-13 16:29:42 -0700571 @property
572 def staging_thread_count(self):
573 """Get the staging thread count."""
574 return self._staging_thread_count
Dan Shiafd0e492015-05-27 14:23:51 -0700575
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700576 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500577 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700578 """Builds the package specified."""
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -0800579 if is_deprecated_server():
580 raise DeprecatedRPCError('build')
581
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700582 import builder
583 if self._builder is None:
584 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500585 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700586
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700587 @cherrypy.expose
Dan Shif8eb0d12013-08-01 17:52:06 -0700588 def is_staged(self, **kwargs):
589 """Check if artifacts have been downloaded.
590
Congbin Guo3afae6c2019-08-13 16:29:42 -0700591 Examples:
592 To check if autotest and test_suites are staged:
593 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
594 artifacts=autotest,test_suites
595
Amin Hassani08e42d22019-06-03 00:31:30 -0700596 Args:
Chris Sosa6b0c6172013-08-05 17:01:33 -0700597 async: True to return without waiting for download to complete.
598 artifacts: Comma separated list of named artifacts to download.
599 These are defined in artifact_info and have their implementation
600 in build_artifact.py.
601 files: Comma separated list of file artifacts to stage. These
602 will be available as is in the corresponding static directory with no
603 custom post-processing.
604
Congbin Guo3afae6c2019-08-13 16:29:42 -0700605 Returns:
606 True of all artifacts are staged.
Dan Shif8eb0d12013-08-01 17:52:06 -0700607 """
Gabe Black3b567202015-09-23 14:07:59 -0700608 dl, factory = _get_downloader_and_factory(kwargs)
Aviv Keshet57d18172016-06-18 20:39:09 -0700609 response = str(dl.IsStaged(factory))
610 _Log('Responding to is_staged %s request with %r', kwargs, response)
611 return response
Dan Shi59ae7092013-06-04 14:37:27 -0700612
Chris Sosa76e44b92013-01-31 12:11:38 -0800613 @cherrypy.expose
Prashanth Ba06d2d22014-03-07 15:35:19 -0800614 def list_image_dir(self, **kwargs):
615 """Take an archive url and list the contents in its staged directory.
616
Amin Hassani08e42d22019-06-03 00:31:30 -0700617 Examples:
Prashanth Ba06d2d22014-03-07 15:35:19 -0800618 To list the contents of where this devserver should have staged
619 gs://image-archive/<board>-release/<build> call:
620 http://devserver_url:<port>/list_image_dir?archive_url=<gs://..>
621
Congbin Guo3afae6c2019-08-13 16:29:42 -0700622 Args:
623 archive_url: Google Storage URL for the build.
624
Prashanth Ba06d2d22014-03-07 15:35:19 -0800625 Returns:
626 A string with information about the contents of the image directory.
627 """
Gabe Black3b567202015-09-23 14:07:59 -0700628 dl = _get_downloader(kwargs)
Prashanth Ba06d2d22014-03-07 15:35:19 -0800629 try:
Gabe Black3b567202015-09-23 14:07:59 -0700630 image_dir_contents = dl.ListBuildDir()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800631 except build_artifact.ArtifactDownloadError as e:
632 return 'Cannot list the contents of staged artifacts. %s' % e
633 if not image_dir_contents:
Gabe Black3b567202015-09-23 14:07:59 -0700634 return '%s has not been staged on this devserver.' % dl.DescribeSource()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800635 return image_dir_contents
636
637 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800638 def stage(self, **kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700639 """Downloads and caches build artifacts.
Chris Sosa76e44b92013-01-31 12:11:38 -0800640
Gabe Black3b567202015-09-23 14:07:59 -0700641 Downloads and caches build artifacts, possibly from a Google Storage URL,
Dan Shi72b16132015-10-08 12:10:33 -0700642 or from Android's build server. Returns once these have been downloaded
Gabe Black3b567202015-09-23 14:07:59 -0700643 on the devserver. A call to this will attempt to cache non-specified
644 artifacts in the background for the given from the given URL following
645 the principle of spatial locality. Spatial locality of different
Chris Sosa76e44b92013-01-31 12:11:38 -0800646 artifacts is explicitly defined in the build_artifact module.
647
648 These artifacts will then be available from the static/ sub-directory of
649 the devserver.
650
Amin Hassani08e42d22019-06-03 00:31:30 -0700651 Examples:
Chris Sosa76e44b92013-01-31 12:11:38 -0800652 To download the autotest and test suites tarballs:
653 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
654 artifacts=autotest,test_suites
655 To download the full update payload:
656 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
657 artifacts=full_payload
Chris Sosa6b0c6172013-08-05 17:01:33 -0700658 To download just a file called blah.bin:
659 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
660 files=blah.bin
Chris Sosa76e44b92013-01-31 12:11:38 -0800661
662 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 16:39:34 -0700663 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 12:11:38 -0800664
665 Note for this example, relative path is the archive_url stripped of its
666 basename i.e. path/ in the examples above. Specific example:
667
668 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
669
670 Will get staged to:
671
joychened64b222013-06-21 16:39:34 -0700672 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Congbin Guo3afae6c2019-08-13 16:29:42 -0700673
674 Args:
675 archive_url: Google Storage URL for the build.
676 local_path: Local path for the build.
677 delete_source: Only meaningful with local_path. bool to indicate if the
678 source files should be deleted. This is especially useful when staging
679 a file locally in resource constrained environments as it allows us to
680 move the relevant files locally instead of copying them.
681 async: True to return without waiting for download to complete.
682 artifacts: Comma separated list of named artifacts to download.
683 These are defined in artifact_info and have their implementation
684 in build_artifact.py.
685 files: Comma separated list of files to stage. These
686 will be available as is in the corresponding static directory with no
687 custom post-processing.
688 clean: True to remove any previously staged artifacts first.
Chris Sosa76e44b92013-01-31 12:11:38 -0800689 """
Gabe Black3b567202015-09-23 14:07:59 -0700690 dl, factory = _get_downloader_and_factory(kwargs)
691
Dan Shi59ae7092013-06-04 14:37:27 -0700692 with DevServerRoot._staging_thread_count_lock:
693 DevServerRoot._staging_thread_count += 1
694 try:
Laurence Goodbyf5c958d2016-01-14 18:23:56 -0800695 boolean_string = kwargs.get('clean')
696 clean = xbuddy.XBuddy.ParseBoolean(boolean_string)
697 if clean and os.path.exists(dl.GetBuildDir()):
698 _Log('Removing %s' % dl.GetBuildDir())
699 shutil.rmtree(dl.GetBuildDir())
Achuith Bhandarkar2a1fcd82019-10-17 17:45:58 -0700700 is_async = kwargs.get('async', False)
701 dl.Download(factory, is_async=is_async)
Dan Shi59ae7092013-06-04 14:37:27 -0700702 finally:
703 with DevServerRoot._staging_thread_count_lock:
704 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800705 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700706
707 @cherrypy.expose
xixuan52c2fba2016-05-20 17:02:48 -0700708 def cros_au(self, **kwargs):
709 """Auto-update a CrOS DUT.
710
711 Args:
712 kwargs:
713 host_name: the hostname of the DUT to auto-update.
714 build_name: the build name for update the DUT.
715 force_update: Force an update even if the version installed is the
716 same. Default: False.
717 full_update: If True, do not run stateful update, directly force a full
718 reimage. If False, try stateful update first if the dut is already
719 installed with the same version.
720 async: Whether the auto_update function is ran in the background.
David Rileyee75de22017-11-02 10:48:15 -0700721 quick_provision: Whether the quick provision path is attempted first.
xixuan52c2fba2016-05-20 17:02:48 -0700722
723 Returns:
724 A tuple includes two elements:
725 a boolean variable represents whether the auto-update process is
726 successfully started.
727 an integer represents the background auto-update process id.
728 """
729 _check_base_args_for_auto_update(kwargs)
730
731 host_name = kwargs['host_name']
732 build_name = kwargs['build_name']
733 force_update = _parse_boolean_arg(kwargs, 'force_update')
734 full_update = _parse_boolean_arg(kwargs, 'full_update')
Achuith Bhandarkar2a1fcd82019-10-17 17:45:58 -0700735 is_async = _parse_boolean_arg(kwargs, 'async')
xixuanac89ce82016-11-30 16:48:20 -0800736 original_build = _parse_string_arg(kwargs, 'original_build')
David Haddock90e49442017-04-07 19:14:09 -0700737 payload_filename = _parse_string_arg(kwargs, 'payload_filename')
David Haddock20559612017-06-28 22:15:08 -0700738 clobber_stateful = _parse_boolean_arg(kwargs, 'clobber_stateful')
David Rileyee75de22017-11-02 10:48:15 -0700739 quick_provision = _parse_boolean_arg(kwargs, 'quick_provision')
740
741 devserver_url = updater.GetDevserverUrl()
742 static_url = updater.GetStaticUrl()
xixuan52c2fba2016-05-20 17:02:48 -0700743
Mike Frysingera777cfc2020-03-02 17:20:46 -0500744 # Command of running auto-update.
745 cmd = ['cros_update', '--hostname', host_name, '-b', build_name,
746 '--static_dir', updater.static_dir]
747
748 # The original_build's format is like: link/3428.210.0
749 # The corresponding release_archive_url's format is like:
750 # gs://chromeos-releases/stable-channel/link/3428.210.0
751 if original_build:
752 release_archive_url = _build_uri_from_build_name(original_build)
753 # First staging the stateful.tgz synchronousely.
754 self.stage(files='stateful.tgz', is_async=False,
755 archive_url=release_archive_url)
756 cmd += ['--original_build', original_build]
757
758 if force_update:
759 cmd += ['--force_update']
760
761 if full_update:
762 cmd += ['--full_update']
763
764 if payload_filename:
765 cmd += ['--payload_filename', payload_filename]
766
767 if clobber_stateful:
768 cmd += ['--clobber_stateful']
769
770 if quick_provision:
771 cmd += ['--quick_provision']
772
773 if devserver_url:
774 cmd += ['--devserver_url', devserver_url]
775
776 if static_url:
777 cmd += ['--static_url', static_url]
778
Achuith Bhandarkar2a1fcd82019-10-17 17:45:58 -0700779 if is_async:
Amin Hassani78520ae2019-10-29 13:26:51 -0700780 p = subprocess.Popen(cmd, preexec_fn=os.setsid)
xixuan2a0970a2016-08-10 12:12:44 -0700781 pid = os.getpgid(p.pid)
xixuan52c2fba2016-05-20 17:02:48 -0700782
783 # Pre-write status in the track_status_file before the first call of
784 # 'get_au_status' to make sure that the track_status_file exists.
xixuan2a0970a2016-08-10 12:12:44 -0700785 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -0700786 progress_tracker.WriteStatus('CrOS update is just started.')
787
xixuan2a0970a2016-08-10 12:12:44 -0700788 return json.dumps((True, pid))
xixuan52c2fba2016-05-20 17:02:48 -0700789 else:
Mike Frysingera777cfc2020-03-02 17:20:46 -0500790 subprocess.check_call(cmd)
xixuan27d50442017-08-09 10:38:25 -0700791 return json.dumps((True, -1))
xixuan52c2fba2016-05-20 17:02:48 -0700792
793 @cherrypy.expose
794 def get_au_status(self, **kwargs):
795 """Check if the auto-update task is finished.
796
797 It handles 4 cases:
798 1. If an error exists in the track_status_file, delete the track file and
799 raise it.
800 2. If cros-update process is finished, delete the file and return the
801 success result.
802 3. If the process is not running, delete the track file and raise an error
803 about 'the process is terminated due to unknown reason'.
804 4. If the track_status_file does not exist, kill the process if it exists,
805 and raise the IOError.
806
807 Args:
808 kwargs:
809 host_name: the hostname of the DUT to auto-update.
810 pid: the background process id of cros-update.
811
812 Returns:
xixuan28d99072016-10-06 12:24:16 -0700813 A dict with three elements:
xixuan52c2fba2016-05-20 17:02:48 -0700814 a boolean variable represents whether the auto-update process is
815 finished.
816 a string represents the current auto-update process status.
817 For example, 'Transfer Devserver/Stateful Update Package'.
xixuan28d99072016-10-06 12:24:16 -0700818 a detailed error message paragraph if there exists an Auto-Update
819 error, in which the last line shows the main exception. Empty
820 string otherwise.
xixuan52c2fba2016-05-20 17:02:48 -0700821 """
822 if 'host_name' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -0800823 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
824 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -0700825
826 if 'pid' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -0800827 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
828 KEY_ERROR_MSG % 'pid')
xixuan52c2fba2016-05-20 17:02:48 -0700829
830 host_name = kwargs['host_name']
831 pid = kwargs['pid']
832 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
833
xixuan28d99072016-10-06 12:24:16 -0700834 result_dict = {'finished': False, 'status': '', 'detailed_error_msg': ''}
xixuan52c2fba2016-05-20 17:02:48 -0700835 try:
836 result = progress_tracker.ReadStatus()
837 if result.startswith(cros_update_progress.ERROR_TAG):
xixuan28d99072016-10-06 12:24:16 -0700838 result_dict['detailed_error_msg'] = result[len(
839 cros_update_progress.ERROR_TAG):]
xixuan28681fd2016-11-23 11:13:56 -0800840 elif result == cros_update_progress.FINISHED:
xixuan28d99072016-10-06 12:24:16 -0700841 result_dict['finished'] = True
842 result_dict['status'] = result
xixuan28681fd2016-11-23 11:13:56 -0800843 elif not cros_update_progress.IsProcessAlive(pid):
xixuan28d99072016-10-06 12:24:16 -0700844 result_dict['detailed_error_msg'] = (
845 'Cros_update process terminated midway due to unknown reason. '
846 'Last update status was %s' % result)
xixuan28681fd2016-11-23 11:13:56 -0800847 else:
848 result_dict['status'] = result
849 except IOError as e:
850 if pid and cros_update_progress.IsProcessAlive(pid):
xixuan2a0970a2016-08-10 12:12:44 -0700851 os.killpg(int(pid), signal.SIGKILL)
xixuan52c2fba2016-05-20 17:02:48 -0700852
xixuan28681fd2016-11-23 11:13:56 -0800853 result_dict['detailed_error_msg'] = str(e)
854
855 return json.dumps(result_dict)
xixuan52c2fba2016-05-20 17:02:48 -0700856
857 @cherrypy.expose
David Riley6d5fca02017-10-31 10:35:47 -0700858 def post_au_status(self, status, **kwargs):
859 """Updates the status of an auto-update task.
860
861 Callers will need to POST to this URL with a body of MIME-type
862 "multipart/form-data".
863 The body should include a single argument, 'status', containing the
864 AU status to record.
865
866 Args:
867 status: The updated status.
868 kwargs:
869 host_name: the hostname of the DUT to auto-update.
870 pid: the background process id of cros-update.
871 """
872 if 'host_name' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -0800873 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
874 KEY_ERROR_MSG % 'host_name')
David Riley6d5fca02017-10-31 10:35:47 -0700875
876 if 'pid' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -0800877 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
878 KEY_ERROR_MSG % 'pid')
David Riley6d5fca02017-10-31 10:35:47 -0700879
880 host_name = kwargs['host_name']
881 pid = kwargs['pid']
David Riley3cea2582017-11-24 22:03:01 -0800882 status = status.rstrip()
883 _Log('Recording status for %s (%s): %s' % (host_name, pid, status))
David Riley6d5fca02017-10-31 10:35:47 -0700884 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
885
David Riley3cea2582017-11-24 22:03:01 -0800886 progress_tracker.WriteStatus(status)
David Riley6d5fca02017-10-31 10:35:47 -0700887
888 return 'True'
889
890 @cherrypy.expose
xixuan52c2fba2016-05-20 17:02:48 -0700891 def handler_cleanup(self, **kwargs):
xixuan3bc974e2016-10-18 17:21:43 -0700892 """Clean track status log and temp directory for CrOS auto-update process.
xixuan52c2fba2016-05-20 17:02:48 -0700893
894 Args:
895 kwargs:
896 host_name: the hostname of the DUT to auto-update.
897 pid: the background process id of cros-update.
898 """
899 if 'host_name' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -0800900 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
901 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -0700902
903 if 'pid' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -0800904 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
905 KEY_ERROR_MSG % 'pid')
xixuan52c2fba2016-05-20 17:02:48 -0700906
907 host_name = kwargs['host_name']
908 pid = kwargs['pid']
909 cros_update_progress.DelTrackStatusFile(host_name, pid)
xixuan3bc974e2016-10-18 17:21:43 -0700910 cros_update_progress.DelAUTempDirectory(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -0700911
912 @cherrypy.expose
913 def kill_au_proc(self, **kwargs):
914 """Kill CrOS auto-update process using given process id.
915
916 Args:
917 kwargs:
918 host_name: Kill all the CrOS auto-update process of this host.
919
920 Returns:
921 True if all processes are killed properly.
922 """
923 if 'host_name' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -0800924 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
925 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -0700926
xixuan447ad9d2017-02-28 14:46:20 -0800927 cur_pid = kwargs.get('pid')
928
xixuan52c2fba2016-05-20 17:02:48 -0700929 host_name = kwargs['host_name']
xixuan3bc974e2016-10-18 17:21:43 -0700930 track_log_list = cros_update_progress.GetAllTrackStatusFileByHostName(
931 host_name)
xixuan52c2fba2016-05-20 17:02:48 -0700932 for log in track_log_list:
933 # The track log's full path is: path/host_name_pid.log
934 # Use splitext to remove file extension, then parse pid from the
935 # filename.
Congbin Guo3afae6c2019-08-13 16:29:42 -0700936 pid = os.path.splitext(os.path.basename(log))[0][len(host_name) + 1:]
xixuan447ad9d2017-02-28 14:46:20 -0800937 _clear_process(host_name, pid)
xixuan52c2fba2016-05-20 17:02:48 -0700938
xixuan447ad9d2017-02-28 14:46:20 -0800939 if cur_pid:
940 _clear_process(host_name, cur_pid)
xixuan52c2fba2016-05-20 17:02:48 -0700941
942 return 'True'
943
944 @cherrypy.expose
945 def collect_cros_au_log(self, **kwargs):
946 """Collect CrOS auto-update log.
947
948 Args:
949 kwargs:
950 host_name: the hostname of the DUT to auto-update.
951 pid: the background process id of cros-update.
952
953 Returns:
David Haddock9f459632017-05-11 14:45:46 -0700954 A dictionary containing the execute log file and any hostlog files.
xixuan52c2fba2016-05-20 17:02:48 -0700955 """
956 if 'host_name' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -0800957 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
958 KEY_ERROR_MSG % 'host_name')
xixuan52c2fba2016-05-20 17:02:48 -0700959
960 if 'pid' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -0800961 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
962 KEY_ERROR_MSG % 'pid')
xixuan52c2fba2016-05-20 17:02:48 -0700963
964 host_name = kwargs['host_name']
965 pid = kwargs['pid']
xixuan3bc974e2016-10-18 17:21:43 -0700966
967 # Fetch the execute log recorded by cros_update_progress.
xixuan1bbfaba2016-10-13 17:53:22 -0700968 au_log = cros_update_progress.ReadExecuteLogFile(host_name, pid)
969 cros_update_progress.DelExecuteLogFile(host_name, pid)
David Haddock9f459632017-05-11 14:45:46 -0700970 # Fetch the cros_au host_logs if they exist
971 au_hostlogs = cros_update_progress.ReadAUHostLogFiles(host_name, pid)
972 return json.dumps({'cros_au_log': au_log, 'host_logs': au_hostlogs})
xixuan1bbfaba2016-10-13 17:53:22 -0700973
xixuan52c2fba2016-05-20 17:02:48 -0700974 @cherrypy.expose
Dan Shi2f136862016-02-11 15:38:38 -0800975 def locate_file(self, **kwargs):
976 """Get the path to the given file name.
977
978 This method looks up the given file name inside specified build artifacts.
979 One use case is to help caller to locate an apk file inside a build
980 artifact. The location of the apk file could be different based on the
981 branch and target.
982
983 Args:
984 file_name: Name of the file to look for.
985 artifacts: A list of artifact names to search for the file.
986
987 Returns:
988 Path to the file with the given name. It's relative to the folder for the
989 build, e.g., DATA/priv-app/sl4a/sl4a.apk
Dan Shi2f136862016-02-11 15:38:38 -0800990 """
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -0800991 if is_deprecated_server():
992 raise DeprecatedRPCError('locate_file')
993
Dan Shi2f136862016-02-11 15:38:38 -0800994 dl, _ = _get_downloader_and_factory(kwargs)
995 try:
Joe Brennan1691f8e2017-03-15 15:53:36 -0700996 file_name = kwargs['file_name']
Dan Shi2f136862016-02-11 15:38:38 -0800997 artifacts = kwargs['artifacts']
998 except KeyError:
Amin Hassanid4e35392019-10-03 11:02:44 -0700999 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -07001000 '`file_name` and `artifacts` are required to search '
1001 'for a file in build artifacts.')
Dan Shi2f136862016-02-11 15:38:38 -08001002 build_path = dl.GetBuildDir()
1003 for artifact in artifacts:
1004 # Get the unzipped folder of the artifact. If it's not defined in
1005 # ARTIFACT_UNZIP_FOLDER_MAP, assume the files are unzipped to the build
1006 # directory directly.
1007 folder = artifact_info.ARTIFACT_UNZIP_FOLDER_MAP.get(artifact, '')
1008 artifact_path = os.path.join(build_path, folder)
1009 for root, _, filenames in os.walk(artifact_path):
Joe Brennan1691f8e2017-03-15 15:53:36 -07001010 if file_name in set([f for f in filenames]):
Dan Shi2f136862016-02-11 15:38:38 -08001011 return os.path.relpath(os.path.join(root, file_name), build_path)
Amin Hassanid4e35392019-10-03 11:02:44 -07001012 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -07001013 'File `%s` can not be found in artifacts: %s' % (file_name, artifacts))
Dan Shi2f136862016-02-11 15:38:38 -08001014
1015 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -08001016 def setup_telemetry(self, **kwargs):
1017 """Extracts and sets up telemetry
1018
1019 This method goes through the telemetry deps packages, and stages them on
1020 the devserver to be used by the drones and the telemetry tests.
1021
1022 Args:
1023 archive_url: Google Storage URL for the build.
1024
1025 Returns:
1026 Path to the source folder for the telemetry codebase once it is staged.
1027 """
Gabe Black3b567202015-09-23 14:07:59 -07001028 dl = _get_downloader(kwargs)
Simran Basi4baad082013-02-14 13:39:18 -08001029
Gabe Black3b567202015-09-23 14:07:59 -07001030 build_path = dl.GetBuildDir()
Simran Basi4baad082013-02-14 13:39:18 -08001031 deps_path = os.path.join(build_path, 'autotest/packages')
1032 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
1033 src_folder = os.path.join(telemetry_path, 'src')
1034
1035 with self._telemetry_lock_dict.lock(telemetry_path):
1036 if os.path.exists(src_folder):
1037 # Telemetry is already fully stage return
1038 return src_folder
1039
1040 common_util.MkDirP(telemetry_path)
1041
1042 # Copy over the required deps tar balls to the telemetry directory.
1043 for dep in TELEMETRY_DEPS:
1044 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -07001045 if not os.path.exists(dep_path):
1046 # This dep does not exist (could be new), do not extract it.
1047 continue
Simran Basi4baad082013-02-14 13:39:18 -08001048 try:
1049 common_util.ExtractTarball(dep_path, telemetry_path)
1050 except common_util.CommonUtilError as e:
1051 shutil.rmtree(telemetry_path)
Amin Hassanid4e35392019-10-03 11:02:44 -07001052 raise DevServerError(str(e))
Simran Basi4baad082013-02-14 13:39:18 -08001053
1054 # By default all the tarballs extract to test_src but some parts of
1055 # the telemetry code specifically hardcoded to exist inside of 'src'.
1056 test_src = os.path.join(telemetry_path, 'test_src')
1057 try:
1058 shutil.move(test_src, src_folder)
1059 except shutil.Error:
1060 # This can occur if src_folder already exists. Remove and retry move.
1061 shutil.rmtree(src_folder)
Amin Hassanid4e35392019-10-03 11:02:44 -07001062 raise DevServerError(
Gabe Black3b567202015-09-23 14:07:59 -07001063 'Failure in telemetry setup for build %s. Appears that the '
1064 'test_src to src move failed.' % dl.GetBuild())
Simran Basi4baad082013-02-14 13:39:18 -08001065
1066 return src_folder
1067
1068 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -08001069 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -07001070 """Symbolicates a minidump using pre-downloaded symbols, returns it.
1071
1072 Callers will need to POST to this URL with a body of MIME-type
1073 "multipart/form-data".
1074 The body should include a single argument, 'minidump', containing the
1075 binary-formatted minidump to symbolicate.
1076
Chris Masone816e38c2012-05-02 12:22:36 -07001077 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -08001078 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -07001079 minidump: The binary minidump file to symbolicate.
1080 """
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -08001081 if is_deprecated_server():
1082 raise DeprecatedRPCError('symbolicate_dump')
1083
Chris Sosa76e44b92013-01-31 12:11:38 -08001084 # Ensure the symbols have been staged.
Dan Shif08fe492016-10-04 14:39:25 -07001085 # Try debug.tar.xz first, then debug.tgz
1086 for artifact in (artifact_info.SYMBOLS_ONLY, artifact_info.SYMBOLS):
1087 kwargs['artifacts'] = artifact
1088 dl = _get_downloader(kwargs)
1089
1090 try:
1091 if self.stage(**kwargs) == 'Success':
1092 break
1093 except build_artifact.ArtifactDownloadError:
1094 continue
1095 else:
Amin Hassanid4e35392019-10-03 11:02:44 -07001096 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -07001097 'Failed to stage symbols for %s' % dl.DescribeSource())
Chris Sosa76e44b92013-01-31 12:11:38 -08001098
Chris Masone816e38c2012-05-02 12:22:36 -07001099 to_return = ''
1100 with tempfile.NamedTemporaryFile() as local:
1101 while True:
1102 data = minidump.file.read(8192)
1103 if not data:
1104 break
1105 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -08001106
Chris Masone816e38c2012-05-02 12:22:36 -07001107 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -08001108
Gabe Black3b567202015-09-23 14:07:59 -07001109 symbols_directory = os.path.join(dl.GetBuildDir(), 'debug', 'breakpad')
Chris Sosa76e44b92013-01-31 12:11:38 -08001110
xixuanab744382017-04-27 10:41:27 -07001111 # The location of minidump_stackwalk is defined in chromeos-admin.
Chris Sosa76e44b92013-01-31 12:11:38 -08001112 stackwalk = subprocess.Popen(
xixuanab744382017-04-27 10:41:27 -07001113 ['/usr/local/bin/minidump_stackwalk', local.name, symbols_directory],
Chris Sosa76e44b92013-01-31 12:11:38 -08001114 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1115
Chris Masone816e38c2012-05-02 12:22:36 -07001116 to_return, error_text = stackwalk.communicate()
1117 if stackwalk.returncode != 0:
Amin Hassanid4e35392019-10-03 11:02:44 -07001118 raise DevServerError(
Congbin Guo4132a272019-08-20 12:32:14 -07001119 "Can't generate stack trace: %s (rc=%d)" % (error_text,
1120 stackwalk.returncode))
Chris Masone816e38c2012-05-02 12:22:36 -07001121
1122 return to_return
1123
1124 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -08001125 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 15:31:36 -04001126 """Return a string representing the latest build for a given target.
1127
1128 Args:
1129 target: The build target, typically a combination of the board and the
1130 type of build e.g. x86-mario-release.
1131 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
1132 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-07 18:21:26 -08001133
Scott Zawalski16954532012-03-20 15:31:36 -04001134 Returns:
1135 A string representation of the latest build if one exists, i.e.
1136 R19-1993.0.0-a1-b1480.
1137 An empty string if no latest could be found.
1138 """
Don Garrettf84631a2014-01-07 18:21:26 -08001139 if not kwargs:
Scott Zawalski16954532012-03-20 15:31:36 -04001140 return _PrintDocStringAsHTML(self.latestbuild)
1141
Don Garrettf84631a2014-01-07 18:21:26 -08001142 if 'target' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -08001143 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
1144 'Error: target= is required!')
Dan Shi61305df2015-10-26 16:52:35 -07001145
1146 if _is_android_build_request(kwargs):
1147 branch = kwargs.get('branch', None)
1148 target = kwargs.get('target', None)
1149 if not target or not branch:
Amin Hassanid4e35392019-10-03 11:02:44 -07001150 raise DevServerError('Both target and branch must be specified to query'
1151 ' for the latest Android build.')
Dan Shi61305df2015-10-26 16:52:35 -07001152 return android_build.BuildAccessor.GetLatestBuildID(target, branch)
1153
Scott Zawalski16954532012-03-20 15:31:36 -04001154 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001155 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-07 18:21:26 -08001156 updater.static_dir, kwargs['target'],
1157 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -07001158 except common_util.CommonUtilError as errmsg:
Amin Hassani722e0962019-11-15 15:45:31 -08001159 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
1160 str(errmsg))
Scott Zawalski16954532012-03-20 15:31:36 -04001161
1162 @cherrypy.expose
xixuan7efd0002016-04-14 15:34:01 -07001163 def list_suite_controls(self, **kwargs):
1164 """Return a list of contents of all known control files.
1165
1166 Example URL:
1167 To List all control files' content:
1168 http://dev-server/list_suite_controls?suite_name=bvt&
1169 build=daisy_spring-release/R29-4279.0.0
1170
1171 Args:
1172 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
1173 suite_name: List the control files belonging to that suite.
1174
1175 Returns:
Dan Shia1cd6522016-04-18 16:07:21 -07001176 A dictionary of all control files's path to its content for given suite.
xixuan7efd0002016-04-14 15:34:01 -07001177 """
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -08001178 if is_deprecated_server():
1179 raise DeprecatedRPCError('list_suite_controls')
1180
xixuan7efd0002016-04-14 15:34:01 -07001181 if not kwargs:
1182 return _PrintDocStringAsHTML(self.controlfiles)
1183
1184 if 'build' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -08001185 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
1186 'Error: build= is required!')
xixuan7efd0002016-04-14 15:34:01 -07001187
1188 if 'suite_name' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -08001189 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
1190 'Error: suite_name= is required!')
xixuan7efd0002016-04-14 15:34:01 -07001191
1192 control_file_list = [
1193 line.rstrip() for line in common_util.GetControlFileListForSuite(
1194 updater.static_dir, kwargs['build'],
1195 kwargs['suite_name']).splitlines()]
1196
Dan Shia1cd6522016-04-18 16:07:21 -07001197 control_file_content_dict = {}
xixuan7efd0002016-04-14 15:34:01 -07001198 for control_path in control_file_list:
Dan Shia1cd6522016-04-18 16:07:21 -07001199 control_file_content_dict[control_path] = (common_util.GetControlFile(
xixuan7efd0002016-04-14 15:34:01 -07001200 updater.static_dir, kwargs['build'], control_path))
1201
Dan Shia1cd6522016-04-18 16:07:21 -07001202 return json.dumps(control_file_content_dict)
xixuan7efd0002016-04-14 15:34:01 -07001203
1204 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -08001205 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 17:17:28 -05001206 """Return a control file or a list of all known control files.
1207
1208 Example URL:
1209 To List all control files:
beepsbd337242013-07-09 22:44:06 -07001210 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
1211 To List all control files for, say, the bvt suite:
1212 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -05001213 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -05001214 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 -05001215
1216 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -05001217 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -05001218 control_path: If you want the contents of a control file set this
1219 to the path. E.g. client/site_tests/sleeptest/control
1220 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -07001221 suite_name: If control_path is not specified but a suite_name is
1222 specified, list the control files belonging to that suite instead of
1223 all control files. The empty string for suite_name will list all control
1224 files for the build.
Don Garrettf84631a2014-01-07 18:21:26 -08001225
Scott Zawalski4647ce62012-01-03 17:17:28 -05001226 Returns:
1227 Contents of a control file if control_path is provided.
1228 A list of control files if no control_path is provided.
1229 """
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -08001230 if is_deprecated_server():
1231 raise DeprecatedRPCError('controlfiles')
1232
Don Garrettf84631a2014-01-07 18:21:26 -08001233 if not kwargs:
Scott Zawalski4647ce62012-01-03 17:17:28 -05001234 return _PrintDocStringAsHTML(self.controlfiles)
1235
Don Garrettf84631a2014-01-07 18:21:26 -08001236 if 'build' not in kwargs:
Amin Hassani722e0962019-11-15 15:45:31 -08001237 raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
1238 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -05001239
Don Garrettf84631a2014-01-07 18:21:26 -08001240 if 'control_path' not in kwargs:
1241 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-09 22:44:06 -07001242 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-07 18:21:26 -08001243 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-09 22:44:06 -07001244 else:
1245 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-07 18:21:26 -08001246 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -05001247 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001248 return common_util.GetControlFile(
Don Garrettf84631a2014-01-07 18:21:26 -08001249 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -08001250
1251 @cherrypy.expose
Simran Basi99e63c02014-05-20 10:39:52 -07001252 def xbuddy_translate(self, *args, **kwargs):
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001253 """Translates an xBuddy path to a real path to artifact if it exists.
1254
1255 Args:
Simran Basi99e63c02014-05-20 10:39:52 -07001256 args: An xbuddy path in the form of {local|remote}/build_id/artifact.
1257 Local searches the devserver's static directory. Remote searches a
1258 Google Storage image archive.
1259
1260 Kwargs:
1261 image_dir: Google Storage image archive to search in if requesting a
1262 remote artifact. If none uses the default bucket.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001263
1264 Returns:
Simran Basi99e63c02014-05-20 10:39:52 -07001265 String in the format of build_id/artifact as stored on the local server
1266 or in Google Storage.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001267 """
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -08001268 if is_deprecated_server():
1269 raise DeprecatedRPCError('xbuddy_translate')
1270
Simran Basi99e63c02014-05-20 10:39:52 -07001271 build_id, filename = self._xbuddy.Translate(
Gabe Black3b567202015-09-23 14:07:59 -07001272 args, image_dir=kwargs.get('image_dir'))
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001273 response = os.path.join(build_id, filename)
1274 _Log('Path translation requested, returning: %s', response)
1275 return response
1276
1277 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -07001278 def xbuddy(self, *args, **kwargs):
1279 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -07001280
1281 Args:
joycheneaf4cfc2013-07-02 08:38:57 -07001282 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -07001283 components of the path. The path can be understood as
1284 "{local|remote}/build_id/artifact" where build_id is composed of
1285 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -07001286
joychen121fc9b2013-08-02 14:30:30 -07001287 The first path element is optional, and can be "remote" or "local"
1288 If local (the default), devserver will not attempt to access Google
1289 Storage, and will only search the static directory for the files.
1290 If remote, devserver will try to obtain the artifact off GS if it's
1291 not found locally.
1292 The board is the familiar board name, optionally suffixed.
1293 The version can be the google storage version number, and may also be
1294 any of a number of xBuddy defined version aliases that will be
1295 translated into the latest built image that fits the description.
1296 Defaults to latest.
1297 The artifact is one of a number of image or artifact aliases used by
1298 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -07001299
1300 Kwargs:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001301 for_update: {true|false}
Amin Hassanie9ffb862019-09-25 17:10:40 -07001302 if true, prepares the update payloads for the image,
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001303 and returns the update uri to pass to the
1304 update_engine_client.
joychen3cb228e2013-06-12 12:13:13 -07001305 return_dir: {true|false}
1306 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001307 relative_path: {true|false}
1308 if set to true, returns the relative path to the payload
1309 directory from static_dir.
joychen3cb228e2013-06-12 12:13:13 -07001310 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -07001311 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -07001312 or
joycheneaf4cfc2013-07-02 08:38:57 -07001313 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -07001314
1315 Returns:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001316 If |for_update|, returns a redirect to the image or update file
1317 on the devserver. E.g.,
1318 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
1319 chromium-test-image.bin
1320 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
1321 http://host:port/static/x86-generic-release/R26-4000.0.0/
1322 If |relative_path| is true, return a relative path the folder where the
1323 payloads are. E.g.,
1324 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 12:13:13 -07001325 """
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -08001326 if is_deprecated_server():
1327 raise DeprecatedRPCError('xbuddy')
1328
Chris Sosa75490802013-09-30 17:21:45 -07001329 boolean_string = kwargs.get('for_update')
1330 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001331 boolean_string = kwargs.get('return_dir')
1332 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
1333 boolean_string = kwargs.get('relative_path')
1334 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -07001335
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001336 if return_dir and relative_path:
Amin Hassani722e0962019-11-15 15:45:31 -08001337 raise DevServerHTTPError(
Amin Hassanid4e35392019-10-03 11:02:44 -07001338 http_client.INTERNAL_SERVER_ERROR,
Amin Hassani08e42d22019-06-03 00:31:30 -07001339 'Cannot specify both return_dir and relative_path')
Chris Sosa75490802013-09-30 17:21:45 -07001340
1341 # For updates, we optimize downloading of test images.
1342 file_name = None
1343 build_id = None
1344 if for_update:
1345 try:
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001346 build_id = self._xbuddy.StageTestArtifactsForUpdate(args)
Chris Sosa75490802013-09-30 17:21:45 -07001347 except build_artifact.ArtifactDownloadError:
1348 build_id = None
1349
1350 if not build_id:
1351 build_id, file_name = self._xbuddy.Get(args)
1352
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001353 if for_update:
Amin Hassanie9ffb862019-09-25 17:10:40 -07001354 _Log('Payloads requested.')
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001355 # Forces payload to be in cache and symlinked into build_id dir.
Amin Hassanie9ffb862019-09-25 17:10:40 -07001356 updater.GetUpdateForLabel(build_id)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001357
1358 response = None
1359 if return_dir:
1360 response = os.path.join(cherrypy.request.base, 'static', build_id)
1361 _Log('Directory requested, returning: %s', response)
1362 elif relative_path:
1363 response = build_id
1364 _Log('Relative path requested, returning: %s', response)
1365 elif for_update:
1366 response = os.path.join(cherrypy.request.base, 'update', build_id)
1367 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 12:13:13 -07001368 else:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001369 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 14:30:30 -07001370 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001371 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 14:30:30 -07001372 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -07001373
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001374 return response
1375
joychen3cb228e2013-06-12 12:13:13 -07001376 @cherrypy.expose
1377 def xbuddy_list(self):
1378 """Lists the currently available images & time since last access.
1379
Gilad Arnold452fd272014-02-04 11:09:28 -08001380 Returns:
1381 A string representation of a list of tuples [(build_id, time since last
1382 access),...]
joychen3cb228e2013-06-12 12:13:13 -07001383 """
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -08001384 if is_deprecated_server():
1385 raise DeprecatedRPCError('xbuddy')
1386
joychen3cb228e2013-06-12 12:13:13 -07001387 return self._xbuddy.List()
1388
1389 @cherrypy.expose
1390 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 11:09:28 -08001391 """Returns the number of images cached by xBuddy."""
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -08001392 if is_deprecated_server():
1393 raise DeprecatedRPCError('xbuddy_capacity')
1394
joychen3cb228e2013-06-12 12:13:13 -07001395 return self._xbuddy.Capacity()
1396
1397 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001398 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001399 """Presents a welcome message and documentation links."""
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -08001400 if is_deprecated_server():
1401 raise DeprecatedRPCError('index')
1402
Congbin Guo6bc32182019-08-20 17:54:30 -07001403 html_template = (
1404 'Welcome to the Dev Server!<br>\n'
1405 '<br>\n'
1406 'Here are the available methods, click for documentation:<br>\n'
1407 '<br>\n'
1408 '%s')
1409
1410 exposed_methods = []
1411 for app in cherrypy.tree.apps.values():
1412 exposed_methods += _FindExposedMethods(
1413 app.root, app.script_name.lstrip('/'),
1414 unlisted=self._UNLISTED_METHODS)
1415
1416 return html_template % '<br>\n'.join(
1417 ['<a href=doc/%s>%s</a>' % (name, name)
1418 for name in sorted(exposed_methods)])
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001419
1420 @cherrypy.expose
1421 def doc(self, *args):
1422 """Shows the documentation for available methods / URLs.
1423
Amin Hassani08e42d22019-06-03 00:31:30 -07001424 Examples:
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001425 http://myhost/doc/update
1426 """
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -08001427 if is_deprecated_server():
1428 raise DeprecatedRPCError('doc')
1429
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001430 name = '/'.join(args)
Congbin Guo6bc32182019-08-20 17:54:30 -07001431 method = _GetExposedMethod(name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001432 if not method:
Amin Hassanid4e35392019-10-03 11:02:44 -07001433 raise DevServerError("No exposed method named `%s'" % name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001434 if not method.__doc__:
Amin Hassanid4e35392019-10-03 11:02:44 -07001435 raise DevServerError("No documentation for exposed method `%s'" % name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001436 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -07001437
Dale Curtisc9aaf3a2011-08-09 15:47:40 -07001438 @cherrypy.expose
Amin Hassani6eec8792020-01-09 14:06:48 -08001439 def update(self, *args, **kwargs):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001440 """Handles an update check from a Chrome OS client.
1441
1442 The HTTP request should contain the standard Omaha-style XML blob. The URL
1443 line may contain an additional intermediate path to the update payload.
1444
joychen121fc9b2013-08-02 14:30:30 -07001445 This request can be handled in one of 4 ways, depending on the devsever
1446 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -07001447
Amin Hassanie9ffb862019-09-25 17:10:40 -07001448 1. No intermediate path. DEPRECATED
joychen121fc9b2013-08-02 14:30:30 -07001449
1450 2. Path explicitly invokes XBuddy
1451 If there is a path given, it can explicitly invoke xbuddy by prefixing it
1452 with 'xbuddy'. This path is then used to acquire an image binary for the
1453 devserver to generate an update payload from. Devserver then serves this
1454 payload.
1455
1456 3. Path is left for the devserver to interpret.
1457 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
1458 to generate a payload from the test image in that directory and serve it.
1459
joychen121fc9b2013-08-02 14:30:30 -07001460 Examples:
joychen121fc9b2013-08-02 14:30:30 -07001461 2. Explicitly invoke xbuddy
1462 update_engine_client --omaha_url=
1463 http://myhost/update/xbuddy/remote/board/version/dev
1464 This would go to GS to download the dev image for the board, from which
1465 the devserver would generate a payload to serve.
1466
1467 3. Give a path for devserver to interpret
1468 update_engine_client --omaha_url=http://myhost/update/some/random/path
1469 This would attempt, in order to:
1470 a) Generate an update from a test image binary if found in
1471 static_dir/some/random/path.
1472 b) Serve an update payload found in static_dir/some/random/path.
1473 c) Hope that some/random/path takes the form "board/version" and
1474 and attempt to download an update payload for that board/version
1475 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001476 """
joychen121fc9b2013-08-02 14:30:30 -07001477 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -08001478 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -07001479 data = cherrypy.request.rfile.read(body_length)
Amin Hassani6aa075c2020-02-21 18:36:44 +00001480
Amin Hassani6eec8792020-01-09 14:06:48 -08001481 return updater.HandleUpdatePing(data, label, **kwargs)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001482
Dan Shif5ce2de2013-04-25 16:06:32 -07001483
Chris Sosadbc20082012-12-10 13:39:11 -08001484def _CleanCache(cache_dir, wipe):
1485 """Wipes any excess cached items in the cache_dir.
1486
1487 Args:
1488 cache_dir: the directory we are wiping from.
1489 wipe: If True, wipe all the contents -- not just the excess.
1490 """
1491 if wipe:
1492 # Clear the cache and exit on error.
1493 cmd = 'rm -rf %s/*' % cache_dir
1494 if os.system(cmd) != 0:
1495 _Log('Failed to clear the cache with %s' % cmd)
1496 sys.exit(1)
1497 else:
1498 # Clear all but the last N cached updates
1499 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
1500 (cache_dir, CACHED_ENTRIES))
1501 if os.system(cmd) != 0:
1502 _Log('Failed to clean up old delta cache files with %s' % cmd)
1503 sys.exit(1)
1504
1505
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001506def _AddTestingOptions(parser):
1507 group = optparse.OptionGroup(
1508 parser, 'Advanced Testing Options', 'These are used by test scripts and '
1509 'developers writing integration tests utilizing the devserver. They are '
1510 'not intended to be really used outside the scope of someone '
1511 'knowledgable about the test.')
1512 group.add_option('--exit',
1513 action='store_true',
Amin Hassanie9ffb862019-09-25 17:10:40 -07001514 help='do not start the server (yet clear cache)')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001515 group.add_option('--proxy_port',
1516 metavar='PORT', default=None, type='int',
1517 help='port to have the client connect to -- basically the '
1518 'devserver lies to the update to tell it to get the payload '
1519 'from a different port that will proxy the request back to '
1520 'the devserver. The proxy must be managed outside the '
1521 'devserver.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001522 parser.add_option_group(group)
1523
1524
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001525def _AddProductionOptions(parser):
1526 group = optparse.OptionGroup(
1527 parser, 'Advanced Server Options', 'These options can be used to changed '
1528 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001529 group.add_option('--clear_cache',
1530 action='store_true', default=False,
1531 help='At startup, removes all cached entries from the'
Amin Hassanid4e35392019-10-03 11:02:44 -07001532 "devserver's cache.")
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001533 group.add_option('--logfile',
1534 metavar='PATH',
1535 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -07001536 group.add_option('--pidfile',
1537 metavar='PATH',
1538 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 11:04:13 -08001539 group.add_option('--portfile',
1540 metavar='PATH',
1541 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001542 group.add_option('--production',
1543 action='store_true', default=False,
1544 help='have the devserver use production values when '
1545 'starting up. This includes using more threads and '
1546 'performing less logging.')
1547 parser.add_option_group(group)
1548
1549
Paul Hobbsef4e0702016-06-27 17:01:42 -07001550def MakeLogHandler(logfile):
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001551 """Create a LogHandler instance used to log all messages."""
1552 hdlr_cls = handlers.TimedRotatingFileHandler
1553 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
xixuan3d48bff2017-01-30 19:00:09 -08001554 interval=_LOG_ROTATION_INTERVAL,
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001555 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -07001556 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001557 return hdlr
1558
1559
Chris Sosacde6bf42012-05-31 18:36:39 -07001560def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001561 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -08001562 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -07001563
1564 # get directory that the devserver is run from
1565 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -07001566 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -07001567 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001568 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -07001569 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -07001570 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001571 parser.add_option('--port',
1572 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 13:13:28 -08001573 help=('port for the dev server to use; if zero, binds to '
1574 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001575 parser.add_option('-t', '--test_image',
1576 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -07001577 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -07001578 parser.add_option('-x', '--xbuddy_manage_builds',
1579 action='store_true',
1580 default=False,
1581 help='If set, allow xbuddy to manage images in'
1582 'build/images.')
Dan Shi72b16132015-10-08 12:10:33 -07001583 parser.add_option('-a', '--android_build_credential',
1584 default=None,
1585 help='Path to a json file which contains the credential '
1586 'needed to access Android builds.')
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -08001587 parser.add_option('--infra_removal',
1588 action='store_true', default=False,
1589 help='If option is present, some RPCs will be disabled to '
1590 'help with infra removal efforts. See '
1591 'go/devserver-deprecation')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001592 _AddProductionOptions(parser)
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001593 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -07001594 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001595
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001596 # Handle options that must be set globally in cherrypy. Do this
1597 # work up front, because calls to _Log() below depend on this
1598 # initialization.
1599 if options.production:
1600 cherrypy.config.update({'environment': 'production'})
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -08001601 cherrypy.config.update({'infra_removal': options.infra_removal})
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001602 if not options.logfile:
1603 cherrypy.config.update({'log.screen': True})
1604 else:
1605 cherrypy.config.update({'log.error_file': '',
1606 'log.access_file': ''})
Paul Hobbsef4e0702016-06-27 17:01:42 -07001607 hdlr = MakeLogHandler(options.logfile)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001608 # Pylint can't seem to process these two calls properly
1609 # pylint: disable=E1101
1610 cherrypy.log.access_log.addHandler(hdlr)
1611 cherrypy.log.error_log.addHandler(hdlr)
1612 # pylint: enable=E1101
1613
joychened64b222013-06-21 16:39:34 -07001614 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001615 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001616
joychened64b222013-06-21 16:39:34 -07001617 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001618 # If our devserver is only supposed to serve payloads, we shouldn't be
1619 # mucking with the cache at all. If the devserver hadn't previously
1620 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001621 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001622 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001623 else:
1624 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001625
Chris Sosadbc20082012-12-10 13:39:11 -08001626 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 16:39:34 -07001627 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001628
Amin Hassanie9ffb862019-09-25 17:10:40 -07001629 _xbuddy = xbuddy.XBuddy(manage_builds=options.xbuddy_manage_builds,
joychen121fc9b2013-08-02 14:30:30 -07001630 static_dir=options.static_dir)
Chris Sosa75490802013-09-30 17:21:45 -07001631 if options.clear_cache and options.xbuddy_manage_builds:
1632 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 14:30:30 -07001633
Chris Sosa6a3697f2013-01-29 16:44:43 -08001634 # We allow global use here to share with cherrypy classes.
1635 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001636 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001637 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001638 _xbuddy,
joychened64b222013-06-21 16:39:34 -07001639 static_dir=options.static_dir,
Don Garrett0ad09372010-12-06 16:20:30 -08001640 proxy_port=options.proxy_port,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001641 )
Chris Sosa7c931362010-10-11 19:49:01 -07001642
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001643 if options.exit:
1644 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001645
joychen3cb228e2013-06-12 12:13:13 -07001646 dev_server = DevServerRoot(_xbuddy)
Congbin Guo3afae6c2019-08-13 16:29:42 -07001647 health_checker_app = health_checker.Root(dev_server, options.static_dir)
joychen3cb228e2013-06-12 12:13:13 -07001648
Chris Sosa855b8932013-08-21 13:24:55 -07001649 if options.pidfile:
1650 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1651
Gilad Arnold11fbef42014-02-10 11:04:13 -08001652 if options.portfile:
1653 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1654
Dan Shiafd5c6c2016-01-07 10:27:03 -08001655 if (options.android_build_credential and
1656 os.path.exists(options.android_build_credential)):
1657 try:
1658 with open(options.android_build_credential) as f:
1659 android_build.BuildAccessor.credential_info = json.load(f)
1660 except ValueError as e:
1661 _Log('Failed to load the android build credential: %s. Error: %s.' %
1662 (options.android_build_credential, e))
Congbin Guo3afae6c2019-08-13 16:29:42 -07001663
1664 cherrypy.tree.mount(health_checker_app, '/check_health',
1665 config=health_checker.get_config())
joychen3cb228e2013-06-12 12:13:13 -07001666 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001667
1668
1669if __name__ == '__main__':
1670 main()