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
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,14 @@ const (
// AnnVMRestartRequested is an annotation on KVVM that represents a request to restart a virtual machine.
AnnVMRestartRequested = AnnAPIGroupV + "/vm-restart-requested"

// AnnVMRestorePowerState is an annotation on VirtualMachine that records the VM power state captured before a
// restore operation, so restore restores it. It is kept on the VM (not the KVVM, which is deleted during the
// restore maintenance window) so it survives KVVM recreation. The value is a MachinePhase string:
// "Running" means start the VM again after restore (see checkNeedStartVM); "Stopped" means keep it stopped,
// which for AlwaysOnUnlessStoppedManually requires overriding the implicit RunStrategy=Always on KVVM create
// (see createKVVM) to honor the "unless stopped manually" contract.
AnnVMRestorePowerState = AnnAPIGroupV + "/restore-power-state"

// AnnVMOPWorkloadUpdate is an annotation on vmop that represents a vmop created by workload-updater controller.
AnnVMOPWorkloadUpdate = AnnAPIGroupV + "/workload-update"
AnnVMOPWorkloadUpdateImage = AnnAPIGroupV + "/workload-update-image"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,10 +193,18 @@ func (v *VirtualMachineHandler) ProcessRestore(ctx context.Context) error {
vm.Annotations = make(map[string]string)
}

// EnterMaintenance records the pre-restore power state on the live VM so it survives the KVVM deletion.
// The snapshot template does not carry it, so preserve it across the full annotation overwrite below;
// otherwise the VM would be left in the wrong power state after restore.
powerState, hasPowerState := vm.Annotations[annotations.AnnVMRestorePowerState]

vm.Spec = v.vm.Spec
vm.Labels = v.vm.Labels
vm.Annotations = v.vm.Annotations
vm.Annotations[annotations.AnnVMOPRestore] = v.restoreUID
if hasPowerState {
vm.Annotations[annotations.AnnVMRestorePowerState] = powerState
}

updErr := v.client.Update(ctx, vm)
if updErr != nil {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,38 @@ var _ = Describe("VirtualMachineRestorer", func() {
}),
)

DescribeTable("preserves the pre-restore power state across the annotation overwrite",
func(powerState string) {
liveVM := vm.DeepCopy()
liveVM.Annotations = map[string]string{
annotations.AnnVMRestorePowerState: powerState,
}
// Different spec so ProcessRestore takes the update path (full annotation overwrite).
liveVM.Spec.RunPolicy = v1alpha2.ManualPolicy

var updatedVM *v1alpha2.VirtualMachine
ic := interceptor.Funcs{
Update: func(_ context.Context, c client.WithWatch, obj client.Object, opts ...client.UpdateOption) error {
if got, ok := obj.(*v1alpha2.VirtualMachine); ok {
updatedVM = got.DeepCopy()
}
return c.Update(ctx, obj, opts...)
},
}
fc, err := testutil.NewFakeClientWithInterceptorWithObjects(ic, liveVM)
Expect(err).ToNot(HaveOccurred())

handler := NewVirtualMachineHandler(fc, vm, restoreUID, v1alpha2.SnapshotOperationModeStrict)
Expect(handler.ProcessRestore(ctx)).To(Succeed())

Expect(updatedVM).ToNot(BeNil())
Expect(updatedVM.Annotations).To(HaveKeyWithValue(annotations.AnnVMRestorePowerState, powerState))
Expect(updatedVM.Annotations).To(HaveKeyWithValue(annotations.AnnVMOPRestore, restoreUID))
},
Entry("running", string(v1alpha2.MachineRunning)),
Entry("stopped", string(v1alpha2.MachineStopped)),
)

Describe("Override", func() {
var rules []v1alpha2.NameReplacement

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,23 @@ func (h *SyncKvvmHandler) createKVVM(ctx context.Context, s state.VirtualMachine
return fmt.Errorf("failed to make the internal virtual machine: %w", err)
}

// Restore the pre-restore power state captured by EnterMaintenance onto the freshly (re)created KVVM.
// "Running" is turned into the regular start-request annotation so the existing power-state machinery
// starts the VM and retries on a failed first boot. "Stopped" must override the implicit RunStrategy=Always
// that AlwaysOnUnlessStoppedManually gets on create, to honor the "unless stopped manually" contract.
changed := s.VirtualMachine().Changed()
switch changed.GetAnnotations()[annotations.AnnVMRestorePowerState] {
case string(v1alpha2.MachineRunning):
annotations.AddAnnotation(kvvm, annotations.AnnVMStartRequested, "true")
delete(changed.Annotations, annotations.AnnVMRestorePowerState)
case string(v1alpha2.MachineStopped):
if changed.Spec.RunPolicy == v1alpha2.AlwaysOnUnlessStoppedManually {
runStrategy := virtv1.RunStrategyManual
kvvm.Spec.RunStrategy = &runStrategy
}
delete(changed.Annotations, annotations.AnnVMRestorePowerState)
}

err = h.client.Create(ctx, kvvm)
if err != nil {
if k8serrors.IsAlreadyExists(err) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"

vmbuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vm"
"github.com/deckhouse/virtualization-controller/pkg/common/annotations"
"github.com/deckhouse/virtualization-controller/pkg/common/network"
"github.com/deckhouse/virtualization-controller/pkg/common/testutil"
"github.com/deckhouse/virtualization-controller/pkg/controller/conditions"
Expand Down Expand Up @@ -285,6 +286,59 @@ var _ = Describe("SyncKvvmHandler", func() {
}
}

Context("restore-power-state intent on KVVM creation", func() {
It("requests start for a VM that was running before restore and clears the intent", func() {
vm := makeVM(v1alpha2.MachineStopped)
vm.Spec.RunPolicy = v1alpha2.ManualPolicy
vm.Annotations = map[string]string{annotations.AnnVMRestorePowerState: string(v1alpha2.MachineRunning)}

fakeClient, reconcileObj, vmState = setupEnvironment(vm, makeVMIP(), makeVMClass())

reconcile()

kvvm := &virtv1.VirtualMachine{}
Expect(fakeClient.Get(ctx, client.ObjectKeyFromObject(vm), kvvm)).To(Succeed())
Expect(kvvm.Annotations).To(HaveKeyWithValue(annotations.AnnVMStartRequested, "true"))

newVM := &v1alpha2.VirtualMachine{}
Expect(fakeClient.Get(ctx, client.ObjectKeyFromObject(vm), newVM)).To(Succeed())
Expect(newVM.Annotations).NotTo(HaveKey(annotations.AnnVMRestorePowerState))
})

It("keeps AlwaysOnUnlessStoppedManually VM stopped and clears the intent", func() {
vm := makeVM(v1alpha2.MachineStopped)
vm.Spec.RunPolicy = v1alpha2.AlwaysOnUnlessStoppedManually
vm.Annotations = map[string]string{annotations.AnnVMRestorePowerState: string(v1alpha2.MachineStopped)}

fakeClient, reconcileObj, vmState = setupEnvironment(vm, makeVMIP(), makeVMClass())

reconcile()

kvvm := &virtv1.VirtualMachine{}
Expect(fakeClient.Get(ctx, client.ObjectKeyFromObject(vm), kvvm)).To(Succeed())
Expect(kvvm.Spec.RunStrategy).NotTo(BeNil())
Expect(*kvvm.Spec.RunStrategy).To(Equal(virtv1.RunStrategyManual))

newVM := &v1alpha2.VirtualMachine{}
Expect(fakeClient.Get(ctx, client.ObjectKeyFromObject(vm), newVM)).To(Succeed())
Expect(newVM.Annotations).NotTo(HaveKey(annotations.AnnVMRestorePowerState))
})

It("starts AlwaysOnUnlessStoppedManually VM on create without the keep-stopped intent", func() {
vm := makeVM(v1alpha2.MachineStopped)
vm.Spec.RunPolicy = v1alpha2.AlwaysOnUnlessStoppedManually

fakeClient, reconcileObj, vmState = setupEnvironment(vm, makeVMIP(), makeVMClass())

reconcile()

kvvm := &virtv1.VirtualMachine{}
Expect(fakeClient.Get(ctx, client.ObjectKeyFromObject(vm), kvvm)).To(Succeed())
Expect(kvvm.Spec.RunStrategy).NotTo(BeNil())
Expect(*kvvm.Spec.RunStrategy).To(Equal(virtv1.RunStrategyAlways))
})
})

DescribeTable("AwaitingRestart Condition Tests",
func(phase v1alpha2.MachinePhase, needChange bool, expectedStatus metav1.ConditionStatus, expectedExistence bool) {
ip := makeVMIP()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"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/object"
"github.com/deckhouse/virtualization-controller/pkg/controller/conditions"
"github.com/deckhouse/virtualization-controller/pkg/eventrecord"
Expand Down Expand Up @@ -84,6 +85,32 @@ func (s EnterMaintenanceStep) Take(ctx context.Context, vmop *v1alpha2.VirtualMa
return nil, nil
}

// Preserve the VM power state across restore. The maintenance window deletes the KVVM (and with it the
// implicit run-strategy state), so capture whether the VM was running or stopped before restore and store it
// as an annotation. ProcessRestore preserves it across the annotation overwrite, and it is consumed once
// restore completes: a running VM is started again (see checkNeedStartVM), a stopped VM is kept stopped
// (see createKVVM for the AlwaysOnUnlessStoppedManually policy).
var powerState string
switch vm.Status.Phase {
case v1alpha2.MachineRunning, v1alpha2.MachinePending:
powerState = string(v1alpha2.MachineRunning)
case v1alpha2.MachineStopped:
powerState = string(v1alpha2.MachineStopped)
}
if powerState != "" && vm.Annotations[annotations.AnnVMRestorePowerState] != powerState {
if vm.Annotations == nil {
vm.Annotations = make(map[string]string)
}
vm.Annotations[annotations.AnnVMRestorePowerState] = powerState
if err = s.client.Update(ctx, vm); err != nil {
if apierrors.IsConflict(err) {
return &reconcile.Result{}, nil
}
s.recorder.Event(vmop, corev1.EventTypeWarning, v1alpha2.ReasonErrVMOPFailed, "Failed to record VM power state for restore: "+err.Error())
return &reconcile.Result{}, err
}
}

conditions.SetCondition(
conditions.NewConditionBuilder(vmcondition.TypeMaintenance).
Generation(vm.GetGeneration()).
Expand Down
Loading