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 v3 4/9] sdn: write structured frr config that can be rendered using templates
Date: Thu,  5 Mar 2026 11:03:20 +0100	[thread overview]
Message-ID: <20260305100331.80741-14-g.goller@proxmox.com> (raw)
In-Reply-To: <20260305100331.80741-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                        |  10 +-
 src/PVE/Network/SDN/Controllers/BgpPlugin.pm  | 104 ++---
 src/PVE/Network/SDN/Controllers/EvpnPlugin.pm | 378 +++++++++---------
 src/PVE/Network/SDN/Controllers/IsisPlugin.pm |  28 +-
 src/PVE/Network/SDN/Fabrics.pm                |  14 +-
 src/PVE/Network/SDN/Frr.pm                    | 188 ++-------
 6 files changed, 295 insertions(+), 427 deletions(-)

diff --git a/src/PVE/Network/SDN.pm b/src/PVE/Network/SDN.pm
index c7c390e80586..037182bc5ae9 100644
--- a/src/PVE/Network/SDN.pm
+++ b/src/PVE/Network/SDN.pm
@@ -419,15 +419,15 @@ 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);
 
-    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..8891541219f6 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} = 0;
+        $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..e3091c63ac8d 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,60 @@ 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 = ();
-
-    #VTEP neighbors
-    push @controller_config, "neighbor VTEP peer-group";
-    push @controller_config, "neighbor VTEP remote-as $remoteas";
-    push @controller_config, "neighbor VTEP bfd";
+    my $bgp_router = $config->{frr}->{bgp}->{vrf_router}->{'default'} //= {};
+
+    # 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->{hard_administrative_reset} = 0;
+        $bgp_router->{graceful_restart_notification} = 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 +262,19 @@ 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;
+    $vrf_router->{hard_administrative_reset} = 0;
+    $vrf_router->{graceful_restart_notification} = 0;
+
+    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 +293,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 +441,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 6d5f43084412..06c40b5da744 100644
--- a/src/PVE/Network/SDN/Frr.pm
+++ b/src/PVE/Network/SDN/Frr.pm
@@ -187,30 +187,6 @@ sub set_daemon_status {
     return $changed;
 }
 
-=head3 to_raw_config(\%frr_config)
-
-Converts a given C<\%frr_config> to the raw config format.
-
-=cut
-
-sub to_raw_config {
-    my ($frr_config) = @_;
-
-    my $raw_config = [];
-
-    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;
-}
-
 =head3 raw_config_to_string(\@raw_config)
 
 Converts a given C<\@raw_config> to a string representing a complete frr
@@ -269,8 +245,35 @@ in-place.
 sub append_local_config {
     my ($frr_config, $local_config) = @_;
 
+    # store the generated and override routemaps here, so that we can write them
+    # at the very end. We need to do this because we need to have all the
+    # routemaps, to then sort them and set the seq for every routemap correctly.
+    my $custom_routemaps = {};
+
     $local_config = read_local_frr_config() if !$local_config;
-    return if !$local_config;
+
+    # add already generated frr routemaps (from the evpn controller) to the
+    # custom_routemaps map. by adding them here early the generated routemaps
+    # are inserted BEFORE the frr.conf.local ones.
+    for my $rm (sort keys %{ $frr_config->{'frr'}->{'routemaps'} }) {
+        push(@{ $custom_routemaps->{$rm} }, \$frr_config->{'frr'}->{'routemaps'}->{$rm});
+    }
+
+    if (!$local_config) {
+        # if we exit early because there is no frr.conf.local, we still need to
+        # adjust the routemap seqs
+        for my $rm (sort keys %{$custom_routemaps}) {
+            my $seq = 1;
+            my $entry = $custom_routemaps->{$rm};
+            for my $rm_line (@{$entry}) {
+                for my $rm_obj_entry (@{$$rm_line}) {
+                    $rm_obj_entry->{seq} = $seq;
+                    $seq++;
+                }
+            }
+        }
+        return;
+    }
 
     my $section = \$frr_config->{""};
     my $router = undef;
@@ -340,139 +343,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-03-05 10:04 UTC|newest]

Thread overview: 20+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-03-05 10:03 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v3 00/19] Generate frr config using jinja templates and rust types Gabriel Goller
2026-03-05 10:03 ` [PATCH proxmox-ve-rs v3 1/8] ve-config: firewall: cargo fmt Gabriel Goller
2026-03-05 10:03 ` [PATCH proxmox-ve-rs v3 2/8] frr: add proxmox-frr-templates package that contains templates Gabriel Goller
2026-03-05 10:03 ` [PATCH proxmox-ve-rs v3 3/8] ve-config: remove FrrConfigBuilder struct Gabriel Goller
2026-03-05 10:03 ` [PATCH proxmox-ve-rs v3 4/8] sdn-types: support variable-length NET identifier Gabriel Goller
2026-03-05 10:03 ` [PATCH proxmox-ve-rs v3 5/8] frr: add template serializer and serialize fabrics using templates Gabriel Goller
2026-03-05 10:03 ` [PATCH proxmox-ve-rs v3 6/8] frr: add isis configuration and templates Gabriel Goller
2026-03-05 10:03 ` [PATCH proxmox-ve-rs v3 7/8] frr: support custom frr configuration lines Gabriel Goller
2026-03-05 10:03 ` [PATCH proxmox-ve-rs v3 8/8] frr: add bgp support with templates and serialization Gabriel Goller
2026-03-05 10:03 ` [PATCH proxmox-perl-rs v3 1/1] sdn: add function to generate the frr config for all daemons Gabriel Goller
2026-03-05 10:03 ` [PATCH pve-network v3 1/9] tests: use Test::Differences to make test assertions Gabriel Goller
2026-03-05 10:03 ` [PATCH pve-network v3 2/9] test: add test for frr.conf.local merging Gabriel Goller
2026-03-05 10:03 ` [PATCH pve-network v3 3/9] test: bgp: add some various integration tests Gabriel Goller
2026-03-05 10:03 ` Gabriel Goller [this message]
2026-03-05 10:03 ` [PATCH pve-network v3 5/9] sdn: remove duplicate comment line '!' in frr config Gabriel Goller
2026-03-05 10:03 ` [PATCH pve-network v3 6/9] tests: rearrange some statements in the " Gabriel Goller
2026-03-05 10:03 ` [PATCH pve-network v3 7/9] sdn: adjust frr.conf.local merging to rust template types Gabriel Goller
2026-03-05 10:03 ` [PATCH pve-network v3 8/9] test: adjust frr_local_merge test for new template generation Gabriel Goller
2026-03-05 10:03 ` [PATCH pve-network v3 9/9] api: add dry-run endpoint for sdn apply to preview changes Gabriel Goller
2026-03-05 10:03 ` [PATCH pve-manager v3 1/1] sdn: add dry-run diff view for sdn apply 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=20260305100331.80741-14-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