| """\ |
| RPC request handler Django. Exposed RPC interface functions should be |
| defined in rpc_interface.py. |
| """ |
| |
| __author__ = 'showard@google.com (Steve Howard)' |
| |
| import inspect |
| import pydoc |
| import re |
| import traceback |
| import urllib |
| |
| from autotest_lib.client.common_lib import error |
| from autotest_lib.frontend.afe import models, rpc_utils |
| from autotest_lib.frontend.afe import rpcserver_logging |
| from autotest_lib.frontend.afe.json_rpc import serviceHandler |
| |
| LOGGING_REGEXPS = [r'.*add_.*', |
| r'delete_.*', |
| r'.*remove_.*', |
| r'modify_.*', |
| r'create.*', |
| r'set_.*'] |
| FULL_REGEXP = '(' + '|'.join(LOGGING_REGEXPS) + ')' |
| COMPILED_REGEXP = re.compile(FULL_REGEXP) |
| |
| SHARD_RPC_INTERFACE = 'shard_rpc_interface' |
| COMMON_RPC_INTERFACE = 'common_rpc_interface' |
| |
| def should_log_message(name): |
| """Detect whether to log message. |
| |
| @param name: the method name. |
| """ |
| return COMPILED_REGEXP.match(name) |
| |
| |
| class RpcMethodHolder(object): |
| 'Dummy class to hold RPC interface methods as attributes.' |
| |
| |
| class RpcValidator(object): |
| """Validate Rpcs handled by RpcHandler. |
| |
| This validator is introduced to filter RPC's callers. If a caller is not |
| allowed to call a given RPC, it will be refused by the validator. |
| """ |
| def __init__(self, rpc_interface_modules): |
| self._shard_rpc_methods = [] |
| self._common_rpc_methods = [] |
| |
| for module in rpc_interface_modules: |
| if COMMON_RPC_INTERFACE in module.__name__: |
| self._common_rpc_methods = self._grab_name_from(module) |
| |
| if SHARD_RPC_INTERFACE in module.__name__: |
| self._shard_rpc_methods = self._grab_name_from(module) |
| |
| |
| def _grab_name_from(self, module): |
| """Grab function name from module and add them to rpc_methods. |
| |
| @param module: an actual module. |
| """ |
| rpc_methods = [] |
| for name in dir(module): |
| if name.startswith('_'): |
| continue |
| attribute = getattr(module, name) |
| if not inspect.isfunction(attribute): |
| continue |
| rpc_methods.append(attribute.func_name) |
| |
| return rpc_methods |
| |
| |
| def validate_rpc_only_called_by_main(self, meth_name, remote_ip): |
| """Validate whether the method name can be called by remote_ip. |
| |
| This funcion checks whether the given method (meth_name) belongs to |
| _shard_rpc_module. |
| |
| If True, it then checks whether the caller's IP (remote_ip) is autotest |
| main. An RPCException will be raised if an RPC method from |
| _shard_rpc_module is called by a caller that is not autotest main. |
| |
| @param meth_name: the RPC method name which is called. |
| @param remote_ip: the caller's IP. |
| """ |
| if meth_name in self._shard_rpc_methods: |
| global_afe_ip = rpc_utils.get_ip(rpc_utils.GLOBAL_AFE_HOSTNAME) |
| if remote_ip != global_afe_ip: |
| raise error.RPCException( |
| 'Shard RPC %r cannot be called by remote_ip %s. It ' |
| 'can only be called by global_afe: %s' % ( |
| meth_name, remote_ip, global_afe_ip)) |
| |
| |
| def encode_validate_result(self, meth_id, err): |
| """Encode the return results for validator. |
| |
| It is used for encoding return response for RPC handler if caller of an |
| RPC is refused by validator. |
| |
| @param meth_id: the id of the request for an RPC method. |
| @param err: The error raised by validator. |
| |
| @return: a raw http response including the encoded error result. It |
| will be parsed by service proxy. |
| """ |
| error_result = serviceHandler.ServiceHandler.blank_result_dict() |
| error_result['id'] = meth_id |
| error_result['err'] = err |
| error_result['err_traceback'] = traceback.format_exc() |
| result = self.encode_result(error_result) |
| return rpc_utils.raw_http_response(result) |
| |
| |
| class RpcHandler(object): |
| """The class to handle Rpc requests.""" |
| |
| def __init__(self, rpc_interface_modules, document_module=None): |
| """Initialize an RpcHandler instance. |
| |
| @param rpc_interface_modules: the included rpc interface modules. |
| @param document_module: the module includes documentation. |
| """ |
| self._rpc_methods = RpcMethodHolder() |
| self._dispatcher = serviceHandler.ServiceHandler(self._rpc_methods) |
| self._rpc_validator = RpcValidator(rpc_interface_modules) |
| |
| # store all methods from interface modules |
| for module in rpc_interface_modules: |
| self._grab_methods_from(module) |
| |
| # get documentation for rpc_interface we can send back to the |
| # user |
| if document_module is None: |
| document_module = rpc_interface_modules[0] |
| self.html_doc = pydoc.html.document(document_module) |
| |
| |
| def get_rpc_documentation(self): |
| """Get raw response from an http documentation.""" |
| return rpc_utils.raw_http_response(self.html_doc) |
| |
| |
| def raw_request_data(self, request): |
| """Return raw data in request. |
| |
| @param request: the request to get raw data from. |
| """ |
| if request.method == 'POST': |
| return request.body |
| return urllib.unquote(request.META['QUERY_STRING']) |
| |
| |
| def execute_request(self, json_request): |
| """Execute a json request. |
| |
| @param json_request: the json request to be executed. |
| """ |
| return self._dispatcher.handleRequest(json_request) |
| |
| |
| def decode_request(self, json_request): |
| """Decode the json request. |
| |
| @param json_request: the json request to be decoded. |
| """ |
| return self._dispatcher.translateRequest(json_request) |
| |
| |
| def dispatch_request(self, decoded_request): |
| """Invoke a RPC call from a decoded request. |
| |
| @param decoded_request: the json request to be processed and run. |
| """ |
| return self._dispatcher.dispatchRequest(decoded_request) |
| |
| |
| def log_request(self, user, decoded_request, decoded_result, |
| remote_ip, log_all=False): |
| """Log request if required. |
| |
| @param user: current user. |
| @param decoded_request: the decoded request. |
| @param decoded_result: the decoded result. |
| @param remote_ip: the caller's ip. |
| @param log_all: whether to log all messages. |
| """ |
| if log_all or should_log_message(decoded_request['method']): |
| msg = '%s| %s:%s %s' % (remote_ip, decoded_request['method'], |
| user, decoded_request['params']) |
| if decoded_result['err']: |
| msg += '\n' + decoded_result['err_traceback'] |
| rpcserver_logging.rpc_logger.error(msg) |
| else: |
| rpcserver_logging.rpc_logger.info(msg) |
| |
| |
| def encode_result(self, results): |
| """Encode the result to translated json result. |
| |
| @param results: the results to be encoded. |
| """ |
| return self._dispatcher.translateResult(results) |
| |
| |
| def handle_rpc_request(self, request): |
| """Handle common rpc request and return raw response. |
| |
| @param request: the rpc request to be processed. |
| """ |
| remote_ip = self._get_remote_ip(request) |
| user = models.User.current_user() |
| json_request = self.raw_request_data(request) |
| decoded_request = self.decode_request(json_request) |
| |
| # Validate whether method can be called by the remote_ip |
| try: |
| meth_id = decoded_request['id'] |
| meth_name = decoded_request['method'] |
| self._rpc_validator.validate_rpc_only_called_by_main( |
| meth_name, remote_ip) |
| except KeyError: |
| raise serviceHandler.BadServiceRequest(decoded_request) |
| except error.RPCException as e: |
| return self._rpc_validator.encode_validate_result(meth_id, e) |
| |
| decoded_request['remote_ip'] = remote_ip |
| decoded_result = self.dispatch_request(decoded_request) |
| result = self.encode_result(decoded_result) |
| if rpcserver_logging.LOGGING_ENABLED: |
| self.log_request(user, decoded_request, decoded_result, |
| remote_ip) |
| return rpc_utils.raw_http_response(result) |
| |
| |
| def handle_jsonp_rpc_request(self, request): |
| """Handle the json rpc request and return raw response. |
| |
| @param request: the rpc request to be handled. |
| """ |
| request_data = request.GET['request'] |
| callback_name = request.GET['callback'] |
| # callback_name must be a simple identifier |
| assert re.search(r'^\w+$', callback_name) |
| |
| result = self.execute_request(request_data) |
| padded_result = '%s(%s)' % (callback_name, result) |
| return rpc_utils.raw_http_response(padded_result, |
| content_type='text/javascript') |
| |
| |
| @staticmethod |
| def _allow_keyword_args(f): |
| """\ |
| Decorator to allow a function to take keyword args even though |
| the RPC layer doesn't support that. The decorated function |
| assumes its last argument is a dictionary of keyword args and |
| passes them to the original function as keyword args. |
| """ |
| def new_fn(*args): |
| """Make the last argument as the keyword args.""" |
| assert args |
| keyword_args = args[-1] |
| args = args[:-1] |
| return f(*args, **keyword_args) |
| new_fn.func_name = f.func_name |
| return new_fn |
| |
| |
| def _grab_methods_from(self, module): |
| for name in dir(module): |
| if name.startswith('_'): |
| continue |
| attribute = getattr(module, name) |
| if not inspect.isfunction(attribute): |
| continue |
| decorated_function = RpcHandler._allow_keyword_args(attribute) |
| setattr(self._rpc_methods, name, decorated_function) |
| |
| |
| def _get_remote_ip(self, request): |
| """Get the ip address of a RPC caller. |
| |
| Returns the IP of the request, accounting for the possibility of |
| being behind a proxy. |
| If a Django server is behind a proxy, request.META["REMOTE_ADDR"] will |
| return the proxy server's IP, not the client's IP. |
| The proxy server would provide the client's IP in the |
| HTTP_X_FORWARDED_FOR header. |
| |
| @param request: django.core.handlers.wsgi.WSGIRequest object. |
| |
| @return: IP address of remote host as a string. |
| Empty string if the IP cannot be found. |
| """ |
| remote = request.META.get('HTTP_X_FORWARDED_FOR', None) |
| if remote: |
| # X_FORWARDED_FOR returns client1, proxy1, proxy2,... |
| remote = remote.split(',')[0].strip() |
| else: |
| remote = request.META.get('REMOTE_ADDR', '') |
| return remote |