blob: b45a235ab5ea73f6133dfaa91162330713ae2ed2 [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' %
Simran Basi40836b22013-03-28 15:08:17 -0700304 (file_path, str(e)))
305 return json.dumps(
306 {'size': file_size, 'sha1': file_sha1, 'sha256': file_sha256})
Gilad Arnold55a2a372012-10-02 09:46:32 -0700307
Chris Sosa76e44b92013-01-31 12:11:38 -0800308
David Rochberg7c79a812011-01-19 14:24:45 -0500309class DevServerRoot(object):
Chris Sosa7c931362010-10-11 19:49:01 -0700310 """The Root Class for the Dev Server.
311
312 CherryPy works as follows:
313 For each method in this class, cherrpy interprets root/path
314 as a call to an instance of DevServerRoot->method_name. For example,
315 a call to http://myhost/build will call build. CherryPy automatically
316 parses http args and places them as keyword arguments in each method.
317 For paths http://myhost/update/dir1/dir2, you can use *args so that
318 cherrypy uses the update method and puts the extra paths in args.
319 """
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700320 # Method names that should not be listed on the index page.
321 _UNLISTED_METHODS = ['index', 'doc']
322
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700323 api = ApiRoot()
Chris Sosa7c931362010-10-11 19:49:01 -0700324
David Rochberg7c79a812011-01-19 14:24:45 -0500325 def __init__(self):
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700326 self._builder = None
Simran Basi4baad082013-02-14 13:39:18 -0800327 self._telemetry_lock_dict = common_util.LockDict()
David Rochberg7c79a812011-01-19 14:24:45 -0500328
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700329 @cherrypy.expose
David Rochberg7c79a812011-01-19 14:24:45 -0500330 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-11 19:49:01 -0700331 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 15:20:41 -0700332 import builder
333 if self._builder is None:
334 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 14:24:45 -0500335 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-11 19:49:01 -0700336
Chris Sosacde6bf42012-05-31 18:36:39 -0700337 @staticmethod
338 def _canonicalize_archive_url(archive_url):
339 """Canonicalizes archive_url strings.
340
341 Raises:
342 DevserverError: if archive_url is not set.
343 """
344 if archive_url:
Chris Sosa76e44b92013-01-31 12:11:38 -0800345 if not archive_url.startswith('gs://'):
346 raise DevServerError("Archive URL isn't from Google Storage.")
347
Chris Sosacde6bf42012-05-31 18:36:39 -0700348 return archive_url.rstrip('/')
349 else:
350 raise DevServerError("Must specify an archive_url in the request")
351
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700352 @cherrypy.expose
Frank Farzanbcb571e2012-01-03 11:48:17 -0800353 def download(self, **kwargs):
354 """Downloads and archives full/delta payloads from Google Storage.
355
Chris Sosa76e44b92013-01-31 12:11:38 -0800356 THIS METHOD IS DEPRECATED: use stage(..., artifacts=...) instead.
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700357 This methods downloads artifacts. It may download artifacts in the
358 background in which case a caller should call wait_for_status to get
359 the status of the background artifact downloads. They should use the same
360 args passed to download.
361
Frank Farzanbcb571e2012-01-03 11:48:17 -0800362 Args:
363 archive_url: Google Storage URL for the build.
364
365 Example URL:
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700366 http://myhost/download?archive_url=gs://chromeos-image-archive/
367 x86-generic/R17-1208.0.0-a1-b338
Frank Farzanbcb571e2012-01-03 11:48:17 -0800368 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800369 return self.stage(archive_url=kwargs.get('archive_url'),
370 artifacts='full_payload,test_suites,stateful')
371
372 @cherrypy.expose
373 def stage(self, **kwargs):
374 """Downloads and caches the artifacts from Google Storage URL.
375
376 Downloads and caches the artifacts Google Storage URL. Returns once these
377 have been downloaded on the devserver. A call to this will attempt to cache
378 non-specified artifacts in the background for the given from the given URL
379 following the principle of spatial locality. Spatial locality of different
380 artifacts is explicitly defined in the build_artifact module.
381
382 These artifacts will then be available from the static/ sub-directory of
383 the devserver.
384
385 Args:
386 archive_url: Google Storage URL for the build.
387 artifacts: Comma separated list of artifacts to download.
388
389 Example:
390 To download the autotest and test suites tarballs:
391 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
392 artifacts=autotest,test_suites
393 To download the full update payload:
394 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
395 artifacts=full_payload
396
397 For both these examples, one could find these artifacts at:
398 http://devserver_url:<port>/static/archive/<relative_path>*
399
400 Note for this example, relative path is the archive_url stripped of its
401 basename i.e. path/ in the examples above. Specific example:
402
403 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
404
405 Will get staged to:
406
407 http://devserver_url:<port>/static/archive/x86-mario-release/R26-3920.0.0
408 """
Chris Sosacde6bf42012-05-31 18:36:39 -0700409 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
Chris Sosa76e44b92013-01-31 12:11:38 -0800410 artifacts = kwargs.get('artifacts', '')
411 if not artifacts:
412 raise DevServerError('No artifacts specified.')
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700413
Chris Sosa76e44b92013-01-31 12:11:38 -0800414 downloader.Downloader(updater.static_dir, archive_url).Download(
415 artifacts.split(','))
416 return 'Success'
Chris Sosacde6bf42012-05-31 18:36:39 -0700417
418 @cherrypy.expose
Simran Basi4baad082013-02-14 13:39:18 -0800419 def setup_telemetry(self, **kwargs):
420 """Extracts and sets up telemetry
421
422 This method goes through the telemetry deps packages, and stages them on
423 the devserver to be used by the drones and the telemetry tests.
424
425 Args:
426 archive_url: Google Storage URL for the build.
427
428 Returns:
429 Path to the source folder for the telemetry codebase once it is staged.
430 """
431 archive_url = kwargs.get('archive_url')
432 self.stage(archive_url=archive_url, artifacts='autotest')
433
434 build = '/'.join(downloader.Downloader.ParseUrl(archive_url))
435 build_path = os.path.join(updater.static_dir, build)
436 deps_path = os.path.join(build_path, 'autotest/packages')
437 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
438 src_folder = os.path.join(telemetry_path, 'src')
439
440 with self._telemetry_lock_dict.lock(telemetry_path):
441 if os.path.exists(src_folder):
442 # Telemetry is already fully stage return
443 return src_folder
444
445 common_util.MkDirP(telemetry_path)
446
447 # Copy over the required deps tar balls to the telemetry directory.
448 for dep in TELEMETRY_DEPS:
449 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 16:40:04 -0700450 if not os.path.exists(dep_path):
451 # This dep does not exist (could be new), do not extract it.
452 continue
Simran Basi4baad082013-02-14 13:39:18 -0800453 try:
454 common_util.ExtractTarball(dep_path, telemetry_path)
455 except common_util.CommonUtilError as e:
456 shutil.rmtree(telemetry_path)
457 raise DevServerError(str(e))
458
459 # By default all the tarballs extract to test_src but some parts of
460 # the telemetry code specifically hardcoded to exist inside of 'src'.
461 test_src = os.path.join(telemetry_path, 'test_src')
462 try:
463 shutil.move(test_src, src_folder)
464 except shutil.Error:
465 # This can occur if src_folder already exists. Remove and retry move.
466 shutil.rmtree(src_folder)
467 raise DevServerError('Failure in telemetry setup for build %s. Appears'
468 ' that the test_src to src move failed.' % build)
469
470 return src_folder
471
472 @cherrypy.expose
Chris Sosacde6bf42012-05-31 18:36:39 -0700473 def wait_for_status(self, **kwargs):
474 """Waits for background artifacts to be downloaded from Google Storage.
475
Chris Sosa76e44b92013-01-31 12:11:38 -0800476 THIS METHOD IS DEPRECATED: use stage(..., artifacts=...) instead.
Chris Sosacde6bf42012-05-31 18:36:39 -0700477 Args:
478 archive_url: Google Storage URL for the build.
479
480 Example URL:
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700481 http://myhost/wait_for_status?archive_url=gs://chromeos-image-archive/
482 x86-generic/R17-1208.0.0-a1-b338
Chris Sosacde6bf42012-05-31 18:36:39 -0700483 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800484 return self.stage(archive_url=kwargs.get('archive_url'),
485 artifacts='full_payload,test_suites,autotest,stateful')
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700486
487 @cherrypy.expose
Chris Masone816e38c2012-05-02 12:22:36 -0700488 def stage_debug(self, **kwargs):
489 """Downloads and stages debug symbol payloads from Google Storage.
490
Chris Sosa76e44b92013-01-31 12:11:38 -0800491 THIS METHOD IS DEPRECATED: use stage(..., artifacts=...) instead.
492 This methods downloads the debug symbol build artifact
493 synchronously, and then stages it for use by symbolicate_dump.
Chris Masone816e38c2012-05-02 12:22:36 -0700494
495 Args:
496 archive_url: Google Storage URL for the build.
497
498 Example URL:
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700499 http://myhost/stage_debug?archive_url=gs://chromeos-image-archive/
500 x86-generic/R17-1208.0.0-a1-b338
Chris Masone816e38c2012-05-02 12:22:36 -0700501 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800502 return self.stage(archive_url=kwargs.get('archive_url'),
503 artifacts='symbols')
Chris Masone816e38c2012-05-02 12:22:36 -0700504
505 @cherrypy.expose
Chris Sosa76e44b92013-01-31 12:11:38 -0800506 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 12:22:36 -0700507 """Symbolicates a minidump using pre-downloaded symbols, returns it.
508
509 Callers will need to POST to this URL with a body of MIME-type
510 "multipart/form-data".
511 The body should include a single argument, 'minidump', containing the
512 binary-formatted minidump to symbolicate.
513
Chris Masone816e38c2012-05-02 12:22:36 -0700514 Args:
Chris Sosa76e44b92013-01-31 12:11:38 -0800515 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -0700516 minidump: The binary minidump file to symbolicate.
517 """
Chris Sosa76e44b92013-01-31 12:11:38 -0800518 # Ensure the symbols have been staged.
519 archive_url = self._canonicalize_archive_url(kwargs.get('archive_url'))
520 if self.stage(archive_url=archive_url, artifacts='symbols') != 'Success':
521 raise DevServerError('Failed to stage symbols for %s' % archive_url)
522
Chris Masone816e38c2012-05-02 12:22:36 -0700523 to_return = ''
524 with tempfile.NamedTemporaryFile() as local:
525 while True:
526 data = minidump.file.read(8192)
527 if not data:
528 break
529 local.write(data)
Chris Sosa76e44b92013-01-31 12:11:38 -0800530
Chris Masone816e38c2012-05-02 12:22:36 -0700531 local.flush()
Chris Sosa76e44b92013-01-31 12:11:38 -0800532
533 symbols_directory = os.path.join(downloader.Downloader.GetBuildDir(
534 updater.static_dir, archive_url), 'debug', 'breakpad')
535
536 stackwalk = subprocess.Popen(
537 ['minidump_stackwalk', local.name, symbols_directory],
538 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
539
Chris Masone816e38c2012-05-02 12:22:36 -0700540 to_return, error_text = stackwalk.communicate()
541 if stackwalk.returncode != 0:
542 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
543 error_text, stackwalk.returncode))
544
545 return to_return
546
547 @cherrypy.expose
Scott Zawalski16954532012-03-20 15:31:36 -0400548 def latestbuild(self, **params):
549 """Return a string representing the latest build for a given target.
550
551 Args:
552 target: The build target, typically a combination of the board and the
553 type of build e.g. x86-mario-release.
554 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
555 provided the latest RXX build will be returned.
556 Returns:
557 A string representation of the latest build if one exists, i.e.
558 R19-1993.0.0-a1-b1480.
559 An empty string if no latest could be found.
560 """
561 if not params:
562 return _PrintDocStringAsHTML(self.latestbuild)
563
564 if 'target' not in params:
565 raise cherrypy.HTTPError('500 Internal Server Error',
566 'Error: target= is required!')
567 try:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700568 return common_util.GetLatestBuildVersion(
Scott Zawalski16954532012-03-20 15:31:36 -0400569 updater.static_dir, params['target'],
570 milestone=params.get('milestone'))
Gilad Arnold17fe03d2012-10-02 10:05:01 -0700571 except common_util.CommonUtilError as errmsg:
Scott Zawalski16954532012-03-20 15:31:36 -0400572 raise cherrypy.HTTPError('500 Internal Server Error', str(errmsg))
573
574 @cherrypy.expose
Scott Zawalski84a39c92012-01-13 15:12:42 -0500575 def controlfiles(self, **params):
Scott Zawalski4647ce62012-01-03 17:17:28 -0500576 """Return a control file or a list of all known control files.
577
578 Example URL:
579 To List all control files:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500580 http://dev-server/controlfiles?board=x86-alex-release&build=R18-1514.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -0500581 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500582 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 -0500583
584 Args:
Scott Zawalski84a39c92012-01-13 15:12:42 -0500585 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500586 control_path: If you want the contents of a control file set this
587 to the path. E.g. client/site_tests/sleeptest/control
588 Optional, if not provided return a list of control files is returned.
589 Returns:
590 Contents of a control file if control_path is provided.
591 A list of control files if no control_path is provided.
592 """
Scott Zawalski4647ce62012-01-03 17:17:28 -0500593 if not params:
594 return _PrintDocStringAsHTML(self.controlfiles)
595
Scott Zawalski84a39c92012-01-13 15:12:42 -0500596 if 'build' not in params:
Scott Zawalski4647ce62012-01-03 17:17:28 -0500597 raise cherrypy.HTTPError('500 Internal Server Error',
Scott Zawalski84a39c92012-01-13 15:12:42 -0500598 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 17:17:28 -0500599
600 if 'control_path' not in params:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700601 return common_util.GetControlFileList(
602 updater.static_dir, params['build'])
Scott Zawalski4647ce62012-01-03 17:17:28 -0500603 else:
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700604 return common_util.GetControlFile(
605 updater.static_dir, params['build'], params['control_path'])
Frank Farzan40160872011-12-12 18:39:18 -0800606
607 @cherrypy.expose
Gilad Arnold6f99b982012-09-12 10:49:40 -0700608 def stage_images(self, **kwargs):
609 """Downloads and stages a Chrome OS image from Google Storage.
610
Chris Sosa76e44b92013-01-31 12:11:38 -0800611 THIS METHOD IS DEPRECATED: use stage(..., artifacts=...) instead.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700612 This method downloads a zipped archive from a specified GS location, then
613 extracts and stages the specified list of images and stages them under
Chris Sosa76e44b92013-01-31 12:11:38 -0800614 static/BOARD/BUILD/. Download is synchronous.
Gilad Arnold6f99b982012-09-12 10:49:40 -0700615
616 Args:
617 archive_url: Google Storage URL for the build.
618 image_types: comma-separated list of images to download, may include
619 'test', 'recovery', and 'base'
620
621 Example URL:
622 http://myhost/stage_images?archive_url=gs://chromeos-image-archive/
623 x86-generic/R17-1208.0.0-a1-b338&image_types=test,base
624 """
Gilad Arnold6f99b982012-09-12 10:49:40 -0700625 image_types = kwargs.get('image_types').split(',')
Chris Sosa76e44b92013-01-31 12:11:38 -0800626 image_types_list = [image + '_image' for image in image_types]
627 self.stage(archive_url=kwargs.get('archive_url'), artifacts=','.join(
628 image_types_list))
Gilad Arnold6f99b982012-09-12 10:49:40 -0700629
630 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700631 def index(self):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700632 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700633 return ('Welcome to the Dev Server!<br>\n'
634 '<br>\n'
635 'Here are the available methods, click for documentation:<br>\n'
636 '<br>\n'
637 '%s' %
638 '<br>\n'.join(
639 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700640 for name in _FindExposedMethods(
641 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700642
643 @cherrypy.expose
644 def doc(self, *args):
645 """Shows the documentation for available methods / URLs.
646
647 Example:
648 http://myhost/doc/update
649 """
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700650 name = '/'.join(args)
651 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700652 if not method:
653 raise DevServerError("No exposed method named `%s'" % name)
654 if not method.__doc__:
655 raise DevServerError("No documentation for exposed method `%s'" % name)
656 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -0700657
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700658 @cherrypy.expose
Chris Sosa7c931362010-10-11 19:49:01 -0700659 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 08:43:01 -0700660 """Handles an update check from a Chrome OS client.
661
662 The HTTP request should contain the standard Omaha-style XML blob. The URL
663 line may contain an additional intermediate path to the update payload.
664
665 Example:
666 http://myhost/update/optional/path/to/payload
667 """
Chris Sosa7c931362010-10-11 19:49:01 -0700668 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 13:47:02 -0800669 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-11 19:49:01 -0700670 data = cherrypy.request.rfile.read(body_length)
671 return updater.HandleUpdatePing(data, label)
672
Chris Sosa0356d3b2010-09-16 15:46:22 -0700673
Chris Sosadbc20082012-12-10 13:39:11 -0800674def _CleanCache(cache_dir, wipe):
675 """Wipes any excess cached items in the cache_dir.
676
677 Args:
678 cache_dir: the directory we are wiping from.
679 wipe: If True, wipe all the contents -- not just the excess.
680 """
681 if wipe:
682 # Clear the cache and exit on error.
683 cmd = 'rm -rf %s/*' % cache_dir
684 if os.system(cmd) != 0:
685 _Log('Failed to clear the cache with %s' % cmd)
686 sys.exit(1)
687 else:
688 # Clear all but the last N cached updates
689 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
690 (cache_dir, CACHED_ENTRIES))
691 if os.system(cmd) != 0:
692 _Log('Failed to clean up old delta cache files with %s' % cmd)
693 sys.exit(1)
694
695
Chris Sosacde6bf42012-05-31 18:36:39 -0700696def main():
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700697 usage = 'usage: %prog [options]'
Gilad Arnold286a0062012-01-12 13:47:02 -0800698 parser = optparse.OptionParser(usage=usage)
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700699 parser.add_option('--archive_dir',
700 metavar='PATH',
Chris Sosadbc20082012-12-10 13:39:11 -0800701 help='Enables serve-only mode. Serves archived builds only')
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700702 parser.add_option('--board',
703 help='when pre-generating update, board for latest image')
704 parser.add_option('--clear_cache',
Satoru Takabayashid733cbe2011-11-15 09:36:32 -0800705 action='store_true', default=False,
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700706 help='clear out all cached updates and exit')
707 parser.add_option('--critical_update',
708 action='store_true', default=False,
709 help='present update payload as critical')
710 parser.add_option('--data_dir',
711 metavar='PATH',
712 default=os.path.dirname(os.path.abspath(sys.argv[0])),
713 help='writable directory where static lives')
714 parser.add_option('--exit',
715 action='store_true',
716 help='do not start server (yet pregenerate/clear cache)')
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700717 parser.add_option('--for_vm',
718 dest='vm', action='store_true',
719 help='update is for a vm image')
Gilad Arnold8318eac2012-10-04 12:52:23 -0700720 parser.add_option('--host_log',
721 action='store_true', default=False,
722 help='record history of host update events (/api/hostlog)')
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700723 parser.add_option('--image',
724 metavar='FILE',
Chris Sosadbc20082012-12-10 13:39:11 -0800725 help='Force update using this image. Can only be used when '
726 'not in serve-only mode as it is used to generate a '
727 'payload.')
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700728 parser.add_option('--logfile',
729 metavar='PATH',
730 help='log output to this file instead of stdout')
Gilad Arnolda564b4b2012-10-04 10:32:44 -0700731 parser.add_option('--max_updates',
Chris Sosa76e44b92013-01-31 12:11:38 -0800732 metavar='NUM', default= -1, type='int',
Gilad Arnolda564b4b2012-10-04 10:32:44 -0700733 help='maximum number of update checks handled positively '
734 '(default: unlimited)')
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700735 parser.add_option('-p', '--pregenerate_update',
736 action='store_true', default=False,
Chris Sosadbc20082012-12-10 13:39:11 -0800737 help='pre-generate update payload. Can only be used when '
738 'not in serve-only mode as it is used to generate a '
739 'payload.')
Gilad Arnold9714d9b2012-10-04 10:09:42 -0700740 parser.add_option('--payload',
741 metavar='PATH',
742 help='use update payload from specified directory')
743 parser.add_option('--port',
744 default=8080, type='int',
745 help='port for the dev server to use (default: 8080)')
746 parser.add_option('--private_key',
747 metavar='PATH', default=None,
748 help='path to the private key in pem format')
749 parser.add_option('--production',
750 action='store_true', default=False,
751 help='have the devserver use production values')
752 parser.add_option('--proxy_port',
753 metavar='PORT', default=None, type='int',
754 help='port to have the client connect to (testing support)')
755 parser.add_option('--remote_payload',
756 action='store_true', default=False,
757 help='Payload is being served from a remote machine')
758 parser.add_option('--src_image',
759 metavar='PATH', default='',
760 help='source image for generating delta updates from')
761 parser.add_option('-t', '--test_image',
762 action='store_true',
763 help='whether or not to use test images')
764 parser.add_option('-u', '--urlbase',
765 metavar='URL',
766 help='base URL for update images, other than the devserver')
Chris Sosa7c931362010-10-11 19:49:01 -0700767 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +0000768
Chris Sosa7c931362010-10-11 19:49:01 -0700769 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
770 root_dir = os.path.realpath('%s/../..' % devserver_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700771 serve_only = False
772
Zdenek Behan608f46c2011-02-19 00:47:16 +0100773 static_dir = os.path.realpath('%s/static' % options.data_dir)
774 os.system('mkdir -p %s' % static_dir)
775
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700776 if options.archive_dir:
Zdenek Behan608f46c2011-02-19 00:47:16 +0100777 # TODO(zbehan) Remove legacy support:
778 # archive_dir is the directory where static/archive will point.
779 # If this is an absolute path, all is fine. If someone calls this
780 # using a relative path, that is relative to src/platform/dev/.
781 # That use case is unmaintainable, but since applications use it
782 # with =./static, instead of a boolean flag, we'll make this relative
783 # to devserver_dir to keep these unbroken. For now.
784 archive_dir = options.archive_dir
785 if not os.path.isabs(archive_dir):
Chris Sosa47a7d4e2012-03-28 11:26:55 -0700786 archive_dir = os.path.realpath(os.path.join(devserver_dir, archive_dir))
Zdenek Behan608f46c2011-02-19 00:47:16 +0100787 _PrepareToServeUpdatesOnly(archive_dir, static_dir)
Zdenek Behan6d93e552011-03-02 22:35:49 +0100788 static_dir = os.path.realpath(archive_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700789 serve_only = True
Chris Sosa0356d3b2010-09-16 15:46:22 -0700790
Don Garrettf90edf02010-11-16 17:36:14 -0800791 cache_dir = os.path.join(static_dir, 'cache')
Chris Sosadbc20082012-12-10 13:39:11 -0800792 # If our devserver is only supposed to serve payloads, we shouldn't be mucking
793 # with the cache at all. If the devserver hadn't previously generated a cache
794 # and is expected, the caller is using it wrong.
795 if serve_only:
796 # Extra check to make sure we're not being called incorrectly.
797 if (options.clear_cache or options.exit or options.pregenerate_update or
798 options.board or options.image):
799 parser.error('Incompatible flags detected for serve_only mode.')
Don Garrettf90edf02010-11-16 17:36:14 -0800800
Chris Sosadbc20082012-12-10 13:39:11 -0800801 elif os.path.exists(cache_dir):
802 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 12:12:17 -0800803 else:
804 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800805
Chris Sosadbc20082012-12-10 13:39:11 -0800806 _Log('Using cache directory %s' % cache_dir)
Gilad Arnoldc65330c2012-09-20 15:17:48 -0700807 _Log('Data dir is %s' % options.data_dir)
808 _Log('Source root is %s' % root_dir)
809 _Log('Serving from %s' % static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +0000810
Chris Sosa6a3697f2013-01-29 16:44:43 -0800811 # We allow global use here to share with cherrypy classes.
812 # pylint: disable=W0603
Chris Sosacde6bf42012-05-31 18:36:39 -0700813 global updater
Andrew de los Reyes52620802010-04-12 13:40:07 -0700814 updater = autoupdate.Autoupdate(
815 root_dir=root_dir,
816 static_dir=static_dir,
Chris Sosa0356d3b2010-09-16 15:46:22 -0700817 serve_only=serve_only,
Andrew de los Reyes52620802010-04-12 13:40:07 -0700818 urlbase=options.urlbase,
819 test_image=options.test_image,
Chris Sosa5d342a22010-09-28 16:54:41 -0700820 forced_image=options.image,
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700821 payload_path=options.payload,
Don Garrett0ad09372010-12-06 16:20:30 -0800822 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-28 23:42:37 -0700823 src_image=options.src_image,
Chris Sosae67b78f2010-11-04 17:33:16 -0700824 vm=options.vm,
Chris Sosa08d55a22011-01-19 16:08:02 -0800825 board=options.board,
Chris Sosa0f1ec842011-02-14 16:33:22 -0800826 copy_to_static_root=not options.exit,
827 private_key=options.private_key,
Satoru Takabayashid733cbe2011-11-15 09:36:32 -0800828 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700829 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 10:32:44 -0700830 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 12:52:23 -0700831 host_log=options.host_log,
Chris Sosa0f1ec842011-02-14 16:33:22 -0800832 )
Chris Sosa7c931362010-10-11 19:49:01 -0700833
Chris Sosa6a3697f2013-01-29 16:44:43 -0800834 if options.pregenerate_update:
835 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 15:46:22 -0700836
Don Garrett0c880e22010-11-17 18:13:37 -0800837 # If the command line requested after setup, it's time to do it.
838 if not options.exit:
Chris Sosa66e2d9c2012-07-11 14:14:14 -0700839 # Handle options that must be set globally in cherrypy.
Chris Sosa2f1c41e2012-07-10 14:32:33 -0700840 if options.production:
Chris Sosa66e2d9c2012-07-11 14:14:14 -0700841 cherrypy.config.update({'environment': 'production'})
842 if not options.logfile:
843 cherrypy.config.update({'log.screen': True})
844 else:
845 cherrypy.config.update({'log.error_file': options.logfile,
846 'log.access_file': options.logfile})
Chris Sosa2f1c41e2012-07-10 14:32:33 -0700847
Don Garrett0c880e22010-11-17 18:13:37 -0800848 cherrypy.quickstart(DevServerRoot(), config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -0700849
850
851if __name__ == '__main__':
852 main()