mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-17 01:59:04 +02:00
dmi: add dmi_info metric via GetSystemFirmwareTable (#2500)
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
# dmi collector
|
||||
|
||||
The dmi collector exposes Desktop Management Interface (DMI) / SMBIOS system
|
||||
information, compatible with node_exporter's
|
||||
[`dmi` collector](https://github.com/prometheus/node_exporter/blob/master/collector/dmi.go).
|
||||
|
||||
|||
|
||||
-|-
|
||||
Metric name prefix | `dmi`
|
||||
Data source | [`GetSystemFirmwareTable`](https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-getsystemfirmwaretable) (SMBIOS)
|
||||
Enabled by default? | No
|
||||
|
||||
## Flags
|
||||
|
||||
None
|
||||
|
||||
## Metrics
|
||||
|
||||
| Name | Description | Type | Labels |
|
||||
|---------------------|-----------------------------------------------------------------------------------------------------|-------|----------------------------------------------------------------------------|
|
||||
| `windows_dmi_info` | A metric with a constant '1' value labeled by SMBIOS Type 1 (System Information) fields. | gauge | `product_name`, `product_serial`, `product_uuid`, `product_version`, `system_vendor` |
|
||||
|
||||
On VMs the labels reflect the hypervisor-assigned identity; on physical hosts
|
||||
they reflect the hardware SMBIOS data. If SMBIOS data cannot be read (e.g. in
|
||||
some container environments), the collector will fail to build.
|
||||
|
||||
### Example metric
|
||||
|
||||
```
|
||||
# HELP windows_dmi_info A metric with a constant '1' value labeled by product_name, product_serial, product_uuid, product_version, system_vendor from SMBIOS Type 1 (System Information).
|
||||
# TYPE windows_dmi_info gauge
|
||||
windows_dmi_info{product_name="Standard PC (Q35 + ICH9, 2009)",product_serial="Not Specified",product_uuid="12345678-1234-1234-1234-123456789abc",product_version="pc-q35-9.2",system_vendor="QEMU"} 1
|
||||
```
|
||||
|
||||
## Useful queries
|
||||
|
||||
### Cross-exporter compatibility
|
||||
|
||||
The label names match node_exporter's `node_dmi_info` metric, so dashboards
|
||||
and alerts work across Linux and Windows guests:
|
||||
|
||||
```promql
|
||||
# Works for both Linux and Windows guests
|
||||
{__name__=~"node_dmi_info|windows_dmi_info"}
|
||||
```
|
||||
|
||||
### Correlate with libvirt_exporter
|
||||
|
||||
With [prometheus-libvirt-exporter](https://github.com/inovex/prometheus-libvirt-exporter),
|
||||
the domain UUID is exposed on `libvirt_domain_info_meta{uuid="..."}`. This UUID
|
||||
matches the SMBIOS UUID passed to the guest by QEMU/KVM.
|
||||
|
||||
Add the `product_uuid` label to any guest metric:
|
||||
|
||||
```promql
|
||||
windows_memory_physical_free_bytes
|
||||
* on(job, instance) group_left(product_uuid)
|
||||
windows_dmi_info
|
||||
```
|
||||
|
||||
Join a host-side libvirt metric with the guest-identified UUID (full guest→host chain):
|
||||
|
||||
```promql
|
||||
libvirt_domain_info_memory_usage_bytes
|
||||
* on(job, instance, domain) group_left(uuid)
|
||||
libvirt_domain_info_meta
|
||||
* on(uuid) group_left()
|
||||
label_replace(windows_dmi_info, "uuid", "$1", "product_uuid", "(.+)")
|
||||
```
|
||||
|
||||
## Alerting examples
|
||||
|
||||
_This collector does not yet have alerting examples, we would appreciate your help adding them!_
|
||||
@@ -5,7 +5,7 @@ The os collector exposes metrics about the operating system
|
||||
|||
|
||||
-|-
|
||||
Metric name prefix | `os`
|
||||
Classes | [`Win32_OperatingSystem`](https://msdn.microsoft.com/en-us/library/aa394239)
|
||||
Data sources | Registry
|
||||
Enabled by default? | Yes
|
||||
|
||||
## Flags
|
||||
@@ -26,7 +26,7 @@ None
|
||||
# HELP windows_os_hostname Labelled system hostname information as provided by ComputerSystem.DNSHostName and ComputerSystem.Domain
|
||||
# TYPE windows_os_hostname gauge
|
||||
windows_os_hostname{domain="",fqdn="PC",hostname="PC"} 1
|
||||
# HELP windows_os_info Contains full product name & version in labels. Note that the "major_version" for Windows 11 is \\"10\\"; a build number greater than 22000 represents Windows 11.
|
||||
# HELP windows_os_info Contains full product name & version in labels. Note that the "major_version" for Windows 11 is \\\"10\\\"; a build number greater than 22000 represents Windows 11.
|
||||
# TYPE windows_os_info gauge
|
||||
windows_os_info{build_number="19045",installation_type="Client",major_version="10",minor_version="0",product="Windows 10 Pro",revision="4842",version="10.0.19045"} 1
|
||||
# HELP windows_os_install_time_timestamp_seconds Unix timestamp of OS installation time
|
||||
@@ -35,7 +35,7 @@ windows_os_install_time_timestamp_seconds 1.6725312e+09
|
||||
```
|
||||
|
||||
## Useful queries
|
||||
_This collector does not yet have useful queries, we would appreciate your help adding them!_
|
||||
_This collector does not yet have useful query examples, we would appreciate your help adding them!_
|
||||
|
||||
## Alerting examples
|
||||
_This collector does not yet have alerting examples, we would appreciate your help adding them!_
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Copyright The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package dmi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alecthomas/kingpin/v2"
|
||||
"github.com/prometheus-community/windows_exporter/internal/headers/sysinfoapi"
|
||||
"github.com/prometheus-community/windows_exporter/internal/mi"
|
||||
"github.com/prometheus-community/windows_exporter/internal/types"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const Name = "dmi"
|
||||
|
||||
type Config struct{}
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var ConfigDefaults = Config{}
|
||||
|
||||
// A Collector is a Prometheus Collector for DMI (SMBIOS) metrics.
|
||||
type Collector struct {
|
||||
config Config
|
||||
|
||||
dmiInfo *prometheus.Desc
|
||||
}
|
||||
|
||||
func New(config *Config) *Collector {
|
||||
if config == nil {
|
||||
config = &ConfigDefaults
|
||||
}
|
||||
|
||||
c := &Collector{
|
||||
config: *config,
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func NewWithFlags(_ *kingpin.Application) *Collector {
|
||||
return &Collector{}
|
||||
}
|
||||
|
||||
func (c *Collector) GetName() string {
|
||||
return Name
|
||||
}
|
||||
|
||||
func (c *Collector) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
|
||||
info, err := sysinfoapi.GetSMBIOSSystemInfo()
|
||||
if err != nil {
|
||||
return fmt.Errorf("SMBIOS system info unavailable: %w", err)
|
||||
}
|
||||
|
||||
// Sanitize label values: firmware strings may contain invalid UTF-8 bytes.
|
||||
// Use "�" as replacement to match node_exporter's behavior.
|
||||
cleanLabel := func(s string) string {
|
||||
return strings.ToValidUTF8(strings.TrimSpace(s), "�")
|
||||
}
|
||||
|
||||
desc := prometheus.NewDesc(
|
||||
prometheus.BuildFQName(types.Namespace, Name, "info"),
|
||||
"A metric with a constant '1' value labeled by product_name, product_serial, "+
|
||||
"product_uuid, product_version, system_vendor from SMBIOS Type 1 (System Information).",
|
||||
nil,
|
||||
prometheus.Labels{
|
||||
"product_name": cleanLabel(info.ProductName),
|
||||
"product_serial": cleanLabel(info.SerialNumber),
|
||||
"product_uuid": strings.ToLower(cleanLabel(info.UUID)),
|
||||
"product_version": cleanLabel(info.Version),
|
||||
"system_vendor": cleanLabel(info.Manufacturer),
|
||||
},
|
||||
)
|
||||
|
||||
// Validate the descriptor by attempting to create a metric.
|
||||
// If label values are still invalid, log a warning and skip.
|
||||
if _, err = prometheus.NewConstMetric(desc, prometheus.GaugeValue, 1.0); err != nil {
|
||||
logger.Warn("SMBIOS labels produced an invalid metric, skipping dmi_info", slog.Any("err", err))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
c.dmiInfo = desc
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Collect sends the metric values for each metric
|
||||
// to the provided prometheus Metric channel.
|
||||
func (c *Collector) Collect(ch chan<- prometheus.Metric, _ time.Duration) error {
|
||||
if c.dmiInfo != nil {
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.dmiInfo,
|
||||
prometheus.GaugeValue,
|
||||
1.0,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Copyright The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package dmi_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/dmi"
|
||||
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
|
||||
)
|
||||
|
||||
func BenchmarkCollector(b *testing.B) {
|
||||
testutils.FuncBenchmarkCollector(b, dmi.Name, dmi.NewWithFlags)
|
||||
}
|
||||
|
||||
func TestCollector(t *testing.T) {
|
||||
testutils.TestCollector(t, dmi.New, nil)
|
||||
}
|
||||
@@ -41,7 +41,7 @@ type Config struct{}
|
||||
//nolint:gochecknoglobals
|
||||
var ConfigDefaults = Config{}
|
||||
|
||||
// A Collector is a Prometheus Collector for WMI metrics.
|
||||
// A Collector is a Prometheus Collector for OS metrics.
|
||||
type Collector struct {
|
||||
config Config
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Copyright The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package sysinfoapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var procGetSystemFirmwareTable = kernel32.NewProc("GetSystemFirmwareTable")
|
||||
|
||||
// SMBIOS table provider signature ('RSMB').
|
||||
const firmwareTableProviderSigRSMB uint32 = 0x52534D42
|
||||
|
||||
// GetSMBIOSSystemInfo retrieves the SMBIOS Type 1 (System Information) data
|
||||
// using the GetSystemFirmwareTable Win32 API. No WMI dependency.
|
||||
func GetSMBIOSSystemInfo() (*SMBIOSSystemInfo, error) {
|
||||
data, err := getSystemFirmwareTable(firmwareTableProviderSigRSMB, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetSystemFirmwareTable: %w", err)
|
||||
}
|
||||
|
||||
return parseSMBIOSSystemInfo(data)
|
||||
}
|
||||
|
||||
// getSystemFirmwareTable wraps the GetSystemFirmwareTable Win32 API.
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-getsystemfirmwaretable
|
||||
func getSystemFirmwareTable(providerSig, tableID uint32) ([]byte, error) {
|
||||
// First call: determine buffer size.
|
||||
r1, _, err := procGetSystemFirmwareTable.Call(
|
||||
uintptr(providerSig),
|
||||
uintptr(tableID),
|
||||
0,
|
||||
0,
|
||||
)
|
||||
|
||||
size := uint32(r1)
|
||||
if size == 0 {
|
||||
return nil, fmt.Errorf("failed to get firmware table size: %w", err)
|
||||
}
|
||||
|
||||
buf := make([]byte, size)
|
||||
|
||||
r1, _, err = procGetSystemFirmwareTable.Call(
|
||||
uintptr(providerSig),
|
||||
uintptr(tableID),
|
||||
uintptr(unsafe.Pointer(&buf[0])),
|
||||
uintptr(size),
|
||||
)
|
||||
|
||||
written := uint32(r1)
|
||||
if written == 0 {
|
||||
return nil, fmt.Errorf("failed to get firmware table: %w", err)
|
||||
}
|
||||
|
||||
if written > size {
|
||||
return nil, fmt.Errorf("firmware table size changed between calls (need %d, have %d)", written, size)
|
||||
}
|
||||
|
||||
return buf[:written], nil
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Copyright The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package sysinfoapi
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// SMBIOSSystemInfo holds the fields extracted from the SMBIOS Type 1
|
||||
// (System Information) structure.
|
||||
type SMBIOSSystemInfo struct {
|
||||
Manufacturer string
|
||||
ProductName string
|
||||
Version string
|
||||
SerialNumber string
|
||||
UUID string
|
||||
}
|
||||
|
||||
// rawSMBIOSDataHeaderSize is the size of the RawSMBIOSData header returned
|
||||
// by GetSystemFirmwareTable with the 'RSMB' provider:
|
||||
// Used20CallingMethod (1) + SMBIOSMajorVersion (1) + SMBIOSMinorVersion (1) +
|
||||
// DMARevision (1) + Length (4) = 8 bytes.
|
||||
const rawSMBIOSDataHeaderSize = 8
|
||||
|
||||
// parseSMBIOSSystemInfo parses the raw RSMB firmware table and extracts
|
||||
// the Type 1 (System Information) structure.
|
||||
func parseSMBIOSSystemInfo(data []byte) (*SMBIOSSystemInfo, error) {
|
||||
if len(data) < rawSMBIOSDataHeaderSize {
|
||||
return nil, errors.New("SMBIOS data too short for header")
|
||||
}
|
||||
|
||||
smbiosMajor := data[1]
|
||||
smbiosMinor := data[2]
|
||||
|
||||
tableLen := binary.LittleEndian.Uint32(data[4:8])
|
||||
tableData := data[rawSMBIOSDataHeaderSize:]
|
||||
|
||||
if uint32(len(tableData)) < tableLen {
|
||||
return nil, errors.New("SMBIOS table data truncated")
|
||||
}
|
||||
|
||||
tableData = tableData[:tableLen]
|
||||
|
||||
// Walk structures looking for Type 1.
|
||||
offset := 0
|
||||
for offset < len(tableData) {
|
||||
if offset+4 > len(tableData) {
|
||||
break
|
||||
}
|
||||
|
||||
structType := tableData[offset]
|
||||
structLen := int(tableData[offset+1])
|
||||
|
||||
if structLen < 4 || offset+structLen > len(tableData) {
|
||||
break
|
||||
}
|
||||
|
||||
// Collect the unformatted (string) area that follows the formatted part.
|
||||
// It is terminated by a double NUL (\0\0).
|
||||
stringsStart := offset + structLen
|
||||
stringsEnd := stringsStart
|
||||
foundTerminator := false
|
||||
|
||||
for stringsEnd < len(tableData)-1 {
|
||||
if tableData[stringsEnd] == 0 && tableData[stringsEnd+1] == 0 {
|
||||
stringsEnd += 2
|
||||
foundTerminator = true
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
stringsEnd++
|
||||
}
|
||||
|
||||
if !foundTerminator {
|
||||
return nil, errors.New("SMBIOS string area not properly terminated (missing double-NUL)")
|
||||
}
|
||||
|
||||
if structType == 1 && structLen >= 0x08 {
|
||||
// Type 1: System Information
|
||||
// Offsets (within the formatted area):
|
||||
// 04h: Manufacturer (string index)
|
||||
// 05h: Product Name (string index)
|
||||
// 06h: Version (string index)
|
||||
// 07h: Serial Number (string index)
|
||||
// 08h-17h: UUID (16 bytes) — present only when structLen >= 0x19
|
||||
strs := extractStrings(tableData[stringsStart:stringsEnd])
|
||||
|
||||
info := &SMBIOSSystemInfo{
|
||||
Manufacturer: getStringByIndex(strs, tableData[offset+0x04]),
|
||||
ProductName: getStringByIndex(strs, tableData[offset+0x05]),
|
||||
Version: getStringByIndex(strs, tableData[offset+0x06]),
|
||||
SerialNumber: getStringByIndex(strs, tableData[offset+0x07]),
|
||||
}
|
||||
|
||||
// UUID is only available in SMBIOS 2.1+ (structLen >= 0x19).
|
||||
if structLen >= 0x19 {
|
||||
// SMBIOS 2.6+ uses little-endian byte order for the first three UUID fields;
|
||||
// earlier versions use network (big-endian) order throughout.
|
||||
smbios26Plus := smbiosMajor > 2 || (smbiosMajor == 2 && smbiosMinor >= 6)
|
||||
info.UUID = formatUUID(tableData[offset+0x08:offset+0x18], smbios26Plus)
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
offset = stringsEnd
|
||||
}
|
||||
|
||||
return nil, errors.New("SMBIOS Type 1 (System Information) structure not found")
|
||||
}
|
||||
|
||||
// extractStrings splits the unformatted string area into individual strings.
|
||||
// Each string is NUL-terminated; the area ends with a double NUL.
|
||||
// Empty strings are preserved by position so that 1-based indices remain correct.
|
||||
func extractStrings(data []byte) []string {
|
||||
var result []string
|
||||
|
||||
start := 0
|
||||
|
||||
for i := range data {
|
||||
if data[i] == 0 {
|
||||
result = append(result, string(data[start:i]))
|
||||
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// getStringByIndex returns the 1-based string from the list, or empty if
|
||||
// the index is 0 or out of range.
|
||||
func getStringByIndex(strs []string, index byte) string {
|
||||
if index == 0 || int(index) > len(strs) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strs[index-1]
|
||||
}
|
||||
|
||||
// formatUUID formats the 16-byte SMBIOS UUID according to RFC 4122.
|
||||
// When smbios26Plus is true, the first three fields are interpreted as
|
||||
// little-endian (SMBIOS 2.6+ convention). When false, all fields are
|
||||
// in network (big-endian) byte order (pre-2.6 convention).
|
||||
func formatUUID(raw []byte, smbios26Plus bool) string {
|
||||
if len(raw) < 16 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Check for "not present" (all 0xFF) or "not settable" (all 0x00).
|
||||
allFF := true
|
||||
allZero := true
|
||||
|
||||
for _, b := range raw {
|
||||
if b != 0xFF {
|
||||
allFF = false
|
||||
}
|
||||
|
||||
if b != 0x00 {
|
||||
allZero = false
|
||||
}
|
||||
}
|
||||
|
||||
if allFF || allZero {
|
||||
return ""
|
||||
}
|
||||
|
||||
if smbios26Plus {
|
||||
// SMBIOS 2.6+: first three components are little-endian.
|
||||
// TimeLow (4 bytes LE), TimeMid (2 bytes LE), TimeHiAndVersion (2 bytes LE),
|
||||
// ClockSeq (2 bytes BE), Node (6 bytes).
|
||||
return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
|
||||
binary.LittleEndian.Uint32(raw[0:4]),
|
||||
binary.LittleEndian.Uint16(raw[4:6]),
|
||||
binary.LittleEndian.Uint16(raw[6:8]),
|
||||
binary.BigEndian.Uint16(raw[8:10]),
|
||||
raw[10:16],
|
||||
)
|
||||
}
|
||||
|
||||
// Pre-2.6: all fields in network byte order.
|
||||
return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
|
||||
binary.BigEndian.Uint32(raw[0:4]),
|
||||
binary.BigEndian.Uint16(raw[4:6]),
|
||||
binary.BigEndian.Uint16(raw[6:8]),
|
||||
binary.BigEndian.Uint16(raw[8:10]),
|
||||
raw[10:16],
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Copyright The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package sysinfoapi
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFormatUUID(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
raw []byte
|
||||
smbios26Plus bool
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "SMBIOS 2.6+ UUID with LE byte swap",
|
||||
// Stored as: 78 56 34 12 34 12 78 56 12 34 56 78 9a bc de f0
|
||||
// Expected: 12345678-1234-5678-1234-56789abcdef0
|
||||
raw: []byte{0x78, 0x56, 0x34, 0x12, 0x34, 0x12, 0x78, 0x56, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0},
|
||||
smbios26Plus: true,
|
||||
expected: "12345678-1234-5678-1234-56789abcdef0",
|
||||
},
|
||||
{
|
||||
name: "pre-2.6 UUID in network byte order",
|
||||
// All fields in big-endian (network) order.
|
||||
raw: []byte{0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0},
|
||||
smbios26Plus: false,
|
||||
expected: "12345678-1234-5678-1234-56789abcdef0",
|
||||
},
|
||||
{
|
||||
name: "all zeros - not settable",
|
||||
raw: make([]byte, 16),
|
||||
smbios26Plus: true,
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "all 0xFF - not present",
|
||||
raw: []byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF},
|
||||
smbios26Plus: true,
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "too short",
|
||||
raw: []byte{0x01, 0x02},
|
||||
smbios26Plus: true,
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, tc.expected, formatUUID(tc.raw, tc.smbios26Plus))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractStrings(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
data := []byte("QEMU\x00Standard PC\x00v1.0\x00\x00")
|
||||
result := extractStrings(data)
|
||||
// The trailing double-NUL produces a final empty string entry;
|
||||
// the first three entries correspond to the actual firmware strings.
|
||||
require.GreaterOrEqual(t, len(result), 3)
|
||||
assert.Equal(t, "QEMU", result[0])
|
||||
assert.Equal(t, "Standard PC", result[1])
|
||||
assert.Equal(t, "v1.0", result[2])
|
||||
}
|
||||
|
||||
func TestExtractStringsEmpty(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Empty strings between NULs must be preserved by position.
|
||||
data := []byte("first\x00\x00third\x00\x00")
|
||||
result := extractStrings(data)
|
||||
require.GreaterOrEqual(t, len(result), 3)
|
||||
assert.Equal(t, "first", result[0])
|
||||
assert.Empty(t, result[1])
|
||||
assert.Equal(t, "third", result[2])
|
||||
}
|
||||
|
||||
func TestGetStringByIndex(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
strs := []string{"first", "second", "third"}
|
||||
assert.Empty(t, getStringByIndex(strs, 0))
|
||||
assert.Equal(t, "first", getStringByIndex(strs, 1))
|
||||
assert.Equal(t, "second", getStringByIndex(strs, 2))
|
||||
assert.Equal(t, "third", getStringByIndex(strs, 3))
|
||||
assert.Empty(t, getStringByIndex(strs, 4))
|
||||
}
|
||||
|
||||
func TestParseSMBIOSSystemInfo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Build a minimal RSMB blob with a Type 1 structure.
|
||||
// Header: 8 bytes (rawSMBIOSData).
|
||||
header := []byte{
|
||||
0x00, // Used20CallingMethod
|
||||
0x03, // SMBIOSMajorVersion
|
||||
0x02, // SMBIOSMinorVersion
|
||||
0x00, // DMARevision
|
||||
0x00, 0x00, 0x00, 0x00, // Length (placeholder)
|
||||
}
|
||||
|
||||
// Type 1 formatted area: 0x19 bytes
|
||||
type1 := make([]byte, 0x19)
|
||||
type1[0] = 1 // Type
|
||||
type1[1] = 0x19 // Length
|
||||
// Handle (2 bytes) - don't care
|
||||
type1[4] = 1 // Manufacturer string index
|
||||
type1[5] = 2 // Product Name string index
|
||||
type1[6] = 3 // Version string index
|
||||
type1[7] = 4 // Serial Number string index
|
||||
|
||||
// UUID at offset 0x08: 16 bytes
|
||||
// Store 12345678-1234-5678-9abc-def012345678 in SMBIOS LE format
|
||||
copy(type1[0x08:], []byte{0x78, 0x56, 0x34, 0x12, 0x34, 0x12, 0x78, 0x56, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78})
|
||||
|
||||
// Strings area
|
||||
strings := []byte("QEMU\x00Standard PC (Q35 + ICH9, 2009)\x00pc-q35-9.2\x00SN-12345\x00\x00")
|
||||
|
||||
// Combine
|
||||
tableData := slices.Concat(type1, strings)
|
||||
tableLen := len(tableData)
|
||||
|
||||
// Fill in length in header
|
||||
header[4] = byte(tableLen)
|
||||
header[5] = byte(tableLen >> 8)
|
||||
header[6] = byte(tableLen >> 16)
|
||||
header[7] = byte(tableLen >> 24)
|
||||
|
||||
data := slices.Concat(header, tableData)
|
||||
|
||||
info, err := parseSMBIOSSystemInfo(data)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "QEMU", info.Manufacturer)
|
||||
assert.Equal(t, "Standard PC (Q35 + ICH9, 2009)", info.ProductName)
|
||||
assert.Equal(t, "pc-q35-9.2", info.Version)
|
||||
assert.Equal(t, "SN-12345", info.SerialNumber)
|
||||
assert.Equal(t, "12345678-1234-5678-9abc-def012345678", info.UUID)
|
||||
}
|
||||
|
||||
func TestParseSMBIOSSystemInfoPre26(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// SMBIOS 2.5 header: UUID bytes in network (big-endian) order.
|
||||
header := []byte{
|
||||
0x00, // Used20CallingMethod
|
||||
0x02, // SMBIOSMajorVersion = 2
|
||||
0x05, // SMBIOSMinorVersion = 5 (pre-2.6)
|
||||
0x00, // DMARevision
|
||||
0x00, 0x00, 0x00, 0x00, // Length (placeholder)
|
||||
}
|
||||
|
||||
type1 := make([]byte, 0x19)
|
||||
type1[0] = 1 // Type
|
||||
type1[1] = 0x19 // Length
|
||||
type1[4] = 1 // Manufacturer string index
|
||||
type1[5] = 2 // Product Name string index
|
||||
type1[6] = 3 // Version string index
|
||||
type1[7] = 4 // Serial Number string index
|
||||
|
||||
// UUID in network byte order: 12345678-1234-5678-9abc-def012345678
|
||||
copy(type1[0x08:], []byte{0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78})
|
||||
|
||||
strs := []byte("QEMU\x00Standard PC\x00v1.0\x00SN-001\x00\x00")
|
||||
|
||||
tableData := slices.Concat(type1, strs)
|
||||
tableLen := len(tableData)
|
||||
header[4] = byte(tableLen)
|
||||
header[5] = byte(tableLen >> 8)
|
||||
header[6] = byte(tableLen >> 16)
|
||||
header[7] = byte(tableLen >> 24)
|
||||
|
||||
data := slices.Concat(header, tableData)
|
||||
|
||||
info, err := parseSMBIOSSystemInfo(data)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "12345678-1234-5678-9abc-def012345678", info.UUID)
|
||||
assert.Equal(t, "QEMU", info.Manufacturer)
|
||||
}
|
||||
|
||||
func TestParseSMBIOSSystemInfoSMBIOS20(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// SMBIOS 2.0 Type 1 structure: only 8 bytes (no UUID field).
|
||||
header := []byte{
|
||||
0x00, // Used20CallingMethod
|
||||
0x02, // SMBIOSMajorVersion = 2
|
||||
0x00, // SMBIOSMinorVersion = 0
|
||||
0x00, // DMARevision
|
||||
0x00, 0x00, 0x00, 0x00, // Length (placeholder)
|
||||
}
|
||||
|
||||
type1 := make([]byte, 0x08)
|
||||
type1[0] = 1 // Type
|
||||
type1[1] = 0x08 // Length (8 bytes, no UUID)
|
||||
type1[4] = 1 // Manufacturer string index
|
||||
type1[5] = 2 // Product Name string index
|
||||
type1[6] = 3 // Version string index
|
||||
type1[7] = 4 // Serial Number string index
|
||||
|
||||
strs := []byte("OldBIOS\x00Legacy PC\x00v0.1\x00SN-OLD\x00\x00")
|
||||
|
||||
tableData := slices.Concat(type1, strs)
|
||||
tableLen := len(tableData)
|
||||
header[4] = byte(tableLen)
|
||||
header[5] = byte(tableLen >> 8)
|
||||
header[6] = byte(tableLen >> 16)
|
||||
header[7] = byte(tableLen >> 24)
|
||||
|
||||
data := slices.Concat(header, tableData)
|
||||
|
||||
info, err := parseSMBIOSSystemInfo(data)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "OldBIOS", info.Manufacturer)
|
||||
assert.Equal(t, "Legacy PC", info.ProductName)
|
||||
assert.Equal(t, "v0.1", info.Version)
|
||||
assert.Equal(t, "SN-OLD", info.SerialNumber)
|
||||
assert.Empty(t, info.UUID, "UUID should be empty for 8-byte SMBIOS 2.0 Type 1 structures")
|
||||
}
|
||||
|
||||
func TestParseSMBIOSSystemInfoWithType0Preceding(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Real firmware tables almost always have Type 0 (BIOS Information) before Type 1.
|
||||
// Verify the walker correctly skips Type 0 and finds Type 1.
|
||||
header := []byte{
|
||||
0x00, // Used20CallingMethod
|
||||
0x03, // SMBIOSMajorVersion
|
||||
0x02, // SMBIOSMinorVersion
|
||||
0x00, // DMARevision
|
||||
0x00, 0x00, 0x00, 0x00, // Length (placeholder)
|
||||
}
|
||||
|
||||
// Type 0 (BIOS Information): 0x12 bytes formatted area.
|
||||
type0 := make([]byte, 0x12)
|
||||
type0[0] = 0 // Type 0
|
||||
type0[1] = 0x12 // Length
|
||||
type0[4] = 1 // Vendor string index
|
||||
type0[5] = 2 // BIOS Version string index
|
||||
|
||||
type0Strs := []byte("BIOS Vendor\x00BIOS v1.0\x00\x00")
|
||||
|
||||
// Type 1 (System Information): 0x19 bytes formatted area.
|
||||
type1 := make([]byte, 0x19)
|
||||
type1[0] = 1 // Type 1
|
||||
type1[1] = 0x19 // Length
|
||||
type1[4] = 1 // Manufacturer string index
|
||||
type1[5] = 2 // Product Name string index
|
||||
type1[6] = 3 // Version string index
|
||||
type1[7] = 4 // Serial Number string index
|
||||
|
||||
copy(type1[0x08:], []byte{0x78, 0x56, 0x34, 0x12, 0x34, 0x12, 0x78, 0x56, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78})
|
||||
|
||||
type1Strs := []byte("Dell Inc.\x00PowerEdge R740\x00v2.0\x00SN-ABCDE\x00\x00")
|
||||
|
||||
tableData := slices.Concat(type0, type0Strs, type1, type1Strs)
|
||||
tableLen := len(tableData)
|
||||
header[4] = byte(tableLen)
|
||||
header[5] = byte(tableLen >> 8)
|
||||
header[6] = byte(tableLen >> 16)
|
||||
header[7] = byte(tableLen >> 24)
|
||||
|
||||
data := slices.Concat(header, tableData)
|
||||
|
||||
info, err := parseSMBIOSSystemInfo(data)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Dell Inc.", info.Manufacturer)
|
||||
assert.Equal(t, "PowerEdge R740", info.ProductName)
|
||||
assert.Equal(t, "v2.0", info.Version)
|
||||
assert.Equal(t, "SN-ABCDE", info.SerialNumber)
|
||||
assert.Equal(t, "12345678-1234-5678-9abc-def012345678", info.UUID)
|
||||
}
|
||||
|
||||
func TestParseSMBIOSSystemInfoUnterminatedStrings(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Build a Type 1 structure whose string area lacks the double-NUL terminator.
|
||||
header := []byte{
|
||||
0x00, // Used20CallingMethod
|
||||
0x03, // SMBIOSMajorVersion
|
||||
0x02, // SMBIOSMinorVersion
|
||||
0x00, // DMARevision
|
||||
0x00, 0x00, 0x00, 0x00, // Length (placeholder)
|
||||
}
|
||||
|
||||
type1 := make([]byte, 0x19)
|
||||
type1[0] = 1 // Type
|
||||
type1[1] = 0x19 // Length
|
||||
type1[4] = 1 // Manufacturer string index
|
||||
|
||||
// String area with only a single NUL (missing the terminating double-NUL).
|
||||
strs := []byte("QEMU\x00")
|
||||
|
||||
tableData := slices.Concat(type1, strs)
|
||||
tableLen := len(tableData)
|
||||
header[4] = byte(tableLen)
|
||||
header[5] = byte(tableLen >> 8)
|
||||
header[6] = byte(tableLen >> 16)
|
||||
header[7] = byte(tableLen >> 24)
|
||||
|
||||
data := slices.Concat(header, tableData)
|
||||
|
||||
_, err := parseSMBIOSSystemInfo(data)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not properly terminated")
|
||||
}
|
||||
|
||||
func FuzzParseSMBIOSSystemInfo(f *testing.F) {
|
||||
// Seed with a valid RSMB blob.
|
||||
header := []byte{
|
||||
0x00, 0x03, 0x02, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
}
|
||||
|
||||
type1 := make([]byte, 0x19)
|
||||
type1[0] = 1
|
||||
type1[1] = 0x19
|
||||
type1[4] = 1
|
||||
type1[5] = 2
|
||||
type1[6] = 3
|
||||
type1[7] = 4
|
||||
|
||||
copy(type1[0x08:], []byte{0x78, 0x56, 0x34, 0x12, 0x34, 0x12, 0x78, 0x56, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78})
|
||||
|
||||
strs := []byte("QEMU\x00Standard PC\x00v1.0\x00SN-001\x00\x00")
|
||||
tableData := slices.Concat(type1, strs)
|
||||
tableLen := len(tableData)
|
||||
header[4] = byte(tableLen)
|
||||
header[5] = byte(tableLen >> 8)
|
||||
header[6] = byte(tableLen >> 16)
|
||||
header[7] = byte(tableLen >> 24)
|
||||
|
||||
f.Add(slices.Concat(header, tableData))
|
||||
|
||||
// Also seed with an empty input and a truncated one.
|
||||
f.Add([]byte{})
|
||||
f.Add(header[:4])
|
||||
|
||||
f.Fuzz(func(t *testing.T, data []byte) {
|
||||
// Must not panic on any input.
|
||||
_, _ = parseSMBIOSSystemInfo(data)
|
||||
})
|
||||
}
|
||||
@@ -38,6 +38,7 @@ import (
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/dfsr"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/dhcp"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/diskdrive"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/dmi"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/dns"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/exchange"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/file"
|
||||
@@ -109,6 +110,7 @@ func NewWithConfig(config Config) *Collection {
|
||||
collectors[dfsr.Name] = dfsr.New(&config.DFSR)
|
||||
collectors[dhcp.Name] = dhcp.New(&config.Dhcp)
|
||||
collectors[diskdrive.Name] = diskdrive.New(&config.DiskDrive)
|
||||
collectors[dmi.Name] = dmi.New(&config.DMI)
|
||||
collectors[dns.Name] = dns.New(&config.DNS)
|
||||
collectors[exchange.Name] = exchange.New(&config.Exchange)
|
||||
collectors[file.Name] = file.New(&config.File)
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/dfsr"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/dhcp"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/diskdrive"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/dmi"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/dns"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/exchange"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/file"
|
||||
@@ -79,6 +80,7 @@ type Config struct {
|
||||
DFSR dfsr.Config `yaml:"dfsr"`
|
||||
Dhcp dhcp.Config `yaml:"dhcp"`
|
||||
DiskDrive diskdrive.Config `yaml:"diskdrive"`
|
||||
DMI dmi.Config `yaml:"dmi"`
|
||||
DNS dns.Config `yaml:"dns"`
|
||||
Exchange exchange.Config `yaml:"exchange"`
|
||||
File file.Config `yaml:"file"`
|
||||
@@ -134,6 +136,7 @@ var ConfigDefaults = Config{
|
||||
DFSR: dfsr.ConfigDefaults,
|
||||
Dhcp: dhcp.ConfigDefaults,
|
||||
DiskDrive: diskdrive.ConfigDefaults,
|
||||
DMI: dmi.ConfigDefaults,
|
||||
DNS: dns.ConfigDefaults,
|
||||
Exchange: exchange.ConfigDefaults,
|
||||
File: file.ConfigDefaults,
|
||||
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/dfsr"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/dhcp"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/diskdrive"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/dmi"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/dns"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/exchange"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/file"
|
||||
@@ -90,6 +91,7 @@ var BuildersWithFlags = map[string]BuilderWithFlags[Collector]{
|
||||
dfsr.Name: NewBuilderWithFlags(dfsr.NewWithFlags),
|
||||
dhcp.Name: NewBuilderWithFlags(dhcp.NewWithFlags),
|
||||
diskdrive.Name: NewBuilderWithFlags(diskdrive.NewWithFlags),
|
||||
dmi.Name: NewBuilderWithFlags(dmi.NewWithFlags),
|
||||
dns.Name: NewBuilderWithFlags(dns.NewWithFlags),
|
||||
exchange.Name: NewBuilderWithFlags(exchange.NewWithFlags),
|
||||
file.Name: NewBuilderWithFlags(file.NewWithFlags),
|
||||
|
||||
Reference in New Issue
Block a user