Files
proxmox-git-pve-manager/www/manager6/qemu/PCIEdit.js
T
dualfroz 7ed7d7cdb0 fix #5798: ui: qemu pci edit: fix warning for shared IOMMU groups
When selecting a raw PCI device, a warning should be shown if the device
is not in a separate IOMMU group. To not count the other functions of
the selected device itself, the check only compares the bus and device
part of the PCI IDs, by taking their first five characters.

But since the PCI selector also shows the domain, for example
'0000:01:00.0', the first five characters are just the domain. So all
devices of the same domain were treated as functions of the selected
device and the warning was never shown.

Compare everything before the function instead, so the domain, bus and
device, like the IOMMU check in the PCI mapping editor does.

Fixes: f36240c5 ("gui: pci passthrough: consider domain in PCISelector")
Signed-off-by: Michal Fox <me@dualfroz.com>
2026-10-02 16:39:29 +02:00

374 lines
11 KiB
JavaScript

Ext.define('PVE.qemu.PCIInputPanel', {
extend: 'Proxmox.panel.InputPanel',
onlineHelp: 'qm_pci_passthrough_vm_config',
controller: {
xclass: 'Ext.app.ViewController',
setVMConfig: function (vmconfig) {
let me = this;
let view = me.getView();
let vm = me.getViewModel();
me.vmconfig = vmconfig;
let hostpci = me.vmconfig[view.confid] || '';
let values = PVE.Parser.parsePropertyString(hostpci, 'host');
if (values.host) {
if (!values.host.match(/^[0-9a-f]{4}:/i)) {
// add optional domain
values.host = '0000:' + values.host;
}
if (values.host.length < 11) {
// 0000:00:00 format not 0000:00:00.0
values.host += '.0';
values.multifunction = true;
}
values.type = 'raw';
} else if (values.mapping) {
values.type = 'mapped';
}
vm.set('isMapped', values.type !== 'raw');
values['x-vga'] = PVE.Parser.parseBoolean(values['x-vga'], 0);
values.pcie = PVE.Parser.parseBoolean(values.pcie, 0);
values.rombar = PVE.Parser.parseBoolean(values.rombar, 1);
view.setValues(values);
if (!me.vmconfig.machine || me.vmconfig.machine.indexOf('q35') === -1) {
// machine is not set to some variant of q35, so we disable pcie
let pcie = me.lookup('pcie');
pcie.setDisabled(true);
pcie.setBoxLabel(gettext('Q35 only'));
}
if (values.romfile) {
me.lookup('romfile').setVisible(true);
}
},
selectorEnable: function (selector) {
let me = this;
me.pciDevChange(selector, selector.getValue());
},
pciDevChange: function (pcisel, value) {
let me = this;
let mdevfield = me.lookup('mdev');
if (!value) {
if (!pcisel.isDisabled()) {
mdevfield.setDisabled(true);
}
return;
}
let pciDev = pcisel.getStore().getById(value);
mdevfield.setDisabled(!pciDev || !pciDev.data.mdev);
if (!pciDev) {
return;
}
let path = value;
if (pciDev.data.map) {
path = pciDev.data.id;
}
if (pciDev.data.mdev) {
mdevfield.setPciIdOrMapping(path);
}
if (pcisel.reference === 'selector') {
let iommu = pciDev.data.iommugroup;
if (iommu === -1) {
return;
}
// try to find out if there are more devices in that iommu group
let id = path.substring(0, path.indexOf('.')); // 0000:00:00
let count = 0;
pcisel.getStore().each(({ data }) => {
if (data.iommugroup === iommu && !data.id.startsWith(id)) {
count++;
return false;
}
return true;
});
me.lookup('group_warning').setVisible(count > 0);
}
},
onGetValues: function (values) {
let me = this;
let view = me.getView();
if (!view.confid) {
for (let i = 0; i < PVE.Utils.hardware_counts.hostpci; i++) {
if (!me.vmconfig['hostpci' + i.toString()]) {
view.confid = 'hostpci' + i.toString();
break;
}
}
// FIXME: what if no confid was found??
}
values.host?.replace(/^0000:/, ''); // remove optional '0000' domain
if (values.multifunction && values.host) {
values.host = values.host.substring(0, values.host.indexOf('.')); // skip the '.X'
delete values.multifunction;
}
if (values.rombar) {
delete values.rombar;
} else {
values.rombar = 0;
}
if (!values.romfile) {
delete values.romfile;
}
delete values.type;
let ret = {};
ret[view.confid] = PVE.Parser.printPropertyString(values, 'host');
return ret;
},
},
viewModel: {
data: {
isMapped: true,
},
},
setVMConfig: function (vmconfig) {
return this.getController().setVMConfig(vmconfig);
},
onGetValues: function (values) {
return this.getController().onGetValues(values);
},
initComponent: function () {
let me = this;
me.nodename = me.pveSelNode.data.node;
if (!me.nodename) {
throw 'no node name specified';
}
me.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 separate IOMMU group, make sure this is intended.',
userCls: 'pmx-hint',
},
];
me.column1 = [
{
xtype: 'radiofield',
name: 'type',
inputValue: 'mapped',
checked: true,
boxLabel: gettext('Mapped Device'),
bind: {
value: '{isMapped}',
},
},
{
xtype: 'pvePCIMapSelector',
fieldLabel: gettext('Device'),
reference: 'mapped_selector',
name: 'mapping',
labelAlign: 'right',
nodename: me.nodename,
allowBlank: false,
bind: {
disabled: '{!isMapped}',
},
listeners: {
change: 'pciDevChange',
enable: 'selectorEnable',
},
},
{
xtype: 'radiofield',
name: 'type',
inputValue: 'raw',
boxLabel: gettext('Raw Device'),
},
{
xtype: 'pvePCISelector',
fieldLabel: gettext('Device'),
name: 'host',
reference: 'selector',
nodename: me.nodename,
labelAlign: 'right',
allowBlank: false,
disabled: true,
bind: {
disabled: '{isMapped}',
},
onLoadCallBack: function (store, records, success) {
if (!success || !records.length) {
return;
}
me.lookup('iommu_warning').setVisible(
records.every((val) => val.data.iommugroup === -1),
);
},
listeners: {
change: 'pciDevChange',
enable: 'selectorEnable',
},
},
{
xtype: 'proxmoxcheckbox',
fieldLabel: gettext('All Functions'),
reference: 'all_functions',
disabled: true,
labelAlign: 'right',
name: 'multifunction',
bind: {
disabled: '{isMapped}',
},
},
];
me.column2 = [
{
xtype: 'pveMDevSelector',
name: 'mdev',
reference: 'mdev',
disabled: true,
fieldLabel: gettext('MDev Type'),
nodename: me.nodename,
listeners: {
change: function (field, value) {
let multiFunction = me.down('field[name=multifunction]');
if (value) {
multiFunction.setValue(false);
}
multiFunction.setDisabled(!!value);
},
},
},
{
xtype: 'proxmoxcheckbox',
fieldLabel: gettext('Primary GPU'),
name: 'x-vga',
},
];
me.advancedColumn1 = [
{
xtype: 'proxmoxcheckbox',
fieldLabel: 'ROM-Bar',
name: 'rombar',
},
{
xtype: 'displayfield',
submitValue: true,
hidden: true,
fieldLabel: 'ROM-File',
reference: 'romfile',
htmlEncode: true,
name: 'romfile',
},
{
xtype: 'textfield',
name: 'vendor-id',
fieldLabel: gettext('Vendor ID'),
emptyText: gettext('From Device'),
vtype: 'PciId',
allowBlank: true,
submitEmpty: false,
},
{
xtype: 'textfield',
name: 'device-id',
fieldLabel: gettext('Device ID'),
emptyText: gettext('From Device'),
vtype: 'PciId',
allowBlank: true,
submitEmpty: false,
},
];
me.advancedColumn2 = [
{
xtype: 'proxmoxcheckbox',
fieldLabel: 'PCI-Express',
reference: 'pcie',
name: 'pcie',
},
{
xtype: 'textfield',
name: 'sub-vendor-id',
fieldLabel: gettext('Sub-Vendor ID'),
emptyText: gettext('From Device'),
vtype: 'PciId',
allowBlank: true,
submitEmpty: false,
},
{
xtype: 'textfield',
name: 'sub-device-id',
fieldLabel: gettext('Sub-Device ID'),
emptyText: gettext('From Device'),
vtype: 'PciId',
allowBlank: true,
submitEmpty: false,
},
];
me.callParent();
},
});
Ext.define('PVE.qemu.PCIEdit', {
extend: 'Proxmox.window.Edit',
subject: gettext('PCI Device'),
vmconfig: undefined,
isAdd: true,
initComponent: function () {
let me = this;
me.isCreate = !me.confid;
let ipanel = Ext.create('PVE.qemu.PCIInputPanel', {
confid: me.confid,
pveSelNode: me.pveSelNode,
});
Ext.apply(me, {
items: [ipanel],
});
me.callParent();
me.load({
success: ({ result }) => ipanel.setVMConfig(result.data),
});
},
});