diff --git a/api/v1alpha1/proxmoxmachine_types.go b/api/v1alpha1/proxmoxmachine_types.go index 39245590..d2fd7119 100644 --- a/api/v1alpha1/proxmoxmachine_types.go +++ b/api/v1alpha1/proxmoxmachine_types.go @@ -50,6 +50,16 @@ const ( IPV6Format = "v6" ) +// ProxmoxMachineChecks defines possibibles checks to skip. +type ProxmoxMachineChecks struct { + // Skip checking CloudInit which can be very useful for specific Operating Systems like TalOS + // +optional + SkipCloudInitStatus *bool `json:"skipCloudInitStatus,omitempty"` + // Skip checking QEMU Agent readiness which can be very useful for specific Operating Systems like TalOS + // +optional + SkipQemuGuestAgent *bool `json:"skipQemuGuestAgent,omitempty"` +} + // ProxmoxMachineSpec defines the desired state of a ProxmoxMachine. type ProxmoxMachineSpec struct { VirtualMachineCloneSpec `json:",inline"` @@ -90,6 +100,8 @@ type ProxmoxMachineSpec struct { // Network is the network configuration for this machine's VM. // +optional Network *NetworkSpec `json:"network,omitempty"` + + Checks *ProxmoxMachineChecks `json:"checks,omitempty"` } // Storage is the physical storage on the node. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index bff863e6..fa1493f9 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -539,6 +539,31 @@ func (in *ProxmoxMachine) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProxmoxMachineChecks) DeepCopyInto(out *ProxmoxMachineChecks) { + *out = *in + if in.SkipCloudInitStatus != nil { + in, out := &in.SkipCloudInitStatus, &out.SkipCloudInitStatus + *out = new(bool) + **out = **in + } + if in.SkipQemuGuestAgent != nil { + in, out := &in.SkipQemuGuestAgent, &out.SkipQemuGuestAgent + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProxmoxMachineChecks. +func (in *ProxmoxMachineChecks) DeepCopy() *ProxmoxMachineChecks { + if in == nil { + return nil + } + out := new(ProxmoxMachineChecks) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ProxmoxMachineList) DeepCopyInto(out *ProxmoxMachineList) { *out = *in @@ -595,6 +620,11 @@ func (in *ProxmoxMachineSpec) DeepCopyInto(out *ProxmoxMachineSpec) { *out = new(NetworkSpec) (*in).DeepCopyInto(*out) } + if in.Checks != nil { + in, out := &in.Checks, &out.Checks + *out = new(ProxmoxMachineChecks) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProxmoxMachineSpec. diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxclusters.yaml index 9a52305b..6bd32e5a 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxclusters.yaml @@ -72,6 +72,20 @@ spec: description: ProxmoxMachineSpec defines the desired state of a ProxmoxMachine. properties: + checks: + description: ProxmoxMachineChecks defines possibibles checks + to skip. + properties: + skipCloudInitStatus: + description: Skip checking CloudInit which can be very + useful for specific Operating Systems like TalOS + type: boolean + skipQemuGuestAgent: + description: Skip checking QEMU Agent readiness which + can be very useful for specific Operating Systems + like TalOS + type: boolean + type: object description: description: Description for the new VM. type: string diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxclustertemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxclustertemplates.yaml index 84c0047a..d70e0f27 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxclustertemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxclustertemplates.yaml @@ -94,6 +94,21 @@ spec: description: ProxmoxMachineSpec defines the desired state of a ProxmoxMachine. properties: + checks: + description: ProxmoxMachineChecks defines possibibles + checks to skip. + properties: + skipCloudInitStatus: + description: Skip checking CloudInit which can + be very useful for specific Operating Systems + like TalOS + type: boolean + skipQemuGuestAgent: + description: Skip checking QEMU Agent readiness + which can be very useful for specific Operating + Systems like TalOS + type: boolean + type: object description: description: Description for the new VM. type: string diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxmachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxmachines.yaml index 915d49e8..a8c06f61 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxmachines.yaml @@ -65,6 +65,18 @@ spec: spec: description: ProxmoxMachineSpec defines the desired state of a ProxmoxMachine. properties: + checks: + description: ProxmoxMachineChecks defines possibibles checks to skip. + properties: + skipCloudInitStatus: + description: Skip checking CloudInit which can be very useful + for specific Operating Systems like TalOS + type: boolean + skipQemuGuestAgent: + description: Skip checking QEMU Agent readiness which can be very + useful for specific Operating Systems like TalOS + type: boolean + type: object description: description: Description for the new VM. type: string diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxmachinetemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxmachinetemplates.yaml index 118e0325..f8967773 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxmachinetemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxmachinetemplates.yaml @@ -76,6 +76,20 @@ spec: description: ProxmoxMachineSpec defines the desired state of a ProxmoxMachine. properties: + checks: + description: ProxmoxMachineChecks defines possibibles checks + to skip. + properties: + skipCloudInitStatus: + description: Skip checking CloudInit which can be very + useful for specific Operating Systems like TalOS + type: boolean + skipQemuGuestAgent: + description: Skip checking QEMU Agent readiness which + can be very useful for specific Operating Systems like + TalOS + type: boolean + type: object description: description: Description for the new VM. type: string diff --git a/internal/service/vmservice/vm.go b/internal/service/vmservice/vm.go index 7c72474c..a68aeb5b 100644 --- a/internal/service/vmservice/vm.go +++ b/internal/service/vmservice/vm.go @@ -114,16 +114,24 @@ func checkCloudInitStatus(ctx context.Context, machineScope *scope.MachineScope) return true, nil } - if running, err := machineScope.InfraCluster.ProxmoxClient.CloudInitStatus(ctx, machineScope.VirtualMachine); err != nil || running { - if running { - return true, nil + if !machineScope.SkipQemuGuestCheck() { + if err := machineScope.InfraCluster.ProxmoxClient.QemuAgentStatus(ctx, machineScope.VirtualMachine); err != nil { + return true, errors.Wrap(err, "error waiting for agent") } - if errors.Is(goproxmox.ErrCloudInitFailed, err) { - conditions.MarkFalse(machineScope.ProxmoxMachine, infrav1alpha1.VMProvisionedCondition, infrav1alpha1.VMProvisionFailedReason, clusterv1.ConditionSeverityError, err.Error()) - machineScope.SetFailureMessage(err) - machineScope.SetFailureReason(capierrors.MachineStatusError("BootstrapFailed")) + } + + if !machineScope.SkipCloudInitCheck() { + if running, err := machineScope.InfraCluster.ProxmoxClient.CloudInitStatus(ctx, machineScope.VirtualMachine); err != nil || running { + if running { + return true, nil + } + if errors.Is(goproxmox.ErrCloudInitFailed, err) { + conditions.MarkFalse(machineScope.ProxmoxMachine, infrav1alpha1.VMProvisionedCondition, infrav1alpha1.VMProvisionFailedReason, clusterv1.ConditionSeverityError, err.Error()) + machineScope.SetFailureMessage(err) + machineScope.SetFailureReason(capierrors.MachineStatusError("BootstrapFailed")) + } + return false, err } - return false, err } return false, nil diff --git a/internal/service/vmservice/vm_test.go b/internal/service/vmservice/vm_test.go index c03fb5c9..686abf98 100644 --- a/internal/service/vmservice/vm_test.go +++ b/internal/service/vmservice/vm_test.go @@ -42,6 +42,67 @@ func TestReconcileVM_EverythingReady(t *testing.T) { proxmoxClient.EXPECT().GetVM(context.Background(), "node1", int64(123)).Return(vm, nil).Once() proxmoxClient.EXPECT().CloudInitStatus(context.Background(), vm).Return(false, nil).Once() + proxmoxClient.EXPECT().QemuAgentStatus(context.Background(), vm).Return(nil).Once() + + result, err := ReconcileVM(context.Background(), machineScope) + require.NoError(t, err) + require.Equal(t, infrav1alpha1.VirtualMachineStateReady, result.State) + require.Equal(t, "10.10.10.10", machineScope.ProxmoxMachine.Status.Addresses[1].Address) +} + +func TestReconcileVM_QemuAgentCheckDisabled(t *testing.T) { + machineScope, proxmoxClient, _ := setupReconcilerTest(t) + vm := newRunningVM() + machineScope.SetVirtualMachineID(int64(vm.VMID)) + machineScope.ProxmoxMachine.Status.IPAddresses = map[string]infrav1alpha1.IPAddress{infrav1alpha1.DefaultNetworkDevice: {IPV4: "10.10.10.10"}} + machineScope.ProxmoxMachine.Status.BootstrapDataProvided = ptr.To(true) + machineScope.ProxmoxMachine.Status.Ready = true + machineScope.ProxmoxMachine.Spec.Checks = &infrav1alpha1.ProxmoxMachineChecks{ + SkipQemuGuestAgent: ptr.To(true), + } + + proxmoxClient.EXPECT().GetVM(context.Background(), "node1", int64(123)).Return(vm, nil).Once() + // proxmoxClient.EXPECT().CloudInitStatus(context.Background(), vm).Return(false, nil).Once() + + result, err := ReconcileVM(context.Background(), machineScope) + require.NoError(t, err) + require.Equal(t, infrav1alpha1.VirtualMachineStateReady, result.State) + require.Equal(t, "10.10.10.10", machineScope.ProxmoxMachine.Status.Addresses[1].Address) +} + +func TestReconcileVM_CloudInitCheckDisabled(t *testing.T) { + machineScope, proxmoxClient, _ := setupReconcilerTest(t) + vm := newRunningVM() + machineScope.SetVirtualMachineID(int64(vm.VMID)) + machineScope.ProxmoxMachine.Status.IPAddresses = map[string]infrav1alpha1.IPAddress{infrav1alpha1.DefaultNetworkDevice: {IPV4: "10.10.10.10"}} + machineScope.ProxmoxMachine.Status.BootstrapDataProvided = ptr.To(true) + machineScope.ProxmoxMachine.Status.Ready = true + machineScope.ProxmoxMachine.Spec.Checks = &infrav1alpha1.ProxmoxMachineChecks{ + SkipCloudInitStatus: ptr.To(true), + } + + proxmoxClient.EXPECT().GetVM(context.Background(), "node1", int64(123)).Return(vm, nil).Once() + proxmoxClient.EXPECT().QemuAgentStatus(context.Background(), vm).Return(nil) + + result, err := ReconcileVM(context.Background(), machineScope) + require.NoError(t, err) + require.Equal(t, infrav1alpha1.VirtualMachineStateReady, result.State) + require.Equal(t, "10.10.10.10", machineScope.ProxmoxMachine.Status.Addresses[1].Address) +} + +func TestReconcileVM_InitCheckDisabled(t *testing.T) { + machineScope, proxmoxClient, _ := setupReconcilerTest(t) + vm := newRunningVM() + machineScope.SetVirtualMachineID(int64(vm.VMID)) + machineScope.ProxmoxMachine.Status.IPAddresses = map[string]infrav1alpha1.IPAddress{infrav1alpha1.DefaultNetworkDevice: {IPV4: "10.10.10.10"}} + machineScope.ProxmoxMachine.Status.BootstrapDataProvided = ptr.To(true) + machineScope.ProxmoxMachine.Status.Ready = true + machineScope.ProxmoxMachine.Spec.Checks = &infrav1alpha1.ProxmoxMachineChecks{ + SkipCloudInitStatus: ptr.To(true), + SkipQemuGuestAgent: ptr.To(true), + } + + proxmoxClient.EXPECT().GetVM(context.Background(), "node1", int64(123)).Return(vm, nil).Once() result, err := ReconcileVM(context.Background(), machineScope) require.NoError(t, err) @@ -323,6 +384,7 @@ func TestReconcileVM_CloudInitFailed(t *testing.T) { proxmoxClient.EXPECT().GetVM(context.Background(), "node1", int64(123)).Return(vm, nil).Once() proxmoxClient.EXPECT().CloudInitStatus(context.Background(), vm).Return(false, goproxmox.ErrCloudInitFailed).Once() + proxmoxClient.EXPECT().QemuAgentStatus(context.Background(), vm).Return(nil).Once() _, err := ReconcileVM(context.Background(), machineScope) require.Error(t, err, "unknown error") @@ -340,6 +402,7 @@ func TestReconcileVM_CloudInitRunning(t *testing.T) { proxmoxClient.EXPECT().GetVM(context.Background(), "node1", int64(123)).Return(vm, nil).Once() proxmoxClient.EXPECT().CloudInitStatus(context.Background(), vm).Return(true, nil).Once() + proxmoxClient.EXPECT().QemuAgentStatus(context.Background(), vm).Return(nil).Once() result, err := ReconcileVM(context.Background(), machineScope) require.NoError(t, err) diff --git a/pkg/proxmox/client.go b/pkg/proxmox/client.go index 76187f6a..4c5f9431 100644 --- a/pkg/proxmox/client.go +++ b/pkg/proxmox/client.go @@ -48,6 +48,7 @@ type Client interface { TagVM(ctx context.Context, vm *proxmox.VirtualMachine, tag string) (*proxmox.Task, error) UnmountCloudInitISO(ctx context.Context, vm *proxmox.VirtualMachine, device string) error - CloudInitStatus(ctx context.Context, vm *proxmox.VirtualMachine) (bool, error) + + QemuAgentStatus(ctx context.Context, vm *proxmox.VirtualMachine) error } diff --git a/pkg/proxmox/goproxmox/api_client.go b/pkg/proxmox/goproxmox/api_client.go index 37502743..e9ecef4a 100644 --- a/pkg/proxmox/goproxmox/api_client.go +++ b/pkg/proxmox/goproxmox/api_client.go @@ -277,7 +277,7 @@ func (c *APIClient) UnmountCloudInitISO(ctx context.Context, vm *proxmox.Virtual // CloudInitStatus returns the cloud-init status of the VM. func (c *APIClient) CloudInitStatus(ctx context.Context, vm *proxmox.VirtualMachine) (running bool, err error) { - if err := vm.WaitForAgent(ctx, 5); err != nil { + if err := c.QemuAgentStatus(ctx, vm); err != nil { return false, errors.Wrap(err, "error waiting for agent") } @@ -300,3 +300,12 @@ func (c *APIClient) CloudInitStatus(ctx context.Context, vm *proxmox.VirtualMach return false, nil } + +// QemuAgentStatus returns the qemu-agent status of the VM. +func (c *APIClient) QemuAgentStatus(ctx context.Context, vm *proxmox.VirtualMachine) error { + if err := vm.WaitForAgent(ctx, 5); err != nil { + return errors.Wrap(err, "error waiting for agent") + } + + return nil +} diff --git a/pkg/proxmox/proxmoxtest/mock_client.go b/pkg/proxmox/proxmoxtest/mock_client.go index 6cb0fe01..8492503f 100644 --- a/pkg/proxmox/proxmoxtest/mock_client.go +++ b/pkg/proxmox/proxmoxtest/mock_client.go @@ -477,6 +477,49 @@ func (_c *MockClient_GetVM_Call) RunAndReturn(run func(context.Context, string, return _c } +// QemuAgentStatus provides a mock function with given fields: ctx, vm +func (_m *MockClient) QemuAgentStatus(ctx context.Context, vm *go_proxmox.VirtualMachine) error { + ret := _m.Called(ctx, vm) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *go_proxmox.VirtualMachine) error); ok { + r0 = rf(ctx, vm) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockClient_QemuAgentStatus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'QemuAgentStatus' +type MockClient_QemuAgentStatus_Call struct { + *mock.Call +} + +// QemuAgentStatus is a helper method to define mock.On call +// - ctx context.Context +// - vm *go_proxmox.VirtualMachine +func (_e *MockClient_Expecter) QemuAgentStatus(ctx interface{}, vm interface{}) *MockClient_QemuAgentStatus_Call { + return &MockClient_QemuAgentStatus_Call{Call: _e.mock.On("QemuAgentStatus", ctx, vm)} +} + +func (_c *MockClient_QemuAgentStatus_Call) Run(run func(ctx context.Context, vm *go_proxmox.VirtualMachine)) *MockClient_QemuAgentStatus_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*go_proxmox.VirtualMachine)) + }) + return _c +} + +func (_c *MockClient_QemuAgentStatus_Call) Return(_a0 error) *MockClient_QemuAgentStatus_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockClient_QemuAgentStatus_Call) RunAndReturn(run func(context.Context, *go_proxmox.VirtualMachine) error) *MockClient_QemuAgentStatus_Call { + _c.Call.Return(run) + return _c +} + // ResizeDisk provides a mock function with given fields: ctx, vm, disk, size func (_m *MockClient) ResizeDisk(ctx context.Context, vm *go_proxmox.VirtualMachine, disk string, size string) error { ret := _m.Called(ctx, vm, disk, size) diff --git a/pkg/scope/machine.go b/pkg/scope/machine.go index 00945b7f..59084ef9 100644 --- a/pkg/scope/machine.go +++ b/pkg/scope/machine.go @@ -243,3 +243,25 @@ func (m *MachineScope) GetBootstrapSecret(ctx context.Context, secret *corev1.Se return m.client.Get(ctx, secretKey, secret) } + +// SkipQemuGuestCheck check whether qemu-agent status check is enabled. +func (m *MachineScope) SkipQemuGuestCheck() bool { + if m.ProxmoxMachine.Spec.Checks != nil { + return ptr.Deref(m.ProxmoxMachine.Spec.Checks.SkipQemuGuestAgent, false) + } + + return false +} + +// SkipCloudInitCheck check whether cloud-init status check is enabled. +func (m *MachineScope) SkipCloudInitCheck() bool { + if m.SkipQemuGuestCheck() { + return true + } + + if m.ProxmoxMachine.Spec.Checks != nil { + return ptr.Deref(m.ProxmoxMachine.Spec.Checks.SkipCloudInitStatus, false) + } + + return false +} diff --git a/pkg/scope/machine_test.go b/pkg/scope/machine_test.go index f13e5801..a4b0247d 100644 --- a/pkg/scope/machine_test.go +++ b/pkg/scope/machine_test.go @@ -122,6 +122,73 @@ func TestMachineScope_HasFailed(t *testing.T) { require.False(t, scope.HasFailed()) } +func TestMachineScope_SkipQemuCheckEnabled(t *testing.T) { + p := infrav1alpha1.ProxmoxMachine{ + Spec: infrav1alpha1.ProxmoxMachineSpec{ + Checks: &infrav1alpha1.ProxmoxMachineChecks{ + SkipCloudInitStatus: ptr.To(true), + }, + }, + } + scope := MachineScope{ + ProxmoxMachine: &p, + } + + require.True(t, scope.SkipCloudInitCheck()) +} + +func TestMachineScope_SkipQemuCheck(t *testing.T) { + p := infrav1alpha1.ProxmoxMachine{ + Spec: infrav1alpha1.ProxmoxMachineSpec{}, + } + scope := MachineScope{ + ProxmoxMachine: &p, + } + + require.False(t, scope.SkipCloudInitCheck()) +} + +func TestMachineScope_SkipCloudInitCheckEnabled(t *testing.T) { + p := infrav1alpha1.ProxmoxMachine{ + Spec: infrav1alpha1.ProxmoxMachineSpec{ + Checks: &infrav1alpha1.ProxmoxMachineChecks{ + SkipCloudInitStatus: ptr.To(true), + }, + }, + } + scope := MachineScope{ + ProxmoxMachine: &p, + } + + require.True(t, scope.SkipCloudInitCheck()) +} + +func TestMachineScope_SkipCloudInit(t *testing.T) { + p := infrav1alpha1.ProxmoxMachine{ + Spec: infrav1alpha1.ProxmoxMachineSpec{}, + } + scope := MachineScope{ + ProxmoxMachine: &p, + } + + require.False(t, scope.SkipQemuGuestCheck()) +} + +func TestMachineScope_SkipQemuDisablesCloudInitCheck(t *testing.T) { + p := infrav1alpha1.ProxmoxMachine{ + Spec: infrav1alpha1.ProxmoxMachineSpec{ + Checks: &infrav1alpha1.ProxmoxMachineChecks{ + SkipQemuGuestAgent: ptr.To(true), + }, + }, + } + scope := MachineScope{ + ProxmoxMachine: &p, + } + + require.True(t, scope.SkipCloudInitCheck()) +} + func TestMachineScope_GetBootstrapSecret(t *testing.T) { client := fake.NewClientBuilder().Build() p := infrav1alpha1.ProxmoxMachine{