]> git.ipfire.org Git - thirdparty/knot-dns.git/commitdiff
tests-extra: initial support for testing Knot with Redis backend
authorLibor Peltan <libor.peltan@nic.cz>
Sun, 27 Jul 2025 08:56:38 +0000 (10:56 +0200)
committerDaniel Salzman <daniel.salzman@nic.cz>
Fri, 12 Sep 2025 14:50:41 +0000 (16:50 +0200)
tests-extra/requirements.txt
tests-extra/tests/redis/basic/test.py [new file with mode: 0644]
tests-extra/tools/dnstest/params.py
tests-extra/tools/dnstest/redis.py [new file with mode: 0644]
tests-extra/tools/dnstest/server.py
tests-extra/tools/dnstest/test.py

index 6ac7ea9d559c0f6c9386e764d1700eecc6215a8e..e2995420a5606e22bdf36e1f234b5f9720ef3452 100644 (file)
@@ -2,3 +2,4 @@ dnspython>=2.2.0
 jsonschema
 psutil
 pyyaml
+setuptools
diff --git a/tests-extra/tests/redis/basic/test.py b/tests-extra/tests/redis/basic/test.py
new file mode 100644 (file)
index 0000000..aff00c4
--- /dev/null
@@ -0,0 +1,73 @@
+#!/usr/bin/env python3
+
+'''Test master-slave-like replication using Redis database.'''
+
+from dnstest.test import Test
+
+t = Test(redis=True)
+
+master = t.server("knot")
+slave = t.server("knot")
+
+zones = t.zone("example.com.")
+
+t.link(zones, master)
+t.link(zones, slave)
+
+master.zonefile_sync = "0"
+
+for z in zones:
+    master.zones[z.name].redis_out = "1"
+    slave.zones[z.name].redis_in = "1"
+    slave.zones[z.name].zfile.remove()
+
+t.start()
+
+master.zones_wait(zones)
+
+master.ctl("zone-flush", wait=True)
+#slave.ctl("zone-reload")
+
+serials = slave.zones_wait(zones)
+t.xfr_diff(master, slave, zones)
+
+for z in zones:
+    up = master.update(z)
+    up.add("suppnot1", 3600, "A", "1.2.3.4")
+    up.send()
+
+t.sleep(2)
+master.ctl("zone-flush", wait=True)
+#slave.ctl("zone-reload")
+
+slave.zones_wait(zones, serials)
+t.xfr_diff(master, slave, zones)
+
+# SOA serial logic rotation
+serials6 = serials5
+
+for i in range(5):
+    if i == 3:
+        slave.ctl("zone-freeze", wait=True)
+
+    for z in zones:
+        if i == 4:
+            serials6[z.name] += 1
+        else:
+            serials6[z.name] += (1 << 30)
+        serials6[z.name] %= (1 << 32)
+        up = master.update(z)
+        up.add(z.name, 3600, "SOA", "dns1 hostmaster %d 10800 3600 1209600 7200" % (serials6[z.name]))
+        up.add("loop", 3600, "AAAA", "1::%d" % i)
+        up.send()
+    master.zones_wait(zones, serials6, equal=True)
+
+slave.ctl("zone-thaw")
+slave.zones_wait(zones, serials6, equal=True)
+t.xfr_diff(master, slave, zones)
+resp = slave.dig("loop." + zones[0].name, "AAAA")
+resp.check(rcode="NOERROR", rdata="1::1")
+resp.check(rcode="NOERROR", rdata="1::4")
+resp.check_count(5, "AAAA")
+
+t.end()
index 7278d3d0d9d9de40d701b4f3583555b6dcf7b1bd..07ace0c21755fa7890507ac6a47a411fe418488b 100644 (file)
@@ -54,6 +54,10 @@ valgrind_flags = get_param("KNOT_TEST_VALGRIND_FLAGS",
 gdb_bin = get_binary("KNOT_TEST_GDB", "gdb")
 # KNOT_TEST_VGDB - vgdb binary.
 vgdb_bin = get_binary("KNOT_TEST_VGDB", "vgdb")
+# KNOT_TEST_REDIS - Redis database server binary.
+redis_bin = get_binary("KNOT_TEST_REDIS", "redis-server")
+# KNOT_TEST_REDIS_CLI - Redis database command line client.
+redis_cli = get_binary("KNOT_TEST_REDIS_CLI", "redis-cli")
 # KNOT_TEST_LIBTOOL - libtool script.
 libtool_bin = get_binary("KNOT_TEST_LIBTOOL", repo_binary("libtool"))
 # KNOT_TEST_LIBKNOT - libknot library.
diff --git a/tests-extra/tools/dnstest/redis.py b/tests-extra/tools/dnstest/redis.py
new file mode 100644 (file)
index 0000000..9989235
--- /dev/null
@@ -0,0 +1,71 @@
+from dnstest.utils import *
+import dnstest.params as params
+import os
+import shutil
+import subprocess
+import time
+
+class Redis(object):
+    def __init__(self, addr, wrk_dir, redis_bin, redis_cli, knotso):
+        self.addr = addr
+        self.port = None
+        self.tls_port = None
+        self.pin = None
+        self.wrk_dir = wrk_dir
+        self.redis_bin = redis_bin
+        self.redis_cli = redis_cli
+        self.knotso = knotso
+        self.proc = None
+        self.monitor = None
+        self.monitor_log = None
+
+        if not os.path.exists(wrk_dir):
+            os.makedirs(wrk_dir)
+
+    def wrk_file(self, filename):
+        return os.path.join(self.wrk_dir, filename)
+
+    def conf_file(self):
+        return self.wrk_file("redis.conf")
+
+    def gen_confile(self):
+        with open(self.conf_file(), "w") as cf:
+            cf.write("dir " + self.wrk_dir + os.linesep)
+            cf.write("logfile " + self.wrk_file("redis.log") + os.linesep)
+            cf.write("loadmodule " + self.knotso + os.linesep)
+            cf.write("bind " + self.addr + os.linesep)
+            cf.write("port " + str(self.port) + os.linesep)
+            cf.write("tls-port " + str(self.tls_port) + os.linesep)
+            cf.write("tls-protocols \"TLSv1.3\"" + os.linesep)
+            cf.write("tls-auth-clients no" + os.linesep)
+            cf.write("tls-key-file key.pem" + os.linesep)
+            cf.write("tls-cert-file cert.pem" + os.linesep)
+            if self.addr != "127.0.0.1" and self.addr != "::1":
+                cf.write("protected-mode no " + os.linesep)
+
+            shutil.copy(os.path.join(params.common_data_dir, "cert", "cert.pem"), self.wrk_dir)
+            shutil.copy(os.path.join(params.common_data_dir, "cert", "key.pem"), self.wrk_dir)
+            keyfile = os.path.join(self.wrk_dir, "key.pem")
+            out = subprocess.check_output(["certtool", "--infile=" + keyfile, "-k"]).rstrip().decode('ascii')
+            self.pin = ssearch(out, r'pin-sha256:([^\n]*)')
+
+    def start(self):
+        self.proc = subprocess.Popen([ self.redis_bin, self.conf_file() ])
+        time.sleep(0.3)
+        monitor_cmd = [ self.redis_cli, "-h", self.addr, "-p", str(self.port), "monitor" ]
+        self.monitor_log = open(os.path.join(self.wrk_dir, "monitor.log"), "a")
+        self.monitor = subprocess.Popen(monitor_cmd, stdout=self.monitor_log, stderr=self.monitor_log)
+
+    def stop(self):
+        if self.monitor:
+            self.monitor.terminate()
+        if self.monitor_log:
+            self.monitor_log.close()
+        if self.proc:
+            self.proc.terminate()
+
+    def cli(self, *params):
+        cmd = [ self.redis_cli, "-h", self.addr, "-p", str(self.port) ] + list(params)
+        p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+        out, _ = p.communicate()
+        return out.decode().strip()
index 02b0b475c94da0c7bdeb973f09d84eeb6dfec99a..137d04defeab4d6552017636f6237e3bf69fa4fb 100644 (file)
@@ -96,6 +96,8 @@ class Zone(object):
         self.zfile = zone_file
         self.masters = set()
         self.slaves = set()
+        self.redis_in = None
+        self.redis_out = None
         self.serial_modulo = None
         self.ddns = ddns
         self.ixfr = ixfr
@@ -237,6 +239,8 @@ class Server(object):
         self.session_log = None
         self.confile = None
 
+        self.redis = None
+
         self.binding_errors = 0
 
     def _check_socket(self, proto, port):
@@ -1882,6 +1886,13 @@ class Knot(Server):
         s.item_str("journal-db-max-size", self.journal_db_size)
         s.item_str("timer-db-max-size", self.timer_db_size)
         s.item_str("catalog-db-max-size", self.catalog_db_size)
+        if self.redis is not None:
+            tls = random.choice([True, False])
+            port = self.redis.tls_port if tls else self.redis.port
+            s.item_str("zone-db-listen", self.redis.addr + "@" + str(port))
+            if tls:
+                s.item_str("zone-db-cert-key", self.redis.pin)
+                s.item_str("zone-db-tls", "on")
         s.end()
 
         s.begin("template")
@@ -1958,6 +1969,9 @@ class Knot(Server):
 
             self.config_xfr(z, s)
 
+            self._str(s, "zone-db-input", z.redis_in)
+            self._str(s, "zone-db-output", z.redis_out)
+
             self._str(s, "serial-policy", self.serial_policy)
             self._str(s, "serial-modulo", z.serial_modulo)
             self._str(s, "ddns-master", self.ddns_master)
index 53bc43e5584c4b471867633ff380fd0b8e6948fc..28ac74ddee6ba0b668d47cc859993d2e71851174 100644 (file)
@@ -16,9 +16,16 @@ from dnstest.utils import *
 from dnstest.context import Context
 import dnstest.params as params
 import dnstest.server
+import dnstest.redis
 import dnstest.keys
 import dnstest.zonefile
 
+def repo_file(*path):
+    module_path = os.path.dirname(os.path.realpath(__file__))
+    repo_path = os.path.realpath(os.path.join(module_path, "..", "..", ".."))
+    file_path = os.path.join(repo_path, *path)
+    return file_path if os.path.isfile(file_path) else None
+
 class Test(object):
     '''Specification of DNS test topology'''
 
@@ -39,7 +46,7 @@ class Test(object):
     rel_time = time.time()
     start_time = 0
 
-    def __init__(self, address=None, tsig=None, stress=True, quic=False, tls=False):
+    def __init__(self, address=None, tsig=None, stress=True, quic=False, tls=False, redis=False):
         if not os.path.exists(Context().out_dir):
             raise Exception("Output directory doesn't exist")
 
@@ -56,6 +63,16 @@ class Test(object):
         else:
             self.addr = Test.LOCAL_ADDR_MULTI[random.choice([4, 6])]
 
+        self.redis = None
+        redis_knotso = repo_file("src", "redis", ".libs", "knot.so")
+        if redis:
+            if params.redis_bin == "":
+                raise Skip("Redis server not available")
+            if redis_knotso is None:
+                raise Skip("Redis knot module not available")
+            self.redis = dnstest.redis.Redis(self.addr, os.path.join(self.out_dir, "redis"),
+                                             params.redis_bin, params.redis_cli, redis_knotso)
+
         self.tsig = None
         if tsig != None:
             if type(tsig) is dnstest.keys.Tsig:
@@ -218,6 +235,8 @@ class Test(object):
             if os.path.isfile(suppressions_file):
                 srv.valgrind.append("--suppressions=%s" % suppressions_file)
 
+        srv.redis = self.redis
+
         self.servers.add(srv)
         return srv
 
@@ -252,6 +271,11 @@ class Test(object):
                 server.xdp_cover_sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
                 server.xdp_cover_sock.bind((server.addr, server.xdp_port))
 
+        if self.redis is not None:
+            self.redis.port = self._gen_port()
+            self.redis.tls_port = self._gen_port()
+            self.redis.gen_confile()
+
         for server in self.servers:
             server.gen_confile()
 
@@ -267,6 +291,9 @@ class Test(object):
 
         self.generate_conf()
 
+        if self.redis:
+            self.redis.start()
+
         def srv_sort(server):
             masters = 0
             for z in server.zones:
@@ -300,6 +327,9 @@ class Test(object):
             else:
                 server.stop(check=check)
 
+        if self.redis:
+            self.redis.stop()
+
     def end(self):
         '''Finish testing'''