Commit b6982ea3 authored by Jelte Jansen's avatar Jelte Jansen
Browse files

[trac811] addressed review comments

parent cd79c2fa
# Copyright (C) 2009 Internet Systems Consortium.
# Copyright (C) 2011 Internet Systems Consortium.
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
......@@ -20,8 +20,10 @@ from xfrin import *
#
# Commonly used (mostly constant) test parameters
#
TEST_ZONE_NAME = "example.com"
TEST_ZONE_NAME_STR = "example.com."
TEST_ZONE_NAME = Name(TEST_ZONE_NAME_STR)
TEST_RRCLASS = RRClass.IN()
TEST_RRCLASS_STR = 'IN'
TEST_DB_FILE = 'db_file'
TEST_MASTER_IPV4_ADDRESS = '127.0.0.1'
TEST_MASTER_IPV4_ADDRINFO = (socket.AF_INET, socket.SOCK_STREAM,
......@@ -40,12 +42,12 @@ TSIG_KEY = TSIGKey("example.com:SFuWd/q99SzF8Yzd1QbB9g==")
soa_rdata = Rdata(RRType.SOA(), TEST_RRCLASS,
'master.example.com. admin.example.com ' +
'1234 3600 1800 2419200 7200')
soa_rrset = RRset(Name(TEST_ZONE_NAME), TEST_RRCLASS, RRType.SOA(),
soa_rrset = RRset(TEST_ZONE_NAME, TEST_RRCLASS, RRType.SOA(),
RRTTL(3600))
soa_rrset.add_rdata(soa_rdata)
example_axfr_question = Question(Name(TEST_ZONE_NAME), TEST_RRCLASS,
example_axfr_question = Question(TEST_ZONE_NAME, TEST_RRCLASS,
RRType.AXFR())
example_soa_question = Question(Name(TEST_ZONE_NAME), TEST_RRCLASS,
example_soa_question = Question(TEST_ZONE_NAME, TEST_RRCLASS,
RRType.SOA())
default_questions = [example_axfr_question]
default_answers = [soa_rrset]
......@@ -60,6 +62,13 @@ def strip_mutable_tsig_data(data):
# Time Signed.
return data[0:-32] + data[-26:-22] + data[-6:]
class MockCC():
def get_default_value(self, identifier):
if identifier == "zones/master_port":
return TEST_MASTER_PORT
if identifier == "zones/class":
return 'IN'
class MockXfrin(Xfrin):
# This is a class attribute of a callable object that specifies a non
# default behavior triggered in _cc_check_command(). Specific test methods
......@@ -70,6 +79,7 @@ class MockXfrin(Xfrin):
def _cc_setup(self):
self._tsig_key_str = None
self._module_cc = MockCC()
pass
def _get_db_file(self):
......@@ -80,6 +90,19 @@ class MockXfrin(Xfrin):
if MockXfrin.check_command_hook:
MockXfrin.check_command_hook()
def xfrin_start(self, zone_name, rrclass, db_file, master_addrinfo, tsig_key_str,
check_soa = True):
# store some of the arguments for verification, then call this
# method in the superclass
self.xfrin_started_zone_name = zone_name
self.xfrin_started_rrclass = rrclass
self.xfrin_started_master_addr = master_addrinfo[2][0]
self.xfrin_started_master_port = master_addrinfo[2][1]
self.xfrin_started_tsig_key_str = tsig_key_str
return Xfrin.xfrin_start(self, zone_name, rrclass, db_file,
master_addrinfo, tsig_key_str,
check_soa = True)
class MockXfrinConnection(XfrinConnection):
def __init__(self, sock_map, zone_name, rrclass, db_file, shutdown_event,
master_addr):
......@@ -450,7 +473,7 @@ class TestXfrin(unittest.TestCase):
sys.stderr = open(os.devnull, 'w')
self.xfr = MockXfrin()
self.args = {}
self.args['zone_name'] = TEST_ZONE_NAME
self.args['zone_name'] = TEST_ZONE_NAME_STR
self.args['port'] = TEST_MASTER_PORT
self.args['master'] = TEST_MASTER_IPV4_ADDRESS
self.args['db_file'] = TEST_DB_FILE
......@@ -502,8 +525,7 @@ class TestXfrin(unittest.TestCase):
def test_parse_cmd_params_nomaster(self):
# master address is mandatory.
del self.args['master']
master_addrinfo = self._do_parse_master_port()
self.assertEqual(master_addrinfo[2][0], DEFAULT_MASTER)
self.assertRaises(XfrinException, self._do_parse_master_port)
def test_parse_cmd_params_bad_ip4(self):
self.args['master'] = '3.3.3.3.3'
......@@ -533,28 +555,30 @@ class TestXfrin(unittest.TestCase):
def test_command_handler_retransfer(self):
self.assertEqual(self.xfr.command_handler("retransfer",
self.args)['result'][0], 0)
self.assertEqual(self.args['master'], self.xfr.xfrin_started_master_addr)
self.assertEqual(int(self.args['port']), self.xfr.xfrin_started_master_port)
def test_command_handler_retransfer_short_command1(self):
# try it when only specifying the zone name (of unknown zone)
short_args = {}
short_args['zone_name'] = TEST_ZONE_NAME
short_args['zone_name'] = TEST_ZONE_NAME_STR
self.assertEqual(self.xfr.command_handler("retransfer",
short_args)['result'][0], 0)
short_args)['result'][0], 1)
def test_command_handler_retransfer_short_command2(self):
# try it when only specifying the zone name (of unknown zone)
short_args = {}
short_args['zone_name'] = TEST_ZONE_NAME + "."
short_args['zone_name'] = TEST_ZONE_NAME_STR
self.assertEqual(self.xfr.command_handler("retransfer",
short_args)['result'][0], 0)
short_args)['result'][0], 1)
def test_command_handler_retransfer_short_command3(self):
# try it when only specifying the zone name (of known zone)
short_args = {}
short_args['zone_name'] = TEST_ZONE_NAME
short_args['zone_name'] = TEST_ZONE_NAME_STR
zones = { 'zones': [
{ 'name': TEST_ZONE_NAME,
{ 'name': TEST_ZONE_NAME_STR,
'master_addr': TEST_MASTER_IPV4_ADDRESS,
'master_port': TEST_MASTER_PORT
}
......@@ -562,6 +586,10 @@ class TestXfrin(unittest.TestCase):
self.xfr.config_handler(zones)
self.assertEqual(self.xfr.command_handler("retransfer",
short_args)['result'][0], 0)
self.assertEqual(TEST_MASTER_IPV4_ADDRESS,
self.xfr.xfrin_started_master_addr)
self.assertEqual(int(TEST_MASTER_PORT),
self.xfr.xfrin_started_master_port)
def test_command_handler_retransfer_badcommand(self):
self.args['master'] = 'invalid'
......@@ -569,13 +597,15 @@ class TestXfrin(unittest.TestCase):
self.args)['result'][0], 1)
def test_command_handler_retransfer_quota(self):
self.args['master'] = '127.0.0.1'
for i in range(self.xfr._max_transfers_in - 1):
self.xfr.recorder.increment(str(i) + TEST_ZONE_NAME)
self.xfr.recorder.increment(Name(str(i) + TEST_ZONE_NAME_STR))
# there can be one more outstanding transfer.
self.assertEqual(self.xfr.command_handler("retransfer",
self.args)['result'][0], 0)
# make sure the # xfrs would excceed the quota
self.xfr.recorder.increment(str(self.xfr._max_transfers_in) + TEST_ZONE_NAME)
self.xfr.recorder.increment(Name(str(self.xfr._max_transfers_in) + TEST_ZONE_NAME_STR))
# this one should fail
self.assertEqual(self.xfr.command_handler("retransfer",
self.args)['result'][0], 1)
......@@ -599,6 +629,10 @@ class TestXfrin(unittest.TestCase):
self.args['master'] = TEST_MASTER_IPV6_ADDRESS
self.assertEqual(self.xfr.command_handler("refresh",
self.args)['result'][0], 0)
self.assertEqual(TEST_MASTER_IPV6_ADDRESS,
self.xfr.xfrin_started_master_addr)
self.assertEqual(int(TEST_MASTER_PORT),
self.xfr.xfrin_started_master_port)
def test_command_handler_notify(self):
# at this level, refresh is no different than retransfer.
......@@ -611,8 +645,9 @@ class TestXfrin(unittest.TestCase):
# try it with a known zone
self.args['master'] = TEST_MASTER_IPV6_ADDRESS
# but use a different address in the actual command
zones = { 'zones': [
{ 'name': TEST_ZONE_NAME,
{ 'name': TEST_ZONE_NAME_STR,
'master_addr': TEST_MASTER_IPV4_ADDRESS,
'master_port': TEST_MASTER_PORT
}
......@@ -621,6 +656,16 @@ class TestXfrin(unittest.TestCase):
self.assertEqual(self.xfr.command_handler("notify",
self.args)['result'][0], 0)
# and see if we used the address from the command, and not from
# the config
# This is actually NOT the address given in the command, which
# would at this point not make sense, see the TODO in
# xfrin.py.in:542)
self.assertEqual(TEST_MASTER_IPV4_ADDRESS,
self.xfr.xfrin_started_master_addr)
self.assertEqual(int(TEST_MASTER_PORT),
self.xfr.xfrin_started_master_port)
def test_command_handler_unknown(self):
self.assertEqual(self.xfr.command_handler("xxx", None)['result'][0], 1)
......@@ -629,37 +674,127 @@ class TestXfrin(unittest.TestCase):
self.assertEqual(self.xfr.config_handler({'transfers_in': 3})['result'][0], 0)
self.assertEqual(self.xfr._max_transfers_in, 3)
def _check_zones_config(self, config_given):
for zone_config in config_given['zones']:
zone_name = zone_config['name']
zone_info = self.xfr._get_zone_info(Name(zone_name), RRClass.IN())
self.assertEqual(zone_info.master_addr_str, zone_config['master_addr'])
self.assertEqual(zone_info.master_port_str, zone_config['master_port'])
if 'tsig_key' in zone_config:
self.assertEqual(zone_info.tsig_key_str, zone_config['tsig_key'])
else:
self.assertIsNone(zone_info.tsig_key_str)
def test_command_handler_zones(self):
zones1 = { 'zones': [
{ 'name': 'test.example.',
'master_addr': '192.0.2.1',
'master_port': 53
}
]}
self.assertEqual(self.xfr.config_handler(zones1)['result'][0], 0)
self._check_zones_config(zones1)
zones2 = { 'zones': [
{ 'name': 'test.example.',
'master_addr': '192.0.2.2',
'master_port': 53,
'tsig_key': "example.com:SFuWd/q99SzF8Yzd1QbB9g=="
}
]}
self.assertEqual(self.xfr.config_handler(zones2)['result'][0], 0)
self._check_zones_config(zones2)
# test that configuring the zone multiple times fails
zones = { 'zones': [
{ 'name': 'test.example.',
'master_addr': '192.0.2.1',
'master_port': 53
},
{ 'name': 'test.example.',
'master_addr': '192.0.2.2',
'master_port': 53
}
]}
self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
# since this has failed, we should still have the previous config
self._check_zones_config(zones2)
zones = { 'zones': [
{ 'name': 'test.example.',
'master_addr': '192.0.2.3',
'master_port': 53,
'class': 'BADCLASS'
}
]}
self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
self._check_zones_config(zones2)
zones = { 'zones': [
{ 'master_addr': '192.0.2.4',
'master_port': 53
}
]}
self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
# since this has failed, we should still have the previous config
self._check_zones_config(zones2)
zones = { 'zones': [
{ 'name': 'test.com.',
'master_addr': '1.1.1.1',
{ 'name': 'bad..zone.',
'master_addr': '192.0.2.5',
'master_port': 53
}
]}
self.assertEqual(self.xfr.config_handler(zones)['result'][0], 0)
self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
# since this has failed, we should still have the previous config
self._check_zones_config(zones2)
zones = { 'zones': [
{ 'master_addr': '1.1.1.1',
{ 'name': '',
'master_addr': '192.0.2.6',
'master_port': 53
}
]}
self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
# since this has failed, we should still have the previous config
self._check_zones_config(zones2)
zones = { 'zones': [
{ 'name': 'test.com',
{ 'name': 'test.example',
'master_addr': 'badaddress',
'master_port': 53
}
]}
self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
# since this has failed, we should still have the previous config
self._check_zones_config(zones2)
zones = { 'zones': [
{ 'name': 'test.com',
'master_addr': '1.1.1.1',
{ 'name': 'test.example',
'master_addr': '192.0.2.7',
'master_port': 'bad_port'
}
]}
self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
# since this has failed, we should still have the previous config
self._check_zones_config(zones2)
# let's also add a zone that is correct too, and make sure
# that the new config is not partially taken
zones = { 'zones': [
{ 'name': 'test.example.',
'master_addr': '192.0.2.8',
'master_port': 53
},
{ 'name': 'test2.example.',
'master_addr': '192.0.2.9',
'master_port': 53,
'tsig_key': 'badkey'
}
]}
self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
# since this has failed, we should still have the previous config
self._check_zones_config(zones2)
def raise_interrupt():
......
#!@PYTHON@
# Copyright (C) 2010 Internet Systems Consortium.
# Copyright (C) 2011 Internet Systems Consortium.
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
......@@ -56,14 +56,12 @@ XFROUT_MODULE_NAME = 'Xfrout'
ZONE_MANAGER_MODULE_NAME = 'Zonemgr'
REFRESH_FROM_ZONEMGR = 'refresh_from_zonemgr'
ZONE_XFRIN_FAILED = 'zone_xfrin_failed'
DEFAULT_MASTER_PORT = 53
__version__ = 'BIND10'
# define xfrin rcode
XFRIN_OK = 0
XFRIN_FAIL = 1
DEFAULT_MASTER_PORT = '53'
DEFAULT_MASTER = '127.0.0.1'
def log_error(msg):
sys.stderr.write("[b10-xfrin] %s\n" % str(msg))
......@@ -71,8 +69,34 @@ class XfrinException(Exception):
pass
class XfrinConfigException(Exception):
"""This exception is raised if there is an error in the given
configuration (part), for instance when the zone's master
address is not a valid IP address, when the zone does not
have a name, or when multiple settings are given for the same
zone."""
pass
def _check_zone_name(zone_name_str):
"""Checks if the given zone name is a valid domain name, and returns it as a Name object.
Raises an XfrinException if it is not."""
try:
return Name(zone_name_str)
except (EmptyLabel, TooLongLabel, BadLabelType, BadEscape,
TooLongName, IncompleteName) as ne:
raise XfrinConfigException("bad zone name: " + zone_name_str + " (" + str(ne) + ")")
def _check_zone_class(zone_class_str):
"""If the given argument is a string: checks if the given class is
a valid one, and returns an RRClass object if so.
Raises XfrinConfigException if not.
If it is None, this function returns the default RRClass.IN()"""
if zone_class_str is None:
return RRClass.IN()
try:
return RRClass(zone_class_str)
except InvalidRRClass as irce:
raise XfrinConfigException("bad zone class: " + zone_class_str + " (" + str(irce) + ")")
class XfrinConnection(asyncore.dispatcher):
'''Do xfrin in this class. '''
......@@ -382,22 +406,31 @@ class XfrinRecorder:
return ret
class ZoneInfo:
def __init__(self, config_data):
def __init__(self, config_data, module_cc = None):
"""Creates a zone_info with the config data element as
specified by the 'zones' list in xfrin.spec"""
self.name = config_data.get('name')
self.class_str = config_data.get('class') or 'IN'
specified by the 'zones' list in xfrin.spec. Module_cc is
needed to get the defaults from the specification"""
self.name_str = config_data.get('name')
self.class_str = config_data.get('class') or \
module_cc.get_default_value("zones/class")
if self.name is None:
if self.name_str is None:
raise XfrinConfigException("Configuration zones list "
"element does not contain "
"'name' attribute")
# add the root dot if the user forgot
if len(self.name) > 0 and self.name[-1] != '.':
self.name += '.'
self.master_addr_str = config_data.get('master_addr') or DEFAULT_MASTER
self.master_port_str = config_data.get('master_port') or DEFAULT_MASTER_PORT
self.master_addr_str = config_data.get('master_addr')
self.master_port_str = config_data.get('master_port') or \
str(module_cc.get_default_value("zones/master_port"))
try:
self.name = Name(self.name_str)
except (EmptyLabel, TooLongLabel, BadLabelType, BadEscape,
TooLongName, IncompleteName) as ne:
errmsg = "bad zone name: " + self.name_str + " (" + str(ne) + ")"
log_error(errmsg)
raise XfrinConfigException(errmsg)
try:
self.master_addr = isc.net.parse.addr_parse(self.master_addr_str)
self.master_port = isc.net.parse.port_parse(self.master_port_str)
......@@ -406,7 +439,21 @@ class ZoneInfo:
log_error(errmsg)
raise XfrinConfigException(errmsg)
try:
self.rrclass = RRClass(self.class_str)
except InvalidRRClass:
errmsg = "invalid class: " + self.class_str
log_error(errmsg)
raise XfrinConfigException(errmsg)
self.tsig_key_str = config_data.get('tsig_key') or None
if self.tsig_key_str is not None:
try:
tsig_key = TSIGKey(self.tsig_key_str)
except InvalidParameter as ipe:
errmsg = "bad TSIG key string: " + self.tsig_key_str
log_error(errmsg)
raise XfrinConfigException(errmsg)
def get_master_addr_info(self):
return (self.master_addr.family, socket.SOCK_STREAM,
......@@ -442,33 +489,50 @@ class Xfrin:
of unit tests.'''
self._module_cc.check_command(False)
def _get_zone_info(self, name, class_str = "IN"):
def _get_zone_info(self, name, rrclass):
"""Returns the ZoneInfo object containing the configured data
for the given zone name. If the zone name did not have any
data, returns None"""
# add the root dot if the user forgot
if len(name) > 0 and name[-1] != '.':
name += '.'
if (name, class_str) in self._zones:
return self._zones[(name, class_str)]
key = (name.to_text(), rrclass.to_text())
if key in self._zones:
return self._zones[key]
else:
return None
def _clear_zone_info(self):
self._zones = {}
def _get_all_zone_info(self):
"""Returns the structure used to store ZoneInfo objects. This
method can be used (together with _set_all_zone_info()) to
revert to the previous zone info configuration when one
of the new config items turns out to be bad"""
return self._zones
def _add_zone_info(self, zone_info):
self._zones[(zone_info.name, zone_info.class_str)] = zone_info
"""Add the zone info. Raises a XfrinConfigException if a zone
with the same name and class is already configured"""
key = (zone_info.name.to_text(), zone_info.class_str)
if key in self._zones:
raise XfrinConfigException("zone " + str(key) +
" configured multiple times")
self._zones[key] = zone_info
def _set_all_zone_info(self, zones):
self._zones = zones
def _clear_zone_info(self):
self._zones = {}
def config_handler(self, new_config):
self._max_transfers_in = new_config.get("transfers_in") or self._max_transfers_in
if 'zones' in new_config:
zones_backup = self._get_all_zone_info()
self._clear_zone_info()
for zone_config in new_config.get('zones'):
try:
zone_info = ZoneInfo(zone_config)
zone_info = ZoneInfo(zone_config, self._module_cc)
self._add_zone_info(zone_info)
except XfrinConfigException as xce:
self._set_all_zone_info(zones_backup)
return create_answer(1, str(xce))
return create_answer(0)
......@@ -494,11 +558,14 @@ class Xfrin:
# notify command maybe has the parameters which
# specify the notifyfrom address and port, according the RFC1996, zone
# transfer should starts first from the notifyfrom, but now, let 'TODO' it.
# (using the value now, while we can only set one master address, would be
# a security hole. Once we add the ability to have multiple master addresses,
# we should check if it matches one of them, and then use it.)
(zone_name, rrclass) = self._parse_zone_name_and_class(args)
zone_info = self._get_zone_info(zone_name)
zone_info = self._get_zone_info(zone_name, rrclass)
if zone_info is None:
# TODO what to do? no info known about zone. defaults?
errmsg = "Got notification to retransfer unknown zone " + zone_name
errmsg = "Got notification to retransfer unknown zone " + zone_name.to_text()
log_error(errmsg)
answer = create_answer(1, errmsg)
else:
......@@ -517,7 +584,7 @@ class Xfrin:
# master address, or else do transfer from the configured masters.
(zone_name, rrclass) = self._parse_zone_name_and_class(args)
master_addr = self._parse_master_and_port(args)
zone_info = self._get_zone_info(zone_name)
zone_info = self._get_zone_info(zone_name, rrclass)
tsig_key_str = None
if zone_info:
tsig_key_str = zone_info.tsig_key_str
......@@ -538,8 +605,8 @@ class Xfrin:
return answer
def _parse_zone_name_and_class(self, args):
zone_name = args.get('zone_name')
if not zone_name:
zone_name_str = args.get('zone_name')
if not zone_name_str:
raise XfrinException('zone name should be provided')
rrclass = args.get('zone_class')
......@@ -551,12 +618,22 @@ class Xfrin:
except InvalidRRClass as e:
raise XfrinException('invalid RRClass: ' + rrclass)
return zone_name, rrclass
return _check_zone_name(zone_name_str), rrclass
def _parse_master_and_port(self, args):
# check if we have configured info about this zone, in case
# port or master are not specified
zone_info = self._get_zone_info(args.get('zone_name'))
zone_name = _check_zone_name(args.get('zone_name'))
zone_class = _check_zone_class(args.get('class'))
zone_info = self._get_zone_info(zone_name, zone_class)
master = args.get('master')
if master is None:
if zone_info is not None:
master = zone_info.master_addr_str
else:
raise XfrinException("Master address not given or "
"configured for " + zone_name.to_text())
port = args.get('port')
if port is None:
......@@ -565,13 +642,6 @@ class Xfrin:
else:
port = DEFAULT_MASTER_PORT
master = args.get('master')
if master is None:
if zone_info is not None:
master = zone_info.master_addr_str
else:
master = DEFAULT_MASTER
return build_addr_info(master, port)
def _get_db_file(self):
......@@ -650,7 +720,8 @@ class Xfrin:
xfrin_thread = threading.Thread(target = process_xfrin,
args = (self,
self.recorder,
zone_name, rrclass,
zone_name.to_text(),
rrclass,
db_file,
self._shutdown_event,
master_addrinfo, check_soa,
......
......@@ -213,6 +213,15 @@ class ConfigData:
return spec['item_default'], True
return None, False
def get_default_value(self, identifier):
"""Returns the default from the specification, or None if there
is no default"""
spec = find_spec_part(self.specification.get_config_spec(), identifier)
if spec and 'item_default' in spec:
return spec['item_default']
else:
return None
def get_module_spec(self):
"""Returns the ModuleSpec object associated with this ConfigData"""
return self.specification
......
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