Repository: metal3-io/baremetal-operator
Pull request #653 (2020-09-25 04:44:50)

Unit tests for ironic.{Adopt,PowerOn,PowerOff,Provision,Deprovision}
this builds on top of https://github.com/metal3-io/baremetal-operator/pull/649

metal3-io-bot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/metal3-io/baremetal-operator/pull/653#" title="Author self-approved">asalkeld</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **hardys**
You can assign the PR to them by writing `/assign @hardys` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=metal3-io%2Fbaremetal-operator).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/metal3-io/baremetal-operator/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["hardys"]} -->
metal3-io-bot: Hi @asalkeld. Thanks for your PR.

I'm waiting for a [metal3-io](https://github.com/orgs/metal3-io/people) member to verify that this patch is reasonable to test. If it is, they should reply with `/ok-to-test` on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work. Regular contributors should [join the org](https://github.com/orgs/metal3-io/people) to skip this step.

Once the patch is verified, the new status will be reflected by the `ok-to-test` label.

I understand the commands that are listed [here](https://go.k8s.io/bot-commands?repo=metal3-io%2Fbaremetal-operator).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>

zaneb: /ok-to-test
metal3-io-bot: @asalkeld: The following test **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
generate | 4b5222e4545a00fa033f973e406681458516b815 | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/653/generate/1309754518971355136) | `/test generate`



<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
asalkeld: /test generate
metal3-io-bot: @asalkeld: The following test **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
generate | 4b5222e4545a00fa033f973e406681458516b815 | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/653/generate/1310336858688851968) | `/test generate`



<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
metal3-io-bot: @asalkeld: The following test **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
generate | 81700797aa2ab877a005baf00ef105c064d2adee | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/653/generate/1310403548885815299) | `/test generate`



<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
andfasano: /test-integration

Patch:
diff --git a/controllers/metal3.io/host_config_data.go b/controllers/metal3.io/host_config_data.go
index 584a8731..630e03b8 100644
--- a/controllers/metal3.io/host_config_data.go
+++ b/controllers/metal3.io/host_config_data.go
@@ -4,17 +4,13 @@ import (
 	"context"
 	"fmt"
 
-	"github.com/pkg/errors"
-
-	metal3v1alpha1 "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1"
-
 	"github.com/go-logr/logr"
-
+	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
-
 	"k8s.io/apimachinery/pkg/types"
-
 	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	metal3v1alpha1 "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1"
 )
 
 // hostConfigData is an implementation of host configuration data interface.
diff --git a/pkg/provisioner/fixture/fixture.go b/pkg/provisioner/fixture/fixture.go
index e4feb2f4..ae597909 100644
--- a/pkg/provisioner/fixture/fixture.go
+++ b/pkg/provisioner/fixture/fixture.go
@@ -15,6 +15,32 @@ var log = logf.Log.WithName("fixture")
 var deprovisionRequeueDelay = time.Second * 10
 var provisionRequeueDelay = time.Second * 10
 
+type fixtureHostConfigData struct {
+	userData    string
+	networkData string
+	metaData    string
+}
+
+func NewHostConfigData(userData string, networkData string, metaData string) provisioner.HostConfigData {
+	return &fixtureHostConfigData{
+		userData:    userData,
+		networkData: networkData,
+		metaData:    metaData,
+	}
+}
+
+func (cd *fixtureHostConfigData) UserData() (string, error) {
+	return cd.userData, nil
+}
+
+func (cd *fixtureHostConfigData) NetworkData() (string, error) {
+	return cd.networkData, nil
+}
+
+func (cd *fixtureHostConfigData) MetaData() (string, error) {
+	return cd.metaData, nil
+}
+
 // fixtureProvisioner implements the provisioning.fixtureProvisioner interface
 // and uses Ironic to manage the host.
 type fixtureProvisioner struct {
diff --git a/pkg/provisioner/ironic/ironic.go b/pkg/provisioner/ironic/ironic.go
index 4ed4c0d1..b971272e 100644
--- a/pkg/provisioner/ironic/ironic.go
+++ b/pkg/provisioner/ironic/ironic.go
@@ -6,17 +6,14 @@ import (
 	"strings"
 	"time"
 
-	"sigs.k8s.io/yaml"
-
+	"github.com/go-logr/logr"
 	"github.com/gophercloud/gophercloud"
 	"github.com/gophercloud/gophercloud/openstack/baremetal/v1/nodes"
 	"github.com/gophercloud/gophercloud/openstack/baremetal/v1/ports"
 	"github.com/gophercloud/gophercloud/openstack/baremetalintrospection/v1/introspection"
-
 	"github.com/pkg/errors"
-
-	"github.com/go-logr/logr"
 	logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
+	"sigs.k8s.io/yaml"
 
 	metal3v1alpha1 "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1"
 	"github.com/metal3-io/baremetal-operator/pkg/bmc"


Test patch:
diff --git a/pkg/provisioner/ironic/adopt_test.go b/pkg/provisioner/ironic/adopt_test.go
new file mode 100644
index 00000000..3363c181
--- /dev/null
+++ b/pkg/provisioner/ironic/adopt_test.go
@@ -0,0 +1,114 @@
+package ironic
+
+import (
+	"testing"
+	"time"
+
+	"github.com/gophercloud/gophercloud/openstack/baremetal/v1/nodes"
+	"github.com/gophercloud/gophercloud/openstack/baremetalintrospection/v1/introspection"
+	"github.com/stretchr/testify/assert"
+
+	"github.com/metal3-io/baremetal-operator/pkg/bmc"
+	"github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/clients"
+	"github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/testserver"
+)
+
+func TestAdopt(t *testing.T) {
+
+	nodeUUID := "33ce8659-7400-4c68-9535-d10766f07a58"
+	cases := []struct {
+		name   string
+		ironic *testserver.IronicMock
+
+		expectedDirty        bool
+		expectedError        bool
+		expectedRequestAfter int
+	}{
+		{
+			name: "node-in-enroll",
+			ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{
+				ProvisionState: string(nodes.Enroll),
+				UUID:           nodeUUID,
+			}),
+
+			expectedDirty: false,
+			expectedError: true,
+		},
+		{
+			name: "node-in-manageable",
+			ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{
+				ProvisionState: string(nodes.Manageable),
+				UUID:           nodeUUID,
+			}).WithNodeStatesProvision(nodeUUID),
+
+			expectedDirty:        true,
+			expectedRequestAfter: 10,
+		},
+		{
+			name: "node-in-adopting",
+			ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{
+				ProvisionState: string(nodes.Adopting),
+				UUID:           nodeUUID,
+			}),
+
+			expectedDirty:        true,
+			expectedRequestAfter: 10,
+		},
+		{
+			name: "node-in-verifying",
+			ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{
+				ProvisionState: string(nodes.Verifying),
+				UUID:           nodeUUID,
+			}),
+
+			expectedDirty:        true,
+			expectedRequestAfter: 10,
+		},
+		{
+			name: "node-in-AdoptFail",
+			ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{
+				ProvisionState: string(nodes.AdoptFail),
+				UUID:           nodeUUID,
+			}),
+
+			expectedDirty:        false,
+			expectedRequestAfter: 0,
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			if tc.ironic != nil {
+				tc.ironic.Start()
+				defer tc.ironic.Stop()
+			}
+
+			inspector := testserver.NewInspector(t).Ready().WithIntrospection(nodeUUID, introspection.Introspection{
+				Finished: false,
+			})
+			inspector.Start()
+			defer inspector.Stop()
+
+			host := makeHost()
+			publisher := func(reason, message string) {}
+			auth := clients.AuthConfig{Type: clients.NoAuth}
+			prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, publisher,
+				tc.ironic.Endpoint(), auth, inspector.Endpoint(), auth,
+			)
+			if err != nil {
+				t.Fatalf("could not create provisioner: %s", err)
+			}
+
+			prov.status.ID = nodeUUID
+			result, err := prov.Adopt()
+
+			assert.Equal(t, tc.expectedDirty, result.Dirty)
+			assert.Equal(t, time.Second*time.Duration(tc.expectedRequestAfter), result.RequeueAfter)
+			if !tc.expectedError {
+				assert.NoError(t, err)
+			} else {
+				assert.Error(t, err)
+			}
+		})
+	}
+}
diff --git a/pkg/provisioner/ironic/power_test.go b/pkg/provisioner/ironic/power_test.go
new file mode 100644
index 00000000..9b45d8e9
--- /dev/null
+++ b/pkg/provisioner/ironic/power_test.go
@@ -0,0 +1,214 @@
+package ironic
+
+import (
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/gophercloud/gophercloud/openstack/baremetal/v1/nodes"
+	"github.com/gophercloud/gophercloud/openstack/baremetalintrospection/v1/introspection"
+	"github.com/stretchr/testify/assert"
+
+	"github.com/metal3-io/baremetal-operator/pkg/bmc"
+	"github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/clients"
+	"github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/testserver"
+)
+
+func TestPowerOn(t *testing.T) {
+
+	nodeUUID := "33ce8659-7400-4c68-9535-d10766f07a58"
+	cases := []struct {
+		name   string
+		ironic *testserver.IronicMock
+
+		expectedDirty        bool
+		expectedError        bool
+		expectedRequestAfter int
+	}{
+		{
+			name: "node-already-power-on",
+			ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{
+				PowerState: powerOn,
+				UUID:       nodeUUID,
+			}),
+		},
+		{
+			name: "waiting-for-target-power-on",
+			ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{
+				PowerState:       powerOff,
+				TargetPowerState: powerOn,
+				UUID:             nodeUUID,
+			}),
+			expectedDirty:        true,
+			expectedRequestAfter: 10,
+		},
+		{
+			name: "power-on normal",
+			ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{
+				PowerState:           powerOff,
+				TargetPowerState:     powerOff,
+				TargetProvisionState: "",
+				UUID:                 nodeUUID,
+			}).WithNodeStatesPower(nodeUUID, http.StatusAccepted),
+			expectedDirty: true,
+		},
+		{
+			name: "power-on wait for Provisioning state",
+			ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{
+				PowerState:           powerOff,
+				TargetPowerState:     powerOff,
+				TargetProvisionState: string(nodes.TargetDeleted),
+				UUID:                 nodeUUID,
+			}),
+			expectedRequestAfter: 10,
+			expectedDirty:        true,
+		},
+		{
+			name: "power-on wait for locked host",
+			ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{
+				PowerState:           powerOff,
+				TargetPowerState:     powerOff,
+				TargetProvisionState: "",
+				UUID:                 nodeUUID,
+			}).WithNodeStatesPower(nodeUUID, http.StatusConflict),
+			expectedRequestAfter: 10,
+			expectedDirty:        true,
+			expectedError:        true,
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			if tc.ironic != nil {
+				tc.ironic.Start()
+				defer tc.ironic.Stop()
+			}
+
+			inspector := testserver.NewInspector(t).Ready().WithIntrospection(nodeUUID, introspection.Introspection{
+				Finished: false,
+			})
+			inspector.Start()
+			defer inspector.Stop()
+
+			host := makeHost()
+			publisher := func(reason, message string) {}
+			auth := clients.AuthConfig{Type: clients.NoAuth}
+			prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, publisher,
+				tc.ironic.Endpoint(), auth, inspector.Endpoint(), auth,
+			)
+			if err != nil {
+				t.Fatalf("could not create provisioner: %s", err)
+			}
+
+			prov.status.ID = nodeUUID
+			result, err := prov.PowerOn()
+
+			assert.Equal(t, tc.expectedDirty, result.Dirty)
+			assert.Equal(t, time.Second*time.Duration(tc.expectedRequestAfter), result.RequeueAfter)
+			if !tc.expectedError {
+				assert.NoError(t, err)
+			} else {
+				assert.Error(t, err)
+			}
+		})
+	}
+}
+
+func TestPowerOff(t *testing.T) {
+
+	nodeUUID := "33ce8659-7400-4c68-9535-d10766f07a58"
+	cases := []struct {
+		name   string
+		ironic *testserver.IronicMock
+
+		expectedDirty        bool
+		expectedError        bool
+		expectedRequestAfter int
+	}{
+		{
+			name: "node-already-power-off",
+			ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{
+				PowerState: powerOff,
+				UUID:       nodeUUID,
+			}),
+		},
+		{
+			name: "waiting-for-target-power-off",
+			ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{
+				PowerState:       powerOn,
+				TargetPowerState: powerOff,
+				UUID:             nodeUUID,
+			}),
+			expectedDirty:        true,
+			expectedRequestAfter: 10,
+		},
+		{
+			name: "power-off normal",
+			ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{
+				PowerState:           powerOn,
+				TargetPowerState:     powerOn,
+				TargetProvisionState: "",
+				UUID:                 nodeUUID,
+			}).WithNodeStatesPower(nodeUUID, http.StatusAccepted),
+			expectedDirty: true,
+		},
+		{
+			name: "power-off wait for Provisioning state",
+			ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{
+				PowerState:           powerOn,
+				TargetPowerState:     powerOn,
+				TargetProvisionState: string(nodes.TargetDeleted),
+				UUID:                 nodeUUID,
+			}),
+			expectedRequestAfter: 10,
+			expectedDirty:        true,
+		},
+		{
+			name: "power-off wait for locked host",
+			ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{
+				PowerState:           powerOn,
+				TargetPowerState:     powerOn,
+				TargetProvisionState: "",
+				UUID:                 nodeUUID,
+			}).WithNodeStatesPower(nodeUUID, http.StatusConflict),
+			expectedRequestAfter: 10,
+			expectedDirty:        true,
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			if tc.ironic != nil {
+				tc.ironic.Start()
+				defer tc.ironic.Stop()
+			}
+
+			inspector := testserver.NewInspector(t).Ready().WithIntrospection(nodeUUID, introspection.Introspection{
+				Finished: false,
+			})
+			inspector.Start()
+			defer inspector.Stop()
+
+			host := makeHost()
+			publisher := func(reason, message string) {}
+			auth := clients.AuthConfig{Type: clients.NoAuth}
+			prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, publisher,
+				tc.ironic.Endpoint(), auth, inspector.Endpoint(), auth,
+			)
+			if err != nil {
+				t.Fatalf("could not create provisioner: %s", err)
+			}
+
+			prov.status.ID = nodeUUID
+			result, err := prov.PowerOff()
+
+			assert.Equal(t, tc.expectedDirty, result.Dirty)
+			assert.Equal(t, time.Second*time.Duration(tc.expectedRequestAfter), result.RequeueAfter)
+			if !tc.expectedError {
+				assert.NoError(t, err)
+			} else {
+				assert.Error(t, err)
+			}
+		})
+	}
+}
diff --git a/pkg/provisioner/ironic/provision_test.go b/pkg/provisioner/ironic/provision_test.go
new file mode 100644
index 00000000..8aec8625
--- /dev/null
+++ b/pkg/provisioner/ironic/provision_test.go
@@ -0,0 +1,249 @@
+package ironic
+
+import (
+	"testing"
+	"time"
+
+	"github.com/gophercloud/gophercloud/openstack/baremetal/v1/nodes"
+	"github.com/gophercloud/gophercloud/openstack/baremetalintrospection/v1/introspection"
+	"github.com/stretchr/testify/assert"
+
+	"github.com/metal3-io/baremetal-operator/pkg/bmc"
+	"github.com/metal3-io/baremetal-operator/pkg/provisioner/fixture"
+	"github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/clients"
+	"github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/testserver"
+)
+
+func TestProvision(t *testing.T) {
+
+	nodeUUID := "33ce8659-7400-4c68-9535-d10766f07a58"
+	cases := []struct {
+		name                 string
+		ironic               *testserver.IronicMock
+		expectedDirty        bool
+		expectedError        bool
+		expectedRequestAfter int
+	}{
+		{
+			name: "deployFail state",
+			ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{
+				ProvisionState: string(nodes.DeployFail),
+				UUID:           nodeUUID,
+			}).WithNodeValidate(nodeUUID).WithNodeStatesProvision(nodeUUID),
+			expectedRequestAfter: 0,
+			expectedDirty:        true,
+		},
+		{
+			name: "manageable state",
+			ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{
+				ProvisionState: string(nodes.Manageable),
+				UUID:           nodeUUID,
+			}).WithNodeValidate(nodeUUID).WithNodeStatesProvision(nodeUUID),
+			expectedRequestAfter: 0,
+			expectedDirty:        true,
+		},
+		{
+			name: "available state",
+			ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{
+				ProvisionState: string(nodes.Available),
+				UUID:           nodeUUID,
+			}).WithNodeStatesProvision(nodeUUID),
+			expectedRequestAfter: 10,
+			expectedDirty:        true,
+		},
+		{
+			name: "active state",
+			ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{
+				ProvisionState: string(nodes.Active),
+				UUID:           nodeUUID,
+			}),
+			expectedRequestAfter: 10,
+			expectedDirty:        false,
+		},
+		{
+			name: "other state: Cleaning",
+			ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{
+				ProvisionState: string(nodes.Cleaning),
+				UUID:           nodeUUID,
+			}),
+			expectedRequestAfter: 10,
+			expectedDirty:        true,
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			if tc.ironic != nil {
+				tc.ironic.Start()
+				defer tc.ironic.Stop()
+			}
+
+			inspector := testserver.NewInspector(t).Ready().WithIntrospection(nodeUUID, introspection.Introspection{
+				Finished: false,
+			})
+			inspector.Start()
+			defer inspector.Stop()
+
+			host := makeHost()
+			publisher := func(reason, message string) {}
+			auth := clients.AuthConfig{Type: clients.NoAuth}
+			prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, publisher,
+				tc.ironic.Endpoint(), auth, inspector.Endpoint(), auth,
+			)
+			if err != nil {
+				t.Fatalf("could not create provisioner: %s", err)
+			}
+
+			prov.status.ID = nodeUUID
+			result, err := prov.Provision(fixture.NewHostConfigData("testUserData", "test: NetworkData", "test: Meta"))
+
+			assert.Equal(t, tc.expectedDirty, result.Dirty)
+			assert.Equal(t, time.Second*time.Duration(tc.expectedRequestAfter), result.RequeueAfter)
+			if !tc.expectedError {
+				assert.NoError(t, err)
+			} else {
+				assert.Error(t, err)
+			}
+		})
+	}
+}
+
+func TestDeprovision(t *testing.T) {
+
+	nodeUUID := "33ce8659-7400-4c68-9535-d10766f07a58"
+	cases := []struct {
+		name                 string
+		ironic               *testserver.IronicMock
+		expectedDirty        bool
+		expectedError        bool
+		expectedRequestAfter int
+	}{
+		{
+			name: "error state",
+			ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{
+				ProvisionState: string(nodes.Error),
+				UUID:           nodeUUID,
+			}).WithNodeStatesProvision(nodeUUID),
+			expectedRequestAfter: 0,
+			expectedDirty:        true,
+		},
+		{
+			name: "available state",
+			ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{
+				ProvisionState: string(nodes.Available),
+				UUID:           nodeUUID,
+			}).WithNodeStatesProvision(nodeUUID),
+			expectedRequestAfter: 10,
+			expectedDirty:        true,
+		},
+		{
+			name: "inspecting state",
+			ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{
+				ProvisionState: string(nodes.Inspecting),
+				UUID:           nodeUUID,
+			}),
+			expectedRequestAfter: 15,
+			expectedDirty:        true,
+		},
+		{
+			name: "inspectWait state",
+			ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{
+				ProvisionState: string(nodes.InspectWait),
+				UUID:           nodeUUID,
+			}).WithNodeStatesProvision(nodeUUID),
+			expectedRequestAfter: 10,
+			expectedDirty:        true,
+		},
+		{
+			name: "deleting state",
+			ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{
+				ProvisionState: string(nodes.Deleting),
+				UUID:           nodeUUID,
+			}),
+			expectedRequestAfter: 10,
+			expectedDirty:        true,
+		},
+		{
+			name: "cleaning state",
+			ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{
+				ProvisionState: string(nodes.Cleaning),
+				UUID:           nodeUUID,
+			}),
+			expectedRequestAfter: 10,
+			expectedDirty:        true,
+		},
+		{
+			name: "cleanWait state",
+			ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{
+				ProvisionState: string(nodes.CleanWait),
+				UUID:           nodeUUID,
+			}),
+			expectedRequestAfter: 10,
+			expectedDirty:        true,
+		},
+
+		{
+			name: "Manageable state",
+			ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{
+				ProvisionState: string(nodes.Manageable),
+				UUID:           nodeUUID,
+			}),
+			expectedRequestAfter: 0,
+			expectedDirty:        false,
+		},
+		{
+			name: "Enroll state",
+			ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{
+				ProvisionState: string(nodes.Enroll),
+				UUID:           nodeUUID,
+			}),
+			expectedRequestAfter: 0,
+			expectedDirty:        false,
+		},
+		{
+			name: "Verifying state",
+			ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{
+				ProvisionState: string(nodes.Verifying),
+				UUID:           nodeUUID,
+			}),
+			expectedRequestAfter: 0,
+			expectedDirty:        false,
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			if tc.ironic != nil {
+				tc.ironic.Start()
+				defer tc.ironic.Stop()
+			}
+
+			inspector := testserver.NewInspector(t).Ready().WithIntrospection(nodeUUID, introspection.Introspection{
+				Finished: false,
+			})
+			inspector.Start()
+			defer inspector.Stop()
+
+			host := makeHost()
+			publisher := func(reason, message string) {}
+			auth := clients.AuthConfig{Type: clients.NoAuth}
+			prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, publisher,
+				tc.ironic.Endpoint(), auth, inspector.Endpoint(), auth,
+			)
+			if err != nil {
+				t.Fatalf("could not create provisioner: %s", err)
+			}
+
+			prov.status.ID = nodeUUID
+			result, err := prov.Deprovision()
+
+			assert.Equal(t, tc.expectedDirty, result.Dirty)
+			assert.Equal(t, time.Second*time.Duration(tc.expectedRequestAfter), result.RequeueAfter)
+			if !tc.expectedError {
+				assert.NoError(t, err)
+			} else {
+				assert.Error(t, err)
+			}
+		})
+	}
+}
diff --git a/pkg/provisioner/ironic/testserver/ironic.go b/pkg/provisioner/ironic/testserver/ironic.go
index 2cfb982e..37d77bf6 100644
--- a/pkg/provisioner/ironic/testserver/ironic.go
+++ b/pkg/provisioner/ironic/testserver/ironic.go
@@ -134,3 +134,15 @@ func (m *IronicMock) CreateNodes() *IronicMock {
 	})
 	return m
 }
+
+// WithNodeStatesPower configures the server with a valid response for /v1/nodes/<node>/states/power
+func (m *IronicMock) WithNodeStatesPower(nodeUUID string, code int) *IronicMock {
+	m.ResponseWithCode("/v1/nodes/"+nodeUUID+"/states/power", "{}", code)
+	return m
+}
+
+// WithNodeValidate configures the server with a valid response for /v1/nodes/<node>/validate
+func (m *IronicMock) WithNodeValidate(nodeUUID string) *IronicMock {
+	m.ResponseWithCode("/v1/nodes/"+nodeUUID+"/validate", "{}", http.StatusOK)
+	return m
+}