Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(inputs.win_wmi): Allow to invoke methods #15300

Merged
merged 5 commits into from
May 10, 2024
Merged
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
111 changes: 91 additions & 20 deletions plugins/inputs/win_wmi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,43 +40,114 @@ to use them.
# username = ""
# password = ""

## WMI query to execute, multiple methods are possible
[[inputs.win_wmi.query]]
# a string representing the WMI namespace to be queried
## Namespace, class and a list of properties to use in the WMI query
namespace = "root\\cimv2"
# a string representing the WMI class to be queried
class_name = "Win32_Volume"
# an array of strings representing the properties of the WMI class to be queried
properties = ["Name", "Capacity", "FreeSpace"]
# a string specifying a WHERE clause to use as a filter for the WQL
filter = 'NOT Name LIKE "\\\\?\\%"'
# WMI class properties which should be considered tags instead of fields
tag_properties = ["Name"]
## 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"
```

### namespace
### Remote execution

A string representing the WMI namespace to be queried. For example,
`root\\cimv2`.
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.

### class_name
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.

A string representing the WMI class to be queried. For example,
`Win32_Processor`.
[remotedoc]: https://learn.microsoft.com/en-us/windows/win32/wmisdk/connecting-to-wmi-on-a-remote-computer#configuring-a-computer-for-a-remote-connection

### properties
### Query settings

An array of strings representing the properties of the WMI class to be queried.
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.

### filter
The `filter` setting specifies a WHERE clause passed to the query in the
WMI Query Language (WQL). See [WHERE Clause][WHERE] for more information.

A string specifying a WHERE clause to use as a filter for 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

### tag_properties
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

Properties which should be considered tags instead of fields.
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

Expand Down
228 changes: 228 additions & 0 deletions plugins/inputs/win_wmi/method.go
Original file line number Diff line number Diff line change
@@ -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"
)

// Method struct
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_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()

// Initialize the WMI service
locator, err := oleutil.CreateObject("WbemScripting.SWbemLocator")
if err != nil {
return 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", "StdRegProv", 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 := map[string]string{}, 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
}
1 change: 1 addition & 0 deletions plugins/inputs/win_wmi/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func (q *Query) prepare(host string, username, password config.Secret) error {
}
q.tagFilter = f

// Setup the connection parameters
q.host = host
if q.host != "" {
q.connectionParams = append(q.connectionParams, q.host)
Expand Down
Loading
Loading