feat: CPU / Memory hotplug support (#413) (#419)

* feat: add maxCPU and maxMemory



* feat: add hotplug dialog



* feat: add restart message



* feat: let VM template support cpuMemoryHotplug



* feat: add feature flag



* feat: add max-hotplug-ratio setting



---------


(cherry picked from commit be9311dc0c)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
This commit is contained in:
mergify[bot]
2025-07-23 14:11:08 +08:00
committed by GitHub
co-authored by Andy Lee
parent 5d9ba584b8
commit 296601d38c
16 changed files with 564 additions and 92 deletions
+71 -9
View File
@@ -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 = [];