All checks were successful
continuous-integration/drone/push Build is passing
913 lines
25 KiB
Go
913 lines
25 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/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", "vcenter", v.Vurl, "vm_id", vm.Reference().Value)
|
|
|
|
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
|
|
}
|