Files
vctp2/internal/vcenter/vcenter.go
Nathan Coad 3e2d95d3b9
All checks were successful
continuous-integration/drone/push Build is passing
fix aggregation logic
2026-01-23 09:38:08 +11:00

814 lines
23 KiB
Go

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/find"
"github.com/vmware/govmomi/object"
"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"
// 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) {
var results []*object.VirtualMachine
finder := find.NewFinder(v.client.Client, true)
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)
// List all datacenters
datacenters, err := finder.DatacenterList(v.ctx, "*")
if err != nil {
return nil, fmt.Errorf("failed to list datacenters: %w", err)
}
for _, dc := range datacenters {
v.Logger.Debug("Getting VMs in", "datacenter", dc.Name())
// Set the current datacenter
finder.SetDatacenter(dc)
// Get the list of all virtual machines in the current datacenter
vms, err := finder.VirtualMachineList(v.ctx, "*")
if err != nil {
v.Logger.Error("Failed to list VMs in", "datacenter", dc.Name(), "error", err)
continue
}
for _, vm := range vms {
//vmRef := vm.Reference()
//v.Logger.Debug("result", "vm", vm, "MoRef", vmRef, "path", vm.InventoryPath)
results = append(results, vm)
}
}
v.Logger.Debug("Found VM references", "count", len(results))
return results, err
}
// 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",
"runtime.powerState",
"runtime.host",
"resourcePool",
}
if err := cv.Retrieve(v.ctx, []string{"VirtualMachine"}, props, &vms); err != nil {
return nil, fmt.Errorf("failed to retrieve VMs: %w", err)
}
return vms, 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) {
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)
recordDeletion := func(vmID string, ts time.Time) {
if vmID == "" {
return
}
if prev, ok := result[vmID]; !ok || ts.Before(prev) {
result[vmID] = ts
}
}
isDeletionMessage := func(msg string) bool {
msg = strings.ToLower(msg)
return strings.Contains(msg, "destroy") || strings.Contains(msg, "deleted")
}
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)
}
case *types.TaskEvent:
// Fallback for destroy task events.
if e.Info.Entity != nil {
vmID := e.Info.Entity.Value
if vmID != "" && isDeletionMessage(e.GetEvent().FullFormattedMessage) {
recordDeletion(vmID, e.CreatedTime)
}
}
case *types.VmEvent:
if e.Vm != nil {
vmID := e.Vm.Vm.Value
if vmID != "" && isDeletionMessage(e.GetEvent().FullFormattedMessage) {
recordDeletion(vmID, e.CreatedTime)
}
}
}
}
}
// First attempt: specific deletion event types.
filter := types.EventFilterSpec{
Time: &types.EventFilterSpecByTime{
BeginTime: &beginUTC,
EndTime: &endUTC,
},
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)
events, err := collector.ReadNextEvents(ctx, 500)
if err != nil {
return result, fmt.Errorf("failed to read events: %w", err)
}
processEvents(events)
// If nothing found, widen the filter to all event types in the window as a fallback.
if len(result) == 0 {
fallbackFilter := types.EventFilterSpec{
Time: &types.EventFilterSpecByTime{
BeginTime: &beginUTC,
EndTime: &endUTC,
},
}
fc, err := mgr.CreateCollectorForEvents(ctx, fallbackFilter)
if err == nil {
defer fc.Destroy(ctx)
if evs, readErr := fc.ReadNextEvents(ctx, 500); readErr == nil {
processEvents(evs)
}
}
}
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
}
for _, host := range hosts {
ref := host.Reference()
var moHost mo.HostSystem
if err := v.client.RetrieveOne(v.ctx, ref, []string{"parent"}, &moHost); err != nil {
v.Logger.Warn("failed to retrieve host info", "host", host.Name(), "error", err)
continue
}
clusterName := ""
if moHost.Parent != nil {
if cached, ok := clusterCache[moHost.Parent.Value]; ok {
clusterName = cached
} else {
var moCompute mo.ComputeResource
if err := v.client.RetrieveOne(v.ctx, *moHost.Parent, []string{"name"}, &moCompute); err == nil {
clusterName = moCompute.Name
clusterCache[moHost.Parent.Value] = clusterName
}
}
}
lookup[ref.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 := folders.Retrieve(v.ctx, []string{"Folder"}, []string{"name", "parent"}, &results); 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) {
// Use the InventoryPath to extract the datacenter name and VM path
inventoryPath := vmObj.InventoryPath
parts := strings.SplitN(inventoryPath, "/", 3)
if len(parts) < 2 {
return nil, fmt.Errorf("invalid InventoryPath: %s", inventoryPath)
}
// The first part of the path is the datacenter name
datacenterName := parts[1]
// Finder to search for datacenter and VM
finder := find.NewFinder(v.client.Client, true)
// Find the specific datacenter by name
datacenter, err := finder.Datacenter(v.ctx, fmt.Sprintf("/%s", datacenterName))
if err != nil {
return nil, fmt.Errorf("failed to find datacenter %s: %w", datacenterName, err)
}
// Set the found datacenter in the finder
finder.SetDatacenter(datacenter)
// Now retrieve the VM using its ManagedObjectReference
vmRef := vmObj.Reference()
// Retrieve the full mo.VirtualMachine object for the reference
var moVM mo.VirtualMachine
err = v.client.RetrieveOne(v.ctx, vmRef, nil, &moVM)
if err != nil {
return nil, fmt.Errorf("failed to retrieve VM %s in datacenter %s: %w", vmObj.Name(), datacenterName, err)
}
// Return the found mo.VirtualMachine object
//v.Logger.Debug("Found VM in datacenter", "vm_name", moVM.Name, "dc_name", datacenterName)
return &moVM, nil
}
func (v *Vcenter) ConvertObjToMoHost(hostObj *object.HostSystem) (*mo.HostSystem, error) {
// Use the InventoryPath to extract the datacenter name and Host path
inventoryPath := hostObj.InventoryPath
parts := strings.SplitN(inventoryPath, "/", 3)
v.Logger.Debug("inventory path", "parts", parts)
if len(parts) < 2 {
return nil, fmt.Errorf("invalid InventoryPath: %s", inventoryPath)
}
// The first part of the path is the datacenter name
datacenterName := parts[1]
// Finder to search for datacenter and VM
finder := find.NewFinder(v.client.Client, true)
// Find the specific datacenter by name
datacenter, err := finder.Datacenter(v.ctx, fmt.Sprintf("/%s", datacenterName))
if err != nil {
return nil, fmt.Errorf("failed to find datacenter %s: %w", datacenterName, err)
}
// Set the found datacenter in the finder
finder.SetDatacenter(datacenter)
// Now retrieve the VM using its ManagedObjectReference
hostRef := hostObj.Reference()
// Retrieve the full mo.HostSystem object for the reference
var moHost mo.HostSystem
err = v.client.RetrieveOne(v.ctx, hostRef, nil, &moHost)
if err != nil {
return nil, fmt.Errorf("failed to retrieve Host %s in datacenter %s: %w", hostObj.Name(), datacenterName, err)
}
// Return the found mo.HostSystem object
v.Logger.Debug("Found Host in datacenter", "host_name", moHost.Name, "dc_name", datacenterName)
return &moHost, nil
}
func (v *Vcenter) GetHostSystemObject(hostRef types.ManagedObjectReference) (*mo.HostSystem, error) {
finder := find.NewFinder(v.client.Client, true)
// List all datacenters
datacenters, err := finder.DatacenterList(v.ctx, "*")
if err != nil {
return nil, fmt.Errorf("failed to list datacenters: %w", err)
}
for _, dc := range datacenters {
v.Logger.Debug("Checking dc for host", "name", dc.Name(), "hostRef", hostRef.String())
// Set the current datacenter
finder.SetDatacenter(dc)
var hs mo.HostSystem
err := v.client.RetrieveOne(v.ctx, hostRef, nil, &hs)
if err != nil {
return nil, err
} else {
v.Logger.Debug("Found hostsystem", "name", hs.Name)
return &hs, nil
}
}
return nil, 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
}
// Get the host object
host, err := v.GetHostSystemObject(*hostRef)
if err != nil {
v.Logger.Error("cant get host", "error", err)
return "", err
}
if host == nil {
v.Logger.Warn("host lookup returned nil", "host_ref", hostRef)
return "", nil
}
v.Logger.Debug("host parent", "parent", host.Parent)
if host.Parent != nil && host.Parent.Type == "ClusterComputeResource" {
// Retrieve properties of the compute resource
var moCompute mo.ComputeResource
err = v.client.RetrieveOne(v.ctx, *host.Parent, nil, &moCompute)
if err != nil {
return "", fmt.Errorf("failed to retrieve compute resource: %w", err)
}
v.Logger.Debug("VM is on host in cluster/compute resource", "name", moCompute.Name)
return moCompute.Name, nil
}
return "", nil
}
// Function to determine the datacenter a VM belongs to
func (v *Vcenter) GetDatacenterForVM(vm mo.VirtualMachine) (string, error) {
// Start with the VM's parent reference
ref := vm.Reference()
// Traverse the inventory hierarchy upwards to find the datacenter
for {
// Get the parent reference of the current object
parentRef, err := v.getParent(ref)
if err != nil {
return "", fmt.Errorf("failed to get parent object: %w", err)
}
// If we get a nil parent reference, it means we've hit the root without finding the datacenter
if parentRef == nil {
return "", fmt.Errorf("failed to find datacenter for VM")
}
// Check if the parent is a Datacenter
switch parentRef.Type {
case "Datacenter":
// If we found a Datacenter, retrieve its properties
datacenter := object.NewDatacenter(v.client.Client, *parentRef)
var moDC mo.Datacenter
err = v.client.RetrieveOne(v.ctx, datacenter.Reference(), nil, &moDC)
if err != nil {
return "", fmt.Errorf("failed to retrieve datacenter: %w", err)
}
//log.Printf("VM is in datacenter: %s", moDC.Name)
v.Logger.Debug("VM datacenter found", "vm_name", vm.Name, "dc_name", moDC.Name)
return moDC.Name, nil
default:
// Continue traversing upwards if not a Datacenter
ref = *parentRef
}
}
}
// Helper function to get the parent ManagedObjectReference of a given object
func (v *Vcenter) getParent(ref types.ManagedObjectReference) (*types.ManagedObjectReference, error) {
// Retrieve the object's properties
var obj mo.ManagedEntity
err := v.client.RetrieveOne(v.ctx, ref, []string{"parent"}, &obj)
if err != nil {
return nil, fmt.Errorf("failed to retrieve parent of object: %w", err)
}
// Return the parent reference
if obj.Parent != nil {
return obj.Parent, nil
}
return nil, nil
}
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 = vms.Retrieve(v.ctx, []string{"VirtualMachine"}, []string{"name"}, &matchingVMs)
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)
finder := find.NewFinder(v.client.Client, true)
// List all datacenters
datacenters, err := finder.DatacenterList(v.ctx, "*")
if err != nil {
return nil, fmt.Errorf("failed to list datacenters: %w", err)
}
for _, dc := range datacenters {
// Set the current datacenter
finder.SetDatacenter(dc)
// Create a ManagedObjectReference for the VM
vmRef := types.ManagedObjectReference{
Type: "VirtualMachine",
Value: vmID,
}
// Try to find the VM by ID in the current datacenter
//vm, err := finder.ObjectReference(v.ctx, vmRef)
var vm mo.VirtualMachine
err := v.client.RetrieveOne(v.ctx, vmRef, []string{"config", "name"}, &vm)
if err == nil {
return &VmProperties{
//Datacenter: dc.Name(),
Vm: vm,
}, nil
} else if _, ok := err.(*find.NotFoundError); !ok {
// If the error is not a NotFoundError, return it
//return nil, fmt.Errorf("failed to retrieve VM with ID %s in datacenter %s: %w", vmID, dc.Name(), err)
v.Logger.Debug("Couldn't find vm in datacenter", "vm_id", vmID, "datacenter_name", dc.Name())
} else {
return nil, fmt.Errorf("failed to retrieve VM: %w", err)
}
}
return nil, fmt.Errorf("VM with ID %s not found in any datacenter", vmID)
}
func (v *Vcenter) FindVMByIDWithDatacenter(vmID string, dcID string) (*mo.VirtualMachine, error) {
var err error
//v.Logger.Debug("searching for vm id", "vm_id", vmID, "datacenter_id", dcID)
finder := find.NewFinder(v.client.Client, true)
// Create a ManagedObjectReference for the datacenter
dcRef := types.ManagedObjectReference{
Type: "Datacenter",
Value: dcID,
}
// Convert the reference to a Datacenter object
datacenter := object.NewDatacenter(v.client.Client, dcRef)
if datacenter == nil {
return nil, fmt.Errorf("Datacenter with id %s not found", dcID)
}
// Use finder.SetDatacenter to set the datacenter
finder.SetDatacenter(datacenter)
// Create a ManagedObjectReference for the VM
vmRef := types.ManagedObjectReference{
Type: "VirtualMachine",
Value: vmID,
}
var vm mo.VirtualMachine
//err := v.client.RetrieveOne(v.ctx, vmRef, []string{"config", "name"}, &vm)
err = v.client.RetrieveOne(v.ctx, vmRef, nil, &vm)
if err == nil {
//v.Logger.Debug("Found VM")
return &vm, nil
} else if _, ok := err.(*find.NotFoundError); !ok {
// If the error is not a NotFoundError, return it
//return nil, fmt.Errorf("failed to retrieve VM with ID %s in datacenter %s: %w", vmID, dc.Name(), err)
//v.Logger.Debug("Couldn't find vm in datacenter", "vm_id", vmID, "datacenter_id", dcID)
return nil, nil
} else {
return nil, fmt.Errorf("failed to retrieve VM: %w", err)
}
}
// 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 {
rp := object.NewResourcePool(v.client.Client, *vm.ResourcePool)
rpName, err := rp.ObjectName(v.ctx)
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 := cv.Retrieve(v.ctx, []string{"ResourcePool"}, []string{"name"}, &pools); 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) {
//finder := find.NewFinder(v.client.Client, true)
v.Logger.Debug("Commencing vm folder path search")
// Start from the VM's parent
parentRef := vm.Parent
if parentRef == nil {
return "", fmt.Errorf("no parent found for the VM")
}
// Traverse the folder hierarchy to build the full folder path
folderPath := ""
//v.Logger.Debug("parent is", "parent", parentRef)
maxHops := 128
for parentRef != nil && parentRef.Type != "Datacenter" && maxHops > 0 {
// Retrieve the parent object
//parentObj, err := finder.ObjectReference(v.ctx, *parentRef)
//if err != nil {
// return "", fmt.Errorf("failed to find parent object in inventory: %w", err)
//}
// Retrieve the folder name
var parentObj mo.Folder
err := v.client.RetrieveOne(v.ctx, *parentRef, nil, &parentObj)
if err != nil {
v.Logger.Error("Failed to get object for parent reference", "ref", parentRef)
break
}
// Prepend the folder name to the path
folderPath = path.Join("/", parentObj.Name, folderPath)
// Move up to the next parent
//if folder, ok := parentObj.(*object.Folder); ok {
if parentObj.Parent != nil {
parentRef = parentObj.Parent
//v.Logger.Debug("Parent uplevel is", "ref", parentRef)
} else {
return "", fmt.Errorf("unexpected parent type: %s", parentObj.Reference().Type)
}
//break
maxHops--
}
if parentRef == nil || maxHops == 0 {
return "", fmt.Errorf("folder traversal terminated early for VM %s", vm.Name)
}
return folderPath, nil
}