xref: /aosp_15_r20/external/autotest/frontend/afe/rpc_handler.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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