github.com/openshift/installer@v1.4.17/pkg/asset/installconfig/openstack/validation/platform.go (about)

     1  package validation
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"net"
     7  	"net/url"
     8  
     9  	"github.com/gophercloud/gophercloud/v2/openstack/image/v2/images"
    10  	"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets"
    11  	"k8s.io/apimachinery/pkg/util/validation/field"
    12  	utilsslice "k8s.io/utils/strings/slices"
    13  
    14  	"github.com/openshift/installer/pkg/types"
    15  	"github.com/openshift/installer/pkg/types/openstack"
    16  )
    17  
    18  // ValidatePlatform checks that the specified platform is valid.
    19  func ValidatePlatform(p *openstack.Platform, n *types.Networking, ci *CloudInfo) field.ErrorList {
    20  	var allErrs field.ErrorList
    21  	fldPath := field.NewPath("platform", "openstack")
    22  
    23  	// validate BYO controlPlanePort usage
    24  	if p.ControlPlanePort != nil {
    25  		allErrs = append(allErrs, validateControlPlanePort(p, n, ci, fldPath)...)
    26  	}
    27  
    28  	// validate the externalNetwork
    29  	allErrs = append(allErrs, validateExternalNetwork(p, ci, fldPath)...)
    30  
    31  	// validate floating ips
    32  	allErrs = append(allErrs, validateFloatingIPs(p, ci, fldPath)...)
    33  
    34  	// validate vips (on OpenStack we need some additional checks)
    35  	allErrs = append(allErrs, validateVIPs(p, ci, fldPath)...)
    36  
    37  	// validate custom cluster os image
    38  	allErrs = append(allErrs, validateClusterOSImage(p, ci, fldPath)...)
    39  
    40  	return allErrs
    41  }
    42  
    43  // validateControlPlanePort validates the machines subnets and network, while enforcing proper byo subnets usage and returns a list of all validation errors.
    44  func validateControlPlanePort(p *openstack.Platform, n *types.Networking, ci *CloudInfo, fldPath *field.Path) (allErrs field.ErrorList) {
    45  	networkID := ""
    46  	hasIPv4Subnet := false
    47  	hasIPv6Subnet := false
    48  	if len(p.ExternalDNS) > 0 {
    49  		allErrs = append(allErrs, field.Invalid(fldPath.Child("externalDNS"), p.ExternalDNS, "externalDNS is set, externalDNS is not supported when ControlPlanePort is set"))
    50  		return allErrs
    51  	}
    52  	networkCIDRs := networksCIDRs(n.MachineNetwork)
    53  	for _, fixedIP := range p.ControlPlanePort.FixedIPs {
    54  		subnet := getSubnet(ci.ControlPlanePortSubnets, fixedIP.Subnet.ID, fixedIP.Subnet.Name)
    55  		if subnet == nil {
    56  			subnetDetail := fixedIP.Subnet.ID
    57  			if subnetDetail == "" {
    58  				subnetDetail = fixedIP.Subnet.Name
    59  			}
    60  			allErrs = append(allErrs, field.NotFound(fldPath.Child("controlPlanePort").Child("fixedIPs"), subnetDetail))
    61  		} else {
    62  			if subnet.IPVersion == 6 {
    63  				hasIPv6Subnet = true
    64  			} else {
    65  				hasIPv4Subnet = true
    66  			}
    67  			if !utilsslice.Contains(networkCIDRs, subnet.CIDR) {
    68  				allErrs = append(allErrs, field.Invalid(fldPath.Child("controlPlanePort").Child("fixedIPs"), subnet.CIDR, "controlPlanePort CIDR does not match machineNetwork"))
    69  			}
    70  			if networkID != "" && networkID != subnet.NetworkID {
    71  				allErrs = append(allErrs, field.Invalid(fldPath.Child("controlPlanePort").Child("fixedIPs"), subnet.NetworkID, "fixedIPs subnets must be on the same Network"))
    72  			}
    73  			networkID = subnet.NetworkID
    74  		}
    75  	}
    76  	if !hasIPv4Subnet && hasIPv6Subnet {
    77  		allErrs = append(allErrs, field.InternalError(fldPath.Child("controlPlanePort").Child("fixedIPs"), fmt.Errorf("one IPv4 subnet must be specified")))
    78  	} else if hasIPv4Subnet && !hasIPv6Subnet && len(p.ControlPlanePort.FixedIPs) == 2 {
    79  		allErrs = append(allErrs, field.InternalError(fldPath.Child("controlPlanePort").Child("fixedIPs"), fmt.Errorf("multiple IPv4 subnets is not supported")))
    80  	}
    81  	controlPlaneNetwork := p.ControlPlanePort.Network
    82  	if controlPlaneNetwork.ID != "" || controlPlaneNetwork.Name != "" {
    83  		networkDetail := controlPlaneNetwork.ID
    84  		if networkDetail == "" {
    85  			networkDetail = controlPlaneNetwork.Name
    86  		}
    87  		// check if the networks does not exist. If it does, verifies if the network contains the subnets
    88  		if ci.ControlPlanePortNetwork == nil {
    89  			allErrs = append(allErrs, field.NotFound(fldPath.Child("controlPlanePort").Child("network"), networkDetail))
    90  		} else if ci.ControlPlanePortNetwork.ID != networkID {
    91  			allErrs = append(allErrs, field.Invalid(fldPath.Child("controlPlanePort").Child("network"), networkDetail, "network must contain subnets"))
    92  		}
    93  	}
    94  	return allErrs
    95  }
    96  
    97  func networksCIDRs(machineNetwork []types.MachineNetworkEntry) []string {
    98  	networks := make([]string, 0, len(machineNetwork))
    99  	for _, network := range machineNetwork {
   100  		networks = append(networks, network.CIDR.String())
   101  	}
   102  	return networks
   103  }
   104  
   105  func getSubnet(controlPlaneSubnets []*subnets.Subnet, subnetID, subnetName string) *subnets.Subnet {
   106  	for _, subnet := range controlPlaneSubnets {
   107  		if subnet.ID == subnetID {
   108  			return subnet
   109  		} else if subnet.Name != "" && subnet.Name == subnetName {
   110  			return subnet
   111  		}
   112  	}
   113  	return nil
   114  }
   115  
   116  // validateExternalNetwork validates the user's input for the externalNetwork and returns a list of all validation errors
   117  func validateExternalNetwork(p *openstack.Platform, ci *CloudInfo, fldPath *field.Path) (allErrs field.ErrorList) {
   118  	// Return an error if external network was specified in the install config, but hasn't been found
   119  	if p.ExternalNetwork != "" && ci.ExternalNetwork == nil {
   120  		allErrs = append(allErrs, field.NotFound(fldPath.Child("externalNetwork"), p.ExternalNetwork))
   121  	}
   122  	return allErrs
   123  }
   124  
   125  func validateFloatingIPs(p *openstack.Platform, ci *CloudInfo, fldPath *field.Path) (allErrs field.ErrorList) {
   126  	if p.APIFloatingIP != "" {
   127  		if ci.APIFIP == nil {
   128  			allErrs = append(allErrs, field.NotFound(fldPath.Child("apiFloatingIP"), p.APIFloatingIP))
   129  		} else if ci.APIFIP.Status != "DOWN" {
   130  			allErrs = append(allErrs, field.Invalid(fldPath.Child("apiFloatingIP"), p.APIFloatingIP, "Floating IP already in use"))
   131  		} else if p.ExternalNetwork == "" {
   132  			allErrs = append(allErrs, field.Invalid(fldPath.Child("apiFloatingIP"), p.APIFloatingIP, "Cannot set floating ips when external network not specified"))
   133  		}
   134  	}
   135  
   136  	if p.IngressFloatingIP != "" {
   137  		if ci.IngressFIP == nil {
   138  			allErrs = append(allErrs, field.NotFound(fldPath.Child("ingressFloatingIP"), p.IngressFloatingIP))
   139  		} else if ci.IngressFIP.Status != "DOWN" {
   140  			allErrs = append(allErrs, field.Invalid(fldPath.Child("ingressFloatingIP"), p.IngressFloatingIP, "Floating IP already in use"))
   141  		} else if p.ExternalNetwork == "" {
   142  			allErrs = append(allErrs, field.Invalid(fldPath.Child("ingressFloatingIP"), p.IngressFloatingIP, "Cannot set floating ips when external network not specified"))
   143  		}
   144  		if p.APIFloatingIP != "" && p.APIFloatingIP == p.IngressFloatingIP {
   145  			allErrs = append(allErrs, field.Invalid(fldPath.Child("ingressFloatingIP"), p.IngressFloatingIP, "ingressFloatingIP can not be the same as apiFloatingIP"))
   146  		}
   147  	}
   148  	return allErrs
   149  }
   150  
   151  // validateVIPs adds some OpenStack specific VIP validation. The universal
   152  // platform VIP validation is done in pkg/types/validation/installconfig.go,
   153  // validateAPIAndIngressVIPs().
   154  func validateVIPs(p *openstack.Platform, ci *CloudInfo, fldPath *field.Path) (allErrs field.ErrorList) {
   155  	// If the subnet is not found in the CloudInfo object, abandon validation.
   156  	// For dual-stack the user needs to pre-create the Port for API and Ingress, so no need for validation.
   157  	if len(ci.ControlPlanePortSubnets) == 1 {
   158  		for _, allocationPool := range ci.ControlPlanePortSubnets[0].AllocationPools {
   159  			start := net.ParseIP(allocationPool.Start)
   160  			end := net.ParseIP(allocationPool.End)
   161  
   162  			// If the allocation pool is undefined, abandon validation
   163  			if start == nil || end == nil {
   164  				continue
   165  			}
   166  
   167  			for _, apiVIPString := range p.APIVIPs {
   168  				apiVIP := net.ParseIP(apiVIPString)
   169  				if bytes.Compare(start, apiVIP) <= 0 && bytes.Compare(end, apiVIP) >= 0 {
   170  					allErrs = append(allErrs, field.Invalid(fldPath.Child("apiVIPs"), apiVIPString, "apiVIP can not fall in a MachineNetwork allocation pool"))
   171  				}
   172  
   173  			}
   174  
   175  			for _, ingressVIPString := range p.IngressVIPs {
   176  				ingressVIP := net.ParseIP(ingressVIPString)
   177  				if bytes.Compare(start, ingressVIP) <= 0 && bytes.Compare(end, ingressVIP) >= 0 {
   178  					allErrs = append(allErrs, field.Invalid(fldPath.Child("ingressVIPs"), ingressVIPString, "ingressVIP can not fall in a MachineNetwork allocation pool"))
   179  				}
   180  			}
   181  		}
   182  	}
   183  
   184  	return allErrs
   185  }
   186  
   187  // validateExternalNetwork validates the user's input for the clusterOSImage and returns a list of all validation errors
   188  func validateClusterOSImage(p *openstack.Platform, ci *CloudInfo, fldPath *field.Path) (allErrs field.ErrorList) {
   189  	if p.ClusterOSImage == "" {
   190  		return
   191  	}
   192  
   193  	// For URLs we support only 'http(s)' and 'file' schemes
   194  	if uri, err := url.ParseRequestURI(p.ClusterOSImage); err == nil {
   195  		switch uri.Scheme {
   196  		case "http", "https", "file":
   197  		default:
   198  			allErrs = append(allErrs, field.Invalid(fldPath.Child("clusterOSImage"), p.ClusterOSImage, fmt.Sprintf("URL scheme should be either http(s) or file but it is '%v'", uri.Scheme)))
   199  		}
   200  		return
   201  	}
   202  
   203  	// Image should exist in OpenStack Glance
   204  	if ci.OSImage == nil {
   205  		allErrs = append(allErrs, field.NotFound(fldPath.Child("clusterOSImage"), p.ClusterOSImage))
   206  		return allErrs
   207  	}
   208  
   209  	// Image should have "active" status
   210  	if ci.OSImage.Status != images.ImageStatusActive {
   211  		allErrs = append(allErrs, field.Invalid(fldPath.Child("clusterOSImage"), p.ClusterOSImage, fmt.Sprintf("OS image must be active but its status is '%s'", ci.OSImage.Status)))
   212  	}
   213  
   214  	return allErrs
   215  }