1"""\ 2RPC request handler Django. Exposed RPC interface functions should be 3defined in rpc_interface.py. 4""" 5 6__author__ = 'showard@google.com (Steve Howard)' 7 8import inspect 9import pydoc 10import re 11import traceback 12import urllib 13 14from autotest_lib.client.common_lib import error 15from autotest_lib.frontend.afe import models, rpc_utils 16from autotest_lib.frontend.afe import rpcserver_logging 17from autotest_lib.frontend.afe.json_rpc import serviceHandler 18 19LOGGING_REGEXPS = [r'.*add_.*', 20 r'delete_.*', 21 r'.*remove_.*', 22 r'modify_.*', 23 r'create.*', 24 r'set_.*'] 25FULL_REGEXP = '(' + '|'.join(LOGGING_REGEXPS) + ')' 26COMPILED_REGEXP = re.compile(FULL_REGEXP) 27 28SHARD_RPC_INTERFACE = 'shard_rpc_interface' 29COMMON_RPC_INTERFACE = 'common_rpc_interface' 30 31def should_log_message(name): 32 """Detect whether to log message. 33 34 @param name: the method name. 35 """ 36 return COMPILED_REGEXP.match(name) 37 38 39class RpcMethodHolder(object): 40 'Dummy class to hold RPC interface methods as attributes.' 41 42 43class RpcValidator(object): 44 """Validate Rpcs handled by RpcHandler. 45 46 This validator is introduced to filter RPC's callers. If a caller is not 47 allowed to call a given RPC, it will be refused by the validator. 48 """ 49 def __init__(self, rpc_interface_modules): 50 self._shard_rpc_methods = [] 51 self._common_rpc_methods = [] 52 53 for module in rpc_interface_modules: 54 if COMMON_RPC_INTERFACE in module.__name__: 55 self._common_rpc_methods = self._grab_name_from(module) 56 57 if SHARD_RPC_INTERFACE in module.__name__: 58 self._shard_rpc_methods = self._grab_name_from(module) 59 60 61 def _grab_name_from(self, module): 62 """Grab function name from module and add them to rpc_methods. 63 64 @param module: an actual module. 65 """ 66 rpc_methods = [] 67 for name in dir(module): 68 if name.startswith('_'): 69 continue 70 attribute = getattr(module, name) 71 if not inspect.isfunction(attribute): 72 continue 73 rpc_methods.append(attribute.func_name) 74 75 return rpc_methods 76 77 78 def validate_rpc_only_called_by_master(self, meth_name, remote_ip): 79 """Validate whether the method name can be called by remote_ip. 80 81 This funcion checks whether the given method (meth_name) belongs to 82 _shard_rpc_module. 83 84 If True, it then checks whether the caller's IP (remote_ip) is autotest 85 master. An RPCException will be raised if an RPC method from 86 _shard_rpc_module is called by a caller that is not autotest master. 87 88 @param meth_name: the RPC method name which is called. 89 @param remote_ip: the caller's IP. 90 """ 91 if meth_name in self._shard_rpc_methods: 92 global_afe_ip = rpc_utils.get_ip(rpc_utils.GLOBAL_AFE_HOSTNAME) 93 if remote_ip != global_afe_ip: 94 raise error.RPCException( 95 'Shard RPC %r cannot be called by remote_ip %s. It ' 96 'can only be called by global_afe: %s' % ( 97 meth_name, remote_ip, global_afe_ip)) 98 99 100 def encode_validate_result(self, meth_id, err): 101 """Encode the return results for validator. 102 103 It is used for encoding return response for RPC handler if caller of an 104 RPC is refused by validator. 105 106 @param meth_id: the id of the request for an RPC method. 107 @param err: The error raised by validator. 108 109 @return: a raw http response including the encoded error result. It 110 will be parsed by service proxy. 111 """ 112 error_result = serviceHandler.ServiceHandler.blank_result_dict() 113 error_result['id'] = meth_id 114 error_result['err'] = err 115 error_result['err_traceback'] = traceback.format_exc() 116 result = self.encode_result(error_result) 117 return rpc_utils.raw_http_response(result) 118 119 120class RpcHandler(object): 121 """The class to handle Rpc requests.""" 122 123 def __init__(self, rpc_interface_modules, document_module=None): 124 """Initialize an RpcHandler instance. 125 126 @param rpc_interface_modules: the included rpc interface modules. 127 @param document_module: the module includes documentation. 128 """ 129 self._rpc_methods = RpcMethodHolder() 130 self._dispatcher = serviceHandler.ServiceHandler(self._rpc_methods) 131 self._rpc_validator = RpcValidator(rpc_interface_modules) 132 133 # store all methods from interface modules 134 for module in rpc_interface_modules: 135 self._grab_methods_from(module) 136 137 # get documentation for rpc_interface we can send back to the 138 # user 139 if document_module is None: 140 document_module = rpc_interface_modules[0] 141 self.html_doc = pydoc.html.document(document_module) 142 143 144 def get_rpc_documentation(self): 145 """Get raw response from an http documentation.""" 146 return rpc_utils.raw_http_response(self.html_doc) 147 148 149 def raw_request_data(self, request): 150 """Return raw data in request. 151 152 @param request: the request to get raw data from. 153 """ 154 if request.method == 'POST': 155 return request.body 156 return urllib.unquote(request.META['QUERY_STRING']) 157 158 159 def execute_request(self, json_request): 160 """Execute a json request. 161 162 @param json_request: the json request to be executed. 163 """ 164 return self._dispatcher.handleRequest(json_request) 165 166 167 def decode_request(self, json_request): 168 """Decode the json request. 169 170 @param json_request: the json request to be decoded. 171 """ 172 return self._dispatcher.translateRequest(json_request) 173 174 175 def dispatch_request(self, decoded_request): 176 """Invoke a RPC call from a decoded request. 177 178 @param decoded_request: the json request to be processed and run. 179 """ 180 return self._dispatcher.dispatchRequest(decoded_request) 181 182 183 def log_request(self, user, decoded_request, decoded_result, 184 remote_ip, log_all=False): 185 """Log request if required. 186 187 @param user: current user. 188 @param decoded_request: the decoded request. 189 @param decoded_result: the decoded result. 190 @param remote_ip: the caller's ip. 191 @param log_all: whether to log all messages. 192 """ 193 if log_all or should_log_message(decoded_request['method']): 194 msg = '%s| %s:%s %s' % (remote_ip, decoded_request['method'], 195 user, decoded_request['params']) 196 if decoded_result['err']: 197 msg += '\n' + decoded_result['err_traceback'] 198 rpcserver_logging.rpc_logger.error(msg) 199 else: 200 rpcserver_logging.rpc_logger.info(msg) 201 202 203 def encode_result(self, results): 204 """Encode the result to translated json result. 205 206 @param results: the results to be encoded. 207 """ 208 return self._dispatcher.translateResult(results) 209 210 211 def handle_rpc_request(self, request): 212 """Handle common rpc request and return raw response. 213 214 @param request: the rpc request to be processed. 215 """ 216 remote_ip = self._get_remote_ip(request) 217 user = models.User.current_user() 218 json_request = self.raw_request_data(request) 219 decoded_request = self.decode_request(json_request) 220 221 # Validate whether method can be called by the remote_ip 222 try: 223 meth_id = decoded_request['id'] 224 meth_name = decoded_request['method'] 225 self._rpc_validator.validate_rpc_only_called_by_master( 226 meth_name, remote_ip) 227 except KeyError: 228 raise serviceHandler.BadServiceRequest(decoded_request) 229 except error.RPCException as e: 230 return self._rpc_validator.encode_validate_result(meth_id, e) 231 232 decoded_request['remote_ip'] = remote_ip 233 decoded_result = self.dispatch_request(decoded_request) 234 result = self.encode_result(decoded_result) 235 if rpcserver_logging.LOGGING_ENABLED: 236 self.log_request(user, decoded_request, decoded_result, 237 remote_ip) 238 return rpc_utils.raw_http_response(result) 239 240 241 def handle_jsonp_rpc_request(self, request): 242 """Handle the json rpc request and return raw response. 243 244 @param request: the rpc request to be handled. 245 """ 246 request_data = request.GET['request'] 247 callback_name = request.GET['callback'] 248 # callback_name must be a simple identifier 249 assert re.search(r'^\w+$', callback_name) 250 251 result = self.execute_request(request_data) 252 padded_result = '%s(%s)' % (callback_name, result) 253 return rpc_utils.raw_http_response(padded_result, 254 content_type='text/javascript') 255 256 257 @staticmethod 258 def _allow_keyword_args(f): 259 """\ 260 Decorator to allow a function to take keyword args even though 261 the RPC layer doesn't support that. The decorated function 262 assumes its last argument is a dictionary of keyword args and 263 passes them to the original function as keyword args. 264 """ 265 def new_fn(*args): 266 """Make the last argument as the keyword args.""" 267 assert args 268 keyword_args = args[-1] 269 args = args[:-1] 270 return f(*args, **keyword_args) 271 new_fn.func_name = f.func_name 272 return new_fn 273 274 275 def _grab_methods_from(self, module): 276 for name in dir(module): 277 if name.startswith('_'): 278 continue 279 attribute = getattr(module, name) 280 if not inspect.isfunction(attribute): 281 continue 282 decorated_function = RpcHandler._allow_keyword_args(attribute) 283 setattr(self._rpc_methods, name, decorated_function) 284 285 286 def _get_remote_ip(self, request): 287 """Get the ip address of a RPC caller. 288 289 Returns the IP of the request, accounting for the possibility of 290 being behind a proxy. 291 If a Django server is behind a proxy, request.META["REMOTE_ADDR"] will 292 return the proxy server's IP, not the client's IP. 293 The proxy server would provide the client's IP in the 294 HTTP_X_FORWARDED_FOR header. 295 296 @param request: django.core.handlers.wsgi.WSGIRequest object. 297 298 @return: IP address of remote host as a string. 299 Empty string if the IP cannot be found. 300 """ 301 remote = request.META.get('HTTP_X_FORWARDED_FOR', None) 302 if remote: 303 # X_FORWARDED_FOR returns client1, proxy1, proxy2,... 304 remote = remote.split(',')[0].strip() 305 else: 306 remote = request.META.get('REMOTE_ADDR', '') 307 return remote 308