diff --git a/pkg/asset/rhcos/bootstrap_image.go b/pkg/asset/rhcos/bootstrap_image.go index c1d48afbdd3..f8c5805fad1 100644 --- a/pkg/asset/rhcos/bootstrap_image.go +++ b/pkg/asset/rhcos/bootstrap_image.go @@ -43,6 +43,12 @@ func (i *BootstrapImage) Generate(p asset.Parents) error { defer cancel() switch config.Platform.Name() { case baremetal.Name: + // Check for RHCOS image URL override + if boi := config.Platform.BareMetal.BootstrapOSImage; boi != "" { + osimage = boi + break + } + // Baremetal IPI launches a local VM for the bootstrap node // Hence requires the QEMU image to use the libvirt backend osimage, err = rhcos.QEMU(ctx) diff --git a/pkg/asset/rhcos/image.go b/pkg/asset/rhcos/image.go index 34e6035ca36..cbba0c60109 100644 --- a/pkg/asset/rhcos/image.go +++ b/pkg/asset/rhcos/image.go @@ -82,6 +82,12 @@ func osImage(config *types.InstallConfig) (string, error) { case azure.Name: osimage, err = rhcos.VHD(ctx) case baremetal.Name: + // Check for RHCOS image URL override + if oi := config.Platform.BareMetal.ClusterOSImage; oi != "" { + osimage = oi + break + } + // Note that baremetal IPI currently uses the OpenStack image // because this contains the necessary ironic config drive // ignition support, which isn't enabled in the UPI BM images diff --git a/pkg/types/baremetal/platform.go b/pkg/types/baremetal/platform.go index 7c28bbe0eed..84fad0fa96b 100644 --- a/pkg/types/baremetal/platform.go +++ b/pkg/types/baremetal/platform.go @@ -61,4 +61,16 @@ type Platform struct { // DNSVIP is the VIP to use for internal DNS communication DNSVIP string `json:"dnsVIP"` + + // BootstrapOSImage is a URL to override the default OS image + // for the bootstrap node. The URL must contain a sha256 hash of the image + // e.g https://mirror.example.com/images/qemu.qcow2.gz?sha256=a07bd... + // +optional + BootstrapOSImage string `json:"bootstrapOSImage,omitempty"` + + // ClusterOSImage is a URL to override the default OS image + // for cluster nodes. The URL must contain a sha256 hash of the image + // e.g https://mirror.example.com/images/metal.qcow2.gz?sha256=3b5a8... + // +optional + ClusterOSImage string `json:"clusterOSImage,omitempty"` } diff --git a/pkg/types/baremetal/validation/platform.go b/pkg/types/baremetal/validation/platform.go index cde76dc4e95..2ea4ccc2acf 100644 --- a/pkg/types/baremetal/validation/platform.go +++ b/pkg/types/baremetal/validation/platform.go @@ -3,6 +3,7 @@ package validation import ( "fmt" "net" + "net/url" "github.com/openshift/installer/pkg/types" "github.com/openshift/installer/pkg/types/baremetal" @@ -32,6 +33,29 @@ func validateIPNotinMachineCIDR(ip string, n *types.Networking) error { return nil } +func validateOSImageURI(uri string) error { + // Check for valid URI and sha256 checksum part of the URL + parsedURL, err := url.ParseRequestURI(uri) + if err != nil { + return fmt.Errorf("the URI provided: %s is invalid", uri) + } + if parsedURL.Scheme == "http" || parsedURL.Scheme == "https" { + var sha256Checksum string + if sha256Checksums, ok := parsedURL.Query()["sha256"]; ok { + sha256Checksum = sha256Checksums[0] + } + if sha256Checksum == "" { + return fmt.Errorf("the sha256 parameter in the %s URI is missing", uri) + } + if len(sha256Checksum) != 64 { + return fmt.Errorf("the sha256 parameter in the %s URI is invalid", uri) + } + } else { + return fmt.Errorf("the URI provided: %s must begin with http/https", uri) + } + return nil +} + // ValidatePlatform checks that the specified platform is valid. func ValidatePlatform(p *baremetal.Platform, n *types.Networking, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} @@ -84,6 +108,16 @@ func ValidatePlatform(p *baremetal.Platform, n *types.Networking, fldPath *field if err := validateIPNotinMachineCIDR(p.BootstrapProvisioningIP, n); err != nil { allErrs = append(allErrs, field.Invalid(fldPath.Child("bootstrapHostIP"), p.BootstrapProvisioningIP, err.Error())) } + if p.BootstrapOSImage != "" { + if err := validateOSImageURI(p.BootstrapOSImage); err != nil { + allErrs = append(allErrs, field.Invalid(fldPath.Child("bootstrapOSImage"), p.BootstrapOSImage, err.Error())) + } + } + if p.ClusterOSImage != "" { + if err := validateOSImageURI(p.ClusterOSImage); err != nil { + allErrs = append(allErrs, field.Invalid(fldPath.Child("clusterOSImage"), p.ClusterOSImage, err.Error())) + } + } for _, validator := range dynamicValidators { allErrs = append(allErrs, validator(p, fldPath)...) diff --git a/pkg/types/baremetal/validation/platform_test.go b/pkg/types/baremetal/validation/platform_test.go index aab411653b5..ce54afedb46 100644 --- a/pkg/types/baremetal/validation/platform_test.go +++ b/pkg/types/baremetal/validation/platform_test.go @@ -50,6 +50,23 @@ func TestValidatePlatform(t *testing.T) { }, network: network, }, + { + name: "valid_with_os_image_overrides", + platform: &baremetal.Platform{ + APIVIP: "192.168.111.2", + DNSVIP: "192.168.111.3", + IngressVIP: "192.168.111.4", + Hosts: []*baremetal.Host{}, + LibvirtURI: "qemu://system", + ClusterProvisioningIP: "172.22.0.3", + BootstrapProvisioningIP: "172.22.0.2", + ExternalBridge: "br0", + ProvisioningBridge: "br1", + BootstrapOSImage: "http://192.168.111.1/images/qemu.x86_64.qcow2.gz?sha256=3b5a882c2af3e19d515b961855d144f293cab30190c2bdedd661af31a1fc4e2f", + ClusterOSImage: "http://192.168.111.1/images/metal.x86_64.qcow2.gz?sha256=340dfa4d92450f2eee852ed1e2d02e3138cc68d824827ef9cf0a40a7ea2f93da", + }, + network: network, + }, { name: "invalid_apivip", platform: &baremetal.Platform{ @@ -195,6 +212,96 @@ func TestValidatePlatform(t *testing.T) { network: network, expected: "Invalid value: \"192.168.111.5\": the IP must not be in 192.168.111.0/24 subnet", }, + { + name: "invalid_bootstraposimage", + platform: &baremetal.Platform{ + APIVIP: "192.168.111.2", + DNSVIP: "192.168.111.3", + IngressVIP: "192.168.111.4", + Hosts: []*baremetal.Host{}, + LibvirtURI: "qemu://system", + ClusterProvisioningIP: "172.22.0.3", + BootstrapProvisioningIP: "172.22.0.2", + ExternalBridge: "br0", + ProvisioningBridge: "br1", + BootstrapOSImage: "192.168.111.1/images/qemu.x86_64.qcow2.gz?sha256=3b5a882c2af3e19d515b961855d144f293cab30190c2bdedd661af31a1fc4e2f", + ClusterOSImage: "http://192.168.111.1/images/metal.x86_64.qcow2.gz?sha256=340dfa4d92450f2eee852ed1e2d02e3138cc68d824827ef9cf0a40a7ea2f93da", + }, + network: network, + expected: "the URI provided.*is invalid", + }, + { + name: "invalid_clusterosimage", + platform: &baremetal.Platform{ + APIVIP: "192.168.111.2", + DNSVIP: "192.168.111.3", + IngressVIP: "192.168.111.4", + Hosts: []*baremetal.Host{}, + LibvirtURI: "qemu://system", + ClusterProvisioningIP: "172.22.0.3", + BootstrapProvisioningIP: "172.22.0.2", + ExternalBridge: "br0", + ProvisioningBridge: "br1", + BootstrapOSImage: "http://192.168.111.1/images/qemu.x86_64.qcow2.gz?sha256=3b5a882c2af3e19d515b961855d144f293cab30190c2bdedd661af31a1fc4e2f", + ClusterOSImage: "http//192.168.111.1/images/metal.x86_64.qcow2.gz?sha256=340dfa4d92450f2eee852ed1e2d02e3138cc68d824827ef9cf0a40a7ea2f93da", + }, + network: network, + expected: "the URI provided.*is invalid", + }, + { + name: "invalid_bootstraposimage_checksum", + platform: &baremetal.Platform{ + APIVIP: "192.168.111.2", + DNSVIP: "192.168.111.3", + IngressVIP: "192.168.111.4", + Hosts: []*baremetal.Host{}, + LibvirtURI: "qemu://system", + ClusterProvisioningIP: "172.22.0.3", + BootstrapProvisioningIP: "172.22.0.2", + ExternalBridge: "br0", + ProvisioningBridge: "br1", + BootstrapOSImage: "http://192.168.111.1/images/qemu.x86_64.qcow2.gz?md5sum=3b5a882c2af3e19d515b961855d144f293cab30190c2bdedd661af31a1fc4e2f", + ClusterOSImage: "http://192.168.111.1/images/metal.x86_64.qcow2.gz?sha256=340dfa4d92450f2eee852ed1e2d02e3138cc68d824827ef9cf0a40a7ea2f93da", + }, + network: network, + expected: "the sha256 parameter in the.*URI is missing", + }, + { + name: "invalid_clusterosimage_checksum", + platform: &baremetal.Platform{ + APIVIP: "192.168.111.2", + DNSVIP: "192.168.111.3", + IngressVIP: "192.168.111.4", + Hosts: []*baremetal.Host{}, + LibvirtURI: "qemu://system", + ClusterProvisioningIP: "172.22.0.3", + BootstrapProvisioningIP: "172.22.0.2", + ExternalBridge: "br0", + ProvisioningBridge: "br1", + BootstrapOSImage: "http://192.168.111.1/images/qemu.x86_64.qcow2.gz?sha256=3b5a882c2af3e19d515b961855d144f293cab30190c2bdedd661af31a1fc4e2f", + ClusterOSImage: "http://192.168.111.1/images/metal.x86_64.qcow2.gz?sha256=3ee852ed1e2d02e3138cc68d824827ef9cf0a40a7ea2f93da", + }, + network: network, + expected: "the sha256 parameter in the.*URI is invalid", + }, + { + name: "invalid_bootstraposimage_uri_scheme", + platform: &baremetal.Platform{ + APIVIP: "192.168.111.2", + DNSVIP: "192.168.111.3", + IngressVIP: "192.168.111.4", + Hosts: []*baremetal.Host{}, + LibvirtURI: "qemu://system", + ClusterProvisioningIP: "172.22.0.3", + BootstrapProvisioningIP: "172.22.0.2", + ExternalBridge: "br0", + ProvisioningBridge: "br1", + BootstrapOSImage: "xttp://192.168.111.1/images/qemu.x86_64.qcow2.gz?sha256=3b5a882c2af3e19d515b961855d144f293cab30190c2bdedd661af31a1fc4e2f", + ClusterOSImage: "http://192.168.111.1/images/metal.x86_64.qcow2.gz?sha256=340dfa4d92450f2eee852ed1e2d02e3138cc68d824827ef9cf0a40a7ea2f93da", + }, + network: network, + expected: "the URI provided.*must begin with http/https", + }, } for _, tc := range cases {