blob: 9b8b39561efd42a00a0bff228b0d3f7a1f7f0a53 [file] [log] [blame]
Gabe Black3b567202015-09-23 14:07:59 -07001#!/usr/bin/python2
Chris Sosa7c931362010-10-11 19:49:01 -07002
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
11systems. It mainly operates in two modes:
12
131) archive mode: In this mode, the devserver is configured to stage and
14serve 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
212) artifact-generation mode: in this mode, the devserver will attempt to
22generate update payloads and build artifacts when requested. This mode only
23works in the Chromium OS chroot as it uses build tools only present in the
24chroot (emerge, cros_generate_update_payload, etc.). By default, when a device
25requests an update from this form of devserver, the devserver will attempt to
26discover if a more recent build of the board has been built by the developer
27and generate a payload that the requested system can autoupdate to. In addition,
28it accepts gmerge requests from devices that will stage the newest version of
joychen84d13772013-08-06 09:17:23 -070029a particular package from a developer's chroot onto a requesting device.
Chris Sosa3ae4dc12013-03-29 11:47:00 -070030
31For example:
32gmerge gmerge -d <devserver_url>
33
34devserver will see if a newer package of gmerge is available. If gmerge is
35cros_work'd on, it will re-build gmerge. After this, gmerge will install that
36version of gmerge that the devserver just created/found.
37
38For autoupdates, there are many more advanced options that can help specify
39how to update and which payload to give to a requester.
40"""
41
Gabe Black3b567202015-09-23 14:07:59 -070042from __future__ import print_function
Chris Sosa7c931362010-10-11 19:49:01 -070043
Gilad Arnold55a2a372012-10-02 09:46:32 -070044import json
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070045import optparse
rtc@google.comded22402009-10-26 22:36:21 +000046import os
Scott Zawalski4647ce62012-01-03 17:17:28 -050047import re
Simran Basi4baad082013-02-14 13:39:18 -080048import shutil
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -080049import socket
Chris Masone816e38c2012-05-02 12:22:36 -070050import subprocess
J. Richard Barnette3d977b82013-04-23 11:05:19 -070051import sys
Chris Masone816e38c2012-05-02 12:22:36 -070052import tempfile
Dan Shi59ae7092013-06-04 14:37:27 -070053import threading
Dan Shiafd0e492015-05-27 14:23:51 -070054import time
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -070055import types
J. Richard Barnette3d977b82013-04-23 11:05:19 -070056from logging import handlers
57
58import cherrypy
Chris Sosa855b8932013-08-21 13:24:55 -070059from cherrypy import _cplogging as cplogging
60from cherrypy.process import plugins
rtc@google.comded22402009-10-26 22:36:21 +000061
Chris Sosa0356d3b2010-09-16 15:46:22 -070062import autoupdate
Dan Shi2f136862016-02-11 15:38:38 -080063import artifact_info
Chris Sosa75490802013-09-30 17:21:45 -070064import build_artifact
Gilad Arnold11fbef42014-02-10 11:04:13 -080065import cherrypy_ext
Gilad Arnoldc65330c2012-09-20 15:17:48 -070066import common_util
Simran Basief83d6a2014-08-28 14:32:01 -070067import devserver_constants
Chris Sosa47a7d4e2012-03-28 11:26:55 -070068import downloader
Chris Sosa7cd23202013-10-15 17:22:57 -070069import gsutil_util
Gilad Arnoldc65330c2012-09-20 15:17:48 -070070import log_util
joychen3cb228e2013-06-12 12:13:13 -070071import xbuddy
Gilad Arnoldc65330c2012-09-20 15:17:48 -070072
Gilad Arnoldc65330c2012-09-20 15:17:48 -070073# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080074def _Log(message, *args):
75 return log_util.LogWithTag('DEVSERVER', message, *args)
Chris Sosa0356d3b2010-09-16 15:46:22 -070076
Dan Shiafd0e492015-05-27 14:23:51 -070077try:
78 import psutil
79except ImportError:
80 # Ignore psutil import failure. This is for backwards compatibility, so
81 # "cros flash" can still update duts with build without psutil installed.
82 # The reason is that, during cros flash, local devserver code is copied over
83 # to DUT, and devserver will be running inside DUT to stage the build.
84 _Log('Python module psutil is not installed, devserver load data will not be '
85 'collected')
86 psutil = None
Dan Shi94dcbe82015-06-08 20:51:13 -070087except OSError as e:
88 # Ignore error like following. psutil may not work properly in builder. Ignore
89 # the error as load information of devserver is not used in builder.
90 # OSError: [Errno 2] No such file or directory: '/dev/pts/0'
91 _Log('psutil is failed to be imported, error: %s. devserver load data will '
92 'not be collected.', e)
93 psutil = None
94
Dan Shi72b16132015-10-08 12:10:33 -070095try:
96 import android_build
97except ImportError as e:
98 # Ignore android_build import failure. This is to support devserver running
99 # inside a ChromeOS device triggered by cros flash. Most ChromeOS test images
100 # do not have google-api-python-client module and they don't need to support
101 # Android updating, therefore, ignore the import failure here.
102 _Log('Import module android_build failed with error: %s', e)
103 android_build = None
Frank Farzan40160872011-12-12 18:39:18 -0800104
Chris Sosa417e55d2011-01-25 16:40:48 -0800105CACHED_ENTRIES = 12
Don Garrettf90edf02010-11-16 17:36:14 -0800106
Simran Basi4baad082013-02-14 13:39:18 -0800107TELEMETRY_FOLDER = 'telemetry_src'
108TELEMETRY_DEPS = ['dep-telemetry_dep.tar.bz2',
109 'dep-page_cycler_dep.tar.bz2',
Simran Basi0d078682013-03-22 16:40:04 -0700110 'dep-chrome_test.tar.bz2',
111 'dep-perf_data_dep.tar.bz2']
Simran Basi4baad082013-02-14 13:39:18 -0800112
Chris Sosa0356d3b2010-09-16 15:46:22 -0700113# Sets up global to share between classes.
rtc@google.com21a5ca32009-11-04 18:23:23 +0000114updater = None
rtc@google.comded22402009-10-26 22:36:21 +0000115
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700116# Log rotation parameters. These settings correspond to once a week
J. Richard Barnette6dfa5342013-06-04 11:48:56 -0700117# at midnight between Friday and Saturday, with about three months
118# of old logs kept for backup.
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700119#
120# For more, see the documentation for
121# logging.handlers.TimedRotatingFileHandler
J. Richard Barnette6dfa5342013-06-04 11:48:56 -0700122_LOG_ROTATION_TIME = 'W4'
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700123_LOG_ROTATION_BACKUP = 13
124
Dan Shiafd0e492015-05-27 14:23:51 -0700125# Number of seconds between the collection of disk and network IO counters.
126STATS_INTERVAL = 10.0
Frank Farzan40160872011-12-12 18:39:18 -0800127
Chris Sosa9164ca32012-03-28 11:04:50 -0700128class DevServerError(Exception):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700129 """Exception class used by this module."""
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700130
131
Dan Shiafd0e492015-05-27 14:23:51 -0700132def require_psutil():
Gabe Black3b567202015-09-23 14:07:59 -0700133 """Decorator for functions require psutil to run."""
Dan Shiafd0e492015-05-27 14:23:51 -0700134 def deco_require_psutil(func):
135 """Wrapper of the decorator function.
136
Gabe Black3b567202015-09-23 14:07:59 -0700137 Args:
138 func: function to be called.
Dan Shiafd0e492015-05-27 14:23:51 -0700139 """
140 def func_require_psutil(*args, **kwargs):
141 """Decorator for functions require psutil to run.
142
143 If psutil is not installed, skip calling the function.
144
Gabe Black3b567202015-09-23 14:07:59 -0700145 Args:
146 *args: arguments for function to be called.
147 **kwargs: keyword arguments for function to be called.
Dan Shiafd0e492015-05-27 14:23:51 -0700148 """
149 if psutil:
150 return func(*args, **kwargs)
151 else:
152 _Log('Python module psutil is not installed. Function call %s is '
153 'skipped.' % func)
154 return func_require_psutil
155 return deco_require_psutil
156
157
Gabe Black3b567202015-09-23 14:07:59 -0700158def _canonicalize_archive_url(archive_url):
159 """Canonicalizes archive_url strings.
160
161 Raises:
162 DevserverError: if archive_url is not set.
163 """
164 if archive_url:
165 if not archive_url.startswith('gs://'):
166 raise DevServerError("Archive URL isn't from Google Storage (%s) ." %
167 archive_url)
168
169 return archive_url.rstrip('/')
170 else:
171 raise DevServerError("Must specify an archive_url in the request")
172
173
174def _canonicalize_local_path(local_path):
175 """Canonicalizes |local_path| strings.
176
177 Raises:
178 DevserverError: if |local_path| is not set.
179 """
180 # Restrict staging of local content to only files within the static
181 # directory.
182 local_path = os.path.abspath(local_path)
183 if not local_path.startswith(updater.static_dir):
184 raise DevServerError('Local path %s must be a subdirectory of the static'
185 ' directory: %s' % (local_path, updater.static_dir))
186
187 return local_path.rstrip('/')
188
189
190def _get_artifacts(kwargs):
191 """Returns a tuple of named and file artifacts given the stage rpc kwargs.
192
193 Raises:
194 DevserverError if no artifacts would be returned.
195 """
196 artifacts = kwargs.get('artifacts')
197 files = kwargs.get('files')
198 if not artifacts and not files:
199 raise DevServerError('No artifacts specified.')
200
201 # Note we NEED to coerce files to a string as we get raw unicode from
202 # cherrypy and we treat files as strings elsewhere in the code.
203 return (str(artifacts).split(',') if artifacts else [],
204 str(files).split(',') if files else [])
205
206
Dan Shi61305df2015-10-26 16:52:35 -0700207def _is_android_build_request(kwargs):
208 """Check if a devserver call is for Android build, based on the arguments.
209
210 This method exams the request's arguments (os_type) to determine if the
211 request is for Android build. If os_type is set to `android`, returns True.
212 If os_type is not set or has other values, returns False.
213
214 Args:
215 kwargs: Keyword arguments for the request.
216
217 Returns:
218 True if the request is for Android build. False otherwise.
219 """
220 os_type = kwargs.get('os_type', None)
221 return os_type == 'android'
222
223
Gabe Black3b567202015-09-23 14:07:59 -0700224def _get_downloader(kwargs):
225 """Returns the downloader based on passed in arguments.
226
227 Args:
228 kwargs: Keyword arguments for the request.
229 """
230 local_path = kwargs.get('local_path')
231 if local_path:
232 local_path = _canonicalize_local_path(local_path)
233
234 dl = None
235 if local_path:
236 dl = downloader.LocalDownloader(updater.static_dir, local_path)
237
Dan Shi61305df2015-10-26 16:52:35 -0700238 if not _is_android_build_request(kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700239 archive_url = kwargs.get('archive_url')
240 if not archive_url and not local_path:
241 raise DevServerError('Requires archive_url or local_path to be '
242 'specified.')
243 if archive_url and local_path:
244 raise DevServerError('archive_url and local_path can not both be '
245 'specified.')
246 if not dl:
247 archive_url = _canonicalize_archive_url(archive_url)
248 dl = downloader.GoogleStorageDownloader(updater.static_dir, archive_url)
249 elif not dl:
250 target = kwargs.get('target', None)
Dan Shi72b16132015-10-08 12:10:33 -0700251 branch = kwargs.get('branch', None)
Dan Shi61305df2015-10-26 16:52:35 -0700252 build_id = kwargs.get('build_id', None)
253 if not target or not branch or not build_id:
Dan Shi72b16132015-10-08 12:10:33 -0700254 raise DevServerError(
Dan Shi61305df2015-10-26 16:52:35 -0700255 'target, branch, build ID must all be specified for downloading '
256 'Android build.')
Dan Shi72b16132015-10-08 12:10:33 -0700257 dl = downloader.AndroidBuildDownloader(updater.static_dir, branch, build_id,
258 target)
Gabe Black3b567202015-09-23 14:07:59 -0700259
260 return dl
261
262
263def _get_downloader_and_factory(kwargs):
264 """Returns the downloader and artifact factory based on passed in arguments.
265
266 Args:
267 kwargs: Keyword arguments for the request.
268 """
269 artifacts, files = _get_artifacts(kwargs)
270 dl = _get_downloader(kwargs)
271
272 if (isinstance(dl, downloader.GoogleStorageDownloader) or
273 isinstance(dl, downloader.LocalDownloader)):
274 factory_class = build_artifact.ChromeOSArtifactFactory
Dan Shi72b16132015-10-08 12:10:33 -0700275 elif isinstance(dl, downloader.AndroidBuildDownloader):
Gabe Black3b567202015-09-23 14:07:59 -0700276 factory_class = build_artifact.AndroidArtifactFactory
277 else:
278 raise DevServerError('Unrecognized value for downloader type: %s' %
279 type(dl))
280
281 factory = factory_class(dl.GetBuildDir(), artifacts, files, dl.GetBuild())
282
283 return dl, factory
284
285
Scott Zawalski4647ce62012-01-03 17:17:28 -0500286def _LeadingWhiteSpaceCount(string):
287 """Count the amount of leading whitespace in a string.
288
289 Args:
290 string: The string to count leading whitespace in.
Don Garrettf84631a2014-01-07 18:21:26 -0800291
Scott Zawalski4647ce62012-01-03 17:17:28 -0500292 Returns:
293 number of white space chars before characters start.
294 """
Gabe Black3b567202015-09-23 14:07:59 -0700295 matched = re.match(r'^\s+', string)
Scott Zawalski4647ce62012-01-03 17:17:28 -0500296 if matched:
297 return len(matched.group())
298
299 return 0
300
301
302def _PrintDocStringAsHTML(func):
303 """Make a functions docstring somewhat HTML style.
304
305 Args:
306 func: The function to return the docstring from.
Don Garrettf84631a2014-01-07 18:21:26 -0800307
Scott Zawalski4647ce62012-01-03 17:17:28 -0500308 Returns:
309 A string that is somewhat formated for a web browser.
310 """
311 # TODO(scottz): Make this parse Args/Returns in a prettier way.
312 # Arguments could be bolded and indented etc.
313 html_doc = []
314 for line in func.__doc__.splitlines():
315 leading_space = _LeadingWhiteSpaceCount(line)
316 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700317 line = '&nbsp;' * leading_space + line
Scott Zawalski4647ce62012-01-03 17:17:28 -0500318
319 html_doc.append('<BR>%s' % line)
320
321 return '\n'.join(html_doc)
322
323
Simran Basief83d6a2014-08-28 14:32:01 -0700324def _GetUpdateTimestampHandler(static_dir):
325 """Returns a handler to update directory staged.timestamp.
326
327 This handler resets the stage.timestamp whenever static content is accessed.
328
329 Args:
330 static_dir: Directory from which static content is being staged.
331
332 Returns:
333 A cherrypy handler to update the timestamp of accessed content.
334 """
335 def UpdateTimestampHandler():
336 if not '404' in cherrypy.response.status:
337 build_match = re.match(devserver_constants.STAGED_BUILD_REGEX,
338 cherrypy.request.path_info)
339 if build_match:
340 build_dir = os.path.join(static_dir, build_match.group('build'))
341 downloader.Downloader.TouchTimestampForStaged(build_dir)
342 return UpdateTimestampHandler
343
344
Chris Sosa7c931362010-10-11 19:49:01 -0700345def _GetConfig(options):
346 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800347
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800348 socket_host = '::'
Yu-Ju Hongc8d4af32013-11-12 15:14:26 -0800349 # Fall back to IPv4 when python is not configured with IPv6.
350 if not socket.has_ipv6:
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800351 socket_host = '0.0.0.0'
352
Simran Basief83d6a2014-08-28 14:32:01 -0700353 # Adds the UpdateTimestampHandler to cherrypy's tools. This tools executes
354 # on the on_end_resource hook. This hook is called once processing is
355 # complete and the response is ready to be returned.
356 cherrypy.tools.update_timestamp = cherrypy.Tool(
357 'on_end_resource', _GetUpdateTimestampHandler(options.static_dir))
358
Gabe Black3b567202015-09-23 14:07:59 -0700359 base_config = {'global':
360 {'server.log_request_headers': True,
361 'server.protocol_version': 'HTTP/1.1',
362 'server.socket_host': socket_host,
363 'server.socket_port': int(options.port),
364 'response.timeout': 6000,
365 'request.show_tracebacks': True,
366 'server.socket_timeout': 60,
367 'server.thread_pool': 2,
368 'engine.autoreload.on': False,
369 },
370 '/api':
371 {
372 # Gets rid of cherrypy parsing post file for args.
373 'request.process_request_body': False,
374 },
375 '/build':
376 {'response.timeout': 100000,
377 },
378 '/update':
379 {
380 # Gets rid of cherrypy parsing post file for args.
381 'request.process_request_body': False,
382 'response.timeout': 10000,
383 },
384 # Sets up the static dir for file hosting.
385 '/static':
386 {'tools.staticdir.dir': options.static_dir,
387 'tools.staticdir.on': True,
388 'response.timeout': 10000,
389 'tools.update_timestamp.on': True,
390 },
391 }
Chris Sosa5f118ef2012-07-12 11:37:50 -0700392 if options.production:
Alex Miller93beca52013-07-30 19:25:09 -0700393 base_config['global'].update({'server.thread_pool': 150})
Chris Sosa7cd23202013-10-15 17:22:57 -0700394 # TODO(sosa): Do this more cleanly.
395 gsutil_util.GSUTIL_ATTEMPTS = 5
Scott Zawalski1c5e7cd2012-02-27 13:12:52 -0500396
Chris Sosa7c931362010-10-11 19:49:01 -0700397 return base_config
rtc@google.com64244662009-11-12 00:52:08 +0000398
Darin Petkove17164a2010-08-11 13:24:41 -0700399
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700400def _GetRecursiveMemberObject(root, member_list):
401 """Returns an object corresponding to a nested member list.
402
403 Args:
404 root: the root object to search
405 member_list: list of nested members to search
Don Garrettf84631a2014-01-07 18:21:26 -0800406
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700407 Returns:
408 An object corresponding to the member name list; None otherwise.
409 """
410 for member in member_list:
411 next_root = root.__class__.__dict__.get(member)
412 if not next_root:
413 return None
414 root = next_root
415 return root
416
417
418def _IsExposed(name):
419 """Returns True iff |name| has an `exposed' attribute and it is set."""
420 return hasattr(name, 'exposed') and name.exposed
421
422
Gilad Arnold748c8322012-10-12 09:51:35 -0700423def _GetExposedMethod(root, nested_member, ignored=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700424 """Returns a CherryPy-exposed method, if such exists.
425
426 Args:
427 root: the root object for searching
428 nested_member: a slash-joined path to the nested member
429 ignored: method paths to be ignored
Don Garrettf84631a2014-01-07 18:21:26 -0800430
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700431 Returns:
432 A function object corresponding to the path defined by |member_list| from
433 the |root| object, if the function is exposed and not ignored; None
434 otherwise.
435 """
Gilad Arnold748c8322012-10-12 09:51:35 -0700436 method = (not (ignored and nested_member in ignored) and
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700437 _GetRecursiveMemberObject(root, nested_member.split('/')))
Gabe Black3b567202015-09-23 14:07:59 -0700438 if method and type(method) == types.FunctionType and _IsExposed(method):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700439 return method
440
441
Gilad Arnold748c8322012-10-12 09:51:35 -0700442def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700443 """Finds exposed CherryPy methods.
444
445 Args:
446 root: the root object for searching
447 prefix: slash-joined chain of members leading to current object
448 unlisted: URLs to be excluded regardless of their exposed status
Don Garrettf84631a2014-01-07 18:21:26 -0800449
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700450 Returns:
451 List of exposed URLs that are not unlisted.
452 """
453 method_list = []
454 for member in sorted(root.__class__.__dict__.keys()):
455 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 09:51:35 -0700456 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700457 continue
458 member_obj = root.__class__.__dict__[member]
459 if _IsExposed(member_obj):
460 if type(member_obj) == types.FunctionType:
461 method_list.append(prefixed_member)
462 else:
463 method_list += _FindExposedMethods(
464 member_obj, prefixed_member, unlisted)
465 return method_list
466
467
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700468class ApiRoot(object):
469 """RESTful API for Dev Server information."""
470 exposed = True
471
472 @cherrypy.expose
473 def hostinfo(self, ip):
474 """Returns a JSON dictionary containing information about the given ip.
475
Gilad Arnold1b908392012-10-05 11:36:27 -0700476 Args:
477 ip: address of host whose info is requested
Don Garrettf84631a2014-01-07 18:21:26 -0800478
Gilad Arnold1b908392012-10-05 11:36:27 -0700479 Returns:
480 A JSON dictionary containing all or some of the following fields:
481 last_event_type (int): last update event type received
482 last_event_status (int): last update event status received
483 last_known_version (string): last known version reported in update ping
484 forced_update_label (string): update label to force next update ping to
485 use, set by setnextupdate
486 See the OmahaEvent class in update_engine/omaha_request_action.h for
487 event type and status code definitions. If the ip does not exist an empty
488 string is returned.
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700489
Gilad Arnold1b908392012-10-05 11:36:27 -0700490 Example URL:
491 http://myhost/api/hostinfo?ip=192.168.1.5
492 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700493 return updater.HandleHostInfoPing(ip)
494
495 @cherrypy.expose
Gilad Arnold286a0062012-01-12 13:47:02 -0800496 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 11:36:27 -0700497 """Returns a JSON object containing a log of host event.
498
499 Args:
500 ip: address of host whose event log is requested, or `all'
Don Garrettf84631a2014-01-07 18:21:26 -0800501
Gilad Arnold1b908392012-10-05 11:36:27 -0700502 Returns:
503 A JSON encoded list (log) of dictionaries (events), each of which
504 containing a `timestamp' and other event fields, as described under
505 /api/hostinfo.
506
507 Example URL:
508 http://myhost/api/hostlog?ip=192.168.1.5
509 """
Gilad Arnold286a0062012-01-12 13:47:02 -0800510 return updater.HandleHostLogPing(ip)
511
512 @cherrypy.expose
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700513 def setnextupdate(self, ip):
514 """Allows the response to the next update ping from a host to be set.
515
516 Takes the IP of the host and an update label as normally provided to the
Gilad Arnold1b908392012-10-05 11:36:27 -0700517 /update command.
518 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700519 body_length = int(cherrypy.request.headers['Content-Length'])
520 label = cherrypy.request.rfile.read(body_length)
521
522 if label:
523 label = label.strip()
524 if label:
525 return updater.HandleSetUpdatePing(ip, label)
Chris Sosa4b951602014-04-09 20:26:07 -0700526 raise common_util.DevServerHTTPError(400, 'No label provided.')
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700527
528
Gilad Arnold55a2a372012-10-02 09:46:32 -0700529 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800530 def fileinfo(self, *args):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700531 """Returns information about a given staged file.
532
533 Args:
Don Garrettf84631a2014-01-07 18:21:26 -0800534 args: path to the file inside the server's static staging directory
535
Gilad Arnold55a2a372012-10-02 09:46:32 -0700536 Returns:
537 A JSON encoded dictionary with information about the said file, which may
538 contain the following keys/values:
Gilad Arnold1b908392012-10-05 11:36:27 -0700539 size (int): the file size in bytes
540 sha1 (string): a base64 encoded SHA1 hash
541 sha256 (string): a base64 encoded SHA256 hash
542
543 Example URL:
544 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 09:46:32 -0700545 """
Don Garrettf84631a2014-01-07 18:21:26 -0800546 file_path = os.path.join(updater.static_dir, *args)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700547 if not os.path.exists(file_path):
548 raise DevServerError('file not found: %s' % file_path)
549 try:
550 file_size = os.path.getsize(file_path)
551 file_sha1 = common_util.GetFileSha1(file_path)
552 file_sha256 = common_util.GetFileSha256(file_path)
553 except os.error, e:
554 raise DevServerError('failed to get info for file %s: %s' %
Gilad Arnolde74b3812013-04-22 11:27:38 -0700555 (file_path, e))
556
557 is_delta = autoupdate.Autoupdate.IsDeltaFormatFile(file_path)
558
559 return json.dumps({
560 autoupdate.Autoupdate.SIZE_ATTR: file_size,
561 autoupdate.Autoupdate.SHA1_ATTR: file_sha1,
562 autoupdate.Autoupdate.SHA256_ATTR: file_sha256,
563 autoupdate.Autoupdate.ISDELTA_ATTR: is_delta
564 })
Gilad Arnold55a2a372012-10-02 09:46:32 -0700565
Chris Sosa76e44b92013-01-31 12:11:38 -0800566
David Rochberg7c79a812011-01-19 14:24:45 -0500567class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700568 """The Root Class for the Dev Server.
569
570 CherryPy works as follows:
571 For each method in this class, cherrpy interprets root/path
572 as a call to an instance of DevServerRoot->method_name. For example,
573 a call to http://myhost/build will call build. CherryPy automatically
574 parses http args and places them as keyword arguments in each method.
575 For paths http://myhost/update/dir1/dir2, you can use *args so that
576 cherrypy uses the update method and puts the extra paths in args.
577 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700578 # Method names that should not be listed on the index page.
579 _UNLISTED_METHODS = ['index', 'doc']
580
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700581 api = ApiRoot()
Chris Sosa7c931362010-10-11 19:49:01 -0700582
Dan Shi59ae7092013-06-04 14:37:27 -0700583 # Number of threads that devserver is staging images.
584 _staging_thread_count = 0
585 # Lock used to lock increasing/decreasing count.
586 _staging_thread_count_lock = threading.Lock()
587
Dan Shiafd0e492015-05-27 14:23:51 -0700588 @require_psutil()
589 def _refresh_io_stats(self):
590 """A call running in a thread to update IO stats periodically."""
591 prev_disk_io_counters = psutil.disk_io_counters()
592 prev_network_io_counters = psutil.net_io_counters()
593 prev_read_time = time.time()
594 while True:
595 time.sleep(STATS_INTERVAL)
596 now = time.time()
597 interval = now - prev_read_time
598 prev_read_time = now
599 # Disk IO is for all disks.
600 disk_io_counters = psutil.disk_io_counters()
601 network_io_counters = psutil.net_io_counters()
602
603 self.disk_read_bytes_per_sec = (
604 disk_io_counters.read_bytes -
605 prev_disk_io_counters.read_bytes)/interval
606 self.disk_write_bytes_per_sec = (
607 disk_io_counters.write_bytes -
608 prev_disk_io_counters.write_bytes)/interval
609 prev_disk_io_counters = disk_io_counters
610
611 self.network_sent_bytes_per_sec = (
612 network_io_counters.bytes_sent -
613 prev_network_io_counters.bytes_sent)/interval
614 self.network_recv_bytes_per_sec = (
615 network_io_counters.bytes_recv -
616 prev_network_io_counters.bytes_recv)/interval
617 prev_network_io_counters = network_io_counters
618
619 @require_psutil()
620 def _start_io_stat_thread(self):
Gabe Black3b567202015-09-23 14:07:59 -0700621 """Start the thread to collect IO stats."""
Dan Shiafd0e492015-05-27 14:23:51 -0700622 thread = threading.Thread(target=self._refresh_io_stats)
623 thread.daemon = True
624 thread.start()
625
joychen3cb228e2013-06-12 12:13:13 -0700626 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700627 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800628 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 12:13:13 -0700629 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 14:24:45 -0500630
Dan Shiafd0e492015-05-27 14:23:51 -0700631 # Cache of disk IO stats, a thread refresh the stats every 10 seconds.
632 # lock is not used for these variables as the only thread writes to these
633 # variables is _refresh_io_stats.
634 self.disk_read_bytes_per_sec = 0
635 self.disk_write_bytes_per_sec = 0
636 # Cache of network IO stats.
637 self.network_sent_bytes_per_sec = 0
638 self.network_recv_bytes_per_sec = 0
639 self._start_io_stat_thread()
640
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700641 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500642 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700643 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700644 import builder
645 if self._builder is None:
646 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500647 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700648
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700649 @cherrypy.expose
Dan Shif8eb0d12013-08-01 17:52:06 -0700650 def is_staged(self, **kwargs):
651 """Check if artifacts have been downloaded.
652
Chris Sosa6b0c6172013-08-05 17:01:33 -0700653 async: True to return without waiting for download to complete.
654 artifacts: Comma separated list of named artifacts to download.
655 These are defined in artifact_info and have their implementation
656 in build_artifact.py.
657 files: Comma separated list of file artifacts to stage. These
658 will be available as is in the corresponding static directory with no
659 custom post-processing.
660
661 returns: True of all artifacts are staged.
Dan Shif8eb0d12013-08-01 17:52:06 -0700662
663 Example:
664 To check if autotest and test_suites are staged:
665 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
666 artifacts=autotest,test_suites
667 """
Gabe Black3b567202015-09-23 14:07:59 -0700668 dl, factory = _get_downloader_and_factory(kwargs)
669 return str(dl.IsStaged(factory))
Dan Shi59ae7092013-06-04 14:37:27 -0700670
Chris Sosa76e44b92013-01-31 12:11:38 -0800671 @cherrypy.expose
Prashanth Ba06d2d22014-03-07 15:35:19 -0800672 def list_image_dir(self, **kwargs):
673 """Take an archive url and list the contents in its staged directory.
674
675 Args:
676 kwargs:
677 archive_url: Google Storage URL for the build.
678
679 Example:
680 To list the contents of where this devserver should have staged
681 gs://image-archive/<board>-release/<build> call:
682 http://devserver_url:<port>/list_image_dir?archive_url=<gs://..>
683
684 Returns:
685 A string with information about the contents of the image directory.
686 """
Gabe Black3b567202015-09-23 14:07:59 -0700687 dl = _get_downloader(kwargs)
Prashanth Ba06d2d22014-03-07 15:35:19 -0800688 try:
Gabe Black3b567202015-09-23 14:07:59 -0700689 image_dir_contents = dl.ListBuildDir()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800690 except build_artifact.ArtifactDownloadError as e:
691 return 'Cannot list the contents of staged artifacts. %s' % e
692 if not image_dir_contents:
Gabe Black3b567202015-09-23 14:07:59 -0700693 return '%s has not been staged on this devserver.' % dl.DescribeSource()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800694 return image_dir_contents
695
696 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800697 def stage(self, **kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700698 """Downloads and caches build artifacts.
Chris Sosa76e44b92013-01-31 12:11:38 -0800699
Gabe Black3b567202015-09-23 14:07:59 -0700700 Downloads and caches build artifacts, possibly from a Google Storage URL,
Dan Shi72b16132015-10-08 12:10:33 -0700701 or from Android's build server. Returns once these have been downloaded
Gabe Black3b567202015-09-23 14:07:59 -0700702 on the devserver. A call to this will attempt to cache non-specified
703 artifacts in the background for the given from the given URL following
704 the principle of spatial locality. Spatial locality of different
Chris Sosa76e44b92013-01-31 12:11:38 -0800705 artifacts is explicitly defined in the build_artifact module.
706
707 These artifacts will then be available from the static/ sub-directory of
708 the devserver.
709
710 Args:
711 archive_url: Google Storage URL for the build.
Simran Basi4243a862014-12-12 12:48:33 -0800712 local_path: Local path for the build.
Dan Shif8eb0d12013-08-01 17:52:06 -0700713 async: True to return without waiting for download to complete.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700714 artifacts: Comma separated list of named artifacts to download.
715 These are defined in artifact_info and have their implementation
716 in build_artifact.py.
717 files: Comma separated list of files to stage. These
718 will be available as is in the corresponding static directory with no
719 custom post-processing.
Laurence Goodbyf5c958d2016-01-14 18:23:56 -0800720 clean: True to remove any previously staged artifacts first.
Chris Sosa76e44b92013-01-31 12:11:38 -0800721
722 Example:
723 To download the autotest and test suites tarballs:
724 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
725 artifacts=autotest,test_suites
726 To download the full update payload:
727 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
728 artifacts=full_payload
Chris Sosa6b0c6172013-08-05 17:01:33 -0700729 To download just a file called blah.bin:
730 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
731 files=blah.bin
Chris Sosa76e44b92013-01-31 12:11:38 -0800732
733 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 16:39:34 -0700734 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 12:11:38 -0800735
736 Note for this example, relative path is the archive_url stripped of its
737 basename i.e. path/ in the examples above. Specific example:
738
739 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
740
741 Will get staged to:
742
joychened64b222013-06-21 16:39:34 -0700743 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Chris Sosa76e44b92013-01-31 12:11:38 -0800744 """
Gabe Black3b567202015-09-23 14:07:59 -0700745 dl, factory = _get_downloader_and_factory(kwargs)
746
Dan Shi59ae7092013-06-04 14:37:27 -0700747 with DevServerRoot._staging_thread_count_lock:
748 DevServerRoot._staging_thread_count += 1
749 try:
Laurence Goodbyf5c958d2016-01-14 18:23:56 -0800750 boolean_string = kwargs.get('clean')
751 clean = xbuddy.XBuddy.ParseBoolean(boolean_string)
752 if clean and os.path.exists(dl.GetBuildDir()):
753 _Log('Removing %s' % dl.GetBuildDir())
754 shutil.rmtree(dl.GetBuildDir())
Gabe Black3b567202015-09-23 14:07:59 -0700755 async = kwargs.get('async', False)
756 dl.Download(factory, async=async)
Dan Shi59ae7092013-06-04 14:37:27 -0700757 finally:
758 with DevServerRoot._staging_thread_count_lock:
759 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800760 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700761
762 @cherrypy.expose
Dan Shi2f136862016-02-11 15:38:38 -0800763 def locate_file(self, **kwargs):
764 """Get the path to the given file name.
765
766 This method looks up the given file name inside specified build artifacts.
767 One use case is to help caller to locate an apk file inside a build
768 artifact. The location of the apk file could be different based on the
769 branch and target.
770
771 Args:
772 file_name: Name of the file to look for.
773 artifacts: A list of artifact names to search for the file.
774
775 Returns:
776 Path to the file with the given name. It's relative to the folder for the
777 build, e.g., DATA/priv-app/sl4a/sl4a.apk
778
779 """
780 dl, _ = _get_downloader_and_factory(kwargs)
781 try:
782 file_name = kwargs['file_name'].lower()
783 artifacts = kwargs['artifacts']
784 except KeyError:
785 raise DevServerError('`file_name` and `artifacts` are required to search '
786 'for a file in build artifacts.')
787 build_path = dl.GetBuildDir()
788 for artifact in artifacts:
789 # Get the unzipped folder of the artifact. If it's not defined in
790 # ARTIFACT_UNZIP_FOLDER_MAP, assume the files are unzipped to the build
791 # directory directly.
792 folder = artifact_info.ARTIFACT_UNZIP_FOLDER_MAP.get(artifact, '')
793 artifact_path = os.path.join(build_path, folder)
794 for root, _, filenames in os.walk(artifact_path):
795 if file_name in set([f.lower() for f in filenames]):
796 return os.path.relpath(os.path.join(root, file_name), build_path)
797 raise DevServerError('File `%s` can not be found in artifacts: %s' %
798 (file_name, artifacts))
799
800 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -0800801 def setup_telemetry(self, **kwargs):
802 """Extracts and sets up telemetry
803
804 This method goes through the telemetry deps packages, and stages them on
805 the devserver to be used by the drones and the telemetry tests.
806
807 Args:
808 archive_url: Google Storage URL for the build.
809
810 Returns:
811 Path to the source folder for the telemetry codebase once it is staged.
812 """
Gabe Black3b567202015-09-23 14:07:59 -0700813 dl = _get_downloader(kwargs)
Simran Basi4baad082013-02-14 13:39:18 -0800814
Gabe Black3b567202015-09-23 14:07:59 -0700815 build_path = dl.GetBuildDir()
Simran Basi4baad082013-02-14 13:39:18 -0800816 deps_path = os.path.join(build_path, 'autotest/packages')
817 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
818 src_folder = os.path.join(telemetry_path, 'src')
819
820 with self._telemetry_lock_dict.lock(telemetry_path):
821 if os.path.exists(src_folder):
822 # Telemetry is already fully stage return
823 return src_folder
824
825 common_util.MkDirP(telemetry_path)
826
827 # Copy over the required deps tar balls to the telemetry directory.
828 for dep in TELEMETRY_DEPS:
829 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -0700830 if not os.path.exists(dep_path):
831 # This dep does not exist (could be new), do not extract it.
832 continue
Simran Basi4baad082013-02-14 13:39:18 -0800833 try:
834 common_util.ExtractTarball(dep_path, telemetry_path)
835 except common_util.CommonUtilError as e:
836 shutil.rmtree(telemetry_path)
837 raise DevServerError(str(e))
838
839 # By default all the tarballs extract to test_src but some parts of
840 # the telemetry code specifically hardcoded to exist inside of 'src'.
841 test_src = os.path.join(telemetry_path, 'test_src')
842 try:
843 shutil.move(test_src, src_folder)
844 except shutil.Error:
845 # This can occur if src_folder already exists. Remove and retry move.
846 shutil.rmtree(src_folder)
Gabe Black3b567202015-09-23 14:07:59 -0700847 raise DevServerError(
848 'Failure in telemetry setup for build %s. Appears that the '
849 'test_src to src move failed.' % dl.GetBuild())
Simran Basi4baad082013-02-14 13:39:18 -0800850
851 return src_folder
852
853 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800854 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -0700855 """Symbolicates a minidump using pre-downloaded symbols, returns it.
856
857 Callers will need to POST to this URL with a body of MIME-type
858 "multipart/form-data".
859 The body should include a single argument, 'minidump', containing the
860 binary-formatted minidump to symbolicate.
861
Chris Masone816e38c2012-05-02 12:22:36 -0700862 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -0800863 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -0700864 minidump: The binary minidump file to symbolicate.
865 """
Gabe Black3b567202015-09-23 14:07:59 -0700866 kwargs['artifacts'] = 'symbols'
867 dl = _get_downloader(kwargs)
868
Chris Sosa76e44b92013-01-31 12:11:38 -0800869 # Ensure the symbols have been staged.
Gabe Black3b567202015-09-23 14:07:59 -0700870 if self.stage(**kwargs) != 'Success':
871 raise DevServerError('Failed to stage symbols for %s' %
872 dl.DescribeSource())
Chris Sosa76e44b92013-01-31 12:11:38 -0800873
Chris Masone816e38c2012-05-02 12:22:36 -0700874 to_return = ''
875 with tempfile.NamedTemporaryFile() as local:
876 while True:
877 data = minidump.file.read(8192)
878 if not data:
879 break
880 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -0800881
Chris Masone816e38c2012-05-02 12:22:36 -0700882 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -0800883
Gabe Black3b567202015-09-23 14:07:59 -0700884 symbols_directory = os.path.join(dl.GetBuildDir(), 'debug', 'breakpad')
Chris Sosa76e44b92013-01-31 12:11:38 -0800885
886 stackwalk = subprocess.Popen(
887 ['minidump_stackwalk', local.name, symbols_directory],
888 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
889
Chris Masone816e38c2012-05-02 12:22:36 -0700890 to_return, error_text = stackwalk.communicate()
891 if stackwalk.returncode != 0:
892 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
893 error_text, stackwalk.returncode))
894
895 return to_return
896
897 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800898 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 15:31:36 -0400899 """Return a string representing the latest build for a given target.
900
901 Args:
902 target: The build target, typically a combination of the board and the
903 type of build e.g. x86-mario-release.
904 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
905 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-07 18:21:26 -0800906
Scott Zawalski16954532012-03-20 15:31:36 -0400907 Returns:
908 A string representation of the latest build if one exists, i.e.
909 R19-1993.0.0-a1-b1480.
910 An empty string if no latest could be found.
911 """
Don Garrettf84631a2014-01-07 18:21:26 -0800912 if not kwargs:
Scott Zawalski16954532012-03-20 15:31:36 -0400913 return _PrintDocStringAsHTML(self.latestbuild)
914
Don Garrettf84631a2014-01-07 18:21:26 -0800915 if 'target' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -0700916 raise common_util.DevServerHTTPError(500, 'Error: target= is required!')
Dan Shi61305df2015-10-26 16:52:35 -0700917
918 if _is_android_build_request(kwargs):
919 branch = kwargs.get('branch', None)
920 target = kwargs.get('target', None)
921 if not target or not branch:
922 raise DevServerError(
923 'Both target and branch must be specified to query for the latest '
924 'Android build.')
925 return android_build.BuildAccessor.GetLatestBuildID(target, branch)
926
Scott Zawalski16954532012-03-20 15:31:36 -0400927 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700928 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-07 18:21:26 -0800929 updater.static_dir, kwargs['target'],
930 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700931 except common_util.CommonUtilError as errmsg:
Chris Sosa4b951602014-04-09 20:26:07 -0700932 raise common_util.DevServerHTTPError(500, str(errmsg))
Scott Zawalski16954532012-03-20 15:31:36 -0400933
934 @cherrypy.expose
xixuan7efd0002016-04-14 15:34:01 -0700935 def list_suite_controls(self, **kwargs):
936 """Return a list of contents of all known control files.
937
938 Example URL:
939 To List all control files' content:
940 http://dev-server/list_suite_controls?suite_name=bvt&
941 build=daisy_spring-release/R29-4279.0.0
942
943 Args:
944 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
945 suite_name: List the control files belonging to that suite.
946
947 Returns:
Dan Shia1cd6522016-04-18 16:07:21 -0700948 A dictionary of all control files's path to its content for given suite.
xixuan7efd0002016-04-14 15:34:01 -0700949 """
950 if not kwargs:
951 return _PrintDocStringAsHTML(self.controlfiles)
952
953 if 'build' not in kwargs:
954 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
955
956 if 'suite_name' not in kwargs:
Dan Shia1cd6522016-04-18 16:07:21 -0700957 raise common_util.DevServerHTTPError(500,
958 'Error: suite_name= is required!')
xixuan7efd0002016-04-14 15:34:01 -0700959
960 control_file_list = [
961 line.rstrip() for line in common_util.GetControlFileListForSuite(
962 updater.static_dir, kwargs['build'],
963 kwargs['suite_name']).splitlines()]
964
Dan Shia1cd6522016-04-18 16:07:21 -0700965 control_file_content_dict = {}
xixuan7efd0002016-04-14 15:34:01 -0700966 for control_path in control_file_list:
Dan Shia1cd6522016-04-18 16:07:21 -0700967 control_file_content_dict[control_path] = (common_util.GetControlFile(
xixuan7efd0002016-04-14 15:34:01 -0700968 updater.static_dir, kwargs['build'], control_path))
969
Dan Shia1cd6522016-04-18 16:07:21 -0700970 return json.dumps(control_file_content_dict)
xixuan7efd0002016-04-14 15:34:01 -0700971
972 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800973 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500974 """Return a control file or a list of all known control files.
975
976 Example URL:
977 To List all control files:
beepsbd337242013-07-09 22:44:06 -0700978 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
979 To List all control files for, say, the bvt suite:
980 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -0500981 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500982 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 -0500983
984 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500985 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500986 control_path: If you want the contents of a control file set this
987 to the path. E.g. client/site_tests/sleeptest/control
988 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -0700989 suite_name: If control_path is not specified but a suite_name is
990 specified, list the control files belonging to that suite instead of
991 all control files. The empty string for suite_name will list all control
992 files for the build.
Don Garrettf84631a2014-01-07 18:21:26 -0800993
Scott Zawalski4647ce62012-01-03 17:17:28 -0500994 Returns:
995 Contents of a control file if control_path is provided.
996 A list of control files if no control_path is provided.
997 """
Don Garrettf84631a2014-01-07 18:21:26 -0800998 if not kwargs:
Scott Zawalski4647ce62012-01-03 17:17:28 -0500999 return _PrintDocStringAsHTML(self.controlfiles)
1000
Don Garrettf84631a2014-01-07 18:21:26 -08001001 if 'build' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -07001002 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -05001003
Don Garrettf84631a2014-01-07 18:21:26 -08001004 if 'control_path' not in kwargs:
1005 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-09 22:44:06 -07001006 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-07 18:21:26 -08001007 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-09 22:44:06 -07001008 else:
1009 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-07 18:21:26 -08001010 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -05001011 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001012 return common_util.GetControlFile(
Don Garrettf84631a2014-01-07 18:21:26 -08001013 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -08001014
1015 @cherrypy.expose
Simran Basi99e63c02014-05-20 10:39:52 -07001016 def xbuddy_translate(self, *args, **kwargs):
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001017 """Translates an xBuddy path to a real path to artifact if it exists.
1018
1019 Args:
Simran Basi99e63c02014-05-20 10:39:52 -07001020 args: An xbuddy path in the form of {local|remote}/build_id/artifact.
1021 Local searches the devserver's static directory. Remote searches a
1022 Google Storage image archive.
1023
1024 Kwargs:
1025 image_dir: Google Storage image archive to search in if requesting a
1026 remote artifact. If none uses the default bucket.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001027
1028 Returns:
Simran Basi99e63c02014-05-20 10:39:52 -07001029 String in the format of build_id/artifact as stored on the local server
1030 or in Google Storage.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001031 """
Simran Basi99e63c02014-05-20 10:39:52 -07001032 build_id, filename = self._xbuddy.Translate(
Gabe Black3b567202015-09-23 14:07:59 -07001033 args, image_dir=kwargs.get('image_dir'))
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001034 response = os.path.join(build_id, filename)
1035 _Log('Path translation requested, returning: %s', response)
1036 return response
1037
1038 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -07001039 def xbuddy(self, *args, **kwargs):
1040 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -07001041
1042 Args:
joycheneaf4cfc2013-07-02 08:38:57 -07001043 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -07001044 components of the path. The path can be understood as
1045 "{local|remote}/build_id/artifact" where build_id is composed of
1046 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -07001047
joychen121fc9b2013-08-02 14:30:30 -07001048 The first path element is optional, and can be "remote" or "local"
1049 If local (the default), devserver will not attempt to access Google
1050 Storage, and will only search the static directory for the files.
1051 If remote, devserver will try to obtain the artifact off GS if it's
1052 not found locally.
1053 The board is the familiar board name, optionally suffixed.
1054 The version can be the google storage version number, and may also be
1055 any of a number of xBuddy defined version aliases that will be
1056 translated into the latest built image that fits the description.
1057 Defaults to latest.
1058 The artifact is one of a number of image or artifact aliases used by
1059 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -07001060
1061 Kwargs:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001062 for_update: {true|false}
1063 if true, pregenerates the update payloads for the image,
1064 and returns the update uri to pass to the
1065 update_engine_client.
joychen3cb228e2013-06-12 12:13:13 -07001066 return_dir: {true|false}
1067 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001068 relative_path: {true|false}
1069 if set to true, returns the relative path to the payload
1070 directory from static_dir.
joychen3cb228e2013-06-12 12:13:13 -07001071 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -07001072 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -07001073 or
joycheneaf4cfc2013-07-02 08:38:57 -07001074 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -07001075
1076 Returns:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001077 If |for_update|, returns a redirect to the image or update file
1078 on the devserver. E.g.,
1079 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
1080 chromium-test-image.bin
1081 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
1082 http://host:port/static/x86-generic-release/R26-4000.0.0/
1083 If |relative_path| is true, return a relative path the folder where the
1084 payloads are. E.g.,
1085 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 12:13:13 -07001086 """
Chris Sosa75490802013-09-30 17:21:45 -07001087 boolean_string = kwargs.get('for_update')
1088 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001089 boolean_string = kwargs.get('return_dir')
1090 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
1091 boolean_string = kwargs.get('relative_path')
1092 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -07001093
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001094 if return_dir and relative_path:
Chris Sosa4b951602014-04-09 20:26:07 -07001095 raise common_util.DevServerHTTPError(
1096 500, 'Cannot specify both return_dir and relative_path')
Chris Sosa75490802013-09-30 17:21:45 -07001097
1098 # For updates, we optimize downloading of test images.
1099 file_name = None
1100 build_id = None
1101 if for_update:
1102 try:
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001103 build_id = self._xbuddy.StageTestArtifactsForUpdate(args)
Chris Sosa75490802013-09-30 17:21:45 -07001104 except build_artifact.ArtifactDownloadError:
1105 build_id = None
1106
1107 if not build_id:
1108 build_id, file_name = self._xbuddy.Get(args)
1109
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001110 if for_update:
1111 _Log('Payload generation triggered by request')
1112 # Forces payload to be in cache and symlinked into build_id dir.
Chris Sosa75490802013-09-30 17:21:45 -07001113 updater.GetUpdateForLabel(autoupdate.FORCED_UPDATE, build_id,
1114 image_name=file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001115
1116 response = None
1117 if return_dir:
1118 response = os.path.join(cherrypy.request.base, 'static', build_id)
1119 _Log('Directory requested, returning: %s', response)
1120 elif relative_path:
1121 response = build_id
1122 _Log('Relative path requested, returning: %s', response)
1123 elif for_update:
1124 response = os.path.join(cherrypy.request.base, 'update', build_id)
1125 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 12:13:13 -07001126 else:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001127 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 14:30:30 -07001128 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001129 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 14:30:30 -07001130 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -07001131
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001132 return response
1133
joychen3cb228e2013-06-12 12:13:13 -07001134 @cherrypy.expose
1135 def xbuddy_list(self):
1136 """Lists the currently available images & time since last access.
1137
Gilad Arnold452fd272014-02-04 11:09:28 -08001138 Returns:
1139 A string representation of a list of tuples [(build_id, time since last
1140 access),...]
joychen3cb228e2013-06-12 12:13:13 -07001141 """
1142 return self._xbuddy.List()
1143
1144 @cherrypy.expose
1145 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 11:09:28 -08001146 """Returns the number of images cached by xBuddy."""
joychen3cb228e2013-06-12 12:13:13 -07001147 return self._xbuddy.Capacity()
1148
1149 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001150 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001151 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001152 return ('Welcome to the Dev Server!<br>\n'
1153 '<br>\n'
1154 'Here are the available methods, click for documentation:<br>\n'
1155 '<br>\n'
1156 '%s' %
1157 '<br>\n'.join(
1158 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001159 for name in _FindExposedMethods(
1160 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001161
1162 @cherrypy.expose
1163 def doc(self, *args):
1164 """Shows the documentation for available methods / URLs.
1165
1166 Example:
1167 http://myhost/doc/update
1168 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001169 name = '/'.join(args)
1170 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001171 if not method:
1172 raise DevServerError("No exposed method named `%s'" % name)
1173 if not method.__doc__:
1174 raise DevServerError("No documentation for exposed method `%s'" % name)
1175 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -07001176
Dale Curtisc9aaf3a2011-08-09 15:47:40 -07001177 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001178 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001179 """Handles an update check from a Chrome OS client.
1180
1181 The HTTP request should contain the standard Omaha-style XML blob. The URL
1182 line may contain an additional intermediate path to the update payload.
1183
joychen121fc9b2013-08-02 14:30:30 -07001184 This request can be handled in one of 4 ways, depending on the devsever
1185 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -07001186
joychen121fc9b2013-08-02 14:30:30 -07001187 1. No intermediate path
1188 If no intermediate path is given, the default behavior is to generate an
1189 update payload from the latest test image locally built for the board
1190 specified in the xml. Devserver serves the generated payload.
1191
1192 2. Path explicitly invokes XBuddy
1193 If there is a path given, it can explicitly invoke xbuddy by prefixing it
1194 with 'xbuddy'. This path is then used to acquire an image binary for the
1195 devserver to generate an update payload from. Devserver then serves this
1196 payload.
1197
1198 3. Path is left for the devserver to interpret.
1199 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
1200 to generate a payload from the test image in that directory and serve it.
1201
1202 4. The devserver is in a 'forced' mode. TO BE DEPRECATED
1203 This comes from the usage of --forced_payload or --image when starting the
1204 devserver. No matter what path (or no path) gets passed in, devserver will
1205 serve the update payload (--forced_payload) or generate an update payload
1206 from the image (--image).
1207
1208 Examples:
1209 1. No intermediate path
1210 update_engine_client --omaha_url=http://myhost/update
1211 This generates an update payload from the latest test image locally built
1212 for the board specified in the xml.
1213
1214 2. Explicitly invoke xbuddy
1215 update_engine_client --omaha_url=
1216 http://myhost/update/xbuddy/remote/board/version/dev
1217 This would go to GS to download the dev image for the board, from which
1218 the devserver would generate a payload to serve.
1219
1220 3. Give a path for devserver to interpret
1221 update_engine_client --omaha_url=http://myhost/update/some/random/path
1222 This would attempt, in order to:
1223 a) Generate an update from a test image binary if found in
1224 static_dir/some/random/path.
1225 b) Serve an update payload found in static_dir/some/random/path.
1226 c) Hope that some/random/path takes the form "board/version" and
1227 and attempt to download an update payload for that board/version
1228 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001229 """
joychen121fc9b2013-08-02 14:30:30 -07001230 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -08001231 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -07001232 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-11 19:49:01 -07001233
joychen121fc9b2013-08-02 14:30:30 -07001234 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001235
Dan Shiafd0e492015-05-27 14:23:51 -07001236 @require_psutil()
1237 def _get_io_stats(self):
1238 """Get the IO stats as a dictionary.
1239
Gabe Black3b567202015-09-23 14:07:59 -07001240 Returns:
1241 A dictionary of IO stats collected by psutil.
Dan Shiafd0e492015-05-27 14:23:51 -07001242
1243 """
1244 return {'disk_read_bytes_per_second': self.disk_read_bytes_per_sec,
1245 'disk_write_bytes_per_second': self.disk_write_bytes_per_sec,
1246 'disk_total_bytes_per_second': (self.disk_read_bytes_per_sec +
1247 self.disk_write_bytes_per_sec),
1248 'network_sent_bytes_per_second': self.network_sent_bytes_per_sec,
1249 'network_recv_bytes_per_second': self.network_recv_bytes_per_sec,
1250 'network_total_bytes_per_second': (self.network_sent_bytes_per_sec +
1251 self.network_recv_bytes_per_sec),
1252 'cpu_percent': psutil.cpu_percent(),}
1253
Dan Shif5ce2de2013-04-25 16:06:32 -07001254 @cherrypy.expose
1255 def check_health(self):
1256 """Collect the health status of devserver to see if it's ready for staging.
1257
Gilad Arnold452fd272014-02-04 11:09:28 -08001258 Returns:
1259 A JSON dictionary containing all or some of the following fields:
1260 free_disk (int): free disk space in GB
1261 staging_thread_count (int): number of devserver threads currently staging
1262 an image
Dan Shif5ce2de2013-04-25 16:06:32 -07001263 """
1264 # Get free disk space.
1265 stat = os.statvfs(updater.static_dir)
1266 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
Dan Shid76e6bb2016-01-28 22:28:51 -08001267 try:
1268 apache_client_count = int(subprocess.check_output('pgrep -fc apache',
1269 shell=True))
1270 except subprocess.CalledProcessError:
1271 apache_client_count = 0
1272 try:
1273 telemetry_test_count = int(subprocess.check_output(
1274 'pgrep -fc "python.*telemetry"', shell=True))
1275 except subprocess.CalledProcessError:
1276 telemetry_test_count = 0
Dan Shif5ce2de2013-04-25 16:06:32 -07001277
Dan Shiafd0e492015-05-27 14:23:51 -07001278 health_data = {
Dan Shif5ce2de2013-04-25 16:06:32 -07001279 'free_disk': free_disk,
Dan Shid76e6bb2016-01-28 22:28:51 -08001280 'staging_thread_count': DevServerRoot._staging_thread_count,
1281 'apache_client_count': apache_client_count,
1282 'telemetry_test_count': telemetry_test_count}
Dan Shiafd0e492015-05-27 14:23:51 -07001283 health_data.update(self._get_io_stats() or {})
1284
1285 return json.dumps(health_data)
Dan Shif5ce2de2013-04-25 16:06:32 -07001286
1287
Chris Sosadbc20082012-12-10 13:39:11 -08001288def _CleanCache(cache_dir, wipe):
1289 """Wipes any excess cached items in the cache_dir.
1290
1291 Args:
1292 cache_dir: the directory we are wiping from.
1293 wipe: If True, wipe all the contents -- not just the excess.
1294 """
1295 if wipe:
1296 # Clear the cache and exit on error.
1297 cmd = 'rm -rf %s/*' % cache_dir
1298 if os.system(cmd) != 0:
1299 _Log('Failed to clear the cache with %s' % cmd)
1300 sys.exit(1)
1301 else:
1302 # Clear all but the last N cached updates
1303 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
1304 (cache_dir, CACHED_ENTRIES))
1305 if os.system(cmd) != 0:
1306 _Log('Failed to clean up old delta cache files with %s' % cmd)
1307 sys.exit(1)
1308
1309
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001310def _AddTestingOptions(parser):
1311 group = optparse.OptionGroup(
1312 parser, 'Advanced Testing Options', 'These are used by test scripts and '
1313 'developers writing integration tests utilizing the devserver. They are '
1314 'not intended to be really used outside the scope of someone '
1315 'knowledgable about the test.')
1316 group.add_option('--exit',
1317 action='store_true',
1318 help='do not start the server (yet pregenerate/clear cache)')
1319 group.add_option('--host_log',
1320 action='store_true', default=False,
1321 help='record history of host update events (/api/hostlog)')
1322 group.add_option('--max_updates',
Gabe Black3b567202015-09-23 14:07:59 -07001323 metavar='NUM', default=-1, type='int',
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001324 help='maximum number of update checks handled positively '
1325 '(default: unlimited)')
1326 group.add_option('--private_key',
1327 metavar='PATH', default=None,
1328 help='path to the private key in pem format. If this is set '
1329 'the devserver will generate update payloads that are '
1330 'signed with this key.')
David Zeuthen52ccd012013-10-31 12:58:26 -07001331 group.add_option('--private_key_for_metadata_hash_signature',
1332 metavar='PATH', default=None,
1333 help='path to the private key in pem format. If this is set '
1334 'the devserver will sign the metadata hash with the given '
1335 'key and transmit in the Omaha-style XML response.')
1336 group.add_option('--public_key',
1337 metavar='PATH', default=None,
1338 help='path to the public key in pem format. If this is set '
1339 'the devserver will transmit a base64 encoded version of '
1340 'the content in the Omaha-style XML response.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001341 group.add_option('--proxy_port',
1342 metavar='PORT', default=None, type='int',
1343 help='port to have the client connect to -- basically the '
1344 'devserver lies to the update to tell it to get the payload '
1345 'from a different port that will proxy the request back to '
1346 'the devserver. The proxy must be managed outside the '
1347 'devserver.')
1348 group.add_option('--remote_payload',
1349 action='store_true', default=False,
Chris Sosa4b951602014-04-09 20:26:07 -07001350 help='Payload is being served from a remote machine. With '
1351 'this setting enabled, this devserver instance serves as '
1352 'just an Omaha server instance. In this mode, the '
1353 'devserver enforces a few extra components of the Omaha '
Chris Sosafc715442014-04-09 20:45:23 -07001354 'protocol, such as hardware class, being sent.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001355 group.add_option('-u', '--urlbase',
1356 metavar='URL',
Gabe Black3b567202015-09-23 14:07:59 -07001357 help='base URL for update images, other than the '
1358 'devserver. Use in conjunction with remote_payload.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001359 parser.add_option_group(group)
1360
1361
1362def _AddUpdateOptions(parser):
1363 group = optparse.OptionGroup(
1364 parser, 'Autoupdate Options', 'These options can be used to change '
1365 'how the devserver either generates or serve update payloads. Please '
1366 'note that all of these option affect how a payload is generated and so '
1367 'do not work in archive-only mode.')
1368 group.add_option('--board',
1369 help='By default the devserver will create an update '
1370 'payload from the latest image built for the board '
1371 'a device that is requesting an update has. When we '
1372 'pre-generate an update (see below) and we do not specify '
1373 'another update_type option like image or payload, the '
1374 'devserver needs to know the board to generate the latest '
1375 'image for. This is that board.')
1376 group.add_option('--critical_update',
1377 action='store_true', default=False,
1378 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001379 group.add_option('--image',
1380 metavar='FILE',
1381 help='Generate and serve an update using this image to any '
1382 'device that requests an update.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001383 group.add_option('--payload',
1384 metavar='PATH',
1385 help='use the update payload from specified directory '
1386 '(update.gz).')
1387 group.add_option('-p', '--pregenerate_update',
1388 action='store_true', default=False,
1389 help='pre-generate the update payload before accepting '
1390 'update requests. Useful to help debug payload generation '
1391 'issues quickly. Also if an update payload will take a '
1392 'long time to generate, a client may timeout if you do not'
1393 'pregenerate the update.')
1394 group.add_option('--src_image',
1395 metavar='PATH', default='',
1396 help='If specified, delta updates will be generated using '
1397 'this image as the source image. Delta updates are when '
1398 'you are updating from a "source image" to a another '
1399 'image.')
1400 parser.add_option_group(group)
1401
1402
1403def _AddProductionOptions(parser):
1404 group = optparse.OptionGroup(
1405 parser, 'Advanced Server Options', 'These options can be used to changed '
1406 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001407 group.add_option('--clear_cache',
1408 action='store_true', default=False,
1409 help='At startup, removes all cached entries from the'
1410 'devserver\'s cache.')
1411 group.add_option('--logfile',
1412 metavar='PATH',
1413 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -07001414 group.add_option('--pidfile',
1415 metavar='PATH',
1416 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 11:04:13 -08001417 group.add_option('--portfile',
1418 metavar='PATH',
1419 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001420 group.add_option('--production',
1421 action='store_true', default=False,
1422 help='have the devserver use production values when '
1423 'starting up. This includes using more threads and '
1424 'performing less logging.')
1425 parser.add_option_group(group)
1426
1427
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001428def _MakeLogHandler(logfile):
1429 """Create a LogHandler instance used to log all messages."""
1430 hdlr_cls = handlers.TimedRotatingFileHandler
1431 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
1432 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -07001433 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001434 return hdlr
1435
1436
Chris Sosacde6bf42012-05-31 18:36:39 -07001437def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001438 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -08001439 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -07001440
1441 # get directory that the devserver is run from
1442 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -07001443 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -07001444 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001445 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -07001446 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -07001447 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001448 parser.add_option('--port',
1449 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 13:13:28 -08001450 help=('port for the dev server to use; if zero, binds to '
1451 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001452 parser.add_option('-t', '--test_image',
1453 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -07001454 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -07001455 parser.add_option('-x', '--xbuddy_manage_builds',
1456 action='store_true',
1457 default=False,
1458 help='If set, allow xbuddy to manage images in'
1459 'build/images.')
Dan Shi72b16132015-10-08 12:10:33 -07001460 parser.add_option('-a', '--android_build_credential',
1461 default=None,
1462 help='Path to a json file which contains the credential '
1463 'needed to access Android builds.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001464 _AddProductionOptions(parser)
1465 _AddUpdateOptions(parser)
1466 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -07001467 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001468
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001469 # Handle options that must be set globally in cherrypy. Do this
1470 # work up front, because calls to _Log() below depend on this
1471 # initialization.
1472 if options.production:
1473 cherrypy.config.update({'environment': 'production'})
1474 if not options.logfile:
1475 cherrypy.config.update({'log.screen': True})
1476 else:
1477 cherrypy.config.update({'log.error_file': '',
1478 'log.access_file': ''})
1479 hdlr = _MakeLogHandler(options.logfile)
1480 # Pylint can't seem to process these two calls properly
1481 # pylint: disable=E1101
1482 cherrypy.log.access_log.addHandler(hdlr)
1483 cherrypy.log.error_log.addHandler(hdlr)
1484 # pylint: enable=E1101
1485
joychened64b222013-06-21 16:39:34 -07001486 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001487 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001488
joychened64b222013-06-21 16:39:34 -07001489 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001490 # If our devserver is only supposed to serve payloads, we shouldn't be
1491 # mucking with the cache at all. If the devserver hadn't previously
1492 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001493 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001494 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001495 else:
1496 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001497
Chris Sosadbc20082012-12-10 13:39:11 -08001498 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 16:39:34 -07001499 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001500
joychen121fc9b2013-08-02 14:30:30 -07001501 _xbuddy = xbuddy.XBuddy(options.xbuddy_manage_builds,
1502 options.board,
joychen121fc9b2013-08-02 14:30:30 -07001503 static_dir=options.static_dir)
Chris Sosa75490802013-09-30 17:21:45 -07001504 if options.clear_cache and options.xbuddy_manage_builds:
1505 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 14:30:30 -07001506
Chris Sosa6a3697f2013-01-29 16:44:43 -08001507 # We allow global use here to share with cherrypy classes.
1508 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001509 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001510 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001511 _xbuddy,
joychened64b222013-06-21 16:39:34 -07001512 static_dir=options.static_dir,
Andrew de los Reyes52620802010-04-12 13:40:07 -07001513 urlbase=options.urlbase,
Chris Sosa5d342a22010-09-28 16:54:41 -07001514 forced_image=options.image,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001515 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001516 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-28 23:42:37 -07001517 src_image=options.src_image,
Chris Sosa08d55a22011-01-19 16:08:02 -08001518 board=options.board,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001519 copy_to_static_root=not options.exit,
1520 private_key=options.private_key,
Gabe Black3b567202015-09-23 14:07:59 -07001521 private_key_for_metadata_hash_signature=(
1522 options.private_key_for_metadata_hash_signature),
David Zeuthen52ccd012013-10-31 12:58:26 -07001523 public_key=options.public_key,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001524 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001525 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001526 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001527 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001528 )
Chris Sosa7c931362010-10-11 19:49:01 -07001529
Chris Sosa6a3697f2013-01-29 16:44:43 -08001530 if options.pregenerate_update:
1531 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 15:46:22 -07001532
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001533 if options.exit:
1534 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001535
joychen3cb228e2013-06-12 12:13:13 -07001536 dev_server = DevServerRoot(_xbuddy)
1537
Gilad Arnold11fbef42014-02-10 11:04:13 -08001538 # Patch CherryPy to support binding to any available port (--port=0).
1539 cherrypy_ext.ZeroPortPatcher.DoPatch(cherrypy)
1540
Chris Sosa855b8932013-08-21 13:24:55 -07001541 if options.pidfile:
1542 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1543
Gilad Arnold11fbef42014-02-10 11:04:13 -08001544 if options.portfile:
1545 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1546
Dan Shiafd5c6c2016-01-07 10:27:03 -08001547 if (options.android_build_credential and
1548 os.path.exists(options.android_build_credential)):
1549 try:
1550 with open(options.android_build_credential) as f:
1551 android_build.BuildAccessor.credential_info = json.load(f)
1552 except ValueError as e:
1553 _Log('Failed to load the android build credential: %s. Error: %s.' %
1554 (options.android_build_credential, e))
joychen3cb228e2013-06-12 12:13:13 -07001555 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001556
1557
1558if __name__ == '__main__':
1559 main()