package vcenter import ( "context" "fmt" "log/slog" "net/url" "path" "strings" "time" "github.com/vmware/govmomi" "github.com/vmware/govmomi/event" "github.com/vmware/govmomi/fault" "github.com/vmware/govmomi/find" "github.com/vmware/govmomi/object" "github.com/vmware/govmomi/property" "github.com/vmware/govmomi/view" "github.com/vmware/govmomi/vim25/mo" "github.com/vmware/govmomi/vim25/soap" "github.com/vmware/govmomi/vim25/types" ) type Vcenter struct { Logger *slog.Logger Vurl string ctx context.Context client *govmomi.Client credentials *VcenterLogin } type VcenterLogin struct { Username string Password string Insecure bool } type VmProperties struct { Vm mo.VirtualMachine ResourcePool string } var clientUserAgent = "vCTP" const ( defaultPropertyPageSize int32 = 500 ) func normalizeKinds(kind []string) []string { if len(kind) > 0 { return kind } return []string{"ManagedEntity"} } func normalizePageSize(pageSize int32) int32 { if pageSize <= 0 { return defaultPropertyPageSize } return pageSize } func buildPropertySpecs(kind []string, props []string) []types.PropertySpec { kinds := normalizeKinds(kind) specs := make([]types.PropertySpec, 0, len(kinds)) for _, t := range kinds { spec := types.PropertySpec{Type: t} if len(props) == 0 { spec.All = types.NewBool(true) } else { spec.PathSet = props } specs = append(specs, spec) } return specs } func refKey(ref types.ManagedObjectReference) string { return ref.Type + ":" + ref.Value } func (v *Vcenter) retrieveContainerViewPaged(ctx context.Context, cv *view.ContainerView, kind []string, props []string, dst any, pageSize int32) error { pc := property.DefaultCollector(v.client.Client) req := types.RetrieveProperties{ SpecSet: []types.PropertyFilterSpec{ { ObjectSet: []types.ObjectSpec{ { Obj: cv.Reference(), Skip: types.NewBool(true), SelectSet: []types.BaseSelectionSpec{ &types.TraversalSpec{ Type: cv.Reference().Type, Path: "view", }, }, }, }, PropSet: buildPropertySpecs(kind, props), }, }, } res, err := pc.RetrieveProperties(ctx, req, normalizePageSize(pageSize)) if err != nil { return err } if d, ok := dst.(*[]types.ObjectContent); ok { *d = res.Returnval return nil } return mo.LoadObjectContent(res.Returnval, dst) } func (v *Vcenter) retrieveByRefsPaged(ctx context.Context, refs []types.ManagedObjectReference, props []string, dst any, pageSize int32) error { if len(refs) == 0 { return nil } kindSet := make(map[string]struct{}, len(refs)) kinds := make([]string, 0, len(refs)) for _, ref := range refs { if _, ok := kindSet[ref.Type]; ok { continue } kindSet[ref.Type] = struct{}{} kinds = append(kinds, ref.Type) } objectSet := make([]types.ObjectSpec, 0, len(refs)) for _, ref := range refs { objectSet = append(objectSet, types.ObjectSpec{ Obj: ref, Skip: types.NewBool(false), }) } pc := property.DefaultCollector(v.client.Client) req := types.RetrieveProperties{ SpecSet: []types.PropertyFilterSpec{ { ObjectSet: objectSet, PropSet: buildPropertySpecs(kinds, props), }, }, } res, err := pc.RetrieveProperties(ctx, req, normalizePageSize(pageSize)) if err != nil { return err } if d, ok := dst.(*[]types.ObjectContent); ok { *d = res.Returnval return nil } return mo.LoadObjectContent(res.Returnval, dst) } // SetUserAgent customizes the User-Agent used when talking to vCenter. func SetUserAgent(ua string) { if strings.TrimSpace(ua) != "" { clientUserAgent = ua } } type HostLookup struct { Cluster string Datacenter string } type FolderLookup map[string]string // New creates a new Vcenter with the given logger func New(logger *slog.Logger, creds *VcenterLogin) *Vcenter { //ctx, cancel := context.WithCancel(context.Background()) //defer cancel() return &Vcenter{ Logger: logger, ctx: context.Background(), credentials: creds, } } func (v *Vcenter) Login(vUrl string) error { if v == nil { return fmt.Errorf("vcenter is nil") } if strings.TrimSpace(vUrl) == "" { return fmt.Errorf("vcenter URL is empty") } if v.credentials == nil { return fmt.Errorf("vcenter credentials are nil") } // Connect to vCenter u, err := soap.ParseURL(vUrl) if err != nil { return fmt.Errorf("error parsing vCenter URL: %w", err) } v.Vurl = vUrl u.User = url.UserPassword(v.credentials.Username, v.credentials.Password) /* c, err := govmomi.NewClient(ctx, u, insecure) if err != nil { log.Fatalf("Error connecting to vCenter: %s", err) } */ c, err := govmomi.NewClient(v.ctx, u, v.credentials.Insecure) if err != nil { v.Logger.Error("Unable to connect to vCenter", "error", err) return fmt.Errorf("unable to connect to vCenter : %s", err) } if clientUserAgent != "" { c.Client.UserAgent = clientUserAgent } //defer c.Logout(v.ctx) v.client = c v.Logger.Debug("successfully connected to vCenter", "url", vUrl, "username", v.credentials.Username) return nil } func (v *Vcenter) Logout(ctx context.Context) error { if ctx == nil { ctx = v.ctx } if ctx == nil { v.Logger.Warn("Nil context, unable to logout") return nil } if v.client.Valid() { return v.client.Logout(ctx) } v.Logger.Debug("vcenter client is not valid") return nil } func (v *Vcenter) GetAllVmReferences() ([]*object.VirtualMachine, error) { m := view.NewManager(v.client.Client) cv, err := m.CreateContainerView(v.ctx, v.client.ServiceContent.RootFolder, []string{"VirtualMachine"}, true) if err != nil { return nil, fmt.Errorf("failed to create VM container view: %w", err) } defer cv.Destroy(v.ctx) var content []types.ObjectContent if err := v.retrieveContainerViewPaged(v.ctx, cv, []string{"VirtualMachine"}, []string{"name"}, &content, defaultPropertyPageSize); err != nil { return nil, fmt.Errorf("failed to retrieve VM references: %w", err) } results := make([]*object.VirtualMachine, 0, len(content)) for _, c := range content { results = append(results, object.NewVirtualMachine(v.client.Client, c.Obj)) } v.Logger.Debug("Found VM references", "count", len(results)) return results, nil } // GetAllVMsWithProps returns all VMs with the properties needed for snapshotting in a single property-collector call. func (v *Vcenter) GetAllVMsWithProps() ([]mo.VirtualMachine, error) { m := view.NewManager(v.client.Client) cv, err := m.CreateContainerView(v.ctx, v.client.ServiceContent.RootFolder, []string{"VirtualMachine"}, true) if err != nil { return nil, fmt.Errorf("failed to create VM container view: %w", err) } defer cv.Destroy(v.ctx) var vms []mo.VirtualMachine props := []string{ "name", "parent", "config.uuid", "config.createDate", "config.hardware", "config.managedBy", "config.template", "summary.storage", "runtime.powerState", "runtime.host", "resourcePool", } if err := v.retrieveContainerViewPaged(v.ctx, cv, []string{"VirtualMachine"}, props, &vms, defaultPropertyPageSize); err != nil { return nil, fmt.Errorf("failed to retrieve VMs: %w", err) } return vms, nil } // GetVMWithSnapshotPropsByRef retrieves snapshot-critical properties for a single VM reference. // Used as a targeted fallback when bulk retrieval returns partial data for a VM. func (v *Vcenter) GetVMWithSnapshotPropsByRef(ref types.ManagedObjectReference) (*mo.VirtualMachine, error) { var vm mo.VirtualMachine props := []string{ "name", "parent", "config", "summary.storage", "runtime.powerState", "runtime.host", "resourcePool", } if err := v.client.RetrieveOne(v.ctx, ref, props, &vm); err != nil { return nil, err } return &vm, nil } // FindVmDeletionEvents returns a map of MoRef (VmId) to the deletion event time within the given window. func (v *Vcenter) FindVmDeletionEvents(ctx context.Context, begin, end time.Time) (map[string]time.Time, error) { return v.findVmDeletionEvents(ctx, begin, end, nil) } // FindVmDeletionEventsForCandidates returns deletion event times for the provided VM IDs only. func (v *Vcenter) FindVmDeletionEventsForCandidates(ctx context.Context, begin, end time.Time, candidates []string) (map[string]time.Time, error) { if len(candidates) == 0 { return map[string]time.Time{}, nil } candidateSet := make(map[string]struct{}, len(candidates)) for _, id := range candidates { if id == "" { continue } candidateSet[id] = struct{}{} } if len(candidateSet) == 0 { return map[string]time.Time{}, nil } return v.findVmDeletionEvents(ctx, begin, end, candidateSet) } func (v *Vcenter) findVmDeletionEvents(ctx context.Context, begin, end time.Time, candidateSet map[string]struct{}) (map[string]time.Time, error) { result := make(map[string]time.Time) if v.client == nil || !v.client.Valid() { return result, fmt.Errorf("vcenter client is not valid") } // vCenter events are stored in UTC; normalize the query window. beginUTC := begin.UTC() endUTC := end.UTC() mgr := event.NewManager(v.client.Client) type deletionHit struct { ts time.Time priority int } const ( deletionPriorityRemoved = iota deletionPriorityVmEvent deletionPriorityTask ) hits := make(map[string]deletionHit) foundCandidates := 0 recordDeletion := func(vmID string, ts time.Time, priority int) { if vmID == "" { return } if candidateSet != nil { if _, ok := candidateSet[vmID]; !ok { return } } if prev, ok := hits[vmID]; !ok { hits[vmID] = deletionHit{ts: ts, priority: priority} if candidateSet != nil { foundCandidates++ } } else if priority < prev.priority || (priority == prev.priority && ts.Before(prev.ts)) { hits[vmID] = deletionHit{ts: ts, priority: priority} } } isDeletionMessage := func(msg string) bool { msg = strings.ToLower(msg) return strings.Contains(msg, "destroy") || strings.Contains(msg, "deleted") || strings.Contains(msg, "unregister") || strings.Contains(msg, "removed from inventory") } isVmDeletionTask := func(info types.TaskInfo, msg string) bool { id := strings.ToLower(strings.TrimSpace(info.DescriptionId)) if id != "" { if strings.Contains(id, "virtualmachine") && (strings.Contains(id, "destroy") || strings.Contains(id, "delete") || strings.Contains(id, "unregister")) { return true } } name := strings.ToLower(strings.TrimSpace(info.Name)) if name != "" { if (strings.Contains(name, "destroy") || strings.Contains(name, "delete") || strings.Contains(name, "unregister")) && (strings.Contains(name, "virtualmachine") || strings.Contains(name, "virtual machine")) { return true } } if msg != "" && isDeletionMessage(msg) { return true } return false } processEvents := func(evts []types.BaseEvent) { for _, ev := range evts { switch e := ev.(type) { case *types.VmRemovedEvent: if e.Vm != nil { vmID := e.Vm.Vm.Value recordDeletion(vmID, e.CreatedTime, deletionPriorityRemoved) } case *types.TaskEvent: // Fallback for destroy task events. if e.Info.Entity != nil { vmID := e.Info.Entity.Value if vmID != "" && isVmDeletionTask(e.Info, e.GetEvent().FullFormattedMessage) { recordDeletion(vmID, e.CreatedTime, deletionPriorityTask) } } case *types.VmEvent: if e.Vm != nil { vmID := e.Vm.Vm.Value if vmID != "" && isDeletionMessage(e.GetEvent().FullFormattedMessage) { recordDeletion(vmID, e.CreatedTime, deletionPriorityVmEvent) } } } } } const ( eventPageSize = int32(1000) maxEventPages = 25 ) readCollector := func(label string, collector *event.HistoryCollector) error { pageCount := 0 for { events, err := collector.ReadNextEvents(ctx, eventPageSize) if err != nil { return err } if len(events) == 0 { break } pageCount++ processEvents(events) if candidateSet != nil && foundCandidates >= len(candidateSet) { break } if pageCount >= maxEventPages { if v.Logger != nil { v.Logger.Warn("vcenter deletion events truncated", "vcenter", v.Vurl, "label", label, "pages", pageCount, "page_size", eventPageSize, "window_start_utc", beginUTC, "window_end_utc", endUTC) } break } } return nil } // First attempt: specific deletion event types. disableFullMessage := false filter := types.EventFilterSpec{ Time: &types.EventFilterSpecByTime{ BeginTime: &beginUTC, EndTime: &endUTC, }, MaxCount: eventPageSize, DisableFullMessage: &disableFullMessage, EventTypeId: []string{ "VmRemovedEvent", "TaskEvent", }, } collector, err := mgr.CreateCollectorForEvents(ctx, filter) if err != nil { return result, fmt.Errorf("failed to create event collector: %w", err) } defer collector.Destroy(ctx) if err := readCollector("primary", collector); err != nil { return result, fmt.Errorf("failed to read events: %w", err) } // If nothing found, widen the filter to all event types in the window as a fallback. if len(hits) == 0 { fallbackFilter := types.EventFilterSpec{ Time: &types.EventFilterSpecByTime{ BeginTime: &beginUTC, EndTime: &endUTC, }, MaxCount: eventPageSize, DisableFullMessage: &disableFullMessage, } fc, err := mgr.CreateCollectorForEvents(ctx, fallbackFilter) if err == nil { defer fc.Destroy(ctx) if readErr := readCollector("fallback", fc); readErr != nil && v.Logger != nil { v.Logger.Warn("vcenter fallback event read failed", "vcenter", v.Vurl, "error", readErr) } } } for vmID, hit := range hits { result[vmID] = hit.ts } return result, nil } func (v *Vcenter) BuildHostLookup() (map[string]HostLookup, error) { finder := find.NewFinder(v.client.Client, true) datacenters, err := finder.DatacenterList(v.ctx, "*") if err != nil { return nil, fmt.Errorf("failed to list datacenters: %w", err) } lookup := make(map[string]HostLookup) clusterCache := make(map[string]string) for _, dc := range datacenters { finder.SetDatacenter(dc) hosts, err := finder.HostSystemList(v.ctx, "*") if err != nil { v.Logger.Warn("failed to list hosts for datacenter", "datacenter", dc.Name(), "error", err) continue } if len(hosts) == 0 { continue } hostRefs := make([]types.ManagedObjectReference, 0, len(hosts)) for _, host := range hosts { hostRefs = append(hostRefs, host.Reference()) } var moHosts []mo.HostSystem if err := v.retrieveByRefsPaged(v.ctx, hostRefs, []string{"parent"}, &moHosts, defaultPropertyPageSize); err != nil { v.Logger.Warn("failed to retrieve host parents for datacenter", "datacenter", dc.Name(), "error", err) continue } parentRefs := make([]types.ManagedObjectReference, 0, len(moHosts)) parentSeen := make(map[string]struct{}) for _, host := range moHosts { if host.Parent == nil { continue } key := refKey(*host.Parent) if _, cached := clusterCache[key]; cached { continue } if _, seen := parentSeen[key]; seen { continue } parentSeen[key] = struct{}{} parentRefs = append(parentRefs, *host.Parent) } if len(parentRefs) > 0 { var computes []mo.ComputeResource if err := v.retrieveByRefsPaged(v.ctx, parentRefs, []string{"name"}, &computes, defaultPropertyPageSize); err != nil { v.Logger.Warn("failed to retrieve cluster names for datacenter", "datacenter", dc.Name(), "error", err) } else { for _, compute := range computes { clusterCache[refKey(compute.Reference())] = compute.Name } } } for _, host := range moHosts { clusterName := "" if host.Parent != nil { clusterName = clusterCache[refKey(*host.Parent)] } lookup[host.Reference().Value] = HostLookup{ Cluster: clusterName, Datacenter: dc.Name(), } } } return lookup, nil } func (v *Vcenter) BuildFolderPathLookup() (FolderLookup, error) { m := view.NewManager(v.client.Client) folders, err := m.CreateContainerView(v.ctx, v.client.ServiceContent.RootFolder, []string{"Folder"}, true) if err != nil { return nil, err } defer folders.Destroy(v.ctx) var results []mo.Folder if err := v.retrieveContainerViewPaged(v.ctx, folders, []string{"Folder"}, []string{"name", "parent"}, &results, defaultPropertyPageSize); err != nil { return nil, err } nameByID := make(map[string]string, len(results)) parentByID := make(map[string]*types.ManagedObjectReference, len(results)) for _, folder := range results { nameByID[folder.Reference().Value] = folder.Name parentByID[folder.Reference().Value] = folder.Parent } paths := make(FolderLookup, len(results)) var buildPath func(id string) string buildPath = func(id string) string { if pathValue, ok := paths[id]; ok { return pathValue } name, ok := nameByID[id] if !ok { return "" } parent := parentByID[id] if parent == nil || parent.Type == "Datacenter" { paths[id] = path.Join("/", name) return paths[id] } if parent.Type != "Folder" { paths[id] = path.Join("/", name) return paths[id] } parentPath := buildPath(parent.Value) if parentPath == "" { paths[id] = path.Join("/", name) return paths[id] } paths[id] = path.Join(parentPath, name) return paths[id] } for id := range nameByID { _ = buildPath(id) } return paths, nil } func (v *Vcenter) GetVMFolderPathFromLookup(vm mo.VirtualMachine, lookup FolderLookup) (string, bool) { if vm.Parent == nil || lookup == nil { return "", false } pathValue, ok := lookup[vm.Parent.Value] return pathValue, ok } func (v *Vcenter) ConvertObjToMoVM(vmObj *object.VirtualMachine) (*mo.VirtualMachine, error) { if vmObj == nil { return nil, fmt.Errorf("vm object is nil") } vmRef := vmObj.Reference() var moVM mo.VirtualMachine err := v.client.RetrieveOne(v.ctx, vmRef, []string{"name", "config"}, &moVM) if err != nil { return nil, fmt.Errorf("failed to retrieve VM %s: %w", vmObj.Name(), err) } return &moVM, nil } func (v *Vcenter) ConvertObjToMoHost(hostObj *object.HostSystem) (*mo.HostSystem, error) { if hostObj == nil { return nil, fmt.Errorf("host object is nil") } hostRef := hostObj.Reference() var moHost mo.HostSystem err := v.client.RetrieveOne(v.ctx, hostRef, []string{"name", "parent"}, &moHost) if err != nil { return nil, fmt.Errorf("failed to retrieve host %s: %w", hostObj.Name(), err) } v.Logger.Debug("found host", "host_name", moHost.Name) return &moHost, nil } func (v *Vcenter) GetHostSystemObject(hostRef types.ManagedObjectReference) (*mo.HostSystem, error) { var hs mo.HostSystem if err := v.client.RetrieveOne(v.ctx, hostRef, []string{"name", "parent"}, &hs); err != nil { return nil, err } v.Logger.Debug("found hostsystem", "name", hs.Name) return &hs, nil } func (v *Vcenter) GetHostLookupByRef(hostRef types.ManagedObjectReference) (HostLookup, error) { lookup := HostLookup{} var host mo.HostSystem if err := v.client.RetrieveOne(v.ctx, hostRef, []string{"parent"}, &host); err != nil { return lookup, fmt.Errorf("failed to retrieve host parent: %w", err) } if host.Parent != nil && (host.Parent.Type == "ClusterComputeResource" || host.Parent.Type == "ComputeResource") { var moCompute mo.ComputeResource if err := v.client.RetrieveOne(v.ctx, *host.Parent, []string{"name"}, &moCompute); err != nil { if v.Logger != nil { v.Logger.Warn("failed to resolve cluster/compute name from host parent", "host_ref", hostRef.Value, "error", err) } } else { lookup.Cluster = moCompute.Name } } entities, err := mo.Ancestors(v.ctx, v.client.Client, v.client.ServiceContent.PropertyCollector, hostRef) if err != nil { if v.Logger != nil { v.Logger.Warn("failed to resolve datacenter from host ancestors", "host_ref", hostRef.Value, "error", err) } return lookup, nil } for _, entity := range entities { if entity.Self.Type != "Datacenter" { continue } lookup.Datacenter = entity.Name break } return lookup, nil } // Function to find the cluster or compute resource from a host reference func (v *Vcenter) GetClusterFromHost(hostRef *types.ManagedObjectReference) (string, error) { if hostRef == nil { v.Logger.Warn("nil hostRef passed to GetClusterFromHost") return "", nil } lookup, err := v.GetHostLookupByRef(*hostRef) if err != nil { v.Logger.Error("cant get host lookup", "error", err) return "", err } return lookup.Cluster, nil } // Function to determine the datacenter a VM belongs to func (v *Vcenter) GetDatacenterForVM(vm mo.VirtualMachine) (string, error) { entities, err := mo.Ancestors(v.ctx, v.client.Client, v.client.ServiceContent.PropertyCollector, vm.Reference()) if err != nil { return "", fmt.Errorf("failed to retrieve VM ancestors: %w", err) } for _, entity := range entities { if entity.Self.Type != "Datacenter" { continue } if entity.Name == "" { return "", fmt.Errorf("datacenter ancestor found without name for VM %s", vm.Reference().Value) } return entity.Name, nil } return "", fmt.Errorf("failed to find datacenter for VM %s", vm.Reference().Value) } func (v *Vcenter) FindVMByName(vmName string) ([]mo.VirtualMachine, error) { m := view.NewManager(v.client.Client) vms, err := m.CreateContainerView(v.ctx, v.client.ServiceContent.RootFolder, []string{"VirtualMachine"}, true) if err != nil { return nil, err } defer vms.Destroy(v.ctx) var matchingVMs []mo.VirtualMachine err = v.retrieveContainerViewPaged(v.ctx, vms, []string{"VirtualMachine"}, []string{"name"}, &matchingVMs, defaultPropertyPageSize) if err != nil { return nil, err } // Temporarily just return all VMs //return matchingVMs, nil var result []mo.VirtualMachine for _, vm := range matchingVMs { if vm.Name == vmName { result = append(result, vm) } } return result, nil } func (v *Vcenter) FindVMByID(vmID string) (*VmProperties, error) { v.Logger.Debug("searching for vm id", "vm_id", vmID) vmRef := types.ManagedObjectReference{ Type: "VirtualMachine", Value: vmID, } var vm mo.VirtualMachine if err := v.client.RetrieveOne(v.ctx, vmRef, []string{"config", "name"}, &vm); err != nil { return nil, fmt.Errorf("failed to retrieve VM %s: %w", vmID, err) } return &VmProperties{Vm: vm}, nil } func (v *Vcenter) FindVMByIDWithDatacenter(vmID string, dcID string) (*mo.VirtualMachine, error) { if dcID != "" { v.Logger.Debug("searching for vm id in datacenter context", "vm_id", vmID, "datacenter_id", dcID) } vmRef := types.ManagedObjectReference{ Type: "VirtualMachine", Value: vmID, } var vm mo.VirtualMachine props := []string{"name", "config", "runtime.powerState", "runtime.host", "resourcePool", "parent"} if err := v.client.RetrieveOne(v.ctx, vmRef, props, &vm); err != nil { if fault.Is(err, &types.ManagedObjectNotFound{}) { return nil, nil } return nil, fmt.Errorf("failed to retrieve VM %s: %w", vmID, err) } return &vm, nil } func (v *Vcenter) GetResourcePoolNameByRef(resourcePoolRef types.ManagedObjectReference) (string, error) { var pool mo.ResourcePool if err := v.client.RetrieveOne(v.ctx, resourcePoolRef, []string{"name"}, &pool); err != nil { return "", fmt.Errorf("failed to retrieve resource pool: %w", err) } return pool.Name, nil } // Helper function to retrieve the resource pool for the VM func (v *Vcenter) GetVmResourcePool(vm mo.VirtualMachine) (string, error) { var resourcePool string if vm.ResourcePool != nil { rpName, err := v.GetResourcePoolNameByRef(*vm.ResourcePool) if err != nil { v.Logger.Error("failed to get resource pool name", "error", err) return resourcePool, err } else { //v.Logger.Debug("Found resource pool name", "rp_name", rpName) resourcePool = rpName } } return resourcePool, nil } // BuildResourcePoolLookup creates a cache of resource pool MoRef -> name for fast lookups. func (v *Vcenter) BuildResourcePoolLookup() (map[string]string, error) { m := view.NewManager(v.client.Client) cv, err := m.CreateContainerView(v.ctx, v.client.ServiceContent.RootFolder, []string{"ResourcePool"}, true) if err != nil { return nil, fmt.Errorf("failed to create resource pool view: %w", err) } defer cv.Destroy(v.ctx) var pools []mo.ResourcePool if err := v.retrieveContainerViewPaged(v.ctx, cv, []string{"ResourcePool"}, []string{"name"}, &pools, defaultPropertyPageSize); err != nil { return nil, fmt.Errorf("failed to retrieve resource pools: %w", err) } lookup := make(map[string]string, len(pools)) for _, pool := range pools { lookup[pool.Reference().Value] = pool.Name } return lookup, nil } // Helper function to retrieve the full folder path for the VM func (v *Vcenter) GetVMFolderPath(vm mo.VirtualMachine) (string, error) { v.Logger.Debug("Commencing vm folder path search") entities, err := mo.Ancestors(v.ctx, v.client.Client, v.client.ServiceContent.PropertyCollector, vm.Reference()) if err != nil { return "", fmt.Errorf("failed to retrieve VM ancestors: %w", err) } var folderParts []string seenDatacenter := false vmRef := vm.Reference() for _, entity := range entities { switch entity.Self.Type { case "Datacenter": seenDatacenter = true folderParts = folderParts[:0] case "VirtualMachine": if entity.Self == vmRef { goto done } case "Folder": if seenDatacenter { folderParts = append(folderParts, entity.Name) } } } done: if !seenDatacenter { return "", fmt.Errorf("failed to find datacenter ancestor for VM %s", vm.Reference().Value) } if len(folderParts) == 0 { return "", fmt.Errorf("failed to find folder path for VM %s", vm.Reference().Value) } return path.Join("/", path.Join(folderParts...)), nil }