Skip to content

Commit

Permalink
feat(inputs.win_wmi): Allow to invoke methods (#15300)
Browse files Browse the repository at this point in the history
  • Loading branch information
srebhan committed May 10, 2024
1 parent bf0c8e8 commit e7703ae
Show file tree
Hide file tree
Showing 6 changed files with 403 additions and 29 deletions.
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

0 comments on commit e7703ae

Please sign in to comment.