普通文本  |  249行  |  7.91 KB

# 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)