BIND 10 master, updated. d66a6761de5dfc6adea4247044bfde7d8db0ecb3 Merge #1454
BIND 10 source code commits
bind10-changes at lists.isc.org
Wed Jan 25 10:06:13 UTC 2012
The branch, master has been updated
via d66a6761de5dfc6adea4247044bfde7d8db0ecb3 (commit)
via aa74005fd2e1c6332dfad4a5a2f397d40dd4660a (commit)
via af944b95fca98047398f0a3b43c24caecdee450e (commit)
via c9a7d521f00d4ed6252a67fd593af5959f027e8c (commit)
via 11d920b3d0dcb91ce84835d555df912a7da29d64 (commit)
via df7eb5eab53ef88f7cfe704aaa72fc1218cb7f95 (commit)
via b8fec5c4dd4b0280dd393c10718f516217435efe (commit)
via 4209099e4baeab29615cc28208f1ca8889ca71ad (commit)
via 6f19208c5954d030c0b9795fe399c4ef1e11c8c4 (commit)
via fb20b8bb0c4ed9445a2a97504fdfd201ff15df38 (commit)
via 0458ade871c5d0c1dfced7970379965a8b9443c5 (commit)
via 0ee2267ab384059dde33817da15395806f45433f (commit)
via 25768c5398397bb271e0da9d8be89f545feb620e (commit)
via a842f40cddc919f5bb0aa68943fd7ef7b8938a04 (commit)
via b6aa3ff5422ec34da9c413aabff15145d86596e6 (commit)
via 9af13dfc788b7ddf5667545aa483ced00c349a20 (commit)
via e7b1c03c8c93645f923e842f4af89bdaf0a59576 (commit)
via e1cf741287a9203fbe00b18858cc908f677c449c (commit)
via ba9038e23c411ba6249a5bc38e2da30f7d95fb49 (commit)
from 54928776a98b487264994fa61221e4448b5ece79 (commit)
Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.
- Log -----------------------------------------------------------------
-----------------------------------------------------------------------
Summary of changes:
src/bin/ddns/ddns.py.in | 113 +++++++++++++++-
src/bin/ddns/ddns_messages.mes | 24 ++++
src/bin/ddns/tests/Makefile.am | 1 +
src/bin/ddns/tests/ddns_test.py | 273 ++++++++++++++++++++++++++++++++++++++-
4 files changed, 404 insertions(+), 7 deletions(-)
-----------------------------------------------------------------------
diff --git a/src/bin/ddns/ddns.py.in b/src/bin/ddns/ddns.py.in
index e5ce31d..1385528 100755
--- a/src/bin/ddns/ddns.py.in
+++ b/src/bin/ddns/ddns.py.in
@@ -23,19 +23,32 @@ from isc.dns import *
from isc.config.ccsession import *
from isc.cc import SessionError, SessionTimeout
import isc.util.process
+import isc.util.io.socketsession
+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
isc.log.init("b10-ddns")
logger = isc.log.Logger("ddns")
+TRACE_BASIC = logger.DBGLVL_TRACE_BASIC
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_SOURCE_LOCALSTATEDIR"] + \
+ "/ddns_socket"
+ else:
+ SOCKET_FILE = os.environ["B10_FROM_BUILD"] + "/ddns_socket"
SPECFILE_LOCATION = DATA_PATH + "/ddns.spec"
@@ -65,6 +78,13 @@ class DDNSSession:
'''Initialize a DDNS Session'''
pass
+def clear_socket():
+ '''
+ Removes the socket file, if it exists.
+ '''
+ if os.path.exists(SOCKET_FILE):
+ os.remove(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._cc.start()
self._shutdown = False
+ # List of the session receivers where we get the requests
+ self._socksession_receivers = {}
+ clear_socket()
+ self._listen_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ self._listen_socket.bind(SOCKET_FILE)
+ self._listen_socket.listen(16)
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":
logger.info(DDNS_RECEIVED_SHUTDOWN_COMMAND)
self.trigger_shutdown()
@@ -125,19 +154,90 @@ class DDNSServer:
'''
pass
+ def accept(self):
+ """
+ Accept another connection and create the session receiver.
+ """
+ try:
+ sock = self._listen_socket.accept()
+ fileno = sock.fileno()
+ logger.debug(TRACE_BASIC, DDNS_NEW_CONN, fileno,
+ sock.getpeername())
+ receiver = isc.util.io.socketsession.SocketSessionReceiver(sock)
+ self._socksession_receivers[fileno] = (sock, receiver)
+ except (socket.error, isc.util.io.socketsession.SocketSessionError) \
+ 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.
+ pass
+
+ 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]
+ try:
+ self.handle_request(receiver.pop())
+ except isc.util.io.socketsession.SocketSessionError 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]
+ socket.close()
+ 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.
'''
logger.info(DDNS_RUNNING)
+ 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.
- self._cc.check_command(False)
+ # 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.
+
+ try:
+ (reads, writes, exceptions) = \
+ select.select([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) = ([], [], [])
+ else:
+ raise
+ for fileno in reads:
+ if fileno == cc_fileno:
+ self._cc.check_command(True)
+ elif fileno == listen_fileno:
+ self.accept()
+ else:
+ self.handle_session(fileno)
self.shutdown_cleanup()
logger.info(DDNS_STOPPED)
@@ -204,6 +304,7 @@ def main(ddns_server=None):
logger.error(DDNS_CC_SESSION_TIMEOUT_ERROR)
except Exception as e:
logger.error(DDNS_UNCAUGHT_EXCEPTION, type(e).__name__, str(e))
+ clear_socket()
if '__main__' == __name__:
main()
diff --git a/src/bin/ddns/ddns_messages.mes b/src/bin/ddns/ddns_messages.mes
index 36c6ed1..996e663 100644
--- a/src/bin/ddns/ddns_messages.mes
+++ b/src/bin/ddns/ddns_messages.mes
@@ -19,6 +19,12 @@
# <topsrcdir>/tools/reorder_message_file.py 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
diff --git a/src/bin/ddns/tests/Makefile.am b/src/bin/ddns/tests/Makefile.am
index d8b1c79..cd1082f 100644
--- a/src/bin/ddns/tests/Makefile.am
+++ b/src/bin/ddns/tests/Makefile.am
@@ -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) \
$(LIBRARY_PATH_PLACEHOLDER) \
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/ \
diff --git a/src/bin/ddns/tests/ddns_test.py b/src/bin/ddns/tests/ddns_test.py
index 601c281..27278b4 100755
--- a/src/bin/ddns/tests/ddns_test.py
+++ b/src/bin/ddns/tests/ddns_test.py
@@ -19,6 +19,37 @@ import unittest
import isc
import ddns
import isc.config
+import select
+import errno
+import isc.util.io.socketsession
+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.assertFalse(cc_session._started)
self.ddns_server = ddns.DDNSServer(cc_session)
+ self.__cc_session = cc_session
self.assertTrue(cc_session._started)
+ self.__select_expected = None
+ self.__select_answer = None
+ self.__select_exception = None
+ self.__hook_called = False
+ self.ddns_server._listen_socket = FakeSocket(2)
+ ddns.select.select = self.__select
+
+ def tearDown(self):
+ ddns.select.select = select.select
+ ddns.isc.util.io.socketsession.SocketSessionReceiver = \
+ isc.util.io.socketsession.SocketSessionReceiver
+
+ 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
+ ddns.clear_socket()
+ # 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(os.path.exists(ddns.SOCKET_FILE))
+ self.assertTrue(isinstance(ddnss._listen_socket, socket.socket))
+ # And deletion of the socket was requested
+ self.assertIsNone(self.__hook_called)
+ # Now make sure the clear_socket really works
+ ddns.clear_socket()
+ self.assertFalse(os.path.exists(ddns.SOCKET_FILE))
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)
self.assertTrue(self.ddns_server._shutdown)
+ 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,
+ timeout))
+ if self.__select_exception is not None:
+ (self.__select_exception, exception) = (None,
+ self.__select_exception)
+ 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"
+ self.ddns_server.run()
+ self.assertTrue(self.ddns_server._shutdown)
+ # The answer got used
+ self.assertIsNone(self.__select_answer)
+ # Reset, when called without parameter
+ self.assertIsNone(self.__hook_called)
+
+ def test_check_command_called(self):
+ """
+ Test the check_command is called when there's something on the
+ socket.
+ """
+ self.__cc_session.check_command = self.__hook
+ self.__select_expected = ([1, 2], [], [], None)
+ self.__select_answer = ([1], [], [])
+ self.ddns_server.run()
+ self.assertTrue(self.ddns_server._shutdown)
+ # The answer got used
+ self.assertIsNone(self.__select_answer)
+ # And the check_command was called with true parameter (eg.
+ # non-blocking)
+ self.assertTrue(self.__hook_called)
+
+ def test_accept(self):
+ """
+ Test that we can accept a new connection.
+ """
+ # There's nothing before the accept
+ ddns.isc.util.io.socketsession.SocketSessionReceiver = \
+ FakeSessionReceiver
+ self.assertEqual({}, self.ddns_server._socksession_receivers)
+ self.ddns_server.accept()
+ # 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.
+ self.assertEqual([3],
+ list(self.ddns_server._socksession_receivers.keys()))
+ (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
+ internals.
+ """
+ # 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
+ self.ddns_server.accept()
+ # 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 isc.util.io.socketsession.SocketSessionError('Test error')
+ ddns.isc.util.io.socketsession.SocketSessionReceiver = \
+ receiver_failure
+ # Doesn't raise the exception
+ self.ddns_server.accept()
+ # 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')
+ ddns.isc.util.io.socketsession.SocketSessionReceiver = \
+ unexpected_failure
+ # 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.
+ """
+ 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.ddns_server.run()
+ self.assertTrue(self.ddns_server._shutdown)
+ self.assertIsNone(self.__select_answer)
+ 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), ('127.0.0.1', 1234), ('127.0.0.1', 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
+ self.ddns_server.handle_session(3)
+ # 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)},
+ self.ddns_server._socksession_receivers)
+
+ 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 isc.util.io.socketsession.SocketSessionError('Test error')
+ receiver.pop = pop
+ socket.close = self.__hook
+ self.__hook_called = False
+ self.ddns_server._socksession_receivers = {3: (socket, receiver)}
+ self.ddns_server.handle_session(3)
+ # The "dead" receiver is removed
+ self.assertEqual({}, self.ddns_server._socksession_receivers)
+ # Close is called with no parameter, so the default None
+ self.assertIsNone(self.__hook_called)
+
+ 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.
+ self.test_check_command_called()
+
+ 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)
+ self.assertRaises(select.error, self.ddns_server.run)
+
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):
self.assertFalse(self._server.run_called)
ddns.main(self._server)
self.assertTrue(self._server.run_called)
+ self.assertTrue(self.__clear_called)
+
+ 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._server.set_exception(BaseException("error"))
self.assertRaises(BaseException, ddns.main, self._server)
self.assertTrue(self._server.exception_raised)
-
if __name__== "__main__":
isc.log.resetUnitTestRootLogger()
More information about the bind10-changes
mailing list