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 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.