Repository: metal3-io/baremetal-operator
Pull request #1623 (2024-03-15 07:09:59)

:sparkles: Add nonbootable iso api
**What this PR does / why we need it**:
This PR defines the API for attaching and detaching non-bootable iso image. The Controller implementation for the API is in progress in this pr : https://github.com/metal3-io/baremetal-operator/pull/1594 


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

This pull-request has been approved by:
**Once this PR has been reviewed and has the lgtm label**, please assign [honza](https://github.com/honza) for approval. For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).

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

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

- **[OWNERS](https://github.com/metal3-io/baremetal-operator/blob/main/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["honza"]} -->
dtantsur: The previous PR modifies a few more API-related files, such as config/base/crds/kustomization.yaml, a sample or RBAC roles. Could you add them too?

Patch:
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 32d89893..8fbebf20 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -10,7 +10,7 @@ permissions: {}
 
 jobs:
   build:
-    name: tag release
+    name: release
     runs-on: ubuntu-latest
 
     permissions:
@@ -36,4 +36,4 @@ jobs:
       with:
         draft: true
         files: out/*
-        body_path: releasenotes/${{ env.RELEASE_TAG }}.md
+        body_path: releasenotes/releasenotes.md
diff --git a/Dockerfile b/Dockerfile
index cd9aa58d..7447f457 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
 # Support FROM override
-ARG BUILD_IMAGE=docker.io/golang:1.21.8@sha256:856073656d1a517517792e6cdd2f7a5ef080d3ca2dff33e518c8412f140fdd2d
+ARG BUILD_IMAGE=docker.io/golang:1.21.7@sha256:549dd88a1a53715f177b41ab5fee25f7a376a6bb5322ac7abe263480d9554021
 ARG BASE_IMAGE=gcr.io/distroless/static:nonroot@sha256:9ecc53c269509f63c69a266168e4a687c7eb8c0cfd753bd8bfcaa4f58a90876f
 
 # Build the manager binary
diff --git a/Makefile b/Makefile
index 71b600fe..f898bdde 100644
--- a/Makefile
+++ b/Makefile
@@ -2,7 +2,7 @@ RUN_NAMESPACE = metal3
 GO_TEST_FLAGS = $(TEST_FLAGS)
 DEBUG = --debug
 COVER_PROFILE = cover.out
-GO_VERSION ?= 1.21.8
+GO_VERSION ?= 1.21.7
 
 ROOT_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
 
@@ -128,7 +128,7 @@ test-e2e: $(GINKGO) ## Run the end-to-end tests
 ## --------------------------------------
 
 .PHONY: linters
-linters: lint generate-check
+linters: lint generate-check fmt-check
 
 $(GOLANGCI_LINT):
 	GOBIN=$(TOOLS_BIN_DIR) go install $(GOLANGCI_LINT_PKG)@$(GOLANGCI_LINT_VER)
@@ -167,7 +167,7 @@ demo: generate lint manifests ## Run in demo mode
 	go run -ldflags $(LDFLAGS) ./main.go -namespace=$(RUN_NAMESPACE) -dev -demo-mode -webhook-port=0 $(RUN_FLAGS)
 
 .PHONY: run-test-mode
-run-test-mode: generate lint manifests ## Run against the configured Kubernetes cluster in ~/.kube/config
+run-test-mode: generate fmt-check lint manifests ## Run against the configured Kubernetes cluster in ~/.kube/config
 	go run -ldflags $(LDFLAGS) ./main.go -namespace=$(RUN_NAMESPACE) -dev -test-mode -webhook-port=0 $(RUN_FLAGS)
 
 .PHONY: install
@@ -260,6 +260,14 @@ generate-check:
 generate-check-local:
 	IS_CONTAINER=local ./hack/generate.sh
 
+.PHONY: fmt-check
+fmt-check: ## Run gofmt and report an error if any changes are made
+	./hack/gofmt.sh
+
+.PHONY: fmt
+fmt: ## Run gofmt and fix files with formatting issues
+	gofmt -s -w .
+
 ## --------------------------------------
 ## Documentation
 ## --------------------------------------
@@ -320,15 +328,15 @@ mod: ## Clean up go module settings
 ## Release
 ## --------------------------------------
 RELEASE_TAG ?= $(shell git describe --abbrev=0 2>/dev/null)
+PREVIOUS_TAG ?= $(shell git tag -l | grep -B 1 "^$(RELEASE_TAG)" | head -n 1)
 RELEASE_NOTES_DIR := releasenotes
-PREVIOUS_TAG ?= $(shell git tag -l | grep -E "^v[0-9]+\.[0-9]+\.[0-9]+" | sort -V | grep -B1 $(RELEASE_TAG) | grep -E "^v[0-9]+\.[0-9]+\.[0-9]+$$" | head -n 1 2>/dev/null)
 
 $(RELEASE_NOTES_DIR):
 	mkdir -p $(RELEASE_NOTES_DIR)/
 
 .PHONY: release-notes
 release-notes: $(RELEASE_NOTES_DIR)
-	go run ./hack/tools/release/notes.go --from=$(PREVIOUS_TAG) > $(RELEASE_NOTES_DIR)/$(RELEASE_TAG).md
+	go run ./hack/tools/release_notes.go --from=$(PREVIOUS_TAG) > $(RELEASE_NOTES_DIR)/releasenotes.md
 
 go-version: ## Print the go version we use to compile our binaries and images
 	@echo $(GO_VERSION)
diff --git a/PROJECT b/PROJECT
index 5241f3d5..01f6bffc 100644
--- a/PROJECT
+++ b/PROJECT
@@ -69,4 +69,13 @@ resources:
   kind: HostFirmwareComponents
   path: github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha
   version: v1alpha1
+- api:
+    crdVersion: v1
+    namespaced: true
+  controller: true
+  domain: metal3.io
+  group: metal3.io
+  kind: DataImage
+  path: github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1
+  version: v1alpha1
 version: "3"
diff --git a/apis/metal3.io/v1alpha1/dataimage_types.go b/apis/metal3.io/v1alpha1/dataimage_types.go
new file mode 100644
index 00000000..c2807e81
--- /dev/null
+++ b/apis/metal3.io/v1alpha1/dataimage_types.go
@@ -0,0 +1,79 @@
+/*
+
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1alpha1
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+const DataImageFinalizer = "dataimage.metal3.io"
+
+// Contains the DataImage currently attached to the BMH.
+type AttachedImageReference struct {
+	URL string `json:"url"`
+}
+
+// Contains the count of errors and the last error message.
+type DataImageError struct {
+	Count   int    `json:"count"`
+	Message string `json:"message"`
+}
+
+// DataImageSpec defines the desired state of DataImage.
+type DataImageSpec struct {
+	// Url is the address of the dataImage that we want to attach
+	// to a BareMetalHost
+	URL string `json:"url"`
+}
+
+// DataImageStatus defines the observed state of DataImage.
+type DataImageStatus struct {
+	// Time of last reconciliation
+	// +optional
+	LastReconciled *metav1.Time `json:"lastReconciled,omitempty"`
+
+	// Currently attached DataImage
+	AttachedImage *AttachedImageReference `json:"attachedImage,omitempty"`
+
+	// Error count and message when attaching/detaching
+	Error *DataImageError `json:"error,omitempty"`
+}
+
+//+kubebuilder:object:root=true
+//+kubebuilder:subresource:status
+
+// DataImage is the Schema for the dataimages API.
+type DataImage struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec   DataImageSpec   `json:"spec,omitempty"`
+	Status DataImageStatus `json:"status,omitempty"`
+}
+
+//+kubebuilder:object:root=true
+
+// DataImageList contains a list of DataImage.
+type DataImageList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []DataImage `json:"items"`
+}
+
+func init() {
+	SchemeBuilder.Register(&DataImage{}, &DataImageList{})
+}
diff --git a/apis/metal3.io/v1alpha1/zz_generated.deepcopy.go b/apis/metal3.io/v1alpha1/zz_generated.deepcopy.go
index 59c29f2d..21843aa7 100644
--- a/apis/metal3.io/v1alpha1/zz_generated.deepcopy.go
+++ b/apis/metal3.io/v1alpha1/zz_generated.deepcopy.go
@@ -27,6 +27,21 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 )
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AttachedImageReference) DeepCopyInto(out *AttachedImageReference) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AttachedImageReference.
+func (in *AttachedImageReference) DeepCopy() *AttachedImageReference {
+	if in == nil {
+		return nil
+	}
+	out := new(AttachedImageReference)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *BIOS) DeepCopyInto(out *BIOS) {
 	*out = *in
@@ -361,6 +376,124 @@ func (in *CustomDeploy) DeepCopy() *CustomDeploy {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *DataImage) DeepCopyInto(out *DataImage) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	out.Spec = in.Spec
+	in.Status.DeepCopyInto(&out.Status)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataImage.
+func (in *DataImage) DeepCopy() *DataImage {
+	if in == nil {
+		return nil
+	}
+	out := new(DataImage)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *DataImage) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *DataImageError) DeepCopyInto(out *DataImageError) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataImageError.
+func (in *DataImageError) DeepCopy() *DataImageError {
+	if in == nil {
+		return nil
+	}
+	out := new(DataImageError)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *DataImageList) DeepCopyInto(out *DataImageList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]DataImage, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataImageList.
+func (in *DataImageList) DeepCopy() *DataImageList {
+	if in == nil {
+		return nil
+	}
+	out := new(DataImageList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *DataImageList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *DataImageSpec) DeepCopyInto(out *DataImageSpec) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataImageSpec.
+func (in *DataImageSpec) DeepCopy() *DataImageSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(DataImageSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *DataImageStatus) DeepCopyInto(out *DataImageStatus) {
+	*out = *in
+	if in.LastReconciled != nil {
+		in, out := &in.LastReconciled, &out.LastReconciled
+		*out = (*in).DeepCopy()
+	}
+	if in.AttachedImage != nil {
+		in, out := &in.AttachedImage, &out.AttachedImage
+		*out = new(AttachedImageReference)
+		**out = **in
+	}
+	if in.Error != nil {
+		in, out := &in.Error, &out.Error
+		*out = new(DataImageError)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataImageStatus.
+func (in *DataImageStatus) DeepCopy() *DataImageStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(DataImageStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in DesiredSettingsMap) DeepCopyInto(out *DesiredSettingsMap) {
 	{
diff --git a/config/base/crds/bases/metal3.io_dataimages.yaml b/config/base/crds/bases/metal3.io_dataimages.yaml
new file mode 100644
index 00000000..bbaec875
--- /dev/null
+++ b/config/base/crds/bases/metal3.io_dataimages.yaml
@@ -0,0 +1,75 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.12.1
+  name: dataimages.metal3.io
+spec:
+  group: metal3.io
+  names:
+    kind: DataImage
+    listKind: DataImageList
+    plural: dataimages
+    singular: dataimage
+  scope: Namespaced
+  versions:
+  - name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: DataImage is the Schema for the dataimages API.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: DataImageSpec defines the desired state of DataImage.
+            properties:
+              url:
+                description: Url is the address of the dataImage that we want to attach
+                  to a BareMetalHost
+                type: string
+            required:
+            - url
+            type: object
+          status:
+            description: DataImageStatus defines the observed state of DataImage.
+            properties:
+              attachedImage:
+                description: Currently attached DataImage
+                properties:
+                  url:
+                    type: string
+                required:
+                - url
+                type: object
+              error:
+                description: Error count and message when attaching/detaching
+                properties:
+                  count:
+                    type: integer
+                  message:
+                    type: string
+                required:
+                - count
+                - message
+                type: object
+              lastReconciled:
+                description: Time of last reconciliation
+                format: date-time
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
diff --git a/config/base/crds/kustomization.yaml b/config/base/crds/kustomization.yaml
index 85b4033a..3aae0699 100644
--- a/config/base/crds/kustomization.yaml
+++ b/config/base/crds/kustomization.yaml
@@ -8,6 +8,7 @@ resources:
 - bases/metal3.io_preprovisioningimages.yaml
 - bases/metal3.io_bmceventsubscriptions.yaml
 - bases/metal3.io_hardwaredata.yaml
+- bases/metal3.io_dataimages.yaml
 #+kubebuilder:scaffold:crdkustomizeresource
 
 patches:
@@ -19,6 +20,7 @@ patches:
 #- patches/webhook_in_preprovisioningimages.yaml
 #- patches/webhook_in_bmceventsubscriptions.yaml
 #- patches/webhook_in_hardwaredata.yaml
+#- patches/webhook_in_dataimages.yaml
 #+kubebuilder:scaffold:crdkustomizewebhookpatch
 
 # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix.
@@ -29,6 +31,7 @@ patches:
 #- patches/cainjection_in_preprovisioningimages.yaml
 #- patches/cainjection_in_bmceventsubscriptions.yaml
 #- patches/cainjection_in_hardwaredata.yaml
+#- patches/cainjection_in_dataimages.yaml
 #+kubebuilder:scaffold:crdkustomizecainjectionpatch
 
 # the following config is for teaching kustomize how to do kustomization for CRDs.
diff --git a/config/base/crds/patches/cainjection_in_dataimages.yaml b/config/base/crds/patches/cainjection_in_dataimages.yaml
new file mode 100644
index 00000000..5e919613
--- /dev/null
+++ b/config/base/crds/patches/cainjection_in_dataimages.yaml
@@ -0,0 +1,7 @@
+# The following patch adds a directive for certmanager to inject CA into the CRD
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME)
+  name: dataimages.metal3.io
diff --git a/config/base/crds/patches/webhook_in_dataimages.yaml b/config/base/crds/patches/webhook_in_dataimages.yaml
new file mode 100644
index 00000000..71ae1d6f
--- /dev/null
+++ b/config/base/crds/patches/webhook_in_dataimages.yaml
@@ -0,0 +1,17 @@
+# The following patch enables a conversion webhook for the CRD
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  name: dataimages.metal3.io
+spec:
+  conversion:
+    strategy: Webhook
+    webhook:
+      clientConfig:
+        service:
+          namespace: system
+          name: webhook-service
+          path: /convert
+        caBundle: Cg==
+      conversionReviewVersions:
+      - v1
diff --git a/config/render/capm3.yaml b/config/render/capm3.yaml
index e1d24366..1f5ea863 100644
--- a/config/render/capm3.yaml
+++ b/config/render/capm3.yaml
@@ -1229,6 +1229,81 @@ spec:
 ---
 apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.12.1
+  name: dataimages.metal3.io
+spec:
+  group: metal3.io
+  names:
+    kind: DataImage
+    listKind: DataImageList
+    plural: dataimages
+    singular: dataimage
+  scope: Namespaced
+  versions:
+  - name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: DataImage is the Schema for the dataimages API.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: DataImageSpec defines the desired state of DataImage.
+            properties:
+              url:
+                description: Url is the address of the dataImage that we want to attach
+                  to a BareMetalHost
+                type: string
+            required:
+            - url
+            type: object
+          status:
+            description: DataImageStatus defines the observed state of DataImage.
+            properties:
+              attachedImage:
+                description: Currently attached DataImage
+                properties:
+                  url:
+                    type: string
+                required:
+                - url
+                type: object
+              error:
+                description: Error count and message when attaching/detaching
+                properties:
+                  count:
+                    type: integer
+                  message:
+                    type: string
+                required:
+                - count
+                - message
+                type: object
+              lastReconciled:
+                description: Time of last reconciliation
+                format: date-time
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
 metadata:
   annotations:
     controller-gen.kubebuilder.io/version: v0.12.1
diff --git a/config/samples/metal3.io_v1alpha1_dataimage.yaml b/config/samples/metal3.io_v1alpha1_dataimage.yaml
new file mode 100644
index 00000000..0b09452b
--- /dev/null
+++ b/config/samples/metal3.io_v1alpha1_dataimage.yaml
@@ -0,0 +1,6 @@
+apiVersion: metal3.io/v1alpha1
+kind: DataImage
+metadata:
+  name: dataimage-sample
+spec:
+  url: "http://dataimage.example.com/non-bootable.iso"
diff --git a/docs/api.md b/docs/api.md
index 9ad98ce4..af8d6c39 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -36,9 +36,11 @@ communicate with them.
 | Generic IPMI    | IPMI     | iPXE          | `ipmi://<host>:<port>` or just `<host>:<port>`    | Port is optional, defaults to 623                                       |
 | Generic Redfish | Redfish  | iPXE          | `redfish://<host>:<port>/<systemID>`              | System ID is a path like `/redfish/v1/Systems/System.Embedded.1`        |
 |                 |          | Virtual media | `redfish-virtualmedia://<host>:<port>/<systemID>` | Virtual media support is vendor-dependent. Should not be used for Dell. |
-| Dell iDRAC      | Redfish  | iPXE          | `idrac-redfish://<host>:<port>/<systemID>`        | See above about system ID.                                              |
+| Dell iDRAC      | WSMAN    | iPXE          | `idrac://<host>:<port>`                           |                                                                         |
+|                 | Redfish  | iPXE          | `idrac-redfish://<host>:<port>/<systemID>`        | See above about system ID.                                              |
 |                 | Redfish  | Virtual media | `idrac-virtualmedia://<host>:<port>/<systemID>`   | See above about system ID.                                              |
 | Fujitsu iRMC    | iRMC     | iPXE          | `irmc://<host>:<port>`                            | Port is optional, the default is 443.                                   |
+| HUAWEI ibmc     | ibmc     | iPXE          | `ibmc://<host>:<port>`                            |                                                                         |
 | HPE iLO 4       | iLO      | iPXE          | `ilo4://<host>:<port>`                            | Port is optional, the default is 443.                                   |
 |                 | iLO      | Virtual media | `ilo4-virtualmedia://<host>:<port>`               |                                                                         |
 | HPE iLO 5       | iLO      | iPXE          | `ilo5://<host>:<port>`                            |                                                                         |
diff --git a/hack/gofmt.sh b/hack/gofmt.sh
new file mode 100755
index 00000000..81084cba
--- /dev/null
+++ b/hack/gofmt.sh
@@ -0,0 +1,27 @@
+#!/bin/sh
+
+set -eux
+
+IS_CONTAINER=${IS_CONTAINER:-false}
+CONTAINER_RUNTIME="${CONTAINER_RUNTIME:-podman}"
+
+if [ "${IS_CONTAINER}" != "false" ]; then
+  TOP_DIR="${1:-.}"
+  export XDG_CACHE_HOME="/tmp/.cache"
+
+  cd "${TOP_DIR}"
+
+  # shellcheck disable=SC2086
+  if [ -n "$(gofmt -l .)" ]; then
+      gofmt -s -d -e .
+      exit 1
+  fi
+else
+  "${CONTAINER_RUNTIME}" run --rm \
+    --env IS_CONTAINER=TRUE \
+    --volume "${PWD}:/workdir:rw,z" \
+    --entrypoint sh \
+    --workdir /workdir \
+    docker.io/golang:1.21 \
+    /workdir/hack/gofmt.sh "${@}"
+fi;
diff --git a/hack/gosec.sh b/hack/gosec.sh
new file mode 100755
index 00000000..0b809822
--- /dev/null
+++ b/hack/gosec.sh
@@ -0,0 +1,24 @@
+#!/bin/sh
+
+set -eux
+
+IS_CONTAINER=${IS_CONTAINER:-false}
+CONTAINER_RUNTIME="${CONTAINER_RUNTIME:-podman}"
+
+if [ "${IS_CONTAINER}" != "false" ]; then
+  export XDG_CACHE_HOME="/tmp/.cache"
+
+  # It seems like gosec does not handle submodules well. Therefore we skip them and run separately.
+  gosec -severity medium --confidence medium -quiet -exclude-dir=apis -exclude-dir=hack/tools -exclude-dir=test ./...
+  (cd apis && gosec -severity medium --confidence medium -quiet ./...)
+  (cd hack/tools && gosec -severity medium --confidence medium -quiet ./...)
+  (cd test && gosec -severity medium --confidence medium -quiet ./...)
+else
+  "${CONTAINER_RUNTIME}" run --rm \
+    --env IS_CONTAINER=TRUE \
+    --volume "${PWD}:/go/src/github.com/metal3-io/baremetal-operator:ro,z" \
+    --entrypoint sh \
+    --workdir /go/src/github.com/metal3-io/baremetal-operator \
+    docker.io/securego/gosec:2.18.2@sha256:2f9daee1739765788945b79de7f46229f33fda5ed35127393d8a1e459f3a7577 \
+    /go/src/github.com/metal3-io/baremetal-operator/hack/gosec.sh "$@"
+fi
diff --git a/hack/tools/release/notes.go b/hack/tools/release_notes.go
similarity index 79%
rename from hack/tools/release/notes.go
rename to hack/tools/release_notes.go
index 0283d6f4..9446f5d9 100644
--- a/hack/tools/release/notes.go
+++ b/hack/tools/release_notes.go
@@ -45,10 +45,6 @@ const (
 	superseded    = ":recycle: Superseded or Reverted"
 )
 
-const (
-	warningTemplate = ":rotating_light: This is a %s. Use it only for testing purposes. If you find any bugs, file an [issue](https://github.com/metal3-io/baremetal-operator/issues/new/).\n\n"
-)
-
 var (
 	outputOrder = []string{
 		warning,
@@ -89,14 +85,6 @@ func lastTag() string {
 	return string(bytes.TrimSpace(out))
 }
 
-func isBeta(tag string) bool {
-	return strings.Contains(tag, "-beta.")
-}
-
-func isRC(tag string) bool {
-	return strings.Contains(tag, "-rc.")
-}
-
 func firstCommit() string {
 	cmd := exec.Command("git", "rev-list", "--max-parents=0", "HEAD")
 	out, err := cmd.Output()
@@ -109,7 +97,7 @@ func firstCommit() string {
 func run() int {
 	lastTag := lastTag()
 	latestTag := latestTag()
-	cmd := exec.Command("git", "rev-list", lastTag+"..HEAD", "--merges", "--pretty=format:%B") // #nosec G204:gosec
+	cmd := exec.Command("git", "rev-list", lastTag+"..HEAD", "--merges", "--pretty=format:%B")
 
 	merges := map[string][]string{
 		features:      {},
@@ -186,15 +174,12 @@ func run() int {
 		merges[key] = append(merges[key], formatMerge(body, prNumber))
 	}
 
-	// Add empty superseded section, if not beta/rc, we don't cleanup those notes
-	if !isBeta(latestTag) && !isRC(latestTag) {
-		merges[superseded] = append(merges[superseded], "- `<insert superseded bumps and reverts here>`")
-	}
+	// Add empty superseded section
+	merges[superseded] = append(merges[superseded], "- `<insert superseded bumps and reverts here>`")
 
 	// TODO Turn this into a link (requires knowing the project name + organization)
 	fmt.Printf("Changes since %v\n---\n", lastTag)
 
-	// print the changes by category
 	for _, key := range outputOrder {
 		mergeslice := merges[key]
 		if len(mergeslice) > 0 {
@@ -204,29 +189,10 @@ func run() int {
 			}
 			fmt.Println()
 		}
-
-		// if we're doing beta/rc, print breaking changes and hide the rest of the changes
-		if key == warning {
-			if isBeta(latestTag) {
-				fmt.Printf(warningTemplate, "BETA RELEASE")
-			}
-			if isRC(latestTag) {
-				fmt.Printf(warningTemplate, "RELEASE CANDIDATE")
-			}
-			if isBeta(latestTag) || isRC(latestTag) {
-				fmt.Printf("<details>\n")
-				fmt.Printf("<summary>More details about the release</summary>\n\n")
-			}
-		}
-	}
-
-	// then close the details if we had it open
-	if isBeta(latestTag) || isRC(latestTag) {
-		fmt.Printf("</details>\n\n")
 	}
 
-	fmt.Printf("The image for this release is: %v\n", latestTag)
-	fmt.Println("\n_Thanks to all our contributors!_ 😊")
+	fmt.Printf("The container image for this release is: %v\n", latestTag)
+	fmt.Println("\nThanks to all our contributors! 😊")
 
 	return 0
 }
diff --git a/pkg/hardwareutils/bmc/access.go b/pkg/hardwareutils/bmc/access.go
index d3860286..81830c06 100644
--- a/pkg/hardwareutils/bmc/access.go
+++ b/pkg/hardwareutils/bmc/access.go
@@ -9,13 +9,6 @@ import (
 	"strings"
 )
 
-const (
-	noRaid   = "no-raid"
-	ipxe     = "ipxe"
-	enabled  = "Enabled"
-	disabled = "Disabled"
-)
-
 // AccessDetailsFactory describes a callable that returns a new
 // AccessDetails based on the input parameters.
 type AccessDetailsFactory func(parsedURL *url.URL, disableCertificateVerification bool) (AccessDetails, error)
diff --git a/pkg/hardwareutils/bmc/ibmc.go b/pkg/hardwareutils/bmc/ibmc.go
new file mode 100644
index 00000000..24227b39
--- /dev/null
+++ b/pkg/hardwareutils/bmc/ibmc.go
@@ -0,0 +1,132 @@
+package bmc
+
+import (
+	"fmt"
+	"net/url"
+	"strings"
+)
+
+const (
+	noRaid = "no-raid"
+	ibmc   = "ibmc"
+	ipxe   = "ipxe"
+)
+
+func init() {
+	RegisterFactory(ibmc, newIbmcAccessDetails, []string{"http", "https"})
+}
+
+func newIbmcAccessDetails(parsedURL *url.URL, disableCertificateVerification bool) (AccessDetails, error) {
+	return &ibmcAccessDetails{
+		bmcType:                        parsedURL.Scheme,
+		host:                           parsedURL.Host,
+		path:                           parsedURL.Path,
+		disableCertificateVerification: disableCertificateVerification,
+	}, nil
+}
+
+type ibmcAccessDetails struct {
+	bmcType                        string
+	host                           string
+	path                           string
+	disableCertificateVerification bool
+}
+
+func (a *ibmcAccessDetails) Type() string {
+	return a.bmcType
+}
+
+// NeedsMAC returns true when the host is going to need a separate
+// port created rather than having it discovered.
+func (a *ibmcAccessDetails) NeedsMAC() bool {
+	// For the inspection to work, we need a MAC address
+	// https://github.com/metal3-io/baremetal-operator/pull/284#discussion_r317579040
+	return true
+}
+
+func (a *ibmcAccessDetails) Driver() string {
+	return ibmc
+}
+
+func (a *ibmcAccessDetails) DisableCertificateVerification() bool {
+	return a.disableCertificateVerification
+}
+
+const ibmcDefaultScheme = "https"
+
+// DriverInfo returns a data structure to pass as the DriverInfo
+// parameter when creating a node in Ironic. The structure is
+// pre-populated with the access information, and the caller is
+// expected to add any other information that might be needed (such as
+// the kernel and ramdisk locations).
+func (a *ibmcAccessDetails) DriverInfo(bmcCreds Credentials) map[string]interface{} {
+	ibmcAddress := []string{}
+	schemes := strings.Split(a.bmcType, "+")
+	if len(schemes) > 1 {
+		ibmcAddress = append(ibmcAddress, schemes[1])
+	} else {
+		ibmcAddress = append(ibmcAddress, ibmcDefaultScheme)
+	}
+	ibmcAddress = append(ibmcAddress, "://")
+	ibmcAddress = append(ibmcAddress, a.host)
+	ibmcAddress = append(ibmcAddress, a.path)
+
+	result := map[string]interface{}{
+		"ibmc_username": bmcCreds.Username,
+		"ibmc_password": bmcCreds.Password,
+		"ibmc_address":  strings.Join(ibmcAddress, ""),
+	}
+
+	if a.disableCertificateVerification {
+		result["ibmc_verify_ca"] = false
+	}
+
+	return result
+}
+
+func (a *ibmcAccessDetails) BIOSInterface() string {
+	return ""
+}
+
+func (a *ibmcAccessDetails) BootInterface() string {
+	return ipxe
+}
+
+func (a *ibmcAccessDetails) FirmwareInterface() string {
+	return ""
+}
+
+func (a *ibmcAccessDetails) ManagementInterface() string {
+	return ibmc
+}
+
+func (a *ibmcAccessDetails) PowerInterface() string {
+	return ibmc
+}
+
+func (a *ibmcAccessDetails) RAIDInterface() string {
+	return noRaid
+}
+
+func (a *ibmcAccessDetails) VendorInterface() string {
+	return ""
+}
+
+func (a *ibmcAccessDetails) SupportsSecureBoot() bool {
+	return false
+}
+
+func (a *ibmcAccessDetails) SupportsISOPreprovisioningImage() bool {
+	return false
+}
+
+func (a *ibmcAccessDetails) RequiresProvisioningNetwork() bool {
+	return true
+}
+
+func (a *ibmcAccessDetails) BuildBIOSSettings(firmwareConfig *FirmwareConfig) (settings []map[string]string, err error) {
+	if firmwareConfig != nil {
+		return nil, fmt.Errorf("firmware settings for %s are not supported", a.Driver())
+	}
+	return nil, nil
+}
diff --git a/pkg/hardwareutils/bmc/idrac.go b/pkg/hardwareutils/bmc/idrac.go
new file mode 100644
index 00000000..3744c44a
--- /dev/null
+++ b/pkg/hardwareutils/bmc/idrac.go
@@ -0,0 +1,172 @@
+package bmc
+
+import (
+	"net/url"
+	"strings"
+)
+
+const (
+	enabled  = "Enabled"
+	disabled = "Disabled"
+	idrac    = "idrac"
+)
+
+func init() {
+	RegisterFactory(idrac, newIDRACAccessDetails, []string{"http", "https"})
+}
+
+func newIDRACAccessDetails(parsedURL *url.URL, disableCertificateVerification bool) (AccessDetails, error) {
+	return &iDracAccessDetails{
+		bmcType:                        parsedURL.Scheme,
+		portNum:                        parsedURL.Port(),
+		hostname:                       parsedURL.Hostname(),
+		path:                           parsedURL.Path,
+		disableCertificateVerification: disableCertificateVerification,
+	}, nil
+}
+
+type iDracAccessDetails struct {
+	bmcType                        string
+	portNum                        string
+	hostname                       string
+	path                           string
+	disableCertificateVerification bool
+}
+
+func (a *iDracAccessDetails) Type() string {
+	return a.bmcType
+}
+
+// NeedsMAC returns true when the host is going to need a separate
+// port created rather than having it discovered.
+func (a *iDracAccessDetails) NeedsMAC() bool {
+	return false
+}
+
+func (a *iDracAccessDetails) Driver() string {
+	return idrac
+}
+
+func (a *iDracAccessDetails) DisableCertificateVerification() bool {
+	return a.disableCertificateVerification
+}
+
+// DriverInfo returns a data structure to pass as the DriverInfo
+// parameter when creating a node in Ironic. The structure is
+// pre-populated with the access information, and the caller is
+// expected to add any other information that might be needed (such as
+// the kernel and ramdisk locations).
+func (a *iDracAccessDetails) DriverInfo(bmcCreds Credentials) map[string]interface{} {
+	result := map[string]interface{}{
+		"drac_username": bmcCreds.Username,
+		"drac_password": bmcCreds.Password,
+		"drac_address":  a.hostname,
+	}
+	if a.disableCertificateVerification {
+		result["drac_verify_ca"] = false
+	}
+
+	schemes := strings.Split(a.bmcType, "+")
+	if len(schemes) > 1 {
+		result["drac_protocol"] = schemes[1]
+	}
+	if a.portNum != "" {
+		result["drac_port"] = a.portNum
+	}
+	if a.path != "" {
+		result["drac_path"] = a.path
+	}
+
+	return result
+}
+
+func (a *iDracAccessDetails) BIOSInterface() string {
+	return ""
+}
+
+func (a *iDracAccessDetails) BootInterface() string {
+	return ipxe
+}
+
+func (a *iDracAccessDetails) FirmwareInterface() string {
+	return ""
+}
+
+func (a *iDracAccessDetails) ManagementInterface() string {
+	return ""
+}
+
+func (a *iDracAccessDetails) PowerInterface() string {
+	return ""
+}
+
+func (a *iDracAccessDetails) RAIDInterface() string {
+	return "idrac-wsman"
+}
+
+func (a *iDracAccessDetails) VendorInterface() string {
+	return ""
+}
+
+// NOTE(dtantsur): change to true if we switch to redfish-based implementations
+// by default.
+func (a *iDracAccessDetails) SupportsSecureBoot() bool {
+	return false
+}
+
+func (a *iDracAccessDetails) SupportsISOPreprovisioningImage() bool {
+	return false
+}
+
+func (a *iDracAccessDetails) RequiresProvisioningNetwork() bool {
+	return true
+}
+
+func (a *iDracAccessDetails) BuildBIOSSettings(firmwareConfig *FirmwareConfig) (settings []map[string]string, err error) {
+	if firmwareConfig == nil {
+		return nil, nil
+	}
+
+	var value string
+
+	if firmwareConfig.VirtualizationEnabled != nil {
+		value = disabled
+		if *firmwareConfig.VirtualizationEnabled {
+			value = enabled
+		}
+		settings = append(settings,
+			map[string]string{
+				"name":  "ProcVirtualization",
+				"value": value,
+			},
+		)
+	}
+
+	if firmwareConfig.SimultaneousMultithreadingEnabled != nil {
+		value = disabled
+		if *firmwareConfig.SimultaneousMultithreadingEnabled {
+			value = enabled
+		}
+		settings = append(settings,
+			map[string]string{
+				"name":  "LogicalProc",
+				"value": value,
+			},
+		)
+	}
+
+	if firmwareConfig.SriovEnabled != nil {
+		value = disabled
+		if *firmwareConfig.SriovEnabled {
+			value = enabled
+		}
+		settings = append(settings,
+			map[string]string{
+				"name":  "SriovGlobalEnable",
+				"value": value,
+			},
+		)
+	}
+
+	return
+}
diff --git a/pkg/hardwareutils/bmc/idrac_virtualmedia.go b/pkg/hardwareutils/bmc/idrac_virtualmedia.go
index 1ad56eda..8f6f4887 100644
--- a/pkg/hardwareutils/bmc/idrac_virtualmedia.go
+++ b/pkg/hardwareutils/bmc/idrac_virtualmedia.go
@@ -6,7 +6,6 @@ import (
 )
 
 const (
-	idrac        = "idrac"
 	idracRedfish = "idrac-redfish"
 	redfish      = "redfish"
 )


Test patch:
diff --git a/apis/metal3.io/v1alpha1/baremetalhost_validation_test.go b/apis/metal3.io/v1alpha1/baremetalhost_validation_test.go
index 0222249c..521a6a7e 100644
--- a/apis/metal3.io/v1alpha1/baremetalhost_validation_test.go
+++ b/apis/metal3.io/v1alpha1/baremetalhost_validation_test.go
@@ -303,7 +303,7 @@ func TestValidateCreate(t *testing.T) {
 				ObjectMeta: om,
 				Spec: BareMetalHostSpec{
 					BMC: BMCDetails{
-						Address:         "redfish://127.0.0.1",
+						Address:         "idrac://127.0.0.1",
 						CredentialsName: "test1",
 					},
 					RAID: &RAIDConfig{
@@ -329,7 +329,7 @@ func TestValidateCreate(t *testing.T) {
 				ObjectMeta: om,
 				Spec: BareMetalHostSpec{
 					BMC: BMCDetails{
-						Address:         "redfish://127.0.0.1",
+						Address:         "idrac://127.0.0.1",
 						CredentialsName: "test1",
 					},
 					RAID: &RAIDConfig{
@@ -501,7 +501,7 @@ func TestValidateCreate(t *testing.T) {
 				ObjectMeta: om,
 				Spec: BareMetalHostSpec{
 					BMC: BMCDetails{
-						Address:         "redfish://127.0.0.1",
+						Address:         "idrac://127.0.0.1",
 						CredentialsName: "test1",
 					},
 					Image: &Image{
diff --git a/pkg/hardwareutils/bmc/access_test.go b/pkg/hardwareutils/bmc/access_test.go
index 952c77f0..420b57c6 100644
--- a/pkg/hardwareutils/bmc/access_test.go
+++ b/pkg/hardwareutils/bmc/access_test.go
@@ -138,6 +138,46 @@ func TestParse(t *testing.T) {
 			Path:     "",
 		},
 
+		{
+			Scenario: "idrac url",
+			Address:  "idrac://192.168.122.1",
+			Type:     "idrac",
+			Port:     "",
+			Host:     "192.168.122.1",
+			Hostname: "192.168.122.1",
+			Path:     "",
+		},
+
+		{
+			Scenario: "idrac url with path",
+			Address:  "idrac://192.168.122.1:6233/foo",
+			Type:     "idrac",
+			Port:     "6233",
+			Host:     "192.168.122.1",
+			Hostname: "192.168.122.1:6233",
+			Path:     "/foo",
+		},
+
+		{
+			Scenario: "idrac url ipv6",
+			Address:  "idrac://[fe80::fc33:62ff:fe83:8a76]",
+			Type:     "idrac",
+			Port:     "",
+			Host:     "fe80::fc33:62ff:fe83:8a76",
+			Hostname: "[fe80::fc33:62ff:fe83:8a76]",
+			Path:     "",
+		},
+
+		{
+			Scenario: "idrac url, no sep",
+			Address:  "idrac:192.168.122.1",
+			Type:     "idrac",
+			Port:     "",
+			Host:     "192.168.122.1",
+			Hostname: "192.168.122.1",
+			Path:     "",
+		},
+
 		{
 			Scenario: "irmc url",
 			Address:  "irmc://192.168.122.1",
@@ -218,6 +258,87 @@ func TestParse(t *testing.T) {
 			Path:     "",
 		},
 
+		// ibmc
+		{
+			Scenario: "ibmc url",
+			Address:  "ibmc://192.168.122.1",
+			Type:     "ibmc",
+			Port:     "",
+			Host:     "192.168.122.1",
+			Hostname: "192.168.122.1",
+			Path:     "",
+		},
+
+		{
+			Scenario: "ibmc url path",
+			Address:  "ibmc://192.168.122.1:6233/foo",
+			Type:     "ibmc",
+			Port:     "6233",
+			Host:     "192.168.122.1",
+			Hostname: "192.168.122.1:6233",
+			Path:     "/foo",
+		},
+
+		{
+			Scenario: "ibmc url with http scheme",
+			Address:  "ibmc+http://192.168.122.1",
+			Type:     "ibmc+http",
+			Port:     "",
+			Host:     "192.168.122.1",
+			Hostname: "192.168.122.1",
+			Path:     "",
+		},
+
+		{
+			Scenario: "ibmc url with http scheme",
+			Address:  "ibmc+https://192.168.122.1",
+			Type:     "ibmc+https",
+			Port:     "",
+			Host:     "192.168.122.1",
+			Hostname: "192.168.122.1",
+			Path:     "",
+		},
+
+		{
+			Scenario: "ibmc url path",
+			Address:  "ibmc://192.168.122.1:6233/foo",
+			Type:     "ibmc",
+			Port:     "6233",
+			Host:     "192.168.122.1",
+			Hostname: "192.168.122.1:6233",
+			Path:     "/foo",
+		},
+
+		{
+			Scenario: "ibmc url ipv6",
+			Address:  "ibmc://[fe80::fc33:62ff:fe83:8a76]",
+			Type:     "ibmc",
+			Port:     "",
+			Host:     "fe80::fc33:62ff:fe83:8a76",
+			Hostname: "[fe80::fc33:62ff:fe83:8a76]",
+			Path:     "",
+		},
+
+		{
+			Scenario: "ibmc url path ipv6",
+			Address:  "ibmc://[fe80::fc33:62ff:fe83:8a76]:6233/foo",
+			Type:     "ibmc",
+			Port:     "6233",
+			Host:     "fe80::fc33:62ff:fe83:8a76",
+			Hostname: "[fe80::fc33:62ff:fe83:8a76]:6233",
+			Path:     "/foo",
+		},
+
+		{
+			Scenario: "ibmc url no sep",
+			Address:  "ibmc:192.168.122.1",
+			Type:     "ibmc",
+			Port:     "",
+			Host:     "192.168.122.1",
+			Hostname: "192.168.122.1",
+			Path:     "",
+		},
+
 		{
 			Scenario: "ilo4 url",
 			Address:  "ilo4://192.168.122.1",
@@ -372,6 +493,18 @@ func TestStaticDriverInfo(t *testing.T) {
 			power:      "",
 		},
 
+		{
+			Scenario:   "idrac",
+			input:      "idrac://192.168.122.1",
+			needsMac:   false,
+			driver:     "idrac",
+			bios:       "",
+			boot:       "ipxe",
+			firmware:   "",
+			management: "",
+			power:      "",
+		},
+
 		{
 			Scenario:   "irmc",
 			input:      "irmc://192.168.122.1",
@@ -514,6 +647,18 @@ func TestStaticDriverInfo(t *testing.T) {
 			vendor:     "idrac-redfish",
 		},
 
+		{
+			Scenario:   "ibmc",
+			input:      "ibmc://192.168.122.1:6233",
+			needsMac:   true,
+			driver:     "ibmc",
+			bios:       "",
+			boot:       "ipxe",
+			firmware:   "",
+			management: "ibmc",
+			power:      "ibmc",
+		},
+
 		{
 			Scenario:   "ilo4",
 			input:      "ilo4://192.168.122.1",
@@ -613,6 +758,79 @@ func TestDriverInfo(t *testing.T) {
 			},
 		},
 
+		{
+			Scenario: "idrac",
+			input:    "idrac://192.168.122.1",
+			expects: map[string]interface{}{
+				"drac_address":   "192.168.122.1",
+				"drac_password":  "",
+				"drac_username":  "",
+				"drac_verify_ca": false,
+			},
+		},
+
+		{
+			Scenario: "idrac http",
+			input:    "idrac+http://192.168.122.1",
+			expects: map[string]interface{}{
+				"drac_address":   "192.168.122.1",
+				"drac_protocol":  "http",
+				"drac_password":  "",
+				"drac_username":  "",
+				"drac_verify_ca": false,
+			},
+		},
+
+		{
+			Scenario: "idrac https",
+			input:    "idrac+https://192.168.122.1",
+			expects: map[string]interface{}{
+				"drac_address":   "192.168.122.1",
+				"drac_protocol":  "https",
+				"drac_password":  "",
+				"drac_username":  "",
+				"drac_verify_ca": false,
+			},
+		},
+
+		{
+			Scenario: "idrac port and path http",
+			input:    "idrac://192.168.122.1:8080/foo",
+			expects: map[string]interface{}{
+				"drac_address":   "192.168.122.1",
+				"drac_port":      "8080",
+				"drac_path":      "/foo",
+				"drac_password":  "",
+				"drac_username":  "",
+				"drac_verify_ca": false,
+			},
+		},
+
+		{
+			Scenario: "idrac ipv6",
+			input:    "idrac://[fe80::fc33:62ff:fe83:8a76]/foo",
+			expects: map[string]interface{}{
+				"drac_address":   "fe80::fc33:62ff:fe83:8a76",
+				"drac_path":      "/foo",
+				"drac_password":  "",
+				"drac_username":  "",
+				"drac_verify_ca": false,
+			},
+		},
+
+		{
+			Scenario: "idrac ipv6 port and path",
+			input:    "idrac://[fe80::fc33:62ff:fe83:8a76]:8080/foo",
+			expects: map[string]interface{}{
+				"drac_address":   "fe80::fc33:62ff:fe83:8a76",
+				"drac_port":      "8080",
+				"drac_path":      "/foo",
+				"drac_password":  "",
+				"drac_username":  "",
+				"drac_verify_ca": false,
+			},
+		},
+
 		{
 			Scenario: "irmc",
 			input:    "irmc://192.168.122.1",
@@ -791,6 +1009,73 @@ func TestDriverInfo(t *testing.T) {
 			},
 		},
 
+		// ibmc driver testcases
+		{
+			Scenario: "ibmc",
+			input:    "ibmc://192.168.122.1/foo/bar",
+			expects: map[string]interface{}{
+				"ibmc_address":   "https://192.168.122.1/foo/bar",
+				"ibmc_password":  "",
+				"ibmc_username":  "",
+				"ibmc_verify_ca": false,
+			},
+		},
+
+		{
+			Scenario: "ibmc http",
+			input:    "ibmc+http://192.168.122.1/foo/bar",
+			expects: map[string]interface{}{
+				"ibmc_address":   "http://192.168.122.1/foo/bar",
+				"ibmc_password":  "",
+				"ibmc_username":  "",
+				"ibmc_verify_ca": false,
+			},
+		},
+
+		{
+			Scenario: "ibmc https",
+			input:    "ibmc+https://192.168.122.1/foo/bar",
+			expects: map[string]interface{}{
+				"ibmc_address":   "https://192.168.122.1/foo/bar",
+				"ibmc_password":  "",
+				"ibmc_username":  "",
+				"ibmc_verify_ca": false,
+			},
+		},
+
+		{
+			Scenario: "ibmc port",
+			input:    "ibmc://192.168.122.1:8080/foo/bar",
+			expects: map[string]interface{}{
+				"ibmc_address":   "https://192.168.122.1:8080/foo/bar",
+				"ibmc_password":  "",
+				"ibmc_username":  "",
+				"ibmc_verify_ca": false,
+			},
+		},
+
+		{
+			Scenario: "ibmc ipv6",
+			input:    "ibmc://[fe80::fc33:62ff:fe83:8a76]/foo/bar",
+			expects: map[string]interface{}{
+				"ibmc_address":   "https://[fe80::fc33:62ff:fe83:8a76]/foo/bar",
+				"ibmc_password":  "",
+				"ibmc_username":  "",
+				"ibmc_verify_ca": false,
+			},
+		},
+
+		{
+			Scenario: "ibmc ipv6 port",
+			input:    "ibmc://[fe80::fc33:62ff:fe83:8a76]:8080/foo",
+			expects: map[string]interface{}{
+				"ibmc_address":   "https://[fe80::fc33:62ff:fe83:8a76]:8080/foo",
+				"ibmc_password":  "",
+				"ibmc_username":  "",
+				"ibmc_verify_ca": false,
+			},
+		},
+
 		{
 			Scenario: "ilo4",
 			input:    "ilo4://192.168.122.1",