Home | History | Annotate | Download | only in afe
      1 """\
      2 RPC request handler Django.  Exposed RPC interface functions should be
      3 defined in rpc_interface.py.
      4 """
      5 
      6 __author__ = 'showard (at] google.com (Steve Howard)'
      7 
      8 import inspect
      9 import pydoc
     10 import re
     11 import traceback
     12 import urllib
     13 
     14 from autotest_lib.client.common_lib import error
     15 from autotest_lib.frontend.afe import models, rpc_utils
     16 from autotest_lib.frontend.afe import rpcserver_logging
     17 from autotest_lib.frontend.afe.json_rpc import serviceHandler
     18 
     19 LOGGING_REGEXPS = [r'.*add_.*',
     20                    r'delete_.*',
     21                    r'.*remove_.*',
     22                    r'modify_.*',
     23                    r'create.*',
     24                    r'set_.*']
     25 FULL_REGEXP = '(' + '|'.join(LOGGING_REGEXPS) + ')'
     26 COMPILED_REGEXP = re.compile(FULL_REGEXP)
     27 
     28 SHARD_RPC_INTERFACE = 'shard_rpc_interface'
     29 COMMON_RPC_INTERFACE = 'common_rpc_interface'
     30 
     31 def 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 
     39 class RpcMethodHolder(object):
     40     'Dummy class to hold RPC interface methods as attributes.'
     41 
     42 
     43 class 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 
    120 class 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