From 3e302e7f192778a30bc04a84570d247fd3ddc440 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Tue, 8 Sep 2026 12:11:42 -0300 Subject: [PATCH 01/62] test(xcat-core): Introduce BATS & convert shell scripting tests to it The go-xcat shell behavior tests were written as Perl harnesses, which made the shell assertions harder to read and kept shell-specific setup outside a native shell test framework. Add BATS to the GitHub Actions dependency set, run BATS tests from the same preserved source tree as the Perl unit suite, and move the go-xcat repository checks into xCAT-test/autotest/bats. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- .github/workflows/xcat_test.yml | 2 +- github_action_xcat_test.pl | 49 ++++- .../dracut_105/el/module-setup.sh | 13 +- .../dracut_105/ubuntu/module-setup.sh | 13 +- .../share/xcat/install/scripts/post.xcat | 5 +- .../share/xcat/install/scripts/pre.sles | 11 - .../share/xcat/install/scripts/scriptlib | 24 ++ xCAT-test/README.md | 20 ++ xCAT-test/autotest/bats/README.md | 21 ++ .../autotest/bats/genesis_ib_modules.bats | 118 ++++++++++ .../bats/go_xcat_common_repository.bats | 122 +++++++++++ .../autotest/bats/go_xcat_el_repo_check.bats | 206 ++++++++++++++++++ xCAT-test/autotest/bats/helpers/go_xcat.bash | 56 +++++ .../autotest/bats/helpers/shell_source.bash | 132 +++++++++++ .../bats/post_xcat_download_policy.bats | 76 +++++++ .../autotest/bats/remoteshell_restart.bats | 77 +++++++ xCAT-test/autotest/bats/sles_pre_script.bats | 45 ++++ .../autotest/bats/statelite_add_ssh.bats | 40 ++++ xCAT-test/unit/README.md | 5 + xCAT-test/unit/genesis_ib_modules.t | 40 ---- xCAT-test/unit/go_xcat_common_repository.t | 165 -------------- xCAT-test/unit/go_xcat_el_repo_check.t | 161 -------------- xCAT-test/unit/post_xcat_download_policy.t | 48 ---- xCAT-test/unit/remoteshell_kill_signal.t | 26 --- xCAT-test/unit/remoteshell_kill_wait.t | 62 ------ xCAT-test/unit/sles_pre_script.t | 20 -- xCAT-test/unit/statelite_add_ssh.t | 17 -- xCAT/postscripts/remoteshell | 16 +- xCAT/postscripts/xcatdsklspost | 67 +----- xCAT/postscripts/xcatlib.sh | 101 +++++++++ 30 files changed, 1113 insertions(+), 645 deletions(-) create mode 100644 xCAT-test/README.md create mode 100644 xCAT-test/autotest/bats/README.md create mode 100644 xCAT-test/autotest/bats/genesis_ib_modules.bats create mode 100644 xCAT-test/autotest/bats/go_xcat_common_repository.bats create mode 100644 xCAT-test/autotest/bats/go_xcat_el_repo_check.bats create mode 100644 xCAT-test/autotest/bats/helpers/go_xcat.bash create mode 100644 xCAT-test/autotest/bats/helpers/shell_source.bash create mode 100644 xCAT-test/autotest/bats/post_xcat_download_policy.bats create mode 100644 xCAT-test/autotest/bats/remoteshell_restart.bats create mode 100644 xCAT-test/autotest/bats/sles_pre_script.bats create mode 100644 xCAT-test/autotest/bats/statelite_add_ssh.bats delete mode 100644 xCAT-test/unit/genesis_ib_modules.t delete mode 100644 xCAT-test/unit/go_xcat_common_repository.t delete mode 100644 xCAT-test/unit/go_xcat_el_repo_check.t delete mode 100644 xCAT-test/unit/post_xcat_download_policy.t delete mode 100644 xCAT-test/unit/remoteshell_kill_signal.t delete mode 100644 xCAT-test/unit/remoteshell_kill_wait.t delete mode 100644 xCAT-test/unit/sles_pre_script.t delete mode 100644 xCAT-test/unit/statelite_add_ssh.t diff --git a/.github/workflows/xcat_test.yml b/.github/workflows/xcat_test.yml index 0fe2cd911..04d50aff0 100644 --- a/.github/workflows/xcat_test.yml +++ b/.github/workflows/xcat_test.yml @@ -7,7 +7,7 @@ jobs: steps: - uses: actions/checkout@v6 - name: Install dependencies - run: sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends --no-install-suggests 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 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: Run tests run: perl github_action_xcat_test.pl diff --git a/github_action_xcat_test.pl b/github_action_xcat_test.pl index f4480771b..42d565885 100644 --- a/github_action_xcat_test.pl +++ b/github_action_xcat_test.pl @@ -311,8 +311,11 @@ sub preserve_source_tree{ return 1; } - @output = runcmd("ls $unitsrc/xCAT-test/unit/*.t | wc -l"); - print "[preserve_source_tree] preserved $srcdir in $unitsrc ($output[0] unit tests)\n"; + @output = runcmd("find $unitsrc/xCAT-test/unit -name '*.t' | wc -l"); + my $perl_count = $output[0]; + @output = runcmd("find $unitsrc/xCAT-test/autotest/bats -name '*.bats' 2>/dev/null | wc -l"); + my $bats_count = $output[0]; + print "[preserve_source_tree] preserved $srcdir in $unitsrc ($perl_count Perl unit tests, $bats_count BATS tests)\n"; return 0; } @@ -466,6 +469,39 @@ sub run_unit_tests{ return 0; } +#-------------------------------------------------------- +# Fuction name: run_bats_tests +# Description: Run shell-script unit tests under xCAT-test/autotest/bats. +# Runs against the pre-build copy of the source tree taken by +# preserve_source_tree(), like the Perl unit tests. +# Attributes: +# Return code: 0 all tests passed, 1 otherwise +#-------------------------------------------------------- +sub run_bats_tests{ + my $testdir = "$unitsrc/xCAT-test/autotest/bats"; + my @output = runcmd("find $testdir -name '*.bats' -print -quit 2>/dev/null"); + if (!@output) { + print "[run_bats_tests] no BATS tests found under $testdir\n"; + return 0; + } + + my $cmd = "cd $unitsrc && bats -r xCAT-test/autotest/bats"; + print "[run_bats_tests] running $cmd\n"; + @output = runcmd("$cmd"); + print Dumper \@output; + if($::RUNCMD_RC){ + print RED "[run_bats_tests] $cmd ....[Failed]\n"; + $check_result_str .= "> **BATS TESTS Failed** : Please click ``Details`` label in ``Merge pull request`` box for detailed information\n"; + print $check_result_str; + return 1; + } + + print "[run_bats_tests] $cmd ....[Pass]\n"; + $check_result_str .= "> **BATS TESTS Successful**\n"; + print $check_result_str; + return 0; +} + #-------------------------------------------------------- # Fuction name: check_syntax # Description: @@ -689,6 +725,15 @@ if($rst){ } mark_time("run_unit_tests"); +#Run shell-script unit tests. +print GREEN "\n------Running xCAT-test BATS tests ------\n"; +$rst = run_bats_tests(); +if($rst){ + print RED "Run of xCAT-test BATS tests failed\n"; + exit $rst; +} +mark_time("run_bats_tests"); + #Check the syntax of changing code print GREEN "\n------ Checking the syntax of changed code------\n"; $rst = check_syntax(); diff --git a/xCAT-genesis-builder/dracut_105/el/module-setup.sh b/xCAT-genesis-builder/dracut_105/el/module-setup.sh index e2bb98250..302c4758f 100755 --- a/xCAT-genesis-builder/dracut_105/el/module-setup.sh +++ b/xCAT-genesis-builder/dracut_105/el/module-setup.sh @@ -9,14 +9,15 @@ depends() { } installkernel() { - local modules_dep modfile modname + local modules_dep modules_root modfile modname - if [[ -n "${kernel:-}" && -r "/lib/modules/$kernel/modules.dep" ]]; then - modules_dep="/lib/modules/$kernel/modules.dep" - elif [[ -n "${KERNELVERSION:-}" && -r "/lib/modules/$KERNELVERSION/modules.dep" ]]; then - modules_dep="/lib/modules/$KERNELVERSION/modules.dep" + modules_root="${DRACUT_MODULES_ROOT:-/lib/modules}" + if [[ -n "${kernel:-}" && -r "$modules_root/$kernel/modules.dep" ]]; then + modules_dep="$modules_root/$kernel/modules.dep" + elif [[ -n "${KERNELVERSION:-}" && -r "$modules_root/$KERNELVERSION/modules.dep" ]]; then + modules_dep="$modules_root/$KERNELVERSION/modules.dep" else - modules_dep=$(ls -1 /lib/modules/*/modules.dep 2>/dev/null | head -n 1) + modules_dep=$(ls -1 "$modules_root"/*/modules.dep 2>/dev/null | head -n 1) fi [[ -r "$modules_dep" ]] || return 0 diff --git a/xCAT-genesis-builder/dracut_105/ubuntu/module-setup.sh b/xCAT-genesis-builder/dracut_105/ubuntu/module-setup.sh index e4b8b0e3d..ba5325247 100755 --- a/xCAT-genesis-builder/dracut_105/ubuntu/module-setup.sh +++ b/xCAT-genesis-builder/dracut_105/ubuntu/module-setup.sh @@ -9,14 +9,15 @@ depends() { } installkernel() { - local modules_dep modfile modname + local modules_dep modules_root modfile modname - if [[ -n "${kernel:-}" && -r "/lib/modules/$kernel/modules.dep" ]]; then - modules_dep="/lib/modules/$kernel/modules.dep" - elif [[ -n "${KERNELVERSION:-}" && -r "/lib/modules/$KERNELVERSION/modules.dep" ]]; then - modules_dep="/lib/modules/$KERNELVERSION/modules.dep" + modules_root="${DRACUT_MODULES_ROOT:-/lib/modules}" + if [[ -n "${kernel:-}" && -r "$modules_root/$kernel/modules.dep" ]]; then + modules_dep="$modules_root/$kernel/modules.dep" + elif [[ -n "${KERNELVERSION:-}" && -r "$modules_root/$KERNELVERSION/modules.dep" ]]; then + modules_dep="$modules_root/$KERNELVERSION/modules.dep" else - modules_dep=$(ls -1 /lib/modules/*/modules.dep 2>/dev/null | head -n 1) + modules_dep=$(ls -1 "$modules_root"/*/modules.dep 2>/dev/null | head -n 1) fi [[ -r "$modules_dep" ]] || return 0 diff --git a/xCAT-server/share/xcat/install/scripts/post.xcat b/xCAT-server/share/xcat/install/scripts/post.xcat index d6ac63ff0..bb40d0f22 100755 --- a/xCAT-server/share/xcat/install/scripts/post.xcat +++ b/xCAT-server/share/xcat/install/scripts/post.xcat @@ -100,10 +100,7 @@ if [ ! -x /usr/bin/wget ]; then sleep 36500d fi -# These dispatcher scripts are not needed by the legacy post.xcat path. Newer -# wget parses HTML-looking regex strings inside downloaded scripts and fails the -# whole recursive download on bogus URLs. -wget -l inf -N -r --waitretry=10 --random-wait --retry-connrefused -e robots=off -nH --cut-dirs=2 --reject "index.html*,post.xcat.ng,post.xcat.rhels10" --no-parent -t 20 -T 60 http://${MASTER_IP}:${HTTPPORT}${INSTALLDIR}/postscripts/ -P /xcatpost 2> /tmp/wget.log +xcat_download_postscripts "${MASTER_IP}:${HTTPPORT}" "$INSTALLDIR" "/xcatpost" "/tmp/wget.log" if [ "$?" != "0" ]; then msgutil_r "$MASTER_IP" "error" "failed to download postscripts from http://$MASTER_IP$INSTALLDIR/postscripts/,check /tmp/wget.log on the node, halt ..." "/var/log/xcat/xcat.log" "$log_label" /tmp/updateflag $MASTER $XCATIPORT "installstatus failed" diff --git a/xCAT-server/share/xcat/install/scripts/pre.sles b/xCAT-server/share/xcat/install/scripts/pre.sles index 292b4c6b7..1eaa4c7c1 100644 --- a/xCAT-server/share/xcat/install/scripts/pre.sles +++ b/xCAT-server/share/xcat/install/scripts/pre.sles @@ -191,17 +191,6 @@ if [ -e "/tmp/xcat.install_disk" ]; then fi msgutil_r "$MASTER_IP" "info" "Found $instdisk, generate partition file..." "/var/log/xcat/xcat.log" "$log_label" -set_sles11_uefi_bootloader() -{ - if grep -E 'install=.*sles11' /proc/cmdline >/dev/null 2>&1; then - # SLES 11 AutoYaST keeps the template's legacy MBR bootloader - # location unless the UEFI path explicitly selects elilo. - sed -i -e '/mbr!elilo!' \ - /tmp/profile/modified.xml - fi -} if [ -d /sys/firmware/efi ]; then sed -e 's!XCATPARTITIONHOOK!'$instdisk'vfat/boot/efi128mbswapauto/auto!' /tmp/profile/autoinst.xml > /tmp/profile/modified.xml diff --git a/xCAT-server/share/xcat/install/scripts/scriptlib b/xCAT-server/share/xcat/install/scripts/scriptlib index ab74bddb6..de427d3e2 100644 --- a/xCAT-server/share/xcat/install/scripts/scriptlib +++ b/xCAT-server/share/xcat/install/scripts/scriptlib @@ -63,3 +63,27 @@ declare -F xcat_enable_active_nm_autoconnect &>/dev/null || function xcat_enable nmcli con mod "$con_name" connection.autoconnect yes done } + +declare -F xcat_download_postscripts &>/dev/null || function xcat_download_postscripts { + local server="$1" + local install_dir="${2:-/install}" + local postroot="${3:-/xcatpost}" + local log_file="${4:-/tmp/wget.log}" + + export LANG=C + wget -l inf -N -r --waitretry=10 --random-wait --retry-connrefused -e robots=off -nH --cut-dirs=2 --reject "index.html*,post.xcat.ng,post.xcat.rhels10" --no-parent -t 20 -T 60 "http://${server}${install_dir}/postscripts/" -P "$postroot" 2> "$log_file" +} + +declare -F set_sles11_uefi_bootloader &>/dev/null || function set_sles11_uefi_bootloader { + local cmdline="${1:-/proc/cmdline}" + local profile="${2:-/tmp/profile/modified.xml}" + + if grep -E 'install=.*sles11' "$cmdline" >/dev/null 2>&1; then + # SLES 11 AutoYaST keeps the template's legacy MBR bootloader + # location unless the UEFI path explicitly selects elilo. + sed -i -e '/mbr!elilo!' \ + "$profile" + fi +} diff --git a/xCAT-test/README.md b/xCAT-test/README.md new file mode 100644 index 000000000..7406be0d4 --- /dev/null +++ b/xCAT-test/README.md @@ -0,0 +1,20 @@ +# xCAT-test + +Unit tests that run from the source checkout are split by implementation +language: + +| Test type | Location | Runner | +| --------- | -------- | ------ | +| Perl unit tests | `xCAT-test/unit/*.t` | `prove -r xCAT-test/unit` | +| Shell unit tests | `xCAT-test/autotest/bats/*.bats` | `bats -r xCAT-test/autotest/bats` | + +Use Perl `.t` tests for Perl modules, Perl scripts, templates, and repository +artifacts. Use BATS tests for shell-script behavior that can be exercised from +the checkout by sourcing a shell library or script and shadowing external +commands. + +Shell behavior should not be tested by Perl tests that grep shell source. Put +those tests under `xCAT-test/autotest/bats` instead. + +See `unit/README.md` and `autotest/bats/README.md` for the detailed rules for +each unit-test suite. diff --git a/xCAT-test/autotest/bats/README.md b/xCAT-test/autotest/bats/README.md new file mode 100644 index 000000000..29a1403e8 --- /dev/null +++ b/xCAT-test/autotest/bats/README.md @@ -0,0 +1,21 @@ +# xCAT-test/autotest/bats + +Shell-script unit tests live here and run with: + +```bash +bats -r xCAT-test/autotest/bats +``` + +The GitHub Actions `xcat_test` workflow runs this command after the Perl `.t` +unit tests. Use BATS for shell behavior that can be exercised from the source +tree without an installed xCAT, a live management node, or real services. + +Prefer sourcing an existing shell library or sourceable script and calling the +function under test. Keep reusable install-template helpers in +`xCAT-server/share/xcat/install/scripts/scriptlib`, and reusable postscript +helpers in `xCAT/postscripts/xcatlib.sh`. Use scratch directories and shadowed +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. diff --git a/xCAT-test/autotest/bats/genesis_ib_modules.bats b/xCAT-test/autotest/bats/genesis_ib_modules.bats new file mode 100644 index 000000000..ed132043e --- /dev/null +++ b/xCAT-test/autotest/bats/genesis_ib_modules.bats @@ -0,0 +1,118 @@ +#!/usr/bin/env bats + +load 'helpers/shell_source' + +setup() +{ + GENESIS_SPEC="$(repo_path 'xCAT-genesis-builder/xCAT-genesis-base.spec')" + DRACUT_MODULE="$(repo_path 'xCAT-genesis-builder/dracut_105/el/module-setup.sh')" + DOXCAT="$(repo_path 'xCAT-genesis-scripts/usr/bin/doxcat')" + [ -r "$GENESIS_SPEC" ] || skip "$GENESIS_SPEC is required" + [ -r "$DRACUT_MODULE" ] || skip "$DRACUT_MODULE is required" + [ -r "$DOXCAT" ] || skip "$DOXCAT is required" + export GENESIS_SPEC DRACUT_MODULE DOXCAT +} + +run_installkernel() +{ + local modules_root="$1" + local instmods_log="$2" + + kernel=5.14.0-test + DRACUT_MODULES_ROOT="$modules_root" + instmods() + { + printf '%s\n' "$1" >>"$instmods_log" + } + + source "$DRACUT_MODULE" + installkernel +} + +run_doxcat_modprobe_preamble() +{ + local preamble="$1" + local modprobe_log="$2" + + modprobe() + { + printf '%s\n' "$*" >>"$modprobe_log" + } + + eval "$preamble" +} + +run_doxcat_bootif_block() +{ + local block="$1" + + BOOTIF=01-aa-bb-cc-dd-ee-ff + bootnic= + log_label=test + gripeiter=2 + + logger() { :; } + sleep() { :; } + ip() + { + printf '%s\n' "$*" >>"$IP_LOG" + if [ "$*" = "link show" ]; then + cat <<'EOF' +2: eth0: mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000 + link/ether 00:11:22:33:44:55 brd ff:ff:ff:ff:ff:ff +3: ib0: mtu 65520 qdisc mq state UP mode DEFAULT group default qlen 256 + link/infiniband 00:bb:cc:dd:ee:ff brd 00:ff:ff:ff:ff:ff +EOF + fi + } + + eval "$block" + printf '%s\n' "$bootnic" +} + +@test "genesis build requires kernel module packages" { + grep -Fxq 'BuildRequires: kernel-core' "$GENESIS_SPEC" + grep -Fxq 'BuildRequires: kernel-modules' "$GENESIS_SPEC" + grep -Fxq 'BuildRequires: kernel-modules-extra' "$GENESIS_SPEC" +} + +@test "dracut genesis module installs every module from modules.dep" { + local modules_root="${BATS_TEST_TMPDIR}/modules" + local modules_dep="${modules_root}/5.14.0-test/modules.dep" + local instmods_log="${BATS_TEST_TMPDIR}/instmods.log" + + mkdir -p "${modules_root}/5.14.0-test" + cat >"$modules_dep" <<'EOF' +kernel/drivers/infiniband/ulp/ipoib/ib_ipoib.ko.xz: +kernel/drivers/net/ethernet/intel/e1000e/e1000e.ko.xz: +EOF + + run run_installkernel "$modules_root" "$instmods_log" + [ "$status" -eq 0 ] + grep -Fxq 'ib_ipoib' "$instmods_log" + grep -Fxq 'e1000e' "$instmods_log" +} + +@test "doxcat loads IP over InfiniBand support during startup" { + local preamble + local modprobe_log="${BATS_TEST_TMPDIR}/modprobe.log" + + preamble="$(extract_line_range "$DOXCAT" '^modprobe acpi_cpufreq' '^modprobe ib_ipoib$')" || return 1 + + run run_doxcat_modprobe_preamble "$preamble" "$modprobe_log" + [ "$status" -eq 0 ] + grep -Fxq 'ib_ipoib' "$modprobe_log" +} + +@test "doxcat falls back to InfiniBand BOOTIF lookup after Ethernet lookup misses" { + local block + + IP_LOG="${BATS_TEST_TMPDIR}/ip.log" + export IP_LOG + block="$(extract_shell_if_block "$DOXCAT" 'if [ ! -z "$BOOTIF" ]; then')" || return 1 + + run run_doxcat_bootif_block "$block" + [ "$status" -eq 0 ] + [ "$output" = "ib0" ] + [ "$(grep -c '^link show$' "$IP_LOG")" -eq 2 ] +} diff --git a/xCAT-test/autotest/bats/go_xcat_common_repository.bats b/xCAT-test/autotest/bats/go_xcat_common_repository.bats new file mode 100644 index 000000000..b49f4fce0 --- /dev/null +++ b/xCAT-test/autotest/bats/go_xcat_common_repository.bats @@ -0,0 +1,122 @@ +#!/usr/bin/env bats + +load 'helpers/go_xcat' + +setup() +{ + go_xcat_require_source +} + +run_common_repository_case() +{ + local case_dir="$1" + local common_present="$2" + shift 2 + + mkdir -p "$case_dir" + export ADD_LOG="${case_dir}/add.log" + export COMMON_PRESENT="$common_present" + export DOWNLOAD_LOG="${case_dir}/download.log" + export ID_LOG="${case_dir}/id.log" + export TEST_TMP="$case_dir" + + go_xcat_load_functions \ + add_xcat_dep_common_repo_yum_or_zypper \ + xcat_dep_common_repo_configured \ + refresh_xcat_dep_repository_ids + + TMP_DIR="$TEST_TMP" + GO_XCAT_DEFAULT_BASE_URL=https://repo.example.invalid + GO_XCAT_DEP_REPOSITORY_IDS=(xcat-dep) + + yum() { :; } + + download_file() + { + printf '%s\n' "$1" >>"$DOWNLOAD_LOG" + [[ ${COMMON_PRESENT:-0} == 1 ]] || return 1 + : >"$2" + } + + add_repo_by_url_yum_or_zypper() + { + printf '%s %s\n' "$1" "$2" >>"$ADD_LOG" + } + + xcat_dep_common_repo_configured() + { + [[ -s "$ADD_LOG" ]] + } + + ( add_xcat_dep_common_repo_yum_or_zypper "$@" ) + refresh_xcat_dep_repository_ids + printf '%s\n' "${GO_XCAT_DEP_REPOSITORY_IDS[*]}" >"$ID_LOG" +} + +run_template_generation() +{ + local tmp_dir="$1" + local repo_log="$2" + + mkdir -p "$tmp_dir" + export REPO_LOG="$repo_log" + export TEST_TMP="$tmp_dir" + + go_xcat_load_functions add_repo_by_url_yum_or_zypper + + TMP_DIR="$TEST_TMP" + GO_XCAT_DEFAULT_INSTALL_PATH=/install/xcat + yum() { :; } + add_repo_by_file() { cp "$1" "$REPO_LOG"; } + + add_repo_by_url_yum_or_zypper \ + https://repo.example.invalid/xcat-dep/common xcat-dep-common optional +} + +@test "an available remote common repository is enabled" { + local case_dir="${BATS_TEST_TMPDIR}/remote-present" + + run run_common_repository_case "$case_dir" 1 "" latest + [ "$status" -eq 0 ] + [ "$(read_file_or_empty "${case_dir}/download.log")" = "https://repo.example.invalid/yum/latest/xcat-dep/common/repodata/repomd.xml" ] + [ "$(read_file_or_empty "${case_dir}/add.log")" = "https://repo.example.invalid/yum/latest/xcat-dep/common xcat-dep-common" ] + [ "$(read_file_or_empty "${case_dir}/id.log")" = "xcat-dep xcat-dep-common" ] +} + +@test "a release without the remote common repository remains usable" { + local case_dir="${BATS_TEST_TMPDIR}/remote-missing" + + run run_common_repository_case "$case_dir" 0 "" 2.18 + [ "$status" -eq 0 ] + [ "$(read_file_or_empty "${case_dir}/add.log")" = "" ] + [ "$(read_file_or_empty "${case_dir}/id.log")" = "xcat-dep" ] +} + +@test "a custom repository file does not guess an unrelated common repository" { + local case_dir="${BATS_TEST_TMPDIR}/repo-file" + + run run_common_repository_case "$case_dir" 1 https://repo.example.invalid/custom/xcat-dep.repo latest + [ "$status" -eq 0 ] + [ "$(read_file_or_empty "${case_dir}/download.log")" = "" ] + [ "$(read_file_or_empty "${case_dir}/add.log")" = "" ] +} + +@test "a local common repository is enabled beside the distribution repository" { + local local_root="${BATS_TEST_TMPDIR}/local-repository" + local case_dir="${BATS_TEST_TMPDIR}/local-present" + mkdir -p "${local_root}/common/repodata" + : >"${local_root}/common/repodata/repomd.xml" + + run run_common_repository_case "$case_dir" 0 "$local_root" latest + [ "$status" -eq 0 ] + [ "$(read_file_or_empty "${case_dir}/add.log")" = "${local_root}/common xcat-dep-common" ] +} + +@test "the optional common repository template tolerates outages and verifies metadata" { + local template_log="${BATS_TEST_TMPDIR}/generated-common.repo" + + run run_template_generation "$BATS_TEST_TMPDIR" "$template_log" + [ "$status" -eq 0 ] + grep -Fxq 'skip_if_unavailable=1' "$template_log" + grep -Fxq 'repo_gpgcheck=1' "$template_log" +} diff --git a/xCAT-test/autotest/bats/go_xcat_el_repo_check.bats b/xCAT-test/autotest/bats/go_xcat_el_repo_check.bats new file mode 100644 index 000000000..0064f8673 --- /dev/null +++ b/xCAT-test/autotest/bats/go_xcat_el_repo_check.bats @@ -0,0 +1,206 @@ +#!/usr/bin/env bats + +load 'helpers/go_xcat' + +setup() +{ + go_xcat_require_source + export CALLS="${BATS_TEST_TMPDIR}/calls" + export GO_XCAT_ARCH=x86_64 + export GO_XCAT_LINUX_DISTRO=rocky + export GO_XCAT_LINUX_VERSION=10.2 + export EPEL_HAS=1 + export CRB_HAS=1 + export QUERY_FAIL=0 + export QUERY_WARNS=0 + export SOURCE_ONLY=0 + export ENTRY=check +} + +run_el_repo_check() +{ + rm -f "$CALLS" + + go_xcat_load_functions \ + repo_carries \ + el_epel_and_crb_check \ + install_packages_dnf \ + install_packages_yum + + EL_EPEL_TEST_RPM=perl-Crypt-CBC + EL_CRB_TEST_RPM=perl-IO-Tty + + dnf() + { + echo "$*" >>"$CALLS" + if [[ ${QUERY_FAIL:-0} == 1 ]]; then + echo "Error: Failed to download metadata for repo 'epel'" >&2 + return 1 + fi + [[ ${QUERY_WARNS:-0} == 1 ]] && echo "Warning: repository 'extras' metadata is stale" >&2 + + local has=0 + case "$*" in + *perl-Crypt-CBC*) has="$EPEL_HAS" ;; + *perl-IO-Tty*) has="$CRB_HAS" ;; + esac + + [[ ${SOURCE_ONLY:-0} == 1 && "$*" != *"--arch"* ]] && has=1 + case "$1" in + repoquery) [[ $has == 1 ]] && { echo "${@: -1}"; echo "${@: -1}"; }; return 0 ;; + list) [[ $has == 1 ]] && return 0; return 1 ;; + esac + return 0 + } + + yum() + { + dnf "$@" + } + + case "${ENTRY:-check}" in + dnf) install_packages_dnf -y xCAT ;; + yum) install_packages_yum -y xCAT ;; + *) el_epel_and_crb_check dnf ;; + esac +} + +@test "EL9 with EPEL and CRB passes after probing binary repositories" { + export GO_XCAT_LINUX_VERSION=9.5 + + run run_el_repo_check + [ "$status" -eq 0 ] + probes="$(joined_file_lines "$CALLS")" + [[ "$probes" =~ perl-Crypt-CBC.*\;.*perl-IO-Tty ]] + [[ "$probes" =~ ^repoquery\ ]] + [[ "$probes" =~ --arch\ x86_64,noarch ]] +} + +@test "EL9 without EPEL stops and names the EL9 release package" { + export GO_XCAT_LINUX_VERSION=9.5 + export EPEL_HAS=0 + + run run_el_repo_check + [ "$status" -eq 1 ] + [[ "$output" =~ epel-release-latest-9\.noarch ]] +} + +@test "EL10 with EPEL and CRB passes after probing both repositories" { + run run_el_repo_check + [ "$status" -eq 0 ] + probes="$(joined_file_lines "$CALLS")" + [[ "$probes" =~ perl-Crypt-CBC.*\;.*perl-IO-Tty ]] +} + +@test "EL10 without EPEL stops with the EL10 EPEL release package" { + export EPEL_HAS=0 + + run run_el_repo_check + [ "$status" -eq 1 ] + [[ "$output" =~ requires\ EPEL\ repository ]] + [[ "$output" =~ epel-release-latest-10\.noarch ]] +} + +@test "EL10 without CRB stops with current CRB guidance" { + export GO_XCAT_LINUX_DISTRO=rhel + export CRB_HAS=0 + + run run_el_repo_check + [ "$status" -eq 1 ] + [[ "$output" =~ requires\ CRB\ repository ]] + [[ "$output" =~ "'dnf update epel-release' and then 'crb enable'" ]] + [[ ! "$output" =~ gpgcheck=0|centos-crb|subscription-manager ]] +} + +@test "a source repository does not stand in for the binary one" { + export EPEL_HAS=0 + export SOURCE_ONLY=1 + + run run_el_repo_check + [ "$status" -eq 1 ] + [[ "$output" =~ requires\ EPEL\ repository ]] +} + +@test "Oracle Linux 10 without CRB names its CodeReady Builder command" { + export GO_XCAT_LINUX_DISTRO=ol + export GO_XCAT_LINUX_VERSION=10.1 + export CRB_HAS=0 + + run run_el_repo_check + [ "$status" -eq 1 ] + [[ "$output" =~ dnf\ config-manager\ --enable\ ol10_codeready_builder ]] +} + +@test "a failed repository query stops with the package manager error" { + export QUERY_FAIL=1 + + run run_el_repo_check + [ "$status" -eq 1 ] + [[ "$output" =~ Failed\ to\ download\ metadata ]] + [[ ! "$output" =~ requires\ EPEL\ repository ]] +} + +@test "a warning on stderr does not stand in for a package" { + export EPEL_HAS=0 + export QUERY_WARNS=1 + + run run_el_repo_check + [ "$status" -eq 1 ] + [[ "$output" =~ requires\ EPEL\ repository ]] +} + +@test "a warning beside a real match does not fail the check" { + export QUERY_WARNS=1 + + run run_el_repo_check + [ "$status" -eq 0 ] +} + +@test "CentOS Stream 10, which reports the major version alone, is checked" { + export GO_XCAT_LINUX_DISTRO=centos + export GO_XCAT_LINUX_VERSION=10 + export EPEL_HAS=0 + export CRB_HAS=0 + + run run_el_repo_check + [ "$status" -eq 1 ] + [[ "$output" =~ requires\ EPEL\ repository ]] +} + +@test "EL8 and Fedora are not checked" { + export GO_XCAT_LINUX_VERSION=8.10 + export EPEL_HAS=0 + export CRB_HAS=0 + + run run_el_repo_check + [ "$status" -eq 0 ] + [ "$(joined_file_lines "$CALLS")" = "" ] + + export GO_XCAT_LINUX_DISTRO=fedora + export GO_XCAT_LINUX_VERSION=42 + + run run_el_repo_check + [ "$status" -eq 0 ] +} + +@test "dnf and yum installer paths run the check before installing" { + for entry in dnf yum; do + export ENTRY="$entry" + export EPEL_HAS=0 + export CRB_HAS=1 + + run run_el_repo_check + [ "$status" -eq 1 ] + [[ "$output" =~ requires\ EPEL\ repository ]] + probes="$(joined_file_lines "$CALLS")" + [[ ! "$probes" =~ install ]] + + export EPEL_HAS=1 + export CRB_HAS=1 + + run run_el_repo_check + [ "$status" -eq 0 ] + probes="$(joined_file_lines "$CALLS")" + [[ "$probes" =~ perl-IO-Tty.*\;.*install\ initscripts.*\;.*install\ xCAT ]] + done +} diff --git a/xCAT-test/autotest/bats/helpers/go_xcat.bash b/xCAT-test/autotest/bats/helpers/go_xcat.bash new file mode 100644 index 000000000..b863c963f --- /dev/null +++ b/xCAT-test/autotest/bats/helpers/go_xcat.bash @@ -0,0 +1,56 @@ +#!/usr/bin/env bash + +go_xcat_default_source() +{ + printf '%s\n' "${BATS_TEST_DIRNAME}/../../../xCAT-server/share/xcat/tools/go-xcat" +} + +go_xcat_require_source() +{ + GO_XCAT_SOURCE="${XCAT_TEST_GO_XCAT:-$(go_xcat_default_source)}" + export GO_XCAT_SOURCE + [ -r "$GO_XCAT_SOURCE" ] || skip "$GO_XCAT_SOURCE is required" +} + +go_xcat_extract_functions() +{ + local function_name + for function_name in "$@"; do + awk -v name="$function_name" ' + $0 == "function " name "()" { copy = 1 } + copy { print } + copy && /^}$/ { exit } + ' "$GO_XCAT_SOURCE" + done +} + +go_xcat_load_functions() +{ + local function_body function_name + function_body="$(go_xcat_extract_functions "$@")" || return 1 + eval "$function_body" + for function_name in "$@"; do + declare -F "$function_name" >/dev/null || { + printf 'missing %s\n' "$function_name" >&2 + return 70 + } + done +} + +read_file_or_empty() +{ + local path="$1" + [ -f "$path" ] || return 0 + cat "$path" +} + +joined_file_lines() +{ + local path="$1" + local line separator="" + [ -f "$path" ] || return 0 + while IFS= read -r line; do + printf '%s%s' "$separator" "$line" + separator=";" + done <"$path" +} diff --git a/xCAT-test/autotest/bats/helpers/shell_source.bash b/xCAT-test/autotest/bats/helpers/shell_source.bash new file mode 100644 index 000000000..f76ed55d3 --- /dev/null +++ b/xCAT-test/autotest/bats/helpers/shell_source.bash @@ -0,0 +1,132 @@ +#!/usr/bin/env bash + +repo_root() +{ + printf '%s\n' "${BATS_TEST_DIRNAME}/../../.." +} + +repo_path() +{ + printf '%s/%s\n' "$(repo_root)" "$1" +} + +require_repo_file() +{ + local path + path="$(repo_path "$1")" + [ -r "$path" ] || skip "$path is required" + printf '%s\n' "$path" +} + +read_file_or_empty() +{ + local path="$1" + [ -f "$path" ] || return 0 + cat "$path" +} + +extract_shell_function() +{ + local file="$1" + local name="$2" + + awk -v name="$name" ' + BEGIN { + signature = "^[[:space:]]*(function[[:space:]]+)?" name "([[:space:]]*\\(\\))?[[:space:]]*$" + inline_signature = "^[[:space:]]*(function[[:space:]]+)?" name "([[:space:]]*\\(\\))?[[:space:]]*\\{" + } + $0 ~ signature || $0 ~ inline_signature { + copy = 1 + } + copy { + print + opened += gsub(/\{/, "{") + closed += gsub(/\}/, "}") + if (opened > 0 && opened == closed) { + found = 1 + exit + } + } + END { + if (!found) { + exit 1 + } + } + ' "$file" +} + +extract_shell_if_block() +{ + local file="$1" + local start="$2" + + awk -v start="$start" ' + index($0, start) { + copy = 1 + } + copy { + print + if ($0 ~ /^[[:space:]]*if[[:space:]\[]/) { + depth++ + } + line = $0 + while (line ~ /(^|[;[:space:]])fi([;[:space:]]|$)/) { + depth-- + sub(/(^|[;[:space:]])fi([;[:space:]]|$)/, " ", line) + } + if (depth == 0) { + found = 1 + exit + } + } + END { + if (!found) { + exit 1 + } + } + ' "$file" +} + +extract_line_range() +{ + local file="$1" + local start="$2" + local end="$3" + + awk -v start="$start" -v end="$end" ' + $0 ~ start { + copy = 1 + } + copy { + print + if ($0 ~ end) { + found = 1 + exit + } + } + END { + if (!found) { + exit 1 + } + } + ' "$file" +} + +extract_first_matching_line() +{ + local file="$1" + local pattern="$2" + + awk -v pattern="$pattern" ' + $0 ~ pattern { + print + found = 1 + exit + } + END { + if (!found) { + exit 1 + } + } + ' "$file" +} diff --git a/xCAT-test/autotest/bats/post_xcat_download_policy.bats b/xCAT-test/autotest/bats/post_xcat_download_policy.bats new file mode 100644 index 000000000..4ec06387a --- /dev/null +++ b/xCAT-test/autotest/bats/post_xcat_download_policy.bats @@ -0,0 +1,76 @@ +#!/usr/bin/env bats + +load 'helpers/shell_source' + +setup() +{ + SCRIPT_LIB="$(repo_path 'xCAT-server/share/xcat/install/scripts/scriptlib')" + XCATLIB="$(repo_path 'xCAT/postscripts/xcatlib.sh')" + [ -r "$SCRIPT_LIB" ] || skip "$SCRIPT_LIB is required" + [ -r "$XCATLIB" ] || skip "$XCATLIB is required" + export SCRIPT_LIB XCATLIB +} + +capture_install_scriptlib_wget() +{ + local wget_log="$1" + + wget() + { + printf '%s\n' "$*" >"$wget_log" + return 0 + } + + source "$SCRIPT_LIB" + xcat_download_postscripts "192.0.2.10:80" "/install" "/xcatpost" "$wget_log" +} + +capture_xcatlib_wget() +{ + local wget_log="$1" + + xcatpost="${BATS_TEST_TMPDIR}/xcatpost" + INSTALLDIR=/install + + echolog() { :; } + sleep() { :; } + grep() + { + [ "${*: -1}" = "/tmp/wget.log" ] && return 1 + command grep "$@" + } + wget() + { + printf '%s\n' "$*" >"$wget_log" + return 0 + } + + source "$XCATLIB" + download_postscripts 192.0.2.10:80 +} + +assert_download_policy() +{ + local args="$1" + + [[ "$args" == *'--reject index.html*,post.xcat.ng,post.xcat.rhels10'* ]] + [[ "$args" == *'--no-parent'* ]] + [[ "$args" == *'postscripts/'* ]] + [[ "$args" != *'>"$KILL_LOG" + [ "$1" = "-0" ] && return 1 + return 0 + } + sleep() { :; } + sshd() + { + printf '%s\n' start >>"$SSHD_LOG" + } + + source "$XCATLIB" + xcat_restart_sshd_after_failed_service_restart sshd +} + +run_wait_for_processes() +{ + local rc + + source "$XCATLIB" + xcat_wait_for_processes_to_exit "$*" 3 + rc=$? + printf '%s\n' "$rc" + return 0 +} + +@test "remoteshell restart fallback sends an uncatchable signal before starting sshd" { + KILL_LOG="${BATS_TEST_TMPDIR}/kill.log" + SSHD_LOG="${BATS_TEST_TMPDIR}/sshd.log" + export KILL_LOG SSHD_LOG + + run run_restart_fallback + [ "$status" -eq 0 ] + grep -Fxq -- '-9 4321' "$KILL_LOG" + ! grep -Eq '^9( |$)' "$KILL_LOG" + [ "$(read_file_or_empty "$SSHD_LOG")" = "start" ] +} + +@test "remoteshell wait loop reports a still-running process and gives up" { + local child + + sleep 30 & + child=$! + + run run_wait_for_processes "$child" + kill -KILL "$child" 2>/dev/null || true + wait "$child" 2>/dev/null || true + + [ "$status" -eq 0 ] + [ "$output" = "1" ] +} + +@test "remoteshell wait loop returns as soon as killed processes are gone" { + run run_wait_for_processes 999999 + [ "$status" -eq 0 ] + [ "$output" = "0" ] +} diff --git a/xCAT-test/autotest/bats/sles_pre_script.bats b/xCAT-test/autotest/bats/sles_pre_script.bats new file mode 100644 index 000000000..f35153200 --- /dev/null +++ b/xCAT-test/autotest/bats/sles_pre_script.bats @@ -0,0 +1,45 @@ +#!/usr/bin/env bats + +load 'helpers/shell_source' + +setup() +{ + SCRIPT_LIB="$(repo_path 'xCAT-server/share/xcat/install/scripts/scriptlib')" + [ -r "$SCRIPT_LIB" ] || skip "$SCRIPT_LIB is required" + export SCRIPT_LIB +} + +@test "SLES 11 UEFI install changes the AutoYaST bootloader to elilo" { + local cmdline="${BATS_TEST_TMPDIR}/cmdline" + local profile="${BATS_TEST_TMPDIR}/modified.xml" + + printf '%s\n' 'BOOT_IMAGE=/linux install=http://192.0.2.10/install/sles11/ppc64le' >"$cmdline" + cat >"$profile" <<'EOF' + +true +true +mbr + +EOF + + source "$SCRIPT_LIB" + run set_sles11_uefi_bootloader "$cmdline" "$profile" + [ "$status" -eq 0 ] + grep -Fxq 'elilo' "$profile" + ! grep -q 'mbr' "$profile" + ! grep -q '"$cmdline" + printf '%s\n' 'mbr' >"$profile" + + source "$SCRIPT_LIB" + run set_sles11_uefi_bootloader "$cmdline" "$profile" + [ "$status" -eq 0 ] + grep -Fxq 'mbr' "$profile" +} diff --git a/xCAT-test/autotest/bats/statelite_add_ssh.bats b/xCAT-test/autotest/bats/statelite_add_ssh.bats new file mode 100644 index 000000000..b633666f9 --- /dev/null +++ b/xCAT-test/autotest/bats/statelite_add_ssh.bats @@ -0,0 +1,40 @@ +#!/usr/bin/env bats + +load 'helpers/shell_source' + +setup() +{ + ADD_SSH="$(repo_path 'xCAT-server/share/xcat/netboot/add-on/statelite/add_ssh')" + [ -r "$ADD_SSH" ] || skip "$ADD_SSH is required" + export ADD_SSH +} + +run_sshd_config_block() +{ + local root="$1" + local block + + block="$(extract_shell_if_block "$ADD_SSH" 'if [ -r $ROOTDIR/etc/ssh/sshd_config ]')" || return 1 + ROOTDIR="$root" + eval "$block" +} + +@test "statelite add_ssh writes sshd settings below systemd's open-file limit" { + local root="${BATS_TEST_TMPDIR}/rootimg" + local sshd_config="${root}/etc/ssh/sshd_config" + + mkdir -p "${root}/etc/ssh" + cat >"$sshd_config" <<'EOF' +X11Forwarding no +KeyRegenerationInterval 3600 +MaxStartups 1024 +EOF + + run run_sshd_config_block "$root" + [ "$status" -eq 0 ] + grep -Fxq 'X11Forwarding yes' "$sshd_config" + grep -Fxq 'KeyRegenerationInterval 0' "$sshd_config" + grep -Fxq '#MaxStartups 1024' "$sshd_config" + grep -Fxq 'MaxStartups 100:30:200' "$sshd_config" + ! grep -Fxq 'MaxStartups 1024' "$sshd_config" +} diff --git a/xCAT-test/unit/README.md b/xCAT-test/unit/README.md index 79096bcbf..2fd67bcb6 100644 --- a/xCAT-test/unit/README.md +++ b/xCAT-test/unit/README.md @@ -46,6 +46,11 @@ so putting a test in `integration/` does not cost it CI coverage. What differs i each suite is allowed to depend on, and that unit tests also run standalone from a bare checkout with no xCAT at all. +Shell-script unit tests belong in [`../autotest/bats`](../autotest/bats/README.md) +and run with BATS. Do not add Perl `.t` tests that grep shell source when the +behavior can be exercised by sourcing a shell library or script and shadowing the +external commands it calls. + The distinction matters because a test that needs an absent environment does not fail -- it calls `plan skip_all` and reports as skipped. A handful of those in a suite of several hundred assertions is easy to stop reading. Keeping the two kinds in separate diff --git a/xCAT-test/unit/genesis_ib_modules.t b/xCAT-test/unit/genesis_ib_modules.t deleted file mode 100644 index ef2c3f36e..000000000 --- a/xCAT-test/unit/genesis_ib_modules.t +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env perl -use strict; -use warnings; - -use File::Spec; -use FindBin; -use Test::More; - -my $repo_root = File::Spec->rel2abs(File::Spec->catdir($FindBin::Bin, '..', '..')); - -my $spec = read_file('xCAT-genesis-builder/xCAT-genesis-base.spec'); -like($spec, qr/^BuildRequires:\s+kernel-core$/m, 'genesis build installs the kernel core'); -like($spec, qr/^BuildRequires:\s+kernel-modules$/m, 'genesis build installs the standard kernel modules'); -like($spec, qr/^BuildRequires:\s+kernel-modules-extra$/m, 'genesis build installs the extra kernel modules'); - -my $dracut_module = read_file('xCAT-genesis-builder/dracut_105/el/module-setup.sh'); -like( - $dracut_module, - qr/installkernel\(\) \{.*?modules_dep=.*?while IFS= read -r modfile;.*?instmods "\$modname".*?done < "\$modules_dep"/s, - 'genesis dracut module installs every module available to the build' -); - -my $doxcat = read_file('xCAT-genesis-scripts/usr/bin/doxcat'); -like($doxcat, qr/^modprobe ib_ipoib$/m, 'genesis loads the IP over InfiniBand module'); -like( - $doxcat, - qr/if \[ -z "\$bootnic" \]; then\s+bootnic=`ip link show\|grep -B1 infiniband/s, - 'genesis checks InfiniBand addresses only after the Ethernet lookup misses' -); - -done_testing(); - -sub read_file { - my ($file) = @_; - my $path = File::Spec->catfile($repo_root, split m{/}, $file); - open(my $fh, '<', $path) or die "open $path: $!"; - my $contents = do { local $/; <$fh> }; - close($fh) or die "close $path: $!"; - return $contents; -} diff --git a/xCAT-test/unit/go_xcat_common_repository.t b/xCAT-test/unit/go_xcat_common_repository.t deleted file mode 100644 index 21931d248..000000000 --- a/xCAT-test/unit/go_xcat_common_repository.t +++ /dev/null @@ -1,165 +0,0 @@ -#!/usr/bin/env perl -use strict; -use warnings; - -use File::Path qw(make_path); -use File::Temp qw(tempdir); -use FindBin; -use Test::More; - -my $go_xcat = "$FindBin::Bin/../../xCAT-server/share/xcat/tools/go-xcat"; -my $tmpdir = tempdir(CLEANUP => 1); -my $driver = "$tmpdir/driver.sh"; - -open(my $driver_fh, '>', $driver) or die "open $driver: $!"; -print {$driver_fh} <<'DRIVER'; -#!/bin/bash -set -euo pipefail - -function_body=$( - for function_name in \ - add_xcat_dep_common_repo_yum_or_zypper \ - xcat_dep_common_repo_configured \ - refresh_xcat_dep_repository_ids - do - awk -v name="$function_name" ' - $0 == "function " name "()" { copy = 1 } - copy { print } - copy && /^}$/ { exit } - ' "$GO_XCAT_SOURCE" - done -) -eval "$function_body" - -TMP_DIR=$TEST_TMP -GO_XCAT_DEFAULT_BASE_URL=https://repo.example.invalid -GO_XCAT_DEP_REPOSITORY_IDS=(xcat-dep) - -yum() { :; } - -download_file() { - printf '%s\n' "$1" >>"$DOWNLOAD_LOG" - [[ ${COMMON_PRESENT:-0} == 1 ]] || return 1 - : >"$2" -} - -add_repo_by_url_yum_or_zypper() { - printf '%s %s\n' "$1" "$2" >>"$ADD_LOG" -} - -xcat_dep_common_repo_configured() { [[ -s "$ADD_LOG" ]]; } -( add_xcat_dep_common_repo_yum_or_zypper "$@" ) -refresh_xcat_dep_repository_ids -printf '%s\n' "${GO_XCAT_DEP_REPOSITORY_IDS[*]}" >"$ID_LOG" -DRIVER -close($driver_fh) or die "close $driver: $!"; -chmod(0755, $driver) or die "chmod $driver: $!"; - -sub run_case { - my ($name, $present, @arguments) = @_; - my $case_dir = "$tmpdir/$name"; - make_path($case_dir); - local %ENV = ( - %ENV, - ADD_LOG => "$case_dir/add.log", - COMMON_PRESENT => $present, - DOWNLOAD_LOG => "$case_dir/download.log", - GO_XCAT_SOURCE => $go_xcat, - ID_LOG => "$case_dir/id.log", - TEST_TMP => $case_dir, - ); - my $status = system('bash', $driver, @arguments); - return ($status >> 8, $case_dir); -} - -sub read_file { - my ($path) = @_; - return '' unless -f $path; - open(my $fh, '<', $path) or die "open $path: $!"; - my $content = do { local $/; <$fh> }; - close($fh) or die "close $path: $!"; - return $content; -} - -my ($status, $case_dir) = run_case('remote-present', 1, '', 'latest'); -is($status, 0, 'an available common repository is accepted'); -is( - read_file("$case_dir/download.log"), - "https://repo.example.invalid/yum/latest/xcat-dep/common/repodata/repomd.xml\n", - 'the default repository is probed before it is enabled', -); -is( - read_file("$case_dir/add.log"), - "https://repo.example.invalid/yum/latest/xcat-dep/common xcat-dep-common\n", - 'the common repository uses its own repository ID', -); -is(read_file("$case_dir/id.log"), "xcat-dep xcat-dep-common\n", - 'common packages are included in repository listings'); - -($status, $case_dir) = run_case('remote-missing', 0, '', '2.18'); -is($status, 0, 'a release without the common repository remains usable'); -is(read_file("$case_dir/add.log"), '', 'a missing common repository is not enabled'); -is(read_file("$case_dir/id.log"), "xcat-dep\n", - 'legacy package listings remain unchanged when common is absent'); - -($status, $case_dir) = run_case( - 'repo-file', 1, 'https://repo.example.invalid/custom/xcat-dep.repo', 'latest' -); -is($status, 0, 'a custom repository file remains supported'); -is(read_file("$case_dir/download.log"), '', - 'the common URL is not guessed from a custom repository file'); -is(read_file("$case_dir/add.log"), '', - 'a custom repository file does not enable an unrelated repository'); - -my $local_root = "$tmpdir/local-repository"; -make_path("$local_root/common/repodata"); -open(my $repomd_fh, '>', "$local_root/common/repodata/repomd.xml") or die $!; -close($repomd_fh) or die $!; -($status, $case_dir) = run_case('local-present', 0, $local_root, 'latest'); -is($status, 0, 'a local common repository is accepted'); -is( - read_file("$case_dir/add.log"), - "$local_root/common xcat-dep-common\n", - 'the local common repository is enabled beside the distribution repository', -); - -my $template_driver = "$tmpdir/template-driver.sh"; -open(my $template_fh, '>', $template_driver) or die "open $template_driver: $!"; -print {$template_fh} <<'DRIVER'; -#!/bin/bash -set -euo pipefail - -function_body=$( - awk ' - /^function add_repo_by_url_yum_or_zypper\(\)/ { copy = 1 } - copy { print } - copy && /^}$/ { exit } - ' "$GO_XCAT_SOURCE" -) -eval "$function_body" - -TMP_DIR=$TEST_TMP -GO_XCAT_DEFAULT_INSTALL_PATH=/install/xcat -yum() { :; } -add_repo_by_file() { cp "$1" "$REPO_LOG"; } -add_repo_by_url_yum_or_zypper \ - https://repo.example.invalid/xcat-dep/common xcat-dep-common optional -DRIVER -close($template_fh) or die "close $template_driver: $!"; -chmod(0755, $template_driver) or die "chmod $template_driver: $!"; - -my $template_log = "$tmpdir/generated-common.repo"; -local %ENV = ( - %ENV, - GO_XCAT_SOURCE => $go_xcat, - REPO_LOG => $template_log, - TEST_TMP => $tmpdir, -); -$status = system('bash', $template_driver); -is($status >> 8, 0, 'go-xcat can generate the optional common repository'); -like(read_file($template_log), qr/^skip_if_unavailable=1$/m, - 'the generated common repository stays optional during outages'); -like(read_file($template_log), qr/^repo_gpgcheck=1$/m, - 'the generated common repository verifies signed metadata'); - -done_testing(); diff --git a/xCAT-test/unit/go_xcat_el_repo_check.t b/xCAT-test/unit/go_xcat_el_repo_check.t deleted file mode 100644 index f8413b165..000000000 --- a/xCAT-test/unit/go_xcat_el_repo_check.t +++ /dev/null @@ -1,161 +0,0 @@ -#!/usr/bin/env perl -use strict; -use warnings; - -use File::Temp qw(tempdir); -use FindBin; -use Test::More; - -# go-xcat checked for the EPEL and CRB repositories on EL9 only, and only when the version -# carried a minor number, so CentOS Stream was never checked. On EL10 a management node without -# them failed inside dnf install with a dependency error instead of the message that names the -# missing repository, and the CRB message proposed a CentOS Stream repository file with -# signature checks disabled. The probe used dnf list, which an installed copy of the probe -# package satisfies, and which reports a failed query as a missing repository. The check -# functions are taken from the shipped script and run against a dnf stand-in that answers for -# each probe and records what was asked. - -my $go_xcat = "$FindBin::Bin/../../xCAT-server/share/xcat/tools/go-xcat"; -plan skip_all => 'go-xcat not found' unless -r $go_xcat; - -my $tmpdir = tempdir( CLEANUP => 1 ); -my $driver = "$tmpdir/driver.sh"; -my $calls = "$tmpdir/calls"; -open( my $fh, '>', $driver ) or die "open $driver: $!"; -print {$fh} <<'DRIVER'; -#!/bin/bash -set -uo pipefail -eval "$(awk ' - /^function (repo_carries|el9?_epel_and_crb_check|install_packages_(dnf|yum))\(\)/ { copy = 1 } - copy { print } - copy && /^}$/ { copy = 0 } -' "$GO_XCAT_SOURCE")" -EL9_EPEL_TEST_RPM="perl-Crypt-CBC"; EL_EPEL_TEST_RPM="perl-Crypt-CBC" -EL9_CRB_TEST_RPM="perl-IO-Tty"; EL_CRB_TEST_RPM="perl-IO-Tty" -# repoquery prints the name when an enabled repository carries the package and nothing -# otherwise, and fails with a message when the repositories cannot be read. list -q exits 1 -# when neither a repository nor the installed set has the package. -dnf() { - echo "$*" >> "$CALLS" - if [[ ${QUERY_FAIL:-0} == 1 ]]; then - echo "Error: Failed to download metadata for repo 'epel'" >&2 - return 1 - fi - [[ ${QUERY_WARNS:-0} == 1 ]] && echo "Warning: repository 'extras' metadata is stale" >&2 - local has=0 - case "$*" in - *perl-Crypt-CBC*) has="$EPEL_HAS" ;; - *perl-IO-Tty*) has="$CRB_HAS" ;; - esac - # A source repository answers for the name unless the query is limited to binary architectures. - [[ ${SOURCE_ONLY:-0} == 1 && "$*" != *"--arch"* ]] && has=1 - case "$1" in - repoquery) [[ $has == 1 ]] && { echo "${@: -1}"; echo "${@: -1}"; }; return 0 ;; - list) [[ $has == 1 ]] && return 0; return 1 ;; - esac - return 0 -} -yum() { dnf "$@"; } -# ENTRY=dnf or yum runs the installer function go-xcat dispatches to, which owns the check. -case "${ENTRY:-check}" in - dnf) install_packages_dnf -y xCAT ;; - yum) install_packages_yum -y xCAT ;; - *) if declare -F el_epel_and_crb_check >/dev/null; then el_epel_and_crb_check dnf; else el9_epel_and_crb_check dnf; fi ;; -esac -DRIVER -close($fh); - -# Runs the check as go-xcat would on a host of this distro and version. Returns the exit status, -# the output, and the probes the dnf stand-in saw. -sub check { - my (%host) = @_; - unlink $calls; - local $ENV{GO_XCAT_SOURCE} = $go_xcat; - local $ENV{CALLS} = $calls; - local $ENV{GO_XCAT_LINUX_DISTRO} = $host{distro} || 'rocky'; - local $ENV{GO_XCAT_LINUX_VERSION} = $host{version}; - local $ENV{GO_XCAT_ARCH} = 'x86_64'; - local $ENV{EPEL_HAS} = $host{epel} ? 1 : 0; - local $ENV{CRB_HAS} = $host{crb} ? 1 : 0; - local $ENV{QUERY_FAIL} = $host{query_fail} ? 1 : 0; - local $ENV{QUERY_WARNS} = $host{query_warns} ? 1 : 0; - local $ENV{SOURCE_ONLY} = $host{source_only} ? 1 : 0; - local $ENV{ENTRY} = $host{entry} || 'check'; - my $out = `bash '$driver' 2>&1`; - my $rc = $? >> 8; - my @probes; - if ( open( my $cfh, '<', $calls ) ) { chomp( @probes = <$cfh> ); close($cfh); } - return ( $rc, $out, join( ';', @probes ) ); -} - -my ( $rc, $out, $probes ); - -( $rc, $out, $probes ) = check( version => '9.5', epel => 1, crb => 1 ); -is( $rc, 0, 'EL9 with EPEL and CRB passes' ); -like( $probes, qr/perl-Crypt-CBC.*;.*perl-IO-Tty/, '... after probing both repositories' ); -like( $probes, qr/^repoquery /, '... through repoquery, which an installed copy does not satisfy' ); -like( $probes, qr/--arch x86_64,noarch/, '... limited to the binary architectures' ); - -( $rc, $out, $probes ) = check( version => '9.5', epel => 0, crb => 1 ); -is( $rc, 1, 'EL9 without EPEL stops' ); -like( $out, qr/epel-release-latest-9\.noarch/, '... and names the EL9 EPEL release package' ); - -( $rc, $out, $probes ) = check( version => '10.2', epel => 1, crb => 1 ); -is( $rc, 0, 'EL10 with EPEL and CRB passes' ); -like( $probes, qr/perl-Crypt-CBC.*;.*perl-IO-Tty/, '... after probing both repositories' ); - -( $rc, $out, $probes ) = check( version => '10.2', epel => 0, crb => 1 ); -is( $rc, 1, 'EL10 without EPEL stops' ); -like( $out, qr/requires EPEL repository/, '... and names the missing repository' ); -like( $out, qr/epel-release-latest-10\.noarch/, '... and the EL10 EPEL release package' ); - -( $rc, $out, $probes ) = check( distro => 'rhel', version => '10.2', epel => 1, crb => 0 ); -is( $rc, 1, 'EL10 without CRB stops' ); -like( $out, qr/requires CRB repository/, '... and names the missing repository' ); -like( $out, qr/'dnf update epel-release' and then 'crb enable'/, '... with a current crb helper, which covers RHEL under RHSM and RHUI' ); -unlike( $out, qr/gpgcheck=0|centos-crb|subscription-manager/, '... and no repository file or subscription-only command' ); - -( $rc, $out, $probes ) = check( version => '10.2', epel => 0, crb => 1, source_only => 1 ); -is( $rc, 1, 'a source repository does not stand in for the binary one' ); -like( $out, qr/requires EPEL repository/, '... so the missing repository is still reported' ); - -( $rc, $out, $probes ) = check( distro => 'ol', version => '10.1', epel => 1, crb => 0 ); -is( $rc, 1, 'Oracle Linux 10 without CRB stops' ); -like( $out, qr/dnf config-manager --enable ol10_codeready_builder/, '... with the command that enables its CodeReady Builder' ); - -( $rc, $out, $probes ) = check( version => '10.2', epel => 1, crb => 1, query_fail => 1 ); -is( $rc, 1, 'a failed repository query stops' ); -like( $out, qr/Failed to download metadata/, '... with the package manager error' ); -unlike( $out, qr/requires EPEL repository/, '... and not as a missing repository' ); - -( $rc, $out, $probes ) = check( version => '10.2', epel => 0, crb => 1, query_warns => 1 ); -is( $rc, 1, 'a warning on stderr does not stand in for a package' ); -like( $out, qr/requires EPEL repository/, '... so the missing repository is still reported' ); - -( $rc, $out, $probes ) = check( version => '10.2', epel => 1, crb => 1, query_warns => 1 ); -is( $rc, 0, 'a warning beside a real match does not fail the check' ); - -( $rc, $out, $probes ) = check( distro => 'centos', version => '10', epel => 0, crb => 0 ); -is( $rc, 1, 'CentOS Stream 10, which reports the major version alone, is checked' ); -like( $out, qr/requires EPEL repository/, '... and told about EPEL' ); - -( $rc, $out, $probes ) = check( version => '8.10', epel => 0, crb => 0 ); -is( $rc, 0, 'EL8 is not checked' ); -is( $probes, '', '... and nothing is probed' ); - -( $rc, $out, $probes ) = check( distro => 'fedora', version => '42', epel => 0, crb => 0 ); -is( $rc, 0, 'Fedora is not checked' ); - -# The installer functions go-xcat dispatches to run the check before they install anything. -foreach my $entry (qw(dnf yum)) { - ( $rc, $out, $probes ) = check( entry => $entry, version => '10.2', epel => 0, crb => 1 ); - is( $rc, 1, "install_packages_$entry on EL10 without EPEL stops" ); - like( $out, qr/requires EPEL repository/, '... with the EPEL message' ); - unlike( $probes, qr/install/, '... before anything is installed' ); - - ( $rc, $out, $probes ) = check( entry => $entry, version => '10.2', epel => 1, crb => 1 ); - is( $rc, 0, "install_packages_$entry on EL10 with EPEL and CRB installs" ); - like( $probes, qr/perl-IO-Tty.*;.*install initscripts.*;.*install xCAT/, '... after the check passed' ); -} - -done_testing(); diff --git a/xCAT-test/unit/post_xcat_download_policy.t b/xCAT-test/unit/post_xcat_download_policy.t deleted file mode 100644 index 7cbf70492..000000000 --- a/xCAT-test/unit/post_xcat_download_policy.t +++ /dev/null @@ -1,48 +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 %scripts = ( - 'legacy install post.xcat' => File::Spec->catfile( - $repo_root, - 'xCAT-server/share/xcat/install/scripts/post.xcat' - ), - 'legacy netboot xcatdsklspost' => File::Spec->catfile( - $repo_root, - 'xCAT/postscripts/xcatdsklspost' - ), -); - -foreach my $path ( values %scripts ) { - plan skip_all => "$path not found" unless -r $path; -} - -foreach my $name ( sort keys %scripts ) { - my $path = $scripts{$name}; - open( my $fh, '<', $path ) or die "Unable to read $path: $!"; - my $script = do { local $/; <$fh> }; - close($fh); - - like( - $script, - qr/--reject\s+"index\.html\*,post\.xcat\.ng,post\.xcat\.rhels10"/, - "$name recursive wget ignores dispatcher scripts that contain literal HTML-link regexes" - ); - like( - $script, - qr/Newer\s+(?:#\s+)?wget\s+parses\s+HTML-looking\s+regex\s+strings/s, - "$name download policy documents why dispatcher scripts are excluded" - ); - unlike( - $script, - qr/catfile( $FindBin::Bin, '..', '..', - 'xCAT', 'postscripts', 'remoteshell' ); -plan skip_all => 'remoteshell not found' unless -r $script; - -open( my $fh, '<', $script ) or die "Unable to read $script: $!"; -my $source = do { local $/; <$fh> }; -close($fh); - -# A kill without the hyphen reads the signal number as one more process id, so -# the target gets the default signal, which a process can catch, and the -# process with that id is signalled as well. -my @bare = ( $source =~ /^[^#\n]*\bkill\s+\d/gm ); -is( scalar(@bare), 0, 'no kill gives the signal number without a hyphen' ); - -like( $source, qr/kill -9 \$PIDLIST/, - 'the ssh daemon gets the signal that a process cannot catch' ); - -done_testing(); diff --git a/xCAT-test/unit/remoteshell_kill_wait.t b/xCAT-test/unit/remoteshell_kill_wait.t deleted file mode 100644 index 505fd2418..000000000 --- a/xCAT-test/unit/remoteshell_kill_wait.t +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env perl -use strict; -use warnings; - -use File::Spec; -use FindBin; -use Test::More; - -my $script = File::Spec->catfile( $FindBin::Bin, '..', '..', - 'xCAT', 'postscripts', 'remoteshell' ); -plan skip_all => 'remoteshell not found' unless -r $script; -plan skip_all => 'no bash' unless -x '/bin/bash'; - -open( my $fh, '<', $script ) or die "Unable to read $script: $!"; -my $source = do { local $/; <$fh> }; -close($fh); - -my ($loop) = $source =~ /(waited=0\n.*?\n done)/s; -ok( defined($loop), 'the wait loop was found in the postscript' ); - -# Run the loop of the postscript against real processes, with a shorter bound -# so that the test does not spend the whole timeout of the postscript. -sub waits_for { - my (@pids) = @_; - ( my $body = $loop ) =~ s/\$waited -lt 10/\$waited -lt 3/; - my $list = join( ' ', @pids ); - my $out = `/bin/bash -c 'PIDLIST="$list"\n$body\necho \$alive' 2>/dev/null`; - chomp $out; - return $out; -} - -# A process that is still running must be reported as alive, and the loop must -# give up rather than run for ever. -my $child = fork(); -if ( !defined $child ) { plan skip_all => 'cannot fork' } -if ( $child == 0 ) { exec( 'sleep', '30' ); exit 1 } -my $start = time; -is( waits_for($child), '1', 'a process that is still running is reported alive' ); -cmp_ok( time - $start, '<', 10, 'the loop gives up instead of waiting for ever' ); -kill 'KILL', $child; -waitpid( $child, 0 ); - -# A process that has ended must be reported as gone, without waiting. -my $gone = fork(); -if ( $gone == 0 ) { exit 0 } -waitpid( $gone, 0 ); -$start = time; -is( waits_for($gone), '0', 'a process that has ended is reported gone' ); -cmp_ok( time - $start, '<', 3, 'no time is spent once the process is gone' ); - -# The bound of the postscript itself, and the shape of the test. -like( $source, qr/while \[ \$waited -lt 10 \]/, 'the postscript waits at most ten seconds' ); -like( $source, qr/kill -0 \$pid/, 'the check for a running process sends no signal' ); -like( $source, qr/kill -9 \$PIDLIST/, 'the daemon still gets the signal it cannot catch' ); - -# The wait has to happen before the new daemon starts. -my ($block) = $source =~ /(PIDLIST=.*?\/usr\/sbin\/sshd)/s; -ok( defined($block), 'the fallback block was found' ); -cmp_ok( index( $block, 'waited=0' ), '<', index( $block, '/usr/sbin/sshd' ), - 'the wait comes before the new daemon starts' ); - -done_testing(); diff --git a/xCAT-test/unit/sles_pre_script.t b/xCAT-test/unit/sles_pre_script.t deleted file mode 100644 index 1ffa01eea..000000000 --- a/xCAT-test/unit/sles_pre_script.t +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env perl -use strict; -use warnings; -use Test::More; - -my $pre_sles_path = defined $ENV{XCATROOT} ? "$ENV{XCATROOT}/share/xcat/install/scripts/pre.sles" : ''; -$pre_sles_path = "xCAT-server/share/xcat/install/scripts/pre.sles" - unless -f $pre_sles_path; - -plan skip_all => "pre.sles not found" unless -f $pre_sles_path; - -my $src = do { local $/; open my $fh, '<', $pre_sles_path or die $!; <$fh> }; - -like($src, qr/sub set_sles11_uefi_bootloader\b|set_sles11_uefi_bootloader\(\)/, 'SLES UEFI bootloader helper exists'); -like($src, qr/install=\.\*sles11/, 'SLES 11 UEFI bootloader change is scoped to SLES 11 install media'); -like($src, qr/elilo<\/loader_type>/, 'SLES 11 UEFI install selects elilo'); -like($src, qr/mbr<\/location>/, 'legacy MBR template value is replaced at install time'); -like($src, qr/if \[ -d \/sys\/firmware\/efi \]; then\s+sed .*?set_sles11_uefi_bootloader/s, 'UEFI default partitioning applies bootloader helper'); - -done_testing(); diff --git a/xCAT-test/unit/statelite_add_ssh.t b/xCAT-test/unit/statelite_add_ssh.t deleted file mode 100644 index b3dc6ddbe..000000000 --- a/xCAT-test/unit/statelite_add_ssh.t +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env perl -use strict; -use warnings; -use Test::More; - -my $script_path = defined $ENV{XCATROOT} ? "$ENV{XCATROOT}/share/xcat/netboot/add-on/statelite/add_ssh" : ''; -$script_path = "xCAT-server/share/xcat/netboot/add-on/statelite/add_ssh" - unless -f $script_path; - -plan skip_all => "add_ssh not found" unless -f $script_path; - -my $script = do { local $/; open my $fh, '<', $script_path or die $!; <$fh> }; - -like($script, qr/MaxStartups 100:30:200/, 'add_ssh caps MaxStartups below systemd soft nofile limit'); -unlike($script, qr/echo\s+"MaxStartups 1024"/, 'add_ssh does not force MaxStartups 1024'); - -done_testing(); diff --git a/xCAT/postscripts/remoteshell b/xCAT/postscripts/remoteshell index 3850585f9..04fc145b4 100755 --- a/xCAT/postscripts/remoteshell +++ b/xCAT/postscripts/remoteshell @@ -631,20 +631,6 @@ fi #if the service restart with "service/systemctl" failed #try to kill the process and start if [ "$?" != "0" ];then - PIDLIST=`ps aux | grep -v grep | grep "/usr/sbin/sshd"|awk -F" " '{print $2}'|xargs` - if [ -n "$PIDLIST" ]; then - kill -9 $PIDLIST - waited=0 - while [ $waited -lt 10 ]; do - alive=0 - for pid in $PIDLIST; do - if kill -0 $pid 2>/dev/null; then alive=1; fi - done - if [ $alive -eq 0 ]; then break; fi - sleep 1 - waited=`expr $waited + 1` - done - fi - /usr/sbin/sshd + xcat_restart_sshd_after_failed_service_restart fi kill -9 $CREDPID diff --git a/xCAT/postscripts/xcatdsklspost b/xCAT/postscripts/xcatdsklspost index 13110575d..6e2ad718e 100755 --- a/xCAT/postscripts/xcatdsklspost +++ b/xCAT/postscripts/xcatdsklspost @@ -19,7 +19,12 @@ # ##################################################### -[ -f "/xcatpost/xcatlib.sh" ] && . /xcatpost/xcatlib.sh +if [ -f "/xcatpost/xcatlib.sh" ]; then + . /xcatpost/xcatlib.sh +else + str_dir_name=`dirname $0` + [ -f "$str_dir_name/xcatlib.sh" ] && . "$str_dir_name/xcatlib.sh" +fi if [ -f /xcatpost/mypostscript.post ]; then XCATDEBUGMODE=`grep 'XCATDEBUGMODE=' /xcatpost/mypostscript.post | cut -d= -f2 | tr -d \'\" | tr A-Z a-z` @@ -83,66 +88,6 @@ echolog() } -download_postscripts() -{ - server=$1 - if [ -z $server ]; then - return 1; - fi - - # Do not override the parameter --installdir - if [ -z "$INSTALLDIR" ]; then - if [ -f /opt/xcat/xcatinfo ]; then - INSTALLDIR=`grep 'INSTALLDIR' /opt/xcat/xcatinfo |cut -d= -f2` - fi - if [ -z "$INSTALLDIR" ]; then - INSTALLDIR="/install" - fi - fi - echolog "debug" "trying to download postscripts from http://$server$INSTALLDIR/postscripts/" - max_retries=5 - retry=0 - rc=1 # this is a fail return - while [ 0 -eq 0 ]; do - if [ -e "$xcatpost" ]; then - rm -rf "$xcatpost" - fi - - # These dispatcher scripts are not needed by the legacy netboot post - # path. Newer wget parses HTML-looking regex strings inside downloaded - # scripts and fails the whole recursive download on bogus URLs. - export LANG=C; wget -l inf -nH -N -r --waitretry=10 --random-wait -e robots=off -T 60 -nH --cut-dirs=2 --reject "index.html*,post.xcat.ng,post.xcat.rhels10" --no-parent http://$server$INSTALLDIR/postscripts/ -P /$xcatpost 2> /tmp/wget.log - rc=$? - if [ $rc -eq 0 ]; then - # return from wget was 0 but some OS do not return errors, so we - # have additional checks for - # failed: Connection httpd not running - # 404: Not Found - if directory does not exist - grep -i -E "... failed: Connection refused.$" /tmp/wget.log - rc1=$? - grep -i -E "ERROR 404: Not Found.$" /tmp/wget.log - rc2=$? - # check to see no errors at all, grep returns 1 - if [ $rc1 -eq 1 ] && [ $rc2 -eq 1 ]; then - echolog "debug" "postscripts are downloaded from $server successfully." - return 0 - fi - fi - - retry=$(($retry+1)) - echolog "debug" "download_postscripts retry $retry" - if [ $retry -eq $max_retries ]; then - echolog "debug" "failed to download postscripts from http://$server$INSTALLDIR/postscripts/ after several retries." - break - fi - - SLI=$(awk 'BEGIN{srand(); printf("%d\n",rand()*20)}') - sleep $SLI - done - return $rc -} - - download_mypostscript() { server=$1 diff --git a/xCAT/postscripts/xcatlib.sh b/xCAT/postscripts/xcatlib.sh index b06838589..8e2dd1431 100755 --- a/xCAT/postscripts/xcatlib.sh +++ b/xCAT/postscripts/xcatlib.sh @@ -833,6 +833,107 @@ function msgutil { msgutil_r "" "$@" } +function xcat_download_postscripts { + local server="$1" + local install_dir="${2:-${INSTALLDIR:-/install}}" + local postroot="${3:-/$xcatpost}" + local log_file="${4:-/tmp/wget.log}" + + export LANG=C + wget -l inf -nH -N -r --waitretry=10 --random-wait -e robots=off -T 60 -nH --cut-dirs=2 --reject "index.html*,post.xcat.ng,post.xcat.rhels10" --no-parent "http://$server$install_dir/postscripts/" -P "$postroot" 2> "$log_file" +} + +function download_postscripts { + server=$1 + if [ -z $server ]; then + return 1; + fi + + # Do not override the parameter --installdir + if [ -z "$INSTALLDIR" ]; then + if [ -f /opt/xcat/xcatinfo ]; then + INSTALLDIR=`grep 'INSTALLDIR' /opt/xcat/xcatinfo |cut -d= -f2` + fi + if [ -z "$INSTALLDIR" ]; then + INSTALLDIR="/install" + fi + fi + echolog "debug" "trying to download postscripts from http://$server$INSTALLDIR/postscripts/" + max_retries=5 + retry=0 + rc=1 # this is a fail return + while [ 0 -eq 0 ]; do + if [ -e "$xcatpost" ]; then + rm -rf "$xcatpost" + fi + + xcat_download_postscripts "$server" "$INSTALLDIR" "/$xcatpost" "/tmp/wget.log" + rc=$? + if [ $rc -eq 0 ]; then + # return from wget was 0 but some OS do not return errors, so we + # have additional checks for + # failed: Connection httpd not running + # 404: Not Found - if directory does not exist + grep -i -E "... failed: Connection refused.$" /tmp/wget.log + rc1=$? + grep -i -E "ERROR 404: Not Found.$" /tmp/wget.log + rc2=$? + # check to see no errors at all, grep returns 1 + if [ $rc1 -eq 1 ] && [ $rc2 -eq 1 ]; then + echolog "debug" "postscripts are downloaded from $server successfully." + return 0 + fi + fi + + retry=$(($retry+1)) + echolog "debug" "download_postscripts retry $retry" + if [ $retry -eq $max_retries ]; then + echolog "debug" "failed to download postscripts from http://$server$INSTALLDIR/postscripts/ after several retries." + break + fi + + SLI=$(awk 'BEGIN{srand(); printf("%d\n",rand()*20)}') + sleep $SLI + done + return $rc +} + +function xcat_wait_for_processes_to_exit { + local pidlist="$1" + local max_wait="${2:-10}" + local waited=0 + local alive + local pid + + while [ $waited -lt $max_wait ]; do + alive=0 + for pid in $pidlist; do + if kill -0 $pid 2>/dev/null; then + alive=1 + fi + done + if [ $alive -eq 0 ]; then + return 0 + fi + sleep 1 + waited=`expr $waited + 1` + done + + return 1 +} + +function xcat_restart_sshd_after_failed_service_restart { + local sshd_cmd="${1:-/usr/sbin/sshd}" + local PIDLIST + + PIDLIST=`ps aux | grep -v grep | grep "/usr/sbin/sshd"|awk -F" " '{print $2}'|xargs` + if [ -n "$PIDLIST" ]; then + kill -9 $PIDLIST + xcat_wait_for_processes_to_exit "$PIDLIST" + fi + $sshd_cmd +} + function fetch_mypostscript { local postroot postroot=$1 From 58c030a8f38c178fc9bbda7bdbae0e7d83069fb0 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Tue, 8 Sep 2026 13:03:34 -0300 Subject: [PATCH 02/62] test(xcat-core): Move BATS tests beside unit tests Shell unit tests were introduced under xCAT-test/autotest/bats, but the existing source-tree unit suite already lives directly under xCAT-test/unit. Keeping the BATS suite under xCAT-test/bats makes the unit-test layout consistent and keeps autotest reserved for xcattest-driven functional cases. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- github_action_xcat_test.pl | 8 ++++---- xCAT-test/README.md | 7 ++++--- xCAT-test/{autotest => }/bats/README.md | 4 ++-- xCAT-test/{autotest => }/bats/genesis_ib_modules.bats | 0 .../{autotest => }/bats/go_xcat_common_repository.bats | 0 xCAT-test/{autotest => }/bats/go_xcat_el_repo_check.bats | 0 xCAT-test/{autotest => }/bats/helpers/go_xcat.bash | 2 +- xCAT-test/{autotest => }/bats/helpers/shell_source.bash | 2 +- .../{autotest => }/bats/post_xcat_download_policy.bats | 0 xCAT-test/{autotest => }/bats/remoteshell_restart.bats | 0 xCAT-test/{autotest => }/bats/sles_pre_script.bats | 0 xCAT-test/{autotest => }/bats/statelite_add_ssh.bats | 0 xCAT-test/unit/README.md | 2 +- 13 files changed, 13 insertions(+), 12 deletions(-) rename xCAT-test/{autotest => }/bats/README.md (93%) rename xCAT-test/{autotest => }/bats/genesis_ib_modules.bats (100%) rename xCAT-test/{autotest => }/bats/go_xcat_common_repository.bats (100%) rename xCAT-test/{autotest => }/bats/go_xcat_el_repo_check.bats (100%) rename xCAT-test/{autotest => }/bats/helpers/go_xcat.bash (93%) rename xCAT-test/{autotest => }/bats/helpers/shell_source.bash (98%) rename xCAT-test/{autotest => }/bats/post_xcat_download_policy.bats (100%) rename xCAT-test/{autotest => }/bats/remoteshell_restart.bats (100%) rename xCAT-test/{autotest => }/bats/sles_pre_script.bats (100%) rename xCAT-test/{autotest => }/bats/statelite_add_ssh.bats (100%) diff --git a/github_action_xcat_test.pl b/github_action_xcat_test.pl index 42d565885..4a47d4c7d 100644 --- a/github_action_xcat_test.pl +++ b/github_action_xcat_test.pl @@ -313,7 +313,7 @@ sub preserve_source_tree{ @output = runcmd("find $unitsrc/xCAT-test/unit -name '*.t' | wc -l"); my $perl_count = $output[0]; - @output = runcmd("find $unitsrc/xCAT-test/autotest/bats -name '*.bats' 2>/dev/null | wc -l"); + @output = runcmd("find $unitsrc/xCAT-test/bats -name '*.bats' 2>/dev/null | wc -l"); my $bats_count = $output[0]; print "[preserve_source_tree] preserved $srcdir in $unitsrc ($perl_count Perl unit tests, $bats_count BATS tests)\n"; return 0; @@ -471,21 +471,21 @@ sub run_unit_tests{ #-------------------------------------------------------- # Fuction name: run_bats_tests -# Description: Run shell-script unit tests under xCAT-test/autotest/bats. +# Description: Run shell-script unit tests under xCAT-test/bats. # Runs against the pre-build copy of the source tree taken by # preserve_source_tree(), like the Perl unit tests. # Attributes: # Return code: 0 all tests passed, 1 otherwise #-------------------------------------------------------- sub run_bats_tests{ - my $testdir = "$unitsrc/xCAT-test/autotest/bats"; + my $testdir = "$unitsrc/xCAT-test/bats"; my @output = runcmd("find $testdir -name '*.bats' -print -quit 2>/dev/null"); if (!@output) { print "[run_bats_tests] no BATS tests found under $testdir\n"; return 0; } - my $cmd = "cd $unitsrc && bats -r xCAT-test/autotest/bats"; + my $cmd = "cd $unitsrc && bats -r xCAT-test/bats"; print "[run_bats_tests] running $cmd\n"; @output = runcmd("$cmd"); print Dumper \@output; diff --git a/xCAT-test/README.md b/xCAT-test/README.md index 7406be0d4..01fbe54d2 100644 --- a/xCAT-test/README.md +++ b/xCAT-test/README.md @@ -6,7 +6,8 @@ language: | Test type | Location | Runner | | --------- | -------- | ------ | | Perl unit tests | `xCAT-test/unit/*.t` | `prove -r xCAT-test/unit` | -| Shell unit tests | `xCAT-test/autotest/bats/*.bats` | `bats -r xCAT-test/autotest/bats` | +| Shell unit tests | `xCAT-test/bats/*.bats` | `bats -r xCAT-test/bats` | +| CLI functional tests | `xCAT-test/autotest/testcase/` and `xCAT-test/autotest/bundle/` | `xcattest -f -t ` or `xcattest -f -b ` | Use Perl `.t` tests for Perl modules, Perl scripts, templates, and repository artifacts. Use BATS tests for shell-script behavior that can be exercised from @@ -14,7 +15,7 @@ the checkout by sourcing a shell library or script and shadowing external commands. Shell behavior should not be tested by Perl tests that grep shell source. Put -those tests under `xCAT-test/autotest/bats` instead. +those tests under `xCAT-test/bats` instead. -See `unit/README.md` and `autotest/bats/README.md` for the detailed rules for +See `unit/README.md` and `bats/README.md` for the detailed rules for each unit-test suite. diff --git a/xCAT-test/autotest/bats/README.md b/xCAT-test/bats/README.md similarity index 93% rename from xCAT-test/autotest/bats/README.md rename to xCAT-test/bats/README.md index 29a1403e8..ac65379ab 100644 --- a/xCAT-test/autotest/bats/README.md +++ b/xCAT-test/bats/README.md @@ -1,9 +1,9 @@ -# xCAT-test/autotest/bats +# xCAT-test/bats Shell-script unit tests live here and run with: ```bash -bats -r xCAT-test/autotest/bats +bats -r xCAT-test/bats ``` The GitHub Actions `xcat_test` workflow runs this command after the Perl `.t` diff --git a/xCAT-test/autotest/bats/genesis_ib_modules.bats b/xCAT-test/bats/genesis_ib_modules.bats similarity index 100% rename from xCAT-test/autotest/bats/genesis_ib_modules.bats rename to xCAT-test/bats/genesis_ib_modules.bats diff --git a/xCAT-test/autotest/bats/go_xcat_common_repository.bats b/xCAT-test/bats/go_xcat_common_repository.bats similarity index 100% rename from xCAT-test/autotest/bats/go_xcat_common_repository.bats rename to xCAT-test/bats/go_xcat_common_repository.bats diff --git a/xCAT-test/autotest/bats/go_xcat_el_repo_check.bats b/xCAT-test/bats/go_xcat_el_repo_check.bats similarity index 100% rename from xCAT-test/autotest/bats/go_xcat_el_repo_check.bats rename to xCAT-test/bats/go_xcat_el_repo_check.bats diff --git a/xCAT-test/autotest/bats/helpers/go_xcat.bash b/xCAT-test/bats/helpers/go_xcat.bash similarity index 93% rename from xCAT-test/autotest/bats/helpers/go_xcat.bash rename to xCAT-test/bats/helpers/go_xcat.bash index b863c963f..ee4ec3c44 100644 --- a/xCAT-test/autotest/bats/helpers/go_xcat.bash +++ b/xCAT-test/bats/helpers/go_xcat.bash @@ -2,7 +2,7 @@ go_xcat_default_source() { - printf '%s\n' "${BATS_TEST_DIRNAME}/../../../xCAT-server/share/xcat/tools/go-xcat" + printf '%s\n' "${BATS_TEST_DIRNAME}/../../xCAT-server/share/xcat/tools/go-xcat" } go_xcat_require_source() diff --git a/xCAT-test/autotest/bats/helpers/shell_source.bash b/xCAT-test/bats/helpers/shell_source.bash similarity index 98% rename from xCAT-test/autotest/bats/helpers/shell_source.bash rename to xCAT-test/bats/helpers/shell_source.bash index f76ed55d3..61a0bea8d 100644 --- a/xCAT-test/autotest/bats/helpers/shell_source.bash +++ b/xCAT-test/bats/helpers/shell_source.bash @@ -2,7 +2,7 @@ repo_root() { - printf '%s\n' "${BATS_TEST_DIRNAME}/../../.." + printf '%s\n' "${BATS_TEST_DIRNAME}/../.." } repo_path() diff --git a/xCAT-test/autotest/bats/post_xcat_download_policy.bats b/xCAT-test/bats/post_xcat_download_policy.bats similarity index 100% rename from xCAT-test/autotest/bats/post_xcat_download_policy.bats rename to xCAT-test/bats/post_xcat_download_policy.bats diff --git a/xCAT-test/autotest/bats/remoteshell_restart.bats b/xCAT-test/bats/remoteshell_restart.bats similarity index 100% rename from xCAT-test/autotest/bats/remoteshell_restart.bats rename to xCAT-test/bats/remoteshell_restart.bats diff --git a/xCAT-test/autotest/bats/sles_pre_script.bats b/xCAT-test/bats/sles_pre_script.bats similarity index 100% rename from xCAT-test/autotest/bats/sles_pre_script.bats rename to xCAT-test/bats/sles_pre_script.bats diff --git a/xCAT-test/autotest/bats/statelite_add_ssh.bats b/xCAT-test/bats/statelite_add_ssh.bats similarity index 100% rename from xCAT-test/autotest/bats/statelite_add_ssh.bats rename to xCAT-test/bats/statelite_add_ssh.bats diff --git a/xCAT-test/unit/README.md b/xCAT-test/unit/README.md index 2fd67bcb6..daff2691c 100644 --- a/xCAT-test/unit/README.md +++ b/xCAT-test/unit/README.md @@ -46,7 +46,7 @@ so putting a test in `integration/` does not cost it CI coverage. What differs i each suite is allowed to depend on, and that unit tests also run standalone from a bare checkout with no xCAT at all. -Shell-script unit tests belong in [`../autotest/bats`](../autotest/bats/README.md) +Shell-script unit tests belong in [`../bats`](../bats/README.md) and run with BATS. Do not add Perl `.t` tests that grep shell source when the behavior can be exercised by sourcing a shell library or script and shadowing the external commands it calls. From cc36d25c354dc444e0ecbae131c4a5cf2111a8b8 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Tue, 8 Sep 2026 13:10:13 -0300 Subject: [PATCH 03/62] test(xcat-core): Keep xcatdsklspost download local xcatdsklspost can run before xcatlib.sh is available beside it in stateless and statelite image contexts. Moving download_postscripts into xcatlib.sh could leave the legacy postscript without its callee when it is copied by itself into the image. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- xCAT-test/bats/post_xcat_download_policy.bats | 14 ++-- xCAT/postscripts/xcatdsklspost | 64 ++++++++++++++++++ xCAT/postscripts/xcatlib.sh | 65 ------------------- 3 files changed, 71 insertions(+), 72 deletions(-) diff --git a/xCAT-test/bats/post_xcat_download_policy.bats b/xCAT-test/bats/post_xcat_download_policy.bats index 4ec06387a..ec643410c 100644 --- a/xCAT-test/bats/post_xcat_download_policy.bats +++ b/xCAT-test/bats/post_xcat_download_policy.bats @@ -5,10 +5,10 @@ load 'helpers/shell_source' setup() { SCRIPT_LIB="$(repo_path 'xCAT-server/share/xcat/install/scripts/scriptlib')" - XCATLIB="$(repo_path 'xCAT/postscripts/xcatlib.sh')" + XCATDSKLSPOST="$(repo_path 'xCAT/postscripts/xcatdsklspost')" [ -r "$SCRIPT_LIB" ] || skip "$SCRIPT_LIB is required" - [ -r "$XCATLIB" ] || skip "$XCATLIB is required" - export SCRIPT_LIB XCATLIB + [ -r "$XCATDSKLSPOST" ] || skip "$XCATDSKLSPOST is required" + export SCRIPT_LIB XCATDSKLSPOST } capture_install_scriptlib_wget() @@ -25,7 +25,7 @@ capture_install_scriptlib_wget() xcat_download_postscripts "192.0.2.10:80" "/install" "/xcatpost" "$wget_log" } -capture_xcatlib_wget() +capture_xcatdsklspost_wget() { local wget_log="$1" @@ -45,7 +45,7 @@ capture_xcatlib_wget() return 0 } - source "$XCATLIB" + XCATDSKLSPOST_SOURCE_ONLY=1 source "$XCATDSKLSPOST" download_postscripts 192.0.2.10:80 } @@ -67,10 +67,10 @@ assert_download_policy() assert_download_policy "$(read_file_or_empty "$wget_log")" } -@test "postscript xcatlib recursive download rejects dispatcher scripts" { +@test "xcatdsklspost recursive download rejects dispatcher scripts" { local wget_log="${BATS_TEST_TMPDIR}/xcatdsklspost-wget.log" - run capture_xcatlib_wget "$wget_log" + run capture_xcatdsklspost_wget "$wget_log" [ "$status" -eq 0 ] assert_download_policy "$(read_file_or_empty "$wget_log")" } diff --git a/xCAT/postscripts/xcatdsklspost b/xCAT/postscripts/xcatdsklspost index 6e2ad718e..40d1dde3d 100755 --- a/xCAT/postscripts/xcatdsklspost +++ b/xCAT/postscripts/xcatdsklspost @@ -127,6 +127,66 @@ download_mypostscript() } +download_postscripts() +{ + server=$1 + if [ -z $server ]; then + return 1; + fi + + # Do not override the parameter --installdir + if [ -z "$INSTALLDIR" ]; then + if [ -f /opt/xcat/xcatinfo ]; then + INSTALLDIR=`grep 'INSTALLDIR' /opt/xcat/xcatinfo |cut -d= -f2` + fi + if [ -z "$INSTALLDIR" ]; then + INSTALLDIR="/install" + fi + fi + echolog "debug" "trying to download postscripts from http://$server$INSTALLDIR/postscripts/" + max_retries=5 + retry=0 + rc=1 # this is a fail return + while [ 0 -eq 0 ]; do + if [ -e "$xcatpost" ]; then + rm -rf "$xcatpost" + fi + + # These dispatcher scripts are not needed by the legacy netboot post + # path. Newer wget parses HTML-looking regex strings inside downloaded + # scripts and fails the whole recursive download on bogus URLs. + export LANG=C; wget -l inf -nH -N -r --waitretry=10 --random-wait -e robots=off -T 60 -nH --cut-dirs=2 --reject "index.html*,post.xcat.ng,post.xcat.rhels10" --no-parent http://$server$INSTALLDIR/postscripts/ -P /$xcatpost 2> /tmp/wget.log + rc=$? + if [ $rc -eq 0 ]; then + # return from wget was 0 but some OS do not return errors, so we + # have additional checks for + # failed: Connection httpd not running + # 404: Not Found - if directory does not exist + grep -i -E "... failed: Connection refused.$" /tmp/wget.log + rc1=$? + grep -i -E "ERROR 404: Not Found.$" /tmp/wget.log + rc2=$? + # check to see no errors at all, grep returns 1 + if [ $rc1 -eq 1 ] && [ $rc2 -eq 1 ]; then + echolog "debug" "postscripts are downloaded from $server successfully." + return 0 + fi + fi + + retry=$(($retry+1)) + echolog "debug" "download_postscripts retry $retry" + if [ $retry -eq $max_retries ]; then + echolog "debug" "failed to download postscripts from http://$server$INSTALLDIR/postscripts/ after several retries." + break + fi + + SLI=$(awk 'BEGIN{srand(); printf("%d\n",rand()*20)}') + sleep $SLI + done + return $rc +} + + # pmatch determines if 1st argument string is matched by 2nd argument pattern pmatch () @@ -161,6 +221,10 @@ parsehttpserver () fi } +if [ "$XCATDSKLSPOST_SOURCE_ONLY" = "1" ]; then + return 0 2>/dev/null || exit 0 +fi + # Main # parse the arguments log_label="xcat.updatenode" diff --git a/xCAT/postscripts/xcatlib.sh b/xCAT/postscripts/xcatlib.sh index 8e2dd1431..6dd27ded5 100755 --- a/xCAT/postscripts/xcatlib.sh +++ b/xCAT/postscripts/xcatlib.sh @@ -833,71 +833,6 @@ function msgutil { msgutil_r "" "$@" } -function xcat_download_postscripts { - local server="$1" - local install_dir="${2:-${INSTALLDIR:-/install}}" - local postroot="${3:-/$xcatpost}" - local log_file="${4:-/tmp/wget.log}" - - export LANG=C - wget -l inf -nH -N -r --waitretry=10 --random-wait -e robots=off -T 60 -nH --cut-dirs=2 --reject "index.html*,post.xcat.ng,post.xcat.rhels10" --no-parent "http://$server$install_dir/postscripts/" -P "$postroot" 2> "$log_file" -} - -function download_postscripts { - server=$1 - if [ -z $server ]; then - return 1; - fi - - # Do not override the parameter --installdir - if [ -z "$INSTALLDIR" ]; then - if [ -f /opt/xcat/xcatinfo ]; then - INSTALLDIR=`grep 'INSTALLDIR' /opt/xcat/xcatinfo |cut -d= -f2` - fi - if [ -z "$INSTALLDIR" ]; then - INSTALLDIR="/install" - fi - fi - echolog "debug" "trying to download postscripts from http://$server$INSTALLDIR/postscripts/" - max_retries=5 - retry=0 - rc=1 # this is a fail return - while [ 0 -eq 0 ]; do - if [ -e "$xcatpost" ]; then - rm -rf "$xcatpost" - fi - - xcat_download_postscripts "$server" "$INSTALLDIR" "/$xcatpost" "/tmp/wget.log" - rc=$? - if [ $rc -eq 0 ]; then - # return from wget was 0 but some OS do not return errors, so we - # have additional checks for - # failed: Connection httpd not running - # 404: Not Found - if directory does not exist - grep -i -E "... failed: Connection refused.$" /tmp/wget.log - rc1=$? - grep -i -E "ERROR 404: Not Found.$" /tmp/wget.log - rc2=$? - # check to see no errors at all, grep returns 1 - if [ $rc1 -eq 1 ] && [ $rc2 -eq 1 ]; then - echolog "debug" "postscripts are downloaded from $server successfully." - return 0 - fi - fi - - retry=$(($retry+1)) - echolog "debug" "download_postscripts retry $retry" - if [ $retry -eq $max_retries ]; then - echolog "debug" "failed to download postscripts from http://$server$INSTALLDIR/postscripts/ after several retries." - break - fi - - SLI=$(awk 'BEGIN{srand(); printf("%d\n",rand()*20)}') - sleep $SLI - done - return $rc -} - function xcat_wait_for_processes_to_exit { local pidlist="$1" local max_wait="${2:-10}" From a8e770c42ebd340cf5f35e7ee2497720f45eab38 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:28:04 -0300 Subject: [PATCH 04/62] fix(xcat-core): Prevent BATS checks from missing failures Negative checks could pass when a later command succeeded. The diskless test also read host state and wrote to the host wget log. The SSH fallback test did not prove that the restart waited for the killed process. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- xCAT-test/bats/post_xcat_download_policy.bats | 26 +++++++--- xCAT-test/bats/remoteshell_restart.bats | 33 ++++++++---- xCAT-test/bats/sles_pre_script.bats | 8 +-- xCAT/postscripts/xcatdsklspost | 51 ++++++++++--------- 4 files changed, 73 insertions(+), 45 deletions(-) diff --git a/xCAT-test/bats/post_xcat_download_policy.bats b/xCAT-test/bats/post_xcat_download_policy.bats index ec643410c..41810bb97 100644 --- a/xCAT-test/bats/post_xcat_download_policy.bats +++ b/xCAT-test/bats/post_xcat_download_policy.bats @@ -28,25 +28,37 @@ capture_install_scriptlib_wget() capture_xcatdsklspost_wget() { local wget_log="$1" + local host_init_log="${BATS_TEST_TMPDIR}/host-init.log" + local download_log="${BATS_TEST_TMPDIR}/xcatdsklspost-wget-errors.log" + + cat() { printf 'cat %s\n' "$*" >>"$host_init_log"; return 1; } + grep() + { + printf 'grep %s\n' "$*" >>"$host_init_log" + command grep "$@" + } + dirname() { printf 'dirname %s\n' "$*" >>"$host_init_log"; return 1; } + + XCATDSKLSPOST_SOURCE_ONLY=1 + XCAT_WGET_LOG="$download_log" + source "$XCATDSKLSPOST" + [ ! -e "$host_init_log" ] || return 1 + [ "$XCAT_WGET_LOG" = "$download_log" ] || return 1 + unset -f cat grep dirname xcatpost="${BATS_TEST_TMPDIR}/xcatpost" INSTALLDIR=/install - echolog() { :; } sleep() { :; } - grep() - { - [ "${*: -1}" = "/tmp/wget.log" ] && return 1 - command grep "$@" - } wget() { printf '%s\n' "$*" >"$wget_log" + printf '%s\n' 'mock wget stderr' >&2 return 0 } - XCATDSKLSPOST_SOURCE_ONLY=1 source "$XCATDSKLSPOST" download_postscripts 192.0.2.10:80 + [ "$(read_file_or_empty "$download_log")" = "mock wget stderr" ] } assert_download_policy() diff --git a/xCAT-test/bats/remoteshell_restart.bats b/xCAT-test/bats/remoteshell_restart.bats index 1ddce6cfc..b4727d7fc 100644 --- a/xCAT-test/bats/remoteshell_restart.bats +++ b/xCAT-test/bats/remoteshell_restart.bats @@ -1,5 +1,7 @@ #!/usr/bin/env bats +bats_require_minimum_version 1.5.0 + load 'helpers/shell_source' setup() @@ -11,6 +13,8 @@ setup() run_restart_fallback() { + local poll_count=0 + ps() { cat <<'EOF' @@ -19,14 +23,23 @@ EOF } kill() { - printf '%s\n' "$*" >>"$KILL_LOG" - [ "$1" = "-0" ] && return 1 - return 0 + if [ "$1" = "-9" ]; then + printf 'kill %s\n' "$*" >>"$EVENT_LOG" + return 0 + fi + + poll_count=$((poll_count + 1)) + if [ "$poll_count" -eq 1 ]; then + printf 'poll %s alive\n' "$2" >>"$EVENT_LOG" + return 0 + fi + printf 'poll %s gone\n' "$2" >>"$EVENT_LOG" + return 1 } sleep() { :; } sshd() { - printf '%s\n' start >>"$SSHD_LOG" + printf '%s\n' start >>"$EVENT_LOG" } source "$XCATLIB" @@ -44,16 +57,14 @@ run_wait_for_processes() return 0 } -@test "remoteshell restart fallback sends an uncatchable signal before starting sshd" { - KILL_LOG="${BATS_TEST_TMPDIR}/kill.log" - SSHD_LOG="${BATS_TEST_TMPDIR}/sshd.log" - export KILL_LOG SSHD_LOG +@test "remoteshell restart fallback kills, waits, then starts sshd" { + EVENT_LOG="${BATS_TEST_TMPDIR}/events.log" + export EVENT_LOG run run_restart_fallback [ "$status" -eq 0 ] - grep -Fxq -- '-9 4321' "$KILL_LOG" - ! grep -Eq '^9( |$)' "$KILL_LOG" - [ "$(read_file_or_empty "$SSHD_LOG")" = "start" ] + [ "$(read_file_or_empty "$EVENT_LOG")" = $'kill -9 4321\npoll 4321 alive\npoll 4321 gone\nstart' ] + run -1 grep -Eq '^kill 9( |$)' "$EVENT_LOG" } @test "remoteshell wait loop reports a still-running process and gives up" { diff --git a/xCAT-test/bats/sles_pre_script.bats b/xCAT-test/bats/sles_pre_script.bats index f35153200..bd8387ab0 100644 --- a/xCAT-test/bats/sles_pre_script.bats +++ b/xCAT-test/bats/sles_pre_script.bats @@ -1,5 +1,7 @@ #!/usr/bin/env bats +bats_require_minimum_version 1.5.0 + load 'helpers/shell_source' setup() @@ -26,9 +28,9 @@ EOF run set_sles11_uefi_bootloader "$cmdline" "$profile" [ "$status" -eq 0 ] grep -Fxq 'elilo' "$profile" - ! grep -q 'mbr' "$profile" - ! grep -q 'mbr' "$profile" + run -1 grep -q '> /tmp/wget.log + wget -N --waitretry=10 --random-wait -T 60 http://$server$TFTPDIR/mypostscripts/mypostscript.$node -P /$xcatpost 2>> "$XCAT_WGET_LOG" rc=$? # if no error and the file was downloaded if [ $rc -eq 0 ] && [ -f /$xcatpost/mypostscript.$node ]; then @@ -155,16 +158,16 @@ download_postscripts() # These dispatcher scripts are not needed by the legacy netboot post # path. Newer wget parses HTML-looking regex strings inside downloaded # scripts and fails the whole recursive download on bogus URLs. - export LANG=C; wget -l inf -nH -N -r --waitretry=10 --random-wait -e robots=off -T 60 -nH --cut-dirs=2 --reject "index.html*,post.xcat.ng,post.xcat.rhels10" --no-parent http://$server$INSTALLDIR/postscripts/ -P /$xcatpost 2> /tmp/wget.log + export LANG=C; wget -l inf -nH -N -r --waitretry=10 --random-wait -e robots=off -T 60 -nH --cut-dirs=2 --reject "index.html*,post.xcat.ng,post.xcat.rhels10" --no-parent http://$server$INSTALLDIR/postscripts/ -P /$xcatpost 2> "$XCAT_WGET_LOG" rc=$? if [ $rc -eq 0 ]; then # return from wget was 0 but some OS do not return errors, so we # have additional checks for # failed: Connection httpd not running # 404: Not Found - if directory does not exist - grep -i -E "... failed: Connection refused.$" /tmp/wget.log + grep -i -E "... failed: Connection refused.$" "$XCAT_WGET_LOG" rc1=$? - grep -i -E "ERROR 404: Not Found.$" /tmp/wget.log + grep -i -E "ERROR 404: Not Found.$" "$XCAT_WGET_LOG" rc2=$? # check to see no errors at all, grep returns 1 if [ $rc1 -eq 1 ] && [ $rc2 -eq 1 ]; then From 4e36e1a9f261466ac8d01f39986e62d9682d6273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 4 Sep 2026 11:23:27 -0300 Subject: [PATCH 05/62] feat(debian): recognize riscv64 Ubuntu media MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The riscv64 live-server image keeps its kernel at casper/vmlinux, where every other live image keeps casper/vmlinuz, so the probe found no kernel and mkinstall reported that the install image was missing. Add the riscv64 candidate pair and let copycd name the architecture the media reports. Verified against Ubuntu-Server 24.04.4 riscv64, which carries casper/vmlinux, casper/initrd and casper/install-sources.yaml. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- perl-xCAT/xCAT/Utils.pm | 1 + xCAT-server/lib/xcat/plugins/debian.pm | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/perl-xCAT/xCAT/Utils.pm b/perl-xCAT/xCAT/Utils.pm index 838e40bbb..4f79e92e4 100644 --- a/perl-xCAT/xCAT/Utils.pm +++ b/perl-xCAT/xCAT/Utils.pm @@ -4898,6 +4898,7 @@ my @XCAT_ARCH_FROM_DEBIAN = ( [ qr/^ppc64el$/ => 'ppc64el' ], [ qr/ppc|powerpc/ => 'ppc64' ], [ qr/^amd64$/ => 'x86_64' ], + [ qr/^riscv64$/ => 'riscv64' ], ); sub xcat_arch_from_debian { diff --git a/xCAT-server/lib/xcat/plugins/debian.pm b/xCAT-server/lib/xcat/plugins/debian.pm index 7f98fdf7e..eedc8beb9 100644 --- a/xCAT-server/lib/xcat/plugins/debian.pm +++ b/xCAT-server/lib/xcat/plugins/debian.pm @@ -192,6 +192,9 @@ my %INSTALL_BOOT_FILES = ( [ 'install/netboot/ubuntu-installer/{darch}/vmlinux', 'install/netboot/ubuntu-installer/{darch}/initrd.gz' ], [ 'install/vmlinux', 'install/netboot/initrd.gz' ], ], + 'riscv64' => [ + [ 'casper/vmlinux', 'casper/initrd' ], + ], ); sub install_boot_files @@ -201,9 +204,10 @@ sub install_boot_files $darch = '' unless defined $darch; my $family = - $arch =~ /x86/i ? 'x86' - : $arch =~ /ppc64/i ? 'ppc64' - : undef; + $arch =~ /x86/i ? 'x86' + : $arch =~ /ppc64/i ? 'ppc64' + : $arch =~ /riscv64/i ? 'riscv64' + : undef; return unless $family; foreach my $candidate (@{ $INSTALL_BOOT_FILES{$family} }) { From d09452c0b9533236db29da34c4e919d22a61a381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 4 Sep 2026 11:23:48 -0300 Subject: [PATCH 06/62] test(xCAT-test): cover riscv64 Ubuntu media MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pin the kernel and initrd the riscv64 live image carries, that the kernel name the other live images use is not accepted for it, and that the architecture the media reports maps to riscv64 in both directions. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-test/unit/debian_arch_map.t | 4 ++++ xCAT-test/unit/debian_install_boot_files.t | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/xCAT-test/unit/debian_arch_map.t b/xCAT-test/unit/debian_arch_map.t index 617004a43..08c140cec 100644 --- a/xCAT-test/unit/debian_arch_map.t +++ b/xCAT-test/unit/debian_arch_map.t @@ -38,6 +38,10 @@ is(xCAT::Utils->xcat_arch_from_debian('ppc64el'), 'ppc64el', 'POWER LE media keeps the Debian name xCAT uses for Ubuntu'); is(xCAT::Utils->xcat_arch_from_debian('powerpc'), 'ppc64', 'POWER BE media installs ppc64 nodes'); +is(xCAT::Utils->xcat_arch_from_debian('riscv64'), 'riscv64', + 'riscv64 media installs riscv64 nodes'); +is(xCAT::Utils->debian_arch('riscv64'), 'riscv64', + 'Debian and xCAT agree on the riscv64 name'); is(xCAT::Utils->xcat_arch_from_debian('nonesuch'), undef, 'media xCAT has no name for resolves to nothing'); is(xCAT::Utils->xcat_arch_from_debian(''), undef, diff --git a/xCAT-test/unit/debian_install_boot_files.t b/xCAT-test/unit/debian_install_boot_files.t index b5783bc94..dc664cbb0 100644 --- a/xCAT-test/unit/debian_install_boot_files.t +++ b/xCAT-test/unit/debian_install_boot_files.t @@ -91,6 +91,18 @@ is( 'POWER does not accept the x86 live layout', ); +# --- riscv64 ------------------------------------------------------------- +is( + resolved('riscv64', 'riscv64', media('casper/vmlinux', 'casper/initrd')), + 'casper/vmlinux|casper/initrd', + 'the riscv64 live image keeps its kernel under a different name', +); +is( + resolved('riscv64', 'riscv64', media('casper/vmlinuz', 'casper/initrd')), + undef, + 'riscv64 does not accept the kernel name the other live images use', +); + # --- nothing to boot ------------------------------------------------------- is(resolved('x86_64', 'amd64', media('casper/vmlinuz')), undef, 'a kernel without its initrd is not a match'); From 75fa389d33f811d78c5a5275f792a61d32f14653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 4 Sep 2026 12:15:09 -0300 Subject: [PATCH 07/62] feat(genimage): add the riscv64 resolver libraries to the netboot image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The image carries the name service libraries of its architecture, and riscv64 matched neither the x86_64 nor the ppc64el branch. It fell through to the generic path, which looks for lib/libnss_dns.so.2, so a riscv64 image shipped without a resolver and the node could not resolve any name. Ubuntu keeps them in lib/riscv64-linux-gnu, confirmed in the 24.04.4 riscv64 server filesystem. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-server/share/xcat/netboot/ubuntu/genimage | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/xCAT-server/share/xcat/netboot/ubuntu/genimage b/xCAT-server/share/xcat/netboot/ubuntu/genimage index 475d9ae00..c63de378e 100755 --- a/xCAT-server/share/xcat/netboot/ubuntu/genimage +++ b/xCAT-server/share/xcat/netboot/ubuntu/genimage @@ -1786,6 +1786,11 @@ EOMS push @filestoadd, $_ if (-e "$rootimg_dir/$_"); } + } elsif ($arch =~ /riscv64/) { + foreach ("lib/riscv64-linux-gnu/libnss_files.so.2", "lib/riscv64-linux-gnu/libnss_dns.so.2") { + push @filestoadd, $_ if (-e "$rootimg_dir/$_"); + } + } else { push @filestoadd, "lib/libnss_dns.so.2" if (-e "$rootimg_dir/lib/libnss_dns.so.2"); } From 7a2499fbfb519477148d40c5dbb52be9fcf390e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 4 Sep 2026 12:15:45 -0300 Subject: [PATCH 08/62] test(xCAT-test): cover the resolver libraries the netboot image takes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build a root filesystem for each architecture and run genimage's selection over it, so the riscv64 libraries are taken from their own directory and the other architectures keep the files they take today. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- .../unit/ubuntu_genimage_resolver_libs.t | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 xCAT-test/unit/ubuntu_genimage_resolver_libs.t diff --git a/xCAT-test/unit/ubuntu_genimage_resolver_libs.t b/xCAT-test/unit/ubuntu_genimage_resolver_libs.t new file mode 100644 index 000000000..d14460fa1 --- /dev/null +++ b/xCAT-test/unit/ubuntu_genimage_resolver_libs.t @@ -0,0 +1,67 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use File::Path qw(make_path); +use File::Spec; +use File::Temp qw(tempdir); +use FindBin; +use Test::More; + +# A netboot image resolves names with the libnss libraries of its own architecture, and +# Ubuntu keeps them in a per-architecture directory. Build a root filesystem for each +# architecture and run genimage's selection over it. + +my $repo_root = File::Spec->rel2abs(File::Spec->catdir($FindBin::Bin, '..', '..')); +my $genimage = File::Spec->catfile( + $repo_root, 'xCAT-server', 'share', 'xcat', 'netboot', 'ubuntu', 'genimage'); +plan skip_all => "genimage not found at $genimage" unless -f $genimage; + +my $src = do { local $/; open my $fh, '<', $genimage or die $!; <$fh> }; +my ($selection) = + $src =~ /^(\s*if \(\$arch =~ \/x86_64\/\) \{.*?\n\s*\} else \{.*?\n\s*\}\n)/ms; +ok(defined $selection, 'found the resolver library selection in genimage') + or do { done_testing(); exit }; + +sub selected { + my ($arch, @present) = @_; + my $rootimg_dir = tempdir(CLEANUP => 1); + foreach my $file (@present) { + my $full = "$rootimg_dir/$file"; + ($full =~ m{^(.*)/[^/]+$}) and make_path($1); + open(my $fh, '>', $full) or die $!; + close($fh); + } + my @filestoadd; + eval "$selection 1" or die $@; ## no critic (BuiltinFunctions::ProhibitStringyEval) + return join(',', sort @filestoadd); +} + +is( + selected('riscv64', 'lib/riscv64-linux-gnu/libnss_files.so.2', + 'lib/riscv64-linux-gnu/libnss_dns.so.2'), + 'lib/riscv64-linux-gnu/libnss_dns.so.2,lib/riscv64-linux-gnu/libnss_files.so.2', + 'a riscv64 image takes the riscv64 resolver libraries', +); +is( + selected('riscv64', 'lib/libnss_dns.so.2'), + '', + 'riscv64 does not fall back to the path that holds no riscv64 library', +); +is( + selected('x86_64', 'lib/x86_64-linux-gnu/libnss_dns.so.2'), + 'lib/x86_64-linux-gnu/libnss_dns.so.2', + 'x86_64 selection is unchanged', +); +is( + selected('ppc64el', 'lib/powerpc64le-linux-gnu/libnss_files.so.2'), + 'lib/powerpc64le-linux-gnu/libnss_files.so.2', + 'ppc64el selection is unchanged', +); +is( + selected('s390x', 'lib/libnss_dns.so.2'), + 'lib/libnss_dns.so.2', + 'an architecture with no branch keeps the generic library', +); + +done_testing(); From 7d161b822bf7802d08ff1bc8213f33c946bc7735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 4 Sep 2026 12:17:00 -0300 Subject: [PATCH 09/62] feat(xCAT-server): add the riscv64 Ubuntu package lists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 24.04 and 26.04 had no riscv64 package list, so a diskless image or an install for the architecture fell back to the generic list and reached debootstrap without a kernel or the tools the boot scripts call. The lists hold the same packages as their x86_64 counterparts. Every one of them is published for riscv64 in noble and resolute, main or universe. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- .../compute.ubuntu26.04.riscv64.pkglist | 15 +++++++++++++ .../compute.ubuntu24.04.riscv64.pkglist | 22 +++++++++++++++++++ .../compute.ubuntu26.04.riscv64.pkglist | 18 +++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 xCAT-server/share/xcat/install/ubuntu/compute.ubuntu26.04.riscv64.pkglist create mode 100644 xCAT-server/share/xcat/netboot/ubuntu/compute.ubuntu24.04.riscv64.pkglist create mode 100644 xCAT-server/share/xcat/netboot/ubuntu/compute.ubuntu26.04.riscv64.pkglist diff --git a/xCAT-server/share/xcat/install/ubuntu/compute.ubuntu26.04.riscv64.pkglist b/xCAT-server/share/xcat/install/ubuntu/compute.ubuntu26.04.riscv64.pkglist new file mode 100644 index 000000000..813cb2bc2 --- /dev/null +++ b/xCAT-server/share/xcat/install/ubuntu/compute.ubuntu26.04.riscv64.pkglist @@ -0,0 +1,15 @@ +bash +nfs-common +openssl +isc-dhcp-client +libc-bin +openssh-server +openssh-client +wget +vim +rsync +busybox-static +gawk +bind9-dnsutils +chrony +gpg diff --git a/xCAT-server/share/xcat/netboot/ubuntu/compute.ubuntu24.04.riscv64.pkglist b/xCAT-server/share/xcat/netboot/ubuntu/compute.ubuntu24.04.riscv64.pkglist new file mode 100644 index 000000000..4c6b2b3c4 --- /dev/null +++ b/xCAT-server/share/xcat/netboot/ubuntu/compute.ubuntu24.04.riscv64.pkglist @@ -0,0 +1,22 @@ +bash +nfs-common +openssl +isc-dhcp-client +libc-bin +linux-image-generic +openssh-server +openssh-client +wget +rsync +busybox-static +gawk +bind9-dnsutils +tar +gzip +xz-utils +cpio +chrony +util-linux-extra +iproute2 +dracut +dracut-network diff --git a/xCAT-server/share/xcat/netboot/ubuntu/compute.ubuntu26.04.riscv64.pkglist b/xCAT-server/share/xcat/netboot/ubuntu/compute.ubuntu26.04.riscv64.pkglist new file mode 100644 index 000000000..ef2f26fe1 --- /dev/null +++ b/xCAT-server/share/xcat/netboot/ubuntu/compute.ubuntu26.04.riscv64.pkglist @@ -0,0 +1,18 @@ +bash +nfs-common +openssl +isc-dhcp-client +libc-bin +linux-image-generic +openssh-server +openssh-client +wget +rsync +busybox-static +gawk +bind9-dnsutils +tar +gzip +xz-utils +cpio +chrony From fc73cd91f1623bde35ab2e2d9ea65cf8c2eae7a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 4 Sep 2026 12:17:25 -0300 Subject: [PATCH 10/62] test(xCAT-test): cover the riscv64 Ubuntu package lists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pin that both releases carry a riscv64 list, that it holds what the x86_64 list holds, and that it keeps the kernel and the NFS client a diskless node needs. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-test/unit/ubuntu_riscv64_pkglists.t | 46 ++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 xCAT-test/unit/ubuntu_riscv64_pkglists.t diff --git a/xCAT-test/unit/ubuntu_riscv64_pkglists.t b/xCAT-test/unit/ubuntu_riscv64_pkglists.t new file mode 100644 index 000000000..2d7097c3e --- /dev/null +++ b/xCAT-test/unit/ubuntu_riscv64_pkglists.t @@ -0,0 +1,46 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use File::Spec; +use FindBin; +use Test::More; + +# A diskless image needs a kernel and the tools its boot scripts call. Without a package +# list for the architecture, genimage debootstraps whatever the generic list holds and the +# image cannot boot. Compare the riscv64 lists with the x86_64 ones they follow. + +use lib "$FindBin::Bin/../lib"; +use XCAT::Test::File qw(repo_path); + +sub packages { + my ($path) = @_; + my $full = repo_path($path); + return unless -r $full; + open(my $fh, '<', $full) or die "cannot read $full: $!"; + my @packages = grep { length && !/^#/ } map { my $l = $_; chomp $l; $l =~ s/\s+//g; $l } <$fh>; + close($fh); + return \@packages; +} + +foreach my $release (qw(24.04 26.04)) { + my $netboot = "xCAT-server/share/xcat/netboot/ubuntu/compute.ubuntu$release.riscv64.pkglist"; + my $x86 = "xCAT-server/share/xcat/netboot/ubuntu/compute.ubuntu$release.x86_64.pkglist"; + my $riscv_packages = packages($netboot); + ok($riscv_packages, "$release has a riscv64 netboot package list"); + is_deeply($riscv_packages, packages($x86), + "the $release riscv64 image installs what the x86_64 image installs"); + ok(scalar(grep { $_ eq 'linux-image-generic' } @{$riscv_packages}), + "the $release riscv64 image installs a kernel"); + ok(scalar(grep { $_ eq 'nfs-common' } @{$riscv_packages}), + "the $release riscv64 image can mount its root over NFS"); +} + +my $install = packages( + 'xCAT-server/share/xcat/install/ubuntu/compute.ubuntu26.04.riscv64.pkglist'); +ok($install, '26.04 has a riscv64 install package list'); +is_deeply($install, + packages('xCAT-server/share/xcat/install/ubuntu/compute.ubuntu26.04.x86_64.pkglist'), + 'the 26.04 riscv64 install takes what the x86_64 install takes'); + +done_testing(); From 01e9ea515228eb06d579adbe39fa2326c94c7b31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sat, 5 Sep 2026 02:07:31 -0300 Subject: [PATCH 11/62] feat(copycds): build the riscv64 grub2 loader from the Ubuntu media MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit riscv64 nodes have no boot loader unless one reaches /tftpboot/boot/grub2, and nothing on an Ubuntu management node puts one there. The grub2 image the media carry cannot serve: it holds a built-in configuration that looks for the live filesystem, so a node that loads it drops to a grub prompt instead of reading the configuration nodeset writes. copycd now builds a netboot image from the grub2 package the media ship, and warns when it cannot, because the node has no other source for one. An image already in place is kept only when it is a whole executable image for the architecture the firmware loads and carries the prefix this boot path needs; one that is not is removed, so a rebuild that cannot run leaves nodeset reporting a missing loader rather than serving an unusable one. The media of every other architecture are untouched. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-server/lib/xcat/plugins/debian.pm | 161 +++++++++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/xCAT-server/lib/xcat/plugins/debian.pm b/xCAT-server/lib/xcat/plugins/debian.pm index eedc8beb9..b102146bd 100644 --- a/xCAT-server/lib/xcat/plugins/debian.pm +++ b/xCAT-server/lib/xcat/plugins/debian.pm @@ -219,6 +219,166 @@ sub install_boot_files return; } +# The grub2 image on the media boots only from the media: it carries a built-in +# configuration that looks for the live filesystem and never reads the network +# configuration nodeset writes. A netboot image is built from the grub2 package +# the media ships instead. +my %MEDIA_GRUB2_BUILDS = ( + 'riscv64' => { + format => 'riscv64-efi', + package => 'grub-efi-riscv64-bin', + machine => 0x5064, + }, +); + +# Where the loader looks for the configuration nodeset writes, stored inside the image. +my $GRUB2_PREFIX = '/boot/grub2'; + +# The modules the network path needs before it can read a configuration file. +my @GRUB2_NETBOOT_MODULES = qw( + efinet tftp http net normal linux echo test configfile + search search_label search_fs_uuid search_fs_file + gzio part_gpt part_msdos ext2 fat all_video video font terminal reboot halt +); + +sub install_media_grub2_loader { + my ($path, $arch, $callback) = @_; + + my $build = $MEDIA_GRUB2_BUILDS{$arch}; + return unless $build; + + my $tftpdir = xCAT::TableUtils->getTftpDir(); + unless ($tftpdir) { + _no_grub2_loader($arch, 'the TFTP directory is not known', $callback); + return; + } + my $target = "$tftpdir/boot/grub2/grub2.$arch"; + return $target if _is_netboot_loader($target, $build); + + # A file that failed the check is left where it is. The check cannot tell an image built for + # another boot path from one this plugin did not build: the loader on the media carries the + # same modules and differs only in the prefix. Moving a file aside on that evidence can take + # a working loader away from every node of the architecture, so it is replaced only once a + # working one exists, by the rename below. + my $kept = -e $target ? 1 : 0; + + my ($package) = glob("$path/pool/main/g/grub2/$build->{package}_*_$arch.deb"); + unless ($package && -r $package) { + _no_grub2_loader($arch, "the media carry no $build->{package} package", $callback, $kept); + return; + } + + my $workdir = tempdir(CLEANUP => 1); + if (system('dpkg-deb', '-x', $package, $workdir) != 0) { + _no_grub2_loader($arch, "$package could not be unpacked", $callback, $kept); + return; + } + + my $moduledir = "$workdir/usr/lib/grub/$build->{format}"; + unless (-d $moduledir) { + _no_grub2_loader($arch, "$package carries no $build->{format} modules", $callback, $kept); + return; + } + + mkpath("$tftpdir/boot/grub2"); + + # Built beside the target and renamed, so an interrupted run cannot leave a partial + # loader that nodeset would hand to every node of the architecture. + my $partial = "$target.$$"; + my $rc = system('grub-mkimage', '-O', $build->{format}, '-d', $moduledir, + '-p', $GRUB2_PREFIX, '-o', $partial, @GRUB2_NETBOOT_MODULES); + unless ($rc == 0 and _is_netboot_loader($partial, $build)) { + unlink $partial; + _no_grub2_loader($arch, 'grub-mkimage could not build it', $callback, $kept); + return; + } + chmod 0644, $partial; + unless (rename($partial, $target)) { + unlink $partial; + _no_grub2_loader($arch, "it could not be renamed to $target: $!", $callback, $kept); + return; + } + $callback->({ data => "Installed $target from the media" }) if $callback; + return $target; +} + +# UEFI loads the loader as a PE image for one machine, so anything else -- a truncated +# file, a stub carrying only headers, or the loader of another architecture -- cannot boot +# a node and is replaced. The fields below are the ones an image must have to be executed +# at all: sections to load, an entry point to jump to, and the subsystem UEFI runs. +sub _is_uefi_image { + my ($file, $machine) = @_; + + my $size = -s $file; + return 0 unless ($machine and $size); + open(my $fh, '<', $file) or return 0; + binmode($fh); + + my $ok = 0; + my ($dos, $coff, $optional); + if (read($fh, $dos, 64) == 64 + and substr($dos, 0, 2) eq 'MZ' + and seek($fh, unpack('V', substr($dos, 60, 4)), 0) + and read($fh, $coff, 24) == 24 + and substr($coff, 0, 4) eq "PE\0\0" + and unpack('v', substr($coff, 4, 2)) == $machine + and unpack('v', substr($coff, 6, 2)) > 0 + and read($fh, $optional, 72) == 72 + and unpack('v', substr($optional, 0, 2)) == 0x20b) + { + my $entry = unpack('V', substr($optional, 16, 4)); + my $image = unpack('V', substr($optional, 56, 4)); + my $headers = unpack('V', substr($optional, 60, 4)); + my $system = unpack('v', substr($optional, 68, 2)); + + # 10 is the EFI application subsystem, the only one the firmware loads. + $ok = ($entry and $image and $headers and $system == 10 + and $headers <= $size and $image <= $size); + } + close($fh); + return $ok ? 1 : 0; +} + +# The modules a net boot cannot happen without. grub-mkimage records the name of every module +# it embeds, so their absence says the file is not a grub2 image built for this boot path, +# whatever its headers claim. +my @GRUB2_REQUIRED_MODULES = qw(efinet tftp http linux normal configfile search); + +# grub-mkimage stores the prefix inside the image, so a loader built for this boot path +# carries the directory nodeset writes its configuration into. The image on the media has +# the same modules but no such prefix, which is why it cannot find that configuration: a +# file without it is replaced rather than trusted. +sub _is_netboot_loader { + my ($file, $build) = @_; + + return 0 unless _is_uefi_image($file, $build->{machine}); + open(my $fh, '<', $file) or return 0; + binmode($fh); + my $image = do { local $/; <$fh> }; + close($fh); + return 0 unless defined $image and index($image, $GRUB2_PREFIX) >= 0; + for my $module (@GRUB2_REQUIRED_MODULES) { + return 0 if index($image, "\0$module\0") < 0; + } + return 1; +} + +# Nothing else on an Ubuntu management node installs this loader, so a node of this +# architecture cannot boot until an administrator supplies one. +sub _no_grub2_loader { + my ($arch, $reason, $callback, $kept) = @_; + + return unless $callback; + my $consequence = $kept + ? "The grub2.$arch already in the boot/grub2 directory of the TFTP root was left alone. It " + . "was not built for this boot path, so check that nodes of this architecture still boot." + : "Nodes of this architecture will not boot until one is placed in the boot/grub2 " + . "directory of the TFTP root."; + $callback->({ + warning => [ "No grub2.$arch boot loader was installed because $reason. $consequence" ] }); + return; +} + sub is_ubuntu_live_media { my $media_path = shift; @@ -521,6 +681,7 @@ sub copycd } $callback->({ data => "Media copy operation successful" }); + install_media_grub2_loader($temppath, $arch, $callback); unless ($noosimage) { my @ret = xCAT::SvrUtils->update_tables_with_templates($distname, $arch, $temppath, $osdistroname, $legacyUB20); if ($ret[0] != 0) { From 459ec2a8c3156e2b06206999bb0e175d0044991c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sat, 5 Sep 2026 02:07:31 -0300 Subject: [PATCH 12/62] test(xCAT-test): cover the loader copycd builds from Ubuntu media MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drives the build with dpkg-deb and grub-mkimage shadowed by stubs, so the assertions read the arguments that decide whether the image can boot over the network: the firmware format, the prefix nodeset writes into, the module directory, and the network modules. Also covers the warning a node without a loader gets, an interrupted build leaving nothing behind, a real riscv64 loader being kept, text, a truncated image, another architecture's loader, images with nothing to execute and an image built for another boot path all being replaced, and a rejected loader being gone when the media cannot replace it. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-test/unit/ubuntu_copycd_grub2_loader.t | 256 ++++++++++++++++++++ 1 file changed, 256 insertions(+) create mode 100644 xCAT-test/unit/ubuntu_copycd_grub2_loader.t diff --git a/xCAT-test/unit/ubuntu_copycd_grub2_loader.t b/xCAT-test/unit/ubuntu_copycd_grub2_loader.t new file mode 100644 index 000000000..aab00ca4b --- /dev/null +++ b/xCAT-test/unit/ubuntu_copycd_grub2_loader.t @@ -0,0 +1,256 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use File::Path qw(make_path); +use File::Temp qw(tempdir); +use FindBin; +use Test::More; + +# riscv64 nodes have no boot loader unless one reaches /tftpboot/boot/grub2. The image on +# the media cannot be used: it carries a built-in configuration that looks for the live +# filesystem, so a node that loads it drops to a grub prompt instead of reading the network +# configuration. copycd builds a netboot image from the grub2 package the media ship. +# +# dpkg-deb and grub-mkimage are shadowed by stubs ahead of $PATH, because a management node +# is the only place they exist. They record what copycd asked for, so the arguments that +# decide whether the image can boot over the network are what the assertions read. + +BEGIN { + package xCAT::TableUtils; + our $tftpdir; + sub getTftpDir { return $tftpdir; } + $INC{'xCAT/TableUtils.pm'} = __FILE__; +} + +use lib "$FindBin::Bin/../../perl-xCAT"; +use lib "$FindBin::Bin/../../xCAT-server/lib/perl"; +my $plugin = "$FindBin::Bin/../../xCAT-server/lib/xcat/plugins/debian.pm"; +plan skip_all => 'debian.pm not found' unless -r $plugin; +eval { require $plugin; 1 } or plan skip_all => "could not load debian.pm: $@"; + +my $stubs = tempdir(CLEANUP => 1); +my $log = "$stubs/mkimage.args"; + +sub write_stub { + my ($name, $body) = @_; + open(my $fh, '>', "$stubs/$name") or die $!; + print {$fh} "#!/bin/bash\n$body"; + close($fh); + chmod 0755, "$stubs/$name"; +} + +# dpkg-deb -x lays down the module tree the package carries. +write_stub('dpkg-deb', <<'SH'); +dir="${!#}" +mkdir -p "$dir/usr/lib/grub/riscv64-efi" +: > "$dir/usr/lib/grub/riscv64-efi/kernel.img" +SH + +# grub-mkimage records its arguments and writes a riscv64 PE image to the -o path. Like the real +# one it embeds the prefix and the name of every module it was asked for, which is what tells a +# grub2 image apart from a file that merely carries the headers. +write_stub('grub-mkimage', <<'SH'); +echo "$@" >> "$MKIMAGE_LOG" +[ -n "$MKIMAGE_FAIL" ] && exit 1 +out=""; prefix=""; modules=() +while [ $# -gt 0 ]; do + case "$1" in + -o) out="$2"; shift 2 ;; + -p) prefix="$2"; shift 2 ;; + -O|-d) shift 2 ;; + *) modules+=("$1"); shift ;; + esac +done +perl -e 'my ($prefix, @modules) = @ARGV; + my $i = "MZ" . "\0" x 58 . pack("V", 64) . "PE\0\0" . pack("v", 0x5064) + . pack("v", 1) . "\0" x 16 . pack("v", 0x20b) . "\0" x 14 . pack("V", 0x1000) + . "\0" x 36 . pack("V", 4096) . pack("V", 4096) . "\0" x 4 . pack("v", 10) . "\0" x 2; + $i .= "$prefix\0"; + $i .= "\0$_\0" for @modules; + print $i, "\0" x (4096 - length $i)' "$prefix" "${modules[@]}" > "$out" +SH + +$ENV{PATH} = "$stubs:$ENV{PATH}"; +$ENV{MKIMAGE_LOG} = $log; + +sub media_with { + my (@files) = @_; + my $root = tempdir(CLEANUP => 1); + foreach my $file (@files) { + my $full = "$root/$file"; + ($full =~ m{^(.*)/[^/]+$}) and make_path($1); + open(my $fh, '>', $full) or die $!; + print {$fh} "content of $file"; + close($fh); + } + return $root; +} + +sub publish { + my ($arch, $media) = @_; + $xCAT::TableUtils::tftpdir = tempdir(CLEANUP => 1); + unlink $log; + my @said; + xCAT_plugin::debian::install_media_grub2_loader( + $media, $arch, + sub { push @said, ($_[0]->{data} // ()), @{ $_[0]->{warning} || [] } }); + my $target = "$xCAT::TableUtils::tftpdir/boot/grub2/grub2.$arch"; + return { + published => (-e $target ? 1 : 0), + built => (-r $log ? do { open my $fh, '<', $log; local $/; <$fh> } : ''), + said => join(' ', @said), + }; +} + +my $package = 'pool/main/g/grub2/grub-efi-riscv64-bin_2.12-1ubuntu7.3_riscv64.deb'; + +my $riscv = publish('riscv64', media_with($package, 'casper/vmlinux')); +is($riscv->{published}, 1, 'riscv64 media publish a grub2 loader'); +like($riscv->{built}, qr/-O riscv64-efi/, 'the image is built for the riscv64 firmware'); +like($riscv->{built}, qr{-p /boot/grub2}, + 'the image looks for its configuration where nodeset writes it'); +like($riscv->{built}, qr{-d \S+/usr/lib/grub/riscv64-efi}, + 'the modules come from the package the media ship'); +like($riscv->{built}, qr/\befinet\b.*\btftp\b/s, 'the image can reach the network'); +like($riscv->{built}, qr/\bhttp\b/, 'the image can read a configuration over HTTP'); +like($riscv->{said}, qr/Installed .*grub2\.riscv64 from the media/, + 'copycd says where the loader came from'); + +my $x86 = publish('x86_64', media_with('EFI/boot/bootx64.efi', 'casper/vmlinuz')); +is($x86->{published}, 0, 'media of another architecture publish nothing'); + +my $none = publish('riscv64', media_with('casper/vmlinux')); +is($none->{published}, 0, 'riscv64 media without the grub2 package publish nothing'); +like($none->{said}, qr/No grub2\.riscv64 boot loader was installed/, + 'copycd says a riscv64 node will not boot without a loader'); + +# a build that fails must leave no loader behind, so nodeset reports the missing file +$ENV{MKIMAGE_FAIL} = 1; +my $failed = publish('riscv64', media_with($package)); +delete $ENV{MKIMAGE_FAIL}; +is($failed->{published}, 0, 'a failed build leaves no loader'); +like($failed->{said}, qr/No grub2\.riscv64 boot loader was installed/, + 'copycd reports a build that failed'); + +# The image is built beside the target and renamed, so an interrupted run cannot leave a +# partial loader behind for nodeset to hand out. +$xCAT::TableUtils::tftpdir = tempdir(CLEANUP => 1); +make_path("$xCAT::TableUtils::tftpdir/boot/grub2"); +$ENV{MKIMAGE_FAIL} = 1; +xCAT_plugin::debian::install_media_grub2_loader(media_with($package), 'riscv64', undef); +delete $ENV{MKIMAGE_FAIL}; +my @leftovers = glob("$xCAT::TableUtils::tftpdir/boot/grub2/grub2.riscv64*"); +is(scalar @leftovers, 0, 'a failed build leaves nothing beside the target either'); + +# An empty target is what an interrupted older run left behind, so it must not be mistaken +# for an installed loader. +$xCAT::TableUtils::tftpdir = tempdir(CLEANUP => 1); +make_path("$xCAT::TableUtils::tftpdir/boot/grub2"); +open(my $efh, '>', "$xCAT::TableUtils::tftpdir/boot/grub2/grub2.riscv64") or die $!; +close($efh); +unlink $log; +xCAT_plugin::debian::install_media_grub2_loader(media_with($package), 'riscv64', undef); +ok(-s "$xCAT::TableUtils::tftpdir/boot/grub2/grub2.riscv64", + 'an empty loader left by an earlier run is replaced'); + +# UEFI loads the loader as a PE image for one machine. A real riscv64 one is kept, and +# anything else -- text, a header-only stub, another architecture's loader -- is replaced, +# because a node given one of those cannot boot. +sub uefi_image { + my (%f) = @_; + my $size = $f{size} // 4096; + my $image = + "MZ" . ( "\0" x 58 ) . pack( 'V', 64 ) + . "PE\0\0" + . pack( 'v', $f{machine} // 0x5064 ) + . pack( 'v', $f{sections} // 1 ) . ( "\0" x 16 ) + . pack( 'v', 0x20b ) . ( "\0" x 14 ) + . pack( 'V', $f{entry} // 0x1000 ) . ( "\0" x 36 ) + . pack( 'V', $size ) . pack( 'V', $size ) . ( "\0" x 4 ) + . pack( 'v', $f{subsystem} // 10 ) . ( "\0" x 2 ); + my $prefix = exists $f{prefix} ? $f{prefix} : '/boot/grub2'; + $image .= "$prefix\0" if length $prefix; + # grub-mkimage records the name of every embedded module, so a real loader carries them. + my @modules = exists $f{modules} + ? @{ $f{modules} } + : qw(efinet tftp http linux normal configfile search); + $image .= "\0$_\0" for @modules; + return $image . ( "\0" x ( $size - length $image ) ); +} + +sub existing_loader_kept { + my ($bytes) = @_; + $xCAT::TableUtils::tftpdir = tempdir( CLEANUP => 1 ); + make_path("$xCAT::TableUtils::tftpdir/boot/grub2"); + my $target = "$xCAT::TableUtils::tftpdir/boot/grub2/grub2.riscv64"; + open( my $fh, '>', $target ) or die $!; + binmode($fh); + print {$fh} $bytes; + close($fh); + xCAT_plugin::debian::install_media_grub2_loader( media_with($package), 'riscv64', undef ); + open( my $rfh, '<', $target ) or die $!; + binmode($rfh); + my $now = do { local $/; <$rfh> }; + close($rfh); + return $now eq $bytes; +} + +ok( existing_loader_kept( uefi_image() ), 'a riscv64 loader already in place is kept' ); + +ok( !existing_loader_kept('not a loader at all'), + 'a file that is not a UEFI image is replaced' ); +ok( !existing_loader_kept('MZ and nothing else'), + 'a file that only starts with the DOS signature is replaced' ); +ok( !existing_loader_kept( substr( uefi_image(), 0, 200 ) ), + 'a loader truncated to its headers is replaced' ); +ok( !existing_loader_kept( uefi_image( machine => 0x8664 ) ), + "another architecture's loader is replaced" ); + +# The fields below are what makes a PE image executable at all. A file carrying the +# signatures but none of them cannot boot a node, so it must not be mistaken for a loader. +ok( !existing_loader_kept( uefi_image( sections => 0 ) ), + 'an image with no sections to load is replaced' ); +ok( !existing_loader_kept( uefi_image( entry => 0 ) ), + 'an image with no entry point is replaced' ); +ok( !existing_loader_kept( uefi_image( subsystem => 3 ) ), + 'an image that is not an EFI application is replaced' ); + +# The image on the media carries the same modules but not the prefix this plugin builds with, +# so it looks for the live filesystem instead of the configuration nodeset writes. +ok( !existing_loader_kept( uefi_image( prefix => '' ) ), + 'a valid EFI image built for another boot path is replaced' ); + +# Headers and a prefix are cheap to fabricate and say nothing about what the image can do. +# Without the modules a net boot goes through, the file cannot fetch a kernel over the network. +ok( !existing_loader_kept( uefi_image( modules => [] ) ), + 'an image carrying no grub2 modules is replaced' ); +ok( !existing_loader_kept( uefi_image( modules => [qw(linux normal configfile search)] ) ), + 'an image with no network modules is replaced' ); +ok( existing_loader_kept( uefi_image( modules => [qw(efinet tftp http linux normal configfile search gzio)] ) ), + 'an image carrying more modules than the minimum is kept' ); + +# A rebuild that cannot run must leave the loader alone. The check cannot tell an image this +# plugin did not build from one built for another boot path -- the media loader carries the same +# modules -- so removing one on that evidence can take a working loader away from every node. +$xCAT::TableUtils::tftpdir = tempdir( CLEANUP => 1 ); +make_path("$xCAT::TableUtils::tftpdir/boot/grub2"); +my $rejected = "$xCAT::TableUtils::tftpdir/boot/grub2/grub2.riscv64"; +open( my $bad, '>', $rejected ) or die $!; +print {$bad} 'not a loader at all'; +close($bad); +my @told; +xCAT_plugin::debian::install_media_grub2_loader( + media_with('casper/vmlinux'), 'riscv64', + sub { push @told, ( $_[0]->{data} // () ), @{ $_[0]->{warning} || [] } } ); +ok( -e $rejected, 'a rejected loader survives a media that cannot replace it' ); +my $left = -e $rejected + ? do { open my $fh, '<', $rejected or die $!; local $/; <$fh> } + : '(removed)'; +is( $left, 'not a loader at all', '... byte for byte' ); +like( join( ' ', @told ), qr/No grub2\.riscv64 boot loader was installed/, + 'and copycd says no loader was installed' ); +like( join( ' ', @told ), qr/was left alone/, + '... and that it did not touch what was there' ); + +done_testing(); From 4766f4441f4d9751e8d636555657e800fa745712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sat, 5 Sep 2026 01:49:10 -0300 Subject: [PATCH 13/62] feat(build): build and publish the Ubuntu management node for riscv64 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The xcat and xcatsn packages declared only amd64 and ppc64el, and the builder built and indexed only those two architectures, so a riscv64 management node had no package to install. Both packages are now built for riscv64, every release the repository serves declares the architecture, and the generated mklocalrepo.sh maps a riscv64 host to its own repository instead of amd64. Each package now carries its own architecture list: xCAT-genesis-scripts keeps the two it has control files for, because riscv64 Genesis ships as an OpenEmbedded package instead. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- build-utils/lib/XCAT/BuildUtils.pm | 20 +++++++++++++------- builddebs.pl | 10 +++++----- xCAT/debian/control | 2 +- xCATsn/debian/control | 2 +- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/build-utils/lib/XCAT/BuildUtils.pm b/build-utils/lib/XCAT/BuildUtils.pm index e0deac209..54df11487 100644 --- a/build-utils/lib/XCAT/BuildUtils.pm +++ b/build-utils/lib/XCAT/BuildUtils.pm @@ -147,16 +147,22 @@ use constant XCAT_PROBE_HELPERS => qw( ServiceNodeUtils.pm ); -# Packages whose .deb carries a real architecture. Everything else in xcat-core is -# Perl and ships as Architecture: all -- one binary serving every Ubuntu release and -# every arch, which is why this build never needs a per-codename chroot. -my %ARCH_PACKAGES = map { $_ => 1 } qw(xCAT xCATsn xCAT-genesis-scripts); +# Packages whose .deb carries a real architecture, and the architectures each is built +# for. Everything else in xcat-core is Perl and ships as Architecture: all -- one binary +# serving every Ubuntu release and every arch, which is why this build never needs a +# per-codename chroot. xCAT-genesis-scripts has no riscv64 control file: riscv64 Genesis +# ships as an OpenEmbedded package. +my %ARCH_PACKAGES = ( + 'xCAT' => [qw(amd64 ppc64el riscv64)], + 'xCATsn' => [qw(amd64 ppc64el riscv64)], + 'xCAT-genesis-scripts' => [qw(amd64 ppc64el)], +); # Ubuntu releases predating ppc64el. Kept as data rather than an `if` in the caller so # the repo-assembly and the package-selection paths cannot disagree about it. my %NO_PPC64EL = map { $_ => 1 } qw(saucy); -my @DEB_ARCHES = qw(amd64 ppc64el); +my @DEB_ARCHES = qw(amd64 ppc64el riscv64); # The Ubuntu releases the apt repository serves by default. Single source of truth: # the builder, the repo assembly and the tests all read it here, so they cannot drift. @@ -323,8 +329,8 @@ sub stage_probe_helpers { # 'all' is a single arch-independent build; the three arch packages get one per arch. sub deb_package_arches { my ($package) = @_; - return @DEB_ARCHES if $ARCH_PACKAGES{$package // ''}; - return ('all'); + my $arches = $ARCH_PACKAGES{ $package // '' }; + return $arches ? @{$arches} : ('all'); } # dist_arches: the architectures a release's apt repo declares. diff --git a/builddebs.pl b/builddebs.pl index 7e148223e..873936b44 100755 --- a/builddebs.pl +++ b/builddebs.pl @@ -317,11 +317,11 @@ sub write_repo_metadata { . /etc/lsb-release cd `dirname $0` host_arch=`uname -m` -if [ "$host_arch" != "ppc64le" ];then - host_arch="amd64" -else - host_arch="ppc64el" -fi +case "$host_arch" in + ppc64le) host_arch="ppc64el" ;; + riscv64) host_arch="riscv64" ;; + *) host_arch="amd64" ;; +esac echo deb [arch=$host_arch] file://"`pwd`" $DISTRIB_CODENAME main > /etc/apt/sources.list.d/xcat-core.list SCRIPT diff --git a/xCAT/debian/control b/xCAT/debian/control index 57a82ffdf..4889209aa 100644 --- a/xCAT/debian/control +++ b/xCAT/debian/control @@ -8,7 +8,7 @@ Vcs-browser: https://github.com/xcat2/xcat-core.git Homepage: https://xcat.org/ Package: xcat -Architecture: amd64 ppc64el +Architecture: amd64 ppc64el riscv64 Depends: ${perl:Depends}, goconserver(>= 0.3.3-snap000000000000), xcat-server (>= 2.13-snap000000000000), xcat-client (>= 2.13-snap000000000000), libdbd-sqlite3-perl, isc-dhcp-server | kea, bind9, apache2, nfs-kernel-server, libxml-parser-perl, rsync, tftpd-hpa, libnet-telnet-perl, chrony | ntp, xcat-genesis-scripts-amd64 (>= 2.13-snap000000000000) Recommends: net-tools, nmap, kea, tftp-hpa, ipmitool-xcat (>= 1.8.17-1), syslinux[any-amd64], libsys-virt-perl, syslinux-xcat, xnba-undi, elilo-xcat, util-linux-extra, xcat-buildkit (>= 2.13-snap000000000000), xcat-probe (>= 2.13-snap000000000000), xcat-genesis-openembedded-x86-64, xcat-genesis-openembedded-ppc64le, xcat-genesis-openembedded-riscv64, xcat-genesis-openembedded-s390x Suggests: yaboot-xcat diff --git a/xCATsn/debian/control b/xCATsn/debian/control index 063bf4716..ef6baa130 100644 --- a/xCATsn/debian/control +++ b/xCATsn/debian/control @@ -7,7 +7,7 @@ Standards-Version: 3.9.4 Homepage: https://xcat.org/ Package: xcatsn -Architecture: amd64 ppc64el +Architecture: amd64 ppc64el riscv64 Depends: ${perl:Depends}, goconserver (>=0.3.3-snap000000000000), xcat-server (>= 2.13-snap000000000000), xcat-client (>= 2.13-snap000000000000), libdbd-sqlite3-perl, libxml-parser-perl, tftpd-hpa, libnet-telnet-perl, isc-dhcp-server | kea, bind9, apache2, nfs-kernel-server, xcat-genesis-scripts-amd64 (>= 2.13-snap000000000000) Recommends: net-tools, nmap, kea, tftp-hpa, ipmitool-xcat (>= 1.8.17-1), syslinux[any-amd64], libsys-virt-perl, syslinux-xcat, xnba-undi, elilo-xcat, xcat-buildkit (>= 2.13-snap000000000000), xcat-probe (>= 2.13-snap000000000000), xcat-genesis-openembedded-x86-64, xcat-genesis-openembedded-ppc64le, xcat-genesis-openembedded-riscv64, xcat-genesis-openembedded-s390x Suggests: yaboot-xcat From b0bf36d27a5229ed7777207dde7e72f58c2b8a0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sat, 5 Sep 2026 01:49:10 -0300 Subject: [PATCH 14/62] test(xCAT-test): cover the riscv64 Ubuntu management node packaging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the builder tests with the per-package architecture lists and the architectures a release declares, and adds the generated mklocalrepo.sh mapping a riscv64 host to its own repository. Also checks that xcat and xcatsn declare riscv64 without losing amd64 or ppc64el. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-test/unit/build_utils.t | 18 +++--- xCAT-test/unit/builddebs_riscv64.t | 88 ++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 7 deletions(-) create mode 100644 xCAT-test/unit/builddebs_riscv64.t diff --git a/xCAT-test/unit/build_utils.t b/xCAT-test/unit/build_utils.t index 8a44fbe39..eaa3a43ff 100644 --- a/xCAT-test/unit/build_utils.t +++ b/xCAT-test/unit/build_utils.t @@ -60,15 +60,19 @@ is_deeply( [deb_package_arches('perl-xCAT')], ['all'], 'a Perl package is built once, arch-independent' ); is_deeply( [deb_package_arches('xCAT-probe')], ['all'], 'xCAT-probe is arch-independent too' ); -for my $pkg (qw(xCAT xCATsn xCAT-genesis-scripts)) { - is_deeply( [deb_package_arches($pkg)], ['amd64', 'ppc64el'], - "$pkg is built per architecture" ); +for my $pkg (qw(xCAT xCATsn)) { + is_deeply( [deb_package_arches($pkg)], ['amd64', 'ppc64el', 'riscv64'], + "$pkg is built for every architecture a management node runs on" ); } +# xCAT-genesis-scripts has one control file per architecture and there is no riscv64 one, +# so asking for that build would stop the whole run. +is_deeply( [deb_package_arches('xCAT-genesis-scripts')], ['amd64', 'ppc64el'], + 'xCAT-genesis-scripts is built only for the architectures it has a control file for' ); is_deeply( [deb_package_arches(undef)], ['all'], 'an undefined package name does not blow up the arch lookup' ); -is_deeply( [dist_arches('noble')], ['amd64', 'ppc64el'], - 'a current release serves both architectures' ); +is_deeply( [dist_arches('noble')], ['amd64', 'ppc64el', 'riscv64'], + 'a current release serves every architecture' ); is_deeply( [dist_arches('saucy')], ['amd64'], 'saucy predates ppc64el and serves only amd64' ); @@ -151,8 +155,8 @@ is( scalar( () = $rewritten =~ /^ -- xCAT Build /mg ), 1, my $dists = reprepro_distributions([qw(focal noble)], 'DEADBEEF'); is( scalar(() = $dists =~ /^Codename:/mg), 2, 'one stanza per release' ); -like( $dists, qr/^Codename: focal\nArchitectures: amd64 ppc64el$/m, - 'a release declares both architectures, on the line after its codename' ); +like( $dists, qr/^Codename: focal\nArchitectures: amd64 ppc64el riscv64$/m, + 'a release declares every architecture, on the line after its codename' ); is( scalar(() = $dists =~ /^SignWith: DEADBEEF$/mg), 2, 'every stanza is signed when a key is given' ); diff --git a/xCAT-test/unit/builddebs_riscv64.t b/xCAT-test/unit/builddebs_riscv64.t new file mode 100644 index 000000000..5dc2fc813 --- /dev/null +++ b/xCAT-test/unit/builddebs_riscv64.t @@ -0,0 +1,88 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use File::Spec; +use File::Temp qw(tempdir); +use FindBin; +use Test::More; + +# A riscv64 management node needs an xcat and xcatsn deb built for the architecture and a +# mklocalrepo.sh that points the host at the matching repository instead of amd64. +# +# The generated script is extracted from builddebs.pl and run with a stub uname ahead of +# $PATH, so the mapping under test is the shipped code. Only the path it writes is +# redirected into the sandbox, because it writes an apt source list. + +my $repo_root = File::Spec->rel2abs( File::Spec->catdir( $FindBin::Bin, '..', '..' ) ); +my $builder = File::Spec->catfile( $repo_root, 'builddebs.pl' ); +plan skip_all => "builddebs.pl not found" unless -f $builder; + +my $src = do { local $/; open my $fh, '<', $builder or die $!; <$fh> }; + +# BAIL_OUT rather than skip: a rename that stops this matching must fail loudly instead of +# silently covering nothing. +my ($script) = $src =~ /write_script\("\$repodir\/mklocalrepo\.sh", <<'SCRIPT'\);\n(.*?)\nSCRIPT\n/ms; +BAIL_OUT('could not extract mklocalrepo.sh from builddebs.pl') unless defined $script; + +my $dir = tempdir( CLEANUP => 1 ); +my $run = 0; + +# Run the generated script for one host architecture and return the apt source line it wrote. +sub sources_line_for { + my ($uname) = @_; + $run++; + my $root = File::Spec->catdir( $dir, "run$run" ); + mkdir $root; + mkdir "$root/bin"; + + open( my $stub, '>', "$root/bin/uname" ) or die $!; + print {$stub} "#!/bin/bash\necho $uname\n"; + close($stub); + chmod 0755, "$root/bin/uname"; + + my $release = File::Spec->catfile( $root, 'lsb-release' ); + open( my $rel, '>', $release ) or die $!; + print {$rel} "DISTRIB_CODENAME=noble\n"; + close($rel); + + my $listed = File::Spec->catfile( $root, 'sources.list' ); + ( my $sandboxed = $script ) =~ s{/etc/lsb-release}{$release}; + $sandboxed =~ s{/etc/apt/sources\.list\.d/\S+}{$listed}; + + my $harness = File::Spec->catfile( $root, 'harness.sh' ); + open( my $fh, '>', $harness ) or die $!; + print {$fh} "#!/bin/bash\n$sandboxed\n"; + close($fh); + + local $ENV{PATH} = "$root/bin:$ENV{PATH}"; + system( '/bin/bash', $harness ); + open( my $out, '<', $listed ) or die $!; + my $line = do { local $/; <$out> }; + close($out); + return $line; +} + +for my $case ( + [ 'riscv64', 'riscv64' ], + [ 'ppc64le', 'ppc64el' ], + [ 'x86_64', 'amd64' ], + ) +{ + my ( $uname, $want ) = @$case; + like( sources_line_for($uname), qr/^deb \[arch=\Q$want\E\] /, + "mklocalrepo.sh gives a $uname host the $want repository" ); +} + +# The debs themselves must exist for the architecture. +for my $case ( [ 'xCAT', 'xcat' ], [ 'xCATsn', 'xcatsn' ] ) { + my ( $component, $package ) = @$case; + my $control = File::Spec->catfile( $repo_root, $component, 'debian', 'control' ); + my $text = do { local $/; open my $fh, '<', $control or die $!; <$fh> }; + my ($arches) = $text =~ /^Package: \Q$package\E\nArchitecture: (.*)$/m; + ok( defined $arches, "$package declares an architecture" ); + like( $arches || '', qr/\briscv64\b/, "$package is built for riscv64" ); + like( $arches || '', qr/\bamd64\b.*\bppc64el\b/, "$package keeps amd64 and ppc64el" ); +} + +done_testing(); From f1b044f2a8cb6e323419d5522843779ae7270368 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sat, 5 Sep 2026 01:49:30 -0300 Subject: [PATCH 15/62] fix(genimage): take the netboot mirror from the ports archive off amd64 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit archive.ubuntu.com publishes amd64 and i386 only, so debootstrap could not find a single package for a ppc64el or riscv64 netboot image and genimage failed on every architecture except x86. The default mirror is now the ports archive for those architectures. site.ubuntu_apt_mirror still overrides it, for a local mirror that serves every architecture. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-server/share/xcat/netboot/ubuntu/genimage | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/xCAT-server/share/xcat/netboot/ubuntu/genimage b/xCAT-server/share/xcat/netboot/ubuntu/genimage index c63de378e..17005cdb5 100755 --- a/xCAT-server/share/xcat/netboot/ubuntu/genimage +++ b/xCAT-server/share/xcat/netboot/ubuntu/genimage @@ -259,8 +259,13 @@ unless ($onlyinitrd) { # site.ubuntu_apt_mirror overrides; otherwise default to the public archive. A live-server # ISO is never a complete debootstrap source, so a real mirror is always required here. my @aptmirror = xCAT::TableUtils->get_site_attribute("ubuntu_apt_mirror"); + # archive.ubuntu.com publishes amd64 and i386 only. Every other architecture, ppc64el + # and riscv64 included, is on the ports archive. + my $default = ($uarch =~ /^(?:amd64|i386)$/) + ? 'http://archive.ubuntu.com/ubuntu' + : 'http://ports.ubuntu.com/ubuntu-ports'; my $mirror = (defined $aptmirror[0] && length $aptmirror[0]) - ? $aptmirror[0] : 'http://archive.ubuntu.com/ubuntu'; + ? $aptmirror[0] : $default; (my $codename = $osver) =~ s/^ubuntu//; # Strip ONLY a trailing third component. A bare s/\.\d+$// also strips the minor from a # two-part osvers (ubuntu26.04 -> "26"), which misses the %cn map below and reaches From 9812d14928ca75674dc628a4651dabfb09aa6745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sat, 5 Sep 2026 01:49:30 -0300 Subject: [PATCH 16/62] test(xCAT-test): cover the netboot mirror genimage defaults to MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Evaluates the selection genimage performs, so the test tracks the script. Covers the ports archive for ppc64el and riscv64, the main archive for amd64 and i386, and site.ubuntu_apt_mirror overriding both without an empty value blanking the mirror. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-test/unit/ubuntu_genimage_apt_mirror.t | 60 +++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 xCAT-test/unit/ubuntu_genimage_apt_mirror.t diff --git a/xCAT-test/unit/ubuntu_genimage_apt_mirror.t b/xCAT-test/unit/ubuntu_genimage_apt_mirror.t new file mode 100644 index 000000000..172ab8b47 --- /dev/null +++ b/xCAT-test/unit/ubuntu_genimage_apt_mirror.t @@ -0,0 +1,60 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use File::Spec; +use FindBin; +use Test::More; + +# archive.ubuntu.com publishes amd64 and i386 only. A ppc64el or riscv64 netboot image built +# against it finds no package at all, and debootstrap fails before it copies anything, so the +# default mirror has to follow the image architecture. +# +# Driven by the real selection code: the two statements are extracted from genimage and +# evaluated here, so this test tracks the script rather than a copy of it. + +my $repo_root = File::Spec->rel2abs( File::Spec->catdir( $FindBin::Bin, '..', '..' ) ); +my $genimage_path = File::Spec->catfile( + $repo_root, 'xCAT-server', 'share', 'xcat', 'netboot', 'ubuntu', 'genimage' +); +plan skip_all => "genimage not found at $genimage_path" unless -f $genimage_path; + +my $src = do { local $/; open my $fh, '<', $genimage_path or die $!; <$fh> }; + +my ($default) = $src =~ /^\s*(my \$default = \(\$uarch =~.*?;)\s*$/ms; +ok( defined $default, 'found the default mirror selection in genimage' ) + or done_testing(), exit; + +my ($pick) = $src =~ /^\s*(my \$mirror = \(defined \$aptmirror\[0\].*?;)\s*$/ms; +ok( defined $pick, 'found the mirror override in genimage' ) + or done_testing(), exit; + +sub choose { + my ( $uarch, $site ) = @_; + my $set = defined $site ? "('$site')" : "()"; + ## no critic (BuiltinFunctions::ProhibitStringyEval) + my $mirror = eval "my \$uarch = '$uarch'; my \@aptmirror = $set; $default $pick \$mirror"; + ## use critic + die "failed to evaluate the genimage mirror selection: $@" if $@; + return $mirror; +} + +is( choose('riscv64'), 'http://ports.ubuntu.com/ubuntu-ports', + 'a riscv64 image takes the ports archive' ); +is( choose('ppc64el'), 'http://ports.ubuntu.com/ubuntu-ports', + 'a ppc64el image takes the ports archive' ); +is( choose('amd64'), 'http://archive.ubuntu.com/ubuntu', + 'an amd64 image keeps the main archive' ); +is( choose('i386'), 'http://archive.ubuntu.com/ubuntu', + 'an i386 image keeps the main archive' ); + +is( choose( 'riscv64', 'http://mirror.example.invalid/ubuntu' ), + 'http://mirror.example.invalid/ubuntu', + 'site.ubuntu_apt_mirror overrides the ports archive' ); +is( choose( 'amd64', 'http://mirror.example.invalid/ubuntu' ), + 'http://mirror.example.invalid/ubuntu', + 'site.ubuntu_apt_mirror overrides the main archive' ); +is( choose( 'riscv64', '' ), 'http://ports.ubuntu.com/ubuntu-ports', + 'an empty site.ubuntu_apt_mirror does not blank the mirror' ); + +done_testing(); From 53709af9b395e24ea73d5671e78df104f4093371 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sat, 5 Sep 2026 01:49:30 -0300 Subject: [PATCH 17/62] feat(imgutils): give riscv64 Ubuntu netboot images their network drivers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Ubuntu driver table had no riscv64 entry, so genimage was handed an empty list and built an image carrying no network module. A node whose NIC is not built into the kernel then has no interface to fetch its root filesystem with. The architecture now gets the same drivers the enterprise Linux table lists for it, plus the overlay module every Ubuntu image needs. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-server/share/xcat/netboot/imgutils/imgutils.pm | 1 + 1 file changed, 1 insertion(+) diff --git a/xCAT-server/share/xcat/netboot/imgutils/imgutils.pm b/xCAT-server/share/xcat/netboot/imgutils/imgutils.pm index 644a6e671..dbea8cd28 100644 --- a/xCAT-server/share/xcat/netboot/imgutils/imgutils.pm +++ b/xCAT-server/share/xcat/netboot/imgutils/imgutils.pm @@ -278,6 +278,7 @@ sub default_net_drivers { x86_64 => [qw(tg3 bnx2 bnx2x e1000 e1000e igb mlx_en mlx5_core virtio_net overlay)], ppc64el => [qw(tg3 bnx2 bnx2x e1000 e1000e igb ibmveth ehea mlx_en mlx4_en mlx5_core virtio_net overlay)], ppc64 => [qw(e1000 e1000e igb ibmveth ehea)], + riscv64 => [qw(e1000 e1000e igb ixgbe r8169 tg3 bnx2x mlx5_core virtio_net overlay)], s390x => [qw(qdio ccwgroup)], }, ); From 7fed9001e8c2063cf2c5cfe0223d3ab8e288d9fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sat, 5 Sep 2026 01:49:30 -0300 Subject: [PATCH 18/62] test(xCAT-test): cover the riscv64 Ubuntu network drivers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reads the list imgutils returns. Covers the drivers a node needs to reach its root filesystem, the overlay module, the architectures that already worked keeping theirs, and an unknown architecture still getting an empty list. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-test/unit/ubuntu_riscv64_net_drivers.t | 38 +++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 xCAT-test/unit/ubuntu_riscv64_net_drivers.t diff --git a/xCAT-test/unit/ubuntu_riscv64_net_drivers.t b/xCAT-test/unit/ubuntu_riscv64_net_drivers.t new file mode 100644 index 000000000..6e98c33a2 --- /dev/null +++ b/xCAT-test/unit/ubuntu_riscv64_net_drivers.t @@ -0,0 +1,38 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use FindBin; +use Test::More; + +# Without a driver list the Ubuntu netboot image ships no network module at all, and a node +# whose NIC is not built into the kernel cannot reach its root filesystem. QEMU hides this, +# because virtio-net is built into the Ubuntu riscv64 kernel; a machine with an r8169 or an +# e1000e does not boot. + +use lib "$FindBin::Bin/../../perl-xCAT"; +use lib "$FindBin::Bin/../../xCAT-server/lib/perl"; +use lib "$FindBin::Bin/../../xCAT-server/share/xcat/netboot/imgutils"; +require imgutils; + +my @riscv = imgutils::default_net_drivers( 'ubuntu', 'riscv64' ); +ok( scalar @riscv, 'a riscv64 Ubuntu image is given network drivers' ); + +for my $driver (qw(virtio_net e1000 e1000e igb r8169 tg3 mlx5_core)) { + ok( scalar( grep { $_ eq $driver } @riscv ), "the list carries $driver" ); +} + +# Every Ubuntu row carries overlay, because the netboot root is an overlay mount. +ok( scalar( grep { $_ eq 'overlay' } @riscv ), 'the list carries overlay' ); + +# The architectures that already worked must keep the drivers they had. +is_deeply( + [ imgutils::default_net_drivers( 'ubuntu', 'x86_64' ) ], + [qw(tg3 bnx2 bnx2x e1000 e1000e igb mlx_en mlx5_core virtio_net overlay)], + 'x86_64 keeps its drivers' ); +ok( scalar( imgutils::default_net_drivers( 'ubuntu', 'ppc64el' ) ), + 'ppc64el keeps its drivers' ); +is_deeply( [ imgutils::default_net_drivers( 'ubuntu', 'sparc' ) ], [], + 'an architecture with no entry still gets an empty list' ); + +done_testing(); From 52e468884eae73784e422ceb90b92ed09da5d806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sat, 5 Sep 2026 01:49:31 -0300 Subject: [PATCH 19/62] fix(grub2): keep the whole kernel command line past a grub2 separator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit grub2 reads its configuration as a script, so an unquoted command separator ends the linux command and everything after it is lost. The Ubuntu installer seed is written as ds=nocloud-net;s=, so the node booted without the seed URL and without the arguments that followed it, including BOOTIF. The installer then found no autoinstall configuration and waited for someone to answer its questions. A separator that is neither escaped nor inside a quoted span is now escaped where it stands, which grub2 removes before it hands the line to the kernel. A value the caller escaped or quoted keeps exactly the form the caller gave it. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-server/lib/xcat/plugins/grub2.pm | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/xCAT-server/lib/xcat/plugins/grub2.pm b/xCAT-server/lib/xcat/plugins/grub2.pm index 88a693069..17e48e3b4 100644 --- a/xCAT-server/lib/xcat/plugins/grub2.pm +++ b/xCAT-server/lib/xcat/plugins/grub2.pm @@ -85,6 +85,29 @@ sub getstate { } } +# grub2 reads its configuration as a script, so an unquoted word carrying one of the +# characters below ends the linux command and the rest of the kernel command line is lost. +# The Ubuntu installer seed (ds=nocloud-net;s=) is the usual casualty. +my $GRUB2_TERMINATOR = qr/[;{}|&<>()]/; + +sub quote_kcmdline { + my $kcmdline = shift; + + return $kcmdline unless (defined $kcmdline and $kcmdline =~ $GRUB2_TERMINATOR); + + # Escaped in place rather than quoted as a whole: a value the caller quoted keeps the + # quoting it was given, which grub2 removes before the kernel sees the value. + my $escaped = ''; + while (length $kcmdline) { + if ($kcmdline =~ s/^('[^']*'|"[^"]*")//) { $escaped .= $1; next; } + if ($kcmdline =~ s/^(\\.)//) { $escaped .= $1; next; } + $kcmdline =~ s/^(.)//s; + my $char = $1; + $escaped .= ($char =~ $GRUB2_TERMINATOR) ? "\\$char" : $char; + } + return $escaped; +} + sub setstate { =pod @@ -260,7 +283,8 @@ sub setstate { } if ($kern and $kern->{kcmdline}) { - print $pcfg " linux$efi $protocolrootdir/$kern->{kernel} $kern->{kcmdline} BOOTIF=\$net_default_mac\n"; + my $kcmdline = quote_kcmdline($kern->{kcmdline}); + print $pcfg " linux$efi $protocolrootdir/$kern->{kernel} $kcmdline BOOTIF=\$net_default_mac\n"; } else { print $pcfg " linux$efi $protocolrootdir/$kern->{kernel} BOOTIF=\$net_default_mac\n"; } From 5c4b83b8539e85881944067700925735fc803eec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sat, 5 Sep 2026 01:49:31 -0300 Subject: [PATCH 20/62] test(xCAT-test): cover the grub2 kernel command line quoting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Calls the escaping the plugin performs. Covers the Ubuntu installer seed keeping the arguments after it, every separator grub2 recognizes, a command line without one staying byte for byte the same, escaped and quoted values surviving unchanged, a separator beside a quoted span in the same word, and a variable reference being left alone so BOOTIF still expands. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-test/unit/grub2_kcmdline_quoting.t | 63 +++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 xCAT-test/unit/grub2_kcmdline_quoting.t diff --git a/xCAT-test/unit/grub2_kcmdline_quoting.t b/xCAT-test/unit/grub2_kcmdline_quoting.t new file mode 100644 index 000000000..1e2066c3d --- /dev/null +++ b/xCAT-test/unit/grub2_kcmdline_quoting.t @@ -0,0 +1,63 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use FindBin; +use Test::More; + +# grub2 reads its configuration as a script. A word carrying a command separator ends the +# linux command, so the kernel never sees the rest of the line. The Ubuntu installer seed +# (ds=nocloud-net;s=) is written as one such word: the node booted without the seed URL +# and without BOOTIF, and the installer waited for someone to answer its questions. + +use lib "$FindBin::Bin/../../perl-xCAT"; +use lib "$FindBin::Bin/../../xCAT-server/lib/perl"; +my $plugin = "$FindBin::Bin/../../xCAT-server/lib/xcat/plugins/grub2.pm"; +plan skip_all => 'grub2.pm not found' unless -r $plugin; +eval { require $plugin; 1 } or plan skip_all => "could not load grub2.pm: $@"; + +my $quote = \&xCAT_plugin::grub2::quote_kcmdline; + +my $seed = 'imgurl=http://mn/rootimg.cpio.gz ds=nocloud-net;s=http://mn/install/autoinst/n1/ quiet'; +is( $quote->($seed), + 'imgurl=http://mn/rootimg.cpio.gz ds=nocloud-net\;s=http://mn/install/autoinst/n1/ quiet', + 'the installer seed keeps the arguments after it' ); + +my $plain = 'imgurl=http://mn/rootimg.cpio.gz XCAT=10.0.0.1:3001 console=ttyS0,115200 quiet'; +is( $quote->($plain), $plain, 'a command line without a separator is unchanged' ); + +is( $quote->(undef), undef, 'an undefined command line stays undefined' ); +is( $quote->(''), '', 'an empty command line stays empty' ); + +for my $char ( ';', '{', '}', '|', '&', '<', '>', '(', ')' ) { + is( $quote->("first opt=a${char}b last"), "first opt=a\\${char}b last", + "a separator $char is escaped" ); +} + +# grub2 removes the quoting before the kernel sees the value, so a value the caller quoted +# must keep the quotes it was given rather than gaining a second layer. +is( $quote->('first opt="a;b c" last'), 'first opt="a;b c" last', + 'a double quoted value is passed through unchanged' ); +is( $quote->(q{first 'ds=nocloud-net;s=http://mn/seed' last}), + q{first 'ds=nocloud-net;s=http://mn/seed' last}, + 'a single quoted value is passed through unchanged' ); + +# A quoted span and a bare separator in the same word: the span keeps its meaning and only +# the separator outside it is escaped. +is( $quote->(q{opt='a b';c}), q{opt='a b'\;c}, + 'a separator beside a quoted span is escaped without touching the span' ); +is( $quote->('opt="a;b c" other=x;y'), 'opt="a;b c" other=x\;y', + 'a bare separator beside a quoted word is escaped' ); + +# An escaped separator is already literal to grub2, so escaping it again would hand the +# kernel a backslash the caller never wrote. +is( $quote->('first ds=nocloud-net\;s=http://mn/seed last'), + 'first ds=nocloud-net\;s=http://mn/seed last', + 'a separator the caller escaped is passed through unchanged' ); +is( $quote->('a\;b c;d'), 'a\;b c\;d', + 'an escaped separator does not stop a real one being escaped' ); + +is( $quote->('BOOTIF=$net_default_mac x;y'), 'BOOTIF=$net_default_mac x\;y', + 'a variable reference is left alone so BOOTIF still expands' ); + +done_testing(); From 5b6120a182105761b11fb916034b34bb33ddce40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sat, 5 Sep 2026 01:49:31 -0300 Subject: [PATCH 21/62] fix(template): take the install mirror from the ports archive off amd64 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit archive.ubuntu.com publishes amd64 and i386 only, so a ppc64el or riscv64 node was given an apt mirror carrying no package for it and the installer could not fetch what the minimal live media lacks. The default is now the ports archive for those architectures, chosen from the osimage's architecture rather than the package directory, which is whatever path the administrator configured. site.ubuntu_apt_mirror still overrides it. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-server/lib/perl/xCAT/Template.pm | 19 ++++++++++++++----- xCAT-server/lib/xcat/plugins/debian.pm | 3 ++- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/xCAT-server/lib/perl/xCAT/Template.pm b/xCAT-server/lib/perl/xCAT/Template.pm index 177cf48ec..de954065f 100644 --- a/xCAT-server/lib/perl/xCAT/Template.pm +++ b/xCAT-server/lib/perl/xCAT/Template.pm @@ -364,7 +364,7 @@ sub subvars { $inc =~ s/#INSTALL_SOURCES_IN_PRE#/$source_in_pre/g; if (("ubuntu" eq $platform) || ("debian" eq $platform)) { $inc =~ s/#INCLUDE_OSIMAGE_PKGDIR#/$pkgdirs[-1]/; - $inc =~ s/#UBUNTU_SUBIQUITY_APT_CONFIG#/ubuntu_subiquity_apt_config($media_dir)/eg; + $inc =~ s/#UBUNTU_SUBIQUITY_APT_CONFIG#/ubuntu_subiquity_apt_config($media_dir, $namedargs{osarch})/eg; } $inc =~ s/#WRITEREPO#/$writerepo/g; } @@ -377,7 +377,7 @@ sub subvars { $inc =~ s/#INCLUDE_NOP:([^#^\n]+)#/includefile($1,1,0)/eg; $inc =~ s/#XCATVAR:([^#]+)#/envvar($1)/eg; $inc =~ s/#ENV:([^#]+)#/envvar($1)/eg; - $inc =~ s/#UBUNTU_SUBIQUITY_APT_CONFIG#/ubuntu_subiquity_apt_config($media_dir)/eg; + $inc =~ s/#UBUNTU_SUBIQUITY_APT_CONFIG#/ubuntu_subiquity_apt_config($media_dir, $namedargs{osarch})/eg; $inc =~ s/#SUBIQUITYINSTALLNIC#/subiquity_install_nic()/eg; $inc =~ s/#SUBIQUITYINSTALLMAC#/subiquity_install_mac()/eg; $inc =~ s/#MACHINEPASSWORD#/machinepassword()/eg; @@ -1766,11 +1766,20 @@ sub subiquity_install_mac { sub ubuntu_subiquity_apt_mirror { + my ($osarch) = @_; + # Apt mirror for Subiquity installs. site.ubuntu_apt_mirror overrides; otherwise default to the # public archive. The minimal live-server install media is not a complete package source, so a # real mirror is always required -- set site.ubuntu_apt_mirror to a local full mirror for # airgapped clusters (or to a geo/ports mirror as needed). - my $default = 'http://archive.ubuntu.com/ubuntu'; + # + # archive.ubuntu.com publishes amd64 and i386 only. Every other architecture, ppc64el and + # riscv64 included, is on the ports archive. The osimage's architecture decides it, because + # pkgdir is whatever path the administrator configured. + my $default = (!$osarch || $osarch =~ /^(?:amd64|i386|x86|x86_64)$/) + ? 'http://archive.ubuntu.com/ubuntu' + : 'http://ports.ubuntu.com/ubuntu-ports'; + my $site_tab = xCAT::Table->new('site'); return $default unless $site_tab; my $ent = $site_tab->getAttribs({ key => 'ubuntu_apt_mirror' }, 'value'); @@ -1779,11 +1788,11 @@ sub ubuntu_subiquity_apt_mirror sub ubuntu_subiquity_apt_config { - my ($media_dir) = @_; + my ($media_dir, $osarch) = @_; my $use_deb822 = ubuntu_subiquity_uses_deb822_sources($media_dir); my @otherpkg_sources = ubuntu_subiquity_otherpkg_sources(); - my $online_mirror = ubuntu_subiquity_apt_mirror(); + my $online_mirror = ubuntu_subiquity_apt_mirror($osarch); if ($online_mirror) { # Online install: use the configured archive as the primary apt mirror so # Subiquity/curtin can fetch whatever the minimal media lacks. No diff --git a/xCAT-server/lib/xcat/plugins/debian.pm b/xCAT-server/lib/xcat/plugins/debian.pm index b102146bd..c84f33638 100644 --- a/xCAT-server/lib/xcat/plugins/debian.pm +++ b/xCAT-server/lib/xcat/plugins/debian.pm @@ -1122,7 +1122,8 @@ sub mkinstall { $pkgdir, $platform, $partitionfile, - \%tmpl_hash + \%tmpl_hash, + osarch => $arch ); } From 44f125b63d0eccd3e7c6723f40ffcc42c5974378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sat, 5 Sep 2026 01:49:31 -0300 Subject: [PATCH 22/62] test(xCAT-test): cover the install mirror the template renders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Calls the selection the template performs. Covers the ports archive for ppc64el and both spellings of ppc64, the main archive for every x86 spelling, an unknown architecture keeping the previous default, and site.ubuntu_apt_mirror overriding both without an empty value blanking the mirror. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-test/unit/ubuntu_subiquity_apt_mirror.t | 61 ++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 xCAT-test/unit/ubuntu_subiquity_apt_mirror.t diff --git a/xCAT-test/unit/ubuntu_subiquity_apt_mirror.t b/xCAT-test/unit/ubuntu_subiquity_apt_mirror.t new file mode 100644 index 000000000..59e397746 --- /dev/null +++ b/xCAT-test/unit/ubuntu_subiquity_apt_mirror.t @@ -0,0 +1,61 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use FindBin; +use Test::More; + +# archive.ubuntu.com publishes amd64 and i386 only, so a ppc64el or riscv64 stateful install +# is handed a mirror that carries no package for it. The architecture being installed is the +# last component of the media directory. + +BEGIN { + package xCAT::Table; + our $value; + sub new { return bless {}, shift } + sub getAttribs { return defined $value ? { value => $value } : undef } + $INC{'xCAT/Table.pm'} = __FILE__; +} + +use lib "$FindBin::Bin/../../perl-xCAT"; +use lib "$FindBin::Bin/../../xCAT-server/lib/perl"; +my $module = "$FindBin::Bin/../../xCAT-server/lib/perl/xCAT/Template.pm"; +plan skip_all => 'Template.pm not found' unless -r $module; +eval { require $module; 1 } or plan skip_all => "could not load Template.pm: $@"; + +my $mirror = \&xCAT::Template::ubuntu_subiquity_apt_mirror; + +# The osimage's architecture decides the mirror. pkgdir cannot: it is whatever path the +# administrator configured, so an amd64 image under /srv/custom-media would be read as a +# non-x86 architecture and sent to the ports archive. +is( $mirror->('riscv64'), 'http://ports.ubuntu.com/ubuntu-ports', + 'a riscv64 image takes the ports archive' ); +is( $mirror->('ppc64el'), 'http://ports.ubuntu.com/ubuntu-ports', + 'a ppc64el image takes the ports archive' ); +is( $mirror->('ppc64le'), 'http://ports.ubuntu.com/ubuntu-ports', + 'the other spelling of ppc64 takes the ports archive' ); + +for my $x86 (qw(x86_64 amd64 x86 i386)) { + is( $mirror->($x86), 'http://archive.ubuntu.com/ubuntu', + "an $x86 image keeps the main archive" ); +} + +# With no architecture there is nothing to key on, so the previous default stands. +is( $mirror->(undef), 'http://archive.ubuntu.com/ubuntu', + 'an unknown architecture keeps the previous default' ); + +# site.ubuntu_apt_mirror still wins, which is how an airgapped cluster points at its own. +{ + local $xCAT::Table::value = 'http://mirror.example.invalid/ubuntu'; + is( $mirror->('riscv64'), 'http://mirror.example.invalid/ubuntu', + 'site.ubuntu_apt_mirror overrides the ports archive' ); + is( $mirror->('x86_64'), 'http://mirror.example.invalid/ubuntu', + 'site.ubuntu_apt_mirror overrides the main archive' ); +} +{ + local $xCAT::Table::value = ''; + is( $mirror->('riscv64'), 'http://ports.ubuntu.com/ubuntu-ports', + 'an empty site.ubuntu_apt_mirror does not blank the mirror' ); +} + +done_testing(); From 5957e7125db25d01bcaf883f57bb2e2e30792c2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 4 Sep 2026 16:50:18 -0300 Subject: [PATCH 23/62] feat(go-xcat): install xCAT on a riscv64 management node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit go-xcat stopped on riscv64 before it reached the package manager, so the installer xCAT documents could not set up the management node the riscv64 packages are built for. The architecture is now accepted alongside the others. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-server/share/xcat/tools/go-xcat | 34 +++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/xCAT-server/share/xcat/tools/go-xcat b/xCAT-server/share/xcat/tools/go-xcat index a076797b9..f1c452c7c 100755 --- a/xCAT-server/share/xcat/tools/go-xcat +++ b/xCAT-server/share/xcat/tools/go-xcat @@ -2461,13 +2461,45 @@ case "${GO_XCAT_OS}" in esac case "${GO_XCAT_ARCH}" in -"ppc64"|"ppc64le"|"x86_64") +"ppc64"|"ppc64le"|"riscv64"|"x86_64") ;; *) exit_if_bad 1 "${GO_XCAT_ARCH}: unsupported instruction set architecture" ;; esac +# A riscv64 management node has no legacy Genesis: its image ships as the OpenEmbedded package, +# which mknb installs. The legacy Genesis scripts and bases are therefore dropped and the +# OpenEmbedded package of the architecture is asked for instead. +# +# The x86 boot loaders stay: they are payload a management node SERVES to x86 nodes over TFTP, +# not host binaries, so a riscv64 management node needs them to boot a mixed cluster. +function riscv64_install_list() +{ + local genesis_package="xCAT-genesis-openembedded-riscv64" + type dpkg >/dev/null 2>&1 && genesis_package="xcat-genesis-openembedded-riscv64" + local package + for package in "$@"; do + case "${package}" in + *genesis-scripts-*|*genesis-base-*) + ;; + *) + printf '%s\n' "${package}" + ;; + esac + done + printf '%s\n' "${genesis_package}" +} + +if [ "${GO_XCAT_ARCH}" = "riscv64" ]; then + riscv64_list=() + while read -r package; do + riscv64_list+=("${package}") + done < <(riscv64_install_list "${GO_XCAT_INSTALL_LIST[@]}") + GO_XCAT_INSTALL_LIST=("${riscv64_list[@]}") + unset riscv64_list package +fi + GO_XCAT_LINUX_DISTRO="$(check_linux_distro)" GO_XCAT_LINUX_VERSION="$(check_linux_version)" From 33eba52b96c5a7fbbe0c69cb4299a50c5afe9b07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sun, 6 Sep 2026 02:12:31 -0300 Subject: [PATCH 24/62] test(xCAT-test): cover the packages go-xcat installs on riscv64 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-test/unit/go_xcat_riscv64_packages.t | 83 +++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 xCAT-test/unit/go_xcat_riscv64_packages.t diff --git a/xCAT-test/unit/go_xcat_riscv64_packages.t b/xCAT-test/unit/go_xcat_riscv64_packages.t new file mode 100644 index 000000000..0067209b3 --- /dev/null +++ b/xCAT-test/unit/go_xcat_riscv64_packages.t @@ -0,0 +1,83 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use File::Temp qw(tempdir); +use FindBin; +use Test::More; + +# go-xcat installs a fixed package list. On riscv64 that list asked for the amd64 and ppc64 +# Genesis scripts and bases, so the supported install path put the x86 Genesis on a riscv64 +# management node even though the xcat metapackage excludes it there. +# +# The x86 boot loaders are a different matter: they are payload the management node serves to x86 +# nodes, so they belong on a riscv64 management node of a mixed cluster and must survive the filter. +# +# The function that builds the riscv64 list is taken from the shipped script and run, so the +# assertions read the package names go-xcat would hand to the package manager. + +my $go_xcat = "$FindBin::Bin/../../xCAT-server/share/xcat/tools/go-xcat"; +plan skip_all => 'go-xcat not found' unless -r $go_xcat; + +my $tmpdir = tempdir( CLEANUP => 1 ); +my $driver = "$tmpdir/driver.sh"; +open( my $fh, '>', $driver ) or die "open $driver: $!"; +print {$fh} <<'DRIVER'; +#!/bin/bash +set -euo pipefail +eval "$(awk ' + $0 == "function riscv64_install_list()" { copy = 1 } + copy { print } + copy && /^}$/ { exit } +' "$GO_XCAT_SOURCE")" + +# The real list, as go-xcat defines it for each package manager. +if [[ ${WITH_DPKG:-0} == 1 ]]; then + dpkg() { :; } + list=(perl-xcat xcat-client xcat xcat-buildkit + xcat-genesis-scripts-amd64 xcat-genesis-scripts-ppc64 xcat-server + elilo-xcat grub2-xcat ipmitool-xcat syslinux-xcat + xcat-genesis-base-amd64 xcat-genesis-base-ppc64 xnba-undi) +else + type() { return 1; } + list=(perl-xCAT xCAT-client xCAT xCAT-buildkit + xCAT-genesis-scripts-ppc64 xCAT-genesis-scripts-x86_64 xCAT-server + elilo-xcat grub2-xcat ipmitool-xcat syslinux-xcat + xCAT-genesis-base-ppc64 xCAT-genesis-base-x86_64 xnba-undi) +fi +riscv64_install_list "${list[@]}" +DRIVER +close($fh); +chmod 0755, $driver; + +sub riscv64_list { + my ($with_dpkg) = @_; + my $out = `GO_XCAT_SOURCE='$go_xcat' WITH_DPKG=$with_dpkg bash '$driver' 2>&1`; + is( $?, 0, "the riscv64 list builds (dpkg=$with_dpkg)" ) or diag($out); + return [ grep { length } split( /\n/, $out ) ]; +} + +foreach my $case ( [ 1, 'deb', 'xcat-genesis-openembedded-riscv64', 'xcat-server' ], + [ 0, 'rpm', 'xCAT-genesis-openembedded-riscv64', 'xCAT-server' ] ) +{ + my ( $with_dpkg, $name, $genesis, $server ) = @{$case}; + my $list = riscv64_list($with_dpkg); + + is_deeply( [ grep { /genesis-scripts-/i } @{$list} ], [], + "$name: no legacy Genesis scripts on riscv64" ); + is_deeply( [ grep { /genesis-base-/i } @{$list} ], [], + "$name: no legacy Genesis base on riscv64" ); + is_deeply( [ sort grep { /^(elilo-xcat|syslinux-xcat|xnba-undi)$/ } @{$list} ], + [ sort qw(elilo-xcat syslinux-xcat xnba-undi) ], + "$name: the x86 boot payload a mixed cluster needs is still installed" ); + + ok( scalar( grep { $_ eq $genesis } @{$list} ), + "$name: the riscv64 OpenEmbedded Genesis package is asked for" ); + + # The filter must take nothing else with it. + foreach my $keep ( $server, 'grub2-xcat', 'ipmitool-xcat' ) { + ok( scalar( grep { $_ eq $keep } @{$list} ), "$name: $keep is still installed" ); + } +} + +done_testing(); From c92a6b1927bbc0a8aa600d091b358fb5779d38ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sun, 6 Sep 2026 00:17:05 -0300 Subject: [PATCH 25/62] fix(perl-xCAT): name Ubuntu in the riscv64 netboot values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The noderes.netboot table gave riscv64 the condition >=el10, so the only documented way to reach grub2 on the architecture was an EL node, although lookupNetboot answers grub2 for riscv64 whatever the operating system. The condition now names Ubuntu as well, the way ppc64 names both of its families. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- perl-xCAT/xCAT/Schema.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/perl-xCAT/xCAT/Schema.pm b/perl-xCAT/xCAT/Schema.pm index 50dde765c..4fdafac6b 100644 --- a/perl-xCAT/xCAT/Schema.pm +++ b/perl-xCAT/xCAT/Schema.pm @@ -676,7 +676,7 @@ passed as argument rather than by table value', ppc64le NonVirtualize ALL petitboot ppc64le PowerKVM Guest ALL grub2,grub2-http,grub2-tftp aarch64 >=el8 grub2 - riscv64 >=el10 grub2,grub2-http,grub2-tftp + riscv64 >=el10, >=ubuntu24.04 grub2,grub2-http,grub2-tftp ', tftpserver => 'The TFTP server for this node (as known by this node). If not set, it defaults to networks.tftpserver.', From f9baf6e05becbca92a71651db398a98f09e55d3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sun, 6 Sep 2026 01:17:04 -0300 Subject: [PATCH 26/62] test(xCAT-test): follow the riscv64 netboot values naming Ubuntu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The assertion pinned the EL-only text and failed once the documented values named Ubuntu as well. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-test/unit/lookup_netboot_arch.t | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/xCAT-test/unit/lookup_netboot_arch.t b/xCAT-test/unit/lookup_netboot_arch.t index d0491991b..2b5801bfb 100644 --- a/xCAT-test/unit/lookup_netboot_arch.t +++ b/xCAT-test/unit/lookup_netboot_arch.t @@ -2,11 +2,23 @@ use strict; use warnings; +use File::Path qw(make_path); +use File::Temp qw(tempdir); use FindBin; use lib "$FindBin::Bin/../lib"; use lib "$FindBin::Bin/../../perl-xCAT"; use Test::More; +# xCAT modules put $::XCATROOT/lib/perl ahead of @INC as they compile, so on a host with xCAT +# installed the schema, loaded after them, would come from /opt/xcat. XCATROOT points at this +# checkout before any of them compiles. +BEGIN { + my $root = tempdir( CLEANUP => 1 ); + make_path("$root/lib"); + symlink( "$FindBin::Bin/../../perl-xCAT", "$root/lib/perl" ) or die "symlink: $!"; + $ENV{XCATROOT} = $root; +} + use XCAT::Test::File qw(repo_path); use xCAT::ProfiledNodeUtils; use xCAT::Utils; @@ -56,9 +68,13 @@ is( xCAT::ProfiledNodeUtils::cal_netboot( $rule_table, [ 'aarch64', 'rhels', '9' # --------------------------------------------------------------------------- # Schema descriptions # --------------------------------------------------------------------------- -SKIP: { - skip 'xCAT::Schema is not loadable here', 5 - unless eval { require lib; lib->import( repo_path('perl-xCAT') ); require xCAT::Schema; 1 }; +# The schema comes from this checkout, not from an installed xCAT, or the descriptions checked +# below would be those of whatever version the host carries. +{ + require xCAT::Schema; + require Cwd; + like( Cwd::realpath( $INC{'xCAT/Schema.pm'} ), qr/^\Q@{[ Cwd::realpath( repo_path('perl-xCAT') ) ]}\E/, + 'xCAT::Schema is loaded from the checkout' ); like( $xCAT::Schema::tabspec{nodetype}{descriptions}{arch}, qr/\briscv64\b/, 'nodetype.arch documents riscv64 as a valid value' ); @@ -68,8 +84,9 @@ SKIP: { 'osimage.osarch documents riscv64 as a valid value' ); like( $xCAT::Schema::tabspec{osimage}{descriptions}{osarch}, qr/\bs390x\b/, 'osimage.osarch documents s390x as a valid value' ); - like( $xCAT::Schema::tabspec{noderes}{descriptions}{netboot}, qr/riscv64\s+>=el10\s+grub2,grub2-http,grub2-tftp/, - 'noderes.netboot documents the riscv64 grub2 methods' ); + like( $xCAT::Schema::tabspec{noderes}{descriptions}{netboot}, + qr/riscv64\s+>=el10, >=ubuntu24\.04\s+grub2,grub2-http,grub2-tftp/, + 'noderes.netboot documents the riscv64 grub2 methods for EL and Ubuntu' ); } done_testing(); From c32c941ed7475375ea9b8eee96daeb5964a7157d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sun, 6 Sep 2026 00:17:05 -0300 Subject: [PATCH 27/62] fix(mknb): say where the riscv64 loader comes from on Ubuntu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The note shown when boot/grub2/grub2. is missing told the administrator it comes from grub2-xcat or the EL installation media. On Ubuntu neither is true: copycd builds the loader from the grub2 package on the media, because the image the media carry cannot boot over the network. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-server/lib/xcat/plugins/mknb.pm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/xCAT-server/lib/xcat/plugins/mknb.pm b/xCAT-server/lib/xcat/plugins/mknb.pm index c7a6ced38..3c75d0634 100644 --- a/xCAT-server/lib/xcat/plugins/mknb.pm +++ b/xCAT-server/lib/xcat/plugins/mknb.pm @@ -907,9 +907,9 @@ sub process_request { } } if (exists $GRUB2_DISCOVERY_ARCHES{$arch} && !-e "$tftpdir/boot/grub2/grub2.$arch") { - # These configurations are only reachable through grub2., which xCAT - # does not build. - $callback->({ data => ["Note: $tftpdir/boot/grub2/grub2.$arch is missing; $arch nodes need it to reach these configurations (it is installed by grub2-xcat, or copied from the EL $arch installation media)"] }); + # These configurations are only reachable through grub2.. copycd builds it from + # Ubuntu media; on EL it is supplied by grub2-xcat or copied from the media. + $callback->({ data => ["Note: $tftpdir/boot/grub2/grub2.$arch is missing; $arch nodes need it to reach these configurations (copycd builds it from Ubuntu media; on EL it is installed by grub2-xcat or copied from the $arch installation media)"] }); } if ($configfileonly && !$s390x_config_error) { $callback->({ data => ["Write netboot config file done"] }); From 79cddca53e2ca2a119e4d08af795b11d2897ee11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sun, 6 Sep 2026 00:17:05 -0300 Subject: [PATCH 28/62] docs(riscv64): document Ubuntu 24.04 and 26.04 support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The guides said riscv64 covered EL10 only, and the riscv64 page listed Ubuntu as unsupported. Both support matrices now carry the architecture for Ubuntu, and the riscv64 page describes the Ubuntu paths: the loader copycd builds from the media, the installer needing none of the accommodations EL10 requires, the ports archive the packages come from, and a management node running on riscv64. The 26.04 media need the RVA23 profile, which is recorded as a limitation. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- .../building_stateless_images.rst | 2 +- .../advanced/mixed_cluster/support_matrix.rst | 75 ++++++++--------- docs/source/developers/guides/code/builds.rst | 5 +- .../basic_concepts/xcat_object/node.rst | 2 +- .../manage_clusters/riscv64/index.rst | 83 ++++++++++++++++--- .../references/man5/noderes.5.rst | 2 +- .../admin-guides/references/man7/group.7.rst | 2 +- .../admin-guides/references/man7/node.7.rst | 2 +- docs/source/overview/differentiators.rst | 2 +- docs/source/overview/support_matrix.rst | 4 +- 10 files changed, 122 insertions(+), 57 deletions(-) diff --git a/docs/source/advanced/mixed_cluster/building_stateless_images.rst b/docs/source/advanced/mixed_cluster/building_stateless_images.rst index c3a1ffa5a..590829abd 100644 --- a/docs/source/advanced/mixed_cluster/building_stateless_images.rst +++ b/docs/source/advanced/mixed_cluster/building_stateless_images.rst @@ -9,7 +9,7 @@ In a homogeneous cluster, the management node is the same hardware architecture The issues arises in a heterogeneous cluster, where the management node is running a different level operating system *or* hardware architecture as the compute nodes in which to deploy the image. The ``genimage`` command that builds stateless images depends on various utilities provided by the base operating system and needs to be run on a node with the same hardware architecture and *major* Operating System release as the nodes that will be booted from the image. -When running xCAT >= 2.17 on EL >= 8 based management node with x86_64 architecture, qemu-user-static can be used to cross-build ppc64*, aarch64 and riscv64 osimages. Therefore, you don't need to build images on systems with the target architecture anymore. +When running xCAT >= 2.17 on EL >= 8 based management node with x86_64 architecture, qemu-user-static can be used to cross-build ppc64*, aarch64 and riscv64 osimages. Therefore, you don't need to build images on systems with the target architecture anymore. The same applies to an Ubuntu management node, where ``qemu-user-static`` and ``binfmt-support`` are packages of the distribution and register the handler on install. Cross-build ppc64*/aarch64/riscv64 stateless/statelite image on x86_64 management node -------------------------------------------------------------------------------------- diff --git a/docs/source/advanced/mixed_cluster/support_matrix.rst b/docs/source/advanced/mixed_cluster/support_matrix.rst index 8bf5e69e7..50a6d9ad4 100644 --- a/docs/source/advanced/mixed_cluster/support_matrix.rst +++ b/docs/source/advanced/mixed_cluster/support_matrix.rst @@ -1,43 +1,43 @@ Support Matrix ============== -+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ -| | RHEL | SLES | RHEL | SLES | Ubuntu | RHEL | SLES | Ubuntu | RHEL | SLES | Ubuntu | RHEL | -| | ppc64 | ppc64 | x86_64 | x86_64 | x86_64 | ppc64le | ppc64le | ppc64el | aarch64 | aarch64 | aarch64 | riscv64 | -| | CN | CN | CN | CN | CN | CN | CN | CN | CN | CN | CN | CN | -+=========+=========+=========+=========+=========+=========+=========+=========+=========+=========+=========+=========+=========+ -| RHEL | | | | | | | | | | | | | -| ppc64 | yes | yes | yes | yes | yes | yes | yes | yes | no | no | no | no | -| MN/SN | | | [1]_ | [1]_ | [1]_ | | | | | | | | -+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ -| SLES | | | | | | | | | | | | | -| ppc64 | yes | yes | yes | yes | yes | yes | yes | yes | no | no | no | no | -| MN/SN | | | [1]_ | [1]_ | [1]_ | | | | | | | | -+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ -| RHEL | | | | | | | | | | | | | -| x86_64 | yes | yes | yes | yes | yes | yes | yes | yes | yes | no | no | yes | -| MN/SN | [4]_ | [4]_ | | | | | | | | | | [6]_ | -+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ -| SLES | | | | | | | | | | | | | -| x86_64 | yes | yes | yes | yes | yes | yes | yes | yes | yes | no | no | no | -| MN/SN | [4]_ | [4]_ | | | | | | | | | | | -+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ -| Ubuntu | | | | | | | | | | | | | -| x86_64 | yes | yes | yes | yes | yes | yes | yes | yes | yes | no | no | no | -| MN/SN | [5]_ | [5]_ | | | | | | | | | | | -+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ -| RHEL | | | | | | | | | | | | | -| ppc64le | yes | yes | yes | yes | yes | yes | yes | yes | no | no | no | no | -| MN/SN | [2]_ | [2]_ | | | | | | | | | | | -+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ -| SLES | | | | | | | | | | | | | -| ppc64le | no | no | yes | yes | yes | yes | yes | yes | no | no | no | no | -| MN/SN | | | | | | | | | | | | | -+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ -| Ubuntu | | | | | | | | | | | | | -| ppc64el | yes | yes | yes | yes | yes | yes | yes | yes | no | no | no | no | -| MN/SN | [3]_ | [3]_ | | | | | | | | | | | -+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ ++---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ +| | RHEL | SLES | RHEL | SLES | Ubuntu | RHEL | SLES | Ubuntu | RHEL | SLES | Ubuntu | RHEL | Ubuntu | +| | ppc64 | ppc64 | x86_64 | x86_64 | x86_64 | ppc64le | ppc64le | ppc64el | aarch64 | aarch64 | aarch64 | riscv64 | riscv64 | +| | CN | CN | CN | CN | CN | CN | CN | CN | CN | CN | CN | CN | CN | ++=========+=========+=========+=========+=========+=========+=========+=========+=========+=========+=========+=========+=========+=========+ +| RHEL | | | | | | | | | | | | | | +| ppc64 | yes | yes | yes | yes | yes | yes | yes | yes | no | no | no | no | no | +| MN/SN | | | [1]_ | [1]_ | [1]_ | | | | | | | | | ++---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ +| SLES | | | | | | | | | | | | | | +| ppc64 | yes | yes | yes | yes | yes | yes | yes | yes | no | no | no | no | no | +| MN/SN | | | [1]_ | [1]_ | [1]_ | | | | | | | | | ++---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ +| RHEL | | | | | | | | | | | | | | +| x86_64 | yes | yes | yes | yes | yes | yes | yes | yes | yes | no | no | yes | no | +| MN/SN | [4]_ | [4]_ | | | | | | | | | | [6]_ | | ++---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ +| SLES | | | | | | | | | | | | | | +| x86_64 | yes | yes | yes | yes | yes | yes | yes | yes | yes | no | no | no | no | +| MN/SN | [4]_ | [4]_ | | | | | | | | | | | | ++---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ +| Ubuntu | | | | | | | | | | | | | | +| x86_64 | yes | yes | yes | yes | yes | yes | yes | yes | yes | no | no | no | yes | +| MN/SN | [5]_ | [5]_ | | | | | | | | | | | [7]_ | ++---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ +| RHEL | | | | | | | | | | | | | | +| ppc64le | yes | yes | yes | yes | yes | yes | yes | yes | no | no | no | no | no | +| MN/SN | [2]_ | [2]_ | | | | | | | | | | | | ++---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ +| SLES | | | | | | | | | | | | | | +| ppc64le | no | no | yes | yes | yes | yes | yes | yes | no | no | no | no | no | +| MN/SN | | | | | | | | | | | | | | ++---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ +| Ubuntu | | | | | | | | | | | | | | +| ppc64el | yes | yes | yes | yes | yes | yes | yes | yes | no | no | no | no | no | +| MN/SN | [3]_ | [3]_ | | | | | | | | | | | | ++---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ Notes: @@ -54,3 +54,4 @@ Notes: .. [4] If the compute nodes are DFM managed systems, will need the ppc64le DFM and ppc64le hardware server on the management node. .. [5] Does not support DFM managed compute nodes, hardware control does not work. .. [6] riscv64 compute nodes boot through UEFI firmware and grub2 only. The management node needs the riscv64 Genesis image (``xCAT-genesis-openembedded-riscv64``) and ``/tftpboot/boot/grub2/grub2.riscv64`` (see :doc:`/guides/install-guides/yum/grub2`). EL10 compute nodes are supported, validated from an EL10 x86_64 management node; a riscv64 management node is documented in :doc:`/guides/admin-guides/manage_clusters/riscv64/index`. +.. [7] Ubuntu 24.04 and 26.04 riscv64 compute nodes, stateful and stateless, validated from an Ubuntu x86_64 management node. The riscv64 packages that carry a binary (``goconserver``, ``ipmitool-xcat``, ``conserver-xcat``) come from a riscv64 xcat-dep apt repository; see :doc:`/guides/admin-guides/manage_clusters/riscv64/index`. diff --git a/docs/source/developers/guides/code/builds.rst b/docs/source/developers/guides/code/builds.rst index 607075ce5..1146e4225 100644 --- a/docs/source/developers/guides/code/builds.rst +++ b/docs/source/developers/guides/code/builds.rst @@ -60,7 +60,10 @@ the repository declares. Only ``xCAT``, ``xCATsn`` and ``xCAT-genesis-scripts`` carry an architecture, and there the difference is packaging metadata rather than compiled output. That is why this build needs no ``sbuild`` and no per-codename chroot -- unlike xcat-deps, whose packages are compiled and genuinely differ per -release. +release. ``xCAT`` and ``xCATsn`` are built for riscv64 as well as amd64 and +ppc64el, and every release the repository serves declares the architecture; +``xCAT-genesis-scripts`` keeps the two architectures it has control files for, +because riscv64 Genesis ships as an OpenEmbedded package instead. Helpers shared by both builders live in ``build-utils/lib/XCAT/BuildUtils.pm``. diff --git a/docs/source/guides/admin-guides/basic_concepts/xcat_object/node.rst b/docs/source/guides/admin-guides/basic_concepts/xcat_object/node.rst index c72a78d42..debe934e0 100644 --- a/docs/source/guides/admin-guides/basic_concepts/xcat_object/node.rst +++ b/docs/source/guides/admin-guides/basic_concepts/xcat_object/node.rst @@ -45,7 +45,7 @@ Key Attributes +--------------------------+----------------------+-----------------------------------+ | aarch64 | >=el8 | grub2 | +--------------------------+----------------------+-----------------------------------+ - | riscv64 | >=el10 | grub2,grub2-http,grub2-tftp | + | riscv64 | >=el10, >=ubuntu24.04| grub2,grub2-http,grub2-tftp | +--------------------------+----------------------+-----------------------------------+ * postscripts: diff --git a/docs/source/guides/admin-guides/manage_clusters/riscv64/index.rst b/docs/source/guides/admin-guides/manage_clusters/riscv64/index.rst index faa4ce191..1605ae0ae 100644 --- a/docs/source/guides/admin-guides/manage_clusters/riscv64/index.rst +++ b/docs/source/guides/admin-guides/manage_clusters/riscv64/index.rst @@ -1,9 +1,10 @@ RISC-V 64-bit (riscv64) ======================= -xCAT manages RISC-V 64-bit (``riscv64``) compute nodes running EL10. Rocky -Linux 10 is the reference distribution; the RHEL 10 RISC-V developer preview -uses the same media layout. The general cluster management documentation under +xCAT manages RISC-V 64-bit (``riscv64``) compute nodes running EL10 or Ubuntu. +Rocky Linux 10 is the reference EL distribution and the RHEL 10 RISC-V developer +preview uses the same media layout; on the Ubuntu side, 24.04 and 26.04 are +supported from the live-server media. The general cluster management documentation under :doc:`/guides/admin-guides/manage_clusters/index` applies; this page only covers what is specific to the architecture. @@ -21,16 +22,31 @@ What riscv64 nodes need ``grub2-http`` is recommended for installers, whose initrd is large. * ``nodetype.arch`` and ``osimage.osarch`` are ``riscv64``. No alias is needed: ``uname -m``, rpm and dpkg all use the same token. -* ``/tftpboot/boot/grub2/grub2.riscv64``: the EL grub2 UEFI image for riscv64 - (the ``EFI/BOOT/grubriscv64.efi`` of the EL10 riscv64 BaseOS tree). - ``copycds`` publishes it from the installation media when the management node - does not have it yet, the ``grub2-xcat`` package installs the same image, and - :doc:`/guides/install-guides/yum/grub2` describes copying it by hand. An image - that is already there is never replaced. +* ``/tftpboot/boot/grub2/grub2.riscv64``: the grub2 UEFI image the firmware + loads. On EL media it is the ``EFI/BOOT/grubriscv64.efi`` of the riscv64 + BaseOS tree, which the ``grub2-xcat`` package also installs and + :doc:`/guides/install-guides/yum/grub2` describes copying by hand; ``copycds`` + publishes it when the management node does not have it yet and keeps an image + that is already there. + + Ubuntu media are different: the loader they carry boots only from the media, + because it holds a built-in configuration that searches for the live + filesystem and never reads the configuration ``nodeset`` writes. ``copycds`` + therefore builds a netboot image from the ``grub-efi-riscv64-bin`` package on + the media, with the network modules and the ``/boot/grub2`` prefix compiled + in, and installs it under that name. An image already there is kept when it + carries that prefix and those modules, which is what the loader this path + builds looks like; anything else is replaced, because the image the media + carry cannot reach the configuration. When the media cannot produce a + replacement, whatever is there is left untouched and ``copycds`` says so: + check that the nodes still boot, since the file was not built for this path. + Building the loader needs ``grub-mkimage``, which ``grub-common`` provides and + ``xcat-server`` requires. * The riscv64 Genesis image (``xCAT-genesis-openembedded-riscv64``) for discovery, BMC setup and flashing. Its kernel is loaded by grub2 through the EFI stub. ``go-xcat`` installs the package; on a management node built another - way, install it explicitly (``dnf install xCAT-genesis-openembedded-riscv64``), + way, install it explicitly (``dnf install xCAT-genesis-openembedded-riscv64``, + or ``apt install xcat-genesis-openembedded-riscv64`` on Ubuntu), the same way the images of other architectures are installed for a mixed cluster. ``xcatconfig`` runs ``mknb riscv64`` for every installed image. The management node itself is x86_64 (the validated combination, see @@ -65,6 +81,9 @@ for the other architectures. Stateful (diskful) installation -------------------------------- +EL10 +~~~~ + Import the Rocky Linux 10 riscv64 DVD with ``copycds``; it creates the ``rocky10.x-riscv64-install-compute`` osimage. The installer kernel and initrd come from ``images/pxeboot`` on the media, like x86_64 and aarch64. @@ -84,6 +103,28 @@ package lists add ``grub2-efi-riscv64`` and ``efibootmgr``, and the these files. After ``nodeset boot`` the firmware boots the installed system because the per-node ``grub2-`` loader link is removed. +Ubuntu +~~~~~~ + +Import the Ubuntu 24.04 or 26.04 riscv64 live-server ISO with ``copycds``; it +creates the ``ubuntu-riscv64-install-compute`` osimage. The installer +kernel and initrd come from ``casper/vmlinux`` and ``casper/initrd``, where the +riscv64 media keep them. + +The installer needs no riscv64 accommodation of the kind EL10 requires: Subiquity +installs ``grub-efi-riscv64`` itself, writes both ``\EFI\ubuntu\grubriscv64.efi`` +and the removable-media fallback ``\EFI\BOOT\BOOTRISCV64.EFI``, and registers the +UEFI boot entry. The shared ``compute.subiquity.tmpl`` is used unchanged. + +The autoinstall configuration is fetched from the management node over HTTP with +``ds=nocloud-net``. That argument holds a semicolon, which grub2 reads as a +command separator, so the boot loader configuration quotes it; a node whose +kernel command line ends before the seed URL is a sign of an unquoted separator. + +Packages the media do not carry are taken from ``ports.ubuntu.com``, which is +where every architecture other than amd64 and i386 is published. Set +``site.ubuntu_apt_mirror`` to point at a local mirror instead. + Crash dumps ~~~~~~~~~~~ @@ -116,6 +157,15 @@ management node needs the riscv64 user-mode emulator registered with systemd-binfmt, as described in :doc:`/advanced/mixed_cluster/building_stateless_images`. +On Ubuntu, ``genimage`` builds the image with ``debootstrap`` from the +``compute.ubuntu24.04.riscv64`` and ``compute.ubuntu26.04.riscv64`` package +lists. It bootstraps from ``ports.ubuntu.com``, because ``archive.ubuntu.com`` +publishes amd64 and i386 only; ``site.ubuntu_apt_mirror`` overrides that for a +local mirror serving every architecture. A management node of another +architecture needs the same ``qemu-user-static`` binfmt registration as EL, and +a release older than the one being built needs the target's ``debootstrap`` +script, which is a symlink to ``gutsy`` for every modern Ubuntu. + Management node on riscv64 -------------------------- @@ -142,6 +192,15 @@ that disables weak dependencies (``install_weak_deps=False``) has to install ``perl-DB_File`` explicitly to keep the Confluent client working. xCAT does not install anything from CPAN; every dependency is an rpm. +On Ubuntu the ``xcat`` and ``xcatsn`` packages are built for riscv64 and the +apt repository indexes the architecture, so ``apt install xcat`` brings up a +riscv64 management node. Everything else xCAT needs comes from the Ubuntu +riscv64 archive, except the xcat-dep packages that carry a binary: +``goconserver``, ``ipmitool-xcat`` and ``conserver-xcat`` must come from a +riscv64 xcat-dep apt repository. The remaining xcat-dep packages, including +``grub2-xcat`` and the x86-only boot loaders, are ``Architecture: all`` and +install anywhere. + Limitations ----------- @@ -151,7 +210,9 @@ Limitations HTTP boot firmware yet, and a node that ``nodeset`` has configured is offered its per-node boot loader over TFTP, as on the other architectures, so keep PXE boot enabled in the firmware. -* Ubuntu riscv64 is not supported yet. +* Ubuntu 26.04 riscv64 requires the RVA23 profile. A machine that implements + only the older profile stops with an illegal instruction early in userspace; + 24.04 runs on the older profile. * The serial console defaults to ``ttyS``; boards whose firmware exposes the console on another device need ``linuximage.addkcmdline`` or the serial settings adjusted. diff --git a/docs/source/guides/admin-guides/references/man5/noderes.5.rst b/docs/source/guides/admin-guides/references/man5/noderes.5.rst index 733a5afab..d10f8fd4b 100644 --- a/docs/source/guides/admin-guides/references/man5/noderes.5.rst +++ b/docs/source/guides/admin-guides/references/man5/noderes.5.rst @@ -62,7 +62,7 @@ noderes Attributes: ppc64le NonVirtualize ALL petitboot ppc64le PowerKVM Guest ALL grub2,grub2-http,grub2-tftp aarch64 >=el8 grub2 - riscv64 >=el10 grub2,grub2-http,grub2-tftp + riscv64 >=el10, >=ubuntu24.04 grub2,grub2-http,grub2-tftp diff --git a/docs/source/guides/admin-guides/references/man7/group.7.rst b/docs/source/guides/admin-guides/references/man7/group.7.rst index 477bda943..da1ae65dd 100644 --- a/docs/source/guides/admin-guides/references/man7/group.7.rst +++ b/docs/source/guides/admin-guides/references/man7/group.7.rst @@ -547,7 +547,7 @@ group Attributes: ppc64le NonVirtualize ALL petitboot ppc64le PowerKVM Guest ALL grub2,grub2-http,grub2-tftp aarch64 >=el8 grub2 - riscv64 >=el10 grub2,grub2-http,grub2-tftp + riscv64 >=el10, >=ubuntu24.04 grub2,grub2-http,grub2-tftp diff --git a/docs/source/guides/admin-guides/references/man7/node.7.rst b/docs/source/guides/admin-guides/references/man7/node.7.rst index 6c290d1d7..34067aaa5 100644 --- a/docs/source/guides/admin-guides/references/man7/node.7.rst +++ b/docs/source/guides/admin-guides/references/man7/node.7.rst @@ -547,7 +547,7 @@ node Attributes: ppc64le NonVirtualize ALL petitboot ppc64le PowerKVM Guest ALL grub2,grub2-http,grub2-tftp aarch64 >=el8 grub2 - riscv64 >=el10 grub2,grub2-http,grub2-tftp + riscv64 >=el10, >=ubuntu24.04 grub2,grub2-http,grub2-tftp diff --git a/docs/source/overview/differentiators.rst b/docs/source/overview/differentiators.rst index 7e9e68206..d8c335351 100644 --- a/docs/source/overview/differentiators.rst +++ b/docs/source/overview/differentiators.rst @@ -16,7 +16,7 @@ Differentiators * Support Multiple Hardware - IBM Power, IBM Power LE, x86_64, aarch64 (alpha support), riscv64 (EL10, UEFI + grub2) + IBM Power, IBM Power LE, x86_64, aarch64 (alpha support), riscv64 (EL10 and Ubuntu, UEFI + grub2) * Support Multiple Virtualization Infrastructures diff --git a/docs/source/overview/support_matrix.rst b/docs/source/overview/support_matrix.rst index 6d360f634..31b5ae63a 100644 --- a/docs/source/overview/support_matrix.rst +++ b/docs/source/overview/support_matrix.rst @@ -11,7 +11,7 @@ Operating System & Hardware Support Matrix |SLES | yes | yes | yes | yes | yes | yes | yes | no | no | | | | | | | | | | | | +-------+-------+-------+-----+-------+--------+--------+--------+----------+----------+ -|Ubuntu | no | yes | no | yes | yes | yes | yes | no | no | +|Ubuntu | no | yes | no | yes | yes | yes | yes | no | yes | | | | | | | | | | | | +-------+-------+-------+-----+-------+--------+--------+--------+----------+----------+ |CentOS | no | no | no | no | yes | yes | yes | no | no | @@ -21,4 +21,4 @@ Operating System & Hardware Support Matrix | | | | | | | | | | | +-------+-------+-------+-----+-------+--------+--------+--------+----------+----------+ -riscv64 support covers EL10 compute nodes (Rocky Linux 10 and the RHEL 10 RISC-V developer preview) that boot through UEFI firmware and grub2. See :doc:`/guides/admin-guides/manage_clusters/riscv64/index` for details. +riscv64 support covers EL10 (Rocky Linux 10 and the RHEL 10 RISC-V developer preview) and Ubuntu 24.04 and 26.04, on nodes that boot through UEFI firmware and grub2. Both stateful and stateless nodes are supported, and the management node itself can run on riscv64. See :doc:`/guides/admin-guides/manage_clusters/riscv64/index` for details. From 588a0504bf3d9009faac5ce3e39b83fa7c88d7f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sun, 6 Sep 2026 00:47:10 -0300 Subject: [PATCH 29/62] fix(xCAT): stop a riscv64 management node pulling the x86 Genesis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit xcat-genesis-scripts-amd64 is Architecture: all, so the plain Depends installed the x86 legacy Genesis scripts, and the x86 Genesis base with them, on a riscv64 management node. riscv64 has no legacy Genesis: its image ships as xcat-genesis-openembedded-riscv64, which the metapackage already recommends and mknb consumes. xCAT.spec makes the same distinction on the rpm side. The dependency is now restricted to the architectures that have a legacy Genesis. amd64 and ppc64el keep it unchanged. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT/debian/control | 2 +- xCATsn/debian/control | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/xCAT/debian/control b/xCAT/debian/control index 4889209aa..e7a8dd54b 100644 --- a/xCAT/debian/control +++ b/xCAT/debian/control @@ -9,7 +9,7 @@ Homepage: https://xcat.org/ Package: xcat Architecture: amd64 ppc64el riscv64 -Depends: ${perl:Depends}, goconserver(>= 0.3.3-snap000000000000), xcat-server (>= 2.13-snap000000000000), xcat-client (>= 2.13-snap000000000000), libdbd-sqlite3-perl, isc-dhcp-server | kea, bind9, apache2, nfs-kernel-server, libxml-parser-perl, rsync, tftpd-hpa, libnet-telnet-perl, chrony | ntp, xcat-genesis-scripts-amd64 (>= 2.13-snap000000000000) +Depends: ${perl:Depends}, goconserver(>= 0.3.3-snap000000000000), xcat-server (>= 2.13-snap000000000000), xcat-client (>= 2.13-snap000000000000), libdbd-sqlite3-perl, isc-dhcp-server | kea, bind9, apache2, nfs-kernel-server, libxml-parser-perl, rsync, tftpd-hpa, libnet-telnet-perl, chrony | ntp, xcat-genesis-scripts-amd64 (>= 2.13-snap000000000000) [!riscv64] Recommends: net-tools, nmap, kea, tftp-hpa, ipmitool-xcat (>= 1.8.17-1), syslinux[any-amd64], libsys-virt-perl, syslinux-xcat, xnba-undi, elilo-xcat, util-linux-extra, xcat-buildkit (>= 2.13-snap000000000000), xcat-probe (>= 2.13-snap000000000000), xcat-genesis-openembedded-x86-64, xcat-genesis-openembedded-ppc64le, xcat-genesis-openembedded-riscv64, xcat-genesis-openembedded-s390x Suggests: yaboot-xcat Description: Metapackage for a common, default xCAT setup diff --git a/xCATsn/debian/control b/xCATsn/debian/control index ef6baa130..f02a421ea 100644 --- a/xCATsn/debian/control +++ b/xCATsn/debian/control @@ -8,7 +8,7 @@ Homepage: https://xcat.org/ Package: xcatsn Architecture: amd64 ppc64el riscv64 -Depends: ${perl:Depends}, goconserver (>=0.3.3-snap000000000000), xcat-server (>= 2.13-snap000000000000), xcat-client (>= 2.13-snap000000000000), libdbd-sqlite3-perl, libxml-parser-perl, tftpd-hpa, libnet-telnet-perl, isc-dhcp-server | kea, bind9, apache2, nfs-kernel-server, xcat-genesis-scripts-amd64 (>= 2.13-snap000000000000) +Depends: ${perl:Depends}, goconserver (>=0.3.3-snap000000000000), xcat-server (>= 2.13-snap000000000000), xcat-client (>= 2.13-snap000000000000), libdbd-sqlite3-perl, libxml-parser-perl, tftpd-hpa, libnet-telnet-perl, isc-dhcp-server | kea, bind9, apache2, nfs-kernel-server, xcat-genesis-scripts-amd64 (>= 2.13-snap000000000000) [!riscv64] Recommends: net-tools, nmap, kea, tftp-hpa, ipmitool-xcat (>= 1.8.17-1), syslinux[any-amd64], libsys-virt-perl, syslinux-xcat, xnba-undi, elilo-xcat, xcat-buildkit (>= 2.13-snap000000000000), xcat-probe (>= 2.13-snap000000000000), xcat-genesis-openembedded-x86-64, xcat-genesis-openembedded-ppc64le, xcat-genesis-openembedded-riscv64, xcat-genesis-openembedded-s390x Suggests: yaboot-xcat Description: Metapackage for a common, default xCAT service node setup From 828d0d2fdf186ffa65aae6b7696df4fde13c8297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sun, 6 Sep 2026 00:47:10 -0300 Subject: [PATCH 30/62] test(xCAT-test): capture a riscv64 build depending on the x86 Genesis scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four assertions fail against the previous debian/control: the dependency has no architecture restriction, and dpkg's own parser still reports it for riscv64. The last assertion pins that the restriction drops nothing else. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- .../unit/xcat_riscv64_genesis_dependency.t | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 xCAT-test/unit/xcat_riscv64_genesis_dependency.t diff --git a/xCAT-test/unit/xcat_riscv64_genesis_dependency.t b/xCAT-test/unit/xcat_riscv64_genesis_dependency.t new file mode 100644 index 000000000..9e956e356 --- /dev/null +++ b/xCAT-test/unit/xcat_riscv64_genesis_dependency.t @@ -0,0 +1,72 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use File::Spec; +use FindBin; +use Test::More; + +# A riscv64 management node has no legacy Genesis: the image ships as the OpenEmbedded package +# xcat-genesis-openembedded-riscv64, which mknb consumes. xCAT.spec already says so for the rpm +# side and gives riscv64 its own dependency block. +# +# The deb side named xcat-genesis-scripts-amd64 in a plain Depends, and that package is +# Architecture: all, so apt installed the x86 Genesis scripts (and, through them, the x86 Genesis +# base) on a riscv64 management node. Restrict the dependency to the architectures that have a +# legacy Genesis, and leave amd64 and ppc64el untouched. + +my $repo_root = File::Spec->rel2abs( + File::Spec->catdir( $FindBin::Bin, '..', '..' ) +); + +sub read_file { + my ($filename) = @_; + open( my $fh, '<', $filename ) or die "Unable to read $filename: $!"; + my $content = do { local $/; <$fh> }; + close($fh); + return $content; +} + +my $have_dpkg_deps = eval { require Dpkg::Deps; 1 } ? 1 : 0; + +foreach my $pkg ( [ 'xCAT', 'xcat' ], [ 'xCATsn', 'xcatsn' ] ) { + my ( $dir, $name ) = @$pkg; + my $control = read_file( File::Spec->catfile( $repo_root, $dir, 'debian', 'control' ) ); + my ($depends) = $control =~ /^Depends:\s*(.*)$/m; + ok( defined $depends, "$name debian/control has a Depends line" ); + + my ($entry) = grep { /xcat-genesis-scripts/ } split( /\s*,\s*/, $depends ); + ok( defined $entry, "$name depends on a legacy Genesis scripts package" ); + like( $entry, qr/\[!riscv64\]/, + "$name excludes riscv64 from the legacy Genesis scripts dependency" ); + + SKIP: { + skip( "Dpkg::Deps is not available", 4 ) unless $have_dpkg_deps; + + # Dpkg::Deps cannot parse a substvar, which dpkg-gencontrol expands before it gets here. + ( my $parsable = $depends ) =~ s/\$\{[^}]*\}\s*,?\s*//g; + my %reduced = map { + my $d = Dpkg::Deps::deps_parse( $parsable, reduce_arch => 1, host_arch => $_ ); + $_ => ( defined $d ? $d->output() : '' ) + } qw(riscv64 amd64 ppc64el); + + unlike( $reduced{riscv64}, qr/xcat-genesis-scripts/, + "$name on riscv64 does not pull the legacy Genesis scripts" ); + like( $reduced{amd64}, qr/xcat-genesis-scripts-amd64/, + "$name on amd64 still pulls them" ); + like( $reduced{ppc64el}, qr/xcat-genesis-scripts-amd64/, + "$name on ppc64el still pulls them" ); + + # The restriction must not take anything else with it: every other dependency of the + # amd64 package must survive on riscv64. + my @lost = grep { $reduced{riscv64} !~ /\Q$_\E/ } + grep { !/xcat-genesis-scripts/ } + map { my $d = $_; $d =~ s/\s*\(.*//; $d =~ s/\s*\[.*//; $d } + split( /\s*,\s*/, $reduced{amd64} ); + is_deeply( \@lost, [], + "$name on riscv64 keeps every other dependency" ) + or diag( "dropped: @lost" ); + } +} + +done_testing(); From a2139d03edbf1aa7d1029e94c95bb6195bfb1039 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sun, 6 Sep 2026 02:57:13 -0300 Subject: [PATCH 31/62] fix(perl-xCAT): map the 32-bit x86 architecture to i386 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit genimage converts osimage.osarch with debian_arch and uses the result twice: it picks the apt mirror and it becomes debootstrap --arch. The map knew x86_64 but not x86, so an image with osarch=x86 selected the ports archive, which carries no i386, and then asked debootstrap for an architecture it does not know. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- perl-xCAT/xCAT/Utils.pm | 1 + 1 file changed, 1 insertion(+) diff --git a/perl-xCAT/xCAT/Utils.pm b/perl-xCAT/xCAT/Utils.pm index 4f79e92e4..7a4405330 100644 --- a/perl-xCAT/xCAT/Utils.pm +++ b/perl-xCAT/xCAT/Utils.pm @@ -4876,6 +4876,7 @@ sub splitkcmdline { # without network drivers instead of stopping at debootstrap. my %DEBIAN_ARCH = ( 'x86_64' => 'amd64', + 'x86' => 'i386', ); sub debian_arch { From 56910729ce2d581abb72e99f03890e9cc361f453 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sun, 6 Sep 2026 02:57:13 -0300 Subject: [PATCH 32/62] test(xCAT-test): drive the mirror choice with the osarch value genimage reads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mirror assertions passed a Debian architecture straight in and so never exercised the conversion genimage performs first. They now start from the xCAT osarch value, which is what let the 32-bit x86 token reach the wrong archive. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-test/unit/debian_arch_map.t | 5 +++++ xCAT-test/unit/ubuntu_genimage_apt_mirror.t | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/xCAT-test/unit/debian_arch_map.t b/xCAT-test/unit/debian_arch_map.t index 08c140cec..a98df8224 100644 --- a/xCAT-test/unit/debian_arch_map.t +++ b/xCAT-test/unit/debian_arch_map.t @@ -16,6 +16,11 @@ use xCAT::Utils; # --- what debootstrap and the package lists are given ---------------------- is(xCAT::Utils->debian_arch('x86_64'), 'amd64', 'Debian calls x86_64 amd64'); +# genimage passes this value to debootstrap --arch and reads it to pick the apt mirror, and +# debootstrap knows i386, not xCAT's x86. +is(xCAT::Utils->debian_arch('x86'), 'i386', + 'debian_arch: the 32-bit x86 token becomes i386'); + is(xCAT::Utils->debian_arch('ppc64el'), 'ppc64el', 'the Debian name for POWER LE is unchanged'); is(xCAT::Utils->debian_arch('ppc64le'), 'ppc64le', diff --git a/xCAT-test/unit/ubuntu_genimage_apt_mirror.t b/xCAT-test/unit/ubuntu_genimage_apt_mirror.t index 172ab8b47..c3ad36a43 100644 --- a/xCAT-test/unit/ubuntu_genimage_apt_mirror.t +++ b/xCAT-test/unit/ubuntu_genimage_apt_mirror.t @@ -14,6 +14,8 @@ use Test::More; # evaluated here, so this test tracks the script rather than a copy of it. my $repo_root = File::Spec->rel2abs( File::Spec->catdir( $FindBin::Bin, '..', '..' ) ); +use lib "$FindBin::Bin/../../perl-xCAT"; +require xCAT::Utils; my $genimage_path = File::Spec->catfile( $repo_root, 'xCAT-server', 'share', 'xcat', 'netboot', 'ubuntu', 'genimage' ); @@ -29,6 +31,14 @@ my ($pick) = $src =~ /^\s*(my \$mirror = \(defined \$aptmirror\[0\].*?;)\s*$/ms; ok( defined $pick, 'found the mirror override in genimage' ) or done_testing(), exit; +# genimage does not read osimage.osarch directly: it converts it first, so the value the selection +# sees is whatever xCAT::Utils::debian_arch returns. Driving the xCAT token through that conversion +# is what catches a token the map does not know. +sub choose_osarch { + my ( $osarch, $site ) = @_; + return choose( xCAT::Utils->debian_arch($osarch), $site ); +} + sub choose { my ( $uarch, $site ) = @_; my $set = defined $site ? "('$site')" : "()"; @@ -57,4 +67,14 @@ is( choose( 'amd64', 'http://mirror.example.invalid/ubuntu' ), is( choose( 'riscv64', '' ), 'http://ports.ubuntu.com/ubuntu-ports', 'an empty site.ubuntu_apt_mirror does not blank the mirror' ); +# The architecture reaches the selection as an xCAT osarch value, not as a Debian one. +is( choose_osarch('x86_64'), 'http://archive.ubuntu.com/ubuntu', + 'osarch x86_64 reaches the archive' ); +is( choose_osarch('x86'), 'http://archive.ubuntu.com/ubuntu', + 'osarch x86 reaches the archive, which is where i386 lives' ); +is( choose_osarch('riscv64'), 'http://ports.ubuntu.com/ubuntu-ports', + 'osarch riscv64 reaches the ports archive' ); +is( choose_osarch('ppc64el'), 'http://ports.ubuntu.com/ubuntu-ports', + 'osarch ppc64el reaches the ports archive' ); + done_testing(); From 9f35a4b8cb685f59ee7341f0695a568a2298fe1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sun, 6 Sep 2026 03:25:18 -0300 Subject: [PATCH 33/62] test(xCAT-test): build the riscv64 loader from real Ubuntu media MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The stubbed test proves the decisions copycd makes; this one proves the artifact. It runs the real grub-mkimage over the grub2 package of copied media and checks the image against the validation nodeset depends on, so a package layout change or a grub-mkimage that stops accepting these inputs is caught where it happens. It needs media and the grub2 tools, so it skips without XCAT_TEST_UBUNTU_RISCV64_MEDIA. Run on a management node against the 24.04 and 26.04 riscv64 trees. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- .../unit/ubuntu_copycd_grub2_loader_media.t | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 xCAT-test/unit/ubuntu_copycd_grub2_loader_media.t diff --git a/xCAT-test/unit/ubuntu_copycd_grub2_loader_media.t b/xCAT-test/unit/ubuntu_copycd_grub2_loader_media.t new file mode 100644 index 000000000..7ec01706c --- /dev/null +++ b/xCAT-test/unit/ubuntu_copycd_grub2_loader_media.t @@ -0,0 +1,63 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use File::Temp qw(tempdir); +use FindBin; +use Test::More; + +# The stubbed test proves the decisions copycd makes. This one proves the artifact: it runs the +# real grub-mkimage over the grub2 package of real Ubuntu media and checks that what comes out is +# what the management node will serve to a riscv64 node. +# +# It needs copied riscv64 media and the grub2 build tools, so it runs where those exist: +# XCAT_TEST_UBUNTU_RISCV64_MEDIA=/install/ubuntu24.04.4/riscv64 perl +# Without them it skips, which is why the stubbed test still covers the decisions. + +BEGIN { + package xCAT::TableUtils; + our $tftpdir; + sub getTftpDir { return $tftpdir; } + $INC{'xCAT/TableUtils.pm'} = __FILE__; +} + +my $media = $ENV{XCAT_TEST_UBUNTU_RISCV64_MEDIA}; +plan skip_all => 'set XCAT_TEST_UBUNTU_RISCV64_MEDIA to copied riscv64 media' unless $media; +plan skip_all => "no media at $media" unless -d $media; +foreach my $tool (qw(dpkg-deb grub-mkimage)) { + my $found = grep { -x "$_/$tool" } split( /:/, $ENV{PATH} // '' ); + plan skip_all => "$tool is not installed" unless $found; +} + +use lib "$FindBin::Bin/../../perl-xCAT"; +use lib "$FindBin::Bin/../../xCAT-server/lib/perl"; +my $plugin = "$FindBin::Bin/../../xCAT-server/lib/xcat/plugins/debian.pm"; +plan skip_all => 'debian.pm not found' unless -r $plugin; +eval { require $plugin; 1 } or plan skip_all => "could not load debian.pm: $@"; + +$xCAT::TableUtils::tftpdir = tempdir( CLEANUP => 1 ); +my @told; +my $target = xCAT_plugin::debian::install_media_grub2_loader( + $media, 'riscv64', + sub { push @told, ( $_[0]->{data} // () ), @{ $_[0]->{warning} || [] } } ); + +ok( defined $target, 'the media produced a loader' ) + or diag( join( "\n", @told ) ), done_testing(), exit; +is( $target, "$xCAT::TableUtils::tftpdir/boot/grub2/grub2.riscv64", + 'it is where nodeset serves it from' ); +ok( -s $target, '... and it is not empty' ); + +# The same checks nodeset depends on, against a real grub-mkimage image rather than a fabricated one. +my $build = { machine => 0x5064, format => 'riscv64-efi', package => 'grub-efi-riscv64-bin' }; +ok( xCAT_plugin::debian::_is_uefi_image( $target, $build->{machine} ), + 'the image is a riscv64 UEFI application' ); +ok( xCAT_plugin::debian::_is_netboot_loader( $target, $build ), + '... carrying the prefix and the modules a net boot needs' ); + +# A second import must not rebuild it: the image already in place is the one the nodes booted. +my $before = ( stat($target) )[9]; +my $again = xCAT_plugin::debian::install_media_grub2_loader( $media, 'riscv64', undef ); +is( $again, $target, 'a second import returns the loader already in place' ); +is( ( stat($target) )[9], $before, '... without rewriting it' ); + +done_testing(); From 53692323b450c5c7e349884fd0c0346f2359a9f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sun, 6 Sep 2026 04:09:12 -0300 Subject: [PATCH 34/62] fix(xCAT-server): declare the tool copycd builds the riscv64 loader with MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit copycd builds the riscv64 boot loader by running grub-mkimage, which grub-common ships on every supported Ubuntu release. Nothing declared it, so a management or service node installed without that package copies riscv64 media and produces no loader, while DHCP keeps pointing every riscv64 node at the path where the loader should be. The declaration belongs to xcat-server, which carries the plugin that runs the command, so both metapackages inherit it. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-server/debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xCAT-server/debian/control b/xCAT-server/debian/control index 4560b5462..1797c1af8 100644 --- a/xCAT-server/debian/control +++ b/xCAT-server/debian/control @@ -8,7 +8,7 @@ Homepage: https://xcat.org/ Package: xcat-server Architecture: all -Depends: ${perl:Depends}, grub2-xcat (>= 2.02-0.76.el7.1.snap201905160255), perl-xcat (>= 2.13-snap000000000000), xcat-client (>= 2.13-snap000000000000), libsys-syslog-perl, libio-socket-ssl-perl, libxml-simple-perl, make, ucf, libdbd-sqlite3-perl, libexpect-perl, libnet-dns-perl, libsoap-lite-perl, libxml-libxml-perl, libsnmp-perl, debootstrap, libdigest-sha-perl,libcrypt-rijndael-perl,libcrypt-cbc-perl,libjson-perl, libnet-https-nb-perl, libhttp-async-perl +Depends: ${perl:Depends}, grub-common, grub2-xcat (>= 2.02-0.76.el7.1.snap201905160255), perl-xcat (>= 2.13-snap000000000000), xcat-client (>= 2.13-snap000000000000), libsys-syslog-perl, libio-socket-ssl-perl, libxml-simple-perl, make, ucf, libdbd-sqlite3-perl, libexpect-perl, libnet-dns-perl, libsoap-lite-perl, libxml-libxml-perl, libsnmp-perl, debootstrap, libdigest-sha-perl,libcrypt-rijndael-perl,libcrypt-cbc-perl,libjson-perl, libnet-https-nb-perl, libhttp-async-perl Description: Server and configuration utilities of xCAT xCAT-server provides the core server and configuration management components of xCAT. From f0c7803595b0769e56ffe132559fc539c79bad77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sun, 6 Sep 2026 04:31:10 -0300 Subject: [PATCH 35/62] test(xCAT-test): cover the declaration of the loader build tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- .../xcat_server_grub_mkimage_dependency.t | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 xCAT-test/unit/xcat_server_grub_mkimage_dependency.t diff --git a/xCAT-test/unit/xcat_server_grub_mkimage_dependency.t b/xCAT-test/unit/xcat_server_grub_mkimage_dependency.t new file mode 100644 index 000000000..a130a0f3a --- /dev/null +++ b/xCAT-test/unit/xcat_server_grub_mkimage_dependency.t @@ -0,0 +1,39 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use File::Spec; +use FindBin; +use Test::More; + +# copycd builds the riscv64 boot loader by running grub-mkimage, and the plugin that runs it ships +# in xcat-server. Nothing declared the package that provides that command, so a management or +# service node installed without it copies riscv64 media and then produces no loader at all, while +# DHCP keeps pointing every riscv64 node at the path where the loader should be. +# +# grub-common provides /usr/bin/grub-mkimage on every supported Ubuntu release. That the plugin +# runs grub-mkimage is shown by ubuntu_copycd_grub2_loader.t, which drives it through a stub. + +my $repo_root = File::Spec->rel2abs( File::Spec->catdir( $FindBin::Bin, '..', '..' ) ); + +sub depends_of { + my ($package) = @_; + my $file = File::Spec->catfile( $repo_root, $package, 'debian', 'control' ); + open( my $fh, '<', $file ) or die "Unable to read $file: $!"; + my $control = do { local $/; <$fh> }; + close($fh); + my ($depends) = $control =~ /^Depends:\s*(.*)$/m; + return [ split( /\s*,\s*/, $depends // '' ) ]; +} + +my $server = depends_of('xCAT-server'); +ok( scalar( grep { /^grub-common\b/ } @{$server} ), + 'xcat-server depends on the package that provides grub-mkimage' ); + +# The plugin runs there, so the metapackages inherit it and must not carry their own copy. +foreach my $package (qw(xCAT xCATsn)) { + is_deeply( [ grep { /^grub-common\b/ } @{ depends_of($package) } ], [], + "$package leaves the dependency with the package that runs the command" ); +} + +done_testing(); From 5a541814df0d3d28ce30f5ad614990aa7efeb01f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sun, 6 Sep 2026 04:09:13 -0300 Subject: [PATCH 36/62] test(xCAT-test): pin what a riscv64 management node installs and serves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The grub-common assertion fails against the previous metapackage. The rest pins what must NOT change: a riscv64 management node still recommends the x86 boot payload and the Genesis images of the other architectures, because it serves them to the nodes it provisions. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- .../unit/xcat_riscv64_genesis_dependency.t | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/xCAT-test/unit/xcat_riscv64_genesis_dependency.t b/xCAT-test/unit/xcat_riscv64_genesis_dependency.t index 9e956e356..4a1930c4a 100644 --- a/xCAT-test/unit/xcat_riscv64_genesis_dependency.t +++ b/xCAT-test/unit/xcat_riscv64_genesis_dependency.t @@ -34,6 +34,8 @@ foreach my $pkg ( [ 'xCAT', 'xcat' ], [ 'xCATsn', 'xcatsn' ] ) { my $control = read_file( File::Spec->catfile( $repo_root, $dir, 'debian', 'control' ) ); my ($depends) = $control =~ /^Depends:\s*(.*)$/m; ok( defined $depends, "$name debian/control has a Depends line" ); + my ($recommends) = $control =~ /^Recommends:\s*(.*)$/m; + ok( defined $recommends, "$name debian/control has a Recommends line" ); my ($entry) = grep { /xcat-genesis-scripts/ } split( /\s*,\s*/, $depends ); ok( defined $entry, "$name depends on a legacy Genesis scripts package" ); @@ -41,7 +43,7 @@ foreach my $pkg ( [ 'xCAT', 'xcat' ], [ 'xCATsn', 'xcatsn' ] ) { "$name excludes riscv64 from the legacy Genesis scripts dependency" ); SKIP: { - skip( "Dpkg::Deps is not available", 4 ) unless $have_dpkg_deps; + skip( "Dpkg::Deps is not available", 7 ) unless $have_dpkg_deps; # Dpkg::Deps cannot parse a substvar, which dpkg-gencontrol expands before it gets here. ( my $parsable = $depends ) =~ s/\$\{[^}]*\}\s*,?\s*//g; @@ -66,6 +68,22 @@ foreach my $pkg ( [ 'xCAT', 'xcat' ], [ 'xCATsn', 'xcatsn' ] ) { is_deeply( \@lost, [], "$name on riscv64 keeps every other dependency" ) or diag( "dropped: @lost" ); + + # A management node of any architecture can provision nodes of another, so what it SERVES + # to those nodes -- the x86 boot payload and the Genesis images of the other architectures + # -- stays recommended everywhere. Only the legacy Genesis of this node is architecture + # specific, and that one is a dependency, not a recommendation. + my %reduced_recommends = map { + my $d = Dpkg::Deps::deps_parse( $recommends, reduce_arch => 1, host_arch => $_ ); + $_ => ( defined $d ? $d->output() : '' ) + } qw(riscv64 amd64); + + like( $reduced_recommends{riscv64}, qr/\bsyslinux-xcat\b/, + "$name on riscv64 still recommends the x86 boot payload it serves" ); + like( $reduced_recommends{riscv64}, qr/xcat-genesis-openembedded-x86-64/, + "$name on riscv64 still recommends the Genesis image of the other architectures" ); + like( $reduced_recommends{amd64}, qr/\bsyslinux-xcat\b/, + "$name on amd64 is unchanged" ); } } From a6e69e88a473a04dd18b8c3aea25382bc3bf5dad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Tue, 8 Sep 2026 17:00:42 -0300 Subject: [PATCH 37/62] fix(debian): stop reporting riscv64 as an unknown install architecture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mkinstall mapped x86_64 and x86 to their Debian names and accepted ppc64le and ppc64el. Every other architecture, riscv64 included, was logged as "Unknown arch" on each diskful install, although the install went on with the name unchanged, which is right for riscv64. Move the mapping into install_darch, which takes the Debian name from xCAT::Utils::debian_arch and knows the architectures xCAT installs Ubuntu on. riscv64 is one of them. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-server/lib/xcat/plugins/debian.pm | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/xCAT-server/lib/xcat/plugins/debian.pm b/xCAT-server/lib/xcat/plugins/debian.pm index c84f33638..2a4c2fb39 100644 --- a/xCAT-server/lib/xcat/plugins/debian.pm +++ b/xCAT-server/lib/xcat/plugins/debian.pm @@ -810,6 +810,15 @@ sub subiquity_boot_params { return (subiquity_kcmdline($base, $nfsip, $pkgdir, $instserver, $httpport, $node), undef); } +# The Debian name of an install architecture, and whether xCAT installs Ubuntu +# on it. ppc64le is kept as is: the media paths key on both spellings. +my %INSTALL_ARCH = map { $_ => 1 } qw(x86_64 x86 ppc64le ppc64el riscv64); + +sub install_darch { + my ($arch) = @_; + return ( xCAT::Utils::debian_arch($arch), $INSTALL_ARCH{ $arch // '' } ? 1 : 0 ); +} + sub mkinstall { xCAT::MsgUtils->message("S", "Doing debian mkinstall"); my $request = shift; @@ -1033,18 +1042,9 @@ sub mkinstall { xCAT::MsgUtils->trace($verbose_on_off, "d", "debian->mkinstall: pkgdir=$pkgdir pkglistfile=$pkglistfile tmplfile=$tmplfile"); } - if ($arch eq "x86_64") { - $darch = "amd64"; - } - elsif ($arch eq "x86") { - $darch = "i386"; - } - else { - if ($arch ne "ppc64le" and $arch ne "ppc64el") { - xCAT::MsgUtils->message("S", "debian.pm: Unknown arch ($arch)"); - } - $darch = $arch; - } + my $known; + ($darch, $known) = install_darch($arch); + xCAT::MsgUtils->message("S", "debian.pm: Unknown arch ($arch)") unless $known; my @missingparms; unless ($os) { From 166da7c1b61368a8921b0b091c149166cd41229f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Tue, 8 Sep 2026 17:00:42 -0300 Subject: [PATCH 38/62] test(xCAT-test): cover the install architectures debian.pm accepts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The plugin is loaded and install_darch is called for the architectures xCAT installs Ubuntu on and two it does not. Against the previous plugin the test fails on the missing function: the mapping was inline in mkinstall. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-test/unit/debian_install_arch.t | 53 ++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 xCAT-test/unit/debian_install_arch.t diff --git a/xCAT-test/unit/debian_install_arch.t b/xCAT-test/unit/debian_install_arch.t new file mode 100644 index 000000000..481718290 --- /dev/null +++ b/xCAT-test/unit/debian_install_arch.t @@ -0,0 +1,53 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use File::Path qw(make_path); +use File::Temp qw(tempdir); +use FindBin; +use Test::More; + +# mkinstall logged "Unknown arch" for every architecture other than x86_64, x86, ppc64le and +# ppc64el, so each diskful riscv64 install produced a false message. install_darch names the +# architectures xCAT installs Ubuntu on and the Debian name each one maps to. + +# xCAT modules put $::XCATROOT/lib/perl ahead of @INC as they compile, so on a host with xCAT +# installed the modules loaded after the first one would come from /opt/xcat. XCATROOT points +# at this checkout before any of them compiles. +BEGIN { + my $root = tempdir( CLEANUP => 1 ); + make_path("$root/lib"); + symlink( "$FindBin::Bin/../../perl-xCAT", "$root/lib/perl" ) or die "symlink: $!"; + $ENV{XCATROOT} = $root; +} + +BEGIN { + package xCAT::TableUtils; + our $tftpdir; + sub getTftpDir { return $tftpdir; } + $INC{'xCAT/TableUtils.pm'} = __FILE__; +} + +use lib "$FindBin::Bin/../../perl-xCAT"; +use lib "$FindBin::Bin/../../xCAT-server/lib/perl"; +my $plugin = "$FindBin::Bin/../../xCAT-server/lib/xcat/plugins/debian.pm"; +require $plugin; + +my @cases = ( + # arch Debian name installs Ubuntu + [ 'x86_64', 'amd64', 1 ], + [ 'x86', 'i386', 1 ], + [ 'ppc64le', 'ppc64le', 1 ], + [ 'ppc64el', 'ppc64el', 1 ], + [ 'riscv64', 'riscv64', 1 ], + [ 'aarch64', 'aarch64', 0 ], + [ 'sparc', 'sparc', 0 ], +); +foreach my $case (@cases) { + my ( $arch, $darch, $known ) = @$case; + my ( $got_darch, $got_known ) = xCAT_plugin::debian::install_darch($arch); + is( $got_darch, $darch, "$arch installs from the $darch tree" ); + is( $got_known, $known, $known ? "... and is an architecture xCAT installs Ubuntu on" : "... and is reported as unknown" ); +} + +done_testing(); From 92a1c5f01ce50dc7094d4469143c99bbfd0e1482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Wed, 9 Sep 2026 11:20:46 -0300 Subject: [PATCH 39/62] fix(debian): declare the perl modules perl-xcat loads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit perl-xcat declared libhtml-form-perl and nothing else. Its files load XML::Simple, IO::Socket::SSL, DBI, JSON, LWP, XML::LibXML, Expect and SNMP at top level, and every xCAT command goes through Client.pm, which loads two of them and makes XML::Parser the XML::Simple parser. On a management node xcat-server and the metapackage declared them, so the gap showed only on a client-only install. dh_perl adds no module dependencies, unlike the rpm generator. Declare them where they are loaded. Socket6 and IO::Socket::INET6 are declared too: their loads are guarded, but without them xcatd, the client, SLP, nodestat, IPMI and getipaddr have no IPv6, and the Kea DHCPv6 reservation builder receives an IPv4 address for a node. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- perl-xCAT/debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/perl-xCAT/debian/control b/perl-xCAT/debian/control index 0726e8de6..8e1714b83 100644 --- a/perl-xCAT/debian/control +++ b/perl-xCAT/debian/control @@ -8,7 +8,7 @@ Homepage: https://xcat.org/ Package: perl-xcat Architecture: all -Depends: ${perl:Depends}, libhtml-form-perl +Depends: ${perl:Depends}, libhtml-form-perl, libxml-simple-perl, libxml-parser-perl, libio-socket-ssl-perl, libdbi-perl, libjson-perl, libwww-perl, libxml-libxml-perl, libexpect-perl, libsnmp-perl, libsocket6-perl, libio-socket-inet6-perl Description: xCAT perl libraries Provides perl xCAT libraries for core functionality. Required for all xCAT installations. Includes xCAT::Table, xCAT::NodeRange, among others. From f4523640a73d8d10bd9fed9be9ef7b148b533617 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Wed, 9 Sep 2026 11:20:46 -0300 Subject: [PATCH 40/62] fix(debian): declare libcapture-tiny-perl for the z/VM helpers xcat-client ships MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit zxcatexport.pl, zxcatimport.pl and zxcatCopyCloneList.pl load Capture::Tiny at top level. The deb ships them on every architecture and declared nothing for it. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-client/debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xCAT-client/debian/control b/xCAT-client/debian/control index db9b62fe4..f18dae974 100644 --- a/xCAT-client/debian/control +++ b/xCAT-client/debian/control @@ -7,7 +7,7 @@ Standards-Version: 3.9.4 Package: xcat-client Architecture: all -Depends: ${perl:Depends}, perl-xcat (>= 2.13-snap000000000000) +Depends: ${perl:Depends}, perl-xcat (>= 2.13-snap000000000000), libcapture-tiny-perl Recommends: libsort-versions-perl, nmap Description: Core executables and data of the xCAT management project xCAT-client provides the fundamental xCAT commands (chtab, chnode, rpower, From a7bd3fb631c7840e92345ce441afe27a1e124fbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Wed, 9 Sep 2026 11:20:46 -0300 Subject: [PATCH 41/62] fix(debian): declare libcgi-pm-perl for the REST API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit xcat-server ships xcatws.cgi and restapi.pl with the endpoint enabled in apache, and both load CGI at top level. Nothing declared the module, so on a management node where nothing else pulled it every /xcatws request answered 500. The rpm package requires perl(CGI). Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-server/debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xCAT-server/debian/control b/xCAT-server/debian/control index 1797c1af8..9e0fbe6e6 100644 --- a/xCAT-server/debian/control +++ b/xCAT-server/debian/control @@ -8,7 +8,7 @@ Homepage: https://xcat.org/ Package: xcat-server Architecture: all -Depends: ${perl:Depends}, grub-common, grub2-xcat (>= 2.02-0.76.el7.1.snap201905160255), perl-xcat (>= 2.13-snap000000000000), xcat-client (>= 2.13-snap000000000000), libsys-syslog-perl, libio-socket-ssl-perl, libxml-simple-perl, make, ucf, libdbd-sqlite3-perl, libexpect-perl, libnet-dns-perl, libsoap-lite-perl, libxml-libxml-perl, libsnmp-perl, debootstrap, libdigest-sha-perl,libcrypt-rijndael-perl,libcrypt-cbc-perl,libjson-perl, libnet-https-nb-perl, libhttp-async-perl +Depends: ${perl:Depends}, grub-common, libcgi-pm-perl, grub2-xcat (>= 2.02-0.76.el7.1.snap201905160255), perl-xcat (>= 2.13-snap000000000000), xcat-client (>= 2.13-snap000000000000), libsys-syslog-perl, libio-socket-ssl-perl, libxml-simple-perl, make, ucf, libdbd-sqlite3-perl, libexpect-perl, libnet-dns-perl, libsoap-lite-perl, libxml-libxml-perl, libsnmp-perl, debootstrap, libdigest-sha-perl,libcrypt-rijndael-perl,libcrypt-cbc-perl,libjson-perl, libnet-https-nb-perl, libhttp-async-perl Description: Server and configuration utilities of xCAT xCAT-server provides the core server and configuration management components of xCAT. From 69e62f1be6adae7c976739f4a0acd1879fb71909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Wed, 9 Sep 2026 11:20:47 -0300 Subject: [PATCH 42/62] fix(debian): require nmap and ipmitool-xcat as the rpm packages do MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The deb metapackages and xcat-client only recommended nmap and ipmitool-xcat, while xCAT.spec, xCATsn.spec and xCAT-client.spec require them. An install with --no-install-recommends therefore left bmcdiscover without a scan method and out-of-band management without the tool ipmi.pm and bmcdiscover.pm hard-code. Both move to Depends. The ipmitool-xcat floor rises from 1.8.17-1 to 1.8.18-4, the floor of xCAT.spec. Builds before 1.8.18-3 lack the CVE-2020-5208 patch. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-client/debian/control | 4 ++-- xCAT/debian/control | 4 ++-- xCATsn/debian/control | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/xCAT-client/debian/control b/xCAT-client/debian/control index f18dae974..689bbb216 100644 --- a/xCAT-client/debian/control +++ b/xCAT-client/debian/control @@ -7,8 +7,8 @@ Standards-Version: 3.9.4 Package: xcat-client Architecture: all -Depends: ${perl:Depends}, perl-xcat (>= 2.13-snap000000000000), libcapture-tiny-perl -Recommends: libsort-versions-perl, nmap +Depends: ${perl:Depends}, perl-xcat (>= 2.13-snap000000000000), libcapture-tiny-perl, nmap +Recommends: libsort-versions-perl Description: Core executables and data of the xCAT management project xCAT-client provides the fundamental xCAT commands (chtab, chnode, rpower, etc) helpful in administrating systems at scale, with particular attention diff --git a/xCAT/debian/control b/xCAT/debian/control index e7a8dd54b..0037da394 100644 --- a/xCAT/debian/control +++ b/xCAT/debian/control @@ -9,8 +9,8 @@ Homepage: https://xcat.org/ Package: xcat Architecture: amd64 ppc64el riscv64 -Depends: ${perl:Depends}, goconserver(>= 0.3.3-snap000000000000), xcat-server (>= 2.13-snap000000000000), xcat-client (>= 2.13-snap000000000000), libdbd-sqlite3-perl, isc-dhcp-server | kea, bind9, apache2, nfs-kernel-server, libxml-parser-perl, rsync, tftpd-hpa, libnet-telnet-perl, chrony | ntp, xcat-genesis-scripts-amd64 (>= 2.13-snap000000000000) [!riscv64] -Recommends: net-tools, nmap, kea, tftp-hpa, ipmitool-xcat (>= 1.8.17-1), syslinux[any-amd64], libsys-virt-perl, syslinux-xcat, xnba-undi, elilo-xcat, util-linux-extra, xcat-buildkit (>= 2.13-snap000000000000), xcat-probe (>= 2.13-snap000000000000), xcat-genesis-openembedded-x86-64, xcat-genesis-openembedded-ppc64le, xcat-genesis-openembedded-riscv64, xcat-genesis-openembedded-s390x +Depends: ${perl:Depends}, goconserver(>= 0.3.3-snap000000000000), xcat-server (>= 2.13-snap000000000000), xcat-client (>= 2.13-snap000000000000), libdbd-sqlite3-perl, isc-dhcp-server | kea, bind9, apache2, nfs-kernel-server, libxml-parser-perl, rsync, tftpd-hpa, libnet-telnet-perl, chrony | ntp, nmap, ipmitool-xcat (>= 1.8.18-4), xcat-genesis-scripts-amd64 (>= 2.13-snap000000000000) [!riscv64] +Recommends: net-tools, kea, tftp-hpa, syslinux[any-amd64], libsys-virt-perl, syslinux-xcat, xnba-undi, elilo-xcat, util-linux-extra, xcat-buildkit (>= 2.13-snap000000000000), xcat-probe (>= 2.13-snap000000000000), xcat-genesis-openembedded-x86-64, xcat-genesis-openembedded-ppc64le, xcat-genesis-openembedded-riscv64, xcat-genesis-openembedded-s390x Suggests: yaboot-xcat Description: Metapackage for a common, default xCAT setup xCAT is Extreme Cluster/Cloud Administration Toolkit. xCAT offers complete diff --git a/xCATsn/debian/control b/xCATsn/debian/control index f02a421ea..460081543 100644 --- a/xCATsn/debian/control +++ b/xCATsn/debian/control @@ -8,8 +8,8 @@ Homepage: https://xcat.org/ Package: xcatsn Architecture: amd64 ppc64el riscv64 -Depends: ${perl:Depends}, goconserver (>=0.3.3-snap000000000000), xcat-server (>= 2.13-snap000000000000), xcat-client (>= 2.13-snap000000000000), libdbd-sqlite3-perl, libxml-parser-perl, tftpd-hpa, libnet-telnet-perl, isc-dhcp-server | kea, bind9, apache2, nfs-kernel-server, xcat-genesis-scripts-amd64 (>= 2.13-snap000000000000) [!riscv64] -Recommends: net-tools, nmap, kea, tftp-hpa, ipmitool-xcat (>= 1.8.17-1), syslinux[any-amd64], libsys-virt-perl, syslinux-xcat, xnba-undi, elilo-xcat, xcat-buildkit (>= 2.13-snap000000000000), xcat-probe (>= 2.13-snap000000000000), xcat-genesis-openembedded-x86-64, xcat-genesis-openembedded-ppc64le, xcat-genesis-openembedded-riscv64, xcat-genesis-openembedded-s390x +Depends: ${perl:Depends}, goconserver (>=0.3.3-snap000000000000), xcat-server (>= 2.13-snap000000000000), xcat-client (>= 2.13-snap000000000000), libdbd-sqlite3-perl, libxml-parser-perl, tftpd-hpa, libnet-telnet-perl, isc-dhcp-server | kea, bind9, apache2, nfs-kernel-server, nmap, ipmitool-xcat (>= 1.8.18-4), xcat-genesis-scripts-amd64 (>= 2.13-snap000000000000) [!riscv64] +Recommends: net-tools, kea, tftp-hpa, syslinux[any-amd64], libsys-virt-perl, syslinux-xcat, xnba-undi, elilo-xcat, xcat-buildkit (>= 2.13-snap000000000000), xcat-probe (>= 2.13-snap000000000000), xcat-genesis-openembedded-x86-64, xcat-genesis-openembedded-ppc64le, xcat-genesis-openembedded-riscv64, xcat-genesis-openembedded-s390x Suggests: yaboot-xcat Description: Metapackage for a common, default xCAT service node setup xCATsn is a service node management package intended for at-scale From c486387bd4e969eb4b18936fe2adf2b82a7fd690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Wed, 9 Sep 2026 11:21:57 -0300 Subject: [PATCH 43/62] test(xCAT-test): pin the Debian dependency declarations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The control files are parsed the way dpkg reads them, one stanza per binary package, and the declarations of perl-xcat, xcat-client, xcat-server, xcat and xcatsn are held to the modules and tools their files use, with the ipmitool-xcat floor. 25 of 26 assertions fail against the previous control files. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-test/unit/debian_control_declarations.t | 73 ++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 xCAT-test/unit/debian_control_declarations.t diff --git a/xCAT-test/unit/debian_control_declarations.t b/xCAT-test/unit/debian_control_declarations.t new file mode 100644 index 000000000..e97feccbb --- /dev/null +++ b/xCAT-test/unit/debian_control_declarations.t @@ -0,0 +1,73 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use File::Spec; +use FindBin; +use Test::More; + +# dh_perl adds no module dependencies, unlike the rpm generator, so a deb declares only what its +# control file names. perl-xcat declared libhtml-form-perl alone while its files load nine other +# modules at top level, xcat-server shipped the REST API without CGI, xcat-client ships z/VM +# helpers that load Capture::Tiny, and nmap and ipmitool-xcat were recommendations where the rpm +# packages require them. + +my $repo_root = File::Spec->rel2abs( File::Spec->catdir( $FindBin::Bin, '..', '..' ) ); + +# The relation fields of one binary package stanza as sets of package names, plus the version +# bound of each relation that has one. +sub relations_of { + my ( $source, $package ) = @_; + my $file = File::Spec->catfile( $repo_root, $source, 'debian', 'control' ); + open( my $fh, '<', $file ) or die "Unable to read $file: $!"; + my $control = do { local $/; <$fh> }; + close($fh); + my ($stanza) = $control =~ /^Package:\s*\Q$package\E\s*\n(.*?)(?:\n\n|\z)/ms; + die "no stanza for $package in $file" unless defined $stanza; + my %rel; + foreach my $field (qw(Depends Recommends)) { + my ($line) = $stanza =~ /^$field:\s*(.*)$/m; + foreach my $entry ( split( /[,|]/, $line // '' ) ) { + my ( $name, $bound ) = $entry =~ /^\s*(\S+?)\s*(?:\(([^)]*)\))?\s*(?:\[[^\]]*\])?\s*$/; + next unless defined $name; + $rel{$field}{$name} = 1; + $rel{version}{$name} = $bound if defined $bound; + } + } + return \%rel; +} + +sub depends_on { + my ( $rel, $name, $label ) = @_; + ok( $rel->{Depends}{$name}, "$label depends on $name" ); + ok( !$rel->{Recommends}{$name}, "... and no longer only recommends it" ) if $rel->{Recommends}{$name}; +} + +my $perl_xcat = relations_of( 'perl-xCAT', 'perl-xcat' ); +depends_on( $perl_xcat, $_, 'perl-xcat' ) for qw( + libhtml-form-perl libxml-simple-perl libxml-parser-perl libio-socket-ssl-perl libdbi-perl libjson-perl + libwww-perl libxml-libxml-perl libexpect-perl libsnmp-perl libsocket6-perl libio-socket-inet6-perl +); + +my $client = relations_of( 'xCAT-client', 'xcat-client' ); +depends_on( $client, 'libcapture-tiny-perl', 'xcat-client' ); + +my $server = relations_of( 'xCAT-server', 'xcat-server' ); +depends_on( $server, 'libcgi-pm-perl', 'xcat-server' ); + +# nmap and ipmitool-xcat are hard requirements on EL and were only recommended here. +my $xcat = relations_of( 'xCAT', 'xcat' ); +my $xcatsn = relations_of( 'xCATsn', 'xcatsn' ); +foreach my $case ( [ $xcat, 'xcat' ], [ $xcatsn, 'xcatsn' ], [ $client, 'xcat-client' ] ) { + my ( $rel, $label ) = @$case; + ok( $rel->{Depends}{nmap}, "$label depends on nmap" ); + ok( !$rel->{Recommends}{nmap}, "... and does not recommend it as well" ); +} +foreach my $case ( [ $xcat, 'xcat' ], [ $xcatsn, 'xcatsn' ] ) { + my ( $rel, $label ) = @$case; + ok( $rel->{Depends}{'ipmitool-xcat'}, "$label depends on ipmitool-xcat" ); + ok( !$rel->{Recommends}{'ipmitool-xcat'}, "... and does not recommend it as well" ); + is( $rel->{version}{'ipmitool-xcat'}, '>= 1.8.18-4', "... at the floor xCAT.spec requires" ); +} + +done_testing(); From fa00892778cca204c8196f954ba4bc1394c30887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:58:32 -0300 Subject: [PATCH 44/62] fix(postscripts): stop passing the deprecated apt --force-yes flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ospkgs and otherpkgs passed --force-yes to every apt-get upgrade and install. apt has printed a deprecation warning for it since 1.1, and the flag also allowed downgrades, changes to held packages and the removal of essential packages, none of which an unattended package update should do. Three of the ospkgs installs relied on it alone for the unsigned xCAT repositories and ran without DEBIAN_FRONTEND=noninteractive, unlike the upgrade. The 8 call sites now go through xcat_apt_get in xcatpkgutils.sh, which runs apt-get with -y and --allow-unauthenticated under DEBIAN_FRONTEND=noninteractive. Each call site keeps its own quiet, Dpkg and recommends options. A held package or an explicit older version now fails the install instead of being forced, as on the rpm side. --allow-unauthenticated is accepted by every apt release xCAT provisions. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT/postscripts/ospkgs | 8 ++++---- xCAT/postscripts/otherpkgs | 8 ++++---- xCAT/postscripts/xcatpkgutils.sh | 6 ++++++ 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/xCAT/postscripts/ospkgs b/xCAT/postscripts/ospkgs index 712be9738..fb23ab952 100755 --- a/xCAT/postscripts/ospkgs +++ b/xCAT/postscripts/ospkgs @@ -829,7 +829,7 @@ elif ( pmatch "$OSVER" "ubuntu*" ); then # upgrade existing packages apt-get -y update - command="DEBIAN_FRONTEND=noninteractive apt-get -y --allow-unauthenticated --force-yes -o Dpkg::Options::='--force-confold' -o Dpkg::Options::='--force-confdef' upgrade " + command="xcat_apt_get -o Dpkg::Options::='--force-confold' -o Dpkg::Options::='--force-confdef' upgrade" echo "=== $command" eval $command R=$? @@ -841,7 +841,7 @@ elif ( pmatch "$OSVER" "ubuntu*" ); then # but keeping this here doesn't really hurt. Anything that looks like a group will # be installed as though it were a package.) if [ -n "$groups" ]; then - command="$ENVLIST apt-get -q -y --force-yes install --no-install-recommends $groups" + command="$ENVLIST xcat_apt_get -q install --no-install-recommends $groups" echo "=== $command" eval $command R=$? @@ -855,7 +855,7 @@ elif ( pmatch "$OSVER" "ubuntu*" ); then # install packages if [ -n "$pkgs" ]; then - command="$ENVLIST apt-get -q -y --force-yes install --no-install-recommends $pkgs" + command="$ENVLIST xcat_apt_get -q install --no-install-recommends $pkgs" echo "=== $command" eval $command R=$? @@ -865,7 +865,7 @@ elif ( pmatch "$OSVER" "ubuntu*" ); then fi if [ -n "$cudapkgs" ]; then - command="$ENVLIST apt-get -q -y --force-yes install --no-install-recommends $cudapkgs" + command="$ENVLIST xcat_apt_get -q install --no-install-recommends $cudapkgs" echo "=== $command" # the nvidia-346 postinstall script will trigger the building of nvidia driver, it will use the ARCH environment parameter, unset the ARCH env variable will resolve this issue original_arch=$ARCH diff --git a/xCAT/postscripts/otherpkgs b/xCAT/postscripts/otherpkgs index f252fc393..c89256e86 100755 --- a/xCAT/postscripts/otherpkgs +++ b/xCAT/postscripts/otherpkgs @@ -905,9 +905,9 @@ EOF` elif [ $hasapt -eq 1 ]; then apt_get_update_if_repos_changed $REPOFILE if [ $VERBOSE ]; then - echo "$envlist DEBIAN_FRONTEND=noninteractive apt-get -y --allow-unauthenticated --force-yes -o Dpkg::Options::='--force-confold' -o Dpkg::Options::='--force-confdef' upgrade" + echo "$envlist xcat_apt_get -o Dpkg::Options::='--force-confold' -o Dpkg::Options::='--force-confdef' upgrade" fi - result=`eval $envlist DEBIAN_FRONTEND=noninteractive apt-get -y --allow-unauthenticated --force-yes -o Dpkg::Options::='--force-confold' -o Dpkg::Options::='--force-confdef' upgrade 2>&1` + result=`eval $envlist xcat_apt_get -o Dpkg::Options::='--force-confold' -o Dpkg::Options::='--force-confdef' upgrade 2>&1` R=$? if [ $R -ne 0 ]; then RETURNVAL=$R @@ -1022,9 +1022,9 @@ EOF` elif [ $hasapt -eq 1 ]; then apt_get_update_if_repos_changed $REPOFILE if [ $VERBOSE ]; then - echo "$envlist DEBIAN_FRONTEND=noninteractive apt-get -q -y --force-yes -o Dpkg::Options::='--force-confold' -o Dpkg::Options::='--force-confdef' install $repo_pkgs" + echo "$envlist xcat_apt_get -q -o Dpkg::Options::='--force-confold' -o Dpkg::Options::='--force-confdef' install $repo_pkgs" fi - result=`eval $envlist DEBIAN_FRONTEND=noninteractive apt-get -q -y --force-yes -o Dpkg::Options::='--force-confold' -o Dpkg::Options::='--force-confdef' install $repo_pkgs 2>&1` + result=`eval $envlist xcat_apt_get -q -o Dpkg::Options::='--force-confold' -o Dpkg::Options::='--force-confdef' install $repo_pkgs 2>&1` R=$? if [ $R -ne 0 ]; then RETURNVAL=$R diff --git a/xCAT/postscripts/xcatpkgutils.sh b/xCAT/postscripts/xcatpkgutils.sh index 766f98ac7..7848cd755 100755 --- a/xCAT/postscripts/xcatpkgutils.sh +++ b/xCAT/postscripts/xcatpkgutils.sh @@ -14,6 +14,12 @@ xcat_find_rpm_package_manager() fi } +# The xCAT repositories are unsigned, and an unattended node cannot answer a debconf prompt. +xcat_apt_get() +{ + DEBIAN_FRONTEND=noninteractive apt-get -y --allow-unauthenticated "$@" +} + # Keep the marker assignment last so callers know the whole library loaded. xcat_is_el_modular_pkgdir() From c7b442598e220e52c0c1a1b604d522fa10761bcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:58:32 -0300 Subject: [PATCH 45/62] test(xCAT-test): cover the apt calls of ospkgs and otherpkgs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit xcat_apt_get is sourced from xcatpkgutils.sh and called with a shadowed apt-get that records its environment and arguments. The apt block of ospkgs and the two apt commands of otherpkgs are extracted from the scripts and executed the same way, since neither script can be sourced. Against the previous scripts 6 of the 7 tests fail: the helper does not exist, and the recorded calls carry --force-yes. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-test/bats/postscripts_apt_get.bats | 137 ++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 xCAT-test/bats/postscripts_apt_get.bats diff --git a/xCAT-test/bats/postscripts_apt_get.bats b/xCAT-test/bats/postscripts_apt_get.bats new file mode 100644 index 000000000..da2fbcf95 --- /dev/null +++ b/xCAT-test/bats/postscripts_apt_get.bats @@ -0,0 +1,137 @@ +#!/usr/bin/env bats + +load 'helpers/shell_source' + +setup() +{ + PKGUTILS="$(repo_path 'xCAT/postscripts/xcatpkgutils.sh')" + OSPKGS="$(repo_path 'xCAT/postscripts/ospkgs')" + OTHERPKGS="$(repo_path 'xCAT/postscripts/otherpkgs')" + [ -r "$PKGUTILS" ] || skip "$PKGUTILS is required" + [ -r "$OSPKGS" ] || skip "$OSPKGS is required" + [ -r "$OTHERPKGS" ] || skip "$OTHERPKGS is required" + APT_LOG="${BATS_TEST_TMPDIR}/apt-get.log" + export PKGUTILS OSPKGS OTHERPKGS APT_LOG +} + +# Every apt-get call is recorded as "|" and answers with APT_STATUS. +shadow_apt_get() +{ + apt-get() + { + printf '%s|%s\n' "${DEBIAN_FRONTEND:-unset}" "$*" >>"$APT_LOG" + return "${APT_STATUS:-0}" + } +} + +apt_call() +{ + sed -n "${1}p" "$APT_LOG" +} + +apt_calls() +{ + wc -l <"$APT_LOG" | tr -d ' ' +} + +run_ospkgs_apt_block() +{ + local block + block="$(extract_line_range "$OSPKGS" '# upgrade existing packages' '# remove packages')" || return 99 + local ENVLIST="" groups="" pkgs=" foo bar" cudapkgs="" RETURNVAL=0 ARCH=x86_64 + eval "$block" + printf 'RETURNVAL=%s\n' "$RETURNVAL" +} + +run_otherpkgs_apt_line() +{ + local line + line="$(extract_first_matching_line "$OTHERPKGS" "$1")" || return 99 + local envlist="" repo_pkgs="foo bar" result="" + eval "$line" + printf 'R=%s\n' "$?" + printf '%s\n' "$result" +} + +@test "the postscripts and the package utilities ship executable" { + [ -x "$OSPKGS" ] + [ -x "$OTHERPKGS" ] + [ -x "$PKGUTILS" ] +} + +@test "xcat_apt_get runs apt-get unattended and accepts the unsigned xCAT repositories" { + source "$PKGUTILS" + shadow_apt_get + + run xcat_apt_get -q install --no-install-recommends foo bar + [ "$status" -eq 0 ] + [ "$(apt_call 1)" = "noninteractive|-y --allow-unauthenticated -q install --no-install-recommends foo bar" ] + [ "$(apt_calls)" -eq 1 ] +} + +@test "xcat_apt_get returns the apt-get status" { + source "$PKGUTILS" + shadow_apt_get + + APT_STATUS=100 run xcat_apt_get upgrade + [ "$status" -eq 100 ] +} + +@test "a pkglist environment prefix reaches apt-get through the eval the postscripts use" { + source "$PKGUTILS" + apt-get() + { + printf '%s\n' "${ACCEPT_EULA:-unset}" >>"$APT_LOG" + } + local ENVLIST="ACCEPT_EULA=y" + + run eval "$ENVLIST xcat_apt_get -q install foo" + [ "$status" -eq 0 ] + [ "$(apt_call 1)" = "y" ] +} + +@test "ospkgs upgrades and installs through xcat_apt_get without --force-yes" { + source "$PKGUTILS" + shadow_apt_get + + run run_ospkgs_apt_block + [ "$status" -eq 0 ] + [[ "$output" == *'RETURNVAL=0'* ]] + [ "$(apt_call 1 | cut -d'|' -f2)" = "-y update" ] + [ "$(apt_call 2)" = "noninteractive|-y --allow-unauthenticated -o Dpkg::Options::=--force-confold -o Dpkg::Options::=--force-confdef upgrade" ] + [ "$(apt_call 3)" = "noninteractive|-y --allow-unauthenticated -q install --no-install-recommends foo bar" ] + [ "$(apt_calls)" -eq 3 ] + ! grep -q -- '--force-yes' "$APT_LOG" +} + +@test "ospkgs keeps the apt-get failure status and still runs the later steps" { + source "$PKGUTILS" + shadow_apt_get + + APT_STATUS=100 run run_ospkgs_apt_block + [ "$status" -eq 0 ] + [[ "$output" == *'RETURNVAL=100'* ]] + [ "$(apt_calls)" -eq 3 ] +} + +@test "otherpkgs upgrades through xcat_apt_get without --force-yes" { + source "$PKGUTILS" + shadow_apt_get + + run run_otherpkgs_apt_line 'result=`eval [$]envlist .*Dpkg::Options.* upgrade 2>&1`' + [ "$status" -eq 0 ] + [[ "$output" == *'R=0'* ]] + [ "$(apt_call 1)" = "noninteractive|-y --allow-unauthenticated -o Dpkg::Options::=--force-confold -o Dpkg::Options::=--force-confdef upgrade" ] + [ "$(apt_calls)" -eq 1 ] +} + +@test "otherpkgs installs through xcat_apt_get without --force-yes" { + source "$PKGUTILS" + shadow_apt_get + + run run_otherpkgs_apt_line 'result=`eval [$]envlist .*Dpkg::Options.* install [$]repo_pkgs 2>&1`' + [ "$status" -eq 0 ] + [[ "$output" == *'R=0'* ]] + [ "$(apt_call 1)" = "noninteractive|-y --allow-unauthenticated -q -o Dpkg::Options::=--force-confold -o Dpkg::Options::=--force-confdef install foo bar" ] + [ "$(apt_calls)" -eq 1 ] +} From efef7546ddc59b99fda86ecb9bf0adb14e9afe69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Wed, 9 Sep 2026 20:10:23 -0300 Subject: [PATCH 46/62] fix(ospkgs): report a failed cuda package install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both cuda install blocks read the package manager status after they had restored and exported ARCH, so R was always 0 and a failed cuda install left the node reporting success. The status is now read directly after the install on the apt path and on the yum and dnf path. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT/postscripts/ospkgs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xCAT/postscripts/ospkgs b/xCAT/postscripts/ospkgs index fb23ab952..2900a92c0 100755 --- a/xCAT/postscripts/ospkgs +++ b/xCAT/postscripts/ospkgs @@ -871,10 +871,10 @@ elif ( pmatch "$OSVER" "ubuntu*" ); then original_arch=$ARCH unset ARCH eval $command + R=$? # re declare the ARCH env after installing cuda command done ARCH=$original_arch export ARCH - R=$? if [ $R -ne 0 ]; then RETURNVAL=$R fi @@ -997,10 +997,10 @@ else original_arch=$ARCH unset ARCH result=`eval $cmd 2>&1` + R=$? # re declare the ARCH env after installing cuda command done ARCH=$original_arch export ARCH - R=$? if [ $R -ne 0 ]; then RETURNVAL=$R logger -t $log_label -p local4.info "ospkgs: $cmd\n $result" From 58828b7a0e532be87a0a54cb5a20ac171a7786f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Wed, 9 Sep 2026 20:10:24 -0300 Subject: [PATCH 47/62] test(xCAT-test): cover the cuda install status of ospkgs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ospkgs apt block runs with a shadowed apt-get that fails only for the cuda install, and RETURNVAL must carry that status. Against the previous script the test fails with RETURNVAL=0. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-test/bats/postscripts_apt_get.bats | 44 ++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/xCAT-test/bats/postscripts_apt_get.bats b/xCAT-test/bats/postscripts_apt_get.bats index da2fbcf95..9d295a583 100644 --- a/xCAT-test/bats/postscripts_apt_get.bats +++ b/xCAT-test/bats/postscripts_apt_get.bats @@ -38,7 +38,7 @@ run_ospkgs_apt_block() { local block block="$(extract_line_range "$OSPKGS" '# upgrade existing packages' '# remove packages')" || return 99 - local ENVLIST="" groups="" pkgs=" foo bar" cudapkgs="" RETURNVAL=0 ARCH=x86_64 + local ENVLIST="" groups="" pkgs=" foo bar" cudapkgs="${1:-}" RETURNVAL=0 ARCH=x86_64 eval "$block" printf 'RETURNVAL=%s\n' "$RETURNVAL" } @@ -114,6 +114,48 @@ run_otherpkgs_apt_line() [ "$(apt_calls)" -eq 3 ] } +@test "ospkgs reports a failed cuda package install" { + source "$PKGUTILS" + apt-get() + { + printf '%s|%s\n' "${DEBIAN_FRONTEND:-unset}" "$*" >>"$APT_LOG" + case "$*" in + *cuda*) return 100 ;; + esac + return 0 + } + + run run_ospkgs_apt_block " cuda-toolkit" + [ "$status" -eq 0 ] + [[ "$output" == *'RETURNVAL=100'* ]] + [ "$(apt_call 4)" = "noninteractive|-y --allow-unauthenticated -q install --no-install-recommends cuda-toolkit" ] + [ "$(apt_calls)" -eq 4 ] +} + +run_ospkgs_rpm_cuda_block() +{ + local block tail="${BATS_TEST_TMPDIR}/ospkgs-rpm-tail" + sed -n '/#install cuda package if any/,$p' "$OSPKGS" >"$tail" + block="$(extract_shell_if_block "$tail" 'if [ -n "$cudapkgs" ]; then')" || return 99 + local ENVLIST="" yumcmd=fake_dnf cudapkgs=" cuda-toolkit" RETURNVAL=0 ARCH=x86_64 debug=0 log_label=ospkgs + logger() { :; } + fake_dnf() + { + printf '%s\n' "$*" >>"$APT_LOG" + return 100 + } + eval "$block" + printf 'RETURNVAL=%s\n' "$RETURNVAL" +} + +@test "ospkgs reports a failed cuda package install on yum and dnf nodes" { + run run_ospkgs_rpm_cuda_block + [ "$status" -eq 0 ] + [[ "$output" == *'RETURNVAL=100'* ]] + [ "$(apt_call 1)" = "-y install cuda-toolkit" ] + [ "$(apt_calls)" -eq 1 ] +} + @test "otherpkgs upgrades through xcat_apt_get without --force-yes" { source "$PKGUTILS" shadow_apt_get From 955713c4b2dc1f961a4d4b70c0d0de8e9fc03f75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 10 Sep 2026 02:10:24 -0300 Subject: [PATCH 48/62] fix(detect_dhcpd): capture into a private file and stop only its own tcpdump MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both copies wrote the capture to /tmp/dhcpdumpfile.log, so a file left by another user or an earlier root run blocked the probe and two runs overwrote each other. tcpdump ran behind a shell, so the script killed it by searching the process table for any tcpdump on the interface, and a tcpdump that failed to start left an empty file that read as zero DHCP servers. An interrupt left the capture running. The capture file is now a private temporary file removed on every exit. The child execs tcpdump itself, so the script stops and reaps exactly that pid, and a tcpdump that ended before the window did fails the run. A failed send and an INT or TERM stop the capture and exit 1 as well. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-probe/subcmds/detect_dhcpd | 81 +++++++++++++++++------ xCAT-server/share/xcat/tools/detect_dhcpd | 81 ++++++++++++++++------- 2 files changed, 119 insertions(+), 43 deletions(-) diff --git a/xCAT-probe/subcmds/detect_dhcpd b/xCAT-probe/subcmds/detect_dhcpd index c8e073d88..9cb01f23b 100755 --- a/xCAT-probe/subcmds/detect_dhcpd +++ b/xCAT-probe/subcmds/detect_dhcpd @@ -7,6 +7,8 @@ use lib "$::XCATROOT/probe/lib/perl"; use probe_utils; use xCAT::CommandUtils; use File::Basename; +use File::Temp qw(tempfile); +use POSIX qw(_exit sigprocmask WNOHANG SIG_BLOCK SIG_SETMASK SIGINT SIGTERM); use IO::Socket::INET; use Time::HiRes qw(gettimeofday sleep); use Getopt::Long; @@ -17,7 +19,6 @@ my $program_name = basename("$0"); my $output = "stdout"; my $duration = 10; my $test = 0; -my $dumpfile = "/tmp/dhcpdumpfile.log"; my $nic; $::USAGE = "Usage: @@ -153,44 +154,57 @@ my $package = packdhcppkg($MAC); probe_utils->send_msg("$output", "i", "Start to detect DHCP, please wait $duration seconds"); $msg = "fork a process to capture the packet by tcpdump"; +my ($dumpfh, $dumpfile) = tempfile("detect_dhcpd.XXXXXX", TMPDIR => 1, UNLINK => 1); +close($dumpfh); +# INT and TERM stay blocked from the fork until the handler that stops the capture is in place. +my $stop_signals = POSIX::SigSet->new(SIGINT, SIGTERM); +my $signal_mask = POSIX::SigSet->new(); +sigprocmask(SIG_BLOCK, $stop_signals, $signal_mask); my $pid = fork; if (!defined $pid) { + sigprocmask(SIG_SETMASK, $signal_mask); probe_utils->send_msg("$output", "f", $msg); exit 1; } elsif ($pid == 0) { - # Child process - my $cmd = "$tcpdump -i $nic port 68 -n -vvvvvv > $dumpfile 2>/dev/null"; - `$cmd`; - exit 0; + # Child process: tcpdump itself, so the parent owns exactly this pid. + sigprocmask(SIG_SETMASK, $signal_mask); + open(STDOUT, '>', $dumpfile) or _exit(1); + open(STDERR, '>', '/dev/null'); + exec($tcpdump, '-i', $nic, 'port', '68', '-n', '-vvvvvv'); + _exit(1); } +$SIG{INT} = $SIG{TERM} = sub { kill_child(); exit 1; }; +sigprocmask(SIG_SETMASK, $signal_mask); probe_utils->send_msg("$output", "d", "The id of process which is used to capture the packet by tcpdump is $pid") if ($::VERBOSE); my $start = Time::HiRes::gettimeofday(); $start =~ s/(\d.*)\.(\d.*)/$1/; my $end = $start; while ($end - $start <= $duration) { - $sock->send($package); - probe_utils->send_msg("$output", "d", "Send DHCP rquest result: $@") if ($::VERBOSE && $@); + unless ($sock->send($package)) { + probe_utils->send_msg("$output", "d", "Send DHCP discover error: $!") if ($::VERBOSE); + probe_utils->send_msg("$output", "f", "Send out DHCP discover"); + kill_child(); + exit 1; + } sleep 2; $end = Time::HiRes::gettimeofday(); $end =~ s/(\d.*)\.(\d.*)/$1/; } -$msg = "Kill the process which is used to capture the packet by tcpdump"; -kill_child(); -waitpid($pid, 0); -sleep 1; -`ps aux|grep -v grep |grep $pid > /dev/null 2>&1`; -if (!$?) { +$msg = "Capture the packets by tcpdump"; +my $capture_problem = kill_child(); +if ($capture_problem) { + probe_utils->send_msg("$output", "d", "tcpdump $capture_problem") if ($::VERBOSE); probe_utils->send_msg("$output", "f", $msg); + exit 1; } $msg = "Dump test result"; unless (open(FILE, "<$dumpfile")) { probe_utils->send_msg("$output", "d", "Open dump file $dumpfile failed") if ($::VERBOSE); probe_utils->send_msg("$output", "f", $msg); - `rm -f $dumpfile` if (-e "$dumpfile"); exit 1; } my %output; @@ -273,7 +287,6 @@ if (scalar(@server)) { } } -`rm -f $dumpfile` if (-e "$dumpfile"); exit 0; @@ -379,11 +392,39 @@ sub packdhcppkg { return $package; } +# Stop the capture and reap it, once: INT and TERM are blocked while the pid is taken and until +# tcpdump is reaped, so an interrupt in that window cannot orphan it or reach a recycled pid. The +# handlers stay installed, so a later signal still leaves through exit and the END cleanup. +# Returns '' when tcpdump ran until this TERM and stopped cleanly, otherwise what went wrong: it +# ended on its own, ignored TERM and was killed, or left on another signal or with a non-zero +# status. sub kill_child { - kill 15, $pid; - my @pidoftcpdump = `ps -ef | grep -E "[0-9]+:[0-9]+:[0-9]+ $tcpdump -i $nic" | awk -F' ' '{print \$2}'`; - foreach my $cpid (@pidoftcpdump) { - kill 15, $cpid; + sigprocmask(SIG_BLOCK, $stop_signals); + my $child = $pid; + $pid = undef; + unless ($child) { + sigprocmask(SIG_SETMASK, $signal_mask); + return ''; } - probe_utils->send_msg("$output", "d", "Kill process $pid used to capture the packet by 'tcpdump'") if ($::VERBOSE); + my $reaped = waitpid($child, WNOHANG); + my $early = ($reaped == $child) ? 1 : 0; + if ($reaped == 0) { + kill 'TERM', $child; + foreach (1 .. 50) { + last if ($reaped = waitpid($child, WNOHANG)) != 0; + select(undef, undef, undef, 0.1); + } + if ($reaped == 0) { + kill 'KILL', $child; + $reaped = waitpid($child, 0); + } + } + sigprocmask(SIG_SETMASK, $signal_mask); + return "could not be reaped" if $reaped != $child; + my ($signal, $status) = ($? & 127, $? >> 8); + probe_utils->send_msg("$output", "d", "Kill process $child used to capture the packet by 'tcpdump'") if ($::VERBOSE); + my $how = $signal ? "on signal $signal" : "with status $status"; + return "ended before the capture window did, $how" if $early; + return '' if $signal == 15 || (!$signal && !$status); + return "left the capture $how"; } diff --git a/xCAT-server/share/xcat/tools/detect_dhcpd b/xCAT-server/share/xcat/tools/detect_dhcpd index 8d9288301..7671219bb 100755 --- a/xCAT-server/share/xcat/tools/detect_dhcpd +++ b/xCAT-server/share/xcat/tools/detect_dhcpd @@ -5,6 +5,8 @@ BEGIN { use lib "$::XCATROOT/lib/perl"; use xCAT::CommandUtils; use IO::Socket::INET; +use File::Temp qw(tempfile); +use POSIX qw(_exit sigprocmask WNOHANG SIG_BLOCK SIG_SETMASK SIGINT SIGTERM); use Time::HiRes qw(gettimeofday sleep); use Getopt::Long; Getopt::Long::Configure("bundling"); @@ -92,16 +94,25 @@ if (-f "/etc/redhat-release") { } # fork a process to capture the packet by tcpdump +my ($dumpfh, $dumpfile) = tempfile("detect_dhcpd.XXXXXX", TMPDIR => 1, UNLINK => 1); +close($dumpfh); +# INT and TERM stay blocked from the fork until the handler that stops the capture is in place. +my $stop_signals = POSIX::SigSet->new(SIGINT, SIGTERM); +my $signal_mask = POSIX::SigSet->new(); +sigprocmask(SIG_BLOCK, $stop_signals, $signal_mask); my $pid = fork; -if (!defined $pid) { print "Fork failed.\n"; exit 1; } -my $dumpfile = "/tmp/dhcpdumpfile.log"; +if (!defined $pid) { sigprocmask(SIG_SETMASK, $signal_mask); print "Fork failed.\n"; exit 1; } if ($pid == 0) { - # Child process - my $cmd = "$tcpdump -i $IF port 68 -n -vvvvvv > $dumpfile 2>/dev/null"; - `$cmd`; - exit 0; + # Child process: tcpdump itself, so the parent owns exactly this pid. + sigprocmask(SIG_SETMASK, $signal_mask); + open(STDOUT, '>', $dumpfile) or _exit(1); + open(STDERR, '>', '/dev/null'); + exec($tcpdump, '-i', $nic, 'port', '68', '-n', '-vvvvvv'); + _exit(1); } +$SIG{INT} = $SIG{TERM} = sub { kill_child(); exit 1; }; +sigprocmask(SIG_SETMASK, $signal_mask); # generate the discover package my $package = packdhcppkg($MAC); @@ -141,25 +152,24 @@ if ($::TIMEOUT) { my $end = Time::HiRes::gettimeofday(); $end =~ s/(\d.*)\.(\d.*)/$1/; while ($end - $start <= $timeout) { - $sock->send($package) or die "Send discover error: $@\n"; + unless ($sock->send($package)) { + print "Send discover error: $!\n"; + kill_child(); + exit 1; + } sleep 2; $end = Time::HiRes::gettimeofday(); $end =~ s/(\d.*)\.(\d.*)/$1/; } -kill_child(); - #kill the child process -kill 15, $pid; -my @pidoftcpdump = `ps -ef | grep -E "[0-9]+:[0-9]+:[0-9]+ $tcpdump -i $IF" | awk -F' ' '{print \$2}'`; -foreach my $cpid (@pidoftcpdump) { - kill 15, $cpid; - - # print "try to kill $cpid\n"; +my $capture_problem = kill_child(); +if ($capture_problem) { + print "tcpdump $capture_problem.\n"; + exit 1; } -sleep 2; open(FILE, "<$dumpfile") or die "Cannot open $dumpfile\n"; my %output; my @snack = (); @@ -239,7 +249,6 @@ if (scalar(@server)) { } } -#`rm -f $dumpfile`; exit 0; @@ -345,12 +354,38 @@ sub packdhcppkg { return $package; } +# Stop the capture and reap it, once: INT and TERM are blocked while the pid is taken and until +# tcpdump is reaped, so an interrupt in that window cannot orphan it or reach a recycled pid. The +# handlers stay installed, so a later signal still leaves through exit and the END cleanup. +# Returns '' when tcpdump ran until this TERM and stopped cleanly, otherwise what went wrong: it +# ended on its own, ignored TERM and was killed, or left on another signal or with a non-zero +# status. sub kill_child { - kill 15, $pid; - my @pidoftcpdump = `ps -ef | grep -E "[0-9]+:[0-9]+:[0-9]+ $tcpdump -i $IF" | awk -F' ' '{print \$2}'`; - foreach my $cpid (@pidoftcpdump) { - kill 15, $cpid; - - #print "try to kill $cpid\n"; + sigprocmask(SIG_BLOCK, $stop_signals); + my $child = $pid; + $pid = undef; + unless ($child) { + sigprocmask(SIG_SETMASK, $signal_mask); + return ''; } + my $reaped = waitpid($child, WNOHANG); + my $early = ($reaped == $child) ? 1 : 0; + if ($reaped == 0) { + kill 'TERM', $child; + foreach (1 .. 50) { + last if ($reaped = waitpid($child, WNOHANG)) != 0; + select(undef, undef, undef, 0.1); + } + if ($reaped == 0) { + kill 'KILL', $child; + $reaped = waitpid($child, 0); + } + } + sigprocmask(SIG_SETMASK, $signal_mask); + return "could not be reaped" if $reaped != $child; + my ($signal, $status) = ($? & 127, $? >> 8); + my $how = $signal ? "on signal $signal" : "with status $status"; + return "ended before the capture window did, $how" if $early; + return '' if $signal == 15 || (!$signal && !$status); + return "left the capture $how"; } From b8ab7be46e22d490abeb365c512b7db64ffeb45d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 10 Sep 2026 02:10:25 -0300 Subject: [PATCH 49/62] test(xCAT-test): cover the detect_dhcpd capture lifecycle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inside the private network namespace the fake tcpdump records its parent, its output file and the TERM it receives, ps records any use, and a second fake tcpdump fails at once. Both copies must start tcpdump directly, write under TMPDIR, stop it by pid, leave no file behind, and exit 1 when the capture fails. The block skips where no namespace is available. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-test/unit/detect_dhcpd_tcpdump_lookup.t | 136 ++++++++++++++++++- 1 file changed, 134 insertions(+), 2 deletions(-) diff --git a/xCAT-test/unit/detect_dhcpd_tcpdump_lookup.t b/xCAT-test/unit/detect_dhcpd_tcpdump_lookup.t index 8122b1d67..4f410e77d 100644 --- a/xCAT-test/unit/detect_dhcpd_tcpdump_lookup.t +++ b/xCAT-test/unit/detect_dhcpd_tcpdump_lookup.t @@ -52,8 +52,8 @@ my $marker = File::Spec->catfile( tempdir( @fixture_dir, CLEANUP => 1 ), 'tcpdum sub fixture_bin { my (%with) = @_; my $bindir = tempdir( @fixture_dir, CLEANUP => 1 ); - write_script( "$bindir/tcpdump", qq{printf '%s\\n' "\$0" "\$*" > '$marker'\nexit 0\n} ); - foreach my $tool (qw(awk head grep ps)) { + write_script( "$bindir/tcpdump", qq{printf '%s\\n' "\$0" "\$*" > '$marker'\ntrap 'exit 0' TERM\nwhile :; do sleep 1; done\n} ); + foreach my $tool (qw(awk head grep ps sleep)) { my $real = xCAT::CommandUtils::find_executable($tool) or next; symlink( $real, "$bindir/$tool" ) or die "symlink $tool: $!"; } @@ -142,4 +142,136 @@ SKIP: { is( $args, "-i lo port 68 -n -vvvvvv", '... with the capture arguments' ); } +# ---- capture lifecycle: a private dump file, tcpdump run directly and stopped by pid ------------ +# Both copies wrote /tmp/dhcpdumpfile.log, ran tcpdump behind a shell, killed every tcpdump on the +# interface by pattern, and reported zero servers when tcpdump failed to start. The fake tcpdump +# below records who started it and where its output goes, waits for the TERM the script owes it, +# and a second one fails at once. ps records any use, since the scripts no longer need it. +sub lifecycle_bin { + my (%with) = @_; + my $bindir = tempdir( @fixture_dir, CLEANUP => 1 ); + my $record = "$with{record}"; + if ( $with{fail} ) { + write_script( "$bindir/tcpdump", qq{printf 'started\\n' >> '$record'\nexit 1\n} ); + } elsif ( $with{quit} ) { + write_script( "$bindir/tcpdump", qq{printf 'started\\n' >> '$record'\nexit 0\n} ); + } elsif ( $with{killed} ) { + write_script( "$bindir/tcpdump", qq{printf 'started\\n' >> '$record'\nkill -9 \$\$\n} ); + } elsif ( $with{stubborn} ) { + write_script( "$bindir/tcpdump", qq{trap '' TERM\nprintf 'started\\n' >> '$record'\nwhile :; do sleep 1; done\n} ); + } else { + write_script( "$bindir/tcpdump", + qq{parent=\$(cat /proc/\$PPID/comm 2>/dev/null)\n} + . qq{out=\$(readlink /proc/\$\$/fd/1 2>/dev/null)\n} + . qq{printf 'pid=%s ppid=%s parent=%s out=%s\\n' "\$\$" "\$PPID" "\$parent" "\$out" >> '$record'\n} + . qq{trap 'printf "TERM\\n" >> "$record"; exit 0' TERM\n} + . qq{while :; do sleep 1; done\n} ); + } + write_script( "$bindir/ps", qq{printf 'ps %s\\n' "\$*" >> '$record.ps'\nexit 0\n} ); + foreach my $tool (qw(awk head grep cat readlink sleep)) { + my $real = xCAT::CommandUtils::find_executable($tool) or next; + symlink( $real, "$bindir/$tool" ) or die "symlink $tool: $!"; + } + symlink( $with{real_ip}, "$bindir/ip" ) or die "symlink ip: $!"; + return $bindir; +} + +sub run_isolated_status { + my ( $ns, $bindir, $tmpdir, $script, @args ) = @_; + my $command = "env PATH='$bindir' XCATROOT='$root' TMPDIR='$tmpdir' " . script_command( $script, @args ); + my $shell = "$ns->{unshare} $ns->{flags} sh -c \"$ns->{setup} && exec $command\" 2>&1"; + my $out = `$shell`; + return ( $out, $? >> 8 ); +} + +sub slurp_lines { + my ($path) = @_; + open( my $fh, '<', $path ) or return; + chomp( my @lines = <$fh> ); + close($fh); + return @lines; +} + +SKIP: { + skip 'the private /tmp would hide this checkout or its fixtures', 44 + if index( $repo, '/tmp/' ) == 0 || !@fixture_dir; + my $ns = isolation(); + skip 'no private network namespace on this host', 44 unless $ns; + + foreach my $case ( [ $tools, 'the tool', '-t' ], [ $probe, 'the probe', '-d' ] ) { + my ( $script, $label, $window ) = @$case; + my $tmpdir = tempdir( @fixture_dir, CLEANUP => 1 ); + my $record = File::Spec->catfile( tempdir( @fixture_dir, CLEANUP => 1 ), 'tcpdump.record' ); + my $bindir = lifecycle_bin( record => $record, real_ip => $ns->{ip} ); + + my ( $out, $status ) = run_isolated_status( $ns, $bindir, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '1' ); + is( $status, 0, "$label exits 0 after a capture window" ); + my @rec = slurp_lines($record); + my ($start) = grep { /^pid=/ } @rec; + ok( defined $start, "$label started tcpdump" ) or diag($out); + like( $start // '', qr/ parent=perl /, '... directly from the script, with no shell in between' ); + like( $start // '', qr{ out=\Q$tmpdir\E/detect_dhcpd\.\w+$}, '... writing a private capture file under TMPDIR' ); + ok( ( grep { $_ eq 'TERM' } @rec ), '... and stopped it with TERM when the window ended' ); + ok( !-e "$record.ps", '... without searching the process table' ); + my @left = glob("$tmpdir/detect_dhcpd.*"); + is( scalar(@left), 0, '... and removed the capture file on exit' ); + + my $failing = lifecycle_bin( record => $record, real_ip => $ns->{ip}, fail => 1 ); + ( $out, $status ) = run_isolated_status( $ns, $failing, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '1' ); + is( $status, 1, "$label exits 1 when tcpdump fails to start" ) or diag($out); + like( $out, qr/tcpdump ended before the capture window did, with status 1|Capture the packets by tcpdump/, '... and says the capture failed' ); + unlike( $out, qr/0 servers repl/, '... instead of reporting zero servers' ); + @left = glob("$tmpdir/detect_dhcpd.*"); + is( scalar(@left), 0, '... and leaves no capture file behind' ); + + my $quitting = lifecycle_bin( record => $record, real_ip => $ns->{ip}, quit => 1 ); + ( $out, $status ) = run_isolated_status( $ns, $quitting, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '1' ); + is( $status, 1, "$label exits 1 when tcpdump quits early with status 0" ) or diag($out); + unlike( $out, qr/0 servers repl/, '... instead of reporting zero servers' ); + + my $dying = lifecycle_bin( record => $record, real_ip => $ns->{ip}, killed => 1 ); + ( $out, $status ) = run_isolated_status( $ns, $dying, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '1' ); + is( $status, 1, "$label exits 1 when tcpdump dies on another signal" ) or diag($out); + like( $out, qr/on signal 9|Capture the packets by tcpdump/, '... and names the signal or the failed step' ); + + my $stubborn = lifecycle_bin( record => $record, real_ip => $ns->{ip}, stubborn => 1 ); + my $began = time; + ( $out, $status ) = run_isolated_status( $ns, $stubborn, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '1' ); + is( $status, 1, "$label exits 1 when tcpdump ignores TERM" ) or diag($out); + cmp_ok( time - $began, '<', 20, '... after a bounded grace period' ); + like( $out, qr/on signal 9|Capture the packets by tcpdump/, '... having killed it' ); + + # An interrupt while the capture runs: the script is signalled once the fake tcpdump has + # reported the script's pid, and must stop tcpdump and remove the file on its way out. + my $int_record = File::Spec->catfile( tempdir( @fixture_dir, CLEANUP => 1 ), 'tcpdump.record' ); + my $int_bin = lifecycle_bin( record => $int_record, real_ip => $ns->{ip} ); + my $runner = fork; + die "fork: $!" unless defined $runner; + if ( $runner == 0 ) { + my ( undef, $st ) = run_isolated_status( $ns, $int_bin, $tmpdir, $script, '-i', 'lo', '-m', $mac, $window, '30' ); + exit $st; + } + my $started; + foreach ( 1 .. 150 ) { + ($started) = grep { /^pid=/ } slurp_lines($int_record); + last if $started; + select( undef, undef, undef, 0.2 ); + } + my ($tcpdump_pid) = ( $started // '' ) =~ /^pid=(\d+)/; + my ($script_pid) = ( $started // '' ) =~ / ppid=(\d+)/; + ok( $script_pid, "$label reports the pid to interrupt" ) or diag( $started // 'tcpdump never started' ); + if ($script_pid) { + kill 'INT', $script_pid; + select( undef, undef, undef, 0.3 ); + kill 'INT', $script_pid; + } + waitpid( $runner, 0 ); + is( $? >> 8, 1, '... and exits 1 on a double interrupt during the capture' ); + ok( ( grep { $_ eq 'TERM' } slurp_lines($int_record) ), '... after stopping tcpdump' ); + ok( !( $tcpdump_pid && kill( 0, $tcpdump_pid ) ), '... which is gone' ); + @left = glob("$tmpdir/detect_dhcpd.*"); + is( scalar(@left), 0, '... and the capture file is removed' ); + } +} + done_testing(); From e36cdec3e97ff5e9cb9f3a09a173341eb5d61f13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:10:49 -0300 Subject: [PATCH 50/62] fix(Template): give the installer otherpkgs sources apt can read MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The online autoinstall apt configuration offered every otherpkgdir entry as a flat one-line source with trusted=yes. From 24.04 on curtin converts a one-line source to Deb822 before it writes the file and keeps only the type, URI, suite and components, so the unsigned repository reached apt without the option and apt rejected it during the install. An entry written as URL, suite and components, the form the otherpkgs documentation gives for a mirror, was written whole as the URL, which apt cannot parse either. The sources are now Deb822 stanzas on those releases, which curtin writes as they are, with Trusted: yes, and a mirror entry keeps its suite and components as fields. The releases before 24.04 keep the one-line form, with the same fields. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-server/lib/perl/xCAT/Template.pm | 54 +++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/xCAT-server/lib/perl/xCAT/Template.pm b/xCAT-server/lib/perl/xCAT/Template.pm index de954065f..b4ebc6f04 100644 --- a/xCAT-server/lib/perl/xCAT/Template.pm +++ b/xCAT-server/lib/perl/xCAT/Template.pm @@ -1790,7 +1790,7 @@ sub ubuntu_subiquity_apt_config { my ($media_dir, $osarch) = @_; my $use_deb822 = ubuntu_subiquity_uses_deb822_sources($media_dir); - my @otherpkg_sources = ubuntu_subiquity_otherpkg_sources(); + my @otherpkg_sources = map { ubuntu_subiquity_otherpkg_source_spec($_) } ubuntu_subiquity_otherpkg_sources(); my $online_mirror = ubuntu_subiquity_apt_mirror($osarch); if ($online_mirror) { @@ -1822,8 +1822,7 @@ sub ubuntu_subiquity_apt_config push @lines, ' sources:' unless $need_sources_block; my $index = 0; foreach my $source (@otherpkg_sources) { - push @lines, " xcat-otherpkgs-$index.list:"; - push @lines, qq( source: "deb [trusted=yes] $source ./"); + push @lines, ubuntu_subiquity_source_lines( "xcat-otherpkgs-$index", $source, $use_deb822 ); $index++; } } @@ -1864,9 +1863,9 @@ sub ubuntu_subiquity_apt_config foreach my $source (@otherpkg_sources) { push @lines, ''; push @lines, ' Types: deb'; - push @lines, " URIs: $source"; - push @lines, ' Suites: ./'; - push @lines, ' Components:'; + push @lines, " URIs: $source->{uri}"; + push @lines, " Suites: $source->{suites}"; + push @lines, ' Components:' . ( length $source->{components} ? " $source->{components}" : '' ); push @lines, ' Trusted: yes'; } } else { @@ -1879,7 +1878,7 @@ sub ubuntu_subiquity_apt_config my $index = 0; foreach my $source (@otherpkg_sources) { push @lines, " xcat-otherpkgs-$index.list:"; - push @lines, qq( source: "deb [trusted=yes] $source ./"); + push @lines, qq( source: "$source->{line}"); $index++; } } @@ -1915,6 +1914,47 @@ sub ubuntu_subiquity_otherpkg_sources return @sources; } +# ubuntu_subiquity_source_line: the one-line form of a source, with the option its Deb822 form carries. +sub ubuntu_subiquity_source_line +{ + my ($spec) = @_; + my @option = $spec->{trusted} ? ('[trusted=yes]') : $spec->{signed_by} ? ("[signed-by=$spec->{signed_by}]") : (); + return join( ' ', 'deb', @option, $spec->{uri}, $spec->{suites}, grep { length } $spec->{components} ); +} + +# ubuntu_subiquity_otherpkg_source_spec: the apt source of one otherpkgdir entry the installer gets. +# A bare URL or a local repository is a flat repository, and an entry written as URL, suite and +# components is that source; otherpkgs trusts both, so the installer does too. +sub ubuntu_subiquity_otherpkg_source_spec +{ + my ($entry) = @_; + my ( $uri, $suite, @components ) = split( /\s+/, $entry ); + my %spec = ( uri => $uri, suites => './', components => '', trusted => 1, signed_by => '' ); + @spec{qw(suites components)} = ( $suite, join( ' ', @components ) ) if defined $suite && length $suite; + $spec{line} = ubuntu_subiquity_source_line( \%spec ); + return \%spec; +} + +# ubuntu_subiquity_source_lines: one entry of the autoinstall sources mapping, a one-line source +# before Deb822 and a Deb822 stanza from 24.04 on: curtin converts a one-line source to Deb822 +# there and keeps only its type, URI, suite and components, so a trusted repository would come out +# unsigned and be rejected. +sub ubuntu_subiquity_source_lines +{ + my ( $name, $source, $use_deb822 ) = @_; + return ( " $name.list:", qq( source: "$source->{line}") ) unless $use_deb822; + my @lines = ( + " $name.sources:", + ' source: |', + ' Types: deb', + " URIs: $source->{uri}", + " Suites: $source->{suites}", + ' Components:' . ( length $source->{components} ? " $source->{components}" : '' ), + ); + push @lines, ' Trusted: yes' if $source->{trusted}; + return @lines; +} + sub ubuntu_subiquity_uses_deb822_sources { my ($media_dir) = @_; From f995c8dcb328e9cf64c6be16e5e35295c62d9ff1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:11:21 -0300 Subject: [PATCH 51/62] test(xCAT-test): pin the otherpkgs source form per release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The online configuration is rendered with an otherpkgs repository on a classic release and on a Deb822 release: the first must carry the one-line trusted source, the second a Deb822 stanza with Trusted: yes and no one-line form. Against the previous module the Deb822 case renders the one-line form. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-test/unit/ubuntu_subiquity_apt_sources.t | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/xCAT-test/unit/ubuntu_subiquity_apt_sources.t b/xCAT-test/unit/ubuntu_subiquity_apt_sources.t index 0c55d0e8c..d1e53b9d2 100644 --- a/xCAT-test/unit/ubuntu_subiquity_apt_sources.t +++ b/xCAT-test/unit/ubuntu_subiquity_apt_sources.t @@ -21,11 +21,32 @@ sub apt_config_for { no warnings 'redefine'; local *xCAT::Template::ubuntu_subiquity_apt_mirror = sub { $opt{mirror} }; local *xCAT::Template::ubuntu_subiquity_uses_deb822_sources = sub { $opt{deb822} }; - local *xCAT::Template::ubuntu_subiquity_otherpkg_sources = sub { () }; + local *xCAT::Template::ubuntu_subiquity_otherpkg_sources = sub { @{ $opt{others} || [] } }; local *xCAT::Template::ubuntu_subiquity_uses_generated_cdrom_source = sub { 0 }; return xCAT::Template::ubuntu_subiquity_apt_config('/some/media/dir'); } +# --- the otherpkgs repository: a one-line source before Deb822, a Deb822 stanza from 24.04 on, --- +# --- since curtin drops the options of a one-line source when it converts it there --- +my $others = [ 'http://192.0.2.10/install/post/otherpkgs/ubuntu24.04/x86_64' ]; +my $classic_others = apt_config_for( mirror => $MIRROR, deb822 => 0, others => $others ); +like( $classic_others, qr{^ xcat-otherpkgs-0\.list:\n source: "deb \[trusted=yes\] http://192\.0\.2\.10/install/post/otherpkgs/ubuntu24\.04/x86_64 \./"$}m, + 'classic: the otherpkgs repository is a one-line trusted source' ); +my $deb822_others = apt_config_for( mirror => $MIRROR, deb822 => 1, others => $others ); +like( $deb822_others, + qr{^ xcat-otherpkgs-0\.sources:\n source: \|\n Types: deb\n URIs: http://192\.0\.2\.10/install/post/otherpkgs/ubuntu24\.04/x86_64\n Suites: \./\n Components:\n Trusted: yes(?:\n|\z)}m, + 'Deb822: the otherpkgs repository is a Deb822 stanza carrying Trusted: yes' ); +unlike( $deb822_others, qr/xcat-otherpkgs-0\.list|trusted=yes/, 'Deb822: and no one-line form remains' ); + +# an otherpkgdir written as URL, suite and components is that source, trusted, not a flat repository at a URL with spaces +my $mirror_others = [ 'http://mirror.example/ubuntu noble main universe' ]; +like( apt_config_for( mirror => $MIRROR, deb822 => 0, others => $mirror_others ), + qr{^ xcat-otherpkgs-0\.list:\n source: "deb \[trusted=yes\] http://mirror\.example/ubuntu noble main universe"$}m, + 'classic: an otherpkgdir mirror entry keeps its suite and components' ); +like( apt_config_for( mirror => $MIRROR, deb822 => 1, others => $mirror_others ), + qr{^ xcat-otherpkgs-0\.sources:\n source: \|\n Types: deb\n URIs: http://mirror\.example/ubuntu\n Suites: noble\n Components: main universe\n Trusted: yes(?:\n|\z)}m, + 'Deb822: and becomes a stanza with them as fields, so apt reads one URI' ); + # --- online, classic sources (20.04 / 22.04): the archive must be added via sources: --- my $classic = apt_config_for( mirror => $MIRROR, deb822 => 0 ); From 539486b7c828566a88d346b501713a1de17df4bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 10 Sep 2026 12:28:43 -0300 Subject: [PATCH 52/62] feat(Template): give the Subiquity installer the pkgdir mirrors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An osimage pkgdir can name mirrors after the install media, and ospkgs installs from all of them, but the autoinstall apt configuration offered the installer only the archive mirror and the otherpkgs repositories. A package that only a pkgdir mirror carries could not be installed before the first boot. The mirrors now join the installer's apt sources, next to the otherpkgs ones, in every form the configuration takes. An entry written as URL, suite and components is an apt source line, as ospkgs writes it, and a suite that is an exact path needs no component. A local directory that is a flat repository is served by the management node and trusted, as an otherpkgdir is. An entry that names an Ubuntu archive mirror the installer already has a source for, the configured one or a default one, carries that source's signing key, the archive keyring on the Deb822 releases and none before them, because apt rejects a second source for the same suite whose signing key differs. For the same reason a repository is offered once, whether pkgdir names it twice, as a directory and as its URL, or the otherpkgdir names it too. Anything else is no apt source for ospkgs either and is left out. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-server/lib/perl/xCAT/Template.pm | 132 ++++++++++++++++++++++--- xCAT-server/lib/xcat/plugins/debian.pm | 3 +- 2 files changed, 123 insertions(+), 12 deletions(-) diff --git a/xCAT-server/lib/perl/xCAT/Template.pm b/xCAT-server/lib/perl/xCAT/Template.pm index b4ebc6f04..e37652ab1 100644 --- a/xCAT-server/lib/perl/xCAT/Template.pm +++ b/xCAT-server/lib/perl/xCAT/Template.pm @@ -364,7 +364,7 @@ sub subvars { $inc =~ s/#INSTALL_SOURCES_IN_PRE#/$source_in_pre/g; if (("ubuntu" eq $platform) || ("debian" eq $platform)) { $inc =~ s/#INCLUDE_OSIMAGE_PKGDIR#/$pkgdirs[-1]/; - $inc =~ s/#UBUNTU_SUBIQUITY_APT_CONFIG#/ubuntu_subiquity_apt_config($media_dir, $namedargs{osarch})/eg; + $inc =~ s/#UBUNTU_SUBIQUITY_APT_CONFIG#/ubuntu_subiquity_apt_config($media_dir, $namedargs{osarch}, $namedargs{pkgdirs})/eg; } $inc =~ s/#WRITEREPO#/$writerepo/g; } @@ -377,7 +377,7 @@ sub subvars { $inc =~ s/#INCLUDE_NOP:([^#^\n]+)#/includefile($1,1,0)/eg; $inc =~ s/#XCATVAR:([^#]+)#/envvar($1)/eg; $inc =~ s/#ENV:([^#]+)#/envvar($1)/eg; - $inc =~ s/#UBUNTU_SUBIQUITY_APT_CONFIG#/ubuntu_subiquity_apt_config($media_dir, $namedargs{osarch})/eg; + $inc =~ s/#UBUNTU_SUBIQUITY_APT_CONFIG#/ubuntu_subiquity_apt_config($media_dir, $namedargs{osarch}, $namedargs{pkgdirs})/eg; $inc =~ s/#SUBIQUITYINSTALLNIC#/subiquity_install_nic()/eg; $inc =~ s/#SUBIQUITYINSTALLMAC#/subiquity_install_mac()/eg; $inc =~ s/#MACHINEPASSWORD#/machinepassword()/eg; @@ -1786,13 +1786,28 @@ sub ubuntu_subiquity_apt_mirror return ($ent && defined($ent->{value}) && length($ent->{value})) ? $ent->{value} : $default; } +# The key curtin names in the Deb822 source it writes for the primary apt mirror on 24.04 and later. +my $UBUNTU_ARCHIVE_KEYRING = '/usr/share/keyrings/ubuntu-archive-keyring.gpg'; + sub ubuntu_subiquity_apt_config { - my ($media_dir, $osarch) = @_; + my ($media_dir, $osarch, $pkgdirs) = @_; my $use_deb822 = ubuntu_subiquity_uses_deb822_sources($media_dir); - my @otherpkg_sources = map { ubuntu_subiquity_otherpkg_source_spec($_) } ubuntu_subiquity_otherpkg_sources(); + my $online_mirror = ubuntu_subiquity_apt_mirror($osarch); + my $mirror_key = $use_deb822 ? $UBUNTU_ARCHIVE_KEYRING : ''; + my @otherpkg_sources = map { ubuntu_subiquity_otherpkg_source_spec( $_, $mirror_key, $online_mirror ) } ubuntu_subiquity_otherpkg_sources(); + my @pkgdir_sources = ubuntu_subiquity_pkgdir_source_specs( $pkgdirs, $mirror_key, $online_mirror ); + + # apt rejects two sources for one repository whose options differ, so a pkgdir entry that repeats an + # otherpkgs repository adds its components to that source instead + my %otherpkg_by_key = map { ( my $uri = $_->{uri} ) =~ s{/+$}{}; ( "$uri $_->{suites}" => $_ ) } @otherpkg_sources; + @pkgdir_sources = grep { + ( my $uri = $_->{uri} ) =~ s{/+$}{}; + my $other = $otherpkg_by_key{"$uri $_->{suites}"}; + ubuntu_subiquity_add_components( $other, $_->{components} ) if $other; + !$other; + } @pkgdir_sources; - my $online_mirror = ubuntu_subiquity_apt_mirror($osarch); if ($online_mirror) { # Online install: use the configured archive as the primary apt mirror so # Subiquity/curtin can fetch whatever the minimal media lacks. No @@ -1818,13 +1833,18 @@ sub ubuntu_subiquity_apt_config push @lines, ' xcat-ubuntu-updates.list:'; push @lines, qq( source: "deb $online_mirror \$RELEASE-updates main restricted universe multiverse"); } - if (@otherpkg_sources) { + if (@otherpkg_sources || @pkgdir_sources) { push @lines, ' sources:' unless $need_sources_block; my $index = 0; foreach my $source (@otherpkg_sources) { push @lines, ubuntu_subiquity_source_lines( "xcat-otherpkgs-$index", $source, $use_deb822 ); $index++; } + $index = 0; + foreach my $source (@pkgdir_sources) { + push @lines, ubuntu_subiquity_source_lines( "xcat-pkgdir-$index", $source, $use_deb822 ); + $index++; + } } return join( "\n", @lines ); } @@ -1868,12 +1888,20 @@ sub ubuntu_subiquity_apt_config push @lines, ' Components:' . ( length $source->{components} ? " $source->{components}" : '' ); push @lines, ' Trusted: yes'; } + foreach my $source (@pkgdir_sources) { + push @lines, ''; + push @lines, ' Types: deb'; + push @lines, " URIs: $source->{uri}"; + push @lines, " Suites: $source->{suites}"; + push @lines, ' Components:' . ( length $source->{components} ? " $source->{components}" : '' ); + push @lines, ' Trusted: yes' if $source->{trusted}; + } } else { push @lines, ' mirror-selection:'; push @lines, ' primary:'; push @lines, ' - uri: file:/cdrom'; - if (@otherpkg_sources) { + if (@otherpkg_sources || @pkgdir_sources) { push @lines, ' sources:'; my $index = 0; foreach my $source (@otherpkg_sources) { @@ -1881,12 +1909,68 @@ sub ubuntu_subiquity_apt_config push @lines, qq( source: "$source->{line}"); $index++; } + $index = 0; + foreach my $source (@pkgdir_sources) { + push @lines, " xcat-pkgdir-$index.list:"; + push @lines, qq( source: "$source->{line}"); + $index++; + } } } return join( "\n", @lines ); } +# ubuntu_subiquity_pkgdir_source_specs: the apt sources of the entries after the install media in +# an osimage pkgdir value, which mkinstall hands over as pkgdirs and ospkgs receives as OSPKGDIR. +# An entry written as "URL suite components" is an apt source line, as ospkgs writes it, and a +# suite that is an exact path needs no component. A local directory that is a flat repository is +# served by the management node and trusted, as an otherpkgdir is. Anything else is no apt source +# for ospkgs either and is left out. An entry that names an Ubuntu archive mirror the installer +# already has a source for, the configured one or a default one, carries that source's signing +# key, the archive keyring on the Deb822 releases and none before them: apt rejects a second source +# for the same suite whose signing key differs. +sub ubuntu_subiquity_pkgdir_source_specs +{ + my ( $pkgdirval, $mirror_key, @mirrors ) = @_; + $mirror_key //= ''; + my %signed_uri = ubuntu_subiquity_signed_mirror_uris(@mirrors); + my @specs; + foreach my $entry ( split( /,/, $pkgdirval // '' ) ) { + $entry =~ s/^\s+|\s+$//g; + next if $entry eq ''; + if ( $entry =~ m{^https?://} ) { + my ( $uri, $suite, @components ) = split( /\s+/, $entry ); + next unless defined $suite && ( @components || $suite =~ m{/$} ); + ( my $bare = $uri ) =~ s{/+$}{}; + my %spec = ( uri => $uri, suites => $suite, components => join( ' ', @components ), trusted => 0, signed_by => $signed_uri{$bare} ? $mirror_key : '' ); + $spec{line} = ubuntu_subiquity_source_line( \%spec ); + push @specs, \%spec; + } + elsif ( $entry !~ m{^[a-z]+://} && ubuntu_subiquity_local_apt_repo($entry) ) { + my $uri = ubuntu_subiquity_pkgdir_uri($entry); + my %spec = ( uri => $uri, suites => './', components => '', trusted => 1, signed_by => '' ); + $spec{line} = ubuntu_subiquity_source_line( \%spec ); + push @specs, \%spec; + } + } + + # a directory and its own URL are one repository: one source, with the trust and components of both + my ( %kept, @unique ); + foreach my $spec (@specs) { + ( my $uri = $spec->{uri} ) =~ s{/+$}{}; + if ( my $first = $kept{"$uri $spec->{suites}"} ) { + $first->{trusted} ||= $spec->{trusted}; + $first->{signed_by} ||= $spec->{signed_by}; + ubuntu_subiquity_add_components( $first, $spec->{components} ); + next; + } + push @unique, $kept{"$uri $spec->{suites}"} = $spec; + } + return @unique; +} + + sub ubuntu_subiquity_otherpkg_sources { my $nodetype_tab = xCAT::Table->new('nodetype'); @@ -1914,6 +1998,24 @@ sub ubuntu_subiquity_otherpkg_sources return @sources; } +# ubuntu_subiquity_add_components: the components of a repeated repository join the source kept for it. +sub ubuntu_subiquity_add_components +{ + my ( $spec, $components ) = @_; + my %have = map { $_ => 1 } split( ' ', $spec->{components} ); + $spec->{components} = join( ' ', split( ' ', $spec->{components} ), grep { !$have{$_}++ } split( ' ', $components // '' ) ); + $spec->{line} = ubuntu_subiquity_source_line($spec); + return; +} + +# ubuntu_subiquity_signed_mirror_uris: the apt mirror the installer already has a source for, the +# configured one or the architecture default, without a trailing slash. +sub ubuntu_subiquity_signed_mirror_uris +{ + my (@mirrors) = @_; + return map { ( my $uri = $_ ) =~ s{/+$}{}; ( $uri => 1 ) } grep { defined && length } @mirrors; +} + # ubuntu_subiquity_source_line: the one-line form of a source, with the option its Deb822 form carries. sub ubuntu_subiquity_source_line { @@ -1923,14 +2025,21 @@ sub ubuntu_subiquity_source_line } # ubuntu_subiquity_otherpkg_source_spec: the apt source of one otherpkgdir entry the installer gets. -# A bare URL or a local repository is a flat repository, and an entry written as URL, suite and -# components is that source; otherpkgs trusts both, so the installer does too. +# A bare URL or a local repository is a flat trusted repository, as otherpkgs treats it. An entry +# written as URL, suite and components is that source, trusted as well, unless the URL is an Ubuntu +# archive mirror the installer already has a source for: that one gets the same signing key and no +# trust, since apt rejects a second source for one suite whose options differ. sub ubuntu_subiquity_otherpkg_source_spec { - my ($entry) = @_; + my ( $entry, $mirror_key, @mirrors ) = @_; my ( $uri, $suite, @components ) = split( /\s+/, $entry ); my %spec = ( uri => $uri, suites => './', components => '', trusted => 1, signed_by => '' ); - @spec{qw(suites components)} = ( $suite, join( ' ', @components ) ) if defined $suite && length $suite; + if ( defined $suite && length $suite ) { + my %signed = ubuntu_subiquity_signed_mirror_uris(@mirrors); + ( my $bare = $uri ) =~ s{/+$}{}; + @spec{qw(suites components)} = ( $suite, join( ' ', @components ) ); + @spec{qw(trusted signed_by)} = ( 0, $mirror_key // '' ) if $signed{$bare}; + } $spec{line} = ubuntu_subiquity_source_line( \%spec ); return \%spec; } @@ -1951,6 +2060,7 @@ sub ubuntu_subiquity_source_lines " Suites: $source->{suites}", ' Components:' . ( length $source->{components} ? " $source->{components}" : '' ), ); + push @lines, " Signed-By: $source->{signed_by}" if $source->{signed_by}; push @lines, ' Trusted: yes' if $source->{trusted}; return @lines; } diff --git a/xCAT-server/lib/xcat/plugins/debian.pm b/xCAT-server/lib/xcat/plugins/debian.pm index 2a4c2fb39..7edf607bc 100644 --- a/xCAT-server/lib/xcat/plugins/debian.pm +++ b/xCAT-server/lib/xcat/plugins/debian.pm @@ -1123,7 +1123,8 @@ sub mkinstall { $platform, $partitionfile, \%tmpl_hash, - osarch => $arch + osarch => $arch, + pkgdirs => $pkgdirval ); } From c96d879367b2f4d300aa630b886aa42c688bdd2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 10 Sep 2026 12:28:43 -0300 Subject: [PATCH 53/62] test(xCAT-test): cover the pkgdir mirrors in the Subiquity apt configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The source specs are derived from a pkgdir value alone, and the apt configuration is rendered with only the database readers stubbed, online and offline, Deb822 and legacy, with and without otherpkgs repositories. Against the previous module the spec helper does not exist. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- .../unit/ubuntu_subiquity_pkgdir_sources.t | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 xCAT-test/unit/ubuntu_subiquity_pkgdir_sources.t diff --git a/xCAT-test/unit/ubuntu_subiquity_pkgdir_sources.t b/xCAT-test/unit/ubuntu_subiquity_pkgdir_sources.t new file mode 100644 index 000000000..2dc89dfb9 --- /dev/null +++ b/xCAT-test/unit/ubuntu_subiquity_pkgdir_sources.t @@ -0,0 +1,138 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use File::Spec; +use File::Temp; +use FindBin; +use Test::More; + +# ospkgs installs the pkglist from every pkgdir entry, the media first and the mirrors after it, +# and the Subiquity autoinstall now installs the pkglist too, so the installer must be given the +# same mirrors. The specs are derived from the pkgdir value alone, and the apt configuration is +# rendered for real with only the database readers stubbed. + +my $repo = File::Spec->rel2abs( File::Spec->catdir( $FindBin::Bin, '..', '..' ) ); +my @incs = ( "$repo/perl-xCAT", "$repo/xCAT-server/lib/perl" ); +my $devnull = File::Spec->devnull(); +my $probe = join( ' ', $^X, ( map { "-I$_" } @incs ), '-e', "'require xCAT::Template; 1'", ">$devnull", "2>&1" ); +plan skip_all => 'xCAT::Template cannot be loaded here' if system($probe) != 0; +require lib; +lib->import(@incs); +require xCAT::Template; + +{ + no warnings qw(redefine once); + *xCAT::Template::ubuntu_subiquity_pkgdir_uri = sub { return "http://192.0.2.10$_[0]" }; +} +my $flat = File::Temp->newdir(); +open( my $pfh, '>', "$flat/Packages" ) or die $!; close $pfh; +open( my $rfh, '>', "$flat/Release" ) or die $!; close $rfh; +my @specs = xCAT::Template::ubuntu_subiquity_pkgdir_source_specs( + "/install/ubuntu24.04.4/x86_64, http://mirror.example/ubuntu noble main universe ,http://repo.example/extra,ssh://host/path,http://mirror.example/ubuntu jammy,http://flat.example/pool ./,$flat" +); +is( scalar(@specs), 3, 'the media path, the bare URL, the ssh entry and a suite without components are no apt sources; the rest are' ); +is_deeply( $specs[0], { uri => 'http://mirror.example/ubuntu', suites => 'noble', components => 'main universe', trusted => 0, signed_by => '', + line => 'deb http://mirror.example/ubuntu noble main universe' }, + 'an entry written as URL suite components is used as written, and is not marked trusted' ); +is_deeply( $specs[1], { uri => 'http://flat.example/pool', suites => './', components => '', trusted => 0, signed_by => '', + line => 'deb http://flat.example/pool ./' }, + 'an exact-path suite needs no component' ); +is_deeply( $specs[2], { uri => "http://192.0.2.10$flat", suites => './', components => '', trusted => 1, signed_by => '', + line => "deb [trusted=yes] http://192.0.2.10$flat ./" }, + 'a local flat repository is served by the management node and trusted, as an otherpkgdir is' ); +is_deeply( [ xCAT::Template::ubuntu_subiquity_pkgdir_source_specs(undef) ], [], 'no pkgdir gives no sources' ); +my @alias = xCAT::Template::ubuntu_subiquity_pkgdir_source_specs("$flat,http://192.0.2.10$flat/ ./"); +is( scalar(@alias), 1, 'a local repository and its own URL in pkgdir are one source' ); +is( $alias[0]{trusted}, 1, '... trusted, as the directory entry is' ); +my $keyring = '/usr/share/keyrings/ubuntu-archive-keyring.gpg'; +is_deeply( + [ xCAT::Template::ubuntu_subiquity_pkgdir_source_specs( 'http://archive.example/ubuntu/ noble-proposed main,http://mirror.example/ubuntu noble main', $keyring, 'http://archive.example/ubuntu' ) ], + [ { uri => 'http://archive.example/ubuntu/', suites => 'noble-proposed', components => 'main', trusted => 0, signed_by => $keyring, + line => "deb [signed-by=$keyring] http://archive.example/ubuntu/ noble-proposed main" }, + { uri => 'http://mirror.example/ubuntu', suites => 'noble', components => 'main', trusted => 0, signed_by => '', line => 'deb http://mirror.example/ubuntu noble main' } ], + 'an entry that names the apt mirror, trailing slash or not, carries the key given for that mirror; another mirror gets none' ); +is_deeply( + [ xCAT::Template::ubuntu_subiquity_pkgdir_source_specs( 'http://archive.example/ubuntu jammy main', '', 'http://archive.example/ubuntu' ) ], + [ { uri => 'http://archive.example/ubuntu', suites => 'jammy', components => 'main', trusted => 0, signed_by => '', line => 'deb http://archive.example/ubuntu jammy main' } ], + 'without a key for the mirror the entry is used as written' ); + +our ( $apt_mirror, @otherpkg_sources ) = ( '', () ); +{ + no warnings qw(redefine once); + *xCAT::Template::ubuntu_subiquity_apt_mirror = sub { return $main::apt_mirror }; + *xCAT::Template::ubuntu_subiquity_otherpkg_sources = sub { return @main::otherpkg_sources }; +} +# the value mkinstall hands over: the media first, then two mirrors +my $mirrors = '/install/ubuntu24.04.4/x86_64,http://mirror.example/ubuntu noble main,http://repo.example/extra'; + +sub apt_config_for { + my ( $media_dir, %args ) = @_; + local $apt_mirror = $args{mirror} // ''; + local @otherpkg_sources = @{ $args{others} || [] }; + return xCAT::Template::ubuntu_subiquity_apt_config( $media_dir, undef, $args{pkgdirs} ); +} + +my $online = apt_config_for( 'ubuntu24.04', mirror => 'http://archive.example/ubuntu', pkgdirs => $mirrors ); +like( $online, qr/^ sources:$/m, 'online: the pkgdir mirrors open the sources mapping' ); +like( $online, qr{^ xcat-pkgdir-0\.sources:\n source: \|\n Types: deb\n URIs: http://mirror\.example/ubuntu\n Suites: noble\n Components: main(?:\n|\z)}m, + '... as a Deb822 stanza on a Deb822 release, with the suite and components as written' ); +unlike( $online, qr{xcat-pkgdir-1|repo\.example}, '... and the bare URL is not offered as a repository' ); + +my $online_both = apt_config_for( 'ubuntu22.04', mirror => 'http://archive.example/ubuntu', others => ['http://mn/otherpkgs'], pkgdirs => $mirrors ); +is( scalar( () = $online_both =~ /^ sources:$/mg ), 1, 'online with otherpkgs and pkgdir mirrors: one sources mapping' ); +like( $online_both, qr/xcat-otherpkgs-0\.list:.*xcat-pkgdir-0\.list:/s, '... otherpkgs first, then the mirror' ); + +my $offline_deb822 = apt_config_for( 'ubuntu24.04', pkgdirs => $mirrors ); +like( $offline_deb822, qr{^ URIs: http://mirror\.example/ubuntu\n Suites: noble\n Components: main(?:\n|\z)}m, 'offline Deb822: a stanza per mirror' ); +unlike( $offline_deb822, qr{repo\.example|Trusted: yes\n(?:.*\n)* URIs: http://mirror}, '... nothing trusted and no bare URL' ); + +my $offline_legacy = apt_config_for( 'ubuntu22.04', pkgdirs => $mirrors ); +like( $offline_legacy, qr{^ sources:\n xcat-pkgdir-0\.list:\n source: "deb http://mirror\.example/ubuntu noble main"$}m, 'offline legacy: .list files under sources' ); + +my $same = apt_config_for( 'ubuntu24.04', mirror => 'http://mirror.example/ubuntu', pkgdirs => 'http://mirror.example/ubuntu noble-proposed main' ); +like( $same, qr{^ xcat-pkgdir-0\.sources:\n source: \|\n Types: deb\n URIs: http://mirror\.example/ubuntu\n Suites: noble-proposed\n Components: main\n Signed-By: \Q$keyring\E(?:\n|\z)}m, + 'another suite of the apt mirror is added with the signing key the installer gives that mirror' ); + +# before Deb822 the archive sources are rendered here without a key, so a repeated mirror entry must carry none either +foreach my $release ( [ 'ubuntu20.04', 'focal' ], [ 'ubuntu22.04', 'jammy' ] ) { + my ( $media, $suite ) = @$release; + my $legacy_same = apt_config_for( $media, mirror => 'http://mirror.example/ubuntu', pkgdirs => "http://mirror.example/ubuntu $suite main" ); + like( $legacy_same, qr{^ xcat-pkgdir-0\.list:\n source: "deb http://mirror\.example/ubuntu \Q$suite\E main"$}m, + "$media: an entry that repeats the apt mirror carries no signing key, like the archive sources rendered next to it" ); + like( $legacy_same, qr{^ xcat-ubuntu-archive\.list:\n source: "deb http://mirror\.example/ubuntu \$RELEASE main restricted universe multiverse"$}m, + "$media: which stay as they were" ); +} + +# a repository named by otherpkgdir and by pkgdir is offered once, trusted, on both source forms +foreach my $media ( 'ubuntu24.04', 'ubuntu22.04' ) { + my $overlap = apt_config_for( $media, mirror => 'http://archive.example/ubuntu', others => ['http://repo.example/ubuntu'], + pkgdirs => '/install/ubuntu24.04.4/x86_64,http://repo.example/ubuntu/ ./' ); + like( $overlap, qr/xcat-otherpkgs-0\./, "$media: the repository the otherpkgdir names is offered as the trusted otherpkgs source" ); + unlike( $overlap, qr/xcat-pkgdir/, "$media: and not again as a pkgdir source with other options" ); +} + +# an otherpkgdir entry that names the apt mirror itself gets the options of the source the installer already has for it +my $archive_others = ['http://mirror.example/ubuntu noble universe']; +like( apt_config_for( 'ubuntu22.04', mirror => 'http://mirror.example/ubuntu', others => $archive_others ), + qr{^ xcat-otherpkgs-0\.list:\n source: "deb http://mirror\.example/ubuntu noble universe"$}m, + 'classic: an otherpkgdir entry for the apt mirror carries no option, like the archive sources next to it' ); +like( apt_config_for( 'ubuntu24.04', mirror => 'http://mirror.example/ubuntu', others => $archive_others ), + qr{^ xcat-otherpkgs-0\.sources:\n source: \|\n Types: deb\n URIs: http://mirror\.example/ubuntu\n Suites: noble\n Components: universe\n Signed-By: \Q$keyring\E(?:\n|\z)}m, + 'Deb822: and carries the archive keyring and no Trusted, as the primary source does' ); + +# the same repository and suite with other components: the pkgdir components join the otherpkgs source +my $union = apt_config_for( 'ubuntu24.04', mirror => 'http://archive.example/ubuntu', others => ['http://repo.example/team stable tools'], + pkgdirs => '/install/ubuntu24.04.4/x86_64,http://repo.example/team/ stable compute tools' ); +like( $union, qr{^ xcat-otherpkgs-0\.sources:\n source: \|\n Types: deb\n URIs: http://repo\.example/team\n Suites: stable\n Components: tools compute\n Trusted: yes(?:\n|\z)}m, + 'a pkgdir entry that repeats an otherpkgs repository adds its components to that source' ); +unlike( $union, qr/xcat-pkgdir/, '... and is not a second source' ); +my $union_legacy = apt_config_for( 'ubuntu22.04', mirror => 'http://archive.example/ubuntu', others => ['http://repo.example/team stable tools'], + pkgdirs => '/install/ubuntu24.04.4/x86_64,http://repo.example/team stable compute' ); +like( $union_legacy, qr{^ xcat-otherpkgs-0\.list:\n source: "deb \[trusted=yes\] http://repo\.example/team stable tools compute"$}m, + 'in the one-line form as well' ); + +my $none = apt_config_for( 'ubuntu24.04', mirror => 'http://archive.example/ubuntu' ); +unlike( $none, qr/sources:/, 'without otherpkgs or mirrors no sources mapping is rendered on a Deb822 release' ); + +done_testing(); From a7812caca8fddebe343cb3a99625676c17dfb6f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 10 Sep 2026 12:51:05 -0300 Subject: [PATCH 54/62] feat(Postage): read a pkglist as whole records MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit get_pkglist_tex joins the records of a pkglist with commas for the preseed and OSPKGS consumers, so a caller that needs the records themselves cannot recover a record that contains a comma, such as a tasksel directive. The new reader returns the records whole, comments dropped and includes followed in place. It reads each line as get_pkglist_tex does and resolves every include, nested ones too, against the directory of the listed pkglist, as get_pkglist_tex does. The comma text is unchanged. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-server/lib/perl/xCAT/Postage.pm | 62 ++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/xCAT-server/lib/perl/xCAT/Postage.pm b/xCAT-server/lib/perl/xCAT/Postage.pm index e9d40ba8c..0eccbb26c 100644 --- a/xCAT-server/lib/perl/xCAT/Postage.pm +++ b/xCAT-server/lib/perl/xCAT/Postage.pm @@ -1889,6 +1889,68 @@ sub get_pkglist_tex #---------------------------------------------------------------------------- +=head3 get_pkglist_records + + The records of one or more pkglist files, one per line kept whole and + includes followed. get_pkglist_tex joins records with commas, so it + cannot separate a record that itself contains a comma. + Arguments: comma-separated pkglist file names + Returns: list of records +=cut + +#----------------------------------------------------------------------------- +sub get_pkglist_records +{ + my $allfiles_pkglist = shift; + if ($allfiles_pkglist =~ "xCAT::") { + $allfiles_pkglist = shift; + } + my @records; + foreach my $pkglist (split(/,/, $allfiles_pkglist // '')) + { + next if $pkglist eq ''; + push(@records, pkglist_file_records($pkglist, dirname($pkglist), 0)); + } + return @records; +} + +# pkglist_file_records: the records of one pkglist file, read as get_pkglist_tex reads them, with an +# #INCLUDE: record replaced by the records of the named file. +# A nested include resolves against the directory of the listed pkglist, as get_pkglist_tex resolves it. +sub pkglist_file_records +{ + my ($file, $idir, $depth) = @_; + my @records; + open(my $fh, '<', $file) or return ("#INCLUDEBAD:cannot open pkglist file $file#"); + while (my $line = <$fh>) + { + chomp($line); + $line =~ s/\s+$//; + $line =~ s/^\s*//; + next if $line eq ''; + next + if ($line =~ /^#/ + && $line !~ /^#INCLUDE:[^#^\n]+#/ + && $line !~ /^#NEW_INSTALL_LIST#/ + && $line !~ /^#ENV:[^#^\n]+#/); + if ($line =~ /^#INCLUDE:([^#^\n]+)#(.*)$/ && $depth < 20) + { + my ($name, $note) = ($1, $2); + my $include = xCAT::Utils->varsubinline($name, \%ENV); + $include = "$idir/$include" unless $include =~ m{^/}; + my @included = pkglist_file_records($include, $idir, $depth + 1); + $included[-1] .= $note if @included && $note ne ''; + push(@records, @included); + next; + } + push(@records, $line); + } + close($fh); + return @records; +} + +#---------------------------------------------------------------------------- + =head3 includefile handles #INCLUDE# in otherpkg.pkglist file From ac50225541f64ae1f6fa17db8447872406c96db9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 10 Sep 2026 11:37:47 -0300 Subject: [PATCH 55/62] feat(Template): render the osimage pkglist into the autoinstall package list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Subiquity autoinstall installs what its user-data packages list names, and the templates could only name a fixed set, so the osimage pkglist reached an Ubuntu node through ospkgs after the first boot. The preseed token has no autoinstall form: the package list is YAML, one item per line. A list line that carries #INCLUDE_DEFAULT_PKGLIST_AUTOINSTALL# is now replaced by one item per pkglist package at the same indentation, with includes followed and without repeating the items the template lists above it. The line is replaced in the include pass of subvars, so a site template that includes the stock one is served too. A plain name and a task are installed this way, and a comment after them ends the record. A version pin or a target release stays with ospkgs, because the installer runs apt-get without --allow-downgrades and a pin can require one, and so does a name with an architecture qualifier, because a foreign architecture is enabled by a postscript that runs later. A record that begins with a removal or a group is left out whole, as ospkgs removes or installs it whole, and so are a removal written with a trailing hyphen, markers and preseed directives. A list that carries a #ENV: setting or an unreadable include is left to ospkgs whole. So is the list of an osimage with environvar, which mkinstall now hands over, and such an image's pkgdir mirrors stay out of the installer's sources as well: those variables reach apt-get only through ospkgs, and a mirror may need them. An osimage without a pkglist loses only the token line. The installer's apt configuration turns recommended packages off, as ospkgs installs the list without them; curtin writes that setting into the target, where the template removes it with the installer's sources. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-server/lib/perl/xCAT/Template.pm | 67 +++++++++++++++++++++++++- xCAT-server/lib/xcat/plugins/debian.pm | 10 ++-- 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/xCAT-server/lib/perl/xCAT/Template.pm b/xCAT-server/lib/perl/xCAT/Template.pm index e37652ab1..79b26101f 100644 --- a/xCAT-server/lib/perl/xCAT/Template.pm +++ b/xCAT-server/lib/perl/xCAT/Template.pm @@ -173,6 +173,11 @@ sub subvars { $inc =~ s/#INCLUDE_DEFAULT_RMPKGLIST_S#/#INCLUDE_RMPKGLIST:$pkglistfile#/g; } + # osimage environvar reaches apt-get through ospkgs alone, so such an image installs its list there, + # and its pkgdir mirrors, which may need those variables too, stay out of the installer's sources + my $environvar_set = ( $namedargs{environvar} // '' ) =~ /\S/; + my $installer_pkgdirs = $environvar_set ? undef : $namedargs{pkgdirs}; + my @autoinstall; if (("ubuntu" eq $platform) || ("debian" eq $platform)) { # since debian/ubuntu uses a preseed file instead of a kickstart file, pkglist @@ -186,6 +191,7 @@ sub subvars { if ($allpkglist =~ /#INCLUDEBAD:(.*)#/) { return "$1"; } + @autoinstall = ubuntu_autoinstall_packages( xCAT::Postage->get_pkglist_records($pkglistfile) ) unless $environvar_set; $allpkglist =~ s/,/ /g; $inc =~ s/#INCLUDE_DEFAULT_PKGLIST_PRESEED#/$allpkglist/g; @@ -364,7 +370,7 @@ sub subvars { $inc =~ s/#INSTALL_SOURCES_IN_PRE#/$source_in_pre/g; if (("ubuntu" eq $platform) || ("debian" eq $platform)) { $inc =~ s/#INCLUDE_OSIMAGE_PKGDIR#/$pkgdirs[-1]/; - $inc =~ s/#UBUNTU_SUBIQUITY_APT_CONFIG#/ubuntu_subiquity_apt_config($media_dir, $namedargs{osarch}, $namedargs{pkgdirs})/eg; + $inc =~ s/#UBUNTU_SUBIQUITY_APT_CONFIG#/ubuntu_subiquity_apt_config($media_dir, $namedargs{osarch}, $installer_pkgdirs)/eg; } $inc =~ s/#WRITEREPO#/$writerepo/g; } @@ -377,7 +383,9 @@ sub subvars { $inc =~ s/#INCLUDE_NOP:([^#^\n]+)#/includefile($1,1,0)/eg; $inc =~ s/#XCATVAR:([^#]+)#/envvar($1)/eg; $inc =~ s/#ENV:([^#]+)#/envvar($1)/eg; - $inc =~ s/#UBUNTU_SUBIQUITY_APT_CONFIG#/ubuntu_subiquity_apt_config($media_dir, $namedargs{osarch}, $namedargs{pkgdirs})/eg; + $inc =~ s/#UBUNTU_SUBIQUITY_APT_CONFIG#/ubuntu_subiquity_apt_config($media_dir, $namedargs{osarch}, $installer_pkgdirs)/eg; + # in the include pass, so a template that includes the stock Subiquity one gets its list as well + $inc =~ s/^((?:[ \t]*- [^\n]*\n)*)([ \t]*)- #INCLUDE_DEFAULT_PKGLIST_AUTOINSTALL#[ \t]*\n/$1 . ubuntu_autoinstall_items($2, $1, \@autoinstall)/meg; $inc =~ s/#SUBIQUITYINSTALLNIC#/subiquity_install_nic()/eg; $inc =~ s/#SUBIQUITYINSTALLMAC#/subiquity_install_mac()/eg; $inc =~ s/#MACHINEPASSWORD#/machinepassword()/eg; @@ -1764,6 +1772,59 @@ sub subiquity_install_mac { return $macaddress; } +# ubuntu_autoinstall_packages: the packages of the pkglist records (whole lines, as +# get_pkglist_records returns them) that a Subiquity autoinstall can install through its packages +# list. A record holds one or more space-separated packages, as the preseed path reads it, each a +# plain name or a task. A version pin or a target release stays with ospkgs, because the installer +# runs apt-get without --allow-downgrades and a pin can require one, and so does a name with an +# architecture qualifier, because a foreign architecture is enabled by a postscript that runs later. A preseed directive, told by its question type, a record that begins with a removal +# or a group, which ospkgs removes or installs whole, a removal written with a trailing hyphen as +# apt-get reads it, or a marker has +# no autoinstall form, a comment ends the packages of a record, and a record with a token that is +# none of these is left out whole. A list +# that carries a #ENV: setting, which only ospkgs can pass to apt-get, or an unreadable include is +# left to ospkgs whole; ospkgs still applies the whole list after the install. +my %PRESEED_TYPE = map { $_ => 1 } qw(string boolean select multiselect note password text seen title error); + +sub ubuntu_autoinstall_packages +{ + my @records = grep { defined } @_; + return () if grep { /#(?:ENV:|INCLUDEBAD:)/ } @records; + my (@packages, %seen); + RECORD: foreach my $record (@records) { + my @tokens = grep { length } split( /\s+/, $record ); + next unless @tokens; + next if @tokens >= 3 && $PRESEED_TYPE{ $tokens[2] }; + next if $tokens[0] =~ /^[-@]/; # ospkgs removes or installs the whole record + my @found; + foreach my $token (@tokens) { + last if $token =~ /^#/; + next if $token =~ /^[-@]/ || $token =~ /-$/; + next RECORD unless $token =~ m{^[a-z0-9][a-z0-9+.-]*(?::[a-z0-9-]+)?(?:[=/][^\s/=]+|\^)?$}; + next if $token =~ m{[:=/]}; + push @found, $token; + } + push @packages, grep { !$seen{$_}++ } @found; + } + return @packages; +} + +# ubuntu_autoinstall_items: the list items for the pkglist packages at the token's indentation, +# leaving out packages the items above the token already name, each quoted so a name such as null +# or true stays a string. The time daemons exclude each +# other, so when the template names one, the pkglist's stay with ospkgs, as they did before. +my %UBUNTU_TIME_DAEMON = map { $_ => 1 } qw(chrony ntp ntpsec ntpdate ntpsec-ntpdate openntpd systemd-timesyncd); + +sub ubuntu_autoinstall_items +{ + my ($indent, $listed, $packages) = @_; + my %named = map { $_ => 1 } ( $listed =~ /^[ \t]*- (\S+)[ \t]*$/mg ); + my $fixed_time_daemon = grep { $UBUNTU_TIME_DAEMON{$_} } keys %named; + my @items = grep { !$named{$_} } @$packages; + @items = grep { !$UBUNTU_TIME_DAEMON{ (split /[:=\/^]/, $_)[0] } } @items if $fixed_time_daemon; + return join( '', map { "$indent- \"$_\"\n" } @items ); +} + sub ubuntu_subiquity_apt_mirror { my ($osarch) = @_; @@ -1816,6 +1877,7 @@ sub ubuntu_subiquity_apt_config ' apt:', ' preserve_sources_list: false', ' geoip: false', + q( conf: 'APT::Install-Recommends "false";'), ' mirror-selection:', ' primary:', " - uri: $online_mirror", @@ -1854,6 +1916,7 @@ sub ubuntu_subiquity_apt_config ' preserve_sources_list: false', ' fallback: offline-install', ' geoip: false', + q( conf: 'APT::Install-Recommends "false";'), ' disable_suites:', ' - updates', ' - backports', diff --git a/xCAT-server/lib/xcat/plugins/debian.pm b/xCAT-server/lib/xcat/plugins/debian.pm index 7edf607bc..e085efc28 100644 --- a/xCAT-server/lib/xcat/plugins/debian.pm +++ b/xCAT-server/lib/xcat/plugins/debian.pm @@ -895,6 +895,7 @@ sub mkinstall { my $partitionfile; my $pkgdir; my $pkgdirval; + my $environvar; my @mirrors; my $pkglistfile; my $imagename; # set it if running of 'nodeset osimage=xxx' @@ -917,12 +918,13 @@ sub mkinstall { if (!$osimagetab) { $osimagetab = xCAT::Table->new('osimage', -create => 1); } - (my $ref) = $osimagetab->getAttribs({ imagename => $imagename }, 'osvers', 'osarch', 'profile', 'provmethod'); + (my $ref) = $osimagetab->getAttribs({ imagename => $imagename }, 'osvers', 'osarch', 'profile', 'provmethod', 'environvar'); if ($ref) { $img_hash{$imagename}->{osver} = $ref->{'osvers'}; $img_hash{$imagename}->{osarch} = $ref->{'osarch'}; $img_hash{$imagename}->{profile} = $ref->{'profile'}; $img_hash{$imagename}->{provmethod} = $ref->{'provmethod'}; + $img_hash{$imagename}->{environvar} = $ref->{'environvar'}; if (!$linuximagetab) { $linuximagetab = xCAT::Table->new('linuximage', -create => 1); } @@ -995,6 +997,7 @@ sub mkinstall { $tmplfile = $ph->{template}; $pkgdirval = $ph->{pkgdir}; + $environvar = $ph->{environvar}; my @pkgdirlist = split(/,/, $pkgdirval); foreach (@pkgdirlist) { if ($_ =~ /^http|ssh/) { @@ -1123,8 +1126,9 @@ sub mkinstall { $platform, $partitionfile, \%tmpl_hash, - osarch => $arch, - pkgdirs => $pkgdirval + osarch => $arch, + pkgdirs => $pkgdirval, + environvar => $environvar ); } From d266a6ef7baa350640c588e5ee7ace4e6cbbfa5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 10 Sep 2026 11:37:47 -0300 Subject: [PATCH 56/62] test(xCAT-test): cover the autoinstall package list rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The entry filter is called directly, and a template with the token is rendered through subvars against a pkglist with a comment, a removal, a group and an include. The rendered list must carry one item per package at the token's indentation, and no token line without a pkglist. Against the previous module the helper does not exist. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-test/unit/ubuntu_subiquity_pkglist.t | 200 ++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 xCAT-test/unit/ubuntu_subiquity_pkglist.t diff --git a/xCAT-test/unit/ubuntu_subiquity_pkglist.t b/xCAT-test/unit/ubuntu_subiquity_pkglist.t new file mode 100644 index 000000000..05d15c361 --- /dev/null +++ b/xCAT-test/unit/ubuntu_subiquity_pkglist.t @@ -0,0 +1,200 @@ +#!/usr/bin/env perl +use strict; +use warnings; +no warnings 'once'; + +use FindBin; +use lib "$FindBin::Bin/../lib"; +use File::Spec; +use File::Temp; +use Test::More; + +use XCAT::Test::File qw(repo_path); + +sub read_text { my ($path) = @_; open( my $fh, '<', $path ) or die "$path: $!"; local $/; my $text = <$fh>; close($fh); return $text; } +sub write_text { my ( $path, $text ) = @_; open( my $fh, '>', $path ) or die "$path: $!"; print {$fh} $text; close($fh); return; } + +# A Subiquity autoinstall installs the packages of its user-data packages list, and until now the +# template named a fixed set, so the osimage pkglist reached the node only through ospkgs after +# the first boot. The template can now carry #INCLUDE_DEFAULT_PKGLIST_AUTOINSTALL# on a list line, +# and Template.pm renders one list item per pkglist package in its place. + +my $module = repo_path('xCAT-server/lib/perl/xCAT/Template.pm'); +plan skip_all => 'Template.pm not found' unless -r $module; + +my @incs = ( repo_path('perl-xCAT'), repo_path('xCAT-server/lib/perl') ); + +# Every xCAT module prepends $XCATROOT/lib/perl as it compiles, so on a host with xCAT installed +# the modules Template.pm loads afterwards would come from /opt/xcat. Point it at the checkout. +my $xcatroot = File::Temp->newdir(); +mkdir "$xcatroot/lib" or die "$xcatroot/lib: $!"; +symlink( repo_path('xCAT-server/lib/perl'), "$xcatroot/lib/perl" ) or die "symlink: $!"; +$ENV{XCATROOT} = "$xcatroot"; + +my $devnull = File::Spec->devnull(); +my $probe = join( ' ', $^X, ( map { "-I$_" } @incs ), '-e', "'require xCAT::Template; 1'", ">$devnull", "2>&1" ); +plan skip_all => 'xCAT::Template cannot be loaded here' if system($probe) != 0; + +require lib; +lib->import(@incs); +require xCAT::Template; +require xCAT::Postage; # the pkglist reader Template.pm calls, loaded by the plugin in production + +# ---- the record filter: only what apt can be asked for in an autoinstall packages list -------- +my @packages = xCAT::Template::ubuntu_autoinstall_packages( + 'openssh-server', ' gawk', 'ntp', '-snmpd', '@core', '#NEW_INSTALL_LIST#', 'd-i pkgsel/include string foo', + 'd-i tasksel/first multiselect standard,not-a-real-package', 'nfs-common=1:2.6.4-3ubuntu5', 'gawk', 'Bad_Name', '', + 'libc6', 'libc6:i386', 'curl/noble', 'dns-server^', 'a/b/c', 'vim rsync -busybox-static gpg', '@Group With Space', 'wget Bad_Name', 'bc # a calculator', + 'wget-', 'libc6:i386-', 'tree+', '-snmpd apache2', '@core bc' +); +is_deeply( \@packages, [qw(openssh-server gawk ntp libc6 dns-server^ vim rsync gpg bc tree+)], + 'names and tasks are kept once each, a space-separated line gives each package, a comment ends it; pins, target releases, architecture qualifiers, removals in either hyphen form, a record that begins with a removal or a group, markers, directives and unknown syntax are not' ); +is_deeply( [ xCAT::Template::ubuntu_autoinstall_packages( 'msodbcsql18', '#ENV:ACCEPT_EULA=Y#', 'gawk' ) ], [], + 'a list with an apt environment setting stays with ospkgs whole' ); +is_deeply( [ xCAT::Template::ubuntu_autoinstall_packages( 'gawk', 'msodbcsql18 #ENV:ACCEPT_EULA=Y#' ) ], [], + 'so does a list with the setting after a package on the same line, where get_envlist reads it too' ); +is_deeply( [ xCAT::Template::ubuntu_autoinstall_packages( 'gawk', '#INCLUDEBAD:cannot open pkglist file /absent.pkglist#' ) ], [], + 'a list with an unreadable include stays with ospkgs whole' ); +is_deeply( [ xCAT::Template::ubuntu_autoinstall_packages() ], [], 'no records give no packages' ); + +# ---- the record reader: lines kept whole, includes followed, the comma text unchanged --------- +{ + my $d = File::Temp->newdir(); + write_text( "$d/common.pkglist", "# shared\nnfs-common\n\@Group With Space\n" ); + write_text( "$d/compute.pkglist", "openssh-server\n # a comment\nd-i tasksel/first multiselect standard,not-a-real-package\n#INCLUDE:$d/common.pkglist#\n#NEW_INSTALL_LIST#\nchrony\n" ); + my @records = xCAT::Postage->get_pkglist_records("$d/compute.pkglist"); + is_deeply( \@records, + [ 'openssh-server', 'd-i tasksel/first multiselect standard,not-a-real-package', 'nfs-common', '@Group With Space', '#NEW_INSTALL_LIST#', 'chrony' ], + 'records are whole lines, comments dropped, the include expanded in place' ); + is( xCAT::Postage->get_pkglist_tex("$d/compute.pkglist"), + 'openssh-server,d-i tasksel/first multiselect standard,not-a-real-package,nfs-common,@Group With Space,#NEW_INSTALL_LIST#,chrony', + 'the comma text ospkgs receives is unchanged, and cannot tell the directive comma apart' ); + my @missing = xCAT::Postage->get_pkglist_records("$d/absent.pkglist"); + like( $missing[0], qr/^#INCLUDEBAD:/, 'an unreadable file yields the INCLUDEBAD marker record' ); + + # top.pkglist includes sub/common.pkglist, which includes leaf.pkglist: the leaf next to top.pkglist is the one meant + mkdir "$d/sub" or die "$d/sub: $!"; + write_text( "$d/top.pkglist", "#INCLUDE:sub/common.pkglist#\n" ); + write_text( "$d/sub/common.pkglist", "#INCLUDE:leaf.pkglist#\n" ); + write_text( "$d/leaf.pkglist", "nfs-common\n" ); + write_text( "$d/sub/leaf.pkglist", "snmpd\n" ); + is_deeply( [ xCAT::Postage->get_pkglist_records("$d/top.pkglist") ], ['nfs-common'], + 'a nested include resolves against the directory of the listed pkglist' ); + is_deeply( [ xCAT::Postage->get_pkglist_records("$d/top.pkglist") ], [ split /,/, xCAT::Postage->get_pkglist_tex("$d/top.pkglist") ], + 'and reads the same files get_pkglist_tex reads' ); + + write_text( "$d/note.pkglist", "#INCLUDE:leaf.pkglist# # the shared leaf\nbc # a calculator\n" ); + my @noted = xCAT::Postage->get_pkglist_records("$d/note.pkglist"); + is_deeply( \@noted, [ split /,/, xCAT::Postage->get_pkglist_tex("$d/note.pkglist") ], + 'an include followed by a note is expanded, the note staying on the last record as get_pkglist_tex leaves it' ); + is_deeply( [ xCAT::Template::ubuntu_autoinstall_packages(@noted) ], [qw(nfs-common bc)], 'and the notes add no packages' ); +} +is( xCAT::Template::ubuntu_autoinstall_items( " ", " - wget\n - gpg\n", [qw(gawk gpg chrony)] ), + " - \"gawk\"\n - \"chrony\"\n", 'items already listed above the token are not repeated, and every item is a quoted string' ); +is( xCAT::Template::ubuntu_autoinstall_items( " ", "", [qw(null true 12)] ), " - \"null\"\n - \"true\"\n - \"12\"\n", + 'names YAML would read as null, boolean or number stay strings' ); +is( xCAT::Template::ubuntu_autoinstall_items( " ", " - wget\n - chrony\n", [qw(gawk ntp ntpdate snmpd)] ), + " - \"gawk\"\n - \"snmpd\"\n", 'a time daemon the template installs keeps the pkglist time daemons with ospkgs' ); +is( xCAT::Template::ubuntu_autoinstall_items( " ", " - wget\n", [qw(gawk ntp)] ), + " - \"gawk\"\n - \"ntp\"\n", 'without a fixed time daemon the pkglist one is installed' ); + +# ---- the rendering: the token line becomes one item per package, at its own indentation ------- +my %site; +no warnings 'redefine', 'once'; +local *xCAT::TableUtils::get_site_attribute = sub { + my ( undef, $key ) = @_; + return defined $site{$key} ? ( $site{$key} ) : (); +}; +local *xCAT::NetworkUtils::getipaddr = sub { return '192.0.2.10'; }; +local *xCAT::Template::getPersistentKcmdline = sub { return ''; }; +use warnings; + +my $dir = File::Temp->newdir(); +my $included = File::Spec->catfile( "$dir", 'common.pkglist' ); +my $pkglist = File::Spec->catfile( "$dir", 'compute.pkglist' ); +write_text( $included, "# shared\nnfs-common\nsnmpd\n" ); +write_text( $pkglist, "openssh-server\n# a comment\nchrony rsync # time and files\nwget=1.21.2-2ubuntu1\n-ntp\nwget-\n\@standard\nd-i tasksel/first multiselect standard,not-a-real-package\n#INCLUDE:$included#\n" ); + +my $in = File::Spec->catfile( "$dir", 'in.tmpl' ); +write_text( $in, + " packages:\n" + . " - openssh-server\n" + . " - wget\n" + . " - #INCLUDE_DEFAULT_PKGLIST_AUTOINSTALL#\n" + . " late-commands:\n" + . " - echo done\n" ); + +my $render = sub { + my ( $list, %extra ) = @_; + %site = ( installdir => '/install' ); + my $out = File::Spec->catfile( "$dir", 'out.' . ( defined $list ? 'list' : 'none' ) ); + xCAT::Template->subvars( $in, $out, 'testnode', $list, '/install/ubuntu24.04/x86_64', 'ubuntu', undef, + { xcatmaster => '192.0.2.10' }, osarch => 'x86_64', %extra ); + return read_text($out); +}; + +my $rendered = $render->($pkglist); +is( $rendered, + " packages:\n" + . " - openssh-server\n" + . " - wget\n" + . " - \"chrony\"\n" + . " - \"rsync\"\n" + . " - \"nfs-common\"\n" + . " - \"snmpd\"\n" + . " late-commands:\n" + . " - echo done\n", + 'the pkglist packages, includes followed, become list items at the token indentation, without repeating the items above' ); +unlike( $rendered, qr/not-a-real-package/, 'a package name inside a preseed directive with commas is not a package' ); +unlike( $rendered, qr/wget=/, 'a version pin is not an item: it stays with ospkgs' ); +unlike( $rendered, qr/wget-/, 'a trailing-hyphen removal is not an item either: the installer would remove the package the postscripts need' ); +unlike( $rendered, qr/"(?:time|and|files)"/, 'an inline comment adds no items' ); + +# a site template that includes the stock one: the token arrives with the include and must be expanded too +my $wrapper = File::Spec->catfile( "$dir", 'wrapper.tmpl' ); +write_text( $wrapper, "#INCLUDE:$in#\n" ); +my $render_via = sub { + my ($list) = @_; + %site = ( installdir => '/install' ); + my $out = File::Spec->catfile( "$dir", 'out.wrapper.' . ( defined $list ? 'list' : 'none' ) ); + xCAT::Template->subvars( $wrapper, $out, 'testnode', $list, '/install/ubuntu24.04/x86_64', 'ubuntu', undef, { xcatmaster => '192.0.2.10' }, osarch => 'x86_64' ); + return read_text($out); +}; +is( $render_via->($pkglist), $rendered, 'a template that includes the stock one renders the same package list' ); + +my $without = $render->(undef); +is( $without, + " packages:\n - openssh-server\n - wget\n late-commands:\n - echo done\n", + 'an osimage without a pkglist keeps the template packages and loses only the token line' ); +is( $render_via->(undef), $without, 'and through an including template the token line goes as well, leaving no empty item' ); + +my $env_list = File::Spec->catfile( "$dir", 'env.pkglist' ); +my $env_included = File::Spec->catfile( "$dir", 'env-common.pkglist' ); +write_text( $env_included, "msodbcsql18 #ENV:ACCEPT_EULA=Y#\n" ); +write_text( $env_list, "gawk\n#INCLUDE:$env_included#\n" ); +is( $render->($env_list), $without, 'a pkglist whose include carries an apt environment setting is left to ospkgs whole, and the token line goes' ); +is( $render->( $pkglist, environvar => 'ACCEPT_EULA=Y' ), $without, + 'an osimage with environvar installs its pkglist through ospkgs alone, where the variables reach apt-get, and the token line goes' ); + +# the pkgdir mirrors may need those variables as well, so they stay out of the installer's apt sources too +{ + no warnings 'redefine', 'once'; + local *xCAT::Template::ubuntu_subiquity_otherpkg_sources = sub { () }; + local *xCAT::Template::ubuntu_subiquity_apt_mirror = sub { 'http://archive.example/ubuntu' }; + my $apt_in = File::Spec->catfile( "$dir", 'apt.tmpl' ); + write_text( $apt_in, "#UBUNTU_SUBIQUITY_APT_CONFIG#\n" ); + my $apt_render = sub { + my (%extra) = @_; + %site = ( installdir => '/install' ); + my $out = File::Spec->catfile( "$dir", 'out.apt' ); + xCAT::Template->subvars( $apt_in, $out, 'testnode', $pkglist, '/install/ubuntu24.04/x86_64', 'ubuntu', undef, { xcatmaster => '192.0.2.10' }, + osarch => 'x86_64', pkgdirs => '/install/ubuntu24.04/x86_64,http://mirror.example/ubuntu noble main', %extra ); + return read_text($out); + }; + like( $apt_render->(), qr{URIs: http://mirror\.example/ubuntu}, 'the pkgdir mirror joins the installer sources' ); + like( $apt_render->(), qr{^ conf: 'APT::Install-Recommends "false";'$}m, 'the installer installs without recommended packages, as ospkgs does' ); + unlike( $apt_render->( environvar => 'http_proxy=http://proxy.example:3128' ), qr{mirror\.example|xcat-pkgdir}, + 'but not for an osimage with environvar, whose mirrors may need those variables' ); +} + +done_testing(); From a711f6c1e1044d97b3e170c77f9b98ec3859b3c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 10 Sep 2026 11:37:47 -0300 Subject: [PATCH 57/62] feat(ubuntu): install the osimage pkglist during the Subiquity autoinstall MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit compute.subiquity.tmpl named a fixed package set, so the osimage pkglist took effect only when ospkgs ran after the first boot. The packages list now ends with the autoinstall token, so the pkglist packages install from the configured apt mirror during the autoinstall. The fixed set stays, so a node installs the same packages as before plus its pkglist. The apt sources the installer gets for the pkgdir mirrors and the otherpkgs repository are removed from the target at the end of the install. ospkgs and otherpkgs write their own after the first boot, and a second source for one repository with other options makes apt refuse the whole list. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- .../share/xcat/install/ubuntu/compute.subiquity.tmpl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl b/xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl index 5918a61e3..328ccd258 100644 --- a/xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl +++ b/xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl @@ -43,6 +43,7 @@ autoinstall: - bind9-dnsutils - chrony - gpg + - #INCLUDE_DEFAULT_PKGLIST_AUTOINSTALL# early-commands: - | exec >/tmp/pre-install.log 2>&1 @@ -111,6 +112,11 @@ autoinstall: cp ./#HOSTNAME#.post /target/root/post.script; curtin in-target --target /target /root/post.script; } >>/target/var/log/xcat/xcat.log 2>&1' + # The installer's sources for the otherpkgs repository and the pkgdir mirrors, and the apt + # configuration that kept recommended packages out, served the install; ospkgs and otherpkgs + # write their own after the first boot. A separate item, so the status of the post script above + # still decides whether the install goes on. + - rm -f /target/etc/apt/sources.list.d/xcat-otherpkgs-*.list /target/etc/apt/sources.list.d/xcat-otherpkgs-*.sources /target/etc/apt/sources.list.d/xcat-pkgdir-*.list /target/etc/apt/sources.list.d/xcat-pkgdir-*.sources /target/etc/apt/apt.conf.d/94curtin-config # Flip the node to local-disk boot, or it PXE-loops back into the installer on reboot. # xcatd's install monitor greets with "ready", then answers "next" with "done" and runs # "nodeset next". Require both tokens: another service on that port is not a flipped From 8d4fd845edb0f52549fdd055bf6d28a092f336dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 10 Sep 2026 11:37:47 -0300 Subject: [PATCH 58/62] test(xCAT-test): pin the autoinstall token in the Subiquity template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The packages list must carry the token and keep openssh-server and wget, which xCAT and the template's own commands need. Against the previous template the token assertion fails. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-test/unit/ubuntu_subiquity_template.t | 37 ++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/xCAT-test/unit/ubuntu_subiquity_template.t b/xCAT-test/unit/ubuntu_subiquity_template.t index 140dfaf39..ecff4688e 100644 --- a/xCAT-test/unit/ubuntu_subiquity_template.t +++ b/xCAT-test/unit/ubuntu_subiquity_template.t @@ -16,6 +16,9 @@ like($tmpl, qr/autoinstall:/, 'template has autoinstall: key'); like($tmpl, qr/version:\s*1/, 'template has version: 1'); like($tmpl, qr/^\s*identity:/m, 'template has an identity section so subiquity does not prompt'); +like($tmpl, qr/^ - #INCLUDE_DEFAULT_PKGLIST_AUTOINSTALL#\n/m, 'the packages list carries the osimage pkglist through the autoinstall token'); +like($tmpl, qr/^ - openssh-server\n/m, '... and keeps openssh-server, which xCAT needs on the node'); +like($tmpl, qr/^ - wget\n/m, '... and wget, which the early and late commands use'); like($tmpl, qr/kernel:/, 'template has kernel section'); like($tmpl, qr/package:\s*linux-generic/, 'template specifies linux-generic kernel'); like($tmpl, qr/#UBUNTU_SUBIQUITY_APT_CONFIG#/, 'template renders apt section from osimage context'); @@ -78,5 +81,39 @@ unlike($tmpl, qr/if \[ -x \/tmp\/pre\.sh \]/, 'pre.sh not checked with -x'); # Subiquity behavior can be handled without cloning this template per release. unlike($tmpl, qr/noble-|jammy-|focal-/, 'template avoids release-specific apt suite names'); like($tmpl, qr/#UBUNTU_SUBIQUITY_APT_CONFIG#/, 'template keeps dynamic apt renderer marker'); +like($tmpl, qr{2>&1'\n(?:\s*#[^\n]*\n)*\s*- rm -f (?:/target/etc/apt/sources\.list\.d/xcat-(?:otherpkgs|pkgdir)-\*\.(?:list|sources)\s+){4}/target/etc/apt/apt\.conf\.d/94curtin-config$}m, + 'the installer sources for the otherpkgs repository and the pkgdir mirrors, and the apt configuration curtin wrote, are removed from the target by a late-command of their own, after the post script block'); + +# the post script block: its status is the post script's, so a failed post script stops the install +{ + my ($block) = $tmpl =~ /\n - '(\{\n.*?\n \} >>\/target\/var\/log\/xcat\/xcat\.log 2>&1)'\n/s; + ok( defined $block, 'the post script block is found' ) or last; + $block =~ s/''/'/g; + require File::Temp; + my $root = File::Temp->newdir(); + mkdir "$root/bin" or die; + for my $tool ( 'curtin', 'wget' ) { + open( my $fh, '>', "$root/bin/$tool" ) or die; + print {$fh} $tool eq 'curtin' ? "#!/bin/sh\ncase \"\$*\" in *post.script*) exit 42;; esac\nexit 0\n" : "#!/bin/sh\nfor a; do case \"\$a\" in http*) touch \"\${a##*/}\";; esac; done\nexit 0\n"; + close $fh; chmod 0755, "$root/bin/$tool"; + } + ( my $script = $block ) =~ s{/target}{$root/target}g; + $script =~ s{/tmp/pre-install\.log}{$root/pre-install.log}g; + for my $token ( [ '#SUBIQUITYINSTALLNIC#', '' ], [ '#SUBIQUITYINSTALLMAC#', '52:54:00:00:00:01' ], [ '#HOSTNAME#', 'cn1' ], [ '#XCATVAR:XCATMASTER#', '192.0.2.10' ], + [ '#COLONHTTPPORT#', '' ], [ '#TABLEBLANKOKAY:bootparams:$NODE:kcmdline#', '' ] ) { + $script =~ s/\Q$token->[0]\E/$token->[1]/g; + } + require File::Path; + File::Path::make_path( map { "$root/target/$_" } qw(etc/default root var/log/xcat) ); + open( my $hosts, '>', "$root/target/etc/hosts" ) or die; print {$hosts} "127.0.0.1 localhost\n"; close $hosts; + open( my $pre, '>', "$root/pre-install.log" ) or die; close $pre; + my $cwd = File::Spec->rel2abs('.'); + chdir $root or die; + local $ENV{PATH} = "$root/bin:$ENV{PATH}"; + system( 'sh', '-c', $script ); + my $status = $? >> 8; + chdir $cwd or die; + is( $status, 42, 'a failing post script fails the late-command block, so Subiquity stops the install instead of switching the node to disk boot' ); +} done_testing(); From 0b6478453c273a4762ed1b8a08e4e58349ec1e94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 10 Sep 2026 11:37:48 -0300 Subject: [PATCH 59/62] docs(deployment): the pkglist installs during a Subiquity autoinstall MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- .../common/deployment/additionalpkg/additional_pkg_overview.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/guides/admin-guides/manage_clusters/common/deployment/additionalpkg/additional_pkg_overview.rst b/docs/source/guides/admin-guides/manage_clusters/common/deployment/additionalpkg/additional_pkg_overview.rst index f867e901a..27ce4d7a3 100644 --- a/docs/source/guides/admin-guides/manage_clusters/common/deployment/additionalpkg/additional_pkg_overview.rst +++ b/docs/source/guides/admin-guides/manage_clusters/common/deployment/additionalpkg/additional_pkg_overview.rst @@ -6,6 +6,8 @@ The name of the packages that will be installed on the node are stored in the pa * The package list file contains the names of the packages that comes from the os distro. They are stored in .pkglist file. * The other package list file contains the names of the packages that do NOT come from the os distro. They are stored in .otherpkgs.pkglist file. +On Ubuntu releases that install with Subiquity, the packages in the .pkglist file are installed during the autoinstall from the configured apt mirror and the pkgdir mirrors, without recommended packages as ``ospkgs`` installs them, and ``ospkgs`` applies the whole list again after the first boot. A version pin, a target release or an architecture qualifier in the list, a list that carries a ``#ENV:`` setting, and the list of an osimage with ``environvar`` are installed by ``ospkgs`` only. The apt sources the installer uses for the pkgdir mirrors and the otherpkgs repository do not remain on the node: ``ospkgs`` and ``otherpkgs`` write their own after the first boot, as before. ``ospkgs`` writes http mirrors only, so an https mirror or a local directory in pkgdir serves the autoinstall and is not an apt source after the first boot. + The path to the package lists will be read from the osimage definition. Which osimage a node is using is specified by the provmethod attribute. To display this value for a node: :: lsdef node1 -i provmethod From bf2f4531511768a0cc6f1daaeeff56793b3b8d8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 10 Sep 2026 12:30:49 -0300 Subject: [PATCH 60/62] test(xCAT-test): stop pinning the shared list for the 24.04 compute profile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The install compute profile gets a 24.04 pkglist of its own next, so the assertion that it resolves the shared list with ntp is removed ahead of it. The service, kvm and netboot cases keep that pin. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-test/unit/ubuntu_shared_pkglists.t | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/xCAT-test/unit/ubuntu_shared_pkglists.t b/xCAT-test/unit/ubuntu_shared_pkglists.t index 0fa526fdc..ff1d0092e 100644 --- a/xCAT-test/unit/ubuntu_shared_pkglists.t +++ b/xCAT-test/unit/ubuntu_shared_pkglists.t @@ -39,7 +39,8 @@ sub resolved { } # The shared lists keep ntp for the releases that still carry it. aarch64 has no list of its own. -foreach my $case ( [ $install, 'compute' ], [ $install, 'service' ], [ $install, 'kvm' ], [ $netboot, 'compute' ] ) { +# The install compute profile gets a 24.04 list of its own next, so it is no longer pinned here. +foreach my $case ( [ $install, 'service' ], [ $install, 'kvm' ], [ $netboot, 'compute' ] ) { my ( $dir, $profile ) = @$case; my ( $file, $p ) = resolved( $dir, $profile, 'ubuntu24.04.4', 'aarch64' ); is( $file, "$profile.pkglist", "$profile on 24.04 without a list of its own resolves to the shared list" ); From eb587982e8c9320f8afbd77aae49dab0ffe6f759 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 10 Sep 2026 12:07:12 -0300 Subject: [PATCH 61/62] fix(ubuntu): give the Subiquity releases default pkglists with chrony MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ubuntu 20.04, 22.04 and 24.04 resolved the shared compute.pkglist on every architecture but x86_64 20.04, and that list names ntp for the releases before Subiquity. The Subiquity template installs chrony, and on these releases ntp pulls ntpsec, which conflicts with it, so one apt transaction with both cannot be satisfied and ospkgs replaced chrony after the first boot. Each of the three releases now has its own default list with chrony, the list 26.04 already had. The shared list keeps ntp for the releases where chrony was not the default. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- .../share/xcat/install/ubuntu/compute.ubuntu20.04.pkglist | 5 +++++ .../share/xcat/install/ubuntu/compute.ubuntu22.04.pkglist | 5 +++++ .../share/xcat/install/ubuntu/compute.ubuntu24.04.pkglist | 5 +++++ 3 files changed, 15 insertions(+) create mode 100644 xCAT-server/share/xcat/install/ubuntu/compute.ubuntu20.04.pkglist create mode 100644 xCAT-server/share/xcat/install/ubuntu/compute.ubuntu22.04.pkglist create mode 100644 xCAT-server/share/xcat/install/ubuntu/compute.ubuntu24.04.pkglist diff --git a/xCAT-server/share/xcat/install/ubuntu/compute.ubuntu20.04.pkglist b/xCAT-server/share/xcat/install/ubuntu/compute.ubuntu20.04.pkglist new file mode 100644 index 000000000..aaec41835 --- /dev/null +++ b/xCAT-server/share/xcat/install/ubuntu/compute.ubuntu20.04.pkglist @@ -0,0 +1,5 @@ +openssh-server +chrony +gawk +nfs-common +snmpd diff --git a/xCAT-server/share/xcat/install/ubuntu/compute.ubuntu22.04.pkglist b/xCAT-server/share/xcat/install/ubuntu/compute.ubuntu22.04.pkglist new file mode 100644 index 000000000..aaec41835 --- /dev/null +++ b/xCAT-server/share/xcat/install/ubuntu/compute.ubuntu22.04.pkglist @@ -0,0 +1,5 @@ +openssh-server +chrony +gawk +nfs-common +snmpd diff --git a/xCAT-server/share/xcat/install/ubuntu/compute.ubuntu24.04.pkglist b/xCAT-server/share/xcat/install/ubuntu/compute.ubuntu24.04.pkglist new file mode 100644 index 000000000..aaec41835 --- /dev/null +++ b/xCAT-server/share/xcat/install/ubuntu/compute.ubuntu24.04.pkglist @@ -0,0 +1,5 @@ +openssh-server +chrony +gawk +nfs-common +snmpd From 65232ac215d5eac7a4379f2b67cf3a1ef1e0ecbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 10 Sep 2026 12:30:50 -0300 Subject: [PATCH 62/62] test(xCAT-test): pin one time daemon per Subiquity install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The compute pkglist of each Subiquity release and architecture is resolved as mkinstall resolves it, its packages read as ospkgs reads them, and joined with the template's fixed set: chrony must be there, ntp must not, and the union must carry exactly one time daemon. 16.04 must still resolve the shared list with ntp. Against the previous tree the 20.04, 22.04 and 24.04 cases fail. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-test/unit/ubuntu_shared_pkglists.t | 8 ++- xCAT-test/unit/ubuntu_subiquity_pkglists.t | 62 ++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 xCAT-test/unit/ubuntu_subiquity_pkglists.t diff --git a/xCAT-test/unit/ubuntu_shared_pkglists.t b/xCAT-test/unit/ubuntu_shared_pkglists.t index ff1d0092e..3046c3985 100644 --- a/xCAT-test/unit/ubuntu_shared_pkglists.t +++ b/xCAT-test/unit/ubuntu_shared_pkglists.t @@ -39,7 +39,8 @@ sub resolved { } # The shared lists keep ntp for the releases that still carry it. aarch64 has no list of its own. -# The install compute profile gets a 24.04 list of its own next, so it is no longer pinned here. +# The install compute profile has a 24.04 list of its own, with chrony: the Subiquity template +# installs chrony and ntp cannot join it in one apt transaction (ubuntu_subiquity_pkglists.t). foreach my $case ( [ $install, 'service' ], [ $install, 'kvm' ], [ $netboot, 'compute' ] ) { my ( $dir, $profile ) = @$case; my ( $file, $p ) = resolved( $dir, $profile, 'ubuntu24.04.4', 'aarch64' ); @@ -47,6 +48,11 @@ foreach my $case ( [ $install, 'service' ], [ $install, 'kvm' ], [ $netboot, 'co ok( $p->{ntp}, "... which keeps ntp" ); ok( !$p->{$_}, "... and no longer names $_" ) for qw(libodbc1 qemu-kvm libvirt-bin); } +{ + my ( $file, $p ) = resolved( $install, 'compute', 'ubuntu24.04.4', 'aarch64' ); + is( $file, 'compute.ubuntu24.04.pkglist', 'the install compute profile on 24.04 resolves its own list' ); + ok( $p->{chrony} && !$p->{ntp}, '... which names chrony and not ntp' ); +} ok( packages_in("$install/service.pkglist")->{unixodbc}, 'the shared service list names unixodbc' ); ok( packages_in("$install/service.pkglist")->{'libdbd-pg-perl'}, '... and the PostgreSQL driver beside the MySQL one' ); diff --git a/xCAT-test/unit/ubuntu_subiquity_pkglists.t b/xCAT-test/unit/ubuntu_subiquity_pkglists.t new file mode 100644 index 000000000..393dee965 --- /dev/null +++ b/xCAT-test/unit/ubuntu_subiquity_pkglists.t @@ -0,0 +1,62 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use File::Spec; +use FindBin; +use Test::More; + +use lib "$FindBin::Bin/../../perl-xCAT"; +use lib "$FindBin::Bin/../../xCAT-server/lib/perl"; +use xCAT::Postage; +use xCAT::SvrUtils; + +# The Subiquity autoinstall installs the template's fixed packages and the osimage pkglist in one +# apt transaction, and the template names chrony. The shared compute.pkglist names ntp for the +# releases before Subiquity, and on the Subiquity releases ntp pulls ntpsec, which conflicts with +# chrony, so those releases need their own default list. The lists are resolved the way +# mkinstall resolves them and their packages are read the way ospkgs reads them. + +my $repo = File::Spec->rel2abs( File::Spec->catdir( $FindBin::Bin, '..', '..' ) ); +my $install = "$repo/xCAT-server/share/xcat/install/ubuntu"; +my $template = "$install/compute.subiquity.tmpl"; +plan skip_all => 'the Ubuntu install directory is not here' unless -d $install && -f $template; + +sub packages_in { + my ($path) = @_; + return { map { $_ => 1 } grep { length } split /,/, xCAT::Postage::get_pkglist_tex($path) }; +} + +sub resolved { + my ( $os, $arch ) = @_; + return xCAT::SvrUtils->get_pkglist_file_name( $install, 'compute', $os, $arch, $os =~ /^(ubuntu\d+\.\d+)/ ? $1 : $os ); +} + +open( my $tfh, '<', $template ) or die "$template: $!"; +my $body = do { local $/; <$tfh> }; +close($tfh); +my ($block) = $body =~ /^ packages:\n((?: - .*\n)+)/m; +my %fixed = map { $_ => 1 } ( $block =~ /^ - ([^#\s]+)$/mg ); +ok( $fixed{chrony}, 'the Subiquity template names chrony among its fixed packages' ); + +my %time_daemon = map { $_ => 1 } qw(chrony ntp ntpsec); +foreach my $case ( [ 'ubuntu20.04.6', 'x86_64' ], [ 'ubuntu20.04.6', 'ppc64le' ], + [ 'ubuntu22.04.5', 'x86_64' ], [ 'ubuntu22.04.5', 'ppc64le' ], + [ 'ubuntu24.04.4', 'x86_64' ], [ 'ubuntu24.04.4', 'ppc64le' ], [ 'ubuntu24.04.4', 'riscv64' ], + [ 'ubuntu26.04.1', 'x86_64' ], [ 'ubuntu26.04.1', 'riscv64' ] ) { + my ( $os, $arch ) = @$case; + my $file = resolved( $os, $arch ); + ok( $file, "$os $arch resolves a compute pkglist" ) or next; + my $packages = packages_in($file); + ok( $packages->{chrony}, "$os $arch: the list names chrony, the daemon the template installs" ); + ok( !$packages->{ntp} && !$packages->{ntpdate}, "$os $arch: ... and not ntp, which would conflict with it" ); + my @daemons = grep { $time_daemon{$_} } keys %{ { %fixed, %$packages } }; + is( scalar(@daemons), 1, "$os $arch: the autoinstall transaction carries exactly one time daemon" ); +} + +# The releases before Subiquity keep the shared list and its ntp. +my $legacy = resolved( 'ubuntu16.04.7', 'x86_64' ); +is( $legacy, "$install/compute.pkglist", '16.04 still resolves the shared list' ); +ok( packages_in($legacy)->{ntp}, '... which keeps ntp for the releases where chrony was not the default' ); + +done_testing();