[pve-devel] [PATCH pve-manager v3 03/18] fabric: add common interface panel
Stefan Hanreich
s.hanreich at proxmox.com
Thu May 22 18:17:14 CEST 2025
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 sources.
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
Co-authored-by: Gabriel Goller <g.goller at proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich at proxmox.com>
---
www/manager6/Makefile | 1 +
www/manager6/sdn/fabrics/InterfacePanel.js | 220 +++++++++++++++++++++
2 files changed, 221 insertions(+)
create mode 100644 www/manager6/sdn/fabrics/InterfacePanel.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index efb016948..469a1092e 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -308,6 +308,7 @@ JSSRC= \
sdn/zones/VlanEdit.js \
sdn/zones/VxlanEdit.js \
sdn/fabrics/Common.js \
+ sdn/fabrics/InterfacePanel.js \
storage/ContentView.js \
storage/BackupView.js \
storage/Base.js \
diff --git a/www/manager6/sdn/fabrics/InterfacePanel.js b/www/manager6/sdn/fabrics/InterfacePanel.js
new file mode 100644
index 000000000..0633b3673
--- /dev/null
+++ b/www/manager6/sdn/fabrics/InterfacePanel.js
@@ -0,0 +1,220 @@
+Ext.define('PVE.sdn.Fabric.InterfacePanel', {
+ extend: 'Ext.grid.Panel',
+ mixins: ['Ext.form.field.Field'],
+
+ xtype: 'pveSDNFabricsInterfacePanel',
+
+ nodeInterfaces: {},
+
+ selModel: {
+ mode: 'SIMPLE',
+ type: 'checkboxmodel',
+ },
+
+ commonColumns: [
+ {
+ text: gettext('Status'),
+ dataIndex: 'status',
+ width: 30,
+ renderer: function(value, metaData, record) {
+ let me = this;
+
+ let warning;
+ let nodeInterface = me.nodeInterfaces[record.data.name];
+
+ if (!nodeInterface) {
+ warning = gettext('Interface does not exist on node');
+ } else if ((nodeInterface.ip && record.data.ip) || (nodeInterface.ip6 && record.data.ip6)) {
+ warning = gettext('Interface already has an address configured in /etc/network/interfaces');
+ } else if (nodeInterface.ip || nodeInterface.ip6) {
+ warning = gettext('Configure the IP in the fabric, instead of /etc/network/interfaces');
+ }
+
+ if (warning) {
+ metaData.tdAttr = `data-qtip="${Ext.htmlEncode(Ext.htmlEncode(warning))}"`;
+ return `<i class="fa warning fa-warning"></i>`;
+ }
+
+ return '';
+ },
+
+ },
+ {
+ 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: [],
+
+ controller: {
+ onValueChange: function(field, value) {
+ let me = this;
+
+ let record = field.getWidgetRecord();
+
+ if (!record) {
+ return;
+ }
+
+ let column = field.getWidgetColumn();
+
+ record.set(column.dataIndex, value);
+ record.commit();
+
+ me.getView().checkChange();
+ },
+
+ control: {
+ field: {
+ change: 'onValueChange',
+ },
+ },
+ },
+
+ listeners: {
+ selectionchange: function() {
+ this.checkChange();
+ },
+ },
+
+ 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();
+ },
+
+ setNodeInterfaces: function(interfaces) {
+ let me = this;
+
+ let nodeInterfaces = {};
+ for (const iface of interfaces) {
+ nodeInterfaces[iface.name] = iface;
+ }
+
+ me.nodeInterfaces = nodeInterfaces;
+
+ // reset value when setting new available interfaces
+ me.setValue([]);
+ },
+
+ getValue: function() {
+ let me = this;
+
+ return me.getSelection()
+ .map((rec) => {
+ let data = {};
+
+ for (const [key, value] of Object.entries(rec.data)) {
+ if (value === '' || value === undefined || value === null) {
+ continue;
+ }
+
+ if (['type', 'isDisabled'].includes(key)) {
+ continue;
+ }
+
+ data[key] = value;
+ }
+
+ return PVE.Parser.printPropertyString(data);
+ });
+ },
+
+ setValue: function(value) {
+ let me = this;
+
+ let store = me.getStore();
+
+ let selection = me.getSelectionModel();
+ selection.deselectAll();
+
+ let data = structuredClone(me.nodeInterfaces);
+
+ for (const iface of Object.values(data)) {
+ iface.isDisabled = iface.ip || iface.ip6;
+ }
+
+ let selected = [];
+ let fabricInterfaces = structuredClone(value);
+
+ for (let iface of fabricInterfaces) {
+ iface = PVE.Parser.parsePropertyString(iface);
+
+ selected.push(iface.name);
+
+ // if the fabric configuration defines an interface that was
+ // previously disabled, re-enable the field to allow editing of the
+ // value set in the fabric - we show a warning as well if there is
+ // already an IP configured in /e/n/i
+ iface.isDisabled = false;
+
+ if (Object.prototype.hasOwnProperty.call(data, iface.name)) {
+ data[iface.name] = {
+ ...data[iface.name],
+ // fabric properties have precedence
+ ...iface,
+ };
+ } else {
+ data[iface.name] = iface;
+ }
+ }
+
+ store.setData(Object.values(data));
+
+ let selected_records = selected.map((name) => store.findRecord('name', name));
+ selection.select(selected_records);
+
+ me.resetOriginalValue();
+ },
+
+ getSubmitData: function() {
+ let me = this;
+
+ let name = me.getName();
+ let value = me.getValue();
+
+ if (value.length === 0 && !me.isCreate) {
+ return {
+ 'delete': name,
+ };
+ }
+
+ return {
+ [name]: value,
+ };
+ },
+});
--
2.39.5
More information about the pve-devel
mailing list