Adding upstream version 1.34.4.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
e393c3af3f
commit
4978089aab
4963 changed files with 677545 additions and 0 deletions
383
plugins/inputs/win_wmi/README.md
Normal file
383
plugins/inputs/win_wmi/README.md
Normal file
|
@ -0,0 +1,383 @@
|
|||
# Windows Management Instrumentation Input Plugin
|
||||
|
||||
This document presents the input plugin to read WMI classes on Windows
|
||||
operating systems. With the win_wmi plugin, it is possible to
|
||||
capture and filter virtually any configuration or metric value exposed
|
||||
through the Windows Management Instrumentation ([WMI][WMIdoc])
|
||||
service. At minimum, the telegraf service user must have permission
|
||||
to [read][ACL] the WMI namespace that is being queried.
|
||||
|
||||
[ACL]: https://learn.microsoft.com/en-us/windows/win32/wmisdk/access-to-wmi-namespaces
|
||||
[WMIdoc]: https://learn.microsoft.com/en-us/windows/win32/wmisdk/wmi-start-page
|
||||
|
||||
## Global configuration options <!-- @/docs/includes/plugin_config.md -->
|
||||
|
||||
In addition to the plugin-specific configuration settings, plugins support
|
||||
additional global and plugin configuration settings. These settings are used to
|
||||
modify metrics, tags, and field or create aliases and configure ordering, etc.
|
||||
See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
|
||||
|
||||
[CONFIGURATION.md]: ../../../docs/CONFIGURATION.md#plugins
|
||||
|
||||
## Secret-store support
|
||||
|
||||
This plugin supports secrets from secret-stores for the `username` and
|
||||
`password` option.
|
||||
See the [secret-store documentation][SECRETSTORE] for more details on how
|
||||
to use them.
|
||||
|
||||
[SECRETSTORE]: ../../../docs/CONFIGURATION.md#secret-store-secrets
|
||||
|
||||
## Configuration
|
||||
|
||||
```toml @sample.conf
|
||||
# Input plugin to query Windows Management Instrumentation
|
||||
# This plugin ONLY supports Windows
|
||||
[[inputs.win_wmi]]
|
||||
## Hostname or IP for remote connections, by default the local machine is queried
|
||||
# host = ""
|
||||
## Credentials for the connection, by default no credentials are used
|
||||
# username = ""
|
||||
# password = ""
|
||||
|
||||
## WMI query to execute, multiple methods are possible
|
||||
[[inputs.win_wmi.query]]
|
||||
## Namespace, class and a list of properties to use in the WMI query
|
||||
namespace = "root\\cimv2"
|
||||
class_name = "Win32_Volume"
|
||||
properties = ["Name", "Capacity", "FreeSpace"]
|
||||
## Optional WHERE clause for the WQL query
|
||||
# filter = 'NOT Name LIKE "\\\\?\\%"'
|
||||
## Returned properties to use as tags instead of fields
|
||||
# tag_properties = ["Name"]
|
||||
|
||||
# ## WMI method to invoke, multiple methods are possible
|
||||
# [[inputs.win_wmi.method]]
|
||||
# ## WMI namespace, class and method to use
|
||||
# namespace = 'root\default'
|
||||
# class_name = "StdRegProv"
|
||||
# method = "GetStringValue"
|
||||
# ## Returned WMI method values to use as tags instead of fields
|
||||
# # tag_properties = ["ReturnValue"]
|
||||
# ## Named arguments for the method call
|
||||
# [inputs.win_wmi.method.arguments]
|
||||
# hDefKey = '2147483650'
|
||||
# sSubKeyName = 'Software\Microsoft\windows NT\CurrentVersion'
|
||||
# sValueName = 'ProductName'
|
||||
# ## Mapping of the name of the returned property to a field-name
|
||||
# [inputs.win_wmi.method.fields]
|
||||
# sValue = "product_name"
|
||||
```
|
||||
|
||||
### Remote execution
|
||||
|
||||
This plugin allows to execute queries and methods on a remote host. To do so,
|
||||
you need to provide the `host` as a hostname or IP-address as well as the
|
||||
credentials to execute the query or method as.
|
||||
|
||||
Please note, the remote machine must be configured to allow remote execution and
|
||||
the user needs to have sufficient permission to execute the query or method!
|
||||
Check the [Microsoft guide][remotedoc] for how to do this and test the
|
||||
connection with the `Get-WmiObject` method first.
|
||||
|
||||
[remotedoc]: https://learn.microsoft.com/en-us/windows/win32/wmisdk/connecting-to-wmi-on-a-remote-computer#configuring-a-computer-for-a-remote-connection
|
||||
|
||||
### Query settings
|
||||
|
||||
To issue a query you need to provide the `namespace` (e.g. `root\cimv2`) and the
|
||||
`class_name` (e.g. `Win32_Processor`) for the WMI query. Furthermore, you need
|
||||
to define which `properties` to output. An asterix (`*`) will output all values
|
||||
provided by the query.
|
||||
|
||||
The `filter` setting specifies a WHERE clause passed to the query in the
|
||||
WMI Query Language (WQL). See [WHERE Clause][WHERE] for more information.
|
||||
|
||||
The `tag_properties` allows to provide a list of returned properties that should
|
||||
be provided as tags instead of fields in the metric.
|
||||
|
||||
[WHERE]: https://learn.microsoft.com/en-us/windows/win32/wmisdk/where-clause?source=recommendations
|
||||
|
||||
As an example
|
||||
|
||||
```toml
|
||||
[[inputs.win_wmi]]
|
||||
[[inputs.win_wmi.query]]
|
||||
namespace = "root\\cimv2"
|
||||
class_name = "Win32_Processor"
|
||||
properties = ["Name"]
|
||||
```
|
||||
|
||||
corresponds to executing
|
||||
|
||||
```powershell
|
||||
Get-WmiObject -Namespace "root\cimv2" -Class "Win32_Processor" -Property "Name"
|
||||
```
|
||||
|
||||
### Method settings
|
||||
|
||||
To invoke a method you need to provide the `namespace` (e.g. `root\default`),
|
||||
the `class_name` (e.g. `StdRegProv`) and the `method` name
|
||||
(e.g. `GetStringValue`)for the method to invoke. Furthermore, you may need to
|
||||
provide `arguments` as key-value pair(s) to the method. The number and type of
|
||||
arguments depends on the method specified above.
|
||||
|
||||
Check the [WMI reference][wmireferenc] for available methods and their
|
||||
arguments.
|
||||
|
||||
The `tag_properties` allows to provide a list of returned properties that should
|
||||
be provided as tags instead of fields in the metric.
|
||||
|
||||
[wmireferenc]: https://learn.microsoft.com/en-us/windows/win32/wmisdk/wmi-reference
|
||||
|
||||
As an example
|
||||
|
||||
```toml
|
||||
[[inputs.win_wmi]]
|
||||
[[inputs.win_wmi.method]]
|
||||
namespace = 'root\default'
|
||||
class_name = "StdRegProv"
|
||||
method = "GetStringValue"
|
||||
[inputs.win_wmi.method.arguments]
|
||||
hDefKey = '2147483650'
|
||||
sSubKeyName = 'Software\Microsoft\windows NT\CurrentVersion'
|
||||
sValueName = 'ProductName'
|
||||
```
|
||||
|
||||
corresponds to executing
|
||||
|
||||
```powershell
|
||||
Invoke-WmiMethod -Namespace "root\default" -Class "StdRegProv" -Name "GetStringValue" @(2147483650,"Software\Microsoft\windows NT\CurrentVersion", "ProductName")
|
||||
```
|
||||
|
||||
## Metrics
|
||||
|
||||
By default, a WMI class property's value is used as a metric field. If a class
|
||||
property's value is specified in `tag_properties`, then the value is
|
||||
instead included with the metric as a tag.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Errors
|
||||
|
||||
If you are getting an error about an invalid WMI namespace, class, or property,
|
||||
use the `Get-WmiObject` or `Get-CimInstance` PowerShell commands in order to
|
||||
verify their validity. For example:
|
||||
|
||||
```powershell
|
||||
Get-WmiObject -Namespace root\cimv2 -Class Win32_Volume -Property Capacity, FreeSpace, Name -Filter 'NOT Name LIKE "\\\\?\\%"'
|
||||
```
|
||||
|
||||
```powershell
|
||||
Get-CimInstance -Namespace root\cimv2 -ClassName Win32_Volume -Property Capacity, FreeSpace, Name -Filter 'NOT Name LIKE "\\\\?\\%"'
|
||||
```
|
||||
|
||||
### Data types
|
||||
|
||||
Some WMI classes will return the incorrect data type for a field. In those
|
||||
cases, it is necessary to use a processor to convert the data type. For
|
||||
example, the Capacity and FreeSpace properties of the Win32_Volume class must
|
||||
be converted to integers:
|
||||
|
||||
```toml
|
||||
[[processors.converter]]
|
||||
namepass = ["win_wmi_Win32_Volume"]
|
||||
[processors.converter.fields]
|
||||
integer = ["Capacity", "FreeSpace"]
|
||||
```
|
||||
|
||||
## Example Output
|
||||
|
||||
### Physical Memory
|
||||
|
||||
This query provides metrics for the speed and capacity of each physical memory
|
||||
device, along with tags describing the manufacturer, part number, and device
|
||||
locator of each device.
|
||||
|
||||
```toml
|
||||
[[inputs.win_wmi]]
|
||||
name_prefix = "win_wmi_"
|
||||
[[inputs.win_wmi.query]]
|
||||
namespace = "root\\cimv2"
|
||||
class_name = "Win32_PhysicalMemory"
|
||||
properties = [
|
||||
"Name",
|
||||
"Capacity",
|
||||
"DeviceLocator",
|
||||
"Manufacturer",
|
||||
"PartNumber",
|
||||
"Speed",
|
||||
]
|
||||
tag_properties = ["Name","DeviceLocator","Manufacturer","PartNumber"]
|
||||
```
|
||||
|
||||
Example Output:
|
||||
|
||||
```text
|
||||
win_wmi_Win32_PhysicalMemory,DeviceLocator=DIMM1,Manufacturer=80AD000080AD,Name=Physical\ Memory,PartNumber=HMA82GU6DJR8N-XN\ \ \ \ ,host=foo Capacity=17179869184i,Speed=3200i 1654269272000000000
|
||||
```
|
||||
|
||||
### Processor
|
||||
|
||||
This query provides metrics for the number of cores in each physical processor.
|
||||
Since the Name property of the WMI class is included by default, the metrics
|
||||
will also contain a tag value describing the model of each CPU.
|
||||
|
||||
```toml
|
||||
[[inputs.win_wmi]]
|
||||
name_prefix = "win_wmi_"
|
||||
[[inputs.win_wmi.query]]
|
||||
namespace = "root\\cimv2"
|
||||
class_name = "Win32_Processor"
|
||||
properties = ["Name","NumberOfCores"]
|
||||
tag_properties = ["Name"]
|
||||
```
|
||||
|
||||
Example Output:
|
||||
|
||||
```text
|
||||
win_wmi_Win32_Processor,Name=Intel(R)\ Core(TM)\ i9-10900\ CPU\ @\ 2.80GHz,host=foo NumberOfCores=10i 1654269272000000000
|
||||
```
|
||||
|
||||
### Computer System
|
||||
|
||||
This query provides metrics for the number of socketted processors, number of
|
||||
logical cores on each processor, and the total physical memory in the computer.
|
||||
The metrics include tag values for the domain, manufacturer, and model of the
|
||||
computer.
|
||||
|
||||
```toml
|
||||
[[inputs.win_wmi]]
|
||||
name_prefix = "win_wmi_"
|
||||
[[inputs.win_wmi.query]]
|
||||
namespace = "root\\cimv2"
|
||||
class_name = "Win32_ComputerSystem"
|
||||
properties = [
|
||||
"Name",
|
||||
"Domain",
|
||||
"Manufacturer",
|
||||
"Model",
|
||||
"NumberOfLogicalProcessors",
|
||||
"NumberOfProcessors",
|
||||
"TotalPhysicalMemory"
|
||||
]
|
||||
tag_properties = ["Name","Domain","Manufacturer","Model"]
|
||||
```
|
||||
|
||||
Example Output:
|
||||
|
||||
```text
|
||||
win_wmi_Win32_ComputerSystem,Domain=company.com,Manufacturer=Lenovo,Model=X1\ Carbon,Name=FOO,host=foo NumberOfLogicalProcessors=20i,NumberOfProcessors=1i,TotalPhysicalMemory=34083926016i 1654269272000000000
|
||||
```
|
||||
|
||||
### Operating System
|
||||
|
||||
This query provides metrics for the paging file's free space, the operating
|
||||
system's free virtual memory, the operating system SKU installed on the
|
||||
computer, and the Windows product type. The OS architecture is included as a
|
||||
tagged value to describe whether the installation is 32-bit or 64-bit.
|
||||
|
||||
```toml
|
||||
[[inputs.win_wmi]]
|
||||
name_prefix = "win_wmi_"
|
||||
[[inputs.win_wmi.query]]
|
||||
class_name = "Win32_OperatingSystem"
|
||||
namespace = "root\\cimv2"
|
||||
properties = [
|
||||
"Name",
|
||||
"Caption",
|
||||
"FreeSpaceInPagingFiles",
|
||||
"FreeVirtualMemory",
|
||||
"OperatingSystemSKU",
|
||||
"OSArchitecture",
|
||||
"ProductType"
|
||||
]
|
||||
tag_properties = ["Name","Caption","OSArchitecture"]
|
||||
```
|
||||
|
||||
Example Output:
|
||||
|
||||
```text
|
||||
win_wmi_Win32_OperatingSystem,Caption=Microsoft\ Windows\ 10\ Enterprise,InstallationType=Client,Name=Microsoft\ Windows\ 10\ Enterprise|C:\WINDOWS|\Device\Harddisk0\Partition3,OSArchitecture=64-bit,host=foo FreeSpaceInPagingFiles=5203244i,FreeVirtualMemory=16194496i,OperatingSystemSKU=4i,ProductType=1i 1654269272000000000
|
||||
```
|
||||
|
||||
### Failover Clusters
|
||||
|
||||
This query provides a boolean metric describing whether Dynamic Quorum is
|
||||
enabled for the cluster. The tag values for the metric also include the name of
|
||||
the Windows Server Failover Cluster and the type of Quorum in use.
|
||||
|
||||
```toml
|
||||
[[inputs.win_wmi]]
|
||||
name_prefix = "win_wmi_"
|
||||
[[inputs.win_wmi.query]]
|
||||
namespace = "root\\mscluster"
|
||||
class_name = "MSCluster_Cluster"
|
||||
properties = [
|
||||
"Name",
|
||||
"QuorumType",
|
||||
"DynamicQuorumEnabled"
|
||||
]
|
||||
tag_properties = ["Name","QuorumType"]
|
||||
```
|
||||
|
||||
Example Output:
|
||||
|
||||
```text
|
||||
win_wmi_MSCluster_Cluster,Name=testcluster1,QuorumType=Node\ and\ File\ Share\ Majority,host=testnode1 DynamicQuorumEnabled=1i 1671553260000000000
|
||||
```
|
||||
|
||||
### Bitlocker
|
||||
|
||||
This query provides a list of volumes which are eligible for bitlocker
|
||||
encryption and their compliance status. Because the MBAM_Volume class does not
|
||||
include a Name property, the ExcludeNameKey configuration is included. The
|
||||
VolumeName property is included in the metric as a tagged value.
|
||||
|
||||
```toml
|
||||
[[inputs.win_wmi]]
|
||||
name_prefix = "win_wmi_"
|
||||
[[inputs.win_wmi.query]]
|
||||
namespace = "root\\Microsoft\\MBAM"
|
||||
class_name = "MBAM_Volume"
|
||||
properties = [
|
||||
"Compliant",
|
||||
"VolumeName"
|
||||
]
|
||||
tag_properties = ["VolumeName"]
|
||||
```
|
||||
|
||||
Example Output:
|
||||
|
||||
```text
|
||||
win_wmi_MBAM_Volume,VolumeName=C:,host=foo Compliant=1i 1654269272000000000
|
||||
```
|
||||
|
||||
### SQL Server
|
||||
|
||||
This query provides metrics which contain tags describing the version and SKU
|
||||
of SQL Server. These properties are useful for creating a dashboard of your SQL
|
||||
Server inventory, which includes the patch level and edition of SQL Server that
|
||||
is installed.
|
||||
|
||||
```toml
|
||||
[[inputs.win_wmi]]
|
||||
name_prefix = "win_wmi_"
|
||||
[[inputs.win_wmi.query]]
|
||||
namespace = "Root\\Microsoft\\SqlServer\\ComputerManagement15"
|
||||
class_name = "SqlServiceAdvancedProperty"
|
||||
properties = [
|
||||
"PropertyName",
|
||||
"ServiceName",
|
||||
"PropertyStrValue",
|
||||
"SqlServiceType"
|
||||
]
|
||||
filter = "ServiceName LIKE 'MSSQLSERVER' AND SqlServiceType = 1 AND (PropertyName LIKE 'FILEVERSION' OR PropertyName LIKE 'SKUNAME')"
|
||||
tag_properties = ["PropertyName","ServiceName","PropertyStrValue"]
|
||||
```
|
||||
|
||||
Example Output:
|
||||
|
||||
```text
|
||||
win_wmi_SqlServiceAdvancedProperty,PropertyName=FILEVERSION,PropertyStrValue=2019.150.4178.1,ServiceName=MSSQLSERVER,host=foo,sqlinstance=foo SqlServiceType=1i 1654269272000000000
|
||||
win_wmi_SqlServiceAdvancedProperty,PropertyName=SKUNAME,PropertyStrValue=Developer\ Edition\ (64-bit),ServiceName=MSSQLSERVER,host=foo,sqlinstance=foo SqlServiceType=1i 1654269272000000000
|
||||
```
|
228
plugins/inputs/win_wmi/method.go
Normal file
228
plugins/inputs/win_wmi/method.go
Normal file
|
@ -0,0 +1,228 @@
|
|||
//go:build windows
|
||||
|
||||
package win_wmi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/go-ole/go-ole"
|
||||
"github.com/go-ole/go-ole/oleutil"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/config"
|
||||
"github.com/influxdata/telegraf/filter"
|
||||
"github.com/influxdata/telegraf/internal"
|
||||
)
|
||||
|
||||
type method struct {
|
||||
Namespace string `toml:"namespace"`
|
||||
ClassName string `toml:"class_name"`
|
||||
Method string `toml:"method"`
|
||||
Arguments map[string]interface{} `toml:"arguments"`
|
||||
FieldMapping map[string]string `toml:"fields"`
|
||||
Filter string `toml:"filter"`
|
||||
TagPropertiesInclude []string `toml:"tag_properties"`
|
||||
|
||||
host string
|
||||
connectionParams []interface{}
|
||||
tagFilter filter.Filter
|
||||
}
|
||||
|
||||
func (m *method) prepare(host string, username, password config.Secret) error {
|
||||
// Compile the filter
|
||||
f, err := filter.Compile(m.TagPropertiesInclude)
|
||||
if err != nil {
|
||||
return fmt.Errorf("compiling tag-filter failed: %w", err)
|
||||
}
|
||||
m.tagFilter = f
|
||||
|
||||
// Setup the connection parameters
|
||||
m.host = host
|
||||
if m.host != "" {
|
||||
m.connectionParams = append(m.connectionParams, m.host)
|
||||
} else {
|
||||
m.connectionParams = append(m.connectionParams, nil)
|
||||
}
|
||||
m.connectionParams = append(m.connectionParams, m.Namespace)
|
||||
if !username.Empty() {
|
||||
u, err := username.Get()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting username secret failed: %w", err)
|
||||
}
|
||||
m.connectionParams = append(m.connectionParams, u.String())
|
||||
username.Destroy()
|
||||
}
|
||||
if !password.Empty() {
|
||||
p, err := password.Get()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting password secret failed: %w", err)
|
||||
}
|
||||
m.connectionParams = append(m.connectionParams, p.String())
|
||||
password.Destroy()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *method) execute(acc telegraf.Accumulator) error {
|
||||
// The only way to run WMI queries in parallel while being thread-safe is to
|
||||
// ensure the CoInitialize[Ex]() call is bound to its current OS thread.
|
||||
// Otherwise, attempting to initialize and run parallel queries across
|
||||
// goroutines will result in protected memory errors.
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
// Init the COM client
|
||||
if err := ole.CoInitializeEx(0, ole.COINIT_APARTMENTTHREADED); err != nil {
|
||||
var oleCode *ole.OleError
|
||||
if !errors.As(err, &oleCode) || (oleCode.Code() != ole.S_OK && oleCode.Code() != sFalse) {
|
||||
return fmt.Errorf("initialization of COM object failed: %w", err)
|
||||
}
|
||||
}
|
||||
defer ole.CoUninitialize()
|
||||
|
||||
// Initialize the WMI service
|
||||
locator, err := oleutil.CreateObject("WbemScripting.SWbemLocator")
|
||||
if err != nil {
|
||||
return fmt.Errorf("creation of OLE object failed: %w", err)
|
||||
}
|
||||
if locator == nil {
|
||||
return errors.New("failed to create WbemScripting.SWbemLocator, maybe WMI is broken")
|
||||
}
|
||||
defer locator.Release()
|
||||
wmi, err := locator.QueryInterface(ole.IID_IDispatch)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to query interface: %w", err)
|
||||
}
|
||||
defer wmi.Release()
|
||||
|
||||
serviceRaw, err := oleutil.CallMethod(wmi, "ConnectServer", m.connectionParams...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed calling method ConnectServer: %w", err)
|
||||
}
|
||||
service := serviceRaw.ToIDispatch()
|
||||
defer serviceRaw.Clear()
|
||||
|
||||
// Get the specified class-method
|
||||
classRaw, err := oleutil.CallMethod(service, "Get", m.ClassName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get class %s: %w", m.ClassName, err)
|
||||
}
|
||||
class := classRaw.ToIDispatch()
|
||||
defer classRaw.Clear()
|
||||
|
||||
classMethodsRaw, err := class.GetProperty("Methods_")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to call method %s: %w", m.Method, err)
|
||||
}
|
||||
classMethods := classMethodsRaw.ToIDispatch()
|
||||
defer classMethodsRaw.Clear()
|
||||
|
||||
methodRaw, err := classMethods.CallMethod("Item", m.Method)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to call method %s: %w", m.Method, err)
|
||||
}
|
||||
method := methodRaw.ToIDispatch()
|
||||
defer methodRaw.Clear()
|
||||
|
||||
// Fill the input parameters of the method
|
||||
inputParamsRaw, err := oleutil.GetProperty(method, "InParameters")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get input parameters for %s: %w", m.Method, err)
|
||||
}
|
||||
inputParams := inputParamsRaw.ToIDispatch()
|
||||
defer inputParamsRaw.Clear()
|
||||
for k, v := range m.Arguments {
|
||||
if _, err := inputParams.PutProperty(k, v); err != nil {
|
||||
return fmt.Errorf("setting param %q for method %q failed: %w", k, m.Method, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Get the output parameters of the method
|
||||
outputParamsRaw, err := oleutil.GetProperty(method, "OutParameters")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get output parameters for %s: %w", m.Method, err)
|
||||
}
|
||||
outputParams := outputParamsRaw.ToIDispatch()
|
||||
defer outputParamsRaw.Clear()
|
||||
|
||||
// Execute the method
|
||||
outputRaw, err := service.CallMethod("ExecMethod", m.ClassName, m.Method, inputParamsRaw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute method %s: %w", m.Method, err)
|
||||
}
|
||||
output := outputRaw.ToIDispatch()
|
||||
defer outputRaw.Clear()
|
||||
|
||||
outputPropertiesRaw, err := oleutil.GetProperty(outputParams, "Properties_")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get output properties for method %s: %w", m.Method, err)
|
||||
}
|
||||
outputProperties := outputPropertiesRaw.ToIDispatch()
|
||||
defer outputPropertiesRaw.Clear()
|
||||
|
||||
// Convert the results to fields and tags
|
||||
tags, fields := make(map[string]string), make(map[string]interface{})
|
||||
|
||||
// Add a source tag if we use remote queries
|
||||
if m.host != "" {
|
||||
tags["source"] = m.host
|
||||
}
|
||||
|
||||
err = oleutil.ForEach(outputProperties, func(p *ole.VARIANT) error {
|
||||
// Name of the returned result item
|
||||
nameProperty, err := p.ToIDispatch().GetProperty("Name")
|
||||
if err != nil {
|
||||
return errors.New("cannot get output property name")
|
||||
}
|
||||
name := nameProperty.ToString()
|
||||
defer nameProperty.Clear()
|
||||
|
||||
// Value of the returned result item
|
||||
property, err := output.GetProperty(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get value for output property %s: %w", name, err)
|
||||
}
|
||||
|
||||
// Map the fieldname if provided
|
||||
if n, found := m.FieldMapping[name]; found {
|
||||
name = n
|
||||
}
|
||||
|
||||
// We might get either scalar values or an array of values...
|
||||
if value := property.Value(); value != nil {
|
||||
if m.tagFilter != nil && m.tagFilter.Match(name) {
|
||||
if s, err := internal.ToString(value); err == nil && s != "" {
|
||||
tags[name] = s
|
||||
}
|
||||
} else {
|
||||
fields[name] = value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if array := property.ToArray(); array != nil {
|
||||
if m.tagFilter != nil && m.tagFilter.Match(name) {
|
||||
for i, v := range array.ToValueArray() {
|
||||
if s, err := internal.ToString(v); err == nil && s != "" {
|
||||
tags[fmt.Sprintf("%s_%d", name, i)] = s
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for i, v := range array.ToValueArray() {
|
||||
fields[fmt.Sprintf("%s_%d", name, i)] = v
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("cannot handle property %q with value %v", name, property)
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot iterate the output properties: %w", err)
|
||||
}
|
||||
|
||||
acc.AddFields(m.ClassName, fields, tags)
|
||||
|
||||
return nil
|
||||
}
|
191
plugins/inputs/win_wmi/query.go
Normal file
191
plugins/inputs/win_wmi/query.go
Normal file
|
@ -0,0 +1,191 @@
|
|||
//go:build windows
|
||||
|
||||
package win_wmi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/go-ole/go-ole"
|
||||
"github.com/go-ole/go-ole/oleutil"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/config"
|
||||
"github.com/influxdata/telegraf/filter"
|
||||
"github.com/influxdata/telegraf/internal"
|
||||
)
|
||||
|
||||
type query struct {
|
||||
Namespace string `toml:"namespace"`
|
||||
ClassName string `toml:"class_name"`
|
||||
Properties []string `toml:"properties"`
|
||||
Filter string `toml:"filter"`
|
||||
TagPropertiesInclude []string `toml:"tag_properties"`
|
||||
|
||||
host string
|
||||
query string
|
||||
connectionParams []interface{}
|
||||
tagFilter filter.Filter
|
||||
}
|
||||
|
||||
func (q *query) prepare(host string, username, password config.Secret) error {
|
||||
// Compile the filter
|
||||
f, err := filter.Compile(q.TagPropertiesInclude)
|
||||
if err != nil {
|
||||
return fmt.Errorf("compiling tag-filter failed: %w", err)
|
||||
}
|
||||
q.tagFilter = f
|
||||
|
||||
// Setup the connection parameters
|
||||
q.host = host
|
||||
if q.host != "" {
|
||||
q.connectionParams = append(q.connectionParams, q.host)
|
||||
} else {
|
||||
q.connectionParams = append(q.connectionParams, nil)
|
||||
}
|
||||
q.connectionParams = append(q.connectionParams, q.Namespace)
|
||||
if !username.Empty() {
|
||||
u, err := username.Get()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting username secret failed: %w", err)
|
||||
}
|
||||
q.connectionParams = append(q.connectionParams, u.String())
|
||||
username.Destroy()
|
||||
}
|
||||
if !password.Empty() {
|
||||
p, err := password.Get()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting password secret failed: %w", err)
|
||||
}
|
||||
q.connectionParams = append(q.connectionParams, p.String())
|
||||
password.Destroy()
|
||||
}
|
||||
|
||||
// Construct the overall query from the given parts
|
||||
wql := fmt.Sprintf("SELECT %s FROM %s", strings.Join(q.Properties, ", "), q.ClassName)
|
||||
if len(q.Filter) > 0 {
|
||||
wql += " WHERE " + q.Filter
|
||||
}
|
||||
q.query = wql
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *query) execute(acc telegraf.Accumulator) error {
|
||||
// The only way to run WMI queries in parallel while being thread-safe is to
|
||||
// ensure the CoInitialize[Ex]() call is bound to its current OS thread.
|
||||
// Otherwise, attempting to initialize and run parallel queries across
|
||||
// goroutines will result in protected memory errors.
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
// init COM
|
||||
if err := ole.CoInitializeEx(0, ole.COINIT_MULTITHREADED); err != nil {
|
||||
var oleCode *ole.OleError
|
||||
if errors.As(err, &oleCode) && oleCode.Code() != ole.S_OK && oleCode.Code() != sFalse {
|
||||
return err
|
||||
}
|
||||
}
|
||||
defer ole.CoUninitialize()
|
||||
|
||||
unknown, err := oleutil.CreateObject("WbemScripting.SWbemLocator")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if unknown == nil {
|
||||
return errors.New("failed to create WbemScripting.SWbemLocator, maybe WMI is broken")
|
||||
}
|
||||
defer unknown.Release()
|
||||
|
||||
wmi, err := unknown.QueryInterface(ole.IID_IDispatch)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to QueryInterface: %w", err)
|
||||
}
|
||||
defer wmi.Release()
|
||||
|
||||
// service is a SWbemServices
|
||||
serviceRaw, err := oleutil.CallMethod(wmi, "ConnectServer", q.connectionParams...)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed calling method ConnectServer: %w", err)
|
||||
}
|
||||
service := serviceRaw.ToIDispatch()
|
||||
defer serviceRaw.Clear()
|
||||
|
||||
// result is a SWBemObjectSet
|
||||
resultRaw, err := oleutil.CallMethod(service, "ExecQuery", q.query)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed calling method ExecQuery for query %s: %w", q.query, err)
|
||||
}
|
||||
result := resultRaw.ToIDispatch()
|
||||
defer resultRaw.Clear()
|
||||
|
||||
countRaw, err := oleutil.GetProperty(result, "Count")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed getting Count: %w", err)
|
||||
}
|
||||
count := countRaw.Val
|
||||
defer countRaw.Clear()
|
||||
|
||||
for i := int64(0); i < count; i++ {
|
||||
itemRaw, err := oleutil.CallMethod(result, "ItemIndex", i)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed calling method ItemIndex: %w", err)
|
||||
}
|
||||
|
||||
if err := q.extractProperties(acc, itemRaw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *query) extractProperties(acc telegraf.Accumulator, itemRaw *ole.VARIANT) error {
|
||||
tags, fields := make(map[string]string), make(map[string]interface{})
|
||||
|
||||
if q.host != "" {
|
||||
tags["source"] = q.host
|
||||
}
|
||||
|
||||
item := itemRaw.ToIDispatch()
|
||||
defer item.Release()
|
||||
|
||||
for _, name := range q.Properties {
|
||||
propertyRaw, err := oleutil.GetProperty(item, name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting property %q failed: %w", name, err)
|
||||
}
|
||||
value := propertyRaw.Value()
|
||||
propertyRaw.Clear()
|
||||
|
||||
if q.tagFilter != nil && q.tagFilter.Match(name) {
|
||||
s, err := internal.ToString(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("converting property %q failed: %w", s, err)
|
||||
}
|
||||
tags[name] = s
|
||||
continue
|
||||
}
|
||||
|
||||
switch v := value.(type) {
|
||||
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
|
||||
fields[name] = v
|
||||
case string:
|
||||
fields[name] = v
|
||||
case bool:
|
||||
fields[name] = v
|
||||
case []byte:
|
||||
fields[name] = string(v)
|
||||
case fmt.Stringer:
|
||||
fields[name] = v.String()
|
||||
case nil:
|
||||
fields[name] = nil
|
||||
default:
|
||||
return fmt.Errorf("property %q of type \"%T\" unsupported", name, v)
|
||||
}
|
||||
}
|
||||
acc.AddFields(q.ClassName, fields, tags)
|
||||
return nil
|
||||
}
|
36
plugins/inputs/win_wmi/sample.conf
Normal file
36
plugins/inputs/win_wmi/sample.conf
Normal file
|
@ -0,0 +1,36 @@
|
|||
# Input plugin to query Windows Management Instrumentation
|
||||
# This plugin ONLY supports Windows
|
||||
[[inputs.win_wmi]]
|
||||
## Hostname or IP for remote connections, by default the local machine is queried
|
||||
# host = ""
|
||||
## Credentials for the connection, by default no credentials are used
|
||||
# username = ""
|
||||
# password = ""
|
||||
|
||||
## WMI query to execute, multiple methods are possible
|
||||
[[inputs.win_wmi.query]]
|
||||
## Namespace, class and a list of properties to use in the WMI query
|
||||
namespace = "root\\cimv2"
|
||||
class_name = "Win32_Volume"
|
||||
properties = ["Name", "Capacity", "FreeSpace"]
|
||||
## Optional WHERE clause for the WQL query
|
||||
# filter = 'NOT Name LIKE "\\\\?\\%"'
|
||||
## Returned properties to use as tags instead of fields
|
||||
# tag_properties = ["Name"]
|
||||
|
||||
# ## WMI method to invoke, multiple methods are possible
|
||||
# [[inputs.win_wmi.method]]
|
||||
# ## WMI namespace, class and method to use
|
||||
# namespace = 'root\default'
|
||||
# class_name = "StdRegProv"
|
||||
# method = "GetStringValue"
|
||||
# ## Returned WMI method values to use as tags instead of fields
|
||||
# # tag_properties = ["ReturnValue"]
|
||||
# ## Named arguments for the method call
|
||||
# [inputs.win_wmi.method.arguments]
|
||||
# hDefKey = '2147483650'
|
||||
# sSubKeyName = 'Software\Microsoft\windows NT\CurrentVersion'
|
||||
# sValueName = 'ProductName'
|
||||
# ## Mapping of the name of the returned property to a field-name
|
||||
# [inputs.win_wmi.method.fields]
|
||||
# sValue = "product_name"
|
78
plugins/inputs/win_wmi/win_wmi.go
Normal file
78
plugins/inputs/win_wmi/win_wmi.go
Normal file
|
@ -0,0 +1,78 @@
|
|||
//go:generate ../../../tools/readme_config_includer/generator
|
||||
//go:build windows
|
||||
|
||||
package win_wmi
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/config"
|
||||
"github.com/influxdata/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
//go:embed sample.conf
|
||||
var sampleConfig string
|
||||
|
||||
// S_FALSE is returned by CoInitializeEx if it was already called on this thread.
|
||||
const sFalse = 0x00000001
|
||||
|
||||
type Wmi struct {
|
||||
Host string `toml:"host"`
|
||||
Username config.Secret `toml:"username"`
|
||||
Password config.Secret `toml:"password"`
|
||||
Queries []query `toml:"query"`
|
||||
Methods []method `toml:"method"`
|
||||
Log telegraf.Logger `toml:"-"`
|
||||
}
|
||||
|
||||
func (*Wmi) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
func (w *Wmi) Init() error {
|
||||
for i := range w.Queries {
|
||||
q := &w.Queries[i]
|
||||
if err := q.prepare(w.Host, w.Username, w.Password); err != nil {
|
||||
return fmt.Errorf("preparing query %q failed: %w", q.ClassName, err)
|
||||
}
|
||||
}
|
||||
|
||||
for i := range w.Methods {
|
||||
m := &w.Methods[i]
|
||||
if err := m.prepare(w.Host, w.Username, w.Password); err != nil {
|
||||
return fmt.Errorf("preparing method %q failed: %w", m.Method, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Wmi) Gather(acc telegraf.Accumulator) error {
|
||||
var wg sync.WaitGroup
|
||||
for _, q := range w.Queries {
|
||||
wg.Add(1)
|
||||
go func(q query) {
|
||||
defer wg.Done()
|
||||
acc.AddError(q.execute(acc))
|
||||
}(q)
|
||||
}
|
||||
|
||||
for _, m := range w.Methods {
|
||||
wg.Add(1)
|
||||
go func(m method) {
|
||||
defer wg.Done()
|
||||
acc.AddError(m.execute(acc))
|
||||
}(m)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("win_wmi", func() telegraf.Input { return &Wmi{} })
|
||||
}
|
31
plugins/inputs/win_wmi/win_wmi_notwindows.go
Normal file
31
plugins/inputs/win_wmi/win_wmi_notwindows.go
Normal file
|
@ -0,0 +1,31 @@
|
|||
//go:generate ../../../tools/readme_config_includer/generator
|
||||
//go:build !windows
|
||||
|
||||
package win_wmi
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
//go:embed sample.conf
|
||||
var sampleConfig string
|
||||
|
||||
type Wmi struct {
|
||||
Log telegraf.Logger `toml:"-"`
|
||||
}
|
||||
|
||||
func (*Wmi) SampleConfig() string { return sampleConfig }
|
||||
|
||||
func (w *Wmi) Init() error {
|
||||
w.Log.Warn("Current platform is not supported")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*Wmi) Gather(telegraf.Accumulator) error { return nil }
|
||||
|
||||
func init() {
|
||||
inputs.Add("win_wmi", func() telegraf.Input { return &Wmi{} })
|
||||
}
|
119
plugins/inputs/win_wmi/win_wmi_test.go
Normal file
119
plugins/inputs/win_wmi/win_wmi_test.go
Normal file
|
@ -0,0 +1,119 @@
|
|||
//go:build windows
|
||||
|
||||
package win_wmi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/metric"
|
||||
"github.com/influxdata/telegraf/testutil"
|
||||
)
|
||||
|
||||
var sysDrive = os.Getenv("SystemDrive") + `\` // C:\
|
||||
|
||||
func TestBuildWqlStatements(t *testing.T) {
|
||||
plugin := &Wmi{
|
||||
Queries: []query{
|
||||
{
|
||||
Namespace: "ROOT\\cimv2",
|
||||
ClassName: "Win32_Volume",
|
||||
Properties: []string{"Name", "FreeSpace", "Purpose"},
|
||||
//nolint:gocritic // sprintfQuotedString - "%s" used by purpose, string escaping is done by special function
|
||||
Filter: fmt.Sprintf(`NOT Name LIKE "\\\\?\\%%" AND Name LIKE "%s"`, regexp.QuoteMeta(sysDrive)),
|
||||
TagPropertiesInclude: []string{"Name"},
|
||||
},
|
||||
},
|
||||
Log: testutil.Logger{},
|
||||
}
|
||||
require.NoError(t, plugin.Init())
|
||||
require.NotEmpty(t, plugin.Queries)
|
||||
|
||||
//nolint:gocritic // sprintfQuotedString - "%s" used by purpose, string escaping is done by special function
|
||||
expected := fmt.Sprintf(
|
||||
`SELECT Name, FreeSpace, Purpose FROM Win32_Volume WHERE NOT Name LIKE "\\\\?\\%%" AND Name LIKE "%s"`,
|
||||
regexp.QuoteMeta(sysDrive),
|
||||
)
|
||||
require.Equal(t, expected, plugin.Queries[0].query)
|
||||
}
|
||||
|
||||
func TestInit(t *testing.T) {
|
||||
plugin := &Wmi{}
|
||||
require.NoError(t, plugin.Init())
|
||||
}
|
||||
|
||||
func TestQueryIntegration(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
plugin := &Wmi{
|
||||
Queries: []query{
|
||||
{
|
||||
Namespace: "ROOT\\cimv2",
|
||||
ClassName: "Win32_Volume",
|
||||
Properties: []string{"Name", "FreeSpace", "Purpose"},
|
||||
//nolint:gocritic // sprintfQuotedString - "%s" used by purpose, string escaping is done by special function
|
||||
Filter: fmt.Sprintf(`NOT Name LIKE "\\\\?\\%%" AND Name LIKE "%s"`, regexp.QuoteMeta(sysDrive)),
|
||||
TagPropertiesInclude: []string{"Name"},
|
||||
},
|
||||
},
|
||||
Log: testutil.Logger{},
|
||||
}
|
||||
require.NoError(t, plugin.Init())
|
||||
|
||||
var acc testutil.Accumulator
|
||||
require.NoError(t, plugin.Gather(&acc))
|
||||
require.Empty(t, acc.Errors)
|
||||
// Only one metric was returned (because we filtered for SystemDrive)
|
||||
require.Len(t, acc.Metrics, 1)
|
||||
// Name property collected and is the SystemDrive
|
||||
require.Equal(t, sysDrive, acc.Metrics[0].Tags["Name"])
|
||||
// FreeSpace property was collected as a field
|
||||
require.NotEmpty(t, acc.Metrics[0].Fields["FreeSpace"])
|
||||
}
|
||||
|
||||
func TestMethodIntegration(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
plugin := &Wmi{
|
||||
Methods: []method{
|
||||
{
|
||||
Namespace: "ROOT\\default",
|
||||
ClassName: "StdRegProv",
|
||||
Method: "GetStringValue",
|
||||
Arguments: map[string]interface{}{
|
||||
"hDefKey": `2147483650`,
|
||||
"sSubKeyName": `software\microsoft\windows nt\currentversion`,
|
||||
"sValueName": `ProductName`,
|
||||
},
|
||||
TagPropertiesInclude: []string{"ReturnValue"},
|
||||
},
|
||||
},
|
||||
Log: testutil.Logger{},
|
||||
}
|
||||
require.NoError(t, plugin.Init())
|
||||
|
||||
expected := []telegraf.Metric{
|
||||
metric.New(
|
||||
"StdRegProv",
|
||||
map[string]string{"ReturnValue": "0"},
|
||||
map[string]interface{}{"sValue": "Windows ..."},
|
||||
time.Unix(0, 0),
|
||||
),
|
||||
}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
require.NoError(t, plugin.Gather(&acc))
|
||||
require.Empty(t, acc.Errors)
|
||||
actual := acc.GetTelegrafMetrics()
|
||||
testutil.RequireMetricsStructureEqual(t, expected, actual, testutil.IgnoreTime())
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue