* [PATCH pve-manager 1/9] ui: replace var with let to match style guide for variable declaration
2026-06-11 14:59 [PATCH access-control/cluster/manager/network/qemu-server 0/9] fix #7294: pool: add SDN VNets as pool members David Riley
@ 2026-06-11 14:59 ` David Riley
2026-06-11 14:59 ` [PATCH pve-manager 2/9] fix #7294: api: pool: add SDN VNets as pool members David Riley
` (7 subsequent siblings)
8 siblings, 0 replies; 17+ messages in thread
From: David Riley @ 2026-06-11 14:59 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] 17+ messages in thread* [PATCH pve-manager 2/9] fix #7294: api: pool: add SDN VNets as pool members
2026-06-11 14:59 [PATCH access-control/cluster/manager/network/qemu-server 0/9] fix #7294: pool: add SDN VNets as pool members David Riley
2026-06-11 14:59 ` [PATCH pve-manager 1/9] ui: replace var with let to match style guide for variable declaration David Riley
@ 2026-06-11 14:59 ` David Riley
2026-06-11 14:59 ` [PATCH pve-manager 3/9] fix #7294: ui: " David Riley
` (6 subsequent siblings)
8 siblings, 0 replies; 17+ messages in thread
From: David Riley @ 2026-06-11 14:59 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 | 137 ++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 135 insertions(+), 2 deletions(-)
diff --git a/PVE/API2/Pool.pm b/PVE/API2/Pool.pm
index 63aff5bb..f1eb6eb3 100644
--- a/PVE/API2/Pool.pm
+++ b/PVE/API2/Pool.pm
@@ -5,8 +5,10 @@ use warnings;
use PVE::AccessControl;
use PVE::Cluster qw (cfs_read_file cfs_write_file);
+use PVE::Cluster::Helpers qw(assert_min_cluster_version);
use PVE::Exception qw(raise_param_exc);
use PVE::INotify;
+use PVE::Network;
use PVE::Storage;
use PVE::SafeSyslog;
@@ -61,7 +63,7 @@ __PACKAGE__->register_method({
properties => {
type => {
type => 'string',
- enum => ['qemu', 'lxc', 'openvz', 'storage'],
+ enum => ['qemu', 'lxc', 'openvz', 'storage', 'network'],
},
id => {
type => 'string',
@@ -77,6 +79,10 @@ __PACKAGE__->register_method({
type => 'string',
optional => 1,
},
+ vnet => {
+ type => 'string',
+ optional => 1,
+ },
},
},
},
@@ -135,6 +141,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, $vlan) = @path;
+
+ my $description = "$vnet ($zoneid)";
+ $description = "$vnet.$vlan ($zoneid)" if defined($vlan);
+
+ push @$members,
+ {
+ type => 'network',
+ id => $net_key,
+ text => $description,
+ 'network-type' => $type,
+ };
+ }
+ }
+ }
+ }
+
my $pool_info = {
members => $members,
};
@@ -243,6 +272,25 @@ __PACKAGE__->register_method({
format => 'pve-storage-id-list',
optional => 1,
},
+ zone => {
+ description => 'SDN Zone',
+ type => 'string',
+ format => 'pve-sdn-zone-id',
+ optional => 1,
+ },
+ vnet => {
+ description => 'VNet to add or remove from this pool.',
+ type => 'string',
+ format => 'pve-sdn-vnet-id',
+ optional => 1,
+ },
+ tag => {
+ description => "Specify a VLAN tag",
+ optional => 1,
+ type => 'integer',
+ minimum => 1,
+ maximum => 4094,
+ },
'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 +343,25 @@ __PACKAGE__->register_method({
format => 'pve-storage-id-list',
optional => 1,
},
+ zone => {
+ description => 'SDN Zone',
+ type => 'string',
+ format => 'pve-sdn-zone-id',
+ optional => 1,
+ },
+ vnet => {
+ description => 'VNet to add or remove from this pool.',
+ type => 'string',
+ format => 'pve-sdn-vnet-id',
+ optional => 1,
+ },
+ tag => {
+ description => "Specify a VLAN tag",
+ optional => 1,
+ type => 'integer',
+ minimum => 1,
+ maximum => 4094,
+ },
'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 +371,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 VNets instead of adding them.',
type => 'boolean',
optional => 1,
default => 0,
@@ -373,6 +440,56 @@ __PACKAGE__->register_method({
}
}
+ if (defined($param->{vnet}) && defined($param->{zone})) {
+ # gatekeep vnet as pool members
+ assert_min_cluster_version(9, 2, 3);
+
+ my $zones_cfg = PVE::Network::SDN::Zones::config();
+ my $zone = $param->{zone};
+
+ if (!$zones_cfg->{ids}->{$zone}) {
+ die "SDN Zone '$zone' does not exist\n";
+ }
+
+ my $vnets_cfg = PVE::Network::SDN::Vnets::config();
+ my $tag = $param->{tag};
+
+ my $vnetid = $param->{vnet};
+ 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 $has_tag = defined($tag) && $tag ne '';
+ if ($has_tag) {
+ my $native_tag = $vnet_data->{tag};
+ if (!defined($native_tag) || $tag != $native_tag) {
+ die
+ "VNet '$vnetid' is not VLAN-aware, cannot assign a specific tag\n"
+ if !$vnet_data->{vlanaware};
+ }
+ }
+
+ my $network_key = "vnet/$vnet_zone/$vnetid";
+ $network_key .= "/$tag" if $has_tag;
+
+ $rpcenv->check_perm_modify(
+ $authuser,
+ "/sdn/zones/$vnet_zone/$vnetid",
+ ['SDN.Allocate'],
+ );
+
+ if ($param->{delete}) {
+ delete $pool_config->{network}->{$network_key};
+ } else {
+ $pool_config->{network}->{$network_key} = 1;
+ }
+ }
+
cfs_write_file("user.cfg", $usercfg);
},
"update pools failed",
@@ -437,6 +554,14 @@ __PACKAGE__->register_method({
type => 'string',
optional => 1,
},
+ zone => {
+ type => 'string',
+ optional => 1,
+ },
+ vnet => {
+ type => 'string',
+ optional => 1,
+ },
},
},
},
@@ -524,6 +649,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';
+ $id //= $netid;
+
+ 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] 17+ messages in thread* [PATCH pve-manager 3/9] fix #7294: ui: pool: add SDN VNets as pool members
2026-06-11 14:59 [PATCH access-control/cluster/manager/network/qemu-server 0/9] fix #7294: pool: add SDN VNets as pool members David Riley
2026-06-11 14:59 ` [PATCH pve-manager 1/9] ui: replace var with let to match style guide for variable declaration David Riley
2026-06-11 14:59 ` [PATCH pve-manager 2/9] fix #7294: api: pool: add SDN VNets as pool members David Riley
@ 2026-06-11 14:59 ` David Riley
2026-06-11 14:59 ` [PATCH pve-access-control 4/9] fix #7294: acl: " David Riley
` (5 subsequent siblings)
8 siblings, 0 replies; 17+ messages in thread
From: David Riley @ 2026-06-11 14:59 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 | 13 ++++
www/manager6/Utils.js | 1 +
www/manager6/grid/PoolMembers.js | 102 +++++++++++++++++++++++++++++++
3 files changed, 116 insertions(+)
diff --git a/www/css/ext6-pve.css b/www/css/ext6-pve.css
index 27742a74..9ec2a3af 100644
--- a/www/css/ext6-pve.css
+++ b/www/css/ext6-pve.css
@@ -470,6 +470,19 @@ div.right-aligned {
content: " ";
}
+.x-fa-pool-net:before {
+ width: 14px;
+ height: 14px;
+ position: absolute;
+ left: 1px;
+ top: 1px;
+}
+
+.x-fa-pool-net-grid:before {
+ left: 14px;
+ top: 6px;
+}
+
.x-fa-treepanel:before {
width: 16px;
height: 24px;
diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index bbf59d8f..80331933 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1320,6 +1320,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..3c902b67 100644
--- a/www/manager6/grid/PoolMembers.js
+++ b/www/manager6/grid/PoolMembers.js
@@ -158,6 +158,88 @@ 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 () {
+ var 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: 'pveSDNZoneSelector',
+ fieldLabel: gettext('Zone'),
+ name: 'zone',
+ allowBlank: false,
+ bind: {
+ value: '{zone}',
+ },
+ listeners: {
+ change: function (_f, _value) {
+ var 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) {
+ var zone = me.getViewModel().get('zone');
+ var 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 +305,17 @@ Ext.define('PVE.grid.PoolMembers', {
rec.data.type === 'openvz'
) {
params.vms = rec.data.vmid;
+ } else if (rec.data.type === 'network') {
+ if (rec.get('network-type') === 'vnet') {
+ let [_type, zone, vnet, tag] = rec.data.id.split('/');
+
+ params.zone = zone;
+ params.vnet = vnet;
+
+ if (tag) {
+ params.tag = tag;
+ }
+ }
} else {
throw 'unknown resource type';
}
@@ -268,6 +361,15 @@ Ext.define('PVE.grid.PoolMembers', {
win.show();
},
},
+ {
+ text: gettext('VNet'),
+ iconCls: 'fa fa-network-wired x-fa-pool-net',
+ handler: function () {
+ var win = Ext.create('PVE.pool.AddVnet', { pool: me.pool });
+ win.on('destroy', reload);
+ win.show();
+ },
+ },
],
}),
},
--
2.47.3
^ permalink raw reply related [flat|nested] 17+ messages in thread* [PATCH pve-access-control 4/9] fix #7294: acl: pool: add SDN VNets as pool members
2026-06-11 14:59 [PATCH access-control/cluster/manager/network/qemu-server 0/9] fix #7294: pool: add SDN VNets as pool members David Riley
` (2 preceding siblings ...)
2026-06-11 14:59 ` [PATCH pve-manager 3/9] fix #7294: ui: " David Riley
@ 2026-06-11 14:59 ` David Riley
2026-06-11 14:59 ` [PATCH pve-network 5/9] fix #7294: sdn: register api formats for zones and vnets David Riley
` (4 subsequent siblings)
8 siblings, 0 replies; 17+ messages in thread
From: David Riley @ 2026-06-11 14:59 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 | 88 +++++++++++++++++++++++++++++++++++++--
src/PVE/RPCEnvironment.pm | 47 +++++++++++++++++++++
src/test/parser_writer.pl | 53 +++++++++++++++++++----
3 files changed, 176 insertions(+), 12 deletions(-)
diff --git a/src/PVE/AccessControl.pm b/src/PVE/AccessControl.pm
index b7bb3eb..db452a2 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,7 +1576,8 @@ 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 => {} }
+ $cfg->{pools}->{$parent} =
+ { vms => {}, storage => {}, pools => {}, network => {} }
if !$cfg->{pools}->{$parent};
$cfg->{pools}->{$parent}->{pools}->{$curr} = 1;
$curr = $parent;
@@ -1610,6 +1611,18 @@ sub parse_user_config {
}
$cfg->{pools}->{$pool}->{storage}->{$storeid} = 1;
}
+
+ foreach 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 +1704,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 +1988,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 $delVNetfromPoolFn = sub {
+ my $usercfg = cfs_read_file("user.cfg");
+ return if !$usercfg->{pools};
+
+ my $modified = 0;
+ foreach my $pool (keys %{ $usercfg->{pools} }) {
+ my $pool_cfg = $usercfg->{pools}->{$pool};
+ next if !$pool_cfg->{network};
+
+ foreach 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($delVNetfromPoolFn, "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 $updateVNetZoneFn = sub {
+ my $usercfg = cfs_read_file("user.cfg");
+ return if !$usercfg->{pools};
+
+ my $modified = 0;
+ foreach my $pool (keys %{ $usercfg->{pools} }) {
+ my $pool_cfg = $usercfg->{pools}->{$pool};
+ next if !$pool_cfg->{network};
+
+ foreach 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($updateVNetZoneFn, "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..d8fb671 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;
}
}
+
+ foreach 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) && $vlan ne '';
+
+ for my $role (keys %$pool_roles) {
+ $data->{poolroles}->{$acl_path}->{$role} = 1;
+ }
+ }
+ }
}
}
@@ -219,6 +234,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 +291,17 @@ sub get_effective_permissions {
foreach my $storeid (keys %{ $d->{storage} }) {
$paths->{"/storage/$storeid"} = 1;
}
+
+ foreach 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) && $vlan ne '';
+ $paths->{$vnet_path} = 1;
+ }
+ }
}
my $perms = {};
@@ -353,6 +381,25 @@ sub check_sdn_bridge {
}
}
+ # check access to VLANs via pools
+ foreach my $pool (keys %{ $cfg->{pools} }) {
+ my $d = $cfg->{pools}->{$pool};
+ next if !$d->{network};
+
+ foreach my $network_key (keys %{ $d->{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] 17+ messages in thread* [PATCH pve-network 5/9] fix #7294: sdn: register api formats for zones and vnets
2026-06-11 14:59 [PATCH access-control/cluster/manager/network/qemu-server 0/9] fix #7294: pool: add SDN VNets as pool members David Riley
` (3 preceding siblings ...)
2026-06-11 14:59 ` [PATCH pve-access-control 4/9] fix #7294: acl: " David Riley
@ 2026-06-11 14:59 ` David Riley
2026-06-12 12:18 ` Gabriel Goller
2026-06-11 14:59 ` [PATCH pve-network 6/9] fix #7294: sdn: vnet: update pool members on vnet migration and deletion David Riley
` (3 subsequent siblings)
8 siblings, 1 reply; 17+ messages in thread
From: David Riley @ 2026-06-11 14:59 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 | 23 ++++++++++++++++++++---
src/PVE/Network/SDN/Zones/Plugin.pm | 23 ++++++++++++++++++++---
2 files changed, 40 insertions(+), 6 deletions(-)
diff --git a/src/PVE/Network/SDN/VnetPlugin.pm b/src/PVE/Network/SDN/VnetPlugin.pm
index e041575..2299b46 100644
--- a/src/PVE/Network/SDN/VnetPlugin.pm
+++ b/src/PVE/Network/SDN/VnetPlugin.pm
@@ -16,17 +16,34 @@ 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) = @_;
+
+ if ($vnet !~ m/^$sdn_vnet_id_pattern$/) {
+ 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..cd761e0 100644
--- a/src/PVE/Network/SDN/Zones/Plugin.pm
+++ b/src/PVE/Network/SDN/Zones/Plugin.pm
@@ -19,17 +19,34 @@ 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) = @_;
+
+ if ($zone !~ m/^$sdn_zone_id_pattern$/) {
+ 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] 17+ messages in thread* Re: [PATCH pve-network 5/9] fix #7294: sdn: register api formats for zones and vnets
2026-06-11 14:59 ` [PATCH pve-network 5/9] fix #7294: sdn: register api formats for zones and vnets David Riley
@ 2026-06-12 12:18 ` Gabriel Goller
2026-06-12 12:51 ` David Riley
0 siblings, 1 reply; 17+ messages in thread
From: Gabriel Goller @ 2026-06-12 12:18 UTC (permalink / raw)
To: David Riley; +Cc: pve-devel
Generally on this series:
What was your rationale on adding the vlan tag? IMO having
`vnet/<zone>/<vnet>/<vlan>` doesn't really make sense, as not all vnets have
a tag property, and it's also not always a vlan e.g. EVPN vnets have a vni-tag
property.
One small comment inline as well.
> [snip]
> diff --git a/src/PVE/Network/SDN/VnetPlugin.pm b/src/PVE/Network/SDN/VnetPlugin.pm
> index e04157573083..2299b46601c2 100644
> --- a/src/PVE/Network/SDN/VnetPlugin.pm
> +++ b/src/PVE/Network/SDN/VnetPlugin.pm
> @@ -16,17 +16,34 @@ 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) = @_;
> +
> + if ($vnet !~ m/^$sdn_vnet_id_pattern$/) {
> + 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;
> +}
I think this is missing a min/max lenght check?
> +
> +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 74a3384cd7ae..cd761e0448c3 100644
> --- a/src/PVE/Network/SDN/Zones/Plugin.pm
> +++ b/src/PVE/Network/SDN/Zones/Plugin.pm
> @@ -19,17 +19,34 @@ 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) = @_;
> +
> + if ($zone !~ m/^$sdn_zone_id_pattern$/) {
> + 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;
> +}
Here as well.
--
Gabriel Goller <g.goller@proxmox.com>
^ permalink raw reply [flat|nested] 17+ messages in thread* Re: [PATCH pve-network 5/9] fix #7294: sdn: register api formats for zones and vnets
2026-06-12 12:18 ` Gabriel Goller
@ 2026-06-12 12:51 ` David Riley
2026-06-12 13:46 ` Gabriel Goller
0 siblings, 1 reply; 17+ messages in thread
From: David Riley @ 2026-06-12 12:51 UTC (permalink / raw)
To: Gabriel Goller; +Cc: pve-devel
Thanks for the feedback.
The intention behind adding this segment to the ACL path is to allow
for fine-grained, hierarchical permission scoping, not to couple the
ACL system to specific VNet properties.
I used vlan as a placeholder for 'tag', but in retrospect, the naming
is a bit confusing, and I'm happy to adapt this in a v2.
From a permission perspective, including the tag in the path makes
sense, as it allows us to restrict pool users to a specific VNet and
tag combination.
So if you have a pool with a VM, storage and VNet + Tag and assign the
pool permissions: PVEVMAdmin, PVESDNUser
The user can fully manage this VM, including adding a new NIC, but
they can only add it using the exact VNet + Tag combination. Just
adding the VNet would not work.
Let me know if this makes sense.
More inline.
On 6/12/26 2:17 PM, Gabriel Goller wrote:
> Generally on this series:
> What was your rationale on adding the vlan tag? IMO having
> `vnet/<zone>/<vnet>/<vlan>` doesn't really make sense, as not all vnets have
> a tag property, and it's also not always a vlan e.g. EVPN vnets have a vni-tag
> property.
>
> One small comment inline as well.
>
>> [snip]
>> diff --git a/src/PVE/Network/SDN/VnetPlugin.pm b/src/PVE/Network/SDN/VnetPlugin.pm
>> index e04157573083..2299b46601c2 100644
>> --- a/src/PVE/Network/SDN/VnetPlugin.pm
>> +++ b/src/PVE/Network/SDN/VnetPlugin.pm
>> @@ -16,17 +16,34 @@ 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) = @_;
>> +
>> + if ($vnet !~ m/^$sdn_vnet_id_pattern$/) {
>> + 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;
>> +}
> I think this is missing a min/max lenght check?
>
You are right. Will fix this in a v2.
>> +
>> +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 74a3384cd7ae..cd761e0448c3 100644
>> --- a/src/PVE/Network/SDN/Zones/Plugin.pm
>> +++ b/src/PVE/Network/SDN/Zones/Plugin.pm
>> @@ -19,17 +19,34 @@ 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) = @_;
>> +
>> + if ($zone !~ m/^$sdn_zone_id_pattern$/) {
>> + 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;
>> +}
> Here as well.
ack.
^ permalink raw reply [flat|nested] 17+ messages in thread* Re: [PATCH pve-network 5/9] fix #7294: sdn: register api formats for zones and vnets
2026-06-12 12:51 ` David Riley
@ 2026-06-12 13:46 ` Gabriel Goller
2026-06-12 14:17 ` David Riley
0 siblings, 1 reply; 17+ messages in thread
From: Gabriel Goller @ 2026-06-12 13:46 UTC (permalink / raw)
To: David Riley; +Cc: pve-devel
On 2026-06-12 14:51 +0200, David Riley wrote:
> Thanks for the feedback.
>
> The intention behind adding this segment to the ACL path is to allow
> for fine-grained, hierarchical permission scoping, not to couple the
> ACL system to specific VNet properties.
>
> I used vlan as a placeholder for 'tag', but in retrospect, the naming
> is a bit confusing, and I'm happy to adapt this in a v2.
>
> From a permission perspective, including the tag in the path makes
> sense, as it allows us to restrict pool users to a specific VNet and
> tag combination.
Maybe I'm a bit dim, but you mean the VNet tag right -- so the tag I configure
on each VNet?
> So if you have a pool with a VM, storage and VNet + Tag and assign the
> pool permissions: PVEVMAdmin, PVESDNUser
>
> The user can fully manage this VM, including adding a new NIC, but
> they can only add it using the exact VNet + Tag combination. Just
> adding the VNet would not work.
How is this different than just adding the VNet? Will this restrict the user
from adding another vlan tag on the VM (when using vlanaware vnets)? Is this
just to prevent the user from editing the VNet tag?
> Let me know if this makes sense.
>
> [snip]
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH pve-network 5/9] fix #7294: sdn: register api formats for zones and vnets
2026-06-12 13:46 ` Gabriel Goller
@ 2026-06-12 14:17 ` David Riley
0 siblings, 0 replies; 17+ messages in thread
From: David Riley @ 2026-06-12 14:17 UTC (permalink / raw)
To: Gabriel Goller; +Cc: pve-devel
On 6/12/26 3:46 PM, Gabriel Goller wrote:
> On 2026-06-12 14:51 +0200, David Riley wrote:
>> Thanks for the feedback.
>>
>> The intention behind adding this segment to the ACL path is to allow
>> for fine-grained, hierarchical permission scoping, not to couple the
>> ACL system to specific VNet properties.
>>
>> I used vlan as a placeholder for 'tag', but in retrospect, the naming
>> is a bit confusing, and I'm happy to adapt this in a v2.
>>
>> From a permission perspective, including the tag in the path makes
>> sense, as it allows us to restrict pool users to a specific VNet and
>> tag combination.
> Maybe I'm a bit dim, but you mean the VNet tag right -- so the tag I configure
> on each VNet?
No, I don't mean the tag configured on the VNet itself. I mean the
VLAN tag configured on the guest's network interface (NIC) when
attaching the VM to a VLAN-aware VNet.
>> So if you have a pool with a VM, storage and VNet + Tag and assign the
>> pool permissions: PVEVMAdmin, PVESDNUser
>>
>> The user can fully manage this VM, including adding a new NIC, but
>> they can only add it using the exact VNet + Tag combination. Just
>> adding the VNet would not work.
> How is this different than just adding the VNet? Will this restrict the user
> from adding another vlan tag on the VM (when using vlanaware vnets)? Is this
> just to prevent the user from editing the VNet tag?
Yes it will restrict the user.
This is how I tested it (and what I envision the primary use case to
be) is this:
* You create a simple zone.
* In that zone, you create a VNet and make it vlanaware.
* You add this VNet to the pool by selecting the zone, the VNet, and
setting a specific tag (e.g., 1).
The user then gets the following ACL path from the pool:
/sdn/zones/zone/vnet/1
If the user tries to add a network card to their VM using only the
zone and VNet without the VLAN tag, it will fail:
Permission check failed (/sdn/zones/zone/vnet, SDN.Use) (403)
Trying to assign a different VLAN tag (e.g., 2) will also fail:
Permission check failed (/sdn/zones/zone/vnet/2, SDN.Use) (403)
Only using the zone, VNet, and tag 1 will work.
So it effectively allows an admin to share a single VLAN-aware VNet
across multiple pools, strictly isolating users to their assigned
VLANs.
>> Let me know if this makes sense.
>>
>> [snip]
^ permalink raw reply [flat|nested] 17+ messages in thread
* [PATCH pve-network 6/9] fix #7294: sdn: vnet: update pool members on vnet migration and deletion
2026-06-11 14:59 [PATCH access-control/cluster/manager/network/qemu-server 0/9] fix #7294: pool: add SDN VNets as pool members David Riley
` (4 preceding siblings ...)
2026-06-11 14:59 ` [PATCH pve-network 5/9] fix #7294: sdn: register api formats for zones and vnets David Riley
@ 2026-06-11 14:59 ` David Riley
2026-06-11 16:21 ` Gabriel Goller
2026-06-11 14:59 ` [PATCH pve-cluster 7/9] cluster: add helpers module with version comparison functions David Riley
` (2 subsequent siblings)
8 siblings, 1 reply; 17+ messages in thread
From: David Riley @ 2026-06-11 14:59 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 6a49621..1541f8e 100644
--- a/src/PVE/Network/SDN.pm
+++ b/src/PVE/Network/SDN.pm
@@ -259,11 +259,26 @@ sub cleanup {
PVE::AccessControl::migrate_sdn_resource_access($vnet_move_paths);
}
+ foreach 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);
}
+ foreach 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] 17+ messages in thread* Re: [PATCH pve-network 6/9] fix #7294: sdn: vnet: update pool members on vnet migration and deletion
2026-06-11 14:59 ` [PATCH pve-network 6/9] fix #7294: sdn: vnet: update pool members on vnet migration and deletion David Riley
@ 2026-06-11 16:21 ` Gabriel Goller
2026-06-12 6:37 ` David Riley
0 siblings, 1 reply; 17+ messages in thread
From: Gabriel Goller @ 2026-06-11 16:21 UTC (permalink / raw)
To: David Riley; +Cc: pve-devel
On 2026-06-11 16:59 +0200, 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>
> ---
This patch doesn't apply on latest master.
> 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 6a49621..1541f8e 100644
> --- a/src/PVE/Network/SDN.pm
> +++ b/src/PVE/Network/SDN.pm
> @@ -259,11 +259,26 @@ sub cleanup {
> PVE::AccessControl::migrate_sdn_resource_access($vnet_move_paths);
> }
>
> + foreach 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);
> }
>
> + foreach 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 [flat|nested] 17+ messages in thread* Re: [PATCH pve-network 6/9] fix #7294: sdn: vnet: update pool members on vnet migration and deletion
2026-06-11 16:21 ` Gabriel Goller
@ 2026-06-12 6:37 ` David Riley
2026-06-12 8:41 ` Gabriel Goller
0 siblings, 1 reply; 17+ messages in thread
From: David Riley @ 2026-06-12 6:37 UTC (permalink / raw)
To: Gabriel Goller; +Cc: pve-devel
On 6/11/26 6:21 PM, Gabriel Goller wrote:
> On 2026-06-11 16:59 +0200, 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>
>> ---
> This patch doesn't apply on latest master.
Thanks for taking a look at it.
You are completely right that it fails on the current master. As mentioned in the
cover letter, this series depends on my earlier patch series [0].
Just tested it locally and it should apply cleanly after applying this series first.
[0] https://lore.proxmox.com/pve-devel/20260603145523.120075-1-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 6a49621..1541f8e 100644
>> --- a/src/PVE/Network/SDN.pm
>> +++ b/src/PVE/Network/SDN.pm
>> @@ -259,11 +259,26 @@ sub cleanup {
>> PVE::AccessControl::migrate_sdn_resource_access($vnet_move_paths);
>> }
>>
>> + foreach 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);
>> }
>>
>> + foreach 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 [flat|nested] 17+ messages in thread* Re: [PATCH pve-network 6/9] fix #7294: sdn: vnet: update pool members on vnet migration and deletion
2026-06-12 6:37 ` David Riley
@ 2026-06-12 8:41 ` Gabriel Goller
0 siblings, 0 replies; 17+ messages in thread
From: Gabriel Goller @ 2026-06-12 8:41 UTC (permalink / raw)
To: David Riley; +Cc: pve-devel
On 12.06.2026 08:37, David Riley wrote:
>
> On 6/11/26 6:21 PM, Gabriel Goller wrote:
> > On 2026-06-11 16:59 +0200, 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>
> > > ---
> > This patch doesn't apply on latest master.
>
> Thanks for taking a look at it.
> You are completely right that it fails on the current master. As mentioned in the
> cover letter, this series depends on my earlier patch series [0].
> Just tested it locally and it should apply cleanly after applying this series first.
>
> [0] https://lore.proxmox.com/pve-devel/20260603145523.120075-1-d.riley@proxmox.com/
Turns out I can't read.
Sorry for the noise.
> > > 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 6a49621..1541f8e 100644
> > > --- a/src/PVE/Network/SDN.pm
> > > +++ b/src/PVE/Network/SDN.pm
> > > @@ -259,11 +259,26 @@ sub cleanup {
> > > PVE::AccessControl::migrate_sdn_resource_access($vnet_move_paths);
> > > }
> > > + foreach 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);
> > > }
> > > + foreach 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 [flat|nested] 17+ messages in thread
* [PATCH pve-cluster 7/9] cluster: add helpers module with version comparison functions
2026-06-11 14:59 [PATCH access-control/cluster/manager/network/qemu-server 0/9] fix #7294: pool: add SDN VNets as pool members David Riley
` (5 preceding siblings ...)
2026-06-11 14:59 ` [PATCH pve-network 6/9] fix #7294: sdn: vnet: update pool members on vnet migration and deletion David Riley
@ 2026-06-11 14:59 ` David Riley
2026-06-11 14:59 ` [PATCH pve-cluster 8/9] fix #7294: cluster: helpers: add cluster-wide version assertion David Riley
2026-06-11 14:59 ` [PATCH qemu-server 9/9] fix #7294: helpers: use cluster-wide version helper David Riley
8 siblings, 0 replies; 17+ messages in thread
From: David Riley @ 2026-06-11 14:59 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>
---
debian/pve-cluster.install | 1 +
src/PVE/Cluster/Helpers.pm | 51 ++++++++++++++++++++++++++++++++++++++
src/PVE/Cluster/Makefile | 2 +-
3 files changed, 53 insertions(+), 1 deletion(-)
create mode 100644 src/PVE/Cluster/Helpers.pm
diff --git a/debian/pve-cluster.install b/debian/pve-cluster.install
index f66cd06..46d40c4 100644
--- a/debian/pve-cluster.install
+++ b/debian/pve-cluster.install
@@ -5,4 +5,5 @@ usr/lib/
usr/share/man/man8/pmxcfs.8
usr/share/perl5/PVE/Cluster.pm
usr/share/perl5/PVE/Cluster/IPCConst.pm
+usr/share/perl5/PVE/Cluster/Helpers.pm
usr/share/perl5/PVE/IPCC.pm
diff --git a/src/PVE/Cluster/Helpers.pm b/src/PVE/Cluster/Helpers.pm
new file mode 100644
index 0000000..a4b41c9
--- /dev/null
+++ b/src/PVE/Cluster/Helpers.pm
@@ -0,0 +1,51 @@
+package PVE::Cluster::Helpers;
+
+use strict;
+use warnings;
+
+use JSON;
+
+use base 'Exporter';
+our @EXPORT_OK = qw(pvecfg_min_version assert_min_cluster_version version_cmp);
+
+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;
+
diff --git a/src/PVE/Cluster/Makefile b/src/PVE/Cluster/Makefile
index 3f920cb..db685bc 100644
--- a/src/PVE/Cluster/Makefile
+++ b/src/PVE/Cluster/Makefile
@@ -1,6 +1,6 @@
PVEDIR=$(DESTDIR)/usr/share/perl5/PVE
-SOURCES=IPCConst.pm Setup.pm
+SOURCES=Helpers.pm IPCConst.pm Setup.pm
.PHONY: install
install: $(SOURCES)
--
2.47.3
^ permalink raw reply related [flat|nested] 17+ messages in thread* [PATCH pve-cluster 8/9] fix #7294: cluster: helpers: add cluster-wide version assertion
2026-06-11 14:59 [PATCH access-control/cluster/manager/network/qemu-server 0/9] fix #7294: pool: add SDN VNets as pool members David Riley
` (6 preceding siblings ...)
2026-06-11 14:59 ` [PATCH pve-cluster 7/9] cluster: add helpers module with version comparison functions David Riley
@ 2026-06-11 14:59 ` David Riley
2026-06-11 14:59 ` [PATCH qemu-server 9/9] fix #7294: helpers: use cluster-wide version helper David Riley
8 siblings, 0 replies; 17+ messages in thread
From: David Riley @ 2026-06-11 14:59 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 user.cfg
entries.
Link: https://bugzilla.proxmox.com/show_bug.cgi?id=7294
Signed-off-by: David Riley <d.riley@proxmox.com>
---
src/PVE/Cluster/Helpers.pm | 35 +++++++++++++++++++++++++++++++++++
1 file changed, 35 insertions(+)
diff --git a/src/PVE/Cluster/Helpers.pm b/src/PVE/Cluster/Helpers.pm
index a4b41c9..3937e6d 100644
--- a/src/PVE/Cluster/Helpers.pm
+++ b/src/PVE/Cluster/Helpers.pm
@@ -47,5 +47,40 @@ sub version_cmp {
return 0;
}
+# 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.
+sub assert_min_cluster_version {
+ my ($major, $minor, $release) = @_;
+
+ my $nodestat = PVE::Cluster::get_node_kv('version-info') || {};
+
+ my $clinfo = PVE::Cluster::get_clinfo() || {};
+ my $nodelist = $clinfo->{nodelist} || {};
+
+ foreach my $node (sort keys %$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] 17+ messages in thread* [PATCH qemu-server 9/9] fix #7294: helpers: use cluster-wide version helper
2026-06-11 14:59 [PATCH access-control/cluster/manager/network/qemu-server 0/9] fix #7294: pool: add SDN VNets as pool members David Riley
` (7 preceding siblings ...)
2026-06-11 14:59 ` [PATCH pve-cluster 8/9] fix #7294: cluster: helpers: add cluster-wide version assertion David Riley
@ 2026-06-11 14:59 ` David Riley
8 siblings, 0 replies; 17+ messages in thread
From: David Riley @ 2026-06-11 14:59 UTC (permalink / raw)
To: pve-devel
Drop the local `version_cmp` and `pvecfg_min_version` implementations
and update call sites to use the centralized helpers module in
pve-cluster.
Link: https://bugzilla.proxmox.com/show_bug.cgi?id=7294
Signed-off-by: David Riley <d.riley@proxmox.com>
---
src/PVE/QemuMigrate.pm | 2 +-
src/PVE/QemuServer/Helpers.pm | 40 +----------------------------------
2 files changed, 2 insertions(+), 40 deletions(-)
diff --git a/src/PVE/QemuMigrate.pm b/src/PVE/QemuMigrate.pm
index d4de9f87..331d263b 100644
--- a/src/PVE/QemuMigrate.pm
+++ b/src/PVE/QemuMigrate.pm
@@ -1394,7 +1394,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::Cluster::Helpers::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..b0d72ab4 100644
--- a/src/PVE/QemuServer/Helpers.pm
+++ b/src/PVE/QemuServer/Helpers.pm
@@ -8,6 +8,7 @@ use IO::File;
use JSON;
use PVE::Cluster;
+use PVE::Cluster::Helpers qw(version_cmp);
use PVE::INotify;
use PVE::ProcFSTools;
use PVE::Tools;
@@ -235,32 +236,6 @@ sub min_version {
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] 17+ messages in thread