From 8183d4730c6cf953d2e3e51efbdbbd93a4853312 Mon Sep 17 00:00:00 2001 From: Sven Rebhan <36194019+srebhan@users.noreply.github.com> Date: Tue, 12 Mar 2024 15:45:26 +0100 Subject: [PATCH] chore(inputs.win_wmi): Cleanup and refactor code (#14965) --- plugins/inputs/win_wmi/README.md | 550 +++++++++---------- plugins/inputs/win_wmi/query.go | 158 ++++++ plugins/inputs/win_wmi/win_wmi.go | 213 +------ plugins/inputs/win_wmi/win_wmi_notwindows.go | 8 +- 4 files changed, 452 insertions(+), 477 deletions(-) create mode 100644 plugins/inputs/win_wmi/query.go diff --git a/plugins/inputs/win_wmi/README.md b/plugins/inputs/win_wmi/README.md index 50a51fd46..748858731 100644 --- a/plugins/inputs/win_wmi/README.md +++ b/plugins/inputs/win_wmi/README.md @@ -1,16 +1,16 @@ -# 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 +# 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 In addition to the plugin-specific configuration settings, plugins support additional global and plugin configuration settings. These settings are used to @@ -19,9 +19,9 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details. [CONFIGURATION.md]: ../../../docs/CONFIGURATION.md#plugins -## Configuration - -```toml @sample.conf +## Configuration + +```toml @sample.conf # Input plugin to query Windows Management Instrumentation # This plugin ONLY supports Windows [[inputs.win_wmi]] @@ -36,262 +36,262 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details. filter = 'NOT Name LIKE "\\\\?\\%"' # WMI class properties which should be considered tags instead of fields tag_properties = ["Name"] -``` - -### namespace - -A string representing the WMI namespace to be queried. For example, -`root\\cimv2`. - -### class_name - -A string representing the WMI class to be queried. For example, -`Win32_Processor`. - -### properties - -An array of strings representing the properties of the WMI class to be queried. - -### filter - -A string specifying a WHERE clause to use as a filter for the WMI Query -Language (WQL). See [WHERE Clause][WHERE] for more information. - -[WHERE]: https://learn.microsoft.com/en-us/windows/win32/wmisdk/where-clause?source=recommendations - -### tag_properties - -Properties which should be considered tags instead of fields. - -## 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 -``` +``` + +### namespace + +A string representing the WMI namespace to be queried. For example, +`root\\cimv2`. + +### class_name + +A string representing the WMI class to be queried. For example, +`Win32_Processor`. + +### properties + +An array of strings representing the properties of the WMI class to be queried. + +### filter + +A string specifying a WHERE clause to use as a filter for the WMI Query +Language (WQL). See [WHERE Clause][WHERE] for more information. + +[WHERE]: https://learn.microsoft.com/en-us/windows/win32/wmisdk/where-clause?source=recommendations + +### tag_properties + +Properties which should be considered tags instead of fields. + +## 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 +``` diff --git a/plugins/inputs/win_wmi/query.go b/plugins/inputs/win_wmi/query.go new file mode 100644 index 000000000..d19993cb1 --- /dev/null +++ b/plugins/inputs/win_wmi/query.go @@ -0,0 +1,158 @@ +//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/filter" + "github.com/influxdata/telegraf/internal" +) + +// Query struct +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"` + + tagFilter filter.Filter + query string +} + +func (q *Query) prepare() 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 + + // 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", nil, q.Namespace) + 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 := map[string]string{}, map[string]interface{}{} + + 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 +} diff --git a/plugins/inputs/win_wmi/win_wmi.go b/plugins/inputs/win_wmi/win_wmi.go index 75c8e1aef..0e584e74d 100644 --- a/plugins/inputs/win_wmi/win_wmi.go +++ b/plugins/inputs/win_wmi/win_wmi.go @@ -1,235 +1,54 @@ +//go:generate ../../../tools/readme_config_includer/generator //go:build windows -// +build windows package win_wmi import ( _ "embed" - "errors" "fmt" - "runtime" - "strings" "sync" - "github.com/go-ole/go-ole" - "github.com/go-ole/go-ole/oleutil" - "github.com/influxdata/telegraf" - "github.com/influxdata/telegraf/filter" - "github.com/influxdata/telegraf/internal" "github.com/influxdata/telegraf/plugins/inputs" ) //go:embed sample.conf var sampleConfig string -// Query struct -type Query struct { - query string - Namespace string `toml:"namespace"` - ClassName string `toml:"class_name"` - Properties []string `toml:"properties"` - Filter string `toml:"filter"` - TagPropertiesInclude []string `toml:"tag_properties"` - tagFilter filter.Filter -} - // Wmi struct type Wmi struct { - Queries []Query `toml:"query"` - Log telegraf.Logger + Queries []Query `toml:"query"` + Log telegraf.Logger `toml:"-"` } // S_FALSE is returned by CoInitializeEx if it was already called on this thread. const sFalse = 0x00000001 -func oleInt64(item *ole.IDispatch, prop string) (int64, error) { - v, err := oleutil.GetProperty(item, prop) - if err != nil { - return 0, err - } - defer v.Clear() - - return v.Val, nil -} - // Init function -func (s *Wmi) Init() error { - return compileInputs(s) +func (w *Wmi) Init() error { + for i := range w.Queries { + q := &w.Queries[i] + if err := q.prepare(); err != nil { + return fmt.Errorf("preparing query %q failed: %w", q.ClassName, err) + } + } + + return nil } // SampleConfig function -func (s *Wmi) SampleConfig() string { +func (*Wmi) SampleConfig() string { return sampleConfig } -func compileInputs(s *Wmi) error { - buildWqlStatements(s) - return compileTagFilters(s) -} - -func compileTagFilters(s *Wmi) error { - for i, q := range s.Queries { - var err error - s.Queries[i].tagFilter, err = compileTagFilter(q) - if err != nil { - return err - } - } - return nil -} - -func compileTagFilter(q Query) (filter.Filter, error) { - tagFilter, err := filter.NewIncludeExcludeFilterDefaults(q.TagPropertiesInclude, nil, false, false) - if err != nil { - return nil, fmt.Errorf("creating tag filter failed: %w", err) - } - return tagFilter, nil -} - -// build a WMI query from input configuration -func buildWqlStatements(s *Wmi) { - for i, q := range s.Queries { - wql := fmt.Sprintf("SELECT %s FROM %s", strings.Join(q.Properties, ", "), q.ClassName) - if len(q.Filter) > 0 { - wql = fmt.Sprintf("%s WHERE %s", wql, q.Filter) - } - s.Queries[i].query = wql - } -} - -func (q *Query) doQuery(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", nil, q.Namespace) - 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() - - count, err := oleInt64(result, "Count") - if err != nil { - return fmt.Errorf("failed getting Count: %w", err) - } - - for i := int64(0); i < count; i++ { - // item is a SWbemObject - itemRaw, err := oleutil.CallMethod(result, "ItemIndex", i) - if err != nil { - return fmt.Errorf("failed calling method ItemIndex: %w", err) - } - - err = q.extractProperties(itemRaw, acc) - if err != nil { - return err - } - } - return nil -} - -func (q *Query) extractProperties(itemRaw *ole.VARIANT, acc telegraf.Accumulator) error { - tags, fields := map[string]string{}, map[string]interface{}{} - - item := itemRaw.ToIDispatch() - defer item.Release() - - for _, wmiProperty := range q.Properties { - propertyValue, err := getPropertyValue(item, wmiProperty) - if err != nil { - return err - } - - if q.tagFilter.Match(wmiProperty) { - valStr, err := internal.ToString(propertyValue) - if err != nil { - return fmt.Errorf("converting property %q failed: %w", wmiProperty, err) - } - tags[wmiProperty] = valStr - } else { - var fieldValue interface{} - switch v := propertyValue.(type) { - case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64: - fieldValue = v - case string: - fieldValue = v - case bool: - fieldValue = v - case []byte: - fieldValue = string(v) - case fmt.Stringer: - fieldValue = v.String() - case nil: - fieldValue = nil - default: - return fmt.Errorf("property %q of type \"%T\" unsupported", wmiProperty, v) - } - fields[wmiProperty] = fieldValue - } - } - acc.AddFields(q.ClassName, fields, tags) - return nil -} - -func getPropertyValue(item *ole.IDispatch, wmiProperty string) (interface{}, error) { - prop, err := oleutil.GetProperty(item, wmiProperty) - if err != nil { - return nil, fmt.Errorf("failed GetProperty: %w", err) - } - defer prop.Clear() - - return prop.Value(), nil -} - // Gather function -func (s *Wmi) Gather(acc telegraf.Accumulator) error { +func (w *Wmi) Gather(acc telegraf.Accumulator) error { var wg sync.WaitGroup - for _, query := range s.Queries { + for _, query := range w.Queries { wg.Add(1) go func(q Query) { defer wg.Done() - err := q.doQuery(acc) - if err != nil { - acc.AddError(err) - } + acc.AddError(q.execute(acc)) }(query) } wg.Wait() diff --git a/plugins/inputs/win_wmi/win_wmi_notwindows.go b/plugins/inputs/win_wmi/win_wmi_notwindows.go index 70f02f218..1e85a2fe5 100644 --- a/plugins/inputs/win_wmi/win_wmi_notwindows.go +++ b/plugins/inputs/win_wmi/win_wmi_notwindows.go @@ -21,11 +21,9 @@ func (w *Wmi) Init() error { w.Log.Warn("current platform is not supported") return nil } -func (w *Wmi) SampleConfig() string { return sampleConfig } -func (w *Wmi) Gather(_ telegraf.Accumulator) error { return nil } +func (*Wmi) SampleConfig() string { return sampleConfig } +func (*Wmi) Gather(_ telegraf.Accumulator) error { return nil } func init() { - inputs.Add("win_wmi", func() telegraf.Input { - return &Wmi{} - }) + inputs.Add("win_wmi", func() telegraf.Input { return &Wmi{} }) }