summaryrefslogtreecommitdiffstats
path: root/bin/tests/system/statschannel
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 07:24:22 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 07:24:22 +0000
commit45d6379135504814ab723b57f0eb8be23393a51d (patch)
treed4f2ec4acca824a8446387a758b0ce4238a4dffa /bin/tests/system/statschannel
parentInitial commit. (diff)
downloadbind9-upstream.tar.xz
bind9-upstream.zip
Adding upstream version 1:9.16.44.upstream/1%9.16.44upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--bin/tests/system/statschannel/clean.sh32
-rw-r--r--bin/tests/system/statschannel/conftest.py25
-rw-r--r--bin/tests/system/statschannel/fetch.pl43
-rw-r--r--bin/tests/system/statschannel/generic.py106
-rw-r--r--bin/tests/system/statschannel/generic_dnspython.py128
-rw-r--r--bin/tests/system/statschannel/mem-xml.pl21
-rw-r--r--bin/tests/system/statschannel/ns1/example.db49
-rw-r--r--bin/tests/system/statschannel/ns1/named.conf.in43
-rw-r--r--bin/tests/system/statschannel/ns2/dnssec.db.in28
-rw-r--r--bin/tests/system/statschannel/ns2/example.db49
-rw-r--r--bin/tests/system/statschannel/ns2/manykeys.db.in28
-rw-r--r--bin/tests/system/statschannel/ns2/named.conf.in72
-rw-r--r--bin/tests/system/statschannel/ns2/named2.conf.in68
-rw-r--r--bin/tests/system/statschannel/ns2/sign.sh45
-rw-r--r--bin/tests/system/statschannel/ns3/named.conf.in43
-rw-r--r--bin/tests/system/statschannel/prereq.sh27
-rw-r--r--bin/tests/system/statschannel/server-json.pl35
-rw-r--r--bin/tests/system/statschannel/server-xml.pl25
-rw-r--r--bin/tests/system/statschannel/setup.sh21
-rw-r--r--bin/tests/system/statschannel/tests.sh392
-rwxr-xr-xbin/tests/system/statschannel/tests_json.py105
-rwxr-xr-xbin/tests/system/statschannel/tests_xml.py135
-rw-r--r--bin/tests/system/statschannel/traffic-json.pl49
-rw-r--r--bin/tests/system/statschannel/traffic-xml.pl46
-rw-r--r--bin/tests/system/statschannel/traffic.expect.12
-rw-r--r--bin/tests/system/statschannel/traffic.expect.24
-rw-r--r--bin/tests/system/statschannel/traffic.expect.45
-rw-r--r--bin/tests/system/statschannel/traffic.expect.57
-rw-r--r--bin/tests/system/statschannel/traffic.expect.68
-rw-r--r--bin/tests/system/statschannel/zones-json.pl37
-rw-r--r--bin/tests/system/statschannel/zones-xml.pl40
31 files changed, 1718 insertions, 0 deletions
diff --git a/bin/tests/system/statschannel/clean.sh b/bin/tests/system/statschannel/clean.sh
new file mode 100644
index 0000000..5ad2a2c
--- /dev/null
+++ b/bin/tests/system/statschannel/clean.sh
@@ -0,0 +1,32 @@
+#!/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 compressed.headers regular.headers compressed.out regular.out
+rm -f dig.out*
+rm -f ns*/managed-keys.bind*
+rm -f ns*/named.conf
+rm -f ns*/named.lock
+rm -f ns*/named.memstats
+rm -f ns*/named.run*
+rm -f ns*/named.stats
+rm -f ns*/signzone.out.*
+rm -f ns2/*.db.signed* ns2/dsset-*. ns2/*.jbk
+rm -f ns2/Kdnssec* ns2/dnssec.*.id
+rm -f ns2/Kmanykeys* ns2/manykeys.*.id
+rm -f ns2/dnssec.db.signed* ns2/dsset-dnssec.
+rm -f ns3/*.db
+rm -f traffic traffic.out.* traffic.json.* traffic.xml.*
+rm -f xml.*mem json.*mem
+rm -f xml.*stats json.*stats
+rm -f zones zones.out.* zones.json.* zones.xml.* zones.expect.*
+rm -rf ./__pycache__
diff --git a/bin/tests/system/statschannel/conftest.py b/bin/tests/system/statschannel/conftest.py
new file mode 100644
index 0000000..363dd7a
--- /dev/null
+++ b/bin/tests/system/statschannel/conftest.py
@@ -0,0 +1,25 @@
+# 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.
+
+import os
+import pytest
+
+
+@pytest.fixture
+def statsport(request):
+ # pylint: disable=unused-argument
+ env_port = os.getenv("EXTRAPORT1")
+ if env_port is None:
+ env_port = 5301
+ else:
+ env_port = int(env_port)
+
+ return env_port
diff --git a/bin/tests/system/statschannel/fetch.pl b/bin/tests/system/statschannel/fetch.pl
new file mode 100644
index 0000000..b09ed54
--- /dev/null
+++ b/bin/tests/system/statschannel/fetch.pl
@@ -0,0 +1,43 @@
+#!/usr/bin/perl
+
+# 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.
+
+# fetch.pl:
+# Simple script to fetch HTTP content from the statistics channel
+# of a BIND server. Fetches the full XML stats from 10.53.0.2 port
+# 8853 by default; these can be overridden by command line arguments.
+
+use File::Fetch;
+use Getopt::Std;
+
+sub usage {
+ print ("Usage: fetch.pl [-s address] [-p port] [path]\n");
+ exit 1;
+}
+
+my %options={};
+getopts("s:p:", \%options);
+
+my $addr = "10.53.0.2";
+$addr = $options{s} if defined $options{s};
+
+my $path = 'xml/v3';
+if (@ARGV >= 1) {
+ $path = shift @ARGV;
+}
+
+my $port = 8853;
+$port = $options{p} if defined $options{p};
+
+my $ff = File::Fetch->new(uri => "http://$addr:$port/$path");
+my $file = $ff->fetch() or die $ff->error;
+print ("$file\n");
diff --git a/bin/tests/system/statschannel/generic.py b/bin/tests/system/statschannel/generic.py
new file mode 100644
index 0000000..5ff09e2
--- /dev/null
+++ b/bin/tests/system/statschannel/generic.py
@@ -0,0 +1,106 @@
+# 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 datetime import datetime, timedelta
+import os
+
+
+# ISO datetime format without msec
+fmt = "%Y-%m-%dT%H:%M:%SZ"
+
+# The constants were taken from BIND 9 source code (lib/dns/zone.c)
+max_refresh = timedelta(seconds=2419200) # 4 weeks
+max_expires = timedelta(seconds=14515200) # 24 weeks
+now = datetime.utcnow().replace(microsecond=0)
+dayzero = datetime.utcfromtimestamp(0).replace(microsecond=0)
+
+
+# Generic helper functions
+def check_expires(expires, min_time, max_time):
+ assert expires >= min_time
+ assert expires <= max_time
+
+
+def check_refresh(refresh, min_time, max_time):
+ assert refresh >= min_time
+ assert refresh <= max_time
+
+
+def check_loaded(loaded, expected):
+ # Sanity check the zone timers values
+ assert loaded == expected
+ assert loaded < now
+
+
+def check_zone_timers(loaded, expires, refresh, loaded_exp):
+ # Sanity checks the zone timers values
+ if expires is not None:
+ check_expires(expires, now, now + max_expires)
+ if refresh is not None:
+ check_refresh(refresh, now, now + max_refresh)
+ check_loaded(loaded, loaded_exp)
+
+
+#
+# The output is gibberish, but at least make sure it does not crash.
+#
+def check_manykeys(name, zone=None):
+ # pylint: disable=unused-argument
+ assert name == "manykeys"
+
+
+def zone_mtime(zonedir, name):
+ try:
+ si = os.stat(os.path.join(zonedir, "{}.db".format(name)))
+ except FileNotFoundError:
+ return dayzero
+
+ mtime = datetime.utcfromtimestamp(si.st_mtime).replace(microsecond=0)
+
+ return mtime
+
+
+def test_zone_timers_primary(fetch_zones, load_timers, **kwargs):
+ statsip = kwargs["statsip"]
+ statsport = kwargs["statsport"]
+ zonedir = kwargs["zonedir"]
+
+ zones = fetch_zones(statsip, statsport)
+
+ for zone in zones:
+ (name, loaded, expires, refresh) = load_timers(zone, True)
+ mtime = zone_mtime(zonedir, name)
+ check_zone_timers(loaded, expires, refresh, mtime)
+
+
+def test_zone_timers_secondary(fetch_zones, load_timers, **kwargs):
+ statsip = kwargs["statsip"]
+ statsport = kwargs["statsport"]
+ zonedir = kwargs["zonedir"]
+
+ zones = fetch_zones(statsip, statsport)
+
+ for zone in zones:
+ (name, loaded, expires, refresh) = load_timers(zone, False)
+ mtime = zone_mtime(zonedir, name)
+ check_zone_timers(loaded, expires, refresh, mtime)
+
+
+def test_zone_with_many_keys(fetch_zones, load_zone, **kwargs):
+ statsip = kwargs["statsip"]
+ statsport = kwargs["statsport"]
+
+ zones = fetch_zones(statsip, statsport)
+
+ for zone in zones:
+ name = load_zone(zone)
+ if name == "manykeys":
+ check_manykeys(name)
diff --git a/bin/tests/system/statschannel/generic_dnspython.py b/bin/tests/system/statschannel/generic_dnspython.py
new file mode 100644
index 0000000..34a0398
--- /dev/null
+++ b/bin/tests/system/statschannel/generic_dnspython.py
@@ -0,0 +1,128 @@
+# 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 import defaultdict
+
+import dns.message
+import dns.query
+import dns.rcode
+
+
+TIMEOUT = 10
+
+
+def create_msg(qname, qtype):
+ msg = dns.message.make_query(
+ qname, qtype, want_dnssec=True, use_edns=0, payload=4096
+ )
+
+ return msg
+
+
+def udp_query(ip, port, msg):
+ ans = dns.query.udp(msg, ip, TIMEOUT, port=port)
+ assert ans.rcode() == dns.rcode.NOERROR
+
+ return ans
+
+
+def tcp_query(ip, port, msg):
+ ans = dns.query.tcp(msg, ip, TIMEOUT, port=port)
+ assert ans.rcode() == dns.rcode.NOERROR
+
+ return ans
+
+
+def create_expected(data):
+ expected = {
+ "dns-tcp-requests-sizes-received-ipv4": defaultdict(int),
+ "dns-tcp-responses-sizes-sent-ipv4": defaultdict(int),
+ "dns-tcp-requests-sizes-received-ipv6": defaultdict(int),
+ "dns-tcp-responses-sizes-sent-ipv6": defaultdict(int),
+ "dns-udp-requests-sizes-received-ipv4": defaultdict(int),
+ "dns-udp-requests-sizes-received-ipv6": defaultdict(int),
+ "dns-udp-responses-sizes-sent-ipv4": defaultdict(int),
+ "dns-udp-responses-sizes-sent-ipv6": defaultdict(int),
+ }
+
+ for k, v in data.items():
+ for kk, vv in v.items():
+ expected[k][kk] += vv
+
+ return expected
+
+
+def update_expected(expected, key, msg):
+ msg_len = len(msg.to_wire())
+ bucket_num = (msg_len // 16) * 16
+ bucket = "{}-{}".format(bucket_num, bucket_num + 15)
+
+ expected[key][bucket] += 1
+
+
+def check_traffic(data, expected):
+ def ordered(obj):
+ if isinstance(obj, dict):
+ return sorted((k, ordered(v)) for k, v in obj.items())
+ if isinstance(obj, list):
+ return sorted(ordered(x) for x in obj)
+ return obj
+
+ ordered_data = ordered(data)
+ ordered_expected = ordered(expected)
+
+ assert len(ordered_data) == 8
+ assert len(ordered_expected) == 8
+ assert len(data) == len(ordered_data)
+ assert len(expected) == len(ordered_expected)
+
+ assert ordered_data == ordered_expected
+
+
+def test_traffic(fetch_traffic, **kwargs):
+ statsip = kwargs["statsip"]
+ statsport = kwargs["statsport"]
+ port = kwargs["port"]
+
+ data = fetch_traffic(statsip, statsport)
+ exp = create_expected(data)
+
+ msg = create_msg("short.example.", "TXT")
+ update_expected(exp, "dns-udp-requests-sizes-received-ipv4", msg)
+ ans = udp_query(statsip, port, msg)
+ update_expected(exp, "dns-udp-responses-sizes-sent-ipv4", ans)
+ data = fetch_traffic(statsip, statsport)
+
+ check_traffic(data, exp)
+
+ msg = create_msg("long.example.", "TXT")
+ update_expected(exp, "dns-udp-requests-sizes-received-ipv4", msg)
+ ans = udp_query(statsip, port, msg)
+ update_expected(exp, "dns-udp-responses-sizes-sent-ipv4", ans)
+ data = fetch_traffic(statsip, statsport)
+
+ check_traffic(data, exp)
+
+ msg = create_msg("short.example.", "TXT")
+ update_expected(exp, "dns-tcp-requests-sizes-received-ipv4", msg)
+ ans = tcp_query(statsip, port, msg)
+ update_expected(exp, "dns-tcp-responses-sizes-sent-ipv4", ans)
+ data = fetch_traffic(statsip, statsport)
+
+ check_traffic(data, exp)
+
+ msg = create_msg("long.example.", "TXT")
+ update_expected(exp, "dns-tcp-requests-sizes-received-ipv4", msg)
+ ans = tcp_query(statsip, port, msg)
+ update_expected(exp, "dns-tcp-responses-sizes-sent-ipv4", ans)
+ data = fetch_traffic(statsip, statsport)
+
+ check_traffic(data, exp)
diff --git a/bin/tests/system/statschannel/mem-xml.pl b/bin/tests/system/statschannel/mem-xml.pl
new file mode 100644
index 0000000..4483aae
--- /dev/null
+++ b/bin/tests/system/statschannel/mem-xml.pl
@@ -0,0 +1,21 @@
+#!/usr/bin/perl
+
+# 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.
+
+# server-xml.pl:
+# Parses the XML version of the server stats into a normalized format.
+
+use XML::Simple;
+use Data::Dumper;
+
+my $ref = XMLin("xml.mem");
+print Dumper($ref);
diff --git a/bin/tests/system/statschannel/ns1/example.db b/bin/tests/system/statschannel/ns1/example.db
new file mode 100644
index 0000000..5c2635e
--- /dev/null
+++ b/bin/tests/system/statschannel/ns1/example.db
@@ -0,0 +1,49 @@
+; 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
+example IN SOA mname1. . (
+ 1 ; serial
+ 20 ; refresh (20 seconds)
+ 20 ; retry (20 seconds)
+ 1814400 ; expire (3 weeks)
+ 3600 ; minimum (1 hour)
+ )
+example. NS ns2.example.
+ns2.example. A 10.53.0.2
+
+$ORIGIN example.
+a A 10.0.0.1
+ MX 10 mail.example.
+short TXT "short text"
+long TXT (
+ "longlonglonglonglonglonglonglonglonglong"
+ "longlonglonglonglonglonglonglonglonglong"
+ "longlonglonglonglonglonglonglonglonglong"
+ "longlonglonglonglonglonglonglonglonglong"
+ "longlonglonglonglonglonglonglonglonglong"
+ "longlonglonglonglonglonglonglonglonglong"
+ "longlonglonglonglonglonglonglonglonglong"
+ "longlonglonglonglonglonglonglonglonglong"
+ "longlonglonglonglonglonglonglonglonglong"
+ "longlonglonglonglonglonglonglonglonglong"
+ "longlonglonglonglonglonglonglonglonglong"
+ "longlonglonglonglonglonglonglonglonglong"
+ "longlonglonglonglonglonglonglonglonglong"
+ "longlonglonglonglonglonglonglonglonglong"
+ "longlonglonglonglonglonglonglonglonglong"
+ "longlonglonglonglonglonglonglonglonglong"
+ "longlonglonglonglonglonglonglonglonglong"
+ "longlonglonglonglonglonglonglonglonglong"
+ )
+
+mail A 10.0.0.2
diff --git a/bin/tests/system/statschannel/ns1/named.conf.in b/bin/tests/system/statschannel/ns1/named.conf.in
new file mode 100644
index 0000000..04ead33
--- /dev/null
+++ b/bin/tests/system/statschannel/ns1/named.conf.in
@@ -0,0 +1,43 @@
+/*
+ * 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.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 no;
+ notify explicit;
+ minimal-responses no;
+ version none; // make statistics independent of the version number
+};
+
+statistics-channels { inet 10.53.0.1 port @EXTRAPORT1@ allow { localhost; }; };
+
+key rndc_key {
+ secret "1234abcd8765";
+ algorithm hmac-sha256;
+};
+
+controls {
+ inet 10.53.0.1 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
+};
+
+zone "example" {
+ type primary;
+ file "example.db";
+ allow-transfer { any; };
+};
diff --git a/bin/tests/system/statschannel/ns2/dnssec.db.in b/bin/tests/system/statschannel/ns2/dnssec.db.in
new file mode 100644
index 0000000..90ae166
--- /dev/null
+++ b/bin/tests/system/statschannel/ns2/dnssec.db.in
@@ -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
+
+dnssec. IN SOA mname1. . (
+ 1 ; serial
+ 20 ; refresh (20 seconds)
+ 20 ; retry (20 seconds)
+ 1814400 ; expire (3 weeks)
+ 3600 ; minimum (1 hour)
+ )
+dnssec. NS ns2.dnssec.
+ns2.dnssec. A 10.53.0.2
+
+$ORIGIN dnssec.
+a A 10.0.0.1
+ MX 10 mail.dnssec.
+mail A 10.0.0.2
diff --git a/bin/tests/system/statschannel/ns2/example.db b/bin/tests/system/statschannel/ns2/example.db
new file mode 100644
index 0000000..5c2635e
--- /dev/null
+++ b/bin/tests/system/statschannel/ns2/example.db
@@ -0,0 +1,49 @@
+; 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
+example IN SOA mname1. . (
+ 1 ; serial
+ 20 ; refresh (20 seconds)
+ 20 ; retry (20 seconds)
+ 1814400 ; expire (3 weeks)
+ 3600 ; minimum (1 hour)
+ )
+example. NS ns2.example.
+ns2.example. A 10.53.0.2
+
+$ORIGIN example.
+a A 10.0.0.1
+ MX 10 mail.example.
+short TXT "short text"
+long TXT (
+ "longlonglonglonglonglonglonglonglonglong"
+ "longlonglonglonglonglonglonglonglonglong"
+ "longlonglonglonglonglonglonglonglonglong"
+ "longlonglonglonglonglonglonglonglonglong"
+ "longlonglonglonglonglonglonglonglonglong"
+ "longlonglonglonglonglonglonglonglonglong"
+ "longlonglonglonglonglonglonglonglonglong"
+ "longlonglonglonglonglonglonglonglonglong"
+ "longlonglonglonglonglonglonglonglonglong"
+ "longlonglonglonglonglonglonglonglonglong"
+ "longlonglonglonglonglonglonglonglonglong"
+ "longlonglonglonglonglonglonglonglonglong"
+ "longlonglonglonglonglonglonglonglonglong"
+ "longlonglonglonglonglonglonglonglonglong"
+ "longlonglonglonglonglonglonglonglonglong"
+ "longlonglonglonglonglonglonglonglonglong"
+ "longlonglonglonglonglonglonglonglonglong"
+ "longlonglonglonglonglonglonglonglonglong"
+ )
+
+mail A 10.0.0.2
diff --git a/bin/tests/system/statschannel/ns2/manykeys.db.in b/bin/tests/system/statschannel/ns2/manykeys.db.in
new file mode 100644
index 0000000..3281a39
--- /dev/null
+++ b/bin/tests/system/statschannel/ns2/manykeys.db.in
@@ -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
+
+manykeys. IN SOA mname1. . (
+ 1 ; serial
+ 20 ; refresh (20 seconds)
+ 20 ; retry (20 seconds)
+ 1814400 ; expire (3 weeks)
+ 3600 ; minimum (1 hour)
+ )
+manykeys. NS ns2.manykeys.
+ns2.manykeys. A 10.53.0.2
+
+$ORIGIN manykeys.
+a A 10.0.0.1
+ MX 10 mail.manykeys.
+mail A 10.0.0.2
diff --git a/bin/tests/system/statschannel/ns2/named.conf.in b/bin/tests/system/statschannel/ns2/named.conf.in
new file mode 100644
index 0000000..fd25fff
--- /dev/null
+++ b/bin/tests/system/statschannel/ns2/named.conf.in
@@ -0,0 +1,72 @@
+/*
+ * 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.2;
+ notify-source 10.53.0.2;
+ transfer-source 10.53.0.2;
+ port @PORT@;
+ pid-file "named.pid";
+ listen-on { 10.53.0.2; };
+ listen-on-v6 { none; };
+ recursion no;
+ notify no;
+ minimal-responses no;
+ version none; // make statistics independent of the version number
+};
+
+statistics-channels { inet 10.53.0.2 port @EXTRAPORT1@ allow { localhost; }; };
+
+key rndc_key {
+ secret "1234abcd8765";
+ algorithm hmac-sha256;
+};
+
+controls {
+ inet 10.53.0.2 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
+};
+
+dnssec-policy "manykeys" {
+ keys {
+ ksk lifetime unlimited algorithm 8;
+ zsk lifetime unlimited algorithm 8;
+ ksk lifetime unlimited algorithm 13;
+ zsk lifetime unlimited algorithm 13;
+ ksk lifetime unlimited algorithm 14;
+ zsk lifetime unlimited algorithm 14;
+ };
+};
+
+zone "example" {
+ type primary;
+ file "example.db";
+ allow-transfer { any; };
+};
+
+zone "dnssec" {
+ type primary;
+ file "dnssec.db.signed";
+ auto-dnssec maintain;
+ allow-update { any; };
+ zone-statistics full;
+ dnssec-dnskey-kskonly yes;
+ update-check-ksk yes;
+};
+
+zone "manykeys" {
+ type primary;
+ file "manykeys.db.signed";
+ allow-update { any; };
+ zone-statistics full;
+ dnssec-policy "manykeys";
+};
diff --git a/bin/tests/system/statschannel/ns2/named2.conf.in b/bin/tests/system/statschannel/ns2/named2.conf.in
new file mode 100644
index 0000000..d45f9f5
--- /dev/null
+++ b/bin/tests/system/statschannel/ns2/named2.conf.in
@@ -0,0 +1,68 @@
+/*
+ * 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.2;
+ notify-source 10.53.0.2;
+ transfer-source 10.53.0.2;
+ port @PORT@;
+ pid-file "named.pid";
+ listen-on { 10.53.0.2; };
+ listen-on-v6 { none; };
+ recursion no;
+ notify no;
+ minimal-responses no;
+ version none; // make statistics independent of the version number
+};
+
+statistics-channels { inet 10.53.0.2 port @EXTRAPORT1@ allow { localhost; }; };
+
+key rndc_key {
+ secret "1234abcd8765";
+ algorithm hmac-sha256;
+};
+
+controls {
+ inet 10.53.0.2 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
+};
+
+dnssec-policy "manykeys" {
+ keys {
+ ksk lifetime unlimited algorithm 8;
+ zsk lifetime unlimited algorithm 8;
+ };
+};
+
+zone "example" {
+ type primary;
+ file "example.db";
+ allow-transfer { any; };
+};
+
+zone "dnssec" {
+ type primary;
+ file "dnssec.db.signed";
+ auto-dnssec maintain;
+ allow-update { any; };
+ zone-statistics full;
+ dnssec-dnskey-kskonly yes;
+ update-check-ksk yes;
+};
+
+zone "manykeys" {
+ type primary;
+ file "manykeys.db.signed";
+ allow-update { any; };
+ zone-statistics full;
+ dnssec-policy "manykeys";
+};
diff --git a/bin/tests/system/statschannel/ns2/sign.sh b/bin/tests/system/statschannel/ns2/sign.sh
new file mode 100644
index 0000000..ab23550
--- /dev/null
+++ b/bin/tests/system/statschannel/ns2/sign.sh
@@ -0,0 +1,45 @@
+#!/bin/sh -e
+
+# 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.
+
+# shellcheck source=conf.sh
+. "$SYSTEMTESTTOP/conf.sh"
+
+set -e
+
+zone=dnssec.
+infile=dnssec.db.in
+zonefile=dnssec.db.signed
+ksk=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" -f KSK "$zone")
+zsk=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" "$zone")
+# Sign deliberately with a very short expiration date.
+"$SIGNER" -P -S -x -O full -e "now"+1s -o "$zone" -f "$zonefile" "$infile" > "signzone.out.$zone" 2>&1
+keyfile_to_key_id "$ksk" > dnssec.ksk.id
+keyfile_to_key_id "$zsk" > dnssec.zsk.id
+
+zone=manykeys.
+infile=manykeys.db.in
+zonefile=manykeys.db.signed
+ksk8=$("$KEYGEN" -q -a RSASHA256 -b 2048 -f KSK "$zone")
+zsk8=$("$KEYGEN" -q -a RSASHA256 -b 2048 "$zone")
+ksk13=$("$KEYGEN" -q -a ECDSAP256SHA256 -b 256 -f KSK "$zone")
+zsk13=$("$KEYGEN" -q -a ECDSAP256SHA256 -b 256 "$zone")
+ksk14=$("$KEYGEN" -q -a ECDSAP384SHA384 -b 384 -f KSK "$zone")
+zsk14=$("$KEYGEN" -q -a ECDSAP384SHA384 -b 384 "$zone")
+# Sign deliberately with a very short expiration date.
+"$SIGNER" -S -x -O full -e "now"+1s -o "$zone" -f "$zonefile" "$infile" > "signzone.out.$zone" 2>&1
+keyfile_to_key_id "$ksk8" > manykeys.ksk8.id
+keyfile_to_key_id "$zsk8" > manykeys.zsk8.id
+keyfile_to_key_id "$ksk13" > manykeys.ksk13.id
+keyfile_to_key_id "$zsk13" > manykeys.zsk13.id
+keyfile_to_key_id "$ksk14" > manykeys.ksk14.id
+keyfile_to_key_id "$zsk14" > manykeys.zsk14.id
diff --git a/bin/tests/system/statschannel/ns3/named.conf.in b/bin/tests/system/statschannel/ns3/named.conf.in
new file mode 100644
index 0000000..5f08c3f
--- /dev/null
+++ b/bin/tests/system/statschannel/ns3/named.conf.in
@@ -0,0 +1,43 @@
+/*
+ * 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.3;
+ notify-source 10.53.0.3;
+ transfer-source 10.53.0.3;
+ port @PORT@;
+ pid-file "named.pid";
+ listen-on { 10.53.0.3; };
+ listen-on-v6 { none; };
+ recursion no;
+ notify no;
+ minimal-responses no;
+ version none; // make statistics independent of the version number
+};
+
+statistics-channels { inet 10.53.0.3 port @EXTRAPORT1@ allow { localhost; }; };
+
+key rndc_key {
+ secret "1234abcd8765";
+ algorithm hmac-sha256;
+};
+
+controls {
+ inet 10.53.0.3 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
+};
+
+zone "example" {
+ type secondary;
+ file "example.db";
+ primaries { 10.53.0.1; };
+};
diff --git a/bin/tests/system/statschannel/prereq.sh b/bin/tests/system/statschannel/prereq.sh
new file mode 100644
index 0000000..4f8a444
--- /dev/null
+++ b/bin/tests/system/statschannel/prereq.sh
@@ -0,0 +1,27 @@
+#!/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.
+
+SYSTEMTESTTOP=..
+. $SYSTEMTESTTOP/conf.sh
+
+fail=0
+
+if $PERL -e 'use File::Fetch;' 2>/dev/null
+then
+ :
+else
+ echo_i "This test requires the File::Fetch library." >&2
+ fail=1
+fi
+
+exit $fail
diff --git a/bin/tests/system/statschannel/server-json.pl b/bin/tests/system/statschannel/server-json.pl
new file mode 100644
index 0000000..3715318
--- /dev/null
+++ b/bin/tests/system/statschannel/server-json.pl
@@ -0,0 +1,35 @@
+#!/usr/bin/perl
+
+# 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.
+
+# server-json.pl:
+# Parses the JSON version of the server stats into a normalized format.
+
+use JSON;
+
+open(INPUT, "<json.stats");
+my $text = do{local$/;<INPUT>};
+close(INPUT);
+
+my $ref = decode_json($text);
+foreach $key (keys %{$ref->{opcodes}}) {
+ print "opcode " . $key . ": " . $ref->{opcodes}->{$key} . "\n";
+}
+foreach $key (keys %{$ref->{rcodes}}) {
+ print "rcode " . $key . ": " . $ref->{rcodes}->{$key} . "\n";
+}
+foreach $key (keys %{$ref->{qtypes}}) {
+ print "qtype " . $key . ": " . $ref->{qtypes}->{$key} . "\n";
+}
+foreach $key (keys %{$ref->{nsstats}}) {
+ print "nsstat " . $key . ": " . $ref->{nsstats}->{$key} . "\n";
+}
diff --git a/bin/tests/system/statschannel/server-xml.pl b/bin/tests/system/statschannel/server-xml.pl
new file mode 100644
index 0000000..5f76360
--- /dev/null
+++ b/bin/tests/system/statschannel/server-xml.pl
@@ -0,0 +1,25 @@
+#!/usr/bin/perl
+
+# 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.
+
+# server-xml.pl:
+# Parses the XML version of the server stats into a normalized format.
+
+use XML::Simple;
+
+my $ref = XMLin("xml.stats");
+my $counters = $ref->{server}->{counters};
+foreach $group (@$counters) {
+ foreach $key (keys %{$group->{counter}}) {
+ print $group->{type} . " " . $key . ": ". $group->{counter}->{$key}->{content} . "\n";
+ }
+}
diff --git a/bin/tests/system/statschannel/setup.sh b/bin/tests/system/statschannel/setup.sh
new file mode 100644
index 0000000..4ebc39b
--- /dev/null
+++ b/bin/tests/system/statschannel/setup.sh
@@ -0,0 +1,21 @@
+#!/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.
+
+# shellcheck source=conf.sh
+. "$SYSTEMTESTTOP/conf.sh"
+
+for conf in ns*/named.conf.in; do
+ copy_setports "$conf" "$(dirname "$conf")/$(basename "$conf" .in)"
+done
+
+(cd ns2 && $SHELL sign.sh)
diff --git a/bin/tests/system/statschannel/tests.sh b/bin/tests/system/statschannel/tests.sh
new file mode 100644
index 0000000..0480b01
--- /dev/null
+++ b/bin/tests/system/statschannel/tests.sh
@@ -0,0 +1,392 @@
+#!/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.
+
+SYSTEMTESTTOP=..
+# shellcheck source=conf.sh
+. "$SYSTEMTESTTOP/conf.sh"
+
+DIGCMD="$DIG @10.53.0.2 -p ${PORT}"
+RNDCCMD="$RNDC -c $SYSTEMTESTTOP/common/rndc.conf -p ${CONTROLPORT} -s"
+
+if ! $FEATURETEST --have-json-c
+then
+ unset PERL_JSON
+ echo_i "JSON was not configured; skipping" >&2
+elif $PERL -e 'use JSON;' 2>/dev/null
+then
+ PERL_JSON=1
+else
+ unset PERL_JSON
+ echo_i "JSON tests require JSON library; skipping" >&2
+fi
+
+if ! $FEATURETEST --have-libxml2
+then
+ unset PERL_XML
+ echo_i "XML was not configured; skipping" >&2
+elif $PERL -e 'use XML::Simple;' 2>/dev/null
+then
+ PERL_XML=1
+else
+ unset PERL_XML
+ echo_i "XML tests require XML::Simple; skipping" >&2
+fi
+
+if [ ! "$PERL_JSON" -a ! "$PERL_XML" ]; then
+ echo_i "skipping all tests"
+ exit 0
+fi
+
+
+getzones() {
+ sleep 1
+ echo_i "... using $1"
+ case $1 in
+ xml) path='xml/v3/zones' ;;
+ json) path='json/v1/zones' ;;
+ *) return 1 ;;
+ esac
+ file=`$PERL fetch.pl -p ${EXTRAPORT1} $path`
+ cp $file $file.$1.$3
+ $PERL zones-${1}.pl $file $2 2>/dev/null | sort > zones.out.$3
+ result=$?
+ return $result
+}
+
+# TODO: Move loadkeys_on to conf.sh.common
+loadkeys_on() {
+ nsidx=$1
+ zone=$2
+ nextpart ns${nsidx}/named.run > /dev/null
+ $RNDCCMD 10.53.0.${nsidx} loadkeys ${zone} | sed "s/^/ns${nsidx} /" | cat_i
+ wait_for_log 20 "next key event" ns${nsidx}/named.run
+}
+
+status=0
+n=1
+ret=0
+echo_i "checking consistency between named.stats and xml/json ($n)"
+rm -f ns2/named.stats
+$DIGCMD +tcp example ns > dig.out.$n || ret=1
+$RNDCCMD 10.53.0.2 stats 2>&1 | sed 's/^/I:ns1 /'
+query_count=`awk '/QUERY/ {print $1}' ns2/named.stats`
+txt_count=`awk '/TXT/ {print $1}' ns2/named.stats`
+noerror_count=`awk '/NOERROR/ {print $1}' ns2/named.stats`
+if [ $PERL_XML ]; then
+ file=`$PERL fetch.pl -p ${EXTRAPORT1} xml/v3/server`
+ mv $file xml.stats
+ $PERL server-xml.pl > xml.fmtstats 2> /dev/null
+ xml_query_count=`awk '/opcode QUERY/ { print $NF }' xml.fmtstats`
+ xml_query_count=${xml_query_count:-0}
+ [ "$query_count" -eq "$xml_query_count" ] || ret=1
+ xml_txt_count=`awk '/qtype TXT/ { print $NF }' xml.fmtstats`
+ xml_txt_count=${xml_txt_count:-0}
+ [ "$txt_count" -eq "$xml_txt_count" ] || ret=1
+ xml_noerror_count=`awk '/rcode NOERROR/ { print $NF }' xml.fmtstats`
+ xml_noerror_count=${xml_noerror_count:-0}
+ [ "$noerror_count" -eq "$xml_noerror_count" ] || ret=1
+fi
+if [ $PERL_JSON ]; then
+ file=`$PERL fetch.pl -p ${EXTRAPORT1} json/v1/server`
+ mv $file json.stats
+ $PERL server-json.pl > json.fmtstats 2> /dev/null
+ json_query_count=`awk '/opcode QUERY/ { print $NF }' json.fmtstats`
+ json_query_count=${json_query_count:-0}
+ [ "$query_count" -eq "$json_query_count" ] || ret=1
+ json_txt_count=`awk '/qtype TXT/ { print $NF }' json.fmtstats`
+ json_txt_count=${json_txt_count:-0}
+ [ "$txt_count" -eq "$json_txt_count" ] || ret=1
+ json_noerror_count=`awk '/rcode NOERROR/ { print $NF }' json.fmtstats`
+ json_noerror_count=${json_noerror_count:-0}
+ [ "$noerror_count" -eq "$json_noerror_count" ] || ret=1
+fi
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=`expr $status + $ret`
+n=`expr $n + 1`
+
+ret=0
+echo_i "checking malloced memory statistics xml/json ($n)"
+if [ $PERL_XML ]; then
+ file=`$PERL fetch.pl -p ${EXTRAPORT1} xml/v3/mem`
+ mv $file xml.mem
+ $PERL mem-xml.pl $file > xml.fmtmem
+ grep "'Malloced' => '[0-9][0-9]*'" xml.fmtmem > /dev/null || ret=1
+ grep "'malloced' => '[0-9][0-9]*'" xml.fmtmem > /dev/null || ret=1
+ grep "'maxmalloced' => '[0-9][0-9]*'" xml.fmtmem > /dev/null || ret=1
+fi
+if [ $PERL_JSON ]; then
+ file=`$PERL fetch.pl -p ${EXTRAPORT1} json/v1/mem`
+ mv $file json.mem
+ grep '"malloced":[0-9][0-9]*,' json.mem > /dev/null || ret=1
+ grep '"maxmalloced":[0-9][0-9]*,' json.mem > /dev/null || ret=1
+ grep '"Malloced":[0-9][0-9]*,' json.mem > /dev/null || ret=1
+fi
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=`expr $status + $ret`
+n=`expr $n + 1`
+
+echo_i "checking consistency between regular and compressed output ($n)"
+for i in 1 2 3 4 5; do
+ ret=0
+ if $FEATURETEST --have-libxml2;
+ then
+ URL=http://10.53.0.2:${EXTRAPORT1}/xml/v3/server
+ filter_str='s#<current-time>.*</current-time>##g'
+ else
+ URL=http://10.53.0.2:${EXTRAPORT1}/json/v1/server
+ filter_str='s#"current-time.*",##g'
+ fi
+ $CURL -D regular.headers $URL 2>/dev/null | \
+ sed -e "$filter_str" > regular.out
+ $CURL -D compressed.headers --compressed $URL 2>/dev/null | \
+ sed -e "$filter_str" > compressed.out
+ diff regular.out compressed.out >/dev/null || ret=1
+ if [ $ret != 0 ]; then
+ echo_i "failed on try $i, probably a timing issue, trying again"
+ sleep 1
+ else
+ break
+ fi
+done
+
+status=`expr $status + $ret`
+n=`expr $n + 1`
+
+ret=0
+echo_i "checking if compressed output is really compressed ($n)"
+if $FEATURETEST --with-zlib;
+then
+ REGSIZE=`cat regular.headers | \
+ grep -i Content-Length | sed -e "s/.*: \([0-9]*\).*/\1/"`
+ COMPSIZE=`cat compressed.headers | \
+ grep -i Content-Length | sed -e "s/.*: \([0-9]*\).*/\1/"`
+ if [ ! `expr $REGSIZE / $COMPSIZE` -gt 2 ]; then
+ ret=1
+ fi
+else
+ echo_i "skipped"
+fi
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=`expr $status + $ret`
+n=`expr $n + 1`
+
+# Test dnssec sign statistics.
+zone="dnssec"
+sign_prefix="dnssec-sign operations"
+refresh_prefix="dnssec-refresh operations"
+ksk_id=`cat ns2/$zone.ksk.id`
+zsk_id=`cat ns2/$zone.zsk.id`
+
+# Test sign operations for scheduled resigning.
+ret=0
+# The dnssec zone has 10 RRsets to sign (including NSEC) with the ZSK and one
+# RRset (DNSKEY) with the KSK. So starting named with signatures that expire
+# almost right away, this should trigger 10 zsk and 1 ksk sign operations.
+echo "${refresh_prefix} ${zsk_id}: 10" > zones.expect
+echo "${refresh_prefix} ${ksk_id}: 1" >> zones.expect
+echo "${sign_prefix} ${zsk_id}: 10" >> zones.expect
+echo "${sign_prefix} ${ksk_id}: 1" >> zones.expect
+cat zones.expect | sort > zones.expect.$n
+rm -f zones.expect
+# Fetch and check the dnssec sign statistics.
+echo_i "fetching zone '$zone' stats data after zone maintenance at startup ($n)"
+if [ $PERL_XML ]; then
+ getzones xml $zone x$n || ret=1
+ cmp zones.out.x$n zones.expect.$n || ret=1
+fi
+if [ $PERL_JSON ]; then
+ getzones json 0 j$n || ret=1
+ cmp zones.out.j$n zones.expect.$n || ret=1
+fi
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=`expr $status + $ret`
+n=`expr $n + 1`
+
+# Test sign operations after dynamic update.
+ret=0
+(
+# Update dnssec zone to trigger signature creation.
+echo zone $zone
+echo server 10.53.0.2 "$PORT"
+echo update add $zone. 300 in txt "nsupdate added me"
+echo send
+) | $NSUPDATE
+# This should trigger the resign of SOA, TXT and NSEC (+3 zsk).
+echo "${refresh_prefix} ${zsk_id}: 10" > zones.expect
+echo "${refresh_prefix} ${ksk_id}: 1" >> zones.expect
+echo "${sign_prefix} ${zsk_id}: 13" >> zones.expect
+echo "${sign_prefix} ${ksk_id}: 1" >> zones.expect
+cat zones.expect | sort > zones.expect.$n
+rm -f zones.expect
+# Fetch and check the dnssec sign statistics.
+echo_i "fetching zone '$zone' stats data after dynamic update ($n)"
+if [ $PERL_XML ]; then
+ getzones xml $zone x$n || ret=1
+ cmp zones.out.x$n zones.expect.$n || ret=1
+fi
+if [ $PERL_JSON ]; then
+ getzones json 0 j$n || ret=1
+ cmp zones.out.j$n zones.expect.$n || ret=1
+fi
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=`expr $status + $ret`
+n=`expr $n + 1`
+
+# Test sign operations of KSK.
+ret=0
+echo_i "fetch zone '$zone' stats data after updating DNSKEY RRset ($n)"
+# Add a standby DNSKEY, this triggers resigning the DNSKEY RRset.
+zsk=$("$KEYGEN" -K ns2 -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" "$zone")
+$SETTIME -K ns2 -P now -A never $zsk.key > /dev/null
+loadkeys_on 2 $zone || ret=1
+# This should trigger the resign of SOA (+1 zsk) and DNSKEY (+1 ksk).
+echo "${refresh_prefix} ${zsk_id}: 11" > zones.expect
+echo "${refresh_prefix} ${ksk_id}: 2" >> zones.expect
+echo "${sign_prefix} ${zsk_id}: 14" >> zones.expect
+echo "${sign_prefix} ${ksk_id}: 2" >> zones.expect
+cat zones.expect | sort > zones.expect.$n
+rm -f zones.expect
+# Fetch and check the dnssec sign statistics.
+if [ $PERL_XML ]; then
+ getzones xml $zone x$n || ret=1
+ cmp zones.out.x$n zones.expect.$n || ret=1
+fi
+if [ $PERL_JSON ]; then
+ getzones json 0 j$n || ret=1
+ cmp zones.out.j$n zones.expect.$n || ret=1
+fi
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=`expr $status + $ret`
+n=`expr $n + 1`
+
+# Test sign operations for scheduled resigning (many keys).
+ret=0
+zone="manykeys"
+ksk8_id=`cat ns2/$zone.ksk8.id`
+zsk8_id=`cat ns2/$zone.zsk8.id`
+ksk13_id=`cat ns2/$zone.ksk13.id`
+zsk13_id=`cat ns2/$zone.zsk13.id`
+ksk14_id=`cat ns2/$zone.ksk14.id`
+zsk14_id=`cat ns2/$zone.zsk14.id`
+num_ids=$( (echo $ksk8_id; echo $zsk8_id; echo $ksk13_id; echo $zsk13_id; echo $ksk14_id; echo $zsk14_id;) | sort -u | wc -l)
+# The dnssec zone has 10 RRsets to sign (including NSEC) with the ZSKs and one
+# RRset (DNSKEY) with the KSKs. So starting named with signatures that expire
+# almost right away, this should trigger 10 zsk and 1 ksk sign operations per
+# key.
+echo "${refresh_prefix} ${zsk8_id}: 10" > zones.expect
+echo "${refresh_prefix} ${zsk13_id}: 10" >> zones.expect
+echo "${refresh_prefix} ${zsk14_id}: 10" >> zones.expect
+echo "${refresh_prefix} ${ksk8_id}: 1" >> zones.expect
+echo "${refresh_prefix} ${ksk13_id}: 1" >> zones.expect
+echo "${refresh_prefix} ${ksk14_id}: 1" >> zones.expect
+echo "${sign_prefix} ${zsk8_id}: 10" >> zones.expect
+echo "${sign_prefix} ${zsk13_id}: 10" >> zones.expect
+echo "${sign_prefix} ${zsk14_id}: 10" >> zones.expect
+echo "${sign_prefix} ${ksk8_id}: 1" >> zones.expect
+echo "${sign_prefix} ${ksk13_id}: 1" >> zones.expect
+echo "${sign_prefix} ${ksk14_id}: 1" >> zones.expect
+cat zones.expect | sort > zones.expect.$n
+rm -f zones.expect
+# Fetch and check the dnssec sign statistics.
+echo_i "fetching zone '$zone' stats data after zone maintenance at startup ($n)"
+if test $num_ids -eq 6
+then
+ if [ $PERL_XML ]; then
+ getzones xml $zone x$n || ret=1
+ cmp zones.out.x$n zones.expect.$n || ret=1
+ fi
+ if [ $PERL_JSON ]; then
+ getzones json 2 j$n || ret=1
+ cmp zones.out.j$n zones.expect.$n || ret=1
+ fi
+ if [ $ret != 0 ]; then echo_i "failed"; fi
+else
+ echo_i "skipped: duplicate key id detected (fixed in BIND 9.19)"
+fi
+status=`expr $status + $ret`
+n=`expr $n + 1`
+
+# Test sign operations after dynamic update (many keys).
+ret=0
+(
+# Update dnssec zone to trigger signature creation.
+echo zone $zone
+echo server 10.53.0.2 "$PORT"
+echo update add $zone. 300 in txt "nsupdate added me"
+echo send
+) | $NSUPDATE
+# This should trigger the resign of SOA, TXT and NSEC (+3 zsk).
+echo "${refresh_prefix} ${zsk8_id}: 10" > zones.expect
+echo "${refresh_prefix} ${zsk13_id}: 10" >> zones.expect
+echo "${refresh_prefix} ${zsk14_id}: 10" >> zones.expect
+echo "${refresh_prefix} ${ksk8_id}: 1" >> zones.expect
+echo "${refresh_prefix} ${ksk13_id}: 1" >> zones.expect
+echo "${refresh_prefix} ${ksk14_id}: 1" >> zones.expect
+echo "${sign_prefix} ${zsk8_id}: 13" >> zones.expect
+echo "${sign_prefix} ${zsk13_id}: 13" >> zones.expect
+echo "${sign_prefix} ${zsk14_id}: 13" >> zones.expect
+echo "${sign_prefix} ${ksk8_id}: 1" >> zones.expect
+echo "${sign_prefix} ${ksk13_id}: 1" >> zones.expect
+echo "${sign_prefix} ${ksk14_id}: 1" >> zones.expect
+cat zones.expect | sort > zones.expect.$n
+rm -f zones.expect
+# Fetch and check the dnssec sign statistics.
+echo_i "fetching zone '$zone' stats data after dynamic update ($n)"
+if test $num_ids -eq 6
+then
+ if [ $PERL_XML ]; then
+ getzones xml $zone x$n || ret=1
+ cmp zones.out.x$n zones.expect.$n || ret=1
+ fi
+ if [ $PERL_JSON ]; then
+ getzones json 2 j$n || ret=1
+ cmp zones.out.j$n zones.expect.$n || ret=1
+ fi
+ if [ $ret != 0 ]; then echo_i "failed"; fi
+else
+ echo_i "skipped: duplicate key id detected (fixed in BIND 9.19)"
+fi
+status=`expr $status + $ret`
+n=`expr $n + 1`
+
+# Test sign operations after dnssec-policy change (removing keys).
+ret=0
+copy_setports ns2/named2.conf.in ns2/named.conf
+$RNDCCMD 10.53.0.2 reload 2>&1 | sed 's/^/I:ns2 /'
+# This should trigger the resign of DNSKEY (+1 ksk), and SOA, NSEC,
+# TYPE65534 (+3 zsk). The dnssec-sign statistics for the removed keys should
+# be cleared and thus no longer visible. But NSEC and SOA are (mistakenly)
+# counted double, one time because of zone_resigninc and one time because of
+# zone_nsec3chain. So +5 zsk in total.
+echo "${refresh_prefix} ${zsk8_id}: 15" > zones.expect
+echo "${refresh_prefix} ${ksk8_id}: 2" >> zones.expect
+echo "${sign_prefix} ${zsk8_id}: 18" >> zones.expect
+echo "${sign_prefix} ${ksk8_id}: 2" >> zones.expect
+cat zones.expect | sort > zones.expect.$n
+rm -f zones.expect
+# Fetch and check the dnssec sign statistics.
+echo_i "fetching zone '$zone' stats data after dnssec-policy change ($n)"
+if [ $PERL_XML ]; then
+ getzones xml $zone x$n || ret=1
+ cmp zones.out.x$n zones.expect.$n || ret=1
+fi
+if [ $PERL_JSON ]; then
+ getzones json 2 j$n || ret=1
+ cmp zones.out.j$n zones.expect.$n || ret=1
+fi
+if [ $ret != 0 ]; then echo_i "failed"; fi
+status=`expr $status + $ret`
+n=`expr $n + 1`
+
+echo_i "exit status: $status"
+[ $status -eq 0 ] || exit 1
diff --git a/bin/tests/system/statschannel/tests_json.py b/bin/tests/system/statschannel/tests_json.py
new file mode 100755
index 0000000..c459925
--- /dev/null
+++ b/bin/tests/system/statschannel/tests_json.py
@@ -0,0 +1,105 @@
+#!/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 datetime import datetime
+
+import pytest
+
+import generic
+import pytest_custom_markers
+
+pytestmark = pytest_custom_markers.have_json_c
+requests = pytest.importorskip("requests")
+
+
+# JSON helper functions
+def fetch_zones_json(statsip, statsport):
+ r = requests.get(
+ "http://{}:{}/json/v1/zones".format(statsip, statsport), timeout=600
+ )
+ assert r.status_code == 200
+
+ data = r.json()
+ return data["views"]["_default"]["zones"]
+
+
+def fetch_traffic_json(statsip, statsport):
+ r = requests.get(
+ "http://{}:{}/json/v1/traffic".format(statsip, statsport), timeout=600
+ )
+ assert r.status_code == 200
+
+ data = r.json()
+
+ return data["traffic"]
+
+
+def load_timers_json(zone, primary=True):
+ name = zone["name"]
+
+ # Check if the primary zone timer exists
+ assert "loaded" in zone
+ loaded = datetime.strptime(zone["loaded"], generic.fmt)
+
+ if primary:
+ # Check if the secondary zone timers does not exist
+ assert "expires" not in zone
+ assert "refresh" not in zone
+ expires = None
+ refresh = None
+ else:
+ assert "expires" in zone
+ assert "refresh" in zone
+ expires = datetime.strptime(zone["expires"], generic.fmt)
+ refresh = datetime.strptime(zone["refresh"], generic.fmt)
+
+ return (name, loaded, expires, refresh)
+
+
+def load_zone_json(zone):
+ name = zone["name"]
+
+ return name
+
+
+def test_zone_timers_primary_json(statsport):
+ generic.test_zone_timers_primary(
+ fetch_zones_json,
+ load_timers_json,
+ statsip="10.53.0.1",
+ statsport=statsport,
+ zonedir="ns1",
+ )
+
+
+def test_zone_timers_secondary_json(statsport):
+ generic.test_zone_timers_secondary(
+ fetch_zones_json,
+ load_timers_json,
+ statsip="10.53.0.3",
+ statsport=statsport,
+ zonedir="ns3",
+ )
+
+
+def test_zone_with_many_keys_json(statsport):
+ generic.test_zone_with_many_keys(
+ fetch_zones_json, load_zone_json, statsip="10.53.0.2", statsport=statsport
+ )
+
+
+def test_traffic_json(named_port, statsport):
+ generic_dnspython = pytest.importorskip("generic_dnspython")
+ generic_dnspython.test_traffic(
+ fetch_traffic_json, statsip="10.53.0.2", statsport=statsport, port=named_port
+ )
diff --git a/bin/tests/system/statschannel/tests_xml.py b/bin/tests/system/statschannel/tests_xml.py
new file mode 100755
index 0000000..7f0b37e
--- /dev/null
+++ b/bin/tests/system/statschannel/tests_xml.py
@@ -0,0 +1,135 @@
+#!/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 datetime import datetime
+import xml.etree.ElementTree as ET
+
+import pytest
+
+import generic
+import pytest_custom_markers
+
+pytestmark = pytest_custom_markers.have_libxml2
+requests = pytest.importorskip("requests")
+
+
+# XML helper functions
+def fetch_zones_xml(statsip, statsport):
+ r = requests.get(
+ "http://{}:{}/xml/v3/zones".format(statsip, statsport), 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
+
+ return default_view.find("zones").findall("zone")
+
+
+def fetch_traffic_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(
+ "http://{}:{}/xml/v3/traffic".format(statsip, statsport), timeout=600
+ )
+ assert r.status_code == 200
+
+ root = ET.fromstring(r.text)
+
+ traffic = {}
+ for ip in ["ipv4", "ipv6"]:
+ for proto in ["udp", "tcp"]:
+ proto_root = root.find("traffic").find(ip).find(proto)
+ for counters in proto_root.findall("counters"):
+ if counters.attrib["type"] == "request-size":
+ key = "dns-{}-requests-sizes-received-{}".format(proto, ip)
+ else:
+ key = "dns-{}-responses-sizes-sent-{}".format(proto, ip)
+
+ values = load_counters(counters)
+ traffic[key] = values
+
+ return traffic
+
+
+def load_timers_xml(zone, primary=True):
+ name = zone.attrib["name"]
+
+ loaded_el = zone.find("loaded")
+ assert loaded_el is not None
+ loaded = datetime.strptime(loaded_el.text, generic.fmt)
+
+ expires_el = zone.find("expires")
+ refresh_el = zone.find("refresh")
+ if primary:
+ assert expires_el is None
+ assert refresh_el is None
+ expires = None
+ refresh = None
+ else:
+ assert expires_el is not None
+ assert refresh_el is not None
+ expires = datetime.strptime(expires_el.text, generic.fmt)
+ refresh = datetime.strptime(refresh_el.text, generic.fmt)
+
+ return (name, loaded, expires, refresh)
+
+
+def load_zone_xml(zone):
+ name = zone.attrib["name"]
+
+ return name
+
+
+def test_zone_timers_primary_xml(statsport):
+ generic.test_zone_timers_primary(
+ fetch_zones_xml,
+ load_timers_xml,
+ statsip="10.53.0.1",
+ statsport=statsport,
+ zonedir="ns1",
+ )
+
+
+def test_zone_timers_secondary_xml(statsport):
+ generic.test_zone_timers_secondary(
+ fetch_zones_xml,
+ load_timers_xml,
+ statsip="10.53.0.3",
+ statsport=statsport,
+ zonedir="ns3",
+ )
+
+
+def test_zone_with_many_keys_xml(statsport):
+ generic.test_zone_with_many_keys(
+ fetch_zones_xml, load_zone_xml, statsip="10.53.0.2", statsport=statsport
+ )
+
+
+def test_traffic_xml(named_port, statsport):
+ generic_dnspython = pytest.importorskip("generic_dnspython")
+ generic_dnspython.test_traffic(
+ fetch_traffic_xml, statsip="10.53.0.2", statsport=statsport, port=named_port
+ )
diff --git a/bin/tests/system/statschannel/traffic-json.pl b/bin/tests/system/statschannel/traffic-json.pl
new file mode 100644
index 0000000..353d6c7
--- /dev/null
+++ b/bin/tests/system/statschannel/traffic-json.pl
@@ -0,0 +1,49 @@
+#!/usr/bin/perl
+
+# 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.
+
+# traffic-json.pl:
+# Parses the JSON version of the RSSAC002 traffic stats into a
+# normalized format.
+
+use JSON;
+
+my $file = $ARGV[0];
+open(INPUT, "<$file");
+my $text = do{local$/;<INPUT>};
+close(INPUT);
+
+my $ref = decode_json($text);
+
+my $tcprcvd = $ref->{traffic}->{"dns-tcp-requests-sizes-received-ipv4"};
+my $type = "tcp request-size ";
+foreach $key (keys %{$tcprcvd}) {
+ print $type . $key . ": ". $tcprcvd->{$key} ."\n";
+}
+
+my $tcpsent = $ref->{traffic}->{"dns-tcp-responses-sizes-sent-ipv4"};
+my $type = "tcp response-size ";
+foreach $key (keys %{$tcpsent}) {
+ print $type . $key . ": ". $tcpsent->{$key} ."\n";
+}
+
+my $udprcvd = $ref->{traffic}->{"dns-udp-requests-sizes-received-ipv4"};
+my $type = "udp request-size ";
+foreach $key (keys %{$udprcvd}) {
+ print $type . $key . ": ". $udprcvd->{$key} ."\n";
+}
+
+my $udpsent = $ref->{traffic}->{"dns-udp-responses-sizes-sent-ipv4"};
+my $type = "udp response-size ";
+foreach $key (keys %{$udpsent}) {
+ print $type . $key . ": ". $udpsent->{$key} ."\n";
+}
diff --git a/bin/tests/system/statschannel/traffic-xml.pl b/bin/tests/system/statschannel/traffic-xml.pl
new file mode 100644
index 0000000..5552cc5
--- /dev/null
+++ b/bin/tests/system/statschannel/traffic-xml.pl
@@ -0,0 +1,46 @@
+#!/usr/bin/perl
+
+# 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.
+
+# traffic-xml.pl:
+# Parses the XML version of the RSSAC002 traffic stats into a
+# normalized format.
+
+use XML::Simple;
+
+my $file = $ARGV[0];
+
+my $ref = XMLin($file);
+
+my $udp = $ref->{traffic}->{ipv4}->{udp}->{counters};
+foreach $group (@$udp) {
+ my $type = "udp " . $group->{type} . " ";
+ if (exists $group->{counter}->{name}) {
+ print $type . $group->{counter}->{name} . ": " . $group->{counter}->{content} . "\n";
+ } else {
+ foreach $key (keys %{$group->{counter}}) {
+ print $type . $key . ": ". $group->{counter}->{$key}->{content} ."\n";
+ }
+ }
+}
+
+my $tcp = $ref->{traffic}->{ipv4}->{tcp}->{counters};
+foreach $group (@$tcp) {
+ my $type = "tcp " . $group->{type} . " ";
+ if (exists $group->{counter}->{name}) {
+ print $type . $group->{counter}->{name} . ": " . $group->{counter}->{content} . "\n";
+ } else {
+ foreach $key (keys %{$group->{counter}}) {
+ print $type . $key . ": ". $group->{counter}->{$key}->{content} ."\n";
+ }
+ }
+}
diff --git a/bin/tests/system/statschannel/traffic.expect.1 b/bin/tests/system/statschannel/traffic.expect.1
new file mode 100644
index 0000000..5938d5d
--- /dev/null
+++ b/bin/tests/system/statschannel/traffic.expect.1
@@ -0,0 +1,2 @@
+tcp request-size 16-31: 1
+tcp response-size 64-79: 1
diff --git a/bin/tests/system/statschannel/traffic.expect.2 b/bin/tests/system/statschannel/traffic.expect.2
new file mode 100644
index 0000000..6c9e25a
--- /dev/null
+++ b/bin/tests/system/statschannel/traffic.expect.2
@@ -0,0 +1,4 @@
+tcp request-size 16-31: 1
+tcp response-size 64-79: 1
+udp request-size 48-63: 1
+udp response-size 112-127: 1
diff --git a/bin/tests/system/statschannel/traffic.expect.4 b/bin/tests/system/statschannel/traffic.expect.4
new file mode 100644
index 0000000..3f892f5
--- /dev/null
+++ b/bin/tests/system/statschannel/traffic.expect.4
@@ -0,0 +1,5 @@
+tcp request-size 16-31: 1
+tcp response-size 64-79: 1
+udp request-size 48-63: 2
+udp response-size 112-127: 1
+udp response-size 848-863: 1
diff --git a/bin/tests/system/statschannel/traffic.expect.5 b/bin/tests/system/statschannel/traffic.expect.5
new file mode 100644
index 0000000..15911b1
--- /dev/null
+++ b/bin/tests/system/statschannel/traffic.expect.5
@@ -0,0 +1,7 @@
+tcp request-size 16-31: 1
+tcp request-size 48-63: 1
+tcp response-size 112-127: 1
+tcp response-size 64-79: 1
+udp request-size 48-63: 2
+udp response-size 112-127: 1
+udp response-size 848-863: 1
diff --git a/bin/tests/system/statschannel/traffic.expect.6 b/bin/tests/system/statschannel/traffic.expect.6
new file mode 100644
index 0000000..73fc8f1
--- /dev/null
+++ b/bin/tests/system/statschannel/traffic.expect.6
@@ -0,0 +1,8 @@
+tcp request-size 16-31: 1
+tcp request-size 48-63: 2
+tcp response-size 112-127: 1
+tcp response-size 64-79: 1
+tcp response-size 848-863: 1
+udp request-size 48-63: 2
+udp response-size 112-127: 1
+udp response-size 848-863: 1
diff --git a/bin/tests/system/statschannel/zones-json.pl b/bin/tests/system/statschannel/zones-json.pl
new file mode 100644
index 0000000..9eec9db
--- /dev/null
+++ b/bin/tests/system/statschannel/zones-json.pl
@@ -0,0 +1,37 @@
+#!/usr/bin/perl
+
+# 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.
+
+# zones-json.pl:
+# Parses the JSON version of the dnssec sign stats for the
+# "dnssec" zone in the default view into a normalized format.
+
+use JSON;
+
+my $file = $ARGV[0];
+my $zone = $ARGV[1];
+open(INPUT, "<$file");
+my $text = do{local$/;<INPUT>};
+close(INPUT);
+
+my $ref = decode_json($text);
+
+my $dnssecsign = $ref->{views}->{_default}->{zones}[$zone]->{"dnssec-sign"};
+my $type = "dnssec-sign operations ";
+foreach $key (keys %{$dnssecsign}) {
+ print $type . $key . ": ". $dnssecsign->{$key} ."\n";
+}
+my $dnssecrefresh = $ref->{views}->{_default}->{zones}[$zone]->{"dnssec-refresh"};
+my $type = "dnssec-refresh operations ";
+foreach $key (keys %{$dnssecrefresh}) {
+ print $type . $key . ": ". $dnssecrefresh->{$key} ."\n";
+}
diff --git a/bin/tests/system/statschannel/zones-xml.pl b/bin/tests/system/statschannel/zones-xml.pl
new file mode 100644
index 0000000..be86852
--- /dev/null
+++ b/bin/tests/system/statschannel/zones-xml.pl
@@ -0,0 +1,40 @@
+#!/usr/bin/perl
+
+# 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.
+
+# zones-xml.pl:
+# Parses the XML version of the dnssec sign stats for the
+# "dnssec" zone in the default view into a normalized format.
+
+use XML::Simple;
+
+my $file = $ARGV[0];
+my $zone = $ARGV[1];
+
+my $ref = XMLin($file);
+
+my $counters = $ref->{views}->{view}->{_default}->{zones}->{zone}->{$zone}->{counters};
+
+foreach $group (@$counters) {
+
+ my $type = $group->{type};
+
+ if ($type eq "dnssec-sign" || $type eq "dnssec-refresh") {
+ if (exists $group->{counter}->{name}) {
+ print $type . " operations " . $group->{counter}->{name} . ": " . $group->{counter}->{content} . "\n";
+ } else {
+ foreach $key (keys %{$group->{counter}}) {
+ print $type . " operations " . $key . ": ". $group->{counter}->{$key}->{content} ."\n";
+ }
+ }
+ }
+}