blob: 23091426ff6720f93719458e04111919c244d60d [file] [log] [blame]
# Copyright 2014 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.
"""UmpireServerProxy handles connection to UmpireServer."""
import json
import logging
import mimetypes
import sys
import traceback
import urllib2
import xmlrpclib
import factory_common # pylint: disable=unused-import
from cros.factory.umpire.client import umpire_client
from cros.factory.umpire import common
from cros.factory.utils import net_utils
class UmpireServerError(object):
"""Class to hold error code and message."""
def __init__(self, code, message):
self.code = code
self.message = message
def CheckProtocolError(protocol_error, umpire_server_error):
"""Checks if an xmlrpclib.ProtocolError equals to an UmpireServerError.
Args:
protocol_error: An xmlrpclib.ProtocolError.
umpire_server_error: An UmpireServerError.
Returns:
True if error code and error message are the same.
"""
return (protocol_error.errcode == umpire_server_error.code and
protocol_error.errmsg == umpire_server_error.message)
class UmpireServerProxyException(Exception):
"""Exception of UmpireServerProxy."""
pass
class UmpireServerProxy(xmlrpclib.ServerProxy):
"""Class to maintain proxy to XMLRPC handler served on the server.
UmpireServerProxy subclasses xmlrpclib.ServerProxy. If server is not an
Umpire server, then UmpireServerProxy acts as an xmlrpclib.ServerProxy.
If server is an Umpire server, then UmpireServerProxy will handle
extra tasks for Umpire server.
Whether a server is an Umpire server can be found by "Ping" it. If the return
value is a dict with 'version=UMPIRE_DUT_RPC_VERSION', it is an Umpire server;
otherwise, it is a simple XMPRPC server instance.
At least four services running on an Umpire server the class needs to work
with:
1. Base Umpire XMLRPC handler to serve basic methods like Ping. Client can
identify the version of server through this call.
2. HTTP server for resources like cros_payload data.
3. Umpire XMLRPC handler to serve methods that are not specific to bundle,
e.g. NeedUpdate. The supported list of methods is queried by introspection.
In operation mode, the four services are served on the same port. The request
is routed by Umpire server. For example:
1. Base Umpire XMLRPC handler: http://10.3.0.1:8080
2. HTTP server: http://10.3.0.1:8080
3. Umpire XMLRPC handler: http://10.3.0.1:8080/umpire
In test mode, the four services are served on the same host, but using
different ports. Check docstrings of _SetUmpireUri for details.
The base class connects to Base Umpire XMLRPC handler at init time. This is
to determine the server version. After that, base class will connect to
Umpire XMLRPC handler. If server version can not be determined at init time,
It should be determined when user calls methods (through _Request method
implicitly).
This class maintains an object which implements UmpireClientInfoInterface.
If client info is updated, it will fetch resource map and update the
properties accordingly.
This class dispatches method calls to Umpire XMLRPC handler.
Properties:
_server_uri: A string containing Umpire server URI (including port).
This is also the URI to request resource map and other resources.
This server can be an instance of legacy simple XMLRPC server for backward
compatibility.
_umpire_http_server_uri = The URI of HTTP server on Umpire server. In
operation mode, it is the same as Umpire server URI. In test mode, it is
at the next port of Umpire server URI.
_umpire_handler_uri: A string containing the Umpire handler URI.
Check _test_mode for how this property is determined.
_use_umpire: True if the object should work with an Umpire server; False if
object should work with simple XMLRPC handler.
_umpire_client_info: An object which implements UmpireClientInfoInterface.
_resources: A dict containing parsed results in resource map.
_umpire_methods: A set of method names that Umpire XMLRPC handler
supports. It is queried from ServerProxy.system.listMethods.
_test_mode: True for testing. The difference is in _SetUmpireUri; see its
docstring for details. In test mode, Umpire HTTP server and Umpire XMLRPC
handler use different ports from Umpire server, while in operation mode,
they are at different paths, which complicates unittest.
"""
def __init__(self, server_uri, test_mode=False, umpire_client_info=None,
quiet=False, *args, **kwargs):
"""Initializes an UmpireServerProxy.
Args:
server_uri: A string containing Umpire server URI or legacy XMLRPC server
URI. By Ping method, UmpireServerProxy can determine if it is working
with a legacy simple XMLRPC server or an Umpire server. Check docstring
of this class for details.
test_mode: True for testing. The difference is in _SetUmpireUri.
umpire_client_info: An object which implements UmpireClientInfoInterface.
This is useful when user wants to use implementation other than
UmpireClientInfo, e.g. when UmpireServerProxy is used in chroot.
quiet: Suppresses error messages when server can not be reached.
Other args are for base class.
"""
self._server_uri = server_uri.rstrip('/')
self._umpire_http_server_uri = None
self._use_umpire = None
self._umpire_client_info = None
self._resources = {}
self._umpire_handler_uri = None
self._umpire_methods = set()
self._args = args
self._kwargs = kwargs
self._test_mode = test_mode
self._quiet = quiet
if umpire_client_info:
logging.warning('Using injected Umpire client info.')
self._umpire_client_info = umpire_client_info
if self._test_mode:
logging.warning('Using UmpireServerProxy in test mode.')
# Connect to server URI first. If the server is not an Umpire server,
# keep the connection. Otherwise, reconnect it to Umpire handler URI.
logging.debug('Connecting to %r', self._server_uri)
xmlrpclib.ServerProxy.__init__(self, self._server_uri, *args, **kwargs)
self._Init(raise_exception=False)
def _Init(self, raise_exception=True):
"""Checks server version and initializes Umpire server proxy.
Checks if server is an Umpire server. If it is an Umpire server, then
initialize the proxies for Umpire server.
Args:
raise_exception: Raises exception if server version can not be decided.
Raises:
UmpireServerProxyException: If server version can not be decided,
and raise_exception is True.
"""
# Determine if server is an Umpire server or a simple XMLRPC server.
self._use_umpire = self._CheckUsingUmpire()
logging.debug('Using Umpire: %r', self._use_umpire)
if self._use_umpire is None and raise_exception:
raise UmpireServerProxyException('Can not decide using Umpire or not.')
if self._use_umpire:
self._InitForUmpireProxy()
def _InitForUmpireProxy(self):
"""Initializes properties to work with Umpire server.
Initializes Umpire client info as an UmpireClientInfo object if it is
not given from class init argument.
Sets Umpire handler URI depending on test mode.
Initializes the object itself connecting to Umpire XMLRPC handler.
"""
if not self._use_umpire:
raise UmpireServerProxyException(
'Initializes Umpire proxies when not using Umpire.')
if not self._umpire_client_info:
self._umpire_client_info = umpire_client.UmpireClientInfo()
# Sets Umpire Handler URI depending on test mode.
self._SetUmpireUri()
# Initializes the object itself connecting to Umpire XMLRPC handler.
logging.debug('Connecting to Umpire handler at %r',
self._umpire_handler_uri)
xmlrpclib.ServerProxy.__init__(self, self._umpire_handler_uri,
*self._args, **self._kwargs)
# Gets resource map and sets handlers.
self._RequestUmpireForHandler()
@property
def use_umpire(self):
"""Checks if this object is talking to an Umpire server.
Returns:
True if this object is talking to an Umpire server.
False if it talks to a simple XMLRPC server.
None if it cannot decide as it fails to get response for 'Ping'.
Raises:
UmpireServerProxyException: If server version can not be determined.
"""
# Try to contact server to decide using Umpire or not.
if self._use_umpire is None:
self._use_umpire = self._CheckUsingUmpire()
if self._use_umpire is None:
raise UmpireServerProxyException('Can not decide server version')
return self._use_umpire
def __request(self, methodname, params):
"""Wrapper for base class __request method.
Args:
methodname: Name of the method to call that is registered on XMLRPC
handler.
params: A tuple containing the args to methodname call.
Returns:
The return value of the remote procedure call.
"""
logging.debug('Using base class __request method with methodname: %r, '
'params: %r', methodname, params)
return xmlrpclib.ServerProxy._ServerProxy__request(self, methodname, params)
def _CheckUsingUmpire(self):
"""Returns if the server is an Umpire server.
Calls Ping method through XMLRPC server proxy in the object itself assuming
it is connecting to Umpire server URI.
Returns:
True if the server is an Umpire server.
"""
try:
result = self.__request('Ping', ())
except Exception:
# This is pretty common and not necessarily an error because by the time
# when proxy instance is initiated, connection might not be ready.
if not self._quiet:
logging.warning(
'Unable to contact factory server to decide using'
' Umpire protocol or not : %s',
'\n'.join(
traceback.format_exception_only(*sys.exc_info()[:2])).strip())
return None
if isinstance(result, dict) and (
result.get('version') == common.UMPIRE_DUT_RPC_VERSION):
logging.debug('Got Umpire server version %r', result.get('version'))
return True
else:
logging.debug('Got factory server response %r', result)
return False
def _SetUmpireUri(self):
"""Sets Umpire Handler URI.
This call is used when server is an Umpire server.
The URI in operation mode are in docstrings of this class.
The URI in test mode are for example:
Base Umpire XMLRPC handler: http://localhost:49998
(This is Umpire server URI from init argument)
HTTP server: http://localhost:49999
Umpire XMLRPC handler: http://localhost:50000
"""
if not self._use_umpire:
raise UmpireServerProxyException(
'_SetUmpireUri method should only be used when working with Umpire'
' server. ')
if self._test_mode:
umpire_scheme_host, port = urllib2.splitport(self._server_uri)
test_http_server_port = int(port) + 1
test_handler_port = int(port) + 2
self._umpire_http_server_uri = '%s:%d' % (
umpire_scheme_host, test_http_server_port)
self._umpire_handler_uri = '%s:%d' % (
umpire_scheme_host, test_handler_port)
else:
self._umpire_http_server_uri = self._server_uri
self._umpire_handler_uri = '%s/umpire' % self._server_uri
logging.debug('Set Umpire HTTP server URI to %s',
self._umpire_http_server_uri)
logging.debug('Set Umpire handler URI to %s', self._umpire_handler_uri)
def _SendPOSTRequest(self, handler, args):
"""Sends HTTP POST request to Umpire HTTP server.
The URI is same as Umpire HTTP server URI.
Args:
handler: case-sensitive Umpire POST handler name
args: A dict which stores content to be sent, saving field => value pair.
multipart/form-data allows multiple values share the same field name,
so value can be an list of values.
Values of fields named 'file' or starts with 'file-' will be treated
as file path, file content will be read and sent instead.
The order of fields is not preserved.
Returns:
A tuple (http_code, response_json) where response_json is a JSON object
decoded from POST response
"""
uri = '%s/post/%s' % (self._umpire_http_server_uri, handler)
fields = {}
files = {}
for k, v in args.iteritems():
if k == 'file' or k.startswith('file-'):
files[k] = v
else:
fields[k] = v
content_type, body = self._EncodeMultipartFormdata(fields, files)
content_length = len(body)
header = {'content-type': content_type, 'content-length': content_length}
response = urllib2.urlopen(urllib2.Request(uri, body, header))
response_json = json.loads(response.read())
return (response.getcode(), response_json)
def _EncodeMultipartFormdata(self, fields, files):
"""Writes multipart/form-data request body.
Args:
fields: A dict which contains (field_name, field_value).
Multipart/form-data allows multiple values share the same field name,
so field_value can be a list.
files: A dict which contains (field_name, field_value), same as above
except every value will be treated as file path.
It reads each file and writes their contents as value.
"""
BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$'
DELIMITER = '--' + BOUNDARY
EOF = '--' + BOUNDARY + '--'
CRLF = '\r\n'
lines = []
for k, v in fields.iteritems():
if not isinstance(v, list):
v = [v]
for item in v:
lines.append(DELIMITER)
lines.append('Content-Disposition: form-data; name="%s"' % k)
lines.append('')
lines.append(str(item))
for k, v in files.iteritems():
if not isinstance(v, list):
v = [v]
for item in v:
lines.append(DELIMITER)
lines.append('Content-Disposition: form-data; name="%s"; filename="%s"'
% (k, item))
lines.append('Content-Type: %s' % self._GetContentType(item))
lines.append('')
with open(item, 'rb') as f:
lines.append(f.read())
lines.append(EOF)
lines.append('')
body = CRLF.join(lines)
content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
return (content_type, body)
def _GetContentType(self, filename):
"""Guesses file type by filename."""
return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
def _RequestUmpireForHandler(self):
"""Refresh supported methods."""
self._umpire_methods = set(self.__request('system.listMethods', ()))
logging.debug('Umpire server methods: %r', self._umpire_methods)
def _CallHandler(self, methodname, params):
"""Calls XMLRPC handler through server proxy.
The handler is currently always Umpire XMLRPC handler, and may be extended
to support handlers from Umpire services, depending on the methodname. Note
that this method is used only when _use_umpire is True.
Args:
methodname: Name of the method to call that is registered on XMLRPC
handler.
params: A tuple containing the args to methodname call.
Returns:
The return value of the remove procedure call.
"""
logging.debug('_CallHandler with methodname: %r, params: %r', methodname,
params)
if methodname not in self._umpire_methods:
# TODO(hungte) Allow extending by Umpire services.
logging.warn('Unknown method: %s', methodname)
logging.debug(
'Calling method %s with params %r using Umpire server proxy %s',
methodname, params, self._umpire_handler_uri)
result = self.__request(methodname, params)
logging.debug('Get result %r', result)
return result
def _Request(self, methodname, params):
"""Main entry point for this server proxy.
Args:
methodname: Name of the method to call that is registered on XMLRPC
handler.
params: A tuple containing the args to methodname call.
Returns:
The return value from server by invocation of methodname.
"""
logging.debug(
'Using UmpireServerProxy _Request method with methodname: %r,'
' params: %r', methodname, params)
# Using Umpire or not is not decided yet. Tries to decide it and initializes
# proxies if needed. Raises exception if it still can not be decided.
if self._use_umpire is None:
logging.debug('Need to decide using Umpire or not')
self._Init(raise_exception=True)
# Not using Umpire. Uses __request in base class.
if not self._use_umpire:
result = self.__request(methodname, params)
return result
# Checks if there is change in client info.
if self._umpire_client_info.Update():
logging.info('Client info has changed')
self._RequestUmpireForHandler()
return self._CallHandler(methodname, params)
def __getattr__(self, name):
# Same magic dispatcher as that in xmlrpclib.ServerProxybase but using
# self._Request instead of _request in the base class.
# pylint: disable=protected-access
return xmlrpclib._Method(self._Request, name)
class TimeoutUmpireServerProxy(UmpireServerProxy):
"""UmpireServerProxy supporting timeout."""
def __init__(self, server_uri, timeout=10, *args, **kwargs):
"""Initializes UmpireServerProxy with its transport supporting timeout.
Args:
server_uri: server_uri passed to UmpireServerProxy. Checks the docstrings
in UmpireServerProxy.
timeout: Timeout in seconds for a method called through this proxy.
*args: The arguments passed to UmpireServerProxy.
**kwargs: The keyword arguments passed to UmpireServerProxy.
Raises:
socket.error: If timeout is reached before the call is finished.
"""
if timeout:
logging.debug('Using TimeoutUmpireServerProxy with timeout %r seconds',
timeout)
kwargs['transport'] = net_utils.TimeoutXMLRPCTransport(timeout=timeout)
UmpireServerProxy.__init__(self, server_uri, *args, **kwargs)