From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 866B41FF183 for ; Wed, 2 Jul 2025 16:57:25 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 0CE8E1FFC0; Wed, 2 Jul 2025 16:52:16 +0200 (CEST) From: Gabriel Goller To: pve-devel@lists.proxmox.com Date: Wed, 2 Jul 2025 16:50:54 +0200 Message-Id: <20250702145101.894299-70-g.goller@proxmox.com> X-Mailer: git-send-email 2.39.5 In-Reply-To: <20250702145101.894299-1-g.goller@proxmox.com> References: <20250702145101.894299-1-g.goller@proxmox.com> MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.014 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 v4 12/17] fabrics: Add main FabricView X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Proxmox VE development discussion Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pve-devel-bounces@lists.proxmox.com Sender: "pve-devel" TreeView that shows all the fabrics and nodes in a hierarchical structure. It also shows all the pending changes from the running-config. From here all entities in the fabrics can be added / edited and deleted, utilizing the previously created EditWindow components for Fabrics / Nodes. We decided against including all the interfaces (as children of nodes in the tree view) because otherwise the indentation would be too much and detailed information on the interfaces is rarely needed, so we only show the names of the configured interfaces instead. Co-authored-by: Stefan Hanreich Signed-off-by: Gabriel Goller --- www/manager6/Makefile | 1 + www/manager6/dc/Config.js | 17 +- www/manager6/sdn/FabricsView.js | 464 ++++++++++++++++++++++++++++++++ 3 files changed, 473 insertions(+), 9 deletions(-) create mode 100644 www/manager6/sdn/FabricsView.js diff --git a/www/manager6/Makefile b/www/manager6/Makefile index 6f19852af017..e5537606cb63 100644 --- a/www/manager6/Makefile +++ b/www/manager6/Makefile @@ -307,6 +307,7 @@ JSSRC= \ sdn/zones/SimpleEdit.js \ sdn/zones/VlanEdit.js \ sdn/zones/VxlanEdit.js \ + sdn/FabricsView.js \ sdn/fabrics/Common.js \ sdn/fabrics/InterfacePanel.js \ sdn/fabrics/NodeEdit.js \ diff --git a/www/manager6/dc/Config.js b/www/manager6/dc/Config.js index 6173a9b2b7b3..198124c4acdb 100644 --- a/www/manager6/dc/Config.js +++ b/www/manager6/dc/Config.js @@ -236,18 +236,17 @@ Ext.define('PVE.dc.Config', { iconCls: 'fa fa-shield', itemId: 'sdnfirewall', }, + { + xtype: 'pveSDNFabricView', + groups: ['sdn'], + title: gettext('Fabrics'), + hidden: true, + iconCls: 'fa fa-road', + itemId: 'sdnfabrics', + }, ); } - if (Proxmox.UserName === 'root@pam') { - me.items.push({ - xtype: 'pveACMEClusterView', - title: 'ACME', - iconCls: 'fa fa-certificate', - itemId: 'acme', - }); - } - me.items.push( { xtype: 'pveFirewallRules', diff --git a/www/manager6/sdn/FabricsView.js b/www/manager6/sdn/FabricsView.js new file mode 100644 index 000000000000..2a1af3ab73ce --- /dev/null +++ b/www/manager6/sdn/FabricsView.js @@ -0,0 +1,464 @@ +Ext.define('PVE.sdn.Fabric.TreeModel', { + extend: 'Ext.data.TreeModel', + idProperty: 'tree_id', +}); + +Ext.define('PVE.sdn.Fabric.View', { + extend: 'Ext.tree.Panel', + + xtype: 'pveSDNFabricView', + + onlineHelp: 'pvesdn_config_fabrics', + + columns: [ + { + xtype: 'treecolumn', + text: gettext('Name'), + dataIndex: 'node_id', + width: 200, + renderer: function(value, metaData, rec) { + if (rec.data.type === 'fabric') { + return PVE.Utils.render_sdn_pending(rec, rec.data.id, 'id'); + } + + return PVE.Utils.render_sdn_pending(rec, value, 'node_id'); + }, + }, + { + text: gettext('Protocol'), + dataIndex: 'protocol', + width: 100, + renderer: function(value, metaData, rec) { + if (rec.data.type === 'fabric') { + const PROTOCOL_DISPLAY_NAMES = { + 'openfabric': 'OpenFabric', + 'ospf': 'OSPF', + }; + const displayValue = PROTOCOL_DISPLAY_NAMES[value]; + if (rec.data.state === undefined || rec.data.state === null) { + return Ext.htmlEncode(displayValue); + } + if (rec.data.state === 'deleted') { + if (value === undefined) { + return ' '; + } else { + let encoded = Ext.htmlEncode(displayValue); + return `${encoded}`; + } + } + return Ext.htmlEncode(displayValue); + } + + return ""; + }, + }, + { + text: gettext('IPv4'), + dataIndex: 'ip', + width: 150, + renderer: function(value, metaData, rec) { + if (rec.data.type === 'fabric') { + return PVE.Utils.render_sdn_pending(rec, rec.data.ip_prefix, 'ip_prefix'); + } + + return PVE.Utils.render_sdn_pending(rec, value, 'ip'); + }, + }, + { + text: gettext('IPv6'), + dataIndex: 'ip6', + width: 150, + renderer: function(value, metaData, rec) { + if (rec.data.type === 'fabric') { + return PVE.Utils.render_sdn_pending(rec, rec.data.ip6_prefix, 'ip6_prefix'); + } + + return PVE.Utils.render_sdn_pending(rec, value, 'ip6'); + }, + }, + { + header: gettext('Interfaces'), + width: 200, + dataIndex: 'interface', + renderer: function(value, metaData, rec) { + const interfaces = rec.data.pending?.interfaces || rec.data.interfaces || []; + + let names = interfaces.map((iface) => { + const properties = Proxmox.Utils.parsePropertyString(iface); + return properties.name; + }); + + names.sort(); + const displayValue = Ext.htmlEncode(names.join(", ")); + if (rec.data.state === 'deleted') { + return `${displayValue}`; + } + return displayValue; + }, + }, + { + text: gettext('Action'), + xtype: 'actioncolumn', + dataIndex: 'text', + width: 100, + items: [ + { + handler: 'addActionTreeColumn', + getTip: (_v, _m, _rec) => gettext('Add Node'), + getClass: (_v, _m, { data }) => { + if (data.type === 'fabric') { + return 'fa fa-plus-circle'; + } + + return 'pmx-hidden'; + }, + isActionDisabled: (_v, _r, _c, _i, { data }) => data.type !== 'fabric', + }, + { + tooltip: gettext('Edit'), + handler: 'editAction', + getClass: (_v, _m, { data }) => { + // the fabric type (openfabric, ospf, etc.) cannot be edited + if (data.type && data.state !== 'deleted') { + return 'fa fa-pencil fa-fw'; + } + + return 'pmx-hidden'; + }, + isActionDisabled: (_v, _r, _c, _i, { data }) => !data.type, + }, + { + tooltip: gettext('Delete'), + handler: 'deleteAction', + getClass: (_v, _m, { data }) => { + // the fabric type (openfabric, ospf, etc.) cannot be deleted + if (data.type && data.state !== 'deleted') { + return 'fa critical fa-trash-o'; + } + + return 'pmx-hidden'; + }, + isActionDisabled: (_v, _r, _c, _i, { data }) => !data.type, + }, + ], + }, + { + header: gettext('State'), + width: 100, + dataIndex: 'state', + renderer: function(value, metaData, rec) { + return PVE.Utils.render_sdn_pending_state(rec, value); + }, + }, + ], + + store: { + sorters: ['tree_id'], + model: 'PVE.sdn.Fabric.TreeModel', + }, + + layout: 'fit', + rootVisible: false, + animate: false, + + initComponent: function() { + let me = this; + + let addNodeButton = new Proxmox.button.Button({ + text: gettext('Add Node'), + handler: 'addActionTbar', + disabled: true, + }); + + let setAddNodeButtonStatus = function() { + let selection = me.view.getSelection(); + + if (selection.length === 0) { + return; + } + + let enabled = selection[0].data.type === 'fabric'; + addNodeButton.setDisabled(!enabled); + }; + + Ext.apply(me, { + tbar: [ + { + text: gettext('Add Fabric'), + menu: [ + { + text: 'OpenFabric', + handler: 'addOpenfabric', + }, + { + text: 'OSPF', + handler: 'addOspf', + }, + ], + }, + addNodeButton, + { + xtype: 'proxmoxButton', + text: gettext('Reload'), + handler: function() { + const view = this.up('pveSDNFabricView'); + view.getController().reload(); + }, + }, + ], + listeners: { + selectionchange: setAddNodeButtonStatus, + }, + }); + + me.callParent(); + }, + + controller: { + xclass: 'Ext.app.ViewController', + + reload: function(successCallback) { + let me = this; + + Proxmox.Utils.API2Request({ + url: `/cluster/sdn/fabrics/all?pending=1`, + method: 'GET', + success: function(response, opts) { + let fabrics = {}; + + for (const fabric of response.result.data.fabrics) { + let mergedFabric = { + expanded: true, + type: 'fabric', + iconCls: 'fa fa-road x-fa-treepanel', + children: [], + ...fabric, + ...fabric.pending, + }; + + mergedFabric.tree_id = mergedFabric.id; + + fabrics[mergedFabric.id] = mergedFabric; + } + + for (const node of response.result.data.nodes) { + let mergedNode = { + type: 'node', + iconCls: 'fa fa-desktop x-fa-treepanel', + leaf: true, + ...node, + ...node.pending, + }; + + mergedNode.tree_id = `${mergedNode.fabric_id}_${mergedNode.node_id}`; + + fabrics[mergedNode.fabric_id].children.push(mergedNode); + } + + me.getView().setRootNode({ + name: '__root', + expanded: true, + children: Object.values(fabrics), + }); + + if (successCallback) { + successCallback(); + } + }, + }); + }, + + getFabricEditPanel: function(protocol) { + const FABRIC_PANELS = { + openfabric: 'PVE.sdn.Fabric.OpenFabric.Fabric.Edit', + ospf: 'PVE.sdn.Fabric.Ospf.Fabric.Edit', + }; + + return FABRIC_PANELS[protocol]; + }, + + getNodeEditPanel: function(protocol) { + const NODE_PANELS = { + openfabric: 'PVE.sdn.Fabric.OpenFabric.Node.Edit', + ospf: 'PVE.sdn.Fabric.Ospf.Node.Edit', + }; + + return NODE_PANELS[protocol]; + }, + + addOpenfabric: function() { + let me = this; + me.openFabricAddWindow('openfabric'); + }, + + addOspf: function() { + let me = this; + me.openFabricAddWindow('ospf'); + }, + + openFabricAddWindow: function(protocol) { + let me = this; + + let component = me.getFabricEditPanel(protocol); + + let window = Ext.create(component, { + autoShow: true, + autoLoad: false, + isCreate: true, + }); + + window.on('destroy', () => me.reload()); + }, + + addActionTreeColumn: function(_grid, _rI, _cI, _item, _e, rec) { + this.openNodeAddWindow(rec.data); + }, + + addActionTbar: function() { + let me = this; + + let selection = me.view.getSelection(); + + if (selection.length === 0) { + return; + } + + if (selection[0].data.type === 'fabric') { + me.openNodeAddWindow(selection[0].data); + } + }, + + openNodeAddWindow: function(fabric) { + let me = this; + + let component = me.getNodeEditPanel(fabric.protocol); + + let disallowedNodes = fabric.children + .filter((node) => !node.state || node.state !== 'deleted') + .map((node) => node.node_id); + + Ext.create(component, { + autoShow: true, + fabricId: fabric.id, + protocol: fabric.protocol, + disallowedNodes, + addAnotherCallback: () => { + let successCallback = () => { + let new_fabric = me.getView() + .getStore() + .findRecord('tree_id', fabric.tree_id); + + me.openNodeAddWindow(new_fabric.data); + }; + + me.reload(successCallback); + }, + apiCallDone: (success, _response, _options) => { + if (success) { + me.reload(); + } + }, + }); + }, + + openFabricEditWindow: function(fabric) { + let me = this; + + let component = me.getFabricEditPanel(fabric.protocol); + + let window = Ext.create(component, { + autoShow: true, + fabricId: fabric.id, + }); + + window.on('destroy', () => me.reload()); + }, + + openNodeEditWindow: function(node) { + let me = this; + + let component = me.getNodeEditPanel(node.protocol); + + let window = Ext.create(component, { + autoShow: true, + fabricId: node.fabric_id, + nodeId: node.node_id, + protocol: node.protocol, + }); + + window.on('destroy', () => me.reload()); + }, + + editAction: function(_grid, _rI, _cI, _item, _e, rec) { + let me = this; + + if (rec.data.type === 'fabric') { + me.openFabricEditWindow(rec.data); + } else if (rec.data.type === 'node') { + me.openNodeEditWindow(rec.data); + } else { + console.warn(`unknown type ${rec.data.type}`); + } + }, + + handleDeleteAction: function(url, message) { + let me = this; + let view = me.getView(); + + Ext.Msg.show({ + title: gettext('Confirm'), + icon: Ext.Msg.WARNING, + message: Ext.htmlEncode(message), + buttons: Ext.Msg.YESNO, + defaultFocus: 'no', + callback: function(btn) { + if (btn !== 'yes') { + return; + } + + Proxmox.Utils.API2Request({ + url, + method: 'DELETE', + waitMsgTarget: view, + failure: function(response, opts) { + Ext.Msg.alert(Proxmox.Utils.errorText, response.htmlStatus); + }, + callback: () => me.reload(), + }); + }, + }); + }, + + deleteAction: function(table, rI, cI, item, e, rec) { + let me = this; + + if (rec.data.type === "fabric") { + let message = Ext.String.format( + gettext('Are you sure you want to remove the fabric "{0}"?'), + rec.data.id, + ); + + let url = `/cluster/sdn/fabrics/fabric/${rec.data.id}`; + + me.handleDeleteAction(url, message); + } else if (rec.data.type === "node") { + let message = Ext.String.format( + gettext('Are you sure you want to remove the node "{0}" from the fabric "{1}"?'), + rec.data.node_id, + rec.data.fabric_id, + ); + + let url = `/cluster/sdn/fabrics/node/${rec.data.fabric_id}/${rec.data.node_id}`; + + me.handleDeleteAction(url, message); + } else { + console.warn(`unknown type: ${rec.data.type}`); + } + }, + + init: function(view) { + let me = this; + me.reload(); + }, + }, +}); -- 2.39.5 _______________________________________________ pve-devel mailing list pve-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel