diff --git a/api/core/v1alpha2/vmcondition/condition.go b/api/core/v1alpha2/vmcondition/condition.go index 6abfecbe0c..2320fd5da9 100644 --- a/api/core/v1alpha2/vmcondition/condition.go +++ b/api/core/v1alpha2/vmcondition/condition.go @@ -165,8 +165,10 @@ func (r RunningReason) String() string { } const ( - ReasonVirtualMachineNotRunning RunningReason = "NotRunning" - ReasonVirtualMachineRunning RunningReason = "Running" + ReasonVirtualMachineNotRunning RunningReason = "NotRunning" + ReasonVirtualMachineRunning RunningReason = "Running" + // ReasonNoBootableDeviceFound indicates that a virtual machine is running but no bootable device for an operating system was found. + ReasonNoBootableDeviceFound RunningReason = "NoBootableDevice" ReasonInternalVirtualMachineError RunningReason = "InternalVirtualMachineError" ReasonPodNotStarted RunningReason = "PodNotStarted" ReasonPodTerminating RunningReason = "PodTerminating" diff --git a/build/components/versions.yml b/build/components/versions.yml index 7f016066e7..f79008f9d2 100644 --- a/build/components/versions.yml +++ b/build/components/versions.yml @@ -3,7 +3,7 @@ firmware: libvirt: v10.9.0 edk2: stable202411 core: - 3p-kubevirt: v1.6.2-v12n.36 + 3p-kubevirt: v1.6.2-v12n.37 3p-containerized-data-importer: v1.60.3-v12n.19 distribution: 2.8.3 package: diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go index 370a26d909..bcd0e1ebcf 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go @@ -208,6 +208,17 @@ func (h *LifeCycleHandler) syncRunning(ctx context.Context, vm *v1alpha2.Virtual if vm.Status.Phase == v1alpha2.MachineRunning { cb.Reason(vmcondition.ReasonVirtualMachineRunning).Status(metav1.ConditionTrue) + + for _, c := range kvvmi.Status.Conditions { + if c.Type == "BootFailed" { + cb.Status(conditionStatus(string(c.Status))). + Reason(vmcondition.ReasonNoBootableDeviceFound). + Message(fmt.Sprintf("Among all the virtual machine’s block devices, there is no device from which it can boot. Check OS image is compatible with the chosen bootloader %q.", vm.Spec.Bootloader)) + conditions.SetCondition(cb, &vm.Status.Conditions) + return nil + } + } + conditions.SetCondition(cb, &vm.Status.Conditions) return nil } diff --git a/test/e2e/vm/no_bootable_device.go b/test/e2e/vm/no_bootable_device.go new file mode 100644 index 0000000000..b35403bce4 --- /dev/null +++ b/test/e2e/vm/no_bootable_device.go @@ -0,0 +1,81 @@ +/* +Copyright 2026 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" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + crclient "sigs.k8s.io/controller-runtime/pkg/client" + + 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/precheck" + "github.com/deckhouse/virtualization/test/e2e/internal/util" +) + +var _ = Describe("VirtualMachineNoBootableDevice", Label(precheck.NoPrecheck), func() { + var ( + f *framework.Framework + ctx context.Context + ) + + BeforeEach(func() { + ctx = context.Background() + f = framework.NewFramework("vm-no-bootable-device") + DeferCleanup(f.After) + f.Before() + }) + + It("sets Running condition reason to NoBootableDevice", func() { + By("Generating a blank disk and virtual machine with no bootable devices") + vdBlank := object.NewBlankVD("vd-blank", f.Namespace().Name, nil, ptr.To(resource.MustParse("100Mi"))) + + vm := object.NewMinimalVM("vm-", f.Namespace().Name, + vmbuilder.WithBlockDeviceRefs(v1alpha2.BlockDeviceSpecRef{ + Kind: v1alpha2.DiskDevice, + Name: vdBlank.Name, + }), + ) + + By("Creating resources") + err := f.CreateWithDeferredDeletion(ctx, vdBlank, vm) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for virtual machine to be Running") + util.UntilObjectPhase(ctx, string(v1alpha2.MachineRunning), framework.LongTimeout, vm) + + By("Checking Running condition reason indicates no bootable device") + Eventually(func(g Gomega) { + err = f.GenericClient().Get(ctx, crclient.ObjectKeyFromObject(vm), vm) + g.Expect(err).NotTo(HaveOccurred()) + + runningCondition, found := conditions.GetCondition(vmcondition.TypeRunning, vm.Status.Conditions) + g.Expect(found).To(BeTrue()) + g.Expect(runningCondition.Reason).To(Equal(vmcondition.ReasonNoBootableDeviceFound.String())) + g.Expect(runningCondition.Status).To(Equal(metav1.ConditionTrue)) + }).WithTimeout(framework.LongTimeout).WithPolling(framework.PollingInterval).Should(Succeed()) + }) +})