From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id EF26D1FF135 for ; Thu, 02 Jul 2026 15:26:48 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 298C12141E; Thu, 02 Jul 2026 15:26:48 +0200 (CEST) Mime-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset=UTF-8 Date: Thu, 02 Jul 2026 15:26:12 +0200 Message-Id: To: "David Riley" , Subject: Re: [PATCH pve-network v4 5/5] fix #7520: config: prune orphaned ACLs and relocate moved VNets From: "Daniel Kral" X-Mailer: aerc 0.21.0-147-g43ac7b685014-dirty References: <20260626105258.56914-1-d.riley@proxmox.com> <20260626105258.56914-6-d.riley@proxmox.com> In-Reply-To: <20260626105258.56914-6-d.riley@proxmox.com> X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1782998766030 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.100 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) POISEN_SPAM_PILL 0.1 Meta: its spam POISEN_SPAM_PILL_1 0.1 random spam to be learned in bayes POISEN_SPAM_PILL_3 0.1 random spam to be learned in bayes SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: WN5LMWDSEF2YIWCJ5Z52TF67ALLLS2ZE X-Message-ID-Hash: WN5LMWDSEF2YIWCJ5Z52TF67ALLLS2ZE X-MailFrom: d.kral@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: On Fri Jun 26, 2026 at 12:52 PM CEST, David Riley wrote: > Compare the running configuration with the newly compiled state during > config commit to identify and prune orphaned SDN ACL entries. This > ensures state consistency for manual applies via the UI/API as well as > during automatic configuration reloads on system boot. > > Track configuration changes across: > * Zones > * VNets > * Fabrics > * Controllers > * Route maps > * Prefix lists > > Trigger a validation check when a VNet is moved between zones to > mitigate potential ACL path conflicts. Abort the apply operation if a > conflict is detected to prevent configuration overwrites. If the path > is clear, relocate the ACLs to the updated zone path. > > Suggested-by: Stefan Hanreich > Signed-off-by: David Riley > --- > src/PVE/Network/SDN.pm | 141 +++++++++++++++++++++++++++++++++++++++++ > 1 file changed, 141 insertions(+) > > diff --git a/src/PVE/Network/SDN.pm b/src/PVE/Network/SDN.pm > index 33a3cf3..cd563d2 100644 > --- a/src/PVE/Network/SDN.pm > +++ b/src/PVE/Network/SDN.pm > @@ -10,6 +10,7 @@ use LWP::UserAgent; > use Net::SSLeay; > use UUID; > =20 > +use PVE::AccessControl; > use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_lock_file); > use PVE::INotify; > use PVE::RESTEnvironment qw(log_warn); > @@ -238,11 +239,151 @@ sub compile_running_cfg { > } > =20 > sub commit_config { > + my $old_cfg =3D cfs_read_file($RUNNING_CFG_FILENAME); > my $cfg =3D compile_running_cfg(); > =20 > + cleanup($old_cfg, $cfg); > + > cfs_write_file($RUNNING_CFG_FILENAME, $cfg); > } > =20 > +sub cleanup { maybe a more telling name like cleanup_sdn_resource_acls(...) or something similar? Otherwise, it might suggest that it cleans up something in the SDN config. > + my ($old_cfg, $cfg) =3D @_; > + if (!defined($old_cfg) || !defined($cfg)) { > + return; > + } AFAICT $old_cfg and $cfg are always defined as the cfs_read_file() parser for $RUNNING_CFG_FILENAME will always at least return an empty hash ref and compile_running_cfg() will also always return a hash ref, so this should not be needed? > + > + my $route_map_paths =3D diff_route_maps($old_cfg, $cfg); > + my $generic_paths =3D diff_generic_resources($old_cfg, $cfg); > + my ($vnet_delete_paths, $vnet_move_paths) =3D diff_vnets($old_cfg, $= cfg); > + > + if (@$vnet_move_paths) { > + PVE::AccessControl::migrate_sdn_resource_access($vnet_move_paths= ); > + } > + > + my @paths_to_delete =3D (@$vnet_delete_paths, @$route_map_paths, @$g= eneric_paths); > + if (@paths_to_delete) { > + PVE::AccessControl::remove_sdn_resource_access(\@paths_to_delete= ); > + } > + > +} > + > +sub diff_generic_resources { > + my ($old_cfg, $cfg) =3D @_; > + > + my @paths_to_delete; nit: initialize with empty array () > + > + my @types =3D qw(zones controllers fabrics prefix-lists); > + for my $type (@types) { > + if (!defined($old_cfg->{$type}) || !defined($old_cfg->{$type}->{= ids})) { > + next; > + } > + > + my $old_ids =3D $old_cfg->{$type}->{ids}; > + my $new_ids =3D {}; > + > + if (defined($cfg->{$type}) && defined($cfg->{$type}->{ids})) { > + $new_ids =3D $cfg->{$type}->{ids}; > + } As compile_running_cfg() does always set the $cfg->{$type}->{ids} this could probably be set unconditionally? > + > + for my $id (keys %$old_ids) { > + if (defined($new_ids->{$id})) { > + next; > + } nit: use post-if statement > + > + push(@paths_to_delete, [$type, $id]); > + } > + } > + > + return \@paths_to_delete; > +} > + > +sub diff_route_maps { > + my ($old_cfg, $cfg) =3D @_; > + > + my @paths_to_delete; nit: initialize with empty array () > + > + if (defined($old_cfg->{'route-maps'}) && defined($old_cfg->{'route-m= aps'}->{ids})) { > + my $old_route_maps =3D $old_cfg->{'route-maps'}->{ids}; > + my $route_map_suffix =3D qr/_[0-9]+$/; nit: suffix with _re(gex) > + > + my %active_route_maps; nit: initialize with empty hash () > + if (defined($cfg->{'route-maps'}) && defined($cfg->{'route-maps'= }->{ids})) { > + for my $id (keys %{ $cfg->{'route-maps'}->{ids} }) { nit: use postfix: keys $cfg->{'route-maps'}->{ids}->%* > + (my $base_name =3D $id) =3D~ s/$route_map_suffix//; > + $active_route_maps{$base_name} =3D 1; > + } > + } > + > + my %queued_route_maps; nit: initialize with empty hash () > + for my $id (keys %$old_route_maps) { > + if (defined($cfg->{'route-maps'}->{ids}->{$id})) { > + next; > + } nit: use post-if statement > + > + (my $base_name =3D $id) =3D~ s/$route_map_suffix//; > + > + if ($active_route_maps{$base_name}) { > + next; > + } nit: use post-if statement > + > + if ($queued_route_maps{$base_name}) { > + next; > + } nit: use post-if statement > + > + push(@paths_to_delete, ['route-maps', $base_name]); > + $queued_route_maps{$base_name} =3D 1; > + } > + } > + > + return \@paths_to_delete; > +} > + > +sub diff_vnets { > + my ($old_cfg, $cfg) =3D @_; > + > + my @paths_to_delete; > + my @paths_to_move; nit: initialize with empty arrays () > + > + if (defined($old_cfg->{vnets}) && defined($old_cfg->{vnets}->{ids}))= { > + my $old_vnets =3D $old_cfg->{vnets}->{ids}; > + my $new_vnets =3D {}; > + > + if (defined($cfg->{vnets}) && defined($cfg->{vnets}->{ids})) { > + $new_vnets =3D $cfg->{vnets}->{ids}; > + } same as above, could be set unconditionally as compile_running_cfg() always sets the $cfg->{vnets}->{ids} hash ref > + > + for my $vnetid (keys %$old_vnets) { > + my $old_zone =3D $old_vnets->{$vnetid}->{zone}; > + > + if (!defined($new_vnets->{$vnetid})) { > + if (defined($old_zone)) { > + push(@paths_to_delete, ['zones', $old_zone, $vnetid]= ); > + } else { > + log_warn( > + "SDN Cleanup: Could not find zone for deleted VN= et $vnetid, skipping ACL" > + . " cleanup"); > + } > + next; > + } > + > + my $new_zone =3D $new_vnets->{$vnetid}->{zone}; > + > + if (defined($old_zone) && defined($new_zone) && $old_zone ne= $new_zone) { > + push( > + @paths_to_move, > + { > + src_path =3D> ['zones', $old_zone, $vnetid], > + dest_path =3D> ['zones', $new_zone, $vnetid], > + }, > + ); > + } Hm, there's a lot of testing whether the zone exists... Can this actually happen besides users directly editing the sdn/vnets.cfg file and removing the vnet property? The API handler doesn't permit me to add a VNet without or with a non-existing zone (only the zone 'abc' is existing; the error message is a bit misleading, because the type is undefined here because the zone does not exist at all): # pvesh create /cluster/sdn/vnets --vnet aeaewk --zone '' --type 'vnet' create sdn vnet object failed: cannot lookup undefined type! at /usr/share/= perl5/PVE/API2/Network/SDN/Vnets.pm line 326. # pvesh create /cluster/sdn/vnets --vnet aeaewk --zone 'abc' --type 'vnet' # pvesh create /cluster/sdn/vnets --vnet aeaewk2 --zone 'def' --type 'vnet' create sdn vnet object failed: cannot lookup undefined type! at /usr/share/= perl5/PVE/API2/Network/SDN/Vnets.pm line 326. > + } > + } > + > + return (\@paths_to_delete, \@paths_to_move); > +} > + > sub has_pending_changes { > my $running_cfg =3D PVE::Network::SDN::running_config(); > =20