From 35d01515eb61bfc04d6aa64d5d91df9df6debefe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:05:52 -0300 Subject: [PATCH] test(mknb): exercise compression and archive publication --- .github/workflows/xcat_test.yml | 2 +- xCAT-test/bats/fixtures/mknb-compress.pl | 26 +++++++ xCAT-test/bats/mknb_genesis_compression.bats | 50 +++++++++++++ xCAT-test/unit/mknb_genesis_compressor.t | 77 +++++++++----------- 4 files changed, 111 insertions(+), 44 deletions(-) create mode 100644 xCAT-test/bats/fixtures/mknb-compress.pl create mode 100644 xCAT-test/bats/mknb_genesis_compression.bats diff --git a/.github/workflows/xcat_test.yml b/.github/workflows/xcat_test.yml index fc30f70a8..6949eaa8b 100644 --- a/.github/workflows/xcat_test.yml +++ b/.github/workflows/xcat_test.yml @@ -9,7 +9,7 @@ jobs: - name: Refresh package indexes run: sudo apt-get update - name: Install dependencies - run: sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends --no-install-suggests bats bubblewrap cpio gawk mawk rpm rsyslog build-essential fakeroot reprepro devscripts debhelper libcapture-tiny-perl libfile-slurper-perl libjson-perl libyaml-pp-perl libparallel-forkmanager-perl libsoap-lite-perl libdbi-perl libdbd-sqlite3-perl libcgi-pm-perl quilt openssh-server dpkg looptools genometools software-properties-common grub-common grub-efi-amd64-bin squashfs-tools pigz + run: sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends --no-install-suggests bats bubblewrap cpio xz-utils gzip gawk mawk rpm rsyslog build-essential fakeroot reprepro devscripts debhelper libcapture-tiny-perl libfile-slurper-perl libjson-perl libyaml-pp-perl libparallel-forkmanager-perl libsoap-lite-perl libdbi-perl libdbd-sqlite3-perl libcgi-pm-perl quilt openssh-server dpkg looptools genometools software-properties-common grub-common grub-efi-amd64-bin squashfs-tools pigz - name: Enable postscript test namespaces run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 - name: Install lifecycle test dependencies diff --git a/xCAT-test/bats/fixtures/mknb-compress.pl b/xCAT-test/bats/fixtures/mknb-compress.pl new file mode 100644 index 000000000..c03593d54 --- /dev/null +++ b/xCAT-test/bats/fixtures/mknb-compress.pl @@ -0,0 +1,26 @@ +use strict; +use warnings; +use FindBin; +use lib "$FindBin::Bin/../../../perl-xCAT"; +use lib "$FindBin::Bin/../../../xCAT-server/lib/perl"; +use JSON qw(encode_json); + +$ENV{XCATROOT} = "$FindBin::Bin/../../../xCAT-server"; +$ENV{XCATCFG} = '/tmp/fixture/config'; +my $plugin = "$FindBin::Bin/../../../xCAT-server/lib/xcat/plugins/mknb.pm"; +require $plugin; +$::XCATROOT = '/tmp/fixture/source'; + +no warnings qw(redefine once); +local *xCAT::TableUtils::getTftpDir = sub { return '/tmp/fixture/tftp'; }; +local *xCAT::TableUtils::get_site_attribute = sub { return; }; +local *xCAT::NetworkUtils::my_nets = sub { return {}; }; +local *xCAT::NetworkUtils::my_hexnets = sub { return {}; }; + +my @responses; +umask oct($ARGV[0]); +xCAT_plugin::mknb::process_request( + { arg => ['x86_64'] }, sub { push @responses, @_; }, +); +print encode_json(\@responses), "\n"; +exit((grep { $_->{error} || $_->{errorcode} } @responses) ? 1 : 0); diff --git a/xCAT-test/bats/mknb_genesis_compression.bats b/xCAT-test/bats/mknb_genesis_compression.bats new file mode 100644 index 000000000..951676b2a --- /dev/null +++ b/xCAT-test/bats/mknb_genesis_compression.bats @@ -0,0 +1,50 @@ +#!/usr/bin/env bats + +setup() +{ + [ "$(uname -s)" = Linux ] || skip 'Genesis compression requires Linux namespaces' + fixture="$BATS_TEST_TMPDIR/build" + [ -n "$BATS_TEST_TMPDIR" ] || return 1 + mkdir -p "$fixture"/{source/share/xcat/netboot/genesis/x86_64/fs,root/.ssh,etc/xcat,config,tftp,bin,unpacked} + printf 'fixture payload\n' > "$fixture/source/share/xcat/netboot/genesis/x86_64/fs/payload" + printf 'fixture kernel\n' > "$fixture/source/share/xcat/netboot/genesis/x86_64/kernel" + printf 'fixture public key\n' > "$fixture/root/.ssh/id_rsa.pub" +} + +build_image() +{ + bwrap --die-with-parent --unshare-net --ro-bind / / --dev /dev --proc /proc \ + --tmpfs /tmp --ro-bind "$BATS_TEST_DIRNAME/../.." /tmp/source \ + --bind "$fixture" /tmp/fixture --bind "$fixture/root" /root \ + --bind "$fixture/etc" /etc --chdir /tmp/fixture \ + --setenv PATH /tmp/fixture/bin:/usr/bin:/bin \ + perl /tmp/source/xCAT-test/bats/fixtures/mknb-compress.pl "$1" +} + +check_payload() +{ + local image="$1" compression="$2" mode="$3" + [ "$(stat -c '%a' "$image")" = "$mode" ] + (cd "$fixture/unpacked" && "$compression" -dc "$image" | cpio -id) + [ "$(cat "$fixture/unpacked/payload")" = 'fixture payload' ] + [ "$(cat "$fixture/unpacked/.ssh/authorized_keys")" = 'fixture public key' ] + [ "$(stat -c '%a' "$fixture/unpacked/.ssh/authorized_keys")" = 600 ] + [ "$(cat "$fixture/tftp/xcat/genesis.kernel.x86_64")" = 'fixture kernel' ] + [ "$(find "$fixture/tftp/xcat" -maxdepth 1 -type f | wc -l)" -eq 2 ] +} + +@test "mknb publishes a complete LZMA image with the caller's umask" { + run build_image 022 + [ "$status" -eq 0 ] || { printf '%s\n' "$output"; return 1; } + check_payload "$fixture/tftp/xcat/genesis.fs.x86_64.lzma" xz 644 +} + +@test "mknb falls back to gzip and removes the failed LZMA staging file" { + printf '#!/bin/sh\nprintf partial\nexit 42\n' > "$fixture/bin/lzma" + cp "$fixture/bin/lzma" "$fixture/bin/xz" + chmod +x "$fixture/bin/lzma" "$fixture/bin/xz" + run build_image 027 + [ "$status" -eq 0 ] || { printf '%s\n' "$output"; return 1; } + [[ "$output" == *'failed, falling back to gzip'* ]] + check_payload "$fixture/tftp/xcat/genesis.fs.x86_64.gz" gzip 640 +} diff --git a/xCAT-test/unit/mknb_genesis_compressor.t b/xCAT-test/unit/mknb_genesis_compressor.t index 529f2b74a..61834b2d9 100644 --- a/xCAT-test/unit/mknb_genesis_compressor.t +++ b/xCAT-test/unit/mknb_genesis_compressor.t @@ -1,54 +1,45 @@ #!/usr/bin/env perl use strict; use warnings; - use FindBin; -use File::Spec; +use lib "$FindBin::Bin/../lib"; +use lib "$FindBin::Bin/../../perl-xCAT"; +use lib "$FindBin::Bin/../../xCAT-server/lib/perl"; +use File::Path qw(make_path); +use File::Temp qw(tempdir); +use File::Slurper qw(read_binary write_text); use Test::More; +use XCAT::Test::File qw(repo_path); -my $plugin = File::Spec->catfile( $FindBin::Bin, '..', '..', - 'xCAT-server', 'lib', 'xcat', 'plugins', 'mknb.pm' ); -plan skip_all => 'mknb.pm not found' unless -r $plugin; +my $root = tempdir(CLEANUP => 1); +$ENV{XCATROOT} = repo_path('xCAT-server'); +$ENV{XCATCFG} = "$root/config"; +make_path($ENV{XCATCFG}); +require(repo_path('xCAT-server/lib/xcat/plugins/mknb.pm')); -open( my $fh, '<', $plugin ) or die "Unable to read $plugin: $!"; -my $source = do { local $/; <$fh> }; -close($fh); +# xz supports the lzma invocation name even where the distribution omits that link. +symlink('/usr/bin/xz', "$root/lzma") or die "create lzma alias: $!"; +local $ENV{PATH} = "$root:$ENV{PATH}"; -# mknb.pm needs a management node to load, so lift the routine out and drive -# the real code on its own. -my ($routine) = $source =~ /(sub genesis_lzma_command \{.*?\n\}\n)/s; -BAIL_OUT('could not extract genesis_lzma_command from mknb.pm') unless $routine; -eval "package MknbCompressor; $routine 1;" or BAIL_OUT("could not evaluate: $@"); +write_text("$root/payload", "Genesis payload\n"); +is(system("cd '$root' && printf 'payload\\n' | cpio -o -H newc > archive.cpio"), 0, + 'builds an actual newc archive'); +my $archive = read_binary("$root/archive.cpio"); +like($archive, qr/^070701/, 'fixture is a newc archive'); -sub command { return MknbCompressor::genesis_lzma_command(@_); } - -# Debian and Ubuntu ship both names, and lzma is the one the plugin has always -# used, so nothing changes on those systems. -is( command( 1, 1 ), 'lzma -C crc32 -9', 'lzma is used when it is there' ); - -# Red Hat ships xz alone. -is( command( 0, 1 ), 'xz --format=lzma -C crc32 -9', - 'xz stands in for lzma when only xz is there' ); - -# The gzip path below the caller handles a system with neither. -is( command( 0, 0 ), undef, 'nothing is returned when neither program is there' ); - -# The container has to stay the one the file name promises. "xz" on its own -# writes the xz container, which the file name does not describe and which a -# reader of a .lzma file cannot open. -my ($xz_form) = command( 0, 1 ) =~ /--format=(\S+)/; -is( $xz_form, 'lzma', 'xz is asked for the lzma container, not its own' ); - -# The caller has to take the command from the routine, and the name of the -# written file must not change with it. -like( $source, qr/genesis_lzma_command\(-x "\/usr\/bin\/lzma", -x "\/usr\/bin\/xz"\)/, - 'the caller asks the routine which program to run' ); -like( $source, qr/cpio -o -H newc \| \$lzma_command > \$tftpdir\/xcat\/genesis\.fs\.\$arch\.lzma\.\$suffix/, - 'the chosen command writes the same file under the same suffix' ); - -# The fallback to gzip and the atomic rename both have to survive. -like( $source, qr/falling back to gzip/, 'the gzip fallback is still reported' ); -like( $source, qr/move\("\$tftpdir\/xcat\/genesis\.fs\.\$arch\.lzma\.\$suffix"/, - 'the finished image is still renamed into place' ); +for my $case ([1, 1, 'lzma'], [1, 0, 'lzma'], [0, 1, 'xz']) { + my ($lzma, $xz, $name) = @$case; + subtest "$name with lzma=$lzma xz=$xz" => sub { + my $command = xCAT_plugin::mknb::genesis_lzma_command($lzma, $xz); + like($command, qr/^\Q$name\E /, 'selects the available compressor'); + is(system("$command < '$root/archive.cpio' > '$root/archive.lzma'"), 0, + 'selected command compresses the archive'); + is(system("xz --format=lzma -dc '$root/archive.lzma' > '$root/decoded.cpio'"), 0, + 'output has the promised LZMA container'); + is(read_binary("$root/decoded.cpio"), $archive, 'decompression preserves every archive byte'); + }; +} +is(xCAT_plugin::mknb::genesis_lzma_command(0, 0), undef, + 'no LZMA command is selected when neither compressor is available'); done_testing();