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