• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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