diff --git a/.github/workflows/xcat_test.yml b/.github/workflows/xcat_test.yml index e24819ed6..0c9033826 100644 --- a/.github/workflows/xcat_test.yml +++ b/.github/workflows/xcat_test.yml @@ -6,7 +6,11 @@ jobs: timeout-minutes: 60 steps: - uses: actions/checkout@v6 + - name: Refresh package indexes + run: sudo apt-get update - name: Install dependencies - run: sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends --no-install-suggests bats rpm build-essential fakeroot reprepro devscripts debhelper libcapture-tiny-perl libfile-slurper-perl libjson-perl libparallel-forkmanager-perl libsoap-lite-perl libdbi-perl libcgi-pm-perl quilt openssh-server dpkg looptools genometools software-properties-common + run: sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends --no-install-suggests bats bubblewrap rpm build-essential fakeroot reprepro devscripts debhelper libcapture-tiny-perl libfile-slurper-perl libjson-perl libparallel-forkmanager-perl libsoap-lite-perl libdbi-perl libcgi-pm-perl quilt openssh-server dpkg looptools genometools software-properties-common + - name: Enable postscript test namespaces + run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 - name: Run tests run: perl github_action_xcat_test.pl diff --git a/docs/source/conf.py b/docs/source/conf.py index 57880777a..8449c33f2 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -60,7 +60,7 @@ author = u'IBM Corporation, xCAT Consortium' # The short X.Y version. version = '2' # The full version, including alpha/beta/rc tags. -release = '2.17.0' +release = '2.20.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/source/developers/releases/checklist.rst b/docs/source/developers/releases/checklist.rst index db884ad2d..6809f492b 100644 --- a/docs/source/developers/releases/checklist.rst +++ b/docs/source/developers/releases/checklist.rst @@ -106,8 +106,10 @@ The release is the last candidate that passed the tests. xcat-dep/2.x_Linux/xcat-dep-X.Y.Z-linux.tar.bz2 xcat-dep/2.x_Ubuntu/xcat-dep-X.Y.Z-ubuntu.tar.bz2 -#. Make sure that the ``.repo`` files in the published repositories point at the published - location, not at ``devel``. +#. Make sure that each ``.repo`` file under ``repos/yum/X.Y/`` points at ``repos/yum/X.Y/``, not at + ``devel`` or ``latest``. ``latest`` is a link into the newest series, so a series path is right + through both. A file that names ``latest`` gives the next series to users of this one as soon as + that series ships. #. If X.Y is the newest series, point ``latest`` at ``X.Y`` for yum and for apt. @@ -146,9 +148,9 @@ Verify the Published Packages ``go-xcat`` installation does not prove the signatures. On EL, use the three published ``.repo`` files, which set ``gpgcheck=1``. Enable the other - repositories that the installation guide requires first. The published files point at - ``latest``. For an older series, replace ``latest`` with ``X.Y`` in the three files. Install the - Genesis image for the host architecture by name, because ``xCAT`` only recommends it. :: + repositories that the installation guide requires first. Use the files as published: each one + must point at ``repos/yum/X.Y/``. Install the Genesis image for the host architecture by name, + because ``xCAT`` only recommends it. :: $ curl -fsSo /etc/yum.repos.d/xcat-core.repo \ https://xcat.org/files/xcat/repos/yum/X.Y/xcat-core/xcat-core.repo diff --git a/docs/source/overview/_files/2.18.x.csv b/docs/source/overview/_files/2.18.x.csv index fd704eb94..03e944485 100644 --- a/docs/source/overview/_files/2.18.x.csv +++ b/docs/source/overview/_files/2.18.x.csv @@ -1,2 +1,3 @@ Version,Release Date,New OS Supported,Release Notes +2.18.2,2026-08-01,,`2.18.2 Release Notes `_ 2.18.0,2026-06-22,"RHEL 10,AlmaLinux 10",`2.18.0 Release Notes `_ diff --git a/docs/source/overview/_files/2.19.x.csv b/docs/source/overview/_files/2.19.x.csv new file mode 100644 index 000000000..71ce99390 --- /dev/null +++ b/docs/source/overview/_files/2.19.x.csv @@ -0,0 +1,3 @@ +Version,Release Date,New OS Supported,Release Notes +2.19.1,2026-09-25,,`2.19.1 Release Notes `_ +2.19.0,2026-09-18,"Ubuntu 26.04, riscv64 on EL10, Ubuntu 24.04 and Ubuntu 26.04",`2.19.0 Release Notes `_ diff --git a/docs/source/overview/xcat2_release.rst b/docs/source/overview/xcat2_release.rst index 07188d3a3..da1e5d468 100644 --- a/docs/source/overview/xcat2_release.rst +++ b/docs/source/overview/xcat2_release.rst @@ -6,6 +6,15 @@ The following tables documents the xCAT release versions and release dates. For .. tabularcolumns:: |p{1cm}|p{4cm}|p{7cm}|p{7cm} +xCAT 2.19.x +----------- + +.. csv-table:: 2.19.x Release Information + :file: _files/2.19.x.csv + :header-rows: 1 + :class: longtable + :widths: 1 1 1 1 + xCAT 2.18.x ----------- diff --git a/github_action_xcat_test.pl b/github_action_xcat_test.pl index 34fc6b675..93a3da0f9 100644 --- a/github_action_xcat_test.pl +++ b/github_action_xcat_test.pl @@ -360,8 +360,8 @@ sub install_xcat{ my @cmds = ("sudo $repo/mklocalrepo.sh", "sudo chmod 777 /etc/apt/sources.list", - "sudo echo \"deb [arch=amd64 allow-insecure=yes] http://xcat.org/files/xcat/repos/apt/latest/xcat-dep noble main\" >> /etc/apt/sources.list", - "sudo echo \"deb [arch=ppc64el allow-insecure=yes] http://xcat.org/files/xcat/repos/apt/latest/xcat-dep noble main\" >> /etc/apt/sources.list", + "sudo echo \"deb [arch=amd64 allow-insecure=yes] http://xcat.org/files/xcat/repos/apt/devel/xcat-dep noble main\" >> /etc/apt/sources.list", + "sudo echo \"deb [arch=ppc64el allow-insecure=yes] http://xcat.org/files/xcat/repos/apt/devel/xcat-dep noble main\" >> /etc/apt/sources.list", "sudo timeout 600 apt-get -qq -o Acquire::Retries=3 -o Acquire::http::Timeout=30 --allow-insecure-repositories update"); chdir $ENV{RUNNER_WORKSPACE};; diff --git a/xCAT-server/lib/xcat/plugins/mknb.pm b/xCAT-server/lib/xcat/plugins/mknb.pm index 01ddc5b95..baaac6e97 100644 --- a/xCAT-server/lib/xcat/plugins/mknb.pm +++ b/xCAT-server/lib/xcat/plugins/mknb.pm @@ -828,7 +828,7 @@ sub process_request { open($cfg, ">", "$tftpdir/xcat/xnba/nets/$net"); print $cfg "#!gpxe\n"; if ($invisibletouch) { - print $cfg 'imgfetch -n kernel http://${next-server}'.$portsuffix.'/tftpboot/xcat/genesis.kernel.' . "$arch xcatd=" . $xcatd_address . ":$xcatdport $consolecmdline BOOTIF=01-" . '${netX/machyp}' . "\n"; + print $cfg 'imgfetch -n kernel http://${next-server}'.$portsuffix.'/tftpboot/xcat/genesis.kernel.' . "$arch xcatd=" . $xcatd_address . ":$xcatdport $consolecmdline BOOTIF=01-" . '${netX/mac:hexhyp}' . "\n"; print $cfg 'imgfetch -n nbfs http://${next-server}'.$portsuffix . "$initrd_file\n"; } else { print $cfg 'imgfetch -n kernel http://${next-server}'.$portsuffix.'/tftpboot/xcat/nbk.' . "$arch xcatd=" . $xcatd_address . ":$xcatdport $consolecmdline\n"; diff --git a/xCAT-test/bats/README.md b/xCAT-test/bats/README.md index ac65379ab..2ed29552e 100644 --- a/xCAT-test/bats/README.md +++ b/xCAT-test/bats/README.md @@ -19,3 +19,14 @@ commands so tests cannot write to the host. Extraction helpers in `helpers/shell_source.bash` are only for legacy code that cannot safely be sourced yet. Do not add Perl `.t` tests that grep shell source when the behavior can be tested with BATS. + +## Postscript sandbox prerequisites + +`otherpkgs_upgrade_scope.bats` runs the complete postscript in a Linux filesystem +sandbox because it writes to `/etc/yum.repos.d`. It requires Bubblewrap, Bash, +GNU core utilities, and permission to create user namespaces. The CI workflow +installs Bubblewrap and enables those namespaces. + +Missing prerequisites fail this test without stopping unrelated test files. +Non-Linux hosts report a skip. Set `TMPDIR` to a writable, executable filesystem +if the default temporary directory is mounted with `noexec`. diff --git a/xCAT-test/bats/otherpkgs_upgrade_scope.bats b/xCAT-test/bats/otherpkgs_upgrade_scope.bats new file mode 100644 index 000000000..d924f932b --- /dev/null +++ b/xCAT-test/bats/otherpkgs_upgrade_scope.bats @@ -0,0 +1,239 @@ +#!/usr/bin/env bats + +load 'helpers/shell_source' + +setup() +{ + [ "$(uname -s)" = Linux ] || skip 'otherpkgs filesystem isolation requires Linux' + local utility executable + local utilities=(bash sh basename dirname cat cp expr grep ls mkdir rm uname wc) + local bwrap + bwrap=$(PATH=/usr/bin:/bin type -P bwrap) || { + echo 'Install bubblewrap to run the otherpkgs test' >&2 + return 1 + } + postscripts=$(repo_path xCAT/postscripts) + fixture="$BATS_TEST_TMPDIR/fixture" + mkdir -p "$fixture/bin" + sandbox=(env -i PATH=/usr/bin:/bin LC_ALL=C "$bwrap" + --unshare-all --die-with-parent --new-session + --ro-bind / / --tmpfs /etc --tmpfs /usr/bin --tmpfs /tmp + --proc /proc --dev /dev --setenv PATH /usr/bin --setenv LC_ALL C) + if PATH=/usr/bin:/bin type -P coreutils >/dev/null; then + utilities+=(coreutils) + fi + for utility in "${utilities[@]}"; do + executable=$(PATH=/usr/bin:/bin type -P "$utility") || { + echo "Required utility is unavailable: $utility" >&2 + return 1 + } + sandbox+=(--ro-bind "$executable" "/usr/bin/$utility") + done + run "${sandbox[@]}" /usr/bin/sh -c 'test ! -e /etc/os-release' + if [ "$status" -ne 0 ]; then + echo "Cannot isolate otherpkgs: $output" >&2 + return 1 + fi +} + +run_case() +{ + local manager=$1 scenario=$2 + local verbose='' remote='' mounted='' repoonly='' multiple='' empty='' + local upgrade_status=0 install_status=0 + case "$scenario" in + http) verbose=1; remote=1 ;; + mounted) mounted=1 ;; + upgrade_failure) upgrade_status=17; verbose=1 ;; + install_failure) install_status=23; verbose=1 ;; + repoonly) repoonly=1; remote=1 ;; + multiple) multiple=1; verbose=1 ;; + empty) remote=1; empty=1 ;; + esac + printf '#!/usr/bin/sh\nexit 0\n' >"$fixture/bin/logger" + printf '#!/usr/bin/sh\nexit 1\n' >"$fixture/bin/dpkg" + printf '#!/usr/bin/sh\n[ "$*" = --version ]\n' >"$fixture/bin/rpm" + if [ "$mounted" ]; then + printf '#!/usr/bin/sh\nprintf "%%s\\n" "package-server:/install on /install type nfs (rw)"\n' >"$fixture/bin/mount" + else + printf '#!/usr/bin/sh\nexit 0\n' >"$fixture/bin/mount" + fi + cat >"$fixture/bin/$manager" <<'SH' +#!/usr/bin/sh +printf '%s\t' "${0##*/}" "SCOPE_ENV=${SCOPE_ENV:-}" "$@" >> /tmp/fixture/commands +printf '\n' >> /tmp/fixture/commands +sequence=$(wc -l < /tmp/fixture/commands) +mkdir "/tmp/fixture/repos.$sequence" +cp /etc/yum.repos.d/*.repo "/tmp/fixture/repos.$sequence/" 2>/dev/null || : +for argument do + case "$argument" in + upgrade) printf '%s\n' upgrade-result; exit "$UPGRADE_STATUS" ;; + install) printf '%s\n' install-result; exit "$INSTALL_STATUS" ;; + esac +done +exit 0 +SH + chmod +x "$fixture/bin/"* + + local otherpkgdir=/install/other packages=alpha/tool-one,beta/tool-two + local list_count=1 + [ ! "$remote" ] || otherpkgdir=https://packages.example.invalid/extra,/install/other + [ ! "$empty" ] || packages= + [ ! "$multiple" ] || list_count=2 + local command=("${sandbox[@]}" + --bind "$fixture" /tmp/fixture + --ro-bind "$postscripts" /tmp/postscripts --chdir /tmp/fixture) + local tool + for tool in logger dpkg rpm mount "$manager"; do + command+=(--ro-bind "$fixture/bin/$tool" "/usr/bin/$tool") + done + command+=( + --setenv OSVER rhel9 --setenv ARCH x86_64 --setenv UPDATENODE 1 + --setenv NFSSERVER package-server --setenv HTTPPORT 80 + --setenv INSTALLDIR /install --setenv OTHERPKGDIR "$otherpkgdir" + --setenv OTHERPKGS_INDEX "$list_count" --setenv OTHERPKGS1 "$packages" + --setenv ENVLIST1 SCOPE_ENV=first --setenv VERBOSE "$verbose" + --setenv UPGRADE_STATUS "$upgrade_status" --setenv INSTALL_STATUS "$install_status") + if [ "$multiple" ]; then + command+=(--setenv OTHERPKGS2 gamma/tool-three --setenv ENVLIST2 SCOPE_ENV=second) + fi + local runner + runner=$(cat <<'SH' +/usr/bin/bash /tmp/postscripts/otherpkgs "$@" +status=$? +mkdir /tmp/fixture/final-repos +cp /etc/yum.repos.d/*.repo /tmp/fixture/final-repos/ 2>/dev/null || : +exit "$status" +SH + ) + command+=(/usr/bin/sh -c "$runner" otherpkgs-test) + [ ! "$repoonly" ] || command+=(--repoonly) + + run "${command[@]}" + if [ "$status" -ne "$((upgrade_status + install_status))" ]; then + echo "$output" >&2 + return 1 + fi + [ -f "$fixture/commands" ] + + local line operand sequence=0 + local arguments=() paths=() + : >"$fixture/transactions" + while IFS= read -r line; do + sequence=$((sequence + 1)) + IFS=$'\t' read -r -a arguments <<<"$line" + operand= + for operand in "${arguments[@]:2}"; do + [[ "$operand" = -* ]] || break + done + case "$operand" in clean|list) continue ;; esac + printf '%s\n' "$line" >>"$fixture/transactions" + paths=(alpha beta) + [ ! "$empty" ] || paths=() + if [ "$multiple" ] && [ "${arguments[1]}" = SCOPE_ENV=second ]; then + paths=(gamma) + fi + check_repositories "$fixture/repos.$sequence" "${paths[@]}" + done <"$fixture/commands" + + : >"$fixture/expected-transactions" + : >"$fixture/expected-printed" + if [ ! "$repoonly" ]; then + expect_transaction first -y '--disablerepo=*' '--enablerepo=xcat-otherpkgs*' upgrade + if [ ! "$empty" ]; then + expect_transaction first -y install tool-one tool-two + fi + if [ "$multiple" ]; then + expect_transaction second -y '--disablerepo=*' '--enablerepo=xcat-otherpkgs*' upgrade + expect_transaction second -y install tool-three + fi + fi + diff -u "$fixture/expected-transactions" "$fixture/transactions" + + : >"$fixture/printed" + while IFS= read -r line; do + [[ "$line" = SCOPE_ENV=* ]] || continue + read -r -a arguments <<<"$line" + printf '%s\t' "${arguments[@]}" >>"$fixture/printed" + printf '\n' >>"$fixture/printed" + done <<<"$output" + diff -u "$fixture/expected-printed" "$fixture/printed" + + paths=(alpha beta) + [ ! "$empty" ] || paths=() + [ ! "$multiple" ] || paths=(gamma) + check_repositories "$fixture/final-repos" "${paths[@]}" + if [ "$verbose" ] && [ ! "$repoonly" ]; then + [[ $'\n'"$output"$'\n' = *$'\nupgrade-result\n'* ]] + if [ ! "$empty" ]; then + [[ $'\n'"$output"$'\n' = *$'\ninstall-result\n'* ]] + fi + fi +} + +expect_transaction() +{ + local label=$1 + shift + printf '%s\t' "$manager" "SCOPE_ENV=$label" "$@" >>"$fixture/expected-transactions" + printf '\n' >>"$fixture/expected-transactions" + if [ "$verbose" ]; then + printf '%s\t' "SCOPE_ENV=$label" "$manager" "$@" >>"$fixture/expected-printed" + printf '\n' >>"$fixture/expected-printed" + fi +} + +check_repositories() +{ + local directory=$1 + shift + local base=http://package-server:80 index=0 pkgpath file + [ ! "$mounted" ] || base=file:// + { + printf '%s\t%s\t1\t0\n' xCAT-rhel9-path0 "$base/install/rhel9/x86_64/BaseOS" + printf '%s\t%s\t1\t0\n' xCAT-rhel9-path1 "$base/install/rhel9/x86_64/AppStream" + if [ "$remote" ]; then + printf '%s\t%s\t1\t0\n' xcat-otherpkgs0 https://packages.example.invalid/extra + index=1 + fi + for pkgpath do + printf '%s\t%s\t1\t0\n' "xcat-otherpkgs$index" "$base/install/other/$pkgpath" + index=$((index + 1)) + done + } | LC_ALL=C sort >"$fixture/expected-repos" + : >"$fixture/repos" + for file in "$directory/"*.repo; do + [ -f "$file" ] || continue + awk ' + /^\[[^]]+\]$/ { sections = sections substr($0, 2, length($0) - 2) "\t" } + /^(baseurl|enabled|gpgcheck)=/ { + key = substr($0, 1, index($0, "=") - 1) + value = substr($0, index($0, "=") + 1) + sub(/[[:space:]]+$/, "", value) + values[key] = values[key] value "\t" + } + END { + record = sections values["baseurl"] values["enabled"] values["gpgcheck"] + sub(/\t$/, "", record) + print record + } + ' "$file" >>"$fixture/repos" + done + LC_ALL=C sort "$fixture/repos" >"$fixture/sorted-repos" + diff -u "$fixture/expected-repos" "$fixture/sorted-repos" +} + +@test "dnf: HTTP and local repositories" { run_case dnf http; } +@test "dnf: mounted repositories" { run_case dnf mounted; } +@test "dnf: upgrade failure" { run_case dnf upgrade_failure; } +@test "dnf: install failure" { run_case dnf install_failure; } +@test "dnf: repository-only mode" { run_case dnf repoonly; } +@test "dnf: separate package lists" { run_case dnf multiple; } +@test "dnf: remote repository without installs" { run_case dnf empty; } +@test "yum: HTTP and local repositories" { run_case yum http; } +@test "yum: mounted repositories" { run_case yum mounted; } +@test "yum: upgrade failure" { run_case yum upgrade_failure; } +@test "yum: install failure" { run_case yum install_failure; } +@test "yum: repository-only mode" { run_case yum repoonly; } +@test "yum: separate package lists" { run_case yum multiple; } +@test "yum: remote repository without installs" { run_case yum empty; } diff --git a/xCAT-test/unit/mknb_exported_genesis.t b/xCAT-test/unit/mknb_exported_genesis.t index 5f523a9bf..3fd26df00 100644 --- a/xCAT-test/unit/mknb_exported_genesis.t +++ b/xCAT-test/unit/mknb_exported_genesis.t @@ -553,4 +553,32 @@ my $unsafe = xCAT_plugin::mknb::_select_genesis_source( ); is($unsafe, undef, 'unsupported architecture names cannot escape the image root'); +$::XCATROOT = "$tmpdir/openembedded-x86_64-xcatroot"; +prepare_export( + "$::XCATROOT/share/xcat/netboot/genesis-openembedded/x86_64", + 'openembedded x86_64 kernel', + 'openembedded x86_64 initramfs', + 'x86_64', +); +$xCAT::TableUtils::tftpdir = "$tmpdir/openembedded-x86_64-tftpboot"; +@responses = (); +xCAT_plugin::mknb::process_request( + { arg => ['x86_64'] }, + sub { push(@responses, @_); }, +); +ok( + !grep({ ref($_) eq 'HASH' && $_->{error} } @responses), + 'mknb installs an x86_64 OpenEmbedded export', +); +is( + read_file("$xCAT::TableUtils::tftpdir/xcat/genesis.kernel.x86_64"), + 'openembedded x86_64 kernel', + 'mknb publishes the OpenEmbedded x86_64 kernel', +); +like( + read_file("$xCAT::TableUtils::tftpdir/xcat/xnba/nets/192.0.2.0_24"), + qr{^imgfetch -n kernel \S+/xcat/genesis\.kernel\.x86_64 .* BOOTIF=01-\$\{netX/mac:hexhyp\}$}m, + 'the OpenEmbedded BIOS Genesis script takes BOOTIF from mac:hexhyp', +); + done_testing(); diff --git a/xCAT-test/unit/mknb_xcatd_address.t b/xCAT-test/unit/mknb_xcatd_address.t index 0a69dd2e0..281a1af8a 100644 --- a/xCAT-test/unit/mknb_xcatd_address.t +++ b/xCAT-test/unit/mknb_xcatd_address.t @@ -232,6 +232,20 @@ foreach my $relative_path ( ); } +my $bios_genesis = read_config( + "$xCAT::TableUtils::tftpdir/xcat/xnba/nets/192.168.144.0_20" +); +like( + $bios_genesis, + qr{^imgfetch -n kernel \S+/xcat/genesis\.kernel\.x86_64 .* BOOTIF=01-\$\{netX/mac:hexhyp\}$}m, + 'the BIOS Genesis script takes BOOTIF from mac:hexhyp', +); +unlike( + $bios_genesis, + qr/machyp/, + 'the BIOS Genesis script does not use the xNBA-only machyp setting', +); + use_reporter_address_maps(); prepare_tftpdir($tmpdir, 'tftpboot-x86-legacy', 'x86_64', 'legacy'); $responses = run_mknb('x86_64'); diff --git a/xCAT-test/unit/otherpkgs_upgrade_scope.t b/xCAT-test/unit/otherpkgs_upgrade_scope.t deleted file mode 100644 index c34db50f5..000000000 --- a/xCAT-test/unit/otherpkgs_upgrade_scope.t +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env perl -use strict; -use warnings; - -use FindBin; -use File::Spec; -use Test::More; - -my $repo_root = File::Spec->catdir( $FindBin::Bin, '..', '..' ); -my $script_path = File::Spec->catfile( $repo_root, 'xCAT/postscripts/otherpkgs' ); - -plan skip_all => "$script_path not found" unless -r $script_path; - -open( my $fh, '<', $script_path ) or die "Unable to read $script_path: $!"; -my $script = do { local $/; <$fh> }; -close($fh); - -# The postscript writes its repositories as [xcat-otherpkgs]. The upgrade -# below is scoped with --enablerepo=xcat-otherpkgs*, so the two have to agree or -# the upgrade silently matches no repository at all. -like( - $script, - qr/echo\s+"\[xcat-otherpkgs\$urlrepoindex\]"/, - 'remote repositories are still defined as xcat-otherpkgs' -); -like( - $script, - qr/echo\s+"\[xcat-otherpkgs\$localrepoindex\]"/, - 'local repositories are still defined as xcat-otherpkgs' -); - -# Both the verbose echo and the command actually executed must carry the same -# scoping, otherwise verbose output reports a command that was never run. -my @scoped = $script =~ /\$yumcmd\s+-y\s+--disablerepo=\*\s+--enablerepo=xcat-otherpkgs\*\s+upgrade/g; -is( - scalar(@scoped), - 2, - 'the yum/dnf upgrade is scoped to the xcat-otherpkgs repositories in both the verbose echo and the executed command' -); - -# Counted rather than matched with unlike(), so that a failure reports the count -# instead of dumping the whole postscript into the test output. -my @unscoped = $script =~ /(\$yumcmd\s+-y\s+upgrade)/g; -is( - scalar(@unscoped), - 0, - 'no unscoped yum/dnf upgrade remains, which would also apply unrelated distribution updates' -); - -# The install path must keep every repository enabled so that dependencies of -# the otherpkgs packages can still be resolved from the distribution. -like( - $script, - qr/\$yumcmd\s+-y\s+install\s+\$repo_pkgs/, - 'the package install path is left unscoped so dependencies still resolve' -); - -done_testing(); diff --git a/xCAT-test/unit/syncfiles_xdcp_identity.t b/xCAT-test/unit/syncfiles_xdcp_identity.t index a31de3ca8..e281e88dd 100644 --- a/xCAT-test/unit/syncfiles_xdcp_identity.t +++ b/xCAT-test/unit/syncfiles_xdcp_identity.t @@ -3,62 +3,227 @@ use strict; use warnings; use FindBin; -use File::Spec; +use lib "$FindBin::Bin/../lib"; +use Storable qw(dclone); use Test::More; +use XCAT::Test::File qw(repo_path); -my $repo_root = File::Spec->catdir( $FindBin::Bin, '..', '..' ); +our $RCP; -sub slurp { - my ($rel) = @_; - my $path = File::Spec->catfile( $repo_root, $rel ); - return unless -r $path; - open( my $fh, '<', $path ) or die "Unable to read $path: $!"; - my $c = do { local $/; <$fh> }; - close($fh); - return $c; +BEGIN { + package xCAT::Utils; + $INC{'xCAT/Utils.pm'} = __FILE__; + + package xCAT::Postage; + $INC{'xCAT/Postage.pm'} = __FILE__; + + package xCAT::SvrUtils; + our $synclist; + sub getsynclistfile { + my ( $class, $nodes ) = @_; + return unless defined $synclist; + return { map { $_ => $synclist->{$_} } @$nodes }; + } + $INC{'xCAT/SvrUtils.pm'} = __FILE__; + + package xCAT::MsgUtils; + our @messages; + sub message { + my ( $class, @message ) = @_; + push @messages, \@message; + return; + } + $INC{'xCAT/MsgUtils.pm'} = __FILE__; + + package xCAT::NodeRange; + use Exporter qw(import); + our @EXPORT = qw(noderange); + our %nodes; + sub noderange { return $nodes{ $_[0] }; } + $INC{'xCAT/NodeRange.pm'} = __FILE__; } -my $syncfiles = slurp('xCAT-server/lib/xcat/plugins/syncfiles.pm'); -my $updatenode = slurp('xCAT-server/lib/xcat/plugins/updatenode.pm'); -my $xdsh = slurp('xCAT-server/lib/xcat/plugins/xdsh.pm'); +my $plugin = repo_path('xCAT-server/lib/xcat/plugins/syncfiles.pm'); +require $plugin; -plan skip_all => 'plugins not found' - unless defined($syncfiles) && defined($updatenode) && defined($xdsh); +sub run_syncfiles { + my ($case) = @_; + local %xCAT::NodeRange::nodes = ( + 'node1.example.test' => 'node1', + 'node2.example.test' => 'node2', + ); + local $xCAT::SvrUtils::synclist = $case->{synclist}; + local @xCAT::MsgUtils::messages; + local @ARGV; + my @sent; + my $callback = sub { return; }; + my $request = dclone($case->{request} || { + command => ['syncfiles'], + username => ['operator'], + arg => $case->{args}, + _xcat_clienthost => [ $case->{client} ], + }); + my $original = dclone($request); + my @warnings; -# The xdcp subrequest has to state the identity the sync runs as. -my ($call) = $syncfiles =~ /(\$subreq->\(\{[^}]*command\s*=>\s*\['xdcp'\][^}]*\})/s; -ok( $call, 'the xdcp subrequest was located in syncfiles' ) - or BAIL_OUT('syncfiles.pm no longer matches the expected subrequest shape'); + { + local $SIG{__WARN__} = sub { push @warnings, @_ } if $case->{warnings}; + xCAT_plugin::syncfiles::process_request( + $request, + $callback, + sub { + my ( $outgoing, $response_callback ) = @_; + push @sent, [ dclone($outgoing), $response_callback ]; + return; + }, + ); + } -like( $call, qr/username\s*=>/, 'the xdcp subrequest names a username' ); + is_deeply( \@warnings, $case->{warnings}, 'the option diagnostics match' ) if $case->{warnings}; + is_deeply( $request, $original, 'the caller request is unchanged' ); + is( scalar @sent, scalar @{ $case->{expected} }, 'the request count matches' ); + for my $index ( 0 .. $#{ $case->{expected} } ) { + my ( $node, $file, $copy_args ) = @{ $case->{expected}->[$index] }; + my $sent = $sent[$index] || []; + is_deeply( + $sent->[0], + { + command => ['xdcp'], + username => ['root'], + node => [$node], + arg => [ '-F', $file, @$copy_args ], + env => ["DSH_RSYNC_FILE=$file"], + }, + "request $index carries the root identity and copy parameters", + ); + is( $sent->[1], $callback, "request $index retains the response callback" ); + } + is( scalar @xCAT::MsgUtils::messages, scalar @{ $case->{messages} }, 'the diagnostic count matches' ); + for my $index ( 0 .. $#{ $case->{messages} } ) { + my $message = $xCAT::MsgUtils::messages[$index] || []; + is( $message->[0], 'S', 'the diagnostic goes to the system log' ); + like( $message->[1], $case->{messages}->[$index], 'the diagnostic identifies the failure' ); + } + return; +} -# It must be an array reference. A bare string was the original form of this -# change and broke the non-hierarchical path, because the consumers index it as -# $request->{username}->[0]. -like( - $call, - qr/username\s*=>\s*\['root'\]/, - 'the username is the arrayref form the consumers index into' -); -unlike( - $call, - qr/username\s*=>\s*'root'\s*,/, - 'the username is not a bare string, which would not survive ->[0]' +my @cases = ( + { + name => 'daemon request without arguments or username', + request => { command => ['syncfiles'], _xcat_clienthost => ['node1.example.test'] }, + synclist => { node1 => '/install/custom/sync-a' }, + expected => [ [ 'node1', '/install/custom/sync-a', [] ] ], + messages => [], + }, + { + name => 'multiple sync files retain order and identity', + client => 'node2.example.test', + args => [], + synclist => { node2 => '/install/custom/sync-b,/install/custom/sync-a,/install/custom/sync-c' }, + expected => [ + [ 'node2', '/install/custom/sync-b', [] ], + [ 'node2', '/install/custom/sync-a', [] ], + [ 'node2', '/install/custom/sync-c', [] ], + ], + messages => [], + }, ); -# The consumers this has to satisfy, pinned so the shape cannot drift apart. -like( - $xdsh, - qr/\$ENV\{DSH_FROM_USERID\}\s*=\s*\$request->\{username\}->\[0\]/, - 'xdsh still derives DSH_FROM_USERID from the request username' -); +for my $option ( '-r', '-c', '--node-rcp' ) { + push @cases, { + name => "copy override $option retains identity on every request", + client => 'node1.example.test', + args => [ $option, '/usr/bin/scp' ], + synclist => { node1 => '/install/custom/sync-a,/install/custom/sync-b' }, + expected => [ + [ 'node1', '/install/custom/sync-a', [ '-r', '/usr/bin/scp' ] ], + [ 'node1', '/install/custom/sync-b', [ '-r', '/usr/bin/scp' ] ], + ], + messages => [], + }; +} -# updatenode makes the same xdcp call and already passes a username. The two -# should not diverge again. -like( - $updatenode, - qr/command\s*=>\s*\["xdcp"\][^;]*username\s*=>/s, - 'updatenode still passes a username on its own xdcp call' -); +push @cases, + { + name => 'one sync file', + client => 'node1.example.test', + args => [], + synclist => { node1 => '/install/custom/sync-a' }, + expected => [ [ 'node1', '/install/custom/sync-a', [] ] ], + messages => [], + }, + { + name => 'invalid option sends no request', + client => 'node1.example.test', + args => ['--bogus'], + synclist => { node1 => '/install/custom/sync-a' }, + expected => [], + messages => [ qr/Received syncfiles from node1\.example\.test, with invalid options\b/ ], + warnings => ["Unknown option: bogus\n"], + }, + { + name => 'unavailable synclist lookup sends no request', + client => 'node1.example.test', + args => [], + synclist => undef, + expected => [], + messages => [ qr/\ACannot find synclist file for the node1\z/ ], + }, + { + name => 'node without a synclist sends no request', + client => 'node1.example.test', + args => [], + synclist => { node1 => undef }, + expected => [], + messages => [], + }, + { + name => 'unresolved client sends no request', + client => 'unknown.example.test', + args => [], + synclist => { node1 => '/install/custom/sync-a' }, + expected => [], + messages => [ qr/couldn't be correlated to a node/ ], + }; + +for my $case (@cases) { + subtest $case->{name} => sub { + local $RCP; + run_syncfiles($case); + }; +} + +# Direct same-process calls share the existing override; daemon process isolation is outside this test. +for my $option ( '-r', '-c', '--node-rcp' ) { + subtest "same-process copy override $option" => sub { + local $RCP; + for my $step ( + [ 'clean sequence state', [], 'node1', 'sync-default', undef ], + [ 'initial override', [ $option, '/usr/bin/scp' ], 'node1', 'sync-a', '/usr/bin/scp' ], + [ 'rejected override', [ $option, '/usr/bin/false', '--bogus' ], 'node1', 'sync-rejected', undef, 1 ], + [ 'request without override', [], 'node2', 'sync-b', '/usr/bin/scp' ], + [ 'replacement override', [ $option, '/usr/bin/rsync' ], 'node1', 'sync-c', '/usr/bin/rsync' ], + [ 'request after replacement', [], 'node2', 'sync-d', '/usr/bin/rsync' ], + ) + { + my ( $name, $args, $node, $list, $copy_command, $invalid ) = @$step; + subtest $name => sub { + my $file = "/install/custom/$list"; + my $copy_args = defined $copy_command ? [ '-r', $copy_command ] : []; + my %diagnostics = $invalid ? ( + messages => [ qr/Received syncfiles from \Q$node.example.test\E, with invalid options\b/ ], + warnings => ["Unknown option: bogus\n"], + ) : ( messages => [] ); + run_syncfiles({ + client => "$node.example.test", + args => $args, + synclist => { $node => $file }, + expected => $invalid ? [] : [ [ $node, $file, $copy_args ] ], + %diagnostics, + }); + }; + } + }; +} done_testing();