public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
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





  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
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal