Add support for custom attributes to vsphere input (#5971)

This commit is contained in:
Pontus Rydin 2019-08-14 20:03:33 -04:00 committed by Daniel Nelson
parent 5e0c63f2e6
commit 2755595019
6 changed files with 205 additions and 57 deletions

View File

@ -118,9 +118,13 @@ vm_metric_exclude = [ "*" ]
"storageAdapter.write.average", "storageAdapter.write.average",
"sys.uptime.latest", "sys.uptime.latest",
] ]
## Collect IP addresses? Valid values are "ipv4" and "ipv6"
# ip_addresses = ["ipv6", "ipv4" ]
# host_metric_exclude = [] ## Nothing excluded by default # host_metric_exclude = [] ## Nothing excluded by default
# host_instances = true ## true by default # host_instances = true ## true by default
## Clusters ## Clusters
# cluster_include = [ "/*/host/**"] # Inventory path to clusters to collect (by default all are collected) # cluster_include = [ "/*/host/**"] # Inventory path to clusters to collect (by default all are collected)
# cluster_metric_include = [] ## if omitted or empty, all metrics are collected # cluster_metric_include = [] ## if omitted or empty, all metrics are collected
@ -173,6 +177,17 @@ vm_metric_exclude = [ "*" ]
## the plugin. Setting this flag to "false" will send values as floats to ## the plugin. Setting this flag to "false" will send values as floats to
## preserve the full precision when averaging takes place. ## preserve the full precision when averaging takes place.
# use_int_samples = true # use_int_samples = true
## Custom attributes from vCenter can be very useful for queries in order to slice the
## metrics along different dimension and for forming ad-hoc relationships. They are disabled
## by default, since they can add a considerable amount of tags to the resulting metrics. To
## enable, simply set custom_attribute_exlude to [] (empty set) and use custom_attribute_include
## to select the attributes you want to include.
# by default, since they can add a considerable amount of tags to the resulting metrics. To
# enable, simply set custom_attribute_exlude to [] (empty set) and use custom_attribute_include
# to select the attributes you want to include.
# custom_attribute_include = []
# custom_attribute_exclude = ["*"] # Default is to exclude everything
## Optional SSL Config ## Optional SSL Config
# ssl_ca = "/path/to/cafile" # ssl_ca = "/path/to/cafile"
@ -241,7 +256,7 @@ to a file system. A vSphere inventory has a structure similar to this:
#### Using Inventory Paths #### Using Inventory Paths
Using familiar UNIX-style paths, one could select e.g. VM2 with the path ```/DC0/vm/VM2```. Using familiar UNIX-style paths, one could select e.g. VM2 with the path ```/DC0/vm/VM2```.
Often, we want to select a group of resource, such as all the VMs in a folder. We could use the path ```/DC0/vm/Folder1/*``` for that. Often, we want to select a group of resource, such as all the VMs in a folder. We could use the path ```/DC0/vm/Folder1/*``` for that.
Another possibility is to select objects using a partial name, such as ```/DC0/vm/Folder1/hadoop*``` yielding all vms in Folder1 with a name starting with "hadoop". Another possibility is to select objects using a partial name, such as ```/DC0/vm/Folder1/hadoop*``` yielding all vms in Folder1 with a name starting with "hadoop".

View File

@ -305,3 +305,18 @@ func (c *Client) ListResources(ctx context.Context, root *view.ContainerView, ki
defer cancel1() defer cancel1()
return root.Retrieve(ctx1, kind, ps, dst) return root.Retrieve(ctx1, kind, ps, dst)
} }
func (c *Client) GetCustomFields(ctx context.Context) (map[int32]string, error) {
ctx1, cancel1 := context.WithTimeout(ctx, c.Timeout)
defer cancel1()
cfm := object.NewCustomFieldsManager(c.Client.Client)
fields, err := cfm.Field(ctx1)
if err != nil {
return nil, err
}
r := make(map[int32]string)
for _, f := range fields {
r[f.Key] = f.Name
}
return r, nil
}

View File

@ -26,6 +26,10 @@ import (
var isolateLUN = regexp.MustCompile(".*/([^/]+)/?$") var isolateLUN = regexp.MustCompile(".*/([^/]+)/?$")
var isIPv4 = regexp.MustCompile("^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$")
var isIPv6 = regexp.MustCompile("^(?:[A-Fa-f0-9]{0,4}:){1,7}[A-Fa-f0-9]{1,4}$")
const metricLookback = 3 // Number of time periods to look back at for non-realtime metrics const metricLookback = 3 // Number of time periods to look back at for non-realtime metrics
const rtMetricLookback = 3 // Number of time periods to look back at for realtime metrics const rtMetricLookback = 3 // Number of time periods to look back at for realtime metrics
@ -37,16 +41,19 @@ const maxMetadataSamples = 100 // Number of resources to sample for metric metad
// Endpoint is a high-level representation of a connected vCenter endpoint. It is backed by the lower // Endpoint is a high-level representation of a connected vCenter endpoint. It is backed by the lower
// level Client type. // level Client type.
type Endpoint struct { type Endpoint struct {
Parent *VSphere Parent *VSphere
URL *url.URL URL *url.URL
resourceKinds map[string]*resourceKind resourceKinds map[string]*resourceKind
hwMarks *TSCache hwMarks *TSCache
lun2ds map[string]string lun2ds map[string]string
discoveryTicker *time.Ticker discoveryTicker *time.Ticker
collectMux sync.RWMutex collectMux sync.RWMutex
initialized bool initialized bool
clientFactory *ClientFactory clientFactory *ClientFactory
busy sync.Mutex busy sync.Mutex
customFields map[int32]string
customAttrFilter filter.Filter
customAttrEnabled bool
} }
type resourceKind struct { type resourceKind struct {
@ -80,12 +87,14 @@ type metricEntry struct {
type objectMap map[string]objectRef type objectMap map[string]objectRef
type objectRef struct { type objectRef struct {
name string name string
altID string altID string
ref types.ManagedObjectReference ref types.ManagedObjectReference
parentRef *types.ManagedObjectReference //Pointer because it must be nillable parentRef *types.ManagedObjectReference //Pointer because it must be nillable
guest string guest string
dcname string dcname string
customValues map[string]string
lookup map[string]string
} }
func (e *Endpoint) getParent(obj *objectRef, res *resourceKind) (*objectRef, bool) { func (e *Endpoint) getParent(obj *objectRef, res *resourceKind) (*objectRef, bool) {
@ -101,12 +110,14 @@ func (e *Endpoint) getParent(obj *objectRef, res *resourceKind) (*objectRef, boo
// as parameters. // as parameters.
func NewEndpoint(ctx context.Context, parent *VSphere, url *url.URL) (*Endpoint, error) { func NewEndpoint(ctx context.Context, parent *VSphere, url *url.URL) (*Endpoint, error) {
e := Endpoint{ e := Endpoint{
URL: url, URL: url,
Parent: parent, Parent: parent,
hwMarks: NewTSCache(1 * time.Hour), hwMarks: NewTSCache(1 * time.Hour),
lun2ds: make(map[string]string), lun2ds: make(map[string]string),
initialized: false, initialized: false,
clientFactory: NewClientFactory(ctx, url, parent), clientFactory: NewClientFactory(ctx, url, parent),
customAttrFilter: newFilterOrPanic(parent.CustomAttributeInclude, parent.CustomAttributeExclude),
customAttrEnabled: anythingEnabled(parent.CustomAttributeExclude),
} }
e.resourceKinds = map[string]*resourceKind{ e.resourceKinds = map[string]*resourceKind{
@ -259,6 +270,20 @@ func (e *Endpoint) initalDiscovery(ctx context.Context) {
} }
func (e *Endpoint) init(ctx context.Context) error { func (e *Endpoint) init(ctx context.Context) error {
client, err := e.clientFactory.GetClient(ctx)
if err != nil {
return err
}
// Initial load of custom field metadata
if e.customAttrEnabled {
fields, err := client.GetCustomFields(ctx)
if err != nil {
log.Println("W! [inputs.vsphere] Could not load custom field metadata")
} else {
e.customFields = fields
}
}
if e.Parent.ObjectDiscoveryInterval.Duration > 0 { if e.Parent.ObjectDiscoveryInterval.Duration > 0 {
@ -427,6 +452,16 @@ func (e *Endpoint) discover(ctx context.Context) error {
} }
} }
// Load custom field metadata
var fields map[int32]string
if e.customAttrEnabled {
fields, err = client.GetCustomFields(ctx)
if err != nil {
log.Println("W! [inputs.vsphere] Could not load custom field metadata")
fields = nil
}
}
// Atomically swap maps // Atomically swap maps
e.collectMux.Lock() e.collectMux.Lock()
defer e.collectMux.Unlock() defer e.collectMux.Unlock()
@ -436,6 +471,10 @@ func (e *Endpoint) discover(ctx context.Context) error {
} }
e.lun2ds = l2d e.lun2ds = l2d
if fields != nil {
e.customFields = fields
}
sw.Stop() sw.Stop()
SendInternalCounterWithTags("discovered_objects", e.URL.Host, map[string]string{"type": "instance-total"}, numRes) SendInternalCounterWithTags("discovered_objects", e.URL.Host, map[string]string{"type": "instance-total"}, numRes)
return nil return nil
@ -609,14 +648,77 @@ func getVMs(ctx context.Context, e *Endpoint, filter *ResourceFilter) (objectMap
} }
guest := "unknown" guest := "unknown"
uuid := "" uuid := ""
lookup := make(map[string]string)
// Extract host name
if r.Guest != nil && r.Guest.HostName != "" {
lookup["guesthostname"] = r.Guest.HostName
}
// Collect network information
for _, net := range r.Guest.Net {
if net.DeviceConfigId == -1 {
continue
}
if net.IpConfig == nil || net.IpConfig.IpAddress == nil {
continue
}
ips := make(map[string][]string)
for _, ip := range net.IpConfig.IpAddress {
addr := ip.IpAddress
for _, ipType := range e.Parent.IpAddresses {
if !(ipType == "ipv4" && isIPv4.MatchString(addr) ||
ipType == "ipv6" && isIPv6.MatchString(addr)) {
continue
}
// By convention, we want the preferred addresses to appear first in the array.
if _, ok := ips[ipType]; !ok {
ips[ipType] = make([]string, 0)
}
if ip.State == "preferred" {
ips[ipType] = append([]string{addr}, ips[ipType]...)
} else {
ips[ipType] = append(ips[ipType], addr)
}
}
}
for ipType, ipList := range ips {
lookup["nic/"+strconv.Itoa(int(net.DeviceConfigId))+"/"+ipType] = strings.Join(ipList, ",")
}
}
// Sometimes Config is unknown and returns a nil pointer // Sometimes Config is unknown and returns a nil pointer
//
if r.Config != nil { if r.Config != nil {
guest = cleanGuestID(r.Config.GuestId) guest = cleanGuestID(r.Config.GuestId)
uuid = r.Config.Uuid uuid = r.Config.Uuid
} }
cvs := make(map[string]string)
if e.customAttrEnabled {
for _, cv := range r.Summary.CustomValue {
val := cv.(*types.CustomFieldStringValue)
if val.Value == "" {
continue
}
key, ok := e.customFields[val.Key]
if !ok {
log.Printf("W! [inputs.vsphere] Metadata for custom field %d not found. Skipping", val.Key)
continue
}
if e.customAttrFilter.Match(key) {
cvs[key] = val.Value
}
}
}
m[r.ExtensibleManagedObject.Reference().Value] = objectRef{ m[r.ExtensibleManagedObject.Reference().Value] = objectRef{
name: r.Name, ref: r.ExtensibleManagedObject.Reference(), parentRef: r.Runtime.Host, guest: guest, altID: uuid} name: r.Name,
ref: r.ExtensibleManagedObject.Reference(),
parentRef: r.Runtime.Host,
guest: guest,
altID: uuid,
customValues: cvs,
lookup: lookup,
}
} }
return m, nil return m, nil
} }
@ -1032,6 +1134,9 @@ func (e *Endpoint) populateTags(objectRef *objectRef, resourceType string, resou
if objectRef.guest != "" { if objectRef.guest != "" {
t["guest"] = objectRef.guest t["guest"] = objectRef.guest
} }
if gh := objectRef.lookup["guesthostname"]; gh != "" {
t["guesthostname"] = gh
}
if c, ok := e.resourceKinds["cluster"].objects[parent.parentRef.Value]; ok { if c, ok := e.resourceKinds["cluster"].objects[parent.parentRef.Value]; ok {
t["clustername"] = c.name t["clustername"] = c.name
} }
@ -1062,6 +1167,17 @@ func (e *Endpoint) populateTags(objectRef *objectRef, resourceType string, resou
t["disk"] = cleanDiskTag(instance) t["disk"] = cleanDiskTag(instance)
} else if strings.HasPrefix(name, "net.") { } else if strings.HasPrefix(name, "net.") {
t["interface"] = instance t["interface"] = instance
// Add IP addresses to NIC data.
if resourceType == "vm" && objectRef.lookup != nil {
key := "nic/" + t["interface"] + "/"
if ip, ok := objectRef.lookup[key+"ipv6"]; ok {
t["ipv6"] = ip
}
if ip, ok := objectRef.lookup[key+"ipv4"]; ok {
t["ipv4"] = ip
}
}
} else if strings.HasPrefix(name, "storageAdapter.") { } else if strings.HasPrefix(name, "storageAdapter.") {
t["adapter"] = instance t["adapter"] = instance
} else if strings.HasPrefix(name, "storagePath.") { } else if strings.HasPrefix(name, "storagePath.") {
@ -1076,6 +1192,15 @@ func (e *Endpoint) populateTags(objectRef *objectRef, resourceType string, resou
// default // default
t["instance"] = v.Instance t["instance"] = v.Instance
} }
// Fill in custom values if they exist
if objectRef.customValues != nil {
for k, v := range objectRef.customValues {
if v != "" {
t[k] = v
}
}
}
} }
func (e *Endpoint) makeMetricIdentifier(prefix, metric string) (string, string) { func (e *Endpoint) makeMetricIdentifier(prefix, metric string) (string, string) {

View File

@ -231,8 +231,9 @@ func init() {
} }
addFields = map[string][]string{ addFields = map[string][]string{
"HostSystem": {"parent"}, "HostSystem": {"parent"},
"VirtualMachine": {"runtime.host", "config.guestId", "config.uuid", "runtime.powerState"}, "VirtualMachine": {"runtime.host", "config.guestId", "config.uuid", "runtime.powerState",
"summary.customValue", "guest.net", "guest.hostName"},
"Datastore": {"parent", "info"}, "Datastore": {"parent", "info"},
"ClusterComputeResource": {"parent"}, "ClusterComputeResource": {"parent"},
"Datacenter": {"parent"}, "Datacenter": {"parent"},

View File

@ -40,7 +40,10 @@ type VSphere struct {
DatastoreMetricExclude []string DatastoreMetricExclude []string
DatastoreInclude []string DatastoreInclude []string
Separator string Separator string
CustomAttributeInclude []string
CustomAttributeExclude []string
UseIntSamples bool UseIntSamples bool
IpAddresses []string
MaxQueryObjects int MaxQueryObjects int
MaxQueryMetrics int MaxQueryMetrics int
@ -155,6 +158,8 @@ var sampleConfig = `
"storageAdapter.write.average", "storageAdapter.write.average",
"sys.uptime.latest", "sys.uptime.latest",
] ]
## Collect IP addresses? Valid values are "ipv4" and "ipv6"
# ip_addresses = ["ipv6", "ipv4" ]
# host_metric_exclude = [] ## Nothing excluded by default # host_metric_exclude = [] ## Nothing excluded by default
# host_instances = true ## true by default # host_instances = true ## true by default
@ -173,7 +178,7 @@ var sampleConfig = `
datacenter_metric_exclude = [ "*" ] ## Datacenters are not collected by default. datacenter_metric_exclude = [ "*" ] ## Datacenters are not collected by default.
# datacenter_instances = false ## false by default for Datastores only # datacenter_instances = false ## false by default for Datastores only
## Plugin Settings ## Plugin Settings
## separator character to use for measurement and field names (default: "_") ## separator character to use for measurement and field names (default: "_")
# separator = "_" # separator = "_"
@ -208,6 +213,14 @@ var sampleConfig = `
## preserve the full precision when averaging takes place. ## preserve the full precision when averaging takes place.
# use_int_samples = true # use_int_samples = true
## Custom attributes from vCenter can be very useful for queries in order to slice the
## metrics along different dimension and for forming ad-hoc relationships. They are disabled
## by default, since they can add a considerable amount of tags to the resulting metrics. To
## enable, simply set custom_attribute_exlude to [] (empty set) and use custom_attribute_include
## to select the attributes you want to include.
# custom_attribute_include = []
# custom_attribute_exclude = ["*"]
## Optional SSL Config ## Optional SSL Config
# ssl_ca = "/path/to/cafile" # ssl_ca = "/path/to/cafile"
# ssl_cert = "/path/to/certfile" # ssl_cert = "/path/to/certfile"
@ -321,7 +334,10 @@ func init() {
DatastoreMetricExclude: nil, DatastoreMetricExclude: nil,
DatastoreInclude: []string{"/*/datastore/**"}, DatastoreInclude: []string{"/*/datastore/**"},
Separator: "_", Separator: "_",
CustomAttributeInclude: []string{},
CustomAttributeExclude: []string{"*"},
UseIntSamples: true, UseIntSamples: true,
IpAddresses: []string{},
MaxQueryObjects: 256, MaxQueryObjects: 256,
MaxQueryMetrics: 256, MaxQueryMetrics: 256,

View File

@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"regexp" "regexp"
"sort" "sort"
"strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"testing" "testing"
@ -256,34 +255,6 @@ func TestThrottledExecutor(t *testing.T) {
require.Equal(t, int64(5), max, "Wrong number of goroutines spawned") require.Equal(t, int64(5), max, "Wrong number of goroutines spawned")
} }
func TestTimeout(t *testing.T) {
// Don't run test on 32-bit machines due to bug in simulator.
// https://github.com/vmware/govmomi/issues/1330
var i int
if unsafe.Sizeof(i) < 8 {
return
}
m, s, err := createSim()
if err != nil {
t.Fatal(err)
}
defer m.Remove()
defer s.Close()
v := defaultVSphere()
var acc testutil.Accumulator
v.Vcenters = []string{s.URL.String()}
v.Timeout = internal.Duration{Duration: 1 * time.Nanosecond}
require.NoError(t, v.Start(nil)) // We're not using the Accumulator, so it can be nil.
defer v.Stop()
err = v.Gather(&acc)
// The accumulator must contain exactly one error and it must be a deadline exceeded.
require.Equal(t, 1, len(acc.Errors))
require.True(t, strings.Contains(acc.Errors[0].Error(), "context deadline exceeded"))
}
func TestMaxQuery(t *testing.T) { func TestMaxQuery(t *testing.T) {
// Don't run test on 32-bit machines due to bug in simulator. // Don't run test on 32-bit machines due to bug in simulator.
// https://github.com/vmware/govmomi/issues/1330 // https://github.com/vmware/govmomi/issues/1330
@ -414,6 +385,11 @@ func TestFinder(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 2, len(vm)) require.Equal(t, 2, len(vm))
vm = []mo.VirtualMachine{}
err = f.Find(ctx, "VirtualMachine", "/DC0/**/DC0_H0_VM*", &vm)
require.NoError(t, err)
require.Equal(t, 2, len(vm))
vm = []mo.VirtualMachine{} vm = []mo.VirtualMachine{}
err = f.Find(ctx, "VirtualMachine", "/**/vm/**", &vm) err = f.Find(ctx, "VirtualMachine", "/**/vm/**", &vm)
require.NoError(t, err) require.NoError(t, err)