blob: ca7e7ac86274733931d6633640bed5d60a247367 [file] [log] [blame]
mblighe8819cd2008-02-15 16:48:40 +00001"""\
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
xixuan543835d2016-08-22 14:39:27 -07008import inspect
9import pydoc
10import re
11import traceback
12import urllib
13
14from autotest_lib.client.common_lib import error
showard64a95952010-01-13 21:27:16 +000015from autotest_lib.frontend.afe import models, rpc_utils
showard02ed4bd2009-09-09 15:30:11 +000016from autotest_lib.frontend.afe import rpcserver_logging
xixuan543835d2016-08-22 14:39:27 -070017from autotest_lib.frontend.afe.json_rpc import serviceHandler
showard02ed4bd2009-09-09 15:30:11 +000018
showard02ed4bd2009-09-09 15:30:11 +000019LOGGING_REGEXPS = [r'.*add_.*',
20 r'delete_.*',
MK Ryu3e1de8b2015-05-27 16:47:10 -070021 r'.*remove_.*',
showard02ed4bd2009-09-09 15:30:11 +000022 r'modify_.*',
MK Ryu3e1de8b2015-05-27 16:47:10 -070023 r'create.*',
24 r'set_.*']
showard02ed4bd2009-09-09 15:30:11 +000025FULL_REGEXP = '(' + '|'.join(LOGGING_REGEXPS) + ')'
26COMPILED_REGEXP = re.compile(FULL_REGEXP)
27
xixuan543835d2016-08-22 14:39:27 -070028SHARD_RPC_INTERFACE = 'shard_rpc_interface'
29COMMON_RPC_INTERFACE = 'common_rpc_interface'
showard02ed4bd2009-09-09 15:30:11 +000030
31def should_log_message(name):
xixuan543835d2016-08-22 14:39:27 -070032 """Detect whether to log message.
33
34 @param name: the method name.
35 """
showard02ed4bd2009-09-09 15:30:11 +000036 return COMPILED_REGEXP.match(name)
mblighe8819cd2008-02-15 16:48:40 +000037
mblighe8819cd2008-02-15 16:48:40 +000038
39class RpcMethodHolder(object):
jadmanski0afbb632008-06-06 21:10:57 +000040 'Dummy class to hold RPC interface methods as attributes.'
mblighe8819cd2008-02-15 16:48:40 +000041
42
xixuan543835d2016-08-22 14:39:27 -070043class 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
showard7c785282008-05-29 19:45:12 +0000120class RpcHandler(object):
xixuan543835d2016-08-22 14:39:27 -0700121 """The class to handle Rpc requests."""
122
jadmanski0afbb632008-06-06 21:10:57 +0000123 def __init__(self, rpc_interface_modules, document_module=None):
xixuan543835d2016-08-22 14:39:27 -0700124 """Initialize an RpcHandler instance.
125
126 @param rpc_interface_modules: the included rpc interface modules.
127 @param document_module: the module includes documentation.
128 """
jadmanski0afbb632008-06-06 21:10:57 +0000129 self._rpc_methods = RpcMethodHolder()
showardef6fe022009-03-27 20:55:16 +0000130 self._dispatcher = serviceHandler.ServiceHandler(self._rpc_methods)
xixuan543835d2016-08-22 14:39:27 -0700131 self._rpc_validator = RpcValidator(rpc_interface_modules)
mblighe8819cd2008-02-15 16:48:40 +0000132
jadmanski0afbb632008-06-06 21:10:57 +0000133 # store all methods from interface modules
134 for module in rpc_interface_modules:
135 self._grab_methods_from(module)
showard7c785282008-05-29 19:45:12 +0000136
jadmanski0afbb632008-06-06 21:10:57 +0000137 # 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)
showard7c785282008-05-29 19:45:12 +0000142
143
showardef6fe022009-03-27 20:55:16 +0000144 def get_rpc_documentation(self):
xixuan543835d2016-08-22 14:39:27 -0700145 """Get raw response from an http documentation."""
showard2d7ac832009-06-22 18:14:10 +0000146 return rpc_utils.raw_http_response(self.html_doc)
showardef6fe022009-03-27 20:55:16 +0000147
148
showard3d6ae112009-05-02 00:45:48 +0000149 def raw_request_data(self, request):
xixuan543835d2016-08-22 14:39:27 -0700150 """Return raw data in request.
151
152 @param request: the request to get raw data from.
153 """
showardef6fe022009-03-27 20:55:16 +0000154 if request.method == 'POST':
155 return request.raw_post_data
156 return urllib.unquote(request.META['QUERY_STRING'])
157
158
showard3d6ae112009-05-02 00:45:48 +0000159 def execute_request(self, json_request):
xixuan543835d2016-08-22 14:39:27 -0700160 """Execute a json request.
161
162 @param json_request: the json request to be executed.
163 """
showard3d6ae112009-05-02 00:45:48 +0000164 return self._dispatcher.handleRequest(json_request)
165
166
167 def decode_request(self, json_request):
xixuan543835d2016-08-22 14:39:27 -0700168 """Decode the json request.
169
170 @param json_request: the json request to be decoded.
171 """
showard3d6ae112009-05-02 00:45:48 +0000172 return self._dispatcher.translateRequest(json_request)
173
174
175 def dispatch_request(self, decoded_request):
xixuan543835d2016-08-22 14:39:27 -0700176 """Invoke a RPC call from a decoded request.
177
178 @param decoded_request: the json request to be processed and run.
179 """
showard3d6ae112009-05-02 00:45:48 +0000180 return self._dispatcher.dispatchRequest(decoded_request)
181
182
showard02ed4bd2009-09-09 15:30:11 +0000183 def log_request(self, user, decoded_request, decoded_result,
MK Ryue782e852015-06-29 18:10:36 -0700184 remote_ip, log_all=False):
xixuan543835d2016-08-22 14:39:27 -0700185 """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 """
showard02ed4bd2009-09-09 15:30:11 +0000193 if log_all or should_log_message(decoded_request['method']):
MK Ryue782e852015-06-29 18:10:36 -0700194 msg = '%s| %s:%s %s' % (remote_ip, decoded_request['method'],
195 user, decoded_request['params'])
showard02ed4bd2009-09-09 15:30:11 +0000196 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):
xixuan543835d2016-08-22 14:39:27 -0700204 """Encode the result to translated json result.
205
206 @param results: the results to be encoded.
207 """
showard02ed4bd2009-09-09 15:30:11 +0000208 return self._dispatcher.translateResult(results)
209
210
showardef6fe022009-03-27 20:55:16 +0000211 def handle_rpc_request(self, request):
xixuan543835d2016-08-22 14:39:27 -0700212 """Handle common rpc request and return raw response.
213
214 @param request: the rpc request to be processed.
215 """
MK Ryue782e852015-06-29 18:10:36 -0700216 remote_ip = self._get_remote_ip(request)
showard64a95952010-01-13 21:27:16 +0000217 user = models.User.current_user()
showard3d6ae112009-05-02 00:45:48 +0000218 json_request = self.raw_request_data(request)
showard02ed4bd2009-09-09 15:30:11 +0000219 decoded_request = self.decode_request(json_request)
MK Ryue782e852015-06-29 18:10:36 -0700220
xixuan543835d2016-08-22 14:39:27 -0700221 # 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
MK Ryue782e852015-06-29 18:10:36 -0700232 decoded_request['remote_ip'] = remote_ip
showard02ed4bd2009-09-09 15:30:11 +0000233 decoded_result = self.dispatch_request(decoded_request)
234 result = self.encode_result(decoded_result)
showard902c96c2009-09-11 18:47:35 +0000235 if rpcserver_logging.LOGGING_ENABLED:
MK Ryue782e852015-06-29 18:10:36 -0700236 self.log_request(user, decoded_request, decoded_result,
237 remote_ip)
showard3d6ae112009-05-02 00:45:48 +0000238 return rpc_utils.raw_http_response(result)
showardef6fe022009-03-27 20:55:16 +0000239
240
241 def handle_jsonp_rpc_request(self, request):
xixuan543835d2016-08-22 14:39:27 -0700242 """Handle the json rpc request and return raw response.
243
244 @param request: the rpc request to be handled.
245 """
showardef6fe022009-03-27 20:55:16 +0000246 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
showard3d6ae112009-05-02 00:45:48 +0000251 result = self.execute_request(request_data)
showardef6fe022009-03-27 20:55:16 +0000252 padded_result = '%s(%s)' % (callback_name, result)
showard3d6ae112009-05-02 00:45:48 +0000253 return rpc_utils.raw_http_response(padded_result,
254 content_type='text/javascript')
showardef6fe022009-03-27 20:55:16 +0000255
256
jadmanski0afbb632008-06-06 21:10:57 +0000257 @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):
xixuan543835d2016-08-22 14:39:27 -0700266 """Make the last argument as the keyword args."""
jadmanski0afbb632008-06-06 21:10:57 +0000267 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
showard7c785282008-05-29 19:45:12 +0000273
274
jadmanski0afbb632008-06-06 21:10:57 +0000275 def _grab_methods_from(self, module):
276 for name in dir(module):
showard5deef7f2009-09-09 18:16:58 +0000277 if name.startswith('_'):
278 continue
jadmanski0afbb632008-06-06 21:10:57 +0000279 attribute = getattr(module, name)
showarda849ceb2010-01-20 01:12:42 +0000280 if not inspect.isfunction(attribute):
jadmanski0afbb632008-06-06 21:10:57 +0000281 continue
showardef6fe022009-03-27 20:55:16 +0000282 decorated_function = RpcHandler._allow_keyword_args(attribute)
jadmanski0afbb632008-06-06 21:10:57 +0000283 setattr(self._rpc_methods, name, decorated_function)
MK Ryue782e852015-06-29 18:10:36 -0700284
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