# Copyright (c) 2013 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 base64 import hashlib import httplib import json import logging import socket import StringIO import urllib2 import urlparse try: import pycurl except ImportError: pycurl = None import common from autotest_lib.client.bin import utils from autotest_lib.client.common_lib import error from autotest_lib.client.common_lib.cros import retry from autotest_lib.server import frontend from autotest_lib.server import site_utils # Give all our rpcs about six seconds of retry time. If a longer timeout # is desired one should retry from the caller, this timeout is only meant # to avoid uncontrolled circumstances like network flake, not, say, retry # right across a reboot. BASE_REQUEST_TIMEOUT = 0.1 JSON_HEADERS = {'Content-Type': 'application/json'} RPC_EXCEPTIONS = (httplib.BadStatusLine, socket.error, urllib2.HTTPError) MANIFEST_KEY = ('MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC+hlN5FB+tjCsBszmBIvI' 'cD/djLLQm2zZfFygP4U4/o++ZM91EWtgII10LisoS47qT2TIOg4Un4+G57e' 'lZ9PjEIhcJfANqkYrD3t9dpEzMNr936TLB2u683B5qmbB68Nq1Eel7KVc+F' '0BqhBondDqhvDvGPEV0vBsbErJFlNH7SQIDAQAB') SONIC_BOARD_LABEL = 'board:sonic' def get_extension_id(pub_key_pem=MANIFEST_KEY): """Computes the extension id from the public key. @param pub_key_pem: The public key used in the extension. @return: The extension id. """ pub_key_der = base64.b64decode(pub_key_pem) sha = hashlib.sha256(pub_key_der).hexdigest() prefix = sha[:32] reencoded = "" ord_a = ord('a') for old_char in prefix: code = int(old_char, 16) new_char = chr(ord_a + code) reencoded += new_char return reencoded class Url(object): """Container for URL information.""" def __init__(self): self.scheme = 'http' self.netloc = '' self.path = '' self.params = '' self.query = '' self.fragment = '' def Build(self): """Returns the URL.""" return urlparse.urlunparse(( self.scheme, self.netloc, self.path, self.params, self.query, self.fragment)) # TODO(beeps): Move get and post to curl too, since we have the need for # custom requests anyway. @retry.retry(RPC_EXCEPTIONS, timeout_min=BASE_REQUEST_TIMEOUT) def _curl_request(host, app_path, port, custom_request='', payload=None): """Sends a custom request throug pycurl, to the url specified. """ url = Url() url.netloc = ':'.join((host, str(port))) url.path = app_path full_url = url.Build() response = StringIO.StringIO() conn = pycurl.Curl() conn.setopt(conn.URL, full_url) conn.setopt(conn.WRITEFUNCTION, response.write) if custom_request: conn.setopt(conn.CUSTOMREQUEST, custom_request) if payload: conn.setopt(conn.POSTFIELDS, payload) conn.perform() conn.close() return response.getvalue() @retry.retry(RPC_EXCEPTIONS, timeout_min=BASE_REQUEST_TIMEOUT) def _get(url): """Get request to the give url. @raises: Any of the retry exceptions, if we hit the timeout. @raises: error.TimeoutException, if the call itself times out. eg: a hanging urlopen will get killed with a TimeoutException while multiple retries that hit different Http errors will raise the last HttpError instead of the TimeoutException. """ return urllib2.urlopen(url).read() @retry.retry(RPC_EXCEPTIONS, timeout_min=BASE_REQUEST_TIMEOUT) def _post(url, data): """Post data to the given url. @param data: Json data to post. @raises: Any of the retry exceptions, if we hit the timeout. @raises: error.TimeoutException, if the call itself times out. For examples see docstring for _get method. """ request = urllib2.Request(url, json.dumps(data), headers=JSON_HEADERS) urllib2.urlopen(request) @retry.retry(RPC_EXCEPTIONS + (error.TestError,), timeout_min=30) def acquire_sonic(lock_manager, additional_labels=None): """Lock a host that has the sonic host labels. @param lock_manager: A manager for locking/unlocking hosts, as defined by server.cros.host_lock_manager. @param additional_labels: A list of additional labels to apply in the search for a sonic device. @return: A string specifying the hostname of a locked sonic host. @raises ValueError: Is no hosts matching the given labels are found. """ sonic_host = None afe = frontend.AFE(debug=True) labels = [SONIC_BOARD_LABEL] if additional_labels: labels += additional_labels sonic_hostname = utils.poll_for_condition( lambda: site_utils.lock_host_with_labels(afe, lock_manager, labels), sleep_interval=60, exception=SonicProxyException('Timed out trying to find a sonic ' 'host with labels %s.' % labels)) logging.info('Acquired sonic host returned %s', sonic_hostname) return sonic_hostname class SonicProxyException(Exception): """Generic exception raised when a sonic rpc fails.""" pass class SonicProxy(object): """Client capable of making calls to the sonic device server.""" POLLING_INTERVAL = 5 SONIC_SERVER_PORT = '8008' def __init__(self, hostname): """ @param hostname: The name of the host for this sonic proxy. """ self._sonic_server = 'http://%s:%s' % (hostname, self.SONIC_SERVER_PORT) self._hostname = hostname def check_server(self): """Checks if the sonic server is up and running. @raises: SonicProxyException if the server is unreachable. """ try: json.loads(_get(self._sonic_server)) except (RPC_EXCEPTIONS, error.TimeoutException) as e: raise SonicProxyException('Could not retrieve information about ' 'sonic device: %s' % e) def reboot(self, when="now"): """ Post to the server asking for a reboot. @param when: The time till reboot. Can be any of: now: immediately fdr: set factory data reset flag and reboot now ota: set recovery flag and reboot now ota fdr: set both recovery and fdr flags, and reboot now ota foreground: reboot and start force update page idle: reboot only when idle screen usage > 10 mins @raises SonicProxyException: if we're unable to post a reboot request. """ reboot_url = '%s/%s/%s' % (self._sonic_server, 'setup', 'reboot') reboot_params = {"params": when} logging.info('Rebooting device through %s.', reboot_url) try: _post(reboot_url, reboot_params) except (RPC_EXCEPTIONS, error.TimeoutException) as e: raise SonicProxyException('Could not reboot sonic device through ' '%s: %s' % (self.SETUP_SERVER_PORT, e)) def stop_app(self, app): """Stops the app. Performs a hard reboot if pycurl isn't available. @param app: An app name, eg YouTube, Fling, Netflix etc. @raises pycurl.error: If the DELETE request fails after retries. """ if not pycurl: logging.warning('Rebooting sonic host to stop %s, please install ' 'pycurl if you do not wish to reboot.', app) self.reboot() return _curl_request(self._hostname, 'apps/%s' % app, self.SONIC_SERVER_PORT, 'DELETE') def start_app(self, app, payload): """Starts an app. @param app: An app name, eg YouTube, Fling, Netflix etc. @param payload: An url payload for the app, eg: http://www.youtube.com. @raises error.TimeoutException: If the call times out. """ url = '%s/apps/%s' % (self._sonic_server, app) _post(url, payload)