all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Gabriel Goller <g.goller@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH pve-network v5 7/9] sdn: adjust frr.conf.local merging to rust template types
Date: Tue, 10 Mar 2026 13:06:43 +0100	[thread overview]
Message-ID: <20260310120705.150425-17-g.goller@proxmox.com> (raw)
In-Reply-To: <20260310120705.150425-1-g.goller@proxmox.com>

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.

There are a few options each block (e.g. router, interface, route-map)
can be merged. We either don't merge it at all and just throw it into
the global `custom_frr_config` vec, or we search for the router in the
generated frr_config and then add it to an internal custom_frr_config
vec. For example when we want to merge a 'router bgp 6500 vrf coolzone'
and exactly this router has already been generated by the evpn
controller, we check add the following frr.conf.local content into
`$frr_config->{'bgp'}->{'vrf_router'}->{'coolzone'}->{custom_frr_config}
` (after checking if the asn matches obviously). The same also happens
with the `address- family` block, but here we also need to check if the
outer `router bgp` block exists.

Route-maps are never merged into existing entries. Instead, both
generated and override route-maps are collected into a temporary
`custom_routemaps` map, sorted by name, and assigned new ascending
sequence numbers. Generated route-maps have their `seq` property
updated in-place; override route-maps are re-serialised with the new
sequence number and appended to `custom_frr_config`. This is done because we did
this before and we need to be backwards-compatible. Changing the sequence
numbers of a route-map is a breaking change.

We also decided to officially only support the statements which are explicitly
stated in the old `append_local_config` function. This means that (as before)
statements such as `ip route` or `ip nht` will be ignored.

Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 src/PVE/Network/SDN/Frr.pm | 221 +++++++++++++++++++++++++++++++------
 1 file changed, 186 insertions(+), 35 deletions(-)

diff --git a/src/PVE/Network/SDN/Frr.pm b/src/PVE/Network/SDN/Frr.pm
index 69b58345f0ec..96784d3c5cb9 100644
--- a/src/PVE/Network/SDN/Frr.pm
+++ b/src/PVE/Network/SDN/Frr.pm
@@ -274,70 +274,221 @@ sub append_local_config {
         return;
     }
 
-    my $section = \$frr_config->{""};
-    my $router = undef;
+    $frr_config->{'frr'}->{'custom_frr_config'} //= [];
+    my $section = \$frr_config->{''};
+    my $isis_router_name = undef;
+    my $bgp_router_asn = undef;
+    my $bgp_router_vrf = undef;
+    my $custom_router_name = 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';
+
+            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;
+
+                my $config_line =
+                    defined($2)
+                    ? "router bgp $bgp_router_asn vrf $bgp_router_vrf"
+                    : "router bgp $bgp_router_asn";
+
+                push(
+                    $frr_config->{'frr'}->{'custom_frr_config'}->@*, $config_line,
+                );
+                $section = \$frr_config->{'frr'}->{'custom_frr_config'};
+            }
+            next;
+        } elsif ($line =~ m/^router (.+)$/) {
+            $custom_router_name = $1;
+            $new_block = 1;
+            push(
+                $frr_config->{'frr'}->{'custom_frr_config'}->@*, "router $custom_router_name",
+            );
+            $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;
+            # NEVER merge the route-maps, we always just add them to the
+            # custom_routemaps map so that we can push them at the very end.
+            $new_block = 1;
+            push(
+                $custom_routemaps->{$routemap}->@*,
+                "route-map $routemap $routemap_action $seq_number",
+            );
+            $section = \$custom_routemaps->{$routemap};
             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'}->{'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->{''};
-                $routemap = undef;
-                $routemap_action = undef;
-                $routemap_config = ();
+        } elsif ($line =~ m/^exit/) {
+            # this means we just added a new router/vrf/interface/routemap
+            if ($new_block) {
+                push(@{$$section}, $line);
+                push(@{$$section}, "!");
             }
+            $section = \$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;
+            $custom_router_name = undef;
+            $vrf = undef;
+            $interface = undef;
+            $routemap = undef;
+            $new_block = 0;
             next;
         } elsif ($line =~ m/!/) {
             next;
         }
 
         next if !$section;
-        if ($routemap) {
-            push(@{$routemap_config}, $line);
-        } else {
-            push(@{$$section}, $line);
+        push(@{$$section}, $line);
+    }
+
+    # go through custom_routemaps, which holds generated and override routemaps.
+    # We need to sort by name and then give each rule in the route-map name a
+    # ascending seq number. If we have a routemap generated by the perl code, we
+    # get an object and need to change the seq property (this is rendered using
+    # the templates). If we have a override route-map (from frr.conf.local), then
+    # we just get the strings of the lines and we need to parse it again and write
+    # with the correct seq.
+    for my $rm (sort keys %{$custom_routemaps}) {
+        my $seq = 1;
+        my $entry = $custom_routemaps->{$rm};
+        for my $rm_line (@{$entry}) {
+            if (!ref($rm_line)) {
+                if ($rm_line =~ m/^route-map (.+) (permit|deny) (\d+)/) {
+                    my $name = $1;
+                    my $action = $2;
+                    push(
+                        $frr_config->{'frr'}->{'custom_frr_config'}->@*,
+                        "route-map $name $action $seq",
+                    );
+                    $seq++;
+                } else {
+                    push($frr_config->{'frr'}->{'custom_frr_config'}->@*, $rm_line);
+                }
+            } else {
+                for my $rm_obj_entry (@{$$rm_line}) {
+                    $rm_obj_entry->{seq} = $seq;
+                    $seq++;
+                }
+            }
         }
     }
 }
-- 
2.47.3





  parent reply	other threads:[~2026-03-10 12:09 UTC|newest]

Thread overview: 40+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-03-10 12:06 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v5 00/19] Generate frr config using jinja templates and rust types Gabriel Goller
2026-03-10 12:06 ` [PATCH proxmox-ve-rs v5 1/8] ve-config: firewall: cargo fmt Gabriel Goller
2026-03-10 12:06 ` [PATCH proxmox-ve-rs v5 2/8] frr: add proxmox-frr-templates package that contains templates Gabriel Goller
2026-03-12  8:01   ` Hannes Laimer
2026-03-12 10:03     ` Gabriel Goller
2026-03-12  8:19   ` Hannes Laimer
2026-03-12  9:30     ` Gabriel Goller
2026-03-12  8:33   ` Hannes Laimer
2026-03-12  9:31     ` Gabriel Goller
2026-03-10 12:06 ` [PATCH proxmox-ve-rs v5 3/8] ve-config: remove FrrConfigBuilder struct Gabriel Goller
2026-03-10 12:06 ` [PATCH proxmox-ve-rs v5 4/8] sdn-types: support variable-length NET identifier Gabriel Goller
2026-03-10 12:06 ` [PATCH proxmox-ve-rs v5 5/8] frr: add template serializer and serialize fabrics using templates Gabriel Goller
2026-03-12  8:44   ` Hannes Laimer
2026-03-12 10:07     ` Gabriel Goller
2026-03-12  9:12   ` Hannes Laimer
2026-03-12 10:11     ` Gabriel Goller
2026-03-10 12:06 ` [PATCH proxmox-ve-rs v5 6/8] frr: add isis configuration and templates Gabriel Goller
2026-03-10 12:06 ` [PATCH proxmox-ve-rs v5 7/8] frr: support custom frr configuration lines Gabriel Goller
2026-03-10 12:06 ` [PATCH proxmox-ve-rs v5 8/8] frr: add bgp support with templates and serialization Gabriel Goller
2026-03-12  8:30   ` Hannes Laimer
2026-03-12  9:35     ` Gabriel Goller
2026-03-10 12:06 ` [PATCH proxmox-perl-rs v5 1/1] sdn: add function to generate the frr config for all daemons Gabriel Goller
2026-03-10 12:06 ` [PATCH pve-network v5 1/9] tests: use Test::Differences to make test assertions Gabriel Goller
2026-03-10 12:06 ` [PATCH pve-network v5 2/9] test: add tests for frr.conf.local merging Gabriel Goller
2026-03-10 12:06 ` [PATCH pve-network v5 3/9] test: bgp: add some various integration tests Gabriel Goller
2026-03-10 12:06 ` [PATCH pve-network v5 4/9] sdn: write structured frr config that can be rendered using templates Gabriel Goller
2026-03-12  9:20   ` Hannes Laimer
2026-03-12 10:20     ` Gabriel Goller
2026-03-10 12:06 ` [PATCH pve-network v5 5/9] sdn: remove duplicate comment line '!' in frr config Gabriel Goller
2026-03-10 12:06 ` [PATCH pve-network v5 6/9] tests: rearrange some statements in the " Gabriel Goller
2026-03-10 12:06 ` Gabriel Goller [this message]
2026-03-10 12:06 ` [PATCH pve-network v5 8/9] test: adjust frr_local_merge test for new template generation Gabriel Goller
2026-03-10 12:06 ` [PATCH pve-network v5 9/9] api: add dry-run endpoint for sdn apply to preview changes Gabriel Goller
2026-03-10 12:06 ` [PATCH pve-manager v5 1/1] sdn: add dry-run diff view for sdn apply Gabriel Goller
2026-03-12  7:58   ` Hannes Laimer
2026-03-12 10:22     ` Gabriel Goller
2026-03-11 17:22 ` [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v5 00/19] Generate frr config using jinja templates and rust types Stefan Hanreich
2026-03-12 13:35 ` Hannes Laimer
2026-03-12 14:28 ` Gabriel Goller
2026-03-12 14:31 ` Gabriel Goller

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=20260310120705.150425-17-g.goller@proxmox.com \
    --to=g.goller@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.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal