| # Copyright (c) 2012 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import httplib |
| import logging |
| import urllib2 |
| import HTMLParser |
| import cStringIO |
| |
| from autotest_lib.client.common_lib import error, global_config |
| # TODO(cmasone): redo this class using requests module; http://crosbug.com/30107 |
| |
| |
| CONFIG = global_config.global_config |
| |
| |
| class MarkupStripper(HTMLParser.HTMLParser): |
| """HTML parser that strips HTML tags, coded characters like & |
| |
| Works by, basically, not doing anything for any tags, and only recording |
| the content of text nodes in an internal data structure. |
| """ |
| def __init__(self): |
| self.reset() |
| self.fed = [] |
| |
| |
| def handle_data(self, d): |
| """Consume content of text nodes, store it away.""" |
| self.fed.append(d) |
| |
| |
| def get_data(self): |
| """Concatenate and return all stored data.""" |
| return ''.join(self.fed) |
| |
| |
| def _get_image_storage_server(): |
| return CONFIG.get_config_value('CROS', 'image_storage_server', type=str) |
| |
| |
| def _get_dev_server(): |
| return CONFIG.get_config_value('CROS', 'dev_server', type=str) |
| |
| |
| def remote_devserver_call(method): |
| """A decorator to use with remote devserver calls. |
| |
| This decorator converts urllib2.HTTPErrors into DevServerExceptions with |
| any embedded error info converted into plain text. |
| """ |
| def wrapper(*args, **kwargs): |
| """This wrapper actually catches the HTTPError.""" |
| try: |
| return method(*args, **kwargs) |
| except urllib2.HTTPError as e: |
| strip = MarkupStripper() |
| strip.feed(e.read()) |
| raise DevServerException(strip.get_data()) |
| |
| return wrapper |
| |
| |
| class DevServerException(Exception): |
| """Raised when the dev server returns a non-200 HTTP response.""" |
| pass |
| |
| |
| class DevServer(object): |
| """Helper class for interacting with the Dev Server via http.""" |
| def __init__(self, dev_host=None): |
| """Constructor. |
| |
| Args: |
| @param dev_host: Address of the Dev Server. |
| Defaults to None. If not set, CROS.dev_server is used. |
| """ |
| self._dev_server = dev_host if dev_host else _get_dev_server() |
| |
| |
| @staticmethod |
| def create(dev_host=None): |
| """Wraps the constructor. Purely for mocking purposes.""" |
| return DevServer(dev_host) |
| |
| |
| def _build_call(self, method, **kwargs): |
| """Build a URL that calls |method|, passing |kwargs|. |
| |
| Build a URL that calls |method| on the dev server, passing a set |
| of key/value pairs built from the dict |kwargs|. |
| |
| @param method: the dev server method to call. |
| @param kwargs: a dict mapping arg names to arg values |
| @return the URL string |
| """ |
| argstr = '&'.join(map(lambda x: "%s=%s" % x, kwargs.iteritems())) |
| return "%(host)s/%(method)s?%(args)s" % {'host': self._dev_server, |
| 'method': method, |
| 'args': argstr} |
| |
| |
| @remote_devserver_call |
| def trigger_download(self, image, synchronous=True): |
| """Tell the dev server to download and stage |image|. |
| |
| Tells the dev server at |self._dev_server| to fetch |image| |
| from the image storage server named by _get_image_storage_server(). |
| |
| If |synchronous| is True, waits for the entire download to finish |
| staging before returning. Otherwise only the artifacts necessary |
| to start installing images onto DUT's will be staged before returning. |
| A caller can then call finish_download to guarantee the rest of the |
| artifacts have finished staging. |
| |
| @param image: the image to fetch and stage. |
| @param synchronous: if True, waits until all components of the image are |
| staged before returning. |
| @raise DevServerException upon any return code that's not HTTP OK. |
| """ |
| call = self._build_call( |
| 'download', |
| archive_url=_get_image_storage_server() + image) |
| response = urllib2.urlopen(call) |
| was_successful = response.read() == 'Success' |
| if was_successful and synchronous: |
| self.finish_download(image) |
| elif not was_successful: |
| raise DevServerException("trigger_download for %s failed;" |
| "HTTP OK not accompanied by 'Success'." % |
| image) |
| |
| |
| @remote_devserver_call |
| def finish_download(self, image): |
| """Tell the dev server to finish staging |image|. |
| |
| If trigger_download is called with synchronous=False, it will return |
| before all artifacts have been staged. This method contacts the |
| devserver and blocks until all staging is completed and should be |
| called after a call to trigger_download. |
| |
| @param image: the image to fetch and stage. |
| @raise DevServerException upon any return code that's not HTTP OK. |
| """ |
| call = self._build_call( |
| 'wait_for_status', |
| archive_url=_get_image_storage_server() + image) |
| if urllib2.urlopen(call).read() != 'Success': |
| raise DevServerException("finish_download for %s failed;" |
| "HTTP OK not accompanied by 'Success'." % |
| image) |
| |
| |
| @remote_devserver_call |
| def list_control_files(self, build): |
| """Ask the dev server to list all control files for |build|. |
| |
| Ask the dev server at |self._dev_server| to list all control files |
| for |build|. |
| |
| @param build: The build (e.g. x86-mario-release/R18-1586.0.0-a1-b1514) |
| whose control files the caller wants listed. |
| @return None on failure, or a list of control file paths |
| (e.g. server/site_tests/autoupdate/control) |
| @raise DevServerException upon any return code that's not HTTP OK. |
| """ |
| call = self._build_call('controlfiles', build=build) |
| response = urllib2.urlopen(call) |
| return [line.rstrip() for line in response] |
| |
| |
| @remote_devserver_call |
| def get_control_file(self, build, control_path): |
| """Ask the dev server for the contents of a control file. |
| |
| Ask the dev server at |self._dev_server| for the contents of the |
| control file at |control_path| for |build|. |
| |
| @param build: The build (e.g. x86-mario-release/R18-1586.0.0-a1-b1514) |
| whose control files the caller wants listed. |
| @param control_path: The file to list |
| (e.g. server/site_tests/autoupdate/control) |
| @return The contents of the desired file. |
| @raise DevServerException upon any return code that's not HTTP OK. |
| """ |
| call = self._build_call('controlfiles', |
| build=build, control_path=control_path) |
| return urllib2.urlopen(call).read() |
| |
| |
| @remote_devserver_call |
| def symbolicate_dump(self, minidump_path, build): |
| """Ask the dev server to symbolicate the dump at minidump_path. |
| |
| Stage the debug symbols for |build| and, if that works, ask the |
| dev server at |self._dev_server| to symbolicate the dump at |
| minidump_path. |
| |
| @param minidump_path: the on-disk path of the minidump. |
| @param build: The build (e.g. x86-mario-release/R18-1586.0.0-a1-b1514) |
| whose debug symbols are needed for symbolication. |
| @return The contents of the stack trace |
| @raise DevServerException upon any return code that's not HTTP OK. |
| """ |
| try: |
| import requests |
| except ImportError: |
| logging.warning("Can't 'import requests' to connect to dev server.") |
| return '' |
| # Stage debug symbols. |
| call = self._build_call( |
| 'stage_debug', |
| archive_url=_get_image_storage_server() + build) |
| request = requests.get(call) |
| if (request.status_code != requests.codes.ok or |
| request.text != 'Success'): |
| error_fd = cStringIO.StringIO(request.text) |
| raise urllib2.HTTPError(call, |
| request.status_code, |
| request.text, |
| request.headers, |
| error_fd) |
| # Symbolicate minidump. |
| call = self._build_call('symbolicate_dump') |
| request = requests.post(call, |
| files={'minidump': open(minidump_path, 'rb')}) |
| if request.status_code == requests.codes.OK: |
| return request.text |
| error_fd = cStringIO.StringIO(request.text) |
| raise urllib2.HTTPError(call, |
| request.status_code, |
| request.text, |
| request.headers, |
| error_fd) |
| |
| |
| @remote_devserver_call |
| def get_latest_build(self, target, milestone=''): |
| """Ask the dev server for the latest build for a given target. |
| |
| Ask the dev server at |self._dev_server|for the latest build for |
| |target|. |
| |
| @param target: The build target, typically a combination of the board |
| and the type of build e.g. x86-mario-release. |
| @param milestone: For latest build set to '', for builds only in a |
| specific milestone set to a str of format Rxx |
| (e.g. R16). Default: ''. Since we are dealing with a |
| webserver sending an empty string, '', ensures that |
| the variable in the URL is ignored as if it was set |
| to None. |
| @return A string of the returned build e.g. R20-2226.0.0. |
| @raise DevServerException upon any return code that's not HTTP OK. |
| """ |
| call = self._build_call('latestbuild', target=target, |
| milestone=milestone) |
| return urllib2.urlopen(call).read() |