Skip to content

Commit 3a6522a

Browse files
committed
chore(vm): more messages about cpu and memory hotplug fails
- Do not start live migration to apply cpu or memory changes with hotplug if VM is NonMigratable. - Fix sync_kvvm_test: fill PrintableStatus for kvvm as it is required to detect if disruptive changes may be applied. - Add test for Upgrade*ChangesToRestart mutators. Signed-off-by: Ivan Mikheykin <ivan.mikheykin@flant.com>
1 parent ed203a2 commit 3a6522a

6 files changed

Lines changed: 181 additions & 77 deletions

File tree

images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ func (h *SyncKvvmHandler) Handle(ctx context.Context, s state.VirtualMachineStat
155155
if kvvmiErr == nil && hasNonHotpluggableVolumes(kvvmi) {
156156
changes.UpgradeBlockDeviceChangesToRestart()
157157
}
158+
// Require restart for CPU and memory changes if VM is non migratable.
159+
if h.isVMNonMigratable(current) {
160+
changes.UpgradeHotplugComputeChangesToRestart()
161+
}
158162
allChanges.Add(changes.GetAll()...)
159163
}
160164
if class != nil {
@@ -771,6 +775,13 @@ func (h *SyncKvvmHandler) isVMUnschedulable(
771775
return false
772776
}
773777

778+
func (h *SyncKvvmHandler) isVMNonMigratable(
779+
vm *v1alpha2.VirtualMachine,
780+
) bool {
781+
vmMigratable, has := conditions.GetCondition(vmcondition.TypeMigratable, vm.Status.Conditions)
782+
return has && vmMigratable.Status == metav1.ConditionFalse
783+
}
784+
774785
// isPlacementPolicyChanged returns true if any of the Affinity, NodePlacement, or Toleration rules have changed.
775786
func hasNonHotpluggableVolumes(kvvmi *virtv1.VirtualMachineInstance) bool {
776787
if kvvmi == nil {

images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm_test.go

Lines changed: 136 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222

2323
. "github.com/onsi/ginkgo/v2"
2424
. "github.com/onsi/gomega"
25+
"k8s.io/apimachinery/pkg/api/resource"
2526
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2627
"k8s.io/utils/ptr"
2728
virtv1 "kubevirt.io/api/core/v1"
@@ -41,24 +42,24 @@ import (
4142
"github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition"
4243
)
4344

44-
var _ = Describe("SyncKvvmHandler", func() {
45+
var _ = FDescribe("SyncKvvmHandler", func() {
4546
const (
4647
name = "vm-sync"
4748
namespace = "default"
4849
)
4950

5051
var (
51-
ctx context.Context
52-
fakeClient client.WithWatch
53-
resource *reconciler.Resource[*v1alpha2.VirtualMachine, v1alpha2.VirtualMachineStatus]
54-
vmState state.VirtualMachineState
55-
recorder *eventrecord.EventRecorderLoggerMock
52+
ctx context.Context
53+
fakeClient client.WithWatch
54+
reconcileObj *reconciler.Resource[*v1alpha2.VirtualMachine, v1alpha2.VirtualMachineStatus]
55+
vmState state.VirtualMachineState
56+
recorder *eventrecord.EventRecorderLoggerMock
5657
)
5758

5859
BeforeEach(func() {
5960
ctx = testutil.ContextBackgroundWithNoOpLogger()
6061
fakeClient = nil
61-
resource = nil
62+
reconcileObj = nil
6263
vmState = nil
6364
recorder = &eventrecord.EventRecorderLoggerMock{
6465
EventFunc: func(_ client.Object, _, _, _ string) {},
@@ -69,27 +70,49 @@ var _ = Describe("SyncKvvmHandler", func() {
6970

7071
AfterEach(func() {
7172
fakeClient = nil
72-
resource = nil
73+
reconcileObj = nil
7374
vmState = nil
7475
recorder = nil
7576
})
7677

77-
newVM := func(phase v1alpha2.MachinePhase) *v1alpha2.VirtualMachine {
78+
makeVM := func(phase v1alpha2.MachinePhase) *v1alpha2.VirtualMachine {
7879
vm := vmbuilder.NewEmpty(name, namespace)
79-
vm.Status.Phase = phase
80+
8081
vm.Spec.VirtualMachineClassName = "vmclass"
8182
vm.Spec.CPU.Cores = 2
83+
vm.Spec.Memory.Size = resource.MustParse("2Gi")
8284
vm.Spec.RunPolicy = v1alpha2.ManualPolicy
8385
vm.Spec.VirtualMachineIPAddress = "test-ip"
8486
vm.Spec.OsType = v1alpha2.GenericOs
8587
vm.Spec.Disruptions = &v1alpha2.Disruptions{
8688
RestartApprovalMode: v1alpha2.Manual,
8789
}
8890

91+
vm.Status.Phase = phase
92+
8993
return vm
9094
}
9195

92-
newKVVM := func(vm *v1alpha2.VirtualMachine) *virtv1.VirtualMachine {
96+
// It is like mapPhases in vm/internal/util.go but reversed.
97+
mapVMPhaseToKVVMPrintableStatus := func(phase v1alpha2.MachinePhase) virtv1.VirtualMachinePrintableStatus {
98+
switch phase {
99+
case v1alpha2.MachineRunning:
100+
return virtv1.VirtualMachineStatusRunning
101+
case v1alpha2.MachineMigrating:
102+
return virtv1.VirtualMachineStatusMigrating
103+
case v1alpha2.MachineStopping:
104+
return virtv1.VirtualMachineStatusStopping
105+
case v1alpha2.MachineStopped:
106+
return virtv1.VirtualMachineStatusStopped
107+
case v1alpha2.MachineStarting:
108+
return virtv1.VirtualMachineStatusProvisioning
109+
case v1alpha2.MachinePending:
110+
return virtv1.VirtualMachineStatusUnknown
111+
}
112+
return ""
113+
}
114+
115+
makeKVVM := func(vm *v1alpha2.VirtualMachine) *virtv1.VirtualMachine {
93116
kvvm := &virtv1.VirtualMachine{
94117
ObjectMeta: metav1.ObjectMeta{
95118
Name: name,
@@ -101,11 +124,17 @@ var _ = Describe("SyncKvvmHandler", func() {
101124
}
102125
kvvm.Spec.RunStrategy = ptr.To(virtv1.RunStrategyAlways)
103126

127+
// Printable status is required for proper detection if changes are disruptive.
128+
kvvm.Status.PrintableStatus = mapVMPhaseToKVVMPrintableStatus(vm.Status.Phase)
129+
104130
Expect(kvbuilder.SetLastAppliedSpec(kvvm, &v1alpha2.VirtualMachine{
105131
Spec: v1alpha2.VirtualMachineSpec{
106132
CPU: v1alpha2.CPUSpec{
107133
Cores: vm.Spec.CPU.Cores,
108134
},
135+
Memory: v1alpha2.MemorySpec{
136+
Size: vm.Spec.Memory.Size,
137+
},
109138
VirtualMachineIPAddress: vm.Spec.VirtualMachineIPAddress,
110139
RunPolicy: vm.Spec.RunPolicy,
111140
OsType: vm.Spec.OsType,
@@ -132,16 +161,50 @@ var _ = Describe("SyncKvvmHandler", func() {
132161
return kvvm
133162
}
134163

135-
newKVVMI := func() *virtv1.VirtualMachineInstance {
164+
makeKVVMI := func() *virtv1.VirtualMachineInstance {
136165
kvvmi := newEmptyKVVMI(name, namespace)
137166
return kvvmi
138167
}
139168

169+
makeVMIP := func() *v1alpha2.VirtualMachineIPAddress {
170+
return &v1alpha2.VirtualMachineIPAddress{
171+
ObjectMeta: metav1.ObjectMeta{
172+
Name: "test-ip",
173+
Namespace: namespace,
174+
},
175+
Spec: v1alpha2.VirtualMachineIPAddressSpec{
176+
Type: v1alpha2.VirtualMachineIPAddressTypeStatic,
177+
StaticIP: "192.168.1.10",
178+
},
179+
Status: v1alpha2.VirtualMachineIPAddressStatus{
180+
Address: "192.168.1.10",
181+
Phase: v1alpha2.VirtualMachineIPAddressPhaseAttached,
182+
},
183+
}
184+
}
185+
186+
makeVMClass := func() *v1alpha2.VirtualMachineClass {
187+
return &v1alpha2.VirtualMachineClass{
188+
ObjectMeta: metav1.ObjectMeta{
189+
Name: "vmclass",
190+
}, Spec: v1alpha2.VirtualMachineClassSpec{
191+
CPU: v1alpha2.CPU{
192+
Type: v1alpha2.CPUTypeHost,
193+
},
194+
NodeSelector: v1alpha2.NodeSelector{
195+
MatchLabels: map[string]string{
196+
"node1": "node1",
197+
},
198+
},
199+
},
200+
}
201+
}
202+
140203
reconcile := func() {
141204
h := NewSyncKvvmHandler(nil, fakeClient, recorder, featuregates.Default(), vmservice.NewMigrationVolumesService(fakeClient, MakeKVVMFromVMSpec, 10*time.Second))
142205
_, err := h.Handle(ctx, vmState)
143206
Expect(err).NotTo(HaveOccurred())
144-
err = resource.Update(context.Background())
207+
err = reconcileObj.Update(context.Background())
145208
Expect(err).NotTo(HaveOccurred())
146209
}
147210

@@ -175,47 +238,33 @@ var _ = Describe("SyncKvvmHandler", func() {
175238
})).To(Succeed())
176239
}
177240

241+
mutateCPUCores := func(cores int) func(fakeClient client.WithWatch, vm *v1alpha2.VirtualMachine, kvvm *virtv1.VirtualMachine) {
242+
return func(fakeClient client.WithWatch, vm *v1alpha2.VirtualMachine, kvvm *virtv1.VirtualMachine) {
243+
vm.Spec.CPU.Cores = cores
244+
}
245+
}
246+
247+
mutateMemorySize := func(size string) func(fakeClient client.WithWatch, vm *v1alpha2.VirtualMachine, kvvm *virtv1.VirtualMachine) {
248+
memSize := resource.MustParse(size)
249+
return func(fakeClient client.WithWatch, vm *v1alpha2.VirtualMachine, kvvm *virtv1.VirtualMachine) {
250+
vm.Spec.Memory.Size = memSize
251+
}
252+
}
253+
178254
DescribeTable("AwaitingRestart Condition Tests",
179255
func(phase v1alpha2.MachinePhase, needChange bool, expectedStatus metav1.ConditionStatus, expectedExistence bool) {
180-
ip := &v1alpha2.VirtualMachineIPAddress{
181-
ObjectMeta: metav1.ObjectMeta{
182-
Name: "test-ip",
183-
Namespace: namespace,
184-
},
185-
Spec: v1alpha2.VirtualMachineIPAddressSpec{
186-
Type: v1alpha2.VirtualMachineIPAddressTypeStatic,
187-
StaticIP: "192.168.1.10",
188-
},
189-
Status: v1alpha2.VirtualMachineIPAddressStatus{
190-
Address: "192.168.1.10",
191-
Phase: v1alpha2.VirtualMachineIPAddressPhaseAttached,
192-
},
193-
}
256+
ip := makeVMIP()
257+
vmClass := makeVMClass()
194258

195-
vmClass := &v1alpha2.VirtualMachineClass{
196-
ObjectMeta: metav1.ObjectMeta{
197-
Name: "vmclass",
198-
}, Spec: v1alpha2.VirtualMachineClassSpec{
199-
CPU: v1alpha2.CPU{
200-
Type: v1alpha2.CPUTypeHost,
201-
},
202-
NodeSelector: v1alpha2.NodeSelector{
203-
MatchLabels: map[string]string{
204-
"node1": "node1",
205-
},
206-
},
207-
},
208-
}
209-
210-
vm := newVM(phase)
211-
kvvm := newKVVM(vm)
212-
kvvmi := newKVVMI()
259+
vm := makeVM(phase)
260+
kvvm := makeKVVM(vm)
261+
kvvmi := makeKVVMI()
213262

214263
if needChange {
215264
mutateKVVM(vm, kvvm)
216265
}
217266

218-
fakeClient, resource, vmState = setupEnvironment(vm, kvvm, kvvmi, ip, vmClass)
267+
fakeClient, reconcileObj, vmState = setupEnvironment(vm, kvvm, kvvmi, ip, vmClass)
219268

220269
reconcile()
221270

@@ -248,49 +297,59 @@ var _ = Describe("SyncKvvmHandler", func() {
248297
Entry("Pending phase without changes, shouldn't have condition", v1alpha2.MachinePending, false, metav1.ConditionUnknown, false),
249298
)
250299

251-
DescribeTable("ConfigurationApplied Condition Tests",
252-
func(phase v1alpha2.MachinePhase, notReady bool, expectedStatus metav1.ConditionStatus, expectedExistence bool) {
253-
ip := &v1alpha2.VirtualMachineIPAddress{
254-
ObjectMeta: metav1.ObjectMeta{
255-
Name: "test-ip",
256-
Namespace: namespace,
257-
},
258-
Spec: v1alpha2.VirtualMachineIPAddressSpec{
259-
Type: v1alpha2.VirtualMachineIPAddressTypeStatic,
260-
StaticIP: "192.168.1.10",
261-
},
262-
Status: v1alpha2.VirtualMachineIPAddressStatus{
263-
Address: "192.168.1.10",
264-
Phase: v1alpha2.VirtualMachineIPAddressPhaseAttached,
265-
},
300+
DescribeTable("AwaitingRestart Condition for NonMigratable VM",
301+
func(phase v1alpha2.MachinePhase, mutateFn func(fakeClient client.WithWatch, vm *v1alpha2.VirtualMachine, kvvm *virtv1.VirtualMachine), expectedStatus metav1.ConditionStatus, expectedExistence bool) {
302+
ip := makeVMIP()
303+
vmClass := makeVMClass()
304+
305+
vm := makeVM(phase)
306+
vm.Status.Conditions = append(vm.Status.Conditions, metav1.Condition{
307+
Type: vmcondition.TypeMigratable.String(),
308+
Status: metav1.ConditionFalse,
309+
Reason: string(vmcondition.ReasonHostDevicesNotMigratable),
310+
})
311+
kvvm := makeKVVM(vm)
312+
kvvmi := makeKVVMI()
313+
314+
if mutateFn != nil {
315+
mutateFn(fakeClient, vm, kvvm)
266316
}
267317

268-
vmClass := &v1alpha2.VirtualMachineClass{
269-
ObjectMeta: metav1.ObjectMeta{
270-
Name: "vmclass",
271-
}, Spec: v1alpha2.VirtualMachineClassSpec{
272-
CPU: v1alpha2.CPU{
273-
Type: v1alpha2.CPUTypeHost,
274-
},
275-
NodeSelector: v1alpha2.NodeSelector{
276-
MatchLabels: map[string]string{
277-
"node1": "node1",
278-
},
279-
},
280-
},
318+
fakeClient, reconcileObj, vmState = setupEnvironment(vm, kvvm, kvvmi, ip, vmClass)
319+
320+
reconcile()
321+
322+
newVM := &v1alpha2.VirtualMachine{}
323+
err := fakeClient.Get(ctx, client.ObjectKeyFromObject(vm), newVM)
324+
Expect(err).NotTo(HaveOccurred())
325+
326+
awaitCond, awaitExists := conditions.GetCondition(vmcondition.TypeAwaitingRestartToApplyConfiguration, newVM.Status.Conditions)
327+
Expect(awaitExists).To(Equal(expectedExistence))
328+
if awaitExists {
329+
Expect(awaitCond.Status).To(Equal(expectedStatus))
281330
}
331+
},
332+
Entry("should present on hotplugging cpu.cores", v1alpha2.MachineRunning, mutateCPUCores(3), metav1.ConditionTrue, true),
333+
Entry("should present on hotplugging memory.size", v1alpha2.MachineRunning, mutateMemorySize("4Gi"), metav1.ConditionTrue, true),
334+
)
335+
336+
DescribeTable("ConfigurationApplied Condition Tests",
337+
func(phase v1alpha2.MachinePhase, notReady bool, expectedStatus metav1.ConditionStatus, expectedExistence bool) {
338+
ip := makeVMIP()
339+
vmClass := makeVMClass()
282340

283-
vm := newVM(phase)
341+
vm := makeVM(phase)
284342
if notReady {
285343
vm.Status.Conditions = append(vm.Status.Conditions, metav1.Condition{
286344
Type: vmcondition.TypeBlockDevicesReady.String(),
287345
Status: metav1.ConditionFalse,
288346
Reason: "BlockDevicesNotReady",
289347
})
290348
}
291-
kvvm := newKVVM(vm)
349+
kvvm := makeKVVM(vm)
350+
kvvmi := makeKVVMI()
292351

293-
fakeClient, resource, vmState = setupEnvironment(vm, kvvm, ip, vmClass)
352+
fakeClient, reconcileObj, vmState = setupEnvironment(vm, kvvm, kvvmi, ip, vmClass)
294353
reconcile()
295354

296355
newVM := &v1alpha2.VirtualMachine{}

images/virtualization-artifact/pkg/controller/vmchange/comparator_cpu.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ type comparatorCPU struct {
2828
featureGate featuregate.FeatureGate
2929
}
3030

31+
const CPUPath = "cpu"
32+
3133
func NewComparatorCPU(featureGate featuregate.FeatureGate) VMSpecFieldComparator {
3234
return &comparatorCPU{
3335
featureGate: featureGate,

images/virtualization-artifact/pkg/controller/vmchange/comparator_memory.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import (
2525
"github.com/deckhouse/virtualization/api/core/v1alpha2"
2626
)
2727

28+
const MemoryPath = "memory"
29+
2830
type comparatorMemory struct {
2931
featureGate featuregate.FeatureGate
3032
}

images/virtualization-artifact/pkg/controller/vmchange/compare_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,26 @@ networks:
561561
}
562562
}
563563

564+
// Test ApplyImmediate to Restart upgraders.
565+
func TestUpgradeHotplugComputeChangesToRestart(t *testing.T) {
566+
const cpuReason = "cpu restart reason"
567+
568+
changes := SpecChanges{}
569+
changes.Add(
570+
FieldChange{Path: "cpu.cores", ActionRequired: ActionApplyImmediate, RestartMessage: cpuReason},
571+
FieldChange{Path: "memory.size", ActionRequired: ActionApplyImmediate},
572+
FieldChange{Path: "blockDeviceRefs.0", ActionRequired: ActionApplyImmediate},
573+
)
574+
575+
changes.UpgradeHotplugComputeChangesToRestart()
576+
changes.UpgradeBlockDeviceChangesToRestart()
577+
578+
require.Equal(t, ActionRestart, changes.GetAll()[0].ActionRequired)
579+
require.Equal(t, cpuReason, changes.GetAll()[0].RestartMessage)
580+
require.Equal(t, ActionRestart, changes.GetAll()[1].ActionRequired)
581+
require.Equal(t, ActionRestart, changes.GetAll()[2].ActionRequired)
582+
}
583+
564584
func loadVMSpec(t *testing.T, inYAML string) *v1alpha2.VirtualMachineSpec {
565585
t.Helper()
566586
var spec v1alpha2.VirtualMachineSpec

0 commit comments

Comments
 (0)