package main import ( "context" "encoding/json" "flag" "fmt" "log" "net/url" "os" "reflect" "strings" "time" _ "time/tzdata" "github.com/vmware/govmomi" "github.com/vmware/govmomi/object" "github.com/vmware/govmomi/performance" "github.com/vmware/govmomi/view" "github.com/vmware/govmomi/vim25" "github.com/vmware/govmomi/vim25/mo" "github.com/vmware/govmomi/vim25/types" ) var ( c *govmomi.Client ctx context.Context cancel context.CancelFunc location *time.Location sha1ver string // sha1 revision used to build the program buildTime string // when the executable was built results []DrsResults ) type DrsResults struct { ClusterName string DrsScore int32 Vcenter string NumVmsDrsBucket0To20 int32 NumVmsDrsBucket21To40 int32 NumVmsDrsBucket41To60 int32 NumVmsDrsBucket61To80 int32 NumVmsDrsBucket81To100 int32 } // From https://github.com/vmware/govmomi/blob/main/performance/manager.go#L197 /* type groupPerfCounterInfo struct { info map[int32]*types.PerfCounterInfo ids []types.PerfMetricId } */ /* func findPerfCounter(perfManager *vim25.PerformanceManager, counterName string) (*vim25.PerfCounterInfo, error) { perfCounters, err := perfManager.QueryPerfCounter(context.Background(), nil) if err != nil { return nil, err } for _, counter := range perfCounters { if counter.NameInfo.GetElement().Key == counterName { return &counter, nil } } return nil, fmt.Errorf("Performance counter '%s' not found", counterName) } */ func findVMByName(ctx context.Context, client *vim25.Client, vmName string) ([]mo.VirtualMachine, error) { m := view.NewManager(client) vms, err := m.CreateContainerView(ctx, client.ServiceContent.RootFolder, []string{"VirtualMachine"}, true) if err != nil { return nil, err } defer vms.Destroy(ctx) var matchingVMs []mo.VirtualMachine err = vms.Retrieve(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 findClusterByName(ctx context.Context, client *vim25.Client, name string) mo.ClusterComputeResource { // Create a container view so that we can search vCenter m := view.NewManager(client) cv, _ := m.CreateContainerView(ctx, c.ServiceContent.RootFolder, []string{"ClusterComputeResource"}, true) var clusters []mo.ClusterComputeResource log.Printf("Searching for Cluster '%s'\n", name) err := cv.Retrieve(ctx, []string{"ClusterComputeResource"}, []string{"summary", "name"}, &clusters) if err != nil { log.Printf("Failed searching for Cluster %s : %s\n", name, err) return mo.ClusterComputeResource{} } else { for _, cluster := range clusters { if cluster.Name == name { log.Printf("Found corresponding Cluster with MoRef '%s'\n", cluster.Reference()) return cluster } } } // If we reached here then we didn't find the Cluster return mo.ClusterComputeResource{} } func getMetricIds(counters []types.PerfCounterInfo) []types.PerfMetricId { var metricIds []types.PerfMetricId for _, counter := range counters { var metricId types.PerfMetricId metricId.CounterId = counter.Key metricIds = append(metricIds, metricId) } fmt.Printf("metric IDs : '%v'\n", metricIds) return metricIds } func getSpecificVMMetrics(ctx context.Context, c *vim25.Client, vmName string) { //perfManager := c.ServiceContent.PerfManager perfManager := performance.NewManager(c) var perfCounterInfos []types.PerfCounterInfo var perfCounterNames []string startTime := time.Now().Add(-time.Hour) timeNow := time.Now() // Find the performance counters for CPU Entitlement, CPU Consumed, and CPU Demand // % Entitled Resource Delivered = Consumed/(min(Demand,Entitlement)) * 100 // As per https://www.unknownfault.com/posts/how-is-percentage-entitled-resource/ counterNames := []string{"cpu.entitlement.latest", "cpu.usagemhz.none", "cpu.demand.average"} //counterNames := []string{"cpu.entitlement.latest"} // Retrieve counters name list counters, err := perfManager.CounterInfoByName(ctx) if err != nil { //return err fmt.Printf("Error getting counter info : '%s'\n", err) return } fmt.Printf("Found '%d' counters\n", len(counters)) for _, counter := range counters { perfCounterNames = append(perfCounterNames, counter.Name()) for _, counterName := range counterNames { if counter.Name() == counterName { groupInfo := counter.GroupInfo.GetElementDescription() nameInfo := counter.NameInfo.GetElementDescription() fmt.Printf("Found counter matching name '%s' : '%s';'%s'\n", counterName, groupInfo, nameInfo) perfCounterInfos = append(perfCounterInfos, *counter) } else { log.Printf("Ignoring '%s' : '%s'\n", counter.StatsType, counter.Name()) } } } fmt.Printf("Ended search with %d counter infos\n", len(perfCounterInfos)) // Find the VM by its name vmMatches, err := findVMByName(ctx, c, vmName) if err != nil { fmt.Println("Error finding VM:", err) return } //fmt.Printf("Processing %d vm matches : %v\n", len(vmMatches), vmMatches) var vmReferences []types.ManagedObjectReference for _, vm := range vmMatches { vmReferences = append(vmReferences, vm.Reference()) } //fmt.Printf("Processing %d vm references : %v\n", len(vmReferences), vmReferences) // Create PerfQuerySpec as per https://github.com/vmware/govmomi/blob/main/performance/example_test.go spec := types.PerfQuerySpec{ //MaxSample: 180, // In one hour there are 180 instances of 20 second intervals MetricId: getMetricIds(perfCounterInfos), IntervalId: 20, StartTime: &startTime, // 1 hour ago EndTime: &timeNow, } // Query metrics //sample, err := perfManager.SampleByName(ctx, spec, counterNames, []types.ManagedObjectReference{vm.Reference()}) /* sample, err := perfManager.SampleByName(ctx, spec, counterNames, vmReferences) if err != nil { fmt.Printf("Error getting SampleByName : '%s'\n", err) return } fmt.Printf("Sample result is %d\n", len(sample)) */ var query []types.PerfQuerySpec for _, e := range vmMatches { spec.Entity = e.Reference() query = append(query, spec) } sample, err := perfManager.Query(ctx, query) if err != nil { fmt.Printf("Error running query : '%s'\n", err) return } /* for _, v := range sample { fmt.Printf("%v\n", v.GetPerfEntityMetricBase().Entity) } */ allCounters, err := perfManager.CounterInfoByKey(ctx) if err != nil { fmt.Printf("Error getting CounterInfoByKey : '%s'\n", err) return } var result []performance.EntityMetric for i := range sample { fmt.Printf("Processing sample %d\n", i) var values []performance.MetricSeries s, ok := sample[i].(*types.PerfEntityMetric) if !ok { panic(fmt.Errorf("expected type %T, got: %T", s, sample[i])) } fmt.Printf("Found %d samples\n", len(s.Value)) //fmt.Printf("%v\n", sample[i]) for j := range s.Value { v := s.Value[j].(*types.PerfMetricIntSeries) info, ok := allCounters[v.Id.CounterId] if !ok { continue } vm := object.NewVirtualMachine(c, s.Entity) entityName, err := vm.ObjectName(ctx) if err != nil { fmt.Printf("Error getting vm name : '%s'\n", err) return } fmt.Printf("%s\t%s\t%s\t%v (%s)\n", entityName, info.Name(), v.Id.Instance, v.Value, info.UnitInfo.GetElementDescription().Key) /* values = append(values, performance.MetricSeries{ Name: info.Name(), unit: info.UnitInfo.GetElementDescription().Key, Instance: v.Id.Instance, Value: v.Value, }) */ } result = append(result, performance.EntityMetric{ Entity: s.Entity, SampleInfo: s.SampleInfo, Value: values, }) } /* result, err := perfManager.ToMetricSeries(ctx, sample) if err != nil { fmt.Printf("Error getting ToMetricSeries : '%s'\n", err) return } fmt.Printf("Retrieved %d metric series\n", len(result)) // Read result for _, metric := range result { vm := object.NewVirtualMachine(c, metric.Entity) name, err := vm.ObjectName(ctx) if err != nil { fmt.Printf("Error getting vm name : '%s'\n", err) return } fmt.Printf("Vm %s has %d metric values : %v\n", name, len(metric.Value), metric) for i, v := range metric.Value { fmt.Printf("Processing [%d] : '%v'\n", i, v) counter := counters[v.Name] units := counter.UnitInfo.GetElementDescription().Label instance := v.Instance if instance == "" { instance = "-" } if len(v.Value) != 0 { //fmt.Printf("%s\t%s\t%s\t%s : %v\n", name, instance, v.Name, units, v.ValueCSV()) fmt.Printf("%s\t%s\t%s\t%s : %v\n", name, instance, v.Name, units, v.Value) } } } fmt.Printf("Finished receiving result.\n") */ /* // Create a query specification query := perfManager.QueryPerf(ctx, &vim25.QueryPerf{ Entity: []vim25.ManagedObjectReference{vm.Reference()}, MaxSample: 1, MetricId: getMetricIds(perfCounterInfos), IntervalId: 20, // 20-second interval StartTime: time.Now().Add(-time.Hour), // 1 hour ago EndTime: time.Now(), }) // Process the query result for _, perf := range query { fmt.Println("VM:", vmName) for i, value := range perf.Value { counterInfo := perfCounterInfos[i] fmt.Printf("%s: %v\n", counterInfo.NameInfo.GetElement().Key, value.Value[0]) } } */ } // From https://stackoverflow.com/a/33769379 func Scan(d interface{}) { v := reflect.ValueOf(d) i := reflect.Indirect(v) s := i.Type() println(s.NumField()) // will print out 0, if you change Host to have 1 field, it prints out 1 } func query(t reflect.Type) { value := reflect.New(t).Interface() Scan(value) } func main() { // Command line flags vURL := flag.String("url", "", "The URL of a vCenter server, eg https://server.domain.example/sdk") vUser := flag.String("user", "", "The username to use when connecting to vCenter") vPass := flag.String("password", "", "The password to use when connecting to vCenter") vTZ := flag.String("tz", "Australia/Sydney", "The timezone to use when converting vCenter UTC times") vInsecure := flag.Bool("insecure", true, "Allow insecure connections to vCenter") vmName := flag.String("vmname", "example-vm", "The vm to query metrics") //begin := flag.Duration("b", time.Hour, "Begin time") // default BeginTime is 1h ago flag.Parse() // Print logs to file f, err := os.OpenFile("log.txt", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { log.Fatalf("error opening file: %v", err) } defer f.Close() log.SetOutput(f) log.Printf("Starting execution. Built on %s from sha1 %s\n", buildTime, sha1ver) // So we can convert vCenter UTC to our local timezone log.Printf("Setting timezone to '%s'\n", *vTZ) location, err = time.LoadLocation(*vTZ) if err != nil { fmt.Fprintf(os.Stderr, "Error setting timezone to %s : %s\n", *vTZ, err) os.Exit(1) } u, err := url.Parse(*vURL) if err != nil { fmt.Fprintf(os.Stderr, "Error parsing url %s : %s\n", *vURL, err) os.Exit(1) } else { if !strings.HasSuffix(u.Path, "/sdk") { u.Path, _ = url.JoinPath(u.Path, "/sdk") log.Printf("Updated vCenter URL to '%v'\n", u) } } log.Printf("Connecting to vCenter %s\n", u) u.User = url.UserPassword(*vUser, *vPass) ctx, cancel = context.WithCancel(context.Background()) defer cancel() // Login to vcenter c, err = govmomi.NewClient(ctx, u, *vInsecure) if err != nil { fmt.Fprintf(os.Stderr, "Logging in error: %s\n", err) os.Exit(1) } defer c.Logout(ctx) getSpecificVMMetrics(ctx, c.Client, *vmName) //err = getNumVmsPerDrsScoreBucket(c) if err != nil { log.Printf("Error retrieving NumVmsPerDrsScoreBucket: %s\n", err) return } // Output final results in JSON if len(results) > 0 { j, _ := json.Marshal(results) fmt.Println(string(j)) } else { fmt.Println("{}") } }