From: Aram Sargsyan Date: Fri, 16 Jan 2026 14:02:54 +0000 (+0000) Subject: Add RTT statistics tests both for XML and JSON outputs X-Git-Tag: v9.21.20~24^2~1 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=165a776137d02a688addf403bef452f8276315c5;p=thirdparty%2Fbind9.git Add RTT statistics tests both for XML and JSON outputs Add a resolver instance "ns4" in the statschannel test and a "ans5" instance which adds latency to the queries delegeated to it from the resolver. Make queries which add latency, and compare the expected values to the values received from the statistics channel. --- diff --git a/bin/tests/system/statschannel/ans5/ans.py b/bin/tests/system/statschannel/ans5/ans.py new file mode 100644 index 00000000000..6f7dcc2a4a4 --- /dev/null +++ b/bin/tests/system/statschannel/ans5/ans.py @@ -0,0 +1,56 @@ +""" +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 collections.abc import AsyncGenerator + +import dns.rcode +import dns.rdatatype +import dns.rrset + +from isctest.asyncserver import ( + ControllableAsyncDnsServer, + DnsResponseSend, + QueryContext, + ResponseHandler, +) + + +class DelayedAddressAnswerHandler(ResponseHandler): + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + if qctx.qtype in (dns.rdatatype.A, dns.rdatatype.AAAA): + addr = "192.0.2.1" if qctx.qtype == dns.rdatatype.A else "2001:db8:beef::1" + rrset = dns.rrset.from_text(qctx.qname, 300, qctx.qclass, qctx.qtype, addr) + qctx.response.answer.append(rrset) + + delay = 0 + if ( + len(qctx.qname.labels) >= 2 + and qctx.qname.labels[1] == b"latency" + and qctx.qname.labels[0].isdigit() + ): + delay = int(qctx.qname.labels[0]) / 1000 + yield DnsResponseSend(qctx.response, delay=delay) + + +def main() -> None: + server = ControllableAsyncDnsServer( + default_aa=True, default_rcode=dns.rcode.NOERROR + ) + server.install_response_handler(DelayedAddressAnswerHandler()) + server.run() + + +if __name__ == "__main__": + main() diff --git a/bin/tests/system/statschannel/generic.py b/bin/tests/system/statschannel/generic.py index 0b0ee82de05..02a7eea3830 100644 --- a/bin/tests/system/statschannel/generic.py +++ b/bin/tests/system/statschannel/generic.py @@ -58,6 +58,11 @@ def check_zone_timers(loaded, expires, refresh, loaded_exp): check_loaded(loaded, loaded_exp, now) +def check_rtt(rtt, rtt_expected): + for val in rtt_expected: + assert rtt[val[0]] == val[1] + + # # The output is gibberish, but at least make sure it does not crash. # @@ -225,3 +230,33 @@ def test_traffic(fetch_traffic, **kwargs): data = fetch_traffic(statsip, statsport) check_traffic(data, exp) + + +def test_rtt(fetch_views, **kwargs): + statsip = kwargs["statsip"] + statsport = kwargs["statsport"] + + # auth query, 0 delay is expected, only for "in" + msg = create_msg("a.example2.", "TXT") + ans = isctest.query.tcp(msg, statsip, attempts=1) + isctest.check.noerror(ans) + + # resolver query with a 530ms delay for both "in" and "out" + msg = create_msg("530.latency.example2.", "A") + ans = isctest.query.tcp(msg, statsip, attempts=1) + isctest.check.noerror(ans) + + # resolver query with a 540ms delay for both "in" and "out" + msg = create_msg("540.latency.example2.", "A") + ans = isctest.query.tcp(msg, statsip, attempts=1) + isctest.check.noerror(ans) + + # resolver query with a 730ms delay for both "in" and "out" + msg = create_msg("730.latency.example2.", "A") + ans = isctest.query.tcp(msg, statsip, attempts=1) + isctest.check.noerror(ans) + + data = fetch_views(statsip, statsport) + + check_rtt(data["in-queries-rtt"], [["~0", 1], ["512-575", 2], ["704-767", 1]]) + check_rtt(data["out-queries-rtt"], [["512-575", 2], ["704-767", 1]]) diff --git a/bin/tests/system/statschannel/ns4/example2.db b/bin/tests/system/statschannel/ns4/example2.db new file mode 100644 index 00000000000..2d623eb8cdc --- /dev/null +++ b/bin/tests/system/statschannel/ns4/example2.db @@ -0,0 +1,28 @@ +; 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. + +$ORIGIN . +$TTL 300 ; 5 minutes +example2 IN SOA mname1. . ( + 1 ; serial + 20 ; refresh (20 seconds) + 20 ; retry (20 seconds) + 1814400 ; expire (3 weeks) + 3600 ; minimum (1 hour) + ) +example2. NS ns4.example2. +ns4.example2. A 10.53.0.4 + +$ORIGIN example2. +a A 10.0.0.1 + +latency NS ns5.example2. +ns5.example2. A 10.53.0.5 diff --git a/bin/tests/system/statschannel/ns4/named.conf.j2 b/bin/tests/system/statschannel/ns4/named.conf.j2 new file mode 100644 index 00000000000..4e2c17ee2b1 --- /dev/null +++ b/bin/tests/system/statschannel/ns4/named.conf.j2 @@ -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. + */ + +options { + query-source address 10.53.0.4; + notify-source 10.53.0.4; + transfer-source 10.53.0.4; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.4; }; + listen-on-v6 { none; }; + recursion yes; + dnssec-validation no; + notify no; + minimal-responses no; + version none; // make statistics independent of the version number +}; + +statistics-channels { inet 10.53.0.4 port @EXTRAPORT1@ allow { localhost; }; }; + +key rndc_key { + secret "1234abcd8765"; + algorithm @DEFAULT_HMAC@; +}; + +controls { + inet 10.53.0.4 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; + +zone "example2" { + type primary; + file "example2.db"; + allow-transfer { any; }; +}; diff --git a/bin/tests/system/statschannel/tests_json.py b/bin/tests/system/statschannel/tests_json.py index 30348f4e69f..f1da9e9c826 100755 --- a/bin/tests/system/statschannel/tests_json.py +++ b/bin/tests/system/statschannel/tests_json.py @@ -24,6 +24,7 @@ pytestmark = [ isctest.mark.with_json_c, pytest.mark.extra_artifacts( [ + "ans5/ans.run", "ns2/*.jnl", "ns2/*.signed", "ns2/dsset-*", @@ -62,6 +63,19 @@ def fetch_traffic_json(statsip, statsport): return data["traffic"] +def fetch_rtt_json(statsip, statsport): + r = requests.get(f"http://{statsip}:{statsport}/json/v1", timeout=600) + assert r.status_code == 200 + + views = r.json()["views"] + data = { + "in-queries-rtt": views["_default"]["resolver"]["in-queries-rtt"], + "out-queries-rtt": views["_default"]["resolver"]["out-queries-rtt"], + } + + return data + + def load_timers_json(zone, primary=True): name = zone["name"] @@ -119,3 +133,8 @@ def test_zone_with_many_keys_json(statsport): @pytest.mark.flaky(max_runs=2) def test_traffic_json(statsport): generic.test_traffic(fetch_traffic_json, statsip="10.53.0.2", statsport=statsport) + + +@pytest.mark.flaky(max_runs=2) +def test_rtt_json(statsport): + generic.test_rtt(fetch_rtt_json, statsip="10.53.0.4", statsport=statsport) diff --git a/bin/tests/system/statschannel/tests_sh_statschannel.py b/bin/tests/system/statschannel/tests_sh_statschannel.py index 7b5788010d6..473646cf1b3 100644 --- a/bin/tests/system/statschannel/tests_sh_statschannel.py +++ b/bin/tests/system/statschannel/tests_sh_statschannel.py @@ -14,6 +14,7 @@ import pytest pytestmark = pytest.mark.extra_artifacts( [ "K*", + "ans5/ans.run", "bind9.xsl.1", "bind9.xsl.2", "compressed.headers", diff --git a/bin/tests/system/statschannel/tests_xml.py b/bin/tests/system/statschannel/tests_xml.py index 0707681de7f..133c2f09633 100755 --- a/bin/tests/system/statschannel/tests_xml.py +++ b/bin/tests/system/statschannel/tests_xml.py @@ -27,6 +27,7 @@ pytestmark = [ pytest.mark.extra_artifacts( [ "ns2/K*", + "ans5/ans.run", "ns2/*.jnl", "ns2/*.signed", "ns2/dsset-*", @@ -91,6 +92,35 @@ def fetch_traffic_xml(statsip, statsport): return traffic +def fetch_rtt_xml(statsip, statsport): + def load_counters(data): + out = {} + for counter in data.findall("counter"): + out[counter.attrib["name"]] = int(counter.text) + + return out + + r = requests.get(f"http://{statsip}:{statsport}/xml/v3", timeout=600) + assert r.status_code == 200 + + root = ET.fromstring(r.text) + + default_view = None + for view in root.find("views").iter("view"): + if view.attrib["name"] == "_default": + default_view = view + break + assert default_view is not None + + rtt = {} + for counters in default_view.find("rtt").findall("counters"): + key = counters.attrib["type"] + values = load_counters(counters) + rtt[key] = values + + return rtt + + def load_timers_xml(zone, primary=True): name = zone.attrib["name"] @@ -149,3 +179,8 @@ def test_zone_with_many_keys_xml(statsport): @pytest.mark.flaky(max_runs=2) def test_traffic_xml(statsport): generic.test_traffic(fetch_traffic_xml, statsip="10.53.0.2", statsport=statsport) + + +@pytest.mark.flaky(max_runs=2) +def test_rtt_xml(statsport): + generic.test_rtt(fetch_rtt_xml, statsip="10.53.0.4", statsport=statsport)