diff --git a/pkg/harvester/detail/harvesterhci.io.vmsnapshot/index.vue b/pkg/harvester/detail/harvesterhci.io.vmsnapshot/index.vue
index f155af46..fc54aa70 100644
--- a/pkg/harvester/detail/harvesterhci.io.vmsnapshot/index.vue
+++ b/pkg/harvester/detail/harvesterhci.io.vmsnapshot/index.vue
@@ -172,6 +172,9 @@ export default {
:cpu="cpu"
:mode="mode"
:memory="memory"
+ :max-cpu="maxCpu"
+ :max-memory="maxMemory"
+ :enable-hot-plug="cpuMemoryHotplugEnabled"
/>
diff --git a/pkg/harvester/detail/kubevirt.io.virtualmachine/VirtualMachineTabs/VirtualMachineBasics.vue b/pkg/harvester/detail/kubevirt.io.virtualmachine/VirtualMachineTabs/VirtualMachineBasics.vue
index 2e3acf2a..7f92f48e 100644
--- a/pkg/harvester/detail/kubevirt.io.virtualmachine/VirtualMachineTabs/VirtualMachineBasics.vue
+++ b/pkg/harvester/detail/kubevirt.io.virtualmachine/VirtualMachineTabs/VirtualMachineBasics.vue
@@ -5,6 +5,7 @@ import CreateEditView from '@shell/mixins/create-edit-view';
import HarvesterIpAddress from '../../../formatters/HarvesterIpAddress';
import VMConsoleBar from '../../../components/VMConsoleBar';
import { HCI } from '../../../types';
+import { getVmCPUMemoryValues } from '../../../utils/cpuMemory';
const UNDEFINED = 'n/a';
@@ -91,9 +92,9 @@ export default {
},
flavor() {
- const domain = this.value?.spec?.template?.spec?.domain;
+ const { cpu, memory } = getVmCPUMemoryValues(this.value);
- return `${ domain.cpu?.cores } vCPU , ${ domain.resources?.limits?.memory } ${ this.t('harvester.virtualMachine.input.memory') }`;
+ return `${ cpu } vCPU , ${ memory } ${ this.t('harvester.virtualMachine.input.memory') }`;
},
kernelRelease() {
diff --git a/pkg/harvester/dialog/HarvesterCPUMemoryHotPlugDialog.vue b/pkg/harvester/dialog/HarvesterCPUMemoryHotPlugDialog.vue
new file mode 100644
index 00000000..a597107a
--- /dev/null
+++ b/pkg/harvester/dialog/HarvesterCPUMemoryHotPlugDialog.vue
@@ -0,0 +1,185 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pkg/harvester/edit/harvesterhci.io.virtualmachinetemplateversion.vue b/pkg/harvester/edit/harvesterhci.io.virtualmachinetemplateversion.vue
index 14ca5fd3..6c78ad7e 100644
--- a/pkg/harvester/edit/harvesterhci.io.virtualmachinetemplateversion.vue
+++ b/pkg/harvester/edit/harvesterhci.io.virtualmachinetemplateversion.vue
@@ -259,6 +259,9 @@ export default {
diff --git a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineCpuMemory.vue b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineCpuMemory.vue
index b71333aa..165d71bb 100644
--- a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineCpuMemory.vue
+++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineCpuMemory.vue
@@ -2,23 +2,44 @@
import UnitInput from '@shell/components/form/UnitInput';
import InputOrDisplay from '@shell/components/InputOrDisplay';
import { GIBIBYTE } from '../../utils/unit';
+import { Checkbox } from '@components/Form/Checkbox';
+import { _VIEW } from '@shell/config/query-params';
+import { HCI } from '../../types';
+import { allHash } from '@shell/utils/promise';
+import { HCI_SETTING } from '../../config/settings';
+
+const DEFAULT_HOT_PLUG_TIMES = 4;
export default {
name: 'HarvesterEditCpuMemory',
emits: ['updateCpuMemory'],
- components: { UnitInput, InputOrDisplay },
+ components: {
+ UnitInput, InputOrDisplay, Checkbox
+ },
props: {
cpu: {
type: Number,
default: null
},
+ maxCpu: {
+ type: Number,
+ default: null
+ },
memory: {
type: String,
default: null
},
+ maxMemory: {
+ type: String,
+ default: null
+ },
+ enableHotPlug: {
+ type: Boolean,
+ default: false
+ },
mode: {
type: String,
default: 'create',
@@ -29,21 +50,59 @@ export default {
}
},
+ async fetch() {
+ const inStore = this.$store.getters['currentProduct'].inStore;
+
+ const res = await allHash({ settings: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SETTING }) });
+
+ this.settings = res.settings || [];
+ },
+
data() {
return {
GIBIBYTE,
- localCpu: this.cpu,
- localMemory: this.memory
+ localCpu: this.cpu,
+ localMemory: this.memory,
+ maxLocalCpu: this.maxCpu,
+ maxLocalMemory: this.maxMemory,
+ localEnableHotPlug: this.enableHotPlug,
+ settings: []
};
},
computed: {
- cupDisplay() {
+ isView() {
+ return this.mode === _VIEW;
+ },
+ cpuDisplay() {
return `${ this.localCpu } C`;
},
+ maxCpuDisplay() {
+ return `${ this.maxLocalCpu } C`;
+ },
+
memoryDisplay() {
return `${ this.localMemory }`;
+ },
+
+ maxMemoryDisplay() {
+ return `${ this.maxLocalMemory }`;
+ },
+
+ cpuMemoryHotplugTooltip() {
+ return this.t('harvester.virtualMachine.hotplug.tooltip', { hotPlugTimes: this.maxHotplugRatio });
+ },
+
+ isCPUMemoryHotPlugFeatureEnabled() {
+ return this.$store.getters['harvester-common/getFeatureEnabled']('cpuMemoryHotplug');
+ },
+
+ maxHotplugRatio() {
+ const maxHotPlugRatioSetting = this.settings.find((s) => s.id === HCI_SETTING.MAX_HOTPLUG_RATIO);
+ const maxPlugRatio = maxHotPlugRatioSetting?.value ? parseInt(maxHotPlugRatioSetting.value, 10) : DEFAULT_HOT_PLUG_TIMES;
+
+ return maxPlugRatio;
}
},
@@ -55,71 +114,161 @@ export default {
if (neu && !neu.includes('null')) {
this.localMemory = neu;
}
- }
+ },
+ maxCpu(neu) {
+ this.maxLocalCpu = neu;
+ },
+ maxMemory(neu) {
+ if (neu && !neu.includes('null')) {
+ this.maxLocalMemory = neu;
+ }
+ },
+ enableHotPlug(neu) {
+ this.localEnableHotPlug = neu;
+ },
+
},
methods: {
- change() {
- let memory = '';
-
- if (String(this.localMemory).includes('Gi')) {
- memory = this.localMemory;
+ hotPlugChanged(neu) {
+ // If hot plug is enabled, we need to update the maxCpu and maxMemory values
+ if (neu) {
+ this.maxLocalCpu = this.localCpu ? this.localCpu * this.maxHotplugRatio : null;
+ this.maxLocalMemory = this.localMemory ? `${ parseInt(this.localMemory, 10) * this.maxHotplugRatio }${ GIBIBYTE }` : null;
+ this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, neu);
} else {
- memory = `${ this.localMemory }${ GIBIBYTE }`;
+ this.$emit('updateCpuMemory', this.localCpu, this.localMemory, '', null, neu);
}
- if (memory.includes('null')) {
- memory = null;
- }
- this.$emit('updateCpuMemory', this.localCpu, memory);
},
-
+ changeMemory() {
+ if (this.localEnableHotPlug) {
+ this.maxLocalMemory = this.localMemory ? `${ parseInt(this.localMemory, 10) * this.maxHotplugRatio }${ GIBIBYTE }` : null;
+ }
+ this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, this.localEnableHotPlug);
+ },
+ changeCPU() {
+ if (this.localEnableHotPlug) {
+ this.maxLocalCpu = this.localCpu ? this.localCpu * this.maxHotplugRatio : null;
+ }
+ this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, this.localEnableHotPlug);
+ },
+ changeMaxCPUMemory() {
+ this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, this.localEnableHotPlug);
+ },
}
};
-
-
-
-
+
+
+
-
+ class="mb-10"
+ >
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
-
+ class="mt-20"
+ >
+
+
+
+
+
+
+
+
diff --git a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineSSHKey.vue b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineSSHKey.vue
index c0eeaef8..ad22942b 100644
--- a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineSSHKey.vue
+++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineSSHKey.vue
@@ -240,7 +240,7 @@ export default {
-
+
0) {
return Promise.reject(errors);
@@ -604,8 +606,11 @@ export default {
>
diff --git a/pkg/harvester/l10n/en-us.yaml b/pkg/harvester/l10n/en-us.yaml
index d2709772..98d5b86a 100644
--- a/pkg/harvester/l10n/en-us.yaml
+++ b/pkg/harvester/l10n/en-us.yaml
@@ -157,6 +157,10 @@ harvester:
hotplug:
success: 'Volume { diskName } is mounted to the virtual machine { vm }.'
title: Add Volume
+ cpuMemoryHotplug:
+ success: 'CPU and Memory are updated to the virtual machine { vm }.'
+ title: Edit CPU and Memory
+ maxResourcesMessage: 'You can increase the CPU to maximum { maxCpu }C and memory to maximum { maxMemory }.'
hotunplug:
success: 'Volume { name } is detached successfully.'
snapshot:
@@ -213,6 +217,7 @@ harvester:
suspendSchedule: Suspend
restoreExistingVM: Replace Existing
migrate: Migrate
+ cpuAndMemoryHotplug: Edit CPU and Memory
abortMigration: Abort Migration
createTemplate: Generate Template
enableMaintenance: Enable Maintenance Mode
@@ -571,6 +576,10 @@ harvester:
virtualMachine:
label: Virtual Machines
osType: OS Type
+ hotplug:
+ title: Enable CPU and memory hotplug
+ tooltip: The default maximum CPU and maximum memory are {hotPlugTimes} times based on CPU and memory.
+ restartVMMessage: Restart action is required for the virtual machine configuration change to take effect
instance:
singleInstance:
multipleInstance:
@@ -716,6 +725,9 @@ harvester:
invalidUser: Invalid Username.
input:
name: Name
+ cpu: CPU
+ maxCpu: Maximum CPU
+ maxMemory: Maximum Memory
memory: Memory
image: Image
sshKey: SSHKey
@@ -1712,6 +1724,7 @@ advancedSettings:
'harv-upgrade-config': 'Configure image preloading and VM restore options for upgrades. See related fields in settings/upgrade-config'
'harv-vm-migration-network': 'Segregated network for VM migration traffic.'
'harv-rancher-cluster': 'Configure Rancher cluster integration settings for guest cluster management.'
+ 'harv-max-hotplug-ratio': 'The ratio for kubevirt to limit the maximum CPU and memory that can be hotplugged to a VM. The value could be an integer between 1 and 20, default to 4.'
typeLabel:
kubevirt.io.virtualmachine: |-
diff --git a/pkg/harvester/list/kubevirt.io.virtualmachine.vue b/pkg/harvester/list/kubevirt.io.virtualmachine.vue
index c0ab97c5..7750423e 100644
--- a/pkg/harvester/list/kubevirt.io.virtualmachine.vue
+++ b/pkg/harvester/list/kubevirt.io.virtualmachine.vue
@@ -22,8 +22,8 @@ export const VM_HEADERS = [
{
name: 'CPU',
label: 'CPU',
- sort: ['spec.template.spec.domain.cpu.cores'],
- value: 'spec.template.spec.domain.cpu.cores',
+ sort: ['displayCPU'],
+ value: 'displayCPU',
align: 'center',
dashIfEmpty: true,
},
diff --git a/pkg/harvester/mixins/harvester-vm/index.js b/pkg/harvester/mixins/harvester-vm/index.js
index ca1da7d7..9a0427c3 100644
--- a/pkg/harvester/mixins/harvester-vm/index.js
+++ b/pkg/harvester/mixins/harvester-vm/index.js
@@ -3,10 +3,9 @@ import jsyaml from 'js-yaml';
import isEqual from 'lodash/isEqual';
import isEmpty from 'lodash/isEmpty';
import difference from 'lodash/difference';
-
import { sortBy } from '@shell/utils/sort';
import { set } from '@shell/utils/object';
-
+import { getVmCPUMemoryValues } from '../../utils/cpuMemory';
import { allHash } from '@shell/utils/promise';
import { randomStr } from '@shell/utils/string';
import { base64Decode } from '@shell/utils/crypto';
@@ -164,6 +163,9 @@ export default {
deleteAgent: true,
memory: null,
cpu: '',
+ maxMemory: null,
+ maxCpu: '',
+ cpuMemoryHotplugEnabled: false,
reservedMemory: null,
accessCredentials: [],
efiEnabled: false,
@@ -348,8 +350,11 @@ export default {
const runStrategy = spec.runStrategy || 'RerunOnFailure';
const machineType = spec.template.spec.domain?.machine?.type || this.machineTypes[0];
- const cpu = spec.template.spec.domain?.cpu?.cores;
- const memory = spec.template.spec.domain.resources.limits.memory;
+ const {
+ cpu, memory, maxCpu, maxMemory, isHotplugEnabled
+ } = getVmCPUMemoryValues(vm);
+ const cpuMemoryHotplugEnabled = isHotplugEnabled;
+
const reservedMemory = vm.metadata?.annotations?.[HCI_ANNOTATIONS.VM_RESERVED_MEMORY];
const terminationGracePeriodSeconds = spec.template.spec?.terminationGracePeriodSeconds || this.defaultTerminationSetting;
@@ -409,6 +414,9 @@ export default {
this['cpu'] = cpu;
this['memory'] = memory;
+ this['maxCpu'] = maxCpu;
+ this['maxMemory'] = maxMemory;
+ this['cpuMemoryHotplugEnabled'] = cpuMemoryHotplugEnabled;
this['reservedMemory'] = reservedMemory;
this['machineType'] = machineType;
this['terminationGracePeriodSeconds'] = terminationGracePeriodSeconds;
@@ -633,20 +641,26 @@ export default {
this.spec.template.spec.domain.machine['type'] = this.machineType;
}
- this.spec.template.spec.domain.cpu.cores = this.cpu;
- this.spec.template.spec.domain.resources.limits.cpu = this.cpu ? this.cpu.toString() : this.cpu;
- this.spec.template.spec.domain.resources.limits.memory = this.memory;
+ this.setCPUAndMemory();
+ // update terminationGracePeriodSeconds
this.spec.template.spec.terminationGracePeriodSeconds = this.terminationGracePeriodSeconds;
- // parse reserved memory
const vm = this.resourceType === HCI.VM ? this.value : this.value.spec.vm;
+ // parse reserved memory
if (!this.reservedMemory) {
delete vm.metadata.annotations[HCI_ANNOTATIONS.VM_RESERVED_MEMORY];
} else {
vm.metadata.annotations[HCI_ANNOTATIONS.VM_RESERVED_MEMORY] = this.reservedMemory;
}
+ // add or remove cpu memory hotplug annotation
+ if (this.cpuMemoryHotplugEnabled) {
+ vm.metadata.annotations[HCI_ANNOTATIONS.VM_CPU_MEMORY_HOTPLUG] = this.cpuMemoryHotplugEnabled.toString();
+ } else {
+ delete vm.metadata.annotations[HCI_ANNOTATIONS.VM_CPU_MEMORY_HOTPLUG];
+ }
+
if (this.maintenanceStrategy === 'Migrate') {
delete vm.metadata.labels[HCI_ANNOTATIONS.VM_MAINTENANCE_MODE_STRATEGY];
} else {
@@ -654,6 +668,36 @@ export default {
}
},
+ setCPUAndMemory() {
+ if (this.cpuMemoryHotplugEnabled) {
+ // set CPU
+ this.spec.template.spec.domain.cpu.sockets = this.cpu;
+ this.spec.template.spec.domain.cpu.cores = 1;
+
+ // set max CPU
+ set(this.spec.template.spec, 'domain.cpu.maxSockets', this.maxCpu);
+ // domain.resources.limits.cpu and memory are defined by k8s which requires string values
+ // see https://kubernetes.io/docs/tasks/configure-pod-container/assign-cpu-resource/
+ set(this.spec.template.spec, 'domain.resources.limits.cpu', this.maxCpu?.toString());
+
+ // set memory
+ set(this.spec.template.spec, 'domain.memory.guest', this.memory);
+
+ // set max memory
+ set(this.spec.template.spec, 'domain.memory.maxGuest', this.maxMemory);
+ set(this.spec.template.spec, 'domain.resources.limits.memory', this.maxMemory);
+ } else {
+ this.spec.template.spec.domain.cpu.sockets = 1;
+ this.spec.template.spec.domain.cpu.cores = this.cpu;
+ this.spec.template.spec.domain.resources.limits.cpu = this.cpu?.toString();
+ this.spec.template.spec.domain.resources.limits.memory = this.memory;
+ // clean
+ delete this.spec.template.spec.resources;
+ delete this.spec.template.spec.domain.memory;
+ delete this.spec.template.spec.domain.cpu.maxSockets;
+ }
+ },
+
parseDiskRows(disk) {
const disks = [];
const volumes = [];
@@ -952,9 +996,12 @@ export default {
this['sshKey'] = neu;
},
- updateCpuMemory(cpu, memory) {
+ updateCpuMemory(cpu, memory, maxCpu = '', maxMemory = null, cpuMemoryHotplugEnabled = false) {
this['cpu'] = cpu;
this['memory'] = memory;
+ this['maxCpu'] = maxCpu;
+ this['maxMemory'] = maxMemory;
+ this['cpuMemoryHotplugEnabled'] = cpuMemoryHotplugEnabled;
},
parseDisk(R, index) {
@@ -1311,6 +1358,21 @@ export default {
}
},
+ getCPUMemoryValidation() {
+ const errors = [];
+ const { cpu, memory } = this;
+
+ if ((!cpu)) {
+ errors.push(this.t('validation.required', { key: this.t('harvester.virtualMachine.input.cpu') }, true));
+ }
+
+ if ((!memory)) {
+ errors.push(this.t('validation.required', { key: this.t('harvester.virtualMachine.input.memory') }, true));
+ }
+
+ return errors;
+ },
+
getAccessCredentialsValidation() {
const errors = [];
diff --git a/pkg/harvester/models/kubevirt.io.virtualmachine.js b/pkg/harvester/models/kubevirt.io.virtualmachine.js
index eaf9fb05..2d783ea6 100644
--- a/pkg/harvester/models/kubevirt.io.virtualmachine.js
+++ b/pkg/harvester/models/kubevirt.io.virtualmachine.js
@@ -13,6 +13,7 @@ import { parseVolumeClaimTemplates } from '@pkg/utils/vm';
import { BACKUP_TYPE } from '../config/types';
import { HCI } from '../types';
import HarvesterResource from './harvester';
+import { getVmCPUMemoryValues } from '../utils/cpuMemory';
export const OFF = 'Off';
@@ -169,6 +170,12 @@ export default class VirtVm extends HarvesterResource {
icon: 'icon icon-storage',
label: this.t('harvester.action.editVMQuota')
},
+ {
+ action: 'cpuMemoryHotplug',
+ enabled: !!this.actions?.cpuAndMemoryHotplug,
+ icon: 'icon icon-os-management',
+ label: this.t('harvester.action.cpuAndMemoryHotplug')
+ },
{
action: 'createSchedule',
enabled: this.schedulingVMBackupFeatureEnabled,
@@ -476,6 +483,13 @@ export default class VirtVm extends HarvesterResource {
});
}
+ cpuMemoryHotplug(resources = this) {
+ this.$dispatch('promptModal', {
+ resources,
+ component: 'HarvesterCPUMemoryHotPlugDialog'
+ });
+ }
+
abortMigrationVM() {
this.doActionGrowl('abortMigration', {});
}
@@ -1091,19 +1105,6 @@ export default class VirtVm extends HarvesterResource {
maxLength: 63,
translationKey: 'harvester.fields.name'
},
- {
- nullable: false,
- path: 'spec.template.spec.domain.cpu.cores',
- min: 1,
- required: true,
- translationKey: 'harvester.fields.cpu'
- },
- {
- nullable: false,
- path: 'spec.template.spec.domain.resources.limits.memory',
- required: true,
- translationKey: 'harvester.fields.memory'
- },
{
nullable: false,
path: 'spec.template.spec',
@@ -1127,12 +1128,11 @@ export default class VirtVm extends HarvesterResource {
}
get memorySort() {
- const memory =
- this?.spec?.template?.spec?.domain?.resources?.requests?.memory || 0;
+ const memory = getVmCPUMemoryValues(this).memory;
const formatSize = parseSi(memory);
- return parseInt(formatSize);
+ return parseInt(formatSize, 10);
}
get ingoreVMMessage() {
@@ -1158,14 +1158,22 @@ export default class VirtVm extends HarvesterResource {
}
get stateDescription() {
+ const conditions = get(this, 'status.conditions');
+ const restartRequired = findBy(conditions, 'type', 'RestartRequired');
+
+ if (restartRequired && restartRequired.status === 'True') {
+ return this.t('harvester.virtualMachine.hotplug.restartVMMessage');
+ }
+
return this.ingoreVMMessage ? '' : super.stateDescription;
}
+ get displayCPU() {
+ return getVmCPUMemoryValues(this).cpu;
+ }
+
get displayMemory() {
- return (
- this.spec.template.spec.domain.resources?.limits?.memory ||
- this.spec.template.spec.domain.resources?.requests?.memory
- );
+ return getVmCPUMemoryValues(this).memory;
}
get isQemuInstalled() {
diff --git a/pkg/harvester/utils/cpuMemory.js b/pkg/harvester/utils/cpuMemory.js
new file mode 100644
index 00000000..8ba47a8e
--- /dev/null
+++ b/pkg/harvester/utils/cpuMemory.js
@@ -0,0 +1,36 @@
+import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
+
+export function getVmCPUMemoryValues(vm) {
+ if (!vm) {
+ return {
+ cpu: 0,
+ memory: null,
+ isHotplugEnabled: false
+ };
+ }
+
+ const isHotplugEnabled = isCPUMemoryHotPlugEnabled(vm);
+
+ if (isHotplugEnabled) {
+ return {
+ cpu: vm.spec.template.spec.domain.cpu.sockets,
+ memory: vm.spec.template.spec.domain?.memory?.guest || null,
+ maxCpu: vm.spec.template.spec.domain.cpu?.maxSockets || 0,
+ maxMemory: vm.spec.template.spec.domain?.memory?.maxGuest || null,
+ isHotplugEnabled
+ };
+ } else {
+ return {
+ cpu: vm.spec.template.spec.domain.cpu.cores,
+ memory: vm.spec.template.spec.domain.resources?.limits?.memory || null,
+ isHotplugEnabled
+ };
+ }
+}
+
+export function isCPUMemoryHotPlugEnabled(vm) {
+ return vm?.metadata?.annotations[HCI_ANNOTATIONS.VM_CPU_MEMORY_HOTPLUG] === 'true' ||
+ !!vm?.spec?.template?.spec?.domain.cpu?.maxSockets ||
+ !!vm?.spec?.template?.spec?.domain?.memory?.maxGuest ||
+ false;
+}