def setup_bus(self, exchange_name, connection_url, qkwargs): """ Sets up variables needed for the bus connection. :param exchange_name: Name of the topic exchange. :type exchange_name: str :param connection_url: Kombu connection url. :type connection_url: str :param qkwargs: One or more dicts keyword arguments for queue creation :type qkwargs: list """ self.logger.debug('Setting up bus connection.') self.bus = Bus(exchange_name, connection_url, qkwargs) self.bus.connect() self.dispatcher._bus = self.bus self.logger.info('Bus connection ready.')
class TestBus(TestCase): """ Test for the Bus class. """ def setUp(self): """ Creates a new instance to test with per test. """ self.bus_instance = Bus(EXCHANGE, CONNECTION_URL, QUEUE_KWARGS) def test_init_kwargs(self): """ Verify the Bus.init_kwargs returns the arguments used to create the instance. """ self.assertEquals( { 'exchange_name': EXCHANGE, 'connection_url': CONNECTION_URL, 'qkwargs': QUEUE_KWARGS, }, self.bus_instance.init_kwargs) @mock.patch('commissaire_http.bus.Connection') @mock.patch('commissaire_http.bus.Exchange') @mock.patch('commissaire_http.bus.Producer') def test_connect(self, _producer, _exchange, _connection): """ Verify Bus.connect opens the connection to the bus. """ self.bus_instance.connect() # Connection should be called with the proper url _connection.assert_called_once_with(self.bus_instance.connection_url) _connection().channel.assert_called_once_with() # Exchange should be called ... _exchange.assert_called_once_with( self.bus_instance.exchange_name, type='topic') # .. and bound # One queue should be Created self.assertEqual(1, len(self.bus_instance._queues)) _exchange().bind.assert_called_once_with(self.bus_instance._channel) # We should have a new producer _producer.assert_called_once_with( self.bus_instance._channel, self.bus_instance._exchange)
def setup_bus(self, exchange_name, connection_url, qkwargs): """ Sets up a bus connection with the given configuration. Call this method only once after instantiating a Dispatcher. :param exchange_name: Name of the topic exchange :type exchange_name: str :param connection_url: Kombu connection URL :type connection_url: str :param qkwargs: One or more keyword argument dicts for queue creation :type qkwargs: list """ self.logger.debug('Setting up bus connection.') bus_init_kwargs = { 'exchange_name': exchange_name, 'connection_url': connection_url, 'qkwargs': qkwargs } self._bus = Bus(**bus_init_kwargs) self.logger.debug('Bus instance created with: %s', bus_init_kwargs) self._bus.connect() self.logger.info('Bus connection ready.')
class Dispatcher: """ Dispatches and translates between HTTP requests and bus services. """ #: Logging instance for all Dispatchers logger = logging.getLogger('Dispatcher') def __init__(self, router, handler_packages): """ Initializes a new Dispatcher instance. :param router: The router to dispatch with. :type router: router.TopicRouter :param handler_packages: List of packages to load handlers from. :type handler_packages: list """ self._router = router self._handler_packages = handler_packages self._handler_map = {} self.reload_handlers() self._bus = None def setup_bus(self, exchange_name, connection_url, qkwargs): """ Sets up a bus connection with the given configuration. Call this method only once after instantiating a Dispatcher. :param exchange_name: Name of the topic exchange :type exchange_name: str :param connection_url: Kombu connection URL :type connection_url: str :param qkwargs: One or more keyword argument dicts for queue creation :type qkwargs: list """ self.logger.debug('Setting up bus connection.') bus_init_kwargs = { 'exchange_name': exchange_name, 'connection_url': connection_url, 'qkwargs': qkwargs } self._bus = Bus(**bus_init_kwargs) self.logger.debug( 'Bus instance created with: {}'.format(bus_init_kwargs)) self._bus.connect() self.logger.info('Bus connection ready.') def reload_handlers(self): """ Reloads the handler mapping. """ for pkg in self._handler_packages: try: mod = import_module(pkg) for item, attr, mod_path in ls_mod(mod, pkg): if isinstance(attr, BasicHandler): self._handler_map[mod_path] = attr self.logger.info( 'Loaded function handler {} to {}'.format( mod_path, attr)) elif (isclass(attr) and issubclass(attr, object) and not issubclass(attr, BasicHandler)): handler_instance = attr() for handler_meth, sub_attr, sub_mod_path in \ ls_mod(handler_instance, pkg): key = '.'.join([mod_path, handler_meth]) self._handler_map[key] = getattr( handler_instance, handler_meth) self.logger.info( 'Instantiated and loaded class handler ' '{} to {}'.format(key, handler_instance)) else: self.logger.debug( '{} can not be used as a handler.'.format( mod_path)) except ImportError as error: self.logger.error( 'Unable to import handler package "{}". {}: {}'.format( pkg, type(error), error)) def dispatch(self, environ, start_response): """ Dispatches an HTTP request into a jsonrpc message, passes it to a handler, translates the results, and returns the HTTP response back to the requestor. :param environ: WSGI environment dictionary. :type environ: dict :param start_response: WSGI start_response callable. :type start_response: callable :returns: The body of the HTTP response. :rtype: Mixed """ # Fail early if _bus has never been set. if self._bus is None: raise DispatcherError('Bus can not be None when dispatching. ' 'Please call dispatcher.setup_bus().') # Add the bus instance to the WSGI environment dictionary. environ['commissaire.bus'] = self._bus # Add the routematch results to the WSGI environment dictionary. match_result = self._router.routematch(environ['PATH_INFO'], environ) if match_result is None: start_response('404 Not Found', [('content-type', 'text/html')]) return [bytes('Not Found', 'utf8')] environ['commissaire.routematch'] = match_result route_dict = match_result[0] route_controller = route_dict['controller'] try: # If the handler registered is a callable, use it if callable(route_controller): handler = route_controller # Else load what we found earlier else: handler = self._handler_map.get(route_controller) self.logger.debug('Using controller {}->{}'.format( route_dict, handler)) return handler(environ, start_response) except Exception as error: self.logger.error('Exception raised in handler {}:\n{}'.format( route_controller, traceback.format_exc())) start_response('500 Internal Server Error', [('content-type', 'text/html')]) return [bytes('Internal Server Error', 'utf8')]
def setUp(self): """ Creates a new instance to test with per test. """ self.bus_instance = Bus(EXCHANGE, CONNECTION_URL, QUEUE_KWARGS)
class CommissaireHttpServer: """ Http Server for Commissaire. """ #: Class level logger logger = logging.getLogger('CommissaireHttpServer') def __init__(self, bind_host, bind_port, dispatcher, tls_pem_file=None, tls_clientverify_file=None): """ Initializes a new CommissaireHttpServer instance. :param bind_host: Host adapter to listen on. :type bind_host: str :param bind_port: Host port to listen on. :type bind_port: int :param dispatcher: Dispatcher instance (WSGI) to route and respond. :type dispatcher: commissaire_http.dispatcher.Dispatcher :param tls_pem_file: Full path to the PEM file for TLS. :type tls_pem_file: str :param tls_clientverify_file: Full path to CA to verify certs. :type tls_clientverify_file: str """ # To use the bus call setup_bus() self.bus = None self._bind_host = bind_host self._bind_port = bind_port self._tls_pem_file = tls_pem_file self._tls_clientverify_file = tls_clientverify_file self.dispatcher = dispatcher self._httpd = make_server( self._bind_host, self._bind_port, self.dispatcher.dispatch, server_class=ThreadedWSGIServer, handler_class=CommissaireRequestHandler) # If we are given a PEM file then wrap the socket if tls_pem_file: import ssl client_side_cert_kwargs = {} if self._tls_clientverify_file: client_side_cert_kwargs = { 'cert_reqs': ssl.CERT_REQUIRED, 'ca_certs': self._tls_clientverify_file, } self.logger.info( 'Requiring client side certificate CA validation.') self._httpd.socket = ssl.wrap_socket( self._httpd.socket, certfile=self._tls_pem_file, ssl_version=ssl.PROTOCOL_TLSv1_2, server_side=True, **client_side_cert_kwargs) self.logger.info('Using TLS with {}'.format(self._tls_pem_file)) self.logger.debug('Created httpd server: {}:{}'.format( self._bind_host, self._bind_port)) def setup_bus(self, exchange_name, connection_url, qkwargs): """ Sets up variables needed for the bus connection. :param exchange_name: Name of the topic exchange. :type exchange_name: str :param connection_url: Kombu connection url. :type connection_url: str :param qkwargs: One or more dicts keyword arguments for queue creation :type qkwargs: list """ self.logger.debug('Setting up bus connection.') self.bus = Bus(exchange_name, connection_url, qkwargs) self.bus.connect() self.dispatcher._bus = self.bus self.logger.info('Bus connection ready.') def serve_forever(self): """ Serve HTTP. """ try: self._httpd.serve_forever() except Exception as error: self.logger.error('Server shut down {}: {}'.format( type(error), error))
def dispatch(self, environ, start_response): """ Dispatches an HTTP request into a jsonrpc message, passes it to a handler, translates the results, and returns the HTTP response back to the requestor. :param environ: WSGI environment dictionary. :type environ: dict :param start_response: WSGI start_response callable. :type start_response: callable :returns: The body of the HTTP response. :rtype: Mixed """ route_info = self._router.routematch(environ['PATH_INFO'], environ) # If we have valid route_info if route_info: # Split up the route from the route data route, route_data = route_info # Get the parameter params = self._get_params(environ, route, route_data) # method is normally supposed to be the method to be called # but we hijack it for the method that was used over HTTP jsonrpc_msg = { 'jsonrpc': '2.0', 'id': str(uuid.uuid4()), 'method': environ['REQUEST_METHOD'], 'params': params, } self.logger.debug( 'Request transformed to "{}".'.format(jsonrpc_msg)) # Get the resulting message back try: # If the handler registered is a callable, use it if callable(route['controller']): handler = route['controller'] # Else load what we found earlier else: handler = self._handler_map.get(route['controller']) self.logger.debug('Using controller {}->{}'.format( route, handler)) # Pass the message and, if needed, a new instance of the # bus to the handler bus = None if self._bus: bus = Bus(**self._bus.init_kwargs).connect() result = handler(jsonrpc_msg, bus=bus) self.logger.debug('Handler {} returned "{}"'.format( route['controller'], result)) if 'error' in result.keys(): error = result['error'] # If it's Invalid params handle it if error['code'] == JSONRPC_ERRORS['BAD_REQUEST']: start_response('400 Bad Request', [('content-type', 'application/json')]) return [bytes(json.dumps(error), 'utf8')] elif error['code'] == JSONRPC_ERRORS['NOT_FOUND']: start_response('404 Not Found', [('content-type', 'application/json')]) return [bytes(json.dumps(error), 'utf8')] elif error['code'] == JSONRPC_ERRORS['CONFLICT']: start_response('409 Conflict', [('content-type', 'application/json')]) return [bytes(json.dumps(error), 'utf8')] # Otherwise treat it like a 500 by raising raise Exception(result['error']) elif 'result' in result.keys(): start_response('200 OK', [('content-type', 'application/json')]) return [bytes(json.dumps(result['result']), 'utf8')] except Exception as error: self.logger.error( 'Exception raised while {} handled "{}". {}: {}'.format( route['controller'], jsonrpc_msg, type(error), error)) start_response('500 Internal Server Error', [('content-type', 'text/html')]) return [bytes('Internal Server Error', 'utf8')] # Otherwise handle it as a generic 404 start_response('404 Not Found', [('content-type', 'text/html')]) return [bytes('Not Found', 'utf8')]