Mapping edits change cluster-wide records, but the ebpf backend
answers from a per-node map, so an edit made through another node's
API left the map of the node running the guest stale until the next
apply or guest start there. Resolve the MAC to its guest and re-apply
the mapping on that node through a node-scoped call, the same
remove-and-add the editing node runs locally. No other node needs the
record, each picks it up at that guest's next start there, so the
cost of an edit stays one call however large the cluster is.

The push runs detached from the edit request, an unreachable node
cannot hold the edit up, it just catches up on its next apply or the
guest's next start.

Signed-off-by: Hannes Laimer <[email protected]>
---
 src/PVE/API2/Network/SDN/Ips.pm          |  3 ++
 src/PVE/API2/Network/SDN/Nodes/Status.pm | 42 ++++++++++++++-
 src/PVE/Network/SDN/Dhcp.pm              | 65 ++++++++++++++++++++++++
 3 files changed, 109 insertions(+), 1 deletion(-)

diff --git a/src/PVE/API2/Network/SDN/Ips.pm b/src/PVE/API2/Network/SDN/Ips.pm
index d7b682d..5b45de7 100644
--- a/src/PVE/API2/Network/SDN/Ips.pm
+++ b/src/PVE/API2/Network/SDN/Ips.pm
@@ -47,6 +47,7 @@ __PACKAGE__->register_method({
         die "$@\n" if $@;
 
         PVE::Network::SDN::Dhcp::update_mapping($vnet, $mac);
+        PVE::Network::SDN::Dhcp::notify_guest_node($vnet, $mac);
 
         return undef;
     },
@@ -85,6 +86,7 @@ __PACKAGE__->register_method({
         PVE::Network::SDN::Vnets::add_ip($vnet, $ip, '', $mac, undef);
 
         PVE::Network::SDN::Dhcp::update_mapping($vnet, $mac);
+        PVE::Network::SDN::Dhcp::notify_guest_node($vnet, $mac);
 
         return undef;
     },
@@ -138,6 +140,7 @@ __PACKAGE__->register_method({
         die "$error\n" if $error;
 
         PVE::Network::SDN::Dhcp::update_mapping($vnet, $mac);
+        PVE::Network::SDN::Dhcp::notify_guest_node($vnet, $mac);
 
         return undef;
     },
diff --git a/src/PVE/API2/Network/SDN/Nodes/Status.pm 
b/src/PVE/API2/Network/SDN/Nodes/Status.pm
index 7977e0c..7054b9d 100644
--- a/src/PVE/API2/Network/SDN/Nodes/Status.pm
+++ b/src/PVE/API2/Network/SDN/Nodes/Status.pm
@@ -9,6 +9,9 @@ use PVE::API2::Network::SDN::Nodes::Vnets;
 
 use PVE::JSONSchema qw(get_standard_option);
 
+use PVE::Network::SDN::Dhcp;
+use PVE::Network::SDN::Vnets;
+
 use PVE::RESTHandler;
 use base qw(PVE::RESTHandler);
 
@@ -27,6 +30,40 @@ __PACKAGE__->register_method({
     path => 'vnets',
 });
 
+__PACKAGE__->register_method({
+    name => 'dhcp_mapping',
+    path => 'dhcp-mapping',
+    method => 'POST',
+    description =>
+        'Re-apply the DHCP mapping of a MAC address on this node from the 
current records.',
+    permissions => {
+        check => ['perm', '/sdn/zones/{zone}/{vnet}', ['SDN.Allocate']],
+    },
+    protected => 1,
+    proxyto => 'node',
+    parameters => {
+        additionalProperties => 0,
+        properties => {
+            node => get_standard_option('pve-node'),
+            zone => get_standard_option('pve-sdn-zone-id'),
+            vnet => get_standard_option('pve-sdn-vnet-id'),
+            mac => get_standard_option('mac-addr'),
+        },
+    },
+    returns => { type => 'null' },
+    code => sub {
+        my ($param) = @_;
+
+        my $vnet = PVE::Network::SDN::Vnets::get_vnet($param->{vnet}, 1);
+        die "vnet '$param->{vnet}' does not exist in zone '$param->{zone}'\n"
+            if !$vnet || $vnet->{zone} ne $param->{zone};
+
+        PVE::Network::SDN::Dhcp::update_mapping($param->{vnet}, $param->{mac});
+
+        return undef;
+    },
+});
+
 __PACKAGE__->register_method({
     name => 'sdnindex',
     path => '',
@@ -52,7 +89,10 @@ __PACKAGE__->register_method({
         my ($param) = @_;
 
         my $result = [
-            { name => 'fabrics' }, { name => 'vnets' }, { name => 'zones' },
+            { name => 'dhcp-mapping' },
+            { name => 'fabrics' },
+            { name => 'vnets' },
+            { name => 'zones' },
         ];
         return $result;
     },
diff --git a/src/PVE/Network/SDN/Dhcp.pm b/src/PVE/Network/SDN/Dhcp.pm
index 45e9a94..413e7cd 100644
--- a/src/PVE/Network/SDN/Dhcp.pm
+++ b/src/PVE/Network/SDN/Dhcp.pm
@@ -3,6 +3,8 @@ package PVE::Network::SDN::Dhcp;
 use strict;
 use warnings;
 
+use POSIX qw();
+
 use PVE::Cluster;
 
 use PVE::Network::SDN;
@@ -80,6 +82,69 @@ sub update_mapping {
     warn "could not update dhcp mapping for $mac: $@" if $@;
 }
 
+# the guest config lines come from pmxcfs in one go, no guest config
+# gets parsed for this
+my sub guest_node_by_mac {
+    my ($mac) = @_;
+
+    my $vmlist = PVE::Cluster::get_vmlist();
+    my $nets = PVE::Cluster::get_guest_config_properties([map { "net$_" } 0 .. 
31]);
+    for my $vmid (keys %$nets) {
+        for my $net (values %{ $nets->{$vmid} }) {
+            return $vmlist->{ids}->{$vmid}->{node} if $net =~ 
m/=\Q$mac\E(?:,|$)/i;
+        }
+    }
+
+    return undef;
+}
+
+# The records are cluster-wide, but a node answers from its own map,
+# so an edit has to reach the node running the guest behind the MAC.
+# Every other node picks the record up at that guest's next start
+# there. The push runs detached from the request, an unreachable node
+# must not hold the edit up.
+sub notify_guest_node {
+    my ($vnetid, $mac) = @_;
+
+    my $vnet = PVE::Network::SDN::Vnets::get_vnet($vnetid, 1);
+    return if !$vnet;
+
+    my $zone = PVE::Network::SDN::Zones::get_zone($vnet->{zone}, 1);
+    return if !$zone || !$zone->{dhcp};
+
+    my $node = guest_node_by_mac($mac);
+    return if !$node || $node eq PVE::INotify::nodename();
+
+    # double fork, the grandchild is reparented to init so nothing on
+    # the request path ever waits on it
+    my $pid = fork();
+    if (!defined($pid)) {
+        warn "could not fork for the dhcp mapping push to $node: $!\n";
+        return;
+    }
+    if ($pid) {
+        waitpid($pid, 0);
+        return;
+    }
+
+    POSIX::setsid();
+    my $child = fork();
+    POSIX::_exit(1) if !defined($child);
+    POSIX::_exit(0) if $child;
+
+    exec(
+        'pvesh',
+        'create',
+        "/nodes/$node/sdn/dhcp-mapping",
+        '--zone',
+        $vnet->{zone},
+        '--vnet',
+        $vnetid,
+        '--mac',
+        $mac,
+    ) or POSIX::_exit(1);
+}
+
 sub tap_plug {
     my ($zoneid, $zone, $iface) = @_;
 
-- 
2.47.3




Reply via email to