2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-10-22 07:38:08 +00:00
Files
xcat-core/xCAT-test/unit/dhcp_debian_interfaces.t
T
Daniel Hilst 38f395f64d test(xcat-core): read the interfaces config without starting sh
launched_with wrote the generated isc-dhcp-server file to disk and ran
sh to source it, so every check also depended on sh and hid its errors.
The writer emits only KEY="value" lines. launched_with now returns the
value of the last assignment to the variable, as sh would see it.

Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
2026-09-25 12:33:11 -03:00

197 lines
8.4 KiB
Perl

#!/usr/bin/env perl
use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/../../xCAT-server/lib";
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
use lib "$FindBin::Bin/../../perl-xCAT";
use Test::More;
$ENV{XCATCFG} ||= 'SQLite:/tmp';
my $source_dhcp_plugin = "$FindBin::Bin/../../xCAT-server/lib/xcat/plugins/dhcp.pm";
if ( -f $source_dhcp_plugin ) {
require $source_dhcp_plugin;
} else {
require xCAT_plugin::dhcp;
}
# What `makedhcp` writes into /etc/default/isc-dhcp-server matters for one
# reason: it decides which interfaces dhcpd is launched with. So assert that,
# not the name of any variable.
#
# isc-dhcp-server.service sources that file and expands one variable onto the
# command line, and which one changed with the package. Verified by unpacking
# the archive's own debs:
#
# trusty 4.2.4-7ubuntu12 sysvinit only INTERFACES
# xenial 4.3.3-5ubuntu12 unit $INTERFACES
# bionic 4.3.5-3ubuntu7 unit $INTERFACES
# focal 4.4.1-2.1ubuntu5 unit $INTERFACES
# jammy 4.4.1-2.3ubuntu2 unit $INTERFACESv4
# noble 4.4.3-P1-4ubuntu2 unit $INTERFACESv4 (+ v6 unit)
#
# dhcpd given no interface argument does not fail: it binds every interface it
# can find. So the damage is silent -- a management node naming one provisioning
# NIC in site.dhcpinterfaces gets a daemon listening on all of them.
# The stock file as the package ships it on bionic and later.
my $stock = <<'EOF';
# Defaults for isc-dhcp-server (sourced by /etc/init.d/isc-dhcp-server)
# Path to dhcpd's config file (default: /etc/dhcp/dhcpd.conf).
#DHCPDv4_CONF=/etc/dhcp/dhcpd.conf
# On what interfaces should the DHCP server (dhcpd) serve DHCP requests?
INTERFACESv4=""
INTERFACESv6=""
EOF
# The value of the last assignment to $variable, as sh would see it after
# sourcing the file. The writer emits only KEY="value" lines.
sub launched_with {
my ($content, $variable) = @_;
my ($value) = (reverse($content =~ /^\s*\Q$variable\E=(.*)$/mg), '');
$value =~ s/^"(.*)"$/$1/;
return $value;
}
# Every release in support, the package it ships, and the variable its unit
# expands. This table is the contract: the writer has to satisfy every row.
my @RELEASES = (
[ '16.04', '4.3.3-5ubuntu12', 'INTERFACES' ],
[ '18.04', '4.3.5-3ubuntu7', 'INTERFACES' ],
[ '20.04', '4.4.1-2.1ubuntu5', 'INTERFACES' ],
[ '22.04', '4.4.1-2.3ubuntu2', 'INTERFACESv4' ],
[ '24.04', '4.4.3-P1-4ubuntu2', 'INTERFACESv4' ],
[ '26.04', '4.4.3-P1-4ubuntu2', 'INTERFACESv4' ],
);
sub written_for {
my ($version, $nics, $content) = @_;
my @keys = xCAT_plugin::dhcp::debian_sysconfig_interface_keys($version);
return xCAT_plugin::dhcp::_sysconfig_interfaces_content(
defined($content) ? $content : $stock, [@keys], $nics);
}
# makedhcp is serving one provisioning NIC, named in site.dhcpinterfaces. On
# every release, that NIC has to be what dhcpd is launched with.
foreach my $release (@RELEASES) {
my ($ubuntu, $version, $variable) = @{$release};
my $written = written_for($version, ['eth1']);
is( launched_with($written, $variable), 'eth1',
"$ubuntu ($version): dhcpd is launched restricted to the interface xCAT serves" );
isnt( launched_with($written, $variable), '',
"$ubuntu ($version): dhcpd is not left to bind every interface on the machine" );
# Serving several interfaces must reach the daemon as several interfaces.
my $multi = written_for($version, ['eth1', 'eth2']);
my @served = sort split /\s+/, launched_with($multi, $variable);
is_deeply( \@served, ['eth1', 'eth2'],
"$ubuntu ($version): every served interface reaches dhcpd" );
# A remote (service node) interface is not something this daemon can bind.
my $remote = written_for($version, ['eth1', '!remote!eth9']);
unlike( launched_with($remote, $variable), qr/remote/,
"$ubuntu ($version): a !remote! interface is not passed to the local daemon" );
# The v6 unit takes $INTERFACES before 22.04 and $INTERFACESv6 after. Leaving
# its variable empty is how dhcpd6, once enabled, ends up bound to everything.
my $v6 = $variable eq 'INTERFACES' ? 'INTERFACES' : 'INTERFACESv6';
is( launched_with($written, $v6), 'eth1',
"$ubuntu ($version): the v6 unit is restricted to the same interfaces" );
# A value the package's debconf prompt or an admin left behind has to lose
# to site.dhcpinterfaces, or the setting is decoration.
my $preset = $stock;
$preset =~ s/^\Q$variable\E=.*$/$variable="eth0"/m
or $preset .= qq{$variable="eth0"\n};
my $overridden = written_for($version, ['eth1'], $preset);
is( launched_with($overridden, $variable), 'eth1',
"$ubuntu ($version): a value left by debconf is replaced" );
# Whichever variable is not the one this release reads must still be left
# alone, not claimed by a prefix match.
foreach my $other (grep { $_ ne $variable } qw(INTERFACESv4 INTERFACESv6)) {
next if ($written =~ m/^\s*\Q$other\E\s*=/m);
fail("$ubuntu ($version): the $other line the package ships was removed");
}
}
# With no package version to go on, err towards writing every spelling: an unset
# variable is what leaves dhcpd bound to everything.
foreach my $unknown (undef, '', 'none') {
my @keys = xCAT_plugin::dhcp::debian_sysconfig_interface_keys($unknown);
my $written = xCAT_plugin::dhcp::_sysconfig_interfaces_content(
$stock, [@keys], ['eth1']);
foreach my $variable (qw(INTERFACES INTERFACESv4 INTERFACESv6)) {
is( launched_with($written, $variable), 'eth1',
'an unknown package version still restricts dhcpd, via ' . $variable );
}
}
# ...and on every machine, not just one with no isc-dhcp-server. A writer that
# asks the local dpkg when handed no version answers differently on the build
# host than on the machine being configured.
{
no warnings qw(redefine once);
local *xCAT_plugin::dhcp::isc_dhcp_installed_version = sub {
return '4.4.3-P1-4ubuntu2';
};
my @keys = xCAT_plugin::dhcp::debian_sysconfig_interface_keys(undef);
my $written = xCAT_plugin::dhcp::_sysconfig_interfaces_content(
$stock, [@keys], ['eth1']);
is( launched_with($written, 'INTERFACES'), 'eth1',
'an unknown version is not silently replaced by the local package version' );
}
# Ordering of the package versions themselves. 20.04 and 22.04 both ship upstream
# 4.4.1 and differ only in the Debian revision.
is( xCAT_plugin::dhcp::_isc_dhcp_version_cmp('4.4.1-2.1ubuntu5', '4.4.1-2.3ubuntu2'), -1,
'20.04 sorts below 22.04 despite sharing upstream 4.4.1' );
is( xCAT_plugin::dhcp::_isc_dhcp_version_cmp('4.4.3-P1-4ubuntu2', '4.4.1-2.3ubuntu2'), 1,
'24.04 sorts above 22.04' );
is( xCAT_plugin::dhcp::_isc_dhcp_version_cmp('4.3.5-3ubuntu7', '4.4.1-2.3'), -1,
'18.04 sorts below the split' );
is( xCAT_plugin::dhcp::_isc_dhcp_version_cmp('4.4.1-2.3ubuntu2', '4.4.1-2.3ubuntu2'), 0,
'a version equals itself' );
# Debian's own packages have no systemd unit; the sysvinit script reads
# INTERFACESv4 and falls back to INTERFACES only when v4 is empty.
foreach my $debian ('4.4.1-2.3+deb11u2', '4.4.3-P1-2', '4.4.3-P1-8') {
my $written = written_for($debian, ['eth1']);
is( launched_with($written, 'INTERFACESv4'), 'eth1',
"Debian $debian: dhcpd is launched restricted to the interface xCAT serves" );
}
# A node upgraded from an xCAT that wrote the wrong key has the damage on disk
# already: no INTERFACESv4 and two INTERFACES lines. Running makedhcp again has
# to repair that file, not add to it.
my $damaged = <<'EOF';
# Defaults for isc-dhcp-server (sourced by /etc/init.d/isc-dhcp-server)
INTERFACES="eth1"
INTERFACES="eth1"
EOF
my $repaired = written_for('4.4.3-P1-4ubuntu2', ['eth2'], $damaged);
is( launched_with($repaired, 'INTERFACESv4'), 'eth2',
'a file left behind by an older xCAT is repaired' );
foreach my $key (qw(INTERFACESv4 INTERFACESv6)) {
my $count = () = ($repaired =~ m/^\s*\Q$key\E\s*=/mg);
is( $count, 1, "$key is assigned exactly once" );
}
# The EL and SLES paths pass a single key and must keep working unchanged.
my $el = xCAT_plugin::dhcp::_sysconfig_interfaces_content(
qq{# Command line options here\nDHCPDARGS=\n}, 'DHCPDARGS', ['eth1', 'eth2']);
is( launched_with($el, 'DHCPDARGS'), 'eth1 eth2',
'the single-key sysconfig path is unchanged' );
done_testing();