On 2/14/25 14:39, Gabriel Goller wrote: > Add the FabricsView in the sdn category of the datacenter view. The > FabricsView allows to show all the fabrics on all the nodes of the > cluster. > > Co-authored-by: Stefan Hanreich <s.hanre...@proxmox.com> > Signed-off-by: Gabriel Goller <g.gol...@proxmox.com> > --- > PVE/API2/Cluster.pm | 7 +- > PVE/API2/Network.pm | 7 +- > www/manager6/.lint-incremental | 0 > www/manager6/Makefile | 8 + > www/manager6/dc/Config.js | 8 + > www/manager6/sdn/FabricsView.js | 359 ++++++++++++++++++++++++++++++++ > 6 files changed, 379 insertions(+), 10 deletions(-) > create mode 100644 www/manager6/.lint-incremental > create mode 100644 www/manager6/sdn/FabricsView.js > > diff --git a/PVE/API2/Cluster.pm b/PVE/API2/Cluster.pm > index a0e5c11b6e8e..7730aab82a25 100644 > --- a/PVE/API2/Cluster.pm > +++ b/PVE/API2/Cluster.pm > @@ -35,11 +35,8 @@ use PVE::API2::Firewall::Cluster; > use PVE::API2::HAConfig; > use PVE::API2::ReplicationConfig; > > -my $have_sdn; > -eval { > - require PVE::API2::Network::SDN; > - $have_sdn = 1; > -}; > +my $have_sdn = 1; > +require PVE::API2::Network::SDN; > > use base qw(PVE::RESTHandler); > > diff --git a/PVE/API2/Network.pm b/PVE/API2/Network.pm > index cfccdd9e3da3..3c45fe2fb7bf 100644 > --- a/PVE/API2/Network.pm > +++ b/PVE/API2/Network.pm > @@ -16,11 +16,8 @@ use IO::File; > > use base qw(PVE::RESTHandler); > > -my $have_sdn; > -eval { > - require PVE::Network::SDN; > - $have_sdn = 1; > -}; > +my $have_sdn = 1; > +require PVE::Network::SDN; > > my $iflockfn = "/etc/network/.pve-interfaces.lock"; > > diff --git a/www/manager6/.lint-incremental b/www/manager6/.lint-incremental > new file mode 100644 > index 000000000000..e69de29bb2d1 > diff --git a/www/manager6/Makefile b/www/manager6/Makefile > index c94a5cdfbf70..224b6079e833 100644 > --- a/www/manager6/Makefile > +++ b/www/manager6/Makefile > @@ -303,6 +303,14 @@ JSSRC= > \ > sdn/zones/SimpleEdit.js \ > sdn/zones/VlanEdit.js \ > sdn/zones/VxlanEdit.js \ > + sdn/FabricsView.js \ > + sdn/fabrics/Common.js \ > + sdn/fabrics/openfabric/FabricEdit.js \ > + sdn/fabrics/openfabric/NodeEdit.js \ > + sdn/fabrics/openfabric/InterfaceEdit.js \ > + sdn/fabrics/ospf/FabricEdit.js \ > + sdn/fabrics/ospf/NodeEdit.js \ > + sdn/fabrics/ospf/InterfaceEdit.js \ > storage/ContentView.js \ > storage/BackupView.js \ > storage/Base.js \ > diff --git a/www/manager6/dc/Config.js b/www/manager6/dc/Config.js > index 74728c8320e9..68f7be8d6042 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 000000000000..f090ee894b75 > --- /dev/null > +++ b/www/manager6/sdn/FabricsView.js > @@ -0,0 +1,359 @@ > +const FABRIC_PANELS = { > + 'openfabric': 'PVE.sdn.Fabric.OpenFabric.Fabric.Edit', > + 'ospf': 'PVE.sdn.Fabric.Ospf.Fabric.Edit', > +}; > + > +const NODE_PANELS = { > + 'openfabric': 'PVE.sdn.Fabric.OpenFabric.Node.Edit', > + 'ospf': 'PVE.sdn.Fabric.Ospf.Node.Edit', > +}; > + > +const INTERFACE_PANELS = { > + 'openfabric': 'PVE.sdn.Fabric.OpenFabric.Interface.Edit', > + 'ospf': 'PVE.sdn.Fabric.Ospf.Interface.Edit', > +}; > + > +Ext.define('PVE.sdn.Fabric.View', { > + extend: 'Ext.tree.Panel', > + > + xtype: 'pveSDNFabricView', > + > + columns: [ > + { > + xtype: 'treecolumn', > + text: gettext('Name'), > + dataIndex: 'name', > + width: 200, > + }, > + { > + text: gettext('Identifier'), > + dataIndex: 'identifier', > + width: 200, > + }, > + { > + text: gettext('Action'), > + xtype: 'actioncolumn', > + dataIndex: 'text', > + width: 150, > + items: [ > + { > + handler: 'addAction', > + getTip: (_v, _m, _rec) => gettext('Add'), > + getClass: (_v, _m, { data }) => { > + if (data.type === 'fabric') { > + return 'fa fa-plus-square'; > + } > + > + 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) { > + 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) { > + return 'fa critical fa-trash-o'; > + } > + > + return 'pmx-hidden'; > + }, > + isActionDisabled: (_v, _r, _c, _i, { data }) => !data.type, > + }, > + ], > + }, > + ], > + > + store: { > + sorters: ['name'], > + }, > + > + layout: 'fit', > + rootVisible: false, > + animate: false, > + > + tbar: [ > + { > + text: gettext('Add Fabric'), > + menu: [ > + { > + text: gettext('OpenFabric'), > + handler: 'openAddOpenFabricWindow', > + }, > + { > + text: gettext('OSPF'), > + handler: 'openAddOspfWindow', > + }, > + ], > + }, > + { > + xtype: 'proxmoxButton', > + text: gettext('Reload'), > + handler: 'reload', > + }, > + ], > + > + controller: { > + xclass: 'Ext.app.ViewController', > + > + reload: function() { > + let me = this; > + > + Proxmox.Utils.API2Request({ > + url: `/cluster/sdn/fabrics/`, > + method: 'GET', > + success: function(response, opts) { > + let ospf = Object.entries(response.result.data.ospf); > + let openfabric = > Object.entries(response.result.data.openfabric); > + > + // add some metadata so we can merge the objects later and > still know the protocol/type > + ospf = ospf.map(x => { > + if (x["1"].fabric) { > + return Object.assign(x["1"].fabric, { _protocol: > "ospf", _type: "fabric", name: x["0"] }); > + } else if (x["1"].node) { > + let id = x["0"].split("_");
I think we already talked about this, but I don't really remember the outcome. Can we return this already from the API so we don't have to parse it in the frontend? > + return Object.assign(x["1"].node, > + { > + _protocol: "ospf", > + _type: "node", > + node: id[1], > + fabric: id[0], > + }, > + ); > + } else { > + return x; > + } > + }); > + openfabric = openfabric.map(x => { > + if (x["1"].fabric) { > + return Object.assign(x["1"].fabric, { _protocol: > "openfabric", _type: "fabric", name: x["0"] }); > + } else if (x["1"].node) { > + let id = x["0"].split("_"); > + return Object.assign(x["1"].node, > + { > + _protocol: "openfabric", > + _type: "node", > + node: id[1], > + fabric: id[0], > + }, > + ); > + } else { > + return x; > + } > + }); > + > + let data = {}; > + data.ospf = ospf; > + data.openfabric = openfabric; > + > + let fabrics = Object.entries(data).map((protocol) => { > + let protocol_entry = {}; > + protocol_entry.children = protocol["1"].filter(e => > e._type === "fabric").map(fabric => { > + fabric.children = protocol["1"].filter(e => e._type > === "node") > + .filter((node) => > + node.fabric === fabric.name && > node._protocol === fabric._protocol) > + .map((node) => { > + node.children = node.interface > + .map((nic) => { > + let parsed = > PVE.Parser.parsePropertyString(nic); > + parsed.leaf = true; > + parsed.type = > 'interface'; > + // Add meta information > that we need to edit and remove > + parsed._protocol = > node._protocol; > + parsed._fabric = > fabric.name; > + parsed._node = > node.node; > + parsed.iconCls = > 'x-tree-icon-none'; > + return parsed; > + }); > + > + node.expanded = true; > + node.type = 'node'; > + node.name = node.node; > + node._fabric = fabric.name; > + node.identifier = node.net || > node.router_id; > + node.iconCls = 'fa fa-desktop > x-fa-treepanel'; > + > + return node; > + }); > + > + fabric.type = 'fabric'; > + fabric.expanded = true; > + fabric.iconCls = 'fa fa-road > x-fa-treepanel'; > + > + return fabric; > + }); > + protocol_entry.name = protocol["0"]; > + protocol_entry.expanded = true; > + return protocol_entry; > + }); > + > + me.getView().setRootNode({ > + name: '__root', > + expanded: true, > + children: fabrics, > + }); > + }, > + }); > + }, > + > + getFabricEditPanel: function(type) { > + return FABRIC_PANELS[type]; > + }, > + > + getNodeEditPanel: function(type) { > + return NODE_PANELS[type]; > + }, > + > + getInterfaceEditPanel: function(type) { > + return INTERFACE_PANELS[type]; > + }, > + > + addAction: function(_grid, _rI, _cI, _item, _e, rec) { > + let me = this; > + > + let component = me.getNodeEditPanel(rec.data._protocol); > + > + if (!component) { > + console.warn(`unknown protocol ${rec.data._protocol}`); > + return; > + } > + > + let extraRequestParams = { > + type: rec.data.type, > + protocol: rec.data._protocol, > + fabric: rec.data.name, > + }; > + > + let window = Ext.create(component, { > + autoShow: true, > + isCreate: true, > + autoLoad: false, > + extraRequestParams, > + }); > + > + window.on('destroy', () => me.reload()); > + }, > + > + editAction: function(_grid, _rI, _cI, _item, _e, rec) { > + let me = this; > + > + let component = ''; > + let url = ''; > + let autoLoad = true; > + > + if (rec.data.type === 'fabric') { > + component = me.getFabricEditPanel(rec.data._protocol); > + url = > `/cluster/sdn/fabrics/${rec.data._protocol}/${rec.data.name}`; > + } else if (rec.data.type === 'node') { > + component = me.getNodeEditPanel(rec.data._protocol); > + // no url, every request is done manually > + url = > `/cluster/sdn/fabrics/${rec.data._protocol}/${rec.data._fabric}/node/${rec.data.node}`; > + autoLoad = false; > + } else if (rec.data.type === 'interface') { > + component = me.getInterfaceEditPanel(rec.data._protocol); > + url = > `/cluster/sdn/fabrics/${rec.data._protocol}/${rec.data._fabric}/node\ > + /${rec.data._node}/interface/${rec.data.name}`; > + } > + > + if (!component) { > + console.warn(`unknown protocol ${rec.data._protocol} or unknown > type ${rec.data.type}`); > + return; > + } > + > + let window = Ext.create(component, { > + autoShow: true, > + autoLoad: autoLoad, > + isCreate: false, > + submitUrl: url, > + loadUrl: url, > + fabric: rec.data._fabric, > + node: rec.data.node, > + }); > + > + window.on('destroy', () => me.reload()); > + }, > + > + deleteAction: function(table, rI, cI, item, e, { data }) { > + let me = this; > + let view = me.getView(); > + > + Ext.Msg.show({ > + title: gettext('Confirm'), > + icon: Ext.Msg.WARNING, > + message: Ext.String.format(gettext('Are you sure you want to > remove the fabric {0}?'), `${data.name}`), > + buttons: Ext.Msg.YESNO, > + defaultFocus: 'no', > + callback: function(btn) { > + if (btn !== 'yes') { > + return; > + } > + > + let url; > + if (data.type === "node") { > + url = > `/cluster/sdn/fabrics/${data._protocol}/${data._fabric}/node/${data.name}`; > + } else if (data.type === "fabric") { > + url = > `/cluster/sdn/fabrics/${data._protocol}/${data.name}`; > + } else if (data.type === "interface") { > + url = > `/cluster/sdn/fabrics/${data._protocol}/${data._fabric}/node/\ > + ${data._node}/interface/${data.name}`; > + } else { > + console.warn("deleteAction: missing type"); > + } > + > + Proxmox.Utils.API2Request({ > + url, > + method: 'DELETE', > + waitMsgTarget: view, > + failure: function(response, opts) { > + Ext.Msg.alert(gettext('Error'), > response.htmlStatus); > + }, > + callback: me.reload.bind(me), > + }); > + }, > + }); > + }, > + > + openAddOpenFabricWindow: function() { > + let me = this; > + > + let window = Ext.create('PVE.sdn.Fabric.OpenFabric.Fabric.Edit', { > + autoShow: true, > + autoLoad: false, > + isCreate: true, > + }); > + > + window.on('destroy', () => me.reload()); > + }, > + > + openAddOspfWindow: function() { > + let me = this; > + > + let window = Ext.create('PVE.sdn.Fabric.Ospf.Fabric.Edit', { > + autoShow: true, > + autoLoad: false, > + isCreate: true, > + }); > + > + window.on('destroy', () => me.reload()); > + }, > + > + init: function(view) { > + let me = this; > + me.reload(); > + }, > + }, > +}); _______________________________________________ pve-devel mailing list pve-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel