From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 1DBAF1FF137 for ; Tue, 03 Feb 2026 17:05:26 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id E894626BDD; Tue, 3 Feb 2026 17:04:05 +0100 (CET) From: Gabriel Goller To: pve-devel@lists.proxmox.com Subject: [PATCH pve-network 06/10] sdn: adjust frr.conf.local merging to rust template types Date: Tue, 3 Feb 2026 17:01:24 +0100 Message-ID: <20260203160246.353351-18-g.goller@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260203160246.353351-1-g.goller@proxmox.com> References: <20260203160246.353351-1-g.goller@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1770134497585 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.003 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment 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: 7DOH3ZV3RKYW4YWV3NA2VXU3AXXHH67O X-Message-ID-Hash: 7DOH3ZV3RKYW4YWV3NA2VXU3AXXHH67O X-MailFrom: g.goller@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: The frr object in perl which stores the whole frr config is now also modeled in rust, so it changed a bit. Adjust the frr.conf.local merging code so that the frr.conf.local is still merged correctly. This makes use of the `custom_frr_config` properties scattered in many rust types. So if we encounter a line in frr.conf.local that we need to merge, we just throw it into this string vec and render it as-is. Co-authored-by: Stefan Hanreich Signed-off-by: Gabriel Goller --- src/PVE/Network/SDN/Controllers/EvpnPlugin.pm | 4 +- src/PVE/Network/SDN/Frr.pm | 204 +++++++++++++++--- 2 files changed, 171 insertions(+), 37 deletions(-) diff --git a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm index 3ea3ce2f033a..1f221807cb7b 100644 --- a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm +++ b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm @@ -377,10 +377,10 @@ sub generate_zone_frr_config { my $main_bgp_router = $config->{frr}->{bgp}->{vrf_router}->{'default'}; if ($main_bgp_router) { $main_bgp_router->{address_families}->{ipv4_unicast} //= {}; - push(@ {$main_bgp_router->{address_families}->{ipv4_unicast}->{import_vrf} }, $vrf); + push(@{ $main_bgp_router->{address_families}->{ipv4_unicast}->{import_vrf} }, $vrf); $main_bgp_router->{address_families}->{ipv6_unicast} //= {}; - push(@ {$main_bgp_router->{address_families}->{ipv6_unicast}->{import_vrf} }, $vrf); + push(@{ $main_bgp_router->{address_families}->{ipv6_unicast}->{import_vrf} }, $vrf); } # Redistribute connected in VRF router diff --git a/src/PVE/Network/SDN/Frr.pm b/src/PVE/Network/SDN/Frr.pm index f084ad5a578f..99002c6b65bf 100644 --- a/src/PVE/Network/SDN/Frr.pm +++ b/src/PVE/Network/SDN/Frr.pm @@ -197,7 +197,7 @@ numbers to be consecutive, starting from 1 and incrementing by 1 for each entry. sub fix_routemap_seqs { my ($frr_config) = @_; - my $routemaps = $frr_config->{'frr'}->{'bgp'}->{'routemaps'}; + my $routemaps = $frr_config->{'frr'}->{'routemaps'}; foreach my $id (sort keys %$routemaps) { my $routemap = $routemaps->{$id}; @@ -270,70 +270,204 @@ sub append_local_config { return if !$local_config; my $section = \$frr_config->{""}; - my $router = undef; + my $isis_router_name = undef; + my $bgp_router_asn = undef; + my $bgp_router_vrf = undef; my $routemap = undef; - my $routemap_config = (); - my $routemap_action = undef; + my $interface = undef; + my $vrf = undef; + my $new_block = 0; + my $new_af_block = 0; - while ($local_config =~ /^\s*(.+?)\s*$/gm) { + while ($local_config =~ /^(.+?)\s*$/gm) { my $line = $1; - $line =~ s/^\s+|\s+$//g; - - if ($line =~ m/^router (.+)$/) { - $router = $1; - $section = \$frr_config->{'frr'}->{'router'}->{$router}->{""}; + $line =~ s/\s+$//g; + + if ($line =~ m/^router isis (.+)$/) { + $isis_router_name = $1; + if (defined $frr_config->{'frr'}->{'isis'}->{'router'}->{$isis_router_name}) { + $section = + \($frr_config->{'frr'}->{'isis'}->{'router'}->{$isis_router_name} + ->{'custom_frr_config'} //= []); + } else { + $new_block = 1; + push( + $frr_config->{'frr'}->{'custom_frr_config'}->@*, + "router isis $isis_router_name", + ); + $section = \$frr_config->{'frr'}->{'custom_frr_config'}; + } + next; + } elsif ($line =~ m/^router bgp (\S+)(?: vrf (.+))?$/) { + $bgp_router_asn = $1; + $bgp_router_vrf = $2 // 'default'; + + my $config_line = + defined($2) + ? "router bgp $bgp_router_asn vrf $bgp_router_vrf" + : "router bgp $bgp_router_asn"; + + if ( + defined $frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf} + and $frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}->{'asn'} + eq $bgp_router_asn + ) { + $section = + \($frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf} + ->{'custom_frr_config'} //= []); + } else { + $new_block = 1; + push( + $frr_config->{'frr'}->{'custom_frr_config'}->@*, $config_line, + ); + $section = \$frr_config->{'frr'}->{'custom_frr_config'}; + } next; } elsif ($line =~ m/^vrf (.+)$/) { - $section = \$frr_config->{'frr'}->{'vrf'}->{$1}; + $vrf = $1; + if (defined $frr_config->{'frr'}->{'bgp'}->{'vrfs'}->{$vrf}) { + $section = \$frr_config->{'frr'}->{'bgp'}->{'vrfs'}->{$vrf}->{'custom_frr_config'}; + } else { + $new_block = 1; + push($frr_config->{'frr'}->{'custom_frr_config'}->@*, "vrf $vrf"); + $section = \$frr_config->{'frr'}->{'custom_frr_config'}; + } next; } elsif ($line =~ m/^interface (.+)$/) { - $section = \$frr_config->{'frr_interfaces'}->{$1}; + $interface = $1; + if (defined $frr_config->{'frr'}->{'isis'}->{'interfaces'}->{$interface}) { + $section = \($frr_config->{'frr'}->{'isis'}->{'interfaces'}->{$interface} + ->{'custom_frr_config'} //= []); + } else { + $new_block = 1; + push( + $frr_config->{'frr'}->{'custom_frr_config'}->@*, "interface $interface", + ); + $section = \$frr_config->{'frr'}->{'custom_frr_config'}; + } next; } elsif ($line =~ m/^bgp community-list (.+)$/) { - push(@{ $frr_config->{'frr_bgp_community_list'} }, $line); + push(@{ $frr_config->{'frr'}->{'custom_frr_config'} }, $line); next; } elsif ($line =~ m/address-family (.+)$/) { - $section = \$frr_config->{'frr'}->{'router'}->{$router}->{'address-family'}->{$1}; + # convert the address family from frr (e.g. l2vpn evpn) into the rust property (e.g. l2vpn_evpn) + my $address_family_unchanged = $1; + my $address_family = $1 =~ s/ /_/gr; + + if ( + defined $frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf} + and $frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}->{'asn'} + eq $bgp_router_asn + ) { + if ( + defined $frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf} + ->{'address_families'}->{$address_family} + ) { + $section = + \($frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf} + ->{'address_families'}->{$address_family}->{custom_frr_config} //= []); + } else { + $new_af_block = 1; + push( + $frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf} + ->{'custom_frr_config'}->@*, + " address-family $address_family_unchanged", + ); + $section = \$frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf} + ->{'custom_frr_config'}; + } + } else { + $new_af_block = 1; + push( + $frr_config->{'frr'}->{'custom_frr_config'}->@*, + " address-family $address_family_unchanged", + ); + $section = \$frr_config->{'frr'}->{'custom_frr_config'}; + } next; } elsif ($line =~ m/^route-map (.+) (permit|deny) (\d+)/) { $routemap = $1; - $routemap_config = (); - $routemap_action = $2; - $section = \$frr_config->{'frr_routemap'}->{$routemap}; + my $routemap_action = $2; + my $seq_number = $3; + if (defined $frr_config->{'frr'}->{'routemaps'}->{$routemap}) { + my $index = 0; + foreach my $single_routemap ($frr_config->{'frr'}->{'routemaps'}->{$routemap}->@*) { + if ( + $single_routemap->{'seq'} == $seq_number + && $single_routemap->{'action'} eq $routemap_action + ) { + last; + } + $index++; + } + if ($index < scalar @{ $frr_config->{'frr'}->{'routemaps'}->{$routemap} }) { + $section = \($frr_config->{'frr'}->{'routemaps'}->{$routemap}->[$index] + ->{'custom_frr_config'} //= []); + } else { + $new_block = 1; + push( + $frr_config->{'frr'}->{'custom_frr_config'}->@*, + "route-map $routemap $routemap_action $seq_number", + ); + $section = \$frr_config->{'frr'}->{'custom_frr_config'}; + } + } else { + $new_block = 1; + push( + $frr_config->{'frr'}->{'custom_frr_config'}->@*, + "route-map $routemap $routemap_action $seq_number", + ); + $section = \$frr_config->{'frr'}->{'custom_frr_config'}; + } next; } elsif ($line =~ m/^access-list (.+) seq (\d+) (.+)$/) { - $frr_config->{'frr_access_list'}->{$1}->{$2} = $3; + push($frr_config->{'frr'}->{'custom_frr_config'}->@*, $line); next; } elsif ($line =~ m/^ip prefix-list (.+) seq (\d+) (.*)$/) { - $frr_config->{'frr_prefix_list'}->{$1}->{$2} = $3; + push($frr_config->{'frr'}->{'custom_frr_config'}->@*, $line); next; } elsif ($line =~ m/^ipv6 prefix-list (.+) seq (\d+) (.*)$/) { - $frr_config->{'frr_prefix_list_v6'}->{$1}->{$2} = $3; + push($frr_config->{'frr'}->{'custom_frr_config'}->@*, $line); next; - } elsif ($line =~ m/^exit-address-family$/) { + } elsif ($line =~ m/exit-address-family$/) { + if ($new_af_block) { + push(@{$$section}, $line); + $section = \$frr_config->{'frr'}->{'bgp'}->{'custom_frr_config'}; + } else { + $section = + \($frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf} + ->{'custom_frr_config'} //= []); + } + $new_af_block = 0; next; - } elsif ($line =~ m/^exit$/) { - if ($router) { - $section = \$frr_config->{''}; - $router = undef; - } elsif ($routemap) { - push(@{$$section}, { rule => $routemap_config, action => $routemap_action }); - $section = \$frr_config->{''}; + } elsif ($line =~ m/^exit/) { + if ($bgp_router_vrf || $vrf || $interface || $routemap || $isis_router_name) { + # this means we just added a new router/vrf/interface/routemap + if ($new_block) { + push(@{$$section}, $line); + push(@{$$section}, "!"); + } + $section = \$frr_config->{'frr'}->{'custom_frr_config'}; + # we can't stack these, so exit out of all of them (technically we can have a vrf inside of a router bgp block, but we don't support that) + $isis_router_name = undef; + $bgp_router_vrf = undef; + $bgp_router_asn = undef; + $vrf = undef; + $interface = undef; $routemap = undef; - $routemap_action = undef; - $routemap_config = (); + } else { + $section = \$frr_config->{'frr'}->{'custom_frr_config'}; + push(@{$$section}, $line); + push(@{$$section}, "!"); } + $new_block = 0; next; } elsif ($line =~ m/!/) { next; } next if !$section; - if ($routemap) { - push(@{$routemap_config}, $line); - } else { - push(@{$$section}, $line); - } + push(@{$$section}, $line); } } -- 2.47.3