diff --git a/cluster-api-controller-image/Dockerfile b/cluster-api-controller-image/Dockerfile
new file mode 100644
index 0000000..f4057c5
--- /dev/null
+++ b/cluster-api-controller-image/Dockerfile
@@ -0,0 +1,36 @@
+# SPDX-License-Identifier: Apache-2.0
+#!BuildTag: %%IMG_PREFIX%%cluster-api-controller:v%%cluster-api_version%%
+#!BuildTag: %%IMG_PREFIX%%cluster-api-controller:%%cluster-api_version%%
+#!BuildTag: %%IMG_PREFIX%%cluster-api-controller:%%cluster-api_version%%-%RELEASE%
+#!BuildVersion: 15.6
+ARG SLE_VERSION
+FROM registry.suse.com/bci/bci-micro:$SLE_VERSION AS micro
+
+FROM registry.suse.com/bci/bci-base:$SLE_VERSION AS base
+COPY --from=micro / /installroot/
+RUN zypper --installroot /installroot --non-interactive install --no-recommends cluster-api-175 shadow; zypper -n clean; rm -rf /var/log/*
+
+FROM micro AS final
+# Define labels according to https://en.opensuse.org/Building_derived_containers
+# labelprefix=com.suse.application.cluster-api
+LABEL org.opencontainers.image.authors="SUSE LLC (https://www.suse.com/)"
+LABEL org.opencontainers.image.title="SLE cluster-api Container Image"
+LABEL org.opencontainers.image.description="cluster-api based on the SLE Base Container Image."
+LABEL org.opencontainers.image.version="%%cluster-api_version%%"
+LABEL org.opencontainers.image.url="https://www.suse.com/products/server/"
+LABEL org.opencontainers.image.created="%BUILDTIME%"
+LABEL org.opencontainers.image.vendor="SUSE LLC"
+LABEL org.opensuse.reference="%%IMG_REPO%%/%%IMG_PREFIX%%cluster-api:%%cluster-api_version%%-%RELEASE%"
+LABEL org.openbuildservice.disturl="%DISTURL%"
+LABEL com.suse.supportlevel="l3"
+LABEL com.suse.eula="SUSE Combined EULA February 2024"
+LABEL com.suse.lifecycle-url="https://www.suse.com/lifecycle"
+LABEL com.suse.image-type="application"
+LABEL com.suse.release-stage="released"
+# endlabelprefix
+
+COPY --from=base /installroot /
+RUN mv /usr/bin/cluster-api-controller /manager
+# Use uid of nonroot user (65532) because kubernetes expects numeric user when applying pod security policies
+USER 65532
+ENTRYPOINT [ "/manager" ]
diff --git a/cluster-api-controller-image/_service b/cluster-api-controller-image/_service
new file mode 100644
index 0000000..9f48c56
--- /dev/null
+++ b/cluster-api-controller-image/_service
@@ -0,0 +1,17 @@
+
+
+
+
+ Dockerfile
+ %%cluster-api_version%%
+ cluster-api-175
+ patch
+
+
+ Dockerfile
+ IMG_PREFIX=$(rpm --macros=/root/.rpmmacros -E %img_prefix)
+ IMG_PREFIX
+ IMG_REPO=$(rpm --macros=/root/.rpmmacros -E %img_repo)
+ IMG_REPO
+
+
diff --git a/cluster-api-operator-image/Dockerfile b/cluster-api-operator-image/Dockerfile
new file mode 100644
index 0000000..8244d9c
--- /dev/null
+++ b/cluster-api-operator-image/Dockerfile
@@ -0,0 +1,35 @@
+# SPDX-License-Identifier: Apache-2.0
+#!BuildTag: %%IMG_PREFIX%%cluster-api-operator:%%cluster-api-operator_version%%
+#!BuildTag: %%IMG_PREFIX%%cluster-api-operator:%%cluster-api-operator_version%%-%RELEASE%
+#!BuildVersion: 15.6
+ARG SLE_VERSION
+FROM registry.suse.com/bci/bci-micro:$SLE_VERSION AS micro
+
+FROM registry.suse.com/bci/bci-base:$SLE_VERSION AS base
+COPY --from=micro / /installroot/
+RUN zypper --installroot /installroot --non-interactive install --no-recommends cluster-api-operator-012 shadow; zypper -n clean; rm -rf /var/log/*
+
+FROM micro AS final
+# Define labels according to https://en.opensuse.org/Building_derived_containers
+# labelprefix=com.suse.application.cluster-api-operator
+LABEL org.opencontainers.image.authors="SUSE LLC (https://www.suse.com/)"
+LABEL org.opencontainers.image.title="SLE cluster-api-operator Container Image"
+LABEL org.opencontainers.image.description="cluster-api-operator based on the SLE Base Container Image."
+LABEL org.opencontainers.image.version="%%cluster-api-operator_version%%"
+LABEL org.opencontainers.image.url="https://www.suse.com/products/server/"
+LABEL org.opencontainers.image.created="%BUILDTIME%"
+LABEL org.opencontainers.image.vendor="SUSE LLC"
+LABEL org.opensuse.reference="%%IMG_REPO%%/%%IMG_PREFIX%%cluster-api-operator:%%cluster-api-operator_version%%-%RELEASE%"
+LABEL org.openbuildservice.disturl="%DISTURL%"
+LABEL com.suse.supportlevel="l3"
+LABEL com.suse.eula="SUSE Combined EULA February 2024"
+LABEL com.suse.lifecycle-url="https://www.suse.com/lifecycle"
+LABEL com.suse.image-type="application"
+LABEL com.suse.release-stage="released"
+# endlabelprefix
+
+COPY --from=base /installroot /
+RUN mv /usr/bin/cluster-api-operator-controller /manager
+# Use uid of nonroot user (65532) because kubernetes expects numeric user when applying pod security policies
+USER 65532
+ENTRYPOINT [ "/manager" ]
diff --git a/cluster-api-operator-image/_service b/cluster-api-operator-image/_service
new file mode 100644
index 0000000..646f2d0
--- /dev/null
+++ b/cluster-api-operator-image/_service
@@ -0,0 +1,17 @@
+
+
+
+
+ Dockerfile
+ %%cluster-api-operator_version%%
+ cluster-api-operator-012
+ patch
+
+
+ Dockerfile
+ IMG_PREFIX=$(rpm --macros=/root/.rpmmacros -E %img_prefix)
+ IMG_PREFIX
+ IMG_REPO=$(rpm --macros=/root/.rpmmacros -E %img_repo)
+ IMG_REPO
+
+
diff --git a/cluster-api-provider-metal3-image/Dockerfile b/cluster-api-provider-metal3-image/Dockerfile
new file mode 100644
index 0000000..edd6d38
--- /dev/null
+++ b/cluster-api-provider-metal3-image/Dockerfile
@@ -0,0 +1,36 @@
+# SPDX-License-Identifier: Apache-2.0
+#!BuildTag: %%IMG_PREFIX%%cluster-api-provider-metal3:v%%cluster-api-provider-metal3_version%%
+#!BuildTag: %%IMG_PREFIX%%cluster-api-provider-metal3:%%cluster-api-provider-metal3_version%%
+#!BuildTag: %%IMG_PREFIX%%cluster-api-provider-metal3:%%cluster-api-provider-metal3_version%%-%RELEASE%
+#!BuildVersion: 15.6
+ARG SLE_VERSION
+FROM registry.suse.com/bci/bci-micro:$SLE_VERSION AS micro
+
+FROM registry.suse.com/bci/bci-base:$SLE_VERSION AS base
+COPY --from=micro / /installroot/
+RUN zypper --installroot /installroot --non-interactive install --no-recommends cluster-api-provider-metal3-171 shadow; zypper -n clean; rm -rf /var/log/*
+
+FROM micro AS final
+# Define labels according to https://en.opensuse.org/Building_derived_containers
+# labelprefix=com.suse.application.cluster-api-provider-metal3
+LABEL org.opencontainers.image.authors="SUSE LLC (https://www.suse.com/)"
+LABEL org.opencontainers.image.title="SLE cluster-api-provider-metal3 Container Image"
+LABEL org.opencontainers.image.description="cluster-api-provider-metal3 based on the SLE Base Container Image."
+LABEL org.opencontainers.image.version="%%cluster-api-provider-metal3_version%%"
+LABEL org.opencontainers.image.url="https://www.suse.com/products/server/"
+LABEL org.opencontainers.image.created="%BUILDTIME%"
+LABEL org.opencontainers.image.vendor="SUSE LLC"
+LABEL org.opensuse.reference="%%IMG_REPO%%/%%IMG_PREFIX%%cluster-api-provider-metal3:%%cluster-api-provider-metal3_version%%-%RELEASE%"
+LABEL org.openbuildservice.disturl="%DISTURL%"
+LABEL com.suse.supportlevel="l3"
+LABEL com.suse.eula="SUSE Combined EULA February 2024"
+LABEL com.suse.lifecycle-url="https://www.suse.com/lifecycle"
+LABEL com.suse.image-type="application"
+LABEL com.suse.release-stage="released"
+# endlabelprefix
+
+COPY --from=base /installroot /
+RUN mv /usr/bin/cluster-api-provider-metal3 /manager
+# Use uid of nonroot user (65532) because kubernetes expects numeric user when applying pod security policies
+USER 65532
+ENTRYPOINT [ "/manager" ]
diff --git a/cluster-api-provider-metal3-image/_service b/cluster-api-provider-metal3-image/_service
new file mode 100644
index 0000000..2ef5aa3
--- /dev/null
+++ b/cluster-api-provider-metal3-image/_service
@@ -0,0 +1,17 @@
+
+
+
+
+ Dockerfile
+ %%cluster-api-provider-metal3_version%%
+ cluster-api-provider-metal3-171
+ patch
+
+
+ Dockerfile
+ IMG_PREFIX=$(rpm --macros=/root/.rpmmacros -E %img_prefix)
+ IMG_PREFIX
+ IMG_REPO=$(rpm --macros=/root/.rpmmacros -E %img_repo)
+ IMG_REPO
+
+
diff --git a/cluster-api-provider-rke2-bootstrap-image/Dockerfile b/cluster-api-provider-rke2-bootstrap-image/Dockerfile
new file mode 100644
index 0000000..902d5db
--- /dev/null
+++ b/cluster-api-provider-rke2-bootstrap-image/Dockerfile
@@ -0,0 +1,36 @@
+# SPDX-License-Identifier: Apache-2.0
+#!BuildTag: %%IMG_PREFIX%%cluster-api-provider-rke2-bootstrap:v%%cluster-api-provider-rke2_version%%
+#!BuildTag: %%IMG_PREFIX%%cluster-api-provider-rke2-bootstrap:%%cluster-api-provider-rke2_version%%
+#!BuildTag: %%IMG_PREFIX%%cluster-api-provider-rke2-bootstrap:%%cluster-api-provider-rke2_version%%-%RELEASE%
+#!BuildVersion: 15.6
+ARG SLE_VERSION
+FROM registry.suse.com/bci/bci-micro:$SLE_VERSION AS micro
+
+FROM registry.suse.com/bci/bci-base:$SLE_VERSION AS base
+COPY --from=micro / /installroot/
+RUN zypper --installroot /installroot --non-interactive install --no-recommends cluster-api-provider-rke2-070-bootstrap shadow; zypper -n clean; rm -rf /var/log/*
+
+FROM micro AS final
+# Define labels according to https://en.opensuse.org/Building_derived_containers
+# labelprefix=com.suse.application.cluster-api-provider-rke2
+LABEL org.opencontainers.image.authors="SUSE LLC (https://www.suse.com/)"
+LABEL org.opencontainers.image.title="SLE cluster-api-provider-rke2 Container Image"
+LABEL org.opencontainers.image.description="cluster-api-provider-rke2 based on the SLE Base Container Image."
+LABEL org.opencontainers.image.version="%%cluster-api-provider-rke2_version%%"
+LABEL org.opencontainers.image.url="https://www.suse.com/products/server/"
+LABEL org.opencontainers.image.created="%BUILDTIME%"
+LABEL org.opencontainers.image.vendor="SUSE LLC"
+LABEL org.opensuse.reference="%%IMG_REPO%%/%%IMG_PREFIX%%cluster-api-provider-rke2-bootstrap:%%cluster-api-provider-rke2_version%%-%RELEASE%"
+LABEL org.openbuildservice.disturl="%DISTURL%"
+LABEL com.suse.supportlevel="l3"
+LABEL com.suse.eula="SUSE Combined EULA February 2024"
+LABEL com.suse.lifecycle-url="https://www.suse.com/lifecycle"
+LABEL com.suse.image-type="application"
+LABEL com.suse.release-stage="released"
+# endlabelprefix
+
+COPY --from=base /installroot /
+RUN mv /usr/bin/rke2-bootstrap-manager /manager
+# Use uid of nonroot user (65532) because kubernetes expects numeric user when applying pod security policies
+USER 65532
+ENTRYPOINT [ "/manager" ]
diff --git a/cluster-api-provider-rke2-bootstrap-image/_service b/cluster-api-provider-rke2-bootstrap-image/_service
new file mode 100644
index 0000000..fa241da
--- /dev/null
+++ b/cluster-api-provider-rke2-bootstrap-image/_service
@@ -0,0 +1,17 @@
+
+
+
+
+ Dockerfile
+ %%cluster-api-provider-rke2_version%%
+ cluster-api-provider-rke2-070-bootstrap
+ patch
+
+
+ Dockerfile
+ IMG_PREFIX=$(rpm --macros=/root/.rpmmacros -E %img_prefix)
+ IMG_PREFIX
+ IMG_REPO=$(rpm --macros=/root/.rpmmacros -E %img_repo)
+ IMG_REPO
+
+
diff --git a/cluster-api-provider-rke2-controlplane-image/Dockerfile b/cluster-api-provider-rke2-controlplane-image/Dockerfile
new file mode 100644
index 0000000..db8ac94
--- /dev/null
+++ b/cluster-api-provider-rke2-controlplane-image/Dockerfile
@@ -0,0 +1,36 @@
+# SPDX-License-Identifier: Apache-2.0
+#!BuildTag: %%IMG_PREFIX%%cluster-api-provider-rke2-controlplane:v%%cluster-api-provider-rke2_version%%
+#!BuildTag: %%IMG_PREFIX%%cluster-api-provider-rke2-controlplane:%%cluster-api-provider-rke2_version%%
+#!BuildTag: %%IMG_PREFIX%%cluster-api-provider-rke2-controlplane:%%cluster-api-provider-rke2_version%%-%RELEASE%
+#!BuildVersion: 15.6
+ARG SLE_VERSION
+FROM registry.suse.com/bci/bci-micro:$SLE_VERSION AS micro
+
+FROM registry.suse.com/bci/bci-base:$SLE_VERSION AS base
+COPY --from=micro / /installroot/
+RUN zypper --installroot /installroot --non-interactive install --no-recommends cluster-api-provider-rke2-070-control-plane shadow; zypper -n clean; rm -rf /var/log/*
+
+FROM micro AS final
+# Define labels according to https://en.opensuse.org/Building_derived_containers
+# labelprefix=com.suse.application.cluster-api-provider-rke2
+LABEL org.opencontainers.image.authors="SUSE LLC (https://www.suse.com/)"
+LABEL org.opencontainers.image.title="SLE cluster-api-provider-rke2 Container Image"
+LABEL org.opencontainers.image.description="cluster-api-provider-rke2 based on the SLE Base Container Image."
+LABEL org.opencontainers.image.version="%%cluster-api-provider-rke2_version%%"
+LABEL org.opencontainers.image.url="https://www.suse.com/products/server/"
+LABEL org.opencontainers.image.created="%BUILDTIME%"
+LABEL org.opencontainers.image.vendor="SUSE LLC"
+LABEL org.opensuse.reference="%%IMG_REPO%%/%%IMG_PREFIX%%cluster-api-provider-rke2-controlplane:%%cluster-api-provider-rke2_version%%-%RELEASE%"
+LABEL org.openbuildservice.disturl="%DISTURL%"
+LABEL com.suse.supportlevel="l3"
+LABEL com.suse.eula="SUSE Combined EULA February 2024"
+LABEL com.suse.lifecycle-url="https://www.suse.com/lifecycle"
+LABEL com.suse.image-type="application"
+LABEL com.suse.release-stage="released"
+# endlabelprefix
+
+COPY --from=base /installroot /
+RUN mv /usr/bin/rke2-control-plane-manager /manager
+# Use uid of nonroot user (65532) because kubernetes expects numeric user when applying pod security policies
+USER 65532
+ENTRYPOINT [ "/manager" ]
diff --git a/cluster-api-provider-rke2-controlplane-image/_service b/cluster-api-provider-rke2-controlplane-image/_service
new file mode 100644
index 0000000..7d398fe
--- /dev/null
+++ b/cluster-api-provider-rke2-controlplane-image/_service
@@ -0,0 +1,17 @@
+
+
+
+
+ Dockerfile
+ %%cluster-api-provider-rke2_version%%
+ cluster-api-provider-rke2-070-control-plane
+ patch
+
+
+ Dockerfile
+ IMG_PREFIX=$(rpm --macros=/root/.rpmmacros -E %img_prefix)
+ IMG_PREFIX
+ IMG_REPO=$(rpm --macros=/root/.rpmmacros -E %img_repo)
+ IMG_REPO
+
+
diff --git a/edge-image-builder-image/Dockerfile b/edge-image-builder-image/Dockerfile
new file mode 100644
index 0000000..714da54
--- /dev/null
+++ b/edge-image-builder-image/Dockerfile
@@ -0,0 +1,40 @@
+#!BuildTag: %%IMG_PREFIX%%edge-image-builder:1.1.0
+#!BuildTag: %%IMG_PREFIX%%edge-image-builder:1.1.0-%RELEASE%
+#!BuildVersion: 15.6
+ARG SLE_VERSION
+FROM registry.suse.com/bci/bci-base:$SLE_VERSION
+MAINTAINER SUSE LLC (https://www.suse.com/)
+
+COPY artifacts.yaml artifacts.yaml
+
+RUN sed -i -e 's%^# rpm.install.excludedocs = no.*%rpm.install.excludedocs = yes%g' /etc/zypp/zypp.conf
+RUN zypper --non-interactive install --no-recommends edge-image-builder-110 qemu-x86 qemu-uefi-aarch64 cni-plugins; zypper -n clean; rm -rf /var/log/*
+
+# Define labels according to https://en.opensuse.org/Building_derived_containers
+# labelprefix=com.suse.application.edge-image-builder
+LABEL org.opencontainers.image.authors="SUSE LLC (https://www.suse.com/)"
+LABEL org.opencontainers.image.title="SLE edge-image-builder Container Image"
+LABEL org.opencontainers.image.description="edge-image-builder based on the SLE Base Container Image."
+LABEL org.opencontainers.image.version="1.1.0"
+LABEL org.opencontainers.image.url="https://www.suse.com/products/server/"
+LABEL org.opencontainers.image.created="%BUILDTIME%"
+LABEL org.opencontainers.image.vendor="SUSE LLC"
+LABEL org.opensuse.reference="%%IMG_REPO%%/%%IMG_PREFIX%%edge-image-builder:1.1.0-%RELEASE%"
+LABEL org.openbuildservice.disturl="%DISTURL%"
+LABEL com.suse.supportlevel="l3"
+LABEL com.suse.eula="SUSE Combined EULA February 2024"
+LABEL com.suse.lifecycle-url="https://www.suse.com/lifecycle"
+LABEL com.suse.image-type="application"
+LABEL com.suse.release-stage="released"
+# endlabelprefix
+
+# Make adjustments for running guestfish and image modifications on aarch64
+# guestfish looks for very specific locations on the filesystem for UEFI firmware
+# and also expects the boot kernel to be a portable executable (PE), not ELF.
+RUN mkdir -p /usr/share/edk2/aarch64 && \
+ cp /usr/share/qemu/aavmf-aarch64-code.bin /usr/share/edk2/aarch64/QEMU_EFI-pflash.raw && \
+ cp /usr/share/qemu/aavmf-aarch64-vars.bin /usr/share/edk2/aarch64/vars-template-pflash.raw && \
+ mv /boot/vmlinux* /boot/backup-vmlinux
+
+ENTRYPOINT ["/usr/bin/eib"]
+
diff --git a/edge-image-builder-image/_service b/edge-image-builder-image/_service
new file mode 100644
index 0000000..3990297
--- /dev/null
+++ b/edge-image-builder-image/_service
@@ -0,0 +1,14 @@
+
+
+
+ Dockerfile
+ IMG_PREFIX=$(rpm --macros=/root/.rpmmacros -E %img_prefix)
+ IMG_PREFIX
+ IMG_REPO=$(rpm --macros=/root/.rpmmacros -E %img_repo)
+ IMG_REPO
+ artifacts.yaml
+ CHART_REPO=$(rpm --macros=/root/.rpmmacros -E %chart_repo)
+ CHART_REPO
+
+
+
diff --git a/edge-image-builder-image/artifacts.yaml b/edge-image-builder-image/artifacts.yaml
new file mode 100644
index 0000000..c1c06f3
--- /dev/null
+++ b/edge-image-builder-image/artifacts.yaml
@@ -0,0 +1,16 @@
+metallb:
+ chart: metallb-chart
+ repository: %%CHART_REPO%%/3.1
+ version: 0.14.9
+endpoint-copier-operator:
+ chart: endpoint-copier-operator-chart
+ repository: %%CHART_REPO%%/3.1
+ version: 0.2.1
+kubernetes:
+ k3s:
+ selinuxPackage: k3s-selinux-1.6-1.slemicro.noarch
+ selinuxRepository: https://rpm.rancher.io/k3s/stable/common/slemicro/noarch
+ rke2:
+ selinuxPackage: rke2-selinux
+ selinuxRepository: https://rpm.rancher.io/rke2/stable/common/slemicro/noarch
+
diff --git a/ip-address-manager-image/Dockerfile b/ip-address-manager-image/Dockerfile
new file mode 100644
index 0000000..672ad17
--- /dev/null
+++ b/ip-address-manager-image/Dockerfile
@@ -0,0 +1,36 @@
+# SPDX-License-Identifier: Apache-2.0
+#!BuildTag: %%IMG_PREFIX%%ip-address-manager:v%%ip-address-manager_version%%
+#!BuildTag: %%IMG_PREFIX%%ip-address-manager:%%ip-address-manager_version%%
+#!BuildTag: %%IMG_PREFIX%%ip-address-manager:%%ip-address-manager_version%%-%RELEASE%
+#!BuildVersion: 15.6
+ARG SLE_VERSION
+FROM registry.suse.com/bci/bci-micro:$SLE_VERSION AS micro
+
+FROM registry.suse.com/bci/bci-base:$SLE_VERSION AS base
+COPY --from=micro / /installroot/
+RUN zypper --installroot /installroot --non-interactive install --no-recommends ip-address-manager-171 shadow; zypper -n clean; rm -rf /var/log/*
+
+FROM micro AS final
+# Define labels according to https://en.opensuse.org/Building_derived_containers
+# labelprefix=com.suse.application.ip-address-manager
+LABEL org.opencontainers.image.authors="SUSE LLC (https://www.suse.com/)"
+LABEL org.opencontainers.image.title="SLE ip-address-manager Container Image"
+LABEL org.opencontainers.image.description="ip-address-manager based on the SLE Base Container Image."
+LABEL org.opencontainers.image.version="%%ip-address-manager_version%%"
+LABEL org.opencontainers.image.url="https://www.suse.com/products/server/"
+LABEL org.opencontainers.image.created="%BUILDTIME%"
+LABEL org.opencontainers.image.vendor="SUSE LLC"
+LABEL org.opensuse.reference="%%IMG_REPO%%/%%IMG_PREFIX%%ip-address-manager:%%ip-address-manager_version%%-%RELEASE%"
+LABEL org.openbuildservice.disturl="%DISTURL%"
+LABEL com.suse.supportlevel="l3"
+LABEL com.suse.eula="SUSE Combined EULA February 2024"
+LABEL com.suse.lifecycle-url="https://www.suse.com/lifecycle"
+LABEL com.suse.image-type="application"
+LABEL com.suse.release-stage="released"
+# endlabelprefix
+
+COPY --from=base /installroot /
+RUN mv /usr/bin/ip-address-manager /manager
+# Use uid of nonroot user (65532) because kubernetes expects numeric user when applying pod security policies
+USER 65532
+ENTRYPOINT [ "/manager" ]
diff --git a/ip-address-manager-image/_service b/ip-address-manager-image/_service
new file mode 100644
index 0000000..a0d1450
--- /dev/null
+++ b/ip-address-manager-image/_service
@@ -0,0 +1,17 @@
+
+
+
+
+ Dockerfile
+ %%ip-address-manager_version%%
+ ip-address-manager-171
+ patch
+
+
+ Dockerfile
+ IMG_PREFIX=$(rpm --macros=/root/.rpmmacros -E %img_prefix)
+ IMG_PREFIX
+ IMG_REPO=$(rpm --macros=/root/.rpmmacros -E %img_repo)
+ IMG_REPO
+
+
diff --git a/ironic-image/Dockerfile b/ironic-image/Dockerfile
new file mode 100644
index 0000000..830d31f
--- /dev/null
+++ b/ironic-image/Dockerfile
@@ -0,0 +1,91 @@
+# SPDX-License-Identifier: Apache-2.0
+#!BuildTag: %%IMG_PREFIX%%ironic:24.1.2.0
+#!BuildTag: %%IMG_PREFIX%%ironic:24.1.2.0-%RELEASE%
+#!BuildVersion: 15.6
+
+ARG SLE_VERSION
+FROM registry.suse.com/bci/bci-micro:$SLE_VERSION AS micro
+
+FROM registry.suse.com/bci/bci-base:$SLE_VERSION AS base
+
+RUN set -euo pipefail; zypper -n in --no-recommends gcc git make xz-devel shim dosfstools mtools glibc-extra grub2-x86_64-efi grub2; zypper -n clean; rm -rf /var/log/*
+WORKDIR /tmp
+COPY prepare-efi.sh /bin/
+RUN set -euo pipefail; chmod +x /bin/prepare-efi.sh
+RUN /bin/prepare-efi.sh
+
+COPY --from=micro / /installroot/
+RUN sed -i -e 's%^# rpm.install.excludedocs = no.*%rpm.install.excludedocs = yes%g' /etc/zypp/zypp.conf
+RUN zypper --installroot /installroot --non-interactive install --no-recommends python311-devel python311 python311-pip python-dracclient python311-sushy-oem-idrac python311-proliantutils python311-sushy python3-ironicclient git curl sles-release tar gzip vim gawk dnsmasq dosfstools apache2 apache2-mod_wsgi inotify-tools ipcalc ipmitool iproute2 procps qemu-tools sqlite3 util-linux xorriso tftp syslinux ipxe-bootimgs python311-sushy-tools crudini openstack-ironic openstack-ironic-inspector-api
+
+FROM micro AS final
+MAINTAINER SUSE LLC (https://www.suse.com/)
+# Define labels according to https://en.opensuse.org/Building_derived_containers
+LABEL org.opencontainers.image.title="SLE Openstack Ironic Container Image"
+LABEL org.opencontainers.image.description="Openstack Ironic based on the SLE Base Container Image."
+LABEL org.opencontainers.image.url="https://www.suse.com/products/server/"
+LABEL org.opencontainers.image.created="%BUILDTIME%"
+LABEL org.opencontainers.image.vendor="SUSE LLC"
+LABEL org.opencontainers.image.version="24.1.2.0"
+LABEL org.opensuse.reference="%%IMG_REPO%%/%%IMG_PREFIX%%ironic:24.1.2.0-%RELEASE%"
+LABEL org.openbuildservice.disturl="%DISTURL%"
+LABEL com.suse.supportlevel="l3"
+LABEL com.suse.eula="SUSE Combined EULA February 2024"
+LABEL com.suse.lifecycle-url="https://www.suse.com/lifecycle"
+LABEL com.suse.image-type="application"
+LABEL com.suse.release-stage="released"
+# endlabelprefix
+
+COPY --from=base /installroot /
+
+RUN set -euo pipefail; ln -s /usr/bin/python3.11 /usr/local/bin/python3; \
+ ln -s /usr/bin/pydoc3.11 /usr/local/bin/pydoc
+
+ENV GRUB_DIR=/tftpboot/boot/grub
+
+# workaround for mkisofs command failing
+RUN echo 'alias mkisofs="xorriso -as mkisofs"' >> ~/.bashrc
+COPY mkisofs_wrapper /usr/bin/mkisofs
+RUN set -euo pipefail; chmod +x /usr/bin/mkisofs
+
+COPY auth-common.sh configure-ironic.sh ironic-common.sh rundnsmasq runhttpd runironic runironic-api runironic-conductor runironic-exporter runironic-inspector runlogwatch.sh tls-common.sh configure-nonroot.sh /bin/
+RUN set -euo pipefail; chmod +x /bin/auth-common.sh; chmod +x /bin/configure-ironic.sh; chmod +x /bin/ironic-common.sh; chmod +x /bin/rundnsmasq; chmod +x /bin/runhttpd; chmod +x /bin/runironic; chmod +x /bin/runironic-api; chmod +x /bin/runironic-conductor; chmod +x /bin/runironic-exporter; chmod +x /bin/runironic-inspector; chmod +x /bin/runlogwatch.sh; chmod +x /bin/tls-common.sh; chmod +x /bin/configure-nonroot.sh;
+RUN mkdir -p /tftpboot
+RUN mkdir -p $GRUB_DIR
+
+# No need to support the Legacy BIOS boot
+#RUN cp /usr/share/syslinux/pxelinux.0 /tftpboot
+#RUN cp /usr/share/syslinux/chain.c32 /tftpboot/
+
+# IRONIC #
+RUN cp /usr/share/ipxe/undionly.kpxe /tftpboot/undionly.kpxe
+RUN cp /usr/share/ipxe/ipxe-x86_64.efi /tftpboot/ipxe.efi
+COPY --from=base /tmp/esp.img /tmp/uefi_esp.img
+
+COPY ironic.conf.j2 /etc/ironic/
+COPY inspector.ipxe.j2 httpd-ironic-api.conf.j2 /tmp/
+COPY network-data-schema-empty.json /etc/ironic/
+
+# DNSMASQ
+COPY dnsmasq.conf.j2 /etc/
+
+# Custom httpd config, removes all but the bare minimum needed modules
+COPY httpd.conf.j2 /etc/httpd/conf/
+COPY httpd-modules.conf /etc/httpd/conf.modules.d/
+COPY apache2-vmedia.conf.j2 /etc/httpd-vmedia.conf.j2
+
+# IRONIC-INSPECTOR #
+RUN mkdir -p /var/lib/ironic /var/lib/ironic-inspector && \
+ sqlite3 /var/lib/ironic/ironic.db "pragma journal_mode=wal" && \
+ sqlite3 /var/lib/ironic-inspector/ironic-inspector.db "pragma journal_mode=wal"
+
+COPY ironic-inspector.conf.j2 /etc/ironic-inspector/
+COPY inspector-apache.conf.j2 /etc/httpd/conf.d/
+
+# Workaround
+# Removing the 010-ironic.conf file that comes with the package
+RUN rm /etc/ironic/ironic.conf.d/010-ironic.conf
+
+# configure non-root user and set relevant permissions
+RUN configure-nonroot.sh && \
+ rm -f /bin/configure-nonroot.sh
diff --git a/ironic-image/_service b/ironic-image/_service
new file mode 100644
index 0000000..219c79f
--- /dev/null
+++ b/ironic-image/_service
@@ -0,0 +1,17 @@
+
+
+
+
+ Dockerfile
+ %%openstack-ironic_version%%
+ openstack-ironic
+ patch
+
+
+ Dockerfile
+ IMG_PREFIX=$(rpm --macros=/root/.rpmmacros -E %img_prefix)
+ IMG_PREFIX
+ IMG_REPO=$(rpm --macros=/root/.rpmmacros -E %img_repo)
+ IMG_REPO
+
+
diff --git a/ironic-image/apache2-vmedia.conf.j2 b/ironic-image/apache2-vmedia.conf.j2
new file mode 100644
index 0000000..1d7ad21
--- /dev/null
+++ b/ironic-image/apache2-vmedia.conf.j2
@@ -0,0 +1,27 @@
+Listen {{ env.VMEDIA_TLS_PORT }}
+
+
+ ErrorLog /dev/stderr
+ LogLevel debug
+ CustomLog /dev/stdout combined
+
+ SSLEngine on
+ SSLProtocol {{ env.IRONIC_VMEDIA_SSL_PROTOCOL }}
+ SSLCertificateFile {{ env.IRONIC_VMEDIA_CERT_FILE }}
+ SSLCertificateKeyFile {{ env.IRONIC_VMEDIA_KEY_FILE }}
+
+
+ AllowOverride None
+ Require all granted
+
+
+
+ Options Indexes FollowSymLinks
+ AllowOverride None
+ Require all granted
+
+
+
+
+ SSLRequireSSL
+
diff --git a/ironic-image/auth-common.sh b/ironic-image/auth-common.sh
new file mode 100644
index 0000000..9906776
--- /dev/null
+++ b/ironic-image/auth-common.sh
@@ -0,0 +1,71 @@
+#!/usr/bin/bash
+
+set -euxo pipefail
+
+export IRONIC_HTPASSWD=${IRONIC_HTPASSWD:-${HTTP_BASIC_HTPASSWD:-}}
+export INSPECTOR_HTPASSWD=${INSPECTOR_HTPASSWD:-${HTTP_BASIC_HTPASSWD:-}}
+export IRONIC_DEPLOYMENT="${IRONIC_DEPLOYMENT:-}"
+export IRONIC_REVERSE_PROXY_SETUP=${IRONIC_REVERSE_PROXY_SETUP:-false}
+export INSPECTOR_REVERSE_PROXY_SETUP=${INSPECTOR_REVERSE_PROXY_SETUP:-false}
+
+IRONIC_HTPASSWD_FILE=/etc/ironic/htpasswd
+INSPECTOR_HTPASSWD_FILE=/etc/ironic-inspector/htpasswd
+
+configure_client_basic_auth()
+{
+ local auth_config_file="/auth/$1/auth-config"
+ local dest="${2:-/etc/ironic/ironic.conf}"
+ if [[ -f "${auth_config_file}" ]]; then
+ # Merge configurations in the "auth" directory into the default ironic configuration file because there is no way to choose the configuration file
+ # when running the api as a WSGI app.
+ crudini --merge "${dest}" < "${auth_config_file}"
+ fi
+}
+
+configure_json_rpc_auth()
+{
+ export JSON_RPC_AUTH_STRATEGY="noauth"
+ if [[ -n "${IRONIC_HTPASSWD}" ]]; then
+ if [[ "${IRONIC_DEPLOYMENT}" == "Conductor" ]]; then
+ export JSON_RPC_AUTH_STRATEGY="http_basic"
+ printf "%s\n" "${IRONIC_HTPASSWD}" > "${IRONIC_HTPASSWD_FILE}-rpc"
+ else
+ printf "%s\n" "${IRONIC_HTPASSWD}" > "${IRONIC_HTPASSWD_FILE}"
+ fi
+ fi
+}
+
+configure_ironic_auth()
+{
+ local config=/etc/ironic/ironic.conf
+ # Configure HTTP basic auth for API server
+ if [[ -n "${IRONIC_HTPASSWD}" ]]; then
+ printf "%s\n" "${IRONIC_HTPASSWD}" > "${IRONIC_HTPASSWD_FILE}"
+ if [[ "${IRONIC_REVERSE_PROXY_SETUP}" == "false" ]]; then
+ crudini --set "${config}" DEFAULT auth_strategy http_basic
+ crudini --set "${config}" DEFAULT http_basic_auth_user_file "${IRONIC_HTPASSWD_FILE}"
+ fi
+ fi
+}
+
+configure_inspector_auth()
+{
+ local config=/etc/ironic-inspector/ironic-inspector.conf
+ if [[ -n "${INSPECTOR_HTPASSWD}" ]]; then
+ printf "%s\n" "${INSPECTOR_HTPASSWD}" > "${INSPECTOR_HTPASSWD_FILE}"
+ if [[ "${INSPECTOR_REVERSE_PROXY_SETUP}" == "false" ]]; then
+ crudini --set "${config}" DEFAULT auth_strategy http_basic
+ crudini --set "${config}" DEFAULT http_basic_auth_user_file "${INSPECTOR_HTPASSWD_FILE}"
+ fi
+ fi
+}
+
+write_htpasswd_files()
+{
+ if [[ -n "${IRONIC_HTPASSWD:-}" ]]; then
+ printf "%s\n" "${IRONIC_HTPASSWD}" > "${IRONIC_HTPASSWD_FILE}"
+ fi
+ if [[ -n "${INSPECTOR_HTPASSWD:-}" ]]; then
+ printf "%s\n" "${INSPECTOR_HTPASSWD}" > "${INSPECTOR_HTPASSWD_FILE}"
+ fi
+}
diff --git a/ironic-image/configure-ironic.sh b/ironic-image/configure-ironic.sh
new file mode 100644
index 0000000..fa07f43
--- /dev/null
+++ b/ironic-image/configure-ironic.sh
@@ -0,0 +1,102 @@
+#!/usr/bin/bash
+
+set -euxo pipefail
+
+IRONIC_DEPLOYMENT="${IRONIC_DEPLOYMENT:-}"
+IRONIC_EXTERNAL_IP="${IRONIC_EXTERNAL_IP:-}"
+
+# Define the VLAN interfaces to be included in introspection report, e.g.
+# all - all VLANs on all interfaces using LLDP information
+# - all VLANs on a particular interface using LLDP information
+# - a particular VLAN on an interface, not relying on LLDP
+export IRONIC_INSPECTOR_VLAN_INTERFACES=${IRONIC_INSPECTOR_VLAN_INTERFACES:-all}
+
+# shellcheck disable=SC1091
+. /bin/tls-common.sh
+# shellcheck disable=SC1091
+. /bin/ironic-common.sh
+# shellcheck disable=SC1091
+. /bin/auth-common.sh
+
+export HTTP_PORT=${HTTP_PORT:-80}
+
+MARIADB_PASSWORD=${MARIADB_PASSWORD}
+MARIADB_DATABASE=${MARIADB_DATABASE:-ironic}
+MARIADB_USER=${MARIADB_USER:-ironic}
+MARIADB_HOST=${MARIADB_HOST:-127.0.0.1}
+export MARIADB_CONNECTION="mysql+pymysql://${MARIADB_USER}:${MARIADB_PASSWORD}@${MARIADB_HOST}/${MARIADB_DATABASE}?charset=utf8"
+if [[ "$MARIADB_TLS_ENABLED" == "true" ]]; then
+ export MARIADB_CONNECTION="${MARIADB_CONNECTION}&ssl=on&ssl_ca=${MARIADB_CACERT_FILE}"
+fi
+
+# TODO(dtantsur): remove the explicit default once we get
+# https://review.opendev.org/761185 in the repositories
+NUMPROC="$(grep -c "^processor" /proc/cpuinfo)"
+if [[ "$NUMPROC" -lt 4 ]]; then
+ NUMPROC=4
+fi
+export NUMWORKERS=${NUMWORKERS:-$NUMPROC}
+
+export IRONIC_USE_MARIADB=${IRONIC_USE_MARIADB:-true}
+export IRONIC_EXPOSE_JSON_RPC=${IRONIC_EXPOSE_JSON_RPC:-true}
+
+# Whether to enable fast_track provisioning or not
+export IRONIC_FAST_TRACK=${IRONIC_FAST_TRACK:-true}
+
+# Whether cleaning disks before and after deployment
+export IRONIC_AUTOMATED_CLEAN=${IRONIC_AUTOMATED_CLEAN:-true}
+
+# Wheter to enable the sensor data collection
+export SEND_SENSOR_DATA=${SEND_SENSOR_DATA:-false}
+
+# Set of collectors that should be used with IPA inspection
+export IRONIC_IPA_COLLECTORS=${IRONIC_IPA_COLLECTORS:-default,logs}
+
+wait_for_interface_or_ip
+
+# Hostname to use for the current conductor instance.
+export IRONIC_CONDUCTOR_HOST=${IRONIC_CONDUCTOR_HOST:-${IRONIC_URL_HOST}}
+
+export IRONIC_BASE_URL=${IRONIC_BASE_URL:-"${IRONIC_SCHEME}://${IRONIC_URL_HOST}:${IRONIC_ACCESS_PORT}"}
+export IRONIC_INSPECTOR_BASE_URL=${IRONIC_INSPECTOR_BASE_URL:-"${IRONIC_INSPECTOR_SCHEME}://${IRONIC_URL_HOST}:${IRONIC_INSPECTOR_ACCESS_PORT}"}
+
+if [[ -n "$IRONIC_EXTERNAL_IP" ]]; then
+ export IRONIC_EXTERNAL_CALLBACK_URL="${IRONIC_SCHEME}://${IRONIC_EXTERNAL_IP}:${IRONIC_ACCESS_PORT}"
+ if [[ "$IRONIC_VMEDIA_TLS_SETUP" == "true" ]]; then
+ export IRONIC_EXTERNAL_HTTP_URL="https://${IRONIC_EXTERNAL_IP}:${VMEDIA_TLS_PORT}"
+ else
+ export IRONIC_EXTERNAL_HTTP_URL="http://${IRONIC_EXTERNAL_IP}:${HTTP_PORT}"
+ fi
+ export IRONIC_INSPECTOR_CALLBACK_ENDPOINT_OVERRIDE="https://${IRONIC_EXTERNAL_IP}:${IRONIC_INSPECTOR_ACCESS_PORT}"
+fi
+
+IMAGE_CACHE_PREFIX=/shared/html/images/ironic-python-agent
+if [[ -f "${IMAGE_CACHE_PREFIX}.kernel" ]] && [[ -f "${IMAGE_CACHE_PREFIX}.initramfs" ]]; then
+ export IRONIC_DEFAULT_KERNEL="${IMAGE_CACHE_PREFIX}.kernel"
+ export IRONIC_DEFAULT_RAMDISK="${IMAGE_CACHE_PREFIX}.initramfs"
+fi
+
+if [[ -f /etc/ironic/ironic.conf ]]; then
+ # Make a copy of the original supposed empty configuration file
+ cp /etc/ironic/ironic.conf /etc/ironic/ironic.conf_orig
+fi
+
+# oslo.config also supports Config Opts From Environment, log them to stdout
+echo 'Options set from Environment variables'
+env | grep "^OS_" || true
+
+mkdir -p /shared/html
+mkdir -p /shared/ironic_prometheus_exporter
+
+configure_json_rpc_auth
+
+# The original ironic.conf is empty, and can be found in ironic.conf_orig
+render_j2_config /etc/ironic/ironic.conf.j2 /etc/ironic/ironic.conf
+
+if [[ "${USE_IRONIC_INSPECTOR}" == "true" ]]; then
+ configure_client_basic_auth ironic-inspector
+fi
+configure_client_basic_auth ironic-rpc
+
+# Make sure ironic traffic bypasses any proxies
+export NO_PROXY="${NO_PROXY:-},$IRONIC_IP"
diff --git a/ironic-image/configure-nonroot.sh b/ironic-image/configure-nonroot.sh
new file mode 100644
index 0000000..caeec02
--- /dev/null
+++ b/ironic-image/configure-nonroot.sh
@@ -0,0 +1,50 @@
+#!/usr/bin/bash
+
+NONROOT_UID=10475
+NONROOT_GID=10475
+USER="ironic-suse"
+
+groupadd -r -g ${NONROOT_GID} ${USER}
+useradd -r -g ${NONROOT_GID} \
+ -u ${NONROOT_UID} \
+ -d /var/lib/ironic \
+ -s /sbin/nologin \
+ ${USER}
+
+# create ironic's http_root directory
+mkdir -p /shared/html
+chown "${NONROOT_UID}":"${NONROOT_GID}" /shared/html
+
+# we'll bind mount shared ca and ironic/inspector certificate dirs here
+# that need to have correct ownership as the entire ironic in BMO
+# deployment shares a single fsGroup in manifest's securityContext
+mkdir -p /certs/ca
+chown "${NONROOT_UID}":"${NONROOT_GID}" /certs{,/ca}
+chmod 2775 /certs{,/ca}
+
+# apache2 permission changes
+chown -R "${NONROOT_UID}":"${NONROOT_GID}" /etc/apache2
+chown -R "${NONROOT_UID}":"${NONROOT_GID}" /run
+
+# ironic, inspector and httpd related changes
+chown -R "${NONROOT_UID}":"${NONROOT_GID}" /etc/ironic /etc/httpd /etc/httpd
+chown -R "${NONROOT_UID}":"${NONROOT_GID}" /etc/ironic-inspector
+chown -R "${NONROOT_UID}":"${NONROOT_GID}" /var/log
+chmod 2775 /etc/ironic /etc/ironic-inspector /etc/httpd/conf /etc/httpd/conf.d
+chmod 664 /etc/ironic/* /etc/ironic-inspector/* /etc/httpd/conf/* /etc/httpd/conf.d/*
+
+chown -R "${NONROOT_UID}":"${NONROOT_GID}" /var/lib/ironic
+chown -R "${NONROOT_UID}":"${NONROOT_GID}" /var/lib/ironic-inspector
+chmod 2775 /var/lib/ironic /var/lib/ironic-inspector
+chmod 664 /var/lib/ironic/ironic.db /var/lib/ironic-inspector/ironic-inspector.db
+
+# dnsmasq, and the capabilities required to run it as non-root user
+chown -R "${NONROOT_UID}":"${NONROOT_GID}" /etc/dnsmasq.conf /var/lib/dnsmasq
+chmod 2775 /var/lib/dnsmasq
+touch /var/lib/dnsmasq/dnsmasq.leases
+chmod 664 /etc/dnsmasq.conf /var/lib/dnsmasq/dnsmasq.leases
+
+# ca-certificates permission changes
+touch /var/lib/ca-certificates/ca-bundle.pem.new
+chown -R "${NONROOT_UID}":"${NONROOT_GID}" /var/lib/ca-certificates/
+chmod -R +w /var/lib/ca-certificates/
diff --git a/ironic-image/dnsmasq.conf.j2 b/ironic-image/dnsmasq.conf.j2
new file mode 100644
index 0000000..502de9a
--- /dev/null
+++ b/ironic-image/dnsmasq.conf.j2
@@ -0,0 +1,79 @@
+interface={{ env.PROVISIONING_INTERFACE }}
+bind-dynamic
+enable-tftp
+tftp-root=/shared/tftpboot
+log-queries
+
+# Configure listening for DNS (0 disables DNS)
+port={{ env.DNS_PORT }}
+
+{%- if env.DHCP_RANGE | length %}
+log-dhcp
+dhcp-range={{ env.DHCP_RANGE }}
+
+# It can be used when setting DNS or GW variables.
+{%- if env["GATEWAY_IP"] is undefined %}
+# Disable default router(s)
+dhcp-option=3
+{% else %}
+dhcp-option=option{% if ":" in env["GATEWAY_IP"] %}6{% endif %}:router,{{ env["GATEWAY_IP"] }}
+{% endif %}
+{%- if env["DNS_IP"] is undefined %}
+# Disable DNS over provisioning network
+dhcp-option=6
+{% else %}
+dhcp-option=option{% if ":" in env["DNS_IP"] %}6{% endif %}:dns-server,{{ env["DNS_IP"] }}
+{% endif %}
+
+{%- if env.IPV == "4" or env.IPV is undefined %}
+# IPv4 Configuration:
+dhcp-match=ipxe,175
+# Client is already running iPXE; move to next stage of chainloading
+dhcp-boot=tag:ipxe,http://{{ env.IRONIC_URL_HOST }}:{{ env.HTTP_PORT }}/boot.ipxe
+
+# Note: Need to test EFI booting
+dhcp-match=set:efi,option:client-arch,7
+dhcp-match=set:efi,option:client-arch,9
+dhcp-match=set:efi,option:client-arch,11
+# Client is PXE booting over EFI without iPXE ROM; send EFI version of iPXE chainloader
+dhcp-boot=tag:efi,tag:!ipxe,snponly.efi
+
+# Client is running PXE over BIOS; send BIOS version of iPXE chainloader
+dhcp-boot=/undionly.kpxe,{{ env.IRONIC_IP }}
+{% endif %}
+
+{% if env.IPV == "6" %}
+# IPv6 Configuration:
+enable-ra
+ra-param={{ env.PROVISIONING_INTERFACE }},0,0
+
+dhcp-vendorclass=set:pxe6,enterprise:343,PXEClient
+dhcp-userclass=set:ipxe6,iPXE
+dhcp-option=tag:pxe6,option6:bootfile-url,tftp://{{ env.IRONIC_URL_HOST }}/snponly.efi
+dhcp-option=tag:ipxe6,option6:bootfile-url,http://{{ env.IRONIC_URL_HOST }}:{{ env.HTTP_PORT }}/boot.ipxe
+
+# It can be used when setting DNS or GW variables.
+{%- if env["GATEWAY_IP"] is undefined %}
+# Disable default router(s)
+dhcp-option=3
+{% else %}
+dhcp-option=3,{{ env["GATEWAY_IP"] }}
+{% endif %}
+{%- if env["DNS_IP"] is undefined %}
+# Disable DNS over provisioning network
+dhcp-option=6
+{% else %}
+dhcp-option=6,{{ env["DNS_IP"] }}
+{% endif %}
+{% endif %}
+{% endif %}
+
+{%- if env.DHCP_IGNORE | length %}
+dhcp-ignore={{ env.DHCP_IGNORE }}
+{% endif %}
+
+{%- if env.DHCP_HOSTS | length %}
+{%- for item in env.DHCP_HOSTS.split(";") %}
+dhcp-host={{ item }}
+{%- endfor %}
+{% endif %}
diff --git a/ironic-image/httpd-ironic-api.conf.j2 b/ironic-image/httpd-ironic-api.conf.j2
new file mode 100644
index 0000000..2132c9f
--- /dev/null
+++ b/ironic-image/httpd-ironic-api.conf.j2
@@ -0,0 +1,85 @@
+# 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.
+
+
+{% if env.LISTEN_ALL_INTERFACES | lower == "true" %}
+Listen {{ env.IRONIC_LISTEN_PORT }}
+
+{% else %}
+Listen {{ env.IRONIC_URL_HOST }}:{{ env.IRONIC_LISTEN_PORT }}
+
+{% endif %}
+
+ {% if env.IRONIC_REVERSE_PROXY_SETUP | lower == "true" %}
+
+ {% if env.IRONIC_PRIVATE_PORT == "unix" %}
+ ProxyPass "/" "unix:/shared/ironic.sock|http://127.0.0.1/"
+ ProxyPassReverse "/" "unix:/shared/ironic.sock|http://127.0.0.1/"
+ {% else %}
+ ProxyPass "/" "http://127.0.0.1:{{ env.IRONIC_PRIVATE_PORT }}/"
+ ProxyPassReverse "/" "http://127.0.0.1:{{ env.IRONIC_PRIVATE_PORT }}/"
+ {% endif %}
+
+ {% else %}
+ WSGIDaemonProcess ironic user=ironic group=ironic threads=10 display-name=%{GROUP}
+ WSGIScriptAlias / /usr/bin/ironic-api-wsgi
+ {% endif %}
+
+ SetEnv APACHE_RUN_USER ironic-suse
+ SetEnv APACHE_RUN_GROUP ironic-suse
+ WSGIProcessGroup ironic-suse
+
+ ErrorLog /dev/stderr
+ LogLevel debug
+ CustomLog /dev/stdout combined
+
+{% if env.IRONIC_TLS_SETUP == "true" %}
+ SSLEngine on
+ SSLProtocol {{ env.IRONIC_SSL_PROTOCOL }}
+ SSLCertificateFile {{ env.IRONIC_CERT_FILE }}
+ SSLCertificateKeyFile {{ env.IRONIC_KEY_FILE }}
+{% endif %}
+
+ {% if env.IRONIC_REVERSE_PROXY_SETUP | lower == "true" %}
+
+ {% if "IRONIC_HTPASSWD" in env and env.IRONIC_HTPASSWD | length %}
+ AuthType Basic
+ AuthName "Restricted area"
+ AuthUserFile "/etc/ironic/htpasswd"
+ Require valid-user
+ {% endif %}
+
+ {% else %}
+
+ WSGIProcessGroup ironic
+ WSGIApplicationGroup %{GLOBAL}
+ AllowOverride None
+
+ {% if "IRONIC_HTPASSWD" in env and env.IRONIC_HTPASSWD | length %}
+ AuthType Basic
+ AuthName "Restricted WSGI area"
+ AuthUserFile "/etc/ironic/htpasswd"
+ Require valid-user
+ {% else %}
+ Require all granted
+ {% endif %}
+
+ {% endif %}
+
+
+ Require all granted
+
+
+
+ Require all granted
+
+
diff --git a/ironic-image/httpd-modules.conf b/ironic-image/httpd-modules.conf
new file mode 100644
index 0000000..c1c5aaa
--- /dev/null
+++ b/ironic-image/httpd-modules.conf
@@ -0,0 +1,21 @@
+# Bare minimum set of modules
+LoadModule log_config_module /usr/lib64/apache2/mod_log_config.so
+LoadModule mime_module /usr/lib64/apache2/mod_mime.so
+LoadModule dir_module /usr/lib64/apache2/mod_dir.so
+LoadModule authz_core_module /usr/lib64/apache2/mod_authz_core.so
+#LoadModule unixd_module modules/mod_unixd.so
+#LoadModule mpm_event_module modules/mod_mpm_event.so
+LoadModule wsgi_module /usr/lib64/apache2/mod_wsgi.so
+LoadModule ssl_module /usr/lib64/apache2/mod_ssl.so
+LoadModule env_module /usr/lib64/apache2/mod_env.so
+LoadModule proxy_module /usr/lib64/apache2/mod_proxy.so
+LoadModule proxy_ajp_module /usr/lib64/apache2/mod_proxy_ajp.so
+LoadModule proxy_balancer_module /usr/lib64/apache2/mod_proxy_balancer.so
+LoadModule proxy_http_module /usr/lib64/apache2/mod_proxy_http.so
+LoadModule slotmem_shm_module /usr/lib64/apache2/mod_slotmem_shm.so
+LoadModule headers_module /usr/lib64/apache2/mod_headers.so
+LoadModule authn_core_module /usr/lib64/apache2/mod_authn_core.so
+LoadModule auth_basic_module /usr/lib64/apache2/mod_auth_basic.so
+LoadModule authn_file_module /usr/lib64/apache2/mod_authn_file.so
+LoadModule authz_user_module /usr/lib64/apache2/mod_authz_user.so
+LoadModule access_compat_module /usr/lib64/apache2/mod_access_compat.so
diff --git a/ironic-image/httpd.conf.j2 b/ironic-image/httpd.conf.j2
new file mode 100644
index 0000000..16f5470
--- /dev/null
+++ b/ironic-image/httpd.conf.j2
@@ -0,0 +1,84 @@
+ServerRoot "/etc/httpd"
+{%- if env.LISTEN_ALL_INTERFACES | lower == "true" %}
+Listen [::]:{{ env.HTTP_PORT }}
+{% else %}
+Listen {{ env.IRONIC_URL_HOST }}:{{ env.HTTP_PORT }}
+{% endif %}
+Include conf.modules.d/*.conf
+User ironic-suse
+Group ironic-suse
+
+
+ AllowOverride none
+ Require all denied
+
+
+DocumentRoot "/shared/html"
+
+
+ Options Indexes FollowSymLinks
+ AllowOverride None
+ Require all granted
+
+
+{%- if env.HTTPD_SERVE_NODE_IMAGES | lower == "true" %}
+
+ Options Indexes FollowSymLinks
+ AllowOverride None
+ Require all granted
+
+{% endif %}
+
+
+ DirectoryIndex index.html
+
+
+
+ Require all denied
+
+
+ErrorLog "/dev/stderr"
+
+LogLevel warn
+
+
+ LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
+ LogFormat "%h %l %u %t \"%r\" %>s %b" common
+
+ LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio
+
+ CustomLog "/dev/stderr" combined
+
+
+
+ TypesConfig /etc/mime.types
+ AddType application/x-compress .Z
+ AddType application/x-gzip .gz .tgz
+ AddType text/html .shtml
+ AddOutputFilter INCLUDES .shtml
+
+
+AddDefaultCharset UTF-8
+
+
+ MIMEMagicFile conf/magic
+
+
+PidFile /var/tmp/httpd.pid
+
+# EnableSendfile directive could speed up deployments but it could also cause
+# issues depending on the underlying file system, to learn more:
+# https://httpd.apache.org/docs/current/mod/core.html#enablesendfile
+{%- if env.HTTPD_ENABLE_SENDFILE | lower == "true" %}
+EnableSendfile on
+{% endif %}
+
+# http TRACE can be subjected to abuse and should be disabled
+TraceEnable off
+
+# provide minimal server information
+ServerTokens Prod
+ServerSignature Off
+
+IncludeOptional conf.d/*.conf
+
diff --git a/ironic-image/inspector-apache.conf.j2 b/ironic-image/inspector-apache.conf.j2
new file mode 100644
index 0000000..b0a9d7f
--- /dev/null
+++ b/ironic-image/inspector-apache.conf.j2
@@ -0,0 +1,57 @@
+# 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.
+
+
+{% if env.LISTEN_ALL_INTERFACES | lower == "true" %}
+Listen {{ env.IRONIC_INSPECTOR_LISTEN_PORT }}
+
+{% else %}
+Listen {{ env.IRONIC_URL_HOST }}:{{ env.IRONIC_INSPECTOR_LISTEN_PORT }}
+
+{% endif %}
+ {% if env.IRONIC_INSPECTOR_PRIVATE_PORT == "unix" %}
+ ProxyPass "/" "unix:/shared/inspector.sock|http://127.0.0.1/"
+ ProxyPassReverse "/" "unix:/shared/inspector.sock|http://127.0.0.1/"
+ {% else %}
+ ProxyPass "/" "http://127.0.0.1:{{ env.IRONIC_INSPECTOR_PRIVATE_PORT }}/"
+ ProxyPassReverse "/" "http://127.0.0.1:{{ env.IRONIC_INSPECTOR_PRIVATE_PORT }}/"
+ {% endif %}
+
+ SetEnv APACHE_RUN_USER ironic-suse
+ SetEnv APACHE_RUN_GROUP ironic-suse
+
+ ErrorLog /dev/stdout
+ LogLevel debug
+ CustomLog /dev/stdout combined
+
+ SSLEngine On
+ SSLProtocol {{ env.IRONIC_SSL_PROTOCOL }}
+ SSLCertificateFile {{ env.IRONIC_INSPECTOR_CERT_FILE }}
+ SSLCertificateKeyFile {{ env.IRONIC_INSPECTOR_KEY_FILE }}
+
+ {% if "INSPECTOR_HTPASSWD" in env and env.INSPECTOR_HTPASSWD | length %}
+
+ AuthType Basic
+ AuthName "Restricted area"
+ AuthUserFile "/etc/ironic-inspector/htpasswd"
+ Require valid-user
+
+
+
+ Require all granted
+
+
+
+ Require all granted
+
+ {% endif %}
+
diff --git a/ironic-image/inspector.ipxe.j2 b/ironic-image/inspector.ipxe.j2
new file mode 100644
index 0000000..93f8c75
--- /dev/null
+++ b/ironic-image/inspector.ipxe.j2
@@ -0,0 +1,10 @@
+#!ipxe
+
+:retry_boot
+echo In inspector.ipxe
+imgfree
+# NOTE(dtantsur): keep inspection kernel params in [mdns]params in
+# ironic-inspector-image and configuration in configure-ironic.sh
+kernel --timeout 60000 http://{{ env.IRONIC_IP }}:{{ env.HTTP_PORT }}/images/ironic-python-agent.kernel ipa-insecure=1 ipa-inspection-collectors={{ env.IRONIC_IPA_COLLECTORS }} systemd.journald.forward_to_console=yes BOOTIF=${mac} ipa-debug=1 ipa-enable-vlan-interfaces={{ env.IRONIC_INSPECTOR_VLAN_INTERFACES }} ipa-inspection-dhcp-all-interfaces=1 ipa-collect-lldp=1 {{ env.INSPECTOR_EXTRA_ARGS }} initrd=ironic-python-agent.initramfs {% if env.IRONIC_RAMDISK_SSH_KEY %}sshkey="{{ env.IRONIC_RAMDISK_SSH_KEY|trim }}"{% endif %} {{ env.IRONIC_KERNEL_PARAMS|trim }} || goto retry_boot
+initrd --timeout 60000 http://{{ env.IRONIC_IP }}:{{ env.HTTP_PORT }}/images/ironic-python-agent.initramfs || goto retry_boot
+boot
diff --git a/ironic-image/ironic-common.sh b/ironic-image/ironic-common.sh
new file mode 100644
index 0000000..f388c6b
--- /dev/null
+++ b/ironic-image/ironic-common.sh
@@ -0,0 +1,110 @@
+#!/usr/bin/bash
+
+set -euxo pipefail
+
+IRONIC_IP="${IRONIC_IP:-}"
+PROVISIONING_INTERFACE="${PROVISIONING_INTERFACE:-}"
+PROVISIONING_IP="${PROVISIONING_IP:-}"
+PROVISIONING_MACS="${PROVISIONING_MACS:-}"
+
+get_provisioning_interface()
+{
+ if [[ -n "$PROVISIONING_INTERFACE" ]]; then
+ # don't override the PROVISIONING_INTERFACE if one is provided
+ echo "$PROVISIONING_INTERFACE"
+ return
+ fi
+
+ local interface="provisioning"
+
+ if [[ -n "${PROVISIONING_IP}" ]]; then
+ if ip -br addr show | grep -qi " ${PROVISIONING_IP}/"; then
+ interface="$(ip -br addr show | grep -i " ${PROVISIONING_IP}/" | cut -f 1 -d ' ' | cut -f 1 -d '@')"
+ fi
+ fi
+
+ for mac in ${PROVISIONING_MACS//,/ }; do
+ if ip -br link show up | grep -qi "$mac"; then
+ interface="$(ip -br link show up | grep -i "$mac" | cut -f 1 -d ' ' | cut -f 1 -d '@')"
+ break
+ fi
+ done
+
+ echo "$interface"
+}
+
+PROVISIONING_INTERFACE="$(get_provisioning_interface)"
+export PROVISIONING_INTERFACE
+
+export LISTEN_ALL_INTERFACES="${LISTEN_ALL_INTERFACES:-true}"
+
+# Wait for the interface or IP to be up, sets $IRONIC_IP
+wait_for_interface_or_ip()
+{
+ # If $PROVISIONING_IP is specified, then we wait for that to become available on an interface, otherwise we look at $PROVISIONING_INTERFACE for an IP
+ if [[ -n "$PROVISIONING_IP" ]]; then
+ # Convert the address using ipcalc which strips out the subnet. For IPv6 addresses, this will give the short-form address
+ IRONIC_IP="$(ipcalc "${PROVISIONING_IP}" | grep "^Address:" | awk '{print $2}')"
+ export IRONIC_IP
+ until grep -F " ${IRONIC_IP}/" <(ip -br addr show); do
+ echo "Waiting for ${IRONIC_IP} to be configured on an interface"
+ sleep 1
+ done
+ else
+ until [[ -n "$IRONIC_IP" ]]; do
+ echo "Waiting for ${PROVISIONING_INTERFACE} interface to be configured"
+ IRONIC_IP="$(ip -br add show scope global up dev "${PROVISIONING_INTERFACE}" | awk '{print $3}' | sed -e 's%/.*%%' | head -n 1)"
+ export IRONIC_IP
+ sleep 1
+ done
+ fi
+
+ # If the IP contains a colon, then it's an IPv6 address, and the HTTP
+ # host needs surrounding with brackets
+ if [[ "$IRONIC_IP" =~ .*:.* ]]; then
+ export IPV=6
+ export IRONIC_URL_HOST="[$IRONIC_IP]"
+ else
+ export IPV=4
+ export IRONIC_URL_HOST="$IRONIC_IP"
+ fi
+}
+
+render_j2_config()
+{
+ python3 -c 'import os; import sys; import jinja2; sys.stdout.write(jinja2.Template(sys.stdin.read()).render(env=os.environ))' < "$1" > "$2"
+}
+
+run_ironic_dbsync()
+{
+ if [[ "${IRONIC_USE_MARIADB:-true}" == "true" ]]; then
+ # It's possible for the dbsync to fail if mariadb is not up yet, so
+ # retry until success
+ until ironic-dbsync --config-file /etc/ironic/ironic.conf upgrade; do
+ echo "WARNING: ironic-dbsync failed, retrying"
+ sleep 1
+ done
+ else
+ # SQLite does not support some statements. Fortunately, we can just create
+ # the schema in one go instead of going through an upgrade.
+ ironic-dbsync --config-file /etc/ironic/ironic.conf create_schema
+ fi
+}
+
+# Use the special value "unix" for unix sockets
+export IRONIC_PRIVATE_PORT=${IRONIC_PRIVATE_PORT:-6388}
+export IRONIC_INSPECTOR_PRIVATE_PORT=${IRONIC_INSPECTOR_PRIVATE_PORT:-5049}
+
+export IRONIC_ACCESS_PORT=${IRONIC_ACCESS_PORT:-6385}
+export IRONIC_LISTEN_PORT=${IRONIC_LISTEN_PORT:-$IRONIC_ACCESS_PORT}
+
+export IRONIC_INSPECTOR_ACCESS_PORT=${IRONIC_INSPECTOR_ACCESS_PORT:-5050}
+export IRONIC_INSPECTOR_LISTEN_PORT=${IRONIC_INSPECTOR_LISTEN_PORT:-$IRONIC_INSPECTOR_ACCESS_PORT}
+
+# If this is false, built-in inspection is used.
+export USE_IRONIC_INSPECTOR=${USE_IRONIC_INSPECTOR:-true}
+export IRONIC_INSPECTOR_ENABLE_DISCOVERY=${IRONIC_INSPECTOR_ENABLE_DISCOVERY:-false}
+if [[ "${USE_IRONIC_INSPECTOR}" != "true" ]] && [[ "${IRONIC_INSPECTOR_ENABLE_DISCOVERY}" == "true" ]]; then
+ echo "Discovery is only supported with ironic-inspector at this point"
+ exit 1
+fi
diff --git a/ironic-image/ironic-inspector.conf.j2 b/ironic-image/ironic-inspector.conf.j2
new file mode 100644
index 0000000..9932980
--- /dev/null
+++ b/ironic-image/ironic-inspector.conf.j2
@@ -0,0 +1,68 @@
+[DEFAULT]
+auth_strategy = noauth
+debug = true
+transport_url = fake://
+use_stderr = true
+{% if env.INSPECTOR_REVERSE_PROXY_SETUP == "true" %}
+{% if env.IRONIC_INSPECTOR_PRIVATE_PORT == "unix" %}
+listen_unix_socket = /shared/inspector.sock
+# NOTE(dtantsur): this is not ideal, but since the socket is accessed from
+# another container, we need to make it world-writeable.
+listen_unix_socket_mode = 0666
+{% else %}
+listen_port = {{ env.IRONIC_INSPECTOR_PRIVATE_PORT }}
+listen_address = 127.0.0.1
+{% endif %}
+{% elif env.LISTEN_ALL_INTERFACES | lower == "true" %}
+listen_port = {{ env.IRONIC_INSPECTOR_LISTEN_PORT }}
+listen_address = ::
+{% else %}
+listen_port = {{ env.IRONIC_INSPECTOR_LISTEN_PORT }}
+listen_address = {{ env.IRONIC_IP }}
+{% endif %}
+host = {{ env.IRONIC_IP }}
+{% if env.IRONIC_INSPECTOR_TLS_SETUP == "true" and env.INSPECTOR_REVERSE_PROXY_SETUP == "false" %}
+use_ssl = true
+{% endif %}
+
+[database]
+connection = sqlite:////var/lib/ironic-inspector/ironic-inspector.db
+
+{% if env.IRONIC_INSPECTOR_ENABLE_DISCOVERY == "true" %}
+[discovery]
+enroll_node_driver = ipmi
+{% endif %}
+
+[ironic]
+auth_type = none
+endpoint_override = {{ env.IRONIC_BASE_URL }}
+{% if env.IRONIC_TLS_SETUP == "true" %}
+cafile = {{ env.IRONIC_CACERT_FILE }}
+insecure = {{ env.IRONIC_INSECURE }}
+{% endif %}
+
+[processing]
+add_ports = all
+always_store_ramdisk_logs = true
+keep_ports = present
+{% if env.IRONIC_INSPECTOR_ENABLE_DISCOVERY == "true" %}
+node_not_found_hook = enroll
+{% endif %}
+permit_active_introspection = true
+power_off = false
+processing_hooks = $default_processing_hooks,lldp_basic
+ramdisk_logs_dir = /shared/log/ironic-inspector/ramdisk
+store_data = database
+
+[pxe_filter]
+driver = noop
+
+[service_catalog]
+auth_type = none
+endpoint_override = {{ env.IRONIC_INSPECTOR_BASE_URL }}
+
+{% if env.IRONIC_INSPECTOR_TLS_SETUP == "true" and env.INSPECTOR_REVERSE_PROXY_SETUP == "false" %}
+[ssl]
+cert_file = {{ env.IRONIC_INSPECTOR_CERT_FILE }}
+key_file = {{ env.IRONIC_INSPECTOR_KEY_FILE }}
+{% endif %}
diff --git a/ironic-image/ironic.conf.j2 b/ironic-image/ironic.conf.j2
new file mode 100644
index 0000000..5bce6d2
--- /dev/null
+++ b/ironic-image/ironic.conf.j2
@@ -0,0 +1,253 @@
+[DEFAULT]
+{% if env.AUTH_STRATEGY is defined %}
+auth_strategy = {{ env.AUTH_STRATEGY }}
+{% if env.AUTH_STRATEGY == "http_basic" %}
+http_basic_auth_user_file=/etc/ironic/htpasswd
+{% endif %}
+{% else %}
+auth_strategy = noauth
+{% endif %}
+debug = true
+default_deploy_interface = direct
+default_inspect_interface = {% if env.USE_IRONIC_INSPECTOR == "true" %}inspector{% else %}agent{% endif %}
+default_network_interface = noop
+enabled_bios_interfaces = idrac-wsman,no-bios,redfish,idrac-redfish,irmc,ilo
+enabled_boot_interfaces = ipxe,ilo-ipxe,pxe,ilo-pxe,fake,redfish-virtual-media,idrac-redfish-virtual-media,ilo-virtual-media
+enabled_deploy_interfaces = direct,fake,ramdisk,custom-agent
+# NOTE(dtantsur): when changing this, make sure to update the driver
+# dependencies in Dockerfile.
+enabled_hardware_types = ipmi,idrac,irmc,fake-hardware,redfish,manual-management,ilo,ilo5
+enabled_inspect_interfaces = {% if env.USE_IRONIC_INSPECTOR == "true" %}inspector{% else %}agent{% endif %},idrac-wsman,irmc,fake,redfish,ilo
+enabled_management_interfaces = ipmitool,idrac-wsman,irmc,fake,redfish,idrac-redfish,ilo,ilo5,noop
+enabled_power_interfaces = ipmitool,idrac-wsman,irmc,fake,redfish,idrac-redfish,ilo
+enabled_raid_interfaces = no-raid,irmc,agent,fake,idrac-wsman,redfish,idrac-redfish,ilo5
+enabled_vendor_interfaces = no-vendor,ipmitool,idrac-wsman,idrac-redfish,redfish,ilo,fake
+enabled_firmware_interfaces = no-firmware,fake,redfish
+{% if env.IRONIC_EXPOSE_JSON_RPC | lower == "true" %}
+rpc_transport = json-rpc
+{% else %}
+rpc_transport = none
+{% endif %}
+use_stderr = true
+# NOTE(dtantsur): the default md5 is not compatible with FIPS mode
+hash_ring_algorithm = sha256
+my_ip = {{ env.IRONIC_IP }}
+{% if env.IRONIC_DEPLOYMENT == "Conductor" and env.JSON_RPC_AUTH_STRATEGY == "noauth" %}
+# if access is unauthenticated, we bind only to localhost - use that as the
+# host name also, so that the client can find the server
+# If we run both API and conductor in the same pod, use localhost
+host = localhost
+{% else %}
+host = {{ env.IRONIC_CONDUCTOR_HOST }}
+{% endif %}
+
+# If a path to a certificate is defined, use that first for webserver
+{% if env.WEBSERVER_CACERT_FILE %}
+webserver_verify_ca = {{ env.WEBSERVER_CACERT_FILE }}
+{% elif env.IRONIC_INSECURE == "true" %}
+webserver_verify_ca = false
+{% endif %}
+
+isolinux_bin = /usr/share/syslinux/isolinux.bin
+
+# NOTE(dtantsur): this path is specific to the GRUB image that is built into
+# the ESP provided in [conductor]bootloader.
+grub_config_path = EFI/BOOT/grub.cfg
+
+[agent]
+deploy_logs_collect = always
+deploy_logs_local_path = /shared/log/ironic/deploy
+# NOTE(dtantsur): in some environments temporary networking issues can cause
+# the whole deployment to fail on inability to reach the ramdisk. Increasing
+# retries here works around such problems without affecting the normal path.
+# See https://bugzilla.redhat.com/show_bug.cgi?id=1822763
+max_command_attempts = 30
+
+[api]
+{% if env.IRONIC_REVERSE_PROXY_SETUP == "true" %}
+{% if env.IRONIC_PRIVATE_PORT == "unix" %}
+unix_socket = /shared/ironic.sock
+# NOTE(dtantsur): this is not ideal, but since the socket is accessed from
+# another container, we need to make it world-writeable.
+unix_socket_mode = 0666
+{% else %}
+host_ip = 127.0.0.1
+port = {{ env.IRONIC_PRIVATE_PORT }}
+{% endif %}
+public_endpoint = {{ env.IRONIC_BASE_URL }}
+{% else %}
+host_ip = {% if env.LISTEN_ALL_INTERFACES | lower == "true" %}::{% else %}{{ env.IRONIC_IP }}{% endif %}
+port = {{ env.IRONIC_LISTEN_PORT }}
+{% if env.IRONIC_TLS_SETUP == "true" %}
+enable_ssl_api = true
+{% endif %}
+{% endif %}
+api_workers = {{ env.NUMWORKERS }}
+
+# Disable schema validation so we can pass nmstate format
+network_data_schema = /etc/ironic/network-data-schema-empty.json
+
+[conductor]
+automated_clean = {{ env.IRONIC_AUTOMATED_CLEAN }}
+# NOTE(dtantsur): keep aligned with [pxe]boot_retry_timeout below.
+deploy_callback_timeout = 4800
+send_sensor_data = {{ env.SEND_SENSOR_DATA }}
+# NOTE(TheJulia): Do not lower this value below 120 seconds.
+# Power state is checked every 60 seconds and BMC activity should
+# be avoided more often than once every sixty seconds.
+send_sensor_data_interval = 160
+bootloader = {{ env.IRONIC_BOOT_BASE_URL }}/uefi_esp.img
+verify_step_priority_override = management.clear_job_queue:90
+# We don't use this feature, and it creates an additional load on the database
+node_history = False
+# Provide for a timeout longer than 60 seconds for certain vendor's hardware
+power_state_change_timeout = 120
+{% if env.IRONIC_DEFAULT_KERNEL is defined %}
+deploy_kernel = file://{{ env.IRONIC_DEFAULT_KERNEL }}
+{% endif %}
+{% if env.IRONIC_DEFAULT_RAMDISK is defined %}
+deploy_ramdisk = file://{{ env.IRONIC_DEFAULT_RAMDISK }}
+{% endif %}
+
+[database]
+{% if env.IRONIC_USE_MARIADB | lower == "false" %}
+connection = sqlite:////var/lib/ironic/ironic.sqlite
+# Synchronous mode is required for data integrity in case of operating system
+# crash. In our case we restart the container from scratch, so we can save some
+# IO by not doing syncs all the time.
+sqlite_synchronous = False
+{% else %}
+connection = {{ env.MARIADB_CONNECTION }}
+{% endif %}
+
+[deploy]
+default_boot_option = local
+erase_devices_metadata_priority = 10
+erase_devices_priority = 0
+http_root = /shared/html/
+http_url = {{ env.IRONIC_BOOT_BASE_URL }}
+fast_track = {{ env.IRONIC_FAST_TRACK }}
+{% if env.IRONIC_BOOT_ISO_SOURCE %}
+ramdisk_image_download_source = {{ env.IRONIC_BOOT_ISO_SOURCE }}
+{% endif %}
+{% if env.IRONIC_EXTERNAL_HTTP_URL %}
+external_http_url = {{ env.IRONIC_EXTERNAL_HTTP_URL }}
+{% elif env.IRONIC_VMEDIA_TLS_SETUP == "true" %}
+external_http_url = https://{{ env.IRONIC_URL_HOST }}:{{ env.VMEDIA_TLS_PORT }}
+{% endif %}
+{% if env.IRONIC_EXTERNAL_CALLBACK_URL %}
+external_callback_url = {{ env.IRONIC_EXTERNAL_CALLBACK_URL }}
+{% endif %}
+
+[dhcp]
+dhcp_provider = none
+
+[inspector]
+power_off = {{ false if env.IRONIC_FAST_TRACK == "true" else true }}
+# NOTE(dtantsur): keep inspection arguments synchronized with inspector.ipxe
+# Also keep in mind that only parameters unique for inspection go here.
+# No need to duplicate pxe_append_params/kernel_append_params.
+extra_kernel_params = ipa-inspection-collectors={{ env.IRONIC_IPA_COLLECTORS }} ipa-enable-vlan-interfaces={{ env.IRONIC_INSPECTOR_VLAN_INTERFACES }} ipa-inspection-dhcp-all-interfaces=1 ipa-collect-lldp=1 net.ifnames={{ '0' if env.PREDICTABLE_NIC_NAMES == 'false' else '1' }}
+
+{% if env.USE_IRONIC_INSPECTOR == "true" %}
+endpoint_override = {{ env.IRONIC_INSPECTOR_BASE_URL }}
+{% if env.IRONIC_INSPECTOR_TLS_SETUP == "true" %}
+cafile = {{ env.IRONIC_INSPECTOR_CACERT_FILE }}
+insecure = {{ env.IRONIC_INSPECTOR_INSECURE }}
+{% endif %}
+{% if env.IRONIC_INSPECTOR_CALLBACK_ENDPOINT_OVERRIDE %}
+callback_endpoint_override = {{ env.IRONIC_INSPECTOR_CALLBACK_ENDPOINT_OVERRIDE }}
+{% endif %}
+{% else %}
+hooks = $default_hooks,parse-lldp
+add_ports = all
+keep_ports = present
+{% endif %}
+
+[ipmi]
+# use_ipmitool_retries transfers the responsibility of retrying to ipmitool
+# when supported. If set to false, then ipmitool is called as follows :
+# $ipmitool -R 1 -N 1 ...
+# and Ironic handles the retry loop.
+use_ipmitool_retries = false
+# The following parameters are the defaults in Ironic. They are used in the
+# following way if use_ipmitool_retries is set to true:
+# $ipmitool -R -N ...
+# where :
+# X = command_retry_timeout / min_command_interval
+# Y = min_command_interval
+# If use_ipmitool_retries is false, then ironic retries X times, with an
+# interval of Y in between each tries.
+min_command_interval = 5
+command_retry_timeout = 60
+# List of possible cipher suites versions that can be
+# supported by the hardware in case the field `cipher_suite`
+# is not set for the node. (list value)
+cipher_suite_versions = 3,17
+
+{% if env.IRONIC_EXPOSE_JSON_RPC | lower == "true" %}
+[json_rpc]
+# We assume that when we run API and conductor in the same container, they use
+# authentication over localhost, using the same credentials as API, to prevent
+# unauthenticated connections from other processes in the same host since the
+# containers are in host networking.
+auth_strategy = {{ env.JSON_RPC_AUTH_STRATEGY }}
+http_basic_auth_user_file = /etc/ironic/htpasswd-rpc
+{% if env.IRONIC_DEPLOYMENT == "Conductor" and env.JSON_RPC_AUTH_STRATEGY == "noauth" %}
+# if access is unauthenticated, we bind only to localhost - use that as the
+# host name also, so that the client can find the server
+host_ip = localhost
+{% else %}
+host_ip = {% if env.LISTEN_ALL_INTERFACES | lower == "true" %}::{% else %}{{ env.IRONIC_IP }}{% endif %}
+{% endif %}
+{% if env.IRONIC_TLS_SETUP == "true" %}
+use_ssl = true
+cafile = {{ env.IRONIC_CACERT_FILE }}
+insecure = {{ env.IRONIC_INSECURE }}
+{% endif %}
+{% endif %}
+
+[nova]
+send_power_notifications = false
+
+[oslo_messaging_notifications]
+driver = prometheus_exporter
+location = /shared/ironic_prometheus_exporter
+transport_url = fake://
+
+[pxe]
+# NOTE(dtantsur): keep this value at least 3x lower than
+# [conductor]deploy_callback_timeout so that at least some retries happen.
+# The default settings enable 3 retries after 20 minutes each.
+boot_retry_timeout = 1200
+images_path = /shared/html/tmp
+instance_master_path = /shared/html/master_images
+tftp_master_path = /shared/tftpboot/master_images
+tftp_root = /shared/tftpboot
+kernel_append_params = nofb nomodeset vga=normal ipa-insecure={{ env.IPA_INSECURE }} {% if env.IRONIC_RAMDISK_SSH_KEY %}sshkey="{{ env.IRONIC_RAMDISK_SSH_KEY|trim }}"{% endif %} {{ env.IRONIC_KERNEL_PARAMS|trim }} systemd.journald.forward_to_console=yes
+# This makes networking boot templates generated even for nodes using local
+# boot (the default), ensuring that they boot correctly even if they start
+# netbooting for some reason (e.g. with the noop management interface).
+enable_netboot_fallback = true
+# Enable the fallback path to in-band inspection
+ipxe_fallback_script = inspector.ipxe
+
+[redfish]
+use_swift = false
+kernel_append_params = nofb nomodeset vga=normal ipa-insecure={{ env.IPA_INSECURE }} {% if env.IRONIC_RAMDISK_SSH_KEY %}sshkey="{{ env.IRONIC_RAMDISK_SSH_KEY|trim }}"{% endif %} {{ env.IRONIC_KERNEL_PARAMS|trim }} systemd.journald.forward_to_console=yes
+
+[ilo]
+kernel_append_params = nofb nomodeset vga=normal ipa-insecure={{ env.IPA_INSECURE }} {% if env.IRONIC_RAMDISK_SSH_KEY %}sshkey="{{ env.IRONIC_RAMDISK_SSH_KEY|trim }}"{% endif %} {{ env.IRONIC_KERNEL_PARAMS|trim }} systemd.journald.forward_to_console=yes
+use_web_server_for_images = true
+
+[irmc]
+kernel_append_params = nofb nomodeset vga=normal ipa-insecure={{ env.IPA_INSECURE }} {% if env.IRONIC_RAMDISK_SSH_KEY %}sshkey="{{ env.IRONIC_RAMDISK_SSH_KEY|trim }}"{% endif %} {{ env.IRONIC_KERNEL_PARAMS|trim }} systemd.journald.forward_to_console=yes
+
+[service_catalog]
+endpoint_override = {{ env.IRONIC_BASE_URL }}
+
+{% if env.IRONIC_TLS_SETUP == "true" %}
+[ssl]
+cert_file = {{ env.IRONIC_CERT_FILE }}
+key_file = {{ env.IRONIC_KEY_FILE }}
+{% endif %}
diff --git a/ironic-image/mkisofs_wrapper b/ironic-image/mkisofs_wrapper
new file mode 100644
index 0000000..d1d1a3c
--- /dev/null
+++ b/ironic-image/mkisofs_wrapper
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+xorriso -as mkisofs "${@}"
\ No newline at end of file
diff --git a/ironic-image/network-data-schema-empty.json b/ironic-image/network-data-schema-empty.json
new file mode 100644
index 0000000..d31a3bc
--- /dev/null
+++ b/ironic-image/network-data-schema-empty.json
@@ -0,0 +1 @@
+{}
diff --git a/ironic-image/prepare-efi.sh b/ironic-image/prepare-efi.sh
new file mode 100644
index 0000000..84e0808
--- /dev/null
+++ b/ironic-image/prepare-efi.sh
@@ -0,0 +1,27 @@
+#!/bin/bash
+
+set -euxo pipefail
+
+ARCH=$(uname -m)
+DEST=${2:-/tmp/esp.img}
+OS=${1:-sles}
+
+BOOTEFI=BOOTX64.efi
+GRUBEFI=grubx64.efi
+
+dd bs=1024 count=6400 if=/dev/zero of=$DEST
+mkfs.msdos -F 12 -n 'ESP_IMAGE' $DEST
+
+mkdir -p /boot/efi/EFI/BOOT
+cp -L /usr/lib64/efi/shim.efi /boot/efi/EFI/BOOT/$BOOTEFI
+mkdir -p /boot/efi/EFI/$OS
+#cp /usr/share/grub2/x86_64-efi/grub.efi /boot/efi/EFI/$OS/$GRUBEFI
+cp /usr/share/grub2/x86_64-efi/grub.efi /boot/efi/EFI/$OS/grub.efi
+
+mmd -i $DEST EFI
+mmd -i $DEST EFI/BOOT
+mcopy -i $DEST -v /boot/efi/EFI/BOOT/$BOOTEFI ::EFI/BOOT
+#mcopy -i $DEST -v /boot/efi/EFI/$OS/$GRUBEFI ::EFI/BOOT
+mcopy -i $DEST -v /boot/efi/EFI/$OS/grub.efi ::EFI/BOOT
+mdir -i $DEST ::EFI/BOOT;
+
diff --git a/ironic-image/rundnsmasq b/ironic-image/rundnsmasq
new file mode 100644
index 0000000..92af2eb
--- /dev/null
+++ b/ironic-image/rundnsmasq
@@ -0,0 +1,35 @@
+#!/usr/bin/bash
+
+set -eux
+
+# shellcheck disable=SC1091
+. /bin/ironic-common.sh
+
+export HTTP_PORT=${HTTP_PORT:-80}
+DNSMASQ_EXCEPT_INTERFACE=${DNSMASQ_EXCEPT_INTERFACE:-lo}
+export DNS_PORT=${DNS_PORT:-0}
+
+wait_for_interface_or_ip
+if [[ "${DNS_IP:-}" == "provisioning" ]]; then
+ export DNS_IP="$IRONIC_URL_HOST"
+fi
+
+mkdir -p /shared/tftpboot
+mkdir -p /shared/html/images
+mkdir -p /shared/html/pxelinux.cfg
+
+# Copy files to shared mount
+cp /tftpboot/undionly.kpxe /tftpboot/snponly.efi /shared/tftpboot
+
+# Template and write dnsmasq.conf
+# we template via /tmp as sed otherwise creates temp files in /etc directory
+# where we can't write
+python3 -c 'import os; import sys; import jinja2; sys.stdout.write(jinja2.Template(sys.stdin.read()).render(env=os.environ))' /tmp/dnsmasq.conf
+
+for iface in $(echo "$DNSMASQ_EXCEPT_INTERFACE" | tr ',' ' '); do
+ sed -i -e "/^interface=.*/ a\except-interface=${iface}" /tmp/dnsmasq.conf
+done
+cat /tmp/dnsmasq.conf > /etc/dnsmasq.conf
+rm /tmp/dnsmasq.conf
+
+exec /usr/sbin/dnsmasq -d -q -C /etc/dnsmasq.conf
diff --git a/ironic-image/runhttpd b/ironic-image/runhttpd
new file mode 100644
index 0000000..57e7c97
--- /dev/null
+++ b/ironic-image/runhttpd
@@ -0,0 +1,101 @@
+#!/usr/bin/bash
+
+# shellcheck disable=SC1091
+. /bin/tls-common.sh
+. /bin/ironic-common.sh
+. /bin/auth-common.sh
+
+export HTTP_PORT=${HTTP_PORT:-80}
+export VMEDIA_TLS_PORT=${VMEDIA_TLS_PORT:-8083}
+
+INSPECTOR_ORIG_HTTPD_CONFIG=/etc/httpd/conf.d/inspector-apache.conf.j2
+INSPECTOR_RESULT_HTTPD_CONFIG=/etc/httpd/conf.d/ironic-inspector.conf
+export IRONIC_REVERSE_PROXY_SETUP=${IRONIC_REVERSE_PROXY_SETUP:-false}
+export INSPECTOR_REVERSE_PROXY_SETUP=${INSPECTOR_REVERSE_PROXY_SETUP:-false}
+
+# In Metal3 context they are called node images in Ironic context they are
+# called user images.
+export HTTPD_SERVE_NODE_IMAGES="${HTTPD_SERVE_NODE_IMAGES:-true}"
+
+# Whether to enable fast_track provisioning or not
+IRONIC_FAST_TRACK=${IRONIC_FAST_TRACK:-true}
+
+# Whether to activate the EnableSendfile apache directive for httpd
+HTTPD_ENABLE_SENDFILE="${HTTPD_ENABLE_SENDFILE:-false}"
+
+# Set of collectors that should be used with IPA inspection
+export IRONIC_IPA_COLLECTORS=${IRONIC_IPA_COLLECTORS:-default,logs}
+
+wait_for_interface_or_ip
+
+mkdir -p /shared/html
+chmod 0777 /shared/html
+
+IRONIC_BASE_URL="${IRONIC_SCHEME}://${IRONIC_URL_HOST}"
+
+if [[ "${USE_IRONIC_INSPECTOR}" == "true" ]]; then
+ INSPECTOR_EXTRA_ARGS=" ipa-inspection-callback-url=${IRONIC_BASE_URL}:${IRONIC_INSPECTOR_ACCESS_PORT}/v1/continue"
+else
+ INSPECTOR_EXTRA_ARGS=" ipa-inspection-callback-url=${IRONIC_BASE_URL}:${IRONIC_ACCESS_PORT}/v1/continue_inspection"
+fi
+
+if [[ "$IRONIC_FAST_TRACK" == "true" ]]; then
+ INSPECTOR_EXTRA_ARGS+=" ipa-api-url=${IRONIC_BASE_URL}:${IRONIC_ACCESS_PORT}"
+fi
+export INSPECTOR_EXTRA_ARGS
+
+# Copy files to shared mount
+render_j2_config /tmp/inspector.ipxe.j2 /shared/html/inspector.ipxe
+cp /tmp/uefi_esp.img /shared/html/uefi_esp.img
+
+# Render the core httpd config
+render_j2_config /etc/httpd/conf/httpd.conf.j2 /etc/httpd/conf/httpd.conf
+
+if [[ "$USE_IRONIC_INSPECTOR" == "true" ]] && [[ "$IRONIC_INSPECTOR_TLS_SETUP" == "true" ]]; then
+ if [[ "${INSPECTOR_REVERSE_PROXY_SETUP}" == "true" ]]; then
+ render_j2_config "$INSPECTOR_ORIG_HTTPD_CONFIG" "$INSPECTOR_RESULT_HTTPD_CONFIG"
+ fi
+else
+ export INSPECTOR_REVERSE_PROXY_SETUP="false" # If TLS is not used, we have no reason to use the reverse proxy
+fi
+
+if [[ "$IRONIC_TLS_SETUP" == "true" ]]; then
+ if [[ "${IRONIC_REVERSE_PROXY_SETUP}" == "true" ]]; then
+ render_j2_config /tmp/httpd-ironic-api.conf.j2 /etc/httpd/conf.d/ironic.conf
+ fi
+else
+ export IRONIC_REVERSE_PROXY_SETUP="false" # If TLS is not used, we have no reason to use the reverse proxy
+fi
+
+write_htpasswd_files
+
+# Render httpd TLS configuration for /shared/html/
+if [[ "$IRONIC_VMEDIA_TLS_SETUP" == "true" ]]; then
+ render_j2_config /etc/httpd-vmedia.conf.j2 /etc/httpd/conf.d/vmedia.conf
+fi
+
+# Set up inotify to kill the container (restart) whenever cert files for ironic inspector change
+if [[ "$IRONIC_INSPECTOR_TLS_SETUP" == "true" ]] && [[ "${RESTART_CONTAINER_CERTIFICATE_UPDATED}" == "true" ]]; then
+ # shellcheck disable=SC2034
+ inotifywait -m -e delete_self "${IRONIC_INSPECTOR_CERT_FILE}" | while read -r file event; do
+ kill -WINCH $(pgrep httpd)
+ done &
+fi
+
+# Set up inotify to kill the container (restart) whenever cert files for ironic api change
+if [[ "$IRONIC_TLS_SETUP" == "true" ]] && [[ "${RESTART_CONTAINER_CERTIFICATE_UPDATED}" == "true" ]]; then
+ # shellcheck disable=SC2034
+ inotifywait -m -e delete_self "${IRONIC_CERT_FILE}" | while read -r file event; do
+ kill -WINCH $(pgrep httpd)
+ done &
+fi
+
+# Set up inotify to kill the container (restart) whenever cert of httpd for /shared/html/ path change
+if [[ "$IRONIC_VMEDIA_TLS_SETUP" == "true" ]] && [[ "${RESTART_CONTAINER_CERTIFICATE_UPDATED}" == "true" ]]; then
+ # shellcheck disable=SC2034
+ inotifywait -m -e delete_self "${IRONIC_VMEDIA_CERT_FILE}" | while read -r file event; do
+ kill -WINCH $(pgrep httpd)
+ done &
+fi
+
+exec /usr/sbin/httpd -DFOREGROUND -f /etc/httpd/conf/httpd.conf
diff --git a/ironic-image/runironic b/ironic-image/runironic
new file mode 100644
index 0000000..5dd6ef2
--- /dev/null
+++ b/ironic-image/runironic
@@ -0,0 +1,25 @@
+#!/usr/bin/bash
+
+# These settings must go before configure-ironic since it has different
+# defaults.
+export IRONIC_USE_MARIADB=${IRONIC_USE_MARIADB:-false}
+export IRONIC_EXPOSE_JSON_RPC=${IRONIC_EXPOSE_JSON_RPC:-false}
+
+# shellcheck disable=SC1091
+. /bin/configure-ironic.sh
+
+# Ramdisk logs
+mkdir -p /shared/log/ironic/deploy
+
+run_ironic_dbsync
+
+if [[ "$IRONIC_TLS_SETUP" == "true" ]] && [[ "${RESTART_CONTAINER_CERTIFICATE_UPDATED}" == "true" ]]; then
+ # shellcheck disable=SC2034
+ inotifywait -m -e delete_self "${IRONIC_CERT_FILE}" | while read -r file event; do
+ kill $(pgrep ironic)
+ done &
+fi
+
+configure_ironic_auth
+
+exec /usr/bin/ironic
diff --git a/ironic-image/runironic-api b/ironic-image/runironic-api
new file mode 100644
index 0000000..9deb9ac
--- /dev/null
+++ b/ironic-image/runironic-api
@@ -0,0 +1,13 @@
+#!/usr/bin/bash
+
+export IRONIC_DEPLOYMENT="API"
+
+# shellcheck disable=SC1091
+. /bin/configure-ironic.sh
+
+export IRONIC_REVERSE_PROXY_SETUP=false
+
+python3 -c 'import os; import sys; import jinja2; sys.stdout.write(jinja2.Template(sys.stdin.read()).render(env=os.environ))' < /tmp/httpd-ironic-api.conf.j2 > /etc/httpd/conf.d/ironic.conf
+
+# shellcheck disable=SC1091
+. /bin/runhttpd
diff --git a/ironic-image/runironic-conductor b/ironic-image/runironic-conductor
new file mode 100644
index 0000000..64b2c82
--- /dev/null
+++ b/ironic-image/runironic-conductor
@@ -0,0 +1,20 @@
+#!/usr/bin/bash
+
+export IRONIC_DEPLOYMENT="Conductor"
+
+# shellcheck disable=SC1091
+. /bin/configure-ironic.sh
+
+# Ramdisk logs
+mkdir -p /shared/log/ironic/deploy
+
+run_ironic_dbsync
+
+if [[ "$IRONIC_TLS_SETUP" == "true" ]] && [[ "${RESTART_CONTAINER_CERTIFICATE_UPDATED}" == "true" ]]; then
+ # shellcheck disable=SC2034
+ inotifywait -m -e delete_self "${IRONIC_CERT_FILE}" | while read -r file event; do
+ kill $(pgrep ironic)
+ done &
+fi
+
+exec /usr/bin/ironic-conductor
diff --git a/ironic-image/runironic-exporter b/ironic-image/runironic-exporter
new file mode 100644
index 0000000..f2f60f2
--- /dev/null
+++ b/ironic-image/runironic-exporter
@@ -0,0 +1,12 @@
+#!/usr/bin/bash
+
+# shellcheck disable=SC1091
+. /bin/configure-ironic.sh
+
+FLASK_RUN_HOST=${FLASK_RUN_HOST:-0.0.0.0}
+FLASK_RUN_PORT=${FLASK_RUN_PORT:-9608}
+
+export IRONIC_CONFIG="/etc/ironic/ironic.conf"
+
+exec gunicorn -b "${FLASK_RUN_HOST}:${FLASK_RUN_PORT}" -w 4 \
+ ironic_prometheus_exporter.app.wsgi:application
diff --git a/ironic-image/runironic-inspector b/ironic-image/runironic-inspector
new file mode 100644
index 0000000..c43782d
--- /dev/null
+++ b/ironic-image/runironic-inspector
@@ -0,0 +1,62 @@
+#!/usr/bin/bash
+
+set -euxo pipefail
+
+CONFIG=/etc/ironic-inspector/ironic-inspector.conf
+
+export IRONIC_INSPECTOR_ENABLE_DISCOVERY=${IRONIC_INSPECTOR_ENABLE_DISCOVERY:-false}
+export INSPECTOR_REVERSE_PROXY_SETUP=${INSPECTOR_REVERSE_PROXY_SETUP:-false}
+
+# shellcheck disable=SC1091
+. /bin/tls-common.sh
+# shellcheck disable=SC1091
+. /bin/ironic-common.sh
+# shellcheck disable=SC1091
+. /bin/auth-common.sh
+
+if [[ "$USE_IRONIC_INSPECTOR" == "false" ]]; then
+ echo "FATAL: ironic-inspector is disabled via USE_IRONIC_INSPECTOR"
+ exit 1
+fi
+
+wait_for_interface_or_ip
+
+IRONIC_INSPECTOR_PORT=${IRONIC_INSPECTOR_ACCESS_PORT}
+if [[ "$IRONIC_INSPECTOR_TLS_SETUP" == "true" ]]; then
+ if [[ "${INSPECTOR_REVERSE_PROXY_SETUP}" == "true" ]] && [[ "${IRONIC_INSPECTOR_PRIVATE_PORT}" != "unix" ]]; then
+ IRONIC_INSPECTOR_PORT=$IRONIC_INSPECTOR_PRIVATE_PORT
+ fi
+else
+ export INSPECTOR_REVERSE_PROXY_SETUP="false" # If TLS is not used, we have no reason to use the reverse proxy
+fi
+
+export IRONIC_INSPECTOR_BASE_URL="${IRONIC_INSPECTOR_SCHEME}://${IRONIC_URL_HOST}:${IRONIC_INSPECTOR_PORT}"
+export IRONIC_BASE_URL="${IRONIC_SCHEME}://${IRONIC_URL_HOST}:${IRONIC_ACCESS_PORT}"
+
+build_j2_config()
+{
+ local CONFIG_FILE="$1"
+ python3 -c 'import os; import sys; import jinja2; sys.stdout.write(jinja2.Template(sys.stdin.read()).render(env=os.environ))' < "$CONFIG_FILE.j2"
+}
+
+# Merge with the original configuration file from the package.
+build_j2_config "$CONFIG" | crudini --merge "$CONFIG"
+
+configure_inspector_auth
+
+configure_client_basic_auth ironic "${CONFIG}"
+
+ironic-inspector-dbsync --config-file "${CONFIG}" upgrade
+
+if [[ "$INSPECTOR_REVERSE_PROXY_SETUP" == "false" ]] && [[ "${RESTART_CONTAINER_CERTIFICATE_UPDATED}" == "true" ]]; then
+ # shellcheck disable=SC2034
+ inotifywait -m -e delete_self "${IRONIC_INSPECTOR_CERT_FILE}" | while read -r file event; do
+ kill $(pgrep ironic)
+ done &
+fi
+
+# Make sure ironic traffic bypasses any proxies
+export NO_PROXY="${NO_PROXY:-},$IRONIC_IP"
+
+# shellcheck disable=SC2086
+exec /usr/bin/ironic-inspector
diff --git a/ironic-image/runlogwatch.sh b/ironic-image/runlogwatch.sh
new file mode 100644
index 0000000..8b2124e
--- /dev/null
+++ b/ironic-image/runlogwatch.sh
@@ -0,0 +1,20 @@
+#!/usr/bin/bash
+
+# Ramdisk logs path
+LOG_DIRS=("/shared/log/ironic/deploy" "/shared/log/ironic-inspector/ramdisk")
+
+while :; do
+ for LOG_DIR in "${LOG_DIRS[@]}"; do
+ if ! ls "${LOG_DIR}"/*.tar.gz 1> /dev/null 2>&1; then
+ continue
+ fi
+
+ for fn in "${LOG_DIR}"/*.tar.gz; do
+ echo "************ Contents of $fn ramdisk log file bundle **************"
+ tar -xOzvvf "$fn" | sed -e "s/^/$(basename "$fn"): /"
+ rm -f "$fn"
+ done
+ done
+
+ sleep 5
+done
diff --git a/ironic-image/tls-common.sh b/ironic-image/tls-common.sh
new file mode 100644
index 0000000..992f475
--- /dev/null
+++ b/ironic-image/tls-common.sh
@@ -0,0 +1,101 @@
+#!/bin/bash
+
+export IRONIC_CERT_FILE=/certs/ironic/tls.crt
+export IRONIC_KEY_FILE=/certs/ironic/tls.key
+export IRONIC_CACERT_FILE=/certs/ca/ironic/tls.crt
+export IRONIC_INSECURE=${IRONIC_INSECURE:-false}
+export IRONIC_SSL_PROTOCOL=${IRONIC_SSL_PROTOCOL:-"-ALL +TLSv1.2 +TLSv1.3"}
+export IRONIC_VMEDIA_SSL_PROTOCOL=${IRONIC_VMEDIA_SSL_PROTOCOL:-"ALL"}
+
+export IRONIC_INSPECTOR_CERT_FILE=/certs/ironic-inspector/tls.crt
+export IRONIC_INSPECTOR_KEY_FILE=/certs/ironic-inspector/tls.key
+export IRONIC_INSPECTOR_CACERT_FILE=/certs/ca/ironic-inspector/tls.crt
+export IRONIC_INSPECTOR_INSECURE=${IRONIC_INSPECTOR_INSECURE:-$IRONIC_INSECURE}
+
+export IRONIC_VMEDIA_CERT_FILE=/certs/vmedia/tls.crt
+export IRONIC_VMEDIA_KEY_FILE=/certs/vmedia/tls.key
+
+export RESTART_CONTAINER_CERTIFICATE_UPDATED=${RESTART_CONTAINER_CERTIFICATE_UPDATED:-"false"}
+
+export MARIADB_CACERT_FILE=/certs/ca/mariadb/tls.crt
+
+mkdir -p /certs/ironic
+mkdir -p /certs/ironic-inspector
+mkdir -p /certs/ca/ironic
+mkdir -p /certs/ca/ironic-inspector
+
+if [[ -f "$IRONIC_CERT_FILE" ]] && [[ ! -f "$IRONIC_KEY_FILE" ]]; then
+ echo "Missing TLS Certificate key file $IRONIC_KEY_FILE"
+ exit 1
+fi
+if [[ ! -f "$IRONIC_CERT_FILE" ]] && [[ -f "$IRONIC_KEY_FILE" ]]; then
+ echo "Missing TLS Certificate file $IRONIC_CERT_FILE"
+ exit 1
+fi
+
+if [[ -f "$IRONIC_INSPECTOR_CERT_FILE" ]] && [[ ! -f "$IRONIC_INSPECTOR_KEY_FILE" ]]; then
+ echo "Missing TLS Certificate key file $IRONIC_INSPECTOR_KEY_FILE"
+ exit 1
+fi
+if [[ ! -f "$IRONIC_INSPECTOR_CERT_FILE" ]] && [[ -f "$IRONIC_INSPECTOR_KEY_FILE" ]]; then
+ echo "Missing TLS Certificate file $IRONIC_INSPECTOR_CERT_FILE"
+ exit 1
+fi
+
+if [[ -f "$IRONIC_VMEDIA_CERT_FILE" ]] && [[ ! -f "$IRONIC_VMEDIA_KEY_FILE" ]]; then
+ echo "Missing TLS Certificate key file $IRONIC_VMEDIA_KEY_FILE"
+ exit 1
+fi
+if [[ ! -f "$IRONIC_VMEDIA_CERT_FILE" ]] && [[ -f "$IRONIC_VMEDIA_KEY_FILE" ]]; then
+ echo "Missing TLS Certificate file $IRONIC_VMEDIA_CERT_FILE"
+ exit 1
+fi
+
+copy_atomic()
+{
+ local src="$1"
+ local dest="$2"
+ local tmpdest
+
+ tmpdest=$(mktemp "$dest.XXX")
+ cp "$src" "$tmpdest"
+ # Hard linking is atomic, but only works on the same volume
+ ln -f "$tmpdest" "$dest"
+ rm -f "$tmpdest"
+}
+
+if [[ -f "$IRONIC_CERT_FILE" ]] || [[ -f "$IRONIC_CACERT_FILE" ]]; then
+ export IRONIC_TLS_SETUP="true"
+ export IRONIC_SCHEME="https"
+ if [[ ! -f "$IRONIC_CACERT_FILE" ]]; then
+ copy_atomic "$IRONIC_CERT_FILE" "$IRONIC_CACERT_FILE"
+ fi
+else
+ export IRONIC_TLS_SETUP="false"
+ export IRONIC_SCHEME="http"
+fi
+
+if [[ -f "$IRONIC_INSPECTOR_CERT_FILE" ]] || [[ -f "$IRONIC_INSPECTOR_CACERT_FILE" ]]; then
+ export IRONIC_INSPECTOR_TLS_SETUP="true"
+ export IRONIC_INSPECTOR_SCHEME="https"
+ if [[ ! -f "$IRONIC_INSPECTOR_CACERT_FILE" ]]; then
+ copy_atomic "$IRONIC_INSPECTOR_CERT_FILE" "$IRONIC_INSPECTOR_CACERT_FILE"
+ fi
+else
+ export IRONIC_INSPECTOR_TLS_SETUP="false"
+ export IRONIC_INSPECTOR_SCHEME="http"
+fi
+
+if [[ -f "$IRONIC_VMEDIA_CERT_FILE" ]]; then
+ export IRONIC_VMEDIA_SCHEME="https"
+ export IRONIC_VMEDIA_TLS_SETUP="true"
+else
+ export IRONIC_VMEDIA_SCHEME="http"
+ export IRONIC_VMEDIA_TLS_SETUP="false"
+fi
+
+if [[ -f "$MARIADB_CACERT_FILE" ]]; then
+ export MARIADB_TLS_ENABLED="true"
+else
+ export MARIADB_TLS_ENABLED="false"
+fi
diff --git a/ironic-ipa-downloader-image/Dockerfile b/ironic-ipa-downloader-image/Dockerfile
new file mode 100644
index 0000000..98fa91e
--- /dev/null
+++ b/ironic-ipa-downloader-image/Dockerfile
@@ -0,0 +1,45 @@
+# SPDX-License-Identifier: Apache-2.0
+#!BuildTag: %%IMG_PREFIX%%ironic-ipa-downloader:2.0.0
+#!BuildTag: %%IMG_PREFIX%%ironic-ipa-downloader:2.0.0-%RELEASE%
+#!BuildVersion: 15.6
+ARG SLE_VERSION
+FROM registry.suse.com/bci/bci-micro:$SLE_VERSION AS micro
+
+FROM registry.suse.com/bci/bci-base:$SLE_VERSION AS base
+COPY --from=micro / /installroot/
+RUN sed -i -e 's%^# rpm.install.excludedocs = no.*%rpm.install.excludedocs = yes%g' /etc/zypp/zypp.conf
+RUN zypper --installroot /installroot --non-interactive install --no-recommends openstack-ironic-image-200-x86_64 python311-devel python311 python311-pip tar gawk git curl xz fakeroot shadow sed cpio; zypper -n clean; rm -rf /var/log/*
+#RUN zypper --installroot /installroot --non-interactive install --no-recommends sles-release;
+RUN cp /usr/bin/getopt /installroot/
+
+FROM micro AS final
+
+# Define labels according to https://en.opensuse.org/Building_derived_containers
+# labelprefix=com.suse.application.ironic
+LABEL org.opencontainers.image.authors="SUSE LLC (https://www.suse.com/)"
+LABEL org.opencontainers.image.title="SLE Based Ironic IPA Downloader Container Image"
+LABEL org.opencontainers.image.description="ironic-ipa-downloader based on the SLE Base Container Image."
+LABEL org.opencontainers.image.version="2.0.0"
+LABEL org.opencontainers.image.url="https://www.suse.com/solutions/edge-computing/"
+LABEL org.opencontainers.image.created="%BUILDTIME%"
+LABEL org.opencontainers.image.vendor="SUSE LLC"
+LABEL org.opensuse.reference="%%IMG_REPO%%/%%IMG_PREFIX%%ironic-ipa-downloader:2.0.0-%RELEASE%"
+LABEL org.openbuildservice.disturl="%DISTURL%"
+LABEL com.suse.supportlevel="l3"
+LABEL com.suse.eula="SUSE Combined EULA February 2024"
+LABEL com.suse.lifecycle-url="https://www.suse.com/lifecycle"
+LABEL com.suse.image-type="application"
+LABEL com.suse.release-stage="released"
+# endlabelprefix
+
+COPY --from=base /installroot /
+RUN cp /getopt /usr/bin/
+RUN cp /srv/tftpboot/openstack-ironic-image/initrd.xz /tmp
+RUN cp /srv/tftpboot/openstack-ironic-image/openstack-ironic-image*.kernel /tmp
+# configure non-root user
+COPY configure-nonroot.sh /bin/
+RUN set -euo pipefail; chmod +x /bin/configure-nonroot.sh
+RUN set -euo pipefail; /bin/configure-nonroot.sh && rm -f /bin/configure-nonroot.sh
+COPY get-resource.sh /usr/local/bin/get-resource.sh
+
+RUN set -euo pipefail; chmod +x /usr/local/bin/get-resource.sh
diff --git a/ironic-ipa-downloader-image/_service b/ironic-ipa-downloader-image/_service
new file mode 100644
index 0000000..67ff653
--- /dev/null
+++ b/ironic-ipa-downloader-image/_service
@@ -0,0 +1,17 @@
+
+
+
+
+ Dockerfile
+ %%openstack-ironic-image-200-x86_64_version%%
+ openstack-ironic-image-200-x86_64
+ patch
+
+
+ Dockerfile
+ IMG_PREFIX=$(rpm --macros=/root/.rpmmacros -E %img_prefix)
+ IMG_PREFIX
+ IMG_REPO=$(rpm --macros=/root/.rpmmacros -E %img_repo)
+ IMG_REPO
+
+
diff --git a/ironic-ipa-downloader-image/configure-nonroot.sh b/ironic-ipa-downloader-image/configure-nonroot.sh
new file mode 100644
index 0000000..e85754f
--- /dev/null
+++ b/ironic-ipa-downloader-image/configure-nonroot.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/bash
+
+NONROOT_UID=10475
+NONROOT_GID=10475
+USER="ironic-suse"
+
+groupadd -r -g ${NONROOT_GID} ${USER}
+useradd -r -g ${NONROOT_GID} \
+ -u ${NONROOT_UID} \
+ -d /home \
+ -s /sbin/nologin \
+ ${USER}
diff --git a/ironic-ipa-downloader-image/get-resource.sh b/ironic-ipa-downloader-image/get-resource.sh
new file mode 100644
index 0000000..373553f
--- /dev/null
+++ b/ironic-ipa-downloader-image/get-resource.sh
@@ -0,0 +1,71 @@
+#!/bin/bash -xe
+#CACHEURL=http://172.22.0.1/images
+
+# Check and set http(s)_proxy. Required for cURL to use a proxy
+export http_proxy=${http_proxy:-$HTTP_PROXY}
+export https_proxy=${https_proxy:-$HTTPS_PROXY}
+export no_proxy=${no_proxy:-$NO_PROXY}
+
+# Which image should we use
+if [ -z "${IPA_BASEURI}" ]; then
+ # SLES BASED IPA - openstack-ironic-image-x86_64 package
+ mkdir -p /shared/html/images
+ cp /tmp/initrd.xz /shared/html/images/ironic-python-agent.initramfs
+ cp /tmp/openstack-ironic-image*.x86_64*.kernel /shared/html/images/ironic-python-agent.kernel
+else
+ FILENAME=ironic-python-agent
+ FILENAME_EXT=.tar
+ FFILENAME=$FILENAME$FILENAME_EXT
+
+ mkdir -p /shared/html/images /shared/tmp
+ cd /shared/html/images
+
+ TMPDIR=$(mktemp -d -p /shared/tmp)
+
+ # If we have a CACHEURL and nothing has yet been downloaded
+ # get header info from the cache
+ ls -l
+ if [ -n "$CACHEURL" -a ! -e $FFILENAME.headers ] ; then
+ curl -g --verbose --fail -O "$CACHEURL/$FFILENAME.headers" || true
+ fi
+
+ # Download the most recent version of IPA
+ if [ -e $FFILENAME.headers ] ; then
+ ETAG=$(awk '/ETag:/ {print $2}' $FFILENAME.headers | tr -d "\r")
+ cd $TMPDIR
+ curl -g --verbose --dump-header $FFILENAME.headers -O $IPA_BASEURI/$FFILENAME --header "If-None-Match: $ETAG" || cp /shared/html/images/$FFILENAME.headers .
+ # curl didn't download anything because we have the ETag already
+ # but we don't have it in the images directory
+ # Its in the cache, go get it
+ ETAG=$(awk '/ETag:/ {print $2}' $FFILENAME.headers | tr -d "\"\r")
+ if [ ! -s $FFILENAME -a ! -e /shared/html/images/$FILENAME-$ETAG/$FFILENAME ] ; then
+ mv /shared/html/images/$FFILENAME.headers .
+ curl -g --verbose -O "$CACHEURL/$FILENAME-$ETAG/$FFILENAME"
+ fi
+ else
+ cd $TMPDIR
+ curl -g --verbose --dump-header $FFILENAME.headers -O $IPA_BASEURI/$FFILENAME
+ fi
+
+ if [ -s $FFILENAME ] ; then
+ tar -xf $FFILENAME
+
+ ETAG=$(awk '/ETag:/ {print $2}' $FFILENAME.headers | tr -d "\"\r")
+ cd -
+ chmod 755 $TMPDIR
+ mv $TMPDIR $FILENAME-$ETAG
+ ln -sf $FILENAME-$ETAG/$FFILENAME.headers $FFILENAME.headers
+ ln -sf $FILENAME-$ETAG/$FILENAME.initramfs $FILENAME.initramfs
+ ln -sf $FILENAME-$ETAG/$FILENAME.kernel $FILENAME.kernel
+ else
+ rm -rf $TMPDIR
+ fi
+fi
+
+if [ -d "/tmp/ironic-certificates" ]; then
+ mkdir -p /tmp/ca/tmp-initrd && cd /tmp/ca/tmp-initrd
+ xz -d -c -k --fast /shared/html/images/ironic-python-agent.initramfs | fakeroot -s ../initrd.fakeroot cpio -i
+ mkdir -p etc/ironic-python-agent.d/ca-certs
+ cp /tmp/ironic-certificates/* etc/ironic-python-agent.d/ca-certs/
+ find . | fakeroot -i ../initrd.fakeroot cpio -o -H newc | xz --check=crc32 --x86 --lzma2 --fast > /shared/html/images/ironic-python-agent.initramfs
+fi
\ No newline at end of file