* [PATCH access-control/cluster/common/manager/network/proxmox-widget-toolkit/qemu-server v2 00/10] fix #7294: pool: add SDN VNets as pool members
@ 2026-06-26 13:10 David Riley
2026-06-26 13:10 ` [PATCH pve-manager v2 01/10] ui: replace var with let to match style guide for variable declaration David Riley
` (10 more replies)
0 siblings, 11 replies; 23+ messages in thread
From: David Riley @ 2026-06-26 13:10 UTC (permalink / raw)
To: pve-devel
This series implements support for adding SDN VNets to resource pools,
resolving #7294 [0]. This series depends on the v4 'fix #7520: sdn:
prune orphaned ACLs and handle VNet migrations' [1].
It does not, however, add zones as pool members as requested in #7294.
Zones currently share ACL paths for managing the zone itself and
allocating VNets within it. This makes self-service VNet management
without also granting zone management (and its associated
side-effects) difficult.
This patch series extends the pool section in the user.cfg and
introduces a new network property to the pool configuration which will
hold VNet entries:
* vnet/<zone>/<vnet>
* vnet/<zone>/<vnet>/<vlan>
The type prefix allows future extension to other network resource
types.
To prevent potential data loss from overwriting newly added VNets, a
cluster-version check is added which ensures all nodes are running a
version that supports this feature. Note: The hardcoded version guard
should be updated to match the final target release when being
applied.
The existing version check helpers were moved from `qemu-server` to a
new module within `pve-cluster` to make them available for this
implementation, and any future developments that require gatekeeping.
Appropriate attribution has been included for the relocated code.
Please let me know if this organizational move aligns with current
design preferences or additional adjustments are needed.
---
Thanks @Gabriel, @Daniel K. and @Jakob for the feedback.
Differences from v1:
- Access: Fix permissions propagation.
Pool ACL paths are setup without propagation, therefore checking
/sdn/zones/<zone>/<vnet>/<tag> fails even if the user has the
permission for the base path /sdn/zones/<zone>/<vnet>.
To allow this, the roles of the base VNet path are looked up if
the exact tagged path is not found in the pool (see patch 5/9).
- API: Add a unified property string format for VNets
(zone=<zone>,vnet=<vnet>,tag=<tag>]), ensuring that zone and vnet
are strictly required and coupled.
- API CLI: Add typetext to the vnet format for better error messages
in the CLI
- API: The registered format validation for VNets and Zones now also
check the length.
- API: Add membership checks for network resources during add/delete
operations (matching storage/VM behavior).
- Relocated version helpers from PVE::Cluster to PVE::Tools
- UI: Fix light mode. The icons are now light grey to match the
storage and vm icons.
- Series is now based on v4 instead of v3 of [1]
- Minor refactors
[0] https://bugzilla.proxmox.com/show_bug.cgi?id=7294
[1] https://lore.proxmox.com/pve-devel/20260626105258.56914-1-d.riley@proxmox.com/
pve-manager:
David Riley (3):
ui: replace var with let to match style guide for variable declaration
fix #7294: api: pool: add SDN VNets as pool members
fix #7294: ui: pool: add SDN VNets as pool members
PVE/API2/Pool.pm | 135 +++++++++++++++++++++++++--
www/css/ext6-pve.css | 15 +++
www/manager6/Utils.js | 1 +
www/manager6/grid/PoolMembers.js | 151 ++++++++++++++++++++++++++++---
4 files changed, 282 insertions(+), 20 deletions(-)
proxmox-widget-toolkit:
David Riley (1):
fix #7294: css: theme: add opacity override for pool VNet icon
src/proxmox-dark/scss/other/_icons.scss | 12 ++++++++++++
1 file changed, 12 insertions(+)
pve-access-control:
David Riley (1):
fix #7294: acl: pool: add SDN VNets as pool members
src/PVE/AccessControl.pm | 93 ++++++++++++++++++++++++++++++++++++---
src/PVE/RPCEnvironment.pm | 68 ++++++++++++++++++++++++++--
src/test/parser_writer.pl | 53 ++++++++++++++++++----
3 files changed, 198 insertions(+), 16 deletions(-)
pve-network:
David Riley (2):
fix #7294: sdn: register api formats for zones and vnets
fix #7294: sdn: vnet: update pool members on vnet migration and
deletion
src/PVE/Network/SDN.pm | 15 +++++++++++++++
src/PVE/Network/SDN/VnetPlugin.pm | 25 ++++++++++++++++++++++---
src/PVE/Network/SDN/Zones/Plugin.pm | 25 ++++++++++++++++++++++---
3 files changed, 59 insertions(+), 6 deletions(-)
pve-common:
David Riley (1):
tools: add helpers for version comparison
src/PVE/Tools.pm | 41 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 41 insertions(+)
pve-cluster:
David Riley (1):
fix #7294: cluster: helpers: add cluster-wide version assertion
src/PVE/Cluster.pm | 43 +++++++++++++++++++++++++++++++++++++++++--
1 file changed, 41 insertions(+), 2 deletions(-)
qemu-server:
David Riley (1):
fix #7294: helpers: use cluster-wide version helper
src/PVE/QemuMigrate.pm | 3 ++-
src/PVE/QemuServer/Helpers.pm | 42 ++---------------------------------
2 files changed, 4 insertions(+), 41 deletions(-)
Summary over all repositories:
15 files changed, 637 insertions(+), 85 deletions(-)
--
Generated by murpp 0.11.0
^ permalink raw reply [flat|nested] 23+ messages in thread
* [PATCH pve-manager v2 01/10] ui: replace var with let to match style guide for variable declaration
2026-06-26 13:10 [PATCH access-control/cluster/common/manager/network/proxmox-widget-toolkit/qemu-server v2 00/10] fix #7294: pool: add SDN VNets as pool members David Riley
@ 2026-06-26 13:10 ` David Riley
2026-07-03 13:14 ` Daniel Kral
2026-06-26 13:10 ` [PATCH pve-manager v2 02/10] fix #7294: api: pool: add SDN VNets as pool members David Riley
` (9 subsequent siblings)
10 siblings, 1 reply; 23+ messages in thread
From: David Riley @ 2026-06-26 13:10 UTC (permalink / raw)
To: pve-devel
No semantic change intended.
Link: https://pve.proxmox.com/wiki/Javascript_Style_Guide#Variables
Signed-off-by: David Riley <d.riley@proxmox.com>
---
www/manager6/grid/PoolMembers.js | 28 ++++++++++++++--------------
1 file changed, 14 insertions(+), 14 deletions(-)
diff --git a/www/manager6/grid/PoolMembers.js b/www/manager6/grid/PoolMembers.js
index b0c9ccb3..69f50e30 100644
--- a/www/manager6/grid/PoolMembers.js
+++ b/www/manager6/grid/PoolMembers.js
@@ -13,7 +13,7 @@ Ext.define('PVE.pool.AddVM', {
},
initComponent: function () {
- var me = this;
+ let me = this;
if (!me.pool) {
throw 'no pool specified';
@@ -23,7 +23,7 @@ Ext.define('PVE.pool.AddVM', {
me.method = 'PUT';
me.extraRequestParams.poolid = me.pool;
- var vmsField = Ext.create('Ext.form.field.Text', {
+ let vmsField = Ext.create('Ext.form.field.Text', {
name: 'vms',
hidden: true,
allowBlank: false,
@@ -32,7 +32,7 @@ Ext.define('PVE.pool.AddVM', {
let basicFilter = (data) =>
(data.type === 'lxc' || data.type === 'qemu') && data.pool !== me.pool;
- var vmStore = Ext.create('Ext.data.Store', {
+ let vmStore = Ext.create('Ext.data.Store', {
model: 'PVEResources',
sorters: [
{
@@ -43,7 +43,7 @@ Ext.define('PVE.pool.AddVM', {
filters: [(item) => basicFilter(item.data)],
});
- var vmGrid = Ext.create('widget.grid', {
+ let vmGrid = Ext.create('widget.grid', {
store: vmStore,
border: true,
height: 480,
@@ -53,7 +53,7 @@ Ext.define('PVE.pool.AddVM', {
mode: 'SIMPLE',
listeners: {
selectionchange: function (model, selected, opts) {
- var selectedVms = [];
+ let selectedVms = [];
selected.forEach(function (vm) {
selectedVms.push(vm.data.vmid);
});
@@ -127,7 +127,7 @@ Ext.define('PVE.pool.AddStorage', {
extend: 'Proxmox.window.Edit',
initComponent: function () {
- var me = this;
+ let me = this;
if (!me.pool) {
throw 'no pool specified';
@@ -166,7 +166,7 @@ Ext.define('PVE.grid.PoolMembers', {
stateId: 'grid-pool-members',
initComponent: function () {
- var me = this;
+ let me = this;
if (!me.pool) {
throw 'no pool specified';
@@ -193,7 +193,7 @@ Ext.define('PVE.grid.PoolMembers', {
],
});
- var coldef = PVE.data.ResourceStore.defaultColumns().filter(
+ let coldef = PVE.data.ResourceStore.defaultColumns().filter(
(c) => c.dataIndex !== 'tags' && c.dataIndex !== 'lock',
);
@@ -201,9 +201,9 @@ Ext.define('PVE.grid.PoolMembers', {
me.rstore.load();
};
- var sm = Ext.create('Ext.selection.RowModel', {});
+ let sm = Ext.create('Ext.selection.RowModel', {});
- var remove_btn = new Proxmox.button.Button({
+ let remove_btn = new Proxmox.button.Button({
text: gettext('Remove'),
disabled: true,
selModel: sm,
@@ -214,7 +214,7 @@ Ext.define('PVE.grid.PoolMembers', {
);
},
handler: function (btn, event, rec) {
- var params = { delete: 1, poolid: me.pool };
+ let params = { delete: 1, poolid: me.pool };
if (rec.data.type === 'storage') {
params.storage = rec.data.storage;
} else if (
@@ -254,7 +254,7 @@ Ext.define('PVE.grid.PoolMembers', {
text: gettext('Virtual Machine'),
iconCls: 'fa fa-desktop',
handler: function () {
- var win = Ext.create('PVE.pool.AddVM', { pool: me.pool });
+ let win = Ext.create('PVE.pool.AddVM', { pool: me.pool });
win.on('destroy', reload);
win.show();
},
@@ -263,7 +263,7 @@ Ext.define('PVE.grid.PoolMembers', {
text: gettext('Storage'),
iconCls: 'fa fa-hdd-o',
handler: function () {
- var win = Ext.create('PVE.pool.AddStorage', { pool: me.pool });
+ let win = Ext.create('PVE.pool.AddStorage', { pool: me.pool });
win.on('destroy', reload);
win.show();
},
@@ -280,7 +280,7 @@ Ext.define('PVE.grid.PoolMembers', {
listeners: {
itemcontextmenu: PVE.Utils.createCmdMenu,
itemdblclick: function (v, record) {
- var ws = me.up('pveStdWorkspace');
+ let ws = me.up('pveStdWorkspace');
ws.selectById(record.data.id);
},
activate: reload,
--
2.47.3
^ permalink raw reply related [flat|nested] 23+ messages in thread
* [PATCH pve-manager v2 02/10] fix #7294: api: pool: add SDN VNets as pool members
2026-06-26 13:10 [PATCH access-control/cluster/common/manager/network/proxmox-widget-toolkit/qemu-server v2 00/10] fix #7294: pool: add SDN VNets as pool members David Riley
2026-06-26 13:10 ` [PATCH pve-manager v2 01/10] ui: replace var with let to match style guide for variable declaration David Riley
@ 2026-06-26 13:10 ` David Riley
2026-07-03 13:19 ` Daniel Kral
2026-06-26 13:10 ` [PATCH pve-manager v2 03/10] fix #7294: ui: " David Riley
` (8 subsequent siblings)
10 siblings, 1 reply; 23+ messages in thread
From: David Riley @ 2026-06-26 13:10 UTC (permalink / raw)
To: pve-devel
Extend the pool API to accept SDN VNets and optional VLAN tags. Group
VNets under the new 'network' property type in the pool configuration.
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.
Link: https://bugzilla.proxmox.com/show_bug.cgi?id=7294
Signed-off-by: David Riley <d.riley@proxmox.com>
---
PVE/API2/Pool.pm | 135 ++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 129 insertions(+), 6 deletions(-)
diff --git a/PVE/API2/Pool.pm b/PVE/API2/Pool.pm
index 63aff5bb..6bd99abb 100644
--- a/PVE/API2/Pool.pm
+++ b/PVE/API2/Pool.pm
@@ -4,10 +4,12 @@ use strict;
use warnings;
use PVE::AccessControl;
-use PVE::Cluster qw (cfs_read_file cfs_write_file);
+use PVE::Cluster qw (cfs_read_file cfs_write_file assert_min_cluster_version);
use PVE::Exception qw(raise_param_exc);
use PVE::INotify;
+use PVE::Network;
use PVE::Storage;
+use PVE::Tools;
use PVE::SafeSyslog;
@@ -16,6 +18,26 @@ use PVE::RESTHandler;
use base qw(PVE::RESTHandler);
+my $pool_network_format = {
+ zone => {
+ description => 'SDN Zone',
+ type => 'string',
+ format => 'pve-sdn-zone-id',
+ },
+ vnet => {
+ description => 'VNet to add or remove from this pool.',
+ type => 'string',
+ format => 'pve-sdn-vnet-id',
+ },
+ tag => {
+ description => "Specify a VLAN tag",
+ optional => 1,
+ type => 'integer',
+ minimum => 1,
+ maximum => 4094,
+ },
+};
+
__PACKAGE__->register_method({
name => 'index',
path => '',
@@ -36,7 +58,7 @@ __PACKAGE__->register_method({
},
type => {
type => 'string',
- enum => ['qemu', 'lxc', 'storage'],
+ enum => ['qemu', 'lxc', 'storage', 'network'],
optional => 1,
requires => 'poolid',
},
@@ -61,7 +83,7 @@ __PACKAGE__->register_method({
properties => {
type => {
type => 'string',
- enum => ['qemu', 'lxc', 'openvz', 'storage'],
+ enum => ['qemu', 'lxc', 'openvz', 'storage', 'network'],
},
id => {
type => 'string',
@@ -135,6 +157,29 @@ __PACKAGE__->register_method({
}
}
+ if (!defined($param->{type}) || $param->{type} eq 'network') {
+ if ($pool_config->{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 +288,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 => 'zone=<zone>,vnet=<vnet>[,tag=<tag>]',
+ format => $pool_network_format,
+ 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 +347,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 => 'zone=<zone>,vnet=<vnet>[,tag=<tag>]',
+ format => $pool_network_format,
+ 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 +363,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 +432,62 @@ __PACKAGE__->register_method({
}
}
+ if (defined($param->{network})) {
+ my $vnet_entry = PVE::JSONSchema::parse_property_string(
+ $pool_network_format, $param->{network},
+ );
+
+ # gatekeep vnet as pool members
+ PVE::Cluster::assert_min_cluster_version(9, 2, 3);
+
+ my ($zone, $vnetid, $tag) = $vnet_entry->@{qw(zone vnet tag)};
+
+ my $zones_cfg = PVE::Network::SDN::Zones::config();
+ die "SDN Zone '$zone' does not exist\n" if !$zones_cfg->{ids}->{$zone};
+
+ my $vnets_cfg = PVE::Network::SDN::Vnets::config();
+
+ 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";
+
+ if (defined($tag)) {
+ if (!$vnet_data->{vlanaware}) {
+ die
+ "VNet '$vnetid' is not VLAN-aware, cannot assign a specific tag\n";
+ }
+
+ $network_key .= "/$tag";
+ }
+
+ $rpcenv->check_perm_modify(
+ $authuser,
+ "/sdn/zones/$vnet_zone/$vnetid",
+ ['SDN.Allocate'],
+ );
+
+ if ($param->{delete}) {
+ if (!$pool_config->{network}->{$network_key}) {
+ die "Network resource '$network_key' is not a pool member\n";
+ }
+
+ delete $pool_config->{network}->{$network_key};
+ } else {
+ if ($pool_config->{network}->{$network_key}) {
+ die "Network resource '$network_key' is already a pool member\n";
+ }
+
+ $pool_config->{network}->{$network_key} = 1;
+ }
+ }
+
cfs_write_file("user.cfg", $usercfg);
},
"update pools failed",
@@ -400,7 +515,7 @@ __PACKAGE__->register_method({
},
type => {
type => 'string',
- enum => ['qemu', 'lxc', 'storage'],
+ enum => ['qemu', 'lxc', 'storage', 'network'],
optional => 1,
},
},
@@ -421,7 +536,7 @@ __PACKAGE__->register_method({
properties => {
type => {
type => 'string',
- enum => ['qemu', 'lxc', 'openvz', 'storage'],
+ enum => ['qemu', 'lxc', 'openvz', 'storage', 'network'],
},
id => {
type => 'string',
@@ -524,6 +639,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
^ permalink raw reply related [flat|nested] 23+ messages in thread
* [PATCH pve-manager v2 03/10] fix #7294: ui: pool: add SDN VNets as pool members
2026-06-26 13:10 [PATCH access-control/cluster/common/manager/network/proxmox-widget-toolkit/qemu-server v2 00/10] fix #7294: pool: add SDN VNets as pool members David Riley
2026-06-26 13:10 ` [PATCH pve-manager v2 01/10] ui: replace var with let to match style guide for variable declaration David Riley
2026-06-26 13:10 ` [PATCH pve-manager v2 02/10] fix #7294: api: pool: add SDN VNets as pool members David Riley
@ 2026-06-26 13:10 ` David Riley
2026-07-06 14:17 ` Daniel Kral
2026-06-26 13:10 ` [PATCH proxmox-widget-toolkit v2 04/10] fix #7294: css: theme: add opacity override for pool VNet icon David Riley
` (7 subsequent siblings)
10 siblings, 1 reply; 23+ messages in thread
From: David Riley @ 2026-06-26 13:10 UTC (permalink / raw)
To: pve-devel
Add user interface to manage SDN VNets inside resource pools.
In the dialog window users can select an SDN zone, pick an associated
VNet, and optionally specify a VLAN tag.
The VNet selector only shows VNets of the selected zone to prevent
invalid configurations.
Link: https://bugzilla.proxmox.com/show_bug.cgi?id=7294
Signed-off-by: David Riley <d.riley@proxmox.com>
---
www/css/ext6-pve.css | 15 ++++
www/manager6/Utils.js | 1 +
www/manager6/grid/PoolMembers.js | 123 +++++++++++++++++++++++++++++++
3 files changed, 139 insertions(+)
diff --git a/www/css/ext6-pve.css b/www/css/ext6-pve.css
index 5c37dd29..3c383dfa 100644
--- a/www/css/ext6-pve.css
+++ b/www/css/ext6-pve.css
@@ -464,6 +464,21 @@ div.right-aligned {
content: " ";
}
+.x-fa-pool-net:before {
+ width: 14px;
+ height: 14px;
+ position: absolute;
+ left: 1px;
+ top: 1px;
+ opacity: 0.45;
+}
+
+.x-fa-pool-net-grid:before {
+ left: 14px;
+ top: 6px;
+ opacity: 0.65;
+}
+
.x-fa-treepanel:before {
width: 16px;
height: 24px;
diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index 040b5ae0..8f53dfb6 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1322,6 +1322,7 @@ Ext.define('PVE.Utils', {
const networkTypeMapping = {
fabric: 'fa fa-road',
zone: 'fa fa-th',
+ vnet: 'fa fa-network-wired x-fa-pool-net x-fa-pool-net-grid',
};
return networkTypeMapping[record['network-type']] ?? '';
diff --git a/www/manager6/grid/PoolMembers.js b/www/manager6/grid/PoolMembers.js
index 69f50e30..5aaa2b71 100644
--- a/www/manager6/grid/PoolMembers.js
+++ b/www/manager6/grid/PoolMembers.js
@@ -158,6 +158,108 @@ Ext.define('PVE.pool.AddStorage', {
},
});
+Ext.define('PVE.pool.AddVnet', {
+ extend: 'Proxmox.window.Edit',
+
+ viewModel: {
+ data: {
+ zone: '',
+ },
+ formulas: {
+ vnetDisabled: function (get) {
+ return !get('zone');
+ },
+ },
+ },
+
+ initComponent: function () {
+ let me = this;
+
+ if (!me.pool) {
+ throw 'no pool specified';
+ }
+
+ me.isCreate = true;
+ me.isAdd = true;
+ me.url = '/pools/';
+ me.method = 'PUT';
+ me.extraRequestParams.poolid = me.pool;
+
+ Ext.apply(me, {
+ subject: gettext('VNet'),
+ width: 350,
+ items: [
+ {
+ xtype: 'inputpanel',
+ onGetValues: function (values) {
+ let network = `zone=${values.zone},vnet=${values.vnet}`;
+
+ if (values.tag) {
+ network += `,tag=${values.tag}`;
+ }
+
+ delete values.zone;
+ delete values.vnet;
+ delete values.tag;
+
+ values.network = network;
+
+ return values;
+ },
+ items: [
+ {
+ xtype: 'pveSDNZoneSelector',
+ fieldLabel: gettext('Zone'),
+ name: 'zone',
+ allowBlank: false,
+ bind: {
+ value: '{zone}',
+ },
+ listeners: {
+ change: function (_f, _value) {
+ let vnetField = me.down('field[name=vnet]');
+ if (vnetField) {
+ vnetField.setValue('');
+ }
+ },
+ },
+ },
+ {
+ xtype: 'pveSDNVnetSelector',
+ fieldLabel: gettext('VNet'),
+ name: 'vnet',
+ allowBlank: false,
+ bind: {
+ disabled: '{vnetDisabled}',
+ },
+ listeners: {
+ beforequery: function (_queryPlan) {
+ let zone = me.getViewModel().get('zone');
+ let store = this.getStore();
+ store.clearFilter();
+ store.filter('zone', zone);
+ return true;
+ },
+ },
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'tag',
+ fieldLabel: gettext('VLAN Tag'),
+ minValue: 1,
+ maxValue: 4094,
+ allowBlank: true,
+ emptyText: gettext('All'),
+ },
+ ],
+ },
+ ],
+ });
+
+ me.callParent();
+ },
+});
+
Ext.define('PVE.grid.PoolMembers', {
extend: 'Ext.grid.GridPanel',
alias: ['widget.pvePoolMembers'],
@@ -223,6 +325,18 @@ Ext.define('PVE.grid.PoolMembers', {
rec.data.type === 'openvz'
) {
params.vms = rec.data.vmid;
+ } else if (rec.get('type') === 'network') {
+ if (rec.get('network-type') === 'vnet') {
+ let [_type, zone, vnet, tag] = rec.data.id.split('/');
+
+ let network = `zone=${zone},vnet=${vnet}`;
+
+ if (tag) {
+ network += `,tag=${tag}`;
+ }
+
+ params.network = network;
+ }
} else {
throw 'unknown resource type';
}
@@ -268,6 +382,15 @@ Ext.define('PVE.grid.PoolMembers', {
win.show();
},
},
+ {
+ text: gettext('VNet'),
+ iconCls: 'fa fa-network-wired x-fa-pool-net',
+ handler: function () {
+ let win = Ext.create('PVE.pool.AddVnet', { pool: me.pool });
+ win.on('destroy', reload);
+ win.show();
+ },
+ },
],
}),
},
--
2.47.3
^ permalink raw reply related [flat|nested] 23+ messages in thread
* [PATCH proxmox-widget-toolkit v2 04/10] fix #7294: css: theme: add opacity override for pool VNet icon
2026-06-26 13:10 [PATCH access-control/cluster/common/manager/network/proxmox-widget-toolkit/qemu-server v2 00/10] fix #7294: pool: add SDN VNets as pool members David Riley
` (2 preceding siblings ...)
2026-06-26 13:10 ` [PATCH pve-manager v2 03/10] fix #7294: ui: " David Riley
@ 2026-06-26 13:10 ` David Riley
2026-07-03 13:30 ` Daniel Kral
2026-06-26 13:10 ` [PATCH pve-access-control v2 05/10] fix #7294: acl: pool: add SDN VNets as pool members David Riley
` (6 subsequent siblings)
10 siblings, 1 reply; 23+ messages in thread
From: David Riley @ 2026-06-26 13:10 UTC (permalink / raw)
To: pve-devel
Add opacity adjustments for the `fa-network-wired` icon used by pool
networks.
The light/base theme lowers the icons opacity to simulate a light grey
color that matches other configuration elements. This override
restores the opacity to 1 in the dark theme so the native white SVG
remains clearly visible against the dark background.
Signed-off-by: David Riley <d.riley@proxmox.com>
---
src/proxmox-dark/scss/other/_icons.scss | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/src/proxmox-dark/scss/other/_icons.scss b/src/proxmox-dark/scss/other/_icons.scss
index ad08906..3712094 100644
--- a/src/proxmox-dark/scss/other/_icons.scss
+++ b/src/proxmox-dark/scss/other/_icons.scss
@@ -250,3 +250,15 @@
.usage {
background-color: $icon-color;
}
+
+// fa-network-wired override for dark mode.
+// The base/light theme lowers the opacity to simulate a light grey
+// color. This restores the opacity to 1 so the icon remains white and
+// clearly visible in the dark mode
+.x-fa-pool-net:before {
+ opacity: 1;
+}
+
+.x-fa-pool-net-grid:before {
+ opacity: 1;
+}
--
2.47.3
^ permalink raw reply related [flat|nested] 23+ messages in thread
* [PATCH pve-access-control v2 05/10] fix #7294: acl: pool: add SDN VNets as pool members
2026-06-26 13:10 [PATCH access-control/cluster/common/manager/network/proxmox-widget-toolkit/qemu-server v2 00/10] fix #7294: pool: add SDN VNets as pool members David Riley
` (3 preceding siblings ...)
2026-06-26 13:10 ` [PATCH proxmox-widget-toolkit v2 04/10] fix #7294: css: theme: add opacity override for pool VNet icon David Riley
@ 2026-06-26 13:10 ` David Riley
2026-07-03 14:35 ` Daniel Kral
2026-07-06 11:56 ` Daniel Kral
2026-06-26 13:10 ` [PATCH pve-network v2 06/10] fix #7294: sdn: register api formats for zones and vnets David Riley
` (5 subsequent siblings)
10 siblings, 2 replies; 23+ messages in thread
From: David Riley @ 2026-06-26 13:10 UTC (permalink / raw)
To: pve-devel
Extend the pool configuration to allow SDN VNets as pool members by
introducing a new 'network' property.
The property tracks entries using a type prefix for future expansion:
* vnet/<zone>/<vnet>
* vnet/<zone>/<vnet>/<vlan>
Adapt the path resolution for bridges to ensure pool configurations
are considered. This is necessary to allow users to assign a VNet to
a VM when they only have access to a specific VLAN tag. Note that if
a VLAN tag is present in the pool configuration, the user is
restricted to that specific tag and cannot assign the base VNet
untagged.
Update the parser_writer tests to verify serialization and parsing
of the updated configuration format.
Link: https://bugzilla.proxmox.com/show_bug.cgi?id=7294
Signed-off-by: David Riley <d.riley@proxmox.com>
---
src/PVE/AccessControl.pm | 93 ++++++++++++++++++++++++++++++++++++---
src/PVE/RPCEnvironment.pm | 68 ++++++++++++++++++++++++++--
src/test/parser_writer.pl | 53 ++++++++++++++++++----
3 files changed, 198 insertions(+), 16 deletions(-)
diff --git a/src/PVE/AccessControl.pm b/src/PVE/AccessControl.pm
index 34e56b6..6217f10 100644
--- a/src/PVE/AccessControl.pm
+++ b/src/PVE/AccessControl.pm
@@ -1559,7 +1559,7 @@ sub parse_user_config {
warn "user config - ignore invalid path in acl '$pathtxt'\n";
}
} elsif ($et eq 'pool') {
- my ($pool, $comment, $vmlist, $storelist) = @data;
+ my ($pool, $comment, $vmlist, $storelist, $networklist) = @data;
if (!verify_poolname($pool, 1)) {
warn "user config - ignore pool '$pool' - invalid characters in pool name\n";
@@ -1567,7 +1567,7 @@ sub parse_user_config {
}
# make sure to add the pool (even if there are no members)
- $cfg->{pools}->{$pool} = { vms => {}, storage => {}, pools => {} }
+ $cfg->{pools}->{$pool} = { vms => {}, storage => {}, pools => {}, network => {} }
if !$cfg->{pools}->{$pool};
if ($pool =~ m!/!) {
@@ -1576,8 +1576,12 @@ sub parse_user_config {
# ensure nested pool info is correctly recorded
my $parent = $1;
$cfg->{pools}->{$curr}->{parent} = $parent;
- $cfg->{pools}->{$parent} = { vms => {}, storage => {}, pools => {} }
- if !$cfg->{pools}->{$parent};
+
+ if (!$cfg->{pools}->{$parent}) {
+ $cfg->{pools}->{$parent} =
+ { vms => {}, storage => {}, pools => {}, network => {} };
+ }
+
$cfg->{pools}->{$parent}->{pools}->{$curr} = 1;
$curr = $parent;
}
@@ -1610,6 +1614,18 @@ sub parse_user_config {
}
$cfg->{pools}->{$pool}->{storage}->{$storeid} = 1;
}
+
+ for my $network (split_list($networklist)) {
+
+ if ($network !~ m/^[a-z0-9][a-z0-9\-_]*(?:\/[a-z0-9][a-z0-9\-_]*)*$/i) {
+ warn "user config - ignore invalid sdn resource entry '$network' in pool"
+ . " '$pool'\n";
+ next;
+ }
+
+ $cfg->{pools}->{$pool}->{network}->{$network} = 1;
+ }
+
} elsif ($et eq 'token') {
my ($tokenid, $expire, $privsep, $comment) = @data;
@@ -1691,8 +1707,9 @@ sub write_user_config {
my $d = $cfg->{pools}->{$pool};
my $vmlist = join(',', sort keys %{ $d->{vms} });
my $storelist = join(',', sort keys %{ $d->{storage} });
+ my $networklist = join(',', sort keys %{ $d->{network} });
my $comment = $d->{comment} ? PVE::Tools::encode_text($d->{comment}) : '';
- $data .= "pool:$pool:$comment:$vmlist:$storelist:\n";
+ $data .= "pool:$pool:$comment:$vmlist:$storelist:$networklist:\n";
}
$data .= "\n";
@@ -1974,6 +1991,72 @@ sub remove_vm_from_pool {
lock_user_config($delVMfromPoolFn, "pool cleanup for VM $vmid failed");
}
+sub remove_vnet_from_pool {
+ my ($zone, $vnet) = @_;
+ my $network_key = "vnet/$zone/$vnet";
+
+ my $del_vnet_from_pool_fn = sub {
+ my $usercfg = cfs_read_file("user.cfg");
+ return if !$usercfg->{pools};
+
+ my $modified = 0;
+ for my $pool (keys $usercfg->{pools}->%*) {
+ my $pool_cfg = $usercfg->{pools}->{$pool};
+ next if !$pool_cfg->{network};
+
+ for my $net_key (keys $pool_cfg->{network}->%*) {
+ if ($net_key eq $network_key || $net_key =~ m!^\Q$network_key\E/\d+$!) {
+ delete $pool_cfg->{network}->{$net_key};
+ $modified = 1;
+ }
+ }
+ }
+
+ if ($modified) {
+ cfs_write_file("user.cfg", $usercfg);
+ }
+ };
+
+ lock_user_config($del_vnet_from_pool_fn, "pool cleanup for VNet $vnet failed");
+}
+
+sub migrate_vnet_zone_in_pool {
+ my ($src_zone, $dest_zone, $vnet) = @_;
+
+ my $src_key = "vnet/$src_zone/$vnet";
+ my $dest_key = "vnet/$dest_zone/$vnet";
+
+ my $update_vnet_zone_fn = sub {
+ my $usercfg = cfs_read_file("user.cfg");
+ return if !$usercfg->{pools};
+
+ my $modified = 0;
+ for my $pool (keys $usercfg->{pools}->%*) {
+ my $pool_cfg = $usercfg->{pools}->{$pool};
+ next if !$pool_cfg->{network};
+
+ for my $net_key (keys $pool_cfg->{network}->%*) {
+ if ($net_key eq $src_key || $net_key =~ m!^\Q$src_key\E/(\d+)$!) {
+ my $vlan = $1;
+ delete $pool_cfg->{network}->{$net_key};
+
+ my $target_key = $dest_key;
+ $target_key .= "/$vlan" if defined($vlan);
+ $pool_cfg->{network}->{$target_key} = 1;
+
+ $modified = 1;
+ }
+ }
+ }
+
+ if ($modified) {
+ cfs_write_file("user.cfg", $usercfg);
+ }
+ };
+
+ lock_user_config($update_vnet_zone_fn, "pool update for VNet $vnet failed");
+}
+
sub remove_sdn_resource_access {
my ($paths) = @_; # [ ['zones', '<zone>'], ['zones', '<zone>', '<vnet>'] ]
diff --git a/src/PVE/RPCEnvironment.pm b/src/PVE/RPCEnvironment.pm
index 7591aa9..d9372a0 100644
--- a/src/PVE/RPCEnvironment.pm
+++ b/src/PVE/RPCEnvironment.pm
@@ -53,6 +53,21 @@ my $compile_acl_path = sub {
$data->{poolroles}->{"/storage/$storeid"}->{$role} = 1;
}
}
+
+ for my $network_key (keys $d->{network}->%*) {
+ my ($type, @path) = split('/', $network_key);
+
+ if ($type eq 'vnet') {
+ my ($zoneid, $vnetid, $vlan) = @path;
+
+ my $acl_path = "/sdn/zones/$zoneid/$vnetid";
+ $acl_path = "$acl_path/$vlan" if defined($vlan);
+
+ for my $role (keys $pool_roles->%*) {
+ $data->{poolroles}->{$acl_path}->{$role} = 1;
+ }
+ }
+ }
}
}
@@ -63,15 +78,30 @@ my $compile_acl_path = sub {
# means the role is set
my $roles = PVE::AccessControl::roles($cfg, $user, $path);
+ my $poolroles_path = $data->{poolroles}->{$path};
+
+ # Pool ACL paths are setup without propagation, therefore checking
+ # /sdn/zones/<zone>/<vnet>/<tag> fails even if the user has the
+ # permission for the base path /sdn/zones/<zone>/<vnet>.
+ # To allow this, the roles of the base VNet path are looked up if
+ # the exact tagged path is not found in the pool.
+ if (!defined($poolroles_path) && $path =~ m|^/sdn/zones/[^/]+/[^/]+/\d+$|) {
+ my @parts = split('/', $path);
+ my $base_vnet_path = join('/', @parts[0 .. 4]); # remove tag
+
+ # Inherit the permissions of the base VNet path for this request
+ $poolroles_path = $data->{poolroles}->{$base_vnet_path};
+ }
+
# apply roles inherited from pools
- if ($data->{poolroles}->{$path}) {
+ if ($poolroles_path) {
# NoAccess must not be trumped by pool ACLs
if (!defined($roles->{NoAccess})) {
- if ($data->{poolroles}->{$path}->{NoAccess}) {
+ if ($poolroles_path->{NoAccess}) {
# but pool ACL NoAccess trumps regular ACL
$roles = { 'NoAccess' => 0 };
} else {
- foreach my $role (keys %{ $data->{poolroles}->{$path} }) {
+ for my $role (keys $poolroles_path->%*) {
# only use role from pool ACL if regular ACL didn't already
# set it, and never set propagation for pool-derived ACLs
$roles->{$role} = 0 if !defined($roles->{$role});
@@ -219,6 +249,8 @@ sub compute_api_permission {
$res->{vms}->{$priv} = 1;
} elsif ($priv =~ m/^Datastore\./) {
$res->{storage}->{$priv} = 1;
+ } elsif ($priv =~ m/^SDN\./) {
+ $res->{sdn}->{$priv} = 1;
} elsif ($priv eq 'Permissions.Modify') {
$res->{storage}->{$priv} = 1;
$res->{vms}->{$priv} = 1;
@@ -274,6 +306,17 @@ sub get_effective_permissions {
foreach my $storeid (keys %{ $d->{storage} }) {
$paths->{"/storage/$storeid"} = 1;
}
+
+ for my $network_key (keys $d->{network}->%*) {
+ my ($type, @path) = split('/', $network_key);
+
+ if ($type eq 'vnet') {
+ my ($zoneid, $vnetid, $vlan) = @path;
+ my $vnet_path = "/sdn/zones/$zoneid/$vnetid";
+ $vnet_path .= "/$vlan" if defined($vlan);
+ $paths->{$vnet_path} = 1;
+ }
+ }
}
my $perms = {};
@@ -353,6 +396,25 @@ sub check_sdn_bridge {
}
}
+ # check access to VLANs via pools
+ for my $pool (keys $cfg->{pools}->%*) {
+ my $poolcfg = $cfg->{pools}->{$pool};
+ next if !$poolcfg->{network};
+
+ for my $network_key (keys $poolcfg->{network}->%*) {
+ my ($type, @path) = split('/', $network_key);
+
+ if (defined($type) && $type eq 'vnet') {
+ my ($zoneid, $vnetid, $vlan) = @path;
+
+ if ($zoneid eq $zone && $vnetid eq $bridge && defined($vlan)) {
+ my $vlanpath = "/sdn/zones/$zoneid/$vnetid/$vlan";
+ return 1 if $self->check_any($username, $vlanpath, $privs, 1);
+ }
+ }
+ }
+ }
+
# repeat check, but fatal
$self->check_any($username, $path, $privs, 0) if !$noerr;
diff --git a/src/test/parser_writer.pl b/src/test/parser_writer.pl
index ea2778e..a809e21 100755
--- a/src/test/parser_writer.pl
+++ b/src/test/parser_writer.pl
@@ -238,24 +238,35 @@ my $default_cfg = {
vms => {},
storage => {},
pools => {},
+ network => {},
},
test_pool_members => {
'id' => 'testpool',
vms => { 123 => 1, 1234 => 1 },
storage => { 'local' => 1, 'local-zfs' => 1 },
pools => {},
+ network => { "vnet/zone1/vnet1" => 1, 'vnet/zone2/vnet2' => 1 },
},
test_pool_duplicate_vms => {
'id' => 'test_duplicate_vms',
vms => {},
storage => {},
pools => {},
+ network => {},
},
test_pool_duplicate_storages => {
'id' => 'test_duplicate_storages',
vms => {},
storage => { 'local' => 1, 'local-zfs' => 1 },
pools => {},
+ network => {},
+ },
+ test_pool_duplicate_networks => {
+ 'id' => 'test_duplicate_networks',
+ vms => {},
+ storage => {},
+ pools => {},
+ network => { "vnet/zone1/vnet1" => 1, "vnet/zone2/vnet2" => 1 },
},
acl_simple_user => {
'path' => '/',
@@ -431,12 +442,15 @@ my $default_raw = {
'test_role_privs_invalid' => 'role:testrole:VM.Invalid,Datastore.Audit,VM.Allocate:',
},
pools => {
- 'test_pool_empty' => 'pool:testpool::::',
- 'test_pool_invalid' => 'pool:testpool::non-numeric:inval!d:',
- 'test_pool_members' => 'pool:testpool::123,1234:local,local-zfs:',
- 'test_pool_duplicate_vms' => 'pool:test_duplicate_vms::123,1234::',
- 'test_pool_duplicate_vms_expected' => 'pool:test_duplicate_vms::::',
- 'test_pool_duplicate_storages' => 'pool:test_duplicate_storages:::local,local-zfs:',
+ 'test_pool_empty' => 'pool:testpool:::::',
+ 'test_pool_invalid' => 'pool:testpool::non-numeric:inval!d::',
+ 'test_pool_members' =>
+ 'pool:testpool::123,1234:local,local-zfs:vnet/zone1/vnet1,vnet/zone2/vnet2:',
+ 'test_pool_duplicate_vms' => 'pool:test_duplicate_vms::123,1234:::',
+ 'test_pool_duplicate_vms_expected' => 'pool:test_duplicate_vms:::::',
+ 'test_pool_duplicate_storages' => 'pool:test_duplicate_storages:::local,local-zfs::',
+ 'test_pool_duplicate_networks' =>
+ 'pool:test_duplicate_networks::::vnet/zone1/vnet1,vnet/zone2/vnet2:',
},
acl => {
'acl_simple_user' => 'acl:1:/:test@pam:PVEVMAdmin:',
@@ -696,6 +710,7 @@ my $tests = [
$default_cfg->{test_pool_members},
$default_cfg->{test_pool_duplicate_vms},
$default_cfg->{test_pool_duplicate_storages},
+ $default_cfg->{test_pool_duplicate_networks},
],
),
vms => default_pool_vms_with([$default_cfg->{test_pool_members}]),
@@ -705,15 +720,37 @@ my $tests = [
. "\n\n\n"
. $default_raw->{pools}->{'test_pool_members'} . "\n"
. $default_raw->{pools}->{'test_pool_duplicate_vms'} . "\n"
- . $default_raw->{pools}->{'test_pool_duplicate_storages'} . "\n",
+ . $default_raw->{pools}->{'test_pool_duplicate_storages'} . "\n"
+ . $default_raw->{pools}->{'test_pool_duplicate_networks'} . "\n",
expected_raw => ""
. $default_raw->{users}->{'root@pam'}
. "\n\n\n"
+ . $default_raw->{pools}->{'test_pool_duplicate_networks'} . "\n"
. $default_raw->{pools}->{'test_pool_duplicate_storages'} . "\n"
. $default_raw->{pools}->{'test_pool_duplicate_vms_expected'} . "\n"
. $default_raw->{pools}->{'test_pool_members'}
. "\n\n\n",
},
+ {
+ name => "pool_format_backward_compatibility",
+ config => {
+ acl_root => default_acls(),
+ users => default_users(),
+ roles => default_roles(),
+ pools => default_pools_with([$default_cfg->{test_pool_empty}]),
+ },
+ raw => ""
+ . $default_raw->{users}->{'root@pam'}
+ . "\n\n\n"
+ # old format: 4 colons
+ . "pool:testpool::::\n\n\n",
+ expected_raw => ""
+ . $default_raw->{users}->{'root@pam'}
+ . "\n\n\n"
+ # new format 5: colons
+ . $default_raw->{pools}->{'test_pool_empty'}
+ . "\n\n\n",
+ },
{
name => "acl_simple_user",
config => {
@@ -1102,7 +1139,7 @@ my $tests = [
. 'user:test@pam:0:0::::::' . "\n"
. 'token:test@pam!test:0:0::' . "\n\n"
. 'group:testgroup:::' . "\n\n"
- . 'pool:testpool::::' . "\n\n"
+ . 'pool:testpool:::::' . "\n\n"
. 'role:testrole::' . "\n\n",
},
];
--
2.47.3
^ permalink raw reply related [flat|nested] 23+ messages in thread
* [PATCH pve-network v2 06/10] fix #7294: sdn: register api formats for zones and vnets
2026-06-26 13:10 [PATCH access-control/cluster/common/manager/network/proxmox-widget-toolkit/qemu-server v2 00/10] fix #7294: pool: add SDN VNets as pool members David Riley
` (4 preceding siblings ...)
2026-06-26 13:10 ` [PATCH pve-access-control v2 05/10] fix #7294: acl: pool: add SDN VNets as pool members David Riley
@ 2026-06-26 13:10 ` David Riley
2026-07-06 12:21 ` Daniel Kral
2026-06-26 13:10 ` [PATCH pve-network v2 07/10] fix #7294: sdn: vnet: update pool members on vnet migration and deletion David Riley
` (4 subsequent siblings)
10 siblings, 1 reply; 23+ messages in thread
From: David Riley @ 2026-06-26 13:10 UTC (permalink / raw)
To: pve-devel
Register JSONSchema format validators for both SDN zones and VNets,
enabling the pool members API to verify these network identifiers.
Link: https://bugzilla.proxmox.com/show_bug.cgi?id=7294
Signed-off-by: David Riley <d.riley@proxmox.com>
---
src/PVE/Network/SDN/VnetPlugin.pm | 25 ++++++++++++++++++++++---
src/PVE/Network/SDN/Zones/Plugin.pm | 25 ++++++++++++++++++++++---
2 files changed, 44 insertions(+), 6 deletions(-)
diff --git a/src/PVE/Network/SDN/VnetPlugin.pm b/src/PVE/Network/SDN/VnetPlugin.pm
index e041575..fbddc06 100644
--- a/src/PVE/Network/SDN/VnetPlugin.pm
+++ b/src/PVE/Network/SDN/VnetPlugin.pm
@@ -16,17 +16,36 @@ PVE::Cluster::cfs_register_file(
sub { __PACKAGE__->write_config(@_); },
);
+my $sdn_vnet_id_pattern = '[a-zA-Z][a-zA-Z0-9]*[a-zA-Z0-9]';
+my $vnet_min_length = 2;
+my $vnet_max_length = 8;
+
PVE::JSONSchema::register_standard_option(
'pve-sdn-vnet-id',
{
description => "The SDN vnet object identifier.",
type => 'string',
- pattern => '[a-zA-Z][a-zA-Z0-9]*[a-zA-Z0-9]',
- minLength => 2,
- maxLength => 8,
+ pattern => $sdn_vnet_id_pattern,
+ minLength => $vnet_min_length,
+ maxLength => $vnet_max_length,
},
);
+sub pve_verify_sdn_vnet_id {
+ my ($vnet, $noerr) = @_;
+
+ my $len = length($vnet);
+
+ if ($vnet !~ m/^$sdn_vnet_id_pattern$/ || $len < $vnet_min_length || $len > $vnet_max_length) {
+ return undef if $noerr;
+ die "invalid SDN VNet '$vnet', must be $vnet_min_length-$vnet_max_length characters"
+ . " long, start with a letter, and contain only alphanumeric characters\n";
+ }
+ return $vnet;
+}
+
+PVE::JSONSchema::register_format('pve-sdn-vnet-id', \&pve_verify_sdn_vnet_id);
+
my $defaultData = {
propertyList => {
diff --git a/src/PVE/Network/SDN/Zones/Plugin.pm b/src/PVE/Network/SDN/Zones/Plugin.pm
index 74a3384..81bcf86 100644
--- a/src/PVE/Network/SDN/Zones/Plugin.pm
+++ b/src/PVE/Network/SDN/Zones/Plugin.pm
@@ -19,17 +19,36 @@ PVE::Cluster::cfs_register_file(
sub { __PACKAGE__->write_config(@_); },
);
+my $sdn_zone_id_pattern = '[a-zA-Z][a-zA-Z0-9]*[a-zA-Z0-9]';
+my $zone_min_length = 2;
+my $zone_max_length = 8;
+
PVE::JSONSchema::register_standard_option(
'pve-sdn-zone-id',
{
description => "The SDN zone object identifier.",
type => 'string',
- pattern => '[a-zA-Z][a-zA-Z0-9]*[a-zA-Z0-9]',
- minLength => 2,
- maxLength => 8,
+ pattern => $sdn_zone_id_pattern,
+ minLength => $zone_min_length,
+ maxLength => $zone_max_length,
},
);
+sub pve_verify_sdn_zone_id {
+ my ($zone, $noerr) = @_;
+
+ my $len = length($zone);
+
+ if ($zone !~ m/^$sdn_zone_id_pattern$/ || $len < $zone_min_length || $len > $zone_max_length) {
+ return undef if $noerr;
+ die "invalid SDN zone '$zone', must be $zone_min_length-$zone_max_length characters long,"
+ . " start with a letter, and contain only alphanumeric characters\n";
+ }
+ return $zone;
+}
+
+PVE::JSONSchema::register_format('pve-sdn-zone-id', \&pve_verify_sdn_zone_id);
+
my $defaultData = {
propertyList => {
--
2.47.3
^ permalink raw reply related [flat|nested] 23+ messages in thread
* [PATCH pve-network v2 07/10] fix #7294: sdn: vnet: update pool members on vnet migration and deletion
2026-06-26 13:10 [PATCH access-control/cluster/common/manager/network/proxmox-widget-toolkit/qemu-server v2 00/10] fix #7294: pool: add SDN VNets as pool members David Riley
` (5 preceding siblings ...)
2026-06-26 13:10 ` [PATCH pve-network v2 06/10] fix #7294: sdn: register api formats for zones and vnets David Riley
@ 2026-06-26 13:10 ` David Riley
2026-07-06 12:29 ` Daniel Kral
2026-06-26 13:10 ` [PATCH pve-common v2 08/10] tools: add helpers for version comparison David Riley
` (3 subsequent siblings)
10 siblings, 1 reply; 23+ messages in thread
From: David Riley @ 2026-06-26 13:10 UTC (permalink / raw)
To: pve-devel
Update or remove corresponding resource pool allocations in user.cfg
whenever a VNet is altered or deleted. This prevents stale paths and
dangling references from breaking the configuration integrity.
Link: https://bugzilla.proxmox.com/show_bug.cgi?id=7294
Signed-off-by: David Riley <d.riley@proxmox.com>
---
src/PVE/Network/SDN.pm | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/src/PVE/Network/SDN.pm b/src/PVE/Network/SDN.pm
index cd563d2..81f5d9a 100644
--- a/src/PVE/Network/SDN.pm
+++ b/src/PVE/Network/SDN.pm
@@ -261,11 +261,26 @@ sub cleanup {
PVE::AccessControl::migrate_sdn_resource_access($vnet_move_paths);
}
+ for my $move (@$vnet_move_paths) {
+ my ($src_type, $src_zone, $vnet) = @{ $move->{src_path} };
+ my ($dest_type, $dest_zone, $dest_vnet) = @{ $move->{dest_path} };
+
+ if (defined($src_type) && $src_type eq 'zones' && $src_zone && $vnet && $dest_zone) {
+ PVE::AccessControl::migrate_vnet_zone_in_pool($src_zone, $dest_zone, $vnet);
+ }
+ }
+
my @paths_to_delete = (@$vnet_delete_paths, @$route_map_paths, @$generic_paths);
if (@paths_to_delete) {
PVE::AccessControl::remove_sdn_resource_access(\@paths_to_delete);
}
+ for my $path (@$vnet_delete_paths) {
+ my ($type, $zone, $vnet) = @$path;
+ if ($type && $type eq 'zones' && $zone && $vnet) {
+ PVE::AccessControl::remove_vnet_from_pool($zone, $vnet);
+ }
+ }
}
sub diff_generic_resources {
--
2.47.3
^ permalink raw reply related [flat|nested] 23+ messages in thread
* [PATCH pve-common v2 08/10] tools: add helpers for version comparison
2026-06-26 13:10 [PATCH access-control/cluster/common/manager/network/proxmox-widget-toolkit/qemu-server v2 00/10] fix #7294: pool: add SDN VNets as pool members David Riley
` (6 preceding siblings ...)
2026-06-26 13:10 ` [PATCH pve-network v2 07/10] fix #7294: sdn: vnet: update pool members on vnet migration and deletion David Riley
@ 2026-06-26 13:10 ` David Riley
2026-07-06 13:02 ` Daniel Kral
2026-06-26 13:10 ` [PATCH pve-cluster v2 09/10] fix #7294: cluster: helpers: add cluster-wide version assertion David Riley
` (2 subsequent siblings)
10 siblings, 1 reply; 23+ messages in thread
From: David Riley @ 2026-06-26 13:10 UTC (permalink / raw)
To: pve-devel
Move 'version_cmp' and 'pvecfg_min_version' over from qemu-server, to
make them more accessible.
Originally-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
Originally-by: Alexandre Derumier <aderumier@odiso.com>
Signed-off-by: David Riley <d.riley@proxmox.com>
---
src/PVE/Tools.pm | 41 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 41 insertions(+)
diff --git a/src/PVE/Tools.pm b/src/PVE/Tools.pm
index 0513fe1..7e66546 100644
--- a/src/PVE/Tools.pm
+++ b/src/PVE/Tools.pm
@@ -54,6 +54,8 @@ our @EXPORT_OK = qw(
extract_sensitive_params
file_copy
get_host_arch
+ pvecfg_min_version
+ version_cmp
O_PATH
O_CLOEXEC
O_TMPFILE
@@ -1945,4 +1947,43 @@ sub is_deeply {
return 1;
}
+sub pvecfg_min_version {
+ my ($verstr, $major, $minor, $release) = @_;
+
+ return 0 if !$verstr;
+
+ if ($verstr =~ m/^(\d+)\.(\d+)(?:[.-](\d+))?/) {
+ return 1 if version_cmp($1, $major, $2, $minor, $3 // 0, $release) >= 0;
+ return 0;
+ }
+
+ die "internal error: cannot check version of invalid string '$verstr'";
+}
+
+# gets in pairs the versions you want to compares, i.e.:
+# ($a-major, $b-major, $a-minor, $b-minor, $a-extra, $b-extra, ...)
+# returns 0 if same, -1 if $a is older than $b, +1 if $a is newer than $b
+sub version_cmp {
+ my @versions = @_;
+
+ my $size = scalar(@versions);
+
+ return 0 if $size == 0;
+
+ if ($size & 1) {
+ my (undef, $fn, $line) = caller(0);
+ die "cannot compare odd count of versions, called from $fn:$line\n";
+ }
+
+ for (my $i = 0; $i < $size; $i += 2) {
+ my ($left, $right) = splice(@versions, 0, 2);
+ $left //= 0;
+ $right //= 0;
+
+ return 1 if $left > $right;
+ return -1 if $left < $right;
+ }
+ return 0;
+}
+
1;
--
2.47.3
^ permalink raw reply related [flat|nested] 23+ messages in thread
* [PATCH pve-cluster v2 09/10] fix #7294: cluster: helpers: add cluster-wide version assertion
2026-06-26 13:10 [PATCH access-control/cluster/common/manager/network/proxmox-widget-toolkit/qemu-server v2 00/10] fix #7294: pool: add SDN VNets as pool members David Riley
` (7 preceding siblings ...)
2026-06-26 13:10 ` [PATCH pve-common v2 08/10] tools: add helpers for version comparison David Riley
@ 2026-06-26 13:10 ` David Riley
2026-07-06 13:00 ` Daniel Kral
2026-06-26 13:10 ` [PATCH qemu-server v2 10/10] fix #7294: helpers: use cluster-wide version helper David Riley
2026-07-06 14:07 ` [PATCH access-control/cluster/common/manager/network/proxmox-widget-toolkit/qemu-server v2 00/10] fix #7294: pool: add SDN VNets as pool members Daniel Kral
10 siblings, 1 reply; 23+ messages in thread
From: David Riley @ 2026-06-26 13:10 UTC (permalink / raw)
To: pve-devel
Add function to assert a minimum version requirement for the whole
cluster.
Evaluates against the cluster configuration. Dies if any node is
offline, or running an older version, ensuring cluster-wide feature
compatibility before allowing an operation to proceed.
Prevents older nodes from truncating newly structured configuration
entries.
Link: https://bugzilla.proxmox.com/show_bug.cgi?id=7294
Signed-off-by: David Riley <d.riley@proxmox.com>
---
src/PVE/Cluster.pm | 43 +++++++++++++++++++++++++++++++++++++++++--
1 file changed, 41 insertions(+), 2 deletions(-)
diff --git a/src/PVE/Cluster.pm b/src/PVE/Cluster.pm
index 034b78c..3ecde03 100644
--- a/src/PVE/Cluster.pm
+++ b/src/PVE/Cluster.pm
@@ -20,7 +20,7 @@ use PVE::IPCC;
use PVE::JSONSchema;
use PVE::Network;
use PVE::SafeSyslog;
-use PVE::Tools qw(run_command);
+use PVE::Tools qw(run_command pvecfg_min_version);
use PVE::Cluster::IPCConst;
@@ -30,7 +30,9 @@ our @EXPORT_OK = qw(
cfs_read_file
cfs_write_file
cfs_register_file
- cfs_lock_file);
+ cfs_lock_file
+ assert_min_cluster_version
+);
# x509 certificate utils
@@ -929,4 +931,41 @@ sub cfs_rename_db_unsafe {
rename $dbfile, "$dbfile.$ctime.bak" or warn "failed to rename old config database - $!\n";
}
+# Asserts that all configured nodes in the cluster meet a minimum version requirement.
+# Evaluates against the full, static cluster node list rather than only live nodes.
+# Aborts if any node is offline, unreadable, or running an
+# outdated version, preventing partial feature rollouts from corrupting cluster state.
+# NOTE: Uses PVE::Tools::pvecfg_min_version, which currently checks basic major.minor.release
+# triples. If gatekeeping a feature right around a major release transition, be aware that it
+# does not yet lexically sort trailing Debian revision suffixes (e.g., distinguishing '9.0.0' as
+# newer than '9.0.0~22').
+sub assert_min_cluster_version {
+ my ($major, $minor, $release) = @_;
+
+ my $nodestat = get_node_kv('version-info') || {};
+ my $nodelist = get_nodelist() || [];
+
+ for my $node (sort @$nodelist) {
+ my $raw_json = $nodestat->{$node};
+
+ if (!defined($raw_json)) {
+ die "cannot execute operation: cluster node '$node' is offline or not reporting its"
+ . " version. All nodes must be online to verify cluster-wide feature support.\n";
+ }
+
+ my $vinfo = eval { decode_json($raw_json) };
+ if ($@ || !$vinfo || !$vinfo->{version}) {
+ die "cannot execute operation: failed to parse cluster version string for node"
+ . " '$node'.\n";
+ }
+
+ if (!pvecfg_min_version($vinfo->{version}, $major, $minor, $release)) {
+ die "cannot execute operation: cluster node '$node' runs an older version of Proxmox"
+ . " VE ($vinfo->{version}) than the required $major.$minor.$release. Please"
+ . " upgrade all nodes first.\n";
+ }
+ }
+ return 1;
+}
+
1;
--
2.47.3
^ permalink raw reply related [flat|nested] 23+ messages in thread
* [PATCH qemu-server v2 10/10] fix #7294: helpers: use cluster-wide version helper
2026-06-26 13:10 [PATCH access-control/cluster/common/manager/network/proxmox-widget-toolkit/qemu-server v2 00/10] fix #7294: pool: add SDN VNets as pool members David Riley
` (8 preceding siblings ...)
2026-06-26 13:10 ` [PATCH pve-cluster v2 09/10] fix #7294: cluster: helpers: add cluster-wide version assertion David Riley
@ 2026-06-26 13:10 ` David Riley
2026-07-06 13:20 ` Daniel Kral
2026-07-06 14:07 ` [PATCH access-control/cluster/common/manager/network/proxmox-widget-toolkit/qemu-server v2 00/10] fix #7294: pool: add SDN VNets as pool members Daniel Kral
10 siblings, 1 reply; 23+ messages in thread
From: David Riley @ 2026-06-26 13:10 UTC (permalink / raw)
To: pve-devel
Drop the local `version_cmp` and `pvecfg_min_version` implementations
and update call sites to use the centralized tools module in
pve-common.
Link: https://bugzilla.proxmox.com/show_bug.cgi?id=7294
Signed-off-by: David Riley <d.riley@proxmox.com>
---
src/PVE/QemuMigrate.pm | 3 ++-
src/PVE/QemuServer/Helpers.pm | 42 ++---------------------------------
2 files changed, 4 insertions(+), 41 deletions(-)
diff --git a/src/PVE/QemuMigrate.pm b/src/PVE/QemuMigrate.pm
index 8da6f15d..65950a8f 100644
--- a/src/PVE/QemuMigrate.pm
+++ b/src/PVE/QemuMigrate.pm
@@ -23,6 +23,7 @@ use PVE::Storage::Plugin;
use PVE::Storage;
use PVE::StorageTunnel;
use PVE::Tools;
+use PVE::Tools qw(pvecfg_min_version);
use PVE::Tunnel;
use PVE::QemuConfig;
@@ -1400,7 +1401,7 @@ sub phase2 {
# Check if target is new enough for having the port encoded in the proxy ticket.
if (
$target_version
- && PVE::QemuServer::Helpers::pvecfg_min_version($target_version, 9, 1, 9)
+ && PVE::Tools::pvecfg_min_version($target_version, 9, 1, 9)
) {
$ticket_port = $spice_port;
}
diff --git a/src/PVE/QemuServer/Helpers.pm b/src/PVE/QemuServer/Helpers.pm
index dd17eef5..c0db925b 100644
--- a/src/PVE/QemuServer/Helpers.pm
+++ b/src/PVE/QemuServer/Helpers.pm
@@ -11,6 +11,7 @@ use PVE::Cluster;
use PVE::INotify;
use PVE::ProcFSTools;
use PVE::Tools;
+use PVE::Tools qw(version_cmp);
use base 'Exporter';
our @EXPORT_OK = qw(
@@ -228,39 +229,13 @@ sub min_version {
my ($verstr, $major, $minor, $pve) = @_;
if ($verstr =~ m/^(\d+)\.(\d+)(?:\.(\d+))?(?:\+pve(\d+))?/) {
- return 1 if version_cmp($1, $major, $2, $minor, $4, $pve) >= 0;
+ return 1 if PVE::Tools::version_cmp($1, $major, $2, $minor, $4, $pve) >= 0;
return 0;
}
die "internal error: cannot check version of invalid string '$verstr'";
}
-# gets in pairs the versions you want to compares, i.e.:
-# ($a-major, $b-major, $a-minor, $b-minor, $a-extra, $b-extra, ...)
-# returns 0 if same, -1 if $a is older than $b, +1 if $a is newer than $b
-sub version_cmp {
- my @versions = @_;
-
- my $size = scalar(@versions);
-
- return 0 if $size == 0;
-
- if ($size & 1) {
- my (undef, $fn, $line) = caller(0);
- die "cannot compare odd count of versions, called from $fn:$line\n";
- }
-
- for (my $i = 0; $i < $size; $i += 2) {
- my ($left, $right) = splice(@versions, 0, 2);
- $left //= 0;
- $right //= 0;
-
- return 1 if $left > $right;
- return -1 if $left < $right;
- }
- return 0;
-}
-
sub config_aware_timeout {
my ($config, $memory, $is_suspended) = @_;
my $timeout = 30;
@@ -307,19 +282,6 @@ sub get_node_pvecfg_version {
return $version_info->{version};
}
-sub pvecfg_min_version {
- my ($verstr, $major, $minor, $release) = @_;
-
- return 0 if !$verstr;
-
- if ($verstr =~ m/^(\d+)\.(\d+)(?:[.-](\d+))?/) {
- return 1 if version_cmp($1, $major, $2, $minor, $3 // 0, $release) >= 0;
- return 0;
- }
-
- die "internal error: cannot check version of invalid string '$verstr'";
-}
-
sub parse_number_sets {
my ($set) = @_;
my $res = [];
--
2.47.3
^ permalink raw reply related [flat|nested] 23+ messages in thread
* Re: [PATCH pve-manager v2 01/10] ui: replace var with let to match style guide for variable declaration
2026-06-26 13:10 ` [PATCH pve-manager v2 01/10] ui: replace var with let to match style guide for variable declaration David Riley
@ 2026-07-03 13:14 ` Daniel Kral
0 siblings, 0 replies; 23+ messages in thread
From: Daniel Kral @ 2026-07-03 13:14 UTC (permalink / raw)
To: David Riley, pve-devel
jn Fri Jun 26, 2026 at 3:10 PM CEST, David Riley wrote:
> No semantic change intended.
>
> Link: https://pve.proxmox.com/wiki/Javascript_Style_Guide#Variables
> Signed-off-by: David Riley <d.riley@proxmox.com>
Reviewed-by: Daniel Kral <d.kral@proxmox.com>
^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: [PATCH pve-manager v2 02/10] fix #7294: api: pool: add SDN VNets as pool members
2026-06-26 13:10 ` [PATCH pve-manager v2 02/10] fix #7294: api: pool: add SDN VNets as pool members David Riley
@ 2026-07-03 13:19 ` Daniel Kral
0 siblings, 0 replies; 23+ messages in thread
From: Daniel Kral @ 2026-07-03 13:19 UTC (permalink / raw)
To: David Riley, pve-devel
On Fri Jun 26, 2026 at 3:10 PM CEST, David Riley wrote:
> Extend the pool API to accept SDN VNets and optional VLAN tags. Group
> VNets under the new 'network' property type in the pool configuration.
>
> 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.
>
> Link: https://bugzilla.proxmox.com/show_bug.cgi?id=7294
> Signed-off-by: David Riley <d.riley@proxmox.com>
> ---
This patch relies on some new features from other patches, e.g., from
pve-cluster: PVE::Cluster::assert_min_cluster_version() and
pve-access-control: the new $pool_cfg->{network} property.
We encode such constraints in version dependency, so it's a good measure
to add a comment here for the applying maintainer that these
dependencies' versions should be increased.
Additionally, the multi-repo patch series should be ordered as someone
should build & install the repositories/packages. That is, also ordered
by dependency, e.g., the changes to pve-cluster and pve-access-control,
which are used here should come before these patches.
> PVE/API2/Pool.pm | 135 ++++++++++++++++++++++++++++++++++++++++++++---
> 1 file changed, 129 insertions(+), 6 deletions(-)
>
> diff --git a/PVE/API2/Pool.pm b/PVE/API2/Pool.pm
> index 63aff5bb..6bd99abb 100644
> --- a/PVE/API2/Pool.pm
> +++ b/PVE/API2/Pool.pm
> @@ -4,10 +4,12 @@ use strict;
> use warnings;
>
> use PVE::AccessControl;
> -use PVE::Cluster qw (cfs_read_file cfs_write_file);
> +use PVE::Cluster qw (cfs_read_file cfs_write_file assert_min_cluster_version);
> use PVE::Exception qw(raise_param_exc);
> use PVE::INotify;
> +use PVE::Network;
> use PVE::Storage;
> +use PVE::Tools;
>
> use PVE::SafeSyslog;
>
> @@ -16,6 +18,26 @@ use PVE::RESTHandler;
>
> use base qw(PVE::RESTHandler);
>
> +my $pool_network_format = {
> + zone => {
> + description => 'SDN Zone',
> + type => 'string',
> + format => 'pve-sdn-zone-id',
> + },
> + vnet => {
> + description => 'VNet to add or remove from this pool.',
> + type => 'string',
> + format => 'pve-sdn-vnet-id',
> + },
> + tag => {
> + description => "Specify a VLAN tag",
> + optional => 1,
> + type => 'integer',
> + minimum => 1,
> + maximum => 4094,
> + },
> +};
> +
> __PACKAGE__->register_method({
> name => 'index',
> path => '',
> @@ -36,7 +58,7 @@ __PACKAGE__->register_method({
> },
> type => {
> type => 'string',
> - enum => ['qemu', 'lxc', 'storage'],
> + enum => ['qemu', 'lxc', 'storage', 'network'],
> optional => 1,
> requires => 'poolid',
> },
> @@ -61,7 +83,7 @@ __PACKAGE__->register_method({
> properties => {
> type => {
> type => 'string',
> - enum => ['qemu', 'lxc', 'openvz', 'storage'],
> + enum => ['qemu', 'lxc', 'openvz', 'storage', 'network'],
> },
> id => {
> type => 'string',
> @@ -135,6 +157,29 @@ __PACKAGE__->register_method({
> }
> }
>
> + if (!defined($param->{type}) || $param->{type} eq 'network') {
> + if ($pool_config->{network}) {
nit: the additional check can be dropped with the properly versioned
dependency to pve-access-control, which should always at least contain
an empty hash ref for $pool_config->{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 +288,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 => 'zone=<zone>,vnet=<vnet>[,tag=<tag>]',
> + format => $pool_network_format,
> + optional => 1,
> + },
As discussed off-list, this would prevent the property from being
extended with lists, at least if we want to go with a comma-separated
list, which is pretty much the default we use.
Maybe there's some other representation for this, like
'vnet/<zoneid>/<vnetid>[/<tagid>]' or something similar? Then each entry
(or only one entry for now) would be the $network_key.
Essentially, we could also allow lists of network resources already with
# outside so we only read the configs once
my $zones_cfg = PVE::Network::SDN::Zones::config();
my $vnets_cfg = PVE::Network::SDN::Vnets::config();
for my $network_key (PVE::Tools::split_list($params->{network})) {
# ...
}
then if I didn't miss anything? You don't have to add adding multiple
VNets at once in the web interface though, that can be a separate
feature request.
What do you think?
> '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 +347,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 => 'zone=<zone>,vnet=<vnet>[,tag=<tag>]',
> + format => $pool_network_format,
> + optional => 1,
> + },
Same here
> '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 +363,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 +432,62 @@ __PACKAGE__->register_method({
> }
> }
>
> + if (defined($param->{network})) {
> + my $vnet_entry = PVE::JSONSchema::parse_property_string(
> + $pool_network_format, $param->{network},
> + );
> +
> + # gatekeep vnet as pool members
> + PVE::Cluster::assert_min_cluster_version(9, 2, 3);
Sorry, I missed this in the previous review: This patch should
definitely have some note that the maintainer applying this must update
this version number appropriately with the introduction of this feature.
Either the applying maintainer will change the patch itself or make a
follow-up commit that changes the version number afterwards.
> +
> + my ($zone, $vnetid, $tag) = $vnet_entry->@{qw(zone vnet tag)};
> +
> + my $zones_cfg = PVE::Network::SDN::Zones::config();
> + die "SDN Zone '$zone' does not exist\n" if !$zones_cfg->{ids}->{$zone};
> +
> + my $vnets_cfg = PVE::Network::SDN::Vnets::config();
> +
> + 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";
> + }
nit: use the post-if version here (might allow that the die expr doesn't
need to be on a new line)
> +
> + my $network_key = "vnet/$vnet_zone/$vnetid";
> +
> + if (defined($tag)) {
> + if (!$vnet_data->{vlanaware}) {
> + die
> + "VNet '$vnetid' is not VLAN-aware, cannot assign a specific tag\n";
> + }
nit: use the post-if version here (might allow that the die expr doesn't
need to be on a new line)
> +
> + $network_key .= "/$tag";
> + }
> +
> + $rpcenv->check_perm_modify(
> + $authuser,
> + "/sdn/zones/$vnet_zone/$vnetid",
> + ['SDN.Allocate'],
> + );
> +
> + if ($param->{delete}) {
> + if (!$pool_config->{network}->{$network_key}) {
> + die "Network resource '$network_key' is not a pool member\n";
> + }
nit: use the post-if version here
> +
> + delete $pool_config->{network}->{$network_key};
> + } else {
> + if ($pool_config->{network}->{$network_key}) {
> + die "Network resource '$network_key' is already a pool member\n";
> + }
nit: use the post-if version here
> +
> + $pool_config->{network}->{$network_key} = 1;
> + }
> + }
> +
> cfs_write_file("user.cfg", $usercfg);
> },
> "update pools failed",
> @@ -400,7 +515,7 @@ __PACKAGE__->register_method({
> },
> type => {
> type => 'string',
> - enum => ['qemu', 'lxc', 'storage'],
> + enum => ['qemu', 'lxc', 'storage', 'network'],
> optional => 1,
> },
> },
> @@ -421,7 +536,7 @@ __PACKAGE__->register_method({
> properties => {
> type => {
> type => 'string',
> - enum => ['qemu', 'lxc', 'openvz', 'storage'],
> + enum => ['qemu', 'lxc', 'openvz', 'storage', 'network'],
> },
> id => {
> type => 'string',
> @@ -524,6 +639,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);
>
^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: [PATCH proxmox-widget-toolkit v2 04/10] fix #7294: css: theme: add opacity override for pool VNet icon
2026-06-26 13:10 ` [PATCH proxmox-widget-toolkit v2 04/10] fix #7294: css: theme: add opacity override for pool VNet icon David Riley
@ 2026-07-03 13:30 ` Daniel Kral
0 siblings, 0 replies; 23+ messages in thread
From: Daniel Kral @ 2026-07-03 13:30 UTC (permalink / raw)
To: David Riley, pve-devel
On Fri Jun 26, 2026 at 3:10 PM CEST, David Riley wrote:
> Add opacity adjustments for the `fa-network-wired` icon used by pool
> networks.
>
> The light/base theme lowers the icons opacity to simulate a light grey
> color that matches other configuration elements. This override
> restores the opacity to 1 in the dark theme so the native white SVG
> remains clearly visible against the dark background.
>
> Signed-off-by: David Riley <d.riley@proxmox.com>
existing - so not related to the series directly - but the VM and CT
icons seem to also have lower opacity
With the trailing white spaces removed:
Reviewed-by: Daniel Kral <d.kral@proxmox.com>
> ---
> src/proxmox-dark/scss/other/_icons.scss | 12 ++++++++++++
> 1 file changed, 12 insertions(+)
>
> diff --git a/src/proxmox-dark/scss/other/_icons.scss b/src/proxmox-dark/scss/other/_icons.scss
> index ad08906..3712094 100644
> --- a/src/proxmox-dark/scss/other/_icons.scss
> +++ b/src/proxmox-dark/scss/other/_icons.scss
> @@ -250,3 +250,15 @@
> .usage {
> background-color: $icon-color;
> }
> +
> +// fa-network-wired override for dark mode.
> +// The base/light theme lowers the opacity to simulate a light grey
nit: trailing white space
> +// color. This restores the opacity to 1 so the icon remains white and
> +// clearly visible in the dark mode
nit: trailing white space
> +.x-fa-pool-net:before {
> + opacity: 1;
> +}
> +
> +.x-fa-pool-net-grid:before {
> + opacity: 1;
> +}
^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: [PATCH pve-access-control v2 05/10] fix #7294: acl: pool: add SDN VNets as pool members
2026-06-26 13:10 ` [PATCH pve-access-control v2 05/10] fix #7294: acl: pool: add SDN VNets as pool members David Riley
@ 2026-07-03 14:35 ` Daniel Kral
2026-07-06 11:56 ` Daniel Kral
1 sibling, 0 replies; 23+ messages in thread
From: Daniel Kral @ 2026-07-03 14:35 UTC (permalink / raw)
To: David Riley, pve-devel
On Fri Jun 26, 2026 at 3:10 PM CEST, David Riley wrote:
> Extend the pool configuration to allow SDN VNets as pool members by
> introducing a new 'network' property.
>
> The property tracks entries using a type prefix for future expansion:
> * vnet/<zone>/<vnet>
> * vnet/<zone>/<vnet>/<vlan>
>
> Adapt the path resolution for bridges to ensure pool configurations
> are considered. This is necessary to allow users to assign a VNet to
> a VM when they only have access to a specific VLAN tag. Note that if
> a VLAN tag is present in the pool configuration, the user is
> restricted to that specific tag and cannot assign the base VNet
> untagged.
>
> Update the parser_writer tests to verify serialization and parsing
> of the updated configuration format.
>
> Link: https://bugzilla.proxmox.com/show_bug.cgi?id=7294
> Signed-off-by: David Riley <d.riley@proxmox.com>
> ---
might make sense to move out the remove_vnet_from_pool() and
migrate_vnet_zone_in_pool() functions out to a separate patch as
removing/migrating the pool entries in case of zone reassignments, but
not sure about this.
> src/PVE/AccessControl.pm | 93 ++++++++++++++++++++++++++++++++++++---
> src/PVE/RPCEnvironment.pm | 68 ++++++++++++++++++++++++++--
> src/test/parser_writer.pl | 53 ++++++++++++++++++----
> 3 files changed, 198 insertions(+), 16 deletions(-)
>
> diff --git a/src/PVE/AccessControl.pm b/src/PVE/AccessControl.pm
> index 34e56b6..6217f10 100644
> --- a/src/PVE/AccessControl.pm
> +++ b/src/PVE/AccessControl.pm
[ snip ]
> @@ -1974,6 +1991,72 @@ sub remove_vm_from_pool {
> lock_user_config($delVMfromPoolFn, "pool cleanup for VM $vmid failed");
> }
>
> +sub remove_vnet_from_pool {
> + my ($zone, $vnet) = @_;
> + my $network_key = "vnet/$zone/$vnet";
> +
> + my $del_vnet_from_pool_fn = sub {
> + my $usercfg = cfs_read_file("user.cfg");
> + return if !$usercfg->{pools};
nit: early return is a bit unnecessary as an empty $usercfg->{pools}
will essentially be skipping the loop anyway and not end up writing
the file as $modified == 0
> +
> + my $modified = 0;
> + for my $pool (keys $usercfg->{pools}->%*) {
> + my $pool_cfg = $usercfg->{pools}->{$pool};
> + next if !$pool_cfg->{network};
same here
> +
> + for my $net_key (keys $pool_cfg->{network}->%*) {
> + if ($net_key eq $network_key || $net_key =~ m!^\Q$network_key\E/\d+$!) {
Nice usage of the quotemeta ops!
> + delete $pool_cfg->{network}->{$net_key};
> + $modified = 1;
> + }
> + }
> + }
> +
> + if ($modified) {
> + cfs_write_file("user.cfg", $usercfg);
> + }
> + };
> +
> + lock_user_config($del_vnet_from_pool_fn, "pool cleanup for VNet $vnet failed");
> +}
> +
> +sub migrate_vnet_zone_in_pool {
> + my ($src_zone, $dest_zone, $vnet) = @_;
> +
> + my $src_key = "vnet/$src_zone/$vnet";
> + my $dest_key = "vnet/$dest_zone/$vnet";
> +
> + my $update_vnet_zone_fn = sub {
> + my $usercfg = cfs_read_file("user.cfg");
> + return if !$usercfg->{pools};
same here as in remove_vnet_from_pool
> +
> + my $modified = 0;
> + for my $pool (keys $usercfg->{pools}->%*) {
> + my $pool_cfg = $usercfg->{pools}->{$pool};
> + next if !$pool_cfg->{network};
same here
> +
> + for my $net_key (keys $pool_cfg->{network}->%*) {
> + if ($net_key eq $src_key || $net_key =~ m!^\Q$src_key\E/(\d+)$!) {
> + my $vlan = $1;
> + delete $pool_cfg->{network}->{$net_key};
the delete while iterating over the same hash is not very robust... I'd
prefer a double pass even if it's marginally less efficient just to make
sure that there's nothing funky going on. But no very hard feelings
about this.
Besides that, this code is a little buggy: The if condition will
evaluate the first term $net_key eq $src_key for VNets without vlan
tags, but will also evaluate the second term with the regex for VNets
with vlan tags. So the $1 will only be overwritten if there was a VNet
with a vlan tag and will still be defined if a VNet without a vlan tag
comes afterwards.
Iterating hashes in Perl is random every run, so if the iteration is
something like this:
- vnet/somezone/vnet1 -> vnet/otherzon/vnet1
- vnet/somezone/vnet1.3 -> vnet/otherzon/vnet1.3
- vnet/somezone/vnet1.2 -> vnet/otherzon/vnet1.2
Then everything is fine, but if we only slightly change it, it will be:
- vnet/somezone/vnet1.3 -> vnet/otherzon/vnet1.3
- vnet/somezone/vnet1 -> vnet/otherzon/vnet1.3
- vnet/somezone/vnet1.2 -> vnet/otherzon/vnet1.2
So it will remove vnet1 SOMETIMES, because of the randomness here.
There are multiple ways to fix this, including sorting the keys and
always executing the regex with making the tag optional, so the $1 will
be overwritten every time.
> +
> + my $target_key = $dest_key;
> + $target_key .= "/$vlan" if defined($vlan);
> + $pool_cfg->{network}->{$target_key} = 1;
> +
> + $modified = 1;
> + }
> + }
> + }
> +
> + if ($modified) {
> + cfs_write_file("user.cfg", $usercfg);
> + }
> + };
> +
> + lock_user_config($update_vnet_zone_fn, "pool update for VNet $vnet failed");
> +}
> +
> sub remove_sdn_resource_access {
> my ($paths) = @_; # [ ['zones', '<zone>'], ['zones', '<zone>', '<vnet>'] ]
>
> diff --git a/src/PVE/RPCEnvironment.pm b/src/PVE/RPCEnvironment.pm
> index 7591aa9..d9372a0 100644
> --- a/src/PVE/RPCEnvironment.pm
> +++ b/src/PVE/RPCEnvironment.pm
> @@ -53,6 +53,21 @@ my $compile_acl_path = sub {
> $data->{poolroles}->{"/storage/$storeid"}->{$role} = 1;
> }
> }
> +
> + for my $network_key (keys $d->{network}->%*) {
> + my ($type, @path) = split('/', $network_key);
> +
> + if ($type eq 'vnet') {
> + my ($zoneid, $vnetid, $vlan) = @path;
> +
> + my $acl_path = "/sdn/zones/$zoneid/$vnetid";
> + $acl_path = "$acl_path/$vlan" if defined($vlan);
nit: use the .= operator here
> +
> + for my $role (keys $pool_roles->%*) {
> + $data->{poolroles}->{$acl_path}->{$role} = 1;
> + }
> + }
> + }
> }
> }
>
I'll review the rest from here on next week
^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: [PATCH pve-access-control v2 05/10] fix #7294: acl: pool: add SDN VNets as pool members
2026-06-26 13:10 ` [PATCH pve-access-control v2 05/10] fix #7294: acl: pool: add SDN VNets as pool members David Riley
2026-07-03 14:35 ` Daniel Kral
@ 2026-07-06 11:56 ` Daniel Kral
1 sibling, 0 replies; 23+ messages in thread
From: Daniel Kral @ 2026-07-06 11:56 UTC (permalink / raw)
To: David Riley, pve-devel
Generally, it would be good if this patch also introduces the
network_list entry in the repository's README, where the pool fields are
documented for a quick overview.
On Fri Jun 26, 2026 at 3:10 PM CEST, David Riley wrote:
> diff --git a/src/PVE/RPCEnvironment.pm b/src/PVE/RPCEnvironment.pm
> index 7591aa9..d9372a0 100644
> --- a/src/PVE/RPCEnvironment.pm
> +++ b/src/PVE/RPCEnvironment.pm
> @@ -53,6 +53,21 @@ my $compile_acl_path = sub {
> $data->{poolroles}->{"/storage/$storeid"}->{$role} = 1;
> }
> }
> +
> + for my $network_key (keys $d->{network}->%*) {
> + my ($type, @path) = split('/', $network_key);
> +
> + if ($type eq 'vnet') {
> + my ($zoneid, $vnetid, $vlan) = @path;
> +
> + my $acl_path = "/sdn/zones/$zoneid/$vnetid";
> + $acl_path = "$acl_path/$vlan" if defined($vlan);
nit: use the .= operator here
> +
> + for my $role (keys $pool_roles->%*) {
> + $data->{poolroles}->{$acl_path}->{$role} = 1;
> + }
> + }
> + }
> }
> }
>
> @@ -63,15 +78,30 @@ my $compile_acl_path = sub {
> # means the role is set
> my $roles = PVE::AccessControl::roles($cfg, $user, $path);
>
> + my $poolroles_path = $data->{poolroles}->{$path};
the factoring of $data->{poolroles}->{$path} should be done in a
separate commit beforehand so the changes done in this commit are easier
to understand and review.
> +
> + # Pool ACL paths are setup without propagation, therefore checking
> + # /sdn/zones/<zone>/<vnet>/<tag> fails even if the user has the
> + # permission for the base path /sdn/zones/<zone>/<vnet>.
> + # To allow this, the roles of the base VNet path are looked up if
> + # the exact tagged path is not found in the pool.
> + if (!defined($poolroles_path) && $path =~ m|^/sdn/zones/[^/]+/[^/]+/\d+$|) {
> + my @parts = split('/', $path);
> + my $base_vnet_path = join('/', @parts[0 .. 4]); # remove tag
> +
> + # Inherit the permissions of the base VNet path for this request
> + $poolroles_path = $data->{poolroles}->{$base_vnet_path};
> + }
> +
nit: it's probably better if this is moved before the declaration of
$roles so it's clear that it isn't depending on it/interacting with
it, but only the code below is
Besides that, I'm curious whether there might be an use case where this
might not be wanted? AFAIK, assigning a VM to a VNet does not include
the network as assigning a VM to a VNet VLAN tag would have, so the
propagation might be a bit off here...
It would be possible to introduce the syntax
vnet/<zoneid>/<vnetid>/*
to make the propagation to the individual vlan tag objects more
explicit, so this can still be an opt-in feature, and otherwise
vnet/<zoneid>/<vnetid>
only grants assigning the VNet to pool guests.
Though this is only an idea.
> # apply roles inherited from pools
> - if ($data->{poolroles}->{$path}) {
> + if ($poolroles_path) {
> # NoAccess must not be trumped by pool ACLs
> if (!defined($roles->{NoAccess})) {
> - if ($data->{poolroles}->{$path}->{NoAccess}) {
> + if ($poolroles_path->{NoAccess}) {
> # but pool ACL NoAccess trumps regular ACL
> $roles = { 'NoAccess' => 0 };
> } else {
> - foreach my $role (keys %{ $data->{poolroles}->{$path} }) {
> + for my $role (keys $poolroles_path->%*) {
> # only use role from pool ACL if regular ACL didn't already
> # set it, and never set propagation for pool-derived ACLs
> $roles->{$role} = 0 if !defined($roles->{$role});
> @@ -219,6 +249,8 @@ sub compute_api_permission {
> $res->{vms}->{$priv} = 1;
> } elsif ($priv =~ m/^Datastore\./) {
> $res->{storage}->{$priv} = 1;
> + } elsif ($priv =~ m/^SDN\./) {
> + $res->{sdn}->{$priv} = 1;
What is the intent here to add this?
I'm not too familiar with compute_api_permission(), but it seems like
it's only relevant for OpenID and TFA authentication?
> } elsif ($priv eq 'Permissions.Modify') {
> $res->{storage}->{$priv} = 1;
> $res->{vms}->{$priv} = 1;
> @@ -274,6 +306,17 @@ sub get_effective_permissions {
> foreach my $storeid (keys %{ $d->{storage} }) {
> $paths->{"/storage/$storeid"} = 1;
> }
> +
> + for my $network_key (keys $d->{network}->%*) {
> + my ($type, @path) = split('/', $network_key);
> +
> + if ($type eq 'vnet') {
> + my ($zoneid, $vnetid, $vlan) = @path;
> + my $vnet_path = "/sdn/zones/$zoneid/$vnetid";
> + $vnet_path .= "/$vlan" if defined($vlan);
> + $paths->{$vnet_path} = 1;
> + }
This might benefit from a warn/log_warn if there's a malformed network
pool entry or one with an unknown type... Though this might not get
shown properly to users as this is only called from an API endpoint?
> + }
> }
>
> my $perms = {};
> @@ -353,6 +396,25 @@ sub check_sdn_bridge {
> }
> }
>
> + # check access to VLANs via pools
> + for my $pool (keys $cfg->{pools}->%*) {
> + my $poolcfg = $cfg->{pools}->{$pool};
> + next if !$poolcfg->{network};
> +
> + for my $network_key (keys $poolcfg->{network}->%*) {
> + my ($type, @path) = split('/', $network_key);
> +
> + if (defined($type) && $type eq 'vnet') {
> + my ($zoneid, $vnetid, $vlan) = @path;
> +
> + if ($zoneid eq $zone && $vnetid eq $bridge && defined($vlan)) {
> + my $vlanpath = "/sdn/zones/$zoneid/$vnetid/$vlan";
> + return 1 if $self->check_any($username, $vlanpath, $privs, 1);
> + }
> + }
I think this should also have an else path, where we warn/log_warn that
there's a malformed network pool entry or one with an unknown type...
> + }
> + }
> +
> # repeat check, but fatal
> $self->check_any($username, $path, $privs, 0) if !$noerr;
>
> diff --git a/src/test/parser_writer.pl b/src/test/parser_writer.pl
> index ea2778e..a809e21 100755
> --- a/src/test/parser_writer.pl
> +++ b/src/test/parser_writer.pl
> @@ -238,24 +238,35 @@ my $default_cfg = {
> vms => {},
> storage => {},
> pools => {},
> + network => {},
> },
> test_pool_members => {
> 'id' => 'testpool',
> vms => { 123 => 1, 1234 => 1 },
> storage => { 'local' => 1, 'local-zfs' => 1 },
> pools => {},
> + network => { "vnet/zone1/vnet1" => 1, 'vnet/zone2/vnet2' => 1 },
> },
> test_pool_duplicate_vms => {
> 'id' => 'test_duplicate_vms',
> vms => {},
> storage => {},
> pools => {},
> + network => {},
> },
> test_pool_duplicate_storages => {
> 'id' => 'test_duplicate_storages',
> vms => {},
> storage => { 'local' => 1, 'local-zfs' => 1 },
> pools => {},
> + network => {},
> + },
> + test_pool_duplicate_networks => {
> + 'id' => 'test_duplicate_networks',
> + vms => {},
> + storage => {},
> + pools => {},
> + network => { "vnet/zone1/vnet1" => 1, "vnet/zone2/vnet2" => 1 },
> },
> acl_simple_user => {
> 'path' => '/',
> @@ -431,12 +442,15 @@ my $default_raw = {
> 'test_role_privs_invalid' => 'role:testrole:VM.Invalid,Datastore.Audit,VM.Allocate:',
> },
> pools => {
> - 'test_pool_empty' => 'pool:testpool::::',
> - 'test_pool_invalid' => 'pool:testpool::non-numeric:inval!d:',
> - 'test_pool_members' => 'pool:testpool::123,1234:local,local-zfs:',
> - 'test_pool_duplicate_vms' => 'pool:test_duplicate_vms::123,1234::',
> - 'test_pool_duplicate_vms_expected' => 'pool:test_duplicate_vms::::',
> - 'test_pool_duplicate_storages' => 'pool:test_duplicate_storages:::local,local-zfs:',
> + 'test_pool_empty' => 'pool:testpool:::::',
> + 'test_pool_invalid' => 'pool:testpool::non-numeric:inval!d::',
> + 'test_pool_members' =>
> + 'pool:testpool::123,1234:local,local-zfs:vnet/zone1/vnet1,vnet/zone2/vnet2:',
> + 'test_pool_duplicate_vms' => 'pool:test_duplicate_vms::123,1234:::',
> + 'test_pool_duplicate_vms_expected' => 'pool:test_duplicate_vms:::::',
> + 'test_pool_duplicate_storages' => 'pool:test_duplicate_storages:::local,local-zfs::',
> + 'test_pool_duplicate_networks' =>
> + 'pool:test_duplicate_networks::::vnet/zone1/vnet1,vnet/zone2/vnet2:',
> },
> acl => {
> 'acl_simple_user' => 'acl:1:/:test@pam:PVEVMAdmin:',
> @@ -696,6 +710,7 @@ my $tests = [
> $default_cfg->{test_pool_members},
> $default_cfg->{test_pool_duplicate_vms},
> $default_cfg->{test_pool_duplicate_storages},
> + $default_cfg->{test_pool_duplicate_networks},
> ],
> ),
> vms => default_pool_vms_with([$default_cfg->{test_pool_members}]),
> @@ -705,15 +720,37 @@ my $tests = [
> . "\n\n\n"
> . $default_raw->{pools}->{'test_pool_members'} . "\n"
> . $default_raw->{pools}->{'test_pool_duplicate_vms'} . "\n"
> - . $default_raw->{pools}->{'test_pool_duplicate_storages'} . "\n",
> + . $default_raw->{pools}->{'test_pool_duplicate_storages'} . "\n"
> + . $default_raw->{pools}->{'test_pool_duplicate_networks'} . "\n",
> expected_raw => ""
> . $default_raw->{users}->{'root@pam'}
> . "\n\n\n"
> + . $default_raw->{pools}->{'test_pool_duplicate_networks'} . "\n"
> . $default_raw->{pools}->{'test_pool_duplicate_storages'} . "\n"
> . $default_raw->{pools}->{'test_pool_duplicate_vms_expected'} . "\n"
> . $default_raw->{pools}->{'test_pool_members'}
> . "\n\n\n",
> },
> + {
> + name => "pool_format_backward_compatibility",
> + config => {
> + acl_root => default_acls(),
> + users => default_users(),
> + roles => default_roles(),
> + pools => default_pools_with([$default_cfg->{test_pool_empty}]),
> + },
> + raw => ""
> + . $default_raw->{users}->{'root@pam'}
> + . "\n\n\n"
> + # old format: 4 colons
> + . "pool:testpool::::\n\n\n",
> + expected_raw => ""
> + . $default_raw->{users}->{'root@pam'}
> + . "\n\n\n"
> + # new format 5: colons
> + . $default_raw->{pools}->{'test_pool_empty'}
> + . "\n\n\n",
> + },
nice!
> {
> name => "acl_simple_user",
> config => {
> @@ -1102,7 +1139,7 @@ my $tests = [
> . 'user:test@pam:0:0::::::' . "\n"
> . 'token:test@pam!test:0:0::' . "\n\n"
> . 'group:testgroup:::' . "\n\n"
> - . 'pool:testpool::::' . "\n\n"
> + . 'pool:testpool:::::' . "\n\n"
> . 'role:testrole::' . "\n\n",
> },
> ];
^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: [PATCH pve-network v2 06/10] fix #7294: sdn: register api formats for zones and vnets
2026-06-26 13:10 ` [PATCH pve-network v2 06/10] fix #7294: sdn: register api formats for zones and vnets David Riley
@ 2026-07-06 12:21 ` Daniel Kral
0 siblings, 0 replies; 23+ messages in thread
From: Daniel Kral @ 2026-07-06 12:21 UTC (permalink / raw)
To: David Riley, pve-devel
On Fri Jun 26, 2026 at 3:10 PM CEST, David Riley wrote:
> Register JSONSchema format validators for both SDN zones and VNets,
> enabling the pool members API to verify these network identifiers.
>
> Link: https://bugzilla.proxmox.com/show_bug.cgi?id=7294
> Signed-off-by: David Riley <d.riley@proxmox.com>
> ---
> src/PVE/Network/SDN/VnetPlugin.pm | 25 ++++++++++++++++++++++---
> src/PVE/Network/SDN/Zones/Plugin.pm | 25 ++++++++++++++++++++++---
> 2 files changed, 44 insertions(+), 6 deletions(-)
>
> diff --git a/src/PVE/Network/SDN/VnetPlugin.pm b/src/PVE/Network/SDN/VnetPlugin.pm
> index e041575..fbddc06 100644
> --- a/src/PVE/Network/SDN/VnetPlugin.pm
> +++ b/src/PVE/Network/SDN/VnetPlugin.pm
> @@ -16,17 +16,36 @@ PVE::Cluster::cfs_register_file(
> sub { __PACKAGE__->write_config(@_); },
> );
>
> +my $sdn_vnet_id_pattern = '[a-zA-Z][a-zA-Z0-9]*[a-zA-Z0-9]';
> +my $vnet_min_length = 2;
> +my $vnet_max_length = 8;
> +
> PVE::JSONSchema::register_standard_option(
> 'pve-sdn-vnet-id',
> {
> description => "The SDN vnet object identifier.",
> type => 'string',
> - pattern => '[a-zA-Z][a-zA-Z0-9]*[a-zA-Z0-9]',
> - minLength => 2,
> - maxLength => 8,
> + pattern => $sdn_vnet_id_pattern,
> + minLength => $vnet_min_length,
> + maxLength => $vnet_max_length,
nit: might be overkill, but add a: format => 'pve-sdn-vnet-id'
> },
> );
>
> +sub pve_verify_sdn_vnet_id {
> + my ($vnet, $noerr) = @_;
> +
> + my $len = length($vnet);
> +
> + if ($vnet !~ m/^$sdn_vnet_id_pattern$/ || $len < $vnet_min_length || $len > $vnet_max_length) {
> + return undef if $noerr;
> + die "invalid SDN VNet '$vnet', must be $vnet_min_length-$vnet_max_length characters"
> + . " long, start with a letter, and contain only alphanumeric characters\n";
> + }
> + return $vnet;
> +}
> +
> +PVE::JSONSchema::register_format('pve-sdn-vnet-id', \&pve_verify_sdn_vnet_id);
> +
> my $defaultData = {
>
> propertyList => {
> diff --git a/src/PVE/Network/SDN/Zones/Plugin.pm b/src/PVE/Network/SDN/Zones/Plugin.pm
> index 74a3384..81bcf86 100644
> --- a/src/PVE/Network/SDN/Zones/Plugin.pm
> +++ b/src/PVE/Network/SDN/Zones/Plugin.pm
> @@ -19,17 +19,36 @@ PVE::Cluster::cfs_register_file(
> sub { __PACKAGE__->write_config(@_); },
> );
>
> +my $sdn_zone_id_pattern = '[a-zA-Z][a-zA-Z0-9]*[a-zA-Z0-9]';
> +my $zone_min_length = 2;
> +my $zone_max_length = 8;
> +
> PVE::JSONSchema::register_standard_option(
> 'pve-sdn-zone-id',
> {
> description => "The SDN zone object identifier.",
> type => 'string',
> - pattern => '[a-zA-Z][a-zA-Z0-9]*[a-zA-Z0-9]',
> - minLength => 2,
> - maxLength => 8,
> + pattern => $sdn_zone_id_pattern,
> + minLength => $zone_min_length,
> + maxLength => $zone_max_length,
nit: might be overkill, but add a: format => 'pve-sdn-zone-id'
> },
> );
>
> +sub pve_verify_sdn_zone_id {
> + my ($zone, $noerr) = @_;
> +
> + my $len = length($zone);
> +
> + if ($zone !~ m/^$sdn_zone_id_pattern$/ || $len < $zone_min_length || $len > $zone_max_length) {
> + return undef if $noerr;
> + die "invalid SDN zone '$zone', must be $zone_min_length-$zone_max_length characters long,"
> + . " start with a letter, and contain only alphanumeric characters\n";
> + }
> + return $zone;
> +}
> +
> +PVE::JSONSchema::register_format('pve-sdn-zone-id', \&pve_verify_sdn_zone_id);
> +
> my $defaultData = {
>
> propertyList => {
Modulo the nits, consider this:
Reviewed-by: Daniel Kral <d.kral@proxmox.com>
^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: [PATCH pve-network v2 07/10] fix #7294: sdn: vnet: update pool members on vnet migration and deletion
2026-06-26 13:10 ` [PATCH pve-network v2 07/10] fix #7294: sdn: vnet: update pool members on vnet migration and deletion David Riley
@ 2026-07-06 12:29 ` Daniel Kral
0 siblings, 0 replies; 23+ messages in thread
From: Daniel Kral @ 2026-07-06 12:29 UTC (permalink / raw)
To: David Riley, pve-devel
On Fri Jun 26, 2026 at 3:10 PM CEST, David Riley wrote:
> Update or remove corresponding resource pool allocations in user.cfg
> whenever a VNet is altered or deleted. This prevents stale paths and
> dangling references from breaking the configuration integrity.
>
> Link: https://bugzilla.proxmox.com/show_bug.cgi?id=7294
> Signed-off-by: David Riley <d.riley@proxmox.com>
> ---
> src/PVE/Network/SDN.pm | 15 +++++++++++++++
> 1 file changed, 15 insertions(+)
>
> diff --git a/src/PVE/Network/SDN.pm b/src/PVE/Network/SDN.pm
> index cd563d2..81f5d9a 100644
> --- a/src/PVE/Network/SDN.pm
> +++ b/src/PVE/Network/SDN.pm
> @@ -261,11 +261,26 @@ sub cleanup {
> PVE::AccessControl::migrate_sdn_resource_access($vnet_move_paths);
> }
>
> + for my $move (@$vnet_move_paths) {
> + my ($src_type, $src_zone, $vnet) = @{ $move->{src_path} };
> + my ($dest_type, $dest_zone, $dest_vnet) = @{ $move->{dest_path} };
> +
> + if (defined($src_type) && $src_type eq 'zones' && $src_zone && $vnet && $dest_zone) {
Will there ever be any VNet paths to be moved, that are not zones?
Otherwise, I think the checks are a little overkill here. Since there
already quite a lot of definedness checks in diff_vnets(), these might
be redundant here.
> + PVE::AccessControl::migrate_vnet_zone_in_pool($src_zone, $dest_zone, $vnet);
> + }
> + }
> +
> my @paths_to_delete = (@$vnet_delete_paths, @$route_map_paths, @$generic_paths);
> if (@paths_to_delete) {
> PVE::AccessControl::remove_sdn_resource_access(\@paths_to_delete);
> }
>
> + for my $path (@$vnet_delete_paths) {
> + my ($type, $zone, $vnet) = @$path;
> + if ($type && $type eq 'zones' && $zone && $vnet) {
> + PVE::AccessControl::remove_vnet_from_pool($zone, $vnet);
> + }
Same here.
> + }
> }
>
> sub diff_generic_resources {
^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: [PATCH pve-cluster v2 09/10] fix #7294: cluster: helpers: add cluster-wide version assertion
2026-06-26 13:10 ` [PATCH pve-cluster v2 09/10] fix #7294: cluster: helpers: add cluster-wide version assertion David Riley
@ 2026-07-06 13:00 ` Daniel Kral
0 siblings, 0 replies; 23+ messages in thread
From: Daniel Kral @ 2026-07-06 13:00 UTC (permalink / raw)
To: David Riley, pve-devel
On Fri Jun 26, 2026 at 3:10 PM CEST, David Riley wrote:
> Add function to assert a minimum version requirement for the whole
> cluster.
>
> Evaluates against the cluster configuration. Dies if any node is
> offline, or running an older version, ensuring cluster-wide feature
> compatibility before allowing an operation to proceed.
>
> Prevents older nodes from truncating newly structured configuration
> entries.
>
> Link: https://bugzilla.proxmox.com/show_bug.cgi?id=7294
> Signed-off-by: David Riley <d.riley@proxmox.com>
> ---
> src/PVE/Cluster.pm | 43 +++++++++++++++++++++++++++++++++++++++++--
> 1 file changed, 41 insertions(+), 2 deletions(-)
>
> diff --git a/src/PVE/Cluster.pm b/src/PVE/Cluster.pm
> index 034b78c..3ecde03 100644
> --- a/src/PVE/Cluster.pm
> +++ b/src/PVE/Cluster.pm
> @@ -20,7 +20,7 @@ use PVE::IPCC;
> use PVE::JSONSchema;
> use PVE::Network;
> use PVE::SafeSyslog;
> -use PVE::Tools qw(run_command);
> +use PVE::Tools qw(run_command pvecfg_min_version);
>
> use PVE::Cluster::IPCConst;
>
> @@ -30,7 +30,9 @@ our @EXPORT_OK = qw(
> cfs_read_file
> cfs_write_file
> cfs_register_file
> - cfs_lock_file);
> + cfs_lock_file
> + assert_min_cluster_version
This export is not needed anymore if the function is always called as
PVE::Cluster::assert_min_cluster_version().
> +);
>
> # x509 certificate utils
>
> @@ -929,4 +931,41 @@ sub cfs_rename_db_unsafe {
> rename $dbfile, "$dbfile.$ctime.bak" or warn "failed to rename old config database - $!\n";
> }
>
> +# Asserts that all configured nodes in the cluster meet a minimum version requirement.
> +# Evaluates against the full, static cluster node list rather than only live nodes.
> +# Aborts if any node is offline, unreadable, or running an
> +# outdated version, preventing partial feature rollouts from corrupting cluster state.
> +# NOTE: Uses PVE::Tools::pvecfg_min_version, which currently checks basic major.minor.release
> +# triples. If gatekeeping a feature right around a major release transition, be aware that it
> +# does not yet lexically sort trailing Debian revision suffixes (e.g., distinguishing '9.0.0' as
> +# newer than '9.0.0~22').
> +sub assert_min_cluster_version {
> + my ($major, $minor, $release) = @_;
> +
> + my $nodestat = get_node_kv('version-info') || {};
> + my $nodelist = get_nodelist() || [];
nit: get_node_kv() will always return an empty hash ref {} and
get_nodelist() will always return an empty array ref [], so the
fallbacks aren't really needed
> +
> + for my $node (sort @$nodelist) {
> + my $raw_json = $nodestat->{$node};
> +
> + if (!defined($raw_json)) {
> + die "cannot execute operation: cluster node '$node' is offline or not reporting its"
> + . " version. All nodes must be online to verify cluster-wide feature support.\n";
> + }
> +
> + my $vinfo = eval { decode_json($raw_json) };
> + if ($@ || !$vinfo || !$vinfo->{version}) {
> + die "cannot execute operation: failed to parse cluster version string for node"
> + . " '$node'.\n";
> + }
> +
> + if (!pvecfg_min_version($vinfo->{version}, $major, $minor, $release)) {
> + die "cannot execute operation: cluster node '$node' runs an older version of Proxmox"
> + . " VE ($vinfo->{version}) than the required $major.$minor.$release. Please"
> + . " upgrade all nodes first.\n";
> + }
> + }
> + return 1;
> +}
> +
> 1;
Modulo the nits, consider this:
Reviewed-by: Daniel Kral <d.kral@proxmox.com>
^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: [PATCH pve-common v2 08/10] tools: add helpers for version comparison
2026-06-26 13:10 ` [PATCH pve-common v2 08/10] tools: add helpers for version comparison David Riley
@ 2026-07-06 13:02 ` Daniel Kral
0 siblings, 0 replies; 23+ messages in thread
From: Daniel Kral @ 2026-07-06 13:02 UTC (permalink / raw)
To: David Riley, pve-devel
On Fri Jun 26, 2026 at 3:10 PM CEST, David Riley wrote:
> Move 'version_cmp' and 'pvecfg_min_version' over from qemu-server, to
> make them more accessible.
>
> Originally-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
> Originally-by: Alexandre Derumier <aderumier@odiso.com>
> Signed-off-by: David Riley <d.riley@proxmox.com>
Reviewed-by: Daniel Kral <d.kral@proxmox.com>
^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: [PATCH qemu-server v2 10/10] fix #7294: helpers: use cluster-wide version helper
2026-06-26 13:10 ` [PATCH qemu-server v2 10/10] fix #7294: helpers: use cluster-wide version helper David Riley
@ 2026-07-06 13:20 ` Daniel Kral
0 siblings, 0 replies; 23+ messages in thread
From: Daniel Kral @ 2026-07-06 13:20 UTC (permalink / raw)
To: David Riley, pve-devel
On Fri Jun 26, 2026 at 3:10 PM CEST, David Riley wrote:
> Drop the local `version_cmp` and `pvecfg_min_version` implementations
> and update call sites to use the centralized tools module in
> pve-common.
>
> Link: https://bugzilla.proxmox.com/show_bug.cgi?id=7294
> Signed-off-by: David Riley <d.riley@proxmox.com>
The patch subject here (and in some other patches) shouldn't contain the
'fix #7294' as it isn't directly related to the bugzilla but only some
preparation.
Otherwise consider as:
Reviewed-by: Daniel Kral <d.kral@proxmox.com>
^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: [PATCH access-control/cluster/common/manager/network/proxmox-widget-toolkit/qemu-server v2 00/10] fix #7294: pool: add SDN VNets as pool members
2026-06-26 13:10 [PATCH access-control/cluster/common/manager/network/proxmox-widget-toolkit/qemu-server v2 00/10] fix #7294: pool: add SDN VNets as pool members David Riley
` (9 preceding siblings ...)
2026-06-26 13:10 ` [PATCH qemu-server v2 10/10] fix #7294: helpers: use cluster-wide version helper David Riley
@ 2026-07-06 14:07 ` Daniel Kral
10 siblings, 0 replies; 23+ messages in thread
From: Daniel Kral @ 2026-07-06 14:07 UTC (permalink / raw)
To: David Riley, pve-devel
On Fri Jun 26, 2026 at 3:10 PM CEST, David Riley wrote:
> This series implements support for adding SDN VNets to resource pools,
> resolving #7294 [0]. This series depends on the v4 'fix #7520: sdn:
> prune orphaned ACLs and handle VNet migrations' [1].
>
> It does not, however, add zones as pool members as requested in #7294.
> Zones currently share ACL paths for managing the zone itself and
> allocating VNets within it. This makes self-service VNet management
> without also granting zone management (and its associated
> side-effects) difficult.
>
> This patch series extends the pool section in the user.cfg and
> introduces a new network property to the pool configuration which will
> hold VNet entries:
> * vnet/<zone>/<vnet>
> * vnet/<zone>/<vnet>/<vlan>
>
> The type prefix allows future extension to other network resource
> types.
>
> To prevent potential data loss from overwriting newly added VNets, a
> cluster-version check is added which ensures all nodes are running a
> version that supports this feature. Note: The hardcoded version guard
> should be updated to match the final target release when being
> applied.
>
> The existing version check helpers were moved from `qemu-server` to a
> new module within `pve-cluster` to make them available for this
> implementation, and any future developments that require gatekeeping.
> Appropriate attribution has been included for the relocated code.
> Please let me know if this organizational move aligns with current
> design preferences or additional adjustments are needed.
Nice work on this series!
A nice addition could be to be able to display the changes done on
SDN config commit with the 'dry-run' API endpoint, but that would only
be a nice-to-have and definitely doesn't block this series in any way.
The patches #4, #6, #9, and #10 don't really need the 'fix #7294' prefix
as these are not really related to closing the bugzilla entry, but are
only prepatory steps, but not the actual fix.
Besides the comments inline, another quick thought is that if there is
some propagation from VNets to VLAN tags, either with the explicit
asterisk or not, it might be a good measure to check for duplicates,
e.g., if vnet/zone1/vnet1/* exists, then vnet/zone1/vnet/2 would already
include that and cannot be added. But this is more out of caution, so no
hard feelings about this.
^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: [PATCH pve-manager v2 03/10] fix #7294: ui: pool: add SDN VNets as pool members
2026-06-26 13:10 ` [PATCH pve-manager v2 03/10] fix #7294: ui: " David Riley
@ 2026-07-06 14:17 ` Daniel Kral
0 siblings, 0 replies; 23+ messages in thread
From: Daniel Kral @ 2026-07-06 14:17 UTC (permalink / raw)
To: David Riley, pve-devel
On Fri Jun 26, 2026 at 3:10 PM CEST, David Riley wrote:
> Add user interface to manage SDN VNets inside resource pools.
> In the dialog window users can select an SDN zone, pick an associated
> VNet, and optionally specify a VLAN tag.
>
> The VNet selector only shows VNets of the selected zone to prevent
> invalid configurations.
>
> Link: https://bugzilla.proxmox.com/show_bug.cgi?id=7294
> Signed-off-by: David Riley <d.riley@proxmox.com>
> ---
> www/css/ext6-pve.css | 15 ++++
> www/manager6/Utils.js | 1 +
> www/manager6/grid/PoolMembers.js | 123 +++++++++++++++++++++++++++++++
> 3 files changed, 139 insertions(+)
>
> diff --git a/www/css/ext6-pve.css b/www/css/ext6-pve.css
> index 5c37dd29..3c383dfa 100644
> --- a/www/css/ext6-pve.css
> +++ b/www/css/ext6-pve.css
> @@ -464,6 +464,21 @@ div.right-aligned {
> content: " ";
> }
>
> +.x-fa-pool-net:before {
> + width: 14px;
> + height: 14px;
> + position: absolute;
> + left: 1px;
> + top: 1px;
> + opacity: 0.45;
> +}
> +
> +.x-fa-pool-net-grid:before {
> + left: 14px;
> + top: 6px;
> + opacity: 0.65;
> +}
> +
The patch message should contain the information from patch #4, which
explains why there is an opacity applied here.
> .x-fa-treepanel:before {
> width: 16px;
> height: 24px;
> diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
> index 040b5ae0..8f53dfb6 100644
> --- a/www/manager6/Utils.js
> +++ b/www/manager6/Utils.js
> @@ -1322,6 +1322,7 @@ Ext.define('PVE.Utils', {
> const networkTypeMapping = {
> fabric: 'fa fa-road',
> zone: 'fa fa-th',
> + vnet: 'fa fa-network-wired x-fa-pool-net x-fa-pool-net-grid',
> };
>
> return networkTypeMapping[record['network-type']] ?? '';
> diff --git a/www/manager6/grid/PoolMembers.js b/www/manager6/grid/PoolMembers.js
> index 69f50e30..5aaa2b71 100644
> --- a/www/manager6/grid/PoolMembers.js
> +++ b/www/manager6/grid/PoolMembers.js
> @@ -158,6 +158,108 @@ Ext.define('PVE.pool.AddStorage', {
> },
> });
>
> +Ext.define('PVE.pool.AddVnet', {
> + extend: 'Proxmox.window.Edit',
> +
> + viewModel: {
> + data: {
> + zone: '',
> + },
> + formulas: {
> + vnetDisabled: function (get) {
> + return !get('zone');
> + },
> + },
> + },
> +
> + initComponent: function () {
> + let me = this;
> +
> + if (!me.pool) {
> + throw 'no pool specified';
> + }
> +
> + me.isCreate = true;
> + me.isAdd = true;
> + me.url = '/pools/';
> + me.method = 'PUT';
> + me.extraRequestParams.poolid = me.pool;
> +
> + Ext.apply(me, {
> + subject: gettext('VNet'),
> + width: 350,
> + items: [
> + {
> + xtype: 'inputpanel',
> + onGetValues: function (values) {
> + let network = `zone=${values.zone},vnet=${values.vnet}`;
> +
> + if (values.tag) {
> + network += `,tag=${values.tag}`;
> + }
> +
> + delete values.zone;
> + delete values.vnet;
> + delete values.tag;
> +
> + values.network = network;
> +
> + return values;
> + },
> + items: [
> + {
> + xtype: 'pveSDNZoneSelector',
> + fieldLabel: gettext('Zone'),
> + name: 'zone',
> + allowBlank: false,
> + bind: {
> + value: '{zone}',
> + },
> + listeners: {
> + change: function (_f, _value) {
> + let vnetField = me.down('field[name=vnet]');
> + if (vnetField) {
> + vnetField.setValue('');
> + }
nit: might be nice to use a 'reference' on 'vnet' and then reference it
here with lookupReference('vnet'), but no hard feelings.
> + },
> + },
> + },
> + {
> + xtype: 'pveSDNVnetSelector',
> + fieldLabel: gettext('VNet'),
> + name: 'vnet',
> + allowBlank: false,
> + bind: {
> + disabled: '{vnetDisabled}',
> + },
> + listeners: {
> + beforequery: function (_queryPlan) {
> + let zone = me.getViewModel().get('zone');
> + let store = this.getStore();
> + store.clearFilter();
> + store.filter('zone', zone);
> + return true;
> + },
> + },
> + },
> + {
> + xtype: 'proxmoxintegerfield',
> + name: 'tag',
> + fieldLabel: gettext('VLAN Tag'),
> + minValue: 1,
> + maxValue: 4094,
> + allowBlank: true,
> + emptyText: gettext('All'),
> + },
> + ],
> + },
> + ],
> + });
> +
> + me.callParent();
> + },
> +});
> +
> Ext.define('PVE.grid.PoolMembers', {
> extend: 'Ext.grid.GridPanel',
> alias: ['widget.pvePoolMembers'],
> @@ -223,6 +325,18 @@ Ext.define('PVE.grid.PoolMembers', {
> rec.data.type === 'openvz'
> ) {
> params.vms = rec.data.vmid;
> + } else if (rec.get('type') === 'network') {
> + if (rec.get('network-type') === 'vnet') {
> + let [_type, zone, vnet, tag] = rec.data.id.split('/');
> +
> + let network = `zone=${zone},vnet=${vnet}`;
> +
> + if (tag) {
> + network += `,tag=${tag}`;
> + }
> +
> + params.network = network;
> + }
should maybe console.warn() or something similar if the network type is
not recognized?
> } else {
> throw 'unknown resource type';
> }
> @@ -268,6 +382,15 @@ Ext.define('PVE.grid.PoolMembers', {
> win.show();
> },
> },
> + {
> + text: gettext('VNet'),
> + iconCls: 'fa fa-network-wired x-fa-pool-net',
> + handler: function () {
> + let win = Ext.create('PVE.pool.AddVnet', { pool: me.pool });
> + win.on('destroy', reload);
> + win.show();
> + },
> + },
> ],
> }),
> },
^ permalink raw reply [flat|nested] 23+ messages in thread
end of thread, other threads:[~2026-07-06 14:17 UTC | newest]
Thread overview: 23+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-26 13:10 [PATCH access-control/cluster/common/manager/network/proxmox-widget-toolkit/qemu-server v2 00/10] fix #7294: pool: add SDN VNets as pool members David Riley
2026-06-26 13:10 ` [PATCH pve-manager v2 01/10] ui: replace var with let to match style guide for variable declaration David Riley
2026-07-03 13:14 ` Daniel Kral
2026-06-26 13:10 ` [PATCH pve-manager v2 02/10] fix #7294: api: pool: add SDN VNets as pool members David Riley
2026-07-03 13:19 ` Daniel Kral
2026-06-26 13:10 ` [PATCH pve-manager v2 03/10] fix #7294: ui: " David Riley
2026-07-06 14:17 ` Daniel Kral
2026-06-26 13:10 ` [PATCH proxmox-widget-toolkit v2 04/10] fix #7294: css: theme: add opacity override for pool VNet icon David Riley
2026-07-03 13:30 ` Daniel Kral
2026-06-26 13:10 ` [PATCH pve-access-control v2 05/10] fix #7294: acl: pool: add SDN VNets as pool members David Riley
2026-07-03 14:35 ` Daniel Kral
2026-07-06 11:56 ` Daniel Kral
2026-06-26 13:10 ` [PATCH pve-network v2 06/10] fix #7294: sdn: register api formats for zones and vnets David Riley
2026-07-06 12:21 ` Daniel Kral
2026-06-26 13:10 ` [PATCH pve-network v2 07/10] fix #7294: sdn: vnet: update pool members on vnet migration and deletion David Riley
2026-07-06 12:29 ` Daniel Kral
2026-06-26 13:10 ` [PATCH pve-common v2 08/10] tools: add helpers for version comparison David Riley
2026-07-06 13:02 ` Daniel Kral
2026-06-26 13:10 ` [PATCH pve-cluster v2 09/10] fix #7294: cluster: helpers: add cluster-wide version assertion David Riley
2026-07-06 13:00 ` Daniel Kral
2026-06-26 13:10 ` [PATCH qemu-server v2 10/10] fix #7294: helpers: use cluster-wide version helper David Riley
2026-07-06 13:20 ` Daniel Kral
2026-07-06 14:07 ` [PATCH access-control/cluster/common/manager/network/proxmox-widget-toolkit/qemu-server v2 00/10] fix #7294: pool: add SDN VNets as pool members Daniel Kral
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox