From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [45.144.208.40]) by lore.proxmox.com (Postfix) with ESMTPS id 60D831FF0E6 for ; Fri, 24 Jul 2026 16:33:27 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id B6EEB2151C; Fri, 24 Jul 2026 16:33:07 +0200 (CEST) From: David Riley To: pve-devel@lists.proxmox.com Subject: [PATCH pve-manager v3 10/12] fix #7294: api: pool: add SDN VNets as pool members Date: Fri, 24 Jul 2026 16:25:26 +0200 Message-ID: <20260724142528.109453-11-d.riley@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260724142528.109453-1-d.riley@proxmox.com> References: <20260724142528.109453-1-d.riley@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1784903521872 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.087 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_LOW -0.7 Sender listed at https://www.dnswl.org/, low trust 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: CFGNGRIDBMZ4B4BJPJX2EIKNOQWWIRQH X-Message-ID-Hash: CFGNGRIDBMZ4B4BJPJX2EIKNOQWWIRQH X-MailFrom: d.riley@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: Extend the pool API to accept SDN VNets as pool members under the 'network' parameter. Accept comma-separated lists of network resources for VNets. These can include optional VLAN tags or a wildcard (*) to propagate access to all untagged and tagged traffic. Unlike VMs or containers which strictly belong to a single pool, VNets are shared similar to storage. A single VNet can be assigned to multiple pools simultaneously, allowing cross-team usage without management conflicts. Enforce a cluster-wide version check before allowing network assignments. This prevents older nodes from accidentally overwriting the newly structured pool configurations. Suggested-by: Daniel Kral Signed-off-by: David Riley Link: https://bugzilla.proxmox.com/show_bug.cgi?id=7294 --- PVE/API2/Pool.pm | 152 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 147 insertions(+), 5 deletions(-) diff --git a/PVE/API2/Pool.pm b/PVE/API2/Pool.pm index 63aff5bb..f34ca609 100644 --- a/PVE/API2/Pool.pm +++ b/PVE/API2/Pool.pm @@ -7,7 +7,12 @@ use PVE::AccessControl; use PVE::Cluster qw (cfs_read_file cfs_write_file); use PVE::Exception qw(raise_param_exc); use PVE::INotify; +use PVE::JSONSchema; +use PVE::Network; +use PVE::Network::SDN::Vnets; +use PVE::Network::SDN::Zones; use PVE::Storage; +use PVE::Tools; use PVE::SafeSyslog; @@ -16,6 +21,42 @@ use PVE::RESTHandler; use base qw(PVE::RESTHandler); +use constant NETWORK_VNET_REGEX => qr!^vnet/([^/]+)/([^/]+)(?:/([0-9]+|\*))?$!; + +sub pve_verify_pool_network { + my ($value, $noerr) = @_; + + if ($value =~ NETWORK_VNET_REGEX) { + my ($zone, $vnet, $tag) = ($1, $2, $3); + + eval { PVE::JSONSchema::check_format('pve-sdn-zone-id', $zone) }; + if ($@) { + return undef if $noerr; + die "invalid SDN zone ID '$zone'\n"; + } + + eval { PVE::JSONSchema::check_format('pve-sdn-vnet-id', $vnet) }; + if ($@) { + return undef if $noerr; + die "invalid SDN vnet ID '$vnet'\n"; + } + + if (defined($tag) && $tag ne '*') { + if ($tag < 1 || $tag > 4094) { + return undef if $noerr; + die "vlan tag '$tag' out of range (1-4094)\n"; + } + } + + return $value; + } + + return undef if $noerr; + die "value '$value' is not a valid pool network vnet string (//[/|*])\n"; +} + +PVE::JSONSchema::register_format('pve-pool-network', \&pve_verify_pool_network); + __PACKAGE__->register_method({ name => 'index', path => '', @@ -36,7 +77,7 @@ __PACKAGE__->register_method({ }, type => { type => 'string', - enum => ['qemu', 'lxc', 'storage'], + enum => ['qemu', 'lxc', 'storage', 'network'], optional => 1, requires => 'poolid', }, @@ -61,7 +102,7 @@ __PACKAGE__->register_method({ properties => { type => { type => 'string', - enum => ['qemu', 'lxc', 'openvz', 'storage'], + enum => ['qemu', 'lxc', 'openvz', 'storage', 'network'], }, id => { type => 'string', @@ -135,6 +176,27 @@ __PACKAGE__->register_method({ } } + if (!defined($param->{type}) || $param->{type} eq 'network') { + for my $net_key (sort keys $pool_config->{network}->%*) { + my ($type, @path) = split('/', $net_key); + + if ($type eq 'vnet') { + my ($zoneid, $vnet, $tag) = @path; + + my $description = "$vnet ($zoneid)"; + $description = "$vnet.$tag ($zoneid)" if defined($tag); + + push @$members, + { + type => 'network', + 'network-type' => $type, + id => $net_key, + text => $description, + }; + } + } + } + my $pool_info = { members => $members, }; @@ -243,6 +305,13 @@ __PACKAGE__->register_method({ format => 'pve-storage-id-list', optional => 1, }, + network => { + description => 'Network resource to add or remove from this pool.', + type => 'string', + typetext => '//[/|*]', + format => 'pve-pool-network-list', + optional => 1, + }, 'allow-move' => { description => 'Allow adding a guest even if already in another pool.' . ' The guest will be removed from its current pool and added to this one.', @@ -295,6 +364,13 @@ __PACKAGE__->register_method({ format => 'pve-storage-id-list', optional => 1, }, + network => { + description => 'Network resource to add or remove from this pool.', + type => 'string', + typetext => '//[/|*]', + format => 'pve-pool-network-list', + optional => 1, + }, 'allow-move' => { description => 'Allow adding a guest even if already in another pool.' . ' The guest will be removed from its current pool and added to this one.', @@ -304,7 +380,7 @@ __PACKAGE__->register_method({ }, delete => { description => - 'Remove the passed VMIDs and/or storage IDs instead of adding them.', + 'Remove the passed VMIDs, storage IDs and/or network resource instead of adding them.', type => 'boolean', optional => 1, default => 0, @@ -373,6 +449,64 @@ __PACKAGE__->register_method({ } } + if (defined($param->{network})) { + # FIXME: MAJOR VERSION: 10.0 remove gatekeeper + # gatekeep vnet as pool members + PVE::Cluster::assert_min_cluster_version(9, 2, 5); + + my $network_param = $param->{network}; + my $zones_cfg = PVE::Network::SDN::Zones::config(); + my $vnets_cfg = PVE::Network::SDN::Vnets::config(); + + for my $network (PVE::Tools::split_list($network_param)) { + if ($network =~ NETWORK_VNET_REGEX) { + my ($zone, $vnetid, $tag) = ($1, $2, $3); + + die "SDN Zone '$zone' does not exist\n" + if !$zones_cfg->{ids}->{$zone}; + + my $vnet_data = $vnets_cfg->{ids}->{$vnetid} + or die "VNet '$vnetid' does not exist\n"; + + my $vnet_zone = $vnet_data->{zone}; + if ($zone ne $vnet_zone) { + die + "VNet '$vnetid' does not belong to zone '$zone' (it belongs to '$vnet_zone')\n"; + } + + my $network_key = "vnet/$vnet_zone/$vnetid"; + my $acl_path = "/sdn/zones/$vnet_zone/$vnetid"; + + if (defined($tag)) { + if (!$vnet_data->{vlanaware}) { + die + "VNet '$vnetid' is not VLAN-aware, cannot assign a specific tag\n"; + } + $network_key .= "/$tag"; + $acl_path .= "/$tag" if $tag ne "*"; + } + + $rpcenv->check_perm_modify( + $authuser, $acl_path, ['SDN.Allocate'], + ); + + if ($param->{delete}) { + die "Network resource '$network_key' is not a pool member\n" + if !$pool_config->{network}->{$network_key}; + + delete $pool_config->{network}->{$network_key}; + } else { + die + "Network resource '$network_key' is already a pool member\n" + if ($pool_config->{network}->{$network_key}); + + $pool_config->{network}->{$network_key} = 1; + } + + } + } + } + cfs_write_file("user.cfg", $usercfg); }, "update pools failed", @@ -400,7 +534,7 @@ __PACKAGE__->register_method({ }, type => { type => 'string', - enum => ['qemu', 'lxc', 'storage'], + enum => ['qemu', 'lxc', 'storage', 'network'], optional => 1, }, }, @@ -421,7 +555,7 @@ __PACKAGE__->register_method({ properties => { type => { type => 'string', - enum => ['qemu', 'lxc', 'openvz', 'storage'], + enum => ['qemu', 'lxc', 'openvz', 'storage', 'network'], }, id => { type => 'string', @@ -524,6 +658,14 @@ __PACKAGE__->register_method({ die "pool '$pool' is not empty (contains storage '$storeid')\n"; } + for my $netid (sort keys $pool_config->{network}->%*) { + my ($type, $id) = split('/', $netid, 2); + $type = 'network' if !defined($type); + $id = $netid if !defined($id); + + die "pool '$pool' is not empty (contains $type '$id')\n"; + } + delete($usercfg->{pools}->{$pool}); PVE::AccessControl::delete_pool_acl($pool, $usercfg); -- 2.47.3