From: Hannes Laimer <h.laimer@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH pve-network v2 04/16] sdn: push mapping changes from the ipam API to the dhcp backend
Date: Wed, 9 Sep 2026 12:41:32 +0200 [thread overview]
Message-ID: <20260909104144.1110031-5-h.laimer@proxmox.com> (raw)
In-Reply-To: <20260909104144.1110031-1-h.laimer@proxmox.com>
Mapping edits through the API only wrote the IPAM record and the cache,
the dhcp backend was never told. So dnsmasq kept serving stale
reservations until a later guest start in the zone happened to sweep
them. The same held for a guest whose NIC moved to another bridge. The
old records were released and new ones allocated without the backend
hearing of either.
Push the MAC's current answer after every record change on the node the
change is made on, the other nodes still catch up at a guest start
there. A guest start pushes once after allocating what it lacks, so a
guest whose records exist already is pushed as well. The push is best
effort, the record write stays authoritative. It goes to the applied
zone's backend, a zone edited but not applied has nothing running yet.
dnsmasq rewrites the MAC's reservation in one pass and reloads only when
the file changed. So an unchanged record costs no reload, and a changed
one leaves no moment in which the MAC is unknown. The cache is read
under the ethers lock, a snapshot from before it would sweep a line
another push wrote meanwhile. So the dispatcher hands over only the MAC.
A reservation line is matched to its MAC regardless of case, the cache
and the file may spell it differently. Removing a mapping was dispatched
to the plugins but implemented nowhere, it is the empty case of that
rewrite. A zone confined to other nodes has no backend here and is
skipped. A failed lease refresh only warns, the reservation is written
by then.
Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
src/PVE/API2/Network/SDN/Ips.pm | 7 +
src/PVE/Network/SDN/Dhcp.pm | 44 +++---
src/PVE/Network/SDN/Dhcp/Dnsmasq.pm | 101 ++++++++----
src/PVE/Network/SDN/Dhcp/Plugin.pm | 6 +-
src/PVE/Network/SDN/Vnets.pm | 5 +-
src/test/run_test_vnets_blackbox.pl | 229 +++++++++++++++++++++++++++-
6 files changed, 329 insertions(+), 63 deletions(-)
diff --git a/src/PVE/API2/Network/SDN/Ips.pm b/src/PVE/API2/Network/SDN/Ips.pm
index 5ff05e7..d7b682d 100644
--- a/src/PVE/API2/Network/SDN/Ips.pm
+++ b/src/PVE/API2/Network/SDN/Ips.pm
@@ -46,6 +46,8 @@ __PACKAGE__->register_method({
eval { PVE::Network::SDN::Vnets::del_ip($vnet, $ip, '', $mac); };
die "$@\n" if $@;
+ PVE::Network::SDN::Dhcp::update_mapping($vnet, $mac);
+
return undef;
},
});
@@ -82,6 +84,8 @@ __PACKAGE__->register_method({
PVE::Network::SDN::Vnets::add_ip($vnet, $ip, '', $mac, undef);
+ PVE::Network::SDN::Dhcp::update_mapping($vnet, $mac);
+
return undef;
},
});
@@ -132,6 +136,9 @@ __PACKAGE__->register_method({
}
die "$error\n" if $error;
+
+ PVE::Network::SDN::Dhcp::update_mapping($vnet, $mac);
+
return undef;
},
});
diff --git a/src/PVE/Network/SDN/Dhcp.pm b/src/PVE/Network/SDN/Dhcp.pm
index 65e40d4..28a9f2e 100644
--- a/src/PVE/Network/SDN/Dhcp.pm
+++ b/src/PVE/Network/SDN/Dhcp.pm
@@ -6,7 +6,6 @@ use warnings;
use PVE::Cluster;
use PVE::Network::SDN;
-use PVE::Network::SDN::Ipams;
use PVE::Network::SDN::Subnets;
use PVE::Network::SDN::Dhcp::Plugin;
use PVE::Network::SDN::Dhcp::Dnsmasq;
@@ -22,37 +21,28 @@ sub plugin_types {
return PVE::Network::SDN::Dhcp::Plugin->lookup_types();
}
-sub add_mapping {
- my ($vnetid, $mac, $ip4, $ip6) = @_;
-
- my $vnet = PVE::Network::SDN::Vnets::get_vnet($vnetid);
- return if !$vnet;
-
- my $zoneid = $vnet->{zone};
- my $zone = PVE::Network::SDN::Zones::get_zone($zoneid);
-
- return if !$zone->{ipam} || !$zone->{dhcp};
-
- my $dhcptype = $zone->{dhcp};
-
- my $macdb = PVE::Network::SDN::Ipams::read_macdb();
- my $dhcp_plugin = PVE::Network::SDN::Dhcp::Plugin->lookup($dhcptype);
- $dhcp_plugin->add_ip_mapping($zoneid, $macdb, $mac, $ip4, $ip6);
-}
-
-sub remove_mapping {
+# re-apply a MAC's mapping on this node from the current records, best
+# effort, the record write stays authoritative
+sub update_mapping {
my ($vnetid, $mac) = @_;
- my $vnet = PVE::Network::SDN::Vnets::get_vnet($vnetid);
- return if !$vnet;
+ eval {
+ # the backend to push to is the applied one, a zone edited but not
+ # applied has nothing running yet
+ my $vnet = PVE::Network::SDN::Vnets::get_vnet($vnetid, 1);
+ return if !$vnet;
- my $zoneid = $vnet->{zone};
- my $zone = PVE::Network::SDN::Zones::get_zone($zoneid);
+ my $zoneid = $vnet->{zone};
+ my $zone = PVE::Network::SDN::Zones::get_zone($zoneid, 1);
+ return if !$zone || !$zone->{ipam} || !$zone->{dhcp};
- return if !$zone->{ipam} || !$zone->{dhcp};
+ # a zone confined to other nodes has no backend running here
+ return if defined($zone->{nodes}) && !$zone->{nodes}->{ PVE::INotify::nodename() };
- my $dhcp_plugin = PVE::Network::SDN::Dhcp::Plugin->lookup($zone->{dhcp});
- $dhcp_plugin->del_ip_mapping($zoneid, $mac);
+ PVE::Network::SDN::Dhcp::Plugin->lookup($zone->{dhcp})
+ ->update_ip_mapping($zoneid, $mac);
+ };
+ warn "could not update dhcp mapping for $mac: $@" if $@;
}
sub regenerate_config {
diff --git a/src/PVE/Network/SDN/Dhcp/Dnsmasq.pm b/src/PVE/Network/SDN/Dhcp/Dnsmasq.pm
index 477b700..f3c7739 100644
--- a/src/PVE/Network/SDN/Dhcp/Dnsmasq.pm
+++ b/src/PVE/Network/SDN/Dhcp/Dnsmasq.pm
@@ -12,6 +12,7 @@ use File::Copy;
use Net::DBus;
use PVE::RESTEnvironment qw(log_warn);
+use PVE::Network::SDN::Ipams;
my $DNSMASQ_CONFIG_ROOT = '/etc/dnsmasq.d';
my $DNSMASQ_DEFAULT_ROOT = '/etc/default';
@@ -51,29 +52,65 @@ sub update_lease {
$manager->AddDhcpLease($ip4, $mac, \@hostname, undef, 0, 0, 0) if $ip4;
}
-sub add_ip_mapping {
- my ($class, $dhcpid, $macdb, $mac, $ip4, $ip6) = @_;
+sub update_ip_mapping {
+ my ($class, $dhcpid, $mac) = @_;
my $ethers_file = ethers_file($dhcpid);
my $ethers_tmp_file = "$ethers_file.tmp";
- my $reload = undef;
+ my ($ip4, $ip6);
+ my $reservation = undef;
+ my $changed = undef;
+
+ my $rewriteFn = sub {
+ # read under the ethers lock. A snapshot from before it would sweep a
+ # line another push wrote meanwhile, and an older push would put its
+ # addresses back
+
+ my $macdb = PVE::Network::SDN::Ipams::read_macdb();
+ # the cache spells a MAC and an address as their writer did, the file
+ # holds one spelling of both. An entry without an address says nothing
+ # about its MAC, a line of it is stale
+ my %macs;
+ for my $key (sort keys $macdb->{macs}->%*) {
+ my $entry = $macdb->{macs}->{$key};
+ my %addresses =
+ map { $_ => lc($entry->{$_}) } grep { defined($entry->{$_}) } qw(ip4 ip6);
+ next if !%addresses;
+ $macs{ lc($key) } = { %{ $macs{ lc($key) } // {} }, %addresses };
+ }
+
+ my $entry = $macs{ lc($mac) };
+ ($ip4, $ip6) = ($entry->{ip4}, $entry->{ip6}) if $entry;
+ if ($ip4 || $ip6) {
+ $reservation = $mac;
+ $reservation .= ",$ip4" if $ip4;
+ $reservation .= ",[$ip6]" if $ip6;
+ # an external IPAM spells addresses its own way, the file holds one spelling
+ $reservation = lc($reservation);
+ }
- my $appendFn = sub {
+ # the first reservation of a zone finds no file yet
+ file_set_contents($ethers_file, '') if !-e $ethers_file;
open(my $in, '<', $ethers_file) or die "Could not open file '$ethers_file' $!\n";
open(my $out, '>', $ethers_tmp_file)
or die "Could not open file '$ethers_tmp_file' $!\n";
- my $match = undef;
+ my $kept = undef;
- my $line_no = 0;
while (my $line = <$in>) {
- $line_no++;
chomp($line);
+ next if $line !~ m/\S/;
my ($parsed_mac, $parsed_ip1, $parsed_ip2) = split(/,/, $line);
- if (!defined($parsed_mac)) {
- warn "failed to parse MAC from $dhcpid ethers file on line $line_no: '$line'\n";
+ # the MAC's own reservation is rewritten, an unchanged one stays in place
+ if (lc($parsed_mac) eq lc($mac)) {
+ if (defined($reservation) && !$kept && lc($line) eq $reservation) {
+ $kept = 1;
+ print $out "$line\n";
+ } else {
+ $changed = 1;
+ }
next;
}
@@ -81,41 +118,35 @@ sub add_ip_mapping {
if ($parsed_ip2) {
$parsed_ip4 = $parsed_ip1;
$parsed_ip6 = $parsed_ip2;
- } elsif (Net::IP::ip_is_ipv4($parsed_ip1)) {
+ } elsif (defined($parsed_ip1) && Net::IP::ip_is_ipv4($parsed_ip1)) {
$parsed_ip4 = $parsed_ip1;
- } else {
+ } elsif (defined($parsed_ip1)) {
$parsed_ip6 = $parsed_ip1;
}
$parsed_ip6 = $1 if $parsed_ip6 && $parsed_ip6 =~ m/\[(\S+)\]/;
+ # a line of the old code may spell an address in upper case
+ $parsed_ip6 = lc($parsed_ip6) if $parsed_ip6;
#delete changed
if (
- !defined($macdb->{macs}->{$parsed_mac})
+ !defined($macs{ lc($parsed_mac) })
|| ($parsed_ip4
- && $macdb->{macs}->{$parsed_mac}->{'ip4'}
- && $macdb->{macs}->{$parsed_mac}->{'ip4'} ne $parsed_ip4)
+ && $macs{ lc($parsed_mac) }->{'ip4'}
+ && $macs{ lc($parsed_mac) }->{'ip4'} ne $parsed_ip4)
|| ($parsed_ip6
- && $macdb->{macs}->{$parsed_mac}->{'ip6'}
- && $macdb->{macs}->{$parsed_mac}->{'ip6'} ne $parsed_ip6)
+ && $macs{ lc($parsed_mac) }->{'ip6'}
+ && $macs{ lc($parsed_mac) }->{'ip6'} ne $parsed_ip6)
) {
- $reload = 1;
+ $changed = 1;
next;
}
- if ($parsed_mac eq $mac) {
- $match = 1 if $ip4 && $parsed_ip4 && $ip4;
- $match = 1 if $ip6 && $parsed_ip6 && $ip6;
- }
-
print $out "$line\n";
}
- if (!$match) {
- my $reservation = $mac;
- $reservation .= ",$ip4" if $ip4;
- $reservation .= ",[$ip6]" if $ip6;
+ if (defined($reservation) && !$kept) {
print $out "$reservation\n";
- $reload = 1;
+ $changed = 1;
}
close $in;
@@ -124,16 +155,22 @@ sub add_ip_mapping {
chmod 0644, $ethers_file;
};
- PVE::Tools::lock_file($ethers_file, 10, $appendFn);
+ # the rewrite replaces the reservation file, a lock on the file itself
+ # would not outlive the rename
+ PVE::Tools::lock_file("$ethers_file.lock", 10, $rewriteFn);
if ($@) {
- warn "Unable to add $mac to the dnsmasq configuration: $@\n";
+ warn "Unable to update $mac in the dnsmasq configuration: $@\n";
return;
}
- my $service_name = "dnsmasq\@$dhcpid";
- systemctl_service('reload', $service_name) if $reload;
- update_lease($dhcpid, $ip4, $mac);
+ systemctl_service('reload', "dnsmasq\@$dhcpid") if $changed;
+
+ # the reservation is written, a lease refresh that fails only warns
+ if ($ip4) {
+ eval { update_lease($dhcpid, $ip4, $mac) };
+ log_warn("could not update the dnsmasq lease of $mac: $@") if $@;
+ }
}
sub configure_subnet {
diff --git a/src/PVE/Network/SDN/Dhcp/Plugin.pm b/src/PVE/Network/SDN/Dhcp/Plugin.pm
index b5d32fa..3b95a68 100644
--- a/src/PVE/Network/SDN/Dhcp/Plugin.pm
+++ b/src/PVE/Network/SDN/Dhcp/Plugin.pm
@@ -22,8 +22,10 @@ sub private {
return $defaultData;
}
-sub add_ip_mapping {
- my ($class, $dhcpid, $macdb, $mac, $ip4, $ip6) = @_;
+# the MAC's records changed, make the backend serve the current ones, none
+# meaning its reservation goes
+sub update_ip_mapping {
+ my ($class, $dhcpid, $mac) = @_;
die 'implement in sub class';
}
diff --git a/src/PVE/Network/SDN/Vnets.pm b/src/PVE/Network/SDN/Vnets.pm
index c327a4b..8453a27 100644
--- a/src/PVE/Network/SDN/Vnets.pm
+++ b/src/PVE/Network/SDN/Vnets.pm
@@ -211,6 +211,8 @@ sub del_ips_from_mac {
PVE::Network::SDN::Vnets::del_ip($vnetid, $ip4, $hostname, $mac) if $ip4;
PVE::Network::SDN::Vnets::del_ip($vnetid, $ip6, $hostname, $mac) if $ip6;
+ PVE::Network::SDN::Dhcp::update_mapping($vnetid, $mac) if $ip4 || $ip6;
+
return ($ip4, $ip6);
}
@@ -225,11 +227,12 @@ sub add_dhcp_mapping {
return if !$zone->{ipam} || !$zone->{dhcp};
my ($ip4, $ip6) = PVE::Network::SDN::Vnets::get_ips_from_mac($vnetid, $mac);
+ # one push at the end covers the allocated and the existing records alike
add_next_free_cidr($vnetid, $name, $mac, "$vmid", undef, 1, 4) if !$ip4;
add_next_free_cidr($vnetid, $name, $mac, "$vmid", undef, 1, 6) if !$ip6;
($ip4, $ip6) = PVE::Network::SDN::Vnets::get_ips_from_mac($vnetid, $mac);
- PVE::Network::SDN::Dhcp::add_mapping($vnetid, $mac, $ip4, $ip6) if $ip4 || $ip6;
+ PVE::Network::SDN::Dhcp::update_mapping($vnetid, $mac) if $ip4 || $ip6;
}
1;
diff --git a/src/test/run_test_vnets_blackbox.pl b/src/test/run_test_vnets_blackbox.pl
index 9f4c424..8273715 100755
--- a/src/test/run_test_vnets_blackbox.pl
+++ b/src/test/run_test_vnets_blackbox.pl
@@ -46,6 +46,7 @@ sub clear_test_state {
vnets_config => {},
macdb => {},
ipamdb => {},
+ dnsmasq_calls => [],
ipam_config => {
'ids' => {
'pve' => {
@@ -220,7 +221,9 @@ $mocked_sdn_dhcp_dnsmasq->mock(
assert_dnsmasq_installed => sub { return 1; },
before_configure => sub { },
ethers_file => sub { return "/tmp/ethers"; },
- systemctl_service => sub { },
+ systemctl_service => sub {
+ push $test_state->{dnsmasq_calls}->@*, $_[0];
+ },
update_lease => sub { },
);
@@ -239,6 +242,11 @@ $mocked_rpc_env_obj->mock(
check_any => sub { return 1; },
);
+my $mocked_pve_inotify = Test::MockModule->new('PVE::INotify');
+$mocked_pve_inotify->mock(
+ nodename => sub { return 'localnode'; },
+);
+
my $mocked_pve_cluster_obj = Test::MockModule->new('PVE::Cluster');
$mocked_pve_cluster_obj->mock(
check_cfs_quorum => sub { return 1; },
@@ -288,6 +296,11 @@ sub create_zone {
return $zone;
}
+sub update_zone {
+ my ($zoneid, $params) = @_;
+ PVE::API2::Network::SDN::Zones->update({ zone => $zoneid, %$params });
+}
+
sub get_vnet {
my ($id) = @_;
return eval { PVE::API2::Network::SDN::Vnets->read({ vnet => $id }); };
@@ -330,6 +343,16 @@ sub create_ip {
return PVE::API2::Network::SDN::Ips->ipcreate($param);
}
+sub update_ip {
+ my ($param) = @_;
+ return PVE::API2::Network::SDN::Ips->ipupdate($param);
+}
+
+sub delete_ip {
+ my ($param) = @_;
+ return PVE::API2::Network::SDN::Ips->ipdelete($param);
+}
+
sub run_test {
my $test = shift;
clear_test_state();
@@ -963,4 +986,208 @@ run_test(
2,
);
+# -------------- dnsmasq mapping pushes
+
+sub test_dnsmasq_mapping_push {
+ my $test_name = (split(/::/, (caller(0))[3]))[-1];
+ my $zoneid = "TESTZONE";
+ my $vnetid = "testvnet";
+ my $mac = "da:65:8f:18:9b:6f";
+
+ create_zone({
+ type => "simple",
+ dhcp => "dnsmasq",
+ ipam => "pve",
+ zone => $zoneid,
+ });
+ create_vnet({
+ type => "vnet",
+ zone => $zoneid,
+ vnet => $vnetid,
+ });
+ create_subnet({
+ type => "subnet",
+ vnet => $vnetid,
+ subnet => "10.0.0.0/24",
+ gateway => "10.0.0.1",
+ 'dhcp-range' => ["start-address=10.0.0.100,end-address=10.0.0.200"],
+ });
+
+ # the first push of a zone finds no ethers file yet
+ unlink($TMP_ETHERS_FILE);
+
+ my $ethers = sub { PVE::Tools::file_get_contents($TMP_ETHERS_FILE) };
+ my $reloads = sub {
+ my $count = scalar(grep { $_ eq 'reload' } $test_state->{dnsmasq_calls}->@*);
+ $test_state->{dnsmasq_calls} = [];
+ return $count;
+ };
+
+ eval { nic_start($vnetid, $mac, "999", "testhostname"); };
+ if ($@) {
+ fail("$test_name: nic_start: $@");
+ return;
+ }
+ is($ethers->(), "$mac,10.0.0.100\n", "$test_name: guest start reserves the allocation");
+ is($reloads->(), 1, "$test_name: guest start reloads once");
+
+ # the records exist already, the restart pushes them without a change
+ nic_start($vnetid, $mac, "999", "testhostname");
+ is($ethers->(), "$mac,10.0.0.100\n", "$test_name: restart keeps the reservation");
+ is($reloads->(), 0, "$test_name: restart without a change does not reload");
+
+ update_ip({
+ zone => $zoneid,
+ vnet => $vnetid,
+ mac => $mac,
+ ip => "10.0.0.150",
+ });
+ is($ethers->(), "$mac,10.0.0.150\n", "$test_name: mapping edit replaces the reservation");
+ is($reloads->(), 1, "$test_name: mapping edit reloads once");
+
+ # a push writes what the cache holds, so a repeated one rewrites nothing
+ PVE::Network::SDN::Dhcp::Dnsmasq->update_ip_mapping($zoneid, $mac);
+ is($ethers->(), "$mac,10.0.0.150\n", "$test_name: a repeated push keeps the reservation");
+ is($reloads->(), 0, "$test_name: a repeated push changes nothing");
+
+ # the cache and the file may spell a MAC differently, it is one MAC
+ PVE::Network::SDN::Dhcp::Dnsmasq->update_ip_mapping($zoneid, uc($mac));
+ is(
+ $ethers->(),
+ "$mac,10.0.0.150\n",
+ "$test_name: a push spelled differently keeps the line as it is",
+ );
+ is($reloads->(), 0, "$test_name: and reloads nothing");
+
+ delete_ip({
+ zone => $zoneid,
+ vnet => $vnetid,
+ mac => $mac,
+ ip => "10.0.0.150",
+ });
+ is($ethers->(), "", "$test_name: mapping delete drops the reservation");
+ is($reloads->(), 1, "$test_name: mapping delete reloads once");
+
+ # a zone confined to other nodes runs no dnsmasq here, an edit is not
+ # pushed on this node
+ update_zone($zoneid, { nodes => 'other' });
+ create_ip({
+ zone => $zoneid,
+ vnet => $vnetid,
+ mac => $mac,
+ ip => "10.0.0.150",
+ });
+ is($ethers->(), "", "$test_name: a zone confined to other nodes gets no push here");
+ is($reloads->(), 0, "$test_name: nothing to reload for it");
+}
+
+run_test(\&test_dnsmasq_mapping_push);
+
+sub test_dnsmasq_dual_stack_and_sweep {
+ my $test_name = (split(/::/, (caller(0))[3]))[-1];
+ my $zoneid = "TESTZONE";
+ my $vnetid = "testvnet";
+ my $mac = "da:65:8f:18:9b:6f";
+
+ create_zone({
+ type => "simple",
+ dhcp => "dnsmasq",
+ ipam => "pve",
+ zone => $zoneid,
+ });
+ create_vnet({
+ type => "vnet",
+ zone => $zoneid,
+ vnet => $vnetid,
+ });
+ create_subnet({
+ type => "subnet",
+ vnet => $vnetid,
+ subnet => "10.0.0.0/24",
+ gateway => "10.0.0.1",
+ 'dhcp-range' => ["start-address=10.0.0.100,end-address=10.0.0.200"],
+ });
+ create_subnet({
+ type => "subnet",
+ vnet => $vnetid,
+ subnet => "8888::/64",
+ gateway => "8888::1",
+ 'dhcp-range' => ["start-address=8888::100,end-address=8888::200"],
+ });
+
+ # a reservation of a MAC the cache does not know is swept by the next
+ # rewrite, both addresses of the guest go on one line
+ PVE::Tools::file_set_contents($TMP_ETHERS_FILE, "aa:bb:cc:dd:ee:ff,10.0.0.77\n");
+ $test_state->{dnsmasq_calls} = [];
+
+ eval { nic_start($vnetid, $mac, "999", "testhostname"); };
+ if ($@) {
+ fail("$test_name: nic_start: $@");
+ return;
+ }
+ is(
+ PVE::Tools::file_get_contents($TMP_ETHERS_FILE),
+ "$mac,10.0.0.100,[8888::100]\n",
+ "$test_name: dual-stack reservation on one line, the unknown MAC swept",
+ );
+ is(
+ scalar(grep { $_ eq 'reload' } $test_state->{dnsmasq_calls}->@*),
+ 1,
+ "$test_name: one reload for the rewrite",
+ );
+
+ # an external IPAM may spell an address in upper case, the file holds
+ # one spelling
+ $test_state->{macdb}->{macs}->{$mac}->{ip6} = '8888::AB';
+ $test_state->{dnsmasq_calls} = [];
+ PVE::Network::SDN::Dhcp::Dnsmasq->update_ip_mapping($zoneid, $mac);
+ is(
+ PVE::Tools::file_get_contents($TMP_ETHERS_FILE),
+ "$mac,10.0.0.100,[8888::ab]\n",
+ "$test_name: the reservation is written in one spelling",
+ );
+ is(
+ scalar(grep { $_ eq 'reload' } $test_state->{dnsmasq_calls}->@*),
+ 1,
+ "$test_name: one reload for the changed address",
+ );
+
+ # an entry without an address, as the old code wrote on a miss, says
+ # nothing about its MAC, so a line of it is swept
+ my $stale = 'da:65:8f:18:9b:70';
+ $test_state->{macdb}->{macs}->{$stale} = { ip4 => undef, ip6 => undef };
+ PVE::Tools::file_set_contents(
+ $TMP_ETHERS_FILE,
+ "$mac,10.0.0.100,[8888::ab]\n$stale,10.0.0.101\n",
+ );
+ PVE::Network::SDN::Dhcp::Dnsmasq->update_ip_mapping($zoneid, $mac);
+ is(
+ PVE::Tools::file_get_contents($TMP_ETHERS_FILE),
+ "$mac,10.0.0.100,[8888::ab]\n",
+ "$test_name: a line of a MAC cached without an address is swept",
+ );
+
+ # a line the old code wrote with an upper-case IPv6 address is kept
+ my $other = 'da:65:8f:18:9b:71';
+ $test_state->{macdb}->{macs}->{$other} = { ip4 => '10.0.0.102', ip6 => '8888::AB' };
+ PVE::Tools::file_set_contents(
+ $TMP_ETHERS_FILE,
+ "$mac,10.0.0.100,[8888::ab]\n$other,10.0.0.102,[8888::AB]\n",
+ );
+ $test_state->{dnsmasq_calls} = [];
+ PVE::Network::SDN::Dhcp::Dnsmasq->update_ip_mapping($zoneid, $mac);
+ is(
+ PVE::Tools::file_get_contents($TMP_ETHERS_FILE),
+ "$mac,10.0.0.100,[8888::ab]\n$other,10.0.0.102,[8888::AB]\n",
+ "$test_name: a line of another MAC spelled by the old code is kept",
+ );
+ is(
+ scalar(grep { $_ eq 'reload' } $test_state->{dnsmasq_calls}->@*),
+ 0,
+ "$test_name: without a reload",
+ );
+}
+
+run_test(\&test_dnsmasq_dual_stack_and_sweep);
+
done_testing();
--
2.47.3
next prev parent reply other threads:[~2026-09-09 10:42 UTC|newest]
Thread overview: 17+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-09 10:41 [PATCH container/docs/manager/network/proxmox{-ebpf,-perl-rs}/qemu-server v2 00/16] sdn: implement DHCP for all zones using eBPF Hannes Laimer
2026-09-09 10:41 ` [PATCH proxmox-ebpf v2 01/16] dhcp: add per-tap responder BPF program Hannes Laimer
2026-09-09 10:41 ` [PATCH proxmox-ebpf v2 02/16] dhcp: add responder subsystem Hannes Laimer
2026-09-09 10:41 ` [PATCH proxmox-perl-rs v2 03/16] pve-rs: sdn: add dhcp responder bindings Hannes Laimer
2026-09-09 10:41 ` Hannes Laimer [this message]
2026-09-09 10:41 ` [PATCH pve-network v2 05/16] sdn: ipam: do not cache negative per-MAC answers, lock the write Hannes Laimer
2026-09-09 10:41 ` [PATCH pve-network v2 06/16] sdn: subnets: add dhcp-lease-time property Hannes Laimer
2026-09-09 10:41 ` [PATCH pve-network v2 07/16] sdn: dhcp: only assert a backend's availability for zones using it Hannes Laimer
2026-09-09 10:41 ` [PATCH pve-network v2 08/16] sdn: dhcp: add ebpf plugin Hannes Laimer
2026-09-09 10:41 ` [PATCH pve-network v2 09/16] sdn: zones: attach the dhcp responder on tap plug, detach on unplug Hannes Laimer
2026-09-09 10:41 ` [PATCH pve-network v2 10/16] sdn: dhcp: apply mapping edits on the node serving the guest Hannes Laimer
2026-09-09 10:41 ` [PATCH pve-network v2 11/16] sdn: zones: offer dhcp on all zone types, keep dnsmasq simple-only Hannes Laimer
2026-09-09 10:41 ` [PATCH qemu-server v2 12/16] network: report NIC plug and unplug to SDN with the MAC Hannes Laimer
2026-09-09 10:41 ` [PATCH pve-container v2 13/16] net: report veth plug and unplug to SDN with the hwaddr Hannes Laimer
2026-09-09 10:41 ` [PATCH pve-manager v2 14/16] ui: sdn: dhcp backend selector on all zones, expose dhcp options Hannes Laimer
2026-09-09 10:41 ` [PATCH pve-manager v2 15/16] sdn: bring the dhcp backends up at boot before the guests start Hannes Laimer
2026-09-09 10:41 ` [PATCH pve-docs v2 16/16] sdn: dhcp: document the ebpf backend Hannes Laimer
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260909104144.1110031-5-h.laimer@proxmox.com \
--to=h.laimer@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.