Repository: metal3-io/baremetal-operator

Issue:
Enable "dupl" linter in golangci-lint
Enable "dupl" linter in golangci-lint. Fix any warnings enabling it causes.

/area ci
/help
/triage accepted

Hints from issue comments:
@metal3-io-bot: @tuminoid: 
	This request has been marked as needing help from a contributor.

Please ensure the request meets the requirements listed [here](https://git.k8s.io/community/contributors/guide/help-wanted.md).

If this request no longer meets these requirements, the label can be removed
by commenting with the `/remove-help` command.


<details>

In response to [this](https://github.com/metal3-io/baremetal-operator/issues/2351):

>Enable "dupl" linter in golangci-lint. Fix any warnings enabling it causes.
>
>/area ci
>/help
>/triage accepted


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-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>@metal3-io-bot: @tuminoid: The label(s) `area/ci` cannot be applied, because the repository doesn't have them.

<details>

In response to [this](https://github.com/metal3-io/baremetal-operator/issues/2351):

>Enable "dupl" linter in golangci-lint. Fix any warnings enabling it causes.
>
>/area ci
>/help
>/triage accepted


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-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>

Solution patch:
diff --git a/.github/workflows/e2e-test-optional-periodic.yml b/.github/workflows/e2e-test-optional-periodic.yml
index ea54d1ae..4c67fb86 100644
--- a/.github/workflows/e2e-test-optional-periodic.yml
+++ b/.github/workflows/e2e-test-optional-periodic.yml
@@ -4,6 +4,7 @@ on:
   schedule:
   # Run every day at 04:20 UTC (it is recommended to avoid running at the start of the hour)
   - cron: '20 4 * * *'
+  workflow_dispatch:
 
 permissions: {}
 
diff --git a/.github/workflows/e2e-test-periodic-main.yml b/.github/workflows/e2e-test-periodic-main.yml
index 9ed6cfcd..488c1eb4 100644
--- a/.github/workflows/e2e-test-periodic-main.yml
+++ b/.github/workflows/e2e-test-periodic-main.yml
@@ -4,6 +4,7 @@ on:
   schedule:
   # Run every day at 02:20 UTC (it is recommended to avoid running at the start of the hour)
   - cron: '20 2 * * *'
+  workflow_dispatch:
 
 permissions: {}
 
diff --git a/.github/workflows/e2e-test-periodic-release-0.10.yml b/.github/workflows/e2e-test-periodic-release-0.10.yml
index f3aae76e..d8460693 100644
--- a/.github/workflows/e2e-test-periodic-release-0.10.yml
+++ b/.github/workflows/e2e-test-periodic-release-0.10.yml
@@ -4,6 +4,7 @@ on:
   schedule:
   # Run every day at 02:50 UTC (it is recommended to avoid running at the start of the hour)
   - cron: '50 2 * * *'
+  workflow_dispatch:
 
 permissions: {}
 
diff --git a/.github/workflows/e2e-test-periodic-release-0.8.yml b/.github/workflows/e2e-test-periodic-release-0.8.yml
index 28b89786..d2d39780 100644
--- a/.github/workflows/e2e-test-periodic-release-0.8.yml
+++ b/.github/workflows/e2e-test-periodic-release-0.8.yml
@@ -4,6 +4,7 @@ on:
   schedule:
   # Run every day at 02:20 UTC (it is recommended to avoid running at the start of the hour)
   - cron: '20 2 * * *'
+  workflow_dispatch:
 
 permissions: {}
 
diff --git a/.github/workflows/e2e-test-periodic-release-0.9.yml b/.github/workflows/e2e-test-periodic-release-0.9.yml
index 8a3d5ca5..1ecf025f 100644
--- a/.github/workflows/e2e-test-periodic-release-0.9.yml
+++ b/.github/workflows/e2e-test-periodic-release-0.9.yml
@@ -4,6 +4,7 @@ on:
   schedule:
   # Run every day at 03:20 UTC (it is recommended to avoid running at the start of the hour)
   - cron: '20 3 * * *'
+  workflow_dispatch:
 
 permissions: {}
 
diff --git a/.github/workflows/osv-scanner-scan.yml b/.github/workflows/osv-scanner-scan.yml
index 859e0193..9d3f5e2c 100644
--- a/.github/workflows/osv-scanner-scan.yml
+++ b/.github/workflows/osv-scanner-scan.yml
@@ -39,7 +39,7 @@ jobs:
         echo "has_vulnerabilities=$(jq '.results | length > 0' results.json)" >> "${GITHUB_OUTPUT}"
       continue-on-error: true
     - name: "Run OSV Scanner Reporter"
-      uses: google/osv-scanner/actions/reporter@0e986b49c4e7ee5aa545531c4a8908455f8a9e82 # v2.0.0
+      uses: google/osv-scanner/actions/reporter@a2a23858b9650035f019219937f91ae615474b4d # v2.0.2
       with:
         scan-args: |-
           --output=results.sarif
@@ -47,13 +47,13 @@ jobs:
           --gh-annotations=false
       continue-on-error: true
     - name: Upload SARIF file
-      uses: github/codeql-action/upload-sarif@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13
+      uses: github/codeql-action/upload-sarif@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16
       with:
         sarif_file: results.sarif
     # if the results.json has any results, then it means there is some vulnerability to be checked out
     - name: Slack Notification on Failure
       if: ${{ steps.osv-scan.outputs.has_vulnerabilities == 'true' }}
-      uses: rtCamp/action-slack-notify@c33737706dea87cd7784c687dadc9adf1be59990 # 2.3.2
+      uses: rtCamp/action-slack-notify@e31e87e03dd19038e411e38ae27cbad084a90661 # 2.3.3
       env:
         SLACK_TITLE: "OSV-Scanner detected vulnerabilities in ${{ github.repository }}"
         SLACK_COLOR: "#FF0000"
diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml
index 24072a66..bf72050c 100644
--- a/.github/workflows/pipeline.yml
+++ b/.github/workflows/pipeline.yml
@@ -38,8 +38,7 @@ jobs:
       matrix:
         bmc-protocol:
         - redfish-virtualmedia
-        # TODO(lentzi90): Enable again when https://github.com/metal3-io/baremetal-operator/issues/2456 is fixed.
-        # - ipmi
+        - ipmi
     uses: ./.github/workflows/e2e-test.yml
     with:
       bmc-protocol: ${{ matrix.bmc-protocol }}
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 9af1c745..cee2c6d5 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -25,7 +25,7 @@ jobs:
         fetch-depth: 0
     - name: Get changed files
       id: changed-files
-      uses: tj-actions/changed-files@823fcebdb31bb35fdf2229d9f769b400309430d0 # v46.0.3
+      uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
     - name: Get release version
       id: release-version
       run: |
@@ -116,7 +116,7 @@ jobs:
         curl -L "https://raw.githubusercontent.com/${{ github.repository }}/main/releasenotes/${{ env.RELEASE_TAG }}.md" \
         -o "${{ env.RELEASE_TAG }}.md"
     - name: Release
-      uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2.2.1
+      uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2
       with:
         draft: true
         body_path: ${{ env.RELEASE_TAG }}.md
diff --git a/.golangci.yaml b/.golangci.yaml
index 075a0062..2e0f05f0 100644
--- a/.golangci.yaml
+++ b/.golangci.yaml
@@ -14,7 +14,7 @@ linters:
   - copyloopvar
   - decorder
   - dogsled
-  #- dupl
+  - dupl
   - dupword
   - durationcheck
   - errcheck
@@ -24,8 +24,8 @@ linters:
   #- exhaustive
   - exptostd
   - fatcontext
-  #- forbidigo
-  #- forcetypeassert
+  - forbidigo
+  - forcetypeassert
   - gci
   - ginkgolinter
   - gocheckcompilerdirectives
@@ -64,7 +64,7 @@ linters:
   - rowserrcheck
   - staticcheck
   - stylecheck
-  #- tagliatelle
+  - tagliatelle
   - testifylint
   - thelper
   - tparallel
@@ -124,6 +124,10 @@ linters-settings:
     - unnecessaryDefer
     - whyNoLint
     - wrapperFunc
+  tagliatelle:
+    case:
+      rules:
+        json: goCamel
 issues:
   exclude-dirs:
   - mock*
@@ -134,16 +138,11 @@ issues:
   - path: test/e2e
     linters:
     - gosec
-  - path: test/e2e
-    linters:
     - goconst
   - path: _test\.go
     linters:
-    - gci
     - goconst
     - gosec
-  - path: _test\.go
-    linters:
     - errcheck
     - gci
     - noctx
@@ -199,6 +198,9 @@ issues:
     - stylecheck
     text: "ST1016: methods on the same type should have the same receiver name"
     path: .*(api|types)\/.*\/conversion.*\.go$
+  - linters:
+    - tagliatelle
+    text: "RAID|MAC|BMO"
   include:
   - EXC0002 # include "missing comments" issues from golangci-lint
   max-issues-per-linter: 0
diff --git a/apis/go.mod b/apis/go.mod
index 029cf92f..49d69bd3 100644
--- a/apis/go.mod
+++ b/apis/go.mod
@@ -6,8 +6,8 @@ require (
 	github.com/google/uuid v1.6.0
 	github.com/metal3-io/baremetal-operator/pkg/hardwareutils v0.5.1
 	github.com/stretchr/testify v1.10.0
-	k8s.io/api v0.32.4
-	k8s.io/apimachinery v0.32.4
+	k8s.io/api v0.32.5
+	k8s.io/apimachinery v0.32.5
 	sigs.k8s.io/controller-runtime v0.20.4
 )
 
@@ -55,8 +55,8 @@ require (
 	gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	k8s.io/apiextensions-apiserver v0.32.4 // indirect
-	k8s.io/client-go v0.32.4 // indirect
+	k8s.io/apiextensions-apiserver v0.32.5 // indirect
+	k8s.io/client-go v0.32.5 // indirect
 	k8s.io/klog/v2 v2.130.1 // indirect
 	k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
 	k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
diff --git a/apis/go.sum b/apis/go.sum
index 3d37e8b7..fa111729 100644
--- a/apis/go.sum
+++ b/apis/go.sum
@@ -166,14 +166,14 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-k8s.io/api v0.32.4 h1:kw8Y/G8E7EpNy7gjB8gJZl3KJkNz8HM2YHrZPtAZsF4=
-k8s.io/api v0.32.4/go.mod h1:5MYFvLvweRhyKylM3Es/6uh/5hGp0dg82vP34KifX4g=
-k8s.io/apiextensions-apiserver v0.32.4 h1:IA+CoR63UDOijR/vEpow6wQnX4V6iVpzazJBskHrpHE=
-k8s.io/apiextensions-apiserver v0.32.4/go.mod h1:Y06XO/b92H8ymOdG1HlA1submf7gIhbEDc3RjriqZOs=
-k8s.io/apimachinery v0.32.4 h1:8EEksaxA7nd7xWJkkwLDN4SvWS5ot9g6Z/VZb3ju25I=
-k8s.io/apimachinery v0.32.4/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
-k8s.io/client-go v0.32.4 h1:zaGJS7xoYOYumoWIFXlcVrsiYioRPrXGO7dBfVC5R6M=
-k8s.io/client-go v0.32.4/go.mod h1:k0jftcyYnEtwlFW92xC7MTtFv5BNcZBr+zn9jPlT9Ic=
+k8s.io/api v0.32.5 h1:uqjjsYo1kTJr5NIcoIaP9F+TgXgADH7nKQx91FDAhtk=
+k8s.io/api v0.32.5/go.mod h1:bXXFU3fGCZ/eFMZvfHZC69PeGbXEL4zzjuPVzOxHF64=
+k8s.io/apiextensions-apiserver v0.32.5 h1:o0aKvmzIIs8Uk54pidk32pxET+Pg2ULnh9WI1PuKTwE=
+k8s.io/apiextensions-apiserver v0.32.5/go.mod h1:5fpedJa3HJJFBukAZ6ur91DEDye5gYuXISPbOiNLYpU=
+k8s.io/apimachinery v0.32.5 h1:6We3aJ6crC0ap8EhsEXcgX3LpI6SEjubpiOMXLROwPM=
+k8s.io/apimachinery v0.32.5/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
+k8s.io/client-go v0.32.5 h1:huFmQMzgWu0z4kbWsuZci+Gt4Fo72I4CcrvhToZ/Qp0=
+k8s.io/client-go v0.32.5/go.mod h1:Qchw6f9WIVrur7DKojAHpRgGLcANT0RLIvF39Jz58xA=
 k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
 k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
 k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
diff --git a/apis/metal3.io/v1alpha1/baremetalhost_types.go b/apis/metal3.io/v1alpha1/baremetalhost_types.go
index ba1b4333..765a8343 100644
--- a/apis/metal3.io/v1alpha1/baremetalhost_types.go
+++ b/apis/metal3.io/v1alpha1/baremetalhost_types.go
@@ -679,6 +679,7 @@ type NIC struct {
 	VLANs []VLAN `json:"vlans,omitempty"`
 
 	// The untagged VLAN ID
+	//nolint:tagliatelle
 	VLANID VLANID `json:"vlanId,omitempty"`
 
 	// Whether the NIC is PXE Bootable
@@ -866,6 +867,7 @@ type ProvisionStatus struct {
 
 	// The hosts's ID from the underlying provisioning tool (e.g. the
 	// Ironic node UUID).
+	//nolint:tagliatelle
 	ID string `json:"ID"`
 
 	// Image holds the details of the last image successfully
diff --git a/apis/metal3.io/v1alpha1/firmwareschema_types.go b/apis/metal3.io/v1alpha1/firmwareschema_types.go
index 9e9f4f5c..d9d020c4 100644
--- a/apis/metal3.io/v1alpha1/firmwareschema_types.go
+++ b/apis/metal3.io/v1alpha1/firmwareschema_types.go
@@ -30,24 +30,31 @@ type SettingSchema struct {
 
 	// The type of setting.
 	// +kubebuilder:validation:Enum=Enumeration;String;Integer;Boolean;Password
+	//nolint:tagliatelle
 	AttributeType string `json:"attribute_type,omitempty"`
 
+	//nolint:tagliatelle
 	// The allowable value for an Enumeration type setting.
 	AllowableValues []string `json:"allowable_values,omitempty"`
 
 	// The lowest value for an Integer type setting.
+	//nolint:tagliatelle
 	LowerBound *int `json:"lower_bound,omitempty"`
 
 	// The highest value for an Integer type setting.
+	//nolint:tagliatelle
 	UpperBound *int `json:"upper_bound,omitempty"`
 
 	// Minimum length for a String type setting.
+	//nolint:tagliatelle
 	MinLength *int `json:"min_length,omitempty"`
 
 	// Maximum length for a String type setting.
+	//nolint:tagliatelle
 	MaxLength *int `json:"max_length,omitempty"`
 
 	// Whether or not this setting is read only.
+	//nolint:tagliatelle
 	ReadOnly *bool `json:"read_only,omitempty"`
 
 	// Whether or not this setting's value is unique to this node, e.g.
diff --git a/apis/metal3.io/v1alpha1/preprovisioningimage_types.go b/apis/metal3.io/v1alpha1/preprovisioningimage_types.go
index fd655538..99679187 100644
--- a/apis/metal3.io/v1alpha1/preprovisioningimage_types.go
+++ b/apis/metal3.io/v1alpha1/preprovisioningimage_types.go
@@ -63,11 +63,13 @@ const (
 // PreprovisioningImageStatus defines the observed state of PreprovisioningImage.
 type PreprovisioningImageStatus struct {
 	// imageUrl is the URL from which the built image can be downloaded.
+	//nolint:tagliatelle
 	ImageUrl string `json:"imageUrl,omitempty"` //nolint:revive,stylecheck
 
 	// kernelUrl is the URL from which the kernel of the image can be downloaded.
 	// Only makes sense for initrd images.
 	// +optional
+	//nolint:tagliatelle
 	KernelUrl string `json:"kernelUrl,omitempty"` //nolint:revive,stylecheck
 
 	// extraKernelParams is a string with extra parameters to pass to the
diff --git a/cmd/get-hardware-details/main.go b/cmd/get-hardware-details/main.go
index 3638f857..288320a4 100644
--- a/cmd/get-hardware-details/main.go
+++ b/cmd/get-hardware-details/main.go
@@ -6,6 +6,7 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"log"
 	"net"
 	"net/url"
 	"os"
@@ -44,8 +45,7 @@ func main() {
 	endpoint := opts.Endpoint
 	parsedEndpoint, err := url.Parse(endpoint)
 	if err != nil {
-		fmt.Printf("invalid ironic endpoint: %s", err)
-		os.Exit(1)
+		log.Fatalf("invalid ironic endpoint: %s", err)
 	}
 
 	// Previously, this command accepted the Inspector endpoint. But since
@@ -59,37 +59,33 @@ func main() {
 
 	ironic, err := clients.IronicClient(endpoint, opts.AuthConfig, tlsConf)
 	if err != nil {
-		fmt.Printf("could not get ironic client: %s", err)
-		os.Exit(1)
+		log.Fatalf("could not get ironic client: %s", err)
 	}
 
 	introData := nodes.GetInventory(context.TODO(), ironic, opts.NodeID)
 	data, err := introData.Extract()
 	if err != nil {
-		fmt.Printf("could not get inspection data: %s", err)
-		os.Exit(1)
+		log.Fatalf("could not get inspection data: %s", err)
 	}
 
 	json, err := json.MarshalIndent(hardwaredetails.GetHardwareDetails(data, klog.NewKlogr()), "", "\t")
 	if err != nil {
-		fmt.Printf("could not convert inspection data: %s", err)
-		os.Exit(1)
+		log.Fatalf("could not convert inspection data: %s", err)
 	}
 
+	//nolint:forbidigo
 	fmt.Println(string(json))
 }
 
 func getOptions() (o options) {
 	if len(os.Args) != reqOptArgs {
-		fmt.Println("Usage: get-hardware-details <ironic URI> <node UUID>")
-		os.Exit(1)
+		log.Fatalln("Usage: get-hardware-details <ironic URI> <node UUID>")
 	}
 
 	var err error
 	o.Endpoint, o.AuthConfig, err = clients.ConfigFromEndpointURL(os.Args[1])
 	if err != nil {
-		fmt.Printf("Error: %s\n", err)
-		os.Exit(1)
+		log.Fatalf("Error: %s\n", err)
 	}
 	o.NodeID = os.Args[2]
 	return
diff --git a/cmd/make-virt-host/main.go b/cmd/make-virt-host/main.go
index 2a08b537..c4f5edca 100644
--- a/cmd/make-virt-host/main.go
+++ b/cmd/make-virt-host/main.go
@@ -5,7 +5,7 @@ import (
 	"encoding/json"
 	"encoding/xml"
 	"flag"
-	"fmt"
+	"log"
 	"os"
 	"os/exec"
 	"strings"
@@ -115,7 +115,9 @@ vbmc list -f json -c 'Domain name' -c Port
 // VBMC holds the parameters for describing a virtual machine
 // controller.
 type VBMC struct {
-	Port int    `json:"Port"`
+	//nolint:tagliatelle
+	Port int `json:"Port"`
+	//nolint:tagliatelle
 	Name string `json:"Domain name"`
 }
 
@@ -138,39 +140,34 @@ func main() {
 
 	virshDomain := flag.Arg(0)
 	if virshDomain == "" {
-		fmt.Fprintf(os.Stderr, "Missing domain argument\n")
-		os.Exit(1)
+		log.Fatalln("Missing domain argument")
 	}
 
 	if *verbose {
-		fmt.Printf("net: %s domain: %s\n", *provisionNet, virshDomain)
+		log.Printf("net: %s domain: %s\n", *provisionNet, virshDomain)
 	}
 
 	// Figure out the MAC for the VM
 	virshOut, err := exec.Command("sudo", "virsh", "dumpxml", virshDomain).Output() // #nosec
 	if err != nil {
-		fmt.Fprintf(os.Stderr,
-			"ERROR: Could not get details of domain %s: %s\n",
+		log.Fatalf("ERROR: Could not get details of domain %s: %s\n",
 			virshDomain, err)
-		os.Exit(1)
 	}
 
 	domainResult := Domain{}
 	err = xml.Unmarshal(virshOut, &domainResult)
 	if err != nil {
-		fmt.Fprintf(os.Stderr,
-			"ERROR: Could not unmarshal details of domain %s: %s\n",
+		log.Fatalf("ERROR: Could not unmarshal details of domain %s: %s\n",
 			virshDomain, err)
-		os.Exit(1)
 	}
 
 	if *verbose {
-		fmt.Printf("%v\n", domainResult)
+		log.Printf("%v\n", domainResult)
 	}
 
 	for _, iface := range domainResult.Interfaces {
 		if *verbose {
-			fmt.Printf("%v\n", iface)
+			log.Printf("%v\n", iface)
 		}
 		if iface.Source.Bridge == *provisionNet {
 			desiredMAC = iface.MAC.Address
@@ -182,33 +179,30 @@ func main() {
 	b64Password := base64.StdEncoding.EncodeToString([]byte(*password))
 
 	if *verbose {
-		fmt.Printf("Using MAC: %s\n", desiredMAC)
+		log.Printf("Using MAC: %s\n", desiredMAC)
 	}
 	if desiredMAC == "" {
-		fmt.Fprintf(os.Stderr, "Could not find MAC for %s on network %s\n",
+		log.Fatalf("Could not find MAC for %s on network %s\n",
 			virshDomain, *provisionNet)
-		os.Exit(1)
 	}
 
 	vbmcOut, err := exec.Command(
 		"vbmc", "list", "-f", "json", "-c", "Domain name", "-c", "Port",
 	).Output()
 	if err != nil {
-		fmt.Fprintf(os.Stderr, "ERROR: Could not get details of vbmc: %s\n", err)
-		os.Exit(1)
+		log.Fatalf("ERROR: Could not get details of vbmc: %s\n", err)
 	}
 
 	var vbmcResult []VBMC
 	err = json.Unmarshal(vbmcOut, &vbmcResult)
 	if err != nil {
-		fmt.Fprintf(os.Stderr, "ERROR: Could not unmarshal details of vbmc: %s\n", err)
-		os.Exit(1)
+		log.Fatalf("ERROR: Could not unmarshal details of vbmc: %s\n", err)
 	}
 
 	nameToPort := make(map[string]int)
 	for _, vbmc := range vbmcResult {
 		if *verbose {
-			fmt.Printf("VBMC: %s: %d\n", vbmc.Name, vbmc.Port)
+			log.Printf("VBMC: %s: %d\n", vbmc.Name, vbmc.Port)
 		}
 		nameToPort[vbmc.Name] = vbmc.Port
 	}
@@ -228,6 +222,6 @@ func main() {
 	t := yamltemplate.Must(yamltemplate.New("yaml_out").Parse(templateBody))
 	err = t.Execute(os.Stdout, args)
 	if err != nil {
-		fmt.Fprintf(os.Stderr, "ERROR: %s\n", err)
+		log.Printf("ERROR: %s\n", err)
 	}
 }
diff --git a/config/overlays/e2e-release-0.6/ironic.env b/config/overlays/e2e-release-0.10/ironic.env
similarity index 100%
rename from config/overlays/e2e-release-0.6/ironic.env
rename to config/overlays/e2e-release-0.10/ironic.env
diff --git a/config/overlays/e2e-release-0.6/kustomization.yaml b/config/overlays/e2e-release-0.10/kustomization.yaml
similarity index 92%
rename from config/overlays/e2e-release-0.6/kustomization.yaml
rename to config/overlays/e2e-release-0.10/kustomization.yaml
index f78cb6c1..3238d022 100644
--- a/config/overlays/e2e-release-0.6/kustomization.yaml
+++ b/config/overlays/e2e-release-0.10/kustomization.yaml
@@ -2,7 +2,7 @@ apiVersion: kustomize.config.k8s.io/v1beta1
 kind: Kustomization
 namespace: baremetal-operator-system
 resources:
-- https://github.com/metal3-io/baremetal-operator/config/overlays/basic-auth_tls?ref=release-0.6&timeout=120s
+- https://github.com/metal3-io/baremetal-operator/config/overlays/basic-auth_tls?ref=release-0.10&timeout=120s
 configMapGenerator:
 - name: ironic
   behavior: create
@@ -19,7 +19,7 @@ patches:
     name: controller-manager
 images:
 - name: quay.io/metal3-io/baremetal-operator
-  newTag: release-0.6
+  newTag: release-0.10
 # We cannot use suffix hashes since the kustomizations we build on
 # cannot be aware of what suffixes we add.
 generatorOptions:
diff --git a/config/overlays/fixture-release-0.6/kustomization.yaml b/config/overlays/fixture-release-0.10/kustomization.yaml
similarity index 91%
rename from config/overlays/fixture-release-0.6/kustomization.yaml
rename to config/overlays/fixture-release-0.10/kustomization.yaml
index 2de5b40c..6511bfd0 100644
--- a/config/overlays/fixture-release-0.6/kustomization.yaml
+++ b/config/overlays/fixture-release-0.10/kustomization.yaml
@@ -1,8 +1,8 @@
 apiVersion: kustomize.config.k8s.io/v1beta1
 kind: Kustomization
 resources:
-- https://github.com/metal3-io/baremetal-operator/config/namespace?ref=release-0.6
-- https://github.com/metal3-io/baremetal-operator/config/default?ref=release-0.6
+- https://github.com/metal3-io/baremetal-operator/config/namespace?ref=release-0.10
+- https://github.com/metal3-io/baremetal-operator/config/default?ref=release-0.10
 patches:
 - patch: |
     # Enable test mode (fixture provider instead of ironic)
@@ -18,4 +18,4 @@ patches:
     name: controller-manager
 images:
 - name: quay.io/metal3-io/baremetal-operator
-  newTag: release-0.6
+  newTag: release-0.10
diff --git a/docs/configuration.md b/docs/configuration.md
index e6142081..c3e1b63c 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -46,6 +46,10 @@ BMO_CONCURRENCY value lower than the requested PROVISIONING_LIMIT. Default is 20
 image for nodes that use IPv6. In dual stack environments, this can be
 used to tell Ironic which IP version it should set on the BMC.
 
+`PROVISIONING_NETWORK_DISABLED` -- Set to `true` if your deployment does not
+feature a provisioning network. This option disables drivers that require a
+provisioning network (such as IPMI).
+
 Kustomization Configuration
 ---------------------------
 
diff --git a/go.mod b/go.mod
index 56a227ad..ace3b15f 100644
--- a/go.mod
+++ b/go.mod
@@ -13,12 +13,12 @@ require (
 	github.com/pkg/errors v0.9.1
 	github.com/prometheus/client_golang v1.22.0
 	github.com/stretchr/testify v1.10.0
-	go.etcd.io/etcd/client/pkg/v3 v3.5.21
+	go.etcd.io/etcd/client/pkg/v3 v3.6.0
 	go.uber.org/zap v1.27.0
-	k8s.io/api v0.32.4
-	k8s.io/apimachinery v0.32.4
-	k8s.io/client-go v0.32.4
-	k8s.io/component-base v0.32.4
+	k8s.io/api v0.32.5
+	k8s.io/apimachinery v0.32.5
+	k8s.io/client-go v0.32.5
+	k8s.io/component-base v0.32.5
 	k8s.io/klog/v2 v2.130.1
 	k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738
 	sigs.k8s.io/controller-runtime v0.20.4
@@ -94,8 +94,8 @@ require (
 	gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	k8s.io/apiextensions-apiserver v0.32.4 // indirect
-	k8s.io/apiserver v0.32.4 // indirect
+	k8s.io/apiextensions-apiserver v0.32.5 // indirect
+	k8s.io/apiserver v0.32.5 // indirect
 	k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
 	sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 // indirect
 	sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
diff --git a/go.sum b/go.sum
index 6f3a99da..8ff720df 100644
--- a/go.sum
+++ b/go.sum
@@ -118,8 +118,8 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ
 github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
-github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
-github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
+github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
+github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
 github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
@@ -140,8 +140,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
 github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-go.etcd.io/etcd/client/pkg/v3 v3.5.21 h1:lPBu71Y7osQmzlflM9OfeIV2JlmpBjqBNlLtcoBqUTc=
-go.etcd.io/etcd/client/pkg/v3 v3.5.21/go.mod h1:BgqT/IXPjK9NkeSDjbzwsHySX3yIle2+ndz28nVsjUs=
+go.etcd.io/etcd/client/pkg/v3 v3.6.0 h1:nchnPqpuxvv3UuGGHaz0DQKYi5EIW5wOYsgUNRc365k=
+go.etcd.io/etcd/client/pkg/v3 v3.6.0/go.mod h1:Jv5SFWMnGvIBn8o3OaBq/PnT0jjsX8iNokAUessNjoA=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
 go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
@@ -227,18 +227,18 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-k8s.io/api v0.32.4 h1:kw8Y/G8E7EpNy7gjB8gJZl3KJkNz8HM2YHrZPtAZsF4=
-k8s.io/api v0.32.4/go.mod h1:5MYFvLvweRhyKylM3Es/6uh/5hGp0dg82vP34KifX4g=
-k8s.io/apiextensions-apiserver v0.32.4 h1:IA+CoR63UDOijR/vEpow6wQnX4V6iVpzazJBskHrpHE=
-k8s.io/apiextensions-apiserver v0.32.4/go.mod h1:Y06XO/b92H8ymOdG1HlA1submf7gIhbEDc3RjriqZOs=
-k8s.io/apimachinery v0.32.4 h1:8EEksaxA7nd7xWJkkwLDN4SvWS5ot9g6Z/VZb3ju25I=
-k8s.io/apimachinery v0.32.4/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
-k8s.io/apiserver v0.32.4 h1:Yf7sd/y+GOQKH1Qf6wUeayZrYXe2SKZ17Bcq7VQM5HQ=
-k8s.io/apiserver v0.32.4/go.mod h1:JFUMNtE2M5yqLZpIsgCb06SkVSW1YcxW1oyLSTfjXR8=
-k8s.io/client-go v0.32.4 h1:zaGJS7xoYOYumoWIFXlcVrsiYioRPrXGO7dBfVC5R6M=
-k8s.io/client-go v0.32.4/go.mod h1:k0jftcyYnEtwlFW92xC7MTtFv5BNcZBr+zn9jPlT9Ic=
-k8s.io/component-base v0.32.4 h1:HuF+2JVLbFS5GODLIfPCb1Td6b+G2HszJoArcWOSr5I=
-k8s.io/component-base v0.32.4/go.mod h1:10KloJEYw1keU/Xmjfy9TKJqUq7J2mYdiD1VDXoco4o=
+k8s.io/api v0.32.5 h1:uqjjsYo1kTJr5NIcoIaP9F+TgXgADH7nKQx91FDAhtk=
+k8s.io/api v0.32.5/go.mod h1:bXXFU3fGCZ/eFMZvfHZC69PeGbXEL4zzjuPVzOxHF64=
+k8s.io/apiextensions-apiserver v0.32.5 h1:o0aKvmzIIs8Uk54pidk32pxET+Pg2ULnh9WI1PuKTwE=
+k8s.io/apiextensions-apiserver v0.32.5/go.mod h1:5fpedJa3HJJFBukAZ6ur91DEDye5gYuXISPbOiNLYpU=
+k8s.io/apimachinery v0.32.5 h1:6We3aJ6crC0ap8EhsEXcgX3LpI6SEjubpiOMXLROwPM=
+k8s.io/apimachinery v0.32.5/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
+k8s.io/apiserver v0.32.5 h1:phmm2EOUVFI+cLiq8Grtuh166fTt/qgvkGPkpgzp5uY=
+k8s.io/apiserver v0.32.5/go.mod h1:5bfueS1tgARVWVXRJBMI5mHoCmev0jOvbxebai/kiqc=
+k8s.io/client-go v0.32.5 h1:huFmQMzgWu0z4kbWsuZci+Gt4Fo72I4CcrvhToZ/Qp0=
+k8s.io/client-go v0.32.5/go.mod h1:Qchw6f9WIVrur7DKojAHpRgGLcANT0RLIvF39Jz58xA=
+k8s.io/component-base v0.32.5 h1:2HiX+m3s9Iz5CMqdCVDH2V942UqzQvjuhcXb4W+KCsg=
+k8s.io/component-base v0.32.5/go.mod h1:jDsPNFFElv9m27TcYxlpEX7TZ3vdgx2g4PaqMUHpV/Y=
 k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
 k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
 k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
diff --git a/hack/ci-e2e.sh b/hack/ci-e2e.sh
index e3bec672..6d3af775 100755
--- a/hack/ci-e2e.sh
+++ b/hack/ci-e2e.sh
@@ -153,15 +153,16 @@ popd
 # Generate credentials
 BMO_OVERLAYS=(
   "${REPO_ROOT}/config/overlays/e2e"
-  "${REPO_ROOT}/config/overlays/e2e-release-0.6"
   "${REPO_ROOT}/config/overlays/e2e-release-0.8"
   "${REPO_ROOT}/config/overlays/e2e-release-0.9"
+  "${REPO_ROOT}/config/overlays/e2e-release-0.10"
 )
 IRONIC_OVERLAYS=(
   "${REPO_ROOT}/ironic-deployment/overlays/e2e"
-  "${REPO_ROOT}/ironic-deployment/overlays/e2e-release-24.1"
-  "${REPO_ROOT}/ironic-deployment/overlays/e2e-release-25.0"
   "${REPO_ROOT}/ironic-deployment/overlays/e2e-release-26.0"
+  "${REPO_ROOT}/ironic-deployment/overlays/e2e-release-27.0"
+  "${REPO_ROOT}/ironic-deployment/overlays/e2e-release-28.0"
+  "${REPO_ROOT}/ironic-deployment/overlays/e2e-release-29.0"
 )
 
 IRONIC_USERNAME="$(uuidgen)"
diff --git a/hack/tools/deploy-cli/deploy-cli.go b/hack/tools/deploy-cli/deploy-cli.go
index f8a12646..e90da7bd 100644
--- a/hack/tools/deploy-cli/deploy-cli.go
+++ b/hack/tools/deploy-cli/deploy-cli.go
@@ -444,10 +444,10 @@ func kubectlApply(ctx context.Context, kubeconfigPath string, resources []byte,
 		testexec.WithStdin(rbytes),
 	)
 
-	fmt.Printf("Running kubectl %s\n", strings.Join(aargs, " "))
+	log.Printf("Running kubectl %s\n", strings.Join(aargs, " "))
 	stdout, stderr, err := applyCmd.Run(ctx)
-	fmt.Printf("stderr:\n%s\n", string(stderr))
-	fmt.Printf("stdout:\n%s\n", string(stdout))
+	log.Printf("stderr:\n%s\n", string(stderr))
+	log.Printf("stdout:\n%s\n", string(stdout))
 	return err
 }
 
diff --git a/hack/tools/go.mod b/hack/tools/go.mod
index baef2c45..7f9a50b0 100644
--- a/hack/tools/go.mod
+++ b/hack/tools/go.mod
@@ -56,9 +56,9 @@ require (
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	k8s.io/api v0.32.4 // indirect
-	k8s.io/apiextensions-apiserver v0.32.4 // indirect
-	k8s.io/apimachinery v0.32.4 // indirect
+	k8s.io/api v0.32.5 // indirect
+	k8s.io/apiextensions-apiserver v0.32.5 // indirect
+	k8s.io/apimachinery v0.32.5 // indirect
 	k8s.io/klog/v2 v2.130.1 // indirect
 	k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 // indirect
 	k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
diff --git a/hack/tools/go.sum b/hack/tools/go.sum
index f49ec8b9..bb74d09b 100644
--- a/hack/tools/go.sum
+++ b/hack/tools/go.sum
@@ -178,12 +178,12 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-k8s.io/api v0.32.4 h1:kw8Y/G8E7EpNy7gjB8gJZl3KJkNz8HM2YHrZPtAZsF4=
-k8s.io/api v0.32.4/go.mod h1:5MYFvLvweRhyKylM3Es/6uh/5hGp0dg82vP34KifX4g=
-k8s.io/apiextensions-apiserver v0.32.4 h1:IA+CoR63UDOijR/vEpow6wQnX4V6iVpzazJBskHrpHE=
-k8s.io/apiextensions-apiserver v0.32.4/go.mod h1:Y06XO/b92H8ymOdG1HlA1submf7gIhbEDc3RjriqZOs=
-k8s.io/apimachinery v0.32.4 h1:8EEksaxA7nd7xWJkkwLDN4SvWS5ot9g6Z/VZb3ju25I=
-k8s.io/apimachinery v0.32.4/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
+k8s.io/api v0.32.5 h1:uqjjsYo1kTJr5NIcoIaP9F+TgXgADH7nKQx91FDAhtk=
+k8s.io/api v0.32.5/go.mod h1:bXXFU3fGCZ/eFMZvfHZC69PeGbXEL4zzjuPVzOxHF64=
+k8s.io/apiextensions-apiserver v0.32.5 h1:o0aKvmzIIs8Uk54pidk32pxET+Pg2ULnh9WI1PuKTwE=
+k8s.io/apiextensions-apiserver v0.32.5/go.mod h1:5fpedJa3HJJFBukAZ6ur91DEDye5gYuXISPbOiNLYpU=
+k8s.io/apimachinery v0.32.5 h1:6We3aJ6crC0ap8EhsEXcgX3LpI6SEjubpiOMXLROwPM=
+k8s.io/apimachinery v0.32.5/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
 k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
 k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
 k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 h1:hcha5B1kVACrLujCKLbr8XWMxCxzQx42DY8QKYJrDLg=
diff --git a/ironic-deployment/overlays/e2e-release-26.0/ironic_bmo_configmap.env b/ironic-deployment/overlays/e2e-release-26.0/ironic_bmo_configmap.env
index 9dc4c4bf..3e41bfbe 100644
--- a/ironic-deployment/overlays/e2e-release-26.0/ironic_bmo_configmap.env
+++ b/ironic-deployment/overlays/e2e-release-26.0/ironic_bmo_configmap.env
@@ -1,6 +1,8 @@
 HTTP_PORT=6180
 PROVISIONING_INTERFACE=eth0
 CACHEURL=http://192.168.222.1/images
+IRONIC_HTTP_URL=http://192.168.222.1:6180
+IRONIC_BASE_URL=https://192.168.222.1:6385
 IRONIC_EXTERNAL_CALLBACK_URL=https://192.168.222.1:6385
 IRONIC_KERNEL_PARAMS=console=ttyS0
 IRONIC_INSPECTOR_VLAN_INTERFACES=all
diff --git a/ironic-deployment/overlays/e2e-release-24.1/ironic-patch.yaml b/ironic-deployment/overlays/e2e-release-27.0/ironic-patch.yaml
similarity index 100%
rename from ironic-deployment/overlays/e2e-release-24.1/ironic-patch.yaml
rename to ironic-deployment/overlays/e2e-release-27.0/ironic-patch.yaml
diff --git a/ironic-deployment/overlays/e2e-release-25.0/ironic_bmo_configmap.env b/ironic-deployment/overlays/e2e-release-27.0/ironic_bmo_configmap.env
similarity index 73%
rename from ironic-deployment/overlays/e2e-release-25.0/ironic_bmo_configmap.env
rename to ironic-deployment/overlays/e2e-release-27.0/ironic_bmo_configmap.env
index 9dc4c4bf..3e41bfbe 100644
--- a/ironic-deployment/overlays/e2e-release-25.0/ironic_bmo_configmap.env
+++ b/ironic-deployment/overlays/e2e-release-27.0/ironic_bmo_configmap.env
@@ -1,6 +1,8 @@
 HTTP_PORT=6180
 PROVISIONING_INTERFACE=eth0
 CACHEURL=http://192.168.222.1/images
+IRONIC_HTTP_URL=http://192.168.222.1:6180
+IRONIC_BASE_URL=https://192.168.222.1:6385
 IRONIC_EXTERNAL_CALLBACK_URL=https://192.168.222.1:6385
 IRONIC_KERNEL_PARAMS=console=ttyS0
 IRONIC_INSPECTOR_VLAN_INTERFACES=all
diff --git a/ironic-deployment/overlays/e2e-release-25.0/kustomization.yaml b/ironic-deployment/overlays/e2e-release-27.0/kustomization.yaml
similarity index 84%
rename from ironic-deployment/overlays/e2e-release-25.0/kustomization.yaml
rename to ironic-deployment/overlays/e2e-release-27.0/kustomization.yaml
index c5397033..d9ded362 100644
--- a/ironic-deployment/overlays/e2e-release-25.0/kustomization.yaml
+++ b/ironic-deployment/overlays/e2e-release-27.0/kustomization.yaml
@@ -2,12 +2,12 @@ apiVersion: kustomize.config.k8s.io/v1beta1
 kind: Kustomization
 namespace: baremetal-operator-system
 resources:
-- https://github.com/metal3-io/baremetal-operator/config/namespace?ref=release-0.8&timeout=120s
-- https://github.com/metal3-io/baremetal-operator/ironic-deployment/base?ref=release-0.8&timeout=120s
+- https://github.com/metal3-io/baremetal-operator/config/namespace?ref=release-0.9&timeout=120s
+- https://github.com/metal3-io/baremetal-operator/ironic-deployment/base?ref=release-0.9&timeout=120s
 
 components:
-- https://github.com/metal3-io/baremetal-operator/ironic-deployment/components/basic-auth?ref=release-0.8&timeout=120s
-- https://github.com/metal3-io/baremetal-operator/ironic-deployment/components/tls?ref=release-0.8&timeout=120s
+- https://github.com/metal3-io/baremetal-operator/ironic-deployment/components/basic-auth?ref=release-0.9&timeout=120s
+- https://github.com/metal3-io/baremetal-operator/ironic-deployment/components/tls?ref=release-0.9&timeout=120s
 
 configMapGenerator:
 - envs:
@@ -30,7 +30,7 @@ patches:
 
 images:
 - name: quay.io/metal3-io/ironic
-  newTag: release-25.0
+  newTag: release-27.0
 
 # NOTE: These credentials are generated automatically in hack/ci-e2e.sh
 secretGenerator:
diff --git a/ironic-deployment/overlays/e2e-release-25.0/ironic-patch.yaml b/ironic-deployment/overlays/e2e-release-28.0/ironic-patch.yaml
similarity index 100%
rename from ironic-deployment/overlays/e2e-release-25.0/ironic-patch.yaml
rename to ironic-deployment/overlays/e2e-release-28.0/ironic-patch.yaml
diff --git a/ironic-deployment/overlays/e2e-release-28.0/ironic_bmo_configmap.env b/ironic-deployment/overlays/e2e-release-28.0/ironic_bmo_configmap.env
new file mode 100644
index 00000000..3e41bfbe
--- /dev/null
+++ b/ironic-deployment/overlays/e2e-release-28.0/ironic_bmo_configmap.env
@@ -0,0 +1,9 @@
+HTTP_PORT=6180
+PROVISIONING_INTERFACE=eth0
+CACHEURL=http://192.168.222.1/images
+IRONIC_HTTP_URL=http://192.168.222.1:6180
+IRONIC_BASE_URL=https://192.168.222.1:6385
+IRONIC_EXTERNAL_CALLBACK_URL=https://192.168.222.1:6385
+IRONIC_KERNEL_PARAMS=console=ttyS0
+IRONIC_INSPECTOR_VLAN_INTERFACES=all
+USE_IRONIC_INSPECTOR=false
diff --git a/ironic-deployment/overlays/e2e-release-24.1/kustomization.yaml b/ironic-deployment/overlays/e2e-release-28.0/kustomization.yaml
similarity index 76%
rename from ironic-deployment/overlays/e2e-release-24.1/kustomization.yaml
rename to ironic-deployment/overlays/e2e-release-28.0/kustomization.yaml
index 0bf9aa38..3810b8e0 100644
--- a/ironic-deployment/overlays/e2e-release-24.1/kustomization.yaml
+++ b/ironic-deployment/overlays/e2e-release-28.0/kustomization.yaml
@@ -2,12 +2,12 @@ apiVersion: kustomize.config.k8s.io/v1beta1
 kind: Kustomization
 namespace: baremetal-operator-system
 resources:
-- https://github.com/metal3-io/baremetal-operator/config/namespace?ref=release-0.6&timeout=120s
-- https://github.com/metal3-io/baremetal-operator/ironic-deployment/base?ref=release-0.6&timeout=120s
+- https://github.com/metal3-io/baremetal-operator/config/namespace?ref=release-0.10&timeout=120s
+- https://github.com/metal3-io/baremetal-operator/ironic-deployment/base?ref=release-0.10&timeout=120s
 
 components:
-- https://github.com/metal3-io/baremetal-operator/ironic-deployment/components/basic-auth?ref=release-0.6&timeout=120s
-- https://github.com/metal3-io/baremetal-operator/ironic-deployment/components/tls?ref=release-0.6&timeout=120s
+- https://github.com/metal3-io/baremetal-operator/ironic-deployment/components/basic-auth?ref=release-0.10&timeout=120s
+- https://github.com/metal3-io/baremetal-operator/ironic-deployment/components/tls?ref=release-0.10&timeout=120s
 
 configMapGenerator:
 - envs:
@@ -27,17 +27,10 @@ patches:
   target:
     kind: Certificate
     name: ironic-cert
-- patch: |-
-    - op: replace
-      path: /spec/ipAddresses/0
-      value: 192.168.222.1
-  target:
-    kind: Certificate
-    name: ironic-cacert
 
 images:
 - name: quay.io/metal3-io/ironic
-  newTag: release-24.1
+  newTag: release-28.0
 
 # NOTE: These credentials are generated automatically in hack/ci-e2e.sh
 secretGenerator:
diff --git a/ironic-deployment/overlays/e2e-release-29.0/ironic-patch.yaml b/ironic-deployment/overlays/e2e-release-29.0/ironic-patch.yaml
new file mode 100644
index 00000000..6e378383
--- /dev/null
+++ b/ironic-deployment/overlays/e2e-release-29.0/ironic-patch.yaml
@@ -0,0 +1,10 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: ironic
+spec:
+  template:
+    spec:
+      containers:
+        - name: ironic-dnsmasq
+          $patch: delete
diff --git a/ironic-deployment/overlays/e2e-release-24.1/ironic_bmo_configmap.env b/ironic-deployment/overlays/e2e-release-29.0/ironic_bmo_configmap.env
similarity index 100%
rename from ironic-deployment/overlays/e2e-release-24.1/ironic_bmo_configmap.env
rename to ironic-deployment/overlays/e2e-release-29.0/ironic_bmo_configmap.env
diff --git a/ironic-deployment/overlays/e2e-release-29.0/kustomization.yaml b/ironic-deployment/overlays/e2e-release-29.0/kustomization.yaml
new file mode 100644
index 00000000..6c15e42c
--- /dev/null
+++ b/ironic-deployment/overlays/e2e-release-29.0/kustomization.yaml
@@ -0,0 +1,40 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+namespace: baremetal-operator-system
+resources:
+- https://github.com/metal3-io/baremetal-operator/config/namespace?ref=release-0.10&timeout=120s
+- https://github.com/metal3-io/baremetal-operator/ironic-deployment/base?ref=release-0.10&timeout=120s
+
+components:
+- https://github.com/metal3-io/baremetal-operator/ironic-deployment/components/basic-auth?ref=release-0.10&timeout=120s
+- https://github.com/metal3-io/baremetal-operator/ironic-deployment/components/tls?ref=release-0.10&timeout=120s
+
+configMapGenerator:
+- envs:
+  - ironic_bmo_configmap.env
+  name: ironic-bmo-configmap
+  behavior: create
+
+patches:
+- path: ironic-patch.yaml
+# The TLS component adds certificates but it cannot know the exact IPs of our environment.
+# Here we patch the certificates to have the correct IPs.
+# - 192.168.222.1: management computer IP, forwarded to ironic inside kind
+- patch: |-
+    - op: replace
+      path: /spec/ipAddresses/0
+      value: 192.168.222.1
+  target:
+    kind: Certificate
+    name: ironic-cert
+
+images:
+- name: quay.io/metal3-io/ironic
+  newTag: release-29.0
+
+# NOTE: These credentials are generated automatically in hack/ci-e2e.sh
+secretGenerator:
+- name: ironic-htpasswd
+  behavior: create
+  envs:
+  - ironic-htpasswd
diff --git a/ironic-deployment/overlays/e2e/ironic_bmo_configmap.env b/ironic-deployment/overlays/e2e/ironic_bmo_configmap.env
index 7ff71bd2..ae14e9bc 100644
--- a/ironic-deployment/overlays/e2e/ironic_bmo_configmap.env
+++ b/ironic-deployment/overlays/e2e/ironic_bmo_configmap.env
@@ -1,6 +1,7 @@
 HTTP_PORT=6180
 PROVISIONING_INTERFACE=eth0
 CACHEURL=http://192.168.222.1/images
+IRONIC_HTTP_URL=http://192.168.222.1:6180
 IRONIC_BASE_URL=https://192.168.222.1:6385
 IRONIC_EXTERNAL_CALLBACK_URL=https://192.168.222.1:6385
 IRONIC_KERNEL_PARAMS=console=ttyS0
diff --git a/pkg/provisioner/ironic/capabilities.go b/pkg/provisioner/ironic/capabilities.go
index 7b11240c..ca20c098 100644
--- a/pkg/provisioner/ironic/capabilities.go
+++ b/pkg/provisioner/ironic/capabilities.go
@@ -30,7 +30,12 @@ func buildCapabilitiesValue(ironicNode *nodes.Node, bootMode metal3api.BootMode)
 		// There is no existing capabilities value
 		return bootModeCapabilities[bootMode]
 	}
-	existingCapabilities := capabilities.(string)
+	existingCapabilities, ok := capabilities.(string)
+	if !ok {
+		// The existing value is not a string, so we can replace
+		// the whole thing.
+		return bootModeCapabilities[bootMode]
+	}
 
 	if existingCapabilities == "" {
 		// The existing value is empty so we can replace the whole
diff --git a/pkg/provisioner/ironic/factory.go b/pkg/provisioner/ironic/factory.go
index 19571eb0..95cc21b4 100644
--- a/pkg/provisioner/ironic/factory.go
+++ b/pkg/provisioner/ironic/factory.go
@@ -154,6 +154,8 @@ func loadConfigFromEnv(havePreprovImgBuilder bool) (ironicConfig, error) {
 		}
 	}
 
+	c.provNetDisabled = strings.ToLower(os.Getenv("PROVISIONING_NETWORK_DISABLED")) == "true"
+
 	return c, nil
 }
 
diff --git a/pkg/provisioner/ironic/ironic.go b/pkg/provisioner/ironic/ironic.go
index 6e4de555..61340890 100644
--- a/pkg/provisioner/ironic/ironic.go
+++ b/pkg/provisioner/ironic/ironic.go
@@ -69,6 +69,7 @@ type ironicConfig struct {
 	liveISOForcePersistentBootDevice string
 	maxBusyHosts                     int
 	externalURL                      string
+	provNetDisabled                  bool
 }
 
 // Provisioner implements the provisioning.Provisioner interface
@@ -105,8 +106,9 @@ type ironicProvisioner struct {
 // FIXME(hroyrh) : move this to gophercloud when implementing
 // VirtualMediaGetResult Extract function.
 type VirtualMedia struct {
-	Image      string   `json:"image"`
-	Inserted   bool     `json:"inserted"`
+	Image    string `json:"image"`
+	Inserted bool   `json:"inserted"`
+	//nolint:tagliatelle
 	MediaTypes []string `json:"media_types"`
 }
 
diff --git a/pkg/provisioner/ironic/register.go b/pkg/provisioner/ironic/register.go
index 48e069a0..78f84436 100644
--- a/pkg/provisioner/ironic/register.go
+++ b/pkg/provisioner/ironic/register.go
@@ -55,6 +55,13 @@ func (p *ironicProvisioner) Register(data provisioner.ManagementAccessData, cred
 		return result, "", err
 	}
 
+	if bmcAccess.RequiresProvisioningNetwork() && p.config.provNetDisabled {
+		msg := fmt.Sprintf("BMC driver %s requires a provisioning network", bmcAccess.Type())
+		p.log.Info(msg)
+		result, err = operationFailed(msg)
+		return result, "", err
+	}
+
 	// Refuse to manage a node that has Disabled Power off if not supported by ironic,
 	// accidentally powering it off would require a arctic expedition to the data center
 	if data.DisablePowerOff && !p.availableFeatures.HasDisablePowerOff() {



Test patch:
diff --git a/internal/controller/metal3.io/baremetalhost_controller_test.go b/internal/controller/metal3.io/baremetalhost_controller_test.go
index 2f07aece..d206dec6 100644
--- a/internal/controller/metal3.io/baremetalhost_controller_test.go
+++ b/internal/controller/metal3.io/baremetalhost_controller_test.go
@@ -136,10 +136,13 @@ func newDefaultHost(t *testing.T) *metal3api.BareMetalHost {
 	return newDefaultNamedHost(t, t.Name())
 }
 
-func newTestReconcilerWithFixture(fix *fixture.Fixture, initObjs ...runtime.Object) *BareMetalHostReconciler {
+func newTestReconcilerWithFixture(t *testing.T, fix *fixture.Fixture, initObjs ...runtime.Object) *BareMetalHostReconciler {
+	t.Helper()
 	clientBuilder := fakeclient.NewClientBuilder().WithRuntimeObjects(initObjs...)
 	for _, v := range initObjs {
-		clientBuilder = clientBuilder.WithStatusSubresource(v.(client.Object))
+		object, ok := v.(client.Object)
+		require.True(t, ok, "failed to cast object to client.Object")
+		clientBuilder = clientBuilder.WithStatusSubresource(object)
 	}
 	c := clientBuilder.Build()
 	// Add a default secret that can be used by most hosts.
@@ -154,9 +157,10 @@ func newTestReconcilerWithFixture(fix *fixture.Fixture, initObjs ...runtime.Obje
 	}
 }
 
-func newTestReconciler(initObjs ...runtime.Object) *BareMetalHostReconciler {
+func newTestReconciler(t *testing.T, initObjs ...runtime.Object) *BareMetalHostReconciler {
+	t.Helper()
 	fix := fixture.Fixture{}
-	return newTestReconcilerWithFixture(&fix, initObjs...)
+	return newTestReconcilerWithFixture(t, &fix, initObjs...)
 }
 
 type DoneFunc func(host *metal3api.BareMetalHost, result reconcile.Result) bool
@@ -257,7 +261,7 @@ func TestHardwareDetails_EmptyStatus(t *testing.T) {
 		metal3api.HardwareDetailsAnnotation: hwdAnnotation,
 	}
 
-	r := newTestReconciler(host)
+	r := newTestReconciler(t, host)
 
 	tryReconcile(t, r, host,
 		func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
@@ -283,7 +287,7 @@ func TestHardwareDetails_StatusPresent(t *testing.T) {
 	hwd.Hostname = "existinghost"
 	host.Status.HardwareDetails = &hwd
 
-	r := newTestReconciler(host)
+	r := newTestReconciler(t, host)
 
 	tryReconcile(t, r, host,
 		func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
@@ -311,7 +315,7 @@ func TestHardwareDetails_StatusPresentInspectDisabled(t *testing.T) {
 	hwd.Hostname = "existinghost"
 	host.Status.HardwareDetails = &hwd
 
-	r := newTestReconciler(host)
+	r := newTestReconciler(t, host)
 
 	tryReconcile(t, r, host,
 		func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
@@ -339,7 +343,7 @@ func TestHardwareDetails_Invalid(t *testing.T) {
 	hwd.Hostname = "existinghost"
 	host.Status.HardwareDetails = &hwd
 
-	r := newTestReconciler(host)
+	r := newTestReconciler(t, host)
 	request := newRequest(host)
 	_, err := r.Reconcile(context.Background(), request)
 	expectedErr := "json: unknown field"
@@ -356,7 +360,7 @@ func TestStatusAnnotation_EmptyStatus(t *testing.T) {
 	host.Spec.Online = true
 	host.Spec.Image = &metal3api.Image{URL: "foo", Checksum: "123"}
 
-	r := newTestReconciler(host)
+	r := newTestReconciler(t, host)
 
 	tryReconcile(t, r, host,
 		func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
@@ -380,7 +384,7 @@ func TestStatusAnnotation_StatusPresent(t *testing.T) {
 	time := metav1.Now()
 	host.Status.LastUpdated = &time
 	host.Status.Provisioning.Image = metal3api.Image{URL: "foo", Checksum: "123"}
-	r := newTestReconciler(host)
+	r := newTestReconciler(t, host)
 
 	tryReconcile(t, r, host,
 		func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
@@ -418,7 +422,7 @@ func TestStatusAnnotation_Partial(t *testing.T) {
 	host.Spec.Online = true
 	host.Spec.Image = &metal3api.Image{URL: "foo", Checksum: "123"}
 
-	r := newTestReconciler(host)
+	r := newTestReconciler(t, host)
 
 	tryReconcile(t, r, host,
 		func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
@@ -454,7 +458,7 @@ func TestHardwareDataExist(t *testing.T) {
 		},
 	}
 
-	r := newTestReconciler(host, hardwareData)
+	r := newTestReconciler(t, host, hardwareData)
 
 	tryReconcile(t, r, host,
 		func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
@@ -473,7 +477,7 @@ func TestPause(t *testing.T) {
 	host.Annotations = map[string]string{
 		metal3api.PausedAnnotation: "true",
 	}
-	r := newTestReconciler(host)
+	r := newTestReconciler(t, host)
 
 	tryReconcile(t, r, host,
 		func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
@@ -495,7 +499,7 @@ func TestInspectDisabled(t *testing.T) {
 	host.Annotations = map[string]string{
 		metal3api.InspectAnnotationPrefix: "disabled",
 	}
-	r := newTestReconciler(host)
+	r := newTestReconciler(t, host)
 	waitForProvisioningState(t, r, host, metal3api.StatePreparing)
 	assert.Nil(t, host.Status.HardwareDetails)
 }
@@ -503,7 +507,7 @@ func TestInspectDisabled(t *testing.T) {
 // TestInspectEnabled ensures that Inspection is completed when not disabled.
 func TestInspectEnabled(t *testing.T) {
 	host := newDefaultHost(t)
-	r := newTestReconciler(host)
+	r := newTestReconciler(t, host)
 	waitForProvisioningState(t, r, host, metal3api.StatePreparing)
 	assert.NotNil(t, host.Status.HardwareDetails)
 }
@@ -512,7 +516,7 @@ func TestInspectEnabled(t *testing.T) {
 // updated as part of reconciling it.
 func TestAddFinalizers(t *testing.T) {
 	host := newDefaultHost(t)
-	r := newTestReconciler(host)
+	r := newTestReconciler(t, host)
 
 	tryReconcile(t, r, host,
 		func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
@@ -534,7 +538,7 @@ func TestAddFinalizers(t *testing.T) {
 // then the secret finalizer is not added again.
 func TestDoNotAddSecretFinalizersDuringDelete(t *testing.T) {
 	host := newDefaultHost(t)
-	r := newTestReconciler(host)
+	r := newTestReconciler(t, host)
 
 	// Let the host reach the available state before deleting it
 	waitForProvisioningState(t, r, host, metal3api.StateAvailable)
@@ -578,7 +582,7 @@ func TestDoNotAddSecretFinalizersDuringDelete(t *testing.T) {
 // status is set to a non-zero value during reconciliation.
 func TestSetLastUpdated(t *testing.T) {
 	host := newDefaultHost(t)
-	r := newTestReconciler(host)
+	r := newTestReconciler(t, host)
 
 	tryReconcile(t, r, host,
 		func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
@@ -718,7 +722,7 @@ func TestRebootWithSuffixlessAnnotation(t *testing.T) {
 	host.Spec.Image.URL = "foo"
 	host.Status.Provisioning.Image.URL = "foo"
 
-	r := newTestReconciler(host)
+	r := newTestReconciler(t, host)
 
 	tryReconcile(t, r, host,
 		func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
@@ -764,7 +768,7 @@ func TestRebootWithSuffixedAnnotation(t *testing.T) {
 	host.Spec.Image.URL = "foo"
 	host.Status.Provisioning.Image.URL = "foo"
 
-	r := newTestReconciler(host)
+	r := newTestReconciler(t, host)
 
 	tryReconcile(t, r, host,
 		func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
@@ -810,7 +814,7 @@ func TestRebootWithSuffixedAnnotationPowerOffDisabled(t *testing.T) {
 	host.Spec.DisablePowerOff = true
 
 	fix := fixture.Fixture{DisablePowerOff: true, PoweredOn: true}
-	r := newTestReconcilerWithFixture(&fix, host)
+	r := newTestReconcilerWithFixture(t, &fix, host)
 
 	// bmh reconcils until credentials are verified as valid
 	tryReconcile(t, r, host,
@@ -867,7 +871,7 @@ func TestRebootWithServicing(t *testing.T) {
 		},
 	}
 
-	r := newTestReconciler(host, hup)
+	r := newTestReconciler(t, host, hup)
 
 	tryReconcile(t, r, host,
 		func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
@@ -923,7 +927,7 @@ func TestRebootWithoutServicing(t *testing.T) {
 		VirtualizationEnabled: ptr.To(true),
 	}
 
-	r := newTestReconciler(host)
+	r := newTestReconciler(t, host)
 
 	tryReconcile(t, r, host,
 		func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
@@ -971,7 +975,7 @@ func getHostSecret(t *testing.T, r *BareMetalHostReconciler, host *metal3api.Bar
 
 func TestSecretUpdateOwnerRefAndEnvironmentLabelOnStartup(t *testing.T) {
 	host := newDefaultHost(t)
-	r := newTestReconciler(host)
+	r := newTestReconciler(t, host)
 
 	secret := getHostSecret(t, r, host)
 	assert.Empty(t, secret.OwnerReferences)
@@ -992,7 +996,7 @@ func TestSecretUpdateOwnerRefAndEnvironmentLabelOnStartup(t *testing.T) {
 // when the secret used exists and has all of the right fields.
 func TestUpdateCredentialsSecretSuccessFields(t *testing.T) {
 	host := newDefaultHost(t)
-	r := newTestReconciler(host)
+	r := newTestReconciler(t, host)
 
 	tryReconcile(t, r, host,
 		func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
@@ -1008,7 +1012,7 @@ func TestUpdateCredentialsSecretSuccessFields(t *testing.T) {
 // changed to another secret that is also good.
 func TestUpdateGoodCredentialsOnNewSecret(t *testing.T) {
 	host := newDefaultHost(t)
-	r := newTestReconciler(host)
+	r := newTestReconciler(t, host)
 
 	tryReconcile(t, r, host,
 		func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
@@ -1049,7 +1053,7 @@ func TestUpdateGoodCredentialsOnNewSecret(t *testing.T) {
 func TestUpdateGoodCredentialsOnBadSecret(t *testing.T) {
 	host := newDefaultHost(t)
 	badSecret := newBMCCredsSecret("bmc-creds-no-user", "", "Pass")
-	r := newTestReconciler(host, badSecret)
+	r := newTestReconciler(t, host, badSecret)
 
 	tryReconcile(t, r, host,
 		func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
@@ -1090,7 +1094,7 @@ func TestDiscoveredHost(t *testing.T) {
 				CredentialsName: "",
 			},
 		})
-	r := newTestReconciler(noAddressOrSecret)
+	r := newTestReconciler(t, noAddressOrSecret)
 	waitForStatus(t, r, noAddressOrSecret, metal3api.OperationalStatusDiscovered)
 	if noAddressOrSecret.Status.ErrorType != "" {
 		t.Errorf("Unexpected error type %s", noAddressOrSecret.Status.ErrorType)
@@ -1171,7 +1175,7 @@ func TestMissingBMCParameters(t *testing.T) {
 
 	for _, tc := range testCases {
 		t.Run(tc.Scenario, func(t *testing.T) {
-			r := newTestReconciler(tc.Secret, tc.Host)
+			r := newTestReconciler(t, tc.Secret, tc.Host)
 			waitForError(t, r, tc.Host)
 			if tc.Host.Status.OperationalStatus != metal3api.OperationalStatusError {
 				t.Errorf("Unexpected operational status %s", tc.Host.Status.OperationalStatus)
@@ -1194,7 +1198,7 @@ func TestFixSecret(t *testing.T) {
 				CredentialsName: "bmc-creds-no-user",
 			},
 		})
-	r := newTestReconciler(host, secret)
+	r := newTestReconciler(t, host, secret)
 	waitForError(t, r, host)
 
 	secret = &corev1.Secret{}
@@ -1228,7 +1232,7 @@ func TestBreakThenFixSecret(t *testing.T) {
 				CredentialsName: "bmc-creds-toggle-user",
 			},
 		})
-	r := newTestReconciler(host, secret)
+	r := newTestReconciler(t, host, secret)
 	tryReconcile(t, r, host,
 		func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
 			id := host.Status.Provisioning.ID
@@ -1275,7 +1279,7 @@ func TestBreakThenFixSecret(t *testing.T) {
 // the hardware profile name.
 func TestSetHardwareProfile(t *testing.T) {
 	host := newDefaultHost(t)
-	r := newTestReconciler(host)
+	r := newTestReconciler(t, host)
 
 	tryReconcile(t, r, host,
 		func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
@@ -1289,7 +1293,7 @@ func TestSetHardwareProfile(t *testing.T) {
 // of the status block is filled in for new hosts.
 func TestCreateHardwareDetails(t *testing.T) {
 	host := newDefaultHost(t)
-	r := newTestReconciler(host)
+	r := newTestReconciler(t, host)
 
 	tryReconcile(t, r, host,
 		func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
@@ -1410,7 +1414,7 @@ func TestProvision(t *testing.T) {
 		Checksum: "12345",
 	}
 	host.Spec.Online = true
-	r := newTestReconciler(host)
+	r := newTestReconciler(t, host)
 
 	tryReconcile(t, r, host,
 		func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
@@ -1430,7 +1434,7 @@ func TestProvisionCustomDeploy(t *testing.T) {
 		Method: "install_everything",
 	}
 	host.Spec.Online = true
-	r := newTestReconciler(host)
+	r := newTestReconciler(t, host)
 
 	tryReconcile(t, r, host,
 		func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
@@ -1454,7 +1458,7 @@ func TestProvisionCustomDeployWithURL(t *testing.T) {
 		Checksum: "12345",
 	}
 	host.Spec.Online = true
-	r := newTestReconciler(host)
+	r := newTestReconciler(t, host)
 
 	tryReconcile(t, r, host,
 		func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
@@ -1477,7 +1481,7 @@ func TestExternallyProvisionedTransitions(t *testing.T) {
 		host.Spec.Online = true
 		host.Spec.ConsumerRef = &corev1.ObjectReference{} // it doesn't have to point to a real object
 		host.Spec.ExternallyProvisioned = true
-		r := newTestReconciler(host)
+		r := newTestReconciler(t, host)
 
 		waitForProvisioningState(t, r, host, metal3api.StateExternallyProvisioned)
 	})
@@ -1486,7 +1490,7 @@ func TestExternallyProvisionedTransitions(t *testing.T) {
 		host := newDefaultHost(t)
 		host.Spec.Online = true
 		host.Spec.ExternallyProvisioned = true
-		r := newTestReconciler(host)
+		r := newTestReconciler(t, host)
 
 		waitForProvisioningState(t, r, host, metal3api.StateExternallyProvisioned)
 
@@ -1503,7 +1507,7 @@ func TestExternallyProvisionedTransitions(t *testing.T) {
 	t.Run("preparing to externally provisioned", func(t *testing.T) {
 		host := newDefaultHost(t)
 		host.Spec.Online = true
-		r := newTestReconciler(host)
+		r := newTestReconciler(t, host)
 
 		waitForProvisioningState(t, r, host, metal3api.StatePreparing)
 
@@ -1523,7 +1527,7 @@ func TestExternallyProvisionedTransitions(t *testing.T) {
 func TestPowerOn(t *testing.T) {
 	host := newDefaultHost(t)
 	host.Spec.Online = true
-	r := newTestReconciler(host)
+	r := newTestReconciler(t, host)
 
 	tryReconcile(t, r, host,
 		func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
@@ -1538,7 +1542,7 @@ func TestPowerOn(t *testing.T) {
 func TestPowerOff(t *testing.T) {
 	host := newDefaultHost(t)
 	host.Spec.Online = false
-	r := newTestReconciler(host)
+	r := newTestReconciler(t, host)
 
 	tryReconcile(t, r, host,
 		func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
@@ -1609,7 +1613,7 @@ func TestDeleteHost(t *testing.T) {
 			host.Status.Provisioning.ID = "made-up-id"
 			badSecret := newBMCCredsSecret("bmc-creds-no-user", "", "Pass")
 			fix := fixture.Fixture{}
-			r := newTestReconcilerWithFixture(&fix, host, badSecret)
+			r := newTestReconcilerWithFixture(t, &fix, host, badSecret)
 
 			tryReconcile(t, r, host,
 				func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
@@ -1816,7 +1820,7 @@ func TestProvisionerIsReady(t *testing.T) {
 	host := newDefaultHost(t)
 
 	fix := fixture.Fixture{BecomeReadyCounter: 5}
-	r := newTestReconcilerWithFixture(&fix, host)
+	r := newTestReconcilerWithFixture(t, &fix, host)
 
 	assert.InDelta(t, 0.0, promutil.ToFloat64(provisionerNotReady), 0.01)
 	tryReconcile(t, r, host,
@@ -1960,7 +1964,7 @@ func TestUpdateEventHandler(t *testing.T) {
 
 	for _, tc := range cases {
 		t.Run(tc.name, func(t *testing.T) {
-			r := newTestReconciler()
+			r := newTestReconciler(t)
 			assert.Equal(t, tc.expectedProcess, r.updateEventHandler(tc.event))
 		})
 	}
@@ -2266,7 +2270,7 @@ func TestInvalidBMHCanBeDeleted(t *testing.T) {
 	host.Spec.BMC.Address = fmt.Sprintf("%s%s%s", "<", host.Spec.BMC.Address, ">")
 
 	var fix fixture.Fixture
-	r := newTestReconcilerWithFixture(&fix, host)
+	r := newTestReconcilerWithFixture(t, &fix, host)
 
 	fix.SetValidateError("malformed url")
 	waitForError(t, r, host)
@@ -2413,7 +2417,7 @@ func TestGetHostArchitecture(t *testing.T) {
 
 func TestGetPreprovImageNoFormats(t *testing.T) {
 	host := newDefaultHost(t)
-	r := newTestReconciler(host)
+	r := newTestReconciler(t, host)
 	i := makeReconcileInfo(host)
 
 	imgData, err := r.getPreprovImage(i, nil)
@@ -2440,7 +2444,7 @@ func TestGetPreprovImageCreateUpdate(t *testing.T) {
 	host.Labels = map[string]string{
 		"answer.metal3.io": "42",
 	}
-	r := newTestReconciler(host, newSecret(secretName, nil))
+	r := newTestReconciler(t, host, newSecret(secretName, nil))
 	i := makeReconcileInfo(host)
 
 	imgData, err := r.getPreprovImage(i, []metal3api.ImageFormat{"iso"})
@@ -2504,7 +2508,7 @@ func TestGetPreprovImage(t *testing.T) {
 			},
 		},
 	}
-	r := newTestReconciler(host, image)
+	r := newTestReconciler(t, host, image)
 	i := makeReconcileInfo(host)
 
 	imgData, err := r.getPreprovImage(i, acceptFormats)
@@ -2541,7 +2545,7 @@ func TestGetPreprovImageNotCurrent(t *testing.T) {
 			},
 		},
 	}
-	r := newTestReconciler(host, image)
+	r := newTestReconciler(t, host, image)
 	i := makeReconcileInfo(host)
 
 	imgData, err := r.getPreprovImage(i, []metal3api.ImageFormat{metal3api.ImageFormatISO})
@@ -2551,7 +2555,7 @@ func TestGetPreprovImageNotCurrent(t *testing.T) {
 
 func TestPreprovImageAvailable(t *testing.T) {
 	host := newDefaultHost(t)
-	r := newTestReconciler(host, newSecret("network_secret_1", nil))
+	r := newTestReconciler(t, host, newSecret("network_secret_1", nil))
 
 	testCases := []struct {
 		Scenario   string
@@ -2891,7 +2895,7 @@ func TestHostFirmwareSettings(t *testing.T) {
 	for _, tc := range testCases {
 		t.Run(tc.Scenario, func(t *testing.T) {
 			host := newDefaultHost(t)
-			r := newTestReconciler(host)
+			r := newTestReconciler(t, host)
 			i := makeReconcileInfo(host)
 			i.request = newRequest(host)
 
@@ -2915,7 +2919,7 @@ func TestBMHTransitionToPreparing(t *testing.T) {
 	host.Spec.Online = true
 	host.Spec.ExternallyProvisioned = false
 	host.Spec.ConsumerRef = &corev1.ObjectReference{}
-	r := newTestReconciler(host)
+	r := newTestReconciler(t, host)
 
 	waitForProvisioningState(t, r, host, metal3api.StateAvailable)
 
@@ -2942,7 +2946,7 @@ func TestHFSTransitionToPreparing(t *testing.T) {
 	host.Spec.Online = true
 	host.Spec.ConsumerRef = &corev1.ObjectReference{}
 	host.Spec.ExternallyProvisioned = false
-	r := newTestReconciler(host)
+	r := newTestReconciler(t, host)
 
 	waitForProvisioningState(t, r, host, metal3api.StateAvailable)
 
@@ -2979,7 +2983,7 @@ func TestHFSEmptyStatusSettings(t *testing.T) {
 	host.Spec.Online = true
 	host.Spec.ConsumerRef = &corev1.ObjectReference{}
 	host.Spec.ExternallyProvisioned = false
-	r := newTestReconciler(host)
+	r := newTestReconciler(t, host)
 
 	waitForProvisioningState(t, r, host, metal3api.StatePreparing)
 
diff --git a/internal/controller/metal3.io/bmceventsubscription_controller_test.go b/internal/controller/metal3.io/bmceventsubscription_controller_test.go
index 052b99d0..a49c17af 100644
--- a/internal/controller/metal3.io/bmceventsubscription_controller_test.go
+++ b/internal/controller/metal3.io/bmceventsubscription_controller_test.go
@@ -7,6 +7,7 @@ import (
 	metal3api "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1"
 	"github.com/metal3-io/baremetal-operator/pkg/provisioner/fixture"
 	"github.com/metal3-io/baremetal-operator/pkg/utils"
+	"github.com/stretchr/testify/require"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
@@ -17,10 +18,13 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 )
 
-func newBMCTestReconcilerWithFixture(fix *fixture.Fixture, initObjs ...runtime.Object) *BMCEventSubscriptionReconciler {
+func newBMCTestReconcilerWithFixture(t *testing.T, fix *fixture.Fixture, initObjs ...runtime.Object) *BMCEventSubscriptionReconciler {
+	t.Helper()
 	clientBuilder := fakeclient.NewClientBuilder().WithRuntimeObjects(initObjs...)
 	for _, v := range initObjs {
-		clientBuilder = clientBuilder.WithStatusSubresource(v.(client.Object))
+		object, ok := v.(client.Object)
+		require.True(t, ok, "failed to cast object to client.Object")
+		clientBuilder = clientBuilder.WithStatusSubresource(object)
 	}
 	c := clientBuilder.Build()
 	// Add a default secret that can be used by most subscriptions.
@@ -39,9 +43,10 @@ func newBMCTestReconcilerWithFixture(fix *fixture.Fixture, initObjs ...runtime.O
 
 type BMCDoneFunc func(subscription *metal3api.BMCEventSubscription, result reconcile.Result) bool
 
-func newBMCTestReconciler(initObjs ...runtime.Object) *BMCEventSubscriptionReconciler {
+func newBMCTestReconciler(t *testing.T, initObjs ...runtime.Object) *BMCEventSubscriptionReconciler {
+	t.Helper()
 	fix := fixture.Fixture{}
-	return newBMCTestReconcilerWithFixture(&fix, initObjs...)
+	return newBMCTestReconcilerWithFixture(t, &fix, initObjs...)
 }
 
 func newBMCRequest(subscription *metal3api.BMCEventSubscription) ctrl.Request {
@@ -95,7 +100,7 @@ func HostWithProvisioningID(t *testing.T, host *metal3api.BareMetalHost) *metal3
 func TestBMCAddFinalizers(t *testing.T) {
 	host := newDefaultHost(t)
 	subscription := newDefaultSubscription(t)
-	r := newBMCTestReconciler(subscription, host)
+	r := newBMCTestReconciler(t, subscription, host)
 	err := r.addFinalizer(context.Background(), subscription)
 	if err != nil {
 		t.Error(err)
@@ -110,7 +115,7 @@ func TestBMCGetProvisioner(t *testing.T) {
 	host := newDefaultHost(t)
 	subscription := newDefaultSubscription(t)
 	request := newBMCRequest(subscription)
-	r := newBMCTestReconciler(subscription, host)
+	r := newBMCTestReconciler(t, subscription, host)
 	for _, tc := range []struct {
 		Scenario string
 		Host     *metal3api.BareMetalHost
@@ -145,7 +150,7 @@ func TestGetHTTPHeaders(t *testing.T) {
 	// The secret is automatically created by newBMCTestReconciler.
 	host := newDefaultHost(t)
 	subscription := newDefaultSubscription(t)
-	r := newBMCTestReconciler(subscription, host)
+	r := newBMCTestReconciler(t, subscription, host)
 
 	for _, tc := range []struct {
 		Scenario      string
diff --git a/internal/controller/metal3.io/demo_test.go b/internal/controller/metal3.io/demo_test.go
index e849eba4..c05406c1 100644
--- a/internal/controller/metal3.io/demo_test.go
+++ b/internal/controller/metal3.io/demo_test.go
@@ -6,6 +6,7 @@ import (
 
 	metal3api "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1"
 	"github.com/metal3-io/baremetal-operator/pkg/provisioner/demo"
+	"github.com/stretchr/testify/require"
 	"k8s.io/apimachinery/pkg/runtime"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
@@ -13,10 +14,13 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 )
 
-func newDemoReconciler(initObjs ...runtime.Object) *BareMetalHostReconciler {
+func newDemoReconciler(t *testing.T, initObjs ...runtime.Object) *BareMetalHostReconciler {
+	t.Helper()
 	clientBuilder := fakeclient.NewClientBuilder().WithRuntimeObjects(initObjs...)
 	for _, v := range initObjs {
-		clientBuilder = clientBuilder.WithStatusSubresource(v.(client.Object))
+		object, ok := v.(client.Object)
+		require.True(t, ok, "Failed to cast object to client.Object")
+		clientBuilder = clientBuilder.WithStatusSubresource(object)
 	}
 	c := clientBuilder.Build()
 
@@ -35,7 +39,7 @@ func newDemoReconciler(initObjs ...runtime.Object) *BareMetalHostReconciler {
 // a registration error.
 func TestDemoRegistrationError(t *testing.T) {
 	host := newDefaultNamedHost(t, demo.RegistrationErrorHost)
-	r := newDemoReconciler(host)
+	r := newDemoReconciler(t, host)
 
 	tryReconcile(t, r, host,
 		func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
@@ -53,7 +57,7 @@ func TestDemoRegistrationError(t *testing.T) {
 // that it is being registered.
 func TestDemoRegistering(t *testing.T) {
 	host := newDefaultNamedHost(t, demo.RegisteringHost)
-	r := newDemoReconciler(host)
+	r := newDemoReconciler(t, host)
 
 	tryReconcile(t, r, host,
 		func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
@@ -71,7 +75,7 @@ func TestDemoRegistering(t *testing.T) {
 // that it is being inspected.
 func TestDemoInspecting(t *testing.T) {
 	host := newDefaultNamedHost(t, demo.InspectingHost)
-	r := newDemoReconciler(host)
+	r := newDemoReconciler(t, host)
 
 	tryReconcile(t, r, host,
 		func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
@@ -92,7 +96,7 @@ func TestDemoPreparing(t *testing.T) {
 		Checksum: "a-checksum",
 	}
 	host.Spec.Online = true
-	r := newDemoReconciler(host)
+	r := newDemoReconciler(t, host)
 
 	tryReconcile(t, r, host,
 		func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
@@ -113,7 +117,7 @@ func TestDemoPreparingError(t *testing.T) {
 		Checksum: "a-checksum",
 	}
 	host.Spec.Online = true
-	r := newDemoReconciler(host)
+	r := newDemoReconciler(t, host)
 
 	tryReconcile(t, r, host,
 		func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
@@ -131,7 +135,7 @@ func TestDemoPreparingError(t *testing.T) {
 // that it is available to be provisioned.
 func TestDemoAvailable(t *testing.T) {
 	host := newDefaultNamedHost(t, demo.AvailableHost)
-	r := newDemoReconciler(host)
+	r := newDemoReconciler(t, host)
 
 	tryReconcile(t, r, host,
 		func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
@@ -154,7 +158,7 @@ func TestDemoProvisioning(t *testing.T) {
 		Checksum: "a-checksum",
 	}
 	host.Spec.Online = true
-	r := newDemoReconciler(host)
+	r := newDemoReconciler(t, host)
 
 	tryReconcile(t, r, host,
 		func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
@@ -177,7 +181,7 @@ func TestDemoProvisioned(t *testing.T) {
 		Checksum: "a-checksum",
 	}
 	host.Spec.Online = true
-	r := newDemoReconciler(host)
+	r := newDemoReconciler(t, host)
 
 	tryReconcile(t, r, host,
 		func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
@@ -200,7 +204,7 @@ func TestDemoValidationError(t *testing.T) {
 		Checksum: "a-checksum",
 	}
 	host.Spec.Online = true
-	r := newDemoReconciler(host)
+	r := newDemoReconciler(t, host)
 
 	tryReconcile(t, r, host,
 		func(host *metal3api.BareMetalHost, result reconcile.Result) bool {
diff --git a/internal/controller/metal3.io/host_state_machine_test.go b/internal/controller/metal3.io/host_state_machine_test.go
index e6204016..7480983f 100644
--- a/internal/controller/metal3.io/host_state_machine_test.go
+++ b/internal/controller/metal3.io/host_state_machine_test.go
@@ -18,8 +18,9 @@ import (
 	logf "sigs.k8s.io/controller-runtime/pkg/log"
 )
 
-func testStateMachine(host *metal3api.BareMetalHost) *hostStateMachine {
-	r := newTestReconciler()
+func testStateMachine(t *testing.T, host *metal3api.BareMetalHost) *hostStateMachine {
+	t.Helper()
+	r := newTestReconciler(t)
 	p, _ := r.ProvisionerFactory.NewProvisioner(context.TODO(), provisioner.BuildHostData(*host, bmc.Credentials{}),
 		func(reason, message string) {})
 	return newHostStateMachine(host, r, p, true)
@@ -36,6 +37,7 @@ func testNewReconciler(host *metal3api.BareMetalHost) *BareMetalHostReconciler {
 	return reconciler
 }
 
+//nolint:dupl
 func TestProvisioningCapacity(t *testing.T) {
 	testCases := []struct {
 		Scenario string
@@ -165,6 +167,7 @@ func TestProvisioningCapacity(t *testing.T) {
 	}
 }
 
+//nolint:dupl
 func TestDeprovisioningCapacity(t *testing.T) {
 	testCases := []struct {
 		Scenario string
@@ -841,7 +844,7 @@ func TestProvisioningCancelled(t *testing.T) {
 	for _, tc := range testCases {
 		t.Run(tc.Scenario, func(t *testing.T) {
 			host := tc.Host
-			hsm := testStateMachine(&host)
+			hsm := testStateMachine(t, &host)
 			actual := hsm.provisioningCancelled()
 			if tc.Expected && !actual {
 				t.Error("expected to need deprovisioning")
diff --git a/pkg/provisioner/ironic/clients/updateopts_test.go b/pkg/provisioner/ironic/clients/updateopts_test.go
index 6a7a23f9..8193f71c 100644
--- a/pkg/provisioner/ironic/clients/updateopts_test.go
+++ b/pkg/provisioner/ironic/clients/updateopts_test.go
@@ -7,6 +7,7 @@ import (
 	"github.com/go-logr/logr"
 	"github.com/gophercloud/gophercloud/v2/openstack/baremetal/v1/nodes"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 	logf "sigs.k8s.io/controller-runtime/pkg/log"
 )
 
@@ -365,10 +366,13 @@ func TestGetUpdateOperation(t *testing.T) {
 
 			switch c.ExpectedOp {
 			case nodes.AddOp, nodes.ReplaceOp:
+				var ev string
+				var ok bool
 				assert.NotNil(t, updateOp)
-				ev := c.ExpectedValue
+				ev = c.ExpectedValue
 				if ev == "" {
-					ev = c.NewValue.(string)
+					ev, ok = c.NewValue.(string)
+					require.True(t, ok, "expected NewValue to be a string")
 				}
 				assert.Equal(t, c.ExpectedOp, updateOp.Op)
 				assert.Equal(t, ev, updateOp.Value)
@@ -389,7 +393,8 @@ func TestTopLevelUpdateOpt(t *testing.T) {
 	u.SetTopLevelOpt("foo", "baz", "bar")
 	ops := u.Updates
 	assert.Len(t, ops, 1)
-	op := ops[0].(nodes.UpdateOperation)
+	op, ok := ops[0].(nodes.UpdateOperation)
+	require.True(t, ok, "expected UpdateOperation")
 	assert.Equal(t, nodes.AddOp, op.Op)
 	assert.Equal(t, "baz", op.Value)
 	assert.Equal(t, "/foo", op.Path)
@@ -414,7 +419,8 @@ func TestPropertiesUpdateOpts(t *testing.T) {
 	u.SetPropertiesOpts(newValues, &node)
 	ops := u.Updates
 	assert.Len(t, ops, 1)
-	op := ops[0].(nodes.UpdateOperation)
+	op, ok := ops[0].(nodes.UpdateOperation)
+	require.True(t, ok, "expected UpdateOperation")
 	assert.Equal(t, nodes.AddOp, op.Op)
 	assert.Equal(t, "quux", op.Value)
 	assert.Equal(t, "/properties/baz", op.Path)
@@ -435,7 +441,8 @@ func TestInstanceInfoUpdateOpts(t *testing.T) {
 	u.SetInstanceInfoOpts(newValues, &node)
 	ops := u.Updates
 	assert.Len(t, ops, 1)
-	op := ops[0].(nodes.UpdateOperation)
+	op, ok := ops[0].(nodes.UpdateOperation)
+	require.True(t, ok, "expected UpdateOperation")
 	assert.Equal(t, nodes.AddOp, op.Op)
 	assert.Equal(t, "quux", op.Value)
 	assert.Equal(t, "/instance_info/baz", op.Path)
diff --git a/pkg/provisioner/ironic/provision_test.go b/pkg/provisioner/ironic/provision_test.go
index 72ee57b7..d94066ce 100644
--- a/pkg/provisioner/ironic/provision_test.go
+++ b/pkg/provisioner/ironic/provision_test.go
@@ -744,8 +744,10 @@ func TestGetUpdateOptsForNodeWithRootHints(t *testing.T) {
 		t.Run(e.Path, func(t *testing.T) {
 			t.Logf("expected: %v", e)
 			var update nodes.UpdateOperation
+			var ok bool
 			for _, patch := range patches {
-				update = patch.(nodes.UpdateOperation)
+				update, ok = patch.(nodes.UpdateOperation)
+				require.True(t, ok, "expected patch to be UpdateOperation")
 				if update.Path == e.Path {
 					break
 				}
@@ -846,8 +848,10 @@ func TestGetUpdateOptsForNodeVirtual(t *testing.T) {
 		t.Run(e.Path, func(t *testing.T) {
 			t.Logf("expected: %v", e)
 			var update nodes.UpdateOperation
+			var ok bool
 			for _, patch := range patches {
-				update = patch.(nodes.UpdateOperation)
+				update, ok = patch.(nodes.UpdateOperation)
+				require.True(t, ok, "expected patch to be UpdateOperation")
 				if update.Path == e.Path {
 					break
 				}
@@ -940,8 +944,10 @@ func TestGetUpdateOptsForNodeDell(t *testing.T) {
 		t.Run(e.Path, func(t *testing.T) {
 			t.Logf("expected: %v", e)
 			var update nodes.UpdateOperation
+			var ok bool
 			for _, patch := range patches {
-				update = patch.(nodes.UpdateOperation)
+				update, ok = patch.(nodes.UpdateOperation)
+				require.True(t, ok, "expected patch to be UpdateOperation")
 				if update.Path == e.Path {
 					break
 				}
@@ -1001,8 +1007,10 @@ func TestGetUpdateOptsForNodeLiveIso(t *testing.T) {
 		t.Run(e.Path, func(t *testing.T) {
 			t.Logf("expected: %v", e)
 			var update nodes.UpdateOperation
+			var ok bool
 			for _, patch := range patches {
-				update = patch.(nodes.UpdateOperation)
+				update, ok = patch.(nodes.UpdateOperation)
+				require.True(t, ok, "expected patch to be UpdateOperation")
 				if update.Path == e.Path {
 					break
 				}
@@ -1076,8 +1084,10 @@ func TestGetUpdateOptsForNodeImageToLiveIso(t *testing.T) {
 		t.Run(e.Path, func(t *testing.T) {
 			t.Logf("expected: %v", e)
 			var update nodes.UpdateOperation
+			var ok bool
 			for _, patch := range patches {
-				update = patch.(nodes.UpdateOperation)
+				update, ok = patch.(nodes.UpdateOperation)
+				require.True(t, ok, "expected patch to be UpdateOperation")
 				if update.Path == e.Path {
 					break
 				}
@@ -1155,8 +1165,10 @@ func TestGetUpdateOptsForNodeLiveIsoToImage(t *testing.T) {
 		t.Run(e.Path, func(t *testing.T) {
 			t.Logf("expected: %v", e)
 			var update nodes.UpdateOperation
+			var ok bool
 			for _, patch := range patches {
-				update = patch.(nodes.UpdateOperation)
+				update, ok = patch.(nodes.UpdateOperation)
+				require.True(t, ok, "expected patch to be UpdateOperation")
 				if update.Path == e.Path {
 					break
 				}
@@ -1213,8 +1225,10 @@ func TestGetUpdateOptsForNodeCustomDeploy(t *testing.T) {
 		t.Run(e.Path, func(t *testing.T) {
 			t.Logf("expected: %v", e)
 			var update nodes.UpdateOperation
+			var ok bool
 			for _, patch := range patches {
-				update = patch.(nodes.UpdateOperation)
+				update, ok = patch.(nodes.UpdateOperation)
+				require.True(t, ok, "expected patch to be UpdateOperation")
 				if update.Path == e.Path {
 					break
 				}
@@ -1274,8 +1288,10 @@ func TestGetUpdateOptsForNodeCustomDeployWithImage(t *testing.T) {
 		t.Run(e.Path, func(t *testing.T) {
 			t.Logf("expected: %v", e)
 			var update nodes.UpdateOperation
+			var ok bool
 			for _, patch := range patches {
-				update = patch.(nodes.UpdateOperation)
+				update, ok = patch.(nodes.UpdateOperation)
+				require.True(t, ok, "expected patch to be UpdateOperation")
 				if update.Path == e.Path {
 					break
 				}
@@ -1345,8 +1361,10 @@ func TestGetUpdateOptsForNodeImageToCustomDeploy(t *testing.T) {
 		t.Run(e.Path, func(t *testing.T) {
 			t.Logf("expected: %v", e)
 			var update nodes.UpdateOperation
+			var ok bool
 			for _, patch := range patches {
-				update = patch.(nodes.UpdateOperation)
+				update, ok = patch.(nodes.UpdateOperation)
+				require.True(t, ok, "expected patch to be UpdateOperation")
 				if update.Path == e.Path {
 					break
 				}
@@ -1430,8 +1448,10 @@ func TestGetUpdateOptsForNodeSecureBoot(t *testing.T) {
 		t.Run(e.Path, func(t *testing.T) {
 			t.Logf("expected: %v", e)
 			var update nodes.UpdateOperation
+			var ok bool
 			for _, patch := range patches {
-				update = patch.(nodes.UpdateOperation)
+				update, ok = patch.(nodes.UpdateOperation)
+				require.True(t, ok, "expected patch to be UpdateOperation")
 				if update.Path == e.Path {
 					break
 				}
diff --git a/pkg/provisioner/ironic/register_test.go b/pkg/provisioner/ironic/register_test.go
index e6c302b5..bc9aab1c 100644
--- a/pkg/provisioner/ironic/register_test.go
+++ b/pkg/provisioner/ironic/register_test.go
@@ -655,7 +655,8 @@ func TestRegisterNewCredentials(t *testing.T) {
 
 	updates := ironic.GetLastNodeUpdateRequestFor("uuid")
 	assert.Equal(t, "/driver_info", updates[0].Path)
-	newValues := updates[0].Value.(map[string]interface{})
+	newValues, ok := updates[0].Value.(map[string]interface{})
+	require.True(t, ok, "expected to be a map")
 	assert.Equal(t, "test.bmc", newValues["test_address"])
 }
 
@@ -846,6 +847,28 @@ func TestRegisterUnsupportedSecureBoot(t *testing.T) {
 	assert.Contains(t, result.ErrorMessage, "does not support secure boot")
 }
 
+func TestRegisterUnsupportedDriverWithoutProvNet(t *testing.T) {
+	host := makeHost()
+	host.Status.Provisioning.ID = "" // so we don't lookup by uuid
+
+	ironic := testserver.NewIronic(t).NoNode("myns" + nameSeparator + host.Name).NoNode(host.Name)
+	ironic.Start()
+	defer ironic.Stop()
+
+	auth := clients.AuthConfig{Type: clients.NoAuth}
+	prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, nil, ironic.Endpoint(), auth)
+	if err != nil {
+		t.Fatalf("could not create provisioner: %s", err)
+	}
+	prov.config.provNetDisabled = true
+
+	result, _, err := prov.Register(provisioner.ManagementAccessData{}, false, false)
+	if err != nil {
+		t.Fatalf("error from Register: %s", err)
+	}
+	assert.Contains(t, result.ErrorMessage, "requires a provisioning network")
+}
+
 func TestRegisterNoBMCDetails(t *testing.T) {
 	ironic := testserver.NewIronic(t)
 	ironic.Start()
@@ -940,7 +963,8 @@ func TestRegisterUpdateBMCAddressIP(t *testing.T) {
 
 	updates := ironic.GetLastNodeUpdateRequestFor("uuid")
 	assert.Equal(t, "/driver_info", updates[0].Path)
-	newValues := updates[0].Value.(map[string]interface{})
+	newValues, ok := updates[0].Value.(map[string]interface{})
+	require.True(t, ok, "expected to be a map")
 	assert.Equal(t, "192.168.122.10", newValues["ipmi_address"])
 }
 
@@ -1001,7 +1025,8 @@ func TestRegisterUpdateBMCAddressProtocol(t *testing.T) {
 
 	updates := ironic.GetLastNodeUpdateRequestFor("uuid")
 	assert.Equal(t, "/driver_info", updates[0].Path)
-	newValues := updates[0].Value.(map[string]interface{})
+	newValues, ok := updates[0].Value.(map[string]interface{})
+	require.True(t, ok, "expected to be a map")
 	assert.Equal(t, "https://192.168.122.1:623", newValues["redfish_address"])
 }
 
diff --git a/test/e2e/README.md b/test/e2e/README.md
index 5db5f98b..7f6fa1a4 100644
--- a/test/e2e/README.md
+++ b/test/e2e/README.md
@@ -8,11 +8,12 @@ be reusable so that they can run also on real hardware. This is accomplished by
 tuning the tests based on a configuration file and flags. Examples can be seen
 in `config`.
 
-In the CI system we set up a pre-configured minikube cluster that is then used
-for the tests, as seen in [ci-e2e.sh](../../hack/ci-e2e.sh). This allows us to
-have control over the network and have a static configuration for Ironic and the
-Baremetal Operator. The script creates a VM to represent a BareMetalHost, and
-configures either VBMC or sushy-tools to be used as BMC.
+In the CI system we use a script ([ci-e2e.sh](../../hack/ci-e2e.sh)) to make
+some preparations before running the test suite. The script creates VMs to
+represent BareMetalHosts, and configures either VBMC or sushy-tools to be used
+as BMC. Ironic runs in the "host network" of the kind cluster in the test. The
+kind cluster is then configured to expose the relevant ports on the actual host
+so that they can be reached from the BareMetalHost VMs.
 
 Currently there are two sets of tests, which cannot be ran together in the same
 cluster. One is the "optional" set, currently consists of only the
diff --git a/test/e2e/common.go b/test/e2e/common.go
index d4af7504..69fc5c11 100644
--- a/test/e2e/common.go
+++ b/test/e2e/common.go
@@ -11,6 +11,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
+	"log"
 	"net"
 	"net/http"
 	"os"
@@ -444,10 +445,10 @@ func KubectlDelete(ctx context.Context, kubeconfigPath string, resources []byte,
 		testexec.WithStdin(rbytes),
 	)
 
-	fmt.Printf("Running kubectl %s\n", strings.Join(aargs, " "))
+	log.Printf("Running kubectl %s\n", strings.Join(aargs, " "))
 	stdout, stderr, err := deleteCmd.Run(ctx)
-	fmt.Printf("stderr:\n%s\n", string(stderr))
-	fmt.Printf("stdout:\n%s\n", string(stdout))
+	log.Printf("stderr:\n%s\n", string(stderr))
+	log.Printf("stdout:\n%s\n", string(stdout))
 	return err
 }
 
diff --git a/test/e2e/config/fixture.yaml b/test/e2e/config/fixture.yaml
index 5722e637..354f4cca 100644
--- a/test/e2e/config/fixture.yaml
+++ b/test/e2e/config/fixture.yaml
@@ -3,12 +3,12 @@ images:
 - name: quay.io/metal3-io/baremetal-operator:e2e
   loadBehavior: tryLoad
 # Save some time and network by using cached images if available
-- name: quay.io/metal3-io/baremetal-operator:release-0.6
-  loadBehavior: tryLoad
 - name: quay.io/metal3-io/baremetal-operator:release-0.8
   loadBehavior: tryLoad
 - name: quay.io/metal3-io/baremetal-operator:release-0.9
   loadBehavior: tryLoad
+- name: quay.io/metal3-io/baremetal-operator:release-0.10
+  loadBehavior: tryLoad
 - name: quay.io/jetstack/cert-manager-cainjector:v1.17.1
   loadBehavior: tryLoad
 - name: quay.io/jetstack/cert-manager-webhook:v1.17.1
@@ -60,16 +60,16 @@ intervals:
 bmoIronicUpgradeSpecs:
 - deployIronic: false
   deployBMO: true
-  initBMOKustomization: "../../config/overlays/fixture-release-0.6"
+  initBMOKustomization: "../../config/overlays/fixture-release-0.8"
   upgradeEntityKustomization: "../../config/overlays/fixture"
   upgradeEntityName: "bmo"
 - deployIronic: false
   deployBMO: true
-  initBMOKustomization: "../../config/overlays/fixture-release-0.8"
+  initBMOKustomization: "../../config/overlays/fixture-release-0.9"
   upgradeEntityKustomization: "../../config/overlays/fixture"
   upgradeEntityName: "bmo"
 - deployIronic: false
   deployBMO: true
-  initBMOKustomization: "../../config/overlays/fixture-release-0.9"
+  initBMOKustomization: "../../config/overlays/fixture-release-0.10"
   upgradeEntityKustomization: "../../config/overlays/fixture"
   upgradeEntityName: "bmo"
diff --git a/test/e2e/config/ironic.yaml b/test/e2e/config/ironic.yaml
index 84ae1a63..0caa8edd 100644
--- a/test/e2e/config/ironic.yaml
+++ b/test/e2e/config/ironic.yaml
@@ -5,12 +5,12 @@ images:
 # - name: quay.io/metal3-io/ironic:local
 #   loadBehavior: tryLoad
 # Save some time and network by using cached images if available
-- name: quay.io/metal3-io/baremetal-operator:release-0.6
-  loadBehavior: tryLoad
 - name: quay.io/metal3-io/baremetal-operator:release-0.8
   loadBehavior: tryLoad
 - name: quay.io/metal3-io/baremetal-operator:release-0.9
   loadBehavior: tryLoad
+- name: quay.io/metal3-io/baremetal-operator:release-0.10
+  loadBehavior: tryLoad
 - name: quay.io/jetstack/cert-manager-cainjector:v1.17.1
   loadBehavior: tryLoad
 - name: quay.io/jetstack/cert-manager-webhook:v1.17.1
@@ -86,45 +86,51 @@ kindExtraPortMappings:
 
 
 bmoIronicUpgradeSpecs:
-# Upgrade BMO 0.6 -> main
+# Upgrade BMO 0.8 -> main
 - deployIronic: true
   deployBMO: true
-  initBMOKustomization: "../../config/overlays/e2e-release-0.6"
+  initBMOKustomization: "../../config/overlays/e2e-release-0.8"
   initIronicKustomization: "../../ironic-deployment/overlays/e2e"
   upgradeEntityKustomization: "../../config/overlays/e2e"
   upgradeEntityName: "bmo"
-# Upgrade BMO 0.8 -> main
+# Upgrade BMO 0.9 -> main
 - deployIronic: true
   deployBMO: true
-  initBMOKustomization: "../../config/overlays/e2e-release-0.8"
+  initBMOKustomization: "../../config/overlays/e2e-release-0.9"
   initIronicKustomization: "../../ironic-deployment/overlays/e2e"
   upgradeEntityKustomization: "../../config/overlays/e2e"
   upgradeEntityName: "bmo"
-# Upgrade BMO 0.9 -> main
+# Upgrade BMO 0.10 -> main
 - deployIronic: true
   deployBMO: true
-  initBMOKustomization: "../../config/overlays/e2e-release-0.9"
+  initBMOKustomization: "../../config/overlays/e2e-release-0.10"
   initIronicKustomization: "../../ironic-deployment/overlays/e2e"
   upgradeEntityKustomization: "../../config/overlays/e2e"
   upgradeEntityName: "bmo"
-# Upgrade Ironic 24.1 -> latest | BMO 0.9
+# Upgrade Ironic 26.0 -> latest | BMO 0.9
 - deployIronic: true
   deployBMO: true
   initBMOKustomization: "../../config/overlays/e2e-release-0.9"
-  initIronicKustomization: "../../ironic-deployment/overlays/e2e-release-24.1"
+  initIronicKustomization: "../../ironic-deployment/overlays/e2e-release-26.0"
   upgradeEntityKustomization: "../../ironic-deployment/overlays/e2e"
   upgradeEntityName: "ironic"
-# Upgrade Ironic 25.0 -> latest | BMO 0.9
+# Upgrade Ironic 27.0 -> latest | BMO 0.9
 - deployIronic: true
   deployBMO: true
   initBMOKustomization: "../../config/overlays/e2e-release-0.9"
-  initIronicKustomization: "../../ironic-deployment/overlays/e2e-release-25.0"
+  initIronicKustomization: "../../ironic-deployment/overlays/e2e-release-27.0"
   upgradeEntityKustomization: "../../ironic-deployment/overlays/e2e"
   upgradeEntityName: "ironic"
-# Upgrade Ironic 26.0 -> latest | BMO 0.9
+# Upgrade Ironic 28.0 -> latest | BMO 0.10
 - deployIronic: true
   deployBMO: true
-  initBMOKustomization: "../../config/overlays/e2e-release-0.9"
-  initIronicKustomization: "../../ironic-deployment/overlays/e2e-release-26.0"
+  initBMOKustomization: "../../config/overlays/e2e-release-0.10"
+  initIronicKustomization: "../../ironic-deployment/overlays/e2e-release-28.0"
+  upgradeEntityKustomization: "../../ironic-deployment/overlays/e2e"
+# Upgrade Ironic 29.0 -> latest | BMO 0.10
+- deployIronic: true
+  deployBMO: true
+  initBMOKustomization: "../../config/overlays/e2e-release-0.10"
+  initIronicKustomization: "../../ironic-deployment/overlays/e2e-release-29.0"
   upgradeEntityKustomization: "../../ironic-deployment/overlays/e2e"
   upgradeEntityName: "ironic"
diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go
index 19fe4c59..fe787ec0 100644
--- a/test/e2e/e2e_suite_test.go
+++ b/test/e2e/e2e_suite_test.go
@@ -8,6 +8,7 @@ import (
 	"encoding/json"
 	"flag"
 	"fmt"
+	"log"
 	"os"
 	"os/exec"
 	"path/filepath"
@@ -242,7 +243,7 @@ var _ = SynchronizedBeforeSuite(func() []byte {
 			cmd := exec.Command("kubectl", "get", "service", metricsServiceName, "-n", namespace)
 			output, err := cmd.CombinedOutput()
 			if err != nil {
-				fmt.Printf("Service check output: %s\n", string(output))
+				log.Printf("Service check output: %s\n", string(output))
 				return err
 			}
 			return nil
diff --git a/test/go.mod b/test/go.mod
index c18bc22c..450d177a 100644
--- a/test/go.mod
+++ b/test/go.mod
@@ -12,16 +12,16 @@ require (
 	github.com/pkg/errors v0.9.1
 	golang.org/x/crypto v0.38.0
 	gopkg.in/yaml.v2 v2.4.0
-	k8s.io/api v0.32.4
-	k8s.io/apiextensions-apiserver v0.32.4
-	k8s.io/apimachinery v0.32.4
+	k8s.io/api v0.32.5
+	k8s.io/apiextensions-apiserver v0.32.5
+	k8s.io/apimachinery v0.32.5
 	k8s.io/klog/v2 v2.130.1
 	k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738
 	libvirt.org/go/libvirt v1.11002.0
 	sigs.k8s.io/cluster-api v1.10.2
 	sigs.k8s.io/cluster-api/test v1.10.2
 	sigs.k8s.io/controller-runtime v0.20.4
-	sigs.k8s.io/kind v0.27.0
+	sigs.k8s.io/kind v0.29.0
 	sigs.k8s.io/kustomize/api v0.19.0
 	sigs.k8s.io/kustomize/kyaml v0.19.0
 )
@@ -76,7 +76,6 @@ require (
 	github.com/google/go-querystring v1.1.0 // indirect
 	github.com/google/gofuzz v1.2.0 // indirect
 	github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
-	github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2 // indirect
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
@@ -148,10 +147,10 @@ require (
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	gotest.tools/v3 v3.5.0 // indirect
-	k8s.io/apiserver v0.32.4 // indirect
-	k8s.io/client-go v0.32.4 // indirect
+	k8s.io/apiserver v0.32.5 // indirect
+	k8s.io/client-go v0.32.5 // indirect
 	k8s.io/cluster-bootstrap v0.32.3 // indirect
-	k8s.io/component-base v0.32.4 // indirect
+	k8s.io/component-base v0.32.5 // indirect
 	k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 // indirect
 	sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 // indirect
 	sigs.k8s.io/gateway-api v1.1.0 // indirect
diff --git a/test/go.sum b/test/go.sum
index 31f9e2a3..93b76756 100644
--- a/test/go.sum
+++ b/test/go.sum
@@ -129,8 +129,6 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
 github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
 github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
-github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2 h1:SJ+NtwL6QaZ21U+IrK7d0gGgpjGGvd2kz+FzTHVzdqI=
-github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2/go.mod h1:Tv1PlzqC9t8wNnpPdctvtSUOPUUg4SHeE6vR1Ir2hmg=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -378,20 +376,20 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY=
 gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
-k8s.io/api v0.32.4 h1:kw8Y/G8E7EpNy7gjB8gJZl3KJkNz8HM2YHrZPtAZsF4=
-k8s.io/api v0.32.4/go.mod h1:5MYFvLvweRhyKylM3Es/6uh/5hGp0dg82vP34KifX4g=
-k8s.io/apiextensions-apiserver v0.32.4 h1:IA+CoR63UDOijR/vEpow6wQnX4V6iVpzazJBskHrpHE=
-k8s.io/apiextensions-apiserver v0.32.4/go.mod h1:Y06XO/b92H8ymOdG1HlA1submf7gIhbEDc3RjriqZOs=
-k8s.io/apimachinery v0.32.4 h1:8EEksaxA7nd7xWJkkwLDN4SvWS5ot9g6Z/VZb3ju25I=
-k8s.io/apimachinery v0.32.4/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
-k8s.io/apiserver v0.32.4 h1:Yf7sd/y+GOQKH1Qf6wUeayZrYXe2SKZ17Bcq7VQM5HQ=
-k8s.io/apiserver v0.32.4/go.mod h1:JFUMNtE2M5yqLZpIsgCb06SkVSW1YcxW1oyLSTfjXR8=
-k8s.io/client-go v0.32.4 h1:zaGJS7xoYOYumoWIFXlcVrsiYioRPrXGO7dBfVC5R6M=
-k8s.io/client-go v0.32.4/go.mod h1:k0jftcyYnEtwlFW92xC7MTtFv5BNcZBr+zn9jPlT9Ic=
+k8s.io/api v0.32.5 h1:uqjjsYo1kTJr5NIcoIaP9F+TgXgADH7nKQx91FDAhtk=
+k8s.io/api v0.32.5/go.mod h1:bXXFU3fGCZ/eFMZvfHZC69PeGbXEL4zzjuPVzOxHF64=
+k8s.io/apiextensions-apiserver v0.32.5 h1:o0aKvmzIIs8Uk54pidk32pxET+Pg2ULnh9WI1PuKTwE=
+k8s.io/apiextensions-apiserver v0.32.5/go.mod h1:5fpedJa3HJJFBukAZ6ur91DEDye5gYuXISPbOiNLYpU=
+k8s.io/apimachinery v0.32.5 h1:6We3aJ6crC0ap8EhsEXcgX3LpI6SEjubpiOMXLROwPM=
+k8s.io/apimachinery v0.32.5/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
+k8s.io/apiserver v0.32.5 h1:phmm2EOUVFI+cLiq8Grtuh166fTt/qgvkGPkpgzp5uY=
+k8s.io/apiserver v0.32.5/go.mod h1:5bfueS1tgARVWVXRJBMI5mHoCmev0jOvbxebai/kiqc=
+k8s.io/client-go v0.32.5 h1:huFmQMzgWu0z4kbWsuZci+Gt4Fo72I4CcrvhToZ/Qp0=
+k8s.io/client-go v0.32.5/go.mod h1:Qchw6f9WIVrur7DKojAHpRgGLcANT0RLIvF39Jz58xA=
 k8s.io/cluster-bootstrap v0.32.3 h1:AqIpsUhB6MUeaAsl1WvaUw54AHRd2hfZrESlKChtd8s=
 k8s.io/cluster-bootstrap v0.32.3/go.mod h1:CHbBwgOb6liDV6JFUTkx5t85T2xidy0sChBDoyYw344=
-k8s.io/component-base v0.32.4 h1:HuF+2JVLbFS5GODLIfPCb1Td6b+G2HszJoArcWOSr5I=
-k8s.io/component-base v0.32.4/go.mod h1:10KloJEYw1keU/Xmjfy9TKJqUq7J2mYdiD1VDXoco4o=
+k8s.io/component-base v0.32.5 h1:2HiX+m3s9Iz5CMqdCVDH2V942UqzQvjuhcXb4W+KCsg=
+k8s.io/component-base v0.32.5/go.mod h1:jDsPNFFElv9m27TcYxlpEX7TZ3vdgx2g4PaqMUHpV/Y=
 k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
 k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
 k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 h1:hcha5B1kVACrLujCKLbr8XWMxCxzQx42DY8QKYJrDLg=
@@ -412,8 +410,8 @@ sigs.k8s.io/gateway-api v1.1.0 h1:DsLDXCi6jR+Xz8/xd0Z1PYl2Pn0TyaFMOPPZIj4inDM=
 sigs.k8s.io/gateway-api v1.1.0/go.mod h1:ZH4lHrL2sDi0FHZ9jjneb8kKnGzFWyrTya35sWUTrRs=
 sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
 sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
-sigs.k8s.io/kind v0.27.0 h1:PQ3f0iAWNIj66LYkZ1ivhEg/+Zb6UPMbO+qVei/INZA=
-sigs.k8s.io/kind v0.27.0/go.mod h1:RZVFmy6qcwlSWwp6xeIUv7kXCPF3i8MXsEXxW/J+gJY=
+sigs.k8s.io/kind v0.29.0 h1:3TpCsyh908IkXXpcSnsMjWdwdWjIl7o9IMZImZCWFnI=
+sigs.k8s.io/kind v0.29.0/go.mod h1:ldWQisw2NYyM6k64o/tkZng/1qQW7OlzcN5a8geJX3o=
 sigs.k8s.io/kustomize/api v0.19.0 h1:F+2HB2mU1MSiR9Hp1NEgoU2q9ItNOaBJl0I4Dlus5SQ=
 sigs.k8s.io/kustomize/api v0.19.0/go.mod h1:/BbwnivGVcBh1r+8m3tH1VNxJmHSk1PzP5fkP6lbL1o=
 sigs.k8s.io/kustomize/kyaml v0.19.0 h1:RFge5qsO1uHhwJsu3ipV7RNolC7Uozc0jUBC/61XSlA=
diff --git a/test/vbmctl/main.go b/test/vbmctl/main.go
index 71f426b1..2c1da8db 100644
--- a/test/vbmctl/main.go
+++ b/test/vbmctl/main.go
@@ -7,7 +7,7 @@ import (
 	"bytes"
 	"embed"
 	"flag"
-	"fmt"
+	"log"
 	"os"
 	"text/template"
 
@@ -46,12 +46,12 @@ func CreateVolumePool(conn *libvirt.Connect, poolName, poolPath string) (*libvir
 	pool, err := conn.LookupStoragePoolByName(poolName)
 
 	if err == nil {
-		fmt.Println("Pool already exists")
+		log.Println("Pool already exists")
 		return pool, nil
 	}
 
 	if err = os.Mkdir(poolPath, filePerm777); err != nil && !os.IsExist(err) {
-		fmt.Println("Cannot determine the state of the poolPath")
+		log.Println("Cannot determine the state of the poolPath")
 		return nil, err
 	}
 
@@ -66,8 +66,8 @@ func CreateVolumePool(conn *libvirt.Connect, poolName, poolPath string) (*libvir
 	poolCfg, err := RenderTemplate("templates/pool.xml.tpl", data)
 
 	if err != nil {
-		fmt.Println("Failed to read pool XML file")
-		fmt.Printf("Error occurred: %v\n", err)
+		log.Println("Failed to read pool XML file")
+		log.Printf("Error occurred: %v\n", err)
 		return nil, err
 	}
 
@@ -75,24 +75,24 @@ func CreateVolumePool(conn *libvirt.Connect, poolName, poolPath string) (*libvir
 	pool, err = conn.StoragePoolDefineXML(poolCfg, 0)
 
 	if err != nil {
-		fmt.Println("Failed to create volume pool")
-		fmt.Printf("Error occurred: %v\n", err)
+		log.Println("Failed to create volume pool")
+		log.Printf("Error occurred: %v\n", err)
 		return nil, err
 	}
 
 	if err = pool.SetAutostart(true); err != nil {
-		fmt.Println("Failed to Set the pool autostart")
-		fmt.Printf("Error occurred: %v\n", err)
+		log.Println("Failed to Set the pool autostart")
+		log.Printf("Error occurred: %v\n", err)
 		return nil, err
 	}
 
 	if err = pool.Create(0); err != nil {
-		fmt.Println("Failed to Start the pool")
-		fmt.Printf("Error occurred: %v\n", err)
+		log.Println("Failed to Start the pool")
+		log.Printf("Error occurred: %v\n", err)
 		return nil, err
 	}
 
-	fmt.Println("Volume pool created successfully")
+	log.Println("Volume pool created successfully")
 	return pool, nil
 }
 
@@ -100,8 +100,8 @@ func CreateVolume(conn *libvirt.Connect, volumeName, poolName, poolPath string,
 	pool, err := CreateVolumePool(conn, poolName, poolPath)
 
 	if err != nil {
-		fmt.Println("Failed to create storage pool")
-		fmt.Printf("Error occurred: %v\n", err)
+		log.Println("Failed to create storage pool")
+		log.Printf("Error occurred: %v\n", err)
 		return err
 	}
 
@@ -116,8 +116,8 @@ func CreateVolume(conn *libvirt.Connect, volumeName, poolName, poolPath string,
 	volumeCfg, err := RenderTemplate("templates/volume.xml.tpl", data)
 
 	if err != nil {
-		fmt.Println("Failed to read volume XML file")
-		fmt.Printf("Error occurred: %v\n", err)
+		log.Println("Failed to read volume XML file")
+		log.Printf("Error occurred: %v\n", err)
 		return err
 	}
 
@@ -125,12 +125,12 @@ func CreateVolume(conn *libvirt.Connect, volumeName, poolName, poolPath string,
 	_, err = pool.StorageVolCreateXML(volumeCfg, 0)
 
 	if err != nil {
-		fmt.Println("Failed to create volume")
-		fmt.Printf("Error occurred: %v\n", err)
+		log.Println("Failed to create volume")
+		log.Printf("Error occurred: %v\n", err)
 		return err
 	}
 
-	fmt.Println("Volume created successfully")
+	log.Println("Volume created successfully")
 	return nil
 }
 
@@ -152,8 +152,8 @@ func CreateLibvirtVM(conn *libvirt.Connect, name, networkName, macAddress string
 	err := qcow2.Blk_Create("/tmp/"+name+".qcow2", opts)
 
 	if err != nil {
-		fmt.Println("Failed to create qcow2 file")
-		fmt.Printf("Error occurred: %v\n", err)
+		log.Println("Failed to create qcow2 file")
+		log.Printf("Error occurred: %v\n", err)
 		return err
 	}
 
@@ -182,12 +182,12 @@ func CreateLibvirtVM(conn *libvirt.Connect, name, networkName, macAddress string
 	_, err = conn.DomainDefineXML(vmCfg)
 
 	if err != nil {
-		fmt.Println("Failed to define domain")
-		fmt.Printf("Error occurred: %v\n", err)
+		log.Println("Failed to define domain")
+		log.Printf("Error occurred: %v\n", err)
 		return err
 	}
 
-	fmt.Println("Domain created successfully")
+	log.Println("Domain created successfully")
 	return nil
 }
 
@@ -223,8 +223,8 @@ func CreateLibvirtVMWithReservedIPAddress(conn *libvirt.Connect, macAddress, nam
 	err = xmlTpl.Execute(&buf, data)
 
 	if err != nil {
-		fmt.Println("Failed to create BMC")
-		fmt.Printf("Error occurred: %v\n", err)
+		log.Println("Failed to create BMC")
+		log.Printf("Error occurred: %v\n", err)
 		return err
 	}
 
@@ -235,11 +235,11 @@ func CreateLibvirtVMWithReservedIPAddress(conn *libvirt.Connect, macAddress, nam
 		buf.String(),
 		libvirt.NETWORK_UPDATE_AFFECT_LIVE|libvirt.NETWORK_UPDATE_AFFECT_CONFIG,
 	); err != nil {
-		fmt.Printf("Error occurred: %v\n", err)
+		log.Printf("Error occurred: %v\n", err)
 		return err
 	}
 	if err = CreateLibvirtVM(conn, name, networkName, macAddress); err != nil {
-		fmt.Printf("Error occurred: %v\n", err)
+		log.Printf("Error occurred: %v\n", err)
 		return err
 	}
 	return nil
@@ -270,22 +270,20 @@ func main() {
 	} else {
 		bmcs, err = bmoe2e.LoadBMCConfig(*configFile)
 		if err != nil {
-			fmt.Printf("Error occurred: %v\n", err)
-			os.Exit(1)
+			log.Fatalf("Error occurred: %v\n", err)
 		}
 	}
 
 	// Connect to Libvirt
 	conn, err := libvirt.NewConnect("qemu:///system")
 	if err != nil {
-		fmt.Printf("Error occurred: %v\n", err)
-		os.Exit(1)
+		log.Fatalf("Error occurred: %v\n", err)
 	}
 	defer conn.Close()
 
 	for _, bmc := range bmcs {
 		if err = CreateLibvirtVMWithReservedIPAddress(conn, bmc.BootMacAddress, bmc.Name, bmc.IPAddress, "baremetal-e2e"); err != nil {
-			fmt.Printf("Error occurred: %v\n", err)
+			log.Printf("Error occurred: %v\n", err)
 			// Not using os.Exit here so that we still close the connection
 			break
 		}