From c390570e95a6457ae6af794db7fd6d49e7a4d6d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sun, 23 Aug 2026 20:59:34 -0300 Subject: [PATCH 1/2] fix(mknb): compress the genesis image with xz when lzma is absent The genesis image goes into a file whose name ends with .lzma. The plugin writes that file only when /usr/bin/lzma is there, and it falls back to gzip when it is not. Red Hat ships no lzma binary. On AlmaLinux 9 and on AlmaLinux 10 that test fails, the plugin falls back to gzip, and it gives no message that says why. The image is larger on each run of mknb. Debian and Ubuntu ship lzma as a second name for xz, so those systems still get the smaller image. Ask xz for the same container when lzma is absent. The command "xz --format=lzma" writes the same bytes as "lzma", so the file keeps its name, its container and its size. Keep the gzip fallback for a system that has neither program. Recovered from the lenovobuild branch, which asked xz for the xz container. That container is not the lzma container, and the name of the file says lzma. --- xCAT-server/lib/xcat/plugins/mknb.pm | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/xCAT-server/lib/xcat/plugins/mknb.pm b/xCAT-server/lib/xcat/plugins/mknb.pm index 21253dd49..b385f0698 100644 --- a/xCAT-server/lib/xcat/plugins/mknb.pm +++ b/xCAT-server/lib/xcat/plugins/mknb.pm @@ -246,6 +246,13 @@ sub _install_prebuilt_genesis { return ("$destination_dir/genesis.fs.$arch.gz", undef); } +sub genesis_lzma_command { + my ($have_lzma, $have_xz) = @_; + return 'lzma -C crc32 -9' if $have_lzma; + return 'xz --format=lzma -C crc32 -9' if $have_xz; + return; +} + sub process_request { my $request = shift; my $callback = shift; @@ -484,9 +491,10 @@ sub process_request { # place, so concurrent mknb runs sharing $tftpdir cannot read or clobber # a half-written genesis.fs. my $suffix = xCAT::Utils::genpassword(24); - if (-x "/usr/bin/lzma") { #let's reclaim some of that size... + my $lzma_command = genesis_lzma_command(-x "/usr/bin/lzma", -x "/usr/bin/xz"); + if ($lzma_command) { #let's reclaim some of that size... $callback->({ data => ["Creating genesis.fs.$arch.lzma in $tftpdir/xcat"] }); - system("cd $tempdir; find . | cpio -o -H newc | lzma -C crc32 -9 > $tftpdir/xcat/genesis.fs.$arch.lzma.$suffix"); + system("cd $tempdir; find . | cpio -o -H newc | $lzma_command > $tftpdir/xcat/genesis.fs.$arch.lzma.$suffix"); $lzma_exit_value = $? >> 8; if ($lzma_exit_value) { $callback->({ data => ["Creating genesis.fs.$arch.lzma in $tftpdir/xcat failed, falling back to gzip"] }); From 73b145d7ffab5c9b3bb9e4cbafd35a317c1950fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sun, 23 Aug 2026 21:00:15 -0300 Subject: [PATCH 2/2] test(mknb): pin which program compresses the genesis image Add a unit test for the routine that chooses the compression program. The test lifts the routine out of the plugin source, because the plugin needs a management node to load. The test shows that lzma is used when it is there, that xz stands in when it is not, and that xz is asked for the lzma container rather than its own. It also shows that the caller takes the command from the routine, that the file keeps its name and its suffix, and that the gzip fallback and the rename into place both remain. --- xCAT-test/unit/mknb_genesis_compressor.t | 54 ++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 xCAT-test/unit/mknb_genesis_compressor.t diff --git a/xCAT-test/unit/mknb_genesis_compressor.t b/xCAT-test/unit/mknb_genesis_compressor.t new file mode 100644 index 000000000..529f2b74a --- /dev/null +++ b/xCAT-test/unit/mknb_genesis_compressor.t @@ -0,0 +1,54 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use FindBin; +use File::Spec; +use Test::More; + +my $plugin = File::Spec->catfile( $FindBin::Bin, '..', '..', + 'xCAT-server', 'lib', 'xcat', 'plugins', 'mknb.pm' ); +plan skip_all => 'mknb.pm not found' unless -r $plugin; + +open( my $fh, '<', $plugin ) or die "Unable to read $plugin: $!"; +my $source = do { local $/; <$fh> }; +close($fh); + +# 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: $@"); + +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' ); + +done_testing();