mbligh | e8819cd | 2008-02-15 16:48:40 +0000 | [diff] [blame] | 1 | """\ |
| 2 | RPC request handler Django. Exposed RPC interface functions should be |
| 3 | defined in rpc_interface.py. |
| 4 | """ |
| 5 | |
| 6 | __author__ = 'showard@google.com (Steve Howard)' |
| 7 | |
xixuan | 543835d | 2016-08-22 14:39:27 -0700 | [diff] [blame^] | 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 |
showard | 64a9595 | 2010-01-13 21:27:16 +0000 | [diff] [blame] | 15 | from autotest_lib.frontend.afe import models, rpc_utils |
showard | 02ed4bd | 2009-09-09 15:30:11 +0000 | [diff] [blame] | 16 | from autotest_lib.frontend.afe import rpcserver_logging |
xixuan | 543835d | 2016-08-22 14:39:27 -0700 | [diff] [blame^] | 17 | from autotest_lib.frontend.afe.json_rpc import serviceHandler |
showard | 02ed4bd | 2009-09-09 15:30:11 +0000 | [diff] [blame] | 18 | |
showard | 02ed4bd | 2009-09-09 15:30:11 +0000 | [diff] [blame] | 19 | LOGGING_REGEXPS = [r'.*add_.*', |
| 20 | r'delete_.*', |
MK Ryu | 3e1de8b | 2015-05-27 16:47:10 -0700 | [diff] [blame] | 21 | r'.*remove_.*', |
showard | 02ed4bd | 2009-09-09 15:30:11 +0000 | [diff] [blame] | 22 | r'modify_.*', |
MK Ryu | 3e1de8b | 2015-05-27 16:47:10 -0700 | [diff] [blame] | 23 | r'create.*', |
| 24 | r'set_.*'] |
showard | 02ed4bd | 2009-09-09 15:30:11 +0000 | [diff] [blame] | 25 | FULL_REGEXP = '(' + '|'.join(LOGGING_REGEXPS) + ')' |
| 26 | COMPILED_REGEXP = re.compile(FULL_REGEXP) |
| 27 | |
xixuan | 543835d | 2016-08-22 14:39:27 -0700 | [diff] [blame^] | 28 | SHARD_RPC_INTERFACE = 'shard_rpc_interface' |
| 29 | COMMON_RPC_INTERFACE = 'common_rpc_interface' |
showard | 02ed4bd | 2009-09-09 15:30:11 +0000 | [diff] [blame] | 30 | |
| 31 | def should_log_message(name): |
xixuan | 543835d | 2016-08-22 14:39:27 -0700 | [diff] [blame^] | 32 | """Detect whether to log message. |
| 33 | |
| 34 | @param name: the method name. |
| 35 | """ |
showard | 02ed4bd | 2009-09-09 15:30:11 +0000 | [diff] [blame] | 36 | return COMPILED_REGEXP.match(name) |
mbligh | e8819cd | 2008-02-15 16:48:40 +0000 | [diff] [blame] | 37 | |
mbligh | e8819cd | 2008-02-15 16:48:40 +0000 | [diff] [blame] | 38 | |
| 39 | class RpcMethodHolder(object): |
jadmanski | 0afbb63 | 2008-06-06 21:10:57 +0000 | [diff] [blame] | 40 | 'Dummy class to hold RPC interface methods as attributes.' |
mbligh | e8819cd | 2008-02-15 16:48:40 +0000 | [diff] [blame] | 41 | |
| 42 | |
xixuan | 543835d | 2016-08-22 14:39:27 -0700 | [diff] [blame^] | 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 | |
showard | 7c78528 | 2008-05-29 19:45:12 +0000 | [diff] [blame] | 120 | class RpcHandler(object): |
xixuan | 543835d | 2016-08-22 14:39:27 -0700 | [diff] [blame^] | 121 | """The class to handle Rpc requests.""" |
| 122 | |
jadmanski | 0afbb63 | 2008-06-06 21:10:57 +0000 | [diff] [blame] | 123 | def __init__(self, rpc_interface_modules, document_module=None): |
xixuan | 543835d | 2016-08-22 14:39:27 -0700 | [diff] [blame^] | 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 | """ |
jadmanski | 0afbb63 | 2008-06-06 21:10:57 +0000 | [diff] [blame] | 129 | self._rpc_methods = RpcMethodHolder() |
showard | ef6fe02 | 2009-03-27 20:55:16 +0000 | [diff] [blame] | 130 | self._dispatcher = serviceHandler.ServiceHandler(self._rpc_methods) |
xixuan | 543835d | 2016-08-22 14:39:27 -0700 | [diff] [blame^] | 131 | self._rpc_validator = RpcValidator(rpc_interface_modules) |
mbligh | e8819cd | 2008-02-15 16:48:40 +0000 | [diff] [blame] | 132 | |
jadmanski | 0afbb63 | 2008-06-06 21:10:57 +0000 | [diff] [blame] | 133 | # store all methods from interface modules |
| 134 | for module in rpc_interface_modules: |
| 135 | self._grab_methods_from(module) |
showard | 7c78528 | 2008-05-29 19:45:12 +0000 | [diff] [blame] | 136 | |
jadmanski | 0afbb63 | 2008-06-06 21:10:57 +0000 | [diff] [blame] | 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) |
showard | 7c78528 | 2008-05-29 19:45:12 +0000 | [diff] [blame] | 142 | |
| 143 | |
showard | ef6fe02 | 2009-03-27 20:55:16 +0000 | [diff] [blame] | 144 | def get_rpc_documentation(self): |
xixuan | 543835d | 2016-08-22 14:39:27 -0700 | [diff] [blame^] | 145 | """Get raw response from an http documentation.""" |
showard | 2d7ac83 | 2009-06-22 18:14:10 +0000 | [diff] [blame] | 146 | return rpc_utils.raw_http_response(self.html_doc) |
showard | ef6fe02 | 2009-03-27 20:55:16 +0000 | [diff] [blame] | 147 | |
| 148 | |
showard | 3d6ae11 | 2009-05-02 00:45:48 +0000 | [diff] [blame] | 149 | def raw_request_data(self, request): |
xixuan | 543835d | 2016-08-22 14:39:27 -0700 | [diff] [blame^] | 150 | """Return raw data in request. |
| 151 | |
| 152 | @param request: the request to get raw data from. |
| 153 | """ |
showard | ef6fe02 | 2009-03-27 20:55:16 +0000 | [diff] [blame] | 154 | if request.method == 'POST': |
| 155 | return request.raw_post_data |
| 156 | return urllib.unquote(request.META['QUERY_STRING']) |
| 157 | |
| 158 | |
showard | 3d6ae11 | 2009-05-02 00:45:48 +0000 | [diff] [blame] | 159 | def execute_request(self, json_request): |
xixuan | 543835d | 2016-08-22 14:39:27 -0700 | [diff] [blame^] | 160 | """Execute a json request. |
| 161 | |
| 162 | @param json_request: the json request to be executed. |
| 163 | """ |
showard | 3d6ae11 | 2009-05-02 00:45:48 +0000 | [diff] [blame] | 164 | return self._dispatcher.handleRequest(json_request) |
| 165 | |
| 166 | |
| 167 | def decode_request(self, json_request): |
xixuan | 543835d | 2016-08-22 14:39:27 -0700 | [diff] [blame^] | 168 | """Decode the json request. |
| 169 | |
| 170 | @param json_request: the json request to be decoded. |
| 171 | """ |
showard | 3d6ae11 | 2009-05-02 00:45:48 +0000 | [diff] [blame] | 172 | return self._dispatcher.translateRequest(json_request) |
| 173 | |
| 174 | |
| 175 | def dispatch_request(self, decoded_request): |
xixuan | 543835d | 2016-08-22 14:39:27 -0700 | [diff] [blame^] | 176 | """Invoke a RPC call from a decoded request. |
| 177 | |
| 178 | @param decoded_request: the json request to be processed and run. |
| 179 | """ |
showard | 3d6ae11 | 2009-05-02 00:45:48 +0000 | [diff] [blame] | 180 | return self._dispatcher.dispatchRequest(decoded_request) |
| 181 | |
| 182 | |
showard | 02ed4bd | 2009-09-09 15:30:11 +0000 | [diff] [blame] | 183 | def log_request(self, user, decoded_request, decoded_result, |
MK Ryu | e782e85 | 2015-06-29 18:10:36 -0700 | [diff] [blame] | 184 | remote_ip, log_all=False): |
xixuan | 543835d | 2016-08-22 14:39:27 -0700 | [diff] [blame^] | 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 | """ |
showard | 02ed4bd | 2009-09-09 15:30:11 +0000 | [diff] [blame] | 193 | if log_all or should_log_message(decoded_request['method']): |
MK Ryu | e782e85 | 2015-06-29 18:10:36 -0700 | [diff] [blame] | 194 | msg = '%s| %s:%s %s' % (remote_ip, decoded_request['method'], |
| 195 | user, decoded_request['params']) |
showard | 02ed4bd | 2009-09-09 15:30:11 +0000 | [diff] [blame] | 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): |
xixuan | 543835d | 2016-08-22 14:39:27 -0700 | [diff] [blame^] | 204 | """Encode the result to translated json result. |
| 205 | |
| 206 | @param results: the results to be encoded. |
| 207 | """ |
showard | 02ed4bd | 2009-09-09 15:30:11 +0000 | [diff] [blame] | 208 | return self._dispatcher.translateResult(results) |
| 209 | |
| 210 | |
showard | ef6fe02 | 2009-03-27 20:55:16 +0000 | [diff] [blame] | 211 | def handle_rpc_request(self, request): |
xixuan | 543835d | 2016-08-22 14:39:27 -0700 | [diff] [blame^] | 212 | """Handle common rpc request and return raw response. |
| 213 | |
| 214 | @param request: the rpc request to be processed. |
| 215 | """ |
MK Ryu | e782e85 | 2015-06-29 18:10:36 -0700 | [diff] [blame] | 216 | remote_ip = self._get_remote_ip(request) |
showard | 64a9595 | 2010-01-13 21:27:16 +0000 | [diff] [blame] | 217 | user = models.User.current_user() |
showard | 3d6ae11 | 2009-05-02 00:45:48 +0000 | [diff] [blame] | 218 | json_request = self.raw_request_data(request) |
showard | 02ed4bd | 2009-09-09 15:30:11 +0000 | [diff] [blame] | 219 | decoded_request = self.decode_request(json_request) |
MK Ryu | e782e85 | 2015-06-29 18:10:36 -0700 | [diff] [blame] | 220 | |
xixuan | 543835d | 2016-08-22 14:39:27 -0700 | [diff] [blame^] | 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 | |
MK Ryu | e782e85 | 2015-06-29 18:10:36 -0700 | [diff] [blame] | 232 | decoded_request['remote_ip'] = remote_ip |
showard | 02ed4bd | 2009-09-09 15:30:11 +0000 | [diff] [blame] | 233 | decoded_result = self.dispatch_request(decoded_request) |
| 234 | result = self.encode_result(decoded_result) |
showard | 902c96c | 2009-09-11 18:47:35 +0000 | [diff] [blame] | 235 | if rpcserver_logging.LOGGING_ENABLED: |
MK Ryu | e782e85 | 2015-06-29 18:10:36 -0700 | [diff] [blame] | 236 | self.log_request(user, decoded_request, decoded_result, |
| 237 | remote_ip) |
showard | 3d6ae11 | 2009-05-02 00:45:48 +0000 | [diff] [blame] | 238 | return rpc_utils.raw_http_response(result) |
showard | ef6fe02 | 2009-03-27 20:55:16 +0000 | [diff] [blame] | 239 | |
| 240 | |
| 241 | def handle_jsonp_rpc_request(self, request): |
xixuan | 543835d | 2016-08-22 14:39:27 -0700 | [diff] [blame^] | 242 | """Handle the json rpc request and return raw response. |
| 243 | |
| 244 | @param request: the rpc request to be handled. |
| 245 | """ |
showard | ef6fe02 | 2009-03-27 20:55:16 +0000 | [diff] [blame] | 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 | |
showard | 3d6ae11 | 2009-05-02 00:45:48 +0000 | [diff] [blame] | 251 | result = self.execute_request(request_data) |
showard | ef6fe02 | 2009-03-27 20:55:16 +0000 | [diff] [blame] | 252 | padded_result = '%s(%s)' % (callback_name, result) |
showard | 3d6ae11 | 2009-05-02 00:45:48 +0000 | [diff] [blame] | 253 | return rpc_utils.raw_http_response(padded_result, |
| 254 | content_type='text/javascript') |
showard | ef6fe02 | 2009-03-27 20:55:16 +0000 | [diff] [blame] | 255 | |
| 256 | |
jadmanski | 0afbb63 | 2008-06-06 21:10:57 +0000 | [diff] [blame] | 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): |
xixuan | 543835d | 2016-08-22 14:39:27 -0700 | [diff] [blame^] | 266 | """Make the last argument as the keyword args.""" |
jadmanski | 0afbb63 | 2008-06-06 21:10:57 +0000 | [diff] [blame] | 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 |
showard | 7c78528 | 2008-05-29 19:45:12 +0000 | [diff] [blame] | 273 | |
| 274 | |
jadmanski | 0afbb63 | 2008-06-06 21:10:57 +0000 | [diff] [blame] | 275 | def _grab_methods_from(self, module): |
| 276 | for name in dir(module): |
showard | 5deef7f | 2009-09-09 18:16:58 +0000 | [diff] [blame] | 277 | if name.startswith('_'): |
| 278 | continue |
jadmanski | 0afbb63 | 2008-06-06 21:10:57 +0000 | [diff] [blame] | 279 | attribute = getattr(module, name) |
showard | a849ceb | 2010-01-20 01:12:42 +0000 | [diff] [blame] | 280 | if not inspect.isfunction(attribute): |
jadmanski | 0afbb63 | 2008-06-06 21:10:57 +0000 | [diff] [blame] | 281 | continue |
showard | ef6fe02 | 2009-03-27 20:55:16 +0000 | [diff] [blame] | 282 | decorated_function = RpcHandler._allow_keyword_args(attribute) |
jadmanski | 0afbb63 | 2008-06-06 21:10:57 +0000 | [diff] [blame] | 283 | setattr(self._rpc_methods, name, decorated_function) |
MK Ryu | e782e85 | 2015-06-29 18:10:36 -0700 | [diff] [blame] | 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 |