feat: CPU / Memory hotplug support (#413)

* feat: add maxCPU and maxMemory

Signed-off-by: Andy Lee <andy.lee@suse.com>

* feat: add hotplug dialog

Signed-off-by: Andy Lee <andy.lee@suse.com>

* feat: add restart message

Signed-off-by: Andy Lee <andy.lee@suse.com>

* feat: let VM template support cpuMemoryHotplug

Signed-off-by: Andy Lee <andy.lee@suse.com>

* feat: add feature flag

Signed-off-by: Andy Lee <andy.lee@suse.com>

* feat: add max-hotplug-ratio setting

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
This commit is contained in:
Andy Lee 2025-07-23 14:08:59 +08:00 committed by GitHub
parent e294f4c00f
commit be9311dc0c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 564 additions and 92 deletions

View File

@ -44,7 +44,8 @@ const FEATURE_FLAGS = {
'csiOnlineExpandValidation', 'csiOnlineExpandValidation',
'vmNetworkMigration', 'vmNetworkMigration',
'kubeovnVpcSubnet', 'kubeovnVpcSubnet',
'rancherClusterSetting' 'rancherClusterSetting',
'cpuMemoryHotplug'
] ]
}; };

View File

@ -70,5 +70,6 @@ export const HCI = {
K8S_ARCH: 'kubernetes.io/arch', K8S_ARCH: 'kubernetes.io/arch',
IMAGE_DISPLAY_NAME: 'harvesterhci.io/imageDisplayName', IMAGE_DISPLAY_NAME: 'harvesterhci.io/imageDisplayName',
CUSTOM_IP: 'harvesterhci.io/custom-ip', CUSTOM_IP: 'harvesterhci.io/custom-ip',
IMPORTED_IMAGE: 'migration.harvesterhci.io/imported' IMPORTED_IMAGE: 'migration.harvesterhci.io/imported',
VM_CPU_MEMORY_HOTPLUG: 'harvesterhci.io/enableCPUAndMemoryHotplug',
}; };

View File

@ -38,6 +38,7 @@ export const HCI_SETTING = {
UPGRADE_CONFIG: 'upgrade-config', UPGRADE_CONFIG: 'upgrade-config',
VM_MIGRATION_NETWORK: 'vm-migration-network', VM_MIGRATION_NETWORK: 'vm-migration-network',
RANCHER_CLUSTER: 'rancher-cluster', RANCHER_CLUSTER: 'rancher-cluster',
MAX_HOTPLUG_RATIO: 'max-hotplug-ratio'
}; };
export const HCI_ALLOWED_SETTINGS = { export const HCI_ALLOWED_SETTINGS = {
@ -112,6 +113,7 @@ export const HCI_ALLOWED_SETTINGS = {
[HCI_SETTING.RANCHER_CLUSTER]: { [HCI_SETTING.RANCHER_CLUSTER]: {
kind: 'custom', from: 'import', canReset: true, featureFlag: 'rancherClusterSetting' kind: 'custom', from: 'import', canReset: true, featureFlag: 'rancherClusterSetting'
}, },
[HCI_SETTING.MAX_HOTPLUG_RATIO]: { kind: 'number', featureFlag: 'cpuMemoryHotplug' },
}; };
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = { export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {

View File

@ -172,6 +172,9 @@ export default {
:cpu="cpu" :cpu="cpu"
:mode="mode" :mode="mode"
:memory="memory" :memory="memory"
:max-cpu="maxCpu"
:max-memory="maxMemory"
:enable-hot-plug="cpuMemoryHotplugEnabled"
/> />
</div> </div>
<div class="row"> <div class="row">

View File

@ -172,6 +172,9 @@ export default {
:cpu="cpu" :cpu="cpu"
:mode="mode" :mode="mode"
:memory="memory" :memory="memory"
:max-cpu="maxCpu"
:max-memory="maxMemory"
:enable-hot-plug="cpuMemoryHotplugEnabled"
/> />
<div class="row mb-10"> <div class="row mb-10">

View File

@ -5,6 +5,7 @@ import CreateEditView from '@shell/mixins/create-edit-view';
import HarvesterIpAddress from '../../../formatters/HarvesterIpAddress'; import HarvesterIpAddress from '../../../formatters/HarvesterIpAddress';
import VMConsoleBar from '../../../components/VMConsoleBar'; import VMConsoleBar from '../../../components/VMConsoleBar';
import { HCI } from '../../../types'; import { HCI } from '../../../types';
import { getVmCPUMemoryValues } from '../../../utils/cpuMemory';
const UNDEFINED = 'n/a'; const UNDEFINED = 'n/a';
@ -91,9 +92,9 @@ export default {
}, },
flavor() { 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() { kernelRelease() {

View File

@ -0,0 +1,185 @@
<script>
import { exceptionToErrorsArray } from '@shell/utils/error';
import { mapGetters } from 'vuex';
import { getVmCPUMemoryValues } from '../utils/cpuMemory';
import UnitInput from '@shell/components/form/UnitInput';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton';
import { GIBIBYTE } from '../utils/unit';
export default {
name: 'CPUMemoryHotplugModal',
emits: ['close'],
components: {
AsyncButton, Card, Banner, UnitInput
},
props: {
resources: {
type: Array,
required: true
}
},
data() {
const { cpu, memory } = getVmCPUMemoryValues(this.resources[0] || {});
return {
cpu,
memory,
errors: [],
GIBIBYTE
};
},
computed: {
...mapGetters({ t: 'i18n/t' }),
actionResource() {
return this.resources[0] || {};
},
maxResourcesMessage() {
const { maxCpu, maxMemory } = getVmCPUMemoryValues(this.actionResource);
if (maxCpu && maxMemory) {
return this.t('harvester.modal.cpuMemoryHotplug.maxResourcesMessage', { maxCpu, maxMemory });
}
return '';
}
},
methods: {
close() {
this.cpu = '';
this.memory = '';
this.$emit('close');
},
change() {
if (parseInt(this.memory, 10) < 1 ) {
this.memory = '1Gi';
}
if (this.cpu < 1) {
this.cpu = 1;
}
},
async save(buttonCb) {
if (this.actionResource) {
try {
const res = await this.actionResource.doAction('cpuAndMemoryHotplug', { sockets: this.cpu, memory: this.memory });
if (res._status === 200 || res._status === 204) {
this.$store.dispatch('growl/success', {
title: this.t('generic.notification.title.succeed'),
message: this.t('harvester.modal.cpuMemoryHotplug.success', { vm: this.actionResource.nameDisplay })
}, { root: true });
this.close();
buttonCb(true);
} else {
const error = [res?.data] || exceptionToErrorsArray(res);
this['errors'] = error;
buttonCb(false);
}
} catch (err) {
const error = err?.data || err;
const message = exceptionToErrorsArray(error);
this['errors'] = message;
buttonCb(false);
}
}
},
}
};
</script>
<template>
<Card
ref="modal"
name="modal"
:show-highlight-border="false"
>
<template #title>
<h4
v-clean-html="t('harvester.modal.cpuMemoryHotplug.title')"
class="text-default-text"
/>
</template>
<template #body>
<Banner
v-if="maxResourcesMessage"
:label="maxResourcesMessage"
color="info"
/>
<UnitInput
v-model:value="cpu"
:label="t('harvester.virtualMachine.input.cpu')"
:delay="0"
required
suffix="C"
class="mb-20"
:mode="mode"
:min="1"
@update:value="change"
/>
<UnitInput
v-model:value="memory"
:label="t('harvester.virtualMachine.input.memory')"
:mode="mode"
:input-exponent="3"
:delay="0"
:min="1"
:increment="1024"
:output-modifier="true"
:disabled="disabled"
:suffix="GIBIBYTE"
class="mb-20"
@update:value="change"
/>
<Banner
v-for="(err, i) in errors"
:key="i"
:label="err"
color="error"
/>
</template>
<template #actions>
<div class="actions">
<div class="buttons">
<button
type="button"
class="btn role-secondary mr-10"
@click="close"
>
{{ t('generic.cancel') }}
</button>
<AsyncButton
mode="apply"
@click="save"
/>
</div>
</div>
</template>
</Card>
</template>
<style lang="scss" scoped>
.actions {
width: 100%;
}
.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>

View File

@ -259,6 +259,9 @@ export default {
<CpuMemory <CpuMemory
:cpu="cpu" :cpu="cpu"
:memory="memory" :memory="memory"
:max-cpu="maxCpu"
:max-memory="maxMemory"
:enable-hot-plug="cpuMemoryHotplugEnabled"
:disabled="isConfig" :disabled="isConfig"
@updateCpuMemory="updateCpuMemory" @updateCpuMemory="updateCpuMemory"
/> />

View File

@ -2,23 +2,44 @@
import UnitInput from '@shell/components/form/UnitInput'; import UnitInput from '@shell/components/form/UnitInput';
import InputOrDisplay from '@shell/components/InputOrDisplay'; import InputOrDisplay from '@shell/components/InputOrDisplay';
import { GIBIBYTE } from '../../utils/unit'; 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 { export default {
name: 'HarvesterEditCpuMemory', name: 'HarvesterEditCpuMemory',
emits: ['updateCpuMemory'], emits: ['updateCpuMemory'],
components: { UnitInput, InputOrDisplay }, components: {
UnitInput, InputOrDisplay, Checkbox
},
props: { props: {
cpu: { cpu: {
type: Number, type: Number,
default: null default: null
}, },
maxCpu: {
type: Number,
default: null
},
memory: { memory: {
type: String, type: String,
default: null default: null
}, },
maxMemory: {
type: String,
default: null
},
enableHotPlug: {
type: Boolean,
default: false
},
mode: { mode: {
type: String, type: String,
default: 'create', 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() { data() {
return { return {
GIBIBYTE, GIBIBYTE,
localCpu: this.cpu, localCpu: this.cpu,
localMemory: this.memory localMemory: this.memory,
maxLocalCpu: this.maxCpu,
maxLocalMemory: this.maxMemory,
localEnableHotPlug: this.enableHotPlug,
settings: []
}; };
}, },
computed: { computed: {
cupDisplay() { isView() {
return this.mode === _VIEW;
},
cpuDisplay() {
return `${ this.localCpu } C`; return `${ this.localCpu } C`;
}, },
maxCpuDisplay() {
return `${ this.maxLocalCpu } C`;
},
memoryDisplay() { memoryDisplay() {
return `${ this.localMemory }`; 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,35 +114,60 @@ export default {
if (neu && !neu.includes('null')) { if (neu && !neu.includes('null')) {
this.localMemory = neu; this.localMemory = neu;
} }
},
maxCpu(neu) {
this.maxLocalCpu = neu;
},
maxMemory(neu) {
if (neu && !neu.includes('null')) {
this.maxLocalMemory = neu;
} }
}, },
enableHotPlug(neu) {
this.localEnableHotPlug = neu;
},
},
methods: { methods: {
change() { hotPlugChanged(neu) {
let memory = ''; // If hot plug is enabled, we need to update the maxCpu and maxMemory values
if (neu) {
if (String(this.localMemory).includes('Gi')) { this.maxLocalCpu = this.localCpu ? this.localCpu * this.maxHotplugRatio : null;
memory = this.localMemory; 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 { } 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);
},
} }
}; };
</script> </script>
<template> <template>
<div>
<div class="row"> <div class="row">
<div class="col span-6 mb-10"> <div class="col span-6 mb-10">
<InputOrDisplay <InputOrDisplay
name="CPU" name="CPU"
:value="cupDisplay" :value="cpuDisplay"
:mode="mode" :mode="mode"
class="mb-10"
> >
<UnitInput <UnitInput
v-model:value="localCpu" v-model:value="localCpu"
@ -94,11 +178,10 @@ export default {
:disabled="disabled" :disabled="disabled"
:mode="mode" :mode="mode"
class="mb-20" class="mb-20"
@update:value="change" @update:value="changeCPU"
/> />
</InputOrDisplay> </InputOrDisplay>
</div> </div>
<div class="col span-6 mb-10"> <div class="col span-6 mb-10">
<InputOrDisplay <InputOrDisplay
:name="t('harvester.virtualMachine.input.memory')" :name="t('harvester.virtualMachine.input.memory')"
@ -117,9 +200,75 @@ export default {
required required
:suffix="GIBIBYTE" :suffix="GIBIBYTE"
class="mb-20" class="mb-20"
@update:value="change" @update:value="changeMemory"
/> />
</InputOrDisplay> </InputOrDisplay>
</div> </div>
</div> </div>
<div
v-if="isCPUMemoryHotPlugFeatureEnabled"
class="row"
>
<Checkbox
v-model:value="localEnableHotPlug"
class="check"
type="checkbox"
:label="t('harvester.virtualMachine.hotplug.title')"
:disabled="isView"
@update:value="hotPlugChanged"
/>
<i
v-clean-tooltip="{content: cpuMemoryHotplugTooltip, triggers: ['hover', 'touch', 'focus'] }"
v-stripped-aria-label="cpuMemoryHotplugTooltip"
class="icon icon-info"
tabindex="0"
/>
</div>
<div
v-if="localEnableHotPlug && isCPUMemoryHotPlugFeatureEnabled"
class="row"
>
<div class="col span-6 mb-10">
<InputOrDisplay
:name="t('harvester.virtualMachine.input.maxCpu')"
:value="maxCpuDisplay"
:mode="mode"
class="mt-20"
>
<UnitInput
v-model:value="maxLocalCpu"
:label="t('harvester.virtualMachine.input.maxCpu')"
suffix="C"
:delay="0"
:disabled="disabled"
:mode="mode"
class="mt-20"
@update:value="changeMaxCPUMemory"
/>
</InputOrDisplay>
</div>
<div class="col span-6 mb-10">
<InputOrDisplay
:name="t('harvester.virtualMachine.input.maxMemory')"
:value="maxMemoryDisplay"
:mode="mode"
class="mt-20"
>
<UnitInput
v-model:value="maxLocalMemory"
:label="t('harvester.virtualMachine.input.maxMemory')"
:mode="mode"
:input-exponent="3"
:delay="0"
:increment="1024"
:output-modifier="true"
:disabled="disabled"
:suffix="GIBIBYTE"
class="mt-20"
@update:value="changeMaxCPUMemory"
/>
</InputOrDisplay>
</div>
</div>
</div>
</template> </template>

View File

@ -240,7 +240,7 @@ export default {
</script> </script>
<template> <template>
<div> <div class="mt-20">
<LabeledSelect <LabeledSelect
v-model:value="checkedSsh" v-model:value="checkedSsh"
:label="t('harvester.virtualMachine.input.sshKey')" :label="t('harvester.virtualMachine.input.sshKey')"

View File

@ -466,7 +466,9 @@ export default {
} }
} }
const errors = this.getAccessCredentialsValidation(); const cpuMemoryErrors = this.getCPUMemoryValidation();
const accessCredentialsErrors = this.getAccessCredentialsValidation();
const errors = [...cpuMemoryErrors, ...accessCredentialsErrors];
if (errors.length > 0) { if (errors.length > 0) {
return Promise.reject(errors); return Promise.reject(errors);
@ -604,8 +606,11 @@ export default {
> >
<CpuMemory <CpuMemory
:cpu="cpu" :cpu="cpu"
:max-cpu="maxCpu"
:memory="memory" :memory="memory"
:max-memory="maxMemory"
:mode="mode" :mode="mode"
:enable-hot-plug="cpuMemoryHotplugEnabled"
@updateCpuMemory="updateCpuMemory" @updateCpuMemory="updateCpuMemory"
/> />

View File

@ -157,6 +157,10 @@ harvester:
hotplug: hotplug:
success: 'Volume { diskName } is mounted to the virtual machine { vm }.' success: 'Volume { diskName } is mounted to the virtual machine { vm }.'
title: Add Volume 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: hotunplug:
success: 'Volume { name } is detached successfully.' success: 'Volume { name } is detached successfully.'
snapshot: snapshot:
@ -213,6 +217,7 @@ harvester:
suspendSchedule: Suspend suspendSchedule: Suspend
restoreExistingVM: Replace Existing restoreExistingVM: Replace Existing
migrate: Migrate migrate: Migrate
cpuAndMemoryHotplug: Edit CPU and Memory
abortMigration: Abort Migration abortMigration: Abort Migration
createTemplate: Generate Template createTemplate: Generate Template
enableMaintenance: Enable Maintenance Mode enableMaintenance: Enable Maintenance Mode
@ -571,6 +576,10 @@ harvester:
virtualMachine: virtualMachine:
label: Virtual Machines label: Virtual Machines
osType: OS Type 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: instance:
singleInstance: singleInstance:
multipleInstance: multipleInstance:
@ -716,6 +725,9 @@ harvester:
invalidUser: Invalid Username. invalidUser: Invalid Username.
input: input:
name: Name name: Name
cpu: CPU
maxCpu: Maximum CPU
maxMemory: Maximum Memory
memory: Memory memory: Memory
image: Image image: Image
sshKey: SSHKey sshKey: SSHKey
@ -1712,6 +1724,7 @@ advancedSettings:
'harv-upgrade-config': 'Configure image preloading and VM restore options for upgrades. See related fields in <a href="{url}" target="_blank" rel="noopener">settings/upgrade-config</a>' 'harv-upgrade-config': 'Configure image preloading and VM restore options for upgrades. See related fields in <a href="{url}" target="_blank" rel="noopener">settings/upgrade-config</a>'
'harv-vm-migration-network': 'Segregated network for VM migration traffic.' 'harv-vm-migration-network': 'Segregated network for VM migration traffic.'
'harv-rancher-cluster': 'Configure Rancher cluster integration settings for guest cluster management.' '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: typeLabel:
kubevirt.io.virtualmachine: |- kubevirt.io.virtualmachine: |-

View File

@ -22,8 +22,8 @@ export const VM_HEADERS = [
{ {
name: 'CPU', name: 'CPU',
label: 'CPU', label: 'CPU',
sort: ['spec.template.spec.domain.cpu.cores'], sort: ['displayCPU'],
value: 'spec.template.spec.domain.cpu.cores', value: 'displayCPU',
align: 'center', align: 'center',
dashIfEmpty: true, dashIfEmpty: true,
}, },

View File

@ -3,10 +3,9 @@ import jsyaml from 'js-yaml';
import isEqual from 'lodash/isEqual'; import isEqual from 'lodash/isEqual';
import isEmpty from 'lodash/isEmpty'; import isEmpty from 'lodash/isEmpty';
import difference from 'lodash/difference'; import difference from 'lodash/difference';
import { sortBy } from '@shell/utils/sort'; import { sortBy } from '@shell/utils/sort';
import { set } from '@shell/utils/object'; import { set } from '@shell/utils/object';
import { getVmCPUMemoryValues } from '../../utils/cpuMemory';
import { allHash } from '@shell/utils/promise'; import { allHash } from '@shell/utils/promise';
import { randomStr } from '@shell/utils/string'; import { randomStr } from '@shell/utils/string';
import { base64Decode } from '@shell/utils/crypto'; import { base64Decode } from '@shell/utils/crypto';
@ -164,6 +163,9 @@ export default {
deleteAgent: true, deleteAgent: true,
memory: null, memory: null,
cpu: '', cpu: '',
maxMemory: null,
maxCpu: '',
cpuMemoryHotplugEnabled: false,
reservedMemory: null, reservedMemory: null,
accessCredentials: [], accessCredentials: [],
efiEnabled: false, efiEnabled: false,
@ -348,8 +350,11 @@ export default {
const runStrategy = spec.runStrategy || 'RerunOnFailure'; const runStrategy = spec.runStrategy || 'RerunOnFailure';
const machineType = spec.template.spec.domain?.machine?.type || this.machineTypes[0]; const machineType = spec.template.spec.domain?.machine?.type || this.machineTypes[0];
const cpu = spec.template.spec.domain?.cpu?.cores; const {
const memory = spec.template.spec.domain.resources.limits.memory; cpu, memory, maxCpu, maxMemory, isHotplugEnabled
} = getVmCPUMemoryValues(vm);
const cpuMemoryHotplugEnabled = isHotplugEnabled;
const reservedMemory = vm.metadata?.annotations?.[HCI_ANNOTATIONS.VM_RESERVED_MEMORY]; const reservedMemory = vm.metadata?.annotations?.[HCI_ANNOTATIONS.VM_RESERVED_MEMORY];
const terminationGracePeriodSeconds = spec.template.spec?.terminationGracePeriodSeconds || this.defaultTerminationSetting; const terminationGracePeriodSeconds = spec.template.spec?.terminationGracePeriodSeconds || this.defaultTerminationSetting;
@ -409,6 +414,9 @@ export default {
this['cpu'] = cpu; this['cpu'] = cpu;
this['memory'] = memory; this['memory'] = memory;
this['maxCpu'] = maxCpu;
this['maxMemory'] = maxMemory;
this['cpuMemoryHotplugEnabled'] = cpuMemoryHotplugEnabled;
this['reservedMemory'] = reservedMemory; this['reservedMemory'] = reservedMemory;
this['machineType'] = machineType; this['machineType'] = machineType;
this['terminationGracePeriodSeconds'] = terminationGracePeriodSeconds; this['terminationGracePeriodSeconds'] = terminationGracePeriodSeconds;
@ -633,20 +641,26 @@ export default {
this.spec.template.spec.domain.machine['type'] = this.machineType; this.spec.template.spec.domain.machine['type'] = this.machineType;
} }
this.spec.template.spec.domain.cpu.cores = this.cpu; this.setCPUAndMemory();
this.spec.template.spec.domain.resources.limits.cpu = this.cpu ? this.cpu.toString() : this.cpu; // update terminationGracePeriodSeconds
this.spec.template.spec.domain.resources.limits.memory = this.memory;
this.spec.template.spec.terminationGracePeriodSeconds = this.terminationGracePeriodSeconds; this.spec.template.spec.terminationGracePeriodSeconds = this.terminationGracePeriodSeconds;
// parse reserved memory
const vm = this.resourceType === HCI.VM ? this.value : this.value.spec.vm; const vm = this.resourceType === HCI.VM ? this.value : this.value.spec.vm;
// parse reserved memory
if (!this.reservedMemory) { if (!this.reservedMemory) {
delete vm.metadata.annotations[HCI_ANNOTATIONS.VM_RESERVED_MEMORY]; delete vm.metadata.annotations[HCI_ANNOTATIONS.VM_RESERVED_MEMORY];
} else { } else {
vm.metadata.annotations[HCI_ANNOTATIONS.VM_RESERVED_MEMORY] = this.reservedMemory; 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') { if (this.maintenanceStrategy === 'Migrate') {
delete vm.metadata.labels[HCI_ANNOTATIONS.VM_MAINTENANCE_MODE_STRATEGY]; delete vm.metadata.labels[HCI_ANNOTATIONS.VM_MAINTENANCE_MODE_STRATEGY];
} else { } 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) { parseDiskRows(disk) {
const disks = []; const disks = [];
const volumes = []; const volumes = [];
@ -952,9 +996,12 @@ export default {
this['sshKey'] = neu; this['sshKey'] = neu;
}, },
updateCpuMemory(cpu, memory) { updateCpuMemory(cpu, memory, maxCpu = '', maxMemory = null, cpuMemoryHotplugEnabled = false) {
this['cpu'] = cpu; this['cpu'] = cpu;
this['memory'] = memory; this['memory'] = memory;
this['maxCpu'] = maxCpu;
this['maxMemory'] = maxMemory;
this['cpuMemoryHotplugEnabled'] = cpuMemoryHotplugEnabled;
}, },
parseDisk(R, index) { 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() { getAccessCredentialsValidation() {
const errors = []; const errors = [];

View File

@ -13,6 +13,7 @@ import { parseVolumeClaimTemplates } from '@pkg/utils/vm';
import { BACKUP_TYPE } from '../config/types'; import { BACKUP_TYPE } from '../config/types';
import { HCI } from '../types'; import { HCI } from '../types';
import HarvesterResource from './harvester'; import HarvesterResource from './harvester';
import { getVmCPUMemoryValues } from '../utils/cpuMemory';
export const OFF = 'Off'; export const OFF = 'Off';
@ -169,6 +170,12 @@ export default class VirtVm extends HarvesterResource {
icon: 'icon icon-storage', icon: 'icon icon-storage',
label: this.t('harvester.action.editVMQuota') 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', action: 'createSchedule',
enabled: this.schedulingVMBackupFeatureEnabled, enabled: this.schedulingVMBackupFeatureEnabled,
@ -476,6 +483,13 @@ export default class VirtVm extends HarvesterResource {
}); });
} }
cpuMemoryHotplug(resources = this) {
this.$dispatch('promptModal', {
resources,
component: 'HarvesterCPUMemoryHotPlugDialog'
});
}
abortMigrationVM() { abortMigrationVM() {
this.doActionGrowl('abortMigration', {}); this.doActionGrowl('abortMigration', {});
} }
@ -1091,19 +1105,6 @@ export default class VirtVm extends HarvesterResource {
maxLength: 63, maxLength: 63,
translationKey: 'harvester.fields.name' 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, nullable: false,
path: 'spec.template.spec', path: 'spec.template.spec',
@ -1127,12 +1128,11 @@ export default class VirtVm extends HarvesterResource {
} }
get memorySort() { get memorySort() {
const memory = const memory = getVmCPUMemoryValues(this).memory;
this?.spec?.template?.spec?.domain?.resources?.requests?.memory || 0;
const formatSize = parseSi(memory); const formatSize = parseSi(memory);
return parseInt(formatSize); return parseInt(formatSize, 10);
} }
get ingoreVMMessage() { get ingoreVMMessage() {
@ -1158,14 +1158,22 @@ export default class VirtVm extends HarvesterResource {
} }
get stateDescription() { 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; return this.ingoreVMMessage ? '' : super.stateDescription;
} }
get displayCPU() {
return getVmCPUMemoryValues(this).cpu;
}
get displayMemory() { get displayMemory() {
return ( return getVmCPUMemoryValues(this).memory;
this.spec.template.spec.domain.resources?.limits?.memory ||
this.spec.template.spec.domain.resources?.requests?.memory
);
} }
get isQemuInstalled() { get isQemuInstalled() {

View File

@ -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;
}