From 0e70a16e10203f7b6569e6268906e4d202fa441a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 2 Oct 2026 19:31:02 -0300 Subject: [PATCH] refactor(dhcp): build the x86 boot statements in BootPolicy dhcp.pm wrote the ISC host statements of netboot=xnba and netboot=pxe nodes inline. Each Kea class builder named the xNBA files and the xNBA user-class test on its own. Move the host statements into isc_node_boot_statements. Give every ISC and Kea builder its loader files and second-stage tests from x86_loader. The rendered statements, lines and classes do not change. --- perl-xCAT/xCAT/DHCP/BootPolicy.pm | 110 +++++++++++++++++++++------ xCAT-server/lib/xcat/plugins/dhcp.pm | 67 ++++------------ 2 files changed, 101 insertions(+), 76 deletions(-) diff --git a/perl-xCAT/xCAT/DHCP/BootPolicy.pm b/perl-xCAT/xCAT/DHCP/BootPolicy.pm index 55c5ae925..88347bd1b 100644 --- a/perl-xCAT/xCAT/DHCP/BootPolicy.pm +++ b/perl-xCAT/xCAT/DHCP/BootPolicy.pm @@ -6,14 +6,14 @@ use warnings; sub kea_client_classes { my ( $class, %opts ) = @_; - my $xnba_user_class = xnba_user_class_test(); + my $loader = $class->x86_loader(); my $uefi_x64_arch_match = uefi_x64_client_architecture_match_expr(); my $etherboot = etherboot_vendor_class_test(); # No substitute when the loader is not on disk: naming a file the TFTP # server does not have costs the client a timeout it cannot diagnose, and a # different loader boots something nobody asked for. - my $bios_boot = $opts{xnba_kpxe} ? 'xcat/xnba.kpxe' : ''; - my $uefi_boot = $opts{xnba_efi} ? 'xcat/xnba.efi' : ''; + my $bios_boot = $opts{xnba_kpxe} ? $loader->{bios} : ''; + my $uefi_boot = $opts{xnba_efi} ? $loader->{uefi} : ''; my @classes; push @classes, @{ $opts{xnba_node_classes} || [] }; @@ -26,7 +26,7 @@ sub kea_client_classes { push @classes, ( { name => 'xcat-bios', - test => "option[93].hex == 0x0000 and not ($xnba_user_class)", + test => "option[93].hex == 0x0000 and not ($loader->{kea_second_stage_bios})", 'boot-file-name' => $bios_boot, }, # Etherboot predates option 93: it says what it is in option 60 and @@ -42,7 +42,7 @@ sub kea_client_classes { if ($uefi_boot ne '') { push @classes, { name => 'xcat-uefi-x64', - test => "($uefi_x64_arch_match) and not ($xnba_user_class)", + test => "($uefi_x64_arch_match) and not ($loader->{kea_second_stage_uefi})", 'boot-file-name' => $uefi_boot, }; } @@ -288,7 +288,7 @@ sub isc_client_architecture_lines { my $portsuffix = $opts{portsuffix} // ''; my $net = $opts{net} // ''; my $maskbits = $opts{prefix} // ''; - my $xnba = $class->isc_xnba_user_class_test(); + my $loader = $class->x86_loader(); # Which loaders are actually on disk. A branch naming a file the TFTP server # does not have costs the client a full timeout it cannot diagnose, so the @@ -297,8 +297,8 @@ sub isc_client_architecture_lines { 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"); + my $kpxe = $present->("$tftpdir/$loader->{bios}"); + my $efi = $present->("$tftpdir/$loader->{uefi}"); # Each entry is the head of one branch and its statements. They are chained # afterwards so dropping one still leaves a well-formed if/else chain. @@ -306,39 +306,39 @@ sub isc_client_architecture_lines { if ($kpxe) { push @branches, [ - "$xnba and option client-architecture = 00:00 { #x86, xCAT Network Boot Agent\n", + "$loader->{isc_second_stage_bios} 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", + "$loader->{isc_second_stage_uefi} 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", + "$loader->{isc_second_stage_uefi} 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", + " filename \"$loader->{bios}\";\n", ], [ "option vendor-class-identifier = \"Etherboot-5.4\" { #x86\n", - " filename \"xcat/xnba.kpxe\";\n", + " filename \"$loader->{bios}\";\n", ]; } if ($efi) { push @branches, [ "option client-architecture = 00:07 { #x86_64 uefi\n ", - " filename \"xcat/xnba.efi\";\n", + " filename \"$loader->{uefi}\";\n", ], [ "option client-architecture = 00:09 { #x86_64 uefi alternative id\n ", - " filename \"xcat/xnba.efi\";\n", + " filename \"$loader->{uefi}\";\n", ], # 0x0010 is the same x86-64 UEFI firmware and loader as 0x0007, @@ -346,7 +346,7 @@ sub isc_client_architecture_lines { # a mainstream client falls through to /yaboot. [ "option client-architecture = 00:10 { #x86_64 uefi http boot\n ", - " filename \"xcat/xnba.efi\";\n", + " filename \"$loader->{uefi}\";\n", ]; } @@ -426,7 +426,7 @@ sub kea_xnba_node_classes { my ( $class, %opts ) = @_; my $nodes = $opts{nodes} || []; - my $xnba_user_class = xnba_user_class_test(); + my $loader = $class->x86_loader(); my $uefi_x64_arch_match = uefi_x64_client_architecture_match_expr(); my @classes; @@ -440,7 +440,7 @@ sub kea_xnba_node_classes { push @classes, { name => "$class_base-bios", - test => "$xnba_user_class and option[93].hex == 0x0000 and $mac_test", + test => "$loader->{kea_second_stage_bios} and option[93].hex == 0x0000 and $mac_test", 'boot-file-name' => $base_url, 'user-context' => _xnba_user_context($node), }; @@ -448,7 +448,7 @@ sub kea_xnba_node_classes { if ( $opts{xnba_efi} ) { push @classes, { name => "$class_base-uefi", - test => "$xnba_user_class and ($uefi_x64_arch_match) and $mac_test", + test => "$loader->{kea_second_stage_uefi} and ($uefi_x64_arch_match) and $mac_test", 'boot-file-name' => "$base_url.uefi", 'user-context' => _xnba_user_context($node), }; @@ -666,7 +666,7 @@ sub kea_xnba_network_classes { return [] unless $opts{net} && defined $opts{prefix} && $opts{next_server}; - my $xnba_user_class = xnba_user_class_test(); + my $loader = $class->x86_loader(); my $uefi_x64_arch_match = uefi_x64_client_architecture_match_expr(); my $httpport = $opts{httpport} || '80'; my $portsuffix = ( $httpport eq '80' ) ? '' : ":$httpport"; @@ -680,7 +680,7 @@ sub kea_xnba_network_classes { if ( $opts{xnba_kpxe} ) { push @classes, { name => "xcat-xnba-net-$safe_network-bios", - test => "$xnba_user_class and option[93].hex == 0x0000", + test => "$loader->{kea_second_stage_bios} and option[93].hex == 0x0000", 'boot-file-name' => $base_url, additional_only => 1, }; @@ -689,7 +689,7 @@ sub kea_xnba_network_classes { if ( $opts{xnba_efi} ) { push @classes, { name => "xcat-xnba-net-$safe_network-uefi", - test => "$xnba_user_class and ($uefi_x64_arch_match)", + test => "$loader->{kea_second_stage_uefi} and ($uefi_x64_arch_match)", 'boot-file-name' => "$base_url.uefi", additional_only => 1, }; @@ -698,6 +698,72 @@ sub kea_xnba_network_classes { return \@classes; } +# The x86 loader: its BIOS and UEFI files, and the tests that recognize a client that runs it and +# can fetch the boot script. +sub x86_loader { + my ($class) = @_; + + return { + bios => 'xcat/xnba.kpxe', + uefi => 'xcat/xnba.efi', + isc_second_stage_bios => $class->isc_xnba_user_class_test(), + isc_second_stage_uefi => $class->isc_xnba_user_class_test(), + isc_san_boot => 'exists gpxe.bus-id', + kea_second_stage_bios => xnba_user_class_test(), + kea_second_stage_uefi => xnba_user_class_test(), + }; +} + +# The ISC host statements that choose the boot file of a node with netboot xnba or pxe. They reach +# dhcpd through omshell, so their quotes are escaped. +sub isc_node_boot_statements { + my ( $class, %opts ) = @_; + + my $netboot = $opts{netboot} // ''; + return '' if $netboot ne 'xnba' && $netboot ne 'pxe'; + return '' unless $opts{loader_present}; + + my $loader = $class->x86_loader(); + my $bios = $loader->{bios}; + my $uefi = $loader->{uefi}; + my $bios_next = _omapi_quote( $loader->{isc_second_stage_bios} ); + my $uefi_next = _omapi_quote( $loader->{isc_second_stage_uefi} ); + my $san_boot = _omapi_quote( $loader->{isc_san_boot} ); + my $currstate = $opts{currstate} // ''; + my $script = 'http://' . ( $opts{next_server} // '' ) . ( $opts{portsuffix} // '' ) + . '/tftpboot/xcat/xnba/nodes/' . ( $opts{node} // '' ); + + # A node in state boot or iscsiboot has an operating system and is left to start it (S-31). + # An iSCSI root still needs the loader, which attaches the disk. + if ( $currstate eq 'boot' || $currstate eq 'iscsiboot' ) { + return 'filename = \"\";' unless $opts{iscsi}; + return 'if ' . $san_boot . ' { filename = \"\"; } else if exists client-architecture { filename = \"' . $bios . '\"; } ' + if $netboot eq 'pxe'; + return 'if option client-architecture = 00:00 and not ' . $san_boot . ' { filename = \"' . $bios . '\"; } else { filename = \"\"; } '; + } + return 'if option vendor-class-identifier = \"ScaleMP\" { filename = \"vsmp/pxelinux.0\"; } else { filename = \"pxelinux.0\"; }' + if $netboot eq 'pxe'; + + my $uefi_mode = $opts{uefi} // 0; + if ( ( $uefi_mode == 2 && $currstate =~ /^install/ ) || $currstate =~ /^winshell/ ) { + return 'if option client-architecture = 00:00 or option client-architecture = 00:07 or option client-architecture = 00:09 { filename = \"\"; option vendor-class-identifier \"PXEClient\"; } else { filename = \"\"; }' + if $opts{proxydhcp} && $opts{proxydhcp}->(); + return 'if ' . $bios_next . ' and option client-architecture = 00:00 { always-broadcast on; filename = \"' . $script . '\"; } else if option client-architecture = 00:07 or option client-architecture = 00:09 { filename = \"\"; option vendor-class-identifier \"PXEClient\"; } else if option client-architecture = 00:00 { filename = \"' . $bios . '\"; } else { filename = \"\"; }'; + } + if ($uefi_mode) { + # dhcpd's expression grammar has no grouping, so each UEFI architecture gets its own branch. + my $uefi_script = 'filename = \"' . $script . '.uefi\";'; + return 'if ' . $bios_next . ' and option client-architecture = 00:00 { always-broadcast on; filename = \"' . $script . '\"; } else if ' . $uefi_next . ' and option client-architecture = 00:09 { ' . $uefi_script . ' } else if ' . $uefi_next . ' and option client-architecture = 00:07 { ' . $uefi_script . ' } else if option client-architecture = 00:07 { filename = \"' . $uefi . '\"; } else if option client-architecture = 00:00 { filename = \"' . $bios . '\"; } else { filename = \"\"; }'; + } + return 'if ' . $bios_next . ' and option client-architecture = 00:00 { filename = \"' . $script . '\"; } else if option client-architecture = 00:00 { filename = \"' . $bios . '\"; } else { filename = \"\"; }'; +} + +sub _omapi_quote { + my ($text) = @_; + $text =~ s/"/\\"/g; + return $text; +} + sub xnba_user_class_test { return "(option[77].exists and (option[77].text == 'xNBA' or option[77].hex == 0x784e4241 or substring(option[77].hex,1,4) == 'xNBA'))"; } diff --git a/xCAT-server/lib/xcat/plugins/dhcp.pm b/xCAT-server/lib/xcat/plugins/dhcp.pm index 7f0d1deba..cbadf8d44 100644 --- a/xCAT-server/lib/xcat/plugins/dhcp.pm +++ b/xCAT-server/lib/xcat/plugins/dhcp.pm @@ -1123,60 +1123,19 @@ sub addnode } my $douefi = check_uefi_support($ntent); - # These statements reach dhcpd through omshell, so the quoting is \" - # rather than ". Both encodings of option 77 are accepted for the same - # reason as in the per-network classes: a second stage that - # length-prefixes its user class per RFC 3004 would chainload forever. - my $xnba_user_class = xCAT::DHCP::BootPolicy->isc_xnba_user_class_test(quote => '\"'); - - if ($nrent and $nrent->{netboot} and $nrent->{netboot} eq 'xnba' and $lstatements !~ /filename/) { - if (-f "$tftpdir/xcat/xnba.kpxe") { - if ($chainent and $chainent->{currstate} and ($chainent->{currstate} eq 'iscsiboot' or $chainent->{currstate} eq 'boot')) { - - # A node in state boot or iscsiboot has an operating system - # and must be left to start it -- spec.md S-31. iSCSI still - # needs a loader, because the root disk is on the network - # and gPXE attaches it: BIOS firmware is given xnba.kpxe, - # and the second stage, which announces gpxe.bus-id, is - # given nothing. - if ($doiscsi) { - $lstatements = 'if option client-architecture = 00:00 and not exists gpxe.bus-id { filename = \"xcat/xnba.kpxe\"; } else { filename = \"\"; } ' . $lstatements; - } else { - $lstatements = 'filename = \"\";' . $lstatements; - } - } else { - - # If proxydhcp daemon is enabled for windows deployment, do vendor-class-identifier of "PXEClient" to bump it over to proxydhcp.c - if (($douefi == 2 and $chainent->{currstate} =~ /^install/) or $chainent->{currstate} =~ /^winshell/) { - if (proxydhcp($nrent)) { #proxy dhcp required in uefi invocation - $lstatements = 'if option client-architecture = 00:00 or option client-architecture = 00:07 or option client-architecture = 00:09 { filename = \"\"; option vendor-class-identifier \"PXEClient\"; } else { filename = \"\"; }' . $lstatements; #If proxydhcp daemon is enable, use it. - } else { - $lstatements = 'if ' . $xnba_user_class . ' and option client-architecture = 00:00 { always-broadcast on; filename = \"http://' . $nxtsrv . $portsuffix . '/tftpboot/xcat/xnba/nodes/' . $node . '\"; } else if option client-architecture = 00:07 or option client-architecture = 00:09 { filename = \"\"; option vendor-class-identifier \"PXEClient\"; } else if option client-architecture = 00:00 { filename = \"xcat/xnba.kpxe\"; } else { filename = \"\"; }' . $lstatements; #Only PXE compliant clients should ever receive xNBA - } - } elsif ($douefi and $chainent->{currstate} ne "boot" and $chainent->{currstate} ne "iscsiboot") { - # Separate branches rather than one parenthesised - # alternation: dhcpd's expression grammar has no - # grouping, so `... and (a or b) {` is a parse error. - my $uefi_second_stage = 'filename = \"http://' . $nxtsrv . $portsuffix . '/tftpboot/xcat/xnba/nodes/' . $node . '.uefi\";'; - $lstatements = 'if ' . $xnba_user_class . ' and option client-architecture = 00:00 { always-broadcast on; filename = \"http://' . $nxtsrv . $portsuffix . '/tftpboot/xcat/xnba/nodes/' . $node . '\"; } else if ' . $xnba_user_class . ' and option client-architecture = 00:09 { ' . $uefi_second_stage . ' } else if ' . $xnba_user_class . ' and option client-architecture = 00:07 { ' . $uefi_second_stage . ' } else if option client-architecture = 00:07 { filename = \"xcat/xnba.efi\"; } else if option client-architecture = 00:00 { filename = \"xcat/xnba.kpxe\"; } else { filename = \"\"; }' . $lstatements; #Only PXE compliant clients should ever receive xNBA - } else { - $lstatements = 'if ' . $xnba_user_class . ' and option client-architecture = 00:00 { filename = \"http://' . $nxtsrv . $portsuffix . '/tftpboot/xcat/xnba/nodes/' . $node . '\"; } else if option client-architecture = 00:00 { filename = \"xcat/xnba.kpxe\"; } else { filename = \"\"; }' . $lstatements; #Only PXE compliant clients should ever receive xNBA - } - } - } #TODO: warn when windows - } elsif ($nrent and $nrent->{netboot} and $nrent->{netboot} eq 'pxe' and $lstatements !~ /filename/) { - if (-f "$tftpdir/xcat/xnba.kpxe") { - if ($chainent and $chainent->{currstate} and ($chainent->{currstate} eq 'iscsiboot' or $chainent->{currstate} eq 'boot')) { - - # S-31 again, and the same $doiscsi gate. - if ($doiscsi) { - $lstatements = 'if exists gpxe.bus-id { filename = \"\"; } else if exists client-architecture { filename = \"xcat/xnba.kpxe\"; } ' . $lstatements; - } else { - $lstatements = 'filename = \"\";' . $lstatements; - } - } else { - $lstatements = 'if option vendor-class-identifier = \"ScaleMP\" { filename = \"vsmp/pxelinux.0\"; } else { filename = \"pxelinux.0\"; }' . $lstatements; - } + if ($nrent and $nrent->{netboot} and ($nrent->{netboot} eq 'xnba' or $nrent->{netboot} eq 'pxe')) { + if ($lstatements !~ /filename/) { + $lstatements = xCAT::DHCP::BootPolicy->isc_node_boot_statements( + netboot => $nrent->{netboot}, + loader_present => -f "$tftpdir/xcat/xnba.kpxe", + iscsi => $doiscsi, + currstate => $chainent ? $chainent->{currstate} : undef, + uefi => $douefi, + proxydhcp => sub { proxydhcp($nrent) }, + node => $node, + next_server => $nxtsrv, + portsuffix => $portsuffix, + ) . $lstatements; } } elsif ($nrent and $nrent->{netboot} and $nrent->{netboot} eq 'yaboot') { $lstatements = 'filename = \"/yb/node/yaboot-' . $node . '\";' . $lstatements;