diff --git a/plugins/inputs/win_wmi/README.md b/plugins/inputs/win_wmi/README.md index ebf8ee9417543..3ba56de8ecbac 100644 --- a/plugins/inputs/win_wmi/README.md +++ b/plugins/inputs/win_wmi/README.md @@ -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 diff --git a/plugins/inputs/win_wmi/method.go b/plugins/inputs/win_wmi/method.go new file mode 100644 index 0000000000000..42477565424a1 --- /dev/null +++ b/plugins/inputs/win_wmi/method.go @@ -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 +} diff --git a/plugins/inputs/win_wmi/query.go b/plugins/inputs/win_wmi/query.go index 43d1b408aa2b1..ae8d9019581f6 100644 --- a/plugins/inputs/win_wmi/query.go +++ b/plugins/inputs/win_wmi/query.go @@ -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) diff --git a/plugins/inputs/win_wmi/sample.conf b/plugins/inputs/win_wmi/sample.conf index fda02199f8938..78ae120507106 100644 --- a/plugins/inputs/win_wmi/sample.conf +++ b/plugins/inputs/win_wmi/sample.conf @@ -7,14 +7,30 @@ # 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" diff --git a/plugins/inputs/win_wmi/win_wmi.go b/plugins/inputs/win_wmi/win_wmi.go index cf2c2a9e52dff..77a236a34ff66 100644 --- a/plugins/inputs/win_wmi/win_wmi.go +++ b/plugins/inputs/win_wmi/win_wmi.go @@ -22,6 +22,7 @@ type Wmi struct { Username config.Secret `toml:"username"` Password config.Secret `toml:"password"` Queries []Query `toml:"query"` + Methods []Method `toml:"method"` Log telegraf.Logger `toml:"-"` } @@ -37,6 +38,13 @@ func (w *Wmi) Init() error { } } + 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 } @@ -55,6 +63,15 @@ func (w *Wmi) Gather(acc telegraf.Accumulator) error { acc.AddError(q.execute(acc)) }(query) } + + for _, method := range w.Methods { + wg.Add(1) + go func(m Method) { + defer wg.Done() + acc.AddError(m.execute(acc)) + }(method) + } + wg.Wait() return nil diff --git a/plugins/inputs/win_wmi/win_wmi_test.go b/plugins/inputs/win_wmi/win_wmi_test.go index 540e70cb8b94d..a5222e7771ba6 100644 --- a/plugins/inputs/win_wmi/win_wmi_test.go +++ b/plugins/inputs/win_wmi/win_wmi_test.go @@ -1,5 +1,4 @@ //go:build windows -// +build windows package win_wmi @@ -8,7 +7,10 @@ import ( "os" "regexp" "testing" + "time" + "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/metric" "github.com/influxdata/telegraf/testutil" "github.com/stretchr/testify/require" ) @@ -45,7 +47,7 @@ func TestInit(t *testing.T) { require.NoError(t, plugin.Init()) } -func TestQuery(t *testing.T) { +func TestQueryIntegration(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } @@ -75,3 +77,42 @@ func TestQuery(t *testing.T) { // 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()) +}