mirror of
https://github.com/xcat2/xcat-core.git
synced 2026-10-06 17:46:55 +00:00
fix(dhcp): close the last four ISC/Kea parity gaps
Appendix A rows 15 to 18, the remaining spec decisions where the two backends answered the same client differently. Row 15, a loader that is not on disk is not named. The ISC architecture chain named every loader unconditionally, so a client whose loader was never built spent a full TFTP timeout it could not diagnose; the Kea side had always left the class out. The chain is now built from a list of gated branches rather than a literal block, because dropping a branch from a literal if/else if chain can leave a leading "} else if", which dhcpd rejects outright. Row 16, a *NOIP* interface draws no reply. Kea discards a packet assigned to the class named DROP, and only one such class may exist, so every dropped MAC in the cluster shares it and the user-context records which node each belongs to. Syncing one node merges into that list and removing one node prunes only its own entries, so makedhcp for a single node cannot bring another node's interface back. Row 17, a node that boots from disk, and row 18, a node deferring to proxydhcp, are decided per node. Kea host reservations outrank every client class, so the reservation has to fall silent -- an empty boot-file-name -- and let the class carry the answer. Rows 19 and 20 landed earlier and dictated the same shape: the reservation names nothing and two mutually exclusive classes, one for the vendor and one for "not the vendor", decide between them, because Kea evaluates every class independently and has no else.
This commit is contained in:
@@ -300,50 +300,122 @@ sub isc_client_architecture_lines {
|
||||
my $maskbits = $opts{prefix} // '';
|
||||
my $xnba = $class->isc_xnba_user_class_test();
|
||||
|
||||
return [
|
||||
" if $xnba and option client-architecture = 00:00 { #x86, xCAT Network Boot Agent\n",
|
||||
" always-broadcast on;\n",
|
||||
" filename = \"http://$tftp$portsuffix/tftpboot/xcat/xnba/nets/${net}_${maskbits}\";\n",
|
||||
" } else if $xnba and option client-architecture = 00:09 { #x86, xCAT Network Boot Agent\n",
|
||||
" filename = \"http://$tftp$portsuffix/tftpboot/xcat/xnba/nets/${net}_${maskbits}.uefi\";\n",
|
||||
" } else if $xnba and option client-architecture = 00:07 { #x86-64 UEFI, xCAT Network Boot Agent\n",
|
||||
" filename = \"http://$tftp$portsuffix/tftpboot/xcat/xnba/nets/${net}_${maskbits}.uefi\";\n",
|
||||
" } else if option client-architecture = 00:00 { #x86\n",
|
||||
" filename \"xcat/xnba.kpxe\";\n",
|
||||
" } else if option vendor-class-identifier = \"Etherboot-5.4\" { #x86\n",
|
||||
" filename \"xcat/xnba.kpxe\";\n",
|
||||
" } else if option client-architecture = 00:07 { #x86_64 uefi\n ",
|
||||
" filename \"xcat/xnba.efi\";\n",
|
||||
" } else if option client-architecture = 00:09 { #x86_64 uefi alternative id\n ",
|
||||
" filename \"xcat/xnba.efi\";\n",
|
||||
# 0x0010 is the same x86-64 UEFI firmware and the same loader as 0x0007,
|
||||
# announced by a machine set to fetch it over HTTP. Without the branch
|
||||
# a mainstream client falls through to /yaboot.
|
||||
" } else if option client-architecture = 00:10 { #x86_64 uefi http boot\n ",
|
||||
" filename \"xcat/xnba.efi\";\n",
|
||||
" } else if option client-architecture = 00:02 { #ia64\n ",
|
||||
# Which loaders are actually on disk. A branch that names a file the TFTP
|
||||
# server does not have costs the client a full timeout it has no way to
|
||||
# diagnose, so the branch is left out and the client falls through to
|
||||
# whatever the chain answers next. The Kea side has always worked this way
|
||||
# (kea_client_classes takes the same two flags, kea_httpboot_network_classes
|
||||
# the same probe); until now ISC named all of them unconditionally.
|
||||
my $present = $opts{loader_present} || sub { return 1 };
|
||||
my $tftpdir = $opts{tftpdir} || '/tftpboot';
|
||||
$tftpdir =~ s{/+$}{};
|
||||
my $kpxe = $present->("$tftpdir/xcat/xnba.kpxe");
|
||||
my $efi = $present->("$tftpdir/xcat/xnba.efi");
|
||||
|
||||
# Each entry is the head of one branch and the statements inside it. They
|
||||
# are chained afterwards so that dropping one still leaves a well-formed
|
||||
# if/else if chain -- the first branch present has to be the `if`.
|
||||
my @branches;
|
||||
|
||||
if ($kpxe) {
|
||||
push @branches, [
|
||||
"$xnba and option client-architecture = 00:00 { #x86, xCAT Network Boot Agent\n",
|
||||
" always-broadcast on;\n",
|
||||
" filename = \"http://$tftp$portsuffix/tftpboot/xcat/xnba/nets/${net}_${maskbits}\";\n",
|
||||
];
|
||||
}
|
||||
if ($efi) {
|
||||
push @branches, [
|
||||
"$xnba and option client-architecture = 00:09 { #x86, xCAT Network Boot Agent\n",
|
||||
" filename = \"http://$tftp$portsuffix/tftpboot/xcat/xnba/nets/${net}_${maskbits}.uefi\";\n",
|
||||
],
|
||||
[
|
||||
"$xnba and option client-architecture = 00:07 { #x86-64 UEFI, xCAT Network Boot Agent\n",
|
||||
" filename = \"http://$tftp$portsuffix/tftpboot/xcat/xnba/nets/${net}_${maskbits}.uefi\";\n",
|
||||
];
|
||||
}
|
||||
if ($kpxe) {
|
||||
push @branches, [
|
||||
"option client-architecture = 00:00 { #x86\n",
|
||||
" filename \"xcat/xnba.kpxe\";\n",
|
||||
],
|
||||
[
|
||||
"option vendor-class-identifier = \"Etherboot-5.4\" { #x86\n",
|
||||
" filename \"xcat/xnba.kpxe\";\n",
|
||||
];
|
||||
}
|
||||
if ($efi) {
|
||||
push @branches, [
|
||||
"option client-architecture = 00:07 { #x86_64 uefi\n ",
|
||||
" filename \"xcat/xnba.efi\";\n",
|
||||
],
|
||||
[
|
||||
"option client-architecture = 00:09 { #x86_64 uefi alternative id\n ",
|
||||
" filename \"xcat/xnba.efi\";\n",
|
||||
],
|
||||
|
||||
# 0x0010 is the same x86-64 UEFI firmware and the same loader as
|
||||
# 0x0007, announced by a machine set to fetch it over HTTP. Without
|
||||
# the branch a mainstream client falls through to /yaboot.
|
||||
[
|
||||
"option client-architecture = 00:10 { #x86_64 uefi http boot\n ",
|
||||
" filename \"xcat/xnba.efi\";\n",
|
||||
];
|
||||
}
|
||||
|
||||
push @branches, [
|
||||
"option client-architecture = 00:02 { #ia64\n ",
|
||||
" filename \"elilo.efi\";\n",
|
||||
" } else if option client-architecture = 00:0b { #aaarch64\n ",
|
||||
],
|
||||
[
|
||||
"option client-architecture = 00:0b { #aaarch64\n ",
|
||||
" filename \"boot/grub2/grub2.aarch64\";\n",
|
||||
# yaboot, which is what a ppc64 client fell through to without this
|
||||
# branch, is not a UEFI loader and cannot boot one of these machines.
|
||||
" } else if option client-architecture = 00:0c { #ppc64 grub2\n ",
|
||||
],
|
||||
|
||||
# yaboot, which is what a ppc64 client fell through to without this
|
||||
# branch, is not a UEFI loader and cannot boot one of these machines.
|
||||
[
|
||||
"option client-architecture = 00:0c { #ppc64 grub2\n ",
|
||||
" filename \"/boot/grub2/grub2.ppc\";\n",
|
||||
" } else if option client-architecture = 00:1b { #riscv64 uefi\n ",
|
||||
],
|
||||
[
|
||||
"option client-architecture = 00:1b { #riscv64 uefi\n ",
|
||||
" filename \"boot/grub2/grub2.riscv64\";\n",
|
||||
" } else if option client-architecture = 00:1c { #riscv64 uefi http boot\n ",
|
||||
" option vendor-class-identifier \"HTTPClient\";\n",
|
||||
" filename \"http://$tftp$portsuffix/tftpboot/boot/grub2/grub2.riscv64\";\n",
|
||||
" } else if option client-architecture = 00:1f { #QEMU s390x\n ",
|
||||
];
|
||||
|
||||
if ( $present->("$tftpdir/boot/grub2/grub2.riscv64") ) {
|
||||
push @branches, [
|
||||
"option client-architecture = 00:1c { #riscv64 uefi http boot\n ",
|
||||
" option vendor-class-identifier \"HTTPClient\";\n",
|
||||
" filename \"http://$tftp$portsuffix/tftpboot/boot/grub2/grub2.riscv64\";\n",
|
||||
];
|
||||
}
|
||||
|
||||
push @branches, [
|
||||
"option client-architecture = 00:1f { #QEMU s390x\n ",
|
||||
" option conf-file = \"s390x/${net}_${maskbits}\";\n",
|
||||
" } else if option client-architecture = 00:0e { #OPAL-v3\n ",
|
||||
],
|
||||
[
|
||||
"option client-architecture = 00:0e { #OPAL-v3\n ",
|
||||
" option conf-file = \"http://$tftp$portsuffix/tftpboot/pxelinux.cfg/p/${net}_${maskbits}\";\n",
|
||||
" } else if substring (option vendor-class-identifier,0,11) = \"onie_vendor\" { #for onie on cumulus switch\n",
|
||||
],
|
||||
[
|
||||
"substring (option vendor-class-identifier,0,11) = \"onie_vendor\" { #for onie on cumulus switch\n",
|
||||
" option www-server = \"http://$tftp$portsuffix/install/onie/onie-installer\";\n",
|
||||
" } else if substring(filename,0,1) = null { #otherwise, provide yaboot if the client isn't specific\n ",
|
||||
],
|
||||
[
|
||||
"substring(filename,0,1) = null { #otherwise, provide yaboot if the client isn't specific\n ",
|
||||
" filename \"/yaboot\";\n",
|
||||
" }\n",
|
||||
];
|
||||
];
|
||||
|
||||
my @lines;
|
||||
foreach my $branch (@branches) {
|
||||
my ( $head, @body ) = @$branch;
|
||||
push @lines, ( @lines ? " } else if " : " if " ) . $head, @body;
|
||||
}
|
||||
push @lines, " }\n";
|
||||
|
||||
return \@lines;
|
||||
}
|
||||
|
||||
sub kea_xnba_node_classes {
|
||||
@@ -469,6 +541,72 @@ sub kea_iscsi_node_classes {
|
||||
return \@classes;
|
||||
}
|
||||
|
||||
# The tag that hands a client to the proxyDHCP daemon.
|
||||
#
|
||||
# Windows UEFI firmware that is offered no boot file, but sees option 60 set to
|
||||
# PXEClient, goes and asks the daemon listening on port 4011 for one. ISC does
|
||||
# this in the node's own host block, for the three architecture ids its
|
||||
# firmware announces; anything else on that node is simply given no boot file.
|
||||
#
|
||||
# The empty boot file is the reservation's job -- it outranks every class -- so
|
||||
# what is left for the class is the tag, and the architectures it is meant for.
|
||||
sub kea_proxydhcp_node_classes {
|
||||
my ( $class, %opts ) = @_;
|
||||
|
||||
my $nodes = $opts{nodes} || [];
|
||||
my $arches = join ' or ',
|
||||
map { "option[93].hex == $_" } qw(0x0000 0x0007 0x0009);
|
||||
my @classes;
|
||||
|
||||
foreach my $node (@$nodes) {
|
||||
next unless $node->{node} && $node->{mac};
|
||||
push @classes, {
|
||||
name => _node_class_base( 'proxydhcp', $node->{node}, $node->{mac} ),
|
||||
test => _mac_test( $node->{mac} ) . " and ($arches)",
|
||||
'option-data' => [
|
||||
{
|
||||
name => 'vendor-class-identifier',
|
||||
data => 'PXEClient',
|
||||
'always-send' => 1,
|
||||
},
|
||||
],
|
||||
'user-context' => _node_user_context( $node, 'proxydhcp-deferral' ),
|
||||
};
|
||||
}
|
||||
|
||||
return \@classes;
|
||||
}
|
||||
|
||||
# The MACs that are to be answered with nothing at all.
|
||||
#
|
||||
# ISC writes `deny booting;` into the host block of a NIC marked *NOIP* in the
|
||||
# mac table, and dhcpd then says nothing to it. Kea has one way to do that: a
|
||||
# packet assigned to a class named exactly DROP is discarded. Nothing else
|
||||
# about the name is special, and there can only be one of it, so every such
|
||||
# MAC in the cluster shares the class and the user-context records whose they
|
||||
# are, so a later makedhcp for one node can rebuild it without losing the rest.
|
||||
#
|
||||
# Skipping the reservation is not enough on its own: the subnet-wide classes
|
||||
# match on architecture and would still hand the interface a boot file, which
|
||||
# is the whole thing the marking exists to prevent.
|
||||
sub kea_drop_client_class {
|
||||
my ( $class, %opts ) = @_;
|
||||
|
||||
my @macs = grep { $_->{node} && $_->{mac} } @{ $opts{macs} || [] };
|
||||
return unless @macs;
|
||||
|
||||
my @sorted = sort { $a->{node} cmp $b->{node} or $a->{mac} cmp $b->{mac} } @macs;
|
||||
|
||||
return {
|
||||
name => 'DROP',
|
||||
test => join( ' or ', map { _mac_test( $_->{mac} ) } @sorted ),
|
||||
'user-context' => {
|
||||
'xcat-purpose' => 'noip-drop',
|
||||
'xcat-macs' => [ map { { node => $_->{node}, mac => lc( $_->{mac} ) } } @sorted ],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
#: The encapsulated space ISC declares as "option space isan" -- option 43
|
||||
#: carrying the initiator name in 203 and the root path in 201.
|
||||
sub kea_isan_option_defs {
|
||||
|
||||
@@ -3717,7 +3717,20 @@ sub kea_sync_node_client_classes
|
||||
my ( $config, $nodes ) = @_;
|
||||
|
||||
my $changed = kea_remove_node_client_classes($config, $nodes);
|
||||
my $classes = kea_node_client_classes_for_nodes($nodes);
|
||||
my $generated = kea_node_client_classes_for_nodes($nodes);
|
||||
|
||||
# The remove above took this node range's MACs out of the DROP class; these
|
||||
# are the ones it is to have from now on, added to whatever the rest of the
|
||||
# cluster already had there.
|
||||
if (@{ $generated->{noip} }) {
|
||||
kea_set_drop_client_class(
|
||||
$config,
|
||||
[ @{ kea_drop_client_class_macs($config) }, @{ $generated->{noip} } ]
|
||||
);
|
||||
$changed = 1;
|
||||
}
|
||||
|
||||
my $classes = $generated->{classes};
|
||||
return $changed unless @$classes;
|
||||
|
||||
$config->{Dhcp4} ||= {};
|
||||
@@ -3727,11 +3740,37 @@ sub kea_sync_node_client_classes
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub kea_drop_client_class_macs
|
||||
{
|
||||
my ($config) = @_;
|
||||
|
||||
foreach my $class ( @{ ( $config->{Dhcp4} || {} )->{'client-classes'} || [] } ) {
|
||||
next unless ( $class->{name} || '' ) eq 'DROP';
|
||||
return ( $class->{'user-context'} || {} )->{'xcat-macs'} || [];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
sub kea_set_drop_client_class
|
||||
{
|
||||
my ( $config, $macs ) = @_;
|
||||
|
||||
$config->{Dhcp4} ||= {};
|
||||
my @classes = grep { ( $_->{name} || '' ) ne 'DROP' }
|
||||
@{ $config->{Dhcp4}{'client-classes'} || [] };
|
||||
my $drop = xCAT::DHCP::BootPolicy->kea_drop_client_class( macs => $macs );
|
||||
push @classes, $drop if $drop;
|
||||
$config->{Dhcp4}{'client-classes'} = \@classes;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
#: Every per-node class this plugin generates carries one of these, so a node
|
||||
#: that changes netboot method loses the classes the old one wrote.
|
||||
my %KEA_NODE_CLASS_PURPOSES = map { $_ => 1 } qw(
|
||||
xnba-second-stage
|
||||
pxe-vendor
|
||||
proxydhcp-deferral
|
||||
iscsi-initiator
|
||||
);
|
||||
|
||||
@@ -3755,6 +3794,16 @@ sub kea_remove_node_client_classes
|
||||
}
|
||||
|
||||
$config->{Dhcp4}{'client-classes'} = \@kept if $changed;
|
||||
|
||||
# A node's *NOIP* interfaces share one DROP class with the rest of the
|
||||
# cluster, so removing them means rewriting it rather than dropping it.
|
||||
my $macs = kea_drop_client_class_macs($config);
|
||||
my @kept_macs = grep { !$nodes{ $_->{node} || '' } } @$macs;
|
||||
if ( scalar(@kept_macs) != scalar(@$macs) ) {
|
||||
kea_set_drop_client_class( $config, \@kept_macs );
|
||||
$changed = 1;
|
||||
}
|
||||
|
||||
return $changed;
|
||||
}
|
||||
|
||||
@@ -3764,12 +3813,16 @@ sub kea_node_client_classes_for_nodes
|
||||
|
||||
my $nrtab = xCAT::Table->new('noderes');
|
||||
my $mactab = xCAT::Table->new('mac');
|
||||
return [] unless $nrtab && $mactab;
|
||||
return { classes => [], noip => [] } unless $nrtab && $mactab;
|
||||
|
||||
my $iscsitab = xCAT::Table->new('iscsi', -create => 0);
|
||||
my $chaintab = xCAT::Table->new('chain', -create => 0);
|
||||
my $nodetypetab = xCAT::Table->new('nodetype', -create => 0);
|
||||
my $nrents = $nrtab->getNodesAttribs($nodes, [ 'tftpserver', 'netboot', 'proxydhcp', 'xcatmaster', 'servicenode' ]);
|
||||
my $macents = $mactab->getNodesAttribs($nodes, ['mac']);
|
||||
my $ients = $iscsitab ? $iscsitab->getNodesAttribs($nodes, [qw(server target lun iname)]) : undef;
|
||||
my $chainents = $chaintab ? $chaintab->getNodesAttribs($nodes, ['currstate']) : undef;
|
||||
my $ntents = $nodetypetab ? $nodetypetab->getNodesAttribs($nodes, [qw(os provmethod arch)]) : undef;
|
||||
my $httpport = "80";
|
||||
my @hports = xCAT::TableUtils->get_site_attribute("httpport");
|
||||
if ($hports[0]) {
|
||||
@@ -3779,10 +3832,22 @@ sub kea_node_client_classes_for_nodes
|
||||
my @xnba;
|
||||
my @pxe;
|
||||
my @iscsi;
|
||||
my @noip;
|
||||
my @proxydhcp;
|
||||
foreach my $node (@$nodes) {
|
||||
my $nrent = $nrents && $nrents->{$node} ? $nrents->{$node}->[0] : undef;
|
||||
my $netboot = $nrent ? $nrent->{netboot} : undef;
|
||||
|
||||
# A node that is to boot from disk, or that is waiting on the proxyDHCP
|
||||
# daemon, is given no loader at all -- so it is given no boot classes
|
||||
# either. ISC suppresses the same thing by leaving the second-stage
|
||||
# branches out of the node's host block.
|
||||
my $intent = kea_node_boot_intent(
|
||||
$nrent,
|
||||
$chainents && $chainents->{$node} ? $chainents->{$node}->[0] : undef,
|
||||
$ntents && $ntents->{$node} ? $ntents->{$node}->[0] : undef,
|
||||
);
|
||||
|
||||
my $macent = $macents && $macents->{$node} ? $macents->{$node}->[0] : undef;
|
||||
next unless $macent && $macent->{mac};
|
||||
|
||||
@@ -3792,12 +3857,26 @@ sub kea_node_client_classes_for_nodes
|
||||
my $iname = ( $ient and $ient->{server} and $ient->{target} ) ? $ient->{iname} : undef;
|
||||
|
||||
foreach my $mace (split(/\|/, $macent->{mac})) {
|
||||
my ($mac) = split(/!/, $mace);
|
||||
my ( $mac, $hname ) = split(/!/, $mace);
|
||||
$mac = kea_normalize_mac($mac);
|
||||
next unless $mac;
|
||||
my %record = ( node => $node, mac => $mac );
|
||||
|
||||
if ($netboot and $netboot eq 'xnba' and $nxtsrv) {
|
||||
# The interface is marked as having no address on purpose. ISC
|
||||
# answers it with "deny booting;"; the Kea equivalent is the DROP
|
||||
# class, which the caller merges with the rest of the cluster's.
|
||||
if ( defined($hname) and $hname eq '*NOIP*' ) {
|
||||
push @noip, {%record};
|
||||
next;
|
||||
}
|
||||
|
||||
if ( $intent eq 'proxydhcp' ) {
|
||||
push @proxydhcp, {%record};
|
||||
} elsif ( $intent eq 'disk' ) {
|
||||
|
||||
# Nothing: the reservation names an empty boot file and that
|
||||
# outranks anything a class could say.
|
||||
} elsif ($netboot and $netboot eq 'xnba' and $nxtsrv) {
|
||||
push @xnba, { %record, next_server => $nxtsrv, httpport => $httpport };
|
||||
} elsif ($netboot and $netboot eq 'pxe') {
|
||||
push @pxe, {%record};
|
||||
@@ -3810,14 +3889,40 @@ sub kea_node_client_classes_for_nodes
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
@{ xCAT::DHCP::BootPolicy->kea_xnba_node_classes(
|
||||
nodes => \@xnba,
|
||||
xnba_efi => -f "$tftpdir/xcat/xnba.efi" ? 1 : 0,
|
||||
) },
|
||||
@{ xCAT::DHCP::BootPolicy->kea_pxe_node_classes( nodes => \@pxe ) },
|
||||
@{ xCAT::DHCP::BootPolicy->kea_iscsi_node_classes( nodes => \@iscsi ) },
|
||||
];
|
||||
return {
|
||||
classes => [
|
||||
@{ xCAT::DHCP::BootPolicy->kea_xnba_node_classes(
|
||||
nodes => \@xnba,
|
||||
xnba_efi => -f "$tftpdir/xcat/xnba.efi" ? 1 : 0,
|
||||
) },
|
||||
@{ xCAT::DHCP::BootPolicy->kea_pxe_node_classes( nodes => \@pxe ) },
|
||||
@{ xCAT::DHCP::BootPolicy->kea_proxydhcp_node_classes( nodes => \@proxydhcp ) },
|
||||
@{ xCAT::DHCP::BootPolicy->kea_iscsi_node_classes( nodes => \@iscsi ) },
|
||||
],
|
||||
noip => \@noip,
|
||||
};
|
||||
}
|
||||
|
||||
# What the node has been told to do next, as far as this reply is concerned.
|
||||
#
|
||||
# 'disk' -- chain.currstate is boot or iscsiboot: it has an operating
|
||||
# system now and must be left to start it.
|
||||
# 'proxydhcp' -- a Windows install or winshell on UEFI firmware, with the
|
||||
# proxyDHCP daemon running to answer it on 4011.
|
||||
# 'netboot' -- everything else, which is to be given a loader.
|
||||
sub kea_node_boot_intent
|
||||
{
|
||||
my ( $nrent, $chainent, $ntent ) = @_;
|
||||
|
||||
my $currstate = ( $chainent && defined $chainent->{currstate} ) ? $chainent->{currstate} : '';
|
||||
return 'disk' if $currstate eq 'boot' or $currstate eq 'iscsiboot';
|
||||
|
||||
my $douefi = ( $ntent and $ntent->{os} ) ? check_uefi_support($ntent) : 1;
|
||||
if ( ( $douefi == 2 and $currstate =~ /^install/ ) or $currstate =~ /^winshell/ ) {
|
||||
return 'proxydhcp' if proxydhcp($nrent);
|
||||
}
|
||||
|
||||
return 'netboot';
|
||||
}
|
||||
|
||||
sub kea_iscsi_root_path
|
||||
@@ -4020,6 +4125,7 @@ sub kea_boot_for_node
|
||||
|
||||
my %boot = ( 'option-data' => [] );
|
||||
my $netboot = $nrent ? $nrent->{netboot} : undef;
|
||||
my $intent = kea_node_boot_intent( $nrent, $chainent, $ntent );
|
||||
|
||||
# A node with an initiator name has to choose between the ISAN vendor form
|
||||
# and the standard one, and a reservation's option-data outranks any class,
|
||||
@@ -4033,7 +4139,16 @@ sub kea_boot_for_node
|
||||
# netboot=pxe is absent from this chain for the same reason: a ScaleMP
|
||||
# machine has to be able to win, and only a class can outrank nothing.
|
||||
# kea_pxe_node_classes writes both halves of that choice.
|
||||
if ($netboot and $netboot eq 'yaboot') {
|
||||
if ( $intent ne 'netboot' ) {
|
||||
|
||||
# A node told to boot from disk, and a Windows UEFI install waiting on
|
||||
# the proxyDHCP daemon, are both to be handed no boot file. ISC writes
|
||||
# filename = "" into the node's own host block, which outranks the
|
||||
# subnet; the reservation is what outranks a class on Kea, so the empty
|
||||
# name has to be set here or the architecture classes answer instead
|
||||
# and the node netboots forever.
|
||||
$boot{'boot-file-name'} = '';
|
||||
} elsif ($netboot and $netboot eq 'yaboot') {
|
||||
$boot{'boot-file-name'} = "/yb/node/yaboot-$node";
|
||||
} elsif ($netboot and $netboot =~ /^grub2[-]?.*$/) {
|
||||
$boot{'boot-file-name'} = "/boot/grub2/grub2-$node";
|
||||
@@ -4722,11 +4837,12 @@ sub addnet
|
||||
|
||||
# $lstatements = 'if exists gpxe.bus-id { filename = \"\"; } else if exists client-architecture { filename = \"xcat/xnba.kpxe\"; } '.$lstatements;
|
||||
push @netent, @{ xCAT::DHCP::BootPolicy->isc_client_architecture_lines(
|
||||
next_server => $tftp,
|
||||
portsuffix => $portsuffix,
|
||||
tftpdir => $tftpdir,
|
||||
net => $net,
|
||||
prefix => $maskbits,
|
||||
next_server => $tftp,
|
||||
portsuffix => $portsuffix,
|
||||
tftpdir => $tftpdir,
|
||||
net => $net,
|
||||
prefix => $maskbits,
|
||||
loader_present => sub { -e $_[0] },
|
||||
) };
|
||||
|
||||
if ($range) {
|
||||
|
||||
@@ -152,4 +152,68 @@ is(
|
||||
'and both backends land on the same number',
|
||||
);
|
||||
|
||||
# A loader that is not on disk is not named. Naming one costs the client a
|
||||
# full TFTP timeout it cannot diagnose, and the Kea side has always left the
|
||||
# class out for exactly that reason -- so the same missing file has to produce
|
||||
# the same silence on both backends.
|
||||
{
|
||||
my @asked;
|
||||
my %present = map { $_ => 1 } (
|
||||
'/srv/tftp/xcat/xnba.efi',
|
||||
'/srv/tftp/boot/grub2/grub2.riscv64',
|
||||
);
|
||||
my $partial = join '', @{ xCAT::DHCP::BootPolicy->isc_client_architecture_lines(
|
||||
next_server => '192.0.2.10',
|
||||
portsuffix => '',
|
||||
tftpdir => '/srv/tftp',
|
||||
net => '192.0.2.0',
|
||||
prefix => 24,
|
||||
loader_present => sub { push @asked, $_[0]; return $present{ $_[0] } },
|
||||
) };
|
||||
|
||||
unlike( $partial, qr/xnba\.kpxe/,
|
||||
'a BIOS client is not sent after a kpxe loader that was never built' );
|
||||
like( $partial, qr/xnba\.efi/,
|
||||
'and the UEFI loader that is there is still offered' );
|
||||
|
||||
# The second stage is fetched over HTTP, but it is the first stage that
|
||||
# asks for it, so it is gated on the same file.
|
||||
unlike( $partial, qr{/xcat/xnba/nets/192\.0\.2\.0_24"},
|
||||
'no BIOS second stage is advertised without the first stage to reach it' );
|
||||
like( $partial, qr{/xcat/xnba/nets/192\.0\.2\.0_24\.uefi"},
|
||||
'the UEFI second stage is advertised, because its first stage exists' );
|
||||
|
||||
is( scalar( grep { $_ eq '/srv/tftp/boot/grub2/grub2.riscv64' } @asked ), 1,
|
||||
'the riscv64 HTTP branch is probed under the configured tftp directory' );
|
||||
|
||||
# Whatever is dropped, what is left has to still be one chain: dhcpd
|
||||
# rejects a leading "} else if" and refuses to start.
|
||||
like( $partial, qr/\A if /, 'the first surviving branch opens the chain' );
|
||||
unlike( $partial, qr/\n \} else if [^\n]*\n\s*\}\n \} else if /,
|
||||
'no branch is left dangling between two chains' );
|
||||
is( scalar( () = $partial =~ /^ \}\n/mg ), 1,
|
||||
'and the chain is closed exactly once' );
|
||||
}
|
||||
|
||||
{
|
||||
# Nothing on disk at all: the architecture branches that name a file xCAT
|
||||
# never builds stay, and the client that matches none of them still ends up
|
||||
# at the fallback rather than at a parse error.
|
||||
my $bare = join '', @{ xCAT::DHCP::BootPolicy->isc_client_architecture_lines(
|
||||
next_server => '192.0.2.10',
|
||||
portsuffix => '',
|
||||
tftpdir => '/srv/tftp',
|
||||
net => '192.0.2.0',
|
||||
prefix => 24,
|
||||
loader_present => sub { return 0 },
|
||||
) };
|
||||
|
||||
like( $bare, qr/\A if option client-architecture = 00:02 /,
|
||||
'the chain opens on the first branch that survives' );
|
||||
like( $bare, qr/filename "\/yaboot";\n\s*\}\n\z/,
|
||||
'and still ends at the fallback' );
|
||||
unlike( $bare, qr/HTTPClient/,
|
||||
'the riscv64 HTTP branch goes with the image it would have served' );
|
||||
}
|
||||
|
||||
done_testing();
|
||||
|
||||
@@ -858,7 +858,7 @@ foreach my $case (@invalid_mac_cases) {
|
||||
};
|
||||
local *xCAT_plugin::dhcp::kea_next_server_for_node = sub { return ( '192.0.2.1', '192.0.2.1' ); };
|
||||
|
||||
my $classes = xCAT_plugin::dhcp::kea_node_client_classes_for_nodes(['xnba01']);
|
||||
my $classes = xCAT_plugin::dhcp::kea_node_client_classes_for_nodes(['xnba01'])->{classes};
|
||||
my ($bios_class) = grep { $_->{name} =~ /-bios\z/ } @$classes;
|
||||
ok( $bios_class, 'hyphenated xNBA MAC produces a BIOS client class' );
|
||||
is(
|
||||
@@ -1257,7 +1257,7 @@ foreach my $case (@invalid_mac_cases) {
|
||||
};
|
||||
local *xCAT_plugin::dhcp::kea_next_server_for_node = sub { return ( '192.0.2.1', '192.0.2.1' ); };
|
||||
|
||||
my $classes = xCAT_plugin::dhcp::kea_node_client_classes_for_nodes( [ 'smp01', 'san01' ] );
|
||||
my $classes = xCAT_plugin::dhcp::kea_node_client_classes_for_nodes( [ 'smp01', 'san01' ] )->{classes};
|
||||
my %by_name = map { $_->{name} => $_ } @$classes;
|
||||
|
||||
is( $by_name{'xcat-pxe-smp01-aabbccddee01-scalemp'}{'boot-file-name'},
|
||||
@@ -1298,4 +1298,163 @@ foreach my $case (@invalid_mac_cases) {
|
||||
is( $defs{'isan/root-path'}{code}, 201, 'the root path is sub-option 201' );
|
||||
}
|
||||
|
||||
{
|
||||
# A NIC marked *NOIP* in the mac table is meant to be answered with
|
||||
# nothing. ISC writes "deny booting;" into its host block; Kea discards a
|
||||
# packet assigned to the class named DROP, and nothing else will do --
|
||||
# skipping the reservation still leaves the subnet-wide architecture
|
||||
# classes handing the interface a boot file.
|
||||
my %tables = (
|
||||
noderes => DHCPKeaResTable->new(
|
||||
{ cn01 => { netboot => 'xnba' }, cn02 => { netboot => 'pxe' } }
|
||||
),
|
||||
mac => DHCPKeaResTable->new(
|
||||
{
|
||||
cn01 => { mac => 'aa:bb:cc:dd:ee:01|aa:bb:cc:dd:ee:11!*NOIP*' },
|
||||
cn02 => { mac => 'aa:bb:cc:dd:ee:02!*NOIP*' },
|
||||
}
|
||||
),
|
||||
);
|
||||
|
||||
no warnings 'redefine';
|
||||
local *xCAT::Table::new = sub {
|
||||
my ( $class, $name ) = @_;
|
||||
return $tables{$name};
|
||||
};
|
||||
local *xCAT_plugin::dhcp::kea_next_server_for_node = sub { return ( '192.0.2.1', '192.0.2.1' ); };
|
||||
|
||||
my $config = { Dhcp4 => { 'client-classes' => [] } };
|
||||
ok( xCAT_plugin::dhcp::kea_sync_node_client_classes( $config, [ 'cn01', 'cn02' ] ),
|
||||
'marking an interface *NOIP* changes the configuration' );
|
||||
|
||||
my ($drop) = grep { $_->{name} eq 'DROP' } @{ $config->{Dhcp4}{'client-classes'} };
|
||||
ok( $drop, 'the MACs land in the one class Kea treats as a discard' );
|
||||
is( $drop->{test},
|
||||
'pkt4.mac == 0xaabbccddee11 or pkt4.mac == 0xaabbccddee02',
|
||||
'every marked MAC in the range is named, and only those' );
|
||||
|
||||
# cn01's real NIC is untouched: the marking is per interface, not per node.
|
||||
ok( ( grep { $_->{name} =~ /aabbccddee01/ } @{ $config->{Dhcp4}{'client-classes'} } ),
|
||||
'the node\'s addressed NIC still gets its boot classes' );
|
||||
|
||||
# Re-running makedhcp for one node must not take the other node's
|
||||
# interfaces out of the class they share.
|
||||
xCAT_plugin::dhcp::kea_sync_node_client_classes( $config, ['cn01'] );
|
||||
($drop) = grep { $_->{name} eq 'DROP' } @{ $config->{Dhcp4}{'client-classes'} };
|
||||
is( $drop->{test},
|
||||
'pkt4.mac == 0xaabbccddee11 or pkt4.mac == 0xaabbccddee02',
|
||||
'a makedhcp for one node leaves the rest of the cluster in the DROP class' );
|
||||
|
||||
# ...and makedhcp -d for a node takes only that node's out.
|
||||
ok( xCAT_plugin::dhcp::kea_remove_node_client_classes( $config, ['cn02'] ),
|
||||
'removing a node with a marked interface changes the configuration' );
|
||||
($drop) = grep { $_->{name} eq 'DROP' } @{ $config->{Dhcp4}{'client-classes'} };
|
||||
is( $drop->{test}, 'pkt4.mac == 0xaabbccddee11',
|
||||
'and leaves the other node still dropped' );
|
||||
|
||||
xCAT_plugin::dhcp::kea_remove_node_client_classes( $config, ['cn01'] );
|
||||
is_deeply(
|
||||
[ grep { $_->{name} eq 'DROP' } @{ $config->{Dhcp4}{'client-classes'} } ],
|
||||
[],
|
||||
'with nothing left to drop the class goes rather than matching nothing',
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
# A node that has an operating system now, and a Windows UEFI install
|
||||
# waiting on the proxyDHCP daemon, both have to be handed no boot file.
|
||||
# ISC writes filename = "" into the node's host block, which outranks the
|
||||
# subnet chain. On Kea only a reservation outranks a class, so if the
|
||||
# reservation stays silent the subnet's architecture classes answer instead
|
||||
# and an installed node netboots forever.
|
||||
no warnings 'redefine';
|
||||
local *xCAT_plugin::dhcp::proxydhcp = sub { return 1; };
|
||||
|
||||
foreach my $state (qw(boot iscsiboot)) {
|
||||
my $booted = xCAT_plugin::dhcp::kea_boot_for_node(
|
||||
'cn01', { netboot => 'xnba' }, { currstate => $state }, undef, undef, '192.0.2.1'
|
||||
);
|
||||
is( $booted->{'boot-file-name'}, '',
|
||||
"a node in state $state is handed no boot file rather than left to the subnet" );
|
||||
}
|
||||
|
||||
my $installing = xCAT_plugin::dhcp::kea_boot_for_node(
|
||||
'win01', { netboot => 'xnba' }, { currstate => 'install' },
|
||||
{ os => 'win2022' }, undef, '192.0.2.1'
|
||||
);
|
||||
is( $installing->{'boot-file-name'}, '',
|
||||
'a Windows UEFI install names no boot file, which is what defers it to proxyDHCP' );
|
||||
|
||||
# ...and the same node on a Linux install is not deferred to anything.
|
||||
my $linux = xCAT_plugin::dhcp::kea_boot_for_node(
|
||||
'cn02', { netboot => 'pxe' }, { currstate => 'install' },
|
||||
{ os => 'rhels9' }, undef, '192.0.2.1'
|
||||
);
|
||||
ok( !exists $linux->{'boot-file-name'},
|
||||
'a Linux install is left to its classes as before' );
|
||||
|
||||
# An iSCSI node told to boot from disk still needs its root path: it is
|
||||
# what the disk is.
|
||||
my $iscsi = xCAT_plugin::dhcp::kea_boot_for_node(
|
||||
'cn03', {}, { currstate => 'iscsiboot' }, undef,
|
||||
{ server => '192.0.2.9', target => 'iqn.2024-01.test:cn03', lun => 0 },
|
||||
'192.0.2.1'
|
||||
);
|
||||
is( $iscsi->{'boot-file-name'}, '', 'an iscsiboot node is handed no boot file' );
|
||||
ok( ( grep { $_->{name} eq 'root-path' } @{ $iscsi->{'option-data'} } ),
|
||||
'but it keeps the root path that says where its disk is' );
|
||||
}
|
||||
|
||||
{
|
||||
my %tables = (
|
||||
noderes => DHCPKeaResTable->new(
|
||||
{
|
||||
booted => { netboot => 'xnba' },
|
||||
win01 => { netboot => 'xnba' },
|
||||
}
|
||||
),
|
||||
mac => DHCPKeaResTable->new(
|
||||
{
|
||||
booted => { mac => 'aa:bb:cc:dd:ee:03' },
|
||||
win01 => { mac => 'aa:bb:cc:dd:ee:04' },
|
||||
}
|
||||
),
|
||||
chain => DHCPKeaResTable->new(
|
||||
{ booted => { currstate => 'boot' }, win01 => { currstate => 'install' } }
|
||||
),
|
||||
nodetype => DHCPKeaResTable->new(
|
||||
{ booted => { os => 'rhels9' }, win01 => { os => 'win2022' } }
|
||||
),
|
||||
);
|
||||
|
||||
no warnings 'redefine';
|
||||
local *xCAT::Table::new = sub {
|
||||
my ( $class, $name ) = @_;
|
||||
return $tables{$name};
|
||||
};
|
||||
local *xCAT_plugin::dhcp::kea_next_server_for_node = sub { return ( '192.0.2.1', '192.0.2.1' ); };
|
||||
local *xCAT_plugin::dhcp::proxydhcp = sub { return 1; };
|
||||
|
||||
my $classes = xCAT_plugin::dhcp::kea_node_client_classes_for_nodes( [ 'booted', 'win01' ] )->{classes};
|
||||
my %by_name = map { $_->{name} => $_ } @$classes;
|
||||
|
||||
is_deeply(
|
||||
[ grep { /booted/ } keys %by_name ],
|
||||
[],
|
||||
'a node booting from disk is given no second stage to chainload',
|
||||
);
|
||||
|
||||
my $deferral = $by_name{'xcat-proxydhcp-win01-aabbccddee04'};
|
||||
ok( $deferral, 'the Windows UEFI node gets the class that tags its reply' );
|
||||
is_deeply(
|
||||
$deferral->{'option-data'},
|
||||
[ { name => 'vendor-class-identifier', data => 'PXEClient', 'always-send' => 1 } ],
|
||||
'the tag is what sends the firmware to the daemon on 4011',
|
||||
);
|
||||
like( $deferral->{test}, qr/option\[93\]\.hex == 0x0000 or option\[93\]\.hex == 0x0007 or option\[93\]\.hex == 0x0009/,
|
||||
'and only the architectures ISC tags are tagged' );
|
||||
ok( !exists $by_name{'xcat-xnba-win01-aabbccddee04-bios'},
|
||||
'the node gets no xNBA class that would pre-empt the deferral' );
|
||||
}
|
||||
|
||||
done_testing();
|
||||
|
||||
Reference in New Issue
Block a user