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 B54CB1FF165 for ; Thu, 22 May 2025 18:22:12 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 58CCDAEA2; Thu, 22 May 2025 18:18:26 +0200 (CEST) From: Stefan Hanreich To: pve-devel@lists.proxmox.com Date: Thu, 22 May 2025 18:17:23 +0200 Message-Id: <20250522161731.537011-68-s.hanreich@proxmox.com> X-Mailer: git-send-email 2.39.5 In-Reply-To: <20250522161731.537011-1-s.hanreich@proxmox.com> References: <20250522161731.537011-1-s.hanreich@proxmox.com> MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.218 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 KAM_LAZY_DOMAIN_SECURITY 1 Sending domain does not have any anti-forgery methods RDNS_NONE 0.793 Delivered to internal network by a host with no rDNS SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_NONE 0.001 SPF: sender does not publish an SPF Record Subject: [pve-devel] [PATCH pve-manager v3 12/18] 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" From: Gabriel Goller 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 | 8 + www/manager6/sdn/FabricsView.js | 464 ++++++++++++++++++++++++++++++++ 3 files changed, 473 insertions(+) create mode 100644 www/manager6/sdn/FabricsView.js diff --git a/www/manager6/Makefile b/www/manager6/Makefile index 7541c2d0c..bb5f98d3a 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 b79ba8dcc..3af9479c6 100644 --- a/www/manager6/dc/Config.js +++ b/www/manager6/dc/Config.js @@ -229,6 +229,14 @@ Ext.define('PVE.dc.Config', { hidden: true, iconCls: 'fa fa-shield', itemId: 'sdnfirewall', + }, + { + xtype: 'pveSDNFabricView', + groups: ['sdn'], + title: gettext('Fabrics'), + hidden: true, + iconCls: 'fa fa-road', + itemId: 'sdnfabrics', }); } diff --git a/www/manager6/sdn/FabricsView.js b/www/manager6/sdn/FabricsView.js new file mode 100644 index 000000000..4dd74da85 --- /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('panel'); + view.getController().reload(undefined); + }, + }, + ], + 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