-
Notifications
You must be signed in to change notification settings - Fork 243
Expand file tree
/
Copy pathreconciler.go
More file actions
1937 lines (1677 loc) · 66.5 KB
/
reconciler.go
File metadata and controls
1937 lines (1677 loc) · 66.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package vsphere
import (
"context"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"net"
"net/netip"
"regexp"
"slices"
"strconv"
"strings"
"github.com/vmware/govmomi/task"
"github.com/openshift/machine-api-operator/pkg/util/ipam"
"github.com/google/uuid"
"github.com/vmware/govmomi/find"
"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/property"
"github.com/vmware/govmomi/vim25"
"github.com/vmware/govmomi/vim25/mo"
"github.com/vmware/govmomi/vim25/types"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
apimachinerytypes "k8s.io/apimachinery/pkg/types"
apimachineryutilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/component-base/featuregate"
"k8s.io/klog/v2"
"k8s.io/utils/ptr"
apifeatures "github.com/openshift/api/features"
machinev1 "github.com/openshift/api/machine/v1beta1"
machinecontroller "github.com/openshift/machine-api-operator/pkg/controller/machine"
"github.com/openshift/machine-api-operator/pkg/controller/vsphere/session"
"github.com/openshift/machine-api-operator/pkg/metrics"
)
const (
fullCloneDiskMoveType = string(types.VirtualMachineRelocateDiskMoveOptionsMoveAllDiskBackingsAndConsolidate)
linkCloneDiskMoveType = string(types.VirtualMachineRelocateDiskMoveOptionsCreateNewChildDiskBacking)
ethCardType = "vmxnet3"
providerIDPrefix = "vsphere://"
regionKey = "region"
zoneKey = "zone"
minimumHWVersion = 15
// maxUnitNumber constant is used to define the maximum number of devices that can be assigned to a virtual machine's controller.
// Not all controllers support up to 30, but the maximum is 30.
// xref: https://docs.vmware.com/en/VMware-vSphere/8.0/vsphere-vm-administration/GUID-5872D173-A076-42FE-8D0B-9DB0EB0E7362.html#:~:text=If%20you%20add%20a%20hard,values%20from%200%20to%2014.
maxUnitNumber = 30
)
// These are the guestinfo variables used by Ignition.
// https://access.redhat.com/documentation/en-us/openshift_container_platform/4.1/html/installing/installing-on-vsphere
const (
GuestInfoIgnitionData = "guestinfo.ignition.config.data"
GuestInfoIgnitionEncoding = "guestinfo.ignition.config.data.encoding"
GuestInfoHostname = "guestinfo.hostname"
GuestInfoNetworkKargs = "guestinfo.afterburn.initrd.network-kargs"
StealClock = "stealclock.enable"
)
// vSphere tasks description IDs, for determinate task types (clone, delete, etc)
const (
cloneVmTaskDescriptionId = "VirtualMachine.clone"
destroyVmTaskDescriptionId = "VirtualMachine.destroy"
powerOffVmTaskDescriptionId = "VirtualMachine.powerOff"
)
// Reconciler runs the logic to reconciles a machine resource towards its desired state
type Reconciler struct {
*machineScope
}
func newReconciler(scope *machineScope) *Reconciler {
return &Reconciler{
machineScope: scope,
}
}
// create creates machine if it does not exists.
func (r *Reconciler) create() error {
if err := validateMachine(*r.machine); err != nil {
return fmt.Errorf("%v: failed validating machine provider spec: %w", r.machine.GetName(), err)
}
if r.providerSpec.Workspace.VMGroup != "" && !r.featureGates.Enabled(featuregate.Feature(apifeatures.FeatureGateVSphereHostVMGroupZonal)) {
return fmt.Errorf("%v: vmGroup is only available with the VSphereHostVMGroupZonal feature gate", r.machine.GetName())
}
if ipam.HasStaticIPConfiguration(r.providerSpec) {
outstandingClaims, err := ipam.HasOutstandingIPAddressClaims(
r.Context,
r.client,
r.machine,
r.providerSpec.Network.Devices,
)
if err != nil {
return err
}
condition := metav1.Condition{
Type: string(machinev1.IPAddressClaimedCondition),
Reason: machinev1.IPAddressClaimedReason,
Message: "All IP address claims are bound",
Status: metav1.ConditionTrue,
}
if outstandingClaims > 0 {
condition.Message = fmt.Sprintf("Waiting on %d IP address claims to be bound", outstandingClaims)
condition.Reason = machinev1.WaitingForIPAddressReason
condition.Status = metav1.ConditionFalse
klog.Infof("Waiting for IPAddressClaims associated with machine %s to be bound", r.machine.Name)
}
if err := setProviderStatus("", condition, r.machineScope, nil); err != nil {
return fmt.Errorf("could not set provider status: %w", err)
}
}
// We only clone the VM template if we have no taskRef.
if r.providerStatus.TaskRef == "" {
klog.V(4).Infof("%v: ProviderStatus does not have TaskRef", r.machine.GetName())
if !r.machineScope.session.IsVC() {
return fmt.Errorf("%v: not connected to a vCenter", r.machine.GetName())
}
// Attempt to power on instance in situation where we alredy cloned the instance and lost taskRef.
klog.V(4).Infof("%v: InstanceState is: %q", r.machine.GetName(), ptr.Deref(r.machineScope.providerStatus.InstanceState, ""))
if types.VirtualMachinePowerState(ptr.Deref(r.machineScope.providerStatus.InstanceState, "")) == types.VirtualMachinePowerStatePoweredOff {
klog.Infof("Powering on cloned machine without taskID: %v", r.machine.Name)
task, err := powerOn(r.machineScope)
if err != nil {
metrics.RegisterFailedInstanceCreate(&metrics.MachineLabels{
Name: r.machine.Name,
Namespace: r.machine.Namespace,
Reason: "PowerOn task finished with error",
})
conditionFailed := conditionFailed()
conditionFailed.Message = err.Error()
statusError := setProviderStatus(task, conditionFailed, r.machineScope, nil)
if statusError != nil {
return fmt.Errorf("failed to set provider status: %w", err)
}
return fmt.Errorf("%v: failed to power on machine: %w", r.machine.GetName(), err)
}
return setProviderStatus(task, conditionSuccess(), r.machineScope, nil)
}
klog.Infof("%v: cloning", r.machine.GetName())
task, err := clone(r.machineScope)
if err != nil {
metrics.RegisterFailedInstanceCreate(&metrics.MachineLabels{
Name: r.machine.Name,
Namespace: r.machine.Namespace,
Reason: "Clone task finished with error",
})
conditionFailed := conditionFailed()
conditionFailed.Message = err.Error()
statusError := setProviderStatus(task, conditionFailed, r.machineScope, nil)
if statusError != nil {
return fmt.Errorf("failed to set provider status: %w", err)
}
return err
}
return setProviderStatus(task, conditionSuccess(), r.machineScope, nil)
}
moTask, err := r.session.GetTask(r.Context, r.providerStatus.TaskRef)
if err != nil {
// Task may have been cleaned up by vCenter. Check if VM exists and needs to be powered on.
if isRetrieveMONotFound(r.providerStatus.TaskRef, err) {
klog.V(2).Infof("%v: task %s not found, checking if VM exists and needs power-on", r.machine.GetName(), r.providerStatus.TaskRef)
return r.handleTaskNotFound()
}
metrics.RegisterFailedInstanceCreate(&metrics.MachineLabels{
Name: r.machine.Name,
Namespace: r.machine.Namespace,
Reason: "GetTask finished with error",
})
return err
}
if moTask == nil {
// Task object is nil, may indicate eventual consistency or cleanup.
// Check if VM exists and needs to be powered on.
klog.V(2).Infof("%v: task %s returned nil, checking if VM exists and needs power-on", r.machine.GetName(), r.providerStatus.TaskRef)
return r.handleTaskNotFound()
}
if taskIsFinished, err := taskIsFinished(moTask); err != nil {
if taskIsFinished {
metrics.RegisterFailedInstanceCreate(&metrics.MachineLabels{
Name: r.machine.Name,
Namespace: r.machine.Namespace,
Reason: "Task finished with error",
})
conditionFailed := conditionFailed()
conditionFailed.Message = err.Error()
statusError := setProviderStatus(moTask.Reference().Value, conditionFailed, r.machineScope, nil)
if statusError != nil {
return fmt.Errorf("failed to set provider status: %w", statusError)
}
return machinecontroller.CreateMachine("%s", err.Error())
} else {
return fmt.Errorf("failed to check task status: %w", err)
}
} else {
if taskIsFinished {
klog.V(4).Infof("%v task %v has completed", moTask.Info.DescriptionId, moTask.Reference().Value)
} else {
return fmt.Errorf("%v task %v has not finished", moTask.Info.DescriptionId, moTask.Reference().Value)
}
}
// if clone task finished successfully, power on the vm
// The simulator task.Info.DescriptionId is different (VirtualMachine.cloneVM)
if strings.Contains(moTask.Info.DescriptionId, cloneVmTaskDescriptionId) {
if r.machineScope.providerSpec.Workspace.VMGroup != "" {
klog.Infof("Adding on cloned machine: %s to vm group: %s", r.machine.Name, r.machineScope.providerSpec.Workspace.VMGroup)
if err := modifyVMGroup(r.machineScope, false); err != nil {
var taskError task.Error
if errors.As(err, &taskError) {
return fmt.Errorf("could not update VM Group membership: %w", taskError)
}
return fmt.Errorf("could not update VM Group membership: %w", err)
}
}
klog.Infof("Powering on cloned machine: %v", r.machine.Name)
task, err := powerOn(r.machineScope)
if err != nil {
metrics.RegisterFailedInstanceCreate(&metrics.MachineLabels{
Name: r.machine.Name,
Namespace: r.machine.Namespace,
Reason: "PowerOn task finished with error",
})
conditionFailed := conditionFailed()
conditionFailed.Message = err.Error()
statusError := setProviderStatus(task, conditionFailed, r.machineScope, nil)
if statusError != nil {
return fmt.Errorf("failed to set provider status: %w", err)
}
return err
}
return setProviderStatus(task, conditionSuccess(), r.machineScope, nil)
}
// If taskIsFinished then next reconcile should result in update.
return nil
}
// handleTaskNotFound handles the case where the task reference exists but the task
// object is no longer available in vCenter (e.g., cleaned up). It checks if the VM
// exists and if it's powered off, attempts to power it on. This handles the race
// condition where clone completes but power-on hasn't been triggered yet.
func (r *Reconciler) handleTaskNotFound() error {
// Try to find the VM
vmRef, err := findVM(r.machineScope)
if err != nil {
if isNotFound(err) {
// VM doesn't exist yet, this is unexpected if we have a TaskRef
// Clear the TaskRef and let the next reconcile attempt to clone
klog.V(2).Infof("%v: VM not found after task cleanup, clearing TaskRef and requeuing", r.machine.GetName())
r.providerStatus.TaskRef = ""
if err := r.machineScope.PatchMachine(); err != nil {
return fmt.Errorf("failed to patch machine after clearing TaskRef: %w", err)
}
return fmt.Errorf("%v: VM not found, will retry clone operation", r.machine.GetName())
}
return fmt.Errorf("%v: failed to find VM after task cleanup: %w", r.machine.GetName(), err)
}
// VM exists, check its power state
vm := &virtualMachine{
Context: r.machineScope.Context,
Obj: object.NewVirtualMachine(r.machineScope.session.Client.Client, vmRef),
Ref: vmRef,
}
powerState, err := vm.getPowerState()
if err != nil {
return fmt.Errorf("%v: failed to get VM power state after task cleanup: %w", r.machine.GetName(), err)
}
klog.V(2).Infof("%v: VM found with power state %s after task cleanup", r.machine.GetName(), powerState)
// If VM is powered off, we need to power it on
if powerState == types.VirtualMachinePowerStatePoweredOff {
klog.Infof("%v: VM exists but is powered off after task cleanup, powering on", r.machine.GetName())
task, err := vm.powerOnVM()
if err != nil {
metrics.RegisterFailedInstanceCreate(&metrics.MachineLabels{
Name: r.machine.Name,
Namespace: r.machine.Namespace,
Reason: "PowerOn task finished with error",
})
conditionFailed := conditionFailed()
conditionFailed.Message = err.Error()
statusError := setProviderStatus(task, conditionFailed, r.machineScope, vm)
if statusError != nil {
return fmt.Errorf("failed to set provider status: %w", statusError)
}
return fmt.Errorf("%v: failed to power on machine: %w", r.machine.GetName(), err)
}
// Clear the old TaskRef and set the new power-on task
r.providerStatus.TaskRef = task
klog.Infof("%v: Successfully initiated power-on, tracking new task %s", r.machine.GetName(), task)
return setProviderStatus(task, conditionSuccess(), r.machineScope, vm)
}
// VM is already powered on (or in another state), clear the old TaskRef
// and let the next reconcile proceed to update
klog.V(2).Infof("%v: VM is already powered on (%s), clearing old TaskRef", r.machine.GetName(), powerState)
r.providerStatus.TaskRef = ""
if err := r.machineScope.PatchMachine(); err != nil {
return fmt.Errorf("failed to patch machine after clearing TaskRef: %w", err)
}
// Return nil to indicate success, next reconcile will call update()
return nil
}
// update finds a vm and reconciles the machine resource status against it.
func (r *Reconciler) update() error {
if err := validateMachine(*r.machine); err != nil {
return fmt.Errorf("%v: failed validating machine provider spec: %w", r.machine.GetName(), err)
}
if r.providerStatus.TaskRef != "" {
moTask, err := r.session.GetTask(r.Context, r.providerStatus.TaskRef)
if err != nil {
if !isRetrieveMONotFound(r.providerStatus.TaskRef, err) {
metrics.RegisterFailedInstanceUpdate(&metrics.MachineLabels{
Name: r.machine.Name,
Namespace: r.machine.Namespace,
Reason: "GetTask finished with error",
})
return err
}
}
if moTask != nil {
if taskIsFinished, err := taskIsFinished(moTask); err != nil {
metrics.RegisterFailedInstanceUpdate(&metrics.MachineLabels{
Name: r.machine.Name,
Namespace: r.machine.Namespace,
Reason: "Task finished with error",
})
return fmt.Errorf("%v task %v finished with error: %w", moTask.Info.DescriptionId, moTask.Reference().Value, err)
} else if !taskIsFinished {
return fmt.Errorf("%v task %v has not finished", moTask.Info.DescriptionId, moTask.Reference().Value)
}
}
}
vmRef, err := findVM(r.machineScope)
if err != nil {
metrics.RegisterFailedInstanceUpdate(&metrics.MachineLabels{
Name: r.machine.Name,
Namespace: r.machine.Namespace,
Reason: "FindVM finished with error",
})
if !isNotFound(err) {
return err
}
return fmt.Errorf("vm not found on update: %w", err)
}
vm := &virtualMachine{
Context: r.machineScope.Context,
Obj: object.NewVirtualMachine(r.machineScope.session.Client.Client, vmRef),
Ref: vmRef,
}
if err := vm.reconcileTags(r.Context, r.session, r.machine, r.providerSpec); err != nil {
metrics.RegisterFailedInstanceUpdate(&metrics.MachineLabels{
Name: r.machine.Name,
Namespace: r.machine.Namespace,
Reason: "ReconcileTags finished with error",
})
return fmt.Errorf("failed to reconcile tags: %w", err)
}
if err := r.reconcileMachineWithCloudState(vm, r.providerStatus.TaskRef); err != nil {
metrics.RegisterFailedInstanceUpdate(&metrics.MachineLabels{
Name: r.machine.Name,
Namespace: r.machine.Namespace,
Reason: "ReconcileWithCloudState finished with error",
})
return err
}
return nil
}
// exists returns true if machine exists.
func (r *Reconciler) exists() (bool, error) {
if err := validateMachine(*r.machine); err != nil {
return false, fmt.Errorf("%v: failed validating machine provider spec: %w", r.machine.GetName(), err)
}
vmRef, err := findVM(r.machineScope)
if err != nil {
if !isNotFound(err) {
return false, err
}
klog.Infof("%v: does not exist", r.machine.GetName())
return false, nil
}
// Check if machine was powered on after clone.
// If it is powered off and in "Provisioning" phase, treat machine as non-existed yet and proceed with creation procedure.
powerState := types.VirtualMachinePowerState(ptr.Deref(r.machineScope.providerStatus.InstanceState, ""))
if powerState == "" || ptr.Deref(r.machine.Status.Phase, "") == machinev1.PhaseProvisioning {
vm := &virtualMachine{
Context: r.machineScope.Context,
Obj: object.NewVirtualMachine(r.machineScope.session.Client.Client, vmRef),
Ref: vmRef,
}
powerState, err = vm.getPowerState()
if err != nil {
return false, fmt.Errorf("%v: failed checking machine's power state: %w", r.machine.GetName(), err)
}
}
if ptr.Deref(r.machine.Status.Phase, "") == machinev1.PhaseProvisioning && powerState == types.VirtualMachinePowerStatePoweredOff {
klog.Infof("%v: already exists, but was not powered on after clone", r.machine.GetName())
r.machineScope.providerStatus.InstanceState = ptr.To(string(powerState))
if err := r.machineScope.PatchMachine(); err != nil {
return false, fmt.Errorf("%v: failed to patch machine: %w", r.machine.GetName(), err)
}
return false, nil
}
klog.Infof("%v: already exists", r.machine.GetName())
return true, nil
}
func (r *Reconciler) delete() error {
if r.providerStatus.TaskRef != "" {
// TODO: We need to use a separate status field for the create and the
// delete taskref.
moTask, err := r.session.GetTask(r.Context, r.providerStatus.TaskRef)
if err != nil {
if !isRetrieveMONotFound(r.providerStatus.TaskRef, err) {
return err
}
}
if moTask != nil {
if taskIsFinished, err := taskIsFinished(moTask); err != nil {
// Check if latest task is not a task for vm cloning
if taskIsFinished && moTask.Info.DescriptionId != cloneVmTaskDescriptionId {
metrics.RegisterFailedInstanceDelete(&metrics.MachineLabels{
Name: r.machine.Name,
Namespace: r.machine.Namespace,
Reason: "Task finished with error",
})
klog.Errorf("Delete task finished with error: %v", err)
return fmt.Errorf("%v task %v finished with error: %w", moTask.Info.DescriptionId, moTask.Reference().Value, err)
} else {
klog.Warningf(
"TaskRef points to clone task which finished with error: %v. Proceeding with machine deletion", err,
)
}
} else if !taskIsFinished {
return fmt.Errorf("%v task %v has not finished", moTask.Info.DescriptionId, moTask.Reference().Value)
}
}
}
vmRef, err := findVM(r.machineScope)
if err != nil {
if !isNotFound(err) {
metrics.RegisterFailedInstanceDelete(&metrics.MachineLabels{
Name: r.machine.Name,
Namespace: r.machine.Namespace,
Reason: "FindVM finished with error",
})
return err
}
klog.Infof("%v: vm does not exist", r.machine.GetName())
// remove any finalizers for IPAddressClaims which may be associated with the machine
err = ipam.RemoveFinalizersForIPAddressClaims(r.Context, r.client, *r.machine)
if err != nil {
return fmt.Errorf("unable to remove finalizer for IP address claims: %w", err)
}
return nil
}
vm := &virtualMachine{
Context: r.Context,
Obj: object.NewVirtualMachine(r.machineScope.session.Client.Client, vmRef),
Ref: vmRef,
}
powerState, err := vm.getPowerState()
if err != nil {
return fmt.Errorf("can not determine %v vm power state: %w", r.machine.GetName(), err)
}
if powerState != types.VirtualMachinePowerStatePoweredOff {
powerOffTaskRef, err := vm.powerOffVM()
if err != nil {
return fmt.Errorf("%v: failed to power off vm: %w", r.machine.GetName(), err)
}
if err := setProviderStatus(powerOffTaskRef, conditionSuccess(), r.machineScope, vm); err != nil {
return fmt.Errorf("failed to set provider status: %w", err)
}
return fmt.Errorf("powering off vm is in progress, requeuing")
}
// At this point node should be drained and vm powered off already.
// We need to check attached disks and ensure that all disks potentially related to PVs were detached
// to prevent possible data loss.
// Destroying a VM with attached disks might lead to data loss in case pvs are handled by the intree storage driver.
_, drainSkipped := r.machine.ObjectMeta.Annotations[machinecontroller.ExcludeNodeDrainingAnnotation]
// If node linked to the machine, and node was drained checking node status first
if r.machineScope.isNodeLinked() && !drainSkipped {
// After node draining, make sure volumes are detached before deleting the Node.
attached, err := r.nodeHasVolumesAttached(r.Context, r.machine.Status.NodeRef.Name, r.machine.Name)
if err != nil {
return fmt.Errorf("failed to determine if node %v has attached volumes: %w", r.machine.Status.NodeRef.Name, err)
}
if attached {
// If there are volumes still attached, it's possible that node draining did not fully finish,
// this might happen if the kubelet was non-functional during the draining procedure.
// Try forcefully deleting pods in the "Terminating" state to trigger persistent volumes detachment.
klog.Warningf(
"Attached volumes detected on a powered off node, node draining may not succeed. " +
"Attempting to delete unevicted pods",
)
numPodsDeleted, err := r.machineScope.deleteUnevictedPods()
klog.Warningf("Deleted %d pods", numPodsDeleted)
if err != nil {
return fmt.Errorf("unable to fully drain node, can not delete unevicted pods: %w", err)
}
return fmt.Errorf("node %v has attached volumes, requeuing", r.machine.Status.NodeRef.Name)
}
}
klog.V(3).Infof("Checking attached disks before vm destroy")
disks, err := vm.getAttachedDisks()
if err != nil {
return fmt.Errorf("%v: can not obtain virtual disks attached to the vm: %w", r.machine.GetName(), err)
}
additionalDisks := len(r.providerSpec.DataDisks)
// Currently, MAPI only allows VMs to be configured w/ 1 primary disk in the template and a limited number of additional
// disks via the data disks configuration. So, we are expecting the VM to have only one disk, which is OS disk, plus
// the additional disks defined in the DataDisks configuration.
if len(disks) > 1+additionalDisks {
// If node drain was skipped we need to detach disks forcefully to prevent possible data corruption.
if drainSkipped {
klog.V(1).Infof(
"%s: drain was skipped for the machine, detaching disks before vm destruction to prevent data loss",
r.machine.GetName(),
)
if err := vm.detachDisks(filterOutVmOsDisk(disks, r.machine)); err != nil {
return fmt.Errorf("failed to detach disks: %w", err)
}
klog.V(1).Infof(
"%s: disks were detached", r.machine.GetName(),
)
return errors.New(
"disks were detached, vm will be attempted to destroy in next reconciliation, requeuing",
)
}
// Block vm destruction till attach-detach controller has properly detached disks
return errors.New(
"additional attached disks detected, block vm destruction and wait for disks to be detached",
)
}
task, err := vm.Obj.Destroy(r.Context)
if err != nil {
metrics.RegisterFailedInstanceDelete(&metrics.MachineLabels{
Name: r.machine.Name,
Namespace: r.machine.Namespace,
Reason: "Destroy finished with error",
})
return fmt.Errorf("%v: failed to destroy vm: %w", r.machine.GetName(), err)
}
if r.machineScope.providerSpec.Workspace.VMGroup != "" {
klog.Infof("Removing machine: %v from vm group: %v", r.machine.Name, r.machineScope.providerSpec.Workspace.VMGroup)
if err := modifyVMGroup(r.machineScope, true); err != nil {
return fmt.Errorf("failed to remove machine from vm group: %w", err)
}
}
if err := setProviderStatus(task.Reference().Value, conditionSuccess(), r.machineScope, vm); err != nil {
return fmt.Errorf("failed to set provider status: %w", err)
}
// TODO: consider returning an error to specify retry time here
return fmt.Errorf("destroying vm in progress, requeuing")
}
// nodeHasVolumesAttached returns true if node status still have volumes attached
// pod deletion and volume detach happen asynchronously, so pod could be deleted before volume detached from the node
// this could cause issue for some storage provisioner, for example, vsphere-volume this is problematic
// because if the node is deleted before detach success, then the underline VMDK will be deleted together with the Machine
// so after node draining we need to check if all volumes are detached before deleting the node.
func (r *Reconciler) nodeHasVolumesAttached(ctx context.Context, nodeName string, machineName string) (bool, error) {
node := &corev1.Node{}
if err := r.apiReader.Get(ctx, apimachinerytypes.NamespacedName{Name: nodeName}, node); err != nil {
if apierrors.IsNotFound(err) {
klog.Errorf("Could not find node from noderef, it may have already been deleted: %v", err)
return false, nil
}
return true, err
}
return len(node.Status.VolumesAttached) != 0, nil
}
// reconcileMachineWithCloudState reconcile machineSpec and status with the latest cloud state
func (r *Reconciler) reconcileMachineWithCloudState(vm *virtualMachine, taskRef string) error {
klog.V(3).Infof("%v: reconciling machine with cloud state", r.machine.GetName())
// TODO: reconcile task
if err := r.reconcileRegionAndZoneLabels(vm); err != nil {
// Not treating this is as a fatal error for now.
klog.Errorf("Failed to reconcile region and zone labels: %v", err)
}
klog.V(3).Infof("%v: reconciling providerID", r.machine.GetName())
if err := r.reconcileProviderID(vm); err != nil {
return err
}
klog.V(3).Infof("%v: reconciling network", r.machine.GetName())
if err := r.reconcileNetwork(vm); err != nil {
return err
}
klog.V(3).Infof("%v: reconciling powerstate annotation", r.machine.GetName())
if err := r.reconcilePowerStateAnnontation(vm); err != nil {
return err
}
return setProviderStatus(taskRef, conditionSuccess(), r.machineScope, vm)
}
// reconcileRegionAndZoneLabels reconciles the labels on the Machine containing
// region and zone information -- provided the vSphere cloud provider has been
// configured with the labels that identify region and zone, and the configured
// tags are found somewhere in the ancestry of the given virtual machine.
func (r *Reconciler) reconcileRegionAndZoneLabels(vm *virtualMachine) error {
if r.vSphereConfig == nil {
klog.Warning("No vSphere cloud provider config. " +
"Will not set region and zone labels.")
return nil
}
regionLabel := r.vSphereConfig.Labels.Region
zoneLabel := r.vSphereConfig.Labels.Zone
// Use cached tag manager to avoid creating new REST sessions.
// This eliminates excessive vCenter login/logout cycles.
tagManager := r.session.GetCachingTagsManager()
res, err := vm.getRegionAndZone(tagManager, regionLabel, zoneLabel)
if err != nil {
return err
}
if r.machine.Labels == nil {
r.machine.Labels = make(map[string]string)
}
r.machine.Labels[machinecontroller.MachineRegionLabelName] = res[regionKey]
r.machine.Labels[machinecontroller.MachineAZLabelName] = res[zoneKey]
return nil
}
func (r *Reconciler) reconcileProviderID(vm *virtualMachine) error {
providerID, err := convertUUIDToProviderID(vm.Obj.UUID(vm.Context))
if err != nil {
return err
}
r.machine.Spec.ProviderID = &providerID
return nil
}
// convertUUIDToProviderID transforms a UUID string into a provider ID.
func convertUUIDToProviderID(UUID string) (string, error) {
parsedUUID, err := uuid.Parse(UUID)
if err != nil {
return "", err
}
return providerIDPrefix + parsedUUID.String(), nil
}
func (r *Reconciler) reconcileNetwork(vm *virtualMachine) error {
currentNetworkStatusList, err := vm.getNetworkStatusList(r.session.Client.Client)
if err != nil {
return fmt.Errorf("error getting network status: %v", err)
}
//If the VM is powered on then issue requeues until all of the VM's
//networks have IP addresses.
expectNetworkLen, currentNetworkLen := len(r.providerSpec.Network.Devices), len(currentNetworkStatusList)
if expectNetworkLen != currentNetworkLen {
return fmt.Errorf("invalid network count: expected=%d current=%d", expectNetworkLen, currentNetworkLen)
}
var ipAddrs []corev1.NodeAddress
for _, netStatus := range currentNetworkStatusList {
for _, ip := range netStatus.IPAddrs {
ipAddrs = append(ipAddrs, corev1.NodeAddress{
Type: corev1.NodeInternalIP,
Address: ip,
})
}
}
// Using Name() if InventoryPath is empty will return empty name
// see: https://github.com/vmware/govmomi/blob/master/object/common.go#L66-L75
// Using ObjectName() as it will query from VirtualMachine properties
vmName, err := vm.Obj.ObjectName(vm.Context)
if err != nil {
return fmt.Errorf("error getting virtual machine name: %v", err)
}
ipAddrs = append(ipAddrs, corev1.NodeAddress{
Type: corev1.NodeInternalDNS,
Address: vmName,
})
klog.V(3).Infof("%v: reconciling network: IP addresses: %v", r.machine.GetName(), ipAddrs)
r.machine.Status.Addresses = ipAddrs
// If static IP, verify machine still has IPAddressClaim w/ owner field configure
if ipam.HasStaticIPConfiguration(r.providerSpec) {
err = ipam.VerifyIPAddressOwners(r.Context, r.client, r.machine, r.providerSpec.Network.Devices)
if err != nil {
return fmt.Errorf("error verifying ip address claims: %v", err)
}
}
return nil
}
func (r *Reconciler) reconcilePowerStateAnnontation(vm *virtualMachine) error {
if vm == nil {
return errors.New("provided VM is nil")
}
// This can return an error if machine is being deleted
powerState, err := vm.getPowerState()
if err != nil {
return err
}
if r.machine.Annotations == nil {
r.machine.Annotations = map[string]string{}
}
r.machine.Annotations[machinecontroller.MachineInstanceStateAnnotationName] = string(powerState)
return nil
}
func validateMachine(machine machinev1.Machine) error {
if machine.Labels[machinev1.MachineClusterIDLabel] == "" {
return machinecontroller.InvalidMachineConfiguration("%v: missing %q label", machine.GetName(), machinev1.MachineClusterIDLabel)
}
return nil
}
func findVM(s *machineScope) (types.ManagedObjectReference, error) {
uuid := string(s.machine.UID)
vm, err := s.GetSession().FindVM(s.Context, uuid, s.machine.Name)
if err != nil {
if isNotFound(err) {
return types.ManagedObjectReference{}, errNotFound{instanceUUID: true, uuid: uuid}
}
return types.ManagedObjectReference{}, err
}
if vm == nil {
return types.ManagedObjectReference{}, errNotFound{instanceUUID: true, uuid: uuid}
}
return vm.Reference(), nil
}
// errNotFound is returned by the findVM function when a VM is not found.
type errNotFound struct {
instanceUUID bool
uuid string
}
func (e errNotFound) Error() string {
if e.instanceUUID {
return fmt.Sprintf("vm with instance uuid %s not found", e.uuid)
}
return fmt.Sprintf("vm with bios uuid %s not found", e.uuid)
}
func isNotFound(err error) bool {
switch err.(type) {
case errNotFound, *errNotFound, *find.NotFoundError:
return true
default:
return false
}
}
func getSubnetMask(prefix netip.Prefix) (string, error) {
prefixLength := net.IPv4len * 8
if prefix.Addr().Is6() {
prefixLength = net.IPv6len * 8
}
ipMask := net.CIDRMask(prefix.Masked().Bits(), prefixLength)
maskBytes, err := hex.DecodeString(ipMask.String())
if err != nil {
return "", fmt.Errorf("could not translate ip mask: %w", err)
}
ip := net.IP(maskBytes)
maskStr := ip.To16().String()
return maskStr, nil
}
// getAddressesFromPool retrieves IP addresses and associated gateway from IP address pools
func getAddressesFromPool(configIdx int, networkConfig machinev1.NetworkDeviceSpec, s *machineScope) ([]string, string, error) {
addresses := []string{}
var gateway string
for poolIdx := range networkConfig.AddressesFromPools {
claimName := ipam.GetIPAddressClaimName(s.machine, configIdx, poolIdx)
ipAddress, err := ipam.RetrieveBoundIPAddress(s.Context, s.client, s.machine, claimName)
if err != nil {
return nil, "", fmt.Errorf("error retrieving bound IP address: %w", err)
}
ipAddressSpec := ipAddress.Spec
addresses = append(addresses, fmt.Sprintf("%s/%d", ipAddressSpec.Address, ipAddressSpec.Prefix))
if len(ipAddressSpec.Gateway) > 0 {
gateway = ipAddressSpec.Gateway
}
}
return addresses, gateway, nil
}
// constructKargsFromNetworkConfig builds a string which comprises ip and nameserver stanzas
// which are consumed by guestinfo.afterburn.initrd.network-kargs.
func constructKargsFromNetworkConfig(s *machineScope) (string, error) {
outKargs := ""
networkConfigs := s.providerSpec.Network.Devices
for configIdx, networkConfig := range networkConfigs {
// retrieve any IP addresses assigned by an IP address pool
addressesFromPool, gatewayFromPool, err := getAddressesFromPool(configIdx, networkConfig, s)
if err != nil {
return "", fmt.Errorf("error getting addresses from IP pool: %w", err)
}
var gateway string
if len(gatewayFromPool) > 0 {
gateway = gatewayFromPool
} else {
gateway = networkConfig.Gateway
}
var gatewayIp netip.Addr
if len(gateway) > 0 {
gatewayIp, err = netip.ParseAddr(gateway)
if err != nil {
return "", fmt.Errorf("error parsing gateway address: %w", err)
}
}
ipAddresses := []string{}
ipAddresses = append(ipAddresses, networkConfig.IPAddrs...)
ipAddresses = append(ipAddresses, addressesFromPool...)
// construct IP address network kargs for each IP address
for _, address := range ipAddresses {
prefix, err := netip.ParsePrefix(address)
if err != nil {
return "", fmt.Errorf("error parsing prefix: %w", err)
}
var ipStr, gatewayStr, maskStr string
addr := prefix.Addr()
// IPv6 addresses must be wrapped in [] for dracut network kargs
if addr.Is6() {
maskStr = fmt.Sprintf("%d", prefix.Bits())
ipStr = fmt.Sprintf("[%s]", addr.String())
if len(gateway) > 0 && gatewayIp.Is6() {
gatewayStr = fmt.Sprintf("[%s]", gateway)
}
} else if addr.Is4() {
maskStr, err = getSubnetMask(prefix)
if err != nil {
return "", fmt.Errorf("error getting subnet mask: %w", err)
}
if len(gateway) > 0 && gatewayIp.Is4() {
gatewayStr = gateway
}
ipStr = addr.String()
} else {
return "", errors.New("IP address must adhere to IPv4 or IPv6 format")
}
outKargs = outKargs + fmt.Sprintf("ip=%s::%s:%s:::none ", ipStr, gatewayStr, maskStr)
}
// construct nameserver network karg for each defined nameserver
for _, nameserver := range networkConfig.Nameservers {
ip := net.ParseIP(nameserver)
if ip.To4() == nil {
nameserver = fmt.Sprintf("[%s]", nameserver)
}
outKargs = outKargs + fmt.Sprintf("nameserver=%s ", nameserver)
}
}
return outKargs, nil
}
func isRetrieveMONotFound(taskRef string, err error) bool {
return err.Error() == fmt.Sprintf("ServerFaultCode: The object 'vim.Task:%v' has already been deleted or has not been completely created", taskRef)
}
func getHwVersion(ctx context.Context, vm *object.VirtualMachine) (int, error) {
var _vm mo.VirtualMachine
if err := vm.Properties(ctx, vm.Reference(), []string{"config.version"}, &_vm); err != nil {
return 0, fmt.Errorf("error getting hw version information for vm %s: %w", vm.Name(), err)
}
versionString := _vm.Config.Version
version := strings.TrimPrefix(versionString, "vmx-")
parsedVersion, err := strconv.Atoi(version)
if err != nil {
return 0, fmt.Errorf("can not extract hardware version from version string: %s, format unknown", versionString)
}
return parsedVersion, nil
}
func clone(s *machineScope) (string, error) {
userData, err := s.GetUserData()
if err != nil {
return "", err
}
vmTemplate, err := s.GetSession().FindVM(*s, "", s.providerSpec.Template)
if err != nil {
const multipleFoundMsg = "multiple templates found, specify one in config"
const notFoundMsg = "template not found, specify valid value"
defaultError := fmt.Errorf("unable to get template %q: %w", s.providerSpec.Template, err)
return "", handleVSphereError(multipleFoundMsg, notFoundMsg, defaultError, err)
}
hwVersion, err := getHwVersion(s.Context, vmTemplate)
if err != nil {
return "", machinecontroller.InvalidMachineConfiguration(
"Unable to detect machine template HW version for machine '%s': %v", s.machine.GetName(), err,
)
}
if hwVersion < minimumHWVersion {
return "", machinecontroller.InvalidMachineConfiguration(
"Hardware lower than %d is not supported, clone stopped. "+
"Detected machine template version is %d. "+
"Please update machine template: https://docs.openshift.com/container-platform/latest/updating/updating_a_cluster/updating-hardware-on-nodes-running-on-vsphere.html",
minimumHWVersion, hwVersion,
)
}
// Default clone type is FullClone, having snapshot on clonee template will cause incorrect disk sizing.
diskMoveType := fullCloneDiskMoveType
var snapshotRef *types.ManagedObjectReference
// If a linked clone is requested then a MoRef for a snapshot must be
// found with which to perform the linked clone.
// Empty clone mode is a full clone,
// because otherwise disk size from provider spec will not be respected.
if s.providerSpec.CloneMode == machinev1.LinkedClone {
if s.providerSpec.DiskGiB > 0 {
klog.Warningf("LinkedClone mode is set. Disk size parameter from ProviderSpec will be ignored")
}
if s.providerSpec.Snapshot == "" {
klog.V(3).Infof("%v: no snapshot name provided, getting snapshot using template", s.machine.GetName())