2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-10-21 07:08:12 +00:00
Files
xcat-core/xCAT-test/unit/netboot_ipxe_method.t
T
Daniel Hilst 022a80f351 feat(xcat-core): load the kernel of an ipxe node through a Secure Boot shim
This PR adds a shim to the UEFI boot scripts of netboot=ipxe nodes, so a node
with Secure Boot on reaches its kernel.

The firmware signature database does not hold the keys of a Linux
distribution. A UEFI node with netboot=ipxe loads snponly-shim.efi and
snponly.efi, both signed, and then stops at the kernel with "Verification
failed: Security Policy Violation".

xCAT::SecureBoot::shim_url_path names a shim for one node. The shim of the
install source comes first, because its vendor certificate signs the kernel of
its own distribution. copycds keeps the names of the DVD, so the module tries
EFI/BOOT/BOOTX64.EFI, EFI/BOOT/bootx64.efi and EFI/boot/bootx64.efi: EL and
openEuler ship the first, openSUSE the second and Ubuntu the third. When no
install source carries a shim, the module names the shim of ipxe-xcat, which
verifies a kernel that a MOK key signs.

xnba.pm writes the shim line after imgload in the UEFI script of an ipxe node,
and mknb.pm writes it in the UEFI discovery scripts. iPXE fetches the shim only
when it cannot load the kernel itself, so a node with Secure Boot off reads no
shim. netboot=xnba is unchanged: no key signs xnba.efi.

The elilo and ESXi paths and the Genesis kernel stay unsigned, and the iPXE
documentation now says so. The validation matrix gets a row for a UEFI install,
a netboot image and discovery with Secure Boot on.

secureboot_ipxe_node_shim.t and secureboot_genesis_shim.t drive nodeset and
mknb into a scratch TFTP root. Four assertions of ten and two of seven fail
without this change.

Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
2026-10-07 11:57:07 -03:00

112 lines
4.9 KiB
Perl

#!/usr/bin/env perl
# netboot=ipxe runs the boot scripts of the xnba method under xcat/ipxe/nodes, and netboot=xnba keeps
# them under xcat/xnba/nodes. nodeset writes them through the real plugins, and the nodeset state
# comes back through SvrUtils, into a scratch TFTP root. Tables, name resolution and the destiny
# subrequest are stand-ins.
use strict;
use warnings;
use File::Temp qw(tempdir);
use FindBin;
use Test::More;
my $repo = "$FindBin::Bin/../..";
my $tftp = tempdir( CLEANUP => 1 );
our %rows;
BEGIN {
package xCAT::Table;
sub new { my ( $class, $table ) = @_; return bless { table => $table }, $class }
sub getNodesAttribs {
my ( $self, $nodes ) = @_;
return { map { $_ => [ $main::rows{ $self->{table} }{$_} ] } @$nodes };
}
sub getNodeAttribs { my ( $self, $node ) = @_; return $main::rows{ $self->{table} }{$node} }
sub getAttribs { return }
sub close { return }
package xCAT::NetworkUtils;
sub determinehostname { return ('mn') }
sub checkNodeIPaddress { return { ip => '192.0.2.10' } }
sub nodeonmynet { return 1 }
package xCAT::TableUtils;
sub getTftpDir { return $tftp }
sub get_site_attribute { return $_[1] eq 'dhcpsetup' ? ('n') : () } # no makedhcp from nodeset here
package xCAT::MsgUtils;
sub trace { return }
sub message { return }
package main;
for my $module (qw(Table NetworkUtils TableUtils MsgUtils Utils NodeRange Scope ServiceNodeUtils Usage BootUtils)) {
$INC{"xCAT/$module.pm"} = __FILE__;
}
}
# xcatd loads the plugins from their files. SvrUtils loads them by module name, so xCAT_plugin names
# the plugin directory of this checkout.
my $inc = tempdir( CLEANUP => 1 );
symlink( "$repo/xCAT-server/lib/xcat/plugins", "$inc/xCAT_plugin" ) or die "symlink: $!";
unshift @INC, $inc, "$repo/xCAT-server/lib/perl", "$repo/perl-xCAT";
require xCAT_plugin::ipxe;
require xCAT::SvrUtils;
# What setdestiny leaves for each node: its currstate, and the kernel that bootparams names.
sub nodeset {
my ( $plugin, $node, %kernel ) = @_;
$rows{noderes}{$node} = { tftpdir => $tftp, netboot => $plugin };
$rows{chain}{$node} = { currstate => "install rhels9-x86_64-compute" };
$rows{nodetype}{$node} = { provmethod => 'install' };
my @calls;
my $subreq = sub {
my ($request) = @_;
push @calls, $request->{command}[0];
$request->{bootparams}{$node} = [ {%kernel} ] if $request->{command}[0] eq 'setdestiny';
return;
};
"xCAT_plugin::$plugin"->can('process_request')->(
{ command => ['nodeset'], node => [$node], arg => ['osimage'] }, sub { }, $subreq );
return @calls;
}
sub content {
my ($path) = @_;
open( my $fh, '<', "$tftp/$path" ) or return;
local $/;
return <$fh>;
}
my %linux = ( kernel => 'xcat/osimage/vmlinuz', initrd => 'xcat/osimage/initrd.img', kcmdline => 'quiet' );
ok( ( grep { $_ eq 'setdestiny' } nodeset( ipxe => 'cn01', %linux ) ), 'nodeset for an ipxe node runs its destiny' );
like( content('xcat/ipxe/nodes/cn01'), qr{imgfetch -n kernel http://\$\{next-server\}/tftpboot/xcat/osimage/vmlinuz},
'the ipxe method writes the BIOS boot script under xcat/ipxe/nodes' );
# Without an EFI stub in the kernel, UEFI boots through elilo, which reads its configuration beside
# the script.
like( content('xcat/ipxe/nodes/cn01.uefi'), qr{-C /tftpboot/xcat/ipxe/nodes/cn01\.elilo$}m,
'the UEFI boot script beside it points elilo at xcat/ipxe/nodes' );
like( content('xcat/ipxe/nodes/cn01.elilo'), qr{image=/tftpboot/xcat/osimage/vmlinuz}, 'where the ipxe method writes it' );
ok( !-e "$tftp/xcat/xnba/nodes/cn01", 'and nothing under xcat/xnba/nodes' );
nodeset( xnba => 'cn02', %linux );
ok( -e "$tftp/xcat/xnba/nodes/cn02" && !-e "$tftp/xcat/ipxe/nodes/cn02",
'an xnba node keeps its boot script under xcat/xnba/nodes after an ipxe node' );
# A kernel that iPXE cannot run chains pxelinux, which reads its configuration beside the script.
nodeset( ipxe => 'cn03', kernel => 'xcat/tools/memdisk', initrd => 'xcat/tools/disk.img' );
like( content('xcat/ipxe/nodes/cn03'), qr{set 209:string xcat/ipxe/nodes/cn03\.pxelinux},
'pxelinux of an ipxe node reads its configuration from xcat/ipxe/nodes' );
ok( -e "$tftp/xcat/ipxe/nodes/cn03.pxelinux", 'where the ipxe method writes that configuration' );
# nodeset stat reads the state that the boot script records, not the chain table.
$rows{chain}{$_} = { currstate => 'boot' } for qw(cn01 cn02);
my %states;
my ($rc) = xCAT::SvrUtils->getNodesetStates( [qw(cn01 cn02)], \%states );
is( $rc, 0, 'SvrUtils reads the nodeset state of ipxe and xnba nodes' );
is_deeply( [ sort @{ $states{install} || [] } ], [qw(cn01 cn02)], 'both nodes report the state their script records' );
is( xCAT::SvrUtils->get_nodeset_state( 'cn01', global_tab_hash => { noderes => { cn01 => $rows{noderes}{cn01} } } ),
'install', 'and the state of one ipxe node' );
done_testing();