diff --git a/docs/source/guides/admin-guides/references/man8/mknb.8.rst b/docs/source/guides/admin-guides/references/man8/mknb.8.rst index 3f166a58b..0aafa64ae 100644 --- a/docs/source/guides/admin-guides/references/man8/mknb.8.rst +++ b/docs/source/guides/admin-guides/references/man8/mknb.8.rst @@ -34,6 +34,8 @@ for the same architecture that the management node is. So you normally do not n If you make custom changes to the network boot root image, you will need to run \ **mknb**\ again to regenerate the diskless image to include your changes. If you have an xCAT Hierarchical Cluster with Service Nodes having local /tftpboot directories (site.sharedtftp=0), you will need to copy the generated root image to each Service Node. +An exported Genesis image is identified by ``xcat-genesis.manifest``, which records its format version and architecture. The directory also contains ``kernel``, ``initramfs.cpio.gz``, and ``SHA256SUMS`` in ``/opt/xcat/share/xcat/netboot/genesis/ARCH``. \ **mknb**\ verifies the manifest and both boot files before publishing them under the configured TFTP root. An incomplete or invalid export leaves the current boot files unchanged. The legacy ``fs/`` layout remains supported and is packed locally. + When multiple IPv4 addresses are configured for the same network, \ **mknb**\ uses a locally assigned ``site.master`` for the xcatd endpoint, or the first address reported by the operating system when ``site.master`` is not local. POWER discovery configurations also use this address for their kernel and initrd URLs. Presently, the architectures x86_64 and ppc64 are supported. For ppc64le, use the ppc64 architecture. @@ -73,4 +75,3 @@ SEE ALSO makedhcp(8)|makedhcp.8 - diff --git a/xCAT-client/pods/man8/mknb.8.pod b/xCAT-client/pods/man8/mknb.8.pod index c8bb98b34..422a02a2a 100644 --- a/xCAT-client/pods/man8/mknb.8.pod +++ b/xCAT-client/pods/man8/mknb.8.pod @@ -16,6 +16,8 @@ B command yourself. If you make custom changes to the network boot root image, you will need to run B again to regenerate the diskless image to include your changes. If you have an xCAT Hierarchical Cluster with Service Nodes having local /tftpboot directories (site.sharedtftp=0), you will need to copy the generated root image to each Service Node. +An exported Genesis image is identified by C, which records its format version and architecture. The directory also contains C, C, and C in C. B verifies the manifest and both boot files before publishing them under the configured TFTP root. An incomplete or invalid export leaves the current boot files unchanged. The legacy C layout remains supported and is packed locally. + When multiple IPv4 addresses are configured for the same network, B uses a locally assigned C for the xcatd endpoint, or the first address reported by the operating system when C is not local. POWER discovery configurations also use this address for their kernel and initrd URLs. Presently, the architectures x86_64 and ppc64 are supported. For ppc64le, use the ppc64 architecture. @@ -46,4 +48,3 @@ An error has occurred. =head1 SEE ALSO L - diff --git a/xCAT-server/lib/xcat/plugins/mknb.pm b/xCAT-server/lib/xcat/plugins/mknb.pm index 9b0872825..21253dd49 100644 --- a/xCAT-server/lib/xcat/plugins/mknb.pm +++ b/xCAT-server/lib/xcat/plugins/mknb.pm @@ -1,5 +1,6 @@ package xCAT_plugin::mknb; use strict; +use Digest::SHA (); use File::Temp qw(tempdir); use xCAT::Utils; use xCAT::TableUtils; @@ -7,6 +8,8 @@ use xCAT::NodeRange; use File::Path; use File::Copy; +my $GENESIS_EXPORT_MANIFEST = 'xcat-genesis.manifest'; + sub handled_commands { return { mknb => 'mknb', @@ -35,6 +38,214 @@ sub _select_network_addresses { return (\%legacy, \%selected); } +sub _genesis_export_manifest_present { + my ($directory) = @_; + my $manifest = "$directory/$GENESIS_EXPORT_MANIFEST"; + return -e $manifest || -l $manifest; +} + +sub _prebuilt_genesis_requested { + my ($directory) = @_; + foreach my $name ( + $GENESIS_EXPORT_MANIFEST, + 'kernel', + 'initramfs.cpio.gz', + 'SHA256SUMS' + ) + { + my $path = "$directory/$name"; + return 0 unless -e $path || -l $path; + } + return 1; +} + +sub _validate_prebuilt_genesis_manifest { + my ($directory, $arch) = @_; + my $manifest = "$directory/$GENESIS_EXPORT_MANIFEST"; + return "Missing Genesis export manifest: $manifest" + unless -f $manifest && !-l $manifest; + + open(my $manifest_fh, '<:raw', $manifest) + or return "Unable to read Genesis export manifest: $manifest"; + + my %expected = ( + format => 'xcat-genesis', + version => '1', + architecture => $arch, + ); + my %values; + while (my $line = <$manifest_fh>) { + chomp($line); + unless ($line =~ /^([a-z][a-z0-9_-]*)=([A-Za-z0-9][A-Za-z0-9._+-]*)$/) { + close($manifest_fh); + return "Invalid Genesis export manifest entry: $line"; + } + my ($name, $value) = ($1, $2); + unless (exists($expected{$name})) { + close($manifest_fh); + return "Unknown Genesis export manifest entry: $name"; + } + if (exists($values{$name})) { + close($manifest_fh); + return "Duplicate Genesis export manifest entry: $name"; + } + $values{$name} = $value; + } + close($manifest_fh); + + foreach my $name (qw(format version architecture)) { + return "Missing Genesis export manifest entry: $name" + unless exists($values{$name}); + return "Unsupported Genesis export $name: $values{$name}" + unless $values{$name} eq $expected{$name}; + } + + return; +} + +sub _read_prebuilt_genesis_checksums { + my ($directory) = @_; + my $checksum_file = "$directory/SHA256SUMS"; + return (undef, "Missing Genesis checksum file: $checksum_file") + unless -f $checksum_file && !-l $checksum_file; + + open(my $checksum_fh, '<:raw', $checksum_file) + or return (undef, "Unable to read Genesis checksum file: $checksum_file"); + + my %expected; + while (my $line = <$checksum_fh>) { + chomp($line); + unless ($line =~ /^([0-9a-f]{64}) ([A-Za-z0-9][A-Za-z0-9._-]*)$/) { + close($checksum_fh); + return (undef, "Invalid Genesis checksum entry: $line"); + } + my ($digest, $name) = ($1, $2); + if (exists($expected{$name})) { + close($checksum_fh); + return (undef, "Duplicate Genesis checksum entry: $name"); + } + $expected{$name} = $digest; + } + close($checksum_fh); + + return (\%expected, undef); +} + +sub _sha256_file { + my ($path) = @_; + open(my $artifact_fh, '<:raw', $path) + or return (undef, "Unable to read Genesis artifact: $path"); + my $digest = Digest::SHA->new(256)->addfile($artifact_fh)->hexdigest; + close($artifact_fh); + return ($digest, undef); +} + +sub _install_prebuilt_genesis { + my ($source, $tftpdir, $arch) = @_; + return (undef, "Invalid Genesis export directory: $source") + unless -d $source && !-l $source; + + my $manifest_error = + _validate_prebuilt_genesis_manifest($source, $arch); + return (undef, $manifest_error) if $manifest_error; + + my ($expected, $checksum_error) = + _read_prebuilt_genesis_checksums($source); + return (undef, $checksum_error) if $checksum_error; + + unless (exists($expected->{$GENESIS_EXPORT_MANIFEST})) { + return (undef, + "Missing Genesis checksum entry: $GENESIS_EXPORT_MANIFEST"); + } + my ($manifest_digest, $manifest_digest_error) = + _sha256_file("$source/$GENESIS_EXPORT_MANIFEST"); + return (undef, $manifest_digest_error) if $manifest_digest_error; + unless ($manifest_digest eq $expected->{$GENESIS_EXPORT_MANIFEST}) { + return (undef, + "Genesis checksum mismatch: $source/$GENESIS_EXPORT_MANIFEST"); + } + + my $destination_dir = "$tftpdir/xcat"; + eval { mkpath($destination_dir) unless -d $destination_dir; }; + return (undef, "Unable to create Genesis destination: $destination_dir") + unless -d $destination_dir; + + my $suffix = xCAT::Utils::genpassword(24); + my @artifacts = ( + [ 'kernel', "$destination_dir/genesis.kernel.$arch" ], + [ 'initramfs.cpio.gz', "$destination_dir/genesis.fs.$arch.gz" ], + ); + my @staged; + + foreach my $artifact (@artifacts) { + my ($name, $destination) = @{$artifact}; + my $source_path = "$source/$name"; + unless (-f $source_path && !-l $source_path) { + unlink(@staged); + return (undef, "Missing Genesis artifact: $source_path"); + } + unless (exists($expected->{$name})) { + unlink(@staged); + return (undef, "Missing Genesis checksum entry: $name"); + } + + my $temporary = "$destination.$suffix.new"; + unless (copy($source_path, $temporary) && chmod(0644, $temporary)) { + unlink(@staged, $temporary); + return (undef, "Unable to stage Genesis artifact: $source_path"); + } + push(@staged, $temporary); + + my ($digest, $digest_error) = _sha256_file($temporary); + if ($digest_error || $digest ne $expected->{$name}) { + unlink(@staged); + return (undef, $digest_error || "Genesis checksum mismatch: $source_path"); + } + } + + my %backups; + foreach my $artifact (@artifacts) { + my $destination = $artifact->[1]; + next unless -e $destination || -l $destination; + unless (-f $destination && !-l $destination) { + unlink(@staged, values(%backups)); + return (undef, "Invalid Genesis destination: $destination"); + } + + my $backup = "$destination.$suffix.old"; + unless (copy($destination, $backup)) { + unlink(@staged, values(%backups)); + return (undef, "Unable to preserve Genesis artifact: $destination"); + } + $backups{$destination} = $backup; + } + + my @installed; + foreach my $index (0 .. $#artifacts) { + my $destination = $artifacts[$index]->[1]; + unless (rename($staged[$index], $destination)) { + my @rollback_errors; + foreach my $installed (reverse(@installed)) { + if ($backups{$installed}) { + push(@rollback_errors, $installed) + unless rename($backups{$installed}, $installed); + } else { + push(@rollback_errors, $installed) unless unlink($installed); + } + } + unlink(@staged[$index .. $#staged], values(%backups)); + my $error = "Unable to install Genesis artifact: $destination"; + $error .= "; unable to restore: " . join(', ', @rollback_errors) + if @rollback_errors; + return (undef, $error); + } + push(@installed, $destination); + } + + unlink(values(%backups)); + return ("$destination_dir/genesis.fs.$arch.gz", undef); +} + sub process_request { my $request = shift; my $callback = shift; @@ -133,7 +344,8 @@ sub process_request { $request->{arg}->[0] = $arch; } - unless (-d "$::XCATROOT/share/xcat/netboot/$arch" or -d "$::XCATROOT/share/xcat/netboot/genesis/$arch") { + my $genesis_dir = "$::XCATROOT/share/xcat/netboot/genesis/$arch"; + unless (-d "$::XCATROOT/share/xcat/netboot/$arch" or -d $genesis_dir) { $callback->({ error => "Unable to find directory $::XCATROOT/share/xcat/netboot/$arch or $::XCATROOT/share/xcat/netboot/genesis/$arch", errorcode => [1] }); return; } @@ -144,6 +356,25 @@ sub process_request { } elsif ($configfileonly) { goto CREAT_CONF_FILE; } + if (_prebuilt_genesis_requested($genesis_dir)) { + $callback->({ data => ["Installing exported Genesis image for $arch"] }); + my ($installed_initrd, $install_error) = + _install_prebuilt_genesis($genesis_dir, $tftpdir, $arch); + if ($install_error) { + $callback->({ error => [$install_error], errorcode => [1] }); + return; + } + $initrd_file = $installed_initrd; + $invisibletouch = 1; + goto CREAT_CONF_FILE; + } + if (_genesis_export_manifest_present($genesis_dir)) { + $callback->({ + error => ["Incomplete Genesis export: $genesis_dir"], + errorcode => [1], + }); + return; + } # Grab all the standard ssh public keys we can my @ssh_pub_keys = (); if (-r "/root/.ssh/id_rsa.pub") { diff --git a/xCAT-server/xCAT-server.spec b/xCAT-server/xCAT-server.spec index 8074a5a4d..2c4f4b1a6 100644 --- a/xCAT-server/xCAT-server.spec +++ b/xCAT-server/xCAT-server.spec @@ -38,7 +38,7 @@ BuildArch: noarch # Note: ifarch/ifnarch does not work for noarch package, use environment variable instead %if %s390x -Requires: perl-IO-Socket-SSL perl-XML-Simple perl-XML-Parser +Requires: perl-IO-Socket-SSL perl-XML-Simple perl-XML-Parser perl(Digest::SHA) %else %if 0%{?rhel} >= 8 BuildRequires: perl-generators @@ -46,7 +46,7 @@ BuildRequires: perl-generators %if 0%{?fedora} BuildRequires: perl-generators %endif -Requires: perl-IO-Socket-SSL perl-XML-Simple perl-XML-Parser perl-Digest-SHA1 perl(LWP::Protocol::https) perl-XML-LibXML +Requires: perl-IO-Socket-SSL perl-XML-Simple perl-XML-Parser perl-Digest-SHA1 perl(Digest::SHA) perl(LWP::Protocol::https) perl-XML-LibXML # AutoReqProv is off (above). Modules used only by xCAT-server code are # therefore not auto-required, and (unlike SNMP, Expect, JSON, Net::Ping, # Time::HiRes and Text::Balanced) are not pulled in transitively by perl-xCAT diff --git a/xCAT-test/unit/mknb_exported_genesis.t b/xCAT-test/unit/mknb_exported_genesis.t new file mode 100644 index 000000000..c864b4a2e --- /dev/null +++ b/xCAT-test/unit/mknb_exported_genesis.t @@ -0,0 +1,335 @@ +#!/usr/bin/env perl +use strict; +use warnings; +## no critic (Modules::RequireFilenameMatchesPackage, TestingAndDebugging::ProhibitNoStrict, TestingAndDebugging::ProhibitNoWarnings) + +use Digest::SHA qw(sha256_hex); +use File::Path qw(make_path); +use File::Temp qw(tempdir); +use FindBin; +use Test::More; + +BEGIN { + package xCAT::Utils; + sub genpassword { return 'test-token'; } + $INC{'xCAT/Utils.pm'} = __FILE__; + + package xCAT::TableUtils; + our ($tftpdir, $site_master); + sub getTftpDir { return $tftpdir; } + sub get_site_attribute { + my $attribute = $_[-1]; + return ($site_master) if $attribute eq 'master' && defined($site_master); + return; + } + $INC{'xCAT/TableUtils.pm'} = __FILE__; + + package xCAT::NetworkUtils; + our ($normnet_addresses, $hexnet_addresses, @master_addresses); + sub my_nets { return $normnet_addresses; } + sub my_hexnets { return $hexnet_addresses; } + sub getipaddr { return @master_addresses; } + $INC{'xCAT/NetworkUtils.pm'} = __FILE__; + + package xCAT::NodeRange; + sub import { + my $caller = caller; + no strict 'refs'; + *{"${caller}::noderange"} = sub { return; }; + } + $INC{'xCAT/NodeRange.pm'} = __FILE__; +} + +my $source_mknb_plugin = + "$FindBin::Bin/../../xCAT-server/lib/xcat/plugins/mknb.pm"; +if (-f $source_mknb_plugin) { + require $source_mknb_plugin; +} else { + require xCAT_plugin::mknb; +} + +sub write_file { + my ($filename, $content) = @_; + open(my $fh, '>:raw', $filename) or die "Unable to write $filename: $!"; + print {$fh} $content; + close($fh) or die "Unable to close $filename: $!"; +} + +sub read_file { + my ($filename) = @_; + open(my $fh, '<:raw', $filename) or die "Unable to read $filename: $!"; + my $content = do { local $/; <$fh> }; + close($fh); + return $content; +} + +sub export_manifest { + my ($architecture) = @_; + return "format=xcat-genesis\n" + . "version=1\n" + . "architecture=$architecture\n"; +} + +sub prepare_export { + my ($directory, $kernel, $initramfs, $architecture) = @_; + $architecture //= 'x86_64'; + make_path($directory); + write_file("$directory/kernel", $kernel); + write_file("$directory/initramfs.cpio.gz", $initramfs); + my $manifest = export_manifest($architecture); + write_file("$directory/xcat-genesis.manifest", $manifest); + write_file( + "$directory/SHA256SUMS", + sha256_hex($initramfs) . " initramfs.cpio.gz\n" + . sha256_hex($kernel) . " kernel\n" + . sha256_hex($manifest) . " xcat-genesis.manifest\n", + ); +} + +sub published_files { + my ($tftpdir, $arch) = @_; + return ( + "$tftpdir/xcat/genesis.kernel.$arch", + "$tftpdir/xcat/genesis.fs.$arch.gz", + ); +} + +sub temporary_files { + my ($directory) = @_; + opendir(my $dir_fh, $directory) or return; + my @files = grep { /\.test-token\.(?:new|old)$/ } readdir($dir_fh); + closedir($dir_fh); + return @files; +} + +my $tmpdir = tempdir(CLEANUP => 1); +my $export = "$tmpdir/export"; +my $tftpdir = "$tmpdir/tftpboot"; +prepare_export($export, 'new kernel', 'new initramfs'); + +ok( + xCAT_plugin::mknb::_prebuilt_genesis_requested($export), + 'an export manifest selects the prebuilt path', +); + +my ($installed_initrd, $install_error) = + xCAT_plugin::mknb::_install_prebuilt_genesis($export, $tftpdir, 'x86_64'); +is($install_error, undef, 'a valid export installs without an error'); +is( + $installed_initrd, + "$tftpdir/xcat/genesis.fs.x86_64.gz", + 'the installer returns the published initramfs path', +); +my ($published_kernel, $published_initramfs) = + published_files($tftpdir, 'x86_64'); +is(read_file($published_kernel), 'new kernel', 'the kernel is published'); +is(read_file($published_initramfs), 'new initramfs', 'the initramfs is published'); +is(sprintf('%04o', (stat($published_kernel))[2] & oct('7777')), '0644', 'the kernel is readable by TFTP'); +is(sprintf('%04o', (stat($published_initramfs))[2] & oct('7777')), '0644', 'the initramfs is readable by TFTP'); +is_deeply([temporary_files("$tftpdir/xcat")], [], 'staging files are removed'); + +write_file($published_kernel, 'current kernel'); +write_file($published_initramfs, 'current initramfs'); +write_file("$export/kernel", 'corrupt kernel'); +($installed_initrd, $install_error) = + xCAT_plugin::mknb::_install_prebuilt_genesis($export, $tftpdir, 'x86_64'); +like($install_error, qr/Genesis checksum mismatch/, 'a checksum mismatch is rejected'); +is(read_file($published_kernel), 'current kernel', 'a bad export keeps the current kernel'); +is(read_file($published_initramfs), 'current initramfs', 'a bad export keeps the current initramfs'); +is_deeply([temporary_files("$tftpdir/xcat")], [], 'a failed install removes staging files'); + +my $unmarked_export = "$tmpdir/unmarked"; +prepare_export($unmarked_export, 'kernel', 'initramfs'); +unlink("$unmarked_export/xcat-genesis.manifest"); +ok( + !xCAT_plugin::mknb::_prebuilt_genesis_requested($unmarked_export), + 'boot artifacts without an export manifest do not select the prebuilt path', +); +(undef, $install_error) = xCAT_plugin::mknb::_install_prebuilt_genesis( + $unmarked_export, $tftpdir, 'x86_64' +); +like($install_error, qr/Missing Genesis export manifest/, 'the installer requires the export manifest'); + +my $partial_export = "$tmpdir/partial"; +make_path($partial_export); +write_file("$partial_export/initramfs.cpio.gz", 'partial initramfs'); +write_file( + "$partial_export/xcat-genesis.manifest", + export_manifest('x86_64'), +); +ok( + !xCAT_plugin::mknb::_prebuilt_genesis_requested($partial_export), + 'a partial export does not satisfy the prebuilt layout', +); +ok( + xCAT_plugin::mknb::_genesis_export_manifest_present($partial_export), + 'a partial export still declares the new format', +); +(undef, $install_error) = xCAT_plugin::mknb::_install_prebuilt_genesis( + $partial_export, $tftpdir, 'x86_64' +); +like($install_error, qr/Missing Genesis checksum file/, 'a partial export fails closed'); + +my $missing_manifest_checksum = "$tmpdir/missing-manifest-checksum"; +prepare_export($missing_manifest_checksum, 'kernel', 'initramfs'); +write_file( + "$missing_manifest_checksum/SHA256SUMS", + sha256_hex('kernel') . " kernel\n" + . sha256_hex('initramfs') . " initramfs.cpio.gz\n", +); +(undef, $install_error) = xCAT_plugin::mknb::_install_prebuilt_genesis( + $missing_manifest_checksum, $tftpdir, 'x86_64' +); +like($install_error, qr/Missing Genesis checksum entry: xcat-genesis\.manifest/, 'the export manifest needs a checksum'); + +my $bad_manifest_checksum = "$tmpdir/bad-manifest-checksum"; +prepare_export($bad_manifest_checksum, 'kernel', 'initramfs'); +write_file( + "$bad_manifest_checksum/xcat-genesis.manifest", + "architecture=x86_64\nversion=1\nformat=xcat-genesis\n", +); +(undef, $install_error) = xCAT_plugin::mknb::_install_prebuilt_genesis( + $bad_manifest_checksum, $tftpdir, 'x86_64' +); +like($install_error, qr/Genesis checksum mismatch: .*xcat-genesis\.manifest/, 'the export manifest checksum is verified'); + +my @invalid_manifests = ( + [ malformed => "not a manifest\n", qr/Invalid Genesis export manifest entry/ ], + [ unknown => export_manifest('x86_64') . "label=test\n", qr/Unknown Genesis export manifest entry: label/ ], + [ duplicate => export_manifest('x86_64') . "version=1\n", qr/Duplicate Genesis export manifest entry: version/ ], + [ missing => "format=xcat-genesis\nversion=1\n", qr/Missing Genesis export manifest entry: architecture/ ], + [ version => "format=xcat-genesis\nversion=2\narchitecture=x86_64\n", qr/Unsupported Genesis export version: 2/ ], + [ architecture => export_manifest('ppc64le'), qr/Unsupported Genesis export architecture: ppc64le/ ], +); +foreach my $case (@invalid_manifests) { + my ($name, $content, $error_pattern) = @{$case}; + my $invalid_export = "$tmpdir/manifest-$name"; + prepare_export($invalid_export, 'kernel', 'initramfs'); + write_file("$invalid_export/xcat-genesis.manifest", $content); + (undef, $install_error) = xCAT_plugin::mknb::_install_prebuilt_genesis( + $invalid_export, $tftpdir, 'x86_64' + ); + like($install_error, $error_pattern, "$name export manifests are rejected"); +} + +my $symlink_manifest = "$tmpdir/symlink-manifest"; +prepare_export($symlink_manifest, 'kernel', 'initramfs'); +unlink("$symlink_manifest/xcat-genesis.manifest"); +symlink("$symlink_manifest/kernel", "$symlink_manifest/xcat-genesis.manifest") + or die "Unable to create manifest symlink: $!"; +ok( + xCAT_plugin::mknb::_prebuilt_genesis_requested($symlink_manifest), + 'a manifest symlink still selects the prebuilt path', +); +(undef, $install_error) = xCAT_plugin::mknb::_install_prebuilt_genesis( + $symlink_manifest, $tftpdir, 'x86_64' +); +like($install_error, qr/Missing Genesis export manifest/, 'export manifest symlinks fail closed'); + +my $missing_entry = "$tmpdir/missing-entry"; +prepare_export($missing_entry, 'kernel', 'initramfs'); +write_file( + "$missing_entry/SHA256SUMS", + sha256_hex('kernel') . " kernel\n" + . sha256_hex(export_manifest('x86_64')) + . " xcat-genesis.manifest\n", +); +(undef, $install_error) = xCAT_plugin::mknb::_install_prebuilt_genesis( + $missing_entry, $tftpdir, 'x86_64' +); +like($install_error, qr/Missing Genesis checksum entry: initramfs\.cpio\.gz/, 'each published file needs a checksum'); + +my $duplicate_entry = "$tmpdir/duplicate-entry"; +prepare_export($duplicate_entry, 'kernel', 'initramfs'); +write_file( + "$duplicate_entry/SHA256SUMS", + sha256_hex('kernel') . " kernel\n" + . sha256_hex('kernel') . " kernel\n", +); +(undef, $install_error) = xCAT_plugin::mknb::_install_prebuilt_genesis( + $duplicate_entry, $tftpdir, 'x86_64' +); +like($install_error, qr/Duplicate Genesis checksum entry: kernel/, 'duplicate checksum entries are rejected'); + +my $malformed_entry = "$tmpdir/malformed-entry"; +prepare_export($malformed_entry, 'kernel', 'initramfs'); +write_file("$malformed_entry/SHA256SUMS", "not a checksum\n"); +(undef, $install_error) = xCAT_plugin::mknb::_install_prebuilt_genesis( + $malformed_entry, $tftpdir, 'x86_64' +); +like($install_error, qr/Invalid Genesis checksum entry/, 'malformed checksum entries are rejected'); + +my $symlink_export = "$tmpdir/symlink-export"; +prepare_export($symlink_export, 'kernel', 'initramfs'); +unlink("$symlink_export/kernel"); +symlink("$symlink_export/initramfs.cpio.gz", "$symlink_export/kernel") + or die "Unable to create test symlink: $!"; +(undef, $install_error) = xCAT_plugin::mknb::_install_prebuilt_genesis( + $symlink_export, $tftpdir, 'x86_64' +); +like($install_error, qr/Missing Genesis artifact: .*\/kernel/, 'artifact symlinks are rejected'); + +my $legacy_directory = "$tmpdir/legacy"; +make_path("$legacy_directory/fs"); +write_file("$legacy_directory/kernel", 'legacy kernel'); +ok( + !xCAT_plugin::mknb::_prebuilt_genesis_requested($legacy_directory), + 'the legacy fs layout does not select the prebuilt path', +); + +$::XCATROOT = "$tmpdir/xcatroot"; +my $process_export = + "$::XCATROOT/share/xcat/netboot/genesis/ppc64"; +prepare_export($process_export, 'process kernel', 'process initramfs', 'ppc64'); +$xCAT::TableUtils::tftpdir = "$tmpdir/custom-tftpboot"; +$xCAT::NetworkUtils::normnet_addresses = { + '192.0.2.0/24' => ['192.0.2.1'], +}; +$xCAT::NetworkUtils::hexnet_addresses = {}; +$xCAT::TableUtils::site_master = undef; +@xCAT::NetworkUtils::master_addresses = (); + +my @responses; +xCAT_plugin::mknb::process_request( + { arg => ['ppc64'] }, + sub { push(@responses, @_); }, +); +ok( + !grep({ ref($_) eq 'HASH' && $_->{error} } @responses), + 'mknb accepts the exported layout', +); +my ($process_kernel, $process_initramfs) = + published_files($xCAT::TableUtils::tftpdir, 'ppc64'); +is(read_file($process_kernel), 'process kernel', 'mknb publishes the exported kernel'); +is(read_file($process_initramfs), 'process initramfs', 'mknb publishes the exported initramfs'); +ok( + -f "$xCAT::TableUtils::tftpdir/pxelinux.cfg/p/192.0.2.0_24", + 'mknb writes boot configuration after publishing the export', +); + +unlink("$process_export/SHA256SUMS"); +@responses = (); +xCAT_plugin::mknb::process_request( + { arg => ['ppc64'] }, + sub { push(@responses, @_); }, +); +ok( + grep( + { ref($_) eq 'HASH' && $_->{error} + && $_->{error}->[0] =~ /Incomplete Genesis export/ } + @responses + ), + 'mknb rejects an incomplete marked export', +); +is( + read_file($process_kernel), + 'process kernel', + 'an incomplete marked export keeps the published kernel', +); +is( + read_file($process_initramfs), + 'process initramfs', + 'an incomplete marked export keeps the published initramfs', +); + +done_testing(); diff --git a/xCAT-test/unit/xcat_server_digest_sha_dependency.t b/xCAT-test/unit/xcat_server_digest_sha_dependency.t new file mode 100644 index 000000000..417668eb0 --- /dev/null +++ b/xCAT-test/unit/xcat_server_digest_sha_dependency.t @@ -0,0 +1,42 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use File::Spec; +use FindBin; +use Test::More; + +my $repo_root = File::Spec->rel2abs( + File::Spec->catdir( $FindBin::Bin, '..', '..' ) +); + +sub read_file { + my ($filename) = @_; + open( my $fh, '<', $filename ) + or die "Unable to read $filename: $!"; + my $content = do { local $/; <$fh> }; + close($fh); + return $content; +} + +my $rpm_spec = read_file( + File::Spec->catfile( $repo_root, 'xCAT-server', 'xCAT-server.spec' ) +); +my @rpm_sha_requirements = + $rpm_spec =~ /^Requires:.*\bperl\(Digest::SHA\)(?:\s|$)/mg; +is( + scalar(@rpm_sha_requirements), + 2, + 'both RPM architecture branches require Digest::SHA', +); + +my $debian_control = read_file( + File::Spec->catfile( $repo_root, 'xCAT-server', 'debian', 'control' ) +); +like( + $debian_control, + qr/^Depends:.*\blibdigest-sha-perl\b/m, + 'the Debian package requires Digest::SHA', +); + +done_testing();