Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build/components/versions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ firmware:
libvirt: v10.9.0
edk2: stable202411
core:
3p-kubevirt: v1.6.2-v12n.16
3p-kubevirt: dvp/set-memory-limits-while-hotplugging
3p-containerized-data-importer: v1.60.3-v12n.16
distribution: 2.8.3
package:
Expand Down
1 change: 1 addition & 0 deletions images/virt-artifact/werf.inc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ secrets:
shell:
install:
- |
echo rebuild 1
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will be removed after merging PR in 3p-kubevirt

echo "Git clone {{ $gitRepoName }} repository..."
git clone --depth=1 $(cat /run/secrets/SOURCE_REPO)/{{ $gitRepoUrl }} --branch {{ $tag }} /src/kubevirt

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,10 @@ const (
AnnVMRestartRequested = AnnAPIGroupV + "/vm-restart-requested"

// AnnVMOPWorkloadUpdate is an annotation on vmop that represents a vmop created by workload-updater controller.
AnnVMOPWorkloadUpdate = AnnAPIGroupV + "/workload-update"
AnnVMOPWorkloadUpdateImage = AnnAPIGroupV + "/workload-update-image"
AnnVMOPWorkloadUpdateNodePlacementSum = AnnAPIGroupV + "/workload-update-node-placement-sum"
AnnVMOPWorkloadUpdate = AnnAPIGroupV + "/workload-update"
AnnVMOPWorkloadUpdateImage = AnnAPIGroupV + "/workload-update-image"
AnnVMOPWorkloadUpdateNodePlacementSum = AnnAPIGroupV + "/workload-update-node-placement-sum"
AnnVMOPWorkloadUpdateHotplugResourcesSum = AnnAPIGroupV + "/workload-update-hotplug-resources-sum"
// AnnVMRestore is an annotation on a resource that indicates it was created by the vmrestore controller; the value is the UID of the `VirtualMachineRestore` resource.
AnnVMRestore = AnnAPIGroupV + "/vmrestore"
// AnnVMOPEvacuation is an annotation on vmop that represents a vmop created by evacuation controller
Expand Down
73 changes: 67 additions & 6 deletions images/virtualization-artifact/pkg/controller/kvbuilder/kvvm.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
"github.com/deckhouse/virtualization-controller/pkg/common/array"
"github.com/deckhouse/virtualization-controller/pkg/common/resource_builder"
"github.com/deckhouse/virtualization-controller/pkg/common/vm"
"github.com/deckhouse/virtualization-controller/pkg/featuregates"
"github.com/deckhouse/virtualization/api/core/v1alpha2"
)

Expand All @@ -46,6 +47,9 @@ const (

// GenericCPUModel specifies the base CPU model for Features and Discovery CPU model types.
GenericCPUModel = "qemu64"

MaxMemorySizeForHotplug = 256 * 1024 * 1024 * 1024 // 256 Gi (safely limit to not overlap somewhat conservative 38 bit physical address space)
EnableMemoryHotplugThreshold = 1 * 1024 * 1024 * 1024 // 1 Gi (no hotplug for VMs with less than 1Gi)
)

type KVVMOptions struct {
Expand Down Expand Up @@ -269,16 +273,73 @@ func (b *KVVM) SetCPU(cores int, coreFraction string) error {
return nil
}

// SetMemory sets memory in kvvm.
// There are 2 possibilities to set memory:
// 1. Use domain.memory.guest field: it enabled memory hotplugging, but not set resources.limits.
// 2. Explicitly set limits and requests in domain.resources. No hotplugging in this scenario.
//
// (1) is a new approach, and (2) should be respected for Running VMs started by previous version of the controller.
func (b *KVVM) SetMemory(memorySize resource.Quantity) {
// TODO delete this in the future (around version 1.12).
if b.ResourceExists && isVMRunningWithMemoryResources(b.Resource) {
// Keep resources as-is to not trigger a reboot.
res := &b.Resource.Spec.Template.Spec.Domain.Resources
if res.Requests == nil {
res.Requests = make(map[corev1.ResourceName]resource.Quantity)
}
if res.Limits == nil {
res.Limits = make(map[corev1.ResourceName]resource.Quantity)
}
res.Requests[corev1.ResourceMemory] = memorySize
res.Limits[corev1.ResourceMemory] = memorySize
return
}

domain := &b.Resource.Spec.Template.Spec.Domain

currentMaxGuest := int64(-1)
if domain.Memory != nil && domain.Memory.MaxGuest != nil {
currentMaxGuest = domain.Memory.MaxGuest.Value()
}

domain.Memory = &virtv1.Memory{
Guest: &memorySize,
}

// Set maxMemory to enable hotplug for mem size >= 1Gi.
hotplugThreshold := resource.NewQuantity(EnableMemoryHotplugThreshold, resource.BinarySI)
if featuregates.Default().Enabled(featuregates.HotplugMemory) {
if memorySize.Cmp(*hotplugThreshold) >= 0 {
maxMemory := resource.NewQuantity(MaxMemorySizeForHotplug, resource.BinarySI)
domain.Memory.MaxGuest = maxMemory
}
}
// Set maxGuest to 0 if hotplug is disabled now (mem size < 1Gi) and maxGuest was previously set.
// Zero value is just a flag to patch memory and remove maxGuest before updating kvvm.
if memorySize.Cmp(*hotplugThreshold) == -1 && currentMaxGuest > 0 {
domain.Memory.MaxGuest = resource.NewQuantity(0, resource.BinarySI)
}

// Remove memory limits and requests if set by previous versions.
res := &b.Resource.Spec.Template.Spec.Domain.Resources
if res.Requests == nil {
res.Requests = make(map[corev1.ResourceName]resource.Quantity)
delete(res.Requests, corev1.ResourceMemory)
delete(res.Limits, corev1.ResourceMemory)
}

func isVMRunningWithMemoryResources(kvvm *virtv1.VirtualMachine) bool {
if kvvm == nil {
return false
}
if res.Limits == nil {
res.Limits = make(map[corev1.ResourceName]resource.Quantity)

if kvvm.Status.PrintableStatus != virtv1.VirtualMachineStatusRunning {
return false
}
res.Requests[corev1.ResourceMemory] = memorySize
res.Limits[corev1.ResourceMemory] = memorySize

res := kvvm.Spec.Template.Spec.Domain.Resources
_, hasMemoryRequests := res.Requests[corev1.ResourceMemory]
_, hasMemoryLimits := res.Limits[corev1.ResourceMemory]

return hasMemoryRequests && hasMemoryLimits
}

func GetCPURequest(cores int, coreFraction string) (*resource.Quantity, error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package reconciler
import (
"context"
"errors"
"fmt"
"reflect"
"strings"
"time"
Expand Down Expand Up @@ -102,7 +103,8 @@ handlersLoop:
switch {
case err == nil: // OK.
case errors.Is(err, ErrStopHandlerChain):
log.Debug("Handler chain execution stopped")
msg := fmt.Sprintf("Handler %s stopped chain execution", name)
log.Info(msg)
result = MergeResults(result, res)
break handlersLoop
case k8serrors.IsConflict(err):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,22 @@ import (
"context"
"errors"
"fmt"
"reflect"
"time"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
virtv1 "kubevirt.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"

"github.com/deckhouse/virtualization-controller/pkg/common/annotations"
"github.com/deckhouse/virtualization-controller/pkg/common/network"
"github.com/deckhouse/virtualization-controller/pkg/common/object"
"github.com/deckhouse/virtualization-controller/pkg/common/patch"
vmutil "github.com/deckhouse/virtualization-controller/pkg/common/vm"
"github.com/deckhouse/virtualization-controller/pkg/controller/conditions"
"github.com/deckhouse/virtualization-controller/pkg/controller/kvbuilder"
Expand Down Expand Up @@ -298,24 +301,14 @@ func (h *SyncKvvmHandler) syncKVVM(ctx context.Context, s state.VirtualMachineSt
}
return true, nil
case h.isVMStopped(s.VirtualMachine().Current(), kvvm, pod):
// KVVM must be updated when the VM is stopped because all its components,
// like VirtualDisk and other resources,
// can be changed during the restoration process.
// KVVM should be updated when VM become stopped.
// It is safe to update KVVM at this point in general and also all related resources
// can be changed during the restoration process: e.g. VirtualDisks, VMIPs, etc.
// For example, the PVC of the VirtualDisk will be changed,
// and the volume with this PVC must be updated in the KVVM specification.
hasVMChanges, err := h.detectVMSpecChanges(ctx, s)
if err != nil {
return false, fmt.Errorf("detect changes on the stopped internal virtual machine: %w", err)
}
hasVMClassChanges, err := h.detectVMClassSpecChanges(ctx, s)
err := h.updateKVVM(ctx, s)
if err != nil {
return false, fmt.Errorf("detect changes on the stopped internal virtual machine: %w", err)
}
if hasVMChanges || hasVMClassChanges {
err := h.updateKVVM(ctx, s)
if err != nil {
return false, fmt.Errorf("update stopped internal virtual machine: %w", err)
}
return false, fmt.Errorf("update internal virtual machine in 'Stopped' state: %w", err)
}
return true, nil
case h.hasNoneDisruptiveChanges(s.VirtualMachine().Current(), kvvm, kvvmi, allChanges):
Expand Down Expand Up @@ -370,17 +363,48 @@ func (h *SyncKvvmHandler) updateKVVM(ctx context.Context, s state.VirtualMachine
return fmt.Errorf("the virtual machine is empty, please report a bug")
}

kvvm, err := MakeKVVMFromVMSpec(ctx, s)
newKVVM, err := MakeKVVMFromVMSpec(ctx, s)
if err != nil {
return fmt.Errorf("failed to prepare the internal virtual machine: %w", err)
return fmt.Errorf("update internal virtual machine: make kvvm from the virtual machine spec: %w", err)
}

if err = h.client.Update(ctx, kvvm); err != nil {
return fmt.Errorf("failed to create the internal virtual machine: %w", err)
}
// Check for changes to skip unneeded updated.
isChanged, err := IsKVVMChanged(ctx, s, newKVVM)
if err != nil {
return fmt.Errorf("update internal virtual machine: detect changes: %w", err)
}

if isChanged {
memory := newKVVM.Spec.Template.Spec.Domain.Memory
if memory != nil && memory.MaxGuest != nil && memory.MaxGuest.IsZero() {
// Zero maxGuest is a special value to patch KVVM to unset maxGuest.
// Set it to nil for next update call.
memory.MaxGuest = nil

// 2 operations: remove memory.maxGuest; set memory.guest.
// Remove is not enough, remove and set are needed both to pass the kubevirt vm-validator webhook.
patchBytes, err := patch.NewJSONPatch(
patch.WithRemove("/spec/template/spec/domain/memory/maxGuest"),
patch.WithReplace("/spec/template/spec/domain/memory/guest", memory.Guest.String()),
).Bytes()
if err != nil {
return fmt.Errorf("prepare json patch to unset memory.maxGuest: %w", err)
}

if err = h.client.Patch(ctx, newKVVM, client.RawPatch(types.JSONPatchType, patchBytes)); err != nil {
return fmt.Errorf("patch internal virtual machine to unset memory.maxGuest: %w", err)
}
}

log.Info("Update KubeVirt VM done", "name", kvvm.Name)
log.Debug("Update KubeVirt VM done", "name", kvvm.Name, "kvvm", kvvm)
if err = h.client.Update(ctx, newKVVM); err != nil {
return fmt.Errorf("update internal virtual machine: %w", err)
}

log.Info("Update internal virtual machine done", "name", newKVVM.Name)
log.Debug("Update internal virtual machine done", "name", newKVVM.Name, "kvvm", newKVVM)
} else {
log.Debug("Update internal virtual machine is not needed", "name", newKVVM.Name, "kvvm", newKVVM)
}

return nil
}
Expand All @@ -407,7 +431,7 @@ func MakeKVVMFromVMSpec(ctx context.Context, s state.VirtualMachineState) (*virt
bdState := NewBlockDeviceState(s)
err = bdState.Reload(ctx)
if err != nil {
return nil, fmt.Errorf("failed to relaod blockdevice state for the virtual machine: %w", err)
return nil, fmt.Errorf("failed to reload blockdevice state for the virtual machine: %w", err)
}
class, err := s.Class(ctx)
if err != nil {
Expand Down Expand Up @@ -454,6 +478,25 @@ func MakeKVVMFromVMSpec(ctx context.Context, s state.VirtualMachineState) (*virt
return newKVVM, nil
}

// IsKVVMChanged returns whether kvvm spec or special annotations are changed.
func IsKVVMChanged(ctx context.Context, s state.VirtualMachineState, kvvm *virtv1.VirtualMachine) (bool, error) {
currentKVVM, err := s.KVVM(ctx)
if err != nil {
return false, fmt.Errorf("get current kvvm: %w", err)
}

isChanged := currentKVVM.Annotations[annotations.AnnVMLastAppliedSpec] != kvvm.Annotations[annotations.AnnVMLastAppliedSpec]

if !isChanged {
isChanged = currentKVVM.Annotations[annotations.AnnVMClassLastAppliedSpec] != kvvm.Annotations[annotations.AnnVMClassLastAppliedSpec]
}

if !isChanged {
isChanged = !reflect.DeepEqual(kvvm.Spec, currentKVVM.Spec)
}
return isChanged, nil
}

func (h *SyncKvvmHandler) loadLastAppliedSpec(vm *v1alpha2.VirtualMachine, kvvm *virtv1.VirtualMachine) *v1alpha2.VirtualMachineSpec {
if kvvm == nil || vm == nil {
return nil
Expand Down Expand Up @@ -564,36 +607,6 @@ func (h *SyncKvvmHandler) isVMStopped(
return isVMStopped(kvvm) && (!isKVVMICreated(kvvm) || podStopped)
}

// detectVMSpecChanges returns true and no error if specification has changes.
func (h *SyncKvvmHandler) detectVMSpecChanges(ctx context.Context, s state.VirtualMachineState) (bool, error) {
currentKvvm, err := s.KVVM(ctx)
if err != nil {
return false, err
}

newKvvm, err := MakeKVVMFromVMSpec(ctx, s)
if err != nil {
return false, err
}

return currentKvvm.Annotations[annotations.AnnVMLastAppliedSpec] != newKvvm.Annotations[annotations.AnnVMLastAppliedSpec], nil
}

// detectVMClassSpecChanges returns true and no error if specification has changes.
func (h *SyncKvvmHandler) detectVMClassSpecChanges(ctx context.Context, s state.VirtualMachineState) (bool, error) {
currentKvvm, err := s.KVVM(ctx)
if err != nil {
return false, err
}

newKvvm, err := MakeKVVMFromVMSpec(ctx, s)
if err != nil {
return false, err
}

return currentKvvm.Annotations[annotations.AnnVMClassLastAppliedSpec] != newKvvm.Annotations[annotations.AnnVMClassLastAppliedSpec], nil
}

// canApplyChanges returns true if changes can be applied right now.
//
// Wait if changes are disruptive, and approval mode is manual, and VM is still running.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package vmchange
import (
"k8s.io/apimachinery/pkg/api/resource"

"github.com/deckhouse/virtualization-controller/pkg/controller/kvbuilder"
"github.com/deckhouse/virtualization/api/core/v1alpha2"
)

Expand Down Expand Up @@ -153,9 +154,23 @@ func compareCPU(current, desired *v1alpha2.VirtualMachineSpec) []FieldChange {
return nil
}

// compareMemory returns changes in the memory section.
// compareMemory is a special comparator to detect changes in memory.
// It is aware of hotplug mechanism. If hotplug is disabled it requires
// restart if memory.size is changed. If hotplug is enabled, it allows
// changing "on the fly". Also, it requires restart if hotplug boundary
// is crossed.
// Note: memory hotplug is enabled if VM has more than 1Gi of RAM.
func compareMemory(current, desired *v1alpha2.VirtualMachineSpec) []FieldChange {
return compareQuantity("memory.size", current.Memory.Size, desired.Memory.Size, resource.Quantity{}, ActionRestart)
hotplugThreshold := resource.NewQuantity(kvbuilder.EnableMemoryHotplugThreshold, resource.BinarySI)
isHotpluggable := current.Memory.Size.Cmp(*hotplugThreshold) > 0
isHotpluggableDesired := desired.Memory.Size.Cmp(*hotplugThreshold) > 0

actionType := ActionRestart
if isHotpluggable && isHotpluggableDesired {
actionType = ActionApplyImmediate
}

return compareQuantity("memory.size", current.Memory.Size, desired.Memory.Size, resource.Quantity{}, actionType)
}

func compareProvisioning(current, desired *v1alpha2.VirtualMachineSpec) []FieldChange {
Expand Down
Loading
Loading