From 768fcf6b12c06c2099d84e62d8c49368eb82336d Mon Sep 17 00:00:00 2001 From: Ivan Mikheykin Date: Mon, 13 Apr 2026 17:39:06 +0300 Subject: [PATCH] feat(vm): support live change of cpu fractions - Apply immediate on cores fraction change. Signed-off-by: Ivan Mikheykin --- build/components/versions.yml | 2 +- images/virt-artifact/werf.inc.yaml | 1 + .../pkg/controller/vmchange/comparator_cpu.go | 9 +++-- .../pkg/controller/vmchange/compare_test.go | 20 ++++++++++- .../pkg/controller/vmchange/field_change.go | 36 +++++++++++++++++++ .../internal/handler/hotplug.go | 2 +- 6 files changed, 64 insertions(+), 6 deletions(-) diff --git a/build/components/versions.yml b/build/components/versions.yml index 7db9801217..6d3a784848 100644 --- a/build/components/versions.yml +++ b/build/components/versions.yml @@ -3,7 +3,7 @@ firmware: libvirt: v10.9.0 edk2: stable202411 core: - 3p-kubevirt: v1.6.2-v12n.25 + 3p-kubevirt: dvp/hotplug-cpu-fraction 3p-containerized-data-importer: v1.60.3-v12n.18 distribution: 2.8.3 package: diff --git a/images/virt-artifact/werf.inc.yaml b/images/virt-artifact/werf.inc.yaml index f30560fba6..4b2566a4bd 100644 --- a/images/virt-artifact/werf.inc.yaml +++ b/images/virt-artifact/werf.inc.yaml @@ -15,6 +15,7 @@ secrets: shell: install: - | + echo "rebuild me 4" echo "Git clone {{ $gitRepoName }} repository..." git clone --depth=1 $(cat /run/secrets/SOURCE_REPO)/{{ $gitRepoUrl }} --branch {{ $tag }} /src/kubevirt diff --git a/images/virtualization-artifact/pkg/controller/vmchange/comparator_cpu.go b/images/virtualization-artifact/pkg/controller/vmchange/comparator_cpu.go index 945f14a48e..549cf769d2 100644 --- a/images/virtualization-artifact/pkg/controller/vmchange/comparator_cpu.go +++ b/images/virtualization-artifact/pkg/controller/vmchange/comparator_cpu.go @@ -47,15 +47,18 @@ func (c *comparatorCPU) Compare(current, desired *v1alpha2.VirtualMachineSpec) [ coresChangedAction = ActionRestart } + fractionChangedAction := ActionApplyImmediate + // Require reboot if CPU hotplug is not enabled. if !c.featureGate.Enabled(featuregates.HotplugCPUWithLiveMigration) { coresChangedAction = ActionRestart + fractionChangedAction = ActionRestart } coresChanges := compareInts("cpu.cores", current.CPU.Cores, desired.CPU.Cores, 0, coresChangedAction) - fractionChanges := compareStrings("cpu.coreFraction", current.CPU.CoreFraction, desired.CPU.CoreFraction, DefaultCPUCoreFraction, ActionRestart) + fractionChanges := compareStrings("cpu.coreFraction", current.CPU.CoreFraction, desired.CPU.CoreFraction, DefaultCPUCoreFraction, fractionChangedAction) - // Yield full replace if both fields changed. + // Yield a full replace for cpu section if both fields are changed. if HasChanges(coresChanges) && HasChanges(fractionChanges) { return []FieldChange{ { @@ -63,7 +66,7 @@ func (c *comparatorCPU) Compare(current, desired *v1alpha2.VirtualMachineSpec) [ Path: "cpu", CurrentValue: current.CPU, DesiredValue: desired.CPU, - ActionRequired: ActionRestart, + ActionRequired: MostDisruptiveAction(coresChangedAction, fractionChangedAction), }, } } diff --git a/images/virtualization-artifact/pkg/controller/vmchange/compare_test.go b/images/virtualization-artifact/pkg/controller/vmchange/compare_test.go index e428b4fa0c..7038b85335 100644 --- a/images/virtualization-artifact/pkg/controller/vmchange/compare_test.go +++ b/images/virtualization-artifact/pkg/controller/vmchange/compare_test.go @@ -71,7 +71,7 @@ cpu: ), }, { - "restart on cpu section change", + "restart on cpu section change when hotplug is disabled", ` cpu: cores: 2 @@ -88,6 +88,24 @@ cpu: requirePathOperation("cpu", ChangeReplace), ), }, + { + "immediate apply on cpu section change when hotplug is enabled", + ` +cpu: + cores: 2 + coreFraction: 60% +`, + ` +cpu: + cores: 6 + coreFraction: 40% +`, + []featuregate.Feature{featuregates.HotplugCPUWithLiveMigration}, + assertChanges( + actionRequired(ActionApplyImmediate), + requirePathOperation("cpu", ChangeReplace), + ), + }, { "no restart cpu.coreFraction from empty to default value", ` diff --git a/images/virtualization-artifact/pkg/controller/vmchange/field_change.go b/images/virtualization-artifact/pkg/controller/vmchange/field_change.go index 60fa5602b0..c0781ebcf0 100644 --- a/images/virtualization-artifact/pkg/controller/vmchange/field_change.go +++ b/images/virtualization-artifact/pkg/controller/vmchange/field_change.go @@ -16,6 +16,8 @@ limitations under the License. package vmchange +import "cmp" + type ChangeOperation string const ( @@ -33,6 +35,12 @@ const ( ActionApplyImmediate ActionType = "ApplyImmediate" ) +var actionDisruptionOrder = map[ActionType]int{ + ActionNone: 0, + ActionApplyImmediate: 1, + ActionRestart: 2, +} + type FieldChange struct { Operation ChangeOperation `json:"operation,omitempty"` Path string `json:"path,omitempty"` @@ -50,3 +58,31 @@ func HasChanges(changes []FieldChange) bool { } return false } + +// MostDisruptiveAction returns a most dangerous action from the list. +func MostDisruptiveAction(actions ...ActionType) ActionType { + result := ActionNone + for _, action := range actions { + // Break immediately if 'action' is the most disruptive action. + if action == ActionRestart { + return action + } + if action.Cmp(result) == 1 { + result = action + } + } + return result +} + +// Cmp returns 0 if the action is equal to 'other', -1 if the action is less harmless than 'other', +// or 1 if the action is more disruptive than 'other'. +func (a ActionType) Cmp(other ActionType) int { + aOrder, hasA := actionDisruptionOrder[a] + otherOrder, hasOther := actionDisruptionOrder[other] + if hasA && hasOther { + return cmp.Compare(aOrder, otherOrder) + } + + // Should not reach here, but equal is safe. + return 0 +} diff --git a/images/virtualization-artifact/pkg/controller/workload-updater/internal/handler/hotplug.go b/images/virtualization-artifact/pkg/controller/workload-updater/internal/handler/hotplug.go index 64845b963a..fbab644da4 100644 --- a/images/virtualization-artifact/pkg/controller/workload-updater/internal/handler/hotplug.go +++ b/images/virtualization-artifact/pkg/controller/workload-updater/internal/handler/hotplug.go @@ -82,5 +82,5 @@ func (h *HotplugHandler) Name() string { } func getHotplugResourcesSum(vm *v1alpha2.VirtualMachine) string { - return fmt.Sprintf("cpu.cores=%d,memory.size=%s", vm.Spec.CPU.Cores, vm.Spec.Memory.Size.String()) + return fmt.Sprintf("cpu.cores=%d,cpu.fraction=%s,memory.size=%s", vm.Spec.CPU.Cores, vm.Spec.CPU.CoreFraction, vm.Spec.Memory.Size.String()) }