diff --git a/pkg/constants/testlabels/test_labels.go b/pkg/constants/testlabels/test_labels.go index 38f397fb1..640ae57a6 100644 --- a/pkg/constants/testlabels/test_labels.go +++ b/pkg/constants/testlabels/test_labels.go @@ -26,6 +26,9 @@ const ( // Fuzz describes a fuzz test. Fuzz = "fuzz" + // Group describes a test related to group logic. + Group = "group" + // Mutation describes a test related to a mutation webhook. Mutation = "mutation" @@ -35,6 +38,9 @@ const ( // Service describes a test related to a service (non-Controller runnable). Service = "service" + // Snapshot describes a test related to snapshot logic. + Snapshot = "snapshot" + // Update describes a test related to update logic. Update = "update" @@ -59,6 +65,9 @@ const ( // VCSim describes a test that uses vC Sim. VCSim = "vcsim" + // VKS describes a test related to VKS. + VKS = "vks" + // Webhook describes a test related to a webhook. Webhook = "webhook" ) diff --git a/pkg/providers/vsphere/vmprovider_cpu_freq_test.go b/pkg/providers/vsphere/vmprovider_cpu_freq_test.go new file mode 100644 index 000000000..97a2e1529 --- /dev/null +++ b/pkg/providers/vsphere/vmprovider_cpu_freq_test.go @@ -0,0 +1,43 @@ +// © Broadcom. All Rights Reserved. +// The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package vsphere_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/vmware-tanzu/vm-operator/pkg/providers" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" + "github.com/vmware-tanzu/vm-operator/test/builder" +) + +var _ = Describe("CPU Frequency", func() { + var ( + testConfig builder.VCSimTestConfig + ctx *builder.TestContextForVCSim + vmProvider providers.VirtualMachineProviderInterface + ) + + BeforeEach(func() { + testConfig = builder.VCSimTestConfig{} + }) + + JustBeforeEach(func() { + ctx = suite.NewTestContextForVCSim(testConfig) + vmProvider = vsphere.NewVSphereVMProviderFromClient(ctx, ctx.Client, ctx.Recorder) + }) + + AfterEach(func() { + ctx.AfterEach() + ctx = nil + vmProvider = nil + }) + + Context("ComputeCPUMinFrequency", func() { + It("returns success", func() { + Expect(vmProvider.ComputeCPUMinFrequency(ctx)).To(Succeed()) + }) + }) +}) diff --git a/pkg/providers/vsphere/vmprovider_resourcepolicy_test.go b/pkg/providers/vsphere/vmprovider_resourcepolicy_test.go index 48c17d423..338982d2c 100644 --- a/pkg/providers/vsphere/vmprovider_resourcepolicy_test.go +++ b/pkg/providers/vsphere/vmprovider_resourcepolicy_test.go @@ -39,196 +39,194 @@ func getVirtualMachineSetResourcePolicy(name, namespace string) *vmopv1.VirtualM } } -func resourcePolicyTests() { - Describe("VirtualMachineSetResourcePolicy Tests", func() { - - var ( - initObjects []client.Object - ctx *builder.TestContextForVCSim - nsInfo builder.WorkloadNamespaceInfo - testConfig builder.VCSimTestConfig - vmProvider providers.VirtualMachineProviderInterface - ) +var _ = Describe("VirtualMachineSetResourcePolicy Tests", func() { + + var ( + initObjects []client.Object + ctx *builder.TestContextForVCSim + nsInfo builder.WorkloadNamespaceInfo + testConfig builder.VCSimTestConfig + vmProvider providers.VirtualMachineProviderInterface + ) + + BeforeEach(func() { + testConfig = builder.VCSimTestConfig{ + NumFaultDomains: 3, + } + }) - BeforeEach(func() { - testConfig = builder.VCSimTestConfig{ - NumFaultDomains: 3, - } - }) + JustBeforeEach(func() { + ctx = suite.NewTestContextForVCSim(testConfig, initObjects...) + vmProvider = vsphere.NewVSphereVMProviderFromClient(ctx, ctx.Client, ctx.Recorder) + nsInfo = ctx.CreateWorkloadNamespace() + }) - JustBeforeEach(func() { - ctx = suite.NewTestContextForVCSim(testConfig, initObjects...) - vmProvider = vsphere.NewVSphereVMProviderFromClient(ctx, ctx.Client, ctx.Recorder) - nsInfo = ctx.CreateWorkloadNamespace() - }) + AfterEach(func() { + ctx.AfterEach() + ctx = nil + initObjects = nil + }) - AfterEach(func() { - ctx.AfterEach() - ctx = nil - initObjects = nil - }) + assertSetResourcePolicy := func(rp *vmopv1.VirtualMachineSetResourcePolicy, expectedExists bool) { + if folderName := rp.Spec.Folder; folderName != "" { + exists, err := vcenter.DoesChildFolderExist(ctx, ctx.VCClient.Client, nsInfo.Folder.Reference().Value, folderName) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(Equal(expectedExists)) + } - assertSetResourcePolicy := func(rp *vmopv1.VirtualMachineSetResourcePolicy, expectedExists bool) { - if folderName := rp.Spec.Folder; folderName != "" { - exists, err := vcenter.DoesChildFolderExist(ctx, ctx.VCClient.Client, nsInfo.Folder.Reference().Value, folderName) - Expect(err).ToNot(HaveOccurred()) - Expect(exists).To(Equal(expectedExists)) + if rpName := rp.Spec.ResourcePool.Name; rpName != "" { + if expectedExists { + expectedCnt := ctx.ClustersPerZone * ctx.ZoneCount + Expect(rp.Status.ResourcePools).To(HaveLen(expectedCnt)) } - if rpName := rp.Spec.ResourcePool.Name; rpName != "" { - if expectedExists { - expectedCnt := ctx.ClustersPerZone * ctx.ZoneCount - Expect(rp.Status.ResourcePools).To(HaveLen(expectedCnt)) - } + for _, zoneName := range ctx.ZoneNames { + nsRP := ctx.GetResourcePoolForNamespace(rp.Namespace, zoneName, "") - for _, zoneName := range ctx.ZoneNames { - nsRP := ctx.GetResourcePoolForNamespace(rp.Namespace, zoneName, "") - - childRP, err := vcenter.GetChildResourcePool(ctx, nsRP, rpName) - if expectedExists { - Expect(err).ToNot(HaveOccurred()) - Expect(childRP).ToNot(BeNil()) - - ccr, err := nsRP.Owner(ctx) - Expect(err).ToNot(HaveOccurred()) - - Expect(rp.Status.ResourcePools).To(ContainElement(vmopv1.ResourcePoolStatus{ - ClusterMoID: ccr.Reference().Value, - ChildResourcePoolMoID: childRP.Reference().Value, - })) - } else { - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("not found under parent ResourcePool")) - } + childRP, err := vcenter.GetChildResourcePool(ctx, nsRP, rpName) + if expectedExists { + Expect(err).ToNot(HaveOccurred()) + Expect(childRP).ToNot(BeNil()) + + ccr, err := nsRP.Owner(ctx) + Expect(err).ToNot(HaveOccurred()) + + Expect(rp.Status.ResourcePools).To(ContainElement(vmopv1.ResourcePoolStatus{ + ClusterMoID: ccr.Reference().Value, + ChildResourcePoolMoID: childRP.Reference().Value, + })) + } else { + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("not found under parent ResourcePool")) } - } else { - Expect(rp.Status.ResourcePools).To(BeEmpty()) } + } else { + Expect(rp.Status.ResourcePools).To(BeEmpty()) + } - clusterModules, err := cluster.NewManager(ctx.RestClient).ListModules(ctx) - Expect(err).ToNot(HaveOccurred()) + clusterModules, err := cluster.NewManager(ctx.RestClient).ListModules(ctx) + Expect(err).ToNot(HaveOccurred()) - if expectedExists { - expectedCnt := len(rp.Spec.ClusterModuleGroups) * ctx.ClustersPerZone * ctx.ZoneCount - Expect(rp.Status.ClusterModules).To(HaveLen(expectedCnt)) - Expect(clusterModules).To(HaveLen(expectedCnt)) + if expectedExists { + expectedCnt := len(rp.Spec.ClusterModuleGroups) * ctx.ClustersPerZone * ctx.ZoneCount + Expect(rp.Status.ClusterModules).To(HaveLen(expectedCnt)) + Expect(clusterModules).To(HaveLen(expectedCnt)) - cmMap := map[string]struct{}{} - cmUUID := map[string]struct{}{} + cmMap := map[string]struct{}{} + cmUUID := map[string]struct{}{} - for _, cmStatus := range rp.Status.ClusterModules { - k := cmStatus.GroupName + "::" + cmStatus.ClusterMoID - Expect(cmMap).ToNot(HaveKey(k)) - cmMap[k] = struct{}{} + for _, cmStatus := range rp.Status.ClusterModules { + k := cmStatus.GroupName + "::" + cmStatus.ClusterMoID + Expect(cmMap).ToNot(HaveKey(k)) + cmMap[k] = struct{}{} - Expect(cmUUID).ToNot(HaveKey(cmStatus.ModuleUuid)) - cmUUID[cmStatus.ModuleUuid] = struct{}{} + Expect(cmUUID).ToNot(HaveKey(cmStatus.ModuleUuid)) + cmUUID[cmStatus.ModuleUuid] = struct{}{} - expectedSummary := cluster.ModuleSummary{ - Cluster: cmStatus.ClusterMoID, - Module: cmStatus.ModuleUuid, - } - Expect(clusterModules).To(ContainElement(expectedSummary)) + expectedSummary := cluster.ModuleSummary{ + Cluster: cmStatus.ClusterMoID, + Module: cmStatus.ModuleUuid, } + Expect(clusterModules).To(ContainElement(expectedSummary)) + } - // Check that each module was created for each CCR. - for _, zoneName := range ctx.ZoneNames { - ccrs := ctx.GetAZClusterComputes(zoneName) - Expect(ccrs).ToNot(BeEmpty()) - for _, ccr := range ccrs { - for _, cmName := range rp.Spec.ClusterModuleGroups { - k := cmName + "::" + ccr.Reference().Value - Expect(cmMap).To(HaveKey(k)) - } + // Check that each module was created for each CCR. + for _, zoneName := range ctx.ZoneNames { + ccrs := ctx.GetAZClusterComputes(zoneName) + Expect(ccrs).ToNot(BeEmpty()) + for _, ccr := range ccrs { + for _, cmName := range rp.Spec.ClusterModuleGroups { + k := cmName + "::" + ccr.Reference().Value + Expect(cmMap).To(HaveKey(k)) } } - - } else { - Expect(rp.Status.ClusterModules).To(BeEmpty()) - Expect(clusterModules).To(BeEmpty()) } - } - - Context("Empty VirtualMachineSetResourcePolicy", func() { - It("Creates and Deletes successfully", func() { - resourcePolicy := &vmopv1.VirtualMachineSetResourcePolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "empty-policy", - Namespace: nsInfo.Namespace, - }, - } - - By("Create", func() { - Expect(vmProvider.CreateOrUpdateVirtualMachineSetResourcePolicy(ctx, resourcePolicy)).To(Succeed()) - assertSetResourcePolicy(resourcePolicy, true) - }) - By("Delete", func() { - Expect(vmProvider.DeleteVirtualMachineSetResourcePolicy(ctx, resourcePolicy)).To(Succeed()) - assertSetResourcePolicy(resourcePolicy, false) - }) - }) - }) + } else { + Expect(rp.Status.ClusterModules).To(BeEmpty()) + Expect(clusterModules).To(BeEmpty()) + } + } - Context("VirtualMachineSetResourcePolicy", func() { - var ( - resourcePolicy *vmopv1.VirtualMachineSetResourcePolicy - ) + Context("Empty VirtualMachineSetResourcePolicy", func() { + It("Creates and Deletes successfully", func() { + resourcePolicy := &vmopv1.VirtualMachineSetResourcePolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "empty-policy", + Namespace: nsInfo.Namespace, + }, + } - JustBeforeEach(func() { - resourcePolicy = getVirtualMachineSetResourcePolicy("test-policy", nsInfo.Namespace) + By("Create", func() { Expect(vmProvider.CreateOrUpdateVirtualMachineSetResourcePolicy(ctx, resourcePolicy)).To(Succeed()) + assertSetResourcePolicy(resourcePolicy, true) }) - JustAfterEach(func() { + By("Delete", func() { Expect(vmProvider.DeleteVirtualMachineSetResourcePolicy(ctx, resourcePolicy)).To(Succeed()) assertSetResourcePolicy(resourcePolicy, false) }) + }) + }) - It("creates expected resource policy", func() { - assertSetResourcePolicy(resourcePolicy, true) - }) + Context("VirtualMachineSetResourcePolicy", func() { + var ( + resourcePolicy *vmopv1.VirtualMachineSetResourcePolicy + ) + + JustBeforeEach(func() { + resourcePolicy = getVirtualMachineSetResourcePolicy("test-policy", nsInfo.Namespace) + Expect(vmProvider.CreateOrUpdateVirtualMachineSetResourcePolicy(ctx, resourcePolicy)).To(Succeed()) + }) - Context("for an existing resource policy", func() { - It("should keep existing cluster modules", func() { - assertSetResourcePolicy(resourcePolicy, true) - status := resourcePolicy.Status.DeepCopy() + JustAfterEach(func() { + Expect(vmProvider.DeleteVirtualMachineSetResourcePolicy(ctx, resourcePolicy)).To(Succeed()) + assertSetResourcePolicy(resourcePolicy, false) + }) - Expect(vmProvider.CreateOrUpdateVirtualMachineSetResourcePolicy(ctx, resourcePolicy)).To(Succeed()) - Expect(resourcePolicy.Status.ClusterModules).To(HaveExactElements(status.ClusterModules)) - assertSetResourcePolicy(resourcePolicy, true) - }) + It("creates expected resource policy", func() { + assertSetResourcePolicy(resourcePolicy, true) + }) + + Context("for an existing resource policy", func() { + It("should keep existing cluster modules", func() { + assertSetResourcePolicy(resourcePolicy, true) + status := resourcePolicy.Status.DeepCopy() + + Expect(vmProvider.CreateOrUpdateVirtualMachineSetResourcePolicy(ctx, resourcePolicy)).To(Succeed()) + Expect(resourcePolicy.Status.ClusterModules).To(HaveExactElements(status.ClusterModules)) + assertSetResourcePolicy(resourcePolicy, true) }) + }) + + Context("for a resource policy with invalid cluster module", func() { + It("successfully able to delete the resource policy", func() { + assertSetResourcePolicy(resourcePolicy, true) - Context("for a resource policy with invalid cluster module", func() { - It("successfully able to delete the resource policy", func() { - assertSetResourcePolicy(resourcePolicy, true) - - resourcePolicy.Status.ClusterModules = append([]vmopv1.VSphereClusterModuleStatus{ - { - GroupName: "invalid-group", - ModuleUuid: "invalid-uuid", - }, - }, resourcePolicy.Status.ClusterModules...) - }) + resourcePolicy.Status.ClusterModules = append([]vmopv1.VSphereClusterModuleStatus{ + { + GroupName: "invalid-group", + ModuleUuid: "invalid-uuid", + }, + }, resourcePolicy.Status.ClusterModules...) }) + }) - It("should claim cluster module without ClusterMoID set", func() { - Expect(resourcePolicy.Spec.ClusterModuleGroups).ToNot(BeEmpty()) - groupName := resourcePolicy.Spec.ClusterModuleGroups[0] + It("should claim cluster module without ClusterMoID set", func() { + Expect(resourcePolicy.Spec.ClusterModuleGroups).ToNot(BeEmpty()) + groupName := resourcePolicy.Spec.ClusterModuleGroups[0] - moduleStatus := resourcePolicy.Status.DeepCopy() - Expect(moduleStatus.ClusterModules).ToNot(BeEmpty()) + moduleStatus := resourcePolicy.Status.DeepCopy() + Expect(moduleStatus.ClusterModules).ToNot(BeEmpty()) - for i := range resourcePolicy.Status.ClusterModules { - if resourcePolicy.Status.ClusterModules[i].GroupName == groupName { - resourcePolicy.Status.ClusterModules[i].ClusterMoID = "" - } + for i := range resourcePolicy.Status.ClusterModules { + if resourcePolicy.Status.ClusterModules[i].GroupName == groupName { + resourcePolicy.Status.ClusterModules[i].ClusterMoID = "" } - Expect(vmProvider.CreateOrUpdateVirtualMachineSetResourcePolicy(ctx, resourcePolicy)).To(Succeed()) - Expect(resourcePolicy.Status.ClusterModules).To(Equal(moduleStatus.ClusterModules)) - assertSetResourcePolicy(resourcePolicy, true) - }) + } + Expect(vmProvider.CreateOrUpdateVirtualMachineSetResourcePolicy(ctx, resourcePolicy)).To(Succeed()) + Expect(resourcePolicy.Status.ClusterModules).To(Equal(moduleStatus.ClusterModules)) + assertSetResourcePolicy(resourcePolicy, true) }) }) -} +}) diff --git a/pkg/providers/vsphere/vmprovider_sync_vmi_test.go b/pkg/providers/vsphere/vmprovider_sync_vmi_test.go new file mode 100644 index 000000000..f8b80f13f --- /dev/null +++ b/pkg/providers/vsphere/vmprovider_sync_vmi_test.go @@ -0,0 +1,330 @@ +// © Broadcom. All Rights Reserved. +// The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package vsphere_test + +import ( + "errors" + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + + imgregv1a1 "github.com/vmware-tanzu/image-registry-operator-api/api/v1alpha1" + imgregv1 "github.com/vmware-tanzu/image-registry-operator-api/api/v1alpha2" + + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha6" + pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + pkgerr "github.com/vmware-tanzu/vm-operator/pkg/errors" + "github.com/vmware-tanzu/vm-operator/pkg/providers" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" + "github.com/vmware-tanzu/vm-operator/pkg/util" + "github.com/vmware-tanzu/vm-operator/test/builder" +) + +var _ = Describe("SyncVirtualMachineImage", func() { + var ( + ctx *builder.TestContextForVCSim + testConfig builder.VCSimTestConfig + vmProvider providers.VirtualMachineProviderInterface + ) + + BeforeEach(func() { + testConfig.WithContentLibrary = true + ctx = suite.NewTestContextForVCSim(testConfig) + vmProvider = vsphere.NewVSphereVMProviderFromClient(ctx, ctx.Client, ctx.Recorder) + }) + + AfterEach(func() { + ctx.AfterEach() + }) + + When("content library item is an unexpected K8s object type", func() { + It("should return an error", func() { + err := vmProvider.SyncVirtualMachineImage(ctx, &imgregv1a1.ContentLibrary{}, &vmopv1.VirtualMachineImage{}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(fmt.Sprintf("unexpected content library item K8s object type %T", &imgregv1a1.ContentLibrary{}))) + }) + }) + + When("content library item is a v1alpha2 type", func() { + It("should not return an error", func() { + err := vmProvider.SyncVirtualMachineImage(ctx, &imgregv1.ContentLibraryItem{}, &vmopv1.VirtualMachineImage{}) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + When("content library item is not an OVF type", func() { + It("should return early without updating VM Image status", func() { + isoItem := &imgregv1a1.ContentLibraryItem{ + Status: imgregv1a1.ContentLibraryItemStatus{ + Type: imgregv1a1.ContentLibraryItemTypeIso, + }, + } + var vmi vmopv1.VirtualMachineImage + Expect(vmProvider.SyncVirtualMachineImage(ctx, isoItem, &vmi)).To(Succeed()) + Expect(vmi.Status).To(Equal(vmopv1.VirtualMachineImageStatus{})) + }) + }) + + When("content library item is an OVF type", func() { + // TODO(akutz) Promote this block when the FSS WCP_VMService_FastDeploy is + // removed. + When("FSS WCP_VMService_FastDeploy is enabled", func() { + + var ( + err error + cli imgregv1a1.ContentLibraryItem + vmi1 vmopv1.VirtualMachineImage + vmic1 vmopv1.VirtualMachineImageCache + vmicm1 corev1.ConfigMap + createVMIC1 bool + ) + + BeforeEach(func() { + pkgcfg.UpdateContext(ctx, func(config *pkgcfg.Config) { + config.Features.FastDeploy = true + }) + + cli = imgregv1a1.ContentLibraryItem{ + Spec: imgregv1a1.ContentLibraryItemSpec{ + UUID: types.UID(ctx.ContentLibraryItem1ID), + }, + Status: imgregv1a1.ContentLibraryItemStatus{ + ContentVersion: "v1", + Type: imgregv1a1.ContentLibraryItemTypeOvf, + }, + } + + vmi1 = vmopv1.VirtualMachineImage{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "my-namespace", + Name: "my-vmi", + }, + } + + createVMIC1 = true + vmicName := util.VMIName(ctx.ContentLibraryItem1ID) + vmic1 = vmopv1.VirtualMachineImageCache{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: pkgcfg.FromContext(ctx).PodNamespace, + Name: vmicName, + }, + Status: vmopv1.VirtualMachineImageCacheStatus{ + OVF: &vmopv1.VirtualMachineImageCacheOVFStatus{ + ConfigMapName: vmicName, + ProviderVersion: "v1", + }, + Conditions: []metav1.Condition{ + { + Type: vmopv1.VirtualMachineImageCacheConditionHardwareReady, + Status: metav1.ConditionTrue, + }, + }, + }, + } + + vmicm1 = corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: vmic1.Namespace, + Name: vmic1.Name, + }, + Data: map[string]string{ + "value": ctx.ContentLibraryItem1YAML, + }, + } + Expect(ctx.Client.Create(ctx, &vmicm1)).To(Succeed()) + }) + + JustBeforeEach(func() { + if createVMIC1 { + status := vmic1.Status.DeepCopy() + Expect(ctx.Client.Create(ctx, &vmic1)).To(Succeed()) + vmic1.Status = *status + Expect(ctx.Client.Status().Update(ctx, &vmic1)).To(Succeed()) + } + err = vmProvider.SyncVirtualMachineImage(ctx, &cli, &vmi1) + }) + + When("it fails to createOrPatch the VMICache resource", func() { + // TODO(akutz) Add interceptors to the vcSim test context so + // this can be tested. + XIt("should return an error", func() { + + }) + }) + + assertVMICExists := func(namespace, name string) { + var ( + obj vmopv1.VirtualMachineImageCache + key = ctrlclient.ObjectKey{ + Namespace: namespace, + Name: name, + } + ) + ExpectWithOffset(1, ctx.Client.Get(ctx, key, &obj)).To(Succeed()) + } + + assertVMICNotReady := func(err error, name string) { + var e pkgerr.VMICacheNotReadyError + ExpectWithOffset(1, errors.As(err, &e)).To(BeTrue()) + ExpectWithOffset(1, e.Name).To(Equal(name)) + } + + When("OVF condition is False", func() { + BeforeEach(func() { + vmic1.Status.Conditions[0].Status = metav1.ConditionFalse + vmic1.Status.Conditions[0].Message = "fubar" + }) + It("should return an error", func() { + Expect(err).To(MatchError("failed to get hardware: fubar: cache not ready")) + }) + }) + + When("OVF is not ready", func() { + When("condition is missing", func() { + BeforeEach(func() { + createVMIC1 = false + }) + It("should return ErrVMICacheNotReady", func() { + assertVMICExists(vmic1.Namespace, vmic1.Name) + assertVMICNotReady(err, vmic1.Name) + }) + }) + When("condition is unknown", func() { + BeforeEach(func() { + vmic1.Status.Conditions[0].Status = metav1.ConditionUnknown + }) + It("should return ErrVMICacheNotReady", func() { + assertVMICNotReady(err, vmic1.Name) + }) + }) + When("status.ovf is nil", func() { + BeforeEach(func() { + vmic1.Status.OVF = nil + }) + It("should return ErrVMICacheNotReady", func() { + assertVMICNotReady(err, vmic1.Name) + }) + }) + When("status.ovf.providerVersion does not match expected version", func() { + BeforeEach(func() { + vmic1.Status.OVF.ProviderVersion = "" + }) + It("should return ErrVMICacheNotReady", func() { + assertVMICNotReady(err, vmic1.Name) + }) + }) + When("configmap is missing", func() { + BeforeEach(func() { + Expect(ctx.Client.Delete(ctx, &vmicm1)).To(Succeed()) + }) + It("should return ErrVMICacheNotReady", func() { + assertVMICNotReady(err, vmic1.Name) + }) + }) + }) + + When("OVF is ready", func() { + When("marshaled ovf data is invalid", func() { + BeforeEach(func() { + vmicm1.Data["value"] = "invalid" + Expect(ctx.Client.Update(ctx, &vmicm1)).To(Succeed()) + }) + It("should return an error", func() { + Expect(err).To(MatchError("failed to unmarshal ovf yaml into envelope: " + + "error unmarshaling JSON: while decoding JSON: " + + "json: cannot unmarshal string into Go value of type ovf.Envelope")) + }) + }) + When("marshaled ovf data is valid", func() { + It("should return success and update VM Image status accordingly", func() { + Expect(err).ToNot(HaveOccurred()) + Expect(vmi1.Status.Firmware).To(Equal("efi")) + Expect(vmi1.Status.HardwareVersion).NotTo(BeNil()) + Expect(*vmi1.Status.HardwareVersion).To(Equal(int32(9))) + Expect(vmi1.Status.OSInfo.ID).To(Equal("36")) + Expect(vmi1.Status.OSInfo.Type).To(Equal("otherLinuxGuest")) + Expect(vmi1.Status.Disks).To(HaveLen(1)) + Expect(vmi1.Status.Disks[0].Limit.String()).To(Equal("30Mi")) + Expect(vmi1.Status.Disks[0].Requested.String()).To(Equal("30Mi")) + }) + }) + + }) + }) + + // TODO(akutz) Remove this block when the FSS WCP_VMService_FastDeploy is + // removed. + When("FSS WCP_VMService_FastDeploy is disabled", func() { + + BeforeEach(func() { + pkgcfg.UpdateContext(ctx, func(config *pkgcfg.Config) { + config.Features.FastDeploy = false + }) + }) + + When("it fails to get the OVF envelope", func() { + It("should return an error", func() { + cli := &imgregv1a1.ContentLibraryItem{ + Spec: imgregv1a1.ContentLibraryItemSpec{ + // Use an invalid item ID to fail to get the OVF envelope. + UUID: "invalid-library-ID", + }, + Status: imgregv1a1.ContentLibraryItemStatus{ + Type: imgregv1a1.ContentLibraryItemTypeOvf, + }, + } + err := vmProvider.SyncVirtualMachineImage(ctx, cli, &vmopv1.VirtualMachineImage{}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to get OVF envelope for library item \"invalid-library-ID\"")) + }) + }) + + When("OVF envelope is nil", func() { + It("should return an error", func() { + ovfItem := &imgregv1a1.ContentLibraryItem{ + Spec: imgregv1a1.ContentLibraryItemSpec{ + UUID: types.UID(ctx.ContentLibraryIsoItemID), + }, + Status: imgregv1a1.ContentLibraryItemStatus{ + Type: imgregv1a1.ContentLibraryItemTypeOvf, + }, + } + err := vmProvider.SyncVirtualMachineImage(ctx, ovfItem, &vmopv1.VirtualMachineImage{}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(fmt.Sprintf("OVF envelope is nil for library item %q", ctx.ContentLibraryIsoItemID))) + }) + }) + + When("there is a valid OVF envelope", func() { + It("should return success and update VM Image status accordingly", func() { + cli := &imgregv1a1.ContentLibraryItem{ + Spec: imgregv1a1.ContentLibraryItemSpec{ + UUID: types.UID(ctx.ContentLibraryItem1ID), + }, + Status: imgregv1a1.ContentLibraryItemStatus{ + Type: imgregv1a1.ContentLibraryItemTypeOvf, + }, + } + var vmi vmopv1.VirtualMachineImage + Expect(vmProvider.SyncVirtualMachineImage(ctx, cli, &vmi)).To(Succeed()) + Expect(vmi.Status.Firmware).To(Equal("efi")) + Expect(vmi.Status.HardwareVersion).NotTo(BeNil()) + Expect(*vmi.Status.HardwareVersion).To(Equal(int32(9))) + Expect(vmi.Status.OSInfo.ID).To(Equal("36")) + Expect(vmi.Status.OSInfo.Type).To(Equal("otherLinuxGuest")) + Expect(vmi.Status.Disks).To(HaveLen(1)) + Expect(vmi.Status.Disks[0].Limit.String()).To(Equal("30Mi")) + Expect(vmi.Status.Disks[0].Requested.String()).To(Equal("30Mi")) + }) + }) + }) + }) +}) diff --git a/pkg/providers/vsphere/vmprovider_test.go b/pkg/providers/vsphere/vmprovider_test.go index 2de9634c9..6075b85be 100644 --- a/pkg/providers/vsphere/vmprovider_test.go +++ b/pkg/providers/vsphere/vmprovider_test.go @@ -5,431 +5,181 @@ package vsphere_test import ( + "context" "errors" "fmt" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" - - imgregv1a1 "github.com/vmware-tanzu/image-registry-operator-api/api/v1alpha1" - imgregv1 "github.com/vmware-tanzu/image-registry-operator-api/api/v1alpha2" + "github.com/vmware/govmomi/object" + "sigs.k8s.io/controller-runtime/pkg/client" vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha6" pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" - pkgerr "github.com/vmware-tanzu/vm-operator/pkg/errors" + ctxop "github.com/vmware-tanzu/vm-operator/pkg/context/operation" "github.com/vmware-tanzu/vm-operator/pkg/providers" "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" - "github.com/vmware-tanzu/vm-operator/pkg/util" "github.com/vmware-tanzu/vm-operator/test/builder" ) -func cpuFreqTests() { - - var ( - testConfig builder.VCSimTestConfig - ctx *builder.TestContextForVCSim - vmProvider providers.VirtualMachineProviderInterface - ) - - BeforeEach(func() { - testConfig = builder.VCSimTestConfig{} - }) - - JustBeforeEach(func() { - ctx = suite.NewTestContextForVCSim(testConfig) - vmProvider = vsphere.NewVSphereVMProviderFromClient(ctx, ctx.Client, ctx.Recorder) - }) - - AfterEach(func() { - ctx.AfterEach() - ctx = nil - vmProvider = nil - }) - - Context("ComputeCPUMinFrequency", func() { - It("returns success", func() { - Expect(vmProvider.ComputeCPUMinFrequency(ctx)).To(Succeed()) - }) - }) -} - -var _ = Describe("UpdateVcCreds", func() { - var ( - ctx *builder.TestContextForVCSim - testConfig builder.VCSimTestConfig - vmProvider providers.VirtualMachineProviderInterface - ) - - BeforeEach(func() { - ctx = suite.NewTestContextForVCSim(testConfig) - vmProvider = vsphere.NewVSphereVMProviderFromClient(ctx, ctx.Client, ctx.Recorder) - }) +// Shared by vsphere_test VM specs (vmprovider_vm_*_test.go, fast deploy, resize, …). +const ( + cvmiKind = "ClusterVirtualMachineImage" + vcsimCPUFreq = 2294 + dvpgName = "DC0_DVPG0" +) - AfterEach(func() { - ctx.AfterEach() - }) +const ( + createOrUpdateVMMaxAllowedCallCount = 100 +) - When("Invalid Credentials", func() { +func createOrUpdateVM( + testCtx *builder.TestContextForVCSim, + provider providers.VirtualMachineProviderInterface, + vm *vmopv1.VirtualMachine) error { - It("returns error", func() { - data := map[string][]byte{} - Expect(vmProvider.UpdateVcCreds(ctx, data)).To(MatchError("vCenter username and password are missing")) - }) - }) + var fn func(ctx context.Context) error - When("New Credentials", func() { + if pkgcfg.FromContext(testCtx).AsyncSignalEnabled && + pkgcfg.FromContext(testCtx).AsyncCreateEnabled { - It("VC Client is logged out", func() { - vcClient, err := vmProvider.VSphereClient(ctx) - Expect(err).NotTo(HaveOccurred()) - Expect(vcClient).NotTo(BeNil()) - session, err := vcClient.RestClient().Session(ctx) - Expect(err).NotTo(HaveOccurred()) - Expect(session).ToNot(BeNil()) + By("non-blocking createOrUpdateVM") + fn = func(ctx context.Context) error { + return createOrUpdateVMAsync(testCtx, provider, vm) + } + } else { + By("blocking createOrUpdateVM") + fn = func(ctx context.Context) error { + return provider.CreateOrUpdateVirtualMachine(ctx, vm) + } + } - data := map[string][]byte{ - "username": []byte("newUser"), - "password": []byte("newPassword"), - } - Expect(vmProvider.UpdateVcCreds(ctx, data)).To(Succeed()) - By("Client is logged out", func() { - session, err := vcClient.RestClient().Session(ctx) - Expect(err).NotTo(HaveOccurred()) - Expect(session).To(BeNil()) - }) - }) - }) - - When("Same Credentials", func() { - - It("VC Client is not logged out", func() { - vcClient, err := vmProvider.VSphereClient(ctx) - Expect(err).NotTo(HaveOccurred()) - Expect(vcClient).NotTo(BeNil()) - session, err := vcClient.RestClient().Session(ctx) - Expect(err).NotTo(HaveOccurred()) - Expect(session).ToNot(BeNil()) - - data := map[string][]byte{ - "username": []byte(ctx.VCClientConfig.Username), - "password": []byte(ctx.VCClientConfig.Password), - } - Expect(vmProvider.UpdateVcCreds(ctx, data)).To(Succeed()) - By("VC Client is the same", func() { - vcClient2, err := vmProvider.VSphereClient(ctx) - Expect(err).NotTo(HaveOccurred()) - Expect(vcClient2).To(BeIdenticalTo(vcClient)) - - session, err := vcClient.RestClient().Session(ctx) - Expect(err).NotTo(HaveOccurred()) - Expect(session).ToNot(BeNil()) - }) - }) - }) -}) - -var _ = Describe("SyncVirtualMachineImage", func() { var ( - ctx *builder.TestContextForVCSim - testConfig builder.VCSimTestConfig - vmProvider providers.VirtualMachineProviderInterface + totalCallCount = 0 + nonErrorCallCount = 0 ) - BeforeEach(func() { - testConfig.WithContentLibrary = true - ctx = suite.NewTestContextForVCSim(testConfig) - vmProvider = vsphere.NewVSphereVMProviderFromClient(ctx, ctx.Client, ctx.Recorder) - }) - - AfterEach(func() { - ctx.AfterEach() - }) - - When("content library item is an unexpected K8s object type", func() { - It("should return an error", func() { - err := vmProvider.SyncVirtualMachineImage(ctx, &imgregv1a1.ContentLibrary{}, &vmopv1.VirtualMachineImage{}) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring(fmt.Sprintf("unexpected content library item K8s object type %T", &imgregv1a1.ContentLibrary{}))) - }) - }) - - When("content library item is a v1alpha2 type", func() { - It("should not return an error", func() { - err := vmProvider.SyncVirtualMachineImage(ctx, &imgregv1.ContentLibraryItem{}, &vmopv1.VirtualMachineImage{}) - Expect(err).NotTo(HaveOccurred()) - }) - }) - - When("content library item is not an OVF type", func() { - It("should return early without updating VM Image status", func() { - isoItem := &imgregv1a1.ContentLibraryItem{ - Status: imgregv1a1.ContentLibraryItemStatus{ - Type: imgregv1a1.ContentLibraryItemTypeIso, - }, + for { + var ( + err error + repeat bool + opctx = ctxop.WithContext(testCtx) + ) + + err = fn(opctx) + + if ctxop.IsUpdate(opctx) { + ctxop.MarkUpdate(testCtx) + } + + if err != nil { + switch { + case errors.Is(err, vsphere.ErrCreate), + errors.Is(err, vsphere.ErrBackup), + errors.Is(err, vsphere.ErrBootstrapCustomize), + errors.Is(err, vsphere.ErrBootstrapReconfigure), + errors.Is(err, vsphere.ErrReconfigure), + errors.Is(err, vsphere.ErrRestart), + errors.Is(err, vsphere.ErrSetPowerState), + errors.Is(err, vsphere.ErrUpgradeHardwareVersion), + errors.Is(err, vsphere.ErrPromoteDisks), + errors.Is(err, vsphere.ErrSnapshotRevert), + errors.Is(err, vsphere.ErrPolicyNotReady), + errors.Is(err, vsphere.ErrUpgradeSchema), + errors.Is(err, vsphere.ErrUpgradeObject): + + repeat = true + default: + GinkgoLogr.Error(err, "createOrUpdateVM fail") + return err } - var vmi vmopv1.VirtualMachineImage - Expect(vmProvider.SyncVirtualMachineImage(ctx, isoItem, &vmi)).To(Succeed()) - Expect(vmi.Status).To(Equal(vmopv1.VirtualMachineImageStatus{})) - }) - }) - - When("content library item is an OVF type", func() { - // TODO(akutz) Promote this block when the FSS WCP_VMService_FastDeploy is - // removed. - When("FSS WCP_VMService_FastDeploy is enabled", func() { - - var ( - err error - cli imgregv1a1.ContentLibraryItem - vmi1 vmopv1.VirtualMachineImage - vmic1 vmopv1.VirtualMachineImageCache - vmicm1 corev1.ConfigMap - createVMIC1 bool - ) - - BeforeEach(func() { - pkgcfg.UpdateContext(ctx, func(config *pkgcfg.Config) { - config.Features.FastDeploy = true - }) - - cli = imgregv1a1.ContentLibraryItem{ - Spec: imgregv1a1.ContentLibraryItemSpec{ - UUID: types.UID(ctx.ContentLibraryItem1ID), - }, - Status: imgregv1a1.ContentLibraryItemStatus{ - ContentVersion: "v1", - Type: imgregv1a1.ContentLibraryItemTypeOvf, - }, - } + } + + if totalCallCount > 100 { + ExpectWithOffset(1, totalCallCount).To( + BeNumerically("<", createOrUpdateVMMaxAllowedCallCount), + "cannot exceed createOrUpdateVMMaxAllowedCallCount for tests") + } + + totalCallCount++ + + if !repeat { + nonErrorCallCount++ + } + + if nonErrorCallCount == 2 { + GinkgoLogr.Info( + "createOrUpdateVM success", + "totalCalls", totalCallCount) + return nil + } + + GinkgoLogr.Info( + "createOrUpdateVM repeat", + "totalCalls", totalCallCount, + "err", err) + } +} - vmi1 = vmopv1.VirtualMachineImage{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "my-namespace", - Name: "my-vmi", - }, - } +func createOrUpdateAndGetVcVM( + ctx *builder.TestContextForVCSim, + provider providers.VirtualMachineProviderInterface, + vm *vmopv1.VirtualMachine) (*object.VirtualMachine, error) { - createVMIC1 = true - vmicName := util.VMIName(ctx.ContentLibraryItem1ID) - vmic1 = vmopv1.VirtualMachineImageCache{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: pkgcfg.FromContext(ctx).PodNamespace, - Name: vmicName, - }, - Status: vmopv1.VirtualMachineImageCacheStatus{ - OVF: &vmopv1.VirtualMachineImageCacheOVFStatus{ - ConfigMapName: vmicName, - ProviderVersion: "v1", - }, - Conditions: []metav1.Condition{ - { - Type: vmopv1.VirtualMachineImageCacheConditionHardwareReady, - Status: metav1.ConditionTrue, - }, - }, - }, - } + if err := createOrUpdateVM(ctx, provider, vm); err != nil { + return nil, err + } - vmicm1 = corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: vmic1.Namespace, - Name: vmic1.Name, - }, - Data: map[string]string{ - "value": ctx.ContentLibraryItem1YAML, - }, - } - Expect(ctx.Client.Create(ctx, &vmicm1)).To(Succeed()) - }) - - JustBeforeEach(func() { - if createVMIC1 { - status := vmic1.Status.DeepCopy() - Expect(ctx.Client.Create(ctx, &vmic1)).To(Succeed()) - vmic1.Status = *status - Expect(ctx.Client.Status().Update(ctx, &vmic1)).To(Succeed()) - } - err = vmProvider.SyncVirtualMachineImage(ctx, &cli, &vmi1) - }) - - When("it fails to createOrPatch the VMICache resource", func() { - // TODO(akutz) Add interceptors to the vcSim test context so - // this can be tested. - XIt("should return an error", func() { - - }) - }) - - assertVMICExists := func(namespace, name string) { - var ( - obj vmopv1.VirtualMachineImageCache - key = ctrlclient.ObjectKey{ - Namespace: namespace, - Name: name, - } - ) - ExpectWithOffset(1, ctx.Client.Get(ctx, key, &obj)).To(Succeed()) - } + ExpectWithOffset(1, vm.Status.UniqueID).ToNot(BeEmpty()) + vcVM := ctx.GetVMFromMoID(vm.Status.UniqueID) + ExpectWithOffset(1, vcVM).ToNot(BeNil()) + return vcVM, nil +} - assertVMICNotReady := func(err error, name string) { - var e pkgerr.VMICacheNotReadyError - ExpectWithOffset(1, errors.As(err, &e)).To(BeTrue()) - ExpectWithOffset(1, e.Name).To(Equal(name)) +func createOrUpdateVMAsync( + ctx *builder.TestContextForVCSim, + provider providers.VirtualMachineProviderInterface, + vm *vmopv1.VirtualMachine) error { + + GinkgoLogr.Info("entered createOrUpdateVMAsync") + + chanErr, err := provider.CreateOrUpdateVirtualMachineAsync(ctx, vm) + if err != nil { + if errors.Is(err, vsphere.ErrUpgradeSchema) || + errors.Is(err, vsphere.ErrUpgradeObject) { + + ExpectWithOffset(1, ctx.Client.Update( + ctx, + vm)).To(Succeed()) + } + GinkgoLogr.Info("createOrUpdateVMAsync returned", "err", err) + return err + } + + if chanErr != nil { + // Unlike the VM controller, this test helper blocks until the async + // parts of CreateOrUpdateVM are complete. This is to avoid a large + // refactor for now. + for err2 := range chanErr { + if err2 != nil { + GinkgoLogr.Info("createOrUpdateVMAsync chanErr", "err", err2) + if err == nil { + err = err2 + } else { + err = fmt.Errorf("%w,%w", err, err2) + } } - - When("OVF condition is False", func() { - BeforeEach(func() { - vmic1.Status.Conditions[0].Status = metav1.ConditionFalse - vmic1.Status.Conditions[0].Message = "fubar" - }) - It("should return an error", func() { - Expect(err).To(MatchError("failed to get hardware: fubar: cache not ready")) - }) - }) - - When("OVF is not ready", func() { - When("condition is missing", func() { - BeforeEach(func() { - createVMIC1 = false - }) - It("should return ErrVMICacheNotReady", func() { - assertVMICExists(vmic1.Namespace, vmic1.Name) - assertVMICNotReady(err, vmic1.Name) - }) - }) - When("condition is unknown", func() { - BeforeEach(func() { - vmic1.Status.Conditions[0].Status = metav1.ConditionUnknown - }) - It("should return ErrVMICacheNotReady", func() { - assertVMICNotReady(err, vmic1.Name) - }) - }) - When("status.ovf is nil", func() { - BeforeEach(func() { - vmic1.Status.OVF = nil - }) - It("should return ErrVMICacheNotReady", func() { - assertVMICNotReady(err, vmic1.Name) - }) - }) - When("status.ovf.providerVersion does not match expected version", func() { - BeforeEach(func() { - vmic1.Status.OVF.ProviderVersion = "" - }) - It("should return ErrVMICacheNotReady", func() { - assertVMICNotReady(err, vmic1.Name) - }) - }) - When("configmap is missing", func() { - BeforeEach(func() { - Expect(ctx.Client.Delete(ctx, &vmicm1)).To(Succeed()) - }) - It("should return ErrVMICacheNotReady", func() { - assertVMICNotReady(err, vmic1.Name) - }) - }) - }) - - When("OVF is ready", func() { - When("marshaled ovf data is invalid", func() { - BeforeEach(func() { - vmicm1.Data["value"] = "invalid" - Expect(ctx.Client.Update(ctx, &vmicm1)).To(Succeed()) - }) - It("should return an error", func() { - Expect(err).To(MatchError("failed to unmarshal ovf yaml into envelope: " + - "error unmarshaling JSON: while decoding JSON: " + - "json: cannot unmarshal string into Go value of type ovf.Envelope")) - }) - }) - When("marshaled ovf data is valid", func() { - It("should return success and update VM Image status accordingly", func() { - Expect(err).ToNot(HaveOccurred()) - Expect(vmi1.Status.Firmware).To(Equal("efi")) - Expect(vmi1.Status.HardwareVersion).NotTo(BeNil()) - Expect(*vmi1.Status.HardwareVersion).To(Equal(int32(9))) - Expect(vmi1.Status.OSInfo.ID).To(Equal("36")) - Expect(vmi1.Status.OSInfo.Type).To(Equal("otherLinuxGuest")) - Expect(vmi1.Status.Disks).To(HaveLen(1)) - Expect(vmi1.Status.Disks[0].Limit.String()).To(Equal("30Mi")) - Expect(vmi1.Status.Disks[0].Requested.String()).To(Equal("30Mi")) - }) - }) - - }) - }) - - // TODO(akutz) Remove this block when the FSS WCP_VMService_FastDeploy is - // removed. - When("FSS WCP_VMService_FastDeploy is disabled", func() { - - BeforeEach(func() { - pkgcfg.UpdateContext(ctx, func(config *pkgcfg.Config) { - config.Features.FastDeploy = false - }) - }) - - When("it fails to get the OVF envelope", func() { - It("should return an error", func() { - cli := &imgregv1a1.ContentLibraryItem{ - Spec: imgregv1a1.ContentLibraryItemSpec{ - // Use an invalid item ID to fail to get the OVF envelope. - UUID: "invalid-library-ID", - }, - Status: imgregv1a1.ContentLibraryItemStatus{ - Type: imgregv1a1.ContentLibraryItemTypeOvf, - }, - } - err := vmProvider.SyncVirtualMachineImage(ctx, cli, &vmopv1.VirtualMachineImage{}) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to get OVF envelope for library item \"invalid-library-ID\"")) - }) - }) - - When("OVF envelope is nil", func() { - It("should return an error", func() { - ovfItem := &imgregv1a1.ContentLibraryItem{ - Spec: imgregv1a1.ContentLibraryItemSpec{ - UUID: types.UID(ctx.ContentLibraryIsoItemID), - }, - Status: imgregv1a1.ContentLibraryItemStatus{ - Type: imgregv1a1.ContentLibraryItemTypeOvf, - }, - } - err := vmProvider.SyncVirtualMachineImage(ctx, ovfItem, &vmopv1.VirtualMachineImage{}) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring(fmt.Sprintf("OVF envelope is nil for library item %q", ctx.ContentLibraryIsoItemID))) - }) - }) - - When("there is a valid OVF envelope", func() { - It("should return success and update VM Image status accordingly", func() { - cli := &imgregv1a1.ContentLibraryItem{ - Spec: imgregv1a1.ContentLibraryItemSpec{ - UUID: types.UID(ctx.ContentLibraryItem1ID), - }, - Status: imgregv1a1.ContentLibraryItemStatus{ - Type: imgregv1a1.ContentLibraryItemTypeOvf, - }, - } - var vmi vmopv1.VirtualMachineImage - Expect(vmProvider.SyncVirtualMachineImage(ctx, cli, &vmi)).To(Succeed()) - Expect(vmi.Status.Firmware).To(Equal("efi")) - Expect(vmi.Status.HardwareVersion).NotTo(BeNil()) - Expect(*vmi.Status.HardwareVersion).To(Equal(int32(9))) - Expect(vmi.Status.OSInfo.ID).To(Equal("36")) - Expect(vmi.Status.OSInfo.Type).To(Equal("otherLinuxGuest")) - Expect(vmi.Status.Disks).To(HaveLen(1)) - Expect(vmi.Status.Disks[0].Limit.String()).To(Equal("30Mi")) - Expect(vmi.Status.Disks[0].Requested.String()).To(Equal("30Mi")) - }) - }) - }) - }) -}) + } + } + + if errors.Is(err, vsphere.ErrCreate) { + ExpectWithOffset(1, ctx.Client.Get( + ctx, + client.ObjectKeyFromObject(vm), + vm)).To(Succeed()) + } + + GinkgoLogr.Info("createOrUpdateVMAsync returned post channel", "err", err) + return err +} diff --git a/pkg/providers/vsphere/vmprovider_update_creds_test.go b/pkg/providers/vsphere/vmprovider_update_creds_test.go new file mode 100644 index 000000000..3d200b8ea --- /dev/null +++ b/pkg/providers/vsphere/vmprovider_update_creds_test.go @@ -0,0 +1,89 @@ +// © Broadcom. All Rights Reserved. +// The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package vsphere_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/vmware-tanzu/vm-operator/pkg/providers" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" + "github.com/vmware-tanzu/vm-operator/test/builder" +) + +var _ = Describe("UpdateVcCreds", func() { + var ( + ctx *builder.TestContextForVCSim + testConfig builder.VCSimTestConfig + vmProvider providers.VirtualMachineProviderInterface + ) + + BeforeEach(func() { + ctx = suite.NewTestContextForVCSim(testConfig) + vmProvider = vsphere.NewVSphereVMProviderFromClient(ctx, ctx.Client, ctx.Recorder) + }) + + AfterEach(func() { + ctx.AfterEach() + }) + + When("Invalid Credentials", func() { + + It("returns error", func() { + data := map[string][]byte{} + Expect(vmProvider.UpdateVcCreds(ctx, data)).To(MatchError("vCenter username and password are missing")) + }) + }) + + When("New Credentials", func() { + + It("VC Client is logged out", func() { + vcClient, err := vmProvider.VSphereClient(ctx) + Expect(err).NotTo(HaveOccurred()) + Expect(vcClient).NotTo(BeNil()) + session, err := vcClient.RestClient().Session(ctx) + Expect(err).NotTo(HaveOccurred()) + Expect(session).ToNot(BeNil()) + + data := map[string][]byte{ + "username": []byte("newUser"), + "password": []byte("newPassword"), + } + Expect(vmProvider.UpdateVcCreds(ctx, data)).To(Succeed()) + By("Client is logged out", func() { + session, err := vcClient.RestClient().Session(ctx) + Expect(err).NotTo(HaveOccurred()) + Expect(session).To(BeNil()) + }) + }) + }) + + When("Same Credentials", func() { + + It("VC Client is not logged out", func() { + vcClient, err := vmProvider.VSphereClient(ctx) + Expect(err).NotTo(HaveOccurred()) + Expect(vcClient).NotTo(BeNil()) + session, err := vcClient.RestClient().Session(ctx) + Expect(err).NotTo(HaveOccurred()) + Expect(session).ToNot(BeNil()) + + data := map[string][]byte{ + "username": []byte(ctx.VCClientConfig.Username), + "password": []byte(ctx.VCClientConfig.Password), + } + Expect(vmProvider.UpdateVcCreds(ctx, data)).To(Succeed()) + By("VC Client is the same", func() { + vcClient2, err := vmProvider.VSphereClient(ctx) + Expect(err).NotTo(HaveOccurred()) + Expect(vcClient2).To(BeIdenticalTo(vcClient)) + + session, err := vcClient.RestClient().Session(ctx) + Expect(err).NotTo(HaveOccurred()) + Expect(session).ToNot(BeNil()) + }) + }) + }) +}) diff --git a/pkg/providers/vsphere/vmprovider_vm_utils.go b/pkg/providers/vsphere/vmprovider_utils.go similarity index 100% rename from pkg/providers/vsphere/vmprovider_vm_utils.go rename to pkg/providers/vsphere/vmprovider_utils.go diff --git a/pkg/providers/vsphere/vmprovider_vm_utils_test.go b/pkg/providers/vsphere/vmprovider_utils_test.go similarity index 99% rename from pkg/providers/vsphere/vmprovider_vm_utils_test.go rename to pkg/providers/vsphere/vmprovider_utils_test.go index 1a168841a..1919a1aeb 100644 --- a/pkg/providers/vsphere/vmprovider_vm_utils_test.go +++ b/pkg/providers/vsphere/vmprovider_utils_test.go @@ -31,8 +31,7 @@ import ( "github.com/vmware-tanzu/vm-operator/test/builder" ) -func vmUtilTests() { - +var _ = Describe("Utils", func() { var ( k8sClient client.Client initObjects []client.Object @@ -1729,4 +1728,4 @@ func vmUtilTests() { }) }) }) -} +}) diff --git a/pkg/providers/vsphere/vmprovider_utils_vappconfig_test.go b/pkg/providers/vsphere/vmprovider_utils_vappconfig_test.go new file mode 100644 index 000000000..fac544617 --- /dev/null +++ b/pkg/providers/vsphere/vmprovider_utils_vappconfig_test.go @@ -0,0 +1,155 @@ +// © Broadcom. All Rights Reserved. +// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package vsphere_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + vimtypes "github.com/vmware/govmomi/vim25/types" + + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" + "github.com/vmware-tanzu/vm-operator/pkg/util/ptr" +) + +var _ = Describe("NormalizeVAppConfigExpressionProperties", func() { + It("is a no-op when config spec is nil", func() { + vsphere.NormalizeVAppConfigExpressionProperties(nil) + }) + + It("is a no-op when VAppConfig is nil", func() { + spec := &vimtypes.VirtualMachineConfigSpec{} + vsphere.NormalizeVAppConfigExpressionProperties(spec) + Expect(spec.VAppConfig).To(BeNil()) + }) + + It("is a no-op when VmConfigSpec has no properties", func() { + spec := &vimtypes.VirtualMachineConfigSpec{ + VAppConfig: &vimtypes.VmConfigSpec{ + Property: []vimtypes.VAppPropertySpec{}, + }, + } + vsphere.NormalizeVAppConfigExpressionProperties(spec) + Expect(spec.VAppConfig.GetVmConfigSpec().Property).To(BeEmpty()) + }) + + It("converts expression type to string and sets UserConfigurable", func() { + spec := &vimtypes.VirtualMachineConfigSpec{ + VAppConfig: &vimtypes.VmConfigSpec{ + Property: []vimtypes.VAppPropertySpec{ + { + Info: &vimtypes.VAppPropertyInfo{ + Id: "net-expression", + Type: "expression", + DefaultValue: "com.vmware.customization.network", + UserConfigurable: ptr.To(false), + }, + }, + }, + }, + } + vsphere.NormalizeVAppConfigExpressionProperties(spec) + vac := spec.VAppConfig.GetVmConfigSpec() + Expect(vac).ToNot(BeNil()) + Expect(vac.Property).To(HaveLen(1)) + Expect(vac.Property[0].Info.Type).To(Equal("string")) + Expect(vac.Property[0].Info.DefaultValue).To(Equal("")) + Expect(vac.Property[0].Info.UserConfigurable).ToNot(BeNil()) + Expect(*vac.Property[0].Info.UserConfigurable).To(BeTrue()) + }) + + It("converts ip:network type to string and sets UserConfigurable", func() { + spec := &vimtypes.VirtualMachineConfigSpec{ + VAppConfig: &vimtypes.VmConfigSpec{ + Property: []vimtypes.VAppPropertySpec{ + { + Info: &vimtypes.VAppPropertyInfo{ + Id: "ip-network", + Type: "ip:network", + DefaultValue: "192.168.1.0/24", + UserConfigurable: nil, + }, + }, + }, + }, + } + vsphere.NormalizeVAppConfigExpressionProperties(spec) + vac := spec.VAppConfig.GetVmConfigSpec() + Expect(vac).ToNot(BeNil()) + Expect(vac.Property).To(HaveLen(1)) + Expect(vac.Property[0].Info.Type).To(Equal("string")) + Expect(vac.Property[0].Info.DefaultValue).To(Equal("")) + Expect(vac.Property[0].Info.UserConfigurable).ToNot(BeNil()) + Expect(*vac.Property[0].Info.UserConfigurable).To(BeTrue()) + }) + + It("leaves other property types unchanged", func() { + spec := &vimtypes.VirtualMachineConfigSpec{ + VAppConfig: &vimtypes.VmConfigSpec{ + Property: []vimtypes.VAppPropertySpec{ + { + Info: &vimtypes.VAppPropertyInfo{ + Id: "string-prop", Type: "string", + DefaultValue: "hello", UserConfigurable: ptr.To(true), + }, + }, + { + Info: &vimtypes.VAppPropertyInfo{ + Id: "int-prop", Type: "int", + DefaultValue: "42", UserConfigurable: ptr.To(false), + }, + }, + }, + }, + } + vsphere.NormalizeVAppConfigExpressionProperties(spec) + vac := spec.VAppConfig.GetVmConfigSpec() + Expect(vac.Property[0].Info.Type).To(Equal("string")) + Expect(vac.Property[0].Info.DefaultValue).To(Equal("hello")) + Expect(vac.Property[1].Info.Type).To(Equal("int")) + Expect(vac.Property[1].Info.DefaultValue).To(Equal("42")) + }) + + It("converts only expression and ip:network in a mixed list", func() { + spec := &vimtypes.VirtualMachineConfigSpec{ + VAppConfig: &vimtypes.VmConfigSpec{ + Property: []vimtypes.VAppPropertySpec{ + {Info: &vimtypes.VAppPropertyInfo{Id: "a", Type: "string", DefaultValue: "keep"}}, + {Info: &vimtypes.VAppPropertyInfo{Id: "b", Type: "expression", DefaultValue: "expr"}}, + {Info: &vimtypes.VAppPropertyInfo{Id: "c", Type: "ip:network", DefaultValue: "10.0.0.0/8"}}, + {Info: &vimtypes.VAppPropertyInfo{Id: "d", Type: "int", DefaultValue: "1"}}, + }, + }, + } + vsphere.NormalizeVAppConfigExpressionProperties(spec) + vac := spec.VAppConfig.GetVmConfigSpec() + Expect(vac.Property[0].Info.Type).To(Equal("string")) + Expect(vac.Property[0].Info.DefaultValue).To(Equal("keep")) + Expect(vac.Property[1].Info.Type).To(Equal("string")) + Expect(vac.Property[1].Info.DefaultValue).To(Equal("")) + Expect(*vac.Property[1].Info.UserConfigurable).To(BeTrue()) + Expect(vac.Property[2].Info.Type).To(Equal("string")) + Expect(vac.Property[2].Info.DefaultValue).To(Equal("")) + Expect(*vac.Property[2].Info.UserConfigurable).To(BeTrue()) + Expect(vac.Property[3].Info.Type).To(Equal("int")) + Expect(vac.Property[3].Info.DefaultValue).To(Equal("1")) + }) + + It("skips properties with nil Info", func() { + spec := &vimtypes.VirtualMachineConfigSpec{ + VAppConfig: &vimtypes.VmConfigSpec{ + Property: []vimtypes.VAppPropertySpec{ + {Info: nil}, + {Info: &vimtypes.VAppPropertyInfo{Id: "b", Type: "expression", DefaultValue: "x"}}, + }, + }, + } + vsphere.NormalizeVAppConfigExpressionProperties(spec) + vac := spec.VAppConfig.GetVmConfigSpec() + Expect(vac.Property[0].Info).To(BeNil()) + Expect(vac.Property[1].Info.Type).To(Equal("string")) + Expect(vac.Property[1].Info.DefaultValue).To(Equal("")) + }) +}) diff --git a/pkg/providers/vsphere/vmprovider_vm2_test.go b/pkg/providers/vsphere/vmprovider_vm2_test.go deleted file mode 100644 index 035e63387..000000000 --- a/pkg/providers/vsphere/vmprovider_vm2_test.go +++ /dev/null @@ -1,1011 +0,0 @@ -// © Broadcom. All Rights Reserved. -// The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. -// SPDX-License-Identifier: Apache-2.0 - -package vsphere_test - -import ( - "fmt" - "regexp" - "sort" - "strconv" - "strings" - "time" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - vimtypes "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" - "sigs.k8s.io/controller-runtime/pkg/client" - - netopv1alpha1 "github.com/vmware-tanzu/net-operator-api/api/v1alpha1" - vpcv1alpha1 "github.com/vmware-tanzu/nsx-operator/pkg/apis/vpc/v1alpha1" - - ncpv1alpha1 "github.com/vmware-tanzu/vm-operator/external/ncp/api/v1alpha1" - - vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha6" - "github.com/vmware-tanzu/vm-operator/api/v1alpha6/common" - "github.com/vmware-tanzu/vm-operator/pkg/conditions" - pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" - "github.com/vmware-tanzu/vm-operator/pkg/providers" - "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" - "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/network" - "github.com/vmware-tanzu/vm-operator/pkg/util" - "github.com/vmware-tanzu/vm-operator/pkg/util/ptr" - "github.com/vmware-tanzu/vm-operator/test/builder" -) - -const ( - subnetKind = "Subnet" - subnetAPIVersion = "crd.nsx.vmware.com/v1alpha1" -) - -type fakeNetworkProvider interface { - simulateInterfaceReconcile(ctx *builder.TestContextForVCSim, vm *vmopv1.VirtualMachine, interfaceSpec vmopv1.VirtualMachineNetworkInterfaceSpec, networkIdx int) - assertEthernetCard(ctx *builder.TestContextForVCSim, dev vimtypes.BaseVirtualDevice, interfaceSpec vmopv1.VirtualMachineNetworkInterfaceSpec, networkIdx int) - assertNetworkInterfacesDNE(ctx *builder.TestContextForVCSim, vm *vmopv1.VirtualMachine, networkName, interfaceName string) -} - -func extID(networkName, interfaceName string) string { - return networkName + "-" + interfaceName -} - -var ifaceIdxRegex = regexp.MustCompile(`(\d+)$`) - -func idxFromInterfaceName(s string) int { - m := ifaceIdxRegex.FindStringSubmatch(s) - Expect(m).To(HaveLen(2)) - i, _ := strconv.ParseInt(m[1], 10, 32) - return int(i) -} - -type vdsNetworkProvider struct{} - -func (v vdsNetworkProvider) simulateInterfaceReconcile( - ctx *builder.TestContextForVCSim, - vm *vmopv1.VirtualMachine, - interfaceSpec vmopv1.VirtualMachineNetworkInterfaceSpec, - networkIdx int) { - - interfaceName, networkName := interfaceSpec.Name, interfaceSpec.Network.Name - ifaceIdx := idxFromInterfaceName(interfaceName) - - netInterface := &netopv1alpha1.NetworkInterface{ - ObjectMeta: metav1.ObjectMeta{ - Name: network.NetOPCRName(vm.Name, networkName, interfaceName, false), - Namespace: vm.Namespace, - }, - } - Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(netInterface), netInterface)).To(Succeed()) - Expect(netInterface.Spec.NetworkName).To(Equal(networkName)) - - netInterface.Status.NetworkID = ctx.NetworkRefs[networkIdx].Reference().Value - netInterface.Status.ExternalID = extID(networkName, interfaceName) - netInterface.Status.MacAddress = "" // NetOP doesn't set this. - netInterface.Status.IPConfigs = []netopv1alpha1.IPConfig{ - { - IP: fmt.Sprintf("192.168.1.2%d", ifaceIdx), - IPFamily: corev1.IPv4Protocol, - Gateway: "192.168.1.1", - SubnetMask: "255.255.255.0", - }, - } - netInterface.Status.Conditions = []netopv1alpha1.NetworkInterfaceCondition{ - { - Type: netopv1alpha1.NetworkInterfaceReady, - Status: corev1.ConditionTrue, - }, - } - Expect(ctx.Client.Status().Update(ctx, netInterface)).To(Succeed()) -} - -func (v vdsNetworkProvider) assertEthernetCard( - ctx *builder.TestContextForVCSim, - dev vimtypes.BaseVirtualDevice, - interfaceSpec vmopv1.VirtualMachineNetworkInterfaceSpec, - networkIdx int) { - - interfaceName, networkName := interfaceSpec.Name, interfaceSpec.Network.Name - - ethCard := dev.(vimtypes.BaseVirtualEthernetCard).GetVirtualEthernetCard() - backingInfo, ok := ethCard.Backing.(*vimtypes.VirtualEthernetCardDistributedVirtualPortBackingInfo) - Expect(ok).Should(BeTrue()) - ExpectWithOffset(1, backingInfo.Port.PortgroupKey).To(Equal(ctx.NetworkRefs[networkIdx].Reference().Value)) - Expect(ethCard.MacAddress).ToNot(BeEmpty()) - Expect(ethCard.ExternalId).To(Equal(extID(networkName, interfaceName))) - -} - -func (v vdsNetworkProvider) assertNetworkInterfacesDNE( - ctx *builder.TestContextForVCSim, - vm *vmopv1.VirtualMachine, - networkName, interfaceName string) { - - netInterface := &netopv1alpha1.NetworkInterface{ - ObjectMeta: metav1.ObjectMeta{ - Name: network.NetOPCRName(vm.Name, networkName, interfaceName, false), - Namespace: vm.Namespace, - }, - } - err := ctx.Client.Get(ctx, client.ObjectKeyFromObject(netInterface), netInterface) - Expect(err).To(HaveOccurred()) - Expect(apierrors.IsNotFound(err)).To(BeTrue()) -} - -type nsxtNetworkProvider struct{} - -func (n nsxtNetworkProvider) simulateInterfaceReconcile( - ctx *builder.TestContextForVCSim, - vm *vmopv1.VirtualMachine, - interfaceSpec vmopv1.VirtualMachineNetworkInterfaceSpec, - networkIdx int) { - - interfaceName, networkName := interfaceSpec.Name, interfaceSpec.Network.Name - ifaceIdx := idxFromInterfaceName(interfaceName) - - netInterface := &ncpv1alpha1.VirtualNetworkInterface{ - ObjectMeta: metav1.ObjectMeta{ - Name: network.NCPCRName(vm.Name, networkName, interfaceName, false), - Namespace: vm.Namespace, - }, - } - Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(netInterface), netInterface)).To(Succeed()) - Expect(netInterface.Spec.VirtualNetwork).To(Equal(networkName)) - - netInterface.Status.InterfaceID = extID(networkName, interfaceName) - netInterface.Status.MacAddress = fmt.Sprintf("01-23-45-67-89-%02X", ifaceIdx) - netInterface.Status.ProviderStatus = &ncpv1alpha1.VirtualNetworkInterfaceProviderStatus{ - NsxLogicalSwitchID: builder.GetNsxTLogicalSwitchUUID(networkIdx), - } - netInterface.Status.IPAddresses = []ncpv1alpha1.VirtualNetworkInterfaceIP{ - { - IP: fmt.Sprintf("192.168.1.2%d", ifaceIdx), - Gateway: "192.168.1.1", - SubnetMask: "255.255.255.0", - }, - } - netInterface.Status.Conditions = []ncpv1alpha1.VirtualNetworkCondition{ - { - Type: "Ready", - Status: "True", - }, - } - Expect(ctx.Client.Status().Update(ctx, netInterface)).To(Succeed()) -} - -func (n nsxtNetworkProvider) assertEthernetCard( - ctx *builder.TestContextForVCSim, - dev vimtypes.BaseVirtualDevice, - interfaceSpec vmopv1.VirtualMachineNetworkInterfaceSpec, - networkIdx int) { - - interfaceName, networkName := interfaceSpec.Name, interfaceSpec.Network.Name - ifaceIdx := idxFromInterfaceName(interfaceName) - - ethCard := dev.(vimtypes.BaseVirtualEthernetCard).GetVirtualEthernetCard() - backingInfo, ok := ethCard.Backing.(*vimtypes.VirtualEthernetCardDistributedVirtualPortBackingInfo) - Expect(ok).Should(BeTrue()) - Expect(backingInfo.Port.PortgroupKey).To(Equal(ctx.NetworkRefs[networkIdx].Reference().Value)) - Expect(ethCard.MacAddress).To(Equal(fmt.Sprintf("01-23-45-67-89-%02X", ifaceIdx))) - Expect(ethCard.ExternalId).To(Equal(extID(networkName, interfaceName))) -} - -func (n nsxtNetworkProvider) assertNetworkInterfacesDNE( - ctx *builder.TestContextForVCSim, - vm *vmopv1.VirtualMachine, - networkName, interfaceName string) { - - netInterface := &ncpv1alpha1.VirtualNetworkInterface{ - ObjectMeta: metav1.ObjectMeta{ - Name: network.NCPCRName(vm.Name, networkName, interfaceName, false), - Namespace: vm.Namespace, - }, - } - err := ctx.Client.Get(ctx, client.ObjectKeyFromObject(netInterface), netInterface) - Expect(err).To(HaveOccurred()) - Expect(apierrors.IsNotFound(err)).To(BeTrue()) -} - -type vpcNetworkProvider struct{} - -func (v vpcNetworkProvider) simulateInterfaceReconcile( - ctx *builder.TestContextForVCSim, - vm *vmopv1.VirtualMachine, - interfaceSpec vmopv1.VirtualMachineNetworkInterfaceSpec, - networkIdx int) { - - interfaceName, networkName := interfaceSpec.Name, interfaceSpec.Network.Name - ifaceIdx := idxFromInterfaceName(interfaceName) - - subnetPort := &vpcv1alpha1.SubnetPort{ - ObjectMeta: metav1.ObjectMeta{ - Name: network.VPCCRName(vm.Name, networkName, interfaceName), - Namespace: vm.Namespace, - }, - } - Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(subnetPort), subnetPort)).To(Succeed()) - Expect(subnetPort.Spec.Subnet).To(Equal(networkName)) - - subnetPort.Status.Attachment.ID = extID(networkName, interfaceName) - subnetPort.Status.NetworkInterfaceConfig.MACAddress = fmt.Sprintf("01-23-45-67-89-%02X", ifaceIdx) - subnetPort.Status.NetworkInterfaceConfig.LogicalSwitchUUID = builder.GetVPCTLogicalSwitchUUID(networkIdx) - subnetPort.Status.NetworkInterfaceConfig.IPAddresses = []vpcv1alpha1.NetworkInterfaceIPAddress{ - { - IPAddress: fmt.Sprintf("192.168.1.11%d/24", ifaceIdx), - Gateway: "192.168.1.1", - }, - } - subnetPort.Status.Conditions = []vpcv1alpha1.Condition{ - { - Type: vpcv1alpha1.Ready, - Status: corev1.ConditionTrue, - }, - } - Expect(ctx.Client.Status().Update(ctx, subnetPort)).To(Succeed()) -} - -func (v vpcNetworkProvider) assertEthernetCard( - ctx *builder.TestContextForVCSim, - dev vimtypes.BaseVirtualDevice, - interfaceSpec vmopv1.VirtualMachineNetworkInterfaceSpec, - networkIdx int) { - - interfaceName, networkName := interfaceSpec.Name, interfaceSpec.Network.Name - ifaceIdx := idxFromInterfaceName(interfaceName) - - ethCard := dev.(vimtypes.BaseVirtualEthernetCard).GetVirtualEthernetCard() - backingInfo, ok := ethCard.Backing.(*vimtypes.VirtualEthernetCardDistributedVirtualPortBackingInfo) - Expect(ok).Should(BeTrue()) - Expect(backingInfo.Port.PortgroupKey).To(Equal(ctx.NetworkRefs[networkIdx].Reference().Value)) - Expect(ethCard.MacAddress).To(Equal(fmt.Sprintf("01-23-45-67-89-%02X", ifaceIdx))) - Expect(ethCard.ExternalId).To(Equal(extID(networkName, interfaceName))) -} - -func (v vpcNetworkProvider) assertNetworkInterfacesDNE( - ctx *builder.TestContextForVCSim, - vm *vmopv1.VirtualMachine, - networkName, interfaceName string) { - - subnetPort := &vpcv1alpha1.SubnetPort{ - ObjectMeta: metav1.ObjectMeta{ - Name: network.VPCCRName(vm.Name, networkName, interfaceName), - Namespace: vm.Namespace, - }, - } - err := ctx.Client.Get(ctx, client.ObjectKeyFromObject(subnetPort), subnetPort) - Expect(err).To(HaveOccurred()) - Expect(apierrors.IsNotFound(err)).To(BeTrue()) -} - -// vmE2ETests() tries to close the gap in the existing vmTests() have in the sense that we don't do e2e-like -// tests of the typical VM create/update workflow. This somewhat of a super-set of the vmTests() but those -// tests are already kind of unwieldy and in places, and until we switch over to v1a2, I don't want -// to disturb that file so keeping things in sync easier. -// For now, these tests focus on a real - VDS, NSX-T or VPC - network env w/ cloud init or sysprep, -// and we'll see how these need to evolve. -func vmE2ETests() { - - const ( - networkName0 = "my-network-0" - interfaceName0 = "eth0" - networkName1 = "my-network-1" - interfaceName1 = "eth1" - networkName2 = "my-network-2" - - bsCloudInit = "cloudInit" - bsSysprep = "sysPrep" - bsLinuxPrep = "linuxPrep" - ) - - var ( - initObjects []client.Object - testConfig builder.VCSimTestConfig - ctx *builder.TestContextForVCSim - vmProvider providers.VirtualMachineProviderInterface - nsInfo builder.WorkloadNamespaceInfo - - vm *vmopv1.VirtualMachine - vmClass *vmopv1.VirtualMachineClass - cloudInitSecret *corev1.Secret - sysprepSecret *corev1.Secret - ) - - BeforeEach(func() { - // Speed up tests until we Watch the network interface types. Sigh. - network.RetryTimeout = 1 * time.Millisecond - - testConfig = builder.VCSimTestConfig{ - NumNetworks: 3, - WithContentLibrary: true, - } - - vm = builder.DummyBasicVirtualMachine("test-vm-e2e", "") - vmClass = builder.DummyVirtualMachineClassGenName() - }) - - JustBeforeEach(func() { - ctx = suite.NewTestContextForVCSim(testConfig, initObjects...) - pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { - config.AsyncSignalEnabled = false - config.MaxDeployThreadsOnProvider = 1 - config.Features.MutableNetworks = true - }) - vmProvider = vsphere.NewVSphereVMProviderFromClient(ctx, ctx.Client, ctx.Recorder) - nsInfo = ctx.CreateWorkloadNamespace() - - vmClass.Namespace = nsInfo.Namespace - Expect(ctx.Client.Create(ctx, vmClass)).To(Succeed()) - Expect(ctx.Client.Status().Update(ctx, vmClass)).To(Succeed()) - - vm.Namespace = nsInfo.Namespace - vm.Spec.ClassName = vmClass.Name - vm.Spec.ImageName = ctx.ContentLibraryItem1Name - vm.Spec.Image.Kind = cvmiKind - vm.Spec.Image.Name = ctx.ContentLibraryItem1Name - vm.Spec.StorageClass = ctx.StorageClassName - if vm.Spec.Network != nil { - vm.Spec.Network.Interfaces[0].Nameservers = []string{"1.1.1.1", "8.8.8.8"} - vm.Spec.Network.Interfaces[0].SearchDomains = []string{"vmware.local"} - } - - cloudInitSecret = &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "my-cloud-init-secret", - Namespace: nsInfo.Namespace, - }, - Data: map[string][]byte{ - "user-value": []byte(""), - }, - } - - sysprepSecret = &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "my-sysprep-secret", - Namespace: nsInfo.Namespace, - }, - Data: map[string][]byte{ - "unattend": []byte("foo"), - }, - } - }) - - AfterEach(func() { - ctx.AfterEach() - ctx = nil - initObjects = nil - vmProvider = nil - nsInfo = builder.WorkloadNamespaceInfo{} - - vm = nil - vmClass = nil - cloudInitSecret = nil - sysprepSecret = nil - }) - - Context("Nil fields in Spec", func() { - - BeforeEach(func() { - testConfig.WithNetworkEnv = builder.NetworkEnvVDS - - vm.Spec.Network = nil - vm.Spec.Bootstrap = nil - vm.Spec.Advanced = nil - vm.Spec.Reserved = nil - }) - - It("DoIt without an NPE", func() { - err := createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - err = createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - Expect(vm.Status.UniqueID).ToNot(BeEmpty()) - vcVM := ctx.GetVMFromMoID(vm.Status.UniqueID) - Expect(vcVM).ToNot(BeNil()) - }) - }) - - Context("VM E2E", func() { - - DescribeTableSubtree("Simulate VM power on/off with adding/removing network interfaces ", - func(networkEnv builder.NetworkEnv, bootstrap string) { - var np fakeNetworkProvider - - BeforeEach(func() { - testConfig.WithNetworkEnv = networkEnv - - switch networkEnv { - case builder.NetworkEnvVDS: - np = vdsNetworkProvider{} - case builder.NetworkEnvNSXT: - np = nsxtNetworkProvider{} - case builder.NetworkEnvVPC: - np = vpcNetworkProvider{} - } - }) - - JustBeforeEach(func() { - switch bootstrap { - case bsCloudInit: - Expect(ctx.Client.Create(ctx, cloudInitSecret)).To(Succeed()) - vm.Spec.Bootstrap = &vmopv1.VirtualMachineBootstrapSpec{ - CloudInit: &vmopv1.VirtualMachineBootstrapCloudInitSpec{ - RawCloudConfig: &common.SecretKeySelector{ - Name: cloudInitSecret.Name, - }, - }, - } - case bsSysprep: - Expect(ctx.Client.Create(ctx, sysprepSecret)).To(Succeed()) - vm.Spec.Bootstrap = &vmopv1.VirtualMachineBootstrapSpec{ - Sysprep: &vmopv1.VirtualMachineBootstrapSysprepSpec{ - RawSysprep: &common.SecretKeySelector{ - Name: sysprepSecret.Name, - Key: "unattend", - }, - }, - } - case bsLinuxPrep: - vm.Spec.Bootstrap = &vmopv1.VirtualMachineBootstrapSpec{ - LinuxPrep: &vmopv1.VirtualMachineBootstrapLinuxPrepSpec{}, - } - } - - vm.Spec.Network = &vmopv1.VirtualMachineNetworkSpec{ - Interfaces: []vmopv1.VirtualMachineNetworkInterfaceSpec{ - { - Name: interfaceName0, - Network: &common.PartialObjectRef{ - Name: networkName0, - }, - }, - }, - } - - if networkEnv == builder.NetworkEnvVPC { - vm.Spec.Network.Interfaces[0].Network.Kind = subnetKind - vm.Spec.Network.Interfaces[0].Network.APIVersion = subnetAPIVersion - } - }) - - It("DoIt", func() { - err := createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("network interface is not ready yet")) - Expect(conditions.IsFalse(vm, vmopv1.VirtualMachineConditionNetworkReady)).To(BeTrue()) - - By("simulate successful network provider reconcile", func() { - np.simulateInterfaceReconcile(ctx, vm, vm.Spec.Network.Interfaces[0], 0) - }) - - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - - By("has expected conditions", func() { - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionClassReady)).To(BeTrue()) - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionImageReady)).To(BeTrue()) - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionStorageReady)).To(BeTrue()) - if bootstrap != "" { - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionBootstrapReady)).To(BeTrue()) - } else { - Expect(conditions.Get(vm, vmopv1.VirtualMachineConditionBootstrapReady)).To(BeNil()) - } - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionNetworkReady)).To(BeTrue()) - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionPlacementReady)).To(BeTrue()) - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionCreated)).To(BeTrue()) - }) - - Expect(vm.Status.UniqueID).ToNot(BeEmpty()) - vcVM := ctx.GetVMFromMoID(vm.Status.UniqueID) - - By("has expected NIC backing", func() { - devList, err := vcVM.Device(ctx) - Expect(err).ToNot(HaveOccurred()) - l := devList.SelectByType(&vimtypes.VirtualEthernetCard{}) - Expect(l).To(HaveLen(1)) - - dev0 := l[0] - np.assertEthernetCard(ctx, dev0, vm.Spec.Network.Interfaces[0], 0) - }) - - By("add network interface", func() { - vm.Spec.Network.Interfaces = append(vm.Spec.Network.Interfaces, vm.Spec.Network.Interfaces[0]) - vm.Spec.Network.Interfaces[1].Name = interfaceName1 - vm.Spec.Network.Interfaces[1].Network = ptr.To(*vm.Spec.Network.Interfaces[1].Network) - vm.Spec.Network.Interfaces[1].Network.Name = networkName1 - }) - - By("power off VM", func() { - vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOff - err = createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("network interface is not ready yet")) - Expect(vcVM.PowerState(ctx)).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOff)) - }) - - By("simulate successful network provider reconcile on added interface", func() { - np.simulateInterfaceReconcile(ctx, vm, vm.Spec.Network.Interfaces[1], 1) - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - }) - - By("power on VM", func() { - vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOn - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vcVM.PowerState(ctx)).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOn)) - }) - - By("Added interface has expected NIC backing", func() { - devList, err := vcVM.Device(ctx) - Expect(err).ToNot(HaveOccurred()) - l := devList.SelectByType(&vimtypes.VirtualEthernetCard{}) - Expect(l).To(HaveLen(2)) - - dev1 := l[1] - np.assertEthernetCard(ctx, dev1, vm.Spec.Network.Interfaces[1], 1) - }) - - By("remove just added network interface", func() { - vm.Spec.Network.Interfaces = vm.Spec.Network.Interfaces[:1] - }) - - By("power off and on VM", func() { - vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOff - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vcVM.PowerState(ctx)).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOff)) - - vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOn - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vcVM.PowerState(ctx)).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOn)) - }) - - By("interface has been removed", func() { - devList, err := vcVM.Device(ctx) - Expect(err).ToNot(HaveOccurred()) - l := devList.SelectByType(&vimtypes.VirtualEthernetCard{}) - Expect(l).To(HaveLen(1)) - - dev0 := l[0] - np.assertEthernetCard(ctx, dev0, vm.Spec.Network.Interfaces[0], 0) - - By("network interface has been deleted", func() { - np.assertNetworkInterfacesDNE(ctx, vm, networkName1, interfaceName1) - }) - }) - }) - }, - Entry("VDS with CloudInit", builder.NetworkEnvVDS, bsCloudInit), - Entry("NSX-T with CloudInit", builder.NetworkEnvNSXT, bsCloudInit), - Entry("VPC with Sysprep", builder.NetworkEnvVPC, bsSysprep), - ) - - DescribeTableSubtree("Simulate VM power off/on with network interface edits", - func(networkEnv builder.NetworkEnv, bootstrap string) { - var np fakeNetworkProvider - - BeforeEach(func() { - testConfig.WithNetworkEnv = networkEnv - - switch networkEnv { - case builder.NetworkEnvVDS: - np = vdsNetworkProvider{} - case builder.NetworkEnvNSXT: - np = nsxtNetworkProvider{} - case builder.NetworkEnvVPC: - np = vpcNetworkProvider{} - } - - // We assert the device type is preserved when editing an interface spec. - configSpec := vimtypes.VirtualMachineConfigSpec{ - DeviceChange: []vimtypes.BaseVirtualDeviceConfigSpec{ - &vimtypes.VirtualDeviceConfigSpec{ - Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, - Device: &vimtypes.VirtualE1000e{}, - }, - &vimtypes.VirtualDeviceConfigSpec{ - Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, - Device: &vimtypes.VirtualVmxnet2{}, - }, - }, - } - - jsonConfigSpec, err := util.MarshalConfigSpecToJSON(configSpec) - Expect(err).ToNot(HaveOccurred()) - vmClass.Spec.ConfigSpec = jsonConfigSpec - }) - - JustBeforeEach(func() { - switch bootstrap { - case bsCloudInit: - Expect(ctx.Client.Create(ctx, cloudInitSecret)).To(Succeed()) - vm.Spec.Bootstrap = &vmopv1.VirtualMachineBootstrapSpec{ - CloudInit: &vmopv1.VirtualMachineBootstrapCloudInitSpec{ - RawCloudConfig: &common.SecretKeySelector{ - Name: cloudInitSecret.Name, - }, - }, - } - case bsSysprep: - Expect(ctx.Client.Create(ctx, sysprepSecret)).To(Succeed()) - vm.Spec.Bootstrap = &vmopv1.VirtualMachineBootstrapSpec{ - Sysprep: &vmopv1.VirtualMachineBootstrapSysprepSpec{ - RawSysprep: &common.SecretKeySelector{ - Name: sysprepSecret.Name, - Key: "unattend", - }, - }, - } - case bsLinuxPrep: - vm.Spec.Bootstrap = &vmopv1.VirtualMachineBootstrapSpec{ - LinuxPrep: &vmopv1.VirtualMachineBootstrapLinuxPrepSpec{}, - } - } - - vm.Spec.Network = &vmopv1.VirtualMachineNetworkSpec{ - Interfaces: []vmopv1.VirtualMachineNetworkInterfaceSpec{ - { - Name: interfaceName0, - Network: &common.PartialObjectRef{ - Name: networkName0, - }, - }, - { - Name: interfaceName1, - Network: &common.PartialObjectRef{ - Name: networkName1, - }, - }, - }, - } - - if networkEnv == builder.NetworkEnvVPC { - for i := range vm.Spec.Network.Interfaces { - vm.Spec.Network.Interfaces[i].Network.Kind = subnetKind - vm.Spec.Network.Interfaces[i].Network.APIVersion = subnetAPIVersion - } - } - }) - - It("DoIt", func() { - err := createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("network interface is not ready yet")) - Expect(conditions.IsFalse(vm, vmopv1.VirtualMachineConditionNetworkReady)).To(BeTrue()) - - By("simulate successful network provider reconcile", func() { - np.simulateInterfaceReconcile(ctx, vm, vm.Spec.Network.Interfaces[0], 0) - }) - - { - // TODO: We should create all the interface CRs up front. - err := createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("network interface is not ready yet")) - Expect(conditions.IsFalse(vm, vmopv1.VirtualMachineConditionNetworkReady)).To(BeTrue()) - } - - By("simulate successful network provider reconcile", func() { - np.simulateInterfaceReconcile(ctx, vm, vm.Spec.Network.Interfaces[1], 1) - }) - - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - - By("has expected conditions", func() { - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionClassReady)).To(BeTrue()) - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionImageReady)).To(BeTrue()) - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionStorageReady)).To(BeTrue()) - if bootstrap != "" { - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionBootstrapReady)).To(BeTrue()) - } else { - Expect(conditions.Get(vm, vmopv1.VirtualMachineConditionBootstrapReady)).To(BeNil()) - } - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionNetworkReady)).To(BeTrue()) - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionPlacementReady)).To(BeTrue()) - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionCreated)).To(BeTrue()) - }) - - Expect(vm.Status.UniqueID).ToNot(BeEmpty()) - vcVM := ctx.GetVMFromMoID(vm.Status.UniqueID) - - By("created with expected NIC device types and backings", func() { - devList, err := vcVM.Device(ctx) - Expect(err).ToNot(HaveOccurred()) - l := devList.SelectByType(&vimtypes.VirtualEthernetCard{}) - Expect(l).To(HaveLen(2)) - - dev0 := l[0] - _, ok := dev0.(*vimtypes.VirtualE1000e) - Expect(ok).To(BeTrue()) - np.assertEthernetCard(ctx, dev0, vm.Spec.Network.Interfaces[0], 0) - - dev1 := l[1] - _, ok = dev1.(*vimtypes.VirtualVmxnet2) - Expect(ok).To(BeTrue()) - np.assertEthernetCard(ctx, dev1, vm.Spec.Network.Interfaces[1], 1) - }) - - By("edit second network interface to use different network", func() { - vm.Spec.Network.Interfaces[1].Network.Name = networkName2 - }) - - By("power off VM", func() { - vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOff - err = createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("network interface is not ready yet")) - Expect(vcVM.PowerState(ctx)).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOff)) - }) - - By("simulate successful network provider reconcile on updated interface", func() { - np.simulateInterfaceReconcile(ctx, vm, vm.Spec.Network.Interfaces[1], 2) - }) - - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - - By("network interface has been deleted", func() { - np.assertNetworkInterfacesDNE(ctx, vm, networkName1, interfaceName1) - }) - - By("powered off VM has expected NIC device types and backings", func() { - devList, err := vcVM.Device(ctx) - Expect(err).ToNot(HaveOccurred()) - l := devList.SelectByType(&vimtypes.VirtualEthernetCard{}) - Expect(l).To(HaveLen(2)) - - // Sometimes even an edit operation will cause vcsim to reorder the devices. - sort.Slice(l, func(i, j int) bool { - return l[i].GetVirtualDevice().Key < l[j].GetVirtualDevice().Key - }) - - dev0 := l[0] - _, ok := dev0.(*vimtypes.VirtualE1000e) - Expect(ok).To(BeTrue()) - np.assertEthernetCard(ctx, dev0, vm.Spec.Network.Interfaces[0], 0) - - dev1 := l[1] - _, ok = dev1.(*vimtypes.VirtualVmxnet2) - Expect(ok).To(BeTrue()) - np.assertEthernetCard(ctx, dev1, vm.Spec.Network.Interfaces[1], 2) - }) - - By("power on VM", func() { - vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOn - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vcVM.PowerState(ctx)).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOn)) - }) - - By("still has expected NIC device types and backings", func() { - devList, err := vcVM.Device(ctx) - Expect(err).ToNot(HaveOccurred()) - l := devList.SelectByType(&vimtypes.VirtualEthernetCard{}) - Expect(l).To(HaveLen(2)) - - sort.Slice(l, func(i, j int) bool { - return l[i].GetVirtualDevice().Key < l[j].GetVirtualDevice().Key - }) - - dev0 := l[0] - _, ok := dev0.(*vimtypes.VirtualE1000e) - Expect(ok).To(BeTrue()) - np.assertEthernetCard(ctx, dev0, vm.Spec.Network.Interfaces[0], 0) - - dev1 := l[1] - _, ok = dev1.(*vimtypes.VirtualVmxnet2) - Expect(ok).To(BeTrue()) - np.assertEthernetCard(ctx, dev1, vm.Spec.Network.Interfaces[1], 2) - }) - - By("power off VM", func() { - vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOff - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vcVM.PowerState(ctx)).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOff)) - }) - - By("update first network interface to use different network", func() { - vm.Spec.Network.Interfaces[0].Network.Name = networkName2 - }) - - By("try to power on VM", func() { - vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOn - err = createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("network interface is not ready yet")) - Expect(vcVM.PowerState(ctx)).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOff)) - }) - - By("simulate successful network provider reconcile on updated interface", func() { - np.simulateInterfaceReconcile(ctx, vm, vm.Spec.Network.Interfaces[0], 2) - }) - - By("power on VM", func() { - err := createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - Expect(vcVM.PowerState(ctx)).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOn)) - }) - - By("has expected NIC device types and backings", func() { - devList, err := vcVM.Device(ctx) - Expect(err).ToNot(HaveOccurred()) - l := devList.SelectByType(&vimtypes.VirtualEthernetCard{}) - Expect(l).To(HaveLen(2)) - - sort.Slice(l, func(i, j int) bool { - return l[i].GetVirtualDevice().Key < l[j].GetVirtualDevice().Key - }) - - dev0 := l[0] - _, ok := dev0.(*vimtypes.VirtualE1000e) - Expect(ok).To(BeTrue()) - np.assertEthernetCard(ctx, dev0, vm.Spec.Network.Interfaces[0], 2) - - dev1 := l[1] - _, ok = dev1.(*vimtypes.VirtualVmxnet2) - Expect(ok).To(BeTrue()) - np.assertEthernetCard(ctx, dev1, vm.Spec.Network.Interfaces[1], 2) - }) - }) - }, - Entry("VDS with CloudInit", builder.NetworkEnvVDS, bsCloudInit), - Entry("VPC with CloudInit", builder.NetworkEnvVPC, bsCloudInit), - Entry("VDS with LinuxPrep", builder.NetworkEnvVDS, bsLinuxPrep), - Entry("NSX-T with Sysprep", builder.NetworkEnvNSXT, bsSysprep), - ) - - Describe("VPC Backup/Restore", func() { - DescribeTableSubtree("Simulate restore", - func(powerState vmopv1.VirtualMachinePowerState, newLSUUID bool) { - var np fakeNetworkProvider - - BeforeEach(func() { - testConfig.WithNetworkEnv = builder.NetworkEnvVPC - np = vpcNetworkProvider{} - - vm.Spec.Network = &vmopv1.VirtualMachineNetworkSpec{ - Interfaces: []vmopv1.VirtualMachineNetworkInterfaceSpec{ - { - Name: interfaceName0, - Network: &common.PartialObjectRef{ - Name: networkName0, - }, - }, - { - Name: interfaceName1, - Network: &common.PartialObjectRef{ - Name: networkName1, - }, - }, - }, - } - - for i := range vm.Spec.Network.Interfaces { - vm.Spec.Network.Interfaces[i].Network.Kind = subnetKind - vm.Spec.Network.Interfaces[i].Network.APIVersion = subnetAPIVersion - } - }) - - It("DoIt", func() { - err := createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("network interface is not ready yet")) - Expect(conditions.IsFalse(vm, vmopv1.VirtualMachineConditionNetworkReady)).To(BeTrue()) - - By("simulate successful network provider reconcile", func() { - np.simulateInterfaceReconcile(ctx, vm, vm.Spec.Network.Interfaces[0], 0) - }) - - { - // TODO: We should create all the interface CRs up front. - err := createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("network interface is not ready yet")) - Expect(conditions.IsFalse(vm, vmopv1.VirtualMachineConditionNetworkReady)).To(BeTrue()) - } - - By("simulate successful network provider reconcile", func() { - np.simulateInterfaceReconcile(ctx, vm, vm.Spec.Network.Interfaces[1], 1) - }) - - vm.Spec.PowerState = powerState - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - - Expect(vm.Status.UniqueID).ToNot(BeEmpty()) - vcVM := ctx.GetVMFromMoID(vm.Status.UniqueID) - - By("created with expected NIC device types and backings", func() { - devList, err := vcVM.Device(ctx) - Expect(err).ToNot(HaveOccurred()) - l := devList.SelectByType(&vimtypes.VirtualEthernetCard{}) - Expect(l).To(HaveLen(2)) - - dev0 := l[0] - _, ok := dev0.(*vimtypes.VirtualVmxnet3) - Expect(ok).To(BeTrue()) - np.assertEthernetCard(ctx, dev0, vm.Spec.Network.Interfaces[0], 0) - - dev1 := l[1] - _, ok = dev1.(*vimtypes.VirtualVmxnet3) - Expect(ok).To(BeTrue()) - np.assertEthernetCard(ctx, dev1, vm.Spec.Network.Interfaces[1], 1) - }) - - By("Verify VM power state", func() { - ps, err := vcVM.PowerState(ctx) - Expect(err).ToNot(HaveOccurred()) - if powerState == vmopv1.VirtualMachinePowerStateOff { - Expect(ps).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOff)) - } else { - Expect(ps).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOn)) - } - }) - - restoredNetworkIdx := 0 - if newLSUUID { - // Restore can have two behaviors: if the network existed before the backup, - // then we expect just a new ExtID. If the network was created between backup - // and restore, we expect both a new ExtID and LSUUID. Use the third network - // to simulate the restore creating a new network. - restoredNetworkIdx = 2 - } - restoredExtID := "" - - By("simulate VPC restore", func() { - interfaceSpec := vm.Spec.Network.Interfaces[0] - interfaceName, networkName := interfaceSpec.Name, interfaceSpec.Network.Name - - subnetPort := &vpcv1alpha1.SubnetPort{} - objKey := client.ObjectKey{ - Name: network.VPCCRName(vm.Name, networkName, interfaceName), - Namespace: vm.Namespace, - } - Expect(ctx.Client.Get(ctx, objKey, subnetPort)).To(Succeed()) - - subnetPort.Status.Attachment.ID += "-restored" - subnetPort.Status.NetworkInterfaceConfig.LogicalSwitchUUID = builder.GetVPCTLogicalSwitchUUID(restoredNetworkIdx) - Expect(ctx.Client.Status().Update(ctx, subnetPort)).To(Succeed()) - - restoredExtID = subnetPort.Status.Attachment.ID - }) - - vm.Annotations[network.VPCInterfaceRestoredAnnotation] = interfaceName0 - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Annotations).ToNot(HaveKey(network.VPCInterfaceRestoredAnnotation)) - - By("restore NIC with expected device types and backings", func() { - devList, err := vcVM.Device(ctx) - Expect(err).ToNot(HaveOccurred()) - l := devList.SelectByType(&vimtypes.VirtualEthernetCard{}) - Expect(l).To(HaveLen(2)) - - // Sometimes even an edit operation will cause vcsim to reorder the devices. - sort.Slice(l, func(i, j int) bool { - return l[i].GetVirtualDevice().Key < l[j].GetVirtualDevice().Key - }) - - dev0 := l[0] - ethCard, ok := dev0.(*vimtypes.VirtualVmxnet3) - Expect(ok).To(BeTrue()) - { - // Assert here that the interface has its expected new ExtID, but then - // restore the original one so we can use assertEthernetCard() as-is. - Expect(ethCard.ExternalId).To(Equal(restoredExtID)) - ethCard.ExternalId = strings.TrimSuffix(ethCard.ExternalId, "-restored") - } - np.assertEthernetCard(ctx, dev0, vm.Spec.Network.Interfaces[0], restoredNetworkIdx) - - dev1 := l[1] - _, ok = dev1.(*vimtypes.VirtualVmxnet3) - Expect(ok).To(BeTrue()) - np.assertEthernetCard(ctx, dev1, vm.Spec.Network.Interfaces[1], 1) - }) - }) - }, - Entry("PoweredOn - Same Network", vmopv1.VirtualMachinePowerStateOn, false), - Entry("PoweredOn - New Network", vmopv1.VirtualMachinePowerStateOn, true), - Entry("PoweredOff - Same Network", vmopv1.VirtualMachinePowerStateOff, false), - Entry("PoweredOff - New Network", vmopv1.VirtualMachinePowerStateOff, true), - ) - }) - }) -} diff --git a/pkg/providers/vsphere/vmprovider_vm_cleanup_test.go b/pkg/providers/vsphere/vmprovider_vm_cleanup_test.go new file mode 100644 index 000000000..0969bae36 --- /dev/null +++ b/pkg/providers/vsphere/vmprovider_vm_cleanup_test.go @@ -0,0 +1,196 @@ +// © Broadcom. All Rights Reserved. +// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package vsphere_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "sigs.k8s.io/controller-runtime/pkg/client" + + corev1 "k8s.io/api/core/v1" + + "github.com/vmware/govmomi/object" + "github.com/vmware/govmomi/vim25/mo" + vimtypes "github.com/vmware/govmomi/vim25/types" + + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha6" + "github.com/vmware-tanzu/vm-operator/pkg/conditions" + pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + ctxop "github.com/vmware-tanzu/vm-operator/pkg/context/operation" + "github.com/vmware-tanzu/vm-operator/pkg/providers" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/constants" + "github.com/vmware-tanzu/vm-operator/pkg/util/kube/cource" + "github.com/vmware-tanzu/vm-operator/pkg/util/ovfcache" + "github.com/vmware-tanzu/vm-operator/test/builder" +) + +func vmCleanupTests() { + const zoneName = "az-1" + + var ( + parentCtx context.Context + initObjects []client.Object + testConfig builder.VCSimTestConfig + ctx *builder.TestContextForVCSim + vmProvider providers.VirtualMachineProviderInterface + nsInfo builder.WorkloadNamespaceInfo + + vm *vmopv1.VirtualMachine + vmClass *vmopv1.VirtualMachineClass + ) + + BeforeEach(func() { + parentCtx = pkgcfg.NewContextWithDefaultConfig() + parentCtx = ctxop.WithContext(parentCtx) + parentCtx = ovfcache.WithContext(parentCtx) + parentCtx = cource.WithContext(parentCtx) + pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { + config.AsyncCreateEnabled = false + config.AsyncSignalEnabled = false + }) + testConfig = builder.VCSimTestConfig{ + WithContentLibrary: true, + } + + vmClass = builder.DummyVirtualMachineClassGenName() + vm = builder.DummyBasicVirtualMachine("test-vm", "") + + if vm.Spec.Network == nil { + vm.Spec.Network = &vmopv1.VirtualMachineNetworkSpec{} + } + vm.Spec.Network.Disabled = true + + // Explicitly place the VM into one of the zones that the test context will create. + vm.Labels[corev1.LabelTopologyZone] = zoneName + }) + + JustBeforeEach(func() { + ctx = suite.NewTestContextForVCSimWithParentContext( + parentCtx, testConfig, initObjects...) + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.MaxDeployThreadsOnProvider = 1 + }) + vmProvider = vsphere.NewVSphereVMProviderFromClient( + ctx, ctx.Client, ctx.Recorder) + nsInfo = ctx.CreateWorkloadNamespace() + + vmClass.Namespace = nsInfo.Namespace + Expect(ctx.Client.Create(ctx, vmClass)).To(Succeed()) + + clusterVMI1 := &vmopv1.ClusterVirtualMachineImage{} + + if testConfig.WithContentLibrary { + Expect(ctx.Client.Get( + ctx, client.ObjectKey{Name: ctx.ContentLibraryItem1Name}, + clusterVMI1)).To(Succeed()) + } else { + vsphere.SkipVMImageCLProviderCheck = true + clusterVMI1 = builder.DummyClusterVirtualMachineImage("DC0_C0_RP0_VM0") + Expect(ctx.Client.Create(ctx, clusterVMI1)).To(Succeed()) + conditions.MarkTrue(clusterVMI1, vmopv1.ReadyConditionType) + Expect(ctx.Client.Status().Update(ctx, clusterVMI1)).To(Succeed()) + } + + vm.Namespace = nsInfo.Namespace + vm.Spec.ClassName = vmClass.Name + vm.Spec.ImageName = clusterVMI1.Name + vm.Spec.Image.Kind = cvmiKind + vm.Spec.Image.Name = clusterVMI1.Name + vm.Spec.StorageClass = ctx.StorageClassName + + Expect(ctx.Client.Create(ctx, vm)).To(Succeed()) + + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + }) + + AfterEach(func() { + vsphere.SkipVMImageCLProviderCheck = false + + if vm != nil && + !pkgcfg.FromContext(ctx).Features.BringYourOwnEncryptionKey { + By("Assert vm.Status.Crypto is nil when BYOK is disabled", func() { + Expect(vm.Status.Crypto).To(BeNil()) + }) + } + + vmClass = nil + vm = nil + + ctx.AfterEach() + ctx = nil + initObjects = nil + vmProvider = nil + nsInfo = builder.WorkloadNamespaceInfo{} + }) + + It("successfully cleans up VM service state", func() { + vcVM, err := ctx.Finder.VirtualMachine(ctx, vm.Name) + Expect(err).NotTo(HaveOccurred()) + + // Set up some VM Operator managed fields + configSpec := vimtypes.VirtualMachineConfigSpec{ + ExtraConfig: []vimtypes.BaseOptionValue{ + &vimtypes.OptionValue{ + Key: constants.ExtraConfigVMServiceNamespacedName, + Value: vm.Namespace + "/" + vm.Name, + }, + &vimtypes.OptionValue{ + Key: "guestinfo.userdata", + Value: "test-data", + }, + }, + ManagedBy: &vimtypes.ManagedByInfo{ + ExtensionKey: vmopv1.ManagedByExtensionKey, + Type: vmopv1.ManagedByExtensionType, + }, + } + + task, err := vcVM.Reconfigure(ctx, configSpec) + Expect(err).NotTo(HaveOccurred()) + Expect(task.Wait(ctx)).To(Succeed()) + + // Verify fields are set before cleanup + var moVMBefore mo.VirtualMachine + err = vcVM.Properties(ctx, vcVM.Reference(), []string{"config"}, &moVMBefore) + Expect(err).NotTo(HaveOccurred()) + Expect(moVMBefore.Config).ToNot(BeNil()) + Expect(moVMBefore.Config.ManagedBy).ToNot(BeNil()) + Expect(moVMBefore.Config.ManagedBy.ExtensionKey).To(Equal(vmopv1.ManagedByExtensionKey)) + + // Run cleanup + Expect(vmProvider.CleanupVirtualMachine(ctx, vm)).To(Succeed()) + + // Verify VM Operator managed fields were removed + var moVMAfter mo.VirtualMachine + err = vcVM.Properties(ctx, vcVM.Reference(), []string{"config"}, &moVMAfter) + Expect(err).NotTo(HaveOccurred()) + Expect(moVMAfter.Config).ToNot(BeNil()) + + // Check ExtraConfig + ecList := object.OptionValueList(moVMAfter.Config.ExtraConfig) + val, ok := ecList.Get(constants.ExtraConfigVMServiceNamespacedName) + Expect(!ok || val == "").To(BeTrue(), "Expected vmservice.namespacedName to be removed") + + val2, ok2 := ecList.Get("guestinfo.userdata") + Expect(!ok2 || val2 == "").To(BeTrue(), "Expected guestinfo.userdata to be removed") + + // Check ManagedBy + Expect(moVMAfter.Config.ManagedBy).To(BeNil()) + + // Verify VM still exists in vCenter + Expect(ctx.GetVMFromMoID(vm.Status.UniqueID)).ToNot(BeNil()) + }) + + It("returns success when VM does not exist in vCenter", func() { + // Delete the VM from vCenter first + Expect(vmProvider.DeleteVirtualMachine(ctx, vm)).To(Succeed()) + + // Cleanup should succeed even though VM doesn't exist + Expect(vmProvider.CleanupVirtualMachine(ctx, vm)).To(Succeed()) + }) +} diff --git a/pkg/providers/vsphere/vmprovider_vm_cns_test.go b/pkg/providers/vsphere/vmprovider_vm_cns_test.go new file mode 100644 index 000000000..d6d771488 --- /dev/null +++ b/pkg/providers/vsphere/vmprovider_vm_cns_test.go @@ -0,0 +1,183 @@ +// © Broadcom. All Rights Reserved. +// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package vsphere_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "sigs.k8s.io/controller-runtime/pkg/client" + + corev1 "k8s.io/api/core/v1" + + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha6" + "github.com/vmware-tanzu/vm-operator/pkg/conditions" + pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + ctxop "github.com/vmware-tanzu/vm-operator/pkg/context/operation" + "github.com/vmware-tanzu/vm-operator/pkg/providers" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" + "github.com/vmware-tanzu/vm-operator/pkg/util/kube/cource" + "github.com/vmware-tanzu/vm-operator/pkg/util/ovfcache" + "github.com/vmware-tanzu/vm-operator/test/builder" +) + +func vmCNSTests() { + + const cnsVolumeName = "cns-volume-1" + + var ( + parentCtx context.Context + initObjects []client.Object + testConfig builder.VCSimTestConfig + ctx *builder.TestContextForVCSim + vmProvider providers.VirtualMachineProviderInterface + nsInfo builder.WorkloadNamespaceInfo + + vm *vmopv1.VirtualMachine + vmClass *vmopv1.VirtualMachineClass + ) + + BeforeEach(func() { + parentCtx = pkgcfg.NewContextWithDefaultConfig() + parentCtx = ctxop.WithContext(parentCtx) + parentCtx = ovfcache.WithContext(parentCtx) + parentCtx = cource.WithContext(parentCtx) + pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { + config.AsyncCreateEnabled = false + config.AsyncSignalEnabled = false + }) + testConfig = builder.VCSimTestConfig{ + WithContentLibrary: true, + } + + vmClass = builder.DummyVirtualMachineClassGenName() + vm = builder.DummyBasicVirtualMachine("test-vm", "") + + if vm.Spec.Network == nil { + vm.Spec.Network = &vmopv1.VirtualMachineNetworkSpec{} + } + vm.Spec.Network.Disabled = true + }) + + JustBeforeEach(func() { + ctx = suite.NewTestContextForVCSimWithParentContext( + parentCtx, testConfig, initObjects...) + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.MaxDeployThreadsOnProvider = 1 + }) + vmProvider = vsphere.NewVSphereVMProviderFromClient( + ctx, ctx.Client, ctx.Recorder) + nsInfo = ctx.CreateWorkloadNamespace() + + vmClass.Namespace = nsInfo.Namespace + Expect(ctx.Client.Create(ctx, vmClass)).To(Succeed()) + + clusterVMI1 := &vmopv1.ClusterVirtualMachineImage{} + + if testConfig.WithContentLibrary { + Expect(ctx.Client.Get( + ctx, client.ObjectKey{Name: ctx.ContentLibraryItem1Name}, + clusterVMI1)).To(Succeed()) + } else { + vsphere.SkipVMImageCLProviderCheck = true + clusterVMI1 = builder.DummyClusterVirtualMachineImage("DC0_C0_RP0_VM0") + Expect(ctx.Client.Create(ctx, clusterVMI1)).To(Succeed()) + conditions.MarkTrue(clusterVMI1, vmopv1.ReadyConditionType) + Expect(ctx.Client.Status().Update(ctx, clusterVMI1)).To(Succeed()) + } + + vm.Namespace = nsInfo.Namespace + vm.Spec.ClassName = vmClass.Name + vm.Spec.ImageName = clusterVMI1.Name + vm.Spec.Image.Kind = cvmiKind + vm.Spec.Image.Name = clusterVMI1.Name + vm.Spec.StorageClass = ctx.StorageClassName + + Expect(ctx.Client.Create(ctx, vm)).To(Succeed()) + + zoneName := ctx.GetFirstZoneName() + vm.Labels[corev1.LabelTopologyZone] = zoneName + Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) + }) + + AfterEach(func() { + vsphere.SkipVMImageCLProviderCheck = false + + if vm != nil && + !pkgcfg.FromContext(ctx).Features.BringYourOwnEncryptionKey { + By("Assert vm.Status.Crypto is nil when BYOK is disabled", func() { + Expect(vm.Status.Crypto).To(BeNil()) + }) + } + + vmClass = nil + vm = nil + + ctx.AfterEach() + ctx = nil + initObjects = nil + vmProvider = nil + nsInfo = builder.WorkloadNamespaceInfo{} + }) + + It("CSI Volumes workflow", func() { + vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOff + _, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOn + By("Add CNS volume to VM", func() { + vm.Spec.Volumes = []vmopv1.VirtualMachineVolume{ + { + Name: cnsVolumeName, + VirtualMachineVolumeSource: vmopv1.VirtualMachineVolumeSource{ + PersistentVolumeClaim: &vmopv1.PersistentVolumeClaimVolumeSource{ + PersistentVolumeClaimVolumeSource: corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "pvc-volume-1", + }, + }, + }, + }, + } + + err := createOrUpdateVM(ctx, vmProvider, vm) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("one or more persistent volumes is pending")) + Expect(err.Error()).To(ContainSubstring(cnsVolumeName)) + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) + }) + + By("CNS volume is not attached", func() { + errMsg := "blah blah blah not attached" + + vm.Status.Volumes = []vmopv1.VirtualMachineVolumeStatus{ + { + Name: cnsVolumeName, + Attached: false, + Error: errMsg, + }, + } + + err := createOrUpdateVM(ctx, vmProvider, vm) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("one or more persistent volumes is pending")) + Expect(err.Error()).To(ContainSubstring(cnsVolumeName)) + + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) + }) + + By("CNS volume is attached", func() { + vm.Status.Volumes = []vmopv1.VirtualMachineVolumeStatus{ + { + Name: cnsVolumeName, + Attached: true, + }, + } + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOn)) + }) + }) +} diff --git a/pkg/providers/vsphere/vmprovider_vm_configspec_test.go b/pkg/providers/vsphere/vmprovider_vm_configspec_test.go new file mode 100644 index 000000000..ebe2db4ad --- /dev/null +++ b/pkg/providers/vsphere/vmprovider_vm_configspec_test.go @@ -0,0 +1,1235 @@ +// © Broadcom. All Rights Reserved. +// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package vsphere_test + +import ( + "bytes" + "context" + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "sigs.k8s.io/controller-runtime/pkg/client" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/google/uuid" + "github.com/vmware/govmomi/object" + "github.com/vmware/govmomi/vim25/mo" + vimtypes "github.com/vmware/govmomi/vim25/types" + + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha6" + vmopv1common "github.com/vmware-tanzu/vm-operator/api/v1alpha6/common" + "github.com/vmware-tanzu/vm-operator/pkg/conditions" + pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + pkgconst "github.com/vmware-tanzu/vm-operator/pkg/constants" + ctxop "github.com/vmware-tanzu/vm-operator/pkg/context/operation" + "github.com/vmware-tanzu/vm-operator/pkg/providers" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/constants" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/virtualmachine" + "github.com/vmware-tanzu/vm-operator/pkg/util/kube/cource" + "github.com/vmware-tanzu/vm-operator/pkg/util/ovfcache" + "github.com/vmware-tanzu/vm-operator/pkg/util/ptr" + "github.com/vmware-tanzu/vm-operator/test/builder" +) + +func vmConfigSpecTests() { + var ( + parentCtx context.Context + initObjects []client.Object + testConfig builder.VCSimTestConfig + ctx *builder.TestContextForVCSim + vmProvider providers.VirtualMachineProviderInterface + nsInfo builder.WorkloadNamespaceInfo + + vm *vmopv1.VirtualMachine + vmClass *vmopv1.VirtualMachineClass + skipCreateOrUpdateVM bool + + vcVM *object.VirtualMachine + configSpec *vimtypes.VirtualMachineConfigSpec + ethCard vimtypes.VirtualEthernetCard + ) + + BeforeEach(func() { + parentCtx = pkgcfg.NewContextWithDefaultConfig() + parentCtx = ctxop.WithContext(parentCtx) + parentCtx = ovfcache.WithContext(parentCtx) + parentCtx = cource.WithContext(parentCtx) + pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { + config.AsyncCreateEnabled = false + config.AsyncSignalEnabled = false + }) + testConfig = builder.VCSimTestConfig{ + WithContentLibrary: true, + } + + vmClass = builder.DummyVirtualMachineClassGenName() + vm = builder.DummyBasicVirtualMachine("test-vm", "") + + // Reduce diff from old tests: by default don't create an NIC. + if vm.Spec.Network == nil { + vm.Spec.Network = &vmopv1.VirtualMachineNetworkSpec{} + } + vm.Spec.Network.Disabled = true + + testConfig.WithNetworkEnv = builder.NetworkEnvNamed + + ethCard = vimtypes.VirtualEthernetCard{ + VirtualDevice: vimtypes.VirtualDevice{ + Key: 4000, + DeviceInfo: &vimtypes.Description{ + Label: "test-configspec-nic-label", + Summary: "VM Network", + }, + SlotInfo: &vimtypes.VirtualDevicePciBusSlotInfo{ + VirtualDeviceBusSlotInfo: vimtypes.VirtualDeviceBusSlotInfo{}, + PciSlotNumber: 32, + }, + ControllerKey: 100, + }, + AddressType: string(vimtypes.VirtualEthernetCardMacTypeManual), + MacAddress: "00:0c:29:93:d7:27", + ResourceAllocation: &vimtypes.VirtualEthernetCardResourceAllocation{ + Reservation: ptr.To[int64](42), + }, + } + }) + + JustBeforeEach(func() { + ctx = suite.NewTestContextForVCSimWithParentContext(parentCtx, testConfig, initObjects...) + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.MaxDeployThreadsOnProvider = 1 + }) + vmProvider = vsphere.NewVSphereVMProviderFromClient(ctx, ctx.Client, ctx.Recorder) + nsInfo = ctx.CreateWorkloadNamespace() + + vmClass.Namespace = nsInfo.Namespace + Expect(ctx.Client.Create(ctx, vmClass)).To(Succeed()) + + clusterVMI1 := &vmopv1.ClusterVirtualMachineImage{} + + if testConfig.WithContentLibrary { + Expect(ctx.Client.Get(ctx, client.ObjectKey{Name: ctx.ContentLibraryItem1Name}, clusterVMI1)).To(Succeed()) + + } else { + // BMV: VM creation without CL is broken - and has been for a long while - since we assume + // the VM Image will always point to a ContentLibrary item. + // Hack around that with this knob so we can continue to test the VM clone path. + vsphere.SkipVMImageCLProviderCheck = true + + // Use the default VM created by vcsim as the source. + clusterVMI1 = builder.DummyClusterVirtualMachineImage("DC0_C0_RP0_VM0") + Expect(ctx.Client.Create(ctx, clusterVMI1)).To(Succeed()) + conditions.MarkTrue(clusterVMI1, vmopv1.ReadyConditionType) + Expect(ctx.Client.Status().Update(ctx, clusterVMI1)).To(Succeed()) + } + + vm.Namespace = nsInfo.Namespace + vm.Spec.ClassName = vmClass.Name + vm.Spec.ImageName = clusterVMI1.Name + vm.Spec.Image.Kind = cvmiKind + vm.Spec.Image.Name = clusterVMI1.Name + vm.Spec.StorageClass = ctx.StorageClassName + + Expect(ctx.Client.Create(ctx, vm)).To(Succeed()) + + if configSpec != nil { + var w bytes.Buffer + enc := vimtypes.NewJSONEncoder(&w) + Expect(enc.Encode(configSpec)).To(Succeed()) + + // Update the VM Class with the XML. + vmClass.Spec.ConfigSpec = w.Bytes() + Expect(ctx.Client.Update(ctx, vmClass)).To(Succeed()) + } + + vm.Spec.Network.Disabled = false + vm.Spec.Network.Interfaces = []vmopv1.VirtualMachineNetworkInterfaceSpec{ + { + Name: "eth0", + Network: &vmopv1common.PartialObjectRef{Name: dvpgName}, + }, + } + + if !skipCreateOrUpdateVM { + var err error + vcVM, err = createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + } + }) + + AfterEach(func() { + vcVM = nil + configSpec = nil + + vsphere.SkipVMImageCLProviderCheck = false + + if vm != nil && !pkgcfg.FromContext(ctx).Features.BringYourOwnEncryptionKey { + By("Assert vm.Status.Crypto is nil when BYOK is disabled", func() { + Expect(vm.Status.Crypto).To(BeNil()) + }) + } + + vmClass = nil + vm = nil + skipCreateOrUpdateVM = false + + ctx.AfterEach() + ctx = nil + initObjects = nil + vmProvider = nil + nsInfo = builder.WorkloadNamespaceInfo{} + }) + + Context("FSS_WCP_MOBILITY_VM_IMPORT_NEW_NET", func() { + var instanceUUID string + + BeforeEach(func() { + skipCreateOrUpdateVM = true + }) + + JustBeforeEach(func() { + vmList, err := ctx.Finder.VirtualMachineList(ctx, "*") + Expect(err).ToNot(HaveOccurred()) + Expect(vmList).ToNot(BeEmpty()) + + vcVM = vmList[0] + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + instanceUUID = o.Config.InstanceUuid + vm.Spec.InstanceUUID = instanceUUID + + powerState, err := vcVM.PowerState(ctx) + Expect(err).ToNot(HaveOccurred()) + if powerState == vimtypes.VirtualMachinePowerStatePoweredOn { + tsk, err := vcVM.PowerOff(ctx) + Expect(err).ToNot(HaveOccurred()) + Expect(tsk.Wait(ctx)).To(Succeed()) + } + vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOn + }) + + When("fss is disabled", func() { + JustBeforeEach(func() { + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.Features.VMImportNewNet = false + }) + }) + + assertClassNotFound := func( + ctx *builder.TestContextForVCSim, + vmProvider providers.VirtualMachineProviderInterface, + vm *vmopv1.VirtualMachine, + className string) { + + vcVM, err := createOrUpdateAndGetVcVM( + ctx, vmProvider, vm) + ExpectWithOffset(1, err).ToNot(BeNil()) + ExpectWithOffset(1, err.Error()).To(ContainSubstring( + fmt.Sprintf( + "virtualmachineclasses.vmoperator.vmware.com %q not found", + className))) + ExpectWithOffset(1, vcVM).To(BeNil()) + } + + When("spec.className is empty", func() { + JustBeforeEach(func() { + vm.Spec.ClassName = "" + }) + When("spec.instanceUUID matches existing VM", func() { + JustBeforeEach(func() { + vm.Spec.InstanceUUID = instanceUUID + }) + It("should error when getting class", func() { + assertClassNotFound( + ctx, + vmProvider, + vm, + "") + }) + }) + When("spec.instanceUUID does not match existing VM", func() { + JustBeforeEach(func() { + vm.Spec.InstanceUUID = uuid.NewString() + }) + It("should error when getting class", func() { + assertClassNotFound( + ctx, + vmProvider, + vm, + "") + }) + }) + }) + + }) + + When("fss is enabled", func() { + + assertPoweredOnNoVMClassCondition := func() { + var err error + vcVM, err = createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + ExpectWithOffset(1, err).ToNot(HaveOccurred()) + ExpectWithOffset(1, vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOn)) + powerState, err := vcVM.PowerState(ctx) + ExpectWithOffset(1, err).ToNot(HaveOccurred()) + ExpectWithOffset(1, powerState).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOn)) + ExpectWithOffset(1, conditions.IsTrue(vm, vmopv1.VirtualMachineConditionClassReady)).To(BeFalse()) + } + + JustBeforeEach(func() { + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.Features.VMImportNewNet = true + }) + }) + + When("spec.className is empty", func() { + JustBeforeEach(func() { + vm.Spec.ClassName = "" + }) + When("spec.instanceUUID matches existing VM", func() { + JustBeforeEach(func() { + vm.Spec.InstanceUUID = instanceUUID + }) + It("should synthesize class from vSphere VM and power it on", func() { + assertPoweredOnNoVMClassCondition() + }) + }) + When("spec.instanceUUID does not match existing VM", func() { + JustBeforeEach(func() { + vm.Spec.InstanceUUID = uuid.NewString() + }) + It("should return an error", func() { + var err error + vcVM, err = createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).To(MatchError("cannot synthesize class from nil ConfigInfo")) + Expect(vcVM).To(BeNil()) + }) + }) + }) + }) + }) + + Context("GetVirtualMachineProperties", func() { + const ( + propName = "config.name" + propPowerState = "runtime.powerState" + propExtraConfig = "config.extraConfig" + propPathName = "config.files.vmPathName" + propExtraConfigKeyKey = "vmservice.example" + propExtraConfigKey = `config.extraConfig["` + propExtraConfigKeyKey + `"]` + ) + var ( + err error + result map[string]any + propertyPaths []string + ) + AfterEach(func() { + propertyPaths = nil + }) + JustBeforeEach(func() { + if len(propertyPaths) > 0 { + result, err = vmProvider.GetVirtualMachineProperties(ctx, vm, propertyPaths) + } + }) + When("getting "+propExtraConfig, func() { + BeforeEach(func() { + propertyPaths = []string{propExtraConfig} + }) + It("should retrieve a non-zero number of properties", func() { + Expect(err).ToNot(HaveOccurred()) + Expect(result).ToNot(HaveLen(0)) + }) + }) + DescribeTable("getting "+propExtraConfigKey, + func(val any) { + t, err := vcVM.Reconfigure(ctx, vimtypes.VirtualMachineConfigSpec{ + ExtraConfig: []vimtypes.BaseOptionValue{ + &vimtypes.OptionValue{ + Key: propExtraConfigKeyKey, + Value: val, + }, + }, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(t.Wait(ctx)).To(Succeed()) + + result, err := vmProvider.GetVirtualMachineProperties( + ctx, + vm, + []string{propExtraConfigKey}) + + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(HaveKeyWithValue( + propExtraConfigKey, + vimtypes.OptionValue{ + Key: propExtraConfigKeyKey, + Value: val, + })) + }, + Entry("value is a string", "Hello, world."), + Entry("value is a uint8", uint8(8)), + Entry("value is an int32", int32(32)), + Entry("value is a float64", float64(64)), + Entry("value is a bool", true), + ) + When("getting "+propName, func() { + BeforeEach(func() { + propertyPaths = []string{propName} + }) + It("should retrieve a single property", func() { + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(HaveLen(1)) + Expect(result[propName]).To(Equal(vm.Name)) + }) + }) + When("getting "+propPowerState, func() { + BeforeEach(func() { + propertyPaths = []string{propPowerState} + }) + It("should retrieve a single property", func() { + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(HaveLen(1)) + switch vm.Spec.PowerState { + case vmopv1.VirtualMachinePowerStateOn: + Expect(result[propPowerState]).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOn)) + case vmopv1.VirtualMachinePowerStateOff: + Expect(result[propPowerState]).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOff)) + case vmopv1.VirtualMachinePowerStateSuspended: + Expect(result[propPowerState]).To(Equal(vimtypes.VirtualMachinePowerStateSuspended)) + default: + panic(fmt.Sprintf("invalid power state: %s", vm.Spec.PowerState)) + } + }) + }) + When("getting "+propPathName, func() { + BeforeEach(func() { + propertyPaths = []string{propPathName} + }) + It("should not be set", func() { + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(HaveLen(1)) + Expect(result[propName]).To(BeNil()) // should only be set if cdrom is present + }) + }) + }) + + Context("VM Class has no ConfigSpec", func() { + BeforeEach(func() { + configSpec = nil + }) + + It("creates VM", func() { + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionCreated)).To(BeTrue()) + + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + Expect(o.Config.Annotation).To(Equal(constants.VCVMAnnotation)) + Expect(o.Summary.Config.NumCpu).To(BeEquivalentTo(vmClass.Spec.Hardware.Cpus)) + Expect(o.Summary.Config.MemorySizeMB).To(BeEquivalentTo(vmClass.Spec.Hardware.Memory.Value() / 1024 / 1024)) + }) + }) + + Context("ConfigSpec specifies annotation", func() { + BeforeEach(func() { + configSpec = &vimtypes.VirtualMachineConfigSpec{ + Annotation: "my-annotation", + } + }) + + It("VM has class annotation", func() { + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionCreated)).To(BeTrue()) + + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + Expect(o.Config.Annotation).To(Equal("my-annotation")) + }) + }) + + Context("ConfigSpec specifies hardware spec", func() { + BeforeEach(func() { + configSpec = &vimtypes.VirtualMachineConfigSpec{ + Name: "config-spec-name-is-not-used", + NumCPUs: 7, + MemoryMB: 5102, + } + }) + + It("CPU and memory from ConfigSpec are ignored", func() { + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionCreated)).To(BeTrue()) + + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + Expect(o.Summary.Config.Name).To(Equal(vm.Name)) + Expect(o.Summary.Config.NumCpu).To(BeEquivalentTo(vmClass.Spec.Hardware.Cpus)) + Expect(o.Summary.Config.NumCpu).ToNot(BeEquivalentTo(configSpec.NumCPUs)) + Expect(o.Summary.Config.MemorySizeMB).To(BeEquivalentTo(vmClass.Spec.Hardware.Memory.Value() / 1024 / 1024)) + Expect(o.Summary.Config.MemorySizeMB).ToNot(BeEquivalentTo(configSpec.MemoryMB)) + }) + }) + + Context("VM Class spec CPU reservation & limits are non-zero and ConfigSpec specifies CPU reservation", func() { + BeforeEach(func() { + vmClass.Spec.Policies.Resources.Requests.Cpu = resource.MustParse("2") + vmClass.Spec.Policies.Resources.Limits.Cpu = resource.MustParse("3") + + // Specify a CPU reservation via ConfigSpec. This value should not be honored. + configSpec = &vimtypes.VirtualMachineConfigSpec{ + CpuAllocation: &vimtypes.ResourceAllocationInfo{ + Reservation: ptr.To[int64](6), + }, + } + }) + + It("VM gets CPU reservation from VM Class spec", func() { + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionCreated)).To(BeTrue()) + + resources := &vmClass.Spec.Policies.Resources + + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + + reservation := o.Config.CpuAllocation.Reservation + Expect(reservation).ToNot(BeNil()) + Expect(*reservation).To(Equal(virtualmachine.CPUQuantityToMhz(resources.Requests.Cpu, vcsimCPUFreq))) + Expect(*reservation).ToNot(Equal(*configSpec.CpuAllocation.Reservation)) + + limit := o.Config.CpuAllocation.Limit + Expect(limit).ToNot(BeNil()) + Expect(*limit).To(Equal(virtualmachine.CPUQuantityToMhz(resources.Limits.Cpu, vcsimCPUFreq))) + }) + }) + + Context("VM Class spec CPU reservation is zero and ConfigSpec specifies CPU reservation", func() { + BeforeEach(func() { + vmClass.Spec.Policies.Resources.Requests.Cpu = resource.MustParse("0") + vmClass.Spec.Policies.Resources.Limits.Cpu = resource.MustParse("0") + + // Specify a CPU reservation via ConfigSpec + configSpec = &vimtypes.VirtualMachineConfigSpec{ + CpuAllocation: &vimtypes.ResourceAllocationInfo{ + Reservation: ptr.To[int64](6), + }, + } + }) + + It("VM gets CPU reservation from ConfigSpec", func() { + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionCreated)).To(BeTrue()) + + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + + reservation := o.Config.CpuAllocation.Reservation + Expect(reservation).ToNot(BeNil()) + Expect(*reservation).ToNot(BeZero()) + Expect(*reservation).To(Equal(*configSpec.CpuAllocation.Reservation)) + }) + }) + + Context("VM Class spec Memory reservation & limits are non-zero and ConfigSpec specifies memory reservation", func() { + BeforeEach(func() { + vmClass.Spec.Policies.Resources.Requests.Memory = resource.MustParse("4Mi") + vmClass.Spec.Policies.Resources.Limits.Memory = resource.MustParse("4Mi") + + // Specify a Memory reservation via ConfigSpec + configSpec = &vimtypes.VirtualMachineConfigSpec{ + MemoryAllocation: &vimtypes.ResourceAllocationInfo{ + Reservation: ptr.To[int64](5120), + }, + } + }) + + It("VM gets memory reservation from VM Class spec", func() { + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionCreated)).To(BeTrue()) + + resources := &vmClass.Spec.Policies.Resources + + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + + reservation := o.Config.MemoryAllocation.Reservation + Expect(reservation).ToNot(BeNil()) + Expect(*reservation).To(Equal(virtualmachine.MemoryQuantityToMb(resources.Requests.Memory))) + Expect(*reservation).ToNot(Equal(*configSpec.MemoryAllocation.Reservation)) + + limit := o.Config.MemoryAllocation.Limit + Expect(limit).ToNot(BeNil()) + Expect(*limit).To(Equal(virtualmachine.MemoryQuantityToMb(resources.Limits.Memory))) + }) + }) + + Context("VM Class spec Memory reservations are zero and ConfigSpec specifies memory reservation", func() { + BeforeEach(func() { + vmClass.Spec.Policies.Resources.Requests.Memory = resource.MustParse("0Mi") + vmClass.Spec.Policies.Resources.Limits.Memory = resource.MustParse("0Mi") + + // Specify a Memory reservation via ConfigSpec + configSpec = &vimtypes.VirtualMachineConfigSpec{ + MemoryAllocation: &vimtypes.ResourceAllocationInfo{ + Reservation: ptr.To[int64](5120), + }, + } + }) + + It("VM gets memory reservation from ConfigSpec", func() { + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionCreated)).To(BeTrue()) + + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + + reservation := o.Config.MemoryAllocation.Reservation + Expect(reservation).ToNot(BeNil()) + Expect(*reservation).ToNot(BeZero()) + Expect(*reservation).To(Equal(*configSpec.MemoryAllocation.Reservation)) + }) + }) + + Context("VM Class ConfigSpec specifies a network interface", func() { + + BeforeEach(func() { + testConfig.WithNetworkEnv = builder.NetworkEnvNamed + + // Create the ConfigSpec with an ethernet card. + configSpec = &vimtypes.VirtualMachineConfigSpec{ + DeviceChange: []vimtypes.BaseVirtualDeviceConfigSpec{ + &vimtypes.VirtualDeviceConfigSpec{ + Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, + Device: &vimtypes.VirtualE1000{ + VirtualEthernetCard: ethCard, + }, + }, + }, + } + }) + + It("Reconfigures the VM with the NIC specified in ConfigSpec", func() { + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionCreated)).To(BeTrue()) + + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + + devList := object.VirtualDeviceList(o.Config.Hardware.Device) + l := devList.SelectByType(&vimtypes.VirtualEthernetCard{}) + Expect(l).To(HaveLen(1)) + + dev := l[0].GetVirtualDevice() + backing, ok := dev.Backing.(*vimtypes.VirtualEthernetCardDistributedVirtualPortBackingInfo) + Expect(ok).Should(BeTrue()) + _, dvpg := getDVPG(ctx, dvpgName) + Expect(backing.Port.PortgroupKey).To(Equal(dvpg.Reference().Value)) + + ethDevice, ok := l[0].(*vimtypes.VirtualE1000) + Expect(ok).To(BeTrue()) + Expect(ethDevice.AddressType).To(Equal(ethCard.AddressType)) + Expect(ethDevice.MacAddress).To(Equal(ethCard.MacAddress)) + + Expect(dev.DeviceInfo).To(Equal(ethCard.VirtualDevice.DeviceInfo)) + Expect(dev.DeviceGroupInfo).To(Equal(ethCard.VirtualDevice.DeviceGroupInfo)) + Expect(dev.SlotInfo).To(Equal(ethCard.VirtualDevice.SlotInfo)) + Expect(dev.ControllerKey).To(Equal(ethCard.VirtualDevice.ControllerKey)) + Expect(ethDevice.ResourceAllocation).ToNot(BeNil()) + Expect(ethDevice.ResourceAllocation.Reservation).ToNot(BeNil()) + Expect(*ethDevice.ResourceAllocation.Reservation).To(Equal(*ethCard.ResourceAllocation.Reservation)) + }) + }) + + Context("ConfigSpec does not specify any network interfaces", func() { + + BeforeEach(func() { + testConfig.WithNetworkEnv = builder.NetworkEnvNamed + + configSpec = &vimtypes.VirtualMachineConfigSpec{} + }) + + It("Reconfigures the VM with the default NIC settings from provider", func() { + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + + devList := object.VirtualDeviceList(o.Config.Hardware.Device) + l := devList.SelectByType(&vimtypes.VirtualEthernetCard{}) + Expect(l).To(HaveLen(1)) + + dev := l[0].GetVirtualDevice() + backing, ok := dev.Backing.(*vimtypes.VirtualEthernetCardDistributedVirtualPortBackingInfo) + Expect(ok).Should(BeTrue()) + _, dvpg := getDVPG(ctx, dvpgName) + Expect(backing.Port.PortgroupKey).To(Equal(dvpg.Reference().Value)) + }) + }) + + Context("VM Class Spec and ConfigSpec both contain GPU and DirectPath devices", func() { + BeforeEach(func() { + vmClass.Spec.Hardware.Devices = vmopv1.VirtualDevices{ + VGPUDevices: []vmopv1.VGPUDevice{ + { + ProfileName: "profile-from-class", + }, + }, + DynamicDirectPathIODevices: []vmopv1.DynamicDirectPathIODevice{ + { + VendorID: 50, + DeviceID: 51, + CustomLabel: "label-from-class", + }, + }, + } + + // Create the ConfigSpec with a GPU and a DDPIO device. + configSpec = &vimtypes.VirtualMachineConfigSpec{ + DeviceChange: []vimtypes.BaseVirtualDeviceConfigSpec{ + &vimtypes.VirtualDeviceConfigSpec{ + Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, + Device: &vimtypes.VirtualPCIPassthrough{ + VirtualDevice: vimtypes.VirtualDevice{ + Backing: &vimtypes.VirtualPCIPassthroughVmiopBackingInfo{ + Vgpu: "profile-from-config-spec", + }, + }, + }, + }, + &vimtypes.VirtualDeviceConfigSpec{ + Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, + Device: &vimtypes.VirtualPCIPassthrough{ + VirtualDevice: vimtypes.VirtualDevice{ + Backing: &vimtypes.VirtualPCIPassthroughDynamicBackingInfo{ + AllowedDevice: []vimtypes.VirtualPCIPassthroughAllowedDevice{ + { + VendorId: 52, + DeviceId: 53, + }, + }, + CustomLabel: "label-from-config-spec", + }, + }, + }, + }, + }, + } + }) + + It("GPU and DirectPath devices from VM Class Spec.Devices are ignored", func() { + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + + devList := object.VirtualDeviceList(o.Config.Hardware.Device) + p := devList.SelectByType(&vimtypes.VirtualPCIPassthrough{}) + Expect(p).To(HaveLen(2)) + + pciDev1 := p[0].GetVirtualDevice() + pciBacking1, ok1 := pciDev1.Backing.(*vimtypes.VirtualPCIPassthroughVmiopBackingInfo) + Expect(ok1).Should(BeTrue()) + Expect(pciBacking1.Vgpu).To(Equal("profile-from-config-spec")) + + pciDev2 := p[1].GetVirtualDevice() + pciBacking2, ok2 := pciDev2.Backing.(*vimtypes.VirtualPCIPassthroughDynamicBackingInfo) + Expect(ok2).Should(BeTrue()) + Expect(pciBacking2.AllowedDevice).To(HaveLen(1)) + Expect(pciBacking2.AllowedDevice[0].VendorId).To(Equal(int32(52))) + Expect(pciBacking2.AllowedDevice[0].DeviceId).To(Equal(int32(53))) + Expect(pciBacking2.CustomLabel).To(Equal("label-from-config-spec")) + }) + }) + + Context("VM Class Config specifies an ethCard, a GPU and a DDPIO device", func() { + + BeforeEach(func() { + // Create the ConfigSpec with an ethernet card, a GPU and a DDPIO device. + configSpec = &vimtypes.VirtualMachineConfigSpec{ + DeviceChange: []vimtypes.BaseVirtualDeviceConfigSpec{ + &vimtypes.VirtualDeviceConfigSpec{ + Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, + Device: &vimtypes.VirtualE1000{ + VirtualEthernetCard: ethCard, + }, + }, + &vimtypes.VirtualDeviceConfigSpec{ + Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, + Device: &vimtypes.VirtualPCIPassthrough{ + VirtualDevice: vimtypes.VirtualDevice{ + Backing: &vimtypes.VirtualPCIPassthroughVmiopBackingInfo{ + Vgpu: "SampleProfile2", + }, + }, + }, + }, + &vimtypes.VirtualDeviceConfigSpec{ + Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, + Device: &vimtypes.VirtualPCIPassthrough{ + VirtualDevice: vimtypes.VirtualDevice{ + Backing: &vimtypes.VirtualPCIPassthroughDynamicBackingInfo{ + AllowedDevice: []vimtypes.VirtualPCIPassthroughAllowedDevice{ + { + VendorId: 52, + DeviceId: 53, + }, + }, + CustomLabel: "SampleLabel2", + }, + }, + }, + }, + }, + } + }) + + It("Reconfigures the VM with a NIC, GPU and DDPIO device specified in ConfigSpec", func() { + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + + devList := object.VirtualDeviceList(o.Config.Hardware.Device) + l := devList.SelectByType(&vimtypes.VirtualEthernetCard{}) + Expect(l).To(HaveLen(1)) + + dev := l[0].GetVirtualDevice() + backing, ok := dev.Backing.(*vimtypes.VirtualEthernetCardDistributedVirtualPortBackingInfo) + Expect(ok).Should(BeTrue()) + _, dvpg := getDVPG(ctx, dvpgName) + Expect(backing.Port.PortgroupKey).To(Equal(dvpg.Reference().Value)) + + ethDevice, ok := l[0].(*vimtypes.VirtualE1000) + Expect(ok).To(BeTrue()) + Expect(ethDevice.AddressType).To(Equal(ethCard.AddressType)) + Expect(dev.DeviceInfo).To(Equal(ethCard.VirtualDevice.DeviceInfo)) + Expect(dev.DeviceGroupInfo).To(Equal(ethCard.VirtualDevice.DeviceGroupInfo)) + Expect(dev.SlotInfo).To(Equal(ethCard.VirtualDevice.SlotInfo)) + Expect(dev.ControllerKey).To(Equal(ethCard.VirtualDevice.ControllerKey)) + Expect(ethDevice.MacAddress).To(Equal(ethCard.MacAddress)) + Expect(ethDevice.ResourceAllocation).ToNot(BeNil()) + Expect(ethDevice.ResourceAllocation.Reservation).ToNot(BeNil()) + Expect(*ethDevice.ResourceAllocation.Reservation).To(Equal(*ethCard.ResourceAllocation.Reservation)) + + p := devList.SelectByType(&vimtypes.VirtualPCIPassthrough{}) + Expect(p).To(HaveLen(2)) + pciDev1 := p[0].GetVirtualDevice() + pciBacking1, ok1 := pciDev1.Backing.(*vimtypes.VirtualPCIPassthroughVmiopBackingInfo) + Expect(ok1).Should(BeTrue()) + Expect(pciBacking1.Vgpu).To(Equal("SampleProfile2")) + pciDev2 := p[1].GetVirtualDevice() + pciBacking2, ok2 := pciDev2.Backing.(*vimtypes.VirtualPCIPassthroughDynamicBackingInfo) + Expect(ok2).Should(BeTrue()) + Expect(pciBacking2.AllowedDevice).To(HaveLen(1)) + Expect(pciBacking2.AllowedDevice[0].VendorId).To(Equal(int32(52))) + Expect(pciBacking2.AllowedDevice[0].DeviceId).To(Equal(int32(53))) + Expect(pciBacking2.CustomLabel).To(Equal("SampleLabel2")) + + // CPU and memory should be from vm class + Expect(o.Summary.Config.NumCpu).To(BeEquivalentTo(vmClass.Spec.Hardware.Cpus)) + Expect(o.Summary.Config.MemorySizeMB).To(BeEquivalentTo(vmClass.Spec.Hardware.Memory.Value() / 1024 / 1024)) + }) + }) + + Context("VM Class Config specifies disks, disk controllers, other miscellaneous devices", func() { + BeforeEach(func() { + // Create the ConfigSpec with disks, disk controller and some misc devices: pointing device, + // video card, etc. This works fine with vcsim and helps with testing adding misc devices. + // The simulator can still reconfigure the VM with default device types like pointing devices, + // keyboard, video card, etc. But VC has some restrictions with reconfiguring a VM with new + // default device types via ConfigSpec and are usually ignored. + configSpec = &vimtypes.VirtualMachineConfigSpec{ + DeviceChange: []vimtypes.BaseVirtualDeviceConfigSpec{ + &vimtypes.VirtualDeviceConfigSpec{ + Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, + Device: &vimtypes.VirtualPointingDevice{ + VirtualDevice: vimtypes.VirtualDevice{ + Backing: &vimtypes.VirtualPointingDeviceDeviceBackingInfo{ + HostPointingDevice: "autodetect", + }, + Key: 700, + ControllerKey: 300, + }, + }, + }, + &vimtypes.VirtualDeviceConfigSpec{ + Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, + Device: &vimtypes.VirtualPS2Controller{ + VirtualController: vimtypes.VirtualController{ + Device: []int32{700}, + VirtualDevice: vimtypes.VirtualDevice{ + Key: 300, + }, + }, + }, + }, + &vimtypes.VirtualDeviceConfigSpec{ + Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, + Device: &vimtypes.VirtualMachineVideoCard{ + UseAutoDetect: ptr.To(false), + NumDisplays: 1, + VirtualDevice: vimtypes.VirtualDevice{ + Key: 500, + ControllerKey: 100, + }, + }, + }, + &vimtypes.VirtualDeviceConfigSpec{ + Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, + Device: &vimtypes.VirtualPCIController{ + VirtualController: vimtypes.VirtualController{ + Device: []int32{500}, + VirtualDevice: vimtypes.VirtualDevice{ + Key: 100, + }, + }, + }, + }, + &vimtypes.VirtualDeviceConfigSpec{ + Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, + Device: &vimtypes.VirtualDisk{ + CapacityInBytes: 1024, + VirtualDevice: vimtypes.VirtualDevice{ + Key: -42, + Backing: &vimtypes.VirtualDiskFlatVer2BackingInfo{ + ThinProvisioned: ptr.To(true), + }, + }, + }, + }, + &vimtypes.VirtualDeviceConfigSpec{ + Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, + Device: &vimtypes.VirtualSCSIController{ + VirtualController: vimtypes.VirtualController{ + Device: []int32{-42}, + }, + }, + }, + }, + } + }) + + // FIXME: vcsim behavior needs to be closer to real VC here so there aren't dupes + It("Reconfigures the VM with all misc devices in ConfigSpec, including SCSI disk controller", func() { + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + + devList := object.VirtualDeviceList(o.Config.Hardware.Device) + + // VM already has a default pointing device and the spec adds one more + // info about the default device is unknown to assert on + pointingDev := devList.SelectByType(&vimtypes.VirtualPointingDevice{}) + Expect(pointingDev).To(HaveLen(2)) + dev := pointingDev[0].GetVirtualDevice() + backing, ok := dev.Backing.(*vimtypes.VirtualPointingDeviceDeviceBackingInfo) + Expect(ok).Should(BeTrue()) + Expect(backing.HostPointingDevice).To(Equal("autodetect")) + Expect(dev.Key).To(Equal(int32(700))) + Expect(dev.ControllerKey).To(Equal(int32(300))) + + ps2Controllers := devList.SelectByType(&vimtypes.VirtualPS2Controller{}) + Expect(ps2Controllers).To(HaveLen(1)) + dev = ps2Controllers[0].GetVirtualDevice() + Expect(dev.Key).To(Equal(int32(300))) + + pciControllers := devList.SelectByType(&vimtypes.VirtualPCIController{}) + Expect(pciControllers).To(HaveLen(1)) + dev = pciControllers[0].GetVirtualDevice() + Expect(dev.Key).To(Equal(int32(100))) + + // VM already has a default video card and the spec adds one more + // info about the default device is unknown to assert on + video := devList.SelectByType(&vimtypes.VirtualMachineVideoCard{}) + Expect(video).To(HaveLen(2)) + dev = video[0].GetVirtualDevice() + Expect(dev.Key).To(Equal(int32(500))) + Expect(dev.ControllerKey).To(Equal(int32(100))) + + // SCSI disk controllers may remain due to CNS and RDM. + diskControllers := devList.SelectByType(&vimtypes.VirtualSCSIController{}) + Expect(diskControllers).To(HaveLen(1)) + + // Only preexisting disk should be present on VM -- len: 1 + disks := devList.SelectByType(&vimtypes.VirtualDisk{}) + Expect(disks).To(HaveLen(1)) + dev = disks[0].GetVirtualDevice() + Expect(dev.Key).ToNot(Equal(int32(-42))) + }) + }) + + Context("VM Class Config does not specify a hardware version", func() { + + Context("VM Class has vGPU and/or DDPIO devices", func() { + BeforeEach(func() { + // Create the ConfigSpec with a GPU and a DDPIO device. + configSpec = &vimtypes.VirtualMachineConfigSpec{ + Name: "dummy-VM", + DeviceChange: []vimtypes.BaseVirtualDeviceConfigSpec{ + &vimtypes.VirtualDeviceConfigSpec{ + Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, + Device: &vimtypes.VirtualPCIPassthrough{ + VirtualDevice: vimtypes.VirtualDevice{ + Backing: &vimtypes.VirtualPCIPassthroughVmiopBackingInfo{ + Vgpu: "profile-from-configspec", + }, + }, + }, + }, + &vimtypes.VirtualDeviceConfigSpec{ + Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, + Device: &vimtypes.VirtualPCIPassthrough{ + VirtualDevice: vimtypes.VirtualDevice{ + Backing: &vimtypes.VirtualPCIPassthroughDynamicBackingInfo{ + AllowedDevice: []vimtypes.VirtualPCIPassthroughAllowedDevice{ + { + VendorId: 52, + DeviceId: 53, + }, + }, + CustomLabel: "label-from-configspec", + }, + }, + }, + }, + }, + } + }) + + It("creates a VM with a hardware version minimum supported for PCI devices", func() { + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + Expect(o.Config.Version).To(Equal(fmt.Sprintf("vmx-%d", pkgconst.MinSupportedHWVersionForPCIPassthruDevices))) + }) + }) + + Context("VM Class has vGPU and/or DDPIO devices and VM spec has a PVC", func() { + BeforeEach(func() { + // Need to create the PVC before creating the VM. + skipCreateOrUpdateVM = true + + // Create the ConfigSpec with a GPU and a DDPIO device. + configSpec = &vimtypes.VirtualMachineConfigSpec{ + Name: "dummy-VM", + DeviceChange: []vimtypes.BaseVirtualDeviceConfigSpec{ + &vimtypes.VirtualDeviceConfigSpec{ + Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, + Device: &vimtypes.VirtualPCIPassthrough{ + VirtualDevice: vimtypes.VirtualDevice{ + Backing: &vimtypes.VirtualPCIPassthroughVmiopBackingInfo{ + Vgpu: "profile-from-configspec", + }, + }, + }, + }, + &vimtypes.VirtualDeviceConfigSpec{ + Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, + Device: &vimtypes.VirtualPCIPassthrough{ + VirtualDevice: vimtypes.VirtualDevice{ + Backing: &vimtypes.VirtualPCIPassthroughDynamicBackingInfo{ + AllowedDevice: []vimtypes.VirtualPCIPassthroughAllowedDevice{ + { + VendorId: 52, + DeviceId: 53, + }, + }, + CustomLabel: "label-from-configspec", + }, + }, + }, + }, + }, + } + + vm.Spec.Volumes = []vmopv1.VirtualMachineVolume{ + { + Name: "dummy-vol", + VirtualMachineVolumeSource: vmopv1.VirtualMachineVolumeSource{ + PersistentVolumeClaim: &vmopv1.PersistentVolumeClaimVolumeSource{ + PersistentVolumeClaimVolumeSource: corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "pvc-claim-1", + }, + }, + }, + }, + } + + vm.Status.Volumes = []vmopv1.VirtualMachineVolumeStatus{ + { + Name: "dummy-vol", + Attached: true, + }, + } + }) + + It("creates a VM with a hardware version minimum supported for PCI devices", func() { + pvc1 := &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pvc-claim-1", + Namespace: vm.Namespace, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + StorageClassName: ptr.To(ctx.StorageClassName), + }, + } + Expect(ctx.Client.Create(ctx, pvc1)).To(Succeed()) + + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + Expect(o.Config.Version).To(Equal(fmt.Sprintf("vmx-%d", pkgconst.MinSupportedHWVersionForPCIPassthruDevices))) + }) + }) + + Context("VM spec has a PVC", func() { + BeforeEach(func() { + // Need to create the PVC before creating the VM. + skipCreateOrUpdateVM = true + + vm.Spec.Volumes = []vmopv1.VirtualMachineVolume{ + { + Name: "dummy-vol", + VirtualMachineVolumeSource: vmopv1.VirtualMachineVolumeSource{ + PersistentVolumeClaim: &vmopv1.PersistentVolumeClaimVolumeSource{ + PersistentVolumeClaimVolumeSource: corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "pvc-claim-1", + }, + }, + }, + }, + } + + vm.Status.Volumes = []vmopv1.VirtualMachineVolumeStatus{ + { + Name: "dummy-vol", + Attached: true, + }, + } + }) + + It("creates a VM with a hardware version minimum supported for PVCs", func() { + pvc1 := &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pvc-claim-1", + Namespace: vm.Namespace, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + StorageClassName: ptr.To(ctx.StorageClassName), + }, + } + Expect(ctx.Client.Create(ctx, pvc1)).To(Succeed()) + + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + Expect(o.Config.Version).To(Equal(fmt.Sprintf("vmx-%d", pkgconst.MinSupportedHWVersionForPVC))) + }) + + }) + }) + + Context("VM Class Config specifies a hardware version", func() { + BeforeEach(func() { + configSpec = &vimtypes.VirtualMachineConfigSpec{Version: "vmx-14"} + }) + + When("The minimum hardware version on the VMSpec is greater than VMClass", func() { + BeforeEach(func() { + vm.Spec.MinHardwareVersion = 15 + }) + + It("updates the VM to minimum hardware version from the Spec", func() { + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + Expect(o.Config.Version).To(Equal("vmx-15")) + }) + }) + + When("The minimum hardware version on the VMSpec is less than VMClass", func() { + BeforeEach(func() { + vm.Spec.MinHardwareVersion = 13 + }) + + It("uses the hardware version from the VMClass", func() { + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + Expect(o.Config.Version).To(Equal("vmx-14")) + }) + }) + }) + + When("configSpec has disk and disk controllers", func() { + BeforeEach(func() { + configSpec = &vimtypes.VirtualMachineConfigSpec{ + Name: "dummy-VM", + DeviceChange: []vimtypes.BaseVirtualDeviceConfigSpec{ + &vimtypes.VirtualDeviceConfigSpec{ + Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, + Device: &vimtypes.VirtualSATAController{ + VirtualController: vimtypes.VirtualController{ + VirtualDevice: vimtypes.VirtualDevice{ + Key: 101, + }, + }, + }, + }, + &vimtypes.VirtualDeviceConfigSpec{ + Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, + Device: &vimtypes.VirtualSCSIController{ + VirtualController: vimtypes.VirtualController{ + VirtualDevice: vimtypes.VirtualDevice{ + Key: 103, + }, + }, + }, + }, + &vimtypes.VirtualDeviceConfigSpec{ + Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, + Device: &vimtypes.VirtualNVMEController{ + VirtualController: vimtypes.VirtualController{ + VirtualDevice: vimtypes.VirtualDevice{ + Key: 104, + }, + }, + }, + }, + &vimtypes.VirtualDeviceConfigSpec{ + Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, + Device: &vimtypes.VirtualDisk{ + CapacityInBytes: 1024, + VirtualDevice: vimtypes.VirtualDevice{ + Key: -42, + Backing: &vimtypes.VirtualDiskFlatVer2BackingInfo{ + ThinProvisioned: ptr.To(true), + }, + }, + }, + }, + }, + } + }) + + It("creates a VM with disk controllers", func() { + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + + devList := object.VirtualDeviceList(o.Config.Hardware.Device) + satacont := devList.SelectByType(&vimtypes.VirtualSATAController{}) + Expect(satacont).To(HaveLen(1)) + dev := satacont[0].GetVirtualDevice() + Expect(dev.Key).To(Equal(int32(101))) + + scsicont := devList.SelectByType(&vimtypes.VirtualSCSIController{}) + Expect(scsicont).To(HaveLen(1)) + dev = scsicont[0].GetVirtualDevice() + Expect(dev.Key).To(Equal(int32(103))) + + nvmecont := devList.SelectByType(&vimtypes.VirtualNVMEController{}) + Expect(nvmecont).To(HaveLen(1)) + dev = nvmecont[0].GetVirtualDevice() + Expect(dev.Key).To(Equal(int32(104))) + + // only preexisting disk should be present on VM -- len: 1 + disks := devList.SelectByType(&vimtypes.VirtualDisk{}) + Expect(disks).To(HaveLen(1)) + dev1 := disks[0].GetVirtualDevice() + Expect(dev1.Key).ToNot(Equal(int32(-42))) + }) + }) +} diff --git a/pkg/providers/vsphere/vmprovider_vm_connection_state_test.go b/pkg/providers/vsphere/vmprovider_vm_connection_state_test.go new file mode 100644 index 000000000..9c9c0b99d --- /dev/null +++ b/pkg/providers/vsphere/vmprovider_vm_connection_state_test.go @@ -0,0 +1,166 @@ +// © Broadcom. All Rights Reserved. +// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package vsphere_test + +import ( + "context" + "errors" + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/vmware/govmomi/simulator" + "github.com/vmware/govmomi/vim25/mo" + vimtypes "github.com/vmware/govmomi/vim25/types" + + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha6" + "github.com/vmware-tanzu/vm-operator/pkg/conditions" + pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + ctxop "github.com/vmware-tanzu/vm-operator/pkg/context/operation" + pkgerr "github.com/vmware-tanzu/vm-operator/pkg/errors" + "github.com/vmware-tanzu/vm-operator/pkg/providers" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" + "github.com/vmware-tanzu/vm-operator/pkg/util/kube/cource" + "github.com/vmware-tanzu/vm-operator/pkg/util/ovfcache" + "github.com/vmware-tanzu/vm-operator/test/builder" +) + +func vmConnectionStateTests() { + var ( + parentCtx context.Context + initObjects []client.Object + testConfig builder.VCSimTestConfig + ctx *builder.TestContextForVCSim + vmProvider providers.VirtualMachineProviderInterface + nsInfo builder.WorkloadNamespaceInfo + + vm *vmopv1.VirtualMachine + vmClass *vmopv1.VirtualMachineClass + + zoneName string + ) + + BeforeEach(func() { + parentCtx = pkgcfg.NewContextWithDefaultConfig() + parentCtx = ctxop.WithContext(parentCtx) + parentCtx = ovfcache.WithContext(parentCtx) + parentCtx = cource.WithContext(parentCtx) + pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { + config.AsyncCreateEnabled = false + config.AsyncSignalEnabled = false + }) + testConfig = builder.VCSimTestConfig{ + WithContentLibrary: true, + } + + vmClass = builder.DummyVirtualMachineClassGenName() + vm = builder.DummyBasicVirtualMachine("test-vm", "") + + if vm.Spec.Network == nil { + vm.Spec.Network = &vmopv1.VirtualMachineNetworkSpec{} + } + vm.Spec.Network.Disabled = true + }) + + JustBeforeEach(func() { + ctx = suite.NewTestContextForVCSimWithParentContext( + parentCtx, testConfig, initObjects...) + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.MaxDeployThreadsOnProvider = 1 + }) + vmProvider = vsphere.NewVSphereVMProviderFromClient( + ctx, ctx.Client, ctx.Recorder) + nsInfo = ctx.CreateWorkloadNamespace() + + vmClass.Namespace = nsInfo.Namespace + Expect(ctx.Client.Create(ctx, vmClass)).To(Succeed()) + + clusterVMI1 := &vmopv1.ClusterVirtualMachineImage{} + + if testConfig.WithContentLibrary { + Expect(ctx.Client.Get( + ctx, client.ObjectKey{Name: ctx.ContentLibraryItem1Name}, + clusterVMI1)).To(Succeed()) + } else { + vsphere.SkipVMImageCLProviderCheck = true + clusterVMI1 = builder.DummyClusterVirtualMachineImage("DC0_C0_RP0_VM0") + Expect(ctx.Client.Create(ctx, clusterVMI1)).To(Succeed()) + conditions.MarkTrue(clusterVMI1, vmopv1.ReadyConditionType) + Expect(ctx.Client.Status().Update(ctx, clusterVMI1)).To(Succeed()) + } + + vm.Namespace = nsInfo.Namespace + vm.Spec.ClassName = vmClass.Name + vm.Spec.ImageName = clusterVMI1.Name + vm.Spec.Image.Kind = cvmiKind + vm.Spec.Image.Name = clusterVMI1.Name + vm.Spec.StorageClass = ctx.StorageClassName + + Expect(ctx.Client.Create(ctx, vm)).To(Succeed()) + + zoneName = ctx.GetFirstZoneName() + vm.Labels[corev1.LabelTopologyZone] = zoneName + Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) + }) + + AfterEach(func() { + vsphere.SkipVMImageCLProviderCheck = false + + if vm != nil && + !pkgcfg.FromContext(ctx).Features.BringYourOwnEncryptionKey { + By("Assert vm.Status.Crypto is nil when BYOK is disabled", func() { + Expect(vm.Status.Crypto).To(BeNil()) + }) + } + + vmClass = nil + vm = nil + + ctx.AfterEach() + ctx = nil + initObjects = nil + vmProvider = nil + nsInfo = builder.WorkloadNamespaceInfo{} + }) + + DescribeTable("VM is not connected", + func(state vimtypes.VirtualMachineConnectionState) { + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + var moVM mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &moVM)).To(Succeed()) + + sctx := ctx.SimulatorContext() + sctx.WithLock( + vcVM.Reference(), + func() { + vm := sctx.Map.Get(vcVM.Reference()).(*simulator.VirtualMachine) + vm.Summary.Runtime.ConnectionState = state + }) + + _, err = createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + + if state == "" { + Expect(err).ToNot(HaveOccurred()) + } else { + Expect(err).To(HaveOccurred()) + var noRequeueErr pkgerr.NoRequeueError + Expect(errors.As(err, &noRequeueErr)).To(BeTrue()) + Expect(noRequeueErr.Message).To(Equal( + fmt.Sprintf("unsupported connection state: %s", state))) + } + }, + Entry("empty", vimtypes.VirtualMachineConnectionState("")), + Entry("disconnected", vimtypes.VirtualMachineConnectionStateDisconnected), + Entry("inaccessible", vimtypes.VirtualMachineConnectionStateInaccessible), + Entry("invalid", vimtypes.VirtualMachineConnectionStateInvalid), + Entry("orphaned", vimtypes.VirtualMachineConnectionStateOrphaned), + ) +} diff --git a/pkg/providers/vsphere/vmprovider_vm_create_test.go b/pkg/providers/vsphere/vmprovider_vm_create_test.go new file mode 100644 index 000000000..302c72b6a --- /dev/null +++ b/pkg/providers/vsphere/vmprovider_vm_create_test.go @@ -0,0 +1,294 @@ +// © Broadcom. All Rights Reserved. +// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package vsphere_test + +import ( + "context" + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/vmware/govmomi/simulator" + "github.com/vmware/govmomi/vim25/mo" + vimtypes "github.com/vmware/govmomi/vim25/types" + + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha6" + backupapi "github.com/vmware-tanzu/vm-operator/pkg/backup/api" + "github.com/vmware-tanzu/vm-operator/pkg/conditions" + pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + "github.com/vmware-tanzu/vm-operator/pkg/constants/testlabels" + ctxop "github.com/vmware-tanzu/vm-operator/pkg/context/operation" + "github.com/vmware-tanzu/vm-operator/pkg/providers" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/virtualmachine" + pkgutil "github.com/vmware-tanzu/vm-operator/pkg/util" + "github.com/vmware-tanzu/vm-operator/pkg/util/kube/cource" + "github.com/vmware-tanzu/vm-operator/pkg/util/ovfcache" + "github.com/vmware-tanzu/vm-operator/test/builder" +) + +func vmCreateTests() { + var ( + parentCtx context.Context + initObjects []client.Object + testConfig builder.VCSimTestConfig + ctx *builder.TestContextForVCSim + vmProvider providers.VirtualMachineProviderInterface + nsInfo builder.WorkloadNamespaceInfo + + vm *vmopv1.VirtualMachine + vmClass *vmopv1.VirtualMachineClass + + zoneName string + ) + + BeforeEach(func() { + parentCtx = pkgcfg.NewContextWithDefaultConfig() + parentCtx = ctxop.WithContext(parentCtx) + parentCtx = ovfcache.WithContext(parentCtx) + parentCtx = cource.WithContext(parentCtx) + pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { + config.AsyncCreateEnabled = false + config.AsyncSignalEnabled = false + }) + testConfig = builder.VCSimTestConfig{ + WithContentLibrary: true, + } + + vmClass = builder.DummyVirtualMachineClassGenName() + vm = builder.DummyBasicVirtualMachine("test-vm", "") + + if vm.Spec.Network == nil { + vm.Spec.Network = &vmopv1.VirtualMachineNetworkSpec{} + } + vm.Spec.Network.Disabled = true + }) + + JustBeforeEach(func() { + ctx = suite.NewTestContextForVCSimWithParentContext( + parentCtx, testConfig, initObjects...) + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.MaxDeployThreadsOnProvider = 1 + }) + vmProvider = vsphere.NewVSphereVMProviderFromClient( + ctx, ctx.Client, ctx.Recorder) + nsInfo = ctx.CreateWorkloadNamespace() + + vmClass.Namespace = nsInfo.Namespace + Expect(ctx.Client.Create(ctx, vmClass)).To(Succeed()) + + clusterVMI1 := &vmopv1.ClusterVirtualMachineImage{} + + if testConfig.WithContentLibrary { + Expect(ctx.Client.Get( + ctx, client.ObjectKey{Name: ctx.ContentLibraryItem1Name}, + clusterVMI1)).To(Succeed()) + } else { + vsphere.SkipVMImageCLProviderCheck = true + clusterVMI1 = builder.DummyClusterVirtualMachineImage("DC0_C0_RP0_VM0") + Expect(ctx.Client.Create(ctx, clusterVMI1)).To(Succeed()) + conditions.MarkTrue(clusterVMI1, vmopv1.ReadyConditionType) + Expect(ctx.Client.Status().Update(ctx, clusterVMI1)).To(Succeed()) + } + + vm.Namespace = nsInfo.Namespace + vm.Spec.ClassName = vmClass.Name + vm.Spec.ImageName = clusterVMI1.Name + vm.Spec.Image.Kind = cvmiKind + vm.Spec.Image.Name = clusterVMI1.Name + vm.Spec.StorageClass = ctx.StorageClassName + + Expect(ctx.Client.Create(ctx, vm)).To(Succeed()) + + zoneName = ctx.GetFirstZoneName() + vm.Labels[corev1.LabelTopologyZone] = zoneName + Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) + }) + + AfterEach(func() { + vsphere.SkipVMImageCLProviderCheck = false + + if vm != nil && + !pkgcfg.FromContext(ctx).Features.BringYourOwnEncryptionKey { + By("Assert vm.Status.Crypto is nil when BYOK is disabled", func() { + Expect(vm.Status.Crypto).To(BeNil()) + }) + } + + vmClass = nil + vm = nil + + ctx.AfterEach() + ctx = nil + initObjects = nil + vmProvider = nil + nsInfo = builder.WorkloadNamespaceInfo{} + }) + + It("Basic VM", func() { + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + + By("has VC UUID annotation set", func() { + Expect(vm.Annotations).Should(HaveKeyWithValue(vmopv1.ManagerID, ctx.VCClient.Client.ServiceContent.About.InstanceUuid)) + }) + + By("has expected Status values", func() { + Expect(vm.Status.PowerState).To(Equal(vm.Spec.PowerState)) + Expect(vm.Status.NodeName).ToNot(BeEmpty()) + Expect(vm.Status.InstanceUUID).To(And(Not(BeEmpty()), Equal(o.Config.InstanceUuid))) + Expect(vm.Status.BiosUUID).To(And(Not(BeEmpty()), Equal(o.Config.Uuid))) + + Expect(vm.Status.Class).ToNot(BeNil()) + Expect(vm.Status.Class.Name).To(Equal(vm.Spec.ClassName)) + Expect(vm.Status.Class.APIVersion).To(Equal(vmopv1.GroupVersion.String())) + + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionClassReady)).To(BeTrue()) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionImageReady)).To(BeTrue()) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionStorageReady)).To(BeTrue()) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionCreated)).To(BeTrue()) + + By("did not have VMSetResourcePool", func() { + Expect(vm.Spec.Reserved).To(BeNil()) + Expect(conditions.Has(vm, vmopv1.VirtualMachineConditionVMSetResourcePolicyReady)).To(BeFalse()) + }) + By("did not have Bootstrap", func() { + Expect(vm.Spec.Bootstrap).To(BeNil()) + Expect(conditions.Has(vm, vmopv1.VirtualMachineConditionBootstrapReady)).To(BeFalse()) + }) + By("did not have Network", func() { + Expect(vm.Spec.Network.Disabled).To(BeTrue()) + Expect(conditions.Has(vm, vmopv1.VirtualMachineConditionNetworkReady)).To(BeFalse()) + }) + }) + + By("has expected inventory path", func() { + Expect(vcVM.InventoryPath).To(HaveSuffix(fmt.Sprintf("/%s/%s", nsInfo.Namespace, vm.Name))) + }) + + By("has expected namespace resource pool", func() { + rp, err := vcVM.ResourcePool(ctx) + Expect(err).ToNot(HaveOccurred()) + nsRP := ctx.GetResourcePoolForNamespace(nsInfo.Namespace, "", "") + Expect(nsRP).ToNot(BeNil()) + Expect(rp.Reference().Value).To(Equal(nsRP.Reference().Value)) + }) + + By("has expected power state", func() { + Expect(o.Summary.Runtime.PowerState).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOn)) + }) + + vmClassRes := &vmClass.Spec.Policies.Resources + + By("has expected CpuAllocation", func() { + Expect(o.Config.CpuAllocation).ToNot(BeNil()) + + reservation := o.Config.CpuAllocation.Reservation + Expect(reservation).ToNot(BeNil()) + Expect(*reservation).To(Equal(virtualmachine.CPUQuantityToMhz(vmClassRes.Requests.Cpu, vcsimCPUFreq))) + limit := o.Config.CpuAllocation.Limit + Expect(limit).ToNot(BeNil()) + Expect(*limit).To(Equal(virtualmachine.CPUQuantityToMhz(vmClassRes.Limits.Cpu, vcsimCPUFreq))) + }) + + By("has expected MemoryAllocation", func() { + Expect(o.Config.MemoryAllocation).ToNot(BeNil()) + + reservation := o.Config.MemoryAllocation.Reservation + Expect(reservation).ToNot(BeNil()) + Expect(*reservation).To(Equal(virtualmachine.MemoryQuantityToMb(vmClassRes.Requests.Memory))) + limit := o.Config.MemoryAllocation.Limit + Expect(limit).ToNot(BeNil()) + Expect(*limit).To(Equal(virtualmachine.MemoryQuantityToMb(vmClassRes.Limits.Memory))) + }) + + By("has expected hardware config", func() { + Expect(o.Summary.Config.NumCpu).To(BeEquivalentTo(vmClass.Spec.Hardware.Cpus)) + Expect(o.Summary.Config.MemorySizeMB).To(BeEquivalentTo(vmClass.Spec.Hardware.Memory.Value() / 1024 / 1024)) + }) + + By("has expected backup ExtraConfig key", func() { + Expect(o.Config.ExtraConfig).ToNot(BeNil()) + + ecMap := pkgutil.OptionValues(o.Config.ExtraConfig).StringMap() + Expect(ecMap).To(HaveKey(backupapi.VMResourceYAMLExtraConfigKey)) + }) + + // TODO: More assertions! + }) + + When("using async create", func() { + BeforeEach(func() { + pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { + config.AsyncCreateEnabled = true + config.AsyncSignalEnabled = true + }) + }) + JustBeforeEach(func() { + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.MaxDeployThreadsOnProvider = 16 + }) + }) + + It("should succeed", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.UniqueID).ToNot(BeEmpty()) + }) + + When("there is an error getting the pre-reqs", func() { + It("should not prevent a subsequent create attempt from going through", func() { + imgName := vm.Spec.Image.Name + vm.Spec.Image.Name = "does-not-exist" + err := createOrUpdateVM(ctx, vmProvider, vm) + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError( + "clustervirtualmachineimages.vmoperator.vmware.com \"does-not-exist\" not found: " + + "clustervirtualmachineimages.vmoperator.vmware.com \"does-not-exist\" not found")) + vm.Spec.Image.Name = imgName + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.UniqueID).ToNot(BeEmpty()) + }) + }) + + When("there is an error creating the VM", func() { + JustBeforeEach(func() { + ctx.SimulatorContext().Map.Handler = func( + ctx *simulator.Context, + m *simulator.Method) (mo.Reference, vimtypes.BaseMethodFault) { + + if m.Name == "ImportVApp" { + return nil, &vimtypes.InvalidRequest{} + } + return nil, nil + } + }) + + It("should fail to create the VM without an NPE", func() { + err := createOrUpdateVM(ctx, vmProvider, vm) + Expect(err).To(HaveOccurred()) + Eventually(func(g Gomega) { + g.Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(vm), vm)).To(Succeed()) + g.Expect(vm.Status.UniqueID).To(BeEmpty()) + c := conditions.Get(vm, vmopv1.VirtualMachineConditionCreated) + g.Expect(c).ToNot(BeNil()) + g.Expect(c.Status).To(Equal(metav1.ConditionFalse)) + g.Expect(c.Reason).To(Equal("Error")) + g.Expect(c.Message).To(Equal("deploy error: ServerFaultCode: InvalidRequest")) + }).Should(Succeed()) + }) + }) + }) + + Describe("FastDeploy", Label(testlabels.Create), vmFastDeployTests) + +} diff --git a/pkg/providers/vsphere/vmprovider_vm_crypto_test.go b/pkg/providers/vsphere/vmprovider_vm_crypto_test.go new file mode 100644 index 000000000..1ccff37ab --- /dev/null +++ b/pkg/providers/vsphere/vmprovider_vm_crypto_test.go @@ -0,0 +1,574 @@ +// © Broadcom. All Rights Reserved. +// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package vsphere_test + +import ( + "context" + "sync" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" + storagev1 "k8s.io/api/storage/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + vimcrypto "github.com/vmware/govmomi/crypto" + "github.com/vmware/govmomi/vim25/mo" + vimtypes "github.com/vmware/govmomi/vim25/types" + + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha6" + "github.com/vmware-tanzu/vm-operator/pkg/conditions" + pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + ctxop "github.com/vmware-tanzu/vm-operator/pkg/context/operation" + "github.com/vmware-tanzu/vm-operator/pkg/providers" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" + kubeutil "github.com/vmware-tanzu/vm-operator/pkg/util/kube" + "github.com/vmware-tanzu/vm-operator/pkg/util/kube/cource" + "github.com/vmware-tanzu/vm-operator/pkg/util/ovfcache" + "github.com/vmware-tanzu/vm-operator/pkg/vmconfig" + "github.com/vmware-tanzu/vm-operator/pkg/vmconfig/crypto" + "github.com/vmware-tanzu/vm-operator/test/builder" +) + +func vmCryptoTests() { + var ( + parentCtx context.Context + initObjects []client.Object + testConfig builder.VCSimTestConfig + ctx *builder.TestContextForVCSim + vmProvider providers.VirtualMachineProviderInterface + nsInfo builder.WorkloadNamespaceInfo + + vm *vmopv1.VirtualMachine + vmClass *vmopv1.VirtualMachineClass + + zoneName string + ) + + BeforeEach(func() { + parentCtx = pkgcfg.NewContextWithDefaultConfig() + parentCtx = ctxop.WithContext(parentCtx) + parentCtx = ovfcache.WithContext(parentCtx) + parentCtx = cource.WithContext(parentCtx) + pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { + config.AsyncCreateEnabled = false + config.AsyncSignalEnabled = false + }) + testConfig = builder.VCSimTestConfig{ + WithContentLibrary: true, + } + + vmClass = builder.DummyVirtualMachineClassGenName() + vm = builder.DummyBasicVirtualMachine("test-vm", "") + + if vm.Spec.Network == nil { + vm.Spec.Network = &vmopv1.VirtualMachineNetworkSpec{} + } + vm.Spec.Network.Disabled = true + + pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { + config.Features.BringYourOwnEncryptionKey = true + }) + parentCtx = vmconfig.WithContext(parentCtx) + parentCtx = vmconfig.Register(parentCtx, crypto.New()) + + vm.Spec.Crypto = &vmopv1.VirtualMachineCryptoSpec{} + }) + + JustBeforeEach(func() { + ctx = suite.NewTestContextForVCSimWithParentContext( + parentCtx, testConfig, initObjects...) + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.MaxDeployThreadsOnProvider = 1 + }) + vmProvider = vsphere.NewVSphereVMProviderFromClient( + ctx, ctx.Client, ctx.Recorder) + nsInfo = ctx.CreateWorkloadNamespace() + + vmClass.Namespace = nsInfo.Namespace + Expect(ctx.Client.Create(ctx, vmClass)).To(Succeed()) + + clusterVMI1 := &vmopv1.ClusterVirtualMachineImage{} + + if testConfig.WithContentLibrary { + Expect(ctx.Client.Get( + ctx, client.ObjectKey{Name: ctx.ContentLibraryItem1Name}, + clusterVMI1)).To(Succeed()) + } else { + vsphere.SkipVMImageCLProviderCheck = true + clusterVMI1 = builder.DummyClusterVirtualMachineImage("DC0_C0_RP0_VM0") + Expect(ctx.Client.Create(ctx, clusterVMI1)).To(Succeed()) + conditions.MarkTrue(clusterVMI1, vmopv1.ReadyConditionType) + Expect(ctx.Client.Status().Update(ctx, clusterVMI1)).To(Succeed()) + } + + vm.Namespace = nsInfo.Namespace + vm.Spec.ClassName = vmClass.Name + vm.Spec.ImageName = clusterVMI1.Name + vm.Spec.Image.Kind = cvmiKind + vm.Spec.Image.Name = clusterVMI1.Name + vm.Spec.StorageClass = ctx.StorageClassName + + Expect(ctx.Client.Create(ctx, vm)).To(Succeed()) + + zoneName = ctx.GetFirstZoneName() + vm.Labels[corev1.LabelTopologyZone] = zoneName + Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) + + var storageClass storagev1.StorageClass + Expect(ctx.Client.Get( + ctx, + client.ObjectKey{Name: ctx.EncryptedStorageClassName}, + &storageClass)).To(Succeed()) + Expect(kubeutil.MarkEncryptedStorageClass( + ctx, + ctx.Client, + storageClass, + true)).To(Succeed()) + }) + + AfterEach(func() { + vsphere.SkipVMImageCLProviderCheck = false + + if vm != nil && + !pkgcfg.FromContext(ctx).Features.BringYourOwnEncryptionKey { + By("Assert vm.Status.Crypto is nil when BYOK is disabled", func() { + Expect(vm.Status.Crypto).To(BeNil()) + }) + } + + vmClass = nil + vm = nil + + ctx.AfterEach() + ctx = nil + initObjects = nil + vmProvider = nil + nsInfo = builder.WorkloadNamespaceInfo{} + }) + + useExistingVM := func( + cryptoSpec vimtypes.BaseCryptoSpec, vTPM bool) { + + vmList, err := ctx.Finder.VirtualMachineList(ctx, "*") + ExpectWithOffset(1, err).ToNot(HaveOccurred()) + ExpectWithOffset(1, vmList).ToNot(BeEmpty()) + + vcVM := vmList[0] + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + vm.Spec.InstanceUUID = o.Config.InstanceUuid + + powerState, err := vcVM.PowerState(ctx) + ExpectWithOffset(1, err).ToNot(HaveOccurred()) + if powerState == vimtypes.VirtualMachinePowerStatePoweredOn { + tsk, err := vcVM.PowerOff(ctx) + ExpectWithOffset(1, err).ToNot(HaveOccurred()) + ExpectWithOffset(1, tsk.Wait(ctx)).To(Succeed()) + } + + if cryptoSpec != nil || vTPM { + configSpec := vimtypes.VirtualMachineConfigSpec{ + Crypto: cryptoSpec, + } + if vTPM { + configSpec.DeviceChange = []vimtypes.BaseVirtualDeviceConfigSpec{ + &vimtypes.VirtualDeviceConfigSpec{ + Device: &vimtypes.VirtualTPM{ + VirtualDevice: vimtypes.VirtualDevice{ + Key: -1000, + ControllerKey: 100, + }, + }, + Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, + }, + } + } + tsk, err := vcVM.Reconfigure(ctx, configSpec) + ExpectWithOffset(1, err).ToNot(HaveOccurred()) + ExpectWithOffset(1, tsk.Wait(ctx)).To(Succeed()) + } + } + + When("deploying an encrypted vm", func() { + JustBeforeEach(func() { + vm.Spec.StorageClass = ctx.EncryptedStorageClassName + Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) + }) + + When("using a default provider", func() { + + When("default provider is native key provider", func() { + JustBeforeEach(func() { + m := vimcrypto.NewManagerKmip(ctx.VCClient.Client) + Expect(m.MarkDefault(ctx, ctx.NativeKeyProviderID)).To(Succeed()) + }) + + When("using sync create", func() { + BeforeEach(func() { + pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { + config.AsyncCreateEnabled = false + config.AsyncSignalEnabled = true + }) + }) + It("should succeed", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.Crypto).ToNot(BeNil()) + Expect(vm.Status.Crypto.Encrypted).To(HaveExactElements( + []vmopv1.VirtualMachineEncryptionType{ + vmopv1.VirtualMachineEncryptionTypeConfig, + })) + Expect(vm.Status.Crypto.ProviderID).To(Equal(ctx.NativeKeyProviderID)) + Expect(vm.Status.Crypto.KeyID).ToNot(BeEmpty()) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeTrue()) + }) + }) + + When("using async create", func() { + BeforeEach(func() { + pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { + config.AsyncCreateEnabled = true + config.AsyncSignalEnabled = true + }) + }) + It("should succeed", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.Crypto).ToNot(BeNil()) + Expect(vm.Status.Crypto.Encrypted).To(HaveExactElements( + []vmopv1.VirtualMachineEncryptionType{ + vmopv1.VirtualMachineEncryptionTypeConfig, + })) + Expect(vm.Status.Crypto.ProviderID).To(Equal(ctx.NativeKeyProviderID)) + Expect(vm.Status.Crypto.KeyID).ToNot(BeEmpty()) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeTrue()) + }) + + // Please note this test uses FlakeAttempts(5) due to the + // validation of some predictable-over-time behavior. + When("there is a duplicate create", FlakeAttempts(5), func() { + JustBeforeEach(func() { + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.MaxDeployThreadsOnProvider = 16 + }) + }) + It("should return ErrReconcileInProgress", func() { + var ( + errs []error + errsMu sync.Mutex + done sync.WaitGroup + start = make(chan struct{}) + ) + + // Set up five goroutines that race to + // create the VM first. + for i := 0; i < 5; i++ { + done.Add(1) + go func(copyOfVM *vmopv1.VirtualMachine) { + defer done.Done() + <-start + err := createOrUpdateVM(ctx, vmProvider, copyOfVM) + if err != nil { + errsMu.Lock() + errs = append(errs, err) + errsMu.Unlock() + } else { + vm = copyOfVM + } + }(vm.DeepCopy()) + } + + close(start) + + done.Wait() + + Expect(errs).To(HaveLen(4)) + + Expect(errs).Should(ConsistOf( + providers.ErrReconcileInProgress, + providers.ErrReconcileInProgress, + providers.ErrReconcileInProgress, + providers.ErrReconcileInProgress, + )) + + Expect(vm.Status.Crypto).ToNot(BeNil()) + Expect(vm.Status.Crypto.Encrypted).To(HaveExactElements( + []vmopv1.VirtualMachineEncryptionType{ + vmopv1.VirtualMachineEncryptionTypeConfig, + })) + Expect(vm.Status.Crypto.ProviderID).To(Equal(ctx.NativeKeyProviderID)) + Expect(vm.Status.Crypto.KeyID).ToNot(BeEmpty()) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeTrue()) + }) + }) + }) + }) + + When("default provider is not native key provider", func() { + JustBeforeEach(func() { + m := vimcrypto.NewManagerKmip(ctx.VCClient.Client) + Expect(m.MarkDefault(ctx, ctx.EncryptionClass1ProviderID)).To(Succeed()) + }) + + It("should succeed", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.Crypto).ToNot(BeNil()) + Expect(vm.Status.Crypto.Encrypted).To(HaveExactElements( + []vmopv1.VirtualMachineEncryptionType{ + vmopv1.VirtualMachineEncryptionTypeConfig, + })) + Expect(vm.Status.Crypto.ProviderID).To(Equal(ctx.EncryptionClass1ProviderID)) + Expect(vm.Status.Crypto.KeyID).ToNot(BeEmpty()) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeTrue()) + }) + }) + }) + + Context("using an encryption class", func() { + + JustBeforeEach(func() { + vm.Spec.Crypto.EncryptionClassName = ctx.EncryptionClass1Name + }) + + It("should succeed", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.Crypto).ToNot(BeNil()) + Expect(vm.Status.Crypto.Encrypted).To(HaveExactElements( + []vmopv1.VirtualMachineEncryptionType{ + vmopv1.VirtualMachineEncryptionTypeConfig, + })) + Expect(vm.Status.Crypto.ProviderID).To(Equal(ctx.EncryptionClass1ProviderID)) + Expect(vm.Status.Crypto.KeyID).To(Equal(nsInfo.EncryptionClass1KeyID)) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeTrue()) + }) + }) + }) + + When("encrypting an existing vm", func() { + var ( + hasVTPM bool + ) + + BeforeEach(func() { + hasVTPM = false + }) + + JustBeforeEach(func() { + useExistingVM(nil, hasVTPM) + vm.Spec.StorageClass = ctx.EncryptedStorageClassName + }) + + When("using a default provider", func() { + + When("default provider is native key provider", func() { + JustBeforeEach(func() { + m := vimcrypto.NewManagerKmip(ctx.VCClient.Client) + Expect(m.MarkDefault(ctx, ctx.NativeKeyProviderID)).To(Succeed()) + }) + + It("should succeed", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.Crypto).ToNot(BeNil()) + + Expect(vm.Status.Crypto.Encrypted).To(HaveExactElements( + []vmopv1.VirtualMachineEncryptionType{ + vmopv1.VirtualMachineEncryptionTypeConfig, + })) + Expect(vm.Status.Crypto.ProviderID).To(Equal(ctx.NativeKeyProviderID)) + Expect(vm.Status.Crypto.KeyID).ToNot(BeEmpty()) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeTrue()) + }) + }) + + When("default provider is not native key provider", func() { + JustBeforeEach(func() { + m := vimcrypto.NewManagerKmip(ctx.VCClient.Client) + Expect(m.MarkDefault(ctx, ctx.EncryptionClass1ProviderID)).To(Succeed()) + }) + + It("should succeed", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.Crypto).ToNot(BeNil()) + + Expect(vm.Status.Crypto.Encrypted).To(HaveExactElements( + []vmopv1.VirtualMachineEncryptionType{ + vmopv1.VirtualMachineEncryptionTypeConfig, + })) + Expect(vm.Status.Crypto.ProviderID).To(Equal(ctx.EncryptionClass1ProviderID)) + Expect(vm.Status.Crypto.KeyID).ToNot(BeEmpty()) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeTrue()) + }) + }) + }) + + Context("using an encryption class", func() { + + JustBeforeEach(func() { + vm.Spec.Crypto.EncryptionClassName = ctx.EncryptionClass2Name + }) + + It("should succeed", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.Crypto).ToNot(BeNil()) + + Expect(vm.Status.Crypto.Encrypted).To(HaveExactElements( + []vmopv1.VirtualMachineEncryptionType{ + vmopv1.VirtualMachineEncryptionTypeConfig, + })) + Expect(vm.Status.Crypto.ProviderID).To(Equal(ctx.EncryptionClass2ProviderID)) + Expect(vm.Status.Crypto.KeyID).To(Equal(nsInfo.EncryptionClass2KeyID)) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeTrue()) + }) + + When("using a non-encryption storage class", func() { + JustBeforeEach(func() { + vm.Spec.StorageClass = ctx.StorageClassName + vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOff + }) + + When("there is no vTPM", func() { + It("should not error, but have condition", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.Crypto).To(BeNil()) + c := conditions.Get(vm, vmopv1.VirtualMachineEncryptionSynced) + Expect(c).ToNot(BeNil()) + Expect(c.Status).To(Equal(metav1.ConditionFalse)) + Expect(c.Reason).To(Equal("InvalidState")) + Expect(c.Message).To(Equal("Must use encryption storage class or have vTPM when encrypting vm")) + }) + }) + + When("there is a vTPM", func() { + BeforeEach(func() { + hasVTPM = true + }) + It("should succeed", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.Crypto).ToNot(BeNil()) + + Expect(vm.Status.Crypto.Encrypted).To(HaveExactElements( + []vmopv1.VirtualMachineEncryptionType{ + vmopv1.VirtualMachineEncryptionTypeConfig, + })) + Expect(vm.Status.Crypto.ProviderID).To(Equal(ctx.EncryptionClass2ProviderID)) + Expect(vm.Status.Crypto.KeyID).To(Equal(nsInfo.EncryptionClass2KeyID)) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeTrue()) + }) + }) + }) + }) + }) + + When("recrypting a vm", func() { + var ( + hasVTPM bool + ) + + BeforeEach(func() { + hasVTPM = false + }) + + JustBeforeEach(func() { + useExistingVM(&vimtypes.CryptoSpecEncrypt{ + CryptoKeyId: vimtypes.CryptoKeyId{ + KeyId: nsInfo.EncryptionClass1KeyID, + ProviderId: &vimtypes.KeyProviderId{ + Id: ctx.EncryptionClass1ProviderID, + }, + }, + }, hasVTPM) + vm.Spec.StorageClass = ctx.EncryptedStorageClassName + }) + + When("using a default provider", func() { + + When("default provider is native key provider", func() { + JustBeforeEach(func() { + m := vimcrypto.NewManagerKmip(ctx.VCClient.Client) + Expect(m.MarkDefault(ctx, ctx.NativeKeyProviderID)).To(Succeed()) + }) + + It("should succeed", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.Crypto).ToNot(BeNil()) + + Expect(vm.Status.Crypto.Encrypted).To(HaveExactElements( + []vmopv1.VirtualMachineEncryptionType{ + vmopv1.VirtualMachineEncryptionTypeConfig, + })) + Expect(vm.Status.Crypto.ProviderID).To(Equal(ctx.NativeKeyProviderID)) + Expect(vm.Status.Crypto.KeyID).ToNot(BeEmpty()) + Expect(vm.Status.Crypto.KeyID).ToNot(Equal(nsInfo.EncryptionClass1KeyID)) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeTrue()) + }) + }) + + When("default provider is not native key provider", func() { + JustBeforeEach(func() { + m := vimcrypto.NewManagerKmip(ctx.VCClient.Client) + Expect(m.MarkDefault(ctx, ctx.EncryptionClass2ProviderID)).To(Succeed()) + }) + + It("should succeed", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.Crypto).ToNot(BeNil()) + + Expect(vm.Status.Crypto.Encrypted).To(HaveExactElements( + []vmopv1.VirtualMachineEncryptionType{ + vmopv1.VirtualMachineEncryptionTypeConfig, + })) + Expect(vm.Status.Crypto.ProviderID).To(Equal(ctx.EncryptionClass2ProviderID)) + Expect(vm.Status.Crypto.KeyID).ToNot(BeEmpty()) + Expect(vm.Status.Crypto.KeyID).ToNot(Equal(nsInfo.EncryptionClass1KeyID)) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeTrue()) + }) + }) + }) + + Context("using an encryption class", func() { + + JustBeforeEach(func() { + vm.Spec.Crypto.EncryptionClassName = ctx.EncryptionClass2Name + }) + + It("should succeed", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.Crypto).ToNot(BeNil()) + + Expect(vm.Status.Crypto.Encrypted).To(HaveExactElements( + []vmopv1.VirtualMachineEncryptionType{ + vmopv1.VirtualMachineEncryptionTypeConfig, + })) + Expect(vm.Status.Crypto.ProviderID).To(Equal(ctx.EncryptionClass2ProviderID)) + Expect(vm.Status.Crypto.KeyID).To(Equal(nsInfo.EncryptionClass2KeyID)) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeTrue()) + }) + + When("using a non-encryption storage class with a vTPM", func() { + BeforeEach(func() { + hasVTPM = true + }) + + JustBeforeEach(func() { + vm.Spec.StorageClass = ctx.StorageClassName + }) + + It("should succeed", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.Crypto).ToNot(BeNil()) + + Expect(vm.Status.Crypto.Encrypted).To(HaveExactElements( + []vmopv1.VirtualMachineEncryptionType{ + vmopv1.VirtualMachineEncryptionTypeConfig, + })) + Expect(vm.Status.Crypto.ProviderID).To(Equal(ctx.EncryptionClass2ProviderID)) + Expect(vm.Status.Crypto.KeyID).To(Equal(nsInfo.EncryptionClass2KeyID)) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeTrue()) + }) + }) + }) + }) +} diff --git a/pkg/providers/vsphere/vmprovider_vm_delete_test.go b/pkg/providers/vsphere/vmprovider_vm_delete_test.go new file mode 100644 index 000000000..1a4be677b --- /dev/null +++ b/pkg/providers/vsphere/vmprovider_vm_delete_test.go @@ -0,0 +1,277 @@ +// © Broadcom. All Rights Reserved. +// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package vsphere_test + +import ( + "context" + "errors" + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "sigs.k8s.io/controller-runtime/pkg/client" + + corev1 "k8s.io/api/core/v1" + + "github.com/vmware/govmomi/simulator" + vimtypes "github.com/vmware/govmomi/vim25/types" + + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha6" + "github.com/vmware-tanzu/vm-operator/pkg/conditions" + pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + ctxop "github.com/vmware-tanzu/vm-operator/pkg/context/operation" + pkgerr "github.com/vmware-tanzu/vm-operator/pkg/errors" + "github.com/vmware-tanzu/vm-operator/pkg/providers" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/constants" + "github.com/vmware-tanzu/vm-operator/pkg/util/kube/cource" + "github.com/vmware-tanzu/vm-operator/pkg/util/ovfcache" + "github.com/vmware-tanzu/vm-operator/test/builder" +) + +func vmDeleteTests() { + const zoneName = "az-1" + + var ( + parentCtx context.Context + initObjects []client.Object + testConfig builder.VCSimTestConfig + ctx *builder.TestContextForVCSim + vmProvider providers.VirtualMachineProviderInterface + nsInfo builder.WorkloadNamespaceInfo + + vm *vmopv1.VirtualMachine + vmClass *vmopv1.VirtualMachineClass + ) + + BeforeEach(func() { + parentCtx = pkgcfg.NewContextWithDefaultConfig() + parentCtx = ctxop.WithContext(parentCtx) + parentCtx = ovfcache.WithContext(parentCtx) + parentCtx = cource.WithContext(parentCtx) + pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { + config.AsyncCreateEnabled = false + config.AsyncSignalEnabled = false + }) + testConfig = builder.VCSimTestConfig{ + WithContentLibrary: true, + } + + vmClass = builder.DummyVirtualMachineClassGenName() + vm = builder.DummyBasicVirtualMachine("test-vm", "") + + if vm.Spec.Network == nil { + vm.Spec.Network = &vmopv1.VirtualMachineNetworkSpec{} + } + vm.Spec.Network.Disabled = true + }) + + JustBeforeEach(func() { + ctx = suite.NewTestContextForVCSimWithParentContext( + parentCtx, testConfig, initObjects...) + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.MaxDeployThreadsOnProvider = 1 + }) + vmProvider = vsphere.NewVSphereVMProviderFromClient( + ctx, ctx.Client, ctx.Recorder) + nsInfo = ctx.CreateWorkloadNamespace() + + vmClass.Namespace = nsInfo.Namespace + Expect(ctx.Client.Create(ctx, vmClass)).To(Succeed()) + + clusterVMI1 := &vmopv1.ClusterVirtualMachineImage{} + + if testConfig.WithContentLibrary { + Expect(ctx.Client.Get( + ctx, client.ObjectKey{Name: ctx.ContentLibraryItem1Name}, + clusterVMI1)).To(Succeed()) + } else { + vsphere.SkipVMImageCLProviderCheck = true + clusterVMI1 = builder.DummyClusterVirtualMachineImage("DC0_C0_RP0_VM0") + Expect(ctx.Client.Create(ctx, clusterVMI1)).To(Succeed()) + conditions.MarkTrue(clusterVMI1, vmopv1.ReadyConditionType) + Expect(ctx.Client.Status().Update(ctx, clusterVMI1)).To(Succeed()) + } + + vm.Namespace = nsInfo.Namespace + vm.Spec.ClassName = vmClass.Name + vm.Spec.ImageName = clusterVMI1.Name + vm.Spec.Image.Kind = cvmiKind + vm.Spec.Image.Name = clusterVMI1.Name + vm.Spec.StorageClass = ctx.StorageClassName + + Expect(ctx.Client.Create(ctx, vm)).To(Succeed()) + }) + + AfterEach(func() { + vsphere.SkipVMImageCLProviderCheck = false + + if vm != nil && + !pkgcfg.FromContext(ctx).Features.BringYourOwnEncryptionKey { + By("Assert vm.Status.Crypto is nil when BYOK is disabled", func() { + Expect(vm.Status.Crypto).To(BeNil()) + }) + } + + vmClass = nil + vm = nil + + ctx.AfterEach() + ctx = nil + initObjects = nil + vmProvider = nil + nsInfo = builder.WorkloadNamespaceInfo{} + }) + + BeforeEach(func() { + // Explicitly place the VM into one of the zones that the test context will create. + vm.Labels[corev1.LabelTopologyZone] = zoneName + }) + + JustBeforeEach(func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + }) + + Context("when the VM is off", func() { + BeforeEach(func() { + vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOff + }) + + It("deletes the VM", func() { + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) + + uniqueID := vm.Status.UniqueID + Expect(ctx.GetVMFromMoID(uniqueID)).ToNot(BeNil()) + + Expect(vmProvider.DeleteVirtualMachine(ctx, vm)).To(Succeed()) + Expect(ctx.GetVMFromMoID(uniqueID)).To(BeNil()) + }) + }) + + It("when the VM is on", func() { + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOn)) + + uniqueID := vm.Status.UniqueID + Expect(ctx.GetVMFromMoID(uniqueID)).ToNot(BeNil()) + + // This checks that we power off the VM prior to deletion. + Expect(vmProvider.DeleteVirtualMachine(ctx, vm)).To(Succeed()) + Expect(ctx.GetVMFromMoID(uniqueID)).To(BeNil()) + }) + + It("returns success when VM does not exist", func() { + Expect(vmProvider.DeleteVirtualMachine(ctx, vm)).To(Succeed()) + Expect(vmProvider.DeleteVirtualMachine(ctx, vm)).To(Succeed()) + }) + + It("returns NotFound when VM does not exist", func() { + _, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + Expect(vmProvider.DeleteVirtualMachine(ctx, vm)).To(Succeed()) + delete(vm.Labels, corev1.LabelTopologyZone) + Expect(vmProvider.DeleteVirtualMachine(ctx, vm)).To(Succeed()) + }) + + It("Deletes existing VM when zone info is missing", func() { + _, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + uniqueID := vm.Status.UniqueID + Expect(ctx.GetVMFromMoID(uniqueID)).ToNot(BeNil()) + + Expect(vm.Labels).To(HaveKeyWithValue(corev1.LabelTopologyZone, zoneName)) + delete(vm.Labels, corev1.LabelTopologyZone) + + Expect(vmProvider.DeleteVirtualMachine(ctx, vm)).To(Succeed()) + Expect(ctx.GetVMFromMoID(uniqueID)).To(BeNil()) + }) + + It("Does not delete paused VM", func() { + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + uniqueID := vm.Status.UniqueID + Expect(ctx.GetVMFromMoID(uniqueID)).ToNot(BeNil()) + + sctx := ctx.SimulatorContext() + sctx.WithLock( + vcVM.Reference(), + func() { + vm := sctx.Map.Get(vcVM.Reference()).(*simulator.VirtualMachine) + vm.Config.ExtraConfig = append(vm.Config.ExtraConfig, + &vimtypes.OptionValue{ + Key: vmopv1.PauseVMExtraConfigKey, + Value: "True", + }) + }, + ) + + err = vmProvider.DeleteVirtualMachine(ctx, vm) + Expect(err).To(HaveOccurred()) + var noRequeueErr pkgerr.NoRequeueError + Expect(errors.As(err, &noRequeueErr)).To(BeTrue()) + Expect(noRequeueErr.Message).To(Equal(constants.VMPausedByAdminError)) + Expect(ctx.GetVMFromMoID(uniqueID)).ToNot(BeNil()) + }) + + Context("Fast Deploy is enabled", func() { + JustBeforeEach(func() { + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.Features.FastDeploy = true + }) + }) + + It("return success", func() { + // TODO: We don't have explicit promote tests in here so + // punt on that. But with the feature enable, we'll get + // all the VM's tasks. + _, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + uniqueID := vm.Status.UniqueID + Expect(ctx.GetVMFromMoID(uniqueID)).ToNot(BeNil()) + + err = vmProvider.DeleteVirtualMachine(ctx, vm) + Expect(err).ToNot(HaveOccurred()) + + Expect(ctx.GetVMFromMoID(uniqueID)).To(BeNil()) + }) + }) + + DescribeTable("VM is not connected", + func(state vimtypes.VirtualMachineConnectionState) { + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + sctx := ctx.SimulatorContext() + sctx.WithLock( + vcVM.Reference(), + func() { + vm := sctx.Map.Get(vcVM.Reference()).(*simulator.VirtualMachine) + vm.Summary.Runtime.ConnectionState = state + }) + + err = vmProvider.DeleteVirtualMachine(ctx, vm) + + if state == "" { + Expect(err).ToNot(HaveOccurred()) + Expect(ctx.GetVMFromMoID(vm.Status.UniqueID)).To(BeNil()) + } else { + Expect(err).To(HaveOccurred()) + var noRequeueErr pkgerr.NoRequeueError + Expect(errors.As(err, &noRequeueErr)).To(BeTrue()) + Expect(noRequeueErr.Message).To(Equal( + fmt.Sprintf("unsupported connection state: %s", state))) + Expect(ctx.GetVMFromMoID(vm.Status.UniqueID)).ToNot(BeNil()) + } + }, + Entry("empty", vimtypes.VirtualMachineConnectionState("")), + Entry("disconnected", vimtypes.VirtualMachineConnectionStateDisconnected), + Entry("inaccessible", vimtypes.VirtualMachineConnectionStateInaccessible), + Entry("invalid", vimtypes.VirtualMachineConnectionStateInvalid), + Entry("orphaned", vimtypes.VirtualMachineConnectionStateOrphaned), + ) +} diff --git a/pkg/providers/vsphere/vmprovider_vm_disks_test.go b/pkg/providers/vsphere/vmprovider_vm_disks_test.go new file mode 100644 index 000000000..680b491ab --- /dev/null +++ b/pkg/providers/vsphere/vmprovider_vm_disks_test.go @@ -0,0 +1,206 @@ +// © Broadcom. All Rights Reserved. +// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package vsphere_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gstruct" + "sigs.k8s.io/controller-runtime/pkg/client" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + + "github.com/vmware/govmomi/vim25/mo" + + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha6" + "github.com/vmware-tanzu/vm-operator/pkg/conditions" + pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + ctxop "github.com/vmware-tanzu/vm-operator/pkg/context/operation" + "github.com/vmware-tanzu/vm-operator/pkg/providers" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" + "github.com/vmware-tanzu/vm-operator/pkg/util/kube/cource" + "github.com/vmware-tanzu/vm-operator/pkg/util/ovfcache" + "github.com/vmware-tanzu/vm-operator/test/builder" +) + +func vmDisksTests() { + var ( + parentCtx context.Context + initObjects []client.Object + testConfig builder.VCSimTestConfig + ctx *builder.TestContextForVCSim + vmProvider providers.VirtualMachineProviderInterface + nsInfo builder.WorkloadNamespaceInfo + + vm *vmopv1.VirtualMachine + vmClass *vmopv1.VirtualMachineClass + + zoneName string + ) + + BeforeEach(func() { + parentCtx = pkgcfg.NewContextWithDefaultConfig() + parentCtx = ctxop.WithContext(parentCtx) + parentCtx = ovfcache.WithContext(parentCtx) + parentCtx = cource.WithContext(parentCtx) + pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { + config.AsyncCreateEnabled = false + config.AsyncSignalEnabled = false + }) + testConfig = builder.VCSimTestConfig{ + WithContentLibrary: true, + } + + vmClass = builder.DummyVirtualMachineClassGenName() + vm = builder.DummyBasicVirtualMachine("test-vm", "") + + if vm.Spec.Network == nil { + vm.Spec.Network = &vmopv1.VirtualMachineNetworkSpec{} + } + vm.Spec.Network.Disabled = true + }) + + JustBeforeEach(func() { + ctx = suite.NewTestContextForVCSimWithParentContext( + parentCtx, testConfig, initObjects...) + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.MaxDeployThreadsOnProvider = 1 + }) + vmProvider = vsphere.NewVSphereVMProviderFromClient( + ctx, ctx.Client, ctx.Recorder) + nsInfo = ctx.CreateWorkloadNamespace() + + vmClass.Namespace = nsInfo.Namespace + Expect(ctx.Client.Create(ctx, vmClass)).To(Succeed()) + + clusterVMI1 := &vmopv1.ClusterVirtualMachineImage{} + + if testConfig.WithContentLibrary { + Expect(ctx.Client.Get( + ctx, client.ObjectKey{Name: ctx.ContentLibraryItem1Name}, + clusterVMI1)).To(Succeed()) + } else { + vsphere.SkipVMImageCLProviderCheck = true + clusterVMI1 = builder.DummyClusterVirtualMachineImage("DC0_C0_RP0_VM0") + Expect(ctx.Client.Create(ctx, clusterVMI1)).To(Succeed()) + conditions.MarkTrue(clusterVMI1, vmopv1.ReadyConditionType) + Expect(ctx.Client.Status().Update(ctx, clusterVMI1)).To(Succeed()) + } + + vm.Namespace = nsInfo.Namespace + vm.Spec.ClassName = vmClass.Name + vm.Spec.ImageName = clusterVMI1.Name + vm.Spec.Image.Kind = cvmiKind + vm.Spec.Image.Name = clusterVMI1.Name + vm.Spec.StorageClass = ctx.StorageClassName + + Expect(ctx.Client.Create(ctx, vm)).To(Succeed()) + + zoneName = ctx.GetFirstZoneName() + vm.Labels[corev1.LabelTopologyZone] = zoneName + Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) + }) + + AfterEach(func() { + vsphere.SkipVMImageCLProviderCheck = false + + if vm != nil && + !pkgcfg.FromContext(ctx).Features.BringYourOwnEncryptionKey { + By("Assert vm.Status.Crypto is nil when BYOK is disabled", func() { + Expect(vm.Status.Crypto).To(BeNil()) + }) + } + + vmClass = nil + vm = nil + + ctx.AfterEach() + ctx = nil + initObjects = nil + vmProvider = nil + nsInfo = builder.WorkloadNamespaceInfo{} + }) + + Context("VM has thin provisioning", func() { + BeforeEach(func() { + if vm.Spec.Advanced == nil { + vm.Spec.Advanced = &vmopv1.VirtualMachineAdvancedSpec{} + } + vm.Spec.Advanced.DefaultVolumeProvisioningMode = vmopv1.VolumeProvisioningModeThin + }) + + It("Succeeds", func() { + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + + _, backing := getVMHomeDisk(ctx, vcVM, o) + Expect(backing.ThinProvisioned).To(PointTo(BeTrue())) + }) + }) + + XContext("VM has thick provisioning", func() { + BeforeEach(func() { + vm.Spec.Advanced.DefaultVolumeProvisioningMode = vmopv1.VolumeProvisioningModeThick + }) + + It("Succeeds", func() { + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + + /* vcsim CL deploy has "thick" but that isn't reflected for this disk. */ + _, backing := getVMHomeDisk(ctx, vcVM, o) + Expect(backing.ThinProvisioned).To(PointTo(BeFalse())) + }) + }) + + XContext("VM has eager zero provisioning", func() { + BeforeEach(func() { + if vm.Spec.Advanced == nil { + vm.Spec.Advanced = &vmopv1.VirtualMachineAdvancedSpec{} + } + vm.Spec.Advanced.DefaultVolumeProvisioningMode = vmopv1.VolumeProvisioningModeThickEagerZero + }) + + It("Succeeds", func() { + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + + /* vcsim CL deploy has "eagerZeroedThick" but that isn't reflected for this disk. */ + _, backing := getVMHomeDisk(ctx, vcVM, o) + Expect(backing.EagerlyScrub).To(PointTo(BeTrue())) + }) + }) + + Context("Should resize root disk", func() { + It("Succeeds", func() { + newSize := resource.MustParse("4242Gi") + + if vm.Spec.Advanced == nil { + vm.Spec.Advanced = &vmopv1.VirtualMachineAdvancedSpec{} + } + vm.Spec.Advanced.BootDiskCapacity = &newSize + vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOn + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + disk, _ := getVMHomeDisk(ctx, vcVM, o) + Expect(disk.CapacityInBytes).To(BeEquivalentTo(newSize.Value())) + }) + }) +} diff --git a/pkg/providers/vsphere/vmprovider_vm_fast_deploy_test.go b/pkg/providers/vsphere/vmprovider_vm_fast_deploy_test.go index a7e831ae6..620e6b4c6 100644 --- a/pkg/providers/vsphere/vmprovider_vm_fast_deploy_test.go +++ b/pkg/providers/vsphere/vmprovider_vm_fast_deploy_test.go @@ -49,9 +49,8 @@ import ( "github.com/vmware-tanzu/vm-operator/test/testutil" ) -func fastDeployVMTests() { +func vmFastDeployTests() { const ( - dvpgName = "DC0_DVPG0" vmdkExt = ".vmdk" nvramExt = ".nvram" ) diff --git a/pkg/providers/vsphere/vmprovider_vm_group_test.go b/pkg/providers/vsphere/vmprovider_vm_group_test.go index d00e6fd88..dcc452206 100644 --- a/pkg/providers/vsphere/vmprovider_vm_group_test.go +++ b/pkg/providers/vsphere/vmprovider_vm_group_test.go @@ -5,6 +5,9 @@ package vsphere_test import ( + "context" + "math/rand" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -13,314 +16,408 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha6" - vspherepolv1 "github.com/vmware-tanzu/vm-operator/external/vsphere-policy/api/v1alpha1" - pkgcond "github.com/vmware-tanzu/vm-operator/pkg/conditions" + "github.com/vmware-tanzu/vm-operator/pkg/conditions" pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + ctxop "github.com/vmware-tanzu/vm-operator/pkg/context/operation" + pkgerr "github.com/vmware-tanzu/vm-operator/pkg/errors" "github.com/vmware-tanzu/vm-operator/pkg/providers" "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" - pkgutil "github.com/vmware-tanzu/vm-operator/pkg/util" + "github.com/vmware-tanzu/vm-operator/pkg/util/kube/cource" + "github.com/vmware-tanzu/vm-operator/pkg/util/ovfcache" "github.com/vmware-tanzu/vm-operator/test/builder" ) func vmGroupTests() { - var ( + parentCtx context.Context initObjects []client.Object testConfig builder.VCSimTestConfig ctx *builder.TestContextForVCSim vmProvider providers.VirtualMachineProviderInterface nsInfo builder.WorkloadNamespaceInfo - vm1 *vmopv1.VirtualMachine - vm2 *vmopv1.VirtualMachine + vm *vmopv1.VirtualMachine vmClass *vmopv1.VirtualMachineClass - vmGroup *vmopv1.VirtualMachineGroup + + zoneName string ) BeforeEach(func() { + parentCtx = pkgcfg.NewContextWithDefaultConfig() + parentCtx = ctxop.WithContext(parentCtx) + parentCtx = ovfcache.WithContext(parentCtx) + parentCtx = cource.WithContext(parentCtx) + pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { + config.AsyncCreateEnabled = false + config.AsyncSignalEnabled = false + }) testConfig = builder.VCSimTestConfig{ WithContentLibrary: true, } - vm1 = builder.DummyBasicVirtualMachine("group-placement-vm-1", "") - vm2 = builder.DummyBasicVirtualMachine("group-placement-vm-2", "") vmClass = builder.DummyVirtualMachineClassGenName() + vm = builder.DummyBasicVirtualMachine("test-vm", "") - vmGroup = &vmopv1.VirtualMachineGroup{ - ObjectMeta: metav1.ObjectMeta{ - Name: "vm-group-test", - }, - Spec: vmopv1.VirtualMachineGroupSpec{ - BootOrder: make([]vmopv1.VirtualMachineGroupBootOrderGroup, 1), - }, + if vm.Spec.Network == nil { + vm.Spec.Network = &vmopv1.VirtualMachineNetworkSpec{} } - vmGroup.Spec.BootOrder[0].Members = append(vmGroup.Spec.BootOrder[0].Members, - vmopv1.GroupMember{Kind: "VirtualMachine", Name: vm1.Name}, - vmopv1.GroupMember{Kind: "VirtualMachine", Name: vm2.Name}) + vm.Spec.Network.Disabled = true }) JustBeforeEach(func() { - ctx = suite.NewTestContextForVCSim(testConfig, initObjects...) + ctx = suite.NewTestContextForVCSimWithParentContext( + parentCtx, testConfig, initObjects...) pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { - config.Features.FastDeploy = true + config.MaxDeployThreadsOnProvider = 1 }) - vmProvider = vsphere.NewVSphereVMProviderFromClient(ctx, ctx.Client, ctx.Recorder) + vmProvider = vsphere.NewVSphereVMProviderFromClient( + ctx, ctx.Client, ctx.Recorder) nsInfo = ctx.CreateWorkloadNamespace() vmClass.Namespace = nsInfo.Namespace Expect(ctx.Client.Create(ctx, vmClass)).To(Succeed()) - Expect(ctx.Client.Status().Update(ctx, vmClass)).To(Succeed()) - initVM := func(vm *vmopv1.VirtualMachine) { - vm.Namespace = nsInfo.Namespace - vm.Spec.ClassName = vmClass.Name - vm.Spec.ImageName = ctx.ContentLibraryItem1Name - vm.Spec.Image.Kind = cvmiKind - vm.Spec.Image.Name = ctx.ContentLibraryItem1Name - vm.Spec.StorageClass = ctx.StorageClassName - vm.Spec.GroupName = vmGroup.Name + clusterVMI1 := &vmopv1.ClusterVirtualMachineImage{} + + if testConfig.WithContentLibrary { + Expect(ctx.Client.Get( + ctx, client.ObjectKey{Name: ctx.ContentLibraryItem1Name}, + clusterVMI1)).To(Succeed()) + } else { + vsphere.SkipVMImageCLProviderCheck = true + clusterVMI1 = builder.DummyClusterVirtualMachineImage("DC0_C0_RP0_VM0") + Expect(ctx.Client.Create(ctx, clusterVMI1)).To(Succeed()) + conditions.MarkTrue(clusterVMI1, vmopv1.ReadyConditionType) + Expect(ctx.Client.Status().Update(ctx, clusterVMI1)).To(Succeed()) } - initVM(vm1) - initVM(vm2) - vmGroup.Namespace = nsInfo.Namespace + vm.Namespace = nsInfo.Namespace + vm.Spec.ClassName = vmClass.Name + vm.Spec.ImageName = clusterVMI1.Name + vm.Spec.Image.Kind = cvmiKind + vm.Spec.Image.Name = clusterVMI1.Name + vm.Spec.StorageClass = ctx.StorageClassName - { - // TODO: Put this test builder to reduce duplication. + Expect(ctx.Client.Create(ctx, vm)).To(Succeed()) - vmic := vmopv1.VirtualMachineImageCache{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: pkgcfg.FromContext(ctx).PodNamespace, - Name: pkgutil.VMIName(ctx.ContentLibraryItem1ID), - }, - } - Expect(ctx.Client.Create(ctx, &vmic)).To(Succeed()) + zoneName = ctx.GetFirstZoneName() + vm.Labels[corev1.LabelTopologyZone] = zoneName + Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) + }) - vmicm := corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: vmic.Namespace, - Name: vmic.Name, - }, - Data: map[string]string{ - "value": ctx.ContentLibraryItem1YAML, - }, - } - Expect(ctx.Client.Create(ctx, &vmicm)).To(Succeed()) + AfterEach(func() { + vsphere.SkipVMImageCLProviderCheck = false - vmic.Status = vmopv1.VirtualMachineImageCacheStatus{ - OVF: &vmopv1.VirtualMachineImageCacheOVFStatus{ - ConfigMapName: vmic.Name, - ProviderVersion: ctx.ContentLibraryItem1Version, - }, - Conditions: []metav1.Condition{ - { - Type: vmopv1.VirtualMachineImageCacheConditionHardwareReady, - Status: metav1.ConditionTrue, - }, - }, - } - Expect(ctx.Client.Status().Update(ctx, &vmic)).To(Succeed()) - - pkgcond.MarkTrue( - &vmic, - vmopv1.VirtualMachineImageCacheConditionFilesReady) - vmic.Status.Locations = []vmopv1.VirtualMachineImageCacheLocationStatus{ - { - DatacenterID: ctx.Datacenter.Reference().Value, - DatastoreID: ctx.Datastore.Reference().Value, - Files: []vmopv1.VirtualMachineImageCacheFileStatus{ - { - ID: ctx.ContentLibraryItem1Disk1Path, - Type: vmopv1.VirtualMachineImageCacheFileTypeDisk, - DiskType: vmopv1.VolumeTypeClassic, - }, - { - ID: ctx.ContentLibraryItem1NVRAMPath, - Type: vmopv1.VirtualMachineImageCacheFileTypeOther, - }, - }, - Conditions: []metav1.Condition{ - { - Type: vmopv1.ReadyConditionType, - Status: metav1.ConditionTrue, - }, - }, - }, - } - Expect(ctx.Client.Status().Update(ctx, &vmic)).To(Succeed()) + if vm != nil && + !pkgcfg.FromContext(ctx).Features.BringYourOwnEncryptionKey { + By("Assert vm.Status.Crypto is nil when BYOK is disabled", func() { + Expect(vm.Status.Crypto).To(BeNil()) + }) } - }) - AfterEach(func() { + vmClass = nil + vm = nil + ctx.AfterEach() ctx = nil initObjects = nil vmProvider = nil nsInfo = builder.WorkloadNamespaceInfo{} + }) - vm1 = nil - vm2 = nil - vmClass = nil - vmGroup = nil + BeforeEach(func() { + pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { + config.Features.VMGroups = true + }) }) - assertMemberStatusForVM := func(vm *vmopv1.VirtualMachine, ms vmopv1.VirtualMachineGroupMemberStatus) { - GinkgoHelper() - - Expect(ms.Name).To(Equal(vm.Name), "Unexpected Name") - Expect(ms.Kind).To(Equal("VirtualMachine"), "Unexpected Kind") - Expect(ms.Placement).ToNot(BeNil(), "Missing Placement") - Expect(pkgcond.IsTrue(&ms, vmopv1.VirtualMachineGroupMemberConditionPlacementReady)).To(BeTrue(), "No placement ready condition") - Expect(ms.Placement.Zone).ToNot(BeEmpty(), "Missing Placement Zone") - Expect(ms.Placement.Pool).ToNot(BeEmpty(), "Missing Placement Pool") - Expect(ms.Placement.Node).To(BeEmpty(), "Has Placement Node") - if pkgcfg.FromContext(ctx).Features.FastDeploy { - Expect(ms.Placement.Datastores).ToNot(BeEmpty(), "Missing Placement Datastores") - // Verify against VirtualMachineImageCache.Status - } else { - Expect(ms.Placement.Datastores).To(BeEmpty(), "Has Placement Datastores") + var vmg vmopv1.VirtualMachineGroup + JustBeforeEach(func() { + // Remove explicit zone label so group placement can work + if vm.Labels != nil { + delete(vm.Labels, corev1.LabelTopologyZone) + Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) } - } - - assertNotReadyMemberStatusForVM := func( - vm *vmopv1.VirtualMachine, - ms vmopv1.VirtualMachineGroupMemberStatus, - reason string) { - - GinkgoHelper() - - Expect(ms.Name).To(Equal(vm.Name), "Unexpected Name") - Expect(ms.Kind).To(Equal("VirtualMachine"), "Unexpected Kind") - Expect(ms.Placement).To(BeNil(), "Has Placement") - - c := pkgcond.Get(ms, vmopv1.VirtualMachineGroupMemberConditionPlacementReady) - Expect(c).ToNot(BeNil(), "Condition missing") - Expect(c.Status).To(Equal(metav1.ConditionFalse)) - Expect(c.Reason).To(Equal(reason)) - } - - Context("Group placement with VMs specifying affinity policies", func() { - It("should process preferred VM affinity policies during group placement", func() { - // Add preferred affinity policy to vm1 - vm1.Spec.Affinity = &vmopv1.AffinitySpec{ - VMAffinity: &vmopv1.VMAffinitySpec{ - PreferredDuringSchedulingPreferredDuringExecution: []vmopv1.VMAffinityTerm{ + + vmg = vmopv1.VirtualMachineGroup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vmg", + Namespace: vm.Namespace, + }, + } + Expect(ctx.Client.Create(ctx, &vmg)).To(Succeed()) + }) + + Context("VM Creation", func() { + When("spec.groupName is set to a non-existent group", func() { + JustBeforeEach(func() { + vm.Spec.GroupName = "vmg-invalid" + }) + Specify("it should return an error creating VM", func() { + err := createOrUpdateVM(ctx, vmProvider, vm) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("VM is not linked to its group")) + }) + }) + + When("spec.groupName is set to a group to which the VM does not belong", func() { + JustBeforeEach(func() { + vm.Spec.GroupName = vmg.Name + }) + Specify("it should return an error creating VM", func() { + err := createOrUpdateVM(ctx, vmProvider, vm) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("VM is not linked to its group")) + }) + }) + + When("spec.groupName is set to a group to which the VM does belong", func() { + JustBeforeEach(func() { + vm.Spec.GroupName = vmg.Name + vmg.Spec.BootOrder = []vmopv1.VirtualMachineGroupBootOrderGroup{ + { + Members: []vmopv1.GroupMember{ + { + Name: vm.Name, + Kind: "VirtualMachine", + }, + }, + }, + } + Expect(ctx.Client.Update(ctx, &vmg)).To(Succeed()) + }) + + When("VM Group placement condition is not ready", func() { + JustBeforeEach(func() { + vmg.Status.Members = []vmopv1.VirtualMachineGroupMemberStatus{ { - LabelSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "app": "database", + Name: vm.Name, + Kind: "VirtualMachine", + Conditions: []metav1.Condition{ + { + Type: vmopv1.VirtualMachineGroupMemberConditionPlacementReady, + Status: metav1.ConditionFalse, }, }, - TopologyKey: "topology.kubernetes.io/zone", }, - }, - }, - } + } + Expect(ctx.Client.Status().Update(ctx, &vmg)).To(Succeed()) + }) + Specify("it should return an error creating VM", func() { + err := createOrUpdateVM(ctx, vmProvider, vm) + Expect(err).To(HaveOccurred()) + Expect(pkgerr.IsNoRequeueError(err)).To(BeTrue()) + Expect(err.Error()).To(ContainSubstring("VM Group placement is not ready")) + }) + }) - groupPlacements := []providers.VMGroupPlacement{ - { - VMGroup: vmGroup, - VMMembers: []*vmopv1.VirtualMachine{ - vm1, - vm2, - }, - }, - } + When("VM Group placement condition is ready", func() { + var ( + groupZone string + groupHost string + groupPool string + ) + JustBeforeEach(func() { + // Ensure the group zone is different to verify the placement actually from group. + Expect(len(ctx.ZoneNames)).To(BeNumerically(">", 1)) + groupZone = ctx.ZoneNames[rand.Intn(len(ctx.ZoneNames))] + for groupZone == vm.Labels[corev1.LabelTopologyZone] { + groupZone = ctx.ZoneNames[rand.Intn(len(ctx.ZoneNames))] + } + + ccrs := ctx.GetAZClusterComputes(groupZone) + Expect(ccrs).ToNot(BeEmpty()) + ccr := ccrs[0] + hosts, err := ccr.Hosts(ctx) + Expect(err).ToNot(HaveOccurred()) + Expect(hosts).ToNot(BeEmpty()) + groupHost = hosts[0].Reference().Value + + nsRP := ctx.GetResourcePoolForNamespace(nsInfo.Namespace, groupZone, "") + Expect(nsRP).ToNot(BeNil()) + groupPool = nsRP.Reference().Value + + vmg.Status.Members = []vmopv1.VirtualMachineGroupMemberStatus{ + { + Name: vm.Name, + Kind: "VirtualMachine", + Conditions: []metav1.Condition{ + { + Type: vmopv1.VirtualMachineGroupMemberConditionPlacementReady, + Status: metav1.ConditionTrue, + }, + }, + Placement: &vmopv1.VirtualMachinePlacementStatus{ + Zone: groupZone, + Node: groupHost, + Pool: groupPool, + }, + }, + } + Expect(ctx.Client.Status().Update(ctx, &vmg)).To(Succeed()) + }) + Specify("it should successfully create VM from group's placement", func() { + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + By("VM is placed in the expected zone from group", func() { + Expect(vm.Status.Zone).To(Equal(groupZone)) + }) + By("VM is placed in the expected host from group", func() { + vmHost, err := vcVM.HostSystem(ctx) + Expect(err).ToNot(HaveOccurred()) + Expect(vmHost.Reference().Value).To(Equal(groupHost)) + }) + By("VM is created in the expected pool from group", func() { + rp, err := vcVM.ResourcePool(ctx) + Expect(err).ToNot(HaveOccurred()) + Expect(rp.Reference().Value).To(Equal(groupPool)) + }) + By("VM has expected group linked condition", func() { + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineGroupMemberConditionGroupLinked)).To(BeTrue()) + }) + }) + }) + }) + }) - err := vmProvider.PlaceVirtualMachineGroup(ctx, vmGroup, groupPlacements) - Expect(err).ToNot(HaveOccurred()) - Expect(vmGroup.Status.Members).To(HaveLen(2)) - assertMemberStatusForVM(vm1, vmGroup.Status.Members[0]) - assertMemberStatusForVM(vm2, vmGroup.Status.Members[1]) + Context("VM Update", func() { + JustBeforeEach(func() { + // Unset groupName to ensure the VM can be created. + vm.Spec.GroupName = "" + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) }) - It("should process required VM affinity policies during group placement", func() { - // Add required affinity policy to vm1 - vm1.Spec.Affinity = &vmopv1.AffinitySpec{ - VMAffinity: &vmopv1.VMAffinitySpec{ - RequiredDuringSchedulingPreferredDuringExecution: []vmopv1.VMAffinityTerm{ - { - LabelSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "tier": "frontend", - }, + When("spec.groupName is set to a non-existent group", func() { + JustBeforeEach(func() { + vm.Spec.GroupName = "vmg-invalid" + }) + Specify("vm should have group linked condition set to false", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(conditions.IsFalse(vm, vmopv1.VirtualMachineGroupMemberConditionGroupLinked)).To(BeTrue()) + }) + }) + + When("spec.groupName is set to a group to which the VM does not belong", func() { + JustBeforeEach(func() { + vm.Spec.GroupName = vmg.Name + }) + Specify("vm should have group linked condition set to false", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(conditions.IsFalse(vm, vmopv1.VirtualMachineGroupMemberConditionGroupLinked)).To(BeTrue()) + }) + }) + + When("spec.groupName is set to a group to which the VM does belong", func() { + JustBeforeEach(func() { + vm.Spec.GroupName = vmg.Name + vmg.Spec.BootOrder = []vmopv1.VirtualMachineGroupBootOrderGroup{ + { + Members: []vmopv1.GroupMember{ + { + Name: vm.Name, + Kind: "VirtualMachine", }, - TopologyKey: "topology.kubernetes.io/zone", }, }, - }, - } + } + Expect(ctx.Client.Update(ctx, &vmg)).To(Succeed()) + }) + Specify("vm should have group linked condition set to true", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineGroupMemberConditionGroupLinked)).To(BeTrue()) + }) + + When("spec.groupName no longer points to group", func() { + Specify("vm should no longer have group linked condition", func() { + vm.Spec.GroupName = "" + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + c := conditions.Get(vm, vmopv1.VirtualMachineGroupMemberConditionGroupLinked) + Expect(c).To(BeNil()) + }) + }) + }) + }) - groupPlacements := []providers.VMGroupPlacement{ - { - VMGroup: vmGroup, - VMMembers: []*vmopv1.VirtualMachine{ - vm1, - vm2, + Context("Zone Label Override for VM Groups", func() { + var ( + vm *vmopv1.VirtualMachine + vmGroup *vmopv1.VirtualMachineGroup + vmClass *vmopv1.VirtualMachineClass + zoneName string + ) + + BeforeEach(func() { + vmClass = builder.DummyVirtualMachineClassGenName() + vm = builder.DummyBasicVirtualMachine("test-vm-zone-override", "") + vmGroup = &vmopv1.VirtualMachineGroup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-group-zone-override", + }, + Spec: vmopv1.VirtualMachineGroupSpec{ + BootOrder: []vmopv1.VirtualMachineGroupBootOrderGroup{ + { + Members: []vmopv1.GroupMember{ + {Kind: "VirtualMachine", Name: vm.ObjectMeta.Name}, + }, + }, }, }, } - - err := vmProvider.PlaceVirtualMachineGroup(ctx, vmGroup, groupPlacements) - Expect(err).ToNot(HaveOccurred()) - Expect(vmGroup.Status.Members).To(HaveLen(2)) - assertMemberStatusForVM(vm1, vmGroup.Status.Members[0]) - assertMemberStatusForVM(vm2, vmGroup.Status.Members[1]) }) - }) - Context("VSpherePolicies is enabled", func() { JustBeforeEach(func() { pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { - config.Features.VSpherePolicies = true + config.Features.VMGroups = true }) - }) - It("should process VM with PolicyEval during group placement", func() { - groupPlacements := []providers.VMGroupPlacement{ - { - VMGroup: vmGroup, - VMMembers: []*vmopv1.VirtualMachine{ - vm1, - vm2, - }, - }, - } + vmClass.Namespace = nsInfo.Namespace + Expect(ctx.Client.Create(ctx, vmClass)).To(Succeed()) + + vmGroup.Namespace = nsInfo.Namespace + Expect(ctx.Client.Create(ctx, vmGroup)).To(Succeed()) + + clusterVMImage := &vmopv1.ClusterVirtualMachineImage{} + Expect(ctx.Client.Get(ctx, client.ObjectKey{Name: ctx.ContentLibraryItem1Name}, clusterVMImage)).To(Succeed()) + + vm.Namespace = nsInfo.Namespace + vm.Spec.ClassName = vmClass.Name + vm.Spec.ImageName = clusterVMImage.Name + vm.Spec.Image.Kind = cvmiKind + vm.Spec.Image.Name = clusterVMImage.Name + vm.Spec.StorageClass = ctx.StorageClassName - err := vmProvider.PlaceVirtualMachineGroup(ctx, vmGroup, groupPlacements) - Expect(err).To(HaveOccurred()) - - Expect(vmGroup.Status.Members).To(HaveLen(2)) - assertNotReadyMemberStatusForVM(vm1, vmGroup.Status.Members[0], "NotReady") - assertNotReadyMemberStatusForVM(vm2, vmGroup.Status.Members[1], "NotReady") - - markPolicyEvalReady := func(vm *vmopv1.VirtualMachine) { - policyEval := &vspherepolv1.PolicyEvaluation{} - Expect(ctx.Client.Get(ctx, client.ObjectKey{ - Namespace: vm.Namespace, - Name: "vm-" + vm.Name}, - policyEval)).To(Succeed()) - policyEval.Status.ObservedGeneration = policyEval.Generation - pkgcond.MarkTrue(policyEval, vspherepolv1.ReadyConditionType) - Expect(ctx.Client.Status().Update(ctx, policyEval)).To(Succeed()) + vm.Spec.GroupName = vmGroup.Name + + zoneName = ctx.ZoneNames[rand.Intn(len(ctx.ZoneNames))] + vm.Labels = map[string]string{ + corev1.LabelTopologyZone: zoneName, } - markPolicyEvalReady(vm1) - err = vmProvider.PlaceVirtualMachineGroup(ctx, vmGroup, groupPlacements) - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError(vsphere.ErrVMGroupPlacementConfigSpec)) + Expect(ctx.Client.Create(ctx, vm)).To(Succeed()) + }) + + Context("when VM has explicit zone label and is part of group", func() { + It("should create VM in specified zone, not using group placement", func() { + err := vmProvider.CreateOrUpdateVirtualMachine(ctx, vm) + Expect(err).To(HaveOccurred()) + Expect(pkgerr.IsNoRequeueError(err)).To(BeTrue()) + Expect(err).To(MatchError(vsphere.ErrCreate)) - Expect(vmGroup.Status.Members).To(HaveLen(2)) - assertNotReadyMemberStatusForVM(vm1, vmGroup.Status.Members[0], "PendingPlacement") - assertNotReadyMemberStatusForVM(vm2, vmGroup.Status.Members[1], "NotReady") + // Verify VM was created + Expect(vm.Status.UniqueID).ToNot(BeEmpty()) + }) + }) - markPolicyEvalReady(vm2) - err = vmProvider.PlaceVirtualMachineGroup(ctx, vmGroup, groupPlacements) - Expect(err).ToNot(HaveOccurred()) + Context("when VM has explicit zone label but is not linked to group", func() { + BeforeEach(func() { + vmGroup.Spec.BootOrder = []vmopv1.VirtualMachineGroupBootOrderGroup{} + }) - Expect(vmGroup.Status.Members).To(HaveLen(2)) - assertMemberStatusForVM(vm1, vmGroup.Status.Members[0]) - assertMemberStatusForVM(vm2, vmGroup.Status.Members[1]) + It("should fail to create VM", func() { + err := vmProvider.CreateOrUpdateVirtualMachine(ctx, vm) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("VM is not linked to its group")) + }) }) }) } diff --git a/pkg/providers/vsphere/vmprovider_vm_guest_heartbeat_test.go b/pkg/providers/vsphere/vmprovider_vm_guest_heartbeat_test.go new file mode 100644 index 000000000..8cc1a86b0 --- /dev/null +++ b/pkg/providers/vsphere/vmprovider_vm_guest_heartbeat_test.go @@ -0,0 +1,126 @@ +// © Broadcom. All Rights Reserved. +// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package vsphere_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha6" + "github.com/vmware-tanzu/vm-operator/pkg/conditions" + pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + ctxop "github.com/vmware-tanzu/vm-operator/pkg/context/operation" + "github.com/vmware-tanzu/vm-operator/pkg/providers" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" + "github.com/vmware-tanzu/vm-operator/pkg/util/kube/cource" + "github.com/vmware-tanzu/vm-operator/pkg/util/ovfcache" + "github.com/vmware-tanzu/vm-operator/test/builder" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func vmGuestHeartbeatTests() { + var ( + parentCtx context.Context + initObjects []client.Object + testConfig builder.VCSimTestConfig + ctx *builder.TestContextForVCSim + vmProvider providers.VirtualMachineProviderInterface + nsInfo builder.WorkloadNamespaceInfo + + vm *vmopv1.VirtualMachine + vmClass *vmopv1.VirtualMachineClass + ) + + BeforeEach(func() { + parentCtx = pkgcfg.NewContextWithDefaultConfig() + parentCtx = ctxop.WithContext(parentCtx) + parentCtx = ovfcache.WithContext(parentCtx) + parentCtx = cource.WithContext(parentCtx) + pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { + config.AsyncCreateEnabled = false + config.AsyncSignalEnabled = false + }) + testConfig = builder.VCSimTestConfig{ + WithContentLibrary: true, + } + + vmClass = builder.DummyVirtualMachineClassGenName() + vm = builder.DummyBasicVirtualMachine("test-vm", "") + + if vm.Spec.Network == nil { + vm.Spec.Network = &vmopv1.VirtualMachineNetworkSpec{} + } + vm.Spec.Network.Disabled = true + }) + + JustBeforeEach(func() { + ctx = suite.NewTestContextForVCSimWithParentContext( + parentCtx, testConfig, initObjects...) + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.MaxDeployThreadsOnProvider = 1 + }) + vmProvider = vsphere.NewVSphereVMProviderFromClient( + ctx, ctx.Client, ctx.Recorder) + nsInfo = ctx.CreateWorkloadNamespace() + + vmClass.Namespace = nsInfo.Namespace + Expect(ctx.Client.Create(ctx, vmClass)).To(Succeed()) + + clusterVMI1 := &vmopv1.ClusterVirtualMachineImage{} + + if testConfig.WithContentLibrary { + Expect(ctx.Client.Get( + ctx, client.ObjectKey{Name: ctx.ContentLibraryItem1Name}, + clusterVMI1)).To(Succeed()) + } else { + vsphere.SkipVMImageCLProviderCheck = true + clusterVMI1 = builder.DummyClusterVirtualMachineImage("DC0_C0_RP0_VM0") + Expect(ctx.Client.Create(ctx, clusterVMI1)).To(Succeed()) + conditions.MarkTrue(clusterVMI1, vmopv1.ReadyConditionType) + Expect(ctx.Client.Status().Update(ctx, clusterVMI1)).To(Succeed()) + } + + vm.Namespace = nsInfo.Namespace + vm.Spec.ClassName = vmClass.Name + vm.Spec.ImageName = clusterVMI1.Name + vm.Spec.Image.Kind = cvmiKind + vm.Spec.Image.Name = clusterVMI1.Name + vm.Spec.StorageClass = ctx.StorageClassName + + Expect(ctx.Client.Create(ctx, vm)).To(Succeed()) + }) + + AfterEach(func() { + vsphere.SkipVMImageCLProviderCheck = false + + if vm != nil && + !pkgcfg.FromContext(ctx).Features.BringYourOwnEncryptionKey { + By("Assert vm.Status.Crypto is nil when BYOK is disabled", func() { + Expect(vm.Status.Crypto).To(BeNil()) + }) + } + + vmClass = nil + vm = nil + + ctx.AfterEach() + ctx = nil + initObjects = nil + vmProvider = nil + nsInfo = builder.WorkloadNamespaceInfo{} + }) + + JustBeforeEach(func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + }) + + It("return guest heartbeat", func() { + heartbeat, err := vmProvider.GetVirtualMachineGuestHeartbeat(ctx, vm) + Expect(err).ToNot(HaveOccurred()) + // Just testing for property query: field not set in vcsim. + Expect(heartbeat).To(BeEmpty()) + }) +} diff --git a/pkg/providers/vsphere/vmprovider_vm_guestid_test.go b/pkg/providers/vsphere/vmprovider_vm_guestid_test.go index 3e0fd8e2d..c56fe6973 100644 --- a/pkg/providers/vsphere/vmprovider_vm_guestid_test.go +++ b/pkg/providers/vsphere/vmprovider_vm_guestid_test.go @@ -13,7 +13,7 @@ import ( "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" ) -func defaultGuestIDIfEmptyTests() { +func vmGuestIDTests() { const ( otherGuest = string(vimtypes.VirtualMachineGuestOsIdentifierOtherGuest) otherGuest64 = string(vimtypes.VirtualMachineGuestOsIdentifierOtherGuest64) diff --git a/pkg/providers/vsphere/vmprovider_vm_hardwareversion_test.go b/pkg/providers/vsphere/vmprovider_vm_hardwareversion_test.go new file mode 100644 index 000000000..847b7626c --- /dev/null +++ b/pkg/providers/vsphere/vmprovider_vm_hardwareversion_test.go @@ -0,0 +1,127 @@ +// © Broadcom. All Rights Reserved. +// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package vsphere_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha6" + "github.com/vmware-tanzu/vm-operator/pkg/conditions" + pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + ctxop "github.com/vmware-tanzu/vm-operator/pkg/context/operation" + "github.com/vmware-tanzu/vm-operator/pkg/providers" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" + "github.com/vmware-tanzu/vm-operator/pkg/util/kube/cource" + "github.com/vmware-tanzu/vm-operator/pkg/util/ovfcache" + "github.com/vmware-tanzu/vm-operator/test/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + + vimtypes "github.com/vmware/govmomi/vim25/types" +) + +func vmHardwareVersionTests() { + var ( + parentCtx context.Context + initObjects []client.Object + testConfig builder.VCSimTestConfig + ctx *builder.TestContextForVCSim + vmProvider providers.VirtualMachineProviderInterface + nsInfo builder.WorkloadNamespaceInfo + + vm *vmopv1.VirtualMachine + vmClass *vmopv1.VirtualMachineClass + ) + + BeforeEach(func() { + parentCtx = pkgcfg.NewContextWithDefaultConfig() + parentCtx = ctxop.WithContext(parentCtx) + parentCtx = ovfcache.WithContext(parentCtx) + parentCtx = cource.WithContext(parentCtx) + pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { + config.AsyncCreateEnabled = false + config.AsyncSignalEnabled = false + }) + testConfig = builder.VCSimTestConfig{ + WithContentLibrary: true, + } + + vmClass = builder.DummyVirtualMachineClassGenName() + vm = builder.DummyBasicVirtualMachine("test-vm", "") + + if vm.Spec.Network == nil { + vm.Spec.Network = &vmopv1.VirtualMachineNetworkSpec{} + } + vm.Spec.Network.Disabled = true + }) + + JustBeforeEach(func() { + ctx = suite.NewTestContextForVCSimWithParentContext( + parentCtx, testConfig, initObjects...) + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.MaxDeployThreadsOnProvider = 1 + }) + vmProvider = vsphere.NewVSphereVMProviderFromClient( + ctx, ctx.Client, ctx.Recorder) + nsInfo = ctx.CreateWorkloadNamespace() + + vmClass.Namespace = nsInfo.Namespace + Expect(ctx.Client.Create(ctx, vmClass)).To(Succeed()) + + clusterVMI1 := &vmopv1.ClusterVirtualMachineImage{} + + if testConfig.WithContentLibrary { + Expect(ctx.Client.Get( + ctx, client.ObjectKey{Name: ctx.ContentLibraryItem1Name}, + clusterVMI1)).To(Succeed()) + } else { + vsphere.SkipVMImageCLProviderCheck = true + clusterVMI1 = builder.DummyClusterVirtualMachineImage("DC0_C0_RP0_VM0") + Expect(ctx.Client.Create(ctx, clusterVMI1)).To(Succeed()) + conditions.MarkTrue(clusterVMI1, vmopv1.ReadyConditionType) + Expect(ctx.Client.Status().Update(ctx, clusterVMI1)).To(Succeed()) + } + + vm.Namespace = nsInfo.Namespace + vm.Spec.ClassName = vmClass.Name + vm.Spec.ImageName = clusterVMI1.Name + vm.Spec.Image.Kind = cvmiKind + vm.Spec.Image.Name = clusterVMI1.Name + vm.Spec.StorageClass = ctx.StorageClassName + + Expect(ctx.Client.Create(ctx, vm)).To(Succeed()) + }) + + AfterEach(func() { + vsphere.SkipVMImageCLProviderCheck = false + + if vm != nil && + !pkgcfg.FromContext(ctx).Features.BringYourOwnEncryptionKey { + By("Assert vm.Status.Crypto is nil when BYOK is disabled", func() { + Expect(vm.Status.Crypto).To(BeNil()) + }) + } + + vmClass = nil + vm = nil + + ctx.AfterEach() + ctx = nil + initObjects = nil + vmProvider = nil + nsInfo = builder.WorkloadNamespaceInfo{} + }) + + JustBeforeEach(func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + }) + + It("should return the expected version", func() { + version, err := vmProvider.GetVirtualMachineHardwareVersion(ctx, vm) + Expect(err).NotTo(HaveOccurred()) + Expect(version).To(Equal(vimtypes.VMX9)) + }) +} diff --git a/pkg/providers/vsphere/vmprovider_vm_instance_storage_test.go b/pkg/providers/vsphere/vmprovider_vm_instance_storage_test.go new file mode 100644 index 000000000..55f3a30d0 --- /dev/null +++ b/pkg/providers/vsphere/vmprovider_vm_instance_storage_test.go @@ -0,0 +1,225 @@ +// © Broadcom. All Rights Reserved. +// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package vsphere_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "sigs.k8s.io/controller-runtime/pkg/client" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha6" + "github.com/vmware-tanzu/vm-operator/pkg/conditions" + pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + ctxop "github.com/vmware-tanzu/vm-operator/pkg/context/operation" + "github.com/vmware-tanzu/vm-operator/pkg/providers" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/constants" + "github.com/vmware-tanzu/vm-operator/pkg/util/kube/cource" + "github.com/vmware-tanzu/vm-operator/pkg/util/ovfcache" + vmopv1util "github.com/vmware-tanzu/vm-operator/pkg/util/vmopv1" + "github.com/vmware-tanzu/vm-operator/test/builder" +) + +func vmInstanceStorageTests() { + var ( + parentCtx context.Context + initObjects []client.Object + testConfig builder.VCSimTestConfig + ctx *builder.TestContextForVCSim + vmProvider providers.VirtualMachineProviderInterface + nsInfo builder.WorkloadNamespaceInfo + + vm *vmopv1.VirtualMachine + vmClass *vmopv1.VirtualMachineClass + + zoneName string + ) + + BeforeEach(func() { + parentCtx = pkgcfg.NewContextWithDefaultConfig() + parentCtx = ctxop.WithContext(parentCtx) + parentCtx = ovfcache.WithContext(parentCtx) + parentCtx = cource.WithContext(parentCtx) + pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { + config.AsyncCreateEnabled = false + config.AsyncSignalEnabled = false + }) + testConfig = builder.VCSimTestConfig{ + WithContentLibrary: true, + } + + vmClass = builder.DummyVirtualMachineClassGenName() + vm = builder.DummyBasicVirtualMachine("test-vm", "") + + if vm.Spec.Network == nil { + vm.Spec.Network = &vmopv1.VirtualMachineNetworkSpec{} + } + vm.Spec.Network.Disabled = true + }) + + JustBeforeEach(func() { + ctx = suite.NewTestContextForVCSimWithParentContext( + parentCtx, testConfig, initObjects...) + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.MaxDeployThreadsOnProvider = 1 + }) + vmProvider = vsphere.NewVSphereVMProviderFromClient( + ctx, ctx.Client, ctx.Recorder) + nsInfo = ctx.CreateWorkloadNamespace() + + vmClass.Namespace = nsInfo.Namespace + Expect(ctx.Client.Create(ctx, vmClass)).To(Succeed()) + + clusterVMI1 := &vmopv1.ClusterVirtualMachineImage{} + + if testConfig.WithContentLibrary { + Expect(ctx.Client.Get( + ctx, client.ObjectKey{Name: ctx.ContentLibraryItem1Name}, + clusterVMI1)).To(Succeed()) + } else { + vsphere.SkipVMImageCLProviderCheck = true + clusterVMI1 = builder.DummyClusterVirtualMachineImage("DC0_C0_RP0_VM0") + Expect(ctx.Client.Create(ctx, clusterVMI1)).To(Succeed()) + conditions.MarkTrue(clusterVMI1, vmopv1.ReadyConditionType) + Expect(ctx.Client.Status().Update(ctx, clusterVMI1)).To(Succeed()) + } + + vm.Namespace = nsInfo.Namespace + vm.Spec.ClassName = vmClass.Name + vm.Spec.ImageName = clusterVMI1.Name + vm.Spec.Image.Kind = cvmiKind + vm.Spec.Image.Name = clusterVMI1.Name + vm.Spec.StorageClass = ctx.StorageClassName + + Expect(ctx.Client.Create(ctx, vm)).To(Succeed()) + + zoneName = ctx.GetFirstZoneName() + vm.Labels[corev1.LabelTopologyZone] = zoneName + Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) + }) + + AfterEach(func() { + vsphere.SkipVMImageCLProviderCheck = false + + if vm != nil && + !pkgcfg.FromContext(ctx).Features.BringYourOwnEncryptionKey { + By("Assert vm.Status.Crypto is nil when BYOK is disabled", func() { + Expect(vm.Status.Crypto).To(BeNil()) + }) + } + + vmClass = nil + vm = nil + + ctx.AfterEach() + ctx = nil + initObjects = nil + vmProvider = nil + nsInfo = builder.WorkloadNamespaceInfo{} + }) + + BeforeEach(func() { + testConfig.WithInstanceStorage = true + }) + + expectInstanceStorageVolumes := func( + vm *vmopv1.VirtualMachine, + isStorage vmopv1.InstanceStorage) { + + ExpectWithOffset(1, isStorage.Volumes).ToNot(BeEmpty()) + isVolumes := vmopv1util.FilterInstanceStorageVolumes(vm) + ExpectWithOffset(1, isVolumes).To(HaveLen(len(isStorage.Volumes))) + + for _, isVol := range isStorage.Volumes { + found := false + + for idx, vol := range isVolumes { + claim := vol.PersistentVolumeClaim.InstanceVolumeClaim + if claim.StorageClass == isStorage.StorageClass && claim.Size == isVol.Size { + isVolumes = append(isVolumes[:idx], isVolumes[idx+1:]...) + found = true + break + } + } + + ExpectWithOffset(1, found).To(BeTrue(), "failed to find instance storage volume for %v", isVol) + } + } + + It("creates VM without instance storage", func() { + _, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + }) + + It("create VM with instance storage", func() { + Expect(vm.Spec.Volumes).To(BeEmpty()) + + vmClass.Spec.Hardware.InstanceStorage = vmopv1.InstanceStorage{ + StorageClass: vm.Spec.StorageClass, + Volumes: []vmopv1.InstanceStorageVolume{ + { + Size: resource.MustParse("256Gi"), + }, + { + Size: resource.MustParse("512Gi"), + }, + }, + } + Expect(ctx.Client.Update(ctx, vmClass)).To(Succeed()) + + Expect(vmopv1util.IsInstanceStoragePresent(vm)).To(BeFalse()) + + _, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).To(MatchError(vsphere.ErrAddedInstanceStorageVols)) + + By("Instance storage volumes should be added to VM", func() { + Expect(vmopv1util.IsInstanceStoragePresent(vm)).To(BeTrue()) + expectInstanceStorageVolumes(vm, vmClass.Spec.Hardware.InstanceStorage) + }) + + _, err = createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).To(MatchError("instance storage PVCs are not bound yet")) + Expect(vmopv1util.IsInstanceStoragePresent(vm)).To(BeTrue()) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionCreated)).To(BeFalse()) + + By("Placement should have been done", func() { + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionPlacementReady)).To(BeTrue()) + Expect(vm.Annotations).To(HaveKey(constants.InstanceStorageSelectedNodeAnnotationKey)) + Expect(vm.Annotations).To(HaveKey(constants.InstanceStorageSelectedNodeMOIDAnnotationKey)) + }) + + isVol0 := vm.Spec.Volumes[0] + Expect(isVol0.PersistentVolumeClaim.InstanceVolumeClaim).ToNot(BeNil()) + + By("simulate volume controller workflow", func() { + // Simulate what would be set by volume controller. + vm.Annotations[constants.InstanceStoragePVCsBoundAnnotationKey] = "" + + _, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("one or more persistent volumes is pending")) + Expect(err.Error()).To(ContainSubstring(isVol0.Name)) + + // Simulate what would be set by the volume controller. + for _, vol := range vm.Spec.Volumes { + vm.Status.Volumes = append(vm.Status.Volumes, vmopv1.VirtualMachineVolumeStatus{ + Name: vol.Name, + Attached: true, + }) + } + }) + + By("VM is now created", func() { + _, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionCreated)).To(BeTrue()) + }) + }) +} diff --git a/pkg/providers/vsphere/vmprovider_vm_iso_test.go b/pkg/providers/vsphere/vmprovider_vm_iso_test.go new file mode 100644 index 000000000..60417bcd4 --- /dev/null +++ b/pkg/providers/vsphere/vmprovider_vm_iso_test.go @@ -0,0 +1,253 @@ +// © Broadcom. All Rights Reserved. +// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package vsphere_test + +import ( + "context" + "errors" + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "sigs.k8s.io/controller-runtime/pkg/client" + + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/google/uuid" + "github.com/vmware/govmomi/object" + "github.com/vmware/govmomi/simulator" + "github.com/vmware/govmomi/vapi/library" + "github.com/vmware/govmomi/vim25/mo" + vimtypes "github.com/vmware/govmomi/vim25/types" + + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha6" + "github.com/vmware-tanzu/vm-operator/pkg/conditions" + pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + ctxop "github.com/vmware-tanzu/vm-operator/pkg/context/operation" + pkgerr "github.com/vmware-tanzu/vm-operator/pkg/errors" + "github.com/vmware-tanzu/vm-operator/pkg/providers" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" + pkgutil "github.com/vmware-tanzu/vm-operator/pkg/util" + "github.com/vmware-tanzu/vm-operator/pkg/util/kube/cource" + "github.com/vmware-tanzu/vm-operator/pkg/util/ovfcache" + "github.com/vmware-tanzu/vm-operator/pkg/util/ptr" + "github.com/vmware-tanzu/vm-operator/test/builder" +) + +func vmISOTests() { + var ( + parentCtx context.Context + initObjects []client.Object + testConfig builder.VCSimTestConfig + ctx *builder.TestContextForVCSim + vmProvider providers.VirtualMachineProviderInterface + nsInfo builder.WorkloadNamespaceInfo + ) + + BeforeEach(func() { + parentCtx = pkgcfg.NewContextWithDefaultConfig() + parentCtx = ctxop.WithContext(parentCtx) + parentCtx = ovfcache.WithContext(parentCtx) + parentCtx = cource.WithContext(parentCtx) + pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { + config.AsyncCreateEnabled = false + config.AsyncSignalEnabled = false + }) + testConfig = builder.VCSimTestConfig{ + WithContentLibrary: true, + } + }) + + JustBeforeEach(func() { + ctx = suite.NewTestContextForVCSimWithParentContext(parentCtx, testConfig, initObjects...) + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.MaxDeployThreadsOnProvider = 1 + }) + vmProvider = vsphere.NewVSphereVMProviderFromClient(ctx, ctx.Client, ctx.Recorder) + nsInfo = ctx.CreateWorkloadNamespace() + }) + + AfterEach(func() { + ctx.AfterEach() + ctx = nil + initObjects = nil + vmProvider = nil + nsInfo = builder.WorkloadNamespaceInfo{} + }) + + var ( + vm *vmopv1.VirtualMachine + vmClass *vmopv1.VirtualMachineClass + ) + + BeforeEach(func() { + vmClass = builder.DummyVirtualMachineClassGenName() + vm = builder.DummyBasicVirtualMachine("test-vm-iso", "") + + // Reduce diff from old tests: by default don't create an NIC. + if vm.Spec.Network == nil { + vm.Spec.Network = &vmopv1.VirtualMachineNetworkSpec{} + } + vm.Spec.Network.Disabled = true + }) + + JustBeforeEach(func() { + vmClass.Namespace = nsInfo.Namespace + Expect(ctx.Client.Create(ctx, vmClass)).To(Succeed()) + + // Add required objects to get CD-ROM backing file name. + cvmiName := "vmi-iso" + objs := builder.DummyImageAndItemObjectsForCdromBacking(cvmiName, "", cvmiKind, "test-file.iso", ctx.ContentLibraryIsoItemID, true, true, resource.MustParse("100Mi"), true, true, "ISO") + for _, obj := range objs { + Expect(ctx.Client.Create(ctx, obj)).To(Succeed()) + } + + vm.Namespace = nsInfo.Namespace + vm.Spec.ClassName = vmClass.Name + vm.Spec.ImageName = cvmiName + vm.Spec.Image.Kind = cvmiKind + vm.Spec.Image.Name = cvmiName + vm.Spec.StorageClass = ctx.StorageClassName + vm.Spec.Hardware = &vmopv1.VirtualMachineHardwareSpec{ + Cdrom: []vmopv1.VirtualMachineCdromSpec{{ + Name: "cdrom0", + Image: vmopv1.VirtualMachineImageRef{ + Name: cvmiName, + Kind: cvmiKind, + }, + }}, + } + + Expect(ctx.Client.Create(ctx, vm)).To(Succeed()) + }) + + Context("return config", func() { + JustBeforeEach(func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + }) + + It("return config.files", func() { + vmPathName := "config.files.vmPathName" + props, err := vmProvider.GetVirtualMachineProperties(ctx, vm, []string{vmPathName}) + Expect(err).NotTo(HaveOccurred()) + var path object.DatastorePath + path.FromString(props[vmPathName].(string)) + Expect(path.Datastore).NotTo(BeEmpty()) + }) + }) + + When("Fast Deploy is enabled", func() { + + var ( + vmic vmopv1.VirtualMachineImageCache + ) + + BeforeEach(func() { + testConfig.WithContentLibrary = true + pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { + config.Features.FastDeploy = true + }) + // Ensure the VM has a UID to verify the VM directory path + // is different from the VM's Kubernetes UID on vSAN. + vm.UID = "test-vm-iso-uid" + }) + + JustBeforeEach(func() { + vmicName := pkgutil.VMIName(ctx.ContentLibraryIsoItemID) + vmic = vmopv1.VirtualMachineImageCache{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: pkgcfg.FromContext(ctx).PodNamespace, + Name: vmicName, + }, + } + Expect(ctx.Client.Create(ctx, &vmic)).To(Succeed()) + }) + + assertVMICNotReady := func(err error, msg, name, dcID, dsID string) { + var e pkgerr.VMICacheNotReadyError + ExpectWithOffset(1, errors.As(err, &e)).To(BeTrue()) + ExpectWithOffset(1, e.Message).To(Equal(msg)) + ExpectWithOffset(1, e.Name).To(Equal(name)) + ExpectWithOffset(1, e.DatacenterID).To(Equal(dcID)) + ExpectWithOffset(1, e.DatastoreID).To(Equal(dsID)) + } + + When("cache files are not ready", func() { + It("should fail", func() { + err := createOrUpdateVM(ctx, vmProvider, vm) + Expect(err).To(HaveOccurred()) + assertVMICNotReady( + err, + "cached files not ready", + vmic.Name, + ctx.Datacenter.Reference().Value, + ctx.Datastore.Reference().Value) + }) + }) + + When("cache files are ready", func() { + JustBeforeEach(func() { + // Simulate vSAN datastore with TopLevelDirectoryCreateSupported disabled. + sctx := ctx.SimulatorContext() + for _, dsEnt := range sctx.Map.All("Datastore") { + sctx.WithLock( + dsEnt.Reference(), + func() { + ds := sctx.Map.Get(dsEnt.Reference()).(*simulator.Datastore) + ds.Capability.TopLevelDirectoryCreateSupported = ptr.To(false) + ds.Summary.Type = string(vimtypes.HostFileSystemVolumeFileSystemTypeVsan) + }) + } + + // Set required fields for ISO VM creation in the VMIC. + conditions.MarkTrue( + &vmic, + vmopv1.VirtualMachineImageCacheConditionFilesReady) + vmic.Status.Locations = []vmopv1.VirtualMachineImageCacheLocationStatus{ + { + DatacenterID: ctx.Datacenter.Reference().Value, + DatastoreID: ctx.Datastore.Reference().Value, + ProfileID: ctx.StorageProfileID, + Files: []vmopv1.VirtualMachineImageCacheFileStatus{}, + Conditions: []metav1.Condition{ + { + Type: vmopv1.ReadyConditionType, + Status: metav1.ConditionTrue, + }, + }, + }, + } + Expect(ctx.Client.Status().Update(ctx, &vmic)).To(Succeed()) + + libMgr := library.NewManager(ctx.RestClient) + Expect(libMgr.SyncLibraryItem(ctx, &library.Item{ID: ctx.ContentLibraryIsoItemID}, true)).To(Succeed()) + }) + + It("should successfully create the ISO VM in a different UUID-based directory", func() { + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).NotTo(HaveOccurred()) + + var moVM mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &moVM)).To(Succeed()) + + var p object.DatastorePath + p.FromString(moVM.Config.Files.VmPathName) + Expect(p.Datastore).NotTo(BeEmpty()) + + // The VM path should be something like: /test-vm-iso.vmx + // When TopLevelDirectoryCreateSupported is false, + // DatastoreNamespaceManager.CreateDirectory creates a + // new UUID-based directory that is different from the + // VM's Kubernetes UID. + pathParts := strings.Split(p.Path, "/") + Expect(pathParts).To(HaveLen(2)) + _, err = uuid.Parse(pathParts[0]) + Expect(err).NotTo(HaveOccurred(), "expected directory to be a UUID, got: %s", pathParts[0]) + Expect(pathParts[0]).NotTo(Equal(string(vm.UID)), "expected directory to be different from VM's K8s UID") + }) + }) + }) +} diff --git a/pkg/providers/vsphere/vmprovider_vm_metadata_test.go b/pkg/providers/vsphere/vmprovider_vm_metadata_test.go new file mode 100644 index 000000000..d3141c359 --- /dev/null +++ b/pkg/providers/vsphere/vmprovider_vm_metadata_test.go @@ -0,0 +1,200 @@ +// © Broadcom. All Rights Reserved. +// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package vsphere_test + +import ( + "context" + "encoding/json" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha6" + "github.com/vmware-tanzu/vm-operator/pkg/conditions" + pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + ctxop "github.com/vmware-tanzu/vm-operator/pkg/context/operation" + "github.com/vmware-tanzu/vm-operator/pkg/providers" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" + "github.com/vmware-tanzu/vm-operator/pkg/util/kube/cource" + "github.com/vmware-tanzu/vm-operator/pkg/util/ovfcache" + "github.com/vmware-tanzu/vm-operator/test/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/vmware/govmomi/vim25/mo" +) + +func vmMetadataTests() { + var ( + parentCtx context.Context + initObjects []client.Object + testConfig builder.VCSimTestConfig + ctx *builder.TestContextForVCSim + vmProvider providers.VirtualMachineProviderInterface + nsInfo builder.WorkloadNamespaceInfo + + vm *vmopv1.VirtualMachine + vmClass *vmopv1.VirtualMachineClass + + zoneName string + ) + + BeforeEach(func() { + parentCtx = pkgcfg.NewContextWithDefaultConfig() + parentCtx = ctxop.WithContext(parentCtx) + parentCtx = ovfcache.WithContext(parentCtx) + parentCtx = cource.WithContext(parentCtx) + pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { + config.AsyncCreateEnabled = false + config.AsyncSignalEnabled = false + }) + testConfig = builder.VCSimTestConfig{ + WithContentLibrary: true, + } + + vmClass = builder.DummyVirtualMachineClassGenName() + vm = builder.DummyBasicVirtualMachine("test-vm", "") + + if vm.Spec.Network == nil { + vm.Spec.Network = &vmopv1.VirtualMachineNetworkSpec{} + } + vm.Spec.Network.Disabled = true + }) + + JustBeforeEach(func() { + ctx = suite.NewTestContextForVCSimWithParentContext( + parentCtx, testConfig, initObjects...) + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.MaxDeployThreadsOnProvider = 1 + }) + vmProvider = vsphere.NewVSphereVMProviderFromClient( + ctx, ctx.Client, ctx.Recorder) + nsInfo = ctx.CreateWorkloadNamespace() + + vmClass.Namespace = nsInfo.Namespace + Expect(ctx.Client.Create(ctx, vmClass)).To(Succeed()) + + clusterVMI1 := &vmopv1.ClusterVirtualMachineImage{} + + if testConfig.WithContentLibrary { + Expect(ctx.Client.Get( + ctx, client.ObjectKey{Name: ctx.ContentLibraryItem1Name}, + clusterVMI1)).To(Succeed()) + } else { + vsphere.SkipVMImageCLProviderCheck = true + clusterVMI1 = builder.DummyClusterVirtualMachineImage("DC0_C0_RP0_VM0") + Expect(ctx.Client.Create(ctx, clusterVMI1)).To(Succeed()) + conditions.MarkTrue(clusterVMI1, vmopv1.ReadyConditionType) + Expect(ctx.Client.Status().Update(ctx, clusterVMI1)).To(Succeed()) + } + + vm.Namespace = nsInfo.Namespace + vm.Spec.ClassName = vmClass.Name + vm.Spec.ImageName = clusterVMI1.Name + vm.Spec.Image.Kind = cvmiKind + vm.Spec.Image.Name = clusterVMI1.Name + vm.Spec.StorageClass = ctx.StorageClassName + + Expect(ctx.Client.Create(ctx, vm)).To(Succeed()) + + zoneName = ctx.GetFirstZoneName() + vm.Labels[corev1.LabelTopologyZone] = zoneName + Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) + }) + + AfterEach(func() { + vsphere.SkipVMImageCLProviderCheck = false + + if vm != nil && + !pkgcfg.FromContext(ctx).Features.BringYourOwnEncryptionKey { + By("Assert vm.Status.Crypto is nil when BYOK is disabled", func() { + Expect(vm.Status.Crypto).To(BeNil()) + }) + } + + vmClass = nil + vm = nil + + ctx.AfterEach() + ctx = nil + initObjects = nil + vmProvider = nil + nsInfo = builder.WorkloadNamespaceInfo{} + }) + + Context("ExtraConfig Transport", func() { + var ec map[string]interface{} + + JustBeforeEach(func() { + configMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "md-configmap-", + Namespace: vm.Namespace, + }, + Data: map[string]string{ + "foo.bar": "should-be-ignored", + "guestinfo.Foo": "foo", + }, + } + Expect(ctx.Client.Create(ctx, configMap)).To(Succeed()) + + /* + vm.Spec.VmMetadata = &vmopv1.VirtualMachineMetadata{ + ConfigMapName: configMap.Name, + Transport: vmopv1.VirtualMachineMetadataExtraConfigTransport, + } + */ + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + + ec = map[string]interface{}{} + for _, option := range o.Config.ExtraConfig { + if val := option.GetOptionValue(); val != nil { + ec[val.Key] = val.Value.(string) + } + } + }) + + AfterEach(func() { + ec = nil + }) + + // TODO: As is we can't really honor "guestinfo.*" prefix + XIt("Metadata data is included in ExtraConfig", func() { + Expect(ec).ToNot(HaveKey("foo.bar")) + Expect(ec).To(HaveKeyWithValue("guestinfo.Foo", "foo")) + + By("Should include default keys and values", func() { + Expect(ec).To(HaveKeyWithValue("disk.enableUUID", "TRUE")) + Expect(ec).To(HaveKeyWithValue("vmware.tools.gosc.ignoretoolscheck", "TRUE")) + }) + }) + + Context("JSON_EXTRA_CONFIG is specified", func() { + BeforeEach(func() { + b, err := json.Marshal( + struct { + Foo string + Bar string + }{ + Foo: "f00", + Bar: "42", + }, + ) + Expect(err).ToNot(HaveOccurred()) + testConfig.WithJSONExtraConfig = string(b) + }) + + It("Global config is included in ExtraConfig", func() { + Expect(ec).To(HaveKeyWithValue("Foo", "f00")) + Expect(ec).To(HaveKeyWithValue("Bar", "42")) + }) + }) + }) +} diff --git a/pkg/providers/vsphere/vmprovider_vm_misc_test.go b/pkg/providers/vsphere/vmprovider_vm_misc_test.go new file mode 100644 index 000000000..e2ceafcaf --- /dev/null +++ b/pkg/providers/vsphere/vmprovider_vm_misc_test.go @@ -0,0 +1,170 @@ +// © Broadcom. All Rights Reserved. +// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package vsphere_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/vmware/govmomi/vim25/mo" + vimtypes "github.com/vmware/govmomi/vim25/types" + + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha6" + "github.com/vmware-tanzu/vm-operator/pkg/conditions" + pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + ctxop "github.com/vmware-tanzu/vm-operator/pkg/context/operation" + "github.com/vmware-tanzu/vm-operator/pkg/providers" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" + "github.com/vmware-tanzu/vm-operator/pkg/util/kube/cource" + "github.com/vmware-tanzu/vm-operator/pkg/util/ovfcache" + "github.com/vmware-tanzu/vm-operator/test/builder" +) + +func vmMiscTests() { + var ( + parentCtx context.Context + initObjects []client.Object + testConfig builder.VCSimTestConfig + ctx *builder.TestContextForVCSim + vmProvider providers.VirtualMachineProviderInterface + nsInfo builder.WorkloadNamespaceInfo + + vm *vmopv1.VirtualMachine + vmClass *vmopv1.VirtualMachineClass + + zoneName string + ) + + BeforeEach(func() { + parentCtx = pkgcfg.NewContextWithDefaultConfig() + parentCtx = ctxop.WithContext(parentCtx) + parentCtx = ovfcache.WithContext(parentCtx) + parentCtx = cource.WithContext(parentCtx) + pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { + config.AsyncCreateEnabled = false + config.AsyncSignalEnabled = false + }) + testConfig = builder.VCSimTestConfig{ + WithContentLibrary: true, + } + + vmClass = builder.DummyVirtualMachineClassGenName() + vm = builder.DummyBasicVirtualMachine("test-vm", "") + + if vm.Spec.Network == nil { + vm.Spec.Network = &vmopv1.VirtualMachineNetworkSpec{} + } + vm.Spec.Network.Disabled = true + }) + + JustBeforeEach(func() { + ctx = suite.NewTestContextForVCSimWithParentContext( + parentCtx, testConfig, initObjects...) + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.MaxDeployThreadsOnProvider = 1 + }) + vmProvider = vsphere.NewVSphereVMProviderFromClient( + ctx, ctx.Client, ctx.Recorder) + nsInfo = ctx.CreateWorkloadNamespace() + + vmClass.Namespace = nsInfo.Namespace + Expect(ctx.Client.Create(ctx, vmClass)).To(Succeed()) + + clusterVMI1 := &vmopv1.ClusterVirtualMachineImage{} + + if testConfig.WithContentLibrary { + Expect(ctx.Client.Get( + ctx, client.ObjectKey{Name: ctx.ContentLibraryItem1Name}, + clusterVMI1)).To(Succeed()) + } else { + vsphere.SkipVMImageCLProviderCheck = true + clusterVMI1 = builder.DummyClusterVirtualMachineImage("DC0_C0_RP0_VM0") + Expect(ctx.Client.Create(ctx, clusterVMI1)).To(Succeed()) + conditions.MarkTrue(clusterVMI1, vmopv1.ReadyConditionType) + Expect(ctx.Client.Status().Update(ctx, clusterVMI1)).To(Succeed()) + } + + vm.Namespace = nsInfo.Namespace + vm.Spec.ClassName = vmClass.Name + vm.Spec.ImageName = clusterVMI1.Name + vm.Spec.Image.Kind = cvmiKind + vm.Spec.Image.Name = clusterVMI1.Name + vm.Spec.StorageClass = ctx.StorageClassName + + Expect(ctx.Client.Create(ctx, vm)).To(Succeed()) + + zoneName = ctx.GetFirstZoneName() + vm.Labels[corev1.LabelTopologyZone] = zoneName + Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) + }) + + AfterEach(func() { + vsphere.SkipVMImageCLProviderCheck = false + + if vm != nil && + !pkgcfg.FromContext(ctx).Features.BringYourOwnEncryptionKey { + By("Assert vm.Status.Crypto is nil when BYOK is disabled", func() { + Expect(vm.Status.Crypto).To(BeNil()) + }) + } + + vmClass = nil + vm = nil + + ctx.AfterEach() + ctx = nil + initObjects = nil + vmProvider = nil + nsInfo = builder.WorkloadNamespaceInfo{} + }) + + It("Powers VM off", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOn)) + + vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOff + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) + state, err := vcVM.PowerState(ctx) + Expect(err).ToNot(HaveOccurred()) + Expect(state).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOff)) + }) + + It("returns error when StorageClass is required but none specified", func() { + vm.Spec.StorageClass = "" + err := createOrUpdateVM(ctx, vmProvider, vm) + Expect(err).To(MatchError("StorageClass is required but not specified")) + + c := conditions.Get(vm, vmopv1.VirtualMachineConditionStorageReady) + Expect(c).ToNot(BeNil()) + expectedCondition := conditions.FalseCondition( + vmopv1.VirtualMachineConditionStorageReady, + "StorageClassRequired", + "StorageClass is required but not specified") + Expect(*c).To(conditions.MatchCondition(*expectedCondition)) + }) + + It("Can be called multiple times", func() { + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + modified := o.Config.Modified + + _, err = createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + + // Try to assert nothing changed. + Expect(o.Config.Modified).To(Equal(modified)) + }) +} diff --git a/pkg/providers/vsphere/vmprovider_vm_network_test.go b/pkg/providers/vsphere/vmprovider_vm_network_test.go new file mode 100644 index 000000000..3f79c7d44 --- /dev/null +++ b/pkg/providers/vsphere/vmprovider_vm_network_test.go @@ -0,0 +1,1109 @@ +// © Broadcom. All Rights Reserved. +// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package vsphere_test + +import ( + "context" + "fmt" + "regexp" + "sort" + "strconv" + "strings" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/vmware/govmomi/object" + "github.com/vmware/govmomi/vim25/mo" + vimtypes "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" + "sigs.k8s.io/controller-runtime/pkg/client" + + netopv1alpha1 "github.com/vmware-tanzu/net-operator-api/api/v1alpha1" + vpcv1alpha1 "github.com/vmware-tanzu/nsx-operator/pkg/apis/vpc/v1alpha1" + ncpv1alpha1 "github.com/vmware-tanzu/vm-operator/external/ncp/api/v1alpha1" + + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha6" + vmopv1common "github.com/vmware-tanzu/vm-operator/api/v1alpha6/common" + "github.com/vmware-tanzu/vm-operator/pkg/conditions" + pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + ctxop "github.com/vmware-tanzu/vm-operator/pkg/context/operation" + "github.com/vmware-tanzu/vm-operator/pkg/providers" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/network" + "github.com/vmware-tanzu/vm-operator/pkg/util" + "github.com/vmware-tanzu/vm-operator/pkg/util/kube/cource" + "github.com/vmware-tanzu/vm-operator/pkg/util/ovfcache" + "github.com/vmware-tanzu/vm-operator/pkg/util/ptr" + "github.com/vmware-tanzu/vm-operator/test/builder" +) + +func vmNetworkTests() { + + const ( + subnetKind = "Subnet" + subnetAPIVersion = "crd.nsx.vmware.com/v1alpha1" + + networkName0 = "my-network-0" + interfaceName0 = "eth0" + networkName1 = "my-network-1" + interfaceName1 = "eth1" + networkName2 = "my-network-2" + + bsCloudInit = "cloudInit" + bsSysprep = "sysPrep" + bsLinuxPrep = "linuxPrep" + ) + + var ( + parentCtx context.Context + initObjects []client.Object + testConfig builder.VCSimTestConfig + ctx *builder.TestContextForVCSim + vmProvider providers.VirtualMachineProviderInterface + nsInfo builder.WorkloadNamespaceInfo + + vm *vmopv1.VirtualMachine + vmClass *vmopv1.VirtualMachineClass + + zoneName string + ) + + BeforeEach(func() { + parentCtx = pkgcfg.NewContextWithDefaultConfig() + parentCtx = ctxop.WithContext(parentCtx) + parentCtx = ovfcache.WithContext(parentCtx) + parentCtx = cource.WithContext(parentCtx) + pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { + config.AsyncCreateEnabled = false + config.AsyncSignalEnabled = false + config.MaxDeployThreadsOnProvider = 1 + }) + testConfig = builder.VCSimTestConfig{ + WithContentLibrary: true, + } + + vmClass = builder.DummyVirtualMachineClassGenName() + vm = builder.DummyBasicVirtualMachine("test-vm", "") + + if vm.Spec.Network == nil { + vm.Spec.Network = &vmopv1.VirtualMachineNetworkSpec{} + } + vm.Spec.Network.Disabled = false + }) + + JustBeforeEach(func() { + ctx = suite.NewTestContextForVCSimWithParentContext( + parentCtx, testConfig, initObjects...) + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.MaxDeployThreadsOnProvider = 1 + }) + vmProvider = vsphere.NewVSphereVMProviderFromClient( + ctx, ctx.Client, ctx.Recorder) + nsInfo = ctx.CreateWorkloadNamespace() + + vmClass.Namespace = nsInfo.Namespace + Expect(ctx.Client.Create(ctx, vmClass)).To(Succeed()) + + clusterVMI1 := &vmopv1.ClusterVirtualMachineImage{} + + if testConfig.WithContentLibrary { + Expect(ctx.Client.Get( + ctx, client.ObjectKey{Name: ctx.ContentLibraryItem1Name}, + clusterVMI1)).To(Succeed()) + } else { + vsphere.SkipVMImageCLProviderCheck = true + clusterVMI1 = builder.DummyClusterVirtualMachineImage("DC0_C0_RP0_VM0") + Expect(ctx.Client.Create(ctx, clusterVMI1)).To(Succeed()) + conditions.MarkTrue(clusterVMI1, vmopv1.ReadyConditionType) + Expect(ctx.Client.Status().Update(ctx, clusterVMI1)).To(Succeed()) + } + + vm.Namespace = nsInfo.Namespace + vm.Spec.ClassName = vmClass.Name + vm.Spec.ImageName = clusterVMI1.Name + vm.Spec.Image.Kind = cvmiKind + vm.Spec.Image.Name = clusterVMI1.Name + vm.Spec.StorageClass = ctx.StorageClassName + + Expect(ctx.Client.Create(ctx, vm)).To(Succeed()) + + zoneName = ctx.GetFirstZoneName() + vm.Labels[corev1.LabelTopologyZone] = zoneName + Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) + }) + + AfterEach(func() { + vsphere.SkipVMImageCLProviderCheck = false + + if vm != nil && + !pkgcfg.FromContext(ctx).Features.BringYourOwnEncryptionKey { + By("Assert vm.Status.Crypto is nil when BYOK is disabled", func() { + Expect(vm.Status.Crypto).To(BeNil()) + }) + } + + vmClass = nil + vm = nil + + ctx.AfterEach() + ctx = nil + initObjects = nil + vmProvider = nil + nsInfo = builder.WorkloadNamespaceInfo{} + }) + + When("spec.network.disabled is true", func() { + BeforeEach(func() { + vm.Spec.Network.Disabled = true + }) + + It("should not have a nic", func() { + Expect(vm.Spec.Network.Disabled).To(BeTrue()) + + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + Expect(conditions.Has(vm, vmopv1.VirtualMachineConditionNetworkReady)).To(BeFalse()) + + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + + devList := object.VirtualDeviceList(o.Config.Hardware.Device) + l := devList.SelectByType(&vimtypes.VirtualEthernetCard{}) + Expect(l).To(BeEmpty()) + }) + }) + + When("multiple NICs are specified", func() { + BeforeEach(func() { + testConfig.WithNetworkEnv = builder.NetworkEnvNamed + + vm.Spec.Network.Interfaces = []vmopv1.VirtualMachineNetworkInterfaceSpec{ + { + Name: "eth0", + Network: &vmopv1common.PartialObjectRef{Name: "VM Network"}, + }, + { + Name: "eth1", + Network: &vmopv1common.PartialObjectRef{Name: dvpgName}, + }, + } + }) + + It("Has expected devices", func() { + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionNetworkReady)).To(BeTrue()) + + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + + devList := object.VirtualDeviceList(o.Config.Hardware.Device) + l := devList.SelectByType(&vimtypes.VirtualEthernetCard{}) + Expect(l).To(HaveLen(2)) + + dev1 := l[0].GetVirtualDevice() + backing1, ok := dev1.Backing.(*vimtypes.VirtualEthernetCardNetworkBackingInfo) + Expect(ok).Should(BeTrue()) + Expect(backing1.DeviceName).To(Equal("VM Network")) + + dev2 := l[1].GetVirtualDevice() + backing2, ok := dev2.Backing.(*vimtypes.VirtualEthernetCardDistributedVirtualPortBackingInfo) + Expect(ok).Should(BeTrue()) + _, dvpg := getDVPG(ctx, dvpgName) + Expect(backing2.Port.PortgroupKey).To(Equal(dvpg.Reference().Value)) + }) + }) + + Context("simulate", func() { + var ( + cloudInitSecret *corev1.Secret + sysprepSecret *corev1.Secret + ) + + BeforeEach(func() { + testConfig.NumNetworks = 3 + + // Speed up tests until we Watch the network interface types. Sigh. + network.RetryTimeout = 1 * time.Millisecond + + pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { + config.Features.MutableNetworks = true + }) + }) + + JustBeforeEach(func() { + if vm.Spec.Network != nil && + len(vm.Spec.Network.Interfaces) > 0 { + + vm.Spec.Network.Interfaces[0].Nameservers = []string{"1.1.1.1", "8.8.8.8"} + vm.Spec.Network.Interfaces[0].SearchDomains = []string{"vmware.local"} + } + + cloudInitSecret = &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-cloud-init-secret", + Namespace: nsInfo.Namespace, + }, + Data: map[string][]byte{ + "user-value": []byte(""), + }, + } + + sysprepSecret = &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-sysprep-secret", + Namespace: nsInfo.Namespace, + }, + Data: map[string][]byte{ + "unattend": []byte("foo"), + }, + } + }) + + AfterEach(func() { + cloudInitSecret = nil + sysprepSecret = nil + }) + + DescribeTableSubtree("simulate vpc backup/restore", + func(powerState vmopv1.VirtualMachinePowerState, newLSUUID bool) { + var np fakeNetworkProvider + + BeforeEach(func() { + testConfig.WithNetworkEnv = builder.NetworkEnvVPC + np = vpcNetworkProvider{} + + vm.Spec.Network = &vmopv1.VirtualMachineNetworkSpec{ + Interfaces: []vmopv1.VirtualMachineNetworkInterfaceSpec{ + { + Name: interfaceName0, + Network: &vmopv1common.PartialObjectRef{ + Name: networkName0, + }, + }, + { + Name: interfaceName1, + Network: &vmopv1common.PartialObjectRef{ + Name: networkName1, + }, + }, + }, + } + + for i := range vm.Spec.Network.Interfaces { + vm.Spec.Network.Interfaces[i].Network.Kind = subnetKind + vm.Spec.Network.Interfaces[i].Network.APIVersion = subnetAPIVersion + } + }) + + It("should succeed", func() { + err := createOrUpdateVM(ctx, vmProvider, vm) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("network interface is not ready yet")) + Expect(conditions.IsFalse(vm, vmopv1.VirtualMachineConditionNetworkReady)).To(BeTrue()) + + By("simulate successful network provider reconcile", func() { + np.simulateInterfaceReconcile(ctx, vm, vm.Spec.Network.Interfaces[0], 0) + }) + + { + // TODO: We should create all the interface CRs up front. + err := createOrUpdateVM(ctx, vmProvider, vm) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("network interface is not ready yet")) + Expect(conditions.IsFalse(vm, vmopv1.VirtualMachineConditionNetworkReady)).To(BeTrue()) + } + + By("simulate successful network provider reconcile", func() { + np.simulateInterfaceReconcile(ctx, vm, vm.Spec.Network.Interfaces[1], 1) + }) + + vm.Spec.PowerState = powerState + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + + Expect(vm.Status.UniqueID).ToNot(BeEmpty()) + vcVM := ctx.GetVMFromMoID(vm.Status.UniqueID) + + By("created with expected NIC device types and backings", func() { + devList, err := vcVM.Device(ctx) + Expect(err).ToNot(HaveOccurred()) + l := devList.SelectByType(&vimtypes.VirtualEthernetCard{}) + Expect(l).To(HaveLen(2)) + + dev0 := l[0] + _, ok := dev0.(*vimtypes.VirtualVmxnet3) + Expect(ok).To(BeTrue()) + np.assertEthernetCard(ctx, dev0, vm.Spec.Network.Interfaces[0], 0) + + dev1 := l[1] + _, ok = dev1.(*vimtypes.VirtualVmxnet3) + Expect(ok).To(BeTrue()) + np.assertEthernetCard(ctx, dev1, vm.Spec.Network.Interfaces[1], 1) + }) + + By("Verify VM power state", func() { + ps, err := vcVM.PowerState(ctx) + Expect(err).ToNot(HaveOccurred()) + if powerState == vmopv1.VirtualMachinePowerStateOff { + Expect(ps).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOff)) + } else { + Expect(ps).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOn)) + } + }) + + restoredNetworkIdx := 0 + if newLSUUID { + // Restore can have two behaviors: if the network existed before the backup, + // then we expect just a new ExtID. If the network was created between backup + // and restore, we expect both a new ExtID and LSUUID. Use the third network + // to simulate the restore creating a new network. + restoredNetworkIdx = 2 + } + restoredExtID := "" + + By("simulate VPC restore", func() { + interfaceSpec := vm.Spec.Network.Interfaces[0] + interfaceName, networkName := interfaceSpec.Name, interfaceSpec.Network.Name + + subnetPort := &vpcv1alpha1.SubnetPort{} + objKey := client.ObjectKey{ + Name: network.VPCCRName(vm.Name, networkName, interfaceName), + Namespace: vm.Namespace, + } + Expect(ctx.Client.Get(ctx, objKey, subnetPort)).To(Succeed()) + + subnetPort.Status.Attachment.ID += "-restored" + subnetPort.Status.NetworkInterfaceConfig.LogicalSwitchUUID = builder.GetVPCTLogicalSwitchUUID(restoredNetworkIdx) + Expect(ctx.Client.Status().Update(ctx, subnetPort)).To(Succeed()) + + restoredExtID = subnetPort.Status.Attachment.ID + }) + + vm.Annotations[network.VPCInterfaceRestoredAnnotation] = interfaceName0 + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Annotations).ToNot(HaveKey(network.VPCInterfaceRestoredAnnotation)) + + By("restore NIC with expected device types and backings", func() { + devList, err := vcVM.Device(ctx) + Expect(err).ToNot(HaveOccurred()) + l := devList.SelectByType(&vimtypes.VirtualEthernetCard{}) + Expect(l).To(HaveLen(2)) + + // Sometimes even an edit operation will cause vcsim to reorder the devices. + sort.Slice(l, func(i, j int) bool { + return l[i].GetVirtualDevice().Key < l[j].GetVirtualDevice().Key + }) + + dev0 := l[0] + ethCard, ok := dev0.(*vimtypes.VirtualVmxnet3) + Expect(ok).To(BeTrue()) + { + // Assert here that the interface has its expected new ExtID, but then + // restore the original one so we can use assertEthernetCard() as-is. + Expect(ethCard.ExternalId).To(Equal(restoredExtID)) + ethCard.ExternalId = strings.TrimSuffix(ethCard.ExternalId, "-restored") + } + np.assertEthernetCard(ctx, dev0, vm.Spec.Network.Interfaces[0], restoredNetworkIdx) + + dev1 := l[1] + _, ok = dev1.(*vimtypes.VirtualVmxnet3) + Expect(ok).To(BeTrue()) + np.assertEthernetCard(ctx, dev1, vm.Spec.Network.Interfaces[1], 1) + }) + }) + }, + Entry("PoweredOn - Same Network", vmopv1.VirtualMachinePowerStateOn, false), + Entry("PoweredOn - New Network", vmopv1.VirtualMachinePowerStateOn, true), + Entry("PoweredOff - Same Network", vmopv1.VirtualMachinePowerStateOff, false), + Entry("PoweredOff - New Network", vmopv1.VirtualMachinePowerStateOff, true), + ) + + Context("simulate vm power on/off", func() { + DescribeTableSubtree("with adding/removing network interfaces ", + func(networkEnv builder.NetworkEnv, bootstrap string) { + var np fakeNetworkProvider + + BeforeEach(func() { + testConfig.WithNetworkEnv = networkEnv + + switch networkEnv { + case builder.NetworkEnvVDS: + np = vdsNetworkProvider{} + case builder.NetworkEnvNSXT: + np = nsxtNetworkProvider{} + case builder.NetworkEnvVPC: + np = vpcNetworkProvider{} + } + }) + + JustBeforeEach(func() { + switch bootstrap { + case bsCloudInit: + Expect(ctx.Client.Create(ctx, cloudInitSecret)).To(Succeed()) + vm.Spec.Bootstrap = &vmopv1.VirtualMachineBootstrapSpec{ + CloudInit: &vmopv1.VirtualMachineBootstrapCloudInitSpec{ + RawCloudConfig: &vmopv1common.SecretKeySelector{ + Name: cloudInitSecret.Name, + }, + }, + } + case bsSysprep: + Expect(ctx.Client.Create(ctx, sysprepSecret)).To(Succeed()) + vm.Spec.Bootstrap = &vmopv1.VirtualMachineBootstrapSpec{ + Sysprep: &vmopv1.VirtualMachineBootstrapSysprepSpec{ + RawSysprep: &vmopv1common.SecretKeySelector{ + Name: sysprepSecret.Name, + Key: "unattend", + }, + }, + } + case bsLinuxPrep: + vm.Spec.Bootstrap = &vmopv1.VirtualMachineBootstrapSpec{ + LinuxPrep: &vmopv1.VirtualMachineBootstrapLinuxPrepSpec{}, + } + } + + vm.Spec.Network = &vmopv1.VirtualMachineNetworkSpec{ + Interfaces: []vmopv1.VirtualMachineNetworkInterfaceSpec{ + { + Name: interfaceName0, + Network: &vmopv1common.PartialObjectRef{ + Name: networkName0, + }, + }, + }, + } + + if networkEnv == builder.NetworkEnvVPC { + vm.Spec.Network.Interfaces[0].Network.Kind = subnetKind + vm.Spec.Network.Interfaces[0].Network.APIVersion = subnetAPIVersion + } + }) + + It("should succeed", func() { + err := createOrUpdateVM(ctx, vmProvider, vm) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("network interface is not ready yet")) + Expect(conditions.IsFalse(vm, vmopv1.VirtualMachineConditionNetworkReady)).To(BeTrue()) + + By("simulate successful network provider reconcile", func() { + np.simulateInterfaceReconcile(ctx, vm, vm.Spec.Network.Interfaces[0], 0) + }) + + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + + By("has expected conditions", func() { + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionClassReady)).To(BeTrue()) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionImageReady)).To(BeTrue()) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionStorageReady)).To(BeTrue()) + if bootstrap != "" { + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionBootstrapReady)).To(BeTrue()) + } else { + Expect(conditions.Get(vm, vmopv1.VirtualMachineConditionBootstrapReady)).To(BeNil()) + } + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionNetworkReady)).To(BeTrue()) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionPlacementReady)).To(BeTrue()) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionCreated)).To(BeTrue()) + }) + + Expect(vm.Status.UniqueID).ToNot(BeEmpty()) + vcVM := ctx.GetVMFromMoID(vm.Status.UniqueID) + + By("has expected NIC backing", func() { + devList, err := vcVM.Device(ctx) + Expect(err).ToNot(HaveOccurred()) + l := devList.SelectByType(&vimtypes.VirtualEthernetCard{}) + Expect(l).To(HaveLen(1)) + + dev0 := l[0] + np.assertEthernetCard(ctx, dev0, vm.Spec.Network.Interfaces[0], 0) + }) + + By("add network interface", func() { + vm.Spec.Network.Interfaces = append(vm.Spec.Network.Interfaces, vm.Spec.Network.Interfaces[0]) + vm.Spec.Network.Interfaces[1].Name = interfaceName1 + vm.Spec.Network.Interfaces[1].Network = ptr.To(*vm.Spec.Network.Interfaces[1].Network) + vm.Spec.Network.Interfaces[1].Network.Name = networkName1 + }) + + By("power off VM", func() { + vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOff + err = createOrUpdateVM(ctx, vmProvider, vm) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("network interface is not ready yet")) + Expect(vcVM.PowerState(ctx)).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOff)) + }) + + By("simulate successful network provider reconcile on added interface", func() { + np.simulateInterfaceReconcile(ctx, vm, vm.Spec.Network.Interfaces[1], 1) + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + }) + + By("power on VM", func() { + vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOn + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vcVM.PowerState(ctx)).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOn)) + }) + + By("Added interface has expected NIC backing", func() { + devList, err := vcVM.Device(ctx) + Expect(err).ToNot(HaveOccurred()) + l := devList.SelectByType(&vimtypes.VirtualEthernetCard{}) + Expect(l).To(HaveLen(2)) + + dev1 := l[1] + np.assertEthernetCard(ctx, dev1, vm.Spec.Network.Interfaces[1], 1) + }) + + By("remove just added network interface", func() { + vm.Spec.Network.Interfaces = vm.Spec.Network.Interfaces[:1] + }) + + By("power off and on VM", func() { + vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOff + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vcVM.PowerState(ctx)).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOff)) + + vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOn + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vcVM.PowerState(ctx)).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOn)) + }) + + By("interface has been removed", func() { + devList, err := vcVM.Device(ctx) + Expect(err).ToNot(HaveOccurred()) + l := devList.SelectByType(&vimtypes.VirtualEthernetCard{}) + Expect(l).To(HaveLen(1)) + + dev0 := l[0] + np.assertEthernetCard(ctx, dev0, vm.Spec.Network.Interfaces[0], 0) + + By("network interface has been deleted", func() { + np.assertNetworkInterfacesDNE(ctx, vm, networkName1, interfaceName1) + }) + }) + }) + }, + Entry("VDS with CloudInit", builder.NetworkEnvVDS, bsCloudInit), + Entry("NSX-T with CloudInit", builder.NetworkEnvNSXT, bsCloudInit), + Entry("VPC with Sysprep", builder.NetworkEnvVPC, bsSysprep), + ) + + DescribeTableSubtree("with modifying network interface", + func(networkEnv builder.NetworkEnv, bootstrap string) { + var np fakeNetworkProvider + + BeforeEach(func() { + testConfig.WithNetworkEnv = networkEnv + + switch networkEnv { + case builder.NetworkEnvVDS: + np = vdsNetworkProvider{} + case builder.NetworkEnvNSXT: + np = nsxtNetworkProvider{} + case builder.NetworkEnvVPC: + np = vpcNetworkProvider{} + } + + // We assert the device type is preserved when editing an interface spec. + configSpec := vimtypes.VirtualMachineConfigSpec{ + DeviceChange: []vimtypes.BaseVirtualDeviceConfigSpec{ + &vimtypes.VirtualDeviceConfigSpec{ + Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, + Device: &vimtypes.VirtualE1000e{}, + }, + &vimtypes.VirtualDeviceConfigSpec{ + Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, + Device: &vimtypes.VirtualVmxnet2{}, + }, + }, + } + + jsonConfigSpec, err := util.MarshalConfigSpecToJSON(configSpec) + Expect(err).ToNot(HaveOccurred()) + vmClass.Spec.ConfigSpec = jsonConfigSpec + }) + + JustBeforeEach(func() { + switch bootstrap { + case bsCloudInit: + Expect(ctx.Client.Create(ctx, cloudInitSecret)).To(Succeed()) + vm.Spec.Bootstrap = &vmopv1.VirtualMachineBootstrapSpec{ + CloudInit: &vmopv1.VirtualMachineBootstrapCloudInitSpec{ + RawCloudConfig: &vmopv1common.SecretKeySelector{ + Name: cloudInitSecret.Name, + }, + }, + } + case bsSysprep: + Expect(ctx.Client.Create(ctx, sysprepSecret)).To(Succeed()) + vm.Spec.Bootstrap = &vmopv1.VirtualMachineBootstrapSpec{ + Sysprep: &vmopv1.VirtualMachineBootstrapSysprepSpec{ + RawSysprep: &vmopv1common.SecretKeySelector{ + Name: sysprepSecret.Name, + Key: "unattend", + }, + }, + } + case bsLinuxPrep: + vm.Spec.Bootstrap = &vmopv1.VirtualMachineBootstrapSpec{ + LinuxPrep: &vmopv1.VirtualMachineBootstrapLinuxPrepSpec{}, + } + } + + vm.Spec.Network = &vmopv1.VirtualMachineNetworkSpec{ + Interfaces: []vmopv1.VirtualMachineNetworkInterfaceSpec{ + { + Name: interfaceName0, + Network: &vmopv1common.PartialObjectRef{ + Name: networkName0, + }, + }, + { + Name: interfaceName1, + Network: &vmopv1common.PartialObjectRef{ + Name: networkName1, + }, + }, + }, + } + + if networkEnv == builder.NetworkEnvVPC { + for i := range vm.Spec.Network.Interfaces { + vm.Spec.Network.Interfaces[i].Network.Kind = subnetKind + vm.Spec.Network.Interfaces[i].Network.APIVersion = subnetAPIVersion + } + } + }) + + It("should succeed", func() { + err := createOrUpdateVM(ctx, vmProvider, vm) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("network interface is not ready yet")) + Expect(conditions.IsFalse(vm, vmopv1.VirtualMachineConditionNetworkReady)).To(BeTrue()) + + By("simulate successful network provider reconcile", func() { + np.simulateInterfaceReconcile(ctx, vm, vm.Spec.Network.Interfaces[0], 0) + }) + + { + // TODO: We should create all the interface CRs up front. + err := createOrUpdateVM(ctx, vmProvider, vm) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("network interface is not ready yet")) + Expect(conditions.IsFalse(vm, vmopv1.VirtualMachineConditionNetworkReady)).To(BeTrue()) + } + + By("simulate successful network provider reconcile", func() { + np.simulateInterfaceReconcile(ctx, vm, vm.Spec.Network.Interfaces[1], 1) + }) + + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + + By("has expected conditions", func() { + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionClassReady)).To(BeTrue()) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionImageReady)).To(BeTrue()) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionStorageReady)).To(BeTrue()) + if bootstrap != "" { + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionBootstrapReady)).To(BeTrue()) + } else { + Expect(conditions.Get(vm, vmopv1.VirtualMachineConditionBootstrapReady)).To(BeNil()) + } + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionNetworkReady)).To(BeTrue()) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionPlacementReady)).To(BeTrue()) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionCreated)).To(BeTrue()) + }) + + Expect(vm.Status.UniqueID).ToNot(BeEmpty()) + vcVM := ctx.GetVMFromMoID(vm.Status.UniqueID) + + By("created with expected NIC device types and backings", func() { + devList, err := vcVM.Device(ctx) + Expect(err).ToNot(HaveOccurred()) + l := devList.SelectByType(&vimtypes.VirtualEthernetCard{}) + Expect(l).To(HaveLen(2)) + + dev0 := l[0] + _, ok := dev0.(*vimtypes.VirtualE1000e) + Expect(ok).To(BeTrue()) + np.assertEthernetCard(ctx, dev0, vm.Spec.Network.Interfaces[0], 0) + + dev1 := l[1] + _, ok = dev1.(*vimtypes.VirtualVmxnet2) + Expect(ok).To(BeTrue()) + np.assertEthernetCard(ctx, dev1, vm.Spec.Network.Interfaces[1], 1) + }) + + By("edit second network interface to use different network", func() { + vm.Spec.Network.Interfaces[1].Network.Name = networkName2 + }) + + By("power off VM", func() { + vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOff + err = createOrUpdateVM(ctx, vmProvider, vm) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("network interface is not ready yet")) + Expect(vcVM.PowerState(ctx)).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOff)) + }) + + By("simulate successful network provider reconcile on updated interface", func() { + np.simulateInterfaceReconcile(ctx, vm, vm.Spec.Network.Interfaces[1], 2) + }) + + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + + By("network interface has been deleted", func() { + np.assertNetworkInterfacesDNE(ctx, vm, networkName1, interfaceName1) + }) + + By("powered off VM has expected NIC device types and backings", func() { + devList, err := vcVM.Device(ctx) + Expect(err).ToNot(HaveOccurred()) + l := devList.SelectByType(&vimtypes.VirtualEthernetCard{}) + Expect(l).To(HaveLen(2)) + + // Sometimes even an edit operation will cause vcsim to reorder the devices. + sort.Slice(l, func(i, j int) bool { + return l[i].GetVirtualDevice().Key < l[j].GetVirtualDevice().Key + }) + + dev0 := l[0] + _, ok := dev0.(*vimtypes.VirtualE1000e) + Expect(ok).To(BeTrue()) + np.assertEthernetCard(ctx, dev0, vm.Spec.Network.Interfaces[0], 0) + + dev1 := l[1] + _, ok = dev1.(*vimtypes.VirtualVmxnet2) + Expect(ok).To(BeTrue()) + np.assertEthernetCard(ctx, dev1, vm.Spec.Network.Interfaces[1], 2) + }) + + By("power on VM", func() { + vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOn + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vcVM.PowerState(ctx)).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOn)) + }) + + By("still has expected NIC device types and backings", func() { + devList, err := vcVM.Device(ctx) + Expect(err).ToNot(HaveOccurred()) + l := devList.SelectByType(&vimtypes.VirtualEthernetCard{}) + Expect(l).To(HaveLen(2)) + + sort.Slice(l, func(i, j int) bool { + return l[i].GetVirtualDevice().Key < l[j].GetVirtualDevice().Key + }) + + dev0 := l[0] + _, ok := dev0.(*vimtypes.VirtualE1000e) + Expect(ok).To(BeTrue()) + np.assertEthernetCard(ctx, dev0, vm.Spec.Network.Interfaces[0], 0) + + dev1 := l[1] + _, ok = dev1.(*vimtypes.VirtualVmxnet2) + Expect(ok).To(BeTrue()) + np.assertEthernetCard(ctx, dev1, vm.Spec.Network.Interfaces[1], 2) + }) + + By("power off VM", func() { + vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOff + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vcVM.PowerState(ctx)).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOff)) + }) + + By("update first network interface to use different network", func() { + vm.Spec.Network.Interfaces[0].Network.Name = networkName2 + }) + + By("try to power on VM", func() { + vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOn + err = createOrUpdateVM(ctx, vmProvider, vm) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("network interface is not ready yet")) + Expect(vcVM.PowerState(ctx)).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOff)) + }) + + By("simulate successful network provider reconcile on updated interface", func() { + np.simulateInterfaceReconcile(ctx, vm, vm.Spec.Network.Interfaces[0], 2) + }) + + By("power on VM", func() { + err := createOrUpdateVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + Expect(vcVM.PowerState(ctx)).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOn)) + }) + + By("has expected NIC device types and backings", func() { + devList, err := vcVM.Device(ctx) + Expect(err).ToNot(HaveOccurred()) + l := devList.SelectByType(&vimtypes.VirtualEthernetCard{}) + Expect(l).To(HaveLen(2)) + + sort.Slice(l, func(i, j int) bool { + return l[i].GetVirtualDevice().Key < l[j].GetVirtualDevice().Key + }) + + dev0 := l[0] + _, ok := dev0.(*vimtypes.VirtualE1000e) + Expect(ok).To(BeTrue()) + np.assertEthernetCard(ctx, dev0, vm.Spec.Network.Interfaces[0], 2) + + dev1 := l[1] + _, ok = dev1.(*vimtypes.VirtualVmxnet2) + Expect(ok).To(BeTrue()) + np.assertEthernetCard(ctx, dev1, vm.Spec.Network.Interfaces[1], 2) + }) + }) + }, + Entry("VDS with CloudInit", builder.NetworkEnvVDS, bsCloudInit), + Entry("VPC with CloudInit", builder.NetworkEnvVPC, bsCloudInit), + Entry("VDS with LinuxPrep", builder.NetworkEnvVDS, bsLinuxPrep), + Entry("NSX-T with Sysprep", builder.NetworkEnvNSXT, bsSysprep), + ) + }) + }) +} + +type fakeNetworkProvider interface { + simulateInterfaceReconcile(ctx *builder.TestContextForVCSim, vm *vmopv1.VirtualMachine, interfaceSpec vmopv1.VirtualMachineNetworkInterfaceSpec, networkIdx int) + assertEthernetCard(ctx *builder.TestContextForVCSim, dev vimtypes.BaseVirtualDevice, interfaceSpec vmopv1.VirtualMachineNetworkInterfaceSpec, networkIdx int) + assertNetworkInterfacesDNE(ctx *builder.TestContextForVCSim, vm *vmopv1.VirtualMachine, networkName, interfaceName string) +} + +func extID(networkName, interfaceName string) string { + return networkName + "-" + interfaceName +} + +var ifaceIdxRegex = regexp.MustCompile(`(\d+)$`) + +func idxFromInterfaceName(s string) int { + m := ifaceIdxRegex.FindStringSubmatch(s) + Expect(m).To(HaveLen(2)) + i, _ := strconv.ParseInt(m[1], 10, 32) + return int(i) +} + +type vdsNetworkProvider struct{} + +func (v vdsNetworkProvider) simulateInterfaceReconcile( + ctx *builder.TestContextForVCSim, + vm *vmopv1.VirtualMachine, + interfaceSpec vmopv1.VirtualMachineNetworkInterfaceSpec, + networkIdx int) { + + interfaceName, networkName := interfaceSpec.Name, interfaceSpec.Network.Name + ifaceIdx := idxFromInterfaceName(interfaceName) + + netInterface := &netopv1alpha1.NetworkInterface{ + ObjectMeta: metav1.ObjectMeta{ + Name: network.NetOPCRName(vm.Name, networkName, interfaceName, false), + Namespace: vm.Namespace, + }, + } + Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(netInterface), netInterface)).To(Succeed()) + Expect(netInterface.Spec.NetworkName).To(Equal(networkName)) + + netInterface.Status.NetworkID = ctx.NetworkRefs[networkIdx].Reference().Value + netInterface.Status.ExternalID = extID(networkName, interfaceName) + netInterface.Status.MacAddress = "" // NetOP doesn't set this. + netInterface.Status.IPConfigs = []netopv1alpha1.IPConfig{ + { + IP: fmt.Sprintf("192.168.1.2%d", ifaceIdx), + IPFamily: corev1.IPv4Protocol, + Gateway: "192.168.1.1", + SubnetMask: "255.255.255.0", + }, + } + netInterface.Status.Conditions = []netopv1alpha1.NetworkInterfaceCondition{ + { + Type: netopv1alpha1.NetworkInterfaceReady, + Status: corev1.ConditionTrue, + }, + } + Expect(ctx.Client.Status().Update(ctx, netInterface)).To(Succeed()) +} + +func (v vdsNetworkProvider) assertEthernetCard( + ctx *builder.TestContextForVCSim, + dev vimtypes.BaseVirtualDevice, + interfaceSpec vmopv1.VirtualMachineNetworkInterfaceSpec, + networkIdx int) { + + interfaceName, networkName := interfaceSpec.Name, interfaceSpec.Network.Name + + ethCard := dev.(vimtypes.BaseVirtualEthernetCard).GetVirtualEthernetCard() + backingInfo, ok := ethCard.Backing.(*vimtypes.VirtualEthernetCardDistributedVirtualPortBackingInfo) + Expect(ok).Should(BeTrue()) + ExpectWithOffset(1, backingInfo.Port.PortgroupKey).To(Equal(ctx.NetworkRefs[networkIdx].Reference().Value)) + Expect(ethCard.MacAddress).ToNot(BeEmpty()) + Expect(ethCard.ExternalId).To(Equal(extID(networkName, interfaceName))) + +} + +func (v vdsNetworkProvider) assertNetworkInterfacesDNE( + ctx *builder.TestContextForVCSim, + vm *vmopv1.VirtualMachine, + networkName, interfaceName string) { + + netInterface := &netopv1alpha1.NetworkInterface{ + ObjectMeta: metav1.ObjectMeta{ + Name: network.NetOPCRName(vm.Name, networkName, interfaceName, false), + Namespace: vm.Namespace, + }, + } + err := ctx.Client.Get(ctx, client.ObjectKeyFromObject(netInterface), netInterface) + Expect(err).To(HaveOccurred()) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) +} + +type nsxtNetworkProvider struct{} + +func (n nsxtNetworkProvider) simulateInterfaceReconcile( + ctx *builder.TestContextForVCSim, + vm *vmopv1.VirtualMachine, + interfaceSpec vmopv1.VirtualMachineNetworkInterfaceSpec, + networkIdx int) { + + interfaceName, networkName := interfaceSpec.Name, interfaceSpec.Network.Name + ifaceIdx := idxFromInterfaceName(interfaceName) + + netInterface := &ncpv1alpha1.VirtualNetworkInterface{ + ObjectMeta: metav1.ObjectMeta{ + Name: network.NCPCRName(vm.Name, networkName, interfaceName, false), + Namespace: vm.Namespace, + }, + } + Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(netInterface), netInterface)).To(Succeed()) + Expect(netInterface.Spec.VirtualNetwork).To(Equal(networkName)) + + netInterface.Status.InterfaceID = extID(networkName, interfaceName) + netInterface.Status.MacAddress = fmt.Sprintf("01-23-45-67-89-%02X", ifaceIdx) + netInterface.Status.ProviderStatus = &ncpv1alpha1.VirtualNetworkInterfaceProviderStatus{ + NsxLogicalSwitchID: builder.GetNsxTLogicalSwitchUUID(networkIdx), + } + netInterface.Status.IPAddresses = []ncpv1alpha1.VirtualNetworkInterfaceIP{ + { + IP: fmt.Sprintf("192.168.1.2%d", ifaceIdx), + Gateway: "192.168.1.1", + SubnetMask: "255.255.255.0", + }, + } + netInterface.Status.Conditions = []ncpv1alpha1.VirtualNetworkCondition{ + { + Type: "Ready", + Status: "True", + }, + } + Expect(ctx.Client.Status().Update(ctx, netInterface)).To(Succeed()) +} + +func (n nsxtNetworkProvider) assertEthernetCard( + ctx *builder.TestContextForVCSim, + dev vimtypes.BaseVirtualDevice, + interfaceSpec vmopv1.VirtualMachineNetworkInterfaceSpec, + networkIdx int) { + + interfaceName, networkName := interfaceSpec.Name, interfaceSpec.Network.Name + ifaceIdx := idxFromInterfaceName(interfaceName) + + ethCard := dev.(vimtypes.BaseVirtualEthernetCard).GetVirtualEthernetCard() + backingInfo, ok := ethCard.Backing.(*vimtypes.VirtualEthernetCardDistributedVirtualPortBackingInfo) + Expect(ok).Should(BeTrue()) + Expect(backingInfo.Port.PortgroupKey).To(Equal(ctx.NetworkRefs[networkIdx].Reference().Value)) + Expect(ethCard.MacAddress).To(Equal(fmt.Sprintf("01-23-45-67-89-%02X", ifaceIdx))) + Expect(ethCard.ExternalId).To(Equal(extID(networkName, interfaceName))) +} + +func (n nsxtNetworkProvider) assertNetworkInterfacesDNE( + ctx *builder.TestContextForVCSim, + vm *vmopv1.VirtualMachine, + networkName, interfaceName string) { + + netInterface := &ncpv1alpha1.VirtualNetworkInterface{ + ObjectMeta: metav1.ObjectMeta{ + Name: network.NCPCRName(vm.Name, networkName, interfaceName, false), + Namespace: vm.Namespace, + }, + } + err := ctx.Client.Get(ctx, client.ObjectKeyFromObject(netInterface), netInterface) + Expect(err).To(HaveOccurred()) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) +} + +type vpcNetworkProvider struct{} + +func (v vpcNetworkProvider) simulateInterfaceReconcile( + ctx *builder.TestContextForVCSim, + vm *vmopv1.VirtualMachine, + interfaceSpec vmopv1.VirtualMachineNetworkInterfaceSpec, + networkIdx int) { + + interfaceName, networkName := interfaceSpec.Name, interfaceSpec.Network.Name + ifaceIdx := idxFromInterfaceName(interfaceName) + + subnetPort := &vpcv1alpha1.SubnetPort{ + ObjectMeta: metav1.ObjectMeta{ + Name: network.VPCCRName(vm.Name, networkName, interfaceName), + Namespace: vm.Namespace, + }, + } + Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(subnetPort), subnetPort)).To(Succeed()) + Expect(subnetPort.Spec.Subnet).To(Equal(networkName)) + + subnetPort.Status.Attachment.ID = extID(networkName, interfaceName) + subnetPort.Status.NetworkInterfaceConfig.MACAddress = fmt.Sprintf("01-23-45-67-89-%02X", ifaceIdx) + subnetPort.Status.NetworkInterfaceConfig.LogicalSwitchUUID = builder.GetVPCTLogicalSwitchUUID(networkIdx) + subnetPort.Status.NetworkInterfaceConfig.IPAddresses = []vpcv1alpha1.NetworkInterfaceIPAddress{ + { + IPAddress: fmt.Sprintf("192.168.1.11%d/24", ifaceIdx), + Gateway: "192.168.1.1", + }, + } + subnetPort.Status.Conditions = []vpcv1alpha1.Condition{ + { + Type: vpcv1alpha1.Ready, + Status: corev1.ConditionTrue, + }, + } + Expect(ctx.Client.Status().Update(ctx, subnetPort)).To(Succeed()) +} + +func (v vpcNetworkProvider) assertEthernetCard( + ctx *builder.TestContextForVCSim, + dev vimtypes.BaseVirtualDevice, + interfaceSpec vmopv1.VirtualMachineNetworkInterfaceSpec, + networkIdx int) { + + interfaceName, networkName := interfaceSpec.Name, interfaceSpec.Network.Name + ifaceIdx := idxFromInterfaceName(interfaceName) + + ethCard := dev.(vimtypes.BaseVirtualEthernetCard).GetVirtualEthernetCard() + backingInfo, ok := ethCard.Backing.(*vimtypes.VirtualEthernetCardDistributedVirtualPortBackingInfo) + Expect(ok).Should(BeTrue()) + Expect(backingInfo.Port.PortgroupKey).To(Equal(ctx.NetworkRefs[networkIdx].Reference().Value)) + Expect(ethCard.MacAddress).To(Equal(fmt.Sprintf("01-23-45-67-89-%02X", ifaceIdx))) + Expect(ethCard.ExternalId).To(Equal(extID(networkName, interfaceName))) +} + +func (v vpcNetworkProvider) assertNetworkInterfacesDNE( + ctx *builder.TestContextForVCSim, + vm *vmopv1.VirtualMachine, + networkName, interfaceName string) { + + subnetPort := &vpcv1alpha1.SubnetPort{ + ObjectMeta: metav1.ObjectMeta{ + Name: network.VPCCRName(vm.Name, networkName, interfaceName), + Namespace: vm.Namespace, + }, + } + err := ctx.Client.Get(ctx, client.ObjectKeyFromObject(subnetPort), subnetPort) + Expect(err).To(HaveOccurred()) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) +} diff --git a/pkg/providers/vsphere/vmprovider_vm_npe_test.go b/pkg/providers/vsphere/vmprovider_vm_npe_test.go new file mode 100644 index 000000000..45e27e8bf --- /dev/null +++ b/pkg/providers/vsphere/vmprovider_vm_npe_test.go @@ -0,0 +1,160 @@ +// © Broadcom. All Rights Reserved. +// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package vsphere_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha6" + "github.com/vmware-tanzu/vm-operator/pkg/conditions" + pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + ctxop "github.com/vmware-tanzu/vm-operator/pkg/context/operation" + "github.com/vmware-tanzu/vm-operator/pkg/providers" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" + "github.com/vmware-tanzu/vm-operator/pkg/util/kube/cource" + "github.com/vmware-tanzu/vm-operator/pkg/util/ovfcache" + "github.com/vmware-tanzu/vm-operator/test/builder" +) + +func vmNPETests() { + var ( + parentCtx context.Context + initObjects []client.Object + testConfig builder.VCSimTestConfig + ctx *builder.TestContextForVCSim + vmProvider providers.VirtualMachineProviderInterface + nsInfo builder.WorkloadNamespaceInfo + + vm *vmopv1.VirtualMachine + vmClass *vmopv1.VirtualMachineClass + + zoneName string + ) + + BeforeEach(func() { + parentCtx = pkgcfg.NewContextWithDefaultConfig() + parentCtx = ctxop.WithContext(parentCtx) + parentCtx = ovfcache.WithContext(parentCtx) + parentCtx = cource.WithContext(parentCtx) + pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { + config.AsyncCreateEnabled = false + config.AsyncSignalEnabled = false + }) + testConfig = builder.VCSimTestConfig{ + WithContentLibrary: true, + WithNetworkEnv: builder.NetworkEnvVDS, + } + + vmClass = builder.DummyVirtualMachineClassGenName() + vm = builder.DummyBasicVirtualMachine("test-vm", "") + + if vm.Spec.Network == nil { + vm.Spec.Network = &vmopv1.VirtualMachineNetworkSpec{} + } + vm.Spec.Network.Disabled = true + }) + + JustBeforeEach(func() { + ctx = suite.NewTestContextForVCSimWithParentContext( + parentCtx, testConfig, initObjects...) + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.MaxDeployThreadsOnProvider = 1 + }) + vmProvider = vsphere.NewVSphereVMProviderFromClient( + ctx, ctx.Client, ctx.Recorder) + nsInfo = ctx.CreateWorkloadNamespace() + + vmClass.Namespace = nsInfo.Namespace + Expect(ctx.Client.Create(ctx, vmClass)).To(Succeed()) + + clusterVMI1 := &vmopv1.ClusterVirtualMachineImage{} + + if testConfig.WithContentLibrary { + Expect(ctx.Client.Get( + ctx, client.ObjectKey{Name: ctx.ContentLibraryItem1Name}, + clusterVMI1)).To(Succeed()) + } else { + vsphere.SkipVMImageCLProviderCheck = true + clusterVMI1 = builder.DummyClusterVirtualMachineImage("DC0_C0_RP0_VM0") + Expect(ctx.Client.Create(ctx, clusterVMI1)).To(Succeed()) + conditions.MarkTrue(clusterVMI1, vmopv1.ReadyConditionType) + Expect(ctx.Client.Status().Update(ctx, clusterVMI1)).To(Succeed()) + } + + vm.Namespace = nsInfo.Namespace + vm.Spec.ClassName = vmClass.Name + vm.Spec.ImageName = clusterVMI1.Name + vm.Spec.Image.Kind = cvmiKind + vm.Spec.Image.Name = clusterVMI1.Name + vm.Spec.StorageClass = ctx.StorageClassName + + Expect(ctx.Client.Create(ctx, vm)).To(Succeed()) + + zoneName = ctx.GetFirstZoneName() + vm.Labels[corev1.LabelTopologyZone] = zoneName + Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) + }) + + AfterEach(func() { + vsphere.SkipVMImageCLProviderCheck = false + + if vm != nil && + !pkgcfg.FromContext(ctx).Features.BringYourOwnEncryptionKey { + By("Assert vm.Status.Crypto is nil when BYOK is disabled", func() { + Expect(vm.Status.Crypto).To(BeNil()) + }) + } + + vmClass = nil + vm = nil + + ctx.AfterEach() + ctx = nil + initObjects = nil + vmProvider = nil + nsInfo = builder.WorkloadNamespaceInfo{} + }) + + DescribeTable("npe checks", + func(fn func(vm *vmopv1.VirtualMachine)) { + fn(vm) + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + + Expect(vm.Status.UniqueID).ToNot(BeEmpty()) + vcVM := ctx.GetVMFromMoID(vm.Status.UniqueID) + Expect(vcVM).ToNot(BeNil()) + }, + Entry( + "nil spec.advanced", + func(vm *vmopv1.VirtualMachine) { + vm.Spec.Advanced = nil + }, + ), + Entry( + "nil spec.bootstrap", + func(vm *vmopv1.VirtualMachine) { + vm.Spec.Bootstrap = nil + }, + ), + Entry( + "nil spec.network", + func(vm *vmopv1.VirtualMachine) { + vm.Spec.Network = nil + }, + ), + Entry( + "nil spec.reserved", + func(vm *vmopv1.VirtualMachine) { + vm.Spec.Reserved = nil + }, + ), + ) +} diff --git a/pkg/providers/vsphere/vmprovider_vm_pci_test.go b/pkg/providers/vsphere/vmprovider_vm_pci_test.go new file mode 100644 index 000000000..9ff05ca78 --- /dev/null +++ b/pkg/providers/vsphere/vmprovider_vm_pci_test.go @@ -0,0 +1,168 @@ +// © Broadcom. All Rights Reserved. +// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package vsphere_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/vmware/govmomi/object" + "github.com/vmware/govmomi/vim25/mo" + vimtypes "github.com/vmware/govmomi/vim25/types" + + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha6" + "github.com/vmware-tanzu/vm-operator/pkg/conditions" + pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + ctxop "github.com/vmware-tanzu/vm-operator/pkg/context/operation" + "github.com/vmware-tanzu/vm-operator/pkg/providers" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" + "github.com/vmware-tanzu/vm-operator/pkg/util/kube/cource" + "github.com/vmware-tanzu/vm-operator/pkg/util/ovfcache" + "github.com/vmware-tanzu/vm-operator/test/builder" +) + +func vmPCITests() { + var ( + parentCtx context.Context + initObjects []client.Object + testConfig builder.VCSimTestConfig + ctx *builder.TestContextForVCSim + vmProvider providers.VirtualMachineProviderInterface + nsInfo builder.WorkloadNamespaceInfo + + vm *vmopv1.VirtualMachine + vmClass *vmopv1.VirtualMachineClass + + zoneName string + ) + + BeforeEach(func() { + parentCtx = pkgcfg.NewContextWithDefaultConfig() + parentCtx = ctxop.WithContext(parentCtx) + parentCtx = ovfcache.WithContext(parentCtx) + parentCtx = cource.WithContext(parentCtx) + pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { + config.AsyncCreateEnabled = false + config.AsyncSignalEnabled = false + }) + testConfig = builder.VCSimTestConfig{ + WithContentLibrary: true, + } + + vmClass = builder.DummyVirtualMachineClassGenName() + vm = builder.DummyBasicVirtualMachine("test-vm", "") + + // Reduce diff from old tests: by default don't create an NIC. + if vm.Spec.Network == nil { + vm.Spec.Network = &vmopv1.VirtualMachineNetworkSpec{} + } + vm.Spec.Network.Disabled = true + + // For old behavior, we'll fallback to these standalone fields when the + // class does not have a ConfigSpec. + vmClass.Spec.Hardware.Devices = vmopv1.VirtualDevices{ + VGPUDevices: []vmopv1.VGPUDevice{ + { + ProfileName: "profile-from-class-without-class-as-config-fss", + }, + }, + DynamicDirectPathIODevices: []vmopv1.DynamicDirectPathIODevice{ + { + VendorID: 59, + DeviceID: 60, + CustomLabel: "label-from-class-without-class-as-config-fss", + }, + }, + } + }) + + JustBeforeEach(func() { + ctx = suite.NewTestContextForVCSimWithParentContext( + parentCtx, testConfig, initObjects...) + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.MaxDeployThreadsOnProvider = 1 + }) + vmProvider = vsphere.NewVSphereVMProviderFromClient( + ctx, ctx.Client, ctx.Recorder) + nsInfo = ctx.CreateWorkloadNamespace() + + vmClass.Namespace = nsInfo.Namespace + Expect(ctx.Client.Create(ctx, vmClass)).To(Succeed()) + + clusterVMI1 := &vmopv1.ClusterVirtualMachineImage{} + + if testConfig.WithContentLibrary { + Expect(ctx.Client.Get( + ctx, client.ObjectKey{ + Name: ctx.ContentLibraryItem1Name, + }, clusterVMI1)).To(Succeed()) + + } else { + // BMV: VM creation without CL is broken - and has been for a long + // while - since we assume the VM Image will always point to a + // ContentLibrary item. + // Hack around that with this knob so we can continue to test the + // VM clone path. + vsphere.SkipVMImageCLProviderCheck = true + + // Use the default VM created by vcsim as the source. + clusterVMI1 = builder.DummyClusterVirtualMachineImage("DC0_C0_RP0_VM0") + Expect(ctx.Client.Create(ctx, clusterVMI1)).To(Succeed()) + conditions.MarkTrue(clusterVMI1, vmopv1.ReadyConditionType) + Expect(ctx.Client.Status().Update(ctx, clusterVMI1)).To(Succeed()) + } + + vm.Namespace = nsInfo.Namespace + vm.Spec.ClassName = vmClass.Name + vm.Spec.ImageName = clusterVMI1.Name + vm.Spec.Image.Kind = cvmiKind + vm.Spec.Image.Name = clusterVMI1.Name + vm.Spec.StorageClass = ctx.StorageClassName + + Expect(ctx.Client.Create(ctx, vm)).To(Succeed()) + + // Explicitly place the VM into one of the zones that the test context + // will create. + zoneName = ctx.GetFirstZoneName() + vm.Labels[corev1.LabelTopologyZone] = zoneName + Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) + }) + + AfterEach(func() { + vsphere.SkipVMImageCLProviderCheck = false + + if vm != nil && + !pkgcfg.FromContext(ctx).Features.BringYourOwnEncryptionKey { + By("Assert vm.Status.Crypto is nil when BYOK is disabled", func() { + Expect(vm.Status.Crypto).To(BeNil()) + }) + } + + vmClass = nil + vm = nil + + ctx.AfterEach() + ctx = nil + initObjects = nil + vmProvider = nil + nsInfo = builder.WorkloadNamespaceInfo{} + }) + + It("VM should have PCI devices from VM Class", func() { + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + + devList := object.VirtualDeviceList(o.Config.Hardware.Device) + p := devList.SelectByType(&vimtypes.VirtualPCIPassthrough{}) + Expect(p).To(HaveLen(2)) + }) +} diff --git a/pkg/providers/vsphere/vmprovider_vm_policy_test.go b/pkg/providers/vsphere/vmprovider_vm_policy_test.go new file mode 100644 index 000000000..ae69137c8 --- /dev/null +++ b/pkg/providers/vsphere/vmprovider_vm_policy_test.go @@ -0,0 +1,536 @@ +// © Broadcom. All Rights Reserved. +// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package vsphere_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/vmware/govmomi/vapi/tags" + "github.com/vmware/govmomi/vim25/mo" + + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha6" + vspherepolv1 "github.com/vmware-tanzu/vm-operator/external/vsphere-policy/api/v1alpha1" + "github.com/vmware-tanzu/vm-operator/pkg/conditions" + pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + ctxop "github.com/vmware-tanzu/vm-operator/pkg/context/operation" + "github.com/vmware-tanzu/vm-operator/pkg/providers" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" + pkgutil "github.com/vmware-tanzu/vm-operator/pkg/util" + "github.com/vmware-tanzu/vm-operator/pkg/util/kube/cource" + "github.com/vmware-tanzu/vm-operator/pkg/util/ovfcache" + "github.com/vmware-tanzu/vm-operator/pkg/util/ptr" + vmconfpolicy "github.com/vmware-tanzu/vm-operator/pkg/vmconfig/policy" + "github.com/vmware-tanzu/vm-operator/test/builder" +) + +func vmPolicyTests() { + var ( + parentCtx context.Context + initObjects []client.Object + testConfig builder.VCSimTestConfig + ctx *builder.TestContextForVCSim + vmProvider providers.VirtualMachineProviderInterface + nsInfo builder.WorkloadNamespaceInfo + + vm *vmopv1.VirtualMachine + vmClass *vmopv1.VirtualMachineClass + ) + + BeforeEach(func() { + parentCtx = pkgcfg.NewContextWithDefaultConfig() + parentCtx = ctxop.WithContext(parentCtx) + parentCtx = ovfcache.WithContext(parentCtx) + parentCtx = cource.WithContext(parentCtx) + pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { + config.AsyncCreateEnabled = false + config.AsyncSignalEnabled = false + }) + testConfig = builder.VCSimTestConfig{ + WithContentLibrary: true, + } + + vmClass = builder.DummyVirtualMachineClassGenName() + vm = builder.DummyBasicVirtualMachine("test-vm", "") + + if vm.Spec.Network == nil { + vm.Spec.Network = &vmopv1.VirtualMachineNetworkSpec{} + } + vm.Spec.Network.Disabled = true + }) + + JustBeforeEach(func() { + ctx = suite.NewTestContextForVCSimWithParentContext( + parentCtx, testConfig, initObjects...) + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.MaxDeployThreadsOnProvider = 1 + }) + vmProvider = vsphere.NewVSphereVMProviderFromClient( + ctx, ctx.Client, ctx.Recorder) + nsInfo = ctx.CreateWorkloadNamespace() + + vmClass.Namespace = nsInfo.Namespace + Expect(ctx.Client.Create(ctx, vmClass)).To(Succeed()) + + clusterVMI1 := &vmopv1.ClusterVirtualMachineImage{} + + if testConfig.WithContentLibrary { + Expect(ctx.Client.Get( + ctx, client.ObjectKey{Name: ctx.ContentLibraryItem1Name}, + clusterVMI1)).To(Succeed()) + } else { + vsphere.SkipVMImageCLProviderCheck = true + clusterVMI1 = builder.DummyClusterVirtualMachineImage("DC0_C0_RP0_VM0") + Expect(ctx.Client.Create(ctx, clusterVMI1)).To(Succeed()) + conditions.MarkTrue(clusterVMI1, vmopv1.ReadyConditionType) + Expect(ctx.Client.Status().Update(ctx, clusterVMI1)).To(Succeed()) + } + + vm.Namespace = nsInfo.Namespace + vm.Spec.ClassName = vmClass.Name + vm.Spec.ImageName = clusterVMI1.Name + vm.Spec.Image.Kind = cvmiKind + vm.Spec.Image.Name = clusterVMI1.Name + vm.Spec.StorageClass = ctx.StorageClassName + + Expect(ctx.Client.Create(ctx, vm)).To(Succeed()) + }) + + AfterEach(func() { + vsphere.SkipVMImageCLProviderCheck = false + + if vm != nil && + !pkgcfg.FromContext(ctx).Features.BringYourOwnEncryptionKey { + By("Assert vm.Status.Crypto is nil when BYOK is disabled", func() { + Expect(vm.Status.Crypto).To(BeNil()) + }) + } + + vmClass = nil + vm = nil + + ctx.AfterEach() + ctx = nil + initObjects = nil + vmProvider = nil + nsInfo = builder.WorkloadNamespaceInfo{} + }) + + var ( + policyTag1ID string + policyTag2ID string + policyTag3ID string + + tagMgr *tags.Manager + ) + + JustBeforeEach(func() { + var err error + + tagMgr = tags.NewManager(ctx.RestClient) + + // Create a category for the policy tags + categoryID, err := tagMgr.CreateCategory(ctx, &tags.Category{ + Name: "my-policy-category", + Description: "Category for policy tags", + AssociableTypes: []string{"VirtualMachine"}, + }) + Expect(err).ToNot(HaveOccurred()) + + policyTag1ID, err = tagMgr.CreateTag(ctx, &tags.Tag{ + Name: "my-policy-tag-1", + CategoryID: categoryID, + }) + Expect(err).ToNot(HaveOccurred()) + + policyTag2ID, err = tagMgr.CreateTag(ctx, &tags.Tag{ + Name: "my-policy-tag-2", + CategoryID: categoryID, + }) + Expect(err).ToNot(HaveOccurred()) + + policyTag3ID, err = tagMgr.CreateTag(ctx, &tags.Tag{ + Name: "my-policy-tag-3", + CategoryID: categoryID, + }) + Expect(err).ToNot(HaveOccurred()) + }) + + When("Capability is enabled", func() { + JustBeforeEach(func() { + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.Features.VSpherePolicies = true + }) + }) + + When("creating a VM", func() { + When("async create is enabled", func() { + JustBeforeEach(func() { + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.AsyncCreateEnabled = true + }) + }) + It("should successfully create VM", func() { + By("Setting up VM with policy evaluation objects", func() { + // Set VM UID for proper PolicyEvaluation naming + vm.UID = "test-vm-policy-uid" + + // Create a PolicyEvaluation object that will be found during policy reconciliation + policyEval := &vspherepolv1.PolicyEvaluation{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: vm.Namespace, + Name: "vm-" + vm.Name, + Generation: 1, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: vmopv1.GroupVersion.String(), + Kind: "VirtualMachine", + Name: vm.Name, + UID: vm.UID, + Controller: ptr.To(true), + BlockOwnerDeletion: ptr.To(true), + }, + }, + }, + Spec: vspherepolv1.PolicyEvaluationSpec{ + Workload: &vspherepolv1.PolicyEvaluationWorkloadSpec{ + Guest: &vspherepolv1.PolicyEvaluationGuestSpec{ + GuestID: "ubuntu64Guest", + GuestFamily: vspherepolv1.GuestFamilyTypeLinux, + }, + }, + }, + Status: vspherepolv1.PolicyEvaluationStatus{ + ObservedGeneration: 1, + Policies: []vspherepolv1.PolicyEvaluationResult{ + { + Name: "test-active-policy", + Kind: "ComputePolicy", + Tags: []string{policyTag1ID, policyTag2ID}, + }, + }, + Conditions: []metav1.Condition{ + *conditions.TrueCondition(vspherepolv1.ReadyConditionType), + }, + }, + } + + // Create the PolicyEvaluation in the fake Kubernetes client + Expect(ctx.Client.Create(ctx, policyEval)).To(Succeed()) + }) + + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + // Verify VM was created successfully + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + + // Verify placement condition is ready (indicates vmconfpolicy.Reconcile was called) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionPlacementReady)).To(BeTrue()) + + // Verify that policy tags were added to ExtraConfig + By("VM has policy tags in ExtraConfig", func() { + Expect(o.Config.ExtraConfig).ToNot(BeNil()) + + ecMap := pkgutil.OptionValues(o.Config.ExtraConfig).StringMap() + + // Verify tags are present + Expect(ecMap).To(HaveKey(vmconfpolicy.ExtraConfigPolicyTagsKey)) + activeTags := ecMap[vmconfpolicy.ExtraConfigPolicyTagsKey] + Expect(activeTags).To(ContainSubstring(policyTag1ID)) + Expect(activeTags).To(ContainSubstring(policyTag2ID)) + }) + }) + + }) + When("async create is disabled", func() { + JustBeforeEach(func() { + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.AsyncCreateEnabled = false + }) + }) + + It("should successfully create VM and call vmconfig policy.Reconcile during placement", func() { + By("Setting up VM with policy evaluation objects", func() { + // Set VM UID for proper PolicyEvaluation naming + vm.UID = "test-vm-sync-policy-uid" + + // Create a PolicyEvaluation object that will be found during policy reconciliation + policyEval := &vspherepolv1.PolicyEvaluation{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: vm.Namespace, + Name: "vm-" + vm.Name, + Generation: 1, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: vmopv1.GroupVersion.String(), + Kind: "VirtualMachine", + Name: vm.Name, + UID: vm.UID, + Controller: ptr.To(true), + BlockOwnerDeletion: ptr.To(true), + }, + }, + }, + Spec: vspherepolv1.PolicyEvaluationSpec{ + Workload: &vspherepolv1.PolicyEvaluationWorkloadSpec{ + Guest: &vspherepolv1.PolicyEvaluationGuestSpec{ + GuestID: "ubuntu64Guest", + GuestFamily: vspherepolv1.GuestFamilyTypeLinux, + }, + }, + }, + Status: vspherepolv1.PolicyEvaluationStatus{ + ObservedGeneration: 1, + Policies: []vspherepolv1.PolicyEvaluationResult{ + { + Name: "test-sync-active-policy", + Kind: "ComputePolicy", + Tags: []string{policyTag1ID, policyTag2ID}, + }, + }, + Conditions: []metav1.Condition{ + *conditions.TrueCondition(vspherepolv1.ReadyConditionType), + }, + }, + } + + // Create the PolicyEvaluation in the fake Kubernetes client + Expect(ctx.Client.Create(ctx, policyEval)).To(Succeed()) + }) + + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + // Verify VM was created successfully + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + + // Verify placement condition is ready (indicates vmconfpolicy.Reconcile was called) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionPlacementReady)).To(BeTrue()) + + // Verify that policy tags were added to ExtraConfig + By("VM has policy tags in ExtraConfig", func() { + Expect(o.Config.ExtraConfig).ToNot(BeNil()) + + ecMap := pkgutil.OptionValues(o.Config.ExtraConfig).StringMap() + + // Verify tags are present + Expect(ecMap).To(HaveKey(vmconfpolicy.ExtraConfigPolicyTagsKey)) + activeTags := ecMap[vmconfpolicy.ExtraConfigPolicyTagsKey] + Expect(activeTags).To(ContainSubstring(policyTag1ID)) + Expect(activeTags).To(ContainSubstring(policyTag2ID)) + }) + }) + }) + }) + + When("updating a VM", func() { + It("should update VM with policy tags during reconfiguration", func() { + By("Setting up VM with policy evaluation objects", func() { + // Set VM UID for proper PolicyEvaluation naming + vm.UID = "test-vm-policy-uid" + + // Create a PolicyEvaluation object that will be found during policy reconciliation + policyEval := &vspherepolv1.PolicyEvaluation{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: vm.Namespace, + Name: "vm-" + vm.Name, + Generation: 1, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: vmopv1.GroupVersion.String(), + Kind: "VirtualMachine", + Name: vm.Name, + UID: vm.UID, + Controller: ptr.To(true), + BlockOwnerDeletion: ptr.To(true), + }, + }, + }, + Spec: vspherepolv1.PolicyEvaluationSpec{ + Workload: &vspherepolv1.PolicyEvaluationWorkloadSpec{ + Guest: &vspherepolv1.PolicyEvaluationGuestSpec{ + GuestID: "ubuntu64Guest", + GuestFamily: vspherepolv1.GuestFamilyTypeLinux, + }, + }, + }, + Status: vspherepolv1.PolicyEvaluationStatus{ + ObservedGeneration: 1, + Conditions: []metav1.Condition{ + *conditions.TrueCondition(vspherepolv1.ReadyConditionType), + }, + }, + } + + // Create the PolicyEvaluation in the fake Kubernetes client + Expect(ctx.Client.Create(ctx, policyEval)).To(Succeed()) + }) + + // First create the VM + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + By("Adding a non-policy tag to the VM", func() { + mgr := tags.NewManager(ctx.RestClient) + + Expect(mgr.AttachTag(ctx, ctx.TagID, vcVM.Reference())).To(Succeed()) + + list, err := mgr.GetAttachedTags(ctx, vcVM.Reference()) + Expect(err).ToNot(HaveOccurred()) + Expect(list).To(HaveLen(1)) + Expect(list[0].ID).To(Equal(ctx.TagID)) + }) + + By("Setting up PolicyEvaluation for update", func() { + // Create a PolicyEvaluation object with updated tags + policyEval := &vspherepolv1.PolicyEvaluation{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: vm.Namespace, + Name: "vm-" + vm.Name, + }, + } + + Expect(ctx.Client.Get( + ctx, + client.ObjectKeyFromObject(policyEval), + policyEval)).To(Succeed()) + + // Create a PolicyEvaluation object with updated tags + policyEval.Status = vspherepolv1.PolicyEvaluationStatus{ + ObservedGeneration: policyEval.Generation, + Policies: []vspherepolv1.PolicyEvaluationResult{ + { + Name: "test-updated-active-policy", + Kind: "ComputePolicy", + Tags: []string{policyTag1ID, policyTag2ID, policyTag3ID}, + }, + }, + Conditions: []metav1.Condition{ + *conditions.TrueCondition(vspherepolv1.ReadyConditionType), + }, + } + + // Update the PolicyEvaluation in the fake Kubernetes client + Expect(ctx.Client.Status().Update(ctx, policyEval)).To(Succeed()) + }) + + // Trigger VM update. + vcVM, err = createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + // Get VM properties. + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + + // Verify that updated policy tags were added to ExtraConfig + By("VM has updated policy tags in ExtraConfig", func() { + Expect(o.Config.ExtraConfig).ToNot(BeNil()) + + ecMap := pkgutil.OptionValues(o.Config.ExtraConfig).StringMap() + + // Verify updated tags are present + Expect(ecMap).To(HaveKey(vmconfpolicy.ExtraConfigPolicyTagsKey)) + activeTags := ecMap[vmconfpolicy.ExtraConfigPolicyTagsKey] + Expect(activeTags).To(ContainSubstring(policyTag1ID)) + Expect(activeTags).To(ContainSubstring(policyTag2ID)) + Expect(activeTags).To(ContainSubstring(policyTag3ID)) + }) + }) + }) + }) + + When("Capability is disabled", func() { + JustBeforeEach(func() { + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.Features.VSpherePolicies = false + }) + }) + + When("creating a VM", func() { + When("async create is enabled", func() { + JustBeforeEach(func() { + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.AsyncCreateEnabled = true + }) + }) + It("should successfully create VM without calling vmconfpolicy.Reconcile", func() { + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + // Verify VM was created successfully + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + + // Verify placement condition is ready even without policy reconciliation + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionPlacementReady)).To(BeTrue()) + }) + + }) + When("async create is disabled", func() { + JustBeforeEach(func() { + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.AsyncCreateEnabled = false + }) + }) + + It("should successfully create VM without calling vmconfpolicy.Reconcile", func() { + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + // Verify VM was created successfully + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + + // Verify placement condition is ready even without policy reconciliation + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionPlacementReady)).To(BeTrue()) + + // Verify that no policy tags were added to ExtraConfig (policy disabled) + By("VM should not have policy tags in ExtraConfig", func() { + if o.Config.ExtraConfig != nil { + ecMap := pkgutil.OptionValues(o.Config.ExtraConfig).StringMap() + + // Verify no tags are present + Expect(ecMap).ToNot(HaveKey(vmconfpolicy.ExtraConfigPolicyTagsKey)) + } + }) + }) + }) + }) + + When("updating a VM", func() { + It("should update VM without adding policy tags", func() { + // First create the VM + _, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + // Trigger VM update. + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + // Get VM properties. + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + + // Verify that no policy tags were added to ExtraConfig during update + By("VM should not have policy tags in ExtraConfig after update", func() { + if o.Config.ExtraConfig != nil { + ecMap := pkgutil.OptionValues(o.Config.ExtraConfig).StringMap() + + // Verify no tags are present + Expect(ecMap).ToNot(HaveKey(vmconfpolicy.ExtraConfigPolicyTagsKey)) + } + }) + }) + }) + }) +} diff --git a/pkg/providers/vsphere/vmprovider_vm_power_test.go b/pkg/providers/vsphere/vmprovider_vm_power_test.go new file mode 100644 index 000000000..79e7b4b07 --- /dev/null +++ b/pkg/providers/vsphere/vmprovider_vm_power_test.go @@ -0,0 +1,978 @@ +// © Broadcom. All Rights Reserved. +// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package vsphere_test + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "sigs.k8s.io/controller-runtime/pkg/client" + + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/vmware/govmomi/object" + "github.com/vmware/govmomi/simulator" + "github.com/vmware/govmomi/vim25/mo" + vimtypes "github.com/vmware/govmomi/vim25/types" + + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha6" + "github.com/vmware-tanzu/vm-operator/api/v1alpha6/cloudinit" + vmopv1common "github.com/vmware-tanzu/vm-operator/api/v1alpha6/common" + "github.com/vmware-tanzu/vm-operator/pkg/conditions" + pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + pkgconst "github.com/vmware-tanzu/vm-operator/pkg/constants" + ctxop "github.com/vmware-tanzu/vm-operator/pkg/context/operation" + pkgerr "github.com/vmware-tanzu/vm-operator/pkg/errors" + "github.com/vmware-tanzu/vm-operator/pkg/providers" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" + "github.com/vmware-tanzu/vm-operator/pkg/util/kube/cource" + "github.com/vmware-tanzu/vm-operator/pkg/util/ovfcache" + "github.com/vmware-tanzu/vm-operator/pkg/util/ptr" + "github.com/vmware-tanzu/vm-operator/test/builder" + "github.com/vmware-tanzu/vm-operator/test/testutil" +) + +func vmPowerStateTests() { + var ( + parentCtx context.Context + initObjects []client.Object + testConfig builder.VCSimTestConfig + ctx *builder.TestContextForVCSim + vmProvider providers.VirtualMachineProviderInterface + nsInfo builder.WorkloadNamespaceInfo + + vm *vmopv1.VirtualMachine + vmClass *vmopv1.VirtualMachineClass + ) + + BeforeEach(func() { + parentCtx = pkgcfg.NewContextWithDefaultConfig() + parentCtx = ctxop.WithContext(parentCtx) + parentCtx = ovfcache.WithContext(parentCtx) + parentCtx = cource.WithContext(parentCtx) + pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { + config.AsyncCreateEnabled = false + config.AsyncSignalEnabled = false + }) + testConfig = builder.VCSimTestConfig{ + WithContentLibrary: true, + } + + vmClass = builder.DummyVirtualMachineClassGenName() + vm = builder.DummyBasicVirtualMachine("test-vm", "") + + if vm.Spec.Network == nil { + vm.Spec.Network = &vmopv1.VirtualMachineNetworkSpec{} + } + vm.Spec.Network.Disabled = true + }) + + JustBeforeEach(func() { + ctx = suite.NewTestContextForVCSimWithParentContext( + parentCtx, testConfig, initObjects...) + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.MaxDeployThreadsOnProvider = 1 + }) + vmProvider = vsphere.NewVSphereVMProviderFromClient( + ctx, ctx.Client, ctx.Recorder) + nsInfo = ctx.CreateWorkloadNamespace() + + vmClass.Namespace = nsInfo.Namespace + Expect(ctx.Client.Create(ctx, vmClass)).To(Succeed()) + + clusterVMI1 := &vmopv1.ClusterVirtualMachineImage{} + + if testConfig.WithContentLibrary { + Expect(ctx.Client.Get( + ctx, client.ObjectKey{Name: ctx.ContentLibraryItem1Name}, + clusterVMI1)).To(Succeed()) + } else { + vsphere.SkipVMImageCLProviderCheck = true + clusterVMI1 = builder.DummyClusterVirtualMachineImage("DC0_C0_RP0_VM0") + Expect(ctx.Client.Create(ctx, clusterVMI1)).To(Succeed()) + conditions.MarkTrue(clusterVMI1, vmopv1.ReadyConditionType) + Expect(ctx.Client.Status().Update(ctx, clusterVMI1)).To(Succeed()) + } + + vm.Namespace = nsInfo.Namespace + vm.Spec.ClassName = vmClass.Name + vm.Spec.ImageName = clusterVMI1.Name + vm.Spec.Image.Kind = cvmiKind + vm.Spec.Image.Name = clusterVMI1.Name + vm.Spec.StorageClass = ctx.StorageClassName + + Expect(ctx.Client.Create(ctx, vm)).To(Succeed()) + }) + + AfterEach(func() { + vsphere.SkipVMImageCLProviderCheck = false + + if vm != nil && + !pkgcfg.FromContext(ctx).Features.BringYourOwnEncryptionKey { + By("Assert vm.Status.Crypto is nil when BYOK is disabled", func() { + Expect(vm.Status.Crypto).To(BeNil()) + }) + } + + vmClass = nil + vm = nil + + ctx.AfterEach() + ctx = nil + initObjects = nil + vmProvider = nil + nsInfo = builder.WorkloadNamespaceInfo{} + }) + + getLastRestartTime := func(moVM mo.VirtualMachine) string { + for i := range moVM.Config.ExtraConfig { + ov := moVM.Config.ExtraConfig[i].GetOptionValue() + if ov.Key == "vmservice.lastRestartTime" { + return ov.Value.(string) + } + } + return "" + } + + var ( + vcVM *object.VirtualMachine + moVM mo.VirtualMachine + ) + + JustBeforeEach(func() { + var err error + moVM = mo.VirtualMachine{} + vcVM, err = createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &moVM)).To(Succeed()) + }) + + When("vcVM is powered on", func() { + JustBeforeEach(func() { + vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOn + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOn)) + }) + + When("power state is not changed", func() { + BeforeEach(func() { + vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOn + }) + It("should not return an error", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + }) + }) + + When("powering off the VM", func() { + JustBeforeEach(func() { + vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOff + }) + + When("power state should not be updated", func() { + const expectedPowerState = vmopv1.VirtualMachinePowerStateOn + When("vm is paused by devops", func() { + JustBeforeEach(func() { + vm.Annotations = map[string]string{ + vmopv1.PauseAnnotation: "true", + } + }) + It("should not change the power state", func() { + Expect(errors.Is(createOrUpdateVM(ctx, vmProvider, vm), vsphere.ErrIsPaused)).To(BeTrue()) + Expect(vm.Status.PowerState).To(Equal(expectedPowerState)) + }) + }) + When("vm is paused by admin", func() { + JustBeforeEach(func() { + vm.Annotations = map[string]string{ + vmopv1.PauseAnnotation: "true", + } + t, err := vcVM.Reconfigure(ctx, vimtypes.VirtualMachineConfigSpec{ + ExtraConfig: []vimtypes.BaseOptionValue{ + &vimtypes.OptionValue{ + Key: vmopv1.PauseVMExtraConfigKey, + Value: "true", + }, + }, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(t.Wait(ctx)).To(Succeed()) + }) + It("should not change the power state", func() { + Expect(errors.Is(createOrUpdateVM(ctx, vmProvider, vm), vsphere.ErrIsPaused)).To(BeTrue()) + Expect(vm.Status.PowerState).To(Equal(expectedPowerState)) + }) + }) + + When("vm has running task", func() { + var ( + reg *simulator.Registry + simCtx *simulator.Context + taskRef vimtypes.ManagedObjectReference + ) + + JustBeforeEach(func() { + simCtx = ctx.SimulatorContext() + reg = simCtx.Map + taskRef = reg.Put(&mo.Task{ + Info: vimtypes.TaskInfo{ + State: vimtypes.TaskInfoStateRunning, + DescriptionId: "fake.task.1", + }, + }).Reference() + + vmRef := vimtypes.ManagedObjectReference{ + Type: string(vimtypes.ManagedObjectTypeVirtualMachine), + Value: vm.Status.UniqueID, + } + + reg.WithLock( + simCtx, + vmRef, + func() { + vm := reg.Get(vmRef).(*simulator.VirtualMachine) + vm.RecentTask = append(vm.RecentTask, taskRef) + }) + + }) + + AfterEach(func() { + reg.Remove(simCtx, taskRef) + }) + + It("should not change the power state", func() { + Expect(errors.Is(createOrUpdateVM(ctx, vmProvider, vm), vsphere.ErrHasTask)).To(BeTrue()) + Expect(vm.Status.PowerState).To(Equal(expectedPowerState)) + }) + }) + + }) + + DescribeTable("powerOffModes", + func(mode vmopv1.VirtualMachinePowerOpMode) { + vm.Spec.PowerOffMode = mode + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) + }, + Entry("hard", vmopv1.VirtualMachinePowerOpModeHard), + Entry("soft", vmopv1.VirtualMachinePowerOpModeSoft), + Entry("trySoft", vmopv1.VirtualMachinePowerOpModeTrySoft), + ) + + When("there is a config error", func() { + JustBeforeEach(func() { + vm.Spec.Bootstrap = &vmopv1.VirtualMachineBootstrapSpec{ + CloudInit: &vmopv1.VirtualMachineBootstrapCloudInitSpec{ + CloudConfig: &cloudinit.CloudConfig{ + RunCmd: json.RawMessage([]byte("invalid")), + }, + }, + } + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOn)) + }) + It("should still power off the VM", func() { + err := createOrUpdateVM(ctx, vmProvider, vm) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to reconcile config: updating state failed with failed to create bootstrap data")) + + // Do it again to update status. + Expect(createOrUpdateVM(ctx, vmProvider, vm)).ToNot(Succeed()) + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) + }) + }) + }) + + When("restarting the VM", func() { + var ( + oldLastRestartTime string + ) + + JustBeforeEach(func() { + oldLastRestartTime = getLastRestartTime(moVM) + vm.Spec.NextRestartTime = time.Now().UTC().Format(time.RFC3339Nano) + }) + + When("restartMode is hard", func() { + JustBeforeEach(func() { + vm.Spec.RestartMode = vmopv1.VirtualMachinePowerOpModeHard + }) + It("should restart the VM", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &moVM)).To(Succeed()) + newLastRestartTime := getLastRestartTime(moVM) + Expect(newLastRestartTime).ToNot(BeEmpty()) + Expect(newLastRestartTime).ToNot(Equal(oldLastRestartTime)) + }) + }) + When("restartMode is soft", func() { + JustBeforeEach(func() { + vm.Spec.RestartMode = vmopv1.VirtualMachinePowerOpModeSoft + }) + It("should return an error about lacking tools", func() { + Expect(testutil.ContainsError(createOrUpdateVM(ctx, vmProvider, vm), "failed to soft restart vm ServerFaultCode: ToolsUnavailable")).To(BeTrue()) + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &moVM)).To(Succeed()) + newLastRestartTime := getLastRestartTime(moVM) + Expect(newLastRestartTime).To(Equal(oldLastRestartTime)) + }) + }) + When("restartMode is trySoft", func() { + JustBeforeEach(func() { + vm.Spec.RestartMode = vmopv1.VirtualMachinePowerOpModeTrySoft + }) + It("should restart the VM", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &moVM)).To(Succeed()) + newLastRestartTime := getLastRestartTime(moVM) + Expect(newLastRestartTime).ToNot(BeEmpty()) + Expect(newLastRestartTime).ToNot(Equal(oldLastRestartTime)) + }) + }) + }) + + When("suspending the VM", func() { + JustBeforeEach(func() { + vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateSuspended + }) + When("power state should not be updated", func() { + const expectedPowerState = vmopv1.VirtualMachinePowerStateOn + When("vm is paused by devops", func() { + JustBeforeEach(func() { + vm.Annotations = map[string]string{ + vmopv1.PauseAnnotation: "true", + } + }) + It("should not change the power state", func() { + Expect(errors.Is(createOrUpdateVM(ctx, vmProvider, vm), vsphere.ErrIsPaused)).To(BeTrue()) + Expect(vm.Status.PowerState).To(Equal(expectedPowerState)) + }) + }) + When("vm is paused by admin", func() { + JustBeforeEach(func() { + vm.Annotations = map[string]string{ + vmopv1.PauseAnnotation: "true", + } + t, err := vcVM.Reconfigure(ctx, vimtypes.VirtualMachineConfigSpec{ + ExtraConfig: []vimtypes.BaseOptionValue{ + &vimtypes.OptionValue{ + Key: vmopv1.PauseVMExtraConfigKey, + Value: "true", + }, + }, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(t.Wait(ctx)).To(Succeed()) + }) + It("should not change the power state", func() { + Expect(errors.Is(createOrUpdateVM(ctx, vmProvider, vm), vsphere.ErrIsPaused)).To(BeTrue()) + Expect(vm.Status.PowerState).To(Equal(expectedPowerState)) + }) + }) + + When("vm has running task", func() { + var ( + reg *simulator.Registry + simCtx *simulator.Context + taskRef vimtypes.ManagedObjectReference + ) + + JustBeforeEach(func() { + simCtx = ctx.SimulatorContext() + reg = simCtx.Map + taskRef = reg.Put(&mo.Task{ + Info: vimtypes.TaskInfo{ + State: vimtypes.TaskInfoStateRunning, + DescriptionId: "fake.task.2", + }, + }).Reference() + + vmRef := vimtypes.ManagedObjectReference{ + Type: string(vimtypes.ManagedObjectTypeVirtualMachine), + Value: vm.Status.UniqueID, + } + + reg.WithLock( + simCtx, + vmRef, + func() { + vm := reg.Get(vmRef).(*simulator.VirtualMachine) + vm.RecentTask = append(vm.RecentTask, taskRef) + }) + + }) + + AfterEach(func() { + reg.Remove(simCtx, taskRef) + }) + + It("should not change the power state", func() { + Expect(errors.Is(createOrUpdateVM(ctx, vmProvider, vm), vsphere.ErrHasTask)).To(BeTrue()) + Expect(vm.Status.PowerState).To(Equal(expectedPowerState)) + }) + }) + }) + + When("suspendMode is hard", func() { + JustBeforeEach(func() { + vm.Spec.SuspendMode = vmopv1.VirtualMachinePowerOpModeHard + }) + It("should suspend the VM", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateSuspended)) + }) + }) + When("suspendMode is soft", func() { + JustBeforeEach(func() { + vm.Spec.SuspendMode = vmopv1.VirtualMachinePowerOpModeSoft + }) + It("should suspend the VM", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateSuspended)) + }) + }) + When("suspendMode is trySoft", func() { + JustBeforeEach(func() { + vm.Spec.SuspendMode = vmopv1.VirtualMachinePowerOpModeTrySoft + }) + It("should suspend the VM", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateSuspended)) + }) + }) + }) + }) + + When("vcVM is powered off", func() { + JustBeforeEach(func() { + vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOff + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) + }) + + When("power state is not changed", func() { + It("should not return an error", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) + }) + }) + + When("powering on the VM", func() { + + JustBeforeEach(func() { + vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOn + }) + + When("power state should not be updated", func() { + const expectedPowerState = vmopv1.VirtualMachinePowerStateOff + When("vm is paused by devops", func() { + JustBeforeEach(func() { + vm.Annotations = map[string]string{ + vmopv1.PauseAnnotation: "true", + } + }) + It("should not change the power state", func() { + Expect(errors.Is(createOrUpdateVM(ctx, vmProvider, vm), vsphere.ErrIsPaused)).To(BeTrue()) + Expect(vm.Status.PowerState).To(Equal(expectedPowerState)) + }) + }) + When("vm is paused by admin", func() { + JustBeforeEach(func() { + vm.Annotations = map[string]string{ + vmopv1.PauseAnnotation: "true", + } + t, err := vcVM.Reconfigure(ctx, vimtypes.VirtualMachineConfigSpec{ + ExtraConfig: []vimtypes.BaseOptionValue{ + &vimtypes.OptionValue{ + Key: vmopv1.PauseVMExtraConfigKey, + Value: "true", + }, + }, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(t.Wait(ctx)).To(Succeed()) + }) + It("should not change the power state", func() { + Expect(errors.Is(createOrUpdateVM(ctx, vmProvider, vm), vsphere.ErrIsPaused)).To(BeTrue()) + Expect(vm.Status.PowerState).To(Equal(expectedPowerState)) + }) + }) + + When("vm has running task", func() { + var ( + reg *simulator.Registry + simCtx *simulator.Context + taskRef vimtypes.ManagedObjectReference + ) + + JustBeforeEach(func() { + simCtx = ctx.SimulatorContext() + reg = simCtx.Map + taskRef = reg.Put(&mo.Task{ + Info: vimtypes.TaskInfo{ + State: vimtypes.TaskInfoStateRunning, + DescriptionId: "fake.task.3", + }, + }).Reference() + + vmRef := vimtypes.ManagedObjectReference{ + Type: string(vimtypes.ManagedObjectTypeVirtualMachine), + Value: vm.Status.UniqueID, + } + + reg.WithLock( + simCtx, + vmRef, + func() { + vm := reg.Get(vmRef).(*simulator.VirtualMachine) + vm.RecentTask = append(vm.RecentTask, taskRef) + }) + + }) + + AfterEach(func() { + reg.Remove(simCtx, taskRef) + }) + + It("should not change the power state", func() { + Expect(errors.Is(createOrUpdateVM(ctx, vmProvider, vm), vsphere.ErrHasTask)).To(BeTrue()) + Expect(vm.Status.PowerState).To(Equal(expectedPowerState)) + }) + }) + + }) + + When("there is a power on check annotation", func() { + JustBeforeEach(func() { + vm.Annotations = map[string]string{ + vmopv1.CheckAnnotationPowerOn + "/app": "reason", + } + }) + It("should not power on the VM", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) + }) + }) + + When("there is a apply power state change time annotation", func() { + JustBeforeEach(func() { + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.Features.VMGroups = true + }) + }) + + When("the time is in the future", func() { + JustBeforeEach(func() { + vm.Annotations = map[string]string{ + pkgconst.ApplyPowerStateTimeAnnotation: time.Now().UTC().Add(time.Minute).Format(time.RFC3339Nano), + } + }) + + It("should not power on the VM and requeue after remaining time", func() { + err := createOrUpdateVM(ctx, vmProvider, vm) + Expect(err).To(HaveOccurred()) + var requeueErr pkgerr.RequeueError + Expect(errors.As(err, &requeueErr)).To(BeTrue()) + Expect(requeueErr.After).To(BeNumerically("~", time.Minute, time.Second)) + }) + }) + + When("the time is in the past", func() { + JustBeforeEach(func() { + vm.Annotations = map[string]string{ + pkgconst.ApplyPowerStateTimeAnnotation: time.Now().UTC().Add(-time.Minute).Format(time.RFC3339Nano), + } + }) + + It("should power on the VM and remove the annotation", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOn)) + Expect(vm.Annotations).ToNot(HaveKey(pkgconst.ApplyPowerStateTimeAnnotation)) + }) + }) + }) + + const ( + oldDiskSizeBytes = int64(31457280) + newDiskSizeGi = 20 + newDiskSizeBytes = int64(newDiskSizeGi * 1024 * 1024 * 1024) + ) + + When("the boot disk size is changed for non-ISO VMs", func() { + JustBeforeEach(func() { + vmDevs := object.VirtualDeviceList(moVM.Config.Hardware.Device) + disks := vmDevs.SelectByType(&vimtypes.VirtualDisk{}) + Expect(disks).To(HaveLen(1)) + Expect(disks[0]).To(BeAssignableToTypeOf(&vimtypes.VirtualDisk{})) + diskCapacityBytes := disks[0].(*vimtypes.VirtualDisk).CapacityInBytes + Expect(diskCapacityBytes).To(Equal(oldDiskSizeBytes)) + + q := resource.MustParse(fmt.Sprintf("%dGi", newDiskSizeGi)) + vm.Spec.Advanced = &vmopv1.VirtualMachineAdvancedSpec{ + BootDiskCapacity: &q, + } + if vm.Spec.Hardware == nil { + vm.Spec.Hardware = &vmopv1.VirtualMachineHardwareSpec{} + } + vm.Spec.Hardware.Cdrom = nil + }) + It("should power on the VM with the boot disk resized", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOn)) + + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &moVM)).To(Succeed()) + vmDevs := object.VirtualDeviceList(moVM.Config.Hardware.Device) + disks := vmDevs.SelectByType(&vimtypes.VirtualDisk{}) + Expect(disks).To(HaveLen(1)) + Expect(disks[0]).To(BeAssignableToTypeOf(&vimtypes.VirtualDisk{})) + diskCapacityBytes := disks[0].(*vimtypes.VirtualDisk).CapacityInBytes + Expect(diskCapacityBytes).To(Equal(newDiskSizeBytes)) + }) + }) + + When("there are no NICs", func() { + JustBeforeEach(func() { + vm.Spec.Network.Interfaces = nil + }) + It("should power on the VM", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOn)) + }) + }) + + When("there is a single NIC", func() { + JustBeforeEach(func() { + vm.Spec.Network.Interfaces = []vmopv1.VirtualMachineNetworkInterfaceSpec{ + { + Name: "eth0", + Network: &vmopv1common.PartialObjectRef{ + Name: "VM Network", + }, + }, + } + }) + When("with networking disabled", func() { + JustBeforeEach(func() { + vm.Spec.Network.Disabled = true + }) + It("should power on the VM", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOn)) + }) + }) + }) + + When("VM.Spec.GuestID is changed", func() { + + When("the guest ID value is invalid", func() { + + JustBeforeEach(func() { + vm.Spec.GuestID = "invalid-guest-id" + }) + + It("should return an error and set the VM's Guest ID condition false", func() { + err := createOrUpdateVM(ctx, vmProvider, vm) + Expect(err.Error()).To(ContainSubstring("reconfigure VM task failed")) + + c := conditions.Get(vm, vmopv1.GuestIDReconfiguredCondition) + Expect(c).ToNot(BeNil()) + expectedCondition := conditions.FalseCondition( + vmopv1.GuestIDReconfiguredCondition, + "Invalid", + "The specified guest ID value is not supported: invalid-guest-id", + ) + Expect(*c).To(conditions.MatchCondition(*expectedCondition)) + }) + }) + + When("the guest ID value is valid", func() { + + JustBeforeEach(func() { + vm.Spec.GuestID = "vmwarePhoton64Guest" + }) + + It("should power on the VM with the specified guest ID", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOn)) + + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &moVM)).To(Succeed()) + Expect(moVM.Config.GuestId).To(Equal("vmwarePhoton64Guest")) + }) + }) + + When("the guest ID spec is removed", func() { + + JustBeforeEach(func() { + vm.Spec.GuestID = "" + }) + + It("should clear the VM guest ID condition if previously set", func() { + vm.Status.Conditions = []metav1.Condition{ + { + Type: vmopv1.GuestIDReconfiguredCondition, + Status: metav1.ConditionFalse, + }, + } + + // Customize + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOn)) + + Expect(conditions.Get(vm, vmopv1.GuestIDReconfiguredCondition)).To(BeNil()) + }) + }) + }) + + When("VM has CD-ROM", func() { + + const ( + vmiName = "vmi-iso" + vmiKind = "VirtualMachineImage" + vmiFileName = "dummy.iso" + ) + + JustBeforeEach(func() { + vm.Spec.Hardware = &vmopv1.VirtualMachineHardwareSpec{ + Cdrom: []vmopv1.VirtualMachineCdromSpec{ + { + Name: "cdrom1", + Image: vmopv1.VirtualMachineImageRef{ + Name: vmiName, + Kind: vmiKind, + }, + AllowGuestControl: ptr.To(true), + Connected: ptr.To(true), + }, + }, + } + testConfig.WithContentLibrary = true + }) + + JustBeforeEach(func() { + // Add required objects to get CD-ROM backing file name. + objs := builder.DummyImageAndItemObjectsForCdromBacking( + vmiName, + vm.Namespace, + vmiKind, + vmiFileName, + ctx.ContentLibraryIsoItemID, + true, + true, + resource.MustParse("100Mi"), + true, + true, + "ISO") + for _, obj := range objs { + Expect(ctx.Client.Create(ctx, obj)).To(Succeed()) + } + }) + + assertPowerOnVMWithCDROM := func() { + ExpectWithOffset(1, createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + ExpectWithOffset(1, vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOn)) + + ExpectWithOffset(1, vcVM.Properties(ctx, vcVM.Reference(), nil, &moVM)).To(Succeed()) + + cdromDeviceList := object.VirtualDeviceList(moVM.Config.Hardware.Device).SelectByType(&vimtypes.VirtualCdrom{}) + ExpectWithOffset(1, cdromDeviceList).To(HaveLen(1)) + cdrom := cdromDeviceList[0].(*vimtypes.VirtualCdrom) + ExpectWithOffset(1, cdrom.Connectable.StartConnected).To(BeTrue()) + ExpectWithOffset(1, cdrom.Connectable.Connected).To(BeTrue()) + ExpectWithOffset(1, cdrom.Connectable.AllowGuestControl).To(BeTrue()) + ExpectWithOffset(1, cdrom.ControllerKey).ToNot(BeZero()) + ExpectWithOffset(1, cdrom.UnitNumber).ToNot(BeNil()) + ExpectWithOffset(1, cdrom.Backing).To(BeAssignableToTypeOf(&vimtypes.VirtualCdromIsoBackingInfo{})) + backing := cdrom.Backing.(*vimtypes.VirtualCdromIsoBackingInfo) + ExpectWithOffset(1, backing.FileName).To(Equal(vmiFileName)) + } + + assertNotPowerOnVMWithCDROM := func() { + err := createOrUpdateVM(ctx, vmProvider, vm) + ExpectWithOffset(1, err).To(HaveOccurred()) + ExpectWithOffset(1, err.Error()).To(ContainSubstring("no CD-ROM is found for image ref")) + } + + It("should power on the VM with expected CD-ROM device", assertPowerOnVMWithCDROM) + + When("FSS Resize is enabled", func() { + JustBeforeEach(func() { + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.Features.VMResize = true + }) + }) + It("should not power on the VM with expected CD-ROM device", assertNotPowerOnVMWithCDROM) + }) + + When("FSS Resize CPU & Memory is enabled", func() { + JustBeforeEach(func() { + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.Features.VMResizeCPUMemory = true + }) + }) + It("should power on the VM with expected CD-ROM device", assertPowerOnVMWithCDROM) + }) + + When("the boot disk size is changed for VM with CD-ROM", func() { + + JustBeforeEach(func() { + vmDevs := object.VirtualDeviceList(moVM.Config.Hardware.Device) + disks := vmDevs.SelectByType(&vimtypes.VirtualDisk{}) + Expect(disks).To(HaveLen(1)) + Expect(disks[0]).To(BeAssignableToTypeOf(&vimtypes.VirtualDisk{})) + diskCapacityBytes := disks[0].(*vimtypes.VirtualDisk).CapacityInBytes + Expect(diskCapacityBytes).To(Equal(oldDiskSizeBytes)) + + q := resource.MustParse(fmt.Sprintf("%dGi", newDiskSizeGi)) + vm.Spec.Advanced = &vmopv1.VirtualMachineAdvancedSpec{ + BootDiskCapacity: &q, + } + }) + + It("should power on the VM without the boot disk resized", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOn)) + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &moVM)).To(Succeed()) + + vmDevs := object.VirtualDeviceList(moVM.Config.Hardware.Device) + disks := vmDevs.SelectByType(&vimtypes.VirtualDisk{}) + Expect(disks).To(HaveLen(1)) + Expect(disks[0]).To(BeAssignableToTypeOf(&vimtypes.VirtualDisk{})) + diskCapacityBytes := disks[0].(*vimtypes.VirtualDisk).CapacityInBytes + Expect(diskCapacityBytes).To(Equal(oldDiskSizeBytes)) + }) + }) + }) + }) + + When("suspending the VM", func() { + JustBeforeEach(func() { + vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateSuspended + }) + When("suspendMode is hard", func() { + JustBeforeEach(func() { + vm.Spec.SuspendMode = vmopv1.VirtualMachinePowerOpModeHard + }) + It("should not suspend the VM", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) + }) + }) + When("suspendMode is soft", func() { + JustBeforeEach(func() { + vm.Spec.SuspendMode = vmopv1.VirtualMachinePowerOpModeSoft + }) + It("should not suspend the VM", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) + }) + }) + When("suspendMode is trySoft", func() { + JustBeforeEach(func() { + vm.Spec.SuspendMode = vmopv1.VirtualMachinePowerOpModeTrySoft + }) + It("should not suspend the VM", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) + }) + }) + }) + + When("there is a config error", func() { + JustBeforeEach(func() { + vm.Spec.Bootstrap = &vmopv1.VirtualMachineBootstrapSpec{ + CloudInit: &vmopv1.VirtualMachineBootstrapCloudInitSpec{ + CloudConfig: &cloudinit.CloudConfig{ + RunCmd: json.RawMessage([]byte("invalid")), + }, + }, + } + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) + }) + It("should not power on the VM", func() { + err := createOrUpdateVM(ctx, vmProvider, vm) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to reconcile config: updating state failed with failed to create bootstrap data")) + + // Do it again to update status. + Expect(createOrUpdateVM(ctx, vmProvider, vm)).ToNot(Succeed()) + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) + }) + }) + }) + + When("vcVM is suspended", func() { + JustBeforeEach(func() { + vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateSuspended + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateSuspended)) + }) + + When("power state is not changed", func() { + It("should not return an error", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + }) + }) + + When("powering on the VM", func() { + + JustBeforeEach(func() { + vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOn + }) + + It("should power on the VM", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOn)) + }) + + When("there is a power on check annotation", func() { + JustBeforeEach(func() { + vm.Annotations = map[string]string{ + vmopv1.CheckAnnotationPowerOn + "/app": "reason", + } + }) + It("should not power on the VM", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateSuspended)) + }) + }) + }) + + When("powering off the VM", func() { + JustBeforeEach(func() { + vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOff + }) + When("powerOffMode is hard", func() { + JustBeforeEach(func() { + vm.Spec.PowerOffMode = vmopv1.VirtualMachinePowerOpModeHard + }) + It("should power off the VM", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) + }) + }) + When("powerOffMode is soft", func() { + JustBeforeEach(func() { + vm.Spec.PowerOffMode = vmopv1.VirtualMachinePowerOpModeSoft + }) + It("should not power off the VM", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateSuspended)) + }) + }) + When("powerOffMode is trySoft", func() { + JustBeforeEach(func() { + vm.Spec.PowerOffMode = vmopv1.VirtualMachinePowerOpModeTrySoft + }) + It("should power off the VM", func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) + }) + }) + }) + }) +} diff --git a/pkg/providers/vsphere/vmprovider_vm_resize_test.go b/pkg/providers/vsphere/vmprovider_vm_resize_test.go index 09f0c394d..a8bdbbecc 100644 --- a/pkg/providers/vsphere/vmprovider_vm_resize_test.go +++ b/pkg/providers/vsphere/vmprovider_vm_resize_test.go @@ -145,7 +145,7 @@ func vmResizeTests() { assertExpectedReservationFields(o, 0, -1, 0, -1) } - DescribeTableSubtree("Resize VM", + DescribeTableSubtree("Resize", func(fullResize bool) { var ( @@ -678,7 +678,7 @@ func vmResizeTests() { Entry("CPU & Memory", false), ) - Context("Devops Overrides", func() { + Context("Overrides", func() { var ( vm *vmopv1.VirtualMachine diff --git a/pkg/providers/vsphere/vmprovider_vm_setresourcepolicy_test.go b/pkg/providers/vsphere/vmprovider_vm_setresourcepolicy_test.go new file mode 100644 index 000000000..80a64a298 --- /dev/null +++ b/pkg/providers/vsphere/vmprovider_vm_setresourcepolicy_test.go @@ -0,0 +1,199 @@ +// © Broadcom. All Rights Reserved. +// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package vsphere_test + +import ( + "context" + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "sigs.k8s.io/controller-runtime/pkg/client" + + corev1 "k8s.io/api/core/v1" + + "github.com/vmware/govmomi/vapi/cluster" + vimtypes "github.com/vmware/govmomi/vim25/types" + + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha6" + "github.com/vmware-tanzu/vm-operator/pkg/conditions" + pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + ctxop "github.com/vmware-tanzu/vm-operator/pkg/context/operation" + "github.com/vmware-tanzu/vm-operator/pkg/providers" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" + "github.com/vmware-tanzu/vm-operator/pkg/util/kube/cource" + "github.com/vmware-tanzu/vm-operator/pkg/util/ovfcache" + "github.com/vmware-tanzu/vm-operator/test/builder" +) + +func vmSetResourcePolicyTests() { + var ( + parentCtx context.Context + initObjects []client.Object + testConfig builder.VCSimTestConfig + ctx *builder.TestContextForVCSim + vmProvider providers.VirtualMachineProviderInterface + nsInfo builder.WorkloadNamespaceInfo + + vm *vmopv1.VirtualMachine + vmClass *vmopv1.VirtualMachineClass + ) + + BeforeEach(func() { + parentCtx = pkgcfg.NewContextWithDefaultConfig() + parentCtx = ctxop.WithContext(parentCtx) + parentCtx = ovfcache.WithContext(parentCtx) + parentCtx = cource.WithContext(parentCtx) + pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { + config.AsyncCreateEnabled = false + config.AsyncSignalEnabled = false + }) + testConfig = builder.VCSimTestConfig{ + WithContentLibrary: true, + } + + vmClass = builder.DummyVirtualMachineClassGenName() + vm = builder.DummyBasicVirtualMachine("test-vm", "") + + if vm.Spec.Network == nil { + vm.Spec.Network = &vmopv1.VirtualMachineNetworkSpec{} + } + vm.Spec.Network.Disabled = true + }) + + JustBeforeEach(func() { + ctx = suite.NewTestContextForVCSimWithParentContext( + parentCtx, testConfig, initObjects...) + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.MaxDeployThreadsOnProvider = 1 + }) + vmProvider = vsphere.NewVSphereVMProviderFromClient( + ctx, ctx.Client, ctx.Recorder) + nsInfo = ctx.CreateWorkloadNamespace() + + vmClass.Namespace = nsInfo.Namespace + Expect(ctx.Client.Create(ctx, vmClass)).To(Succeed()) + + clusterVMI1 := &vmopv1.ClusterVirtualMachineImage{} + + if testConfig.WithContentLibrary { + Expect(ctx.Client.Get( + ctx, client.ObjectKey{Name: ctx.ContentLibraryItem1Name}, + clusterVMI1)).To(Succeed()) + } else { + vsphere.SkipVMImageCLProviderCheck = true + clusterVMI1 = builder.DummyClusterVirtualMachineImage("DC0_C0_RP0_VM0") + Expect(ctx.Client.Create(ctx, clusterVMI1)).To(Succeed()) + conditions.MarkTrue(clusterVMI1, vmopv1.ReadyConditionType) + Expect(ctx.Client.Status().Update(ctx, clusterVMI1)).To(Succeed()) + } + + vm.Namespace = nsInfo.Namespace + vm.Spec.ClassName = vmClass.Name + vm.Spec.ImageName = clusterVMI1.Name + vm.Spec.Image.Kind = cvmiKind + vm.Spec.Image.Name = clusterVMI1.Name + vm.Spec.StorageClass = ctx.StorageClassName + + Expect(ctx.Client.Create(ctx, vm)).To(Succeed()) + }) + + AfterEach(func() { + vsphere.SkipVMImageCLProviderCheck = false + + if vm != nil && + !pkgcfg.FromContext(ctx).Features.BringYourOwnEncryptionKey { + By("Assert vm.Status.Crypto is nil when BYOK is disabled", func() { + Expect(vm.Status.Crypto).To(BeNil()) + }) + } + + vmClass = nil + vm = nil + + ctx.AfterEach() + ctx = nil + initObjects = nil + vmProvider = nil + nsInfo = builder.WorkloadNamespaceInfo{} + }) + + var resourcePolicy *vmopv1.VirtualMachineSetResourcePolicy + + JustBeforeEach(func() { + resourcePolicyName := "test-policy" + resourcePolicy = getVirtualMachineSetResourcePolicy(resourcePolicyName, nsInfo.Namespace) + Expect(vmProvider.CreateOrUpdateVirtualMachineSetResourcePolicy(ctx, resourcePolicy)).To(Succeed()) + Expect(ctx.Client.Create(ctx, resourcePolicy)).To(Succeed()) + + vm.Annotations["vsphere-cluster-module-group"] = resourcePolicy.Spec.ClusterModuleGroups[0] + if vm.Spec.Reserved == nil { + vm.Spec.Reserved = &vmopv1.VirtualMachineReservedSpec{} + } + vm.Spec.Reserved.ResourcePolicyName = resourcePolicy.Name + }) + + AfterEach(func() { + resourcePolicy = nil + }) + + When("a cluster module is specified without resource policy", func() { + JustBeforeEach(func() { + vm.Spec.Reserved.ResourcePolicyName = "" + }) + + It("returns error", func() { + _, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("cannot set cluster module without resource policy")) + }) + }) + + It("VM is created in child Folder and ResourcePool", func() { + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + By("has expected condition", func() { + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionVMSetResourcePolicyReady)).To(BeTrue()) + }) + + By("has expected inventory path", func() { + Expect(vcVM.InventoryPath).To(HaveSuffix( + fmt.Sprintf("/%s/%s/%s", nsInfo.Namespace, resourcePolicy.Spec.Folder, vm.Name))) + }) + + By("has expected namespace resource pool", func() { + rp, err := vcVM.ResourcePool(ctx) + Expect(err).ToNot(HaveOccurred()) + childRP := ctx.GetResourcePoolForNamespace( + nsInfo.Namespace, + vm.Labels[corev1.LabelTopologyZone], + resourcePolicy.Spec.ResourcePool.Name) + Expect(childRP).ToNot(BeNil()) + Expect(rp.Reference().Value).To(Equal(childRP.Reference().Value)) + }) + }) + + It("Cluster Modules", func() { + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + var members []vimtypes.ManagedObjectReference + for i := range resourcePolicy.Status.ClusterModules { + m, err := cluster.NewManager(ctx.RestClient).ListModuleMembers(ctx, resourcePolicy.Status.ClusterModules[i].ModuleUuid) + Expect(err).ToNot(HaveOccurred()) + members = append(m, members...) + } + + Expect(members).To(ContainElements(vcVM.Reference())) + }) + + It("Returns error with non-existence cluster module", func() { + clusterModName := "bogusClusterMod" + vm.Annotations["vsphere-cluster-module-group"] = clusterModName + err := createOrUpdateVM(ctx, vmProvider, vm) + Expect(err).To(MatchError("VirtualMachineSetResourcePolicy cluster module is not ready")) + }) +} diff --git a/pkg/providers/vsphere/vmprovider_vm_snapshot_test.go b/pkg/providers/vsphere/vmprovider_vm_snapshot_test.go index 6fd1454ad..e0c3ab0aa 100644 --- a/pkg/providers/vsphere/vmprovider_vm_snapshot_test.go +++ b/pkg/providers/vsphere/vmprovider_vm_snapshot_test.go @@ -5,571 +5,821 @@ package vsphere_test import ( - "path/filepath" - "time" + "context" + "errors" + "fmt" - "github.com/go-logr/logr" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + corev1 "k8s.io/api/core/v1" + + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "github.com/vmware/govmomi/object" "github.com/vmware/govmomi/vim25/mo" + vimtypes "github.com/vmware/govmomi/vim25/types" vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha6" + backupapi "github.com/vmware-tanzu/vm-operator/pkg/backup/api" "github.com/vmware-tanzu/vm-operator/pkg/conditions" pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" - pkgctx "github.com/vmware-tanzu/vm-operator/pkg/context" + pkgconst "github.com/vmware-tanzu/vm-operator/pkg/constants" + ctxop "github.com/vmware-tanzu/vm-operator/pkg/context/operation" pkgerr "github.com/vmware-tanzu/vm-operator/pkg/errors" "github.com/vmware-tanzu/vm-operator/pkg/providers" "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/virtualmachine" + pkgutil "github.com/vmware-tanzu/vm-operator/pkg/util" kubeutil "github.com/vmware-tanzu/vm-operator/pkg/util/kube" - vmconfunmanagedvolsfil "github.com/vmware-tanzu/vm-operator/pkg/vmconfig/volumes/unmanaged/backfill" - vmconfunmanagedvolsreg "github.com/vmware-tanzu/vm-operator/pkg/vmconfig/volumes/unmanaged/register" + "github.com/vmware-tanzu/vm-operator/pkg/util/kube/cource" + "github.com/vmware-tanzu/vm-operator/pkg/util/ovfcache" "github.com/vmware-tanzu/vm-operator/test/builder" - "github.com/vmware-tanzu/vm-operator/test/testutil" ) func vmSnapshotTests() { - const ( - dummySnapshot = "dummy-snapshot" - ) - var ( - initObjects []ctrlclient.Object + parentCtx context.Context + initObjects []client.Object + testConfig builder.VCSimTestConfig ctx *builder.TestContextForVCSim vmProvider providers.VirtualMachineProviderInterface nsInfo builder.WorkloadNamespaceInfo - vmSnapshot *vmopv1.VirtualMachineSnapshot - vcVM *object.VirtualMachine - vm *vmopv1.VirtualMachine - vmCtx pkgctx.VirtualMachineContext + + vm *vmopv1.VirtualMachine + vmClass *vmopv1.VirtualMachineClass + + zoneName string ) BeforeEach(func() { - ctx = suite.NewTestContextForVCSim(builder.VCSimTestConfig{}, initObjects...) - vmProvider = vsphere.NewVSphereVMProviderFromClient(ctx, ctx.Client, ctx.Recorder) + parentCtx = pkgcfg.NewContextWithDefaultConfig() + parentCtx = ctxop.WithContext(parentCtx) + parentCtx = ovfcache.WithContext(parentCtx) + parentCtx = cource.WithContext(parentCtx) + pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { + config.AsyncCreateEnabled = false + config.AsyncSignalEnabled = false + }) + testConfig = builder.VCSimTestConfig{ + WithContentLibrary: true, + } + + vmClass = builder.DummyVirtualMachineClassGenName() + vm = builder.DummyBasicVirtualMachine("test-vm", "") + + if vm.Spec.Network == nil { + vm.Spec.Network = &vmopv1.VirtualMachineNetworkSpec{} + } + vm.Spec.Network.Disabled = true + }) + + JustBeforeEach(func() { + ctx = suite.NewTestContextForVCSimWithParentContext( + parentCtx, testConfig, initObjects...) + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.MaxDeployThreadsOnProvider = 1 + }) + vmProvider = vsphere.NewVSphereVMProviderFromClient( + ctx, ctx.Client, ctx.Recorder) nsInfo = ctx.CreateWorkloadNamespace() - var err error - vcVM, err = ctx.Finder.VirtualMachine(ctx, "DC0_C0_RP0_VM0") - Expect(err).ToNot(HaveOccurred()) - Expect(vcVM).ToNot(BeNil()) + vmClass.Namespace = nsInfo.Namespace + Expect(ctx.Client.Create(ctx, vmClass)).To(Succeed()) + + clusterVMI1 := &vmopv1.ClusterVirtualMachineImage{} + + if testConfig.WithContentLibrary { + Expect(ctx.Client.Get( + ctx, client.ObjectKey{Name: ctx.ContentLibraryItem1Name}, + clusterVMI1)).To(Succeed()) + } else { + vsphere.SkipVMImageCLProviderCheck = true + clusterVMI1 = builder.DummyClusterVirtualMachineImage("DC0_C0_RP0_VM0") + Expect(ctx.Client.Create(ctx, clusterVMI1)).To(Succeed()) + conditions.MarkTrue(clusterVMI1, vmopv1.ReadyConditionType) + Expect(ctx.Client.Status().Update(ctx, clusterVMI1)).To(Succeed()) + } + + vm.Namespace = nsInfo.Namespace + vm.Spec.ClassName = vmClass.Name + vm.Spec.ImageName = clusterVMI1.Name + vm.Spec.Image.Kind = cvmiKind + vm.Spec.Image.Name = clusterVMI1.Name + vm.Spec.StorageClass = ctx.StorageClassName - By("Creating VM CR") - vm = builder.DummyBasicVirtualMachine(dummySnapshot, nsInfo.Namespace) - vm.Status.UniqueID = vcVM.Reference().Value Expect(ctx.Client.Create(ctx, vm)).To(Succeed()) - By("Creating snapshot CR") - vmSnapshot = builder.DummyVirtualMachineSnapshot(nsInfo.Namespace, dummySnapshot, vcVM.Name()) - Expect(ctx.Client.Create(ctx, vmSnapshot)).To(Succeed()) + zoneName = ctx.GetFirstZoneName() + vm.Labels[corev1.LabelTopologyZone] = zoneName + Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) + }) - // TODO (lubron): Add FCD to the VM and test the snapshot size once - // vcsim has support to show attached disk as device + AfterEach(func() { + vsphere.SkipVMImageCLProviderCheck = false - By("Creating snapshot on vSphere") - logger := testutil.GinkgoLogr(5) - vmCtx = pkgctx.VirtualMachineContext{ - Context: logr.NewContext(ctx, logger), - Logger: logger.WithValues("vmName", vcVM.Name()), - VM: vm, - } - args := virtualmachine.SnapshotArgs{ - VMCtx: vmCtx, - VMSnapshot: *vmSnapshot, - VcVM: vcVM, + if vm != nil && + !pkgcfg.FromContext(ctx).Features.BringYourOwnEncryptionKey { + By("Assert vm.Status.Crypto is nil when BYOK is disabled", func() { + Expect(vm.Status.Crypto).To(BeNil()) + }) } - snapMo, err := virtualmachine.CreateSnapshot(args) - Expect(err).ToNot(HaveOccurred()) - Expect(snapMo).ToNot(BeNil()) - }) - AfterEach(func() { + vmClass = nil + vm = nil + ctx.AfterEach() ctx = nil initObjects = nil vmProvider = nil - vmSnapshot = nil - vmCtx = pkgctx.VirtualMachineContext{} - vm = nil nsInfo = builder.WorkloadNamespaceInfo{} }) - Context("GetSnapshotSize", func() { - It("should return the size of the snapshot", func() { - size, err := vmProvider.GetSnapshotSize(ctx, vmSnapshot.Name, vm) + var ( + vmSnapshot *vmopv1.VirtualMachineSnapshot + ) + + BeforeEach(func() { + testConfig.WithVMSnapshots = true + vmSnapshot = builder.DummyVirtualMachineSnapshot("", "test-revert-snap", vm.Name) + }) + + JustBeforeEach(func() { + vmSnapshot.Namespace = nsInfo.Namespace + }) + + Context("findDesiredSnapshot error handling", func() { + It("should return regular error (not NoRequeueError) when multiple snapshots exist", func() { + // Create VM first to get vcVM reference + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) Expect(err).ToNot(HaveOccurred()) - // Since we only have one snapshot, the size should be same as the vm - var moVM mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), []string{"layoutEx"}, &moVM)).To(Succeed()) - var total int64 - for _, file := range moVM.LayoutEx.File { - switch filepath.Ext(file.Name) { - case ".vmdk", ".vmsn", ".vmem": - total += file.Size - } - } - Expect(size).To(Equal(total)) - }) + // Create multiple snapshots with the same name + task, err := vcVM.CreateSnapshot(ctx, vmSnapshot.Name, "first snapshot", false, false) + Expect(err).ToNot(HaveOccurred()) + Expect(task.Wait(ctx)).To(Succeed()) - When("there is issue finding vm", func() { - BeforeEach(func() { - vm.Status.UniqueID = "" - }) - It("should return error", func() { - size, err := vmProvider.GetSnapshotSize(ctx, vmSnapshot.Name, vm) - Expect(err).To(HaveOccurred()) - Expect(size).To(BeZero()) - }) - }) + task, err = vcVM.CreateSnapshot(ctx, vmSnapshot.Name, "second snapshot", false, false) + Expect(err).ToNot(HaveOccurred()) + Expect(task.Wait(ctx)).To(Succeed()) - When("there is issue finding snapshot", func() { - BeforeEach(func() { - vmSnapshot.Name = "" - }) - It("should return error", func() { - size, err := vmProvider.GetSnapshotSize(ctx, vmSnapshot.Name, vm) - Expect(err).To(HaveOccurred()) - Expect(size).To(BeZero()) - }) + // Mark the snapshot as ready. + conditions.MarkTrue(vmSnapshot, vmopv1.VirtualMachineSnapshotReadyCondition) + // Create the snapshot CR to which the VM should revert + Expect(ctx.Client.Create(ctx, vmSnapshot)).To(Succeed()) + + // Snapshot should be owned by the VM resource. + o := vmopv1.VirtualMachine{} + Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(vm), &o)).To(Succeed()) + Expect(controllerutil.SetOwnerReference(&o, vmSnapshot, ctx.Scheme)).To(Succeed()) + Expect(ctx.Client.Update(ctx, vmSnapshot)).To(Succeed()) + + vm.Spec.CurrentSnapshotName = vmSnapshot.Name + + // This should return an error because findDesiredSnapshot should return an error + // when there are multiple snapshots with the same name + err = createOrUpdateVM(ctx, vmProvider, vm) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("resolves to 2 snapshots")) + + // Verify that the error causes a requeue (not a NoRequeueError) + Expect(pkgerr.IsNoRequeueError(err)).To(BeFalse(), "Multiple snapshots error should cause requeue") }) }) - Context("DeleteSnapshot", func() { - var ( - deleted bool - err error - ) - - JustBeforeEach(func() { - deleted, err = vmProvider.DeleteSnapshot(ctx, vmSnapshot, vm, true, nil) + Context("when VM has no snapshots", func() { + BeforeEach(func() { + vm.Spec.CurrentSnapshotName = vmSnapshot.Name }) - It("should return false and no error", func() { - Expect(deleted).To(BeFalse()) - Expect(err).NotTo(HaveOccurred()) - snapMoRef, err := vcVM.FindSnapshot(ctx, dummySnapshot) + It("should not trigger a revert (new snapshot workflow)", func() { + // Create the snapshot CR but don't create actual vCenter snapshot + conditions.MarkTrue(vmSnapshot, vmopv1.VirtualMachineSnapshotReadyCondition) + Expect(ctx.Client.Create(ctx, vmSnapshot)).To(Succeed()) + + // Snapshot should be owned by the VM resource. + o := vmopv1.VirtualMachine{} + Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(vm), &o)).To(Succeed()) + Expect(controllerutil.SetOwnerReference(&o, vmSnapshot, ctx.Scheme)).To(Succeed()) + Expect(ctx.Client.Update(ctx, vmSnapshot)).To(Succeed()) + + err := createOrUpdateVM(ctx, vmProvider, vm) Expect(err).To(HaveOccurred()) - Expect(snapMoRef).To(BeNil()) + Expect(err.Error()).To(ContainSubstring("no snapshots for this VM")) }) + }) - Context("VM is not found", func() { - BeforeEach(func() { - vm.Status.UniqueID = "" - }) - It("should return true and no error", func() { - Expect(deleted).To(BeTrue()) - Expect(err).NotTo(HaveOccurred()) - snapMoRef, err := vcVM.FindSnapshot(ctx, dummySnapshot) - Expect(err).NotTo(HaveOccurred()) - Expect(snapMoRef).NotTo(BeNil()) - }) + Context("when desired snapshot CR doesn't exist", func() { + BeforeEach(func() { + vm.Spec.CurrentSnapshotName = vmSnapshot.Name }) - Context("snapshot not found", func() { - BeforeEach(func() { - By("Deleting snapshot in advance") - Expect(virtualmachine.DeleteSnapshot(virtualmachine.SnapshotArgs{ - VMCtx: vmCtx, - VMSnapshot: *vmSnapshot, - VcVM: vcVM, - })).To(Succeed()) - }) - It("should return false and no error", func() { - Expect(deleted).To(BeFalse()) - Expect(err).NotTo(HaveOccurred()) - }) + It("should fail with snapshot CR not found error", func() { + err := createOrUpdateVM(ctx, vmProvider, vm) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("virtualmachinesnapshots.vmoperator.vmware.com \"test-revert-snap\" not found")) + + Expect(conditions.IsFalse(vm, + vmopv1.VirtualMachineSnapshotRevertSucceeded, + )).To(BeTrue()) + Expect(conditions.GetReason(vm, + vmopv1.VirtualMachineSnapshotRevertSucceeded, + )).To(Equal(vmopv1.VirtualMachineSnapshotRevertFailedReason)) }) }) - Context("SyncVMSnapshotTreeStatus", func() { - It("should sync the VM's current and root snapshots status", func() { - Expect(vmProvider.SyncVMSnapshotTreeStatus(ctx, vm)).To(Succeed()) - Expect(vm.Status.CurrentSnapshot).ToNot(BeNil()) - Expect(vm.Status.CurrentSnapshot.Type).To(Equal(vmopv1.VirtualMachineSnapshotReferenceTypeManaged)) - Expect(vm.Status.CurrentSnapshot.Name).To(Equal(vmSnapshot.Name)) - Expect(vm.Status.RootSnapshots).To(HaveLen(1)) - Expect(vm.Status.RootSnapshots[0].Name).To(Equal(vmSnapshot.Name)) - Expect(vm.Status.RootSnapshots[0].Type).To(Equal(vmopv1.VirtualMachineSnapshotReferenceTypeManaged)) + Context("when desired snapshot CR is not ready", func() { + BeforeEach(func() { + vm.Spec.CurrentSnapshotName = vmSnapshot.Name }) - When("VM is not found", func() { - BeforeEach(func() { - vm.Status.UniqueID = "" - }) - It("should return error", func() { - Expect(vmProvider.SyncVMSnapshotTreeStatus(ctx, vm)).NotTo(Succeed()) - }) + JustBeforeEach(func() { + // Create snapshot CR but don't mark it as ready. + Expect(ctx.Client.Create(ctx, vmSnapshot)).To(Succeed()) + + // Snapshot should be owned by the VM resource. + o := vmopv1.VirtualMachine{} + Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(vm), &o)).To(Succeed()) + Expect(controllerutil.SetOwnerReference(&o, vmSnapshot, ctx.Scheme)).To(Succeed()) + Expect(ctx.Client.Update(ctx, vmSnapshot)).To(Succeed()) }) - When("there is no snapshot", func() { - BeforeEach(func() { - Expect(virtualmachine.DeleteSnapshot(virtualmachine.SnapshotArgs{ - VMCtx: vmCtx, - VMSnapshot: *vmSnapshot, - VcVM: vcVM, - })).To(Succeed()) + When("snapshot is not created", func() { + It("should fail with snapshot CR not ready error", func() { + err := createOrUpdateVM(ctx, vmProvider, vm) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring( + fmt.Sprintf("skipping revert for not-ready snapshot %q", + vmSnapshot.Name))) }) - It("should show expected current snapshot and root snapshots", func() { - Expect(vmProvider.SyncVMSnapshotTreeStatus(ctx, vm)).To(Succeed()) - Expect(vm.Status.CurrentSnapshot).To(BeNil()) - Expect(vm.Status.RootSnapshots).To(BeNil()) + }) + + When("snapshot is created but not ready", func() { + It("should fail with snapshot CR not ready error", func() { + // Mark the snapshot as created but not ready. + conditions.MarkTrue(vmSnapshot, vmopv1.VirtualMachineSnapshotCreatedCondition) + Expect(ctx.Client.Status().Update(ctx, vmSnapshot)).To(Succeed()) + + err := createOrUpdateVM(ctx, vmProvider, vm) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring( + fmt.Sprintf("skipping revert for not-ready snapshot %q", + vmSnapshot.Name))) }) }) }) - Context("ReconcileCurrentSnapshot", func() { - var ( - snapshot1 *vmopv1.VirtualMachineSnapshot - snapshot2 *vmopv1.VirtualMachineSnapshot - - verifyK8sVMSnapshot = func(name, namespace string, isCreated bool) { - GinkgoHelper() - vmSnapshot := &vmopv1.VirtualMachineSnapshot{} - Expect(ctx.Client.Get(ctx, ctrlclient.ObjectKey{ - Name: name, - Namespace: namespace, - }, vmSnapshot)).To(Succeed()) - Expect(conditions.IsTrue(vmSnapshot, vmopv1.VirtualMachineSnapshotCreatedCondition)).To(Equal(isCreated)) - } + Context("revert to current snapshot", func() { + It("should succeed", func() { + // Create snapshot CR to trigger a snapshot workflow. + Expect(ctx.Client.Create(ctx, vmSnapshot)).To(Succeed()) - verifyNoVcVMSnapshot = func() { - GinkgoHelper() - var moVM mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), []string{"snapshot"}, &moVM)).To(Succeed()) - Expect(moVM.Snapshot).To(BeNil()) - } - ) + // Snapshot should be owned by the VM resource. + o := vmopv1.VirtualMachine{} + Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(vm), &o)).To(Succeed()) + Expect(controllerutil.SetOwnerReference(&o, vmSnapshot, ctx.Scheme)).To(Succeed()) + Expect(ctx.Client.Update(ctx, vmSnapshot)).To(Succeed()) + // Create VM so snapshot is also created. + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - BeforeEach(func() { - By("Deleting the snapshot on vSphere created in outer BeforeEach") - Expect(virtualmachine.DeleteSnapshot(virtualmachine.SnapshotArgs{ - VMCtx: vmCtx, - VMSnapshot: *vmSnapshot, - VcVM: vcVM, - })).To(Succeed()) - - By("Deleting the snapshot CR created in outer BeforeEach") - Expect(ctx.Client.Delete(ctx, vmSnapshot)).To(Succeed()) - }) + // Mark the snapshot as ready so that revert can proceed. + Expect(ctx.Client.Get(ctx, + client.ObjectKeyFromObject(vmSnapshot), vmSnapshot)).To(Succeed()) + conditions.MarkTrue(vmSnapshot, vmopv1.VirtualMachineSnapshotReadyCondition) + Expect(ctx.Client.Status().Update(ctx, vmSnapshot)).To(Succeed()) - AfterEach(func() { - snapshot1 = nil - snapshot2 = nil - }) + // Set desired snapshot to point to the above snapshot. + vm.Spec.CurrentSnapshotName = vmSnapshot.Name - When("no snapshots exist", func() { - It("should complete without error", func() { - Expect(vsphere.ReconcileCurrentSnapshot(vmCtx, ctx.Client, vcVM)).To(Succeed()) - verifyNoVcVMSnapshot() - }) + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + + // Verify VM status reflects current snapshot. + Expect(vm.Status.CurrentSnapshot).ToNot(BeNil()) + Expect(vm.Status.CurrentSnapshot.Type).To(Equal(vmopv1.VirtualMachineSnapshotReferenceTypeManaged)) + Expect(vm.Status.CurrentSnapshot.Name).To(Equal(vmSnapshot.Name)) + + // Verify the status has root snapshots. + Expect(vm.Status.RootSnapshots).ToNot(BeNil()) + Expect(vm.Status.RootSnapshots).To(HaveLen(1)) + Expect(vm.Status.RootSnapshots[0].Type).To(Equal(vmopv1.VirtualMachineSnapshotReferenceTypeManaged)) + Expect(vm.Status.RootSnapshots[0].Name).To(Equal(vmSnapshot.Name)) }) + }) - When("one snapshot exists and is not created", func() { - JustBeforeEach(func() { - // Create snapshot1 CR with owner reference set to the VM. - snapshot1 = builder.DummyVirtualMachineSnapshot(vm.Namespace, "snapshot-1", vm.Name) - Expect(controllerutil.SetOwnerReference(vm, snapshot1, ctx.Scheme)).To(Succeed()) - Expect(ctx.Client.Create(ctx, snapshot1)).To(Succeed()) - }) + Context("when reverting to valid snapshot", func() { + var secondSnapshot *vmopv1.VirtualMachineSnapshot - It("should process the snapshot", func() { - // Reconcile the current snapshot. - Expect(vsphere.ReconcileCurrentSnapshot(vmCtx, ctx.Client, vcVM)).To(Succeed()) - - // Verify snapshot is created. - verifyK8sVMSnapshot(snapshot1.Name, snapshot1.Namespace, true) - - // Verify snapshot status. - updatedSnapshot := &vmopv1.VirtualMachineSnapshot{} - Expect(ctx.Client.Get(ctx, ctrlclient.ObjectKey{ - Name: snapshot1.Name, - Namespace: snapshot1.Namespace, - }, updatedSnapshot)).To(Succeed()) - Expect(updatedSnapshot.Status.Quiesced).To(BeTrue()) - // Snapshot should be powered off since memory is not included in the snapshot. - Expect(updatedSnapshot.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) + It("should successfully revert to desired snapshot", func() { + // Create VM first + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + // Create first snapshot in vCenter + task, err := vcVM.CreateSnapshot(ctx, vmSnapshot.Name, "first snapshot", false, false) + Expect(err).ToNot(HaveOccurred()) + Expect(task.Wait(ctx)).To(Succeed()) + + // Create first snapshot CR + // Mark the snapshot as created so that the snapshot workflow doesn't try to create it. + conditions.MarkTrue(vmSnapshot, vmopv1.VirtualMachineSnapshotCreatedCondition) + // Mark the snapshot as ready so that the revert snapshot workflow can proceed. + conditions.MarkTrue(vmSnapshot, vmopv1.VirtualMachineSnapshotReadyCondition) + Expect(ctx.Client.Create(ctx, vmSnapshot)).To(Succeed()) + + // Snapshot should be owned by the VM resource. + o := vmopv1.VirtualMachine{} + Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(vm), &o)).To(Succeed()) + Expect(controllerutil.SetOwnerReference(&o, vmSnapshot, ctx.Scheme)).To(Succeed()) + Expect(ctx.Client.Update(ctx, vmSnapshot)).To(Succeed()) + + // Create second snapshot + secondSnapshot = builder.DummyVirtualMachineSnapshot("", "test-second-snap", vm.Name) + secondSnapshot.Namespace = nsInfo.Namespace + + task, err = vcVM.CreateSnapshot(ctx, secondSnapshot.Name, "second snapshot", false, false) + Expect(err).ToNot(HaveOccurred()) + Expect(task.Wait(ctx)).To(Succeed()) + + // Create second snapshot CR + // Mark the snapshot as completed so that the snapshot workflow doesn't try to create it. + conditions.MarkTrue(secondSnapshot, vmopv1.VirtualMachineSnapshotCreatedCondition) + // Mark the snapshot as ready so that the revert snapshot workflow can proceed. + conditions.MarkTrue(secondSnapshot, vmopv1.VirtualMachineSnapshotReadyCondition) + // Snapshot should be owned by the VM resource. + Expect(controllerutil.SetOwnerReference(&o, secondSnapshot, ctx.Scheme)).To(Succeed()) + Expect(ctx.Client.Create(ctx, secondSnapshot)).To(Succeed()) + + // Set desired snapshot to first snapshot (revert from second to first) + vm.Spec.CurrentSnapshotName = vmSnapshot.Name + + By("First reconcile should return ErrSnapshotRevert", func() { + _, createErr := vmProvider.CreateOrUpdateVirtualMachineAsync(ctx, vm) + Expect(createErr).To(HaveOccurred()) + Expect(errors.Is(createErr, vsphere.ErrSnapshotRevert)) + Expect(pkgerr.IsNoRequeueError(createErr)).To(BeTrue(), "Should return NoRequeueError") }) - }) - When("multiple snapshots exist", func() { - It("should process snapshots in order (oldest first)", func() { - // Create snapshot1 CR with owner reference set to the VM. - snapshot1 = builder.DummyVirtualMachineSnapshot(vm.Namespace, "snapshot-1", vm.Name) - creationTimeStamp := metav1.NewTime(time.Now()) - snapshot1.CreationTimestamp = creationTimeStamp - Expect(controllerutil.SetOwnerReference(vm, snapshot1, ctx.Scheme)).To(Succeed()) - Expect(ctx.Client.Create(ctx, snapshot1)).To(Succeed()) - - // Create snapshot2 CR with a later time and owner reference set to the VM. - later := metav1.NewTime(time.Now().Add(1 * time.Second)) - snapshot2 = builder.DummyVirtualMachineSnapshot(vm.Namespace, "snapshot-2", vm.Name) - snapshot2.CreationTimestamp = later - Expect(controllerutil.SetOwnerReference(vm, snapshot2, ctx.Scheme)).To(Succeed()) - Expect(ctx.Client.Create(ctx, snapshot2)).To(Succeed()) - - // First reconcile should process snapshot1, and requeue to process snapshot2. - err := vsphere.ReconcileCurrentSnapshot(vmCtx, ctx.Client, vcVM) - Expect(err).To(HaveOccurred()) - Expect(pkgerr.IsRequeueError(err)).To(BeTrue()) - Expect(err.Error()).To(ContainSubstring("requeuing to process 1 remaining snapshots")) + err = createOrUpdateVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) - // Check that snapshot1 is created. - verifyK8sVMSnapshot(snapshot1.Name, snapshot1.Namespace, true) + // Verify VM status reflects the reverted snapshot + Expect(vm.Status.CurrentSnapshot).ToNot(BeNil()) + Expect(vm.Status.CurrentSnapshot.Type).To(Equal(vmopv1.VirtualMachineSnapshotReferenceTypeManaged)) + Expect(vm.Status.CurrentSnapshot.Name).To(Equal(vmSnapshot.Name)) - // Check that snapshot2 is NOT created. - verifyK8sVMSnapshot(snapshot2.Name, snapshot2.Namespace, false) + // Verify the spec.currentSnapshot is cleared. + Expect(vm.Spec.CurrentSnapshotName).To(BeEmpty()) - // Second reconcile should process snapshot2. - Expect(vsphere.ReconcileCurrentSnapshot(vmCtx, ctx.Client, vcVM)).To(Succeed()) + // Verify the status has root snapshots. + Expect(vm.Status.RootSnapshots).ToNot(BeNil()) + Expect(vm.Status.RootSnapshots).To(HaveLen(1)) + Expect(vm.Status.RootSnapshots[0].Type).To(Equal(vmopv1.VirtualMachineSnapshotReferenceTypeManaged)) + Expect(vm.Status.RootSnapshots[0].Name).To(Equal(vmSnapshot.Name)) - // Check that snapshot2 is now created. - verifyK8sVMSnapshot(snapshot2.Name, snapshot2.Namespace, true) + // Verify the snapshot is actually current in vCenter + var moVM mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), []string{"snapshot"}, &moVM)).To(Succeed()) + Expect(moVM.Snapshot).ToNot(BeNil()) + Expect(moVM.Snapshot.CurrentSnapshot).ToNot(BeNil()) - // Note: The Children status is populated by SyncVMSnapshotTreeStatus, - // not by ReconcileCurrentSnapshot, which is tested separately above. - }) + // Find the snapshot name in the tree to verify it matches + currentSnap, err := virtualmachine.FindSnapshot(moVM, moVM.Snapshot.CurrentSnapshot.Value) + Expect(err).ToNot(HaveOccurred()) + Expect(currentSnap).ToNot(BeNil()) + Expect(currentSnap.Name).To(Equal(vmSnapshot.Name)) }) - When("one snapshot is already in progress", func() { - It("should process the in-progress snapshot and requeue for the next", func() { - // Create snapshot1 CR with in progress condition and owner reference set to the VM. - snapshot1 = builder.DummyVirtualMachineSnapshot(vm.Namespace, "snapshot-1", vm.Name) - conditions.MarkFalse(snapshot1, - vmopv1.VirtualMachineSnapshotCreatedCondition, - vmopv1.VirtualMachineSnapshotCreationInProgressReason, - "in progress", - ) - Expect(controllerutil.SetOwnerReference(vm, snapshot1, ctx.Scheme)).To(Succeed()) - Expect(ctx.Client.Create(ctx, snapshot1)).To(Succeed()) - - // Create snapshot2 CR with owner reference set to the VM. - snapshot2 = builder.DummyVirtualMachineSnapshot(vm.Namespace, "snapshot-2", vm.Name) - Expect(controllerutil.SetOwnerReference(vm, snapshot2, ctx.Scheme)).To(Succeed()) - Expect(ctx.Client.Create(ctx, snapshot2)).To(Succeed()) - - // Reconcile the current snapshot and expect a requeue error. - err := vsphere.ReconcileCurrentSnapshot(vmCtx, ctx.Client, vcVM) - Expect(err).To(HaveOccurred()) - Expect(pkgerr.IsRequeueError(err)).To(BeTrue()) + Context("and the snapshot was taken when VM was powered on and is now powered off", func() { + It("should successfully power on the VM after reverting to a Snapshot in PoweredOn state", func() { + // Create VM first + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + // Create first snapshot in vCenter + task, err := vcVM.CreateSnapshot(ctx, vmSnapshot.Name, "first snapshot", false, false) + Expect(err).ToNot(HaveOccurred()) + Expect(task.Wait(ctx)).To(Succeed()) + + // Create first snapshot CR + // Mark the snapshot as completed so that the snapshot workflow doesn't try to create it. + conditions.MarkTrue(vmSnapshot, vmopv1.VirtualMachineSnapshotCreatedCondition) + // Mark the snapshot as ready so that the revert snapshot workflow can proceed. + conditions.MarkTrue(vmSnapshot, vmopv1.VirtualMachineSnapshotReadyCondition) + Expect(ctx.Client.Create(ctx, vmSnapshot)).To(Succeed()) + + // Verify the snapshot is actually current in vCenter + var moVM mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), []string{"snapshot"}, &moVM)).To(Succeed()) + Expect(moVM.Snapshot).ToNot(BeNil()) + + // verify that the snapshot's power state is powered off + currentSnapshot, err := virtualmachine.FindSnapshot(moVM, moVM.Snapshot.CurrentSnapshot.Value) + Expect(err).ToNot(HaveOccurred()) + Expect(currentSnapshot).ToNot(BeNil()) + Expect(currentSnapshot.State).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOff)) + + // Snapshot should be owned by the VM resource. + Expect(controllerutil.SetOwnerReference(vm, vmSnapshot, ctx.Scheme)).To(Succeed()) + Expect(ctx.Client.Update(ctx, vmSnapshot)).To(Succeed()) + + // Create second snapshot + secondSnapshot = builder.DummyVirtualMachineSnapshotWithMemory("", "test-second-snap", vm.Name) + secondSnapshot.Namespace = nsInfo.Namespace + + task, err = vcVM.CreateSnapshot(ctx, secondSnapshot.Name, "second snapshot", true, false) + Expect(err).ToNot(HaveOccurred()) + Expect(task.Wait(ctx)).To(Succeed()) + + // Create second snapshot CR + // Mark the snapshot as completed so that the snapshot workflow doesn't try to create it. + conditions.MarkTrue(secondSnapshot, vmopv1.VirtualMachineSnapshotCreatedCondition) + // Mark the snapshot as ready so that the revert snapshot workflow can proceed. + conditions.MarkTrue(secondSnapshot, vmopv1.VirtualMachineSnapshotReadyCondition) + // Snapshot should be owned by the VM resource. + Expect(controllerutil.SetOwnerReference(vm, secondSnapshot, ctx.Scheme)).To(Succeed()) + Expect(ctx.Client.Create(ctx, secondSnapshot)).To(Succeed()) + + // Verify the VM is powered on + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOn)) + state, err := vcVM.PowerState(ctx) + Expect(err).ToNot(HaveOccurred()) + Expect(state).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOn)) + + // Set desired snapshot to first snapshot (revert from second to first) + vm.Spec.CurrentSnapshotName = vmSnapshot.Name + + // Revert to the first snapshot + err = createOrUpdateVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + // Verify VM status reflects the reverted snapshot + Expect(vm.Status.CurrentSnapshot).ToNot(BeNil()) + Expect(vm.Status.CurrentSnapshot.Type).To(Equal(vmopv1.VirtualMachineSnapshotReferenceTypeManaged)) + Expect(vm.Status.CurrentSnapshot.Name).To(Equal(vmSnapshot.Name)) + + // Verify the spec.currentSnapshot is cleared. + Expect(vm.Spec.CurrentSnapshotName).To(BeEmpty()) + + // Verify the VM is powered off + Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) + state, err = vcVM.PowerState(ctx) + Expect(err).ToNot(HaveOccurred()) + Expect(state).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOff)) + }) + }) + }) - // First snapshot should be created. - verifyK8sVMSnapshot(snapshot1.Name, snapshot1.Namespace, true) + // Simulate an Imported Snapshot scenario by + // - creating the VC VM and VM while ensuring the backup is not taken. + // - creating a snapshot on VC AND THEN only creating the VMSnapshot CR so that the ExtraConfig is + // not stamped by the controller. + // - change some bits in the VM CR and take a second snapshot. This snapshot can be taken through a + // VMSnapshot. This is needed to make sure that we are not reverting to a snapshot that the VM is + // running off at the same time. + // - Now, revert the VM to the first snapshot. It is expected that the spec fields would now be approximated. + Context("when reverting to imported snapshot", func() { + var secondSnapshot *vmopv1.VirtualMachineSnapshot - // Second snapshot should NOT be created. - verifyK8sVMSnapshot(snapshot2.Name, snapshot2.Namespace, false) + BeforeEach(func() { + pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { + config.Features.VMImportNewNet = true }) }) + It("should fail the revert if the snapshot wasn't imported", func() { + if vm.Labels == nil { + vm.Labels = make(map[string]string) + } - When("snapshot is being deleted", func() { - It("should skip all snapshot creation due to vSphere constraint", func() { - // Create snapshot1 CR with owner reference set to the VM. - snapshot1 = builder.DummyVirtualMachineSnapshot(vm.Namespace, "snapshot-1", vm.Name) - Expect(controllerutil.SetOwnerReference(vm, snapshot1, ctx.Scheme)).To(Succeed()) - // Set a finalizer so we can delete the snapshot CR without it being removed from cluster. - snapshot1.ObjectMeta.Finalizers = []string{"dummy-finalizer"} - Expect(ctx.Client.Create(ctx, snapshot1)).To(Succeed()) + // skip creation of backup VMResourceYAMLExtraConfigKey + // by setting the CAPV cluster role label + vm.Labels[kubeutil.CAPVClusterRoleLabelKey] = "" - // Create snapshot2 CR with owner reference set to the VM. - snapshot2 = builder.DummyVirtualMachineSnapshot(vm.Namespace, "snapshot-2", vm.Name) - Expect(controllerutil.SetOwnerReference(vm, snapshot2, ctx.Scheme)).To(Succeed()) - Expect(ctx.Client.Create(ctx, snapshot2)).To(Succeed()) + // Create VM first + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) - // Delete snapshot1 CR. - Expect(ctx.Client.Delete(ctx, snapshot1)).To(Succeed()) + // make sure the VM doesn't have the ExtraConfig stamped + var moVM mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &moVM)).To(Succeed()) + Expect(moVM.Config.ExtraConfig).ToNot(BeNil()) + ecMap := pkgutil.OptionValues(moVM.Config.ExtraConfig).StringMap() + Expect(ecMap).ToNot(HaveKey(backupapi.VMResourceYAMLExtraConfigKey)) + + // Create first snapshot in vCenter + task, err := vcVM.CreateSnapshot( + ctx, vmSnapshot.Name, "first snapshot", false, false) + Expect(err).ToNot(HaveOccurred()) + Expect(task.Wait(ctx)).To(Succeed()) + + // Create first snapshot CR and Mark the snapshot as ready + // so that the snapshot workflow doesn't try to create it. + conditions.MarkTrue(vmSnapshot, vmopv1.VirtualMachineSnapshotReadyCondition) + Expect(ctx.Client.Create(ctx, vmSnapshot)).To(Succeed()) + + // Snapshot should be owned by the VM resource. + o := vmopv1.VirtualMachine{} + Expect(ctx.Client.Get( + ctx, client.ObjectKeyFromObject(vm), &o)).To(Succeed()) + Expect(controllerutil.SetOwnerReference( + &o, vmSnapshot, ctx.Scheme)).To(Succeed()) + Expect(ctx.Client.Update(ctx, vmSnapshot)).To(Succeed()) + + // mark the snapshot as ready because snapshot workflow + // will skip because of the CAPV cluster role label + cur := &vmopv1.VirtualMachineSnapshot{} + Expect(ctx.Client.Get( + ctx, client.ObjectKeyFromObject(vmSnapshot), cur)).To(Succeed()) + conditions.MarkTrue(cur, vmopv1.VirtualMachineSnapshotReadyCondition) + Expect(ctx.Client.Status().Update(ctx, cur)).To(Succeed()) + + // we don't need the CAPI label anymore + labels := vm.Labels + delete(labels, kubeutil.CAPVClusterRoleLabelKey) + vm.Labels = labels + Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) + + // modify the VM Spec to tinker with some flag + Expect(vm.Spec.PowerOffMode).To(Equal(vmopv1.VirtualMachinePowerOpModeHard)) + vm.Spec.PowerOffMode = vmopv1.VirtualMachinePowerOpModeSoft + Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) + + // Create second snapshot + secondSnapshot = builder.DummyVirtualMachineSnapshot("", "test-second-snap", vm.Name) + secondSnapshot.Namespace = nsInfo.Namespace + + task, err = vcVM.CreateSnapshot(ctx, secondSnapshot.Name, "second snapshot", false, false) + Expect(err).ToNot(HaveOccurred()) + Expect(task.Wait(ctx)).To(Succeed()) - // Reconcile the current snapshot and expect a requeue error. - Expect(vsphere.ReconcileCurrentSnapshot(vmCtx, ctx.Client, vcVM)).To(Succeed()) + // Create second snapshot CR and Mark the snapshot as ready + // so that the snapshot workflow doesn't try to create it. + conditions.MarkTrue(secondSnapshot, vmopv1.VirtualMachineSnapshotReadyCondition) + // Snapshot should be owned by the VM resource. + Expect(controllerutil.SetOwnerReference(&o, secondSnapshot, ctx.Scheme)).To(Succeed()) + Expect(ctx.Client.Create(ctx, secondSnapshot)).To(Succeed()) - // snapshot1 should NOT be created. - verifyK8sVMSnapshot(snapshot1.Name, snapshot1.Namespace, false) + // Set desired snapshot to first snapshot (perform a revert from second to first) + vm.Spec.CurrentSnapshotName = vmSnapshot.Name - // snapshot2 should NOT be created. - verifyK8sVMSnapshot(snapshot2.Name, snapshot2.Namespace, false) - }) + err = createOrUpdateVM(ctx, vmProvider, vm) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("no VM YAML in snapshot config")) + Expect(conditions.IsFalse(vm, + vmopv1.VirtualMachineSnapshotRevertSucceeded)).To(BeTrue()) + Expect(conditions.GetReason(vm, + vmopv1.VirtualMachineSnapshotRevertSucceeded, + )).To(Equal(vmopv1.VirtualMachineSnapshotRevertFailedInvalidVMManifestReason)) }) - When("snapshot already exists and has created condition", func() { - It("should skip ready snapshot and process the next one", func() { - // Create snapshot1 CR with created condition and owner reference set to the VM. - snapshot1 = builder.DummyVirtualMachineSnapshot(vm.Namespace, "snapshot-1", vm.Name) - conditions.MarkTrue(snapshot1, vmopv1.VirtualMachineSnapshotCreatedCondition) - Expect(controllerutil.SetOwnerReference(vm, snapshot1, ctx.Scheme)).To(Succeed()) - Expect(ctx.Client.Create(ctx, snapshot1)).To(Succeed()) + It("should successfully revert to desired snapshot and approximate the VM Spec", func() { + if vm.Labels == nil { + vm.Labels = make(map[string]string) + } - // Create snapshot2 CR with owner reference set to the VM. - snapshot2 = builder.DummyVirtualMachineSnapshot(vm.Namespace, "snapshot-2", vm.Name) - Expect(controllerutil.SetOwnerReference(vm, snapshot2, ctx.Scheme)).To(Succeed()) - Expect(ctx.Client.Create(ctx, snapshot2)).To(Succeed()) + // skip creation of backup VMResourceYAMLExtraConfigKey by setting the CAPV cluster role label + vm.Labels[kubeutil.CAPVClusterRoleLabelKey] = "" - // Reconcile the current snapshot and expect no error. - Expect(vsphere.ReconcileCurrentSnapshot(vmCtx, ctx.Client, vcVM)).To(Succeed()) + // Create VM first + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) - // snapshot1 should remain created. - verifyK8sVMSnapshot(snapshot1.Name, snapshot1.Namespace, true) + // make sure the VM doesn't have the ExtraConfig stamped + var moVM mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &moVM)).To(Succeed()) + Expect(moVM.Config.ExtraConfig).ToNot(BeNil()) + ecMap := pkgutil.OptionValues(moVM.Config.ExtraConfig).StringMap() + Expect(ecMap).ToNot(HaveKey(backupapi.VMResourceYAMLExtraConfigKey)) - // snapshot2 should be processed and marked as created. - verifyK8sVMSnapshot(snapshot2.Name, snapshot2.Namespace, true) - }) - }) + // Create first snapshot in vCenter + task, err := vcVM.CreateSnapshot(ctx, vmSnapshot.Name, "first snapshot", false, false) + Expect(err).ToNot(HaveOccurred()) + Expect(task.Wait(ctx)).To(Succeed()) + + // Create first snapshot CR + // Mark the snapshot as created so that the snapshot workflow doesn't try to create it. + conditions.MarkTrue(vmSnapshot, vmopv1.VirtualMachineSnapshotCreatedCondition) + // Mark the snapshot as ready so that the snapshot workflow doesn't try to create it. + conditions.MarkTrue(vmSnapshot, vmopv1.VirtualMachineSnapshotReadyCondition) + vmSnapshot.Annotations[vmopv1.ImportedSnapshotAnnotation] = "" + Expect(ctx.Client.Create(ctx, vmSnapshot)).To(Succeed()) + + // Snapshot should be owned by the VM resource. + o := vmopv1.VirtualMachine{} + Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(vm), &o)).To(Succeed()) + Expect(controllerutil.SetOwnerReference(&o, vmSnapshot, ctx.Scheme)).To(Succeed()) + Expect(ctx.Client.Update(ctx, vmSnapshot)).To(Succeed()) + + // mark the snapshot as ready because snapshot workflow will skip because of the CAPV cluster role label + cur := &vmopv1.VirtualMachineSnapshot{} + Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(vmSnapshot), cur)).To(Succeed()) + conditions.MarkTrue(cur, vmopv1.VirtualMachineSnapshotReadyCondition) + Expect(ctx.Client.Status().Update(ctx, cur)).To(Succeed()) + + // we don't need the CAPI label anymore + labels := vm.Labels + delete(labels, kubeutil.CAPVClusterRoleLabelKey) + vm.Labels = labels + Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) + + // modify the VM Spec to tinker with some flag + Expect(vm.Spec.PowerOffMode).To(Equal(vmopv1.VirtualMachinePowerOpModeHard)) + vm.Spec.PowerOffMode = vmopv1.VirtualMachinePowerOpModeSoft + Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) + + // Create second snapshot + secondSnapshot = builder.DummyVirtualMachineSnapshot("", "test-second-snap", vm.Name) + secondSnapshot.Namespace = nsInfo.Namespace + + task, err = vcVM.CreateSnapshot(ctx, secondSnapshot.Name, "second snapshot", false, false) + Expect(err).ToNot(HaveOccurred()) + Expect(task.Wait(ctx)).To(Succeed()) - When("snapshot has empty VM name", func() { - It("should skip snapshot with empty VM name and process the next one", func() { - // Create snapshot1 CR with spec.VMName set to empty and owner reference set to the VM. - snapshot1 = builder.DummyVirtualMachineSnapshot(vm.Namespace, "snapshot-1", vm.Name) - snapshot1.Spec.VMName = "" - Expect(controllerutil.SetOwnerReference(vm, snapshot1, ctx.Scheme)).To(Succeed()) - Expect(ctx.Client.Create(ctx, snapshot1)).To(Succeed()) + // Create second snapshot CR + // Mark the snapshot as created so that the snapshot workflow doesn't try to create it. + conditions.MarkTrue(secondSnapshot, vmopv1.VirtualMachineSnapshotCreatedCondition) + // Mark the snapshot as ready so that the revert snapshot workflow can proceed. + conditions.MarkTrue(secondSnapshot, vmopv1.VirtualMachineSnapshotReadyCondition) + // Snapshot should be owned by the VM resource. + Expect(controllerutil.SetOwnerReference(&o, secondSnapshot, ctx.Scheme)).To(Succeed()) + Expect(ctx.Client.Create(ctx, secondSnapshot)).To(Succeed()) - // Create snapshot2 with owner reference set to the VM. - snapshot2 = builder.DummyVirtualMachineSnapshot(vm.Namespace, "snapshot-2", vm.Name) - Expect(controllerutil.SetOwnerReference(vm, snapshot2, ctx.Scheme)).To(Succeed()) - Expect(ctx.Client.Create(ctx, snapshot2)).To(Succeed()) + // Set desired snapshot to first snapshot (perform a revert from second to first) + vm.Spec.CurrentSnapshotName = vmSnapshot.Name - // Reconcile the current snapshot. - Expect(vsphere.ReconcileCurrentSnapshot(vmCtx, ctx.Client, vcVM)).To(Succeed()) + err = createOrUpdateVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) - // snapshot1 should not be processed (empty VMName). - verifyK8sVMSnapshot(snapshot1.Name, snapshot1.Namespace, false) + // Verify VM status reflects the reverted snapshot + Expect(vm.Status.CurrentSnapshot).ToNot(BeNil()) + Expect(vm.Status.CurrentSnapshot.Type).To(Equal(vmopv1.VirtualMachineSnapshotReferenceTypeManaged)) + Expect(vm.Status.CurrentSnapshot.Name).To(Equal(vmSnapshot.Name)) - // snapshot2 should be processed and marked as created. - verifyK8sVMSnapshot(snapshot2.Name, snapshot2.Namespace, true) - }) + // Verify the revert operation reverted to the expected values + Expect(vm.Spec.PowerOffMode).To(Equal(vmopv1.VirtualMachinePowerOpModeTrySoft)) + Expect(vm.Spec.Volumes).To(BeEmpty()) }) + }) - When("snapshot references different VM", func() { - It("should skip snapshot for different VM and process the next one", func() { - // Create snapshot1 CR with spec.VMName set to a different VM name than owner reference VM. - snapshot1 = builder.DummyVirtualMachineSnapshot(vm.Namespace, "snapshot-1", vm.Name) - snapshot1.Spec.VMName = "different-vm" - Expect(controllerutil.SetOwnerReference(vm, snapshot1, ctx.Scheme)).To(Succeed()) - Expect(ctx.Client.Create(ctx, snapshot1)).To(Succeed()) + Context("when VM spec has nil CurrentSnapshot, but the VC VM has a snapshot", func() { + It("should not attempt revert and update status correctly", func() { - // Create snapshot2 CR with owner reference set to the VM. - snapshot2 = builder.DummyVirtualMachineSnapshot(vm.Namespace, "snapshot-2", vm.Name) - Expect(controllerutil.SetOwnerReference(vm, snapshot2, ctx.Scheme)).To(Succeed()) - Expect(ctx.Client.Create(ctx, snapshot2)).To(Succeed()) + // Create VM with snapshot but don't set desired snapshot + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) - // Reconcile the current snapshot. - Expect(vsphere.ReconcileCurrentSnapshot(vmCtx, ctx.Client, vcVM)).To(Succeed()) + // Create snapshot in vCenter + task, err := vcVM.CreateSnapshot(ctx, vmSnapshot.Name, "test snapshot", false, false) + Expect(err).ToNot(HaveOccurred()) + Expect(task.Wait(ctx)).To(Succeed()) - // snapshot1 should not be processed (different VM). - verifyK8sVMSnapshot(snapshot1.Name, snapshot1.Namespace, false) + // Create snapshot CR with the owner reference to the VM. + Expect(ctx.Client.Create(ctx, vmSnapshot)).To(Succeed()) - // snapshot2 should be processed and marked as created. - verifyK8sVMSnapshot(snapshot2.Name, snapshot2.Namespace, true) - }) - }) + o := vmopv1.VirtualMachine{} + Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(vm), &o)).To(Succeed()) + Expect(controllerutil.SetOwnerReference(&o, vmSnapshot, ctx.Scheme)).To(Succeed()) + Expect(ctx.Client.Update(ctx, vmSnapshot)).To(Succeed()) - When("VM is a VKS/TKG node", func() { - It("should skip snapshot processing for VKS/TKG nodes", func() { - // Add CAPI labels to mark VM as VKS/TKG node. - vm.Labels = map[string]string{ - kubeutil.CAPWClusterRoleLabelKey: "worker", - } - Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) - - // Create snapshot1 CR with owner reference set to the VM. - snapshot1 = builder.DummyVirtualMachineSnapshot(vm.Namespace, "snapshot-1", vm.Name) - Expect(controllerutil.SetOwnerReference(vm, snapshot1, ctx.Scheme)).To(Succeed()) - Expect(ctx.Client.Create(ctx, snapshot1)).To(Succeed()) - - // Reconcile the current snapshot. - Expect(vsphere.ReconcileCurrentSnapshot(vmCtx, ctx.Client, vcVM)).To(Succeed()) - - // Snapshot should not be processed. - verifyK8sVMSnapshot(snapshot1.Name, snapshot1.Namespace, false) - verifyNoVcVMSnapshot() - }) + // Explicitly set CurrentSnapshot to nil + vm.Spec.CurrentSnapshotName = "" + + err = createOrUpdateVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + // Status should reflect the actual current snapshot + Expect(vm.Status.CurrentSnapshot).ToNot(BeNil()) + Expect(vm.Status.CurrentSnapshot.Type).To(Equal(vmopv1.VirtualMachineSnapshotReferenceTypeManaged)) + Expect(vm.Status.CurrentSnapshot.Name).To(Equal(vmSnapshot.Name)) + + // Verify the status has root snapshots. + Expect(vm.Status.RootSnapshots).ToNot(BeNil()) + Expect(vm.Status.RootSnapshots).To(HaveLen(1)) + Expect(vm.Status.RootSnapshots[0].Type).To(Equal(vmopv1.VirtualMachineSnapshotReferenceTypeManaged)) + Expect(vm.Status.RootSnapshots[0].Name).To(Equal(vmSnapshot.Name)) }) + }) - When("disk promotion sync is enabled but not ready", func() { - JustBeforeEach(func() { - pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { - config.Features.FastDeploy = true - }) - }) + Context("when VM is a VKS/TKG node", func() { + It("should skip snapshot revert for VKS/TKG nodes", func() { + // Add CAPI labels to mark VM as VKS/TKG node + if vm.Labels == nil { + vm.Labels = make(map[string]string) + } + vm.Labels[kubeutil.CAPWClusterRoleLabelKey] = "worker" + Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) - It("should create snapshot after disk promotion sync is ready", func() { - // Set the VM's promote disks mode to not disabled and disk promotion sync condition to false. - vm.Spec.PromoteDisksMode = vmopv1.VirtualMachinePromoteDisksModeOnline - conditions.MarkFalse(vm, vmopv1.VirtualMachineDiskPromotionSynced, "", "") - Expect(ctx.Client.Status().Update(ctx, vm)).To(Succeed()) + // Create VM first + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) - // Create a snapshot CR with owner reference set to the VM. - snapshot1 = builder.DummyVirtualMachineSnapshot(vm.Namespace, "snapshot-1", vm.Name) - Expect(controllerutil.SetOwnerReference(vm, snapshot1, ctx.Scheme)).To(Succeed()) - Expect(ctx.Client.Create(ctx, snapshot1)).To(Succeed()) + // Create snapshot in vCenter + task, err := vcVM.CreateSnapshot(ctx, vmSnapshot.Name, "test snapshot", false, false) + Expect(err).ToNot(HaveOccurred()) + Expect(task.Wait(ctx)).To(Succeed()) - // Reconcile the snapshot. - Expect(vsphere.ReconcileCurrentSnapshot(vmCtx, ctx.Client, vcVM)).To(Succeed()) + // Create snapshot CR and mark it as ready + conditions.MarkTrue(vmSnapshot, vmopv1.VirtualMachineSnapshotReadyCondition) + Expect(ctx.Client.Create(ctx, vmSnapshot)).To(Succeed()) - // Snapshot should not be processed. - verifyK8sVMSnapshot(snapshot1.Name, snapshot1.Namespace, false) - verifyNoVcVMSnapshot() + // Snapshot should be owned by the VM resource. + o := vmopv1.VirtualMachine{} + Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(vm), &o)).To(Succeed()) + Expect(controllerutil.SetOwnerReference(&o, vmSnapshot, ctx.Scheme)).To(Succeed()) + Expect(ctx.Client.Update(ctx, vmSnapshot)).To(Succeed()) - // Update the VM's VirtualMachineDiskPromotionSynced condition to true. - conditions.MarkTrue(vm, vmopv1.VirtualMachineDiskPromotionSynced) - Expect(ctx.Client.Status().Update(ctx, vm)).To(Succeed()) + // Create a second snapshot in vCenter + secondSnapshot := builder.DummyVirtualMachineSnapshot("", "test-second-snap", vm.Name) + secondSnapshot.Namespace = nsInfo.Namespace - // Reconcile the snapshot. - Expect(vsphere.ReconcileCurrentSnapshot(vmCtx, ctx.Client, vcVM)).To(Succeed()) + task, err = vcVM.CreateSnapshot(ctx, vmSnapshot.Name, "test snapshot", false, false) + Expect(err).ToNot(HaveOccurred()) + Expect(task.Wait(ctx)).To(Succeed()) - // Snapshot should be created. - verifyK8sVMSnapshot(snapshot1.Name, snapshot1.Namespace, true) - }) - }) + // Create snapshot CR and mark it as ready + conditions.MarkTrue(secondSnapshot, vmopv1.VirtualMachineSnapshotReadyCondition) + Expect(ctx.Client.Create(ctx, secondSnapshot)).To(Succeed()) - When("AllDisksArePVCs is enabled but disks are not registered", func() { - JustBeforeEach(func() { - pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { - config.Features.AllDisksArePVCs = true - }) - }) + // Snapshot should be owned by the VM resource. + o = vmopv1.VirtualMachine{} + Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(vm), &o)).To(Succeed()) + Expect(controllerutil.SetOwnerReference(&o, secondSnapshot, ctx.Scheme)).To(Succeed()) + Expect(ctx.Client.Update(ctx, secondSnapshot)).To(Succeed()) - It("should create snapshot after disk registration is ready", func() { - // Set the VM's disk backfill condition to false. - conditions.MarkFalse(vm, vmconfunmanagedvolsfil.Condition, "", "") - Expect(ctx.Client.Status().Update(ctx, vm)).To(Succeed()) + // Set desired snapshot to trigger a revert to the first snapshot. + vm.Spec.CurrentSnapshotName = vmSnapshot.Name - // Create a snapshot CR with owner reference set to the VM. - snapshot1 = builder.DummyVirtualMachineSnapshot(vm.Namespace, "snapshot-1", vm.Name) - Expect(controllerutil.SetOwnerReference(vm, snapshot1, ctx.Scheme)).To(Succeed()) - Expect(ctx.Client.Create(ctx, snapshot1)).To(Succeed()) + err = createOrUpdateVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + // VM status should still point to first snapshot because revert was skipped + Expect(vm.Status.CurrentSnapshot).ToNot(BeNil()) + Expect(conditions.IsFalse(vm, vmopv1.VirtualMachineSnapshotRevertSucceeded)).To(BeTrue()) + Expect(conditions.GetReason(vm, vmopv1.VirtualMachineSnapshotRevertSucceeded)).To(Equal(vmopv1.VirtualMachineSnapshotRevertSkippedReason)) + Expect(vm.Status.CurrentSnapshot.Type).To(Equal(vmopv1.VirtualMachineSnapshotReferenceTypeManaged)) + Expect(vm.Status.CurrentSnapshot.Name).To(Equal(vmSnapshot.Name)) - // Reconcile the snapshot. - Expect(vsphere.ReconcileCurrentSnapshot(vmCtx, ctx.Client, vcVM)).To(Succeed()) + // Verify the snapshot in vCenter is still the original one (no revert happened) + var moVM mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), []string{"snapshot"}, &moVM)).To(Succeed()) + Expect(moVM.Snapshot).ToNot(BeNil()) + Expect(moVM.Snapshot.CurrentSnapshot).ToNot(BeNil()) - // Snapshot should not be processed. - verifyK8sVMSnapshot(snapshot1.Name, snapshot1.Namespace, false) - verifyNoVcVMSnapshot() + // The current snapshot name should still be the original + currentSnap, err := virtualmachine.FindSnapshot(moVM, moVM.Snapshot.CurrentSnapshot.Value) + Expect(err).ToNot(HaveOccurred()) + Expect(currentSnap).ToNot(BeNil()) + Expect(currentSnap.Name).To(Equal(vmSnapshot.Name)) + }) + }) - // Update the VM's disk backfill condition to true. - conditions.MarkTrue(vm, vmconfunmanagedvolsfil.Condition) - Expect(ctx.Client.Status().Update(ctx, vm)).To(Succeed()) + Context("when snapshot revert annotation is present", func() { + It("should skip VM reconciliation when revert annotation exists", func() { + // Create VM first + _, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) - // Reconcile the snapshot. - Expect(vsphere.ReconcileCurrentSnapshot(vmCtx, ctx.Client, vcVM)).To(Succeed()) + // Set the revert in progress annotation manually + if vm.Annotations == nil { + vm.Annotations = make(map[string]string) + } + vm.Annotations[pkgconst.VirtualMachineSnapshotRevertInProgressAnnotationKey] = "" + Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) - // Snapshot should NOT be created (pending disk registration). - verifyK8sVMSnapshot(snapshot1.Name, snapshot1.Namespace, false) - verifyNoVcVMSnapshot() + // Hack: set the label to indicate that this VM is a VKS node otherwise, a + // successful backup returns a NoRequeue error expecting the watcher to + // queue the request. + vm.Labels[kubeutil.CAPVClusterRoleLabelKey] = "" - // Update the VM's disk registration condition to true. - conditions.MarkTrue(vm, vmconfunmanagedvolsreg.Condition) - Expect(ctx.Client.Status().Update(ctx, vm)).To(Succeed()) + // Attempt to reconcile VM - should return NoRequeueError due to annotation + err = vmProvider.CreateOrUpdateVirtualMachine(ctx, vm) + Expect(err).To(HaveOccurred()) + Expect(pkgerr.IsNoRequeueError(err)).To(BeTrue(), "Should return NoRequeueError when annotation is present") + Expect(err.Error()).To(ContainSubstring("snapshot revert in progress")) + }) + }) - // Reconcile the snapshot. - Expect(vsphere.ReconcileCurrentSnapshot(vmCtx, ctx.Client, vcVM)).To(Succeed()) + Context("when snapshot revert fails and revert is aborted", func() { + It("should clear the revert succeeded condition", func() { + vm.Spec.CurrentSnapshotName = vmSnapshot.Name - // Snapshot should be created. - verifyK8sVMSnapshot(snapshot1.Name, snapshot1.Namespace, true) - }) + err := createOrUpdateVM(ctx, vmProvider, vm) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To( + ContainSubstring("virtualmachinesnapshots.vmoperator.vmware.com " + + "\"test-revert-snap\" not found")) + + Expect(conditions.IsFalse(vm, + vmopv1.VirtualMachineSnapshotRevertSucceeded, + )).To(BeTrue()) + Expect(conditions.GetReason(vm, + vmopv1.VirtualMachineSnapshotRevertSucceeded, + )).To(Equal(vmopv1.VirtualMachineSnapshotRevertFailedReason)) + + vm.Spec.CurrentSnapshotName = "" + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + + Expect(conditions.Get(vm, + vmopv1.VirtualMachineSnapshotRevertSucceeded), + ).To(BeNil()) }) }) } diff --git a/pkg/providers/vsphere/vmprovider_vm_storage_test.go b/pkg/providers/vsphere/vmprovider_vm_storage_test.go new file mode 100644 index 000000000..a449fa2d4 --- /dev/null +++ b/pkg/providers/vsphere/vmprovider_vm_storage_test.go @@ -0,0 +1,330 @@ +// © Broadcom. All Rights Reserved. +// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package vsphere_test + +import ( + "context" + "fmt" + "math/rand" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "sigs.k8s.io/controller-runtime/pkg/client" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/vmware/govmomi/vim25/mo" + vimtypes "github.com/vmware/govmomi/vim25/types" + + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha6" + "github.com/vmware-tanzu/vm-operator/pkg/conditions" + pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + ctxop "github.com/vmware-tanzu/vm-operator/pkg/context/operation" + "github.com/vmware-tanzu/vm-operator/pkg/providers" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" + "github.com/vmware-tanzu/vm-operator/pkg/util/kube/cource" + "github.com/vmware-tanzu/vm-operator/pkg/util/ovfcache" + "github.com/vmware-tanzu/vm-operator/pkg/util/ptr" + "github.com/vmware-tanzu/vm-operator/test/builder" +) + +func vmStorageTests() { + var ( + parentCtx context.Context + initObjects []client.Object + testConfig builder.VCSimTestConfig + ctx *builder.TestContextForVCSim + vmProvider providers.VirtualMachineProviderInterface + nsInfo builder.WorkloadNamespaceInfo + + vm *vmopv1.VirtualMachine + vmClass *vmopv1.VirtualMachineClass + + zoneName string + ) + + BeforeEach(func() { + parentCtx = pkgcfg.NewContextWithDefaultConfig() + parentCtx = ctxop.WithContext(parentCtx) + parentCtx = ovfcache.WithContext(parentCtx) + parentCtx = cource.WithContext(parentCtx) + pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { + config.AsyncCreateEnabled = false + config.AsyncSignalEnabled = false + }) + testConfig = builder.VCSimTestConfig{ + WithContentLibrary: true, + } + + vmClass = builder.DummyVirtualMachineClassGenName() + vm = builder.DummyBasicVirtualMachine("test-vm", "") + + if vm.Spec.Network == nil { + vm.Spec.Network = &vmopv1.VirtualMachineNetworkSpec{} + } + vm.Spec.Network.Disabled = true + }) + + JustBeforeEach(func() { + ctx = suite.NewTestContextForVCSimWithParentContext( + parentCtx, testConfig, initObjects...) + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.MaxDeployThreadsOnProvider = 1 + }) + vmProvider = vsphere.NewVSphereVMProviderFromClient( + ctx, ctx.Client, ctx.Recorder) + nsInfo = ctx.CreateWorkloadNamespace() + + vmClass.Namespace = nsInfo.Namespace + Expect(ctx.Client.Create(ctx, vmClass)).To(Succeed()) + + clusterVMI1 := &vmopv1.ClusterVirtualMachineImage{} + + if testConfig.WithContentLibrary { + Expect(ctx.Client.Get( + ctx, client.ObjectKey{Name: ctx.ContentLibraryItem1Name}, + clusterVMI1)).To(Succeed()) + } else { + vsphere.SkipVMImageCLProviderCheck = true + clusterVMI1 = builder.DummyClusterVirtualMachineImage("DC0_C0_RP0_VM0") + Expect(ctx.Client.Create(ctx, clusterVMI1)).To(Succeed()) + conditions.MarkTrue(clusterVMI1, vmopv1.ReadyConditionType) + Expect(ctx.Client.Status().Update(ctx, clusterVMI1)).To(Succeed()) + } + + vm.Namespace = nsInfo.Namespace + vm.Spec.ClassName = vmClass.Name + vm.Spec.ImageName = clusterVMI1.Name + vm.Spec.Image.Kind = cvmiKind + vm.Spec.Image.Name = clusterVMI1.Name + vm.Spec.StorageClass = ctx.StorageClassName + + Expect(ctx.Client.Create(ctx, vm)).To(Succeed()) + + zoneName = ctx.GetFirstZoneName() + vm.Labels[corev1.LabelTopologyZone] = zoneName + Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) + }) + + AfterEach(func() { + vsphere.SkipVMImageCLProviderCheck = false + + if vm != nil && + !pkgcfg.FromContext(ctx).Features.BringYourOwnEncryptionKey { + By("Assert vm.Status.Crypto is nil when BYOK is disabled", func() { + Expect(vm.Status.Crypto).To(BeNil()) + }) + } + + vmClass = nil + vm = nil + + ctx.AfterEach() + ctx = nil + initObjects = nil + vmProvider = nil + nsInfo = builder.WorkloadNamespaceInfo{} + }) + + Context("Without Storage Class", func() { + BeforeEach(func() { + testConfig.WithoutStorageClass = true + }) + + It("Creates VM", func() { + Expect(vm.Spec.StorageClass).To(BeEmpty()) + + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + + By("has expected datastore", func() { + datastore, err := ctx.Finder.DefaultDatastore(ctx) + Expect(err).ToNot(HaveOccurred()) + + Expect(o.Datastore).To(HaveLen(1)) + Expect(o.Datastore[0]).To(Equal(datastore.Reference())) + }) + }) + }) + + Context("Without Content Library", func() { + BeforeEach(func() { + testConfig.WithContentLibrary = false + }) + + // TODO: Dedupe this with "Basic VM" above + It("Clones VM", func() { + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + var o mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) + + By("has expected Status values", func() { + Expect(vm.Status.PowerState).To(Equal(vm.Spec.PowerState)) + Expect(vm.Status.NodeName).ToNot(BeEmpty()) + Expect(vm.Status.InstanceUUID).To(And(Not(BeEmpty()), Equal(o.Config.InstanceUuid))) + Expect(vm.Status.BiosUUID).To(And(Not(BeEmpty()), Equal(o.Config.Uuid))) + + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionClassReady)).To(BeTrue()) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionImageReady)).To(BeTrue()) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionStorageReady)).To(BeTrue()) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionCreated)).To(BeTrue()) + + By("did not have VMSetResourcePool", func() { + Expect(vm.Spec.Reserved).To(BeNil()) + Expect(conditions.Has(vm, vmopv1.VirtualMachineConditionVMSetResourcePolicyReady)).To(BeFalse()) + }) + By("did not have Bootstrap", func() { + Expect(vm.Spec.Bootstrap).To(BeNil()) + Expect(conditions.Has(vm, vmopv1.VirtualMachineConditionBootstrapReady)).To(BeFalse()) + }) + By("did not have Network", func() { + Expect(vm.Spec.Network.Disabled).To(BeTrue()) + Expect(conditions.Has(vm, vmopv1.VirtualMachineConditionNetworkReady)).To(BeFalse()) + }) + }) + + By("has expected inventory path", func() { + Expect(vcVM.InventoryPath).To(HaveSuffix(fmt.Sprintf("/%s/%s", nsInfo.Namespace, vm.Name))) + }) + + By("has expected namespace resource pool", func() { + rp, err := vcVM.ResourcePool(ctx) + Expect(err).ToNot(HaveOccurred()) + nsRP := ctx.GetResourcePoolForNamespace(nsInfo.Namespace, "", "") + Expect(nsRP).ToNot(BeNil()) + Expect(rp.Reference().Value).To(Equal(nsRP.Reference().Value)) + }) + + By("has expected power state", func() { + Expect(o.Summary.Runtime.PowerState).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOn)) + }) + + By("has expected hardware config", func() { + // TODO: Fix vcsim behavior: NumCPU is correct "2" in the CloneSpec.Config but ends up + // with 1 CPU from source VM. Ditto for MemorySize. These assertions are only working + // because the state is on so we reconfigure the VM after it is created. + + // TODO: These assertions are excluded right now because + // of the aforementioned vcsim behavior. The referenced + // loophole is no longer in place because the FSS for + // VM Class as Config was removed, and we now rely on + // the deploy call to set the correct CPU/memory. + // Expect(o.Summary.Config.NumCpu).To(BeEquivalentTo(vmClass.Spec.Hardware.Cpus)) + // Expect(o.Summary.Config.MemorySizeMB).To(BeEquivalentTo(vmClass.Spec.Hardware.Memory.Value() / 1024 / 1024)) + }) + + // TODO: More assertions! + }) + }) + + // BMV: I don't think this is actually supported. + XIt("Create VM from VMTX in ContentLibrary", func() { + imageName := "test-vm-vmtx" + + ctx.ContentLibraryItemTemplate("DC0_C0_RP0_VM0", imageName) + vm.Spec.ImageName = imageName + + _, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + }) + + When("vm has explicit zone", func() { + JustBeforeEach(func() { + delete(vm.Labels, corev1.LabelTopologyZone) + }) + + It("creates VM in placement selected zone", func() { + Expect(vm.Labels).ToNot(HaveKey(corev1.LabelTopologyZone)) + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + azName, ok := vm.Labels[corev1.LabelTopologyZone] + Expect(ok).To(BeTrue()) + Expect(azName).To(BeElementOf(ctx.ZoneNames)) + + By("VM is created in the zone's ResourcePool", func() { + rp, err := vcVM.ResourcePool(ctx) + Expect(err).ToNot(HaveOccurred()) + nsRP := ctx.GetResourcePoolForNamespace(nsInfo.Namespace, azName, "") + Expect(nsRP).ToNot(BeNil()) + Expect(rp.Reference().Value).To(Equal(nsRP.Reference().Value)) + }) + }) + }) + + It("creates VM in assigned zone", func() { + Expect(len(ctx.ZoneNames)).To(BeNumerically(">", 1)) + azName := ctx.ZoneNames[rand.Intn(len(ctx.ZoneNames))] + vm.Labels[corev1.LabelTopologyZone] = azName + + vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + By("VM is created in the zone's ResourcePool", func() { + rp, err := vcVM.ResourcePool(ctx) + Expect(err).ToNot(HaveOccurred()) + nsRP := ctx.GetResourcePoolForNamespace(nsInfo.Namespace, azName, "") + Expect(nsRP).ToNot(BeNil()) + Expect(rp.Reference().Value).To(Equal(nsRP.Reference().Value)) + }) + }) + + When("VM zone is constrained by PVC", func() { + BeforeEach(func() { + // Need to create the PVC before creating the VM. + + vm.Spec.Volumes = []vmopv1.VirtualMachineVolume{ + { + Name: "dummy-vol", + VirtualMachineVolumeSource: vmopv1.VirtualMachineVolumeSource{ + PersistentVolumeClaim: &vmopv1.PersistentVolumeClaimVolumeSource{ + PersistentVolumeClaimVolumeSource: corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "pvc-claim-1", + }, + }, + }, + }, + } + + }) + + It("creates VM in allowed zone", func() { + Expect(len(ctx.ZoneNames)).To(BeNumerically(">", 1)) + azName := ctx.ZoneNames[rand.Intn(len(ctx.ZoneNames))] + + // Make sure we do placement. + delete(vm.Labels, corev1.LabelTopologyZone) + + pvc1 := &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pvc-claim-1", + Namespace: vm.Namespace, + Annotations: map[string]string{ + "csi.vsphere.volume-accessible-topology": fmt.Sprintf(`[{"topology.kubernetes.io/zone":"%s"}]`, azName), + }, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + StorageClassName: ptr.To(ctx.StorageClassName), + }, + Status: corev1.PersistentVolumeClaimStatus{ + Phase: corev1.ClaimBound, + }, + } + Expect(ctx.Client.Create(ctx, pvc1)).To(Succeed()) + Expect(ctx.Client.Status().Update(ctx, pvc1)).To(Succeed()) + + vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOff + _, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + Expect(vm.Status.Zone).To(Equal(azName)) + }) + }) +} diff --git a/pkg/providers/vsphere/vmprovider_vm_test.go b/pkg/providers/vsphere/vmprovider_vm_test.go index 4eee3b888..a785a0598 100644 --- a/pkg/providers/vsphere/vmprovider_vm_test.go +++ b/pkg/providers/vsphere/vmprovider_vm_test.go @@ -5,5493 +5,51 @@ package vsphere_test import ( - "bytes" - "context" - "encoding/json" - "errors" "fmt" - "math/rand" - "strings" - "sync" - "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - . "github.com/onsi/gomega/gstruct" - corev1 "k8s.io/api/core/v1" - storagev1 "k8s.io/api/storage/v1" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - - "github.com/google/uuid" - vimcrypto "github.com/vmware/govmomi/crypto" "github.com/vmware/govmomi/object" - "github.com/vmware/govmomi/simulator" - "github.com/vmware/govmomi/vapi/cluster" - "github.com/vmware/govmomi/vapi/library" - "github.com/vmware/govmomi/vapi/tags" "github.com/vmware/govmomi/vim25/mo" vimtypes "github.com/vmware/govmomi/vim25/types" - vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha6" - "github.com/vmware-tanzu/vm-operator/api/v1alpha6/cloudinit" - vmopv1common "github.com/vmware-tanzu/vm-operator/api/v1alpha6/common" - vspherepolv1 "github.com/vmware-tanzu/vm-operator/external/vsphere-policy/api/v1alpha1" - backupapi "github.com/vmware-tanzu/vm-operator/pkg/backup/api" - "github.com/vmware-tanzu/vm-operator/pkg/conditions" - pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" - pkgconst "github.com/vmware-tanzu/vm-operator/pkg/constants" "github.com/vmware-tanzu/vm-operator/pkg/constants/testlabels" - ctxop "github.com/vmware-tanzu/vm-operator/pkg/context/operation" - pkgerr "github.com/vmware-tanzu/vm-operator/pkg/errors" - "github.com/vmware-tanzu/vm-operator/pkg/providers" - "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" - "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/constants" - "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/virtualmachine" - pkgutil "github.com/vmware-tanzu/vm-operator/pkg/util" - kubeutil "github.com/vmware-tanzu/vm-operator/pkg/util/kube" - "github.com/vmware-tanzu/vm-operator/pkg/util/kube/cource" - "github.com/vmware-tanzu/vm-operator/pkg/util/ovfcache" - "github.com/vmware-tanzu/vm-operator/pkg/util/ptr" - vmopv1util "github.com/vmware-tanzu/vm-operator/pkg/util/vmopv1" - "github.com/vmware-tanzu/vm-operator/pkg/vmconfig" - "github.com/vmware-tanzu/vm-operator/pkg/vmconfig/crypto" - vmconfpolicy "github.com/vmware-tanzu/vm-operator/pkg/vmconfig/policy" "github.com/vmware-tanzu/vm-operator/test/builder" - "github.com/vmware-tanzu/vm-operator/test/testutil" -) - -const ( - // Hardcoded vcsim CPU frequency. - vcsimCPUFreq = 2294 - - cvmiKind = "ClusterVirtualMachineImage" ) -//nolint:gocyclo // allowed is 30, this function is 32 -func vmTests() { - - const ( - // Default network created for free by vcsim. - dvpgName = "DC0_DVPG0" - ) - - var ( - parentCtx context.Context - initObjects []client.Object - testConfig builder.VCSimTestConfig - ctx *builder.TestContextForVCSim - vmProvider providers.VirtualMachineProviderInterface - nsInfo builder.WorkloadNamespaceInfo - ) - - BeforeEach(func() { - parentCtx = pkgcfg.NewContextWithDefaultConfig() - parentCtx = ctxop.WithContext(parentCtx) - parentCtx = ovfcache.WithContext(parentCtx) - parentCtx = cource.WithContext(parentCtx) - pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { - config.AsyncCreateEnabled = false - config.AsyncSignalEnabled = false - }) - testConfig = builder.VCSimTestConfig{ - WithContentLibrary: true, - } - }) - - JustBeforeEach(func() { - ctx = suite.NewTestContextForVCSimWithParentContext(parentCtx, testConfig, initObjects...) - pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { - config.MaxDeployThreadsOnProvider = 1 - }) - vmProvider = vsphere.NewVSphereVMProviderFromClient(ctx, ctx.Client, ctx.Recorder) - nsInfo = ctx.CreateWorkloadNamespace() - }) - - AfterEach(func() { - ctx.AfterEach() - ctx = nil - initObjects = nil - vmProvider = nil - nsInfo = builder.WorkloadNamespaceInfo{} - }) - - Context("Create/Update/Delete VirtualMachine", func() { - var ( - vm *vmopv1.VirtualMachine - vmClass *vmopv1.VirtualMachineClass - skipCreateOrUpdateVM bool - ) - - BeforeEach(func() { - vmClass = builder.DummyVirtualMachineClassGenName() - vm = builder.DummyBasicVirtualMachine("test-vm", "") - - // Reduce diff from old tests: by default don't create an NIC. - if vm.Spec.Network == nil { - vm.Spec.Network = &vmopv1.VirtualMachineNetworkSpec{} - } - vm.Spec.Network.Disabled = true - }) - - AfterEach(func() { - if vm != nil && !pkgcfg.FromContext(ctx).Features.BringYourOwnEncryptionKey { - By("Assert vm.Status.Crypto is nil when BYOK is disabled", func() { - Expect(vm.Status.Crypto).To(BeNil()) - }) - } - - vmClass = nil - vm = nil - skipCreateOrUpdateVM = false - }) - - JustBeforeEach(func() { - vmClass.Namespace = nsInfo.Namespace - Expect(ctx.Client.Create(ctx, vmClass)).To(Succeed()) - - clusterVMI1 := &vmopv1.ClusterVirtualMachineImage{} - - if testConfig.WithContentLibrary { - Expect(ctx.Client.Get(ctx, client.ObjectKey{Name: ctx.ContentLibraryItem1Name}, clusterVMI1)).To(Succeed()) - - } else { - // BMV: VM creation without CL is broken - and has been for a long while - since we assume - // the VM Image will always point to a ContentLibrary item. - // Hack around that with this knob so we can continue to test the VM clone path. - vsphere.SkipVMImageCLProviderCheck = true - - // Use the default VM created by vcsim as the source. - clusterVMI1 = builder.DummyClusterVirtualMachineImage("DC0_C0_RP0_VM0") - Expect(ctx.Client.Create(ctx, clusterVMI1)).To(Succeed()) - conditions.MarkTrue(clusterVMI1, vmopv1.ReadyConditionType) - Expect(ctx.Client.Status().Update(ctx, clusterVMI1)).To(Succeed()) - } - - vm.Namespace = nsInfo.Namespace - vm.Spec.ClassName = vmClass.Name - vm.Spec.ImageName = clusterVMI1.Name - vm.Spec.Image.Kind = cvmiKind - vm.Spec.Image.Name = clusterVMI1.Name - vm.Spec.StorageClass = ctx.StorageClassName - - Expect(ctx.Client.Create(ctx, vm)).To(Succeed()) - }) - - AfterEach(func() { - vsphere.SkipVMImageCLProviderCheck = false - }) - - Context("VM Class and ConfigSpec", func() { - - var ( - vcVM *object.VirtualMachine - configSpec *vimtypes.VirtualMachineConfigSpec - ethCard vimtypes.VirtualEthernetCard - ) - - BeforeEach(func() { - testConfig.WithNetworkEnv = builder.NetworkEnvNamed - - ethCard = vimtypes.VirtualEthernetCard{ - VirtualDevice: vimtypes.VirtualDevice{ - Key: 4000, - DeviceInfo: &vimtypes.Description{ - Label: "test-configspec-nic-label", - Summary: "VM Network", - }, - SlotInfo: &vimtypes.VirtualDevicePciBusSlotInfo{ - VirtualDeviceBusSlotInfo: vimtypes.VirtualDeviceBusSlotInfo{}, - PciSlotNumber: 32, - }, - ControllerKey: 100, - }, - AddressType: string(vimtypes.VirtualEthernetCardMacTypeManual), - MacAddress: "00:0c:29:93:d7:27", - ResourceAllocation: &vimtypes.VirtualEthernetCardResourceAllocation{ - Reservation: ptr.To[int64](42), - }, - } - }) - - JustBeforeEach(func() { - if configSpec != nil { - var w bytes.Buffer - enc := vimtypes.NewJSONEncoder(&w) - Expect(enc.Encode(configSpec)).To(Succeed()) - - // Update the VM Class with the XML. - vmClass.Spec.ConfigSpec = w.Bytes() - Expect(ctx.Client.Update(ctx, vmClass)).To(Succeed()) - } - - vm.Spec.Network.Disabled = false - vm.Spec.Network.Interfaces = []vmopv1.VirtualMachineNetworkInterfaceSpec{ - { - Name: "eth0", - Network: &vmopv1common.PartialObjectRef{Name: dvpgName}, - }, - } - - if !skipCreateOrUpdateVM { - var err error - vcVM, err = createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - } - }) - - AfterEach(func() { - vcVM = nil - configSpec = nil - }) - - Context("FSS_WCP_MOBILITY_VM_IMPORT_NEW_NET", func() { - var instanceUUID string - - BeforeEach(func() { - skipCreateOrUpdateVM = true - }) - - JustBeforeEach(func() { - vmList, err := ctx.Finder.VirtualMachineList(ctx, "*") - Expect(err).ToNot(HaveOccurred()) - Expect(vmList).ToNot(BeEmpty()) - - vcVM = vmList[0] - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - instanceUUID = o.Config.InstanceUuid - vm.Spec.InstanceUUID = instanceUUID - - powerState, err := vcVM.PowerState(ctx) - Expect(err).ToNot(HaveOccurred()) - if powerState == vimtypes.VirtualMachinePowerStatePoweredOn { - tsk, err := vcVM.PowerOff(ctx) - Expect(err).ToNot(HaveOccurred()) - Expect(tsk.Wait(ctx)).To(Succeed()) - } - vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOn - }) - - When("fss is disabled", func() { - JustBeforeEach(func() { - pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { - config.Features.VMImportNewNet = false - }) - }) - - assertClassNotFound := func( - ctx *builder.TestContextForVCSim, - vmProvider providers.VirtualMachineProviderInterface, - vm *vmopv1.VirtualMachine, - className string) { - - vcVM, err := createOrUpdateAndGetVcVM( - ctx, vmProvider, vm) - ExpectWithOffset(1, err).ToNot(BeNil()) - ExpectWithOffset(1, err.Error()).To(ContainSubstring( - fmt.Sprintf( - "virtualmachineclasses.vmoperator.vmware.com %q not found", - className))) - ExpectWithOffset(1, vcVM).To(BeNil()) - } - - When("spec.className is empty", func() { - JustBeforeEach(func() { - vm.Spec.ClassName = "" - }) - When("spec.instanceUUID matches existing VM", func() { - JustBeforeEach(func() { - vm.Spec.InstanceUUID = instanceUUID - }) - It("should error when getting class", func() { - assertClassNotFound( - ctx, - vmProvider, - vm, - "") - }) - }) - When("spec.instanceUUID does not match existing VM", func() { - JustBeforeEach(func() { - vm.Spec.InstanceUUID = uuid.NewString() - }) - It("should error when getting class", func() { - assertClassNotFound( - ctx, - vmProvider, - vm, - "") - }) - }) - }) - - }) - - When("fss is enabled", func() { - - assertPoweredOnNoVMClassCondition := func() { - var err error - vcVM, err = createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - ExpectWithOffset(1, err).ToNot(HaveOccurred()) - ExpectWithOffset(1, vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOn)) - powerState, err := vcVM.PowerState(ctx) - ExpectWithOffset(1, err).ToNot(HaveOccurred()) - ExpectWithOffset(1, powerState).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOn)) - ExpectWithOffset(1, conditions.IsTrue(vm, vmopv1.VirtualMachineConditionClassReady)).To(BeFalse()) - } - - JustBeforeEach(func() { - pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { - config.Features.VMImportNewNet = true - }) - }) - - When("spec.className is empty", func() { - JustBeforeEach(func() { - vm.Spec.ClassName = "" - }) - When("spec.instanceUUID matches existing VM", func() { - JustBeforeEach(func() { - vm.Spec.InstanceUUID = instanceUUID - }) - It("should synthesize class from vSphere VM and power it on", func() { - assertPoweredOnNoVMClassCondition() - }) - }) - When("spec.instanceUUID does not match existing VM", func() { - JustBeforeEach(func() { - vm.Spec.InstanceUUID = uuid.NewString() - }) - It("should return an error", func() { - var err error - vcVM, err = createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).To(MatchError("cannot synthesize class from nil ConfigInfo")) - Expect(vcVM).To(BeNil()) - }) - }) - }) - }) - }) - - Context("GetVirtualMachineProperties", func() { - const ( - propName = "config.name" - propPowerState = "runtime.powerState" - propExtraConfig = "config.extraConfig" - propPathName = "config.files.vmPathName" - propExtraConfigKeyKey = "vmservice.example" - propExtraConfigKey = `config.extraConfig["` + propExtraConfigKeyKey + `"]` - ) - var ( - err error - result map[string]any - propertyPaths []string - ) - AfterEach(func() { - propertyPaths = nil - }) - JustBeforeEach(func() { - if len(propertyPaths) > 0 { - result, err = vmProvider.GetVirtualMachineProperties(ctx, vm, propertyPaths) - } - }) - When("getting "+propExtraConfig, func() { - BeforeEach(func() { - propertyPaths = []string{propExtraConfig} - }) - It("should retrieve a non-zero number of properties", func() { - Expect(err).ToNot(HaveOccurred()) - Expect(result).ToNot(HaveLen(0)) - }) - }) - DescribeTable("getting "+propExtraConfigKey, - func(val any) { - t, err := vcVM.Reconfigure(ctx, vimtypes.VirtualMachineConfigSpec{ - ExtraConfig: []vimtypes.BaseOptionValue{ - &vimtypes.OptionValue{ - Key: propExtraConfigKeyKey, - Value: val, - }, - }, - }) - Expect(err).ToNot(HaveOccurred()) - Expect(t.Wait(ctx)).To(Succeed()) - - result, err := vmProvider.GetVirtualMachineProperties( - ctx, - vm, - []string{propExtraConfigKey}) - - Expect(err).ToNot(HaveOccurred()) - Expect(result).To(HaveKeyWithValue( - propExtraConfigKey, - vimtypes.OptionValue{ - Key: propExtraConfigKeyKey, - Value: val, - })) - }, - Entry("value is a string", "Hello, world."), - Entry("value is a uint8", uint8(8)), - Entry("value is an int32", int32(32)), - Entry("value is a float64", float64(64)), - Entry("value is a bool", true), - ) - When("getting "+propName, func() { - BeforeEach(func() { - propertyPaths = []string{propName} - }) - It("should retrieve a single property", func() { - Expect(err).ToNot(HaveOccurred()) - Expect(result).To(HaveLen(1)) - Expect(result[propName]).To(Equal(vm.Name)) - }) - }) - When("getting "+propPowerState, func() { - BeforeEach(func() { - propertyPaths = []string{propPowerState} - }) - It("should retrieve a single property", func() { - Expect(err).ToNot(HaveOccurred()) - Expect(result).To(HaveLen(1)) - switch vm.Spec.PowerState { - case vmopv1.VirtualMachinePowerStateOn: - Expect(result[propPowerState]).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOn)) - case vmopv1.VirtualMachinePowerStateOff: - Expect(result[propPowerState]).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOff)) - case vmopv1.VirtualMachinePowerStateSuspended: - Expect(result[propPowerState]).To(Equal(vimtypes.VirtualMachinePowerStateSuspended)) - default: - panic(fmt.Sprintf("invalid power state: %s", vm.Spec.PowerState)) - } - }) - }) - When("getting "+propPathName, func() { - BeforeEach(func() { - propertyPaths = []string{propPathName} - }) - It("should not be set", func() { - Expect(err).ToNot(HaveOccurred()) - Expect(result).To(HaveLen(1)) - Expect(result[propName]).To(BeNil()) // should only be set if cdrom is present - }) - }) - }) - - Context("VM Class has no ConfigSpec", func() { - BeforeEach(func() { - configSpec = nil - }) - - It("creates VM", func() { - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionCreated)).To(BeTrue()) - - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - Expect(o.Config.Annotation).To(Equal(constants.VCVMAnnotation)) - Expect(o.Summary.Config.NumCpu).To(BeEquivalentTo(vmClass.Spec.Hardware.Cpus)) - Expect(o.Summary.Config.MemorySizeMB).To(BeEquivalentTo(vmClass.Spec.Hardware.Memory.Value() / 1024 / 1024)) - }) - }) - - Context("ConfigSpec specifies annotation", func() { - BeforeEach(func() { - configSpec = &vimtypes.VirtualMachineConfigSpec{ - Annotation: "my-annotation", - } - }) - - It("VM has class annotation", func() { - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionCreated)).To(BeTrue()) - - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - Expect(o.Config.Annotation).To(Equal("my-annotation")) - }) - }) - - Context("ConfigSpec specifies hardware spec", func() { - BeforeEach(func() { - configSpec = &vimtypes.VirtualMachineConfigSpec{ - Name: "config-spec-name-is-not-used", - NumCPUs: 7, - MemoryMB: 5102, - } - }) - - It("CPU and memory from ConfigSpec are ignored", func() { - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionCreated)).To(BeTrue()) - - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - Expect(o.Summary.Config.Name).To(Equal(vm.Name)) - Expect(o.Summary.Config.NumCpu).To(BeEquivalentTo(vmClass.Spec.Hardware.Cpus)) - Expect(o.Summary.Config.NumCpu).ToNot(BeEquivalentTo(configSpec.NumCPUs)) - Expect(o.Summary.Config.MemorySizeMB).To(BeEquivalentTo(vmClass.Spec.Hardware.Memory.Value() / 1024 / 1024)) - Expect(o.Summary.Config.MemorySizeMB).ToNot(BeEquivalentTo(configSpec.MemoryMB)) - }) - }) - - Context("VM Class spec CPU reservation & limits are non-zero and ConfigSpec specifies CPU reservation", func() { - BeforeEach(func() { - vmClass.Spec.Policies.Resources.Requests.Cpu = resource.MustParse("2") - vmClass.Spec.Policies.Resources.Limits.Cpu = resource.MustParse("3") - - // Specify a CPU reservation via ConfigSpec. This value should not be honored. - configSpec = &vimtypes.VirtualMachineConfigSpec{ - CpuAllocation: &vimtypes.ResourceAllocationInfo{ - Reservation: ptr.To[int64](6), - }, - } - }) - - It("VM gets CPU reservation from VM Class spec", func() { - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionCreated)).To(BeTrue()) - - resources := &vmClass.Spec.Policies.Resources - - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - - reservation := o.Config.CpuAllocation.Reservation - Expect(reservation).ToNot(BeNil()) - Expect(*reservation).To(Equal(virtualmachine.CPUQuantityToMhz(resources.Requests.Cpu, vcsimCPUFreq))) - Expect(*reservation).ToNot(Equal(*configSpec.CpuAllocation.Reservation)) - - limit := o.Config.CpuAllocation.Limit - Expect(limit).ToNot(BeNil()) - Expect(*limit).To(Equal(virtualmachine.CPUQuantityToMhz(resources.Limits.Cpu, vcsimCPUFreq))) - }) - }) - - Context("VM Class spec CPU reservation is zero and ConfigSpec specifies CPU reservation", func() { - BeforeEach(func() { - vmClass.Spec.Policies.Resources.Requests.Cpu = resource.MustParse("0") - vmClass.Spec.Policies.Resources.Limits.Cpu = resource.MustParse("0") - - // Specify a CPU reservation via ConfigSpec - configSpec = &vimtypes.VirtualMachineConfigSpec{ - CpuAllocation: &vimtypes.ResourceAllocationInfo{ - Reservation: ptr.To[int64](6), - }, - } - }) - - It("VM gets CPU reservation from ConfigSpec", func() { - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionCreated)).To(BeTrue()) - - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - - reservation := o.Config.CpuAllocation.Reservation - Expect(reservation).ToNot(BeNil()) - Expect(*reservation).ToNot(BeZero()) - Expect(*reservation).To(Equal(*configSpec.CpuAllocation.Reservation)) - }) - }) - - Context("VM Class spec Memory reservation & limits are non-zero and ConfigSpec specifies memory reservation", func() { - BeforeEach(func() { - vmClass.Spec.Policies.Resources.Requests.Memory = resource.MustParse("4Mi") - vmClass.Spec.Policies.Resources.Limits.Memory = resource.MustParse("4Mi") - - // Specify a Memory reservation via ConfigSpec - configSpec = &vimtypes.VirtualMachineConfigSpec{ - MemoryAllocation: &vimtypes.ResourceAllocationInfo{ - Reservation: ptr.To[int64](5120), - }, - } - }) - - It("VM gets memory reservation from VM Class spec", func() { - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionCreated)).To(BeTrue()) - - resources := &vmClass.Spec.Policies.Resources - - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - - reservation := o.Config.MemoryAllocation.Reservation - Expect(reservation).ToNot(BeNil()) - Expect(*reservation).To(Equal(virtualmachine.MemoryQuantityToMb(resources.Requests.Memory))) - Expect(*reservation).ToNot(Equal(*configSpec.MemoryAllocation.Reservation)) - - limit := o.Config.MemoryAllocation.Limit - Expect(limit).ToNot(BeNil()) - Expect(*limit).To(Equal(virtualmachine.MemoryQuantityToMb(resources.Limits.Memory))) - }) - }) - - Context("VM Class spec Memory reservations are zero and ConfigSpec specifies memory reservation", func() { - BeforeEach(func() { - vmClass.Spec.Policies.Resources.Requests.Memory = resource.MustParse("0Mi") - vmClass.Spec.Policies.Resources.Limits.Memory = resource.MustParse("0Mi") - - // Specify a Memory reservation via ConfigSpec - configSpec = &vimtypes.VirtualMachineConfigSpec{ - MemoryAllocation: &vimtypes.ResourceAllocationInfo{ - Reservation: ptr.To[int64](5120), - }, - } - }) - - It("VM gets memory reservation from ConfigSpec", func() { - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionCreated)).To(BeTrue()) - - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - - reservation := o.Config.MemoryAllocation.Reservation - Expect(reservation).ToNot(BeNil()) - Expect(*reservation).ToNot(BeZero()) - Expect(*reservation).To(Equal(*configSpec.MemoryAllocation.Reservation)) - }) - }) - - Context("VM Class ConfigSpec specifies a network interface", func() { - - BeforeEach(func() { - testConfig.WithNetworkEnv = builder.NetworkEnvNamed - - // Create the ConfigSpec with an ethernet card. - configSpec = &vimtypes.VirtualMachineConfigSpec{ - DeviceChange: []vimtypes.BaseVirtualDeviceConfigSpec{ - &vimtypes.VirtualDeviceConfigSpec{ - Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, - Device: &vimtypes.VirtualE1000{ - VirtualEthernetCard: ethCard, - }, - }, - }, - } - }) - - It("Reconfigures the VM with the NIC specified in ConfigSpec", func() { - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionCreated)).To(BeTrue()) - - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - - devList := object.VirtualDeviceList(o.Config.Hardware.Device) - l := devList.SelectByType(&vimtypes.VirtualEthernetCard{}) - Expect(l).To(HaveLen(1)) - - dev := l[0].GetVirtualDevice() - backing, ok := dev.Backing.(*vimtypes.VirtualEthernetCardDistributedVirtualPortBackingInfo) - Expect(ok).Should(BeTrue()) - _, dvpg := getDVPG(ctx, dvpgName) - Expect(backing.Port.PortgroupKey).To(Equal(dvpg.Reference().Value)) - - ethDevice, ok := l[0].(*vimtypes.VirtualE1000) - Expect(ok).To(BeTrue()) - Expect(ethDevice.AddressType).To(Equal(ethCard.AddressType)) - Expect(ethDevice.MacAddress).To(Equal(ethCard.MacAddress)) - - Expect(dev.DeviceInfo).To(Equal(ethCard.VirtualDevice.DeviceInfo)) - Expect(dev.DeviceGroupInfo).To(Equal(ethCard.VirtualDevice.DeviceGroupInfo)) - Expect(dev.SlotInfo).To(Equal(ethCard.VirtualDevice.SlotInfo)) - Expect(dev.ControllerKey).To(Equal(ethCard.VirtualDevice.ControllerKey)) - Expect(ethDevice.ResourceAllocation).ToNot(BeNil()) - Expect(ethDevice.ResourceAllocation.Reservation).ToNot(BeNil()) - Expect(*ethDevice.ResourceAllocation.Reservation).To(Equal(*ethCard.ResourceAllocation.Reservation)) - }) - }) - - Context("ConfigSpec does not specify any network interfaces", func() { - - BeforeEach(func() { - testConfig.WithNetworkEnv = builder.NetworkEnvNamed - - configSpec = &vimtypes.VirtualMachineConfigSpec{} - }) - - It("Reconfigures the VM with the default NIC settings from provider", func() { - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - - devList := object.VirtualDeviceList(o.Config.Hardware.Device) - l := devList.SelectByType(&vimtypes.VirtualEthernetCard{}) - Expect(l).To(HaveLen(1)) - - dev := l[0].GetVirtualDevice() - backing, ok := dev.Backing.(*vimtypes.VirtualEthernetCardDistributedVirtualPortBackingInfo) - Expect(ok).Should(BeTrue()) - _, dvpg := getDVPG(ctx, dvpgName) - Expect(backing.Port.PortgroupKey).To(Equal(dvpg.Reference().Value)) - }) - }) - - Context("VM Class Spec and ConfigSpec both contain GPU and DirectPath devices", func() { - BeforeEach(func() { - vmClass.Spec.Hardware.Devices = vmopv1.VirtualDevices{ - VGPUDevices: []vmopv1.VGPUDevice{ - { - ProfileName: "profile-from-class", - }, - }, - DynamicDirectPathIODevices: []vmopv1.DynamicDirectPathIODevice{ - { - VendorID: 50, - DeviceID: 51, - CustomLabel: "label-from-class", - }, - }, - } - - // Create the ConfigSpec with a GPU and a DDPIO device. - configSpec = &vimtypes.VirtualMachineConfigSpec{ - DeviceChange: []vimtypes.BaseVirtualDeviceConfigSpec{ - &vimtypes.VirtualDeviceConfigSpec{ - Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, - Device: &vimtypes.VirtualPCIPassthrough{ - VirtualDevice: vimtypes.VirtualDevice{ - Backing: &vimtypes.VirtualPCIPassthroughVmiopBackingInfo{ - Vgpu: "profile-from-config-spec", - }, - }, - }, - }, - &vimtypes.VirtualDeviceConfigSpec{ - Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, - Device: &vimtypes.VirtualPCIPassthrough{ - VirtualDevice: vimtypes.VirtualDevice{ - Backing: &vimtypes.VirtualPCIPassthroughDynamicBackingInfo{ - AllowedDevice: []vimtypes.VirtualPCIPassthroughAllowedDevice{ - { - VendorId: 52, - DeviceId: 53, - }, - }, - CustomLabel: "label-from-config-spec", - }, - }, - }, - }, - }, - } - }) - - It("GPU and DirectPath devices from VM Class Spec.Devices are ignored", func() { - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - - devList := object.VirtualDeviceList(o.Config.Hardware.Device) - p := devList.SelectByType(&vimtypes.VirtualPCIPassthrough{}) - Expect(p).To(HaveLen(2)) - - pciDev1 := p[0].GetVirtualDevice() - pciBacking1, ok1 := pciDev1.Backing.(*vimtypes.VirtualPCIPassthroughVmiopBackingInfo) - Expect(ok1).Should(BeTrue()) - Expect(pciBacking1.Vgpu).To(Equal("profile-from-config-spec")) - - pciDev2 := p[1].GetVirtualDevice() - pciBacking2, ok2 := pciDev2.Backing.(*vimtypes.VirtualPCIPassthroughDynamicBackingInfo) - Expect(ok2).Should(BeTrue()) - Expect(pciBacking2.AllowedDevice).To(HaveLen(1)) - Expect(pciBacking2.AllowedDevice[0].VendorId).To(Equal(int32(52))) - Expect(pciBacking2.AllowedDevice[0].DeviceId).To(Equal(int32(53))) - Expect(pciBacking2.CustomLabel).To(Equal("label-from-config-spec")) - }) - }) - - Context("VM Class Config specifies an ethCard, a GPU and a DDPIO device", func() { - - BeforeEach(func() { - // Create the ConfigSpec with an ethernet card, a GPU and a DDPIO device. - configSpec = &vimtypes.VirtualMachineConfigSpec{ - DeviceChange: []vimtypes.BaseVirtualDeviceConfigSpec{ - &vimtypes.VirtualDeviceConfigSpec{ - Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, - Device: &vimtypes.VirtualE1000{ - VirtualEthernetCard: ethCard, - }, - }, - &vimtypes.VirtualDeviceConfigSpec{ - Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, - Device: &vimtypes.VirtualPCIPassthrough{ - VirtualDevice: vimtypes.VirtualDevice{ - Backing: &vimtypes.VirtualPCIPassthroughVmiopBackingInfo{ - Vgpu: "SampleProfile2", - }, - }, - }, - }, - &vimtypes.VirtualDeviceConfigSpec{ - Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, - Device: &vimtypes.VirtualPCIPassthrough{ - VirtualDevice: vimtypes.VirtualDevice{ - Backing: &vimtypes.VirtualPCIPassthroughDynamicBackingInfo{ - AllowedDevice: []vimtypes.VirtualPCIPassthroughAllowedDevice{ - { - VendorId: 52, - DeviceId: 53, - }, - }, - CustomLabel: "SampleLabel2", - }, - }, - }, - }, - }, - } - }) - - It("Reconfigures the VM with a NIC, GPU and DDPIO device specified in ConfigSpec", func() { - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - - devList := object.VirtualDeviceList(o.Config.Hardware.Device) - l := devList.SelectByType(&vimtypes.VirtualEthernetCard{}) - Expect(l).To(HaveLen(1)) - - dev := l[0].GetVirtualDevice() - backing, ok := dev.Backing.(*vimtypes.VirtualEthernetCardDistributedVirtualPortBackingInfo) - Expect(ok).Should(BeTrue()) - _, dvpg := getDVPG(ctx, dvpgName) - Expect(backing.Port.PortgroupKey).To(Equal(dvpg.Reference().Value)) - - ethDevice, ok := l[0].(*vimtypes.VirtualE1000) - Expect(ok).To(BeTrue()) - Expect(ethDevice.AddressType).To(Equal(ethCard.AddressType)) - Expect(dev.DeviceInfo).To(Equal(ethCard.VirtualDevice.DeviceInfo)) - Expect(dev.DeviceGroupInfo).To(Equal(ethCard.VirtualDevice.DeviceGroupInfo)) - Expect(dev.SlotInfo).To(Equal(ethCard.VirtualDevice.SlotInfo)) - Expect(dev.ControllerKey).To(Equal(ethCard.VirtualDevice.ControllerKey)) - Expect(ethDevice.MacAddress).To(Equal(ethCard.MacAddress)) - Expect(ethDevice.ResourceAllocation).ToNot(BeNil()) - Expect(ethDevice.ResourceAllocation.Reservation).ToNot(BeNil()) - Expect(*ethDevice.ResourceAllocation.Reservation).To(Equal(*ethCard.ResourceAllocation.Reservation)) - - p := devList.SelectByType(&vimtypes.VirtualPCIPassthrough{}) - Expect(p).To(HaveLen(2)) - pciDev1 := p[0].GetVirtualDevice() - pciBacking1, ok1 := pciDev1.Backing.(*vimtypes.VirtualPCIPassthroughVmiopBackingInfo) - Expect(ok1).Should(BeTrue()) - Expect(pciBacking1.Vgpu).To(Equal("SampleProfile2")) - pciDev2 := p[1].GetVirtualDevice() - pciBacking2, ok2 := pciDev2.Backing.(*vimtypes.VirtualPCIPassthroughDynamicBackingInfo) - Expect(ok2).Should(BeTrue()) - Expect(pciBacking2.AllowedDevice).To(HaveLen(1)) - Expect(pciBacking2.AllowedDevice[0].VendorId).To(Equal(int32(52))) - Expect(pciBacking2.AllowedDevice[0].DeviceId).To(Equal(int32(53))) - Expect(pciBacking2.CustomLabel).To(Equal("SampleLabel2")) - - // CPU and memory should be from vm class - Expect(o.Summary.Config.NumCpu).To(BeEquivalentTo(vmClass.Spec.Hardware.Cpus)) - Expect(o.Summary.Config.MemorySizeMB).To(BeEquivalentTo(vmClass.Spec.Hardware.Memory.Value() / 1024 / 1024)) - }) - }) - - Context("VM Class Config specifies disks, disk controllers, other miscellaneous devices", func() { - BeforeEach(func() { - // Create the ConfigSpec with disks, disk controller and some misc devices: pointing device, - // video card, etc. This works fine with vcsim and helps with testing adding misc devices. - // The simulator can still reconfigure the VM with default device types like pointing devices, - // keyboard, video card, etc. But VC has some restrictions with reconfiguring a VM with new - // default device types via ConfigSpec and are usually ignored. - configSpec = &vimtypes.VirtualMachineConfigSpec{ - DeviceChange: []vimtypes.BaseVirtualDeviceConfigSpec{ - &vimtypes.VirtualDeviceConfigSpec{ - Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, - Device: &vimtypes.VirtualPointingDevice{ - VirtualDevice: vimtypes.VirtualDevice{ - Backing: &vimtypes.VirtualPointingDeviceDeviceBackingInfo{ - HostPointingDevice: "autodetect", - }, - Key: 700, - ControllerKey: 300, - }, - }, - }, - &vimtypes.VirtualDeviceConfigSpec{ - Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, - Device: &vimtypes.VirtualPS2Controller{ - VirtualController: vimtypes.VirtualController{ - Device: []int32{700}, - VirtualDevice: vimtypes.VirtualDevice{ - Key: 300, - }, - }, - }, - }, - &vimtypes.VirtualDeviceConfigSpec{ - Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, - Device: &vimtypes.VirtualMachineVideoCard{ - UseAutoDetect: ptr.To(false), - NumDisplays: 1, - VirtualDevice: vimtypes.VirtualDevice{ - Key: 500, - ControllerKey: 100, - }, - }, - }, - &vimtypes.VirtualDeviceConfigSpec{ - Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, - Device: &vimtypes.VirtualPCIController{ - VirtualController: vimtypes.VirtualController{ - Device: []int32{500}, - VirtualDevice: vimtypes.VirtualDevice{ - Key: 100, - }, - }, - }, - }, - &vimtypes.VirtualDeviceConfigSpec{ - Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, - Device: &vimtypes.VirtualDisk{ - CapacityInBytes: 1024, - VirtualDevice: vimtypes.VirtualDevice{ - Key: -42, - Backing: &vimtypes.VirtualDiskFlatVer2BackingInfo{ - ThinProvisioned: ptr.To(true), - }, - }, - }, - }, - &vimtypes.VirtualDeviceConfigSpec{ - Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, - Device: &vimtypes.VirtualSCSIController{ - VirtualController: vimtypes.VirtualController{ - Device: []int32{-42}, - }, - }, - }, - }, - } - }) - - // FIXME: vcsim behavior needs to be closer to real VC here so there aren't dupes - It("Reconfigures the VM with all misc devices in ConfigSpec, including SCSI disk controller", func() { - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - - devList := object.VirtualDeviceList(o.Config.Hardware.Device) - - // VM already has a default pointing device and the spec adds one more - // info about the default device is unknown to assert on - pointingDev := devList.SelectByType(&vimtypes.VirtualPointingDevice{}) - Expect(pointingDev).To(HaveLen(2)) - dev := pointingDev[0].GetVirtualDevice() - backing, ok := dev.Backing.(*vimtypes.VirtualPointingDeviceDeviceBackingInfo) - Expect(ok).Should(BeTrue()) - Expect(backing.HostPointingDevice).To(Equal("autodetect")) - Expect(dev.Key).To(Equal(int32(700))) - Expect(dev.ControllerKey).To(Equal(int32(300))) - - ps2Controllers := devList.SelectByType(&vimtypes.VirtualPS2Controller{}) - Expect(ps2Controllers).To(HaveLen(1)) - dev = ps2Controllers[0].GetVirtualDevice() - Expect(dev.Key).To(Equal(int32(300))) - - pciControllers := devList.SelectByType(&vimtypes.VirtualPCIController{}) - Expect(pciControllers).To(HaveLen(1)) - dev = pciControllers[0].GetVirtualDevice() - Expect(dev.Key).To(Equal(int32(100))) - - // VM already has a default video card and the spec adds one more - // info about the default device is unknown to assert on - video := devList.SelectByType(&vimtypes.VirtualMachineVideoCard{}) - Expect(video).To(HaveLen(2)) - dev = video[0].GetVirtualDevice() - Expect(dev.Key).To(Equal(int32(500))) - Expect(dev.ControllerKey).To(Equal(int32(100))) - - // SCSI disk controllers may remain due to CNS and RDM. - diskControllers := devList.SelectByType(&vimtypes.VirtualSCSIController{}) - Expect(diskControllers).To(HaveLen(1)) - - // Only preexisting disk should be present on VM -- len: 1 - disks := devList.SelectByType(&vimtypes.VirtualDisk{}) - Expect(disks).To(HaveLen(1)) - dev = disks[0].GetVirtualDevice() - Expect(dev.Key).ToNot(Equal(int32(-42))) - }) - }) - - Context("VM Class Config does not specify a hardware version", func() { - - Context("VM Class has vGPU and/or DDPIO devices", func() { - BeforeEach(func() { - // Create the ConfigSpec with a GPU and a DDPIO device. - configSpec = &vimtypes.VirtualMachineConfigSpec{ - Name: "dummy-VM", - DeviceChange: []vimtypes.BaseVirtualDeviceConfigSpec{ - &vimtypes.VirtualDeviceConfigSpec{ - Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, - Device: &vimtypes.VirtualPCIPassthrough{ - VirtualDevice: vimtypes.VirtualDevice{ - Backing: &vimtypes.VirtualPCIPassthroughVmiopBackingInfo{ - Vgpu: "profile-from-configspec", - }, - }, - }, - }, - &vimtypes.VirtualDeviceConfigSpec{ - Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, - Device: &vimtypes.VirtualPCIPassthrough{ - VirtualDevice: vimtypes.VirtualDevice{ - Backing: &vimtypes.VirtualPCIPassthroughDynamicBackingInfo{ - AllowedDevice: []vimtypes.VirtualPCIPassthroughAllowedDevice{ - { - VendorId: 52, - DeviceId: 53, - }, - }, - CustomLabel: "label-from-configspec", - }, - }, - }, - }, - }, - } - }) - - It("creates a VM with a hardware version minimum supported for PCI devices", func() { - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - Expect(o.Config.Version).To(Equal(fmt.Sprintf("vmx-%d", pkgconst.MinSupportedHWVersionForPCIPassthruDevices))) - }) - }) - - Context("VM Class has vGPU and/or DDPIO devices and VM spec has a PVC", func() { - BeforeEach(func() { - // Need to create the PVC before creating the VM. - skipCreateOrUpdateVM = true - - // Create the ConfigSpec with a GPU and a DDPIO device. - configSpec = &vimtypes.VirtualMachineConfigSpec{ - Name: "dummy-VM", - DeviceChange: []vimtypes.BaseVirtualDeviceConfigSpec{ - &vimtypes.VirtualDeviceConfigSpec{ - Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, - Device: &vimtypes.VirtualPCIPassthrough{ - VirtualDevice: vimtypes.VirtualDevice{ - Backing: &vimtypes.VirtualPCIPassthroughVmiopBackingInfo{ - Vgpu: "profile-from-configspec", - }, - }, - }, - }, - &vimtypes.VirtualDeviceConfigSpec{ - Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, - Device: &vimtypes.VirtualPCIPassthrough{ - VirtualDevice: vimtypes.VirtualDevice{ - Backing: &vimtypes.VirtualPCIPassthroughDynamicBackingInfo{ - AllowedDevice: []vimtypes.VirtualPCIPassthroughAllowedDevice{ - { - VendorId: 52, - DeviceId: 53, - }, - }, - CustomLabel: "label-from-configspec", - }, - }, - }, - }, - }, - } - - vm.Spec.Volumes = []vmopv1.VirtualMachineVolume{ - { - Name: "dummy-vol", - VirtualMachineVolumeSource: vmopv1.VirtualMachineVolumeSource{ - PersistentVolumeClaim: &vmopv1.PersistentVolumeClaimVolumeSource{ - PersistentVolumeClaimVolumeSource: corev1.PersistentVolumeClaimVolumeSource{ - ClaimName: "pvc-claim-1", - }, - }, - }, - }, - } - - vm.Status.Volumes = []vmopv1.VirtualMachineVolumeStatus{ - { - Name: "dummy-vol", - Attached: true, - }, - } - }) - - It("creates a VM with a hardware version minimum supported for PCI devices", func() { - pvc1 := &corev1.PersistentVolumeClaim{ - ObjectMeta: metav1.ObjectMeta{ - Name: "pvc-claim-1", - Namespace: vm.Namespace, - }, - Spec: corev1.PersistentVolumeClaimSpec{ - StorageClassName: ptr.To(ctx.StorageClassName), - }, - } - Expect(ctx.Client.Create(ctx, pvc1)).To(Succeed()) - - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - Expect(o.Config.Version).To(Equal(fmt.Sprintf("vmx-%d", pkgconst.MinSupportedHWVersionForPCIPassthruDevices))) - }) - }) - - Context("VM spec has a PVC", func() { - BeforeEach(func() { - // Need to create the PVC before creating the VM. - skipCreateOrUpdateVM = true - - vm.Spec.Volumes = []vmopv1.VirtualMachineVolume{ - { - Name: "dummy-vol", - VirtualMachineVolumeSource: vmopv1.VirtualMachineVolumeSource{ - PersistentVolumeClaim: &vmopv1.PersistentVolumeClaimVolumeSource{ - PersistentVolumeClaimVolumeSource: corev1.PersistentVolumeClaimVolumeSource{ - ClaimName: "pvc-claim-1", - }, - }, - }, - }, - } - - vm.Status.Volumes = []vmopv1.VirtualMachineVolumeStatus{ - { - Name: "dummy-vol", - Attached: true, - }, - } - }) - - It("creates a VM with a hardware version minimum supported for PVCs", func() { - pvc1 := &corev1.PersistentVolumeClaim{ - ObjectMeta: metav1.ObjectMeta{ - Name: "pvc-claim-1", - Namespace: vm.Namespace, - }, - Spec: corev1.PersistentVolumeClaimSpec{ - StorageClassName: ptr.To(ctx.StorageClassName), - }, - } - Expect(ctx.Client.Create(ctx, pvc1)).To(Succeed()) - - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - Expect(o.Config.Version).To(Equal(fmt.Sprintf("vmx-%d", pkgconst.MinSupportedHWVersionForPVC))) - }) - - }) - }) - - Context("VM Class Config specifies a hardware version", func() { - BeforeEach(func() { - configSpec = &vimtypes.VirtualMachineConfigSpec{Version: "vmx-14"} - }) - - When("The minimum hardware version on the VMSpec is greater than VMClass", func() { - BeforeEach(func() { - vm.Spec.MinHardwareVersion = 15 - }) - - It("updates the VM to minimum hardware version from the Spec", func() { - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - Expect(o.Config.Version).To(Equal("vmx-15")) - }) - }) - - When("The minimum hardware version on the VMSpec is less than VMClass", func() { - BeforeEach(func() { - vm.Spec.MinHardwareVersion = 13 - }) - - It("uses the hardware version from the VMClass", func() { - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - Expect(o.Config.Version).To(Equal("vmx-14")) - }) - }) - }) - - When("configSpec has disk and disk controllers", func() { - BeforeEach(func() { - configSpec = &vimtypes.VirtualMachineConfigSpec{ - Name: "dummy-VM", - DeviceChange: []vimtypes.BaseVirtualDeviceConfigSpec{ - &vimtypes.VirtualDeviceConfigSpec{ - Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, - Device: &vimtypes.VirtualSATAController{ - VirtualController: vimtypes.VirtualController{ - VirtualDevice: vimtypes.VirtualDevice{ - Key: 101, - }, - }, - }, - }, - &vimtypes.VirtualDeviceConfigSpec{ - Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, - Device: &vimtypes.VirtualSCSIController{ - VirtualController: vimtypes.VirtualController{ - VirtualDevice: vimtypes.VirtualDevice{ - Key: 103, - }, - }, - }, - }, - &vimtypes.VirtualDeviceConfigSpec{ - Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, - Device: &vimtypes.VirtualNVMEController{ - VirtualController: vimtypes.VirtualController{ - VirtualDevice: vimtypes.VirtualDevice{ - Key: 104, - }, - }, - }, - }, - &vimtypes.VirtualDeviceConfigSpec{ - Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, - Device: &vimtypes.VirtualDisk{ - CapacityInBytes: 1024, - VirtualDevice: vimtypes.VirtualDevice{ - Key: -42, - Backing: &vimtypes.VirtualDiskFlatVer2BackingInfo{ - ThinProvisioned: ptr.To(true), - }, - }, - }, - }, - }, - } - }) - - It("creates a VM with disk controllers", func() { - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - - devList := object.VirtualDeviceList(o.Config.Hardware.Device) - satacont := devList.SelectByType(&vimtypes.VirtualSATAController{}) - Expect(satacont).To(HaveLen(1)) - dev := satacont[0].GetVirtualDevice() - Expect(dev.Key).To(Equal(int32(101))) - - scsicont := devList.SelectByType(&vimtypes.VirtualSCSIController{}) - Expect(scsicont).To(HaveLen(1)) - dev = scsicont[0].GetVirtualDevice() - Expect(dev.Key).To(Equal(int32(103))) - - nvmecont := devList.SelectByType(&vimtypes.VirtualNVMEController{}) - Expect(nvmecont).To(HaveLen(1)) - dev = nvmecont[0].GetVirtualDevice() - Expect(dev.Key).To(Equal(int32(104))) - - // only preexisting disk should be present on VM -- len: 1 - disks := devList.SelectByType(&vimtypes.VirtualDisk{}) - Expect(disks).To(HaveLen(1)) - dev1 := disks[0].GetVirtualDevice() - Expect(dev1.Key).ToNot(Equal(int32(-42))) - }) - }) - }) - - Context("CreateOrUpdate VM", func() { - - var zoneName string - - JustBeforeEach(func() { - zoneName = ctx.GetFirstZoneName() - // Explicitly place the VM into one of the zones that the test context will create. - vm.Labels[corev1.LabelTopologyZone] = zoneName - Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) - }) - - When("vm is not schema or object upgraded", func() { - JustBeforeEach(func() { - pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { - config.Features.VMSharedDisks = true - config.Features.AllDisksArePVCs = false - }) - }) - JustBeforeEach(func() { - // Create the VM. - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - - // Clear its annotations and update it in K8s. - vm.Annotations = nil - Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) - }) - - It("should return ErrUpgradeSchema, then ErrUpgradeObject, then ErrBackup, then success", func() { - Expect(vm.Annotations).To(HaveLen(0)) - - // Update the VM and expect ErrUpgradeSchema. - Expect(vmProvider.CreateOrUpdateVirtualMachine(ctx, vm)).To( - MatchError(vsphere.ErrUpgradeSchema)) - - // Assert that the VM was schema upgraded. - Expect(vm.Annotations).To(HaveKeyWithValue( - pkgconst.UpgradedToBuildVersionAnnotationKey, - pkgcfg.FromContext(ctx).BuildVersion)) - Expect(vm.Annotations).To(HaveKeyWithValue( - pkgconst.UpgradedToSchemaVersionAnnotationKey, - vmopv1.GroupVersion.Version)) - Expect(vm.Annotations).ToNot(HaveKey( - pkgconst.UpgradedToFeatureVersionAnnotationKey)) - - // Update the VM again and expect ErrUpgradeObject. - Expect(vmProvider.CreateOrUpdateVirtualMachine(ctx, vm)).To( - MatchError(vsphere.ErrUpgradeObject)) - - // Assert that the VM was object upgraded. - Expect(vm.Annotations).To(HaveKeyWithValue( - pkgconst.UpgradedToBuildVersionAnnotationKey, - pkgcfg.FromContext(ctx).BuildVersion)) - Expect(vm.Annotations).To(HaveKeyWithValue( - pkgconst.UpgradedToSchemaVersionAnnotationKey, - vmopv1.GroupVersion.Version)) - Expect(vm.Annotations).To(HaveKeyWithValue( - pkgconst.UpgradedToFeatureVersionAnnotationKey, - vmopv1util.ActivatedFeatureVersion(ctx).String())) - - // Update the VM again and expect ErrBackup. - Expect(vmProvider.CreateOrUpdateVirtualMachine(ctx, vm)).To( - MatchError(vsphere.ErrBackup)) - - // Update the VM again and expect no error. - Expect(vmProvider.CreateOrUpdateVirtualMachine(ctx, vm)).To( - Succeed()) - }) - }) - - Context("VirtualMachineGroup", func() { - BeforeEach(func() { - pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { - config.Features.VMGroups = true - }) - }) - - var vmg vmopv1.VirtualMachineGroup - JustBeforeEach(func() { - // Remove explicit zone label so group placement can work - if vm.Labels != nil { - delete(vm.Labels, corev1.LabelTopologyZone) - Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) - } - - vmg = vmopv1.VirtualMachineGroup{ - ObjectMeta: metav1.ObjectMeta{ - Name: "vmg", - Namespace: vm.Namespace, - }, - } - Expect(ctx.Client.Create(ctx, &vmg)).To(Succeed()) - }) - - Context("VM Creation", func() { - When("spec.groupName is set to a non-existent group", func() { - JustBeforeEach(func() { - vm.Spec.GroupName = "vmg-invalid" - }) - Specify("it should return an error creating VM", func() { - err := createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("VM is not linked to its group")) - }) - }) - - When("spec.groupName is set to a group to which the VM does not belong", func() { - JustBeforeEach(func() { - vm.Spec.GroupName = vmg.Name - }) - Specify("it should return an error creating VM", func() { - err := createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("VM is not linked to its group")) - }) - }) - - When("spec.groupName is set to a group to which the VM does belong", func() { - JustBeforeEach(func() { - vm.Spec.GroupName = vmg.Name - vmg.Spec.BootOrder = []vmopv1.VirtualMachineGroupBootOrderGroup{ - { - Members: []vmopv1.GroupMember{ - { - Name: vm.Name, - Kind: "VirtualMachine", - }, - }, - }, - } - Expect(ctx.Client.Update(ctx, &vmg)).To(Succeed()) - }) - - When("VM Group placement condition is not ready", func() { - JustBeforeEach(func() { - vmg.Status.Members = []vmopv1.VirtualMachineGroupMemberStatus{ - { - Name: vm.Name, - Kind: "VirtualMachine", - Conditions: []metav1.Condition{ - { - Type: vmopv1.VirtualMachineGroupMemberConditionPlacementReady, - Status: metav1.ConditionFalse, - }, - }, - }, - } - Expect(ctx.Client.Status().Update(ctx, &vmg)).To(Succeed()) - }) - Specify("it should return an error creating VM", func() { - err := createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).To(HaveOccurred()) - Expect(pkgerr.IsNoRequeueError(err)).To(BeTrue()) - Expect(err.Error()).To(ContainSubstring("VM Group placement is not ready")) - }) - }) - - When("VM Group placement condition is ready", func() { - var ( - groupZone string - groupHost string - groupPool string - ) - JustBeforeEach(func() { - // Ensure the group zone is different to verify the placement actually from group. - Expect(len(ctx.ZoneNames)).To(BeNumerically(">", 1)) - groupZone = ctx.ZoneNames[rand.Intn(len(ctx.ZoneNames))] - for groupZone == vm.Labels[corev1.LabelTopologyZone] { - groupZone = ctx.ZoneNames[rand.Intn(len(ctx.ZoneNames))] - } - - ccrs := ctx.GetAZClusterComputes(groupZone) - Expect(ccrs).ToNot(BeEmpty()) - ccr := ccrs[0] - hosts, err := ccr.Hosts(ctx) - Expect(err).ToNot(HaveOccurred()) - Expect(hosts).ToNot(BeEmpty()) - groupHost = hosts[0].Reference().Value - - nsRP := ctx.GetResourcePoolForNamespace(nsInfo.Namespace, groupZone, "") - Expect(nsRP).ToNot(BeNil()) - groupPool = nsRP.Reference().Value - - vmg.Status.Members = []vmopv1.VirtualMachineGroupMemberStatus{ - { - Name: vm.Name, - Kind: "VirtualMachine", - Conditions: []metav1.Condition{ - { - Type: vmopv1.VirtualMachineGroupMemberConditionPlacementReady, - Status: metav1.ConditionTrue, - }, - }, - Placement: &vmopv1.VirtualMachinePlacementStatus{ - Zone: groupZone, - Node: groupHost, - Pool: groupPool, - }, - }, - } - Expect(ctx.Client.Status().Update(ctx, &vmg)).To(Succeed()) - }) - Specify("it should successfully create VM from group's placement", func() { - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - By("VM is placed in the expected zone from group", func() { - Expect(vm.Status.Zone).To(Equal(groupZone)) - }) - By("VM is placed in the expected host from group", func() { - vmHost, err := vcVM.HostSystem(ctx) - Expect(err).ToNot(HaveOccurred()) - Expect(vmHost.Reference().Value).To(Equal(groupHost)) - }) - By("VM is created in the expected pool from group", func() { - rp, err := vcVM.ResourcePool(ctx) - Expect(err).ToNot(HaveOccurred()) - Expect(rp.Reference().Value).To(Equal(groupPool)) - }) - By("VM has expected group linked condition", func() { - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineGroupMemberConditionGroupLinked)).To(BeTrue()) - }) - }) - }) - }) - }) - - Context("VM Update", func() { - JustBeforeEach(func() { - // Unset groupName to ensure the VM can be created. - vm.Spec.GroupName = "" - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - }) - - When("spec.groupName is set to a non-existent group", func() { - JustBeforeEach(func() { - vm.Spec.GroupName = "vmg-invalid" - }) - Specify("vm should have group linked condition set to false", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(conditions.IsFalse(vm, vmopv1.VirtualMachineGroupMemberConditionGroupLinked)).To(BeTrue()) - }) - }) - - When("spec.groupName is set to a group to which the VM does not belong", func() { - JustBeforeEach(func() { - vm.Spec.GroupName = vmg.Name - }) - Specify("vm should have group linked condition set to false", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(conditions.IsFalse(vm, vmopv1.VirtualMachineGroupMemberConditionGroupLinked)).To(BeTrue()) - }) - }) - - When("spec.groupName is set to a group to which the VM does belong", func() { - JustBeforeEach(func() { - vm.Spec.GroupName = vmg.Name - vmg.Spec.BootOrder = []vmopv1.VirtualMachineGroupBootOrderGroup{ - { - Members: []vmopv1.GroupMember{ - { - Name: vm.Name, - Kind: "VirtualMachine", - }, - }, - }, - } - Expect(ctx.Client.Update(ctx, &vmg)).To(Succeed()) - }) - Specify("vm should have group linked condition set to true", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineGroupMemberConditionGroupLinked)).To(BeTrue()) - }) - - When("spec.groupName no longer points to group", func() { - Specify("vm should no longer have group linked condition", func() { - vm.Spec.GroupName = "" - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - c := conditions.Get(vm, vmopv1.VirtualMachineGroupMemberConditionGroupLinked) - Expect(c).To(BeNil()) - }) - }) - }) - }) - - Context("Zone Label Override for VM Groups", func() { - var ( - vm *vmopv1.VirtualMachine - vmGroup *vmopv1.VirtualMachineGroup - vmClass *vmopv1.VirtualMachineClass - zoneName string - ) - - BeforeEach(func() { - vmClass = builder.DummyVirtualMachineClassGenName() - vm = builder.DummyBasicVirtualMachine("test-vm-zone-override", "") - vmGroup = &vmopv1.VirtualMachineGroup{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-group-zone-override", - }, - Spec: vmopv1.VirtualMachineGroupSpec{ - BootOrder: []vmopv1.VirtualMachineGroupBootOrderGroup{ - { - Members: []vmopv1.GroupMember{ - {Kind: "VirtualMachine", Name: vm.ObjectMeta.Name}, - }, - }, - }, - }, - } - }) - - JustBeforeEach(func() { - pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { - config.Features.VMGroups = true - }) - - vmClass.Namespace = nsInfo.Namespace - Expect(ctx.Client.Create(ctx, vmClass)).To(Succeed()) - - vmGroup.Namespace = nsInfo.Namespace - Expect(ctx.Client.Create(ctx, vmGroup)).To(Succeed()) - - clusterVMImage := &vmopv1.ClusterVirtualMachineImage{} - Expect(ctx.Client.Get(ctx, client.ObjectKey{Name: ctx.ContentLibraryItem1Name}, clusterVMImage)).To(Succeed()) - - vm.Namespace = nsInfo.Namespace - vm.Spec.ClassName = vmClass.Name - vm.Spec.ImageName = clusterVMImage.Name - vm.Spec.Image.Kind = cvmiKind - vm.Spec.Image.Name = clusterVMImage.Name - vm.Spec.StorageClass = ctx.StorageClassName - - vm.Spec.GroupName = vmGroup.Name - - zoneName = ctx.ZoneNames[rand.Intn(len(ctx.ZoneNames))] - vm.Labels = map[string]string{ - corev1.LabelTopologyZone: zoneName, - } - - Expect(ctx.Client.Create(ctx, vm)).To(Succeed()) - }) - - Context("when VM has explicit zone label and is part of group", func() { - It("should create VM in specified zone, not using group placement", func() { - err := vmProvider.CreateOrUpdateVirtualMachine(ctx, vm) - Expect(err).To(HaveOccurred()) - Expect(pkgerr.IsNoRequeueError(err)).To(BeTrue()) - Expect(err).To(MatchError(vsphere.ErrCreate)) - - // Verify VM was created - Expect(vm.Status.UniqueID).ToNot(BeEmpty()) - }) - }) - - Context("when VM has explicit zone label but is not linked to group", func() { - BeforeEach(func() { - vmGroup.Spec.BootOrder = []vmopv1.VirtualMachineGroupBootOrderGroup{} - }) - - It("should fail to create VM", func() { - err := vmProvider.CreateOrUpdateVirtualMachine(ctx, vm) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("VM is not linked to its group")) - }) - }) - }) - }) - - It("Basic VM", func() { - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - - By("has VC UUID annotation set", func() { - Expect(vm.Annotations).Should(HaveKeyWithValue(vmopv1.ManagerID, ctx.VCClient.Client.ServiceContent.About.InstanceUuid)) - }) - - By("has expected Status values", func() { - Expect(vm.Status.PowerState).To(Equal(vm.Spec.PowerState)) - Expect(vm.Status.NodeName).ToNot(BeEmpty()) - Expect(vm.Status.InstanceUUID).To(And(Not(BeEmpty()), Equal(o.Config.InstanceUuid))) - Expect(vm.Status.BiosUUID).To(And(Not(BeEmpty()), Equal(o.Config.Uuid))) - - Expect(vm.Status.Class).ToNot(BeNil()) - Expect(vm.Status.Class.Name).To(Equal(vm.Spec.ClassName)) - Expect(vm.Status.Class.APIVersion).To(Equal(vmopv1.GroupVersion.String())) - - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionClassReady)).To(BeTrue()) - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionImageReady)).To(BeTrue()) - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionStorageReady)).To(BeTrue()) - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionCreated)).To(BeTrue()) - - By("did not have VMSetResourcePool", func() { - Expect(vm.Spec.Reserved).To(BeNil()) - Expect(conditions.Has(vm, vmopv1.VirtualMachineConditionVMSetResourcePolicyReady)).To(BeFalse()) - }) - By("did not have Bootstrap", func() { - Expect(vm.Spec.Bootstrap).To(BeNil()) - Expect(conditions.Has(vm, vmopv1.VirtualMachineConditionBootstrapReady)).To(BeFalse()) - }) - By("did not have Network", func() { - Expect(vm.Spec.Network.Disabled).To(BeTrue()) - Expect(conditions.Has(vm, vmopv1.VirtualMachineConditionNetworkReady)).To(BeFalse()) - }) - }) - - By("has expected inventory path", func() { - Expect(vcVM.InventoryPath).To(HaveSuffix(fmt.Sprintf("/%s/%s", nsInfo.Namespace, vm.Name))) - }) - - By("has expected namespace resource pool", func() { - rp, err := vcVM.ResourcePool(ctx) - Expect(err).ToNot(HaveOccurred()) - nsRP := ctx.GetResourcePoolForNamespace(nsInfo.Namespace, "", "") - Expect(nsRP).ToNot(BeNil()) - Expect(rp.Reference().Value).To(Equal(nsRP.Reference().Value)) - }) - - By("has expected power state", func() { - Expect(o.Summary.Runtime.PowerState).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOn)) - }) - - vmClassRes := &vmClass.Spec.Policies.Resources - - By("has expected CpuAllocation", func() { - Expect(o.Config.CpuAllocation).ToNot(BeNil()) - - reservation := o.Config.CpuAllocation.Reservation - Expect(reservation).ToNot(BeNil()) - Expect(*reservation).To(Equal(virtualmachine.CPUQuantityToMhz(vmClassRes.Requests.Cpu, vcsimCPUFreq))) - limit := o.Config.CpuAllocation.Limit - Expect(limit).ToNot(BeNil()) - Expect(*limit).To(Equal(virtualmachine.CPUQuantityToMhz(vmClassRes.Limits.Cpu, vcsimCPUFreq))) - }) - - By("has expected MemoryAllocation", func() { - Expect(o.Config.MemoryAllocation).ToNot(BeNil()) - - reservation := o.Config.MemoryAllocation.Reservation - Expect(reservation).ToNot(BeNil()) - Expect(*reservation).To(Equal(virtualmachine.MemoryQuantityToMb(vmClassRes.Requests.Memory))) - limit := o.Config.MemoryAllocation.Limit - Expect(limit).ToNot(BeNil()) - Expect(*limit).To(Equal(virtualmachine.MemoryQuantityToMb(vmClassRes.Limits.Memory))) - }) - - By("has expected hardware config", func() { - Expect(o.Summary.Config.NumCpu).To(BeEquivalentTo(vmClass.Spec.Hardware.Cpus)) - Expect(o.Summary.Config.MemorySizeMB).To(BeEquivalentTo(vmClass.Spec.Hardware.Memory.Value() / 1024 / 1024)) - }) - - By("has expected backup ExtraConfig key", func() { - Expect(o.Config.ExtraConfig).ToNot(BeNil()) - - ecMap := pkgutil.OptionValues(o.Config.ExtraConfig).StringMap() - Expect(ecMap).To(HaveKey(backupapi.VMResourceYAMLExtraConfigKey)) - }) - - // TODO: More assertions! - }) - - DescribeTable("VM is not connected", - func(state vimtypes.VirtualMachineConnectionState) { - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - var moVM mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &moVM)).To(Succeed()) - - sctx := ctx.SimulatorContext() - sctx.WithLock( - vcVM.Reference(), - func() { - vm := sctx.Map.Get(vcVM.Reference()).(*simulator.VirtualMachine) - vm.Summary.Runtime.ConnectionState = state - }) - - _, err = createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - - if state == "" { - Expect(err).ToNot(HaveOccurred()) - } else { - Expect(err).To(HaveOccurred()) - var noRequeueErr pkgerr.NoRequeueError - Expect(errors.As(err, &noRequeueErr)).To(BeTrue()) - Expect(noRequeueErr.Message).To(Equal( - fmt.Sprintf("unsupported connection state: %s", state))) - } - }, - Entry("empty", vimtypes.VirtualMachineConnectionState("")), - Entry("disconnected", vimtypes.VirtualMachineConnectionStateDisconnected), - Entry("inaccessible", vimtypes.VirtualMachineConnectionStateInaccessible), - Entry("invalid", vimtypes.VirtualMachineConnectionStateInvalid), - Entry("orphaned", vimtypes.VirtualMachineConnectionStateOrphaned), - ) - - When("using async create", func() { - BeforeEach(func() { - pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { - config.AsyncCreateEnabled = true - config.AsyncSignalEnabled = true - }) - }) - JustBeforeEach(func() { - pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { - config.MaxDeployThreadsOnProvider = 16 - }) - }) - - It("should succeed", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.UniqueID).ToNot(BeEmpty()) - }) - - When("there is an error getting the pre-reqs", func() { - It("should not prevent a subsequent create attempt from going through", func() { - imgName := vm.Spec.Image.Name - vm.Spec.Image.Name = "does-not-exist" - err := createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError( - "clustervirtualmachineimages.vmoperator.vmware.com \"does-not-exist\" not found: " + - "clustervirtualmachineimages.vmoperator.vmware.com \"does-not-exist\" not found")) - vm.Spec.Image.Name = imgName - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.UniqueID).ToNot(BeEmpty()) - }) - }) - - When("there is an error creating the VM", func() { - JustBeforeEach(func() { - ctx.SimulatorContext().Map.Handler = func( - ctx *simulator.Context, - m *simulator.Method) (mo.Reference, vimtypes.BaseMethodFault) { - - if m.Name == "ImportVApp" { - return nil, &vimtypes.InvalidRequest{} - } - return nil, nil - } - }) - - It("should fail to create the VM without an NPE", func() { - err := createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).To(HaveOccurred()) - Eventually(func(g Gomega) { - g.Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(vm), vm)).To(Succeed()) - g.Expect(vm.Status.UniqueID).To(BeEmpty()) - c := conditions.Get(vm, vmopv1.VirtualMachineConditionCreated) - g.Expect(c).ToNot(BeNil()) - g.Expect(c.Status).To(Equal(metav1.ConditionFalse)) - g.Expect(c.Reason).To(Equal("Error")) - g.Expect(c.Message).To(Equal("deploy error: ServerFaultCode: InvalidRequest")) - }).Should(Succeed()) - }) - }) - }) - - It("TKG VM", func() { - if vm.Labels == nil { - vm.Labels = make(map[string]string) - } - vm.Labels[kubeutil.CAPVClusterRoleLabelKey] = "" - vm.Labels[kubeutil.CAPWClusterRoleLabelKey] = "" - - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - - By("does not have any backup ExtraConfig key", func() { - Expect(o.Config.ExtraConfig).ToNot(BeNil()) - ecMap := pkgutil.OptionValues(o.Config.ExtraConfig).StringMap() - Expect(ecMap).ToNot(HaveKey(backupapi.VMResourceYAMLExtraConfigKey)) - }) - }) - It("TKG VM that has opt-in annotation gets the backup EC", func() { - if vm.Labels == nil { - vm.Labels = make(map[string]string) - } - vm.Labels[kubeutil.CAPVClusterRoleLabelKey] = "" - vm.Labels[kubeutil.CAPWClusterRoleLabelKey] = "" - - if vm.Annotations == nil { - vm.Annotations = make(map[string]string) - } - vm.Annotations[vmopv1.ForceEnableBackupAnnotation] = "true" - - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - - By("has backup ExtraConfig key", func() { - Expect(o.Config.ExtraConfig).ToNot(BeNil()) - ecMap := pkgutil.OptionValues(o.Config.ExtraConfig).StringMap() - Expect(ecMap).To(HaveKey(backupapi.VMResourceYAMLExtraConfigKey)) - }) - }) - - Context("Crypto", Label(testlabels.Crypto), func() { - BeforeEach(func() { - pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { - config.Features.BringYourOwnEncryptionKey = true - }) - parentCtx = vmconfig.WithContext(parentCtx) - parentCtx = vmconfig.Register(parentCtx, crypto.New()) - - vm.Spec.Crypto = &vmopv1.VirtualMachineCryptoSpec{} - }) - JustBeforeEach(func() { - var storageClass storagev1.StorageClass - Expect(ctx.Client.Get( - ctx, - client.ObjectKey{Name: ctx.EncryptedStorageClassName}, - &storageClass)).To(Succeed()) - Expect(kubeutil.MarkEncryptedStorageClass( - ctx, - ctx.Client, - storageClass, - true)).To(Succeed()) - }) - - useExistingVM := func( - cryptoSpec vimtypes.BaseCryptoSpec, vTPM bool) { - - vmList, err := ctx.Finder.VirtualMachineList(ctx, "*") - ExpectWithOffset(1, err).ToNot(HaveOccurred()) - ExpectWithOffset(1, vmList).ToNot(BeEmpty()) - - vcVM := vmList[0] - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - vm.Spec.InstanceUUID = o.Config.InstanceUuid - - powerState, err := vcVM.PowerState(ctx) - ExpectWithOffset(1, err).ToNot(HaveOccurred()) - if powerState == vimtypes.VirtualMachinePowerStatePoweredOn { - tsk, err := vcVM.PowerOff(ctx) - ExpectWithOffset(1, err).ToNot(HaveOccurred()) - ExpectWithOffset(1, tsk.Wait(ctx)).To(Succeed()) - } - - if cryptoSpec != nil || vTPM { - configSpec := vimtypes.VirtualMachineConfigSpec{ - Crypto: cryptoSpec, - } - if vTPM { - configSpec.DeviceChange = []vimtypes.BaseVirtualDeviceConfigSpec{ - &vimtypes.VirtualDeviceConfigSpec{ - Device: &vimtypes.VirtualTPM{ - VirtualDevice: vimtypes.VirtualDevice{ - Key: -1000, - ControllerKey: 100, - }, - }, - Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, - }, - } - } - tsk, err := vcVM.Reconfigure(ctx, configSpec) - ExpectWithOffset(1, err).ToNot(HaveOccurred()) - ExpectWithOffset(1, tsk.Wait(ctx)).To(Succeed()) - } - } - - When("deploying an encrypted vm", func() { - JustBeforeEach(func() { - vm.Spec.StorageClass = ctx.EncryptedStorageClassName - Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) - }) - - When("using a default provider", func() { - - When("default provider is native key provider", func() { - JustBeforeEach(func() { - m := vimcrypto.NewManagerKmip(ctx.VCClient.Client) - Expect(m.MarkDefault(ctx, ctx.NativeKeyProviderID)).To(Succeed()) - }) - - When("using sync create", func() { - BeforeEach(func() { - pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { - config.AsyncCreateEnabled = false - config.AsyncSignalEnabled = true - }) - }) - It("should succeed", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.Crypto).ToNot(BeNil()) - Expect(vm.Status.Crypto.Encrypted).To(HaveExactElements( - []vmopv1.VirtualMachineEncryptionType{ - vmopv1.VirtualMachineEncryptionTypeConfig, - })) - Expect(vm.Status.Crypto.ProviderID).To(Equal(ctx.NativeKeyProviderID)) - Expect(vm.Status.Crypto.KeyID).ToNot(BeEmpty()) - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeTrue()) - }) - }) - - When("using async create", func() { - BeforeEach(func() { - pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { - config.AsyncCreateEnabled = true - config.AsyncSignalEnabled = true - }) - }) - It("should succeed", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.Crypto).ToNot(BeNil()) - Expect(vm.Status.Crypto.Encrypted).To(HaveExactElements( - []vmopv1.VirtualMachineEncryptionType{ - vmopv1.VirtualMachineEncryptionTypeConfig, - })) - Expect(vm.Status.Crypto.ProviderID).To(Equal(ctx.NativeKeyProviderID)) - Expect(vm.Status.Crypto.KeyID).ToNot(BeEmpty()) - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeTrue()) - }) - - // Please note this test uses FlakeAttempts(5) due to the - // validation of some predictable-over-time behavior. - When("there is a duplicate create", FlakeAttempts(5), func() { - JustBeforeEach(func() { - pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { - config.MaxDeployThreadsOnProvider = 16 - }) - }) - It("should return ErrReconcileInProgress", func() { - var ( - errs []error - errsMu sync.Mutex - done sync.WaitGroup - start = make(chan struct{}) - ) - - // Set up five goroutines that race to - // create the VM first. - for i := 0; i < 5; i++ { - done.Add(1) - go func(copyOfVM *vmopv1.VirtualMachine) { - defer done.Done() - <-start - err := createOrUpdateVM(ctx, vmProvider, copyOfVM) - if err != nil { - errsMu.Lock() - errs = append(errs, err) - errsMu.Unlock() - } else { - vm = copyOfVM - } - }(vm.DeepCopy()) - } - - close(start) - - done.Wait() - - Expect(errs).To(HaveLen(4)) - - Expect(errs).Should(ConsistOf( - providers.ErrReconcileInProgress, - providers.ErrReconcileInProgress, - providers.ErrReconcileInProgress, - providers.ErrReconcileInProgress, - )) - - Expect(vm.Status.Crypto).ToNot(BeNil()) - Expect(vm.Status.Crypto.Encrypted).To(HaveExactElements( - []vmopv1.VirtualMachineEncryptionType{ - vmopv1.VirtualMachineEncryptionTypeConfig, - })) - Expect(vm.Status.Crypto.ProviderID).To(Equal(ctx.NativeKeyProviderID)) - Expect(vm.Status.Crypto.KeyID).ToNot(BeEmpty()) - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeTrue()) - }) - }) - }) - }) - - When("default provider is not native key provider", func() { - JustBeforeEach(func() { - m := vimcrypto.NewManagerKmip(ctx.VCClient.Client) - Expect(m.MarkDefault(ctx, ctx.EncryptionClass1ProviderID)).To(Succeed()) - }) - - It("should succeed", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.Crypto).ToNot(BeNil()) - Expect(vm.Status.Crypto.Encrypted).To(HaveExactElements( - []vmopv1.VirtualMachineEncryptionType{ - vmopv1.VirtualMachineEncryptionTypeConfig, - })) - Expect(vm.Status.Crypto.ProviderID).To(Equal(ctx.EncryptionClass1ProviderID)) - Expect(vm.Status.Crypto.KeyID).ToNot(BeEmpty()) - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeTrue()) - }) - }) - }) - - Context("using an encryption class", func() { - - JustBeforeEach(func() { - vm.Spec.Crypto.EncryptionClassName = ctx.EncryptionClass1Name - }) - - It("should succeed", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.Crypto).ToNot(BeNil()) - Expect(vm.Status.Crypto.Encrypted).To(HaveExactElements( - []vmopv1.VirtualMachineEncryptionType{ - vmopv1.VirtualMachineEncryptionTypeConfig, - })) - Expect(vm.Status.Crypto.ProviderID).To(Equal(ctx.EncryptionClass1ProviderID)) - Expect(vm.Status.Crypto.KeyID).To(Equal(nsInfo.EncryptionClass1KeyID)) - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeTrue()) - }) - }) - }) - - When("encrypting an existing vm", func() { - var ( - hasVTPM bool - ) - - BeforeEach(func() { - hasVTPM = false - }) - - JustBeforeEach(func() { - useExistingVM(nil, hasVTPM) - vm.Spec.StorageClass = ctx.EncryptedStorageClassName - }) - - When("using a default provider", func() { - - When("default provider is native key provider", func() { - JustBeforeEach(func() { - m := vimcrypto.NewManagerKmip(ctx.VCClient.Client) - Expect(m.MarkDefault(ctx, ctx.NativeKeyProviderID)).To(Succeed()) - }) - - It("should succeed", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.Crypto).ToNot(BeNil()) - - Expect(vm.Status.Crypto.Encrypted).To(HaveExactElements( - []vmopv1.VirtualMachineEncryptionType{ - vmopv1.VirtualMachineEncryptionTypeConfig, - })) - Expect(vm.Status.Crypto.ProviderID).To(Equal(ctx.NativeKeyProviderID)) - Expect(vm.Status.Crypto.KeyID).ToNot(BeEmpty()) - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeTrue()) - }) - }) - - When("default provider is not native key provider", func() { - JustBeforeEach(func() { - m := vimcrypto.NewManagerKmip(ctx.VCClient.Client) - Expect(m.MarkDefault(ctx, ctx.EncryptionClass1ProviderID)).To(Succeed()) - }) - - It("should succeed", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.Crypto).ToNot(BeNil()) - - Expect(vm.Status.Crypto.Encrypted).To(HaveExactElements( - []vmopv1.VirtualMachineEncryptionType{ - vmopv1.VirtualMachineEncryptionTypeConfig, - })) - Expect(vm.Status.Crypto.ProviderID).To(Equal(ctx.EncryptionClass1ProviderID)) - Expect(vm.Status.Crypto.KeyID).ToNot(BeEmpty()) - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeTrue()) - }) - }) - }) - - Context("using an encryption class", func() { - - JustBeforeEach(func() { - vm.Spec.Crypto.EncryptionClassName = ctx.EncryptionClass2Name - }) - - It("should succeed", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.Crypto).ToNot(BeNil()) - - Expect(vm.Status.Crypto.Encrypted).To(HaveExactElements( - []vmopv1.VirtualMachineEncryptionType{ - vmopv1.VirtualMachineEncryptionTypeConfig, - })) - Expect(vm.Status.Crypto.ProviderID).To(Equal(ctx.EncryptionClass2ProviderID)) - Expect(vm.Status.Crypto.KeyID).To(Equal(nsInfo.EncryptionClass2KeyID)) - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeTrue()) - }) - - When("using a non-encryption storage class", func() { - JustBeforeEach(func() { - vm.Spec.StorageClass = ctx.StorageClassName - vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOff - }) - - When("there is no vTPM", func() { - It("should not error, but have condition", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.Crypto).To(BeNil()) - c := conditions.Get(vm, vmopv1.VirtualMachineEncryptionSynced) - Expect(c).ToNot(BeNil()) - Expect(c.Status).To(Equal(metav1.ConditionFalse)) - Expect(c.Reason).To(Equal("InvalidState")) - Expect(c.Message).To(Equal("Must use encryption storage class or have vTPM when encrypting vm")) - }) - }) - - When("there is a vTPM", func() { - BeforeEach(func() { - hasVTPM = true - }) - It("should succeed", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.Crypto).ToNot(BeNil()) - - Expect(vm.Status.Crypto.Encrypted).To(HaveExactElements( - []vmopv1.VirtualMachineEncryptionType{ - vmopv1.VirtualMachineEncryptionTypeConfig, - })) - Expect(vm.Status.Crypto.ProviderID).To(Equal(ctx.EncryptionClass2ProviderID)) - Expect(vm.Status.Crypto.KeyID).To(Equal(nsInfo.EncryptionClass2KeyID)) - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeTrue()) - }) - }) - }) - }) - }) - - When("recrypting a vm", func() { - var ( - hasVTPM bool - ) - - BeforeEach(func() { - hasVTPM = false - }) - - JustBeforeEach(func() { - useExistingVM(&vimtypes.CryptoSpecEncrypt{ - CryptoKeyId: vimtypes.CryptoKeyId{ - KeyId: nsInfo.EncryptionClass1KeyID, - ProviderId: &vimtypes.KeyProviderId{ - Id: ctx.EncryptionClass1ProviderID, - }, - }, - }, hasVTPM) - vm.Spec.StorageClass = ctx.EncryptedStorageClassName - }) - - When("using a default provider", func() { - - When("default provider is native key provider", func() { - JustBeforeEach(func() { - m := vimcrypto.NewManagerKmip(ctx.VCClient.Client) - Expect(m.MarkDefault(ctx, ctx.NativeKeyProviderID)).To(Succeed()) - }) - - It("should succeed", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.Crypto).ToNot(BeNil()) - - Expect(vm.Status.Crypto.Encrypted).To(HaveExactElements( - []vmopv1.VirtualMachineEncryptionType{ - vmopv1.VirtualMachineEncryptionTypeConfig, - })) - Expect(vm.Status.Crypto.ProviderID).To(Equal(ctx.NativeKeyProviderID)) - Expect(vm.Status.Crypto.KeyID).ToNot(BeEmpty()) - Expect(vm.Status.Crypto.KeyID).ToNot(Equal(nsInfo.EncryptionClass1KeyID)) - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeTrue()) - }) - }) - - When("default provider is not native key provider", func() { - JustBeforeEach(func() { - m := vimcrypto.NewManagerKmip(ctx.VCClient.Client) - Expect(m.MarkDefault(ctx, ctx.EncryptionClass2ProviderID)).To(Succeed()) - }) - - It("should succeed", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.Crypto).ToNot(BeNil()) - - Expect(vm.Status.Crypto.Encrypted).To(HaveExactElements( - []vmopv1.VirtualMachineEncryptionType{ - vmopv1.VirtualMachineEncryptionTypeConfig, - })) - Expect(vm.Status.Crypto.ProviderID).To(Equal(ctx.EncryptionClass2ProviderID)) - Expect(vm.Status.Crypto.KeyID).ToNot(BeEmpty()) - Expect(vm.Status.Crypto.KeyID).ToNot(Equal(nsInfo.EncryptionClass1KeyID)) - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeTrue()) - }) - }) - }) - - Context("using an encryption class", func() { - - JustBeforeEach(func() { - vm.Spec.Crypto.EncryptionClassName = ctx.EncryptionClass2Name - }) - - It("should succeed", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.Crypto).ToNot(BeNil()) - - Expect(vm.Status.Crypto.Encrypted).To(HaveExactElements( - []vmopv1.VirtualMachineEncryptionType{ - vmopv1.VirtualMachineEncryptionTypeConfig, - })) - Expect(vm.Status.Crypto.ProviderID).To(Equal(ctx.EncryptionClass2ProviderID)) - Expect(vm.Status.Crypto.KeyID).To(Equal(nsInfo.EncryptionClass2KeyID)) - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeTrue()) - }) - - When("using a non-encryption storage class with a vTPM", func() { - BeforeEach(func() { - hasVTPM = true - }) - - JustBeforeEach(func() { - vm.Spec.StorageClass = ctx.StorageClassName - }) - - It("should succeed", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.Crypto).ToNot(BeNil()) - - Expect(vm.Status.Crypto.Encrypted).To(HaveExactElements( - []vmopv1.VirtualMachineEncryptionType{ - vmopv1.VirtualMachineEncryptionTypeConfig, - })) - Expect(vm.Status.Crypto.ProviderID).To(Equal(ctx.EncryptionClass2ProviderID)) - Expect(vm.Status.Crypto.KeyID).To(Equal(nsInfo.EncryptionClass2KeyID)) - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeTrue()) - }) - }) - }) - }) - }) - - Context("VM Class with PCI passthrough devices", func() { - BeforeEach(func() { - // For old behavior, we'll fallback to these standalone fields when the class - // does not have a ConfigSpec. - vmClass.Spec.Hardware.Devices = vmopv1.VirtualDevices{ - VGPUDevices: []vmopv1.VGPUDevice{ - { - ProfileName: "profile-from-class-without-class-as-config-fss", - }, - }, - DynamicDirectPathIODevices: []vmopv1.DynamicDirectPathIODevice{ - { - VendorID: 59, - DeviceID: 60, - CustomLabel: "label-from-class-without-class-as-config-fss", - }, - }, - } - }) - - It("VM should have PCI devices from VM Class", func() { - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - - devList := object.VirtualDeviceList(o.Config.Hardware.Device) - p := devList.SelectByType(&vimtypes.VirtualPCIPassthrough{}) - Expect(p).To(HaveLen(2)) - }) - }) - - Context("Without Storage Class", func() { - BeforeEach(func() { - testConfig.WithoutStorageClass = true - }) - - It("Creates VM", func() { - Expect(vm.Spec.StorageClass).To(BeEmpty()) - - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - - By("has expected datastore", func() { - datastore, err := ctx.Finder.DefaultDatastore(ctx) - Expect(err).ToNot(HaveOccurred()) - - Expect(o.Datastore).To(HaveLen(1)) - Expect(o.Datastore[0]).To(Equal(datastore.Reference())) - }) - }) - }) - - Context("Without Content Library", func() { - BeforeEach(func() { - testConfig.WithContentLibrary = false - }) - - // TODO: Dedupe this with "Basic VM" above - It("Clones VM", func() { - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - - By("has expected Status values", func() { - Expect(vm.Status.PowerState).To(Equal(vm.Spec.PowerState)) - Expect(vm.Status.NodeName).ToNot(BeEmpty()) - Expect(vm.Status.InstanceUUID).To(And(Not(BeEmpty()), Equal(o.Config.InstanceUuid))) - Expect(vm.Status.BiosUUID).To(And(Not(BeEmpty()), Equal(o.Config.Uuid))) - - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionClassReady)).To(BeTrue()) - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionImageReady)).To(BeTrue()) - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionStorageReady)).To(BeTrue()) - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionCreated)).To(BeTrue()) - - By("did not have VMSetResourcePool", func() { - Expect(vm.Spec.Reserved).To(BeNil()) - Expect(conditions.Has(vm, vmopv1.VirtualMachineConditionVMSetResourcePolicyReady)).To(BeFalse()) - }) - By("did not have Bootstrap", func() { - Expect(vm.Spec.Bootstrap).To(BeNil()) - Expect(conditions.Has(vm, vmopv1.VirtualMachineConditionBootstrapReady)).To(BeFalse()) - }) - By("did not have Network", func() { - Expect(vm.Spec.Network.Disabled).To(BeTrue()) - Expect(conditions.Has(vm, vmopv1.VirtualMachineConditionNetworkReady)).To(BeFalse()) - }) - }) - - By("has expected inventory path", func() { - Expect(vcVM.InventoryPath).To(HaveSuffix(fmt.Sprintf("/%s/%s", nsInfo.Namespace, vm.Name))) - }) - - By("has expected namespace resource pool", func() { - rp, err := vcVM.ResourcePool(ctx) - Expect(err).ToNot(HaveOccurred()) - nsRP := ctx.GetResourcePoolForNamespace(nsInfo.Namespace, "", "") - Expect(nsRP).ToNot(BeNil()) - Expect(rp.Reference().Value).To(Equal(nsRP.Reference().Value)) - }) - - By("has expected power state", func() { - Expect(o.Summary.Runtime.PowerState).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOn)) - }) - - By("has expected hardware config", func() { - // TODO: Fix vcsim behavior: NumCPU is correct "2" in the CloneSpec.Config but ends up - // with 1 CPU from source VM. Ditto for MemorySize. These assertions are only working - // because the state is on so we reconfigure the VM after it is created. - - // TODO: These assertions are excluded right now because - // of the aforementioned vcsim behavior. The referenced - // loophole is no longer in place because the FSS for - // VM Class as Config was removed, and we now rely on - // the deploy call to set the correct CPU/memory. - // Expect(o.Summary.Config.NumCpu).To(BeEquivalentTo(vmClass.Spec.Hardware.Cpus)) - // Expect(o.Summary.Config.MemorySizeMB).To(BeEquivalentTo(vmClass.Spec.Hardware.Memory.Value() / 1024 / 1024)) - }) - - // TODO: More assertions! - }) - }) - - // BMV: I don't think this is actually supported. - XIt("Create VM from VMTX in ContentLibrary", func() { - imageName := "test-vm-vmtx" - - ctx.ContentLibraryItemTemplate("DC0_C0_RP0_VM0", imageName) - vm.Spec.ImageName = imageName - - _, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - }) - - When("vm has explicit zone", func() { - JustBeforeEach(func() { - delete(vm.Labels, corev1.LabelTopologyZone) - }) - - It("creates VM in placement selected zone", func() { - Expect(vm.Labels).ToNot(HaveKey(corev1.LabelTopologyZone)) - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - azName, ok := vm.Labels[corev1.LabelTopologyZone] - Expect(ok).To(BeTrue()) - Expect(azName).To(BeElementOf(ctx.ZoneNames)) - - By("VM is created in the zone's ResourcePool", func() { - rp, err := vcVM.ResourcePool(ctx) - Expect(err).ToNot(HaveOccurred()) - nsRP := ctx.GetResourcePoolForNamespace(nsInfo.Namespace, azName, "") - Expect(nsRP).ToNot(BeNil()) - Expect(rp.Reference().Value).To(Equal(nsRP.Reference().Value)) - }) - }) - }) - - It("creates VM in assigned zone", func() { - Expect(len(ctx.ZoneNames)).To(BeNumerically(">", 1)) - azName := ctx.ZoneNames[rand.Intn(len(ctx.ZoneNames))] - vm.Labels[corev1.LabelTopologyZone] = azName - - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - By("VM is created in the zone's ResourcePool", func() { - rp, err := vcVM.ResourcePool(ctx) - Expect(err).ToNot(HaveOccurred()) - nsRP := ctx.GetResourcePoolForNamespace(nsInfo.Namespace, azName, "") - Expect(nsRP).ToNot(BeNil()) - Expect(rp.Reference().Value).To(Equal(nsRP.Reference().Value)) - }) - }) - - When("VM zone is constrained by PVC", func() { - BeforeEach(func() { - // Need to create the PVC before creating the VM. - skipCreateOrUpdateVM = true - - vm.Spec.Volumes = []vmopv1.VirtualMachineVolume{ - { - Name: "dummy-vol", - VirtualMachineVolumeSource: vmopv1.VirtualMachineVolumeSource{ - PersistentVolumeClaim: &vmopv1.PersistentVolumeClaimVolumeSource{ - PersistentVolumeClaimVolumeSource: corev1.PersistentVolumeClaimVolumeSource{ - ClaimName: "pvc-claim-1", - }, - }, - }, - }, - } - - }) - - It("creates VM in allowed zone", func() { - Expect(len(ctx.ZoneNames)).To(BeNumerically(">", 1)) - azName := ctx.ZoneNames[rand.Intn(len(ctx.ZoneNames))] - - // Make sure we do placement. - delete(vm.Labels, corev1.LabelTopologyZone) - - pvc1 := &corev1.PersistentVolumeClaim{ - ObjectMeta: metav1.ObjectMeta{ - Name: "pvc-claim-1", - Namespace: vm.Namespace, - Annotations: map[string]string{ - "csi.vsphere.volume-accessible-topology": fmt.Sprintf(`[{"topology.kubernetes.io/zone":"%s"}]`, azName), - }, - }, - Spec: corev1.PersistentVolumeClaimSpec{ - StorageClassName: ptr.To(ctx.StorageClassName), - }, - Status: corev1.PersistentVolumeClaimStatus{ - Phase: corev1.ClaimBound, - }, - } - Expect(ctx.Client.Create(ctx, pvc1)).To(Succeed()) - Expect(ctx.Client.Status().Update(ctx, pvc1)).To(Succeed()) - - vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOff - _, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - Expect(vm.Status.Zone).To(Equal(azName)) - }) - }) - - Context("When Instance Storage FSS is enabled", func() { - BeforeEach(func() { - testConfig.WithInstanceStorage = true - }) - - expectInstanceStorageVolumes := func( - vm *vmopv1.VirtualMachine, - isStorage vmopv1.InstanceStorage) { - - ExpectWithOffset(1, isStorage.Volumes).ToNot(BeEmpty()) - isVolumes := vmopv1util.FilterInstanceStorageVolumes(vm) - ExpectWithOffset(1, isVolumes).To(HaveLen(len(isStorage.Volumes))) - - for _, isVol := range isStorage.Volumes { - found := false - - for idx, vol := range isVolumes { - claim := vol.PersistentVolumeClaim.InstanceVolumeClaim - if claim.StorageClass == isStorage.StorageClass && claim.Size == isVol.Size { - isVolumes = append(isVolumes[:idx], isVolumes[idx+1:]...) - found = true - break - } - } - - ExpectWithOffset(1, found).To(BeTrue(), "failed to find instance storage volume for %v", isVol) - } - } - - It("creates VM without instance storage", func() { - _, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - }) - - It("create VM with instance storage", func() { - Expect(vm.Spec.Volumes).To(BeEmpty()) - - vmClass.Spec.Hardware.InstanceStorage = vmopv1.InstanceStorage{ - StorageClass: vm.Spec.StorageClass, - Volumes: []vmopv1.InstanceStorageVolume{ - { - Size: resource.MustParse("256Gi"), - }, - { - Size: resource.MustParse("512Gi"), - }, - }, - } - Expect(ctx.Client.Update(ctx, vmClass)).To(Succeed()) - - Expect(vmopv1util.IsInstanceStoragePresent(vm)).To(BeFalse()) - - _, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).To(MatchError(vsphere.ErrAddedInstanceStorageVols)) - - By("Instance storage volumes should be added to VM", func() { - Expect(vmopv1util.IsInstanceStoragePresent(vm)).To(BeTrue()) - expectInstanceStorageVolumes(vm, vmClass.Spec.Hardware.InstanceStorage) - }) - - _, err = createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).To(MatchError("instance storage PVCs are not bound yet")) - Expect(vmopv1util.IsInstanceStoragePresent(vm)).To(BeTrue()) - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionCreated)).To(BeFalse()) - - By("Placement should have been done", func() { - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionPlacementReady)).To(BeTrue()) - Expect(vm.Annotations).To(HaveKey(constants.InstanceStorageSelectedNodeAnnotationKey)) - Expect(vm.Annotations).To(HaveKey(constants.InstanceStorageSelectedNodeMOIDAnnotationKey)) - }) - - isVol0 := vm.Spec.Volumes[0] - Expect(isVol0.PersistentVolumeClaim.InstanceVolumeClaim).ToNot(BeNil()) - - By("simulate volume controller workflow", func() { - // Simulate what would be set by volume controller. - vm.Annotations[constants.InstanceStoragePVCsBoundAnnotationKey] = "" - - _, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("one or more persistent volumes is pending")) - Expect(err.Error()).To(ContainSubstring(isVol0.Name)) - - // Simulate what would be set by the volume controller. - for _, vol := range vm.Spec.Volumes { - vm.Status.Volumes = append(vm.Status.Volumes, vmopv1.VirtualMachineVolumeStatus{ - Name: vol.Name, - Attached: true, - }) - } - }) - - By("VM is now created", func() { - _, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionCreated)).To(BeTrue()) - }) - }) - }) - - It("Powers VM off", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOn)) - - vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOff - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) - state, err := vcVM.PowerState(ctx) - Expect(err).ToNot(HaveOccurred()) - Expect(state).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOff)) - }) - - It("returns error when StorageClass is required but none specified", func() { - vm.Spec.StorageClass = "" - err := createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).To(MatchError("StorageClass is required but not specified")) - - c := conditions.Get(vm, vmopv1.VirtualMachineConditionStorageReady) - Expect(c).ToNot(BeNil()) - expectedCondition := conditions.FalseCondition( - vmopv1.VirtualMachineConditionStorageReady, - "StorageClassRequired", - "StorageClass is required but not specified") - Expect(*c).To(conditions.MatchCondition(*expectedCondition)) - }) - - It("Can be called multiple times", func() { - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - modified := o.Config.Modified - - _, err = createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - - // Try to assert nothing changed. - Expect(o.Config.Modified).To(Equal(modified)) - }) - - Context("VM Metadata", func() { - - Context("ExtraConfig Transport", func() { - var ec map[string]interface{} - - JustBeforeEach(func() { - configMap := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - GenerateName: "md-configmap-", - Namespace: vm.Namespace, - }, - Data: map[string]string{ - "foo.bar": "should-be-ignored", - "guestinfo.Foo": "foo", - }, - } - Expect(ctx.Client.Create(ctx, configMap)).To(Succeed()) - - /* - vm.Spec.VmMetadata = &vmopv1.VirtualMachineMetadata{ - ConfigMapName: configMap.Name, - Transport: vmopv1.VirtualMachineMetadataExtraConfigTransport, - } - */ - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - - ec = map[string]interface{}{} - for _, option := range o.Config.ExtraConfig { - if val := option.GetOptionValue(); val != nil { - ec[val.Key] = val.Value.(string) - } - } - }) - - AfterEach(func() { - ec = nil - }) - - // TODO: As is we can't really honor "guestinfo.*" prefix - XIt("Metadata data is included in ExtraConfig", func() { - Expect(ec).ToNot(HaveKey("foo.bar")) - Expect(ec).To(HaveKeyWithValue("guestinfo.Foo", "foo")) - - By("Should include default keys and values", func() { - Expect(ec).To(HaveKeyWithValue("disk.enableUUID", "TRUE")) - Expect(ec).To(HaveKeyWithValue("vmware.tools.gosc.ignoretoolscheck", "TRUE")) - }) - }) - - Context("JSON_EXTRA_CONFIG is specified", func() { - BeforeEach(func() { - b, err := json.Marshal( - struct { - Foo string - Bar string - }{ - Foo: "f00", - Bar: "42", - }, - ) - Expect(err).ToNot(HaveOccurred()) - testConfig.WithJSONExtraConfig = string(b) - }) - - It("Global config is included in ExtraConfig", func() { - Expect(ec).To(HaveKeyWithValue("Foo", "f00")) - Expect(ec).To(HaveKeyWithValue("Bar", "42")) - }) - }) - }) - }) - - Context("Network", func() { - - It("Should not have a nic", func() { - Expect(vm.Spec.Network.Disabled).To(BeTrue()) - - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - Expect(conditions.Has(vm, vmopv1.VirtualMachineConditionNetworkReady)).To(BeFalse()) - - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - - devList := object.VirtualDeviceList(o.Config.Hardware.Device) - l := devList.SelectByType(&vimtypes.VirtualEthernetCard{}) - Expect(l).To(BeEmpty()) - }) - - Context("Multiple NICs are specified", func() { - BeforeEach(func() { - testConfig.WithNetworkEnv = builder.NetworkEnvNamed - - vm.Spec.Network.Disabled = false - vm.Spec.Network.Interfaces = []vmopv1.VirtualMachineNetworkInterfaceSpec{ - { - Name: "eth0", - Network: &vmopv1common.PartialObjectRef{Name: "VM Network"}, - }, - { - Name: "eth1", - Network: &vmopv1common.PartialObjectRef{Name: dvpgName}, - }, - } - }) - - It("Has expected devices", func() { - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionNetworkReady)).To(BeTrue()) - - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - - devList := object.VirtualDeviceList(o.Config.Hardware.Device) - l := devList.SelectByType(&vimtypes.VirtualEthernetCard{}) - Expect(l).To(HaveLen(2)) - - dev1 := l[0].GetVirtualDevice() - backing1, ok := dev1.Backing.(*vimtypes.VirtualEthernetCardNetworkBackingInfo) - Expect(ok).Should(BeTrue()) - Expect(backing1.DeviceName).To(Equal("VM Network")) - - dev2 := l[1].GetVirtualDevice() - backing2, ok := dev2.Backing.(*vimtypes.VirtualEthernetCardDistributedVirtualPortBackingInfo) - Expect(ok).Should(BeTrue()) - _, dvpg := getDVPG(ctx, dvpgName) - Expect(backing2.Port.PortgroupKey).To(Equal(dvpg.Reference().Value)) - }) - }) - }) - - Context("Disks", func() { - - Context("VM has thin provisioning", func() { - BeforeEach(func() { - if vm.Spec.Advanced == nil { - vm.Spec.Advanced = &vmopv1.VirtualMachineAdvancedSpec{} - } - vm.Spec.Advanced.DefaultVolumeProvisioningMode = vmopv1.VolumeProvisioningModeThin - }) - - It("Succeeds", func() { - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - - _, backing := getVMHomeDisk(ctx, vcVM, o) - Expect(backing.ThinProvisioned).To(PointTo(BeTrue())) - }) - }) - - XContext("VM has thick provisioning", func() { - BeforeEach(func() { - vm.Spec.Advanced.DefaultVolumeProvisioningMode = vmopv1.VolumeProvisioningModeThick - }) - - It("Succeeds", func() { - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - - /* vcsim CL deploy has "thick" but that isn't reflected for this disk. */ - _, backing := getVMHomeDisk(ctx, vcVM, o) - Expect(backing.ThinProvisioned).To(PointTo(BeFalse())) - }) - }) - - XContext("VM has eager zero provisioning", func() { - BeforeEach(func() { - if vm.Spec.Advanced == nil { - vm.Spec.Advanced = &vmopv1.VirtualMachineAdvancedSpec{} - } - vm.Spec.Advanced.DefaultVolumeProvisioningMode = vmopv1.VolumeProvisioningModeThickEagerZero - }) - - It("Succeeds", func() { - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - - /* vcsim CL deploy has "eagerZeroedThick" but that isn't reflected for this disk. */ - _, backing := getVMHomeDisk(ctx, vcVM, o) - Expect(backing.EagerlyScrub).To(PointTo(BeTrue())) - }) - }) - - Context("Should resize root disk", func() { - It("Succeeds", func() { - newSize := resource.MustParse("4242Gi") - - if vm.Spec.Advanced == nil { - vm.Spec.Advanced = &vmopv1.VirtualMachineAdvancedSpec{} - } - vm.Spec.Advanced.BootDiskCapacity = &newSize - vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOn - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - disk, _ := getVMHomeDisk(ctx, vcVM, o) - Expect(disk.CapacityInBytes).To(BeEquivalentTo(newSize.Value())) - }) - }) - }) - - Context("Snapshot revert", func() { - var ( - vmSnapshot *vmopv1.VirtualMachineSnapshot - ) - - BeforeEach(func() { - testConfig.WithVMSnapshots = true - vmSnapshot = builder.DummyVirtualMachineSnapshot("", "test-revert-snap", vm.Name) - }) - - JustBeforeEach(func() { - vmSnapshot.Namespace = nsInfo.Namespace - }) - - Context("findDesiredSnapshot error handling", func() { - It("should return regular error (not NoRequeueError) when multiple snapshots exist", func() { - // Create VM first to get vcVM reference - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - // Create multiple snapshots with the same name - task, err := vcVM.CreateSnapshot(ctx, vmSnapshot.Name, "first snapshot", false, false) - Expect(err).ToNot(HaveOccurred()) - Expect(task.Wait(ctx)).To(Succeed()) - - task, err = vcVM.CreateSnapshot(ctx, vmSnapshot.Name, "second snapshot", false, false) - Expect(err).ToNot(HaveOccurred()) - Expect(task.Wait(ctx)).To(Succeed()) - - // Mark the snapshot as ready. - conditions.MarkTrue(vmSnapshot, vmopv1.VirtualMachineSnapshotReadyCondition) - // Create the snapshot CR to which the VM should revert - Expect(ctx.Client.Create(ctx, vmSnapshot)).To(Succeed()) - - // Snapshot should be owned by the VM resource. - o := vmopv1.VirtualMachine{} - Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(vm), &o)).To(Succeed()) - Expect(controllerutil.SetOwnerReference(&o, vmSnapshot, ctx.Scheme)).To(Succeed()) - Expect(ctx.Client.Update(ctx, vmSnapshot)).To(Succeed()) - - vm.Spec.CurrentSnapshotName = vmSnapshot.Name - - // This should return an error because findDesiredSnapshot should return an error - // when there are multiple snapshots with the same name - err = createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("resolves to 2 snapshots")) - - // Verify that the error causes a requeue (not a NoRequeueError) - Expect(pkgerr.IsNoRequeueError(err)).To(BeFalse(), "Multiple snapshots error should cause requeue") - }) - }) - - Context("when VM has no snapshots", func() { - BeforeEach(func() { - vm.Spec.CurrentSnapshotName = vmSnapshot.Name - }) - - It("should not trigger a revert (new snapshot workflow)", func() { - // Create the snapshot CR but don't create actual vCenter snapshot - conditions.MarkTrue(vmSnapshot, vmopv1.VirtualMachineSnapshotReadyCondition) - Expect(ctx.Client.Create(ctx, vmSnapshot)).To(Succeed()) - - // Snapshot should be owned by the VM resource. - o := vmopv1.VirtualMachine{} - Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(vm), &o)).To(Succeed()) - Expect(controllerutil.SetOwnerReference(&o, vmSnapshot, ctx.Scheme)).To(Succeed()) - Expect(ctx.Client.Update(ctx, vmSnapshot)).To(Succeed()) - - err := createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("no snapshots for this VM")) - }) - }) - - Context("when desired snapshot CR doesn't exist", func() { - BeforeEach(func() { - vm.Spec.CurrentSnapshotName = vmSnapshot.Name - }) - - It("should fail with snapshot CR not found error", func() { - err := createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("virtualmachinesnapshots.vmoperator.vmware.com \"test-revert-snap\" not found")) - - Expect(conditions.IsFalse(vm, - vmopv1.VirtualMachineSnapshotRevertSucceeded, - )).To(BeTrue()) - Expect(conditions.GetReason(vm, - vmopv1.VirtualMachineSnapshotRevertSucceeded, - )).To(Equal(vmopv1.VirtualMachineSnapshotRevertFailedReason)) - }) - }) - - Context("when desired snapshot CR is not ready", func() { - BeforeEach(func() { - vm.Spec.CurrentSnapshotName = vmSnapshot.Name - }) - - JustBeforeEach(func() { - // Create snapshot CR but don't mark it as ready. - Expect(ctx.Client.Create(ctx, vmSnapshot)).To(Succeed()) - - // Snapshot should be owned by the VM resource. - o := vmopv1.VirtualMachine{} - Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(vm), &o)).To(Succeed()) - Expect(controllerutil.SetOwnerReference(&o, vmSnapshot, ctx.Scheme)).To(Succeed()) - Expect(ctx.Client.Update(ctx, vmSnapshot)).To(Succeed()) - }) - - When("snapshot is not created", func() { - It("should fail with snapshot CR not ready error", func() { - err := createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring( - fmt.Sprintf("skipping revert for not-ready snapshot %q", - vmSnapshot.Name))) - }) - }) - - When("snapshot is created but not ready", func() { - It("should fail with snapshot CR not ready error", func() { - // Mark the snapshot as created but not ready. - conditions.MarkTrue(vmSnapshot, vmopv1.VirtualMachineSnapshotCreatedCondition) - Expect(ctx.Client.Status().Update(ctx, vmSnapshot)).To(Succeed()) - - err := createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring( - fmt.Sprintf("skipping revert for not-ready snapshot %q", - vmSnapshot.Name))) - }) - }) - }) - - Context("revert to current snapshot", func() { - It("should succeed", func() { - // Create snapshot CR to trigger a snapshot workflow. - Expect(ctx.Client.Create(ctx, vmSnapshot)).To(Succeed()) - - // Snapshot should be owned by the VM resource. - o := vmopv1.VirtualMachine{} - Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(vm), &o)).To(Succeed()) - Expect(controllerutil.SetOwnerReference(&o, vmSnapshot, ctx.Scheme)).To(Succeed()) - Expect(ctx.Client.Update(ctx, vmSnapshot)).To(Succeed()) - // Create VM so snapshot is also created. - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - - // Mark the snapshot as ready so that revert can proceed. - Expect(ctx.Client.Get(ctx, - client.ObjectKeyFromObject(vmSnapshot), vmSnapshot)).To(Succeed()) - conditions.MarkTrue(vmSnapshot, vmopv1.VirtualMachineSnapshotReadyCondition) - Expect(ctx.Client.Status().Update(ctx, vmSnapshot)).To(Succeed()) - - // Set desired snapshot to point to the above snapshot. - vm.Spec.CurrentSnapshotName = vmSnapshot.Name - - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - - // Verify VM status reflects current snapshot. - Expect(vm.Status.CurrentSnapshot).ToNot(BeNil()) - Expect(vm.Status.CurrentSnapshot.Type).To(Equal(vmopv1.VirtualMachineSnapshotReferenceTypeManaged)) - Expect(vm.Status.CurrentSnapshot.Name).To(Equal(vmSnapshot.Name)) - - // Verify the status has root snapshots. - Expect(vm.Status.RootSnapshots).ToNot(BeNil()) - Expect(vm.Status.RootSnapshots).To(HaveLen(1)) - Expect(vm.Status.RootSnapshots[0].Type).To(Equal(vmopv1.VirtualMachineSnapshotReferenceTypeManaged)) - Expect(vm.Status.RootSnapshots[0].Name).To(Equal(vmSnapshot.Name)) - }) - }) - - Context("when reverting to valid snapshot", func() { - var secondSnapshot *vmopv1.VirtualMachineSnapshot - - It("should successfully revert to desired snapshot", func() { - // Create VM first - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - // Create first snapshot in vCenter - task, err := vcVM.CreateSnapshot(ctx, vmSnapshot.Name, "first snapshot", false, false) - Expect(err).ToNot(HaveOccurred()) - Expect(task.Wait(ctx)).To(Succeed()) - - // Create first snapshot CR - // Mark the snapshot as created so that the snapshot workflow doesn't try to create it. - conditions.MarkTrue(vmSnapshot, vmopv1.VirtualMachineSnapshotCreatedCondition) - // Mark the snapshot as ready so that the revert snapshot workflow can proceed. - conditions.MarkTrue(vmSnapshot, vmopv1.VirtualMachineSnapshotReadyCondition) - Expect(ctx.Client.Create(ctx, vmSnapshot)).To(Succeed()) - - // Snapshot should be owned by the VM resource. - o := vmopv1.VirtualMachine{} - Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(vm), &o)).To(Succeed()) - Expect(controllerutil.SetOwnerReference(&o, vmSnapshot, ctx.Scheme)).To(Succeed()) - Expect(ctx.Client.Update(ctx, vmSnapshot)).To(Succeed()) - - // Create second snapshot - secondSnapshot = builder.DummyVirtualMachineSnapshot("", "test-second-snap", vm.Name) - secondSnapshot.Namespace = nsInfo.Namespace - - task, err = vcVM.CreateSnapshot(ctx, secondSnapshot.Name, "second snapshot", false, false) - Expect(err).ToNot(HaveOccurred()) - Expect(task.Wait(ctx)).To(Succeed()) - - // Create second snapshot CR - // Mark the snapshot as completed so that the snapshot workflow doesn't try to create it. - conditions.MarkTrue(secondSnapshot, vmopv1.VirtualMachineSnapshotCreatedCondition) - // Mark the snapshot as ready so that the revert snapshot workflow can proceed. - conditions.MarkTrue(secondSnapshot, vmopv1.VirtualMachineSnapshotReadyCondition) - // Snapshot should be owned by the VM resource. - Expect(controllerutil.SetOwnerReference(&o, secondSnapshot, ctx.Scheme)).To(Succeed()) - Expect(ctx.Client.Create(ctx, secondSnapshot)).To(Succeed()) - - // Set desired snapshot to first snapshot (revert from second to first) - vm.Spec.CurrentSnapshotName = vmSnapshot.Name - - By("First reconcile should return ErrSnapshotRevert", func() { - _, createErr := vmProvider.CreateOrUpdateVirtualMachineAsync(ctx, vm) - Expect(createErr).To(HaveOccurred()) - Expect(errors.Is(createErr, vsphere.ErrSnapshotRevert)).To(BeTrue()) - Expect(pkgerr.IsNoRequeueError(createErr)).To(BeTrue(), "Should return NoRequeueError") - }) - - err = createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - // Verify VM status reflects the reverted snapshot - Expect(vm.Status.CurrentSnapshot).ToNot(BeNil()) - Expect(vm.Status.CurrentSnapshot.Type).To(Equal(vmopv1.VirtualMachineSnapshotReferenceTypeManaged)) - Expect(vm.Status.CurrentSnapshot.Name).To(Equal(vmSnapshot.Name)) - - // Verify the spec.currentSnapshot is cleared. - Expect(vm.Spec.CurrentSnapshotName).To(BeEmpty()) - - // Verify the status has root snapshots. - Expect(vm.Status.RootSnapshots).ToNot(BeNil()) - Expect(vm.Status.RootSnapshots).To(HaveLen(1)) - Expect(vm.Status.RootSnapshots[0].Type).To(Equal(vmopv1.VirtualMachineSnapshotReferenceTypeManaged)) - Expect(vm.Status.RootSnapshots[0].Name).To(Equal(vmSnapshot.Name)) - - // Verify the snapshot is actually current in vCenter - var moVM mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), []string{"snapshot"}, &moVM)).To(Succeed()) - Expect(moVM.Snapshot).ToNot(BeNil()) - Expect(moVM.Snapshot.CurrentSnapshot).ToNot(BeNil()) - - // Find the snapshot name in the tree to verify it matches - currentSnap, err := virtualmachine.FindSnapshot(moVM, moVM.Snapshot.CurrentSnapshot.Value) - Expect(err).ToNot(HaveOccurred()) - Expect(currentSnap).ToNot(BeNil()) - Expect(currentSnap.Name).To(Equal(vmSnapshot.Name)) - }) - - Context("and the snapshot was taken when VM was powered on and is now powered off", func() { - It("should successfully power on the VM after reverting to a Snapshot in PoweredOn state", func() { - // Create VM first - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - // Create first snapshot in vCenter - task, err := vcVM.CreateSnapshot(ctx, vmSnapshot.Name, "first snapshot", false, false) - Expect(err).ToNot(HaveOccurred()) - Expect(task.Wait(ctx)).To(Succeed()) - - // Create first snapshot CR - // Mark the snapshot as completed so that the snapshot workflow doesn't try to create it. - conditions.MarkTrue(vmSnapshot, vmopv1.VirtualMachineSnapshotCreatedCondition) - // Mark the snapshot as ready so that the revert snapshot workflow can proceed. - conditions.MarkTrue(vmSnapshot, vmopv1.VirtualMachineSnapshotReadyCondition) - Expect(ctx.Client.Create(ctx, vmSnapshot)).To(Succeed()) - - // Verify the snapshot is actually current in vCenter - var moVM mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), []string{"snapshot"}, &moVM)).To(Succeed()) - Expect(moVM.Snapshot).ToNot(BeNil()) - - // verify that the snapshot's power state is powered off - currentSnapshot, err := virtualmachine.FindSnapshot(moVM, moVM.Snapshot.CurrentSnapshot.Value) - Expect(err).ToNot(HaveOccurred()) - Expect(currentSnapshot).ToNot(BeNil()) - Expect(currentSnapshot.State).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOff)) - - // Snapshot should be owned by the VM resource. - Expect(controllerutil.SetOwnerReference(vm, vmSnapshot, ctx.Scheme)).To(Succeed()) - Expect(ctx.Client.Update(ctx, vmSnapshot)).To(Succeed()) - - // Create second snapshot - secondSnapshot = builder.DummyVirtualMachineSnapshotWithMemory("", "test-second-snap", vm.Name) - secondSnapshot.Namespace = nsInfo.Namespace - - task, err = vcVM.CreateSnapshot(ctx, secondSnapshot.Name, "second snapshot", true, false) - Expect(err).ToNot(HaveOccurred()) - Expect(task.Wait(ctx)).To(Succeed()) - - // Create second snapshot CR - // Mark the snapshot as completed so that the snapshot workflow doesn't try to create it. - conditions.MarkTrue(secondSnapshot, vmopv1.VirtualMachineSnapshotCreatedCondition) - // Mark the snapshot as ready so that the revert snapshot workflow can proceed. - conditions.MarkTrue(secondSnapshot, vmopv1.VirtualMachineSnapshotReadyCondition) - // Snapshot should be owned by the VM resource. - Expect(controllerutil.SetOwnerReference(vm, secondSnapshot, ctx.Scheme)).To(Succeed()) - Expect(ctx.Client.Create(ctx, secondSnapshot)).To(Succeed()) - - // Verify the VM is powered on - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOn)) - state, err := vcVM.PowerState(ctx) - Expect(err).ToNot(HaveOccurred()) - Expect(state).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOn)) - - // Set desired snapshot to first snapshot (revert from second to first) - vm.Spec.CurrentSnapshotName = vmSnapshot.Name - - // Revert to the first snapshot - err = createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - // Verify VM status reflects the reverted snapshot - Expect(vm.Status.CurrentSnapshot).ToNot(BeNil()) - Expect(vm.Status.CurrentSnapshot.Type).To(Equal(vmopv1.VirtualMachineSnapshotReferenceTypeManaged)) - Expect(vm.Status.CurrentSnapshot.Name).To(Equal(vmSnapshot.Name)) - - // Verify the spec.currentSnapshot is cleared. - Expect(vm.Spec.CurrentSnapshotName).To(BeEmpty()) - - // Verify the VM is powered off - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) - state, err = vcVM.PowerState(ctx) - Expect(err).ToNot(HaveOccurred()) - Expect(state).To(Equal(vimtypes.VirtualMachinePowerStatePoweredOff)) - }) - }) - }) - - // Simulate an Imported Snapshot scenario by - // - creating the VC VM and VM while ensuring the backup is not taken. - // - creating a snapshot on VC AND THEN only creating the VMSnapshot CR so that the ExtraConfig is - // not stamped by the controller. - // - change some bits in the VM CR and take a second snapshot. This snapshot can be taken through a - // VMSnapshot. This is needed to make sure that we are not reverting to a snapshot that the VM is - // running off at the same time. - // - Now, revert the VM to the first snapshot. It is expected that the spec fields would now be approximated. - Context("when reverting to imported snapshot", func() { - var secondSnapshot *vmopv1.VirtualMachineSnapshot - - BeforeEach(func() { - pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { - config.Features.VMImportNewNet = true - }) - }) - It("should fail the revert if the snapshot wasn't imported", func() { - if vm.Labels == nil { - vm.Labels = make(map[string]string) - } - - // skip creation of backup VMResourceYAMLExtraConfigKey - // by setting the CAPV cluster role label - vm.Labels[kubeutil.CAPVClusterRoleLabelKey] = "" - - // Create VM first - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - // make sure the VM doesn't have the ExtraConfig stamped - var moVM mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &moVM)).To(Succeed()) - Expect(moVM.Config.ExtraConfig).ToNot(BeNil()) - ecMap := pkgutil.OptionValues(moVM.Config.ExtraConfig).StringMap() - Expect(ecMap).ToNot(HaveKey(backupapi.VMResourceYAMLExtraConfigKey)) - - // Create first snapshot in vCenter - task, err := vcVM.CreateSnapshot( - ctx, vmSnapshot.Name, "first snapshot", false, false) - Expect(err).ToNot(HaveOccurred()) - Expect(task.Wait(ctx)).To(Succeed()) - - // Create first snapshot CR and Mark the snapshot as ready - // so that the snapshot workflow doesn't try to create it. - conditions.MarkTrue(vmSnapshot, vmopv1.VirtualMachineSnapshotReadyCondition) - Expect(ctx.Client.Create(ctx, vmSnapshot)).To(Succeed()) - - // Snapshot should be owned by the VM resource. - o := vmopv1.VirtualMachine{} - Expect(ctx.Client.Get( - ctx, client.ObjectKeyFromObject(vm), &o)).To(Succeed()) - Expect(controllerutil.SetOwnerReference( - &o, vmSnapshot, ctx.Scheme)).To(Succeed()) - Expect(ctx.Client.Update(ctx, vmSnapshot)).To(Succeed()) - - // mark the snapshot as ready because snapshot workflow - // will skip because of the CAPV cluster role label - cur := &vmopv1.VirtualMachineSnapshot{} - Expect(ctx.Client.Get( - ctx, client.ObjectKeyFromObject(vmSnapshot), cur)).To(Succeed()) - conditions.MarkTrue(cur, vmopv1.VirtualMachineSnapshotReadyCondition) - Expect(ctx.Client.Status().Update(ctx, cur)).To(Succeed()) - - // we don't need the CAPI label anymore - labels := vm.Labels - delete(labels, kubeutil.CAPVClusterRoleLabelKey) - vm.Labels = labels - Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) - - // modify the VM Spec to tinker with some flag - Expect(vm.Spec.PowerOffMode).To(Equal(vmopv1.VirtualMachinePowerOpModeHard)) - vm.Spec.PowerOffMode = vmopv1.VirtualMachinePowerOpModeSoft - Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) - - // Create second snapshot - secondSnapshot = builder.DummyVirtualMachineSnapshot("", "test-second-snap", vm.Name) - secondSnapshot.Namespace = nsInfo.Namespace - - task, err = vcVM.CreateSnapshot(ctx, secondSnapshot.Name, "second snapshot", false, false) - Expect(err).ToNot(HaveOccurred()) - Expect(task.Wait(ctx)).To(Succeed()) - - // Create second snapshot CR and Mark the snapshot as ready - // so that the snapshot workflow doesn't try to create it. - conditions.MarkTrue(secondSnapshot, vmopv1.VirtualMachineSnapshotReadyCondition) - // Snapshot should be owned by the VM resource. - Expect(controllerutil.SetOwnerReference(&o, secondSnapshot, ctx.Scheme)).To(Succeed()) - Expect(ctx.Client.Create(ctx, secondSnapshot)).To(Succeed()) - - // Set desired snapshot to first snapshot (perform a revert from second to first) - vm.Spec.CurrentSnapshotName = vmSnapshot.Name - - err = createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("no VM YAML in snapshot config")) - Expect(conditions.IsFalse(vm, - vmopv1.VirtualMachineSnapshotRevertSucceeded)).To(BeTrue()) - Expect(conditions.GetReason(vm, - vmopv1.VirtualMachineSnapshotRevertSucceeded, - )).To(Equal(vmopv1.VirtualMachineSnapshotRevertFailedInvalidVMManifestReason)) - }) - - It("should successfully revert to desired snapshot and approximate the VM Spec", func() { - if vm.Labels == nil { - vm.Labels = make(map[string]string) - } - - // skip creation of backup VMResourceYAMLExtraConfigKey by setting the CAPV cluster role label - vm.Labels[kubeutil.CAPVClusterRoleLabelKey] = "" - - // Create VM first - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - // make sure the VM doesn't have the ExtraConfig stamped - var moVM mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &moVM)).To(Succeed()) - Expect(moVM.Config.ExtraConfig).ToNot(BeNil()) - ecMap := pkgutil.OptionValues(moVM.Config.ExtraConfig).StringMap() - Expect(ecMap).ToNot(HaveKey(backupapi.VMResourceYAMLExtraConfigKey)) - - // Create first snapshot in vCenter - task, err := vcVM.CreateSnapshot(ctx, vmSnapshot.Name, "first snapshot", false, false) - Expect(err).ToNot(HaveOccurred()) - Expect(task.Wait(ctx)).To(Succeed()) - - // Create first snapshot CR - // Mark the snapshot as created so that the snapshot workflow doesn't try to create it. - conditions.MarkTrue(vmSnapshot, vmopv1.VirtualMachineSnapshotCreatedCondition) - // Mark the snapshot as ready so that the snapshot workflow doesn't try to create it. - conditions.MarkTrue(vmSnapshot, vmopv1.VirtualMachineSnapshotReadyCondition) - vmSnapshot.Annotations[vmopv1.ImportedSnapshotAnnotation] = "" - Expect(ctx.Client.Create(ctx, vmSnapshot)).To(Succeed()) - - // Snapshot should be owned by the VM resource. - o := vmopv1.VirtualMachine{} - Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(vm), &o)).To(Succeed()) - Expect(controllerutil.SetOwnerReference(&o, vmSnapshot, ctx.Scheme)).To(Succeed()) - Expect(ctx.Client.Update(ctx, vmSnapshot)).To(Succeed()) - - // mark the snapshot as ready because snapshot workflow will skip because of the CAPV cluster role label - cur := &vmopv1.VirtualMachineSnapshot{} - Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(vmSnapshot), cur)).To(Succeed()) - conditions.MarkTrue(cur, vmopv1.VirtualMachineSnapshotReadyCondition) - Expect(ctx.Client.Status().Update(ctx, cur)).To(Succeed()) - - // we don't need the CAPI label anymore - labels := vm.Labels - delete(labels, kubeutil.CAPVClusterRoleLabelKey) - vm.Labels = labels - Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) - - // modify the VM Spec to tinker with some flag - Expect(vm.Spec.PowerOffMode).To(Equal(vmopv1.VirtualMachinePowerOpModeHard)) - vm.Spec.PowerOffMode = vmopv1.VirtualMachinePowerOpModeSoft - Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) - - // Create second snapshot - secondSnapshot = builder.DummyVirtualMachineSnapshot("", "test-second-snap", vm.Name) - secondSnapshot.Namespace = nsInfo.Namespace - - task, err = vcVM.CreateSnapshot(ctx, secondSnapshot.Name, "second snapshot", false, false) - Expect(err).ToNot(HaveOccurred()) - Expect(task.Wait(ctx)).To(Succeed()) - - // Create second snapshot CR - // Mark the snapshot as created so that the snapshot workflow doesn't try to create it. - conditions.MarkTrue(secondSnapshot, vmopv1.VirtualMachineSnapshotCreatedCondition) - // Mark the snapshot as ready so that the revert snapshot workflow can proceed. - conditions.MarkTrue(secondSnapshot, vmopv1.VirtualMachineSnapshotReadyCondition) - // Snapshot should be owned by the VM resource. - Expect(controllerutil.SetOwnerReference(&o, secondSnapshot, ctx.Scheme)).To(Succeed()) - Expect(ctx.Client.Create(ctx, secondSnapshot)).To(Succeed()) - - // Set desired snapshot to first snapshot (perform a revert from second to first) - vm.Spec.CurrentSnapshotName = vmSnapshot.Name - - err = createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - // Verify VM status reflects the reverted snapshot - Expect(vm.Status.CurrentSnapshot).ToNot(BeNil()) - Expect(vm.Status.CurrentSnapshot.Type).To(Equal(vmopv1.VirtualMachineSnapshotReferenceTypeManaged)) - Expect(vm.Status.CurrentSnapshot.Name).To(Equal(vmSnapshot.Name)) - - // Verify the revert operation reverted to the expected values - Expect(vm.Spec.PowerOffMode).To(Equal(vmopv1.VirtualMachinePowerOpModeTrySoft)) - Expect(vm.Spec.Volumes).To(BeEmpty()) - }) - }) - - Context("when VM spec has nil CurrentSnapshot, but the VC VM has a snapshot", func() { - It("should not attempt revert and update status correctly", func() { - - // Create VM with snapshot but don't set desired snapshot - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - // Create snapshot in vCenter - task, err := vcVM.CreateSnapshot(ctx, vmSnapshot.Name, "test snapshot", false, false) - Expect(err).ToNot(HaveOccurred()) - Expect(task.Wait(ctx)).To(Succeed()) - - // Create snapshot CR with the owner reference to the VM. - Expect(ctx.Client.Create(ctx, vmSnapshot)).To(Succeed()) - - o := vmopv1.VirtualMachine{} - Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(vm), &o)).To(Succeed()) - Expect(controllerutil.SetOwnerReference(&o, vmSnapshot, ctx.Scheme)).To(Succeed()) - Expect(ctx.Client.Update(ctx, vmSnapshot)).To(Succeed()) - - // Explicitly set CurrentSnapshot to nil - vm.Spec.CurrentSnapshotName = "" - - err = createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - // Status should reflect the actual current snapshot - Expect(vm.Status.CurrentSnapshot).ToNot(BeNil()) - Expect(vm.Status.CurrentSnapshot.Type).To(Equal(vmopv1.VirtualMachineSnapshotReferenceTypeManaged)) - Expect(vm.Status.CurrentSnapshot.Name).To(Equal(vmSnapshot.Name)) - - // Verify the status has root snapshots. - Expect(vm.Status.RootSnapshots).ToNot(BeNil()) - Expect(vm.Status.RootSnapshots).To(HaveLen(1)) - Expect(vm.Status.RootSnapshots[0].Type).To(Equal(vmopv1.VirtualMachineSnapshotReferenceTypeManaged)) - Expect(vm.Status.RootSnapshots[0].Name).To(Equal(vmSnapshot.Name)) - }) - }) - - Context("when VM is a VKS/TKG node", func() { - It("should skip snapshot revert for VKS/TKG nodes", func() { - // Add CAPI labels to mark VM as VKS/TKG node - if vm.Labels == nil { - vm.Labels = make(map[string]string) - } - vm.Labels[kubeutil.CAPWClusterRoleLabelKey] = "worker" - Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) - - // Create VM first - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - // Create snapshot in vCenter - task, err := vcVM.CreateSnapshot(ctx, vmSnapshot.Name, "test snapshot", false, false) - Expect(err).ToNot(HaveOccurred()) - Expect(task.Wait(ctx)).To(Succeed()) - - // Create snapshot CR and mark it as ready - conditions.MarkTrue(vmSnapshot, vmopv1.VirtualMachineSnapshotReadyCondition) - Expect(ctx.Client.Create(ctx, vmSnapshot)).To(Succeed()) - - // Snapshot should be owned by the VM resource. - o := vmopv1.VirtualMachine{} - Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(vm), &o)).To(Succeed()) - Expect(controllerutil.SetOwnerReference(&o, vmSnapshot, ctx.Scheme)).To(Succeed()) - Expect(ctx.Client.Update(ctx, vmSnapshot)).To(Succeed()) - - // Create a second snapshot in vCenter - secondSnapshot := builder.DummyVirtualMachineSnapshot("", "test-second-snap", vm.Name) - secondSnapshot.Namespace = nsInfo.Namespace - - task, err = vcVM.CreateSnapshot(ctx, vmSnapshot.Name, "test snapshot", false, false) - Expect(err).ToNot(HaveOccurred()) - Expect(task.Wait(ctx)).To(Succeed()) - - // Create snapshot CR and mark it as ready - conditions.MarkTrue(secondSnapshot, vmopv1.VirtualMachineSnapshotReadyCondition) - Expect(ctx.Client.Create(ctx, secondSnapshot)).To(Succeed()) - - // Snapshot should be owned by the VM resource. - o = vmopv1.VirtualMachine{} - Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(vm), &o)).To(Succeed()) - Expect(controllerutil.SetOwnerReference(&o, secondSnapshot, ctx.Scheme)).To(Succeed()) - Expect(ctx.Client.Update(ctx, secondSnapshot)).To(Succeed()) - - // Set desired snapshot to trigger a revert to the first snapshot. - vm.Spec.CurrentSnapshotName = vmSnapshot.Name - - err = createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - // VM status should still point to first snapshot because revert was skipped - Expect(vm.Status.CurrentSnapshot).ToNot(BeNil()) - Expect(conditions.IsFalse(vm, vmopv1.VirtualMachineSnapshotRevertSucceeded)).To(BeTrue()) - Expect(conditions.GetReason(vm, vmopv1.VirtualMachineSnapshotRevertSucceeded)).To(Equal(vmopv1.VirtualMachineSnapshotRevertSkippedReason)) - Expect(vm.Status.CurrentSnapshot.Type).To(Equal(vmopv1.VirtualMachineSnapshotReferenceTypeManaged)) - Expect(vm.Status.CurrentSnapshot.Name).To(Equal(vmSnapshot.Name)) - - // Verify the snapshot in vCenter is still the original one (no revert happened) - var moVM mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), []string{"snapshot"}, &moVM)).To(Succeed()) - Expect(moVM.Snapshot).ToNot(BeNil()) - Expect(moVM.Snapshot.CurrentSnapshot).ToNot(BeNil()) - - // The current snapshot name should still be the original - currentSnap, err := virtualmachine.FindSnapshot(moVM, moVM.Snapshot.CurrentSnapshot.Value) - Expect(err).ToNot(HaveOccurred()) - Expect(currentSnap).ToNot(BeNil()) - Expect(currentSnap.Name).To(Equal(vmSnapshot.Name)) - }) - }) - - Context("when snapshot revert annotation is present", func() { - It("should skip VM reconciliation when revert annotation exists", func() { - // Create VM first - _, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - // Set the revert in progress annotation manually - if vm.Annotations == nil { - vm.Annotations = make(map[string]string) - } - vm.Annotations[pkgconst.VirtualMachineSnapshotRevertInProgressAnnotationKey] = "" - Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) - - // Hack: set the label to indicate that this VM is a VKS node otherwise, a - // successful backup returns a NoRequeue error expecting the watcher to - // queue the request. - vm.Labels[kubeutil.CAPVClusterRoleLabelKey] = "" - - // Attempt to reconcile VM - should return NoRequeueError due to annotation - err = vmProvider.CreateOrUpdateVirtualMachine(ctx, vm) - Expect(err).To(HaveOccurred()) - Expect(pkgerr.IsNoRequeueError(err)).To(BeTrue(), "Should return NoRequeueError when annotation is present") - Expect(err.Error()).To(ContainSubstring("snapshot revert in progress")) - }) - }) - - Context("when snapshot revert fails and revert is aborted", func() { - It("should clear the revert succeeded condition", func() { - vm.Spec.CurrentSnapshotName = vmSnapshot.Name - - err := createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To( - ContainSubstring("virtualmachinesnapshots.vmoperator.vmware.com " + - "\"test-revert-snap\" not found")) - - Expect(conditions.IsFalse(vm, - vmopv1.VirtualMachineSnapshotRevertSucceeded, - )).To(BeTrue()) - Expect(conditions.GetReason(vm, - vmopv1.VirtualMachineSnapshotRevertSucceeded, - )).To(Equal(vmopv1.VirtualMachineSnapshotRevertFailedReason)) - - vm.Spec.CurrentSnapshotName = "" - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - - Expect(conditions.Get(vm, - vmopv1.VirtualMachineSnapshotRevertSucceeded), - ).To(BeNil()) - }) - }) - }) - - Context("CNS Volumes", func() { - cnsVolumeName := "cns-volume-1" - - It("CSI Volumes workflow", func() { - vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOff - _, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOn - By("Add CNS volume to VM", func() { - vm.Spec.Volumes = []vmopv1.VirtualMachineVolume{ - { - Name: cnsVolumeName, - VirtualMachineVolumeSource: vmopv1.VirtualMachineVolumeSource{ - PersistentVolumeClaim: &vmopv1.PersistentVolumeClaimVolumeSource{ - PersistentVolumeClaimVolumeSource: corev1.PersistentVolumeClaimVolumeSource{ - ClaimName: "pvc-volume-1", - }, - }, - }, - }, - } - - err := createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("one or more persistent volumes is pending")) - Expect(err.Error()).To(ContainSubstring(cnsVolumeName)) - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) - }) - - By("CNS volume is not attached", func() { - errMsg := "blah blah blah not attached" - - vm.Status.Volumes = []vmopv1.VirtualMachineVolumeStatus{ - { - Name: cnsVolumeName, - Attached: false, - Error: errMsg, - }, - } - - err := createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("one or more persistent volumes is pending")) - Expect(err.Error()).To(ContainSubstring(cnsVolumeName)) - - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) - }) - - By("CNS volume is attached", func() { - vm.Status.Volumes = []vmopv1.VirtualMachineVolumeStatus{ - { - Name: cnsVolumeName, - Attached: true, - }, - } - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOn)) - }) - }) - }) - - It("Reverse lookups existing VM into correct zone", func() { - _, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - Expect(vm.Labels).To(HaveKeyWithValue(corev1.LabelTopologyZone, zoneName)) - Expect(vm.Status.Zone).To(Equal(zoneName)) - delete(vm.Labels, corev1.LabelTopologyZone) - - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Labels).To(HaveKeyWithValue(corev1.LabelTopologyZone, zoneName)) - Expect(vm.Status.Zone).To(Equal(zoneName)) - }) - }) - - Context("VM SetResourcePolicy", func() { - var resourcePolicy *vmopv1.VirtualMachineSetResourcePolicy - - JustBeforeEach(func() { - resourcePolicyName := "test-policy" - resourcePolicy = getVirtualMachineSetResourcePolicy(resourcePolicyName, nsInfo.Namespace) - Expect(vmProvider.CreateOrUpdateVirtualMachineSetResourcePolicy(ctx, resourcePolicy)).To(Succeed()) - Expect(ctx.Client.Create(ctx, resourcePolicy)).To(Succeed()) - - vm.Annotations["vsphere-cluster-module-group"] = resourcePolicy.Spec.ClusterModuleGroups[0] - if vm.Spec.Reserved == nil { - vm.Spec.Reserved = &vmopv1.VirtualMachineReservedSpec{} - } - vm.Spec.Reserved.ResourcePolicyName = resourcePolicy.Name - }) - - AfterEach(func() { - resourcePolicy = nil - }) - - When("a cluster module is specified without resource policy", func() { - JustBeforeEach(func() { - vm.Spec.Reserved.ResourcePolicyName = "" - }) - - It("returns error", func() { - _, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("cannot set cluster module without resource policy")) - }) - }) - - It("VM is created in child Folder and ResourcePool", func() { - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - By("has expected condition", func() { - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionVMSetResourcePolicyReady)).To(BeTrue()) - }) - - By("has expected inventory path", func() { - Expect(vcVM.InventoryPath).To(HaveSuffix( - fmt.Sprintf("/%s/%s/%s", nsInfo.Namespace, resourcePolicy.Spec.Folder, vm.Name))) - }) - - By("has expected namespace resource pool", func() { - rp, err := vcVM.ResourcePool(ctx) - Expect(err).ToNot(HaveOccurred()) - childRP := ctx.GetResourcePoolForNamespace( - nsInfo.Namespace, - vm.Labels[corev1.LabelTopologyZone], - resourcePolicy.Spec.ResourcePool.Name) - Expect(childRP).ToNot(BeNil()) - Expect(rp.Reference().Value).To(Equal(childRP.Reference().Value)) - }) - }) - - It("Cluster Modules", func() { - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - var members []vimtypes.ManagedObjectReference - for i := range resourcePolicy.Status.ClusterModules { - m, err := cluster.NewManager(ctx.RestClient).ListModuleMembers(ctx, resourcePolicy.Status.ClusterModules[i].ModuleUuid) - Expect(err).ToNot(HaveOccurred()) - members = append(m, members...) - } - - Expect(members).To(ContainElements(vcVM.Reference())) - }) - - It("Returns error with non-existence cluster module", func() { - clusterModName := "bogusClusterMod" - vm.Annotations["vsphere-cluster-module-group"] = clusterModName - err := createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).To(MatchError("VirtualMachineSetResourcePolicy cluster module is not ready")) - }) - }) - - Context("Delete VM", func() { - const zoneName = "az-1" - - BeforeEach(func() { - // Explicitly place the VM into one of the zones that the test context will create. - vm.Labels[corev1.LabelTopologyZone] = zoneName - }) - - JustBeforeEach(func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - }) - - Context("when the VM is off", func() { - BeforeEach(func() { - vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOff - }) - - It("deletes the VM", func() { - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) - - uniqueID := vm.Status.UniqueID - Expect(ctx.GetVMFromMoID(uniqueID)).ToNot(BeNil()) - - Expect(vmProvider.DeleteVirtualMachine(ctx, vm)).To(Succeed()) - Expect(ctx.GetVMFromMoID(uniqueID)).To(BeNil()) - }) - }) - - It("when the VM is on", func() { - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOn)) - - uniqueID := vm.Status.UniqueID - Expect(ctx.GetVMFromMoID(uniqueID)).ToNot(BeNil()) - - // This checks that we power off the VM prior to deletion. - Expect(vmProvider.DeleteVirtualMachine(ctx, vm)).To(Succeed()) - Expect(ctx.GetVMFromMoID(uniqueID)).To(BeNil()) - }) - - It("returns success when VM does not exist", func() { - Expect(vmProvider.DeleteVirtualMachine(ctx, vm)).To(Succeed()) - Expect(vmProvider.DeleteVirtualMachine(ctx, vm)).To(Succeed()) - }) - - It("returns NotFound when VM does not exist", func() { - _, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - Expect(vmProvider.DeleteVirtualMachine(ctx, vm)).To(Succeed()) - delete(vm.Labels, corev1.LabelTopologyZone) - Expect(vmProvider.DeleteVirtualMachine(ctx, vm)).To(Succeed()) - }) - - It("Deletes existing VM when zone info is missing", func() { - _, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - uniqueID := vm.Status.UniqueID - Expect(ctx.GetVMFromMoID(uniqueID)).ToNot(BeNil()) - - Expect(vm.Labels).To(HaveKeyWithValue(corev1.LabelTopologyZone, zoneName)) - delete(vm.Labels, corev1.LabelTopologyZone) - - Expect(vmProvider.DeleteVirtualMachine(ctx, vm)).To(Succeed()) - Expect(ctx.GetVMFromMoID(uniqueID)).To(BeNil()) - }) - - It("Does not delete paused VM", func() { - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - uniqueID := vm.Status.UniqueID - Expect(ctx.GetVMFromMoID(uniqueID)).ToNot(BeNil()) - - sctx := ctx.SimulatorContext() - sctx.WithLock( - vcVM.Reference(), - func() { - vm := sctx.Map.Get(vcVM.Reference()).(*simulator.VirtualMachine) - vm.Config.ExtraConfig = append(vm.Config.ExtraConfig, - &vimtypes.OptionValue{ - Key: vmopv1.PauseVMExtraConfigKey, - Value: "True", - }) - }, - ) - - err = vmProvider.DeleteVirtualMachine(ctx, vm) - Expect(err).To(HaveOccurred()) - var noRequeueErr pkgerr.NoRequeueError - Expect(errors.As(err, &noRequeueErr)).To(BeTrue()) - Expect(noRequeueErr.Message).To(Equal(constants.VMPausedByAdminError)) - Expect(ctx.GetVMFromMoID(uniqueID)).ToNot(BeNil()) - }) - - Context("Fast Deploy is enabled", func() { - JustBeforeEach(func() { - pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { - config.Features.FastDeploy = true - }) - }) - - It("return success", func() { - // TODO: We don't have explicit promote tests in here so - // punt on that. But with the feature enable, we'll get - // all the VM's tasks. - _, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - uniqueID := vm.Status.UniqueID - Expect(ctx.GetVMFromMoID(uniqueID)).ToNot(BeNil()) - - err = vmProvider.DeleteVirtualMachine(ctx, vm) - Expect(err).ToNot(HaveOccurred()) - - Expect(ctx.GetVMFromMoID(uniqueID)).To(BeNil()) - }) - }) - - DescribeTable("VM is not connected", - func(state vimtypes.VirtualMachineConnectionState) { - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - sctx := ctx.SimulatorContext() - sctx.WithLock( - vcVM.Reference(), - func() { - vm := sctx.Map.Get(vcVM.Reference()).(*simulator.VirtualMachine) - vm.Summary.Runtime.ConnectionState = state - }) - - err = vmProvider.DeleteVirtualMachine(ctx, vm) - - if state == "" { - Expect(err).ToNot(HaveOccurred()) - Expect(ctx.GetVMFromMoID(vm.Status.UniqueID)).To(BeNil()) - } else { - Expect(err).To(HaveOccurred()) - var noRequeueErr pkgerr.NoRequeueError - Expect(errors.As(err, &noRequeueErr)).To(BeTrue()) - Expect(noRequeueErr.Message).To(Equal( - fmt.Sprintf("unsupported connection state: %s", state))) - Expect(ctx.GetVMFromMoID(vm.Status.UniqueID)).ToNot(BeNil()) - } - }, - Entry("empty", vimtypes.VirtualMachineConnectionState("")), - Entry("disconnected", vimtypes.VirtualMachineConnectionStateDisconnected), - Entry("inaccessible", vimtypes.VirtualMachineConnectionStateInaccessible), - Entry("invalid", vimtypes.VirtualMachineConnectionStateInvalid), - Entry("orphaned", vimtypes.VirtualMachineConnectionStateOrphaned), - ) - }) - - Context("Cleanup VM", func() { - const zoneName = "az-1" - - BeforeEach(func() { - // Explicitly place the VM into one of the zones that the test context will create. - vm.Labels[corev1.LabelTopologyZone] = zoneName - }) - - JustBeforeEach(func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - }) - - It("successfully cleans up VM service state", func() { - vcVM, err := ctx.Finder.VirtualMachine(ctx, vm.Name) - Expect(err).NotTo(HaveOccurred()) - - // Set up some VM Operator managed fields - configSpec := vimtypes.VirtualMachineConfigSpec{ - ExtraConfig: []vimtypes.BaseOptionValue{ - &vimtypes.OptionValue{ - Key: constants.ExtraConfigVMServiceNamespacedName, - Value: vm.Namespace + "/" + vm.Name, - }, - &vimtypes.OptionValue{ - Key: "guestinfo.userdata", - Value: "test-data", - }, - }, - ManagedBy: &vimtypes.ManagedByInfo{ - ExtensionKey: vmopv1.ManagedByExtensionKey, - Type: vmopv1.ManagedByExtensionType, - }, - } - - task, err := vcVM.Reconfigure(ctx, configSpec) - Expect(err).NotTo(HaveOccurred()) - Expect(task.Wait(ctx)).To(Succeed()) - - // Verify fields are set before cleanup - var moVMBefore mo.VirtualMachine - err = vcVM.Properties(ctx, vcVM.Reference(), []string{"config"}, &moVMBefore) - Expect(err).NotTo(HaveOccurred()) - Expect(moVMBefore.Config).ToNot(BeNil()) - Expect(moVMBefore.Config.ManagedBy).ToNot(BeNil()) - Expect(moVMBefore.Config.ManagedBy.ExtensionKey).To(Equal(vmopv1.ManagedByExtensionKey)) - - // Run cleanup - Expect(vmProvider.CleanupVirtualMachine(ctx, vm)).To(Succeed()) - - // Verify VM Operator managed fields were removed - var moVMAfter mo.VirtualMachine - err = vcVM.Properties(ctx, vcVM.Reference(), []string{"config"}, &moVMAfter) - Expect(err).NotTo(HaveOccurred()) - Expect(moVMAfter.Config).ToNot(BeNil()) - - // Check ExtraConfig - ecList := object.OptionValueList(moVMAfter.Config.ExtraConfig) - val, ok := ecList.Get(constants.ExtraConfigVMServiceNamespacedName) - Expect(!ok || val == "").To(BeTrue(), "Expected vmservice.namespacedName to be removed") - - val2, ok2 := ecList.Get("guestinfo.userdata") - Expect(!ok2 || val2 == "").To(BeTrue(), "Expected guestinfo.userdata to be removed") - - // Check ManagedBy - Expect(moVMAfter.Config.ManagedBy).To(BeNil()) - - // Verify VM still exists in vCenter - Expect(ctx.GetVMFromMoID(vm.Status.UniqueID)).ToNot(BeNil()) - }) - - It("returns success when VM does not exist in vCenter", func() { - // Delete the VM from vCenter first - Expect(vmProvider.DeleteVirtualMachine(ctx, vm)).To(Succeed()) - - // Cleanup should succeed even though VM doesn't exist - Expect(vmProvider.CleanupVirtualMachine(ctx, vm)).To(Succeed()) - }) - }) - - Context("Guest Heartbeat", func() { - JustBeforeEach(func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - }) - - It("return guest heartbeat", func() { - heartbeat, err := vmProvider.GetVirtualMachineGuestHeartbeat(ctx, vm) - Expect(err).ToNot(HaveOccurred()) - // Just testing for property query: field not set in vcsim. - Expect(heartbeat).To(BeEmpty()) - }) - }) - - Context("Web console ticket", func() { - JustBeforeEach(func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - }) - - It("return ticket", func() { - // vcsim doesn't implement this yet so expect an error. - _, err := vmProvider.GetVirtualMachineWebMKSTicket(ctx, vm, "foo") - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("does not implement: AcquireTicket")) - }) - }) - - Context("VM hardware version", func() { - JustBeforeEach(func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - }) - - It("return version", func() { - version, err := vmProvider.GetVirtualMachineHardwareVersion(ctx, vm) - Expect(err).NotTo(HaveOccurred()) - Expect(version).To(Equal(vimtypes.VMX9)) - }) - }) - - Context("Create/Update/Delete ISO backed VirtualMachine", func() { - var ( - vm *vmopv1.VirtualMachine - vmClass *vmopv1.VirtualMachineClass - ) - - BeforeEach(func() { - vmClass = builder.DummyVirtualMachineClassGenName() - vm = builder.DummyBasicVirtualMachine("test-vm-iso", "") - - // Reduce diff from old tests: by default don't create an NIC. - if vm.Spec.Network == nil { - vm.Spec.Network = &vmopv1.VirtualMachineNetworkSpec{} - } - vm.Spec.Network.Disabled = true - }) - - JustBeforeEach(func() { - vmClass.Namespace = nsInfo.Namespace - Expect(ctx.Client.Create(ctx, vmClass)).To(Succeed()) - - // Add required objects to get CD-ROM backing file name. - cvmiName := "vmi-iso" - objs := builder.DummyImageAndItemObjectsForCdromBacking(cvmiName, "", cvmiKind, "test-file.iso", ctx.ContentLibraryIsoItemID, true, true, resource.MustParse("100Mi"), true, true, "ISO") - for _, obj := range objs { - Expect(ctx.Client.Create(ctx, obj)).To(Succeed()) - } - - vm.Namespace = nsInfo.Namespace - vm.Spec.ClassName = vmClass.Name - vm.Spec.ImageName = cvmiName - vm.Spec.Image.Kind = cvmiKind - vm.Spec.Image.Name = cvmiName - vm.Spec.StorageClass = ctx.StorageClassName - vm.Spec.Hardware = &vmopv1.VirtualMachineHardwareSpec{ - Cdrom: []vmopv1.VirtualMachineCdromSpec{{ - Name: "cdrom0", - Image: vmopv1.VirtualMachineImageRef{ - Name: cvmiName, - Kind: cvmiKind, - }, - }}, - } - - Expect(ctx.Client.Create(ctx, vm)).To(Succeed()) - }) - - Context("return config", func() { - JustBeforeEach(func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - }) - - It("return config.files", func() { - vmPathName := "config.files.vmPathName" - props, err := vmProvider.GetVirtualMachineProperties(ctx, vm, []string{vmPathName}) - Expect(err).NotTo(HaveOccurred()) - var path object.DatastorePath - path.FromString(props[vmPathName].(string)) - Expect(path.Datastore).NotTo(BeEmpty()) - }) - }) - - When("Fast Deploy is enabled", func() { - - var ( - vmic vmopv1.VirtualMachineImageCache - ) - - BeforeEach(func() { - testConfig.WithContentLibrary = true - pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { - config.Features.FastDeploy = true - }) - // Ensure the VM has a UID to verify the VM directory path - // is different from the VM's Kubernetes UID on vSAN. - vm.UID = "test-vm-iso-uid" - }) - - JustBeforeEach(func() { - vmicName := pkgutil.VMIName(ctx.ContentLibraryIsoItemID) - vmic = vmopv1.VirtualMachineImageCache{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: pkgcfg.FromContext(ctx).PodNamespace, - Name: vmicName, - }, - } - Expect(ctx.Client.Create(ctx, &vmic)).To(Succeed()) - }) - - assertVMICNotReady := func(err error, msg, name, dcID, dsID string) { - var e pkgerr.VMICacheNotReadyError - ExpectWithOffset(1, errors.As(err, &e)).To(BeTrue()) - ExpectWithOffset(1, e.Message).To(Equal(msg)) - ExpectWithOffset(1, e.Name).To(Equal(name)) - ExpectWithOffset(1, e.DatacenterID).To(Equal(dcID)) - ExpectWithOffset(1, e.DatastoreID).To(Equal(dsID)) - } - - When("cache files are not ready", func() { - It("should fail", func() { - err := createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).To(HaveOccurred()) - assertVMICNotReady( - err, - "cached files not ready", - vmic.Name, - ctx.Datacenter.Reference().Value, - ctx.Datastore.Reference().Value) - }) - }) - - When("cache files are ready", func() { - JustBeforeEach(func() { - // Simulate vSAN datastore with TopLevelDirectoryCreateSupported disabled. - sctx := ctx.SimulatorContext() - for _, dsEnt := range sctx.Map.All("Datastore") { - sctx.WithLock( - dsEnt.Reference(), - func() { - ds := sctx.Map.Get(dsEnt.Reference()).(*simulator.Datastore) - ds.Capability.TopLevelDirectoryCreateSupported = ptr.To(false) - ds.Summary.Type = string(vimtypes.HostFileSystemVolumeFileSystemTypeVsan) - }) - } - - // Set required fields for ISO VM creation in the VMIC. - conditions.MarkTrue( - &vmic, - vmopv1.VirtualMachineImageCacheConditionFilesReady) - vmic.Status.Locations = []vmopv1.VirtualMachineImageCacheLocationStatus{ - { - DatacenterID: ctx.Datacenter.Reference().Value, - DatastoreID: ctx.Datastore.Reference().Value, - ProfileID: ctx.StorageProfileID, - Files: []vmopv1.VirtualMachineImageCacheFileStatus{}, - Conditions: []metav1.Condition{ - { - Type: vmopv1.ReadyConditionType, - Status: metav1.ConditionTrue, - }, - }, - }, - } - Expect(ctx.Client.Status().Update(ctx, &vmic)).To(Succeed()) - - libMgr := library.NewManager(ctx.RestClient) - Expect(libMgr.SyncLibraryItem(ctx, &library.Item{ID: ctx.ContentLibraryIsoItemID}, true)).To(Succeed()) - }) - - It("should successfully create the ISO VM in a different UUID-based directory", func() { - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).NotTo(HaveOccurred()) - - var moVM mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &moVM)).To(Succeed()) - - var p object.DatastorePath - p.FromString(moVM.Config.Files.VmPathName) - Expect(p.Datastore).NotTo(BeEmpty()) - - // The VM path should be something like: /test-vm-iso.vmx - // When TopLevelDirectoryCreateSupported is false, - // DatastoreNamespaceManager.CreateDirectory creates a - // new UUID-based directory that is different from the - // VM's Kubernetes UID. - pathParts := strings.Split(p.Path, "/") - Expect(pathParts).To(HaveLen(2)) - _, err = uuid.Parse(pathParts[0]) - Expect(err).NotTo(HaveOccurred(), "expected directory to be a UUID, got: %s", pathParts[0]) - Expect(pathParts[0]).NotTo(Equal(string(vm.UID)), "expected directory to be different from VM's K8s UID") - }) - }) - }) - }) - - Context("Power states", func() { - - getLastRestartTime := func(moVM mo.VirtualMachine) string { - for i := range moVM.Config.ExtraConfig { - ov := moVM.Config.ExtraConfig[i].GetOptionValue() - if ov.Key == "vmservice.lastRestartTime" { - return ov.Value.(string) - } - } - return "" - } - - var ( - vcVM *object.VirtualMachine - moVM mo.VirtualMachine - ) - - JustBeforeEach(func() { - var err error - moVM = mo.VirtualMachine{} - vcVM, err = createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &moVM)).To(Succeed()) - }) - - When("vcVM is powered on", func() { - JustBeforeEach(func() { - vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOn - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOn)) - }) - - When("power state is not changed", func() { - BeforeEach(func() { - vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOn - }) - It("should not return an error", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - }) - }) - - When("powering off the VM", func() { - JustBeforeEach(func() { - vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOff - }) - - When("power state should not be updated", func() { - const expectedPowerState = vmopv1.VirtualMachinePowerStateOn - When("vm is paused by devops", func() { - JustBeforeEach(func() { - vm.Annotations = map[string]string{ - vmopv1.PauseAnnotation: "true", - } - }) - It("should not change the power state", func() { - Expect(errors.Is(createOrUpdateVM(ctx, vmProvider, vm), vsphere.ErrIsPaused)).To(BeTrue()) - Expect(vm.Status.PowerState).To(Equal(expectedPowerState)) - }) - }) - When("vm is paused by admin", func() { - JustBeforeEach(func() { - vm.Annotations = map[string]string{ - vmopv1.PauseAnnotation: "true", - } - t, err := vcVM.Reconfigure(ctx, vimtypes.VirtualMachineConfigSpec{ - ExtraConfig: []vimtypes.BaseOptionValue{ - &vimtypes.OptionValue{ - Key: vmopv1.PauseVMExtraConfigKey, - Value: "true", - }, - }, - }) - Expect(err).ToNot(HaveOccurred()) - Expect(t.Wait(ctx)).To(Succeed()) - }) - It("should not change the power state", func() { - Expect(errors.Is(createOrUpdateVM(ctx, vmProvider, vm), vsphere.ErrIsPaused)).To(BeTrue()) - Expect(vm.Status.PowerState).To(Equal(expectedPowerState)) - }) - }) - - When("vm has running task", func() { - var ( - reg *simulator.Registry - simCtx *simulator.Context - taskRef vimtypes.ManagedObjectReference - ) - - JustBeforeEach(func() { - simCtx = ctx.SimulatorContext() - reg = simCtx.Map - taskRef = reg.Put(&mo.Task{ - Info: vimtypes.TaskInfo{ - State: vimtypes.TaskInfoStateRunning, - DescriptionId: "fake.task.1", - }, - }).Reference() - - vmRef := vimtypes.ManagedObjectReference{ - Type: string(vimtypes.ManagedObjectTypeVirtualMachine), - Value: vm.Status.UniqueID, - } - - reg.WithLock( - simCtx, - vmRef, - func() { - vm := reg.Get(vmRef).(*simulator.VirtualMachine) - vm.RecentTask = append(vm.RecentTask, taskRef) - }) - - }) - - AfterEach(func() { - reg.Remove(simCtx, taskRef) - }) - - It("should not change the power state", func() { - Expect(errors.Is(createOrUpdateVM(ctx, vmProvider, vm), vsphere.ErrHasTask)).To(BeTrue()) - Expect(vm.Status.PowerState).To(Equal(expectedPowerState)) - }) - }) - - }) - - DescribeTable("powerOffModes", - func(mode vmopv1.VirtualMachinePowerOpMode) { - vm.Spec.PowerOffMode = mode - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) - }, - Entry("hard", vmopv1.VirtualMachinePowerOpModeHard), - Entry("soft", vmopv1.VirtualMachinePowerOpModeSoft), - Entry("trySoft", vmopv1.VirtualMachinePowerOpModeTrySoft), - ) - - When("there is a config error", func() { - JustBeforeEach(func() { - vm.Spec.Bootstrap = &vmopv1.VirtualMachineBootstrapSpec{ - CloudInit: &vmopv1.VirtualMachineBootstrapCloudInitSpec{ - CloudConfig: &cloudinit.CloudConfig{ - RunCmd: json.RawMessage([]byte("invalid")), - }, - }, - } - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOn)) - }) - It("should still power off the VM", func() { - err := createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to reconcile config: updating state failed with failed to create bootstrap data")) - - // Do it again to update status. - Expect(createOrUpdateVM(ctx, vmProvider, vm)).ToNot(Succeed()) - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) - }) - }) - }) - - When("restarting the VM", func() { - var ( - oldLastRestartTime string - ) - - JustBeforeEach(func() { - oldLastRestartTime = getLastRestartTime(moVM) - vm.Spec.NextRestartTime = time.Now().UTC().Format(time.RFC3339Nano) - }) - - When("restartMode is hard", func() { - JustBeforeEach(func() { - vm.Spec.RestartMode = vmopv1.VirtualMachinePowerOpModeHard - }) - It("should restart the VM", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &moVM)).To(Succeed()) - newLastRestartTime := getLastRestartTime(moVM) - Expect(newLastRestartTime).ToNot(BeEmpty()) - Expect(newLastRestartTime).ToNot(Equal(oldLastRestartTime)) - }) - }) - When("restartMode is soft", func() { - JustBeforeEach(func() { - vm.Spec.RestartMode = vmopv1.VirtualMachinePowerOpModeSoft - }) - It("should return an error about lacking tools", func() { - Expect(testutil.ContainsError(createOrUpdateVM(ctx, vmProvider, vm), "failed to soft restart vm ServerFaultCode: ToolsUnavailable")).To(BeTrue()) - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &moVM)).To(Succeed()) - newLastRestartTime := getLastRestartTime(moVM) - Expect(newLastRestartTime).To(Equal(oldLastRestartTime)) - }) - }) - When("restartMode is trySoft", func() { - JustBeforeEach(func() { - vm.Spec.RestartMode = vmopv1.VirtualMachinePowerOpModeTrySoft - }) - It("should restart the VM", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &moVM)).To(Succeed()) - newLastRestartTime := getLastRestartTime(moVM) - Expect(newLastRestartTime).ToNot(BeEmpty()) - Expect(newLastRestartTime).ToNot(Equal(oldLastRestartTime)) - }) - }) - }) - - When("suspending the VM", func() { - JustBeforeEach(func() { - vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateSuspended - }) - When("power state should not be updated", func() { - const expectedPowerState = vmopv1.VirtualMachinePowerStateOn - When("vm is paused by devops", func() { - JustBeforeEach(func() { - vm.Annotations = map[string]string{ - vmopv1.PauseAnnotation: "true", - } - }) - It("should not change the power state", func() { - Expect(errors.Is(createOrUpdateVM(ctx, vmProvider, vm), vsphere.ErrIsPaused)).To(BeTrue()) - Expect(vm.Status.PowerState).To(Equal(expectedPowerState)) - }) - }) - When("vm is paused by admin", func() { - JustBeforeEach(func() { - vm.Annotations = map[string]string{ - vmopv1.PauseAnnotation: "true", - } - t, err := vcVM.Reconfigure(ctx, vimtypes.VirtualMachineConfigSpec{ - ExtraConfig: []vimtypes.BaseOptionValue{ - &vimtypes.OptionValue{ - Key: vmopv1.PauseVMExtraConfigKey, - Value: "true", - }, - }, - }) - Expect(err).ToNot(HaveOccurred()) - Expect(t.Wait(ctx)).To(Succeed()) - }) - It("should not change the power state", func() { - Expect(errors.Is(createOrUpdateVM(ctx, vmProvider, vm), vsphere.ErrIsPaused)).To(BeTrue()) - Expect(vm.Status.PowerState).To(Equal(expectedPowerState)) - }) - }) - - When("vm has running task", func() { - var ( - reg *simulator.Registry - simCtx *simulator.Context - taskRef vimtypes.ManagedObjectReference - ) - - JustBeforeEach(func() { - simCtx = ctx.SimulatorContext() - reg = simCtx.Map - taskRef = reg.Put(&mo.Task{ - Info: vimtypes.TaskInfo{ - State: vimtypes.TaskInfoStateRunning, - DescriptionId: "fake.task.2", - }, - }).Reference() - - vmRef := vimtypes.ManagedObjectReference{ - Type: string(vimtypes.ManagedObjectTypeVirtualMachine), - Value: vm.Status.UniqueID, - } - - reg.WithLock( - simCtx, - vmRef, - func() { - vm := reg.Get(vmRef).(*simulator.VirtualMachine) - vm.RecentTask = append(vm.RecentTask, taskRef) - }) - - }) - - AfterEach(func() { - reg.Remove(simCtx, taskRef) - }) - - It("should not change the power state", func() { - Expect(errors.Is(createOrUpdateVM(ctx, vmProvider, vm), vsphere.ErrHasTask)).To(BeTrue()) - Expect(vm.Status.PowerState).To(Equal(expectedPowerState)) - }) - }) - }) - - When("suspendMode is hard", func() { - JustBeforeEach(func() { - vm.Spec.SuspendMode = vmopv1.VirtualMachinePowerOpModeHard - }) - It("should suspend the VM", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateSuspended)) - }) - }) - When("suspendMode is soft", func() { - JustBeforeEach(func() { - vm.Spec.SuspendMode = vmopv1.VirtualMachinePowerOpModeSoft - }) - It("should suspend the VM", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateSuspended)) - }) - }) - When("suspendMode is trySoft", func() { - JustBeforeEach(func() { - vm.Spec.SuspendMode = vmopv1.VirtualMachinePowerOpModeTrySoft - }) - It("should suspend the VM", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateSuspended)) - }) - }) - }) - }) - - When("vcVM is powered off", func() { - JustBeforeEach(func() { - vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOff - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) - }) - - When("power state is not changed", func() { - It("should not return an error", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) - }) - }) - - When("powering on the VM", func() { - - JustBeforeEach(func() { - vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOn - }) - - When("power state should not be updated", func() { - const expectedPowerState = vmopv1.VirtualMachinePowerStateOff - When("vm is paused by devops", func() { - JustBeforeEach(func() { - vm.Annotations = map[string]string{ - vmopv1.PauseAnnotation: "true", - } - }) - It("should not change the power state", func() { - Expect(errors.Is(createOrUpdateVM(ctx, vmProvider, vm), vsphere.ErrIsPaused)).To(BeTrue()) - Expect(vm.Status.PowerState).To(Equal(expectedPowerState)) - }) - }) - When("vm is paused by admin", func() { - JustBeforeEach(func() { - vm.Annotations = map[string]string{ - vmopv1.PauseAnnotation: "true", - } - t, err := vcVM.Reconfigure(ctx, vimtypes.VirtualMachineConfigSpec{ - ExtraConfig: []vimtypes.BaseOptionValue{ - &vimtypes.OptionValue{ - Key: vmopv1.PauseVMExtraConfigKey, - Value: "true", - }, - }, - }) - Expect(err).ToNot(HaveOccurred()) - Expect(t.Wait(ctx)).To(Succeed()) - }) - It("should not change the power state", func() { - Expect(errors.Is(createOrUpdateVM(ctx, vmProvider, vm), vsphere.ErrIsPaused)).To(BeTrue()) - Expect(vm.Status.PowerState).To(Equal(expectedPowerState)) - }) - }) - - When("vm has running task", func() { - var ( - reg *simulator.Registry - simCtx *simulator.Context - taskRef vimtypes.ManagedObjectReference - ) - - JustBeforeEach(func() { - simCtx = ctx.SimulatorContext() - reg = simCtx.Map - taskRef = reg.Put(&mo.Task{ - Info: vimtypes.TaskInfo{ - State: vimtypes.TaskInfoStateRunning, - DescriptionId: "fake.task.3", - }, - }).Reference() - - vmRef := vimtypes.ManagedObjectReference{ - Type: string(vimtypes.ManagedObjectTypeVirtualMachine), - Value: vm.Status.UniqueID, - } - - reg.WithLock( - simCtx, - vmRef, - func() { - vm := reg.Get(vmRef).(*simulator.VirtualMachine) - vm.RecentTask = append(vm.RecentTask, taskRef) - }) - - }) - - AfterEach(func() { - reg.Remove(simCtx, taskRef) - }) - - It("should not change the power state", func() { - Expect(errors.Is(createOrUpdateVM(ctx, vmProvider, vm), vsphere.ErrHasTask)).To(BeTrue()) - Expect(vm.Status.PowerState).To(Equal(expectedPowerState)) - }) - }) - - }) - - When("there is a power on check annotation", func() { - JustBeforeEach(func() { - vm.Annotations = map[string]string{ - vmopv1.CheckAnnotationPowerOn + "/app": "reason", - } - }) - It("should not power on the VM", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) - }) - }) - - When("there is a apply power state change time annotation", func() { - JustBeforeEach(func() { - pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { - config.Features.VMGroups = true - }) - }) - - When("the time is in the future", func() { - JustBeforeEach(func() { - vm.Annotations = map[string]string{ - pkgconst.ApplyPowerStateTimeAnnotation: time.Now().UTC().Add(time.Minute).Format(time.RFC3339Nano), - } - }) - - It("should not power on the VM and requeue after remaining time", func() { - err := createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).To(HaveOccurred()) - var requeueErr pkgerr.RequeueError - Expect(errors.As(err, &requeueErr)).To(BeTrue()) - Expect(requeueErr.After).To(BeNumerically("~", time.Minute, time.Second)) - }) - }) - - When("the time is in the past", func() { - JustBeforeEach(func() { - vm.Annotations = map[string]string{ - pkgconst.ApplyPowerStateTimeAnnotation: time.Now().UTC().Add(-time.Minute).Format(time.RFC3339Nano), - } - }) - - It("should power on the VM and remove the annotation", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOn)) - Expect(vm.Annotations).ToNot(HaveKey(pkgconst.ApplyPowerStateTimeAnnotation)) - }) - }) - }) - - const ( - oldDiskSizeBytes = int64(31457280) - newDiskSizeGi = 20 - newDiskSizeBytes = int64(newDiskSizeGi * 1024 * 1024 * 1024) - ) - - When("the boot disk size is changed for non-ISO VMs", func() { - JustBeforeEach(func() { - vmDevs := object.VirtualDeviceList(moVM.Config.Hardware.Device) - disks := vmDevs.SelectByType(&vimtypes.VirtualDisk{}) - Expect(disks).To(HaveLen(1)) - Expect(disks[0]).To(BeAssignableToTypeOf(&vimtypes.VirtualDisk{})) - diskCapacityBytes := disks[0].(*vimtypes.VirtualDisk).CapacityInBytes - Expect(diskCapacityBytes).To(Equal(oldDiskSizeBytes)) - - q := resource.MustParse(fmt.Sprintf("%dGi", newDiskSizeGi)) - vm.Spec.Advanced = &vmopv1.VirtualMachineAdvancedSpec{ - BootDiskCapacity: &q, - } - if vm.Spec.Hardware == nil { - vm.Spec.Hardware = &vmopv1.VirtualMachineHardwareSpec{} - } - vm.Spec.Hardware.Cdrom = nil - }) - It("should power on the VM with the boot disk resized", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOn)) - - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &moVM)).To(Succeed()) - vmDevs := object.VirtualDeviceList(moVM.Config.Hardware.Device) - disks := vmDevs.SelectByType(&vimtypes.VirtualDisk{}) - Expect(disks).To(HaveLen(1)) - Expect(disks[0]).To(BeAssignableToTypeOf(&vimtypes.VirtualDisk{})) - diskCapacityBytes := disks[0].(*vimtypes.VirtualDisk).CapacityInBytes - Expect(diskCapacityBytes).To(Equal(newDiskSizeBytes)) - }) - }) - - When("there are no NICs", func() { - JustBeforeEach(func() { - vm.Spec.Network.Interfaces = nil - }) - It("should power on the VM", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOn)) - }) - }) - - When("there is a single NIC", func() { - JustBeforeEach(func() { - vm.Spec.Network.Interfaces = []vmopv1.VirtualMachineNetworkInterfaceSpec{ - { - Name: "eth0", - Network: &vmopv1common.PartialObjectRef{ - Name: "VM Network", - }, - }, - } - }) - When("with networking disabled", func() { - JustBeforeEach(func() { - vm.Spec.Network.Disabled = true - }) - It("should power on the VM", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOn)) - }) - }) - }) - - When("VM.Spec.GuestID is changed", func() { - - When("the guest ID value is invalid", func() { - - JustBeforeEach(func() { - vm.Spec.GuestID = "invalid-guest-id" - }) - - It("should return an error and set the VM's Guest ID condition false", func() { - err := createOrUpdateVM(ctx, vmProvider, vm) - Expect(err.Error()).To(ContainSubstring("reconfigure VM task failed")) - - c := conditions.Get(vm, vmopv1.GuestIDReconfiguredCondition) - Expect(c).ToNot(BeNil()) - expectedCondition := conditions.FalseCondition( - vmopv1.GuestIDReconfiguredCondition, - "Invalid", - "The specified guest ID value is not supported: invalid-guest-id", - ) - Expect(*c).To(conditions.MatchCondition(*expectedCondition)) - }) - }) - - When("the guest ID value is valid", func() { - - JustBeforeEach(func() { - vm.Spec.GuestID = "vmwarePhoton64Guest" - }) - - It("should power on the VM with the specified guest ID", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOn)) - - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &moVM)).To(Succeed()) - Expect(moVM.Config.GuestId).To(Equal("vmwarePhoton64Guest")) - }) - }) - - When("the guest ID spec is removed", func() { - - JustBeforeEach(func() { - vm.Spec.GuestID = "" - }) - - It("should clear the VM guest ID condition if previously set", func() { - vm.Status.Conditions = []metav1.Condition{ - { - Type: vmopv1.GuestIDReconfiguredCondition, - Status: metav1.ConditionFalse, - }, - } - - // Customize - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOn)) - - Expect(conditions.Get(vm, vmopv1.GuestIDReconfiguredCondition)).To(BeNil()) - }) - }) - }) - - When("VM has CD-ROM", func() { - - const ( - vmiName = "vmi-iso" - vmiKind = "VirtualMachineImage" - vmiFileName = "dummy.iso" - ) - - JustBeforeEach(func() { - vm.Spec.Hardware = &vmopv1.VirtualMachineHardwareSpec{ - Cdrom: []vmopv1.VirtualMachineCdromSpec{ - { - Name: "cdrom1", - Image: vmopv1.VirtualMachineImageRef{ - Name: vmiName, - Kind: vmiKind, - }, - AllowGuestControl: ptr.To(true), - Connected: ptr.To(true), - }, - }, - } - testConfig.WithContentLibrary = true - }) - - JustBeforeEach(func() { - // Add required objects to get CD-ROM backing file name. - objs := builder.DummyImageAndItemObjectsForCdromBacking( - vmiName, - vm.Namespace, - vmiKind, - vmiFileName, - ctx.ContentLibraryIsoItemID, - true, - true, - resource.MustParse("100Mi"), - true, - true, - "ISO") - for _, obj := range objs { - Expect(ctx.Client.Create(ctx, obj)).To(Succeed()) - } - }) - - assertPowerOnVMWithCDROM := func() { - ExpectWithOffset(1, createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - ExpectWithOffset(1, vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOn)) - - ExpectWithOffset(1, vcVM.Properties(ctx, vcVM.Reference(), nil, &moVM)).To(Succeed()) - - cdromDeviceList := object.VirtualDeviceList(moVM.Config.Hardware.Device).SelectByType(&vimtypes.VirtualCdrom{}) - ExpectWithOffset(1, cdromDeviceList).To(HaveLen(1)) - cdrom := cdromDeviceList[0].(*vimtypes.VirtualCdrom) - ExpectWithOffset(1, cdrom.Connectable.StartConnected).To(BeTrue()) - ExpectWithOffset(1, cdrom.Connectable.Connected).To(BeTrue()) - ExpectWithOffset(1, cdrom.Connectable.AllowGuestControl).To(BeTrue()) - ExpectWithOffset(1, cdrom.ControllerKey).ToNot(BeZero()) - ExpectWithOffset(1, cdrom.UnitNumber).ToNot(BeNil()) - ExpectWithOffset(1, cdrom.Backing).To(BeAssignableToTypeOf(&vimtypes.VirtualCdromIsoBackingInfo{})) - backing := cdrom.Backing.(*vimtypes.VirtualCdromIsoBackingInfo) - ExpectWithOffset(1, backing.FileName).To(Equal(vmiFileName)) - } - - assertNotPowerOnVMWithCDROM := func() { - err := createOrUpdateVM(ctx, vmProvider, vm) - ExpectWithOffset(1, err).To(HaveOccurred()) - ExpectWithOffset(1, err.Error()).To(ContainSubstring("no CD-ROM is found for image ref")) - } - - It("should power on the VM with expected CD-ROM device", assertPowerOnVMWithCDROM) - - When("FSS Resize is enabled", func() { - JustBeforeEach(func() { - pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { - config.Features.VMResize = true - }) - }) - It("should not power on the VM with expected CD-ROM device", assertNotPowerOnVMWithCDROM) - }) - - When("FSS Resize CPU & Memory is enabled", func() { - JustBeforeEach(func() { - pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { - config.Features.VMResizeCPUMemory = true - }) - }) - It("should power on the VM with expected CD-ROM device", assertPowerOnVMWithCDROM) - }) - - When("the boot disk size is changed for VM with CD-ROM", func() { - - JustBeforeEach(func() { - vmDevs := object.VirtualDeviceList(moVM.Config.Hardware.Device) - disks := vmDevs.SelectByType(&vimtypes.VirtualDisk{}) - Expect(disks).To(HaveLen(1)) - Expect(disks[0]).To(BeAssignableToTypeOf(&vimtypes.VirtualDisk{})) - diskCapacityBytes := disks[0].(*vimtypes.VirtualDisk).CapacityInBytes - Expect(diskCapacityBytes).To(Equal(oldDiskSizeBytes)) - - q := resource.MustParse(fmt.Sprintf("%dGi", newDiskSizeGi)) - vm.Spec.Advanced = &vmopv1.VirtualMachineAdvancedSpec{ - BootDiskCapacity: &q, - } - }) - - It("should power on the VM without the boot disk resized", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOn)) - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &moVM)).To(Succeed()) - - vmDevs := object.VirtualDeviceList(moVM.Config.Hardware.Device) - disks := vmDevs.SelectByType(&vimtypes.VirtualDisk{}) - Expect(disks).To(HaveLen(1)) - Expect(disks[0]).To(BeAssignableToTypeOf(&vimtypes.VirtualDisk{})) - diskCapacityBytes := disks[0].(*vimtypes.VirtualDisk).CapacityInBytes - Expect(diskCapacityBytes).To(Equal(oldDiskSizeBytes)) - }) - }) - }) - }) - - When("suspending the VM", func() { - JustBeforeEach(func() { - vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateSuspended - }) - When("suspendMode is hard", func() { - JustBeforeEach(func() { - vm.Spec.SuspendMode = vmopv1.VirtualMachinePowerOpModeHard - }) - It("should not suspend the VM", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) - }) - }) - When("suspendMode is soft", func() { - JustBeforeEach(func() { - vm.Spec.SuspendMode = vmopv1.VirtualMachinePowerOpModeSoft - }) - It("should not suspend the VM", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) - }) - }) - When("suspendMode is trySoft", func() { - JustBeforeEach(func() { - vm.Spec.SuspendMode = vmopv1.VirtualMachinePowerOpModeTrySoft - }) - It("should not suspend the VM", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) - }) - }) - }) - - When("there is a config error", func() { - JustBeforeEach(func() { - vm.Spec.Bootstrap = &vmopv1.VirtualMachineBootstrapSpec{ - CloudInit: &vmopv1.VirtualMachineBootstrapCloudInitSpec{ - CloudConfig: &cloudinit.CloudConfig{ - RunCmd: json.RawMessage([]byte("invalid")), - }, - }, - } - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) - }) - It("should not power on the VM", func() { - err := createOrUpdateVM(ctx, vmProvider, vm) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to reconcile config: updating state failed with failed to create bootstrap data")) - - // Do it again to update status. - Expect(createOrUpdateVM(ctx, vmProvider, vm)).ToNot(Succeed()) - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) - }) - }) - }) - - When("vcVM is suspended", func() { - JustBeforeEach(func() { - vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateSuspended - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateSuspended)) - }) - - When("power state is not changed", func() { - It("should not return an error", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - }) - }) - - When("powering on the VM", func() { - - JustBeforeEach(func() { - vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOn - }) - - It("should power on the VM", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOn)) - }) - - When("there is a power on check annotation", func() { - JustBeforeEach(func() { - vm.Annotations = map[string]string{ - vmopv1.CheckAnnotationPowerOn + "/app": "reason", - } - }) - It("should not power on the VM", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateSuspended)) - }) - }) - }) - - When("powering off the VM", func() { - JustBeforeEach(func() { - vm.Spec.PowerState = vmopv1.VirtualMachinePowerStateOff - }) - When("powerOffMode is hard", func() { - JustBeforeEach(func() { - vm.Spec.PowerOffMode = vmopv1.VirtualMachinePowerOpModeHard - }) - It("should power off the VM", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) - }) - }) - When("powerOffMode is soft", func() { - JustBeforeEach(func() { - vm.Spec.PowerOffMode = vmopv1.VirtualMachinePowerOpModeSoft - }) - It("should not power off the VM", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateSuspended)) - }) - }) - When("powerOffMode is trySoft", func() { - JustBeforeEach(func() { - vm.Spec.PowerOffMode = vmopv1.VirtualMachinePowerOpModeTrySoft - }) - It("should power off the VM", func() { - Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) - Expect(vm.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) - }) - }) - }) - }) - }) - - Context("vSphere Policies", func() { - - var ( - policyTag1ID string - policyTag2ID string - policyTag3ID string - - tagMgr *tags.Manager - ) - - JustBeforeEach(func() { - var err error - - tagMgr = tags.NewManager(ctx.RestClient) - - // Create a category for the policy tags - categoryID, err := tagMgr.CreateCategory(ctx, &tags.Category{ - Name: "my-policy-category", - Description: "Category for policy tags", - AssociableTypes: []string{"VirtualMachine"}, - }) - Expect(err).ToNot(HaveOccurred()) - - policyTag1ID, err = tagMgr.CreateTag(ctx, &tags.Tag{ - Name: "my-policy-tag-1", - CategoryID: categoryID, - }) - Expect(err).ToNot(HaveOccurred()) - - policyTag2ID, err = tagMgr.CreateTag(ctx, &tags.Tag{ - Name: "my-policy-tag-2", - CategoryID: categoryID, - }) - Expect(err).ToNot(HaveOccurred()) - - policyTag3ID, err = tagMgr.CreateTag(ctx, &tags.Tag{ - Name: "my-policy-tag-3", - CategoryID: categoryID, - }) - Expect(err).ToNot(HaveOccurred()) - }) - - When("Capability is enabled", func() { - JustBeforeEach(func() { - pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { - config.Features.VSpherePolicies = true - }) - }) - - When("creating a VM", func() { - When("async create is enabled", func() { - JustBeforeEach(func() { - pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { - config.AsyncCreateEnabled = true - }) - }) - It("should successfully create VM", func() { - By("Setting up VM with policy evaluation objects", func() { - // Set VM UID for proper PolicyEvaluation naming - vm.UID = "test-vm-policy-uid" - - // Create a PolicyEvaluation object that will be found during policy reconciliation - policyEval := &vspherepolv1.PolicyEvaluation{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: vm.Namespace, - Name: "vm-" + vm.Name, - Generation: 1, - OwnerReferences: []metav1.OwnerReference{ - { - APIVersion: vmopv1.GroupVersion.String(), - Kind: "VirtualMachine", - Name: vm.Name, - UID: vm.UID, - Controller: ptr.To(true), - BlockOwnerDeletion: ptr.To(true), - }, - }, - }, - Spec: vspherepolv1.PolicyEvaluationSpec{ - Workload: &vspherepolv1.PolicyEvaluationWorkloadSpec{ - Guest: &vspherepolv1.PolicyEvaluationGuestSpec{ - GuestID: "ubuntu64Guest", - GuestFamily: vspherepolv1.GuestFamilyTypeLinux, - }, - }, - }, - Status: vspherepolv1.PolicyEvaluationStatus{ - ObservedGeneration: 1, - Policies: []vspherepolv1.PolicyEvaluationResult{ - { - Name: "test-active-policy", - Kind: "ComputePolicy", - Tags: []string{policyTag1ID, policyTag2ID}, - }, - }, - Conditions: []metav1.Condition{ - *conditions.TrueCondition(vspherepolv1.ReadyConditionType), - }, - }, - } - - // Create the PolicyEvaluation in the fake Kubernetes client - Expect(ctx.Client.Create(ctx, policyEval)).To(Succeed()) - }) - - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - // Verify VM was created successfully - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - - // Verify placement condition is ready (indicates vmconfpolicy.Reconcile was called) - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionPlacementReady)).To(BeTrue()) - - // Verify that policy tags were added to ExtraConfig - By("VM has policy tags in ExtraConfig", func() { - Expect(o.Config.ExtraConfig).ToNot(BeNil()) - - ecMap := pkgutil.OptionValues(o.Config.ExtraConfig).StringMap() - - // Verify tags are present - Expect(ecMap).To(HaveKey(vmconfpolicy.ExtraConfigPolicyTagsKey)) - activeTags := ecMap[vmconfpolicy.ExtraConfigPolicyTagsKey] - Expect(activeTags).To(ContainSubstring(policyTag1ID)) - Expect(activeTags).To(ContainSubstring(policyTag2ID)) - }) - }) - - }) - When("async create is disabled", func() { - JustBeforeEach(func() { - pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { - config.AsyncCreateEnabled = false - }) - }) - - It("should successfully create VM and call vmconfig policy.Reconcile during placement", func() { - By("Setting up VM with policy evaluation objects", func() { - // Set VM UID for proper PolicyEvaluation naming - vm.UID = "test-vm-sync-policy-uid" - - // Create a PolicyEvaluation object that will be found during policy reconciliation - policyEval := &vspherepolv1.PolicyEvaluation{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: vm.Namespace, - Name: "vm-" + vm.Name, - Generation: 1, - OwnerReferences: []metav1.OwnerReference{ - { - APIVersion: vmopv1.GroupVersion.String(), - Kind: "VirtualMachine", - Name: vm.Name, - UID: vm.UID, - Controller: ptr.To(true), - BlockOwnerDeletion: ptr.To(true), - }, - }, - }, - Spec: vspherepolv1.PolicyEvaluationSpec{ - Workload: &vspherepolv1.PolicyEvaluationWorkloadSpec{ - Guest: &vspherepolv1.PolicyEvaluationGuestSpec{ - GuestID: "ubuntu64Guest", - GuestFamily: vspherepolv1.GuestFamilyTypeLinux, - }, - }, - }, - Status: vspherepolv1.PolicyEvaluationStatus{ - ObservedGeneration: 1, - Policies: []vspherepolv1.PolicyEvaluationResult{ - { - Name: "test-sync-active-policy", - Kind: "ComputePolicy", - Tags: []string{policyTag1ID, policyTag2ID}, - }, - }, - Conditions: []metav1.Condition{ - *conditions.TrueCondition(vspherepolv1.ReadyConditionType), - }, - }, - } - - // Create the PolicyEvaluation in the fake Kubernetes client - Expect(ctx.Client.Create(ctx, policyEval)).To(Succeed()) - }) - - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - // Verify VM was created successfully - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - - // Verify placement condition is ready (indicates vmconfpolicy.Reconcile was called) - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionPlacementReady)).To(BeTrue()) - - // Verify that policy tags were added to ExtraConfig - By("VM has policy tags in ExtraConfig", func() { - Expect(o.Config.ExtraConfig).ToNot(BeNil()) - - ecMap := pkgutil.OptionValues(o.Config.ExtraConfig).StringMap() - - // Verify tags are present - Expect(ecMap).To(HaveKey(vmconfpolicy.ExtraConfigPolicyTagsKey)) - activeTags := ecMap[vmconfpolicy.ExtraConfigPolicyTagsKey] - Expect(activeTags).To(ContainSubstring(policyTag1ID)) - Expect(activeTags).To(ContainSubstring(policyTag2ID)) - }) - }) - }) - }) - - When("updating a VM", func() { - It("should update VM with policy tags during reconfiguration", func() { - By("Setting up VM with policy evaluation objects", func() { - // Set VM UID for proper PolicyEvaluation naming - vm.UID = "test-vm-policy-uid" - - // Create a PolicyEvaluation object that will be found during policy reconciliation - policyEval := &vspherepolv1.PolicyEvaluation{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: vm.Namespace, - Name: "vm-" + vm.Name, - Generation: 1, - OwnerReferences: []metav1.OwnerReference{ - { - APIVersion: vmopv1.GroupVersion.String(), - Kind: "VirtualMachine", - Name: vm.Name, - UID: vm.UID, - Controller: ptr.To(true), - BlockOwnerDeletion: ptr.To(true), - }, - }, - }, - Spec: vspherepolv1.PolicyEvaluationSpec{ - Workload: &vspherepolv1.PolicyEvaluationWorkloadSpec{ - Guest: &vspherepolv1.PolicyEvaluationGuestSpec{ - GuestID: "ubuntu64Guest", - GuestFamily: vspherepolv1.GuestFamilyTypeLinux, - }, - }, - }, - Status: vspherepolv1.PolicyEvaluationStatus{ - ObservedGeneration: 1, - Conditions: []metav1.Condition{ - *conditions.TrueCondition(vspherepolv1.ReadyConditionType), - }, - }, - } - - // Create the PolicyEvaluation in the fake Kubernetes client - Expect(ctx.Client.Create(ctx, policyEval)).To(Succeed()) - }) - - // First create the VM - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - By("Adding a non-policy tag to the VM", func() { - mgr := tags.NewManager(ctx.RestClient) - - Expect(mgr.AttachTag(ctx, ctx.TagID, vcVM.Reference())).To(Succeed()) - - list, err := mgr.GetAttachedTags(ctx, vcVM.Reference()) - Expect(err).ToNot(HaveOccurred()) - Expect(list).To(HaveLen(1)) - Expect(list[0].ID).To(Equal(ctx.TagID)) - }) - - By("Setting up PolicyEvaluation for update", func() { - // Create a PolicyEvaluation object with updated tags - policyEval := &vspherepolv1.PolicyEvaluation{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: vm.Namespace, - Name: "vm-" + vm.Name, - }, - } - - Expect(ctx.Client.Get( - ctx, - client.ObjectKeyFromObject(policyEval), - policyEval)).To(Succeed()) - - // Create a PolicyEvaluation object with updated tags - policyEval.Status = vspherepolv1.PolicyEvaluationStatus{ - ObservedGeneration: policyEval.Generation, - Policies: []vspherepolv1.PolicyEvaluationResult{ - { - Name: "test-updated-active-policy", - Kind: "ComputePolicy", - Tags: []string{policyTag1ID, policyTag2ID, policyTag3ID}, - }, - }, - Conditions: []metav1.Condition{ - *conditions.TrueCondition(vspherepolv1.ReadyConditionType), - }, - } - - // Update the PolicyEvaluation in the fake Kubernetes client - Expect(ctx.Client.Status().Update(ctx, policyEval)).To(Succeed()) - }) - - // Trigger VM update. - vcVM, err = createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - // Get VM properties. - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - - // Verify that updated policy tags were added to ExtraConfig - By("VM has updated policy tags in ExtraConfig", func() { - Expect(o.Config.ExtraConfig).ToNot(BeNil()) - - ecMap := pkgutil.OptionValues(o.Config.ExtraConfig).StringMap() - - // Verify updated tags are present - Expect(ecMap).To(HaveKey(vmconfpolicy.ExtraConfigPolicyTagsKey)) - activeTags := ecMap[vmconfpolicy.ExtraConfigPolicyTagsKey] - Expect(activeTags).To(ContainSubstring(policyTag1ID)) - Expect(activeTags).To(ContainSubstring(policyTag2ID)) - Expect(activeTags).To(ContainSubstring(policyTag3ID)) - }) - }) - }) - }) - - When("Capability is disabled", func() { - JustBeforeEach(func() { - pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { - config.Features.VSpherePolicies = false - }) - }) - - When("creating a VM", func() { - When("async create is enabled", func() { - JustBeforeEach(func() { - pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { - config.AsyncCreateEnabled = true - }) - }) - It("should successfully create VM without calling vmconfpolicy.Reconcile", func() { - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - // Verify VM was created successfully - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - - // Verify placement condition is ready even without policy reconciliation - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionPlacementReady)).To(BeTrue()) - }) - - }) - When("async create is disabled", func() { - JustBeforeEach(func() { - pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { - config.AsyncCreateEnabled = false - }) - }) - - It("should successfully create VM without calling vmconfpolicy.Reconcile", func() { - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - // Verify VM was created successfully - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - - // Verify placement condition is ready even without policy reconciliation - Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineConditionPlacementReady)).To(BeTrue()) - - // Verify that no policy tags were added to ExtraConfig (policy disabled) - By("VM should not have policy tags in ExtraConfig", func() { - if o.Config.ExtraConfig != nil { - ecMap := pkgutil.OptionValues(o.Config.ExtraConfig).StringMap() - - // Verify no tags are present - Expect(ecMap).ToNot(HaveKey(vmconfpolicy.ExtraConfigPolicyTagsKey)) - } - }) - }) - }) - }) - - When("updating a VM", func() { - It("should update VM without adding policy tags", func() { - // First create the VM - _, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - // Trigger VM update. - vcVM, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) - Expect(err).ToNot(HaveOccurred()) - - // Get VM properties. - var o mo.VirtualMachine - Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &o)).To(Succeed()) - - // Verify that no policy tags were added to ExtraConfig during update - By("VM should not have policy tags in ExtraConfig after update", func() { - if o.Config.ExtraConfig != nil { - ecMap := pkgutil.OptionValues(o.Config.ExtraConfig).StringMap() - - // Verify no tags are present - Expect(ecMap).ToNot(HaveKey(vmconfpolicy.ExtraConfigPolicyTagsKey)) - } - }) - }) - }) - }) - }) - }) -} +var _ = Describe("VirtualMachine", Label(testlabels.VCSim), func() { + Describe("CNS", vmCNSTests) + Describe("Cleanup", vmCleanupTests) + Describe("ConfigSpec", vmConfigSpecTests) + Describe("ConnectionState", vmConnectionStateTests) + Describe("Create", Label(testlabels.Create), vmCreateTests) + Describe("Crypto", Label(testlabels.Crypto), vmCryptoTests) + Describe("Delete", Label(testlabels.Delete), vmDeleteTests) + Describe("Disks", vmDisksTests) + Describe("Group", Label(testlabels.Group), vmGroupTests) + Describe("GuestHeartbeat", vmGuestHeartbeatTests) + Describe("GuestID", vmGuestIDTests) + Describe("HardwareVersion", vmHardwareVersionTests) + Describe("ISO", vmISOTests) + Describe("InstanceStorage", vmInstanceStorageTests) + Describe("Metadata", vmMetadataTests) + Describe("Misc", vmMiscTests) + Describe("Network", vmNetworkTests) + Describe("NPE", vmNPETests) + Describe("PCI", vmPCITests) + Describe("Policy", vmPolicyTests) + Describe("Power", vmPowerStateTests) + Describe("Resize", vmResizeTests) + Describe("SetResourcePolicy", vmSetResourcePolicyTests) + Describe("Snapshot", Label(testlabels.Snapshot), vmSnapshotTests) + Describe("Storage", vmStorageTests) + Describe("UnmanagedVolumes", vmUnmanagedVolumesTests) + Describe("Upgrade", vmUpgradeTests) + Describe("VKS", Label(testlabels.VKS), vmVKSTests) + Describe("WebConsole", vmWebConsoleTests) + Describe("Zone", vmZoneTests) +}) // getVMHomeDisk gets the VM's "home" disk. It makes some assumptions about the backing and disk name. func getVMHomeDisk( diff --git a/pkg/providers/vsphere/vmprovider_vm_unmanaged_volumes_test.go b/pkg/providers/vsphere/vmprovider_vm_unmanaged_volumes_test.go index 00babd4c9..b66f11660 100644 --- a/pkg/providers/vsphere/vmprovider_vm_unmanaged_volumes_test.go +++ b/pkg/providers/vsphere/vmprovider_vm_unmanaged_volumes_test.go @@ -43,9 +43,9 @@ import ( "github.com/vmware-tanzu/vm-operator/test/builder" ) -// unmanagedVolumesTests provides comprehensive test coverage for the +// vmUnmanagedVolumesTests provides comprehensive test coverage for the // AllDisksArePVCs feature in the vSphere provider. -func unmanagedVolumesTests() { +func vmUnmanagedVolumesTests() { var ( parentCtx context.Context diff --git a/pkg/providers/vsphere/vmprovider_vm_upgrade_test.go b/pkg/providers/vsphere/vmprovider_vm_upgrade_test.go new file mode 100644 index 000000000..4d4ff84b9 --- /dev/null +++ b/pkg/providers/vsphere/vmprovider_vm_upgrade_test.go @@ -0,0 +1,181 @@ +// © Broadcom. All Rights Reserved. +// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package vsphere_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha6" + "github.com/vmware-tanzu/vm-operator/pkg/conditions" + pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + pkgconst "github.com/vmware-tanzu/vm-operator/pkg/constants" + ctxop "github.com/vmware-tanzu/vm-operator/pkg/context/operation" + "github.com/vmware-tanzu/vm-operator/pkg/providers" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" + "github.com/vmware-tanzu/vm-operator/pkg/util/kube/cource" + "github.com/vmware-tanzu/vm-operator/pkg/util/ovfcache" + vmopv1util "github.com/vmware-tanzu/vm-operator/pkg/util/vmopv1" + "github.com/vmware-tanzu/vm-operator/test/builder" +) + +func vmUpgradeTests() { + var ( + parentCtx context.Context + initObjects []client.Object + testConfig builder.VCSimTestConfig + ctx *builder.TestContextForVCSim + vmProvider providers.VirtualMachineProviderInterface + nsInfo builder.WorkloadNamespaceInfo + + vm *vmopv1.VirtualMachine + vmClass *vmopv1.VirtualMachineClass + + zoneName string + ) + + BeforeEach(func() { + parentCtx = pkgcfg.NewContextWithDefaultConfig() + parentCtx = ctxop.WithContext(parentCtx) + parentCtx = ovfcache.WithContext(parentCtx) + parentCtx = cource.WithContext(parentCtx) + pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { + config.AsyncCreateEnabled = false + config.AsyncSignalEnabled = false + }) + testConfig = builder.VCSimTestConfig{ + WithContentLibrary: true, + } + + vmClass = builder.DummyVirtualMachineClassGenName() + vm = builder.DummyBasicVirtualMachine("test-vm", "") + + if vm.Spec.Network == nil { + vm.Spec.Network = &vmopv1.VirtualMachineNetworkSpec{} + } + vm.Spec.Network.Disabled = true + }) + + JustBeforeEach(func() { + ctx = suite.NewTestContextForVCSimWithParentContext( + parentCtx, testConfig, initObjects...) + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.MaxDeployThreadsOnProvider = 1 + }) + vmProvider = vsphere.NewVSphereVMProviderFromClient( + ctx, ctx.Client, ctx.Recorder) + nsInfo = ctx.CreateWorkloadNamespace() + + vmClass.Namespace = nsInfo.Namespace + Expect(ctx.Client.Create(ctx, vmClass)).To(Succeed()) + + clusterVMI1 := &vmopv1.ClusterVirtualMachineImage{} + + if testConfig.WithContentLibrary { + Expect(ctx.Client.Get( + ctx, client.ObjectKey{Name: ctx.ContentLibraryItem1Name}, + clusterVMI1)).To(Succeed()) + } else { + vsphere.SkipVMImageCLProviderCheck = true + clusterVMI1 = builder.DummyClusterVirtualMachineImage("DC0_C0_RP0_VM0") + Expect(ctx.Client.Create(ctx, clusterVMI1)).To(Succeed()) + conditions.MarkTrue(clusterVMI1, vmopv1.ReadyConditionType) + Expect(ctx.Client.Status().Update(ctx, clusterVMI1)).To(Succeed()) + } + + vm.Namespace = nsInfo.Namespace + vm.Spec.ClassName = vmClass.Name + vm.Spec.ImageName = clusterVMI1.Name + vm.Spec.Image.Kind = cvmiKind + vm.Spec.Image.Name = clusterVMI1.Name + vm.Spec.StorageClass = ctx.StorageClassName + + Expect(ctx.Client.Create(ctx, vm)).To(Succeed()) + + zoneName = ctx.GetFirstZoneName() + vm.Labels[corev1.LabelTopologyZone] = zoneName + Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) + }) + + AfterEach(func() { + vsphere.SkipVMImageCLProviderCheck = false + + if vm != nil && + !pkgcfg.FromContext(ctx).Features.BringYourOwnEncryptionKey { + By("Assert vm.Status.Crypto is nil when BYOK is disabled", func() { + Expect(vm.Status.Crypto).To(BeNil()) + }) + } + + vmClass = nil + vm = nil + + ctx.AfterEach() + ctx = nil + initObjects = nil + vmProvider = nil + nsInfo = builder.WorkloadNamespaceInfo{} + }) + + JustBeforeEach(func() { + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.Features.VMSharedDisks = true + config.Features.AllDisksArePVCs = false + }) + }) + JustBeforeEach(func() { + // Create the VM. + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + + // Clear its annotations and update it in K8s. + vm.Annotations = nil + Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) + }) + + It("should return ErrUpgradeSchema, then ErrUpgradeObject, then ErrBackup, then success", func() { + Expect(vm.Annotations).To(HaveLen(0)) + + // Update the VM and expect ErrUpgradeSchema. + Expect(vmProvider.CreateOrUpdateVirtualMachine(ctx, vm)).To( + MatchError(vsphere.ErrUpgradeSchema)) + + // Assert that the VM was schema upgraded. + Expect(vm.Annotations).To(HaveKeyWithValue( + pkgconst.UpgradedToBuildVersionAnnotationKey, + pkgcfg.FromContext(ctx).BuildVersion)) + Expect(vm.Annotations).To(HaveKeyWithValue( + pkgconst.UpgradedToSchemaVersionAnnotationKey, + vmopv1.GroupVersion.Version)) + Expect(vm.Annotations).ToNot(HaveKey( + pkgconst.UpgradedToFeatureVersionAnnotationKey)) + + // Update the VM again and expect ErrUpgradeObject. + Expect(vmProvider.CreateOrUpdateVirtualMachine(ctx, vm)).To( + MatchError(vsphere.ErrUpgradeObject)) + + // Assert that the VM was object upgraded. + Expect(vm.Annotations).To(HaveKeyWithValue( + pkgconst.UpgradedToBuildVersionAnnotationKey, + pkgcfg.FromContext(ctx).BuildVersion)) + Expect(vm.Annotations).To(HaveKeyWithValue( + pkgconst.UpgradedToSchemaVersionAnnotationKey, + vmopv1.GroupVersion.Version)) + Expect(vm.Annotations).To(HaveKeyWithValue( + pkgconst.UpgradedToFeatureVersionAnnotationKey, + vmopv1util.ActivatedFeatureVersion(ctx).String())) + + // Update the VM again and expect ErrBackup. + Expect(vmProvider.CreateOrUpdateVirtualMachine(ctx, vm)).To( + MatchError(vsphere.ErrBackup)) + + // Update the VM again and expect no error. + Expect(vmProvider.CreateOrUpdateVirtualMachine(ctx, vm)).To( + Succeed()) + }) +} diff --git a/pkg/providers/vsphere/vmprovider_vm_vappconfig_test.go b/pkg/providers/vsphere/vmprovider_vm_vappconfig_test.go deleted file mode 100644 index 25ef73dc9..000000000 --- a/pkg/providers/vsphere/vmprovider_vm_vappconfig_test.go +++ /dev/null @@ -1,157 +0,0 @@ -// © Broadcom. All Rights Reserved. -// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. -// SPDX-License-Identifier: Apache-2.0 - -package vsphere_test - -import ( - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - vimtypes "github.com/vmware/govmomi/vim25/types" - - "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" - "github.com/vmware-tanzu/vm-operator/pkg/util/ptr" -) - -func vAppConfigExpressionTests() { - Describe("NormalizeVAppConfigExpressionProperties", func() { - It("is a no-op when config spec is nil", func() { - vsphere.NormalizeVAppConfigExpressionProperties(nil) - }) - - It("is a no-op when VAppConfig is nil", func() { - spec := &vimtypes.VirtualMachineConfigSpec{} - vsphere.NormalizeVAppConfigExpressionProperties(spec) - Expect(spec.VAppConfig).To(BeNil()) - }) - - It("is a no-op when VmConfigSpec has no properties", func() { - spec := &vimtypes.VirtualMachineConfigSpec{ - VAppConfig: &vimtypes.VmConfigSpec{ - Property: []vimtypes.VAppPropertySpec{}, - }, - } - vsphere.NormalizeVAppConfigExpressionProperties(spec) - Expect(spec.VAppConfig.GetVmConfigSpec().Property).To(BeEmpty()) - }) - - It("converts expression type to string and sets UserConfigurable", func() { - spec := &vimtypes.VirtualMachineConfigSpec{ - VAppConfig: &vimtypes.VmConfigSpec{ - Property: []vimtypes.VAppPropertySpec{ - { - Info: &vimtypes.VAppPropertyInfo{ - Id: "net-expression", - Type: "expression", - DefaultValue: "com.vmware.customization.network", - UserConfigurable: ptr.To(false), - }, - }, - }, - }, - } - vsphere.NormalizeVAppConfigExpressionProperties(spec) - vac := spec.VAppConfig.GetVmConfigSpec() - Expect(vac).ToNot(BeNil()) - Expect(vac.Property).To(HaveLen(1)) - Expect(vac.Property[0].Info.Type).To(Equal("string")) - Expect(vac.Property[0].Info.DefaultValue).To(Equal("")) - Expect(vac.Property[0].Info.UserConfigurable).ToNot(BeNil()) - Expect(*vac.Property[0].Info.UserConfigurable).To(BeTrue()) - }) - - It("converts ip:network type to string and sets UserConfigurable", func() { - spec := &vimtypes.VirtualMachineConfigSpec{ - VAppConfig: &vimtypes.VmConfigSpec{ - Property: []vimtypes.VAppPropertySpec{ - { - Info: &vimtypes.VAppPropertyInfo{ - Id: "ip-network", - Type: "ip:network", - DefaultValue: "192.168.1.0/24", - UserConfigurable: nil, - }, - }, - }, - }, - } - vsphere.NormalizeVAppConfigExpressionProperties(spec) - vac := spec.VAppConfig.GetVmConfigSpec() - Expect(vac).ToNot(BeNil()) - Expect(vac.Property).To(HaveLen(1)) - Expect(vac.Property[0].Info.Type).To(Equal("string")) - Expect(vac.Property[0].Info.DefaultValue).To(Equal("")) - Expect(vac.Property[0].Info.UserConfigurable).ToNot(BeNil()) - Expect(*vac.Property[0].Info.UserConfigurable).To(BeTrue()) - }) - - It("leaves other property types unchanged", func() { - spec := &vimtypes.VirtualMachineConfigSpec{ - VAppConfig: &vimtypes.VmConfigSpec{ - Property: []vimtypes.VAppPropertySpec{ - { - Info: &vimtypes.VAppPropertyInfo{ - Id: "string-prop", Type: "string", - DefaultValue: "hello", UserConfigurable: ptr.To(true), - }, - }, - { - Info: &vimtypes.VAppPropertyInfo{ - Id: "int-prop", Type: "int", - DefaultValue: "42", UserConfigurable: ptr.To(false), - }, - }, - }, - }, - } - vsphere.NormalizeVAppConfigExpressionProperties(spec) - vac := spec.VAppConfig.GetVmConfigSpec() - Expect(vac.Property[0].Info.Type).To(Equal("string")) - Expect(vac.Property[0].Info.DefaultValue).To(Equal("hello")) - Expect(vac.Property[1].Info.Type).To(Equal("int")) - Expect(vac.Property[1].Info.DefaultValue).To(Equal("42")) - }) - - It("converts only expression and ip:network in a mixed list", func() { - spec := &vimtypes.VirtualMachineConfigSpec{ - VAppConfig: &vimtypes.VmConfigSpec{ - Property: []vimtypes.VAppPropertySpec{ - {Info: &vimtypes.VAppPropertyInfo{Id: "a", Type: "string", DefaultValue: "keep"}}, - {Info: &vimtypes.VAppPropertyInfo{Id: "b", Type: "expression", DefaultValue: "expr"}}, - {Info: &vimtypes.VAppPropertyInfo{Id: "c", Type: "ip:network", DefaultValue: "10.0.0.0/8"}}, - {Info: &vimtypes.VAppPropertyInfo{Id: "d", Type: "int", DefaultValue: "1"}}, - }, - }, - } - vsphere.NormalizeVAppConfigExpressionProperties(spec) - vac := spec.VAppConfig.GetVmConfigSpec() - Expect(vac.Property[0].Info.Type).To(Equal("string")) - Expect(vac.Property[0].Info.DefaultValue).To(Equal("keep")) - Expect(vac.Property[1].Info.Type).To(Equal("string")) - Expect(vac.Property[1].Info.DefaultValue).To(Equal("")) - Expect(*vac.Property[1].Info.UserConfigurable).To(BeTrue()) - Expect(vac.Property[2].Info.Type).To(Equal("string")) - Expect(vac.Property[2].Info.DefaultValue).To(Equal("")) - Expect(*vac.Property[2].Info.UserConfigurable).To(BeTrue()) - Expect(vac.Property[3].Info.Type).To(Equal("int")) - Expect(vac.Property[3].Info.DefaultValue).To(Equal("1")) - }) - - It("skips properties with nil Info", func() { - spec := &vimtypes.VirtualMachineConfigSpec{ - VAppConfig: &vimtypes.VmConfigSpec{ - Property: []vimtypes.VAppPropertySpec{ - {Info: nil}, - {Info: &vimtypes.VAppPropertyInfo{Id: "b", Type: "expression", DefaultValue: "x"}}, - }, - }, - } - vsphere.NormalizeVAppConfigExpressionProperties(spec) - vac := spec.VAppConfig.GetVmConfigSpec() - Expect(vac.Property[0].Info).To(BeNil()) - Expect(vac.Property[1].Info.Type).To(Equal("string")) - Expect(vac.Property[1].Info.DefaultValue).To(Equal("")) - }) - }) -} diff --git a/pkg/providers/vsphere/vmprovider_vm_vks_test.go b/pkg/providers/vsphere/vmprovider_vm_vks_test.go new file mode 100644 index 000000000..f0d726787 --- /dev/null +++ b/pkg/providers/vsphere/vmprovider_vm_vks_test.go @@ -0,0 +1,176 @@ +// © Broadcom. All Rights Reserved. +// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package vsphere_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/vmware/govmomi/object" + "github.com/vmware/govmomi/vim25/mo" + + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha6" + backupapi "github.com/vmware-tanzu/vm-operator/pkg/backup/api" + "github.com/vmware-tanzu/vm-operator/pkg/conditions" + pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + ctxop "github.com/vmware-tanzu/vm-operator/pkg/context/operation" + "github.com/vmware-tanzu/vm-operator/pkg/providers" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" + pkgutil "github.com/vmware-tanzu/vm-operator/pkg/util" + kubeutil "github.com/vmware-tanzu/vm-operator/pkg/util/kube" + "github.com/vmware-tanzu/vm-operator/pkg/util/kube/cource" + "github.com/vmware-tanzu/vm-operator/pkg/util/ovfcache" + "github.com/vmware-tanzu/vm-operator/test/builder" +) + +func vmVKSTests() { + var ( + parentCtx context.Context + initObjects []client.Object + testConfig builder.VCSimTestConfig + ctx *builder.TestContextForVCSim + vmProvider providers.VirtualMachineProviderInterface + nsInfo builder.WorkloadNamespaceInfo + + vm *vmopv1.VirtualMachine + vmClass *vmopv1.VirtualMachineClass + + vcVM *object.VirtualMachine + moVM mo.VirtualMachine + + optIntoBackup bool + + zoneName string + ) + + BeforeEach(func() { + parentCtx = pkgcfg.NewContextWithDefaultConfig() + parentCtx = ctxop.WithContext(parentCtx) + parentCtx = ovfcache.WithContext(parentCtx) + parentCtx = cource.WithContext(parentCtx) + pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { + config.AsyncCreateEnabled = false + config.AsyncSignalEnabled = false + }) + testConfig = builder.VCSimTestConfig{ + WithContentLibrary: true, + } + + vmClass = builder.DummyVirtualMachineClassGenName() + vm = builder.DummyBasicVirtualMachine("test-vm", "") + + if vm.Spec.Network == nil { + vm.Spec.Network = &vmopv1.VirtualMachineNetworkSpec{} + } + vm.Spec.Network.Disabled = true + }) + + JustBeforeEach(func() { + ctx = suite.NewTestContextForVCSimWithParentContext( + parentCtx, testConfig, initObjects...) + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.MaxDeployThreadsOnProvider = 1 + }) + vmProvider = vsphere.NewVSphereVMProviderFromClient( + ctx, ctx.Client, ctx.Recorder) + nsInfo = ctx.CreateWorkloadNamespace() + + vmClass.Namespace = nsInfo.Namespace + Expect(ctx.Client.Create(ctx, vmClass)).To(Succeed()) + + clusterVMI1 := &vmopv1.ClusterVirtualMachineImage{} + + if testConfig.WithContentLibrary { + Expect(ctx.Client.Get( + ctx, client.ObjectKey{Name: ctx.ContentLibraryItem1Name}, + clusterVMI1)).To(Succeed()) + } else { + vsphere.SkipVMImageCLProviderCheck = true + clusterVMI1 = builder.DummyClusterVirtualMachineImage("DC0_C0_RP0_VM0") + Expect(ctx.Client.Create(ctx, clusterVMI1)).To(Succeed()) + conditions.MarkTrue(clusterVMI1, vmopv1.ReadyConditionType) + Expect(ctx.Client.Status().Update(ctx, clusterVMI1)).To(Succeed()) + } + + vm.Namespace = nsInfo.Namespace + vm.Spec.ClassName = vmClass.Name + vm.Spec.ImageName = clusterVMI1.Name + vm.Spec.Image.Kind = cvmiKind + vm.Spec.Image.Name = clusterVMI1.Name + vm.Spec.StorageClass = ctx.StorageClassName + + Expect(ctx.Client.Create(ctx, vm)).To(Succeed()) + + zoneName = ctx.GetFirstZoneName() + vm.Labels[corev1.LabelTopologyZone] = zoneName + Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) + + if vm.Labels == nil { + vm.Labels = make(map[string]string) + } + vm.Labels[kubeutil.CAPVClusterRoleLabelKey] = "" + vm.Labels[kubeutil.CAPWClusterRoleLabelKey] = "" + + if optIntoBackup { + if vm.Annotations == nil { + vm.Annotations = make(map[string]string) + } + vm.Annotations[vmopv1.ForceEnableBackupAnnotation] = "true" + } + + var err error + vcVM, err = createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + Expect(vcVM.Properties(ctx, vcVM.Reference(), nil, &moVM)).To(Succeed()) + + }) + + AfterEach(func() { + optIntoBackup = false + + vsphere.SkipVMImageCLProviderCheck = false + + if vm != nil && + !pkgcfg.FromContext(ctx).Features.BringYourOwnEncryptionKey { + By("Assert vm.Status.Crypto is nil when BYOK is disabled", func() { + Expect(vm.Status.Crypto).To(BeNil()) + }) + } + + vmClass = nil + vm = nil + vcVM = nil + moVM = mo.VirtualMachine{} + + ctx.AfterEach() + ctx = nil + initObjects = nil + vmProvider = nil + nsInfo = builder.WorkloadNamespaceInfo{} + }) + + It("should not have any backup ExtraConfig key", func() { + Expect(moVM.Config.ExtraConfig).ToNot(BeNil()) + ecMap := pkgutil.OptionValues(moVM.Config.ExtraConfig).StringMap() + Expect(ecMap).ToNot(HaveKey(backupapi.VMResourceYAMLExtraConfigKey)) + }) + + When("node opts into backup", func() { + BeforeEach(func() { + optIntoBackup = true + }) + It("should have backup ExtraConfig key", func() { + Expect(moVM.Config.ExtraConfig).ToNot(BeNil()) + ecMap := pkgutil.OptionValues(moVM.Config.ExtraConfig).StringMap() + Expect(ecMap).To(HaveKey(backupapi.VMResourceYAMLExtraConfigKey)) + }) + }) +} diff --git a/pkg/providers/vsphere/vmprovider_vm_webconsole_test.go b/pkg/providers/vsphere/vmprovider_vm_webconsole_test.go new file mode 100644 index 000000000..35047c101 --- /dev/null +++ b/pkg/providers/vsphere/vmprovider_vm_webconsole_test.go @@ -0,0 +1,126 @@ +// © Broadcom. All Rights Reserved. +// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package vsphere_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha6" + "github.com/vmware-tanzu/vm-operator/pkg/conditions" + pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + ctxop "github.com/vmware-tanzu/vm-operator/pkg/context/operation" + "github.com/vmware-tanzu/vm-operator/pkg/providers" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" + "github.com/vmware-tanzu/vm-operator/pkg/util/kube/cource" + "github.com/vmware-tanzu/vm-operator/pkg/util/ovfcache" + "github.com/vmware-tanzu/vm-operator/test/builder" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func vmWebConsoleTests() { + var ( + parentCtx context.Context + initObjects []client.Object + testConfig builder.VCSimTestConfig + ctx *builder.TestContextForVCSim + vmProvider providers.VirtualMachineProviderInterface + nsInfo builder.WorkloadNamespaceInfo + + vm *vmopv1.VirtualMachine + vmClass *vmopv1.VirtualMachineClass + ) + + BeforeEach(func() { + parentCtx = pkgcfg.NewContextWithDefaultConfig() + parentCtx = ctxop.WithContext(parentCtx) + parentCtx = ovfcache.WithContext(parentCtx) + parentCtx = cource.WithContext(parentCtx) + pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { + config.AsyncCreateEnabled = false + config.AsyncSignalEnabled = false + }) + testConfig = builder.VCSimTestConfig{ + WithContentLibrary: true, + } + + vmClass = builder.DummyVirtualMachineClassGenName() + vm = builder.DummyBasicVirtualMachine("test-vm", "") + + if vm.Spec.Network == nil { + vm.Spec.Network = &vmopv1.VirtualMachineNetworkSpec{} + } + vm.Spec.Network.Disabled = true + }) + + JustBeforeEach(func() { + ctx = suite.NewTestContextForVCSimWithParentContext( + parentCtx, testConfig, initObjects...) + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.MaxDeployThreadsOnProvider = 1 + }) + vmProvider = vsphere.NewVSphereVMProviderFromClient( + ctx, ctx.Client, ctx.Recorder) + nsInfo = ctx.CreateWorkloadNamespace() + + vmClass.Namespace = nsInfo.Namespace + Expect(ctx.Client.Create(ctx, vmClass)).To(Succeed()) + + clusterVMI1 := &vmopv1.ClusterVirtualMachineImage{} + + if testConfig.WithContentLibrary { + Expect(ctx.Client.Get( + ctx, client.ObjectKey{Name: ctx.ContentLibraryItem1Name}, + clusterVMI1)).To(Succeed()) + } else { + vsphere.SkipVMImageCLProviderCheck = true + clusterVMI1 = builder.DummyClusterVirtualMachineImage("DC0_C0_RP0_VM0") + Expect(ctx.Client.Create(ctx, clusterVMI1)).To(Succeed()) + conditions.MarkTrue(clusterVMI1, vmopv1.ReadyConditionType) + Expect(ctx.Client.Status().Update(ctx, clusterVMI1)).To(Succeed()) + } + + vm.Namespace = nsInfo.Namespace + vm.Spec.ClassName = vmClass.Name + vm.Spec.ImageName = clusterVMI1.Name + vm.Spec.Image.Kind = cvmiKind + vm.Spec.Image.Name = clusterVMI1.Name + vm.Spec.StorageClass = ctx.StorageClassName + + Expect(ctx.Client.Create(ctx, vm)).To(Succeed()) + }) + + AfterEach(func() { + vsphere.SkipVMImageCLProviderCheck = false + + if vm != nil && + !pkgcfg.FromContext(ctx).Features.BringYourOwnEncryptionKey { + By("Assert vm.Status.Crypto is nil when BYOK is disabled", func() { + Expect(vm.Status.Crypto).To(BeNil()) + }) + } + + vmClass = nil + vm = nil + + ctx.AfterEach() + ctx = nil + initObjects = nil + vmProvider = nil + nsInfo = builder.WorkloadNamespaceInfo{} + }) + + JustBeforeEach(func() { + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + }) + + It("should return a ticket", func() { + // vcsim doesn't implement this yet so expect an error. + _, err := vmProvider.GetVirtualMachineWebMKSTicket(ctx, vm, "foo") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("does not implement: AcquireTicket")) + }) +} diff --git a/pkg/providers/vsphere/vmprovider_vm_zone_test.go b/pkg/providers/vsphere/vmprovider_vm_zone_test.go new file mode 100644 index 000000000..12f7b4c6d --- /dev/null +++ b/pkg/providers/vsphere/vmprovider_vm_zone_test.go @@ -0,0 +1,136 @@ +// © Broadcom. All Rights Reserved. +// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package vsphere_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha6" + "github.com/vmware-tanzu/vm-operator/pkg/conditions" + pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + ctxop "github.com/vmware-tanzu/vm-operator/pkg/context/operation" + "github.com/vmware-tanzu/vm-operator/pkg/providers" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" + "github.com/vmware-tanzu/vm-operator/pkg/util/kube/cource" + "github.com/vmware-tanzu/vm-operator/pkg/util/ovfcache" + "github.com/vmware-tanzu/vm-operator/test/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + + corev1 "k8s.io/api/core/v1" +) + +func vmZoneTests() { + var ( + parentCtx context.Context + initObjects []client.Object + testConfig builder.VCSimTestConfig + ctx *builder.TestContextForVCSim + vmProvider providers.VirtualMachineProviderInterface + nsInfo builder.WorkloadNamespaceInfo + + vm *vmopv1.VirtualMachine + vmClass *vmopv1.VirtualMachineClass + + zoneName string + ) + + BeforeEach(func() { + parentCtx = pkgcfg.NewContextWithDefaultConfig() + parentCtx = ctxop.WithContext(parentCtx) + parentCtx = ovfcache.WithContext(parentCtx) + parentCtx = cource.WithContext(parentCtx) + pkgcfg.SetContext(parentCtx, func(config *pkgcfg.Config) { + config.AsyncCreateEnabled = false + config.AsyncSignalEnabled = false + }) + testConfig = builder.VCSimTestConfig{ + WithContentLibrary: true, + } + + vmClass = builder.DummyVirtualMachineClassGenName() + vm = builder.DummyBasicVirtualMachine("test-vm", "") + + if vm.Spec.Network == nil { + vm.Spec.Network = &vmopv1.VirtualMachineNetworkSpec{} + } + vm.Spec.Network.Disabled = true + }) + + JustBeforeEach(func() { + ctx = suite.NewTestContextForVCSimWithParentContext( + parentCtx, testConfig, initObjects...) + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.MaxDeployThreadsOnProvider = 1 + }) + vmProvider = vsphere.NewVSphereVMProviderFromClient( + ctx, ctx.Client, ctx.Recorder) + nsInfo = ctx.CreateWorkloadNamespace() + + vmClass.Namespace = nsInfo.Namespace + Expect(ctx.Client.Create(ctx, vmClass)).To(Succeed()) + + clusterVMI1 := &vmopv1.ClusterVirtualMachineImage{} + + if testConfig.WithContentLibrary { + Expect(ctx.Client.Get( + ctx, client.ObjectKey{Name: ctx.ContentLibraryItem1Name}, + clusterVMI1)).To(Succeed()) + } else { + vsphere.SkipVMImageCLProviderCheck = true + clusterVMI1 = builder.DummyClusterVirtualMachineImage("DC0_C0_RP0_VM0") + Expect(ctx.Client.Create(ctx, clusterVMI1)).To(Succeed()) + conditions.MarkTrue(clusterVMI1, vmopv1.ReadyConditionType) + Expect(ctx.Client.Status().Update(ctx, clusterVMI1)).To(Succeed()) + } + + vm.Namespace = nsInfo.Namespace + vm.Spec.ClassName = vmClass.Name + vm.Spec.ImageName = clusterVMI1.Name + vm.Spec.Image.Kind = cvmiKind + vm.Spec.Image.Name = clusterVMI1.Name + vm.Spec.StorageClass = ctx.StorageClassName + + Expect(ctx.Client.Create(ctx, vm)).To(Succeed()) + + zoneName = ctx.GetFirstZoneName() + vm.Labels[corev1.LabelTopologyZone] = zoneName + Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) + }) + + AfterEach(func() { + vsphere.SkipVMImageCLProviderCheck = false + + if vm != nil && + !pkgcfg.FromContext(ctx).Features.BringYourOwnEncryptionKey { + By("Assert vm.Status.Crypto is nil when BYOK is disabled", func() { + Expect(vm.Status.Crypto).To(BeNil()) + }) + } + + vmClass = nil + vm = nil + + ctx.AfterEach() + ctx = nil + initObjects = nil + vmProvider = nil + nsInfo = builder.WorkloadNamespaceInfo{} + }) + + It("Reverse lookups existing VM into correct zone", func() { + _, err := createOrUpdateAndGetVcVM(ctx, vmProvider, vm) + Expect(err).ToNot(HaveOccurred()) + + Expect(vm.Labels).To(HaveKeyWithValue(corev1.LabelTopologyZone, zoneName)) + Expect(vm.Status.Zone).To(Equal(zoneName)) + delete(vm.Labels, corev1.LabelTopologyZone) + + Expect(createOrUpdateVM(ctx, vmProvider, vm)).To(Succeed()) + Expect(vm.Labels).To(HaveKeyWithValue(corev1.LabelTopologyZone, zoneName)) + Expect(vm.Status.Zone).To(Equal(zoneName)) + }) +} diff --git a/pkg/providers/vsphere/vmprovider_vm_group.go b/pkg/providers/vsphere/vmprovider_vmgroup.go similarity index 100% rename from pkg/providers/vsphere/vmprovider_vm_group.go rename to pkg/providers/vsphere/vmprovider_vmgroup.go diff --git a/pkg/providers/vsphere/vmprovider_vmgroup_test.go b/pkg/providers/vsphere/vmprovider_vmgroup_test.go new file mode 100644 index 000000000..1cab1272f --- /dev/null +++ b/pkg/providers/vsphere/vmprovider_vmgroup_test.go @@ -0,0 +1,330 @@ +// © Broadcom. All Rights Reserved. +// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package vsphere_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha6" + vspherepolv1 "github.com/vmware-tanzu/vm-operator/external/vsphere-policy/api/v1alpha1" + pkgcond "github.com/vmware-tanzu/vm-operator/pkg/conditions" + pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + "github.com/vmware-tanzu/vm-operator/pkg/constants/testlabels" + "github.com/vmware-tanzu/vm-operator/pkg/providers" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" + pkgutil "github.com/vmware-tanzu/vm-operator/pkg/util" + "github.com/vmware-tanzu/vm-operator/test/builder" +) + +var _ = Describe( + "VirtualMachineGroup", + Label(testlabels.VCSim), + Label(testlabels.Group), func() { + + var ( + initObjects []client.Object + testConfig builder.VCSimTestConfig + ctx *builder.TestContextForVCSim + vmProvider providers.VirtualMachineProviderInterface + nsInfo builder.WorkloadNamespaceInfo + + vm1 *vmopv1.VirtualMachine + vm2 *vmopv1.VirtualMachine + vmClass *vmopv1.VirtualMachineClass + vmGroup *vmopv1.VirtualMachineGroup + ) + + BeforeEach(func() { + testConfig = builder.VCSimTestConfig{ + WithContentLibrary: true, + } + + vm1 = builder.DummyBasicVirtualMachine("group-placement-vm-1", "") + vm2 = builder.DummyBasicVirtualMachine("group-placement-vm-2", "") + vmClass = builder.DummyVirtualMachineClassGenName() + + vmGroup = &vmopv1.VirtualMachineGroup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vm-group-test", + }, + Spec: vmopv1.VirtualMachineGroupSpec{ + BootOrder: make([]vmopv1.VirtualMachineGroupBootOrderGroup, 1), + }, + } + vmGroup.Spec.BootOrder[0].Members = append(vmGroup.Spec.BootOrder[0].Members, + vmopv1.GroupMember{Kind: "VirtualMachine", Name: vm1.Name}, + vmopv1.GroupMember{Kind: "VirtualMachine", Name: vm2.Name}) + }) + + JustBeforeEach(func() { + ctx = suite.NewTestContextForVCSim(testConfig, initObjects...) + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.Features.FastDeploy = true + }) + vmProvider = vsphere.NewVSphereVMProviderFromClient(ctx, ctx.Client, ctx.Recorder) + nsInfo = ctx.CreateWorkloadNamespace() + + vmClass.Namespace = nsInfo.Namespace + Expect(ctx.Client.Create(ctx, vmClass)).To(Succeed()) + Expect(ctx.Client.Status().Update(ctx, vmClass)).To(Succeed()) + + initVM := func(vm *vmopv1.VirtualMachine) { + vm.Namespace = nsInfo.Namespace + vm.Spec.ClassName = vmClass.Name + vm.Spec.ImageName = ctx.ContentLibraryItem1Name + vm.Spec.Image.Kind = cvmiKind + vm.Spec.Image.Name = ctx.ContentLibraryItem1Name + vm.Spec.StorageClass = ctx.StorageClassName + vm.Spec.GroupName = vmGroup.Name + } + initVM(vm1) + initVM(vm2) + + vmGroup.Namespace = nsInfo.Namespace + + { + // TODO: Put this test builder to reduce duplication. + + vmic := vmopv1.VirtualMachineImageCache{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: pkgcfg.FromContext(ctx).PodNamespace, + Name: pkgutil.VMIName(ctx.ContentLibraryItem1ID), + }, + } + Expect(ctx.Client.Create(ctx, &vmic)).To(Succeed()) + + vmicm := corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: vmic.Namespace, + Name: vmic.Name, + }, + Data: map[string]string{ + "value": ctx.ContentLibraryItem1YAML, + }, + } + Expect(ctx.Client.Create(ctx, &vmicm)).To(Succeed()) + + vmic.Status = vmopv1.VirtualMachineImageCacheStatus{ + OVF: &vmopv1.VirtualMachineImageCacheOVFStatus{ + ConfigMapName: vmic.Name, + ProviderVersion: ctx.ContentLibraryItem1Version, + }, + Conditions: []metav1.Condition{ + { + Type: vmopv1.VirtualMachineImageCacheConditionHardwareReady, + Status: metav1.ConditionTrue, + }, + }, + } + Expect(ctx.Client.Status().Update(ctx, &vmic)).To(Succeed()) + + pkgcond.MarkTrue( + &vmic, + vmopv1.VirtualMachineImageCacheConditionFilesReady) + vmic.Status.Locations = []vmopv1.VirtualMachineImageCacheLocationStatus{ + { + DatacenterID: ctx.Datacenter.Reference().Value, + DatastoreID: ctx.Datastore.Reference().Value, + Files: []vmopv1.VirtualMachineImageCacheFileStatus{ + { + ID: ctx.ContentLibraryItem1Disk1Path, + Type: vmopv1.VirtualMachineImageCacheFileTypeDisk, + DiskType: vmopv1.VolumeTypeClassic, + }, + { + ID: ctx.ContentLibraryItem1NVRAMPath, + Type: vmopv1.VirtualMachineImageCacheFileTypeOther, + }, + }, + Conditions: []metav1.Condition{ + { + Type: vmopv1.ReadyConditionType, + Status: metav1.ConditionTrue, + }, + }, + }, + } + Expect(ctx.Client.Status().Update(ctx, &vmic)).To(Succeed()) + } + }) + + AfterEach(func() { + ctx.AfterEach() + ctx = nil + initObjects = nil + vmProvider = nil + nsInfo = builder.WorkloadNamespaceInfo{} + + vm1 = nil + vm2 = nil + vmClass = nil + vmGroup = nil + }) + + assertMemberStatusForVM := func(vm *vmopv1.VirtualMachine, ms vmopv1.VirtualMachineGroupMemberStatus) { + GinkgoHelper() + + Expect(ms.Name).To(Equal(vm.Name), "Unexpected Name") + Expect(ms.Kind).To(Equal("VirtualMachine"), "Unexpected Kind") + Expect(ms.Placement).ToNot(BeNil(), "Missing Placement") + Expect(pkgcond.IsTrue(&ms, vmopv1.VirtualMachineGroupMemberConditionPlacementReady)).To(BeTrue(), "No placement ready condition") + Expect(ms.Placement.Zone).ToNot(BeEmpty(), "Missing Placement Zone") + Expect(ms.Placement.Pool).ToNot(BeEmpty(), "Missing Placement Pool") + Expect(ms.Placement.Node).To(BeEmpty(), "Has Placement Node") + if pkgcfg.FromContext(ctx).Features.FastDeploy { + Expect(ms.Placement.Datastores).ToNot(BeEmpty(), "Missing Placement Datastores") + // Verify against VirtualMachineImageCache.Status + } else { + Expect(ms.Placement.Datastores).To(BeEmpty(), "Has Placement Datastores") + } + } + + assertNotReadyMemberStatusForVM := func( + vm *vmopv1.VirtualMachine, + ms vmopv1.VirtualMachineGroupMemberStatus, + reason string) { + + GinkgoHelper() + + Expect(ms.Name).To(Equal(vm.Name), "Unexpected Name") + Expect(ms.Kind).To(Equal("VirtualMachine"), "Unexpected Kind") + Expect(ms.Placement).To(BeNil(), "Has Placement") + + c := pkgcond.Get(ms, vmopv1.VirtualMachineGroupMemberConditionPlacementReady) + Expect(c).ToNot(BeNil(), "Condition missing") + Expect(c.Status).To(Equal(metav1.ConditionFalse)) + Expect(c.Reason).To(Equal(reason)) + } + + Context("Group placement with VMs specifying affinity policies", func() { + It("should process preferred VM affinity policies during group placement", func() { + // Add preferred affinity policy to vm1 + vm1.Spec.Affinity = &vmopv1.AffinitySpec{ + VMAffinity: &vmopv1.VMAffinitySpec{ + PreferredDuringSchedulingPreferredDuringExecution: []vmopv1.VMAffinityTerm{ + { + LabelSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "database", + }, + }, + TopologyKey: "topology.kubernetes.io/zone", + }, + }, + }, + } + + groupPlacements := []providers.VMGroupPlacement{ + { + VMGroup: vmGroup, + VMMembers: []*vmopv1.VirtualMachine{ + vm1, + vm2, + }, + }, + } + + err := vmProvider.PlaceVirtualMachineGroup(ctx, vmGroup, groupPlacements) + Expect(err).ToNot(HaveOccurred()) + Expect(vmGroup.Status.Members).To(HaveLen(2)) + assertMemberStatusForVM(vm1, vmGroup.Status.Members[0]) + assertMemberStatusForVM(vm2, vmGroup.Status.Members[1]) + }) + + It("should process required VM affinity policies during group placement", func() { + // Add required affinity policy to vm1 + vm1.Spec.Affinity = &vmopv1.AffinitySpec{ + VMAffinity: &vmopv1.VMAffinitySpec{ + RequiredDuringSchedulingPreferredDuringExecution: []vmopv1.VMAffinityTerm{ + { + LabelSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "tier": "frontend", + }, + }, + TopologyKey: "topology.kubernetes.io/zone", + }, + }, + }, + } + + groupPlacements := []providers.VMGroupPlacement{ + { + VMGroup: vmGroup, + VMMembers: []*vmopv1.VirtualMachine{ + vm1, + vm2, + }, + }, + } + + err := vmProvider.PlaceVirtualMachineGroup(ctx, vmGroup, groupPlacements) + Expect(err).ToNot(HaveOccurred()) + Expect(vmGroup.Status.Members).To(HaveLen(2)) + assertMemberStatusForVM(vm1, vmGroup.Status.Members[0]) + assertMemberStatusForVM(vm2, vmGroup.Status.Members[1]) + }) + }) + + Context("VSpherePolicies is enabled", func() { + JustBeforeEach(func() { + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.Features.VSpherePolicies = true + }) + }) + + It("should process VM with PolicyEval during group placement", func() { + groupPlacements := []providers.VMGroupPlacement{ + { + VMGroup: vmGroup, + VMMembers: []*vmopv1.VirtualMachine{ + vm1, + vm2, + }, + }, + } + + err := vmProvider.PlaceVirtualMachineGroup(ctx, vmGroup, groupPlacements) + Expect(err).To(HaveOccurred()) + + Expect(vmGroup.Status.Members).To(HaveLen(2)) + assertNotReadyMemberStatusForVM(vm1, vmGroup.Status.Members[0], "NotReady") + assertNotReadyMemberStatusForVM(vm2, vmGroup.Status.Members[1], "NotReady") + + markPolicyEvalReady := func(vm *vmopv1.VirtualMachine) { + policyEval := &vspherepolv1.PolicyEvaluation{} + Expect(ctx.Client.Get(ctx, client.ObjectKey{ + Namespace: vm.Namespace, + Name: "vm-" + vm.Name}, + policyEval)).To(Succeed()) + policyEval.Status.ObservedGeneration = policyEval.Generation + pkgcond.MarkTrue(policyEval, vspherepolv1.ReadyConditionType) + Expect(ctx.Client.Status().Update(ctx, policyEval)).To(Succeed()) + } + + markPolicyEvalReady(vm1) + err = vmProvider.PlaceVirtualMachineGroup(ctx, vmGroup, groupPlacements) + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError(vsphere.ErrVMGroupPlacementConfigSpec)) + + Expect(vmGroup.Status.Members).To(HaveLen(2)) + assertNotReadyMemberStatusForVM(vm1, vmGroup.Status.Members[0], "PendingPlacement") + assertNotReadyMemberStatusForVM(vm2, vmGroup.Status.Members[1], "NotReady") + + markPolicyEvalReady(vm2) + err = vmProvider.PlaceVirtualMachineGroup(ctx, vmGroup, groupPlacements) + Expect(err).ToNot(HaveOccurred()) + + Expect(vmGroup.Status.Members).To(HaveLen(2)) + assertMemberStatusForVM(vm1, vmGroup.Status.Members[0]) + assertMemberStatusForVM(vm2, vmGroup.Status.Members[1]) + }) + }) + }) diff --git a/pkg/providers/vsphere/vmprovider_vm_snapshot.go b/pkg/providers/vsphere/vmprovider_vmsnapshot.go similarity index 100% rename from pkg/providers/vsphere/vmprovider_vm_snapshot.go rename to pkg/providers/vsphere/vmprovider_vmsnapshot.go diff --git a/pkg/providers/vsphere/vmprovider_vmsnapshot_test.go b/pkg/providers/vsphere/vmprovider_vmsnapshot_test.go new file mode 100644 index 000000000..dce120e6e --- /dev/null +++ b/pkg/providers/vsphere/vmprovider_vmsnapshot_test.go @@ -0,0 +1,580 @@ +// © Broadcom. All Rights Reserved. +// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package vsphere_test + +import ( + "path/filepath" + "time" + + "github.com/go-logr/logr" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + "github.com/vmware/govmomi/object" + "github.com/vmware/govmomi/vim25/mo" + + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha6" + "github.com/vmware-tanzu/vm-operator/pkg/conditions" + pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + "github.com/vmware-tanzu/vm-operator/pkg/constants/testlabels" + pkgctx "github.com/vmware-tanzu/vm-operator/pkg/context" + pkgerr "github.com/vmware-tanzu/vm-operator/pkg/errors" + "github.com/vmware-tanzu/vm-operator/pkg/providers" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/virtualmachine" + kubeutil "github.com/vmware-tanzu/vm-operator/pkg/util/kube" + vmconfunmanagedvolsfil "github.com/vmware-tanzu/vm-operator/pkg/vmconfig/volumes/unmanaged/backfill" + vmconfunmanagedvolsreg "github.com/vmware-tanzu/vm-operator/pkg/vmconfig/volumes/unmanaged/register" + "github.com/vmware-tanzu/vm-operator/test/builder" + "github.com/vmware-tanzu/vm-operator/test/testutil" +) + +var _ = Describe( + "VirtualMachineSnapshot", + Label(testlabels.VCSim), + Label(testlabels.Snapshot), func() { + + const ( + dummySnapshot = "dummy-snapshot" + ) + + var ( + initObjects []ctrlclient.Object + ctx *builder.TestContextForVCSim + vmProvider providers.VirtualMachineProviderInterface + nsInfo builder.WorkloadNamespaceInfo + vmSnapshot *vmopv1.VirtualMachineSnapshot + vcVM *object.VirtualMachine + vm *vmopv1.VirtualMachine + vmCtx pkgctx.VirtualMachineContext + ) + + BeforeEach(func() { + ctx = suite.NewTestContextForVCSim(builder.VCSimTestConfig{}, initObjects...) + vmProvider = vsphere.NewVSphereVMProviderFromClient(ctx, ctx.Client, ctx.Recorder) + nsInfo = ctx.CreateWorkloadNamespace() + + var err error + vcVM, err = ctx.Finder.VirtualMachine(ctx, "DC0_C0_RP0_VM0") + Expect(err).ToNot(HaveOccurred()) + Expect(vcVM).ToNot(BeNil()) + + By("Creating VM CR") + vm = builder.DummyBasicVirtualMachine(dummySnapshot, nsInfo.Namespace) + vm.Status.UniqueID = vcVM.Reference().Value + Expect(ctx.Client.Create(ctx, vm)).To(Succeed()) + + By("Creating snapshot CR") + vmSnapshot = builder.DummyVirtualMachineSnapshot(nsInfo.Namespace, dummySnapshot, vcVM.Name()) + Expect(ctx.Client.Create(ctx, vmSnapshot)).To(Succeed()) + + // TODO (lubron): Add FCD to the VM and test the snapshot size once + // vcsim has support to show attached disk as device + + By("Creating snapshot on vSphere") + logger := testutil.GinkgoLogr(5) + vmCtx = pkgctx.VirtualMachineContext{ + Context: logr.NewContext(ctx, logger), + Logger: logger.WithValues("vmName", vcVM.Name()), + VM: vm, + } + args := virtualmachine.SnapshotArgs{ + VMCtx: vmCtx, + VMSnapshot: *vmSnapshot, + VcVM: vcVM, + } + snapMo, err := virtualmachine.CreateSnapshot(args) + Expect(err).ToNot(HaveOccurred()) + Expect(snapMo).ToNot(BeNil()) + }) + + AfterEach(func() { + ctx.AfterEach() + ctx = nil + initObjects = nil + vmProvider = nil + vmSnapshot = nil + vmCtx = pkgctx.VirtualMachineContext{} + vm = nil + nsInfo = builder.WorkloadNamespaceInfo{} + }) + + Context("GetSnapshotSize", func() { + It("should return the size of the snapshot", func() { + size, err := vmProvider.GetSnapshotSize(ctx, vmSnapshot.Name, vm) + Expect(err).ToNot(HaveOccurred()) + + // Since we only have one snapshot, the size should be same as the vm + var moVM mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), []string{"layoutEx"}, &moVM)).To(Succeed()) + var total int64 + for _, file := range moVM.LayoutEx.File { + switch filepath.Ext(file.Name) { + case ".vmdk", ".vmsn", ".vmem": + total += file.Size + } + } + Expect(size).To(Equal(total)) + }) + + When("there is issue finding vm", func() { + BeforeEach(func() { + vm.Status.UniqueID = "" + }) + It("should return error", func() { + size, err := vmProvider.GetSnapshotSize(ctx, vmSnapshot.Name, vm) + Expect(err).To(HaveOccurred()) + Expect(size).To(BeZero()) + }) + }) + + When("there is issue finding snapshot", func() { + BeforeEach(func() { + vmSnapshot.Name = "" + }) + It("should return error", func() { + size, err := vmProvider.GetSnapshotSize(ctx, vmSnapshot.Name, vm) + Expect(err).To(HaveOccurred()) + Expect(size).To(BeZero()) + }) + }) + }) + + Context("DeleteSnapshot", func() { + var ( + deleted bool + err error + ) + + JustBeforeEach(func() { + deleted, err = vmProvider.DeleteSnapshot(ctx, vmSnapshot, vm, true, nil) + }) + + It("should return false and no error", func() { + Expect(deleted).To(BeFalse()) + Expect(err).NotTo(HaveOccurred()) + snapMoRef, err := vcVM.FindSnapshot(ctx, dummySnapshot) + Expect(err).To(HaveOccurred()) + Expect(snapMoRef).To(BeNil()) + }) + + Context("VM is not found", func() { + BeforeEach(func() { + vm.Status.UniqueID = "" + }) + It("should return true and no error", func() { + Expect(deleted).To(BeTrue()) + Expect(err).NotTo(HaveOccurred()) + snapMoRef, err := vcVM.FindSnapshot(ctx, dummySnapshot) + Expect(err).NotTo(HaveOccurred()) + Expect(snapMoRef).NotTo(BeNil()) + }) + }) + + Context("snapshot not found", func() { + BeforeEach(func() { + By("Deleting snapshot in advance") + Expect(virtualmachine.DeleteSnapshot(virtualmachine.SnapshotArgs{ + VMCtx: vmCtx, + VMSnapshot: *vmSnapshot, + VcVM: vcVM, + })).To(Succeed()) + }) + It("should return false and no error", func() { + Expect(deleted).To(BeFalse()) + Expect(err).NotTo(HaveOccurred()) + }) + }) + }) + + Context("SyncVMSnapshotTreeStatus", func() { + It("should sync the VM's current and root snapshots status", func() { + Expect(vmProvider.SyncVMSnapshotTreeStatus(ctx, vm)).To(Succeed()) + Expect(vm.Status.CurrentSnapshot).ToNot(BeNil()) + Expect(vm.Status.CurrentSnapshot.Type).To(Equal(vmopv1.VirtualMachineSnapshotReferenceTypeManaged)) + Expect(vm.Status.CurrentSnapshot.Name).To(Equal(vmSnapshot.Name)) + Expect(vm.Status.RootSnapshots).To(HaveLen(1)) + Expect(vm.Status.RootSnapshots[0].Name).To(Equal(vmSnapshot.Name)) + Expect(vm.Status.RootSnapshots[0].Type).To(Equal(vmopv1.VirtualMachineSnapshotReferenceTypeManaged)) + }) + + When("VM is not found", func() { + BeforeEach(func() { + vm.Status.UniqueID = "" + }) + It("should return error", func() { + Expect(vmProvider.SyncVMSnapshotTreeStatus(ctx, vm)).NotTo(Succeed()) + }) + }) + + When("there is no snapshot", func() { + BeforeEach(func() { + Expect(virtualmachine.DeleteSnapshot(virtualmachine.SnapshotArgs{ + VMCtx: vmCtx, + VMSnapshot: *vmSnapshot, + VcVM: vcVM, + })).To(Succeed()) + }) + It("should show expected current snapshot and root snapshots", func() { + Expect(vmProvider.SyncVMSnapshotTreeStatus(ctx, vm)).To(Succeed()) + Expect(vm.Status.CurrentSnapshot).To(BeNil()) + Expect(vm.Status.RootSnapshots).To(BeNil()) + }) + }) + }) + + Context("ReconcileCurrentSnapshot", func() { + var ( + snapshot1 *vmopv1.VirtualMachineSnapshot + snapshot2 *vmopv1.VirtualMachineSnapshot + + verifyK8sVMSnapshot = func(name, namespace string, isCreated bool) { + GinkgoHelper() + vmSnapshot := &vmopv1.VirtualMachineSnapshot{} + Expect(ctx.Client.Get(ctx, ctrlclient.ObjectKey{ + Name: name, + Namespace: namespace, + }, vmSnapshot)).To(Succeed()) + Expect(conditions.IsTrue(vmSnapshot, vmopv1.VirtualMachineSnapshotCreatedCondition)).To(Equal(isCreated)) + } + + verifyNoVcVMSnapshot = func() { + GinkgoHelper() + var moVM mo.VirtualMachine + Expect(vcVM.Properties(ctx, vcVM.Reference(), []string{"snapshot"}, &moVM)).To(Succeed()) + Expect(moVM.Snapshot).To(BeNil()) + } + ) + + BeforeEach(func() { + By("Deleting the snapshot on vSphere created in outer BeforeEach") + Expect(virtualmachine.DeleteSnapshot(virtualmachine.SnapshotArgs{ + VMCtx: vmCtx, + VMSnapshot: *vmSnapshot, + VcVM: vcVM, + })).To(Succeed()) + + By("Deleting the snapshot CR created in outer BeforeEach") + Expect(ctx.Client.Delete(ctx, vmSnapshot)).To(Succeed()) + }) + + AfterEach(func() { + snapshot1 = nil + snapshot2 = nil + }) + + When("no snapshots exist", func() { + It("should complete without error", func() { + Expect(vsphere.ReconcileCurrentSnapshot(vmCtx, ctx.Client, vcVM)).To(Succeed()) + verifyNoVcVMSnapshot() + }) + }) + + When("one snapshot exists and is not created", func() { + JustBeforeEach(func() { + // Create snapshot1 CR with owner reference set to the VM. + snapshot1 = builder.DummyVirtualMachineSnapshot(vm.Namespace, "snapshot-1", vm.Name) + Expect(controllerutil.SetOwnerReference(vm, snapshot1, ctx.Scheme)).To(Succeed()) + Expect(ctx.Client.Create(ctx, snapshot1)).To(Succeed()) + }) + + It("should process the snapshot", func() { + // Reconcile the current snapshot. + Expect(vsphere.ReconcileCurrentSnapshot(vmCtx, ctx.Client, vcVM)).To(Succeed()) + + // Verify snapshot is created. + verifyK8sVMSnapshot(snapshot1.Name, snapshot1.Namespace, true) + + // Verify snapshot status. + updatedSnapshot := &vmopv1.VirtualMachineSnapshot{} + Expect(ctx.Client.Get(ctx, ctrlclient.ObjectKey{ + Name: snapshot1.Name, + Namespace: snapshot1.Namespace, + }, updatedSnapshot)).To(Succeed()) + Expect(updatedSnapshot.Status.Quiesced).To(BeTrue()) + // Snapshot should be powered off since memory is not included in the snapshot. + Expect(updatedSnapshot.Status.PowerState).To(Equal(vmopv1.VirtualMachinePowerStateOff)) + }) + }) + + When("multiple snapshots exist", func() { + It("should process snapshots in order (oldest first)", func() { + // Create snapshot1 CR with owner reference set to the VM. + snapshot1 = builder.DummyVirtualMachineSnapshot(vm.Namespace, "snapshot-1", vm.Name) + creationTimeStamp := metav1.NewTime(time.Now()) + snapshot1.CreationTimestamp = creationTimeStamp + Expect(controllerutil.SetOwnerReference(vm, snapshot1, ctx.Scheme)).To(Succeed()) + Expect(ctx.Client.Create(ctx, snapshot1)).To(Succeed()) + + // Create snapshot2 CR with a later time and owner reference set to the VM. + later := metav1.NewTime(time.Now().Add(1 * time.Second)) + snapshot2 = builder.DummyVirtualMachineSnapshot(vm.Namespace, "snapshot-2", vm.Name) + snapshot2.CreationTimestamp = later + Expect(controllerutil.SetOwnerReference(vm, snapshot2, ctx.Scheme)).To(Succeed()) + Expect(ctx.Client.Create(ctx, snapshot2)).To(Succeed()) + + // First reconcile should process snapshot1, and requeue to process snapshot2. + err := vsphere.ReconcileCurrentSnapshot(vmCtx, ctx.Client, vcVM) + Expect(err).To(HaveOccurred()) + Expect(pkgerr.IsRequeueError(err)).To(BeTrue()) + Expect(err.Error()).To(ContainSubstring("requeuing to process 1 remaining snapshots")) + + // Check that snapshot1 is created. + verifyK8sVMSnapshot(snapshot1.Name, snapshot1.Namespace, true) + + // Check that snapshot2 is NOT created. + verifyK8sVMSnapshot(snapshot2.Name, snapshot2.Namespace, false) + + // Second reconcile should process snapshot2. + Expect(vsphere.ReconcileCurrentSnapshot(vmCtx, ctx.Client, vcVM)).To(Succeed()) + + // Check that snapshot2 is now created. + verifyK8sVMSnapshot(snapshot2.Name, snapshot2.Namespace, true) + + // Note: The Children status is populated by SyncVMSnapshotTreeStatus, + // not by ReconcileCurrentSnapshot, which is tested separately above. + }) + }) + + When("one snapshot is already in progress", func() { + It("should process the in-progress snapshot and requeue for the next", func() { + // Create snapshot1 CR with in progress condition and owner reference set to the VM. + snapshot1 = builder.DummyVirtualMachineSnapshot(vm.Namespace, "snapshot-1", vm.Name) + conditions.MarkFalse(snapshot1, + vmopv1.VirtualMachineSnapshotCreatedCondition, + vmopv1.VirtualMachineSnapshotCreationInProgressReason, + "in progress", + ) + Expect(controllerutil.SetOwnerReference(vm, snapshot1, ctx.Scheme)).To(Succeed()) + Expect(ctx.Client.Create(ctx, snapshot1)).To(Succeed()) + + // Create snapshot2 CR with owner reference set to the VM. + snapshot2 = builder.DummyVirtualMachineSnapshot(vm.Namespace, "snapshot-2", vm.Name) + Expect(controllerutil.SetOwnerReference(vm, snapshot2, ctx.Scheme)).To(Succeed()) + Expect(ctx.Client.Create(ctx, snapshot2)).To(Succeed()) + + // Reconcile the current snapshot and expect a requeue error. + err := vsphere.ReconcileCurrentSnapshot(vmCtx, ctx.Client, vcVM) + Expect(err).To(HaveOccurred()) + Expect(pkgerr.IsRequeueError(err)).To(BeTrue()) + + // First snapshot should be created. + verifyK8sVMSnapshot(snapshot1.Name, snapshot1.Namespace, true) + + // Second snapshot should NOT be created. + verifyK8sVMSnapshot(snapshot2.Name, snapshot2.Namespace, false) + }) + }) + + When("snapshot is being deleted", func() { + It("should skip all snapshot creation due to vSphere constraint", func() { + // Create snapshot1 CR with owner reference set to the VM. + snapshot1 = builder.DummyVirtualMachineSnapshot(vm.Namespace, "snapshot-1", vm.Name) + Expect(controllerutil.SetOwnerReference(vm, snapshot1, ctx.Scheme)).To(Succeed()) + // Set a finalizer so we can delete the snapshot CR without it being removed from cluster. + snapshot1.ObjectMeta.Finalizers = []string{"dummy-finalizer"} + Expect(ctx.Client.Create(ctx, snapshot1)).To(Succeed()) + + // Create snapshot2 CR with owner reference set to the VM. + snapshot2 = builder.DummyVirtualMachineSnapshot(vm.Namespace, "snapshot-2", vm.Name) + Expect(controllerutil.SetOwnerReference(vm, snapshot2, ctx.Scheme)).To(Succeed()) + Expect(ctx.Client.Create(ctx, snapshot2)).To(Succeed()) + + // Delete snapshot1 CR. + Expect(ctx.Client.Delete(ctx, snapshot1)).To(Succeed()) + + // Reconcile the current snapshot and expect a requeue error. + Expect(vsphere.ReconcileCurrentSnapshot(vmCtx, ctx.Client, vcVM)).To(Succeed()) + + // snapshot1 should NOT be created. + verifyK8sVMSnapshot(snapshot1.Name, snapshot1.Namespace, false) + + // snapshot2 should NOT be created. + verifyK8sVMSnapshot(snapshot2.Name, snapshot2.Namespace, false) + }) + }) + + When("snapshot already exists and has created condition", func() { + It("should skip ready snapshot and process the next one", func() { + // Create snapshot1 CR with created condition and owner reference set to the VM. + snapshot1 = builder.DummyVirtualMachineSnapshot(vm.Namespace, "snapshot-1", vm.Name) + conditions.MarkTrue(snapshot1, vmopv1.VirtualMachineSnapshotCreatedCondition) + Expect(controllerutil.SetOwnerReference(vm, snapshot1, ctx.Scheme)).To(Succeed()) + Expect(ctx.Client.Create(ctx, snapshot1)).To(Succeed()) + + // Create snapshot2 CR with owner reference set to the VM. + snapshot2 = builder.DummyVirtualMachineSnapshot(vm.Namespace, "snapshot-2", vm.Name) + Expect(controllerutil.SetOwnerReference(vm, snapshot2, ctx.Scheme)).To(Succeed()) + Expect(ctx.Client.Create(ctx, snapshot2)).To(Succeed()) + + // Reconcile the current snapshot and expect no error. + Expect(vsphere.ReconcileCurrentSnapshot(vmCtx, ctx.Client, vcVM)).To(Succeed()) + + // snapshot1 should remain created. + verifyK8sVMSnapshot(snapshot1.Name, snapshot1.Namespace, true) + + // snapshot2 should be processed and marked as created. + verifyK8sVMSnapshot(snapshot2.Name, snapshot2.Namespace, true) + }) + }) + + When("snapshot has empty VM name", func() { + It("should skip snapshot with empty VM name and process the next one", func() { + // Create snapshot1 CR with spec.VMName set to empty and owner reference set to the VM. + snapshot1 = builder.DummyVirtualMachineSnapshot(vm.Namespace, "snapshot-1", vm.Name) + snapshot1.Spec.VMName = "" + Expect(controllerutil.SetOwnerReference(vm, snapshot1, ctx.Scheme)).To(Succeed()) + Expect(ctx.Client.Create(ctx, snapshot1)).To(Succeed()) + + // Create snapshot2 with owner reference set to the VM. + snapshot2 = builder.DummyVirtualMachineSnapshot(vm.Namespace, "snapshot-2", vm.Name) + Expect(controllerutil.SetOwnerReference(vm, snapshot2, ctx.Scheme)).To(Succeed()) + Expect(ctx.Client.Create(ctx, snapshot2)).To(Succeed()) + + // Reconcile the current snapshot. + Expect(vsphere.ReconcileCurrentSnapshot(vmCtx, ctx.Client, vcVM)).To(Succeed()) + + // snapshot1 should not be processed (empty VMName). + verifyK8sVMSnapshot(snapshot1.Name, snapshot1.Namespace, false) + + // snapshot2 should be processed and marked as created. + verifyK8sVMSnapshot(snapshot2.Name, snapshot2.Namespace, true) + }) + }) + + When("snapshot references different VM", func() { + It("should skip snapshot for different VM and process the next one", func() { + // Create snapshot1 CR with spec.VMName set to a different VM name than owner reference VM. + snapshot1 = builder.DummyVirtualMachineSnapshot(vm.Namespace, "snapshot-1", vm.Name) + snapshot1.Spec.VMName = "different-vm" + Expect(controllerutil.SetOwnerReference(vm, snapshot1, ctx.Scheme)).To(Succeed()) + Expect(ctx.Client.Create(ctx, snapshot1)).To(Succeed()) + + // Create snapshot2 CR with owner reference set to the VM. + snapshot2 = builder.DummyVirtualMachineSnapshot(vm.Namespace, "snapshot-2", vm.Name) + Expect(controllerutil.SetOwnerReference(vm, snapshot2, ctx.Scheme)).To(Succeed()) + Expect(ctx.Client.Create(ctx, snapshot2)).To(Succeed()) + + // Reconcile the current snapshot. + Expect(vsphere.ReconcileCurrentSnapshot(vmCtx, ctx.Client, vcVM)).To(Succeed()) + + // snapshot1 should not be processed (different VM). + verifyK8sVMSnapshot(snapshot1.Name, snapshot1.Namespace, false) + + // snapshot2 should be processed and marked as created. + verifyK8sVMSnapshot(snapshot2.Name, snapshot2.Namespace, true) + }) + }) + + When("VM is a VKS/TKG node", func() { + It("should skip snapshot processing for VKS/TKG nodes", func() { + // Add CAPI labels to mark VM as VKS/TKG node. + vm.Labels = map[string]string{ + kubeutil.CAPWClusterRoleLabelKey: "worker", + } + Expect(ctx.Client.Update(ctx, vm)).To(Succeed()) + + // Create snapshot1 CR with owner reference set to the VM. + snapshot1 = builder.DummyVirtualMachineSnapshot(vm.Namespace, "snapshot-1", vm.Name) + Expect(controllerutil.SetOwnerReference(vm, snapshot1, ctx.Scheme)).To(Succeed()) + Expect(ctx.Client.Create(ctx, snapshot1)).To(Succeed()) + + // Reconcile the current snapshot. + Expect(vsphere.ReconcileCurrentSnapshot(vmCtx, ctx.Client, vcVM)).To(Succeed()) + + // Snapshot should not be processed. + verifyK8sVMSnapshot(snapshot1.Name, snapshot1.Namespace, false) + verifyNoVcVMSnapshot() + }) + }) + + When("disk promotion sync is enabled but not ready", func() { + JustBeforeEach(func() { + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.Features.FastDeploy = true + }) + }) + + It("should create snapshot after disk promotion sync is ready", func() { + // Set the VM's promote disks mode to not disabled and disk promotion sync condition to false. + vm.Spec.PromoteDisksMode = vmopv1.VirtualMachinePromoteDisksModeOnline + conditions.MarkFalse(vm, vmopv1.VirtualMachineDiskPromotionSynced, "", "") + Expect(ctx.Client.Status().Update(ctx, vm)).To(Succeed()) + + // Create a snapshot CR with owner reference set to the VM. + snapshot1 = builder.DummyVirtualMachineSnapshot(vm.Namespace, "snapshot-1", vm.Name) + Expect(controllerutil.SetOwnerReference(vm, snapshot1, ctx.Scheme)).To(Succeed()) + Expect(ctx.Client.Create(ctx, snapshot1)).To(Succeed()) + + // Reconcile the snapshot. + Expect(vsphere.ReconcileCurrentSnapshot(vmCtx, ctx.Client, vcVM)).To(Succeed()) + + // Snapshot should not be processed. + verifyK8sVMSnapshot(snapshot1.Name, snapshot1.Namespace, false) + verifyNoVcVMSnapshot() + + // Update the VM's VirtualMachineDiskPromotionSynced condition to true. + conditions.MarkTrue(vm, vmopv1.VirtualMachineDiskPromotionSynced) + Expect(ctx.Client.Status().Update(ctx, vm)).To(Succeed()) + + // Reconcile the snapshot. + Expect(vsphere.ReconcileCurrentSnapshot(vmCtx, ctx.Client, vcVM)).To(Succeed()) + + // Snapshot should be created. + verifyK8sVMSnapshot(snapshot1.Name, snapshot1.Namespace, true) + }) + }) + + When("AllDisksArePVCs is enabled but disks are not registered", func() { + JustBeforeEach(func() { + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.Features.AllDisksArePVCs = true + }) + }) + + It("should create snapshot after disk registration is ready", func() { + // Set the VM's disk backfill condition to false. + conditions.MarkFalse(vm, vmconfunmanagedvolsfil.Condition, "", "") + Expect(ctx.Client.Status().Update(ctx, vm)).To(Succeed()) + + // Create a snapshot CR with owner reference set to the VM. + snapshot1 = builder.DummyVirtualMachineSnapshot(vm.Namespace, "snapshot-1", vm.Name) + Expect(controllerutil.SetOwnerReference(vm, snapshot1, ctx.Scheme)).To(Succeed()) + Expect(ctx.Client.Create(ctx, snapshot1)).To(Succeed()) + + // Reconcile the snapshot. + Expect(vsphere.ReconcileCurrentSnapshot(vmCtx, ctx.Client, vcVM)).To(Succeed()) + + // Snapshot should not be processed. + verifyK8sVMSnapshot(snapshot1.Name, snapshot1.Namespace, false) + verifyNoVcVMSnapshot() + + // Update the VM's disk backfill condition to true. + conditions.MarkTrue(vm, vmconfunmanagedvolsfil.Condition) + Expect(ctx.Client.Status().Update(ctx, vm)).To(Succeed()) + + // Reconcile the snapshot. + Expect(vsphere.ReconcileCurrentSnapshot(vmCtx, ctx.Client, vcVM)).To(Succeed()) + + // Snapshot should NOT be created (pending disk registration). + verifyK8sVMSnapshot(snapshot1.Name, snapshot1.Namespace, false) + verifyNoVcVMSnapshot() + + // Update the VM's disk registration condition to true. + conditions.MarkTrue(vm, vmconfunmanagedvolsreg.Condition) + Expect(ctx.Client.Status().Update(ctx, vm)).To(Succeed()) + + // Reconcile the snapshot. + Expect(vsphere.ReconcileCurrentSnapshot(vmCtx, ctx.Client, vcVM)).To(Succeed()) + + // Snapshot should be created. + verifyK8sVMSnapshot(snapshot1.Name, snapshot1.Namespace, true) + }) + }) + }) + }) diff --git a/pkg/providers/vsphere/vsphere_suite_test.go b/pkg/providers/vsphere/vsphere_suite_test.go index 351ac052e..658c145ee 100644 --- a/pkg/providers/vsphere/vsphere_suite_test.go +++ b/pkg/providers/vsphere/vsphere_suite_test.go @@ -5,200 +5,19 @@ package vsphere_test import ( - "context" - "errors" - "fmt" "testing" . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "github.com/vmware/govmomi/object" - "sigs.k8s.io/controller-runtime/pkg/client" - - vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha6" - pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" - ctxop "github.com/vmware-tanzu/vm-operator/pkg/context/operation" - "github.com/vmware-tanzu/vm-operator/pkg/providers" - "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" "github.com/vmware-tanzu/vm-operator/test/builder" ) var suite = builder.NewTestSuite() -func vcSimTests() { - Describe("CPUFreq", cpuFreqTests) - Describe("ResourcePolicyTests", resourcePolicyTests) - Describe("VAppConfigExpressionProperties", vAppConfigExpressionTests) - Describe("VirtualMachine", vmTests) - Describe("VirtualMachine FastDeploy", fastDeployVMTests) - Describe("VirtualMachineE2E", vmE2ETests) - Describe("VirtualMachineResize", vmResizeTests) - Describe("VirtualMachineUtilsTest", vmUtilTests) - Describe("VirtualMachineGroup", vmGroupTests) - Describe("VirtualMachineSnapshot", vmSnapshotTests) - Describe("VirtualMachineUnmanagedVolumes", unmanagedVolumesTests) - Describe("DefaultGuestIDIfEmpty", defaultGuestIDIfEmptyTests) -} - func TestVSphereProvider(t *testing.T) { - suite.Register(t, "VMProvider Tests", nil, vcSimTests) + suite.Register(t, "VMProvider Tests", nil, nil) } var _ = BeforeSuite(suite.BeforeSuite) var _ = AfterSuite(suite.AfterSuite) - -const ( - createOrUpdateVMMaxAllowedCallCount = 100 -) - -func createOrUpdateVM( - testCtx *builder.TestContextForVCSim, - provider providers.VirtualMachineProviderInterface, - vm *vmopv1.VirtualMachine) error { - - var fn func(ctx context.Context) error - - if pkgcfg.FromContext(testCtx).AsyncSignalEnabled && - pkgcfg.FromContext(testCtx).AsyncCreateEnabled { - - By("non-blocking createOrUpdateVM") - fn = func(ctx context.Context) error { - return createOrUpdateVMAsync(testCtx, provider, vm) - } - } else { - By("blocking createOrUpdateVM") - fn = func(ctx context.Context) error { - return provider.CreateOrUpdateVirtualMachine(ctx, vm) - } - } - - var ( - totalCallCount = 0 - nonErrorCallCount = 0 - ) - - for { - var ( - err error - repeat bool - opctx = ctxop.WithContext(testCtx) - ) - - err = fn(opctx) - - if ctxop.IsUpdate(opctx) { - ctxop.MarkUpdate(testCtx) - } - - if err != nil { - switch { - case errors.Is(err, vsphere.ErrCreate), - errors.Is(err, vsphere.ErrBackup), - errors.Is(err, vsphere.ErrBootstrapCustomize), - errors.Is(err, vsphere.ErrBootstrapReconfigure), - errors.Is(err, vsphere.ErrReconfigure), - errors.Is(err, vsphere.ErrRestart), - errors.Is(err, vsphere.ErrSetPowerState), - errors.Is(err, vsphere.ErrUpgradeHardwareVersion), - errors.Is(err, vsphere.ErrPromoteDisks), - errors.Is(err, vsphere.ErrSnapshotRevert), - errors.Is(err, vsphere.ErrPolicyNotReady), - errors.Is(err, vsphere.ErrUpgradeSchema), - errors.Is(err, vsphere.ErrUpgradeObject): - - repeat = true - default: - GinkgoLogr.Error(err, "createOrUpdateVM fail") - return err - } - } - - if totalCallCount > 100 { - ExpectWithOffset(1, totalCallCount).To( - BeNumerically("<", createOrUpdateVMMaxAllowedCallCount), - "cannot exceed createOrUpdateVMMaxAllowedCallCount for tests") - } - - totalCallCount++ - - if !repeat { - nonErrorCallCount++ - } - - if nonErrorCallCount == 2 { - GinkgoLogr.Info( - "createOrUpdateVM success", - "totalCalls", totalCallCount) - return nil - } - - GinkgoLogr.Info( - "createOrUpdateVM repeat", - "totalCalls", totalCallCount, - "err", err) - } -} - -func createOrUpdateAndGetVcVM( - ctx *builder.TestContextForVCSim, - provider providers.VirtualMachineProviderInterface, - vm *vmopv1.VirtualMachine) (*object.VirtualMachine, error) { - - if err := createOrUpdateVM(ctx, provider, vm); err != nil { - return nil, err - } - - ExpectWithOffset(1, vm.Status.UniqueID).ToNot(BeEmpty()) - vcVM := ctx.GetVMFromMoID(vm.Status.UniqueID) - ExpectWithOffset(1, vcVM).ToNot(BeNil()) - return vcVM, nil -} - -func createOrUpdateVMAsync( - ctx *builder.TestContextForVCSim, - provider providers.VirtualMachineProviderInterface, - vm *vmopv1.VirtualMachine) error { - - GinkgoLogr.Info("entered createOrUpdateVMAsync") - - chanErr, err := provider.CreateOrUpdateVirtualMachineAsync(ctx, vm) - if err != nil { - if errors.Is(err, vsphere.ErrUpgradeSchema) || - errors.Is(err, vsphere.ErrUpgradeObject) { - - ExpectWithOffset(1, ctx.Client.Update( - ctx, - vm)).To(Succeed()) - } - GinkgoLogr.Info("createOrUpdateVMAsync returned", "err", err) - return err - } - - if chanErr != nil { - // Unlike the VM controller, this test helper blocks until the async - // parts of CreateOrUpdateVM are complete. This is to avoid a large - // refactor for now. - for err2 := range chanErr { - if err2 != nil { - GinkgoLogr.Info("createOrUpdateVMAsync chanErr", "err", err2) - if err == nil { - err = err2 - } else { - err = fmt.Errorf("%w,%w", err, err2) - } - } - } - } - - if errors.Is(err, vsphere.ErrCreate) { - ExpectWithOffset(1, ctx.Client.Get( - ctx, - client.ObjectKeyFromObject(vm), - vm)).To(Succeed()) - } - - GinkgoLogr.Info("createOrUpdateVMAsync returned post channel", "err", err) - return err -}