From: Hannes Laimer <h.laimer@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH pve-manager v3 39/45] ui: sdn: microseg: show and edit the group hierarchy
Date: Tue, 27 Oct 2026 09:55:21 +0100 [thread overview]
Message-ID: <20261027085527.702738-40-h.laimer@proxmox.com> (raw)
In-Reply-To: <20261027085527.702738-1-h.laimer@proxmox.com>
Selecting a group filters the rules to those naming it or a group
above it, so no rule its members inherit is left out.
Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
www/manager6/form/SDNMicrosegGroupSelector.js | 26 +++++++++-
www/manager6/sdn/MicrosegView.js | 8 ++-
www/manager6/sdn/microseg/AssignmentView.js | 5 +-
www/manager6/sdn/microseg/Base.js | 48 ++++++++++++++++--
www/manager6/sdn/microseg/GroupEdit.js | 10 ++++
www/manager6/sdn/microseg/RuleView.js | 26 ++++++++++
www/manager6/sdn/microseg/Tree.js | 49 +++++++++++++++----
7 files changed, 153 insertions(+), 19 deletions(-)
diff --git a/www/manager6/form/SDNMicrosegGroupSelector.js b/www/manager6/form/SDNMicrosegGroupSelector.js
index 1b0c39a1cfba..e7a4077e97df 100644
--- a/www/manager6/form/SDNMicrosegGroupSelector.js
+++ b/www/manager6/form/SDNMicrosegGroupSelector.js
@@ -34,12 +34,28 @@ Ext.define(
initComponent: function () {
let me = this;
- let store = new Ext.data.Store({
+ let store;
+ let isExcluded = (rec) => {
+ let data = store.getData();
+ let all = data.getSource() || data;
+ let seen = new Set();
+ while (rec && !seen.has(rec.getId())) {
+ if (rec.getId() === me.excludeId) {
+ return true;
+ }
+ seen.add(rec.getId());
+ rec = all.get(rec.get('parent'));
+ }
+ return false;
+ };
+
+ store = new Ext.data.Store({
model: 'pve-sdn-microseg-group',
sorters: {
property: 'id',
direction: 'ASC',
},
+ filters: me.excludeId ? [(rec) => !isExcluded(rec)] : [],
});
me.mon(store, 'load', () => me.syncUntagged());
@@ -55,6 +71,12 @@ Ext.define(
dataIndex: 'id',
flex: 1,
},
+ {
+ header: gettext('Parent'),
+ sortable: true,
+ dataIndex: 'parent',
+ flex: 1,
+ },
{
header: gettext('Comment'),
dataIndex: 'comment',
@@ -73,7 +95,7 @@ Ext.define(
function () {
Ext.define('pve-sdn-microseg-group', {
extend: 'Ext.data.Model',
- fields: ['id', 'comment'],
+ fields: ['id', 'parent', 'comment'],
proxy: {
type: 'proxmox',
url: '/api2/json/cluster/sdn/microseg/group',
diff --git a/www/manager6/sdn/MicrosegView.js b/www/manager6/sdn/MicrosegView.js
index 805b5b7ebdd9..028253920081 100644
--- a/www/manager6/sdn/MicrosegView.js
+++ b/www/manager6/sdn/MicrosegView.js
@@ -36,7 +36,13 @@ Ext.define('PVE.sdn.MicrosegView', {
},
addGroup: function () {
- this.lookup('tree').openEdit('group');
+ let me = this;
+ let parent = me.selectedGroup();
+ me.lookup('tree').openEdit(
+ 'group',
+ undefined,
+ parent ? { defaultParent: parent } : undefined,
+ );
},
editGroup: function () {
diff --git a/www/manager6/sdn/microseg/AssignmentView.js b/www/manager6/sdn/microseg/AssignmentView.js
index ea6c447bf710..373efe02eccd 100644
--- a/www/manager6/sdn/microseg/AssignmentView.js
+++ b/www/manager6/sdn/microseg/AssignmentView.js
@@ -68,9 +68,8 @@ Ext.define(
if (sel.vmid !== undefined) {
return !winsNic(rec, sel.vmid, sel.nic);
}
- let group = sel.mode === 'membership' ? sel.groups[0] : undefined;
- if (group && group !== 'untagged') {
- return !(rec.data.groups ?? []).includes(group);
+ if (sel.mode === 'membership' && !sel.untagged) {
+ return !(rec.data.groups ?? []).some((g) => sel.subtree.includes(g));
}
return false;
};
diff --git a/www/manager6/sdn/microseg/Base.js b/www/manager6/sdn/microseg/Base.js
index 6751da1c7e88..02c176b04f5c 100644
--- a/www/manager6/sdn/microseg/Base.js
+++ b/www/manager6/sdn/microseg/Base.js
@@ -71,6 +71,43 @@ Ext.define('PVE.sdn.microseg.Membership', {
});
},
+ hierarchy: function (groups) {
+ let byId = {};
+ for (const group of groups) {
+ byId[group.id] = group;
+ }
+ let childrenOf = {};
+ for (const group of groups) {
+ let parent = group.parent;
+ let key = parent && byId[parent] ? parent : '';
+ (childrenOf[key] = childrenOf[key] || []).push(group);
+ }
+ let ancestors = (group) => {
+ let out = [];
+ let seen = new Set();
+ let parent = group.parent;
+ while (parent && byId[parent] && !seen.has(parent)) {
+ seen.add(parent);
+ out.push(parent);
+ parent = byId[parent].parent;
+ }
+ return out;
+ };
+ let close = (list) => {
+ let out = new Set();
+ for (const g of list) {
+ out.add(g);
+ if (byId[g]) {
+ for (const a of ancestors(byId[g])) {
+ out.add(a);
+ }
+ }
+ }
+ return [...out].sort();
+ };
+ return { byId, childrenOf, ancestors, close };
+ },
+
rulesFor: function (groupSets) {
let classRules = {};
groupSets.forEach((cls) => {
@@ -88,12 +125,13 @@ Ext.define('PVE.sdn.microseg.Membership', {
};
},
- nicGroupSets: function (assignments, rulesFor) {
+ nicGroupSets: function (assignments, hierarchy, rulesFor) {
let nicGroupSet = {};
assignments.forEach((a) => {
let sortedGroups = (a.groups || []).slice().sort();
let groupSet = {
groups: sortedGroups,
+ closedGroups: hierarchy.close(sortedGroups),
rules: rulesFor([sortedGroups]),
note: PVE.Utils.format_sdnmicroseg_matcher(a),
};
@@ -106,12 +144,16 @@ Ext.define('PVE.sdn.microseg.Membership', {
return nicGroupSet;
},
- guestSelection: function (vmid, nicGroupSet, rulesFor) {
+ guestSelection: function (vmid, nicGroupSet, hierarchy, rulesFor) {
let keys = Object.keys(nicGroupSet).filter((key) => key.startsWith(`${vmid}/`));
- let rules = rulesFor(keys.map((key) => nicGroupSet[key].groups));
+ let sets = keys.map((key) => nicGroupSet[key].groups);
+ let ownGroups = sets.flat();
+ let rules = rulesFor(sets);
return {
scope: `vm${vmid}`,
mode: 'group-set',
+ groups: hierarchy.close(ownGroups),
+ ownGroups: ownGroups,
outbound: rules.outbound,
inbound: rules.inbound,
vmid: vmid,
diff --git a/www/manager6/sdn/microseg/GroupEdit.js b/www/manager6/sdn/microseg/GroupEdit.js
index 82639c344264..94f108ad46f8 100644
--- a/www/manager6/sdn/microseg/GroupEdit.js
+++ b/www/manager6/sdn/microseg/GroupEdit.js
@@ -15,6 +15,16 @@ Ext.define('PVE.sdn.microseg.GroupInputPanel', {
allowBlank: false,
maxLength: 32,
},
+ {
+ xtype: 'pveSDNMicrosegGroupSelector',
+ name: 'parent',
+ fieldLabel: gettext('Parent'),
+ allowBlank: true,
+ skipEmptyText: true,
+ deleteEmpty: !me.isCreate,
+ excludeId: me.microsegId,
+ value: me.defaultParent,
+ },
{
xtype: 'proxmoxtextfield',
name: 'comment',
diff --git a/www/manager6/sdn/microseg/RuleView.js b/www/manager6/sdn/microseg/RuleView.js
index 016e1dc4455e..58d81221402c 100644
--- a/www/manager6/sdn/microseg/RuleView.js
+++ b/www/manager6/sdn/microseg/RuleView.js
@@ -35,8 +35,15 @@ Ext.define(
out = sel.outbound.has(data.id);
inbound = sel.inbound.has(data.id);
}
+ let matched = sel
+ ? [...new Set([...src, ...dst])].filter((g) => sel.groups.has(g))
+ : [];
return {
rank: me.rankOf(data),
+ specificity: data.specificity ?? 0,
+ matched: matched
+ .sort()
+ .map((g) => (sel.ownGroups.has(g) ? g : `${g} (${gettext('inherited')})`)),
direction: out && inbound ? 'both' : out ? 'out' : inbound ? 'in' : '',
};
},
@@ -65,6 +72,7 @@ Ext.define(
mode: mode,
untagged: sel.untagged,
groups: new Set(sel.groups),
+ ownGroups: new Set(sel.ownGroups),
outbound: sel.outbound,
inbound: sel.inbound,
}
@@ -79,6 +87,8 @@ Ext.define(
}
me.down('#microsegScopeText').setText(text);
+ me.down('#microsegMatched').setVisible(!!mode);
+
me.loadRows();
},
@@ -221,6 +231,20 @@ Ext.define(
flex: 1,
renderer: (v, m, rec) => Ext.String.htmlEncode(matchText(rec)),
},
+ {
+ text: gettext('Specificity'),
+ dataIndex: 'specificity',
+ width: 100,
+ hidden: true,
+ },
+ {
+ text: gettext('Matched By'),
+ itemId: 'microsegMatched',
+ dataIndex: 'matched',
+ flex: 1,
+ hidden: true,
+ renderer: (v) => Ext.String.htmlEncode((v || []).join(', ')),
+ },
{ xtype: 'pveSDNMicrosegStateColumn' },
{
text: gettext('Comment'),
@@ -274,6 +298,8 @@ Ext.define(
'macro',
'direction',
'rank',
+ 'specificity',
+ 'matched',
],
});
},
diff --git a/www/manager6/sdn/microseg/Tree.js b/www/manager6/sdn/microseg/Tree.js
index e261e4944e81..1a3c0905d0aa 100644
--- a/www/manager6/sdn/microseg/Tree.js
+++ b/www/manager6/sdn/microseg/Tree.js
@@ -27,8 +27,11 @@ Ext.define(
groups.sort((a, b) => a.id.localeCompare(b.id));
let membership = PVE.sdn.microseg.Membership;
+ let hierarchy = membership.hierarchy(groups);
+ let childrenOf = hierarchy.childrenOf;
+ let ancestorsOf = hierarchy.ancestors;
let rulesFor = membership.rulesFor(groupSets);
- let nicGroupSet = membership.nicGroupSets(assignments, rulesFor);
+ let nicGroupSet = membership.nicGroupSets(assignments, hierarchy, rulesFor);
let groupNics = {};
for (const [key, groupSet] of Object.entries(nicGroupSet)) {
@@ -43,7 +46,12 @@ Ext.define(
if (!guests[vmid]) {
guests[vmid] = {
...guestDisplay(vmid),
- selection: membership.guestSelection(vmid, nicGroupSet, rulesFor),
+ selection: membership.guestSelection(
+ vmid,
+ nicGroupSet,
+ hierarchy,
+ rulesFor,
+ ),
};
}
return guests[vmid];
@@ -65,16 +73,20 @@ Ext.define(
mode: 'membership',
untagged: true,
groups: ['untagged'],
+ ownGroups: ['untagged'],
+ subtree: ['untagged'],
},
leaf: true,
},
];
- for (const group of groups) {
+ let groupNode = (group) => {
let groupId = `g/${group.id}`;
let byVmid = {};
+ let nicKeys = new Set();
for (const { vmid, nic } of groupNics[group.id] || []) {
(byVmid[vmid] = byVmid[vmid] || []).push(nic);
+ nicKeys.add(`${vmid}/${nic}`);
}
let guestNodes = Object.keys(byVmid)
.map(Number)
@@ -98,6 +110,8 @@ Ext.define(
microsegSelection: {
scope: `vm${vmid}/net${nic} {${groupSet.groups.join(', ')}}`,
mode: 'group-set',
+ groups: groupSet.closedGroups,
+ ownGroups: groupSet.groups,
outbound: groupSet.rules.outbound,
inbound: groupSet.rules.inbound,
vmid: vmid,
@@ -109,22 +123,37 @@ Ext.define(
};
});
- let count = (groupNics[group.id] || []).length;
- nodes.push({
+ let subgroupNodes = [];
+ let subtree = [group.id];
+ for (const sub of childrenOf[group.id] || []) {
+ let subgroup = groupNode(sub);
+ subgroupNodes.push(subgroup.node);
+ subgroup.nicKeys.forEach((key) => nicKeys.add(key));
+ subtree.push(...subgroup.subtree);
+ }
+ let children = [...subgroupNodes, ...guestNodes];
+ let node = {
id: groupId,
text: group.id,
- microsegNote: count ? `(${count})` : undefined,
+ microsegNote: nicKeys.size ? `(${nicKeys.size})` : undefined,
iconCls: 'fa fa-fw fa-object-group',
microsegId: group.id,
microsegSelection: {
scope: group.id,
mode: 'membership',
- groups: [group.id],
+ groups: [group.id, ...ancestorsOf(group)],
+ ownGroups: [group.id],
+ subtree: subtree,
},
state: group.state,
- leaf: !guestNodes.length,
- children: guestNodes,
- });
+ leaf: !children.length,
+ children: children,
+ };
+ return { node: node, nicKeys: nicKeys, subtree: subtree };
+ };
+
+ for (const group of childrenOf[''] || []) {
+ nodes.push(groupNode(group).node);
}
return nodes;
},
--
2.47.3
next prev parent reply other threads:[~2026-10-27 9:03 UTC|newest]
Thread overview: 46+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-27 8:54 [PATCH access-control/cluster/container/docs/manager/network/proxmox{-ebpf,-ve-rs,-perl-rs}/qemu-server v3 00/45] sdn: add microsegmentation support Hannes Laimer
2026-10-27 8:54 ` [PATCH proxmox-ebpf v3 01/45] build: keep the build directory out of the BPF objects Hannes Laimer
2026-10-27 8:54 ` [PATCH proxmox-ebpf v3 02/45] policy: add the enforcement subsystem for guest interfaces Hannes Laimer
2026-10-27 8:54 ` [PATCH proxmox-ebpf v3 03/45] policy: add a check for non-first fragments Hannes Laimer
2026-10-27 8:54 ` [PATCH proxmox-ve-rs v3 04/45] ve-config: firewall: add a constructor for sctp matches Hannes Laimer
2026-10-27 8:54 ` [PATCH proxmox-ve-rs v3 05/45] ve-config: sdn: microseg: add identity allocation for group sets Hannes Laimer
2026-10-27 8:54 ` [PATCH proxmox-ve-rs v3 06/45] ve-config: sdn: microseg: add config types Hannes Laimer
2026-10-27 8:54 ` [PATCH proxmox-ve-rs v3 07/45] ve-config: sdn: microseg: add tag matcher Hannes Laimer
2026-10-27 8:54 ` [PATCH proxmox-ve-rs v3 08/45] ve-config: sdn: microseg: add name regex matcher Hannes Laimer
2026-10-27 8:54 ` [PATCH proxmox-ve-rs v3 09/45] ve-config: sdn: microseg: narrow rules by protocol and ports Hannes Laimer
2026-10-27 8:54 ` [PATCH proxmox-ve-rs v3 10/45] ve-config: sdn: microseg: add group hierarchy Hannes Laimer
2026-10-27 8:54 ` [PATCH proxmox-ve-rs v3 11/45] ve-config: firewall: expose protocol, icmp type and code numbers Hannes Laimer
2026-10-27 8:54 ` [PATCH proxmox-ve-rs v3 12/45] ve-config: sdn: microseg: add the desired state Hannes Laimer
2026-10-27 8:54 ` [PATCH proxmox-ve-rs v3 13/45] microseg: add a crate that applies the policy to a node's guest NICs Hannes Laimer
2026-10-27 8:54 ` [PATCH proxmox-ve-rs v3 14/45] microseg: add a rule option that matches non-first fragments Hannes Laimer
2026-10-27 8:54 ` [PATCH pve-cluster v3 15/45] cfs: add 'sdn/microseg.cfg' to observed files Hannes Laimer
2026-10-27 8:54 ` [PATCH pve-access-control v3 16/45] permissions: add ACL path for SDN microsegmentation Hannes Laimer
2026-10-27 8:54 ` [PATCH proxmox-perl-rs v3 17/45] pve-rs: sdn: add microseg config binding Hannes Laimer
2026-10-27 8:55 ` [PATCH proxmox-perl-rs v3 18/45] pve-rs: sdn: microseg: bind the data plane Hannes Laimer
2026-10-27 8:55 ` [PATCH proxmox-perl-rs v3 19/45] pve-rs: sdn: microseg: check the data plane limits on render Hannes Laimer
2026-10-27 8:55 ` [PATCH proxmox-perl-rs v3 20/45] pve-rs: debian: fire the pve-microseg-update trigger on upgrade Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-network v3 21/45] sdn: zones: add vxlan-gbp option to vxlan and evpn zones Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-network v3 22/45] sdn: microseg: add config, API and guest inventory Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-network v3 23/45] sdn: dry-run: show pending microseg changes Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-network v3 24/45] sdn: microseg: enforce the policy on guest NICs Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-network v3 25/45] sdn: microseg: add tag matcher Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-network v3 26/45] sdn: microseg: add name regex matcher Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-network v3 27/45] sdn: microseg: add an update for a running NIC Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-network v3 28/45] sdn: microseg: expose protocol, port and macro rule options Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-network v3 29/45] sdn: microseg: allow nesting groups under a parent Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-network v3 30/45] sdn: microseg: expose the fragments rule option Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-manager v3 31/45] network: apply the microseg policy on reload Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-manager v3 32/45] pve-sdn-commit: apply the microseg policy at boot Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-manager v3 33/45] ui: sdn: zones: add vxlan-gbp checkbox to vxlan and evpn Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-manager v3 34/45] ui: sdn: add microsegmentation panel Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-manager v3 35/45] ui: sdn: dry-run: show pending microseg diff Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-manager v3 36/45] ui: sdn: microseg: add tag matcher Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-manager v3 37/45] ui: sdn: microseg: add name regex matcher Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-manager v3 38/45] ui: sdn: microseg: add narrowing to the rule form Hannes Laimer
2026-10-27 8:55 ` Hannes Laimer [this message]
2026-10-27 8:55 ` [PATCH pve-manager v3 40/45] ui: sdn: microseg: add fragments option " Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-manager v3 41/45] ui: sdn: microseg: add a guest tab with its groups and rules Hannes Laimer
2026-10-27 8:55 ` [PATCH qemu-server v3 42/45] network: update the microseg enforcement on a tag or name change Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-container v3 43/45] lxc: update the microseg enforcement on a tag or hostname change Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-docs v3 44/45] sdn: zones: document the VXLAN-GBP option Hannes Laimer
2026-10-27 8:55 ` [PATCH pve-docs v3 45/45] sdn: add microsegmentation section Hannes Laimer
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261027085527.702738-40-h.laimer@proxmox.com \
--to=h.laimer@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox