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, ¬FoundError): 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 }