diff --git a/images/virtualization-artifact/pkg/builder/vm/option.go b/images/virtualization-artifact/pkg/builder/vm/option.go index 755adf1ed8..5e35f43805 100644 --- a/images/virtualization-artifact/pkg/builder/vm/option.go +++ b/images/virtualization-artifact/pkg/builder/vm/option.go @@ -118,3 +118,12 @@ func WithProvisioningUserData(cloudInit string) Option { UserData: cloudInit, }) } + +func WithRestartApprovalMode(restartApprovalMode v1alpha2.RestartApprovalMode) Option { + return func(vm *v1alpha2.VirtualMachine) { + if vm.Spec.Disruptions == nil { + vm.Spec.Disruptions = &v1alpha2.Disruptions{} + } + vm.Spec.Disruptions.RestartApprovalMode = restartApprovalMode + } +} diff --git a/test/e2e/internal/config/config.go b/test/e2e/internal/config/config.go index 3b385e5645..52e872abe2 100644 --- a/test/e2e/internal/config/config.go +++ b/test/e2e/internal/config/config.go @@ -85,7 +85,6 @@ type TestData struct { DiskResizing string `yaml:"diskResizing"` SizingPolicy string `yaml:"sizingPolicy"` ImageHotplug string `yaml:"imageHotplug"` - VMConfiguration string `yaml:"vmConfiguration"` VMLabelAnnotation string `yaml:"vmLabelAnnotation"` VMMigration string `yaml:"vmMigration"` VMMigrationCancel string `yaml:"vmMigrationCancel"` diff --git a/test/e2e/internal/util/vm.go b/test/e2e/internal/util/vm.go index 063727d8a3..f69780daca 100644 --- a/test/e2e/internal/util/vm.go +++ b/test/e2e/internal/util/vm.go @@ -101,3 +101,68 @@ func StopVirtualMachineFromOS(f *framework.Framework, vm *v1alpha2.VirtualMachin } return err } + +func RebootVirtualMachineBySSH(f *framework.Framework, vm *v1alpha2.VirtualMachine) { + GinkgoHelper() + + _, err := f.SSHCommand(vm.Name, vm.Namespace, "sudo reboot") + Expect(err).NotTo(HaveOccurred()) +} + +func RebootVirtualMachineByVMOP(f *framework.Framework, vm *v1alpha2.VirtualMachine) { + GinkgoHelper() + + vmop := vmopbuilder.New( + vmopbuilder.WithGenerateName("vmop-e2e-reboot-"), + vmopbuilder.WithNamespace(vm.Namespace), + vmopbuilder.WithType(v1alpha2.VMOPTypeRestart), + vmopbuilder.WithVirtualMachine(vm.Name), + ) + err := f.CreateWithDeferredDeletion(context.Background(), vmop) + Expect(err).NotTo(HaveOccurred()) +} + +func UntilVirtualMachineRebooted(key client.ObjectKey, previousRunningTime time.Time, timeout time.Duration) { + Eventually(func() error { + vm := &v1alpha2.VirtualMachine{} + err := framework.GetClients().GenericClient().Get(context.Background(), key, vm) + if err != nil { + return fmt.Errorf("failed to get virtual machine: %w", err) + } + + runningCondition, _ := conditions.GetCondition(vmcondition.TypeRunning, vm.Status.Conditions) + + if runningCondition.LastTransitionTime.Time.After(previousRunningTime) && vm.Status.Phase == v1alpha2.MachineRunning { + return nil + } + + return fmt.Errorf("virtual machine %s is not rebooted", key.Name) + }, timeout, time.Second).Should(Succeed()) +} + +func IsVDAttached(vm *v1alpha2.VirtualMachine, vd *v1alpha2.VirtualDisk) bool { + for _, bd := range vm.Status.BlockDeviceRefs { + if bd.Kind == v1alpha2.DiskDevice && bd.Name == vd.Name && bd.Attached { + return true + } + } + return false +} + +func IsRestartRequired(vm *v1alpha2.VirtualMachine, timeout time.Duration) bool { + GinkgoHelper() + + if vm.Spec.Disruptions.RestartApprovalMode != v1alpha2.Manual { + return false + } + + Eventually(func(g Gomega) { + err := framework.GetClients().GenericClient().Get(context.Background(), client.ObjectKeyFromObject(vm), vm) + g.Expect(err).NotTo(HaveOccurred()) + needRestart, _ := conditions.GetCondition(vmcondition.TypeAwaitingRestartToApplyConfiguration, vm.Status.Conditions) + g.Expect(needRestart.Status).To(Equal(metav1.ConditionTrue)) + g.Expect(vm.Status.RestartAwaitingChanges).NotTo(BeNil()) + }).WithTimeout(timeout).WithPolling(time.Second).Should(Succeed()) + + return true +} diff --git a/test/e2e/legacy/testdata/vm-configuration/base/cfg/cloudinit.yaml b/test/e2e/legacy/testdata/vm-configuration/base/cfg/cloudinit.yaml deleted file mode 100644 index b2372f3279..0000000000 --- a/test/e2e/legacy/testdata/vm-configuration/base/cfg/cloudinit.yaml +++ /dev/null @@ -1,11 +0,0 @@ -#cloud-config -users: - - name: cloud - # passwd: cloud - passwd: $6$rounds=4096$vln/.aPHBOI7BMYR$bBMkqQvuGs5Gyd/1H5DP4m9HjQSy.kgrxpaGEHwkX7KEFV8BS.HZWPitAtZ2Vd8ZqIZRqmlykRCagTgPejt1i. - shell: /bin/bash - sudo: ALL=(ALL) NOPASSWD:ALL - lock_passwd: false - ssh_authorized_keys: - # testcases - - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFxcXHmwaGnJ8scJaEN5RzklBPZpVSic4GdaAsKjQoeA your_email@example.com diff --git a/test/e2e/legacy/testdata/vm-configuration/base/kustomization.yaml b/test/e2e/legacy/testdata/vm-configuration/base/kustomization.yaml deleted file mode 100644 index 8ff06116f3..0000000000 --- a/test/e2e/legacy/testdata/vm-configuration/base/kustomization.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - ./vm.yaml - - ./vd-root.yaml - - ./vd-blank.yaml -configurations: - - transformer.yaml -generatorOptions: - disableNameSuffixHash: true -secretGenerator: - - files: - - userData=cfg/cloudinit.yaml - name: cloud-init - type: provisioning.virtualization.deckhouse.io/cloud-init diff --git a/test/e2e/legacy/testdata/vm-configuration/base/transformer.yaml b/test/e2e/legacy/testdata/vm-configuration/base/transformer.yaml deleted file mode 100644 index 1dc146a3af..0000000000 --- a/test/e2e/legacy/testdata/vm-configuration/base/transformer.yaml +++ /dev/null @@ -1,54 +0,0 @@ -# https://github.com/kubernetes-sigs/kustomize/blob/master/examples/transformerconfigs/README.md#transformer-configurations - -namespace: - - kind: ClusterVirtualImage - path: spec/dataSource/objectRef/namespace -nameReference: - - kind: VirtualImage - version: v1alpha2 # optional - fieldSpecs: - - path: spec/dataSource/objectRef/name - kind: ClusterVirtualImage - - path: spec/dataSource/objectRef/name - kind: VirtualImage - - path: spec/dataSource/objectRef/name - kind: VirtualDisk - - path: spec/blockDeviceRefs/name - kind: VirtualMachine - - kind: ClusterVirtualImage - version: v1alpha2 # optional - fieldSpecs: - - path: spec/dataSource/objectRef/name - kind: ClusterVirtualImage - - path: spec/dataSource/objectRef/name - kind: VirtualImage - - path: spec/dataSource/objectRef/name - kind: VirtualDisk - - path: spec/blockDeviceRefs/name - kind: VirtualMachine - - kind: VirtualDisk - version: v1alpha2 # optional - fieldSpecs: - - path: spec/blockDeviceRefs/name - kind: VirtualMachine - - path: spec/blockDeviceRef/name - kind: VirtualMachineBlockDeviceAttachment - - kind: Secret - fieldSpecs: - - path: spec/provisioning/userDataRef/name - kind: VirtualMachine - - kind: VirtualMachineIPAddress - version: v1alpha2 - fieldSpecs: - - path: spec/virtualMachineIPAddressName - kind: VirtualMachine - - kind: VirtualMachine - version: v1alpha2 - fieldSpecs: - - path: spec/virtualMachineName - kind: VirtualMachineBlockDeviceAttachment - - kind: VirtualMachineClass - version: v1alpha3 - fieldSpecs: - - path: spec/virtualMachineClassName - kind: VirtualMachine diff --git a/test/e2e/legacy/testdata/vm-configuration/base/vd-attach.yaml b/test/e2e/legacy/testdata/vm-configuration/base/vd-attach.yaml deleted file mode 100644 index a68911289e..0000000000 --- a/test/e2e/legacy/testdata/vm-configuration/base/vd-attach.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: virtualization.deckhouse.io/v1alpha2 -kind: VirtualDisk -metadata: - name: vd-attach -spec: - persistentVolumeClaim: - storageClassName: "{{ .STORAGE_CLASS_NAME }}" - size: 100Mi diff --git a/test/e2e/legacy/testdata/vm-configuration/base/vd-blank.yaml b/test/e2e/legacy/testdata/vm-configuration/base/vd-blank.yaml deleted file mode 100644 index fcc4b94ea8..0000000000 --- a/test/e2e/legacy/testdata/vm-configuration/base/vd-blank.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: virtualization.deckhouse.io/v1alpha2 -kind: VirtualDisk -metadata: - name: vd-blank -spec: - persistentVolumeClaim: - storageClassName: "{{ .STORAGE_CLASS_NAME }}" - size: 100Mi diff --git a/test/e2e/legacy/testdata/vm-configuration/base/vd-root.yaml b/test/e2e/legacy/testdata/vm-configuration/base/vd-root.yaml deleted file mode 100644 index 0f98ff3633..0000000000 --- a/test/e2e/legacy/testdata/vm-configuration/base/vd-root.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: virtualization.deckhouse.io/v1alpha2 -kind: VirtualDisk -metadata: - name: vd-root -spec: - persistentVolumeClaim: - storageClassName: "{{ .STORAGE_CLASS_NAME }}" - size: 512Mi - dataSource: - type: ObjectRef - objectRef: - kind: VirtualImage - name: vi-alpine-http diff --git a/test/e2e/legacy/testdata/vm-configuration/base/vm.yaml b/test/e2e/legacy/testdata/vm-configuration/base/vm.yaml deleted file mode 100644 index 6a38cd9ee8..0000000000 --- a/test/e2e/legacy/testdata/vm-configuration/base/vm.yaml +++ /dev/null @@ -1,24 +0,0 @@ -apiVersion: virtualization.deckhouse.io/v1alpha2 -kind: VirtualMachine -metadata: - name: vm -spec: - bootloader: EFI - virtualMachineClassName: generic - cpu: - cores: 1 - coreFraction: 5% - memory: - size: 256Mi - disruptions: - restartApprovalMode: Manual - provisioning: - type: UserDataRef - userDataRef: - kind: Secret - name: cloud-init - blockDeviceRefs: - - kind: VirtualDisk - name: vd-root - - kind: VirtualDisk - name: vd-blank diff --git a/test/e2e/legacy/testdata/vm-configuration/kustomization.yaml b/test/e2e/legacy/testdata/vm-configuration/kustomization.yaml deleted file mode 100644 index e0cfe1dffb..0000000000 --- a/test/e2e/legacy/testdata/vm-configuration/kustomization.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -namespace: testcases -namePrefix: pr-number-or-commit-hash- -resources: - - ns.yaml - - vi - - overlays/automatic - - overlays/manual -configurations: - - transformer.yaml -labels: - - includeSelectors: true - pairs: - id: pr-number-or-commit-hash - testcase: vm-configuration diff --git a/test/e2e/legacy/testdata/vm-configuration/ns.yaml b/test/e2e/legacy/testdata/vm-configuration/ns.yaml deleted file mode 100644 index 5efde875b6..0000000000 --- a/test/e2e/legacy/testdata/vm-configuration/ns.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: default diff --git a/test/e2e/legacy/testdata/vm-configuration/overlays/automatic/kustomization.yaml b/test/e2e/legacy/testdata/vm-configuration/overlays/automatic/kustomization.yaml deleted file mode 100644 index 32ee399c63..0000000000 --- a/test/e2e/legacy/testdata/vm-configuration/overlays/automatic/kustomization.yaml +++ /dev/null @@ -1,24 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -nameSuffix: -automatic-conf -resources: - - ../../base -patches: - - patch: |- - - op: replace - path: /spec/runPolicy - value: AlwaysOn - target: - kind: VirtualMachine - name: vm - - patch: |- - - op: replace - path: /spec/disruptions/restartApprovalMode - value: Automatic - target: - kind: VirtualMachine - name: vm -labels: - - includeSelectors: true - pairs: - vm: automatic-conf diff --git a/test/e2e/legacy/testdata/vm-configuration/overlays/manual/kustomization.yaml b/test/e2e/legacy/testdata/vm-configuration/overlays/manual/kustomization.yaml deleted file mode 100644 index 01ad7addbe..0000000000 --- a/test/e2e/legacy/testdata/vm-configuration/overlays/manual/kustomization.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -nameSuffix: -manual-conf -resources: - - ../../base -patches: - - patch: |- - - op: replace - path: /spec/runPolicy - value: AlwaysOn - target: - kind: VirtualMachine - name: vm -labels: - - includeSelectors: true - pairs: - vm: manual-conf diff --git a/test/e2e/legacy/testdata/vm-configuration/transformer.yaml b/test/e2e/legacy/testdata/vm-configuration/transformer.yaml deleted file mode 100644 index ec70d37fcd..0000000000 --- a/test/e2e/legacy/testdata/vm-configuration/transformer.yaml +++ /dev/null @@ -1,52 +0,0 @@ -namespace: - - kind: ClusterVirtualImage - path: spec/dataSource/objectRef/namespace -nameReference: - - kind: VirtualImage - version: v1alpha2 # optional - fieldSpecs: - - path: spec/dataSource/objectRef/name - kind: ClusterVirtualImage - - path: spec/dataSource/objectRef/name - kind: VirtualImage - - path: spec/dataSource/objectRef/name - kind: VirtualDisk - - path: spec/blockDeviceRefs/name - kind: VirtualMachine - - kind: ClusterVirtualImage - version: v1alpha2 # optional - fieldSpecs: - - path: spec/dataSource/objectRef/name - kind: ClusterVirtualImage - - path: spec/dataSource/objectRef/name - kind: VirtualImage - - path: spec/dataSource/objectRef/name - kind: VirtualDisk - - path: spec/blockDeviceRefs/name - kind: VirtualMachine - - kind: VirtualDisk - version: v1alpha2 # optional - fieldSpecs: - - path: spec/blockDeviceRefs/name - kind: VirtualMachine - - path: spec/blockDeviceRef/name - kind: VirtualMachineBlockDeviceAttachment - - kind: Secret - fieldSpecs: - - path: spec/provisioning/userDataRef/name - kind: VirtualMachine - - kind: VirtualMachineIPAddress - version: v1alpha2 - fieldSpecs: - - path: spec/virtualMachineIPAddressName - kind: VirtualMachine - - kind: VirtualMachine - version: v1alpha2 - fieldSpecs: - - path: spec/virtualMachineName - kind: VirtualMachineBlockDeviceAttachment - - kind: VirtualMachineClass - version: v1alpha3 - fieldSpecs: - - path: spec/virtualMachineClassName - kind: VirtualMachine diff --git a/test/e2e/legacy/testdata/vm-configuration/vi/kustomization.yaml b/test/e2e/legacy/testdata/vm-configuration/vi/kustomization.yaml deleted file mode 100644 index b807d8e2d2..0000000000 --- a/test/e2e/legacy/testdata/vm-configuration/vi/kustomization.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - vi-alpine-http.yaml diff --git a/test/e2e/legacy/testdata/vm-configuration/vi/vi-alpine-http.yaml b/test/e2e/legacy/testdata/vm-configuration/vi/vi-alpine-http.yaml deleted file mode 100644 index abac06b48c..0000000000 --- a/test/e2e/legacy/testdata/vm-configuration/vi/vi-alpine-http.yaml +++ /dev/null @@ -1,11 +0,0 @@ ---- -apiVersion: virtualization.deckhouse.io/v1alpha2 -kind: VirtualImage -metadata: - name: vi-alpine-http -spec: - storage: ContainerRegistry - dataSource: - type: HTTP - http: - url: https://89d64382-20df-4581-8cc7-80df331f67fa.selstorage.ru/alpine/alpine-3-21-uefi-perf.qcow2 diff --git a/test/e2e/legacy/util.go b/test/e2e/legacy/util.go index dc0feaf0eb..dea3f0bac0 100644 --- a/test/e2e/legacy/util.go +++ b/test/e2e/legacy/util.go @@ -831,3 +831,19 @@ func CheckExternalConnection(host, httpCode, vmNamespace string, vmNames ...stri CheckResultSSHCommand(vmNamespace, vmName, cmd, httpCode) } } + +func ExecSSHCommand(vmNamespace, vmName, cmd string) { + GinkgoHelper() + + Eventually(func() error { + res := framework.GetClients().D8Virtualization().SSHCommand(vmName, cmd, d8.SSHOptions{ + Namespace: vmNamespace, + Username: conf.TestData.SSHUser, + IdentityFile: conf.TestData.Sshkey, + }) + if res.Error() != nil { + return fmt.Errorf("cmd: %s\nstderr: %s", res.GetCmd(), res.StdErr()) + } + return nil + }).WithTimeout(Timeout).WithPolling(Interval).ShouldNot(HaveOccurred()) +} diff --git a/test/e2e/legacy/vm_configuration.go b/test/e2e/legacy/vm_configuration.go deleted file mode 100644 index 6e3516149f..0000000000 --- a/test/e2e/legacy/vm_configuration.go +++ /dev/null @@ -1,310 +0,0 @@ -/* -Copyright 2024 Flant JSC - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package legacy - -import ( - "fmt" - "strconv" - "strings" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/deckhouse/virtualization/api/core/v1alpha2" - "github.com/deckhouse/virtualization/test/e2e/internal/config" - d8 "github.com/deckhouse/virtualization/test/e2e/internal/d8" - "github.com/deckhouse/virtualization/test/e2e/internal/framework" - kc "github.com/deckhouse/virtualization/test/e2e/internal/kubectl" -) - -const ( - AutomaticMode = "automatic" - ManualMode = "manual" - StageBefore = "before" - StageAfter = "after" -) - -var _ = Describe("VirtualMachineConfiguration", Ordered, func() { - var ( - testCaseLabel = map[string]string{"testcase": "vm-configuration"} - automaticLabel = map[string]string{"vm": "automatic-conf"} - manualLabel = map[string]string{"vm": "manual-conf"} - ns string - ) - - BeforeAll(func() { - kustomization := fmt.Sprintf("%s/%s", conf.TestData.VMConfiguration, "kustomization.yaml") - var err error - ns, err = kustomize.GetNamespace(kustomization) - Expect(err).NotTo(HaveOccurred(), "%w", err) - - CreateNamespace(ns) - }) - - AfterEach(func() { - if CurrentSpecReport().Failed() { - SaveTestCaseDump(testCaseLabel, CurrentSpecReport().LeafNodeText, ns) - } - }) - - Context("When resources are applied", func() { - It("result should be succeeded", func() { - res := kubectl.Apply(kc.ApplyOptions{ - Filename: []string{conf.TestData.VMConfiguration}, - FilenameOption: kc.Kustomize, - }) - Expect(res.WasSuccess()).To(Equal(true), res.StdErr()) - }) - }) - - Context("When virtual images are applied", func() { - It("checks VIs phases", func() { - By(fmt.Sprintf("VIs should be in %s phases", PhaseReady)) - WaitPhaseByLabel(kc.ResourceVI, PhaseReady, kc.WaitOptions{ - Labels: testCaseLabel, - Namespace: ns, - Timeout: MaxWaitTimeout, - }) - }) - }) - - Context("When virtual disks are applied", func() { - It(fmt.Sprintf("should be in %s phase", PhaseReady), func() { - WaitPhaseByLabel(kc.ResourceVD, PhaseReady, kc.WaitOptions{ - Labels: testCaseLabel, - Namespace: ns, - Timeout: MaxWaitTimeout, - }) - }) - }) - - Context("When virtual machines are applied", func() { - It("should be ready", func() { - WaitVMAgentReady(kc.WaitOptions{ - Labels: testCaseLabel, - Namespace: ns, - Timeout: MaxWaitTimeout, - }) - }) - }) - - Describe(fmt.Sprintf("Manual restart approval mode %d", GinkgoParallelProcess()), func() { - var oldCPUCores int - var newCPUCores int - - Context("When virtual machine agents are ready", func() { - It("changes the number of processor cores", func() { - res := kubectl.List(kc.ResourceVM, kc.GetOptions{ - Labels: manualLabel, - Namespace: ns, - Output: "jsonpath='{.items[*].metadata.name}'", - }) - Expect(res.Error()).NotTo(HaveOccurred(), res.StdErr()) - - vmNames := strings.Split(res.StdOut(), " ") - Expect(vmNames).NotTo(BeEmpty()) - - vmResource := v1alpha2.VirtualMachine{} - err := GetObject(kc.ResourceVM, vmNames[0], &vmResource, kc.GetOptions{Namespace: ns}) - Expect(err).NotTo(HaveOccurred()) - - oldCPUCores = vmResource.Spec.CPU.Cores - newCPUCores = 1 + (vmResource.Spec.CPU.Cores & 1) - - CheckCPUCoresNumber(ManualMode, StageBefore, oldCPUCores, ns, vmNames...) - ChangeCPUCoresNumber(newCPUCores, ns, vmNames...) - }) - }) - - Context("When virtual machine is patched", func() { - It("checks the number of processor cores in specification", func() { - res := kubectl.List(kc.ResourceVM, kc.GetOptions{ - Labels: manualLabel, - Namespace: ns, - Output: "jsonpath='{.items[*].metadata.name}'", - }) - Expect(res.WasSuccess()).To(Equal(true), res.StdErr()) - - vmNames := strings.Split(res.StdOut(), " ") - CheckCPUCoresNumber(ManualMode, StageAfter, newCPUCores, ns, vmNames...) - }) - }) - - Context("When virtual machine is restarted", func() { - It("should be ready", func() { - res := kubectl.List(kc.ResourceVM, kc.GetOptions{ - Labels: manualLabel, - Namespace: ns, - Output: "jsonpath='{.items[*].metadata.name}'", - }) - Expect(res.WasSuccess()).To(Equal(true), res.StdErr()) - - vmNames := strings.Split(res.StdOut(), " ") - for _, vmName := range vmNames { - cmd := "sudo nohup reboot -f > /dev/null 2>&1 &" - ExecSSHCommand(ns, vmName, cmd) - } - WaitVMAgentReady(kc.WaitOptions{ - Labels: manualLabel, - Namespace: ns, - Timeout: MaxWaitTimeout, - }) - }) - }) - - Context("When virtual machine agents are ready", func() { - It("checks that the number of processor cores was changed", func() { - res := kubectl.List(kc.ResourceVM, kc.GetOptions{ - Labels: manualLabel, - Namespace: ns, - Output: "jsonpath='{.items[*].metadata.name}'", - }) - Expect(res.WasSuccess()).To(Equal(true), res.StdErr()) - - vmNames := strings.Split(res.StdOut(), " ") - CheckCPUCoresNumberFromVirtualMachine(strconv.FormatInt(int64(newCPUCores), 10), ns, vmNames...) - }) - }) - }) - - Describe(fmt.Sprintf("Automatic restart approval mode %d", GinkgoParallelProcess()), func() { - var oldCPUCores int - var newCPUCores int - - Context(fmt.Sprintf("When virtual machine is in %s phase", PhaseRunning), func() { - It("changes the number of processor cores", func() { - res := kubectl.List(kc.ResourceVM, kc.GetOptions{ - Labels: automaticLabel, - Namespace: ns, - Output: "jsonpath='{.items[*].metadata.name}'", - }) - Expect(res.WasSuccess()).To(Equal(true), res.StdErr()) - - vmNames := strings.Split(res.StdOut(), " ") - Expect(vmNames).NotTo(BeEmpty()) - - vmResource := v1alpha2.VirtualMachine{} - err := GetObject(kc.ResourceVM, vmNames[0], &vmResource, kc.GetOptions{Namespace: ns}) - Expect(err).NotTo(HaveOccurred(), "%v", err) - - oldCPUCores = vmResource.Spec.CPU.Cores - newCPUCores = 1 + (vmResource.Spec.CPU.Cores & 1) - - CheckCPUCoresNumber(AutomaticMode, StageBefore, oldCPUCores, ns, vmNames...) - ChangeCPUCoresNumber(newCPUCores, ns, vmNames...) - }) - }) - - Context("When virtual machine is patched", func() { - It("checks the number of processor cores in specification", func() { - res := kubectl.List(kc.ResourceVM, kc.GetOptions{ - Labels: automaticLabel, - Namespace: ns, - Output: "jsonpath='{.items[*].metadata.name}'", - }) - Expect(res.WasSuccess()).To(Equal(true), res.StdErr()) - - vmNames := strings.Split(res.StdOut(), " ") - CheckCPUCoresNumber(AutomaticMode, StageAfter, newCPUCores, ns, vmNames...) - }) - }) - - Context("When virtual machine is restarted", func() { - It("should be ready", func() { - WaitVMAgentReady(kc.WaitOptions{ - Labels: automaticLabel, - Namespace: ns, - Timeout: MaxWaitTimeout, - }) - }) - }) - - Context("When virtual machine agents are ready", func() { - It("checks that the number of processor cores was changed", func() { - res := kubectl.List(kc.ResourceVM, kc.GetOptions{ - Labels: automaticLabel, - Namespace: ns, - Output: "jsonpath='{.items[*].metadata.name}'", - }) - Expect(res.Error()).NotTo(HaveOccurred(), res.StdErr()) - - vmNames := strings.Split(res.StdOut(), " ") - CheckCPUCoresNumberFromVirtualMachine(strconv.FormatInt(int64(newCPUCores), 10), ns, vmNames...) - }) - }) - }) - - Context("When test is completed", func() { - It("deletes test case resources", func() { - var resourcesToDelete ResourcesToDelete - - if config.IsCleanUpNeeded() { - resourcesToDelete.KustomizationDir = conf.TestData.VMConfiguration - } - - DeleteTestCaseResources(ns, resourcesToDelete) - }) - }) -}) - -func ExecSSHCommand(vmNamespace, vmName, cmd string) { - GinkgoHelper() - - Eventually(func() error { - res := framework.GetClients().D8Virtualization().SSHCommand(vmName, cmd, d8.SSHOptions{ - Namespace: vmNamespace, - Username: conf.TestData.SSHUser, - IdentityFile: conf.TestData.Sshkey, - }) - if res.Error() != nil { - return fmt.Errorf("cmd: %s\nstderr: %s", res.GetCmd(), res.StdErr()) - } - return nil - }).WithTimeout(Timeout).WithPolling(Interval).ShouldNot(HaveOccurred()) -} - -func ChangeCPUCoresNumber(cpuNumber int, vmNamespace string, vmNames ...string) { - vms := strings.Join(vmNames, " ") - cmd := fmt.Sprintf("patch %s --namespace %s %s --type merge --patch '{\"spec\":{\"cpu\":{\"cores\":%d}}}'", kc.ResourceVM, vmNamespace, vms, cpuNumber) - By("Patching virtual machine specification") - patchRes := kubectl.RawCommand(cmd, ShortWaitDuration) - Expect(patchRes.Error()).NotTo(HaveOccurred(), patchRes.StdErr()) -} - -func CheckCPUCoresNumber(approvalMode, stage string, requiredValue int, vmNamespace string, vmNames ...string) { - for _, vmName := range vmNames { - By(fmt.Sprintf("Checking the number of processor cores %s changing", stage)) - vmResource := v1alpha2.VirtualMachine{} - err := GetObject(kc.ResourceVM, vmName, &vmResource, kc.GetOptions{Namespace: vmNamespace}) - Expect(err).NotTo(HaveOccurred(), "%v", err) - Expect(vmResource.Spec.CPU.Cores).To(Equal(requiredValue)) - switch { - case approvalMode == ManualMode && stage == StageAfter: - Expect(vmResource.Status.RestartAwaitingChanges).ShouldNot(BeNil()) - case approvalMode == AutomaticMode && stage == StageAfter: - Expect(vmResource.Status.RestartAwaitingChanges).ShouldNot(BeNil()) - } - } -} - -func CheckCPUCoresNumberFromVirtualMachine(requiredValue, vmNamespace string, vmNames ...string) { - By("Checking the number of processor cores after changing from virtual machine") - for _, vmName := range vmNames { - cmd := "nproc --all" - CheckResultSSHCommand(vmNamespace, vmName, cmd, requiredValue) - } -} diff --git a/test/e2e/vm/configuration.go b/test/e2e/vm/configuration.go new file mode 100644 index 0000000000..c18bcd2b18 --- /dev/null +++ b/test/e2e/vm/configuration.go @@ -0,0 +1,156 @@ +/* +Copyright 2025 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package vm + +import ( + "context" + "fmt" + "strings" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/utils/ptr" + crclient "sigs.k8s.io/controller-runtime/pkg/client" + + vdbuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vd" + vmbuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vm" + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition" + "github.com/deckhouse/virtualization/test/e2e/internal/framework" + "github.com/deckhouse/virtualization/test/e2e/internal/object" + "github.com/deckhouse/virtualization/test/e2e/internal/util" +) + +const ( + initialCPUCores = 1 + initialMemorySize = "256Mi" + initialCoreFraction = "5%" + changedCPUCores = 2 + changedMemorySize = "512Mi" + changedCoreFraction = "10%" +) + +var _ = Describe("VirtualMachineConfiguration", func() { + DescribeTable("the configuration should be applied", func(restartApprovalMode v1alpha2.RestartApprovalMode) { + f := framework.NewFramework(fmt.Sprintf("vm-configuration-%s", strings.ToLower(string(restartApprovalMode)))) + t := NewConfigurationTest(f) + + DeferCleanup(f.After) + f.Before() + + By("Environment preparation") + t.GenerateResources(restartApprovalMode) + err := f.CreateWithDeferredDeletion(context.Background(), t.VM, t.VDRoot, t.VDBlank) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for VM agent to be ready") + util.UntilVMAgentReady(crclient.ObjectKeyFromObject(t.VM), framework.LongTimeout) + + By("Checking initial configuration") + err = f.Clients.GenericClient().Get(context.Background(), crclient.ObjectKeyFromObject(t.VM), t.VM) + Expect(err).NotTo(HaveOccurred()) + Expect(t.VM.Status.Resources.CPU.Cores).To(Equal(initialCPUCores)) + Expect(t.VM.Status.Resources.Memory.Size).To(Equal(resource.MustParse(initialMemorySize))) + Expect(t.VM.Status.Resources.CPU.CoreFraction).To(Equal(initialCoreFraction)) + Expect(util.IsVDAttached(t.VM, t.VDBlank)).To(BeFalse()) + + By("Applying changes") + err = f.Clients.GenericClient().Get(context.Background(), crclient.ObjectKeyFromObject(t.VM), t.VM) + Expect(err).NotTo(HaveOccurred()) + runningCondition, _ := conditions.GetCondition(vmcondition.TypeRunning, t.VM.Status.Conditions) + previousRunningTime := runningCondition.LastTransitionTime.Time + + t.VM.Spec.CPU.Cores = changedCPUCores + t.VM.Spec.Memory.Size = resource.MustParse(changedMemorySize) + t.VM.Spec.CPU.CoreFraction = changedCoreFraction + t.VM.Spec.BlockDeviceRefs = append(t.VM.Spec.BlockDeviceRefs, v1alpha2.BlockDeviceSpecRef{ + Kind: v1alpha2.DiskDevice, + Name: t.VDBlank.Name, + }) + err = f.Clients.GenericClient().Update(context.Background(), t.VM) + Expect(err).NotTo(HaveOccurred()) + + if util.IsRestartRequired(t.VM, 3*time.Second) { + util.RebootVirtualMachineBySSH(f, t.VM) + } + + By("Waiting for VM to be rebooted") + util.UntilVirtualMachineRebooted(crclient.ObjectKeyFromObject(t.VM), previousRunningTime, framework.LongTimeout) + util.UntilVMAgentReady(crclient.ObjectKeyFromObject(t.VM), framework.ShortTimeout) + + By("Checking changed configuration") + err = f.Clients.GenericClient().Get(context.Background(), crclient.ObjectKeyFromObject(t.VM), t.VM) + Expect(err).NotTo(HaveOccurred()) + Expect(t.VM.Status.Resources.CPU.Cores).To(Equal(changedCPUCores)) + Expect(t.VM.Status.Resources.Memory.Size).To(Equal(resource.MustParse(changedMemorySize))) + Expect(t.VM.Status.Resources.CPU.CoreFraction).To(Equal(changedCoreFraction)) + Expect(util.IsVDAttached(t.VM, t.VDBlank)).To(BeTrue()) + }, + Entry("when changes are applied manually", v1alpha2.Manual), + Entry("when changes are applied automatically", v1alpha2.Automatic), + ) +}) + +type configurationTest struct { + Framework *framework.Framework + + VM *v1alpha2.VirtualMachine + VDRoot *v1alpha2.VirtualDisk + VDBlank *v1alpha2.VirtualDisk +} + +func NewConfigurationTest(f *framework.Framework) *configurationTest { + return &configurationTest{ + Framework: f, + } +} + +func (t *configurationTest) GenerateResources(restartApprovalMode v1alpha2.RestartApprovalMode) { + t.VDRoot = vdbuilder.New( + vdbuilder.WithName("vd-root"), + vdbuilder.WithNamespace(t.Framework.Namespace().Name), + vdbuilder.WithDataSourceHTTP(&v1alpha2.DataSourceHTTP{ + URL: object.ImageURLUbuntu, + }), + ) + + t.VDBlank = vdbuilder.New( + vdbuilder.WithName("vd-blank"), + vdbuilder.WithNamespace(t.Framework.Namespace().Name), + vdbuilder.WithSize(ptr.To(resource.MustParse("100Mi"))), + ) + + t.VM = vmbuilder.New( + vmbuilder.WithName("vm"), + vmbuilder.WithNamespace(t.Framework.Namespace().Name), + vmbuilder.WithCPU(1, ptr.To(initialCoreFraction)), + vmbuilder.WithMemory(resource.MustParse(initialMemorySize)), + vmbuilder.WithLiveMigrationPolicy(v1alpha2.AlwaysSafeMigrationPolicy), + vmbuilder.WithVirtualMachineClass(object.DefaultVMClass), + vmbuilder.WithProvisioningUserData(object.DefaultCloudInit), + vmbuilder.WithBlockDeviceRefs( + v1alpha2.BlockDeviceSpecRef{ + Kind: v1alpha2.DiskDevice, + Name: t.VDRoot.Name, + }, + ), + vmbuilder.WithRestartApprovalMode(restartApprovalMode), + ) +}