2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-10-21 07:08:12 +00:00

test(dhcp): cover the upstream loader dispatch

The packet test runs dhcpd and kea-dhcp4 in a network namespace with the
rules that BootPolicy renders, and sends relayed DHCPDISCOVER packets
from a second namespace. It checks the boot file of firmware PXE, xNBA
and iPXE clients, with each iPXE feature present or missing, for an xnba
node and an ipxe node that install, for both methods with a SAN disk,
and for an unknown node. On ISC it also covers an iSCSI node of each
method in the boot state, and repeats the node rows after a restart,
when dhcpd reads the host statements back from dhcpd.leases. On Kea it
repeats the rows of the ipxe nodes with a configuration that makedhcp
upgraded, which keeps the xNBA global classes of the older makedhcp -n.
It also checks that the UEFI clients of an xnba node get xNBA when the
server lacks the xNBA UEFI file.

The Kea validation test checks the x86 classes of xnba and ipxe nodes
with kea-dhcp4 -t. XCAT::Test::DHCP builds the packets, and the
integration case expects seven files.
This commit is contained in:
Vinícius Ferrão
2026-09-27 02:08:40 -03:00
parent 128e7fe197
commit bf5e2bdb20
5 changed files with 506 additions and 4 deletions
@@ -4,5 +4,5 @@ os:Linux
label:mn_only,ci_test,integration
cmd:prove -I/opt/xcat/lib/perl -I/opt/xcat/lib/perl/xCAT -r /opt/xcat/share/xcat/tools/autotest/integration
check:rc==0
check:output=~Files=8,
check:output=~Files=9,
end
+3 -2
View File
@@ -27,8 +27,8 @@ against it.
Note the `-I` flags: unlike the unit tests these run from the installed location, so
they pick up xCAT modules from `/opt/xcat/lib/perl` rather than from a source tree.
The case checks `rc==0` and `output=~Files=8,`. The file count checks that `prove`
found all eight files, which catches a packaging regression or a test renamed away from
The case checks `rc==0` and `output=~Files=9,`. The file count checks that `prove`
found all nine files, which catches a packaging regression or a test renamed away from
`.t` without updating the count. It does not prove that environment-gated cases ran;
inspect the TAP output for skips. **Add to that number when you add a test.** A missing
directory is already caught by `rc==0` -- `prove -r` on a path that does not exist exits
@@ -49,6 +49,7 @@ A test belongs in `integration/` when it needs something the checkout cannot pro
| `copycds_packages_integrity.t` | `/install` populated by a real `copycds` |
| `dhcp_isc_config_validation.t` | a `dhcpd` binary that can read the generated config, as root |
| `dhcp_isc_ipxe_leases.t` | `dhcpd` and `omshell`, root, and `ip netns` for a private network namespace |
| `dhcp_ipxe_dispatch.t` | `dhcpd` with `omshell`, or `kea-dhcp4`, root, and `ip netns` for two private network namespaces |
| `dhcp_kea_config_validation.t` | a `kea-dhcp4` binary that can read the generated config |
| `dhcp_kea_control_agent_smoke.t` | live `kea-dhcp4` and `kea-ctrl-agent`, root, and the Kea host-commands hook |
| `html_form_runtime_dependency.t` | the installed `HTML::Form` module |
+357
View File
@@ -0,0 +1,357 @@
#!/usr/bin/env perl
# makedhcp picks the boot file of an x86 client from its netboot method, its architecture, its user
# class and the iPXE feature options it reports in option 175. The test runs dhcpd and kea-dhcp4 in
# a network namespace with the rules that BootPolicy renders, sends relayed DHCPDISCOVER packets for
# each kind of client from a second namespace, and checks the boot file of each offer.
use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/../../perl-xCAT";
use lib "$FindBin::Bin/lib";
use File::Copy qw(copy);
use File::Path qw(make_path);
use File::Temp qw(tempdir);
use JSON ();
use Test::More;
use xCAT::CommandUtils;
use xCAT::DHCP::Backend::Kea;
use xCAT::DHCP::BootPolicy;
use xCAT::DHCP::OmapiPolicy;
use XCAT::Test::DHCP qw(start_daemon process_running stop_daemons diag_file);
plan skip_all => 'root is required to run DHCP servers in network namespaces' unless $> == 0;
my ($dhcpd) = grep { -x $_ } qw(/usr/sbin/dhcpd /usr/local/sbin/dhcpd);
my $omapi = xCAT::DHCP::OmapiPolicy->settings( site_values => {} );
my $kea_dhcp4 = xCAT::CommandUtils::find_executable('kea-dhcp4');
my @backends = ( ( $dhcpd && -x $omapi->{omshell_path} ? 'isc' : () ), ( $kea_dhcp4 ? 'kea' : () ) );
plan skip_all => 'dhcpd with omshell, or kea-dhcp4, is required' unless @backends;
my $server_ns = "xcat-test-$$-s";
my $client_ns = "xcat-test-$$-c";
my ( @namespaces, %children );
END {
stop_daemons( \%children );
system( 'ip', 'netns', 'delete', $_ ) for @namespaces;
}
for my $namespace ( $server_ns, $client_ns ) {
plan skip_all => 'ip netns is required' unless system("ip netns add $namespace >/dev/null 2>&1") == 0;
push @namespaces, $namespace;
}
my $interface = "xt$$";
for my $command (
[ $server_ns, 'link', 'set', 'lo', 'up' ],
[ $client_ns, 'link', 'set', 'lo', 'up' ],
[ $server_ns, 'link', 'add', "${interface}s", 'type', 'veth', 'peer', 'name', "${interface}c", 'netns', $client_ns ],
[ $server_ns, 'address', 'add', '192.0.2.1/24', 'dev', "${interface}s" ],
[ $client_ns, 'address', 'add', '192.0.2.2/24', 'dev', "${interface}c" ],
[ $server_ns, 'link', 'set', "${interface}s", 'up' ],
[ $client_ns, 'link', 'set', "${interface}c", 'up' ],
)
{
my ( $namespace, @args ) = @$command;
system( 'ip', '-n', $namespace, @args ) == 0 or BAIL_OUT("ip -n $namespace @args failed");
}
# The nodes: one of each method that installs, one of each that boots from SAN, and, on ISC, one of
# each in the boot state of an iSCSI node.
my %xnba = ( name => 'cn01', mac => '52:54:00:00:10:01', ip => '192.0.2.11', netboot => 'xnba' );
my %ipxe = ( name => 'cn04', mac => '52:54:00:00:10:04', ip => '192.0.2.14', netboot => 'ipxe' );
my %xnba_san = ( name => 'cn03', mac => '52:54:00:00:10:03', ip => '192.0.2.13', netboot => 'xnba', iscsi => 1 );
my %ipxe_san = ( name => 'cn06', mac => '52:54:00:00:10:06', ip => '192.0.2.16', netboot => 'ipxe', iscsi => 1 );
my %xnba_iscsi = ( name => 'cn02', mac => '52:54:00:00:10:02', ip => '192.0.2.12', netboot => 'xnba', iscsi => 1 );
my %ipxe_iscsi = ( name => 'cn05', mac => '52:54:00:00:10:05', ip => '192.0.2.15', netboot => 'ipxe', iscsi => 1 );
my $unknown = '52:54:00:00:10:09';
my $network = 'http://192.0.2.1/tftpboot/xcat/ipxe/nets/192.0.2.0_24';
my %first = (
xnba => { bios => 'xcat/xnba.kpxe', uefi => 'xcat/xnba.efi' },
ipxe => { bios => 'xcat/ipxe/i386/undionly.kpxe', uefi => 'xcat/ipxe/x86_64-sb/snponly-shim.efi' },
);
# The option 175 of an iPXE build: its bus-id, then one byte for each feature.
my %feature = ( iscsi => 17, http => 19, bzimage => 24, pxe => 33, efi => 36 );
sub ipxe { return [ [ 177, '01000000000000' ], map { [ $feature{$_}, '01' ] } @_ ] }
# The rows of a node that installs: each client, and the boot file it gets.
sub node_rows {
my ($node) = @_;
my $method = $node->{netboot};
my $script = "http://192.0.2.1/tftpboot/xcat/$method/nodes/$node->{name}";
my %loader = %{ $first{$method} };
my ( $bios_script, $uefi_script ) = $method eq 'ipxe' ? ( $script, "$script.uefi" ) : @loader{qw(bios uefi)};
my %c = ( mac => $node->{mac} );
# UEFI firmware reports architecture 7 or 9. The ISC host statements of an xnba node give nothing
# to a client on 9 that is not xNBA, as they did before, so these rows are for ipxe nodes.
my @arch9 = $method eq 'ipxe' ? (
[ 'firmware PXE, UEFI arch 9', { arch => 9 }, $loader{uefi} ],
[ 'iPXE with HTTP and EFI, UEFI arch 9', { arch => 9, user_class => 'iPXE', ipxe => ipxe(qw(http efi)) }, $uefi_script ],
[ 'iPXE without EFI, UEFI arch 9', { arch => 9, user_class => 'iPXE', ipxe => ipxe(qw(http)) }, $loader{uefi} ],
) : ();
return map { [ "$method node, $_->[0]", { %c, %{ $_->[1] } }, $_->[2] ] } @arch9, (
[ 'firmware PXE, BIOS', { arch => 0 }, $loader{bios} ],
[ 'firmware PXE, UEFI', { arch => 7 }, $loader{uefi} ],
[ 'xNBA, BIOS', { arch => 0, user_class => 'xNBA', ipxe => ipxe(qw(http bzimage pxe iscsi)) }, $script ],
[ 'xNBA, UEFI arch 7', { arch => 7, user_class => 'xNBA', ipxe => ipxe(qw(http efi iscsi)) }, "$script.uefi" ],
[ 'xNBA, UEFI arch 9', { arch => 9, user_class => 'xNBA', ipxe => ipxe(qw(http efi iscsi)) }, "$script.uefi" ],
[ 'iPXE with HTTP, bzImage and PXE, BIOS', { arch => 0, user_class => 'iPXE', ipxe => ipxe(qw(http bzimage pxe)) }, $bios_script ],
[ 'iPXE without HTTP, BIOS', { arch => 0, user_class => 'iPXE', ipxe => ipxe(qw(bzimage pxe)) }, $loader{bios} ],
[ 'iPXE without bzImage, BIOS', { arch => 0, user_class => 'iPXE', ipxe => ipxe(qw(http pxe)) }, $loader{bios} ],
[ 'iPXE without PXE, BIOS', { arch => 0, user_class => 'iPXE', ipxe => ipxe(qw(http bzimage)) }, $loader{bios} ],
[ 'iPXE with HTTP and EFI, UEFI', { arch => 7, user_class => 'iPXE', ipxe => ipxe(qw(http efi)) }, $uefi_script ],
[ 'iPXE without HTTP, UEFI', { arch => 7, user_class => 'iPXE', ipxe => ipxe(qw(efi)) }, $loader{uefi} ],
[ 'iPXE without EFI, UEFI', { arch => 7, user_class => 'iPXE', ipxe => ipxe(qw(http)) }, $loader{uefi} ],
[ 'iPXE without option 175, BIOS', { arch => 0, user_class => 'iPXE' }, $loader{bios} ],
[ 'iPXE with its sub-options reversed, BIOS', { arch => 0, user_class => 'iPXE', ipxe => [ reverse @{ ipxe(qw(http bzimage pxe)) } ] }, $bios_script ],
);
}
# The rows of a SAN node that installs: iPXE hooks its iSCSI root path first, so the script of an
# ipxe node needs iSCSI. An xnba node gives its script to xNBA only.
sub san_rows {
my ($node) = @_;
my $method = $node->{netboot};
my $script = "http://192.0.2.1/tftpboot/xcat/$method/nodes/$node->{name}";
my %loader = %{ $first{$method} };
my $ipxe = $method eq 'ipxe';
my %c = ( mac => $node->{mac} );
return map { [ "$method SAN node installing, $_->[0]", { %c, %{ $_->[1] } }, $_->[2] ] } (
[ 'xNBA, BIOS', { arch => 0, user_class => 'xNBA', ipxe => ipxe(qw(http bzimage pxe iscsi)) }, $script ],
[ 'iPXE with iSCSI, BIOS', { arch => 0, user_class => 'iPXE', ipxe => ipxe(qw(http bzimage pxe iscsi)) }, $ipxe ? $script : $loader{bios} ],
[ 'iPXE without iSCSI, BIOS', { arch => 0, user_class => 'iPXE', ipxe => ipxe(qw(http bzimage pxe)) }, $loader{bios} ],
[ 'iPXE with iSCSI, UEFI', { arch => 7, user_class => 'iPXE', ipxe => ipxe(qw(http efi iscsi)) }, $ipxe ? "$script.uefi" : $loader{uefi} ],
[ 'iPXE without iSCSI, UEFI', { arch => 7, user_class => 'iPXE', ipxe => ipxe(qw(http efi)) }, $loader{uefi} ],
);
}
my @node_rows = ( node_rows( \%xnba ), node_rows( \%ipxe ) );
my @san_rows = ( san_rows( \%xnba_san ), san_rows( \%ipxe_san ) );
my @network_rows = (
[ 'unknown node, firmware PXE, BIOS', { mac => $unknown, arch => 0 }, $first{ipxe}{bios} ],
[ 'unknown node, firmware PXE, UEFI', { mac => $unknown, arch => 7 }, $first{ipxe}{uefi} ],
[ 'unknown node, iPXE with every feature, BIOS', { mac => $unknown, arch => 0, user_class => 'iPXE', ipxe => ipxe(qw(http bzimage pxe)) }, $network ],
[ 'unknown node, iPXE with every feature, UEFI', { mac => $unknown, arch => 7, user_class => 'iPXE', ipxe => ipxe(qw(http efi)) }, "$network.uefi" ],
[ 'unknown node, iPXE without HTTP, BIOS', { mac => $unknown, arch => 0, user_class => 'iPXE', ipxe => ipxe(qw(bzimage pxe)) }, $first{ipxe}{bios} ],
[ 'unknown node, xNBA with every feature, BIOS', { mac => $unknown, arch => 0, user_class => 'xNBA', ipxe => ipxe(qw(http bzimage pxe)) }, $network ],
);
# In the boot state ISC gives a SAN client the empty file name. Kea keeps its own iSCSI handling of
# that state, so these rows are for ISC only.
my @iscsi_rows = (
[ 'xnba iSCSI node, iPXE with iSCSI', { mac => $xnba_iscsi{mac}, arch => 0, user_class => 'iPXE', ipxe => ipxe(qw(http bzimage pxe iscsi)) }, '' ],
[ 'xnba iSCSI node, iPXE without iSCSI', { mac => $xnba_iscsi{mac}, arch => 0, user_class => 'iPXE', ipxe => ipxe(qw(http bzimage pxe)) }, '' ],
[ 'ipxe iSCSI node, iPXE with iSCSI', { mac => $ipxe_iscsi{mac}, arch => 0, user_class => 'iPXE', ipxe => ipxe(qw(http bzimage pxe iscsi)) }, '' ],
[ 'ipxe iSCSI node, iPXE without iSCSI', { mac => $ipxe_iscsi{mac}, arch => 0, user_class => 'iPXE', ipxe => ipxe(qw(http bzimage pxe)) }, $first{ipxe}{bios} ],
[ 'ipxe iSCSI node, firmware PXE, UEFI arch 7', { mac => $ipxe_iscsi{mac}, arch => 7 }, $first{ipxe}{uefi} ],
[ 'ipxe iSCSI node, firmware PXE, UEFI arch 9', { mac => $ipxe_iscsi{mac}, arch => 9 }, $first{ipxe}{uefi} ],
[ 'ipxe iSCSI node, iPXE with iSCSI, UEFI', { mac => $ipxe_iscsi{mac}, arch => 7, user_class => 'iPXE', ipxe => ipxe(qw(http efi iscsi)) }, '' ],
);
sub boot_file {
my ($client) = @_;
my $json = JSON::encode_json( { %$client, relay => '192.0.2.2', server => '192.0.2.1' } );
open( my $fh, '-|', 'ip', 'netns', 'exec', $client_ns, $^X, "-I$FindBin::Bin/lib",
'-MXCAT::Test::DHCP', '-e', 'XCAT::Test::DHCP::relay_discover_main(@ARGV)', $json )
or die "Unable to run the DHCP client: $!";
my $file = <$fh>;
close($fh);
chomp( $file //= 'NO-OFFER' );
return $file;
}
sub check_rows {
my ( $label, @rows ) = @_;
is( boot_file( $_->[1] ), $_->[2], "$label: $_->[0]" ) for @rows;
}
sub wait_for_offers {
my ( $pid, $log ) = @_;
for ( 1 .. 20 ) {
last unless process_running( $pid, \%children );
return 1 if boot_file( { mac => $unknown, arch => 0 } ) ne 'NO-OFFER';
}
diag_file($log);
BAIL_OUT('the DHCP server did not answer');
}
# ---- ISC ------------------------------------------------------------------------------------------
# The dhcpd AppArmor profile of Ubuntu reads only /etc/dhcp and writes only /var/lib/dhcp/dhcpd.leases*.
my $secret = 'eGNhdC10ZXN0LW9tYXBpLXNlY3JldC0wMQ==';
my $isc_dir = tempdir( DIR => -d '/etc/dhcp' ? '/etc/dhcp' : '/etc', CLEANUP => 1 );
my $leases = -d '/var/lib/dhcp' ? "/var/lib/dhcp/dhcpd.leases.xcat-test-$$" : "$isc_dir/dhcpd.leases";
END { unlink glob("$leases*") if $leases }
sub omshell {
my ($commands) = @_;
my $input = "$isc_dir/omshell.in";
open( my $fh, '>', $input ) or die "Cannot create $input: $!";
print {$fh} xCAT::DHCP::OmapiPolicy->omshell_preamble( $omapi, secret => $secret, port => 7911 ), "connect\n", $commands;
close($fh) or die "Cannot close $input: $!";
return scalar `ip netns exec $server_ns $omapi->{omshell_path} < $input 2>&1`;
}
sub start_isc {
my $configuration = "$isc_dir/dhcpd.conf";
open( my $fh, '>', $configuration ) or die "Cannot create $configuration: $!";
print {$fh} "option conf-file code 209 = text;\n",
"option space gpxe;\n",
"option gpxe-encap-opts code 175 = encapsulate gpxe;\n",
"option gpxe.bus-id code 177 = string;\n",
@{ xCAT::DHCP::BootPolicy->isc_ipxe_feature_option_lines() },
"option user-class-identifier code 77 = string;\n",
"option client-architecture code 93 = unsigned integer 16;\n",
"option www-server code 114 = string;\n",
"default-lease-time 600;\nmax-lease-time 600;\nping-check false;\n",
"omapi-port 7911;\n",
"key $omapi->{key_name} {\n algorithm $omapi->{algorithm};\n secret \"$secret\";\n};\n",
"omapi-key $omapi->{key_name};\n",
"subnet 192.0.2.0 netmask 255.255.255.0 {\n",
" range 192.0.2.100 192.0.2.110;\n",
@{ xCAT::DHCP::BootPolicy->isc_client_architecture_lines(
next_server => '192.0.2.1',
portsuffix => '',
net => '192.0.2.0',
prefix => 24,
) },
"}\n";
close($fh) or die "Cannot close $configuration: $!";
my $log = "$isc_dir/dhcpd.log";
my $pid = start_daemon( undef, 'ip', $log, 'netns', 'exec', $server_ns, $dhcpd, '-f', '-d',
'-cf', $configuration, '-lf', $leases, '-pf', "$isc_dir/dhcpd.pid", "${interface}s" );
$children{$pid} = 1;
return wait_for_offers( $pid, $log );
}
sub isc_hosts {
my %common = ( loader_present => 1, next_server => '192.0.2.1', portsuffix => '' );
my @hosts = (
[ \%xnba, { uefi => 1, currstate => 'install rhels9' } ],
[ \%ipxe, { uefi => 1, currstate => 'install rhels9' } ],
[ \%xnba_san, { uefi => 1, currstate => 'install rhels9' } ],
[ \%ipxe_san, { uefi => 1, currstate => 'install rhels9' } ],
[ \%xnba_iscsi, { currstate => 'boot' } ],
[ \%ipxe_iscsi, { currstate => 'boot' } ],
);
my $commands = '';
for my $host (@hosts) {
my ( $node, $opts ) = @$host;
my $statements = xCAT::DHCP::BootPolicy->isc_node_boot_statements(
%common, %$opts, netboot => $node->{netboot}, iscsi => $node->{iscsi}, node => $node->{name} );
$commands .= "new host\nset name = \"$node->{name}\"\nset hardware-address = $node->{mac}\n"
. "set hardware-type = 1\nset ip-address = $node->{ip}\nset statements = \"$statements\"\ncreate\nclose\n";
}
return omshell($commands);
}
if ( grep { $_ eq 'isc' } @backends ) {
open( my $fresh, '>', $leases ) or die "Cannot create $leases: $!";
close($fresh);
start_isc();
my $created = isc_hosts();
unlike( $created, qr/can't|error|eof in string|unknown token|not connected/i, 'ISC: OMAPI creates the hosts' )
or diag($created);
check_rows( 'ISC', @node_rows, @network_rows, @san_rows, @iscsi_rows );
# dhcpd reads the host statements back from dhcpd.leases, where it saved them without parentheses.
stop_daemons( \%children );
start_isc();
check_rows( 'ISC after a restart', @node_rows, @san_rows, @iscsi_rows );
stop_daemons( \%children );
}
# ---- Kea ------------------------------------------------------------------------------------------
if ( grep { $_ eq 'kea' } @backends ) {
my $kea_dir = tempdir( CLEANUP => 1 );
chmod 0755, $kea_dir or die "Unable to make $kea_dir traversable: $!";
# The kea-dhcp4 AppArmor profile of Ubuntu writes only the files of the system service. No profile
# confines a copy of kea-dhcp4, so the test keeps its files in a temporary directory.
my $kea_copy = "$kea_dir/kea-dhcp4";
copy( $kea_dhcp4, $kea_copy ) or die "Unable to copy $kea_dhcp4: $!";
chmod 0755, $kea_copy or die "Unable to make $kea_copy executable: $!";
make_path( "$kea_dir/run", "$kea_dir/data" );
local $ENV{KEA_CONTROL_SOCKET_DIR} = "$kea_dir/run";
local $ENV{KEA_DHCP_DATA_DIR} = "$kea_dir/data";
local $ENV{KEA_PIDFILE_DIR} = "$kea_dir/run";
local $ENV{KEA_LOCKFILE_DIR} = "$kea_dir/run";
my $backend = xCAT::DHCP::Backend::Kea->new( kea_socket_dir => "$kea_dir/run" );
# The server has the upstream loader files, as ipxe-xcat installs them.
my %upstream = ( ipxe_bios => 1, ipxe_uefi => 1 );
my $network_classes = xCAT::DHCP::BootPolicy->kea_xnba_network_classes(
net => '192.0.2.0', prefix => 24, next_server => '192.0.2.1', %upstream );
my $run_kea = sub {
my ( $label, $option_defs, $globals, $xnba_efi, @rows ) = @_;
my $config = JSON::decode_json( $backend->render_dhcp4_config(
{
interfaces => ["${interface}s"],
'lease-database' => { type => 'memfile', name => "$kea_dir/data/kea-leases4.csv", persist => JSON::false },
'option-def' => $option_defs,
'client-classes' => [
@{ xCAT::DHCP::BootPolicy->kea_xnba_node_classes(
xnba_kpxe => 1,
xnba_efi => $xnba_efi,
nodes => [
map { { node => $_->{name}, mac => $_->{mac}, next_server => '192.0.2.1', netboot => $_->{netboot}, iscsi => $_->{iscsi} } }
\%xnba, \%ipxe, \%xnba_san, \%ipxe_san
],
) },
@$globals,
@$network_classes,
],
subnets => [
{
id => 1,
subnet => '192.0.2.0/24',
dynamicrange => '192.0.2.100-192.0.2.110',
next_server => '192.0.2.1',
additional_client_classes => [ map { $_->{name} } @$network_classes ],
},
],
}
) );
# The test sends relayed unicast packets, which need no raw socket.
$config->{Dhcp4}{'interfaces-config'}{'dhcp-socket-type'} = 'udp';
my $path = "$kea_dir/kea-dhcp4.conf";
open( my $fh, '>', $path ) or die "Cannot create $path: $!";
print {$fh} JSON::encode_json($config);
close($fh) or die "Cannot close $path: $!";
my $log = "$kea_dir/kea-dhcp4.log";
my $pid = start_daemon( undef, 'ip', $log, 'netns', 'exec', $server_ns, $kea_copy, '-c', $path );
$children{$pid} = 1;
wait_for_offers( $pid, $log );
check_rows( $label, @rows );
stop_daemons( \%children );
};
my @old_defs = ( { name => 'conf-file', code => 209, type => 'string', space => 'dhcp4' } );
my @defs = ( @old_defs, @{ xCAT::DHCP::BootPolicy->kea_ipxe_option_defs() } );
my $globals = xCAT::DHCP::BootPolicy->kea_client_classes(%upstream);
$run_kea->( 'Kea', \@defs, $globals, 1, @node_rows, @network_rows, @san_rows );
# Without the local xNBA UEFI file the UEFI clients of an xnba node still get that file, as in ISC, and
# not the upstream loader and the discovery script that the global classes give unknown clients.
$run_kea->( 'Kea, no xNBA UEFI file', \@defs, $globals, 0, map { [ "xnba node, $_->[0]", { mac => $xnba{mac}, %{ $_->[1] } }, $first{xnba}{uefi} ] } (
[ 'firmware PXE, UEFI', { arch => 7 } ],
[ 'iPXE with HTTP and EFI, UEFI', { arch => 7, user_class => 'iPXE', ipxe => ipxe(qw(http efi)) } ],
) );
# makedhcp without -n adds the iPXE feature options to the configuration of an older makedhcp -n,
# and keeps the x86 global classes that it wrote, which give xNBA to every client.
my $upgraded = { 'option-def' => [@old_defs] };
xCAT::DHCP::BootPolicy->kea_declare_ipxe_features($upgraded);
my $xnba_test = "(option[77].exists and (option[77].text == 'xNBA' or option[77].hex == 0x784e4241 or substring(option[77].hex,1,4) == 'xNBA'))";
my @old_globals = (
{ name => 'xcat-bios', test => "option[93].hex == 0x0000 and not ($xnba_test)", 'boot-file-name' => 'xcat/xnba.kpxe' },
{
name => 'xcat-uefi-x64',
test => "(option[93].hex == 0x0007 or option[93].hex == 0x0009 or option[93].hex == 0x0010) and not ($xnba_test)",
'boot-file-name' => 'xcat/xnba.efi',
},
);
$run_kea->( 'Kea, upgraded', $upgraded->{'option-def'}, \@old_globals, 1, grep { $_->[0] =~ /^ipxe / } @node_rows, @san_rows );
}
done_testing();
@@ -135,6 +135,51 @@ ok( !$result->{error}, 'generated Kea DHCPv4 config validates with kea-dhcp4 -t'
}
unlink $path;
# The x86 boot classes as makedhcp renders them, with an xnba node, an ipxe node and an ipxe node that
# boots from SAN, and the option 175 definitions that the upstream loader classes test.
{
my $boot_json = $backend->render_dhcp4_config(
{
interfaces => ['*'],
'option-def' => [
{ name => 'conf-file', code => 209, type => 'string', space => 'dhcp4' },
@{ xCAT::DHCP::BootPolicy->kea_ipxe_option_defs() },
],
'client-classes' => [
@{ xCAT::DHCP::BootPolicy->kea_xnba_node_classes(
xnba_kpxe => 1,
xnba_efi => 1,
nodes => [
{ node => 'node01', mac => '52:54:00:12:34:56', next_server => '192.168.122.1', httpport => 80, netboot => 'xnba' },
{ node => 'node02', mac => '52:54:00:12:34:57', next_server => '192.168.122.1', httpport => 80, netboot => 'ipxe' },
{ node => 'node03', mac => '52:54:00:12:34:58', next_server => '192.168.122.1', httpport => 80, netboot => 'ipxe', iscsi => 1 },
],
) },
@{ xCAT::DHCP::BootPolicy->kea_client_classes() },
@{ xCAT::DHCP::BootPolicy->kea_xnba_network_classes(
net => '192.168.122.0',
prefix => 24,
next_server => '192.168.122.1',
httpport => 80,
) },
],
subnets => [
{
id => 1,
subnet => '192.168.122.0/24',
dynamicrange => '192.168.122.100-192.168.122.120',
next_server => '192.168.122.1',
},
],
}
);
my $boot_path = write_validation_file( $boot_json, 'xcat-test-kea-dhcp4-boot' );
my $boot_result = $backend->validate_dhcp4_config($boot_path);
ok( !$boot_result->{error}, 'the x86 boot classes of xnba and ipxe nodes validate with kea-dhcp4 -t' )
or diag $boot_result->{error};
unlink $boot_path;
}
SKIP: {
skip 'kea-dhcp6 is not installed', 1 unless xCAT::CommandUtils::find_executable('kea-dhcp6');
my $dhcp6_json = $backend->render_dhcp6_config(
+100 -1
View File
@@ -7,9 +7,12 @@ use strict;
use warnings;
use Exporter qw(import);
use IO::Select;
use IO::Socket::INET;
use POSIX qw(WNOHANG _exit setgid setuid);
use Socket qw(inet_aton pack_sockaddr_in);
use Test::More ();
use Time::HiRes qw(sleep);
use Time::HiRes qw(sleep time);
our @EXPORT_OK = qw(
start_daemon
@@ -18,6 +21,10 @@ our @EXPORT_OK = qw(
wait_for_socket
wait_for_file
diag_file
discover_packet
offer_boot_file
relay_discover
relay_discover_main
);
sub start_daemon {
@@ -126,4 +133,96 @@ sub diag_file {
return;
}
# A DHCPDISCOVER that a relay agent at $opts{relay} forwards for a PXE client. $opts{ipxe} lists the
# option 175 sub-options in the order to send, as [code, value] pairs.
sub discover_packet {
my (%opts) = @_;
my $mac = pack( 'H12', join '', split /:/, $opts{mac} );
my $packet = pack( 'C4 N n n a4 a4 a4 a4 a16 a64 a128 N',
1, 1, 6, 1, $opts{xid}, 0, 0, "\0" x 4, "\0" x 4, "\0" x 4, inet_aton( $opts{relay} ),
$mac, '', '', 0x63825363 );
my @options = (
[ 53, pack( 'C', 1 ) ],
[ 61, "\x01$mac" ],
[ 60, sprintf( 'PXEClient:Arch:%05d:UNDI:002001', $opts{arch} ) ],
[ 93, pack( 'n', $opts{arch} ) ],
[ 97, "\0" . substr( $mac x 3, 0, 16 ) ],
);
push @options, [ 77, $opts{user_class} ] if defined $opts{user_class};
push @options, [ 175, join '', map { pack( 'CC', $_->[0], length $_->[1] ) . $_->[1] } @{ $opts{ipxe} } ]
if $opts{ipxe};
$packet .= pack( 'CC', $_->[0], length $_->[1] ) . $_->[1] for @options;
return $packet . "\xff";
}
# The boot file of a DHCPOFFER for transaction $xid: option 67 when the server sends it, otherwise
# the file field. Nothing for any other packet.
sub offer_boot_file {
my ( $packet, $xid ) = @_;
return if length($packet) < 240;
my ( $op, $got_xid, $file, $cookie ) = unpack( 'C x3 N x100 Z128 N', $packet );
return if $op != 2 || $got_xid != $xid || $cookie != 0x63825363;
my ( $type, $option67 );
my $offset = 240;
while ( $offset < length $packet ) {
my $code = ord substr( $packet, $offset, 1 );
last if $code == 255;
if ( $code == 0 ) { $offset++; next; }
my $length = ord substr( $packet, $offset + 1, 1 );
my $value = substr( $packet, $offset + 2, $length );
$type = ord $value if $code == 53;
( $option67 = $value ) =~ s/\0+\z// if $code == 67;
$offset += 2 + $length;
}
return if !defined $type || $type != 2;
return defined $option67 ? $option67 : $file;
}
# Send a relayed DHCPDISCOVER to the server and return the boot file of its DHCPOFFER, or undef when
# no offer comes. Each try waits 3 s, and there are 3 tries.
sub relay_discover {
my (%opts) = @_;
my $socket = IO::Socket::INET->new(
LocalAddr => $opts{relay},
LocalPort => 67,
Proto => 'udp',
ReuseAddr => 1,
) or die "Unable to bind $opts{relay}:67: $!";
my $server = pack_sockaddr_in( 67, inet_aton( $opts{server} ) );
my $select = IO::Select->new($socket);
for my $try ( 1 .. 3 ) {
my $xid = int( rand( 0xffffffff ) );
send( $socket, discover_packet( %opts, xid => $xid ), 0, $server ) or die "Unable to send: $!";
my $deadline = time + 3;
while ( ( my $left = $deadline - time ) > 0 ) {
last unless $select->can_read($left);
my $reply = '';
recv( $socket, $reply, 1500, 0 );
my $file = offer_boot_file( $reply, $xid );
return $file if defined $file;
}
}
return;
}
# Run relay_discover for the client that a JSON object describes, with each option 175 value in hex,
# and print the boot file or NO-OFFER. A test runs it in the client network namespace.
sub relay_discover_main {
my ($json) = @_;
require JSON;
my $opts = JSON::decode_json($json);
$opts->{ipxe} = [ map { [ $_->[0], pack( 'H*', $_->[1] ) ] } @{ $opts->{ipxe} } ] if $opts->{ipxe};
my $file = relay_discover(%$opts);
print defined $file ? "$file\n" : "NO-OFFER\n";
return;
}
1;