diff --git a/.github/workflows/xcat_test.yml b/.github/workflows/xcat_test.yml index 772d0e35c..0d6f08cad 100644 --- a/.github/workflows/xcat_test.yml +++ b/.github/workflows/xcat_test.yml @@ -35,7 +35,10 @@ jobs: workdir=$(sudo mktemp -d /tmp/xcat-script-tests.XXXXXX) trap 'sudo rm -rf -- "$workdir"' EXIT sudo cp -R xCAT-test xCAT-server perl-xCAT xCAT "$workdir/" - sudo sh -c 'cd "$1" && prove -v xCAT-test/native/getinstdisk_selection.t xCAT-test/native/enablekdump_per_node.t xCAT-test/native/sudoer_password_source.t' sh "$workdir" + status=0 + sudo sh -c 'cd "$1" && bats xCAT-test/native/getinstdisk_selection.bats' sh "$workdir" || status=1 + sudo sh -c 'cd "$1" && prove -v xCAT-test/native/getinstdisk_installer.t xCAT-test/native/enablekdump_per_node.t xCAT-test/native/sudoer_password_source.t' sh "$workdir" || status=1 + exit "$status" - name: Prepare xCAT syslog directory if: ${{ !cancelled() }} run: sudo install -d -o syslog -g adm -m 0750 /var/log/xcat diff --git a/xCAT-test/bats/helpers/script_sandbox.bash b/xCAT-test/bats/helpers/script_sandbox.bash index 0397b7c20..c9ed8064d 100644 --- a/xCAT-test/bats/helpers/script_sandbox.bash +++ b/xCAT-test/bats/helpers/script_sandbox.bash @@ -1,6 +1,6 @@ #!/usr/bin/env bash -load 'helpers/shell_source' +load "${BASH_SOURCE[0]%/*}/shell_source.bash" setup_script_sandbox() { diff --git a/xCAT-test/native/getinstdisk_installer.t b/xCAT-test/native/getinstdisk_installer.t new file mode 100644 index 000000000..01c71c8a4 --- /dev/null +++ b/xCAT-test/native/getinstdisk_installer.t @@ -0,0 +1,62 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use FindBin; +use lib "$FindBin::Bin/../lib"; +use lib "$FindBin::Bin/../../perl-xCAT", "$FindBin::Bin/../../xCAT-server/lib/perl"; +use File::Path qw(make_path); +use File::Slurper qw(read_binary write_binary); +use Test::More; +use XCAT::Test::File qw(repo_path); +use XCAT::Test::Sandbox qw(sandbox_root sandbox_run); +use XCAT::Test::Template qw(template_database set_row render_template); + +plan skip_all => 'installer execution requires Linux namespaces' unless $^O eq 'linux'; +my $database = template_database(); +set_row('site', {key => 'nodestatus'}, {value => 0}); +set_row('nodetype', {node => 'node'}, {os => 'rhels10', arch => 'x86_64', provmethod => 'install'}); +write_binary("$database/image.pkglist", ''); +render_template($database, repo_path('xCAT-server/share/xcat/install/scripts/pre.rhels10'), + "$database/pre.sh", 'node', "$database/image.pkglist", '/install/rhels10/x86_64', + 'rh', undef, {xcatmaster => '192.0.2.1'}, osarch => 'x86_64'); + +my $root = sandbox_root(); +make_path("$root/libexec"); +write_binary("$root/partitions", "major minor #blocks name\n\n 8 0 524288000 sda\n 8 16 524288000 sdb\n"); +write_binary("$root/bin/udevadm", <<'SH'); +#!/bin/sh +case "$*" in + 'info --attribute-walk --name=sda') driver=megaraid_sas ;; + 'info --attribute-walk --name=sdb') driver=ahci ;; + 'info --query=property --name='*) printf 'DEVTYPE=disk\n'; exit 0 ;; + *) exit 97 ;; +esac +printf ' ATTRS{size}=="1024000000"\n DRIVERS=="%s"\n' "$driver" +SH +write_binary("$root/bin/find", "#!/bin/sh\nprintf '/usr/bin/awk\\n'\n"); +write_binary("$root/libexec/platform-python", "#!/bin/sh\nexit 0\n"); +chmod 0755, "$root/bin/udevadm", "$root/bin/find", "$root/libexec/platform-python" or die $!; +for my $name (qw(logger vgs vgchange vgremove chown)) { + write_binary("$root/bin/$name", "#!/bin/sh\nexit 0\n"); + chmod 0755, "$root/bin/$name" or die $!; +} +write_binary("$root/bin/mknod", "#!/bin/sh\n: >\"\$1\"\n"); +chmod 0755, "$root/bin/mknod" or die $!; +for my $pass (1 .. 2) { + write_binary("$root/tmp/xcat.install_disk", "/dev/not-selected\n"); + my ($rc, $output) = sandbox_run($root, { + read_only => {"$root/partitions" => '/proc/partitions', + "$root/libexec" => '/usr/libexec', "$database/pre.sh" => '/pre.sh'}, + env => {MASTER_IP => '192.0.2.1', log_label => 'install'}, + }, '/bin/bash', '-c', '. /pre.sh'); + is($rc, 0, "pass $pass completes") or diag($output); + unlike($output, qr/msgutil_r/, "pass $pass has the installer logging helper"); + is(-f "$root/tmp/xcat.install_disk" ? scalar read_binary("$root/tmp/xcat.install_disk") : '', + "/dev/sdb\n", "pass $pass selects the direct disk before RAID"); + ok(!-e "$root/tmp/xcat.getinstalldisk", "pass $pass removes scan state"); + my @rules = grep { /^ignoredisk\b/ } split /\n/, + (-f "$root/tmp/partitionfile" ? scalar read_binary("$root/tmp/partitionfile") : ''); + is($rules[-1], 'ignoredisk --only-use=/dev/sdb', + "pass $pass uses the selected disk in the rendered installer partition plan"); +} +done_testing(); diff --git a/xCAT-test/native/getinstdisk_selection.bats b/xCAT-test/native/getinstdisk_selection.bats new file mode 100644 index 000000000..ccf8b13e9 --- /dev/null +++ b/xCAT-test/native/getinstdisk_selection.bats @@ -0,0 +1,213 @@ +#!/usr/bin/env bats + +load '../bats/helpers/script_sandbox' + +setup() +{ + [ "$(uname -s)" = Linux ] || skip 'Disk selection requires Linux namespaces' + [ "$(id -u)" -eq 0 ] || { echo 'Run as root to create dummy block devices' >&2; return 1; } + setup_script_sandbox cat mkdir rm rmdir sed cut grep awk sort head tail find touch + script="$(repo_path xCAT-server/share/xcat/install/scripts/getinstdisk)" + [ -r "$script" ] + mkdir -p "$fixture"/{props,dev/md,discovery} + printf 'major minor #blocks name\n\n' >"$fixture/partitions" + sandbox+=(--ro-bind "$fixture/partitions" /proc/partitions + --ro-bind "$script" /run/getinstdisk + --setenv MASTER_IP 192.0.2.1 --setenv log_label install) + mv "$fixture/bin/find" "$fixture/find" + mv "$fixture/bin/awk" "$fixture/awk" + cat >"$fixture/bin/find" <<'SH' +#!/bin/sh +case "$1" in + /usr/*) exec /run/fixture/find /run/fixture/discovery -name awk ;; + *) exec /run/fixture/find "$@" ;; +esac +SH + cat >"$fixture/bin/udevadm" <<'SH' +#!/bin/sh +while [ "$#" -gt 0 ]; do + case "$1" in + info) shift ;; + --name=*) name=${1#--name=}; shift ;; + --name|-n) name=$2; shift 2 ;; + --query=*) query=${1#--query=}; shift ;; + --query|-q) query=$2; shift 2 ;; + --attribute-walk|-a) query=attributes; shift ;; + *) exit 97 ;; + esac +done +name=${name#/dev/} +case "$query" in + property) cat "/run/fixture/props/$name.props" ;; + attributes) cat "/run/fixture/props/$name.attrs" ;; + *) exit 97 ;; +esac +SH + chmod +x "$fixture/bin/"{find,udevadm} + command='. /run/getinstdisk' +} + +disk() +{ + local name=$1 driver=${2:-} wwn=${3:-} device_path=${4:-} size=${5:-1024000000} + printf ' 8 0 524288000 %s\n' "$name" >>"$fixture/partitions" + printf 'DEVTYPE=disk\nID_WWN=%s\nDEVPATH=%s\n' "$wwn" "$device_path" >"$fixture/props/$name.props" + printf ' ATTRS{size}=="%s"\n DRIVERS=="%s"\n' "$size" "$driver" >"$fixture/props/$name.attrs" +} + +device() +{ + local name=$1 type=${2:-block} + if [ "$type" = block ]; then + mknod "$fixture/dev/$name" b 240 0 + else + : >"$fixture/dev/$name" + fi + sandbox+=(--ro-bind "$fixture/dev/$name" "/dev/$name") +} + +selects() +{ + local expected=$1 discovery pass + for discovery in present absent; do + if [ "$discovery" = present ]; then + cp "$fixture/awk" "$fixture/bin/awk" + : >"$fixture/discovery/awk" + else + rm "$fixture/discovery/awk" "$fixture/bin/awk" + fi + for pass in 1 2; do + printf '/dev/not-selected\n' >"$fixture/tmp/xcat.install_disk" + run "${sandbox[@]}" timeout --kill-after=5 45 /bin/sh -c "$command" + [ "$status" -eq 0 ] || { echo "$discovery awk, pass $pass: $output"; return 1; } + printf '%s\n' "$expected" >"$fixture/expected-disk" + cmp "$fixture/expected-disk" "$fixture/tmp/xcat.install_disk" + [ ! -e "$fixture/tmp/xcat.getinstalldisk" ] + [[ "$output" != *'not found'* ]] + done + done +} + +@test "direct disks take priority over RAID disks" { + disk sda megaraid_sas + disk sdb ahci + selects /dev/sdb +} + +@test "a RAID disk is selected when no direct disk exists" { + disk sda megaraid_sas + selects /dev/sda +} + +@test "direct disks take priority over SAS disks" { + disk sda mpt3sas + disk sdb ahci + selects /dev/sdb +} + +@test "SAS disks take priority over unclassified drivers" { + disk sda mpt3sas + disk sdb virtio_blk + selects /dev/sda +} + +@test "a driverless NVMe disk can be selected" { + disk nvme0n1 + selects /dev/nvme0n1 +} + +@test "an empty scan falls back to sda" { + selects /dev/sda +} + +@test "driver priority wins over a RAID WWN" { + disk sda megaraid_sas 0x5001 + disk sdb ahci + selects /dev/sdb +} + +@test "driver priority also wins when the direct disk is scanned first" { + disk sda ahci + disk sdb megaraid_sas 0x5002 + selects /dev/sda +} + +@test "a WWN takes priority over a missing identifier within a driver group" { + disk sda ahci + disk sdb ahci 0x5001 + selects /dev/sdb +} + +@test "the lower WWN wins within a driver group" { + disk sda ahci 0x5002 + disk sdb ahci 0x5001 + selects /dev/sdb +} + +@test "a device path takes priority over a missing identifier" { + disk sda ahci '' /devices/pci/block/sda + disk sdb ahci + selects /dev/sda +} + +@test "a scanned Xen disk can be selected" { + disk xvda vbd + selects /dev/xvda +} + +@test "Xen disks follow driver priority" { + disk xvda vbd + disk xvdb ahci + selects /dev/xvdb +} + +@test "small media is ignored even with a preferred driver" { + disk sda ahci '' '' 100 + disk sdb virtio_blk + selects /dev/sdb +} + +@test "the numbered VROC volume is a fallback" { + device md/Volume0_0 + selects /dev/md/Volume0_0 +} + +@test "the unnumbered VROC volume is a fallback" { + device md/Volume0 + selects /dev/md/Volume0 +} + +@test "the numbered VROC volume takes priority over other fallbacks" { + device md/Volume0_0 + device md/Volume0 + device xvda + selects /dev/md/Volume0_0 +} + +@test "a Xen block device is a fallback" { + device xvda + selects /dev/xvda +} + +@test "a scanned disk takes priority over fallback devices" { + disk sdb ahci + device md/Volume0_0 + device xvda + selects /dev/sdb +} + +@test "regular files are not fallback block devices" { + device md/Volume0_0 regular + device md/Volume0 regular + device xvda regular + selects /dev/sda +} + +@test "default selection reports a warning when the installer logger exists" { + command='msgutil_r() { printf "%s\n" "$*" >>/run/fixture/warnings; }; . /run/getinstdisk' + selects /dev/sda + for pass in 1 2 3 4; do + printf '%s\n' '192.0.2.1 warn Disk detection failed, defaulting to /dev/sda /var/log/xcat/xcat.log install' + done >"$fixture/expected-warnings" + cmp "$fixture/expected-warnings" "$fixture/warnings" +} diff --git a/xCAT-test/native/getinstdisk_selection.t b/xCAT-test/native/getinstdisk_selection.t deleted file mode 100644 index 35bc8b1ae..000000000 --- a/xCAT-test/native/getinstdisk_selection.t +++ /dev/null @@ -1,158 +0,0 @@ -#!/usr/bin/env perl -use strict; -use warnings; -use FindBin; -use lib "$FindBin::Bin/../lib"; -use lib "$FindBin::Bin/../../perl-xCAT", "$FindBin::Bin/../../xCAT-server/lib/perl"; -use File::Path qw(make_path); -use File::Slurper qw(read_binary write_binary); -use Test::More; -use XCAT::Test::File qw(repo_path); -use XCAT::Test::Sandbox qw(sandbox_root sandbox_run); -use XCAT::Test::Template qw(template_database set_row render_template); - -plan skip_all => 'disk selection requires Linux namespaces' unless $^O eq 'linux'; -die('run as root to create isolated dummy block devices') if $>; -my $script = repo_path('xCAT-server/share/xcat/install/scripts/getinstdisk'); -die("missing disk selector: $script") unless -r $script; -my $database; - -sub selects { - my ($expected, $name, $disks, %options) = @_; - subtest $name => sub { - my $root = sandbox_root(); - make_path("$root/props", "$root/dev/md"); - my $partitions = "major minor #blocks name\n\n"; - my $minor = 0; - for my $disk (sort keys %$disks) { - my $attr = $disks->{$disk}; - $partitions .= sprintf " 8 %5d 524288000 %s\n", $minor++, $disk; - my $properties = "DEVTYPE=disk\n"; - $properties .= "ID_WWN=$attr->{wwn}\n" if $attr->{wwn}; - $properties .= "DEVPATH=$attr->{path}\n" if $attr->{path}; - write_binary("$root/props/$disk.props", $properties); - my $size = $attr->{size} // 1024000000; - my $attributes = qq{ ATTRS{size}=="$size"\n}; - $attributes .= qq{ DRIVERS=="$attr->{driver}"\n} if $attr->{driver}; - write_binary("$root/props/$disk.attrs", $attributes); - } - write_binary("$root/partitions", $partitions); - write_binary("$root/bin/udevadm", <<'SH'); -#!/bin/sh -name= -query= -while [ "$#" -gt 0 ]; do - case "$1" in - info) shift ;; - --name=*) name=${1#--name=}; shift ;; - --name|-n) name=$2; shift 2 ;; - --query=*) query=${1#--query=}; shift ;; - --query|-q) query=$2; shift 2 ;; - --attribute-walk|-a) query=attributes; shift ;; - *) exit 97 ;; - esac -done -name=${name#/dev/} -case "$query" in - property) cat "/fixture/props/$name.props" ;; - attributes) cat "/fixture/props/$name.attrs" ;; - *) exit 97 ;; -esac -SH - chmod 0755, "$root/bin/udevadm" or die $!; - for my $device (@{$options{blocks} // []}) { - system('mknod', "$root/dev/$device", 'b', '240', '0') == 0 or die "mknod: $?"; - } - write_binary("$root/dev/$_", '') for @{$options{regular} // []}; - my %mounts = ($script => '/getinstdisk', "$root/partitions" => '/proc/partitions'); - $mounts{"$root/dev/$_"} = "/dev/$_" for @{$options{blocks} // []}, @{$options{regular} // []}; - if ($options{no_awk}) { - write_binary("$root/bin/find", "#!/bin/sh\nexit 0\n"); - write_binary("$root/bin/awk", "#!/bin/sh\nprintf called >>/fixture/awk-calls\nexit 97\n"); - chmod 0755, "$root/bin/find", "$root/bin/awk" or die $!; - } - my $command = $options{logger} ? - 'msgutil_r() { printf "%s\n" "$*" >> /fixture/warnings; }; . /getinstdisk' : - '. /getinstdisk'; - if ($options{pre}) { - render_template($database, repo_path('xCAT-server/share/xcat/install/scripts/pre.rhels10'), - "$database/pre.sh", 'node', "$database/image.pkglist", '/install/rhels10/x86_64', - 'rh', undef, {xcatmaster => '192.0.2.1'}, osarch => 'x86_64'); - make_path("$root/libexec"); - write_binary("$root/libexec/platform-python", "#!/bin/sh\nexit 0\n"); - chmod 0755, "$root/libexec/platform-python" or die $!; - $mounts{"$root/libexec"} = '/usr/libexec'; - $mounts{"$database/pre.sh"} = '/pre.sh'; - for my $name (qw(logger vgs vgchange vgremove chown)) { - write_binary("$root/bin/$name", "#!/bin/sh\nexit 0\n"); - chmod 0755, "$root/bin/$name" or die $!; - } - write_binary("$root/bin/mknod", "#!/bin/sh\n: >\"\$1\"\n"); - chmod 0755, "$root/bin/mknod" or die $!; - $command = '. /pre.sh'; - } - for my $pass (1 .. 2) { - write_binary("$root/tmp/xcat.install_disk", "/dev/not-selected\n"); - my ($rc, $output) = sandbox_run($root, {read_only => \%mounts, - env => {MASTER_IP => '192.0.2.1', log_label => 'install'}}, - $options{pre} ? '/bin/bash' : '/bin/sh', '-c', $command); - is($rc, 0, "pass $pass completes") or diag($output); - unlike($output, qr/msgutil_r/, "pass $pass needs no absent installer logger") unless $options{logger}; - is(-f "$root/tmp/xcat.install_disk" ? scalar read_binary("$root/tmp/xcat.install_disk") : '', - "$expected\n", "pass $pass selects the expected disk"); - ok(!-e "$root/tmp/xcat.getinstalldisk", "pass $pass removes scan state"); - ok(!-e "$root/awk-calls", "pass $pass works without calling awk") if $options{no_awk}; - if ($options{pre}) { - my $partition = "$root/tmp/partitionfile"; - my @disk_rules = grep { /^ignoredisk\b/ } split /\n/, - (-f $partition ? scalar read_binary($partition) : ''); - is($disk_rules[-1], "ignoredisk --only-use=$expected", - "pass $pass uses the selected disk in the rendered installer partition plan"); - } - } - if ($options{logger}) { - is(scalar read_binary("$root/warnings"), - "192.0.2.1 warn Disk detection failed, defaulting to /dev/sda /var/log/xcat/xcat.log install\n" x 2, - 'default selection reaches the installer logger'); - } - }; - return; -} - -for my $no_awk (0, 1) { - subtest $no_awk ? 'without awk discovery' : 'with awk discovery' => sub { - my @cases = ( - ['/dev/sdb', 'direct disk before RAID', {sda => {driver => 'megaraid_sas'}, sdb => {driver => 'ahci'}}], - ['/dev/sda', 'RAID alone', {sda => {driver => 'megaraid_sas'}}], - ['/dev/sdb', 'direct disk before SAS', {sda => {driver => 'mpt3sas'}, sdb => {driver => 'ahci'}}], - ['/dev/sda', 'SAS before unclassified driver', {sda => {driver => 'mpt3sas'}, sdb => {driver => 'virtio_blk'}}], - ['/dev/nvme0n1', 'driverless NVMe', {nvme0n1 => {}}], - ['/dev/sda', 'empty scan', {}], - ['/dev/sdb', 'group wins over RAID WWN', {sda => {driver => 'megaraid_sas', wwn => '0x5001'}, sdb => {driver => 'ahci'}}], - ['/dev/sda', 'group wins when scanned first', {sda => {driver => 'ahci'}, sdb => {driver => 'megaraid_sas', wwn => '0x5002'}}], - ['/dev/sdb', 'WWN before no identifier', {sda => {driver => 'ahci'}, sdb => {driver => 'ahci', wwn => '0x5001'}}], - ['/dev/sdb', 'lower WWN', {sda => {driver => 'ahci', wwn => '0x5002'}, sdb => {driver => 'ahci', wwn => '0x5001'}}], - ['/dev/sda', 'path before no identifier', {sda => {driver => 'ahci', path => '/devices/pci/block/sda'}, sdb => {driver => 'ahci'}}], - ['/dev/xvda', 'scanned Xen disk', {xvda => {driver => 'vbd'}}], - ['/dev/xvdb', 'Xen driver ordering', {xvda => {driver => 'vbd'}, xvdb => {driver => 'ahci'}}], - ['/dev/sdb', 'ignore small media', {sda => {driver => 'ahci', size => 100}, sdb => {driver => 'virtio_blk'}}], - ); - selects(@$_, no_awk => $no_awk) for @cases; - }; -} -selects('/dev/md/Volume0_0', 'VROC numbered fallback', {}, blocks => ['md/Volume0_0']); -selects('/dev/md/Volume0', 'VROC unnumbered fallback', {}, blocks => ['md/Volume0']); -selects('/dev/md/Volume0_0', 'VROC preference over other fallbacks', {}, blocks => ['md/Volume0_0', 'md/Volume0', 'xvda']); -selects('/dev/xvda', 'Xen fallback', {}, blocks => ['xvda']); -selects('/dev/sdb', 'scanned disk before fallback devices', {sdb => {driver => 'ahci'}}, blocks => ['md/Volume0_0', 'xvda']); -selects('/dev/sda', 'regular files are not block devices', {}, regular => ['md/Volume0_0', 'md/Volume0', 'xvda']); -selects('/dev/sda', 'default selection with installer logger', {}, logger => 1); - -$database = template_database(); -set_row('site', {key => 'nodestatus'}, {value => 0}); -set_row('nodetype', {node => 'node'}, {os => 'rhels10', arch => 'x86_64', provmethod => 'install'}); -write_binary("$database/image.pkglist", ''); -selects('/dev/sdb', 'rendered RHEL 10 installer disk selection', - {sda => {driver => 'megaraid_sas'}, sdb => {driver => 'ahci'}}, pre => 1); - -done_testing();