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

     1  package vsphere
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net"
     7  	"strconv"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/coreos/stream-metadata-go/stream"
    12  	"github.com/hashicorp/go-version"
    13  	"github.com/pkg/errors"
    14  	"github.com/sirupsen/logrus"
    15  	"github.com/vmware/govmomi/find"
    16  	vapitags "github.com/vmware/govmomi/vapi/tags"
    17  	"github.com/vmware/govmomi/vim25"
    18  	"github.com/vmware/govmomi/vim25/mo"
    19  	vim25types "github.com/vmware/govmomi/vim25/types"
    20  	"k8s.io/apimachinery/pkg/util/validation/field"
    21  	"k8s.io/apimachinery/pkg/util/wait"
    22  
    23  	"github.com/openshift/installer/pkg/rhcos"
    24  	"github.com/openshift/installer/pkg/types"
    25  	"github.com/openshift/installer/pkg/types/vsphere"
    26  	"github.com/openshift/installer/pkg/types/vsphere/validation"
    27  )
    28  
    29  //go:generate mockgen -source=./validation.go -destination=./mock/tagmanager_generated.go -package=mock
    30  
    31  // TagManager defines an interface to an implementation of the AuthorizationManager to facilitate mocking.
    32  type TagManager interface {
    33  	ListCategories(ctx context.Context) ([]string, error)
    34  	GetCategories(ctx context.Context) ([]vapitags.Category, error)
    35  	GetCategory(ctx context.Context, id string) (*vapitags.Category, error)
    36  	GetTagsForCategory(ctx context.Context, id string) ([]vapitags.Tag, error)
    37  	GetAttachedTags(ctx context.Context, ref mo.Reference) ([]vapitags.Tag, error)
    38  	GetAttachedTagsOnObjects(ctx context.Context, objectID []mo.Reference) ([]vapitags.AttachedTags, error)
    39  }
    40  
    41  const (
    42  	esxi7U2BuildNumber    int    = 17630552
    43  	vcenter7U2BuildNumber int    = 17694817
    44  	vcenter7U2Version     string = "7.0.2"
    45  )
    46  
    47  var localLogger = logrus.New()
    48  
    49  type validationContext struct {
    50  	AuthManager         AuthManager
    51  	Finder              Finder
    52  	Client              *vim25.Client
    53  	TagManager          TagManager
    54  	regionTagCategoryID string
    55  	zoneTagCategoryID   string
    56  	rhcosStream         *stream.Stream
    57  }
    58  
    59  // Validate executes platform-specific validation.
    60  func Validate(ic *types.InstallConfig) error {
    61  	if ic.Platform.VSphere == nil {
    62  		return errors.New(field.Required(field.NewPath("platform", "vsphere"), "vSphere validation requires a vSphere platform configuration").Error())
    63  	}
    64  	return validation.ValidatePlatform(ic.Platform.VSphere, false, field.NewPath("platform").Child("vsphere"), ic).ToAggregate()
    65  }
    66  
    67  func getVCenterClient(failureDomain vsphere.FailureDomain, ic *types.InstallConfig) (*validationContext, ClientLogout, error) {
    68  	server := failureDomain.Server
    69  	ctx := context.TODO()
    70  	for _, vcenter := range ic.VSphere.VCenters {
    71  		if vcenter.Server == server {
    72  			vim25Client, vim25RestClient, cleanup, err := CreateVSphereClients(ctx,
    73  				vcenter.Server,
    74  				vcenter.Username,
    75  				vcenter.Password)
    76  
    77  			if err != nil {
    78  				return nil, nil, err
    79  			}
    80  
    81  			validationCtx := validationContext{
    82  				TagManager:  vapitags.NewManager(vim25RestClient),
    83  				AuthManager: newAuthManager(vim25Client),
    84  				Finder:      find.NewFinder(vim25Client),
    85  				Client:      vim25Client,
    86  			}
    87  			return &validationCtx, cleanup, err
    88  		}
    89  	}
    90  	return nil, nil, fmt.Errorf("vcenter %s not defined in vcenters", server)
    91  }
    92  
    93  // ValidateForProvisioning performs platform validation specifically
    94  // for multi-zone installer-provisioned infrastructure. In this case,
    95  // self-hosted networking is a requirement when the installer creates
    96  // infrastructure for vSphere clusters.
    97  func ValidateForProvisioning(ic *types.InstallConfig) error {
    98  	allErrs := field.ErrorList{}
    99  
   100  	// If APIVIPs and IngressVIPs is equal to zero
   101  	// then don't validate the VIPs.
   102  	// Instead, ensure there is a configured
   103  	// DNS record for api and test if the load
   104  	// balancer is configured.
   105  
   106  	// The VIP parameters within the Infrastructure status object
   107  	// will be empty. This will cause MCO to not deploy
   108  	// the static pods: haproxy, keepalived and coredns.
   109  	// This will allow the use of an external load balancer
   110  	// and RHCOS nodes to be on multiple L2 segments.
   111  	if len(ic.Platform.VSphere.APIVIPs) == 0 && len(ic.Platform.VSphere.IngressVIPs) == 0 {
   112  		allErrs = append(allErrs, ensureDNS(ic, field.NewPath("platform"), nil)...)
   113  		ensureLoadBalancer(ic)
   114  	}
   115  
   116  	var clients = make(map[string]*validationContext, 0)
   117  
   118  	checkTags := false
   119  	if len(ic.VSphere.FailureDomains) > 1 {
   120  		checkTags = true
   121  	}
   122  
   123  	for i, failureDomain := range ic.VSphere.FailureDomains {
   124  		if _, exists := clients[failureDomain.Server]; !exists {
   125  			validationCtx, cleanup, err := getVCenterClient(failureDomain, ic)
   126  			if err != nil {
   127  				return err
   128  			}
   129  			defer cleanup()
   130  
   131  			err = getRhcosStream(validationCtx)
   132  			if err != nil {
   133  				return err
   134  			}
   135  
   136  			allErrs = append(allErrs, validateVCenterVersion(validationCtx, field.NewPath("platform").Child("vsphere").Child("vcenters"))...)
   137  			clients[failureDomain.Server] = validationCtx
   138  		}
   139  
   140  		validationCtx := clients[failureDomain.Server]
   141  		allErrs = append(allErrs, validateFailureDomain(validationCtx, &ic.VSphere.FailureDomains[i], checkTags)...)
   142  	}
   143  	return allErrs.ToAggregate()
   144  }
   145  
   146  func validateFailureDomain(validationCtx *validationContext, failureDomain *vsphere.FailureDomain, checkTags bool) field.ErrorList {
   147  	allErrs := field.ErrorList{}
   148  	checkDatacenterPrivileges := true
   149  	checkComputeClusterPrivileges := true
   150  
   151  	resourcePool := fmt.Sprintf("%s/Resources", failureDomain.Topology.ComputeCluster)
   152  	if len(failureDomain.Topology.ResourcePool) != 0 {
   153  		resourcePool = failureDomain.Topology.ResourcePool
   154  		checkComputeClusterPrivileges = false
   155  	}
   156  
   157  	vsphereField := field.NewPath("platform").Child("vsphere")
   158  	topologyField := vsphereField.Child("failureDomains").Child("topology")
   159  
   160  	if checkTags {
   161  		regionTagCategoryID, zoneTagCategoryID, err := validateTagCategories(validationCtx)
   162  		if err != nil {
   163  			allErrs = append(allErrs, field.InternalError(vsphereField, err))
   164  		}
   165  		validationCtx.regionTagCategoryID = regionTagCategoryID
   166  		validationCtx.zoneTagCategoryID = zoneTagCategoryID
   167  	}
   168  
   169  	allErrs = append(allErrs, resourcePoolExists(validationCtx, resourcePool, topologyField.Child("resourcePool"))...)
   170  
   171  	if len(failureDomain.Topology.Folder) > 0 {
   172  		allErrs = append(allErrs, folderExists(validationCtx, failureDomain.Topology.Folder, topologyField.Child("folder"))...)
   173  		checkDatacenterPrivileges = false
   174  	}
   175  
   176  	allErrs = append(allErrs, validateESXiVersion(validationCtx, failureDomain.Topology.ComputeCluster, vsphereField, topologyField.Child("computeCluster"))...)
   177  	allErrs = append(allErrs, validateVcenterPrivileges(validationCtx, topologyField.Child("server"))...)
   178  	allErrs = append(allErrs, computeClusterExists(validationCtx, failureDomain.Topology.ComputeCluster, topologyField.Child("computeCluster"), checkComputeClusterPrivileges, checkTags)...)
   179  	allErrs = append(allErrs, datacenterExists(validationCtx, failureDomain.Topology.Datacenter, topologyField.Child("datacenter"), checkDatacenterPrivileges)...)
   180  	allErrs = append(allErrs, datastoreExists(validationCtx, failureDomain.Topology.Datacenter, failureDomain.Topology.Datastore, topologyField.Child("datastore"))...)
   181  
   182  	if failureDomain.Topology.Template != "" {
   183  		allErrs = append(allErrs, validateTemplate(validationCtx, failureDomain.Topology.Template, topologyField.Child("template"))...)
   184  	}
   185  
   186  	for _, network := range failureDomain.Topology.Networks {
   187  		allErrs = append(allErrs, validateNetwork(validationCtx, failureDomain.Topology.Datacenter, failureDomain.Topology.ComputeCluster, network, topologyField)...)
   188  	}
   189  
   190  	return allErrs
   191  }
   192  
   193  func validateVCenterVersion(validationCtx *validationContext, fldPath *field.Path) field.ErrorList {
   194  	allErrs := field.ErrorList{}
   195  
   196  	constraints, err := version.NewConstraint(fmt.Sprintf("< %s", vcenter7U2Version))
   197  	if err != nil {
   198  		allErrs = append(allErrs, field.InternalError(fldPath, err))
   199  	}
   200  
   201  	vCenterVersion, err := version.NewVersion(validationCtx.Client.ServiceContent.About.Version)
   202  	if err != nil {
   203  		allErrs = append(allErrs, field.InternalError(fldPath, err))
   204  	}
   205  	build, err := strconv.Atoi(validationCtx.Client.ServiceContent.About.Build)
   206  	if err != nil {
   207  		allErrs = append(allErrs, field.InternalError(fldPath, err))
   208  	}
   209  
   210  	detail := fmt.Sprintf("The vSphere storage driver requires a minimum of vSphere 7 Update 2. Current vCenter version: %s, build: %s",
   211  		validationCtx.Client.ServiceContent.About.Version, validationCtx.Client.ServiceContent.About.Build)
   212  
   213  	if constraints.Check(vCenterVersion) {
   214  		allErrs = append(allErrs, field.Required(fldPath, detail))
   215  	} else if build < vcenter7U2BuildNumber {
   216  		allErrs = append(allErrs, field.Required(fldPath, detail))
   217  	}
   218  
   219  	return allErrs
   220  }
   221  
   222  func validateESXiVersion(validationCtx *validationContext, clusterPath string, vSphereFldPath, computeClusterFldPath *field.Path) field.ErrorList {
   223  	allErrs := field.ErrorList{}
   224  	finder := validationCtx.Finder
   225  
   226  	ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second)
   227  	defer cancel()
   228  
   229  	clusters, err := finder.ClusterComputeResourceList(ctx, clusterPath)
   230  
   231  	if err != nil {
   232  		var notFoundError *find.NotFoundError
   233  		var defaultNotFoundError *find.DefaultNotFoundError
   234  
   235  		/* These error types also exist, but it seems less likely to occur.
   236  		var *find.MultipleFoundError
   237  		var *find.DefaultMultipleFoundError
   238  		*/
   239  		switch {
   240  		case errors.As(err, &notFoundError):
   241  			return field.ErrorList{field.Invalid(computeClusterFldPath, clusterPath, notFoundError.Error())}
   242  		case errors.As(err, &defaultNotFoundError):
   243  			return field.ErrorList{field.Invalid(computeClusterFldPath, clusterPath, defaultNotFoundError.Error())}
   244  		default:
   245  			return append(allErrs, field.InternalError(vSphereFldPath, err))
   246  		}
   247  	}
   248  
   249  	v7, err := version.NewVersion("7.0")
   250  	if err != nil {
   251  		return append(allErrs, field.InternalError(vSphereFldPath, err))
   252  	}
   253  
   254  	hosts, err := clusters[0].Hosts(context.TODO())
   255  	if err != nil {
   256  		err = errors.Wrapf(err, "unable to find hosts from cluster on path: %s", clusterPath)
   257  		return append(allErrs, field.InternalError(vSphereFldPath, err))
   258  	}
   259  
   260  	for _, h := range hosts {
   261  		var esxiHostVersion *version.Version
   262  		var mh mo.HostSystem
   263  		err := h.Properties(context.TODO(), h.Reference(), []string{"config.product", "runtime"}, &mh)
   264  		if err != nil {
   265  			return append(allErrs, field.InternalError(vSphereFldPath, err))
   266  		}
   267  
   268  		if mh.Runtime.InMaintenanceMode || mh.Runtime.ConnectionState == vim25types.HostSystemConnectionStateDisconnected || mh.Runtime.ConnectionState == vim25types.HostSystemConnectionStateNotResponding {
   269  			continue
   270  		}
   271  
   272  		if mh.Config != nil {
   273  			esxiHostVersion, err = version.NewVersion(mh.Config.Product.Version)
   274  			if err != nil {
   275  				return append(allErrs, field.InternalError(vSphereFldPath, err))
   276  			}
   277  		} else {
   278  			return append(allErrs, field.InternalError(vSphereFldPath, errors.Errorf("vCenter is failing to retrieve config product version information for the ESXi host: %s", h.Name())))
   279  		}
   280  
   281  		detail := fmt.Sprintf("The vSphere storage driver requires a minimum of vSphere 7 Update 2. The ESXi host: %s is version: %s and build: %s",
   282  			h.Name(), mh.Config.Product.Version, mh.Config.Product.Build)
   283  
   284  		if esxiHostVersion.LessThan(v7) {
   285  			allErrs = append(allErrs, field.Required(computeClusterFldPath, detail))
   286  		} else {
   287  			build, err := strconv.Atoi(mh.Config.Product.Build)
   288  			if err != nil {
   289  				return append(allErrs, field.InternalError(vSphereFldPath, err))
   290  			}
   291  			if build < esxi7U2BuildNumber {
   292  				allErrs = append(allErrs, field.Required(computeClusterFldPath, detail))
   293  			}
   294  		}
   295  	}
   296  	return allErrs
   297  }
   298  
   299  func validateNetwork(validationCtx *validationContext, datacenterName string, clusterName string, networkName string, fldPath *field.Path) field.ErrorList {
   300  	finder := validationCtx.Finder
   301  	client := validationCtx.Client
   302  
   303  	// It's not possible to validate a networkName if datacenterName or clusterName are empty strings
   304  	if datacenterName == "" || clusterName == "" || networkName == "" {
   305  		return field.ErrorList{}
   306  	}
   307  	datacenterPath := datacenterName
   308  	ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second)
   309  	defer cancel()
   310  
   311  	if !strings.HasPrefix(datacenterName, "/") && !strings.HasPrefix(datacenterName, "./") {
   312  		datacenterPath = "./" + datacenterName
   313  	}
   314  
   315  	dataCenter, err := finder.Datacenter(ctx, datacenterPath)
   316  	if err != nil {
   317  		return field.ErrorList{field.Invalid(fldPath, datacenterName, err.Error())}
   318  	}
   319  	// Remove any trailing backslash before getting networkMoID
   320  	trimmedPath := strings.TrimPrefix(dataCenter.InventoryPath, "/")
   321  
   322  	network, err := GetNetworkMo(ctx, client, finder, trimmedPath, clusterName, networkName)
   323  	if err != nil {
   324  		return field.ErrorList{field.Invalid(fldPath, networkName, err.Error())}
   325  	}
   326  	permissionGroup := permissions[permissionPortgroup]
   327  	err = comparePrivileges(ctx, validationCtx, network.Reference(), permissionGroup)
   328  	if err != nil {
   329  		return field.ErrorList{field.InternalError(fldPath, err)}
   330  	}
   331  	return field.ErrorList{}
   332  }
   333  
   334  // resourcePoolExists returns an error if a resourcePool is specified in the vSphere platform but a resourcePool with that name is not found in the datacenter.
   335  func computeClusterExists(validationCtx *validationContext, computeCluster string, fldPath *field.Path, checkPrivileges, checkTagAttachment bool) field.ErrorList {
   336  	if computeCluster == "" {
   337  		return field.ErrorList{field.Required(fldPath, "must specify the cluster")}
   338  	}
   339  
   340  	ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second)
   341  	defer cancel()
   342  
   343  	computeClusterMo, err := validationCtx.Finder.ClusterComputeResource(ctx, computeCluster)
   344  	if err != nil {
   345  		return field.ErrorList{field.Invalid(fldPath, computeCluster, err.Error())}
   346  	}
   347  
   348  	if checkPrivileges {
   349  		permissionGroup := permissions[permissionCluster]
   350  		err = comparePrivileges(ctx, validationCtx, computeClusterMo.Reference(), permissionGroup)
   351  
   352  		if err != nil {
   353  			return field.ErrorList{field.InternalError(fldPath, err)}
   354  		}
   355  	}
   356  
   357  	if checkTagAttachment {
   358  		err = validateTagAttachment(validationCtx, computeClusterMo.Reference())
   359  		if err != nil {
   360  			return field.ErrorList{field.InternalError(fldPath, err)}
   361  		}
   362  	}
   363  
   364  	return field.ErrorList{}
   365  }
   366  
   367  // resourcePoolExists returns an error if a resourcePool is specified in the vSphere platform but a resourcePool with that name is not found in the datacenter.
   368  func resourcePoolExists(validationCtx *validationContext, resourcePool string, fldPath *field.Path) field.ErrorList {
   369  	finder := validationCtx.Finder
   370  
   371  	// If no resourcePool is specified, skip this check as the root resourcePool will be used.
   372  	if resourcePool == "" {
   373  		return field.ErrorList{}
   374  	}
   375  
   376  	ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second)
   377  	defer cancel()
   378  
   379  	resourcePoolMo, err := finder.ResourcePool(ctx, resourcePool)
   380  	if err != nil {
   381  		return field.ErrorList{field.Invalid(fldPath, resourcePool, err.Error())}
   382  	}
   383  	permissionGroup := permissions[permissionResourcePool]
   384  	err = comparePrivileges(ctx, validationCtx, resourcePoolMo.Reference(), permissionGroup)
   385  	if err != nil {
   386  		return field.ErrorList{field.InternalError(fldPath, err)}
   387  	}
   388  
   389  	return field.ErrorList{}
   390  }
   391  
   392  // datacenterExists returns an error if a datacenter is specified in the vSphere platform but a datacenter with that
   393  // name is not found in the datacenter or the user does not hold adequate privileges for the datacenter.
   394  func datacenterExists(validationCtx *validationContext, datacenterName string, fldPath *field.Path, checkPrivileges bool) field.ErrorList {
   395  	finder := validationCtx.Finder
   396  
   397  	ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second)
   398  	defer cancel()
   399  
   400  	dataCenter, err := finder.Datacenter(ctx, datacenterName)
   401  	if err != nil {
   402  		return field.ErrorList{field.Invalid(fldPath, datacenterName, err.Error())}
   403  	}
   404  	if checkPrivileges {
   405  		permissionGroup := permissions[permissionDatacenter]
   406  		err = comparePrivileges(ctx, validationCtx, dataCenter.Reference(), permissionGroup)
   407  		if err != nil {
   408  			return field.ErrorList{field.InternalError(fldPath, err)}
   409  		}
   410  	}
   411  	return field.ErrorList{}
   412  }
   413  
   414  // datastoreExists returns an error if a datastore is specified in the vSphere platform but a datastore with that
   415  // name is not found in the datacenter or the user does not hold adequate privileges for the datastore.
   416  func datastoreExists(validationCtx *validationContext, datacenterName string, datastoreName string, fldPath *field.Path) field.ErrorList {
   417  	finder := validationCtx.Finder
   418  
   419  	if datastoreName == "" {
   420  		return field.ErrorList{field.Required(fldPath, "must specify the datastore")}
   421  	}
   422  
   423  	ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second)
   424  	defer cancel()
   425  	dataCenter, err := finder.Datacenter(ctx, datacenterName)
   426  	if err != nil {
   427  		return field.ErrorList{field.Invalid(fldPath, datacenterName, errors.Wrapf(err, "unable to find datacenter %s", datacenterName).Error())}
   428  	}
   429  
   430  	datastorePath := fmt.Sprintf("%s/datastore/...", dataCenter.InventoryPath)
   431  	datastores, err := finder.DatastoreList(ctx, datastorePath)
   432  	if err != nil {
   433  		return field.ErrorList{field.Invalid(fldPath, datastoreName, err.Error())}
   434  	}
   435  
   436  	var datastoreMo *vim25types.ManagedObjectReference
   437  	for _, datastore := range datastores {
   438  		if datastore.InventoryPath == datastoreName || datastore.Name() == datastoreName {
   439  			mo := datastore.Reference()
   440  			datastoreMo = &mo
   441  		}
   442  	}
   443  
   444  	if datastoreMo == nil {
   445  		return field.ErrorList{field.Invalid(fldPath, datastoreName, fmt.Sprintf("could not find datastore %s", datastoreName))}
   446  	}
   447  	permissionGroup := permissions[permissionDatastore]
   448  	err = comparePrivileges(ctx, validationCtx, datastoreMo.Reference(), permissionGroup)
   449  
   450  	if err != nil {
   451  		return field.ErrorList{field.InternalError(fldPath, err)}
   452  	}
   453  	return field.ErrorList{}
   454  }
   455  
   456  // validateVcenterPrivileges verifies the privileges associated with
   457  func validateVcenterPrivileges(validationCtx *validationContext, fldPath *field.Path) field.ErrorList {
   458  	finder := validationCtx.Finder
   459  	ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second)
   460  	defer cancel()
   461  	rootFolder, err := finder.Folder(ctx, "/")
   462  	if err != nil {
   463  		return field.ErrorList{field.InternalError(fldPath, err)}
   464  	}
   465  	permissionGroup := permissions[permissionVcenter]
   466  	err = comparePrivileges(ctx, validationCtx, rootFolder.Reference(), permissionGroup)
   467  	if err != nil {
   468  		return field.ErrorList{field.InternalError(fldPath, err)}
   469  	}
   470  	return field.ErrorList{}
   471  }
   472  
   473  func ensureDNS(installConfig *types.InstallConfig, fldPath *field.Path, resolver *net.Resolver) field.ErrorList {
   474  	var uris []string
   475  	errList := field.ErrorList{}
   476  	ctx, cancel := context.WithTimeout(context.TODO(), 30*time.Second)
   477  	defer cancel()
   478  
   479  	uris = append(uris, fmt.Sprintf("api.%s", installConfig.ClusterDomain()))
   480  	uris = append(uris, fmt.Sprintf("api-int.%s", installConfig.ClusterDomain()))
   481  
   482  	if resolver == nil {
   483  		resolver = &net.Resolver{
   484  			PreferGo: true,
   485  		}
   486  	}
   487  
   488  	// DNS lookup uri
   489  	for _, u := range uris {
   490  		logrus.Debugf("Performing DNS Lookup: %s", u)
   491  		_, err := resolver.LookupHost(ctx, u)
   492  		// Append error if DNS entry does not exist
   493  		if err != nil {
   494  			errList = append(errList, field.Invalid(fldPath, u, err.Error()))
   495  		}
   496  	}
   497  
   498  	return errList
   499  }
   500  
   501  func ensureLoadBalancer(installConfig *types.InstallConfig) {
   502  	var lastErr error
   503  	dialTimeout := time.Second
   504  	tcpTimeout := time.Second * 10
   505  	errorCount := 0
   506  	apiURIPort := fmt.Sprintf("api.%s:%s", installConfig.ClusterDomain(), "6443")
   507  	tcpContext, cancel := context.WithTimeout(context.TODO(), tcpTimeout)
   508  	defer cancel()
   509  
   510  	// If the load balancer is configured properly even
   511  	// without members we should be available to make
   512  	// a connection to port 6443. Check for 10 seconds
   513  	// emit debug message every 2 failures. If unavailable
   514  	// after timeout emit warning only.
   515  	wait.Until(func() {
   516  		conn, err := net.DialTimeout("tcp", apiURIPort, dialTimeout)
   517  		if err == nil {
   518  			conn.Close()
   519  			cancel()
   520  		} else {
   521  			lastErr = err
   522  			if errorCount == 2 {
   523  				logrus.Debug("Still waiting for load balancer...")
   524  				errorCount = 0
   525  			} else {
   526  				errorCount++
   527  			}
   528  		}
   529  	}, 2*time.Second, tcpContext.Done())
   530  
   531  	err := tcpContext.Err()
   532  	if err != nil && !errors.Is(err, context.Canceled) {
   533  		if lastErr != nil {
   534  			localLogger.Warnf("Installation may fail, load balancer not available: %v", lastErr)
   535  		}
   536  	}
   537  }
   538  
   539  func validateTagCategories(validationCtx *validationContext) (string, string, error) {
   540  	if validationCtx.TagManager == nil {
   541  		return "", "", nil
   542  	}
   543  	ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second)
   544  	defer cancel()
   545  
   546  	categories, err := validationCtx.TagManager.GetCategories(ctx)
   547  	if err != nil {
   548  		return "", "", err
   549  	}
   550  
   551  	regionTagCategoryID := ""
   552  	zoneTagCategoryID := ""
   553  	for _, category := range categories {
   554  		switch category.Name {
   555  		case vsphere.TagCategoryRegion:
   556  			regionTagCategoryID = category.ID
   557  		case vsphere.TagCategoryZone:
   558  			zoneTagCategoryID = category.ID
   559  		}
   560  		if len(zoneTagCategoryID) > 0 && len(regionTagCategoryID) > 0 {
   561  			break
   562  		}
   563  	}
   564  	if len(zoneTagCategoryID) == 0 || len(regionTagCategoryID) == 0 {
   565  		return "", "", errors.New("tag categories openshift-zone and openshift-region must be created")
   566  	}
   567  	return regionTagCategoryID, zoneTagCategoryID, nil
   568  }
   569  
   570  func validateTagAttachment(validationCtx *validationContext, reference vim25types.ManagedObjectReference) error {
   571  	if validationCtx.TagManager == nil {
   572  		return nil
   573  	}
   574  	client := validationCtx.Client
   575  	tagManager := validationCtx.TagManager
   576  	regionTagCategoryID := validationCtx.regionTagCategoryID
   577  	zoneTagCategoryID := validationCtx.zoneTagCategoryID
   578  	ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second)
   579  	defer cancel()
   580  
   581  	referencesToCheck := []mo.Reference{reference}
   582  	ancestors, err := mo.Ancestors(ctx,
   583  		client.RoundTripper,
   584  		client.ServiceContent.PropertyCollector,
   585  		reference)
   586  	if err != nil {
   587  		return err
   588  	}
   589  	for _, ancestor := range ancestors {
   590  		referencesToCheck = append(referencesToCheck, ancestor.Reference())
   591  	}
   592  	attachedTags, err := tagManager.GetAttachedTagsOnObjects(ctx, referencesToCheck)
   593  	if err != nil {
   594  		return err
   595  	}
   596  	regionTagAttached := false
   597  	zoneTagAttached := false
   598  	for _, attachedTag := range attachedTags {
   599  		for _, tag := range attachedTag.Tags {
   600  			if !regionTagAttached {
   601  				if tag.CategoryID == regionTagCategoryID {
   602  					regionTagAttached = true
   603  				}
   604  			}
   605  			if !zoneTagAttached {
   606  				if tag.CategoryID == zoneTagCategoryID {
   607  					zoneTagAttached = true
   608  				}
   609  			}
   610  			if regionTagAttached && zoneTagAttached {
   611  				return nil
   612  			}
   613  		}
   614  	}
   615  	var errs []string
   616  	if !regionTagAttached {
   617  		errs = append(errs, fmt.Sprintf("tag associated with tag category %s not attached to this resource or ancestor", vsphere.TagCategoryRegion))
   618  	}
   619  	if !zoneTagAttached {
   620  		errs = append(errs, fmt.Sprintf("tag associated with tag category %s not attached to this resource or ancestor", vsphere.TagCategoryZone))
   621  	}
   622  	return errors.New(strings.Join(errs, ","))
   623  }
   624  
   625  func validateTemplate(validationCtx *validationContext, template string, fldPath *field.Path) field.ErrorList {
   626  	ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second)
   627  	defer cancel()
   628  	var vmMo mo.VirtualMachine
   629  	var arch stream.Arch
   630  	var platformArtifacts stream.PlatformArtifacts
   631  	var ok bool
   632  
   633  	// Not using GetArchitectures() here to make it easier to test
   634  	if arch, ok = validationCtx.rhcosStream.Architectures["x86_64"]; !ok {
   635  		return field.ErrorList{field.InternalError(fldPath, errors.New("unable to find vmware rhcos artifacts"))}
   636  	}
   637  
   638  	if platformArtifacts, ok = arch.Artifacts["vmware"]; !ok {
   639  		return field.ErrorList{field.InternalError(fldPath, errors.New("unable to find vmware rhcos artifacts"))}
   640  	}
   641  	rhcosReleaseVersion := platformArtifacts.Release
   642  
   643  	vm, err := validationCtx.Finder.VirtualMachine(ctx, template)
   644  
   645  	if err != nil {
   646  		return field.ErrorList{field.Invalid(fldPath, template, errors.Wrapf(err, "unable to find template %s", template).Error())}
   647  	}
   648  	err = vm.Properties(ctx, vm.Reference(), nil, &vmMo)
   649  	if err != nil {
   650  		return field.ErrorList{field.InternalError(fldPath, err)}
   651  	}
   652  
   653  	if vmMo.Summary.Config.Product != nil {
   654  		templateProductVersion := vmMo.Summary.Config.Product.Version
   655  		if templateProductVersion == "" {
   656  			localLogger.Warnf("unable to determine RHCOS version of virtual machine: %s, installation may fail.", template)
   657  			return nil
   658  		}
   659  
   660  		err := compareCurrentToTemplate(templateProductVersion, rhcosReleaseVersion)
   661  		if err != nil {
   662  			return field.ErrorList{field.InternalError(fldPath, fmt.Errorf("current template: %s %w", template, err))}
   663  		}
   664  	} else {
   665  		localLogger.Warnf("unable to determine RHCOS version of virtual machine: %s, installation may fail.", template)
   666  	}
   667  
   668  	return nil
   669  }
   670  
   671  func compareCurrentToTemplate(templateProductVersion, rhcosStreamVersion string) error {
   672  	if templateProductVersion != rhcosStreamVersion {
   673  		templateVersion, err := strconv.Atoi(strings.Split(templateProductVersion, ".")[0])
   674  		if err != nil {
   675  			return err
   676  		}
   677  		currentRhcosVersion, err := strconv.Atoi(strings.Split(rhcosStreamVersion, ".")[0])
   678  		if err != nil {
   679  			return err
   680  		}
   681  
   682  		switch versionDiff := currentRhcosVersion - templateVersion; {
   683  		case versionDiff < 0:
   684  			return fmt.Errorf("rhcos version: %s is too many revisions ahead current version: %s", templateProductVersion, rhcosStreamVersion)
   685  		case versionDiff >= 2:
   686  			return fmt.Errorf("rhcos version: %s is too many revisions behind current version: %s", templateProductVersion, rhcosStreamVersion)
   687  		case versionDiff == 1:
   688  			localLogger.Warnf("rhcos version: %s is behind current version: %s, installation may fail", templateProductVersion, rhcosStreamVersion)
   689  		}
   690  	}
   691  	return nil
   692  }
   693  
   694  func getRhcosStream(validationCtx *validationContext) error {
   695  	var err error
   696  	ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second)
   697  	defer cancel()
   698  
   699  	validationCtx.rhcosStream, err = rhcos.FetchCoreOSBuild(ctx)
   700  
   701  	if err != nil {
   702  		return err
   703  	}
   704  	return nil
   705  }