Repository: metal3-io/baremetal-operator
Pull request #515 (2020-05-12 19:57:25)

Add a tool to convert ironic introspection data to HardwareDetails
This adds a tool called "make-introspection" that takes 2 arguments,
the Ironic inspector URI and then node UUID. It queries inspector
for the data, and if found, converts it to the BareMetalHost
HardwareDetails format.

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

This pull-request has been approved by: *<a href="https://github.com/metal3-io/baremetal-operator/pull/515#" title="Author self-approved">stbenjam</a>*

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

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

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

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

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
stbenjam: Output of running:

```bash
$ ./make-introspection http://\[fd00:1101::3\]:5050/v1 b32e285f-a16e-4c23-8456-9e0bbd786910
```

```json
{
	"systemVendor": {
		"manufacturer": "Red Hat",
		"productName": "KVM",
		"serialNumber": ""
	},
	"firmware": {
		"bios": {
			"date": "",
			"vendor": "",
			"version": ""
		}
	},
	"ramMebibytes": 8192,
	"nics": [
		{
			"name": "enp1s0",
			"model": "0x1af4 0x0001",
			"mac": "00:03:1a:e8:1a:db",
			"ip": "fd00:1101::2a",
			"speedGbps": 0,
			"vlanId": 0,
			"pxe": true
		},
		{
			"name": "enp2s0",
			"model": "0x1af4 0x0001",
			"mac": "00:03:1a:e8:1a:dd",
			"ip": "fd2e:6f44:5dd8:c956::17",
			"speedGbps": 0,
			"vlanId": 0,
			"pxe": false
		}
	],
	"storage": [
		{
			"name": "/dev/sda",
			"rotational": true,
			"sizeBytes": 21474836480,
			"vendor": "QEMU",
			"model": "QEMU HARDDISK",
			"serialNumber": "drive-scsi0-0-0-0",
			"hctl": "0:0:0:0"
		}
	],
	"cpu": {
		"arch": "x86_64",
		"model": "Intel(R) Xeon(R) Gold 6230 CPU @ 2.10GHz",
		"clockMegahertz": 2095.076,
		"flags": [
			"3dnowprefetch",
			"abm",
			"adx",
			"aes",
			"apic",
			"arat",
			"arch_capabilities",
			"arch_perfmon",
			"avx",
			"avx2",
			"avx512_vnni",
			"avx512bw",
			"avx512cd",
			"avx512dq",
			"avx512f",
			"avx512vl",
			"bmi1",
			"bmi2",
			"clflush",
			"clflushopt",
			"clwb",
			"cmov",
			"constant_tsc",
			"cpuid",
			"cpuid_fault",
			"cx16",
			"cx8",
			"de",
			"erms",
			"f16c",
			"fma",
			"fpu",
			"fsgsbase",
			"fxsr",
			"hle",
			"hypervisor",
			"ibpb",
			"ibrs",
			"ibrs_enhanced",
			"invpcid",
			"invpcid_single",
			"lahf_lm",
			"lm",
			"mca",
			"mce",
			"md_clear",
			"mmx",
			"movbe",
			"mpx",
			"msr",
			"mtrr",
			"nopl",
			"nx",
			"ospke",
			"pae",
			"pat",
			"pcid",
			"pclmulqdq",
			"pdpe1gb",
			"pge",
			"pku",
			"pni",
			"popcnt",
			"pse",
			"pse36",
			"rdrand",
			"rdseed",
			"rdtscp",
			"rep_good",
			"rtm",
			"sep",
			"smap",
			"smep",
			"ss",
			"ssbd",
			"sse",
			"sse2",
			"sse4_1",
			"sse4_2",
			"ssse3",
			"stibp",
			"syscall",
			"tsc",
			"tsc_adjust",
			"tsc_deadline_timer",
			"tsc_known_freq",
			"umip",
			"vme",
			"x2apic",
			"xgetbv1",
			"xsave",
			"xsavec",
			"xsaveopt",
			"xsaves",
			"xtopology"
		],
		"count": 4
	},
	"hostname": "worker-0.ostest.test.metalkube.org"
}
```
metal3-io-bot: @stbenjam: The following test **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
govet | fc3efa3286b06eae4e71de037cfe3fe222ec5063 | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/515/govet/1260298140137820160) | `/test govet`



<details>

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
govet | fc3efa3286b06eae4e71de037cfe3fe222ec5063 | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/515/govet/1260298140137820160) | `/test govet`
unit | fc3efa3286b06eae4e71de037cfe3fe222ec5063 | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/515/unit/1260298140142014466) | `/test unit`



<details>

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
unit | fc3efa3286b06eae4e71de037cfe3fe222ec5063 | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/515/unit/1260298140142014466) | `/test unit`
govet | 41d45eb09b8140540fbe4d48da522be14ca603ee | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/515/govet/1260543759746076673) | `/test govet`



<details>

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
govet | 41d45eb09b8140540fbe4d48da522be14ca603ee | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/515/govet/1260543759746076673) | `/test govet`
unit | 41d45eb09b8140540fbe4d48da522be14ca603ee | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/515/unit/1260543759746076676) | `/test unit`



<details>

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
govet | 41d45eb09b8140540fbe4d48da522be14ca603ee | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/515/govet/1260543759746076673) | `/test govet`
unit | a036e0ba02ae7b7eb625a21e6f00fe6d641e582a | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/515/unit/1260544137262796802) | `/test unit`



<details>

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
unit | a036e0ba02ae7b7eb625a21e6f00fe6d641e582a | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/515/unit/1260544137262796802) | `/test unit`
govet | a036e0ba02ae7b7eb625a21e6f00fe6d641e582a | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/515/govet/1260544137262796804) | `/test govet`



<details>

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
govet | a036e0ba02ae7b7eb625a21e6f00fe6d641e582a | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/515/govet/1260544137262796804) | `/test govet`
unit | 964b13f18df4c43babf8b95e0468ea421b53a254 | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/515/unit/1260547534674202624) | `/test unit`



<details>

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
unit | 964b13f18df4c43babf8b95e0468ea421b53a254 | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/515/unit/1260547534674202624) | `/test unit`
govet | 964b13f18df4c43babf8b95e0468ea421b53a254 | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/515/govet/1260547534674202627) | `/test govet`



<details>

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
govet | 964b13f18df4c43babf8b95e0468ea421b53a254 | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/515/govet/1260547534674202627) | `/test govet`
unit | a97c152a8693a311f1013889c093a06020041793 | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/515/unit/1260548163832385540) | `/test unit`



<details>

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
unit | a97c152a8693a311f1013889c093a06020041793 | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/515/unit/1260548163832385540) | `/test unit`
govet | a97c152a8693a311f1013889c093a06020041793 | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/515/govet/1260548163832385538) | `/test govet`



<details>

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
unit | a97c152a8693a311f1013889c093a06020041793 | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/515/unit/1260548163832385540) | `/test unit`
govet | a97c152a8693a311f1013889c093a06020041793 | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/515/govet/1260548163832385538) | `/test govet`
gofmt | be8f62679be83adb336725240d2c400db340dcf6 | [link](https://prow.apps.ci.metal3.io/view/gcs/metal3-prow-artifacts/pr-logs/pull/metal3-io_baremetal-operator/515/gofmt/1260549799778717698) | `/test gofmt`



<details>

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

Patch:
diff --git a/Makefile b/Makefile
index 92fcb3a1..5e68b055 100644
--- a/Makefile
+++ b/Makefile
@@ -146,6 +146,11 @@ build:
 	@echo LDFLAGS=$(LDFLAGS)
 	go build -o build/_output/bin/baremetal-operator cmd/manager/main.go
 
+.PHONY: tools
+tools:
+	@echo LDFLAGS=$(LDFLAGS)
+	go build -o build/_output/bin/make-introspection cmd/make-introspection/main.go
+
 .PHONY: deploy
 deploy:
 	cd deploy && kustomize edit set namespace $(RUN_NAMESPACE) && cd ..
diff --git a/cmd/make-introspection/main.go b/cmd/make-introspection/main.go
new file mode 100644
index 00000000..edb2ea93
--- /dev/null
+++ b/cmd/make-introspection/main.go
@@ -0,0 +1,44 @@
+// make-introspection is a tool that can be used to convert raw Ironic introspection data into the HardwareDetails
+// type used by Metal3.
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"os"
+
+	"github.com/gophercloud/gophercloud/openstack/baremetalintrospection/noauth"
+	"github.com/gophercloud/gophercloud/openstack/baremetalintrospection/v1/introspection"
+	"github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/hardwaredetails"
+)
+
+func main() {
+	if len(os.Args) != 3 {
+		fmt.Println("Usage: make-introspection <inspector URI> <node UUID>")
+		return
+	}
+
+	inspector, err := noauth.NewBareMetalIntrospectionNoAuth(
+		noauth.EndpointOpts{
+			IronicInspectorEndpoint: os.Args[1],
+		})
+	if err != nil {
+		fmt.Printf("could not get inpsector client: %s", err)
+		os.Exit(1)
+	}
+
+	introData := introspection.GetIntrospectionData(inspector, os.Args[2])
+	data, err := introData.Extract()
+	if err != nil {
+		fmt.Printf("could not get introspection data: %s", err)
+		os.Exit(1)
+	}
+
+	json, err := json.MarshalIndent(hardwaredetails.GetHardwareDetails(data), "", "\t")
+	if err != nil {
+		fmt.Printf("could not convert introspection data: %s", err)
+		os.Exit(1)
+	}
+
+	fmt.Println(string(json))
+}
diff --git a/pkg/provisioner/ironic/hardwaredetails/hardwaredetails.go b/pkg/provisioner/ironic/hardwaredetails/hardwaredetails.go
new file mode 100644
index 00000000..795aeecd
--- /dev/null
+++ b/pkg/provisioner/ironic/hardwaredetails/hardwaredetails.go
@@ -0,0 +1,143 @@
+package hardwaredetails
+
+import (
+	"fmt"
+	"sort"
+	"strings"
+
+	"github.com/gophercloud/gophercloud/openstack/baremetalintrospection/v1/introspection"
+	metal3v1alpha1 "github.com/metal3-io/baremetal-operator/pkg/apis/metal3/v1alpha1"
+)
+
+// GetHardwareDetails converts Ironic introspection data into BareMetalHost HardwareDetails.
+func GetHardwareDetails(data *introspection.Data) *metal3v1alpha1.HardwareDetails {
+	details := new(metal3v1alpha1.HardwareDetails)
+	details.Firmware = getFirmwareDetails(data.Extra.Firmware)
+	details.SystemVendor = getSystemVendorDetails(data.Inventory.SystemVendor)
+	details.RAMMebibytes = data.MemoryMB
+	details.NIC = getNICDetails(data.Inventory.Interfaces, data.AllInterfaces, data.Extra.Network)
+	details.Storage = getStorageDetails(data.Inventory.Disks)
+	details.CPU = getCPUDetails(&data.Inventory.CPU)
+	details.Hostname = data.Inventory.Hostname
+	return details
+}
+
+func getVLANs(intf introspection.BaseInterfaceType) (vlans []metal3v1alpha1.VLAN, vlanid metal3v1alpha1.VLANID) {
+	if intf.LLDPProcessed == nil {
+		return
+	}
+	if spvs, ok := intf.LLDPProcessed["switch_port_vlans"]; ok {
+		if data, ok := spvs.([]map[string]interface{}); ok {
+			vlans = make([]metal3v1alpha1.VLAN, len(data))
+			for i, vlan := range data {
+				vid, _ := vlan["id"].(int)
+				name, _ := vlan["name"].(string)
+				vlans[i] = metal3v1alpha1.VLAN{
+					ID:   metal3v1alpha1.VLANID(vid),
+					Name: name,
+				}
+			}
+		}
+	}
+	if vid, ok := intf.LLDPProcessed["switch_port_untagged_vlan_id"].(int); ok {
+		vlanid = metal3v1alpha1.VLANID(vid)
+	}
+	return
+}
+
+func getNICSpeedGbps(intfExtradata introspection.ExtraHardwareData) (speedGbps int) {
+	if speed, ok := intfExtradata["speed"].(string); ok {
+		if strings.HasSuffix(speed, "Gbps") {
+			fmt.Sscanf(speed, "%d", &speedGbps)
+		}
+	}
+	return
+}
+
+func getNICDetails(ifdata []introspection.InterfaceType,
+	basedata map[string]introspection.BaseInterfaceType,
+	extradata introspection.ExtraHardwareDataSection) []metal3v1alpha1.NIC {
+	nics := make([]metal3v1alpha1.NIC, len(ifdata))
+	for i, intf := range ifdata {
+		baseIntf := basedata[intf.Name]
+		vlans, vlanid := getVLANs(baseIntf)
+		ip := intf.IPV4Address
+		if ip == "" {
+			ip = intf.IPV6Address
+		}
+		nics[i] = metal3v1alpha1.NIC{
+			Name: intf.Name,
+			Model: strings.TrimLeft(fmt.Sprintf("%s %s",
+				intf.Vendor, intf.Product), " "),
+			MAC:       intf.MACAddress,
+			IP:        ip,
+			VLANs:     vlans,
+			VLANID:    vlanid,
+			SpeedGbps: getNICSpeedGbps(extradata[intf.Name]),
+			PXE:       baseIntf.PXE,
+		}
+	}
+	return nics
+}
+
+func getStorageDetails(diskdata []introspection.RootDiskType) []metal3v1alpha1.Storage {
+	storage := make([]metal3v1alpha1.Storage, len(diskdata))
+	for i, disk := range diskdata {
+		storage[i] = metal3v1alpha1.Storage{
+			Name:               disk.Name,
+			Rotational:         disk.Rotational,
+			SizeBytes:          metal3v1alpha1.Capacity(disk.Size),
+			Vendor:             disk.Vendor,
+			Model:              disk.Model,
+			SerialNumber:       disk.Serial,
+			WWN:                disk.Wwn,
+			WWNVendorExtension: disk.WwnVendorExtension,
+			WWNWithExtension:   disk.WwnWithExtension,
+			HCTL:               disk.Hctl,
+		}
+	}
+	return storage
+}
+
+func getSystemVendorDetails(vendor introspection.SystemVendorType) metal3v1alpha1.HardwareSystemVendor {
+	return metal3v1alpha1.HardwareSystemVendor{
+		Manufacturer: vendor.Manufacturer,
+		ProductName:  vendor.ProductName,
+		SerialNumber: vendor.SerialNumber,
+	}
+}
+
+func getCPUDetails(cpudata *introspection.CPUType) metal3v1alpha1.CPU {
+	var freq float64
+	fmt.Sscanf(cpudata.Frequency, "%f", &freq)
+	sort.Strings(cpudata.Flags)
+	cpu := metal3v1alpha1.CPU{
+		Arch:           cpudata.Architecture,
+		Model:          cpudata.ModelName,
+		ClockMegahertz: metal3v1alpha1.ClockSpeed(freq) * metal3v1alpha1.MegaHertz,
+		Count:          cpudata.Count,
+		Flags:          cpudata.Flags,
+	}
+
+	return cpu
+}
+
+func getFirmwareDetails(firmwaredata introspection.ExtraHardwareDataSection) metal3v1alpha1.Firmware {
+
+	// handle bios optionally
+	var bios metal3v1alpha1.BIOS
+
+	if biosdata, ok := firmwaredata["bios"]; ok {
+		// we do not know if all fields will be supplied
+		// as this is not a structured response
+		// so we must handle each field conditionally
+		bios.Vendor, _ = biosdata["vendor"].(string)
+		bios.Version, _ = biosdata["version"].(string)
+		bios.Date, _ = biosdata["date"].(string)
+	}
+
+	return metal3v1alpha1.Firmware{
+		BIOS: bios,
+	}
+
+}
diff --git a/pkg/provisioner/ironic/ironic.go b/pkg/provisioner/ironic/ironic.go
index ecc3c271..f03d4aa6 100644
--- a/pkg/provisioner/ironic/ironic.go
+++ b/pkg/provisioner/ironic/ironic.go
@@ -3,7 +3,6 @@ package ironic
 import (
 	"fmt"
 	"os"
-	"sort"
 	"strings"
 	"time"
 
@@ -25,6 +24,7 @@ import (
 	"github.com/metal3-io/baremetal-operator/pkg/bmc"
 	"github.com/metal3-io/baremetal-operator/pkg/hardware"
 	"github.com/metal3-io/baremetal-operator/pkg/provisioner"
+	"github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/hardwaredetails"
 )
 
 var log = logf.Log.WithName("baremetalhost_ironic")
@@ -444,138 +444,6 @@ func (p *ironicProvisioner) changeNodeProvisionState(ironicNode *nodes.Node, opt
 	return result, nil
 }
 
-func getVLANs(intf introspection.BaseInterfaceType) (vlans []metal3v1alpha1.VLAN, vlanid metal3v1alpha1.VLANID) {
-	if intf.LLDPProcessed == nil {
-		return
-	}
-	if spvs, ok := intf.LLDPProcessed["switch_port_vlans"]; ok {
-		if data, ok := spvs.([]map[string]interface{}); ok {
-			vlans = make([]metal3v1alpha1.VLAN, len(data))
-			for i, vlan := range data {
-				vid, _ := vlan["id"].(int)
-				name, _ := vlan["name"].(string)
-				vlans[i] = metal3v1alpha1.VLAN{
-					ID:   metal3v1alpha1.VLANID(vid),
-					Name: name,
-				}
-			}
-		}
-	}
-	if vid, ok := intf.LLDPProcessed["switch_port_untagged_vlan_id"].(int); ok {
-		vlanid = metal3v1alpha1.VLANID(vid)
-	}
-	return
-}
-
-func getNICSpeedGbps(intfExtradata introspection.ExtraHardwareData) (speedGbps int) {
-	if speed, ok := intfExtradata["speed"].(string); ok {
-		if strings.HasSuffix(speed, "Gbps") {
-			fmt.Sscanf(speed, "%d", &speedGbps)
-		}
-	}
-	return
-}
-
-func getNICDetails(ifdata []introspection.InterfaceType,
-	basedata map[string]introspection.BaseInterfaceType,
-	extradata introspection.ExtraHardwareDataSection) []metal3v1alpha1.NIC {
-	nics := make([]metal3v1alpha1.NIC, len(ifdata))
-	for i, intf := range ifdata {
-		baseIntf := basedata[intf.Name]
-		vlans, vlanid := getVLANs(baseIntf)
-		ip := intf.IPV4Address
-		if ip == "" {
-			ip = intf.IPV6Address
-		}
-		nics[i] = metal3v1alpha1.NIC{
-			Name: intf.Name,
-			Model: strings.TrimLeft(fmt.Sprintf("%s %s",
-				intf.Vendor, intf.Product), " "),
-			MAC:       intf.MACAddress,
-			IP:        ip,
-			VLANs:     vlans,
-			VLANID:    vlanid,
-			SpeedGbps: getNICSpeedGbps(extradata[intf.Name]),
-			PXE:       baseIntf.PXE,
-		}
-	}
-	return nics
-}
-
-func getStorageDetails(diskdata []introspection.RootDiskType) []metal3v1alpha1.Storage {
-	storage := make([]metal3v1alpha1.Storage, len(diskdata))
-	for i, disk := range diskdata {
-		storage[i] = metal3v1alpha1.Storage{
-			Name:               disk.Name,
-			Rotational:         disk.Rotational,
-			SizeBytes:          metal3v1alpha1.Capacity(disk.Size),
-			Vendor:             disk.Vendor,
-			Model:              disk.Model,
-			SerialNumber:       disk.Serial,
-			WWN:                disk.Wwn,
-			WWNVendorExtension: disk.WwnVendorExtension,
-			WWNWithExtension:   disk.WwnWithExtension,
-			HCTL:               disk.Hctl,
-		}
-	}
-	return storage
-}
-
-func getSystemVendorDetails(vendor introspection.SystemVendorType) metal3v1alpha1.HardwareSystemVendor {
-	return metal3v1alpha1.HardwareSystemVendor{
-		Manufacturer: vendor.Manufacturer,
-		ProductName:  vendor.ProductName,
-		SerialNumber: vendor.SerialNumber,
-	}
-}
-
-func getCPUDetails(cpudata *introspection.CPUType) metal3v1alpha1.CPU {
-	var freq float64
-	fmt.Sscanf(cpudata.Frequency, "%f", &freq)
-	sort.Strings(cpudata.Flags)
-	cpu := metal3v1alpha1.CPU{
-		Arch:           cpudata.Architecture,
-		Model:          cpudata.ModelName,
-		ClockMegahertz: metal3v1alpha1.ClockSpeed(freq) * metal3v1alpha1.MegaHertz,
-		Count:          cpudata.Count,
-		Flags:          cpudata.Flags,
-	}
-
-	return cpu
-}
-
-func getFirmwareDetails(firmwaredata introspection.ExtraHardwareDataSection) metal3v1alpha1.Firmware {
-
-	// handle bios optionally
-	var bios metal3v1alpha1.BIOS
-
-	if biosdata, ok := firmwaredata["bios"]; ok {
-		// we do not know if all fields will be supplied
-		// as this is not a structured response
-		// so we must handle each field conditionally
-		bios.Vendor, _ = biosdata["vendor"].(string)
-		bios.Version, _ = biosdata["version"].(string)
-		bios.Date, _ = biosdata["date"].(string)
-	}
-
-	return metal3v1alpha1.Firmware{
-		BIOS: bios,
-	}
-
-}
-
-func getHardwareDetails(data *introspection.Data) *metal3v1alpha1.HardwareDetails {
-	details := new(metal3v1alpha1.HardwareDetails)
-	details.Firmware = getFirmwareDetails(data.Extra.Firmware)
-	details.SystemVendor = getSystemVendorDetails(data.Inventory.SystemVendor)
-	details.RAMMebibytes = data.MemoryMB
-	details.NIC = getNICDetails(data.Inventory.Interfaces, data.AllInterfaces, data.Extra.Network)
-	details.Storage = getStorageDetails(data.Inventory.Disks)
-	details.CPU = getCPUDetails(&data.Inventory.CPU)
-	details.Hostname = data.Inventory.Hostname
-	return details
-}
-
 // InspectHardware updates the HardwareDetails field of the host with
 // details of devices discovered on the hardware. It may be called
 // multiple times, and should return true for its dirty flag until the
@@ -639,7 +507,7 @@ func (p *ironicProvisioner) InspectHardware() (result provisioner.Result, detail
 	}
 	p.log.Info("received introspection data", "data", introData.Body)
 
-	details = getHardwareDetails(data)
+	details = hardwaredetails.GetHardwareDetails(data)
 	p.publisher("InspectionComplete", "Hardware inspection completed")
 	return
 }


Test patch:
diff --git a/pkg/provisioner/ironic/hardwaredetails/hardwaredetails_test.go b/pkg/provisioner/ironic/hardwaredetails/hardwaredetails_test.go
new file mode 100644
index 00000000..fac70cbe
--- /dev/null
+++ b/pkg/provisioner/ironic/hardwaredetails/hardwaredetails_test.go
@@ -0,0 +1,277 @@
+package hardwaredetails
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/baremetalintrospection/v1/introspection"
+
+	metal3v1alpha1 "github.com/metal3-io/baremetal-operator/pkg/apis/metal3/v1alpha1"
+)
+
+func TestGetVLANs(t *testing.T) {
+	vlans, vid := getVLANs(introspection.BaseInterfaceType{
+		LLDPProcessed: map[string]interface{}{
+			"switch_port_vlans": []map[string]interface{}{
+				{
+					"id":   1,
+					"name": "vlan1",
+				},
+				{
+					"id":   4094,
+					"name": "vlan4094",
+				},
+			},
+			"switch_port_untagged_vlan_id": 1,
+		},
+	})
+	if vid != 1 {
+		t.Errorf("Unexpected untagged VLAN ID %d", vid)
+	}
+	if len(vlans) != 2 {
+		t.Errorf("Expected 2 VLANs, got %d", len(vlans))
+	}
+	if (vlans[0] != metal3v1alpha1.VLAN{ID: 1, Name: "vlan1"}) {
+		t.Errorf("Unexpected VLAN %d %s", vlans[0].ID, vlans[0].Name)
+	}
+	if (vlans[1] != metal3v1alpha1.VLAN{ID: 4094, Name: "vlan4094"}) {
+		t.Errorf("Unexpected VLAN %d %s", vlans[1].ID, vlans[1].Name)
+	}
+}
+
+func TestGetVLANsMalformed(t *testing.T) {
+	vlans, vid := getVLANs(introspection.BaseInterfaceType{
+		LLDPProcessed: map[string]interface{}{
+			"switch_port_vlans": []map[string]interface{}{
+				{
+					"foo":  "bar",
+					"name": "vlan1",
+				},
+				{
+					"foo": "bar",
+					"id":  1,
+				},
+				{
+					"name": "vlan2",
+					"id":   "2",
+				},
+				{
+					"name": 3,
+					"id":   3,
+				},
+				{
+					"foo": "bar",
+				},
+			},
+			"switch_port_untagged_vlan_id": "1",
+		},
+	})
+	if vid != 0 {
+		t.Errorf("Unexpected untagged VLAN ID %d", vid)
+	}
+	if len(vlans) != 5 {
+		t.Errorf("Expected 5 VLANs, got %d", len(vlans))
+	}
+	if (vlans[0] != metal3v1alpha1.VLAN{Name: "vlan1"}) {
+		t.Errorf("Unexpected VLAN %d %s", vlans[0].ID, vlans[0].Name)
+	}
+	if (vlans[1] != metal3v1alpha1.VLAN{ID: 1}) {
+		t.Errorf("Unexpected VLAN %d %s", vlans[0].ID, vlans[0].Name)
+	}
+	if (vlans[2] != metal3v1alpha1.VLAN{Name: "vlan2"}) {
+		t.Errorf("Unexpected VLAN %d %s", vlans[0].ID, vlans[0].Name)
+	}
+	if (vlans[3] != metal3v1alpha1.VLAN{ID: 3}) {
+		t.Errorf("Unexpected VLAN %d %s", vlans[0].ID, vlans[0].Name)
+	}
+	if (vlans[4] != metal3v1alpha1.VLAN{}) {
+		t.Errorf("Unexpected VLAN %d %s", vlans[0].ID, vlans[0].Name)
+	}
+
+	vlans, vid = getVLANs(introspection.BaseInterfaceType{
+		LLDPProcessed: map[string]interface{}{
+			"switch_port_vlans": map[string]interface{}{
+				"foo": "bar",
+			},
+			"switch_port_untagged_vlan_id": "1",
+		},
+	})
+	if vid != 0 {
+		t.Errorf("Unexpected untagged VLAN ID %d", vid)
+	}
+	if len(vlans) != 0 {
+		t.Errorf("Expected 0 VLANs, got %d", len(vlans))
+	}
+
+	vlans, vid = getVLANs(introspection.BaseInterfaceType{
+		LLDPProcessed: map[string]interface{}{
+			"switch_port_vlans": []interface{}{
+				"foo",
+			},
+		},
+	})
+	if vid != 0 {
+		t.Errorf("Unexpected untagged VLAN ID %d", vid)
+	}
+	if len(vlans) != 0 {
+		t.Errorf("Expected 0 VLANs, got %d", len(vlans))
+	}
+
+	vlans, vid = getVLANs(introspection.BaseInterfaceType{
+		LLDPProcessed: map[string]interface{}{
+			"switch_port_vlans": "foo",
+		},
+	})
+	if vid != 0 {
+		t.Errorf("Unexpected untagged VLAN ID %d", vid)
+	}
+	if len(vlans) != 0 {
+		t.Errorf("Expected 0 VLANs, got %d", len(vlans))
+	}
+
+	vlans, vid = getVLANs(introspection.BaseInterfaceType{
+		LLDPProcessed: map[string]interface{}{},
+	})
+	if vid != 0 {
+		t.Errorf("Unexpected untagged VLAN ID %d", vid)
+	}
+	if len(vlans) != 0 {
+		t.Errorf("Expected 0 VLANs, got %d", len(vlans))
+	}
+
+	vlans, vid = getVLANs(introspection.BaseInterfaceType{})
+	if vid != 0 {
+		t.Errorf("Unexpected untagged VLAN ID %d", vid)
+	}
+	if len(vlans) != 0 {
+		t.Errorf("Expected 0 VLANs, got %d", len(vlans))
+	}
+}
+
+func TestGetNICDetails(t *testing.T) {
+	nics := getNICDetails(
+		[]introspection.InterfaceType{
+			{
+				Name:        "eth0",
+				IPV4Address: "192.0.2.1",
+				MACAddress:  "00:11:22:33:44:55"},
+			{
+				Name:        "eth1",
+				IPV6Address: "2001:db8::1",
+				MACAddress:  "66:77:88:99:aa:bb"},
+		},
+		map[string]introspection.BaseInterfaceType{
+			"eth0": {
+				PXE: true,
+				LLDPProcessed: map[string]interface{}{
+					"switch_port_vlans": []map[string]interface{}{
+						{
+							"id": 1,
+						},
+					},
+					"switch_port_untagged_vlan_id": 1,
+				},
+			},
+		},
+		introspection.ExtraHardwareDataSection{
+			"eth1": introspection.ExtraHardwareData{
+				"speed": "1Gbps",
+			},
+		})
+
+	if len(nics) != 2 {
+		t.Errorf("Expected 2 NICs, got %d", len(nics))
+	}
+	if (!reflect.DeepEqual(nics[0], metal3v1alpha1.NIC{
+		Name: "eth0",
+		MAC:  "00:11:22:33:44:55",
+		IP:   "192.0.2.1",
+		PXE:  true,
+		VLANs: []metal3v1alpha1.VLAN{
+			{ID: 1},
+		},
+		VLANID: 1,
+	})) {
+		t.Errorf("Unexpected NIC data")
+	}
+	if (!reflect.DeepEqual(nics[1], metal3v1alpha1.NIC{
+		Name:      "eth1",
+		MAC:       "66:77:88:99:aa:bb",
+		IP:        "2001:db8::1",
+		SpeedGbps: 1,
+	})) {
+		t.Errorf("Unexpected NIC data")
+	}
+}
+
+func TestGetNICSpeedGbps(t *testing.T) {
+	s1 := getNICSpeedGbps(introspection.ExtraHardwareData{
+		"speed": "25Gbps",
+	})
+	if s1 != 25 {
+		t.Errorf("Expected speed 25, got %d", s1)
+	}
+
+	s2 := getNICSpeedGbps(introspection.ExtraHardwareData{
+		"speed": "100Mbps",
+	})
+	if s2 != 0 {
+		t.Errorf("Expected speed 0, got %d", s2)
+	}
+
+	s3 := getNICSpeedGbps(introspection.ExtraHardwareData{
+		"speed": 10,
+	})
+	if s3 != 0 {
+		t.Errorf("Expected speed 0, got %d", s3)
+	}
+
+	s4 := getNICSpeedGbps(introspection.ExtraHardwareData{})
+	if s4 != 0 {
+		t.Errorf("Expected speed 0, got %d", s4)
+	}
+}
+
+func TestGetFirmwareDetails(t *testing.T) {
+	// Test full (known) firmware payload
+	firmware := getFirmwareDetails(introspection.ExtraHardwareDataSection{
+		"bios": {
+			"vendor":  "foobar",
+			"version": "1.2.3",
+			"date":    "2019-07-10",
+		},
+	})
+
+	if firmware.BIOS.Vendor != "foobar" {
+		t.Errorf("Expected firmware BIOS vendor to be foobar, but got: %s", firmware)
+	}
+
+	// Ensure we can handle partial firmware/bios data
+	firmware = getFirmwareDetails(introspection.ExtraHardwareDataSection{
+		"bios": {
+			"vendor":  "foobar",
+			"version": "1.2.3",
+		},
+	})
+
+	if firmware.BIOS.Date != "" {
+		t.Errorf("Expected firmware BIOS date to be empty but got: %s", firmware)
+	}
+
+	// Ensure we can handle unexpected types
+	firmware = getFirmwareDetails(introspection.ExtraHardwareDataSection{
+		"bios": {
+			"vendor":  3,
+			"version": []int{2, 1},
+			"date":    map[string]string{"year": "2019", "month": "07", "day": "10"},
+		},
+	})
+
+	// Finally, ensure we can handle completely empty firmware data
+	firmware = getFirmwareDetails(introspection.ExtraHardwareDataSection{})
+
+	if (firmware != metal3v1alpha1.Firmware{}) {
+		t.Errorf("Expected firmware data to be empty but got: %s", firmware)
+	}
+
+}
diff --git a/pkg/provisioner/ironic/ironic_test.go b/pkg/provisioner/ironic/ironic_test.go
index dfe8f6cd..b2975459 100644
--- a/pkg/provisioner/ironic/ironic_test.go
+++ b/pkg/provisioner/ironic/ironic_test.go
@@ -1,14 +1,12 @@
 package ironic
 
 import (
-	"reflect"
 	"testing"
 
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
 
 	"github.com/gophercloud/gophercloud/openstack/baremetal/v1/nodes"
-	"github.com/gophercloud/gophercloud/openstack/baremetalintrospection/v1/introspection"
 
 	metal3v1alpha1 "github.com/metal3-io/baremetal-operator/pkg/apis/metal3/v1alpha1"
 	"github.com/metal3-io/baremetal-operator/pkg/bmc"
@@ -201,271 +199,3 @@ func TestGetUpdateOptsForNodeDell(t *testing.T) {
 		}
 	}
 }
-
-func TestGetVLANs(t *testing.T) {
-	vlans, vid := getVLANs(introspection.BaseInterfaceType{
-		LLDPProcessed: map[string]interface{}{
-			"switch_port_vlans": []map[string]interface{}{
-				{
-					"id":   1,
-					"name": "vlan1",
-				},
-				{
-					"id":   4094,
-					"name": "vlan4094",
-				},
-			},
-			"switch_port_untagged_vlan_id": 1,
-		},
-	})
-	if vid != 1 {
-		t.Errorf("Unexpected untagged VLAN ID %d", vid)
-	}
-	if len(vlans) != 2 {
-		t.Errorf("Expected 2 VLANs, got %d", len(vlans))
-	}
-	if (vlans[0] != metal3v1alpha1.VLAN{ID: 1, Name: "vlan1"}) {
-		t.Errorf("Unexpected VLAN %d %s", vlans[0].ID, vlans[0].Name)
-	}
-	if (vlans[1] != metal3v1alpha1.VLAN{ID: 4094, Name: "vlan4094"}) {
-		t.Errorf("Unexpected VLAN %d %s", vlans[1].ID, vlans[1].Name)
-	}
-}
-
-func TestGetVLANsMalformed(t *testing.T) {
-	vlans, vid := getVLANs(introspection.BaseInterfaceType{
-		LLDPProcessed: map[string]interface{}{
-			"switch_port_vlans": []map[string]interface{}{
-				{
-					"foo":  "bar",
-					"name": "vlan1",
-				},
-				{
-					"foo": "bar",
-					"id":  1,
-				},
-				{
-					"name": "vlan2",
-					"id":   "2",
-				},
-				{
-					"name": 3,
-					"id":   3,
-				},
-				{
-					"foo": "bar",
-				},
-			},
-			"switch_port_untagged_vlan_id": "1",
-		},
-	})
-	if vid != 0 {
-		t.Errorf("Unexpected untagged VLAN ID %d", vid)
-	}
-	if len(vlans) != 5 {
-		t.Errorf("Expected 5 VLANs, got %d", len(vlans))
-	}
-	if (vlans[0] != metal3v1alpha1.VLAN{Name: "vlan1"}) {
-		t.Errorf("Unexpected VLAN %d %s", vlans[0].ID, vlans[0].Name)
-	}
-	if (vlans[1] != metal3v1alpha1.VLAN{ID: 1}) {
-		t.Errorf("Unexpected VLAN %d %s", vlans[0].ID, vlans[0].Name)
-	}
-	if (vlans[2] != metal3v1alpha1.VLAN{Name: "vlan2"}) {
-		t.Errorf("Unexpected VLAN %d %s", vlans[0].ID, vlans[0].Name)
-	}
-	if (vlans[3] != metal3v1alpha1.VLAN{ID: 3}) {
-		t.Errorf("Unexpected VLAN %d %s", vlans[0].ID, vlans[0].Name)
-	}
-	if (vlans[4] != metal3v1alpha1.VLAN{}) {
-		t.Errorf("Unexpected VLAN %d %s", vlans[0].ID, vlans[0].Name)
-	}
-
-	vlans, vid = getVLANs(introspection.BaseInterfaceType{
-		LLDPProcessed: map[string]interface{}{
-			"switch_port_vlans": map[string]interface{}{
-				"foo": "bar",
-			},
-			"switch_port_untagged_vlan_id": "1",
-		},
-	})
-	if vid != 0 {
-		t.Errorf("Unexpected untagged VLAN ID %d", vid)
-	}
-	if len(vlans) != 0 {
-		t.Errorf("Expected 0 VLANs, got %d", len(vlans))
-	}
-
-	vlans, vid = getVLANs(introspection.BaseInterfaceType{
-		LLDPProcessed: map[string]interface{}{
-			"switch_port_vlans": []interface{}{
-				"foo",
-			},
-		},
-	})
-	if vid != 0 {
-		t.Errorf("Unexpected untagged VLAN ID %d", vid)
-	}
-	if len(vlans) != 0 {
-		t.Errorf("Expected 0 VLANs, got %d", len(vlans))
-	}
-
-	vlans, vid = getVLANs(introspection.BaseInterfaceType{
-		LLDPProcessed: map[string]interface{}{
-			"switch_port_vlans": "foo",
-		},
-	})
-	if vid != 0 {
-		t.Errorf("Unexpected untagged VLAN ID %d", vid)
-	}
-	if len(vlans) != 0 {
-		t.Errorf("Expected 0 VLANs, got %d", len(vlans))
-	}
-
-	vlans, vid = getVLANs(introspection.BaseInterfaceType{
-		LLDPProcessed: map[string]interface{}{},
-	})
-	if vid != 0 {
-		t.Errorf("Unexpected untagged VLAN ID %d", vid)
-	}
-	if len(vlans) != 0 {
-		t.Errorf("Expected 0 VLANs, got %d", len(vlans))
-	}
-
-	vlans, vid = getVLANs(introspection.BaseInterfaceType{})
-	if vid != 0 {
-		t.Errorf("Unexpected untagged VLAN ID %d", vid)
-	}
-	if len(vlans) != 0 {
-		t.Errorf("Expected 0 VLANs, got %d", len(vlans))
-	}
-}
-
-func TestGetNICDetails(t *testing.T) {
-	nics := getNICDetails(
-		[]introspection.InterfaceType{
-			{
-				Name:        "eth0",
-				IPV4Address: "192.0.2.1",
-				MACAddress:  "00:11:22:33:44:55"},
-			{
-				Name:        "eth1",
-				IPV6Address: "2001:db8::1",
-				MACAddress:  "66:77:88:99:aa:bb"},
-		},
-		map[string]introspection.BaseInterfaceType{
-			"eth0": {
-				PXE: true,
-				LLDPProcessed: map[string]interface{}{
-					"switch_port_vlans": []map[string]interface{}{
-						{
-							"id": 1,
-						},
-					},
-					"switch_port_untagged_vlan_id": 1,
-				},
-			},
-		},
-		introspection.ExtraHardwareDataSection{
-			"eth1": introspection.ExtraHardwareData{
-				"speed": "1Gbps",
-			},
-		})
-
-	if len(nics) != 2 {
-		t.Errorf("Expected 2 NICs, got %d", len(nics))
-	}
-	if (!reflect.DeepEqual(nics[0], metal3v1alpha1.NIC{
-		Name: "eth0",
-		MAC:  "00:11:22:33:44:55",
-		IP:   "192.0.2.1",
-		PXE:  true,
-		VLANs: []metal3v1alpha1.VLAN{
-			{ID: 1},
-		},
-		VLANID: 1,
-	})) {
-		t.Errorf("Unexpected NIC data")
-	}
-	if (!reflect.DeepEqual(nics[1], metal3v1alpha1.NIC{
-		Name:      "eth1",
-		MAC:       "66:77:88:99:aa:bb",
-		IP:        "2001:db8::1",
-		SpeedGbps: 1,
-	})) {
-		t.Errorf("Unexpected NIC data")
-	}
-}
-
-func TestGetNICSpeedGbps(t *testing.T) {
-	s1 := getNICSpeedGbps(introspection.ExtraHardwareData{
-		"speed": "25Gbps",
-	})
-	if s1 != 25 {
-		t.Errorf("Expected speed 25, got %d", s1)
-	}
-
-	s2 := getNICSpeedGbps(introspection.ExtraHardwareData{
-		"speed": "100Mbps",
-	})
-	if s2 != 0 {
-		t.Errorf("Expected speed 0, got %d", s2)
-	}
-
-	s3 := getNICSpeedGbps(introspection.ExtraHardwareData{
-		"speed": 10,
-	})
-	if s3 != 0 {
-		t.Errorf("Expected speed 0, got %d", s3)
-	}
-
-	s4 := getNICSpeedGbps(introspection.ExtraHardwareData{})
-	if s4 != 0 {
-		t.Errorf("Expected speed 0, got %d", s4)
-	}
-}
-
-func TestGetFirmwareDetails(t *testing.T) {
-
-	// Test full (known) firmware payload
-	firmware := getFirmwareDetails(introspection.ExtraHardwareDataSection{
-		"bios": {
-			"vendor":  "foobar",
-			"version": "1.2.3",
-			"date":    "2019-07-10",
-		},
-	})
-
-	if firmware.BIOS.Vendor != "foobar" {
-		t.Errorf("Expected firmware BIOS vendor to be foobar, but got: %s", firmware)
-	}
-
-	// Ensure we can handle partial firmware/bios data
-	firmware = getFirmwareDetails(introspection.ExtraHardwareDataSection{
-		"bios": {
-			"vendor":  "foobar",
-			"version": "1.2.3",
-		},
-	})
-
-	if firmware.BIOS.Date != "" {
-		t.Errorf("Expected firmware BIOS date to be empty but got: %s", firmware)
-	}
-
-	// Ensure we can handle unexpected types
-	firmware = getFirmwareDetails(introspection.ExtraHardwareDataSection{
-		"bios": {
-			"vendor":  3,
-			"version": []int{2, 1},
-			"date":    map[string]string{"year": "2019", "month": "07", "day": "10"},
-		},
-	})
-
-	// Finally, ensure we can handle completely empty firmware data
-	firmware = getFirmwareDetails(introspection.ExtraHardwareDataSection{})
-
-	if (firmware != metal3v1alpha1.Firmware{}) {
-		t.Errorf("Expected firmware data to be empty but got: %s", firmware)
-	}
-
-}