From e9d501cae6f5d3311bbab8736395fffa48d2347c Mon Sep 17 00:00:00 2001 From: Jad Haj Yahya Date: Thu, 12 Mar 2026 16:39:55 +0200 Subject: [PATCH 1/7] Add provisioningNetworkGateway field to install-config --- .../systemd/ironic-dnsmasq.container | 1 + .../baremetal/files/etc/ironic.env.template | 1 + .../install.openshift.io_installconfigs.yaml | 10 +++++++ ...aremetal-provisioning-config.yaml.template | 1 + .../ignition/bootstrap/baremetal/template.go | 4 +++ pkg/types/baremetal/platform.go | 11 ++++++++ pkg/types/baremetal/validation/platform.go | 27 +++++++++++++++++-- 7 files changed, 53 insertions(+), 2 deletions(-) diff --git a/data/data/bootstrap/baremetal/files/etc/containers/systemd/ironic-dnsmasq.container b/data/data/bootstrap/baremetal/files/etc/containers/systemd/ironic-dnsmasq.container index eaf450c9fbf..4d365f0e3f5 100644 --- a/data/data/bootstrap/baremetal/files/etc/containers/systemd/ironic-dnsmasq.container +++ b/data/data/bootstrap/baremetal/files/etc/containers/systemd/ironic-dnsmasq.container @@ -15,6 +15,7 @@ AddCapability=NET_ADMIN NET_RAW NET_BIND_SERVICE Volume=ironic.volume:/shared:z Environment="PROVISIONING_INTERFACE=${PROVISIONING_INTERFACE}" Environment="DHCP_RANGE=${DHCP_RANGE}" +Environment="GATEWAY_IP=${GATEWAY_IP}" Environment="HTTP_PORT=${HTTP_PORT}" [Service] diff --git a/data/data/bootstrap/baremetal/files/etc/ironic.env.template b/data/data/bootstrap/baremetal/files/etc/ironic.env.template index a523900840c..e3ee3c3dc02 100644 --- a/data/data/bootstrap/baremetal/files/etc/ironic.env.template +++ b/data/data/bootstrap/baremetal/files/etc/ironic.env.template @@ -3,6 +3,7 @@ HTTP_PORT=6180 # This DHCP range is used by dnsmasq to serve DHCP to the cluster. If empty # dnsmasq will only serve TFTP, and DHCP will be disabled. DHCP_RANGE="{{.PlatformData.BareMetal.ProvisioningDHCPRange}}" +GATEWAY_IP="{{.PlatformData.BareMetal.ProvisioningNetworkGateway}}" DHCP_ALLOW_MACS="{{.PlatformData.BareMetal.ProvisioningDHCPAllowList}}" # Used by ironic to allow ssh to running IPA instances IRONIC_RAMDISK_SSH_KEY="{{.SSHKey}}" diff --git a/data/data/install.openshift.io_installconfigs.yaml b/data/data/install.openshift.io_installconfigs.yaml index b287fd6f861..45d37f257b2 100644 --- a/data/data/install.openshift.io_installconfigs.yaml +++ b/data/data/install.openshift.io_installconfigs.yaml @@ -6107,6 +6107,16 @@ spec: description: ProvisioningNetworkCIDR defines the network to use for provisioning. type: string + provisioningNetworkGateway: + description: |- + ProvisioningNetworkGateway is the IP address of the default gateway + for the provisioning network. This gateway is provided to baremetal + hosts via DHCP to enable routing to external networks during + introspection and provisioning. This field is only honored when + provisioningNetwork is set to Managed (installer-managed DHCP). + It is ignored when provisioningNetwork is Unmanaged or Disabled. + format: ip + type: string provisioningNetworkInterface: description: |- ProvisioningNetworkInterface is the name of the network interface on a control plane diff --git a/data/data/manifests/openshift/baremetal-provisioning-config.yaml.template b/data/data/manifests/openshift/baremetal-provisioning-config.yaml.template index 11694f9efaf..eb425a7e405 100644 --- a/data/data/manifests/openshift/baremetal-provisioning-config.yaml.template +++ b/data/data/manifests/openshift/baremetal-provisioning-config.yaml.template @@ -8,6 +8,7 @@ spec: provisioningNetworkCIDR: "{{.Baremetal.ProvisioningNetworkCIDR}}" provisioningNetwork: "{{.Baremetal.ProvisioningNetwork}}" provisioningDHCPRange: "{{.Baremetal.ProvisioningDHCPRange}}" + provisioningNetworkGateway: "{{.Baremetal.ProvisioningNetworkGateway}}" provisioningOSDownloadURL: "{{.ProvisioningOSDownloadURL}}" additionalNTPServers: [{{range $index, $server := .Baremetal.AdditionalNTPServers}}{{if $index}},{{end}}"{{$server}}"{{end}}] watchAllNamespaces: false diff --git a/pkg/asset/ignition/bootstrap/baremetal/template.go b/pkg/asset/ignition/bootstrap/baremetal/template.go index cae7572ceb6..0669a9273d7 100644 --- a/pkg/asset/ignition/bootstrap/baremetal/template.go +++ b/pkg/asset/ignition/bootstrap/baremetal/template.go @@ -37,6 +37,9 @@ type TemplateData struct { // should be blank. ProvisioningDHCPRange string + // ProvisioningNetworkGateway is the IP address of the default gateway for the provisioning network. + ProvisioningNetworkGateway string + // ProvisioningDHCPAllowList contains a space-separated list of all of the control plane's boot // MAC addresses. Requests to bootstrap DHCP from other hosts will be ignored. ProvisioningDHCPAllowList string @@ -194,6 +197,7 @@ func GetTemplateData(config *baremetal.Platform, networks []types.MachineNetwork switch config.ProvisioningNetwork { case baremetal.ManagedProvisioningNetwork: cidr, _ := config.ProvisioningNetworkCIDR.Mask.Size() + templateData.ProvisioningNetworkGateway = config.ProvisioningNetworkGateway // When provisioning network is managed, we set a DHCP range including // netmask for dnsmasq. templateData.ProvisioningDHCPRange = fmt.Sprintf("%s,%d", config.ProvisioningDHCPRange, cidr) diff --git a/pkg/types/baremetal/platform.go b/pkg/types/baremetal/platform.go index 712a521cb50..081947b6545 100644 --- a/pkg/types/baremetal/platform.go +++ b/pkg/types/baremetal/platform.go @@ -166,6 +166,17 @@ type Platform struct { // +optional ProvisioningDHCPRange string `json:"provisioningDHCPRange,omitempty"` + // ProvisioningNetworkGateway is the IP address of the default gateway + // for the provisioning network. This gateway is provided to baremetal + // hosts via DHCP to enable routing to external networks during + // introspection and provisioning. This field is only honored when + // provisioningNetwork is set to Managed (installer-managed DHCP). + // It is ignored when provisioningNetwork is Unmanaged or Disabled. + // + // +kubebuilder:validation:Format=ip + // +optional + ProvisioningNetworkGateway string `json:"provisioningNetworkGateway,omitempty"` + // Hosts is the information needed to create the objects in Ironic. Hosts []*Host `json:"hosts"` diff --git a/pkg/types/baremetal/validation/platform.go b/pkg/types/baremetal/validation/platform.go index e765b9023f2..def3ccaf068 100644 --- a/pkg/types/baremetal/validation/platform.go +++ b/pkg/types/baremetal/validation/platform.go @@ -175,6 +175,10 @@ func validateDHCPRange(p *baremetal.Platform, fldPath *field.Path) (allErrs fiel allErrs = append(allErrs, field.Invalid(fldPath.Child("bootstrapProvisioningIP"), p.BootstrapProvisioningIP, fmt.Sprintf("%q overlaps with the allocated DHCP range", p.BootstrapProvisioningIP))) } } + // Validate ProvisioningNetworkGateway is not in DHCP range + if provisioningNetworkGateway := net.ParseIP(p.ProvisioningNetworkGateway); provisioningNetworkGateway != nil && bytes.Compare(provisioningNetworkGateway, start) >= 0 && bytes.Compare(provisioningNetworkGateway, end) <= 0 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("provisioningNetworkGateway"), p.ProvisioningNetworkGateway, fmt.Sprintf("%q overlaps with the allocated DHCP range", p.ProvisioningNetworkGateway))) + } return } @@ -461,6 +465,12 @@ func ValidatePlatform(p *baremetal.Platform, agentBasedInstallation bool, n *typ } } + if p.ProvisioningNetworkGateway != "" { + if err := validate.IP(p.ProvisioningNetworkGateway); err != nil { + allErrs = append(allErrs, field.Invalid(fldPath.Child("provisioningNetworkGateway"), p.ProvisioningNetworkGateway, err.Error())) + } + } + enabledCaps := c.GetEnabledCapabilities() if !agentBasedInstallation && enabledCaps.Has(configv1.ClusterVersionCapabilityMachineAPI) && p.Hosts == nil { allErrs = append(allErrs, field.Invalid(fldPath.Child("hosts"), p.Hosts, "bare metal hosts are missing")) @@ -602,8 +612,21 @@ func ValidateProvisioningNetworking(p *baremetal.Platform, n *types.Networking, } // Ensure clusterProvisioningIP is in the provisioningNetworkCIDR - if !p.ProvisioningNetworkCIDR.Contains(net.ParseIP(p.ClusterProvisioningIP)) { - allErrs = append(allErrs, field.Invalid(fldPath.Child("clusterProvisioningIP"), p.ClusterProvisioningIP, fmt.Sprintf("%q is not in the provisioning network", p.ClusterProvisioningIP))) + if p.ClusterProvisioningIP != "" { + if !p.ProvisioningNetworkCIDR.Contains(net.ParseIP(p.ClusterProvisioningIP)) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("clusterProvisioningIP"), p.ClusterProvisioningIP, fmt.Sprintf("%q is not in the provisioning network", p.ClusterProvisioningIP))) + } + } + + // Ensure provisioningNetworkGateway is in the provisioningNetworkCIDR + if p.ProvisioningNetworkGateway != "" { + if !p.ProvisioningNetworkCIDR.Contains(net.ParseIP(p.ProvisioningNetworkGateway)) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("provisioningNetworkGateway"), p.ProvisioningNetworkGateway, fmt.Sprintf("%q is not in the provisioning network", p.ProvisioningNetworkGateway))) + } + // Ensure gateway is not the same as clusterProvisioningIP + if p.ProvisioningNetworkGateway == p.ClusterProvisioningIP { + allErrs = append(allErrs, field.Invalid(fldPath.Child("provisioningNetworkGateway"), p.ProvisioningNetworkGateway, "cannot be the same as clusterProvisioningIP")) + } } // Ensure provisioningNetworkCIDR does not have any host bits set From 3887176f45f7fad431df07cf43a4dc3a9c73587f Mon Sep 17 00:00:00 2001 From: Jad Haj Yahya Date: Tue, 17 Mar 2026 14:53:36 +0200 Subject: [PATCH 2/7] Add ProvisioningNetworkGateway to agent-installer for consistency --- pkg/asset/agent/manifests/agentclusterinstall.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/asset/agent/manifests/agentclusterinstall.go b/pkg/asset/agent/manifests/agentclusterinstall.go index fe8a6c0ea78..eef1c71b10c 100644 --- a/pkg/asset/agent/manifests/agentclusterinstall.go +++ b/pkg/asset/agent/manifests/agentclusterinstall.go @@ -81,6 +81,10 @@ type agentClusterInstallOnPremPlatform struct { // ProvisioningDHCPRange is used to provide DHCP services to hosts // for provisioning. ProvisioningDHCPRange string `json:"provisioningDHCPRange,omitempty"` + + // ProvisioningNetworkGateway is the IP address of the default gateway + // for the provisioning network, provided to hosts via DHCP. + ProvisioningNetworkGateway string `json:"provisioningNetworkGateway,omitempty"` } type agentClusterInstallOnPremExternalPlatform struct { @@ -294,6 +298,7 @@ func (a *AgentClusterInstall) Generate(_ context.Context, dependencies asset.Par baremetalPlatform.ProvisioningNetworkInterface = installConfig.Config.Platform.BareMetal.ProvisioningNetworkInterface baremetalPlatform.ProvisioningNetworkCIDR = installConfig.Config.Platform.BareMetal.ProvisioningNetworkCIDR baremetalPlatform.ProvisioningDHCPRange = installConfig.Config.Platform.BareMetal.ProvisioningDHCPRange + baremetalPlatform.ProvisioningNetworkGateway = installConfig.Config.Platform.BareMetal.ProvisioningNetworkGateway } } if bmIcOverridden { From 918d8cdb10cc40c866a2e3fe11397059d9e7d509 Mon Sep 17 00:00:00 2001 From: Jad Haj Yahya Date: Tue, 17 Mar 2026 19:44:37 +0200 Subject: [PATCH 3/7] Add missing unit tests --- .../manifests/agentclusterinstall_test.go | 15 +++++ pkg/asset/agent/manifests/util_test.go | 7 +++ .../bootstrap/baremetal/template_test.go | 52 ++++++++++++++++ pkg/types/baremetal/validation/platform.go | 60 +++++++++++++++++-- .../baremetal/validation/platform_test.go | 57 ++++++++++++++++++ 5 files changed, 185 insertions(+), 6 deletions(-) diff --git a/pkg/asset/agent/manifests/agentclusterinstall_test.go b/pkg/asset/agent/manifests/agentclusterinstall_test.go index 665fdccb110..e4eae1fcd16 100644 --- a/pkg/asset/agent/manifests/agentclusterinstall_test.go +++ b/pkg/asset/agent/manifests/agentclusterinstall_test.go @@ -143,6 +143,11 @@ func TestAgentClusterInstall_Generate(t *testing.T) { installConfigOverrides: `{"platform":{"baremetal":{"hosts":[{"name":"control-0.example.org","bmc":{"username":"bmc-user","password":"password","address":"172.22.0.10","disableCertificateVerification":true},"role":"master","bootMACAddress":"98:af:65:a5:8d:01","hardwareProfile":""},{"name":"control-1.example.org","bmc":{"username":"user2","password":"foo","address":"172.22.0.11","disableCertificateVerification":false},"role":"master","bootMACAddress":"98:af:65:a5:8d:02","hardwareProfile":""},{"name":"control-2.example.org","bmc":{"username":"admin","password":"bar","address":"172.22.0.12","disableCertificateVerification":true},"role":"master","bootMACAddress":"98:af:65:a5:8d:03","hardwareProfile":""}],"clusterProvisioningIP":"172.22.0.3","provisioningNetwork":"Managed","provisioningNetworkInterface":"eth0","provisioningNetworkCIDR":"172.22.0.0/24","provisioningDHCPRange":"172.22.0.10,172.22.0.254"}}}`, }) + goodBaremetalPlatformBMCGatewayACI := getGoodACI() + goodBaremetalPlatformBMCGatewayACI.SetAnnotations(map[string]string{ + installConfigOverrides: `{"platform":{"baremetal":{"hosts":[{"name":"control-0.example.org","bmc":{"username":"bmc-user","password":"password","address":"172.22.0.10","disableCertificateVerification":true},"role":"master","bootMACAddress":"98:af:65:a5:8d:01","hardwareProfile":""},{"name":"control-1.example.org","bmc":{"username":"user2","password":"foo","address":"172.22.0.11","disableCertificateVerification":false},"role":"master","bootMACAddress":"98:af:65:a5:8d:02","hardwareProfile":""},{"name":"control-2.example.org","bmc":{"username":"admin","password":"bar","address":"172.22.0.12","disableCertificateVerification":true},"role":"master","bootMACAddress":"98:af:65:a5:8d:03","hardwareProfile":""}],"clusterProvisioningIP":"172.22.0.3","provisioningNetwork":"Managed","provisioningNetworkInterface":"eth0","provisioningNetworkCIDR":"172.22.0.0/24","provisioningDHCPRange":"172.22.0.10,172.22.0.254","provisioningNetworkGateway":"172.22.0.1"}}}`, + }) + installConfigWithTrustBundlePolicy := getValidOptionalInstallConfig() installConfigWithTrustBundlePolicy.Config.AdditionalTrustBundlePolicy = types.PolicyAlways @@ -302,6 +307,16 @@ func TestAgentClusterInstall_Generate(t *testing.T) { }, expectedConfig: goodBaremetalPlatformBMCACI, }, + { + name: "valid configuration BMC and provisioning network with gateway", + dependencies: []asset.Asset{ + &workflow.AgentWorkflow{Workflow: workflow.AgentWorkflowTypeInstall}, + getValidOptionalInstallConfigWithProvisioningGateway(), + getAgentHostsWithBMCConfig(), + &agentconfig.AgentConfig{}, + }, + expectedConfig: goodBaremetalPlatformBMCGatewayACI, + }, { name: "valid configuration with AdditionalTrustBundlePolicy", dependencies: []asset.Asset{ diff --git a/pkg/asset/agent/manifests/util_test.go b/pkg/asset/agent/manifests/util_test.go index 8e775ebc12b..c102c62047a 100644 --- a/pkg/asset/agent/manifests/util_test.go +++ b/pkg/asset/agent/manifests/util_test.go @@ -276,6 +276,13 @@ func getValidOptionalInstallConfigWithProvisioning() *agent.OptionalInstallConfi return installConfig } +// getValidOptionalInstallConfigWithProvisioningGateway returns a valid optional install config with baremetal provisioning network settings including gateway. +func getValidOptionalInstallConfigWithProvisioningGateway() *agent.OptionalInstallConfig { + installConfig := getValidOptionalInstallConfigWithProvisioning() + installConfig.Config.Platform.BareMetal.ProvisioningNetworkGateway = "172.22.0.1" + return installConfig +} + func getValidAgentConfig() *agentconfig.AgentConfig { return &agentconfig.AgentConfig{ Config: &agenttypes.Config{ diff --git a/pkg/asset/ignition/bootstrap/baremetal/template_test.go b/pkg/asset/ignition/bootstrap/baremetal/template_test.go index 21495870695..365b2111a75 100644 --- a/pkg/asset/ignition/bootstrap/baremetal/template_test.go +++ b/pkg/asset/ignition/bootstrap/baremetal/template_test.go @@ -105,3 +105,55 @@ func TestTemplatingUnmanagedIPv6(t *testing.T) { assert.Equal(t, result.IronicPassword, "passw0rd") assert.Equal(t, result.ExternalURLv6, "") } + +func TestTemplatingWithGateway(t *testing.T) { + bareMetalConfig := baremetal.Platform{ + ProvisioningNetworkCIDR: ipnet.MustParseCIDR("172.22.0.0/24"), + BootstrapProvisioningIP: "172.22.0.2", + ProvisioningNetwork: baremetal.ManagedProvisioningNetwork, + ProvisioningDHCPRange: "172.22.0.10,172.22.0.100", + ProvisioningNetworkGateway: "172.22.0.1", + Hosts: []*baremetal.Host{ + { + Role: "master", + BootMACAddress: "c0:ff:ee:ca:fe:00", + }, + }, + } + + openshiftDependency := []asset.Asset{ + &manifests.Openshift{}, + } + dependencies := asset.Parents{} + dependencies.Add(openshiftDependency...) + result := GetTemplateData(&bareMetalConfig, nil, 3, "bootstrap-ironic-user", "passw0rd", dependencies) + + assert.Equal(t, result.ProvisioningNetworkGateway, "172.22.0.1") + assert.Equal(t, result.ProvisioningDHCPRange, "172.22.0.10,172.22.0.100,24") + assert.Equal(t, result.ProvisioningIPv6, false) +} + +func TestTemplatingWithoutGateway(t *testing.T) { + bareMetalConfig := baremetal.Platform{ + ProvisioningNetworkCIDR: ipnet.MustParseCIDR("172.22.0.0/24"), + BootstrapProvisioningIP: "172.22.0.2", + ProvisioningNetwork: baremetal.ManagedProvisioningNetwork, + ProvisioningDHCPRange: "172.22.0.10,172.22.0.100", + Hosts: []*baremetal.Host{ + { + Role: "master", + BootMACAddress: "c0:ff:ee:ca:fe:00", + }, + }, + } + + openshiftDependency := []asset.Asset{ + &manifests.Openshift{}, + } + dependencies := asset.Parents{} + dependencies.Add(openshiftDependency...) + result := GetTemplateData(&bareMetalConfig, nil, 3, "bootstrap-ironic-user", "passw0rd", dependencies) + + assert.Equal(t, result.ProvisioningNetworkGateway, "") + assert.Equal(t, result.ProvisioningDHCPRange, "172.22.0.10,172.22.0.100,24") +} diff --git a/pkg/types/baremetal/validation/platform.go b/pkg/types/baremetal/validation/platform.go index def3ccaf068..038793e6e75 100644 --- a/pkg/types/baremetal/validation/platform.go +++ b/pkg/types/baremetal/validation/platform.go @@ -102,6 +102,32 @@ func validateNoOverlapMachineCIDR(target *net.IPNet, n *types.Networking) error return nil } +// isNetworkOrBroadcastAddress checks if the IP is a network or broadcast address. +// For IPv4 with masks narrower than /31, network and broadcast addresses are not usable. +func isNetworkOrBroadcastAddress(ip net.IP, cidr *net.IPNet) bool { + ones, bits := cidr.Mask.Size() + // Only apply network/broadcast checks to IPv4. + if bits != 32 { + return false + } + // For IPv4 /31 and /32, all addresses are usable. + if ones >= 31 { + return false + } + // Regular IPv4 → check network/broadcast + networkAddr := cidr.IP.Mask(cidr.Mask) + if ip.Equal(networkAddr) { + return true + } + // IPv4 broadcast address check (all host bits are 1). + broadcast := make(net.IP, len(networkAddr)) + copy(broadcast, networkAddr) + for i := range broadcast { + broadcast[i] |= ^cidr.Mask[i] + } + return ip.Equal(broadcast) +} + func validateOSImageURI(uri string) error { // Check for valid URI and sha256 checksum part of the URL parsedURL, err := url.ParseRequestURI(uri) @@ -620,12 +646,34 @@ func ValidateProvisioningNetworking(p *baremetal.Platform, n *types.Networking, // Ensure provisioningNetworkGateway is in the provisioningNetworkCIDR if p.ProvisioningNetworkGateway != "" { - if !p.ProvisioningNetworkCIDR.Contains(net.ParseIP(p.ProvisioningNetworkGateway)) { - allErrs = append(allErrs, field.Invalid(fldPath.Child("provisioningNetworkGateway"), p.ProvisioningNetworkGateway, fmt.Sprintf("%q is not in the provisioning network", p.ProvisioningNetworkGateway))) - } - // Ensure gateway is not the same as clusterProvisioningIP - if p.ProvisioningNetworkGateway == p.ClusterProvisioningIP { - allErrs = append(allErrs, field.Invalid(fldPath.Child("provisioningNetworkGateway"), p.ProvisioningNetworkGateway, "cannot be the same as clusterProvisioningIP")) + gatewayIP := net.ParseIP(p.ProvisioningNetworkGateway) + if gatewayIP == nil { + allErrs = append(allErrs, field.Invalid(fldPath.Child("provisioningNetworkGateway"), p.ProvisioningNetworkGateway, fmt.Sprintf("%q is not a valid IP", p.ProvisioningNetworkGateway))) + } else { + if !p.ProvisioningNetworkCIDR.Contains(gatewayIP) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("provisioningNetworkGateway"), p.ProvisioningNetworkGateway, fmt.Sprintf("%q is not in the provisioning network", p.ProvisioningNetworkGateway))) + } + // Ensure gateway is not the same as clusterProvisioningIP + if p.ProvisioningNetworkGateway == p.ClusterProvisioningIP { + allErrs = append(allErrs, field.Invalid(fldPath.Child("provisioningNetworkGateway"), p.ProvisioningNetworkGateway, fmt.Sprintf("%q overlaps with the IP address of the node that runs the bootstrap VM or provision server", p.ProvisioningNetworkGateway))) + } + // Ensure gateway is not network or broadcast address + if isNetworkOrBroadcastAddress(gatewayIP, &p.ProvisioningNetworkCIDR.IPNet) { + networkAddr := p.ProvisioningNetworkCIDR.IP.Mask(p.ProvisioningNetworkCIDR.Mask) + if gatewayIP.Equal(networkAddr) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("provisioningNetworkGateway"), p.ProvisioningNetworkGateway, fmt.Sprintf("%q is the network address of the provisioning network", p.ProvisioningNetworkGateway))) + } else { + // It's the broadcast address + broadcast := make(net.IP, len(networkAddr)) + copy(broadcast, networkAddr) + for i := range broadcast { + broadcast[i] |= ^p.ProvisioningNetworkCIDR.Mask[i] + } + if gatewayIP.Equal(broadcast) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("provisioningNetworkGateway"), p.ProvisioningNetworkGateway, fmt.Sprintf("%q is the broadcast address of the provisioning network", p.ProvisioningNetworkGateway))) + } + } + } } } diff --git a/pkg/types/baremetal/validation/platform_test.go b/pkg/types/baremetal/validation/platform_test.go index 817c8d0cb3e..8e76af5fe77 100644 --- a/pkg/types/baremetal/validation/platform_test.go +++ b/pkg/types/baremetal/validation/platform_test.go @@ -663,6 +663,58 @@ func TestValidateProvisioning(t *testing.T) { BootstrapProvisioningIP("172.22.0.20").build(), expected: "Invalid value: \"172.22.0.20\": \"172.22.0.20\" overlaps with the allocated DHCP range", }, + { + name: "valid_provisioning_network_gateway", + platform: platform(). + ProvisioningNetworkGateway("172.22.0.1").build(), + expected: "", + }, + { + name: "invalid_provisioning_network_gateway_bad_ip", + platform: platform(). + ProvisioningNetworkGateway("not-an-ip").build(), + expected: "provisioningNetworkGateway: Invalid value: \"not-an-ip\": \"not-an-ip\" is not a valid IP", + }, + { + name: "invalid_provisioning_network_gateway_outside_cidr", + platform: platform(). + ProvisioningNetworkGateway("192.168.1.1").build(), + expected: "provisioningNetworkGateway: Invalid value: \"192.168.1.1\": \"192.168.1.1\" is not in the provisioning network", + }, + { + name: "invalid_provisioning_network_gateway_same_as_cluster_ip", + platform: platform(). + ProvisioningNetworkGateway("172.22.0.3").build(), + expected: "provisioningNetworkGateway: Invalid value: \"172.22.0.3\": \"172.22.0.3\" overlaps with the IP address of the node that runs the bootstrap VM or provision server", + }, + { + name: "invalid_provisioning_network_gateway_is_network_address", + platform: platform(). + ProvisioningNetworkGateway("172.22.0.0").build(), + expected: "provisioningNetworkGateway: Invalid value: \"172.22.0.0\": \"172.22.0.0\" is the network address of the provisioning network", + }, + { + name: "invalid_provisioning_network_gateway_is_broadcast_address", + platform: platform(). + ProvisioningNetworkGateway("172.22.0.255").build(), + expected: "provisioningNetworkGateway: Invalid value: \"172.22.0.255\": \"172.22.0.255\" is the broadcast address of the provisioning network", + }, + { + name: "invalid_provisioning_network_gateway_in_dhcp_range", + platform: platform(). + ProvisioningDHCPRange("172.22.0.10,172.22.0.100"). + ProvisioningNetworkGateway("172.22.0.50").build(), + expected: "Invalid value: \"172.22.0.50\": \"172.22.0.50\" overlaps with the allocated DHCP range", + }, + { + name: "valid_provisioning_network_gateway_ipv6", + platform: platform(). + ProvisioningNetworkCIDR("fd00::/64"). + ClusterProvisioningIP("fd00::3"). + BootstrapProvisioningIP("fd00::2"). + ProvisioningNetworkGateway("fd00::1").build(), + expected: "", + }, { name: "invalid_libvirturi", platform: platform(). @@ -967,6 +1019,11 @@ func (pb *platformBuilder) ProvisioningDHCPRange(value string) *platformBuilder return pb } +func (pb *platformBuilder) ProvisioningNetworkGateway(value string) *platformBuilder { + pb.Platform.ProvisioningNetworkGateway = value + return pb +} + func (pb *platformBuilder) Hosts(builders ...*hostBuilder) *platformBuilder { pb.Platform.Hosts = nil for _, builder := range builders { From d38ebd758518d411edefd2f3d4f872d98a3dae7a Mon Sep 17 00:00:00 2001 From: Jad Haj Yahya Date: Thu, 19 Mar 2026 22:08:40 +0200 Subject: [PATCH 4/7] handle GATEWAY_IP Variable exists with empty value --- data/data/bootstrap/baremetal/files/etc/ironic.env.template | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/data/bootstrap/baremetal/files/etc/ironic.env.template b/data/data/bootstrap/baremetal/files/etc/ironic.env.template index e3ee3c3dc02..969be18d81c 100644 --- a/data/data/bootstrap/baremetal/files/etc/ironic.env.template +++ b/data/data/bootstrap/baremetal/files/etc/ironic.env.template @@ -3,7 +3,7 @@ HTTP_PORT=6180 # This DHCP range is used by dnsmasq to serve DHCP to the cluster. If empty # dnsmasq will only serve TFTP, and DHCP will be disabled. DHCP_RANGE="{{.PlatformData.BareMetal.ProvisioningDHCPRange}}" -GATEWAY_IP="{{.PlatformData.BareMetal.ProvisioningNetworkGateway}}" +{{if .PlatformData.BareMetal.ProvisioningNetworkGateway}}GATEWAY_IP="{{.PlatformData.BareMetal.ProvisioningNetworkGateway}}"{{end}} DHCP_ALLOW_MACS="{{.PlatformData.BareMetal.ProvisioningDHCPAllowList}}" # Used by ironic to allow ssh to running IPA instances IRONIC_RAMDISK_SSH_KEY="{{.SSHKey}}" From f44ceaec5d5ea3b44ca5a4b7d39b4140b06a0b64 Mon Sep 17 00:00:00 2001 From: Jad Haj Yahya Date: Fri, 20 Mar 2026 03:08:54 +0200 Subject: [PATCH 5/7] fix shellcheck issue --- hack/go-sec.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hack/go-sec.sh b/hack/go-sec.sh index 33ae0461aef..39488b3736c 100755 --- a/hack/go-sec.sh +++ b/hack/go-sec.sh @@ -3,7 +3,7 @@ set -x if [ "$IS_CONTAINER" != "" ]; then - if [ ! "$(command -v gosec >/dev/null)" ]; then + if ! command -v gosec >/dev/null 2>&1; then go get github.com/securego/gosec/cmd/gosec fi gosec -severity high -confidence high -exclude G304 ./cmd/... ./data/... ./pkg/... "${@}" From ee157de77a5474b4cf81ba5ff27a0ba135163ef0 Mon Sep 17 00:00:00 2001 From: Jad Haj Yahya Date: Fri, 20 Mar 2026 07:00:09 +0200 Subject: [PATCH 6/7] Convert ironic dnsmasq to template to conditionally pass GATEWAY_IP env var --- ...ic-dnsmasq.container => ironic-dnsmasq.container.template} | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) rename data/data/bootstrap/baremetal/files/etc/containers/systemd/{ironic-dnsmasq.container => ironic-dnsmasq.container.template} (64%) diff --git a/data/data/bootstrap/baremetal/files/etc/containers/systemd/ironic-dnsmasq.container b/data/data/bootstrap/baremetal/files/etc/containers/systemd/ironic-dnsmasq.container.template similarity index 64% rename from data/data/bootstrap/baremetal/files/etc/containers/systemd/ironic-dnsmasq.container rename to data/data/bootstrap/baremetal/files/etc/containers/systemd/ironic-dnsmasq.container.template index 4d365f0e3f5..4c36e7faff5 100644 --- a/data/data/bootstrap/baremetal/files/etc/containers/systemd/ironic-dnsmasq.container +++ b/data/data/bootstrap/baremetal/files/etc/containers/systemd/ironic-dnsmasq.container.template @@ -15,8 +15,10 @@ AddCapability=NET_ADMIN NET_RAW NET_BIND_SERVICE Volume=ironic.volume:/shared:z Environment="PROVISIONING_INTERFACE=${PROVISIONING_INTERFACE}" Environment="DHCP_RANGE=${DHCP_RANGE}" -Environment="GATEWAY_IP=${GATEWAY_IP}" Environment="HTTP_PORT=${HTTP_PORT}" +{{ if .PlatformData.BareMetal.ProvisioningNetworkGateway }} +Environment="GATEWAY_IP=${GATEWAY_IP}" +{{ end }} [Service] EnvironmentFile=/etc/ironic.env From 2dfd51f91bf022c6ec6c79e957d27499613ddaaa Mon Sep 17 00:00:00 2001 From: Jad Haj Yahya Date: Mon, 30 Mar 2026 17:26:53 +0300 Subject: [PATCH 7/7] Move ProvisioningNetworkGateway validation inside if start --- pkg/types/baremetal/validation/platform.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pkg/types/baremetal/validation/platform.go b/pkg/types/baremetal/validation/platform.go index 038793e6e75..dce316b4448 100644 --- a/pkg/types/baremetal/validation/platform.go +++ b/pkg/types/baremetal/validation/platform.go @@ -200,10 +200,11 @@ func validateDHCPRange(p *baremetal.Platform, fldPath *field.Path) (allErrs fiel if bootstrapProvisioningIP := net.ParseIP(p.BootstrapProvisioningIP); bootstrapProvisioningIP != nil && bytes.Compare(bootstrapProvisioningIP, start) >= 0 && bytes.Compare(bootstrapProvisioningIP, end) <= 0 { allErrs = append(allErrs, field.Invalid(fldPath.Child("bootstrapProvisioningIP"), p.BootstrapProvisioningIP, fmt.Sprintf("%q overlaps with the allocated DHCP range", p.BootstrapProvisioningIP))) } - } - // Validate ProvisioningNetworkGateway is not in DHCP range - if provisioningNetworkGateway := net.ParseIP(p.ProvisioningNetworkGateway); provisioningNetworkGateway != nil && bytes.Compare(provisioningNetworkGateway, start) >= 0 && bytes.Compare(provisioningNetworkGateway, end) <= 0 { - allErrs = append(allErrs, field.Invalid(fldPath.Child("provisioningNetworkGateway"), p.ProvisioningNetworkGateway, fmt.Sprintf("%q overlaps with the allocated DHCP range", p.ProvisioningNetworkGateway))) + + // Validate ProvisioningNetworkGateway is not in DHCP range + if provisioningNetworkGateway := net.ParseIP(p.ProvisioningNetworkGateway); provisioningNetworkGateway != nil && bytes.Compare(provisioningNetworkGateway, start) >= 0 && bytes.Compare(provisioningNetworkGateway, end) <= 0 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("provisioningNetworkGateway"), p.ProvisioningNetworkGateway, fmt.Sprintf("%q overlaps with the allocated DHCP range", p.ProvisioningNetworkGateway))) + } } return