2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-10-21 23:28:19 +00:00

Merge pull request #7925 from VersatusHPC/test/r26-genesis-behavior

test(genesis): exercise startup and OpenEmbedded build contracts
This commit is contained in:
Daniel Hilst
2026-10-13 10:16:23 -03:00
committed by GitHub
11 changed files with 911 additions and 1156 deletions
+10 -2
View File
@@ -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
+14
View File
@@ -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.
+40 -92
View File
@@ -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: <BROADCAST,MULTICAST,UP,LOWER_UP> 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: <BROADCAST,MULTICAST,UP,LOWER_UP> 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
}
+196
View File
@@ -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
}
+141
View File
@@ -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 </run/fixture/dhcp)" -lt "$expected" ] && [ "$tries" -lt 1000 ]; do
/run/fixture/real/sleep 0.01
tries=$((tries + 1))
done
SH
printf '#!/bin/sh\nprintf "runcmd=exit 0\\n"\n' >"$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; }
}
@@ -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 </dev/null
if [ "$status" -ne "$expected_status" ]; then
printf 'Expected exit %s, got %s\n%s\n' "$expected_status" "$status" "$output" >&2
return 1
fi
}
+33
View File
@@ -103,3 +103,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.
+42
View File
@@ -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)
+363
View File
@@ -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)
+17 -589
View File
@@ -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();
-473
View File
@@ -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();