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