blob: ebe46f210838833d2bf22a28f76b3460489bee42 [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.
Chris Sosa76e44b92013-01-31 12:11:38 -0800719
720 Example:
721 To download the autotest and test suites tarballs:
722 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
723 artifacts=autotest,test_suites
724 To download the full update payload:
725 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
726 artifacts=full_payload
Chris Sosa6b0c6172013-08-05 17:01:33 -0700727 To download just a file called blah.bin:
728 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
729 files=blah.bin
Chris Sosa76e44b92013-01-31 12:11:38 -0800730
731 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 16:39:34 -0700732 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 12:11:38 -0800733
734 Note for this example, relative path is the archive_url stripped of its
735 basename i.e. path/ in the examples above. Specific example:
736
737 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
738
739 Will get staged to:
740
joychened64b222013-06-21 16:39:34 -0700741 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Chris Sosa76e44b92013-01-31 12:11:38 -0800742 """
Gabe Black3b567202015-09-23 14:07:59 -0700743 dl, factory = _get_downloader_and_factory(kwargs)
744
Dan Shi59ae7092013-06-04 14:37:27 -0700745 with DevServerRoot._staging_thread_count_lock:
746 DevServerRoot._staging_thread_count += 1
747 try:
Gabe Black3b567202015-09-23 14:07:59 -0700748 async = kwargs.get('async', False)
749 dl.Download(factory, async=async)
Dan Shi59ae7092013-06-04 14:37:27 -0700750 finally:
751 with DevServerRoot._staging_thread_count_lock:
752 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800753 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700754
755 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -0800756 def setup_telemetry(self, **kwargs):
757 """Extracts and sets up telemetry
758
759 This method goes through the telemetry deps packages, and stages them on
760 the devserver to be used by the drones and the telemetry tests.
761
762 Args:
763 archive_url: Google Storage URL for the build.
764
765 Returns:
766 Path to the source folder for the telemetry codebase once it is staged.
767 """
Gabe Black3b567202015-09-23 14:07:59 -0700768 dl = _get_downloader(kwargs)
Simran Basi4baad082013-02-14 13:39:18 -0800769
Gabe Black3b567202015-09-23 14:07:59 -0700770 build_path = dl.GetBuildDir()
Simran Basi4baad082013-02-14 13:39:18 -0800771 deps_path = os.path.join(build_path, 'autotest/packages')
772 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
773 src_folder = os.path.join(telemetry_path, 'src')
774
775 with self._telemetry_lock_dict.lock(telemetry_path):
776 if os.path.exists(src_folder):
777 # Telemetry is already fully stage return
778 return src_folder
779
780 common_util.MkDirP(telemetry_path)
781
782 # Copy over the required deps tar balls to the telemetry directory.
783 for dep in TELEMETRY_DEPS:
784 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -0700785 if not os.path.exists(dep_path):
786 # This dep does not exist (could be new), do not extract it.
787 continue
Simran Basi4baad082013-02-14 13:39:18 -0800788 try:
789 common_util.ExtractTarball(dep_path, telemetry_path)
790 except common_util.CommonUtilError as e:
791 shutil.rmtree(telemetry_path)
792 raise DevServerError(str(e))
793
794 # By default all the tarballs extract to test_src but some parts of
795 # the telemetry code specifically hardcoded to exist inside of 'src'.
796 test_src = os.path.join(telemetry_path, 'test_src')
797 try:
798 shutil.move(test_src, src_folder)
799 except shutil.Error:
800 # This can occur if src_folder already exists. Remove and retry move.
801 shutil.rmtree(src_folder)
Gabe Black3b567202015-09-23 14:07:59 -0700802 raise DevServerError(
803 'Failure in telemetry setup for build %s. Appears that the '
804 'test_src to src move failed.' % dl.GetBuild())
Simran Basi4baad082013-02-14 13:39:18 -0800805
806 return src_folder
807
808 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800809 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -0700810 """Symbolicates a minidump using pre-downloaded symbols, returns it.
811
812 Callers will need to POST to this URL with a body of MIME-type
813 "multipart/form-data".
814 The body should include a single argument, 'minidump', containing the
815 binary-formatted minidump to symbolicate.
816
Chris Masone816e38c2012-05-02 12:22:36 -0700817 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -0800818 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -0700819 minidump: The binary minidump file to symbolicate.
820 """
Gabe Black3b567202015-09-23 14:07:59 -0700821 kwargs['artifacts'] = 'symbols'
822 dl = _get_downloader(kwargs)
823
Chris Sosa76e44b92013-01-31 12:11:38 -0800824 # Ensure the symbols have been staged.
Gabe Black3b567202015-09-23 14:07:59 -0700825 if self.stage(**kwargs) != 'Success':
826 raise DevServerError('Failed to stage symbols for %s' %
827 dl.DescribeSource())
Chris Sosa76e44b92013-01-31 12:11:38 -0800828
Chris Masone816e38c2012-05-02 12:22:36 -0700829 to_return = ''
830 with tempfile.NamedTemporaryFile() as local:
831 while True:
832 data = minidump.file.read(8192)
833 if not data:
834 break
835 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -0800836
Chris Masone816e38c2012-05-02 12:22:36 -0700837 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -0800838
Gabe Black3b567202015-09-23 14:07:59 -0700839 symbols_directory = os.path.join(dl.GetBuildDir(), 'debug', 'breakpad')
Chris Sosa76e44b92013-01-31 12:11:38 -0800840
841 stackwalk = subprocess.Popen(
842 ['minidump_stackwalk', local.name, symbols_directory],
843 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
844
Chris Masone816e38c2012-05-02 12:22:36 -0700845 to_return, error_text = stackwalk.communicate()
846 if stackwalk.returncode != 0:
847 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
848 error_text, stackwalk.returncode))
849
850 return to_return
851
852 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800853 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 15:31:36 -0400854 """Return a string representing the latest build for a given target.
855
856 Args:
857 target: The build target, typically a combination of the board and the
858 type of build e.g. x86-mario-release.
859 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
860 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-07 18:21:26 -0800861
Scott Zawalski16954532012-03-20 15:31:36 -0400862 Returns:
863 A string representation of the latest build if one exists, i.e.
864 R19-1993.0.0-a1-b1480.
865 An empty string if no latest could be found.
866 """
Don Garrettf84631a2014-01-07 18:21:26 -0800867 if not kwargs:
Scott Zawalski16954532012-03-20 15:31:36 -0400868 return _PrintDocStringAsHTML(self.latestbuild)
869
Don Garrettf84631a2014-01-07 18:21:26 -0800870 if 'target' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -0700871 raise common_util.DevServerHTTPError(500, 'Error: target= is required!')
Dan Shi61305df2015-10-26 16:52:35 -0700872
873 if _is_android_build_request(kwargs):
874 branch = kwargs.get('branch', None)
875 target = kwargs.get('target', None)
876 if not target or not branch:
877 raise DevServerError(
878 'Both target and branch must be specified to query for the latest '
879 'Android build.')
880 return android_build.BuildAccessor.GetLatestBuildID(target, branch)
881
Scott Zawalski16954532012-03-20 15:31:36 -0400882 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700883 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-07 18:21:26 -0800884 updater.static_dir, kwargs['target'],
885 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700886 except common_util.CommonUtilError as errmsg:
Chris Sosa4b951602014-04-09 20:26:07 -0700887 raise common_util.DevServerHTTPError(500, str(errmsg))
Scott Zawalski16954532012-03-20 15:31:36 -0400888
889 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800890 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500891 """Return a control file or a list of all known control files.
892
893 Example URL:
894 To List all control files:
beepsbd337242013-07-09 22:44:06 -0700895 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
896 To List all control files for, say, the bvt suite:
897 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -0500898 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500899 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 -0500900
901 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500902 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500903 control_path: If you want the contents of a control file set this
904 to the path. E.g. client/site_tests/sleeptest/control
905 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -0700906 suite_name: If control_path is not specified but a suite_name is
907 specified, list the control files belonging to that suite instead of
908 all control files. The empty string for suite_name will list all control
909 files for the build.
Don Garrettf84631a2014-01-07 18:21:26 -0800910
Scott Zawalski4647ce62012-01-03 17:17:28 -0500911 Returns:
912 Contents of a control file if control_path is provided.
913 A list of control files if no control_path is provided.
914 """
Don Garrettf84631a2014-01-07 18:21:26 -0800915 if not kwargs:
Scott Zawalski4647ce62012-01-03 17:17:28 -0500916 return _PrintDocStringAsHTML(self.controlfiles)
917
Don Garrettf84631a2014-01-07 18:21:26 -0800918 if 'build' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -0700919 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -0500920
Don Garrettf84631a2014-01-07 18:21:26 -0800921 if 'control_path' not in kwargs:
922 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-09 22:44:06 -0700923 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-07 18:21:26 -0800924 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-09 22:44:06 -0700925 else:
926 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-07 18:21:26 -0800927 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -0500928 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700929 return common_util.GetControlFile(
Don Garrettf84631a2014-01-07 18:21:26 -0800930 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -0800931
932 @cherrypy.expose
Simran Basi99e63c02014-05-20 10:39:52 -0700933 def xbuddy_translate(self, *args, **kwargs):
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700934 """Translates an xBuddy path to a real path to artifact if it exists.
935
936 Args:
Simran Basi99e63c02014-05-20 10:39:52 -0700937 args: An xbuddy path in the form of {local|remote}/build_id/artifact.
938 Local searches the devserver's static directory. Remote searches a
939 Google Storage image archive.
940
941 Kwargs:
942 image_dir: Google Storage image archive to search in if requesting a
943 remote artifact. If none uses the default bucket.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700944
945 Returns:
Simran Basi99e63c02014-05-20 10:39:52 -0700946 String in the format of build_id/artifact as stored on the local server
947 or in Google Storage.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700948 """
Simran Basi99e63c02014-05-20 10:39:52 -0700949 build_id, filename = self._xbuddy.Translate(
Gabe Black3b567202015-09-23 14:07:59 -0700950 args, image_dir=kwargs.get('image_dir'))
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700951 response = os.path.join(build_id, filename)
952 _Log('Path translation requested, returning: %s', response)
953 return response
954
955 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -0700956 def xbuddy(self, *args, **kwargs):
957 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -0700958
959 Args:
joycheneaf4cfc2013-07-02 08:38:57 -0700960 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -0700961 components of the path. The path can be understood as
962 "{local|remote}/build_id/artifact" where build_id is composed of
963 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -0700964
joychen121fc9b2013-08-02 14:30:30 -0700965 The first path element is optional, and can be "remote" or "local"
966 If local (the default), devserver will not attempt to access Google
967 Storage, and will only search the static directory for the files.
968 If remote, devserver will try to obtain the artifact off GS if it's
969 not found locally.
970 The board is the familiar board name, optionally suffixed.
971 The version can be the google storage version number, and may also be
972 any of a number of xBuddy defined version aliases that will be
973 translated into the latest built image that fits the description.
974 Defaults to latest.
975 The artifact is one of a number of image or artifact aliases used by
976 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -0700977
978 Kwargs:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800979 for_update: {true|false}
980 if true, pregenerates the update payloads for the image,
981 and returns the update uri to pass to the
982 update_engine_client.
joychen3cb228e2013-06-12 12:13:13 -0700983 return_dir: {true|false}
984 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800985 relative_path: {true|false}
986 if set to true, returns the relative path to the payload
987 directory from static_dir.
joychen3cb228e2013-06-12 12:13:13 -0700988 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -0700989 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -0700990 or
joycheneaf4cfc2013-07-02 08:38:57 -0700991 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -0700992
993 Returns:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800994 If |for_update|, returns a redirect to the image or update file
995 on the devserver. E.g.,
996 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
997 chromium-test-image.bin
998 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
999 http://host:port/static/x86-generic-release/R26-4000.0.0/
1000 If |relative_path| is true, return a relative path the folder where the
1001 payloads are. E.g.,
1002 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 12:13:13 -07001003 """
Chris Sosa75490802013-09-30 17:21:45 -07001004 boolean_string = kwargs.get('for_update')
1005 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001006 boolean_string = kwargs.get('return_dir')
1007 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
1008 boolean_string = kwargs.get('relative_path')
1009 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -07001010
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001011 if return_dir and relative_path:
Chris Sosa4b951602014-04-09 20:26:07 -07001012 raise common_util.DevServerHTTPError(
1013 500, 'Cannot specify both return_dir and relative_path')
Chris Sosa75490802013-09-30 17:21:45 -07001014
1015 # For updates, we optimize downloading of test images.
1016 file_name = None
1017 build_id = None
1018 if for_update:
1019 try:
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001020 build_id = self._xbuddy.StageTestArtifactsForUpdate(args)
Chris Sosa75490802013-09-30 17:21:45 -07001021 except build_artifact.ArtifactDownloadError:
1022 build_id = None
1023
1024 if not build_id:
1025 build_id, file_name = self._xbuddy.Get(args)
1026
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001027 if for_update:
1028 _Log('Payload generation triggered by request')
1029 # Forces payload to be in cache and symlinked into build_id dir.
Chris Sosa75490802013-09-30 17:21:45 -07001030 updater.GetUpdateForLabel(autoupdate.FORCED_UPDATE, build_id,
1031 image_name=file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001032
1033 response = None
1034 if return_dir:
1035 response = os.path.join(cherrypy.request.base, 'static', build_id)
1036 _Log('Directory requested, returning: %s', response)
1037 elif relative_path:
1038 response = build_id
1039 _Log('Relative path requested, returning: %s', response)
1040 elif for_update:
1041 response = os.path.join(cherrypy.request.base, 'update', build_id)
1042 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 12:13:13 -07001043 else:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001044 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 14:30:30 -07001045 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001046 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 14:30:30 -07001047 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -07001048
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001049 return response
1050
joychen3cb228e2013-06-12 12:13:13 -07001051 @cherrypy.expose
1052 def xbuddy_list(self):
1053 """Lists the currently available images & time since last access.
1054
Gilad Arnold452fd272014-02-04 11:09:28 -08001055 Returns:
1056 A string representation of a list of tuples [(build_id, time since last
1057 access),...]
joychen3cb228e2013-06-12 12:13:13 -07001058 """
1059 return self._xbuddy.List()
1060
1061 @cherrypy.expose
1062 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 11:09:28 -08001063 """Returns the number of images cached by xBuddy."""
joychen3cb228e2013-06-12 12:13:13 -07001064 return self._xbuddy.Capacity()
1065
1066 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001067 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001068 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001069 return ('Welcome to the Dev Server!<br>\n'
1070 '<br>\n'
1071 'Here are the available methods, click for documentation:<br>\n'
1072 '<br>\n'
1073 '%s' %
1074 '<br>\n'.join(
1075 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001076 for name in _FindExposedMethods(
1077 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001078
1079 @cherrypy.expose
1080 def doc(self, *args):
1081 """Shows the documentation for available methods / URLs.
1082
1083 Example:
1084 http://myhost/doc/update
1085 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001086 name = '/'.join(args)
1087 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001088 if not method:
1089 raise DevServerError("No exposed method named `%s'" % name)
1090 if not method.__doc__:
1091 raise DevServerError("No documentation for exposed method `%s'" % name)
1092 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -07001093
Dale Curtisc9aaf3a2011-08-09 15:47:40 -07001094 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001095 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001096 """Handles an update check from a Chrome OS client.
1097
1098 The HTTP request should contain the standard Omaha-style XML blob. The URL
1099 line may contain an additional intermediate path to the update payload.
1100
joychen121fc9b2013-08-02 14:30:30 -07001101 This request can be handled in one of 4 ways, depending on the devsever
1102 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -07001103
joychen121fc9b2013-08-02 14:30:30 -07001104 1. No intermediate path
1105 If no intermediate path is given, the default behavior is to generate an
1106 update payload from the latest test image locally built for the board
1107 specified in the xml. Devserver serves the generated payload.
1108
1109 2. Path explicitly invokes XBuddy
1110 If there is a path given, it can explicitly invoke xbuddy by prefixing it
1111 with 'xbuddy'. This path is then used to acquire an image binary for the
1112 devserver to generate an update payload from. Devserver then serves this
1113 payload.
1114
1115 3. Path is left for the devserver to interpret.
1116 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
1117 to generate a payload from the test image in that directory and serve it.
1118
1119 4. The devserver is in a 'forced' mode. TO BE DEPRECATED
1120 This comes from the usage of --forced_payload or --image when starting the
1121 devserver. No matter what path (or no path) gets passed in, devserver will
1122 serve the update payload (--forced_payload) or generate an update payload
1123 from the image (--image).
1124
1125 Examples:
1126 1. No intermediate path
1127 update_engine_client --omaha_url=http://myhost/update
1128 This generates an update payload from the latest test image locally built
1129 for the board specified in the xml.
1130
1131 2. Explicitly invoke xbuddy
1132 update_engine_client --omaha_url=
1133 http://myhost/update/xbuddy/remote/board/version/dev
1134 This would go to GS to download the dev image for the board, from which
1135 the devserver would generate a payload to serve.
1136
1137 3. Give a path for devserver to interpret
1138 update_engine_client --omaha_url=http://myhost/update/some/random/path
1139 This would attempt, in order to:
1140 a) Generate an update from a test image binary if found in
1141 static_dir/some/random/path.
1142 b) Serve an update payload found in static_dir/some/random/path.
1143 c) Hope that some/random/path takes the form "board/version" and
1144 and attempt to download an update payload for that board/version
1145 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001146 """
joychen121fc9b2013-08-02 14:30:30 -07001147 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -08001148 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -07001149 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-11 19:49:01 -07001150
joychen121fc9b2013-08-02 14:30:30 -07001151 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001152
Dan Shiafd0e492015-05-27 14:23:51 -07001153 @require_psutil()
1154 def _get_io_stats(self):
1155 """Get the IO stats as a dictionary.
1156
Gabe Black3b567202015-09-23 14:07:59 -07001157 Returns:
1158 A dictionary of IO stats collected by psutil.
Dan Shiafd0e492015-05-27 14:23:51 -07001159
1160 """
1161 return {'disk_read_bytes_per_second': self.disk_read_bytes_per_sec,
1162 'disk_write_bytes_per_second': self.disk_write_bytes_per_sec,
1163 'disk_total_bytes_per_second': (self.disk_read_bytes_per_sec +
1164 self.disk_write_bytes_per_sec),
1165 'network_sent_bytes_per_second': self.network_sent_bytes_per_sec,
1166 'network_recv_bytes_per_second': self.network_recv_bytes_per_sec,
1167 'network_total_bytes_per_second': (self.network_sent_bytes_per_sec +
1168 self.network_recv_bytes_per_sec),
1169 'cpu_percent': psutil.cpu_percent(),}
1170
Dan Shif5ce2de2013-04-25 16:06:32 -07001171 @cherrypy.expose
1172 def check_health(self):
1173 """Collect the health status of devserver to see if it's ready for staging.
1174
Gilad Arnold452fd272014-02-04 11:09:28 -08001175 Returns:
1176 A JSON dictionary containing all or some of the following fields:
1177 free_disk (int): free disk space in GB
1178 staging_thread_count (int): number of devserver threads currently staging
1179 an image
Dan Shif5ce2de2013-04-25 16:06:32 -07001180 """
1181 # Get free disk space.
1182 stat = os.statvfs(updater.static_dir)
1183 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
1184
Dan Shiafd0e492015-05-27 14:23:51 -07001185 health_data = {
Dan Shif5ce2de2013-04-25 16:06:32 -07001186 'free_disk': free_disk,
Dan Shiafd0e492015-05-27 14:23:51 -07001187 'staging_thread_count': DevServerRoot._staging_thread_count}
1188 health_data.update(self._get_io_stats() or {})
1189
1190 return json.dumps(health_data)
Dan Shif5ce2de2013-04-25 16:06:32 -07001191
1192
Chris Sosadbc20082012-12-10 13:39:11 -08001193def _CleanCache(cache_dir, wipe):
1194 """Wipes any excess cached items in the cache_dir.
1195
1196 Args:
1197 cache_dir: the directory we are wiping from.
1198 wipe: If True, wipe all the contents -- not just the excess.
1199 """
1200 if wipe:
1201 # Clear the cache and exit on error.
1202 cmd = 'rm -rf %s/*' % cache_dir
1203 if os.system(cmd) != 0:
1204 _Log('Failed to clear the cache with %s' % cmd)
1205 sys.exit(1)
1206 else:
1207 # Clear all but the last N cached updates
1208 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
1209 (cache_dir, CACHED_ENTRIES))
1210 if os.system(cmd) != 0:
1211 _Log('Failed to clean up old delta cache files with %s' % cmd)
1212 sys.exit(1)
1213
1214
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001215def _AddTestingOptions(parser):
1216 group = optparse.OptionGroup(
1217 parser, 'Advanced Testing Options', 'These are used by test scripts and '
1218 'developers writing integration tests utilizing the devserver. They are '
1219 'not intended to be really used outside the scope of someone '
1220 'knowledgable about the test.')
1221 group.add_option('--exit',
1222 action='store_true',
1223 help='do not start the server (yet pregenerate/clear cache)')
1224 group.add_option('--host_log',
1225 action='store_true', default=False,
1226 help='record history of host update events (/api/hostlog)')
1227 group.add_option('--max_updates',
Gabe Black3b567202015-09-23 14:07:59 -07001228 metavar='NUM', default=-1, type='int',
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001229 help='maximum number of update checks handled positively '
1230 '(default: unlimited)')
1231 group.add_option('--private_key',
1232 metavar='PATH', default=None,
1233 help='path to the private key in pem format. If this is set '
1234 'the devserver will generate update payloads that are '
1235 'signed with this key.')
David Zeuthen52ccd012013-10-31 12:58:26 -07001236 group.add_option('--private_key_for_metadata_hash_signature',
1237 metavar='PATH', default=None,
1238 help='path to the private key in pem format. If this is set '
1239 'the devserver will sign the metadata hash with the given '
1240 'key and transmit in the Omaha-style XML response.')
1241 group.add_option('--public_key',
1242 metavar='PATH', default=None,
1243 help='path to the public key in pem format. If this is set '
1244 'the devserver will transmit a base64 encoded version of '
1245 'the content in the Omaha-style XML response.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001246 group.add_option('--proxy_port',
1247 metavar='PORT', default=None, type='int',
1248 help='port to have the client connect to -- basically the '
1249 'devserver lies to the update to tell it to get the payload '
1250 'from a different port that will proxy the request back to '
1251 'the devserver. The proxy must be managed outside the '
1252 'devserver.')
1253 group.add_option('--remote_payload',
1254 action='store_true', default=False,
Chris Sosa4b951602014-04-09 20:26:07 -07001255 help='Payload is being served from a remote machine. With '
1256 'this setting enabled, this devserver instance serves as '
1257 'just an Omaha server instance. In this mode, the '
1258 'devserver enforces a few extra components of the Omaha '
Chris Sosafc715442014-04-09 20:45:23 -07001259 'protocol, such as hardware class, being sent.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001260 group.add_option('-u', '--urlbase',
1261 metavar='URL',
Gabe Black3b567202015-09-23 14:07:59 -07001262 help='base URL for update images, other than the '
1263 'devserver. Use in conjunction with remote_payload.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001264 parser.add_option_group(group)
1265
1266
1267def _AddUpdateOptions(parser):
1268 group = optparse.OptionGroup(
1269 parser, 'Autoupdate Options', 'These options can be used to change '
1270 'how the devserver either generates or serve update payloads. Please '
1271 'note that all of these option affect how a payload is generated and so '
1272 'do not work in archive-only mode.')
1273 group.add_option('--board',
1274 help='By default the devserver will create an update '
1275 'payload from the latest image built for the board '
1276 'a device that is requesting an update has. When we '
1277 'pre-generate an update (see below) and we do not specify '
1278 'another update_type option like image or payload, the '
1279 'devserver needs to know the board to generate the latest '
1280 'image for. This is that board.')
1281 group.add_option('--critical_update',
1282 action='store_true', default=False,
1283 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001284 group.add_option('--image',
1285 metavar='FILE',
1286 help='Generate and serve an update using this image to any '
1287 'device that requests an update.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001288 group.add_option('--payload',
1289 metavar='PATH',
1290 help='use the update payload from specified directory '
1291 '(update.gz).')
1292 group.add_option('-p', '--pregenerate_update',
1293 action='store_true', default=False,
1294 help='pre-generate the update payload before accepting '
1295 'update requests. Useful to help debug payload generation '
1296 'issues quickly. Also if an update payload will take a '
1297 'long time to generate, a client may timeout if you do not'
1298 'pregenerate the update.')
1299 group.add_option('--src_image',
1300 metavar='PATH', default='',
1301 help='If specified, delta updates will be generated using '
1302 'this image as the source image. Delta updates are when '
1303 'you are updating from a "source image" to a another '
1304 'image.')
1305 parser.add_option_group(group)
1306
1307
1308def _AddProductionOptions(parser):
1309 group = optparse.OptionGroup(
1310 parser, 'Advanced Server Options', 'These options can be used to changed '
1311 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001312 group.add_option('--clear_cache',
1313 action='store_true', default=False,
1314 help='At startup, removes all cached entries from the'
1315 'devserver\'s cache.')
1316 group.add_option('--logfile',
1317 metavar='PATH',
1318 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -07001319 group.add_option('--pidfile',
1320 metavar='PATH',
1321 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 11:04:13 -08001322 group.add_option('--portfile',
1323 metavar='PATH',
1324 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001325 group.add_option('--production',
1326 action='store_true', default=False,
1327 help='have the devserver use production values when '
1328 'starting up. This includes using more threads and '
1329 'performing less logging.')
1330 parser.add_option_group(group)
1331
1332
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001333def _MakeLogHandler(logfile):
1334 """Create a LogHandler instance used to log all messages."""
1335 hdlr_cls = handlers.TimedRotatingFileHandler
1336 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
1337 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -07001338 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001339 return hdlr
1340
1341
Chris Sosacde6bf42012-05-31 18:36:39 -07001342def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001343 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -08001344 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -07001345
1346 # get directory that the devserver is run from
1347 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -07001348 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -07001349 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001350 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -07001351 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -07001352 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001353 parser.add_option('--port',
1354 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 13:13:28 -08001355 help=('port for the dev server to use; if zero, binds to '
1356 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001357 parser.add_option('-t', '--test_image',
1358 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -07001359 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -07001360 parser.add_option('-x', '--xbuddy_manage_builds',
1361 action='store_true',
1362 default=False,
1363 help='If set, allow xbuddy to manage images in'
1364 'build/images.')
Dan Shi72b16132015-10-08 12:10:33 -07001365 parser.add_option('-a', '--android_build_credential',
1366 default=None,
1367 help='Path to a json file which contains the credential '
1368 'needed to access Android builds.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001369 _AddProductionOptions(parser)
1370 _AddUpdateOptions(parser)
1371 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -07001372 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001373
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001374 # Handle options that must be set globally in cherrypy. Do this
1375 # work up front, because calls to _Log() below depend on this
1376 # initialization.
1377 if options.production:
1378 cherrypy.config.update({'environment': 'production'})
1379 if not options.logfile:
1380 cherrypy.config.update({'log.screen': True})
1381 else:
1382 cherrypy.config.update({'log.error_file': '',
1383 'log.access_file': ''})
1384 hdlr = _MakeLogHandler(options.logfile)
1385 # Pylint can't seem to process these two calls properly
1386 # pylint: disable=E1101
1387 cherrypy.log.access_log.addHandler(hdlr)
1388 cherrypy.log.error_log.addHandler(hdlr)
1389 # pylint: enable=E1101
1390
joychened64b222013-06-21 16:39:34 -07001391 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001392 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001393
joychened64b222013-06-21 16:39:34 -07001394 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001395 # If our devserver is only supposed to serve payloads, we shouldn't be
1396 # mucking with the cache at all. If the devserver hadn't previously
1397 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001398 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001399 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001400 else:
1401 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001402
Chris Sosadbc20082012-12-10 13:39:11 -08001403 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 16:39:34 -07001404 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001405
joychen121fc9b2013-08-02 14:30:30 -07001406 _xbuddy = xbuddy.XBuddy(options.xbuddy_manage_builds,
1407 options.board,
joychen121fc9b2013-08-02 14:30:30 -07001408 static_dir=options.static_dir)
Chris Sosa75490802013-09-30 17:21:45 -07001409 if options.clear_cache and options.xbuddy_manage_builds:
1410 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 14:30:30 -07001411
Chris Sosa6a3697f2013-01-29 16:44:43 -08001412 # We allow global use here to share with cherrypy classes.
1413 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001414 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001415 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001416 _xbuddy,
joychened64b222013-06-21 16:39:34 -07001417 static_dir=options.static_dir,
Andrew de los Reyes52620802010-04-12 13:40:07 -07001418 urlbase=options.urlbase,
Chris Sosa5d342a22010-09-28 16:54:41 -07001419 forced_image=options.image,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001420 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001421 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-28 23:42:37 -07001422 src_image=options.src_image,
Chris Sosa08d55a22011-01-19 16:08:02 -08001423 board=options.board,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001424 copy_to_static_root=not options.exit,
1425 private_key=options.private_key,
Gabe Black3b567202015-09-23 14:07:59 -07001426 private_key_for_metadata_hash_signature=(
1427 options.private_key_for_metadata_hash_signature),
David Zeuthen52ccd012013-10-31 12:58:26 -07001428 public_key=options.public_key,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001429 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001430 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001431 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001432 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001433 )
Chris Sosa7c931362010-10-11 19:49:01 -07001434
Chris Sosa6a3697f2013-01-29 16:44:43 -08001435 if options.pregenerate_update:
1436 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 15:46:22 -07001437
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001438 if options.exit:
1439 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001440
joychen3cb228e2013-06-12 12:13:13 -07001441 dev_server = DevServerRoot(_xbuddy)
1442
Gilad Arnold11fbef42014-02-10 11:04:13 -08001443 # Patch CherryPy to support binding to any available port (--port=0).
1444 cherrypy_ext.ZeroPortPatcher.DoPatch(cherrypy)
1445
Chris Sosa855b8932013-08-21 13:24:55 -07001446 if options.pidfile:
1447 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1448
Gilad Arnold11fbef42014-02-10 11:04:13 -08001449 if options.portfile:
1450 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1451
Dan Shi72b16132015-10-08 12:10:33 -07001452 if options.android_build_credential:
1453 with open(options.android_build_credential) as f:
1454 android_build.BuildAccessor.credential_info = json.load(f)
joychen3cb228e2013-06-12 12:13:13 -07001455 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001456
1457
1458if __name__ == '__main__':
1459 main()