2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-08-03 16:06:59 +00:00

Merge pull request #7695 from xcat2/backport-7605-to-2.18

[Backport 2.18] fix(dhcp): use Kea runtime paths
This commit is contained in:
xcat2-backport-automation[bot]
2026-07-26 11:53:29 +00:00
committed by GitHub
6 changed files with 741 additions and 137 deletions
+144 -32
View File
@@ -7,6 +7,7 @@ use JSON;
use File::Basename;
use File::Path qw/make_path/;
use Math::BigInt;
use Text::ParseWords qw/shellwords/;
use xCAT::DHCP::Range;
use xCAT::NetworkUtils;
@@ -16,6 +17,7 @@ my %KEA_SERVICE_CANDIDATES = (
'kea-dhcp-ddns' => [ 'kea-dhcp-ddns', 'kea-dhcp-ddns-server' ],
'kea-ctrl-agent' => [ 'kea-ctrl-agent' ],
);
my @KEA_ACCOUNT_CANDIDATES = ( 'kea', '_kea' );
sub new {
my ( $class, %args ) = @_;
@@ -50,6 +52,25 @@ sub ddns_config_file {
return $self->{ddns_config_file} || '/etc/kea/kea-dhcp-ddns.conf';
}
sub control_socket_path {
my ( $self, $socket_name ) = @_;
return $self->_kea_socket_dir() . "/$socket_name";
}
sub service_account {
foreach my $user (@KEA_ACCOUNT_CANDIDATES) {
my @entry = getpwnam($user);
return {
name => $entry[0],
uid => $entry[2],
gid => $entry[3],
} if @entry;
}
return;
}
sub render_dhcp4_config {
my ( $self, $intent ) = @_;
@@ -154,19 +175,19 @@ sub render_ctrl_agent_config {
my %sockets = (
dhcp4 => {
'socket-type' => 'unix',
'socket-name' => $intent->{'dhcp4-socket'} || $self->_kea_control_socket('kea4-ctrl-socket'),
'socket-name' => $intent->{'dhcp4-socket'} || $self->control_socket_path('kea4-ctrl-socket'),
},
);
if ( $intent->{dhcp6} || $intent->{'dhcp6-socket'} ) {
$sockets{dhcp6} = {
'socket-type' => 'unix',
'socket-name' => $intent->{'dhcp6-socket'} || $self->_kea_control_socket('kea6-ctrl-socket'),
'socket-name' => $intent->{'dhcp6-socket'} || $self->control_socket_path('kea6-ctrl-socket'),
};
}
if ( $intent->{ddns} || $intent->{'ddns-socket'} ) {
$sockets{d2} = {
'socket-type' => 'unix',
'socket-name' => $intent->{'ddns-socket'} || $self->_kea_control_socket('kea-ddns-ctrl-socket'),
'socket-name' => $intent->{'ddns-socket'} || $self->control_socket_path('kea-ddns-ctrl-socket'),
};
}
@@ -478,12 +499,12 @@ sub _validate_config_with {
my $prefix = '';
if ( $> == 0 ) {
my $kea_user = _kea_user();
my $service_account = $self->service_account();
my $runuser = _command_path('runuser');
# Validate as the daemon user when possible so root does not hide
# packaged Kea runtime-directory or config-readability failures.
$prefix = _shell_quote($runuser) . ' -u ' . _shell_quote($kea_user) . ' -- '
if $kea_user && $runuser;
$prefix = _shell_quote($runuser) . ' -u ' . _shell_quote( $service_account->{name} ) . ' -- '
if $service_account && $runuser;
}
my $cmd = $prefix . _shell_quote($kea) . " -t " . _shell_quote($path) . " 2>&1";
@@ -911,15 +932,7 @@ sub kea_version {
return $self->{kea_version} if defined $self->{kea_version};
return $self->{_detected_kea_version} if defined $self->{_detected_kea_version};
my $command = $self->{kea_dhcp4_command} || _command_path('kea-dhcp4');
return unless $command;
my $output = '';
if ( open( my $version_fh, '-|', $command, '-V' ) ) {
local $/;
$output = <$version_fh> || '';
close($version_fh);
}
my $output = $self->_kea_command_output('-V') || '';
if ( $output =~ /(\d+(?:\.\d+){1,2})/ ) {
$self->{_detected_kea_version} = $1;
@@ -928,6 +941,34 @@ sub kea_version {
return $self->{_detected_kea_version};
}
sub _kea_build_report {
my ($self) = @_;
return $self->{kea_build_report} if defined $self->{kea_build_report};
return $self->{_detected_kea_build_report} if defined $self->{_detected_kea_build_report};
my $output = $self->_kea_command_output('-W') || '';
$self->{_detected_kea_build_report} = $output;
return $output;
}
sub _kea_command_output {
my ( $self, @args ) = @_;
my $command = $self->{kea_dhcp4_command} || _command_path('kea-dhcp4');
return unless $command;
my $output = '';
if ( open( my $command_fh, '-|', $command, @args ) ) {
local $/;
$output = <$command_fh> || '';
close($command_fh);
}
return $output;
}
sub _first_defined {
my @values = @_;
foreach my $value (@values) {
@@ -971,7 +1012,7 @@ sub _set_config_permissions {
}
sub _kea_group {
foreach my $group ( 'kea', '_kea' ) {
foreach my $group (@KEA_ACCOUNT_CANDIDATES) {
my @entry = getgrnam($group);
return ( $entry[0], $entry[2] ) if @entry;
}
@@ -979,15 +1020,6 @@ sub _kea_group {
return;
}
sub _kea_user {
foreach my $user ( 'kea', '_kea' ) {
my @entry = getpwnam($user);
return $entry[0] if @entry;
}
return;
}
sub _kea_service {
my ( $self, $service ) = @_;
@@ -1028,21 +1060,101 @@ sub _kea_socket_dir {
return $self->{kea_socket_dir} if defined $self->{kea_socket_dir};
if ( defined $self->{kea_socket_dirs} ) {
foreach my $dir ( @{ $self->{kea_socket_dirs} || [] } ) {
return $dir if -d $dir;
}
return '/var/run/kea';
}
my $build_report = $self->_kea_build_report();
if ($build_report) {
my $prefix = _kea_build_option( $build_report, 'prefix' );
if ( !defined($prefix) && $build_report =~ /^\s*Prefix:\s*(\/\S+)/m ) {
$prefix = $1;
}
$prefix = _expand_kea_build_path( $prefix, {} );
# Kea's Meson build unconditionally overrides the installed runtime
# directory for the /usr/local prefix, even when state directories
# are supplied as build options.
if ( defined($prefix) && $prefix eq '/usr/local' && $build_report =~ /^\s*Meson Version:/m ) {
return '/usr/local/var/run/kea';
}
my $local_state = _kea_build_option( $build_report, 'localstatedir' );
$local_state = _expand_kea_build_path( $local_state, { prefix => $prefix }, $prefix );
if ( !defined($local_state) && defined($prefix) ) {
if ( $build_report =~ /^\s*Meson Version:/m ) {
$local_state = '/var' if $prefix eq '/usr';
}
# Kea reverts Meson's /usr/local => /var/local default.
$local_state = _expand_kea_build_path( 'var', {}, $prefix ) unless defined($local_state);
}
my $run_state = _kea_build_option( $build_report, 'runstatedir' );
$run_state = _expand_kea_build_path(
$run_state,
{
prefix => $prefix,
localstatedir => $local_state,
},
$prefix,
);
$run_state = "$local_state/run" if !defined($run_state) && defined($local_state);
return $run_state eq '/' ? '/kea' : "$run_state/kea" if defined($run_state);
}
# Kea validates Control Agent sockets against its packaged runtime
# directory, and newer packages reject /var/run/kea even when it resolves
# to /run/kea. Keep the legacy path as the unknown-state fallback for
# older Kea builds that validate before the runtime directory exists.
foreach my $dir ( @{ $self->{kea_socket_dirs} || [ '/run/kea', '/var/run/kea' ] } ) {
# directory, and newer packages reject a symlink-equivalent spelling.
# Filesystem probing is only a fallback for builds without a usable report.
foreach my $dir ( '/run/kea', '/var/run/kea' ) {
return $dir if -d $dir;
}
return '/var/run/kea';
}
sub _kea_control_socket {
my ( $self, $socket_name ) = @_;
sub _kea_build_option {
my ( $build_report, $option ) = @_;
return $self->_kea_socket_dir() . "/$socket_name";
return unless defined($build_report);
my $options = $build_report;
if ( $build_report =~ /^\s*(?:Configure arguments|Build Options):[ \t]*(.*?)(?=\n[ \t]*\n|^\s*C\+\+ Compiler:|\z)/ms ) {
$options = $1;
}
my @tokens = eval { shellwords($options) };
return if $@;
my $value;
for ( my $index = 0; $index < @tokens; $index++ ) {
my $token = $tokens[$index];
if ( $token =~ /^(?:--|-D)\Q$option\E=(.+)$/ ) {
$value = $1;
} elsif ( $token eq "--$option" && $index + 1 < @tokens ) {
$value = $tokens[ ++$index ];
}
}
return $value;
}
sub _expand_kea_build_path {
my ( $path, $variables, $relative_base ) = @_;
return unless defined($path);
foreach my $variable (qw/prefix localstatedir/) {
next unless defined( $variables->{$variable} );
$path =~ s/\$\{$variable\}/$variables->{$variable}/g;
}
if ( $path !~ m{^/} && defined($relative_base) ) {
$path = $relative_base eq '/' ? "/$path" : "$relative_base/$path";
}
$path =~ s{/$}{} if length($path) > 1;
return $path if $path =~ m{^/};
return;
}
sub _command_path {
+2 -2
View File
@@ -2615,7 +2615,7 @@ sub kea_build_dhcp4_intent
if (kea_control_agent_enabled()) {
$intent->{'control-socket'} = {
'socket-type' => 'unix',
'socket-name' => '/var/run/kea/kea4-ctrl-socket',
'socket-name' => $backend->control_socket_path('kea4-ctrl-socket'),
};
my $hook = $backend->host_cmds_hook_path();
if ($hook) {
@@ -2670,7 +2670,7 @@ sub kea_build_dhcp6_intent
if (kea_control_agent_enabled()) {
$intent->{'control-socket'} = {
'socket-type' => 'unix',
'socket-name' => '/var/run/kea/kea6-ctrl-socket',
'socket-name' => $backend->control_socket_path('kea6-ctrl-socket'),
};
my $hook = $backend->host_cmds_hook_path();
if ($hook) {
@@ -6,7 +6,10 @@ use lib "$FindBin::Bin/../../perl-xCAT";
use File::Path qw/make_path/;
use File::Temp qw/tempdir/;
use IO::Socket::INET;
use POSIX qw/WNOHANG _exit setgid setuid/;
use Test::More;
use Time::HiRes qw/sleep time/;
use xCAT::DHCP::Backend::Kea;
@@ -21,99 +24,172 @@ plan skip_all => 'kea-dhcp4 and kea-ctrl-agent are required'
my $backend = xCAT::DHCP::Backend::Kea->new();
my $hook = $backend->host_cmds_hook_path();
plan skip_all => 'Kea host-commands hook is required' unless $hook;
my $service_account = $backend->service_account();
plan skip_all => 'Kea service account is required'
unless ref($service_account) eq 'HASH'
&& defined( $service_account->{name} )
&& defined( $service_account->{uid} )
&& defined( $service_account->{gid} );
my $tmp = tempdir(CLEANUP => 1);
make_path('/var/run/kea');
make_path('/var/lib/kea');
chmod 0755, $tmp or die "Unable to make $tmp daemon-traversable: $!";
my $socket = "/var/run/kea/kea4-xcat-smoke-$$.sock";
my $lease_file = "/var/lib/kea/kea-leases4-xcat-smoke-$$.csv";
unlink $socket, $lease_file;
my $runtime_dir = "$tmp/run";
my $data_dir = "$tmp/data";
make_path( $runtime_dir, $data_dir );
chown( $service_account->{uid}, $service_account->{gid}, $runtime_dir, $data_dir ) == 2
or die "Unable to set Kea fixture directory ownership: $!";
chmod( 0750, $runtime_dir, $data_dir ) == 2
or die "Unable to set Kea fixture directory permissions: $!";
local $ENV{KEA_CONTROL_SOCKET_DIR} = $runtime_dir;
local $ENV{KEA_DHCP_DATA_DIR} = $data_dir;
local $ENV{KEA_PIDFILE_DIR} = $runtime_dir;
local $ENV{KEA_LOCKFILE_DIR} = $runtime_dir;
$backend = xCAT::DHCP::Backend::Kea->new(kea_socket_dir => $runtime_dir);
my $port_guard = IO::Socket::INET->new(
LocalAddr => '127.0.0.1',
LocalPort => 0,
Listen => 1,
Proto => 'tcp',
) or die "Unable to reserve a Control Agent port: $!";
my $control_agent_port = $port_guard->sockport();
my $socket = $backend->control_socket_path('kea4-ctrl-socket');
my $lease_file = "$data_dir/kea-leases4.csv";
my $dhcp_config = "$tmp/kea-dhcp4.conf";
my $ctrl_config = "$tmp/kea-ctrl-agent.conf";
write_file(
$dhcp_config,
$backend->render_dhcp4_config(
my $dhcp_settings = {
interfaces => [],
'lease-database' => {
type => 'memfile',
name => $lease_file,
},
'control-socket' => {
'socket-type' => 'unix',
'socket-name' => $socket,
},
subnets => [
{
interfaces => ['lo'],
'lease-database' => {
type => 'memfile',
name => $lease_file,
},
'control-socket' => {
'socket-type' => 'unix',
'socket-name' => $socket,
},
'hooks-libraries' => [ { library => $hook } ],
subnets => [
{
id => 1,
subnet => '127.0.0.0/8',
pools => [],
},
],
}
)
);
write_file(
$ctrl_config,
$backend->render_ctrl_agent_config(
{
'http-port' => 18000,
'dhcp4-socket' => $socket,
}
)
);
id => 1,
subnet => '127.0.0.0/8',
pools => [],
},
],
};
$dhcp_settings->{'hooks-libraries'} = [ { library => $hook } ] if $hook;
my $dhcp_validation = $backend->validate_dhcp4_config($dhcp_config);
ok( !$dhcp_validation->{error}, 'live smoke DHCPv4 config validates' )
or diag $dhcp_validation->{error};
my $ctrl_validation = $backend->validate_ctrl_agent_config($ctrl_config);
ok( !$ctrl_validation->{error}, 'live smoke Control Agent config validates' )
or diag $ctrl_validation->{error};
my $dhcp_write = $backend->write_dhcp4_config(
$dhcp_settings,
path => $dhcp_config,
);
ok( !$dhcp_write->{error}, 'live smoke DHCPv4 config validates and writes' )
or diag $dhcp_write->{error};
my @pids;
END {
kill 'TERM', @pids if @pids;
unlink grep { defined($_) && $_ ne '' } ( $socket, $lease_file );
my $ctrl_write = $backend->write_ctrl_agent_config(
{
'http-port' => $control_agent_port,
},
path => $ctrl_config,
);
ok( !$ctrl_write->{error}, 'live smoke Control Agent config validates and writes' )
or diag $ctrl_write->{error};
unless ( !$dhcp_write->{error} && !$ctrl_write->{error} ) {
done_testing();
exit 1;
}
push @pids, start_daemon($kea_dhcp4, '-c', $dhcp_config, '-d', "$tmp/kea-dhcp4.log");
ok( wait_for_process($pids[-1]), 'kea-dhcp4 stays running for smoke test' );
push @pids, start_daemon($kea_ctrl, '-c', $ctrl_config, '-d', "$tmp/kea-ctrl-agent.log");
ok( wait_for_process($pids[-1]), 'kea-ctrl-agent stays running for smoke test' );
my %children;
END { stop_daemons(\%children); }
my $live_backend = xCAT::DHCP::Backend::Kea->new(control_agent_port => 18000);
my $add = $live_backend->live_upsert_reservations(
[
{
'subnet-id' => 1,
'hw-address' => '52:54:00:12:34:56',
'ip-address' => '127.0.0.50',
hostname => 'node-smoke',
},
],
service => ['dhcp4']
);
ok( !$add->{error}, 'reservation-add succeeds through Kea Control Agent' )
or diag $add->{error};
my $dhcp_log = "$tmp/kea-dhcp4.log";
my $dhcp_pid = start_daemon( $service_account, $kea_dhcp4, $dhcp_log, '-c', $dhcp_config, '-d' );
$children{$dhcp_pid} = 1;
my $dhcp_ready = wait_for_socket( $dhcp_pid, $socket, \%children );
ok( $dhcp_ready, 'kea-dhcp4 creates its Control Agent socket' );
diag_file($dhcp_log) unless $dhcp_ready;
unless ($dhcp_ready) {
done_testing();
exit 1;
}
is( ( stat $socket )[4], $service_account->{uid}, 'kea-dhcp4 socket belongs to the service account' );
my $delete = $live_backend->live_delete_reservations(
[
{
'subnet-id' => 1,
'hw-address' => '52:54:00:12:34:56',
'ip-address' => '127.0.0.50',
hostname => 'node-smoke',
},
],
service => ['dhcp4']
);
ok( !$delete->{error}, 'reservation-del succeeds through Kea Control Agent' )
or diag $delete->{error};
my $lease_ready = wait_for_file( $dhcp_pid, $lease_file, \%children );
ok( $lease_ready, 'kea-dhcp4 creates its lease file' );
is( ( stat $lease_file )[4], $service_account->{uid}, 'kea-dhcp4 lease file belongs to the service account' )
if $lease_ready;
close($port_guard) or die "Unable to release the reserved Control Agent port: $!";
my $ctrl_log = "$tmp/kea-ctrl-agent.log";
my $ctrl_pid = start_daemon( $service_account, $kea_ctrl, $ctrl_log, '-c', $ctrl_config, '-d' );
$children{$ctrl_pid} = 1;
my $live_backend = xCAT::DHCP::Backend::Kea->new(control_agent_port => $control_agent_port);
my ( $ctrl_ready, $readiness ) = wait_for_control_agent( $live_backend, $ctrl_pid, \%children );
ok( $ctrl_ready, 'Kea Control Agent forwards commands to kea-dhcp4' );
diag( $readiness->{error} || $readiness->{text} || 'Control Agent returned no response' ) unless $ctrl_ready;
diag_file($ctrl_log) unless $ctrl_ready;
unless ($ctrl_ready) {
done_testing();
exit 1;
}
SKIP: {
skip 'Kea host-commands hook is unavailable', 4 unless $hook;
my $reservation = {
'subnet-id' => 1,
'hw-address' => '52:54:00:12:34:56',
'ip-address' => '127.0.0.50',
hostname => 'node-smoke',
};
my $add = $live_backend->live_upsert_reservations(
[$reservation],
service => ['dhcp4']
);
ok( !$add->{error}, 'reservation-add succeeds through Kea Control Agent' )
or diag $add->{error};
my $lookup = {
'subnet-id' => $reservation->{'subnet-id'},
'identifier-type' => 'hw-address',
identifier => $reservation->{'hw-address'},
};
my $found = $live_backend->control_agent_command( 'reservation-get', $lookup, service => ['dhcp4'] );
my $stored = response_arguments($found);
ok(
ref($stored) eq 'HASH'
&& defined( $stored->{'ip-address'} )
&& $stored->{'ip-address'} eq $reservation->{'ip-address'},
'reservation-get confirms the added reservation is stored'
) or diag( $found->{error} || $found->{text} || 'reservation-get returned no reservation' );
my $delete = $live_backend->live_delete_reservations(
[$reservation],
service => ['dhcp4']
);
ok( !$delete->{error}, 'reservation-del succeeds through Kea Control Agent' )
or diag $delete->{error};
my $missing = $live_backend->control_agent_command( 'reservation-get', $lookup, service => ['dhcp4'] );
my $missing_arguments = response_arguments($missing);
my $expected_not_found = !$missing->{error}
|| ( defined( $missing->{result} ) && $missing->{result} == 3 )
|| ( defined( $missing->{text} ) && $missing->{text} =~ /not\s+found/i );
ok(
defined( $missing->{response} )
&& $expected_not_found
&& ( ref($missing_arguments) ne 'HASH' || !keys %$missing_arguments ),
'reservation-get confirms the deleted reservation is absent'
) or diag( $missing->{error} || $missing->{text} || 'reservation-get returned an unexpected response' );
}
stop_daemons(\%children);
done_testing();
sub command_path {
@@ -131,38 +207,135 @@ sub command_path {
return;
}
sub write_file {
my ( $path, $content ) = @_;
open(my $fh, '>', $path) or die "Unable to write $path: $!";
print $fh $content;
close($fh) or die "Unable to close $path: $!";
return 1;
}
sub start_daemon {
my ( $command, @args ) = @_;
my $log = pop @args;
my ( $account, $command, $log, @args ) = @_;
my $pid = fork();
die "Unable to fork $command: $!" unless defined $pid;
if ($pid == 0) {
open(STDOUT, '>', $log) or die "Unable to write $log: $!";
open(STDERR, '>&', \*STDOUT) or die "Unable to redirect stderr: $!";
exec $command, @args;
die "Unable to exec $command: $!";
open(STDOUT, '>', $log) or child_exit("Unable to write $log: $!");
open(STDERR, '>&', \*STDOUT) or child_exit("Unable to redirect stderr: $!");
$) = "$account->{gid} $account->{gid}";
defined( setgid( $account->{gid} ) )
or child_exit("Unable to set group identity to $account->{gid}: $!");
my @group_ids = split /\s+/, $);
$( == $account->{gid} && @group_ids && !grep { $_ != $account->{gid} } @group_ids
or child_exit("Kea child did not assume group identity $account->{gid}");
defined( setuid( $account->{uid} ) )
or child_exit("Unable to set user identity to $account->{uid}: $!");
$> == $account->{uid} && $< == $account->{uid}
or child_exit("Kea child did not assume user identity $account->{uid}");
{
no warnings 'exec';
exec { $command } $command, @args;
child_exit("Unable to exec $command: $!");
}
}
return $pid;
}
sub wait_for_process {
my ($pid) = @_;
sub child_exit {
my ($message) = @_;
for (1 .. 10) {
sleep 1;
return 0 unless kill 0, $pid;
return 1 if $_ >= 2;
warn "$message\n";
_exit(127);
}
sub wait_for_socket {
my ( $pid, $socket_path, $children ) = @_;
for (1 .. 100) {
return 0 unless process_running( $pid, $children );
return 1 if -S $socket_path;
sleep 0.1;
}
return kill 0, $pid;
return 0;
}
sub wait_for_file {
my ( $pid, $path, $children ) = @_;
for (1 .. 100) {
return 0 unless process_running( $pid, $children );
return 1 if -f $path;
sleep 0.1;
}
return 0;
}
sub wait_for_control_agent {
my ( $backend, $pid, $children ) = @_;
my $last_result = {};
my $deadline = time + 10;
while ( time < $deadline ) {
return ( 0, $last_result ) unless process_running( $pid, $children );
$last_result = $backend->control_agent_command(
'list-commands',
{},
service => ['dhcp4'],
timeout => 1,
);
return ( 1, $last_result ) unless $last_result->{error};
my $remaining = $deadline - time;
sleep( $remaining < 0.1 ? $remaining : 0.1 ) if $remaining > 0;
}
return ( 0, $last_result );
}
sub process_running {
my ( $pid, $children ) = @_;
my $waited = waitpid( $pid, WNOHANG );
return 1 if $waited == 0;
delete $children->{$pid};
return 0;
}
sub stop_daemons {
my ($children) = @_;
my @pids = keys %$children;
kill 'TERM', @pids if @pids;
foreach my $pid (@pids) {
for (1 .. 50) {
last unless process_running( $pid, $children );
sleep 0.1;
}
next unless exists $children->{$pid};
kill 'KILL', $pid;
waitpid( $pid, 0 );
delete $children->{$pid};
}
return;
}
sub response_arguments {
my ($result) = @_;
return unless ref($result) eq 'HASH';
my $response = $result->{response};
my $item = ref($response) eq 'ARRAY' ? $response->[0] : $response;
return unless ref($item) eq 'HASH';
return $item->{arguments} if ref( $item->{arguments} ) eq 'HASH';
return;
}
sub diag_file {
my ($path) = @_;
return unless -e $path;
open( my $fh, '<', $path ) or return;
local $/;
my $content = <$fh>;
close($fh) or diag("Unable to close $path: $!");
diag($content) if defined($content) && $content ne '';
return;
}
+193
View File
@@ -0,0 +1,193 @@
#!/usr/bin/env perl
use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/../../perl-xCAT";
use File::Temp qw/tempfile/;
use Test::More;
our ( %TEST_USERS, %TEST_GROUPS, %TEST_GROUP_NAMES );
our ( @USER_LOOKUPS, @GROUP_LOOKUPS, @GROUP_ID_LOOKUPS );
BEGIN {
no warnings 'redefine';
*CORE::GLOBAL::getpwnam = sub {
my ($name) = @_;
push @USER_LOOKUPS, $name;
my $entry = $TEST_USERS{$name};
return unless $entry;
return @$entry if wantarray;
return $entry->[0];
};
*CORE::GLOBAL::getgrnam = sub {
my ($name) = @_;
push @GROUP_LOOKUPS, $name;
my $entry = $TEST_GROUPS{$name};
return unless $entry;
return @$entry if wantarray;
return $entry->[0];
};
*CORE::GLOBAL::getgrgid = sub {
my ($gid) = @_;
push @GROUP_ID_LOOKUPS, $gid;
my $name = $TEST_GROUP_NAMES{$gid};
return unless defined $name;
return ( $name, 'x', $gid, '' ) if wantarray;
return $name;
};
}
use xCAT::DHCP::Backend::Kea;
my $backend = xCAT::DHCP::Backend::Kea->new();
set_nss(
users => {
_kea => [ '_kea', 'x', 100, 300, '', '', '', '/var/empty', '/sbin/nologin' ],
},
groups => {
kea => [ 'kea', 'x', 200, '' ],
_kea => [ '_kea', 'x', 400, '' ],
},
group_names => { 300 => 'daemon-primary' },
);
my $service_account = selected_service_account($backend);
is_deeply(
$service_account,
{ name => '_kea', uid => 100, gid => 300 },
'service account selection preserves the fallback daemon identity'
);
reset_lookups();
my ( $group, $gid ) = selected_config_group($backend);
is( $group, 'kea', 'the preferred named Kea group owns configuration files' );
is( $gid, 200, 'configuration ownership is independent of the daemon primary GID' );
isnt( $gid, $service_account->{gid}, 'configuration ownership does not inherit the daemon primary GID' );
is_deeply( \@GROUP_LOOKUPS, ['kea'], 'named configuration groups are checked in preference order' );
is_deeply( \@USER_LOOKUPS, [], 'configuration-group selection does not inspect service users' );
is_deeply( \@GROUP_ID_LOOKUPS, [], 'configuration-group selection does not resolve a primary GID' );
set_nss(
groups => {
_kea => [ '_kea', 'x', 400, '' ],
},
);
( $group, $gid ) = selected_config_group($backend);
is( $group, '_kea', 'the fallback named Kea group is selected when needed' );
is( $gid, 400, 'the fallback named Kea group supplies configuration ownership' );
is_deeply( \@GROUP_LOOKUPS, [ 'kea', '_kea' ], 'both named groups are checked before falling back' );
is_deeply( \@USER_LOOKUPS, [], 'named-group fallback remains independent of service users' );
is_deeply( \@GROUP_ID_LOOKUPS, [], 'named-group fallback remains independent of primary GIDs' );
set_nss(
users => {
kea => [ 'kea', 'x', 101, 300, '', '', '', '/var/empty', '/sbin/nologin' ],
},
group_names => { 300 => 'daemon-primary' },
);
( $group, $gid ) = selected_config_group($backend);
ok( !defined($group), 'no configuration group is selected when named Kea groups are absent' );
ok( !defined($gid), 'no configuration GID is inherited from the service account' );
is_deeply( \@GROUP_LOOKUPS, [ 'kea', '_kea' ], 'all named groups are checked before the public-mode fallback' );
is_deeply( \@USER_LOOKUPS, [], 'the public-mode fallback does not inspect service users' );
is_deeply( \@GROUP_ID_LOOKUPS, [], 'the public-mode fallback does not resolve a primary GID' );
SKIP: {
skip 'root privileges are required to verify file ownership and modes', 5 if $> != 0;
set_nss(
users => {
_kea => [ '_kea', 'x', 100, 300, '', '', '', '/var/empty', '/sbin/nologin' ],
},
groups => {
kea => [ 'kea', 'x', 200, '' ],
},
group_names => { 300 => 'daemon-primary' },
);
my ( $named_fh, $named_path ) = tempfile(UNLINK => 1);
close($named_fh) or die "Unable to close $named_path: $!";
my $named_result = apply_config_permissions( $backend, $named_path );
ok( !$named_result->{error}, 'configuration permissions are applied with a named Kea group' )
or diag $named_result->{error};
is( ( stat $named_path )[5], 200, 'configuration file uses the named Kea group GID' );
is( ( stat $named_path )[2] & 07777, 0640, 'configuration file is group-readable with a named Kea group' );
set_nss(
users => {
kea => [ 'kea', 'x', 101, 300, '', '', '', '/var/empty', '/sbin/nologin' ],
},
group_names => { 300 => 'daemon-primary' },
);
my ( $public_fh, $public_path ) = tempfile(UNLINK => 1);
close($public_fh) or die "Unable to close $public_path: $!";
my $public_result = apply_config_permissions( $backend, $public_path );
ok( !$public_result->{error}, 'configuration permissions fall back without a named Kea group' )
or diag $public_result->{error};
is( ( stat $public_path )[2] & 07777, 0644, 'missing named Kea groups preserve the public-read fallback' );
}
done_testing();
sub set_nss {
my (%args) = @_;
%TEST_USERS = %{ $args{users} || {} };
%TEST_GROUPS = %{ $args{groups} || {} };
%TEST_GROUP_NAMES = %{ $args{group_names} || {} };
reset_lookups();
return;
}
sub reset_lookups {
@USER_LOOKUPS = ();
@GROUP_LOOKUPS = ();
@GROUP_ID_LOOKUPS = ();
return;
}
sub selected_service_account {
my ($kea_backend) = @_;
if ( my $resolver = $kea_backend->can('service_account') ) {
return $resolver->($kea_backend);
}
my $legacy_resolver = xCAT::DHCP::Backend::Kea->can('_kea_user');
my $name = $legacy_resolver ? $legacy_resolver->() : undef;
return unless defined $name;
my @entry = getpwnam($name);
return {
name => $entry[0],
uid => $entry[2],
gid => $entry[3],
};
}
sub selected_config_group {
my ($kea_backend) = @_;
if ( my $resolver = xCAT::DHCP::Backend::Kea->can('_kea_group') ) {
return $resolver->();
}
my $resolver = $kea_backend->can('_service_group');
return $resolver ? $resolver->($kea_backend) : undef;
}
sub apply_config_permissions {
my ( $kea_backend, $path ) = @_;
my $permissions = xCAT::DHCP::Backend::Kea->can('_set_config_permissions');
return { error => 'Kea configuration-permission helper is unavailable' } unless $permissions;
return $permissions->($path) if xCAT::DHCP::Backend::Kea->can('_kea_group');
return $permissions->( $kea_backend, $path );
}
+43
View File
@@ -55,6 +55,13 @@ if ( -f $source_dhcp_plugin ) {
} else {
require xCAT_plugin::dhcp;
}
require xCAT::DHCP::Backend::Kea;
{
package DHCPKeaIntentBackend;
our @ISA = ('xCAT::DHCP::Backend::Kea');
sub host_cmds_hook_path { return '/test/libdhcp_host_cmds.so'; }
}
{
package DHCPKeaIntentNetTable;
@@ -172,6 +179,42 @@ ok(!xCAT_plugin::dhcp::dhcpd_sysconfig_uses_interface_key('opensuse-tumbleweed')
is( $intent->{subnets}[0]{subnet}, '10.0.0.0/24', 'rendered subnet comes from local route' );
}
{
no warnings 'redefine';
local *xCAT_plugin::dhcp::kea_ipv4_routes = sub {
return ([ '10.0.0.0', 'eth0', '255.255.255.0', '' ]);
};
local *xCAT_plugin::dhcp::kea_boot_client_classes = sub { return []; };
local *xCAT_plugin::dhcp::kea_option_defs = sub { return []; };
local *xCAT_plugin::dhcp::kea_global_option_data = sub { return []; };
local *xCAT_plugin::dhcp::kea_dhcp_lease_time = sub { return 43200; };
local *xCAT_plugin::dhcp::kea_control_agent_enabled = sub { return 1; };
my $backend = DHCPKeaIntentBackend->new(kea_socket_dir => '/run/kea-xcat-test');
local $xCAT::Table::networks = DHCPKeaIntentNetTable->new( \%network_entry );
my $dhcp4_intent = xCAT_plugin::dhcp::kea_build_dhcp4_intent( $backend, { eth0 => 1 } );
is(
$dhcp4_intent->{'control-socket'}{'socket-name'},
'/run/kea-xcat-test/kea4-ctrl-socket',
'DHCPv4 intent uses the backend-selected Control Agent socket path'
);
local $xCAT::Table::networks = DHCPKeaIntentNetTable->new(
{
%network_entry,
net => 'fd00::/64',
dynamicrange => undef,
}
);
my $dhcp6_intent = xCAT_plugin::dhcp::kea_build_dhcp6_intent( $backend, { eth0 => 1 } );
is(
$dhcp6_intent->{'control-socket'}{'socket-name'},
'/run/kea-xcat-test/kea6-ctrl-socket',
'DHCPv6 intent uses the backend-selected Control Agent socket path'
);
}
{
no warnings 'redefine';
local *xCAT::NetworkUtils::thishostisnot = sub { return 1; };
+84 -1
View File
@@ -414,7 +414,8 @@ is( $ddns_config->{DhcpDdns}{port}, 53001, 'DDNS port is numeric' );
is( $ddns_config->{DhcpDdns}{'dns-server-timeout'}, 500, 'DDNS timeout is numeric' );
is( $ddns_config->{DhcpDdns}{'forward-ddns'}{'ddns-domains'}[0]{name}, 'cluster.example.com.', 'DDNS forward domain is rendered' );
my $ctrl_agent_config = decode_json($backend->render_ctrl_agent_config({ 'http-port' => '8000' }));
my $ctrl_agent_backend = xCAT::DHCP::Backend::Kea->new( kea_socket_dirs => [] );
my $ctrl_agent_config = decode_json($ctrl_agent_backend->render_ctrl_agent_config({ 'http-port' => '8000' }));
is( $ctrl_agent_config->{'Control-agent'}{'http-port'}, 8000, 'Control Agent HTTP port is numeric' );
my $runtime_socket_dir = "$unit_dir/run/kea";
@@ -429,6 +430,88 @@ my $legacy_socket_backend = xCAT::DHCP::Backend::Kea->new( kea_socket_dirs => []
my $legacy_socket_config = decode_json($legacy_socket_backend->render_ctrl_agent_config({}));
is( $legacy_socket_config->{'Control-agent'}{'control-sockets'}{dhcp4}{'socket-name'}, '/var/run/kea/kea4-ctrl-socket', 'Control Agent socket falls back to the legacy runtime path when no runtime directory exists' );
my $meson_socket_backend = xCAT::DHCP::Backend::Kea->new(
kea_build_report => 'Build Options: -Dprefix=/usr -Dlocalstatedir=/var -Drunstatedir=/xcat-test-meson-run',
);
is( $meson_socket_backend->control_socket_path('kea4-ctrl-socket'), '/xcat-test-meson-run/kea/kea4-ctrl-socket', 'Meson runstatedir selects the socket path before the runtime directory exists' );
my $autoconf_socket_backend = xCAT::DHCP::Backend::Kea->new(
kea_build_report => q{Configure arguments: '--prefix=/usr' '--localstatedir=/var' '--runstatedir=/xcat-test-autoconf-run'},
);
is( $autoconf_socket_backend->control_socket_path('kea4-ctrl-socket'), '/xcat-test-autoconf-run/kea/kea4-ctrl-socket', 'Autoconf runstatedir selects the socket path before the runtime directory exists' );
my $local_state_socket_backend = xCAT::DHCP::Backend::Kea->new(
kea_build_report => q{Configure arguments: '--prefix=/usr' '--localstatedir=/xcat-test-local-state'},
);
is( $local_state_socket_backend->control_socket_path('kea4-ctrl-socket'), '/xcat-test-local-state/run/kea/kea4-ctrl-socket', 'localstatedir supplies the runtime path when runstatedir is not configured' );
my $variable_socket_backend = xCAT::DHCP::Backend::Kea->new(
kea_build_report => q{Configure arguments:
'--prefix=/usr/local' '--localstatedir=${prefix}/var' '--runstatedir=${localstatedir}/run'},
);
is( $variable_socket_backend->control_socket_path('kea4-ctrl-socket'), '/usr/local/var/run/kea/kea4-ctrl-socket', 'Autoconf build variables resolve through the configured prefix' );
my $undefined_candidates_socket_backend = xCAT::DHCP::Backend::Kea->new(
kea_socket_dirs => undef,
kea_build_report => 'Build Options: -Dprefix=/usr -Drunstatedir=/xcat-test-undefined-candidates',
);
is( $undefined_candidates_socket_backend->control_socket_path('kea4-ctrl-socket'), '/xcat-test-undefined-candidates/kea/kea4-ctrl-socket', 'undefined socket candidates preserve build-report detection' );
my $source_default_socket_backend = xCAT::DHCP::Backend::Kea->new(
kea_build_report => 'Prefix: /usr/local',
);
is( $source_default_socket_backend->control_socket_path('kea4-ctrl-socket'), '/usr/local/var/run/kea/kea4-ctrl-socket', 'source build defaults resolve through the configured prefix' );
my $meson_default_socket_backend = xCAT::DHCP::Backend::Kea->new(
kea_build_report => "Meson Version: 1.10.1\nBuild Options: -Dprefix=/usr",
);
is( $meson_default_socket_backend->control_socket_path('kea4-ctrl-socket'), '/var/run/kea/kea4-ctrl-socket', 'Meson system prefix defaults local state to /var' );
my $meson_local_default_socket_backend = xCAT::DHCP::Backend::Kea->new(
kea_build_report => "Prefix: /usr/local\nMeson Version: 1.10.1\nBuild Options:\n\nC++ Compiler:\n",
);
is( $meson_local_default_socket_backend->control_socket_path('kea4-ctrl-socket'), '/usr/local/var/run/kea/kea4-ctrl-socket', 'Kea keeps local state prefix-relative for a Meson local prefix' );
my $meson_local_override_socket_backend = xCAT::DHCP::Backend::Kea->new(
kea_build_report => "Prefix: /usr/local\nMeson Version: 1.10.1\nBuild Options: -Dprefix=/usr/local -Dlocalstatedir=/srv/kea -Drunstatedir=/run/kea-custom\n",
);
is( $meson_local_override_socket_backend->control_socket_path('kea4-ctrl-socket'), '/usr/local/var/run/kea/kea4-ctrl-socket', 'Kea overrides Meson state-directory options for a /usr/local prefix' );
my $split_option_socket_backend = xCAT::DHCP::Backend::Kea->new(
kea_build_report => q{Configure arguments: '--prefix=/usr' '--localstatedir=/xcat-test-first' '--localstatedir' '/xcat-test-split-last'},
);
is( $split_option_socket_backend->control_socket_path('kea4-ctrl-socket'), '/xcat-test-split-last/run/kea/kea4-ctrl-socket', 'a space-separated Autoconf option can override an earlier value' );
my $last_option_socket_backend = xCAT::DHCP::Backend::Kea->new(
kea_build_report => <<'BUILD_REPORT',
Configure arguments: '--prefix=/usr' '--runstatedir=/xcat-test-first' '--runstatedir=/xcat-test-last' 'CXXFLAGS=-O2 -Drunstatedir=/xcat-test-embedded-compiler'
C++ Compiler:
CXX_ARGS: -Drunstatedir=/xcat-test-compiler-only
BUILD_REPORT
);
is( $last_option_socket_backend->control_socket_path('kea4-ctrl-socket'), '/xcat-test-last/kea/kea4-ctrl-socket', 'last build option wins without reading embedded or compiler-only flags' );
my $root_socket_backend = xCAT::DHCP::Backend::Kea->new(
kea_build_report => 'Build Options: -Dprefix=/ -Dlocalstatedir=/ -Drunstatedir=/',
);
is( $root_socket_backend->control_socket_path('kea4-ctrl-socket'), '/kea/kea4-ctrl-socket', 'root runstatedir joins the socket path without a duplicate separator' );
my $fake_kea_dhcp4 = "$unit_dir/kea-dhcp4-build-report";
open( my $fake_kea_fh, '>', $fake_kea_dhcp4 ) or die "Unable to write fake Kea command: $!";
print {$fake_kea_fh} "#!$^X\n";
print {$fake_kea_fh} <<'FAKE_KEA';
use strict;
use warnings;
exit 2 unless @ARGV == 1 && $ARGV[0] eq '-W';
print "Build Options: -Dprefix=/usr -Drunstatedir=/xcat-test-command-run\n";
FAKE_KEA
close($fake_kea_fh) or die "Unable to close fake Kea command: $!";
chmod 0755, $fake_kea_dhcp4 or die "Unable to make fake Kea command executable: $!";
my $command_socket_backend = xCAT::DHCP::Backend::Kea->new(kea_dhcp4_command => $fake_kea_dhcp4);
is( $command_socket_backend->control_socket_path('kea4-ctrl-socket'), '/xcat-test-command-run/kea/kea4-ctrl-socket', 'socket path comes from the Kea build-report command' );
my $socket_backend = xCAT::DHCP::Backend::Kea->new( kea_socket_dir => '/run/kea' );
my $ctrl_agent_socket_config = decode_json($socket_backend->render_ctrl_agent_config({ dhcp6 => 1, ddns => 1 }));
is( $ctrl_agent_socket_config->{'Control-agent'}{'control-sockets'}{dhcp4}{'socket-name'}, '/run/kea/kea4-ctrl-socket', 'Control Agent DHCPv4 socket uses the detected Kea socket directory' );