From 14f0e13ec4f125b5ef0256ed891f0ee5f7af4e11 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Tue, 29 Sep 2026 17:18:05 -0300 Subject: [PATCH] fix(xcat-server): a Debian service node's named serves the wrong configuration Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- xCAT-server/lib/perl/xCAT/SvrUtils.pm | 27 ++++++ xCAT-server/lib/xcat/plugins/AAsn.pm | 12 ++- xCAT-server/sbin/makenamed.conf | 64 ++++++++++-- xCAT-test/bats/makenamed_forwarders.bats | 107 +++++++++++++++++++++ xCAT-test/unit/aasn_named_service_action.t | 33 +++++++ 5 files changed, 231 insertions(+), 12 deletions(-) create mode 100644 xCAT-test/bats/makenamed_forwarders.bats create mode 100644 xCAT-test/unit/aasn_named_service_action.t diff --git a/xCAT-server/lib/perl/xCAT/SvrUtils.pm b/xCAT-server/lib/perl/xCAT/SvrUtils.pm index a4440e7b3..d1292d45b 100644 --- a/xCAT-server/lib/perl/xCAT/SvrUtils.pm +++ b/xCAT-server/lib/perl/xCAT/SvrUtils.pm @@ -2528,4 +2528,31 @@ sub searchcompressedrootimg{ } + +#----------------------------------------------------------------------------- + +=head3 named_service_action + + Which service action brings a freshly written named configuration into effect. + + Linux gets a restart, not a start. On Debian the package already runs the daemon, so a start + is a no-op and named keeps serving the configuration it read at install time: the zone + makenamed.conf has just written is never loaded, and a compute node cannot resolve its + service node. AIX keeps the start it has always used. + + Arguments: the platform, 'aix' or 'linux' + Returns: 'start', 'restart', or '' for a platform with no action + +=cut + +#----------------------------------------------------------------------------- +sub named_service_action { + my ($platform) = @_; + $platform = '' unless defined $platform; + return 'start' if $platform eq 'aix'; + return 'restart' if $platform eq 'linux'; + return ''; +} + + 1; diff --git a/xCAT-server/lib/xcat/plugins/AAsn.pm b/xCAT-server/lib/xcat/plugins/AAsn.pm index bc5350ccf..54a93d3e0 100644 --- a/xCAT-server/lib/xcat/plugins/AAsn.pm +++ b/xCAT-server/lib/xcat/plugins/AAsn.pm @@ -788,6 +788,8 @@ sub setup_FTP =cut #----------------------------------------------------------------------------- + + sub setup_DNS { my $srvclist = shift; @@ -827,11 +829,15 @@ sub setup_DNS #} #my $rc = xCAT::Utils->startService($serv); + # Restart, not start. makenamed.conf has just rewritten named.conf, and a start is a no-op + # where the package already runs the daemon -- Debian does -- so the service keeps serving the + # configuration it read at install time. my $rc = 0; - if (xCAT::Utils->isAIX()) { + my $action = xCAT::SvrUtils::named_service_action(xCAT::Utils->isAIX() ? 'aix' : 'linux'); + if ($action eq 'start') { $rc = xCAT::Utils->startService("named"); - } elsif (xCAT::Utils->isLinux()) { - $rc = xCAT::Utils->startservice("named"); + } elsif ($action eq 'restart') { + $rc = xCAT::Utils->restartservice("named"); } if ($rc != 0) diff --git a/xCAT-server/sbin/makenamed.conf b/xCAT-server/sbin/makenamed.conf index f63c0f480..f236b9d5c 100755 --- a/xCAT-server/sbin/makenamed.conf +++ b/xCAT-server/sbin/makenamed.conf @@ -19,7 +19,7 @@ is_lsb_ubuntu () exit 1 # Not Ubuntu } - ' /etc/lsb-release >/dev/null 2>&1 + ' "${1:-/etc/lsb-release}" >/dev/null 2>&1 # Routine exit status is exit status of the last command -- the awk script. # @@ -28,14 +28,60 @@ is_lsb_ubuntu () } -DIRECTORY=/var/named +# forwarder_addresses [ []] +# +# The addresses named must forward to, one per line. +# +# A loopback address is not a forwarder. On a host managed by systemd-resolved /etc/resolv.conf +# holds the 127.0.0.53 stub, and that stub points back at this named for the link, so +# "forward only" to it answers nothing. systemd-resolved writes the real servers to its own +# uplink file, which is the fallback. +forwarder_addresses() +{ + _fa_resolv=${1:-/etc/resolv.conf} + _fa_uplink=${2:-/run/systemd/resolve/resolv.conf} + _fa_addrs=$(_fa_usable "$_fa_resolv") + if [ -z "$_fa_addrs" ]; then + _fa_addrs=$(_fa_usable "$_fa_uplink") + fi + [ -n "$_fa_addrs" ] && printf '%s\n' "$_fa_addrs" + return 0 +} -# check for SLES -grep -s -q sles /etc/os-release -IS_SLES=$? -if [ -f /etc/SuSE-release ] || [ $IS_SLES -eq 0 ]; then - DIRECTORY=/var/lib/named -fi +_fa_usable() +{ + [ -r "$1" ] || return 0 + awk '$1 == "nameserver" && $2 !~ /^127\./ && $2 != "::1" { print $2 }' "$1" 2>/dev/null +} + +# named_directory [ [ []]] +# +# The working directory for named. It must be writable by the user named drops to: Debian runs +# it as "bind" and ships /var/cache/bind for this, while /var/named is created root-owned. named +# writes its managed-keys database there, and when that write fails it answers NXDOMAIN to every +# query, forwarded ones included. +named_directory() +{ + _nd_lsb=${1:-/etc/lsb-release} + _nd_os=${2:-/etc/os-release} + _nd_suse=${3:-/etc/SuSE-release} + + if is_lsb_ubuntu "$_nd_lsb"; then + echo /var/cache/bind + return 0 + fi + if [ -f "$_nd_suse" ] || grep -s -q sles "$_nd_os"; then + echo /var/lib/named + return 0 + fi + echo /var/named +} + +# A test loads this file for the routine above and must not run the rest, which writes +# named.conf and restarts the service. +[ "${MAKENAMED_LIB:-}" = 1 ] && return 0 + +DIRECTORY=$(named_directory) FILE=/etc/named.conf if ( is_lsb_ubuntu ); then @@ -58,7 +104,7 @@ echo "options { forward only; forwarders {" >$FILE -for i in $(grep "^nameserver" /etc/resolv.conf | awk '{print $2}') +forwarder_addresses | while read -r i do echo " $i;" done >>$FILE diff --git a/xCAT-test/bats/makenamed_forwarders.bats b/xCAT-test/bats/makenamed_forwarders.bats new file mode 100644 index 000000000..0bda68e04 --- /dev/null +++ b/xCAT-test/bats/makenamed_forwarders.bats @@ -0,0 +1,107 @@ +#!/usr/bin/env bats +# +# makenamed.conf builds the forwarder list of a service node's named from /etc/resolv.conf. +# +# On a host managed by systemd-resolved that file holds the 127.0.0.53 stub, and the stub points +# back at this named for the link. forward-only to it is a loop: the service node answers +# nothing, and every compute node behind it fails to resolve. Measured on xcat22-sn, where +# "dig @ xcat22-sn.xcat22.lab" returned nothing while the same query to the management node +# answered. The EL service node has the real upstream in /etc/resolv.conf, which is why it works. +# +# systemd-resolved writes the real servers to /run/systemd/resolve/resolv.conf. + +load 'helpers/shell_source' + +setup() +{ + SCRIPT="$(require_repo_file 'xCAT-server/sbin/makenamed.conf')" + RESOLV="${BATS_TEST_TMPDIR}/resolv.conf" + UPLINK="${BATS_TEST_TMPDIR}/uplink.conf" + # MAKENAMED_LIB stops the script before it writes anything, so only the routine is loaded. + MAKENAMED_LIB=1 . "$SCRIPT" + export -f forwarder_addresses _fa_usable +} + +@test "a real nameserver is a forwarder" { + printf 'nameserver 192.168.222.1\nsearch xcat22.lab\n' >"$RESOLV" + run forwarder_addresses "$RESOLV" "$UPLINK" + [ "$status" -eq 0 ] + [ "$output" = "192.168.222.1" ] +} + +@test "the systemd-resolved stub is not a forwarder, and the uplink answers instead" { + printf 'nameserver 127.0.0.53\noptions edns0 trust-ad\n' >"$RESOLV" + printf 'nameserver 192.168.222.1\nsearch xcat22.lab\n' >"$UPLINK" + run forwarder_addresses "$RESOLV" "$UPLINK" + [ "$status" -eq 0 ] + [ "$output" = "192.168.222.1" ] +} + +@test "any loopback address is rejected, not only the stub" { + printf 'nameserver 127.0.0.1\nnameserver ::1\n' >"$RESOLV" + printf 'nameserver 10.0.0.1\n' >"$UPLINK" + run forwarder_addresses "$RESOLV" "$UPLINK" + [ "$output" = "10.0.0.1" ] +} + +@test "several real nameservers are all forwarders, in order" { + printf 'nameserver 192.168.222.1\nnameserver 10.0.0.2\n' >"$RESOLV" + run forwarder_addresses "$RESOLV" "$UPLINK" + [ "$output" = "192.168.222.1 +10.0.0.2" ] +} + +@test "a real nameserver wins over the uplink, which is only the fallback" { + printf 'nameserver 192.168.222.1\n' >"$RESOLV" + printf 'nameserver 10.9.9.9\n' >"$UPLINK" + run forwarder_addresses "$RESOLV" "$UPLINK" + [ "$output" = "192.168.222.1" ] +} + +@test "no usable address anywhere prints nothing rather than a blank forwarder" { + printf 'nameserver 127.0.0.53\n' >"$RESOLV" + : >"$UPLINK" + # Count the lines. A blank line reaches named.conf as an empty forwarder entry, which bind + # rejects as a syntax error, and $output cannot tell one from no output at all. + run bash -c "forwarder_addresses '$RESOLV' '$UPLINK' | wc -l" + [ "$status" -eq 0 ] + [ "$output" = "0" ] +} + +@test "an absent uplink file is not an error" { + printf 'nameserver 127.0.0.53\n' >"$RESOLV" + run forwarder_addresses "$RESOLV" "${BATS_TEST_TMPDIR}/absent" + [ "$status" -eq 0 ] + [ "$output" = "" ] +} + +# named runs as "bind" on Debian and writes its managed-keys database into the configured +# directory. /var/named is created root-owned, so the write fails, DNSSEC initialisation fails +# with it, and the server answers NXDOMAIN to every query -- including forwarded ones, which is +# how a correct forwarder list still resolved nothing on xcat22-sn. + +@test "Ubuntu names the directory its named can write" { + printf 'DISTRIB_ID=Ubuntu\nDISTRIB_RELEASE=24.04\n' >"${BATS_TEST_TMPDIR}/lsb" + run named_directory "${BATS_TEST_TMPDIR}/lsb" "${BATS_TEST_TMPDIR}/absent-os" "${BATS_TEST_TMPDIR}/absent-suse" + [ "$output" = "/var/cache/bind" ] +} + +@test "SLES keeps its own directory" { + : >"${BATS_TEST_TMPDIR}/lsb" + printf 'ID="sles"\n' >"${BATS_TEST_TMPDIR}/os" + run named_directory "${BATS_TEST_TMPDIR}/lsb" "${BATS_TEST_TMPDIR}/os" "${BATS_TEST_TMPDIR}/absent-suse" + [ "$output" = "/var/lib/named" ] +} + +@test "EL keeps /var/named" { + : >"${BATS_TEST_TMPDIR}/lsb" + printf 'ID="almalinux"\n' >"${BATS_TEST_TMPDIR}/os" + run named_directory "${BATS_TEST_TMPDIR}/lsb" "${BATS_TEST_TMPDIR}/os" "${BATS_TEST_TMPDIR}/absent-suse" + [ "$output" = "/var/named" ] +} + +@test "an absent lsb-release is not Ubuntu" { + printf 'ID="almalinux"\n' >"${BATS_TEST_TMPDIR}/os" + run named_directory "${BATS_TEST_TMPDIR}/absent-lsb" "${BATS_TEST_TMPDIR}/os" "${BATS_TEST_TMPDIR}/absent-suse" + [ "$output" = "/var/named" ] +} diff --git a/xCAT-test/unit/aasn_named_service_action.t b/xCAT-test/unit/aasn_named_service_action.t new file mode 100644 index 000000000..2082b557d --- /dev/null +++ b/xCAT-test/unit/aasn_named_service_action.t @@ -0,0 +1,33 @@ +#!/usr/bin/env perl + +# setup_DNS writes named.conf through makenamed.conf and then brings the daemon up. It used to +# START the service. On Debian the package already runs named, so a start is a no-op: the daemon +# keeps serving the configuration it read at install time, the zone xCAT has just written is +# never loaded, and a compute node cannot resolve its service node. +# +# A minimization of this change set dropped that fix, because no fast test could see it. The +# decision lives in xCAT::SvrUtils, which a test can load; AAsn.pm cannot be loaded from a source +# tree at all, since it pulls in xCAT::Table and the rest of the server. + +use strict; +use warnings; + +use FindBin; +use Test::More; + +use lib "$FindBin::Bin/../../perl-xCAT"; +use lib "$FindBin::Bin/../../xCAT-server/lib/perl"; +use xCAT::SvrUtils; + +can_ok('xCAT::SvrUtils', 'named_service_action') or do { done_testing(); exit 1 }; + +is(xCAT::SvrUtils::named_service_action('linux'), 'restart', + 'Linux restarts named, so the configuration just written is the one it serves'); +is(xCAT::SvrUtils::named_service_action('aix'), 'start', + 'AIX keeps the start it has always used'); +is(xCAT::SvrUtils::named_service_action(''), '', + 'an unknown platform does nothing rather than guessing an action'); +is(xCAT::SvrUtils::named_service_action(undef), '', + 'an undefined platform does nothing'); + +done_testing();