""" Copyright (c) 2007 Jan-Klaas Kollhof This file is part of jsonrpc. jsonrpc is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this software; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ import os import socket import subprocess import urllib import urllib2 from autotest_lib.client.common_lib import error as exceptions from autotest_lib.client.common_lib import global_config from json import decoder from json import encoder as json_encoder json_encoder_class = json_encoder.JSONEncoder # Try to upgrade to the Django JSON encoder. It uses the standard json encoder # but can handle DateTime try: # See http://crbug.com/418022 too see why the try except is needed here. from django import conf as django_conf # The serializers can't be imported if django isn't configured. # Using try except here doesn't work, as test_that initializes it's own # django environment (setup_django_lite_environment) which raises import # errors if the django dbutils have been previously imported, as importing # them leaves some state behind. # This the variable name must not be undefined or empty string. if os.environ.get(django_conf.ENVIRONMENT_VARIABLE, None): from django.core.serializers import json as django_encoder json_encoder_class = django_encoder.DjangoJSONEncoder except ImportError: pass class JSONRPCException(Exception): pass class ValidationError(JSONRPCException): """Raised when the RPC is malformed.""" def __init__(self, error, formatted_message): """Constructor. @param error: a dict of error info like so: {error['name']: 'ErrorKind', error['message']: 'Pithy error description.', error['traceback']: 'Multi-line stack trace'} @formatted_message: string representation of this exception. """ self.problem_keys = eval(error['message']) self.traceback = error['traceback'] super(ValidationError, self).__init__(formatted_message) def BuildException(error): """Exception factory. Given a dict of error info, determine which subclass of JSONRPCException to build and return. If can't determine the right one, just return a JSONRPCException with a pretty-printed error string. @param error: a dict of error info like so: {error['name']: 'ErrorKind', error['message']: 'Pithy error description.', error['traceback']: 'Multi-line stack trace'} """ error_message = '%(name)s: %(message)s\n%(traceback)s' % error for cls in JSONRPCException.__subclasses__(): if error['name'] == cls.__name__: return cls(error, error_message) for cls in (exceptions.CrosDynamicSuiteException.__subclasses__() + exceptions.RPCException.__subclasses__()): if error['name'] == cls.__name__: return cls(error_message) return JSONRPCException(error_message) class ServiceProxy(object): def __init__(self, serviceURL, serviceName=None, headers=None): """ @param serviceURL: The URL for the service we're proxying. @param serviceName: Name of the REST endpoint to hit. @param headers: Extra HTTP headers to include. """ self.__serviceURL = serviceURL self.__serviceName = serviceName self.__headers = headers or {} # TODO(pprabhu) We are reading this config value deep in the stack # because we don't want to update all tools with a new command line # argument. Once this has been proven to work, flip the switch -- use # sso by default, and turn it off internally in the lab via # shadow_config. self.__use_sso_client = global_config.global_config.get_config_value( 'CLIENT', 'use_sso_client', type=bool, default=False) def __getattr__(self, name): if self.__serviceName is not None: name = "%s.%s" % (self.__serviceName, name) return ServiceProxy(self.__serviceURL, name, self.__headers) def __call__(self, *args, **kwargs): # Caller can pass in a minimum value of timeout to be used for urlopen # call. Otherwise, the default socket timeout will be used. min_rpc_timeout = kwargs.pop('min_rpc_timeout', None) postdata = json_encoder_class().encode({'method': self.__serviceName, 'params': args + (kwargs,), 'id': 'jsonrpc'}) url_with_args = self.__serviceURL + '?' + urllib.urlencode({ 'method': self.__serviceName}) if self.__use_sso_client: respdata = _sso_request(url_with_args, self.__headers, postdata, min_rpc_timeout) else: respdata = _raw_http_request(url_with_args, self.__headers, postdata, min_rpc_timeout) try: resp = decoder.JSONDecoder().decode(respdata) except ValueError: raise JSONRPCException('Error decoding JSON reponse:\n' + respdata) if resp['error'] is not None: raise BuildException(resp['error']) else: return resp['result'] def _raw_http_request(url_with_args, headers, postdata, timeout): """Make a raw HTPP request. @param url_with_args: url with the GET params formatted. @headers: Any extra headers to include in the request. @postdata: data for a POST request instead of a GET. @timeout: timeout to use (in seconds). @returns: the response from the http request. """ request = urllib2.Request(url_with_args, data=postdata, headers=headers) default_timeout = socket.getdefaulttimeout() if not default_timeout: # If default timeout is None, socket will never time out. return urllib2.urlopen(request).read() else: return urllib2.urlopen( request, timeout=max(timeout, default_timeout), ).read() def _sso_request(url_with_args, headers, postdata, timeout): """Make an HTTP request via sso_client. @param url_with_args: url with the GET params formatted. @headers: Any extra headers to include in the request. @postdata: data for a POST request instead of a GET. @timeout: timeout to use (in seconds). @returns: the response from the http request. """ headers_str = '; '.join(['%s: %s' % (k, v) for k, v in headers.iteritems()]) cmd = [ 'sso_client', '-url', url_with_args, ] if headers_str: cmd += [ '-header_sep', '";"', '-headers', headers_str, ] if postdata: cmd += [ '-method', 'POST', '-data', postdata, ] if timeout: cmd += ['-request_timeout', str(timeout)] else: # sso_client has a default timeout of 5 seconds. To mimick the raw # behaviour of never timing out, we force a large timeout. cmd += ['-request_timeout', '3600'] try: return subprocess.check_output(cmd, stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: if _sso_creds_error(e.output): raise JSONRPCException('RPC blocked by uberproxy. Have your run ' '`prodaccess`') raise JSONRPCException( 'Error (code: %s) retrieving url (%s): %s' % (e.returncode, url_with_args, e.output) ) def _sso_creds_error(output): return 'No user creds available' in output