From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 4B97C1FF137 for ; Tue, 03 Feb 2026 17:04:41 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id CB1DF25D83; Tue, 3 Feb 2026 17:03:39 +0100 (CET) From: Gabriel Goller 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 Message-ID: <20260203160246.353351-16-g.goller@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260203160246.353351-1-g.goller@proxmox.com> References: <20260203160246.353351-1-g.goller@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1770134497362 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.003 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: 36EPZICHULWP62UGN4QGDLOS7SIBYPCD X-Message-ID-Hash: 36EPZICHULWP62UGN4QGDLOS7SIBYPCD X-MailFrom: g.goller@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: 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 Signed-off-by: Gabriel Goller --- 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