Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}}"
Expand Down
10 changes: 10 additions & 0 deletions data/data/install.openshift.io_installconfigs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion hack/go-sec.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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/... "${@}"
Expand Down
5 changes: 5 additions & 0 deletions pkg/asset/agent/manifests/agentclusterinstall.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
15 changes: 15 additions & 0 deletions pkg/asset/agent/manifests/agentclusterinstall_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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{
Expand Down
7 changes: 7 additions & 0 deletions pkg/asset/agent/manifests/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
4 changes: 4 additions & 0 deletions pkg/asset/ignition/bootstrap/baremetal/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
52 changes: 52 additions & 0 deletions pkg/asset/ignition/bootstrap/baremetal/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
11 changes: 11 additions & 0 deletions pkg/types/baremetal/platform.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`

Expand Down
76 changes: 74 additions & 2 deletions pkg/types/baremetal/validation/platform.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"))
Expand Down Expand Up @@ -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
Expand Down
57 changes: 57 additions & 0 deletions pkg/types/baremetal/validation/platform_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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().
Expand Down Expand Up @@ -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 {
Expand Down