From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 9611C7440B for ; Mon, 21 Jun 2021 15:56:07 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id B847C1C48C for ; Mon, 21 Jun 2021 15:55:47 +0200 (CEST) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id 46BF41C36D for ; Mon, 21 Jun 2021 15:55:38 +0200 (CEST) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 1D13B41DA7 for ; Mon, 21 Jun 2021 15:55:38 +0200 (CEST) From: Dominik Csapak To: pve-devel@lists.proxmox.com Date: Mon, 21 Jun 2021 15:55:31 +0200 Message-Id: <20210621135534.14807-19-d.csapak@proxmox.com> X-Mailer: git-send-email 2.20.1 In-Reply-To: <20210621135534.14807-1-d.csapak@proxmox.com> References: <20210621135534.14807-1-d.csapak@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.792 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% 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 manager 5/8] ui: node: add HardwareView and relevant edit windows 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: , X-List-Received-Date: Mon, 21 Jun 2021 13:56:07 -0000 adds a node specific listing of hardware maps, where the user can see if a mapping is wrong (wrong vendor/device etc) and add/edit/delete them Signed-off-by: Dominik Csapak --- www/manager6/Makefile | 1 + www/manager6/node/Config.js | 8 + www/manager6/node/HardwareView.js | 641 ++++++++++++++++++++++++++++++ 3 files changed, 650 insertions(+) create mode 100644 www/manager6/node/HardwareView.js diff --git a/www/manager6/Makefile b/www/manager6/Makefile index b4e48d33..e1d7730c 100644 --- a/www/manager6/Makefile +++ b/www/manager6/Makefile @@ -188,6 +188,7 @@ JSSRC= \ node/Subscription.js \ node/Summary.js \ node/ZFS.js \ + node/HardwareView.js \ pool/Config.js \ pool/StatusView.js \ pool/Summary.js \ diff --git a/www/manager6/node/Config.js b/www/manager6/node/Config.js index 235a7480..fb03c7c2 100644 --- a/www/manager6/node/Config.js +++ b/www/manager6/node/Config.js @@ -178,6 +178,14 @@ Ext.define('PVE.node.Config', { nodename: nodename, onlineHelp: 'sysadmin_network_configuration', }, + { + xtype: 'pveNodeHardwareView', + nodename, + itemId: 'hardware', + title: gettext('Hardware'), + iconCls: 'fa fa-desktop', + groups: ['services'], + }, { xtype: 'pveCertificatesView', title: gettext('Certificates'), diff --git a/www/manager6/node/HardwareView.js b/www/manager6/node/HardwareView.js new file mode 100644 index 00000000..e6c5ffc2 --- /dev/null +++ b/www/manager6/node/HardwareView.js @@ -0,0 +1,641 @@ +Ext.define('pve-node-hardware', { + extend: 'Ext.data.Model', + fields: [ + 'node', 'type', 'name', 'vendor', 'device', 'pcipath', 'usbpath', 'valid', 'errmsg', + { + name: 'path', + calculate: function(data) { + if (data.type === 'usb') { + return data.usbpath; + } else if (data.type === 'pci') { + return data.pcipath; + } else { + return undefined; + } + }, + }, + ], + idProperty: 'name', +}); + +Ext.define('PVE.node.HardwareView', { + extend: 'Ext.grid.GridPanel', + + alias: 'widget.pveNodeHardwareView', + + onlineHelp: 'pveum_users', + + stateful: true, + stateId: 'grid-node-hardware', + + controller: { + xclass: 'Ext.app.ViewController', + + addPCI: function() { + let me = this; + let nodename = me.getView().nodename; + Ext.create('PVE.node.PCIEditWindow', { + url: `/nodes/${nodename}/hardware/mapping/`, + nodename, + autoShow: true, + }); + }, + + addUSB: function() { + let me = this; + let nodename = me.getView().nodename; + Ext.create('PVE.node.USBEditWindow', { + url: `/nodes/${nodename}/hardware/mapping/`, + nodename, + autoShow: true, + }); + }, + + edit: function() { + let me = this; + let view = me.getView(); + let selection = view.getSelection(); + if (!selection || !selection.length) { + return; + } + let rec = selection[0]; + + let type = 'PVE.node.' + (rec.data.type === 'pci' ? 'PCIEditWindow' : 'USBEditWindow'); + + Ext.create(type, { + url: `/nodes/${rec.data.node}/hardware/mapping/${rec.data.name}`, + autoShow: true, + autoLoad: true, + nodename: rec.data.node, + name: rec.data.name, + }); + }, + }, + + columns: [ + { + header: gettext('Type'), + dataIndex: 'type', + }, + { + header: gettext('Name'), + dataIndex: 'name', + }, + { + header: gettext('Vendor'), + dataIndex: 'vendor', + }, + { + header: gettext('Device'), + dataIndex: 'device', + }, + { + header: gettext('Path'), + dataIndex: 'path', + }, + { + header: gettext('Status'), + dataIndex: 'valid', + flex: 1, + renderer: function(value, mD, record) { + let state = value ? 'good' : 'critical'; + let iconCls = PVE.Utils.get_health_icon(state, true); + let status = value ? gettext("OK") : record.data.errmsg || Proxmox.Utils.unknownText; + return ` ${status}`; + }, + }, + ], + + store: { + type: 'diff', + interval: 30*1000, + rstore: { + type: 'update', + model: 'pve-node-hardware', + }, + }, + + tbar: [ + { + text: gettext('Add'), + menu: [ + { + text: gettext('PCI'), + iconCls: 'pve-itype-icon-pci', + handler: 'addPCI', + }, + { + text: gettext('USB'), + iconCls: 'fa fa-fw fa-usb black', + handler: 'addUSB', + }, + ], + }, + { + xtype: 'proxmoxButton', + text: gettext('Edit'), + disabled: true, + handler: 'edit', + }, + { + xtype: 'proxmoxStdRemoveButton', + getUrl: function(rec) { + return `/api2/extjs/nodes/${rec.data.node}/hardware/mapping/${rec.data.name}`; + }, + disabled: true, + text: gettext('Remove'), + }, + ], + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw "no nodename given"; + } + + me.store.rstore.proxy = { + type: 'proxmox', + url: `/api2/json/nodes/${me.nodename}/hardware/mapping`, + }; + + me.callParent(); + + let store = me.getStore(); + store.rstore.startUpdate(); + + Proxmox.Utils.monStoreErrors(me, store); + }, +}); + +Ext.define('PVE.node.PCIEditWindow', { + extend: 'Proxmox.window.Edit', + + mixins: ['Proxmox.Mixin.CBind'], + + title: gettext('Add PCI mapping'), + + onlineHelp: 'qm_pci_passthrough', + + method: 'POST', + + cbindData: function(initialConfig) { + let me = this; + me.isCreate = !me.name; + me.method = me.isCreate ? 'POST' : 'PUT'; + return { name: me.name }; + }, + + controller: { + xclass: 'Ext.app.ViewController', + + onGetValues: function(values) { + let me = this; + + if (values.multifunction) { + values.pcipath = values.pcipath.substring(0, values.pcipath.indexOf('.')); // skip the '.X' + delete values.multifunction; + } + + return values; + }, + + checkIommu: function(store, records, success) { + let me = this; + if (!success || !records.length) { + return; + } + me.lookup('iommu_warning').setVisible( + records.every((val) => val.data.iommugroup === -1), + ); + }, + + allFunctionsChange: function(_, value) { + let me = this; + if (value) { + let pcisel = me.lookup('pciselector'); + let pcivalue = pcisel.getValue(); + // replace the function by .0 so that we get the correct vendor/device + pcivalue = pcivalue.replace(/.$/, "0"); + pcisel.setValue(pcivalue); + } + }, + + pciChange: function(pcisel, value) { + let me = this; + if (!value) { + return; + } + let all_functions = !!me.lookup('all_functions').getValue(); + + if (all_functions) { + // replace the function by .0 so that we get the correct vendor/device + let newvalue = value.replace(/.$/, "0"); + if (newvalue !== value) { + pcisel.setValue(value); + } + } + + let pciDev = pcisel.getStore().getById(value); + if (!pciDev) { + return; + } + let iommu = pciDev.data.iommugroup; + // try to find out if there are more devices in that iommu group + let id = pciDev.data.id.substring(0, 5); // 00:00 + let count = 0; + pcisel.getStore().each(({ data }) => { + if (data.iommugroup === iommu && data.id.substring(0, 5) !== id) { + count++; + return false; + } + return true; + }); + + me.lookup('group_warning').setVisible(count > 0); + + let fields = [ + 'vendor', + 'device', + 'subsystem_vendor', + 'subsystem_device', + 'iommugroup', + 'mdev', + ]; + + fields.forEach((fieldName) => { + let field = me.lookup(fieldName); + let oldValue = field.getValue(); + if (oldValue !== pciDev.data[fieldName]) { + field.setValue(pciDev.data[fieldName]); + } + }); + }, + + init: function(view) { + let me = this; + + if (!view.nodename) { + throw "no nodename given"; + } + }, + + control: { + 'field[name=multifunction]': { + change: 'allFunctionsChange', + }, + 'field[name=pcipath]': { + change: 'pciChange', + }, + }, + }, + + items: [ + { + xtype: 'inputpanel', + onGetValues: function(values) { + return this.up('window').getController().onGetValues(values); + }, + + columnT: [ + { + xtype: 'displayfield', + reference: 'iommu_warning', + hidden: true, + columnWidth: 1, + padding: '0 0 10 0', + value: 'No IOMMU detected, please activate it.' + + 'See Documentation for further information.', + userCls: 'pmx-hint', + }, + { + xtype: 'displayfield', + reference: 'group_warning', + hidden: true, + columnWidth: 1, + padding: '0 0 10 0', + itemId: 'iommuwarning', + value: 'The selected Device is not in a seperate IOMMU group, make sure this is intended.', + userCls: 'pmx-hint', + }, + ], + + column1: [ + { + xtype: 'hidden', + name: 'type', + value: 'pci', + cbind: { + submitValue: '{isCreate}', + }, + }, + { + xtype: 'proxmoxtextfield', + hidden: true, + reference: 'vendor', + name: 'vendor', + }, + { + xtype: 'proxmoxtextfield', + hidden: true, + reference: 'device', + name: 'device', + }, + { + xtype: 'proxmoxtextfield', + hidden: true, + reference: 'subsystem_vendor', + name: 'subsystem_vendor', + cbind: { + deleteEmpty: '{!isCreate}', + }, + }, + { + xtype: 'proxmoxtextfield', + hidden: true, + reference: 'subsystem_device', + name: 'subsystem_device', + cbind: { + deleteEmpty: '{!isCreate}', + }, + }, + { + xtype: 'proxmoxtextfield', + hidden: true, + reference: 'iommugroup', + name: 'iommugroup', + }, + { + xtype: 'proxmoxtextfield', + hidden: true, + reference: 'mdev', + name: 'mdev', + cbind: { + deleteEmpty: '{!isCreate}', + }, + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Node'), + name: 'node', + cbind: { + value: '{nodename}', + }, + submitValue: true, + allowBlank: false, + }, + { + xtype: 'pmxDisplayEditField', + fieldLabel: gettext('Name'), + cbind: { + editable: '{isCreate}', + value: '{name}', + }, + name: 'name', + allowBlank: false, + }, + ], + + column2: [ + { + xtype: 'pvePCISelector', + fieldLabel: gettext('Device'), + reference: 'pciselector', + name: 'pcipath', + cbind: { + nodename: '{nodename}', + }, + allowBlank: false, + onLoadCallBack: 'checkIommu', + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('All Functions'), + reference: 'all_functions', + name: 'multifunction', + }, + ], + }, + ], +}); + +Ext.define('PVE.node.USBEditWindow', { + extend: 'Proxmox.window.Edit', + + mixins: ['Proxmox.Mixin.CBind'], + + cbindData: function(initialConfig) { + let me = this; + me.isCreate = !me.name; + me.method = me.isCreate ? 'POST' : 'PUT'; + return { name: me.name }; + }, + + title: gettext('Add USB mapping'), + + onlineHelp: 'qm_usb_passthrough', + + method: 'POST', + + controller: { + xclass: 'Ext.app.ViewController', + + onGetValues: function(values) { + let me = this; + + var type = me.getView().down('radiofield').getGroupValue(); + + let val = values[type]; + delete values[type]; + + let usbsel = me.lookup(type); + let usbDev = usbsel.getStore().findRecord('usbid', val, 0, false, true, true); + if (!usbDev) { + return {}; + } + + if (type === 'usbpath') { + values.usbpath = val; + } else if (!me.getView().isCreate) { + values.delete = 'usbpath'; + } + + values.vendor = usbDev.data.vendid; + values.device = usbDev.data.prodid; + + return values; + }, + + usbPathChange: function(usbsel, value) { + let me = this; + if (!value) { + return; + } + + let usbDev = usbsel.getStore().findRecord('usbid', value, 0, false, true, true); + if (!usbDev) { + return; + } + + let usbData = { + vendor: usbDev.data.vendid, + device: usbDev.data.prodid, + }; + + ['vendor', 'device'].forEach((fieldName) => { + let field = me.lookup(fieldName); + let oldValue = field.getValue(); + if (oldValue !== usbData[fieldName]) { + field.setValue(usbData[fieldName]); + } + }); + }, + + modeChange: function(field, value) { + let me = this; + let type = field.inputValue; + let usbsel = me.lookup(type); + usbsel.setDisabled(!value); + }, + + init: function(view) { + let me = this; + + if (!view.nodename) { + throw "no nodename given"; + } + }, + + control: { + 'field[name=usbpath]': { + change: 'usbPathChange', + }, + 'radiofield': { + change: 'modeChange', + }, + }, + }, + + items: [ + { + xtype: 'inputpanel', + onGetValues: function(values) { + return this.up('window').getController().onGetValues(values); + }, + + + column1: [ + { + xtype: 'hidden', + name: 'type', + value: 'usb', + cbind: { + submitValue: '{isCreate}', + }, + }, + { + xtype: 'proxmoxtextfield', + hidden: true, + reference: 'vendor', + name: 'vendor', + }, + { + xtype: 'proxmoxtextfield', + hidden: true, + reference: 'device', + name: 'device', + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Node'), + name: 'node', + cbind: { + value: '{nodename}', + }, + allowBlank: false, + }, + { + xtype: 'pmxDisplayEditField', + fieldLabel: gettext('Name'), + cbind: { + editable: '{isCreate}', + value: '{name}', + }, + name: 'name', + allowBlank: false, + }, + ], + + column2: [ + { + xtype: 'fieldcontainer', + defaultType: 'radiofield', + layout: 'fit', + items: [ + { + name: 'usb', + inputValue: 'hostdevice', + checked: true, + boxLabel: gettext('Use USB Vendor/Device ID'), + submitValue: false, + }, + { + xtype: 'pveUSBSelector', + type: 'device', + reference: 'hostdevice', + name: 'hostdevice', + cbind: { + nodename: '{nodename}', + }, + editable: true, + allowBlank: false, + fieldLabel: gettext('Choose Device'), + labelAlign: 'right', + }, + { + name: 'usb', + inputValue: 'usbpath', + boxLabel: gettext('Use USB Port'), + submitValue: false, + }, + { + xtype: 'pveUSBSelector', + disabled: true, + name: 'usbpath', + reference: 'usbpath', + cbind: { + nodename: '{nodename}', + }, + editable: true, + type: 'port', + allowBlank: false, + fieldLabel: gettext('Choose Port'), + labelAlign: 'right', + }, + ], + }, + ], + }, + ], + + initComponent: function() { + let me = this; + me.callParent(); + + if (!me.name) { + return; + } + me.load({ + success: function(response) { + let data = response.result.data; + if (data.usbpath) { + data.usb = 'usbpath'; + } else { + data.usb = 'hostdevice'; + data.hostdevice = `${data.vendor}:${data.device}`; + } + me.setValues(data); + }, + }); + }, +}); -- 2.20.1