Repository: metal3-io/baremetal-operator
Pull request #734 (2020-12-02 21:01:23)

Add a Makefile target for running manifest validation
This adds a new `manifest-lint` target in the Makefile to run
manifest validation checks via ./hack/manifestlint.sh script.

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/734#" title="Author self-approved">fmuyassarov</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **zaneb**
You can assign the PR to them by writing `/assign @zaneb` 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":["zaneb"]} -->
fmuyassarov: /assign @dhellmann 
/cc @furkatgofurov7 
fmuyassarov: /test-integration
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/734#" title="Author self-approved">fmuyassarov</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **dhellmann**
You can assign the PR to them by writing `/assign @dhellmann` 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":["dhellmann"]} -->
metal3-io-bot: @fmuyassarov: The following test **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
markdownlint | 37c845b0f96c2e7c1dd5c83833678aefd110ad53 | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/734/markdownlint/1334443148394369031) | `/test markdownlint`



<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: @fmuyassarov: The following tests **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
markdownlint | 37c845b0f96c2e7c1dd5c83833678aefd110ad53 | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/734/markdownlint/1334443148394369031) | `/test markdownlint`
golint | 37c845b0f96c2e7c1dd5c83833678aefd110ad53 | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/734/golint/1334443148394369027) | `/test golint`



<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: @fmuyassarov: The following tests **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
markdownlint | 37c845b0f96c2e7c1dd5c83833678aefd110ad53 | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/734/markdownlint/1334443148394369031) | `/test markdownlint`
golint | 37c845b0f96c2e7c1dd5c83833678aefd110ad53 | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/734/golint/1334443148394369027) | `/test golint`
govet | 37c845b0f96c2e7c1dd5c83833678aefd110ad53 | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/734/govet/1334443148394369024) | `/test govet`



<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: @fmuyassarov: The following tests **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
govet | 37c845b0f96c2e7c1dd5c83833678aefd110ad53 | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/734/govet/1334443148394369024) | `/test govet`
golint | 80c6e83e678372527af75501c3729667f18c49a0 | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/734/golint/1334443658014887938) | `/test golint`



<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: @fmuyassarov: The following tests **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
golint | 80c6e83e678372527af75501c3729667f18c49a0 | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/734/golint/1334443658014887938) | `/test golint`
govet | 80c6e83e678372527af75501c3729667f18c49a0 | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/734/govet/1334443658010693633) | `/test govet`



<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 -->
fmuyassarov: Maybe we should not add it to the `lint` target because now I'm seeing both `golint` and `givet` running the same (kubeval) thing.
Also, I'm adding a [trigger](https://github.com/metal3-io/project-infra/pull/167) in the prow configuration for this script. 

If I add `manifet-lint` target as a dependency to the `lint`, then there will be even more repetition of the same checks, because we will have a separate prow check only for the manifest linting + `govet` + `golint`.
dhellmann: > Maybe we should not add it to the `lint` target because now I'm seeing both `golint` and `givet` running the same (kubeval) thing.
> Also, I'm adding a [trigger](https://github.com/metal3-io/project-infra/pull/167) in the prow configuration for this script.
> 
> If I add `manifet-lint` target as a dependency to the `lint`, then there will be even more repetition of the same checks, because we will have a separate prow check only for the manifest linting + `govet` + `golint`.

OK, let's just keep the new `manifest-lint` target by itself for now.

Patch:
diff --git a/Makefile b/Makefile
index 6514de9d..80634709 100644
--- a/Makefile
+++ b/Makefile
@@ -104,6 +104,10 @@ lint: $(GOLANGCI_LINT)
 $(GOLANGCI_LINT):
 	./hack/install-golangci-lint.sh
 
+.PHONY: manifest-lint
+manifest-lint: ## Run manifest validation
+	./hack/manifestlint.sh
+
 ## --------------------------------------
 ## Build/Run Targets
 ## --------------------------------------
diff --git a/apis/metal3.io/v1alpha1/baremetalhost_types.go b/apis/metal3.io/v1alpha1/baremetalhost_types.go
index 6f5d4e19..3633798a 100644
--- a/apis/metal3.io/v1alpha1/baremetalhost_types.go
+++ b/apis/metal3.io/v1alpha1/baremetalhost_types.go
@@ -582,11 +582,11 @@ type ProvisionStatus struct {
 // +k8s:openapi-gen=true
 // +kubebuilder:resource:shortName=bmh;bmhost
 // +kubebuilder:subresource:status
-// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.operationalStatus",description="Operational status",priority=1
-// +kubebuilder:printcolumn:name="Provisioning_Status",type="string",JSONPath=".status.provisioning.state",description="Provisioning status"
+// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.operationalStatus",description="Operational status"
+// +kubebuilder:printcolumn:name="Provisioning Status",type="string",JSONPath=".status.provisioning.state",description="Provisioning status"
 // +kubebuilder:printcolumn:name="Consumer",type="string",JSONPath=".spec.consumerRef.name",description="Consumer using this host"
-// +kubebuilder:printcolumn:name="BMC",type="string",JSONPath=".spec.bmc.address",description="Address of management controller",priority=1
-// +kubebuilder:printcolumn:name="Hardware_Profile",type="string",JSONPath=".status.hardwareProfile",description="The type of hardware detected",priority=1
+// +kubebuilder:printcolumn:name="BMC",type="string",JSONPath=".spec.bmc.address",description="Address of management controller"
+// +kubebuilder:printcolumn:name="Hardware Profile",type="string",JSONPath=".status.hardwareProfile",description="The type of hardware detected"
 // +kubebuilder:printcolumn:name="Online",type="string",JSONPath=".spec.online",description="Whether the host is online or not"
 // +kubebuilder:printcolumn:name="Error",type="string",JSONPath=".status.errorMessage",description="Most recent error"
 // +kubebuilder:object:root=true
@@ -850,31 +850,27 @@ func (host *BareMetalHost) OperationMetricForState(operation ProvisioningState)
 
 // GetImageChecksum returns the hash value and its algo.
 func (host *BareMetalHost) GetImageChecksum() (string, string, bool) {
-	return host.Spec.Image.GetChecksum()
-}
-
-func (image *Image) GetChecksum() (checksum, checksumType string, ok bool) {
-	if image == nil {
-		return
+	if host.Spec.Image == nil {
+		return "", "", false
 	}
 
-	if image.Checksum == "" {
+	checksum := host.Spec.Image.Checksum
+	checksumType := host.Spec.Image.ChecksumType
+
+	if checksum == "" {
 		// Return empty if checksum is not provided
-		return
+		return "", "", false
 	}
-
-	switch image.ChecksumType {
-	case "":
-		checksumType = string(MD5)
+	if checksumType == "" {
+		// If only checksum is specified. Assume type is md5
+		return checksum, string(MD5), true
+	}
+	switch checksumType {
 	case MD5, SHA256, SHA512:
-		checksumType = string(image.ChecksumType)
+		return checksum, string(checksumType), true
 	default:
-		return
+		return "", "", false
 	}
-
-	checksum = image.Checksum
-	ok = true
-	return
 }
 
 // +kubebuilder:object:root=true
diff --git a/config/crd/bases/metal3.io_baremetalhosts.yaml b/config/crd/bases/metal3.io_baremetalhosts.yaml
index 1c294e7b..b2f50182 100644
--- a/config/crd/bases/metal3.io_baremetalhosts.yaml
+++ b/config/crd/bases/metal3.io_baremetalhosts.yaml
@@ -23,11 +23,10 @@ spec:
     - description: Operational status
       jsonPath: .status.operationalStatus
       name: Status
-      priority: 1
       type: string
     - description: Provisioning status
       jsonPath: .status.provisioning.state
-      name: Provisioning_Status
+      name: Provisioning Status
       type: string
     - description: Consumer using this host
       jsonPath: .spec.consumerRef.name
@@ -36,12 +35,10 @@ spec:
     - description: Address of management controller
       jsonPath: .spec.bmc.address
       name: BMC
-      priority: 1
       type: string
     - description: The type of hardware detected
       jsonPath: .status.hardwareProfile
-      name: Hardware_Profile
-      priority: 1
+      name: Hardware Profile
       type: string
     - description: Whether the host is online or not
       jsonPath: .spec.online
diff --git a/config/render/capm3.yaml b/config/render/capm3.yaml
index a7ee6de6..4ef2aba7 100644
--- a/config/render/capm3.yaml
+++ b/config/render/capm3.yaml
@@ -21,11 +21,10 @@ spec:
     - description: Operational status
       jsonPath: .status.operationalStatus
       name: Status
-      priority: 1
       type: string
     - description: Provisioning status
       jsonPath: .status.provisioning.state
-      name: Provisioning_Status
+      name: Provisioning Status
       type: string
     - description: Consumer using this host
       jsonPath: .spec.consumerRef.name
@@ -34,12 +33,10 @@ spec:
     - description: Address of management controller
       jsonPath: .spec.bmc.address
       name: BMC
-      priority: 1
       type: string
     - description: The type of hardware detected
       jsonPath: .status.hardwareProfile
-      name: Hardware_Profile
-      priority: 1
+      name: Hardware Profile
       type: string
     - description: Whether the host is online or not
       jsonPath: .spec.online
diff --git a/controllers/metal3.io/action_result.go b/controllers/metal3.io/action_result.go
index 5f3a9717..11cde63f 100644
--- a/controllers/metal3.io/action_result.go
+++ b/controllers/metal3.io/action_result.go
@@ -1,7 +1,6 @@
 package controllers
 
 import (
-	"errors"
 	"math"
 	"math/rand"
 	"time"
@@ -9,7 +8,6 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 
 	metal3 "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1"
-	"github.com/metal3-io/baremetal-operator/pkg/provisioner"
 )
 
 const maxBackOffCount = 10
@@ -98,10 +96,6 @@ func (r actionError) Dirty() bool {
 	return false
 }
 
-func (r actionError) NeedsRegistration() bool {
-	return errors.Is(r.err, provisioner.NeedsRegistration)
-}
-
 // actionFailed is a result indicating that the current action has failed,
 // and that the resource should be marked as in error.
 type actionFailed struct {
diff --git a/controllers/metal3.io/baremetalhost_controller.go b/controllers/metal3.io/baremetalhost_controller.go
index dfc24360..5aab69c3 100644
--- a/controllers/metal3.io/baremetalhost_controller.go
+++ b/controllers/metal3.io/baremetalhost_controller.go
@@ -181,7 +181,6 @@ func (r *BareMetalHostReconciler) Reconcile(request ctrl.Request) (result ctrl.R
 	// management controller.
 	var bmcCreds *bmc.Credentials
 	var bmcCredsSecret *corev1.Secret
-	haveCreds := false
 	switch host.Status.Provisioning.State {
 	case metal3v1alpha1.StateNone, metal3v1alpha1.StateUnmanaged:
 		bmcCreds = &bmc.Credentials{}
@@ -195,8 +194,6 @@ func (r *BareMetalHostReconciler) Reconcile(request ctrl.Request) (result ctrl.R
 			} else {
 				return r.credentialsErrorResult(err, request, host)
 			}
-		} else {
-			haveCreds = true
 		}
 	}
 
@@ -222,7 +219,7 @@ func (r *BareMetalHostReconciler) Reconcile(request ctrl.Request) (result ctrl.R
 		return ctrl.Result{Requeue: true, RequeueAfter: provisionerNotReadyRetryDelay}, nil
 	}
 
-	stateMachine := newHostStateMachine(host, r, prov, haveCreds)
+	stateMachine := newHostStateMachine(host, r, prov)
 	actResult := stateMachine.ReconcileState(info)
 	result, err = actResult.Result()
 
@@ -431,13 +428,15 @@ func (r *BareMetalHostReconciler) actionRegistering(prov provisioner.Provisioner
 	// so clear any previous error and record the success in the
 	// status block.
 	info.log.Info("updating credentials success status fields")
-	registeredNewCreds := !info.host.Status.GoodCredentials.Match(*info.bmcCredsSecret)
 	info.host.UpdateGoodCredentials(*info.bmcCredsSecret)
 	info.log.Info("clearing previous error message")
 	info.host.ClearError()
 
-	if registeredNewCreds {
-		info.publishEvent("BMCAccessValidated", "Verified access to BMC")
+	info.publishEvent("BMCAccessValidated", "Verified access to BMC")
+
+	if info.host.Spec.ExternallyProvisioned {
+		info.publishEvent("ExternallyProvisioned",
+			"Registered host that was externally provisioned")
 	}
 
 	return actionComplete{}
@@ -563,23 +562,9 @@ func clearHostProvisioningSettings(host *metal3v1alpha1.BareMetalHost) {
 }
 
 func (r *BareMetalHostReconciler) actionDeprovisioning(prov provisioner.Provisioner, info *reconcileInfo) actionResult {
-	// Adopt the host in case it has been re-registered during the
-	// deprovisioning process before it completed
-	provResult, err := prov.Adopt(info.host.Status.ErrorType == metal3v1alpha1.RegistrationError)
-	if err != nil {
-		return actionError{err}
-	}
-	if provResult.ErrorMessage != "" {
-		return recordActionFailure(info, metal3v1alpha1.RegistrationError, provResult.ErrorMessage)
-	}
-	if provResult.Dirty {
-		info.host.ClearError()
-		return actionContinue{provResult.RequeueAfter}
-	}
-
 	info.log.Info("deprovisioning")
 
-	provResult, err = prov.Deprovision()
+	provResult, err := prov.Deprovision()
 	if err != nil {
 		return actionError{errors.Wrap(err, "failed to deprovision")}
 	}
@@ -694,12 +679,14 @@ func (r *BareMetalHostReconciler) manageHostPower(prov provisioner.Provisioner,
 	return steadyStateResult
 }
 
-// A host reaching this action handler should be provisioned or externally
-// provisioned -- a state that it will stay in until the user takes further
-// action. We use the Adopt() API to make sure that the provisioner is aware of
-// the provisioning details. Then we monitor its power status.
+// A host reaching this action handler should be provisioned or
+// externally provisioned -- a state that it will stay in until the
+// user takes further action. Both of those states mean that it has
+// been registered with the provisioner once, so we use the Adopt()
+// API to ensure that is still true. Then we monitor its power status.
 func (r *BareMetalHostReconciler) actionManageSteadyState(prov provisioner.Provisioner, info *reconcileInfo) actionResult {
-	provResult, err := prov.Adopt(info.host.Status.ErrorType == metal3v1alpha1.RegistrationError)
+
+	provResult, err := prov.Adopt()
 	if err != nil {
 		return actionError{err}
 	}
@@ -715,10 +702,28 @@ func (r *BareMetalHostReconciler) actionManageSteadyState(prov provisioner.Provi
 }
 
 // A host reaching this action handler should be ready -- a state that
-// it will stay in until the user takes further action. We don't
+// it will stay in until the user takes further action. It has been
+// registered with the provisioner once, so we use
+// ValidateManagementAccess() to ensure that is still true. We don't
 // use Adopt() because we don't want Ironic to treat the host as
 // having been provisioned. Then we monitor its power status.
 func (r *BareMetalHostReconciler) actionManageReady(prov provisioner.Provisioner, info *reconcileInfo) actionResult {
+
+	// We always pass false for credentialsChanged because if they had
+	// changed we would have ended up in actionRegister() instead of
+	// here.
+	provResult, err := prov.ValidateManagementAccess(false)
+	if err != nil {
+		return actionError{err}
+	}
+	if provResult.ErrorMessage != "" {
+		return recordActionFailure(info, metal3v1alpha1.RegistrationError, provResult.ErrorMessage)
+	}
+	if provResult.Dirty {
+		info.host.ClearError()
+		return actionContinue{provResult.RequeueAfter}
+	}
+
 	if info.host.NeedsProvisioning() {
 		// Ensure the provisioning settings we're going to use are stored.
 		dirty, err := saveHostProvisioningSettings(info.host)
diff --git a/controllers/metal3.io/host_state_machine.go b/controllers/metal3.io/host_state_machine.go
index 1835ca47..3427fa47 100644
--- a/controllers/metal3.io/host_state_machine.go
+++ b/controllers/metal3.io/host_state_machine.go
@@ -16,20 +16,17 @@ type hostStateMachine struct {
 	NextState   metal3v1alpha1.ProvisioningState
 	Reconciler  *BareMetalHostReconciler
 	Provisioner provisioner.Provisioner
-	haveCreds   bool
 }
 
 func newHostStateMachine(host *metal3v1alpha1.BareMetalHost,
 	reconciler *BareMetalHostReconciler,
-	provisioner provisioner.Provisioner,
-	haveCreds bool) *hostStateMachine {
+	provisioner provisioner.Provisioner) *hostStateMachine {
 	currentState := host.Status.Provisioning.State
 	r := hostStateMachine{
 		Host:        host,
 		NextState:   currentState, // Remain in current state by default
 		Reconciler:  reconciler,
 		Provisioner: provisioner,
-		haveCreds:   haveCreds,
 	}
 	return &r
 }
@@ -65,7 +62,7 @@ func recordStateBegin(host *metal3v1alpha1.BareMetalHost, state metal3v1alpha1.P
 
 func recordStateEnd(info *reconcileInfo, host *metal3v1alpha1.BareMetalHost, state metal3v1alpha1.ProvisioningState, time metav1.Time) {
 	if prevMetric := host.OperationMetricForState(state); prevMetric != nil {
-		if !prevMetric.Start.IsZero() && prevMetric.End.IsZero() {
+		if !prevMetric.Start.IsZero() {
 			prevMetric.End = time
 			info.postSaveCallbacks = append(info.postSaveCallbacks, func() {
 				observer := stateTime[state].With(hostMetricLabels(info.request))
@@ -163,48 +160,40 @@ func (hsm *hostStateMachine) checkInitiateDelete() bool {
 }
 
 func (hsm *hostStateMachine) ensureRegistered(info *reconcileInfo) (result actionResult) {
-	if !hsm.haveCreds {
-		// If we are in the process of deletion (which may start with
-		// deprovisioning) and we have been unable to obtain any credentials,
-		// don't attempt to re-register the Host as this will always fail.
-		return
-	}
-
-	needsReregister := false
-
 	switch hsm.NextState {
 	case metal3v1alpha1.StateNone, metal3v1alpha1.StateUnmanaged:
 		// We haven't yet reached the Registration state, so don't attempt
 		// to register the Host.
 		return
-	case metal3v1alpha1.StateDeleting:
-		// In the deleting state the whole idea is to de-register the host
+	}
+	if !hsm.Host.DeletionTimestamp.IsZero() {
+		// BUG(zaneb) We currently don't attempt to re-register the Host
+		// if we find it missing once a delete has been requested (in
+		// part because we are also willing to pass empty credentials
+		// after this point), but this means that we may not be able to
+		// deprovision a Host if the Ironic database pod is rescheduled
+		// during deprovisioning, or if the credentials need to be
+		// modified.
 		return
-	case metal3v1alpha1.StateRegistering:
-	default:
-		needsReregister = (hsm.Host.Status.ErrorType == metal3v1alpha1.RegistrationError ||
-			!hsm.Host.Status.GoodCredentials.Match(*info.bmcCredsSecret))
-		if needsReregister {
-			info.log.Info("Retrying registration")
-			recordStateBegin(hsm.Host, metal3v1alpha1.StateRegistering, metav1.Now())
-		}
 	}
 
-	result = hsm.Reconciler.actionRegistering(hsm.Provisioner, info)
-	if _, complete := result.(actionComplete); complete {
-		if hsm.NextState != metal3v1alpha1.StateRegistering {
-			recordStateEnd(info, hsm.Host, metal3v1alpha1.StateRegistering, metav1.Now())
-		}
-		if needsReregister {
-			// Host was re-registered, so requeue and run the state machine on
-			// the next reconcile
-			result = actionContinue{}
-		} else {
-			// Allow the state machine to run, either because we were just
-			// reconfirming an existing registration, or because we are in the
-			// Registering state
-			result = nil
+	if hsm.Host.Status.GoodCredentials.Match(*info.bmcCredsSecret) {
+		// Credentials are unchanged since we verified them.
+		return
+	}
+
+	recordStateBegin(hsm.Host, metal3v1alpha1.StateRegistering, metav1.Now())
+	if hsm.Host.Status.ErrorType == metal3v1alpha1.RegistrationError {
+		if hsm.Host.Status.TriedCredentials.Match(*info.bmcCredsSecret) {
+			// Already tried with these credentials; no point retrying
+			info.log.Info("Unmodified credentials; not retrying")
+			return actionFailed{ErrorType: metal3v1alpha1.RegistrationError}
 		}
+		info.log.Info("Modified credentials detected; will retry registration")
+	}
+	result = hsm.Reconciler.actionRegistering(hsm.Provisioner, info)
+	if _, complete := result.(actionComplete); complete && hsm.Host.Status.Provisioning.State != metal3v1alpha1.StateRegistering {
+		recordStateEnd(info, hsm.Host, metal3v1alpha1.StateRegistering, metav1.Now())
 	}
 	return
 }
@@ -235,6 +224,7 @@ func (hsm *hostStateMachine) handleRegistering(info *reconcileInfo) actionResult
 	// registered using the current BMC credentials, so we can move to the
 	// next state. We will not return to the Registering state, even
 	// if the credentials change and the Host must be re-registered.
+	hsm.Host.ClearError()
 	if hsm.Host.Spec.ExternallyProvisioned {
 		hsm.NextState = metal3v1alpha1.StateExternallyProvisioned
 	} else {
@@ -333,34 +323,23 @@ func (hsm *hostStateMachine) handleProvisioned(info *reconcileInfo) actionResult
 func (hsm *hostStateMachine) handleDeprovisioning(info *reconcileInfo) actionResult {
 	actResult := hsm.Reconciler.actionDeprovisioning(hsm.Provisioner, info)
 
-	if hsm.Host.DeletionTimestamp.IsZero() {
-		if _, complete := actResult.(actionComplete); complete {
-			hsm.NextState = metal3v1alpha1.StateReady
-		}
-	} else {
-		skipToDelete := func() actionResult {
+	switch actResult.(type) {
+	case actionComplete:
+		if !hsm.Host.DeletionTimestamp.IsZero() {
 			hsm.NextState = metal3v1alpha1.StateDeleting
-			info.postSaveCallbacks = append(info.postSaveCallbacks, deleteWithoutDeprov.Inc)
-			return actionComplete{}
+		} else {
+			hsm.NextState = metal3v1alpha1.StateReady
 		}
-
-		switch r := actResult.(type) {
-		case actionComplete:
-			hsm.NextState = metal3v1alpha1.StateDeleting
-		case actionFailed:
+	case actionFailed:
+		if !hsm.Host.DeletionTimestamp.IsZero() {
 			// If the provisioner gives up deprovisioning and
 			// deletion has been requested, continue to delete.
 			// Note that this is entirely theoretical, as the
 			// Ironic provisioner currently never gives up
 			// trying to deprovision.
-			return skipToDelete()
-		case actionError:
-			if r.NeedsRegistration() && !hsm.haveCreds {
-				// If the host is not registered as a node in Ironic and we
-				// lack the credentials to deprovision it, just continue to
-				// delete.
-				return skipToDelete()
-			}
+			hsm.NextState = metal3v1alpha1.StateDeleting
+			info.postSaveCallbacks = append(info.postSaveCallbacks, deleteWithoutDeprov.Inc)
+			actResult = actionComplete{}
 		}
 	}
 	return actResult
diff --git a/hack/manifestlint.sh b/hack/manifestlint.sh
index 3ecd8218..8698aba3 100755
--- a/hack/manifestlint.sh
+++ b/hack/manifestlint.sh
@@ -20,8 +20,11 @@ CONTAINER_RUNTIME="${CONTAINER_RUNTIME:-podman}"
 # matches our regexp pattern (i.e. kustom, patch). 
 
 if [ "${IS_CONTAINER}" != "false" ]; then
+    { set +x; } 2>/dev/null
+    echo "<-------------------------STARTING MANIFESTS VALIDATION CHECKS------------------------->"
     kubeval --strict --ignore-missing-schemas \
     -d config,examples -i kustom,patch -o tap
+    echo "<-------------------------COMPLETED MANIFESTS VALIDATION CHECKS------------------------>"
 else
   "${CONTAINER_RUNTIME}" run --rm \
     --env IS_CONTAINER=TRUE \
diff --git a/pkg/provisioner/demo/demo.go b/pkg/provisioner/demo/demo.go
index f118dc27..137413b0 100644
--- a/pkg/provisioner/demo/demo.go
+++ b/pkg/provisioner/demo/demo.go
@@ -183,7 +183,7 @@ func (p *demoProvisioner) UpdateHardwareState() (result provisioner.Result, err
 }
 
 // Adopt allows an externally-provisioned server to be adopted.
-func (p *demoProvisioner) Adopt(force bool) (result provisioner.Result, err error) {
+func (p *demoProvisioner) Adopt() (result provisioner.Result, err error) {
 	p.log.Info("adopting host")
 	result.Dirty = false
 	return
diff --git a/pkg/provisioner/fixture/fixture.go b/pkg/provisioner/fixture/fixture.go
index bcd44458..86bc04b7 100644
--- a/pkg/provisioner/fixture/fixture.go
+++ b/pkg/provisioner/fixture/fixture.go
@@ -170,7 +170,7 @@ func (p *fixtureProvisioner) UpdateHardwareState() (result provisioner.Result, e
 }
 
 // Adopt allows an externally-provisioned server to be adopted.
-func (p *fixtureProvisioner) Adopt(force bool) (result provisioner.Result, err error) {
+func (p *fixtureProvisioner) Adopt() (result provisioner.Result, err error) {
 	p.log.Info("adopting host")
 	if p.host.Spec.ExternallyProvisioned && !p.adopted {
 		p.adopted = true
diff --git a/pkg/provisioner/ironic/ironic.go b/pkg/provisioner/ironic/ironic.go
index 14a2a159..2446675a 100644
--- a/pkg/provisioner/ironic/ironic.go
+++ b/pkg/provisioner/ironic/ironic.go
@@ -346,23 +346,11 @@ func (p *ironicProvisioner) ValidateManagementAccess(credentialsChanged bool) (r
 			}
 		}
 
-		// If there is an image to be provisioned, or an image has
-		// previously been provisioned, include those details. Either
-		// case may mean we are re-adopting a host that was already
-		// known but removed/lost because the pod restarted.
-		var imageData *metal3v1alpha1.Image
-		switch {
-		case p.host.Status.Provisioning.Image.URL != "":
-			imageData = &p.host.Status.Provisioning.Image
-		case p.host.Spec.Image != nil && p.host.Spec.Image.URL != "":
-			imageData = p.host.Spec.Image
-		}
-
-		checksum, checksumType, ok := imageData.GetChecksum()
+		checksum, checksumType, ok := p.host.GetImageChecksum()
 
 		if ok {
 			p.log.Info("setting instance info",
-				"image_source", imageData.URL,
+				"image_source", p.host.Spec.Image.URL,
 				"image_os_hash_value", checksum,
 				"image_os_hash_algo", checksumType,
 			)
@@ -371,7 +359,7 @@ func (p *ironicProvisioner) ValidateManagementAccess(credentialsChanged bool) (r
 				nodes.UpdateOperation{
 					Op:    nodes.AddOp,
 					Path:  "/instance_info/image_source",
-					Value: imageData.URL,
+					Value: p.host.Spec.Image.URL,
 				},
 				nodes.UpdateOperation{
 					Op:    nodes.AddOp,
@@ -410,11 +398,11 @@ func (p *ironicProvisioner) ValidateManagementAccess(credentialsChanged bool) (r
 				)
 			}
 
-			if imageData.DiskFormat != nil {
+			if p.host.Spec.Image.DiskFormat != nil {
 				updates = append(updates, nodes.UpdateOperation{
 					Op:    nodes.AddOp,
 					Path:  "/instance_info/image_disk_format",
-					Value: *imageData.DiskFormat,
+					Value: *p.host.Spec.Image.DiskFormat,
 				})
 			}
 			_, err = nodes.Update(p.client, ironicNode.UUID, updates).Extract()
@@ -572,7 +560,7 @@ func (p *ironicProvisioner) InspectHardware() (result provisioner.Result, detail
 		return
 	}
 	if ironicNode == nil {
-		return result, nil, provisioner.NeedsRegistration
+		return result, nil, fmt.Errorf("no ironic node for host")
 	}
 
 	status, err := introspection.GetIntrospectionStatus(p.inspector, ironicNode.UUID).Extract()
@@ -662,7 +650,7 @@ func (p *ironicProvisioner) UpdateHardwareState() (result provisioner.Result, er
 		return result, errors.Wrap(err, "failed to find existing host")
 	}
 	if ironicNode == nil {
-		return result, provisioner.NeedsRegistration
+		return result, fmt.Errorf("no ironic node for host")
 	}
 
 	var discoveredVal bool
@@ -993,11 +981,18 @@ func (p *ironicProvisioner) startProvisioning(ironicNode *nodes.Node, hostConf p
 	)
 	p.publisher("ProvisioningStarted",
 		fmt.Sprintf("Image provisioning started for %s", p.host.Spec.Image.URL))
-	return
+
+	var opts nodes.ProvisionStateOpts
+	if nodes.ProvisionState(ironicNode.ProvisionState) == nodes.DeployFail {
+		opts = nodes.ProvisionStateOpts{Target: nodes.TargetActive}
+	} else {
+		opts = nodes.ProvisionStateOpts{Target: nodes.TargetProvide}
+	}
+	return p.changeNodeProvisionState(ironicNode, opts)
 }
 
 // Adopt allows an externally-provisioned server to be adopted by Ironic.
-func (p *ironicProvisioner) Adopt(force bool) (result provisioner.Result, err error) {
+func (p *ironicProvisioner) Adopt() (result provisioner.Result, err error) {
 	var ironicNode *nodes.Node
 
 	if ironicNode, err = p.findExistingHost(); err != nil {
@@ -1005,12 +1000,18 @@ func (p *ironicProvisioner) Adopt(force bool) (result provisioner.Result, err er
 		return
 	}
 	if ironicNode == nil {
-		err = provisioner.NeedsRegistration
-		return
+		// The node does not exist, but we were called so the
+		// controller thinks that the node existed at one time. That
+		// likely means data loss from restarting the database, so
+		// pass through the validation process to register the node
+		// again. Pass true to indicate that we need to re-test the
+		// credentials, just in case.
+		p.log.Info("re-registering host")
+		return p.ValidateManagementAccess(true)
 	}
 
 	switch nodes.ProvisionState(ironicNode.ProvisionState) {
-	case nodes.Enroll, nodes.Verifying:
+	case nodes.Enroll:
 		err = fmt.Errorf("Invalid state for adopt: %s",
 			ironicNode.ProvisionState)
 	case nodes.Manageable:
@@ -1020,21 +1021,12 @@ func (p *ironicProvisioner) Adopt(force bool) (result provisioner.Result, err er
 				Target: nodes.TargetAdopt,
 			},
 		)
-	case nodes.Adopting:
+	case nodes.Adopting, nodes.Verifying:
 		result.RequeueAfter = provisionRequeueDelay
 		result.Dirty = true
 	case nodes.AdoptFail:
-		if force {
-			return p.changeNodeProvisionState(
-				ironicNode,
-				nodes.ProvisionStateOpts{
-					Target: nodes.TargetAdopt,
-				},
-			)
-		} else {
-			result.ErrorMessage = fmt.Sprintf("Host adoption failed: %s",
-				ironicNode.LastError)
-		}
+		result.ErrorMessage = fmt.Sprintf("Host adoption failed: %s",
+			ironicNode.LastError)
 	case nodes.Active:
 	default:
 	}
@@ -1051,7 +1043,7 @@ func (p *ironicProvisioner) Provision(hostConf provisioner.HostConfigData) (resu
 		return result, errors.Wrap(err, "could not find host to receive image")
 	}
 	if ironicNode == nil {
-		return result, provisioner.NeedsRegistration
+		return result, fmt.Errorf("no ironic node for host")
 	}
 
 	p.log.Info("provisioning image to host", "state", ironicNode.ProvisionState)
@@ -1096,22 +1088,12 @@ func (p *ironicProvisioner) Provision(hostConf provisioner.HostConfigData) (resu
 			return result, nil
 		}
 		p.log.Info("recovering from previous failure")
-		if provResult, err := p.startProvisioning(ironicNode, hostConf); err != nil || provResult.Dirty || provResult.ErrorMessage != "" {
-			return provResult, err
-		}
-
-		return p.changeNodeProvisionState(ironicNode,
-			nodes.ProvisionStateOpts{Target: nodes.TargetActive})
+		return p.startProvisioning(ironicNode, hostConf)
 
 	case nodes.Manageable:
-		return p.changeNodeProvisionState(ironicNode,
-			nodes.ProvisionStateOpts{Target: nodes.TargetProvide})
+		return p.startProvisioning(ironicNode, hostConf)
 
 	case nodes.Available:
-		if provResult, err := p.startProvisioning(ironicNode, hostConf); err != nil || provResult.Dirty || provResult.ErrorMessage != "" {
-			return provResult, err
-		}
-
 		// After it is available, we need to start provisioning by
 		// setting the state to "active".
 		p.log.Info("making host active")
@@ -1224,7 +1206,8 @@ func (p *ironicProvisioner) Deprovision() (result provisioner.Result, err error)
 		return result, errors.Wrap(err, "failed to find existing host")
 	}
 	if ironicNode == nil {
-		return result, provisioner.NeedsRegistration
+		p.log.Info("no node found, already deleted")
+		return result, nil
 	}
 
 	p.log.Info("deprovisioning host",
@@ -1233,40 +1216,56 @@ func (p *ironicProvisioner) Deprovision() (result provisioner.Result, err error)
 		"current", ironicNode.ProvisionState,
 		"target", ironicNode.TargetProvisionState,
 		"deploy step", ironicNode.DeployStep,
-		"instance_info", ironicNode.InstanceInfo,
 	)
 
 	switch nodes.ProvisionState(ironicNode.ProvisionState) {
-	case nodes.Error:
+
+	case nodes.Error, nodes.CleanFail:
+		if !ironicNode.Maintenance {
+			p.log.Info("setting host maintenance flag to force image delete")
+			return p.setMaintenanceFlag(ironicNode, true)
+		}
+		// Once it's in maintenance, we can start the delete process.
 		return p.changeNodeProvisionState(
 			ironicNode,
-			nodes.ProvisionStateOpts{Target: nodes.TargetManage},
+			nodes.ProvisionStateOpts{Target: nodes.TargetDeleted},
 		)
 
-	case nodes.CleanFail:
-		if ironicNode.Maintenance {
-			p.log.Info("clearing maintenance flag")
-			return p.setMaintenanceFlag(ironicNode, false)
-		}
+	case nodes.Available:
+		// Move back to manageable
 		return p.changeNodeProvisionState(
 			ironicNode,
 			nodes.ProvisionStateOpts{Target: nodes.TargetManage},
 		)
 
-	case nodes.Available:
-		p.publisher("DeprovisioningComplete", "Image deprovisioning completed")
+	case nodes.Inspecting:
+		p.log.Info("waiting for inspection to complete")
+		result.Dirty = true
+		result.RequeueAfter = introspectionRequeueDelay
 		return result, nil
 
+	case nodes.InspectWait:
+		p.log.Info("cancelling inspection")
+		return p.changeNodeProvisionState(
+			ironicNode,
+			nodes.ProvisionStateOpts{Target: nodes.TargetAbort},
+		)
+
+	case nodes.InspectFail:
+		p.log.Info("inspection failed or cancelled")
+		return p.changeNodeProvisionState(
+			ironicNode,
+			nodes.ProvisionStateOpts{Target: nodes.TargetManage},
+		)
+
 	case nodes.Deleting:
 		p.log.Info("deleting")
-		// Transitions to Cleaning upon completion
 		result.Dirty = true
 		result.RequeueAfter = deprovisionRequeueDelay
 		return result, nil
 
 	case nodes.Cleaning:
 		p.log.Info("cleaning")
-		// Transitions to Available upon completion
 		result.Dirty = true
 		result.RequeueAfter = deprovisionRequeueDelay
 		return result, nil
@@ -1277,16 +1276,17 @@ func (p *ironicProvisioner) Deprovision() (result provisioner.Result, err error)
 		result.RequeueAfter = deprovisionRequeueDelay
 		return result, nil
 
-	case nodes.Active:
+	case nodes.Manageable, nodes.Enroll, nodes.Verifying:
+		p.publisher("DeprovisioningComplete", "Image deprovisioning completed")
+		return result, nil
+
+	default:
 		p.log.Info("starting deprovisioning")
 		p.publisher("DeprovisioningStarted", "Image deprovisioning started")
 		return p.changeNodeProvisionState(
 			ironicNode,
 			nodes.ProvisionStateOpts{Target: nodes.TargetDeleted},
 		)
-
-	default:
-		return result, fmt.Errorf("Unhandled ironic state %s", ironicNode.ProvisionState)
 	}
 }
 
diff --git a/pkg/provisioner/provisioner.go b/pkg/provisioner/provisioner.go
index 8fe30327..da040f75 100644
--- a/pkg/provisioner/provisioner.go
+++ b/pkg/provisioner/provisioner.go
@@ -1,7 +1,6 @@
 package provisioner
 
 import (
-	"errors"
 	"time"
 
 	metal3v1alpha1 "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1"
@@ -60,7 +59,7 @@ type Provisioner interface {
 
 	// Adopt brings an externally-provisioned host under management by
 	// the provisioner.
-	Adopt(force bool) (result Result, err error)
+	Adopt() (result Result, err error)
 
 	// Provision writes the image from the host spec to the host. It
 	// may be called multiple times, and should return true for its
@@ -101,5 +100,3 @@ type Result struct {
 	// Any error message produced by the provisioner.
 	ErrorMessage string
 }
-
-var NeedsRegistration = errors.New("Host not registered")


Test patch:
diff --git a/controllers/metal3.io/host_state_machine_test.go b/controllers/metal3.io/host_state_machine_test.go
index 7f717658..8df390c0 100644
--- a/controllers/metal3.io/host_state_machine_test.go
+++ b/controllers/metal3.io/host_state_machine_test.go
@@ -20,7 +20,7 @@ func testStateMachine(host *metal3v1alpha1.BareMetalHost) *hostStateMachine {
 	r := newTestReconciler()
 	p, _ := r.ProvisionerFactory(host, bmc.Credentials{},
 		func(reason, message string) {})
-	return newHostStateMachine(host, r, p, true)
+	return newHostStateMachine(host, r, p)
 }
 
 func TestProvisioningCancelled(t *testing.T) {
@@ -205,7 +205,7 @@ func TestErrorCountIncreasedWhenProvisionerFails(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.Scenario, func(t *testing.T) {
 			prov := &mockProvisioner{}
-			hsm := newHostStateMachine(tt.Host, &BareMetalHostReconciler{}, prov, true)
+			hsm := newHostStateMachine(tt.Host, &BareMetalHostReconciler{}, prov)
 			info := makeDefaultReconcileInfo(tt.Host)
 
 			prov.setNextError("some error")
@@ -220,7 +220,7 @@ func TestErrorCountIncreasedWhenProvisionerFails(t *testing.T) {
 func TestErrorCountIncreasedWhenRegistrationFails(t *testing.T) {
 	bmh := host(metal3v1alpha1.StateRegistering).build()
 	prov := &mockProvisioner{}
-	hsm := newHostStateMachine(bmh, &BareMetalHostReconciler{}, prov, true)
+	hsm := newHostStateMachine(bmh, &BareMetalHostReconciler{}, prov)
 	info := makeDefaultReconcileInfo(bmh)
 	bmh.Status.GoodCredentials = metal3v1alpha1.CredentialsStatus{}
 
@@ -265,7 +265,7 @@ func TestErrorCountCleared(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.Scenario, func(t *testing.T) {
 			prov := &mockProvisioner{}
-			hsm := newHostStateMachine(tt.Host, &BareMetalHostReconciler{}, prov, true)
+			hsm := newHostStateMachine(tt.Host, &BareMetalHostReconciler{}, prov)
 			info := makeDefaultReconcileInfo(tt.Host)
 
 			info.host.Status.ErrorCount = 1
@@ -367,7 +367,7 @@ func (m *mockProvisioner) UpdateHardwareState() (result provisioner.Result, err
 	return m.nextResult, err
 }
 
-func (m *mockProvisioner) Adopt(force bool) (result provisioner.Result, err error) {
+func (m *mockProvisioner) Adopt() (result provisioner.Result, err error) {
 	return m.nextResult, err
 }
 
diff --git a/docs/testing.md b/docs/testing.md
index 438f1067..40245947 100644
--- a/docs/testing.md
+++ b/docs/testing.md
@@ -18,7 +18,7 @@ The tests can be run via `make`
 make test
 ```
 
-Run linters test before pushing your commit
+Run linters test before pushing your commit.
 
 ```bash
 make lint
diff --git a/pkg/provisioner/ironic/adopt_test.go b/pkg/provisioner/ironic/adopt_test.go
index e47fb9fd..68610dbb 100644
--- a/pkg/provisioner/ironic/adopt_test.go
+++ b/pkg/provisioner/ironic/adopt_test.go
@@ -23,7 +23,6 @@ func TestAdopt(t *testing.T) {
 		expectedDirty        bool
 		expectedError        bool
 		expectedRequestAfter int
-		force                bool
 	}{
 		{
 			name: "node-in-enroll",
@@ -62,8 +61,8 @@ func TestAdopt(t *testing.T) {
 				UUID:           nodeUUID,
 			}),
 
-			expectedDirty: false,
-			expectedError: true,
+			expectedDirty:        true,
+			expectedRequestAfter: 10,
 		},
 		{
 			name: "node-in-AdoptFail",
@@ -75,17 +74,6 @@ func TestAdopt(t *testing.T) {
 			expectedDirty:        false,
 			expectedRequestAfter: 0,
 		},
-		{
-			name: "node-in-AdoptFail force retry",
-			ironic: testserver.NewIronic(t).WithDefaultResponses().Node(nodes.Node{
-				ProvisionState: string(nodes.AdoptFail),
-				UUID:           nodeUUID,
-			}),
-
-			expectedDirty:        true,
-			expectedRequestAfter: 10,
-			force:                true,
-		},
 	}
 
 	for _, tc := range cases {
@@ -112,7 +100,7 @@ func TestAdopt(t *testing.T) {
 			}
 
 			prov.status.ID = nodeUUID
-			result, err := prov.Adopt(tc.force)
+			result, err := prov.Adopt()
 
 			assert.Equal(t, tc.expectedDirty, result.Dirty)
 			assert.Equal(t, time.Second*time.Duration(tc.expectedRequestAfter), result.RequeueAfter)
diff --git a/pkg/provisioner/ironic/provision_test.go b/pkg/provisioner/ironic/provision_test.go
index d924b9b7..a682adaa 100644
--- a/pkg/provisioner/ironic/provision_test.go
+++ b/pkg/provisioner/ironic/provision_test.go
@@ -39,7 +39,7 @@ func TestProvision(t *testing.T) {
 				ProvisionState: string(nodes.Manageable),
 				UUID:           nodeUUID,
 			}),
-			expectedRequestAfter: 10,
+			expectedRequestAfter: 0,
 			expectedDirty:        true,
 		},
 		{
@@ -48,7 +48,7 @@ func TestProvision(t *testing.T) {
 				ProvisionState: string(nodes.Available),
 				UUID:           nodeUUID,
 			}),
-			expectedRequestAfter: 0,
+			expectedRequestAfter: 10,
 			expectedDirty:        true,
 		},
 		{
@@ -119,31 +119,40 @@ func TestDeprovision(t *testing.T) {
 		expectedRequestAfter int
 	}{
 		{
-			name: "active state",
+			name: "error state",
 			ironic: testserver.NewIronic(t).WithDefaultResponses().Node(nodes.Node{
-				ProvisionState: string(nodes.Active),
+				ProvisionState: string(nodes.Error),
 				UUID:           nodeUUID,
 			}),
-			expectedRequestAfter: 10,
+			expectedRequestAfter: 0,
 			expectedDirty:        true,
 		},
 		{
-			name: "error state",
+			name: "available state",
 			ironic: testserver.NewIronic(t).WithDefaultResponses().Node(nodes.Node{
-				ProvisionState: string(nodes.Error),
+				ProvisionState: string(nodes.Available),
 				UUID:           nodeUUID,
 			}),
 			expectedRequestAfter: 10,
 			expectedDirty:        true,
 		},
 		{
-			name: "available state",
+			name: "inspecting state",
+			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+				ProvisionState: string(nodes.Inspecting),
+				UUID:           nodeUUID,
+			}),
+			expectedRequestAfter: 15,
+			expectedDirty:        true,
+		},
+		{
+			name: "inspectWait state",
 			ironic: testserver.NewIronic(t).WithDefaultResponses().Node(nodes.Node{
-				ProvisionState: string(nodes.Available),
+				ProvisionState: string(nodes.InspectWait),
 				UUID:           nodeUUID,
 			}),
-			expectedRequestAfter: 0,
-			expectedDirty:        false,
+			expectedRequestAfter: 10,
+			expectedDirty:        true,
 		},
 		{
 			name: "deleting state",
@@ -172,6 +181,34 @@ func TestDeprovision(t *testing.T) {
 			expectedRequestAfter: 10,
 			expectedDirty:        true,
 		},
+
+		{
+			name: "Manageable state",
+			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+				ProvisionState: string(nodes.Manageable),
+				UUID:           nodeUUID,
+			}),
+			expectedRequestAfter: 0,
+			expectedDirty:        false,
+		},
+		{
+			name: "Enroll state",
+			ironic: testserver.NewIronic(t).Ready().Node(nodes.Node{
+				ProvisionState: string(nodes.Enroll),
+				UUID:           nodeUUID,
+			}),
+			expectedRequestAfter: 0,
+			expectedDirty:        false,
+		},
+		{
+			name: "Verifying state",
+			ironic: testserver.NewIronic(t).WithDefaultResponses().Node(nodes.Node{
+				ProvisionState: string(nodes.Verifying),
+				UUID:           nodeUUID,
+			}),
+			expectedRequestAfter: 0,
+			expectedDirty:        false,
+		},
 	}
 
 	for _, tc := range cases {
diff --git a/pkg/provisioner/ironic/updatehardwarestate_test.go b/pkg/provisioner/ironic/updatehardwarestate_test.go
index 63944b81..5988064d 100644
--- a/pkg/provisioner/ironic/updatehardwarestate_test.go
+++ b/pkg/provisioner/ironic/updatehardwarestate_test.go
@@ -101,7 +101,7 @@ func TestUpdateHardwareState(t *testing.T) {
 			name:   "not-ironic-node",
 			ironic: testserver.NewIronic(t).Ready().NoNode(nodeUUID).NoNode("myhost"),
 
-			expectedError: "Host not registered",
+			expectedError: "no ironic node for host",
 		},
 	}