From: Grigorii Demidov Date: Thu, 19 Nov 2015 12:26:46 +0000 (+0100) Subject: tests: integration tests with deckard X-Git-Tag: v1.0.0-beta2~2^2~4 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=e4868c375fa0dc8151f055b1819bdcb52cfc565f;p=thirdparty%2Fknot-resolver.git tests: integration tests with deckard --- diff --git a/.gitmodules b/.gitmodules index a279829bd..15c757c36 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,9 @@ -[submodule "contrib/socket_wrapper"] - path = contrib/socket_wrapper - url = git://git.samba.org/socket_wrapper.git [submodule "contrib/libfaketime"] path = contrib/libfaketime url = https://github.com/wolfcw/libfaketime.git +[submodule "tests/integration"] + path = tests/integration + url = https://gitlab.labs.nic.cz/knot/deckard.git +[submodule "contrib/libswrap"] + path = contrib/libswrap + url = https://gitlab.labs.nic.cz/labs/socket_wrapper.git diff --git a/contrib/libswrap b/contrib/libswrap new file mode 160000 index 000000000..96f9d5854 --- /dev/null +++ b/contrib/libswrap @@ -0,0 +1 @@ +Subproject commit 96f9d58540f1ae6f219e85ce01a5818146bb49ee diff --git a/tests/integration b/tests/integration new file mode 160000 index 000000000..9f38df11a --- /dev/null +++ b/tests/integration @@ -0,0 +1 @@ +Subproject commit 9f38df11af910ec2a0c09e6c49507e3c2877f4c8 diff --git a/tests/integration.mk b/tests/integration.mk index 8f8795859..b07920076 100644 --- a/tests/integration.mk +++ b/tests/integration.mk @@ -2,31 +2,53 @@ # Integration tests # -TESTS ?= tests/testdata -CWRAP_PATH := $(strip $(socket_wrapper_LIBS)) +# Path to scenario files +TESTS=tests/integration/sets/resolver +# Path to daemon +DAEMON=kresd +# Template file name +TEMPLATE=template/kresd.j2 +# Config file name +CONFIG=config # Targets +deckard_DIR := tests/integration +deckard := $(libfaketime_DIR)/deckard.py + libfaketime_DIR := contrib/libfaketime libfaketime := $(abspath $(libfaketime_DIR))/src/libfaketime$(LIBEXT).1 +libswrap_DIR := contrib/libswrap +libswrap_cmake_DIR := $(libswrap_DIR)/obj +libswrap=$(abspath $(libswrap_cmake_DIR))/src/libsocket_wrapper$(LIBEXT).0 + # Platform-specific targets ifeq ($(PLATFORM),Darwin) libfaketime := $(abspath $(libfaketime_DIR))/src/libfaketime.1$(LIBEXT) - preload_syms := DYLD_FORCE_FLAT_NAMESPACE=1 DYLD_INSERT_LIBRARIES="$(libfaketime):$(CWRAP_PATH)" + libswrap=$(abspath $(libswrap_cmake_DIR))/src/libsocket_wrapper.0$(LIBEXT) + preload_syms := DYLD_FORCE_FLAT_NAMESPACE=1 DYLD_INSERT_LIBRARIES="$(libfaketime):$(libswrap)" else - preload_syms := LD_PRELOAD="$(libfaketime):$(CWRAP_PATH)" + preload_syms := LD_PRELOAD="$(libfaketime):$(libswrap)" endif # Synchronize submodules -$(libfaketime_DIR): - @git submodule init -$(libfaketime_DIR)/Makefile: $(libfaketime_DIR) - @git submodule update +$(deckard): + @git submodule update --init +$(libfaketime_DIR)/Makefile: + @git submodule update --init # Build libfaketime contrib $(libfaketime): $(libfaketime_DIR)/Makefile @CFLAGS="" $(MAKE) -C $(libfaketime_DIR) +$(libswrap_DIR): + @git submodule update --init +$(libswrap_cmake_DIR):$(libswrap_DIR) + mkdir $(libswrap_cmake_DIR) +$(libswrap_cmake_DIR)/Makefile: $(libswrap_cmake_DIR) + @cd $(libswrap_cmake_DIR); cmake .. +$(libswrap): $(libswrap_cmake_DIR)/Makefile + @CFLAGS="-O2 -g" $(MAKE) -C $(libswrap_cmake_DIR) -check-integration: $(libfaketime) - @$(preload_LIBS) $(preload_syms) python tests/test_integration.py $(TESTS) $(abspath daemon/kresd) ./kresd.j2 config +check-integration: $(deckard) $(libswrap) $(libfaketime) + $(preload_syms) tests/integration/deckard.py $(TESTS) $(DAEMON) $(TEMPLATE) $(CONFIG) $(ADDITIONAL) .PHONY: check-integration diff --git a/tests/kresd.j2 b/tests/kresd.j2 deleted file mode 100644 index 7ddcccee9..000000000 --- a/tests/kresd.j2 +++ /dev/null @@ -1,26 +0,0 @@ -net = { '{{SELF_ADDR}}' } -modules = {'stats', 'policy', 'hints'} -cache.size = 1*MB -option('NO_MINIMIZE', {{NO_MINIMIZE}}) -option('ALLOW_LOCAL', true) -hints.config('/dev/null') -hints.root({['k.root-servers.net'] = '{{ROOT_ADDR}}'}) -trust_anchors.add('{{TRUST_ANCHOR}}') -verbose(true) - --- Self-checks on globals -assert(help() ~= nil) -assert(worker.id ~= nil) --- Self-checks on facilities -assert(cache.count() == 0) -assert(cache.stats() ~= nil) -assert(cache.backends() ~= nil) -assert(worker.stats() ~= nil) -assert(net.interfaces() ~= nil) --- Self-checks on loaded stuff -assert(net.list()['{{SELF_ADDR}}']) -assert(#modules.list() > 0) --- Self-check timers -ev = event.recurrent(1 * sec, function (ev) return 1 end) -event.cancel(ev) -ev = event.after(0, function (ev) return 1 end) diff --git a/tests/pydnstest/__init__.py b/tests/pydnstest/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/pydnstest/requirements.txt b/tests/pydnstest/requirements.txt deleted file mode 100644 index 7da86edbb..000000000 --- a/tests/pydnstest/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -dnspython==1.11 diff --git a/tests/pydnstest/scenario.py b/tests/pydnstest/scenario.py deleted file mode 100644 index 17b1d85a1..000000000 --- a/tests/pydnstest/scenario.py +++ /dev/null @@ -1,411 +0,0 @@ -import dns.message -import dns.rrset -import dns.rcode -import dns.dnssec -import binascii -import socket -import os -import itertools -import time -from datetime import datetime - -def dprint(msg): - """ Verbose logging (if enabled). """ - if 'VERBOSE' in os.environ: - print(msg) - -class Entry: - """ - Data entry represents scripted message and extra metadata, notably match criteria and reply adjustments. - """ - - # Globals - default_ttl = 3600 - default_cls = 'IN' - default_rc = 'NOERROR' - - def __init__(self): - """ Initialize data entry. """ - self.match_fields = ['opcode', 'qtype', 'qname'] - self.adjust_fields = ['copy_id'] - self.origin = '.' - self.message = dns.message.Message() - self.message.use_edns(edns = 0, payload = 4096) - self.sections = [] - self.is_raw_data_entry = False - self.raw_data_pending = False - self.raw_data = None - - def match_part(self, code, msg): - """ Compare scripted reply to given message using single criteria. """ - if code not in self.match_fields and 'all' not in self.match_fields: - return True - expected = dns.message.from_text(self.message.to_text()) - if code == 'opcode': - return self.__compare_val(expected.opcode(), msg.opcode()) - elif code == 'qtype': - if len(expected.question) == 0: - return True - return self.__compare_val(expected.question[0].rdtype, msg.question[0].rdtype) - elif code == 'qname': - if len(expected.question) == 0: - return True - qname = dns.name.from_text(msg.question[0].name.to_text().lower()) - return self.__compare_val(expected.question[0].name, qname) - elif code == 'subdomain': - if len(expected.question) == 0: - return True - qname = dns.name.from_text(msg.question[0].name.to_text().lower()) - return self.__compare_sub(expected.question[0].name, qname) - elif code == 'flags': - return self.__compare_val(dns.flags.to_text(expected.flags), dns.flags.to_text(msg.flags)) - elif code == 'question': - return self.__compare_rrs(expected.question, msg.question) - elif code == 'answer': - return self.__compare_rrs(expected.answer, msg.answer) - elif code == 'authority': - return self.__compare_rrs(expected.authority, msg.authority) - elif code == 'additional': - return self.__compare_rrs(expected.additional, msg.additional) - else: - raise Exception('unknown match request "%s"' % code) - - def match(self, msg): - """ Compare scripted reply to given message based on match criteria. """ - match_fields = self.match_fields - if 'all' in match_fields: - match_fields = tuple(['flags'] + self.sections) - for code in match_fields: - try: - res = self.match_part(code, msg) - except Exception as e: - raise Exception("%s: %s" % (code, str(e))) - - def cmp_raw(self, raw_value): - if self.is_raw_data_entry is False: - raise Exception("entry.cmp_raw() misuse") - expected = None - if self.raw_data is not None: - expected = binascii.hexlify(self.raw_data) - got = None - if raw_value is not None: - got = binascii.hexlify(raw_value) - if expected != got: - print("expected '",expected,"', got '",got,"'") - raise Exception("comparsion failed") - - def set_match(self, fields): - """ Set conditions for message comparison [all, flags, question, answer, authority, additional] """ - self.match_fields = fields - - def adjust_reply(self, query): - """ Copy scripted reply and adjust to received query. """ - answer = dns.message.from_text(self.message.to_text()) - answer.use_edns(query.edns, query.ednsflags) - if 'copy_id' in self.adjust_fields: - answer.id = query.id - # Copy letter-case if the template has QD - if len(answer.question) > 0: - answer.question[0].name = query.question[0].name - if 'copy_query' in self.adjust_fields: - answer.question = query.question - return answer - - def set_adjust(self, fields): - """ Set reply adjustment fields [copy_id, copy_query] """ - self.adjust_fields = fields - - def set_reply(self, fields): - """ Set reply flags and rcode. """ - eflags = [] - flags = [] - rcode = dns.rcode.from_text(self.default_rc) - for code in fields: - if code == 'DO': - eflags.append(code) - continue - try: - rcode = dns.rcode.from_text(code) - except: - flags.append(code) - self.message.flags = dns.flags.from_text(' '.join(flags)) - self.message.want_dnssec('DO' in eflags) - self.message.set_rcode(rcode) - - def begin_raw(self): - """ Set raw data pending flag. """ - self.raw_data_pending = True - - def begin_section(self, section): - """ Begin packet section. """ - self.section = section - self.sections.append(section.lower()) - - def add_record(self, owner, args): - """ Add record to current packet section. """ - if self.raw_data_pending is True: - if self.raw_data == None: - if owner == 'NULL': - self.raw_data = None - else: - self.raw_data = binascii.unhexlify(owner) - else: - raise Exception('raw data already set in this entry') - self.raw_data_pending = False - self.is_raw_data_entry = True - else: - rr = self.__rr_from_str(owner, args) - if self.section == 'QUESTION': - self.__rr_add(self.message.question, rr) - elif self.section == 'ANSWER': - self.__rr_add(self.message.answer, rr) - elif self.section == 'AUTHORITY': - self.__rr_add(self.message.authority, rr) - elif self.section == 'ADDITIONAL': - self.__rr_add(self.message.additional, rr) - else: - raise Exception('bad section %s' % self.section) - - def __rr_add(self, section, rr): - """ Merge record to existing RRSet, or append to given section. """ - for existing_rr in section: - if existing_rr.match(rr.name, rr.rdclass, rr.rdtype, 0): - existing_rr += rr - return - section.append(rr) - - def __rr_from_str(self, owner, args): - """ Parse RR from tokenized string. """ - if not owner.endswith('.'): - owner += self.origin - ttl = self.default_ttl - rdclass = self.default_cls - try: - ttl = dns.ttl.from_text(args[0]) - args.pop(0) - except: - pass # optional - try: - rdclass = dns.rdataclass.from_text(args[0]) - args.pop(0) - except: - pass # optional - rdtype = args.pop(0) - rr = dns.rrset.from_text(owner, ttl, rdclass, rdtype) - if len(args) > 0: - if (rr.rdtype == dns.rdatatype.DS): - # convert textual algorithm identifier to number - args[1] = str(dns.dnssec.algorithm_from_text(args[1])) - rd = dns.rdata.from_text(rr.rdclass, rr.rdtype, ' '.join(args), origin=dns.name.from_text(self.origin), relativize=False) - rr.add(rd) - return rr - - def __compare_rrs(self, expected, got): - """ Compare lists of RR sets, throw exception if different. """ - for rr in expected: - if rr not in got: - raise Exception("expected record '%s'" % rr.to_text()) - for rr in got: - if rr not in expected: - raise Exception("unexpected record '%s'" % rr.to_text()) - return True - - def __compare_val(self, expected, got): - """ Compare values, throw exception if different. """ - if expected != got: - raise Exception("expected '%s', got '%s'" % (expected, got)) - return True - - def __compare_sub(self, got, expected): - """ Check if got subdomain of expected, throw exception if different. """ - if not expected.is_subdomain(got): - raise Exception("expected subdomain of '%s', got '%s'" % (expected, got)) - return True - - -class Range: - """ - Range represents a set of scripted queries valid for given step range. - """ - - def __init__(self, a, b): - """ Initialize reply range. """ - self.a = a - self.b = b - self.address = None - self.stored = [] - - def add(self, entry): - """ Append a scripted response to the range""" - self.stored.append(entry) - - def eligible(self, id, address): - """ Return true if this range is eligible for fetching reply. """ - if self.a <= id <= self.b: - return None in (self.address, address) or (self.address == address) - return False - - def reply(self, query): - """ Find matching response to given query. """ - for candidate in self.stored: - try: - candidate.match(query) - return candidate.adjust_reply(query) - except Exception as e: - pass - return None - - -class Step: - """ - Step represents one scripted action in a given moment, - each step has an order identifier, type and optionally data entry. - """ - - require_data = ['TIME_PASSES'] - - def __init__(self, id, type, extra_args): - """ Initialize single scenario step. """ - self.id = int(id) - self.type = type - self.args = extra_args - self.data = [] - self.has_data = self.type not in Step.require_data - self.answer = None - self.raw_answer = None - - def add(self, entry): - """ Append a data entry to this step. """ - self.data.append(entry) - - def play(self, ctx, peeraddr): - """ Play one step from a scenario. """ - dprint('[ STEP %03d ] %s' % (self.id, self.type)) - if self.type == 'QUERY': - dprint(self.data[0].message.to_text()) - return self.__query(ctx, peeraddr) - elif self.type == 'CHECK_OUT_QUERY': - pass # Ignore - elif self.type == 'CHECK_ANSWER': - return self.__check_answer(ctx) - elif self.type == 'TIME_PASSES': - return self.__time_passes(ctx) - elif self.type == 'REPLY': - pass - else: - raise Exception('step %s unsupported' % self.type) - - def __check_answer(self, ctx): - """ Compare answer from previously resolved query. """ - if len(self.data) == 0: - raise Exception("response definition required") - expected = self.data[0] - if expected.is_raw_data_entry is True: - dprint(ctx.last_raw_answer.to_text()) - expected.cmp_raw(ctx.last_raw_answer) - else: - if ctx.last_answer is None: - raise Exception("no answer from preceding query") - dprint(ctx.last_answer.to_text()) - expected.match(ctx.last_answer) - - def __query(self, ctx, peeraddr): - """ Resolve a query. """ - if len(self.data) == 0: - raise Exception("query definition required") - if self.data[0].is_raw_data_entry is True: - data_to_wire = self.data[0].raw_data - else: - # Don't use a message copy as the EDNS data portion is not copied. - data_to_wire = self.data[0].message.to_wire() - # Send query to client and wait for response - while True: - try: - ctx.child_sock.send(data_to_wire) - break - except OSError, e: - # ENOBUFS, throttle sending - if e.errno == errno.ENOBUFS: - time.sleep(0.1) - # Wait for a response for a reasonable time - answer = None - if not self.data[0].is_raw_data_entry: - answer, addr = ctx.child_sock.recvfrom(4096) - # Remember last answer for checking later - self.raw_answer = answer - ctx.last_raw_answer = answer - if self.raw_answer is not None: - self.answer = dns.message.from_wire(self.raw_answer) - else: - self.answer = None - ctx.last_answer = self.answer - - def __time_passes(self, ctx): - """ Modify system time. """ - time_file = open(os.environ["FAKETIME_TIMESTAMP_FILE"], 'r') - line = time_file.readline().strip() - time_file.close() - t = time.mktime(datetime.strptime(line, '%Y-%m-%d %H:%M:%S').timetuple()) - t += int(self.args[1]) - time_file = open(os.environ["FAKETIME_TIMESTAMP_FILE"], 'w') - time_file.write(datetime.fromtimestamp(t).strftime('%Y-%m-%d %H:%M:%S') + "\n") - time_file.close() - -class Scenario: - def __init__(self, info): - """ Initialize scenario with description. """ - self.info = info - self.ranges = [] - self.steps = [] - self.current_step = None - self.child_sock = None - - def reply(self, query, address = None): - """ Attempt to find a range reply for a query. """ - step_id = 0 - if self.current_step is not None: - step_id = self.current_step.id - # Unknown address, select any match - # TODO: workaround until the server supports stub zones - if address not in [rng.address for rng in self.ranges]: - address = None - # Find current valid query response range - for rng in self.ranges: - if rng.eligible(step_id, address): - 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': - continue - try: - candidate = step.data[0] - if candidate.is_raw_data_entry is False: - candidate.match(query) - step.data.remove(candidate) - answer = candidate.adjust_reply(query) - return (answer, False) - else: - answer = candidate.raw_data - return (answer, True) - except: - pass - return (None, True) - - def play(self, saddr, paddr): - """ Play given scenario. """ - self.child_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - self.child_sock.settimeout(2) - self.child_sock.connect((paddr, 53)) - - step = None - if len(self.steps) == 0: - raise ('no steps in this scenario') - try: - for step in self.steps: - self.current_step = step - step.play(self, paddr) - except Exception as e: - raise Exception('step #%d %s' % (step.id, str(e))) - finally: - self.child_sock.close() - self.child_sock = None diff --git a/tests/pydnstest/test.py b/tests/pydnstest/test.py deleted file mode 100644 index 54b94bec7..000000000 --- a/tests/pydnstest/test.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env python -import os -import traceback - -class Test: - """ Small library to imitate CMocka output. """ - - def __init__(self): - self.tests = [] - - def add(self, name, test, *args): - """ Add named test to set. """ - self.tests.append((name, test, args)) - - def run(self): - """ Run planned tests. """ - planned = len(self.tests) - passed = 0 - if planned == 0: - return - - print('[==========] Running %d test(s).' % planned) - for name, test_callback, args in self.tests: - print('[ RUN ] %s' % name) - try: - test_callback(*args) - passed += 1 - print('[ OK ] %s' % name) - except Exception as e: - print('[ FAIL ] %s (%s)' % (name, str(e))) - if 'VERBOSE' in os.environ: - print(traceback.format_exc()) - - # Clear test set - self.tests = [] - print('[==========] %d test(s) run.' % planned) - if passed == planned: - print('[ PASSED ] %d test(s).' % passed) - return 0 - else: - print('[ FAILED ] %d test(s).' % (planned - passed)) - return 1 diff --git a/tests/pydnstest/testserver.py b/tests/pydnstest/testserver.py deleted file mode 100644 index c0b854316..000000000 --- a/tests/pydnstest/testserver.py +++ /dev/null @@ -1,283 +0,0 @@ -import threading -import select -import socket -import os -import time -import dns.message -import dns.rdatatype -import itertools - -def recvfrom_msg(stream): - """ Receive DNS/UDP message. """ - try: - data, addr = stream.recvfrom(4096) - except: - return None, None - return dns.message.from_wire(data), addr - -def sendto_msg(stream, message, addr): - """ Send DNS/UDP message. """ - try: - stream.sendto(message, addr) - except: # Failure to respond is OK, resolver should recover - pass - -def get_local_addr_str(family, iface): - """ Returns pattern string for localhost address """ - if family == socket.AF_INET: - addr_local_pattern = "127.0.0.{}" - elif family == socket.AF_INET6: - addr_local_pattern = "fd00::5357:5f{:02X}" - else: - raise Exception("[get_local_addr_str] family not supported '%i'" % family) - return addr_local_pattern.format(iface) - -class SrvSock (socket.socket): - """ Socket with some additional info """ - def __init__(self, client_address, family=socket.AF_INET, type=socket.SOCK_DGRAM, proto=socket.IPPROTO_UDP): - self.client_address = client_address - socket.socket.__init__(self, family, type, proto) - -class AddrMapInfo: - """ Saves mapping info between adresses from rpl and cwrap adresses """ - def __init__(self, family, local, external): - self.family = family - self.local = local - self.external = external - -class TestServer: - """ This simulates UDP DNS server returning scripted or mirror DNS responses. """ - - def __init__(self, scenario, config, d_iface, p_iface): - """ Initialize server instance. """ - self.thread = None - self.srv_socks = [] - self.client_socks = [] - self.active = False - self.scenario = scenario - self.config = config - self.addr_map = [] - self.start_iface = 2 - self.cur_iface = self.start_iface - self.kroot_local = None - self.kroot_family = None - self.default_iface = d_iface - self.peer_iface = p_iface - self.map_adresses() - - def __del__(self): - """ Cleanup after deletion. """ - if self.active is True: - self.stop() - - def start(self): - """ Synchronous start """ - if self.active is True: - raise Exception('TestServer already started') - self.active = True - self.start_srv(self.kroot_local, self.kroot_family) - - def stop(self): - """ Stop socket server operation. """ - self.active = False - self.thread.join() - for srv_sock in self.srv_socks: - srv_sock.close() - for client_sock in self.client_socks: - client_sock.close() - self.client_socks = [] - self.srv_socks = [] - self.scenario = None - - def map_to_local(self, addr, family, iface): - """ Maps arbitrary IP to localhost for using with cwrap """ - addr_external = None - addr_local = None - addr_local_pattern = None - new_entry = None - try: - n = socket.inet_pton(family, addr) - addr_external = socket.inet_ntop(family, n) - except socket.error: - return addr_local, new_entry - for am in self.addr_map: - if am.family == family and am.external == addr_external: - addr_local = am.local - new_entry = False - if addr_local is None: - # Do not remap addresses already in local range - if addr.startswith('127.0.0.') or addr.startswith('::'): - addr_local = addr - else: - addr_local = get_local_addr_str(family, iface) - am = AddrMapInfo(family,addr_local,addr_external) - self.addr_map.append(am) - new_entry = True - return addr_local, new_entry - - def get_local(self, addr, root): - """ Maps arbitrary IP4 or IP6 addres to local address, """ - """ saves mapping info and returns local address to caller""" - local_address = None - iface = None - is_new_entry = None - family = None - if root is True: - iface = self.default_iface - else: - if self.cur_iface == self.default_iface or self.cur_iface == self.peer_iface: - self.cur_iface = self.cur_iface + 1 - iface = self.cur_iface - family = socket.AF_INET - local_address, is_new_entry = self.map_to_local(addr, family, iface) - if local_address is None: - family = socket.AF_INET6 - local_address, is_new_entry = self.map_to_local(addr, family, iface); - if local_address is None: - family = None - if root is False and is_new_entry is True: - self.cur_iface = self.cur_iface + 1 - while self.cur_iface == self.default_iface or self.cur_iface == self.peer_iface: - self.cur_iface = self.cur_iface + 1 - return local_address, family - - def map_entries(self, entrylist): - """ Translate addresses for A and AAAA records""" - for entry in entrylist : - for rr in itertools.chain(entry.message.answer,entry.message.additional,entry.message.question,entry.message.authority): - for rd in rr: - if rd.rdtype == dns.rdatatype.A or rd.rdtype == dns.rdatatype.AAAA: - rd_local_address, family = self.get_local(rd.address,False) - rd.address = rd_local_address - - def map_adresses(self): - """ Translate addresses for whole scenario """ - """ Raw data not translated """ - if self.config is None: - self.kroot_family = socket.AF_INET - self.kroot_local = get_local_addr_str(self.kroot_family, self.default_iface) - return - kroot_addr = None - for k, v in self.config: - if k == 'stub-addr': - kroot_addr = v - if kroot_addr is not None: - self.kroot_local, self.kroot_family = self.get_local(kroot_addr, True) - if self.kroot_local is None: - raise Exception("[map_adresses] Invalid K.ROOT-SERVERS.NET. address, check the config") - for rng in self.scenario.ranges : - range_local_address, family = self.get_local(rng.address, False) - if range_local_address is None: - raise Exception("[map_adresses] Error translating address '%s', check the config" % rng.address) - rng.address = range_local_address - self.map_entries(rng.stored) - for stp in self.scenario.steps : - self.map_entries(stp.data) - - def address(self): - """ Returns opened sockets list """ - addrlist = []; - for s in self.srv_socks: - addrlist.append(s.getsockname()); - return addrlist; - - def handle_query(self, client): - """ Handle incoming queries. """ - client_address = client.client_address - query, addr = recvfrom_msg(client) - if query is None: - return False - response = dns.message.make_response(query) - is_raw_data = False - if self.scenario is not None: - 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) - sendto_msg(client, response.to_wire(), addr) - else: - sendto_msg(client, response, addr) - - return True - else: - response = dns.message.make_response(query) - response.rcode = dns.rcode.SERVFAIL - sendto_msg(client, response.to_wire(), addr) - return False - - def query_io(self): - """ Main server process """ - if self.active is False: - raise Exception("[query_io] Test server not active") - while self.active is True: - to_read, _, to_error = select.select(self.srv_socks, [], self.srv_socks, 0.1) - for sock in to_read: - self.handle_query(sock) - 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, port = 53): - """ Starts listening thread if necessary """ - if family == None: - family = socket.AF_INET - if family == socket.AF_INET: - if address == '' or address is None: - address = "127.0.0.{}".format(self.default_iface) - 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 = "::1" - else: - raise Exception("[start_srv] unsupported socket type {sock_type}".format(sock_type=type)) - 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.client_address == address : - return srv_sock.getsockname() - - addr_info = socket.getaddrinfo(address,port,family,0,socket.IPPROTO_UDP) - sock = SrvSock(address, family, socket.SOCK_DGRAM, socket.IPPROTO_UDP) - addr_info_entry0 = addr_info[0] - sockaddr = addr_info_entry0[4] - sock.bind(sockaddr) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.srv_socks.append(sock) - sockname = sock.getsockname() - return sockname - - def play(self): - saddr = get_local_addr_str(socket.AF_INET,self.default_iface) - paddr = get_local_addr_str(socket.AF_INET,self.peer_iface) - self.scenario.play(saddr,paddr) - -if __name__ == '__main__': - # Self-test code - 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) - # Mirror server - server = TestServer(None,None,DEFAULT_IFACE,DEFAULT_IFACE) - server.start() - print "[==========] Mirror server running at", server.address() - try: - while True: - time.sleep(0.5) - except KeyboardInterrupt: - print "[==========] Shutdown." - pass - server.stop() diff --git a/tests/test_integration.py b/tests/test_integration.py deleted file mode 100755 index f641ba3f7..000000000 --- a/tests/test_integration.py +++ /dev/null @@ -1,318 +0,0 @@ -#!/usr/bin/env python -import sys -import os -import fileinput -import subprocess -import tempfile -import shutil -import socket -import time -import signal -import stat -import errno -import jinja2 -from pydnstest import scenario, testserver, test -from datetime import datetime - -def str2bool(v): - """ Return conversion of JSON-ish string value to boolean. """ - return v.lower() in ('yes', 'true', 'on') - -def del_files(path_to): - for root, dirs, files in os.walk(path_to): - for f in files: - os.unlink(os.path.join(root, f)) - -DEFAULT_IFACE = 0 -CHILD_IFACE = 0 -TMPDIR = "" - -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) - -if "KRESD_WRAPPER_DEFAULT_IFACE" in os.environ: - CHILD_IFACE = int(os.environ["KRESD_WRAPPER_DEFAULT_IFACE"]) -if CHILD_IFACE < 2 or CHILD_IFACE > 254 or CHILD_IFACE == DEFAULT_IFACE: - OLD_CHILD_IFACE = CHILD_IFACE - CHILD_IFACE = DEFAULT_IFACE + 1 - if CHILD_IFACE > 254: - CHILD_IFACE = 2 - os.environ["KRESD_WRAPPER_DEFAULT_IFACE"] = "{}".format(CHILD_IFACE) - -if "SOCKET_WRAPPER_DIR" in os.environ: - TMPDIR = os.environ["SOCKET_WRAPPER_DIR"] -if TMPDIR == "" or os.path.isdir(TMPDIR) is False: - OLDTMPDIR = TMPDIR - TMPDIR = tempfile.mkdtemp(suffix='', prefix='tmp') - os.environ["SOCKET_WRAPPER_DIR"] = TMPDIR - -def get_next(file_in): - """ Return next token from the input stream. """ - while True: - line = file_in.readline() - if len(line) == 0: - return False - for csep in (';', '#'): - if csep in line: - line = line[0:line.index(csep)] - tokens = ' '.join(line.strip().split()).split() - if len(tokens) == 0: - continue # Skip empty lines - op = tokens.pop(0) - return op, tokens - - -def parse_entry(op, args, file_in): - """ Parse entry definition. """ - out = scenario.Entry() - for op, args in iter(lambda: get_next(file_in), False): - if op == 'ENTRY_END': - break - elif op == 'REPLY': - out.set_reply(args) - elif op == 'MATCH': - out.set_match(args) - elif op == 'ADJUST': - out.set_adjust(args) - elif op == 'SECTION': - out.begin_section(args[0]) - elif op == 'RAW': - out.begin_raw() - else: - out.add_record(op, args) - return out - - -def parse_step(op, args, file_in): - """ Parse range definition. """ - if len(args) < 2: - raise Exception('expected STEP ') - extra_args = [] - if len(args) > 2: - extra_args = args[2:] - out = scenario.Step(args[0], args[1], extra_args) - if out.has_data: - op, args = get_next(file_in) - if op == 'ENTRY_BEGIN': - out.add(parse_entry(op, args, file_in)) - else: - raise Exception('expected "ENTRY_BEGIN"') - return out - - -def parse_range(op, args, file_in): - """ Parse range definition. """ - if len(args) < 2: - raise Exception('expected RANGE_BEGIN ') - out = scenario.Range(int(args[0]), int(args[1])) - for op, args in iter(lambda: get_next(file_in), False): - if op == 'ADDRESS': - out.address = args[0] - elif op == 'ENTRY_BEGIN': - out.add(parse_entry(op, args, file_in)) - elif op == 'RANGE_END': - break - return out - - -def parse_scenario(op, args, file_in): - """ Parse scenario definition. """ - out = scenario.Scenario(args[0]) - for op, args in iter(lambda: get_next(file_in), False): - if op == 'SCENARIO_END': - break - if op == 'RANGE_BEGIN': - out.ranges.append(parse_range(op, args, file_in)) - if op == 'STEP': - out.steps.append(parse_step(op, args, file_in)) - return out - - -def parse_file(file_in): - """ Parse scenario from a file. """ - try: - config = [] - line = file_in.readline() - while len(line): - if line.startswith('CONFIG_END'): - break - if not line.startswith(';'): - if '#' in line: - line = line[0:line.index('#')] - # Break to key-value pairs - # e.g.: ['minimization', 'on'] - kv = [x.strip() for x in line.split(':')] - if len(kv) >= 2: - config.append(kv) - line = file_in.readline() - for op, args in iter(lambda: get_next(file_in), False): - if op == 'SCENARIO_BEGIN': - return parse_scenario(op, args, file_in), config - raise Exception("IGNORE (missing scenario)") - except Exception as e: - raise Exception('line %d: %s' % (file_in.lineno(), str(e))) - - -def find_objects(path): - """ Recursively scan file/directory for scenarios. """ - result = [] - if os.path.isdir(path): - for e in os.listdir(path): - result += find_objects(os.path.join(path, e)) - elif os.path.isfile(path): - if path.endswith('.rpl'): - result.append(path) - return result - -def write_timestamp_file(path, tst): - time_file = open(path, 'w') - time_file.write(datetime.fromtimestamp(tst).strftime('%Y-%m-%d %H:%M:%S')) - time_file.close() - -def setup_env(child_env, config, config_name, j2template): - """ Set up test environment and config """ - # Clear test directory - del_files(TMPDIR) - # Set up libfaketime - os.environ["FAKETIME_NO_CACHE"] = "1" - os.environ["FAKETIME_TIMESTAMP_FILE"] = '%s/.time' % TMPDIR - child_env["FAKETIME_NO_CACHE"] = "1" - child_env["FAKETIME_TIMESTAMP_FILE"] = '%s/.time' % TMPDIR - write_timestamp_file(child_env["FAKETIME_TIMESTAMP_FILE"], 0) - # Set up child process env() - child_env["SOCKET_WRAPPER_DEFAULT_IFACE"] = "%i" % CHILD_IFACE - child_env["SOCKET_WRAPPER_DIR"] = TMPDIR - no_minimize = "true" - trust_anchor_str = "" - stub_addr = "" - for k,v in config: - # Enable selectively for some tests - if k == 'query-minimization' and str2bool(v): - no_minimize = "false" - elif k == 'trust-anchor': - trust_anchor_str = v.strip('"\'') - elif k == 'val-override-date': - override_date_str = v.strip('"\'') - write_timestamp_file(child_env["FAKETIME_TIMESTAMP_FILE"], int(override_date_str)) - elif k == 'stub-addr': - stub_addr = v.strip('"\'') - if stub_addr.startswith('127.0.0.') or stub_addr.startswith('::'): - selfaddr = stub_addr - else: - selfaddr = testserver.get_local_addr_str(socket.AF_INET, DEFAULT_IFACE) - childaddr = testserver.get_local_addr_str(socket.AF_INET, 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(socket.AF_INET, sock_type) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock.bind((childaddr, 53)) - if sock_type == socket.SOCK_STREAM: - sock.listen(5) - # Generate configuration - j2template_ctx = { - "ROOT_ADDR" : selfaddr, - "SELF_ADDR" : childaddr, - "NO_MINIMIZE" : no_minimize, - "TRUST_ANCHOR" : trust_anchor_str, - "WORKING_DIR" : TMPDIR, - } - cfg_rendered = j2template.render(j2template_ctx) - f = open(os.path.join(TMPDIR,config_name), 'w') - f.write(cfg_rendered) - f.close() - -def play_object(path, binary_name, config_name, j2template, binary_additional_pars): - """ 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() - - # Setup daemon environment - daemon_env = os.environ.copy() - setup_env(daemon_env, config, config_name, j2template) - # Start binary - daemon_proc = None - daemon_log = open('%s/server.log' % TMPDIR, 'w') - daemon_args = [binary_name] + binary_additional_pars - try : - 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: - raise Exception("Can't start '%s': %s" % (daemon_args, str(e))) - # Wait until the server accepts TCP clients - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - while True: - time.sleep(0.1) - if daemon_proc.poll() != None: - print(open('%s/server.log' % TMPDIR).read()) - raise Exception('process died "%s", logs in "%s"' % (os.path.basename(binary_name), TMPDIR)) - try: - sock.connect((testserver.get_local_addr_str(socket.AF_INET, CHILD_IFACE), 53)) - except: continue - break - # Play scenario - server = testserver.TestServer(scenario, config, DEFAULT_IFACE, CHILD_IFACE) - server.start() - try: - server.play() - finally: - server.stop() - daemon_proc.terminate() - daemon_proc.wait() - if 'VERBOSE' in os.environ: - print('[ LOG ]\n%s' % open('%s/server.log' % TMPDIR).read()) - # Do not clear files if the server crashed (for analysis) - del_files(TMPDIR) - -def test_platform(*args): - if sys.platform == 'windows': - raise Exception('not supported at all on Windows') - -if __name__ == '__main__': - - if len(sys.argv) < 5: - print "Usage: test_integration.py