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 }