Repository: metal3-io/baremetal-operator
Pull request #1075 (2022-01-20 16:31:01)

Move from Available to Preparing if HostFirmwareSettings changed
Currently if the host is in the Available state and the HFS settings are changed it doesn't transition to the Preparing state as it would when the BareMetalHost settings are changed. This change fixes that.

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

This pull-request has been approved by:
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **dtantsur** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @dtantsur` 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/main/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":["dtantsur"]} -->
bfournie: /test-v1b1-integration
/test-v1b1-centos-integration
dtantsur: /lgtm
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/1075#pullrequestreview-861967186" title="Approved">zhouhao3</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **dtantsur** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @dtantsur` 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/main/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":["dtantsur"]} -->
metal3-io-bot: New changes are detected. LGTM label has been removed.
bfournie: /test-v1b1-centos-integration
/test-v1b1-integration
bfournie: /hold
bfournie: /hold cancel
/test-v1b1-centos-integration
/test-v1b1-integration
bfournie: /test-v1b1-centos-integration
/test-v1b1-integration
bfournie: /test-v1b1-centos-integration
/test-v1b1-integration
bfournie: /test-v1b1-centos-integration
/test-v1b1-centos-integration
zaneb: /lgtm
bfournie: /test-v1b1-integration 
metal3-io-bot: New changes are detected. LGTM label has been removed.

Patch:
diff --git a/apis/metal3.io/v1alpha1/bmceventsubscription_webhook.go b/apis/metal3.io/v1alpha1/bmceventsubscription_webhook.go
index 9ddb2378..4e7cc579 100644
--- a/apis/metal3.io/v1alpha1/bmceventsubscription_webhook.go
+++ b/apis/metal3.io/v1alpha1/bmceventsubscription_webhook.go
@@ -43,22 +43,9 @@ func (s *BMCEventSubscription) ValidateCreate() error {
 }
 
 // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
-//
-// We prevent updates to the spec.  All other updates (e.g. status, finalizers) are allowed.
 func (s *BMCEventSubscription) ValidateUpdate(old runtime.Object) error {
 	bmcsubscriptionlog.Info("validate update", "name", s.Name)
-
-	bes, casted := old.(*BMCEventSubscription)
-	if !casted {
-		bmcsubscriptionlog.Error(fmt.Errorf("old object conversion error"), "validate update error")
-		return nil
-	}
-
-	if s.Spec != bes.Spec {
-		return fmt.Errorf("subscriptions cannot be updated, please recreate it")
-	}
-
-	return nil
+	return fmt.Errorf("subscriptions cannot be updated, please recreate it")
 }
 
 // ValidateDelete implements webhook.Validator so a webhook will be registered for the type
diff --git a/controllers/metal3.io/baremetalhost_controller.go b/controllers/metal3.io/baremetalhost_controller.go
index 934bf25a..9d5eed0e 100644
--- a/controllers/metal3.io/baremetalhost_controller.go
+++ b/controllers/metal3.io/baremetalhost_controller.go
@@ -951,7 +951,7 @@ func (r *BareMetalHostReconciler) actionMatchProfile(prov provisioner.Provisione
 func (r *BareMetalHostReconciler) actionPreparing(prov provisioner.Provisioner, info *reconcileInfo) actionResult {
 	info.log.Info("preparing")
 
-	dirty, newStatus, err := getHostProvisioningSettings(info.host)
+	bmhDirty, newStatus, err := getHostProvisioningSettings(info.host)
 	if err != nil {
 		return actionError{err}
 	}
@@ -962,49 +962,56 @@ func (r *BareMetalHostReconciler) actionPreparing(prov provisioner.Provisioner,
 		RootDeviceHints:  newStatus.Provisioning.RootDeviceHints.DeepCopy(),
 		FirmwareConfig:   newStatus.Provisioning.Firmware.DeepCopy(),
 	}
-	// When manual cleaning fails, we think that the existed RAID configuration
+	// When manual cleaning fails, we think that the existing RAID configuration
 	// is invalid and needs to be reconfigured.
 	if info.host.Status.ErrorType == metal3v1alpha1.PreparationError {
 		prepareData.ActualRAIDConfig = nil
-		dirty = true
+		bmhDirty = true
 	}
 
-	// Use settings in hostFirmwareSettings if available
-	hfs, err := r.getHostFirmwareSettings(info)
+	// The hfsDirty flag is used to push the new settings to Ironic as part of the clean steps.
+	// The HFS Status field will be updated in the HostFirmwareSettingsReconciler when it reads the settings from Ironic.
+	// After manual cleaning is complete the HFS Spec should match the Status.
+	hfsDirty, hfs, err := r.getHostFirmwareSettings(info)
+
 	if err != nil {
 		// wait until hostFirmwareSettings are ready
 		return actionContinue{subResourceNotReadyRetryDelay}
 	}
-	if hfs != nil {
+	if hfsDirty {
 		prepareData.ActualFirmwareSettings = hfs.Status.Settings.DeepCopy()
 		prepareData.TargetFirmwareSettings = hfs.Spec.Settings.DeepCopy()
 	}
 
-	provResult, started, err := prov.Prepare(prepareData, dirty)
+	provResult, started, err := prov.Prepare(prepareData, bmhDirty || hfsDirty,
+		info.host.Status.ErrorType == metal3v1alpha1.PreparationError)
 
 	if err != nil {
 		return actionError{errors.Wrap(err, "error preparing host")}
 	}
 
 	if provResult.ErrorMessage != "" {
-		info.log.Info("handling cleaning error in controller")
-		clearHostProvisioningSettings(info.host)
+		if bmhDirty {
+			info.log.Info("handling cleaning error in controller")
+			clearHostProvisioningSettings(info.host)
+		}
 		return recordActionFailure(info, metal3v1alpha1.PreparationError, provResult.ErrorMessage)
 	}
 
-	if dirty && started {
+	if bmhDirty && started {
 		info.log.Info("saving host provisioning settings")
 		_, err := saveHostProvisioningSettings(info.host)
 		if err != nil {
 			return actionError{errors.Wrap(err, "could not save the host provisioning settings")}
 		}
 	}
+
 	if started && clearError(info.host) {
-		dirty = true
+		bmhDirty = true
 	}
 	if provResult.Dirty {
 		result := actionContinue{provResult.RequeueAfter}
-		if dirty {
+		if bmhDirty {
 			return actionUpdate{result}
 		}
 		return result
@@ -1384,34 +1391,35 @@ func (r *BareMetalHostReconciler) createHostFirmwareSettings(info *reconcileInfo
 }
 
 // Get the stored firmware settings if there are valid changes
-func (r *BareMetalHostReconciler) getHostFirmwareSettings(info *reconcileInfo) (hfs *metal3v1alpha1.HostFirmwareSettings, err error) {
+func (r *BareMetalHostReconciler) getHostFirmwareSettings(info *reconcileInfo) (dirty bool, hfs *metal3v1alpha1.HostFirmwareSettings, err error) {
 
 	hfs = &metal3v1alpha1.HostFirmwareSettings{}
 	if err = r.Get(context.TODO(), info.request.NamespacedName, hfs); err != nil {
 
 		if !k8serrors.IsNotFound(err) {
 			// Error reading the object
-			return nil, errors.Wrap(err, "could not load host firmware settings")
+			return false, nil, errors.Wrap(err, "could not load host firmware settings")
 		}
 
 		// Could not get settings, log it but don't return error as settings may not have been available at provisioner
 		info.log.Info("could not get hostFirmwareSettings", "namespacename", info.request.NamespacedName)
-		return nil, nil
+		return false, nil, nil
 	}
 
 	// Check if there are settings in the Spec that are different than the Status
 	if meta.IsStatusConditionTrue(hfs.Status.Conditions, string(metal3v1alpha1.FirmwareSettingsChangeDetected)) {
 
 		if meta.IsStatusConditionTrue(hfs.Status.Conditions, string(metal3v1alpha1.FirmwareSettingsValid)) {
-			return hfs, nil
+			info.log.Info("hostFirmwareSettings indicating ChangeDetected", "namespacename", info.request.NamespacedName)
+			return true, hfs, nil
 		}
 
 		info.log.Info("hostFirmwareSettings not valid", "namespacename", info.request.NamespacedName)
-		return nil, nil
+		return false, nil, nil
 	}
 
 	info.log.Info("hostFirmwareSettings no updates", "namespacename", info.request.NamespacedName)
-	return nil, nil
+	return false, nil, nil
 }
 
 func (r *BareMetalHostReconciler) saveHostStatus(host *metal3v1alpha1.BareMetalHost) error {
diff --git a/controllers/metal3.io/host_state_machine.go b/controllers/metal3.io/host_state_machine.go
index 8f38165e..3ead90de 100644
--- a/controllers/metal3.io/host_state_machine.go
+++ b/controllers/metal3.io/host_state_machine.go
@@ -419,6 +419,14 @@ func (hsm *hostStateMachine) handleAvailable(info *reconcileInfo) actionResult {
 		return actionComplete{}
 	}
 
+	// Check if hostFirmwareSettings have changed
+	if dirty, _, err := hsm.Reconciler.getHostFirmwareSettings(info); err != nil {
+		return actionError{err}
+	} else if dirty {
+		hsm.NextState = metal3v1alpha1.StatePreparing
+		return actionComplete{}
+	}
+
 	// ErrorCount is cleared when appropriate inside actionManageAvailable
 	actResult := hsm.Reconciler.actionManageAvailable(hsm.Provisioner, info)
 	if _, complete := actResult.(actionComplete); complete {
diff --git a/controllers/metal3.io/hostfirmwaresettings_controller.go b/controllers/metal3.io/hostfirmwaresettings_controller.go
index 33546aeb..60355366 100644
--- a/controllers/metal3.io/hostfirmwaresettings_controller.go
+++ b/controllers/metal3.io/hostfirmwaresettings_controller.go
@@ -41,9 +41,10 @@ import (
 )
 
 const (
-	provisionerRetryDelay          = time.Second * 30
-	resourceNotAvailableRetryDelay = time.Second * 30
-	reconcilerRequeueDelay         = time.Minute * 5
+	provisionerRetryDelay                = time.Second * 30
+	resourceNotAvailableRetryDelay       = time.Second * 30
+	reconcilerRequeueDelay               = time.Minute * 5
+	reconcilerRequeueDelayChangeDetected = time.Minute * 1
 )
 
 // HostFirmwareSettingsReconciler reconciles a HostFirmwareSettings object
@@ -162,6 +163,10 @@ func (r *HostFirmwareSettingsReconciler) Reconcile(ctx context.Context, req ctrl
 	}
 
 	// requeue to run again after delay
+	if meta.IsStatusConditionTrue(info.hfs.Status.Conditions, string(metal3v1alpha1.FirmwareSettingsChangeDetected)) {
+		// If there is a difference between Spec and Status shorten the query from Ironic so that the Status is updated when cleaning completes
+		return ctrl.Result{Requeue: true, RequeueAfter: reconcilerRequeueDelayChangeDetected}, nil
+	}
 	return ctrl.Result{Requeue: true, RequeueAfter: reconcilerRequeueDelay}, nil
 }
 
diff --git a/pkg/hardwareutils/bmc/irmc.go b/pkg/hardwareutils/bmc/irmc.go
index a9c0b789..1bfa03f9 100644
--- a/pkg/hardwareutils/bmc/irmc.go
+++ b/pkg/hardwareutils/bmc/irmc.go
@@ -75,7 +75,7 @@ func (a *iRMCAccessDetails) BIOSInterface() string {
 }
 
 func (a *iRMCAccessDetails) BootInterface() string {
-	return "ipxe"
+	return "pxe"
 }
 
 func (a *iRMCAccessDetails) ManagementInterface() string {
diff --git a/pkg/provisioner/demo/demo.go b/pkg/provisioner/demo/demo.go
index fa57a83a..b38da176 100644
--- a/pkg/provisioner/demo/demo.go
+++ b/pkg/provisioner/demo/demo.go
@@ -193,7 +193,7 @@ func (p *demoProvisioner) UpdateHardwareState() (hwState provisioner.HardwareSta
 }
 
 // Prepare remove existing configuration and set new configuration
-func (p *demoProvisioner) Prepare(data provisioner.PrepareData, unprepared bool) (result provisioner.Result, started bool, err error) {
+func (p *demoProvisioner) Prepare(data provisioner.PrepareData, unprepared bool, force bool) (result provisioner.Result, started bool, err error) {
 	hostName := p.objectMeta.Name
 
 	switch hostName {
diff --git a/pkg/provisioner/fixture/fixture.go b/pkg/provisioner/fixture/fixture.go
index c3d0f8fc..2628e511 100644
--- a/pkg/provisioner/fixture/fixture.go
+++ b/pkg/provisioner/fixture/fixture.go
@@ -191,7 +191,7 @@ func (p *fixtureProvisioner) UpdateHardwareState() (hwState provisioner.Hardware
 }
 
 // Prepare remove existing configuration and set new configuration
-func (p *fixtureProvisioner) Prepare(data provisioner.PrepareData, unprepared bool) (result provisioner.Result, started bool, err error) {
+func (p *fixtureProvisioner) Prepare(data provisioner.PrepareData, unprepared bool, force bool) (result provisioner.Result, started bool, err error) {
 	p.log.Info("preparing host")
 	started = unprepared
 	return
diff --git a/pkg/provisioner/ironic/ironic.go b/pkg/provisioner/ironic/ironic.go
index 20ec3c19..9a8ab1e5 100644
--- a/pkg/provisioner/ironic/ironic.go
+++ b/pkg/provisioner/ironic/ironic.go
@@ -1257,7 +1257,7 @@ func (p *ironicProvisioner) startManualCleaning(bmcAccess bmc.AccessDetails, iro
 
 // Prepare remove existing configuration and set new configuration.
 // If `started` is true,  it means that we successfully executed `tryChangeNodeProvisionState`.
-func (p *ironicProvisioner) Prepare(data provisioner.PrepareData, unprepared bool) (result provisioner.Result, started bool, err error) {
+func (p *ironicProvisioner) Prepare(data provisioner.PrepareData, unprepared bool, force bool) (result provisioner.Result, started bool, err error) {
 	bmcAccess, err := p.bmcAccess()
 	if err != nil {
 		result, err = transientError(err)
@@ -1308,9 +1308,9 @@ func (p *ironicProvisioner) Prepare(data provisioner.PrepareData, unprepared boo
 
 	case nodes.CleanFail:
 		// When clean failed, we need to clean host provisioning settings.
-		// If unprepared is false, means the settings aren't cleared.
+		// If force is false, it means the settings aren't cleared.
 		// So we can't set the node's state to manageable, until the settings are cleared.
-		if !unprepared {
+		if !force {
 			result, err = operationFailed(ironicNode.LastError)
 			return
 		}
diff --git a/pkg/provisioner/provisioner.go b/pkg/provisioner/provisioner.go
index e150198c..c3cf9a74 100644
--- a/pkg/provisioner/provisioner.go
+++ b/pkg/provisioner/provisioner.go
@@ -148,7 +148,7 @@ type Provisioner interface {
 	Adopt(data AdoptData, force bool) (result Result, err error)
 
 	// Prepare remove existing configuration and set new configuration
-	Prepare(data PrepareData, unprepared bool) (result Result, started bool, err error)
+	Prepare(data PrepareData, unprepared bool, force bool) (result Result, started bool, 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
diff --git a/tools/remove_local_ironic.sh b/tools/remove_local_ironic.sh
index dfa17015..f4914be8 100755
--- a/tools/remove_local_ironic.sh
+++ b/tools/remove_local_ironic.sh
@@ -3,8 +3,7 @@
 set -xe
 
 # This script removes local ironic containers. 
-
-CONTAINER_RUNTIME="${CONTAINER_RUNTIME:-podman}"
+# It requires ${CONTAINER_RUNTIME} variable to be defined first
 
 for name in ironic ironic-api ironic-conductor ironic-inspector dnsmasq httpd mariadb ipa-downloader \
     ironic-endpoint-keepalived ironic-log-watch httpd-reverse-proxy ; do
diff --git a/tools/run_local_ironic.sh b/tools/run_local_ironic.sh
index 2afffe17..025b16f1 100755
--- a/tools/run_local_ironic.sh
+++ b/tools/run_local_ironic.sh
@@ -15,12 +15,7 @@ CONTAINER_RUNTIME="${CONTAINER_RUNTIME:-podman}"
 HTTP_PORT="6180"
 PROVISIONING_IP="${PROVISIONING_IP:-"172.22.0.1"}"
 CLUSTER_PROVISIONING_IP="${CLUSTER_PROVISIONING_IP:-"172.22.0.2"}"
-# ironicendpoint is used in the CI setup
-if ip link show ironicendpoint > /dev/null; then
-    PROVISIONING_INTERFACE="${PROVISIONING_INTERFACE:-ironicendpoint}"
-else
-    PROVISIONING_INTERFACE="${PROVISIONING_INTERFACE:-}"
-fi
+PROVISIONING_INTERFACE="${PROVISIONING_INTERFACE:-"ironicendpoint"}"
 CLUSTER_DHCP_RANGE="${CLUSTER_DHCP_RANGE:-"172.22.0.10,172.22.0.100"}"
 IRONIC_KERNEL_PARAMS="${IRONIC_KERNEL_PARAMS:-"console=ttyS0"}"
 IRONIC_BOOT_ISO_SOURCE="${IRONIC_BOOT_ISO_SOURCE:-"local"}"
@@ -50,29 +45,7 @@ then
   chmod 604 "${MARIADB_KEY_FILE}"
 fi
 
-sudo mkdir -p "${IRONIC_DATA_DIR}/auth"
-
-if [ "$IRONIC_TLS_SETUP" = "true" ]; then
-    sudo mkdir -p "${IRONIC_DATA_DIR}/tls"
-
-    if [ -z "$IRONIC_CERT_FILE" ]; then
-        IRONIC_CERT_FILE="${IRONIC_DATA_DIR}/tls/ironic.crt"
-        IRONIC_KEY_FILE="${IRONIC_DATA_DIR}/tls/ironic.key"
-        IRONIC_CACERT_FILE="${IRONIC_CERT_FILE}"
-        sudo openssl req -x509 -newkey rsa:4096 -nodes -days 365 -subj "/CN=ironic" \
-            -addext "subjectAltName = IP:${CLUSTER_PROVISIONING_IP},IP:${PROVISIONING_IP}" \
-            -out "${IRONIC_CERT_FILE}" -keyout "${IRONIC_KEY_FILE}"
-    fi
-
-    if [ -z "$IRONIC_INSPECTOR_CERT_FILE" ]; then
-        IRONIC_INSPECTOR_CERT_FILE="${IRONIC_DATA_DIR}/tls/inspector.crt"
-        IRONIC_INSPECTOR_KEY_FILE="${IRONIC_DATA_DIR}/tls/inspector.key"
-        IRONIC_INSPECTOR_CACERT_FILE="${IRONIC_CERT_FILE}"
-        sudo openssl req -x509 -newkey rsa:4096 -nodes -days 365 -subj "/CN=ironic" \
-            -addext "subjectAltName = IP:${CLUSTER_PROVISIONING_IP},IP:${PROVISIONING_IP}" \
-            -out "${IRONIC_INSPECTOR_CERT_FILE}" -keyout "${IRONIC_INSPECTOR_KEY_FILE}"
-    fi
-
+if [ -n "$IRONIC_CERT_FILE" ]; then
     export IRONIC_BASE_URL="https://${CLUSTER_PROVISIONING_IP}"
     if [ -z "$IRONIC_CACERT_FILE" ]; then
         export IRONIC_CACERT_FILE=$IRONIC_CERT_FILE
@@ -88,17 +61,16 @@ IRONIC_ENDPOINT="${IRONIC_ENDPOINT:-"${IRONIC_BASE_URL}:6385/v1/"}"
 IRONIC_INSPECTOR_ENDPOINT="${IRONIC_INSPECTOR_ENDPOINT:-"${IRONIC_BASE_URL}:5050/v1/"}"
 CACHEURL="${CACHEURL:-"http://${PROVISIONING_IP}/images"}"
 IRONIC_FAST_TRACK="${IRONIC_FAST_TRACK:-"true"}"
-IRONIC_REVERSE_PROXY_SETUP=${IRONIC_REVERSE_PROXY_SETUP:-"true"}
 INSPECTOR_REVERSE_PROXY_SETUP=${INSPECTOR_REVERSE_PROXY_SETUP:-"true"}
-IRONIC_USE_MARIADB=${IRONIC_USE_MARIADB:-"false"}
 if [[ $IRONIC_TLS_SETUP == *false* ]]
 then
-     # No reverse proxy for Ironic if TLS is not used
-     IRONIC_REVERSE_PROXY_SETUP="false"
-     INSPECTOR_REVERSE_PROXY_SETUP="false"
+  INSPECTOR_REVERSE_PROXY_SETUP="false" # No Revese proxy for Ironic inspector if TLS is not used
 fi
 IRONIC_INSPECTOR_VLAN_INTERFACES=${IRONIC_INSPECTOR_VLAN_INTERFACES:-"all"}
 
+sudo mkdir -p "${IRONIC_DATA_DIR}"
+sudo mkdir -p "${IRONIC_DATA_DIR}/auth"
+
 cat << EOF | sudo tee "${IRONIC_DATA_DIR}/ironic-vars.env"
 HTTP_PORT=${HTTP_PORT}
 PROVISIONING_IP=${CLUSTER_PROVISIONING_IP}
@@ -113,16 +85,12 @@ CACHEURL=${CACHEURL}
 IRONIC_FAST_TRACK=${IRONIC_FAST_TRACK}
 IRONIC_KERNEL_PARAMS=${IRONIC_KERNEL_PARAMS}
 IRONIC_BOOT_ISO_SOURCE=${IRONIC_BOOT_ISO_SOURCE}
-IRONIC_TLS_SETUP=${IRONIC_TLS_SETUP}
-IRONIC_INSPECTOR_TLS_SETUP=${IRONIC_TLS_SETUP}
-IRONIC_REVERSE_PROXY_SETUP=${IRONIC_REVERSE_PROXY_SETUP}
 INSPECTOR_REVERSE_PROXY_SETUP=${INSPECTOR_REVERSE_PROXY_SETUP}
 IRONIC_INSPECTOR_VLAN_INTERFACES=${IRONIC_INSPECTOR_VLAN_INTERFACES}
 IPA_BASEURI=${IPA_BASEURI}
-IRONIC_USE_MARIADB=${IRONIC_USE_MARIADB}
 EOF
 
-if [ "$IRONIC_TLS_SETUP" == "true" ] && [ -n "$IRONIC_CA_CERT_B64" ]; then
+if [ "$IRONIC_TLS_SETUP" == "true" ]; then
 # shellcheck disable=SC2086
 cat << EOF | kubectl apply -f -
 apiVersion: v1
@@ -138,9 +106,6 @@ fi
 
 sudo "${CONTAINER_RUNTIME}" pull "$IRONIC_IMAGE"
 sudo "${CONTAINER_RUNTIME}" pull "$IRONIC_KEEPALIVED_IMAGE"
-if [ "$IRONIC_USE_MARIADB" = "true" ]; then
-    sudo "${CONTAINER_RUNTIME}" pull "$MARIADB_IMAGE"
-fi
 
 CERTS_MOUNTS=""
 
@@ -183,16 +148,14 @@ if [ -n "$IRONIC_USERNAME" ]; then
         "${IRONIC_DATA_DIR}/auth/ironic-rpc-auth-config"
      BASIC_AUTH_MOUNTS="-v ${IRONIC_DATA_DIR}/auth/ironic-auth-config:/auth/ironic/auth-config"
      BASIC_AUTH_MOUNTS="${BASIC_AUTH_MOUNTS} -v ${IRONIC_DATA_DIR}/auth/ironic-rpc-auth-config:/auth/ironic-rpc/auth-config"
-     IRONIC_HTPASSWD="$(htpasswd -n -b -B "${IRONIC_USERNAME}" "${IRONIC_PASSWORD}")"
-     IRONIC_HTPASSWD="--env HTTP_BASIC_HTPASSWD=${IRONIC_HTPASSWD} --env IRONIC_HTPASSWD=${IRONIC_HTPASSWD}"
+     IRONIC_HTPASSWD="--env HTTP_BASIC_HTPASSWD=$(htpasswd -n -b -B "${IRONIC_USERNAME}" "${IRONIC_PASSWORD}")"
 fi
 IRONIC_INSPECTOR_HTPASSWD=""
 if [ -n "$IRONIC_INSPECTOR_USERNAME" ]; then
      envsubst < "${SCRIPTDIR}/ironic-deployment/basic-auth/ironic-inspector-auth-config-tpl" > \
         "${IRONIC_DATA_DIR}/auth/ironic-inspector-auth-config"
      BASIC_AUTH_MOUNTS="${BASIC_AUTH_MOUNTS} -v ${IRONIC_DATA_DIR}/auth/ironic-inspector-auth-config:/auth/ironic-inspector/auth-config"
-     IRONIC_INSPECTOR_HTPASSWD="$(htpasswd -n -b -B "${IRONIC_INSPECTOR_USERNAME}" "${IRONIC_INSPECTOR_PASSWORD}")"
-     IRONIC_INSPECTOR_HTPASSWD="--env HTTP_BASIC_HTPASSWD=${IRONIC_INSPECTOR_HTPASSWD} --env INSPECTOR_HTPASSWD=${IRONIC_INSPECTOR_HTPASSWD}"
+     IRONIC_INSPECTOR_HTPASSWD="--env HTTP_BASIC_HTPASSWD=$(htpasswd -n -b -B "${IRONIC_INSPECTOR_USERNAME}" "${IRONIC_INSPECTOR_PASSWORD}")"
 fi
 
 sudo mkdir -p "$IRONIC_DATA_DIR/html/images"
@@ -212,13 +175,8 @@ fi
 
 "$SCRIPTDIR/tools/remove_local_ironic.sh"
 
-if [ "$IRONIC_USE_MARIADB" = "true" ]; then
-    # set password for mariadb
-    mariadb_password=$(echo "$(date;hostname)"|sha256sum |cut -c-20)
-    IRONIC_MARIADB_PASSWORD="--env MARIADB_PASSWORD=$mariadb_password"
-else
-    IRONIC_MARIADB_PASSWORD=
-fi
+# set password for mariadb
+mariadb_password=$(echo "$(date;hostname)"|sha256sum |cut -c-20)
 
 POD=""
 
@@ -251,30 +209,31 @@ sudo "${CONTAINER_RUNTIME}" run -d --net host --privileged --name dnsmasq \
      ${POD} --env-file "${IRONIC_DATA_DIR}/ironic-vars.env" \
      -v "$IRONIC_DATA_DIR:/shared" --entrypoint /bin/rundnsmasq "${IRONIC_IMAGE}"
 
-# See this file for env vars you can set, like IP, DHCP_RANGE, INTERFACE
-# https://github.com/metal3-io/ironic-image/blob/main/scripts/runhttpd
+# https://github.com/metal3-io/mariadb-image/blob/main/runmariadb
 # shellcheck disable=SC2086
-sudo "${CONTAINER_RUNTIME}" run -d --net host --privileged --name httpd \
+sudo "${CONTAINER_RUNTIME}" run -d --net host --privileged --name mariadb \
+     ${POD} ${CERTS_MOUNTS} --env-file "${IRONIC_DATA_DIR}/ironic-vars.env" \
+     -v "$IRONIC_DATA_DIR:/shared" \
+     --env "MARIADB_PASSWORD=$mariadb_password" "${MARIADB_IMAGE}"
+
+# See this file for additional env vars you may want to pass, like IP and INTERFACE
+# https://github.com/metal3-io/ironic-image/blob/main/scripts/runironic-api
+# shellcheck disable=SC2086
+sudo "${CONTAINER_RUNTIME}" run -d --net host --privileged --name ironic-api \
      ${POD} ${CERTS_MOUNTS} ${BASIC_AUTH_MOUNTS} ${IRONIC_HTPASSWD} \
      --env-file "${IRONIC_DATA_DIR}/ironic-vars.env" \
-     -v "$IRONIC_DATA_DIR:/shared" --entrypoint /bin/runhttpd "${IRONIC_IMAGE}"
-
-if [ "$IRONIC_USE_MARIADB" = "true" ]; then
-    # https://github.com/metal3-io/mariadb-image/blob/main/runmariadb
-    # shellcheck disable=SC2086
-    sudo "${CONTAINER_RUNTIME}" run -d --net host --privileged --name mariadb \
-         ${POD} ${CERTS_MOUNTS} --env-file "${IRONIC_DATA_DIR}/ironic-vars.env" \
-         -v "$IRONIC_DATA_DIR:/shared" \
-         --env "MARIADB_PASSWORD=$mariadb_password" "${MARIADB_IMAGE}"
-fi
+     --env "MARIADB_PASSWORD=$mariadb_password" \
+     --entrypoint /bin/runironic-api \
+     -v "$IRONIC_DATA_DIR:/shared" "${IRONIC_IMAGE}"
 
 # See this file for additional env vars you may want to pass, like IP and INTERFACE
-# https://github.com/metal3-io/ironic-image/blob/main/scripts/runironic
+# https://github.com/metal3-io/ironic-image/blob/main/scripts/runironic-conductor
 # shellcheck disable=SC2086
-sudo "${CONTAINER_RUNTIME}" run -d --net host --privileged --name ironic \
+sudo "${CONTAINER_RUNTIME}" run -d --net host --privileged --name ironic-conductor \
      ${POD} ${CERTS_MOUNTS} ${BASIC_AUTH_MOUNTS} ${IRONIC_HTPASSWD} \
      --env-file "${IRONIC_DATA_DIR}/ironic-vars.env" \
-     ${IRONIC_MARIADB_PASSWORD} --entrypoint /bin/runironic \
+     --env "MARIADB_PASSWORD=$mariadb_password" \
+     --entrypoint /bin/runironic-conductor \
      -v "$IRONIC_DATA_DIR:/shared" "${IRONIC_IMAGE}"
 
 # Start ironic-endpoint-keepalived


Test patch:
diff --git a/apis/metal3.io/v1alpha1/bmceventsubscription_webhook_test.go b/apis/metal3.io/v1alpha1/bmceventsubscription_webhook_test.go
deleted file mode 100644
index 78ef7c29..00000000
--- a/apis/metal3.io/v1alpha1/bmceventsubscription_webhook_test.go
+++ /dev/null
@@ -1,156 +0,0 @@
-/*
-Copyright 2019 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package v1alpha1
-
-import (
-	"testing"
-
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-)
-
-func TestBMCEventSubscriptionUpdate(t *testing.T) {
-	tests := []struct {
-		name      string
-		bes       *BMCEventSubscription
-		old       *BMCEventSubscription
-		wantedErr string
-	}{
-		{
-			// This is valid because the spec wasn't updated.
-			name: "valid",
-			bes: &BMCEventSubscription{
-				TypeMeta: metav1.TypeMeta{
-					Kind:       "BMCEventSubscription",
-					APIVersion: "metal3.io/v1alpha1",
-				},
-				ObjectMeta: metav1.ObjectMeta{
-					Name:      "test",
-					Namespace: "test-namespace",
-				},
-				Spec: BMCEventSubscriptionSpec{},
-			},
-			old: &BMCEventSubscription{
-				TypeMeta: metav1.TypeMeta{
-					Kind:       "BMCEventSubscription",
-					APIVersion: "metal3.io/v1alpha1",
-				},
-				ObjectMeta: metav1.ObjectMeta{
-					Name:      "test",
-					Namespace: "test-namespace",
-				},
-				Spec: BMCEventSubscriptionSpec{},
-			},
-			wantedErr: "",
-		},
-		{
-			name: "invalid",
-			bes: &BMCEventSubscription{
-				TypeMeta: metav1.TypeMeta{
-					Kind:       "BMCEventSubscription",
-					APIVersion: "metal3.io/v1alpha1",
-				},
-				ObjectMeta: metav1.ObjectMeta{
-					Name:      "test",
-					Namespace: "test-namespace",
-				},
-				Spec: BMCEventSubscriptionSpec{},
-			},
-			old: &BMCEventSubscription{
-				TypeMeta: metav1.TypeMeta{
-					Kind:       "BMCEventSubscription",
-					APIVersion: "metal3.io/v1alpha1",
-				},
-				ObjectMeta: metav1.ObjectMeta{
-					Name:      "test",
-					Namespace: "test-namespace",
-				},
-				Spec: BMCEventSubscriptionSpec{Context: "abc"},
-			},
-			wantedErr: "subscriptions cannot be updated, please recreate it",
-		},
-		{
-			// Status updates are valid
-			name: "status updated",
-			bes: &BMCEventSubscription{
-				TypeMeta: metav1.TypeMeta{
-					Kind:       "BMCEventSubscription",
-					APIVersion: "metal3.io/v1alpha1",
-				},
-				ObjectMeta: metav1.ObjectMeta{
-					Name:      "test",
-					Namespace: "test-namespace",
-				},
-				Spec: BMCEventSubscriptionSpec{},
-				Status: BMCEventSubscriptionStatus{
-					SubscriptionID: "some-id",
-				},
-			},
-			old: &BMCEventSubscription{
-				TypeMeta: metav1.TypeMeta{
-					Kind:       "BMCEventSubscription",
-					APIVersion: "metal3.io/v1alpha1",
-				},
-				ObjectMeta: metav1.ObjectMeta{
-					Name:      "test",
-					Namespace: "test-namespace",
-				},
-				Spec:   BMCEventSubscriptionSpec{},
-				Status: BMCEventSubscriptionStatus{},
-			},
-			wantedErr: "",
-		},
-		{
-			// Finalizer updates are valid
-			name: "finalizers updated",
-			bes: &BMCEventSubscription{
-				TypeMeta: metav1.TypeMeta{
-					Kind:       "BMCEventSubscription",
-					APIVersion: "metal3.io/v1alpha1",
-				},
-				ObjectMeta: metav1.ObjectMeta{
-					Name:       "test",
-					Namespace:  "test-namespace",
-					Finalizers: []string{BMCEventSubscriptionFinalizer},
-				},
-				Spec:   BMCEventSubscriptionSpec{},
-				Status: BMCEventSubscriptionStatus{},
-			},
-			old: &BMCEventSubscription{
-				TypeMeta: metav1.TypeMeta{
-					Kind:       "BMCEventSubscription",
-					APIVersion: "metal3.io/v1alpha1",
-				},
-				ObjectMeta: metav1.ObjectMeta{
-					Name:       "test",
-					Namespace:  "test-namespace",
-					Finalizers: []string{},
-				},
-				Spec:   BMCEventSubscriptionSpec{},
-				Status: BMCEventSubscriptionStatus{},
-			},
-			wantedErr: "",
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			if err := tt.bes.ValidateUpdate(tt.old); !errorContains(err, tt.wantedErr) {
-				t.Errorf("BMCEventSubscription.ValidateUpdate() error = %v, wantErr %v", err, tt.wantedErr)
-			}
-		})
-	}
-}
diff --git a/controllers/metal3.io/baremetalhost_controller_test.go b/controllers/metal3.io/baremetalhost_controller_test.go
index 92d62c14..33e22fc7 100644
--- a/controllers/metal3.io/baremetalhost_controller_test.go
+++ b/controllers/metal3.io/baremetalhost_controller_test.go
@@ -17,6 +17,7 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/intstr"
 
 	promutil "github.com/prometheus/client_golang/prometheus/testutil"
 	ctrl "sigs.k8s.io/controller-runtime"
@@ -78,6 +79,31 @@ func newHost(name string, spec *metal3v1alpha1.BareMetalHostSpec) *metal3v1alpha
 	}
 }
 
+func newHostFirmwareSettings(host *metal3v1alpha1.BareMetalHost, conditions []metav1.Condition) *metal3v1alpha1.HostFirmwareSettings {
+	hfs := &metal3v1alpha1.HostFirmwareSettings{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      host.Name,
+			Namespace: host.Namespace,
+		},
+		Spec: metal3v1alpha1.HostFirmwareSettingsSpec{
+			Settings: metal3v1alpha1.DesiredSettingsMap{
+				"ProcVirtualization": intstr.FromString("Disabled"),
+				"SecureBoot":         intstr.FromString("Enabled"),
+			},
+		},
+		Status: metal3v1alpha1.HostFirmwareSettingsStatus{
+			Settings: metal3v1alpha1.SettingsMap{
+				"ProcVirtualization": "Disabled",
+				"SecureBoot":         "Enabled",
+			},
+		},
+	}
+
+	hfs.Status.Conditions = conditions
+
+	return hfs
+}
+
 func newDefaultNamedHost(name string, t *testing.T) *metal3v1alpha1.BareMetalHost {
 	spec := &metal3v1alpha1.BareMetalHostSpec{
 		BMC: metal3v1alpha1.BMCDetails{
@@ -2522,3 +2548,115 @@ func TestPreprovImageAvailable(t *testing.T) {
 		})
 	}
 }
+
+// TestHostFirwmareSettings verifies that a change to the HFS
+// can be detected as it will be used to set state to Preparing
+func TestHostFirmwareSettings(t *testing.T) {
+
+	testCases := []struct {
+		Scenario   string
+		Conditions []metav1.Condition
+		Dirty      bool
+	}{
+		{
+			Scenario: "spec and status the same",
+			Conditions: []metav1.Condition{
+				{Type: "Valid", Status: "True", Reason: "Success"},
+			},
+			Dirty: false,
+		},
+		{
+			Scenario: "spec changed",
+			Conditions: []metav1.Condition{
+				{Type: "ChangeDetected", Status: "True", Reason: "Success"},
+				{Type: "Valid", Status: "True", Reason: "Success"},
+			},
+			Dirty: true,
+		},
+		{
+			Scenario: "spec invalid",
+			Conditions: []metav1.Condition{
+				{Type: "ChangeDetected", Status: "True", Reason: "Success"},
+				{Type: "Valid", Status: "False", Reason: "Success"},
+			},
+			Dirty: false,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.Scenario, func(t *testing.T) {
+			host := newDefaultHost(t)
+			r := newTestReconciler(host)
+			i := makeReconcileInfo(host)
+			i.request = newRequest(host)
+
+			hfs := newHostFirmwareSettings(host, tc.Conditions)
+			r.Create(goctx.TODO(), hfs)
+
+			dirty, _, err := r.getHostFirmwareSettings(i)
+			if err != nil {
+				t.Fatal(err)
+			}
+			assert.Equal(t, tc.Dirty, dirty, "dirty flag did not match")
+		})
+	}
+
+}
+
+func TestBMHTransitionToPreparing(t *testing.T) {
+	var True = true
+	var False = false
+	host := newDefaultHost(t)
+	host.Spec.Online = true
+	host.Spec.ExternallyProvisioned = false
+	host.Spec.ConsumerRef = &corev1.ObjectReference{}
+	r := newTestReconciler(host)
+
+	waitForProvisioningState(t, r, host, metal3v1alpha1.StateAvailable)
+
+	// use different values between spec and status to force cleaning
+	host.Status.Provisioning.Firmware = &metal3v1alpha1.FirmwareConfig{
+		VirtualizationEnabled:             &True,
+		SimultaneousMultithreadingEnabled: &False,
+	}
+	host.Spec.Firmware = &metal3v1alpha1.FirmwareConfig{
+		VirtualizationEnabled:             &False,
+		SimultaneousMultithreadingEnabled: &True,
+	}
+
+	err := r.Update(goctx.TODO(), host)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	waitForProvisioningState(t, r, host, metal3v1alpha1.StatePreparing)
+}
+
+func TestHFSTransitionToPreparing(t *testing.T) {
+	host := newDefaultHost(t)
+	host.Spec.Online = true
+	host.Spec.ConsumerRef = &corev1.ObjectReference{}
+	host.Spec.ExternallyProvisioned = false
+	r := newTestReconciler(host)
+
+	waitForProvisioningState(t, r, host, metal3v1alpha1.StateAvailable)
+
+	// Update HFS so host will go through cleaning
+	hfs := &metal3v1alpha1.HostFirmwareSettings{}
+	key := client.ObjectKey{
+		Namespace: host.ObjectMeta.Namespace, Name: host.ObjectMeta.Name}
+	if err := r.Get(goctx.TODO(), key, hfs); err != nil {
+		t.Fatal(err)
+	}
+
+	hfs.Status = metal3v1alpha1.HostFirmwareSettingsStatus{
+		Conditions: []metav1.Condition{
+			{Type: "ChangeDetected", Status: "True", Reason: "Success"},
+			{Type: "Valid", Status: "True", Reason: "Success"},
+		},
+	}
+
+	r.Update(goctx.TODO(), hfs)
+
+	waitForProvisioningState(t, r, host, metal3v1alpha1.StatePreparing)
+}
diff --git a/controllers/metal3.io/host_state_machine_test.go b/controllers/metal3.io/host_state_machine_test.go
index fde1bd08..0b93f10a 100644
--- a/controllers/metal3.io/host_state_machine_test.go
+++ b/controllers/metal3.io/host_state_machine_test.go
@@ -1262,7 +1262,7 @@ func (m *mockProvisioner) UpdateHardwareState() (hwState provisioner.HardwareSta
 	return
 }
 
-func (m *mockProvisioner) Prepare(data provisioner.PrepareData, unprepared bool) (result provisioner.Result, started bool, err error) {
+func (m *mockProvisioner) Prepare(data provisioner.PrepareData, unprepared bool, force bool) (result provisioner.Result, started bool, err error) {
 	return m.getNextResultByMethod("Prepare"), m.nextResults["Prepare"].Dirty, err
 }
 
diff --git a/controllers/metal3.io/hostfirmwaresettings_test.go b/controllers/metal3.io/hostfirmwaresettings_test.go
index 428c29c3..bd69b4a2 100644
--- a/controllers/metal3.io/hostfirmwaresettings_test.go
+++ b/controllers/metal3.io/hostfirmwaresettings_test.go
@@ -75,7 +75,7 @@ func (m *hsfMockProvisioner) UpdateHardwareState() (hwState provisioner.Hardware
 	return
 }
 
-func (m *hsfMockProvisioner) Prepare(data provisioner.PrepareData, unprepared bool) (result provisioner.Result, started bool, err error) {
+func (m *hsfMockProvisioner) Prepare(data provisioner.PrepareData, unprepared bool, force bool) (result provisioner.Result, started bool, err error) {
 	return
 }
 
diff --git a/pkg/hardwareutils/bmc/access_test.go b/pkg/hardwareutils/bmc/access_test.go
index 2619c2d1..9dcd84e2 100644
--- a/pkg/hardwareutils/bmc/access_test.go
+++ b/pkg/hardwareutils/bmc/access_test.go
@@ -479,7 +479,7 @@ func TestStaticDriverInfo(t *testing.T) {
 			needsMac:   true,
 			driver:     "irmc",
 			bios:       "",
-			boot:       "ipxe",
+			boot:       "pxe",
 			management: "",
 			power:      "",
 			raid:       "irmc",
diff --git a/pkg/provisioner/ironic/prepare_test.go b/pkg/provisioner/ironic/prepare_test.go
index 22573288..fc9e21bb 100644
--- a/pkg/provisioner/ironic/prepare_test.go
+++ b/pkg/provisioner/ironic/prepare_test.go
@@ -207,7 +207,7 @@ func TestPrepare(t *testing.T) {
 				t.Fatalf("could not create provisioner: %s", err)
 			}
 
-			result, started, err := prov.Prepare(prepData, tc.unprepared)
+			result, started, err := prov.Prepare(prepData, tc.unprepared, tc.unprepared)
 
 			assert.Equal(t, tc.expectedStarted, started)
 			assert.Equal(t, tc.expectedDirty, result.Dirty)