github.com/vmware/govmomi@v0.43.0/simulator/cluster_compute_resource.go (about) 1 /* 2 Copyright (c) 2017-2023 VMware, Inc. All Rights Reserved. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package simulator 18 19 import ( 20 "log" 21 "math/rand" 22 "sync/atomic" 23 "time" 24 25 "github.com/google/uuid" 26 27 "github.com/vmware/govmomi/object" 28 "github.com/vmware/govmomi/simulator/esx" 29 "github.com/vmware/govmomi/vim25/methods" 30 "github.com/vmware/govmomi/vim25/mo" 31 "github.com/vmware/govmomi/vim25/soap" 32 "github.com/vmware/govmomi/vim25/types" 33 ) 34 35 type ClusterComputeResource struct { 36 mo.ClusterComputeResource 37 38 ruleKey int32 39 } 40 41 func (c *ClusterComputeResource) RenameTask(ctx *Context, req *types.Rename_Task) soap.HasFault { 42 return RenameTask(ctx, c, req) 43 } 44 45 type addHost struct { 46 *ClusterComputeResource 47 48 req *types.AddHost_Task 49 } 50 51 func (add *addHost) Run(task *Task) (types.AnyType, types.BaseMethodFault) { 52 spec := add.req.Spec 53 54 if spec.HostName == "" { 55 return nil, &types.NoHost{} 56 } 57 58 cr := add.ClusterComputeResource 59 template := esx.HostSystem 60 61 if h := task.ctx.Map.FindByName(spec.UserName, cr.Host); h != nil { 62 // "clone" an existing host from the inventory 63 template = h.(*HostSystem).HostSystem 64 template.Vm = nil 65 } else { 66 template.Network = cr.Network[:1] // VM Network 67 } 68 69 host := NewHostSystem(template) 70 host.configure(task.ctx, spec, add.req.AsConnected) 71 72 task.ctx.Map.PutEntity(cr, task.ctx.Map.NewEntity(host)) 73 host.Summary.Host = &host.Self 74 host.Config.Host = host.Self 75 76 task.ctx.Map.WithLock(task.ctx, *cr.EnvironmentBrowser, func() { 77 eb := task.ctx.Map.Get(*cr.EnvironmentBrowser).(*EnvironmentBrowser) 78 eb.addHost(task.ctx, host.Self) 79 }) 80 81 cr.Host = append(cr.Host, host.Reference()) 82 addComputeResource(cr.Summary.GetComputeResourceSummary(), host) 83 84 return host.Reference(), nil 85 } 86 87 func (c *ClusterComputeResource) AddHostTask(ctx *Context, add *types.AddHost_Task) soap.HasFault { 88 return &methods.AddHost_TaskBody{ 89 Res: &types.AddHost_TaskResponse{ 90 Returnval: NewTask(&addHost{c, add}).Run(ctx), 91 }, 92 } 93 } 94 95 func (c *ClusterComputeResource) update(cfg *types.ClusterConfigInfoEx, cspec *types.ClusterConfigSpecEx) types.BaseMethodFault { 96 if cspec.DasConfig != nil { 97 if val := cspec.DasConfig.Enabled; val != nil { 98 cfg.DasConfig.Enabled = val 99 } 100 if val := cspec.DasConfig.AdmissionControlEnabled; val != nil { 101 cfg.DasConfig.AdmissionControlEnabled = val 102 } 103 } 104 if cspec.DrsConfig != nil { 105 if val := cspec.DrsConfig.Enabled; val != nil { 106 cfg.DrsConfig.Enabled = val 107 } 108 if val := cspec.DrsConfig.DefaultVmBehavior; val != "" { 109 cfg.DrsConfig.DefaultVmBehavior = val 110 } 111 } 112 113 return nil 114 } 115 116 func (c *ClusterComputeResource) updateRules(cfg *types.ClusterConfigInfoEx, cspec *types.ClusterConfigSpecEx) types.BaseMethodFault { 117 for _, spec := range cspec.RulesSpec { 118 var i int 119 exists := false 120 121 match := func(info types.BaseClusterRuleInfo) bool { 122 return info.GetClusterRuleInfo().Name == spec.Info.GetClusterRuleInfo().Name 123 } 124 125 if spec.Operation == types.ArrayUpdateOperationRemove { 126 match = func(rule types.BaseClusterRuleInfo) bool { 127 return rule.GetClusterRuleInfo().Key == spec.ArrayUpdateSpec.RemoveKey.(int32) 128 } 129 } 130 131 for i = range cfg.Rule { 132 if match(cfg.Rule[i].GetClusterRuleInfo()) { 133 exists = true 134 break 135 } 136 } 137 138 switch spec.Operation { 139 case types.ArrayUpdateOperationAdd: 140 if exists { 141 return new(types.InvalidArgument) 142 } 143 info := spec.Info.GetClusterRuleInfo() 144 info.Key = atomic.AddInt32(&c.ruleKey, 1) 145 info.RuleUuid = uuid.New().String() 146 cfg.Rule = append(cfg.Rule, spec.Info) 147 case types.ArrayUpdateOperationEdit: 148 if !exists { 149 return new(types.InvalidArgument) 150 } 151 cfg.Rule[i] = spec.Info 152 case types.ArrayUpdateOperationRemove: 153 if !exists { 154 return new(types.InvalidArgument) 155 } 156 cfg.Rule = append(cfg.Rule[:i], cfg.Rule[i+1:]...) 157 } 158 } 159 160 return nil 161 } 162 163 func (c *ClusterComputeResource) updateGroups(cfg *types.ClusterConfigInfoEx, cspec *types.ClusterConfigSpecEx) types.BaseMethodFault { 164 for _, spec := range cspec.GroupSpec { 165 var i int 166 exists := false 167 168 match := func(info types.BaseClusterGroupInfo) bool { 169 return info.GetClusterGroupInfo().Name == spec.Info.GetClusterGroupInfo().Name 170 } 171 172 if spec.Operation == types.ArrayUpdateOperationRemove { 173 match = func(info types.BaseClusterGroupInfo) bool { 174 return info.GetClusterGroupInfo().Name == spec.ArrayUpdateSpec.RemoveKey.(string) 175 } 176 } 177 178 for i = range cfg.Group { 179 if match(cfg.Group[i].GetClusterGroupInfo()) { 180 exists = true 181 break 182 } 183 } 184 185 switch spec.Operation { 186 case types.ArrayUpdateOperationAdd: 187 if exists { 188 return new(types.InvalidArgument) 189 } 190 cfg.Group = append(cfg.Group, spec.Info) 191 case types.ArrayUpdateOperationEdit: 192 if !exists { 193 return new(types.InvalidArgument) 194 } 195 cfg.Group[i] = spec.Info 196 case types.ArrayUpdateOperationRemove: 197 if !exists { 198 return new(types.InvalidArgument) 199 } 200 cfg.Group = append(cfg.Group[:i], cfg.Group[i+1:]...) 201 } 202 } 203 204 return nil 205 } 206 207 func (c *ClusterComputeResource) updateOverridesDAS(cfg *types.ClusterConfigInfoEx, cspec *types.ClusterConfigSpecEx) types.BaseMethodFault { 208 for _, spec := range cspec.DasVmConfigSpec { 209 var i int 210 var key types.ManagedObjectReference 211 exists := false 212 213 if spec.Operation == types.ArrayUpdateOperationRemove { 214 key = spec.RemoveKey.(types.ManagedObjectReference) 215 } else { 216 key = spec.Info.Key 217 } 218 219 for i = range cfg.DasVmConfig { 220 if cfg.DasVmConfig[i].Key == key { 221 exists = true 222 break 223 } 224 } 225 226 switch spec.Operation { 227 case types.ArrayUpdateOperationAdd: 228 if exists { 229 return new(types.InvalidArgument) 230 } 231 cfg.DasVmConfig = append(cfg.DasVmConfig, *spec.Info) 232 case types.ArrayUpdateOperationEdit: 233 if !exists { 234 return new(types.InvalidArgument) 235 } 236 src := spec.Info.DasSettings 237 if src == nil { 238 return new(types.InvalidArgument) 239 } 240 dst := cfg.DasVmConfig[i].DasSettings 241 if src.RestartPriority != "" { 242 dst.RestartPriority = src.RestartPriority 243 } 244 if src.RestartPriorityTimeout != 0 { 245 dst.RestartPriorityTimeout = src.RestartPriorityTimeout 246 } 247 case types.ArrayUpdateOperationRemove: 248 if !exists { 249 return new(types.InvalidArgument) 250 } 251 cfg.DasVmConfig = append(cfg.DasVmConfig[:i], cfg.DasVmConfig[i+1:]...) 252 } 253 } 254 255 return nil 256 } 257 258 func (c *ClusterComputeResource) updateOverridesDRS(cfg *types.ClusterConfigInfoEx, cspec *types.ClusterConfigSpecEx) types.BaseMethodFault { 259 for _, spec := range cspec.DrsVmConfigSpec { 260 var i int 261 var key types.ManagedObjectReference 262 exists := false 263 264 if spec.Operation == types.ArrayUpdateOperationRemove { 265 key = spec.RemoveKey.(types.ManagedObjectReference) 266 } else { 267 key = spec.Info.Key 268 } 269 270 for i = range cfg.DrsVmConfig { 271 if cfg.DrsVmConfig[i].Key == key { 272 exists = true 273 break 274 } 275 } 276 277 switch spec.Operation { 278 case types.ArrayUpdateOperationAdd: 279 if exists { 280 return new(types.InvalidArgument) 281 } 282 cfg.DrsVmConfig = append(cfg.DrsVmConfig, *spec.Info) 283 case types.ArrayUpdateOperationEdit: 284 if !exists { 285 return new(types.InvalidArgument) 286 } 287 if spec.Info.Enabled != nil { 288 cfg.DrsVmConfig[i].Enabled = spec.Info.Enabled 289 } 290 if spec.Info.Behavior != "" { 291 cfg.DrsVmConfig[i].Behavior = spec.Info.Behavior 292 } 293 case types.ArrayUpdateOperationRemove: 294 if !exists { 295 return new(types.InvalidArgument) 296 } 297 cfg.DrsVmConfig = append(cfg.DrsVmConfig[:i], cfg.DrsVmConfig[i+1:]...) 298 } 299 } 300 301 return nil 302 } 303 304 func (c *ClusterComputeResource) updateOverridesVmOrchestration(cfg *types.ClusterConfigInfoEx, cspec *types.ClusterConfigSpecEx) types.BaseMethodFault { 305 for _, spec := range cspec.VmOrchestrationSpec { 306 var i int 307 var key types.ManagedObjectReference 308 exists := false 309 310 if spec.Operation == types.ArrayUpdateOperationRemove { 311 key = spec.RemoveKey.(types.ManagedObjectReference) 312 } else { 313 key = spec.Info.Vm 314 } 315 316 for i = range cfg.VmOrchestration { 317 if cfg.VmOrchestration[i].Vm == key { 318 exists = true 319 break 320 } 321 } 322 323 switch spec.Operation { 324 case types.ArrayUpdateOperationAdd: 325 if exists { 326 return new(types.InvalidArgument) 327 } 328 cfg.VmOrchestration = append(cfg.VmOrchestration, *spec.Info) 329 case types.ArrayUpdateOperationEdit: 330 if !exists { 331 return new(types.InvalidArgument) 332 } 333 if spec.Info.VmReadiness.ReadyCondition != "" { 334 cfg.VmOrchestration[i].VmReadiness.ReadyCondition = spec.Info.VmReadiness.ReadyCondition 335 } 336 if spec.Info.VmReadiness.PostReadyDelay != 0 { 337 cfg.VmOrchestration[i].VmReadiness.PostReadyDelay = spec.Info.VmReadiness.PostReadyDelay 338 } 339 case types.ArrayUpdateOperationRemove: 340 if !exists { 341 return new(types.InvalidArgument) 342 } 343 cfg.VmOrchestration = append(cfg.VmOrchestration[:i], cfg.VmOrchestration[i+1:]...) 344 } 345 } 346 347 return nil 348 } 349 350 func (c *ClusterComputeResource) ReconfigureComputeResourceTask(ctx *Context, req *types.ReconfigureComputeResource_Task) soap.HasFault { 351 task := CreateTask(c, "reconfigureCluster", func(*Task) (types.AnyType, types.BaseMethodFault) { 352 spec, ok := req.Spec.(*types.ClusterConfigSpecEx) 353 if !ok { 354 return nil, new(types.InvalidArgument) 355 } 356 357 updates := []func(*types.ClusterConfigInfoEx, *types.ClusterConfigSpecEx) types.BaseMethodFault{ 358 c.update, 359 c.updateRules, 360 c.updateGroups, 361 c.updateOverridesDAS, 362 c.updateOverridesDRS, 363 c.updateOverridesVmOrchestration, 364 } 365 366 for _, update := range updates { 367 if err := update(c.ConfigurationEx.(*types.ClusterConfigInfoEx), spec); err != nil { 368 return nil, err 369 } 370 } 371 372 return nil, nil 373 }) 374 375 return &methods.ReconfigureComputeResource_TaskBody{ 376 Res: &types.ReconfigureComputeResource_TaskResponse{ 377 Returnval: task.Run(ctx), 378 }, 379 } 380 } 381 382 func (c *ClusterComputeResource) MoveIntoTask(ctx *Context, req *types.MoveInto_Task) soap.HasFault { 383 task := CreateTask(c, "moveInto", func(*Task) (types.AnyType, types.BaseMethodFault) { 384 for _, ref := range req.Host { 385 host := ctx.Map.Get(ref).(*HostSystem) 386 387 if *host.Parent == c.Self { 388 return nil, new(types.DuplicateName) // host already in this cluster 389 } 390 391 switch parent := ctx.Map.Get(*host.Parent).(type) { 392 case *ClusterComputeResource: 393 if !host.Runtime.InMaintenanceMode { 394 return nil, new(types.InvalidState) 395 } 396 397 RemoveReference(&parent.Host, ref) 398 case *mo.ComputeResource: 399 ctx.Map.Remove(ctx, parent.Self) 400 } 401 402 c.Host = append(c.Host, ref) 403 host.Parent = &c.Self 404 } 405 406 return nil, nil 407 }) 408 409 return &methods.MoveInto_TaskBody{ 410 Res: &types.MoveInto_TaskResponse{ 411 Returnval: task.Run(ctx), 412 }, 413 } 414 } 415 416 func (c *ClusterComputeResource) PlaceVm(ctx *Context, req *types.PlaceVm) soap.HasFault { 417 body := new(methods.PlaceVmBody) 418 419 if len(c.Host) == 0 { 420 body.Fault_ = Fault("", new(types.InvalidState)) 421 return body 422 } 423 424 res := types.ClusterRecommendation{ 425 Key: "1", 426 Type: "V1", 427 Time: time.Now(), 428 Rating: 1, 429 Reason: string(types.RecommendationReasonCodeXvmotionPlacement), 430 ReasonText: string(types.RecommendationReasonCodeXvmotionPlacement), 431 Target: &c.Self, 432 } 433 434 hosts := req.PlacementSpec.Hosts 435 if len(hosts) == 0 { 436 hosts = c.Host 437 } 438 439 datastores := req.PlacementSpec.Datastores 440 if len(datastores) == 0 { 441 datastores = c.Datastore 442 } 443 444 switch types.PlacementSpecPlacementType(req.PlacementSpec.PlacementType) { 445 case types.PlacementSpecPlacementTypeClone, types.PlacementSpecPlacementTypeCreate: 446 spec := &types.VirtualMachineRelocateSpec{ 447 Datastore: &datastores[rand.Intn(len(c.Datastore))], 448 Host: &hosts[rand.Intn(len(c.Host))], 449 Pool: c.ResourcePool, 450 } 451 res.Action = append(res.Action, &types.PlacementAction{ 452 Vm: req.PlacementSpec.Vm, 453 TargetHost: spec.Host, 454 RelocateSpec: spec, 455 }) 456 case types.PlacementSpecPlacementTypeReconfigure: 457 // Validate input. 458 if req.PlacementSpec.ConfigSpec == nil { 459 body.Fault_ = Fault("", &types.InvalidArgument{ 460 InvalidProperty: "PlacementSpec.configSpec", 461 }) 462 return body 463 } 464 465 // Update PlacementResult. 466 vmObj := ctx.Map.Get(*req.PlacementSpec.Vm).(*VirtualMachine) 467 spec := &types.VirtualMachineRelocateSpec{ 468 Datastore: &vmObj.Datastore[0], 469 Host: vmObj.Runtime.Host, 470 Pool: vmObj.ResourcePool, 471 DiskMoveType: string(types.VirtualMachineRelocateDiskMoveOptionsMoveAllDiskBackingsAndAllowSharing), 472 } 473 res.Action = append(res.Action, &types.PlacementAction{ 474 Vm: req.PlacementSpec.Vm, 475 TargetHost: spec.Host, 476 RelocateSpec: spec, 477 }) 478 case types.PlacementSpecPlacementTypeRelocate: 479 // Validate fields of req.PlacementSpec, if explicitly provided. 480 if !validatePlacementSpecForPlaceVmRelocate(ctx, req, body) { 481 return body 482 } 483 484 // After validating req.PlacementSpec, we must have a valid req.PlacementSpec.Vm. 485 vmObj := ctx.Map.Get(*req.PlacementSpec.Vm).(*VirtualMachine) 486 487 // Populate RelocateSpec's common fields, if not explicitly provided. 488 populateRelocateSpecForPlaceVmRelocate(&req.PlacementSpec.RelocateSpec, vmObj) 489 490 // Update PlacementResult. 491 res.Action = append(res.Action, &types.PlacementAction{ 492 Vm: req.PlacementSpec.Vm, 493 TargetHost: req.PlacementSpec.RelocateSpec.Host, 494 RelocateSpec: req.PlacementSpec.RelocateSpec, 495 }) 496 default: 497 log.Printf("unsupported placement type: %s", req.PlacementSpec.PlacementType) 498 body.Fault_ = Fault("", new(types.NotSupported)) 499 return body 500 } 501 502 body.Res = &types.PlaceVmResponse{ 503 Returnval: types.PlacementResult{ 504 Recommendations: []types.ClusterRecommendation{res}, 505 }, 506 } 507 508 return body 509 } 510 511 // validatePlacementSpecForPlaceVmRelocate validates the fields of req.PlacementSpec for a relocate placement type. 512 // Returns true if the fields are valid, false otherwise. 513 func validatePlacementSpecForPlaceVmRelocate(ctx *Context, req *types.PlaceVm, body *methods.PlaceVmBody) bool { 514 if req.PlacementSpec.Vm == nil { 515 body.Fault_ = Fault("", &types.InvalidArgument{ 516 InvalidProperty: "PlacementSpec", 517 }) 518 return false 519 } 520 521 // Oddly when the VM is not found, PlaceVm complains about configSpec being invalid, despite this being 522 // a relocate placement type. Possibly due to treating the missing VM as a create placement type 523 // internally, which requires the configSpec to be present. 524 vmObj, exist := ctx.Map.Get(*req.PlacementSpec.Vm).(*VirtualMachine) 525 if !exist { 526 body.Fault_ = Fault("", &types.InvalidArgument{ 527 InvalidProperty: "PlacementSpec.configSpec", 528 }) 529 return false 530 } 531 532 return validateRelocateSpecForPlaceVmRelocate(ctx, req.PlacementSpec.RelocateSpec, body, vmObj) 533 } 534 535 // validateRelocateSpecForPlaceVmRelocate validates the fields of req.PlacementSpec.RelocateSpec for a relocate 536 // placement type. Returns true if the fields are valid, false otherwise. 537 func validateRelocateSpecForPlaceVmRelocate(ctx *Context, spec *types.VirtualMachineRelocateSpec, body *methods.PlaceVmBody, vmObj *VirtualMachine) bool { 538 if spec == nil { 539 // An empty relocate spec is valid, as it will be populated with default values. 540 return true 541 } 542 543 if spec.Host != nil { 544 if _, exist := ctx.Map.Get(*spec.Host).(*HostSystem); !exist { 545 body.Fault_ = Fault("", &types.ManagedObjectNotFound{ 546 Obj: *spec.Host, 547 }) 548 return false 549 } 550 } 551 552 if spec.Datastore != nil { 553 if _, exist := ctx.Map.Get(*spec.Datastore).(*Datastore); !exist { 554 body.Fault_ = Fault("", &types.ManagedObjectNotFound{ 555 Obj: *spec.Datastore, 556 }) 557 return false 558 } 559 } 560 561 if spec.Pool != nil { 562 if _, exist := ctx.Map.Get(*spec.Pool).(*ResourcePool); !exist { 563 body.Fault_ = Fault("", &types.ManagedObjectNotFound{ 564 Obj: *spec.Pool, 565 }) 566 return false 567 } 568 } 569 570 if spec.Disk != nil { 571 deviceList := object.VirtualDeviceList(vmObj.Config.Hardware.Device) 572 vdiskList := deviceList.SelectByType(&types.VirtualDisk{}) 573 for _, disk := range spec.Disk { 574 var diskFound bool 575 for _, vdisk := range vdiskList { 576 if disk.DiskId == vdisk.GetVirtualDevice().Key { 577 diskFound = true 578 break 579 } 580 } 581 if !diskFound { 582 body.Fault_ = Fault("", &types.InvalidArgument{ 583 InvalidProperty: "PlacementSpec.vm", 584 }) 585 return false 586 } 587 588 // Unlike a non-existing spec.Datastore that throws ManagedObjectNotFound, a non-existing disk.Datastore 589 // throws InvalidArgument. 590 if _, exist := ctx.Map.Get(disk.Datastore).(*Datastore); !exist { 591 body.Fault_ = Fault("", &types.InvalidArgument{ 592 InvalidProperty: "RelocateSpec", 593 }) 594 return false 595 } 596 } 597 } 598 599 return true 600 } 601 602 // populateRelocateSpecForPlaceVmRelocate populates the fields of req.PlacementSpec.RelocateSpec for a relocate 603 // placement type, if not explicitly provided. 604 func populateRelocateSpecForPlaceVmRelocate(specPtr **types.VirtualMachineRelocateSpec, vmObj *VirtualMachine) { 605 if *specPtr == nil { 606 *specPtr = &types.VirtualMachineRelocateSpec{} 607 } 608 609 spec := *specPtr 610 611 if spec.DiskMoveType == "" { 612 spec.DiskMoveType = string(types.VirtualMachineRelocateDiskMoveOptionsMoveAllDiskBackingsAndDisallowSharing) 613 } 614 615 if spec.Datastore == nil { 616 spec.Datastore = &vmObj.Datastore[0] 617 } 618 619 if spec.Host == nil { 620 spec.Host = vmObj.Runtime.Host 621 } 622 623 if spec.Pool == nil { 624 spec.Pool = vmObj.ResourcePool 625 } 626 627 if spec.Disk == nil { 628 deviceList := object.VirtualDeviceList(vmObj.Config.Hardware.Device) 629 for _, vdisk := range deviceList.SelectByType(&types.VirtualDisk{}) { 630 spec.Disk = append(spec.Disk, types.VirtualMachineRelocateSpecDiskLocator{ 631 DiskId: vdisk.GetVirtualDevice().Key, 632 Datastore: *spec.Datastore, 633 DiskMoveType: spec.DiskMoveType, 634 }) 635 } 636 } 637 } 638 639 func CreateClusterComputeResource(ctx *Context, f *Folder, name string, spec types.ClusterConfigSpecEx) (*ClusterComputeResource, types.BaseMethodFault) { 640 if e := ctx.Map.FindByName(name, f.ChildEntity); e != nil { 641 return nil, &types.DuplicateName{ 642 Name: e.Entity().Name, 643 Object: e.Reference(), 644 } 645 } 646 647 cluster := &ClusterComputeResource{} 648 cluster.EnvironmentBrowser = newEnvironmentBrowser(ctx) 649 cluster.Name = name 650 cluster.Network = ctx.Map.getEntityDatacenter(f).defaultNetwork() 651 cluster.Summary = &types.ClusterComputeResourceSummary{ 652 UsageSummary: new(types.ClusterUsageSummary), 653 } 654 655 config := &types.ClusterConfigInfoEx{} 656 cluster.ConfigurationEx = config 657 658 config.VmSwapPlacement = string(types.VirtualMachineConfigInfoSwapPlacementTypeVmDirectory) 659 config.DrsConfig.Enabled = types.NewBool(true) 660 661 pool := NewResourcePool() 662 ctx.Map.PutEntity(cluster, ctx.Map.NewEntity(pool)) 663 cluster.ResourcePool = &pool.Self 664 665 folderPutChild(ctx, &f.Folder, cluster) 666 pool.Owner = cluster.Self 667 668 return cluster, nil 669 }