* [pve-devel] [PATCH network] sdn: factor out frr config generation and writing
@ 2025-02-05 16:13 Gabriel Goller
2025-02-11 20:30 ` Thomas Lamprecht
2025-02-12 11:17 ` Stefan Hanreich
0 siblings, 2 replies; 5+ messages in thread
From: Gabriel Goller @ 2025-02-05 16:13 UTC (permalink / raw)
To: pve-devel
Previously the frr config generation and writing was only done in the
evpn plugin. This means that it was not possible to create a standalone
bgp and isis plugin without an evpn plugin in place. (The config would
just never be written.) To fix this, factor out the frr generation and
writing into a separate module and check if a frr-type-plugin is being
used. This also paves the way for the fabrics, which would get the
config from rust and then use this frr helper.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
src/PVE/Network/SDN/Controllers.pm | 47 ++-
src/PVE/Network/SDN/Controllers/BgpPlugin.pm | 18 +-
src/PVE/Network/SDN/Controllers/EvpnPlugin.pm | 289 +----------------
src/PVE/Network/SDN/Controllers/Frr.pm | 296 ++++++++++++++++++
src/PVE/Network/SDN/Controllers/IsisPlugin.pm | 18 +-
src/PVE/Network/SDN/Controllers/Makefile | 2 +-
src/PVE/Network/SDN/Zones/EvpnPlugin.pm | 15 +
7 files changed, 383 insertions(+), 302 deletions(-)
create mode 100644 src/PVE/Network/SDN/Controllers/Frr.pm
diff --git a/src/PVE/Network/SDN/Controllers.pm b/src/PVE/Network/SDN/Controllers.pm
index fd7ad54ac38c..43f154b7338e 100644
--- a/src/PVE/Network/SDN/Controllers.pm
+++ b/src/PVE/Network/SDN/Controllers.pm
@@ -12,6 +12,7 @@ use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_lock_file);
use PVE::Network::SDN::Vnets;
use PVE::Network::SDN::Zones;
+use PVE::Network::SDN::Controllers::Frr;
use PVE::Network::SDN::Controllers::EvpnPlugin;
use PVE::Network::SDN::Controllers::BgpPlugin;
use PVE::Network::SDN::Controllers::IsisPlugin;
@@ -148,10 +149,22 @@ sub reload_controller {
return if !$controller_cfg;
+ my $frr_reload = 0;
+
foreach my $id (keys %{$controller_cfg->{ids}}) {
my $plugin_config = $controller_cfg->{ids}->{$id};
- my $plugin = PVE::Network::SDN::Controllers::Plugin->lookup($plugin_config->{type});
- $plugin->reload_controller();
+ my $type = $plugin_config->{type};
+ my @frr_types = ("bgp", "isis", "evpn");
+ if (grep {$type} @frr_types) {
+ $frr_reload = 1;
+ } else {
+ my $plugin = PVE::Network::SDN::Controllers::Plugin->lookup($type);
+ $plugin->reload_controller();
+ }
+ }
+
+ if ($frr_reload) {
+ PVE::Network::SDN::Controllers::Frr::reload_controller();
}
}
@@ -161,12 +174,22 @@ sub generate_controller_rawconfig {
my $cfg = PVE::Network::SDN::running_config();
my $controller_cfg = $cfg->{controllers};
return if !$controller_cfg;
+ my $frr_generate = 0;
my $rawconfig = "";
foreach my $id (keys %{$controller_cfg->{ids}}) {
my $plugin_config = $controller_cfg->{ids}->{$id};
- my $plugin = PVE::Network::SDN::Controllers::Plugin->lookup($plugin_config->{type});
- $rawconfig .= $plugin->generate_controller_rawconfig($plugin_config, $config);
+ my $type = $plugin_config->{type};
+ my @frr_types = ("bgp", "isis", "evpn");
+ if (grep {$type} @frr_types) {
+ $frr_generate = 1;
+ } else {
+ my $plugin = PVE::Network::SDN::Controllers::Plugin->lookup($type);
+ $rawconfig .= $plugin->generate_controller_rawconfig($plugin_config, $config);
+ }
+ }
+ if ($frr_generate) {
+ $rawconfig .= PVE::Network::SDN::Controllers::Frr::generate_controller_rawconfig($config);
}
return $rawconfig;
}
@@ -178,10 +201,22 @@ sub write_controller_config {
my $controller_cfg = $cfg->{controllers};
return if !$controller_cfg;
+ my $frr_reload = 0;
+
foreach my $id (keys %{$controller_cfg->{ids}}) {
my $plugin_config = $controller_cfg->{ids}->{$id};
- my $plugin = PVE::Network::SDN::Controllers::Plugin->lookup($plugin_config->{type});
- $plugin->write_controller_config($plugin_config, $config);
+ my $type = $plugin_config->{type};
+ my @frr_types = ("bgp", "isis", "evpn");
+ if (grep {$type} @frr_types) {
+ $frr_reload = 1;
+ } else {
+ my $plugin = PVE::Network::SDN::Controllers::Plugin->lookup($type);
+ $plugin->write_controller_config($plugin_config, $config);
+ }
+ }
+
+ if ($frr_reload) {
+ PVE::Network::SDN::Controllers::Frr::write_controller_config($config);
}
}
diff --git a/src/PVE/Network/SDN/Controllers/BgpPlugin.pm b/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
index 53963e5ad7f4..a4d3e9990647 100644
--- a/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
+++ b/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
@@ -7,6 +7,7 @@ use PVE::INotify;
use PVE::JSONSchema qw(get_standard_option);
use PVE::Tools qw(run_command file_set_contents file_get_contents);
+use PVE::Network::SDN::Controllers::Frr;
use PVE::Network::SDN::Controllers::Plugin;
use PVE::Network::SDN::Zones::Plugin;
use Net::IP;
@@ -164,19 +165,22 @@ sub on_update_hook {
}
}
+sub reload_controller {
+ my ($class) = @_;
+ #return PVE::Network::SDN::Controllers::Frr::reload_controller($class);
+ die "implemented in the Frr helper";
+}
+
sub generate_controller_rawconfig {
my ($class, $plugin_config, $config) = @_;
- return "";
+ #return PVE::Network::SDN::Controllers::Frr::generate_controller_rawconfig($class, $plugin_config, $config);
+ die "implemented in the Frr helper";
}
sub write_controller_config {
my ($class, $plugin_config, $config) = @_;
- return;
-}
-
-sub reload_controller {
- my ($class) = @_;
- return;
+ #return PVE::Network::SDN::Controllers::Frr::write_controller_config($class, $plugin_config, $config);
+ die "implemented in the Frr helper";
}
1;
diff --git a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
index c245ea29cf90..6f875cb5dbf9 100644
--- a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
+++ b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
@@ -9,6 +9,7 @@ use PVE::Tools qw(run_command file_set_contents file_get_contents);
use PVE::RESTEnvironment qw(log_warn);
use PVE::Network::SDN::Controllers::Plugin;
+use PVE::Network::SDN::Controllers::Frr;
use PVE::Network::SDN::Zones::Plugin;
use Net::IP;
@@ -109,6 +110,7 @@ sub generate_controller_config {
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";
+ # https://datatracker.ietf.org/doc/html/rfc8365#section-5.1.2.1
push @controller_config, "autort as $autortas" if $autortas;
push(@{$bgp->{"address-family"}->{"l2vpn evpn"}}, @controller_config);
@@ -195,7 +197,7 @@ sub generate_controller_zone_config {
push @controller_config, "no bgp hard-administrative-reset";
push @controller_config, "no bgp graceful-restart notification";
-# push @controller_config, "!";
+ #push @controller_config, "!";
push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{""}}, @controller_config);
if ($autortas) {
@@ -204,7 +206,6 @@ sub generate_controller_zone_config {
}
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";
@@ -356,291 +357,17 @@ sub find_isis_controller {
return $res;
}
-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_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";
- }
- }
-}
-
-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 read_local_frr_config {
- if (-e "/etc/frr/frr.conf.local") {
- return file_get_contents("/etc/frr/frr.conf.local");
- }
-};
-
sub generate_controller_rawconfig {
my ($class, $plugin_config, $config) = @_;
-
- my $nodename = PVE::INotify::nodename();
-
- my $final_config = [];
- push @{$final_config}, "frr version 8.5.2";
- push @{$final_config}, "frr defaults datacenter";
- push @{$final_config}, "hostname $nodename";
- push @{$final_config}, "log syslog informational";
- push @{$final_config}, "service integrated-vtysh-config";
- push @{$final_config}, "!";
-
- my $local_conf = read_local_frr_config();
- if ($local_conf) {
- parse_merge_frr_local_config($config, $local_conf);
- }
-
- generate_frr_vrf($final_config, $config->{frr}->{vrf});
- generate_frr_interfaces($final_config, $config->{frr_interfaces});
- generate_frr_recurse($final_config, $config->{frr}, undef, 0);
- generate_frr_list($final_config, $config->{frr_access_list}, "access-list");
- generate_frr_list($final_config, $config->{frr_prefix_list}, "ip prefix-list");
- generate_frr_list($final_config, $config->{frr_prefix_list_v6}, "ipv6 prefix-list");
- generate_frr_simple_list($final_config, $config->{frr_bgp_community_list});
- generate_frr_routemap($final_config, $config->{frr_routemap});
- generate_frr_simple_list($final_config, $config->{frr_ip_protocol});
-
- push @{$final_config}, "!";
- push @{$final_config}, "line vty";
- push @{$final_config}, "!";
-
- my $rawconfig = join("\n", @{$final_config});
-
- return if !$rawconfig;
- return $rawconfig;
-}
-
-sub parse_merge_frr_local_config {
- my ($config, $local_conf) = @_;
-
- my $section = \$config->{""};
- my $router = undef;
- my $routemap = undef;
- my $routemap_config = ();
- my $routemap_action = undef;
-
- while ($local_conf =~ /^\s*(.+?)\s*$/gm) {
- my $line = $1;
- $line =~ s/^\s+|\s+$//g;
-
- if ($line =~ m/^router (.+)$/) {
- $router = $1;
- $section = \$config->{'frr'}->{'router'}->{$router}->{""};
- next;
- } elsif ($line =~ m/^vrf (.+)$/) {
- $section = \$config->{'frr'}->{'vrf'}->{$1};
- next;
- } elsif ($line =~ m/^interface (.+)$/) {
- $section = \$config->{'frr_interfaces'}->{$1};
- next;
- } elsif ($line =~ m/^bgp community-list (.+)$/) {
- push(@{$config->{'frr_bgp_community_list'}}, $line);
- next;
- } elsif ($line =~ m/address-family (.+)$/) {
- $section = \$config->{'frr'}->{'router'}->{$router}->{'address-family'}->{$1};
- next;
- } elsif ($line =~ m/^route-map (.+) (permit|deny) (\d+)/) {
- $routemap = $1;
- $routemap_config = ();
- $routemap_action = $2;
- $section = \$config->{'frr_routemap'}->{$routemap};
- next;
- } elsif ($line =~ m/^access-list (.+) seq (\d+) (.+)$/) {
- $config->{'frr_access_list'}->{$1}->{$2} = $3;
- next;
- } elsif ($line =~ m/^ip prefix-list (.+) seq (\d+) (.*)$/) {
- $config->{'frr_prefix_list'}->{$1}->{$2} = $3;
- next;
- } elsif ($line =~ m/^ipv6 prefix-list (.+) seq (\d+) (.*)$/) {
- $config->{'frr_prefix_list_v6'}->{$1}->{$2} = $3;
- next;
- } elsif($line =~ m/^exit-address-family$/) {
- next;
- } elsif($line =~ m/^exit$/) {
- if($router) {
- $section = \$config->{''};
- $router = undef;
- } elsif($routemap) {
- push(@{$$section}, { rule => $routemap_config, action => $routemap_action });
- $section = \$config->{''};
- $routemap = undef;
- $routemap_action = undef;
- $routemap_config = ();
- }
- next;
- } elsif($line =~ m/!/) {
- next;
- }
-
- next if !$section;
- if($routemap) {
- push(@{$routemap_config}, $line);
- } else {
- push(@{$$section}, $line);
- }
- }
+ #return PVE::Network::SDN::Controllers::Frr::generate_controller_rawconfig($class, $plugin_config, $config);
+ die "implemented in the Frr helper";
}
sub write_controller_config {
my ($class, $plugin_config, $config) = @_;
-
- my $rawconfig = $class->generate_controller_rawconfig($plugin_config, $config);
- return if !$rawconfig;
- return if !-d "/etc/frr";
-
- file_set_contents("/etc/frr/frr.conf", $rawconfig);
-}
-
-sub reload_controller {
- my ($class) = @_;
-
- my $conf_file = "/etc/frr/frr.conf";
- my $bin_path = "/usr/lib/frr/frr-reload.py";
-
- if (!-e $bin_path) {
- log_warn("missing $bin_path. Please install frr-pythontools package");
- return;
- }
-
- my $err = sub {
- my $line = shift;
- if ($line =~ /ERROR:/) {
- warn "$line \n";
- }
- };
-
- if (-e $conf_file && -e $bin_path) {
- eval {
- run_command([$bin_path, '--stdout', '--reload', $conf_file], outfunc => {}, errfunc => $err);
- };
- if ($@) {
- warn "frr reload command fail. Restarting frr.";
- eval { run_command(['systemctl', 'restart', 'frr']); };
- }
- }
+
+ #return PVE::Network::SDN::Controllers::Frr::write_controller_config($class, $plugin_config, $config);
+ die "implemented in the Frr helper";
}
1;
diff --git a/src/PVE/Network/SDN/Controllers/Frr.pm b/src/PVE/Network/SDN/Controllers/Frr.pm
new file mode 100644
index 000000000000..386dcae543e8
--- /dev/null
+++ b/src/PVE/Network/SDN/Controllers/Frr.pm
@@ -0,0 +1,296 @@
+package PVE::Network::SDN::Controllers::Frr;
+
+use strict;
+use warnings;
+
+use PVE::RESTEnvironment qw(log_warn);
+use PVE::Tools qw(file_get_contents file_set_contents);
+
+sub read_local_frr_config {
+ if (-e "/etc/frr/frr.conf.local") {
+ return file_get_contents("/etc/frr/frr.conf.local");
+ }
+};
+
+sub reload_controller {
+ my $conf_file = "/etc/frr/frr.conf";
+ my $bin_path = "/usr/lib/frr/frr-reload.py";
+
+ if (!-e $bin_path) {
+ log_warn("missing $bin_path. Please install frr-pythontools package");
+ return;
+ }
+
+ my $err = sub {
+ my $line = shift;
+ if ($line =~ /ERROR:/) {
+ warn "$line \n";
+ }
+ };
+
+ if (-e $conf_file && -e $bin_path) {
+ eval {
+ run_command([$bin_path, '--stdout', '--reload', $conf_file], outfunc => {}, errfunc => $err);
+ };
+ if ($@) {
+ warn "frr reload command fail. Restarting frr.";
+ eval { run_command(['systemctl', 'restart', 'frr']); };
+ }
+ }
+}
+
+sub generate_controller_rawconfig {
+ my ($config) = @_;
+
+ my $nodename = PVE::INotify::nodename();
+
+ my $final_config = [];
+ push @{$final_config}, "frr version 8.5.2";
+ push @{$final_config}, "frr defaults datacenter";
+ push @{$final_config}, "hostname $nodename";
+ push @{$final_config}, "log syslog informational";
+ push @{$final_config}, "service integrated-vtysh-config";
+ push @{$final_config}, "!";
+
+ my $local_conf = read_local_frr_config();
+ if ($local_conf) {
+ parse_merge_frr_local_config($config, $local_conf);
+ }
+
+ generate_frr_vrf($final_config, $config->{frr}->{vrf});
+ generate_frr_interfaces($final_config, $config->{frr_interfaces});
+ generate_frr_recurse($final_config, $config->{frr}, undef, 0);
+ generate_frr_list($final_config, $config->{frr_access_list}, "access-list");
+ generate_frr_list($final_config, $config->{frr_prefix_list}, "ip prefix-list");
+ generate_frr_list($final_config, $config->{frr_prefix_list_v6}, "ipv6 prefix-list");
+ generate_frr_simple_list($final_config, $config->{frr_bgp_community_list});
+ generate_frr_routemap($final_config, $config->{frr_routemap});
+ generate_frr_simple_list($final_config, $config->{frr_ip_protocol});
+
+ push @{$final_config}, "!";
+ push @{$final_config}, "line vty";
+ push @{$final_config}, "!";
+
+ my $rawconfig = join("\n", @{$final_config});
+
+ return if !$rawconfig;
+ return $rawconfig;
+}
+
+sub parse_merge_frr_local_config {
+ my ($config, $local_conf) = @_;
+
+ my $section = \$config->{""};
+ my $router = undef;
+ my $routemap = undef;
+ my $routemap_config = ();
+ my $routemap_action = undef;
+
+ while ($local_conf =~ /^\s*(.+?)\s*$/gm) {
+ my $line = $1;
+ $line =~ s/^\s+|\s+$//g;
+
+ if ($line =~ m/^router (.+)$/) {
+ $router = $1;
+ $section = \$config->{'frr'}->{'router'}->{$router}->{""};
+ next;
+ } elsif ($line =~ m/^vrf (.+)$/) {
+ $section = \$config->{'frr'}->{'vrf'}->{$1};
+ next;
+ } elsif ($line =~ m/^interface (.+)$/) {
+ $section = \$config->{'frr_interfaces'}->{$1};
+ next;
+ } elsif ($line =~ m/^bgp community-list (.+)$/) {
+ push(@{$config->{'frr_bgp_community_list'}}, $line);
+ next;
+ } elsif ($line =~ m/address-family (.+)$/) {
+ $section = \$config->{'frr'}->{'router'}->{$router}->{'address-family'}->{$1};
+ next;
+ } elsif ($line =~ m/^route-map (.+) (permit|deny) (\d+)/) {
+ $routemap = $1;
+ $routemap_config = ();
+ $routemap_action = $2;
+ $section = \$config->{'frr_routemap'}->{$routemap};
+ next;
+ } elsif ($line =~ m/^access-list (.+) seq (\d+) (.+)$/) {
+ $config->{'frr_access_list'}->{$1}->{$2} = $3;
+ next;
+ } elsif ($line =~ m/^ip prefix-list (.+) seq (\d+) (.*)$/) {
+ $config->{'frr_prefix_list'}->{$1}->{$2} = $3;
+ next;
+ } elsif ($line =~ m/^ipv6 prefix-list (.+) seq (\d+) (.*)$/) {
+ $config->{'frr_prefix_list_v6'}->{$1}->{$2} = $3;
+ next;
+ } elsif($line =~ m/^exit-address-family$/) {
+ next;
+ } elsif($line =~ m/^exit$/) {
+ if($router) {
+ $section = \$config->{''};
+ $router = undef;
+ } elsif($routemap) {
+ push(@{$$section}, { rule => $routemap_config, action => $routemap_action });
+ $section = \$config->{''};
+ $routemap = undef;
+ $routemap_action = undef;
+ $routemap_config = ();
+ }
+ next;
+ } elsif($line =~ m/!/) {
+ next;
+ }
+
+ next if !$section;
+ if($routemap) {
+ push(@{$routemap_config}, $line);
+ } else {
+ push(@{$$section}, $line);
+ }
+ }
+}
+
+sub write_controller_config {
+ my ($config) = @_;
+
+ my $rawconfig = generate_controller_rawconfig($config);
+ return if !$rawconfig;
+ return if !-d "/etc/frr";
+
+ file_set_contents("/etc/frr/frr.conf", $rawconfig);
+}
+
+
+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;
diff --git a/src/PVE/Network/SDN/Controllers/IsisPlugin.pm b/src/PVE/Network/SDN/Controllers/IsisPlugin.pm
index 97c6876db303..50a11742fff6 100644
--- a/src/PVE/Network/SDN/Controllers/IsisPlugin.pm
+++ b/src/PVE/Network/SDN/Controllers/IsisPlugin.pm
@@ -7,6 +7,7 @@ use PVE::INotify;
use PVE::JSONSchema qw(get_standard_option);
use PVE::Tools qw(run_command file_set_contents file_get_contents);
+use PVE::Network::SDN::Controllers::Frr;
use PVE::Network::SDN::Controllers::Plugin;
use PVE::Network::SDN::Zones::Plugin;
use Net::IP;
@@ -113,19 +114,22 @@ sub on_update_hook {
}
}
+sub reload_controller {
+ my ($class) = @_;
+ #return PVE::Network::SDN::Controllers::Frr::reload_controller($class);
+ die "implemented in the Frr helper";
+}
+
sub generate_controller_rawconfig {
my ($class, $plugin_config, $config) = @_;
- return "";
+ #return PVE::Network::SDN::Controllers::Frr::generate_controller_rawconfig($class, $plugin_config, $config);
+ die "implemented in the Frr helper";
}
sub write_controller_config {
my ($class, $plugin_config, $config) = @_;
- return;
-}
-
-sub reload_controller {
- my ($class) = @_;
- return;
+ #return PVE::Network::SDN::Controllers::Frr::write_controller_config($class, $plugin_config, $config);
+ die "implemented in the Frr helper";
}
1;
diff --git a/src/PVE/Network/SDN/Controllers/Makefile b/src/PVE/Network/SDN/Controllers/Makefile
index fd9f881a0ad2..3b0387913cdc 100644
--- a/src/PVE/Network/SDN/Controllers/Makefile
+++ b/src/PVE/Network/SDN/Controllers/Makefile
@@ -1,4 +1,4 @@
-SOURCES=Plugin.pm FaucetPlugin.pm EvpnPlugin.pm BgpPlugin.pm IsisPlugin.pm
+SOURCES=Plugin.pm FaucetPlugin.pm EvpnPlugin.pm BgpPlugin.pm IsisPlugin.pm Frr.pm
PERL5DIR=${DESTDIR}/usr/share/perl5
diff --git a/src/PVE/Network/SDN/Zones/EvpnPlugin.pm b/src/PVE/Network/SDN/Zones/EvpnPlugin.pm
index 4843756a75bd..6212ba0a02d9 100644
--- a/src/PVE/Network/SDN/Zones/EvpnPlugin.pm
+++ b/src/PVE/Network/SDN/Zones/EvpnPlugin.pm
@@ -323,6 +323,21 @@ sub vnet_update_hook {
}
}
+sub reload_controller {
+ my ($class) = @_;
+ die "implemented in the Frr helper";
+}
+
+sub generate_controller_rawconfig {
+ my ($class, $config) = @_;
+ die "implemented in the Frr helper";
+}
+
+sub write_controller_config {
+ my ($class, $config) = @_;
+ die "implemented in the Frr helper";
+}
+
1;
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [pve-devel] [PATCH network] sdn: factor out frr config generation and writing
2025-02-05 16:13 [pve-devel] [PATCH network] sdn: factor out frr config generation and writing Gabriel Goller
@ 2025-02-11 20:30 ` Thomas Lamprecht
2025-02-12 8:45 ` Stefan Hanreich
2025-02-12 11:17 ` Stefan Hanreich
1 sibling, 1 reply; 5+ messages in thread
From: Thomas Lamprecht @ 2025-02-11 20:30 UTC (permalink / raw)
To: Proxmox VE development discussion, Gabriel Goller
Am 05.02.25 um 17:13 schrieb Gabriel Goller:
> Previously the frr config generation and writing was only done in the
> evpn plugin. This means that it was not possible to create a standalone
> bgp and isis plugin without an evpn plugin in place. (The config would
> just never be written.) To fix this, factor out the frr generation and
> writing into a separate module and check if a frr-type-plugin is being
> used. This also paves the way for the fabrics, which would get the
> config from rust and then use this frr helper.
>
> Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
> ---
> src/PVE/Network/SDN/Controllers.pm | 47 ++-
> src/PVE/Network/SDN/Controllers/BgpPlugin.pm | 18 +-
> src/PVE/Network/SDN/Controllers/EvpnPlugin.pm | 289 +----------------
> src/PVE/Network/SDN/Controllers/Frr.pm | 296 ++++++++++++++++++
> src/PVE/Network/SDN/Controllers/IsisPlugin.pm | 18 +-
> src/PVE/Network/SDN/Controllers/Makefile | 2 +-
> src/PVE/Network/SDN/Zones/EvpnPlugin.pm | 15 +
> 7 files changed, 383 insertions(+), 302 deletions(-)
> create mode 100644 src/PVE/Network/SDN/Controllers/Frr.pm
>
> diff --git a/src/PVE/Network/SDN/Controllers.pm b/src/PVE/Network/SDN/Controllers.pm
> index fd7ad54ac38c..43f154b7338e 100644
> --- a/src/PVE/Network/SDN/Controllers.pm
> +++ b/src/PVE/Network/SDN/Controllers.pm
> @@ -12,6 +12,7 @@ use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_lock_file);
> use PVE::Network::SDN::Vnets;
> use PVE::Network::SDN::Zones;
>
> +use PVE::Network::SDN::Controllers::Frr;
> use PVE::Network::SDN::Controllers::EvpnPlugin;
> use PVE::Network::SDN::Controllers::BgpPlugin;
> use PVE::Network::SDN::Controllers::IsisPlugin;
> @@ -148,10 +149,22 @@ sub reload_controller {
>
> return if !$controller_cfg;
>
> + my $frr_reload = 0;
> +
> foreach my $id (keys %{$controller_cfg->{ids}}) {
> my $plugin_config = $controller_cfg->{ids}->{$id};
> - my $plugin = PVE::Network::SDN::Controllers::Plugin->lookup($plugin_config->{type});
> - $plugin->reload_controller();
> + my $type = $plugin_config->{type};
> + my @frr_types = ("bgp", "isis", "evpn");
instead of having those re-defined three times as array in the same
module you could use a module wide hash, that would make checking
easier too, like:
my $frr_types = { bgp => 1, isis => 1, evpn => 1 };
Looks OK otherwise, albeit I mostly skimmed it a few days ago and had
this lying around as draft, somebody else (@Stefan) checking would not
hurt.
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [pve-devel] [PATCH network] sdn: factor out frr config generation and writing
2025-02-11 20:30 ` Thomas Lamprecht
@ 2025-02-12 8:45 ` Stefan Hanreich
0 siblings, 0 replies; 5+ messages in thread
From: Stefan Hanreich @ 2025-02-12 8:45 UTC (permalink / raw)
To: Thomas Lamprecht, Proxmox VE development discussion, Gabriel Goller
On 2/11/25 21:30, Thomas Lamprecht wrote:
> Looks OK otherwise, albeit I mostly skimmed it a few days ago and had
> this lying around as draft, somebody else (@Stefan) checking would not
> hurt.
wanted to look into it, will do so today
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [pve-devel] [PATCH network] sdn: factor out frr config generation and writing
2025-02-05 16:13 [pve-devel] [PATCH network] sdn: factor out frr config generation and writing Gabriel Goller
2025-02-11 20:30 ` Thomas Lamprecht
@ 2025-02-12 11:17 ` Stefan Hanreich
2025-02-13 10:09 ` Gabriel Goller
1 sibling, 1 reply; 5+ messages in thread
From: Stefan Hanreich @ 2025-02-12 11:17 UTC (permalink / raw)
To: Proxmox VE development discussion, Gabriel Goller
This still has some issues (see below), maybe we can look at it together
next week (will be gone after today) and see if we can make some
additional structural improvements to the whole controller / frr logic?
There were also some regressions/bugs with FRR config generation while
testing, so this series needs some work still.
On 2/5/25 17:13, Gabriel Goller wrote:
> Previously the frr config generation and writing was only done in the
> evpn plugin. This means that it was not possible to create a standalone
> bgp and isis plugin without an evpn plugin in place. (The config would
> just never be written.) To fix this, factor out the frr generation and
> writing into a separate module and check if a frr-type-plugin is being
> used. This also paves the way for the fabrics, which would get the
> config from rust and then use this frr helper.
>
> Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
> ---
> src/PVE/Network/SDN/Controllers.pm | 47 ++-
> src/PVE/Network/SDN/Controllers/BgpPlugin.pm | 18 +-
> src/PVE/Network/SDN/Controllers/EvpnPlugin.pm | 289 +----------------
> src/PVE/Network/SDN/Controllers/Frr.pm | 296 ++++++++++++++++++
> src/PVE/Network/SDN/Controllers/IsisPlugin.pm | 18 +-
> src/PVE/Network/SDN/Controllers/Makefile | 2 +-
> src/PVE/Network/SDN/Zones/EvpnPlugin.pm | 15 +
> 7 files changed, 383 insertions(+), 302 deletions(-)
> create mode 100644 src/PVE/Network/SDN/Controllers/Frr.pm
>
> diff --git a/src/PVE/Network/SDN/Controllers.pm b/src/PVE/Network/SDN/Controllers.pm
> index fd7ad54ac38c..43f154b7338e 100644
> --- a/src/PVE/Network/SDN/Controllers.pm
> +++ b/src/PVE/Network/SDN/Controllers.pm
> @@ -12,6 +12,7 @@ use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_lock_file);
> use PVE::Network::SDN::Vnets;
> use PVE::Network::SDN::Zones;
>
> +use PVE::Network::SDN::Controllers::Frr;
> use PVE::Network::SDN::Controllers::EvpnPlugin;
> use PVE::Network::SDN::Controllers::BgpPlugin;
> use PVE::Network::SDN::Controllers::IsisPlugin;
> @@ -148,10 +149,22 @@ sub reload_controller {
>
> return if !$controller_cfg;
>
> + my $frr_reload = 0;
> +
> foreach my $id (keys %{$controller_cfg->{ids}}) {
> my $plugin_config = $controller_cfg->{ids}->{$id};
> - my $plugin = PVE::Network::SDN::Controllers::Plugin->lookup($plugin_config->{type});
> - $plugin->reload_controller();
> + my $type = $plugin_config->{type};
> + my @frr_types = ("bgp", "isis", "evpn");
> + if (grep {$type} @frr_types) {
this doesn't work. you need:
grep {$_ eq $type} @frr_types
(or what thomas said is even better - just for future reference)
> + $frr_reload = 1;
> + } else {
> + my $plugin = PVE::Network::SDN::Controllers::Plugin->lookup($type);
> + $plugin->reload_controller();
> + }
> + }
> +
> + if ($frr_reload) {
> + PVE::Network::SDN::Controllers::Frr::reload_controller();
> }
> }
>
> @@ -161,12 +174,22 @@ sub generate_controller_rawconfig {
This actually never gets called (except in the tests, ..). It works, but
still takes the old code path without going through the new FRR helper.
Probably not the intention?
> my $cfg = PVE::Network::SDN::running_config();
> my $controller_cfg = $cfg->{controllers};
> return if !$controller_cfg;
> + my $frr_generate = 0;
>
> my $rawconfig = "";
> foreach my $id (keys %{$controller_cfg->{ids}}) {
> my $plugin_config = $controller_cfg->{ids}->{$id};
> - my $plugin = PVE::Network::SDN::Controllers::Plugin->lookup($plugin_config->{type});
> - $rawconfig .= $plugin->generate_controller_rawconfig($plugin_config, $config);
> + my $type = $plugin_config->{type};
> + my @frr_types = ("bgp", "isis", "evpn");
> + if (grep {$type} @frr_types) {
grep issue again
> + $frr_generate = 1;
> + } else {
> + my $plugin = PVE::Network::SDN::Controllers::Plugin->lookup($type);
> + $rawconfig .= $plugin->generate_controller_rawconfig($plugin_config, $config);
> + }
> + }
> + if ($frr_generate) {
> + $rawconfig .= PVE::Network::SDN::Controllers::Frr::generate_controller_rawconfig($config);
> }
> return $rawconfig;
> }
> @@ -178,10 +201,22 @@ sub write_controller_config {
> my $controller_cfg = $cfg->{controllers};
> return if !$controller_cfg;
>
> + my $frr_reload = 0;
> +
> foreach my $id (keys %{$controller_cfg->{ids}}) {
> my $plugin_config = $controller_cfg->{ids}->{$id};
> - my $plugin = PVE::Network::SDN::Controllers::Plugin->lookup($plugin_config->{type});
> - $plugin->write_controller_config($plugin_config, $config);
> + my $type = $plugin_config->{type};
> + my @frr_types = ("bgp", "isis", "evpn");
> + if (grep {$type} @frr_types) {
grep issue again
> + $frr_reload = 1;
> + } else {
> + my $plugin = PVE::Network::SDN::Controllers::Plugin->lookup($type);
> + $plugin->write_controller_config($plugin_config, $config);
> + }
> + }
> +
> + if ($frr_reload) {
> + PVE::Network::SDN::Controllers::Frr::write_controller_config($config);
> }
> }
>
> diff --git a/src/PVE/Network/SDN/Controllers/BgpPlugin.pm b/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
> index 53963e5ad7f4..a4d3e9990647 100644
> --- a/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
> +++ b/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
> @@ -7,6 +7,7 @@ use PVE::INotify;
> use PVE::JSONSchema qw(get_standard_option);
> use PVE::Tools qw(run_command file_set_contents file_get_contents);
>
> +use PVE::Network::SDN::Controllers::Frr;
> use PVE::Network::SDN::Controllers::Plugin;
> use PVE::Network::SDN::Zones::Plugin;
> use Net::IP;
> @@ -164,19 +165,22 @@ sub on_update_hook {
> }
> }
>
> +sub reload_controller {
> + my ($class) = @_;
> + #return PVE::Network::SDN::Controllers::Frr::reload_controller($class);
> + die "implemented in the Frr helper";
> +}
> +
> sub generate_controller_rawconfig {
> my ($class, $plugin_config, $config) = @_;
> - return "";
> + #return PVE::Network::SDN::Controllers::Frr::generate_controller_rawconfig($class, $plugin_config, $config);
> + die "implemented in the Frr helper";
> }
>
> sub write_controller_config {
> my ($class, $plugin_config, $config) = @_;
> - return;
> -}
> -
> -sub reload_controller {
> - my ($class) = @_;
> - return;
> + #return PVE::Network::SDN::Controllers::Frr::write_controller_config($class, $plugin_config, $config);
> + die "implemented in the Frr helper";
> }
>
> 1;
> diff --git a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
> index c245ea29cf90..6f875cb5dbf9 100644
> --- a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
> +++ b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
> @@ -9,6 +9,7 @@ use PVE::Tools qw(run_command file_set_contents file_get_contents);
> use PVE::RESTEnvironment qw(log_warn);
>
> use PVE::Network::SDN::Controllers::Plugin;
> +use PVE::Network::SDN::Controllers::Frr;
> use PVE::Network::SDN::Zones::Plugin;
> use Net::IP;
>
> @@ -109,6 +110,7 @@ sub generate_controller_config {
> 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";
> + # https://datatracker.ietf.org/doc/html/rfc8365#section-5.1.2.1
> push @controller_config, "autort as $autortas" if $autortas;
> push(@{$bgp->{"address-family"}->{"l2vpn evpn"}}, @controller_config);
>
> @@ -195,7 +197,7 @@ sub generate_controller_zone_config {
> push @controller_config, "no bgp hard-administrative-reset";
> push @controller_config, "no bgp graceful-restart notification";
>
> -# push @controller_config, "!";
> + #push @controller_config, "!";
> push(@{$config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{""}}, @controller_config);
>
> if ($autortas) {
> @@ -204,7 +206,6 @@ sub generate_controller_zone_config {
> }
>
> 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";
>
> @@ -356,291 +357,17 @@ sub find_isis_controller {
> return $res;
> }
>
> -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_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";
> - }
> - }
> -}
> -
> -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 read_local_frr_config {
> - if (-e "/etc/frr/frr.conf.local") {
> - return file_get_contents("/etc/frr/frr.conf.local");
> - }
> -};
> -
> sub generate_controller_rawconfig {
> my ($class, $plugin_config, $config) = @_;
> -
> - my $nodename = PVE::INotify::nodename();
> -
> - my $final_config = [];
> - push @{$final_config}, "frr version 8.5.2";
> - push @{$final_config}, "frr defaults datacenter";
> - push @{$final_config}, "hostname $nodename";
> - push @{$final_config}, "log syslog informational";
> - push @{$final_config}, "service integrated-vtysh-config";
> - push @{$final_config}, "!";
> -
> - my $local_conf = read_local_frr_config();
> - if ($local_conf) {
> - parse_merge_frr_local_config($config, $local_conf);
> - }
> -
> - generate_frr_vrf($final_config, $config->{frr}->{vrf});
> - generate_frr_interfaces($final_config, $config->{frr_interfaces});
> - generate_frr_recurse($final_config, $config->{frr}, undef, 0);
> - generate_frr_list($final_config, $config->{frr_access_list}, "access-list");
> - generate_frr_list($final_config, $config->{frr_prefix_list}, "ip prefix-list");
> - generate_frr_list($final_config, $config->{frr_prefix_list_v6}, "ipv6 prefix-list");
> - generate_frr_simple_list($final_config, $config->{frr_bgp_community_list});
> - generate_frr_routemap($final_config, $config->{frr_routemap});
> - generate_frr_simple_list($final_config, $config->{frr_ip_protocol});
> -
> - push @{$final_config}, "!";
> - push @{$final_config}, "line vty";
> - push @{$final_config}, "!";
> -
> - my $rawconfig = join("\n", @{$final_config});
> -
> - return if !$rawconfig;
> - return $rawconfig;
> -}
> -
> -sub parse_merge_frr_local_config {
> - my ($config, $local_conf) = @_;
> -
> - my $section = \$config->{""};
> - my $router = undef;
> - my $routemap = undef;
> - my $routemap_config = ();
> - my $routemap_action = undef;
> -
> - while ($local_conf =~ /^\s*(.+?)\s*$/gm) {
> - my $line = $1;
> - $line =~ s/^\s+|\s+$//g;
> -
> - if ($line =~ m/^router (.+)$/) {
> - $router = $1;
> - $section = \$config->{'frr'}->{'router'}->{$router}->{""};
> - next;
> - } elsif ($line =~ m/^vrf (.+)$/) {
> - $section = \$config->{'frr'}->{'vrf'}->{$1};
> - next;
> - } elsif ($line =~ m/^interface (.+)$/) {
> - $section = \$config->{'frr_interfaces'}->{$1};
> - next;
> - } elsif ($line =~ m/^bgp community-list (.+)$/) {
> - push(@{$config->{'frr_bgp_community_list'}}, $line);
> - next;
> - } elsif ($line =~ m/address-family (.+)$/) {
> - $section = \$config->{'frr'}->{'router'}->{$router}->{'address-family'}->{$1};
> - next;
> - } elsif ($line =~ m/^route-map (.+) (permit|deny) (\d+)/) {
> - $routemap = $1;
> - $routemap_config = ();
> - $routemap_action = $2;
> - $section = \$config->{'frr_routemap'}->{$routemap};
> - next;
> - } elsif ($line =~ m/^access-list (.+) seq (\d+) (.+)$/) {
> - $config->{'frr_access_list'}->{$1}->{$2} = $3;
> - next;
> - } elsif ($line =~ m/^ip prefix-list (.+) seq (\d+) (.*)$/) {
> - $config->{'frr_prefix_list'}->{$1}->{$2} = $3;
> - next;
> - } elsif ($line =~ m/^ipv6 prefix-list (.+) seq (\d+) (.*)$/) {
> - $config->{'frr_prefix_list_v6'}->{$1}->{$2} = $3;
> - next;
> - } elsif($line =~ m/^exit-address-family$/) {
> - next;
> - } elsif($line =~ m/^exit$/) {
> - if($router) {
> - $section = \$config->{''};
> - $router = undef;
> - } elsif($routemap) {
> - push(@{$$section}, { rule => $routemap_config, action => $routemap_action });
> - $section = \$config->{''};
> - $routemap = undef;
> - $routemap_action = undef;
> - $routemap_config = ();
> - }
> - next;
> - } elsif($line =~ m/!/) {
> - next;
> - }
> -
> - next if !$section;
> - if($routemap) {
> - push(@{$routemap_config}, $line);
> - } else {
> - push(@{$$section}, $line);
> - }
> - }
> + #return PVE::Network::SDN::Controllers::Frr::generate_controller_rawconfig($class, $plugin_config, $config);
> + die "implemented in the Frr helper";
> }
>
> sub write_controller_config {
> my ($class, $plugin_config, $config) = @_;
> -
> - my $rawconfig = $class->generate_controller_rawconfig($plugin_config, $config);
> - return if !$rawconfig;
> - return if !-d "/etc/frr";
> -
> - file_set_contents("/etc/frr/frr.conf", $rawconfig);
> -}
> -
> -sub reload_controller {
> - my ($class) = @_;
> -
> - my $conf_file = "/etc/frr/frr.conf";
> - my $bin_path = "/usr/lib/frr/frr-reload.py";
> -
> - if (!-e $bin_path) {
> - log_warn("missing $bin_path. Please install frr-pythontools package");
> - return;
> - }
> -
> - my $err = sub {
> - my $line = shift;
> - if ($line =~ /ERROR:/) {
> - warn "$line \n";
> - }
> - };
> -
> - if (-e $conf_file && -e $bin_path) {
> - eval {
> - run_command([$bin_path, '--stdout', '--reload', $conf_file], outfunc => {}, errfunc => $err);
> - };
> - if ($@) {
> - warn "frr reload command fail. Restarting frr.";
> - eval { run_command(['systemctl', 'restart', 'frr']); };
> - }
> - }
> +
> + #return PVE::Network::SDN::Controllers::Frr::write_controller_config($class, $plugin_config, $config);
> + die "implemented in the Frr helper";
> }
>
> 1;
> diff --git a/src/PVE/Network/SDN/Controllers/Frr.pm b/src/PVE/Network/SDN/Controllers/Frr.pm
> new file mode 100644
> index 000000000000..386dcae543e8
> --- /dev/null
> +++ b/src/PVE/Network/SDN/Controllers/Frr.pm
> @@ -0,0 +1,296 @@
> +package PVE::Network::SDN::Controllers::Frr;
> +
> +use strict;
> +use warnings;
> +
> +use PVE::RESTEnvironment qw(log_warn);
> +use PVE::Tools qw(file_get_contents file_set_contents);
> +
> +sub read_local_frr_config {
> + if (-e "/etc/frr/frr.conf.local") {
> + return file_get_contents("/etc/frr/frr.conf.local");
> + }
> +};
> +
> +sub reload_controller {
> + my $conf_file = "/etc/frr/frr.conf";
> + my $bin_path = "/usr/lib/frr/frr-reload.py";
> +
> + if (!-e $bin_path) {
> + log_warn("missing $bin_path. Please install frr-pythontools package");
> + return;
> + }
> +
> + my $err = sub {
> + my $line = shift;
> + if ($line =~ /ERROR:/) {
> + warn "$line \n";
> + }
> + };
> +
> + if (-e $conf_file && -e $bin_path) {
> + eval {
> + run_command([$bin_path, '--stdout', '--reload', $conf_file], outfunc => {}, errfunc => $err);
> + };
> + if ($@) {
> + warn "frr reload command fail. Restarting frr.";
> + eval { run_command(['systemctl', 'restart', 'frr']); };
> + }
> + }
> +}
> +
> +sub generate_controller_rawconfig {
> + my ($config) = @_;
> +
> + my $nodename = PVE::INotify::nodename();
> +
> + my $final_config = [];
> + push @{$final_config}, "frr version 8.5.2";
> + push @{$final_config}, "frr defaults datacenter";
> + push @{$final_config}, "hostname $nodename";
> + push @{$final_config}, "log syslog informational";
> + push @{$final_config}, "service integrated-vtysh-config";
> + push @{$final_config}, "!";
> +
> + my $local_conf = read_local_frr_config();
> + if ($local_conf) {
> + parse_merge_frr_local_config($config, $local_conf);
> + }
> +
> + generate_frr_vrf($final_config, $config->{frr}->{vrf});
> + generate_frr_interfaces($final_config, $config->{frr_interfaces});
> + generate_frr_recurse($final_config, $config->{frr}, undef, 0);
> + generate_frr_list($final_config, $config->{frr_access_list}, "access-list");
> + generate_frr_list($final_config, $config->{frr_prefix_list}, "ip prefix-list");
> + generate_frr_list($final_config, $config->{frr_prefix_list_v6}, "ipv6 prefix-list");
> + generate_frr_simple_list($final_config, $config->{frr_bgp_community_list});
> + generate_frr_routemap($final_config, $config->{frr_routemap});
> + generate_frr_simple_list($final_config, $config->{frr_ip_protocol});
> +
> + push @{$final_config}, "!";
> + push @{$final_config}, "line vty";
> + push @{$final_config}, "!";
> +
> + my $rawconfig = join("\n", @{$final_config});
> +
> + return if !$rawconfig;
> + return $rawconfig;
> +}
> +
> +sub parse_merge_frr_local_config {
> + my ($config, $local_conf) = @_;
> +
> + my $section = \$config->{""};
> + my $router = undef;
> + my $routemap = undef;
> + my $routemap_config = ();
> + my $routemap_action = undef;
> +
> + while ($local_conf =~ /^\s*(.+?)\s*$/gm) {
> + my $line = $1;
> + $line =~ s/^\s+|\s+$//g;
> +
> + if ($line =~ m/^router (.+)$/) {
> + $router = $1;
> + $section = \$config->{'frr'}->{'router'}->{$router}->{""};
> + next;
> + } elsif ($line =~ m/^vrf (.+)$/) {
> + $section = \$config->{'frr'}->{'vrf'}->{$1};
> + next;
> + } elsif ($line =~ m/^interface (.+)$/) {
> + $section = \$config->{'frr_interfaces'}->{$1};
> + next;
> + } elsif ($line =~ m/^bgp community-list (.+)$/) {
> + push(@{$config->{'frr_bgp_community_list'}}, $line);
> + next;
> + } elsif ($line =~ m/address-family (.+)$/) {
> + $section = \$config->{'frr'}->{'router'}->{$router}->{'address-family'}->{$1};
> + next;
> + } elsif ($line =~ m/^route-map (.+) (permit|deny) (\d+)/) {
> + $routemap = $1;
> + $routemap_config = ();
> + $routemap_action = $2;
> + $section = \$config->{'frr_routemap'}->{$routemap};
> + next;
> + } elsif ($line =~ m/^access-list (.+) seq (\d+) (.+)$/) {
> + $config->{'frr_access_list'}->{$1}->{$2} = $3;
> + next;
> + } elsif ($line =~ m/^ip prefix-list (.+) seq (\d+) (.*)$/) {
> + $config->{'frr_prefix_list'}->{$1}->{$2} = $3;
> + next;
> + } elsif ($line =~ m/^ipv6 prefix-list (.+) seq (\d+) (.*)$/) {
> + $config->{'frr_prefix_list_v6'}->{$1}->{$2} = $3;
> + next;
> + } elsif($line =~ m/^exit-address-family$/) {
> + next;
> + } elsif($line =~ m/^exit$/) {
> + if($router) {
> + $section = \$config->{''};
> + $router = undef;
> + } elsif($routemap) {
> + push(@{$$section}, { rule => $routemap_config, action => $routemap_action });
> + $section = \$config->{''};
> + $routemap = undef;
> + $routemap_action = undef;
> + $routemap_config = ();
> + }
> + next;
> + } elsif($line =~ m/!/) {
> + next;
> + }
> +
> + next if !$section;
> + if($routemap) {
> + push(@{$routemap_config}, $line);
> + } else {
> + push(@{$$section}, $line);
> + }
> + }
> +}
> +
> +sub write_controller_config {
> + my ($config) = @_;
> +
> + my $rawconfig = generate_controller_rawconfig($config);
> + return if !$rawconfig;
> + return if !-d "/etc/frr";
> +
> + file_set_contents("/etc/frr/frr.conf", $rawconfig);
> +}
> +
> +
> +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;
> diff --git a/src/PVE/Network/SDN/Controllers/IsisPlugin.pm b/src/PVE/Network/SDN/Controllers/IsisPlugin.pm
> index 97c6876db303..50a11742fff6 100644
> --- a/src/PVE/Network/SDN/Controllers/IsisPlugin.pm
> +++ b/src/PVE/Network/SDN/Controllers/IsisPlugin.pm
> @@ -7,6 +7,7 @@ use PVE::INotify;
> use PVE::JSONSchema qw(get_standard_option);
> use PVE::Tools qw(run_command file_set_contents file_get_contents);
>
> +use PVE::Network::SDN::Controllers::Frr;
> use PVE::Network::SDN::Controllers::Plugin;
> use PVE::Network::SDN::Zones::Plugin;
> use Net::IP;
> @@ -113,19 +114,22 @@ sub on_update_hook {
> }
> }
>
> +sub reload_controller {
> + my ($class) = @_;
> + #return PVE::Network::SDN::Controllers::Frr::reload_controller($class);
> + die "implemented in the Frr helper";
> +}
> +
> sub generate_controller_rawconfig {
> my ($class, $plugin_config, $config) = @_;
> - return "";
> + #return PVE::Network::SDN::Controllers::Frr::generate_controller_rawconfig($class, $plugin_config, $config);
> + die "implemented in the Frr helper";
> }
>
> sub write_controller_config {
> my ($class, $plugin_config, $config) = @_;
> - return;
> -}
> -
> -sub reload_controller {
> - my ($class) = @_;
> - return;
> + #return PVE::Network::SDN::Controllers::Frr::write_controller_config($class, $plugin_config, $config);
> + die "implemented in the Frr helper";
> }
>
> 1;
> diff --git a/src/PVE/Network/SDN/Controllers/Makefile b/src/PVE/Network/SDN/Controllers/Makefile
> index fd9f881a0ad2..3b0387913cdc 100644
> --- a/src/PVE/Network/SDN/Controllers/Makefile
> +++ b/src/PVE/Network/SDN/Controllers/Makefile
> @@ -1,4 +1,4 @@
> -SOURCES=Plugin.pm FaucetPlugin.pm EvpnPlugin.pm BgpPlugin.pm IsisPlugin.pm
> +SOURCES=Plugin.pm FaucetPlugin.pm EvpnPlugin.pm BgpPlugin.pm IsisPlugin.pm Frr.pm
>
>
> PERL5DIR=${DESTDIR}/usr/share/perl5
> diff --git a/src/PVE/Network/SDN/Zones/EvpnPlugin.pm b/src/PVE/Network/SDN/Zones/EvpnPlugin.pm
> index 4843756a75bd..6212ba0a02d9 100644
> --- a/src/PVE/Network/SDN/Zones/EvpnPlugin.pm
> +++ b/src/PVE/Network/SDN/Zones/EvpnPlugin.pm
> @@ -323,6 +323,21 @@ sub vnet_update_hook {
> }
> }
>
> +sub reload_controller {
> + my ($class) = @_;
> + die "implemented in the Frr helper";
> +}
> +
> +sub generate_controller_rawconfig {
> + my ($class, $config) = @_;
> + die "implemented in the Frr helper";
> +}
> +
> +sub write_controller_config {
> + my ($class, $config) = @_;
> + die "implemented in the Frr helper";
> +}
> +
> 1;
Those methods are only needed in the Controllers, not Zones - they serve
no purpose here afaict?
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [pve-devel] [PATCH network] sdn: factor out frr config generation and writing
2025-02-12 11:17 ` Stefan Hanreich
@ 2025-02-13 10:09 ` Gabriel Goller
0 siblings, 0 replies; 5+ messages in thread
From: Gabriel Goller @ 2025-02-13 10:09 UTC (permalink / raw)
To: Stefan Hanreich; +Cc: Proxmox VE development discussion
On 12.02.2025 12:17, Stefan Hanreich wrote:
>This still has some issues (see below), maybe we can look at it together
>next week (will be gone after today) and see if we can make some
>additional structural improvements to the whole controller / frr logic?
>There were also some regressions/bugs with FRR config generation while
>testing, so this series needs some work still.
We checked this together offliste, the patch doesn't change the output,
so no regressions.
>On 2/5/25 17:13, Gabriel Goller wrote:
>> diff --git a/src/PVE/Network/SDN/Controllers.pm b/src/PVE/Network/SDN/Controllers.pm
>> index fd7ad54ac38c..43f154b7338e 100644
>> --- a/src/PVE/Network/SDN/Controllers.pm
>> +++ b/src/PVE/Network/SDN/Controllers.pm
>> @@ -148,10 +149,22 @@ sub reload_controller {
>>
>> return if !$controller_cfg;
>>
>> + my $frr_reload = 0;
>> +
>> foreach my $id (keys %{$controller_cfg->{ids}}) {
>> my $plugin_config = $controller_cfg->{ids}->{$id};
>> - my $plugin = PVE::Network::SDN::Controllers::Plugin->lookup($plugin_config->{type});
>> - $plugin->reload_controller();
>> + my $type = $plugin_config->{type};
>> + my @frr_types = ("bgp", "isis", "evpn");
>> + if (grep {$type} @frr_types) {
>
>this doesn't work. you need:
>
> grep {$_ eq $type} @frr_types
>
>(or what thomas said is even better - just for future reference)
Yup, my bad, will fix this.
>> + $frr_reload = 1;
>> + } else {
>> + my $plugin = PVE::Network::SDN::Controllers::Plugin->lookup($type);
>> + $plugin->reload_controller();
>> + }
>> + }
>> +
>> + if ($frr_reload) {
>> + PVE::Network::SDN::Controllers::Frr::reload_controller();
>> }
>> }
>>
>> @@ -161,12 +174,22 @@ sub generate_controller_rawconfig {
>
>This actually never gets called (except in the tests, ..). It works, but
>still takes the old code path without going through the new FRR helper.
>Probably not the intention?
Yep, this function is only really called in the unit tests. With the
usual flow, we call write_controller_config, which resolves the plugin
and calls this function on the each plugin.
We could call the generate_controller_rawconfig function here though:
src/PVE/Network/SDN/Controllers.pm:212. So instead of getting the plugin
and calling the rawconfig implementations in write_controller_config,
just use this function.
>> my $cfg = PVE::Network::SDN::running_config();
>> my $controller_cfg = $cfg->{controllers};
>> return if !$controller_cfg;
>> + my $frr_generate = 0;
>>
>> my $rawconfig = "";
>> foreach my $id (keys %{$controller_cfg->{ids}}) {
>> my $plugin_config = $controller_cfg->{ids}->{$id};
>> - my $plugin = PVE::Network::SDN::Controllers::Plugin->lookup($plugin_config->{type});
>> - $rawconfig .= $plugin->generate_controller_rawconfig($plugin_config, $config);
>> + my $type = $plugin_config->{type};
>> + my @frr_types = ("bgp", "isis", "evpn");
>> + if (grep {$type} @frr_types) {
>> @@ -323,6 +323,21 @@ sub vnet_update_hook {
>> }
>> }
>>
>> +sub reload_controller {
>> + my ($class) = @_;
>> + die "implemented in the Frr helper";
>> +}
>> +
>> +sub generate_controller_rawconfig {
>> + my ($class, $config) = @_;
>> + die "implemented in the Frr helper";
>> +}
>> +
>> +sub write_controller_config {
>> + my ($class, $config) = @_;
>> + die "implemented in the Frr helper";
>> +}
>> +
>> 1;
>
>Those methods are only needed in the Controllers, not Zones - they serve
>no purpose here afaict?
Yep, true, didn't check the directory I was in (zone vs controller) :)
Thanks for the review!
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2025-02-13 10:10 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-02-05 16:13 [pve-devel] [PATCH network] sdn: factor out frr config generation and writing Gabriel Goller
2025-02-11 20:30 ` Thomas Lamprecht
2025-02-12 8:45 ` Stefan Hanreich
2025-02-12 11:17 ` Stefan Hanreich
2025-02-13 10:09 ` Gabriel Goller
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal