diff --git a/perl-xCAT/xCAT/DHCP/Backend/Kea.pm b/perl-xCAT/xCAT/DHCP/Backend/Kea.pm index 2525ae7d3..30f6a028f 100644 --- a/perl-xCAT/xCAT/DHCP/Backend/Kea.pm +++ b/perl-xCAT/xCAT/DHCP/Backend/Kea.pm @@ -630,6 +630,42 @@ sub query_reservations { return \@found; } +sub preserve_reservations { + my ( $self, $config, $intent ) = @_; + + my $target_subnets = $intent->{subnets} || []; + return $intent unless @$target_subnets; + + my $target_config = $config->{Dhcp6} + ? { Dhcp6 => { subnet6 => $target_subnets } } + : { Dhcp4 => { subnet4 => $target_subnets } }; + + foreach my $source_subnet ( _subnets_for_config($config) ) { + foreach my $reservation ( @{ $source_subnet->{reservations} || [] } ) { + my $ip = $reservation->{'ip-address'}; + $ip = $reservation->{'ip-addresses'}[0] + if !$ip && ref( $reservation->{'ip-addresses'} ) eq 'ARRAY'; + + my $target_subnet; + if ($ip) { + my $subnet_id = $self->subnet_id_for_ip( $target_config, $ip ); + $target_subnet = _find_subnet_by_id( $target_config, $subnet_id ) if defined($subnet_id); + } + if ( !$target_subnet && $source_subnet->{subnet} ) { + ($target_subnet) = grep { + ( $_->{subnet} || '' ) eq $source_subnet->{subnet} + } @$target_subnets; + } + next unless $target_subnet; + + $target_subnet->{reservations} ||= []; + push @{ $target_subnet->{reservations} }, { %$reservation }; + } + } + + return $intent; +} + sub subnet_id_for_ip { my ( $self, $config, $ip ) = @_; diff --git a/xCAT-server/lib/xcat/plugins/dhcp.pm b/xCAT-server/lib/xcat/plugins/dhcp.pm index 1a0e5ba42..396370d9a 100644 --- a/xCAT-server/lib/xcat/plugins/dhcp.pm +++ b/xCAT-server/lib/xcat/plugins/dhcp.pm @@ -2380,6 +2380,24 @@ sub kea_process_request } if ($opt->{n}) { + my $loaded4 = $backend->load_dhcp4_config(); + if ($loaded4->{error}) { + $callback->({ error => [ $loaded4->{error} ], errorcode => [1] }); + flock($dhcplockfd, LOCK_UN); + return; + } + $backend->preserve_reservations($loaded4, $intent4); + + if ($using_dhcp6) { + my $loaded6 = $backend->load_dhcp6_config(); + if ($loaded6->{error}) { + $callback->({ error => [ $loaded6->{error} ], errorcode => [1] }); + flock($dhcplockfd, LOCK_UN); + return; + } + $backend->preserve_reservations($loaded6, $intent6); + } + my $result = $backend->write_dhcp4_config($intent4, backup_existing => 1); if ($result->{error}) { $callback->({ error => [ $result->{error} ], errorcode => [1] }); diff --git a/xCAT-test/unit/dhcp_kea_renderer.t b/xCAT-test/unit/dhcp_kea_renderer.t index 89ae04cfb..877877090 100644 --- a/xCAT-test/unit/dhcp_kea_renderer.t +++ b/xCAT-test/unit/dhcp_kea_renderer.t @@ -329,6 +329,96 @@ my $deleted = $backend->delete_reservations( $reservation_config, { 'hw-address' is( scalar @$deleted, 1, 'reservation delete returns deleted reservation' ); is( scalar @{ $reservation_config->{Dhcp4}{subnet4}[0]{reservations} }, 0, 'reservation is removed from config' ); +my $existing_dhcp4 = { + Dhcp4 => { + subnet4 => [ + { + id => 1, + subnet => '10.20.0.0/24', + reservations => [ + { + 'hw-address' => '00:11:22:33:44:66', + 'ip-address' => '10.20.0.12', + hostname => 'node20', + }, + ], + }, + { + id => 2, + subnet => '10.30.0.0/24', + reservations => [ + { + 'hw-address' => '00:11:22:33:44:77', + 'ip-address' => '10.30.0.12', + hostname => 'removed-node', + }, + ], + }, + ], + }, +}; +my $regenerated_dhcp4 = { + subnets => [ + { + id => 9, + subnet => '10.20.0.0/24', + }, + ], +}; +$backend->preserve_reservations( $existing_dhcp4, $regenerated_dhcp4 ); +is_deeply( + $regenerated_dhcp4->{subnets}[0]{reservations}, + $existing_dhcp4->{Dhcp4}{subnet4}[0]{reservations}, + 'regeneration preserves IPv4 reservations in the matching subnet' +); +is( + scalar @{ $regenerated_dhcp4->{subnets}[0]{reservations} }, + 1, + 'regeneration drops reservations for removed IPv4 subnets' +); +is( + $regenerated_dhcp4->{subnets}[0]{id}, + 9, + 'regeneration does not copy the old IPv4 subnet id' +); + +my $existing_dhcp6 = { + Dhcp6 => { + subnet6 => [ + { + id => 10001, + subnet => '2001:db8:20::/64', + reservations => [ + { + duid => '00:04:00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff', + 'ip-addresses' => ['2001:db8:20::12'], + hostname => 'node20-v6', + }, + ], + }, + ], + }, +}; +my $regenerated_dhcp6 = { + subnets => [ + { + id => 10009, + subnet => '2001:db8:20::/64', + }, + ], +}; +$backend->preserve_reservations( $existing_dhcp6, $regenerated_dhcp6 ); +is_deeply( + $regenerated_dhcp6->{subnets}[0]{reservations}, + $existing_dhcp6->{Dhcp6}{subnet6}[0]{reservations}, + 'regeneration preserves IPv6 reservations in the matching subnet' +); +is( + $regenerated_dhcp6->{subnets}[0]{id}, + 10009, + 'regeneration does not copy the old IPv6 subnet id' +); + my $hookdir = tempdir(CLEANUP => 1); my $hook = "$hookdir/libdhcp_host_cmds.so"; open(my $hookfh, '>', $hook) or die "Unable to create fake hook: $!";