diff --git a/docs/source/guides/admin-guides/references/man5/site.5.rst b/docs/source/guides/admin-guides/references/man5/site.5.rst index 0496a919e..5da893f95 100644 --- a/docs/source/guides/admin-guides/references/man5/site.5.rst +++ b/docs/source/guides/admin-guides/references/man5/site.5.rst @@ -288,7 +288,9 @@ site Attributes: cleanupdiskfullxcatpost: (yes/1 or no/0). Set to 'yes' or '1' to clean up the /xcatpost directory on the diskfull nodes after the - postscripts are run with no errors. Default is no. + postscripts are run with no errors: at deployment, after each + updatenode run, and after a reboot. updateflag.awk stays, so + the node can still report its status. Default is no. db2installloc: The location which the service nodes should mount for the db2 code to install. Format is hostname:/path. If hostname is diff --git a/perl-xCAT/xCAT/Schema.pm b/perl-xCAT/xCAT/Schema.pm index 4fdafac6b..d3618bef1 100644 --- a/perl-xCAT/xCAT/Schema.pm +++ b/perl-xCAT/xCAT/Schema.pm @@ -1198,7 +1198,9 @@ passed as argument rather than by table value', " postscripts are run. Default is no.\n\n" . " cleanupdiskfullxcatpost: (yes/1 or no/0). Set to 'yes' or '1' to clean up the /xcatpost\n" . " directory on the diskfull nodes after the\n" . -" postscripts are run with no errors. Default is no.\n\n" . +" postscripts are run with no errors: at deployment, after each\n" . +" updatenode run, and after a reboot. updateflag.awk stays, so\n" . +" the node can still report its status. Default is no.\n\n" . " db2installloc: The location which the service nodes should mount for\n" . " the db2 code to install. Format is hostname:/path. If hostname is\n" . " omitted, it defaults to the management node. Default is /mntdb2.\n\n" . diff --git a/xCAT-test/bats/xcatdsklspost_xcatpost_cleanup.bats b/xCAT-test/bats/xcatdsklspost_xcatpost_cleanup.bats new file mode 100644 index 000000000..60c95ed51 --- /dev/null +++ b/xCAT-test/bats/xcatdsklspost_xcatpost_cleanup.bats @@ -0,0 +1,170 @@ +#!/usr/bin/env bats + +load 'helpers/shell_source' + +setup() +{ + DSKLSPOST="$(repo_path 'xCAT/postscripts/xcatdsklspost')" + [ -r "$DSKLSPOST" ] || skip "$DSKLSPOST is required" + XCATPOST="${BATS_TEST_TMPDIR}/xcatpost" + MYPS="${BATS_TEST_TMPDIR}/mypostscript" + MSGLOG="${BATS_TEST_TMPDIR}/msgutil.log" + export DSKLSPOST XCATPOST MYPS MSGLOG +} + +# A scratch /xcatpost with what an updatenode run downloads into it. +make_xcatpost() +{ + mkdir -p "$XCATPOST/_xcat" + printf 'updateflag\n' >"$XCATPOST/updateflag.awk" + printf 'setroute\n' >"$XCATPOST/setroute" + printf 'xcatlib\n' >"$XCATPOST/xcatlib.sh" + printf 'credentials\n' >"$XCATPOST/_xcat/postscript.cfg" +} + +# A scratch mypostscript that carries the site values and the run result. +# $1 is the value of return_value, the rest are the site table lines. +make_mypostscript() +{ + local result=$1 + shift + { + printf '%s\n' '#!/bin/bash' + printf 'msgutil_r() { printf "%%s\\n" "$3" >>"%s"; }\n' "$MSGLOG" + printf 'MASTER_IP=192.0.2.1\n' + printf 'NODE=node1\n' + printf 'log_label=xcat.updatenode\n' + printf 'return_value=%s\n' "$result" + printf '%s\n' "$@" + } >"$MYPS" +} + +append_cleanup() +{ + ( + XCATDSKLSPOST_SOURCE_ONLY=1 + export XCATDSKLSPOST_SOURCE_ONLY + # shellcheck disable=SC1090 + . "$DSKLSPOST" + type -t append_xcatpost_cleanup >/dev/null \ + || { echo "xcatdsklspost defines no append_xcatpost_cleanup" >&2; exit 99; } + append_xcatpost_cleanup "$MYPS" "$XCATPOST" + ) +} + +@test "an updatenode run that succeeds removes the postscripts when site.cleanupdiskfullxcatpost is set" { + make_xcatpost + make_mypostscript 0 "CLEANUPXCATPOST='no'" "CLEANUPDISKFULLXCATPOST='yes'" + append_cleanup + + run bash "$MYPS" + [ "$status" -eq 0 ] + [ ! -e "$XCATPOST/setroute" ] + [ ! -e "$XCATPOST/xcatlib.sh" ] + [ ! -e "$XCATPOST/_xcat" ] +} + +@test "the cleanup keeps updateflag.awk so the node can still report its status" { + make_xcatpost + make_mypostscript 0 "CLEANUPXCATPOST='no'" "CLEANUPDISKFULLXCATPOST='yes'" + append_cleanup + + run bash "$MYPS" + [ "$status" -eq 0 ] + [ -f "$XCATPOST/updateflag.awk" ] + [ -d "$XCATPOST" ] + grep -q 'cleanup of .* completed' "$MSGLOG" +} + +@test "an updatenode run that fails keeps the postscripts for diagnosis" { + make_xcatpost + make_mypostscript 1 "CLEANUPXCATPOST='no'" "CLEANUPDISKFULLXCATPOST='yes'" + append_cleanup + + run bash "$MYPS" + [ "$status" -eq 0 ] + [ -f "$XCATPOST/setroute" ] + [ -f "$XCATPOST/_xcat/postscript.cfg" ] + refute_grep -q 'cleanup of .* completed' "$MSGLOG" +} + +@test "site.cleanupdiskfullxcatpost accepts the other true values of the site table" { + make_xcatpost + make_mypostscript 0 "CLEANUPDISKFULLXCATPOST='1'" + append_cleanup + + run bash "$MYPS" + [ "$status" -eq 0 ] + [ ! -e "$XCATPOST/setroute" ] +} + +# cleanupdiskfullxcatpost names the node type it applies to. updatenode calls this script for a +# diskless or statelite node as well, and the site value is one row that reaches all of them, so +# without a guard a diskless node loses its postscripts to a setting that does not name it. +@test "a netboot node keeps its postscripts when site.cleanupdiskfullxcatpost is set" { + make_xcatpost + make_mypostscript 0 "NODESETSTATE='netboot'" "CLEANUPDISKFULLXCATPOST='yes'" + append_cleanup + + run bash "$MYPS" + [ "$status" -eq 0 ] + [ -f "$XCATPOST/setroute" ] + [ -f "$XCATPOST/_xcat/postscript.cfg" ] + # Nothing was appended, so the run cannot report a cleanup it did not do. + refute_grep -q 'cleanup of .* completed' "$MSGLOG" +} + +@test "a statelite node keeps its postscripts when site.cleanupdiskfullxcatpost is set" { + make_xcatpost + make_mypostscript 0 "NODESETSTATE='statelite'" "CLEANUPDISKFULLXCATPOST='yes'" + append_cleanup + + run bash "$MYPS" + [ "$status" -eq 0 ] + [ -f "$XCATPOST/setroute" ] +} + +@test "a diskful node is still cleaned when site.cleanupdiskfullxcatpost is set" { + make_xcatpost + make_mypostscript 0 "NODESETSTATE='install'" "CLEANUPDISKFULLXCATPOST='yes'" + append_cleanup + + run bash "$MYPS" + [ "$status" -eq 0 ] + [ ! -e "$XCATPOST/setroute" ] + [ -f "$XCATPOST/updateflag.awk" ] +} + +# site.cleanupxcatpost names no node type, so it keeps applying to every one. A guard added to the +# wrong branch would show up here. +@test "site.cleanupxcatpost still applies to a netboot node" { + make_xcatpost + make_mypostscript 0 "NODESETSTATE='netboot'" "CLEANUPXCATPOST='yes'" + append_cleanup + + run bash "$MYPS" + [ "$status" -eq 0 ] + [ -z "$(ls -A "$XCATPOST")" ] +} + +@test "site.cleanupxcatpost removes every file including updateflag.awk" { + make_xcatpost + make_mypostscript 0 "CLEANUPXCATPOST='yes'" "CLEANUPDISKFULLXCATPOST='no'" + append_cleanup + + run bash "$MYPS" + [ "$status" -eq 0 ] + [ -z "$(ls -A "$XCATPOST")" ] +} + +@test "neither setting leaves the downloaded postscripts in place" { + make_xcatpost + make_mypostscript 0 "CLEANUPXCATPOST='no'" "CLEANUPDISKFULLXCATPOST='no'" + append_cleanup + + run bash "$MYPS" + [ "$status" -eq 0 ] + [ -f "$XCATPOST/setroute" ] + [ -f "$XCATPOST/updateflag.awk" ] + refute_grep -q -E 'rm -rf|-delete' "$MYPS" +} diff --git a/xCAT-test/unit/postscript_heredoc_embedding.t b/xCAT-test/unit/postscript_heredoc_embedding.t new file mode 100644 index 000000000..32d6c17c6 --- /dev/null +++ b/xCAT-test/unit/postscript_heredoc_embedding.t @@ -0,0 +1,82 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use FindBin; +use File::Temp qw(tempdir); +use Test::More; + +# The install-time post script embeds whole postscripts with "#INCLUDE:#" inside a +# here-document. The template copies the file verbatim, so a line in the postscript that +# equals the here-document delimiter ends that here-document early. The generated install +# script then holds the rest of the postscript as shell code. +# +# That happened: a "cat >> ... </opt/xcat/xcatdsklspost << 'EOF'", and every diskfull +# install ran a post script that bash refused to parse. No postscript ran, so remoteshell +# never installed the root key and the node answered "Permission denied (publickey)". + +my $root = "$FindBin::Bin/../.."; +my $scriptdir = "$root/xCAT-server/share/xcat/install/scripts"; +die "$scriptdir not found\n" unless -d $scriptdir; + +# Return every (post script, delimiter, embedded file) the install scripts declare. +sub embeddings { + my @found; + opendir(my $dh, $scriptdir) or die "cannot read $scriptdir: $!"; + my @posts = sort grep { /^post\./ and -f "$scriptdir/$_" } readdir($dh); + closedir $dh; + + for my $post (@posts) { + open(my $fh, '<', "$scriptdir/$post") or die "cannot read $post: $!"; + my @lines = <$fh>; + close $fh; + for my $i (0 .. $#lines - 1) { + # cat >file << 'EOF' / (cat << 'EOF' + my ($delim) = $lines[$i] =~ /<<-?\s*'([A-Za-z_][A-Za-z0-9_]*)'\s*$/; + next unless defined $delim; + my ($include) = $lines[$i + 1] =~ /^#INCLUDE:(.+)#\s*$/; + next unless defined $include; + # The include path is a template expression; only its last element names the file. + next unless $include =~ m{/postscripts/([^/#]+)$}; + my $file = "$root/xCAT/postscripts/$1"; + next unless -f $file; + push @found, { post => $post, delim => $delim, file => $file, name => $1 }; + } + } + return @found; +} + +my @embeddings = embeddings(); +die 'no "#INCLUDE:" inside a here-document was found; the scan no longer matches the install scripts' + unless @embeddings; + +my $scratch = tempdir(CLEANUP => 1); + +for my $e (@embeddings) { + my $label = "$e->{post} embeds $e->{name} in a <<'$e->{delim}' here-document"; + + open(my $fh, '<', $e->{file}) or die "cannot read $e->{file}: $!"; + my @body = <$fh>; + close $fh; + + my @collisions = grep { $body[$_] =~ /^\Q$e->{delim}\E\s*$/ } 0 .. $#body; + is(scalar @collisions, 0, + "$label, and no line of $e->{name} is \"$e->{delim}\"") + or diag("$e->{name} line(s) " . join(', ', map { $_ + 1 } @collisions) + . " end the here-document early"); + + # Assemble the embedding the way the template does and let bash parse it. bash reads the + # here-document up to the first delimiter line, so an early end leaves the rest of the + # postscript as commands. -n parses and runs nothing. + my $script = "$scratch/$e->{post}.$e->{name}.sh"; + open(my $out, '>', $script) or die "cannot write $script: $!"; + print $out "cat >\"\$1\" <<'$e->{delim}'\n", @body, "$e->{delim}\n"; + close $out; + + my $errors = qx{bash -n \Q$script\E 2>&1}; + is($?, 0, "$label, and bash parses the result") + or diag($errors); +} + +done_testing(); diff --git a/xCAT/postscripts/xcatdsklspost b/xCAT/postscripts/xcatdsklspost index 6f7dcc63a..ffd209849 100755 --- a/xCAT/postscripts/xcatdsklspost +++ b/xCAT/postscripts/xcatdsklspost @@ -224,6 +224,60 @@ parsehttpserver () fi } +##################################################### +# Append to the generated mypostscript the /xcatpost cleanup the site table asks for. +# +# The append happens on every path this script serves: node deployment, and each +# updatenode, moncfg or reboot run. +# +# The two site attributes do not ask for the same thing. cleanupxcatpost removes +# every file and ignores the result of the run. cleanupdiskfullxcatpost keeps +# updateflag.awk, which the node needs to report its status, and removes nothing +# after a failed run, so the postscripts stay on the node for diagnosis. +# xcatinstallpost applies the same rule at install time. +# +# cleanupdiskfullxcatpost names the node type it applies to, and this script serves +# every type: updatenode calls it for a diskless or statelite node as well. The site +# value is one row and reaches all of them, so the diskful branch reads NODESETSTATE +# and does nothing for netboot or statelite. cleanupxcatpost carries no such word and +# keeps applying everywhere. +# +# $1 - path of the generated mypostscript +# $2 - the xcatpost directory to remove the postscripts from +##################################################### +append_xcatpost_cleanup() +{ + local mypostscript=$1 + local postdir=$2 + local cleanupxcatpost + local cleanupdiskfullxcatpost + + cleanupxcatpost=`grep CLEANUPXCATPOST= "$mypostscript" |awk -F = '{print $2}' | tr -d \'\" | tr A-Z a-z` + if [[ "$cleanupxcatpost" =~ ^(1|yes|y)$ ]]; then + echo "cd /" >> "$mypostscript" + # /xcatpost might be read-only for statelite nodes + echo "rm -rf $postdir/*" >> "$mypostscript" + fi + + local nodesetstate + nodesetstate=`grep '^NODESETSTATE=' "$mypostscript" |awk -F = '{print $2}' | tr -d \'\" | tr A-Z a-z` + cleanupdiskfullxcatpost=`grep CLEANUPDISKFULLXCATPOST= "$mypostscript" |awk -F = '{print $2}' | tr -d \'\" | tr A-Z a-z` + if [[ "$cleanupdiskfullxcatpost" =~ ^(1|yes|y)$ ]] \ + && [ "$nodesetstate" != "netboot" ] && [ "$nodesetstate" != "statelite" ]; then + # post.xcat and the other install scripts embed this whole file in a here-document + # that ends at a line "EOF". A line "EOF" here ends that here-document early and + # leaves the rest of this file as shell code in the generated install script. + cat >> "$mypostscript" </dev/null || exit 0 fi @@ -1086,12 +1140,7 @@ fi DHCP_TMP=`sed 's/\(DHCPINTERFACES=\)\(.*\)$/\1"\2"/' /$xcatpost/mypostscript` echo "$DHCP_TMP" > /$xcatpost/mypostscript -CLEANUPXCATPOST=`grep CLEANUPXCATPOST= /$xcatpost/mypostscript |awk -F = '{print $2}' | tr -d \'\" | tr A-Z a-z` -if [[ "$CLEANUPXCATPOST" =~ ^(1|yes|y)$ ]]; then - echo "cd /" >> /$xcatpost/mypostscript - # /xcatpost might be read-only for statelite nodes - echo "rm -rf /$xcatpost/*" >> /$xcatpost/mypostscript -fi +append_xcatpost_cleanup /$xcatpost/mypostscript /$xcatpost