Ext.define('proxmox-networks', { extend: 'Ext.data.Model', fields: [ { name: 'active', type: 'boolean' }, 'address', 'address6', 'altnames', { name: 'autostart', type: 'boolean' }, 'bridge_ports', { name: 'bridge_vlan_aware', type: 'boolean' }, 'cidr', 'cidr6', 'comments', 'gateway', 'gateway6', 'iface', 'netmask', 'netmask6', 'slaves', 'type', 'vlan-id', 'vlan-raw-device', ], idProperty: 'iface', }); Ext.define('Proxmox.node.NetworkView', { extend: 'Ext.panel.Panel', alias: ['widget.proxmoxNodeNetworkView'], // defines what types of network devices we want to create // order is always the same types: ['bridge', 'bond', 'vlan', 'ovs'], showApplyBtn: false, // if true, the altnames column will be shown by default showAltNames: false, // for options passed down to the network edit window editOptions: {}, initComponent: function () { let me = this; if (!me.nodename) { throw 'no node name specified'; } let baseUrl = `/nodes/${me.nodename}/network`; // sort numbers in names by value, so that 'eno2' comes before 'eno10' let naturalCompare = (a, b) => (a ?? '').localeCompare(b ?? '', undefined, { numeric: true }); let getPortsOrSlaves = (data) => { if (data.type === 'bridge') { return data.bridge_ports; } else if (data.type === 'bond') { return data.slaves; } else if (data.type === 'OVSBridge') { return data.ovs_ports; } else if (data.type === 'OVSBond') { return data.ovs_bonds; } return ''; }; // shared with the name column, so that its header shows the initial sort order let ifaceSorter = Ext.create('Ext.util.Sorter', { id: 'iface', sorterFn: (a, b) => naturalCompare(a.data.iface, b.data.iface), }); let store = Ext.create('Ext.data.Store', { model: 'proxmox-networks', proxy: { type: 'proxmox', url: '/api2/json' + baseUrl, }, sorters: [ifaceSorter], }); let reload = function () { let changeitem = me.down('#changes'); let apply_btn = me.down('#apply'); let revert_btn = me.down('#revert'); Proxmox.Utils.API2Request({ url: baseUrl, failure: function (response, opts) { store.loadData({}); Proxmox.Utils.setErrorMask(me, response.htmlStatus); changeitem.update(''); changeitem.setHidden(true); }, success: function (response, opts) { let result = Ext.decode(response.responseText); store.loadData(result.data); let changes = result.changes; if (changes === undefined || changes === '') { changes = gettext('No changes'); changeitem.setHidden(true); apply_btn.setDisabled(true); revert_btn.setDisabled(true); } else { changeitem.update('
' + Ext.htmlEncode(changes) + '
'); changeitem.setHidden(false); apply_btn.setDisabled(false); revert_btn.setDisabled(false); } }, }); }; let run_editor = function () { let grid = me.down('gridpanel'); let sm = grid.getSelectionModel(); let rec = sm.getSelection()[0]; if (!rec) { return; } Ext.create('Proxmox.node.NetworkEdit', { autoShow: true, nodename: me.nodename, iface: rec.data.iface, iftype: rec.data.type, showAltNames: me.showAltNames, ...me.editOptions, listeners: { destroy: () => reload(), }, }); }; let edit_btn = new Ext.Button({ text: gettext('Edit'), disabled: true, handler: run_editor, }); let sm = Ext.create('Ext.selection.RowModel', {}); let del_btn = new Proxmox.button.StdRemoveButton({ selModel: sm, getUrl: ({ data }) => `${baseUrl}/${data.iface}`, callback: () => reload(), }); let apply_btn = Ext.create('Proxmox.button.Button', { text: gettext('Apply Configuration'), itemId: 'apply', disabled: true, confirmMsg: 'Do you want to apply pending network changes?', hidden: !me.showApplyBtn, handler: function () { Proxmox.Utils.API2Request({ url: baseUrl, method: 'PUT', waitMsgTarget: me, success: function ({ result }, opts) { Ext.create('Proxmox.window.TaskProgress', { autoShow: true, taskDone: reload, upid: result.data, }); }, failure: (response) => Ext.Msg.alert(gettext('Error'), response.htmlStatus), }); }, }); let set_button_status = function () { let rec = sm.getSelection()[0]; edit_btn.setDisabled(!rec); del_btn.setDisabled(!rec); }; let findNextFreeInterfaceId = function (prefix) { for (let next = 0; next <= 9999; next++) { let id = `${prefix}${next.toString()}`; if (!store.getById(id)) { return id; } } Ext.Msg.alert('Error', `No free ID for ${prefix} found!`); return ''; }; let menu_items = []; let addEditWindowToMenu = (iType, iDefault) => { menu_items.push({ text: Proxmox.Utils.render_network_iface_type(iType), handler: () => Ext.create('Proxmox.node.NetworkEdit', { autoShow: true, nodename: me.nodename, iftype: iType, iface_default: findNextFreeInterfaceId(iDefault ?? iType), ...me.editOptions, onlineHelp: 'sysadmin_network_configuration', listeners: { destroy: () => reload(), }, }), }); }; if (me.types.indexOf('bridge') !== -1) { addEditWindowToMenu('bridge', 'vmbr'); } if (me.types.indexOf('bond') !== -1) { addEditWindowToMenu('bond'); } if (me.types.indexOf('vlan') !== -1) { addEditWindowToMenu('vlan'); } if (me.types.indexOf('ovs') !== -1) { if (menu_items.length > 0) { menu_items.push({ xtype: 'menuseparator' }); } addEditWindowToMenu('OVSBridge', 'vmbr'); addEditWindowToMenu('OVSBond', 'bond'); menu_items.push({ text: Proxmox.Utils.render_network_iface_type('OVSIntPort'), handler: () => Ext.create('Proxmox.node.NetworkEdit', { autoShow: true, nodename: me.nodename, iftype: 'OVSIntPort', listeners: { destroy: () => reload(), }, }), }); } let renderer_generator = function (fieldname) { return function (val, metaData, rec) { let tmp = []; if (rec.data[fieldname]) { tmp.push(rec.data[fieldname]); } if (rec.data[fieldname + '6']) { tmp.push(rec.data[fieldname + '6']); } return tmp.join('
') || ''; }; }; Ext.apply(me, { layout: 'border', tbar: [ { text: gettext('Create'), menu: { plain: true, items: menu_items, }, }, '-', { text: gettext('Revert'), itemId: 'revert', handler: function () { Proxmox.Utils.API2Request({ url: baseUrl, method: 'DELETE', waitMsgTarget: me, callback: function () { reload(); }, failure: (response) => Ext.Msg.alert(gettext('Error'), response.htmlStatus), }); }, }, edit_btn, del_btn, '-', apply_btn, ], items: [ { xtype: 'gridpanel', stateful: true, stateId: 'grid-node-network', store: store, selModel: sm, region: 'center', border: false, columns: [ { header: gettext('Name'), sortable: true, dataIndex: 'iface', sorter: ifaceSorter, }, { header: gettext('Alternative Names'), dataIndex: 'altnames', hidden: !me.showAltNames, width: 140, // enough space for 'enx' renderer: (value) => { if (!value) { return ''; } if (Ext.isArray(value)) { return value.map(Ext.htmlEncode).join('
'); } return Ext.htmlEncode(value); }, }, { header: gettext('Type'), sortable: true, width: 120, renderer: Proxmox.Utils.render_network_iface_type, dataIndex: 'type', }, { xtype: 'booleancolumn', header: gettext('Active'), width: 80, sortable: true, dataIndex: 'active', trueText: Proxmox.Utils.yesText, falseText: Proxmox.Utils.noText, undefinedText: Proxmox.Utils.noText, }, { xtype: 'booleancolumn', header: gettext('Autostart'), width: 80, sortable: true, dataIndex: 'autostart', trueText: Proxmox.Utils.yesText, falseText: Proxmox.Utils.noText, undefinedText: Proxmox.Utils.noText, }, { xtype: 'booleancolumn', header: gettext('VLAN aware'), width: 80, sortable: true, dataIndex: 'bridge_vlan_aware', trueText: Proxmox.Utils.yesText, falseText: Proxmox.Utils.noText, undefinedText: Proxmox.Utils.noText, }, { header: gettext('Ports/Slaves'), dataIndex: 'type', renderer: (value, metaData, { data }) => getPortsOrSlaves(data), sorter: (a, b) => naturalCompare(getPortsOrSlaves(a.data), getPortsOrSlaves(b.data)), }, { header: gettext('Bond Mode'), dataIndex: 'bond_mode', renderer: Proxmox.Utils.render_bond_mode, }, { header: gettext('Hash Policy'), hidden: true, dataIndex: 'bond_xmit_hash_policy', }, { header: gettext('IP address'), sortable: true, width: 120, hidden: true, dataIndex: 'address', renderer: renderer_generator('address'), }, { header: gettext('Subnet mask'), width: 120, sortable: true, hidden: true, dataIndex: 'netmask', renderer: renderer_generator('netmask'), }, { header: gettext('CIDR'), width: 150, sortable: true, dataIndex: 'cidr', renderer: renderer_generator('cidr'), }, { header: gettext('Gateway'), width: 150, sortable: true, dataIndex: 'gateway', renderer: renderer_generator('gateway'), }, { header: gettext('VLAN ID'), hidden: true, sortable: true, dataIndex: 'vlan-id', }, { header: gettext('VLAN raw device'), hidden: true, sortable: true, dataIndex: 'vlan-raw-device', }, { header: 'MTU', hidden: true, sortable: true, dataIndex: 'mtu', }, { header: gettext('Comment'), dataIndex: 'comments', flex: 1, renderer: Ext.String.htmlEncode, }, ], listeners: { selectionchange: set_button_status, itemdblclick: run_editor, }, }, { border: false, region: 'south', autoScroll: true, hidden: true, itemId: 'changes', tbar: [ gettext('Pending changes') + ' (' + gettext( "Either reboot or use 'Apply Configuration' (needs ifupdown2) to activate", ) + ')', ], split: true, bodyPadding: 5, flex: 0.6, html: gettext('No changes'), }, ], }); me.callParent(); reload(); }, });