[pve-devel] [PATCH manager 5/8] ui: node: add HardwareView and relevant edit windows

Dominik Csapak d.csapak at proxmox.com
Mon Jun 21 15:55:31 CEST 2021


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 <d.csapak at proxmox.com>
---
 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 `<i class="fa ${iconCls}"></i> ${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






More information about the pve-devel mailing list