mirror of
https://github.com/xcat2/xcat-core.git
synced 2026-10-06 17:46:55 +00:00
fix(dhcp): split the per-node xNBA UEFI branch dhcpd could not parse
The per-node statement for a UEFI node running netboot=xnba matched its two
architecture ids with a parenthesised alternation:
else if <user class> and (option client-architecture = 00:09
or option client-architecture = 00:07) { ... }
ISC dhcpd has no parenthesised grouping in its expression grammar, so that
condition cannot parse. The node-level second stage it guards -- the
per-node .uefi script that keeps two machines chainloading at the same
moment from running the same script -- has never been reachable.
Written as two branches instead, one per architecture id, which is how the
per-network chain in BootPolicy.pm already spells the same test.
dhcp_isc_expression_grouping.t is a guard rather than a test of this branch:
the per-node statements are built inline in addnode against a live database
and cannot be called from a unit test, so it scans the plugin for the one
construct that produces the failure and checks the rendered per-network
chain and the user class test for it as well. dhcpd's tokens only -- Perl's
own parenthesised `exists` and `not` are excluded, and a bareword `option`
after an opening paren cannot be Perl.
This commit is contained in:
@@ -1188,7 +1188,12 @@ sub addnode
|
||||
$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") {
|
||||
$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 or option client-architecture = 00:07) { filename = \"http://' . $nxtsrv . $portsuffix . '/tftpboot/xcat/xnba/nodes/' . $node . '.uefi\"; } 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
|
||||
# The two UEFI architecture ids are written as 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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env perl
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use FindBin;
|
||||
use lib "$FindBin::Bin/../../perl-xCAT";
|
||||
use Test::More;
|
||||
|
||||
use xCAT::DHCP::BootPolicy;
|
||||
|
||||
# ISC dhcpd's expression grammar has no parenthesised grouping. dhcp-eval(5)
|
||||
# documents exactly three boolean forms -- `not E`, `E1 and E2`, `E1 or E2` --
|
||||
# and nothing that groups them, so a condition written as
|
||||
#
|
||||
# if (option user-class-identifier = "xNBA" or ...) and option client-architecture = 00:00 {
|
||||
#
|
||||
# is rejected at the opening paren:
|
||||
#
|
||||
# /etc/dhcp/dhcpd.conf line 11: left brace expected.
|
||||
#
|
||||
# followed by a cascade of "expecting a parameter or declaration" at every
|
||||
# `} else` after it. The daemon does not start, so the whole cluster stops
|
||||
# answering DHCP -- not just the branch that was mis-written.
|
||||
#
|
||||
# There is no way to unit test the per-node statements in dhcp.pm directly:
|
||||
# they are built inline inside addnode against a live database. What can be
|
||||
# checked cheaply is that no generated ISC condition anywhere in the plugin
|
||||
# groups a boolean with parentheses, which is the only construct that produces
|
||||
# this failure. A function call -- substring(...), suffix(...), binary-to-ascii
|
||||
# -- is fine and is deliberately not matched: `option` never follows an opening
|
||||
# paren in a call, only in a grouped comparison.
|
||||
#
|
||||
# The tokens looked for are dhcpd's, not Perl's: `exists` and `not` are left out
|
||||
# because the plugin's own Perl uses them parenthesised on nearly every page,
|
||||
# and a bareword `option` or `filename` immediately after `(` cannot be Perl --
|
||||
# a Perl variable there would carry its sigil.
|
||||
|
||||
my $grouping = qr/\b(?:if|and|or)\s+\(\s*(?:option|substring|suffix|hardware|packet|filename)\b/;
|
||||
|
||||
my $plugin = "$FindBin::Bin/../../xCAT-server/lib/xcat/plugins/dhcp.pm";
|
||||
open( my $fh, '<', $plugin ) or BAIL_OUT("cannot read $plugin: $!");
|
||||
my @offenders;
|
||||
while ( my $line = <$fh> ) {
|
||||
next if $line =~ /^\s*#/;
|
||||
push @offenders, "$.: $line" if $line =~ $grouping;
|
||||
}
|
||||
close $fh;
|
||||
|
||||
is_deeply( \@offenders, [],
|
||||
'no ISC condition in the dhcp plugin groups a boolean with parentheses' )
|
||||
or diag("dhcpd cannot parse these:\n@offenders");
|
||||
|
||||
# The same rule for the per-network architecture chain, checked against what it
|
||||
# actually renders rather than against its source.
|
||||
my $rendered = join '', @{ xCAT::DHCP::BootPolicy->isc_client_architecture_lines(
|
||||
next_server => '192.0.2.10',
|
||||
portsuffix => ':8080',
|
||||
net => '192.0.2.0',
|
||||
prefix => 24,
|
||||
) };
|
||||
|
||||
unlike( $rendered, $grouping,
|
||||
'the rendered per-network architecture chain groups nothing' );
|
||||
|
||||
# And the user class test itself, which is spliced into conditions that already
|
||||
# carry a trailing `and option client-architecture = ...`.
|
||||
foreach my $quote ( '"', '\\"' ) {
|
||||
my $test = xCAT::DHCP::BootPolicy->isc_xnba_user_class_test(quote => $quote);
|
||||
unlike( $test, qr/^\(/,
|
||||
'the xNBA user class test is a single ungrouped expression' );
|
||||
}
|
||||
|
||||
done_testing();
|
||||
Reference in New Issue
Block a user