From: Hannes Laimer <h.laimer@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH pve-network 07/16] sdn: microseg: add config and API
Date: Tue, 9 Jun 2026 15:25:13 +0200 [thread overview]
Message-ID: <20260609132522.235917-8-h.laimer@proxmox.com> (raw)
In-Reply-To: <20260609132522.235917-1-h.laimer@proxmox.com>
Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
src/PVE/API2/Network/SDN.pm | 12 +
src/PVE/API2/Network/SDN/Makefile | 2 +
src/PVE/API2/Network/SDN/Microseg.pm | 126 +++++++
.../API2/Network/SDN/Microseg/Assignment.pm | 163 +++++++++
src/PVE/API2/Network/SDN/Microseg/Bridge.pm | 171 ++++++++++
src/PVE/API2/Network/SDN/Microseg/Group.pm | 171 ++++++++++
src/PVE/API2/Network/SDN/Microseg/Makefile | 8 +
src/PVE/API2/Network/SDN/Microseg/Rule.pm | 163 +++++++++
src/PVE/Network/SDN.pm | 5 +
src/PVE/Network/SDN/Makefile | 1 +
src/PVE/Network/SDN/Microseg.pm | 316 ++++++++++++++++++
11 files changed, 1138 insertions(+)
create mode 100644 src/PVE/API2/Network/SDN/Microseg.pm
create mode 100644 src/PVE/API2/Network/SDN/Microseg/Assignment.pm
create mode 100644 src/PVE/API2/Network/SDN/Microseg/Bridge.pm
create mode 100644 src/PVE/API2/Network/SDN/Microseg/Group.pm
create mode 100644 src/PVE/API2/Network/SDN/Microseg/Makefile
create mode 100644 src/PVE/API2/Network/SDN/Microseg/Rule.pm
create mode 100644 src/PVE/Network/SDN/Microseg.pm
diff --git a/src/PVE/API2/Network/SDN.pm b/src/PVE/API2/Network/SDN.pm
index e3c8d9d..5a31e6f 100644
--- a/src/PVE/API2/Network/SDN.pm
+++ b/src/PVE/API2/Network/SDN.pm
@@ -22,6 +22,7 @@ use PVE::API2::Network::SDN::Zones;
use PVE::API2::Network::SDN::Ipams;
use PVE::API2::Network::SDN::Dns;
use PVE::API2::Network::SDN::Fabrics;
+use PVE::API2::Network::SDN::Microseg;
use PVE::API2::Network::SDN::PrefixLists;
use PVE::API2::Network::SDN::RouteMaps;
@@ -57,6 +58,11 @@ __PACKAGE__->register_method({
path => 'fabrics',
});
+__PACKAGE__->register_method({
+ subclass => "PVE::API2::Network::SDN::Microseg",
+ path => 'microseg',
+});
+
__PACKAGE__->register_method({
subclass => "PVE::API2::Network::SDN::PrefixLists",
path => 'prefix-lists',
@@ -99,6 +105,7 @@ __PACKAGE__->register_method({
{ id => 'ipams' },
{ id => 'dns' },
{ id => 'fabrics' },
+ { id => 'microseg' },
{ id => 'prefix-lists' },
{ id => 'route-maps' },
];
@@ -271,6 +278,11 @@ __PACKAGE__->register_method({
PVE::RS::SDN::PrefixLists->running_config($prefix_list_config);
PVE::Network::SDN::PrefixLists::write_config($parsed_prefix_list_config);
+ my $microseg_config = $running_config->{microseg}->{ids} // {};
+ my $parsed_microseg_config =
+ PVE::RS::SDN::Microseg->running_config($microseg_config);
+ PVE::Network::SDN::Microseg::write_config($parsed_microseg_config);
+
PVE::Network::SDN::delete_global_lock() if $lock_token && $release_lock;
};
diff --git a/src/PVE/API2/Network/SDN/Makefile b/src/PVE/API2/Network/SDN/Makefile
index 6b91f8c..3ae11b0 100644
--- a/src/PVE/API2/Network/SDN/Makefile
+++ b/src/PVE/API2/Network/SDN/Makefile
@@ -6,6 +6,7 @@ SOURCES=Vnets.pm\
Dns.pm\
Ips.pm\
Fabrics.pm\
+ Microseg.pm\
PrefixLists.pm\
RouteMaps.pm
@@ -18,4 +19,5 @@ install:
make -C Nodes install
make -C RouteMaps install
make -C PrefixLists install
+ make -C Microseg install
diff --git a/src/PVE/API2/Network/SDN/Microseg.pm b/src/PVE/API2/Network/SDN/Microseg.pm
new file mode 100644
index 0000000..4c21593
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/Microseg.pm
@@ -0,0 +1,126 @@
+package PVE::API2::Network::SDN::Microseg;
+
+use strict;
+use warnings;
+
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::RPCEnvironment;
+
+use PVE::Network::SDN;
+use PVE::Network::SDN::Microseg;
+
+use PVE::API2::Network::SDN::Microseg::Group;
+use PVE::API2::Network::SDN::Microseg::Rule;
+use PVE::API2::Network::SDN::Microseg::Assignment;
+use PVE::API2::Network::SDN::Microseg::Bridge;
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method({
+ subclass => "PVE::API2::Network::SDN::Microseg::Group",
+ path => 'group',
+});
+
+__PACKAGE__->register_method({
+ subclass => "PVE::API2::Network::SDN::Microseg::Rule",
+ path => 'rule',
+});
+
+__PACKAGE__->register_method({
+ subclass => "PVE::API2::Network::SDN::Microseg::Assignment",
+ path => 'assignment',
+});
+
+__PACKAGE__->register_method({
+ subclass => "PVE::API2::Network::SDN::Microseg::Bridge",
+ path => 'bridge',
+});
+
+__PACKAGE__->register_method({
+ name => 'index',
+ path => '',
+ method => 'GET',
+ description => "Microseg index.",
+ permissions => {
+ check => ['perm', '/sdn', ['SDN.Audit']],
+ },
+ parameters => {
+ properties => {},
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => {
+ subdir => { type => 'string' },
+ },
+ },
+ links => [{ rel => 'child', href => "{subdir}" }],
+ },
+ code => sub {
+ return [
+ { subdir => 'group' },
+ { subdir => 'rule' },
+ { subdir => 'assignment' },
+ { subdir => 'bridge' },
+ { subdir => 'all' },
+ ];
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'all',
+ path => 'all',
+ method => 'GET',
+ description => "List every microseg object across all types, for the tree view.",
+ permissions => {
+ check => ['perm', '/sdn', ['SDN.Audit']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ running => {
+ type => 'boolean',
+ optional => 1,
+ description => "Display the running (committed) config.",
+ },
+ pending => {
+ type => 'boolean',
+ optional => 1,
+ description => "Display the pending config with change markers.",
+ },
+ },
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => {
+ id => { type => 'string', description => 'Object identifier.' },
+ type => {
+ type => 'string',
+ enum => ['group', 'rule', 'assignment', 'bridge'],
+ },
+ state => get_standard_option('pve-sdn-config-state'),
+ pending => {
+ type => 'object',
+ optional => 1,
+ description =>
+ 'Changes that have not yet been applied to the running configuration.',
+ },
+ },
+ },
+ links => [{ rel => 'child', href => "{id}" }],
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+ $rpcenv->check($rpcenv->get_user(), '/sdn', ['SDN.Audit']);
+
+ return PVE::Network::SDN::Microseg::list_objects($param, undef);
+ },
+});
+
+1;
diff --git a/src/PVE/API2/Network/SDN/Microseg/Assignment.pm b/src/PVE/API2/Network/SDN/Microseg/Assignment.pm
new file mode 100644
index 0000000..440e0e4
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/Microseg/Assignment.pm
@@ -0,0 +1,163 @@
+package PVE::API2::Network::SDN::Microseg::Assignment;
+
+use strict;
+use warnings;
+
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::RPCEnvironment;
+use PVE::Tools qw(extract_param);
+
+use PVE::Network::SDN::Microseg;
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method({
+ name => 'index',
+ path => '',
+ method => 'GET',
+ description => "List microseg NIC-to-group assignments.",
+ permissions => {
+ check => ['perm', '/sdn', ['SDN.Audit']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ running => {
+ type => 'boolean',
+ optional => 1,
+ description => "Display the running (committed) config.",
+ },
+ pending => {
+ type => 'boolean',
+ optional => 1,
+ description => "Display the pending config with change markers.",
+ },
+ },
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => {
+ id => get_standard_option('pve-sdn-microseg-id'),
+ },
+ },
+ links => [{ rel => 'child', href => "{id}" }],
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+ $rpcenv->check($rpcenv->get_user(), '/sdn', ['SDN.Audit']);
+
+ return PVE::Network::SDN::Microseg::list_objects($param, 'assignment');
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'read',
+ path => '{id}',
+ method => 'GET',
+ description => "Read one microseg assignment.",
+ permissions => {
+ check => ['perm', '/sdn', ['SDN.Audit']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ id => get_standard_option('pve-sdn-microseg-id'),
+ },
+ },
+ returns => { type => 'object' },
+ code => sub {
+ my ($param) = @_;
+
+ return PVE::Network::SDN::Microseg::get_object('assignment',
+ extract_param($param, 'id'));
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'create',
+ protected => 1,
+ path => '',
+ method => 'POST',
+ description => "Create a microseg assignment. The id is derived from the vmid and iface.",
+ permissions => {
+ check => ['perm', '/sdn', ['SDN.Allocate']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ PVE::Network::SDN::Microseg::assignment_properties(0)->%*,
+ 'lock-token' => get_standard_option('pve-sdn-lock-token'),
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ PVE::Network::SDN::Microseg::create_object('assignment', $param);
+
+ return undef;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'update',
+ protected => 1,
+ path => '{id}',
+ method => 'PUT',
+ description => "Update a microseg assignment.",
+ permissions => {
+ check => ['perm', '/sdn', ['SDN.Allocate']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ id => get_standard_option('pve-sdn-microseg-id'),
+ PVE::Network::SDN::Microseg::assignment_properties(1)->%*,
+ digest => get_standard_option('pve-config-digest'),
+ 'lock-token' => get_standard_option('pve-sdn-lock-token'),
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ my $id = extract_param($param, 'id');
+ PVE::Network::SDN::Microseg::update_object('assignment', $id, $param);
+
+ return undef;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'delete',
+ protected => 1,
+ path => '{id}',
+ method => 'DELETE',
+ description => "Delete a microseg assignment.",
+ permissions => {
+ check => ['perm', '/sdn', ['SDN.Allocate']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ id => get_standard_option('pve-sdn-microseg-id'),
+ 'lock-token' => get_standard_option('pve-sdn-lock-token'),
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ my $id = extract_param($param, 'id');
+ PVE::Network::SDN::Microseg::delete_object('assignment', $id, $param);
+
+ return undef;
+ },
+});
+
+1;
diff --git a/src/PVE/API2/Network/SDN/Microseg/Bridge.pm b/src/PVE/API2/Network/SDN/Microseg/Bridge.pm
new file mode 100644
index 0000000..216cde4
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/Microseg/Bridge.pm
@@ -0,0 +1,171 @@
+package PVE::API2::Network::SDN::Microseg::Bridge;
+
+use strict;
+use warnings;
+
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::RPCEnvironment;
+use PVE::Tools qw(extract_param);
+
+use PVE::Network::SDN::Microseg;
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method({
+ name => 'index',
+ path => '',
+ method => 'GET',
+ description => "List microseg carrier bridges.",
+ permissions => {
+ check => ['perm', '/sdn', ['SDN.Audit']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ running => {
+ type => 'boolean',
+ optional => 1,
+ description => "Display the running (committed) config.",
+ },
+ pending => {
+ type => 'boolean',
+ optional => 1,
+ description => "Display the pending config with change markers.",
+ },
+ },
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => {
+ id => get_standard_option('pve-sdn-microseg-id'),
+ },
+ },
+ links => [{ rel => 'child', href => "{id}" }],
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+ $rpcenv->check($rpcenv->get_user(), '/sdn', ['SDN.Audit']);
+
+ return PVE::Network::SDN::Microseg::list_objects($param, 'bridge');
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'read',
+ path => '{id}',
+ method => 'GET',
+ description => "Read one microseg carrier bridge.",
+ permissions => {
+ check => ['perm', '/sdn', ['SDN.Audit']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ id => get_standard_option('pve-sdn-microseg-id'),
+ },
+ },
+ returns => { type => 'object' },
+ code => sub {
+ my ($param) = @_;
+
+ return PVE::Network::SDN::Microseg::get_object('bridge', extract_param($param, 'id'));
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'create',
+ protected => 1,
+ path => '',
+ method => 'POST',
+ description => "Create a microseg carrier bridge.",
+ permissions => {
+ check => ['perm', '/sdn', ['SDN.Allocate']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ id => get_standard_option('pve-sdn-microseg-id'),
+ PVE::Network::SDN::Microseg::bridge_properties(0)->%*,
+ 'lock-token' => get_standard_option('pve-sdn-lock-token'),
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ PVE::Network::SDN::Microseg::create_object('bridge', $param);
+
+ return undef;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'update',
+ protected => 1,
+ path => '{id}',
+ method => 'PUT',
+ description => "Update a microseg carrier bridge.",
+ permissions => {
+ check => ['perm', '/sdn', ['SDN.Allocate']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ id => get_standard_option('pve-sdn-microseg-id'),
+ PVE::Network::SDN::Microseg::bridge_properties(1)->%*,
+ delete => {
+ type => 'array',
+ optional => 1,
+ items => {
+ type => 'string',
+ enum => ['nodes'],
+ },
+ },
+ digest => get_standard_option('pve-config-digest'),
+ 'lock-token' => get_standard_option('pve-sdn-lock-token'),
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ my $id = extract_param($param, 'id');
+ PVE::Network::SDN::Microseg::update_object('bridge', $id, $param);
+
+ return undef;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'delete',
+ protected => 1,
+ path => '{id}',
+ method => 'DELETE',
+ description => "Delete a microseg carrier bridge.",
+ permissions => {
+ check => ['perm', '/sdn', ['SDN.Allocate']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ id => get_standard_option('pve-sdn-microseg-id'),
+ 'lock-token' => get_standard_option('pve-sdn-lock-token'),
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ my $id = extract_param($param, 'id');
+ PVE::Network::SDN::Microseg::delete_object('bridge', $id, $param);
+
+ return undef;
+ },
+});
+
+1;
diff --git a/src/PVE/API2/Network/SDN/Microseg/Group.pm b/src/PVE/API2/Network/SDN/Microseg/Group.pm
new file mode 100644
index 0000000..0cb4e74
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/Microseg/Group.pm
@@ -0,0 +1,171 @@
+package PVE::API2::Network::SDN::Microseg::Group;
+
+use strict;
+use warnings;
+
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::RPCEnvironment;
+use PVE::Tools qw(extract_param);
+
+use PVE::Network::SDN::Microseg;
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method({
+ name => 'index',
+ path => '',
+ method => 'GET',
+ description => "List microseg groups.",
+ permissions => {
+ check => ['perm', '/sdn', ['SDN.Audit']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ running => {
+ type => 'boolean',
+ optional => 1,
+ description => "Display the running (committed) config.",
+ },
+ pending => {
+ type => 'boolean',
+ optional => 1,
+ description => "Display the pending config with change markers.",
+ },
+ },
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => {
+ id => get_standard_option('pve-sdn-microseg-id'),
+ },
+ },
+ links => [{ rel => 'child', href => "{id}" }],
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+ $rpcenv->check($rpcenv->get_user(), '/sdn', ['SDN.Audit']);
+
+ return PVE::Network::SDN::Microseg::list_objects($param, 'group');
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'read',
+ path => '{id}',
+ method => 'GET',
+ description => "Read one microseg group.",
+ permissions => {
+ check => ['perm', '/sdn', ['SDN.Audit']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ id => get_standard_option('pve-sdn-microseg-id'),
+ },
+ },
+ returns => { type => 'object' },
+ code => sub {
+ my ($param) = @_;
+
+ return PVE::Network::SDN::Microseg::get_object('group', extract_param($param, 'id'));
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'create',
+ protected => 1,
+ path => '',
+ method => 'POST',
+ description => "Create a microseg group.",
+ permissions => {
+ check => ['perm', '/sdn', ['SDN.Allocate']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ id => get_standard_option('pve-sdn-microseg-id'),
+ PVE::Network::SDN::Microseg::group_properties(0)->%*,
+ 'lock-token' => get_standard_option('pve-sdn-lock-token'),
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ PVE::Network::SDN::Microseg::create_object('group', $param);
+
+ return undef;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'update',
+ protected => 1,
+ path => '{id}',
+ method => 'PUT',
+ description => "Update a microseg group.",
+ permissions => {
+ check => ['perm', '/sdn', ['SDN.Allocate']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ id => get_standard_option('pve-sdn-microseg-id'),
+ PVE::Network::SDN::Microseg::group_properties(1)->%*,
+ delete => {
+ type => 'array',
+ optional => 1,
+ items => {
+ type => 'string',
+ enum => ['comment', 'parent'],
+ },
+ },
+ digest => get_standard_option('pve-config-digest'),
+ 'lock-token' => get_standard_option('pve-sdn-lock-token'),
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ my $id = extract_param($param, 'id');
+ PVE::Network::SDN::Microseg::update_object('group', $id, $param);
+
+ return undef;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'delete',
+ protected => 1,
+ path => '{id}',
+ method => 'DELETE',
+ description => "Delete a microseg group.",
+ permissions => {
+ check => ['perm', '/sdn', ['SDN.Allocate']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ id => get_standard_option('pve-sdn-microseg-id'),
+ 'lock-token' => get_standard_option('pve-sdn-lock-token'),
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ my $id = extract_param($param, 'id');
+ PVE::Network::SDN::Microseg::delete_object('group', $id, $param);
+
+ return undef;
+ },
+});
+
+1;
diff --git a/src/PVE/API2/Network/SDN/Microseg/Makefile b/src/PVE/API2/Network/SDN/Microseg/Makefile
new file mode 100644
index 0000000..1dab1d3
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/Microseg/Makefile
@@ -0,0 +1,8 @@
+SOURCES=Group.pm Rule.pm Assignment.pm Bridge.pm
+
+
+PERL5DIR=${DESTDIR}/usr/share/perl5
+
+.PHONY: install
+install:
+ for i in ${SOURCES}; do install -D -m 0644 $$i ${PERL5DIR}/PVE/API2/Network/SDN/Microseg/$$i; done
diff --git a/src/PVE/API2/Network/SDN/Microseg/Rule.pm b/src/PVE/API2/Network/SDN/Microseg/Rule.pm
new file mode 100644
index 0000000..b06e338
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/Microseg/Rule.pm
@@ -0,0 +1,163 @@
+package PVE::API2::Network::SDN::Microseg::Rule;
+
+use strict;
+use warnings;
+
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::RPCEnvironment;
+use PVE::Tools qw(extract_param);
+
+use PVE::Network::SDN::Microseg;
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method({
+ name => 'index',
+ path => '',
+ method => 'GET',
+ description => "List microseg policy rules.",
+ permissions => {
+ check => ['perm', '/sdn', ['SDN.Audit']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ running => {
+ type => 'boolean',
+ optional => 1,
+ description => "Display the running (committed) config.",
+ },
+ pending => {
+ type => 'boolean',
+ optional => 1,
+ description => "Display the pending config with change markers.",
+ },
+ },
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => {
+ id => get_standard_option('pve-sdn-microseg-id'),
+ },
+ },
+ links => [{ rel => 'child', href => "{id}" }],
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+ $rpcenv->check($rpcenv->get_user(), '/sdn', ['SDN.Audit']);
+
+ return PVE::Network::SDN::Microseg::list_objects($param, 'rule');
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'read',
+ path => '{id}',
+ method => 'GET',
+ description => "Read one microseg policy rule.",
+ permissions => {
+ check => ['perm', '/sdn', ['SDN.Audit']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ id => get_standard_option('pve-sdn-microseg-id'),
+ },
+ },
+ returns => { type => 'object' },
+ code => sub {
+ my ($param) = @_;
+
+ return PVE::Network::SDN::Microseg::get_object('rule', extract_param($param, 'id'));
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'create',
+ protected => 1,
+ path => '',
+ method => 'POST',
+ description =>
+ "Create a microseg policy rule. The id is derived from the src and dst marks.",
+ permissions => {
+ check => ['perm', '/sdn', ['SDN.Allocate']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ PVE::Network::SDN::Microseg::rule_properties(0)->%*,
+ 'lock-token' => get_standard_option('pve-sdn-lock-token'),
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ PVE::Network::SDN::Microseg::create_object('rule', $param);
+
+ return undef;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'update',
+ protected => 1,
+ path => '{id}',
+ method => 'PUT',
+ description => "Update a microseg policy rule.",
+ permissions => {
+ check => ['perm', '/sdn', ['SDN.Allocate']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ id => get_standard_option('pve-sdn-microseg-id'),
+ PVE::Network::SDN::Microseg::rule_properties(1)->%*,
+ digest => get_standard_option('pve-config-digest'),
+ 'lock-token' => get_standard_option('pve-sdn-lock-token'),
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ my $id = extract_param($param, 'id');
+ PVE::Network::SDN::Microseg::update_object('rule', $id, $param);
+
+ return undef;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'delete',
+ protected => 1,
+ path => '{id}',
+ method => 'DELETE',
+ description => "Delete a microseg policy rule.",
+ permissions => {
+ check => ['perm', '/sdn', ['SDN.Allocate']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ id => get_standard_option('pve-sdn-microseg-id'),
+ 'lock-token' => get_standard_option('pve-sdn-lock-token'),
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ my $id = extract_param($param, 'id');
+ PVE::Network::SDN::Microseg::delete_object('rule', $id, $param);
+
+ return undef;
+ },
+});
+
+1;
diff --git a/src/PVE/Network/SDN.pm b/src/PVE/Network/SDN.pm
index 33a3cf3..4521465 100644
--- a/src/PVE/Network/SDN.pm
+++ b/src/PVE/Network/SDN.pm
@@ -25,6 +25,7 @@ use PVE::Network::SDN::Subnets;
use PVE::Network::SDN::Dhcp;
use PVE::Network::SDN::Frr;
use PVE::Network::SDN::Fabrics;
+use PVE::Network::SDN::Microseg;
use PVE::Network::SDN::RouteMaps;
use PVE::Network::SDN::PrefixLists;
@@ -212,6 +213,7 @@ sub compile_running_cfg {
my $controllers_cfg = PVE::Network::SDN::Controllers::config();
my $subnets_cfg = PVE::Network::SDN::Subnets::config();
my $fabrics_cfg = PVE::Network::SDN::Fabrics::config();
+ my $microseg_cfg = PVE::Network::SDN::Microseg::config();
my $route_maps_cfg = PVE::Network::SDN::RouteMaps::config();
my $prefix_lists_cfg = PVE::Network::SDN::PrefixLists::config();
@@ -220,6 +222,7 @@ sub compile_running_cfg {
my $controllers = { ids => $controllers_cfg->{ids} };
my $subnets = { ids => $subnets_cfg->{ids} };
my $fabrics = { ids => $fabrics_cfg->to_sections() };
+ my $microseg = { ids => $microseg_cfg->to_sections() };
my $route_maps = { ids => $route_maps_cfg->to_sections() };
my $prefix_lists = { ids => $prefix_lists_cfg->to_sections() };
@@ -230,6 +233,7 @@ sub compile_running_cfg {
controllers => $controllers,
subnets => $subnets,
fabrics => $fabrics,
+ microseg => $microseg,
'route-maps' => $route_maps,
'prefix-lists' => $prefix_lists,
};
@@ -253,6 +257,7 @@ sub has_pending_changes {
subnets => PVE::Network::SDN::Subnets::config(),
controllers => PVE::Network::SDN::Controllers::config(),
fabrics => { ids => PVE::Network::SDN::Fabrics::config()->to_sections() },
+ microseg => { ids => PVE::Network::SDN::Microseg::config()->to_sections() },
'route-maps' => { ids => PVE::Network::SDN::RouteMaps::config()->to_sections() },
'prefix-lists' => { ids => PVE::Network::SDN::PrefixLists::config()->to_sections() },
};
diff --git a/src/PVE/Network/SDN/Makefile b/src/PVE/Network/SDN/Makefile
index d0b4bce..3042bab 100644
--- a/src/PVE/Network/SDN/Makefile
+++ b/src/PVE/Network/SDN/Makefile
@@ -9,6 +9,7 @@ SOURCES=Vnets.pm\
Dhcp.pm\
Fabrics.pm\
Frr.pm\
+ Microseg.pm\
PrefixLists.pm\
RouteMaps.pm\
WireGuard.pm
diff --git a/src/PVE/Network/SDN/Microseg.pm b/src/PVE/Network/SDN/Microseg.pm
new file mode 100644
index 0000000..83f6a67
--- /dev/null
+++ b/src/PVE/Network/SDN/Microseg.pm
@@ -0,0 +1,316 @@
+package PVE::Network::SDN::Microseg;
+
+use strict;
+use warnings;
+
+use PVE::Cluster qw(cfs_register_file cfs_read_file cfs_write_file);
+use PVE::Exception qw(raise_param_exc);
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::Tools;
+
+use PVE::Network::SDN;
+use PVE::RS::SDN::Microseg;
+
+PVE::JSONSchema::register_format(
+ 'pve-sdn-microseg-id',
+ sub {
+ my ($id, $noerr) = @_;
+ if ($id !~ m/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,30}[a-zA-Z0-9]$/) {
+ return undef if $noerr;
+ die "microseg id '$id' contains illegal characters or is too long\n";
+ }
+ return $id;
+ },
+);
+
+PVE::JSONSchema::register_standard_option(
+ 'pve-sdn-microseg-id',
+ {
+ description => "The microseg object identifier.",
+ type => 'string',
+ format => 'pve-sdn-microseg-id',
+ minLength => 2,
+ maxLength => 32,
+ },
+);
+
+cfs_register_file('sdn/microseg.cfg', \&parse_microseg_config, \&write_microseg_config);
+
+sub parse_microseg_config {
+ my ($filename, $raw) = @_;
+ return $raw // '';
+}
+
+sub write_microseg_config {
+ my ($filename, $config) = @_;
+ return $config // '';
+}
+
+sub config {
+ my ($running) = @_;
+
+ if ($running) {
+ my $running_config = PVE::Network::SDN::running_config();
+ my $microseg_config = $running_config->{microseg}->{ids} // {};
+ return PVE::RS::SDN::Microseg->running_config($microseg_config);
+ }
+
+ my $microseg_config = cfs_read_file("sdn/microseg.cfg");
+ return PVE::RS::SDN::Microseg->config($microseg_config);
+}
+
+sub write_config {
+ my ($config) = @_;
+ cfs_write_file("sdn/microseg.cfg", $config->to_raw(), 1);
+}
+
+sub group_properties {
+ my ($update) = @_;
+
+ my $properties = {
+ comment => {
+ type => 'string',
+ optional => 1,
+ maxLength => 256,
+ description => "Free-form comment.",
+ },
+ parent => get_standard_option(
+ 'pve-sdn-microseg-id',
+ {
+ optional => 1,
+ description => "Parent group this group is nested under.",
+ },
+ ),
+ };
+
+ if (!$update) {
+ $properties->{mark} = {
+ type => 'integer',
+ minimum => 1,
+ maximum => 65535,
+ optional => 1,
+ description => "Numeric mark stamped into skb->mark and carried on the wire."
+ . " Auto-assigned if omitted.",
+ };
+ }
+
+ return $properties;
+}
+
+sub rule_properties {
+ my ($update) = @_;
+
+ my $properties = {
+ allow => {
+ type => 'boolean',
+ optional => $update,
+ description => "0 = deny, 1 = allow. No matching rule = deny (stateless).",
+ },
+ };
+
+ # src and dst define the rule's identity (its id is derived from their marks), so they are
+ # create-only. Change them by deleting and recreating the rule.
+ if (!$update) {
+ $properties->{src} = get_standard_option(
+ 'pve-sdn-microseg-id',
+ {
+ optional => 1,
+ description => "Source group. Omit to match unstamped traffic (mark 0).",
+ },
+ );
+ $properties->{dst} = get_standard_option(
+ 'pve-sdn-microseg-id', { description => "Destination group." },
+ );
+ }
+
+ return $properties;
+}
+
+sub assignment_properties {
+ my ($update) = @_;
+
+ my $properties = {
+ group => get_standard_option(
+ 'pve-sdn-microseg-id',
+ { optional => $update, description => "Group this NIC belongs to." },
+ ),
+ };
+
+ if (!$update) {
+ $properties->{vmid} = get_standard_option('pve-vmid');
+ $properties->{iface} = {
+ type => 'integer',
+ minimum => 0,
+ maximum => 31,
+ description => "Index N of the guest's netN interface.",
+ };
+ }
+
+ return $properties;
+}
+
+sub bridge_properties {
+ my ($update) = @_;
+
+ return {
+ nodes => get_standard_option(
+ 'pve-node-list',
+ {
+ optional => 1,
+ description => "Nodes this bridge applies on. Empty means all nodes.",
+ },
+ ),
+ };
+}
+
+# Shared CRUD helpers for the per-type API endpoints. Each takes the object's type and the API
+# parameter hash, so the per-type modules only declare their schema and delegate here.
+
+# Read one object, asserting it exists and is of the expected type, stamped with id and digest.
+sub get_object {
+ my ($type, $id) = @_;
+
+ my $config = config();
+ my $entry = $config->get($id);
+ raise_param_exc({ id => "microseg $type '$id' does not exist" })
+ if !$entry || $entry->{type} ne $type;
+
+ $entry->{id} = $id;
+ $entry->{digest} = $config->digest();
+
+ return $entry;
+}
+
+sub create_object {
+ my ($type, $param) = @_;
+
+ my $lock_token = PVE::Tools::extract_param($param, 'lock-token');
+ $param->{type} = $type;
+
+ PVE::Cluster::check_cfs_quorum();
+ mkdir("/etc/pve/sdn");
+
+ PVE::Network::SDN::lock_sdn_config(
+ sub {
+ my $config = config();
+ $config->create($param);
+ write_config($config);
+ },
+ "create sdn microseg $type failed",
+ $lock_token,
+ );
+}
+
+sub update_object {
+ my ($type, $id, $param) = @_;
+
+ my $digest = PVE::Tools::extract_param($param, 'digest');
+ my $lock_token = PVE::Tools::extract_param($param, 'lock-token');
+ $param->{type} = $type;
+
+ PVE::Network::SDN::lock_sdn_config(
+ sub {
+ my $config = config();
+ my $entry = $config->get($id);
+ raise_param_exc({ id => "microseg $type '$id' does not exist" })
+ if !$entry || $entry->{type} ne $type;
+
+ PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
+
+ $config->update($id, $param);
+ write_config($config);
+ },
+ "update sdn microseg $type failed",
+ $lock_token,
+ );
+}
+
+sub delete_object {
+ my ($type, $id, $param) = @_;
+
+ my $lock_token = PVE::Tools::extract_param($param, 'lock-token');
+
+ PVE::Network::SDN::lock_sdn_config(
+ sub {
+ my $config = config();
+ my $entry = $config->get($id);
+ raise_param_exc({ id => "microseg $type '$id' does not exist" })
+ if !$entry || $entry->{type} ne $type;
+
+ $config->delete($id);
+ write_config($config);
+ },
+ "delete sdn microseg $type failed",
+ $lock_token,
+ );
+}
+
+# Shared lister for the index endpoints: returns the objects as an arrayref, each stamped with its
+# id (and digest), honoring the pending / running view flags and an optional type filter.
+sub list_objects {
+ my ($param, $type) = @_;
+
+ my $ids;
+ my $digest;
+ if ($param->{pending}) {
+ my $running_cfg = PVE::Network::SDN::running_config();
+ my $config = config();
+ my $sections = { ids => $config->to_sections() };
+ my $pending = PVE::Network::SDN::pending_config($running_cfg, $sections, 'microseg');
+ $ids = $pending->{ids};
+ $digest = $config->digest();
+ } elsif ($param->{running}) {
+ my $running_cfg = PVE::Network::SDN::running_config();
+ $ids = $running_cfg->{microseg}->{ids} // {};
+ } else {
+ my $config = config();
+ $ids = $config->to_sections();
+ $digest = $config->digest();
+ }
+
+ my $res = [];
+ for my $id (sort keys %$ids) {
+ my $scfg = $ids->{$id};
+ next if defined $type && $scfg->{type} ne $type;
+ $scfg->{id} = $id;
+ $scfg->{digest} = $digest if defined $digest;
+ push @$res, $scfg;
+ }
+
+ return $res;
+}
+
+my $MICROSEG_AGENT = '/usr/libexec/proxmox/proxmox-ebpf';
+
+sub apply_interface {
+ my ($iface) = @_;
+
+ return if !-x $MICROSEG_AGENT;
+
+ # An assigned NIC that cannot be enforced must not come up, so a non-zero exit fails the plug
+ # and the task that triggered it. Capture the agent's output and put it in the error so that
+ # task shows why, instead of a bare exit code.
+ my $output = '';
+ my $collect = sub { $output .= "$_[0]\n" };
+ eval {
+ PVE::Tools::run_command(
+ [$MICROSEG_AGENT, 'apply', $iface],
+ outfunc => $collect,
+ errfunc => $collect,
+ );
+ };
+ if (my $err = $@) {
+ chomp $output;
+ die "microseg: refusing to bring up '$iface' unenforced\n"
+ . ($output ne '' ? "$output\n" : "$err");
+ }
+}
+
+sub apply_all {
+ return if !-x $MICROSEG_AGENT;
+
+ eval { PVE::Tools::run_command([$MICROSEG_AGENT, 'apply']); };
+ warn "microseg: failed to apply running config: $@" if $@;
+}
+
+1;
--
2.47.3
next prev parent reply other threads:[~2026-06-09 13:27 UTC|newest]
Thread overview: 17+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-09 13:25 [RFC cluster/docs/ifupdown2/manager/network/proxmox{-ebpf,-ve-rs,-perl-rs} 00/16] sdn: add microsegmentation support Hannes Laimer
2026-06-09 13:25 ` [PATCH proxmox-ebpf 01/16] agent: add userspace coordinator and stateless policy subsystem Hannes Laimer
2026-06-09 13:25 ` [PATCH proxmox-ebpf 02/16] bpf: add bridge subsystem Hannes Laimer
2026-06-09 13:25 ` [PATCH proxmox-ebpf 03/16] debian: add packaging and boot-time oneshot unit Hannes Laimer
2026-06-09 13:25 ` [PATCH proxmox-ve-rs 04/16] ve-config: sdn: add microseg config types Hannes Laimer
2026-06-09 13:25 ` [PATCH proxmox-perl-rs 05/16] sdn: add microseg config binding Hannes Laimer
2026-06-09 13:25 ` [PATCH pve-cluster 06/16] cfs: add 'sdn/microseg.cfg' to observed files Hannes Laimer
2026-06-09 13:25 ` Hannes Laimer [this message]
2026-06-09 13:25 ` [PATCH pve-network 08/16] sdn: zones: trigger microseg apply on tap_plug Hannes Laimer
2026-06-09 13:25 ` [PATCH pve-network 09/16] sdn: zones: add vxlan-gbp option to vxlan and evpn zones Hannes Laimer
2026-06-09 13:25 ` [PATCH pve-network 10/16] evpn: disable vxlan-learning on create if GBP is enabled Hannes Laimer
2026-06-09 13:25 ` [PATCH pve-manager 11/16] ui: sdn: add microsegmentation Hannes Laimer
2026-06-09 13:25 ` [PATCH pve-manager 12/16] network: apply microseg state on reload Hannes Laimer
2026-06-09 13:25 ` [PATCH pve-manager 13/16] ui: sdn: zones: add vxlan-gbp checkbox to vxlan and evpn Hannes Laimer
2026-06-09 13:25 ` [PATCH pve-docs 14/16] sdn: add microsegmentation section Hannes Laimer
2026-06-09 13:25 ` [PATCH pve-docs 15/16] sdn: add VXLAN-GBP flag to evpn/vxlan zone sections Hannes Laimer
2026-06-09 13:25 ` [PATCH ifupdown2 16/16] d/patches: add support for VXLAN-GBP flag Hannes Laimer
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=20260609132522.235917-8-h.laimer@proxmox.com \
--to=h.laimer@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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox