Commit 0557f306 authored by Mukund Sivaraman's avatar Mukund Sivaraman

Merge branch 'master' into trac2036

parents f57a34bf 33313805
446. [bug] muks
A number of warnings reported by Python about unclosed file and
socket objects were fixed. Some related code was also made safer.
(Trac #1828, git 464682a2180c672f1ed12d8a56fd0a5ab3eb96ed)
445. [bug]* jinmei
The pre-install check for older SQLite3 DB now refers to the DB
file with the prefix of DESTDIR. This ensures that 'make install'
......@@ -16,6 +16,8 @@ DISTCHECK_CONFIGURE_FLAGS = --disable-install-configurations
# Use same --with-gtest flag if set
@if [ $(USE_LCOV) = yes ] ; then \
$(LCOV) --directory . --zerocounters; \
......@@ -1320,6 +1320,7 @@ Developer:
Valgrind Suppressions: $use_valgrind_suppressions
C++ Code Coverage: $USE_LCOV
Python Code Coverage: $USE_PYCOVERAGE
Logger checks: $enable_logger_checks
Generate Manuals: $enable_man
......@@ -579,7 +579,7 @@ INPUT = ../src/lib/exceptions ../src/lib/cc \
../src/lib/testutils ../src/lib/cache ../src/lib/server_common/ \
../src/bin/sockcreator/ ../src/lib/util/ ../src/lib/util/io/ \
../src/lib/resolve ../src/lib/acl ../src/bin/dhcp6 ../src/lib/dhcp \
../src/bin/dhcp4 devel
../src/bin/dhcp4 ../tests/tools/perfdhcp devel
# This tag can be used to specify the character encoding of the source files
# that doxygen parses. Internally doxygen uses the UTF-8 encoding, which is
EXTRA_DIST = bind10-guide.css
EXTRA_DIST += bind10-guide.xml bind10-guide.html bind10-guide.txt
EXTRA_DIST += bind10-messages.xml bind10-messages.html
dist_doc_DATA = bind10-guide.txt
dist_html_DATA = bind10-guide.css bind10-guide.html bind10-messages.html
EXTRA_DIST = bind10-guide.xml bind10-messages.xml
# This is not a "man" manual, but reuse this for now for docbook.
......@@ -131,7 +131,9 @@
and <command>b10-zonemgr</command> components require the
libpython3 library and the Python module
(which is included with Python).
The Python module needs to be built for the corresponding Python 3.
The <command>b10-stats-httpd</command> component uses the
Python module.
The Python modules need to be built for the corresponding Python 3.
<!-- TODO: this will change ... -->
......@@ -1055,22 +1055,29 @@ class TestPIDFile(unittest.TestCase):
# dump PID to the file, and confirm the content is correct
my_pid = os.getpid()
self.assertEqual(my_pid, int(open(self.pid_file, "r").read()))
with open(self.pid_file, "r") as f:
self.assertEqual(my_pid, int(
def test_dump_pid(self):
# make sure any existing content will be removed
open(self.pid_file, "w").write('dummy data\n')
with open(self.pid_file, "w") as f:
f.write('dummy data\n')
def test_unlink_pid_file_notexist(self):
dummy_data = 'dummy_data\n'
open(self.pid_file, "w").write(dummy_data)
with open(self.pid_file, "w") as f:
# the file specified for unlink_pid_file doesn't exist,
# and the original content of the file should be intact.
self.assertEqual(dummy_data, open(self.pid_file, "r").read())
with open(self.pid_file, "r") as f:
def test_dump_pid_with_none(self):
# Check the behavior of dump_pid() and unlink_pid_file() with None.
......@@ -1079,9 +1086,14 @@ class TestPIDFile(unittest.TestCase):
dummy_data = 'dummy_data\n'
open(self.pid_file, "w").write(dummy_data)
with open(self.pid_file, "w") as f:
self.assertEqual(dummy_data, open(self.pid_file, "r").read())
with open(self.pid_file, "r") as f:
def test_dump_pid_failure(self):
# the attempt to open file will fail, which should result in exception.
......@@ -425,6 +425,12 @@ class FakeBindCmdInterpreter(bindcmd.BindCmdInterpreter):
class TestBindCmdInterpreter(unittest.TestCase):
def setUp(self):
self.old_stdout = sys.stdout
def tearDown(self):
sys.stdout = self.old_stdout
def _create_invalid_csv_file(self, csvfilename):
import csv
csvfile = open(csvfilename, 'w')
......@@ -447,19 +453,17 @@ class TestBindCmdInterpreter(unittest.TestCase):
self.assertEqual(new_csv_dir, custom_cmd.csv_file_dir)
def test_get_saved_user_info(self):
old_stdout = sys.stdout
sys.stdout = open(os.devnull, 'w')
cmd = bindcmd.BindCmdInterpreter()
users = cmd._get_saved_user_info('/notexist', 'csv_file.csv')
self.assertEqual([], users)
csvfilename = 'csv_file.csv'
users = cmd._get_saved_user_info('./', csvfilename)
self.assertEqual([], users)
sys.stdout = old_stdout
with open(os.devnull, 'w') as f:
sys.stdout = f
cmd = bindcmd.BindCmdInterpreter()
users = cmd._get_saved_user_info('/notexist', 'csv_file.csv')
self.assertEqual([], users)
csvfilename = 'csv_file.csv'
users = cmd._get_saved_user_info('./', csvfilename)
self.assertEqual([], users)
class TestCommandLineOptions(unittest.TestCase):
def setUp(self):
......@@ -84,6 +84,7 @@ class TestSecureHTTPRequestHandler(unittest.TestCase):
self.handler.rfile = open("check.tmp", 'w+b')
def tearDown(self):
sys.stdout = self.old_stdout
......@@ -306,6 +307,7 @@ class TestCommandControl(unittest.TestCase):
self.cmdctl = MyCommandControl(None, True)
def tearDown(self):
sys.stdout = self.old_stdout
def _check_config(self, cmdctl):
......@@ -427,6 +429,9 @@ class TestSecureHTTPServer(unittest.TestCase):
MyCommandControl, verbose=True)
def tearDown(self):
# both sys.stdout and sys.stderr are the same, so closing one is
# sufficient
sys.stdout = self.old_stdout
sys.stderr = self.old_stderr
......@@ -25,6 +25,7 @@ import isc.ddns.session
from isc.ddns.zone_config import ZoneConfig
from isc.ddns.logger import ClientFormatter, ZoneFormatter
from isc.config.ccsession import *
from isc.config.module_spec import ModuleSpecError
from import SessionError, SessionTimeout, ProtocolError
import isc.util.process
import isc.util.cio.socketsession
......@@ -34,6 +35,7 @@ from isc.server_common.dns_tcp import DNSTCPContext
from isc.datasrc import DataSourceClient
from isc.server_common.auth_command import auth_loadzone_command
import select
import time
import errno
from isc.log_messages.ddns_messages import *
......@@ -67,24 +69,22 @@ else:
if "B10_FROM_BUILD" in os.environ:
AUTH_SPECFILE_PATH = os.environ["B10_FROM_BUILD"] + "/src/bin/auth"
if "B10_FROM_SOURCE_LOCALSTATEDIR" in os.environ:
# Cooperating modules
# Cooperating or dependency modules
class DDNSConfigError(Exception):
'''An exception indicating an error in updating ddns configuration.
......@@ -143,15 +143,23 @@ def get_datasrc_client(cc_session):
file = os.environ["B10_FROM_BUILD"] + "/bind10_zones.sqlite3"
datasrc_config = '{ "database_file": "' + file + '"}'
return HARDCODED_DATASRC_CLASS, DataSourceClient('sqlite3',
DataSourceClient('sqlite3', datasrc_config), file)
except isc.datasrc.Error as ex:
class DummyDataSourceClient:
def __init__(self, ex):
self.__ex = ex
def find_zone(self, zone_name):
raise isc.datasrc.Error(self.__ex)
return HARDCODED_DATASRC_CLASS, DummyDataSourceClient(ex)
return (HARDCODED_DATASRC_CLASS, DummyDataSourceClient(ex), file)
def add_pause(sec):
'''Pause a specified period for inter module synchronization.
This is a trivial wrapper of time.sleep, but defined as a separate function
so tests can customize it.
class DDNSServer:
# The number of TCP clients that can be handled by the server at the same
......@@ -181,8 +189,23 @@ class DDNSServer:
# Internal attributes derived from other modules. They will be
# initialized via dd_remote_xxx below and will be kept updated
# through their callbacks. They are defined as 'protected' so tests
# can examine them; but they are essentially private to the class.
# Datasource client used for handling update requests: when set,
# should a tuple of RRClass and DataSourceClient. Constructed and
# maintained based on auth configuration.
self._datasrc_info = None
# A set of secondary zones, retrieved from zonemgr configuration.
self._secondary_zones = None
# Get necessary configurations from remote modules.
for mod in [(AUTH_MODULE_NAME, self.__auth_config_handler),
(ZONEMGR_MODULE_NAME, self.__zonemgr_config_handler)]:
self.__add_remote_module(mod[0], mod[1])
# This should succeed as long as cfgmgr is up.
self._shutdown = False
......@@ -256,6 +279,88 @@ class DDNSServer:
answer = create_answer(1, "Unknown command: " + str(cmd))
return answer
def __add_remote_module(self, mod_name, callback):
'''Register interest in other module's config with a callback.'''
# Due to startup timing, add_remote_config can fail. We could make it
# more sophisticated, but for now we simply retry a few times, each
# separated by a short period (3 times and 1 sec, arbitrary chosen,
# and hardcoded for now). In practice this should be more than
# sufficient, but if it turns out to be a bigger problem we can
# consider more elegant solutions.
for n_try in range(0, 3):
# by_name() version can fail with ModuleSpecError in getting
# the module spec because cfgmgr returns a "successful" answer
# with empty data if it cannot find the specified module.
# This seems to be a deviant behavior (see Trac #2039), but
# we need to deal with it.
self._cc.add_remote_config_by_name(mod_name, callback)
except (ModuleSpecError, ModuleCCSessionError) as ex:
logger.warn(DDNS_GET_REMOTE_CONFIG_FAIL, mod_name, n_try + 1,
last_ex = ex
raise last_ex
def __auth_config_handler(self, new_config, module_config):
# If we've got the config before and the new config doesn't update
# the DB file, there's nothing we should do with it.
# Note: there seems to be a bug either in bindctl or cfgmgr, and
# new_config can contain 'database_file' even if it's not really
# updated. We still perform the check so we can avoid redundant
# resetting when the bug is fixed. The redundant reset itself is not
# good, but such configuration update should not happen so often and
# it should be acceptable in practice.
if self._datasrc_info is not None and \
not 'database_file' in new_config:
rrclass, client, db_file = get_datasrc_client(self._cc)
self._datasrc_info = (rrclass, client), db_file)
def __zonemgr_config_handler(self, new_config, module_config):
# If we've got the config before and the new config doesn't update
# the secondary zone list, there's nothing we should do with it.
# (Same note as that for auth's config applies)
if self._secondary_zones is not None and \
not 'secondary_zones' in new_config:
# Get the latest secondary zones. Use get_remote_config_value() so
# it can work for both the initial default case and updates.
sec_zones, _ = self._cc.get_remote_config_value(ZONEMGR_MODULE_NAME,
new_secondary_zones = set()
# Parse the new config and build a new list of secondary zones.
# Unfortunately, in the current implementation, even an observer
# module needs to perform full validation. This should be changed
# so that only post-validation (done by the main module) config is
# delivered to observer modules, but until it's supported we need
# to protect ourselves.
for zone_spec in sec_zones:
zname = Name(zone_spec['name'])
# class has the default value in case it's unspecified.
# ideally this should be merged within the config module, but
# the current implementation doesn't esnure that, so we need to
# subsitute it ourselves.
if 'class' in zone_spec:
zclass = RRClass(zone_spec['class'])
zclass = RRClass(module_config.get_default_value(
new_secondary_zones.add((zname, zclass))
self._secondary_zones = new_secondary_zones, len(self._secondary_zones))
except Exception as ex:
def trigger_shutdown(self):
'''Initiate a shutdown sequence.
......@@ -273,10 +378,13 @@ class DDNSServer:
Perform any cleanup that is necessary when shutting down the server.
Do NOT call this to initialize shutdown, use trigger_shutdown().
Currently, it only causes the ModuleCCSession to send a message that
this module is stopping.
# tell the ModuleCCSession to send a message that this module is
# stopping.
# make sure any open socket is explicitly closed, per Python
# convention.
def accept(self):
......@@ -366,9 +474,8 @@ class DDNSServer:
# Let an update session object handle the request. Note: things around
# ZoneConfig will soon be substantially revised. For now we don't
# bother to generalize it.
datasrc_class, datasrc_client = get_datasrc_client(self._cc)
zone_cfg = ZoneConfig([], datasrc_class, datasrc_client,
zone_cfg = ZoneConfig(self._secondary_zones, self._datasrc_info[0],
self._datasrc_info[1], self._zone_config)
update_session = self._UpdateSessionClass(self.__request_msg,
remote_addr, zone_cfg)
result, zname, zclass = update_session.handle()
......@@ -605,7 +712,7 @@ def main(ddns_server=None):
except SessionError as e:
logger.error(DDNS_CC_SESSION_ERROR, str(e))
except ModuleCCSessionError as e:
except (ModuleSpecError, ModuleCCSessionError) as e:
logger.error(DDNS_MODULECC_SESSION_ERROR, str(e))
except DDNSConfigError as e:
logger.error(DDNS_CONFIG_ERROR, str(e))
......@@ -25,6 +25,12 @@ There was a low-level error when we tried to accept an incoming connection
connections we already have, but this connection is dropped. The reason
is logged.
% DDNS_AUTH_DBFILE_UPDATE updated auth DB file to %1
b10-ddns was notified of updates to the SQLite3 DB file that b10-auth
uses for the underlying data source and on which b10-ddns needs to
make updates. b10-ddns then updated its internal setup so further
updates would be made on the new DB.
% 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.
......@@ -53,6 +59,29 @@ 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_GET_REMOTE_CONFIG_FAIL failed to get %1 module configuration %2 times: %3
b10-ddns tried to get configuration of some remote modules for its
operation, but it failed. The most likely cause of this is that the
remote module has not fully started up and b10-ddns couldn't get the
configuration in a timely fashion. b10-ddns attempts to retry it a
few times, imposing a short delay, hoping it eventually succeeds if
it's just a timing issue. The number of total failed attempts is also
logged. If it reaches an internal threshold b10-ddns considers it a
fatal error and terminates. Even in that case, if b10-ddns is
configured as a "dispensable" component (which is the default), the
parent bind10 process will restart it, and there will be another
chance of getting the remote configuration successfully. These are
not the optimal behavior, but it's believed to be sufficient in
practice (there would normally be no failure in the first place). If
it really causes an operational trouble other than having a few of
these log messages, please submit a bug report; there can be several
ways to make it more sophisticated. Another, less likely reason for
having this error is because the remote modules are not actually
configured to run. If that's the case fixing the configuration should
solve the problem - either by making sure the remote module will run
or by not running b10-ddns (without these remote modules b10-ddns is
not functional, so there's no point in running it in this case).
% 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
......@@ -66,10 +95,21 @@ 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_AUTH_UPDATE received configuration updates from auth server
b10-ddns is notified of updates to b10-auth configuration
(including a report of the initial configuration) that b10-ddns might
be interested in.
% DDNS_RECEIVED_SHUTDOWN_COMMAND shutdown command received
The ddns process received a shutdown command from the command channel
and will now shut down.
% DDNS_RECEIVED_ZONEMGR_UPDATE received configuration updates from zonemgr
b10-ddns is notified of updates to b10-zonemgr's configuration
(including a report of the initial configuration). It may possibly
contain changes to the secondary zones, in which case b10-ddns will
update its internal copy of that configuration.
% DDNS_REQUEST_PARSE_FAIL failed to parse update request: %1
b10-ddns received an update request via b10-auth, but the received
data failed to pass minimum validation: it was either broken wire
......@@ -117,6 +157,31 @@ with the client address.
The ddns process has successfully started and is now ready to receive commands
and updates.
% DDNS_SECONDARY_ZONES_UPDATE updated secondary zone list (%1 zones are listed)
b10-ddns has successfully updated the internal copy of secondary zones
obtained from b10-zonemgr, based on a latest update to zonemgr's
configuration. The number of newly configured (unique) secondary
zones is logged.
% DDNS_SECONDARY_ZONES_UPDATE_FAIL failed to update secondary zone list: %1
An error message. b10-ddns was notified of updates to a list of
secondary zones from b10-zonemgr and tried to update its own internal
copy of the list, but it failed. This can happen if the configuration
contains an error, and b10-zonemgr should also reject that update.
Unfortunately, in the current implementation there is no way to ensure
that both zonemgr and ddns have consistent information when an update
contains an error; further, as of this writing zonemgr has a bug that
it could partially update the list of secondary zones if part of the
list has an error (see Trac ticket #2038). b10-ddns still keeps
running with the previous configuration, but it's strongly advisable
to check log messages from zonemgr, and if it indicates there can be
inconsistent state, it's better to restart the entire BIND 10 system
(just restarting b10-ddns wouldn't be enough, because zonemgr can have
partially updated configuration due to bug #2038). The log message
contains an error description, but it's intentionally kept simple as
it's primarily a matter of zonemgr. To know the details of the error,
log messages of zonemgr should be consulted.
% 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
......@@ -21,7 +21,10 @@ from isc.acl.acl import ACCEPT
import isc.util.cio.socketsession
from import SessionTimeout, SessionError, ProtocolError
from isc.datasrc import DataSourceClient
from isc.config.ccsession import create_answer
from isc.config import module_spec_from_file
from isc.config.config_data import ConfigData
from isc.config.ccsession import create_answer, ModuleCCSessionError
from isc.config.module_spec import ModuleSpecError
from isc.server_common.dns_tcp import DNSTCPContext
import ddns
import errno
......@@ -56,6 +59,11 @@ TEST_TSIG_KEYRING.add(TEST_TSIG_KEY)
# Another TSIG key not in the keyring, making verification fail
# Incorporate it so we can use the real default values of zonemgr config
# in the tests.
ZONEMGR_MODULE_SPEC = module_spec_from_file(
os.environ["B10_FROM_BUILD"] + "/src/bin/zonemgr/zonemgr.spec")
class FakeSocket:
A fake socket. It only provides a file number, peer name and accept method.
......@@ -208,6 +216,13 @@ class MyCCSession(isc.config.ConfigData):
self._sendmsg_exception = None # will be raised from sendmsg if !None
self._recvmsg_exception = None # will be raised from recvmsg if !None
# Attributes to handle (faked) remote configurations
self.__callbacks = {} # record callbacks for updates to remote confs
self._raise_mods = {} # map of module to exceptions to be triggered
# on add_remote. settable by tests.
self._auth_config = {} # faked auth cfg, settable by tests
self._zonemgr_config = {} # faked zonemgr cfg, settable by tests
def start(self):
'''Called by DDNSServer initialization, but not used in tests'''
self._started = True
......@@ -222,8 +237,27 @@ class MyCCSession(isc.config.ConfigData):
return FakeSocket(1)
def add_remote_config(self, spec_file_name):
def add_remote_config_by_name(self, module_name, update_callback=None):
# If a list of exceptions is given for the module, raise the front one,
# removing that exception from the list (so the list length controls
# how many (and which) exceptions should be raised on add_remote).
if module_name in self._raise_mods.keys() and \
len(self._raise_mods[module_name]) != 0:
ex = self._raise_mods[module_name][0]
self._raise_mods[module_name] = self._raise_mods[module_name][1:]
raise ex('Failure requesting remote config data')
if update_callback is not None:
self.__callbacks[module_name] = update_callback
if module_name is 'Auth':
if module_name in self.__callbacks:
# ddns implementation doesn't use the 2nd element, so just
# setting it to None
self.__callbacks[module_name](self._auth_config, None)
if module_name is 'Zonemgr':
if module_name in self.__callbacks:
def get_remote_config_value(self, module_name, item):
if module_name == "Auth" and item == "database_file":
......@@ -233,6 +267,14 @@ class MyCCSession(isc.config.ConfigData):
return [], True # default
return self.auth_datasources, False
if module_name == 'Zonemgr' and item == 'secondary_zones':
if item in self._zonemgr_config:
return self._zonemgr_config[item], False
seczone_default = \
return seczone_default, True
def group_sendmsg(self, msg, group):
# remember the passed parameter, and return dummy sequence
......@@ -299,6 +341,10 @@ class TestDDNSServer(unittest.TestCase):
self.__select_answer = None
self.__select_exception = None
self.__hook_called = False
# Because we overwrite the _listen_socket, close any existing
# socket object.
if self.ddns_server._listen_socket is not None:
self.ddns_server._listen_socket = FakeSocket(2) = self.__select
......@@ -306,12 +352,15 @@ class TestDDNSServer(unittest.TestCase):
self.__tcp_sock = FakeSocket(10, socket.IPPROTO_TCP)
self.__tcp_ctx = DNSTCPContext(self.__tcp_sock)
self.__tcp_data = b'A' * 12 # dummy, just the same size as DNS header
# some tests will override this, which will be restored in tearDown:
self.__orig_add_pause = ddns.add_pause
def tearDown(self): =
ddns.isc.util.cio.socketsession.SocketSessionReceiver = \
isc.server_common.tsig_keyring = self.orig_tsig_keyring
ddns.add_pause = self.__orig_add_pause
def test_listen(self):
......@@ -334,6 +383,9 @@ class TestDDNSServer(unittest.TestCase):
# Now make sure the clear_socket really works
# Let ddns object complete any necessary cleanup (not part of the test,
# but for suppressing any warnings from the Python interpreter)
def test_initial_config(self):
# right now, the only configuration is the zone configuration, whose
......@@ -422,6 +474,112 @@ class TestDDNSServer(unittest.TestCase):
acl = self.ddns_server._zone_config[(TEST_ZONE_NAME, TEST_RRCLASS)]
self.assertEqual(ACCEPT, acl.execute(TEST_ACL_CONTEXT))
def test_datasrc_config(self):
# By default (in our faked config) it should be derived from the
# test data source
rrclass, datasrc_client = self.ddns_server._datasrc_info
self.assertEqual(RRClass.IN(), rrclass)
# emulating an update. calling add_remote_config_by_name is a
# convenient faked way to invoke the callback. We set the db file
# to a bogus one; the current implementation will create an unusable
# data source client.
self.__cc_session.auth_db_file = './notexistentdir/somedb.sqlite3'
self.__cc_session._auth_config = \
{'database_file': './notexistentdir/somedb.sqlite3'}
rrclass, datasrc_client = self.ddns_server._datasrc_info
self.assertEqual(RRClass.IN(), rrclass)
datasrc_client.find_zone, Name(''))
# Check the current info isn't changed if the new config doesn't
# update it.
info_orig = self.ddns_server._datasrc_info
self.ddns_server._datasrc_info = 42 # dummy value, should be kept.
self.__cc_session._auth_config = {'other_config': 'value'}
self.assertEqual(42, self.ddns_server._datasrc_info)
self.ddns_server._datasrc_info = info_orig
def test_secondary_zones_config(self):
# By default it should be an empty list
self.assertEqual(set(), self.ddns_server._secondary_zones)
# emulating an update.
self.__cc_session._zonemgr_config = {'secondary_zones': [
# The new set of secondary zones should be stored.
self.assertEqual({(TEST_ZONE_NAME, TEST_RRCLASS)},
# Similar to the above, but 'class' is unspecified. The default value
# should be used.
self.__cc_session._zonemgr_config = {'secondary_zones': [
{'name': TEST_ZONE_NAME_STR}]}
self.assertEqual({(TEST_ZONE_NAME, TEST_RRCLASS)},
# The given list has a duplicate. The resulting set should unify them.
self.__cc_session._zonemgr_config = {'secondary_zones': [
self.assertEqual({(TEST_ZONE_NAME, TEST_RRCLASS)},
# Check the 2ndary zones aren't changed if the new config doesn't