Commit b8e60548 authored by Marek Vavrusa's avatar Marek Vavrusa

pydnstest: useable with no socketwrapper, cleanup

the pydnstest is a generic library
scenario is not responsible for any socket binding
testserver provides mock servers
deckard picks eligible addresses from scenario,
and tells testserver to listen there

this makes the library standalone useable even
without prebinding
parent 8a80055c
...@@ -11,6 +11,7 @@ import signal ...@@ -11,6 +11,7 @@ import signal
import stat import stat
import errno import errno
import jinja2 import jinja2
import dns.rdatatype
from pydnstest import scenario, testserver, test from pydnstest import scenario, testserver, test
from datetime import datetime from datetime import datetime
import random import random
...@@ -276,12 +277,13 @@ def setup_env(scenario, child_env, config, config_name_list, j2template_list): ...@@ -276,12 +277,13 @@ def setup_env(scenario, child_env, config, config_name_list, j2template_list):
childaddr = testserver.get_local_addr_str(self_sockfamily, CHILD_IFACE) childaddr = testserver.get_local_addr_str(self_sockfamily, CHILD_IFACE)
# Prebind to sockets to create necessary files # Prebind to sockets to create necessary files
# @TODO: this is probably a workaround for socket_wrapper bug # @TODO: this is probably a workaround for socket_wrapper bug
for sock_type in (socket.SOCK_STREAM, socket.SOCK_DGRAM): if 'NOPRELOAD' not in os.environ:
sock = socket.socket(self_sockfamily, sock_type) for sock_type in (socket.SOCK_STREAM, socket.SOCK_DGRAM):
sock.setsockopt(self_sockfamily, socket.SO_REUSEADDR, 1) sock = socket.socket(self_sockfamily, sock_type)
sock.bind((childaddr, 53)) sock.setsockopt(self_sockfamily, socket.SO_REUSEADDR, 1)
if sock_type == socket.SOCK_STREAM: sock.bind((childaddr, 53))
sock.listen(5) if sock_type == socket.SOCK_STREAM:
sock.listen(5)
# Generate configuration files # Generate configuration files
j2template_loader = jinja2.FileSystemLoader(searchpath=os.path.dirname(os.path.abspath(__file__))) j2template_loader = jinja2.FileSystemLoader(searchpath=os.path.dirname(os.path.abspath(__file__)))
j2template_env = jinja2.Environment(loader=j2template_loader) j2template_env = jinja2.Environment(loader=j2template_loader)
...@@ -305,19 +307,13 @@ def play_object(path, binary_name, config_name, j2template, binary_additional_pa ...@@ -305,19 +307,13 @@ def play_object(path, binary_name, config_name, j2template, binary_additional_pa
""" Play scenario from a file object. """ """ Play scenario from a file object. """
# Parse scenario # Parse scenario
file_in = fileinput.input(path) case, config = scenario.parse_file(fileinput.input(path))
scenario = None
config = None
try:
scenario, config = parse_file(file_in)
finally:
file_in.close()
# Setup daemon environment # Setup daemon environment
daemon_env = os.environ.copy() daemon_env = os.environ.copy()
setup_env(scenario, daemon_env, config, config_name, j2template) setup_env(case, daemon_env, config, config_name, j2template)
server = testserver.TestServer(scenario, config, DEFAULT_IFACE, CHILD_IFACE) server = testserver.TestServer(case, config, DEFAULT_IFACE)
server.start() server.start()
# Start binary # Start binary
...@@ -328,15 +324,18 @@ def play_object(path, binary_name, config_name, j2template, binary_additional_pa ...@@ -328,15 +324,18 @@ def play_object(path, binary_name, config_name, j2template, binary_additional_pa
daemon_proc = subprocess.Popen(daemon_args, stdout=daemon_log, stderr=daemon_log, daemon_proc = subprocess.Popen(daemon_args, stdout=daemon_log, stderr=daemon_log,
cwd=TMPDIR, preexec_fn=os.setsid, env=daemon_env) cwd=TMPDIR, preexec_fn=os.setsid, env=daemon_env)
except Exception as e: except Exception as e:
server.stop()
raise Exception("Can't start '%s': %s" % (daemon_args, str(e))) raise Exception("Can't start '%s': %s" % (daemon_args, str(e)))
# Wait until the server accepts TCP clients # Wait until the server accepts TCP clients
sockfamily = socket.AF_INET sockfamily = socket.AF_INET
if scenario.force_ipv6 == True: if case.force_ipv6 == True:
sockfamily = socket.AF_INET6 sockfamily = socket.AF_INET6
sock = socket.socket(sockfamily, socket.SOCK_STREAM) sock = socket.socket(sockfamily, socket.SOCK_STREAM)
while True: while True:
time.sleep(0.1) time.sleep(0.1)
if daemon_proc.poll() != None: if daemon_proc.poll() != None:
server.stop()
print(open('%s/server.log' % TMPDIR).read()) print(open('%s/server.log' % TMPDIR).read())
raise Exception('process died "%s", logs in "%s"' % (os.path.basename(binary_name), TMPDIR)) raise Exception('process died "%s", logs in "%s"' % (os.path.basename(binary_name), TMPDIR))
try: try:
...@@ -345,9 +344,24 @@ def play_object(path, binary_name, config_name, j2template, binary_additional_pa ...@@ -345,9 +344,24 @@ def play_object(path, binary_name, config_name, j2template, binary_additional_pa
break break
sock.close() sock.close()
# Play scenario # Bind to test servers
for r in case.ranges:
family = socket.AF_INET6 if ':' in r.address else socket.AF_INET
server.start_srv((r.address, 53), family)
# Bind addresses in ad-hoc REPLYs
for s in case.steps:
if s.type == 'REPLY':
reply = s.data[0].message
for rr in itertools.chain(reply.answer,reply.additional,reply.question,reply.authority):
for rd in rr:
if rd.rdtype == dns.rdatatype.A:
server.start_srv((rd.address, 53), socket.AF_INET)
elif rd.rdtype == dns.rdatatype.AAAA:
server.start_srv((rd.address, 53), socket.AF_INET6)
# Play test scenario
try: try:
server.play() server.play(CHILD_IFACE)
if VERBOSE: if VERBOSE:
print(open('%s/server.log' % TMPDIR).read()) print(open('%s/server.log' % TMPDIR).read())
except: except:
......
...@@ -306,12 +306,12 @@ class Step: ...@@ -306,12 +306,12 @@ class Step:
""" Append a data entry to this step. """ """ Append a data entry to this step. """
self.data.append(entry) self.data.append(entry)
def play(self, ctx, peeraddr): def play(self, ctx):
""" Play one step from a scenario. """ """ Play one step from a scenario. """
dtag = '[ STEP %03d ] %s' % (self.id, self.type) dtag = '[ STEP %03d ] %s' % (self.id, self.type)
if self.type == 'QUERY': if self.type == 'QUERY':
dprint(dtag, self.data[0].message.to_text()) dprint(dtag, self.data[0].message.to_text())
return self.__query(ctx, peeraddr) return self.__query(ctx)
elif self.type == 'CHECK_OUT_QUERY': elif self.type == 'CHECK_OUT_QUERY':
dprint(dtag, '') dprint(dtag, '')
pass # Ignore pass # Ignore
...@@ -342,7 +342,7 @@ class Step: ...@@ -342,7 +342,7 @@ class Step:
dprint("[ __check_answer ]", ctx.last_answer.to_text()) dprint("[ __check_answer ]", ctx.last_answer.to_text())
expected.match(ctx.last_answer) expected.match(ctx.last_answer)
def __query(self, ctx, peeraddr): def __query(self, ctx):
""" Resolve a query. """ """ Resolve a query. """
if len(self.data) == 0: if len(self.data) == 0:
raise Exception("query definition required") raise Exception("query definition required")
...@@ -416,7 +416,7 @@ class Scenario: ...@@ -416,7 +416,7 @@ class Scenario:
return (rng.reply(query), False) return (rng.reply(query), False)
# Find any prescripted one-shot replies # Find any prescripted one-shot replies
for step in self.steps: for step in self.steps:
if step.id <= step_id or step.type != 'REPLY': if step.id < step_id or step.type != 'REPLY':
continue continue
try: try:
candidate = step.data[0] candidate = step.data[0]
...@@ -432,11 +432,11 @@ class Scenario: ...@@ -432,11 +432,11 @@ class Scenario:
pass pass
return (None, True) return (None, True)
def play(self, family, saddr, paddr): def play(self, family, paddr):
""" Play given scenario. """ """ Play given scenario. """
self.child_sock = socket.socket(family, socket.SOCK_DGRAM) self.child_sock = socket.socket(family, socket.SOCK_DGRAM)
self.child_sock.settimeout(3) self.child_sock.settimeout(3)
self.child_sock.connect((paddr, 53)) self.child_sock.connect(paddr)
if len(self.steps) == 0: if len(self.steps) == 0:
raise ('no steps in this scenario') raise ('no steps in this scenario')
...@@ -448,7 +448,7 @@ class Scenario: ...@@ -448,7 +448,7 @@ class Scenario:
step = self.steps[i] step = self.steps[i]
self.current_step = step self.current_step = step
try: try:
step.play(self, paddr) step.play(self)
except Exception as e: except Exception as e:
if (step.repeat_if_fail > 0): if (step.repeat_if_fail > 0):
dprint ('[play]',"step %d: exception catched - '%s', retrying step %d (%d left)" % (step.id, e, step.next_if_fail, step.repeat_if_fail)) dprint ('[play]',"step %d: exception catched - '%s', retrying step %d (%d left)" % (step.id, e, step.next_if_fail, step.repeat_if_fail))
......
...@@ -65,7 +65,7 @@ class AddrMapInfo: ...@@ -65,7 +65,7 @@ class AddrMapInfo:
class TestServer: class TestServer:
""" This simulates UDP DNS server returning scripted or mirror DNS responses. """ """ This simulates UDP DNS server returning scripted or mirror DNS responses. """
def __init__(self, scenario, config, d_iface, p_iface): def __init__(self, scenario, config, d_iface):
""" Initialize server instance. """ """ Initialize server instance. """
self.thread = None self.thread = None
self.srv_socks = [] self.srv_socks = []
...@@ -78,9 +78,8 @@ class TestServer: ...@@ -78,9 +78,8 @@ class TestServer:
self.start_iface = 2 self.start_iface = 2
self.cur_iface = self.start_iface self.cur_iface = self.start_iface
self.kroot_local = None self.kroot_local = None
self.kroot_family = None self.addr_family = None
self.default_iface = d_iface self.default_iface = d_iface
self.peer_iface = p_iface
self.set_initial_address() self.set_initial_address()
def __del__(self): def __del__(self):
...@@ -88,13 +87,13 @@ class TestServer: ...@@ -88,13 +87,13 @@ class TestServer:
if self.active is True: if self.active is True:
self.stop() self.stop()
def start(self): def start(self, port = 53):
""" Synchronous start """ """ Synchronous start """
if self.active is True: if self.active is True:
raise Exception('TestServer already started') raise Exception('TestServer already started')
self.active = True self.active = True
self.start_srv(self.kroot_local, self.kroot_family) self.addr, _ = self.start_srv((self.kroot_local, port), self.addr_family)
self.start_srv(self.kroot_local, self.kroot_family, socket.IPPROTO_TCP) self.start_srv(self.addr, self.addr_family, socket.IPPROTO_TCP)
def stop(self): def stop(self):
""" Stop socket server operation. """ """ Stop socket server operation. """
...@@ -124,25 +123,24 @@ class TestServer: ...@@ -124,25 +123,24 @@ class TestServer:
def set_initial_address(self): def set_initial_address(self):
""" Set address for starting thread """ """ Set address for starting thread """
if self.config is None: if self.config is None:
self.kroot_family = socket.AF_INET self.addr_family = socket.AF_INET
self.kroot_local = get_local_addr_str(self.kroot_family, self.default_iface) self.kroot_local = get_local_addr_str(self.addr_family, self.default_iface)
return return
# Default address is localhost
kroot_addr = None kroot_addr = None
for k, v in self.config: for k, v in self.config:
if k == 'stub-addr': if k == 'stub-addr':
kroot_addr = v kroot_addr = v
if kroot_addr is not None: if kroot_addr is not None:
if self.check_family (kroot_addr, socket.AF_INET): if self.check_family (kroot_addr, socket.AF_INET):
self.kroot_family = socket.AF_INET self.addr_family = socket.AF_INET
self.kroot_local = kroot_addr self.kroot_local = kroot_addr
elif self.check_family (kroot_addr, socket.AF_INET6): elif self.check_family (kroot_addr, socket.AF_INET6):
self.kroot_family = socket.AF_INET6 self.addr_family = socket.AF_INET6
self.kroot_local = kroot_addr self.kroot_local = kroot_addr
else:
raise Exception("[set_initial_adress] Invalid 'stub-addr' address (%s), must be IPv4 or IPv6, check the config")
else: else:
self.kroot_family = socket.AF_INET self.addr_family = socket.AF_INET
self.kroot_local = get_local_addr_str(self.kroot_family, self.default_iface) self.kroot_local = get_local_addr_str(self.addr_family, self.default_iface)
def address(self): def address(self):
""" Returns opened sockets list """ """ Returns opened sockets list """
...@@ -164,13 +162,7 @@ class TestServer: ...@@ -164,13 +162,7 @@ class TestServer:
response, is_raw_data = self.scenario.reply(query, client_address) response, is_raw_data = self.scenario.reply(query, client_address)
if response: if response:
if is_raw_data is False: if is_raw_data is False:
for rr in itertools.chain(response.answer,response.additional,response.question,response.authority): data_to_wire = response.to_wire(max_size = 65535)
for rd in rr:
if rd.rdtype == dns.rdatatype.A:
self.start_srv(rd.address, socket.AF_INET)
elif rd.rdtype == dns.rdatatype.AAAA:
self.start_srv(rd.address, socket.AF_INET6)
data_to_wire = response.to_wire()
dprint ("[ handle_query ]", "response\n%s" % response) dprint ("[ handle_query ]", "response\n%s" % response)
else: else:
data_to_wire = response data_to_wire = response
...@@ -208,19 +200,18 @@ class TestServer: ...@@ -208,19 +200,18 @@ class TestServer:
for sock in to_error: for sock in to_error:
raise Exception("[query_io] Socket IO error {}, exit".format(sock.getsockname())) 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, port = 53): def start_srv(self, address = None, family = socket.AF_INET, proto = socket.IPPROTO_UDP):
""" Starts listening thread if necessary """ """ Starts listening thread if necessary """
if family == None: if family == None:
family = socket.AF_INET family = socket.AF_INET
if family == socket.AF_INET: if family == socket.AF_INET:
if address == '' or address is None: if address[0] is None:
address = get_local_addr_str(family, self.default_iface) address = (get_local_addr_str(family, self.default_iface), 53)
elif family == socket.AF_INET6: elif family == socket.AF_INET6:
if socket.has_ipv6 is not True: if socket.has_ipv6 is not True:
raise Exception("[start_srv] IPV6 is not supported") raise Exception("[start_srv] IPV6 is not supported")
if address == '' or address is None: if address[0] is None:
address = get_local_addr_str(family, self.default_iface) address = (get_local_addr_str(family, self.default_iface), 53)
else: else:
raise Exception("[start_srv] unsupported protocol family {family}".format(family=family)) raise Exception("[start_srv] unsupported protocol family {family}".format(family=family))
...@@ -233,21 +224,16 @@ class TestServer: ...@@ -233,21 +224,16 @@ class TestServer:
else: else:
raise Exception("[start_srv] unsupported protocol {protocol}".format(protocol=proto)) raise Exception("[start_srv] unsupported protocol {protocol}".format(protocol=proto))
if port == 0 or port is None:
port = 53
if (self.thread is None): if (self.thread is None):
self.thread = threading.Thread(target=self.query_io) self.thread = threading.Thread(target=self.query_io)
self.thread.start() self.thread.start()
for srv_sock in self.srv_socks: for srv_sock in self.srv_socks:
if srv_sock.family == family and srv_sock.getsockname()[0] == address and srv_sock.proto == proto: if srv_sock.family == family and srv_sock.getsockname() == address and srv_sock.proto == proto:
return srv_sock.getsockname() return srv_sock.getsockname()
addr_info = socket.getaddrinfo(address,port,family,socktype,proto)
sock = socket.socket(family, socktype, proto) sock = socket.socket(family, socktype, proto)
sockaddr = addr_info[0][-1] sock.bind(address)
sock.bind(sockaddr)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
if proto == socket.IPPROTO_TCP: if proto == socket.IPPROTO_TCP:
sock.listen(5) sock.listen(5)
...@@ -255,13 +241,12 @@ class TestServer: ...@@ -255,13 +241,12 @@ class TestServer:
sockname = sock.getsockname() sockname = sock.getsockname()
return sockname, proto return sockname, proto
def play(self): def play(self, subject_addr):
sockfamily = socket.AF_INET sockfamily = socket.AF_INET
if self.scenario.force_ipv6 == True: if self.scenario.force_ipv6 == True:
sockfamily = socket.AF_INET6 sockfamily = socket.AF_INET6
saddr = get_local_addr_str(sockfamily,self.default_iface) paddr = get_local_addr_str(sockfamily, subject_addr)
paddr = get_local_addr_str(sockfamily,self.peer_iface) self.scenario.play(sockfamily, (paddr, 53))
self.scenario.play(sockfamily,saddr,paddr)
if __name__ == '__main__': if __name__ == '__main__':
# Self-test code # Self-test code
...@@ -273,7 +258,7 @@ if __name__ == '__main__': ...@@ -273,7 +258,7 @@ if __name__ == '__main__':
DEFAULT_IFACE = 10 DEFAULT_IFACE = 10
os.environ["SOCKET_WRAPPER_DEFAULT_IFACE"]="{}".format(DEFAULT_IFACE) os.environ["SOCKET_WRAPPER_DEFAULT_IFACE"]="{}".format(DEFAULT_IFACE)
# Mirror server # Mirror server
server = TestServer(None,None,DEFAULT_IFACE,DEFAULT_IFACE) server = TestServer(None,None,DEFAULT_IFACE)
server.start() server.start()
print "[==========] Mirror server running at", server.address() print "[==========] Mirror server running at", server.address()
try: try:
......
...@@ -7,9 +7,14 @@ CONFIG_END ...@@ -7,9 +7,14 @@ CONFIG_END
SCENARIO_BEGIN Test resolution with dependency cycle SCENARIO_BEGIN Test resolution with dependency cycle
; query for ns.example.com, needs ns.example.net, needs ns.example.com. ; query for ns.example.com, needs ns.example.net, needs ns.example.com.
; Invalid server
RANGE_BEGIN 0 100
ADDRESS 1.2.3.1
RANGE_END
; K.ROOT-SERVERS.NET. ; K.ROOT-SERVERS.NET.
RANGE_BEGIN 0 100 RANGE_BEGIN 0 100
ADDRESS 193.0.14.129 ADDRESS 193.0.14.129
ENTRY_BEGIN ENTRY_BEGIN
MATCH opcode qtype qname MATCH opcode qtype qname
ADJUST copy_id ADJUST copy_id
......
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