Commit f2eaf2a7 authored by Tomas Krizek's avatar Tomas Krizek

Merge branch 'ci-updates' into 'master'

ci: updates

See merge request !104
parents e82f8a5e 1bbf709a
Pipeline #76703 passed with stage
in 1 minute and 36 seconds
Subproject commit 04dd33b568b173ad7e6c01604f61999b091191d1
......@@ -26,10 +26,10 @@ def prepare_dir(directory: str, clean: bool = False) -> None:
pass
try:
os.makedirs(directory)
except FileExistsError:
except FileExistsError as e:
raise RuntimeError(
'Directory "{}" already exists! Use -l label / --clean or (re)move the '
'directory manually to resolve the issue.'.format(directory))
'directory manually to resolve the issue.'.format(directory)) from e
def copy_file(name: str, destdir: str, destname: str = ''):
......@@ -123,8 +123,9 @@ def create_respdiff_files(directory: str, config: Dict[str, Any]):
if config['respdiff_stats']: # copy optional stats file
try:
shutil.copyfile(config['respdiff_stats'], os.path.join(directory, 'stats.json'))
except FileNotFoundError:
raise RuntimeError("Statistics file not found: {}".format(config['respdiff_stats']))
except FileNotFoundError as e:
raise RuntimeError(
"Statistics file not found: {}".format(config['respdiff_stats'])) from e
def create_template_files(directory: str, config: Dict[str, Any]):
......
......@@ -139,8 +139,8 @@ def msg_from_text(text):
"""
try:
qname, qtype = text.split()
except ValueError:
raise ValueError('space is only allowed as separator between qname qtype')
except ValueError as e:
raise ValueError('space is only allowed as separator between qname qtype') from e
qname = dns.name.from_text(qname.encode('ascii'))
qtype = int_or_fromtext(qtype, dns.rdatatype.from_text)
msg = dns.message.make_query(qname, qtype, dns.rdataclass.IN,
......
......@@ -92,11 +92,11 @@ def cfg2dict_convert(fmt, cparser):
except ValueError as ex:
raise ValueError('config section [{}] key "{}" has invalid format: '
'{}; expected format: {}'.format(
sectname, valname, ex, valfmt))
except KeyError:
sectname, valname, ex, valfmt)) from ex
except KeyError as ex:
if valreq:
raise KeyError('config section [{}] key "{}" not found'.format(
sectname, valname))
sectname, valname)) from ex
unsupported_keys = set(cparser[sectname].keys()) - set(sectfmt.keys())
if unsupported_keys:
raise ValueError('unexpected keys {} in section [{}]'.format(
......@@ -178,7 +178,7 @@ def read_cfg(filename):
cfg2dict_check_fields(cdict)
except Exception as exc:
logging.critical('Failed to parse config: %s', exc)
raise ValueError(exc)
raise ValueError(exc) from exc
return cdict
......
......@@ -31,7 +31,7 @@ def read_stats(filename: str) -> SummaryStatistics:
try:
return SummaryStatistics.from_json(filename)
except (FileNotFoundError, InvalidFileFormat) as exc:
raise ValueError(exc)
raise ValueError(exc) from exc
def _handle_empty_report(exc: Exception, skip_empty: bool):
......
......@@ -105,8 +105,8 @@ class LMDB:
def get_db(self, dbname: bytes):
try:
return self.dbs[dbname]
except KeyError:
raise ValueError("Database {} isn't open!".format(dbname.decode('utf-8')))
except KeyError as e:
raise ValueError("Database {} isn't open!".format(dbname.decode('utf-8'))) from e
def key_stream(self, dbname: bytes) -> Iterator[bytes]:
"""yield all keys from given db"""
......@@ -222,8 +222,8 @@ class DNSRepliesFactory:
for server in self.servers:
try:
reply = replies[server]
except KeyError:
raise ValueError('Missing reply for server "{}"!'.format(server))
except KeyError as e:
raise ValueError('Missing reply for server "{}"!'.format(server)) from e
else:
data.append(reply.binary)
return b''.join(data)
......@@ -249,7 +249,7 @@ class Database(ABC):
try:
self.db = self.lmdb.open_db(self.DB_NAME, create=self.create)
except lmdb.Error as exc:
raise RuntimeError('Failed to open LMDB database: {}'.format(exc))
raise RuntimeError('Failed to open LMDB database: {}'.format(exc)) from exc
with self.lmdb.env.begin(self.db, write=write) as txn:
yield txn
......
......@@ -35,8 +35,8 @@ class JSONDataObject:
try:
with open(filename) as f:
restore_dict = json.load(f)
except json.decoder.JSONDecodeError:
raise InvalidFileFormat("Couldn't parse JSON file: {}".format(filename))
except json.decoder.JSONDecodeError as e:
raise InvalidFileFormat("Couldn't parse JSON file: {}".format(filename)) from e
inst = cls(_restore_dict=restore_dict)
inst.fileorigin = filename
return inst
......
......@@ -221,10 +221,10 @@ def sock_init(retry: int = 3) -> Tuple[Selector, Sequence[Tuple[ResolverID, Sock
sock = ctx.wrap_socket(sock)
try:
sock.connect(destination)
except ConnectionRefusedError: # TCP socket is closed
except ConnectionRefusedError as e: # TCP socket is closed
raise RuntimeError(
"socket: Failed to connect to {dest[0]} port {dest[1]}".format(
dest=destination))
dest=destination)) from e
except OSError as exc:
if exc.errno != 0 and not isinstance(exc, ConnectionResetError):
raise
......@@ -235,7 +235,7 @@ def sock_init(retry: int = 3) -> Tuple[Selector, Sequence[Tuple[ResolverID, Sock
if attempt > retry:
raise RuntimeError(
"socket: Failed to connect to {dest[0]} port {dest[1]}".format(
dest=destination))
dest=destination)) from exc
else:
break
sock.setblocking(False)
......@@ -250,8 +250,8 @@ def _recv_msg(sock: Socket, isstream: IsStreamFlag) -> WireFormat:
if isstream: # parse preambule
try:
blength = sock.recv(2)
except ssl.SSLWantReadError:
raise TcpDnsLengthError('failed to recv DNS packet length')
except ssl.SSLWantReadError as e:
raise TcpDnsLengthError('failed to recv DNS packet length') from e
else:
if len(blength) != 2: # FIN / RST
raise TcpDnsLengthError('failed to recv DNS packet length')
......@@ -321,9 +321,9 @@ def send_recv_parallel(
try: # get sockets and selector
selector = __worker_state.selector
sockets = __worker_state.sockets
except AttributeError:
except AttributeError as e:
# handle improper initialization
raise __worker_state.exception
raise __worker_state.exception from e
try:
replies, reinit = _send_recv_parallel(dgram, selector, sockets, timeout)
......
import os
import subprocess
import sys
import pytest
DECKARD_PATH = os.path.normpath(os.path.join(
os.path.dirname(os.path.abspath(__file__)), '..', '..', 'ci', 'deckard'))
try:
subprocess.run(
'test -f "{path}/env.sh" || make depend -C "{path}"'.format(path=DECKARD_PATH),
cwd=DECKARD_PATH,
shell=True,
check=True)
except subprocess.CalledProcessError as exc:
pytest.skip(
"Failed to compile deckard: {}".format(exc), allow_module_level=True)
else:
sys.path.append(DECKARD_PATH)
pytest.importorskip("pydnstest")
[sendrecv]
# in seconds (float)
timeout = 1
# number of queries to run simultaneously
jobs = 16
# in seconds (float); delay each query by a random time (uniformly distributed) between min and max; set max to 0 to disable
time_delay_min = 0
time_delay_max = 0
# number of maximum consecutive timeouts received from a single resolver before exiting
max_timeouts = 10
[servers]
names = deckard1, deckard2, deckard3
# symbolic names of DNS servers under test
# separate multiple values by ,
# each symbolic name in [servers] section refers to config section
# containing IP address and port of particular server
[deckard1]
ip = 1.2.3.4
port = 53
transport = udp
# optional graph color: common names or hex (#00FFFF) allowed
graph_color = cyan
# optional restart script to clean cache and restart resolver, used by diffrepro
# restart_script = /usr/local/bin/restart-kresd
[deckard2]
ip = 2.3.4.5
port = 53
transport = udp
[deckard3]
ip = 3.4.5.6
port = 53
transport = udp
[diff]
# symbolic name of server under test
# other servers are used as reference when comparing answers from the target
target = deckard1
# fields and comparison methods used when comparing two DNS messages
criteria = opcode, rcode, flags, question, answertypes, answerrrsigs
# other supported criteria values: authority, additional, edns, nsid
[report]
# diffsum reports mismatches in field values in this order
# if particular message has multiple mismatches, it is counted only once into category with highest weight
field_weights = timeout, malformed, opcode, question, rcode, flags, answertypes, answerrrsigs, answer, authority, additional, edns, nsid
stub-addr: 1.2.3.4
CONFIG_END
SCENARIO_BEGIN Respond to any query with malformed DNS message
RANGE_BEGIN 0 100
ADDRESS 1.2.3.4
ENTRY_BEGIN
MATCH opcode qname qtype
SECTION QUESTION
length1.malformed. A
RAW
00
ENTRY_END
ENTRY_BEGIN
MATCH opcode qname qtype
ADJUST raw_id
SECTION QUESTION
incomplete.header.malformed. A
RAW
00008180
ENTRY_END
ENTRY_BEGIN
MATCH opcode qname qtype
ADJUST raw_id
SECTION QUESTION
incomplete.body.malformed. A
RAW
000081a000010001000000000200
;reply QUERY RD RA NOERROR 1 question 1 answer
ENTRY_END
ENTRY_BEGIN
MATCH opcode qname qtype
ADJUST raw_id
SECTION QUESTION
trailing.zeros.malformed. A
RAW
000081a0000100010000000008747261696c696e67057a65726f73096d616c666f726d65640000010001c00c00010001000007080004020304050000
;reply QUERY RD RA NOERRIR TTL 1800 trailing.zeros.malformed. IN A 2.3.4.5
ENTRY_END
ENTRY_BEGIN
MATCH opcode qname qtype
ADJUST raw_id
SECTION QUESTION
trailing.garbage.malformed. A
RAW
000081a0000100010000000008747261696c696e670767617262616765096d616c666f726d65640000010001c00c00010001000007080004020304051234
;reply QUERY RD RA NOERRIR TTL 1800 trailing.garbage.malformed. IN A 2.3.4.5
ENTRY_END
RANGE_END
RANGE_BEGIN 0 100
ADDRESS 2.3.4.5
ENTRY_BEGIN
MATCH opcode qname qtype
ADJUST copy_id
SECTION QUESTION
length1.malformed. A
SECTION ANSWER
length1.malformed. IN A 9.0.0.1
ENTRY_END
ENTRY_BEGIN
MATCH opcode qname qtype
ADJUST copy_id
SECTION QUESTION
incomplete.header.malformed. A
SECTION ANSWER
incomplete.header.malformed. IN A 9.0.0.1
ENTRY_END
ENTRY_BEGIN
MATCH opcode qname qtype
ADJUST copy_id
SECTION QUESTION
incomplete.body.malformed. A
SECTION ANSWER
incomplete.body.malformed. IN A 9.0.0.1
ENTRY_END
ENTRY_BEGIN
MATCH opcode qname qtype
ADJUST copy_id
SECTION QUESTION
trailing.zeros.malformed. A
SECTION ANSWER
trailing.zeros.malformed. IN A 9.0.0.1
ENTRY_END
ENTRY_BEGIN
MATCH opcode qname qtype
ADJUST copy_id
SECTION QUESTION
trailing.garbage.malformed. A
SECTION ANSWER
trailing.garbage.malformed. IN A 9.0.0.1
ENTRY_END
RANGE_END
RANGE_BEGIN 0 100
ADDRESS 3.4.5.6
ENTRY_BEGIN
MATCH opcode qname qtype
ADJUST copy_id
SECTION QUESTION
length1.malformed. A
SECTION ANSWER
length1.malformed. IN A 9.0.0.1
ENTRY_END
ENTRY_BEGIN
MATCH opcode qname qtype
ADJUST copy_id
SECTION QUESTION
incomplete.header.malformed. A
SECTION ANSWER
incomplete.header.malformed. IN A 9.0.0.1
ENTRY_END
ENTRY_BEGIN
MATCH opcode qname qtype
ADJUST copy_id
SECTION QUESTION
incomplete.body.malformed. A
SECTION ANSWER
incomplete.body.malformed. IN A 9.0.0.1
ENTRY_END
ENTRY_BEGIN
MATCH opcode qname qtype
ADJUST copy_id
SECTION QUESTION
trailing.zeros.malformed. A
SECTION ANSWER
trailing.zeros.malformed. IN A 9.0.0.1
ENTRY_END
ENTRY_BEGIN
MATCH opcode qname qtype
ADJUST copy_id
SECTION QUESTION
trailing.garbage.malformed. A
SECTION ANSWER
trailing.garbage.malformed. IN A 9.0.0.1
ENTRY_END
RANGE_END
STEP 1 TIME_PASSES ELAPSE 1
SCENARIO_END
from respdiff.database import DNSReply
from respdiff.match import DataMismatch
from .util import diffsum_toolchain
@diffsum_toolchain('malformed.rpl')
def test_malformed(report):
fcs = report.summary.get_field_counters()
assert len(report.target_disagreements) == 5
assert fcs['malformed'][DataMismatch(DNSReply.WIREFORMAT_VALID, 'FormError')] == 1
assert fcs['malformed'][DataMismatch(DNSReply.WIREFORMAT_VALID, 'ShortHeader')] == 1
assert fcs['malformed'][DataMismatch(DNSReply.WIREFORMAT_VALID, 'TrailingJunk')] == 2
assert sum(fcs['timeout'].values()) == 1
"""
Test the respdiff toolchain by mocking DNS traffic using Deckard as a mock server.
https://gitlab.nic.cz/knot/deckard
"""
import os
import subprocess
import tempfile
from typing import Optional, Sequence # noqa
from pydnstest.augwrap import AugeasWrapper # pylint: disable=import-error
from respdiff.dataformat import DiffReport
from . import DECKARD_PATH
TEST_DIR = os.path.dirname(os.path.abspath(__file__))
SCENARIO_DIR = os.path.join(TEST_DIR, 'scenarios')
CONFIG_PATH = os.path.join(TEST_DIR, 'respdiff.cfg')
RESPDIFF_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..')
class MockServer(tempfile.TemporaryDirectory):
def __init__(self, deckard_path: str, scenario_path: str) -> None:
super(MockServer, self).__init__()
self.deckard_path = deckard_path
self.scenario_path = scenario_path
self.tmpdir = ''
self.deckard = None # type: Optional[subprocess.Popen]
def __enter__(self) -> 'MockServer':
assert os.path.exists(self.scenario_path), \
"Scenario {} not found!".format(self.scenario_path)
assert os.path.exists(os.path.join(self.deckard_path, 'env.sh')), \
"env.sh file missing in deckard dir; was it compiled?"
self.tmpdir = super(MockServer, self).__enter__()
assert self.tmpdir is not None
cmd = (
'. {ms.deckard_path}/env.sh; ' # 'source' that's compatible with /bin/sh
'python3 {ms.deckard_path}/pydnstest/testserver.py '
'--scenario {ms.scenario_path}').format(ms=self)
my_env = os.environ.copy()
my_env['PYTHONPATH'] = self.deckard_path
my_env['SOCKET_WRAPPER_DIR'] = self.tmpdir
self.deckard = subprocess.Popen(
[cmd],
shell=True,
env=my_env,
stderr=subprocess.PIPE)
assert self.deckard is not None
# run and wait for server to get initialized
while True:
output = self.deckard.stderr.readline()
if b'server running' in output:
break
if self.deckard.poll() is not None:
raise RuntimeError("Deckard didn't start properly!")
return self
def __exit__(self, exc_type, exc_value, traceback):
assert self.deckard is not None
self.deckard.terminate()
self.deckard.wait()
return super(MockServer, self).__exit__(exc_type, exc_value, traceback)
def execute(self, cmd: str) -> subprocess.Popen:
my_env = os.environ.copy()
my_env['SOCKET_WRAPPER_DIR'] = self.tmpdir
shcmd = '. {ms.deckard_path}/env.sh; {cmd}'.format(ms=self, cmd=cmd)
process = subprocess.Popen(
[shcmd],
shell=True,
env=my_env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
process.wait()
return process
def qprep(queries: Sequence[str], envdir: str):
data = b'\n'.join((query.encode('ascii') for query in queries))
with subprocess.Popen([
os.path.join(RESPDIFF_DIR, 'qprep.py'),
envdir],
stdin=subprocess.PIPE) as proc:
try:
proc.communicate(data)
except Exception:
proc.kill()
raise
finally:
proc.wait()
def orchestrator(envdir: str, config_path: str, deckard_path: str, scenario_path: str):
with MockServer(
deckard_path=deckard_path,
scenario_path=scenario_path) as mock:
mock.execute(
'{respdiffdir}/orchestrator.py -c {config_path} {envdir}'.format(
respdiffdir=RESPDIFF_DIR,
config_path=config_path,
envdir=envdir))
def msgdiff(envdir: str, config_path: str):
subprocess.call('{respdiffdir}/msgdiff.py -c {config_path} {envdir}'.format(
respdiffdir=RESPDIFF_DIR,
config_path=config_path,
envdir=envdir),
shell=True)
def diffsum(envdir: str, config_path: str):
subprocess.call('{respdiffdir}/diffsum.py -c {config_path} {envdir}'.format(
respdiffdir=RESPDIFF_DIR,
config_path=config_path,
envdir=envdir),
shell=True)
def queries_from_rpl(scenario_path):
aug = AugeasWrapper(
confpath=scenario_path,
lens='Deckard',
loadpath=os.path.join(DECKARD_PATH, 'pydnstest'))
queries = []
for qnode in aug.tree.match('/scenario/range/entry/section/question/record'):
domain = qnode.get('/domain').value
type_ = qnode.get('/type').value
queries.append((domain, type_))
return [' '.join((domain, type_)) for domain, type_ in set(queries)]
def diffsum_toolchain(scenario):
def decorator(func):
def wrapper():
scenario_path = os.path.join(SCENARIO_DIR, scenario)
with tempfile.TemporaryDirectory() as envdir:
qprep(queries_from_rpl(scenario_path), envdir)
orchestrator(
envdir,
CONFIG_PATH,
deckard_path=DECKARD_PATH,
scenario_path=scenario_path)
msgdiff(envdir, CONFIG_PATH)
diffsum(envdir, CONFIG_PATH)
report = DiffReport.from_json(os.path.join(envdir, 'report.json'))
func(report)
return wrapper
return decorator
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