From: "Rémi PAETA" <remi.paeta@comaite.com>
To: pve-devel@lists.proxmox.com
Subject: [RFC PATCH network 03/16] sdn: zones: add ovn plugin
Date: Tue, 13 Oct 2026 14:14:19 +0200 [thread overview]
Message-ID: <20261013121432.2642979-4-remi.paeta@comaite.com> (raw)
In-Reply-To: <20261013121432.2642979-1-remi.paeta@comaite.com>
Every vnet of an OVN zone maps to the OVN logical switch of the same
name. Guest interfaces get plugged into the OVN integration bridge
(br-int), through a firewall bridge if the guest firewall is enabled,
and bound to the logical switch port named after the interface. New
ports get the 'unknown' address, so the logical switch forwards to
them like a learning switch.
Each vnet still gets an OVS bridge in the SDN interfaces file. It never
has any port, but it lets qemu-server and pve-container read the vnet
MTU, and since it is an OVS bridge, they set up the firewall bridge
like for any other OVS bridge, so the guest firewall works unchanged.
br-int is created by ovn-controller and deliberately not managed by
ifupdown2, whose OVS addon would remove the guest ports it does not
know about.
The default MTU of 1442 leaves room for the Geneve overhead on a 1500
bytes underlay. VLAN tags, VLAN-aware vnets and isolated ports are not
supported and refused on vnets of OVN zones.
Errors of ovn-nbctl are reported with its own message, for example that
the northbound database cannot be reached, instead of its whole command
line.
Signed-off-by: Rémi PAETA <remi.paeta@comaite.com>
---
debian/control | 1 +
src/PVE/Network/SDN/Controllers.pm | 4 +
src/PVE/Network/SDN/Makefile | 3 +-
src/PVE/Network/SDN/Ovn.pm | 183 ++++++++++++++++++
src/PVE/Network/SDN/Zones.pm | 2 +
src/PVE/Network/SDN/Zones/Makefile | 2 +-
src/PVE/Network/SDN/Zones/OvnPlugin.pm | 124 ++++++++++++
src/test/run_test_ovn.pl | 131 +++++++++++++
.../ovn/basic/expected_controller_config | 8 +
.../zones/ovn/basic/expected_sdn_interfaces | 12 ++
src/test/zones/ovn/basic/interfaces | 5 +
src/test/zones/ovn/basic/sdn_config | 24 +++
.../ovn/with_evpn/expected_controller_config | 44 +++++
.../ovn/with_evpn/expected_sdn_interfaces | 47 +++++
src/test/zones/ovn/with_evpn/interfaces | 7 +
src/test/zones/ovn/with_evpn/sdn_config | 35 ++++
16 files changed, 630 insertions(+), 2 deletions(-)
create mode 100644 src/PVE/Network/SDN/Ovn.pm
create mode 100644 src/PVE/Network/SDN/Zones/OvnPlugin.pm
create mode 100644 src/test/zones/ovn/basic/expected_controller_config
create mode 100644 src/test/zones/ovn/basic/expected_sdn_interfaces
create mode 100644 src/test/zones/ovn/basic/interfaces
create mode 100644 src/test/zones/ovn/basic/sdn_config
create mode 100644 src/test/zones/ovn/with_evpn/expected_controller_config
create mode 100644 src/test/zones/ovn/with_evpn/expected_sdn_interfaces
create mode 100644 src/test/zones/ovn/with_evpn/interfaces
create mode 100644 src/test/zones/ovn/with_evpn/sdn_config
diff --git a/debian/control b/debian/control
index d8c1a03..a28cc2d 100644
--- a/debian/control
+++ b/debian/control
@@ -34,6 +34,7 @@ Depends: libpve-common-perl (>= 9.1.1),
Recommends: frr-pythontools (>= 10.3.1-1+pve2~),
wireguard-tools,
Suggests: dnsmasq,
+ ovn-host,
Description: Proxmox VE's SDN (Software Defined Network) stack
This package contains the perl side of the Software Defined Network
implementation for Proxmox VE.
diff --git a/src/PVE/Network/SDN/Controllers.pm b/src/PVE/Network/SDN/Controllers.pm
index ac0d70a..cae94db 100644
--- a/src/PVE/Network/SDN/Controllers.pm
+++ b/src/PVE/Network/SDN/Controllers.pm
@@ -122,6 +122,9 @@ sub generate_frr_config {
foreach my $id (sort keys %{ $zone_cfg->{ids} }) {
my $plugin_config = $zone_cfg->{ids}->{$id};
+ # OVN zones are not handled by FRR
+ next if $plugin_config->{type} eq 'ovn';
+
my $controllerid = $plugin_config->{controller};
next if !$controllerid;
@@ -161,6 +164,7 @@ sub generate_frr_config {
next if !$zoneid;
my $zone = $zone_cfg->{ids}->{$zoneid};
next if !$zone;
+ next if $zone->{type} eq 'ovn';
my $controllerid = $zone->{controller};
next if !$controllerid;
my $controller = $controller_cfg->{ids}->{$controllerid};
diff --git a/src/PVE/Network/SDN/Makefile b/src/PVE/Network/SDN/Makefile
index d0b4bce..69c2ed7 100644
--- a/src/PVE/Network/SDN/Makefile
+++ b/src/PVE/Network/SDN/Makefile
@@ -11,7 +11,8 @@ SOURCES=Vnets.pm\
Frr.pm\
PrefixLists.pm\
RouteMaps.pm\
- WireGuard.pm
+ WireGuard.pm\
+ Ovn.pm
PERL5DIR=${DESTDIR}/usr/share/perl5
diff --git a/src/PVE/Network/SDN/Ovn.pm b/src/PVE/Network/SDN/Ovn.pm
new file mode 100644
index 0000000..93235e8
--- /dev/null
+++ b/src/PVE/Network/SDN/Ovn.pm
@@ -0,0 +1,183 @@
+package PVE::Network::SDN::Ovn;
+
+use v5.36;
+
+use PVE::Network;
+use PVE::Tools qw(run_command);
+
+our $NBCTL = '/usr/bin/ovn-nbctl';
+our $VSCTL = '/usr/bin/ovs-vsctl';
+my $NBCTL_TIMEOUT = 10;
+
+our $INTEGRATION_BRIDGE = 'br-int';
+
+# external_ids keys tagging the objects managed by PVE. Northbound objects
+# without them are never touched, so the OVN deployment can be shared with
+# other consumers.
+my $EXT_CONTROLLER = 'pve-sdn-controller';
+my $EXT_ZONE = 'pve-sdn-zone';
+my $EXT_VNET = 'pve-sdn-vnet';
+my $EXT_VMID = 'pve-sdn-vmid';
+
+sub nbctl {
+ my ($controller, @args) = @_;
+
+ my ($output, $errors) = ('', '');
+ eval {
+ run_command(
+ [$NBCTL, "--db=$controller->{'nb-connection'}", "--timeout=$NBCTL_TIMEOUT", @args],
+ outfunc => sub { $output .= "$_[0]\n"; },
+ errfunc => sub { $errors .= "$_[0]\n"; },
+ );
+ };
+ if (my $err = $@) {
+ # the error of ovn-nbctl is more helpful than its whole command line
+ die $errors ne '' ? $errors : $err;
+ }
+
+ return $output;
+}
+
+# runs all commands in a single northbound transaction
+sub nbctl_transaction {
+ my ($controller, $commands) = @_;
+
+ return if !scalar(@$commands);
+
+ nbctl($controller, map { ('--', @$_) } @$commands);
+}
+
+=head3 port_external_ids($controllerid, $zoneid, $vnetid, $iface)
+
+Returns the external_ids identifying the logical switch port of the guest
+interface C<$iface>.
+
+=cut
+
+sub port_external_ids {
+ my ($controllerid, $zoneid, $vnetid, $iface) = @_;
+
+ my $external_ids = {
+ $EXT_CONTROLLER => $controllerid,
+ $EXT_ZONE => $zoneid,
+ $EXT_VNET => $vnetid,
+ };
+ $external_ids->{$EXT_VMID} = $1 if $iface =~ m/^(?:tap|veth)(\d+)i\d+$/;
+
+ return $external_ids;
+}
+
+=head3 port_commands($vnetid, $iface, \%external_ids, $current_switch)
+
+Returns the C<ovn-nbctl> commands creating the logical switch port C<$iface> on
+the logical switch C<$vnetid>. C<$current_switch> is the switch the port
+currently belongs to, if any.
+
+New ports get the C<unknown> address, so the logical switch behaves like a
+learning switch for them. Existing ports keep their addresses.
+
+=cut
+
+sub port_commands {
+ my ($vnetid, $iface, $external_ids, $current_switch) = @_;
+
+ my $commands = [];
+
+ if (!defined($current_switch)) {
+ push @$commands, ['lsp-add', $vnetid, $iface], ['lsp-set-addresses', $iface, 'unknown'];
+ } elsif ($current_switch ne $vnetid) {
+ push @$commands, ['lsp-del', $iface], ['lsp-add', $vnetid, $iface],
+ ['lsp-set-addresses', $iface, 'unknown'];
+ }
+
+ push @$commands,
+ [
+ 'set',
+ 'Logical_Switch_Port',
+ $iface,
+ map { "external_ids:$_=$external_ids->{$_}" } sort keys %$external_ids,
+ ];
+
+ return $commands;
+}
+
+=head3 ensure_port($controllerid, $controller, $zoneid, $vnetid, $iface)
+
+Creates or updates the logical switch port of the guest interface C<$iface> on
+the logical switch of C<$vnetid>, moving it over from another switch if needed.
+
+=cut
+
+sub ensure_port {
+ my ($controllerid, $controller, $zoneid, $vnetid, $iface) = @_;
+
+ eval {
+ my $current_switch;
+ my $exists = nbctl(
+ $controller,
+ '--bare',
+ '--columns=_uuid',
+ 'find',
+ 'Logical_Switch_Port',
+ "name=$iface",
+ );
+ if ($exists =~ m/\S/) {
+ # prints "<uuid> (<switch name>)"
+ my $output = nbctl($controller, 'lsp-get-ls', $iface);
+ $current_switch = $1 if $output =~ m/^\S+ \((\S+)\)$/m;
+ }
+
+ my $external_ids = port_external_ids($controllerid, $zoneid, $vnetid, $iface);
+ my $commands = port_commands($vnetid, $iface, $external_ids, $current_switch);
+ nbctl_transaction($controller, $commands);
+ };
+ die "unable to create OVN logical switch port '$iface' on '$vnetid' - $@" if $@;
+}
+
+=head3 firewall_port_name($iface)
+
+Returns the name of the OVS port that pve-common creates on the integration
+bridge for the firewall bridge of the guest interface C<$iface>.
+
+=cut
+
+sub firewall_port_name {
+ my ($iface) = @_;
+
+ die "unable to parse guest interface name '$iface'\n"
+ if $iface !~ m/^(?:tap|veth)(\d+)i(\d+)$/;
+
+ return "fwln${1}o${2}";
+}
+
+=head3 plug_port($iface, $firewall, $rate)
+
+Attaches the guest interface C<$iface> to the integration bridge, through a
+firewall bridge if C<$firewall> is set, and binds it to the logical switch port
+of the same name.
+
+=cut
+
+sub plug_port {
+ my ($iface, $firewall, $rate) = @_;
+
+ die "integration bridge '$INTEGRATION_BRIDGE' does not exist, is ovn-host running?\n"
+ if !-d "/sys/class/net/$INTEGRATION_BRIDGE";
+
+ # the integration bridge is an OVS bridge, so pve-common already knows how
+ # to plug guests into it, including the firewall bridge
+ PVE::Network::tap_plug(
+ $iface,
+ $INTEGRATION_BRIDGE,
+ undef,
+ $firewall,
+ undef,
+ $rate,
+ { learning => 1 },
+ );
+
+ my $port = $firewall ? firewall_port_name($iface) : $iface;
+ run_command([$VSCTL, 'set', 'Interface', $port, "external_ids:iface-id=$iface"]);
+}
+
+1;
diff --git a/src/PVE/Network/SDN/Zones.pm b/src/PVE/Network/SDN/Zones.pm
index f668303..491be7d 100644
--- a/src/PVE/Network/SDN/Zones.pm
+++ b/src/PVE/Network/SDN/Zones.pm
@@ -16,6 +16,7 @@ use PVE::Network::SDN::Zones::VxlanPlugin;
use PVE::Network::SDN::Zones::EvpnPlugin;
use PVE::Network::SDN::Zones::FaucetPlugin;
use PVE::Network::SDN::Zones::SimplePlugin;
+use PVE::Network::SDN::Zones::OvnPlugin;
use PVE::Network::SDN::Zones::Plugin;
PVE::Network::SDN::Zones::VlanPlugin->register();
@@ -24,6 +25,7 @@ PVE::Network::SDN::Zones::VxlanPlugin->register();
PVE::Network::SDN::Zones::EvpnPlugin->register();
PVE::Network::SDN::Zones::FaucetPlugin->register();
PVE::Network::SDN::Zones::SimplePlugin->register();
+PVE::Network::SDN::Zones::OvnPlugin->register();
PVE::Network::SDN::Zones::Plugin->init();
my $local_network_sdn_file = "/etc/network/interfaces.d/sdn";
diff --git a/src/PVE/Network/SDN/Zones/Makefile b/src/PVE/Network/SDN/Zones/Makefile
index 8454388..8941acb 100644
--- a/src/PVE/Network/SDN/Zones/Makefile
+++ b/src/PVE/Network/SDN/Zones/Makefile
@@ -1,4 +1,4 @@
-SOURCES=Plugin.pm VlanPlugin.pm VxlanPlugin.pm FaucetPlugin.pm EvpnPlugin.pm QinQPlugin.pm SimplePlugin.pm
+SOURCES=Plugin.pm VlanPlugin.pm VxlanPlugin.pm FaucetPlugin.pm EvpnPlugin.pm QinQPlugin.pm SimplePlugin.pm OvnPlugin.pm
PERL5DIR=${DESTDIR}/usr/share/perl5
diff --git a/src/PVE/Network/SDN/Zones/OvnPlugin.pm b/src/PVE/Network/SDN/Zones/OvnPlugin.pm
new file mode 100644
index 0000000..26557e7
--- /dev/null
+++ b/src/PVE/Network/SDN/Zones/OvnPlugin.pm
@@ -0,0 +1,124 @@
+package PVE::Network::SDN::Zones::OvnPlugin;
+
+use v5.36;
+
+use PVE::Exception qw(raise_param_exc);
+use PVE::Network;
+
+use PVE::Network::SDN::Ovn;
+use PVE::Network::SDN::Zones::Plugin;
+
+use base('PVE::Network::SDN::Zones::Plugin');
+
+# leaves room for the Geneve overhead on a 1500 bytes underlay
+my $DEFAULT_MTU = 1442;
+
+sub type {
+ return 'ovn';
+}
+
+sub options {
+ return {
+ nodes => { optional => 1 },
+ controller => { optional => 0 },
+ mtu => { optional => 1 },
+ dns => { optional => 1 },
+ reversedns => { optional => 1 },
+ dnszone => { optional => 1 },
+ ipam => { optional => 1 },
+ };
+}
+
+# Plugin implementation
+sub generate_sdn_config {
+ my (
+ $class,
+ $plugin_config,
+ $zoneid,
+ $vnetid,
+ $vnet,
+ $controller,
+ $controller_cfg,
+ $subnet_cfg,
+ $interfaces_config,
+ $config,
+ ) = @_;
+
+ die "missing OVN controller\n" if !$controller || $controller->{type} ne 'ovn';
+
+ # Guest interfaces get plugged into the OVN integration bridge, so the vnet
+ # bridge never gets any port. It exists so that qemu-server and
+ # pve-container can read the vnet MTU, and since it is an OVS bridge, they
+ # request a firewall bridge for guests with the firewall enabled.
+ my @iface_config = ();
+ push @iface_config, "ovs_type OVSBridge";
+ push @iface_config, "ovs_mtu " . $class->get_mtu($plugin_config);
+ push @iface_config, "alias $vnet->{alias}" if $vnet->{alias};
+ push(@{ $config->{$vnetid} }, @iface_config) if !$config->{$vnetid};
+
+ return $config;
+}
+
+sub status {
+ my ($class, $plugin_config, $zone, $vnetid, $vnet, $status) = @_;
+
+ my $err_msg = $class->generate_status_message($vnetid, $status, []);
+
+ if (!-d "/sys/class/net/$PVE::Network::SDN::Ovn::INTEGRATION_BRIDGE") {
+ push @$err_msg, "missing $PVE::Network::SDN::Ovn::INTEGRATION_BRIDGE";
+ }
+
+ return $err_msg;
+}
+
+sub tap_plug {
+ my ($class, $plugin_config, $vnet, $tag, $iface, $vnetid, $firewall, $trunks, $rate) = @_;
+
+ my $controllerid = $plugin_config->{controller};
+ my $controller =
+ PVE::Network::SDN::running_config()->{controllers}->{ids}->{$controllerid};
+ die "OVN controller '$controllerid' of vnet '$vnetid' is not applied yet\n"
+ if !$controller;
+
+ PVE::Network::SDN::Ovn::ensure_port(
+ $controllerid, $controller, $vnet->{zone}, $vnetid, $iface,
+ );
+
+ # detach from any previous bridge
+ PVE::Network::tap_unplug($iface);
+ PVE::Network::SDN::Ovn::plug_port($iface, $firewall, $rate);
+
+ # Only fail after plugging: on hotplug, qemu-server already unplugged the
+ # interface, so failing earlier would leave the guest disconnected.
+ die "VLAN tags and trunks are not supported on OVN vnet '$vnetid'\n" if $tag || $trunks;
+}
+
+sub on_update_hook {
+ my ($class, $zoneid, $zone_cfg, $controller_cfg) = @_;
+
+ my $controllerid = $zone_cfg->{ids}->{$zoneid}->{controller};
+ my $controller = $controller_cfg->{ids}->{$controllerid};
+
+ die "controller $controllerid don't exist\n" if !$controller;
+ die "$controllerid is not an ovn controller type\n" if $controller->{type} ne 'ovn';
+}
+
+sub vnet_update_hook {
+ my ($class, $vnet_cfg, $vnetid, $zone_cfg) = @_;
+
+ my $vnet = $vnet_cfg->{ids}->{$vnetid};
+
+ raise_param_exc({ tag => "tag is not supported on OVN zones" }) if defined($vnet->{tag});
+ raise_param_exc({ vlanaware => "vlanaware is not supported on OVN zones" })
+ if $vnet->{vlanaware};
+ raise_param_exc({ 'isolate-ports' => "isolate-ports is not supported on OVN zones" })
+ if $vnet->{'isolate-ports'};
+}
+
+sub get_mtu {
+ my ($class, $plugin_config) = @_;
+
+ return $plugin_config->{mtu} // $DEFAULT_MTU;
+}
+
+1;
diff --git a/src/test/run_test_ovn.pl b/src/test/run_test_ovn.pl
index 60e752b..411c60e 100755
--- a/src/test/run_test_ovn.pl
+++ b/src/test/run_test_ovn.pl
@@ -8,7 +8,138 @@ use lib qw(..);
use Test::More;
use PVE::Network::SDN;
+use PVE::Network::SDN::Ovn;
use PVE::Network::SDN::Controllers::OvnPlugin;
+use PVE::Network::SDN::Zones::OvnPlugin;
+
+my $controller = {
+ type => 'ovn',
+ 'nb-connection' => 'tcp:192.168.0.1:6641,tcp:192.168.0.2:6641',
+ 'sb-connection' => 'tcp:192.168.0.1:6642,tcp:192.168.0.2:6642',
+};
+
+my $running_cfg = {
+ controllers => {
+ ids => {
+ ovnctl => $controller,
+ evpnctl => { type => 'evpn', asn => 65000, peers => '192.168.0.1' },
+ },
+ },
+ zones => {
+ ids => {
+ zone1 => { type => 'ovn', controller => 'ovnctl' },
+ zone2 => {
+ type => 'ovn',
+ controller => 'ovnctl',
+ nodes => { node2 => 1 },
+ },
+ evpn => { type => 'evpn', controller => 'evpnctl' },
+ },
+ },
+ vnets => {
+ ids => {
+ vnet1 => { type => 'vnet', zone => 'zone1' },
+ vnet2 => { type => 'vnet', zone => 'zone2' },
+ evpnvnet => { type => 'vnet', zone => 'evpn', tag => 100 },
+ },
+ },
+};
+
+sub ext {
+ my ($zone, $vnet, $vmid) = @_;
+
+ my $ext = {
+ 'pve-sdn-controller' => 'ovnctl',
+ 'pve-sdn-zone' => $zone,
+ 'pve-sdn-vnet' => $vnet,
+ };
+ $ext->{'pve-sdn-vmid'} = $vmid if defined($vmid);
+ return $ext;
+}
+
+# ports
+
+is_deeply(
+ PVE::Network::SDN::Ovn::port_external_ids('ovnctl', 'zone1', 'vnet1', 'veth105i1'),
+ ext('zone1', 'vnet1', 105),
+ 'port_external_ids: container interface',
+);
+is_deeply(
+ PVE::Network::SDN::Ovn::port_external_ids('ovnctl', 'zone1', 'vnet1', 'custom0'),
+ ext('zone1', 'vnet1'),
+ 'port_external_ids: no guest interface',
+);
+
+my $port_ext = ext('zone1', 'vnet1', 100);
+my @port_tail = (
+ [
+ 'set',
+ 'Logical_Switch_Port',
+ 'tap100i0',
+ 'external_ids:pve-sdn-controller=ovnctl',
+ 'external_ids:pve-sdn-vmid=100',
+ 'external_ids:pve-sdn-vnet=vnet1',
+ 'external_ids:pve-sdn-zone=zone1',
+ ],
+);
+
+is_deeply(
+ PVE::Network::SDN::Ovn::port_commands('vnet1', 'tap100i0', $port_ext, undef),
+ [['lsp-add', 'vnet1', 'tap100i0'], ['lsp-set-addresses', 'tap100i0', 'unknown'], @port_tail],
+ 'port_commands: new port',
+);
+is_deeply(
+ PVE::Network::SDN::Ovn::port_commands('vnet1', 'tap100i0', $port_ext, 'vnet1'),
+ [@port_tail],
+ 'port_commands: existing port keeps its addresses',
+);
+is_deeply(
+ PVE::Network::SDN::Ovn::port_commands('vnet1', 'tap100i0', $port_ext, 'vnet9'),
+ [
+ ['lsp-del', 'tap100i0'],
+ ['lsp-add', 'vnet1', 'tap100i0'],
+ ['lsp-set-addresses', 'tap100i0', 'unknown'],
+ @port_tail,
+ ],
+ 'port_commands: port moved to another vnet',
+);
+
+{
+ no warnings 'redefine';
+ local *PVE::Network::SDN::Ovn::run_command = sub {
+ my ($cmd, %param) = @_;
+ $param{errfunc}->('ovn-nbctl: tcp:192.168.0.1:6641: database connection failed');
+ die "command '@$cmd' failed: exit code 1\n";
+ };
+ eval {
+ PVE::Network::SDN::Ovn::ensure_port('ovnctl', $controller, 'zone1', 'vnet1',
+ 'tap100i0');
+ };
+}
+is(
+ $@,
+ "unable to create OVN logical switch port 'tap100i0' on 'vnet1' -"
+ . " ovn-nbctl: tcp:192.168.0.1:6641: database connection failed\n",
+ 'ensure_port: error of ovn-nbctl instead of its command line',
+);
+
+is(PVE::Network::SDN::Ovn::firewall_port_name('tap100i2'), 'fwln100o2', 'firewall_port_name: vm');
+is(
+ PVE::Network::SDN::Ovn::firewall_port_name('veth105i0'),
+ 'fwln105o0',
+ 'firewall_port_name: container',
+);
+
+# unsupported options
+
+eval {
+ PVE::Network::SDN::Zones::OvnPlugin->vnet_update_hook(
+ { ids => { vnet1 => { zone => 'zone1', 'isolate-ports' => 1 } } },
+ 'vnet1',
+ {},
+ );
+};
+like($@, qr/isolate-ports is not supported/, 'vnet_update_hook: isolate-ports refused');
# controller schema
diff --git a/src/test/zones/ovn/basic/expected_controller_config b/src/test/zones/ovn/basic/expected_controller_config
new file mode 100644
index 0000000..2961e18
--- /dev/null
+++ b/src/test/zones/ovn/basic/expected_controller_config
@@ -0,0 +1,8 @@
+frr version 10.6.1
+frr defaults datacenter
+hostname localhost
+log syslog informational
+service integrated-vtysh-config
+!
+line vty
+!
diff --git a/src/test/zones/ovn/basic/expected_sdn_interfaces b/src/test/zones/ovn/basic/expected_sdn_interfaces
new file mode 100644
index 0000000..e0086c1
--- /dev/null
+++ b/src/test/zones/ovn/basic/expected_sdn_interfaces
@@ -0,0 +1,12 @@
+#version:1
+
+auto myvnet
+iface myvnet
+ ovs_type OVSBridge
+ ovs_mtu 1442
+
+auto myvnet2
+iface myvnet2
+ ovs_type OVSBridge
+ ovs_mtu 1400
+ alias tenant2
diff --git a/src/test/zones/ovn/basic/interfaces b/src/test/zones/ovn/basic/interfaces
new file mode 100644
index 0000000..68b6a88
--- /dev/null
+++ b/src/test/zones/ovn/basic/interfaces
@@ -0,0 +1,5 @@
+auto vmbr0
+iface vmbr0 inet manual
+ bridge-ports eth0
+ bridge-stp off
+ bridge-fd 0
diff --git a/src/test/zones/ovn/basic/sdn_config b/src/test/zones/ovn/basic/sdn_config
new file mode 100644
index 0000000..1034a77
--- /dev/null
+++ b/src/test/zones/ovn/basic/sdn_config
@@ -0,0 +1,24 @@
+{
+ version => 1,
+ vnets => {
+ ids => {
+ myvnet => { type => "vnet", zone => "myzone" },
+ myvnet2 => { type => "vnet", zone => "myzone2", alias => "tenant2" },
+ },
+ },
+ zones => {
+ ids => {
+ myzone => { type => "ovn", controller => "ovnctl", ipam => "pve" },
+ myzone2 => { type => "ovn", controller => "ovnctl", ipam => "pve", mtu => 1400 },
+ },
+ },
+ controllers => {
+ ids => {
+ ovnctl => {
+ type => "ovn",
+ "nb-connection" => "tcp:192.168.0.1:6641,tcp:192.168.0.2:6641",
+ "sb-connection" => "tcp:192.168.0.1:6642,tcp:192.168.0.2:6642",
+ },
+ },
+ },
+}
diff --git a/src/test/zones/ovn/with_evpn/expected_controller_config b/src/test/zones/ovn/with_evpn/expected_controller_config
new file mode 100644
index 0000000..0c6cc09
--- /dev/null
+++ b/src/test/zones/ovn/with_evpn/expected_controller_config
@@ -0,0 +1,44 @@
+frr version 10.6.1
+frr defaults datacenter
+hostname localhost
+log syslog informational
+service integrated-vtysh-config
+!
+vrf vrf_myzone
+ vni 1000
+exit-vrf
+!
+router bgp 65000
+ bgp router-id 192.168.0.1
+ no bgp hard-administrative-reset
+ no bgp default ipv4-unicast
+ coalesce-time 1000
+ no bgp graceful-restart notification
+ neighbor VTEP peer-group
+ neighbor VTEP remote-as 65000
+ neighbor VTEP bfd
+ neighbor 192.168.0.2 peer-group VTEP
+ neighbor 192.168.0.3 peer-group VTEP
+ !
+ address-family l2vpn evpn
+ neighbor VTEP activate
+ neighbor VTEP route-map MAP_VTEP_IN in
+ neighbor VTEP route-map MAP_VTEP_OUT out
+ advertise-all-vni
+ exit-address-family
+exit
+!
+router bgp 65000 vrf vrf_myzone
+ bgp router-id 192.168.0.1
+ no bgp hard-administrative-reset
+ no bgp graceful-restart notification
+exit
+!
+route-map MAP_VTEP_IN permit 1
+exit
+!
+route-map MAP_VTEP_OUT permit 1
+exit
+!
+line vty
+!
diff --git a/src/test/zones/ovn/with_evpn/expected_sdn_interfaces b/src/test/zones/ovn/with_evpn/expected_sdn_interfaces
new file mode 100644
index 0000000..28eda33
--- /dev/null
+++ b/src/test/zones/ovn/with_evpn/expected_sdn_interfaces
@@ -0,0 +1,47 @@
+#version:1
+
+auto myvnet
+iface myvnet
+ address 10.0.0.1/24
+ hwaddress A2:1D:CB:1A:C0:8B
+ bridge_ports vxlan_myvnet
+ bridge_stp off
+ bridge_fd 0
+ mtu 1450
+ ip-forward on
+ arp-accept on
+ vrf vrf_myzone
+
+auto ovnvnet
+iface ovnvnet
+ ovs_type OVSBridge
+ ovs_mtu 1442
+
+auto vrf_myzone
+iface vrf_myzone
+ vrf-table auto
+ post-up ip route add vrf vrf_myzone unreachable default metric 4278198272
+
+auto vrfbr_myzone
+iface vrfbr_myzone
+ bridge-ports vrfvx_myzone
+ bridge_stp off
+ bridge_fd 0
+ mtu 1450
+ vrf vrf_myzone
+
+auto vrfvx_myzone
+iface vrfvx_myzone
+ vxlan-id 1000
+ vxlan-local-tunnelip 192.168.0.1
+ bridge-learning off
+ bridge-arp-nd-suppress on
+ mtu 1450
+
+auto vxlan_myvnet
+iface vxlan_myvnet
+ vxlan-id 100
+ vxlan-local-tunnelip 192.168.0.1
+ bridge-learning off
+ bridge-arp-nd-suppress on
+ mtu 1450
diff --git a/src/test/zones/ovn/with_evpn/interfaces b/src/test/zones/ovn/with_evpn/interfaces
new file mode 100644
index 0000000..66bb826
--- /dev/null
+++ b/src/test/zones/ovn/with_evpn/interfaces
@@ -0,0 +1,7 @@
+auto vmbr0
+iface vmbr0 inet static
+ address 192.168.0.1/24
+ gateway 192.168.0.254
+ bridge-ports eth0
+ bridge-stp off
+ bridge-fd 0
diff --git a/src/test/zones/ovn/with_evpn/sdn_config b/src/test/zones/ovn/with_evpn/sdn_config
new file mode 100644
index 0000000..505f928
--- /dev/null
+++ b/src/test/zones/ovn/with_evpn/sdn_config
@@ -0,0 +1,35 @@
+{
+ version => 1,
+ vnets => {
+ ids => {
+ myvnet => { tag => "100", type => "vnet", zone => "myzone" },
+ ovnvnet => { type => "vnet", zone => "ovnzone" },
+ },
+ },
+
+ zones => {
+ ids => {
+ myzone => { ipam => "pve", type => "evpn", controller => "evpnctl", 'vrf-vxlan' => 1000, 'mac' => 'A2:1D:CB:1A:C0:8B' },
+ ovnzone => { ipam => "pve", type => "ovn", controller => "ovnctl" },
+ },
+ },
+ controllers => {
+ ids => {
+ evpnctl => { type => "evpn", 'peers' => '192.168.0.1,192.168.0.2,192.168.0.3', asn => "65000" },
+ ovnctl => {
+ type => "ovn",
+ 'nb-connection' => 'tcp:192.168.0.1:6641',
+ 'sb-connection' => 'tcp:192.168.0.1:6642',
+ },
+ },
+ },
+
+ subnets => {
+ ids => { 'myzone-10.0.0.0-24' => {
+ 'type' => 'subnet',
+ 'vnet' => 'myvnet',
+ 'gateway' => '10.0.0.1',
+ }
+ }
+ }
+}
--
2.47.3
next prev parent reply other threads:[~2026-10-13 12:33 UTC|newest]
Thread overview: 18+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-09 9:22 [RFC] sdn: OVN zone and controller plugins Rémi PAETA
2026-10-13 12:14 ` [RFC PATCH ve-rs/network/manager/docs 00/16] sdn: add " Rémi PAETA
2026-10-13 12:14 ` [RFC PATCH ve-rs 01/16] ve-config: sdn: add ovn zone type Rémi PAETA
2026-10-13 12:14 ` SPAM: [RFC PATCH network 02/16] sdn: controllers: add ovn plugin Rémi PAETA
2026-10-13 12:14 ` Rémi PAETA [this message]
2026-10-13 12:14 ` [RFC PATCH network 04/16] sdn: ovn: sync the northbound database on reload Rémi PAETA
2026-10-13 12:14 ` [RFC PATCH network 05/16] sdn: ovn: configure the chassis on boot and on network reload Rémi PAETA
2026-10-13 12:14 ` [RFC PATCH network 06/16] sdn: ovn: add port security Rémi PAETA
2026-10-13 12:14 ` [RFC PATCH network 07/16] sdn: ovn: allow overriding the port security per vnet Rémi PAETA
2026-10-13 12:14 ` [RFC PATCH network 08/16] sdn: subnets: refuse gateway, snat and dhcp on ovn zones Rémi PAETA
2026-10-13 12:14 ` [RFC PATCH network 09/16] sdn: ovn: support ssl connections to the databases Rémi PAETA
2026-10-13 12:14 ` [RFC PATCH network 10/16] sdn: ovn: bind guest ports to the chassis of their node Rémi PAETA
2026-10-13 12:14 ` [RFC PATCH manager 11/16] ui: sdn: controller selector: allow filtering by controller type Rémi PAETA
2026-10-13 12:14 ` [RFC PATCH manager 12/16] ui: sdn: add ovn controller Rémi PAETA
2026-10-13 12:14 ` [RFC PATCH manager 13/16] ui: sdn: add ovn zone Rémi PAETA
2026-10-13 12:14 ` [RFC PATCH manager 14/16] ui: sdn: vnet: add port security for ovn zones Rémi PAETA
2026-10-13 12:14 ` [RFC PATCH manager 15/16] ui: sdn: vnet: disable isolate ports " Rémi PAETA
2026-10-13 12:14 ` [RFC PATCH docs 16/16] sdn: document ovn zones and controllers Rémi PAETA
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=20261013121432.2642979-4-remi.paeta@comaite.com \
--to=remi.paeta@comaite.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox