Exemplo n.º 1
0
    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.')
Exemplo n.º 2
0
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)
Exemplo n.º 3
0
    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.')
Exemplo n.º 4
0
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')]
Exemplo n.º 5
0
 def setUp(self):
     """
     Creates a new instance to test with per test.
     """
     self.bus_instance = Bus(EXCHANGE, CONNECTION_URL, QUEUE_KWARGS)
Exemplo n.º 6
0
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))
Exemplo n.º 7
0
    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')]