blob: d3ad97cc8987897d9cdc116e9146808531f06903 [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 Sosa7c931362010-10-11 19:49:01 -07007"""A CherryPy-based webserver to host images and build packages."""
8
Chris Sosadbc20082012-12-10 13:39:11 -08009import cherrypy
Gilad Arnold55a2a372012-10-02 09:46:32 -070010import json
Sean O'Connor14b6a0a2010-03-20 23:23:48 -070011import optparse
rtc@google.comded22402009-10-26 22:36:21 +000012import os
Scott Zawalski4647ce62012-01-03 17:17:28 -050013import re
Simran Basi4baad082013-02-14 13:39:18 -080014import shutil
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -080015import socket
chocobo@google.com4dc25812009-10-27 23:46:26 +000016import sys
Chris Masone816e38c2012-05-02 12:22:36 -070017import subprocess
18import tempfile
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -070019import types
rtc@google.comded22402009-10-26 22:36:21 +000020
Chris Sosa0356d3b2010-09-16 15:46:22 -070021import autoupdate
Gilad Arnoldc65330c2012-09-20 15:17:48 -070022import common_util
Chris Sosa47a7d4e2012-03-28 11:26:55 -070023import downloader
Gilad Arnoldc65330c2012-09-20 15:17:48 -070024import log_util
25
26
27# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080028def _Log(message, *args):
29 return log_util.LogWithTag('DEVSERVER', message, *args)
Chris Sosa0356d3b2010-09-16 15:46:22 -070030
Frank Farzan40160872011-12-12 18:39:18 -080031
Chris Sosa417e55d2011-01-25 16:40:48 -080032CACHED_ENTRIES = 12
Don Garrettf90edf02010-11-16 17:36:14 -080033
Simran Basi4baad082013-02-14 13:39:18 -080034TELEMETRY_FOLDER = 'telemetry_src'
35TELEMETRY_DEPS = ['dep-telemetry_dep.tar.bz2',
36 'dep-page_cycler_dep.tar.bz2',
Simran Basi0d078682013-03-22 16:40:04 -070037 'dep-chrome_test.tar.bz2',
38 'dep-perf_data_dep.tar.bz2']
Simran Basi4baad082013-02-14 13:39:18 -080039
Chris Sosa0356d3b2010-09-16 15:46:22 -070040# Sets up global to share between classes.
rtc@google.com21a5ca32009-11-04 18:23:23 +000041updater = None
rtc@google.comded22402009-10-26 22:36:21 +000042
Frank Farzan40160872011-12-12 18:39:18 -080043
Chris Sosa9164ca32012-03-28 11:04:50 -070044class DevServerError(Exception):
Chris Sosa47a7d4e2012-03-28 11:26:55 -070045 """Exception class used by this module."""
46 pass
47
48
Scott Zawalski4647ce62012-01-03 17:17:28 -050049def _LeadingWhiteSpaceCount(string):
50 """Count the amount of leading whitespace in a string.
51
52 Args:
53 string: The string to count leading whitespace in.
54 Returns:
55 number of white space chars before characters start.
56 """
57 matched = re.match('^\s+', string)
58 if matched:
59 return len(matched.group())
60
61 return 0
62
63
64def _PrintDocStringAsHTML(func):
65 """Make a functions docstring somewhat HTML style.
66
67 Args:
68 func: The function to return the docstring from.
69 Returns:
70 A string that is somewhat formated for a web browser.
71 """
72 # TODO(scottz): Make this parse Args/Returns in a prettier way.
73 # Arguments could be bolded and indented etc.
74 html_doc = []
75 for line in func.__doc__.splitlines():
76 leading_space = _LeadingWhiteSpaceCount(line)
77 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 11:26:55 -070078 line = ' ' * leading_space + line
Scott Zawalski4647ce62012-01-03 17:17:28 -050079
80 html_doc.append('<BR>%s' % line)
81
82 return '\n'.join(html_doc)
83
84
Chris Sosa7c931362010-10-11 19:49:01 -070085def _GetConfig(options):
86 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -080087
88 # On a system with IPv6 not compiled into the kernel,
89 # AF_INET6 sockets will return a socket.error exception.
90 # On such systems, fall-back to IPv4.
91 socket_host = '::'
92 try:
93 socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
94 except socket.error:
95 socket_host = '0.0.0.0'
96
Chris Sosa7c931362010-10-11 19:49:01 -070097 base_config = { 'global':
98 { 'server.log_request_headers': True,
99 'server.protocol_version': 'HTTP/1.1',
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800100 'server.socket_host': socket_host,
Chris Sosa7c931362010-10-11 19:49:01 -0700101 'server.socket_port': int(options.port),
Chris Sosa374c62d2010-10-14 09:13:54 -0700102 'response.timeout': 6000,
Chris Sosa6fe23942012-07-02 15:44:46 -0700103 'request.show_tracebacks': True,
Chris Sosa72333d12012-06-13 11:28:05 -0700104 'server.socket_timeout': 60,
Zdenek Behan1347a312011-02-10 03:59:17 +0100105 'tools.staticdir.root':
106 os.path.dirname(os.path.abspath(sys.argv[0])),
Chris Sosa7c931362010-10-11 19:49:01 -0700107 },
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700108 '/api':
109 {
110 # Gets rid of cherrypy parsing post file for args.
111 'request.process_request_body': False,
112 },
Chris Sosaa1ef0102010-10-21 16:22:35 -0700113 '/build':
114 {
115 'response.timeout': 100000,
116 },
Chris Sosa7c931362010-10-11 19:49:01 -0700117 '/update':
118 {
119 # Gets rid of cherrypy parsing post file for args.
120 'request.process_request_body': False,
Chris Sosaf65f4b92010-10-21 15:57:51 -0700121 'response.timeout': 10000,
Chris Sosa7c931362010-10-11 19:49:01 -0700122 },
123 # Sets up the static dir for file hosting.
124 '/static':
125 { 'tools.staticdir.dir': 'static',
126 'tools.staticdir.on': True,
Chris Sosaf65f4b92010-10-21 15:57:51 -0700127 'response.timeout': 10000,
Chris Sosa7c931362010-10-11 19:49:01 -0700128 },
129 }
Chris Sosa5f118ef2012-07-12 11:37:50 -0700130 if options.production:
Chris Sosad1ea86b2012-07-12 13:35:37 -0700131 base_config['global'].update({'server.thread_pool': 75})
Scott Zawalski1c5e7cd2012-02-27 13:12:52 -0500132
Chris Sosa7c931362010-10-11 19:49:01 -0700133 return base_config
rtc@google.com64244662009-11-12 00:52:08 +0000134
Darin Petkove17164a2010-08-11 13:24:41 -0700135
Zdenek Behan608f46c2011-02-19 00:47:16 +0100136def _PrepareToServeUpdatesOnly(image_dir, static_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700137 """Sets up symlink to image_dir for serving purposes."""
138 assert os.path.exists(image_dir), '%s must exist.' % image_dir
139 # If we're serving out of an archived build dir (e.g. a
140 # buildbot), prepare this webserver's magic 'static/' dir with a
141 # link to the build archive.
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700142 _Log('Preparing autoupdate for "serve updates only" mode.')
Zdenek Behan608f46c2011-02-19 00:47:16 +0100143 if os.path.lexists('%s/archive' % static_dir):
144 if image_dir != os.readlink('%s/archive' % static_dir):
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700145 _Log('removing stale symlink to %s' % image_dir)
Zdenek Behan608f46c2011-02-19 00:47:16 +0100146 os.unlink('%s/archive' % static_dir)
147 os.symlink(image_dir, '%s/archive' % static_dir)
Chris Sosacde6bf42012-05-31 18:36:39 -0700148
Chris Sosa0356d3b2010-09-16 15:46:22 -0700149 else:
Zdenek Behan608f46c2011-02-19 00:47:16 +0100150 os.symlink(image_dir, '%s/archive' % static_dir)
Chris Sosacde6bf42012-05-31 18:36:39 -0700151
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700152 _Log('archive dir: %s ready to be used to serve images.' % image_dir)
Chris Sosa7c931362010-10-11 19:49:01 -0700153
154
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700155def _GetRecursiveMemberObject(root, member_list):
156 """Returns an object corresponding to a nested member list.
157
158 Args:
159 root: the root object to search
160 member_list: list of nested members to search
161 Returns:
162 An object corresponding to the member name list; None otherwise.
163 """
164 for member in member_list:
165 next_root = root.__class__.__dict__.get(member)
166 if not next_root:
167 return None
168 root = next_root
169 return root
170
171
172def _IsExposed(name):
173 """Returns True iff |name| has an `exposed' attribute and it is set."""
174 return hasattr(name, 'exposed') and name.exposed
175
176
Gilad Arnold748c8322012-10-12 09:51:35 -0700177def _GetExposedMethod(root, nested_member, ignored=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700178 """Returns a CherryPy-exposed method, if such exists.
179
180 Args:
181 root: the root object for searching
182 nested_member: a slash-joined path to the nested member
183 ignored: method paths to be ignored
184 Returns:
185 A function object corresponding to the path defined by |member_list| from
186 the |root| object, if the function is exposed and not ignored; None
187 otherwise.
188 """
Gilad Arnold748c8322012-10-12 09:51:35 -0700189 method = (not (ignored and nested_member in ignored) and
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700190 _GetRecursiveMemberObject(root, nested_member.split('/')))
191 if (method and type(method) == types.FunctionType and _IsExposed(method)):
192 return method
193
194
Gilad Arnold748c8322012-10-12 09:51:35 -0700195def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700196 """Finds exposed CherryPy methods.
197
198 Args:
199 root: the root object for searching
200 prefix: slash-joined chain of members leading to current object
201 unlisted: URLs to be excluded regardless of their exposed status
202 Returns:
203 List of exposed URLs that are not unlisted.
204 """
205 method_list = []
206 for member in sorted(root.__class__.__dict__.keys()):
207 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 09:51:35 -0700208 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700209 continue
210 member_obj = root.__class__.__dict__[member]
211 if _IsExposed(member_obj):
212 if type(member_obj) == types.FunctionType:
213 method_list.append(prefixed_member)
214 else:
215 method_list += _FindExposedMethods(
216 member_obj, prefixed_member, unlisted)
217 return method_list
218
219
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700220class ApiRoot(object):
221 """RESTful API for Dev Server information."""
222 exposed = True
223
224 @cherrypy.expose
225 def hostinfo(self, ip):
226 """Returns a JSON dictionary containing information about the given ip.
227
Gilad Arnold1b908392012-10-05 11:36:27 -0700228 Args:
229 ip: address of host whose info is requested
230 Returns:
231 A JSON dictionary containing all or some of the following fields:
232 last_event_type (int): last update event type received
233 last_event_status (int): last update event status received
234 last_known_version (string): last known version reported in update ping
235 forced_update_label (string): update label to force next update ping to
236 use, set by setnextupdate
237 See the OmahaEvent class in update_engine/omaha_request_action.h for
238 event type and status code definitions. If the ip does not exist an empty
239 string is returned.
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700240
Gilad Arnold1b908392012-10-05 11:36:27 -0700241 Example URL:
242 http://myhost/api/hostinfo?ip=192.168.1.5
243 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700244 return updater.HandleHostInfoPing(ip)
245
246 @cherrypy.expose
Gilad Arnold286a0062012-01-12 13:47:02 -0800247 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 11:36:27 -0700248 """Returns a JSON object containing a log of host event.
249
250 Args:
251 ip: address of host whose event log is requested, or `all'
252 Returns:
253 A JSON encoded list (log) of dictionaries (events), each of which
254 containing a `timestamp' and other event fields, as described under
255 /api/hostinfo.
256
257 Example URL:
258 http://myhost/api/hostlog?ip=192.168.1.5
259 """
Gilad Arnold286a0062012-01-12 13:47:02 -0800260 return updater.HandleHostLogPing(ip)
261
262 @cherrypy.expose
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700263 def setnextupdate(self, ip):
264 """Allows the response to the next update ping from a host to be set.
265
266 Takes the IP of the host and an update label as normally provided to the
Gilad Arnold1b908392012-10-05 11:36:27 -0700267 /update command.
268 """
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700269 body_length = int(cherrypy.request.headers['Content-Length'])
270 label = cherrypy.request.rfile.read(body_length)
271
272 if label:
273 label = label.strip()
274 if label:
275 return updater.HandleSetUpdatePing(ip, label)
276 raise cherrypy.HTTPError(400, 'No label provided.')
277
278
Gilad Arnold55a2a372012-10-02 09:46:32 -0700279 @cherrypy.expose
280 def fileinfo(self, *path_args):
281 """Returns information about a given staged file.
282
283 Args:
284 path_args: path to the file inside the server's static staging directory
285 Returns:
286 A JSON encoded dictionary with information about the said file, which may
287 contain the following keys/values:
Gilad Arnold1b908392012-10-05 11:36:27 -0700288 size (int): the file size in bytes
289 sha1 (string): a base64 encoded SHA1 hash
290 sha256 (string): a base64 encoded SHA256 hash
291
292 Example URL:
293 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 09:46:32 -0700294 """
295 file_path = os.path.join(updater.static_dir, *path_args)
296 if not os.path.exists(file_path):
297 raise DevServerError('file not found: %s' % file_path)
298 try:
299 file_size = os.path.getsize(file_path)
300 file_sha1 = common_util.GetFileSha1(file_path)
301 file_sha256 = common_util.GetFileSha256(file_path)
302 except os.error, e:
303 raise DevServerError('failed to get info for file %s: %s' %
Gilad Arnold29021592013-02-15 16:19:17 -0800304 (file_path, e))
305
306 is_delta = autoupdate.Autoupdate.IsDeltaFormatFile(file_path)
307
308 return json.dumps({
309 autoupdate.Autoupdate.SIZE_ATTR: file_size,
310 autoupdate.Autoupdate.SHA1_ATTR: file_sha1,
311 autoupdate.Autoupdate.SHA256_ATTR: file_sha256,
312 autoupdate.Autoupdate.ISDELTA_ATTR: is_delta
313 })
Gilad Arnold55a2a372012-10-02 09:46:32 -0700314
Chris Sosa76e44b92013-01-31 12:11:38 -0800315
David Rochberg7c79a812011-01-19 14:24:45 -0500316class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700317 """The Root Class for the Dev Server.
318
319 CherryPy works as follows:
320 For each method in this class, cherrpy interprets root/path
321 as a call to an instance of DevServerRoot->method_name. For example,
322 a call to http://myhost/build will call build. CherryPy automatically
323 parses http args and places them as keyword arguments in each method.
324 For paths http://myhost/update/dir1/dir2, you can use *args so that
325 cherrypy uses the update method and puts the extra paths in args.
326 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700327 # Method names that should not be listed on the index page.
328 _UNLISTED_METHODS = ['index', 'doc']
329
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700330 api = ApiRoot()
Chris Sosa7c931362010-10-11 19:49:01 -0700331
David Rochberg7c79a812011-01-19 14:24:45 -0500332 def __init__(self):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700333 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800334 self._telemetry_lock_dict = common_util.LockDict()
David Rochberg7c79a812011-01-19 14:24:45 -0500335
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700336 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500337 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700338 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700339 import builder
340 if self._builder is None:
341 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500342 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700343
Chris Sosacde6bf42012-05-31 18:36:39 -0700344 @staticmethod
345 def _canonicalize_archive_url(archive_url):
346 """Canonicalizes archive_url strings.
347
348 Raises:
349 DevserverError: if archive_url is not set.
350 """
351 if archive_url:
Chris Sosa76e44b92013-01-31 12:11:38 -0800352 if not archive_url.startswith('gs://'):
353 raise DevServerError("Archive URL isn't from Google Storage.")
354
Chris Sosacde6bf42012-05-31 18:36:39 -0700355 return archive_url.rstrip('/')
356 else:
357 raise DevServerError("Must specify an archive_url in the request")
358
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700359 @cherrypy.expose
Frank Farzanbcb571e2012-01-03 11:48:17 -0800360 def download(self, **kwargs):
361 """Downloads and archives full/delta payloads from Google Storage.
362
Chris Sosa76e44b92013-01-31 12:11:38 -0800363 THIS METHOD IS DEPRECATED: use stage(..., artifacts=...) instead.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700364 This methods downloads artifacts. It may download artifacts in the
365 background in which case a caller should call wait_for_status to get
366 the status of the background artifact downloads. They should use the same
367 args passed to download.
368
Frank Farzanbcb571e2012-01-03 11:48:17 -0800369 Args:
370 archive_url: Google Storage URL for the build.
371
372 Example URL:
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700373 http://myhost/download?archive_url=gs://chromeos-image-archive/
374 x86-generic/R17-1208.0.0-a1-b338
Frank Farzanbcb571e2012-01-03 11:48:17 -0800375 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800376 return self.stage(archive_url=kwargs.get('archive_url'),
377 artifacts='full_payload,test_suites,stateful')
378
379 @cherrypy.expose
380 def stage(self, **kwargs):
381 """Downloads and caches the artifacts from Google Storage URL.
382
383 Downloads and caches the artifacts Google Storage URL. Returns once these
384 have been downloaded on the devserver. A call to this will attempt to cache
385 non-specified artifacts in the background for the given from the given URL
386 following the principle of spatial locality. Spatial locality of different
387 artifacts is explicitly defined in the build_artifact module.
388
389 These artifacts will then be available from the static/ sub-directory of
390 the devserver.
391
392 Args:
393 archive_url: Google Storage URL for the build.
394 artifacts: Comma separated list of artifacts to download.
395
396 Example:
397 To download the autotest and test suites tarballs:
398 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
399 artifacts=autotest,test_suites
400 To download the full update payload:
401 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
402 artifacts=full_payload
403
404 For both these examples, one could find these artifacts at:
405 http://devserver_url:<port>/static/archive/<relative_path>*
406
407 Note for this example, relative path is the archive_url stripped of its
408 basename i.e. path/ in the examples above. Specific example:
409
410 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
411
412 Will get staged to:
413
414 http://devserver_url:<port>/static/archive/x86-mario-release/R26-3920.0.0
415 """
Chris Sosacde6bf42012-05-31 18:36:39 -0700416 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
Chris Sosa76e44b92013-01-31 12:11:38 -0800417 artifacts = kwargs.get('artifacts', '')
418 if not artifacts:
419 raise DevServerError('No artifacts specified.')
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700420
Chris Sosa76e44b92013-01-31 12:11:38 -0800421 downloader.Downloader(updater.static_dir, archive_url).Download(
422 artifacts.split(','))
423 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700424
425 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -0800426 def setup_telemetry(self, **kwargs):
427 """Extracts and sets up telemetry
428
429 This method goes through the telemetry deps packages, and stages them on
430 the devserver to be used by the drones and the telemetry tests.
431
432 Args:
433 archive_url: Google Storage URL for the build.
434
435 Returns:
436 Path to the source folder for the telemetry codebase once it is staged.
437 """
438 archive_url = kwargs.get('archive_url')
439 self.stage(archive_url=archive_url, artifacts='autotest')
440
441 build = '/'.join(downloader.Downloader.ParseUrl(archive_url))
442 build_path = os.path.join(updater.static_dir, build)
443 deps_path = os.path.join(build_path, 'autotest/packages')
444 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
445 src_folder = os.path.join(telemetry_path, 'src')
446
447 with self._telemetry_lock_dict.lock(telemetry_path):
448 if os.path.exists(src_folder):
449 # Telemetry is already fully stage return
450 return src_folder
451
452 common_util.MkDirP(telemetry_path)
453
454 # Copy over the required deps tar balls to the telemetry directory.
455 for dep in TELEMETRY_DEPS:
456 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -0700457 if not os.path.exists(dep_path):
458 # This dep does not exist (could be new), do not extract it.
459 continue
Simran Basi4baad082013-02-14 13:39:18 -0800460 try:
461 common_util.ExtractTarball(dep_path, telemetry_path)
462 except common_util.CommonUtilError as e:
463 shutil.rmtree(telemetry_path)
464 raise DevServerError(str(e))
465
466 # By default all the tarballs extract to test_src but some parts of
467 # the telemetry code specifically hardcoded to exist inside of 'src'.
468 test_src = os.path.join(telemetry_path, 'test_src')
469 try:
470 shutil.move(test_src, src_folder)
471 except shutil.Error:
472 # This can occur if src_folder already exists. Remove and retry move.
473 shutil.rmtree(src_folder)
474 raise DevServerError('Failure in telemetry setup for build %s. Appears'
475 ' that the test_src to src move failed.' % build)
476
477 return src_folder
478
479 @cherrypy.expose
Chris Sosacde6bf42012-05-31 18:36:39 -0700480 def wait_for_status(self, **kwargs):
481 """Waits for background artifacts to be downloaded from Google Storage.
482
Chris Sosa76e44b92013-01-31 12:11:38 -0800483 THIS METHOD IS DEPRECATED: use stage(..., artifacts=...) instead.
Chris Sosacde6bf42012-05-31 18:36:39 -0700484 Args:
485 archive_url: Google Storage URL for the build.
486
487 Example URL:
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700488 http://myhost/wait_for_status?archive_url=gs://chromeos-image-archive/
489 x86-generic/R17-1208.0.0-a1-b338
Chris Sosacde6bf42012-05-31 18:36:39 -0700490 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800491 return self.stage(archive_url=kwargs.get('archive_url'),
492 artifacts='full_payload,test_suites,autotest,stateful')
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700493
494 @cherrypy.expose
Chris Masone816e38c2012-05-02 12:22:36 -0700495 def stage_debug(self, **kwargs):
496 """Downloads and stages debug symbol payloads from Google Storage.
497
Chris Sosa76e44b92013-01-31 12:11:38 -0800498 THIS METHOD IS DEPRECATED: use stage(..., artifacts=...) instead.
499 This methods downloads the debug symbol build artifact
500 synchronously, and then stages it for use by symbolicate_dump.
Chris Masone816e38c2012-05-02 12:22:36 -0700501
502 Args:
503 archive_url: Google Storage URL for the build.
504
505 Example URL:
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700506 http://myhost/stage_debug?archive_url=gs://chromeos-image-archive/
507 x86-generic/R17-1208.0.0-a1-b338
Chris Masone816e38c2012-05-02 12:22:36 -0700508 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800509 return self.stage(archive_url=kwargs.get('archive_url'),
510 artifacts='symbols')
Chris Masone816e38c2012-05-02 12:22:36 -0700511
512 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800513 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -0700514 """Symbolicates a minidump using pre-downloaded symbols, returns it.
515
516 Callers will need to POST to this URL with a body of MIME-type
517 "multipart/form-data".
518 The body should include a single argument, 'minidump', containing the
519 binary-formatted minidump to symbolicate.
520
Chris Masone816e38c2012-05-02 12:22:36 -0700521 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -0800522 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -0700523 minidump: The binary minidump file to symbolicate.
524 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800525 # Ensure the symbols have been staged.
526 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
527 if self.stage(archive_url=archive_url, artifacts='symbols') != 'Success':
528 raise DevServerError('Failed to stage symbols for %s' % archive_url)
529
Chris Masone816e38c2012-05-02 12:22:36 -0700530 to_return = ''
531 with tempfile.NamedTemporaryFile() as local:
532 while True:
533 data = minidump.file.read(8192)
534 if not data:
535 break
536 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -0800537
Chris Masone816e38c2012-05-02 12:22:36 -0700538 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -0800539
540 symbols_directory = os.path.join(downloader.Downloader.GetBuildDir(
541 updater.static_dir, archive_url), 'debug', 'breakpad')
542
543 stackwalk = subprocess.Popen(
544 ['minidump_stackwalk', local.name, symbols_directory],
545 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
546
Chris Masone816e38c2012-05-02 12:22:36 -0700547 to_return, error_text = stackwalk.communicate()
548 if stackwalk.returncode != 0:
549 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
550 error_text, stackwalk.returncode))
551
552 return to_return
553
554 @cherrypy.expose
Scott Zawalski16954532012-03-20 15:31:36 -0400555 def latestbuild(self, **params):
556 """Return a string representing the latest build for a given target.
557
558 Args:
559 target: The build target, typically a combination of the board and the
560 type of build e.g. x86-mario-release.
561 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
562 provided the latest RXX build will be returned.
563 Returns:
564 A string representation of the latest build if one exists, i.e.
565 R19-1993.0.0-a1-b1480.
566 An empty string if no latest could be found.
567 """
568 if not params:
569 return _PrintDocStringAsHTML(self.latestbuild)
570
571 if 'target' not in params:
572 raise cherrypy.HTTPError('500 Internal Server Error',
573 'Error: target= is required!')
574 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700575 return common_util.GetLatestBuildVersion(
Scott Zawalski16954532012-03-20 15:31:36 -0400576 updater.static_dir, params['target'],
577 milestone=params.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700578 except common_util.CommonUtilError as errmsg:
Scott Zawalski16954532012-03-20 15:31:36 -0400579 raise cherrypy.HTTPError('500 Internal Server Error', str(errmsg))
580
581 @cherrypy.expose
Scott Zawalski84a39c92012-01-13 15:12:42 -0500582 def controlfiles(self, **params):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500583 """Return a control file or a list of all known control files.
584
585 Example URL:
586 To List all control files:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500587 http://dev-server/controlfiles?board=x86-alex-release&build=R18-1514.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -0500588 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500589 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 -0500590
591 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500592 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500593 control_path: If you want the contents of a control file set this
594 to the path. E.g. client/site_tests/sleeptest/control
595 Optional, if not provided return a list of control files is returned.
596 Returns:
597 Contents of a control file if control_path is provided.
598 A list of control files if no control_path is provided.
599 """
Scott Zawalski4647ce62012-01-03 17:17:28 -0500600 if not params:
601 return _PrintDocStringAsHTML(self.controlfiles)
602
Scott Zawalski84a39c92012-01-13 15:12:42 -0500603 if 'build' not in params:
Scott Zawalski4647ce62012-01-03 17:17:28 -0500604 raise cherrypy.HTTPError('500 Internal Server Error',
Scott Zawalski84a39c92012-01-13 15:12:42 -0500605 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -0500606
607 if 'control_path' not in params:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700608 return common_util.GetControlFileList(
609 updater.static_dir, params['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -0500610 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700611 return common_util.GetControlFile(
612 updater.static_dir, params['build'], params['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -0800613
614 @cherrypy.expose
Gilad Arnold6f99b982012-09-12 10:49:40 -0700615 def stage_images(self, **kwargs):
616 """Downloads and stages a Chrome OS image from Google Storage.
617
Chris Sosa76e44b92013-01-31 12:11:38 -0800618 THIS METHOD IS DEPRECATED: use stage(..., artifacts=...) instead.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700619 This method downloads a zipped archive from a specified GS location, then
620 extracts and stages the specified list of images and stages them under
Chris Sosa76e44b92013-01-31 12:11:38 -0800621 static/BOARD/BUILD/. Download is synchronous.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700622
623 Args:
624 archive_url: Google Storage URL for the build.
625 image_types: comma-separated list of images to download, may include
626 'test', 'recovery', and 'base'
627
628 Example URL:
629 http://myhost/stage_images?archive_url=gs://chromeos-image-archive/
630 x86-generic/R17-1208.0.0-a1-b338&image_types=test,base
631 """
Gilad Arnold6f99b982012-09-12 10:49:40 -0700632 image_types = kwargs.get('image_types').split(',')
Chris Sosa76e44b92013-01-31 12:11:38 -0800633 image_types_list = [image + '_image' for image in image_types]
634 self.stage(archive_url=kwargs.get('archive_url'), artifacts=','.join(
635 image_types_list))
Gilad Arnold6f99b982012-09-12 10:49:40 -0700636
637 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700638 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700639 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700640 return ('Welcome to the Dev Server!<br>\n'
641 '<br>\n'
642 'Here are the available methods, click for documentation:<br>\n'
643 '<br>\n'
644 '%s' %
645 '<br>\n'.join(
646 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700647 for name in _FindExposedMethods(
648 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700649
650 @cherrypy.expose
651 def doc(self, *args):
652 """Shows the documentation for available methods / URLs.
653
654 Example:
655 http://myhost/doc/update
656 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700657 name = '/'.join(args)
658 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700659 if not method:
660 raise DevServerError("No exposed method named `%s'" % name)
661 if not method.__doc__:
662 raise DevServerError("No documentation for exposed method `%s'" % name)
663 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -0700664
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700665 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700666 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700667 """Handles an update check from a Chrome OS client.
668
669 The HTTP request should contain the standard Omaha-style XML blob. The URL
670 line may contain an additional intermediate path to the update payload.
671
672 Example:
673 http://myhost/update/optional/path/to/payload
674 """
Chris Sosa7c931362010-10-11 19:49:01 -0700675 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -0800676 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -0700677 data = cherrypy.request.rfile.read(body_length)
678 return updater.HandleUpdatePing(data, label)
679
Chris Sosa0356d3b2010-09-16 15:46:22 -0700680
Chris Sosadbc20082012-12-10 13:39:11 -0800681def _CleanCache(cache_dir, wipe):
682 """Wipes any excess cached items in the cache_dir.
683
684 Args:
685 cache_dir: the directory we are wiping from.
686 wipe: If True, wipe all the contents -- not just the excess.
687 """
688 if wipe:
689 # Clear the cache and exit on error.
690 cmd = 'rm -rf %s/*' % cache_dir
691 if os.system(cmd) != 0:
692 _Log('Failed to clear the cache with %s' % cmd)
693 sys.exit(1)
694 else:
695 # Clear all but the last N cached updates
696 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
697 (cache_dir, CACHED_ENTRIES))
698 if os.system(cmd) != 0:
699 _Log('Failed to clean up old delta cache files with %s' % cmd)
700 sys.exit(1)
701
702
Chris Sosacde6bf42012-05-31 18:36:39 -0700703def main():
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700704 usage = 'usage: %prog [options]'
Gilad Arnold286a0062012-01-12 13:47:02 -0800705 parser = optparse.OptionParser(usage=usage)
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700706 parser.add_option('--archive_dir',
707 metavar='PATH',
Chris Sosadbc20082012-12-10 13:39:11 -0800708 help='Enables serve-only mode. Serves archived builds only')
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700709 parser.add_option('--board',
710 help='when pre-generating update, board for latest image')
711 parser.add_option('--clear_cache',
Satoru Takabayashid733cbe2011-11-15 09:36:32 -0800712 action='store_true', default=False,
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700713 help='clear out all cached updates and exit')
714 parser.add_option('--critical_update',
715 action='store_true', default=False,
716 help='present update payload as critical')
717 parser.add_option('--data_dir',
718 metavar='PATH',
719 default=os.path.dirname(os.path.abspath(sys.argv[0])),
720 help='writable directory where static lives')
721 parser.add_option('--exit',
722 action='store_true',
723 help='do not start server (yet pregenerate/clear cache)')
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700724 parser.add_option('--for_vm',
725 dest='vm', action='store_true',
726 help='update is for a vm image')
Gilad Arnold8318eac2012-10-04 12:52:23 -0700727 parser.add_option('--host_log',
728 action='store_true', default=False,
729 help='record history of host update events (/api/hostlog)')
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700730 parser.add_option('--image',
731 metavar='FILE',
Chris Sosadbc20082012-12-10 13:39:11 -0800732 help='Force update using this image. Can only be used when '
733 'not in serve-only mode as it is used to generate a '
734 'payload.')
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700735 parser.add_option('--logfile',
736 metavar='PATH',
737 help='log output to this file instead of stdout')
Gilad Arnolda564b4b2012-10-04 10:32:44 -0700738 parser.add_option('--max_updates',
Chris Sosa76e44b92013-01-31 12:11:38 -0800739 metavar='NUM', default= -1, type='int',
Gilad Arnolda564b4b2012-10-04 10:32:44 -0700740 help='maximum number of update checks handled positively '
741 '(default: unlimited)')
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700742 parser.add_option('-p', '--pregenerate_update',
743 action='store_true', default=False,
Chris Sosadbc20082012-12-10 13:39:11 -0800744 help='pre-generate update payload. Can only be used when '
745 'not in serve-only mode as it is used to generate a '
746 'payload.')
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700747 parser.add_option('--payload',
748 metavar='PATH',
749 help='use update payload from specified directory')
750 parser.add_option('--port',
751 default=8080, type='int',
752 help='port for the dev server to use (default: 8080)')
753 parser.add_option('--private_key',
754 metavar='PATH', default=None,
755 help='path to the private key in pem format')
756 parser.add_option('--production',
757 action='store_true', default=False,
758 help='have the devserver use production values')
759 parser.add_option('--proxy_port',
760 metavar='PORT', default=None, type='int',
761 help='port to have the client connect to (testing support)')
762 parser.add_option('--remote_payload',
763 action='store_true', default=False,
764 help='Payload is being served from a remote machine')
765 parser.add_option('--src_image',
766 metavar='PATH', default='',
767 help='source image for generating delta updates from')
768 parser.add_option('-t', '--test_image',
769 action='store_true',
770 help='whether or not to use test images')
771 parser.add_option('-u', '--urlbase',
772 metavar='URL',
773 help='base URL for update images, other than the devserver')
Chris Sosa7c931362010-10-11 19:49:01 -0700774 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +0000775
Chris Sosa7c931362010-10-11 19:49:01 -0700776 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
777 root_dir = os.path.realpath('%s/../..' % devserver_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700778 serve_only = False
779
Zdenek Behan608f46c2011-02-19 00:47:16 +0100780 static_dir = os.path.realpath('%s/static' % options.data_dir)
781 os.system('mkdir -p %s' % static_dir)
782
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700783 if options.archive_dir:
Zdenek Behan608f46c2011-02-19 00:47:16 +0100784 # TODO(zbehan) Remove legacy support:
785 # archive_dir is the directory where static/archive will point.
786 # If this is an absolute path, all is fine. If someone calls this
787 # using a relative path, that is relative to src/platform/dev/.
788 # That use case is unmaintainable, but since applications use it
789 # with =./static, instead of a boolean flag, we'll make this relative
790 # to devserver_dir to keep these unbroken. For now.
791 archive_dir = options.archive_dir
792 if not os.path.isabs(archive_dir):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700793 archive_dir = os.path.realpath(os.path.join(devserver_dir, archive_dir))
Zdenek Behan608f46c2011-02-19 00:47:16 +0100794 _PrepareToServeUpdatesOnly(archive_dir, static_dir)
Zdenek Behan6d93e552011-03-02 22:35:49 +0100795 static_dir = os.path.realpath(archive_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700796 serve_only = True
Chris Sosa0356d3b2010-09-16 15:46:22 -0700797
Don Garrettf90edf02010-11-16 17:36:14 -0800798 cache_dir = os.path.join(static_dir, 'cache')
Chris Sosadbc20082012-12-10 13:39:11 -0800799 # If our devserver is only supposed to serve payloads, we shouldn't be mucking
800 # with the cache at all. If the devserver hadn't previously generated a cache
801 # and is expected, the caller is using it wrong.
802 if serve_only:
803 # Extra check to make sure we're not being called incorrectly.
804 if (options.clear_cache or options.exit or options.pregenerate_update or
805 options.board or options.image):
806 parser.error('Incompatible flags detected for serve_only mode.')
Don Garrettf90edf02010-11-16 17:36:14 -0800807
Chris Sosadbc20082012-12-10 13:39:11 -0800808 elif os.path.exists(cache_dir):
809 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -0800810 else:
811 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800812
Chris Sosadbc20082012-12-10 13:39:11 -0800813 _Log('Using cache directory %s' % cache_dir)
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700814 _Log('Data dir is %s' % options.data_dir)
815 _Log('Source root is %s' % root_dir)
816 _Log('Serving from %s' % static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +0000817
Chris Sosa6a3697f2013-01-29 16:44:43 -0800818 # We allow global use here to share with cherrypy classes.
819 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -0700820 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -0700821 updater = autoupdate.Autoupdate(
822 root_dir=root_dir,
823 static_dir=static_dir,
Chris Sosa0356d3b2010-09-16 15:46:22 -0700824 serve_only=serve_only,
Andrew de los Reyes52620802010-04-12 13:40:07 -0700825 urlbase=options.urlbase,
826 test_image=options.test_image,
Chris Sosa5d342a22010-09-28 16:54:41 -0700827 forced_image=options.image,
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700828 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -0800829 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-28 23:42:37 -0700830 src_image=options.src_image,
Chris Sosae67b78f2010-11-04 17:33:16 -0700831 vm=options.vm,
Chris Sosa08d55a22011-01-19 16:08:02 -0800832 board=options.board,
Chris Sosa0f1ec842011-02-14 16:33:22 -0800833 copy_to_static_root=not options.exit,
834 private_key=options.private_key,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -0800835 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700836 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 10:32:44 -0700837 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -0700838 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -0800839 )
Chris Sosa7c931362010-10-11 19:49:01 -0700840
Chris Sosa6a3697f2013-01-29 16:44:43 -0800841 if options.pregenerate_update:
842 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 15:46:22 -0700843
Don Garrett0c880e22010-11-17 18:13:37 -0800844 # If the command line requested after setup, it's time to do it.
845 if not options.exit:
Chris Sosa66e2d9c2012-07-11 14:14:14 -0700846 # Handle options that must be set globally in cherrypy.
Chris Sosa2f1c41e2012-07-10 14:32:33 -0700847 if options.production:
Chris Sosa66e2d9c2012-07-11 14:14:14 -0700848 cherrypy.config.update({'environment': 'production'})
849 if not options.logfile:
850 cherrypy.config.update({'log.screen': True})
851 else:
852 cherrypy.config.update({'log.error_file': options.logfile,
853 'log.access_file': options.logfile})
Chris Sosa2f1c41e2012-07-10 14:32:33 -0700854
Don Garrett0c880e22010-11-17 18:13:37 -0800855 cherrypy.quickstart(DevServerRoot(), config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -0700856
857
858if __name__ == '__main__':
859 main()