2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-10-06 17:46:55 +00:00

fix(provtest,dhcptest): make a failed teardown safe to fail

Teardown removed nodes, restored tables and unpacked the saved zone tree
without checking any of it, then deleted the state directory regardless.
A restore that half-failed left the machine wrong and the record of what
to put back gone.

Every removal and restore is checked now. The zone archive is verified
when written and listed again before the live tree is removed, so an
unreadable archive leaves the tree alone. Anything that fails keeps the
state directory and returns non-zero. The kickstarts mkinstall renders
under autoinst are removed by name, and the loader moved aside during the
absent-loader stage stays in tftpdir rather than under /tmp.
This commit is contained in:
Daniel Hilst
2026-09-11 11:13:01 -03:00
parent 68075b6b44
commit ecbdd53e83
2 changed files with 99 additions and 26 deletions
@@ -817,7 +817,10 @@ do_run_loader_absent() {
tftp=$(tftpdir)
path="$tftp/xcat/xnba.kpxe"
[ -f "$path" ] || die "$path is not there to remove"
mv -f "$path" "$STATE/xnba.kpxe.away" || die "cannot move $path aside"
# Moved aside within $tftpdir and not into $STATE: $STATE is under /tmp, and
# this is the machine's only copy of a loader xCAT does not rebuild. A
# reboot between here and restore_absent_loader would lose it.
mv -f "$path" "$path.provtest-away" || die "cannot move $path aside"
echo "$path" > "$STATE/loader-away"
if do_generate; then
@@ -837,7 +840,7 @@ restore_absent_loader() {
local path
[ -f "$STATE/loader-away" ] || return 0
path=$(cat "$STATE/loader-away")
mv -f "$STATE/xnba.kpxe.away" "$path" 2>/dev/null
[ -n "$path" ] && mv -f "$path.provtest-away" "$path" 2>/dev/null
rm -f "$STATE/loader-away"
}
@@ -922,11 +925,14 @@ do_run_hierarchy() {
}
do_teardown() {
local unit
local unit failed=0
[ -d "$STATE" ] || return 0
[ -f "$STATE/node" ] && { makedhcp -d "$NODE" >/dev/null 2>&1; makehosts -d "$NODE" >/dev/null 2>&1; rmdef "$NODE" >/dev/null 2>&1; }
[ -f "$STATE/adopt" ] && { makedhcp -d "$ADOPT_NODE" >/dev/null 2>&1; makehosts -d "$ADOPT_NODE" >/dev/null 2>&1; rmdef "$ADOPT_NODE" >/dev/null 2>&1; }
# Each removal is checked. A definition left behind makes the next run on
# this machine refuse, and deleting $STATE anyway would throw away the
# record of what still has to be put back.
[ -f "$STATE/node" ] && { makedhcp -d "$NODE" >/dev/null 2>&1; makehosts -d "$NODE" >/dev/null 2>&1; rmdef "$NODE" >/dev/null 2>&1 || { say "FAILED: cannot remove the node $NODE"; failed=1; }; }
[ -f "$STATE/adopt" ] && { makedhcp -d "$ADOPT_NODE" >/dev/null 2>&1; makehosts -d "$ADOPT_NODE" >/dev/null 2>&1; rmdef "$ADOPT_NODE" >/dev/null 2>&1 || { say "FAILED: cannot remove the node $ADOPT_NODE"; failed=1; }; }
netboot_undefine
extra_undefine
[ -f "$STATE/iscsi" ] && chtab -d node="$ISCSI_NODE" iscsi >/dev/null 2>&1
@@ -939,11 +945,15 @@ do_teardown() {
# tabrestore replaces the table wholesale, which is what is wanted here:
# dhcpinterfaces and dhcpbackend go back to what they were, unset included.
[ -f "$STATE/site.csv" ] && tabrestore "$STATE/site.csv" >/dev/null 2>&1
if [ -f "$STATE/site.csv" ]; then
tabrestore "$STATE/site.csv" >/dev/null 2>&1 \
|| { say "FAILED: cannot restore the site table from $STATE/site.csv"; failed=1; }
fi
for f in /etc/dhcp/dhcpd.conf /etc/dhcpd.conf /etc/kea/kea-dhcp4.conf; do
local saved="$STATE/$(echo "$f" | tr / _)"
[ -f "$saved" ] && cp -f "$saved" "$f"
[ -f "$saved" ] || continue
cp -f "$saved" "$f" || { say "FAILED: cannot restore $f"; failed=1; }
done
if [ -f "$STATE/backend" ]; then
@@ -958,6 +968,10 @@ do_teardown() {
[ -n "$unit" ] && systemctl restart "$unit" >/dev/null 2>&1
fi
if [ "$failed" != 0 ]; then
say "$STATE was kept; it holds what this machine has to be put back to"
return 1
fi
rm -rf "$STATE"
say "fixture removed"
}
@@ -467,9 +467,16 @@ define_node() {
mn_hosts_entry() {
local short addr
short=$(hostname -s) || die "cannot read this machine's hostname"
addr=$(getent ahostsv4 "$(hostname)" 2>/dev/null | awk '{print $1; exit}')
[ -n "$addr" ] || addr=$(hostname -I 2>/dev/null | awk '{print $1}')
[ -n "$addr" ] || die "cannot find an address for $short"
# Not a loopback address: Debian and Ubuntu map the hostname to 127.0.1.1 in
# /etc/hosts, and makedns would write that as the address of the zone's
# server -- a nameserver every node is told to ask and none can reach.
addr=$(getent ahostsv4 "$(hostname)" 2>/dev/null \
| awk '$1 !~ /^127\./ {print $1; exit}')
[ -n "$addr" ] || addr=$(hostname -I 2>/dev/null | tr ' ' '\n' \
| awk '$1 != "" && $1 !~ /^127\./ {print; exit}')
# Last resort, and a true one: $IF_SRV holds it, and it is the address the
# node reaches this machine on.
[ -n "$addr" ] || addr=$SRV_IP
printf '%s %s.%s\n' "$addr" "$short" "$DOMAIN" >> /etc/hosts \
|| die "cannot add $short.$DOMAIN to /etc/hosts"
}
@@ -606,27 +613,46 @@ do_setup() {
}
save_dns_config() {
local f
local f saved
for f in /etc/named.conf /etc/bind/named.conf /etc/bind/named.conf.local; do
[ -f "$f" ] && cp -f "$f" "$STATE/$(echo "$f" | tr / _)"
[ -f "$f" ] || continue
cp -f "$f" "$STATE/$(echo "$f" | tr / _)" || die "cannot save $f"
done
# The zone tree as a whole: makedns removes inside it, and a file-by-file
# restore would miss that.
# restore would miss that. Both written and read back here, because a tar
# that ran out of room still leaves a file behind -- and teardown deletes a
# live zone tree on the strength of this archive.
for f in /var/named /var/lib/bind /etc/bind; do
[ -d "$f" ] && tar -C / -czf "$STATE/$(echo "$f" | tr / _).tgz" "${f#/}" 2>/dev/null
[ -d "$f" ] || continue
saved="$STATE/$(echo "$f" | tr / _).tgz"
tar -C / -czf "$saved" "${f#/}" 2>/dev/null || die "cannot archive $f to $saved"
tar -tzf "$saved" >/dev/null 2>&1 || die "the archive of $f is unreadable"
done
return 0
}
# Non-zero if anything could not be put back. Teardown needs to know: makedns
# reads the site table and rewrites the zones, so regenerating over a restore
# that half-failed is what turns a recoverable mess into a permanent one.
restore_dns_config() {
local f saved
local f saved rc=0
for f in /var/named /var/lib/bind /etc/bind; do
saved="$STATE/$(echo "$f" | tr / _).tgz"
[ -f "$saved" ] && { rm -rf "$f"; tar -C / -xzf "$saved" 2>/dev/null; }
[ -f "$saved" ] || continue
# Listed before the live tree is removed: an archive that cannot be read
# is one that cannot be extracted either, and then $f would simply be
# gone.
tar -tzf "$saved" >/dev/null 2>&1 \
|| { say "FAILED: $saved is unreadable, so $f was left as it is"; rc=1; continue; }
rm -rf "$f"
tar -C / -xzf "$saved" 2>/dev/null || { say "FAILED: cannot restore $f from $saved"; rc=1; }
done
for f in /etc/named.conf /etc/bind/named.conf /etc/bind/named.conf.local; do
saved="$STATE/$(echo "$f" | tr / _)"
[ -f "$saved" ] && cp -f "$saved" "$f"
[ -f "$saved" ] || continue
cp -f "$saved" "$f" || { say "FAILED: cannot restore $f"; rc=1; }
done
return $rc
}
# The file a node of each netboot type is given, by the name the loader asks for
@@ -1008,28 +1034,43 @@ do_run_dns_removal() {
# --- teardown -------------------------------------------------------------
do_teardown() {
local name path dir
local name path dir failed=0
[ -d "$STATE" ] || return 0
# Every loop reads its list on file descriptor 3, because the xCAT clients
# inside them read standard input themselves: on the first iteration the
# command swallows the rest of the file and teardown stops after one name,
# leaving nodes and a rewritten site table behind.
# Each removal is checked. A definition left behind is what makes the next
# run on this machine refuse -- and if $STATE were deleted anyway, the
# record of what to put back would be gone with it.
if [ -f "$STATE/nodes" ]; then
while read -r name <&3; do
[ -n "$name" ] || continue
nodeset "$name" offline >/dev/null 2>&1
makedns -d "$name" >/dev/null 2>&1
makehosts -d "$name" >/dev/null 2>&1
rmdef "$name" >/dev/null 2>&1
rmdef "$name" >/dev/null 2>&1 \
|| { say "FAILED: cannot remove the node $name"; failed=1; }
done 3< "$STATE/nodes"
fi
if [ -f "$STATE/osimages" ]; then
while read -r name <&3; do
[ -n "$name" ] && rmdef -t osimage -o "$name" >/dev/null 2>&1
[ -n "$name" ] || continue
rmdef -t osimage -o "$name" >/dev/null 2>&1 \
|| { say "FAILED: cannot remove the osimage $name"; failed=1; }
done 3< "$STATE/osimages"
fi
[ -f "$STATE/network" ] && rmdef -t network -o "$NETOBJ" >/dev/null 2>&1
if [ -f "$STATE/network" ]; then
rmdef -t network -o "$NETOBJ" >/dev/null 2>&1 \
|| { say "FAILED: cannot remove the network $NETOBJ"; failed=1; }
fi
# mkinstall renders the node's kickstart here and nothing records it, so it
# is removed by name. Guarded on a non-empty name: this is a glob.
for name in "$NODE" "$PXE_NODE" "$BOOT_NODE" "$XNBA_NODE" "$PTB_NODE"; do
[ -n "$name" ] && rm -rf "$(installdir)/autoinst/$name" "$(installdir)/autoinst/$name".*
done
# What nodeset offline did not take with it. It is not reliable here: it
# exits as soon as it cannot reach the DHCP backend, and leaves the kernel
@@ -1083,20 +1124,38 @@ do_teardown() {
# tabrestore replaces the table wholesale, which is what is wanted here:
# site.master and site.domain go back to what they were, unset included.
[ -f "$STATE/site.csv" ] && tabrestore "$STATE/site.csv" >/dev/null 2>&1
[ -f "$STATE/hosts" ] && cp -f "$STATE/hosts" /etc/hosts
if [ -f "$STATE/site.csv" ]; then
tabrestore "$STATE/site.csv" >/dev/null 2>&1 \
|| { say "FAILED: cannot restore the site table from $STATE/site.csv"; failed=1; }
fi
if [ -f "$STATE/hosts" ]; then
cp -f "$STATE/hosts" /etc/hosts \
|| { say "FAILED: cannot restore /etc/hosts from $STATE/hosts"; failed=1; }
fi
restore_dns_config
restore_dns_config || failed=1
# Regenerated from the restored configuration, so a machine that was serving
# its own zones is serving them again.
makedns -n >/dev/null 2>&1
# its own zones is serving them again -- but only once everything is back.
# makedns reads site.domain and site.master: run over a site table that did
# not restore, it would write this fixture's domain into the zones of a real
# cluster, and teardown would be the thing that broke it.
if [ "$failed" = 0 ]; then
[ -f "$STATE/dns" ] && { makedns -n >/dev/null 2>&1 || say "makedns -n reported an error"; }
else
say "the zones were left alone: regenerating them from a configuration that did not fully restore would make the damage permanent"
fi
restore_services
if [ "$failed" != 0 ]; then
say "$STATE was kept; it holds what this machine has to be put back to"
return 1
fi
rm -rf "$STATE"
say "fixture removed"
}
dispatch() {
STAGE=${1:-fixture}
case "${1:-}" in
check) do_check ;;
setup) do_setup ;;