diff --git a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachinePciDevices/index.vue b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachinePciDevices/index.vue
index 4db1987d..e3534f89 100644
--- a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachinePciDevices/index.vue
+++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachinePciDevices/index.vue
@@ -54,6 +54,11 @@ export default {
const vmDevices = this.value?.domain?.devices?.hostDevices || [];
const otherDevices = this.otherDevices(vmDevices).map(({ name }) => name);
+ const vmDeviceNames = vmDevices.map(({ name }) => name);
+
+ this.pciDevices.forEach((row) => {
+ row.allowDisable = !vmDeviceNames.includes(row.metadata.name);
+ });
vmDevices.forEach(({ name, deviceName }) => {
const checkName = (deviceName || '').split('/')?.[1];
diff --git a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineUSBDevices/index.vue b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineUSBDevices/index.vue
index 56646a96..a2afacca 100644
--- a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineUSBDevices/index.vue
+++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineUSBDevices/index.vue
@@ -48,6 +48,13 @@ export default {
this[key] = res[key];
}
+ const vmDevices = this.value?.domain?.devices?.hostDevices || [];
+ const vmDeviceNames = vmDevices.map(({ name }) => name);
+
+ this.devices.forEach((row) => {
+ row.allowDisable = !vmDeviceNames.includes(row.metadata.name);
+ });
+
this.selectedDevices = (this.value?.domain?.devices?.hostDevices || [])
.map(({ name }) => name)
.filter((name) => this.enabledDevices.find((device) => device?.metadata?.name === name));
diff --git a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVGpuDevices/index.vue b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVGpuDevices/index.vue
index fe3ed759..77ec1b1a 100644
--- a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVGpuDevices/index.vue
+++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVGpuDevices/index.vue
@@ -46,6 +46,13 @@ export default {
this[key] = res[key];
}
+ const vmDevices = this.value?.domain?.devices?.gpus || [];
+ const vmDeviceNames = vmDevices.map(({ name }) => name);
+
+ this.devices.forEach((row) => {
+ row.allowDisable = !vmDeviceNames.includes(row.metadata.name);
+ });
+
const vGpus = this.vm.isOff ? [
...(this.value?.domain?.devices?.gpus || []).map(({ name }) => name),
] : [
diff --git a/pkg/harvester/l10n/en-us.yaml b/pkg/harvester/l10n/en-us.yaml
index 8979af8d..bd8db38d 100644
--- a/pkg/harvester/l10n/en-us.yaml
+++ b/pkg/harvester/l10n/en-us.yaml
@@ -369,6 +369,9 @@ harvester:
claimError: Error enabling passthrough on {name}
unclaimError: Error disabling passthrough on {name}
cantUnclaim: You cannot disable passthrough on a device claimed by another user.
+ detachWarning:
+ title: Cannot Disable Passthrough
+ message: Please detach the device from the VM and save it first before disabling passthrough.
enableGroup: Enable Group
disableGroup: Disable Group
labelRequired: "This rule should not be manually altered: it ensures that the PCI devices selected for this virtual machine are available on the virtual machine's host."
@@ -1821,6 +1824,9 @@ harvester:
vgpu:
label: vGPU Devices
noPermission: Please contact system administrator to add Harvester add-ons first.
+ detachWarning:
+ title: Cannot Disable vGPU
+ message: Please detach the device from the VM and save it first before disabling this vGPU device.
goSetting:
prefix: The nvidia-driver-toolkit add-on is not enabled, click
middle: here
@@ -1855,6 +1861,9 @@ harvester:
claimError: Error enabling passthrough on {name}
unclaimError: Error disabling passthrough on {name}
cantUnclaim: You cannot disable passthrough on a device claimed by another user.
+ detachWarning:
+ title: Cannot Disable Passthrough
+ message: Please detach the device from the VM and save it first before disabling passthrough.
enablePassthroughWarning: 'Please re-enable the USB device if the device path changes in the following situations:
 1) Re-plugging the USB device.
 2) Rebooting the node.
An incorrect device path may cause passthrough to fail.'
harvesterVlanConfigMigrateDialog:
diff --git a/pkg/harvester/list/devices.harvesterhci.io.pcidevice.vue b/pkg/harvester/list/devices.harvesterhci.io.pcidevice.vue
index ddb62da8..3bf639c4 100644
--- a/pkg/harvester/list/devices.harvesterhci.io.pcidevice.vue
+++ b/pkg/harvester/list/devices.harvesterhci.io.pcidevice.vue
@@ -64,6 +64,10 @@ export default {
const inStore = this.$store.getters['currentProduct'].inStore;
const rows = this.$store.getters[`${ inStore }/all`](HCI.PCI_DEVICE);
+ rows.forEach((row) => {
+ row.allowDisable = true;
+ });
+
return rows;
}
},
diff --git a/pkg/harvester/list/devices.harvesterhci.io.usbdevice.vue b/pkg/harvester/list/devices.harvesterhci.io.usbdevice.vue
index e63b1db8..6a1ce306 100644
--- a/pkg/harvester/list/devices.harvesterhci.io.usbdevice.vue
+++ b/pkg/harvester/list/devices.harvesterhci.io.usbdevice.vue
@@ -54,7 +54,13 @@ export default {
devices() {
const inStore = this.$store.getters['currentProduct'].inStore;
- return this.$store.getters[`${ inStore }/all`](HCI.USB_DEVICE) || [];
+ const data = this.$store.getters[`${ inStore }/all`](HCI.USB_DEVICE) || [];
+
+ data.forEach((row) => {
+ row.allowDisable = true;
+ });
+
+ return data;
}
},
diff --git a/pkg/harvester/list/devices.harvesterhci.io.vgpudevice.vue b/pkg/harvester/list/devices.harvesterhci.io.vgpudevice.vue
index 53bfa358..80dff8fa 100644
--- a/pkg/harvester/list/devices.harvesterhci.io.vgpudevice.vue
+++ b/pkg/harvester/list/devices.harvesterhci.io.vgpudevice.vue
@@ -65,6 +65,10 @@ export default {
const vGpuDevices = this.$store.getters[`${ inStore }/all`](HCI.VGPU_DEVICE) || [];
const srioVGpuDevices = this.$store.getters[`${ inStore }/all`](HCI.SR_IOVGPU_DEVICE) || [];
+ vGpuDevices.forEach((row) => {
+ row.allowDisable = true;
+ });
+
if (this.hasSRIOVGPUSchema) {
return vGpuDevices.filter((device) => !!srioVGpuDevices.find((s) => s.isEnabled && s.spec?.nodeName === device.spec?.nodeName));
}
diff --git a/pkg/harvester/models/devices.harvesterhci.io.pcidevice.js b/pkg/harvester/models/devices.harvesterhci.io.pcidevice.js
index 26104a7e..6b83ca19 100644
--- a/pkg/harvester/models/devices.harvesterhci.io.pcidevice.js
+++ b/pkg/harvester/models/devices.harvesterhci.io.pcidevice.js
@@ -144,6 +144,12 @@ export default class PCIDevice extends SteveModel {
// 'disable' passthrough deletes claim
// backend should return error if device is in use
async disablePassthrough() {
+ if (!this.allowDisable) {
+ this.showDetachWarning();
+
+ return;
+ }
+
try {
if (!this.claimedByMe) {
throw new Error(this.$rootGetters['i18n/t']('harvester.pci.cantUnclaim', { name: escapeHtml(this.metadata.name) }));
@@ -169,4 +175,20 @@ export default class PCIDevice extends SteveModel {
get groupByDevice() {
return this.status?.description;
}
+
+ showDetachWarning() {
+ this.$dispatch('growl/warning', {
+ title: this.$rootGetters['i18n/t']('harvester.pci.detachWarning.title'),
+ message: this.$rootGetters['i18n/t']('harvester.pci.detachWarning.message'),
+ timeout: 5000
+ }, { root: true });
+ }
+
+ get allowDisable() {
+ return this._allowDisable;
+ }
+
+ set allowDisable(value) {
+ this._allowDisable = value;
+ }
}
diff --git a/pkg/harvester/models/devices.harvesterhci.io.usbdevice.js b/pkg/harvester/models/devices.harvesterhci.io.usbdevice.js
index c6a995c1..27ab6ccf 100644
--- a/pkg/harvester/models/devices.harvesterhci.io.usbdevice.js
+++ b/pkg/harvester/models/devices.harvesterhci.io.usbdevice.js
@@ -133,6 +133,12 @@ export default class USBDevice extends SteveModel {
// 'disable' passthrough deletes claim
// backend should return error if device is in use
async disablePassthrough() {
+ if (!this.allowDisable) {
+ this.showDetachWarning();
+
+ return;
+ }
+
try {
if (!this.claimedByMe) {
throw new Error(this.$rootGetters['i18n/t']('harvester.usb.cantUnclaim', { name: escapeHtml(this.metadata.name) }));
@@ -158,4 +164,20 @@ export default class USBDevice extends SteveModel {
get groupByDevice() {
return this.status?.description;
}
+
+ showDetachWarning() {
+ this.$dispatch('growl/warning', {
+ title: this.$rootGetters['i18n/t']('harvester.usb.detachWarning.title'),
+ message: this.$rootGetters['i18n/t']('harvester.usb.detachWarning.message'),
+ timeout: 5000
+ }, { root: true });
+ }
+
+ get allowDisable() {
+ return this._allowDisable;
+ }
+
+ set allowDisable(value) {
+ this._allowDisable = value;
+ }
}
diff --git a/pkg/harvester/models/devices.harvesterhci.io.vgpudevice.js b/pkg/harvester/models/devices.harvesterhci.io.vgpudevice.js
index f529bb52..3db4e2cb 100644
--- a/pkg/harvester/models/devices.harvesterhci.io.vgpudevice.js
+++ b/pkg/harvester/models/devices.harvesterhci.io.vgpudevice.js
@@ -100,6 +100,12 @@ export default class VGpuDevice extends SteveModel {
}
async disableVGpu() {
+ if (!this.allowDisable) {
+ this.showDetachWarning();
+
+ return;
+ }
+
const { vGPUTypeName, enabled } = this.spec;
try {
@@ -126,4 +132,20 @@ export default class VGpuDevice extends SteveModel {
get vGpuAvailableTypes() {
return this.status?.availableTypes ? Object.keys(this.status.availableTypes) : [];
}
+
+ showDetachWarning() {
+ this.$dispatch('growl/warning', {
+ title: this.$rootGetters['i18n/t']('harvester.vgpu.detachWarning.title'),
+ message: this.$rootGetters['i18n/t']('harvester.vgpu.detachWarning.message'),
+ timeout: 5000
+ }, { root: true });
+ }
+
+ get allowDisable() {
+ return this._allowDisable;
+ }
+
+ set allowDisable(value) {
+ this._allowDisable = value;
+ }
}