blob: c5bd8bd12f40f72c25f059d7dee5a9788cfed5d3 [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
Chris Sosa75490802013-09-30 17:21:45 -070063import build_artifact
Gilad Arnold11fbef42014-02-10 11:04:13 -080064import cherrypy_ext
Gilad Arnoldc65330c2012-09-20 15:17:48 -070065import common_util
Simran Basief83d6a2014-08-28 14:32:01 -070066import devserver_constants
Chris Sosa47a7d4e2012-03-28 11:26:55 -070067import downloader
Chris Sosa7cd23202013-10-15 17:22:57 -070068import gsutil_util
Gilad Arnoldc65330c2012-09-20 15:17:48 -070069import log_util
joychen3cb228e2013-06-12 12:13:13 -070070import xbuddy
Gilad Arnoldc65330c2012-09-20 15:17:48 -070071
Gilad Arnoldc65330c2012-09-20 15:17:48 -070072# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080073def _Log(message, *args):
74 return log_util.LogWithTag('DEVSERVER', message, *args)
Chris Sosa0356d3b2010-09-16 15:46:22 -070075
Dan Shiafd0e492015-05-27 14:23:51 -070076try:
77 import psutil
78except ImportError:
79 # Ignore psutil import failure. This is for backwards compatibility, so
80 # "cros flash" can still update duts with build without psutil installed.
81 # The reason is that, during cros flash, local devserver code is copied over
82 # to DUT, and devserver will be running inside DUT to stage the build.
83 _Log('Python module psutil is not installed, devserver load data will not be '
84 'collected')
85 psutil = None
Dan Shi94dcbe82015-06-08 20:51:13 -070086except OSError as e:
87 # Ignore error like following. psutil may not work properly in builder. Ignore
88 # the error as load information of devserver is not used in builder.
89 # OSError: [Errno 2] No such file or directory: '/dev/pts/0'
90 _Log('psutil is failed to be imported, error: %s. devserver load data will '
91 'not be collected.', e)
92 psutil = None
93
Dan Shi72b16132015-10-08 12:10:33 -070094try:
95 import android_build
96except ImportError as e:
97 # Ignore android_build import failure. This is to support devserver running
98 # inside a ChromeOS device triggered by cros flash. Most ChromeOS test images
99 # do not have google-api-python-client module and they don't need to support
100 # Android updating, therefore, ignore the import failure here.
101 _Log('Import module android_build failed with error: %s', e)
102 android_build = None
Frank Farzan40160872011-12-12 18:39:18 -0800103
Chris Sosa417e55d2011-01-25 16:40:48 -0800104CACHED_ENTRIES = 12
Don Garrettf90edf02010-11-16 17:36:14 -0800105
Simran Basi4baad082013-02-14 13:39:18 -0800106TELEMETRY_FOLDER = 'telemetry_src'
107TELEMETRY_DEPS = ['dep-telemetry_dep.tar.bz2',
108 'dep-page_cycler_dep.tar.bz2',
Simran Basi0d078682013-03-22 16:40:04 -0700109 'dep-chrome_test.tar.bz2',
110 'dep-perf_data_dep.tar.bz2']
Simran Basi4baad082013-02-14 13:39:18 -0800111
Chris Sosa0356d3b2010-09-16 15:46:22 -0700112# Sets up global to share between classes.
rtc@google.com21a5ca32009-11-04 18:23:23 +0000113updater = None
rtc@google.comded22402009-10-26 22:36:21 +0000114
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700115# Log rotation parameters. These settings correspond to once a week
J. Richard Barnette6dfa5342013-06-04 11:48:56 -0700116# at midnight between Friday and Saturday, with about three months
117# of old logs kept for backup.
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700118#
119# For more, see the documentation for
120# logging.handlers.TimedRotatingFileHandler
J. Richard Barnette6dfa5342013-06-04 11:48:56 -0700121_LOG_ROTATION_TIME = 'W4'
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700122_LOG_ROTATION_BACKUP = 13
123
Dan Shiafd0e492015-05-27 14:23:51 -0700124# Number of seconds between the collection of disk and network IO counters.
125STATS_INTERVAL = 10.0
Frank Farzan40160872011-12-12 18:39:18 -0800126
Chris Sosa9164ca32012-03-28 11:04:50 -0700127class DevServerError(Exception):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700128 """Exception class used by this module."""
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700129
130
Dan Shiafd0e492015-05-27 14:23:51 -0700131def require_psutil():
Gabe Black3b567202015-09-23 14:07:59 -0700132 """Decorator for functions require psutil to run."""
Dan Shiafd0e492015-05-27 14:23:51 -0700133 def deco_require_psutil(func):
134 """Wrapper of the decorator function.
135
Gabe Black3b567202015-09-23 14:07:59 -0700136 Args:
137 func: function to be called.
Dan Shiafd0e492015-05-27 14:23:51 -0700138 """
139 def func_require_psutil(*args, **kwargs):
140 """Decorator for functions require psutil to run.
141
142 If psutil is not installed, skip calling the function.
143
Gabe Black3b567202015-09-23 14:07:59 -0700144 Args:
145 *args: arguments for function to be called.
146 **kwargs: keyword arguments for function to be called.
Dan Shiafd0e492015-05-27 14:23:51 -0700147 """
148 if psutil:
149 return func(*args, **kwargs)
150 else:
151 _Log('Python module psutil is not installed. Function call %s is '
152 'skipped.' % func)
153 return func_require_psutil
154 return deco_require_psutil
155
156
Gabe Black3b567202015-09-23 14:07:59 -0700157def _canonicalize_archive_url(archive_url):
158 """Canonicalizes archive_url strings.
159
160 Raises:
161 DevserverError: if archive_url is not set.
162 """
163 if archive_url:
164 if not archive_url.startswith('gs://'):
165 raise DevServerError("Archive URL isn't from Google Storage (%s) ." %
166 archive_url)
167
168 return archive_url.rstrip('/')
169 else:
170 raise DevServerError("Must specify an archive_url in the request")
171
172
173def _canonicalize_local_path(local_path):
174 """Canonicalizes |local_path| strings.
175
176 Raises:
177 DevserverError: if |local_path| is not set.
178 """
179 # Restrict staging of local content to only files within the static
180 # directory.
181 local_path = os.path.abspath(local_path)
182 if not local_path.startswith(updater.static_dir):
183 raise DevServerError('Local path %s must be a subdirectory of the static'
184 ' directory: %s' % (local_path, updater.static_dir))
185
186 return local_path.rstrip('/')
187
188
189def _get_artifacts(kwargs):
190 """Returns a tuple of named and file artifacts given the stage rpc kwargs.
191
192 Raises:
193 DevserverError if no artifacts would be returned.
194 """
195 artifacts = kwargs.get('artifacts')
196 files = kwargs.get('files')
197 if not artifacts and not files:
198 raise DevServerError('No artifacts specified.')
199
200 # Note we NEED to coerce files to a string as we get raw unicode from
201 # cherrypy and we treat files as strings elsewhere in the code.
202 return (str(artifacts).split(',') if artifacts else [],
203 str(files).split(',') if files else [])
204
205
Dan Shi61305df2015-10-26 16:52:35 -0700206def _is_android_build_request(kwargs):
207 """Check if a devserver call is for Android build, based on the arguments.
208
209 This method exams the request's arguments (os_type) to determine if the
210 request is for Android build. If os_type is set to `android`, returns True.
211 If os_type is not set or has other values, returns False.
212
213 Args:
214 kwargs: Keyword arguments for the request.
215
216 Returns:
217 True if the request is for Android build. False otherwise.
218 """
219 os_type = kwargs.get('os_type', None)
220 return os_type == 'android'
221
222
Gabe Black3b567202015-09-23 14:07:59 -0700223def _get_downloader(kwargs):
224 """Returns the downloader based on passed in arguments.
225
226 Args:
227 kwargs: Keyword arguments for the request.
228 """
229 local_path = kwargs.get('local_path')
230 if local_path:
231 local_path = _canonicalize_local_path(local_path)
232
233 dl = None
234 if local_path:
235 dl = downloader.LocalDownloader(updater.static_dir, local_path)
236
Dan Shi61305df2015-10-26 16:52:35 -0700237 if not _is_android_build_request(kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700238 archive_url = kwargs.get('archive_url')
239 if not archive_url and not local_path:
240 raise DevServerError('Requires archive_url or local_path to be '
241 'specified.')
242 if archive_url and local_path:
243 raise DevServerError('archive_url and local_path can not both be '
244 'specified.')
245 if not dl:
246 archive_url = _canonicalize_archive_url(archive_url)
247 dl = downloader.GoogleStorageDownloader(updater.static_dir, archive_url)
248 elif not dl:
249 target = kwargs.get('target', None)
Dan Shi72b16132015-10-08 12:10:33 -0700250 branch = kwargs.get('branch', None)
Dan Shi61305df2015-10-26 16:52:35 -0700251 build_id = kwargs.get('build_id', None)
252 if not target or not branch or not build_id:
Dan Shi72b16132015-10-08 12:10:33 -0700253 raise DevServerError(
Dan Shi61305df2015-10-26 16:52:35 -0700254 'target, branch, build ID must all be specified for downloading '
255 'Android build.')
Dan Shi72b16132015-10-08 12:10:33 -0700256 dl = downloader.AndroidBuildDownloader(updater.static_dir, branch, build_id,
257 target)
Gabe Black3b567202015-09-23 14:07:59 -0700258
259 return dl
260
261
262def _get_downloader_and_factory(kwargs):
263 """Returns the downloader and artifact factory based on passed in arguments.
264
265 Args:
266 kwargs: Keyword arguments for the request.
267 """
268 artifacts, files = _get_artifacts(kwargs)
269 dl = _get_downloader(kwargs)
270
271 if (isinstance(dl, downloader.GoogleStorageDownloader) or
272 isinstance(dl, downloader.LocalDownloader)):
273 factory_class = build_artifact.ChromeOSArtifactFactory
Dan Shi72b16132015-10-08 12:10:33 -0700274 elif isinstance(dl, downloader.AndroidBuildDownloader):
Gabe Black3b567202015-09-23 14:07:59 -0700275 factory_class = build_artifact.AndroidArtifactFactory
276 else:
277 raise DevServerError('Unrecognized value for downloader type: %s' %
278 type(dl))
279
280 factory = factory_class(dl.GetBuildDir(), artifacts, files, dl.GetBuild())
281
282 return dl, factory
283
284
Scott Zawalski4647ce62012-01-03 17:17:28 -0500285def _LeadingWhiteSpaceCount(string):
286 """Count the amount of leading whitespace in a string.
287
288 Args:
289 string: The string to count leading whitespace in.
Don Garrettf84631a2014-01-07 18:21:26 -0800290
Scott Zawalski4647ce62012-01-03 17:17:28 -0500291 Returns:
292 number of white space chars before characters start.
293 """
Gabe Black3b567202015-09-23 14:07:59 -0700294 matched = re.match(r'^\s+', string)
Scott Zawalski4647ce62012-01-03 17:17:28 -0500295 if matched:
296 return len(matched.group())
297
298 return 0
299
300
301def _PrintDocStringAsHTML(func):
302 """Make a functions docstring somewhat HTML style.
303
304 Args:
305 func: The function to return the docstring from.
Don Garrettf84631a2014-01-07 18:21:26 -0800306
Scott Zawalski4647ce62012-01-03 17:17:28 -0500307 Returns:
308 A string that is somewhat formated for a web browser.
309 """
310 # TODO(scottz): Make this parse Args/Returns in a prettier way.
311 # Arguments could be bolded and indented etc.
312 html_doc = []
313 for line in func.__doc__.splitlines():
314 leading_space = _LeadingWhiteSpaceCount(line)
315 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700316 line = '&nbsp;' * leading_space + line
Scott Zawalski4647ce62012-01-03 17:17:28 -0500317
318 html_doc.append('<BR>%s' % line)
319
320 return '\n'.join(html_doc)
321
322
Simran Basief83d6a2014-08-28 14:32:01 -0700323def _GetUpdateTimestampHandler(static_dir):
324 """Returns a handler to update directory staged.timestamp.
325
326 This handler resets the stage.timestamp whenever static content is accessed.
327
328 Args:
329 static_dir: Directory from which static content is being staged.
330
331 Returns:
332 A cherrypy handler to update the timestamp of accessed content.
333 """
334 def UpdateTimestampHandler():
335 if not '404' in cherrypy.response.status:
336 build_match = re.match(devserver_constants.STAGED_BUILD_REGEX,
337 cherrypy.request.path_info)
338 if build_match:
339 build_dir = os.path.join(static_dir, build_match.group('build'))
340 downloader.Downloader.TouchTimestampForStaged(build_dir)
341 return UpdateTimestampHandler
342
343
Chris Sosa7c931362010-10-11 19:49:01 -0700344def _GetConfig(options):
345 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800346
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800347 socket_host = '::'
Yu-Ju Hongc8d4af32013-11-12 15:14:26 -0800348 # Fall back to IPv4 when python is not configured with IPv6.
349 if not socket.has_ipv6:
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800350 socket_host = '0.0.0.0'
351
Simran Basief83d6a2014-08-28 14:32:01 -0700352 # Adds the UpdateTimestampHandler to cherrypy's tools. This tools executes
353 # on the on_end_resource hook. This hook is called once processing is
354 # complete and the response is ready to be returned.
355 cherrypy.tools.update_timestamp = cherrypy.Tool(
356 'on_end_resource', _GetUpdateTimestampHandler(options.static_dir))
357
Gabe Black3b567202015-09-23 14:07:59 -0700358 base_config = {'global':
359 {'server.log_request_headers': True,
360 'server.protocol_version': 'HTTP/1.1',
361 'server.socket_host': socket_host,
362 'server.socket_port': int(options.port),
363 'response.timeout': 6000,
364 'request.show_tracebacks': True,
365 'server.socket_timeout': 60,
366 'server.thread_pool': 2,
367 'engine.autoreload.on': False,
368 },
369 '/api':
370 {
371 # Gets rid of cherrypy parsing post file for args.
372 'request.process_request_body': False,
373 },
374 '/build':
375 {'response.timeout': 100000,
376 },
377 '/update':
378 {
379 # Gets rid of cherrypy parsing post file for args.
380 'request.process_request_body': False,
381 'response.timeout': 10000,
382 },
383 # Sets up the static dir for file hosting.
384 '/static':
385 {'tools.staticdir.dir': options.static_dir,
386 'tools.staticdir.on': True,
387 'response.timeout': 10000,
388 'tools.update_timestamp.on': True,
389 },
390 }
Chris Sosa5f118ef2012-07-12 11:37:50 -0700391 if options.production:
Alex Miller93beca52013-07-30 19:25:09 -0700392 base_config['global'].update({'server.thread_pool': 150})
Chris Sosa7cd23202013-10-15 17:22:57 -0700393 # TODO(sosa): Do this more cleanly.
394 gsutil_util.GSUTIL_ATTEMPTS = 5
Scott Zawalski1c5e7cd2012-02-27 13:12:52 -0500395
Chris Sosa7c931362010-10-11 19:49:01 -0700396 return base_config
rtc@google.com64244662009-11-12 00:52:08 +0000397
Darin Petkove17164a2010-08-11 13:24:41 -0700398
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700399def _GetRecursiveMemberObject(root, member_list):
400 """Returns an object corresponding to a nested member list.
401
402 Args:
403 root: the root object to search
404 member_list: list of nested members to search
Don Garrettf84631a2014-01-07 18:21:26 -0800405
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700406 Returns:
407 An object corresponding to the member name list; None otherwise.
408 """
409 for member in member_list:
410 next_root = root.__class__.__dict__.get(member)
411 if not next_root:
412 return None
413 root = next_root
414 return root
415
416
417def _IsExposed(name):
418 """Returns True iff |name| has an `exposed' attribute and it is set."""
419 return hasattr(name, 'exposed') and name.exposed
420
421
Gilad Arnold748c8322012-10-12 09:51:35 -0700422def _GetExposedMethod(root, nested_member, ignored=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700423 """Returns a CherryPy-exposed method, if such exists.
424
425 Args:
426 root: the root object for searching
427 nested_member: a slash-joined path to the nested member
428 ignored: method paths to be ignored
Don Garrettf84631a2014-01-07 18:21:26 -0800429
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700430 Returns:
431 A function object corresponding to the path defined by |member_list| from
432 the |root| object, if the function is exposed and not ignored; None
433 otherwise.
434 """
Gilad Arnold748c8322012-10-12 09:51:35 -0700435 method = (not (ignored and nested_member in ignored) and
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700436 _GetRecursiveMemberObject(root, nested_member.split('/')))
Gabe Black3b567202015-09-23 14:07:59 -0700437 if method and type(method) == types.FunctionType and _IsExposed(method):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700438 return method
439
440
Gilad Arnold748c8322012-10-12 09:51:35 -0700441def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700442 """Finds exposed CherryPy methods.
443
444 Args:
445 root: the root object for searching
446 prefix: slash-joined chain of members leading to current object
447 unlisted: URLs to be excluded regardless of their exposed status
Don Garrettf84631a2014-01-07 18:21:26 -0800448
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700449 Returns:
450 List of exposed URLs that are not unlisted.
451 """
452 method_list = []
453 for member in sorted(root.__class__.__dict__.keys()):
454 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 09:51:35 -0700455 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700456 continue
457 member_obj = root.__class__.__dict__[member]
458 if _IsExposed(member_obj):
459 if type(member_obj) == types.FunctionType:
460 method_list.append(prefixed_member)
461 else:
462 method_list += _FindExposedMethods(
463 member_obj, prefixed_member, unlisted)
464 return method_list
465
466
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700467class ApiRoot(object):
468 """RESTful API for Dev Server information."""
469 exposed = True
470
471 @cherrypy.expose
472 def hostinfo(self, ip):
473 """Returns a JSON dictionary containing information about the given ip.
474
Gilad Arnold1b908392012-10-05 11:36:27 -0700475 Args:
476 ip: address of host whose info is requested
Don Garrettf84631a2014-01-07 18:21:26 -0800477
Gilad Arnold1b908392012-10-05 11:36:27 -0700478 Returns:
479 A JSON dictionary containing all or some of the following fields:
480 last_event_type (int): last update event type received
481 last_event_status (int): last update event status received
482 last_known_version (string): last known version reported in update ping
483 forced_update_label (string): update label to force next update ping to
484 use, set by setnextupdate
485 See the OmahaEvent class in update_engine/omaha_request_action.h for
486 event type and status code definitions. If the ip does not exist an empty
487 string is returned.
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700488
Gilad Arnold1b908392012-10-05 11:36:27 -0700489 Example URL:
490 http://myhost/api/hostinfo?ip=192.168.1.5
491 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700492 return updater.HandleHostInfoPing(ip)
493
494 @cherrypy.expose
Gilad Arnold286a0062012-01-12 13:47:02 -0800495 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 11:36:27 -0700496 """Returns a JSON object containing a log of host event.
497
498 Args:
499 ip: address of host whose event log is requested, or `all'
Don Garrettf84631a2014-01-07 18:21:26 -0800500
Gilad Arnold1b908392012-10-05 11:36:27 -0700501 Returns:
502 A JSON encoded list (log) of dictionaries (events), each of which
503 containing a `timestamp' and other event fields, as described under
504 /api/hostinfo.
505
506 Example URL:
507 http://myhost/api/hostlog?ip=192.168.1.5
508 """
Gilad Arnold286a0062012-01-12 13:47:02 -0800509 return updater.HandleHostLogPing(ip)
510
511 @cherrypy.expose
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700512 def setnextupdate(self, ip):
513 """Allows the response to the next update ping from a host to be set.
514
515 Takes the IP of the host and an update label as normally provided to the
Gilad Arnold1b908392012-10-05 11:36:27 -0700516 /update command.
517 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700518 body_length = int(cherrypy.request.headers['Content-Length'])
519 label = cherrypy.request.rfile.read(body_length)
520
521 if label:
522 label = label.strip()
523 if label:
524 return updater.HandleSetUpdatePing(ip, label)
Chris Sosa4b951602014-04-09 20:26:07 -0700525 raise common_util.DevServerHTTPError(400, 'No label provided.')
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700526
527
Gilad Arnold55a2a372012-10-02 09:46:32 -0700528 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800529 def fileinfo(self, *args):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700530 """Returns information about a given staged file.
531
532 Args:
Don Garrettf84631a2014-01-07 18:21:26 -0800533 args: path to the file inside the server's static staging directory
534
Gilad Arnold55a2a372012-10-02 09:46:32 -0700535 Returns:
536 A JSON encoded dictionary with information about the said file, which may
537 contain the following keys/values:
Gilad Arnold1b908392012-10-05 11:36:27 -0700538 size (int): the file size in bytes
539 sha1 (string): a base64 encoded SHA1 hash
540 sha256 (string): a base64 encoded SHA256 hash
541
542 Example URL:
543 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 09:46:32 -0700544 """
Don Garrettf84631a2014-01-07 18:21:26 -0800545 file_path = os.path.join(updater.static_dir, *args)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700546 if not os.path.exists(file_path):
547 raise DevServerError('file not found: %s' % file_path)
548 try:
549 file_size = os.path.getsize(file_path)
550 file_sha1 = common_util.GetFileSha1(file_path)
551 file_sha256 = common_util.GetFileSha256(file_path)
552 except os.error, e:
553 raise DevServerError('failed to get info for file %s: %s' %
Gilad Arnolde74b3812013-04-22 11:27:38 -0700554 (file_path, e))
555
556 is_delta = autoupdate.Autoupdate.IsDeltaFormatFile(file_path)
557
558 return json.dumps({
559 autoupdate.Autoupdate.SIZE_ATTR: file_size,
560 autoupdate.Autoupdate.SHA1_ATTR: file_sha1,
561 autoupdate.Autoupdate.SHA256_ATTR: file_sha256,
562 autoupdate.Autoupdate.ISDELTA_ATTR: is_delta
563 })
Gilad Arnold55a2a372012-10-02 09:46:32 -0700564
Chris Sosa76e44b92013-01-31 12:11:38 -0800565
David Rochberg7c79a812011-01-19 14:24:45 -0500566class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700567 """The Root Class for the Dev Server.
568
569 CherryPy works as follows:
570 For each method in this class, cherrpy interprets root/path
571 as a call to an instance of DevServerRoot->method_name. For example,
572 a call to http://myhost/build will call build. CherryPy automatically
573 parses http args and places them as keyword arguments in each method.
574 For paths http://myhost/update/dir1/dir2, you can use *args so that
575 cherrypy uses the update method and puts the extra paths in args.
576 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700577 # Method names that should not be listed on the index page.
578 _UNLISTED_METHODS = ['index', 'doc']
579
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700580 api = ApiRoot()
Chris Sosa7c931362010-10-11 19:49:01 -0700581
Dan Shi59ae7092013-06-04 14:37:27 -0700582 # Number of threads that devserver is staging images.
583 _staging_thread_count = 0
584 # Lock used to lock increasing/decreasing count.
585 _staging_thread_count_lock = threading.Lock()
586
Dan Shiafd0e492015-05-27 14:23:51 -0700587 @require_psutil()
588 def _refresh_io_stats(self):
589 """A call running in a thread to update IO stats periodically."""
590 prev_disk_io_counters = psutil.disk_io_counters()
591 prev_network_io_counters = psutil.net_io_counters()
592 prev_read_time = time.time()
593 while True:
594 time.sleep(STATS_INTERVAL)
595 now = time.time()
596 interval = now - prev_read_time
597 prev_read_time = now
598 # Disk IO is for all disks.
599 disk_io_counters = psutil.disk_io_counters()
600 network_io_counters = psutil.net_io_counters()
601
602 self.disk_read_bytes_per_sec = (
603 disk_io_counters.read_bytes -
604 prev_disk_io_counters.read_bytes)/interval
605 self.disk_write_bytes_per_sec = (
606 disk_io_counters.write_bytes -
607 prev_disk_io_counters.write_bytes)/interval
608 prev_disk_io_counters = disk_io_counters
609
610 self.network_sent_bytes_per_sec = (
611 network_io_counters.bytes_sent -
612 prev_network_io_counters.bytes_sent)/interval
613 self.network_recv_bytes_per_sec = (
614 network_io_counters.bytes_recv -
615 prev_network_io_counters.bytes_recv)/interval
616 prev_network_io_counters = network_io_counters
617
618 @require_psutil()
619 def _start_io_stat_thread(self):
Gabe Black3b567202015-09-23 14:07:59 -0700620 """Start the thread to collect IO stats."""
Dan Shiafd0e492015-05-27 14:23:51 -0700621 thread = threading.Thread(target=self._refresh_io_stats)
622 thread.daemon = True
623 thread.start()
624
joychen3cb228e2013-06-12 12:13:13 -0700625 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700626 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800627 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 12:13:13 -0700628 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 14:24:45 -0500629
Dan Shiafd0e492015-05-27 14:23:51 -0700630 # Cache of disk IO stats, a thread refresh the stats every 10 seconds.
631 # lock is not used for these variables as the only thread writes to these
632 # variables is _refresh_io_stats.
633 self.disk_read_bytes_per_sec = 0
634 self.disk_write_bytes_per_sec = 0
635 # Cache of network IO stats.
636 self.network_sent_bytes_per_sec = 0
637 self.network_recv_bytes_per_sec = 0
638 self._start_io_stat_thread()
639
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700640 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500641 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700642 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700643 import builder
644 if self._builder is None:
645 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500646 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700647
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700648 @cherrypy.expose
Dan Shif8eb0d12013-08-01 17:52:06 -0700649 def is_staged(self, **kwargs):
650 """Check if artifacts have been downloaded.
651
Chris Sosa6b0c6172013-08-05 17:01:33 -0700652 async: True to return without waiting for download to complete.
653 artifacts: Comma separated list of named artifacts to download.
654 These are defined in artifact_info and have their implementation
655 in build_artifact.py.
656 files: Comma separated list of file artifacts to stage. These
657 will be available as is in the corresponding static directory with no
658 custom post-processing.
659
660 returns: True of all artifacts are staged.
Dan Shif8eb0d12013-08-01 17:52:06 -0700661
662 Example:
663 To check if autotest and test_suites are staged:
664 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
665 artifacts=autotest,test_suites
666 """
Gabe Black3b567202015-09-23 14:07:59 -0700667 dl, factory = _get_downloader_and_factory(kwargs)
668 return str(dl.IsStaged(factory))
Dan Shi59ae7092013-06-04 14:37:27 -0700669
Chris Sosa76e44b92013-01-31 12:11:38 -0800670 @cherrypy.expose
Prashanth Ba06d2d22014-03-07 15:35:19 -0800671 def list_image_dir(self, **kwargs):
672 """Take an archive url and list the contents in its staged directory.
673
674 Args:
675 kwargs:
676 archive_url: Google Storage URL for the build.
677
678 Example:
679 To list the contents of where this devserver should have staged
680 gs://image-archive/<board>-release/<build> call:
681 http://devserver_url:<port>/list_image_dir?archive_url=<gs://..>
682
683 Returns:
684 A string with information about the contents of the image directory.
685 """
Gabe Black3b567202015-09-23 14:07:59 -0700686 dl = _get_downloader(kwargs)
Prashanth Ba06d2d22014-03-07 15:35:19 -0800687 try:
Gabe Black3b567202015-09-23 14:07:59 -0700688 image_dir_contents = dl.ListBuildDir()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800689 except build_artifact.ArtifactDownloadError as e:
690 return 'Cannot list the contents of staged artifacts. %s' % e
691 if not image_dir_contents:
Gabe Black3b567202015-09-23 14:07:59 -0700692 return '%s has not been staged on this devserver.' % dl.DescribeSource()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800693 return image_dir_contents
694
695 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800696 def stage(self, **kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700697 """Downloads and caches build artifacts.
Chris Sosa76e44b92013-01-31 12:11:38 -0800698
Gabe Black3b567202015-09-23 14:07:59 -0700699 Downloads and caches build artifacts, possibly from a Google Storage URL,
Dan Shi72b16132015-10-08 12:10:33 -0700700 or from Android's build server. Returns once these have been downloaded
Gabe Black3b567202015-09-23 14:07:59 -0700701 on the devserver. A call to this will attempt to cache non-specified
702 artifacts in the background for the given from the given URL following
703 the principle of spatial locality. Spatial locality of different
Chris Sosa76e44b92013-01-31 12:11:38 -0800704 artifacts is explicitly defined in the build_artifact module.
705
706 These artifacts will then be available from the static/ sub-directory of
707 the devserver.
708
709 Args:
710 archive_url: Google Storage URL for the build.
Simran Basi4243a862014-12-12 12:48:33 -0800711 local_path: Local path for the build.
Dan Shif8eb0d12013-08-01 17:52:06 -0700712 async: True to return without waiting for download to complete.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700713 artifacts: Comma separated list of named artifacts to download.
714 These are defined in artifact_info and have their implementation
715 in build_artifact.py.
716 files: Comma separated list of files to stage. These
717 will be available as is in the corresponding static directory with no
718 custom post-processing.
Laurence Goodbyf5c958d2016-01-14 18:23:56 -0800719 clean: True to remove any previously staged artifacts first.
Chris Sosa76e44b92013-01-31 12:11:38 -0800720
721 Example:
722 To download the autotest and test suites tarballs:
723 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
724 artifacts=autotest,test_suites
725 To download the full update payload:
726 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
727 artifacts=full_payload
Chris Sosa6b0c6172013-08-05 17:01:33 -0700728 To download just a file called blah.bin:
729 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
730 files=blah.bin
Chris Sosa76e44b92013-01-31 12:11:38 -0800731
732 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 16:39:34 -0700733 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 12:11:38 -0800734
735 Note for this example, relative path is the archive_url stripped of its
736 basename i.e. path/ in the examples above. Specific example:
737
738 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
739
740 Will get staged to:
741
joychened64b222013-06-21 16:39:34 -0700742 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Chris Sosa76e44b92013-01-31 12:11:38 -0800743 """
Gabe Black3b567202015-09-23 14:07:59 -0700744 dl, factory = _get_downloader_and_factory(kwargs)
745
Dan Shi59ae7092013-06-04 14:37:27 -0700746 with DevServerRoot._staging_thread_count_lock:
747 DevServerRoot._staging_thread_count += 1
748 try:
Laurence Goodbyf5c958d2016-01-14 18:23:56 -0800749 boolean_string = kwargs.get('clean')
750 clean = xbuddy.XBuddy.ParseBoolean(boolean_string)
751 if clean and os.path.exists(dl.GetBuildDir()):
752 _Log('Removing %s' % dl.GetBuildDir())
753 shutil.rmtree(dl.GetBuildDir())
Gabe Black3b567202015-09-23 14:07:59 -0700754 async = kwargs.get('async', False)
755 dl.Download(factory, async=async)
Dan Shi59ae7092013-06-04 14:37:27 -0700756 finally:
757 with DevServerRoot._staging_thread_count_lock:
758 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800759 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700760
761 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -0800762 def setup_telemetry(self, **kwargs):
763 """Extracts and sets up telemetry
764
765 This method goes through the telemetry deps packages, and stages them on
766 the devserver to be used by the drones and the telemetry tests.
767
768 Args:
769 archive_url: Google Storage URL for the build.
770
771 Returns:
772 Path to the source folder for the telemetry codebase once it is staged.
773 """
Gabe Black3b567202015-09-23 14:07:59 -0700774 dl = _get_downloader(kwargs)
Simran Basi4baad082013-02-14 13:39:18 -0800775
Gabe Black3b567202015-09-23 14:07:59 -0700776 build_path = dl.GetBuildDir()
Simran Basi4baad082013-02-14 13:39:18 -0800777 deps_path = os.path.join(build_path, 'autotest/packages')
778 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
779 src_folder = os.path.join(telemetry_path, 'src')
780
781 with self._telemetry_lock_dict.lock(telemetry_path):
782 if os.path.exists(src_folder):
783 # Telemetry is already fully stage return
784 return src_folder
785
786 common_util.MkDirP(telemetry_path)
787
788 # Copy over the required deps tar balls to the telemetry directory.
789 for dep in TELEMETRY_DEPS:
790 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -0700791 if not os.path.exists(dep_path):
792 # This dep does not exist (could be new), do not extract it.
793 continue
Simran Basi4baad082013-02-14 13:39:18 -0800794 try:
795 common_util.ExtractTarball(dep_path, telemetry_path)
796 except common_util.CommonUtilError as e:
797 shutil.rmtree(telemetry_path)
798 raise DevServerError(str(e))
799
800 # By default all the tarballs extract to test_src but some parts of
801 # the telemetry code specifically hardcoded to exist inside of 'src'.
802 test_src = os.path.join(telemetry_path, 'test_src')
803 try:
804 shutil.move(test_src, src_folder)
805 except shutil.Error:
806 # This can occur if src_folder already exists. Remove and retry move.
807 shutil.rmtree(src_folder)
Gabe Black3b567202015-09-23 14:07:59 -0700808 raise DevServerError(
809 'Failure in telemetry setup for build %s. Appears that the '
810 'test_src to src move failed.' % dl.GetBuild())
Simran Basi4baad082013-02-14 13:39:18 -0800811
812 return src_folder
813
814 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800815 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -0700816 """Symbolicates a minidump using pre-downloaded symbols, returns it.
817
818 Callers will need to POST to this URL with a body of MIME-type
819 "multipart/form-data".
820 The body should include a single argument, 'minidump', containing the
821 binary-formatted minidump to symbolicate.
822
Chris Masone816e38c2012-05-02 12:22:36 -0700823 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -0800824 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -0700825 minidump: The binary minidump file to symbolicate.
826 """
Gabe Black3b567202015-09-23 14:07:59 -0700827 kwargs['artifacts'] = 'symbols'
828 dl = _get_downloader(kwargs)
829
Chris Sosa76e44b92013-01-31 12:11:38 -0800830 # Ensure the symbols have been staged.
Gabe Black3b567202015-09-23 14:07:59 -0700831 if self.stage(**kwargs) != 'Success':
832 raise DevServerError('Failed to stage symbols for %s' %
833 dl.DescribeSource())
Chris Sosa76e44b92013-01-31 12:11:38 -0800834
Chris Masone816e38c2012-05-02 12:22:36 -0700835 to_return = ''
836 with tempfile.NamedTemporaryFile() as local:
837 while True:
838 data = minidump.file.read(8192)
839 if not data:
840 break
841 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -0800842
Chris Masone816e38c2012-05-02 12:22:36 -0700843 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -0800844
Gabe Black3b567202015-09-23 14:07:59 -0700845 symbols_directory = os.path.join(dl.GetBuildDir(), 'debug', 'breakpad')
Chris Sosa76e44b92013-01-31 12:11:38 -0800846
847 stackwalk = subprocess.Popen(
848 ['minidump_stackwalk', local.name, symbols_directory],
849 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
850
Chris Masone816e38c2012-05-02 12:22:36 -0700851 to_return, error_text = stackwalk.communicate()
852 if stackwalk.returncode != 0:
853 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
854 error_text, stackwalk.returncode))
855
856 return to_return
857
858 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800859 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 15:31:36 -0400860 """Return a string representing the latest build for a given target.
861
862 Args:
863 target: The build target, typically a combination of the board and the
864 type of build e.g. x86-mario-release.
865 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
866 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-07 18:21:26 -0800867
Scott Zawalski16954532012-03-20 15:31:36 -0400868 Returns:
869 A string representation of the latest build if one exists, i.e.
870 R19-1993.0.0-a1-b1480.
871 An empty string if no latest could be found.
872 """
Don Garrettf84631a2014-01-07 18:21:26 -0800873 if not kwargs:
Scott Zawalski16954532012-03-20 15:31:36 -0400874 return _PrintDocStringAsHTML(self.latestbuild)
875
Don Garrettf84631a2014-01-07 18:21:26 -0800876 if 'target' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -0700877 raise common_util.DevServerHTTPError(500, 'Error: target= is required!')
Dan Shi61305df2015-10-26 16:52:35 -0700878
879 if _is_android_build_request(kwargs):
880 branch = kwargs.get('branch', None)
881 target = kwargs.get('target', None)
882 if not target or not branch:
883 raise DevServerError(
884 'Both target and branch must be specified to query for the latest '
885 'Android build.')
886 return android_build.BuildAccessor.GetLatestBuildID(target, branch)
887
Scott Zawalski16954532012-03-20 15:31:36 -0400888 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700889 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-07 18:21:26 -0800890 updater.static_dir, kwargs['target'],
891 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700892 except common_util.CommonUtilError as errmsg:
Chris Sosa4b951602014-04-09 20:26:07 -0700893 raise common_util.DevServerHTTPError(500, str(errmsg))
Scott Zawalski16954532012-03-20 15:31:36 -0400894
895 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800896 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500897 """Return a control file or a list of all known control files.
898
899 Example URL:
900 To List all control files:
beepsbd337242013-07-09 22:44:06 -0700901 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
902 To List all control files for, say, the bvt suite:
903 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -0500904 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500905 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 -0500906
907 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500908 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500909 control_path: If you want the contents of a control file set this
910 to the path. E.g. client/site_tests/sleeptest/control
911 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -0700912 suite_name: If control_path is not specified but a suite_name is
913 specified, list the control files belonging to that suite instead of
914 all control files. The empty string for suite_name will list all control
915 files for the build.
Don Garrettf84631a2014-01-07 18:21:26 -0800916
Scott Zawalski4647ce62012-01-03 17:17:28 -0500917 Returns:
918 Contents of a control file if control_path is provided.
919 A list of control files if no control_path is provided.
920 """
Don Garrettf84631a2014-01-07 18:21:26 -0800921 if not kwargs:
Scott Zawalski4647ce62012-01-03 17:17:28 -0500922 return _PrintDocStringAsHTML(self.controlfiles)
923
Don Garrettf84631a2014-01-07 18:21:26 -0800924 if 'build' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -0700925 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -0500926
Don Garrettf84631a2014-01-07 18:21:26 -0800927 if 'control_path' not in kwargs:
928 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-09 22:44:06 -0700929 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-07 18:21:26 -0800930 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-09 22:44:06 -0700931 else:
932 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-07 18:21:26 -0800933 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -0500934 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700935 return common_util.GetControlFile(
Don Garrettf84631a2014-01-07 18:21:26 -0800936 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -0800937
938 @cherrypy.expose
Simran Basi99e63c02014-05-20 10:39:52 -0700939 def xbuddy_translate(self, *args, **kwargs):
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700940 """Translates an xBuddy path to a real path to artifact if it exists.
941
942 Args:
Simran Basi99e63c02014-05-20 10:39:52 -0700943 args: An xbuddy path in the form of {local|remote}/build_id/artifact.
944 Local searches the devserver's static directory. Remote searches a
945 Google Storage image archive.
946
947 Kwargs:
948 image_dir: Google Storage image archive to search in if requesting a
949 remote artifact. If none uses the default bucket.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700950
951 Returns:
Simran Basi99e63c02014-05-20 10:39:52 -0700952 String in the format of build_id/artifact as stored on the local server
953 or in Google Storage.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700954 """
Simran Basi99e63c02014-05-20 10:39:52 -0700955 build_id, filename = self._xbuddy.Translate(
Gabe Black3b567202015-09-23 14:07:59 -0700956 args, image_dir=kwargs.get('image_dir'))
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700957 response = os.path.join(build_id, filename)
958 _Log('Path translation requested, returning: %s', response)
959 return response
960
961 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -0700962 def xbuddy(self, *args, **kwargs):
963 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -0700964
965 Args:
joycheneaf4cfc2013-07-02 08:38:57 -0700966 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -0700967 components of the path. The path can be understood as
968 "{local|remote}/build_id/artifact" where build_id is composed of
969 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -0700970
joychen121fc9b2013-08-02 14:30:30 -0700971 The first path element is optional, and can be "remote" or "local"
972 If local (the default), devserver will not attempt to access Google
973 Storage, and will only search the static directory for the files.
974 If remote, devserver will try to obtain the artifact off GS if it's
975 not found locally.
976 The board is the familiar board name, optionally suffixed.
977 The version can be the google storage version number, and may also be
978 any of a number of xBuddy defined version aliases that will be
979 translated into the latest built image that fits the description.
980 Defaults to latest.
981 The artifact is one of a number of image or artifact aliases used by
982 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -0700983
984 Kwargs:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800985 for_update: {true|false}
986 if true, pregenerates the update payloads for the image,
987 and returns the update uri to pass to the
988 update_engine_client.
joychen3cb228e2013-06-12 12:13:13 -0700989 return_dir: {true|false}
990 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800991 relative_path: {true|false}
992 if set to true, returns the relative path to the payload
993 directory from static_dir.
joychen3cb228e2013-06-12 12:13:13 -0700994 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -0700995 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -0700996 or
joycheneaf4cfc2013-07-02 08:38:57 -0700997 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -0700998
999 Returns:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001000 If |for_update|, returns a redirect to the image or update file
1001 on the devserver. E.g.,
1002 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
1003 chromium-test-image.bin
1004 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
1005 http://host:port/static/x86-generic-release/R26-4000.0.0/
1006 If |relative_path| is true, return a relative path the folder where the
1007 payloads are. E.g.,
1008 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 12:13:13 -07001009 """
Chris Sosa75490802013-09-30 17:21:45 -07001010 boolean_string = kwargs.get('for_update')
1011 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001012 boolean_string = kwargs.get('return_dir')
1013 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
1014 boolean_string = kwargs.get('relative_path')
1015 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -07001016
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001017 if return_dir and relative_path:
Chris Sosa4b951602014-04-09 20:26:07 -07001018 raise common_util.DevServerHTTPError(
1019 500, 'Cannot specify both return_dir and relative_path')
Chris Sosa75490802013-09-30 17:21:45 -07001020
1021 # For updates, we optimize downloading of test images.
1022 file_name = None
1023 build_id = None
1024 if for_update:
1025 try:
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001026 build_id = self._xbuddy.StageTestArtifactsForUpdate(args)
Chris Sosa75490802013-09-30 17:21:45 -07001027 except build_artifact.ArtifactDownloadError:
1028 build_id = None
1029
1030 if not build_id:
1031 build_id, file_name = self._xbuddy.Get(args)
1032
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001033 if for_update:
1034 _Log('Payload generation triggered by request')
1035 # Forces payload to be in cache and symlinked into build_id dir.
Chris Sosa75490802013-09-30 17:21:45 -07001036 updater.GetUpdateForLabel(autoupdate.FORCED_UPDATE, build_id,
1037 image_name=file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001038
1039 response = None
1040 if return_dir:
1041 response = os.path.join(cherrypy.request.base, 'static', build_id)
1042 _Log('Directory requested, returning: %s', response)
1043 elif relative_path:
1044 response = build_id
1045 _Log('Relative path requested, returning: %s', response)
1046 elif for_update:
1047 response = os.path.join(cherrypy.request.base, 'update', build_id)
1048 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 12:13:13 -07001049 else:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001050 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 14:30:30 -07001051 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001052 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 14:30:30 -07001053 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -07001054
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001055 return response
1056
joychen3cb228e2013-06-12 12:13:13 -07001057 @cherrypy.expose
1058 def xbuddy_list(self):
1059 """Lists the currently available images & time since last access.
1060
Gilad Arnold452fd272014-02-04 11:09:28 -08001061 Returns:
1062 A string representation of a list of tuples [(build_id, time since last
1063 access),...]
joychen3cb228e2013-06-12 12:13:13 -07001064 """
1065 return self._xbuddy.List()
1066
1067 @cherrypy.expose
1068 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 11:09:28 -08001069 """Returns the number of images cached by xBuddy."""
joychen3cb228e2013-06-12 12:13:13 -07001070 return self._xbuddy.Capacity()
1071
1072 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001073 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001074 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001075 return ('Welcome to the Dev Server!<br>\n'
1076 '<br>\n'
1077 'Here are the available methods, click for documentation:<br>\n'
1078 '<br>\n'
1079 '%s' %
1080 '<br>\n'.join(
1081 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001082 for name in _FindExposedMethods(
1083 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001084
1085 @cherrypy.expose
1086 def doc(self, *args):
1087 """Shows the documentation for available methods / URLs.
1088
1089 Example:
1090 http://myhost/doc/update
1091 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001092 name = '/'.join(args)
1093 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001094 if not method:
1095 raise DevServerError("No exposed method named `%s'" % name)
1096 if not method.__doc__:
1097 raise DevServerError("No documentation for exposed method `%s'" % name)
1098 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -07001099
Dale Curtisc9aaf3a2011-08-09 15:47:40 -07001100 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001101 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001102 """Handles an update check from a Chrome OS client.
1103
1104 The HTTP request should contain the standard Omaha-style XML blob. The URL
1105 line may contain an additional intermediate path to the update payload.
1106
joychen121fc9b2013-08-02 14:30:30 -07001107 This request can be handled in one of 4 ways, depending on the devsever
1108 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -07001109
joychen121fc9b2013-08-02 14:30:30 -07001110 1. No intermediate path
1111 If no intermediate path is given, the default behavior is to generate an
1112 update payload from the latest test image locally built for the board
1113 specified in the xml. Devserver serves the generated payload.
1114
1115 2. Path explicitly invokes XBuddy
1116 If there is a path given, it can explicitly invoke xbuddy by prefixing it
1117 with 'xbuddy'. This path is then used to acquire an image binary for the
1118 devserver to generate an update payload from. Devserver then serves this
1119 payload.
1120
1121 3. Path is left for the devserver to interpret.
1122 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
1123 to generate a payload from the test image in that directory and serve it.
1124
1125 4. The devserver is in a 'forced' mode. TO BE DEPRECATED
1126 This comes from the usage of --forced_payload or --image when starting the
1127 devserver. No matter what path (or no path) gets passed in, devserver will
1128 serve the update payload (--forced_payload) or generate an update payload
1129 from the image (--image).
1130
1131 Examples:
1132 1. No intermediate path
1133 update_engine_client --omaha_url=http://myhost/update
1134 This generates an update payload from the latest test image locally built
1135 for the board specified in the xml.
1136
1137 2. Explicitly invoke xbuddy
1138 update_engine_client --omaha_url=
1139 http://myhost/update/xbuddy/remote/board/version/dev
1140 This would go to GS to download the dev image for the board, from which
1141 the devserver would generate a payload to serve.
1142
1143 3. Give a path for devserver to interpret
1144 update_engine_client --omaha_url=http://myhost/update/some/random/path
1145 This would attempt, in order to:
1146 a) Generate an update from a test image binary if found in
1147 static_dir/some/random/path.
1148 b) Serve an update payload found in static_dir/some/random/path.
1149 c) Hope that some/random/path takes the form "board/version" and
1150 and attempt to download an update payload for that board/version
1151 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001152 """
joychen121fc9b2013-08-02 14:30:30 -07001153 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -08001154 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -07001155 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-11 19:49:01 -07001156
joychen121fc9b2013-08-02 14:30:30 -07001157 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001158
Dan Shiafd0e492015-05-27 14:23:51 -07001159 @require_psutil()
1160 def _get_io_stats(self):
1161 """Get the IO stats as a dictionary.
1162
Gabe Black3b567202015-09-23 14:07:59 -07001163 Returns:
1164 A dictionary of IO stats collected by psutil.
Dan Shiafd0e492015-05-27 14:23:51 -07001165
1166 """
1167 return {'disk_read_bytes_per_second': self.disk_read_bytes_per_sec,
1168 'disk_write_bytes_per_second': self.disk_write_bytes_per_sec,
1169 'disk_total_bytes_per_second': (self.disk_read_bytes_per_sec +
1170 self.disk_write_bytes_per_sec),
1171 'network_sent_bytes_per_second': self.network_sent_bytes_per_sec,
1172 'network_recv_bytes_per_second': self.network_recv_bytes_per_sec,
1173 'network_total_bytes_per_second': (self.network_sent_bytes_per_sec +
1174 self.network_recv_bytes_per_sec),
1175 'cpu_percent': psutil.cpu_percent(),}
1176
Dan Shif5ce2de2013-04-25 16:06:32 -07001177 @cherrypy.expose
1178 def check_health(self):
1179 """Collect the health status of devserver to see if it's ready for staging.
1180
Gilad Arnold452fd272014-02-04 11:09:28 -08001181 Returns:
1182 A JSON dictionary containing all or some of the following fields:
1183 free_disk (int): free disk space in GB
1184 staging_thread_count (int): number of devserver threads currently staging
1185 an image
Dan Shif5ce2de2013-04-25 16:06:32 -07001186 """
1187 # Get free disk space.
1188 stat = os.statvfs(updater.static_dir)
1189 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
Dan Shid76e6bb2016-01-28 22:28:51 -08001190 try:
1191 apache_client_count = int(subprocess.check_output('pgrep -fc apache',
1192 shell=True))
1193 except subprocess.CalledProcessError:
1194 apache_client_count = 0
1195 try:
1196 telemetry_test_count = int(subprocess.check_output(
1197 'pgrep -fc "python.*telemetry"', shell=True))
1198 except subprocess.CalledProcessError:
1199 telemetry_test_count = 0
Dan Shif5ce2de2013-04-25 16:06:32 -07001200
Dan Shiafd0e492015-05-27 14:23:51 -07001201 health_data = {
Dan Shif5ce2de2013-04-25 16:06:32 -07001202 'free_disk': free_disk,
Dan Shid76e6bb2016-01-28 22:28:51 -08001203 'staging_thread_count': DevServerRoot._staging_thread_count,
1204 'apache_client_count': apache_client_count,
1205 'telemetry_test_count': telemetry_test_count}
Dan Shiafd0e492015-05-27 14:23:51 -07001206 health_data.update(self._get_io_stats() or {})
1207
1208 return json.dumps(health_data)
Dan Shif5ce2de2013-04-25 16:06:32 -07001209
1210
Chris Sosadbc20082012-12-10 13:39:11 -08001211def _CleanCache(cache_dir, wipe):
1212 """Wipes any excess cached items in the cache_dir.
1213
1214 Args:
1215 cache_dir: the directory we are wiping from.
1216 wipe: If True, wipe all the contents -- not just the excess.
1217 """
1218 if wipe:
1219 # Clear the cache and exit on error.
1220 cmd = 'rm -rf %s/*' % cache_dir
1221 if os.system(cmd) != 0:
1222 _Log('Failed to clear the cache with %s' % cmd)
1223 sys.exit(1)
1224 else:
1225 # Clear all but the last N cached updates
1226 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
1227 (cache_dir, CACHED_ENTRIES))
1228 if os.system(cmd) != 0:
1229 _Log('Failed to clean up old delta cache files with %s' % cmd)
1230 sys.exit(1)
1231
1232
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001233def _AddTestingOptions(parser):
1234 group = optparse.OptionGroup(
1235 parser, 'Advanced Testing Options', 'These are used by test scripts and '
1236 'developers writing integration tests utilizing the devserver. They are '
1237 'not intended to be really used outside the scope of someone '
1238 'knowledgable about the test.')
1239 group.add_option('--exit',
1240 action='store_true',
1241 help='do not start the server (yet pregenerate/clear cache)')
1242 group.add_option('--host_log',
1243 action='store_true', default=False,
1244 help='record history of host update events (/api/hostlog)')
1245 group.add_option('--max_updates',
Gabe Black3b567202015-09-23 14:07:59 -07001246 metavar='NUM', default=-1, type='int',
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001247 help='maximum number of update checks handled positively '
1248 '(default: unlimited)')
1249 group.add_option('--private_key',
1250 metavar='PATH', default=None,
1251 help='path to the private key in pem format. If this is set '
1252 'the devserver will generate update payloads that are '
1253 'signed with this key.')
David Zeuthen52ccd012013-10-31 12:58:26 -07001254 group.add_option('--private_key_for_metadata_hash_signature',
1255 metavar='PATH', default=None,
1256 help='path to the private key in pem format. If this is set '
1257 'the devserver will sign the metadata hash with the given '
1258 'key and transmit in the Omaha-style XML response.')
1259 group.add_option('--public_key',
1260 metavar='PATH', default=None,
1261 help='path to the public key in pem format. If this is set '
1262 'the devserver will transmit a base64 encoded version of '
1263 'the content in the Omaha-style XML response.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001264 group.add_option('--proxy_port',
1265 metavar='PORT', default=None, type='int',
1266 help='port to have the client connect to -- basically the '
1267 'devserver lies to the update to tell it to get the payload '
1268 'from a different port that will proxy the request back to '
1269 'the devserver. The proxy must be managed outside the '
1270 'devserver.')
1271 group.add_option('--remote_payload',
1272 action='store_true', default=False,
Chris Sosa4b951602014-04-09 20:26:07 -07001273 help='Payload is being served from a remote machine. With '
1274 'this setting enabled, this devserver instance serves as '
1275 'just an Omaha server instance. In this mode, the '
1276 'devserver enforces a few extra components of the Omaha '
Chris Sosafc715442014-04-09 20:45:23 -07001277 'protocol, such as hardware class, being sent.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001278 group.add_option('-u', '--urlbase',
1279 metavar='URL',
Gabe Black3b567202015-09-23 14:07:59 -07001280 help='base URL for update images, other than the '
1281 'devserver. Use in conjunction with remote_payload.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001282 parser.add_option_group(group)
1283
1284
1285def _AddUpdateOptions(parser):
1286 group = optparse.OptionGroup(
1287 parser, 'Autoupdate Options', 'These options can be used to change '
1288 'how the devserver either generates or serve update payloads. Please '
1289 'note that all of these option affect how a payload is generated and so '
1290 'do not work in archive-only mode.')
1291 group.add_option('--board',
1292 help='By default the devserver will create an update '
1293 'payload from the latest image built for the board '
1294 'a device that is requesting an update has. When we '
1295 'pre-generate an update (see below) and we do not specify '
1296 'another update_type option like image or payload, the '
1297 'devserver needs to know the board to generate the latest '
1298 'image for. This is that board.')
1299 group.add_option('--critical_update',
1300 action='store_true', default=False,
1301 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001302 group.add_option('--image',
1303 metavar='FILE',
1304 help='Generate and serve an update using this image to any '
1305 'device that requests an update.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001306 group.add_option('--payload',
1307 metavar='PATH',
1308 help='use the update payload from specified directory '
1309 '(update.gz).')
1310 group.add_option('-p', '--pregenerate_update',
1311 action='store_true', default=False,
1312 help='pre-generate the update payload before accepting '
1313 'update requests. Useful to help debug payload generation '
1314 'issues quickly. Also if an update payload will take a '
1315 'long time to generate, a client may timeout if you do not'
1316 'pregenerate the update.')
1317 group.add_option('--src_image',
1318 metavar='PATH', default='',
1319 help='If specified, delta updates will be generated using '
1320 'this image as the source image. Delta updates are when '
1321 'you are updating from a "source image" to a another '
1322 'image.')
1323 parser.add_option_group(group)
1324
1325
1326def _AddProductionOptions(parser):
1327 group = optparse.OptionGroup(
1328 parser, 'Advanced Server Options', 'These options can be used to changed '
1329 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001330 group.add_option('--clear_cache',
1331 action='store_true', default=False,
1332 help='At startup, removes all cached entries from the'
1333 'devserver\'s cache.')
1334 group.add_option('--logfile',
1335 metavar='PATH',
1336 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -07001337 group.add_option('--pidfile',
1338 metavar='PATH',
1339 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 11:04:13 -08001340 group.add_option('--portfile',
1341 metavar='PATH',
1342 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001343 group.add_option('--production',
1344 action='store_true', default=False,
1345 help='have the devserver use production values when '
1346 'starting up. This includes using more threads and '
1347 'performing less logging.')
1348 parser.add_option_group(group)
1349
1350
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001351def _MakeLogHandler(logfile):
1352 """Create a LogHandler instance used to log all messages."""
1353 hdlr_cls = handlers.TimedRotatingFileHandler
1354 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
1355 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -07001356 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001357 return hdlr
1358
1359
Chris Sosacde6bf42012-05-31 18:36:39 -07001360def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001361 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -08001362 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -07001363
1364 # get directory that the devserver is run from
1365 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -07001366 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -07001367 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001368 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -07001369 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -07001370 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001371 parser.add_option('--port',
1372 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 13:13:28 -08001373 help=('port for the dev server to use; if zero, binds to '
1374 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001375 parser.add_option('-t', '--test_image',
1376 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -07001377 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -07001378 parser.add_option('-x', '--xbuddy_manage_builds',
1379 action='store_true',
1380 default=False,
1381 help='If set, allow xbuddy to manage images in'
1382 'build/images.')
Dan Shi72b16132015-10-08 12:10:33 -07001383 parser.add_option('-a', '--android_build_credential',
1384 default=None,
1385 help='Path to a json file which contains the credential '
1386 'needed to access Android builds.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001387 _AddProductionOptions(parser)
1388 _AddUpdateOptions(parser)
1389 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -07001390 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001391
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001392 # Handle options that must be set globally in cherrypy. Do this
1393 # work up front, because calls to _Log() below depend on this
1394 # initialization.
1395 if options.production:
1396 cherrypy.config.update({'environment': 'production'})
1397 if not options.logfile:
1398 cherrypy.config.update({'log.screen': True})
1399 else:
1400 cherrypy.config.update({'log.error_file': '',
1401 'log.access_file': ''})
1402 hdlr = _MakeLogHandler(options.logfile)
1403 # Pylint can't seem to process these two calls properly
1404 # pylint: disable=E1101
1405 cherrypy.log.access_log.addHandler(hdlr)
1406 cherrypy.log.error_log.addHandler(hdlr)
1407 # pylint: enable=E1101
1408
joychened64b222013-06-21 16:39:34 -07001409 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001410 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001411
joychened64b222013-06-21 16:39:34 -07001412 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001413 # If our devserver is only supposed to serve payloads, we shouldn't be
1414 # mucking with the cache at all. If the devserver hadn't previously
1415 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001416 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001417 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001418 else:
1419 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001420
Chris Sosadbc20082012-12-10 13:39:11 -08001421 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 16:39:34 -07001422 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001423
joychen121fc9b2013-08-02 14:30:30 -07001424 _xbuddy = xbuddy.XBuddy(options.xbuddy_manage_builds,
1425 options.board,
joychen121fc9b2013-08-02 14:30:30 -07001426 static_dir=options.static_dir)
Chris Sosa75490802013-09-30 17:21:45 -07001427 if options.clear_cache and options.xbuddy_manage_builds:
1428 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 14:30:30 -07001429
Chris Sosa6a3697f2013-01-29 16:44:43 -08001430 # We allow global use here to share with cherrypy classes.
1431 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001432 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001433 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001434 _xbuddy,
joychened64b222013-06-21 16:39:34 -07001435 static_dir=options.static_dir,
Andrew de los Reyes52620802010-04-12 13:40:07 -07001436 urlbase=options.urlbase,
Chris Sosa5d342a22010-09-28 16:54:41 -07001437 forced_image=options.image,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001438 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001439 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-28 23:42:37 -07001440 src_image=options.src_image,
Chris Sosa08d55a22011-01-19 16:08:02 -08001441 board=options.board,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001442 copy_to_static_root=not options.exit,
1443 private_key=options.private_key,
Gabe Black3b567202015-09-23 14:07:59 -07001444 private_key_for_metadata_hash_signature=(
1445 options.private_key_for_metadata_hash_signature),
David Zeuthen52ccd012013-10-31 12:58:26 -07001446 public_key=options.public_key,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001447 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001448 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001449 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001450 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001451 )
Chris Sosa7c931362010-10-11 19:49:01 -07001452
Chris Sosa6a3697f2013-01-29 16:44:43 -08001453 if options.pregenerate_update:
1454 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 15:46:22 -07001455
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001456 if options.exit:
1457 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001458
joychen3cb228e2013-06-12 12:13:13 -07001459 dev_server = DevServerRoot(_xbuddy)
1460
Gilad Arnold11fbef42014-02-10 11:04:13 -08001461 # Patch CherryPy to support binding to any available port (--port=0).
1462 cherrypy_ext.ZeroPortPatcher.DoPatch(cherrypy)
1463
Chris Sosa855b8932013-08-21 13:24:55 -07001464 if options.pidfile:
1465 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1466
Gilad Arnold11fbef42014-02-10 11:04:13 -08001467 if options.portfile:
1468 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1469
Dan Shiafd5c6c2016-01-07 10:27:03 -08001470 if (options.android_build_credential and
1471 os.path.exists(options.android_build_credential)):
1472 try:
1473 with open(options.android_build_credential) as f:
1474 android_build.BuildAccessor.credential_info = json.load(f)
1475 except ValueError as e:
1476 _Log('Failed to load the android build credential: %s. Error: %s.' %
1477 (options.android_build_credential, e))
joychen3cb228e2013-06-12 12:13:13 -07001478 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001479
1480
1481if __name__ == '__main__':
1482 main()