blob: 74e129048bb9e3e9b0d5fcba335e117da7b4bbe9 [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
206def _get_downloader(kwargs):
207 """Returns the downloader based on passed in arguments.
208
209 Args:
210 kwargs: Keyword arguments for the request.
211 """
212 local_path = kwargs.get('local_path')
213 if local_path:
214 local_path = _canonicalize_local_path(local_path)
215
216 dl = None
217 if local_path:
218 dl = downloader.LocalDownloader(updater.static_dir, local_path)
219
220 # Only Android build requires argument build_id. If it's not set, assume
221 # the download request is for ChromeOS.
222 build_id = kwargs.get('build_id', None)
223 if not build_id:
224 archive_url = kwargs.get('archive_url')
225 if not archive_url and not local_path:
226 raise DevServerError('Requires archive_url or local_path to be '
227 'specified.')
228 if archive_url and local_path:
229 raise DevServerError('archive_url and local_path can not both be '
230 'specified.')
231 if not dl:
232 archive_url = _canonicalize_archive_url(archive_url)
233 dl = downloader.GoogleStorageDownloader(updater.static_dir, archive_url)
234 elif not dl:
235 target = kwargs.get('target', None)
Dan Shi72b16132015-10-08 12:10:33 -0700236 branch = kwargs.get('branch', None)
237 if not target or not branch:
238 raise DevServerError(
239 'Both target and branch must be specified for Android build.')
240 dl = downloader.AndroidBuildDownloader(updater.static_dir, branch, build_id,
241 target)
Gabe Black3b567202015-09-23 14:07:59 -0700242
243 return dl
244
245
246def _get_downloader_and_factory(kwargs):
247 """Returns the downloader and artifact factory based on passed in arguments.
248
249 Args:
250 kwargs: Keyword arguments for the request.
251 """
252 artifacts, files = _get_artifacts(kwargs)
253 dl = _get_downloader(kwargs)
254
255 if (isinstance(dl, downloader.GoogleStorageDownloader) or
256 isinstance(dl, downloader.LocalDownloader)):
257 factory_class = build_artifact.ChromeOSArtifactFactory
Dan Shi72b16132015-10-08 12:10:33 -0700258 elif isinstance(dl, downloader.AndroidBuildDownloader):
Gabe Black3b567202015-09-23 14:07:59 -0700259 factory_class = build_artifact.AndroidArtifactFactory
260 else:
261 raise DevServerError('Unrecognized value for downloader type: %s' %
262 type(dl))
263
264 factory = factory_class(dl.GetBuildDir(), artifacts, files, dl.GetBuild())
265
266 return dl, factory
267
268
Scott Zawalski4647ce62012-01-03 17:17:28 -0500269def _LeadingWhiteSpaceCount(string):
270 """Count the amount of leading whitespace in a string.
271
272 Args:
273 string: The string to count leading whitespace in.
Don Garrettf84631a2014-01-07 18:21:26 -0800274
Scott Zawalski4647ce62012-01-03 17:17:28 -0500275 Returns:
276 number of white space chars before characters start.
277 """
Gabe Black3b567202015-09-23 14:07:59 -0700278 matched = re.match(r'^\s+', string)
Scott Zawalski4647ce62012-01-03 17:17:28 -0500279 if matched:
280 return len(matched.group())
281
282 return 0
283
284
285def _PrintDocStringAsHTML(func):
286 """Make a functions docstring somewhat HTML style.
287
288 Args:
289 func: The function to return the docstring from.
Don Garrettf84631a2014-01-07 18:21:26 -0800290
Scott Zawalski4647ce62012-01-03 17:17:28 -0500291 Returns:
292 A string that is somewhat formated for a web browser.
293 """
294 # TODO(scottz): Make this parse Args/Returns in a prettier way.
295 # Arguments could be bolded and indented etc.
296 html_doc = []
297 for line in func.__doc__.splitlines():
298 leading_space = _LeadingWhiteSpaceCount(line)
299 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700300 line = '&nbsp;' * leading_space + line
Scott Zawalski4647ce62012-01-03 17:17:28 -0500301
302 html_doc.append('<BR>%s' % line)
303
304 return '\n'.join(html_doc)
305
306
Simran Basief83d6a2014-08-28 14:32:01 -0700307def _GetUpdateTimestampHandler(static_dir):
308 """Returns a handler to update directory staged.timestamp.
309
310 This handler resets the stage.timestamp whenever static content is accessed.
311
312 Args:
313 static_dir: Directory from which static content is being staged.
314
315 Returns:
316 A cherrypy handler to update the timestamp of accessed content.
317 """
318 def UpdateTimestampHandler():
319 if not '404' in cherrypy.response.status:
320 build_match = re.match(devserver_constants.STAGED_BUILD_REGEX,
321 cherrypy.request.path_info)
322 if build_match:
323 build_dir = os.path.join(static_dir, build_match.group('build'))
324 downloader.Downloader.TouchTimestampForStaged(build_dir)
325 return UpdateTimestampHandler
326
327
Chris Sosa7c931362010-10-11 19:49:01 -0700328def _GetConfig(options):
329 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800330
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800331 socket_host = '::'
Yu-Ju Hongc8d4af32013-11-12 15:14:26 -0800332 # Fall back to IPv4 when python is not configured with IPv6.
333 if not socket.has_ipv6:
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800334 socket_host = '0.0.0.0'
335
Simran Basief83d6a2014-08-28 14:32:01 -0700336 # Adds the UpdateTimestampHandler to cherrypy's tools. This tools executes
337 # on the on_end_resource hook. This hook is called once processing is
338 # complete and the response is ready to be returned.
339 cherrypy.tools.update_timestamp = cherrypy.Tool(
340 'on_end_resource', _GetUpdateTimestampHandler(options.static_dir))
341
Gabe Black3b567202015-09-23 14:07:59 -0700342 base_config = {'global':
343 {'server.log_request_headers': True,
344 'server.protocol_version': 'HTTP/1.1',
345 'server.socket_host': socket_host,
346 'server.socket_port': int(options.port),
347 'response.timeout': 6000,
348 'request.show_tracebacks': True,
349 'server.socket_timeout': 60,
350 'server.thread_pool': 2,
351 'engine.autoreload.on': False,
352 },
353 '/api':
354 {
355 # Gets rid of cherrypy parsing post file for args.
356 'request.process_request_body': False,
357 },
358 '/build':
359 {'response.timeout': 100000,
360 },
361 '/update':
362 {
363 # Gets rid of cherrypy parsing post file for args.
364 'request.process_request_body': False,
365 'response.timeout': 10000,
366 },
367 # Sets up the static dir for file hosting.
368 '/static':
369 {'tools.staticdir.dir': options.static_dir,
370 'tools.staticdir.on': True,
371 'response.timeout': 10000,
372 'tools.update_timestamp.on': True,
373 },
374 }
Chris Sosa5f118ef2012-07-12 11:37:50 -0700375 if options.production:
Alex Miller93beca52013-07-30 19:25:09 -0700376 base_config['global'].update({'server.thread_pool': 150})
Chris Sosa7cd23202013-10-15 17:22:57 -0700377 # TODO(sosa): Do this more cleanly.
378 gsutil_util.GSUTIL_ATTEMPTS = 5
Scott Zawalski1c5e7cd2012-02-27 13:12:52 -0500379
Chris Sosa7c931362010-10-11 19:49:01 -0700380 return base_config
rtc@google.com64244662009-11-12 00:52:08 +0000381
Darin Petkove17164a2010-08-11 13:24:41 -0700382
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700383def _GetRecursiveMemberObject(root, member_list):
384 """Returns an object corresponding to a nested member list.
385
386 Args:
387 root: the root object to search
388 member_list: list of nested members to search
Don Garrettf84631a2014-01-07 18:21:26 -0800389
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700390 Returns:
391 An object corresponding to the member name list; None otherwise.
392 """
393 for member in member_list:
394 next_root = root.__class__.__dict__.get(member)
395 if not next_root:
396 return None
397 root = next_root
398 return root
399
400
401def _IsExposed(name):
402 """Returns True iff |name| has an `exposed' attribute and it is set."""
403 return hasattr(name, 'exposed') and name.exposed
404
405
Gilad Arnold748c8322012-10-12 09:51:35 -0700406def _GetExposedMethod(root, nested_member, ignored=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700407 """Returns a CherryPy-exposed method, if such exists.
408
409 Args:
410 root: the root object for searching
411 nested_member: a slash-joined path to the nested member
412 ignored: method paths to be ignored
Don Garrettf84631a2014-01-07 18:21:26 -0800413
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700414 Returns:
415 A function object corresponding to the path defined by |member_list| from
416 the |root| object, if the function is exposed and not ignored; None
417 otherwise.
418 """
Gilad Arnold748c8322012-10-12 09:51:35 -0700419 method = (not (ignored and nested_member in ignored) and
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700420 _GetRecursiveMemberObject(root, nested_member.split('/')))
Gabe Black3b567202015-09-23 14:07:59 -0700421 if method and type(method) == types.FunctionType and _IsExposed(method):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700422 return method
423
424
Gilad Arnold748c8322012-10-12 09:51:35 -0700425def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700426 """Finds exposed CherryPy methods.
427
428 Args:
429 root: the root object for searching
430 prefix: slash-joined chain of members leading to current object
431 unlisted: URLs to be excluded regardless of their exposed status
Don Garrettf84631a2014-01-07 18:21:26 -0800432
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700433 Returns:
434 List of exposed URLs that are not unlisted.
435 """
436 method_list = []
437 for member in sorted(root.__class__.__dict__.keys()):
438 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 09:51:35 -0700439 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700440 continue
441 member_obj = root.__class__.__dict__[member]
442 if _IsExposed(member_obj):
443 if type(member_obj) == types.FunctionType:
444 method_list.append(prefixed_member)
445 else:
446 method_list += _FindExposedMethods(
447 member_obj, prefixed_member, unlisted)
448 return method_list
449
450
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700451class ApiRoot(object):
452 """RESTful API for Dev Server information."""
453 exposed = True
454
455 @cherrypy.expose
456 def hostinfo(self, ip):
457 """Returns a JSON dictionary containing information about the given ip.
458
Gilad Arnold1b908392012-10-05 11:36:27 -0700459 Args:
460 ip: address of host whose info is requested
Don Garrettf84631a2014-01-07 18:21:26 -0800461
Gilad Arnold1b908392012-10-05 11:36:27 -0700462 Returns:
463 A JSON dictionary containing all or some of the following fields:
464 last_event_type (int): last update event type received
465 last_event_status (int): last update event status received
466 last_known_version (string): last known version reported in update ping
467 forced_update_label (string): update label to force next update ping to
468 use, set by setnextupdate
469 See the OmahaEvent class in update_engine/omaha_request_action.h for
470 event type and status code definitions. If the ip does not exist an empty
471 string is returned.
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700472
Gilad Arnold1b908392012-10-05 11:36:27 -0700473 Example URL:
474 http://myhost/api/hostinfo?ip=192.168.1.5
475 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700476 return updater.HandleHostInfoPing(ip)
477
478 @cherrypy.expose
Gilad Arnold286a0062012-01-12 13:47:02 -0800479 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 11:36:27 -0700480 """Returns a JSON object containing a log of host event.
481
482 Args:
483 ip: address of host whose event log is requested, or `all'
Don Garrettf84631a2014-01-07 18:21:26 -0800484
Gilad Arnold1b908392012-10-05 11:36:27 -0700485 Returns:
486 A JSON encoded list (log) of dictionaries (events), each of which
487 containing a `timestamp' and other event fields, as described under
488 /api/hostinfo.
489
490 Example URL:
491 http://myhost/api/hostlog?ip=192.168.1.5
492 """
Gilad Arnold286a0062012-01-12 13:47:02 -0800493 return updater.HandleHostLogPing(ip)
494
495 @cherrypy.expose
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700496 def setnextupdate(self, ip):
497 """Allows the response to the next update ping from a host to be set.
498
499 Takes the IP of the host and an update label as normally provided to the
Gilad Arnold1b908392012-10-05 11:36:27 -0700500 /update command.
501 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700502 body_length = int(cherrypy.request.headers['Content-Length'])
503 label = cherrypy.request.rfile.read(body_length)
504
505 if label:
506 label = label.strip()
507 if label:
508 return updater.HandleSetUpdatePing(ip, label)
Chris Sosa4b951602014-04-09 20:26:07 -0700509 raise common_util.DevServerHTTPError(400, 'No label provided.')
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700510
511
Gilad Arnold55a2a372012-10-02 09:46:32 -0700512 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800513 def fileinfo(self, *args):
Gilad Arnold55a2a372012-10-02 09:46:32 -0700514 """Returns information about a given staged file.
515
516 Args:
Don Garrettf84631a2014-01-07 18:21:26 -0800517 args: path to the file inside the server's static staging directory
518
Gilad Arnold55a2a372012-10-02 09:46:32 -0700519 Returns:
520 A JSON encoded dictionary with information about the said file, which may
521 contain the following keys/values:
Gilad Arnold1b908392012-10-05 11:36:27 -0700522 size (int): the file size in bytes
523 sha1 (string): a base64 encoded SHA1 hash
524 sha256 (string): a base64 encoded SHA256 hash
525
526 Example URL:
527 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 09:46:32 -0700528 """
Don Garrettf84631a2014-01-07 18:21:26 -0800529 file_path = os.path.join(updater.static_dir, *args)
Gilad Arnold55a2a372012-10-02 09:46:32 -0700530 if not os.path.exists(file_path):
531 raise DevServerError('file not found: %s' % file_path)
532 try:
533 file_size = os.path.getsize(file_path)
534 file_sha1 = common_util.GetFileSha1(file_path)
535 file_sha256 = common_util.GetFileSha256(file_path)
536 except os.error, e:
537 raise DevServerError('failed to get info for file %s: %s' %
Gilad Arnolde74b3812013-04-22 11:27:38 -0700538 (file_path, e))
539
540 is_delta = autoupdate.Autoupdate.IsDeltaFormatFile(file_path)
541
542 return json.dumps({
543 autoupdate.Autoupdate.SIZE_ATTR: file_size,
544 autoupdate.Autoupdate.SHA1_ATTR: file_sha1,
545 autoupdate.Autoupdate.SHA256_ATTR: file_sha256,
546 autoupdate.Autoupdate.ISDELTA_ATTR: is_delta
547 })
Gilad Arnold55a2a372012-10-02 09:46:32 -0700548
Chris Sosa76e44b92013-01-31 12:11:38 -0800549
David Rochberg7c79a812011-01-19 14:24:45 -0500550class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700551 """The Root Class for the Dev Server.
552
553 CherryPy works as follows:
554 For each method in this class, cherrpy interprets root/path
555 as a call to an instance of DevServerRoot->method_name. For example,
556 a call to http://myhost/build will call build. CherryPy automatically
557 parses http args and places them as keyword arguments in each method.
558 For paths http://myhost/update/dir1/dir2, you can use *args so that
559 cherrypy uses the update method and puts the extra paths in args.
560 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700561 # Method names that should not be listed on the index page.
562 _UNLISTED_METHODS = ['index', 'doc']
563
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700564 api = ApiRoot()
Chris Sosa7c931362010-10-11 19:49:01 -0700565
Dan Shi59ae7092013-06-04 14:37:27 -0700566 # Number of threads that devserver is staging images.
567 _staging_thread_count = 0
568 # Lock used to lock increasing/decreasing count.
569 _staging_thread_count_lock = threading.Lock()
570
Dan Shiafd0e492015-05-27 14:23:51 -0700571 @require_psutil()
572 def _refresh_io_stats(self):
573 """A call running in a thread to update IO stats periodically."""
574 prev_disk_io_counters = psutil.disk_io_counters()
575 prev_network_io_counters = psutil.net_io_counters()
576 prev_read_time = time.time()
577 while True:
578 time.sleep(STATS_INTERVAL)
579 now = time.time()
580 interval = now - prev_read_time
581 prev_read_time = now
582 # Disk IO is for all disks.
583 disk_io_counters = psutil.disk_io_counters()
584 network_io_counters = psutil.net_io_counters()
585
586 self.disk_read_bytes_per_sec = (
587 disk_io_counters.read_bytes -
588 prev_disk_io_counters.read_bytes)/interval
589 self.disk_write_bytes_per_sec = (
590 disk_io_counters.write_bytes -
591 prev_disk_io_counters.write_bytes)/interval
592 prev_disk_io_counters = disk_io_counters
593
594 self.network_sent_bytes_per_sec = (
595 network_io_counters.bytes_sent -
596 prev_network_io_counters.bytes_sent)/interval
597 self.network_recv_bytes_per_sec = (
598 network_io_counters.bytes_recv -
599 prev_network_io_counters.bytes_recv)/interval
600 prev_network_io_counters = network_io_counters
601
602 @require_psutil()
603 def _start_io_stat_thread(self):
Gabe Black3b567202015-09-23 14:07:59 -0700604 """Start the thread to collect IO stats."""
Dan Shiafd0e492015-05-27 14:23:51 -0700605 thread = threading.Thread(target=self._refresh_io_stats)
606 thread.daemon = True
607 thread.start()
608
joychen3cb228e2013-06-12 12:13:13 -0700609 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700610 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800611 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 12:13:13 -0700612 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 14:24:45 -0500613
Dan Shiafd0e492015-05-27 14:23:51 -0700614 # Cache of disk IO stats, a thread refresh the stats every 10 seconds.
615 # lock is not used for these variables as the only thread writes to these
616 # variables is _refresh_io_stats.
617 self.disk_read_bytes_per_sec = 0
618 self.disk_write_bytes_per_sec = 0
619 # Cache of network IO stats.
620 self.network_sent_bytes_per_sec = 0
621 self.network_recv_bytes_per_sec = 0
622 self._start_io_stat_thread()
623
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700624 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500625 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700626 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700627 import builder
628 if self._builder is None:
629 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500630 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700631
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700632 @cherrypy.expose
Dan Shif8eb0d12013-08-01 17:52:06 -0700633 def is_staged(self, **kwargs):
634 """Check if artifacts have been downloaded.
635
Chris Sosa6b0c6172013-08-05 17:01:33 -0700636 async: True to return without waiting for download to complete.
637 artifacts: Comma separated list of named artifacts to download.
638 These are defined in artifact_info and have their implementation
639 in build_artifact.py.
640 files: Comma separated list of file artifacts to stage. These
641 will be available as is in the corresponding static directory with no
642 custom post-processing.
643
644 returns: True of all artifacts are staged.
Dan Shif8eb0d12013-08-01 17:52:06 -0700645
646 Example:
647 To check if autotest and test_suites are staged:
648 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
649 artifacts=autotest,test_suites
650 """
Gabe Black3b567202015-09-23 14:07:59 -0700651 dl, factory = _get_downloader_and_factory(kwargs)
652 return str(dl.IsStaged(factory))
Dan Shi59ae7092013-06-04 14:37:27 -0700653
Chris Sosa76e44b92013-01-31 12:11:38 -0800654 @cherrypy.expose
Prashanth Ba06d2d22014-03-07 15:35:19 -0800655 def list_image_dir(self, **kwargs):
656 """Take an archive url and list the contents in its staged directory.
657
658 Args:
659 kwargs:
660 archive_url: Google Storage URL for the build.
661
662 Example:
663 To list the contents of where this devserver should have staged
664 gs://image-archive/<board>-release/<build> call:
665 http://devserver_url:<port>/list_image_dir?archive_url=<gs://..>
666
667 Returns:
668 A string with information about the contents of the image directory.
669 """
Gabe Black3b567202015-09-23 14:07:59 -0700670 dl = _get_downloader(kwargs)
Prashanth Ba06d2d22014-03-07 15:35:19 -0800671 try:
Gabe Black3b567202015-09-23 14:07:59 -0700672 image_dir_contents = dl.ListBuildDir()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800673 except build_artifact.ArtifactDownloadError as e:
674 return 'Cannot list the contents of staged artifacts. %s' % e
675 if not image_dir_contents:
Gabe Black3b567202015-09-23 14:07:59 -0700676 return '%s has not been staged on this devserver.' % dl.DescribeSource()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800677 return image_dir_contents
678
679 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800680 def stage(self, **kwargs):
Gabe Black3b567202015-09-23 14:07:59 -0700681 """Downloads and caches build artifacts.
Chris Sosa76e44b92013-01-31 12:11:38 -0800682
Gabe Black3b567202015-09-23 14:07:59 -0700683 Downloads and caches build artifacts, possibly from a Google Storage URL,
Dan Shi72b16132015-10-08 12:10:33 -0700684 or from Android's build server. Returns once these have been downloaded
Gabe Black3b567202015-09-23 14:07:59 -0700685 on the devserver. A call to this will attempt to cache non-specified
686 artifacts in the background for the given from the given URL following
687 the principle of spatial locality. Spatial locality of different
Chris Sosa76e44b92013-01-31 12:11:38 -0800688 artifacts is explicitly defined in the build_artifact module.
689
690 These artifacts will then be available from the static/ sub-directory of
691 the devserver.
692
693 Args:
694 archive_url: Google Storage URL for the build.
Simran Basi4243a862014-12-12 12:48:33 -0800695 local_path: Local path for the build.
Dan Shif8eb0d12013-08-01 17:52:06 -0700696 async: True to return without waiting for download to complete.
Chris Sosa6b0c6172013-08-05 17:01:33 -0700697 artifacts: Comma separated list of named artifacts to download.
698 These are defined in artifact_info and have their implementation
699 in build_artifact.py.
700 files: Comma separated list of files to stage. These
701 will be available as is in the corresponding static directory with no
702 custom post-processing.
Chris Sosa76e44b92013-01-31 12:11:38 -0800703
704 Example:
705 To download the autotest and test suites tarballs:
706 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
707 artifacts=autotest,test_suites
708 To download the full update payload:
709 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
710 artifacts=full_payload
Chris Sosa6b0c6172013-08-05 17:01:33 -0700711 To download just a file called blah.bin:
712 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
713 files=blah.bin
Chris Sosa76e44b92013-01-31 12:11:38 -0800714
715 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 16:39:34 -0700716 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 12:11:38 -0800717
718 Note for this example, relative path is the archive_url stripped of its
719 basename i.e. path/ in the examples above. Specific example:
720
721 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
722
723 Will get staged to:
724
joychened64b222013-06-21 16:39:34 -0700725 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Chris Sosa76e44b92013-01-31 12:11:38 -0800726 """
Gabe Black3b567202015-09-23 14:07:59 -0700727 dl, factory = _get_downloader_and_factory(kwargs)
728
Dan Shi59ae7092013-06-04 14:37:27 -0700729 with DevServerRoot._staging_thread_count_lock:
730 DevServerRoot._staging_thread_count += 1
731 try:
Gabe Black3b567202015-09-23 14:07:59 -0700732 async = kwargs.get('async', False)
733 dl.Download(factory, async=async)
Dan Shi59ae7092013-06-04 14:37:27 -0700734 finally:
735 with DevServerRoot._staging_thread_count_lock:
736 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800737 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700738
739 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -0800740 def setup_telemetry(self, **kwargs):
741 """Extracts and sets up telemetry
742
743 This method goes through the telemetry deps packages, and stages them on
744 the devserver to be used by the drones and the telemetry tests.
745
746 Args:
747 archive_url: Google Storage URL for the build.
748
749 Returns:
750 Path to the source folder for the telemetry codebase once it is staged.
751 """
Gabe Black3b567202015-09-23 14:07:59 -0700752 dl = _get_downloader(kwargs)
Simran Basi4baad082013-02-14 13:39:18 -0800753
Gabe Black3b567202015-09-23 14:07:59 -0700754 build_path = dl.GetBuildDir()
Simran Basi4baad082013-02-14 13:39:18 -0800755 deps_path = os.path.join(build_path, 'autotest/packages')
756 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
757 src_folder = os.path.join(telemetry_path, 'src')
758
759 with self._telemetry_lock_dict.lock(telemetry_path):
760 if os.path.exists(src_folder):
761 # Telemetry is already fully stage return
762 return src_folder
763
764 common_util.MkDirP(telemetry_path)
765
766 # Copy over the required deps tar balls to the telemetry directory.
767 for dep in TELEMETRY_DEPS:
768 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -0700769 if not os.path.exists(dep_path):
770 # This dep does not exist (could be new), do not extract it.
771 continue
Simran Basi4baad082013-02-14 13:39:18 -0800772 try:
773 common_util.ExtractTarball(dep_path, telemetry_path)
774 except common_util.CommonUtilError as e:
775 shutil.rmtree(telemetry_path)
776 raise DevServerError(str(e))
777
778 # By default all the tarballs extract to test_src but some parts of
779 # the telemetry code specifically hardcoded to exist inside of 'src'.
780 test_src = os.path.join(telemetry_path, 'test_src')
781 try:
782 shutil.move(test_src, src_folder)
783 except shutil.Error:
784 # This can occur if src_folder already exists. Remove and retry move.
785 shutil.rmtree(src_folder)
Gabe Black3b567202015-09-23 14:07:59 -0700786 raise DevServerError(
787 'Failure in telemetry setup for build %s. Appears that the '
788 'test_src to src move failed.' % dl.GetBuild())
Simran Basi4baad082013-02-14 13:39:18 -0800789
790 return src_folder
791
792 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800793 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -0700794 """Symbolicates a minidump using pre-downloaded symbols, returns it.
795
796 Callers will need to POST to this URL with a body of MIME-type
797 "multipart/form-data".
798 The body should include a single argument, 'minidump', containing the
799 binary-formatted minidump to symbolicate.
800
Chris Masone816e38c2012-05-02 12:22:36 -0700801 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -0800802 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -0700803 minidump: The binary minidump file to symbolicate.
804 """
Gabe Black3b567202015-09-23 14:07:59 -0700805 kwargs['artifacts'] = 'symbols'
806 dl = _get_downloader(kwargs)
807
Chris Sosa76e44b92013-01-31 12:11:38 -0800808 # Ensure the symbols have been staged.
Gabe Black3b567202015-09-23 14:07:59 -0700809 if self.stage(**kwargs) != 'Success':
810 raise DevServerError('Failed to stage symbols for %s' %
811 dl.DescribeSource())
Chris Sosa76e44b92013-01-31 12:11:38 -0800812
Chris Masone816e38c2012-05-02 12:22:36 -0700813 to_return = ''
814 with tempfile.NamedTemporaryFile() as local:
815 while True:
816 data = minidump.file.read(8192)
817 if not data:
818 break
819 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -0800820
Chris Masone816e38c2012-05-02 12:22:36 -0700821 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -0800822
Gabe Black3b567202015-09-23 14:07:59 -0700823 symbols_directory = os.path.join(dl.GetBuildDir(), 'debug', 'breakpad')
Chris Sosa76e44b92013-01-31 12:11:38 -0800824
825 stackwalk = subprocess.Popen(
826 ['minidump_stackwalk', local.name, symbols_directory],
827 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
828
Chris Masone816e38c2012-05-02 12:22:36 -0700829 to_return, error_text = stackwalk.communicate()
830 if stackwalk.returncode != 0:
831 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
832 error_text, stackwalk.returncode))
833
834 return to_return
835
836 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800837 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 15:31:36 -0400838 """Return a string representing the latest build for a given target.
839
840 Args:
841 target: The build target, typically a combination of the board and the
842 type of build e.g. x86-mario-release.
843 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
844 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-07 18:21:26 -0800845
Scott Zawalski16954532012-03-20 15:31:36 -0400846 Returns:
847 A string representation of the latest build if one exists, i.e.
848 R19-1993.0.0-a1-b1480.
849 An empty string if no latest could be found.
850 """
Don Garrettf84631a2014-01-07 18:21:26 -0800851 if not kwargs:
Scott Zawalski16954532012-03-20 15:31:36 -0400852 return _PrintDocStringAsHTML(self.latestbuild)
853
Don Garrettf84631a2014-01-07 18:21:26 -0800854 if 'target' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -0700855 raise common_util.DevServerHTTPError(500, 'Error: target= is required!')
Scott Zawalski16954532012-03-20 15:31:36 -0400856 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700857 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-07 18:21:26 -0800858 updater.static_dir, kwargs['target'],
859 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700860 except common_util.CommonUtilError as errmsg:
Chris Sosa4b951602014-04-09 20:26:07 -0700861 raise common_util.DevServerHTTPError(500, str(errmsg))
Scott Zawalski16954532012-03-20 15:31:36 -0400862
863 @cherrypy.expose
Don Garrettf84631a2014-01-07 18:21:26 -0800864 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500865 """Return a control file or a list of all known control files.
866
867 Example URL:
868 To List all control files:
beepsbd337242013-07-09 22:44:06 -0700869 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
870 To List all control files for, say, the bvt suite:
871 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -0500872 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500873 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 -0500874
875 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500876 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500877 control_path: If you want the contents of a control file set this
878 to the path. E.g. client/site_tests/sleeptest/control
879 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-09 22:44:06 -0700880 suite_name: If control_path is not specified but a suite_name is
881 specified, list the control files belonging to that suite instead of
882 all control files. The empty string for suite_name will list all control
883 files for the build.
Don Garrettf84631a2014-01-07 18:21:26 -0800884
Scott Zawalski4647ce62012-01-03 17:17:28 -0500885 Returns:
886 Contents of a control file if control_path is provided.
887 A list of control files if no control_path is provided.
888 """
Don Garrettf84631a2014-01-07 18:21:26 -0800889 if not kwargs:
Scott Zawalski4647ce62012-01-03 17:17:28 -0500890 return _PrintDocStringAsHTML(self.controlfiles)
891
Don Garrettf84631a2014-01-07 18:21:26 -0800892 if 'build' not in kwargs:
Chris Sosa4b951602014-04-09 20:26:07 -0700893 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -0500894
Don Garrettf84631a2014-01-07 18:21:26 -0800895 if 'control_path' not in kwargs:
896 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-09 22:44:06 -0700897 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-07 18:21:26 -0800898 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-09 22:44:06 -0700899 else:
900 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-07 18:21:26 -0800901 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -0500902 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700903 return common_util.GetControlFile(
Don Garrettf84631a2014-01-07 18:21:26 -0800904 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -0800905
906 @cherrypy.expose
Simran Basi99e63c02014-05-20 10:39:52 -0700907 def xbuddy_translate(self, *args, **kwargs):
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700908 """Translates an xBuddy path to a real path to artifact if it exists.
909
910 Args:
Simran Basi99e63c02014-05-20 10:39:52 -0700911 args: An xbuddy path in the form of {local|remote}/build_id/artifact.
912 Local searches the devserver's static directory. Remote searches a
913 Google Storage image archive.
914
915 Kwargs:
916 image_dir: Google Storage image archive to search in if requesting a
917 remote artifact. If none uses the default bucket.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700918
919 Returns:
Simran Basi99e63c02014-05-20 10:39:52 -0700920 String in the format of build_id/artifact as stored on the local server
921 or in Google Storage.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700922 """
Simran Basi99e63c02014-05-20 10:39:52 -0700923 build_id, filename = self._xbuddy.Translate(
Gabe Black3b567202015-09-23 14:07:59 -0700924 args, image_dir=kwargs.get('image_dir'))
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700925 response = os.path.join(build_id, filename)
926 _Log('Path translation requested, returning: %s', response)
927 return response
928
929 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -0700930 def xbuddy(self, *args, **kwargs):
931 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -0700932
933 Args:
joycheneaf4cfc2013-07-02 08:38:57 -0700934 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 14:30:30 -0700935 components of the path. The path can be understood as
936 "{local|remote}/build_id/artifact" where build_id is composed of
937 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -0700938
joychen121fc9b2013-08-02 14:30:30 -0700939 The first path element is optional, and can be "remote" or "local"
940 If local (the default), devserver will not attempt to access Google
941 Storage, and will only search the static directory for the files.
942 If remote, devserver will try to obtain the artifact off GS if it's
943 not found locally.
944 The board is the familiar board name, optionally suffixed.
945 The version can be the google storage version number, and may also be
946 any of a number of xBuddy defined version aliases that will be
947 translated into the latest built image that fits the description.
948 Defaults to latest.
949 The artifact is one of a number of image or artifact aliases used by
950 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -0700951
952 Kwargs:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800953 for_update: {true|false}
954 if true, pregenerates the update payloads for the image,
955 and returns the update uri to pass to the
956 update_engine_client.
joychen3cb228e2013-06-12 12:13:13 -0700957 return_dir: {true|false}
958 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800959 relative_path: {true|false}
960 if set to true, returns the relative path to the payload
961 directory from static_dir.
joychen3cb228e2013-06-12 12:13:13 -0700962 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -0700963 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -0700964 or
joycheneaf4cfc2013-07-02 08:38:57 -0700965 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -0700966
967 Returns:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800968 If |for_update|, returns a redirect to the image or update file
969 on the devserver. E.g.,
970 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
971 chromium-test-image.bin
972 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
973 http://host:port/static/x86-generic-release/R26-4000.0.0/
974 If |relative_path| is true, return a relative path the folder where the
975 payloads are. E.g.,
976 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 12:13:13 -0700977 """
Chris Sosa75490802013-09-30 17:21:45 -0700978 boolean_string = kwargs.get('for_update')
979 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800980 boolean_string = kwargs.get('return_dir')
981 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
982 boolean_string = kwargs.get('relative_path')
983 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -0700984
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800985 if return_dir and relative_path:
Chris Sosa4b951602014-04-09 20:26:07 -0700986 raise common_util.DevServerHTTPError(
987 500, 'Cannot specify both return_dir and relative_path')
Chris Sosa75490802013-09-30 17:21:45 -0700988
989 # For updates, we optimize downloading of test images.
990 file_name = None
991 build_id = None
992 if for_update:
993 try:
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -0700994 build_id = self._xbuddy.StageTestArtifactsForUpdate(args)
Chris Sosa75490802013-09-30 17:21:45 -0700995 except build_artifact.ArtifactDownloadError:
996 build_id = None
997
998 if not build_id:
999 build_id, file_name = self._xbuddy.Get(args)
1000
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001001 if for_update:
1002 _Log('Payload generation triggered by request')
1003 # Forces payload to be in cache and symlinked into build_id dir.
Chris Sosa75490802013-09-30 17:21:45 -07001004 updater.GetUpdateForLabel(autoupdate.FORCED_UPDATE, build_id,
1005 image_name=file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001006
1007 response = None
1008 if return_dir:
1009 response = os.path.join(cherrypy.request.base, 'static', build_id)
1010 _Log('Directory requested, returning: %s', response)
1011 elif relative_path:
1012 response = build_id
1013 _Log('Relative path requested, returning: %s', response)
1014 elif for_update:
1015 response = os.path.join(cherrypy.request.base, 'update', build_id)
1016 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 12:13:13 -07001017 else:
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001018 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 14:30:30 -07001019 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001020 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 14:30:30 -07001021 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -07001022
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001023 return response
1024
joychen3cb228e2013-06-12 12:13:13 -07001025 @cherrypy.expose
1026 def xbuddy_list(self):
1027 """Lists the currently available images & time since last access.
1028
Gilad Arnold452fd272014-02-04 11:09:28 -08001029 Returns:
1030 A string representation of a list of tuples [(build_id, time since last
1031 access),...]
joychen3cb228e2013-06-12 12:13:13 -07001032 """
1033 return self._xbuddy.List()
1034
1035 @cherrypy.expose
1036 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 11:09:28 -08001037 """Returns the number of images cached by xBuddy."""
joychen3cb228e2013-06-12 12:13:13 -07001038 return self._xbuddy.Capacity()
1039
1040 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001041 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001042 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001043 return ('Welcome to the Dev Server!<br>\n'
1044 '<br>\n'
1045 'Here are the available methods, click for documentation:<br>\n'
1046 '<br>\n'
1047 '%s' %
1048 '<br>\n'.join(
1049 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001050 for name in _FindExposedMethods(
1051 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001052
1053 @cherrypy.expose
1054 def doc(self, *args):
1055 """Shows the documentation for available methods / URLs.
1056
1057 Example:
1058 http://myhost/doc/update
1059 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -07001060 name = '/'.join(args)
1061 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001062 if not method:
1063 raise DevServerError("No exposed method named `%s'" % name)
1064 if not method.__doc__:
1065 raise DevServerError("No documentation for exposed method `%s'" % name)
1066 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -07001067
Dale Curtisc9aaf3a2011-08-09 15:47:40 -07001068 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -07001069 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001070 """Handles an update check from a Chrome OS client.
1071
1072 The HTTP request should contain the standard Omaha-style XML blob. The URL
1073 line may contain an additional intermediate path to the update payload.
1074
joychen121fc9b2013-08-02 14:30:30 -07001075 This request can be handled in one of 4 ways, depending on the devsever
1076 settings and intermediate path.
joychenb0dfe552013-07-30 10:02:06 -07001077
joychen121fc9b2013-08-02 14:30:30 -07001078 1. No intermediate path
1079 If no intermediate path is given, the default behavior is to generate an
1080 update payload from the latest test image locally built for the board
1081 specified in the xml. Devserver serves the generated payload.
1082
1083 2. Path explicitly invokes XBuddy
1084 If there is a path given, it can explicitly invoke xbuddy by prefixing it
1085 with 'xbuddy'. This path is then used to acquire an image binary for the
1086 devserver to generate an update payload from. Devserver then serves this
1087 payload.
1088
1089 3. Path is left for the devserver to interpret.
1090 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
1091 to generate a payload from the test image in that directory and serve it.
1092
1093 4. The devserver is in a 'forced' mode. TO BE DEPRECATED
1094 This comes from the usage of --forced_payload or --image when starting the
1095 devserver. No matter what path (or no path) gets passed in, devserver will
1096 serve the update payload (--forced_payload) or generate an update payload
1097 from the image (--image).
1098
1099 Examples:
1100 1. No intermediate path
1101 update_engine_client --omaha_url=http://myhost/update
1102 This generates an update payload from the latest test image locally built
1103 for the board specified in the xml.
1104
1105 2. Explicitly invoke xbuddy
1106 update_engine_client --omaha_url=
1107 http://myhost/update/xbuddy/remote/board/version/dev
1108 This would go to GS to download the dev image for the board, from which
1109 the devserver would generate a payload to serve.
1110
1111 3. Give a path for devserver to interpret
1112 update_engine_client --omaha_url=http://myhost/update/some/random/path
1113 This would attempt, in order to:
1114 a) Generate an update from a test image binary if found in
1115 static_dir/some/random/path.
1116 b) Serve an update payload found in static_dir/some/random/path.
1117 c) Hope that some/random/path takes the form "board/version" and
1118 and attempt to download an update payload for that board/version
1119 from GS.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001120 """
joychen121fc9b2013-08-02 14:30:30 -07001121 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -08001122 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -07001123 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-11 19:49:01 -07001124
joychen121fc9b2013-08-02 14:30:30 -07001125 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001126
Dan Shiafd0e492015-05-27 14:23:51 -07001127 @require_psutil()
1128 def _get_io_stats(self):
1129 """Get the IO stats as a dictionary.
1130
Gabe Black3b567202015-09-23 14:07:59 -07001131 Returns:
1132 A dictionary of IO stats collected by psutil.
Dan Shiafd0e492015-05-27 14:23:51 -07001133
1134 """
1135 return {'disk_read_bytes_per_second': self.disk_read_bytes_per_sec,
1136 'disk_write_bytes_per_second': self.disk_write_bytes_per_sec,
1137 'disk_total_bytes_per_second': (self.disk_read_bytes_per_sec +
1138 self.disk_write_bytes_per_sec),
1139 'network_sent_bytes_per_second': self.network_sent_bytes_per_sec,
1140 'network_recv_bytes_per_second': self.network_recv_bytes_per_sec,
1141 'network_total_bytes_per_second': (self.network_sent_bytes_per_sec +
1142 self.network_recv_bytes_per_sec),
1143 'cpu_percent': psutil.cpu_percent(),}
1144
Dan Shif5ce2de2013-04-25 16:06:32 -07001145 @cherrypy.expose
1146 def check_health(self):
1147 """Collect the health status of devserver to see if it's ready for staging.
1148
Gilad Arnold452fd272014-02-04 11:09:28 -08001149 Returns:
1150 A JSON dictionary containing all or some of the following fields:
1151 free_disk (int): free disk space in GB
1152 staging_thread_count (int): number of devserver threads currently staging
1153 an image
Dan Shif5ce2de2013-04-25 16:06:32 -07001154 """
1155 # Get free disk space.
1156 stat = os.statvfs(updater.static_dir)
1157 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
1158
Dan Shiafd0e492015-05-27 14:23:51 -07001159 health_data = {
Dan Shif5ce2de2013-04-25 16:06:32 -07001160 'free_disk': free_disk,
Dan Shiafd0e492015-05-27 14:23:51 -07001161 'staging_thread_count': DevServerRoot._staging_thread_count}
1162 health_data.update(self._get_io_stats() or {})
1163
1164 return json.dumps(health_data)
Dan Shif5ce2de2013-04-25 16:06:32 -07001165
1166
Chris Sosadbc20082012-12-10 13:39:11 -08001167def _CleanCache(cache_dir, wipe):
1168 """Wipes any excess cached items in the cache_dir.
1169
1170 Args:
1171 cache_dir: the directory we are wiping from.
1172 wipe: If True, wipe all the contents -- not just the excess.
1173 """
1174 if wipe:
1175 # Clear the cache and exit on error.
1176 cmd = 'rm -rf %s/*' % cache_dir
1177 if os.system(cmd) != 0:
1178 _Log('Failed to clear the cache with %s' % cmd)
1179 sys.exit(1)
1180 else:
1181 # Clear all but the last N cached updates
1182 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
1183 (cache_dir, CACHED_ENTRIES))
1184 if os.system(cmd) != 0:
1185 _Log('Failed to clean up old delta cache files with %s' % cmd)
1186 sys.exit(1)
1187
1188
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001189def _AddTestingOptions(parser):
1190 group = optparse.OptionGroup(
1191 parser, 'Advanced Testing Options', 'These are used by test scripts and '
1192 'developers writing integration tests utilizing the devserver. They are '
1193 'not intended to be really used outside the scope of someone '
1194 'knowledgable about the test.')
1195 group.add_option('--exit',
1196 action='store_true',
1197 help='do not start the server (yet pregenerate/clear cache)')
1198 group.add_option('--host_log',
1199 action='store_true', default=False,
1200 help='record history of host update events (/api/hostlog)')
1201 group.add_option('--max_updates',
Gabe Black3b567202015-09-23 14:07:59 -07001202 metavar='NUM', default=-1, type='int',
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001203 help='maximum number of update checks handled positively '
1204 '(default: unlimited)')
1205 group.add_option('--private_key',
1206 metavar='PATH', default=None,
1207 help='path to the private key in pem format. If this is set '
1208 'the devserver will generate update payloads that are '
1209 'signed with this key.')
David Zeuthen52ccd012013-10-31 12:58:26 -07001210 group.add_option('--private_key_for_metadata_hash_signature',
1211 metavar='PATH', default=None,
1212 help='path to the private key in pem format. If this is set '
1213 'the devserver will sign the metadata hash with the given '
1214 'key and transmit in the Omaha-style XML response.')
1215 group.add_option('--public_key',
1216 metavar='PATH', default=None,
1217 help='path to the public key in pem format. If this is set '
1218 'the devserver will transmit a base64 encoded version of '
1219 'the content in the Omaha-style XML response.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001220 group.add_option('--proxy_port',
1221 metavar='PORT', default=None, type='int',
1222 help='port to have the client connect to -- basically the '
1223 'devserver lies to the update to tell it to get the payload '
1224 'from a different port that will proxy the request back to '
1225 'the devserver. The proxy must be managed outside the '
1226 'devserver.')
1227 group.add_option('--remote_payload',
1228 action='store_true', default=False,
Chris Sosa4b951602014-04-09 20:26:07 -07001229 help='Payload is being served from a remote machine. With '
1230 'this setting enabled, this devserver instance serves as '
1231 'just an Omaha server instance. In this mode, the '
1232 'devserver enforces a few extra components of the Omaha '
Chris Sosafc715442014-04-09 20:45:23 -07001233 'protocol, such as hardware class, being sent.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001234 group.add_option('-u', '--urlbase',
1235 metavar='URL',
Gabe Black3b567202015-09-23 14:07:59 -07001236 help='base URL for update images, other than the '
1237 'devserver. Use in conjunction with remote_payload.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001238 parser.add_option_group(group)
1239
1240
1241def _AddUpdateOptions(parser):
1242 group = optparse.OptionGroup(
1243 parser, 'Autoupdate Options', 'These options can be used to change '
1244 'how the devserver either generates or serve update payloads. Please '
1245 'note that all of these option affect how a payload is generated and so '
1246 'do not work in archive-only mode.')
1247 group.add_option('--board',
1248 help='By default the devserver will create an update '
1249 'payload from the latest image built for the board '
1250 'a device that is requesting an update has. When we '
1251 'pre-generate an update (see below) and we do not specify '
1252 'another update_type option like image or payload, the '
1253 'devserver needs to know the board to generate the latest '
1254 'image for. This is that board.')
1255 group.add_option('--critical_update',
1256 action='store_true', default=False,
1257 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001258 group.add_option('--image',
1259 metavar='FILE',
1260 help='Generate and serve an update using this image to any '
1261 'device that requests an update.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001262 group.add_option('--payload',
1263 metavar='PATH',
1264 help='use the update payload from specified directory '
1265 '(update.gz).')
1266 group.add_option('-p', '--pregenerate_update',
1267 action='store_true', default=False,
1268 help='pre-generate the update payload before accepting '
1269 'update requests. Useful to help debug payload generation '
1270 'issues quickly. Also if an update payload will take a '
1271 'long time to generate, a client may timeout if you do not'
1272 'pregenerate the update.')
1273 group.add_option('--src_image',
1274 metavar='PATH', default='',
1275 help='If specified, delta updates will be generated using '
1276 'this image as the source image. Delta updates are when '
1277 'you are updating from a "source image" to a another '
1278 'image.')
1279 parser.add_option_group(group)
1280
1281
1282def _AddProductionOptions(parser):
1283 group = optparse.OptionGroup(
1284 parser, 'Advanced Server Options', 'These options can be used to changed '
1285 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001286 group.add_option('--clear_cache',
1287 action='store_true', default=False,
1288 help='At startup, removes all cached entries from the'
1289 'devserver\'s cache.')
1290 group.add_option('--logfile',
1291 metavar='PATH',
1292 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 13:24:55 -07001293 group.add_option('--pidfile',
1294 metavar='PATH',
1295 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 11:04:13 -08001296 group.add_option('--portfile',
1297 metavar='PATH',
1298 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001299 group.add_option('--production',
1300 action='store_true', default=False,
1301 help='have the devserver use production values when '
1302 'starting up. This includes using more threads and '
1303 'performing less logging.')
1304 parser.add_option_group(group)
1305
1306
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001307def _MakeLogHandler(logfile):
1308 """Create a LogHandler instance used to log all messages."""
1309 hdlr_cls = handlers.TimedRotatingFileHandler
1310 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
1311 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 13:24:55 -07001312 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001313 return hdlr
1314
1315
Chris Sosacde6bf42012-05-31 18:36:39 -07001316def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001317 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -08001318 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -07001319
1320 # get directory that the devserver is run from
1321 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 09:17:23 -07001322 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 16:39:34 -07001323 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001324 metavar='PATH',
joychen84d13772013-08-06 09:17:23 -07001325 default=default_static_dir,
joychened64b222013-06-21 16:39:34 -07001326 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001327 parser.add_option('--port',
1328 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 13:13:28 -08001329 help=('port for the dev server to use; if zero, binds to '
1330 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 10:09:42 -07001331 parser.add_option('-t', '--test_image',
1332 action='store_true',
joychen121fc9b2013-08-02 14:30:30 -07001333 help='Deprecated.')
joychen5260b9a2013-07-16 14:48:01 -07001334 parser.add_option('-x', '--xbuddy_manage_builds',
1335 action='store_true',
1336 default=False,
1337 help='If set, allow xbuddy to manage images in'
1338 'build/images.')
Dan Shi72b16132015-10-08 12:10:33 -07001339 parser.add_option('-a', '--android_build_credential',
1340 default=None,
1341 help='Path to a json file which contains the credential '
1342 'needed to access Android builds.')
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001343 _AddProductionOptions(parser)
1344 _AddUpdateOptions(parser)
1345 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -07001346 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001347
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001348 # Handle options that must be set globally in cherrypy. Do this
1349 # work up front, because calls to _Log() below depend on this
1350 # initialization.
1351 if options.production:
1352 cherrypy.config.update({'environment': 'production'})
1353 if not options.logfile:
1354 cherrypy.config.update({'log.screen': True})
1355 else:
1356 cherrypy.config.update({'log.error_file': '',
1357 'log.access_file': ''})
1358 hdlr = _MakeLogHandler(options.logfile)
1359 # Pylint can't seem to process these two calls properly
1360 # pylint: disable=E1101
1361 cherrypy.log.access_log.addHandler(hdlr)
1362 cherrypy.log.error_log.addHandler(hdlr)
1363 # pylint: enable=E1101
1364
joychened64b222013-06-21 16:39:34 -07001365 # set static_dir, from which everything will be served
joychen84d13772013-08-06 09:17:23 -07001366 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001367
joychened64b222013-06-21 16:39:34 -07001368 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001369 # If our devserver is only supposed to serve payloads, we shouldn't be
1370 # mucking with the cache at all. If the devserver hadn't previously
1371 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 11:14:07 -07001372 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 13:39:11 -08001373 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001374 else:
1375 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001376
Chris Sosadbc20082012-12-10 13:39:11 -08001377 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 16:39:34 -07001378 _Log('Serving from %s' % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001379
joychen121fc9b2013-08-02 14:30:30 -07001380 _xbuddy = xbuddy.XBuddy(options.xbuddy_manage_builds,
1381 options.board,
joychen121fc9b2013-08-02 14:30:30 -07001382 static_dir=options.static_dir)
Chris Sosa75490802013-09-30 17:21:45 -07001383 if options.clear_cache and options.xbuddy_manage_builds:
1384 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 14:30:30 -07001385
Chris Sosa6a3697f2013-01-29 16:44:43 -08001386 # We allow global use here to share with cherrypy classes.
1387 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001388 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001389 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 14:30:30 -07001390 _xbuddy,
joychened64b222013-06-21 16:39:34 -07001391 static_dir=options.static_dir,
Andrew de los Reyes52620802010-04-12 13:40:07 -07001392 urlbase=options.urlbase,
Chris Sosa5d342a22010-09-28 16:54:41 -07001393 forced_image=options.image,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001394 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001395 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-28 23:42:37 -07001396 src_image=options.src_image,
Chris Sosa08d55a22011-01-19 16:08:02 -08001397 board=options.board,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001398 copy_to_static_root=not options.exit,
1399 private_key=options.private_key,
Gabe Black3b567202015-09-23 14:07:59 -07001400 private_key_for_metadata_hash_signature=(
1401 options.private_key_for_metadata_hash_signature),
David Zeuthen52ccd012013-10-31 12:58:26 -07001402 public_key=options.public_key,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001403 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001404 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001405 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001406 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001407 )
Chris Sosa7c931362010-10-11 19:49:01 -07001408
Chris Sosa6a3697f2013-01-29 16:44:43 -08001409 if options.pregenerate_update:
1410 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 15:46:22 -07001411
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001412 if options.exit:
1413 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001414
joychen3cb228e2013-06-12 12:13:13 -07001415 dev_server = DevServerRoot(_xbuddy)
1416
Gilad Arnold11fbef42014-02-10 11:04:13 -08001417 # Patch CherryPy to support binding to any available port (--port=0).
1418 cherrypy_ext.ZeroPortPatcher.DoPatch(cherrypy)
1419
Chris Sosa855b8932013-08-21 13:24:55 -07001420 if options.pidfile:
1421 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1422
Gilad Arnold11fbef42014-02-10 11:04:13 -08001423 if options.portfile:
1424 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1425
Dan Shi72b16132015-10-08 12:10:33 -07001426 if options.android_build_credential:
1427 with open(options.android_build_credential) as f:
1428 android_build.BuildAccessor.credential_info = json.load(f)
joychen3cb228e2013-06-12 12:13:13 -07001429 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001430
1431
1432if __name__ == '__main__':
1433 main()