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 B1E291FF164 for <inbox@lore.proxmox.com>; Fri, 28 Mar 2025 18:23:04 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 47832A08F; Fri, 28 Mar 2025 18:22:25 +0100 (CET) From: Gabriel Goller <g.goller@proxmox.com> To: pve-devel@lists.proxmox.com Date: Fri, 28 Mar 2025 18:13:34 +0100 Message-Id: <20250328171340.885413-47-g.goller@proxmox.com> X-Mailer: git-send-email 2.39.5 In-Reply-To: <20250328171340.885413-1-g.goller@proxmox.com> References: <20250328171340.885413-1-g.goller@proxmox.com> MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.024 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 2/7] fabrics: 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 | 285 +++++++++++++++++++++++++++++ 2 files changed, 286 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..d71127d9c57f --- /dev/null +++ b/www/manager6/sdn/fabrics/Common.js @@ -0,0 +1,285 @@ +Ext.define('PVE.sdn.Fabric.InterfacePanel', { + extend: 'Ext.grid.Panel', + mixins: ['Ext.form.field.Field'], + + network_interfaces: 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.network_interfaces) { + let nodeInterfaces = me.network_interfaces + .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 elem = fabricInterfaces.find(j => j.name === i.name); + if (elem) { + if ((elem.ip && i.ip) || (elem.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, elem); + }); + + // Add any fabric interface that doesn't exist in node_interfaces + for (const fabricIface of fabricInterfaces) { + if (!nodeInterfaces.some(nodeIface => nodeIface.name === fabricIface.name)) { + nodeInterfaces.push({ + name: fabricIface.name, + statusIcon: 'warning fa-warning', + statusTooltip: gettext('Interface not found on node'), + ...fabricIface, + }); + } + } + 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. + let 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(network_interfaces) { + this.network_interfaces = network_interfaces; + }, + + 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); + }); + 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) { + let icon = record.data.statusIcon || ''; + let tooltip = record.data.statusTooltip || ''; + + if (tooltip) { + metaData.tdAttr = 'data-qtip="' + 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