public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
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





  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
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal