Exemplo n.º 1
0
    def test_no_subscriptions(self):
        adapter = StompAdapterImpl(Reactor(), defaultdict(list), {})
        dispatcher = AsyncDispatcher(FakeConnection(adapter), adapter)

        frame1 = Frame(
            Command.SUBSCRIBE, {
                Headers.DESTINATION: 'jms.queue.events',
                'ack': 'auto',
                'id': 'ad052acb-a934-4e10-8ec3-00c7417ef8d1'
            })

        frame2 = Frame(
            Command.SUBSCRIBE, {
                Headers.DESTINATION: 'jms.queue.events',
                'ack': 'auto',
                'id': 'ad052acb-a934-4e10-8ec3-00c7417ef8d2'
            })

        destinations = defaultdict(list)

        adapter = StompAdapterImpl(Reactor(), destinations, {})
        adapter.handle_frame(dispatcher, frame1)
        adapter.handle_frame(dispatcher, frame2)

        adapter.handle_timeout(dispatcher)

        self.assertEqual(len(adapter._sub_ids), 0)
        self.assertEqual(len(destinations), 0)
Exemplo n.º 2
0
    def test_close(self):
        reactor = Reactor()
        thread = concurrent.thread(reactor.process_requests, name="test ractor")
        thread.start()
        s1, s2 = socket.socketpair()
        with closing(s2):
            disp = reactor.create_dispatcher(s1, impl=TestingImpl())
            reactor.stop()

        thread.join(timeout=1)

        self.assertTrue(disp.closing)
        self.assertFalse(reactor._wakeupEvent.closing)
Exemplo n.º 3
0
    def test_close(self):
        reactor = Reactor()
        thread = concurrent.thread(reactor.process_requests,
                                   name='test ractor')
        thread.start()
        s1, s2 = socket.socketpair()
        with closing(s2):
            disp = reactor.create_dispatcher(s1, impl=TestingImpl())
            reactor.stop()

        thread.join(timeout=1)

        self.assertTrue(disp.closing)
        self.assertFalse(reactor._wakeupEvent.closing)
Exemplo n.º 4
0
    def test_send_internal_and_broker(self):
        frame = Frame(command=Command.SEND,
                      headers={Headers.DESTINATION: 'jms.topic.vdsm_requests',
                               Headers.REPLY_TO: 'jms.topic.vdsm_responses',
                               Headers.CONTENT_LENGTH: '103'},
                      body=('{"jsonrpc":"2.0","method":"Host.getAllVmStats",'
                            '"params":{},"id":"e8a936a6-d886-4cfa-97b9-2d54209'
                            '053ff"}'
                            )
                      )

        ids = {}

        subscription = FakeSubscription('jms.topic.vdsm_requests',
                                        'e8a936a6-d886-4cfa-97b9-2d54209053ff')
        client = FakeAsyncClient()
        subscription.set_client(client)

        destinations = defaultdict(list)
        destinations['jms.topic.vdsm_requests'].append(subscription)

        adapter = StompAdapterImpl(Reactor(), destinations, ids)
        adapter.handle_frame(FakeAsyncDispatcher(adapter), frame)

        data = adapter.pop_message()
        self.assertIsNot(data, None)
        request = JsonRpcRequest.decode(data)
        self.assertEqual(request.method, 'Host.getAllVmStats')
        self.assertEqual(len(ids), 1)

        resp_frame = client.pop_message()
        self.assertIsNot(resp_frame, None)
        self.assertEqual(resp_frame.command, Command.MESSAGE)
        self.assertEqual(resp_frame.body, data)
Exemplo n.º 5
0
 def __init__(
     self,
     host,
     port,
     sslctx=None,
     ssl_hanshake_timeout=SSLHandshakeDispatcher.SSL_HANDSHAKE_TIMEOUT,
 ):
     self._sslctx = sslctx
     self._reactor = Reactor()
     sock = _create_socket(host, port)
     self._host, self._port = sock.getsockname()
     self.log.info("Listening at %s:%d", self._host, self._port)
     self._acceptor = self._reactor.create_dispatcher(
         sock, _AcceptorImpl(self.handle_accept))
     self._acceptor.listen(5)
     self._handlers = []
     self.TIMEOUT = ssl_hanshake_timeout
Exemplo n.º 6
0
    def test_no_headers(self):
        frame = Frame(Command.CONNECT)

        adapter = StompAdapterImpl(Reactor(), defaultdict(list), {})
        adapter.handle_frame(FakeAsyncDispatcher(adapter), frame)

        resp_frame = adapter.pop_message()
        self.assertEqual(resp_frame.command, Command.ERROR)
        self.assertEqual(resp_frame.body, b'Version unsupported')
Exemplo n.º 7
0
    def test_send_no_destination(self):
        frame = Frame(Command.SEND, {Headers.DESTINATION: 'jms.topic.unknown'})

        adapter = StompAdapterImpl(Reactor(), defaultdict(list), {})
        adapter.handle_frame(FakeAsyncDispatcher(adapter), frame)

        resp_frame = adapter.pop_message()
        self.assertEqual(resp_frame.command, Command.ERROR)
        self.assertEqual(resp_frame.body, 'Subscription not available')
Exemplo n.º 8
0
    def test_no_id(self):
        frame = Frame(Command.UNSUBSCRIBE)

        adapter = StompAdapterImpl(Reactor(), defaultdict(list), {})
        adapter.handle_frame(FakeAsyncDispatcher(adapter), frame)

        resp_frame = adapter.pop_message()
        self.assertEqual(resp_frame.command, Command.ERROR)
        self.assertEqual(resp_frame.body, b'Missing id header')
Exemplo n.º 9
0
    def test_unsuported_version(self):
        frame = Frame(Command.CONNECT, {Headers.ACCEPT_VERSION: '1.0'})

        adapter = StompAdapterImpl(Reactor(), defaultdict(list), {})
        adapter.handle_frame(FakeAsyncDispatcher(adapter), frame)

        resp_frame = adapter.pop_message()
        self.assertEqual(resp_frame.command, Command.ERROR)
        self.assertEqual(resp_frame.body, 'Version unsupported')
Exemplo n.º 10
0
def listener(dummy_register_protocol_detector, key_cert_pair, request):
    key_file, cert_file = key_cert_pair
    reactor = Reactor()

    sslctx = SSLContext(cert_file=cert_file,
                        key_file=key_file,
                        ca_certs=cert_file)

    acceptor = MultiProtocolAcceptor(reactor, '127.0.0.1', 0, sslctx=sslctx)

    try:
        t = concurrent.thread(reactor.process_requests)
        t.start()
        (host, port) = acceptor._acceptor.socket.getsockname()[0:2]
        yield (host, port)
    finally:
        acceptor.stop()
        reactor.stop()
        t.join()
Exemplo n.º 11
0
    def test_no_heartbeat(self):
        frame = Frame(Command.CONNECT, {Headers.ACCEPT_VERSION: '1.2'})

        adapter = StompAdapterImpl(Reactor(), defaultdict(list), {})
        adapter.handle_frame(FakeAsyncDispatcher(adapter), frame)

        resp_frame = adapter.pop_message()
        self.assertEqual(resp_frame.command, Command.CONNECTED)
        self.assertEqual(resp_frame.headers['version'], '1.2')
        self.assertEqual(resp_frame.headers[Headers.HEARTBEAT], '0,0')
Exemplo n.º 12
0
    def test_connect(self):
        frame = Frame(Command.CONNECT,
                      {Headers.ACCEPT_VERSION: '1.2',
                       Headers.HEARTEBEAT: '0,8000'})

        adapter = StompAdapterImpl(Reactor(), defaultdict(list), {})
        adapter.handle_frame(TestDispatcher(adapter), frame)

        resp_frame = adapter.pop_message()
        self.assertEquals(resp_frame.command, Command.CONNECTED)
        self.assertEquals(resp_frame.headers['version'], '1.2')
        self.assertEquals(resp_frame.headers[Headers.HEARTEBEAT], '8000,0')
Exemplo n.º 13
0
 def start_acceptor(self, use_ssl, address='127.0.0.1'):
     self.reactor = Reactor()
     self.acceptor = MultiProtocolAcceptor(
         self.reactor, address, 0, sslctx=self.SSLCTX if use_ssl else None)
     self.acceptor.TIMEOUT = 1
     self.acceptor.add_detector(Echo())
     self.acceptor.add_detector(Uppercase())
     self.acceptor_address = \
         self.acceptor._acceptor.socket.getsockname()[0:2]
     t = threading.Thread(target=self.reactor.process_requests)
     t.deamon = True
     t.start()
Exemplo n.º 14
0
    def test_no_destination(self):
        frame = Frame(Command.SUBSCRIBE,
                      {'ack': 'auto',
                       'id': 'ad052acb-a934-4e10-8ec3-00c7417ef8d1'})

        adapter = StompAdapterImpl(Reactor(), defaultdict(list), {})
        adapter.handle_frame(FakeAsyncDispatcher(adapter), frame)

        resp_frame = adapter.pop_message()
        self.assertEqual(resp_frame.command, Command.ERROR)
        self.assertEqual(resp_frame.body,
                         b'Missing destination or subscription id header')
Exemplo n.º 15
0
    def test_no_id(self):
        frame = Frame(Command.SUBSCRIBE,
                      {'ack': 'auto',
                       Headers.DESTINATION: 'jms.queue.events'})

        adapter = StompAdapterImpl(Reactor(), defaultdict(list), {})
        adapter.handle_frame(FakeAsyncDispatcher(adapter), frame)

        resp_frame = adapter.pop_message()
        self.assertEqual(resp_frame.command, Command.ERROR)
        self.assertEqual(resp_frame.body,
                         b'Missing destination or subscription id header')
Exemplo n.º 16
0
def constructAcceptor(log,
                      ssl,
                      jsonBridge,
                      dest=LEGACY_SUBSCRIPTION_ID_RESPONSE):
    sslctx = DEAFAULT_SSL_CONTEXT if ssl else None
    reactor = Reactor()
    acceptor = MultiProtocolAcceptor(
        reactor,
        "::1",
        0,
        sslctx,
    )

    scheduler = schedule.Scheduler(name="test.Scheduler",
                                   clock=utils.monotonic_time)
    scheduler.start()

    cif = FakeClientIf(dest)

    json_binding = BindingJsonRpc(jsonBridge, defaultdict(list), 60, scheduler,
                                  cif)
    json_binding.start()

    cif.json_binding = json_binding

    with namedTemporaryDir() as tmp_dir:
        client_log = os.path.join(tmp_dir, 'client.log')
        with MonkeyPatchScope([(API.clientIF, 'getInstance', lambda _: cif),
                               (constants, 'P_VDSM_CLIENT_LOG', client_log)]):
            xml_binding = BindingXMLRPC(cif, cif.log)
            xml_binding.start()
            xmlDetector = XmlDetector(xml_binding)
            acceptor.add_detector(xmlDetector)

            jsonBridge.cif = cif

            stompDetector = StompDetector(json_binding)
            acceptor.add_detector(stompDetector)

            thread = threading.Thread(target=reactor.process_requests,
                                      name='Detector thread')
            thread.setDaemon(True)
            thread.start()

            try:
                yield acceptor
            finally:
                acceptor.stop()
                json_binding.stop()
                xml_binding.stop()
                scheduler.stop(wait=False)
Exemplo n.º 17
0
    def test_incoming_heartbeat(self):
        frame = Frame(Command.CONNECT,
                      {Headers.ACCEPT_VERSION: '1.2',
                       Headers.HEARTBEAT: '6000,5000'})

        adapter = StompAdapterImpl(Reactor(), defaultdict(list), {})
        dispatcher = AsyncDispatcher(FakeConnection(adapter), adapter)
        adapter.handle_frame(dispatcher, frame)

        resp_frame = adapter.pop_message()
        self.assertEqual(resp_frame.command, Command.CONNECTED)
        self.assertEqual(resp_frame.headers['version'], '1.2')
        self.assertEqual(resp_frame.headers[Headers.HEARTBEAT], '5000,6000')
        self.assertEqual(dispatcher._incoming_heartbeat_in_milis, 6000)
        self.assertEqual(dispatcher._outgoing_heartbeat_in_milis, 5000)
Exemplo n.º 18
0
    def test_subscribe(self):
        frame = Frame(Command.SUBSCRIBE,
                      {Headers.DESTINATION: 'jms.queue.events',
                       'ack': 'auto',
                       'id': 'ad052acb-a934-4e10-8ec3-00c7417ef8d1'})

        destinations = defaultdict(list)

        adapter = StompAdapterImpl(Reactor(), destinations, {})
        adapter.handle_frame(FakeAsyncDispatcher(adapter), frame)

        subscription = destinations['jms.queue.events'][0]
        self.assertEqual(subscription.id,
                         'ad052acb-a934-4e10-8ec3-00c7417ef8d1')
        self.assertEqual(subscription.destination,
                         'jms.queue.events')
Exemplo n.º 19
0
    def test_no_flow_header(self):
        frame = Frame(command=Command.SEND,
                      headers={Headers.DESTINATION: SUBSCRIPTION_ID_REQUEST,
                               Headers.REPLY_TO: 'jms.topic.vdsm_responses',
                               Headers.CONTENT_LENGTH: '103'},
                      body=('{"jsonrpc":"2.0","method":"Host.getAllVmStats",'
                            '"params":{},"id":"e8a936a6-d886-4cfa-97b9-2d54209'
                            '053ff"}'
                            )
                      )

        adapter = StompAdapterImpl(Reactor(), defaultdict(list), {})
        dispatcher = FakeAsyncDispatcher(adapter)
        adapter.handle_frame(dispatcher, frame)

        self.assertIsNone(dispatcher.connection.flow_id)
Exemplo n.º 20
0
    def test_unsubscribe(self):
        frame = Frame(Command.UNSUBSCRIBE,
                      {'id': 'ad052acb-a934-4e10-8ec3-00c7417ef8d1'})

        subscription = FakeSubscription('jms.queue.events',
                                        'ad052acb-a934-4e10-8ec3-00c7417ef8d1')

        destinations = defaultdict(list)
        destinations['jms.queue.events'].append(subscription)

        adapter = StompAdapterImpl(Reactor(), destinations, {})
        adapter._sub_ids['ad052acb-a934-4e10-8ec3-00c7417ef8d1'] = subscription
        adapter.handle_frame(FakeAsyncDispatcher(adapter), frame)

        self.assertEqual(len(adapter._sub_ids), 0)
        self.assertEqual(len(destinations), 0)
Exemplo n.º 21
0
    def test_send_broker_parent_topic(self):
        frame = Frame(command=Command.SEND,
                      headers={Headers.DESTINATION:
                               'jms.topic.vdsm_requests.getAllVmStats',
                               Headers.REPLY_TO:
                               'jms.topic.vdsm_responses.getAllVmStats',
                               Headers.CONTENT_LENGTH: '103'},
                      body=('{"jsonrpc":"2.0","method":"Host.getAllVmStats",'
                            '"params":{},"id":"e8a936a6-d886-4cfa-97b9-2d54209'
                            '053ff"}'
                            )
                      )

        ids = {}

        subscription = TestSubscription('jms.topic.vdsm_requests',
                                        'e8a936a6-d886-4cfa-97b9-2d54209053ff')
        client = TestClient()
        subscription.set_client(client)

        destinations = defaultdict(list)
        destinations['jms'].append(subscription)
        destinations['jms.topic'].append(subscription)
        destinations['jms.topic.vdsm_requests'].append(subscription)

        # Topics that should not match
        destinations['jms.other'].append(subscription)
        destinations['jms.top'].append(subscription)

        adapter = StompAdapterImpl(Reactor(), destinations, ids)
        adapter.handle_frame(TestDispatcher(adapter), frame)

        data = adapter.pop_message()
        self.assertIsNot(data, None)
        request = JsonRpcRequest.decode(data)
        self.assertEqual(request.method, 'Host.getAllVmStats')
        self.assertEqual(len(ids), 1)

        for i in range(3):
            resp_frame = client.pop_message()
            self.assertIsNot(resp_frame, None)
            self.assertEqual(resp_frame.command, Command.MESSAGE)
            self.assertEqual(resp_frame.body, data)

        # The last two subscriptions do not match
        self.assertTrue(client.empty())
Exemplo n.º 22
0
 def __init__(
     self,
     host,
     port,
     sslctx=None,
     ssl_hanshake_timeout=SSLHandshakeDispatcher.SSL_HANDSHAKE_TIMEOUT,
 ):
     self._sslctx = sslctx
     self._reactor = Reactor()
     sock = _create_socket(host, port)
     self._host, self._port = sock.getsockname()
     self.log.info("Listening at %s:%d", self._host, self._port)
     self._acceptor = self._reactor.create_dispatcher(
         sock, _AcceptorImpl(self.handle_accept))
     self._acceptor.listen(5)
     self._handlers = []
     self.TIMEOUT = ssl_hanshake_timeout
Exemplo n.º 23
0
def constructAcceptor(log,
                      ssl,
                      jsonBridge,
                      dest=LEGACY_SUBSCRIPTION_ID_RESPONSE):
    sslctx = DEAFAULT_SSL_CONTEXT if ssl else None
    reactor = Reactor()
    acceptor = MultiProtocolAcceptor(
        reactor,
        "127.0.0.1",
        0,
        sslctx,
    )

    scheduler = schedule.Scheduler(name="test.Scheduler",
                                   clock=utils.monotonic_time)
    scheduler.start()
    json_binding = BindingJsonRpc(jsonBridge, defaultdict(list), 60, scheduler)
    json_binding.start()

    cif = FakeClientIf(json_binding, dest)

    xml_binding = BindingXMLRPC(cif, cif.log)
    xml_binding.start()
    xmlDetector = XmlDetector(xml_binding)
    acceptor.add_detector(xmlDetector)

    jsonBridge.cif = cif

    stompDetector = StompDetector(json_binding)
    acceptor.add_detector(stompDetector)

    thread = threading.Thread(target=reactor.process_requests,
                              name='Detector thread')
    thread.setDaemon(True)
    thread.start()

    try:
        yield acceptor
    finally:
        acceptor.stop()
        json_binding.stop()
        xml_binding.stop()
        scheduler.stop(wait=False)
Exemplo n.º 24
0
def constructAcceptor(log, ssl, jsonBridge,
                      dest=SUBSCRIPTION_ID_RESPONSE):
    sslctx = DEAFAULT_SSL_CONTEXT if ssl else None
    reactor = Reactor()
    acceptor = MultiProtocolAcceptor(
        reactor,
        "::1" if ipv6_enabled() else "127.0.0.1",
        0,
        sslctx,
    )

    scheduler = schedule.Scheduler(name="test.Scheduler",
                                   clock=time.monotonic_time)
    scheduler.start()

    cif = FakeClientIf(dest)

    json_binding = BindingJsonRpc(jsonBridge, defaultdict(list), 60,
                                  scheduler, cif)
    json_binding.start()

    cif.json_binding = json_binding

    with MonkeyPatchScope([
        (API.clientIF, 'getInstance', lambda _: cif),
        (API, 'confirm_connectivity', lambda: None)
    ]):
        jsonBridge.cif = cif

        stompDetector = StompDetector(json_binding)
        acceptor.add_detector(stompDetector)

        thread = threading.Thread(target=reactor.process_requests,
                                  name='Detector thread')
        thread.setDaemon(True)
        thread.start()

        try:
            yield acceptor
        finally:
            acceptor.stop()
            json_binding.stop()
            scheduler.stop(wait=False)
Exemplo n.º 25
0
def listener(dummy_register_protocol_detector, request):
    reactor = Reactor()

    excludes = getattr(request, 'param', 0)
    sslctx = SSLContext(cert_file=CRT_FILE,
                        key_file=KEY_FILE,
                        ca_certs=CRT_FILE,
                        excludes=excludes,
                        protocol=CLIENT_PROTOCOL)

    acceptor = MultiProtocolAcceptor(reactor, '127.0.0.1', 0, sslctx=sslctx)

    try:
        t = concurrent.thread(reactor.process_requests)
        t.start()
        (host, port) = acceptor._acceptor.socket.getsockname()[0:2]
        yield (host, port)
    finally:
        acceptor.stop()
        t.join()
Exemplo n.º 26
0
    def test_multipe_subscriptions(self):
        frame = Frame(Command.UNSUBSCRIBE,
                      {'id': 'ad052acb-a934-4e10-8ec3-00c7417ef8d1'})

        subscription = FakeSubscription('jms.queue.events',
                                        'ad052acb-a934-4e10-8ec3-00c7417ef8d1')
        subscription2 = FakeSubscription('jms.queue.events',
                                         'e8a93a6-d886-4cfa-97b9-2d54209053ff')

        destinations = defaultdict(list)
        destinations['jms.queue.events'].append(subscription)
        destinations['jms.queue.events'].append(subscription2)

        adapter = StompAdapterImpl(Reactor(), destinations, {})
        adapter._sub_ids['ad052acb-a934-4e10-8ec3-00c7417ef8d1'] = subscription
        adapter.handle_frame(FakeAsyncDispatcher(adapter), frame)

        self.assertEqual(len(adapter._sub_ids), 0)
        self.assertEqual(len(destinations), 1)
        self.assertEqual(destinations['jms.queue.events'], [subscription2])
Exemplo n.º 27
0
    def listen(self, excludes=0, protocol=CLIENT_PROTOCOL):
        reactor = Reactor()

        sslctx = SSLContext(cert_file=CRT_FILE,
                            key_file=KEY_FILE,
                            ca_certs=CRT_FILE,
                            excludes=excludes,
                            protocol=protocol)

        acceptor = MultiProtocolAcceptor(reactor,
                                         '127.0.0.1',
                                         0,
                                         sslctx=sslctx)

        try:
            t = concurrent.thread(reactor.process_requests)
            t.start()
            yield self.get_address(acceptor)
        finally:
            acceptor.stop()
            t.join()
Exemplo n.º 28
0
    def test_send_batch(self):
        body = ('[{"jsonrpc":"2.0","method":"Host.getAllVmStats","params":{},'
                '"id":"e8a936a6-d886-4cfa-97b9-2d54209053ff"},'
                '{"jsonrpc":"2.0","method":"Host.getAllVmStats","params":{},'
                '"id":"1202b274-5a06-4671-8b13-1d2715429668"}]')

        frame = Frame(command=Command.SEND,
                      headers={Headers.DESTINATION: 'jms.topic.vdsm_requests',
                               Headers.REPLY_TO: 'jms.topic.vdsm_responses',
                               Headers.CONTENT_LENGTH: '209'},
                      body=body
                      )

        ids = {}

        adapter = StompAdapterImpl(Reactor(), defaultdict(list), ids)
        adapter.handle_frame(FakeAsyncDispatcher(adapter), frame)

        data = adapter.pop_message()
        self.assertIsNot(data, None)
        self.assertEqual(len(ids), 2)
Exemplo n.º 29
0
    def test_send(self):
        frame = Frame(command=Command.SEND,
                      headers={Headers.DESTINATION: 'jms.topic.vdsm_requests',
                               Headers.REPLY_TO: 'jms.topic.vdsm_responses',
                               Headers.CONTENT_LENGTH: '103'},
                      body=('{"jsonrpc":"2.0","method":"Host.getAllVmStats",'
                            '"params":{},"id":"e8a936a6-d886-4cfa-97b9-2d54209'
                            '053ff"}'
                            )
                      )

        ids = {}

        adapter = StompAdapterImpl(Reactor(), defaultdict(list), ids)
        adapter.handle_frame(FakeAsyncDispatcher(adapter), frame)

        data = adapter.pop_message()
        self.assertIsNot(data, None)
        request = JsonRpcRequest.decode(data)
        self.assertEqual(request.method, 'Host.getAllVmStats')
        self.assertEqual(len(ids), 1)
Exemplo n.º 30
0
    def test_send_broker(self):
        frame = Frame(command=Command.SEND,
                      headers={Headers.DESTINATION: 'jms.topic.destination',
                               Headers.CONTENT_LENGTH: '103'},
                      body=('{"jsonrpc":"2.0","method":"Host.getAllVmStats",'
                            '"params":{},"id":"e8a936a6-d886-4cfa-97b9-2d54209'
                            '053ff"}'
                            )
                      )

        subscription = FakeSubscription('jms.topic.destination',
                                        'e8a936a6-d886-4cfa-97b9-2d54209053ff')

        destinations = defaultdict(list)
        destinations['jms.topic.destination'].append(subscription)

        adapter = StompAdapterImpl(Reactor(), destinations, {})
        subscription.set_client(adapter)
        adapter.handle_frame(FakeAsyncDispatcher(adapter), frame)

        resp_frame = adapter.pop_message()
        self.assertEqual(resp_frame.command, Command.MESSAGE)
Exemplo n.º 31
0
    def _createAcceptor(self, host, port):
        sslctx = sslutils.create_ssl_context()
        self._reactor = Reactor()

        self._acceptor = MultiProtocolAcceptor(self._reactor, host, port,
                                               sslctx)
Exemplo n.º 32
0
class clientIF(object):
    """
    The client interface of vdsm.

    Exposes vdsm verbs as json-rpc or xml-rpc functions.
    """
    _instance = None
    _instanceLock = threading.Lock()

    def __init__(self, irs, log, scheduler):
        """
        Initialize the (single) clientIF instance

        :param irs: a Dispatcher object to be used as this object's irs.
        :type irs: :class:`vdsm.storage.dispatcher.Dispatcher`
        :param log: a log object to be used for this object's logging.
        :type log: :class:`logging.Logger`
        """
        self.vm_container_lock = threading.Lock()
        self.vm_start_stop_lock = threading.Lock()
        self._networkSemaphore = threading.Semaphore()
        self._shutdownSemaphore = threading.Semaphore()
        self.irs = irs
        if self.irs:
            self._contEIOVmsCB = partial(clientIF.contEIOVms, proxy(self))
            self.irs.registerDomainStateChangeCallback(self._contEIOVmsCB)
        self.log = log
        self._recovery = True
        # TODO: The guest agent related code spreads around too much. There is
        # QemuGuestAgentPoller and ChannelListner here and then many instances
        # of GuestAgent per VM in vm.py. This should be refactored and
        # operated by single object. Idealy the distinction between what is
        # served by QEMU-GA and what is server by oVirt GA should not be
        # visible to the rest of the code.
        self.channelListener = Listener(self.log)
        self.qga_poller = QemuGuestAgentPoller(self, log, scheduler)
        self.mom = None
        self.servers = {}
        self._broker_client = None
        self._subscriptions = defaultdict(list)
        self._scheduler = scheduler
        self._unknown_vm_ids = set()
        if _glusterEnabled:
            self.gluster = gapi.GlusterApi()
        else:
            self.gluster = None
        try:
            self.vmContainer: Dict[str, vm.Vm] = {}
            self.lastRemoteAccess = 0
            self._enabled = True
            self._netConfigDirty = False
            self.mom = MomClient(config.get("mom", "socket_path"))
            self.mom.connect()
            secret.clear()
            concurrent.thread(self._recoverThread, name='vmrecovery').start()
            self.channelListener.settimeout(
                config.getint('vars', 'guest_agent_timeout'))
            self.channelListener.start()
            self.qga_poller.start()
            self.threadLocal = threading.local()
            self.threadLocal.client = ''

            host = config.get('addresses', 'management_ip')
            port = config.getint('addresses', 'management_port')

            # When IPv6 is not enabled, fallback to listen on IPv4 address
            try:
                self._createAcceptor(host, port)
            except socket.error as e:
                if e.errno == errno.EAFNOSUPPORT and host in ('::', '::1'):
                    fallback_host = '0.0.0.0'
                    self._createAcceptor(fallback_host, port)
                else:
                    raise

            self._prepareHttpServer()
            self._prepareJSONRPCServer()
            self._connectToBroker()
        except:
            self.log.error('failed to init clientIF, '
                           'shutting down storage dispatcher')
            if self.irs:
                self.irs.prepareForShutdown()
            raise

    def getVMs(self):
        """
        Get a snapshot of the currently registered VMs.
        Return value will be a dict of {vmUUID: VM_object}
        """
        with self.vm_container_lock:
            return self.vmContainer.copy()

    def pop_unknown_vm_ids(self):
        """
        Return iterable of unknown VM ids that were spotted.
        Only VM ids spotted since the last call of this method or since
        creation of this instance (in case this method hasn't been called yet)
        are returned.

        This is intended to serve for detection of external VMs.
        """
        with self.vm_container_lock:
            unknown_vm_ids = [
                vm_id for vm_id in self._unknown_vm_ids
                if vm_id not in self.vmContainer
            ]
            self._unknown_vm_ids = set()
        return unknown_vm_ids

    def add_unknown_vm_id(self, vm_id):
        """
        Add `vm_id` to the set of unknown VM ids.

        :param vm_id: VM id to add
        :type vm_id: basestring
        """
        with self.vm_container_lock:
            self._unknown_vm_ids.add(vm_id)

    @property
    def ready(self):
        return (self.irs is None or self.irs.ready) and not self._recovery

    def notify(self, event_id, params=None):
        """
        Send notification using provided subscription id as
        event_id and a dictionary as event body. Before sending
        there is notify_time added on top level to the dictionary.

        Please consult event-schema.yml in order to build an appropriate event.
        https://github.com/oVirt/vdsm/blob/master/lib/api/vdsm-events.yml

        Args:
            event_id (string): unique event name
            params (dict): event content
        """
        if not params:
            params = {}

        if not self.ready:
            self.log.warning('Not ready yet, ignoring event %r args=%r',
                             event_id, params)
            return

        json_binding = self.servers['jsonrpc']

        def _send_notification(message):
            json_binding.reactor.server.send(
                message, config.get('addresses', 'event_queue'))

        try:
            notification = Notification(event_id, _send_notification,
                                        json_binding.bridge.event_schema)
            notification.emit(params)
            self.log.debug("Sending notification %s with params %s ", event_id,
                           params)
        except KeyError:
            self.log.warning("Attempt to send an event when jsonrpc binding"
                             " not available")

    def contEIOVms(self, sdUUID, isDomainStateValid):
        # This method is called everytime the onDomainStateChange
        # event is emitted, this event is emitted even when a domain goes
        # INVALID if this happens there is nothing to do
        if not isDomainStateValid:
            return

        libvirtCon = libvirtconnection.get()
        libvirtVms = libvirtCon.listAllDomains(
            libvirt.VIR_CONNECT_LIST_DOMAINS_PAUSED)

        with self.vm_start_stop_lock:
            self.log.info("vm_start_stop_lock acquired")
            for libvirtVm in libvirtVms:
                state = libvirtVm.state(0)
                if state[1] == libvirt.VIR_DOMAIN_PAUSED_IOERROR:
                    vmId = libvirtVm.UUIDString()
                    vmObj = self.vmContainer[vmId]
                    if sdUUID in vmObj.sdIds:
                        self.log.info("Trying to resume VM %s after EIO", vmId)
                        try:
                            vmObj.maybe_resume()
                        except DestroyedOnResumeError:
                            pass

    @classmethod
    def getInstance(cls, irs=None, log=None, scheduler=None) -> 'clientIF':
        with cls._instanceLock:
            if cls._instance is None:
                if log is None:
                    raise Exception("Logging facility is required to create "
                                    "the single clientIF instance")
                else:
                    cls._instance = clientIF(irs, log, scheduler)
        return cls._instance

    def _createAcceptor(self, host, port):
        sslctx = sslutils.create_ssl_context()
        self._reactor = Reactor()

        self._acceptor = MultiProtocolAcceptor(self._reactor, host, port,
                                               sslctx)

    def _connectToBroker(self):
        if config.getboolean('vars', 'broker_enable'):
            broker_address = config.get('addresses', 'broker_address')
            broker_port = config.getint('addresses', 'broker_port')
            request_queues = config.get('addresses', 'request_queues')

            sslctx = sslutils.create_ssl_context()
            sock = socket.socket()
            sock.connect((broker_address, broker_port))
            if sslctx:
                sock = sslctx.wrapSocket(sock)

            self._broker_client = StompClient(sock, self._reactor)
            for destination in request_queues.split(","):
                self._subscriptions[destination] = StompRpcServer(
                    self.servers['jsonrpc'].server, self._broker_client,
                    destination, broker_address,
                    config.getint('vars', 'connection_stats_timeout'), self)

    def _prepareHttpServer(self):
        if config.getboolean('vars', 'http_enable'):
            try:
                from vdsm.rpc.http import Server
                from vdsm.rpc.http import HttpDetector
            except ImportError:
                self.log.error('Unable to load the http server module. '
                               'Please make sure it is installed.')
            else:
                http_server = Server(self, self.log)
                self.servers['http'] = http_server
                http_detector = HttpDetector(http_server)
                self._acceptor.add_detector(http_detector)

    def _prepareJSONRPCServer(self):
        if config.getboolean('vars', 'jsonrpc_enable'):
            try:
                from vdsm.rpc import Bridge
                from vdsm.rpc.bindingjsonrpc import BindingJsonRpc
                from yajsonrpc.stompserver import StompDetector
            except ImportError:
                self.log.warn('Unable to load the json rpc server module. '
                              'Please make sure it is installed.')
            else:
                bridge = Bridge.DynamicBridge()
                json_binding = BindingJsonRpc(
                    bridge, self._subscriptions,
                    config.getint('vars', 'connection_stats_timeout'),
                    self._scheduler, self)
                self.servers['jsonrpc'] = json_binding
                stomp_detector = StompDetector(json_binding)
                self._acceptor.add_detector(stomp_detector)

    def _wait_for_shutting_down_vms(self):
        """
        Wait loop checking remaining VMs in vm container

        This method is helper method that highers the
        probability of engine to properly acknowledge
        that all VMs are terminated by host shutdown.

        The VMs are shutdown by external service: libvirt-guests
        The service pauses system shutdown on systemd shutdown
        and gracefully shutdowns the running VMs.

        This method applies only when the host is in shutdown.
        If the host is running, the method ends immediately.
        """
        # how long to wait before release shutdown
        # we are waiting in whole seconds
        # if config is not present, do not wait
        timeout = config.getint('vars', 'timeout_engine_clear_vms')
        # time to wait in the final phase in seconds
        # it allows host to flush its final state to the engine
        final_wait = 2

        if not host_in_shutdown():
            return

        self.log.info('host in shutdown waiting')

        for _ in range((timeout - final_wait) * 10):
            if not self.vmContainer:
                # once all VMs are cleared exit
                break
            time.sleep(0.1)

        time.sleep(final_wait)

    def prepareForShutdown(self):
        """
        Prepare server for shutdown.

        Should be called before taking server down.
        """
        if not self._shutdownSemaphore.acquire(blocking=False):
            self.log.debug('cannot run prepareForShutdown concurrently')
            return errCode['unavail']
        try:
            if not self._enabled:
                self.log.debug('cannot run prepareForShutdown twice')
                return errCode['unavail']

            self._wait_for_shutting_down_vms()

            self._acceptor.stop()
            for binding in self.servers.values():
                binding.stop()
            self._reactor.stop()

            self._enabled = False
            secret.clear()
            self.channelListener.stop()
            self.qga_poller.stop()
            if self.irs:
                return self.irs.prepareForShutdown()
            else:
                return {'status': doneCode}
        finally:
            self._shutdownSemaphore.release()

    def start(self):
        for binding in self.servers.values():
            binding.start()
        self.thread = concurrent.thread(self._reactor.process_requests,
                                        name='Reactor thread')
        self.thread.start()

    def prepareVolumePath(self, drive, vmId=None, path=None):
        """
        :param drive: the drive to prepare path for
        :type drive: dict, string or None
        :param vmId: VM UUID
        :type vmId: string or None
        :param path: defines payload path for devices providing
            payload; if omitted and `drive` is a payload device then
            the path will be generated
        :type path: string or None
        """
        if type(drive) is dict:
            device = drive['device']
            # PDIV drive format
            # Since version 4.2 cdrom may use a PDIV format
            if device in ("cdrom", "disk") and isVdsmImage(drive):
                res = self.irs.prepareImage(drive['domainID'], drive['poolID'],
                                            drive['imageID'],
                                            drive['volumeID'])

                if res['status']['code']:
                    raise vm.VolumeError(drive)

                # The order of imgVolumesInfo is not guaranteed
                drive['volumeChain'] = res['imgVolumesInfo']
                drive['volumeInfo'] = res['info']

                if drive.get('diskType') == DISK_TYPE.NETWORK:
                    if device == "cdrom":
                        raise exception.UnsupportedOperation(
                            "A cdrom device is not supported as network disk",
                            drive=drive)

                    # Not applicable for Ceph network disk as
                    # Ceph disks are not vdsm images
                    volPath = self._prepare_network_drive(drive, res)
                else:
                    if 'diskType' not in drive:
                        if res['info']['type'] == DISK_TYPE.BLOCK:
                            drive['diskType'] = DISK_TYPE.BLOCK
                        else:
                            # Volume type may be 'network', but if engine did
                            # not speicfy the type, we must use 'file'.
                            drive['diskType'] = DISK_TYPE.FILE
                    volPath = res['path']
            # GUID drive format
            elif "GUID" in drive:
                res = self.irs.getDevicesVisibility([drive["GUID"]])
                if not res["visible"][drive["GUID"]]:
                    raise vm.VolumeError("Drive %r not visible" %
                                         drive["GUID"])

                # Managed drives are prepared in ManagedVolume.attach_volume
                if drive.get("managed", False):
                    volPath = '/dev/mapper/' + drive["GUID"]
                else:
                    res = self.irs.appropriateDevice(drive["GUID"], vmId,
                                                     'mpath')
                    if res['status']['code']:
                        raise vm.VolumeError("Cannot appropriate drive %r" %
                                             drive["GUID"])

                    # Update size for LUN volume
                    drive["truesize"] = res['truesize']
                    drive["apparentsize"] = res['apparentsize']

                    if 'diskType' not in drive:
                        drive['diskType'] = DISK_TYPE.BLOCK

                    volPath = res['path']

            elif "RBD" in drive:
                volPath = drive["RBD"]

            # cdrom and floppy drives
            elif (device in ('cdrom', 'floppy') and 'specParams' in drive):
                params = drive['specParams']
                if 'vmPayload' in params:
                    volPath = self._prepareVolumePathFromPayload(
                        vmId, device, params['vmPayload'], path)
                # next line can be removed in future, when < 3.3 engine
                # is not supported
                elif (params.get('path', '') == ''
                      and drive.get('path', '') == ''):
                    volPath = ''
                else:
                    volPath = drive.get('path', '')

            elif "path" in drive:
                volPath = drive['path']

            else:
                raise vm.VolumeError(drive)

            # Noramalize the missing diskType when cluster version < 4.2.
            if 'diskType' not in drive:
                drive['diskType'] = DISK_TYPE.FILE

        # For BC sake: None as argument
        elif not drive:
            volPath = drive

        #  For BC sake: path as a string.
        elif os.path.exists(drive):
            volPath = drive

        else:
            raise vm.VolumeError(drive)

        self.log.info("prepared volume path: %s", volPath)
        return volPath

    def _prepareVolumePathFromPayload(self, vmId, device, payload, path):
        """
        :param vmId: VM UUID or None
        :param device: either 'floppy' or 'cdrom'
        :param payload: a dict formed like this:
            {'volId': 'volume id',   # volId is optional
             'file': {'filename': 'content', ...}}
        :param path: payload path as a string; if not given, it will
           be generated
        """
        funcs = {'cdrom': 'mkIsoFs', 'floppy': 'mkFloppyFs'}
        if device not in funcs:
            raise vm.VolumeError("Unsupported 'device': %s" % device)
        func = getattr(supervdsm.getProxy(), funcs[device])
        return func(vmId, payload['file'], payload.get('volId'), path=path)

    def teardownVolumePath(self, drive):
        res = {'status': doneCode}
        try:
            if isVdsmImage(drive):
                res = self.irs.teardownImage(drive['domainID'],
                                             drive['poolID'], drive['imageID'])
        except TypeError:
            # paths (strings) are not deactivated
            if not isinstance(drive, six.string_types):
                self.log.warning("Drive is not a vdsm image: %s",
                                 drive,
                                 exc_info=True)

        return res['status']['code']

    def getDiskAlignment(self, drive):
        """
        Returns the alignment of the disk partitions

        param drive:
        is either {"poolID": , "domainID": , "imageID": , "volumeID": }
        or {"GUID": }

        Return type: a dictionary with partition names as keys and
        True for aligned partitions and False for unaligned as values
        """
        aligning = {}
        volPath = self.prepareVolumePath(drive)
        try:
            out = alignmentScan.scanImage(volPath)
            for line in out:
                aligning[line.partitionName] = line.alignmentScanResult
        finally:
            self.teardownVolumePath(drive)

        return {'status': doneCode, 'alignment': aligning}

    def createVm(self, vmParams, vmRecover=False):
        with self.vm_start_stop_lock:
            if not vmRecover:
                if vmParams['vmId'] in self.vmContainer:
                    return errCode['exist']
            vm = Vm(self, vmParams, vmRecover)
            ret = vm.run()
            if not response.is_error(ret):
                with self.vm_container_lock:
                    self.vmContainer[vm.id] = vm
            return ret

    def getAllVmStats(self):
        return [v.getStats() for v in self.getVMs().values()]

    def getAllVmIoTunePolicies(self):
        vm_io_tune_policies = {}
        for v in self.getVMs().values():
            info = v.io_tune_policy_values()
            if info:
                vm_io_tune_policies[v.id] = info
        return vm_io_tune_policies

    def createStompClient(self, client_socket):
        if 'jsonrpc' in self.servers:
            json_binding = self.servers['jsonrpc']
            reactor = json_binding.reactor
            return reactor.createClient(client_socket)
        else:
            raise JsonRpcBindingsError()

    def _recoverThread(self):
        # Trying to run recover process until it works. During that time vdsm
        # stays in recovery mode (_recover=True), means all api requests
        # returns with "vdsm is in initializing process" message.
        function.retry(self._recoverExistingVms, sleep=5)

    def _recoverExistingVms(self):
        start_time = vdsm.common.time.monotonic_time()
        try:
            self.log.debug('recovery: started')

            # Starting up libvirt might take long when host under high load,
            # we prefer running this code in external thread to avoid blocking
            # API response.
            mog = min(config.getint('vars', 'max_outgoing_migrations'),
                      numa.cpu_topology().cores)
            migration.SourceThread.ongoingMigrations.bound = mog

            recovery.all_domains(self)

            # recover stage 3: waiting for domains to go up
            self._waitForDomainsUp()

            self._recovery = False

            # Now if we have VMs to restore we should wait pool connection
            # and then prepare all volumes.
            # Actually, we need it just to get the resources for future
            # volumes manipulations
            self._waitForStoragePool()

            self._preparePathsForRecoveredVMs()

            self.log.info('recovery: completed in %is',
                          vdsm.common.time.monotonic_time() - start_time)

        except:
            self.log.exception("recovery: failed")
            raise

    def lookup_vm_from_event(self, dom, *args):
        eventid = args[-1]
        vmid = dom.UUIDString()
        v = self.vmContainer.get(vmid)

        if v is None:
            self.log.debug('unknown vm %s event %s args %s', vmid,
                           events.event_name(eventid), args)

            if (eventid != libvirt.VIR_DOMAIN_EVENT_ID_LIFECYCLE
                    or args[0] != libvirt.VIR_DOMAIN_EVENT_UNDEFINED):
                self._unknown_vm_ids.add(vmid)

        return eventid, v

    def dispatchLibvirtEvents(self, conn, dom, *args):
        eventid, v = self.lookup_vm_from_event(dom, *args)
        if v is None:
            return

        try:
            # pylint cannot tell that unpacking the args tuple is safe, so we
            # must disbale this check here.
            # TODO: The real solution is to create a method per callback with
            # fixed number of arguments, and register the callbacks separately
            # in libvirt.
            # pylint: disable=unbalanced-tuple-unpacking

            if eventid == libvirt.VIR_DOMAIN_EVENT_ID_LIFECYCLE:
                event, detail = args[:-1]
                v.onLibvirtLifecycleEvent(event, detail, None)
            elif eventid == libvirt.VIR_DOMAIN_EVENT_ID_REBOOT:
                v.onReboot()
            elif eventid == libvirt.VIR_DOMAIN_EVENT_ID_RTC_CHANGE:
                utcoffset, = args[:-1]
                v.onRTCUpdate(utcoffset)
            elif eventid == libvirt.VIR_DOMAIN_EVENT_ID_IO_ERROR_REASON:
                srcPath, devAlias, action, reason = args[:-1]
                v.onIOError(devAlias, reason, action)
            elif eventid == libvirt.VIR_DOMAIN_EVENT_ID_GRAPHICS:
                phase, localAddr, remoteAddr, authScheme, subject = args[:-1]
                v.log.debug(
                    'graphics event phase '
                    '%s localAddr %s remoteAddr %s'
                    'authScheme %s subject %s', phase, localAddr, remoteAddr,
                    authScheme, subject)
                if phase == libvirt.VIR_DOMAIN_EVENT_GRAPHICS_INITIALIZE:
                    v.onConnect(remoteAddr['node'], remoteAddr['service'])
                elif phase == libvirt.VIR_DOMAIN_EVENT_GRAPHICS_DISCONNECT:
                    v.onDisconnect(clientIp=remoteAddr['node'],
                                   clientPort=remoteAddr['service'])
            elif eventid == libvirt.VIR_DOMAIN_EVENT_ID_WATCHDOG:
                action, = args[:-1]
                v.onWatchdogEvent(action)
            elif eventid == libvirt.VIR_DOMAIN_EVENT_ID_JOB_COMPLETED:
                v.onJobCompleted(args)
            elif eventid == libvirt.VIR_DOMAIN_EVENT_ID_DEVICE_REMOVED:
                device_alias, = args[:-1]
                v.onDeviceRemoved(device_alias)
            elif eventid == libvirt.VIR_DOMAIN_EVENT_ID_BLOCK_THRESHOLD:
                dev, path, threshold, excess = args[:-1]
                v.volume_monitor.on_block_threshold(dev, path, threshold,
                                                    excess)
            elif eventid == libvirt.VIR_DOMAIN_EVENT_ID_BLOCK_JOB_2:
                drive, job_type, job_status, _ = args
                v.on_block_job_event(drive, job_type, job_status)
            elif eventid == libvirt.VIR_DOMAIN_EVENT_ID_AGENT_LIFECYCLE:
                vmid = dom.UUIDString()
                state, reason, _ = args
                self.qga_poller.channel_state_changed(vmid, state, reason)
            else:
                v.log.debug('unhandled libvirt event (event_name=%s, args=%s)',
                            events.event_name(eventid), args)

        except:
            self.log.error("Error running VM callback", exc_info=True)

    def _waitForDomainsUp(self):
        while self._enabled:
            launching = sum(
                int(v.lastStatus == vmstatus.WAIT_FOR_LAUNCH)
                for v in self.getVMs().values())
            if not launching:
                break
            else:
                self.log.info('recovery: waiting for %d domains to go up',
                              launching)
            time.sleep(1)

    def _waitForStoragePool(self):
        while (self._enabled and self.vmContainer
               and not self.irs.getConnectedStoragePoolsList()['poollist']):
            self.log.info('recovery: waiting for storage pool to go up')
            time.sleep(5)

    def _preparePathsForRecoveredVMs(self):
        vm_objects = list(self.getVMs().values())
        num_vm_objects = len(vm_objects)
        for idx, vm_obj in enumerate(vm_objects):
            # Let's recover as much VMs as possible
            try:
                # Do not prepare volumes when system goes down
                if self._enabled:
                    self.log.info(
                        'recovery [%d/%d]: preparing paths for'
                        ' domain %s', idx + 1, num_vm_objects, vm_obj.id)
                    vm_obj.preparePaths()
            except:
                self.log.exception("recovery [%d/%d]: failed for vm %s",
                                   idx + 1, num_vm_objects, vm_obj.id)

    def _prepare_network_drive(self, drive, res):
        """
        Fills drive object for network drives with network-specific data.

        Network (gluster) drives have a very special ephemeral runtime
        path specification, and it can't be resolved to a typical storage
        path in runtime. Therefore, we have to replace storage path
        with a VM path.

        So
            /rhev/data-center/mnt/glusterSD/host:vol/sd_id/images/img_id/vol_id
        is replaced with
            vol/sd_id/images/img_id/vol_id

        Arguments:
            drive (dict like): Drive description. Function modifies it
                as a side-effect.
            res (dict): drive description.

        Returns:
            Network friendly drive's path value.
        """
        volinfo = res['info']
        img_dir, _ = os.path.split(volinfo["path"])
        for entry in drive['volumeChain']:
            entry["path"] = os.path.join(img_dir, entry["volumeID"])
        drive['protocol'] = volinfo['protocol']
        # currently, single host is provided due to this bug:
        # https://bugzilla.redhat.com/1465810
        drive['hosts'] = [volinfo['hosts'][0]]
        return volinfo['path']
Exemplo n.º 33
0
class MultiProtocolAcceptor:
    """
    Provides multiple protocol support on a single port.

    MultiProtocolAcceptor binds and listen on a single port. It accepts
    incoming connections and handles handshake if required. Next it peeks
    into the first bytes sent to detect the protocol, and pass the connection
    to the server handling this protocol.

    To support a new protocol, register a detector object using
    add_detector. Protocol detectors must implement this interface:

    class ProtocolDetector(object):
        NAME = "protocol name"

        # How many bytes are needed to detect this protocol
        REQUIRED_SIZE = 6

        def detect(self, data):
            Given first bytes read from the connection, try to detect the
            protocol. Returns True if protocol is detected.

        def handle_dispatcher(self, client_dispatcher, socket_address):
            Called after detect() succeeded. The detector owns the socket and
            is responsible for closing it or changing the implementation.
    """
    log = logging.getLogger("vds.MultiProtocolAcceptor")

    def __init__(
        self,
        host,
        port,
        sslctx=None,
        ssl_hanshake_timeout=SSLHandshakeDispatcher.SSL_HANDSHAKE_TIMEOUT,
    ):
        self._sslctx = sslctx
        self._reactor = Reactor()
        sock = _create_socket(host, port)
        self._host, self._port = sock.getsockname()
        self.log.info("Listening at %s:%d", self._host, self._port)
        self._acceptor = self._reactor.create_dispatcher(
            sock, _AcceptorImpl(self.handle_accept))
        self._acceptor.listen(5)
        self._handlers = []
        self.TIMEOUT = ssl_hanshake_timeout

    def handle_accept(self, client):
        if self._sslctx is None:
            dispatcher = self._reactor.create_dispatcher(client)
            self._register_protocol_detector(dispatcher)
        else:
            dispatcher = SSLHandshakeDispatcher(
                self._sslctx, self._register_protocol_detector, self.TIMEOUT)
            self._reactor.create_dispatcher(client, dispatcher)

    def _register_protocol_detector(self, dispatcher):
        dispatcher.switch_implementation(
            _ProtocolDetector(
                self._handlers,
                self.TIMEOUT,
            ),
        )

        return dispatcher

    @traceback(on=log.name)
    def serve_forever(self):
        self.log.debug("Running")
        self._reactor.process_requests()

    def add_detector(self, detector):
        self.log.debug("Adding detector %s", detector)
        self._handlers.append(detector)

    def stop(self):
        self.log.debug("Stopping Acceptor")
        self._reactor.stop()