From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id 9303F1FF0E3 for ; Tue, 27 Oct 2026 10:02:55 +0100 (CET) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id B944421B9B; Tue, 27 Oct 2026 09:57:17 +0100 (CET) From: Hannes Laimer To: pve-devel@lists.proxmox.com Subject: [PATCH pve-manager v3 41/45] ui: sdn: microseg: add a guest tab with its groups and rules Date: Tue, 27 Oct 2026 09:55:23 +0100 Message-ID: <20261027085527.702738-42-h.laimer@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20261027085527.702738-1-h.laimer@proxmox.com> References: <20261027085527.702738-1-h.laimer@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1793091378208 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.304 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) PROLO_LEO2 0.1 Meta Catches all Leo drug variations so far RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: 7HSMWWTAM3ZFXU5IDKBSP6YVDMX4T2RE X-Message-ID-Hash: 7HSMWWTAM3ZFXU5IDKBSP6YVDMX4T2RE X-MailFrom: h.laimer@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: The tab lists each assigned NIC with its assignment and groups, and below that the rules that apply to the guest. They are grouped by direction instead of by priority as in the SDN panel, since for a single guest the direction matters more. Editing stays in the SDN panel. Signed-off-by: Hannes Laimer --- www/manager6/Makefile | 1 + www/manager6/lxc/Config.js | 10 ++ www/manager6/qemu/Config.js | 10 ++ www/manager6/sdn/microseg/Base.js | 41 +++++---- www/manager6/sdn/microseg/GuestPanel.js | 117 ++++++++++++++++++++++++ www/manager6/sdn/microseg/RuleView.js | 40 ++++++-- 6 files changed, 195 insertions(+), 24 deletions(-) create mode 100644 www/manager6/sdn/microseg/GuestPanel.js diff --git a/www/manager6/Makefile b/www/manager6/Makefile index 7c48b0622aa7..1f429fb20bdf 100644 --- a/www/manager6/Makefile +++ b/www/manager6/Makefile @@ -366,6 +366,7 @@ JSSRC= \ sdn/microseg/AssignmentEdit.js \ sdn/microseg/RuleView.js \ sdn/microseg/AssignmentView.js \ + sdn/microseg/GuestPanel.js \ storage/ContentView.js \ storage/BackupView.js \ storage/Base.js \ diff --git a/www/manager6/lxc/Config.js b/www/manager6/lxc/Config.js index 3a99d6a71dec..d14bfa244958 100644 --- a/www/manager6/lxc/Config.js +++ b/www/manager6/lxc/Config.js @@ -387,6 +387,16 @@ Ext.define('PVE.lxc.Config', { }); } + if (caps.vms['VM.Audit'] && caps.sdn['SDN.Audit']) { + me.items.push({ + xtype: 'pveSDNMicrosegGuest', + title: gettext('Microsegmentation'), + iconCls: 'fa fa-tags', + itemId: 'microseg', + vmid: vmid, + }); + } + if (caps.vms['Permissions.Modify']) { me.items.push({ xtype: 'pveACLView', diff --git a/www/manager6/qemu/Config.js b/www/manager6/qemu/Config.js index 842d35de1202..7d7b17bfa870 100644 --- a/www/manager6/qemu/Config.js +++ b/www/manager6/qemu/Config.js @@ -440,6 +440,16 @@ Ext.define('PVE.qemu.Config', { }); } + if (caps.vms['VM.Audit'] && caps.sdn['SDN.Audit']) { + me.items.push({ + xtype: 'pveSDNMicrosegGuest', + title: gettext('Microsegmentation'), + iconCls: 'fa fa-tags', + itemId: 'microseg', + vmid: vmid, + }); + } + if (caps.vms['Permissions.Modify']) { me.items.push({ xtype: 'pveACLView', diff --git a/www/manager6/sdn/microseg/Base.js b/www/manager6/sdn/microseg/Base.js index 02c176b04f5c..327a49b49aad 100644 --- a/www/manager6/sdn/microseg/Base.js +++ b/www/manager6/sdn/microseg/Base.js @@ -134,6 +134,7 @@ Ext.define('PVE.sdn.microseg.Membership', { closedGroups: hierarchy.close(sortedGroups), rules: rulesFor([sortedGroups]), note: PVE.Utils.format_sdnmicroseg_matcher(a), + assignment: a, }; (a.members || []).forEach((m) => { m.nics.forEach((nic) => { @@ -223,33 +224,37 @@ Ext.define('PVE.sdn.microseg.BaseGrid', { me.openEdit(rec.data.type, rec.data.id, me.editPanelConfig?.(rec)); }; + let editButtons = me.readOnly + ? [] + : [ + me.addButton, + new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + enableFn: editable, + handler: runEditor, + }), + Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + enableFn: editable, + baseurl: me.baseurl, + getRecordName: me.getRecordName, + callback: () => me.reload(), + }), + ]; + Ext.apply(me, { selModel: sm, tbar: [ - me.addButton, - new Proxmox.button.Button({ - text: gettext('Edit'), - disabled: true, - selModel: sm, - enableFn: editable, - handler: runEditor, - }), - Ext.create('Proxmox.button.StdRemoveButton', { - selModel: sm, - enableFn: editable, - baseurl: me.baseurl, - getRecordName: me.getRecordName, - callback: () => me.reload(), - }), + ...editButtons, { text: gettext('Reload'), handler: () => me.reload(), }, ...(me.tbar ?? []), ], - listeners: { - itemdblclick: runEditor, - }, + listeners: me.readOnly ? {} : { itemdblclick: runEditor }, }); me.callParent(); diff --git a/www/manager6/sdn/microseg/GuestPanel.js b/www/manager6/sdn/microseg/GuestPanel.js new file mode 100644 index 000000000000..657fa4d1fbc7 --- /dev/null +++ b/www/manager6/sdn/microseg/GuestPanel.js @@ -0,0 +1,117 @@ +Ext.define('PVE.sdn.microseg.GuestPanel', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveSDNMicrosegGuest', + + onlineHelp: 'pvesdn_config_microseg', + + layout: 'border', + + controller: { + xclass: 'Ext.app.ViewController', + + reload: function () { + let me = this; + let view = me.getView(); + let membership = PVE.sdn.microseg.Membership; + membership.load(view, (parts) => { + let hierarchy = membership.hierarchy(parts.groups); + let rulesFor = membership.rulesFor(parts.groupSets); + let nicGroupSet = membership.nicGroupSets(parts.assignments, hierarchy, rulesFor); + let prefix = `${view.vmid}/`; + let nics = Object.entries(nicGroupSet) + .filter(([key]) => key.startsWith(prefix)) + .map(([key, groupSet]) => ({ + nic: Number(key.slice(prefix.length)), + groups: groupSet.groups, + matcher: groupSet.note, + comment: groupSet.assignment.comment, + state: groupSet.assignment.state, + })) + .sort((a, b) => a.nic - b.nic); + me.lookup('nics').getStore().loadData(nics); + + let rulesView = me.lookup('rules'); + rulesView.setData(parts.rules); + rulesView.setTreeSelection( + membership.guestSelection(view.vmid, nicGroupSet, hierarchy, rulesFor), + ); + }); + }, + }, + + initComponent: function () { + let me = this; + + if (!me.vmid) { + throw 'no vmid given'; + } + + me.items = [ + { + xtype: 'grid', + reference: 'nics', + region: 'north', + height: 150, + split: true, + border: false, + store: { + fields: ['nic', 'groups', 'matcher', 'comment', 'state'], + sorters: [{ property: 'nic', direction: 'ASC' }], + }, + viewConfig: { + getRowClass: PVE.Utils.sdnmicroseg_row_cls, + }, + emptyText: gettext('No assignment covers this guest'), + tbar: [ + { + text: gettext('Reload'), + handler: 'reload', + }, + ], + columns: [ + { + text: gettext('Interface'), + dataIndex: 'nic', + width: 100, + renderer: (v) => `net${v}`, + }, + { + text: gettext('Groups'), + dataIndex: 'groups', + flex: 2, + renderer: (v) => (v || []).map(PVE.Utils.render_sdnmicroseg_group).join(''), + }, + { + text: gettext('Assignment'), + dataIndex: 'matcher', + flex: 1, + renderer: Ext.String.htmlEncode, + }, + { xtype: 'pveSDNMicrosegStateColumn' }, + { + text: gettext('Comment'), + dataIndex: 'comment', + flex: 1, + renderer: Ext.String.htmlEncode, + }, + ], + }, + { + xtype: 'pveSDNMicrosegRules', + reference: 'rules', + region: 'center', + readOnly: true, + groupByDirection: true, + listeners: { + microsegreload: 'reload', + }, + }, + ]; + + me.callParent(); + }, + + listeners: { + activate: 'reload', + }, +}); diff --git a/www/manager6/sdn/microseg/RuleView.js b/www/manager6/sdn/microseg/RuleView.js index 95c08343e725..8c982a7bc5aa 100644 --- a/www/manager6/sdn/microseg/RuleView.js +++ b/www/manager6/sdn/microseg/RuleView.js @@ -38,13 +38,15 @@ Ext.define( let matched = sel ? [...new Set([...src, ...dst])].filter((g) => sel.groups.has(g)) : []; + let direction = out && inbound ? 'both' : out ? 'out' : inbound ? 'in' : ''; 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' : '', + direction: direction, + directionRank: { in: 0, out: 1, both: 2 }[direction] ?? 3, }; }, @@ -95,13 +97,32 @@ Ext.define( initComponent: function () { let me = this; + if (me.groupByDirection) { + let directionNames = { + in: gettext('Inbound'), + out: gettext('Outbound'), + both: gettext('Both'), + }; + me.features = [ + { + ftype: 'grouping', + groupHeaderTpl: [ + '{groupValue:this.directionName}', + { + directionName: (name) => + Ext.String.htmlEncode(directionNames[name] ?? name), + }, + ], + enableGroupingMenu: false, + }, + ]; + } + let store = Ext.create('Ext.data.Store', { model: 'pve-sdn-microseg-rule', - grouper: { - property: 'prio', - sortProperty: 'prio', - direction: 'DESC', - }, + grouper: me.groupByDirection + ? { property: 'direction', sortProperty: 'directionRank', direction: 'ASC' } + : { property: 'prio', sortProperty: 'prio', direction: 'DESC' }, sorters: [ { property: 'rank', direction: 'ASC' }, { property: 'id', direction: 'ASC' }, @@ -234,6 +255,12 @@ Ext.define( flex: 1, renderer: (v, m, rec) => Ext.String.htmlEncode(matchText(rec)), }, + { + text: gettext('Priority'), + dataIndex: 'prio', + width: 80, + hidden: !me.groupByDirection, + }, { text: gettext('Specificity'), dataIndex: 'specificity', @@ -301,6 +328,7 @@ Ext.define( 'macro', 'fragments', 'direction', + 'directionRank', 'rank', 'specificity', 'matched', -- 2.47.3