From e0e04f017d638084578e6f22d099852edd3dc08e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 23 Apr 2026 19:39:25 -0300 Subject: [PATCH] Render Kea additional classes by version --- perl-xCAT/xCAT/DHCP/Backend/Kea.pm | 106 ++++++++++++++++++-- xCAT-server/lib/xcat/plugins/dhcp.pm | 10 +- xCAT-test/unit/dhcp_kea_config_validation.t | 10 +- xCAT-test/unit/dhcp_kea_renderer.t | 44 ++++++-- 4 files changed, 146 insertions(+), 24 deletions(-) diff --git a/perl-xCAT/xCAT/DHCP/Backend/Kea.pm b/perl-xCAT/xCAT/DHCP/Backend/Kea.pm index ddcda8bca..ba2b4de5e 100644 --- a/perl-xCAT/xCAT/DHCP/Backend/Kea.pm +++ b/perl-xCAT/xCAT/DHCP/Backend/Kea.pm @@ -62,10 +62,10 @@ sub render_dhcp4_config { $dhcp4{'control-socket'} = $intent->{'control-socket'} if $intent->{'control-socket'}; $dhcp4{'hooks-libraries'} = $intent->{'hooks-libraries'} if $intent->{'hooks-libraries'}; - $dhcp4{'option-def'} = $intent->{'option-def'} if $intent->{'option-def'}; - $dhcp4{'client-classes'} = $intent->{'client-classes'} if $intent->{'client-classes'}; - $dhcp4{'option-data'} = $intent->{'option-data'} if $intent->{'option-data'}; - $dhcp4{'dhcp-ddns'} = $intent->{'dhcp-ddns'} if $intent->{'dhcp-ddns'}; + $dhcp4{'option-def'} = $intent->{'option-def'} if $intent->{'option-def'}; + $dhcp4{'client-classes'} = [ map { $self->_render_client_class($_) } @{ $intent->{'client-classes'} } ] if $intent->{'client-classes'}; + $dhcp4{'option-data'} = $intent->{'option-data'} if $intent->{'option-data'}; + $dhcp4{'dhcp-ddns'} = $intent->{'dhcp-ddns'} if $intent->{'dhcp-ddns'}; foreach my $field (qw/ddns-send-updates ddns-override-no-update ddns-override-client-update ddns-qualifying-suffix ddns-update-on-renew/) { $dhcp4{$field} = $intent->{$field} if exists $intent->{$field}; } @@ -659,13 +659,19 @@ sub _render_subnet4 { $rendered{interface} = $subnet->{interface} if defined $subnet->{interface}; $rendered{'next-server'} = _first_defined( $subnet->{'next-server'}, $subnet->{next_server} ); $rendered{'boot-file-name'} = _first_defined( $subnet->{'boot-file-name'}, $subnet->{boot_file_name} ); - $rendered{'option-data'} = _first_defined( $subnet->{'option-data'}, $subnet->{option_data} ); - $rendered{'require-client-classes'} = _first_defined( $subnet->{'require-client-classes'}, $subnet->{require_client_classes} ); - $rendered{reservations} = $subnet->{reservations} if $subnet->{reservations}; + $rendered{'option-data'} = _first_defined( $subnet->{'option-data'}, $subnet->{option_data} ); + my $additional_classes = _first_defined( + $subnet->{additional_client_classes}, + $subnet->{'evaluate-additional-classes'}, + $subnet->{evaluate_additional_classes}, + $subnet->{'require-client-classes'}, + $subnet->{require_client_classes}, + ); + $rendered{ $self->_additional_class_list_field() } = $additional_classes if defined $additional_classes; + $rendered{reservations} = $subnet->{reservations} if $subnet->{reservations}; delete $rendered{'next-server'} unless defined $rendered{'next-server'}; delete $rendered{'boot-file-name'} unless defined $rendered{'boot-file-name'}; delete $rendered{'option-data'} unless defined $rendered{'option-data'}; - delete $rendered{'require-client-classes'} unless defined $rendered{'require-client-classes'}; if ( $subnet->{pools} ) { $rendered{pools} = $subnet->{pools}; @@ -678,6 +684,26 @@ sub _render_subnet4 { return \%rendered; } +sub _render_client_class { + my ( $self, $client_class ) = @_; + + my %rendered = %$client_class; + my $additional_only = _first_defined( + $client_class->{additional_only}, + $client_class->{'only-in-additional-list'}, + $client_class->{only_in_additional_list}, + $client_class->{'only-if-required'}, + $client_class->{only_if_required}, + ); + + delete @rendered{qw/additional_only only_in_additional_list only_if_required/}; + delete $rendered{'only-in-additional-list'}; + delete $rendered{'only-if-required'}; + $rendered{ $self->_additional_class_flag_field() } = $additional_only if defined $additional_only; + + return \%rendered; +} + sub _render_subnet6 { my ( $self, $subnet ) = @_; @@ -761,6 +787,51 @@ sub _control_agent_not_found { return 0; } +sub _additional_class_flag_field { + my ($self) = @_; + + return $self->_use_modern_additional_class_syntax() ? 'only-in-additional-list' : 'only-if-required'; +} + +sub _additional_class_list_field { + my ($self) = @_; + + return $self->_use_modern_additional_class_syntax() ? 'evaluate-additional-classes' : 'require-client-classes'; +} + +sub _use_modern_additional_class_syntax { + my ($self) = @_; + + return 1 if $self->{additional_class_syntax} && $self->{additional_class_syntax} eq 'modern'; + return 0 if $self->{additional_class_syntax} && $self->{additional_class_syntax} eq 'legacy'; + + my $version = $self->kea_version(); + return _version_at_least( $version, '2.7.4' ); +} + +sub kea_version { + my ($self) = @_; + + 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); + } + + if ( $output =~ /(\d+(?:\.\d+){1,2})/ ) { + $self->{_detected_kea_version} = $1; + } + + return $self->{_detected_kea_version}; +} + sub _first_defined { my @values = @_; foreach my $value (@values) { @@ -770,6 +841,25 @@ sub _first_defined { return; } +sub _version_at_least { + my ( $version, $minimum ) = @_; + + return 0 unless defined($version) && $version =~ /^\d+(?:\.\d+)*/; + + my @version_parts = split /\./, $version; + my @minimum_parts = split /\./, $minimum; + my $max = @version_parts > @minimum_parts ? @version_parts : @minimum_parts; + + for my $idx ( 0 .. $max - 1 ) { + my $left = $version_parts[$idx] || 0; + my $right = $minimum_parts[$idx] || 0; + return 1 if $left > $right; + return 0 if $left < $right; + } + + return 1; +} + sub _integer { my ($value) = @_; diff --git a/xCAT-server/lib/xcat/plugins/dhcp.pm b/xCAT-server/lib/xcat/plugins/dhcp.pm index 31bb6b3f0..a17e83462 100644 --- a/xCAT-server/lib/xcat/plugins/dhcp.pm +++ b/xCAT-server/lib/xcat/plugins/dhcp.pm @@ -2777,7 +2777,7 @@ sub kea_subnet4_intent ); $subnet{interface} = $interface unless $remote; if ($opal_class) { - $subnet{'require-client-classes'} = [ $opal_class->{name} ]; + $subnet{additional_client_classes} = [ $opal_class->{name} ]; $subnet{client_classes} = [$opal_class]; } @@ -2794,10 +2794,10 @@ sub kea_opal_client_class $class_name =~ s/[^A-Za-z0-9_.-]/_/g; return { - name => $class_name, - test => 'option[93].hex == 0x000e', - 'only-if-required' => JSON::true, - 'option-data' => [ + name => $class_name, + test => 'option[93].hex == 0x000e', + additional_only => JSON::true, + 'option-data' => [ { name => 'conf-file', data => "http://$tftp:$httpport/tftpboot/pxelinux.cfg/p/" . $net . "_" . $prefix, diff --git a/xCAT-test/unit/dhcp_kea_config_validation.t b/xCAT-test/unit/dhcp_kea_config_validation.t index a06fb64be..95f1fe483 100644 --- a/xCAT-test/unit/dhcp_kea_config_validation.t +++ b/xCAT-test/unit/dhcp_kea_config_validation.t @@ -33,10 +33,10 @@ my $json = $backend->render_dhcp4_config( 'boot-file-name' => 'http://192.168.122.1:80/tftpboot/xcat/xnba/nodes/node01', }, { - name => 'xcat-opal-v3-192.168.122.0-24', - test => 'option[93].hex == 0x000e', - 'only-if-required' => JSON::true, - 'option-data' => [ + name => 'xcat-opal-v3-192.168.122.0-24', + test => 'option[93].hex == 0x000e', + additional_only => JSON::true, + 'option-data' => [ { name => 'conf-file', data => 'http://192.168.122.1:80/tftpboot/pxelinux.cfg/p/192.168.122.0_24' }, ], }, @@ -64,7 +64,7 @@ my $json = $backend->render_dhcp4_config( subnet => '192.168.122.0/24', dynamicrange => '192.168.122.100-192.168.122.120', next_server => '192.168.122.1', - 'require-client-classes' => ['xcat-opal-v3-192.168.122.0-24'], + additional_client_classes => ['xcat-opal-v3-192.168.122.0-24'], option_data => [ { name => 'routers', data => '192.168.122.1' }, { name => 'domain-name', data => 'cluster.test' }, diff --git a/xCAT-test/unit/dhcp_kea_renderer.t b/xCAT-test/unit/dhcp_kea_renderer.t index 247ecde11..4446a20de 100644 --- a/xCAT-test/unit/dhcp_kea_renderer.t +++ b/xCAT-test/unit/dhcp_kea_renderer.t @@ -10,7 +10,7 @@ use Test::More; use xCAT::DHCP::Backend::Kea; -my $backend = xCAT::DHCP::Backend::Kea->new(); +my $backend = xCAT::DHCP::Backend::Kea->new( kea_version => '2.4.1' ); my $json = $backend->render_dhcp4_config( { interfaces => ['eth0'], @@ -22,7 +22,7 @@ my $json = $backend->render_dhcp4_config( interface => 'eth0', dynamicrange => '10.0.0.100-10.0.0.120;10.0.0.130,10.0.0.140', next_server => '10.0.0.1', - 'require-client-classes' => ['xcat-opal-v3-10.0.0.0-24'], + additional_client_classes => ['xcat-opal-v3-10.0.0.0-24'], option_data => [ { name => 'routers', data => '10.0.0.1' }, { name => 'domain-name-servers', data => '10.0.0.2, 10.0.0.3' }, @@ -44,10 +44,10 @@ my $json = $backend->render_dhcp4_config( 'boot-file-name' => 'xcat/xnba.efi', }, { - name => 'xcat-opal-v3-10.0.0.0-24', - test => 'option[93].hex == 0x000e', - 'only-if-required' => JSON::true, - 'option-data' => [ + name => 'xcat-opal-v3-10.0.0.0-24', + test => 'option[93].hex == 0x000e', + additional_only => JSON::true, + 'option-data' => [ { name => 'conf-file', data => 'http://10.0.0.1/tftpboot/pxelinux.cfg/p/10.0.0.0_24' }, ], }, @@ -123,6 +123,38 @@ is_deeply( 'client classes are preserved, including subnet-specific OPAL conf-file class' ); +my $modern_backend = xCAT::DHCP::Backend::Kea->new( kea_version => '3.0.1' ); +my $modern_json = $modern_backend->render_dhcp4_config( + { + subnets => [ + { + id => 3, + subnet => '10.0.2.0/24', + pools => [], + additional_client_classes => ['xcat-opal-v3-10.0.2.0-24'], + }, + ], + 'client-classes' => [ + { + name => 'xcat-opal-v3-10.0.2.0-24', + test => 'option[93].hex == 0x000e', + additional_only => JSON::true, + }, + ], + } +); +my $modern_config = decode_json($modern_json); +my $modern_subnet = $modern_config->{Dhcp4}{subnet4}[0]; +my $modern_class = $modern_config->{Dhcp4}{'client-classes'}[0]; +is_deeply( + $modern_subnet->{'evaluate-additional-classes'}, + ['xcat-opal-v3-10.0.2.0-24'], + 'Kea 3.x renders modern subnet additional-class evaluation field' +); +ok( !exists $modern_subnet->{'require-client-classes'}, 'Kea 3.x output omits deprecated subnet additional-class field' ); +is( $modern_class->{'only-in-additional-list'}, JSON::true, 'Kea 3.x renders modern class additional-evaluation flag' ); +ok( !exists $modern_class->{'only-if-required'}, 'Kea 3.x output omits deprecated class additional-evaluation flag' ); + my $empty_boot_json = $backend->render_dhcp4_config( { subnets => [