Repository: metal3-io/baremetal-operator (go)

Issue:
Test live-ISO i e2e
**User Story**

As a developer I would like to ensure that virtuamedia/live-ISO provisioning works by testing it in the e2e tests.

**Detailed Description**

Check [this](https://github.com/metal3-io/cluster-api-provider-metal3/blob/main/test/e2e/live_iso_test.go) for how it was implemented in CAPM3. This test makes more sense in BMO and we should implement it here. (Afterwards we can discuss if we should also remove it from CAPM3.)
My hope is that we can implement this test similar to how we already support ipmi and redfish by just changing the BMC details, but I suspect there is more to it. At least we need an ISO...

/kind feature


Pull request description:
🌱E2e live iso ssh implementation
Fixes #1429


Patch:
diff --git a/go.mod b/go.mod
index 69f0b396..c4b8dfba 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.20
 require (
 	github.com/go-logr/logr v1.3.0
 	github.com/google/safetext v0.0.0-20230106111101-7156a760e523
-	github.com/gophercloud/gophercloud v1.5.1-0.20231117122435-08456f7fe42e
+	github.com/gophercloud/gophercloud v1.5.1-0.20231106162611-af1813efe0d1
 	github.com/metal3-io/baremetal-operator/apis v0.4.0
 	github.com/metal3-io/baremetal-operator/pkg/hardwareutils v0.4.0
 	github.com/onsi/gomega v1.30.0
@@ -62,7 +62,7 @@ require (
 	golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
 	golang.org/x/net v0.17.0 // indirect
 	golang.org/x/oauth2 v0.8.0 // indirect
-	golang.org/x/sys v0.14.0 // indirect
+	golang.org/x/sys v0.13.0 // indirect
 	golang.org/x/term v0.13.0 // indirect
 	golang.org/x/text v0.13.0 // indirect
 	golang.org/x/time v0.3.0 // indirect
diff --git a/go.sum b/go.sum
index c470351e..6571f6ed 100644
--- a/go.sum
+++ b/go.sum
@@ -73,8 +73,8 @@ github.com/google/safetext v0.0.0-20230106111101-7156a760e523 h1:i4NsbmB9pD5+Ggp
 github.com/google/safetext v0.0.0-20230106111101-7156a760e523/go.mod h1:mJNEy0r5YPHC7ChQffpOszlGB4L1iqjXWpIEKcFpr9s=
 github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
 github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/gophercloud/gophercloud v1.5.1-0.20231117122435-08456f7fe42e h1:acHzJUXVfS+GImmi+yyM2+AOiaO94Z4KaTTFVU6Ov9U=
-github.com/gophercloud/gophercloud v1.5.1-0.20231117122435-08456f7fe42e/go.mod h1:ecnaHbFxBunc2UeAONDQbkQacv6WP1syeZ02TgAB7kA=
+github.com/gophercloud/gophercloud v1.5.1-0.20231106162611-af1813efe0d1 h1:4crYiUFYzhxv60/bsLnyc7esUsOqQl3DgErAGNsOgL8=
+github.com/gophercloud/gophercloud v1.5.1-0.20231106162611-af1813efe0d1/go.mod h1:99P1GdytPKaSR7/ugQJo1dX6th5FlHdXXvKqvRJQfO4=
 github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
 github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
 github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
@@ -197,8 +197,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
-golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
 golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
diff --git a/pkg/hardwareutils/bmc/access.go b/pkg/hardwareutils/bmc/access.go
index f00770f6..960b9eff 100644
--- a/pkg/hardwareutils/bmc/access.go
+++ b/pkg/hardwareutils/bmc/access.go
@@ -74,9 +74,6 @@ type AccessDetails interface {
 	RAIDInterface() string
 	VendorInterface() string
 
-	// Firmware interface to set
-	FirmwareInterface() string
-
 	// Whether the driver supports changing secure boot state.
 	SupportsSecureBoot() bool
 
diff --git a/pkg/hardwareutils/bmc/ibmc.go b/pkg/hardwareutils/bmc/ibmc.go
index 01e85d8d..7cb4b9af 100644
--- a/pkg/hardwareutils/bmc/ibmc.go
+++ b/pkg/hardwareutils/bmc/ibmc.go
@@ -87,10 +87,6 @@ func (a *ibmcAccessDetails) BootInterface() string {
 	return "ipxe"
 }
 
-func (a *ibmcAccessDetails) FirmwareInterface() string {
-	return ""
-}
-
 func (a *ibmcAccessDetails) ManagementInterface() string {
 	return "ibmc"
 }
diff --git a/pkg/hardwareutils/bmc/idrac.go b/pkg/hardwareutils/bmc/idrac.go
index bddc0267..b7b8cb84 100644
--- a/pkg/hardwareutils/bmc/idrac.go
+++ b/pkg/hardwareutils/bmc/idrac.go
@@ -82,10 +82,6 @@ func (a *iDracAccessDetails) BootInterface() string {
 	return "ipxe"
 }
 
-func (a *iDracAccessDetails) FirmwareInterface() string {
-	return ""
-}
-
 func (a *iDracAccessDetails) ManagementInterface() string {
 	return ""
 }
diff --git a/pkg/hardwareutils/bmc/idrac_virtualmedia.go b/pkg/hardwareutils/bmc/idrac_virtualmedia.go
index 79ffa57e..a30a2a20 100644
--- a/pkg/hardwareutils/bmc/idrac_virtualmedia.go
+++ b/pkg/hardwareutils/bmc/idrac_virtualmedia.go
@@ -76,10 +76,6 @@ func (a *redfishiDracVirtualMediaAccessDetails) BootInterface() string {
 	return "idrac-redfish-virtual-media"
 }
 
-func (a *redfishiDracVirtualMediaAccessDetails) FirmwareInterface() string {
-	return "redfish"
-}
-
 func (a *redfishiDracVirtualMediaAccessDetails) ManagementInterface() string {
 	return "idrac-redfish"
 }
diff --git a/pkg/hardwareutils/bmc/ilo4.go b/pkg/hardwareutils/bmc/ilo4.go
index 6ccc68b1..bff98bc6 100644
--- a/pkg/hardwareutils/bmc/ilo4.go
+++ b/pkg/hardwareutils/bmc/ilo4.go
@@ -94,10 +94,6 @@ func (a *iLOAccessDetails) BootInterface() string {
 	}
 }
 
-func (a *iLOAccessDetails) FirmwareInterface() string {
-	return ""
-}
-
 func (a *iLOAccessDetails) ManagementInterface() string {
 	return ""
 }
diff --git a/pkg/hardwareutils/bmc/ilo5.go b/pkg/hardwareutils/bmc/ilo5.go
index e79bf442..2cfbee1f 100644
--- a/pkg/hardwareutils/bmc/ilo5.go
+++ b/pkg/hardwareutils/bmc/ilo5.go
@@ -78,10 +78,6 @@ func (a *iLO5AccessDetails) BootInterface() string {
 	return "ilo-ipxe"
 }
 
-func (a *iLO5AccessDetails) FirmwareInterface() string {
-	return ""
-}
-
 func (a *iLO5AccessDetails) ManagementInterface() string {
 	return ""
 }
diff --git a/pkg/hardwareutils/bmc/ipmi.go b/pkg/hardwareutils/bmc/ipmi.go
index eb30a35f..c4a29d00 100644
--- a/pkg/hardwareutils/bmc/ipmi.go
+++ b/pkg/hardwareutils/bmc/ipmi.go
@@ -97,10 +97,6 @@ func (a *ipmiAccessDetails) BootInterface() string {
 	return "ipxe"
 }
 
-func (a *ipmiAccessDetails) FirmwareInterface() string {
-	return ""
-}
-
 func (a *ipmiAccessDetails) ManagementInterface() string {
 	return ""
 }
diff --git a/pkg/hardwareutils/bmc/irmc.go b/pkg/hardwareutils/bmc/irmc.go
index f87c7147..a9c0b789 100644
--- a/pkg/hardwareutils/bmc/irmc.go
+++ b/pkg/hardwareutils/bmc/irmc.go
@@ -78,10 +78,6 @@ func (a *iRMCAccessDetails) BootInterface() string {
 	return "ipxe"
 }
 
-func (a *iRMCAccessDetails) FirmwareInterface() string {
-	return ""
-}
-
 func (a *iRMCAccessDetails) ManagementInterface() string {
 	return ""
 }
diff --git a/pkg/hardwareutils/bmc/redfish.go b/pkg/hardwareutils/bmc/redfish.go
index 7fccbf71..61db0848 100644
--- a/pkg/hardwareutils/bmc/redfish.go
+++ b/pkg/hardwareutils/bmc/redfish.go
@@ -107,10 +107,6 @@ func (a *redfishAccessDetails) BootInterface() string {
 	return "ipxe"
 }
 
-func (a *redfishAccessDetails) FirmwareInterface() string {
-	return "redfish"
-}
-
 func (a *redfishAccessDetails) ManagementInterface() string {
 	return ""
 }
@@ -159,10 +155,6 @@ func (a *redfishiDracAccessDetails) BootInterface() string {
 	return "ipxe"
 }
 
-func (a *redfishiDracAccessDetails) FirmwareInterface() string {
-	return "redfish"
-}
-
 func (a *redfishiDracAccessDetails) ManagementInterface() string {
 	return "idrac-redfish"
 }
diff --git a/pkg/hardwareutils/bmc/redfish_virtualmedia.go b/pkg/hardwareutils/bmc/redfish_virtualmedia.go
index 41a5f4b9..fa7a8b57 100644
--- a/pkg/hardwareutils/bmc/redfish_virtualmedia.go
+++ b/pkg/hardwareutils/bmc/redfish_virtualmedia.go
@@ -75,10 +75,6 @@ func (a *redfishVirtualMediaAccessDetails) BootInterface() string {
 	return "redfish-virtual-media"
 }
 
-func (a *redfishVirtualMediaAccessDetails) FirmwareInterface() string {
-	return "redfish"
-}
-
 func (a *redfishVirtualMediaAccessDetails) ManagementInterface() string {
 	return ""
 }
diff --git a/pkg/provisioner/ironic/clients/features.go b/pkg/provisioner/ironic/clients/features.go
index b98cc65c..46452d65 100644
--- a/pkg/provisioner/ironic/clients/features.go
+++ b/pkg/provisioner/ironic/clients/features.go
@@ -41,15 +41,15 @@ func (af AvailableFeatures) Log(logger logr.Logger) {
 	logger.Info("supported Ironic API features",
 		"maxVersion", fmt.Sprintf("1.%d", af.MaxVersion),
 		"chosenVersion", af.ChooseMicroversion(),
-		"firmwareUpdates", af.HasFirmwareUpdates())
+		"firmwareUpgrades", af.HasFirmwareUpgrades())
 }
 
-func (af AvailableFeatures) HasFirmwareUpdates() bool {
+func (af AvailableFeatures) HasFirmwareUpgrades() bool {
 	return af.MaxVersion >= 86
 }
 
 func (af AvailableFeatures) ChooseMicroversion() string {
-	if af.HasFirmwareUpdates() {
+	if af.HasFirmwareUpgrades() {
 		return "1.86"
 	}
 
diff --git a/pkg/provisioner/ironic/ironic.go b/pkg/provisioner/ironic/ironic.go
index 58eadceb..6d8d7e21 100644
--- a/pkg/provisioner/ironic/ironic.go
+++ b/pkg/provisioner/ironic/ironic.go
@@ -367,28 +367,24 @@ func (p *ironicProvisioner) ValidateManagementAccess(data provisioner.Management
 			return
 		}
 
-		nodeCreateOpts := nodes.CreateOpts{
-			Driver:              bmcAccess.Driver(),
-			BIOSInterface:       bmcAccess.BIOSInterface(),
-			BootInterface:       bmcAccess.BootInterface(),
-			Name:                ironicNodeName(p.objectMeta),
-			DriverInfo:          driverInfo,
-			DeployInterface:     p.deployInterface(data),
-			InspectInterface:    "inspector",
-			ManagementInterface: bmcAccess.ManagementInterface(),
-			PowerInterface:      bmcAccess.PowerInterface(),
-			RAIDInterface:       bmcAccess.RAIDInterface(),
-			VendorInterface:     bmcAccess.VendorInterface(),
-			Properties: map[string]interface{}{
-				"capabilities": bootModeCapabilities[data.BootMode],
-			},
-		}
-
-		if p.availableFeatures.HasFirmwareUpdates() {
-			nodeCreateOpts.FirmwareInterface = bmcAccess.FirmwareInterface()
-		}
-
-		ironicNode, err = nodes.Create(p.client, nodeCreateOpts).Extract()
+		ironicNode, err = nodes.Create(
+			p.client,
+			nodes.CreateOpts{
+				Driver:              bmcAccess.Driver(),
+				BIOSInterface:       bmcAccess.BIOSInterface(),
+				BootInterface:       bmcAccess.BootInterface(),
+				Name:                ironicNodeName(p.objectMeta),
+				DriverInfo:          driverInfo,
+				DeployInterface:     p.deployInterface(data),
+				InspectInterface:    "inspector",
+				ManagementInterface: bmcAccess.ManagementInterface(),
+				PowerInterface:      bmcAccess.PowerInterface(),
+				RAIDInterface:       bmcAccess.RAIDInterface(),
+				VendorInterface:     bmcAccess.VendorInterface(),
+				Properties: map[string]interface{}{
+					"capabilities": bootModeCapabilities[data.BootMode],
+				},
+			}).Extract()
 		switch err.(type) {
 		case nil:
 			p.publisher("Registered", "Registered new host")


Test patch:
diff --git a/hack/ci-e2e.sh b/hack/ci-e2e.sh
index 9bd9ac27..b2accf3b 100755
--- a/hack/ci-e2e.sh
+++ b/hack/ci-e2e.sh
@@ -115,7 +115,7 @@ IMAGE_DIR="${REPO_ROOT}/test/e2e/images"
 ## Download and run image server
 mkdir -p "${IMAGE_DIR}"
 pushd "${IMAGE_DIR}"
-wget --quiet "https://download.cirros-cloud.net/${CIRROS_VERSION}/${IMAGE_FILE}"
+wget --quiet https://artifactory.nordix.org/artifactory/metal3/images/iso/"${IMAGE_FILE}"
 popd
 
 docker run --name image-server-e2e -d \
diff --git a/hack/clean-e2e.sh b/hack/clean-e2e.sh
index 75c4a597..06d29ceb 100755
--- a/hack/clean-e2e.sh
+++ b/hack/clean-e2e.sh
@@ -8,10 +8,10 @@ docker rm -f vbmc
 docker rm -f image-server-e2e
 docker rm -f sushy-tools
 virsh -c qemu:///system destroy --domain bmo-e2e-0
-virsh -c qemu:///system undefine --domain bmo-e2e-0 --remove-all-storage
+virsh -c qemu:///system undefine --domain bmo-e2e-0 --nvram --remove-all-storage
 virsh -c qemu:///system net-destroy baremetal-e2e
 virsh -c qemu:///system net-undefine baremetal-e2e
 
-rm -rfv "${REPO_ROOT}/test/e2e/_artifacts"
-rm -rfv "${REPO_ROOT}/artifacts.tar.gz"
-rm -rfv "${REPO_ROOT}/test/e2e/images"
+rm -rf "${REPO_ROOT}/test/e2e/_artifacts"
+rm -rf "${REPO_ROOT}/artifacts.tar.gz"
+rm -rf "${REPO_ROOT}/test/e2e/images"
diff --git a/hack/e2e/net.xml b/hack/e2e/net.xml
index f64fd04f..54a2740b 100644
--- a/hack/e2e/net.xml
+++ b/hack/e2e/net.xml
@@ -9,7 +9,8 @@
   <ip address='192.168.222.1' netmask='255.255.255.0'>
     <dhcp>
       <range start='192.168.222.2' end='192.168.222.199'/>
-      <host mac='52:54:00:6c:3c:01' name='vm1' ip='192.168.222.199'/>
+      <host mac='52:54:00:6c:3c:01' name='minikube' ip='192.168.222.199'/>
+      <host mac='00:60:2f:31:81:01' name='bmo-e2e-0' ip='192.168.222.122'/>
       <bootp file='http://192.168.222.199:6180/boot.ipxe'/>
     </dhcp>
   </ip>
diff --git a/pkg/hardwareutils/bmc/access_test.go b/pkg/hardwareutils/bmc/access_test.go
index 2ed2a319..b0f5ac1a 100644
--- a/pkg/hardwareutils/bmc/access_test.go
+++ b/pkg/hardwareutils/bmc/access_test.go
@@ -464,7 +464,6 @@ func TestStaticDriverInfo(t *testing.T) {
 		driver     string
 		bios       string
 		boot       string
-		firmware   string
 		management string
 		power      string
 		vendor     string
@@ -476,7 +475,6 @@ func TestStaticDriverInfo(t *testing.T) {
 			driver:     "ipmi",
 			bios:       "",
 			boot:       "ipxe",
-			firmware:   "",
 			management: "",
 			power:      "",
 		},
@@ -488,7 +486,6 @@ func TestStaticDriverInfo(t *testing.T) {
 			driver:     "ipmi",
 			bios:       "",
 			boot:       "ipxe",
-			firmware:   "",
 			management: "",
 			power:      "",
 		},
@@ -500,7 +497,6 @@ func TestStaticDriverInfo(t *testing.T) {
 			driver:     "idrac",
 			bios:       "",
 			boot:       "ipxe",
-			firmware:   "",
 			management: "",
 			power:      "",
 		},
@@ -512,7 +508,6 @@ func TestStaticDriverInfo(t *testing.T) {
 			driver:     "irmc",
 			bios:       "",
 			boot:       "ipxe",
-			firmware:   "",
 			management: "",
 			power:      "",
 		},
@@ -524,7 +519,6 @@ func TestStaticDriverInfo(t *testing.T) {
 			driver:     "redfish",
 			bios:       "",
 			boot:       "ipxe",
-			firmware:   "redfish",
 			management: "",
 			power:      "",
 		},
@@ -536,7 +530,6 @@ func TestStaticDriverInfo(t *testing.T) {
 			driver:     "redfish",
 			bios:       "",
 			boot:       "redfish-virtual-media",
-			firmware:   "redfish",
 			management: "",
 			power:      "",
 		},
@@ -548,7 +541,6 @@ func TestStaticDriverInfo(t *testing.T) {
 			driver:     "redfish",
 			bios:       "",
 			boot:       "redfish-virtual-media",
-			firmware:   "redfish",
 			management: "",
 			power:      "",
 		},
@@ -560,7 +552,6 @@ func TestStaticDriverInfo(t *testing.T) {
 			driver:     "redfish",
 			bios:       "",
 			boot:       "redfish-virtual-media",
-			firmware:   "redfish",
 			management: "",
 			power:      "",
 		},
@@ -572,7 +563,6 @@ func TestStaticDriverInfo(t *testing.T) {
 			driver:     "idrac",
 			bios:       "idrac-redfish",
 			boot:       "ipxe",
-			firmware:   "redfish",
 			management: "idrac-redfish",
 			power:      "idrac-redfish",
 			vendor:     "idrac-redfish",
@@ -585,7 +575,6 @@ func TestStaticDriverInfo(t *testing.T) {
 			driver:   "redfish",
 			bios:     "",
 			boot:     "redfish-virtual-media",
-			firmware: "redfish",
 		},
 
 		{
@@ -595,7 +584,6 @@ func TestStaticDriverInfo(t *testing.T) {
 			driver:   "redfish",
 			bios:     "",
 			boot:     "redfish-virtual-media",
-			firmware: "redfish",
 		},
 
 		{
@@ -605,7 +593,6 @@ func TestStaticDriverInfo(t *testing.T) {
 			driver:   "redfish",
 			bios:     "",
 			boot:     "redfish-virtual-media",
-			firmware: "redfish",
 		},
 
 		{
@@ -615,7 +602,6 @@ func TestStaticDriverInfo(t *testing.T) {
 			driver:     "idrac",
 			bios:       "idrac-redfish",
 			boot:       "idrac-redfish-virtual-media",
-			firmware:   "redfish",
 			management: "idrac-redfish",
 			power:      "idrac-redfish",
 			vendor:     "idrac-redfish",
@@ -628,7 +614,6 @@ func TestStaticDriverInfo(t *testing.T) {
 			driver:     "idrac",
 			bios:       "idrac-redfish",
 			boot:       "idrac-redfish-virtual-media",
-			firmware:   "redfish",
 			management: "idrac-redfish",
 			power:      "idrac-redfish",
 			vendor:     "idrac-redfish",
@@ -641,7 +626,6 @@ func TestStaticDriverInfo(t *testing.T) {
 			driver:     "idrac",
 			bios:       "idrac-redfish",
 			boot:       "idrac-redfish-virtual-media",
-			firmware:   "redfish",
 			management: "idrac-redfish",
 			power:      "idrac-redfish",
 			vendor:     "idrac-redfish",
@@ -654,7 +638,6 @@ func TestStaticDriverInfo(t *testing.T) {
 			driver:     "ibmc",
 			bios:       "",
 			boot:       "ipxe",
-			firmware:   "",
 			management: "ibmc",
 			power:      "ibmc",
 		},
@@ -666,7 +649,6 @@ func TestStaticDriverInfo(t *testing.T) {
 			driver:     "ilo",
 			bios:       "",
 			boot:       "ilo-ipxe",
-			firmware:   "",
 			management: "",
 			power:      "",
 		},
@@ -678,7 +660,6 @@ func TestStaticDriverInfo(t *testing.T) {
 			driver:     "ilo",
 			bios:       "",
 			boot:       "ilo-virtual-media",
-			firmware:   "",
 			management: "",
 			power:      "",
 		},
@@ -690,7 +671,6 @@ func TestStaticDriverInfo(t *testing.T) {
 			driver:     "ilo5",
 			bios:       "",
 			boot:       "ilo-ipxe",
-			firmware:   "",
 			management: "",
 			power:      "",
 		},
@@ -714,10 +694,6 @@ func TestStaticDriverInfo(t *testing.T) {
 				t.Fatalf("Unexpected bios interface %q, expected %q",
 					acc.BIOSInterface(), tc.bios)
 			}
-			if acc.FirmwareInterface() != tc.firmware {
-				t.Fatalf("Unexpected firmware interface %q, expected %q",
-					acc.FirmwareInterface(), tc.firmware)
-			}
 			if acc.VendorInterface() != tc.vendor {
 				t.Fatalf("Unexpected vendor interface %q, expected %q",
 					acc.VendorInterface(), tc.vendor)
diff --git a/pkg/provisioner/ironic/prepare_test.go b/pkg/provisioner/ironic/prepare_test.go
index ca24f9ea..1ae1c5b7 100644
--- a/pkg/provisioner/ironic/prepare_test.go
+++ b/pkg/provisioner/ironic/prepare_test.go
@@ -25,7 +25,6 @@ func (r *RAIDTestBMC) DriverInfo(bmc.Credentials) (i map[string]interface{}) { r
 func (r *RAIDTestBMC) SupportsISOPreprovisioningImage() bool                 { return false }
 func (r *RAIDTestBMC) BIOSInterface() string                                 { return "" }
 func (r *RAIDTestBMC) BootInterface() string                                 { return "" }
-func (r *RAIDTestBMC) FirmwareInterface() string                             { return "" }
 func (r *RAIDTestBMC) ManagementInterface() string                           { return "" }
 func (r *RAIDTestBMC) PowerInterface() string                                { return "" }
 func (r *RAIDTestBMC) RAIDInterface() string                                 { return "" }
diff --git a/pkg/provisioner/ironic/testbmc/testbmc.go b/pkg/provisioner/ironic/testbmc/testbmc.go
index 500cd97a..50a900a1 100644
--- a/pkg/provisioner/ironic/testbmc/testbmc.go
+++ b/pkg/provisioner/ironic/testbmc/testbmc.go
@@ -70,10 +70,6 @@ func (a *testAccessDetails) BootInterface() string {
 	return "ipxe"
 }
 
-func (a *testAccessDetails) FirmwareInterface() string {
-	return ""
-}
-
 func (a *testAccessDetails) ManagementInterface() string {
 	return ""
 }
diff --git a/test/e2e/common.go b/test/e2e/common.go
index 3913d576..4e96b6ab 100644
--- a/test/e2e/common.go
+++ b/test/e2e/common.go
@@ -8,6 +8,8 @@ import (
 	"strings"
 	"time"
 
+	"golang.org/x/crypto/ssh"
+
 	"github.com/pkg/errors"
 	"gopkg.in/yaml.v2"
 
@@ -19,6 +21,8 @@ import (
 
 	metal3api "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1"
 
+	capm3_e2e "github.com/metal3-io/cluster-api-provider-metal3/test/e2e"
+
 	"k8s.io/apimachinery/pkg/types"
 	"sigs.k8s.io/cluster-api/test/framework"
 
@@ -296,3 +300,62 @@ func CreateBMHCredentialsSecret(ctx context.Context, client client.Client, secre
 
 	return nil
 }
+
+func executeSSHCommand(client *ssh.Client, command string) (string, error) {
+	session, err := client.NewSession()
+	if err != nil {
+		return "", fmt.Errorf("failed to create SSH session: %v", err)
+	}
+	defer session.Close()
+
+	output, err := session.CombinedOutput(command)
+	if err != nil {
+		return "", fmt.Errorf("failed to execute command '%s': %v", command, err)
+	}
+
+	return string(output), nil
+}
+
+// HasRootOnDisk parses the output from 'df -h' and checks if the root filesystem is on a disk (as opposed to tmpfs).
+func HasRootOnDisk(output string) bool {
+	lines := strings.Split(output, "\n")
+
+	for _, line := range lines[1:] { // Skip header line
+		if line == "" {
+			continue
+		}
+
+		fields := strings.Fields(line)
+		if len(fields) < 6 {
+			continue // Skip malformed lines
+		}
+
+		if fields[5] == "/" && !strings.Contains(fields[0], "tmpfs") {
+			return true // Found a non-tmpfs root filesystem
+		}
+	}
+
+	return false
+}
+
+// IsBootedFromDisk checks if the system, accessed via the provided ssh.Client, is booted
+// from a disk. It executes the 'df -h' command on the remote system to analyze the filesystem
+// layout. In the case of a disk boot, the output includes a disk-based root filesystem
+// (e.g., '/dev/vda1'). Conversely, in the case of a Live-ISO boot, the primary filesystems
+// are memory-based (tmpfs).
+func IsBootedFromDisk(client *ssh.Client) (bool, error) {
+	cmd := "df -h"
+	output, err := executeSSHCommand(client, cmd)
+	if err != nil {
+		return false, fmt.Errorf("error executing 'df -h': %w", err)
+	}
+
+	isDisk := HasRootOnDisk(output)
+	if isDisk {
+		capm3_e2e.Logf("System is booted from a disk.")
+	} else {
+		capm3_e2e.Logf("System is booted from a live ISO.")
+	}
+
+	return isDisk, nil
+}
diff --git a/test/e2e/config/fixture.yaml b/test/e2e/config/fixture.yaml
index ac5507c6..d0ade6a3 100644
--- a/test/e2e/config/fixture.yaml
+++ b/test/e2e/config/fixture.yaml
@@ -25,6 +25,7 @@ variables:
   IMAGE_URL: "http://192.168.222.1/cirros-0.6.2-x86_64-disk.img"
   IMAGE_CHECKSUM: "c8fc807773e5354afe61636071771906"
   CERT_MANAGER_VERSION: "v1.13.0"
+  SSH_CHECK_PROVISIONED: "false"
 
 intervals:
   inspection/wait-unmanaged: ["1m", "10ms"]
diff --git a/test/e2e/config/ironic.yaml b/test/e2e/config/ironic.yaml
index 7c930b76..7194f858 100644
--- a/test/e2e/config/ironic.yaml
+++ b/test/e2e/config/ironic.yaml
@@ -19,7 +19,7 @@ variables:
   DEPLOY_CERT_MANAGER: "true"
   BMO_KUSTOMIZATION: "../../config/overlays/e2e"
   IRONIC_KUSTOMIZATION: "../../ironic-deployment/overlays/e2e"
-  EXPECTED_HOST_NAME: "localhost.localdomain"
+  EXPECTED_HOST_NAME: "bmo-e2e-0"
   # Test credentials. The tests will create a BMH with these.
   BMC_USER: admin
   BMC_PASSWORD: password
@@ -32,6 +32,9 @@ variables:
   IMAGE_URL: "http://192.168.222.1/cirros-0.6.2-x86_64-disk.img"
   IMAGE_CHECKSUM: "c8fc807773e5354afe61636071771906"
   CERT_MANAGER_VERSION: "v1.13.0"
+  SSH_CHECK_PROVISIONED: "true"
+  IP_BMO_E2E_0: "192.168.222.122"
+  SSH_PORT: "22"
 
 intervals:
   inspection/wait-unmanaged: ["1m", "5s"]
@@ -49,3 +52,4 @@ intervals:
   default/wait-deprovisioning: ["1m", "10ms"]
   default/wait-deleted: ["20s", "10ms"]
   default/wait-secret-deletion: ["1m", "1s"]
+  default/wait-connect-ssh: ["2m", "10s"]
diff --git a/test/e2e/external_inspection_test.go b/test/e2e/external_inspection_test.go
index 49e33348..5de4f24b 100644
--- a/test/e2e/external_inspection_test.go
+++ b/test/e2e/external_inspection_test.go
@@ -133,7 +133,7 @@ const hardwareDetails = `
       "version": "1.15.0-1"
     }
   },
-  "hostname": "localhost.localdomain",
+  "hostname": "bmo-e2e-0",
   "nics": [
     {
       "ip": "192.168.222.122",
diff --git a/test/e2e/live_iso_test.go b/test/e2e/live_iso_test.go
new file mode 100644
index 00000000..7a02e9ac
--- /dev/null
+++ b/test/e2e/live_iso_test.go
@@ -0,0 +1,161 @@
+package e2e
+
+import (
+	"context"
+	"fmt"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	"golang.org/x/crypto/ssh"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/utils/pointer"
+	"sigs.k8s.io/cluster-api/test/framework"
+	"sigs.k8s.io/cluster-api/util"
+	"sigs.k8s.io/cluster-api/util/patch"
+
+	metal3api "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1"
+
+	capm3_e2e "github.com/metal3-io/cluster-api-provider-metal3/test/e2e"
+)
+
+var _ = Describe("Live-ISO", func() {
+	var (
+		specName       = "live-iso-ops"
+		secretName     = "bmc-credentials"
+		namespace      *corev1.Namespace
+		cancelWatches  context.CancelFunc
+		bmcUser        string
+		bmcPassword    string
+		bmcAddress     string
+		bootMacAddress string
+		imageURL       string
+	)
+
+	BeforeEach(func() {
+		bmcUser = e2eConfig.GetVariable("BMC_USER")
+		bmcPassword = e2eConfig.GetVariable("BMC_PASSWORD")
+		bmcAddress = e2eConfig.GetVariable("BMC_ADDRESS")
+		bootMacAddress = e2eConfig.GetVariable("BOOT_MAC_ADDRESS")
+		imageURL = e2eConfig.GetVariable("IMAGE_URL")
+
+		namespace, cancelWatches = framework.CreateNamespaceAndWatchEvents(ctx, framework.CreateNamespaceAndWatchEventsInput{
+			Creator:   clusterProxy.GetClient(),
+			ClientSet: clusterProxy.GetClientSet(),
+			Name:      fmt.Sprintf("%s-%s", specName, util.RandomString(6)),
+			LogFolder: artifactFolder,
+		})
+	})
+
+	It("should provision a BMH with live ISO and then deprovision it", func() {
+		By("Creating a secret with BMH credentials")
+		CreateBMHCredentialsSecret(ctx, clusterProxy.GetClient(), namespace.Name, secretName, bmcUser, bmcPassword)
+
+		By("Creating a BMH with inspection disabled and hardware details added")
+		bmh := metal3api.BareMetalHost{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      specName,
+				Namespace: namespace.Name,
+				Annotations: map[string]string{
+					metal3api.InspectAnnotationPrefix:   "disabled",
+					metal3api.HardwareDetailsAnnotation: hardwareDetails,
+				},
+			},
+			Spec: metal3api.BareMetalHostSpec{
+				Online: true,
+				BMC: metal3api.BMCDetails{
+					Address:         bmcAddress,
+					CredentialsName: secretName,
+				},
+				Image: &metal3api.Image{
+					URL:        imageURL,
+					DiskFormat: pointer.String("live-iso"),
+				},
+				BootMode:              metal3api.Legacy,
+				BootMACAddress:        bootMacAddress,
+				AutomatedCleaningMode: "disabled",
+				RootDeviceHints: &metal3api.RootDeviceHints{
+					DeviceName: "/dev/vda",
+				},
+			},
+		}
+		err := clusterProxy.GetClient().Create(ctx, &bmh)
+		Expect(err).NotTo(HaveOccurred())
+
+		By("Waiting for the BMH to be in provisioning state")
+		WaitForBmhInProvisioningState(ctx, WaitForBmhInProvisioningStateInput{
+			Client: clusterProxy.GetClient(),
+			Bmh:    bmh,
+			State:  metal3api.StateProvisioning,
+		}, e2eConfig.GetIntervals(specName, "wait-provisioning")...)
+
+		By("Waiting for the BMH to become provisioned")
+		WaitForBmhInProvisioningState(ctx, WaitForBmhInProvisioningStateInput{
+			Client: clusterProxy.GetClient(),
+			Bmh:    bmh,
+			State:  metal3api.StateProvisioned,
+		}, e2eConfig.GetIntervals(specName, "wait-provisioned")...)
+
+		// The ssh check is not possible in all situations (e.g. fixture) so it can be skipped
+		if e2eConfig.GetVariable("SSH_CHECK_PROVISIONED") == "true" {
+			By("Verifying the node booted from live ISO image")
+
+			// Set up SSH client configuration
+			config := &ssh.ClientConfig{
+				User: "cirros",
+				Auth: []ssh.AuthMethod{
+					ssh.Password("gocubsgo"),
+				},
+				HostKeyCallback: ssh.InsecureIgnoreHostKey(), // #nosec G106
+			}
+
+			ip := e2eConfig.GetVariable("IP_BMO_E2E_0")
+			sshPort := e2eConfig.GetVariable("SSH_PORT")
+			address := fmt.Sprintf("%s:%s", ip, sshPort)
+
+			// Establish an SSH connection
+			var client *ssh.Client
+			var err error
+
+			Eventually(func() error {
+				client, err = ssh.Dial("tcp", address, config)
+				return err
+			}, e2eConfig.GetIntervals(specName, "wait-connect-ssh")...).Should(Succeed(), "Failed to establish SSH connection")
+
+			defer func() {
+				if client != nil {
+					client.Close()
+				}
+			}()
+			isDisk, err := IsBootedFromDisk(client)
+			Expect(err).NotTo(HaveOccurred(), "Error in verifying boot mode")
+			Expect(isDisk).To(Equal(false), "Error booting from disk when live ISO is expected")
+		} else {
+			capm3_e2e.Logf("WARNING: Skipping ssh check since SSH_CHECK_PROVISIONED != true")
+		}
+
+		By("Triggering the deprovisioning of the BMH")
+		helper, err := patch.NewHelper(&bmh, clusterProxy.GetClient())
+		Expect(err).NotTo(HaveOccurred())
+		bmh.Spec.Image = nil
+		Expect(helper.Patch(ctx, &bmh)).To(Succeed())
+
+		By("Waiting for the BMH to be in deprovisioning state")
+		WaitForBmhInProvisioningState(ctx, WaitForBmhInProvisioningStateInput{
+			Client: clusterProxy.GetClient(),
+			Bmh:    bmh,
+			State:  metal3api.StateDeprovisioning,
+		}, e2eConfig.GetIntervals(specName, "wait-deprovisioning")...)
+
+		By("Waiting for the BMH to become available again")
+		WaitForBmhInProvisioningState(ctx, WaitForBmhInProvisioningStateInput{
+			Client: clusterProxy.GetClient(),
+			Bmh:    bmh,
+			State:  metal3api.StateAvailable,
+		}, e2eConfig.GetIntervals(specName, "wait-available")...)
+	})
+
+	AfterEach(func() {
+		cleanup(ctx, clusterProxy, namespace, cancelWatches, e2eConfig.GetIntervals("default", "wait-namespace-deleted")...)
+	})
+})
diff --git a/test/e2e/re_inspection_test.go b/test/e2e/re_inspection_test.go
index 93d947e7..d9b64986 100644
--- a/test/e2e/re_inspection_test.go
+++ b/test/e2e/re_inspection_test.go
@@ -54,7 +54,7 @@ var _ = Describe("Re-Inspection", func() {
 		CreateBMHCredentialsSecret(ctx, clusterProxy.GetClient(), namespace.Name, secretName, bmcUser, bmcPassword)
 
 		By("creating a BMH with inspection disabled and hardware details added with wrong HostName")
-		newHardwareDetails := strings.Replace(hardwareDetails, "localhost.localdomain", wrongHostName, 1)
+		newHardwareDetails := strings.Replace(hardwareDetails, "bmo-e2e-0", wrongHostName, 1)
 		bmh := metal3api.BareMetalHost{
 			ObjectMeta: metav1.ObjectMeta{
 				Name:      specName + "-reinspect",
diff --git a/test/go.mod b/test/go.mod
index d27866e3..3249f27d 100644
--- a/test/go.mod
+++ b/test/go.mod
@@ -9,6 +9,7 @@ require (
 	github.com/onsi/ginkgo/v2 v2.13.0
 	github.com/onsi/gomega v1.29.0
 	github.com/pkg/errors v0.9.1
+	golang.org/x/crypto v0.14.0
 	gopkg.in/yaml.v2 v2.4.0
 	k8s.io/api v0.27.7
 	k8s.io/apimachinery v0.27.7
@@ -108,7 +109,6 @@ require (
 	github.com/valyala/fastjson v1.6.4 // indirect
 	github.com/xlab/treeprint v1.1.0 // indirect
 	go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
-	golang.org/x/crypto v0.14.0 // indirect
 	golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
 	golang.org/x/mod v0.12.0 // indirect
 	golang.org/x/net v0.17.0 // indirect