Home | History | Annotate | Download | only in json_rpc
      1 
      2 """
      3   Copyright (c) 2007 Jan-Klaas Kollhof
      4 
      5   This file is part of jsonrpc.
      6 
      7   jsonrpc is free software; you can redistribute it and/or modify
      8   it under the terms of the GNU Lesser General Public License as published by
      9   the Free Software Foundation; either version 2.1 of the License, or
     10   (at your option) any later version.
     11 
     12   This software is distributed in the hope that it will be useful,
     13   but WITHOUT ANY WARRANTY; without even the implied warranty of
     14   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     15   GNU Lesser General Public License for more details.
     16 
     17   You should have received a copy of the GNU Lesser General Public License
     18   along with this software; if not, write to the Free Software
     19   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
     20 """
     21 
     22 import os
     23 import socket
     24 import subprocess
     25 import urllib
     26 import urllib2
     27 from autotest_lib.client.common_lib import error as exceptions
     28 from autotest_lib.client.common_lib import global_config
     29 
     30 from json import decoder
     31 
     32 from json import encoder as json_encoder
     33 json_encoder_class = json_encoder.JSONEncoder
     34 
     35 
     36 # Try to upgrade to the Django JSON encoder. It uses the standard json encoder
     37 # but can handle DateTime
     38 try:
     39     # See http://crbug.com/418022 too see why the try except is needed here.
     40     from django import conf as django_conf
     41     # The serializers can't be imported if django isn't configured.
     42     # Using try except here doesn't work, as test_that initializes it's own
     43     # django environment (setup_django_lite_environment) which raises import
     44     # errors if the django dbutils have been previously imported, as importing
     45     # them leaves some state behind.
     46     # This the variable name must not be undefined or empty string.
     47     if os.environ.get(django_conf.ENVIRONMENT_VARIABLE, None):
     48         from django.core.serializers import json as django_encoder
     49         json_encoder_class = django_encoder.DjangoJSONEncoder
     50 except ImportError:
     51     pass
     52 
     53 
     54 class JSONRPCException(Exception):
     55     pass
     56 
     57 
     58 class ValidationError(JSONRPCException):
     59     """Raised when the RPC is malformed."""
     60     def __init__(self, error, formatted_message):
     61         """Constructor.
     62 
     63         @param error: a dict of error info like so:
     64                       {error['name']: 'ErrorKind',
     65                        error['message']: 'Pithy error description.',
     66                        error['traceback']: 'Multi-line stack trace'}
     67         @formatted_message: string representation of this exception.
     68         """
     69         self.problem_keys = eval(error['message'])
     70         self.traceback = error['traceback']
     71         super(ValidationError, self).__init__(formatted_message)
     72 
     73 
     74 def BuildException(error):
     75     """Exception factory.
     76 
     77     Given a dict of error info, determine which subclass of
     78     JSONRPCException to build and return.  If can't determine the right one,
     79     just return a JSONRPCException with a pretty-printed error string.
     80 
     81     @param error: a dict of error info like so:
     82                   {error['name']: 'ErrorKind',
     83                    error['message']: 'Pithy error description.',
     84                    error['traceback']: 'Multi-line stack trace'}
     85     """
     86     error_message = '%(name)s: %(message)s\n%(traceback)s' % error
     87     for cls in JSONRPCException.__subclasses__():
     88         if error['name'] == cls.__name__:
     89             return cls(error, error_message)
     90     for cls in (exceptions.CrosDynamicSuiteException.__subclasses__() +
     91                 exceptions.RPCException.__subclasses__()):
     92         if error['name'] == cls.__name__:
     93             return cls(error_message)
     94     return JSONRPCException(error_message)
     95 
     96 
     97 class ServiceProxy(object):
     98     def __init__(self, serviceURL, serviceName=None, headers=None):
     99         """
    100         @param serviceURL: The URL for the service we're proxying.
    101         @param serviceName: Name of the REST endpoint to hit.
    102         @param headers: Extra HTTP headers to include.
    103         """
    104         self.__serviceURL = serviceURL
    105         self.__serviceName = serviceName
    106         self.__headers = headers or {}
    107 
    108         # TODO(pprabhu) We are reading this config value deep in the stack
    109         # because we don't want to update all tools with a new command line
    110         # argument. Once this has been proven to work, flip the switch -- use
    111         # sso by default, and turn it off internally in the lab via
    112         # shadow_config.
    113         self.__use_sso_client = global_config.global_config.get_config_value(
    114             'CLIENT', 'use_sso_client', type=bool, default=False)
    115 
    116 
    117     def __getattr__(self, name):
    118         if self.__serviceName is not None:
    119             name = "%s.%s" % (self.__serviceName, name)
    120         return ServiceProxy(self.__serviceURL, name, self.__headers)
    121 
    122     def __call__(self, *args, **kwargs):
    123         # Caller can pass in a minimum value of timeout to be used for urlopen
    124         # call. Otherwise, the default socket timeout will be used.
    125         min_rpc_timeout = kwargs.pop('min_rpc_timeout', None)
    126         postdata = json_encoder_class().encode({'method': self.__serviceName,
    127                                                 'params': args + (kwargs,),
    128                                                 'id': 'jsonrpc'})
    129         url_with_args = self.__serviceURL + '?' + urllib.urlencode({
    130             'method': self.__serviceName})
    131         if self.__use_sso_client:
    132             respdata = _sso_request(url_with_args, self.__headers, postdata,
    133                                     min_rpc_timeout)
    134         else:
    135             respdata = _raw_http_request(url_with_args, self.__headers,
    136                                          postdata, min_rpc_timeout)
    137 
    138         try:
    139             resp = decoder.JSONDecoder().decode(respdata)
    140         except ValueError:
    141             raise JSONRPCException('Error decoding JSON reponse:\n' + respdata)
    142         if resp['error'] is not None:
    143             raise BuildException(resp['error'])
    144         else:
    145             return resp['result']
    146 
    147 
    148 def _raw_http_request(url_with_args, headers, postdata, timeout):
    149     """Make a raw HTPP request.
    150 
    151     @param url_with_args: url with the GET params formatted.
    152     @headers: Any extra headers to include in the request.
    153     @postdata: data for a POST request instead of a GET.
    154     @timeout: timeout to use (in seconds).
    155 
    156     @returns: the response from the http request.
    157     """
    158     request = urllib2.Request(url_with_args, data=postdata, headers=headers)
    159     default_timeout = socket.getdefaulttimeout()
    160     if not default_timeout:
    161         # If default timeout is None, socket will never time out.
    162         return urllib2.urlopen(request).read()
    163     else:
    164         return urllib2.urlopen(
    165                 request,
    166                 timeout=max(timeout, default_timeout),
    167         ).read()
    168 
    169 
    170 def _sso_request(url_with_args, headers, postdata, timeout):
    171     """Make an HTTP request via sso_client.
    172 
    173     @param url_with_args: url with the GET params formatted.
    174     @headers: Any extra headers to include in the request.
    175     @postdata: data for a POST request instead of a GET.
    176     @timeout: timeout to use (in seconds).
    177 
    178     @returns: the response from the http request.
    179     """
    180     headers_str = '; '.join(['%s: %s' % (k, v) for k, v in headers.iteritems()])
    181     cmd = [
    182         'sso_client',
    183         '-url', url_with_args,
    184     ]
    185     if headers_str:
    186         cmd += [
    187                 '-header_sep', '";"',
    188                 '-headers', headers_str,
    189         ]
    190     if postdata:
    191         cmd += [
    192                 '-method', 'POST',
    193                 '-data', postdata,
    194         ]
    195     if timeout:
    196         cmd += ['-request_timeout', str(timeout)]
    197     else:
    198         # sso_client has a default timeout of 5 seconds. To mimick the raw
    199         # behaviour of never timing out, we force a large timeout.
    200         cmd += ['-request_timeout', '3600']
    201 
    202     try:
    203         return subprocess.check_output(cmd, stderr=subprocess.STDOUT)
    204     except subprocess.CalledProcessError as e:
    205         if _sso_creds_error(e.output):
    206             raise JSONRPCException('RPC blocked by uberproxy. Have your run '
    207                                    '`prodaccess`')
    208 
    209         raise JSONRPCException(
    210                 'Error (code: %s) retrieving url (%s): %s' %
    211                 (e.returncode, url_with_args, e.output)
    212         )
    213 
    214 
    215 def _sso_creds_error(output):
    216     return 'No user creds available' in output
    217