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