Compare commits
16 Commits
4959-imple
...
stepan/rew
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19bc2daf65 | ||
|
|
eba0f37c5c | ||
|
|
8c659586f0 | ||
|
|
98d73ac8cf | ||
|
|
68e4740a18 | ||
|
|
128908d45b | ||
|
|
04c7b6e8ff | ||
|
|
9373dc6978 | ||
|
|
70e12fdf4d | ||
|
|
caf17105df | ||
|
|
c8916551ca | ||
|
|
9c00bff08c | ||
|
|
e8dcb7e86b | ||
|
|
73f71b63e7 | ||
|
|
83874b771f | ||
|
|
ae3dd1fb88 |
@@ -547,6 +547,8 @@ coccinelle:
|
||||
pylint:
|
||||
<<: *precheck_job
|
||||
needs: []
|
||||
variables:
|
||||
PYTHONPATH: "${CI_PROJECT_DIR}/bin/tests/system"
|
||||
script:
|
||||
- pylint --rcfile $CI_PROJECT_DIR/.pylintrc $(git ls-files '*.py' | grep -vE '(ans\.py|dangerfile\.py|^bin/tests/system/)')
|
||||
# Ignore Pylint wrong-import-position error in system test to enable use of pytest.importorskip
|
||||
@@ -578,6 +580,11 @@ checkbashisms:
|
||||
script:
|
||||
- checkbashisms $(find . -path './.git' -prune -o -type f -exec sh -c 'head -n 1 "{}" | grep -qsF "#!/bin/sh"' \; -print)
|
||||
|
||||
mypy:
|
||||
<<: *precheck_job
|
||||
script:
|
||||
- mypy bin/tests/system/isctest.py
|
||||
|
||||
tarball-create:
|
||||
stage: precheck
|
||||
<<: *base_image
|
||||
|
||||
@@ -10,25 +10,11 @@
|
||||
# information regarding copyright ownership.
|
||||
|
||||
import concurrent.futures
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
|
||||
def run_rndc(server, rndc_command):
|
||||
"""
|
||||
Send the specified 'rndc_command' to 'server' with a timeout of 10 seconds
|
||||
"""
|
||||
rndc = os.getenv("RNDC")
|
||||
port = os.getenv("CONTROLPORT")
|
||||
|
||||
cmdline = [rndc, "-c", "../common/rndc.conf", "-p", port, "-s", server]
|
||||
cmdline.extend(rndc_command)
|
||||
|
||||
subprocess.check_output(cmdline, stderr=subprocess.STDOUT, timeout=10)
|
||||
|
||||
|
||||
def rndc_loop(test_state, domain):
|
||||
def rndc_loop(test_state, domain, ns3):
|
||||
"""
|
||||
Run "rndc addzone", "rndc modzone", and "rndc delzone" in a tight loop
|
||||
until the test is considered finished, ignoring errors
|
||||
@@ -45,35 +31,33 @@ def rndc_loop(test_state, domain):
|
||||
|
||||
while not test_state["finished"]:
|
||||
for command in rndc_commands:
|
||||
try:
|
||||
run_rndc("10.53.0.3", command)
|
||||
except subprocess.SubprocessError:
|
||||
pass
|
||||
ns3.rndc(" ".join(command), ignore_errors=True, log=False)
|
||||
|
||||
|
||||
def check_if_server_is_responsive():
|
||||
def check_if_server_is_responsive(ns3):
|
||||
"""
|
||||
Check if server status can be successfully retrieved using "rndc status"
|
||||
"""
|
||||
try:
|
||||
run_rndc("10.53.0.3", ["status"])
|
||||
ns3.rndc("status", log=False)
|
||||
return True
|
||||
except subprocess.SubprocessError:
|
||||
return False
|
||||
|
||||
|
||||
def test_rndc_deadlock():
|
||||
def test_rndc_deadlock(servers):
|
||||
"""
|
||||
Test whether running "rndc addzone", "rndc modzone", and "rndc delzone"
|
||||
commands concurrently does not trigger a deadlock
|
||||
"""
|
||||
test_state = {"finished": False}
|
||||
ns3 = servers["ns3"]
|
||||
|
||||
# Create 4 worker threads running "rndc" commands in a loop.
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
for i in range(1, 5):
|
||||
domain = "example%d" % i
|
||||
executor.submit(rndc_loop, test_state, domain)
|
||||
executor.submit(rndc_loop, test_state, domain, ns3)
|
||||
|
||||
# Run "rndc status" 10 times, with 1-second pauses between attempts.
|
||||
# Each "rndc status" invocation has a timeout of 10 seconds. If any of
|
||||
@@ -81,7 +65,7 @@ def test_rndc_deadlock():
|
||||
server_is_responsive = True
|
||||
attempts = 10
|
||||
while server_is_responsive and attempts > 0:
|
||||
server_is_responsive = check_if_server_is_responsive()
|
||||
server_is_responsive = check_if_server_is_responsive(ns3)
|
||||
attempts -= 1
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
# See the COPYRIGHT file distributed with this work for additional
|
||||
# information regarding copyright ownership.
|
||||
|
||||
import mmap
|
||||
from typing import NamedTuple, Tuple
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -27,7 +28,6 @@ import dns.query
|
||||
import dns.rcode
|
||||
import dns.rdataclass
|
||||
import dns.rdatatype
|
||||
import dns.resolver
|
||||
|
||||
|
||||
def has_signed_apex_nsec(zone, response):
|
||||
@@ -38,8 +38,8 @@ def has_signed_apex_nsec(zone, response):
|
||||
nextname = "a."
|
||||
labelcount = zone.count(".") # zone is specified as FQDN
|
||||
types = "NS SOA RRSIG NSEC DNSKEY"
|
||||
match = "{0} {1} IN NSEC {2}{0} {3}".format(zone, ttl, nextname, types)
|
||||
sig = "{0} {1} IN RRSIG NSEC 13 {2} 300".format(zone, ttl, labelcount)
|
||||
match = f"{zone} {ttl} IN NSEC {nextname}{zone} {types}"
|
||||
sig = f"{zone} {ttl} IN RRSIG NSEC 13 {labelcount} 300"
|
||||
|
||||
for rr in response.answer:
|
||||
if match in rr.to_text():
|
||||
@@ -59,19 +59,11 @@ def do_query(server, qname, qtype, tcp=False):
|
||||
query = dns.message.make_query(qname, qtype, use_edns=True, want_dnssec=True)
|
||||
try:
|
||||
if tcp:
|
||||
response = dns.query.tcp(
|
||||
query, server.nameservers[0], timeout=3, port=server.port
|
||||
)
|
||||
response = dns.query.tcp(query, server.ip, timeout=3, port=server.ports.dns)
|
||||
else:
|
||||
response = dns.query.udp(
|
||||
query, server.nameservers[0], timeout=3, port=server.port
|
||||
)
|
||||
response = dns.query.udp(query, server.ip, timeout=3, port=server.ports.dns)
|
||||
except dns.exception.Timeout:
|
||||
print(
|
||||
"error: query timeout for query {} {} to {}".format(
|
||||
qname, qtype, server.nameservers[0]
|
||||
)
|
||||
)
|
||||
print(f"error: query timeout for query {qname} {qtype} to {server.ip}")
|
||||
return None
|
||||
|
||||
return response
|
||||
@@ -81,7 +73,7 @@ def verify_zone(zone, transfer):
|
||||
verify = os.getenv("VERIFY")
|
||||
assert verify is not None
|
||||
|
||||
filename = "{}out".format(zone)
|
||||
filename = f"{zone}out"
|
||||
with open(filename, "w", encoding="utf-8") as file:
|
||||
for rr in transfer.answer:
|
||||
file.write(rr.to_text())
|
||||
@@ -93,21 +85,21 @@ def verify_zone(zone, transfer):
|
||||
verifier = subprocess.run(verify_cmd, capture_output=True, check=True)
|
||||
|
||||
if verifier.returncode != 0:
|
||||
print("error: dnssec-verify {} failed".format(zone))
|
||||
print(f"error: dnssec-verify {zone} failed")
|
||||
sys.stderr.buffer.write(verifier.stderr)
|
||||
|
||||
return verifier.returncode == 0
|
||||
|
||||
|
||||
def read_statefile(server, zone):
|
||||
addr = server.nameservers[0]
|
||||
addr = server.ip
|
||||
count = 0
|
||||
keyid = 0
|
||||
state = {}
|
||||
|
||||
response = do_query(server, zone, "DS", tcp=True)
|
||||
if not isinstance(response, dns.message.Message):
|
||||
print("error: no response for {} DS from {}".format(zone, addr))
|
||||
print(f"error: no response for {zone} DS from {addr}")
|
||||
return {}
|
||||
|
||||
if response.rcode() == dns.rcode.NOERROR:
|
||||
@@ -125,20 +117,16 @@ def read_statefile(server, zone):
|
||||
|
||||
if count != 1:
|
||||
print(
|
||||
"error: expected a single DS in response for {} from {},"
|
||||
"got {}".format(zone, addr, count)
|
||||
f"error: expected a single DS in response for {zone} from {addr}, got {count}"
|
||||
)
|
||||
return {}
|
||||
else:
|
||||
print(
|
||||
"error: {} response for {} DNSKEY from {}".format(
|
||||
dns.rcode.to_text(response.rcode()), zone, addr
|
||||
)
|
||||
)
|
||||
rcode = dns.rcode.to_text(response.rcode())
|
||||
print(f"error: {rcode} response for {zone} DNSKEY from {addr}")
|
||||
return {}
|
||||
|
||||
filename = "ns9/K{}+013+{:05d}.state".format(zone, keyid)
|
||||
print("read state file {}".format(filename))
|
||||
filename = f"ns9/K{zone}+013+{keyid:05d}.state"
|
||||
print(f"read state file {filename}")
|
||||
|
||||
try:
|
||||
with open(filename, "r", encoding="utf-8") as file:
|
||||
@@ -156,23 +144,20 @@ def read_statefile(server, zone):
|
||||
|
||||
|
||||
def zone_check(server, zone):
|
||||
addr = server.nameservers[0]
|
||||
fqdn = "{}.".format(zone)
|
||||
addr = server.ip
|
||||
fqdn = f"{zone}."
|
||||
|
||||
# wait until zone is fully signed.
|
||||
signed = False
|
||||
for _ in range(10):
|
||||
response = do_query(server, fqdn, "NSEC")
|
||||
if not isinstance(response, dns.message.Message):
|
||||
print("error: no response for {} NSEC from {}".format(fqdn, addr))
|
||||
print(f"error: no response for {fqdn} NSEC from {addr}")
|
||||
elif response.rcode() == dns.rcode.NOERROR:
|
||||
signed = has_signed_apex_nsec(fqdn, response)
|
||||
else:
|
||||
print(
|
||||
"error: {} response for {} NSEC from {}".format(
|
||||
dns.rcode.to_text(response.rcode()), fqdn, addr
|
||||
)
|
||||
)
|
||||
rcode = dns.rcode.to_text(response.rcode())
|
||||
print(f"error: {rcode} response for {fqdn} NSEC from {addr}")
|
||||
|
||||
if signed:
|
||||
break
|
||||
@@ -185,21 +170,18 @@ def zone_check(server, zone):
|
||||
verified = False
|
||||
transfer = do_query(server, fqdn, "AXFR", tcp=True)
|
||||
if not isinstance(transfer, dns.message.Message):
|
||||
print("error: no response for {} AXFR from {}".format(fqdn, addr))
|
||||
print(f"error: no response for {fqdn} AXFR from {addr}")
|
||||
elif transfer.rcode() == dns.rcode.NOERROR:
|
||||
verified = verify_zone(fqdn, transfer)
|
||||
else:
|
||||
print(
|
||||
"error: {} response for {} AXFR from {}".format(
|
||||
dns.rcode.to_text(transfer.rcode()), fqdn, addr
|
||||
)
|
||||
)
|
||||
rcode = dns.rcode.to_text(transfer.rcode())
|
||||
print(f"error: {rcode} response for {fqdn} AXFR from {addr}")
|
||||
|
||||
assert verified
|
||||
|
||||
|
||||
def keystate_check(server, zone, key):
|
||||
fqdn = "{}.".format(zone)
|
||||
fqdn = f"{zone}."
|
||||
val = 0
|
||||
deny = False
|
||||
|
||||
@@ -250,420 +232,265 @@ def rekey(zone):
|
||||
controller = subprocess.run(rndc_cmd, capture_output=True, check=True)
|
||||
|
||||
if controller.returncode != 0:
|
||||
print("error: rndc loadkeys {} failed".format(zone))
|
||||
print(f"error: rndc loadkeys {zone} failed")
|
||||
sys.stderr.buffer.write(controller.stderr)
|
||||
|
||||
assert controller.returncode == 0
|
||||
|
||||
|
||||
def wait_for_log(filename, zone, log):
|
||||
found = False
|
||||
|
||||
for _ in range(10):
|
||||
print("read log file {}".format(filename))
|
||||
|
||||
try:
|
||||
with open(filename, "r", encoding="utf-8") as file:
|
||||
s = mmap.mmap(file.fileno(), 0, access=mmap.ACCESS_READ)
|
||||
if s.find(bytes(log, "ascii")) != -1:
|
||||
found = True
|
||||
except FileNotFoundError:
|
||||
print("file not found {}".format(filename))
|
||||
|
||||
if found:
|
||||
break
|
||||
|
||||
print("rekey")
|
||||
rekey(zone)
|
||||
|
||||
print("sleep")
|
||||
time.sleep(1)
|
||||
|
||||
assert found
|
||||
class CheckDSTest(NamedTuple):
|
||||
zone: str
|
||||
logs_to_wait_for: Tuple[str]
|
||||
expected_parent_state: str
|
||||
|
||||
|
||||
def checkds_dspublished(named_port, checkds, addr):
|
||||
# We create resolver instances that will be used to send queries.
|
||||
server = dns.resolver.Resolver()
|
||||
server.nameservers = ["10.53.0.9"]
|
||||
server.port = named_port
|
||||
|
||||
parent = dns.resolver.Resolver()
|
||||
parent.nameservers = ["10.53.0.2"]
|
||||
parent.port = named_port
|
||||
|
||||
#
|
||||
# 1.1.1: DS is correctly published in parent.
|
||||
# parental-agents: ns2
|
||||
#
|
||||
|
||||
# The simple case.
|
||||
zone = "good.{}.dspublish.ns2".format(checkds)
|
||||
zone_check(server, zone)
|
||||
wait_for_log(
|
||||
"ns9/named.run",
|
||||
zone,
|
||||
"zone {}/IN (signed): checkds: DS response from {}".format(zone, addr),
|
||||
)
|
||||
keystate_check(parent, zone, "DSPublish")
|
||||
|
||||
#
|
||||
# 1.1.2: DS is not published in parent.
|
||||
# parental-agents: ns5
|
||||
#
|
||||
zone = "not-yet.{}.dspublish.ns5".format(checkds)
|
||||
zone_check(server, zone)
|
||||
wait_for_log(
|
||||
"ns9/named.run",
|
||||
zone,
|
||||
"zone {}/IN (signed): checkds: empty DS response from 10.53.0.5".format(zone),
|
||||
)
|
||||
keystate_check(parent, zone, "!DSPublish")
|
||||
|
||||
#
|
||||
# 1.1.3: The parental agent is badly configured.
|
||||
# parental-agents: ns6
|
||||
#
|
||||
zone = "bad.{}.dspublish.ns6".format(checkds)
|
||||
zone_check(server, zone)
|
||||
if checkds == "explicit":
|
||||
wait_for_log(
|
||||
"ns9/named.run",
|
||||
zone,
|
||||
"zone {}/IN (signed): checkds: bad DS response from 10.53.0.6".format(zone),
|
||||
)
|
||||
elif checkds == "yes":
|
||||
wait_for_log(
|
||||
"ns9/named.run",
|
||||
zone,
|
||||
"zone {}/IN (signed): checkds: error during parental-agents processing".format(
|
||||
zone
|
||||
),
|
||||
)
|
||||
keystate_check(parent, zone, "!DSPublish")
|
||||
|
||||
#
|
||||
# 1.1.4: DS is published, but has bogus signature.
|
||||
#
|
||||
# TBD
|
||||
|
||||
#
|
||||
# 1.2.1: DS is correctly published in all parents.
|
||||
# parental-agents: ns2, ns4
|
||||
#
|
||||
zone = "good.{}.dspublish.ns2-4".format(checkds)
|
||||
zone_check(server, zone)
|
||||
wait_for_log(
|
||||
"ns9/named.run",
|
||||
zone,
|
||||
"zone {}/IN (signed): checkds: DS response from {}".format(zone, addr),
|
||||
)
|
||||
wait_for_log(
|
||||
"ns9/named.run",
|
||||
zone,
|
||||
"zone {}/IN (signed): checkds: DS response from 10.53.0.4".format(zone),
|
||||
)
|
||||
keystate_check(parent, zone, "DSPublish")
|
||||
|
||||
#
|
||||
# 1.2.2: DS is not published in some parents.
|
||||
# parental-agents: ns2, ns4, ns5
|
||||
#
|
||||
zone = "incomplete.{}.dspublish.ns2-4-5".format(checkds)
|
||||
zone_check(server, zone)
|
||||
wait_for_log(
|
||||
"ns9/named.run",
|
||||
zone,
|
||||
"zone {}/IN (signed): checkds: DS response from {}".format(zone, addr),
|
||||
)
|
||||
wait_for_log(
|
||||
"ns9/named.run",
|
||||
zone,
|
||||
"zone {}/IN (signed): checkds: DS response from 10.53.0.4".format(zone),
|
||||
)
|
||||
wait_for_log(
|
||||
"ns9/named.run",
|
||||
zone,
|
||||
"zone {}/IN (signed): checkds: empty DS response from 10.53.0.5".format(zone),
|
||||
)
|
||||
keystate_check(parent, zone, "!DSPublish")
|
||||
|
||||
#
|
||||
# 1.2.3: One parental agent is badly configured.
|
||||
# parental-agents: ns2, ns4, ns6
|
||||
#
|
||||
zone = "bad.{}.dspublish.ns2-4-6".format(checkds)
|
||||
zone_check(server, zone)
|
||||
wait_for_log(
|
||||
"ns9/named.run",
|
||||
zone,
|
||||
"zone {}/IN (signed): checkds: DS response from {}".format(zone, addr),
|
||||
)
|
||||
wait_for_log(
|
||||
"ns9/named.run",
|
||||
zone,
|
||||
"zone {}/IN (signed): checkds: DS response from 10.53.0.4".format(zone),
|
||||
)
|
||||
wait_for_log(
|
||||
"ns9/named.run",
|
||||
zone,
|
||||
"zone {}/IN (signed): checkds: bad DS response from 10.53.0.6".format(zone),
|
||||
)
|
||||
keystate_check(parent, zone, "!DSPublish")
|
||||
|
||||
#
|
||||
# 1.2.4: DS is completely published, bogus signature.
|
||||
#
|
||||
# TBD
|
||||
|
||||
# TBD: Check with TSIG
|
||||
# TBD: Check with TLS
|
||||
|
||||
|
||||
def checkds_dswithdrawn(named_port, checkds, addr):
|
||||
# We create resolver instances that will be used to send queries.
|
||||
server = dns.resolver.Resolver()
|
||||
server.nameservers = ["10.53.0.9"]
|
||||
server.port = named_port
|
||||
|
||||
parent = dns.resolver.Resolver()
|
||||
parent.nameservers = ["10.53.0.2"]
|
||||
parent.port = named_port
|
||||
|
||||
#
|
||||
# 2.1.1: DS correctly withdrawn from the parent.
|
||||
# parental-agents: ns5
|
||||
#
|
||||
|
||||
# The simple case.
|
||||
zone = "good.{}.dsremoved.ns5".format(checkds)
|
||||
zone_check(server, zone)
|
||||
wait_for_log(
|
||||
"ns9/named.run",
|
||||
zone,
|
||||
"zone {}/IN (signed): checkds: empty DS response from {}".format(zone, addr),
|
||||
)
|
||||
keystate_check(parent, zone, "DSRemoved")
|
||||
|
||||
#
|
||||
# 2.1.2: DS is published in the parent.
|
||||
# parental-agents: ns2
|
||||
#
|
||||
zone = "still-there.{}.dsremoved.ns2".format(checkds)
|
||||
zone_check(server, zone)
|
||||
wait_for_log(
|
||||
"ns9/named.run",
|
||||
zone,
|
||||
"zone {}/IN (signed): checkds: DS response from 10.53.0.2".format(zone),
|
||||
)
|
||||
keystate_check(parent, zone, "!DSRemoved")
|
||||
|
||||
#
|
||||
# 2.1.3: The parental agent is badly configured.
|
||||
# parental-agents: ns6
|
||||
#
|
||||
zone = "bad.{}.dsremoved.ns6".format(checkds)
|
||||
zone_check(server, zone)
|
||||
if checkds == "explicit":
|
||||
wait_for_log(
|
||||
"ns9/named.run",
|
||||
zone,
|
||||
"zone {}/IN (signed): checkds: bad DS response from 10.53.0.6".format(zone),
|
||||
)
|
||||
elif checkds == "yes":
|
||||
wait_for_log(
|
||||
"ns9/named.run",
|
||||
zone,
|
||||
"zone {}/IN (signed): checkds: error during parental-agents processing".format(
|
||||
zone
|
||||
),
|
||||
)
|
||||
keystate_check(parent, zone, "!DSRemoved")
|
||||
|
||||
#
|
||||
# 2.1.4: DS is withdrawn, but has bogus signature.
|
||||
#
|
||||
# TBD
|
||||
|
||||
#
|
||||
# 2.2.1: DS is correctly withdrawn from all parents.
|
||||
# parental-agents: ns5, ns7
|
||||
#
|
||||
zone = "good.{}.dsremoved.ns5-7".format(checkds)
|
||||
zone_check(server, zone)
|
||||
wait_for_log(
|
||||
"ns9/named.run",
|
||||
zone,
|
||||
"zone {}/IN (signed): checkds: empty DS response from {}".format(zone, addr),
|
||||
)
|
||||
wait_for_log(
|
||||
"ns9/named.run",
|
||||
zone,
|
||||
"zone {}/IN (signed): checkds: empty DS response from 10.53.0.7".format(zone),
|
||||
)
|
||||
keystate_check(parent, zone, "DSRemoved")
|
||||
|
||||
#
|
||||
# 2.2.2: DS is not withdrawn from some parents.
|
||||
# parental-agents: ns2, ns5, ns7
|
||||
#
|
||||
zone = "incomplete.{}.dsremoved.ns2-5-7".format(checkds)
|
||||
zone_check(server, zone)
|
||||
wait_for_log(
|
||||
"ns9/named.run",
|
||||
zone,
|
||||
"zone {}/IN (signed): checkds: DS response from 10.53.0.2".format(zone),
|
||||
)
|
||||
wait_for_log(
|
||||
"ns9/named.run",
|
||||
zone,
|
||||
"zone {}/IN (signed): checkds: empty DS response from {}".format(zone, addr),
|
||||
)
|
||||
wait_for_log(
|
||||
"ns9/named.run",
|
||||
zone,
|
||||
"zone {}/IN (signed): checkds: empty DS response from 10.53.0.7".format(zone),
|
||||
)
|
||||
keystate_check(parent, zone, "!DSRemoved")
|
||||
|
||||
#
|
||||
# 2.2.3: One parental agent is badly configured.
|
||||
# parental-agents: ns5, ns6, ns7
|
||||
#
|
||||
zone = "bad.{}.dsremoved.ns5-6-7".format(checkds)
|
||||
zone_check(server, zone)
|
||||
wait_for_log(
|
||||
"ns9/named.run",
|
||||
zone,
|
||||
"zone {}/IN (signed): checkds: empty DS response from {}".format(zone, addr),
|
||||
)
|
||||
wait_for_log(
|
||||
"ns9/named.run",
|
||||
zone,
|
||||
"zone {}/IN (signed): checkds: empty DS response from 10.53.0.7".format(zone),
|
||||
)
|
||||
wait_for_log(
|
||||
"ns9/named.run",
|
||||
zone,
|
||||
"zone {}/IN (signed): checkds: bad DS response from 10.53.0.6".format(zone),
|
||||
)
|
||||
keystate_check(parent, zone, "!DSRemoved")
|
||||
|
||||
#
|
||||
# 2.2.4:: DS is removed completely, bogus signature.
|
||||
#
|
||||
# TBD
|
||||
|
||||
|
||||
def test_checkds_reference(named_port):
|
||||
# We create resolver instances that will be used to send queries.
|
||||
server = dns.resolver.Resolver()
|
||||
server.nameservers = ["10.53.0.9"]
|
||||
server.port = named_port
|
||||
|
||||
parent = dns.resolver.Resolver()
|
||||
parent.nameservers = ["10.53.0.2"]
|
||||
parent.port = named_port
|
||||
|
||||
parental_agents_tests = [
|
||||
# Using a reference to parental-agents.
|
||||
zone = "reference.explicit.dspublish.ns2"
|
||||
zone_check(server, zone)
|
||||
wait_for_log(
|
||||
"ns9/named.run",
|
||||
zone,
|
||||
"zone {}/IN (signed): checkds: DS response from 10.53.0.8".format(zone),
|
||||
)
|
||||
keystate_check(parent, zone, "DSPublish")
|
||||
|
||||
|
||||
def test_checkds_resolver(named_port):
|
||||
# We create resolver instances that will be used to send queries.
|
||||
server = dns.resolver.Resolver()
|
||||
server.nameservers = ["10.53.0.9"]
|
||||
server.port = named_port
|
||||
|
||||
parent = dns.resolver.Resolver()
|
||||
parent.nameservers = ["10.53.0.2"]
|
||||
parent.port = named_port
|
||||
|
||||
CheckDSTest(
|
||||
zone="reference.explicit.dspublish.ns2",
|
||||
logs_to_wait_for=("DS response from 10.53.0.8",),
|
||||
expected_parent_state="DSPublish",
|
||||
),
|
||||
# Using a resolver as parental-agent (ns3).
|
||||
zone = "resolver.explicit.dspublish.ns2"
|
||||
zone_check(server, zone)
|
||||
wait_for_log(
|
||||
"ns9/named.run",
|
||||
zone,
|
||||
"zone {}/IN (signed): checkds: DS response from 10.53.0.3".format(zone),
|
||||
)
|
||||
keystate_check(parent, zone, "DSPublish")
|
||||
|
||||
CheckDSTest(
|
||||
zone="resolver.explicit.dspublish.ns2",
|
||||
logs_to_wait_for=("DS response from 10.53.0.3",),
|
||||
expected_parent_state="DSPublish",
|
||||
),
|
||||
# Using a resolver as parental-agent (ns3).
|
||||
zone = "resolver.explicit.dsremoved.ns5"
|
||||
zone_check(server, zone)
|
||||
wait_for_log(
|
||||
"ns9/named.run",
|
||||
zone,
|
||||
"zone {}/IN (signed): checkds: empty DS response from 10.53.0.3".format(zone),
|
||||
)
|
||||
keystate_check(parent, zone, "DSRemoved")
|
||||
CheckDSTest(
|
||||
zone="resolver.explicit.dsremoved.ns5",
|
||||
logs_to_wait_for=("empty DS response from 10.53.0.3",),
|
||||
expected_parent_state="DSRemoved",
|
||||
),
|
||||
]
|
||||
|
||||
no_ent_tests = [
|
||||
CheckDSTest(
|
||||
zone="no-ent.ns2",
|
||||
logs_to_wait_for=("DS response from 10.53.0.2",),
|
||||
expected_parent_state="DSPublish",
|
||||
),
|
||||
CheckDSTest(
|
||||
zone="no-ent.ns5",
|
||||
logs_to_wait_for=("DS response from 10.53.0.5",),
|
||||
expected_parent_state="DSRemoved",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def test_checkds_no_ent(named_port):
|
||||
# We create resolver instances that will be used to send queries.
|
||||
server = dns.resolver.Resolver()
|
||||
server.nameservers = ["10.53.0.9"]
|
||||
server.port = named_port
|
||||
|
||||
parent = dns.resolver.Resolver()
|
||||
parent.nameservers = ["10.53.0.2"]
|
||||
parent.port = named_port
|
||||
|
||||
zone = "no-ent.ns2"
|
||||
zone_check(server, zone)
|
||||
wait_for_log(
|
||||
"ns9/named.run",
|
||||
zone,
|
||||
"zone {}/IN (signed): checkds: DS response from 10.53.0.2".format(zone),
|
||||
)
|
||||
keystate_check(parent, zone, "DSPublish")
|
||||
|
||||
zone = "no-ent.ns5"
|
||||
zone_check(server, zone)
|
||||
wait_for_log(
|
||||
"ns9/named.run",
|
||||
zone,
|
||||
"zone {}/IN (signed): checkds: DS response from 10.53.0.5".format(zone),
|
||||
)
|
||||
keystate_check(parent, zone, "DSRemoved")
|
||||
def dspublished_tests(checkds, addr):
|
||||
return [
|
||||
#
|
||||
# 1.1.1: DS is correctly published in parent.
|
||||
# parental-agents: ns2
|
||||
#
|
||||
# The simple case.
|
||||
CheckDSTest(
|
||||
zone=f"good.{checkds}.dspublish.ns2",
|
||||
logs_to_wait_for=(f"DS response from {addr}",),
|
||||
expected_parent_state="DSPublish",
|
||||
),
|
||||
#
|
||||
# 1.1.2: DS is not published in parent.
|
||||
# parental-agents: ns5
|
||||
#
|
||||
CheckDSTest(
|
||||
zone=f"not-yet.{checkds}.dspublish.ns5",
|
||||
logs_to_wait_for=("empty DS response from 10.53.0.5",),
|
||||
expected_parent_state="!DSPublish",
|
||||
),
|
||||
#
|
||||
# 1.1.3: The parental agent is badly configured.
|
||||
# parental-agents: ns6
|
||||
#
|
||||
CheckDSTest(
|
||||
zone=f"bad.{checkds}.dspublish.ns6",
|
||||
logs_to_wait_for=(
|
||||
"bad DS response from 10.53.0.6"
|
||||
if checkds == "explicit"
|
||||
else "error during parental-agents processing",
|
||||
),
|
||||
expected_parent_state="!DSPublish",
|
||||
),
|
||||
#
|
||||
# 1.1.4: DS is published, but has bogus signature.
|
||||
#
|
||||
# TBD
|
||||
#
|
||||
# 1.2.1: DS is correctly published in all parents.
|
||||
# parental-agents: ns2, ns4
|
||||
#
|
||||
CheckDSTest(
|
||||
zone=f"good.{checkds}.dspublish.ns2-4",
|
||||
logs_to_wait_for=(f"DS response from {addr}", "DS response from 10.53.0.4"),
|
||||
expected_parent_state="DSPublish",
|
||||
),
|
||||
#
|
||||
# 1.2.2: DS is not published in some parents.
|
||||
# parental-agents: ns2, ns4, ns5
|
||||
#
|
||||
CheckDSTest(
|
||||
zone=f"incomplete.{checkds}.dspublish.ns2-4-5",
|
||||
logs_to_wait_for=(
|
||||
f"DS response from {addr}",
|
||||
"DS response from 10.53.0.4",
|
||||
"empty DS response from 10.53.0.5",
|
||||
),
|
||||
expected_parent_state="!DSPublish",
|
||||
),
|
||||
#
|
||||
# 1.2.3: One parental agent is badly configured.
|
||||
# parental-agents: ns2, ns4, ns6
|
||||
#
|
||||
CheckDSTest(
|
||||
zone=f"bad.{checkds}.dspublish.ns2-4-6",
|
||||
logs_to_wait_for=(
|
||||
f"DS response from {addr}",
|
||||
"DS response from 10.53.0.4",
|
||||
"bad DS response from 10.53.0.6",
|
||||
),
|
||||
expected_parent_state="!DSPublish",
|
||||
),
|
||||
#
|
||||
# 1.2.4: DS is completely published, bogus signature.
|
||||
#
|
||||
# TBD
|
||||
# TBD: Check with TSIG
|
||||
# TBD: Check with TLS
|
||||
]
|
||||
|
||||
|
||||
def test_checkds_dspublished(named_port):
|
||||
checkds_dspublished(named_port, "explicit", "10.53.0.8")
|
||||
checkds_dspublished(named_port, "yes", "10.53.0.2")
|
||||
def dswithdrawn_tests(checkds, addr):
|
||||
return [
|
||||
#
|
||||
# 2.1.1: DS correctly withdrawn from the parent.
|
||||
# parental-agents: ns5
|
||||
#
|
||||
# The simple case.
|
||||
CheckDSTest(
|
||||
zone=f"good.{checkds}.dsremoved.ns5",
|
||||
logs_to_wait_for=(f"empty DS response from {addr}",),
|
||||
expected_parent_state="DSRemoved",
|
||||
),
|
||||
#
|
||||
# 2.1.2: DS is published in the parent.
|
||||
# parental-agents: ns2
|
||||
#
|
||||
CheckDSTest(
|
||||
zone=f"still-there.{checkds}.dsremoved.ns2",
|
||||
logs_to_wait_for=("DS response from 10.53.0.2",),
|
||||
expected_parent_state="!DSRemoved",
|
||||
),
|
||||
#
|
||||
# 2.1.3: The parental agent is badly configured.
|
||||
# parental-agents: ns6
|
||||
#
|
||||
CheckDSTest(
|
||||
zone=f"bad.{checkds}.dsremoved.ns6",
|
||||
logs_to_wait_for=(
|
||||
"bad DS response from 10.53.0.6"
|
||||
if checkds == "explicit"
|
||||
else "error during parental-agents processing",
|
||||
),
|
||||
expected_parent_state="!DSRemoved",
|
||||
),
|
||||
#
|
||||
# 2.1.4: DS is withdrawn, but has bogus signature.
|
||||
#
|
||||
# TBD
|
||||
#
|
||||
# 2.2.1: DS is correctly withdrawn from all parents.
|
||||
# parental-agents: ns5, ns7
|
||||
#
|
||||
CheckDSTest(
|
||||
zone=f"good.{checkds}.dsremoved.ns5-7",
|
||||
logs_to_wait_for=(
|
||||
f"empty DS response from {addr}",
|
||||
"empty DS response from 10.53.0.7",
|
||||
),
|
||||
expected_parent_state="DSRemoved",
|
||||
),
|
||||
#
|
||||
# 2.2.2: DS is not withdrawn from some parents.
|
||||
# parental-agents: ns2, ns5, ns7
|
||||
#
|
||||
CheckDSTest(
|
||||
zone=f"incomplete.{checkds}.dsremoved.ns2-5-7",
|
||||
logs_to_wait_for=(
|
||||
"DS response from 10.53.0.2",
|
||||
f"empty DS response from {addr}",
|
||||
"empty DS response from 10.53.0.7",
|
||||
),
|
||||
expected_parent_state="!DSRemoved",
|
||||
),
|
||||
#
|
||||
# 2.2.3: One parental agent is badly configured.
|
||||
# parental-agents: ns5, ns6, ns7
|
||||
#
|
||||
CheckDSTest(
|
||||
zone=f"bad.{checkds}.dsremoved.ns5-6-7",
|
||||
logs_to_wait_for=(
|
||||
f"empty DS response from {addr}",
|
||||
"empty DS response from 10.53.0.7",
|
||||
"bad DS response from 10.53.0.6",
|
||||
),
|
||||
expected_parent_state="!DSRemoved",
|
||||
),
|
||||
#
|
||||
# 2.2.4:: DS is removed completely, bogus signature.
|
||||
#
|
||||
# TBD
|
||||
]
|
||||
|
||||
|
||||
def test_checkds_dswithdrawn(named_port):
|
||||
checkds_dswithdrawn(named_port, "explicit", "10.53.0.10")
|
||||
checkds_dswithdrawn(named_port, "yes", "10.53.0.5")
|
||||
checkds_no_tests = [
|
||||
CheckDSTest(
|
||||
zone="good.no.dspublish.ns2",
|
||||
logs_to_wait_for=(),
|
||||
expected_parent_state="!DSPublish",
|
||||
),
|
||||
CheckDSTest(
|
||||
zone="good.no.dspublish.ns2-4",
|
||||
logs_to_wait_for=(),
|
||||
expected_parent_state="!DSPublish",
|
||||
),
|
||||
CheckDSTest(
|
||||
zone="good.no.dsremoved.ns5",
|
||||
logs_to_wait_for=(),
|
||||
expected_parent_state="!DSRemoved",
|
||||
),
|
||||
CheckDSTest(
|
||||
zone="good.no.dsremoved.ns5-7",
|
||||
logs_to_wait_for=(),
|
||||
expected_parent_state="!DSRemoved",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def test_checkds_no(named_port):
|
||||
# We create resolver instances that will be used to send queries.
|
||||
server = dns.resolver.Resolver()
|
||||
server.nameservers = ["10.53.0.9"]
|
||||
server.port = named_port
|
||||
checkds_tests = (
|
||||
parental_agents_tests
|
||||
+ no_ent_tests
|
||||
+ dspublished_tests("explicit", "10.53.0.8")
|
||||
+ dspublished_tests("yes", "10.53.0.2")
|
||||
+ dswithdrawn_tests("explicit", "10.53.0.10")
|
||||
+ dswithdrawn_tests("yes", "10.53.0.5")
|
||||
+ checkds_no_tests
|
||||
)
|
||||
|
||||
parent = dns.resolver.Resolver()
|
||||
parent.nameservers = ["10.53.0.2"]
|
||||
parent.port = named_port
|
||||
|
||||
zone_check(server, "good.no.dspublish.ns2")
|
||||
keystate_check(parent, "good.no.dspublish.ns2", "!DSPublish")
|
||||
@pytest.mark.parametrize("params", checkds_tests, ids=lambda t: t.zone)
|
||||
def test_checkds(named_port, servers, params):
|
||||
# Wait until the provided zone is signed and then verify its DNSSEC data.
|
||||
zone_check(servers["ns9"], params.zone)
|
||||
|
||||
zone_check(server, "good.no.dspublish.ns2-4")
|
||||
keystate_check(parent, "good.no.dspublish.ns2-4", "!DSPublish")
|
||||
# Wait until all the expected log lines are found in the log file for the
|
||||
# provided server.
|
||||
for log_string in params.logs_to_wait_for:
|
||||
with servers["ns9"].watch_log_from_start() as watcher:
|
||||
line = f"zone {params.zone}/IN (signed): checkds: {log_string}"
|
||||
watcher.wait_for_line(line)
|
||||
|
||||
zone_check(server, "good.no.dsremoved.ns5")
|
||||
keystate_check(parent, "good.no.dsremoved.ns5", "!DSRemoved")
|
||||
|
||||
zone_check(server, "good.no.dsremoved.ns5-7")
|
||||
keystate_check(parent, "good.no.dsremoved.ns5-7", "!DSRemoved")
|
||||
# Check whether key states on the parent server provided match
|
||||
# expectations.
|
||||
keystate_check(servers["ns2"], params.zone, params.expected_parent_state)
|
||||
|
||||
@@ -13,6 +13,8 @@ import logging
|
||||
import os
|
||||
import pytest
|
||||
|
||||
import isctest
|
||||
|
||||
|
||||
# ======================= LEGACY=COMPATIBLE FIXTURES =========================
|
||||
# The following fixtures are designed to work with both pytest system test
|
||||
@@ -41,6 +43,33 @@ def control_port():
|
||||
return int(os.environ.get("CONTROLPORT", default=9953))
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def rndc_logger():
|
||||
formatter = logging.Formatter("[%(asctime)s] %(message)s")
|
||||
handler = logging.FileHandler("rndc.log")
|
||||
handler.setFormatter(formatter)
|
||||
logger = logging.getLogger("rndc")
|
||||
logger.addHandler(handler)
|
||||
logger.setLevel("DEBUG")
|
||||
return logger
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def servers(named_port, control_port, rndc_logger):
|
||||
instances = {}
|
||||
with os.scandir() as iterator:
|
||||
for entry in iterator:
|
||||
if entry.is_dir():
|
||||
try:
|
||||
dir_name = entry.name
|
||||
ports = isctest.NamedPorts(dns=named_port, rndc=control_port)
|
||||
instance = isctest.NamedInstance(dir_name, ports, rndc_logger)
|
||||
instances[dir_name] = instance
|
||||
except ValueError:
|
||||
continue
|
||||
return instances
|
||||
|
||||
|
||||
if os.getenv("LEGACY_TEST_RUNNER", "0") != "0":
|
||||
|
||||
@pytest.fixture
|
||||
@@ -549,7 +578,9 @@ else:
|
||||
perl("testsock.pl", ["-p", env["PORT"]])
|
||||
except subprocess.CalledProcessError as exc:
|
||||
mlogger.error("testsock.pl: exited with code %d", exc.returncode)
|
||||
pytest.skip("Network interface aliases not set up.")
|
||||
pytest.skip(
|
||||
"Network interface aliases not set up.\n Run `sudo sh bin/tests/system/ifconfig.sh up` to set them up."
|
||||
)
|
||||
|
||||
def check_prerequisites():
|
||||
try:
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
|
||||
#
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
#
|
||||
# See the COPYRIGHT file distributed with this work for additional
|
||||
# information regarding copyright ownership.
|
||||
|
||||
rm -f ns1/named.conf
|
||||
rm -f ns*/named.lock
|
||||
rm -f ns*/named.run
|
||||
rm -f ns*/named.memstats
|
||||
rm -f dig.out.test*
|
||||
rm -f ns*/managed-keys.bind*
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (C) Internet Systems Consortium, Inc. ("ISC")
|
||||
*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* See the COPYRIGHT file distributed with this work for additional
|
||||
* information regarding copyright ownership.
|
||||
*/
|
||||
|
||||
key rndc_key {
|
||||
algorithm @DEFAULT_HMAC@;
|
||||
secret "1234abcd8765";
|
||||
};
|
||||
|
||||
controls {
|
||||
inet 10.53.0.1 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
|
||||
};
|
||||
|
||||
options {
|
||||
query-source address 10.53.0.1;
|
||||
notify-source 10.53.0.1;
|
||||
transfer-source 10.53.0.1;
|
||||
port @PORT@;
|
||||
pid-file "named.pid";
|
||||
listen-on { 10.53.0.1; };
|
||||
listen-on-v6 { none; };
|
||||
recursion yes;
|
||||
dnssec-validation no;
|
||||
deny-answer-addresses { 192.0.2.0/24; 2001:db8:beef::/48; }
|
||||
except-from { "example.org"; };
|
||||
deny-answer-aliases { "example.org"; }
|
||||
except-from { "goodcname.example.net";
|
||||
"gooddname.example.net"; };
|
||||
allow-query {!10.53.0.8; any; };
|
||||
};
|
||||
|
||||
zone "." {
|
||||
type hint;
|
||||
file "root.hint";
|
||||
};
|
||||
@@ -12,6 +12,5 @@
|
||||
# information regarding copyright ownership.
|
||||
|
||||
. ../conf.sh
|
||||
|
||||
$SHELL clean.sh
|
||||
copy_setports ns1/named1.conf.in ns1/named.conf
|
||||
# TODO: Find a way to do this in pytest and then delete this file
|
||||
copy_setports ns1/manual_empty_zones.conf.in ns1/named.conf
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
|
||||
#
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
#
|
||||
# See the COPYRIGHT file distributed with this work for additional
|
||||
# information regarding copyright ownership.
|
||||
|
||||
set -e
|
||||
|
||||
. ../conf.sh
|
||||
|
||||
DIGOPTS="-p ${PORT}"
|
||||
RNDCCMD="$RNDC -c ../common/rndc.conf -p ${CONTROLPORT} -s"
|
||||
|
||||
status=0
|
||||
n=0
|
||||
|
||||
n=$((n + 1))
|
||||
echo_i "check that switching to automatic empty zones works ($n)"
|
||||
ret=0
|
||||
rndc_reload ns1 10.53.0.1
|
||||
|
||||
copy_setports ns1/named2.conf.in ns1/named.conf
|
||||
$RNDCCMD 10.53.0.1 reload > /dev/null || ret=1
|
||||
sleep 5
|
||||
|
||||
$DIG $DIGOPTS +vc version.bind txt ch @10.53.0.1 > /dev/null || ret=1
|
||||
if [ $ret != 0 ]; then echo_i "failed"; fi
|
||||
status=$((status + ret))
|
||||
|
||||
n=$((n + 1))
|
||||
echo_i "check that allow-transfer { none; } works ($n)"
|
||||
ret=0
|
||||
$DIG $DIGOPTS axfr 10.in-addr.arpa @10.53.0.1 +all > dig.out.test$n || ret=1
|
||||
grep "status: REFUSED" dig.out.test$n > /dev/null || ret=1
|
||||
if [ $ret != 0 ]; then echo_i "failed"; fi
|
||||
status=$((status + ret))
|
||||
|
||||
echo_i "exit status: $status"
|
||||
[ $status -eq 0 ] || exit 1
|
||||
34
bin/tests/system/emptyzones/tests_emptyzones.py
Normal file
34
bin/tests/system/emptyzones/tests_emptyzones.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
|
||||
#
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
#
|
||||
# See the COPYRIGHT file distributed with this work for additional
|
||||
# information regarding copyright ownership.
|
||||
|
||||
from typing import Dict
|
||||
|
||||
import isctest
|
||||
|
||||
|
||||
def test_emptyzones(servers: Dict[str, isctest.NamedInstance]):
|
||||
"""check that switching to automatic empty zones works"""
|
||||
ns1 = servers["ns1"]
|
||||
|
||||
# TODO: these could really be one call
|
||||
# something like: ns1.reconfig("automatic_empty_zones.conf.in")
|
||||
ns1.copy_setports("automatic_empty_zones.conf.in", "named.conf")
|
||||
ns1.reload()
|
||||
|
||||
ns1.tcp_query("version.bind", "TXT", "CH")
|
||||
|
||||
|
||||
def test_emptyzones_allow_transfer_none(servers: Dict[str, isctest.NamedInstance]):
|
||||
"""check allow-transfer { none; } is correctly inherited from automatic empty zone"""
|
||||
ns1 = servers["ns1"]
|
||||
ns1.copy_setports("automatic_empty_zones_deny_transfer.conf.in", "named.conf")
|
||||
ns1.reload()
|
||||
ns1.tcp_query("10.in-addr.arpa", "AXFR").expect_refused()
|
||||
@@ -1,14 +0,0 @@
|
||||
# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
|
||||
#
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
#
|
||||
# See the COPYRIGHT file distributed with this work for additional
|
||||
# information regarding copyright ownership.
|
||||
|
||||
|
||||
def test_emptyzones(run_tests_sh):
|
||||
run_tests_sh()
|
||||
531
bin/tests/system/isctest.py
Normal file
531
bin/tests/system/isctest.py
Normal file
@@ -0,0 +1,531 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
|
||||
#
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
#
|
||||
# See the COPYRIGHT file distributed with this work for additional
|
||||
# information regarding copyright ownership.
|
||||
|
||||
from typing import Any, Dict, NamedTuple, Optional, TextIO, Union
|
||||
|
||||
import abc
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
import dns.message
|
||||
import dns.name
|
||||
import dns.rcode
|
||||
import dns.rdataclass
|
||||
import dns.rdatatype
|
||||
import dns.query
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
|
||||
class WatchLog:
|
||||
|
||||
"""
|
||||
Wait for a log message to appear in a text file.
|
||||
|
||||
This class should not be used directly; instead, its subclasses,
|
||||
`WatchLogFromStart` and `WatchLogFromHere`, should be used. For `named`
|
||||
instances used in system tests, it is recommended to use the
|
||||
`watch_log_from_start()` and `watch_log_from_here()` helper methods exposed
|
||||
by the `NamedInstance` class (see below for recommended usage patterns).
|
||||
"""
|
||||
|
||||
def __init__(self, path: str) -> None:
|
||||
"""
|
||||
`path` is the path to the log file to watch.
|
||||
|
||||
Every instance of this class must call one of the `wait_for_*()`
|
||||
methods exactly once or else an `Exception` is thrown.
|
||||
|
||||
>>> with WatchLogFromStart("/dev/null") as watcher:
|
||||
... print("Just print something without waiting for a log line")
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
Exception: wait_for_*() was not called
|
||||
|
||||
>>> with WatchLogFromHere("/dev/null") as watcher:
|
||||
... try:
|
||||
... watcher.wait_for_line("foo", timeout=0)
|
||||
... except TimeoutError:
|
||||
... pass
|
||||
... try:
|
||||
... watcher.wait_for_lines({"bar": 42}, timeout=0)
|
||||
... except TimeoutError:
|
||||
... pass
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
Exception: wait_for_*() was already called
|
||||
"""
|
||||
self._fd = None # type: Optional[TextIO]
|
||||
self._path = path
|
||||
self._wait_function_called = False
|
||||
|
||||
def wait_for_line(self, string: str, timeout: int = 10) -> None:
|
||||
"""
|
||||
Block execution until a line containing the provided `string` appears
|
||||
in the log file. Return `None` once the line is found or raise a
|
||||
`TimeoutError` after `timeout` seconds (default: 10) if `string` does
|
||||
not appear in the log file. (Catching this exception is discouraged as
|
||||
it indicates that the test code did not behave as expected.)
|
||||
|
||||
Recommended use:
|
||||
|
||||
```python
|
||||
import isctest
|
||||
|
||||
def test_foo(servers):
|
||||
with servers["ns1"].watch_log_from_here() as watcher:
|
||||
# ... do stuff here ...
|
||||
watcher.wait_for_line("foo bar")
|
||||
```
|
||||
|
||||
One of `wait_for_line()` or `wait_for_lines()` must be called exactly
|
||||
once for every `WatchLogFrom*` instance.
|
||||
|
||||
>>> # For `WatchLogFromStart`, `wait_for_line()` returns without
|
||||
>>> # raising an exception as soon as the line being looked for appears
|
||||
>>> # anywhere in the file, no matter whether that happens before of
|
||||
>>> # after the `with` statement is reached.
|
||||
>>> import tempfile
|
||||
>>> with tempfile.NamedTemporaryFile("w") as file:
|
||||
... print("foo", file=file, flush=True)
|
||||
... with WatchLogFromStart(file.name) as watcher:
|
||||
... retval = watcher.wait_for_line("foo", timeout=1)
|
||||
>>> print(retval)
|
||||
None
|
||||
>>> with tempfile.NamedTemporaryFile("w") as file:
|
||||
... with WatchLogFromStart(file.name) as watcher:
|
||||
... print("foo", file=file, flush=True)
|
||||
... retval = watcher.wait_for_line("foo", timeout=1)
|
||||
>>> print(retval)
|
||||
None
|
||||
|
||||
>>> # For `WatchLogFromHere`, `wait_for_line()` only returns without
|
||||
>>> # raising an exception if the string being looked for appears in
|
||||
>>> # the log file after the `with` statement is reached.
|
||||
>>> import tempfile
|
||||
>>> with tempfile.NamedTemporaryFile("w") as file:
|
||||
... print("foo", file=file, flush=True)
|
||||
... with WatchLogFromHere(file.name) as watcher:
|
||||
... watcher.wait_for_line("foo", timeout=1) #doctest: +ELLIPSIS
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
TimeoutError: Timeout reached watching ...
|
||||
>>> with tempfile.NamedTemporaryFile("w") as file:
|
||||
... print("foo", file=file, flush=True)
|
||||
... with WatchLogFromHere(file.name) as watcher:
|
||||
... print("foo", file=file, flush=True)
|
||||
... retval = watcher.wait_for_line("foo", timeout=1)
|
||||
>>> print(retval)
|
||||
None
|
||||
"""
|
||||
return self._wait_for({string: None}, timeout)
|
||||
|
||||
def wait_for_lines(self, strings: Dict[str, Any], timeout: int = 10) -> None:
|
||||
"""
|
||||
Block execution until a line of interest appears in the log file. This
|
||||
function is a "multi-match" variant of `wait_for_line()` which is
|
||||
useful when some action may cause several different (mutually
|
||||
exclusive) messages to appear in the log file.
|
||||
|
||||
`strings` is a `dict` associating each string to look for with the
|
||||
value this function should return when that string is found in the log
|
||||
file. If none of the `strings` being looked for appear in the log file
|
||||
after `timeout` seconds (default: 10), a `TimeoutError` is raised.
|
||||
(Catching this exception is discouraged as it indicates that the test
|
||||
code did not behave as expected.)
|
||||
|
||||
`strings` are assumed to be mutually exclusive; no guarantees are made
|
||||
about the order in which these `strings` will be looked for in any
|
||||
single line. Values provided in the `strings` dictionary (i.e. values
|
||||
which this function is expected to return upon a successful match) can
|
||||
be of any type.
|
||||
|
||||
Recommended use:
|
||||
|
||||
```python
|
||||
import isctest
|
||||
|
||||
def test_foo(servers):
|
||||
triggers = {
|
||||
"message A": "value returned when message A is found",
|
||||
"message B": "value returned when message B is found",
|
||||
}
|
||||
with servers["ns1"].watch_log_from_here() as watcher:
|
||||
# ... do stuff here ...
|
||||
retval = watcher.wait_for_lines(triggers)
|
||||
```
|
||||
|
||||
One of `wait_for_line()` or `wait_for_lines()` must be called exactly
|
||||
once for every `WatchLogFromHere` instance.
|
||||
|
||||
>>> # Different values must be returned depending on which line is
|
||||
>>> # found in the log file.
|
||||
>>> import tempfile
|
||||
>>> triggers = {"foo": 42, "bar": 1337}
|
||||
>>> with tempfile.NamedTemporaryFile("w") as file:
|
||||
... print("foo", file=file, flush=True)
|
||||
... with WatchLogFromStart(file.name) as watcher:
|
||||
... retval1 = watcher.wait_for_lines(triggers, timeout=1)
|
||||
... with WatchLogFromHere(file.name) as watcher:
|
||||
... print("bar", file=file, flush=True)
|
||||
... retval2 = watcher.wait_for_lines(triggers, timeout=1)
|
||||
>>> print(retval1)
|
||||
42
|
||||
>>> print(retval2)
|
||||
1337
|
||||
"""
|
||||
return self._wait_for(strings, timeout)
|
||||
|
||||
def _wait_for(self, strings: Dict[str, Any], timeout: int) -> Any:
|
||||
"""
|
||||
Block execution until one of the `strings` being looked for appears in
|
||||
the log file. Raise a `TimeoutError` if none of the `strings` being
|
||||
looked for are found in the log file for `timeout` seconds.
|
||||
"""
|
||||
if self._wait_function_called:
|
||||
raise Exception("wait_for_*() was already called")
|
||||
self._wait_function_called = True
|
||||
if not self._fd:
|
||||
raise Exception("No file to watch")
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
for line in self._fd.readlines():
|
||||
for string, retval in strings.items():
|
||||
if string in line:
|
||||
return retval
|
||||
time.sleep(0.1)
|
||||
raise TimeoutError(
|
||||
"Timeout reached watching {} for {}".format(
|
||||
self._path, list(strings.keys())
|
||||
)
|
||||
)
|
||||
|
||||
def __enter__(self) -> Any:
|
||||
self._fd = open(self._path, encoding="utf-8")
|
||||
self._seek_on_enter()
|
||||
return self
|
||||
|
||||
def _seek_on_enter(self) -> None:
|
||||
"""
|
||||
This method is responsible for setting the file position indicator for
|
||||
the file being watched when execution reaches the __enter__() method.
|
||||
It is expected to be set differently depending on which `WatchLog`
|
||||
subclass is used. Since the base `WatchLog` class should not be used
|
||||
directly, raise an exception upon any attempt of such use.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def __exit__(self, *_: Any) -> None:
|
||||
if not self._wait_function_called:
|
||||
raise Exception("wait_for_*() was not called")
|
||||
if self._fd:
|
||||
self._fd.close()
|
||||
|
||||
|
||||
class WatchLogFromStart(WatchLog):
|
||||
"""
|
||||
A `WatchLog` subclass which looks for the provided string(s) in the entire
|
||||
log file.
|
||||
"""
|
||||
|
||||
def _seek_on_enter(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class WatchLogFromHere(WatchLog):
|
||||
"""
|
||||
A `WatchLog` subclass which only looks for the provided string(s) in the
|
||||
portion of the log file which is appended to it after the `with` statement
|
||||
is reached.
|
||||
"""
|
||||
|
||||
def _seek_on_enter(self) -> None:
|
||||
if self._fd:
|
||||
self._fd.seek(0, os.SEEK_END)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class RNDCExecutor(abc.ABC):
|
||||
|
||||
"""
|
||||
An interface which RNDC executors have to implement in order for the
|
||||
`NamedInstance` class to be able to use them.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def call(self, ip: str, port: int, command: str) -> str:
|
||||
"""
|
||||
Send RNDC `command` to the `named` instance at `ip:port` and return the
|
||||
server's response.
|
||||
"""
|
||||
|
||||
|
||||
class RNDCException(Exception):
|
||||
"""
|
||||
Raised by classes implementing the `RNDCExecutor` interface when sending an
|
||||
RNDC command fails for any reason.
|
||||
"""
|
||||
|
||||
|
||||
class RNDCBinaryExecutor(RNDCExecutor):
|
||||
|
||||
"""
|
||||
An `RNDCExecutor` which sends RNDC commands to servers using the `rndc`
|
||||
binary.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
This class needs the `RNDC` environment variable to be set to the path
|
||||
to the `rndc` binary to use.
|
||||
"""
|
||||
rndc_path = os.environ.get("RNDC", "/usr/sbin/rndc")
|
||||
rndc_conf = os.path.join("..", "common", "rndc.conf")
|
||||
self._base_cmdline = [rndc_path, "-c", rndc_conf]
|
||||
|
||||
def call(self, ip: str, port: int, command: str) -> str:
|
||||
"""
|
||||
Send RNDC `command` to the `named` instance at `ip:port` and return the
|
||||
server's response.
|
||||
"""
|
||||
cmdline = self._base_cmdline[:]
|
||||
cmdline.extend(["-s", ip])
|
||||
cmdline.extend(["-p", str(port)])
|
||||
cmdline.extend(command.split())
|
||||
|
||||
try:
|
||||
return subprocess.check_output(
|
||||
cmdline, stderr=subprocess.STDOUT, timeout=10, encoding="utf-8"
|
||||
)
|
||||
except subprocess.SubprocessError as exc:
|
||||
msg = getattr(exc, "output", "RNDC exception occurred")
|
||||
raise RNDCException(msg) from exc
|
||||
|
||||
|
||||
class NamedPorts(NamedTuple):
|
||||
dns: int = 53
|
||||
rndc: int = 953
|
||||
|
||||
|
||||
class NamedInstance:
|
||||
|
||||
"""
|
||||
A class representing a `named` instance used in a system test.
|
||||
|
||||
This class is expected to be instantiated as part of the `servers` fixture:
|
||||
|
||||
```python
|
||||
def test_foo(servers):
|
||||
servers["ns1"].rndc("status")
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
identifier: str,
|
||||
ports: NamedPorts = NamedPorts(),
|
||||
rndc_logger: logging.Logger = logging.getLogger(),
|
||||
rndc_executor: RNDCExecutor = RNDCBinaryExecutor(),
|
||||
) -> None:
|
||||
"""
|
||||
`identifier` must be an `ns<X>` string, where `<X>` is an integer
|
||||
identifier of the `named` instance this object should represent.
|
||||
|
||||
`ports` is the `NamedPorts` instance listing the UDP/TCP ports on which
|
||||
this `named` instance is listening for various types of traffic (both
|
||||
DNS traffic and RNDC commands).
|
||||
|
||||
`rndc_logger` is the `logging.Logger` to use for logging RNDC
|
||||
commands sent to this `named` instance.
|
||||
|
||||
`rndc_executor` is an object implementing the `RNDCExecutor` interface
|
||||
that is used for executing RNDC commands on this `named` instance.
|
||||
"""
|
||||
regex_match = re.match(r"^ns(?P<index>[0-9]{1,2})$", identifier)
|
||||
if not regex_match:
|
||||
raise ValueError("Invalid named instance identifier" + identifier)
|
||||
self.ip = "10.53.0." + regex_match.group("index")
|
||||
self.ports = ports
|
||||
self.identifier = identifier
|
||||
|
||||
self._log_file = os.path.join(identifier, "named.run")
|
||||
self._rndc_executor = rndc_executor
|
||||
self._rndc_logger = rndc_logger
|
||||
|
||||
loader = FileSystemLoader(identifier)
|
||||
self._jinja_env = Environment(
|
||||
loader=loader, variable_start_string="@", variable_end_string="@"
|
||||
)
|
||||
|
||||
def rndc(self, command: str, ignore_errors: bool = False, log: bool = True) -> str:
|
||||
"""
|
||||
Send `command` to this named instance using RNDC. Return the server's
|
||||
response.
|
||||
|
||||
If the RNDC command fails, an `RNDCException` is raised unless
|
||||
`ignore_errors` is set to `True`.
|
||||
|
||||
The RNDC command will be logged to `rndc.log` (along with the server's
|
||||
response) unless `log` is set to `False`.
|
||||
|
||||
>>> # Instances of the `NamedInstance` class are expected to be passed
|
||||
>>> # to pytest tests as fixtures; here, some instances are created
|
||||
>>> # directly (with a fake RNDC executor) so that doctest can work.
|
||||
>>> import unittest.mock
|
||||
>>> mock_rndc_executor = unittest.mock.Mock()
|
||||
>>> ns1 = NamedInstance("ns1", rndc_executor=mock_rndc_executor)
|
||||
>>> ns2 = NamedInstance("ns2", rndc_executor=mock_rndc_executor)
|
||||
>>> ns3 = NamedInstance("ns3", rndc_executor=mock_rndc_executor)
|
||||
>>> ns4 = NamedInstance("ns4", rndc_executor=mock_rndc_executor)
|
||||
|
||||
>>> # Send the "status" command to ns1. An `RNDCException` will be
|
||||
>>> # raised if the RNDC command fails. This command will be logged.
|
||||
>>> response = ns1.rndc("status")
|
||||
|
||||
>>> # Send the "thaw foo" command to ns2. No exception will be raised
|
||||
>>> # in case the RNDC command fails. This command will be logged
|
||||
>>> # (even if it fails).
|
||||
>>> response = ns2.rndc("thaw foo", ignore_errors=True)
|
||||
|
||||
>>> # Send the "stop" command to ns3. An `RNDCException` will be
|
||||
>>> # raised if the RNDC command fails, but this command will not be
|
||||
>>> # logged (the server's response will still be returned to the
|
||||
>>> # caller, though).
|
||||
>>> response = ns3.rndc("stop", log=False)
|
||||
|
||||
>>> # Send the "halt" command to ns4 in "fire & forget mode": no
|
||||
>>> # exceptions will be raised and no logging will take place (the
|
||||
>>> # server's response will still be returned to the caller, though).
|
||||
>>> response = ns4.rndc("stop", ignore_errors=True, log=False)
|
||||
"""
|
||||
try:
|
||||
response = self._rndc_executor.call(self.ip, self.ports.rndc, command)
|
||||
if log:
|
||||
self._rndc_log(command, response)
|
||||
except RNDCException as exc:
|
||||
response = str(exc)
|
||||
if log:
|
||||
self._rndc_log(command, response)
|
||||
if not ignore_errors:
|
||||
raise
|
||||
|
||||
return response
|
||||
|
||||
def watch_log_from_start(self) -> WatchLogFromStart:
|
||||
"""
|
||||
Return an instance of the `WatchLogFromStart` context manager for this
|
||||
`named` instance's log file.
|
||||
"""
|
||||
return WatchLogFromStart(self._log_file)
|
||||
|
||||
def watch_log_from_here(self) -> WatchLogFromHere:
|
||||
"""
|
||||
Return an instance of the `WatchLogFromHere` context manager for this
|
||||
`named` instance's log file.
|
||||
"""
|
||||
return WatchLogFromHere(self._log_file)
|
||||
|
||||
def reconfigure(self) -> None:
|
||||
"""
|
||||
Reconfigure this named `instance` and wait until reconfiguration is
|
||||
finished. Raise an `RNDCException` if reconfiguration fails.
|
||||
"""
|
||||
with self.watch_log_from_here() as watcher:
|
||||
self.rndc("reconfig")
|
||||
watcher.wait_for_line("any newly configured zones are now loaded")
|
||||
|
||||
def reload(self, zone: Optional[str] = None) -> None:
|
||||
"""
|
||||
TODO: document this and maybe split it into reload and reload_zone as
|
||||
it's weird that one is synchronous and the other isn't. Also I don't
|
||||
like the fact that we rely on the exact wording of log messages.
|
||||
"""
|
||||
if zone is not None:
|
||||
self.rndc(f"reload {zone}")
|
||||
return
|
||||
with self.watch_log_from_here() as watcher:
|
||||
self.rndc("reload")
|
||||
watcher.wait_for_line("all zones loaded")
|
||||
|
||||
def _rndc_log(self, command: str, response: str) -> None:
|
||||
"""
|
||||
Log an `rndc` invocation (and its output) to the `rndc.log` file in the
|
||||
current working directory.
|
||||
"""
|
||||
fmt = '%(ip)s: "%(command)s"\n%(separator)s\n%(response)s%(separator)s'
|
||||
self._rndc_logger.info(
|
||||
fmt,
|
||||
{
|
||||
"ip": self.ip,
|
||||
"command": command,
|
||||
"separator": "-" * 80,
|
||||
"response": response,
|
||||
},
|
||||
)
|
||||
|
||||
def copy_setports(
|
||||
self, path_in: Union[str, os.PathLike], path_out: Union[str, os.PathLike]
|
||||
):
|
||||
template = self._jinja_env.get_template(path_in)
|
||||
with open(os.path.join(self.identifier, path_out), "w") as f:
|
||||
f.write(template.render(os.environ))
|
||||
|
||||
# TODO: find a nice way for this to support flags
|
||||
def tcp_query(
|
||||
self,
|
||||
qname: Union[dns.name.Name, str],
|
||||
qtype: Union[dns.rdatatype.RdataType, str],
|
||||
qclass: Union[dns.rdataclass.RdataClass, str] = dns.rdataclass.IN,
|
||||
):
|
||||
question = dns.message.make_query(qname, qtype, qclass)
|
||||
reply = dns.query.tcp(question, self.ip, timeout=5, port=self.ports.dns)
|
||||
return CheckableReply(reply)
|
||||
|
||||
|
||||
# TODO: find a better name for this
|
||||
class ReplyError(Exception):
|
||||
def __init__(self, what, expected, got):
|
||||
self.what = what
|
||||
self.expected = expected
|
||||
self.got = got
|
||||
|
||||
def __str__(self):
|
||||
return f"Expected {self.what} {self.expected} got {self.got}"
|
||||
|
||||
|
||||
# TODO: find a better name for this
|
||||
class CheckableReply:
|
||||
"""
|
||||
Wrapper class for `dns.message.Message` to easily check different attributes
|
||||
of an incomming DNS message.
|
||||
"""
|
||||
|
||||
def __init__(self, message: dns.message.Message):
|
||||
self.message = message
|
||||
|
||||
def expect_rcode(self, rcode: dns.rcode.Rcode) -> "CheckableReply":
|
||||
if self.message.rcode() != rcode:
|
||||
raise ReplyError(
|
||||
"rcode",
|
||||
dns.rcode.Rcode.to_text(rcode),
|
||||
dns.rcode.Rcode.to_text(self.message.rcode()),
|
||||
)
|
||||
return self
|
||||
|
||||
def expect_noerror(self):
|
||||
return self.expect_rcode(dns.rcode.NOERROR)
|
||||
|
||||
def expect_refused(self):
|
||||
return self.expect_rcode(dns.rcode.REFUSED)
|
||||
@@ -114,7 +114,7 @@ if [ "${srcdir}" != "${builddir}" ]; then
|
||||
cp -a "${srcdir}/common" "${builddir}"
|
||||
fi
|
||||
# Some tests require additional files to work for out-of-tree test runs.
|
||||
for file in ckdnsrps.sh conftest.py digcomp.pl ditch.pl fromhex.pl get_core_dumps.sh kasp.sh packet.pl pytest_custom_markers.py start.pl stop.pl testcrypto.sh; do
|
||||
for file in ckdnsrps.sh conftest.py digcomp.pl ditch.pl fromhex.pl get_core_dumps.sh isctest.py kasp.sh packet.pl pytest_custom_markers.py start.pl stop.pl testcrypto.sh; do
|
||||
if [ ! -r "${file}" ]; then
|
||||
cp -a "${srcdir}/${file}" "${builddir}"
|
||||
fi
|
||||
|
||||
@@ -25,8 +25,10 @@ pytest.importorskip("dns", minversion="2.0.0")
|
||||
import dns.exception
|
||||
import dns.resolver
|
||||
|
||||
import isctest
|
||||
|
||||
def do_work(named_proc, resolver, rndc_cmd, kill_method, n_workers, n_queries):
|
||||
|
||||
def do_work(named_proc, resolver, instance, kill_method, n_workers, n_queries):
|
||||
"""Creates a number of A queries to run in parallel
|
||||
in order simulate a slightly more realistic test scenario.
|
||||
|
||||
@@ -48,8 +50,8 @@ def do_work(named_proc, resolver, rndc_cmd, kill_method, n_workers, n_queries):
|
||||
:param resolver: target resolver
|
||||
:type resolver: dns.resolver.Resolver
|
||||
|
||||
:param rndc_cmd: rndc command with default arguments
|
||||
:type rndc_cmd: list of strings, e.g. ["rndc", "-p", "23750"]
|
||||
:param instance: the named instance to send RNDC commands to
|
||||
:type instance: isctest.NamedInstance
|
||||
|
||||
:kill_method: "rndc" or "sigterm"
|
||||
:type kill_method: str
|
||||
@@ -63,9 +65,13 @@ def do_work(named_proc, resolver, rndc_cmd, kill_method, n_workers, n_queries):
|
||||
# pylint: disable-msg=too-many-arguments
|
||||
# pylint: disable-msg=too-many-locals
|
||||
|
||||
# helper function, args must be a list or tuple with arguments to rndc.
|
||||
def launch_rndc(args):
|
||||
return subprocess.call(rndc_cmd + args, timeout=10)
|
||||
# helper function, 'command' is the rndc command to run
|
||||
def launch_rndc(command):
|
||||
try:
|
||||
instance.rndc(command, log=False)
|
||||
return 0
|
||||
except isctest.RNDCException:
|
||||
return -1
|
||||
|
||||
# We're going to execute queries in parallel by means of a thread pool.
|
||||
# dnspython functions block, so we need to circunvent that.
|
||||
@@ -99,13 +105,13 @@ def do_work(named_proc, resolver, rndc_cmd, kill_method, n_workers, n_queries):
|
||||
elif shutdown: # We attempt to stop named in the middle
|
||||
shutdown = False
|
||||
if kill_method == "rndc":
|
||||
futures[executor.submit(launch_rndc, ["stop"])] = "stop"
|
||||
futures[executor.submit(launch_rndc, "stop")] = "stop"
|
||||
else:
|
||||
futures[executor.submit(named_proc.terminate)] = "kill"
|
||||
else:
|
||||
# We attempt to send couple rndc commands while named is
|
||||
# being shutdown
|
||||
futures[executor.submit(launch_rndc, ["-t", "5", "status"])] = "status"
|
||||
futures[executor.submit(launch_rndc, "-t 5 status")] = "status"
|
||||
|
||||
ret_code = -1
|
||||
for future in as_completed(futures):
|
||||
@@ -157,14 +163,14 @@ def wait_for_proc_termination(proc, max_timeout=10):
|
||||
|
||||
|
||||
# We test named shutting down using two methods:
|
||||
# Method 1: using rndc ctop
|
||||
# Method 1: using rndc stop
|
||||
# Method 2: killing with SIGTERM
|
||||
# In both methods named should exit gracefully.
|
||||
@pytest.mark.parametrize(
|
||||
"kill_method",
|
||||
[pytest.param("rndc", marks=pytest.mark.xfail(reason="GL#4060")), "sigterm"],
|
||||
)
|
||||
def test_named_shutdown(named_port, control_port, kill_method):
|
||||
def test_named_shutdown(named_port, kill_method):
|
||||
# pylint: disable-msg=too-many-locals
|
||||
cfg_dir = os.path.join(os.getcwd(), "resolver")
|
||||
assert os.path.isdir(cfg_dir)
|
||||
@@ -175,15 +181,13 @@ def test_named_shutdown(named_port, control_port, kill_method):
|
||||
named = os.getenv("NAMED")
|
||||
assert named is not None
|
||||
|
||||
rndc = os.getenv("RNDC")
|
||||
assert rndc is not None
|
||||
|
||||
# rndc configuration resides in ../common/rndc.conf
|
||||
rndc_cfg = os.path.join("..", "common", "rndc.conf")
|
||||
assert os.path.isfile(rndc_cfg)
|
||||
|
||||
# rndc command with default arguments.
|
||||
rndc_cmd = [rndc, "-c", rndc_cfg, "-p", str(control_port), "-s", "10.53.0.3"]
|
||||
# This test launches and monitors a named instance itself rather than using
|
||||
# bin/tests/system/start.pl, so manually defining a NamedInstance here is
|
||||
# necessary for sending RNDC commands to that instance. This "custom"
|
||||
# instance listens on 10.53.0.3, so use "ns3" as the identifier passed to
|
||||
# the NamedInstance constructor.
|
||||
ports = isctest.NamedPorts(dns=named_port, rndc=os.getenv("CONTROLPORT"))
|
||||
instance = isctest.NamedInstance("ns3", ports)
|
||||
|
||||
# We create a resolver instance that will be used to send queries.
|
||||
resolver = dns.resolver.Resolver()
|
||||
@@ -198,7 +202,7 @@ def test_named_shutdown(named_port, control_port, kill_method):
|
||||
do_work(
|
||||
named_proc,
|
||||
resolver,
|
||||
rndc_cmd,
|
||||
instance,
|
||||
kill_method,
|
||||
n_workers=12,
|
||||
n_queries=16,
|
||||
|
||||
Reference in New Issue
Block a user