Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
Sebastian Schrader
Kea
Commits
d66a6761
Commit
d66a6761
authored
Jan 25, 2012
by
Michal 'vorner' Vaner
Browse files
Merge #1454
parents
54928776
aa74005f
Changes
4
Hide whitespace changes
Inline
Side-by-side
src/bin/ddns/ddns.py.in
View file @
d66a6761
...
...
@@ -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()
src/bin/ddns/ddns_messages.mes
View file @
d66a6761
...
...
@@ -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
...
...
src/bin/ddns/tests/Makefile.am
View file @
d66a6761
...
...
@@ -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/
\
...
...
src/bin/ddns/tests/ddns_test.py
View file @
d66a6761
...
...
@@ -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
()
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment