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 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.