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  }