github.com/openshift/installer@v1.4.17/pkg/asset/installconfig/openstack/validation/machinepool.go (about) 1 package validation 2 3 import ( 4 "fmt" 5 6 guuid "github.com/google/uuid" 7 "github.com/sirupsen/logrus" 8 "k8s.io/apimachinery/pkg/util/sets" 9 "k8s.io/apimachinery/pkg/util/validation/field" 10 11 "github.com/openshift/installer/pkg/types/openstack" 12 ) 13 14 type flavorRequirements struct { 15 RAM, VCPUs, Disk, RecommendedDisk int 16 } 17 18 const ( 19 minimumStorage = 25 20 recommendedStorage = 100 21 ) 22 23 var ( 24 ctrlPlaneFlavorMinimums = flavorRequirements{ 25 RAM: 16384, 26 VCPUs: 4, 27 Disk: minimumStorage, 28 RecommendedDisk: recommendedStorage, 29 } 30 computeFlavorMinimums = flavorRequirements{ 31 RAM: 8192, 32 VCPUs: 2, 33 Disk: minimumStorage, 34 RecommendedDisk: recommendedStorage, 35 } 36 ) 37 38 // ValidateMachinePool checks that the specified machine pool is valid. 39 func ValidateMachinePool(p *openstack.MachinePool, ci *CloudInfo, controlPlane bool, fldPath *field.Path) field.ErrorList { 40 allErrs := field.ErrorList{} 41 42 var checkStorageFlavor bool 43 // Validate Root Volumes 44 if p.RootVolume != nil { 45 allErrs = append(allErrs, validateVolumeTypes(p.RootVolume.Types, ci.VolumeTypes, fldPath.Child("rootVolume").Child("types"))...) 46 if p.RootVolume.Size < minimumStorage { 47 allErrs = append(allErrs, field.Invalid(fldPath.Child("rootVolume").Child("size"), p.RootVolume.Size, fmt.Sprintf("Volume size must be greater than %d GB to use root volumes, had %d GB", minimumStorage, p.RootVolume.Size))) 48 } else if p.RootVolume.Size < recommendedStorage { 49 logrus.Warnf("Volume size is recommended to be greater than %d GB to use root volumes, had %d GB", recommendedStorage, p.RootVolume.Size) 50 } 51 52 allErrs = append(allErrs, validateZones(p.RootVolume.Zones, ci.VolumeZones, fldPath.Child("rootVolume").Child("zones"))...) 53 if len(p.RootVolume.Zones) != 1 && len(p.RootVolume.Zones) != len(p.Zones) { 54 allErrs = append(allErrs, field.Invalid(fldPath.Child("rootVolume", "zones"), p.RootVolume.Zones, "there must be either just one volume availability zone common to all nodes or the number of compute and volume availability zones must be equal")) 55 } 56 57 } else { 58 // Not using root volume, so must check flavor 59 checkStorageFlavor = true 60 } 61 62 if controlPlane { 63 allErrs = append(allErrs, validateFlavor(p.FlavorName, ci, ctrlPlaneFlavorMinimums, fldPath.Child("type"), checkStorageFlavor)...) 64 } else { 65 allErrs = append(allErrs, validateFlavor(p.FlavorName, ci, computeFlavorMinimums, fldPath.Child("type"), checkStorageFlavor)...) 66 } 67 68 allErrs = append(allErrs, validateZones(p.Zones, ci.ComputeZones, fldPath.Child("zones"))...) 69 allErrs = append(allErrs, validateUUIDV4s(p.AdditionalNetworkIDs, fldPath.Child("additionalNetworkIDs"))...) 70 allErrs = append(allErrs, validateUUIDV4s(p.AdditionalSecurityGroupIDs, fldPath.Child("additionalSecurityGroupIDs"))...) 71 allErrs = append(allErrs, validateAdditionalNetworks(p.AdditionalNetworkIDs, ci.Networks, fldPath.Child("additionalNetworkIDs"))...) 72 allErrs = append(allErrs, validateAdditionalSecurityGroups(p.AdditionalSecurityGroupIDs, ci.SecurityGroups, fldPath.Child("additionalSecurityGroupIDs"))...) 73 74 return allErrs 75 } 76 77 func validateAdditionalNetworks(additionalNetworkIDs, availableNetworks []string, fldPath *field.Path) field.ErrorList { 78 allErrs := field.ErrorList{} 79 networkSet := make(map[string]struct{}, len(availableNetworks)) 80 for i := range availableNetworks { 81 networkSet[availableNetworks[i]] = struct{}{} 82 } 83 for i, n := range additionalNetworkIDs { 84 if _, ok := networkSet[n]; !ok { 85 allErrs = append(allErrs, field.Invalid(fldPath.Index(i), n, "Network either does not exist in this cloud, or is not available")) 86 } 87 } 88 return allErrs 89 } 90 91 func validateAdditionalSecurityGroups(additionalSecurityGroupIDs, availableSecurityGroups []string, fldPath *field.Path) field.ErrorList { 92 allErrs := field.ErrorList{} 93 sgSet := make(map[string]struct{}, len(availableSecurityGroups)) 94 for i := range availableSecurityGroups { 95 sgSet[availableSecurityGroups[i]] = struct{}{} 96 } 97 for i, n := range additionalSecurityGroupIDs { 98 if _, ok := sgSet[n]; !ok { 99 allErrs = append(allErrs, field.Invalid(fldPath.Index(i), n, "Security group either does not exist in this cloud, or is not available")) 100 } 101 } 102 return allErrs 103 } 104 105 func validateZones(input []string, available []string, fldPath *field.Path) field.ErrorList { 106 // check if machinepool default 107 if len(input) == 1 && input[0] == "" { 108 return field.ErrorList{} 109 } 110 111 allErrs := field.ErrorList{} 112 availableZones := sets.NewString(available...) 113 for idx, zone := range input { 114 if !availableZones.Has(zone) { 115 allErrs = append(allErrs, field.Invalid(fldPath.Child("zone").Index(idx), zone, "Zone either does not exist in this cloud, or is not available")) 116 } 117 } 118 119 return allErrs 120 } 121 122 func validateVolumeTypes(input []string, available []string, fldPath *field.Path) field.ErrorList { 123 allErrs := field.ErrorList{} 124 if len(input) == 0 { 125 return allErrs 126 } 127 volumeTypes := sets.New[string](available...) 128 for _, volumeType := range input { 129 if !volumeTypes.Has(volumeType) { 130 allErrs = append(allErrs, field.Invalid(fldPath, volumeType, "Volume type either does not exist in this cloud, or is not available")) 131 } 132 } 133 134 return allErrs 135 } 136 137 func validateUUIDV4s(input []string, fldPath *field.Path) field.ErrorList { 138 allErrs := field.ErrorList{} 139 for idx, uuid := range input { 140 if !ValidUUIDv4(uuid) { 141 allErrs = append(allErrs, field.Invalid(fldPath.Index(idx), uuid, "valid UUID v4 must be specified")) 142 } 143 } 144 145 return allErrs 146 } 147 148 // validUUIDv4 checks if string is in UUID v4 format 149 // For more information: https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random) 150 func ValidUUIDv4(s string) bool { 151 uuid, err := guuid.Parse(s) 152 if err != nil { 153 return false 154 } 155 156 // check that version of the uuid 157 if uuid.Version().String() != "VERSION_4" { 158 return false 159 } 160 161 return true 162 } 163 164 // validate flavor checks to make sure that a given flavor exists and meets the minimum requrement to run a cluster 165 // this function does not validate proper install config usage 166 func validateFlavor(flavorName string, ci *CloudInfo, req flavorRequirements, fldPath *field.Path, storage bool) field.ErrorList { 167 if flavorName == "" { 168 return field.ErrorList{field.Required(fldPath, "Flavor name must be provided")} 169 } 170 171 flavor, ok := ci.Flavors[flavorName] 172 if !ok { 173 return field.ErrorList{field.NotFound(fldPath, flavorName)} 174 } 175 176 // OpenStack administrators don't always fill in accurate metadata for 177 // baremetal flavors. Skipping validation. 178 if flavor.Baremetal { 179 return nil 180 } 181 182 errs := []string{} 183 if flavor.RAM < req.RAM { 184 errs = append(errs, fmt.Sprintf("Must have minimum of %d MB RAM, had %d MB", req.RAM, flavor.RAM)) 185 } 186 if flavor.VCPUs < req.VCPUs { 187 errs = append(errs, fmt.Sprintf("Must have minimum of %d VCPUs, had %d", req.VCPUs, flavor.VCPUs)) 188 } 189 if storage { 190 if flavor.Disk < req.Disk { 191 errs = append(errs, fmt.Sprintf("Must have minimum of %d GB Disk, had %d GB", req.Disk, flavor.Disk)) 192 } else if flavor.Disk < req.RecommendedDisk { 193 logrus.Warnf("Flavor does not meet the following recommended requirements: It is recommended to have %d GB Disk, had %d GB", req.RecommendedDisk, flavor.Disk) 194 } 195 } 196 197 if len(errs) == 0 { 198 return nil 199 } 200 201 errString := "Flavor did not meet the following minimum requirements: " 202 for i, err := range errs { 203 errString = errString + err 204 if i != len(errs)-1 { 205 errString = errString + "; " 206 } 207 } 208 209 return field.ErrorList{field.Invalid(fldPath, flavor.Name, errString)} 210 }