Commit b6c5c5bf authored by Petr Špaček's avatar Petr Špaček

Deckard: support multiple binaries inside single Deckard instance

Deckard now can run multiple processes inside single simulated network
and communicate with each other.

This introduces couple of incompatible changes:
- command line syntax was changed to accomodate new possibilities (see --help)
- SOCKET_WRAPPER environment variables except DIR are always managed by Deckard
parent 43061fa2
......@@ -45,7 +45,7 @@ depend: $(libfaketime) $(libcwrap)
# Generic rule to run test
$(SOURCES): depend
%.out: %.rpl
@$(preload_syms) $(PYTHON) $(abspath ./deckard.py) $< $(DAEMON) $(TEMPLATE) $(CONFIG) -- $(ADDITIONAL)
@$(preload_syms) $(PYTHON) $(abspath ./deckard.py) $< one $(DAEMON) $(TEMPLATE) $(CONFIG) -- $(ADDITIONAL)
# Synchronize submodules
submodules: .gitmodules
......
......@@ -19,6 +19,7 @@ Deckard requires next software to be installed:
- Python >= 2.7
- dnspython_ - DNS library for Python.
- Jinja2_ - template engine for generating config files.
- PyYAML_ - YAML parser for Python.
- `socket_wrapper`_ - a modification of `initial socket_wrapper`_ library (part of the cwrap_ tool set for creating an isolated networks).
It also depends on libfaketime_, but it is embedded as it requires a rather recent version (automatically synchronised with ``make``).
......@@ -128,21 +129,18 @@ See `scenario guide <https://gitlab.labs.nic.cz/knot/deckard/blob/master/SCENARI
Setting up socket wrapper library (cwrap)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Detailed instructions on using cwrap you can read here_
Detailed instructions on using cwrap you can be found here_
Generally, explicit environment setup for cwrap is not required.
When cwrap environment is absent, default values will be used :
cwrap environment is managed by Deckard. Default values are sufficient, do not touch the environment unless you are trying to debug something. Variables available for direct use are:
- ``SOCKET_WRAPPER_DEFAULT_IFACE`` = 2
- ``SOCKET_WRAPPER_DIR`` will be created in default temporary directory with
randomly generated name, prefixed by ``/tmp``
- ``SOCKET_WRAPPER_DEBUGLEVEL`` will not be set
- ``SOCKET_WRAPPER_DIR`` is a generic working directory. It defaults
to a new temporary directory with randomly generated name,
prefixed by ``tmpdeckard``. When a test fails, the work directory can contain useful
information for post-mortem analysis. You can explicitly set ``SOCKET_WRAPPER_DIR``
to a custom path for more convenient analysis.
- ``SOCKET_WRAPPER_DEBUGLEVEL`` is not set by default.
``SOCKET_WRAPPER_DIR`` can also be used as a work directory for binary under test. When a test
fails, the work directory can contain useful information for post-mortem analysis. You can explicitly
set ``SOCKET_WRAPPER_DIR`` to a custom path for more convenient analysis.
If ``SOCKET_WRAPPER_PCAP_FILE`` contains a path to a writeable file, all the network traffic will be appended to specified file.
Deckard automatically sets ``SOCKET_WRAPPER_PCAP_FILE`` to create separate PCAP files in working directory for Deckard itself and each daemon. Feel free to inspect them.
Acknowledgments
---------------
......@@ -153,6 +151,7 @@ The original test case format is described in the `Doxygen documentation <http:/
.. _cwrap: https://cwrap.org/
.. _`dnspython`: http://www.dnspython.org/
.. _Jinja2: http://jinja.pocoo.org/
.. _`PyYAML`: http://pyyaml.org/
.. _`socket_wrapper`: https://gitlab.labs.nic.cz/labs/socket_wrapper
.. _`initial socket_wrapper`: https://cwrap.org/socket_wrapper.html
.. _Libfaketime: https://github.com/wolfcw/libfaketime
......
......@@ -9,5 +9,5 @@ CMD ["/bin/bash"]
RUN apt-get update
RUN apt-get upgrade -y
RUN apt-get install -y knot-resolver
RUN apt-get install -y python-pep8 pylint python-dnspython python-jinja2
RUN apt-get install -y python3-pep8 pylint3 python3-dnspython python3-jinja2
RUN apt-get install -y python-pep8 pylint python-dnspython python-jinja2 python-yaml
RUN apt-get install -y python3-pep8 pylint3 python3-dnspython python3-jinja2 python3-yaml
This diff is collapsed.
from __future__ import absolute_import
import calendar
import logging
import dns.message
import dns.rrset
......@@ -19,6 +20,11 @@ import time
from datetime import datetime
def str2bool(v):
""" Return conversion of JSON-ish string value to boolean. """
return v.lower() in ('yes', 'true', 'on', '1')
# Global statistics
g_rtt = 0.0
g_nqueries = 0
......@@ -708,7 +714,6 @@ class Scenario:
self.steps = []
self.current_step = None
self.client = {}
self.sockfamily = socket.AF_INET
def reply(self, query, address=None):
"""
......@@ -918,6 +923,99 @@ def parse_scenario(op, args, file_in):
return out
def parse_config(scn_cfg, qmin, installdir):
"""
Transform scene config (key, value) pairs into dict filled with defaults.
"""
# defaults
do_not_query_localhost = True
harden_glue = True
sockfamily = 0 # auto-select value for socket.getaddrinfo
trust_anchor_list = []
stub_addr = None
override_timestamp = None
features = {}
feature_list_delimiter = ';'
feature_pair_delimiter = '='
for k, v in scn_cfg:
# Enable selectively for some tests
if k == 'do-not-query-localhost':
do_not_query_localhost = str2bool(v)
if k == 'harden-glue':
harden_glue = str2bool(v)
if k == 'query-minimization':
qmin = str2bool(v)
elif k == 'trust-anchor':
trust_anchor_list.append(v.strip('"\''))
elif k == 'val-override-timestamp':
override_timestamp_str = v.strip('"\'')
override_timestamp = int(override_timestamp_str)
elif k == 'val-override-date':
override_date_str = v.strip('"\'')
ovr_yr = override_date_str[0:4]
ovr_mnt = override_date_str[4:6]
ovr_day = override_date_str[6:8]
ovr_hr = override_date_str[8:10]
ovr_min = override_date_str[10:12]
ovr_sec = override_date_str[12:]
override_date_str_arg = '{0} {1} {2} {3} {4} {5}'.format(
ovr_yr, ovr_mnt, ovr_day, ovr_hr, ovr_min, ovr_sec)
override_date = time.strptime(override_date_str_arg, "%Y %m %d %H %M %S")
override_timestamp = calendar.timegm(override_date)
elif k == 'stub-addr':
stub_addr = v.strip('"\'')
elif k == 'features':
feature_list = v.split(feature_list_delimiter)
try:
for f_item in feature_list:
if f_item.find(feature_pair_delimiter) != -1:
f_key, f_value = [x.strip()
for x
in f_item.split(feature_pair_delimiter, 1)]
else:
f_key = f_item.strip()
f_value = ""
features[f_key] = f_value
except Exception as e:
raise Exception("can't parse features (%s) in config section (%s)" % (v, str(e)))
elif k == 'feature-list':
try:
f_key, f_value = [x.strip() for x in v.split(feature_pair_delimiter, 1)]
if f_key not in features:
features[f_key] = []
f_value = f_value.replace("{{INSTALL_DIR}}", installdir)
features[f_key].append(f_value)
except Exception as e:
raise Exception("can't parse feature-list (%s) in config section (%s)"
% (v, str(e)))
elif k == 'force-ipv6' and v.upper() == 'TRUE':
sockfamily = socket.AF_INET6
ctx = {
"DO_NOT_QUERY_LOCALHOST": str(do_not_query_localhost).lower(),
"FEATURES": features,
"HARDEN_GLUE": str(harden_glue).lower(),
"INSTALL_DIR": installdir,
"QMIN": str(qmin).lower(),
"TRUST_ANCHORS": trust_anchor_list,
}
if stub_addr:
ctx['ROOT_ADDR'] = stub_addr
# determine and verify socket family for specified root address
gai = socket.getaddrinfo(stub_addr, 53, sockfamily, 0,
socket.IPPROTO_UDP, socket.AI_NUMERICHOST)
assert len(gai) == 1
sockfamily = gai[0][0]
if not sockfamily:
sockfamily = socket.AF_INET # default to IPv4
ctx['_SOCKET_FAMILY'] = sockfamily
if override_timestamp:
ctx['_OVERRIDE_TIMESTAMP'] = override_timestamp
return ctx
def parse_file(file_in):
""" Parse scenario from a file. """
try:
......
......@@ -11,9 +11,9 @@ class Test:
def __init__(self):
self.tests = []
def add(self, name, test, args):
def add(self, name, test, args, config):
""" Add named test to set. """
self.tests.append((name, test, args))
self.tests.append((name, test, args, config))
def run(self):
""" Run planned tests. """
......@@ -22,9 +22,9 @@ class Test:
if planned == 0:
return
for name, test_callback, args in self.tests:
for name, test_callback, args, config in self.tests:
try:
test_callback(name, args)
test_callback(name, args, config)
passed += 1
self.log.info('[ OK ] %s', name)
except Exception as e:
......
......@@ -23,7 +23,7 @@ def get_local_addr_str(family, iface):
addr_local_pattern = "fd00::5357:5f{:02X}"
else:
raise NotImplementedError("[get_local_addr_str] family not supported '%i'" % family)
return addr_local_pattern.format(iface)
return addr_local_pattern.format(int(iface))
class AddrMapInfo:
......@@ -38,7 +38,7 @@ class AddrMapInfo:
class TestServer:
""" This simulates UDP DNS server returning scripted or mirror DNS responses. """
def __init__(self, test_scenario, config, d_iface):
def __init__(self, test_scenario, root_addr, addr_family):
""" Initialize server instance. """
self.thread = None
self.srv_socks = []
......@@ -46,14 +46,11 @@ class TestServer:
self.connections = []
self.active = False
self.scenario = test_scenario
self.config = config
self.addr_map = []
self.start_iface = 2
self.cur_iface = self.start_iface
self.kroot_local = None
self.addr_family = None
self.default_iface = d_iface
self.set_initial_address()
self.kroot_local = root_addr
self.addr_family = addr_family
def __del__(self):
""" Cleanup after deletion. """
......@@ -95,28 +92,6 @@ class TestServer:
return False
return True
def set_initial_address(self):
""" Set address for starting thread """
if self.config is None:
self.addr_family = socket.AF_INET
self.kroot_local = get_local_addr_str(self.addr_family, self.default_iface)
return
# Default address is localhost
kroot_addr = None
for k, v in self.config:
if k == 'stub-addr':
kroot_addr = v
if kroot_addr is not None:
if self.check_family(kroot_addr, socket.AF_INET):
self.addr_family = socket.AF_INET
self.kroot_local = kroot_addr
elif self.check_family(kroot_addr, socket.AF_INET6):
self.addr_family = socket.AF_INET6
self.kroot_local = kroot_addr
else:
self.addr_family = socket.AF_INET
self.kroot_local = get_local_addr_str(self.addr_family, self.default_iface)
def address(self):
""" Returns opened sockets list """
addrlist = []
......@@ -184,20 +159,18 @@ class TestServer:
for sock in to_error:
raise Exception("[query_io] Socket IO error {}, exit".format(sock.getsockname()))
def start_srv(self, address=None, family=socket.AF_INET, proto=socket.IPPROTO_UDP):
def start_srv(self, address, family, proto=socket.IPPROTO_UDP):
""" Starts listening thread if necessary """
assert address
assert address[0] # host
assert address[1] # port
assert family
assert proto
if family == socket.AF_INET:
if address[0] is None:
address = (get_local_addr_str(family, self.default_iface), 53)
elif family == socket.AF_INET6:
if socket.has_ipv6 is not True:
if family == socket.AF_INET6:
if not socket.has_ipv6:
raise NotImplementedError("[start_srv] IPv6 is not supported by socket {0}"
.format(socket))
if address[0] is None:
address = (get_local_addr_str(family, self.default_iface), 53)
else:
elif family != socket.AF_INET:
raise NotImplementedError("[start_srv] unsupported protocol family {0}".format(family))
if proto == socket.IPPROTO_TCP:
......@@ -251,8 +224,7 @@ class TestServer:
self.start_srv((rd.address, 53), socket.AF_INET6)
def play(self, subject_addr):
paddr = get_local_addr_str(self.scenario.sockfamily, subject_addr)
self.scenario.play({'': (paddr, 53)})
self.scenario.play({'': (subject_addr, 53)})
def empty_test_case():
......@@ -275,7 +247,8 @@ def empty_test_case():
test_scenario.steps.append(step)
test_scenario.current_step = step
test_config = [('stub-addr', '127.0.0.10')]
test_config = {'ROOT_ADDR': '127.0.0.10',
'_SOCKET_FAMILY': socket.AF_INET}
return (test_scenario, test_config)
......@@ -295,7 +268,8 @@ def standalone_self_test():
required=False, type=int)
args = argparser.parse_args()
if args.scenario:
test_scenario, test_config = scenario.parse_file(fileinput.input(args.scenario))
test_scenario, test_config_text = scenario.parse_file(fileinput.input(args.scenario))
test_config = scenario.parse_config(test_config_text, True, os.getcwd())
else:
test_scenario, test_config = empty_test_case()
......@@ -308,15 +282,7 @@ def standalone_self_test():
else:
test_scenario.current_step = test_scenario.steps[0]
DEFAULT_IFACE = 0
CHILD_IFACE = 0
if "SOCKET_WRAPPER_DEFAULT_IFACE" in os.environ:
DEFAULT_IFACE = int(os.environ["SOCKET_WRAPPER_DEFAULT_IFACE"])
if DEFAULT_IFACE < 2 or DEFAULT_IFACE > 254:
DEFAULT_IFACE = 10
os.environ["SOCKET_WRAPPER_DEFAULT_IFACE"] = "{}".format(DEFAULT_IFACE)
server = TestServer(test_scenario, test_config, DEFAULT_IFACE)
server = TestServer(test_scenario, test_config['ROOT_ADDR'], test_config['_SOCKET_FAMILY'])
server.start()
logging.info("[==========] Mirror server running at %s", server.address())
......
......@@ -19,7 +19,8 @@ kwargs = {
'packages': ['pydnstest'],
'install_requires': [
'dnspython',
'jinja2'
'jinja2',
'PyYAML'
],
'classifiers': [
"Intended Audience :: Developers",
......
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