diff --git a/xCAT-test/unit/dhcp_omapi_capability.t b/xCAT-test/unit/dhcp_omapi_capability.t new file mode 100644 index 000000000..c40eb250d --- /dev/null +++ b/xCAT-test/unit/dhcp_omapi_capability.t @@ -0,0 +1,199 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use FindBin; +use lib "$FindBin::Bin/../lib"; +use lib "$FindBin::Bin/../../perl-xCAT"; +use lib "$FindBin::Bin/../../xCAT-server/lib/perl"; +use Config; +use File::Path qw(make_path); +use File::Slurper qw(read_text write_text); +use File::Temp qw(tempdir); +use Test::More; +use XCAT::Test::File qw(repo_path); + +BEGIN { + $ENV{XCATROOT} = repo_path('xCAT-server'); + $ENV{XCATCFG} = 'SQLite:' . tempdir(CLEANUP => 1); +} + +use xCAT::DHCP::OmapiPolicy; +use xCAT::DHCP::OmapiRunner; + +my $directory = tempdir(CLEANUP => 1); +my $client = "$directory/custom-omshell"; +write_text($client, "#!$Config{perlpath}\n" . <<'PERL'); +use strict; +use warnings; +my $input = do { local $/; }; +open(my $trace, '>>', $ENV{OMAPI_TRACE}) or die $!; +print {$trace} $input or die $!; +close($trace) or die $!; +if ($input eq "key-algorithm\n") { + if ($ENV{OMAPI_CLIENT} eq 'legacy') { + warn "unknown token: key-algorithm\n"; + print '> '; + } elsif ($ENV{OMAPI_CLIENT} eq 'silent') { + exit 0; + } else { + $| = 1; + my $command = $ENV{OMAPI_CLIENT} eq 'corrected' ? 'key-algorithm' : 'key-algoritm'; + print "> missing or invalid algorithm name\nusage: $command \n> "; + exit 1 if $ENV{OMAPI_CLIENT} eq 'failed'; + if ($ENV{OMAPI_CLIENT} eq 'stalled') { + $SIG{TERM} = 'IGNORE'; + open(my $pid, '>', $ENV{OMAPI_PID}) or die $!; + print {$pid} $$; + close($pid); + sleep 60; + } + } +} +PERL +chmod 0755, $client or die $!; +local $ENV{OMAPI_TRACE} = "$directory/trace"; +local $ENV{OMAPI_PID} = "$directory/pid"; + +{ + package FastCapabilityRunner; + use parent 'xCAT::DHCP::OmapiRunner'; + sub _completion_delay { die 'a probe has no server state to settle'; } + sub _poll_interval { return 0.01; } + sub _completion_attempts { return 500; } + sub _termination_attempts { return 10; } +} + +sub settings { + my ($algorithm, $fips) = @_; + return xCAT::DHCP::OmapiPolicy->settings( + fips_mode => $fips || 0, + site_values => { dhcpomapialgorithm => $algorithm, dhcpomshellpath => $client } + ); +} + +for my $algorithm (undef, 'hmac-md5') { + local $ENV{OMAPI_CLIENT} = 'legacy'; + unlink $ENV{OMAPI_TRACE}; + is(FastCapabilityRunner->key_algorithm_error(settings($algorithm)), undef, + 'legacy MD5 does not require a SHA-capable client'); + ok(!-e $ENV{OMAPI_TRACE}, 'MD5 does not execute the probe'); +} +for my $mode (qw(modern corrected legacy silent failed stalled)) { + local $ENV{OMAPI_CLIENT} = $mode; + write_text($ENV{OMAPI_TRACE}, ''); + my $error = FastCapabilityRunner->key_algorithm_error(settings('hmac-sha256')); + if ($mode eq 'modern' || $mode eq 'corrected') { + is($error, undef, "$mode omshell supports SHA selection"); + } else { + like($error, qr/\Q$client\E.*hmac-sha256.*dhcpomshellpath/, + "$mode omshell is rejected with a remediation message"); + } + is(read_text($ENV{OMAPI_TRACE}), "key-algorithm\n", + "$mode probe sends no connection command or secret"); +} +my $stalled_pid = read_text($ENV{OMAPI_PID}); +ok(!kill(0, $stalled_pid), 'the timed-out probe is reaped'); +{ + my $missing = settings('hmac-sha512'); + $missing->{omshell_path} = "$directory/missing"; + like(FastCapabilityRunner->key_algorithm_error($missing), qr/Cannot verify/, + 'a missing custom client fails closed'); +} + +my $plugin = repo_path('xCAT-server/lib/xcat/plugins/dhcp.pm'); +require $plugin; +{ + package CapabilityBackend; + sub name { return $_[0]->{name}; } + sub check_services { return {error => 'service boundary'}; } +} +{ + no warnings qw(redefine once); + local *xCAT::Utils::isServiceNode = sub { return 0; }; + local *xCAT::Utils::isLinux = sub { return 1; }; + local *xCAT::Utils::isFIPS = sub { return 0; }; + local *xCAT::Utils::checkservicestatus = sub { return 1; }; + local %::XCATSITEVALS = (dhcpomapialgorithm => 'hmac-sha256', dhcpomshellpath => $client); + for my $site ( + ['rhels8.0', '', 1], + ['ubuntu20.04', '', 0], ['ubuntu20.04', '192.0.2.1', 1], + ['ubuntu22.04', '', 0], ['ubuntu22.04', '192.0.2.1', 1] + ) { + local $xCAT_plugin::dhcp::distro = $site->[0]; + local $::XCATSITEVALS{externaldhcpservers} = $site->[1]; + for my $backend (qw(isc kea)) { + local *xCAT::DHCP::Backend::new_backend = sub { return bless {name => $backend}, 'CapabilityBackend'; }; + for my $mode (qw(legacy modern)) { + local $ENV{OMAPI_CLIENT} = $mode; + write_text($ENV{OMAPI_TRACE}, ''); + my @messages; + my $mask = umask; + xCAT_plugin::dhcp::process_request({arg => ['-q'], node => ['node01']}, + sub { push @messages, map { @{$_->{error} || []}, @{$_->{data} || []} } @_; }); + umask $mask; + my $messages = join ' ', @messages; + my $probe = $backend eq 'isc' && $site->[2]; + my $label = "$site->[0] external=$site->[1] $backend $mode"; + if ($probe && $mode eq 'legacy') { + like($messages, qr/Cannot verify key-algorithm/, "$label rejects an unsupported SHA client"); + } else { + like($messages, qr/dhcp server is not running|service boundary/, + "$label reaches the service check"); + } + is(read_text($ENV{OMAPI_TRACE}), $probe ? "key-algorithm\n" : '', + "$label probes only when the query uses OMAPI"); + } + } + } +} + +my $root = "$directory/root"; +make_path("$root/lib/perl/xCAT/DHCP", "$directory/commands"); +write_text("$root/lib/perl/xCAT/DHCP/Backend.pm", <<'PERL'); +package xCAT::DHCP::Backend; +sub new_backend { return bless {}, __PACKAGE__; } +sub name { return 'isc'; } +1; +PERL +write_text("$root/lib/perl/xCAT/Table.pm", <<'PERL'); +package xCAT::Table; +sub new { return bless {}, __PACKAGE__; } +sub getAttribs { return {password => 'dGVzdA=='}; } +1; +PERL +my $wrapper = "$directory/dhcpop-wrapper"; +write_text($wrapper, <<'PERL'); +use xCAT::DHCP::OmapiRunner; +use xCAT::Utils; +no warnings 'redefine'; +*xCAT::Utils::isFIPS = sub { return $ENV{OMAPI_FIPS}; }; +*xCAT::DHCP::OmapiRunner::_completion_delay = sub { return 0; }; +*xCAT::DHCP::OmapiRunner::_command_directory = sub { return $ENV{OMAPI_DIRECTORY}; }; +our %XCATSITEVALS = (dhcpomapialgorithm => $ENV{OMAPI_ALGORITHM}, dhcpomshellpath => $ENV{OMAPI_PATH}); +my $script = shift @ARGV; +do $script; +die $@ if $@; +PERL +for my $case (['hmac-md5', 0, 'legacy', 0], ['hmac-sha256', 0, 'legacy', 1], + ['hmac-sha256', 0, 'modern', 0], ['', 1, 'legacy', 1]) { + local @ENV{qw(XCATROOT OMAPI_ALGORITHM OMAPI_FIPS OMAPI_CLIENT OMAPI_DIRECTORY OMAPI_PATH)} = + ($root, $case->[0], $case->[1], $case->[2], "$directory/commands", $client); + write_text($ENV{OMAPI_TRACE}, ''); + open(my $child, '-|', $Config{perlpath}, "-I$root/lib/perl", + '-I' . repo_path('perl-xCAT'), '-I' . repo_path('xCAT-server/lib/perl'), + '-I' . repo_path('xCAT-test/lib'), $wrapper, + repo_path('xCAT-server/share/xcat/tools/dhcpop'), '-r', '-n', 'node01') or die $!; + my $output = do { local $/; <$child> }; + close($child); + is($? >> 8, $case->[3], "dhcpop: algorithm=$case->[0] FIPS=$case->[1] client=$case->[2]"); + if ($case->[3]) { + like($output, qr/Cannot verify key-algorithm/, 'dhcpop reports the capability error'); + unlike(read_text($ENV{OMAPI_TRACE}), qr/\bconnect\b|dGVzdA==/, + 'dhcpop stops before sending a secret or changing a lease'); + } else { + like(read_text($ENV{OMAPI_TRACE}), qr/connect\nnew host/, 'dhcpop continues with the lease operation'); + } +} + +done_testing(); diff --git a/xCAT-test/unit/fips_dhcp_packaging.t b/xCAT-test/unit/fips_dhcp_packaging.t new file mode 100644 index 000000000..86e1d1102 --- /dev/null +++ b/xCAT-test/unit/fips_dhcp_packaging.t @@ -0,0 +1,39 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use FindBin; +use lib "$FindBin::Bin/../lib"; +use Test::More; + +use XCAT::Test::File qw(repo_path); + +my $rpmspec = qx(command -v rpmspec 2>/dev/null); +chomp($rpmspec); +plan skip_all => 'rpmspec is required' unless $rpmspec && -x $rpmspec; + +foreach my $spec ('xCAT/xCAT.spec', 'xCATsn/xCATsn.spec') { + foreach my $build ( + ['EL8', 'rhel 8'], ['EL9', 'rhel 9'], ['EL10', 'rhel 10'], + ['SLES15', 'suse_version 1500'], ['openEuler', 'openEuler 1'], + ) { + foreach my $arch (qw(x86_64 ppc64le aarch64 s390x riscv64)) { + open(my $fh, '-|', $rpmspec, '-q', '--requires', '--target', $arch, + '--define', 'version 2.20.0', '--define', 'release 1', + '--undefine', 'rhel', '--undefine', 'suse_version', + '--undefine', 'openEuler', '--define', $build->[1], + repo_path($spec)) or die "run rpmspec: $!"; + my @requires = <$fh>; + close($fh) or BAIL_OUT("rpmspec failed for $spec on $build->[0]/$arch"); + chomp @requires; + is_deeply( + [grep { /\bdhcp-server\b/ } @requires], [], + "$spec built on $build->[0]/$arch adds no SHA-only DHCP package floor" + ); + is(scalar(grep { m{/usr/sbin/dhcpd} } @requires), 1, + "$spec built on $build->[0]/$arch retains its DHCP backend requirement"); + } + } +} + +done_testing();