From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 1F78B1FF146 for ; Tue, 09 Jun 2026 15:27:08 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 0043B129E8; Tue, 9 Jun 2026 15:26:19 +0200 (CEST) From: Hannes Laimer 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 Message-ID: <20260609132522.235917-8-h.laimer@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260609132522.235917-1-h.laimer@proxmox.com> References: <20260609132522.235917-1-h.laimer@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1781011483757 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.916 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment KAM_MAILER 2 Automated Mailer Tag Left in Email SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: OHQ3L6LQVUCBEAIQVZE2GNLKO5O3B6BO X-Message-ID-Hash: OHQ3L6LQVUCBEAIQVZE2GNLKO5O3B6BO X-MailFrom: h.laimer@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Signed-off-by: Hannes Laimer --- 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