From e92b6e040e967a527216786d4e1e3749a6d86472 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gon=C3=A7alves?= Date: Wed, 14 Oct 2020 18:57:23 +0200 Subject: [PATCH] Allow dockerDNS module to have more interactions with caller. * Replace run function by a class * Allow this class to be startable and stopable * Make tests autonomous * Add Makefile in order to run tests * Modify dockerDNs module version --- Makefile | 2 ++ dockerDNS.py | 39 +++++++++++++++++--- dockerDNS/__init__.py | 6 ++-- dockerDNS/dockerDNS.py | 79 ++++++++++++++++++++++++++++------------- tests/test_dockerDNS.py | 32 +++++++++-------- 5 files changed, 111 insertions(+), 47 deletions(-) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1256637 --- /dev/null +++ b/Makefile @@ -0,0 +1,2 @@ +test: + @python -m twisted.trial tests diff --git a/dockerDNS.py b/dockerDNS.py index 1f1f57b..82549d6 100755 --- a/dockerDNS.py +++ b/dockerDNS.py @@ -7,7 +7,9 @@ import argparse -from dockerDNS import DNS_PORT, LISTEN_ADDRESS, run +import signal +import sys +from dockerDNS import DNS_PORT, LISTEN_ADDRESS, DockerDNS def getForwarders(forwarders=None, listenAddress=LISTEN_ADDRESS): @@ -32,6 +34,30 @@ def getForwarders(forwarders=None, listenAddress=LISTEN_ADDRESS): return forwarders +class DNSHandler(): + """Handle DockerDNS start and stop""" + def __init__(self, port=DNS_PORT, + listenAddress=LISTEN_ADDRESS, + forwarders=None): + self.runner = DockerDNS(port=port, + listenAddress=listenAddress, + forwarders=forwarders) + signal.signal(signal.SIGTERM, self.signal_handler) + signal.signal(signal.SIGINT, self.signal_handler) + + def signal_handler(self, signum, frame): + """Signal handler that can stop the runner""" + self.runner.stop() + + def start(self): + """Runner starter""" + self.runner.start() + + def clean(self): + """Runner cleaner""" + self.runner.clean() + + if __name__ == "__main__": description = "Resolve docker container's name into IPv4 address" parser = argparse.ArgumentParser(description=description) @@ -46,6 +72,11 @@ def getForwarders(forwarders=None, listenAddress=LISTEN_ADDRESS): options = parser.parse_args() forwarders = getForwarders(forwarders=options.forwarders, listenAddress=options.listenAddress) - run(port=options.port, - listenAddress=options.listenAddress, - forwarders=forwarders) + + DNSHandler = DNSHandler(port=options.port, + listenAddress=options.listenAddress, + forwarders=forwarders) + DNSHandler.start() + DNSHandler.clean() + + sys.exit() diff --git a/dockerDNS/__init__.py b/dockerDNS/__init__.py index 70f58bc..57e3828 100755 --- a/dockerDNS/__init__.py +++ b/dockerDNS/__init__.py @@ -1,9 +1,9 @@ -from .dockerDNS import DNS_PORT, LISTEN_ADDRESS, run +from .dockerDNS import DNS_PORT, LISTEN_ADDRESS, DockerDNS __all__ = [ "DNS_PORT", "LISTEN_ADDRESS", - "run" + "DockerDNS" ] -__version__ = 0.2 +__version__ = 0.3 diff --git a/dockerDNS/dockerDNS.py b/dockerDNS/dockerDNS.py index e7b476d..4dc7d3f 100755 --- a/dockerDNS/dockerDNS.py +++ b/dockerDNS/dockerDNS.py @@ -1,17 +1,11 @@ #!/usr/bin/env python3 -""" -Resolve docker container's name into IPv4 address +"""Resolve docker container's name into IPv4 address""" - python3 docker-dns.py -""" - -import os import docker from threading import Thread from twisted.internet import reactor, defer from twisted.names import client, dns, server - LISTEN_ADDRESS = "127.0.0.1" DNS_PORT = 53 @@ -52,15 +46,20 @@ class EventsListener(Thread): def __init__(self, resolver): super().__init__() self.resolver = resolver + self.eventListener = None def run(self): - eventListener = self.resolver.dockerClient.events( + self.eventListener = self.resolver.dockerClient.events( filters={"event": ["start", "die"]}, decode=True) - for e in eventListener: + for e in self.eventListener: callback = getattr(self, e["Action"] + "Callback") callback(e) + def join(self, timeout=None): + self.eventListener.close() + super().join(timeout) + def startCallback(self, event): containerName = event["Actor"]["Attributes"]["name"] api = self.resolver.dockerClient.api @@ -73,19 +72,49 @@ def dieCallback(self, event): self.resolver.removeContainer(containerName) -def run(port=DNS_PORT, listenAddress=LISTEN_ADDRESS, forwarders=None): - """Configure and execute the DNS server.""" - dockerClient = docker.from_env() - resolver = DockerResolver(dockerClient=dockerClient, - servers=forwarders) - eventsListener = EventsListener(resolver) - eventsListener.start() - factory = server.DNSServerFactory(clients=[resolver]) - protocol = dns.DNSDatagramProtocol(controller=factory) - reactor.listenUDP(port=port, protocol=protocol, interface=listenAddress) - reactor.listenTCP(port=port, factory=factory, interface=listenAddress) - reactor.run() - eventsListener.join(1) - # For an unknown reason sys.exit() does not work - # so we use this hack. - os._exit(0) +class DockerDNS(): + """Start and stop DockerDNS Service""" + def __init__(self, port=None, listenAddress=None, forwarders=None): + self.port = port + self.listenAddress = listenAddress + self.forwarders = forwarders + + self.eventsListener = None + + self.udp_listener = None + self.tcp_listener = None + + if self.port is None: + self.port = DNS_PORT + if self.listenAddress is None: + self.listenAddress = LISTEN_ADDRESS + + def start(self): + """Configure and execute the DNS server.""" + dockerClient = docker.from_env() + resolver = DockerResolver(dockerClient=dockerClient, + servers=self.forwarders) + + self.eventsListener = EventsListener(resolver) + self.eventsListener.start() + factory = server.DNSServerFactory(clients=[resolver]) + protocol = dns.DNSDatagramProtocol(controller=factory) + self.udp_listener = reactor.listenUDP(port=self.port, + protocol=protocol, + interface=self.listenAddress) + self.tcp_listener = reactor.listenTCP(port=self.port, + factory=factory, + interface=self.listenAddress) + reactor.run() + + def clean(self): + """Clean all the resources""" + self.stop() + self.eventsListener.join() + + def stop(self): + """Stop the reactor if running""" + if reactor.running: + self.udp_listener.stopListening() + self.tcp_listener.stopListening() + reactor.stop() diff --git a/tests/test_dockerDNS.py b/tests/test_dockerDNS.py index 1584f9b..8e40c3a 100755 --- a/tests/test_dockerDNS.py +++ b/tests/test_dockerDNS.py @@ -3,8 +3,11 @@ import unittest import docker import time +import os +import signal +from multiprocessing import Process +from dockerDNS import DockerDNS from dnslib.dns import DNSRecord -# from dockerDNS import run def resolveDNS(query, server, port): @@ -19,23 +22,22 @@ def resolveDNS(query, server, port): class TestDockerDNS(unittest.TestCase): + def dockerDNSProcess(self): + p = DockerDNS(port=35353, + listenAddress="127.0.0.1", + forwarders="8.8.8.8") + p.start() + p.clean() + def setUp(self): self.dockerClient = docker.from_env() - # FIXME: Tests should be autonomous. - # - # Server cannot be started at this time - # because we have to stop it gracefully - # and the run function does not support - # that actually. - # - # By the way, this mean that we have to - # start the server manually before starting - # tests. - # Keep in mind that we have to use the - # port 35353 in order to start the server - # as a regular user. - # run(35353, "127.0.0.1", "8.8.8.8") + self.process = Process(target=self.dockerDNSProcess) + self.process.start() + + def tearDown(self): + os.kill(self.process.pid, signal.SIGTERM) + self.process.join() def test_basic_dns_request(self): self.dockerClient.containers.run(