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

     1  package validation
     2  
     3  import (
     4  	"testing"
     5  
     6  	"github.com/gophercloud/gophercloud/v2/openstack/compute/v2/flavors"
     7  	logrusTest "github.com/sirupsen/logrus/hooks/test"
     8  	"github.com/stretchr/testify/assert"
     9  	"k8s.io/apimachinery/pkg/util/validation/field"
    10  
    11  	"github.com/openshift/installer/pkg/types/openstack"
    12  )
    13  
    14  const (
    15  	validZone   = "valid-zone"
    16  	invalidZone = "invalid-zone"
    17  
    18  	validCtrlPlaneFlavor = "valid-control-plane-flavor"
    19  	validComputeFlavor   = "valid-compute-flavor"
    20  
    21  	notExistFlavor = "non-existant-flavor"
    22  
    23  	invalidComputeFlavor   = "invalid-compute-flavor"
    24  	invalidCtrlPlaneFlavor = "invalid-control-plane-flavor"
    25  	warningComputeFlavor   = "warning-compute-flavor"
    26  	warningCtrlPlaneFlavor = "warning-control-plane-flavor"
    27  
    28  	baremetalFlavor = "baremetal-flavor"
    29  
    30  	invalidType      = "invalid-type"
    31  	volumeSmallSize  = 10
    32  	volumeMediumSize = 40
    33  	volumeLargeSize  = 100
    34  )
    35  
    36  var volumeTypes = []string{"performance", "standard"}
    37  var invalidVolumeTypes = []string{"performance", "invalid-type"}
    38  var volumeType = volumeTypes[0]
    39  
    40  func validMachinePool() *openstack.MachinePool {
    41  	return &openstack.MachinePool{
    42  		FlavorName: validCtrlPlaneFlavor,
    43  		Zones:      []string{""},
    44  	}
    45  }
    46  
    47  func invalidMachinePoolSmallVolume() *openstack.MachinePool {
    48  	return &openstack.MachinePool{
    49  		FlavorName: validCtrlPlaneFlavor,
    50  		Zones:      []string{""},
    51  		RootVolume: &openstack.RootVolume{
    52  			Size:  volumeSmallSize,
    53  			Types: volumeTypes,
    54  			Zones: []string{""},
    55  		},
    56  	}
    57  }
    58  
    59  func warningMachinePoolMediumVolume() *openstack.MachinePool {
    60  	return &openstack.MachinePool{
    61  		FlavorName: validCtrlPlaneFlavor,
    62  		Zones:      []string{""},
    63  		RootVolume: &openstack.RootVolume{
    64  			Size:  volumeMediumSize,
    65  			Types: volumeTypes,
    66  			Zones: []string{""},
    67  		},
    68  	}
    69  }
    70  
    71  func validMachinePoolLargeVolume() *openstack.MachinePool {
    72  	return &openstack.MachinePool{
    73  		FlavorName: validCtrlPlaneFlavor,
    74  		Zones:      []string{""},
    75  		RootVolume: &openstack.RootVolume{
    76  			Size:  volumeLargeSize,
    77  			Types: volumeTypes,
    78  			Zones: []string{validZone},
    79  		},
    80  	}
    81  }
    82  
    83  func validMpoolCloudInfo() *CloudInfo {
    84  	return &CloudInfo{
    85  		Flavors: map[string]Flavor{
    86  			validCtrlPlaneFlavor: {
    87  				Flavor: flavors.Flavor{
    88  					Name:  validCtrlPlaneFlavor,
    89  					RAM:   16384,
    90  					Disk:  100,
    91  					VCPUs: 4,
    92  				},
    93  			},
    94  			validComputeFlavor: {
    95  				Flavor: flavors.Flavor{
    96  					Name:  validComputeFlavor,
    97  					RAM:   8192,
    98  					Disk:  100,
    99  					VCPUs: 2,
   100  				},
   101  			},
   102  			invalidCtrlPlaneFlavor: {
   103  				Flavor: flavors.Flavor{
   104  					Name:  invalidCtrlPlaneFlavor,
   105  					RAM:   8192, // too low
   106  					Disk:  100,
   107  					VCPUs: 2, // too low
   108  				},
   109  			},
   110  			invalidComputeFlavor: {
   111  				Flavor: flavors.Flavor{
   112  					Name:  invalidComputeFlavor,
   113  					RAM:   8192,
   114  					Disk:  10, // too low
   115  					VCPUs: 2,
   116  				},
   117  			},
   118  			warningCtrlPlaneFlavor: {
   119  				Flavor: flavors.Flavor{
   120  					Name:  warningCtrlPlaneFlavor,
   121  					RAM:   16384,
   122  					Disk:  40, // not recommended
   123  					VCPUs: 4,
   124  				},
   125  			},
   126  			warningComputeFlavor: {
   127  				Flavor: flavors.Flavor{
   128  					Name:  invalidComputeFlavor,
   129  					RAM:   8192,
   130  					Disk:  40, // not recommended
   131  					VCPUs: 2,
   132  				},
   133  			},
   134  			baremetalFlavor: {
   135  				Flavor: flavors.Flavor{
   136  					Name:  baremetalFlavor,
   137  					RAM:   8192, // too low
   138  					Disk:  10,   // too low
   139  					VCPUs: 2,    // too low
   140  				},
   141  				Baremetal: true,
   142  			},
   143  		},
   144  		ComputeZones: []string{
   145  			validZone,
   146  		},
   147  		VolumeZones: []string{
   148  			validZone,
   149  		},
   150  		VolumeTypes: volumeTypes,
   151  	}
   152  }
   153  
   154  func TestOpenStackMachinepoolValidation(t *testing.T) {
   155  	cases := []struct {
   156  		name            string
   157  		controlPlane    bool // only matters for flavor
   158  		mpool           *openstack.MachinePool
   159  		cloudInfo       *CloudInfo
   160  		expectedError   bool
   161  		expectedErrMsg  string // NOTE: this is a REGEXP
   162  		expectedWarnMsg string //NOTE: this is a REGEXP
   163  	}{
   164  		{
   165  			name:           "valid control plane",
   166  			controlPlane:   true,
   167  			mpool:          validMachinePool(),
   168  			cloudInfo:      validMpoolCloudInfo(),
   169  			expectedError:  false,
   170  			expectedErrMsg: "",
   171  		},
   172  		{
   173  			name: "valid zone",
   174  			mpool: func() *openstack.MachinePool {
   175  				mp := validMachinePool()
   176  				mp.Zones = []string{validZone}
   177  				return mp
   178  			}(),
   179  			cloudInfo:      validMpoolCloudInfo(),
   180  			expectedError:  false,
   181  			expectedErrMsg: "",
   182  		},
   183  		{
   184  			name: "invalid zone",
   185  			mpool: func() *openstack.MachinePool {
   186  				mp := validMachinePool()
   187  				mp.Zones = []string{"invalid-zone"}
   188  				return mp
   189  			}(),
   190  			cloudInfo:      validMpoolCloudInfo(),
   191  			expectedError:  true,
   192  			expectedErrMsg: "Zone either does not exist in this cloud, or is not available",
   193  		},
   194  		{
   195  			name:           "valid compute",
   196  			controlPlane:   false,
   197  			mpool:          validMachinePool(),
   198  			cloudInfo:      validMpoolCloudInfo(),
   199  			expectedError:  false,
   200  			expectedErrMsg: "",
   201  		},
   202  		{
   203  			name:         "not found control plane flavorName",
   204  			controlPlane: true,
   205  			mpool: func() *openstack.MachinePool {
   206  				mp := validMachinePool()
   207  				mp.FlavorName = notExistFlavor
   208  				return mp
   209  			}(),
   210  			cloudInfo: func() *CloudInfo {
   211  				ci := validMpoolCloudInfo()
   212  				return ci
   213  			}(),
   214  			expectedError:  true,
   215  			expectedErrMsg: "controlPlane.platform.openstack.type: Not found: \"non-existant-flavor\"",
   216  		},
   217  		{
   218  			name: "not found compute flavorName",
   219  			mpool: func() *openstack.MachinePool {
   220  				mp := validMachinePool()
   221  				mp.FlavorName = notExistFlavor
   222  				return mp
   223  			}(),
   224  			cloudInfo: func() *CloudInfo {
   225  				ci := validMpoolCloudInfo()
   226  				return ci
   227  			}(),
   228  			expectedError:  true,
   229  			expectedErrMsg: `compute\[0\].platform.openstack.type: Not found: "non-existant-flavor"`,
   230  		},
   231  		{
   232  			name: "no flavor name",
   233  			mpool: func() *openstack.MachinePool {
   234  				mp := validMachinePool()
   235  				mp.FlavorName = ""
   236  				return mp
   237  			}(),
   238  			cloudInfo: func() *CloudInfo {
   239  				ci := validMpoolCloudInfo()
   240  				return ci
   241  			}(),
   242  			expectedError:  true,
   243  			expectedErrMsg: `compute\[0\].platform.openstack.type: Required value: Flavor name must be provided`,
   244  		},
   245  		{
   246  			name:         "invalid control plane flavorName",
   247  			controlPlane: true,
   248  			mpool: func() *openstack.MachinePool {
   249  				mp := validMachinePool()
   250  				mp.FlavorName = invalidCtrlPlaneFlavor
   251  				return mp
   252  			}(),
   253  			cloudInfo:      validMpoolCloudInfo(),
   254  			expectedError:  true,
   255  			expectedErrMsg: "controlPlane.platform.openstack.type: Invalid value: \"invalid-control-plane-flavor\": Flavor did not meet the following minimum requirements: Must have minimum of 16384 MB RAM, had 8192 MB; Must have minimum of 4 VCPUs, had 2",
   256  		},
   257  		{
   258  			name:         "invalid compute flavorName",
   259  			controlPlane: false,
   260  			mpool: func() *openstack.MachinePool {
   261  				mp := validMachinePool()
   262  				mp.FlavorName = invalidComputeFlavor
   263  				return mp
   264  			}(),
   265  			cloudInfo:      validMpoolCloudInfo(),
   266  			expectedError:  true,
   267  			expectedErrMsg: `compute\[0\].platform.openstack.type: Invalid value: "invalid-compute-flavor": Flavor did not meet the following minimum requirements: Must have minimum of 25 GB Disk, had 10 GB`,
   268  		},
   269  		{
   270  			name:         "warning control plane flavorName",
   271  			controlPlane: true,
   272  			mpool: func() *openstack.MachinePool {
   273  				mp := validMachinePool()
   274  				mp.FlavorName = warningCtrlPlaneFlavor
   275  				return mp
   276  			}(),
   277  			cloudInfo:       validMpoolCloudInfo(),
   278  			expectedWarnMsg: `Flavor does not meet the following recommended requirements: It is recommended to have 100 GB Disk, had 40 GB`,
   279  		},
   280  		{
   281  			name:         "warning compute flavorName",
   282  			controlPlane: false,
   283  			mpool: func() *openstack.MachinePool {
   284  				mp := validMachinePool()
   285  				mp.FlavorName = warningComputeFlavor
   286  				return mp
   287  			}(),
   288  			cloudInfo:       validMpoolCloudInfo(),
   289  			expectedWarnMsg: `Flavor does not meet the following recommended requirements: It is recommended to have 100 GB Disk, had 40 GB`,
   290  		},
   291  		{
   292  			name:         "valid baremetal compute",
   293  			controlPlane: false,
   294  			mpool: func() *openstack.MachinePool {
   295  				mp := validMachinePool()
   296  				mp.FlavorName = baremetalFlavor
   297  				return mp
   298  			}(),
   299  			cloudInfo:      validMpoolCloudInfo(),
   300  			expectedError:  false,
   301  			expectedErrMsg: "",
   302  		},
   303  		{
   304  			name:         "volume too small",
   305  			controlPlane: false,
   306  			mpool: func() *openstack.MachinePool {
   307  				mp := invalidMachinePoolSmallVolume()
   308  				mp.FlavorName = invalidCtrlPlaneFlavor
   309  				return mp
   310  			}(),
   311  			cloudInfo:      validMpoolCloudInfo(),
   312  			expectedError:  true,
   313  			expectedErrMsg: "Volume size must be greater than 25 GB to use root volumes, had 10 GB",
   314  		},
   315  		{
   316  			name:         "volume not recommended",
   317  			controlPlane: false,
   318  			mpool: func() *openstack.MachinePool {
   319  				mp := warningMachinePoolMediumVolume()
   320  				mp.FlavorName = invalidCtrlPlaneFlavor
   321  				return mp
   322  			}(),
   323  			cloudInfo:       validMpoolCloudInfo(),
   324  			expectedWarnMsg: "Volume size is recommended to be greater than 100 GB to use root volumes, had 40 GB",
   325  		},
   326  		{
   327  			name:         "volume big enough",
   328  			controlPlane: false,
   329  			mpool: func() *openstack.MachinePool {
   330  				mp := validMachinePoolLargeVolume()
   331  				mp.FlavorName = invalidCtrlPlaneFlavor
   332  				return mp
   333  			}(),
   334  			cloudInfo:      validMpoolCloudInfo(),
   335  			expectedError:  false,
   336  			expectedErrMsg: "",
   337  		},
   338  		{
   339  			name:         "valid root volume az",
   340  			controlPlane: false,
   341  			mpool: func() *openstack.MachinePool {
   342  				mp := validMachinePoolLargeVolume()
   343  				return mp
   344  			}(),
   345  			cloudInfo:      validMpoolCloudInfo(),
   346  			expectedError:  false,
   347  			expectedErrMsg: "",
   348  		},
   349  		{
   350  			name:         "invalid root volume az",
   351  			controlPlane: false,
   352  			mpool: func() *openstack.MachinePool {
   353  				mp := validMachinePoolLargeVolume()
   354  				mp.RootVolume.Zones = []string{invalidZone}
   355  				return mp
   356  			}(),
   357  			cloudInfo:      validMpoolCloudInfo(),
   358  			expectedError:  true,
   359  			expectedErrMsg: `compute\[0\].platform.openstack.rootVolume.zones.zone\[0\]: Invalid value: \"invalid-zone\": Zone either does not exist in this cloud, or is not available`,
   360  		},
   361  		{
   362  			name:         "volume and compute zones number mismatch",
   363  			controlPlane: false,
   364  			mpool: func() *openstack.MachinePool {
   365  				mp := validMachinePoolLargeVolume()
   366  				mp.RootVolume.Zones = []string{"AZ1", "AZ2"}
   367  				return mp
   368  			}(),
   369  			cloudInfo:      validMpoolCloudInfo(),
   370  			expectedError:  true,
   371  			expectedErrMsg: `compute\[0\].platform.openstack.rootVolume.zones: Invalid value: \[\]string{"AZ1", "AZ2"}: 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`,
   372  		},
   373  		{
   374  			name:         "invalid volume types",
   375  			controlPlane: true,
   376  			mpool: func() *openstack.MachinePool {
   377  				mp := validMachinePoolLargeVolume()
   378  				mp.RootVolume.Types = invalidVolumeTypes
   379  				return mp
   380  			}(),
   381  			cloudInfo:      validMpoolCloudInfo(),
   382  			expectedError:  true,
   383  			expectedErrMsg: "controlPlane.platform.openstack.rootVolume.types: Invalid value: \"invalid-type\": Volume type either does not exist in this cloud, or is not available",
   384  		},
   385  		{
   386  			name:         "valid volume type",
   387  			controlPlane: true,
   388  			mpool: func() *openstack.MachinePool {
   389  				mp := validMachinePoolLargeVolume()
   390  				mp.RootVolume.DeprecatedType = volumeType
   391  				mp.RootVolume.Types = []string{}
   392  				return mp
   393  			}(),
   394  			cloudInfo:      validMpoolCloudInfo(),
   395  			expectedError:  false,
   396  			expectedErrMsg: "",
   397  		},
   398  		{
   399  			name:         "valid volume types",
   400  			controlPlane: true,
   401  			mpool: func() *openstack.MachinePool {
   402  				mp := validMachinePoolLargeVolume()
   403  				mp.RootVolume.Types = volumeTypes
   404  				return mp
   405  			}(),
   406  			cloudInfo:      validMpoolCloudInfo(),
   407  			expectedError:  false,
   408  			expectedErrMsg: "",
   409  		},
   410  	}
   411  
   412  	for _, tc := range cases {
   413  		t.Run(tc.name, func(t *testing.T) {
   414  			var fieldPath *field.Path
   415  			if tc.controlPlane {
   416  				fieldPath = field.NewPath("controlPlane", "platform", "openstack")
   417  			} else {
   418  				fieldPath = field.NewPath("compute").Index(0).Child("platform", "openstack")
   419  			}
   420  
   421  			hook := logrusTest.NewGlobal()
   422  			aggregatedErrors := ValidateMachinePool(tc.mpool, tc.cloudInfo, tc.controlPlane, fieldPath).ToAggregate()
   423  			if tc.expectedError {
   424  				assert.Regexp(t, tc.expectedErrMsg, aggregatedErrors)
   425  			} else {
   426  				assert.NoError(t, aggregatedErrors)
   427  			}
   428  			if len(tc.expectedWarnMsg) > 0 {
   429  				assert.Regexp(t, tc.expectedWarnMsg, hook.LastEntry().Message)
   430  			}
   431  		})
   432  	}
   433  }