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 eaf450c9fbf..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 @@ -16,6 +16,9 @@ Volume=ironic.volume:/shared:z Environment="PROVISIONING_INTERFACE=${PROVISIONING_INTERFACE}" Environment="DHCP_RANGE=${DHCP_RANGE}" Environment="HTTP_PORT=${HTTP_PORT}" +{{ if .PlatformData.BareMetal.ProvisioningNetworkGateway }} +Environment="GATEWAY_IP=${GATEWAY_IP}" +{{ end }} [Service] EnvironmentFile=/etc/ironic.env diff --git a/data/data/bootstrap/baremetal/files/etc/ironic.env.template b/data/data/bootstrap/baremetal/files/etc/ironic.env.template index a523900840c..969be18d81c 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}}" +{{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}}" 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/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/... "${@}" 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 { 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.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/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/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..dce316b4448 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) @@ -174,6 +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))) + } } return @@ -461,6 +492,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 +639,43 @@ 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 != "" { + 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))) + } + } + } + } } // Ensure provisioningNetworkCIDR does not have any host bits set 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 {