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.csa...@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



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

Reply via email to