Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ The options available are:
| -collector.cluster | Enables or disables Cluster metrics collection (default: enabled) |
| -collector.datastore | Enables or disables Datastore metrics collection (default: enabled) |
| -collector.host | Enables or disables Host metrics collection (default: enabled) |
| -collector.vm | Enables or disables Virtual Machine metrics collection (default: enabled) |
| -collector.vm | Enables or disables Virtual Machine metrics collection (default: enabled). Also emits `vmware_vm_disk_capacity{vmmo,vm,disk,label}` = configured size in bytes for every virtual disk, where `disk` is a stable 0-based index (controller bus, then unit number) and `label` is the vSphere device label (e.g. "Hard disk 1") |
| -collector.tags | Enables or disables vSphere tag collection (default: enabled). Emits `vmware_vm_tag{vmmo,vm,category,tag}` = 1 for every tag attached to a VM - join it onto other VM metrics via the `vmmo` label |
| -collector.esxcli.host.nic | Collects ESXi NIC firmware information using esxcli invoked through the vCenter (default: disabled) |
| -collector.esxcli.storage | Collects ESXi storage firmware information using esxcli invoked through the vCenter (default: disabled) |
| -vmware.granularity | The frequency of the sampled data. Default is 20s (default 20) |
Expand Down
36 changes: 36 additions & 0 deletions vmware/api/vmware.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ import (
"log/slog"
"net/http"
"net/url"
"strconv"
"time"

"github.com/prezhdarov/prometheus-exporter/pkg/collector"

"github.com/vmware/govmomi/performance"
"github.com/vmware/govmomi/session/cache"
"github.com/vmware/govmomi/vapi/rest"
"github.com/vmware/govmomi/vapi/tags"
"github.com/vmware/govmomi/view"
"github.com/vmware/govmomi/vim25"
"github.com/vmware/govmomi/vim25/soap"
Expand Down Expand Up @@ -90,6 +93,12 @@ func (vm *VMware) Login(target string, logger *slog.Logger) (map[string]interfac

func (vm *VMware) Logout(loginData map[string]interface{}, logger *slog.Logger) error {

if restClient, ok := loginData["rest"].(*rest.Client); ok {
if err := restClient.Logout(loginData["ctx"].(context.Context)); err != nil {
logger.Debug("rest logout failed", "target", loginData["target"], "err", err)
}
}

/*
url := fmt.Sprintf("%s://%s/api/session", *vmwSchema, loginData["target"].(string))

Expand Down Expand Up @@ -248,5 +257,32 @@ func govmomiLogin(loginData map[string]interface{}) error {
loginData["interval"] = int32(*vmwInterval)
loginData["samples"] = int32(*vmwInterval / *vmGranularity)

//Tags live behind the vAPI REST endpoint, which needs its own session on top of the SOAP one
if tagsCollectorEnabled() {
restClient := rest.NewClient(client)

if err := restClient.Login(ctx, urlx.User); err != nil {
cancel()
return fmt.Errorf("rest login err: %s", err)
}

loginData["rest"] = restClient
loginData["tags"] = tags.NewManager(restClient)
}

return nil
}

// tagsCollectorEnabled peeks at the tags collector flag registered in the collectors
// package, so the REST session is only established when someone will use it
func tagsCollectorEnabled() bool {

tagsFlag := flag.Lookup("collector.tags")
if tagsFlag == nil {
return false
}

enabled, err := strconv.ParseBool(tagsFlag.Value.String())

return err == nil && enabled
}
107 changes: 107 additions & 0 deletions vmware/collectors/tags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package vmwareCollectors

import (
"context"
"flag"
"fmt"
"log/slog"
"time"

"github.com/prezhdarov/prometheus-exporter/pkg/collector"
"github.com/prometheus/client_golang/prometheus"
"github.com/vmware/govmomi/vapi/tags"
"github.com/vmware/govmomi/view"
"github.com/vmware/govmomi/vim25"
"github.com/vmware/govmomi/vim25/mo"
)

const (
tagsSubsystem = "tags"
//vCenter caps vAPI list-attached-tags-on-objects requests, so ask in batches
tagsBatchSize = 500
)

var tagsCollectorFlag = flag.Bool(fmt.Sprintf("collector.%s", tagsSubsystem), collector.DefaultEnabled, fmt.Sprintf("Enable the %s collector (default: %v)", tagsSubsystem, collector.DefaultEnabled))

type tagsCollector struct {
logger *slog.Logger
}

func init() {
collector.RegisterCollector(tagsSubsystem, tagsCollectorFlag, NewtagsCollector)
}

// NewtagsCollector returns a new Collector exposing vSphere tag assignments.
func NewtagsCollector(logger *slog.Logger) (collector.Collector, error) {
return &tagsCollector{logger}, nil
}

func (c *tagsCollector) Update(ch chan<- prometheus.Metric, namespace string, clientAPI collector.ClientAPI, loginData map[string]interface{}, params map[string]string) error {

tagsManager, ok := loginData["tags"].(*tags.Manager)
if !ok {
return fmt.Errorf("no tags manager in login data - has the vAPI REST login succeeded?")
}

ctx := loginData["ctx"].(context.Context)

var vms []mo.VirtualMachine

err := fetchProperties(
ctx, loginData["view"].(*view.Manager), loginData["client"].(*vim25.Client),
[]string{"VirtualMachine"}, []string{"name"}, &vms, c.logger,
)
if err != nil {
return err
}

begin := time.Now()

categories, err := tagsManager.GetCategories(ctx)
if err != nil {
return fmt.Errorf("tag categories err: %s", err)
}

categoryNames := make(map[string]string, len(categories))
for _, category := range categories {
categoryNames[category.ID] = category.Name
}

vmNames := make(map[string]string, len(vms))
vmRefs := make([]mo.Reference, 0, len(vms))

for _, vm := range vms {
vmNames[vm.Self.Value] = vm.Name
vmRefs = append(vmRefs, vm.Self)
}

for start := 0; start < len(vmRefs); start += tagsBatchSize {
end := min(start+tagsBatchSize, len(vmRefs))

attached, err := tagsManager.GetAttachedTagsOnObjects(ctx, vmRefs[start:end])
if err != nil {
return fmt.Errorf("attached tags err: %s", err)
}

for _, object := range attached {
objectRef := object.ObjectID.Reference()

for _, tag := range object.Tags {

ch <- prometheus.MustNewConstMetric(
prometheus.NewDesc(
prometheus.BuildFQName(namespace, "vm", "tag"),
"vSphere tag attached to this virtual machine. Value is always 1 - join on vmmo", nil,
map[string]string{"vmmo": objectRef.Value, "vm": vmNames[objectRef.Value],
"category": categoryNames[tag.CategoryID], "tag": tag.Name,
"vcenter": loginData["target"].(string)},
), prometheus.GaugeValue, 1.0,
)
}
}
}

c.logger.Debug("time to fetch vm tags", "vms", len(vmRefs), "duration_seconds", time.Since(begin).Seconds())

return nil
}
62 changes: 60 additions & 2 deletions vmware/collectors/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"flag"
"fmt"
"log/slog"
"sort"
"strconv"
"sync"
"time"

Expand Down Expand Up @@ -58,7 +60,7 @@ func (c *vmCollector) Update(ch chan<- prometheus.Metric, namespace string, clie

err := fetchProperties(
loginData["ctx"].(context.Context), loginData["view"].(*view.Manager), loginData["client"].(*vim25.Client),
[]string{"VirtualMachine"}, []string{"summary", "runtime", "storage", "snapshot", "snapshot.rootSnapshotList", "snapshot.currentSnapshot"}, &vms, c.logger,
[]string{"VirtualMachine"}, []string{"summary", "runtime", "storage", "snapshot", "snapshot.rootSnapshotList", "snapshot.currentSnapshot", "config.hardware.device"}, &vms, c.logger,
)
if err != nil {
return err
Expand Down Expand Up @@ -104,12 +106,68 @@ func (c *vmCollector) Update(ch chan<- prometheus.Metric, namespace string, clie
ch <- prometheus.MustNewConstMetric(
prometheus.NewDesc(
prometheus.BuildFQName(namespace, vmSubsystem, "datastore_capacity_used"),
"Virtual memory configured in MB", nil,
"Capacity used by the VM on this datastore in bytes", nil,
map[string]string{"vmmo": vm.Self.Value, "vm": vm.Summary.Config.Name,
"vcenter": loginData["target"].(string), "dsmo": datastore.Datastore.Value},
), prometheus.GaugeValue, float64(datastore.Committed),
)
}

if vm.Config != nil {

busNumbers := make(map[int32]int32)
disks := make([]*types.VirtualDisk, 0)

for _, device := range vm.Config.Hardware.Device {
if ctrl, ok := device.(types.BaseVirtualController); ok {
vc := ctrl.GetVirtualController()
busNumbers[vc.Key] = vc.BusNumber
}
if disk, ok := device.(*types.VirtualDisk); ok {
disks = append(disks, disk)
}
}

// Stable "disk 0, disk 1, ..." ordering: controller bus, then unit number, then device key
sort.Slice(disks, func(i, j int) bool {
if bi, bj := busNumbers[disks[i].ControllerKey], busNumbers[disks[j].ControllerKey]; bi != bj {
return bi < bj
}
var ui, uj int32
if disks[i].UnitNumber != nil {
ui = *disks[i].UnitNumber
}
if disks[j].UnitNumber != nil {
uj = *disks[j].UnitNumber
}
if ui != uj {
return ui < uj
}
return disks[i].Key < disks[j].Key
})

for i, disk := range disks {

capacity := disk.CapacityInBytes
if capacity == 0 {
capacity = disk.CapacityInKB * 1024
}

label := ""
if disk.DeviceInfo != nil {
label = disk.DeviceInfo.GetDescription().Label
}

ch <- prometheus.MustNewConstMetric(
prometheus.NewDesc(
prometheus.BuildFQName(namespace, vmSubsystem, "disk_capacity"),
"Configured virtual disk capacity in bytes", nil,
map[string]string{"vmmo": vm.Self.Value, "vm": vm.Summary.Config.Name,
"vcenter": loginData["target"].(string), "disk": strconv.Itoa(i), "label": label},
), prometheus.GaugeValue, float64(capacity),
)
}
}
// Check if the VM has any snapshots, set value of metric to unix timestamp of snapshot creation time
if vm.Snapshot != nil {
c.logger.Debug("vm has snapshots", "vm", vm.Summary.Config.Name, "vm_moref", vm.Self.Value)
Expand Down