public inbox for pve-devel@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 04/10] sdn: write structured frr config that can be rendered using templates
Date: Tue,  3 Feb 2026 17:01:22 +0100	[thread overview]
Message-ID: <20260203160246.353351-16-g.goller@proxmox.com> (raw)
In-Reply-To: <20260203160246.353351-1-g.goller@proxmox.com>

The structured frr config can be deserialized by rust and rendered using
the templates (isis and bgp) in proxmox-frr.

Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 src/PVE/Network/SDN.pm                        |  11 +-
 src/PVE/Network/SDN/Controllers/BgpPlugin.pm  | 104 ++---
 src/PVE/Network/SDN/Controllers/EvpnPlugin.pm | 372 +++++++++---------
 src/PVE/Network/SDN/Controllers/IsisPlugin.pm |  28 +-
 src/PVE/Network/SDN/Fabrics.pm                |  14 +-
 src/PVE/Network/SDN/Frr.pm                    | 163 +-------
 6 files changed, 276 insertions(+), 416 deletions(-)

diff --git a/src/PVE/Network/SDN.pm b/src/PVE/Network/SDN.pm
index c7c390e80586..c000bed498ec 100644
--- a/src/PVE/Network/SDN.pm
+++ b/src/PVE/Network/SDN.pm
@@ -419,15 +419,16 @@ sub generate_frr_raw_config {
     $fabric_config = PVE::Network::SDN::Fabrics::config(1) if !$fabric_config;
 
     my $frr_config = {};
+
     PVE::Network::SDN::Controllers::generate_frr_config($frr_config, $running_config);
     PVE::Network::SDN::Frr::append_local_config($frr_config);
+    PVE::Network::SDN::Frr::fix_routemap_seqs($frr_config);
 
-    my $raw_config = PVE::Network::SDN::Frr::to_raw_config($frr_config);
-
-    my $fabrics_config = PVE::Network::SDN::Fabrics::generate_frr_raw_config($fabric_config);
-    push @$raw_config, @$fabrics_config;
+    my $nodename = PVE::INotify::nodename();
 
-    return $raw_config;
+    return PVE::RS::SDN::get_frr_raw_config(
+        $frr_config->{'frr'}, $fabric_config, $nodename,
+    );
 }
 
 =head3 get_frr_daemon_status(\%fabric_config)
diff --git a/src/PVE/Network/SDN/Controllers/BgpPlugin.pm b/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
index 447ebf1ba744..5651b85b64af 100644
--- a/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
+++ b/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
@@ -62,7 +62,7 @@ sub generate_frr_config {
     my @peers;
     @peers = PVE::Tools::split_list($plugin_config->{'peers'}) if $plugin_config->{'peers'};
 
-    my $asn = $plugin_config->{asn};
+    my $asn = int($plugin_config->{asn});
     my $ebgp = $plugin_config->{ebgp};
     my $ebgp_multihop = $plugin_config->{'ebgp-multihop'};
     my $loopback = $plugin_config->{loopback};
@@ -73,66 +73,80 @@ sub generate_frr_config {
     return if !$asn;
     return if $local_node ne $plugin_config->{node};
 
-    my $bgp = $config->{frr}->{router}->{"bgp $asn"} //= {};
-
     my ($ifaceip, $interface) =
         PVE::Network::SDN::Zones::Plugin::find_local_ip_interface_peers(\@peers, $loopback);
     my $routerid = PVE::Network::SDN::Controllers::Plugin::get_router_id($ifaceip, $interface);
 
-    my $remoteas = $ebgp ? "external" : $asn;
-
-    #global options
-    my @controller_config = (
-        "bgp router-id $routerid", "no bgp default ipv4-unicast", "coalesce-time 1000",
-    );
-
-    push(@{ $bgp->{""} }, @controller_config) if keys %{$bgp} == 0;
+    my $bgp_router = $config->{frr}->{bgp}->{vrf_router}->{'default'} //= {};
 
-    @controller_config = ();
-    if ($ebgp) {
-        push @controller_config, "bgp disable-ebgp-connected-route-check" if $loopback;
+    # Initialize router if not already configured
+    if (!keys %{$bgp_router}) {
+        $bgp_router->{asn} = $asn;
+        $bgp_router->{router_id} = $routerid;
+        $bgp_router->{default_ipv4_unicast} = 1;
+        $bgp_router->{coalesce_time} = 1000;
+        $bgp_router->{neighbor_groups} = [];
+        $bgp_router->{address_families} = {};
     }
 
-    push @controller_config, "bgp bestpath as-path multipath-relax" if $multipath_relax;
+    # Add BGP-specific options
+    $bgp_router->{disable_ebgp_connected_route_check} = 1 if $loopback && $ebgp;
+    $bgp_router->{bestpath_as_path_multipath_relax} = 1 if $multipath_relax;
 
-    #BGP neighbors
-    if (@peers) {
-        push @controller_config, "neighbor BGP peer-group";
-        push @controller_config, "neighbor BGP remote-as $remoteas";
-        push @controller_config, "neighbor BGP bfd";
-        push @controller_config, "neighbor BGP ebgp-multihop $ebgp_multihop"
-            if $ebgp && $ebgp_multihop;
-    }
-
-    # BGP peers
-    foreach my $address (@peers) {
-        push @controller_config, "neighbor $address peer-group BGP";
-    }
-    push(@{ $bgp->{""} }, @controller_config);
-
-    # address-family unicast
+    # Build BGP neighbor group
     if (@peers) {
+        my $neighbor_group = {
+            name => "BGP",
+            bfd => 1,
+            remote_as => $ebgp ? "external" : $asn,
+            ips => \@peers,
+            interfaces => [],
+        };
+        $neighbor_group->{ebgp_multihop} = int($ebgp_multihop) if $ebgp && $ebgp_multihop;
+
+        push @{ $bgp_router->{neighbor_groups} }, $neighbor_group;
+
+        # Configure address-family unicast
         my $ipversion = Net::IP::ip_is_ipv6($ifaceip) ? "ipv6" : "ipv4";
         my $mask = Net::IP::ip_is_ipv6($ifaceip) ? "128" : "32";
+        my $af_key = "${ipversion}_unicast";
+
+        $bgp_router->{address_families}->{$af_key} //= {
+            networks => [],
+            neighbors => [{
+                name => "BGP",
+                soft_reconfiguration_inbound => 1,
+            }],
+        };
 
-        push(@{ $bgp->{"address-family"}->{"$ipversion unicast"} }, "network $ifaceip/$mask")
+        push @{ $bgp_router->{address_families}->{$af_key}->{networks} }, "$ifaceip/$mask"
             if $loopback;
-        push(@{ $bgp->{"address-family"}->{"$ipversion unicast"} }, "neighbor BGP activate");
-        push(
-            @{ $bgp->{"address-family"}->{"$ipversion unicast"} },
-            "neighbor BGP soft-reconfiguration inbound",
-        );
     }
 
+    # Configure route-map for source IP correction with loopback
     if ($loopback) {
-        $config->{frr_prefix_list}->{loopbacks_ips}->{10} = "permit 0.0.0.0/0 le 32";
-        push(@{ $config->{frr_ip_protocol} }, "ip protocol bgp route-map correct_src");
-
-        my $routemap_config = ();
-        push @{$routemap_config}, "match ip address prefix-list loopbacks_ips";
-        push @{$routemap_config}, "set src $ifaceip";
-        my $routemap = { rule => $routemap_config, action => "permit" };
-        push(@{ $config->{frr_routemap}->{'correct_src'} }, $routemap);
+        $config->{frr}->{prefix_lists}->{loopbacks_ips} = [{
+            seq => 10,
+            action => 'permit',
+            network => '0.0.0.0/0',
+            le => 32,
+            is_ipv6 => 0,
+        }];
+
+        $config->{frr}->{protocol_routemaps}->{bgp}->{v4} = "correct_src";
+
+        my $routemap_config = {
+            protocol_type => 'ip',
+            match_type => 'address',
+            value => { list_type => 'prefixlist', list_name => 'loopbacks_ips' },
+        };
+        my $routemap = {
+            matches => [$routemap_config],
+            sets => [{ set_type => 'src', value => $ifaceip }],
+            action => "permit",
+            seq => 1,
+        };
+        push(@{ $config->{frr}->{routemaps}->{'correct_src'} }, $routemap);
     }
 
     return $config;
diff --git a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
index cc217126607f..3ea3ce2f033a 100644
--- a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
+++ b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
@@ -55,15 +55,15 @@ sub generate_frr_config {
     my $local_node = PVE::INotify::nodename();
 
     my @peers;
-    my $asn = $plugin_config->{asn};
+    my $asn = int($plugin_config->{asn});
     my $ebgp = undef;
     my $loopback = undef;
     my $autortas = undef;
     my $ifaceip = undef;
     my $routerid = undef;
 
-    my $bgprouter = find_bgp_controller($local_node, $controller_cfg);
-    my $isisrouter = find_isis_controller($local_node, $controller_cfg);
+    my $bgp_controller = find_bgp_controller($local_node, $controller_cfg);
+    my $isis_controller = find_isis_controller($local_node, $controller_cfg);
 
     if ($plugin_config->{'fabric'}) {
         my $config = PVE::Network::SDN::Fabrics::config(1);
@@ -102,10 +102,10 @@ sub generate_frr_config {
     } elsif ($plugin_config->{'peers'}) {
         @peers = PVE::Tools::split_list($plugin_config->{'peers'});
 
-        if ($bgprouter) {
-            $loopback = $bgprouter->{loopback} if $bgprouter->{loopback};
-        } elsif ($isisrouter) {
-            $loopback = $isisrouter->{loopback} if $isisrouter->{loopback};
+        if ($bgp_controller) {
+            $loopback = $bgp_controller->{loopback} if $bgp_controller->{loopback};
+        } elsif ($isis_controller) {
+            $loopback = $isis_controller->{loopback} if $isis_controller->{loopback};
         }
 
         ($ifaceip, my $interface) =
@@ -116,58 +116,58 @@ sub generate_frr_config {
         return;
     }
 
-    if ($bgprouter) {
-        $ebgp = 1 if $plugin_config->{'asn'} ne $bgprouter->{asn};
-        $asn = $bgprouter->{asn} if $bgprouter->{asn};
+    if ($bgp_controller) {
+        $ebgp = 1 if $plugin_config->{'asn'} ne $bgp_controller->{asn};
+        $asn = $bgp_controller->{asn} if $bgp_controller->{asn};
         $autortas = $plugin_config->{'asn'} if $ebgp;
     }
 
     return if !$asn || !$routerid;
-    my $bgp = $config->{frr}->{router}->{"bgp $asn"} //= {};
-
-    my $remoteas = $ebgp ? "external" : $asn;
-
-    #global options
-    my @controller_config = (
-        "bgp router-id $routerid",
-        "no bgp hard-administrative-reset",
-        "no bgp default ipv4-unicast",
-        "coalesce-time 1000",
-        "no bgp graceful-restart notification",
-    );
-
-    push(@{ $bgp->{""} }, @controller_config) if keys %{$bgp} == 0;
 
-    @controller_config = ();
+    my $bgp_router = $config->{frr}->{bgp}->{vrf_router}->{'default'} //= {};
 
-    #VTEP neighbors
-    push @controller_config, "neighbor VTEP peer-group";
-    push @controller_config, "neighbor VTEP remote-as $remoteas";
-    push @controller_config, "neighbor VTEP bfd";
+    # Initialize router if not already configured
+    if (!keys %{$bgp_router}) {
+        $bgp_router->{asn} = $asn;
+        $bgp_router->{router_id} = $routerid;
+        $bgp_router->{default_ipv4_unicast} = 0;
+        $bgp_router->{coalesce_time} = 1000;
+        $bgp_router->{neighbor_groups} = [];
+        $bgp_router->{address_families} = {};
+    }
 
-    push @controller_config, "neighbor VTEP ebgp-multihop 10" if $ebgp && $loopback;
-    push @controller_config, "neighbor VTEP update-source $loopback" if $loopback;
+    # Build VTEP neighbor group
+    my @vtep_ips = grep { $_ ne $ifaceip } @peers;
 
-    # VTEP peers
-    foreach my $address (@peers) {
-        next if $address eq $ifaceip;
-        push @controller_config, "neighbor $address peer-group VTEP";
-    }
+    my $neighbor_group = {
+        name => "VTEP",
+        bfd => 1,
+        remote_as => $ebgp ? "external" : $asn,
+        ips => \@vtep_ips,
+        interfaces => [],
+    };
+    $neighbor_group->{ebgp_multihop} = 10 if $ebgp && $loopback;
+    $neighbor_group->{update_source} = $loopback if $loopback;
+
+    push @{ $bgp_router->{neighbor_groups} }, $neighbor_group;
+
+    # Configure l2vpn evpn address family
+    $bgp_router->{address_families}->{l2vpn_evpn} //= {
+        neighbors => [{
+            name => "VTEP",
+            route_map_in => 'MAP_VTEP_IN',
+            route_map_out => 'MAP_VTEP_OUT',
+        }],
+        advertise_all_vni => 1,
+    };
 
-    push(@{ $bgp->{""} }, @controller_config);
+    $bgp_router->{address_families}->{l2vpn_evpn}->{autort_as} = $autortas if $autortas;
 
-    # address-family l2vpn
-    @controller_config = ();
-    push @controller_config, "neighbor VTEP activate";
-    push @controller_config, "neighbor VTEP route-map MAP_VTEP_IN in";
-    push @controller_config, "neighbor VTEP route-map MAP_VTEP_OUT out";
-    push @controller_config, "advertise-all-vni";
-    push @controller_config, "autort as $autortas" if $autortas;
-    push(@{ $bgp->{"address-family"}->{"l2vpn evpn"} }, @controller_config);
+    my $routemap_in = { seq => 1, action => "permit" };
+    my $routemap_out = { seq => 1, action => "permit" };
 
-    my $routemap = { rule => undef, action => "permit" };
-    push(@{ $config->{frr_routemap}->{'MAP_VTEP_IN'} }, $routemap);
-    push(@{ $config->{frr_routemap}->{'MAP_VTEP_OUT'} }, $routemap);
+    push($config->{frr}->{routemaps}->{'MAP_VTEP_IN'}->@*, $routemap_in);
+    push($config->{frr}->{routemaps}->{'MAP_VTEP_OUT'}->@*, $routemap_out);
 
     return $config;
 }
@@ -260,11 +260,17 @@ sub generate_zone_frr_config {
 
     my $is_gateway = $exitnodes->{$local_node};
 
-    # vrf
-    my @controller_config = ();
-    push @controller_config, "vni $vrfvxlan";
-    #avoid to routes between nodes through the exit nodes
-    #null routes subnets of other zones
+    # Configure VRF
+    my $vrf_router = $config->{frr}->{bgp}->{vrf_router}->{$vrf} //= {};
+    $vrf_router->{asn} = $asn;
+    $vrf_router->{router_id} = $routerid;
+
+    my $bgp_vrf = $config->{frr}->{bgp}->{vrfs}->{$vrf} //= {};
+
+    $bgp_vrf->{vni} = $vrfvxlan;
+    $bgp_vrf->{ip_routes} = [];
+
+    # Add null routes for other zones to avoid routing between nodes through exit nodes
     if ($is_gateway) {
         my $subnets = PVE::Network::SDN::Vnets::get_subnets();
         my $cidrs = {};
@@ -283,162 +289,135 @@ sub generate_zone_frr_config {
             keys $cidrs->%*;
 
         foreach my $ip (@sorted_ip) {
-            my $ipversion = Net::IP::ip_is_ipv4($ip) ? 'ip' : 'ipv6';
-            push @controller_config, "$ipversion route $ip/$cidrs->{$ip} null0";
+            my $is_ipv6 = Net::IP::ip_is_ipv6($ip);
+            push @{ $bgp_vrf->{ip_routes} },
+                {
+                    is_ipv6 => $is_ipv6,
+                    prefix => "$ip/$cidrs->{$ip}",
+                    via => "null0",
+                };
         }
     }
 
-    push(@{ $config->{frr}->{vrf}->{"$vrf"} }, @controller_config);
-
-    #main vrf router
-    @controller_config = ();
-    push @controller_config, "bgp router-id $routerid";
-    push @controller_config, "no bgp hard-administrative-reset";
-    push @controller_config, "no bgp graceful-restart notification";
-
-    #    push @controller_config, "!";
-    push(@{ $config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{""} }, @controller_config);
+    # Configure VRF BGP router
+    $vrf_router->{neighbor_groups} = [];
+    $vrf_router->{address_families} = {};
 
+    # Configure L2VPN EVPN address family with route targets
     if ($autortas) {
-        push(
-            @{
-                $config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}
-                    ->{"l2vpn evpn"}
-            },
-            "route-target import $autortas:$vrfvxlan",
-        );
-        push(
-            @{
-                $config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}
-                    ->{"l2vpn evpn"}
-            },
-            "route-target export $autortas:$vrfvxlan",
-        );
+        $vrf_router->{address_families}->{l2vpn_evpn} //= {};
+        $vrf_router->{address_families}->{l2vpn_evpn}->{route_targets} = {
+            import => ["$autortas:$vrfvxlan"],
+            export => ["$autortas:$vrfvxlan"],
+        };
     }
 
     if ($is_gateway) {
-
-        $config->{frr_prefix_list}->{'only_default'}->{1} = "permit 0.0.0.0/0";
-        $config->{frr_prefix_list_v6}->{'only_default_v6'}->{1} = "permit ::/0";
+        push(
+            @{ $config->{frr}->{prefix_lists}->{only_default} },
+            { seq => 1, action => 'permit', network => '0.0.0.0/0', is_ipv6 => 0 },
+        ) if !defined($config->{frr}->{prefix_lists}->{only_default});
+        push(
+            @{ $config->{frr}->{prefix_lists}->{only_default_v6} },
+            { seq => 1, action => 'permit', network => '::/0', is_ipv6 => 1 },
+        ) if !defined($config->{frr}->{prefix_lists}->{only_default_v6});
 
         if (!$exitnodes_primary || $exitnodes_primary eq $local_node) {
-            #filter default route coming from other exit nodes on primary node or both nodes if no primary is defined.
-            my $routemap_config_v6 = ();
-            push @{$routemap_config_v6}, "match ipv6 address prefix-list only_default_v6";
-            my $routemap_v6 = { rule => $routemap_config_v6, action => "deny" };
-            unshift(@{ $config->{frr_routemap}->{'MAP_VTEP_IN'} }, $routemap_v6);
+            # Filter default route coming from other exit nodes on primary node
+            my $routemap_config_v6 = {
+                protocol_type => 'ipv6',
+                match_type => 'address',
+                value => { list_type => 'prefixlist', list_name => 'only_default_v6' },
+            };
+            my $routemap_v6 = { seq => 1, matches => [$routemap_config_v6], action => "deny" };
+            unshift(
+                @{ $config->{frr}->{routemaps}->{'MAP_VTEP_IN'} }, $routemap_v6,
+            );
 
-            my $routemap_config = ();
-            push @{$routemap_config}, "match ip address prefix-list only_default";
-            my $routemap = { rule => $routemap_config, action => "deny" };
-            unshift(@{ $config->{frr_routemap}->{'MAP_VTEP_IN'} }, $routemap);
+            my $routemap_config = {
+                protocol_type => 'ip',
+                match_type => 'address',
+                value => { list_type => 'prefixlist', list_name => 'only_default' },
+            };
+            my $routemap = { seq => 1, matches => [$routemap_config], action => "deny" };
+            unshift(@{ $config->{frr}->{routemaps}->{'MAP_VTEP_IN'} }, $routemap);
 
         } elsif ($exitnodes_primary ne $local_node) {
-            my $routemap_config_v6 = ();
-            push @{$routemap_config_v6}, "match ipv6 address prefix-list only_default_v6";
-            push @{$routemap_config_v6}, "set metric 200";
-            my $routemap_v6 = { rule => $routemap_config_v6, action => "permit" };
-            unshift(@{ $config->{frr_routemap}->{'MAP_VTEP_OUT'} }, $routemap_v6);
-
-            my $routemap_config = ();
-            push @{$routemap_config}, "match ip address prefix-list only_default";
-            push @{$routemap_config}, "set metric 200";
-            my $routemap = { rule => $routemap_config, action => "permit" };
-            unshift(@{ $config->{frr_routemap}->{'MAP_VTEP_OUT'} }, $routemap);
+            my $routemap_config_v6 = {
+                protocol_type => 'ipv6',
+                match_type => 'address',
+                value => { list_type => 'prefixlist', list_name => 'only_default_v6' },
+            };
+            my $routemap_v6 = {
+                seq => 1,
+                matches => [$routemap_config_v6],
+                sets => [{ set_type => 'metric', value => 200 }],
+                action => "permit",
+            };
+            unshift(
+                @{ $config->{frr}->{routemaps}->{'MAP_VTEP_OUT'} }, $routemap_v6,
+            );
+
+            my $routemap_config = {
+                protocol_type => 'ip',
+                match_type => 'address',
+                value => { list_type => 'prefixlist', list_name => 'only_default' },
+            };
+            my $routemap = {
+                seq => 1,
+                matches => [$routemap_config],
+                sets => [{ set_type => 'metric', value => 200 }],
+                action => "permit",
+            };
+            unshift(@{ $config->{frr}->{routemaps}->{'MAP_VTEP_OUT'} }, $routemap);
         }
 
         if (!$exitnodes_local_routing) {
-            @controller_config = ();
-            #import /32 routes of evpn network from vrf1 to default vrf (for packet return)
-            push @controller_config, "import vrf $vrf";
-            push(
-                @{
-                    $config->{frr}->{router}->{"bgp $asn"}->{"address-family"}->{"ipv4 unicast"}
-                },
-                @controller_config,
-            );
-            push(
-                @{
-                    $config->{frr}->{router}->{"bgp $asn"}->{"address-family"}->{"ipv6 unicast"}
-                },
-                @controller_config,
-            );
-
-            @controller_config = ();
-            #redistribute connected to be able to route to local vms on the gateway
-            push @controller_config, "redistribute connected";
-            push(
-                @{
-                    $config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}
-                        ->{"ipv4 unicast"}
-                },
-                @controller_config,
-            );
-            push(
-                @{
-                    $config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}
-                        ->{"ipv6 unicast"}
-                },
-                @controller_config,
-            );
+            # Import /32 routes from VRF to main router
+            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);
+
+                $main_bgp_router->{address_families}->{ipv6_unicast} //= {};
+                push(@ {$main_bgp_router->{address_families}->{ipv6_unicast}->{import_vrf} }, $vrf);
+            }
+
+            # Redistribute connected in VRF router
+            $vrf_router->{address_families}->{ipv4_unicast} //= { redistribute => [] };
+            push @{ $vrf_router->{address_families}->{ipv4_unicast}->{redistribute} },
+                { protocol => "connected" };
+
+            $vrf_router->{address_families}->{ipv6_unicast} //= { redistribute => [] };
+            push @{ $vrf_router->{address_families}->{ipv6_unicast}->{redistribute} },
+                { protocol => "connected" };
         }
 
-        @controller_config = ();
-        #add default originate to announce 0.0.0.0/0 type5 route in evpn
-        push @controller_config, "default-originate ipv4";
-        push @controller_config, "default-originate ipv6";
-        push(
-            @{
-                $config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}
-                    ->{"l2vpn evpn"}
-            },
-            @controller_config,
-        );
-    } elsif ($advertisesubnets) {
+        # Add default originate to announce 0.0.0.0/0 type5 route in evpn
+        $vrf_router->{address_families}->{l2vpn_evpn} //= {};
+        $vrf_router->{address_families}->{l2vpn_evpn}->{default_originate} = ["ipv4", "ipv6"];
 
-        @controller_config = ();
-        #redistribute connected networks
-        push @controller_config, "redistribute connected";
-        push(
-            @{
-                $config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}
-                    ->{"ipv4 unicast"}
-            },
-            @controller_config,
-        );
-        push(
-            @{
-                $config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}
-                    ->{"ipv6 unicast"}
-            },
-            @controller_config,
-        );
-
-        @controller_config = ();
-        #advertise connected networks type5 route in evpn
-        push @controller_config, "advertise ipv4 unicast";
-        push @controller_config, "advertise ipv6 unicast";
-        push(
-            @{
-                $config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}
-                    ->{"l2vpn evpn"}
-            },
-            @controller_config,
-        );
+    } elsif ($advertisesubnets) {
+        # Redistribute connected networks
+        $vrf_router->{address_families}->{ipv4_unicast} //= { redistribute => [] };
+        push @{ $vrf_router->{address_families}->{ipv4_unicast}->{redistribute} },
+            { protocol => "connected" };
+
+        $vrf_router->{address_families}->{ipv6_unicast} //= { redistribute => [] };
+        push @{ $vrf_router->{address_families}->{ipv6_unicast}->{redistribute} },
+            { protocol => "connected" };
+
+        # Advertise connected networks type5 route in evpn
+        $vrf_router->{address_families}->{l2vpn_evpn} //= {};
+        $vrf_router->{address_families}->{l2vpn_evpn}->{advertise_ipv4_unicast} = 1;
+        $vrf_router->{address_families}->{l2vpn_evpn}->{advertise_ipv6_unicast} = 1;
     }
 
     if ($rt_import) {
-        @controller_config = ();
-        foreach my $rt (sort @{$rt_import}) {
-            push @controller_config, "route-target import $rt";
-        }
-        push(
-            @{
-                $config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}
-                    ->{"l2vpn evpn"}
-            },
-            @controller_config,
-        );
+        $vrf_router->{address_families}->{l2vpn_evpn} //= { route_targets => {} };
+        $vrf_router->{address_families}->{l2vpn_evpn}->{route_targets}->{import} //= [];
+        push @{ $vrf_router->{address_families}->{l2vpn_evpn}->{route_targets}->{import} },
+            @{$rt_import};
     }
 
     return $config;
@@ -458,18 +437,29 @@ sub generate_vnet_frr_config {
     return if !$is_gateway;
 
     my $subnets = PVE::Network::SDN::Vnets::get_subnets($vnetid, 1);
-    my @controller_config = ();
+    $config->{frr}->{ip_routes} //= [];
     foreach my $subnetid (sort keys %{$subnets}) {
         my $subnet = $subnets->{$subnetid};
         my $cidr = $subnet->{cidr};
         my ($ip) = split(/\//, $cidr, 2);
         if (Net::IP::ip_is_ipv6($ip)) {
-            push @controller_config, "ipv6 route $cidr fe80::2 xvrf_$zoneid";
+            push @{ $config->{frr}->{ip_routes} },
+                {
+                    prefix => $cidr,
+                    via => "fe80::2",
+                    vrf => "xvrf_$zoneid",
+                    is_ipv6 => 1,
+                };
         } else {
-            push @controller_config, "ip route $cidr 10.255.255.2 xvrf_$zoneid";
+            push @{ $config->{frr}->{ip_routes} },
+                {
+                    prefix => $cidr,
+                    via => "10.255.255.2",
+                    vrf => "xvrf_$zoneid",
+                    is_ipv6 => 0,
+                };
         }
     }
-    push(@{ $config->{frr_ip_protocol} }, @controller_config);
 }
 
 sub on_delete_hook {
diff --git a/src/PVE/Network/SDN/Controllers/IsisPlugin.pm b/src/PVE/Network/SDN/Controllers/IsisPlugin.pm
index 3a9acfda0744..454bdda6d316 100644
--- a/src/PVE/Network/SDN/Controllers/IsisPlugin.pm
+++ b/src/PVE/Network/SDN/Controllers/IsisPlugin.pm
@@ -69,23 +69,27 @@ sub generate_frr_config {
     return if !$isis_ifaces || !$isis_net || !$isis_domain;
     return if $local_node ne $plugin_config->{node};
 
-    my @router_config = (
-        "net $isis_net",
-        "redistribute ipv4 connected level-1",
-        "redistribute ipv6 connected level-1",
-        "log-adjacency-changes",
-    );
-
-    push(@{ $config->{frr}->{router}->{"isis $isis_domain"} }, @router_config);
-
-    my @iface_config = ("ip router isis $isis_domain");
+    # Configure IS-IS router
+    my $isis_router = $config->{frr}->{isis}->{router}->{$isis_domain} //= {};
+
+    $isis_router->{net} = $isis_net;
+    $isis_router->{log_adjacency_changes} = 1;
+    $isis_router->{redistribute} = {
+        ipv4_connected => "level-1",
+        ipv6_connected => "level-1",
+    };
 
+    # Configure interfaces
     my $altnames = PVE::Network::altname_mapping();
-
     my @ifaces = PVE::Tools::split_list($isis_ifaces);
+
+    $config->{frr}->{isis}->{interfaces} //= {};
     for my $iface (sort @ifaces) {
         my $iface_name = $altnames->{$iface} // $iface;
-        push(@{ $config->{frr_interfaces}->{$iface_name} }, @iface_config);
+        $config->{frr}->{isis}->{interfaces}->{$iface_name} //= {};
+        $config->{frr}->{isis}->{interfaces}->{$iface_name}->{domain} = $isis_domain;
+        $config->{frr}->{isis}->{interfaces}->{$iface_name}->{is_ipv4} = 1;
+        $config->{frr}->{isis}->{interfaces}->{$iface_name}->{is_ipv6} = 0;
     }
 
     return $config;
diff --git a/src/PVE/Network/SDN/Fabrics.pm b/src/PVE/Network/SDN/Fabrics.pm
index d90992a7eceb..3ca362a02660 100644
--- a/src/PVE/Network/SDN/Fabrics.pm
+++ b/src/PVE/Network/SDN/Fabrics.pm
@@ -6,6 +6,7 @@ use warnings;
 use PVE::Cluster qw(cfs_register_file cfs_read_file cfs_lock_file cfs_write_file);
 use PVE::JSONSchema qw(get_standard_option);
 use PVE::INotify;
+use PVE::RS::SDN;
 use PVE::RS::SDN::Fabrics;
 
 PVE::JSONSchema::register_format(
@@ -100,19 +101,6 @@ sub get_frr_daemon_status {
     return $daemon_status;
 }
 
-sub generate_frr_raw_config {
-    my ($fabric_config) = @_;
-
-    my @raw_config = ();
-
-    my $nodename = PVE::INotify::nodename();
-
-    my $frr_config = $fabric_config->get_frr_raw_config($nodename);
-    push @raw_config, @$frr_config if @$frr_config;
-
-    return \@raw_config;
-}
-
 sub generate_etc_network_config {
     my $nodename = PVE::INotify::nodename();
     my $fabric_config = PVE::Network::SDN::Fabrics::config(1);
diff --git a/src/PVE/Network/SDN/Frr.pm b/src/PVE/Network/SDN/Frr.pm
index 9f81369c079b..f084ad5a578f 100644
--- a/src/PVE/Network/SDN/Frr.pm
+++ b/src/PVE/Network/SDN/Frr.pm
@@ -187,28 +187,26 @@ sub set_daemon_status {
     return $changed;
 }
 
-=head3 to_raw_config(\%frr_config)
+=head3 fix_routemap_seqs(\$frr_config)
 
-Converts a given C<\%frr_config> to the raw config format.
+Iterates over all bgp route-maps in C<\$frr_config> and renumbers their sequence
+numbers to be consecutive, starting from 1 and incrementing by 1 for each entry.
 
 =cut
 
-sub to_raw_config {
+sub fix_routemap_seqs {
     my ($frr_config) = @_;
 
-    my $raw_config = [];
+    my $routemaps = $frr_config->{'frr'}->{'bgp'}->{'routemaps'};
 
-    generate_frr_vrf($raw_config, $frr_config->{frr}->{vrf});
-    generate_frr_interfaces($raw_config, $frr_config->{frr_interfaces});
-    generate_frr_recurse($raw_config, $frr_config->{frr}, undef, 0);
-    generate_frr_list($raw_config, $frr_config->{frr_access_list}, "access-list");
-    generate_frr_list($raw_config, $frr_config->{frr_prefix_list}, "ip prefix-list");
-    generate_frr_list($raw_config, $frr_config->{frr_prefix_list_v6}, "ipv6 prefix-list");
-    generate_frr_simple_list($raw_config, $frr_config->{frr_bgp_community_list});
-    generate_frr_routemap($raw_config, $frr_config->{frr_routemap});
-    generate_frr_simple_list($raw_config, $frr_config->{frr_ip_protocol});
-
-    return $raw_config;
+    foreach my $id (sort keys %$routemaps) {
+        my $routemap = $routemaps->{$id};
+        my $order = 0;
+        foreach my $seq (@$routemap) {
+            $order++;
+            $seq->{seq} = $order;
+        }
+    }
 }
 
 =head3 raw_config_to_string(\@raw_config)
@@ -339,139 +337,4 @@ sub append_local_config {
     }
 }
 
-sub generate_frr_recurse {
-    my ($final_config, $content, $parentkey, $level) = @_;
-
-    my $keylist = {};
-    $keylist->{'address-family'} = 1;
-    $keylist->{router} = 1;
-
-    my $exitkeylist = {};
-    $exitkeylist->{'address-family'} = 1;
-
-    my $simple_exitkeylist = {};
-    $simple_exitkeylist->{router} = 1;
-
-    # FIXME: make this generic
-    my $paddinglevel = undef;
-    if ($level == 1 || $level == 2) {
-        $paddinglevel = $level - 1;
-    } elsif ($level == 3 || $level == 4) {
-        $paddinglevel = $level - 2;
-    }
-
-    my $padding = "";
-    $padding = ' ' x ($paddinglevel) if $paddinglevel;
-
-    if (ref $content eq 'HASH') {
-        foreach my $key (sort keys %$content) {
-            next if $key eq 'vrf';
-            if ($parentkey && defined($keylist->{$parentkey})) {
-                push @{$final_config}, $padding . "!";
-                push @{$final_config}, $padding . "$parentkey $key";
-            } elsif ($key ne '' && !defined($keylist->{$key})) {
-                push @{$final_config}, $padding . "$key";
-            }
-
-            my $option = $content->{$key};
-            generate_frr_recurse($final_config, $option, $key, $level + 1);
-
-            push @{$final_config}, $padding . "exit-$parentkey"
-                if $parentkey && defined($exitkeylist->{$parentkey});
-            push @{$final_config}, $padding . "exit"
-                if $parentkey && defined($simple_exitkeylist->{$parentkey});
-        }
-    }
-
-    if (ref $content eq 'ARRAY') {
-        push @{$final_config}, map { $padding . "$_" } @$content;
-    }
-}
-
-sub generate_frr_vrf {
-    my ($final_config, $vrfs) = @_;
-
-    return if !$vrfs;
-
-    my @config = ();
-
-    foreach my $id (sort keys %$vrfs) {
-        my $vrf = $vrfs->{$id};
-        push @config, "!";
-        push @config, "vrf $id";
-        foreach my $rule (@$vrf) {
-            push @config, " $rule";
-
-        }
-        push @config, "exit-vrf";
-    }
-
-    push @{$final_config}, @config;
-}
-
-sub generate_frr_simple_list {
-    my ($final_config, $rules) = @_;
-
-    return if !$rules;
-
-    my @config = ();
-    push @{$final_config}, "!";
-    foreach my $rule (sort @$rules) {
-        push @{$final_config}, $rule;
-    }
-}
-
-sub generate_frr_list {
-    my ($final_config, $lists, $type) = @_;
-
-    my $config = [];
-
-    for my $id (sort keys %$lists) {
-        my $list = $lists->{$id};
-
-        for my $seq (sort keys %$list) {
-            my $rule = $list->{$seq};
-            push @$config, "$type $id seq $seq $rule";
-        }
-    }
-
-    if (@$config > 0) {
-        push @{$final_config}, "!", @$config;
-    }
-}
-
-sub generate_frr_interfaces {
-    my ($final_config, $interfaces) = @_;
-
-    foreach my $k (sort keys %$interfaces) {
-        my $iface = $interfaces->{$k};
-        push @{$final_config}, "!";
-        push @{$final_config}, "interface $k";
-        foreach my $rule (sort @$iface) {
-            push @{$final_config}, " $rule";
-        }
-    }
-}
-
-sub generate_frr_routemap {
-    my ($final_config, $routemaps) = @_;
-
-    foreach my $id (sort keys %$routemaps) {
-
-        my $routemap = $routemaps->{$id};
-        my $order = 0;
-        foreach my $seq (@$routemap) {
-            $order++;
-            next if !defined($seq->{action});
-            my @config = ();
-            push @config, "!";
-            push @config, "route-map $id $seq->{action} $order";
-            my $rule = $seq->{rule};
-            push @config, map { " $_" } @$rule;
-            push @{$final_config}, @config;
-            push @{$final_config}, "exit";
-        }
-    }
-}
-
 1;
-- 
2.47.3





  parent reply	other threads:[~2026-02-03 16:04 UTC|newest]

Thread overview: 24+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-02-03 16:01 [PATCH docs/manager/network/proxmox{-ve-rs,-perl-rs} 00/23] Generate frr config using jinja templates and rust types Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 1/9] ve-config: firewall: cargo fmt Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 2/9] frr: add proxmox-frr-templates package that contains templates Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 3/9] ve-config: remove FrrConfigBuilder struct Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 4/9] sdn-types: support variable-length NET identifier Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 5/9] frr: add template serializer and serialize fabrics using templates Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 6/9] frr: add isis configuration and templates Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 7/9] frr: support custom frr configuration lines Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 8/9] frr: add bgp support with templates and serialization Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 9/9] frr: store frr template content as a const map Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-perl-rs 1/2] sdn: add function to generate the frr config for all daemons Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-perl-rs 2/2] sdn: add method to get a frr template Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 01/10] sdn: remove duplicate comment line '!' in frr config Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 02/10] sdn: tests: add missing comment " Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 03/10] tests: use Test::Differences to make test assertions Gabriel Goller
2026-02-03 16:01 ` Gabriel Goller [this message]
2026-02-03 16:01 ` [PATCH pve-network 05/10] tests: rearrange some statements in the frr config Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 06/10] sdn: adjust frr.conf.local merging to rust template types Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 07/10] cli: add pvesdn cli tool for managing frr template overrides Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 08/10] debian: handle user modifications to FRR templates via ucf Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 09/10] api: add dry-run endpoint for sdn apply to preview changes Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 10/10] test: add test for frr.conf.local merging Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-manager 1/1] sdn: add dry-run view for sdn apply Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-docs 1/1] docs: add man page for the `pvesdn` cli 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=20260203160246.353351-16-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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal