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
next prev 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