blob: 6b2e98361e6c19a558a8305ca70e194b9e93fc5d [file] [log] [blame]
Chris Sosa7c931362010-10-11 19:49:01 -07001#!/usr/bin/python
2
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
29a particular package from a developer's chroot onto a requesting device. Note
30if archive_dir is specified, this mode is disabled.
31
32For example:
33gmerge gmerge -d <devserver_url>
34
35devserver will see if a newer package of gmerge is available. If gmerge is
36cros_work'd on, it will re-build gmerge. After this, gmerge will install that
37version of gmerge that the devserver just created/found.
38
39For autoupdates, there are many more advanced options that can help specify
40how to update and which payload to give to a requester.
41"""
42
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
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -070054import types
J. Richard Barnette3d977b82013-04-23 11:05:19 -070055from logging import handlers
56
57import cherrypy
58import cherrypy._cplogging
rtc@google.comded22402009-10-26 22:36:21 +000059
Chris Sosa0356d3b2010-09-16 15:46:22 -070060import autoupdate
Gilad Arnoldc65330c2012-09-20 15:17:48 -070061import common_util
Chris Sosa47a7d4e2012-03-28 11:26:55 -070062import downloader
Gilad Arnoldc65330c2012-09-20 15:17:48 -070063import log_util
joychen3cb228e2013-06-12 12:13:13 -070064import xbuddy
Gilad Arnoldc65330c2012-09-20 15:17:48 -070065
Gilad Arnoldc65330c2012-09-20 15:17:48 -070066# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080067def _Log(message, *args):
68 return log_util.LogWithTag('DEVSERVER', message, *args)
Chris Sosa0356d3b2010-09-16 15:46:22 -070069
Frank Farzan40160872011-12-12 18:39:18 -080070
Chris Sosa417e55d2011-01-25 16:40:48 -080071CACHED_ENTRIES = 12
Don Garrettf90edf02010-11-16 17:36:14 -080072
Simran Basi4baad082013-02-14 13:39:18 -080073TELEMETRY_FOLDER = 'telemetry_src'
74TELEMETRY_DEPS = ['dep-telemetry_dep.tar.bz2',
75 'dep-page_cycler_dep.tar.bz2',
Simran Basi0d078682013-03-22 16:40:04 -070076 'dep-chrome_test.tar.bz2',
77 'dep-perf_data_dep.tar.bz2']
Simran Basi4baad082013-02-14 13:39:18 -080078
Chris Sosa0356d3b2010-09-16 15:46:22 -070079# Sets up global to share between classes.
rtc@google.com21a5ca32009-11-04 18:23:23 +000080updater = None
rtc@google.comded22402009-10-26 22:36:21 +000081
J. Richard Barnette3d977b82013-04-23 11:05:19 -070082# Log rotation parameters. These settings correspond to once a week
J. Richard Barnette6dfa5342013-06-04 11:48:56 -070083# at midnight between Friday and Saturday, with about three months
84# of old logs kept for backup.
J. Richard Barnette3d977b82013-04-23 11:05:19 -070085#
86# For more, see the documentation for
87# logging.handlers.TimedRotatingFileHandler
J. Richard Barnette6dfa5342013-06-04 11:48:56 -070088_LOG_ROTATION_TIME = 'W4'
J. Richard Barnette3d977b82013-04-23 11:05:19 -070089_LOG_ROTATION_BACKUP = 13
90
Frank Farzan40160872011-12-12 18:39:18 -080091
Chris Sosa9164ca32012-03-28 11:04:50 -070092class DevServerError(Exception):
Chris Sosa47a7d4e2012-03-28 11:26:55 -070093 """Exception class used by this module."""
94 pass
95
96
Scott Zawalski4647ce62012-01-03 17:17:28 -050097def _LeadingWhiteSpaceCount(string):
98 """Count the amount of leading whitespace in a string.
99
100 Args:
101 string: The string to count leading whitespace in.
102 Returns:
103 number of white space chars before characters start.
104 """
105 matched = re.match('^\s+', string)
106 if matched:
107 return len(matched.group())
108
109 return 0
110
111
112def _PrintDocStringAsHTML(func):
113 """Make a functions docstring somewhat HTML style.
114
115 Args:
116 func: The function to return the docstring from.
117 Returns:
118 A string that is somewhat formated for a web browser.
119 """
120 # TODO(scottz): Make this parse Args/Returns in a prettier way.
121 # Arguments could be bolded and indented etc.
122 html_doc = []
123 for line in func.__doc__.splitlines():
124 leading_space = _LeadingWhiteSpaceCount(line)
125 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700126 line = '&nbsp;' * leading_space + line
Scott Zawalski4647ce62012-01-03 17:17:28 -0500127
128 html_doc.append('<BR>%s' % line)
129
130 return '\n'.join(html_doc)
131
132
Chris Sosa7c931362010-10-11 19:49:01 -0700133def _GetConfig(options):
134 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800135
136 # On a system with IPv6 not compiled into the kernel,
137 # AF_INET6 sockets will return a socket.error exception.
138 # On such systems, fall-back to IPv4.
139 socket_host = '::'
140 try:
141 socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
142 except socket.error:
143 socket_host = '0.0.0.0'
144
Chris Sosa7c931362010-10-11 19:49:01 -0700145 base_config = { 'global':
146 { 'server.log_request_headers': True,
147 'server.protocol_version': 'HTTP/1.1',
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800148 'server.socket_host': socket_host,
Chris Sosa7c931362010-10-11 19:49:01 -0700149 'server.socket_port': int(options.port),
Chris Sosa374c62d2010-10-14 09:13:54 -0700150 'response.timeout': 6000,
Chris Sosa6fe23942012-07-02 15:44:46 -0700151 'request.show_tracebacks': True,
Chris Sosa72333d12012-06-13 11:28:05 -0700152 'server.socket_timeout': 60,
Zdenek Behan1347a312011-02-10 03:59:17 +0100153 'tools.staticdir.root':
154 os.path.dirname(os.path.abspath(sys.argv[0])),
Chris Sosa7c931362010-10-11 19:49:01 -0700155 },
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700156 '/api':
157 {
158 # Gets rid of cherrypy parsing post file for args.
159 'request.process_request_body': False,
160 },
Chris Sosaa1ef0102010-10-21 16:22:35 -0700161 '/build':
162 {
163 'response.timeout': 100000,
164 },
Chris Sosa7c931362010-10-11 19:49:01 -0700165 '/update':
166 {
167 # Gets rid of cherrypy parsing post file for args.
168 'request.process_request_body': False,
Chris Sosaf65f4b92010-10-21 15:57:51 -0700169 'response.timeout': 10000,
Chris Sosa7c931362010-10-11 19:49:01 -0700170 },
171 # Sets up the static dir for file hosting.
172 '/static':
173 { 'tools.staticdir.dir': 'static',
174 'tools.staticdir.on': True,
Chris Sosaf65f4b92010-10-21 15:57:51 -0700175 'response.timeout': 10000,
Chris Sosa7c931362010-10-11 19:49:01 -0700176 },
177 }
Chris Sosa5f118ef2012-07-12 11:37:50 -0700178 if options.production:
Chris Sosad1ea86b2012-07-12 13:35:37 -0700179 base_config['global'].update({'server.thread_pool': 75})
Scott Zawalski1c5e7cd2012-02-27 13:12:52 -0500180
Chris Sosa7c931362010-10-11 19:49:01 -0700181 return base_config
rtc@google.com64244662009-11-12 00:52:08 +0000182
Darin Petkove17164a2010-08-11 13:24:41 -0700183
Zdenek Behan608f46c2011-02-19 00:47:16 +0100184def _PrepareToServeUpdatesOnly(image_dir, static_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700185 """Sets up symlink to image_dir for serving purposes."""
186 assert os.path.exists(image_dir), '%s must exist.' % image_dir
187 # If we're serving out of an archived build dir (e.g. a
188 # buildbot), prepare this webserver's magic 'static/' dir with a
189 # link to the build archive.
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700190 _Log('Preparing autoupdate for "serve updates only" mode.')
Zdenek Behan608f46c2011-02-19 00:47:16 +0100191 if os.path.lexists('%s/archive' % static_dir):
192 if image_dir != os.readlink('%s/archive' % static_dir):
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700193 _Log('removing stale symlink to %s' % image_dir)
Zdenek Behan608f46c2011-02-19 00:47:16 +0100194 os.unlink('%s/archive' % static_dir)
195 os.symlink(image_dir, '%s/archive' % static_dir)
Chris Sosacde6bf42012-05-31 18:36:39 -0700196
Chris Sosa0356d3b2010-09-16 15:46:22 -0700197 else:
Zdenek Behan608f46c2011-02-19 00:47:16 +0100198 os.symlink(image_dir, '%s/archive' % static_dir)
Chris Sosacde6bf42012-05-31 18:36:39 -0700199
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700200 _Log('archive dir: %s ready to be used to serve images.' % image_dir)
Chris Sosa7c931362010-10-11 19:49:01 -0700201
202
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700203def _GetRecursiveMemberObject(root, member_list):
204 """Returns an object corresponding to a nested member list.
205
206 Args:
207 root: the root object to search
208 member_list: list of nested members to search
209 Returns:
210 An object corresponding to the member name list; None otherwise.
211 """
212 for member in member_list:
213 next_root = root.__class__.__dict__.get(member)
214 if not next_root:
215 return None
216 root = next_root
217 return root
218
219
220def _IsExposed(name):
221 """Returns True iff |name| has an `exposed' attribute and it is set."""
222 return hasattr(name, 'exposed') and name.exposed
223
224
Gilad Arnold748c8322012-10-12 09:51:35 -0700225def _GetExposedMethod(root, nested_member, ignored=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700226 """Returns a CherryPy-exposed method, if such exists.
227
228 Args:
229 root: the root object for searching
230 nested_member: a slash-joined path to the nested member
231 ignored: method paths to be ignored
232 Returns:
233 A function object corresponding to the path defined by |member_list| from
234 the |root| object, if the function is exposed and not ignored; None
235 otherwise.
236 """
Gilad Arnold748c8322012-10-12 09:51:35 -0700237 method = (not (ignored and nested_member in ignored) and
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700238 _GetRecursiveMemberObject(root, nested_member.split('/')))
239 if (method and type(method) == types.FunctionType and _IsExposed(method)):
240 return method
241
242
Gilad Arnold748c8322012-10-12 09:51:35 -0700243def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700244 """Finds exposed CherryPy methods.
245
246 Args:
247 root: the root object for searching
248 prefix: slash-joined chain of members leading to current object
249 unlisted: URLs to be excluded regardless of their exposed status
250 Returns:
251 List of exposed URLs that are not unlisted.
252 """
253 method_list = []
254 for member in sorted(root.__class__.__dict__.keys()):
255 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 09:51:35 -0700256 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700257 continue
258 member_obj = root.__class__.__dict__[member]
259 if _IsExposed(member_obj):
260 if type(member_obj) == types.FunctionType:
261 method_list.append(prefixed_member)
262 else:
263 method_list += _FindExposedMethods(
264 member_obj, prefixed_member, unlisted)
265 return method_list
266
267
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700268class ApiRoot(object):
269 """RESTful API for Dev Server information."""
270 exposed = True
271
272 @cherrypy.expose
273 def hostinfo(self, ip):
274 """Returns a JSON dictionary containing information about the given ip.
275
Gilad Arnold1b908392012-10-05 11:36:27 -0700276 Args:
277 ip: address of host whose info is requested
278 Returns:
279 A JSON dictionary containing all or some of the following fields:
280 last_event_type (int): last update event type received
281 last_event_status (int): last update event status received
282 last_known_version (string): last known version reported in update ping
283 forced_update_label (string): update label to force next update ping to
284 use, set by setnextupdate
285 See the OmahaEvent class in update_engine/omaha_request_action.h for
286 event type and status code definitions. If the ip does not exist an empty
287 string is returned.
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700288
Gilad Arnold1b908392012-10-05 11:36:27 -0700289 Example URL:
290 http://myhost/api/hostinfo?ip=192.168.1.5
291 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700292 return updater.HandleHostInfoPing(ip)
293
294 @cherrypy.expose
Gilad Arnold286a0062012-01-12 13:47:02 -0800295 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 11:36:27 -0700296 """Returns a JSON object containing a log of host event.
297
298 Args:
299 ip: address of host whose event log is requested, or `all'
300 Returns:
301 A JSON encoded list (log) of dictionaries (events), each of which
302 containing a `timestamp' and other event fields, as described under
303 /api/hostinfo.
304
305 Example URL:
306 http://myhost/api/hostlog?ip=192.168.1.5
307 """
Gilad Arnold286a0062012-01-12 13:47:02 -0800308 return updater.HandleHostLogPing(ip)
309
310 @cherrypy.expose
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700311 def setnextupdate(self, ip):
312 """Allows the response to the next update ping from a host to be set.
313
314 Takes the IP of the host and an update label as normally provided to the
Gilad Arnold1b908392012-10-05 11:36:27 -0700315 /update command.
316 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700317 body_length = int(cherrypy.request.headers['Content-Length'])
318 label = cherrypy.request.rfile.read(body_length)
319
320 if label:
321 label = label.strip()
322 if label:
323 return updater.HandleSetUpdatePing(ip, label)
324 raise cherrypy.HTTPError(400, 'No label provided.')
325
326
Gilad Arnold55a2a372012-10-02 09:46:32 -0700327 @cherrypy.expose
328 def fileinfo(self, *path_args):
329 """Returns information about a given staged file.
330
331 Args:
332 path_args: path to the file inside the server's static staging directory
333 Returns:
334 A JSON encoded dictionary with information about the said file, which may
335 contain the following keys/values:
Gilad Arnold1b908392012-10-05 11:36:27 -0700336 size (int): the file size in bytes
337 sha1 (string): a base64 encoded SHA1 hash
338 sha256 (string): a base64 encoded SHA256 hash
339
340 Example URL:
341 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 09:46:32 -0700342 """
343 file_path = os.path.join(updater.static_dir, *path_args)
344 if not os.path.exists(file_path):
345 raise DevServerError('file not found: %s' % file_path)
346 try:
347 file_size = os.path.getsize(file_path)
348 file_sha1 = common_util.GetFileSha1(file_path)
349 file_sha256 = common_util.GetFileSha256(file_path)
350 except os.error, e:
351 raise DevServerError('failed to get info for file %s: %s' %
Gilad Arnolde74b3812013-04-22 11:27:38 -0700352 (file_path, e))
353
354 is_delta = autoupdate.Autoupdate.IsDeltaFormatFile(file_path)
355
356 return json.dumps({
357 autoupdate.Autoupdate.SIZE_ATTR: file_size,
358 autoupdate.Autoupdate.SHA1_ATTR: file_sha1,
359 autoupdate.Autoupdate.SHA256_ATTR: file_sha256,
360 autoupdate.Autoupdate.ISDELTA_ATTR: is_delta
361 })
Gilad Arnold55a2a372012-10-02 09:46:32 -0700362
Chris Sosa76e44b92013-01-31 12:11:38 -0800363
David Rochberg7c79a812011-01-19 14:24:45 -0500364class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700365 """The Root Class for the Dev Server.
366
367 CherryPy works as follows:
368 For each method in this class, cherrpy interprets root/path
369 as a call to an instance of DevServerRoot->method_name. For example,
370 a call to http://myhost/build will call build. CherryPy automatically
371 parses http args and places them as keyword arguments in each method.
372 For paths http://myhost/update/dir1/dir2, you can use *args so that
373 cherrypy uses the update method and puts the extra paths in args.
374 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700375 # Method names that should not be listed on the index page.
376 _UNLISTED_METHODS = ['index', 'doc']
377
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700378 api = ApiRoot()
Chris Sosa7c931362010-10-11 19:49:01 -0700379
Dan Shi59ae7092013-06-04 14:37:27 -0700380 # Number of threads that devserver is staging images.
381 _staging_thread_count = 0
382 # Lock used to lock increasing/decreasing count.
383 _staging_thread_count_lock = threading.Lock()
384
joychen3cb228e2013-06-12 12:13:13 -0700385 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700386 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800387 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 12:13:13 -0700388 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 14:24:45 -0500389
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700390 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500391 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700392 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700393 import builder
394 if self._builder is None:
395 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500396 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700397
Chris Sosacde6bf42012-05-31 18:36:39 -0700398 @staticmethod
399 def _canonicalize_archive_url(archive_url):
400 """Canonicalizes archive_url strings.
401
402 Raises:
403 DevserverError: if archive_url is not set.
404 """
405 if archive_url:
Chris Sosa76e44b92013-01-31 12:11:38 -0800406 if not archive_url.startswith('gs://'):
407 raise DevServerError("Archive URL isn't from Google Storage.")
408
Chris Sosacde6bf42012-05-31 18:36:39 -0700409 return archive_url.rstrip('/')
410 else:
411 raise DevServerError("Must specify an archive_url in the request")
412
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700413 @cherrypy.expose
Frank Farzanbcb571e2012-01-03 11:48:17 -0800414 def download(self, **kwargs):
415 """Downloads and archives full/delta payloads from Google Storage.
416
Chris Sosa76e44b92013-01-31 12:11:38 -0800417 THIS METHOD IS DEPRECATED: use stage(..., artifacts=...) instead.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700418 This methods downloads artifacts. It may download artifacts in the
419 background in which case a caller should call wait_for_status to get
420 the status of the background artifact downloads. They should use the same
421 args passed to download.
422
Frank Farzanbcb571e2012-01-03 11:48:17 -0800423 Args:
424 archive_url: Google Storage URL for the build.
425
426 Example URL:
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700427 http://myhost/download?archive_url=gs://chromeos-image-archive/
428 x86-generic/R17-1208.0.0-a1-b338
Frank Farzanbcb571e2012-01-03 11:48:17 -0800429 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800430 return self.stage(archive_url=kwargs.get('archive_url'),
431 artifacts='full_payload,test_suites,stateful')
432
Dan Shi59ae7092013-06-04 14:37:27 -0700433
Chris Sosa76e44b92013-01-31 12:11:38 -0800434 @cherrypy.expose
435 def stage(self, **kwargs):
436 """Downloads and caches the artifacts from Google Storage URL.
437
438 Downloads and caches the artifacts Google Storage URL. Returns once these
439 have been downloaded on the devserver. A call to this will attempt to cache
440 non-specified artifacts in the background for the given from the given URL
441 following the principle of spatial locality. Spatial locality of different
442 artifacts is explicitly defined in the build_artifact module.
443
444 These artifacts will then be available from the static/ sub-directory of
445 the devserver.
446
447 Args:
448 archive_url: Google Storage URL for the build.
449 artifacts: Comma separated list of artifacts to download.
450
451 Example:
452 To download the autotest and test suites tarballs:
453 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
454 artifacts=autotest,test_suites
455 To download the full update payload:
456 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
457 artifacts=full_payload
458
459 For both these examples, one could find these artifacts at:
460 http://devserver_url:<port>/static/archive/<relative_path>*
461
462 Note for this example, relative path is the archive_url stripped of its
463 basename i.e. path/ in the examples above. Specific example:
464
465 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
466
467 Will get staged to:
468
469 http://devserver_url:<port>/static/archive/x86-mario-release/R26-3920.0.0
470 """
Chris Sosacde6bf42012-05-31 18:36:39 -0700471 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
Chris Sosa76e44b92013-01-31 12:11:38 -0800472 artifacts = kwargs.get('artifacts', '')
473 if not artifacts:
474 raise DevServerError('No artifacts specified.')
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700475
Dan Shi59ae7092013-06-04 14:37:27 -0700476 with DevServerRoot._staging_thread_count_lock:
477 DevServerRoot._staging_thread_count += 1
478 try:
479 downloader.Downloader(updater.static_dir, archive_url).Download(
480 artifacts.split(','))
481 finally:
482 with DevServerRoot._staging_thread_count_lock:
483 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 12:11:38 -0800484 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700485
Dan Shi59ae7092013-06-04 14:37:27 -0700486
Chris Sosacde6bf42012-05-31 18:36:39 -0700487 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -0800488 def setup_telemetry(self, **kwargs):
489 """Extracts and sets up telemetry
490
491 This method goes through the telemetry deps packages, and stages them on
492 the devserver to be used by the drones and the telemetry tests.
493
494 Args:
495 archive_url: Google Storage URL for the build.
496
497 Returns:
498 Path to the source folder for the telemetry codebase once it is staged.
499 """
500 archive_url = kwargs.get('archive_url')
501 self.stage(archive_url=archive_url, artifacts='autotest')
502
503 build = '/'.join(downloader.Downloader.ParseUrl(archive_url))
504 build_path = os.path.join(updater.static_dir, build)
505 deps_path = os.path.join(build_path, 'autotest/packages')
506 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
507 src_folder = os.path.join(telemetry_path, 'src')
508
509 with self._telemetry_lock_dict.lock(telemetry_path):
510 if os.path.exists(src_folder):
511 # Telemetry is already fully stage return
512 return src_folder
513
514 common_util.MkDirP(telemetry_path)
515
516 # Copy over the required deps tar balls to the telemetry directory.
517 for dep in TELEMETRY_DEPS:
518 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -0700519 if not os.path.exists(dep_path):
520 # This dep does not exist (could be new), do not extract it.
521 continue
Simran Basi4baad082013-02-14 13:39:18 -0800522 try:
523 common_util.ExtractTarball(dep_path, telemetry_path)
524 except common_util.CommonUtilError as e:
525 shutil.rmtree(telemetry_path)
526 raise DevServerError(str(e))
527
528 # By default all the tarballs extract to test_src but some parts of
529 # the telemetry code specifically hardcoded to exist inside of 'src'.
530 test_src = os.path.join(telemetry_path, 'test_src')
531 try:
532 shutil.move(test_src, src_folder)
533 except shutil.Error:
534 # This can occur if src_folder already exists. Remove and retry move.
535 shutil.rmtree(src_folder)
536 raise DevServerError('Failure in telemetry setup for build %s. Appears'
537 ' that the test_src to src move failed.' % build)
538
539 return src_folder
540
541 @cherrypy.expose
Chris Sosacde6bf42012-05-31 18:36:39 -0700542 def wait_for_status(self, **kwargs):
543 """Waits for background artifacts to be downloaded from Google Storage.
544
Chris Sosa76e44b92013-01-31 12:11:38 -0800545 THIS METHOD IS DEPRECATED: use stage(..., artifacts=...) instead.
Chris Sosacde6bf42012-05-31 18:36:39 -0700546 Args:
547 archive_url: Google Storage URL for the build.
548
549 Example URL:
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700550 http://myhost/wait_for_status?archive_url=gs://chromeos-image-archive/
551 x86-generic/R17-1208.0.0-a1-b338
Chris Sosacde6bf42012-05-31 18:36:39 -0700552 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800553 return self.stage(archive_url=kwargs.get('archive_url'),
554 artifacts='full_payload,test_suites,autotest,stateful')
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700555
556 @cherrypy.expose
Chris Masone816e38c2012-05-02 12:22:36 -0700557 def stage_debug(self, **kwargs):
558 """Downloads and stages debug symbol payloads from Google Storage.
559
Chris Sosa76e44b92013-01-31 12:11:38 -0800560 THIS METHOD IS DEPRECATED: use stage(..., artifacts=...) instead.
561 This methods downloads the debug symbol build artifact
562 synchronously, and then stages it for use by symbolicate_dump.
Chris Masone816e38c2012-05-02 12:22:36 -0700563
564 Args:
565 archive_url: Google Storage URL for the build.
566
567 Example URL:
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700568 http://myhost/stage_debug?archive_url=gs://chromeos-image-archive/
569 x86-generic/R17-1208.0.0-a1-b338
Chris Masone816e38c2012-05-02 12:22:36 -0700570 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800571 return self.stage(archive_url=kwargs.get('archive_url'),
572 artifacts='symbols')
Chris Masone816e38c2012-05-02 12:22:36 -0700573
574 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800575 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -0700576 """Symbolicates a minidump using pre-downloaded symbols, returns it.
577
578 Callers will need to POST to this URL with a body of MIME-type
579 "multipart/form-data".
580 The body should include a single argument, 'minidump', containing the
581 binary-formatted minidump to symbolicate.
582
Chris Masone816e38c2012-05-02 12:22:36 -0700583 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -0800584 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -0700585 minidump: The binary minidump file to symbolicate.
586 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800587 # Ensure the symbols have been staged.
588 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
589 if self.stage(archive_url=archive_url, artifacts='symbols') != 'Success':
590 raise DevServerError('Failed to stage symbols for %s' % archive_url)
591
Chris Masone816e38c2012-05-02 12:22:36 -0700592 to_return = ''
593 with tempfile.NamedTemporaryFile() as local:
594 while True:
595 data = minidump.file.read(8192)
596 if not data:
597 break
598 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -0800599
Chris Masone816e38c2012-05-02 12:22:36 -0700600 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -0800601
602 symbols_directory = os.path.join(downloader.Downloader.GetBuildDir(
603 updater.static_dir, archive_url), 'debug', 'breakpad')
604
605 stackwalk = subprocess.Popen(
606 ['minidump_stackwalk', local.name, symbols_directory],
607 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
608
Chris Masone816e38c2012-05-02 12:22:36 -0700609 to_return, error_text = stackwalk.communicate()
610 if stackwalk.returncode != 0:
611 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
612 error_text, stackwalk.returncode))
613
614 return to_return
615
616 @cherrypy.expose
Scott Zawalski16954532012-03-20 15:31:36 -0400617 def latestbuild(self, **params):
618 """Return a string representing the latest build for a given target.
619
620 Args:
621 target: The build target, typically a combination of the board and the
622 type of build e.g. x86-mario-release.
623 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
624 provided the latest RXX build will be returned.
625 Returns:
626 A string representation of the latest build if one exists, i.e.
627 R19-1993.0.0-a1-b1480.
628 An empty string if no latest could be found.
629 """
630 if not params:
631 return _PrintDocStringAsHTML(self.latestbuild)
632
633 if 'target' not in params:
634 raise cherrypy.HTTPError('500 Internal Server Error',
635 'Error: target= is required!')
636 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700637 return common_util.GetLatestBuildVersion(
Scott Zawalski16954532012-03-20 15:31:36 -0400638 updater.static_dir, params['target'],
639 milestone=params.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700640 except common_util.CommonUtilError as errmsg:
Scott Zawalski16954532012-03-20 15:31:36 -0400641 raise cherrypy.HTTPError('500 Internal Server Error', str(errmsg))
642
643 @cherrypy.expose
Scott Zawalski84a39c92012-01-13 15:12:42 -0500644 def controlfiles(self, **params):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500645 """Return a control file or a list of all known control files.
646
647 Example URL:
648 To List all control files:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500649 http://dev-server/controlfiles?board=x86-alex-release&build=R18-1514.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -0500650 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500651 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 -0500652
653 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500654 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500655 control_path: If you want the contents of a control file set this
656 to the path. E.g. client/site_tests/sleeptest/control
657 Optional, if not provided return a list of control files is returned.
658 Returns:
659 Contents of a control file if control_path is provided.
660 A list of control files if no control_path is provided.
661 """
Scott Zawalski4647ce62012-01-03 17:17:28 -0500662 if not params:
663 return _PrintDocStringAsHTML(self.controlfiles)
664
Scott Zawalski84a39c92012-01-13 15:12:42 -0500665 if 'build' not in params:
Scott Zawalski4647ce62012-01-03 17:17:28 -0500666 raise cherrypy.HTTPError('500 Internal Server Error',
Scott Zawalski84a39c92012-01-13 15:12:42 -0500667 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -0500668
669 if 'control_path' not in params:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700670 return common_util.GetControlFileList(
671 updater.static_dir, params['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -0500672 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700673 return common_util.GetControlFile(
674 updater.static_dir, params['build'], params['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -0800675
676 @cherrypy.expose
Gilad Arnold6f99b982012-09-12 10:49:40 -0700677 def stage_images(self, **kwargs):
678 """Downloads and stages a Chrome OS image from Google Storage.
679
Chris Sosa76e44b92013-01-31 12:11:38 -0800680 THIS METHOD IS DEPRECATED: use stage(..., artifacts=...) instead.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700681 This method downloads a zipped archive from a specified GS location, then
682 extracts and stages the specified list of images and stages them under
Chris Sosa76e44b92013-01-31 12:11:38 -0800683 static/BOARD/BUILD/. Download is synchronous.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700684
685 Args:
686 archive_url: Google Storage URL for the build.
687 image_types: comma-separated list of images to download, may include
688 'test', 'recovery', and 'base'
689
690 Example URL:
691 http://myhost/stage_images?archive_url=gs://chromeos-image-archive/
692 x86-generic/R17-1208.0.0-a1-b338&image_types=test,base
693 """
Gilad Arnold6f99b982012-09-12 10:49:40 -0700694 image_types = kwargs.get('image_types').split(',')
Chris Sosa76e44b92013-01-31 12:11:38 -0800695 image_types_list = [image + '_image' for image in image_types]
696 self.stage(archive_url=kwargs.get('archive_url'), artifacts=','.join(
697 image_types_list))
Gilad Arnold6f99b982012-09-12 10:49:40 -0700698
699 @cherrypy.expose
joycheneaf4cfc2013-07-02 08:38:57 -0700700 def xbuddy(self, *args, **kwargs):
701 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -0700702
703 Args:
joycheneaf4cfc2013-07-02 08:38:57 -0700704 path_parts: the path following xbuddy/ in the call url is split into the
705 components of the path.
706 The path can be understood as a build_id/artifact, build_id is
707 composed of "board/version"
708
709 path_parts[0], the board, is the familiar board name, optionally
710 suffixed.
711 path_parts[1], the version, can be the google storage version
712 number, and may also be any of a number of xBuddy defined version
713 aliases that will be translated into the latest built image that
714 fits the description. defaults to latest.
715 path_parts[2], the artifact, is one of a number of image or artifact
716 aliases used by xbuddy, defined in xbuddy:ALIASES. Defaults to test
717
718 Kwargs:
joychen3cb228e2013-06-12 12:13:13 -0700719 return_dir: {true|false}
720 if set to true, returns the url to the update.gz
721 instead.
722
723 Example URL:
joycheneaf4cfc2013-07-02 08:38:57 -0700724 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 12:13:13 -0700725 or
joycheneaf4cfc2013-07-02 08:38:57 -0700726 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -0700727
728 Returns:
729 A redirect to the image or update file on the devserver.
730 e.g. http://host:port/static/archive/x86-generic-release/
731 R26-4000.0.0/chromium-test-image.bin
732 or if return_dir is True, return path to the folder where
733 image or update file is
734 http://host:port/static/x86-generic-release/R26-4000.0.0/
735 """
736 boolean_string = kwargs.get('return_dir')
737 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
joycheneaf4cfc2013-07-02 08:38:57 -0700738 return_url = self._xbuddy.Get(args,
joychen3cb228e2013-06-12 12:13:13 -0700739 return_dir)
740 if return_dir:
joycheneaf4cfc2013-07-02 08:38:57 -0700741 directory = os.path.join(cherrypy.request.base, return_url)
742 _Log("Directory requested, returning: %s", directory)
743 return directory
joychen3cb228e2013-06-12 12:13:13 -0700744 else:
joycheneaf4cfc2013-07-02 08:38:57 -0700745 return_url = '/' + return_url
746 _Log("Payload requested, returning: %s", return_url)
joychen3cb228e2013-06-12 12:13:13 -0700747 raise cherrypy.HTTPRedirect(return_url, 302)
748
749 @cherrypy.expose
750 def xbuddy_list(self):
751 """Lists the currently available images & time since last access.
752
753 @return: A string representation of a list of tuples
754 [(build_id, time since last access),...]
755 """
756 return self._xbuddy.List()
757
758 @cherrypy.expose
759 def xbuddy_capacity(self):
760 """Returns the number of images cached by xBuddy.
761
762 @return: Capacity of this devserver.
763 """
764 return self._xbuddy.Capacity()
765
766 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700767 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700768 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700769 return ('Welcome to the Dev Server!<br>\n'
770 '<br>\n'
771 'Here are the available methods, click for documentation:<br>\n'
772 '<br>\n'
773 '%s' %
774 '<br>\n'.join(
775 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700776 for name in _FindExposedMethods(
777 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700778
779 @cherrypy.expose
780 def doc(self, *args):
781 """Shows the documentation for available methods / URLs.
782
783 Example:
784 http://myhost/doc/update
785 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700786 name = '/'.join(args)
787 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700788 if not method:
789 raise DevServerError("No exposed method named `%s'" % name)
790 if not method.__doc__:
791 raise DevServerError("No documentation for exposed method `%s'" % name)
792 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -0700793
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700794 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700795 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700796 """Handles an update check from a Chrome OS client.
797
798 The HTTP request should contain the standard Omaha-style XML blob. The URL
799 line may contain an additional intermediate path to the update payload.
800
801 Example:
802 http://myhost/update/optional/path/to/payload
803 """
Chris Sosa7c931362010-10-11 19:49:01 -0700804 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -0800805 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -0700806 data = cherrypy.request.rfile.read(body_length)
807 return updater.HandleUpdatePing(data, label)
808
Chris Sosa0356d3b2010-09-16 15:46:22 -0700809
Dan Shif5ce2de2013-04-25 16:06:32 -0700810 @cherrypy.expose
811 def check_health(self):
812 """Collect the health status of devserver to see if it's ready for staging.
813
814 @return: A JSON dictionary containing all or some of the following fields:
Dan Shi59ae7092013-06-04 14:37:27 -0700815 free_disk (int): free disk space in GB
816 staging_thread_count (int): number of devserver threads currently
817 staging an image
Dan Shif5ce2de2013-04-25 16:06:32 -0700818 """
819 # Get free disk space.
820 stat = os.statvfs(updater.static_dir)
821 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
822
823 return json.dumps({
824 'free_disk': free_disk,
Dan Shi59ae7092013-06-04 14:37:27 -0700825 'staging_thread_count': DevServerRoot._staging_thread_count,
Dan Shif5ce2de2013-04-25 16:06:32 -0700826 })
827
828
Chris Sosadbc20082012-12-10 13:39:11 -0800829def _CleanCache(cache_dir, wipe):
830 """Wipes any excess cached items in the cache_dir.
831
832 Args:
833 cache_dir: the directory we are wiping from.
834 wipe: If True, wipe all the contents -- not just the excess.
835 """
836 if wipe:
837 # Clear the cache and exit on error.
838 cmd = 'rm -rf %s/*' % cache_dir
839 if os.system(cmd) != 0:
840 _Log('Failed to clear the cache with %s' % cmd)
841 sys.exit(1)
842 else:
843 # Clear all but the last N cached updates
844 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
845 (cache_dir, CACHED_ENTRIES))
846 if os.system(cmd) != 0:
847 _Log('Failed to clean up old delta cache files with %s' % cmd)
848 sys.exit(1)
849
850
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700851def _AddTestingOptions(parser):
852 group = optparse.OptionGroup(
853 parser, 'Advanced Testing Options', 'These are used by test scripts and '
854 'developers writing integration tests utilizing the devserver. They are '
855 'not intended to be really used outside the scope of someone '
856 'knowledgable about the test.')
857 group.add_option('--exit',
858 action='store_true',
859 help='do not start the server (yet pregenerate/clear cache)')
860 group.add_option('--host_log',
861 action='store_true', default=False,
862 help='record history of host update events (/api/hostlog)')
863 group.add_option('--max_updates',
864 metavar='NUM', default= -1, type='int',
865 help='maximum number of update checks handled positively '
866 '(default: unlimited)')
867 group.add_option('--private_key',
868 metavar='PATH', default=None,
869 help='path to the private key in pem format. If this is set '
870 'the devserver will generate update payloads that are '
871 'signed with this key.')
872 group.add_option('--proxy_port',
873 metavar='PORT', default=None, type='int',
874 help='port to have the client connect to -- basically the '
875 'devserver lies to the update to tell it to get the payload '
876 'from a different port that will proxy the request back to '
877 'the devserver. The proxy must be managed outside the '
878 'devserver.')
879 group.add_option('--remote_payload',
880 action='store_true', default=False,
881 help='Payload is being served from a remote machine')
882 group.add_option('-u', '--urlbase',
883 metavar='URL',
884 help='base URL for update images, other than the '
885 'devserver. Use in conjunction with remote_payload.')
886 parser.add_option_group(group)
887
888
889def _AddUpdateOptions(parser):
890 group = optparse.OptionGroup(
891 parser, 'Autoupdate Options', 'These options can be used to change '
892 'how the devserver either generates or serve update payloads. Please '
893 'note that all of these option affect how a payload is generated and so '
894 'do not work in archive-only mode.')
895 group.add_option('--board',
896 help='By default the devserver will create an update '
897 'payload from the latest image built for the board '
898 'a device that is requesting an update has. When we '
899 'pre-generate an update (see below) and we do not specify '
900 'another update_type option like image or payload, the '
901 'devserver needs to know the board to generate the latest '
902 'image for. This is that board.')
903 group.add_option('--critical_update',
904 action='store_true', default=False,
905 help='Present update payload as critical')
906 group.add_option('--for_vm',
907 dest='vm', action='store_true',
908 help='DEPRECATED: see no_patch_kernel.')
909 group.add_option('--image',
910 metavar='FILE',
911 help='Generate and serve an update using this image to any '
912 'device that requests an update.')
913 group.add_option('--no_patch_kernel',
914 dest='patch_kernel', action='store_false', default=True,
915 help='When generating an update payload, do not patch the '
916 'kernel with kernel verification blob from the stateful '
917 'partition.')
918 group.add_option('--payload',
919 metavar='PATH',
920 help='use the update payload from specified directory '
921 '(update.gz).')
922 group.add_option('-p', '--pregenerate_update',
923 action='store_true', default=False,
924 help='pre-generate the update payload before accepting '
925 'update requests. Useful to help debug payload generation '
926 'issues quickly. Also if an update payload will take a '
927 'long time to generate, a client may timeout if you do not'
928 'pregenerate the update.')
929 group.add_option('--src_image',
930 metavar='PATH', default='',
931 help='If specified, delta updates will be generated using '
932 'this image as the source image. Delta updates are when '
933 'you are updating from a "source image" to a another '
934 'image.')
935 parser.add_option_group(group)
936
937
938def _AddProductionOptions(parser):
939 group = optparse.OptionGroup(
940 parser, 'Advanced Server Options', 'These options can be used to changed '
941 'for advanced server behavior.')
942 # TODO(sosa): Clean up the fact we have archive_dir and data_dir. It's ugly.
943 # Should be --archive_mode + optional data_dir.
944 group.add_option('--archive_dir',
945 metavar='PATH',
946 help='Enables archive-only mode. This disables any '
947 'update or package generation related functionality. This '
948 'mode also works without a Chromium OS chroot.')
949 group.add_option('--clear_cache',
950 action='store_true', default=False,
951 help='At startup, removes all cached entries from the'
952 'devserver\'s cache.')
953 group.add_option('--logfile',
954 metavar='PATH',
955 help='log output to this file instead of stdout')
956 group.add_option('--production',
957 action='store_true', default=False,
958 help='have the devserver use production values when '
959 'starting up. This includes using more threads and '
960 'performing less logging.')
961 parser.add_option_group(group)
962
963
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700964def _MakeLogHandler(logfile):
965 """Create a LogHandler instance used to log all messages."""
966 hdlr_cls = handlers.TimedRotatingFileHandler
967 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
968 backupCount=_LOG_ROTATION_BACKUP)
969 # The cherrypy documentation says to use the _cplogging module for
970 # this, even though it's named as a private module.
971 # pylint: disable=W0212
972 hdlr.setFormatter(cherrypy._cplogging.logfmt)
973 return hdlr
974
975
Chris Sosacde6bf42012-05-31 18:36:39 -0700976def main():
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700977 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 13:47:02 -0800978 parser = optparse.OptionParser(usage=usage)
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700979 parser.add_option('--data_dir',
980 metavar='PATH',
981 default=os.path.dirname(os.path.abspath(sys.argv[0])),
982 help='writable directory where static lives')
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700983 parser.add_option('--port',
984 default=8080, type='int',
985 help='port for the dev server to use (default: 8080)')
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700986 parser.add_option('-t', '--test_image',
987 action='store_true',
Chris Sosa3ae4dc12013-03-29 11:47:00 -0700988 help='If set, look for the chromiumos_test_image.bin file '
989 'when generating update payloads rather than the '
990 'chromiumos_image.bin which is the default.')
991 _AddProductionOptions(parser)
992 _AddUpdateOptions(parser)
993 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-11 19:49:01 -0700994 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +0000995
J. Richard Barnette3d977b82013-04-23 11:05:19 -0700996 # Handle options that must be set globally in cherrypy. Do this
997 # work up front, because calls to _Log() below depend on this
998 # initialization.
999 if options.production:
1000 cherrypy.config.update({'environment': 'production'})
1001 if not options.logfile:
1002 cherrypy.config.update({'log.screen': True})
1003 else:
1004 cherrypy.config.update({'log.error_file': '',
1005 'log.access_file': ''})
1006 hdlr = _MakeLogHandler(options.logfile)
1007 # Pylint can't seem to process these two calls properly
1008 # pylint: disable=E1101
1009 cherrypy.log.access_log.addHandler(hdlr)
1010 cherrypy.log.error_log.addHandler(hdlr)
1011 # pylint: enable=E1101
1012
Chris Sosa7c931362010-10-11 19:49:01 -07001013 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
1014 root_dir = os.path.realpath('%s/../..' % devserver_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001015 serve_only = False
1016
Zdenek Behan608f46c2011-02-19 00:47:16 +01001017 static_dir = os.path.realpath('%s/static' % options.data_dir)
1018 os.system('mkdir -p %s' % static_dir)
1019
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001020 # TODO(sosa): Remove after deprecation.
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001021 if options.vm:
1022 options.patch_kernel = False
1023
Sean O'Connor14b6a0a2010-03-20 23:23:48 -07001024 if options.archive_dir:
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001025 # TODO(zbehan) Remove legacy support:
1026 # archive_dir is the directory where static/archive will point.
1027 # If this is an absolute path, all is fine. If someone calls this
1028 # using a relative path, that is relative to src/platform/dev/.
1029 # That use case is unmaintainable, but since applications use it
1030 # with =./static, instead of a boolean flag, we'll make this
1031 # relative to devserver_dir to keep these unbroken. For now.
Zdenek Behan608f46c2011-02-19 00:47:16 +01001032 archive_dir = options.archive_dir
1033 if not os.path.isabs(archive_dir):
Chris Sosa47a7d4e2012-03-28 11:26:55 -07001034 archive_dir = os.path.realpath(os.path.join(devserver_dir, archive_dir))
Zdenek Behan608f46c2011-02-19 00:47:16 +01001035 _PrepareToServeUpdatesOnly(archive_dir, static_dir)
Zdenek Behan6d93e552011-03-02 22:35:49 +01001036 static_dir = os.path.realpath(archive_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001037 serve_only = True
Chris Sosa0356d3b2010-09-16 15:46:22 -07001038
Don Garrettf90edf02010-11-16 17:36:14 -08001039 cache_dir = os.path.join(static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001040 # If our devserver is only supposed to serve payloads, we shouldn't be
1041 # mucking with the cache at all. If the devserver hadn't previously
1042 # generated a cache and is expected, the caller is using it wrong.
Chris Sosadbc20082012-12-10 13:39:11 -08001043 if serve_only:
1044 # Extra check to make sure we're not being called incorrectly.
1045 if (options.clear_cache or options.exit or options.pregenerate_update or
1046 options.board or options.image):
1047 parser.error('Incompatible flags detected for serve_only mode.')
Chris Sosadbc20082012-12-10 13:39:11 -08001048 elif os.path.exists(cache_dir):
1049 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -08001050 else:
1051 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001052
Chris Sosadbc20082012-12-10 13:39:11 -08001053 _Log('Using cache directory %s' % cache_dir)
Gilad Arnoldc65330c2012-09-20 15:17:48 -07001054 _Log('Data dir is %s' % options.data_dir)
1055 _Log('Source root is %s' % root_dir)
1056 _Log('Serving from %s' % static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001057
Chris Sosa6a3697f2013-01-29 16:44:43 -08001058 # We allow global use here to share with cherrypy classes.
1059 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -07001060 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -07001061 updater = autoupdate.Autoupdate(
1062 root_dir=root_dir,
1063 static_dir=static_dir,
Chris Sosa0356d3b2010-09-16 15:46:22 -07001064 serve_only=serve_only,
Andrew de los Reyes52620802010-04-12 13:40:07 -07001065 urlbase=options.urlbase,
1066 test_image=options.test_image,
Chris Sosa5d342a22010-09-28 16:54:41 -07001067 forced_image=options.image,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001068 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -08001069 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-28 23:42:37 -07001070 src_image=options.src_image,
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001071 patch_kernel=options.patch_kernel,
Chris Sosa08d55a22011-01-19 16:08:02 -08001072 board=options.board,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001073 copy_to_static_root=not options.exit,
1074 private_key=options.private_key,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -08001075 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-02 23:58:58 -07001076 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 10:32:44 -07001077 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -07001078 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -08001079 )
Chris Sosa7c931362010-10-11 19:49:01 -07001080
Chris Sosa6a3697f2013-01-29 16:44:43 -08001081 if options.pregenerate_update:
1082 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 15:46:22 -07001083
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001084 if options.exit:
1085 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001086
joychen3cb228e2013-06-12 12:13:13 -07001087 _xbuddy = xbuddy.XBuddy(static_dir)
1088 dev_server = DevServerRoot(_xbuddy)
1089
1090 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001091
1092
1093if __name__ == '__main__':
1094 main()