From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: <pve-devel-bounces@lists.proxmox.com> Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id A8EA41FF15C for <inbox@lore.proxmox.com>; Fri, 4 Apr 2025 18:31:49 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 3CC5D37D26; Fri, 4 Apr 2025 18:29:53 +0200 (CEST) From: Gabriel Goller <g.goller@proxmox.com> To: pve-devel@lists.proxmox.com Date: Fri, 4 Apr 2025 18:28:57 +0200 Message-Id: <20250404162908.563060-47-g.goller@proxmox.com> X-Mailer: git-send-email 2.39.5 In-Reply-To: <20250404162908.563060-1-g.goller@proxmox.com> References: <20250404162908.563060-1-g.goller@proxmox.com> MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.022 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pve-devel] [PATCH pve-manager v2 02/11] fabric: add common interface panel X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion <pve-devel.lists.proxmox.com> List-Unsubscribe: <https://lists.proxmox.com/cgi-bin/mailman/options/pve-devel>, <mailto:pve-devel-request@lists.proxmox.com?subject=unsubscribe> List-Archive: <http://lists.proxmox.com/pipermail/pve-devel/> List-Post: <mailto:pve-devel@lists.proxmox.com> List-Help: <mailto:pve-devel-request@lists.proxmox.com?subject=help> List-Subscribe: <https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel>, <mailto:pve-devel-request@lists.proxmox.com?subject=subscribe> Reply-To: Proxmox VE development discussion <pve-devel@lists.proxmox.com> Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pve-devel-bounces@lists.proxmox.com Sender: "pve-devel" <pve-devel-bounces@lists.proxmox.com> Implements a shared interface selector panel for openfabric and ospf fabrics. This GridPanel combines data from two sources: the node network interfaces (/nodes/<node>/network) and the fabrics section configuration, displaying a merged view of both. It implements the following warning states: - When an interface has an IP address configured in /etc/network/interfaces, we display a warning and disable the input field, prompting users to configure addresses only via the fabrics interface - When addresses exist in both /etc/network/interfaces and /etc/network/interfaces.d/sdn, we show a warning without disabling the field, allowing users to remove the SDN interface configuration while preserving the underlying one Signed-off-by: Gabriel Goller <g.goller@proxmox.com> Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com> --- www/manager6/Makefile | 1 + www/manager6/sdn/fabrics/Common.js | 292 +++++++++++++++++++++++++++++ 2 files changed, 293 insertions(+) create mode 100644 www/manager6/sdn/fabrics/Common.js diff --git a/www/manager6/Makefile b/www/manager6/Makefile index c94a5cdfbf70..7df96f58eb1f 100644 --- a/www/manager6/Makefile +++ b/www/manager6/Makefile @@ -303,6 +303,7 @@ JSSRC= \ sdn/zones/SimpleEdit.js \ sdn/zones/VlanEdit.js \ sdn/zones/VxlanEdit.js \ + sdn/fabrics/Common.js \ storage/ContentView.js \ storage/BackupView.js \ storage/Base.js \ diff --git a/www/manager6/sdn/fabrics/Common.js b/www/manager6/sdn/fabrics/Common.js new file mode 100644 index 000000000000..434feb0582b1 --- /dev/null +++ b/www/manager6/sdn/fabrics/Common.js @@ -0,0 +1,292 @@ +Ext.define('PVE.sdn.Fabric.InterfacePanel', { + extend: 'Ext.grid.Panel', + xtype: 'pveSDNFabricsInterfacePanel', + mixins: ['Ext.form.field.Field'], + + networkInterfaces: undefined, + parentClass: undefined, + + selectionChange: function(_grid, _selection) { + let me = this; + me.value = me.getSelection().map((rec) => { + let submitValue = structuredClone(rec.data); + delete submitValue.selected; + delete submitValue.isDisabled; + delete submitValue.statusIcon; + delete submitValue.statusTooltip; + delete submitValue.type; + return PVE.Parser.printPropertyString(submitValue); + }); + me.checkChange(); + }, + + getValue: function() { + let me = this; + return me.value ?? []; + }, + + setValue: function(value) { + let me = this; + + value ??= []; + + me.updateSelectedInterfaces(value); + + return me.mixins.field.setValue.call(me, value); + }, + + addInterfaces: function(fabricInterfaces) { + let me = this; + if (me.networkInterfaces) { + let nodeInterfaces = me.networkInterfaces + .map((elem) => { + const obj = { + name: elem.iface, + type: elem.type, + ip: elem.cidr, + ipv6: elem.cidr6, + }; + return obj; + }); + + if (fabricInterfaces) { + // Map existing node interfaces with fabric data + nodeInterfaces = nodeInterfaces.map(i => { + let matchingInterface = fabricInterfaces.find(j => j.name === i.name); + if (matchingInterface) { + if ((matchingInterface.ip && i.ip) || (matchingInterface.ipv6 && i.ipv6)) { + i.statusIcon = 'warning fa-warning'; + i.statusTooltip = gettext('Interface already has an address configured in /etc/network/interfaces'); + } else if (i.ip || i.ipv6) { + i.statusIcon = 'warning fa-warning'; + i.statusTooltip = gettext('Configure the ip-address using the fabrics interface'); + i.isDisabled = true; + } + } + return Object.assign(i, matchingInterface); + }); + + // Add any fabric interface that doesn't exist in node_interfaces + for (const fabricInterface of fabricInterfaces) { + if (!nodeInterfaces.some(nodeInterface => nodeInterface.name === fabricInterface.name)) { + nodeInterfaces.push({ + name: fabricInterface.name, + statusIcon: 'warning fa-warning', + statusTooltip: gettext('Interface not found on node'), + ...fabricInterface, + }); + } + } + let store = me.getStore(); + store.setData(nodeInterfaces); + } else { + let store = me.getStore(); + store.setData(nodeInterfaces); + } + } else if (fabricInterfaces) { + // We could not get the available interfaces of the node, so we display the configured ones only. + const interfaces = fabricInterfaces.map((elem) => { + const obj = { + name: elem.name, + ...elem, + }; + return obj; + }); + + let store = me.getStore(); + store.setData(interfaces); + } else { + console.warn("no fabric_interfaces and cluster_interfaces available!"); + } + }, + + updateSelectedInterfaces: function(values) { + let me = this; + if (values) { + let recs = []; + let store = me.getStore(); + + for (const i of values) { + let rec = store.getById(i.name); + if (rec) { + recs.push(rec); + } + } + me.suspendEvent('change'); + me.setSelection(); + me.setSelection(recs); + } else { + me.suspendEvent('change'); + me.setSelection(); + } + me.resumeEvent('change'); + }, + + setNetworkInterfaces: function(networkInterfaces) { + this.networkInterfaces = networkInterfaces; + }, + + getSubmitData: function() { + let records = this.getSelection().map((record) => { + let submitData = structuredClone(record.data); + delete submitData.selected; + delete submitData.isDisabled; + delete submitData.statusIcon; + delete submitData.statusTooltip; + delete submitData.type; + + // Delete any properties that are null or undefined + Object.keys(submitData).forEach(function(key) { + if (submitData[key] === null || submitData[key] === undefined || submitData[key] === '') { + delete submitData[key]; + } + }); + + return Proxmox.Utils.printPropertyString(submitData); + }); + + // Only include interfaces in the submission if there are selected interfaces + if (records.length === 0) { + return {}; + } + + return { + 'interfaces': records, + }; + }, + + controller: { + onValueChange: function(field, value) { + let me = this; + let record = field.getWidgetRecord(); + let column = field.getWidgetColumn(); + if (record) { + record.set(column.dataIndex, value); + record.commit(); + + me.getView().checkChange(); + me.getView().selectionChange(); + } + }, + + control: { + 'field': { + change: 'onValueChange', + }, + }, + }, + + selModel: { + type: 'checkboxmodel', + mode: 'SIMPLE', + }, + + listeners: { + selectionchange: function() { + this.selectionChange(...arguments); + }, + }, + + commonColumns: [ + { + text: gettext('Status'), + dataIndex: 'status', + width: 30, + renderer: function(value, metaData, record) { + const icon = record.data.statusIcon || ''; + const tooltip = record.data.statusTooltip || ''; + + if (tooltip) { + metaData.tdAttr = `data-qtip="${Ext.htmlEncode(Ext.htmlEncode(tooltip))}"`; + } + + if (icon) { + return `<i class="fa ${icon}"></i>`; + } + + return value || ''; + }, + + }, + { + text: gettext('Name'), + dataIndex: 'name', + flex: 2, + }, + { + text: gettext('Type'), + dataIndex: 'type', + flex: 1, + }, + { + text: gettext('IP'), + xtype: 'widgetcolumn', + dataIndex: 'ip', + flex: 1, + + widget: { + xtype: 'proxmoxtextfield', + isFormField: false, + bind: { + disabled: '{record.isDisabled}', + }, + }, + }, + ], + + additionalColumns: [], + + initComponent: function() { + let me = this; + + Ext.apply(me, { + store: Ext.create("Ext.data.Store", { + model: "Pve.sdn.Interface", + sorters: { + property: 'name', + direction: 'ASC', + }, + }), + columns: me.commonColumns.concat(me.additionalColumns), + }); + + me.callParent(); + + Proxmox.Utils.monStoreErrors(me, me.getStore(), true); + me.initField(); + }, +}); + + +Ext.define('Pve.sdn.Fabric', { + extend: 'Ext.data.Model', + idProperty: 'name', + fields: [ + 'name', + 'type', + ], +}); + +Ext.define('Pve.sdn.Node', { + extend: 'Ext.data.Model', + idProperty: 'name', + fields: [ + 'name', + 'fabric', + 'type', + ], +}); + +Ext.define('Pve.sdn.Interface', { + extend: 'Ext.data.Model', + idProperty: 'name', + fields: [ + 'name', + 'ip', + 'ipv6', + 'passive', + 'hello_interval', + 'hello_multiplier', + 'csnp_interval', + ], +}); -- 2.39.5 _______________________________________________ pve-devel mailing list pve-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel