diff --git a/www/manager6/Makefile b/www/manager6/Makefile index 0cb922d6..2d884f4a 100644 --- a/www/manager6/Makefile +++ b/www/manager6/Makefile @@ -174,6 +174,8 @@ JSSRC= \ dc/UserTagAccessEdit.js \ dc/RegisteredTagsEdit.js \ dc/RealmSyncJob.js \ + dc/PCIMapView.js \ + dc/USBMapView.js \ lxc/CmdMenu.js \ lxc/Config.js \ lxc/CreateWizard.js \ diff --git a/www/manager6/dc/Config.js b/www/manager6/dc/Config.js index f9f937a5..10a4d83a 100644 --- a/www/manager6/dc/Config.js +++ b/www/manager6/dc/Config.js @@ -274,8 +274,50 @@ Ext.define('PVE.dc.Config', { iconCls: 'fa fa-bar-chart', itemId: 'metricservers', onlineHelp: 'external_metric_server', - }, - { + }); + } + + if (caps.mapping['Mapping.Audit'] || + caps.mapping['Mapping.Use'] || + caps.mapping['Mapping.Modify']) { + me.items.push( + { + xtype: 'container', + onlineHelp: 'resource_mapping', + title: gettext('Resource Mappings'), + itemId: 'resources', + iconCls: 'fa fa-folder-o', + layout: { + type: 'vbox', + align: 'stretch', + multi: true, + }, + scrollable: true, + defaults: { + collapsible: true, + animCollapse: false, + margin: '7 10 3 10', + }, + items: [ + { + collapsible: true, + xtype: 'pveDcPCIMapView', + title: gettext('PCI Devices'), + flex: 1, + }, + { + collapsible: true, + xtype: 'pveDcUSBMapView', + title: gettext('USB Devices'), + flex: 1, + }, + ], + }, + ); + } + + if (caps.dc['Sys.Audit']) { + me.items.push({ xtype: 'pveDcSupport', title: gettext('Support'), itemId: 'support', diff --git a/www/manager6/dc/PCIMapView.js b/www/manager6/dc/PCIMapView.js new file mode 100644 index 00000000..3efa19d8 --- /dev/null +++ b/www/manager6/dc/PCIMapView.js @@ -0,0 +1,106 @@ +Ext.define('pve-resource-pci-tree', { + extend: 'Ext.data.Model', + idProperty: 'internalId', + fields: ['type', 'text', 'path', 'id', 'subsystem-id', 'iommugroup', 'description', 'digest'], +}); + +Ext.define('PVE.dc.PCIMapView', { + extend: 'PVE.tree.ResourceMapTree', + alias: 'widget.pveDcPCIMapView', + + editWindowClass: 'PVE.window.PCIMapEditWindow', + baseUrl: '/cluster/mapping/pci', + mapIconCls: 'pve-itype-icon-pci', + getStatusCheckUrl: (node) => `/nodes/${node}/hardware/pci?pci-class-blacklist=`, + entryIdProperty: 'path', + + checkValidity: function(data, node) { + let me = this; + let ids = {}; + data.forEach((entry) => { + ids[entry.id] = entry; + }); + me.getRootNode()?.cascade(function(rec) { + if (rec.data.node !== node || rec.data.type !== 'map') { + return; + } + + let id = rec.data.path; + if (!id.match(/\.\d$/)) { + id += '.0'; + } + let device = ids[id]; + if (!device) { + rec.set('valid', 0); + rec.set('errmsg', Ext.String.format(gettext("Cannot find PCI id {0}"), id)); + rec.commit(); + return; + } + + + let deviceId = `${device.vendor}:${device.device}`.replace(/0x/g, ''); + let subId = `${device.subsystem_vendor}:${device.subsystem_device}`.replace(/0x/g, ''); + + let toCheck = { + id: deviceId, + 'subsystem-id': subId, + iommugroup: device.iommugroup !== -1 ? device.iommugroup : undefined, + }; + + let valid = 1; + let errors = []; + let errText = gettext("Configuration for {0} not correct ('{1}' != '{2}')"); + for (const [key, validValue] of Object.entries(toCheck)) { + if (`${rec.data[key]}` !== `${validValue}`) { + errors.push(Ext.String.format(errText, key, rec.data[key] ?? '', validValue)); + valid = 0; + } + } + + rec.set('valid', valid); + rec.set('errmsg', errors.join('
')); + rec.commit(); + }); + }, + + store: { + sorters: 'text', + model: 'pve-resource-pci-tree', + data: {}, + }, + + columns: [ + { + xtype: 'treecolumn', + text: gettext('ID/Node/Path'), + dataIndex: 'text', + width: 200, + }, + { + text: gettext('Vendor/Device'), + dataIndex: 'id', + }, + { + text: gettext('Subsystem Vendor/Device'), + dataIndex: 'subsystem-id', + }, + { + text: gettext('IOMMU group'), + dataIndex: 'iommugroup', + }, + { + header: gettext('Status'), + dataIndex: 'valid', + flex: 1, + renderer: 'renderStatus', + }, + { + header: gettext('Comment'), + dataIndex: 'description', + renderer: function(value, _meta, record) { + return value ?? record.data.comment; + }, + flex: 1, + }, + ], +}); diff --git a/www/manager6/dc/USBMapView.js b/www/manager6/dc/USBMapView.js new file mode 100644 index 00000000..953e2425 --- /dev/null +++ b/www/manager6/dc/USBMapView.js @@ -0,0 +1,98 @@ +Ext.define('pve-resource-usb-tree', { + extend: 'Ext.data.Model', + idProperty: 'internalId', + fields: ['type', 'text', 'path', 'id', 'description', 'digest'], +}); + +Ext.define('PVE.dc.USBMapView', { + extend: 'PVE.tree.ResourceMapTree', + alias: 'widget.pveDcUSBMapView', + + editWindowClass: 'PVE.window.USBMapEditWindow', + baseUrl: '/cluster/mapping/usb', + mapIconCls: 'fa fa-usb', + getStatusCheckUrl: (node) => `/nodes/${node}/hardware/usb`, + entryIdProperty: 'id', + + checkValidity: function(data, node) { + let me = this; + let ids = {}; + let paths = {}; + data.forEach((entry) => { + ids[`${entry.vendid}:${entry.prodid}`] = entry; + paths[`${entry.busnum}-${entry.usbpath}`] = entry; + }); + me.getRootNode()?.cascade(function(rec) { + if (rec.data.node !== node || rec.data.type !== 'map') { + return; + } + + let device; + if (rec.data.path) { + device = paths[rec.data.path]; + } + device ??= ids[rec.data.id]; + + if (!device) { + rec.set('valid', 0); + rec.set('errmsg', Ext.String.format(gettext("Cannot find USB device {0}"), rec.data.id)); + rec.commit(); + return; + } + + + let deviceId = `${device.vendid}:${device.prodid}`.replace(/0x/g, ''); + + let toCheck = { + id: deviceId, + }; + + let valid = 1; + let errors = []; + let errText = gettext("Configuration for {0} not correct ('{1}' != '{2}')"); + for (const [key, validValue] of Object.entries(toCheck)) { + if (rec.data[key] !== validValue) { + errors.push(Ext.String.format(errText, key, rec.data[key] ?? '', validValue)); + valid = 0; + } + } + + rec.set('valid', valid); + rec.set('errmsg', errors.join('
')); + rec.commit(); + }); + }, + + store: { + sorters: 'text', + model: 'pve-resource-usb-tree', + data: {}, + }, + + columns: [ + { + xtype: 'treecolumn', + text: gettext('ID/Node/Vendor&Device'), + dataIndex: 'text', + width: 200, + }, + { + text: gettext('Path'), + dataIndex: 'path', + }, + { + header: gettext('Status'), + dataIndex: 'valid', + flex: 1, + renderer: 'renderStatus', + }, + { + header: gettext('Comment'), + dataIndex: 'description', + renderer: function(value, _meta, record) { + return value ?? record.data.comment; + }, + flex: 1, + }, + ], +});