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 22import os 23import socket 24import subprocess 25import urllib 26import urllib2 27from autotest_lib.client.common_lib import error as exceptions 28from autotest_lib.client.common_lib import global_config 29 30from json import decoder 31 32from json import encoder as json_encoder 33json_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 38try: 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 50except ImportError: 51 pass 52 53 54class JSONRPCException(Exception): 55 pass 56 57 58class 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 74def 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 97class 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 148def _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 170def _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 215def _sso_creds_error(output): 216 return 'No user creds available' in output 217