Commit d66a6761 authored by Michal 'vorner' Vaner's avatar Michal 'vorner' Vaner
Browse files

Merge #1454

parents 54928776 aa74005f
......@@ -23,19 +23,32 @@ from isc.dns import *
from isc.config.ccsession import *
from import SessionError, SessionTimeout
import isc.util.process
import select
import errno
from isc.log_messages.ddns_messages import *
from optparse import OptionParser, OptionValueError
import os
import os.path
import signal
import socket
logger = isc.log.Logger("ddns")
DATA_PATH = bind10_config.DATA_PATH
SOCKET_FILE = DATA_PATH + '/ddns_socket'
if "B10_FROM_SOURCE" in os.environ:
DATA_PATH = os.environ['B10_FROM_SOURCE'] + "/src/bin/ddns"
if "B10_FROM_BUILD" in os.environ:
if "B10_FROM_SOURCE_LOCALSTATEDIR" in os.environ:
SOCKET_FILE = os.environ["B10_FROM_BUILD"] + "/ddns_socket"
......@@ -65,6 +78,13 @@ class DDNSSession:
'''Initialize a DDNS Session'''
def clear_socket():
Removes the socket file, if it exists.
if os.path.exists(SOCKET_FILE):
class DDNSServer:
def __init__(self, cc_session=None):
......@@ -85,9 +105,17 @@ class DDNSServer:
self._config_data = self._cc.get_full_config()
self._shutdown = False
# List of the session receivers where we get the requests
self._socksession_receivers = {}
self._listen_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
def config_handler(self, new_config):
'''Update config data.'''
# TODO: Handle exceptions and turn them to an error response
# (once we have any configuration)
answer = create_answer(0)
return answer
......@@ -96,6 +124,7 @@ class DDNSServer:
Handle a CC session command, as sent from bindctl or other
BIND 10 modules.
# TODO: Handle exceptions and turn them to an error response
if cmd == "shutdown":
......@@ -125,19 +154,90 @@ class DDNSServer:
def accept(self):
Accept another connection and create the session receiver.
sock = self._listen_socket.accept()
fileno = sock.fileno()
logger.debug(TRACE_BASIC, DDNS_NEW_CONN, fileno,
receiver =
self._socksession_receivers[fileno] = (sock, receiver)
except (socket.error, \
as e:
# These exceptions mean the connection didn't work, but we can
# continue with the rest
logger.error(DDNS_ACCEPT_FAILURE, e)
def handle_request(self, request):
This is the place where the actual DDNS processing is done. Other
methods are either subroutines of this method or methods doing the
uninteresting "accounting" stuff, like accepting socket,
initialization, etc.
It is called with the request being session as received from
SocketSessionReceiver, i.e. tuple
(socket, local_address, remote_address, data).
# TODO: Implement the magic
# TODO: Don't propagate most of the exceptions (like datasrc errors),
# just drop the packet.
def handle_session(self, fileno):
Handle incoming session on the socket with given fileno.
logger.debug(TRACE_BASIC, DDNS_SESSION, fileno)
(socket, receiver) = self._socksession_receivers[fileno]
except as se:
# No matter why this failed, the connection is in unknown, possibly
# broken state. So, we close the socket and remove the receiver.
del self._socksession_receivers[fileno]
logger.warn(DDNS_DROP_CONN, fileno, se)
def run(self):
Get and process all commands sent from cfgmgr or other modules.
This loops waiting for events until self.shutdown() has been called.
cc_fileno = self._cc.get_socket().fileno()
listen_fileno = self._listen_socket.fileno()
while not self._shutdown:
# We do not catch any exceptions here right now, but this would
# be a good place to catch any exceptions that b10-ddns can
# recover from. We currently have no exception hierarchy to
# make such a distinction easily, but once we do, this would
# be the place to catch.
# In this event loop, we propagate most of exceptions, which will
# subsequently kill the process. We expect the handling functions
# to catch their own exceptions which they can recover from
# (malformed packets, lost connections, etc). The rationale behind
# this is they know best which exceptions are recoverable there
# and an exception may be recoverable somewhere, but not elsewhere.
(reads, writes, exceptions) = \[cc_fileno, listen_fileno] +
list(self._socksession_receivers.keys()), [],
except select.error as se:
# In case it is just interrupted, we continue like nothing
# happened
if se.args[0] == errno.EINTR:
(reads, writes, exceptions) = ([], [], [])
for fileno in reads:
if fileno == cc_fileno:
elif fileno == listen_fileno:
......@@ -204,6 +304,7 @@ def main(ddns_server=None):
except Exception as e:
logger.error(DDNS_UNCAUGHT_EXCEPTION, type(e).__name__, str(e))
if '__main__' == __name__:
......@@ -19,6 +19,12 @@
# <topsrcdir>/tools/ to make sure the
# messages are in the correct order.
% DDNS_ACCEPT_FAILURE error accepting a connection: %1
There was a low-level error when we tried to accept an incoming connection
(probably coming from b10-auth). We continue serving on whatever other
connections we already have, but this connection is dropped. The reason
is logged.
% DDNS_CC_SESSION_ERROR error reading from cc channel: %1
There was a problem reading from the command and control channel. The
most likely cause is that the msgq process is not running.
......@@ -32,6 +38,13 @@ configuration manager b10-cfgmgr is not running.
The ddns process encountered an error when installing the configuration at
startup time. Details of the error are included in the log message.
% DDNS_DROP_CONN dropping connection on file descriptor %1 because of error %2
There was an error on a connection with the b10-auth server (or whatever
connects to the ddns daemon). This might be OK, for example when the
authoritative server shuts down, the connection would get closed. It also
can mean the system is busy and can't keep up or that the other side got
confused and sent bad data.
% DDNS_MODULECC_SESSION_ERROR error encountered by configuration/command module: %1
There was a problem in the lower level module handling configuration and
control commands. This could happen for various reasons, but the most likely
......@@ -39,6 +52,12 @@ cause is that the configuration database contains a syntax error and ddns
failed to start at initialization. A detailed error message from the module
will also be displayed.
% DDNS_NEW_CONN new connection on file descriptor %1 from %2
Debug message. We received a connection and we are going to start handling
requests from it. The file descriptor number and the address where the request
comes from is logged. The connection is over a unix domain socket and is likely
coming from a b10-auth process.
% DDNS_RECEIVED_SHUTDOWN_COMMAND shutdown command received
The ddns process received a shutdown command from the command channel
and will now shut down.
......@@ -47,6 +66,11 @@ and will now shut down.
The ddns process has successfully started and is now ready to receive commands
and updates.
% DDNS_SESSION session arrived on file descriptor %1
A debug message, informing there's some activity on the given file descriptor.
It will be either a request or the file descriptor will be closed. See
following log messages to see what of it.
% DDNS_SHUTDOWN ddns server shutting down
The ddns process is shutting down. It will no longer listen for new commands
or updates. Any command or update that is being addressed at this moment will
......@@ -21,6 +21,7 @@ endif
for pytest in $(PYTESTS) ; do \
echo Running test: $$pytest ; \
B10_FROM_SOURCE=$(abs_top_srcdir) \
B10_FROM_BUILD=$(abs_top_builddir) \
PYTHONPATH=$(COMMON_PYTHON_PATH):$(abs_top_builddir)/src/bin/ddns:$(abs_top_builddir)/src/lib/dns/python/.libs:$(abs_top_builddir)/src/lib/util/io/.libs \
TESTDATASRCDIR=$(abs_srcdir)/testdata/ \
......@@ -19,6 +19,37 @@ import unittest
import isc
import ddns
import isc.config
import select
import errno
import socket
import os.path
class FakeSocket:
A fake socket. It only provides a file number, peer name and accept method.
def __init__(self, fileno):
self.__fileno = fileno
def fileno(self):
return self.__fileno
def getpeername(self):
return "fake_unix_socket"
def accept(self):
return FakeSocket(self.__fileno + 1)
class FakeSessionReceiver:
A fake socket session receiver, for our tests.
def __init__(self, socket):
self._socket = socket
def socket(self):
This method is not present in the real receiver, but we use it to
inspect the socket passed to the constructor.
return self._socket
class MyCCSession(isc.config.ConfigData):
'''Fake session with minimal interface compliance'''
......@@ -32,6 +63,12 @@ class MyCCSession(isc.config.ConfigData):
'''Called by DDNSServer initialization, but not used in tests'''
self._started = True
def get_socket(self):
Used to get the file number for select.
return FakeSocket(1)
class MyDDNSServer():
'''Fake DDNS server used to test the main() function'''
def __init__(self):
......@@ -63,7 +100,41 @@ class TestDDNSServer(unittest.TestCase):
cc_session = MyCCSession()
self.ddns_server = ddns.DDNSServer(cc_session)
self.__cc_session = cc_session
self.__select_expected = None
self.__select_answer = None
self.__select_exception = None
self.__hook_called = False
self.ddns_server._listen_socket = FakeSocket(2) = self.__select
def tearDown(self): = = \
def test_listen(self):
Test the old socket file is removed (if any) and a new socket
is created when the ddns server is created.
# Make sure the socket does not exist now
# Hook the call for clearing the socket
orig_clear = ddns.clear_socket
ddns.clear_socket = self.__hook
# Create the server
ddnss = ddns.DDNSServer(MyCCSession())
ddns.clear_socket = orig_clear
# The socket is created
self.assertTrue(isinstance(ddnss._listen_socket, socket.socket))
# And deletion of the socket was requested
# Now make sure the clear_socket really works
def test_config_handler(self):
# Config handler does not do anything yet, but should at least
......@@ -93,14 +164,215 @@ class TestDDNSServer(unittest.TestCase):
signal_handler(None, None)
def __select(self, reads, writes, exceptions, timeout=None):
A fake select. It checks it was called with the correct parameters and
returns a preset answer.
If there's an exception stored in __select_exception, it is raised
instead and the exception is cleared.
self.assertEqual(self.__select_expected, (reads, writes, exceptions,
if self.__select_exception is not None:
(self.__select_exception, exception) = (None,
raise exception
answer = self.__select_answer
self.__select_answer = None
self.ddns_server._shutdown = True
return answer
def __hook(self, param=None):
A hook that can be installed to any nullary or unary function and see
if it was really called.
self.__hook_called = param
def test_accept_called(self):
Test we call the accept function when a new connection comes.
self.ddns_server.accept = self.__hook
self.__select_expected = ([1, 2], [], [], None)
self.__select_answer = ([2], [], [])
self.__hook_called = "Not called"
# The answer got used
# Reset, when called without parameter
def test_check_command_called(self):
Test the check_command is called when there's something on the
self.__cc_session.check_command = self.__hook
self.__select_expected = ([1, 2], [], [], None)
self.__select_answer = ([1], [], [])
# The answer got used
# And the check_command was called with true parameter (eg.
# non-blocking)
def test_accept(self):
Test that we can accept a new connection.
# There's nothing before the accept = \
self.assertEqual({}, self.ddns_server._socksession_receivers)
# Now the new socket session receiver is stored in the dict
# The 3 comes from _listen_socket.accept() - _listen_socket has
# fileno 2 and accept returns socket with fileno increased by one.
(socket, receiver) = self.ddns_server._socksession_receivers[3]
self.assertTrue(isinstance(socket, FakeSocket))
self.assertEqual(3, socket.fileno())
self.assertTrue(isinstance(receiver, FakeSessionReceiver))
self.assertEqual(socket, receiver.socket())
def test_accept_fail(self):
Test we don't crash if an accept fails and that we don't modify the
# Make the accept fail
def accept_failure():
raise socket.error(errno.ECONNABORTED)
orig = self.ddns_server._listen_socket.accept
self.ddns_server._listen_socket.accept = accept_failure
self.assertEqual({}, self.ddns_server._socksession_receivers)
# Doesn't raise the exception
# And nothing is stored
self.assertEqual({}, self.ddns_server._socksession_receivers)
# Now make the socket receiver fail
self.ddns_server._listen_socket.accept = orig
def receiver_failure(sock):
raise'Test error') = \
# Doesn't raise the exception
# And nothing is stored
self.assertEqual({}, self.ddns_server._socksession_receivers)
# Check we don't catch everything, so raise just an exception
def unexpected_failure(sock):
raise Exception('Test error') = \
# This one gets through
self.assertRaises(Exception, self.ddns_server.accept)
# Nothing is stored as well
self.assertEqual({}, self.ddns_server._socksession_receivers)
def test_session_called(self):
Test the run calls handle_session when there's something on the
socket = FakeSocket(3)
self.ddns_server._socksession_receivers = \
{3: (socket, FakeSessionReceiver(socket))}
self.ddns_server.handle_session = self.__hook
self.__select_expected = ([1, 2, 3], [], [], None)
self.__select_answer = ([3], [], [])
self.assertEqual(3, self.__hook_called)
def test_handle_session_ok(self):
Test the handle_session pops the receiver and calls handle_request
when everything is OK.
socket = FakeSocket(3)
receiver = FakeSessionReceiver(socket)
# It doesn't really matter what data we use here, it is only passed
# through the code
param = (FakeSocket(4), ('', 1234), ('', 1235),
'Some data')
def pop():
return param
# Prepare data into the receiver
receiver.pop = pop
self.ddns_server._socksession_receivers = {3: (socket, receiver)}
self.ddns_server.handle_request = self.__hook
# Call it
# The popped data are passed into the handle_request
self.assertEqual(param, self.__hook_called)
# The receivers are kept the same
self.assertEqual({3: (socket, receiver)},
def test_handle_session_fail(self):
Test the handle_session removes (and closes) the socket and receiver
when the receiver complains.
socket = FakeSocket(3)
receiver = FakeSessionReceiver(socket)
def pop():
raise'Test error')
receiver.pop = pop
socket.close = self.__hook
self.__hook_called = False
self.ddns_server._socksession_receivers = {3: (socket, receiver)}
# The "dead" receiver is removed
self.assertEqual({}, self.ddns_server._socksession_receivers)
# Close is called with no parameter, so the default None
def test_select_exception_ignored(self):
Test that the EINTR is ignored in select.
# Prepare the EINTR exception
self.__select_exception = select.error(errno.EINTR)
# We reuse the test here, as it should act the same. The exception
# should just get ignored.
def test_select_exception_fatal(self):
Test that other exceptions are fatal to the run.
# Prepare a different exception
self.__select_exception = select.error(errno.EBADF)
self.__select_expected = ([1, 2], [], [], None)
class TestMain(unittest.TestCase):
def setUp(self):
self._server = MyDDNSServer()
self.__orig_clear = ddns.clear_socket
ddns.clear_socket = self.__clear_socket
self.__clear_called = False
def tearDown(self):
ddns.clear_socket = self.__orig_clear
def test_main(self):
def __clear_socket(self):
self.__clear_called = True
def check_exception(self, ex):
'''Common test sequence to see if the given exception is caused.
......@@ -135,7 +407,6 @@ class TestMain(unittest.TestCase):
self.assertRaises(BaseException, ddns.main, self._server)
if __name__== "__main__":
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment