From 4a202f1b521a3509af06d792d613eeb819130b59 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, 9 Oct 2026 18:56:38 -0300 Subject: [PATCH 1/4] test(genesis): exercise InfiniBand startup through doxcat --- xCAT-test/bats/genesis_ib_modules.bats | 132 ++++++------------ xCAT-test/bats/helpers/doxcat_sandbox.bash | 141 ++++++++++++++++++++ xCAT-test/bats/helpers/genesis_sandbox.bash | 55 ++++++++ 3 files changed, 236 insertions(+), 92 deletions(-) create mode 100644 xCAT-test/bats/helpers/doxcat_sandbox.bash create mode 100644 xCAT-test/bats/helpers/genesis_sandbox.bash diff --git a/xCAT-test/bats/genesis_ib_modules.bats b/xCAT-test/bats/genesis_ib_modules.bats index 8d2462df7..f4847c59b 100644 --- a/xCAT-test/bats/genesis_ib_modules.bats +++ b/xCAT-test/bats/genesis_ib_modules.bats @@ -1,118 +1,66 @@ #!/usr/bin/env bats -load 'helpers/shell_source' +load 'helpers/doxcat_sandbox' setup() { - GENESIS_SPEC="$(repo_path 'xCAT-genesis-base/xCAT-genesis-base.spec')" - DRACUT_MODULE="$(repo_path 'xCAT-genesis-base/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 + GENESIS_SPEC=$(repo_path xCAT-genesis-base/xCAT-genesis-base.spec) + DRACUT_MODULE=$(repo_path xCAT-genesis-base/dracut_105/el/module-setup.sh) + [ -r "$GENESIS_SPEC" ] + [ -r "$DRACUT_MODULE" ] } 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" - } - + DRACUT_MODULES_ROOT=$1 + local instmods_log=$2 + 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" + command -v rpmspec >/dev/null + run rpmspec -q --buildrequires --target x86_64 \ + --undefine openEuler --undefine rhel --undefine suse_version "$GENESIS_SPEC" + [ "$status" -eq 0 ] + local package + for package in kernel-core kernel-modules kernel-modules-extra; do + printf '%s\n' "$output" | grep -Fx "$package" + done } @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 + local modules_root="$BATS_TEST_TMPDIR/modules" + local instmods_log="$BATS_TEST_TMPDIR/instmods.log" + mkdir -p "$modules_root/5.14.0-test" + printf '%s\n' \ + 'kernel/drivers/infiniband/ulp/ipoib/ib_ipoib.ko.xz:' \ + 'kernel/drivers/net/ethernet/intel/e1000e/e1000e.ko.xz:' \ + >"$modules_root/5.14.0-test/modules.dep" run run_installkernel "$modules_root" "$instmods_log" [ "$status" -eq 0 ] - grep -Fxq 'ib_ipoib' "$instmods_log" - grep -Fxq 'e1000e' "$instmods_log" + 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 loads IPoIB and selects an InfiniBand boot interface" { + setup_doxcat + add_doxcat_link eth0 BROADCAST,MULTICAST,UP ether 02:bb:cc:dd:ee:ff + add_doxcat_link ib0 BROADCAST,MULTICAST,UP infiniband "$ib_address" + run_genesis 0 --setenv BOOTIF 01-aa-bb-cc-dd-ee-ff + [ "$(cat "$fixture/bootnic")" = ib0 ] + grep -Fxq ib_ipoib "$fixture/modules" + assert_doxcat_dhcp } -@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 ] +@test "doxcat prefers an exact Ethernet BOOTIF over an InfiniBand suffix" { + setup_doxcat + add_doxcat_link eth0 BROADCAST,MULTICAST,UP ether aa:bb:cc:dd:ee:ff + add_doxcat_link ib0 BROADCAST,MULTICAST,UP infiniband "$ib_address" + run_genesis 0 --setenv BOOTIF 01-aa-bb-cc-dd-ee-ff + [ "$(cat "$fixture/bootnic")" = eth0 ] + assert_doxcat_dhcp } diff --git a/xCAT-test/bats/helpers/doxcat_sandbox.bash b/xCAT-test/bats/helpers/doxcat_sandbox.bash new file mode 100644 index 000000000..cbab15c09 --- /dev/null +++ b/xCAT-test/bats/helpers/doxcat_sandbox.bash @@ -0,0 +1,141 @@ +#!/usr/bin/env bash + +load 'helpers/genesis_sandbox' + +setup_doxcat() +{ + setup_genesis_sandbox doxcat + ib_address=80:00:00:48:fe:80:00:00:00:00:00:00:00:02:00:bb:cc:dd:ee:ff + local utility fips library + fips=$(repo_path xCAT-genesis-scripts/usr/lib/xcat/fips.sh) + [ -r "$fips" ] + mkdir -p "$fixture"/{etc/ssh,etc/pki/tls,real,links,addrs} + for utility in bash sh cat cp awk grep sed sort tr tail rm wc cut head uname \ + timeout env ls mkdir touch readlink basename sleep; do + cp -L "$(PATH=/usr/bin:/bin type -P "$utility")" "$fixture/bin/$utility" + done + if [ -x /usr/bin/coreutils ]; then + cp -L /usr/bin/coreutils "$fixture/bin/coreutils" + fi + mv "$fixture/bin/sleep" "$fixture/real/sleep" + printf '#!/bin/sh\nexit 0\n' >"$fixture/bin/quiet" + for utility in sleep rpcbind rpc.statd ssh-keygen lldpad chronyd; do + cp "$fixture/bin/quiet" "$fixture/bin/$utility" + done + cp "$fixture/bin/quiet" "$fixture/bin/sshd" + cp "$fixture/bin/quiet" "$fixture/bin/getcert" + cp "$fixture/bin/quiet" "$fixture/bin/getdestiny" + printf '#!/bin/sh\nexit 1\n' >"$fixture/bin/ipmitool" + printf '#!/bin/sh\nprintf "rsyslogd 8.0\\n"\n' >"$fixture/bin/rsyslogd" + cat >"$fixture/bin/openssl" <<'SH' +#!/bin/sh +printf 'fixture-public-key\n' +SH + cat >"$fixture/bin/modprobe" <<'SH' +#!/bin/sh +printf '%s\n' "$*" >>/run/fixture/modules +SH + cat >"$fixture/bin/logger" <<'SH' +#!/bin/sh +printf '%s\n' "$*" >>/run/fixture/messages +SH + cat >"$fixture/bin/ip" <<'SH' +#!/bin/sh +case "$*" in + 'link'|'link show') + cat /run/fixture/link-list + if grep -Fxq ib_ipoib /run/fixture/modules; then + cat /run/fixture/infiniband-links + fi ;; + '-o link show dev '*) + [ ! -e "/run/fixture/links/$5.fail" ] || exit 17 + cat "/run/fixture/links/$5" ;; + '-o addr show dev '*) + [ ! -e "/run/fixture/addrs/$5.fail" ] || exit 19 + cat "/run/fixture/addrs/$5" ;; + '-4 -o a show dev '*) printf '2: %s inet 192.0.2.20/24 scope global\n' "$6" ;; + *) printf 'unexpected ip %s\n' "$*" >&2; exit 97 ;; +esac +SH + cat >"$fixture/bin/ethtool" <<'SH' +#!/bin/sh +printf 'Link detected: yes\n' +SH + cat >"$fixture/bin/dhclient" <<'SH' +#!/bin/sh +printf '%s\n' "$*" >>/run/fixture/dhcp +SH + cat >"$fixture/bin/finish" <<'SH' +#!/bin/sh +printf '%s\n' "$DEVICE" > /run/fixture/bootnic +printf '%s\n' "$NICSTOBRINGUP" > /run/fixture/selected +set -- $NICSTOBRINGUP +expected=$((2 + 2 * $#)) +tries=0 +while [ "$(wc -l "$fixture/bin/nextdestiny" + chmod +x "$fixture/bin/"* + : >"$fixture/etc/rsyslog.conf" + : >"$fixture/link-list" + : >"$fixture/infiniband-links" + : >"$fixture/modules" + : >"$fixture/dhcp" + printf '0\n' >"$fixture/fips_enabled" + sandbox+=(--ro-bind "$fixture/bin" /usr/bin + --tmpfs /proc/sys + --ro-bind "$fixture/fips_enabled" /proc/sys/crypto/fips_enabled + --tmpfs /usr/lib) + if [ ! -L /usr/sbin ]; then + sandbox+=(--ro-bind "$fixture/bin" /usr/sbin) + fi + if [ ! -L /bin ]; then + sandbox+=(--ro-bind "$fixture/bin" /bin) + fi + if [ ! -L /sbin ]; then + sandbox+=(--ro-bind "$fixture/bin" /sbin) + fi + for library in /usr/lib/*; do + [ "${library##*/}" = xcat ] && continue + sandbox+=(--ro-bind "$library" "$library") + done + sandbox+=(--ro-bind "$fips" /usr/lib/xcat/fips.sh + --setenv PATH /usr/bin:/usr/sbin + --setenv destiny runcmd=/run/fixture/bin/finish) +} + +add_doxcat_link() +{ + local nic=$1 flags=$2 kind=${3:-ether} address=${4:-00:11:22:33:44:55} + mkdir -p "$fixture/sys/class/net/$nic" + printf '2: %s: <%s> mtu 1500 state UP\n link/%s %s\n' \ + "$nic" "$flags" "$kind" "$address" >"$fixture/links/$nic" + if [ "$kind" = infiniband ]; then + cat "$fixture/links/$nic" >>"$fixture/infiniband-links" + else + cat "$fixture/links/$nic" >>"$fixture/link-list" + fi + : >"$fixture/addrs/$nic" +} + +assert_doxcat_dhcp() +{ + local nic + printf '%s\n' "$@" | sed '/^$/d' | sort >"$fixture/expected-selection" + tr ' ' '\n' <"$fixture/selected" | sed '/^$/d' | sort >"$fixture/actual-selection" + run diff -u "$fixture/expected-selection" "$fixture/actual-selection" + [ "$status" -eq 0 ] || { printf '%s\n' "$output" >&2; return 1; } + : >"$fixture/expected-dhcp" + for nic in "$(cat "$fixture/bootnic")" "$@"; do + printf '%s\n' "-cf /etc/dhclient.conf -pf /var/run/dhclient.$nic.pid $nic" \ + "-6 -pf /var/run/dhclient6.$nic.pid $nic -lf /var/lib/dhclient/dhclient6.leases" \ + >>"$fixture/expected-dhcp" + done + sort "$fixture/expected-dhcp" >"$fixture/expected-sorted" + sort "$fixture/dhcp" >"$fixture/actual-sorted" + run diff -u "$fixture/expected-sorted" "$fixture/actual-sorted" + [ "$status" -eq 0 ] || { printf '%s\n' "$output" >&2; return 1; } +} diff --git a/xCAT-test/bats/helpers/genesis_sandbox.bash b/xCAT-test/bats/helpers/genesis_sandbox.bash new file mode 100644 index 000000000..2501343d9 --- /dev/null +++ b/xCAT-test/bats/helpers/genesis_sandbox.bash @@ -0,0 +1,55 @@ +#!/usr/bin/env bash + +load 'helpers/shell_source' + +setup_genesis_sandbox() +{ + [ "$(uname -s)" = Linux ] || skip 'Genesis filesystem isolation requires Linux' + local bwrap utility source + bwrap=$(PATH=/usr/bin:/bin type -P bwrap) || { + echo 'Install bubblewrap to run the Genesis script tests' >&2 + return 1 + } + for utility in bash cat cp awk grep sed sort tr tail rm wc cut head uname timeout; do + PATH=/usr/bin:/bin type -P "$utility" >/dev/null || { + echo "Required utility is unavailable: $utility" >&2 + return 1 + } + done + source=$(repo_path "xCAT-genesis-scripts/usr/bin/$1") + [ -r "$source" ] || { + echo "Required Genesis script is unreadable: $source" >&2 + return 1 + } + fixture="${BATS_TEST_TMPDIR:?bats-core 1.4 or newer is required}/fixture" + mkdir -p "$fixture"/{bin,etc/xcat,sys/class/net/lo,tmp,var/lib/dhclient} + : >"$fixture/cmdline" + sandbox=(env -i PATH=/usr/bin:/bin LC_ALL=C "$bwrap" + --unshare-all --die-with-parent --new-session + --ro-bind / / --tmpfs /run --proc /proc --dev /dev + --bind "$fixture" /run/fixture --bind "$fixture/tmp" /tmp + --bind "$fixture/etc" /etc --ro-bind "$fixture/sys" /sys + --bind "$fixture/var" /var + --ro-bind "$fixture/cmdline" /proc/cmdline + --ro-bind "$source" /run/script --chdir /tmp + --setenv PATH /run/fixture/bin:/usr/bin:/bin --setenv LC_ALL C) + if [ -d /etc/alternatives ]; then + sandbox+=(--ro-bind /etc/alternatives /etc/alternatives) + fi + run "${sandbox[@]}" /bin/bash -c 'command -v awk && test -r /run/script' + if [ "$status" -ne 0 ]; then + echo "Cannot isolate Genesis script: $output" >&2 + return 1 + fi +} + +run_genesis() +{ + local expected_status=$1 + shift + run "${sandbox[@]}" "$@" timeout 20 /bin/bash /run/script &2 + return 1 + fi +} From 5408e25a5424e8dc6dac0b591cc5a22d8d496758 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, 9 Oct 2026 18:57:38 -0300 Subject: [PATCH 2/4] test(genesis): run secondary DHCP cases through startup --- xCAT-test/bats/genesis_secondary_dhcp.bats | 196 +++++++++ xCAT-test/unit/genesis_secondary_dhcp.t | 473 --------------------- 2 files changed, 196 insertions(+), 473 deletions(-) create mode 100644 xCAT-test/bats/genesis_secondary_dhcp.bats delete mode 100644 xCAT-test/unit/genesis_secondary_dhcp.t diff --git a/xCAT-test/bats/genesis_secondary_dhcp.bats b/xCAT-test/bats/genesis_secondary_dhcp.bats new file mode 100644 index 000000000..c78dbfc28 --- /dev/null +++ b/xCAT-test/bats/genesis_secondary_dhcp.bats @@ -0,0 +1,196 @@ +#!/usr/bin/env bats + +load 'helpers/doxcat_sandbox' + +setup() +{ + setup_doxcat + add_doxcat_link boot0 BROADCAST,MULTICAST,UP ether aa:bb:cc:dd:ee:ff +} + +physical_nic() +{ + local nic=$1 vendor=${2:-} product=${3:-} + mkdir -p "$fixture/sys/devices/$nic/interface" + ln -s "/sys/devices/$nic/interface" "$fixture/sys/class/net/$nic/device" + [ -z "$vendor" ] || printf '%s\n' "$vendor" >"$fixture/sys/devices/$nic/idVendor" + [ -z "$product" ] || printf '%s\n' "$product" >"$fixture/sys/devices/$nic/idProduct" +} + +enslave_nic() +{ + mkdir -p "$fixture/sys/class/net/bond0" + ln -s /sys/class/net/bond0 "$fixture/sys/class/net/$1/master" +} + +check_secondary() +{ + : >"$fixture/dhcp" + rm -f "$fixture/bootnic" "$fixture/selected" + run_genesis 0 --setenv BOOTIF 01-aa-bb-cc-dd-ee-ff + [ "$(cat "$fixture/bootnic")" = boot0 ] + assert_doxcat_dhcp "$@" +} + +@test "DOWN virtual interface keeps the legacy DHCP path" { + add_doxcat_link veth0 BROADCAST,MULTICAST + check_secondary veth0 +} + +@test "DOWN interface short-circuits every ownership guard" { + add_doxcat_link eno1 BROADCAST,MULTICAST + physical_nic eno1 046b ffb0 + enslave_nic eno1 + printf 'eno1\n' >"$fixture/tmp/tsmhostnic" + printf '2: eno1 inet 192.0.2.21/24 scope global eno1\n' >"$fixture/addrs/eno1" + check_secondary eno1 +} + +@test "operational state does not replace the IFF_UP flag" { + add_doxcat_link eno2 BROADCAST,MULTICAST + check_secondary eno2 +} + +@test "UP physical unaddressed interface gets both DHCP families" { + add_doxcat_link eno3 BROADCAST,MULTICAST,UP,LOWER_UP + physical_nic eno3 + check_secondary eno3 +} + +@test "UP without carrier is still administratively UP" { + add_doxcat_link eno4 BROADCAST,MULTICAST,UP + sed -i '/ eno4: /s/state UP/state DOWN/' "$fixture/links/eno4" "$fixture/link-list" + check_secondary +} + +@test "IFF_UP is recognized as the first flag" { + add_doxcat_link eno5 UP,BROADCAST + check_secondary +} + +@test "IFF_UP is recognized as the only flag" { + add_doxcat_link eno6 UP + check_secondary +} + +@test "IFF_UP is recognized as the last flag" { + add_doxcat_link eno7 BROADCAST,UP + check_secondary +} + +@test "IFF_UP is recognized before the carrier flag" { + add_doxcat_link eno18 BROADCAST,MULTICAST,UP,LOWER_UP + check_secondary +} + +@test "physical IPoIB interface remains eligible" { + add_doxcat_link ib0 BROADCAST,MULTICAST,UP,LOWER_UP infiniband "$ib_address" + physical_nic ib0 + check_secondary ib0 +} + +@test "link-local IPv4 and IPv6 addresses do not claim the interface" { + add_doxcat_link eno8 BROADCAST,MULTICAST,UP + physical_nic eno8 + printf '%s\n' '2: eno8 inet 169.254.10.20/16 scope link eno8' \ + '2: eno8 inet6 fe80::20/64 scope link' '2: eno8 inet6 feb0::20/64 scope link' \ + >"$fixture/addrs/eno8" + check_secondary eno8 +} + +@test "global IPv4 address preserves existing ownership" { + add_doxcat_link eno9 BROADCAST,MULTICAST,UP + physical_nic eno9 + printf '2: eno9 inet 198.51.100.9/24 scope global eno9\n' >"$fixture/addrs/eno9" + check_secondary +} + +@test "global IPv6 address preserves existing ownership" { + add_doxcat_link eno10 BROADCAST,MULTICAST,UP + physical_nic eno10 + printf '2: eno10 inet6 2001:db8::10/64 scope global\n' >"$fixture/addrs/eno10" + check_secondary +} + +@test "global address wins when link-local addresses also exist" { + add_doxcat_link eno11 BROADCAST,MULTICAST,UP + physical_nic eno11 + printf '%s\n' '2: eno11 inet6 fe80::11/64 scope link' \ + '2: eno11 inet 203.0.113.11/24 scope global eno11' >"$fixture/addrs/eno11" + check_secondary +} + +@test "TSM-owned interface is excluded by exact name" { + add_doxcat_link eno12 BROADCAST,MULTICAST,UP + physical_nic eno12 + printf 'eno12\n' >"$fixture/tmp/tsmhostnic" + check_secondary +} + +@test "known management USB devices are excluded before setup markers" { + add_doxcat_link enp18s0f0u1 BROADCAST,MULTICAST,UP + add_doxcat_link enp18s0f0u2 BROADCAST,MULTICAST,UP + physical_nic enp18s0f0u1 046b ffb0 + physical_nic enp18s0f0u2 04b3 4010 + check_secondary +} + +@test "other USB device identities remain eligible" { + local nic vendor product + while read -r nic vendor product; do + add_doxcat_link "$nic" BROADCAST,MULTICAST,UP + physical_nic "$nic" "$vendor" "$product" + done <<'NICS' +enp18s0f0u3 046b ffb1 +enp18s0f0u4 04b4 4010 +enp18s0f0u5 046c ffb0 +enp18s0f0u6 04b3 4011 +NICS + check_secondary enp18s0f0u3 enp18s0f0u4 enp18s0f0u5 enp18s0f0u6 +} + +@test "TSM ownership does not use substring matching" { + add_doxcat_link eno13 BROADCAST,MULTICAST,UP + physical_nic eno13 + printf 'eno130\n' >"$fixture/tmp/tsmhostnic" + check_secondary eno13 +} + +@test "UP virtual interface is excluded" { + add_doxcat_link vnet0 BROADCAST,MULTICAST,UP + check_secondary +} + +@test "UP physical slave is excluded" { + add_doxcat_link eno14 BROADCAST,MULTICAST,UP + physical_nic eno14 + enslave_nic eno14 + check_secondary +} + +@test "link-state query failure is closed" { + add_doxcat_link eno15 BROADCAST,MULTICAST,UP + physical_nic eno15 + touch "$fixture/links/eno15.fail" + check_secondary +} + +@test "address query failure is closed" { + add_doxcat_link eno16 BROADCAST,MULTICAST,UP + physical_nic eno16 + touch "$fixture/addrs/eno16.fail" + check_secondary +} + +@test "caller excludes loopback, boot and USB interfaces on repeated startup" { + local nic + add_doxcat_link lo LOOPBACK,UP + physical_nic lo + physical_nic boot0 + for nic in usb0 eno17; do + add_doxcat_link "$nic" BROADCAST,MULTICAST,UP + physical_nic "$nic" + done + check_secondary eno17 + check_secondary eno17 +} diff --git a/xCAT-test/unit/genesis_secondary_dhcp.t b/xCAT-test/unit/genesis_secondary_dhcp.t deleted file mode 100644 index ab0383959..000000000 --- a/xCAT-test/unit/genesis_secondary_dhcp.t +++ /dev/null @@ -1,473 +0,0 @@ -#!/usr/bin/env perl -use strict; -use warnings; - -use File::Path qw(make_path); -use File::Spec; -use File::Temp qw(tempdir tempfile); -use FindBin; -use Test::More; - -my $repo_root = File::Spec->catdir( $FindBin::Bin, '..', '..' ); -my $doxcat = File::Spec->catfile( - $repo_root, qw(xCAT-genesis-scripts usr bin doxcat) -); - -open( my $doxcat_fh, '<', $doxcat ) or die "Unable to read $doxcat: $!"; -my $source = do { local $/; <$doxcat_fh> }; -close($doxcat_fh); - -my ($helper) = - $source =~ /^(secondary_nic_needs_dhcp\(\)\s*\{.*?^\}\n)/ms; -ok( defined($helper), 'doxcat defines the secondary DHCP eligibility helper' ); - -my ($selection) = - $source =~ /(^\s*NICCANDIDATES=`.*?^\s*export NICSTOBRINGUP\s*$)/ms; -ok( defined($selection), 'doxcat filters candidates before exporting them' ); - -# Keep the test runnable against the old source for a behavioral negative control. -if ( !defined($helper) ) { - $helper = "secondary_nic_needs_dhcp() { return 0; }\n"; -} -if ( !defined($selection) ) { - ($selection) = $source =~ /(^\s*NICSTOBRINGUP=`ip link.*?`\s*$)/m; - $selection = defined($selection) - ? "$selection\nexport NICSTOBRINGUP" - : "NICSTOBRINGUP=\nexport NICSTOBRINGUP"; -} - -my $test_tsm_file = '"$TEST_TSM_FILE"'; -my $test_sys_class_net = '"$TEST_SYS_CLASS_NET"'; -$selection =~ s{/tmp/tsmhostnic}{$test_tsm_file}g; -$selection =~ s{/sys/class/net}{$test_sys_class_net}g; - -my $scratch = tempdir( CLEANUP => 1 ); -my ( $runner_fh, $runner ) = tempfile( DIR => $scratch, UNLINK => 1 ); -print {$runner_fh} $helper; -print {$runner_fh} <<'BASH'; - -ip() { - if [ "$#" -eq 1 ] && [ "$1" = link ]; then - command cat "$TEST_FIXTURE/link-list" - elif [ "$#" -eq 5 ] && [ "$1" = -o ] && [ "$2" = link ] && - [ "$3" = show ] && [ "$4" = dev ]; then - [ ! -e "$TEST_FIXTURE/links/$5.fail" ] || return 17 - [ -f "$TEST_FIXTURE/links/$5" ] || return 18 - command cat "$TEST_FIXTURE/links/$5" - elif [ "$#" -eq 5 ] && [ "$1" = -o ] && [ "$2" = addr ] && - [ "$3" = show ] && [ "$4" = dev ]; then - [ ! -e "$TEST_FIXTURE/addrs/$5.fail" ] || return 19 - [ -f "$TEST_FIXTURE/addrs/$5" ] || return 20 - command cat "$TEST_FIXTURE/addrs/$5" - else - return 64 - fi -} - -bootnic=$TEST_BOOTNIC -BASH -print {$runner_fh} "$selection\n"; -print {$runner_fh} <<'BASH'; -printf '%s\n' "$NICSTOBRINGUP" -BASH -close($runner_fh); - -sub write_file { - my ( $path, $contents ) = @_; - open( my $fh, '>', $path ) or die "Unable to write $path: $!"; - print {$fh} $contents; - close($fh); -} - -sub link_output { - my ( $interface, $index ) = @_; - my $flags = $interface->{flags}; - my $state = exists( $interface->{state} ) - ? $interface->{state} - : ( $flags =~ /(?:^|,)UP(?:,|$)/ ? 'UP' : 'DOWN' ); - my $link_type = $interface->{link_type} || 'ether'; - - return sprintf( - "%d: %s: <%s> mtu 1500 qdisc noop state %s mode DEFAULT group default qlen 1000\n" . - " link/%s 02:00:00:00:00:%02x brd ff:ff:ff:ff:ff:ff\n", - $index, $interface->{nic}, $flags, $state, $link_type, $index - ); -} - -sub run_case { - my ($case) = @_; - my $root = tempdir( DIR => $scratch, CLEANUP => 1 ); - my $links = File::Spec->catdir( $root, 'links' ); - my $addrs = File::Spec->catdir( $root, 'addrs' ); - my $sys_class_net = File::Spec->catdir( $root, qw(sys class net) ); - my $devices = File::Spec->catdir( $root, 'devices' ); - my $masters = File::Spec->catdir( $root, 'masters' ); - make_path( $links, $addrs, $sys_class_net, $devices, $masters ); - - my $link_list = ''; - my $index = 1; - foreach my $interface ( @{ $case->{interfaces} } ) { - my $nic = $interface->{nic}; - my $output = link_output( $interface, $index++ ); - $link_list .= $output; - - if ( $interface->{link_fail} ) { - write_file( File::Spec->catfile( $links, "$nic.fail" ), '' ); - } else { - write_file( File::Spec->catfile( $links, $nic ), $output ); - } - - if ( $interface->{addr_fail} ) { - write_file( File::Spec->catfile( $addrs, "$nic.fail" ), '' ); - } elsif ( exists( $interface->{addresses} ) ) { - write_file( - File::Spec->catfile( $addrs, $nic ), - $interface->{addresses} - ); - } - - my $sys_nic = File::Spec->catdir( $sys_class_net, $nic ); - make_path($sys_nic); - if ( $interface->{physical} ) { - my $device_root = File::Spec->catdir( $devices, $nic ); - my $device = File::Spec->catdir( $device_root, 'interface' ); - make_path($device); - if ( exists( $interface->{usb_vendor} ) ) { - write_file( - File::Spec->catfile( $device_root, 'idVendor' ), - "$interface->{usb_vendor}\n" - ); - } - if ( exists( $interface->{usb_product} ) ) { - write_file( - File::Spec->catfile( $device_root, 'idProduct' ), - "$interface->{usb_product}\n" - ); - } - symlink( $device, File::Spec->catfile( $sys_nic, 'device' ) ) - or die "Unable to link physical device for $nic: $!"; - } - if ( $interface->{master} ) { - my $master = File::Spec->catdir( $masters, $nic ); - make_path($master); - symlink( $master, File::Spec->catfile( $sys_nic, 'master' ) ) - or die "Unable to link master for $nic: $!"; - } - } - write_file( File::Spec->catfile( $root, 'link-list' ), $link_list ); - - my $tsm_file = File::Spec->catfile( $root, 'tsmhostnic' ); - write_file( $tsm_file, $case->{tsmnic} ) if defined( $case->{tsmnic} ); - - local %ENV = ( - %ENV, - TEST_BOOTNIC => $case->{bootnic} || 'boot0', - TEST_FIXTURE => $root, - TEST_SYS_CLASS_NET => $sys_class_net, - TEST_TSM_FILE => $tsm_file, - ); - - open( my $result_fh, '-|', 'bash', '--noprofile', '--norc', $runner ) - or die "Unable to run secondary DHCP harness: $!"; - my $output = do { local $/; <$result_fh> }; - close($result_fh); - my $status = $? >> 8; - my @actual = grep { length($_) } split /\s+/, $output; - - is( $status, 0, "$case->{name}: selector succeeds" ); - is_deeply( \@actual, $case->{expected}, "$case->{name}: selected NICs" ); -} - -my @cases = ( - { - name => 'DOWN virtual interface keeps the legacy path', - interfaces => [ - { nic => 'veth0', flags => 'BROADCAST,MULTICAST', state => 'DOWN' }, - ], - expected => ['veth0'], - }, - { - name => 'DOWN interface short-circuits every ownership guard', - tsmnic => 'eno1', - interfaces => [ - { - nic => 'eno1', - flags => 'BROADCAST,MULTICAST', - physical => 1, - master => 1, - usb_vendor => '046b', - usb_product => 'ffb0', - addresses => "2: eno1 inet 192.0.2.21/24 scope global eno1\n", - }, - ], - expected => ['eno1'], - }, - { - name => 'operational state does not replace the IFF_UP flag', - interfaces => [ - { nic => 'eno2', flags => 'BROADCAST,MULTICAST', state => 'UP' }, - ], - expected => ['eno2'], - }, - { - name => 'UP physical unaddressed interface is selected', - interfaces => [ - { - nic => 'eno3', - flags => 'BROADCAST,MULTICAST,UP,LOWER_UP', - physical => 1, - addresses => '', - }, - ], - expected => ['eno3'], - }, - { - name => 'UP without carrier is still administratively UP', - interfaces => [ - { - nic => 'eno4', - flags => 'BROADCAST,MULTICAST,UP', - state => 'DOWN', - physical => 1, - addresses => '', - }, - ], - expected => ['eno4'], - }, - { - name => 'IFF_UP is recognized as the first flag', - interfaces => [ - { nic => 'eno5', flags => 'UP,BROADCAST', physical => 1, addresses => '' }, - ], - expected => ['eno5'], - }, - { - name => 'IFF_UP is recognized as the only flag', - interfaces => [ - { nic => 'eno6', flags => 'UP', physical => 1, addresses => '' }, - ], - expected => ['eno6'], - }, - { - name => 'IFF_UP is recognized as the last flag', - interfaces => [ - { nic => 'eno7', flags => 'BROADCAST,UP', physical => 1, addresses => '' }, - ], - expected => ['eno7'], - }, - { - name => 'physical IPoIB interface remains eligible', - interfaces => [ - { - nic => 'ib0', - flags => 'BROADCAST,MULTICAST,UP,LOWER_UP', - link_type => 'infiniband', - physical => 1, - addresses => '', - }, - ], - expected => ['ib0'], - }, - { - name => 'link-local IPv4 and IPv6 addresses do not claim the interface', - interfaces => [ - { - nic => 'eno8', - flags => 'BROADCAST,MULTICAST,UP', - physical => 1, - addresses => - "2: eno8 inet 169.254.10.20/16 scope link eno8\n" . - "2: eno8 inet6 fe80::20/64 scope link\n" . - "2: eno8 inet6 feb0::20/64 scope link\n", - }, - ], - expected => ['eno8'], - }, - { - name => 'global IPv4 address preserves existing ownership', - interfaces => [ - { - nic => 'eno9', - flags => 'BROADCAST,MULTICAST,UP', - physical => 1, - addresses => "2: eno9 inet 198.51.100.9/24 scope global eno9\n", - }, - ], - expected => [], - }, - { - name => 'global IPv6 address preserves existing ownership', - interfaces => [ - { - nic => 'eno10', - flags => 'BROADCAST,MULTICAST,UP', - physical => 1, - addresses => "2: eno10 inet6 2001:db8::10/64 scope global\n", - }, - ], - expected => [], - }, - { - name => 'global address wins when link-local addresses also exist', - interfaces => [ - { - nic => 'eno11', - flags => 'BROADCAST,MULTICAST,UP', - physical => 1, - addresses => - "2: eno11 inet6 fe80::11/64 scope link\n" . - "2: eno11 inet 203.0.113.11/24 scope global eno11\n", - }, - ], - expected => [], - }, - { - name => 'TSM-owned interface is excluded by exact name', - tsmnic => 'eno12', - interfaces => [ - { - nic => 'eno12', - flags => 'BROADCAST,MULTICAST,UP', - physical => 1, - addresses => '', - }, - ], - expected => [], - }, - { - name => 'known management USB devices are excluded before setup markers', - interfaces => [ - { - nic => 'enp18s0f0u1', - flags => 'BROADCAST,MULTICAST,UP', - physical => 1, - usb_vendor => '046b', - usb_product => 'ffb0', - addresses => '', - }, - { - nic => 'enp18s0f0u2', - flags => 'BROADCAST,MULTICAST,UP', - physical => 1, - usb_vendor => '04b3', - usb_product => '4010', - addresses => '', - }, - ], - expected => [], - }, - { - name => 'other USB device identities remain eligible', - interfaces => [ - { - nic => 'enp18s0f0u3', - flags => 'BROADCAST,MULTICAST,UP', - physical => 1, - usb_vendor => '046b', - usb_product => 'ffb1', - addresses => '', - }, - { - nic => 'enp18s0f0u4', - flags => 'BROADCAST,MULTICAST,UP', - physical => 1, - usb_vendor => '04b4', - usb_product => '4010', - addresses => '', - }, - { - nic => 'enp18s0f0u5', - flags => 'BROADCAST,MULTICAST,UP', - physical => 1, - usb_vendor => '046c', - usb_product => 'ffb0', - addresses => '', - }, - { - nic => 'enp18s0f0u6', - flags => 'BROADCAST,MULTICAST,UP', - physical => 1, - usb_vendor => '04b3', - usb_product => '4011', - addresses => '', - }, - ], - expected => [ - 'enp18s0f0u3', 'enp18s0f0u4', - 'enp18s0f0u5', 'enp18s0f0u6' - ], - }, - { - name => 'TSM ownership does not use substring matching', - tsmnic => 'eno130', - interfaces => [ - { - nic => 'eno13', - flags => 'BROADCAST,MULTICAST,UP', - physical => 1, - addresses => '', - }, - ], - expected => ['eno13'], - }, - { - name => 'UP virtual interface is excluded', - interfaces => [ - { nic => 'vnet0', flags => 'BROADCAST,MULTICAST,UP', addresses => '' }, - ], - expected => [], - }, - { - name => 'UP physical slave is excluded', - interfaces => [ - { - nic => 'eno14', - flags => 'BROADCAST,MULTICAST,UP', - physical => 1, - master => 1, - addresses => '', - }, - ], - expected => [], - }, - { - name => 'link-state query failure is closed', - interfaces => [ - { - nic => 'eno15', - flags => 'BROADCAST,MULTICAST,UP', - physical => 1, - link_fail => 1, - addresses => '', - }, - ], - expected => [], - }, - { - name => 'address query failure is closed', - interfaces => [ - { - nic => 'eno16', - flags => 'BROADCAST,MULTICAST,UP', - physical => 1, - addr_fail => 1, - }, - ], - expected => [], - }, - { - name => 'caller excludes loopback, boot, and USB interfaces', - bootnic => 'boot0', - interfaces => [ - { nic => 'lo', flags => 'LOOPBACK,UP,LOWER_UP', physical => 1, addresses => '' }, - { nic => 'boot0', flags => 'BROADCAST,MULTICAST,UP', physical => 1, addresses => '' }, - { nic => 'usb0', flags => 'BROADCAST,MULTICAST,UP', physical => 1, addresses => '' }, - { nic => 'eno17', flags => 'BROADCAST,MULTICAST,UP', physical => 1, addresses => '' }, - ], - expected => ['eno17'], - }, -); - -run_case($_) foreach @cases; - -done_testing(); From 54ce837c6f907e65962577ce15f704dce5d4435f 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, 9 Oct 2026 19:08:58 -0300 Subject: [PATCH 3/4] test(genesis): evaluate OpenEmbedded build contracts --- .github/workflows/genesis_openembedded.yml | 12 +- xCAT-test/native/genesis_console.py | 42 ++ xCAT-test/native/genesis_metadata.py | 363 ++++++++++++ xCAT-test/unit/genesis_openembedded.t | 606 +-------------------- 4 files changed, 432 insertions(+), 591 deletions(-) create mode 100644 xCAT-test/native/genesis_console.py create mode 100644 xCAT-test/native/genesis_metadata.py diff --git a/.github/workflows/genesis_openembedded.yml b/.github/workflows/genesis_openembedded.yml index 4d38f7131..5ffed2b38 100644 --- a/.github/workflows/genesis_openembedded.yml +++ b/.github/workflows/genesis_openembedded.yml @@ -4,13 +4,15 @@ on: paths: - 'xCAT-genesis-base/oe/**' - 'xCAT-genesis-scripts/**' + - 'xCAT-test/native/genesis_metadata.py' + - 'xCAT-test/native/genesis_console.py' - '.github/workflows/genesis_openembedded.yml' workflow_dispatch: jobs: genesis_openembedded: name: OpenEmbedded metadata runs-on: ubuntu-24.04 - timeout-minutes: 30 + timeout-minutes: 45 steps: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 @@ -20,14 +22,18 @@ jobs: cache-dependency-path: xCAT-genesis-base/oe/requirements.txt - name: Install OpenEmbedded host dependencies run: >- + sudo env DEBIAN_FRONTEND=noninteractive apt-get update && sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends --no-install-suggests build-essential chrpath cpio diffstat file gawk git locales lz4 openssl socat texinfo unzip wget xz-utils zstd + meson ninja-build pkg-config libnewt-dev libsystemd-dev - name: Install KAS run: python -m pip install -r xCAT-genesis-base/oe/requirements.txt - name: Enable BitBake user namespaces run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 + - name: Build and run the Genesis console + run: python3 xCAT-test/native/genesis_console.py - name: Validate every architecture configuration shell: bash env: @@ -37,8 +43,10 @@ jobs: for architecture in x86 x86_64 ppc64 ppc64le armv7hf aarch64 riscv64 s390x; do export KAS_BUILD_DIR="${RUNNER_TEMP}/kas-build-${architecture}" configuration="xCAT-genesis-base/oe/kas/${architecture}.yml" - kas dump "${configuration}" >/dev/null + kas dump --format json "${configuration}" >"${KAS_BUILD_DIR}.json" kas shell "${configuration}" -c 'bitbake -p' + kas shell "${configuration}" -c \ + "GENESIS_ARCHITECTURE='${architecture}' GENESIS_KAS_CONFIG='${KAS_BUILD_DIR}.json' python3 '${GITHUB_WORKSPACE}/xCAT-test/native/genesis_metadata.py'" done - name: Validate the image task graph shell: bash diff --git a/xCAT-test/native/genesis_console.py b/xCAT-test/native/genesis_console.py new file mode 100644 index 000000000..fa6769c47 --- /dev/null +++ b/xCAT-test/native/genesis_console.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +"""Build the Genesis console with its installed Meson definition.""" + +import json +import os +from pathlib import Path +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[2] +SOURCE = ROOT / "xCAT-genesis-base/oe/meta-xcat-genesis/recipes-core/xcat-genesis-console/files/xcat-genesis-console" + + +class ConsoleBuild(unittest.TestCase): + def test_build_install_and_snapshot(self): + with tempfile.TemporaryDirectory(prefix="genesis-console-") as temporary: + root = Path(temporary) + build = root / "build" + stage = root / "stage" + subprocess.run(["meson", "setup", str(build), str(SOURCE), "--prefix=/usr"], check=True) + options = json.loads(subprocess.check_output(["meson", "introspect", "--buildoptions", str(build)])) + options = {option["name"]: option["value"] for option in options} + self.assertEqual(options["c_std"], "c17") + self.assertIs(options["werror"], True) + subprocess.run(["meson", "compile", "-C", str(build)], check=True) + subprocess.run(["meson", "install", "-C", str(build), "--destdir", str(stage)], check=True) + binary = stage / "usr/sbin/xcat-genesis-console" + self.assertTrue(os.access(binary, os.X_OK)) + environment = {"PATH": os.environ["PATH"], "LC_ALL": "C"} + for name in ("CMDLINE_FILE", "UPTIME_FILE", "OS_RELEASE", "STATUS_DIR", "STATE_FILE", + "DESTINY_FILE", "RESPONSE_FILE", "SYS_ROOT", "PROC_ROOT", "EXTENSION_DIR", "PROVIDER_DIR"): + environment["XCAT_" + name] = str(root / name.lower()) + Path(environment["XCAT_RESPONSE_FILE"]).write_text("XCAT_NODE_NAME=console-test-node\n") + result = subprocess.run([str(binary), "--once"], env=environment, text=True, + capture_output=True, timeout=10, check=True) + self.assertIn("xCAT Genesis", result.stdout) + self.assertIn("node: console-test-node", result.stdout) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/xCAT-test/native/genesis_metadata.py b/xCAT-test/native/genesis_metadata.py new file mode 100644 index 000000000..39898c737 --- /dev/null +++ b/xCAT-test/native/genesis_metadata.py @@ -0,0 +1,363 @@ +#!/usr/bin/env python3 +"""Run in a configured kas shell; exercise BitBake's evaluated recipes and tasks.""" + +import configparser +import hashlib +import json +import os +from pathlib import Path +import shutil +import stat +import sys +import tempfile +import unittest + +BITBAKE = shutil.which("bitbake") +if not BITBAKE: + raise SystemExit("Run this test inside the Genesis kas shell") +sys.path.insert(0, str(Path(BITBAKE).resolve().parents[1] / "lib")) +import bb.build +import bb.fetch2 +import bb.process +import bb.providers +import bb.tinfoil + +ROOT = Path(__file__).resolve().parents[2] +LAYER = ROOT / "xCAT-genesis-base/oe/meta-xcat-genesis" +MACHINES = { + "x86": ("i686", "ttyS0"), + "x86_64": ("x86-64", "ttyS0"), + "ppc64": ("powerpc64", "hvc0"), + "ppc64le": ("ppc64p8le", "hvc0"), + "armv7hf": ("armv7ahf", "ttyAMA0"), + "aarch64": ("aarch64", "ttyAMA0"), + "riscv64": ("riscv64", "ttyS0"), + "s390x": ("s390x", "ttysclp0"), +} + + +class GenesisMetadata(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.tinfoil = bb.tinfoil.Tinfoil() + cls.addClassCleanup(cls.tinfoil.shutdown) + cls.tinfoil.prepare(quiet=2) + sys.path.insert(0, str(Path(cls.tinfoil.config_data.getVar("COREBASE")) / "meta/lib")) + from oe import utils + cls.oe_utils = utils + cls.recipes = {} + cls.image = cls.recipe("xcat-genesis-image") + cls.arch = cls.image.getVar("XCAT_GENESIS_ARCHITECTURE") + if cls.arch not in MACHINES: + raise AssertionError("Unknown Genesis architecture: " + str(cls.arch)) + if cls.arch != os.environ["GENESIS_ARCHITECTURE"]: + raise AssertionError("Configured machine does not match the requested architecture") + + @classmethod + def recipe(cls, name): + if name not in cls.recipes: + data = cls.tinfoil.parse_recipe(name) + if name.startswith("xcat-genesis") or name == "packagegroup-xcat-genesis-hardware": + Path(data.getVar("FILE")).resolve().relative_to(LAYER) + cls.recipes[name] = data + return cls.recipes[name].createCopy() + + def words(self, data, variable): + return set((data.getVar(variable) or "").split()) + + def dependencies(self, recipe): + data = self.recipe(recipe) + return self.words(data, "RDEPENDS:" + data.getVar("PN")) + + def task_data(self, recipe): + data = self.recipe(recipe) + temporary = tempfile.TemporaryDirectory(prefix="genesis-metadata-") + self.addCleanup(temporary.cleanup) + work = Path(temporary.name) + for variable, directory in (("WORKDIR", work), ("UNPACKDIR", work / "sources"), + ("D", work / "image"), ("T", work / "temp")): + directory.mkdir(exist_ok=True) + data.setVar(variable, str(directory)) + # These local-file tasks need host tools, not an unbuilt target sysroot. + data.setVar("PATH", os.environ["PATH"]) + return data + + def install(self, recipe): + data = self.task_data(recipe) + urls = data.getVar("SRC_URI").split() + self.assertTrue(urls) + self.assertTrue(all(url.startswith("file://") for url in urls)) + fetcher = bb.fetch2.Fetch(urls, data) + fetcher.download() + fetcher.unpack(data.getVar("UNPACKDIR")) + bb.build.exec_func("do_install", data, dirs=[data.getVar("WORKDIR")]) + return data, Path(data.getVar("D")) + + def installed(self, data, root, variable, suffix, mode): + file = root / data.getVar(variable).lstrip("/") / suffix + self.assertTrue(file.is_file(), str(file)) + self.assertEqual(stat.S_IMODE(file.stat().st_mode), mode, str(file)) + return file + + def unit(self, file): + parser = configparser.ConfigParser(interpolation=None, strict=False) + parser.optionxform = str + parser.read(file) + return parser + + def test_release_configuration(self): + config = json.loads(Path(os.environ["GENESIS_KAS_CONFIG"]).read_text()) + self.assertEqual(config["machine"], self.image.getVar("MACHINE")) + self.assertEqual(config["distro"], "xcat-genesis") + pins = { + "bitbake": "acfe02fa38b5da9e6a36c6cedcf91d4fcbefbfbd", + "openembedded-core": "5d1aa5c806c061a2994f4decb59016610f093213", + "meta-openembedded": "af8b6d6b2f0b11595b0a0d5b82efa3129d52a628", + } + for name, revision in pins.items(): + self.assertEqual(config["repos"][name]["commit"], revision) + for name in ("bitbake", "openembedded-core"): + repo = config["repos"][name] + self.assertEqual(repo["tag"], "yocto-6.0.2") + self.assertIs(repo["signed"], True) + self.assertEqual(repo["allowed_signers"], ["yocto-release"]) + self.assertEqual(config["signers"]["yocto-release"], { + "fingerprint": "2AFB13F28FBBB0D1B9DAF63087EB3D32FB631AD9", + "repo": "xcat-core", "path": "xCAT-genesis-base/oe/keys/yocto-release.asc", + }) + self.assertNotIn("gpg_keyserver", config) + self.assertIn("xCAT-genesis-base/oe/meta-xcat-genesis", config["repos"]["xcat-core"]["layers"]) + self.assertLessEqual({"archiver", "create-spdx", "vex"}, self.words(self.image, "INHERIT")) + for flag, value in (("src", "original"), ("diff", "1"), ("recipe", "1")): + self.assertEqual(self.image.getVarFlag("ARCHIVER_MODE", flag), value) + + def test_machine_and_distro(self): + data = self.image + tune, terminal = MACHINES[self.arch] + for variable, value in { + "DEFAULTTUNE": tune, "XCAT_GENESIS_CONSOLE_TTY": terminal, + "DISTRO_CODENAME": "cheetah", "LINUX_VERSION_EXTENSION": "-xCAT-genesis", + "TCLIBC": "glibc", "INIT_MANAGER": "systemd", "NO_RECOMMENDATIONS": "1", + "QB_DEFAULT_FSTYPE": "ext4", + }.items(): + self.assertEqual(data.getVar(variable), value, variable) + self.assertEqual(self.words(data, "IMAGE_FSTYPES"), {"cpio.gz", "ext4"}) + self.assertEqual(self.words(data, "DISTRO_FEATURES"), + {"acl", "ipv4", "ipv6", "largefile", "pci", "seccomp", "systemd", "usrmerge"}) + self.assertEqual("pci" in self.words(data, "MACHINE_FEATURES"), self.arch != "s390x") + if self.arch == "x86_64": + self.assertEqual(self.words(data, "MACHINE_FEATURES"), {"pci", "qemu-usermode", "rtc"}) + self.assertNotIn("xcat.debug-shell", self.words(data, "QB_KERNEL_CMDLINE_APPEND")) + if self.arch == "ppc64le": + self.assertEqual(data.getVar("KERNEL_IMAGETYPE"), "vmlinux") + if self.arch == "x86": + self.assertEqual(data.getVar("QB_SYSTEM_NAME"), "qemu-system-i386") + if self.arch == "armv7hf": + self.assertEqual(data.getVar("ARM_INSTRUCTION_SET"), "arm") + self.assertNotIn("neon", self.words(data, "TUNE_FEATURES")) + if self.arch == "riscv64": + self.assertIn("opensbi", self.words(data, "EXTRA_IMAGEDEPENDS")) + self.assertEqual(data.getVar("QB_DEFAULT_BIOS"), "fw_jump.elf") + qemu = { + "ppc64": ("-machine pseries", "-cpu POWER8"), + "ppc64le": ("-machine pseries", "-cpu POWER8"), + "armv7hf": ("-machine virt,highmem=off", "-cpu cortex-a15"), + "aarch64": ("-machine virt", "-cpu cortex-a57"), + "riscv64": ("-machine virt", "-cpu rva23s64,pmp=true"), + } + if self.arch in qemu: + self.assertEqual((data.getVar("QB_MACHINE"), data.getVar("QB_CPU")), qemu[self.arch]) + + def test_image_and_hardware_packages(self): + image = self.words(self.image, "IMAGE_INSTALL") + self.assertLessEqual({"xcat-genesis-network", "xcat-genesis-protocol", "xcat-genesis-extensions", + "xcat-genesis-console", "packagegroup-xcat-genesis-hardware"}, image) + self.assertFalse(image & {"dhclient", "dhcpcd", "busybox-udhcpc", "systemd-networkd"}) + hardware = self.dependencies("packagegroup-xcat-genesis-hardware") + self.assertLessEqual(set("pciutils usbutils hwloc ethtool rdma-core iproute2-rdma lldpd ipmitool curl jq " + "nvme-cli smartmontools sg3-utils lsscsi mdadm hdparm parted lvm2 memtester " + "stress-ng edac-utils kernel-modules mstflint xcat-genesis-hardware-control".split()), hardware) + scoped = {"x86_64": {"dmidecode", "lshw", "mcelog"}, "x86": {"dmidecode", "lshw"}, + "armv7hf": {"dmidecode", "lshw"}, "aarch64": {"dmidecode", "lshw"}, + "ppc64": {"dmidecode", "xcat-genesis-hardware-control-iprutils"}, + "ppc64le": {"dmidecode", "xcat-genesis-hardware-control-iprutils"}, + "riscv64": {"lshw"}, "s390x": set()} + self.assertEqual(hardware & {"dmidecode", "lshw", "mcelog", "xcat-genesis-hardware-control-iprutils"}, + scoped[self.arch]) + self.assertFalse((image | hardware) & {"storcli", "perccli", "ssacli", "arcconf", "nvidia-smi", "rocm-smi"}) + + def test_runtime_dependencies(self): + for recipe, expected in { + "xcat-genesis-network": "networkmanager-daemon networkmanager-nmcli", + "xcat-genesis-protocol": "bash openssl-bin", + "xcat-genesis-extensions": "bash coreutils jq openssl-bin systemd xcat-genesis-init", + "xcat-genesis-hardware-control": "bash coreutils jq mstflint nvme-cli util-linux-flock", + "xcat-genesis-console": "libnewt libsystemd ncurses-terminfo-base xcat-genesis-init", + "mstflint": "python3-modules", + }.items(): + self.assertEqual(self.dependencies(recipe), set(expected.split()), recipe) + self.assertLessEqual({"coreutils", "util-linux-logger"}, self.dependencies("xcat-genesis-init")) + console = self.recipe("xcat-genesis-console") + self.assertLessEqual({"libnewt", "systemd"}, self.words(console, "DEPENDS")) + self.assertIn("sysext", self.words(self.recipe("systemd"), "PACKAGECONFIG")) + self.assertIn("rdma", self.words(self.recipe("iproute2"), "PACKAGECONFIG")) + network = self.recipe("networkmanager") + self.assertEqual(self.words(network, "PACKAGECONFIG"), + set("crypto-null libedit man-resolv-conf nmcli systemd".split())) + self.assertEqual(network.getVar("NETWORKMANAGER_DHCP_DEFAULT"), "internal") + self.assertEqual(network.getVar("NETWORKMANAGER_DNS_RC_MANAGER_DEFAULT"), "symlink") + self.assertIn("-Dtests=no", self.words(network, "EXTRA_OEMESON")) + + def test_hardware_recipe_policy(self): + data = self.recipe("mstflint") + self.assertEqual(data.getVar("LICENSE"), "Linux-OpenIB & MIT & BSD-2-Clause") + self.assertEqual(data.getVar("SRCREV"), "b9d9e844a14ae1c85cb90c76ccd4a56a0eccd599") + self.assertEqual(self.words(data, "PACKAGECONFIG"), set("adb cables dc inband openssl".split())) + self.assertFalse(self.words(data, "EXTRA_OECONF") & {"--enable-fw-mgr", "--enable-nvml"}) + if self.arch in {"ppc64", "ppc64le"}: + data = self.recipe("iprutils") + self.assertEqual(data.getVar("COMPATIBLE_HOST"), "powerpc64.*-linux") + self.assertEqual(data.getVar("LICENSE"), "CPL-1.0") + self.assertEqual(data.getVar("SRCREV"), "9961e538736a6e81f1072a926c3ad91b8513c8c1") + self.assertLessEqual({"--without-systemd", "--without-initscripts"}, self.words(data, "EXTRA_OECONF")) + control = self.recipe("xcat-genesis-hardware-control") + self.assertIn("xcat-genesis-hardware-control-iprutils", self.words(control, "PACKAGES")) + self.assertEqual(self.words(control, "RDEPENDS:xcat-genesis-hardware-control-iprutils"), + {"xcat-genesis-hardware-control", "bash", "iprutils"}) + else: + with self.assertRaisesRegex(bb.providers.NoProvider, "incompatible with host"): + self.tinfoil.get_recipe_file("iprutils") + + def test_kernel_fragment_selection(self): + data = self.recipe("linux-yocto") + fragments = { + "x86": {"x86-common", "x86"}, "x86_64": {"x86-common", "x86-64"}, + "ppc64": {"powerpc64", "ppc64"}, "ppc64le": {"powerpc64"}, + }.get(self.arch, {self.arch}) + urls = {url.split(";")[0] for url in data.getVar("SRC_URI").split()} + self.assertLessEqual({"file://xcat-genesis-" + name + ".cfg" for name in fragments}, urls) + if self.arch not in {"x86", "x86_64"}: + self.assertEqual(data.getVar("SRCREV_machine"), "b1ba5428513b52c2bd6acfd3ad0a910f699bc395") + self.assertEqual(data.getVar("KERNEL_MODULE_PACKAGE_SUFFIX"), "-" + data.getVar("KERNEL_VERSION_PKG_NAME")) + + def test_installed_protocol(self): + data, root = self.install("xcat-genesis-protocol") + for name in ("getdestiny", "nextdestiny"): + file = self.installed(data, root, "bindir", name, 0o755) + self.assertEqual(file.read_bytes(), (ROOT / "xCAT-genesis-scripts/usr/bin" / name).read_bytes()) + self.assertFalse(list(root.rglob("doxcat"))) + + def test_installed_network(self): + data, root = self.install("xcat-genesis-network") + file = self.installed(data, root, "sysconfdir", "NetworkManager/conf.d/10-xcat-genesis.conf", 0o644) + config = self.unit(file) + self.assertEqual(config["connection"]["ipv4.dhcp-timeout"], "20") + self.assertEqual(config["connection"]["ipv6.dhcp-timeout"], "20") + self.assertEqual(config["connection"]["ipv6.dhcp-duid"], "ll") + + def test_installed_hardware_providers(self): + data, root = self.install("xcat-genesis-hardware-control") + providers = {"mstflint", "nvme"} + if self.arch in {"ppc64", "ppc64le"}: + providers.add("iprutils") + directory = root / data.getVar("datadir").lstrip("/") / "xcat/genesis/providers" + self.assertEqual({file.stem for file in directory.glob("*.json")}, providers) + for name in providers: + file = self.installed(data, root, "datadir", "xcat/genesis/providers/" + name + ".json", 0o644) + manifest = json.loads(file.read_text()) + self.assertEqual(manifest["name"], name) + self.assertTrue(manifest["capabilities"]) + for capability in manifest["capabilities"]: + self.assertIs(capability["destructive"], False) + self.installed(data, root, "libexecdir", "xcat/genesis/providers/" + name, 0o755) + + def test_installed_init(self): + data, root = self.install("xcat-genesis-init") + self.installed(data, root, "libexecdir", "xcat/genesis-functions", 0o644) + for name in ("genesis-status", "genesis-maintenance-shell"): + self.installed(data, root, "libexecdir", "xcat/" + name, 0o755) + self.assertFalse(list(root.rglob("*debug-shell*"))) + network = self.unit(self.installed(data, root, "systemd_system_unitdir", "xcat-genesis-network-state.service", 0o644)) + self.assertEqual(network["Unit"]["After"], "NetworkManager.service") + self.assertEqual(network["Unit"]["Before"], "xcat-genesis-network-ready.target") + register = self.unit(self.installed(data, root, "systemd_system_unitdir", "xcat-genesis-register.service", 0o644)) + for option in ("After", "Requires"): + self.assertEqual(set(register["Unit"][option].split()), + {"xcat-genesis-network-ready.target", "xcat-genesis-extensions.service"}) + preset = root / data.getVar("systemd_unitdir").lstrip("/") / "system-preset/00-xcat-genesis.preset" + self.assertEqual(preset.read_text(), "disable getty@.service\n") + + def test_installed_extension_service(self): + data, root = self.install("xcat-genesis-extensions") + service = self.unit(self.installed(data, root, "systemd_system_unitdir", "xcat-genesis-extensions.service", 0o644)) + self.assertEqual(service["Unit"]["ConditionDirectoryNotEmpty"], "/var/lib/xcat/genesis/extensions") + self.assertEqual(service["Unit"]["Before"], "xcat-genesis-register.service") + + def test_extension_manifest(self): + data = self.task_data("xcat-genesis-extension-smoke") + self.assertEqual(self.words(data, "IMAGE_FSTYPES"), {"squashfs-zst"}) + self.assertEqual(data.getVar("XCAT_GENESIS_EXTENSION_ARCHITECTURE"), self.arch) + deploy = Path(data.getVar("WORKDIR")) / "deploy" + deploy.mkdir() + data.setVar("IMGDEPLOYDIR", str(deploy)) + data.setVar("IMAGE_LINK_NAME", "test-extension") + payload = b"extension image fixture\n" + image = deploy / "test-extension.squashfs-zst" + image.write_bytes(payload) + expected = { + "architecture": self.arch, "capabilities": ["diagnostic.smoke"], + "genesis_release": data.getVar("DISTRO_VERSION"), "kernel_modules": False, + "kernel_release": None, "key_id": "xcat-release", "license_class": "open", + "name": "xcat-smoke", "pci_ids": [], "schema": 1, + "sha256": hashlib.sha256(payload).hexdigest(), "version": "1.0", + } + for unused in range(2): + self.oe_utils.execute_pre_post_process(data, data.getVar("IMAGE_POSTPROCESS_COMMAND")) + self.assertEqual(json.loads((deploy / "test-extension.manifest.json").read_text()), expected) + data.setVar("XCAT_GENESIS_EXTENSION_KERNEL_MODULES", "true") + data.setVar("XCAT_GENESIS_EXTENSION_KERNEL_RELEASE", "6.18-test") + expected.update(kernel_modules=True, kernel_release="6.18-test") + self.oe_utils.execute_pre_post_process(data, data.getVar("IMAGE_POSTPROCESS_COMMAND")) + self.assertEqual(json.loads((deploy / "test-extension.manifest.json").read_text()), expected) + image.unlink() + with self.assertRaises(bb.process.ExecutionError) as failure: + self.oe_utils.execute_pre_post_process(data, data.getVar("IMAGE_POSTPROCESS_COMMAND")) + self.assertEqual(failure.exception.exitcode, 1) + + def test_extension_parse_policy(self): + recipe = self.tinfoil.get_recipe_file("xcat-genesis-extension-smoke") + cases = [ + ({"ARCHITECTURE": "armv7"}, "Invalid Genesis extension architecture"), + ({"NAME": "../escape"}, "Invalid Genesis extension name"), + ({"VERSION": "bad version"}, "Invalid Genesis extension version"), + ({"KEY_ID": "bad/key"}, "Invalid Genesis extension key ID"), + ({"LICENSE_CLASS": "unknown"}, "Invalid Genesis extension license class"), + ({"LICENSE_CLASS": "restricted"}, "Restricted Genesis extensions must set LICENSE_FLAGS"), + ({"KERNEL_MODULES": "yes"}, "must be true or false"), + ({"KERNEL_MODULES": "true"}, "Kernel extensions must declare"), + ({"CAPABILITIES": "invalid"}, "Invalid JSON"), + ({"CAPABILITIES": "{}"}, "must be a JSON string array"), + ({"PCI_IDS": '["not-a-pci-id"]'}, "Invalid value"), + ] + for values, message in cases: + with self.subTest(values=values): + config = self.tinfoil.config_data.createCopy() + for name, value in values.items(): + config.setVar("XCAT_GENESIS_EXTENSION_" + name + ":forcevariable", value) + with self.assertLogs("BitBake", level="ERROR") as errors: + with self.assertRaises(bb.BBHandledException): + self.tinfoil.parse_recipe_file(recipe, config_data=config) + self.assertTrue(any(message in error for error in errors.output), errors.output) + for values in ({"ARCHITECTURE": "armv7hf"}, + {"LICENSE_CLASS": "restricted"}, + {"KERNEL_MODULES": "true", "KERNEL_RELEASE": "6.18-test"}): + config = self.tinfoil.config_data.createCopy() + config.setVar("LICENSE_FLAGS:forcevariable", "commercial_test") + config.setVar("LICENSE_FLAGS_ACCEPTED", "commercial_test") + for name, value in values.items(): + config.setVar("XCAT_GENESIS_EXTENSION_" + name + ":forcevariable", value) + self.assertIsNotNone(self.tinfoil.parse_recipe_file(recipe, config_data=config)) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/xCAT-test/unit/genesis_openembedded.t b/xCAT-test/unit/genesis_openembedded.t index 1fd313d20..ef1c7ff6f 100644 --- a/xCAT-test/unit/genesis_openembedded.t +++ b/xCAT-test/unit/genesis_openembedded.t @@ -7,431 +7,22 @@ use FindBin; use lib "$FindBin::Bin/../lib"; use Test::More; -use XCAT::Test::File qw(repo_path slurp_repo_file); - -# The Yocto configuration, recipes, kernel fragments and units of the OpenEmbedded Genesis -# image are the artifact: kas and bitbake read these lines. The scripts and programs the image -# runs are tested in xCAT-test/bats/genesis_openembedded_*.bats. - -my $kas = slurp_repo_file('xCAT-genesis-base/oe/kas/common.yml'); -like( $kas, qr/tag: yocto-6\.0\.2/, 'build uses Yocto 6.0.2' ); -like( $kas, qr/fingerprint: 2AFB13F28FBBB0D1B9DAF63087EB3D32FB631AD9/, - 'build trusts the Yocto release key' ); -like( $kas, - qr{yocto-release:.*?repo: xcat-core.*?path: xCAT-genesis-base/oe/keys/yocto-release\.asc}s, - 'build loads the Yocto release key from the source tree' ); -like( $kas, - qr{xcat-core:.*?layers:.*?xCAT-genesis-base/oe/meta-xcat-genesis:}s, - 'build loads the Genesis layer from the source tree' ); -unlike( $kas, qr/gpg_keyserver:/, - 'build does not depend on a public keyserver' ); -like( $kas, qr/INHERIT \+= "archiver create-spdx vex"/, - 'build generates source archives, an SBOM, and VEX metadata' ); -like( $kas, qr/ARCHIVER_MODE\[src\] = "original"/, - 'release archives retain original sources' ); -like( $kas, qr/ARCHIVER_MODE\[diff\] = "1"/, - 'release archives retain applied changes' ); -like( $kas, qr/ARCHIVER_MODE\[recipe\] = "1"/, - 'release archives retain recipe metadata' ); +use XCAT::Test::File qw(slurp_repo_file); +# Fragments specify requested kernel options; they do not prove the built kernel configuration. my $yocto_release_key = slurp_repo_file('xCAT-genesis-base/oe/keys/yocto-release.asc'); is( sha256_hex($yocto_release_key), '42d49e59f2aa01a1c1417a52d3c915a24a64060c852f33e3bd04fbb738457e70', 'vendored Yocto release key matches the reviewed key material' ); -like( $kas, - qr{bitbake:.*?commit: acfe02fa38b5da9e6a36c6cedcf91d4fcbefbfbd.*?signed: true}s, - 'BitBake revision requires a signed tag' ); -like( $kas, - qr{openembedded-core:.*?commit: 5d1aa5c806c061a2994f4decb59016610f093213.*?signed: true}s, - 'OpenEmbedded-Core revision requires a signed tag' ); -like( $kas, - qr{meta-openembedded:.*?commit: af8b6d6b2f0b11595b0a0d5b82efa3129d52a628}s, - 'meta-openembedded revision is pinned' ); - -my $x86_kas = slurp_repo_file('xCAT-genesis-base/oe/kas/x86_64.yml'); -like( $x86_kas, qr{includes:\s*\n\s*- xCAT-genesis-base/oe/kas/common\.yml}, - 'x86_64 build includes the common configuration' ); -like( $x86_kas, qr/^machine: xcat-genesis-x86-64$/m, - 'x86_64 build selects its machine' ); - -my $x86_32_kas = slurp_repo_file('xCAT-genesis-base/oe/kas/x86.yml'); -like( $x86_32_kas, qr{includes:\s*\n\s*- xCAT-genesis-base/oe/kas/common\.yml}, - 'x86 build includes the common configuration' ); -like( $x86_32_kas, qr/^machine: xcat-genesis-x86$/m, - 'x86 build selects its machine' ); - -my $armv7hf_kas = slurp_repo_file('xCAT-genesis-base/oe/kas/armv7hf.yml'); -like( $armv7hf_kas, qr{includes:\s*\n\s*- xCAT-genesis-base/oe/kas/common\.yml}, - 'armv7hf build includes the common configuration' ); -like( $armv7hf_kas, qr/^machine: xcat-genesis-armv7hf$/m, - 'armv7hf build selects its machine' ); - -my $aarch64_kas = slurp_repo_file('xCAT-genesis-base/oe/kas/aarch64.yml'); -like( $aarch64_kas, qr{includes:\s*\n\s*- xCAT-genesis-base/oe/kas/common\.yml}, - 'aarch64 build includes the common configuration' ); -like( $aarch64_kas, qr/^machine: xcat-genesis-aarch64$/m, - 'aarch64 build selects its machine' ); - -my $riscv64_kas = slurp_repo_file('xCAT-genesis-base/oe/kas/riscv64.yml'); -like( $riscv64_kas, qr{includes:\s*\n\s*- xCAT-genesis-base/oe/kas/common\.yml}, - 'riscv64 build includes the common configuration' ); -like( $riscv64_kas, qr/^machine: xcat-genesis-riscv64$/m, - 'riscv64 build selects its machine' ); - -my $s390x_kas = slurp_repo_file('xCAT-genesis-base/oe/kas/s390x.yml'); -like( $s390x_kas, qr{includes:\s*\n\s*- xCAT-genesis-base/oe/kas/common\.yml}, - 's390x build includes the common configuration' ); -like( $s390x_kas, qr/^machine: xcat-genesis-s390x$/m, - 's390x build selects its machine' ); - -my $ppc64le_kas = slurp_repo_file('xCAT-genesis-base/oe/kas/ppc64le.yml'); -like( $ppc64le_kas, qr{includes:\s*\n\s*- xCAT-genesis-base/oe/kas/common\.yml}, - 'ppc64le build includes the common configuration' ); -like( $ppc64le_kas, qr/^machine: xcat-genesis-ppc64le$/m, - 'ppc64le build selects its machine' ); - -my $ppc64_kas = slurp_repo_file('xCAT-genesis-base/oe/kas/ppc64.yml'); -like( $ppc64_kas, qr{includes:\s*\n\s*- xCAT-genesis-base/oe/kas/common\.yml}, - 'ppc64 build includes the common configuration' ); -like( $ppc64_kas, qr/^machine: xcat-genesis-ppc64$/m, - 'ppc64 build selects its machine' ); - - -my $distro = slurp_repo_file( - 'xCAT-genesis-base/oe/meta-xcat-genesis/conf/distro/xcat-genesis.conf' -); -like( $distro, qr/^DISTRO_CODENAME = "cheetah"$/m, - 'distro uses the Cheetah codename' ); -like( $distro, qr/^LINUX_VERSION_EXTENSION = "-xCAT-genesis"$/m, - 'kernel release identifies xCAT Genesis' ); -like( $distro, - qr/^KERNEL_MODULE_PACKAGE_SUFFIX = "-\$\{KERNEL_VERSION_PKG_NAME\}"$/m, - 'kernel module package names use the normalized kernel release' ); -like( $distro, qr/^TCLIBC = "glibc"$/m, 'distro uses glibc' ); -like( $distro, qr/^INIT_MANAGER = "systemd"$/m, - 'distro uses systemd' ); -like( $distro, qr/^DISTRO_FEATURES_DEFAULTS = ""$/m, - 'distro disables inherited feature defaults' ); -like( $distro, - qr/^DISTRO_FEATURES = "acl ipv4 ipv6 largefile pci seccomp"$/m, - 'distro keeps an explicit feature list' ); -like( $distro, qr/^NO_RECOMMENDATIONS = "1"$/m, - 'image excludes package recommendations' ); -like( $distro, qr/PACKAGECONFIG:pn-systemd = ".*\bsysext\b.*"/, - 'systemd includes system extensions' ); -like( $distro, - qr/^PACKAGECONFIG:pn-networkmanager = "crypto-null libedit man-resolv-conf nmcli systemd"$/m, - 'NetworkManager keeps the minimal feature set' ); -like( $distro, qr/^PACKAGECONFIG:append:pn-iproute2 = " rdma"$/m, - 'iproute2 includes the RDMA command' ); -like( $distro, qr/^NETWORKMANAGER_DHCP_DEFAULT:pn-networkmanager = "internal"$/m, - 'NetworkManager uses its internal DHCP client' ); -like( $distro, - qr/^NETWORKMANAGER_DNS_RC_MANAGER_DEFAULT:pn-networkmanager = "symlink"$/m, - 'NetworkManager manages the resolver symlink' ); -unlike( $distro, qr/\b(?:dhclient|dhcpcd)\b/, - 'distro excludes external DHCP clients' ); - -my $machine = slurp_repo_file( - 'xCAT-genesis-base/oe/meta-xcat-genesis/conf/machine/xcat-genesis-x86-64.conf' -); -like( $machine, qr/^DEFAULTTUNE = "x86-64"$/m, - 'x86_64 uses the baseline tune' ); -unlike( $machine, qr/x86-64-v[234]/, - 'x86_64 does not require a newer ISA level' ); -like( $machine, qr/^MACHINE_FEATURES_DEFAULTS = ""$/m, - 'machine disables inherited feature defaults' ); -like( $machine, qr/^MACHINE_FEATURES = "pci"$/m, - 'machine keeps an explicit feature list' ); -like( $machine, qr/^IMAGE_FSTYPES = "cpio\.gz ext4"$/m, - 'machine builds netboot and VM filesystems' ); -like( $machine, qr/^QB_DEFAULT_FSTYPE = "ext4"$/m, - 'QEMU uses a generated filesystem type' ); -like( $machine, qr/^XCAT_GENESIS_CONSOLE_TTY = "ttyS0"$/m, - 'x86_64 status console uses its serial terminal' ); -like( $machine, qr/^XCAT_GENESIS_ARCHITECTURE = "x86_64"$/m, - 'x86_64 exports its canonical extension identity' ); - -my $ppc64le_machine = slurp_repo_file( - 'xCAT-genesis-base/oe/meta-xcat-genesis/conf/machine/xcat-genesis-ppc64le.conf' -); -like( $ppc64le_machine, qr/^DEFAULTTUNE = "ppc64p8le"$/m, - 'ppc64le uses the POWER8 little-endian tune' ); -like( $ppc64le_machine, qr/^KERNEL_IMAGETYPE = "vmlinux"$/m, - 'ppc64le builds a directly bootable kernel' ); -like( $ppc64le_machine, qr/^XCAT_GENESIS_CONSOLE_TTY = "hvc0"$/m, - 'ppc64le status console uses its hypervisor terminal' ); -like( $ppc64le_machine, qr/^QB_MACHINE = "-machine pseries"$/m, - 'ppc64le QEMU target uses pSeries' ); -like( $ppc64le_machine, qr/^QB_CPU = "-cpu POWER8"$/m, - 'ppc64le QEMU target uses the supported CPU baseline' ); -unlike( $ppc64le_machine, qr/^DEFAULTTUNE = "ppc64"$/m, - 'ppc64le is not aliased to big-endian ppc64' ); -like( $ppc64le_machine, qr/^XCAT_GENESIS_ARCHITECTURE = "ppc64le"$/m, - 'ppc64le exports its canonical extension identity' ); - -my $ppc64_machine = slurp_repo_file( - 'xCAT-genesis-base/oe/meta-xcat-genesis/conf/machine/xcat-genesis-ppc64.conf' -); -like( $ppc64_machine, qr/^DEFAULTTUNE = "powerpc64"$/m, - 'ppc64 uses the generic big-endian ABI' ); -unlike( $ppc64_machine, qr/^DEFAULTTUNE = "ppc64p8le"$/m, - 'ppc64 is not aliased to little-endian ppc64le' ); -like( $ppc64_machine, qr/^QB_MACHINE = "-machine pseries"$/m, - 'ppc64 QEMU target uses pSeries' ); -like( $ppc64_machine, qr/^QB_CPU = "-cpu POWER8"$/m, - 'ppc64 QEMU target can emulate the generic ABI' ); -like( $ppc64_machine, qr/^XCAT_GENESIS_CONSOLE_TTY = "hvc0"$/m, - 'ppc64 status console uses its hypervisor terminal' ); -like( $ppc64_machine, qr/^XCAT_GENESIS_ARCHITECTURE = "ppc64"$/m, - 'ppc64 exports its canonical extension identity' ); - -my $x86_machine = slurp_repo_file( - 'xCAT-genesis-base/oe/meta-xcat-genesis/conf/machine/xcat-genesis-x86.conf' -); -like( $x86_machine, qr/^DEFAULTTUNE = "i686"$/m, - 'x86 uses the i686 baseline' ); -unlike( $x86_machine, qr/^DEFAULTTUNE = "i586"$/m, - 'x86 does not retain the i586 baseline' ); -like( $x86_machine, qr/^QB_SYSTEM_NAME = "qemu-system-i386"$/m, - 'x86 uses the 32-bit QEMU system target' ); -like( $x86_machine, qr/^XCAT_GENESIS_CONSOLE_TTY = "ttyS0"$/m, - 'x86 status console uses its serial terminal' ); -like( $x86_machine, qr/^XCAT_GENESIS_ARCHITECTURE = "x86"$/m, - 'x86 exports its canonical extension identity' ); - -my $armv7hf_machine = slurp_repo_file( - 'xCAT-genesis-base/oe/meta-xcat-genesis/conf/machine/xcat-genesis-armv7hf.conf' -); -like( $armv7hf_machine, qr/^DEFAULTTUNE = "armv7ahf"$/m, - 'armv7hf uses the ARMv7-A hard-float tune' ); -like( $armv7hf_machine, qr/^ARM_INSTRUCTION_SET = "arm"$/m, - 'armv7hf does not require Thumb instructions' ); -unlike( $armv7hf_machine, qr/neon/i, - 'armv7hf does not require NEON' ); -like( $armv7hf_machine, qr/^XCAT_GENESIS_ARCHITECTURE = "armv7hf"$/m, - 'armv7hf exports its canonical extension identity' ); -like( $armv7hf_machine, qr/^QB_MACHINE = "-machine virt,highmem=off"$/m, - 'armv7hf QEMU target uses the virtual platform' ); -like( $armv7hf_machine, qr/^QB_CPU = "-cpu cortex-a15"$/m, - 'armv7hf QEMU target uses an ARMv7 CPU' ); -like( $armv7hf_machine, qr/^XCAT_GENESIS_CONSOLE_TTY = "ttyAMA0"$/m, - 'armv7hf status console uses its PL011 terminal' ); - -my $aarch64_machine = slurp_repo_file( - 'xCAT-genesis-base/oe/meta-xcat-genesis/conf/machine/xcat-genesis-aarch64.conf' -); -like( $aarch64_machine, qr/^DEFAULTTUNE = "aarch64"$/m, - 'aarch64 uses the generic ARMv8-A tune' ); -like( $aarch64_machine, qr/^XCAT_GENESIS_ARCHITECTURE = "aarch64"$/m, - 'aarch64 exports its canonical extension identity' ); -like( $aarch64_machine, qr/^QB_MACHINE = "-machine virt"$/m, - 'aarch64 QEMU target uses the virtual platform' ); -like( $aarch64_machine, qr/^QB_CPU = "-cpu cortex-a57"$/m, - 'aarch64 QEMU target uses its conservative CPU baseline' ); -like( $aarch64_machine, qr/^XCAT_GENESIS_CONSOLE_TTY = "ttyAMA0"$/m, - 'aarch64 status console uses its PL011 terminal' ); - -my $riscv64_machine = slurp_repo_file( - 'xCAT-genesis-base/oe/meta-xcat-genesis/conf/machine/xcat-genesis-riscv64.conf' -); -like( $riscv64_machine, qr/^DEFAULTTUNE = "riscv64"$/m, - 'riscv64 uses the RV64GC tune' ); -like( $riscv64_machine, qr/^XCAT_GENESIS_ARCHITECTURE = "riscv64"$/m, - 'riscv64 exports its canonical extension identity' ); -like( $riscv64_machine, qr/^EXTRA_IMAGEDEPENDS = "opensbi"$/m, - 'riscv64 builds its OpenSBI firmware' ); -like( $riscv64_machine, qr/^QB_DEFAULT_BIOS = "fw_jump\.elf"$/m, - 'riscv64 QEMU target boots the built OpenSBI firmware' ); -like( $riscv64_machine, qr/^QB_MACHINE = "-machine virt"$/m, - 'riscv64 QEMU target uses the virtual platform' ); -like( $riscv64_machine, qr/^QB_CPU = "-cpu rv64"$/m, - 'riscv64 QEMU target uses the generic CPU' ); -like( $riscv64_machine, qr/^XCAT_GENESIS_CONSOLE_TTY = "ttyS0"$/m, - 'riscv64 status console uses its 8250 terminal' ); - -for my $machine_console ( - [ x86_64 => $machine ], - [ x86 => $x86_machine ], - [ ppc64 => $ppc64_machine ], - [ ppc64le => $ppc64le_machine ], - [ armv7hf => $armv7hf_machine ], - [ aarch64 => $aarch64_machine ], - [ riscv64 => $riscv64_machine ], - ) -{ - unlike( $machine_console->[1], - qr/^QB_KERNEL_CMDLINE_APPEND = ".*xcat\.debug-shell/m, - "$machine_console->[0] VM starts the status console" ); -} - -my $image = slurp_repo_file( - 'xCAT-genesis-base/oe/meta-xcat-genesis/recipes-core/images/xcat-genesis-image.bb' -); -unlike( $image, - qr/\b(?:dhclient|dhcpcd|busybox-udhcpc|systemd-networkd)\b/, - 'base image excludes alternate network clients' ); -like( $image, qr/\bxcat-genesis-network\b/, - 'base image includes the Genesis network policy' ); -like( $image, qr/\bxcat-genesis-protocol\b/, - 'base image includes the destiny clients' ); -like( $image, qr/\bpackagegroup-xcat-genesis-hardware\b/, - 'base image includes open hardware tools' ); -like( $image, qr/\bxcat-genesis-extensions\b/, - 'base image includes the signed extension loader' ); -like( $image, qr/\bxcat-genesis-console\b/, - 'base image includes the status console' ); - -my $hardware_group = slurp_repo_file( - 'xCAT-genesis-base/oe/meta-xcat-genesis/recipes-core/packagegroups/packagegroup-xcat-genesis-hardware.bb' -); -for my $tool ( - qw( - pciutils usbutils hwloc ethtool rdma-core iproute2-rdma lldpd ipmitool curl jq - nvme-cli smartmontools sg3-utils lsscsi mdadm hdparm parted lvm2 - memtester stress-ng edac-utils kernel-modules mstflint - xcat-genesis-hardware-control - ) - ) -{ - like( $hardware_group, qr/\b\Q$tool\E\b/, - "hardware group includes $tool" ); -} -like( $hardware_group, - qr/^RDEPENDS:\$\{PN\}:append:xcat-genesis-x86-64 = " dmidecode lshw mcelog"$/m, - 'x86_64 diagnostics are architecture scoped' ); -like( $hardware_group, - qr/^RDEPENDS:\$\{PN\}:append:xcat-genesis-x86 = " dmidecode lshw"$/m, - 'x86 omits the unsupported machine-check daemon' ); -like( $hardware_group, - qr/^RDEPENDS:\$\{PN\}:append:xcat-genesis-armv7hf = " dmidecode lshw"$/m, - 'armv7hf diagnostics are architecture scoped' ); -unlike( $hardware_group, qr/xcat-genesis-armv7(?:\s|\b)/, - 'hardware packages do not use the old ARMv7 name' ); -like( $hardware_group, - qr/^RDEPENDS:\$\{PN\}:append:xcat-genesis-ppc64 = " dmidecode xcat-genesis-hardware-control-iprutils"$/m, - 'ppc64 includes the Power RAID provider' ); -like( $hardware_group, - qr/^RDEPENDS:\$\{PN\}:append:xcat-genesis-ppc64le = " dmidecode xcat-genesis-hardware-control-iprutils"$/m, - 'ppc64le includes the Power RAID provider' ); -like( $hardware_group, - qr/^RDEPENDS:\$\{PN\}:append:xcat-genesis-riscv64 = " lshw"$/m, - 'RISC-V omits unsupported DMI tools' ); -unlike( $hardware_group, - qr/\b(?:storcli|perccli|ssacli|arcconf|nvidia-smi|rocm-smi)\b/i, - 'base image excludes vendor-only tools' ); - -my $hardware_recipe = slurp_repo_file( - 'xCAT-genesis-base/oe/meta-xcat-genesis/recipes-core/xcat-genesis-hardware-control/xcat-genesis-hardware-control_1.0.bb' -); -like( $hardware_recipe, - qr/^RDEPENDS:\$\{PN\} = "bash coreutils jq mstflint nvme-cli util-linux-flock"$/m, - 'hardware control dependencies are explicit' ); -like( $hardware_recipe, - qr/^PACKAGES:prepend:xcat-genesis-ppc64 = "\$\{PN\}-iprutils "$/m, - 'iprutils provider package supports ppc64' ); -like( $hardware_recipe, - qr/^PACKAGES:prepend:xcat-genesis-ppc64le = "\$\{PN\}-iprutils "$/m, - 'iprutils provider package supports ppc64le' ); -like( $hardware_recipe, - qr/^RDEPENDS:\$\{PN\}-iprutils:xcat-genesis-ppc64 = "\$\{PN\} bash iprutils"$/m, - 'ppc64 iprutils dependencies are architecture scoped' ); -like( $hardware_recipe, - qr/^RDEPENDS:\$\{PN\}-iprutils:xcat-genesis-ppc64le = "\$\{PN\} bash iprutils"$/m, - 'iprutils provider dependencies are architecture scoped' ); -like( $hardware_recipe, qr/\$\{datadir\}\/xcat\/genesis\/providers/, - 'provider manifests use an architecture-neutral data path' ); -unlike( $hardware_recipe, - qr/\b(?:storcli|perccli|ssacli|arcconf|nvidia-smi|rocm-smi)\b/i, - 'hardware control excludes vendor-only tools' ); - - -for my $provider (qw(nvme mstflint iprutils)) { - my $manifest = slurp_repo_file( - "xCAT-genesis-base/oe/meta-xcat-genesis/recipes-core/xcat-genesis-hardware-control/files/$provider.json" - ); - unlike( $manifest, qr/"destructive"\s*:\s*true/, - "$provider base provider is read-only" ); -} - -my $mstflint = slurp_repo_file( - 'xCAT-genesis-base/oe/meta-xcat-genesis/recipes-support/mstflint/mstflint_4.36.0.bb' -); -like( $mstflint, - qr/^LICENSE = "Linux-OpenIB & MIT & BSD-2-Clause"$/m, - 'mstflint records every linked source license' ); -like( $mstflint, - qr/^SRCREV = "b9d9e844a14ae1c85cb90c76ccd4a56a0eccd599"$/m, - 'mstflint source revision is pinned' ); -like( $mstflint, qr/^RDEPENDS:\$\{PN\} = "python3-modules"$/m, - 'mstflint installs its Python runtime' ); -like( $mstflint, qr/^PACKAGECONFIG \?\?= "adb cables dc inband openssl"$/m, - 'mstflint omits the non-portable rdmem tool' ); -unlike( $mstflint, qr/--enable-(?:fw-mgr|nvml)/, - 'mstflint excludes vendor-coupled features' ); - -my $iprutils = slurp_repo_file( - 'xCAT-genesis-base/oe/meta-xcat-genesis/recipes-support/iprutils/iprutils_2.4.19.bb' -); -like( $iprutils, qr/^LICENSE = "CPL-1\.0"$/m, - 'iprutils records its source license' ); -like( $iprutils, - qr/^SRCREV = "9961e538736a6e81f1072a926c3ad91b8513c8c1"$/m, - 'iprutils source revision is pinned' ); -like( $iprutils, qr/^COMPATIBLE_HOST = "powerpc64\.\*-linux"$/m, - 'iprutils is restricted to 64-bit Power targets' ); -like( $iprutils, qr/--without-systemd --without-initscripts/, - 'iprutils omits unused background services' ); - -my $kernel_append = slurp_repo_file( - 'xCAT-genesis-base/oe/meta-xcat-genesis/recipes-kernel/linux/linux-yocto_%.bbappend' -); -like( $kernel_append, qr/file:\/\/xcat-genesis-x86-64\.cfg/, - 'x86_64 kernel uses the Genesis hardware fragment' ); -like( $kernel_append, qr/file:\/\/xcat-genesis-x86-common\.cfg/, - 'x86 targets share their hardware fragment' ); -like( $kernel_append, qr/file:\/\/xcat-genesis-x86\.cfg/, - 'x86 kernel uses its baseline fragment' ); -like( $kernel_append, qr/do_kernel_metadata:prepend:xcat-genesis-x86/, - 'x86 replaces the upstream QEMU CPU policy' ); -like( $kernel_append, qr/file:\/\/xcat-genesis-armv7hf\.cfg/, - 'armv7hf kernel uses the Genesis hardware fragment' ); -like( $kernel_append, - qr/^SRCREV_machine:xcat-genesis-armv7hf = "b1ba5428513b52c2bd6acfd3ad0a910f699bc395"$/m, - 'armv7hf kernel revision is pinned' ); -like( $kernel_append, qr/file:\/\/xcat-genesis-aarch64\.cfg/, - 'aarch64 kernel uses the Genesis hardware fragment' ); -like( $kernel_append, - qr/^SRCREV_machine:xcat-genesis-aarch64 = "b1ba5428513b52c2bd6acfd3ad0a910f699bc395"$/m, - 'aarch64 kernel revision is pinned' ); -like( $kernel_append, qr/file:\/\/xcat-genesis-riscv64\.cfg/, - 'riscv64 kernel uses the Genesis hardware fragment' ); -like( $kernel_append, - qr/^SRCREV_machine:xcat-genesis-riscv64 = "b1ba5428513b52c2bd6acfd3ad0a910f699bc395"$/m, - 'riscv64 kernel revision is pinned' ); -like( $kernel_append, qr/file:\/\/xcat-genesis-powerpc64\.cfg/, - '64-bit Power kernels share the Genesis hardware fragment' ); -like( $kernel_append, - qr/^SRCREV_machine:xcat-genesis-ppc64le = "b1ba5428513b52c2bd6acfd3ad0a910f699bc395"$/m, - 'ppc64le kernel revision is pinned' ); -like( $kernel_append, qr/do_kernel_metadata:prepend:xcat-genesis-ppc64le/, - 'ppc64le repairs the upstream kernel metadata before configuration' ); -like( $kernel_append, - qr/^SRCREV_machine:xcat-genesis-ppc64 = "b1ba5428513b52c2bd6acfd3ad0a910f699bc395"$/m, - 'ppc64 kernel revision is pinned' ); -like( $kernel_append, qr/do_kernel_metadata:prepend:xcat-genesis-ppc64\(\)/, - 'ppc64 repairs the upstream kernel metadata before configuration' ); -like( $kernel_append, qr/file:\/\/xcat-genesis-ppc64\.cfg/, - 'ppc64 kernel uses its big-endian fragment' ); my $ppc64_kernel_config = slurp_repo_file( 'xCAT-genesis-base/oe/meta-xcat-genesis/recipes-kernel/linux/linux-yocto/xcat-genesis-ppc64.cfg' ); like( $ppc64_kernel_config, qr/^CONFIG_CPU_BIG_ENDIAN=y$/m, - 'ppc64 kernel is big endian' ); + 'ppc64 fragment requests big endian' ); like( $ppc64_kernel_config, qr/^# CONFIG_CPU_LITTLE_ENDIAN is not set$/m, - 'ppc64 kernel is not little endian' ); + 'ppc64 fragment disables little endian' ); my $kernel_config = slurp_repo_file( 'xCAT-genesis-base/oe/meta-xcat-genesis/recipes-kernel/linux/linux-yocto/xcat-genesis-x86-common.cfg' @@ -447,24 +38,24 @@ for my $symbol ( ) { like( $kernel_config, qr/^\Q$symbol\E=[ym]$/m, - "x86_64 kernel enables $symbol" ); + "x86_64 fragment requests $symbol" ); } my $x86_kernel_config = slurp_repo_file( 'xCAT-genesis-base/oe/meta-xcat-genesis/recipes-kernel/linux/linux-yocto/xcat-genesis-x86.cfg' ); like( $x86_kernel_config, qr/^CONFIG_M686=y$/m, - 'x86 kernel selects the i686 processor family' ); + 'x86 fragment selects the i686 processor family' ); like( $x86_kernel_config, qr/^# CONFIG_M586 is not set$/m, - 'x86 kernel does not retain i586' ); + 'x86 fragment disables i586' ); like( $x86_kernel_config, qr/^CONFIG_HIGHMEM4G=y$/m, - 'x86 kernel supports the 32-bit high-memory model' ); + 'x86 fragment requests the 32-bit high-memory model' ); like( $x86_kernel_config, qr/^# CONFIG_X86_MCE is not set$/m, - 'x86 kernel supports processors without machine-check registers' ); + 'x86 fragment disables machine-check registers' ); for my $vendor (qw(AMD INTEL)) { like( $x86_kernel_config, qr/^# CONFIG_X86_MCE_\Q$vendor\E is not set$/m, - "x86 kernel disables the $vendor machine-check feature" ); + "x86 fragment disables the $vendor machine-check feature" ); } my $x86_64_kernel_config = slurp_repo_file( @@ -488,7 +79,7 @@ for my $symbol ( ) { like( $armv7hf_kernel_config, qr/^\Q$symbol\E=[ym]$/m, - "armv7hf kernel enables $symbol" ); + "armv7hf fragment requests $symbol" ); } my $aarch64_kernel_config = slurp_repo_file( @@ -507,7 +98,7 @@ for my $symbol ( ) { like( $aarch64_kernel_config, qr/^\Q$symbol\E=[ym]$/m, - "aarch64 kernel enables $symbol" ); + "aarch64 fragment requests $symbol" ); } my $riscv64_kernel_config = slurp_repo_file( @@ -526,7 +117,7 @@ for my $symbol ( ) { like( $riscv64_kernel_config, qr/^\Q$symbol\E=[ym]$/m, - "riscv64 kernel enables $symbol" ); + "riscv64 fragment requests $symbol" ); } for my $symbol ( @@ -537,7 +128,7 @@ for my $symbol ( ) { like( $riscv64_kernel_config, qr/^\Q$symbol\E=[ym]$/m, - "riscv64 kernel enables $symbol to boot on ACPI firmware" ); + "riscv64 fragment requests $symbol to boot on ACPI firmware" ); } my $powerpc64_kernel_config = slurp_repo_file( @@ -555,104 +146,16 @@ for my $symbol ( ) { like( $powerpc64_kernel_config, qr/^\Q$symbol\E=[ym]$/m, - "64-bit Power kernel enables $symbol" ); + "64-bit Power fragment requests $symbol" ); } like( $powerpc64_kernel_config, qr/^# CONFIG_PPC_VAS is not set$/m, - 'POWER8 image disables unsupported VAS facilities' ); - -my $protocol_recipe = slurp_repo_file( - 'xCAT-genesis-base/oe/meta-xcat-genesis/recipes-xcat/xcat-genesis-protocol/xcat-genesis-protocol_1.0.bb' -); -like( $protocol_recipe, - qr{^FILESEXTRAPATHS:prepend := "\$\{THISDIR\}/\.\./\.\./\.\./\.\./\.\./xCAT-genesis-scripts/usr/bin:"$}m, - 'destiny clients use the canonical scripts' ); -like( $protocol_recipe, qr/file:\/\/getdestiny.*file:\/\/nextdestiny/s, - 'both destiny clients are packaged' ); -like( $protocol_recipe, qr/^RDEPENDS:\$\{PN\} = "bash openssl-bin"$/m, - 'destiny client dependencies are explicit' ); -unlike( $protocol_recipe, qr/\bdoxcat\b/, - 'obsolete bootstrap script is excluded' ); - -my $networkmanager = slurp_repo_file( - 'xCAT-genesis-base/oe/meta-xcat-genesis/recipes-connectivity/networkmanager/networkmanager_%.bbappend' -); -like( $networkmanager, qr/^EXTRA_OEMESON:append = " -Dtests=no"$/m, - 'NetworkManager omits its test suite' ); -like( $networkmanager, - qr{sed -i '/\^Also=NetworkManager-wait-online\.service\$/d'}, - 'NetworkManager does not enable wait-online' ); - -my $network_recipe = slurp_repo_file( - 'xCAT-genesis-base/oe/meta-xcat-genesis/recipes-connectivity/xcat-genesis-network/xcat-genesis-network_1.0.bb' -); -like( $network_recipe, - qr/^RDEPENDS:\$\{PN\} = "networkmanager-daemon networkmanager-nmcli"$/m, - 'network policy installs the daemon and nmcli' ); - -my $network_config = slurp_repo_file( - 'xCAT-genesis-base/oe/meta-xcat-genesis/recipes-connectivity/xcat-genesis-network/files/10-xcat-genesis.conf' -); -like( $network_config, qr/^ipv4\.dhcp-timeout=20$/m, - 'IPv4 DHCP has a bounded timeout' ); -like( $network_config, qr/^ipv6\.dhcp-duid=ll$/m, - 'IPv6 DHCP uses the link-layer DUID' ); -like( $network_config, qr/^ipv6\.dhcp-timeout=20$/m, - 'IPv6 DHCP has a bounded timeout' ); - -my $init_recipe = slurp_repo_file( - 'xCAT-genesis-base/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/xcat-genesis-init_1.0.bb' -); -like( $init_recipe, qr/\bcoreutils\b/, - 'runtime status dependencies are explicit' ); -like( $init_recipe, qr/\butil-linux-logger\b/, - 'runtime logging dependency is explicit' ); -like( $init_recipe, qr/file:\/\/genesis-status/, - 'runtime status helper is packaged' ); -like( $init_recipe, qr/file:\/\/genesis-maintenance-shell/, - 'maintenance shell is packaged' ); -like( $init_recipe, qr/file:\/\/genesis-functions/, - 'shared runtime functions are packaged' ); -like( $init_recipe, - qr/install -m 0644 .*genesis-functions.*\$\{libexecdir\}\/xcat\/genesis-functions/s, - 'shared runtime functions are installed as data' ); -unlike( $init_recipe, qr/genesis-debug-shell|XCAT_GENESIS_DEBUG/, - 'legacy debug shell is absent from the image recipe' ); - -my $INIT_FILES = 'xCAT-genesis-base/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files'; -my $legacy_debug_shell = repo_path("$INIT_FILES/genesis-debug-shell"); -my $legacy_debug_service = repo_path("$INIT_FILES/xcat-genesis-debug-shell\@.service"); -ok( !-e $legacy_debug_shell, 'legacy debug shell script is removed' ); -ok( !-e $legacy_debug_service, 'legacy debug shell service is removed' ); - - -my $console_recipe = slurp_repo_file( - 'xCAT-genesis-base/oe/meta-xcat-genesis/recipes-core/xcat-genesis-console/xcat-genesis-console_1.0.bb' -); -like( $console_recipe, qr/^DEPENDS = "libnewt systemd"$/m, - 'status console builds against libnewt and the journal API' ); -like( $console_recipe, - qr/^RDEPENDS:\$\{PN\} = "libnewt libsystemd ncurses-terminfo-base xcat-genesis-init"$/m, - 'status console installs its terminal and maintenance-shell runtime' ); -like( $console_recipe, qr/^inherit meson pkgconfig systemd$/m, - 'status console uses the project build definition' ); -unlike( $console_recipe, qr/\b(?:whiptail|popt)\b/, - 'status console omits the dialog wrapper' ); - -my $console_build = slurp_repo_file( - 'xCAT-genesis-base/oe/meta-xcat-genesis/recipes-core/xcat-genesis-console/files/xcat-genesis-console/meson.build' -); -like( $console_build, qr/'c_std=c17'/, - 'status console uses the C17 language contract' ); -like( $console_build, qr/dependency\('libnewt'\).*dependency\('libsystemd'\)/s, - 'status console links Newt and the journal client library' ); -like( $console_build, qr/'werror=true'/, - 'status console treats compiler warnings as build failures' ); + 'POWER8 fragment disables VAS facilities' ); my $console_service = slurp_repo_file( 'xCAT-genesis-base/oe/meta-xcat-genesis/recipes-core/xcat-genesis-console/files/xcat-genesis-console@.service' ); like( $console_service, qr/^Environment=TERM=xterm$/m, - 'status console redraws line graphics after a late attach' ); + 'console service sets the terminal type' ); like( $console_service, qr/^Environment=COLUMNS=80$/m, 'status console fixes the serial width' ); like( $console_service, qr/^Environment=LINES=24$/m, @@ -662,80 +165,5 @@ like( $console_service, qr/^TTYPath=\/dev\/%I$/m, unlike( $console_service, qr/xcat\.debug-shell|genesis-debug-shell/, 'status console has no boot-time debug escape' ); -ok( !-e repo_path( - 'xCAT-genesis-base/oe/meta-xcat-genesis/recipes-core/xcat-genesis-console/files/xcat-genesis-console.c' - ), - 'status console is not kept as a monolithic source file' ); - -my $network_state_service = slurp_repo_file( - 'xCAT-genesis-base/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/xcat-genesis-network-state.service' -); -like( $network_state_service, qr/^After=NetworkManager\.service$/m, - 'network selection follows NetworkManager' ); -like( $network_state_service, - qr/^Before=xcat-genesis-network-ready\.target$/m, - 'network selection gates Genesis readiness' ); - -my $register_service = slurp_repo_file( - 'xCAT-genesis-base/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/xcat-genesis-register.service' -); -like( $register_service, - qr/^Requires=xcat-genesis-network-ready\.target xcat-genesis-extensions\.service$/m, - 'registration requires Genesis network readiness' ); -like( $register_service, - qr/^After=xcat-genesis-network-ready\.target xcat-genesis-extensions\.service$/m, - 'registration waits for extension verification' ); -unlike( $register_service, qr/network-online\.target/, - 'registration avoids generic network readiness' ); - -my $extension_recipe = slurp_repo_file( - 'xCAT-genesis-base/oe/meta-xcat-genesis/recipes-core/xcat-genesis-extensions/xcat-genesis-extensions_1.0.bb' -); -like( $extension_recipe, - qr/^RDEPENDS:\$\{PN\} = "bash coreutils jq openssl-bin systemd xcat-genesis-init"$/m, - 'extension verifier dependencies are explicit' ); - - -my $extension_service = slurp_repo_file( - 'xCAT-genesis-base/oe/meta-xcat-genesis/recipes-core/xcat-genesis-extensions/files/xcat-genesis-extensions.service' -); -like( $extension_service, - qr{^ConditionDirectoryNotEmpty=/var/lib/xcat/genesis/extensions$}m, - 'extension loading requires staged artifacts' ); -like( $extension_service, qr/^Before=xcat-genesis-register\.service$/m, - 'extensions load before xCAT registration' ); - -my $extension_class = slurp_repo_file( - 'xCAT-genesis-base/oe/meta-xcat-genesis/classes-recipe/xcat-genesis-extension.bbclass' -); -like( $extension_class, qr/^inherit sysext-image$/m, - 'extensions use the upstream systemd image class' ); -like( $extension_class, qr/^IMAGE_FSTYPES = "squashfs-zst"$/m, - 'extensions are immutable SquashFS images' ); -like( $extension_class, - qr/^XCAT_GENESIS_EXTENSION_ARCHITECTURE \?\?= "\$\{XCAT_GENESIS_ARCHITECTURE\}"$/m, - 'extension manifests use the canonical machine identity' ); -like( $extension_class, qr/"armv7hf"/, - 'extension manifests accept the canonical ARMv7 identity' ); -unlike( $extension_class, qr/"armv7"/, - 'extension manifests reject the ambiguous ARMv7 identity' ); -like( $extension_class, qr/XCAT_GENESIS_EXTENSION_KERNEL_RELEASE/, - 'extension metadata supports kernel ABI pinning' ); -like( $extension_class, qr/Restricted Genesis extensions must set LICENSE_FLAGS/, - 'restricted extensions require explicit license acceptance' ); -like( $extension_class, qr/"sha256":/, - 'extension manifests record the image digest' ); - -my $smoke_extension = slurp_repo_file( - 'xCAT-genesis-base/oe/meta-xcat-genesis/recipes-extensions/images/xcat-genesis-extension-smoke.bb' -); -like( $smoke_extension, qr/^XCAT_GENESIS_EXTENSION_NAME = "xcat-smoke"$/m, - 'open smoke extension exercises the build path' ); - -my $preset = slurp_repo_file( - 'xCAT-genesis-base/oe/meta-xcat-genesis/recipes-core/xcat-genesis-init/files/00-xcat-genesis.preset' -); -is( $preset, "disable getty\@.service\n", - 'preset disables the unused virtual-terminal getty' ); done_testing(); From 105069634d7c85a08fc3842c35ea2503eceefef4 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, 9 Oct 2026 19:08:58 -0300 Subject: [PATCH 4/4] docs(test): describe Genesis test boundaries --- xCAT-test/bats/README.md | 14 ++++++++++++++ xCAT-test/native/README.md | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/xCAT-test/bats/README.md b/xCAT-test/bats/README.md index 7b5f2e036..c89958e17 100644 --- a/xCAT-test/bats/README.md +++ b/xCAT-test/bats/README.md @@ -43,3 +43,17 @@ The two legacy Red Hat version cases use GNU awk. CI installs both interpreters. Package-manager commands are test doubles, so these cases check selection, arguments and error handling, not repository availability or package installation. + +## Genesis startup + +`genesis_ib_modules.bats` and `genesis_secondary_dhcp.bats` run the complete +`doxcat` script in a private filesystem and network namespace. They replace +external services and device commands, then check the boot interface and the +IPv4 and IPv6 DHCP requests. They do not contact a DHCP server or a BMC. + +The startup cases require Linux, bats-core 1.4 or newer, Bubblewrap, Bash, GNU +core utilities, grep, sed, awk and permission to create user namespaces. +Missing Linux prerequisites fail these cases. Non-Linux hosts skip them. +The RPM prerequisite check requires `rpmspec` but does not use the sandbox. +Run containers with an init process to +reap children created by deliberate-fault tests. diff --git a/xCAT-test/native/README.md b/xCAT-test/native/README.md index 6dac73a77..261239e2c 100644 --- a/xCAT-test/native/README.md +++ b/xCAT-test/native/README.md @@ -96,3 +96,36 @@ EL6 or Ubuntu 14.04 environment. These cases do not qualify native SUSE package transactions, dependency resolution, a running xcatd daemon, or machine boot. The namespace fixture targets merged-/usr hosts. Container runs need namespace permissions and an init process that reaps orphaned children. + +## Genesis OpenEmbedded + +`genesis_metadata.py` runs inside a Genesis KAS shell. It uses BitBake to +evaluate machine and package policy, install local-file recipes into temporary +directories, generate extension manifests and reject invalid extension +metadata. It does not extract or evaluate recipe source fragments. + +For example, from the source checkout: + +```sh +export KAS_WORK_DIR=/path/to/kas-work +export KAS_BUILD_DIR=/path/to/kas-build-x86_64 +source_tree=$PWD +kas dump --format json xCAT-genesis-base/oe/kas/x86_64.yml >"${KAS_BUILD_DIR}.json" +kas shell xCAT-genesis-base/oe/kas/x86_64.yml -c \ + "GENESIS_ARCHITECTURE=x86_64 GENESIS_KAS_CONFIG='${KAS_BUILD_DIR}.json' python3 '${source_tree}/xCAT-test/native/genesis_metadata.py'" +``` + +Use the pinned KAS requirements and OpenEmbedded host prerequisites from +`xCAT-genesis-base/oe`. Run as an ordinary user with an executable temporary +directory. BitBake's build directory must use a supported local filesystem. +The `genesis_openembedded` workflow runs the metadata tests for x86, x86_64, +ppc64, ppc64le, armv7hf, aarch64, riscv64 and s390x. + +`python3 xCAT-test/native/genesis_console.py` builds and installs the complete +console with Meson, then runs the installed program. It requires a C compiler, +Meson, Ninja, pkg-config, and development packages for Newt and systemd. + +The unit test retains checks of the vendored release key, requested kernel +fragments and console service configuration. The BATS tests retain runtime, +console, hardware and packet behavior. None of these checks replaces a full +image build, inspection of its resolved kernel configuration or a boot test.