From: Gabriel Goller <g.goller@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH pve-network 10/15] sdn: allow fabrics to use simple zone VRFs
Date: Fri, 21 Aug 2026 16:03:54 +0200 [thread overview]
Message-ID: <20260821140404.322081-11-g.goller@proxmox.com> (raw)
In-Reply-To: <20260821140404.322081-1-g.goller@proxmox.com>
Let BGP and OSPF fabrics run inside the dedicated VRF of a simple zone.
Restrict this to zones with default-vrf disabled, since a fabric without
a zone already uses the default VRFF.
Validate that every fabric node is enabled for the selected zone (the
zone is not automatically on the whole cluster anymore). Repeat the
checks when fabrics, fabric nodes, or zones change, and prevent deleting
a zone while a fabric still uses it. This avoids scenarios where a
fabric node exists on a node where the VRF (zone) doesn't.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
src/PVE/API2/Network/SDN/Fabrics/Fabric.pm | 48 +++++++++++++++----
.../API2/Network/SDN/Fabrics/FabricNode.pm | 18 +++++++
src/PVE/API2/Network/SDN/Zones.pm | 27 +++++++++++
src/PVE/Network/SDN/Controllers/BgpPlugin.pm | 2 +-
src/PVE/Network/SDN/Fabrics.pm | 29 ++++++++++-
5 files changed, 114 insertions(+), 10 deletions(-)
diff --git a/src/PVE/API2/Network/SDN/Fabrics/Fabric.pm b/src/PVE/API2/Network/SDN/Fabrics/Fabric.pm
index 4695a6ee7d71..e381dc2a08fb 100644
--- a/src/PVE/API2/Network/SDN/Fabrics/Fabric.pm
+++ b/src/PVE/API2/Network/SDN/Fabrics/Fabric.pm
@@ -6,13 +6,51 @@ use warnings;
use PVE::Network::SDN;
use PVE::Network::SDN::Controllers;
use PVE::Network::SDN::Fabrics;
+use PVE::Network::SDN::Zones;
+use PVE::Exception qw(raise_param_exc);
use PVE::JSONSchema qw(get_standard_option);
use PVE::Tools qw(extract_param);
use PVE::RESTHandler;
use base qw(PVE::RESTHandler);
+sub validate_fabric_zone {
+ my ($fabric, $config) = @_;
+
+ my $protocol = $fabric->{protocol} // '';
+ if ($protocol eq 'bgp' && !$fabric->{zone}) {
+ my $controller_cfg = PVE::Network::SDN::Controllers::config();
+ for my $controller_id (keys %{ $controller_cfg->{ids} // {} }) {
+ my $controller = $controller_cfg->{ids}->{$controller_id};
+ if ($controller->{type} eq 'bgp') {
+ die "cannot configure a BGP fabric while BGP controller '$controller_id' exists:"
+ . " both target the default-VRF BGP router\n";
+ }
+ }
+ }
+
+ my $zone_id = $fabric->{zone};
+ return if !$zone_id;
+
+ raise_param_exc({ zone => 'VRF zones are only supported for BGP and OSPF fabrics' })
+ if $protocol !~ /^(?:bgp|ospf)$/;
+
+ my $zone = PVE::Network::SDN::Zones::get_zone($zone_id);
+
+ raise_param_exc({ zone => "zone '$zone_id' does not exist" }) if !$zone;
+ raise_param_exc({ zone => "zone '$zone_id' is not a simple zone" })
+ if $zone->{type} ne 'simple';
+ raise_param_exc({ zone => "zone '$zone_id' is configured to use the default VRF" })
+ if $zone->{'default-vrf'} // 1;
+
+ if ($config) {
+ PVE::Network::SDN::Fabrics::assert_fabric_nodes_in_zone(
+ $config, $fabric->{id}, $zone_id, $zone,
+ );
+ }
+}
+
__PACKAGE__->register_method({
name => 'index',
path => '',
@@ -148,14 +186,7 @@ __PACKAGE__->register_method({
my $digest = extract_param($param, 'digest');
PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
- if (($param->{protocol} // '') eq 'bgp') {
- my $controller_cfg = PVE::Network::SDN::Controllers::config();
- for my $id (keys %{ $controller_cfg->{ids} // {} }) {
- die "cannot add a BGP fabric while BGP controller '$id' exists:"
- . " both target the default-VRF BGP router\n"
- if $controller_cfg->{ids}->{$id}->{type} eq 'bgp';
- }
- }
+ validate_fabric_zone($param);
$config->add_fabric($param);
PVE::Network::SDN::Fabrics::write_config($config);
@@ -195,6 +226,7 @@ __PACKAGE__->register_method({
PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
$config->update_fabric($id, $param);
+ validate_fabric_zone($config->get_fabric($id), $config);
PVE::Network::SDN::Fabrics::write_config($config);
},
"updating fabric failed",
diff --git a/src/PVE/API2/Network/SDN/Fabrics/FabricNode.pm b/src/PVE/API2/Network/SDN/Fabrics/FabricNode.pm
index 7a148b550f7d..eb3b429075e5 100644
--- a/src/PVE/API2/Network/SDN/Fabrics/FabricNode.pm
+++ b/src/PVE/API2/Network/SDN/Fabrics/FabricNode.pm
@@ -9,6 +9,7 @@ use PVE::Tools qw(extract_param run_command);
use PVE::Network::SDN;
use PVE::Network::SDN::Fabrics;
use PVE::Network::SDN::WireGuard;
+use PVE::Network::SDN::Zones;
use PVE::RS::SDN::Fabrics;
use PVE::RESTHandler;
@@ -138,6 +139,21 @@ my sub is_internal_wireguard_node {
return $node->{protocol} eq 'wireguard' && $node->{role} eq 'internal';
}
+my sub validate_node_fabric_zone {
+ my ($config, $fabric_id, $node_id) = @_;
+
+ my $fabric = $config->get_fabric($fabric_id);
+ my $zone_id = $fabric->{zone};
+ return if !$zone_id;
+
+ my $zone = PVE::Network::SDN::Zones::get_zone($zone_id);
+
+ die "zone '$zone_id' does not exist\n" if !$zone;
+ PVE::Network::SDN::Fabrics::assert_fabric_node_in_zone(
+ $fabric_id, $node_id, $zone_id, $zone,
+ );
+}
+
__PACKAGE__->register_method({
name => 'add_node',
path => '',
@@ -169,6 +185,8 @@ __PACKAGE__->register_method({
my $digest = extract_param($param, 'digest');
PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
+ validate_node_fabric_zone($config, $param->{fabric_id}, $param->{node_id});
+
if (is_internal_wireguard_node($param) && $param->{interfaces}) {
my $private_keys = PVE::Network::SDN::WireGuard::private_keys();
diff --git a/src/PVE/API2/Network/SDN/Zones.pm b/src/PVE/API2/Network/SDN/Zones.pm
index 95192497fb9e..d9cb1f41ec34 100644
--- a/src/PVE/API2/Network/SDN/Zones.pm
+++ b/src/PVE/API2/Network/SDN/Zones.pm
@@ -13,6 +13,7 @@ use PVE::SafeSyslog;
use PVE::Tools qw(extract_param);
use PVE::Network::SDN::Dns;
+use PVE::Network::SDN::Fabrics;
use PVE::Network::SDN::Subnets;
use PVE::Network::SDN::Vnets;
use PVE::Network::SDN;
@@ -396,6 +397,13 @@ sub create_etc_interfaces_sdn_dir {
mkdir("/etc/pve/sdn");
}
+sub fabrics_using_zone {
+ my ($zone_id, $fabric_config) = @_;
+
+ my $fabrics = $fabric_config->list_fabrics();
+ return grep { ($fabrics->{$_}->{zone} // '') eq $zone_id } sort keys $fabrics->%*;
+}
+
__PACKAGE__->register_method({
name => 'create',
protected => 1,
@@ -517,6 +525,21 @@ __PACKAGE__->register_method({
$scfg->{$_} = $opts->{$_} for keys $opts->%*;
+ my $fabric_config = PVE::Network::SDN::Fabrics::config();
+ my @fabric_ids = fabrics_using_zone($id, $fabric_config);
+
+ if (@fabric_ids && $scfg->{type} eq 'simple' && ($scfg->{'default-vrf'} // 1)) {
+ raise_param_exc({
+ 'default-vrf' => "zone is still used as VRF by fabric '$fabric_ids[0]'",
+ });
+ }
+
+ for my $fabric_id (@fabric_ids) {
+ PVE::Network::SDN::Fabrics::assert_fabric_nodes_in_zone(
+ $fabric_config, $fabric_id, $id, $scfg,
+ );
+ }
+
my $new_ipam = $scfg->{ipam};
if (!$new_ipam != !$old_ipam || (($new_ipam // '') ne ($old_ipam // ''))) {
# don't allow ipam change if subnet are defined for now, need to implement resync ipam content
@@ -598,6 +621,10 @@ __PACKAGE__->register_method({
my $plugin = PVE::Network::SDN::Zones::Plugin->lookup($scfg->{type});
my $vnet_cfg = PVE::Network::SDN::Vnets::config();
+ my $fabric_config = PVE::Network::SDN::Fabrics::config();
+ my @fabric_ids = fabrics_using_zone($id, $fabric_config);
+ die "zone is still used as VRF by fabric '$fabric_ids[0]'\n" if @fabric_ids;
+
$plugin->on_delete_hook($id, $vnet_cfg);
delete $cfg->{ids}->{$id};
diff --git a/src/PVE/Network/SDN/Controllers/BgpPlugin.pm b/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
index ea2ba5a764ef..3d1593e7926a 100644
--- a/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
+++ b/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
@@ -207,7 +207,7 @@ sub on_update_hook {
for my $id (keys %$fabrics) {
die "cannot configure a BGP controller while BGP fabric '$id' exists:"
. " both target the default-VRF BGP router\n"
- if $fabrics->{$id}->{protocol} eq 'bgp';
+ if $fabrics->{$id}->{protocol} eq 'bgp' && !$fabrics->{$id}->{zone};
}
my $controller = $controller_cfg->{ids}->{$controllerid};
diff --git a/src/PVE/Network/SDN/Fabrics.pm b/src/PVE/Network/SDN/Fabrics.pm
index 4f842f1013ef..573cb0b1d197 100644
--- a/src/PVE/Network/SDN/Fabrics.pm
+++ b/src/PVE/Network/SDN/Fabrics.pm
@@ -192,6 +192,23 @@ sub write_config {
cfs_write_file("sdn/fabrics.cfg", $config->to_raw(), 1);
}
+sub assert_fabric_node_in_zone {
+ my ($fabric_id, $node_id, $zone_id, $zone) = @_;
+
+ return if !defined($zone->{nodes}) || $zone->{nodes}->{$node_id};
+
+ die "fabric '$fabric_id' contains node '$node_id', which is not enabled for zone '$zone_id'\n";
+}
+
+sub assert_fabric_nodes_in_zone {
+ my ($config, $fabric_id, $zone_id, $zone) = @_;
+
+ my $nodes = $config->list_nodes_fabric($fabric_id);
+ for my $node_id (sort keys $nodes->%*) {
+ assert_fabric_node_in_zone($fabric_id, $node_id, $zone_id, $zone);
+ }
+}
+
sub get_frr_daemon_status {
my ($fabric_config) = @_;
@@ -469,6 +486,15 @@ sub fabric_properties {
protocol => get_standard_option('pve-sdn-fabric-protocol'),
digest => get_standard_option('pve-config-digest'),
'lock-token' => get_standard_option('pve-sdn-lock-token'),
+ zone => get_standard_option(
+ 'pve-sdn-zone-id',
+ {
+ description => 'Simple zone whose VRF contains this fabric.',
+ 'type-property' => 'protocol',
+ 'instance-types' => ['bgp', 'ospf'],
+ optional => 1,
+ },
+ ),
ip_prefix => {
type => 'string',
format => 'CIDR',
@@ -619,6 +645,7 @@ sub fabric_properties {
enum => [
'ip_prefix',
'ip6_prefix',
+ 'zone',
'redistribute',
'route_filter',
'route_map_in',
@@ -632,7 +659,7 @@ sub fabric_properties {
'instance-types' => ['ospf'],
items => {
type => 'string',
- enum => ['area', 'redistribute', 'route_filter'],
+ enum => ['area', 'redistribute', 'route_filter', 'zone'],
},
optional => 1,
},
--
2.47.3
next prev parent reply other threads:[~2026-08-21 14:06 UTC|newest]
Thread overview: 16+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-21 14:03 [RFC manager/network/proxmox{-ve-rs,-perl-rs} 00/15] SDN VRF support Gabriel Goller
2026-08-21 14:03 ` [PATCH proxmox-ve-rs 01/15] frr: add VRF-aware OSPF rendering Gabriel Goller
2026-08-21 14:03 ` [PATCH proxmox-ve-rs 02/15] sdn: add zone references to OSPF and BGP fabrics Gabriel Goller
2026-08-21 14:03 ` [PATCH proxmox-ve-rs 03/15] sdn: generate fabric routing configuration in zone VRFs Gabriel Goller
2026-08-21 14:03 ` [PATCH proxmox-ve-rs 04/15] sdn: fix VRF route-map scoping and zone ID validation Gabriel Goller
2026-08-21 14:03 ` [PATCH proxmox-ve-rs 05/15] tests: fabrics: add test for fabrics in VRFs Gabriel Goller
2026-08-21 14:03 ` [PATCH proxmox-perl-rs 06/15] pve-rs: fabrics: assign network interfaces to configured VRFs Gabriel Goller
2026-08-21 14:03 ` [PATCH proxmox-perl-rs 07/15] pve-rs: fabrics: make per-fabric status queries VRF-aware Gabriel Goller
2026-08-21 14:03 ` [PATCH proxmox-perl-rs 08/15] pve-rs: fabrics: include VRF routes in aggregate status Gabriel Goller
2026-08-21 14:03 ` [PATCH pve-network 09/15] sdn: add optional VRFs for simple zones Gabriel Goller
2026-08-21 14:03 ` Gabriel Goller [this message]
2026-08-21 14:03 ` [PATCH pve-network 11/15] sdn: always generate EVPN zone VRFs Gabriel Goller
2026-08-21 14:03 ` [PATCH pve-network 12/15] api: sdn: allow EVPN zones as fabric VRFs Gabriel Goller
2026-08-21 14:03 ` [PATCH pve-network 13/15] api: sdn: disallow BGP fabrics in EVPN zone VRFs Gabriel Goller
2026-08-21 14:03 ` [PATCH pve-manager 14/15] ui: sdn: add VRF zone selection for fabrics Gabriel Goller
2026-08-21 14:03 ` [PATCH pve-manager 15/15] ui: sdn: expose EVPN zones as fabric VRFs Gabriel Goller
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260821140404.322081-11-g.goller@proxmox.com \
--to=g.goller@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.