mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-27 22:19:05 +01:00
mscluster: collect metrics through ClusAPI (#2634)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f292a6fbcf
commit
1302cf6899
@@ -0,0 +1,40 @@
|
||||
# mscluster agent notes
|
||||
|
||||
- All subcollectors read through ClusAPI
|
||||
([`internal/headers/clusapi`](../../headers/clusapi)) instead of the
|
||||
`root\MSCluster` WMI provider (ClusWMI.dll). The WMI provider is a thin layer
|
||||
over ClusAPI: most WMI properties are common properties from
|
||||
`CLUSCTL_<OBJECT>_GET_RO_COMMON_PROPERTIES` and
|
||||
`CLUSCTL_<OBJECT>_GET_COMMON_PROPERTIES`; `Characteristics`, `Flags` and
|
||||
`State` come from their own control codes and `GetCluster<Object>State`.
|
||||
Some WMI values are synthesized or never filled (published as 0 from NULL);
|
||||
the parity tests show which ones, and the collector reproduces them.
|
||||
- Keep WMI types when publishing: `sint32` properties (for example
|
||||
`FailbackWindowStart`) are converted from the raw 32-bit value with
|
||||
`int32`, `uint32` properties keep the unsigned bit pattern. Unknown states
|
||||
(-1) are values, as in WMI, not errors.
|
||||
- Each subcollector owns its own `clusapi.Cluster`. A `Cluster` runs one call
|
||||
at a time without holding its lock: ClusAPI RPCs cannot be cancelled, so a
|
||||
second call fails with `clusapi.ErrBusy`, and `Close` leaves the handle to
|
||||
the running call. `callSource` stops waiting at the scrape budget, so one
|
||||
stuck RPC does not hold `Collect` and the other subcollectors.
|
||||
- `closeSources` drops a source even when its `Close` fails, so `Build` can
|
||||
recreate it. Grafana Alloy calls `Build`/`Close` repeatedly.
|
||||
- Every subcollector has a fixture test that checks gathered metric families
|
||||
and a parity test against its WMI class. The CI cluster (`setup-mscluster`
|
||||
in `.github/workflows/ci.yml`) is a single node without storage or
|
||||
administrative access point. It has online, offline and failed resources in
|
||||
two groups (`CIResources`, `CIFailedResources`) on a private network of an
|
||||
internal Hyper-V switch; the parity tests require them. It has no clustered
|
||||
disks, so shared volume parity still compares empty sets.
|
||||
- Property lists returned by the cluster service end with an extra
|
||||
`CLUSPROP_SYNTAX_ENDMARK` after the last value list; fixtures built only from
|
||||
the documentation miss this. The parser validates only the structure and
|
||||
accepts zero padding. `TestDumpPropertyLists` logs real lists as hex when
|
||||
`WINDOWS_EXPORTER_TEST_CLUSAPI_DUMP` is set on a cluster node.
|
||||
- `clusapi.Open` reports an unconfigured cluster or a missing ClusAPI with
|
||||
`errors.ErrUnsupported`; the shared collector test treats that as an
|
||||
unsupported role on hosts where `mscluster` is not a required fixture.
|
||||
- Use `ClusterOpenEnum` and the `Open*Ex` functions (Windows Server 2008 R2+),
|
||||
not `ClusterOpenEnumEx` (Windows Server 2016+), to keep Windows Server
|
||||
2012 R2 support.
|
||||
@@ -18,11 +18,14 @@
|
||||
package mscluster
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/alecthomas/kingpin/v2"
|
||||
@@ -42,6 +45,11 @@ const (
|
||||
subCollectorSharedVolumes = "shared_volumes"
|
||||
)
|
||||
|
||||
// nativeSubCollectors read through ClusAPI and do not need an MI session.
|
||||
//
|
||||
//nolint:gochecknoglobals
|
||||
var nativeSubCollectors = []string{subCollectorCluster, subCollectorNetwork, subCollectorNode, subCollectorResource, subCollectorResourceGroup, subCollectorSharedVolumes}
|
||||
|
||||
type Config struct {
|
||||
CollectorsEnabled []string `yaml:"enabled"`
|
||||
}
|
||||
@@ -67,8 +75,9 @@ type Collector struct {
|
||||
collectorResourceGroup
|
||||
collectorSharedVolumes
|
||||
|
||||
config Config
|
||||
miSession *mi.Session
|
||||
lifecycleMu sync.Mutex
|
||||
config Config
|
||||
miSession *mi.Session
|
||||
}
|
||||
|
||||
func New(config *Config) *Collector {
|
||||
@@ -114,12 +123,122 @@ func (c *Collector) GetName() string {
|
||||
}
|
||||
|
||||
func (c *Collector) Close() error {
|
||||
return nil
|
||||
c.lifecycleMu.Lock()
|
||||
defer c.lifecycleMu.Unlock()
|
||||
|
||||
return c.closeSources()
|
||||
}
|
||||
|
||||
// closeSources releases the ClusAPI sources. A source whose Close fails is
|
||||
// dropped as well: the clusapi.Cluster has already released its handle, and a
|
||||
// later Build must be able to create new sources (Grafana Alloy rebuilds
|
||||
// collectors in place).
|
||||
func (c *Collector) closeSources() error {
|
||||
var errs []error
|
||||
|
||||
closeSource := func(source io.Closer, release func()) {
|
||||
if source == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := source.Close(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
release()
|
||||
}
|
||||
|
||||
closeSource(c.clusterSource, func() { c.clusterSource = nil })
|
||||
closeSource(c.networkSource, func() { c.networkSource = nil })
|
||||
closeSource(c.nodeSource, func() { c.nodeSource = nil })
|
||||
closeSource(c.resourceSource, func() { c.resourceSource = nil })
|
||||
closeSource(c.resourceGroupSource, func() { c.resourceGroupSource = nil })
|
||||
closeSource(c.sharedVolumesSource, func() { c.sharedVolumesSource = nil })
|
||||
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// errNotBuilt reports a sub-collector without a source, for example when a
|
||||
// library consumer calls Collect after a failed Build.
|
||||
func errNotBuilt(name string) error {
|
||||
return fmt.Errorf("%s collector is not built", name)
|
||||
}
|
||||
|
||||
// callSource runs a ClusAPI read in its own goroutine and waits at most for
|
||||
// the scrape budget. ClusAPI RPCs cannot be cancelled: a read that outlives
|
||||
// the budget keeps running, and its source refuses new reads with
|
||||
// clusapi.ErrBusy until the RPC returns. Returning here keeps one stuck
|
||||
// sub-collector from holding Collect, and with it every other sub-collector.
|
||||
// Late results are dropped; the read never writes to the metric channel.
|
||||
//
|
||||
//nolint:ireturn // T is the concrete result type of each source.
|
||||
func callSource[T any](budget time.Duration, read func(deadline time.Time) (T, error)) (T, error) {
|
||||
var deadline time.Time
|
||||
if budget > 0 {
|
||||
deadline = time.Now().Add(budget)
|
||||
}
|
||||
|
||||
type result struct {
|
||||
value T
|
||||
err error
|
||||
}
|
||||
|
||||
done := make(chan result, 1)
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
done <- result{err: fmt.Errorf("panic in ClusAPI read: %v", r)}
|
||||
}
|
||||
}()
|
||||
|
||||
value, err := read(deadline)
|
||||
done <- result{value: value, err: err}
|
||||
}()
|
||||
|
||||
if budget <= 0 {
|
||||
r := <-done
|
||||
|
||||
return r.value, r.err
|
||||
}
|
||||
|
||||
timer := time.NewTimer(budget)
|
||||
defer timer.Stop()
|
||||
|
||||
select {
|
||||
case r := <-done:
|
||||
return r.value, r.err
|
||||
case <-timer.C:
|
||||
var zero T
|
||||
|
||||
return zero, fmt.Errorf("ClusAPI read did not return within %s: %w", budget, context.DeadlineExceeded)
|
||||
}
|
||||
}
|
||||
|
||||
// remainingBudget returns the part of the scrape budget that is left for a
|
||||
// sub-collector started after another one. Zero means no limit.
|
||||
func remainingBudget(maxScrapeDuration time.Duration, scrapeStarted time.Time) (time.Duration, error) {
|
||||
if maxScrapeDuration <= 0 {
|
||||
return maxScrapeDuration, nil
|
||||
}
|
||||
|
||||
budget := maxScrapeDuration - time.Since(scrapeStarted)
|
||||
if budget <= 0 {
|
||||
return 0, context.DeadlineExceeded
|
||||
}
|
||||
|
||||
return budget, nil
|
||||
}
|
||||
|
||||
func (c *Collector) Build(_ *slog.Logger, miSession *mi.Session) error {
|
||||
c.lifecycleMu.Lock()
|
||||
defer c.lifecycleMu.Unlock()
|
||||
|
||||
// A failed close is reported, but the old sources are dropped and rebuilt.
|
||||
closeErr := c.closeSources()
|
||||
|
||||
if len(c.config.CollectorsEnabled) == 0 {
|
||||
return nil
|
||||
return closeErr
|
||||
}
|
||||
|
||||
subCollectors := []string{
|
||||
@@ -133,14 +252,14 @@ func (c *Collector) Build(_ *slog.Logger, miSession *mi.Session) error {
|
||||
|
||||
for _, name := range c.config.CollectorsEnabled {
|
||||
if !slices.Contains(subCollectors, name) {
|
||||
return fmt.Errorf("unknown sub collector: %s. Possible values: %s", name,
|
||||
return errors.Join(closeErr, fmt.Errorf("unknown sub collector: %s. Possible values: %s", name,
|
||||
strings.Join(subCollectors, ", "),
|
||||
)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
if miSession == nil {
|
||||
return errors.New("miSession is nil")
|
||||
if miSession == nil && slices.ContainsFunc(c.config.CollectorsEnabled, func(name string) bool { return !slices.Contains(nativeSubCollectors, name) }) {
|
||||
return errors.Join(closeErr, errors.New("miSession is nil"))
|
||||
}
|
||||
|
||||
c.miSession = miSession
|
||||
@@ -183,12 +302,21 @@ func (c *Collector) Build(_ *slog.Logger, miSession *mi.Session) error {
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Join(errs...)
|
||||
if len(errs) != 0 {
|
||||
errs = append(errs, c.closeSources())
|
||||
}
|
||||
|
||||
return errors.Join(append(errs, closeErr)...)
|
||||
}
|
||||
|
||||
// Collect sends the metric values for each metric
|
||||
// to the provided prometheus Metric channel.
|
||||
func (c *Collector) Collect(ch chan<- prometheus.Metric, maxScrapeDuration time.Duration) error {
|
||||
c.lifecycleMu.Lock()
|
||||
defer c.lifecycleMu.Unlock()
|
||||
|
||||
scrapeStarted := time.Now()
|
||||
|
||||
if len(c.config.CollectorsEnabled) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -231,7 +359,12 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, maxScrapeDuration time.
|
||||
|
||||
if slices.Contains(c.config.CollectorsEnabled, subCollectorResource) {
|
||||
g.Go(func() error {
|
||||
if err := c.collectResource(ch, maxScrapeDuration, nodeNames); err != nil {
|
||||
budget, err := remainingBudget(maxScrapeDuration, scrapeStarted)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to collect resource metrics: %w", err)
|
||||
}
|
||||
|
||||
if err := c.collectResource(ch, budget, nodeNames); err != nil {
|
||||
return fmt.Errorf("failed to collect resource metrics: %w", err)
|
||||
}
|
||||
|
||||
@@ -241,7 +374,12 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, maxScrapeDuration time.
|
||||
|
||||
if slices.Contains(c.config.CollectorsEnabled, subCollectorResourceGroup) {
|
||||
g.Go(func() error {
|
||||
if err := c.collectResourceGroup(ch, maxScrapeDuration, nodeNames); err != nil {
|
||||
budget, err := remainingBudget(maxScrapeDuration, scrapeStarted)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to collect resource group metrics: %w", err)
|
||||
}
|
||||
|
||||
if err := c.collectResourceGroup(ch, budget, nodeNames); err != nil {
|
||||
return fmt.Errorf("failed to collect resource group metrics: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,10 +18,9 @@
|
||||
package mscluster
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/mi"
|
||||
"github.com/prometheus-community/windows_exporter/internal/headers/clusapi"
|
||||
"github.com/prometheus-community/windows_exporter/internal/osversion"
|
||||
"github.com/prometheus-community/windows_exporter/internal/types"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
@@ -29,8 +28,13 @@ import (
|
||||
|
||||
const nameCluster = Name + "_cluster"
|
||||
|
||||
type clusterSource interface {
|
||||
Properties(deadline time.Time) (clusapi.Object, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
type collectorCluster struct {
|
||||
clusterMIQuery mi.Query
|
||||
clusterSource clusterSource
|
||||
|
||||
clusterAddEvictDelay *prometheus.Desc
|
||||
clusterAdminAccessPoint *prometheus.Desc
|
||||
@@ -111,7 +115,8 @@ type collectorCluster struct {
|
||||
clusterWitnessRestartInterval *prometheus.Desc
|
||||
}
|
||||
|
||||
// msClusterCluster represents the MSCluster_Cluster WMI class
|
||||
// msClusterCluster represents the MSCluster_Cluster WMI class. The collector
|
||||
// reads ClusAPI; the parity test compares against this WMI model.
|
||||
// - https://docs.microsoft.com/en-us/previous-versions/windows/desktop/cluswmi/mscluster-cluster
|
||||
type msClusterCluster struct {
|
||||
Name string `mi:"Name"`
|
||||
@@ -196,20 +201,18 @@ type msClusterCluster struct {
|
||||
}
|
||||
|
||||
func (c *Collector) buildCluster() error {
|
||||
buildNumber := osversion.Build()
|
||||
|
||||
wmiSelect := "AddEvictDelay,AdminAccessPoint,AutoAssignNodeSite,AutoBalancerLevel,AutoBalancerMode,BackupInProgress,BlockCacheSize,ClusSvcHangTimeout,ClusSvcRegroupOpeningTimeout,ClusSvcRegroupPruningTimeout,ClusSvcRegroupStageTimeout,ClusSvcRegroupTickInMilliseconds,ClusterEnforcedAntiAffinity,ClusterFunctionalLevel,ClusterGroupWaitDelay,ClusterLogLevel,ClusterLogSize,ClusterUpgradeVersion,CrossSiteDelay,CrossSiteThreshold,CrossSubnetDelay,CrossSubnetThreshold,CsvBalancer,DatabaseReadWriteMode,DefaultNetworkRole,DisableGroupPreferredOwnerRandomization,DrainOnShutdown,DynamicQuorumEnabled,EnableSharedVolumes,FixQuorum,GracePeriodEnabled,GracePeriodTimeout,GroupDependencyTimeout,HangRecoveryAction,IgnorePersistentStateOnStartup,LogResourceControls,LowerQuorumPriorityNodeId,MessageBufferLength,MinimumNeverPreemptPriority,MinimumPreemptorPriority,NetftIPSecEnabled,PlacementOptions,PlumbAllCrossSubnetRoutes,PreventQuorum,QuarantineDuration,QuarantineThreshold,QuorumArbitrationTimeMax,QuorumArbitrationTimeMin,QuorumLogFileSize,QuorumTypeValue,RequestReplyTimeout,ResiliencyDefaultPeriod,ResiliencyLevel,ResourceDllDeadlockPeriod,RootMemoryReserved,RouteHistoryLength,S2DBusTypes,S2DCacheDesiredState,S2DCacheFlashReservePercent,S2DCachePageSizeKBytes,S2DEnabled,S2DIOLatencyThreshold,S2DOptimizations,SameSubnetDelay,SameSubnetThreshold,SecurityLevel,SharedVolumeVssWriterOperationTimeout,ShutdownTimeoutInMinutes,UseClientAccessNetworksForSharedVolumes,WitnessDatabaseWriteTimeout,WitnessDynamicWeight,WitnessRestartInterval"
|
||||
if buildNumber >= osversion.LTSC2022 {
|
||||
wmiSelect += ",DetectManagedEvents,SecurityLevelForStorage,MaxNumberOfNodes,DetectManagedEventsThreshold,DetectedCloudPlatform"
|
||||
}
|
||||
|
||||
clusterMIQuery, err := mi.NewQuery(fmt.Sprintf("SELECT %s FROM MSCluster_Cluster", wmiSelect))
|
||||
source, err := clusapi.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create WMI query: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
c.clusterMIQuery = clusterMIQuery
|
||||
c.clusterSource = source
|
||||
c.buildClusterDescriptors()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Collector) buildClusterDescriptors() {
|
||||
c.clusterAddEvictDelay = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(types.Namespace, nameCluster, "add_evict_delay"),
|
||||
"Provides access to the cluster's AddEvictDelay property, which is the number a seconds that a new node is delayed after an eviction of another node.",
|
||||
@@ -672,563 +675,116 @@ func (c *Collector) buildCluster() error {
|
||||
[]string{"name"},
|
||||
nil,
|
||||
)
|
||||
|
||||
var dst []msClusterCluster
|
||||
if err := c.miSession.Query(&dst, mi.NamespaceRootMSCluster, c.clusterMIQuery, 0); err != nil {
|
||||
return fmt.Errorf("WMI query failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Collector) collectCluster(ch chan<- prometheus.Metric, maxScrapeDuration time.Duration) error {
|
||||
var dst []msClusterCluster
|
||||
if err := c.miSession.Query(&dst, mi.NamespaceRootMSCluster, c.clusterMIQuery, maxScrapeDuration); err != nil {
|
||||
return fmt.Errorf("WMI query failed: %w", err)
|
||||
if c.clusterSource == nil {
|
||||
return errNotBuilt(subCollectorCluster)
|
||||
}
|
||||
|
||||
for _, v := range dst {
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterAddEvictDelay,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.AddEvictDelay),
|
||||
v.Name,
|
||||
)
|
||||
cluster, resultErr := callSource(maxScrapeDuration, c.clusterSource.Properties)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterAdminAccessPoint,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.AdminAccessPoint),
|
||||
v.Name,
|
||||
)
|
||||
return c.publishCluster(ch, cluster, osversion.Build(), resultErr)
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterAutoAssignNodeSite,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.AutoAssignNodeSite),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterAutoBalancerLevel,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.AutoBalancerLevel),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterAutoBalancerMode,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.AutoBalancerMode),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterBackupInProgress,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.BackupInProgress),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterBlockCacheSize,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.BlockCacheSize),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterClusSvcHangTimeout,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.ClusSvcHangTimeout),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterClusSvcRegroupOpeningTimeout,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.ClusSvcRegroupOpeningTimeout),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterClusSvcRegroupPruningTimeout,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.ClusSvcRegroupPruningTimeout),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterClusSvcRegroupStageTimeout,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.ClusSvcRegroupStageTimeout),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterClusSvcRegroupTickInMilliseconds,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.ClusSvcRegroupTickInMilliseconds),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterClusterEnforcedAntiAffinity,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.ClusterEnforcedAntiAffinity),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterClusterFunctionalLevel,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.ClusterFunctionalLevel),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterClusterGroupWaitDelay,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.ClusterGroupWaitDelay),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterClusterLogLevel,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.ClusterLogLevel),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterClusterLogSize,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.ClusterLogSize),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterClusterUpgradeVersion,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.ClusterUpgradeVersion),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterCrossSiteDelay,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.CrossSiteDelay),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterCrossSiteThreshold,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.CrossSiteThreshold),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterCrossSubnetDelay,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.CrossSubnetDelay),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterCrossSubnetThreshold,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.CrossSubnetThreshold),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterCsvBalancer,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.CsvBalancer),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterDatabaseReadWriteMode,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.DatabaseReadWriteMode),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterDefaultNetworkRole,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.DefaultNetworkRole),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterDisableGroupPreferredOwnerRandomization,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.DisableGroupPreferredOwnerRandomization),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterDrainOnShutdown,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.DrainOnShutdown),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterDynamicQuorumEnabled,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.DynamicQuorumEnabled),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterEnableSharedVolumes,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.EnableSharedVolumes),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterFixQuorum,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.FixQuorum),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterGracePeriodEnabled,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.GracePeriodEnabled),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterGracePeriodTimeout,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.GracePeriodTimeout),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterGroupDependencyTimeout,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.GroupDependencyTimeout),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterHangRecoveryAction,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.HangRecoveryAction),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterIgnorePersistentStateOnStartup,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.IgnorePersistentStateOnStartup),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterLogResourceControls,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.LogResourceControls),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterLowerQuorumPriorityNodeId,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.LowerQuorumPriorityNodeId),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterMessageBufferLength,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.MessageBufferLength),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterMinimumNeverPreemptPriority,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.MinimumNeverPreemptPriority),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterMinimumPreemptorPriority,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.MinimumPreemptorPriority),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterNetftIPSecEnabled,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.NetftIPSecEnabled),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterPlacementOptions,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.PlacementOptions),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterPlumbAllCrossSubnetRoutes,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.PlumbAllCrossSubnetRoutes),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterPreventQuorum,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.PreventQuorum),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterQuarantineDuration,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.QuarantineDuration),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterQuarantineThreshold,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.QuarantineThreshold),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterQuorumArbitrationTimeMax,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.QuorumArbitrationTimeMax),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterQuorumArbitrationTimeMin,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.QuorumArbitrationTimeMin),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterQuorumLogFileSize,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.QuorumLogFileSize),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterQuorumTypeValue,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.QuorumTypeValue),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterRequestReplyTimeout,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.RequestReplyTimeout),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterResiliencyDefaultPeriod,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.ResiliencyDefaultPeriod),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterResiliencyLevel,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.ResiliencyLevel),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterResourceDllDeadlockPeriod,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.ResourceDllDeadlockPeriod),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterRootMemoryReserved,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.RootMemoryReserved),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterRouteHistoryLength,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.RouteHistoryLength),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterS2DBusTypes,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.S2DBusTypes),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterS2DCacheDesiredState,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.S2DCacheDesiredState),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterS2DCacheFlashReservePercent,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.S2DCacheFlashReservePercent),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterS2DCachePageSizeKBytes,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.S2DCachePageSizeKBytes),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterS2DEnabled,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.S2DEnabled),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterS2DIOLatencyThreshold,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.S2DIOLatencyThreshold),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterS2DOptimizations,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.S2DOptimizations),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterSameSubnetDelay,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.SameSubnetDelay),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterSameSubnetThreshold,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.SameSubnetThreshold),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterSecurityLevel,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.SecurityLevel),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterSharedVolumeVssWriterOperationTimeout,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.SharedVolumeVssWriterOperationTimeout),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterShutdownTimeoutInMinutes,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.ShutdownTimeoutInMinutes),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterUseClientAccessNetworksForSharedVolumes,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.UseClientAccessNetworksForSharedVolumes),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterWitnessDatabaseWriteTimeout,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.WitnessDatabaseWriteTimeout),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterWitnessDynamicWeight,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.WitnessDynamicWeight),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterWitnessRestartInterval,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.WitnessRestartInterval),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
if osversion.Build() >= osversion.LTSC2022 {
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterDetectManagedEvents,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.DetectManagedEvents),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterDetectManagedEventsThreshold,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.DetectManagedEventsThreshold),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterSecurityLevelForStorage,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.SecurityLevelForStorage),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterMaxNumberOfNodes,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.MaxNumberOfNodes),
|
||||
v.Name,
|
||||
)
|
||||
func (c *Collector) publishCluster(ch chan<- prometheus.Metric, cluster clusapi.Object, build uint16, resultErr error) error {
|
||||
if cluster.Name == "" {
|
||||
return resultErr
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.clusterDetectedCloudPlatform,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.DetectedCloudPlatform),
|
||||
v.Name,
|
||||
)
|
||||
}
|
||||
const (
|
||||
ws2016 = osversion.LTSC2016
|
||||
ws2022 = osversion.LTSC2022
|
||||
)
|
||||
|
||||
// The WMI provider reports properties that the cluster service no longer
|
||||
// has (documented as obsolete or unsupported, or removed in newer
|
||||
// releases) as NULL, which the previous collector published as 0. It
|
||||
// reports MaxNumberOfNodes as the 64 node limit of Windows Server failover
|
||||
// clusters. Both were confirmed against MSCluster_Cluster on Windows Server
|
||||
// 2022.
|
||||
zero, maxNodes := new(uint32(0)), new(uint32(64))
|
||||
|
||||
fields := []objectField{
|
||||
{name: "AddEvictDelay", desc: c.clusterAddEvictDelay},
|
||||
{name: "AdminAccessPoint", desc: c.clusterAdminAccessPoint},
|
||||
{name: "AutoAssignNodeSite", desc: c.clusterAutoAssignNodeSite},
|
||||
{name: "AutoBalancerLevel", desc: c.clusterAutoBalancerLevel, minBuild: ws2016},
|
||||
{name: "AutoBalancerMode", desc: c.clusterAutoBalancerMode, minBuild: ws2016},
|
||||
{name: "BackupInProgress", desc: c.clusterBackupInProgress},
|
||||
{name: "BlockCacheSize", desc: c.clusterBlockCacheSize},
|
||||
{name: "ClusSvcHangTimeout", desc: c.clusterClusSvcHangTimeout},
|
||||
{name: "ClusSvcRegroupOpeningTimeout", desc: c.clusterClusSvcRegroupOpeningTimeout, absent: zero},
|
||||
{name: "ClusSvcRegroupPruningTimeout", desc: c.clusterClusSvcRegroupPruningTimeout, absent: zero},
|
||||
{name: "ClusSvcRegroupStageTimeout", desc: c.clusterClusSvcRegroupStageTimeout},
|
||||
{name: "ClusSvcRegroupTickInMilliseconds", desc: c.clusterClusSvcRegroupTickInMilliseconds},
|
||||
{name: "ClusterEnforcedAntiAffinity", desc: c.clusterClusterEnforcedAntiAffinity},
|
||||
{name: "ClusterFunctionalLevel", desc: c.clusterClusterFunctionalLevel, minBuild: ws2016},
|
||||
{name: "ClusterGroupWaitDelay", desc: c.clusterClusterGroupWaitDelay},
|
||||
{name: "ClusterLogLevel", desc: c.clusterClusterLogLevel},
|
||||
{name: "ClusterLogSize", desc: c.clusterClusterLogSize},
|
||||
{name: "ClusterUpgradeVersion", desc: c.clusterClusterUpgradeVersion, minBuild: ws2016},
|
||||
{name: "CrossSiteDelay", desc: c.clusterCrossSiteDelay, minBuild: ws2016},
|
||||
{name: "CrossSiteThreshold", desc: c.clusterCrossSiteThreshold, minBuild: ws2016},
|
||||
{name: "CrossSubnetDelay", desc: c.clusterCrossSubnetDelay},
|
||||
{name: "CrossSubnetThreshold", desc: c.clusterCrossSubnetThreshold},
|
||||
{name: "CsvBalancer", desc: c.clusterCsvBalancer},
|
||||
{name: "DatabaseReadWriteMode", desc: c.clusterDatabaseReadWriteMode},
|
||||
{name: "DefaultNetworkRole", desc: c.clusterDefaultNetworkRole},
|
||||
{name: "DisableGroupPreferredOwnerRandomization", desc: c.clusterDisableGroupPreferredOwnerRandomization, absent: zero},
|
||||
{name: "DrainOnShutdown", desc: c.clusterDrainOnShutdown},
|
||||
{name: "DynamicQuorumEnabled", desc: c.clusterDynamicQuorumEnabled},
|
||||
{name: "EnableSharedVolumes", desc: c.clusterEnableSharedVolumes},
|
||||
{name: "FixQuorum", desc: c.clusterFixQuorum},
|
||||
{name: "GracePeriodEnabled", desc: c.clusterGracePeriodEnabled, minBuild: ws2016, absent: zero},
|
||||
{name: "GracePeriodTimeout", desc: c.clusterGracePeriodTimeout, minBuild: ws2016, absent: zero},
|
||||
{name: "GroupDependencyTimeout", desc: c.clusterGroupDependencyTimeout, minBuild: ws2016},
|
||||
{name: "HangRecoveryAction", desc: c.clusterHangRecoveryAction},
|
||||
{name: "IgnorePersistentStateOnStartup", desc: c.clusterIgnorePersistentStateOnStartup},
|
||||
{name: "LogResourceControls", desc: c.clusterLogResourceControls},
|
||||
{name: "LowerQuorumPriorityNodeId", desc: c.clusterLowerQuorumPriorityNodeId},
|
||||
{name: "MessageBufferLength", desc: c.clusterMessageBufferLength},
|
||||
{name: "MinimumNeverPreemptPriority", desc: c.clusterMinimumNeverPreemptPriority},
|
||||
{name: "MinimumPreemptorPriority", desc: c.clusterMinimumPreemptorPriority},
|
||||
{name: "NetftIPSecEnabled", desc: c.clusterNetftIPSecEnabled},
|
||||
{name: "PlacementOptions", desc: c.clusterPlacementOptions, minBuild: ws2016},
|
||||
{name: "PlumbAllCrossSubnetRoutes", desc: c.clusterPlumbAllCrossSubnetRoutes},
|
||||
{name: "PreventQuorum", desc: c.clusterPreventQuorum},
|
||||
{name: "QuarantineDuration", desc: c.clusterQuarantineDuration, minBuild: ws2016},
|
||||
{name: "QuarantineThreshold", desc: c.clusterQuarantineThreshold, minBuild: ws2016},
|
||||
{name: "QuorumArbitrationTimeMax", desc: c.clusterQuorumArbitrationTimeMax},
|
||||
{name: "QuorumArbitrationTimeMin", desc: c.clusterQuorumArbitrationTimeMin, absent: zero},
|
||||
{name: "QuorumLogFileSize", desc: c.clusterQuorumLogFileSize},
|
||||
{name: "QuorumTypeValue", desc: c.clusterQuorumTypeValue},
|
||||
{name: "RequestReplyTimeout", desc: c.clusterRequestReplyTimeout},
|
||||
{name: "ResiliencyDefaultPeriod", desc: c.clusterResiliencyDefaultPeriod, minBuild: ws2016},
|
||||
{name: "ResiliencyLevel", desc: c.clusterResiliencyLevel, minBuild: ws2016},
|
||||
{name: "ResourceDllDeadlockPeriod", desc: c.clusterResourceDllDeadlockPeriod, absent: zero},
|
||||
{name: "RootMemoryReserved", desc: c.clusterRootMemoryReserved, absent: zero},
|
||||
{name: "RouteHistoryLength", desc: c.clusterRouteHistoryLength},
|
||||
{name: "S2DBusTypes", desc: c.clusterS2DBusTypes, minBuild: ws2016},
|
||||
{name: "S2DCacheDesiredState", desc: c.clusterS2DCacheDesiredState, minBuild: ws2016},
|
||||
{name: "S2DCacheFlashReservePercent", desc: c.clusterS2DCacheFlashReservePercent, minBuild: ws2016},
|
||||
{name: "S2DCachePageSizeKBytes", desc: c.clusterS2DCachePageSizeKBytes, minBuild: ws2016},
|
||||
{name: "S2DEnabled", desc: c.clusterS2DEnabled, minBuild: ws2016},
|
||||
{name: "S2DIOLatencyThreshold", desc: c.clusterS2DIOLatencyThreshold, minBuild: ws2016},
|
||||
{name: "S2DOptimizations", desc: c.clusterS2DOptimizations, minBuild: ws2016},
|
||||
{name: "SameSubnetDelay", desc: c.clusterSameSubnetDelay},
|
||||
{name: "SameSubnetThreshold", desc: c.clusterSameSubnetThreshold},
|
||||
{name: "SecurityLevel", desc: c.clusterSecurityLevel},
|
||||
{name: "SharedVolumeVssWriterOperationTimeout", desc: c.clusterSharedVolumeVssWriterOperationTimeout},
|
||||
{name: "ShutdownTimeoutInMinutes", desc: c.clusterShutdownTimeoutInMinutes},
|
||||
{name: "UseClientAccessNetworksForSharedVolumes", desc: c.clusterUseClientAccessNetworksForSharedVolumes},
|
||||
{name: "WitnessDatabaseWriteTimeout", desc: c.clusterWitnessDatabaseWriteTimeout},
|
||||
{name: "WitnessDynamicWeight", desc: c.clusterWitnessDynamicWeight},
|
||||
{name: "WitnessRestartInterval", desc: c.clusterWitnessRestartInterval},
|
||||
// These were published only on Windows Server 2022 and newer.
|
||||
{name: "DetectManagedEvents", desc: c.clusterDetectManagedEvents, minBuild: ws2022, older: neverPublish},
|
||||
{name: "DetectManagedEventsThreshold", desc: c.clusterDetectManagedEventsThreshold, minBuild: ws2022, older: neverPublish},
|
||||
{name: "SecurityLevelForStorage", desc: c.clusterSecurityLevelForStorage, minBuild: ws2022, older: neverPublish},
|
||||
{name: "MaxNumberOfNodes", desc: c.clusterMaxNumberOfNodes, minBuild: ws2022, older: neverPublish, absent: maxNodes},
|
||||
{name: "DetectedCloudPlatform", desc: c.clusterDetectedCloudPlatform, minBuild: ws2022, older: neverPublish},
|
||||
}
|
||||
|
||||
return nil
|
||||
return publishObjectFields(ch, "cluster", cluster, fields, build, resultErr)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
// 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 mscluster
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"maps"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/headers/clusapi"
|
||||
"github.com/prometheus-community/windows_exporter/internal/osversion"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
)
|
||||
|
||||
type clusterFixtureSource struct {
|
||||
cluster clusapi.Object
|
||||
err error
|
||||
started chan struct{}
|
||||
release chan struct{}
|
||||
closeCount int
|
||||
}
|
||||
|
||||
func (s *clusterFixtureSource) Properties(time.Time) (clusapi.Object, error) {
|
||||
if s.started != nil {
|
||||
close(s.started)
|
||||
<-s.release
|
||||
}
|
||||
|
||||
return s.cluster, s.err
|
||||
}
|
||||
|
||||
func (s *clusterFixtureSource) Close() error {
|
||||
s.closeCount++
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// clusterFieldNames lists the uint32 properties of the WMI model in order.
|
||||
func clusterFieldNames() []string {
|
||||
model := reflect.TypeFor[msClusterCluster]()
|
||||
names := make([]string, 0, model.NumField())
|
||||
|
||||
for field := range model.Fields() {
|
||||
if field.Name != "Name" {
|
||||
names = append(names, field.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return names
|
||||
}
|
||||
|
||||
// clusterFixture assigns each property a distinct value: its index + 1.
|
||||
func clusterFixture() clusapi.Object {
|
||||
cluster := clusapi.Object{Name: "CICluster", Values: make(map[string]uint32)}
|
||||
for index, name := range clusterFieldNames() {
|
||||
cluster.Values[name] = uint32(index + 1)
|
||||
}
|
||||
|
||||
cluster.Values["QuorumTypeValue"] = ^uint32(0)
|
||||
|
||||
return cluster
|
||||
}
|
||||
|
||||
func gatherCluster(t *testing.T, cluster clusapi.Object, inputErr error, build uint16) (map[string]*dto.MetricFamily, error) {
|
||||
t.Helper()
|
||||
|
||||
c := New(&Config{CollectorsEnabled: []string{subCollectorCluster}})
|
||||
c.buildClusterDescriptors()
|
||||
|
||||
return gatherPublished(t, func(ch chan<- prometheus.Metric) error {
|
||||
return c.publishCluster(ch, cluster, build, inputErr)
|
||||
})
|
||||
}
|
||||
|
||||
func TestClusterNativeMetricContract(t *testing.T) {
|
||||
cluster := clusterFixture()
|
||||
|
||||
families, err := gatherCluster(t, cluster, nil, osversion.LTSC2022)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(families) != len(cluster.Values) {
|
||||
t.Fatalf("families = %d, want %d", len(families), len(cluster.Values))
|
||||
}
|
||||
|
||||
for name, family := range families {
|
||||
if family.GetType() != dto.MetricType_GAUGE || len(family.GetMetric()) != 1 {
|
||||
t.Fatalf("invalid family %s", name)
|
||||
}
|
||||
|
||||
if !maps.Equal(metricLabels(family.GetMetric()[0]), map[string]string{"name": "CICluster"}) {
|
||||
t.Fatalf("%s labels = %v", name, metricLabels(family.GetMetric()[0]))
|
||||
}
|
||||
}
|
||||
|
||||
for metric, property := range map[string]string{
|
||||
"add_evict_delay": "AddEvictDelay",
|
||||
"s2dio_latency_threshold": "S2DIOLatencyThreshold",
|
||||
"s2d_cache_page_size_k_bytes": "S2DCachePageSizeKBytes",
|
||||
"netft_ip_sec_enabled": "NetftIPSecEnabled",
|
||||
"detected_cloud_platform": "DetectedCloudPlatform",
|
||||
"lower_quorum_priority_node_id": "LowerQuorumPriorityNodeId",
|
||||
"detect_managed_events_threshold": "DetectManagedEventsThreshold",
|
||||
"quorum_type_value": "QuorumTypeValue",
|
||||
} {
|
||||
got := families["windows_mscluster_cluster_"+metric].GetMetric()[0].GetGauge().GetValue()
|
||||
if want := float64(cluster.Values[property]); got != want {
|
||||
t.Errorf("%s = %v, want %v", metric, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterVersionDependentFields(t *testing.T) {
|
||||
cluster := clusterFixture()
|
||||
|
||||
// The five Windows Server 2022 properties were not published before.
|
||||
families, err := gatherCluster(t, cluster, nil, osversion.LTSC2019)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, name := range []string{"detect_managed_events", "detect_managed_events_threshold", "security_level_for_storage", "max_number_of_nodes", "detected_cloud_platform"} {
|
||||
if _, exists := families["windows_mscluster_cluster_"+name]; exists {
|
||||
t.Errorf("published %s before Windows Server 2022", name)
|
||||
}
|
||||
}
|
||||
|
||||
// Windows Server 2016 properties are omitted on Windows Server 2012 R2.
|
||||
delete(cluster.Values, "S2DEnabled")
|
||||
delete(cluster.Values, "ClusterFunctionalLevel")
|
||||
|
||||
if _, err := gatherCluster(t, cluster, nil, osversion.LTSC2016-1); err != nil {
|
||||
t.Fatalf("missing Windows Server 2016 properties failed Windows Server 2012 R2: %v", err)
|
||||
}
|
||||
|
||||
_, err = gatherCluster(t, cluster, nil, osversion.LTSC2016)
|
||||
if err == nil || !strings.Contains(err.Error(), "S2DEnabled") {
|
||||
t.Fatalf("missing Windows Server 2016 property ignored: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// The CI cluster showed which MSCluster_Cluster values WMI synthesizes when the
|
||||
// cluster service does not report the property.
|
||||
func TestClusterWMISynthesizedValues(t *testing.T) {
|
||||
cluster := clusterFixture()
|
||||
synthesized := map[string]float64{
|
||||
"ClusSvcRegroupOpeningTimeout": 0, "ClusSvcRegroupPruningTimeout": 0, "DisableGroupPreferredOwnerRandomization": 0,
|
||||
"GracePeriodEnabled": 0, "GracePeriodTimeout": 0, "QuorumArbitrationTimeMin": 0, "ResourceDllDeadlockPeriod": 0,
|
||||
"RootMemoryReserved": 0, "MaxNumberOfNodes": 64,
|
||||
}
|
||||
|
||||
for name := range synthesized {
|
||||
delete(cluster.Values, name)
|
||||
}
|
||||
|
||||
families, err := gatherCluster(t, cluster, nil, osversion.LTSC2022)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, name := range []string{"clus_svc_regroup_opening_timeout", "grace_period_enabled", "root_memory_reserved"} {
|
||||
if got := families["windows_mscluster_cluster_"+name].GetMetric()[0].GetGauge().GetValue(); got != 0 {
|
||||
t.Errorf("%s = %v", name, got)
|
||||
}
|
||||
}
|
||||
|
||||
if got := families["windows_mscluster_cluster_max_number_of_nodes"].GetMetric()[0].GetGauge().GetValue(); got != 64 {
|
||||
t.Errorf("max_number_of_nodes = %v", got)
|
||||
}
|
||||
|
||||
// Reported values win over the synthesized ones.
|
||||
if got := familiesValue(t, clusterFixture(), "max_number_of_nodes"); got == 64 {
|
||||
t.Error("synthesized value replaced the reported one")
|
||||
}
|
||||
}
|
||||
|
||||
func familiesValue(t *testing.T, cluster clusapi.Object, metric string) float64 {
|
||||
t.Helper()
|
||||
|
||||
families, err := gatherCluster(t, cluster, nil, osversion.LTSC2022)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return families["windows_mscluster_cluster_"+metric].GetMetric()[0].GetGauge().GetValue()
|
||||
}
|
||||
|
||||
func TestClusterPartialResultsAndJoinedErrors(t *testing.T) {
|
||||
first, second := errors.New("first cause"), errors.New("second cause")
|
||||
cluster := clusterFixture()
|
||||
delete(cluster.Values, "QuorumTypeValue")
|
||||
|
||||
families, err := gatherCluster(t, cluster, errors.Join(first, second), osversion.LTSC2022)
|
||||
if !errors.Is(err, first) || !errors.Is(err, second) || !strings.Contains(err.Error(), "QuorumTypeValue") {
|
||||
t.Fatalf("lost cause: %v", err)
|
||||
}
|
||||
|
||||
if _, exists := families["windows_mscluster_cluster_quorum_type_value"]; exists {
|
||||
t.Fatal("published missing property")
|
||||
}
|
||||
|
||||
if len(families) != len(cluster.Values) {
|
||||
t.Fatalf("lost successful properties: %d", len(families))
|
||||
}
|
||||
|
||||
// A cluster whose name could not be read is not published.
|
||||
families, err = gatherCluster(t, clusapi.Object{}, first, osversion.LTSC2022)
|
||||
if !errors.Is(err, first) || len(families) != 0 {
|
||||
t.Fatalf("published invalid identity: %d, %v", len(families), err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterCloseWaitsForCollection(t *testing.T) {
|
||||
source := &clusterFixtureSource{cluster: clusterFixture(), started: make(chan struct{}), release: make(chan struct{})}
|
||||
c := New(&Config{CollectorsEnabled: []string{subCollectorCluster}})
|
||||
c.clusterSource = source
|
||||
c.buildClusterDescriptors()
|
||||
|
||||
collectDone := make(chan error, 1)
|
||||
go func() { collectDone <- c.Collect(make(chan prometheus.Metric, 100), time.Second) }()
|
||||
|
||||
<-source.started
|
||||
|
||||
closeDone := make(chan error, 1)
|
||||
go func() { closeDone <- c.Close() }()
|
||||
|
||||
select {
|
||||
case err := <-closeDone:
|
||||
t.Fatalf("closed during collection: %v", err)
|
||||
case <-time.After(10 * time.Millisecond):
|
||||
}
|
||||
|
||||
close(source.release)
|
||||
|
||||
if err := <-collectDone; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := <-closeDone; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := c.Close(); err != nil || source.closeCount != 1 {
|
||||
t.Fatalf("close count = %d, err = %v", source.closeCount, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterRepeatedBuildClosesOldSource(t *testing.T) {
|
||||
source := &clusterFixtureSource{}
|
||||
c := New(&Config{CollectorsEnabled: []string{"unsupported"}})
|
||||
c.clusterSource = source
|
||||
|
||||
if err := c.Build(slog.Default(), nil); err == nil {
|
||||
t.Fatal("accepted unsupported collector")
|
||||
}
|
||||
|
||||
if source.closeCount != 1 || c.clusterSource != nil {
|
||||
t.Fatal("old cluster source leaked")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkClusterPublication(b *testing.B) {
|
||||
c := New(&Config{CollectorsEnabled: []string{subCollectorCluster}})
|
||||
c.buildClusterDescriptors()
|
||||
|
||||
cluster := clusterFixture()
|
||||
ch := make(chan prometheus.Metric, len(cluster.Values))
|
||||
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
if err := c.publishCluster(ch, cluster, osversion.LTSC2022, nil); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
for range len(cluster.Values) {
|
||||
<-ch
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
// 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 mscluster
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/headers/clusapi"
|
||||
"github.com/prometheus-community/windows_exporter/internal/mi"
|
||||
"github.com/prometheus-community/windows_exporter/internal/osversion"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// clusterParityQuery is the query of the previous WMI implementation.
|
||||
func clusterParityQuery() string {
|
||||
query := "SELECT Name,AddEvictDelay,AdminAccessPoint,AutoAssignNodeSite,AutoBalancerLevel,AutoBalancerMode,BackupInProgress,BlockCacheSize,ClusSvcHangTimeout,ClusSvcRegroupOpeningTimeout,ClusSvcRegroupPruningTimeout,ClusSvcRegroupStageTimeout,ClusSvcRegroupTickInMilliseconds,ClusterEnforcedAntiAffinity,ClusterFunctionalLevel,ClusterGroupWaitDelay,ClusterLogLevel,ClusterLogSize,ClusterUpgradeVersion,CrossSiteDelay,CrossSiteThreshold,CrossSubnetDelay,CrossSubnetThreshold,CsvBalancer,DatabaseReadWriteMode,DefaultNetworkRole,DisableGroupPreferredOwnerRandomization,DrainOnShutdown,DynamicQuorumEnabled,EnableSharedVolumes,FixQuorum,GracePeriodEnabled,GracePeriodTimeout,GroupDependencyTimeout,HangRecoveryAction,IgnorePersistentStateOnStartup,LogResourceControls,LowerQuorumPriorityNodeId,MessageBufferLength,MinimumNeverPreemptPriority,MinimumPreemptorPriority,NetftIPSecEnabled,PlacementOptions,PlumbAllCrossSubnetRoutes,PreventQuorum,QuarantineDuration,QuarantineThreshold,QuorumArbitrationTimeMax,QuorumArbitrationTimeMin,QuorumLogFileSize,QuorumTypeValue,RequestReplyTimeout,ResiliencyDefaultPeriod,ResiliencyLevel,ResourceDllDeadlockPeriod,RootMemoryReserved,RouteHistoryLength,S2DBusTypes,S2DCacheDesiredState,S2DCacheFlashReservePercent,S2DCachePageSizeKBytes,S2DEnabled,S2DIOLatencyThreshold,S2DOptimizations,SameSubnetDelay,SameSubnetThreshold,SecurityLevel,SharedVolumeVssWriterOperationTimeout,ShutdownTimeoutInMinutes,UseClientAccessNetworksForSharedVolumes,WitnessDatabaseWriteTimeout,WitnessDynamicWeight,WitnessRestartInterval"
|
||||
if osversion.Build() >= osversion.LTSC2022 {
|
||||
query += ",DetectManagedEvents,SecurityLevelForStorage,MaxNumberOfNodes,DetectManagedEventsThreshold,DetectedCloudPlatform"
|
||||
}
|
||||
|
||||
return query + " FROM MSCluster_Cluster"
|
||||
}
|
||||
|
||||
// TestClusterNativeWMIParity compares gathered metric families from ClusAPI
|
||||
// with the same publication fed by MSCluster_Cluster.
|
||||
func TestClusterNativeWMIParity(t *testing.T) {
|
||||
native, session, _ := resourceComparisonSources(t)
|
||||
|
||||
query, err := mi.NewQuery(clusterParityQuery())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var rows []msClusterCluster
|
||||
|
||||
start := time.Now()
|
||||
|
||||
if err := session.Query(&rows, mi.NamespaceRootMSCluster, query, time.Minute); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Logf("WMI cluster query duration: %s", time.Since(start))
|
||||
|
||||
if len(rows) != 1 {
|
||||
t.Fatalf("cluster rows = %d, want 1", len(rows))
|
||||
}
|
||||
|
||||
expected := clusapi.Object{Name: rows[0].Name, Values: make(map[string]uint32)}
|
||||
|
||||
value := reflect.ValueOf(rows[0])
|
||||
for index := range value.NumField() {
|
||||
if field := value.Type().Field(index); field.Name != "Name" {
|
||||
expected.Values[field.Name] = uint32(value.Field(index).Uint())
|
||||
}
|
||||
}
|
||||
|
||||
start = time.Now()
|
||||
|
||||
cluster, err := native.Properties(time.Now().Add(time.Minute))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
t.Logf("ClusAPI cluster query duration: %s", time.Since(start))
|
||||
|
||||
gotFamilies, err := gatherCluster(t, cluster, nil, osversion.Build())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
wantFamilies, err := gatherCluster(t, expected, nil, osversion.Build())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(gotFamilies) != len(wantFamilies) {
|
||||
t.Errorf("families = %d, want %d", len(gotFamilies), len(wantFamilies))
|
||||
}
|
||||
|
||||
for name, want := range wantFamilies {
|
||||
if !proto.Equal(gotFamilies[name], want) {
|
||||
t.Errorf("native/WMI metric mismatch: %s\nnative: %v\nWMI: %v", name, gotFamilies[name], want)
|
||||
}
|
||||
}
|
||||
|
||||
if t.Failed() {
|
||||
t.Logf("native cluster %q DWORD properties: %v", cluster.Name, cluster.Values)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkClusterSources(b *testing.B) {
|
||||
native, session, _ := resourceComparisonSources(b)
|
||||
|
||||
query, err := mi.NewQuery(clusterParityQuery())
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
b.Run("WMI", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
var rows []msClusterCluster
|
||||
if err := session.Query(&rows, mi.NamespaceRootMSCluster, query, time.Minute); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("ClusAPI", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
if _, err := native.Properties(time.Now().Add(time.Minute)); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// 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 mscluster
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/headers/clusapi"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
// A ClusAPI RPC that hangs (for example in a resource DLL until its deadlock
|
||||
// timeout) must not hold Collect: the other sub-collectors publish, Collect
|
||||
// returns at the budget, and Close does not wait for the stuck read.
|
||||
func TestCollectDoesNotWaitForStuckSource(t *testing.T) {
|
||||
stuck := &resourceFixtureSource{started: make(chan struct{}), release: make(chan struct{})}
|
||||
defer close(stuck.release)
|
||||
|
||||
networks := &objectFixtureSource{objects: []clusapi.Object{networkFixture("Cluster Network 1")}}
|
||||
|
||||
c := New(&Config{CollectorsEnabled: []string{subCollectorNetwork, subCollectorResource}})
|
||||
c.resourceSource, c.networkSource = stuck, networks
|
||||
c.buildResourceDescriptors()
|
||||
c.buildNetworkDescriptors()
|
||||
|
||||
ch := make(chan prometheus.Metric, 100)
|
||||
start := time.Now()
|
||||
|
||||
err := c.Collect(ch, 50*time.Millisecond)
|
||||
if !errors.Is(err, context.DeadlineExceeded) {
|
||||
t.Fatalf("stuck resource read: %v", err)
|
||||
}
|
||||
|
||||
if elapsed := time.Since(start); elapsed > 5*time.Second {
|
||||
t.Fatalf("Collect waited %s for the stuck read", elapsed)
|
||||
}
|
||||
|
||||
close(ch)
|
||||
|
||||
published := 0
|
||||
for range ch {
|
||||
published++
|
||||
}
|
||||
|
||||
if published == 0 {
|
||||
t.Fatal("network metrics were not published")
|
||||
}
|
||||
|
||||
closeDone := make(chan error, 1)
|
||||
|
||||
go func() { closeDone <- c.Close() }()
|
||||
|
||||
select {
|
||||
case err := <-closeDone:
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("Close waited for the stuck read")
|
||||
}
|
||||
}
|
||||
|
||||
// Library consumers may call Collect after a failed Build.
|
||||
func TestCollectWithoutSources(t *testing.T) {
|
||||
c := New(&Config{CollectorsEnabled: []string{subCollectorCluster, subCollectorNetwork, subCollectorNode, subCollectorResource, subCollectorResourceGroup, subCollectorSharedVolumes}})
|
||||
|
||||
err := c.Collect(make(chan prometheus.Metric, 10), time.Second)
|
||||
if err == nil || !strings.Contains(err.Error(), "not built") {
|
||||
t.Fatalf("Collect without sources: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -18,18 +18,22 @@
|
||||
package mscluster
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/mi"
|
||||
"github.com/prometheus-community/windows_exporter/internal/headers/clusapi"
|
||||
"github.com/prometheus-community/windows_exporter/internal/types"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const nameNetwork = Name + "_network"
|
||||
|
||||
type networkSource interface {
|
||||
Networks(deadline time.Time) ([]clusapi.Object, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
type collectorNetwork struct {
|
||||
networkMIQuery mi.Query
|
||||
networkSource networkSource
|
||||
|
||||
networkCharacteristics *prometheus.Desc
|
||||
networkFlags *prometheus.Desc
|
||||
@@ -38,7 +42,8 @@ type collectorNetwork struct {
|
||||
networkState *prometheus.Desc
|
||||
}
|
||||
|
||||
// msClusterNetwork represents the MSCluster_Network WMI class
|
||||
// msClusterNetwork represents the MSCluster_Network WMI class. The collector
|
||||
// reads ClusAPI; the parity test compares against this WMI model.
|
||||
// - https://docs.microsoft.com/en-us/previous-versions/windows/desktop/cluswmi/mscluster-network
|
||||
type msClusterNetwork struct {
|
||||
Name string `mi:"Name"`
|
||||
@@ -51,13 +56,18 @@ type msClusterNetwork struct {
|
||||
}
|
||||
|
||||
func (c *Collector) buildNetwork() error {
|
||||
networkMIQuery, err := mi.NewQuery("SELECT Characteristics,Flags,Metric,Role,State FROM MSCluster_Network")
|
||||
source, err := clusapi.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create WMI query: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
c.networkMIQuery = networkMIQuery
|
||||
c.networkSource = source
|
||||
c.buildNetworkDescriptors()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Collector) buildNetworkDescriptors() {
|
||||
c.networkCharacteristics = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(types.Namespace, nameNetwork, "characteristics"),
|
||||
"Provides the characteristics of the network.",
|
||||
@@ -88,61 +98,38 @@ func (c *Collector) buildNetwork() error {
|
||||
[]string{"name"},
|
||||
nil,
|
||||
)
|
||||
|
||||
var dst []msClusterNetwork
|
||||
|
||||
if err := c.miSession.Query(&dst, mi.NamespaceRootMSCluster, c.networkMIQuery, 0); err != nil {
|
||||
return fmt.Errorf("WMI query failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Collect sends the metric values for each metric
|
||||
// to the provided prometheus metric channel.
|
||||
func (c *Collector) collectNetwork(ch chan<- prometheus.Metric, maxScrapeDuration time.Duration) error {
|
||||
var dst []msClusterNetwork
|
||||
|
||||
if err := c.miSession.Query(&dst, mi.NamespaceRootMSCluster, c.networkMIQuery, maxScrapeDuration); err != nil {
|
||||
return fmt.Errorf("WMI query failed: %w", err)
|
||||
if c.networkSource == nil {
|
||||
return errNotBuilt(subCollectorNetwork)
|
||||
}
|
||||
|
||||
for _, v := range dst {
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.networkCharacteristics,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.Characteristics),
|
||||
v.Name,
|
||||
)
|
||||
networks, resultErr := callSource(maxScrapeDuration, c.networkSource.Networks)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.networkFlags,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.Flags),
|
||||
v.Name,
|
||||
)
|
||||
return c.publishNetworks(ch, networks, resultErr)
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.networkMetric,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.Metric),
|
||||
v.Name,
|
||||
)
|
||||
func (c *Collector) publishNetworks(ch chan<- prometheus.Metric, networks []clusapi.Object, resultErr error) error {
|
||||
fields := []objectField{
|
||||
{name: "Characteristics", desc: c.networkCharacteristics},
|
||||
{name: "Flags", desc: c.networkFlags},
|
||||
{name: "Metric", desc: c.networkMetric},
|
||||
{name: "Role", desc: c.networkRole},
|
||||
{name: "State", desc: c.networkState},
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.networkRole,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.Role),
|
||||
v.Name,
|
||||
)
|
||||
for _, network := range networks {
|
||||
if network.Name == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.networkState,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.State),
|
||||
v.Name,
|
||||
)
|
||||
// All network properties exist since Windows Server 2012, so the
|
||||
// build does not matter.
|
||||
resultErr = publishObjectFields(ch, "network", network, fields, 0, resultErr)
|
||||
}
|
||||
|
||||
return nil
|
||||
return resultErr
|
||||
}
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
// 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 mscluster
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"maps"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/headers/clusapi"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
)
|
||||
|
||||
func networkFixture(name string) clusapi.Object {
|
||||
return clusapi.Object{Name: name, Values: map[string]uint32{
|
||||
"Characteristics": 0, "Flags": 0, "Metric": 70240, "Role": 3, "State": ^uint32(0),
|
||||
}}
|
||||
}
|
||||
|
||||
func gatherNetworks(t *testing.T, networks []clusapi.Object, inputErr error) (map[string]*dto.MetricFamily, error) {
|
||||
t.Helper()
|
||||
|
||||
c := New(&Config{CollectorsEnabled: []string{subCollectorNetwork}})
|
||||
c.buildNetworkDescriptors()
|
||||
|
||||
return gatherPublished(t, func(ch chan<- prometheus.Metric) error {
|
||||
return c.publishNetworks(ch, networks, inputErr)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNetworkNativeMetricContract(t *testing.T) {
|
||||
families, err := gatherNetworks(t, []clusapi.Object{networkFixture("Cluster Network 1")}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := map[string]float64{"characteristics": 0, "flags": 0, "metric": 70240, "role": 3, "state": 4294967295}
|
||||
|
||||
if len(families) != len(expected) {
|
||||
t.Fatalf("families = %d", len(families))
|
||||
}
|
||||
|
||||
for suffix, value := range expected {
|
||||
family := families["windows_mscluster_network_"+suffix]
|
||||
if family == nil || family.GetType() != dto.MetricType_GAUGE || len(family.GetMetric()) != 1 {
|
||||
t.Fatalf("invalid family %s: %v", suffix, family)
|
||||
}
|
||||
|
||||
metric := family.GetMetric()[0]
|
||||
if metric.GetGauge().GetValue() != value {
|
||||
t.Errorf("%s value = %v, want %v", suffix, metric.GetGauge().GetValue(), value)
|
||||
}
|
||||
|
||||
if !maps.Equal(metricLabels(metric), map[string]string{"name": "Cluster Network 1"}) {
|
||||
t.Fatalf("labels = %v", metricLabels(metric))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNetworkPartialResultsAndJoinedErrors(t *testing.T) {
|
||||
first, second := errors.New("first cause"), errors.New("second cause")
|
||||
network := networkFixture("Cluster Network 1")
|
||||
delete(network.Values, "State")
|
||||
|
||||
families, err := gatherNetworks(t, []clusapi.Object{network, {}, networkFixture("Cluster Network 2")}, errors.Join(first, second))
|
||||
if !errors.Is(err, first) || !errors.Is(err, second) {
|
||||
t.Fatalf("lost joined cause: %v", err)
|
||||
}
|
||||
|
||||
for name, family := range families {
|
||||
want := 2
|
||||
if name == "windows_mscluster_network_state" {
|
||||
want = 1
|
||||
}
|
||||
|
||||
if len(family.GetMetric()) != want {
|
||||
t.Errorf("%s samples = %d, want %d", name, len(family.GetMetric()), want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNetworkCloseWaitsForCollection(t *testing.T) {
|
||||
source := &objectFixtureSource{objects: []clusapi.Object{networkFixture("Cluster Network 1")}, started: make(chan struct{}), release: make(chan struct{})}
|
||||
c := New(&Config{CollectorsEnabled: []string{subCollectorNetwork}})
|
||||
c.networkSource = source
|
||||
c.buildNetworkDescriptors()
|
||||
|
||||
collectDone := make(chan error, 1)
|
||||
go func() { collectDone <- c.Collect(make(chan prometheus.Metric, 100), time.Second) }()
|
||||
|
||||
<-source.started
|
||||
|
||||
closeDone := make(chan error, 1)
|
||||
go func() { closeDone <- c.Close() }()
|
||||
|
||||
select {
|
||||
case err := <-closeDone:
|
||||
t.Fatalf("closed during collection: %v", err)
|
||||
case <-time.After(10 * time.Millisecond):
|
||||
}
|
||||
|
||||
close(source.release)
|
||||
|
||||
if err := <-collectDone; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := <-closeDone; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := c.Close(); err != nil || source.closeCount != 1 {
|
||||
t.Fatalf("close count = %d, err = %v", source.closeCount, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNetworkRepeatedBuildClosesOldSource(t *testing.T) {
|
||||
source := &objectFixtureSource{}
|
||||
c := New(&Config{CollectorsEnabled: []string{"unsupported"}})
|
||||
c.networkSource = source
|
||||
|
||||
if err := c.Build(slog.Default(), nil); err == nil {
|
||||
t.Fatal("accepted unsupported collector")
|
||||
}
|
||||
|
||||
if source.closeCount != 1 || c.networkSource != nil {
|
||||
t.Fatal("old network source leaked")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNetworkLargeClusterGather(t *testing.T) {
|
||||
networks := make([]clusapi.Object, 32)
|
||||
for index := range networks {
|
||||
networks[index] = networkFixture(fmt.Sprintf("Cluster Network %d", index))
|
||||
}
|
||||
|
||||
families, err := gatherNetworks(t, networks, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for name, family := range families {
|
||||
if len(family.GetMetric()) != len(networks) {
|
||||
t.Errorf("%s samples = %d", name, len(family.GetMetric()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNetworkPublication(b *testing.B) {
|
||||
c := New(&Config{CollectorsEnabled: []string{subCollectorNetwork}})
|
||||
c.buildNetworkDescriptors()
|
||||
|
||||
networks := []clusapi.Object{networkFixture("Cluster Network 1")}
|
||||
ch := make(chan prometheus.Metric, 5)
|
||||
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
if err := c.publishNetworks(ch, networks, nil); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
for range 5 {
|
||||
<-ch
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
// 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 mscluster
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/headers/clusapi"
|
||||
"github.com/prometheus-community/windows_exporter/internal/mi"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const networkParityQuery = "SELECT Name,Characteristics,Flags,Metric,Role,State FROM MSCluster_Network"
|
||||
|
||||
// TestNetworkNativeWMIParity compares gathered metric families from ClusAPI
|
||||
// with the same publication fed by MSCluster_Network.
|
||||
func TestNetworkNativeWMIParity(t *testing.T) {
|
||||
native, session, _ := resourceComparisonSources(t)
|
||||
|
||||
query, err := mi.NewQuery(networkParityQuery)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var rows []msClusterNetwork
|
||||
|
||||
start := time.Now()
|
||||
|
||||
if err := session.Query(&rows, mi.NamespaceRootMSCluster, query, time.Minute); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Logf("WMI network query duration: %s, networks: %d", time.Since(start), len(rows))
|
||||
|
||||
expected := make([]clusapi.Object, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
expected = append(expected, clusapi.Object{Name: row.Name, Values: map[string]uint32{
|
||||
"Characteristics": uint32(row.Characteristics),
|
||||
"Flags": uint32(row.Flags),
|
||||
"Metric": uint32(row.Metric),
|
||||
"Role": uint32(row.Role),
|
||||
"State": uint32(row.State),
|
||||
}})
|
||||
}
|
||||
|
||||
start = time.Now()
|
||||
|
||||
networks, err := native.Networks(time.Now().Add(time.Minute))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
t.Logf("ClusAPI network query duration: %s, networks: %d", time.Since(start), len(networks))
|
||||
|
||||
gotFamilies, err := gatherNetworks(t, networks, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
wantFamilies, err := gatherNetworks(t, expected, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(gotFamilies) != len(wantFamilies) {
|
||||
t.Errorf("families = %d, want %d", len(gotFamilies), len(wantFamilies))
|
||||
}
|
||||
|
||||
for name, want := range wantFamilies {
|
||||
if !proto.Equal(gotFamilies[name], want) {
|
||||
t.Errorf("native/WMI metric mismatch: %s\nnative: %v\nWMI: %v", name, gotFamilies[name], want)
|
||||
}
|
||||
}
|
||||
|
||||
if t.Failed() {
|
||||
for _, network := range networks {
|
||||
t.Logf("native network %q DWORD properties: %v", network.Name, network.Values)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNetworkSources(b *testing.B) {
|
||||
native, session, _ := resourceComparisonSources(b)
|
||||
|
||||
query, err := mi.NewQuery(networkParityQuery)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
b.Run("WMI", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
var rows []msClusterNetwork
|
||||
if err := session.Query(&rows, mi.NamespaceRootMSCluster, query, time.Minute); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("ClusAPI", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
if _, err := native.Networks(time.Now().Add(time.Minute)); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -18,10 +18,9 @@
|
||||
package mscluster
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/mi"
|
||||
"github.com/prometheus-community/windows_exporter/internal/headers/clusapi"
|
||||
"github.com/prometheus-community/windows_exporter/internal/osversion"
|
||||
"github.com/prometheus-community/windows_exporter/internal/types"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
@@ -29,8 +28,13 @@ import (
|
||||
|
||||
const nameNode = Name + "_node"
|
||||
|
||||
type nodeSource interface {
|
||||
Nodes(deadline time.Time) ([]clusapi.Object, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
type collectorNode struct {
|
||||
nodeMIQuery mi.Query
|
||||
nodeSource nodeSource
|
||||
|
||||
nodeBuildNumber *prometheus.Desc
|
||||
nodeCharacteristics *prometheus.Desc
|
||||
@@ -48,7 +52,8 @@ type collectorNode struct {
|
||||
nodeStatusInformation *prometheus.Desc
|
||||
}
|
||||
|
||||
// msClusterNode represents the MSCluster_Node WMI class
|
||||
// msClusterNode represents the MSCluster_Node WMI class. The collector reads
|
||||
// ClusAPI; the parity test compares against this WMI model.
|
||||
// - https://docs.microsoft.com/en-us/previous-versions/windows/desktop/cluswmi/mscluster-node
|
||||
type msClusterNode struct {
|
||||
Name string `mi:"Name"`
|
||||
@@ -70,20 +75,18 @@ type msClusterNode struct {
|
||||
}
|
||||
|
||||
func (c *Collector) buildNode() error {
|
||||
buildNumber := osversion.Build()
|
||||
|
||||
wmiSelect := "BuildNumber,Characteristics,DynamicWeight,Flags,MajorVersion,MinorVersion,NeedsPreventQuorum,NodeDrainStatus,NodeHighestVersion,NodeLowestVersion,NodeWeight,State,StatusInformation"
|
||||
if buildNumber >= osversion.LTSC2022 {
|
||||
wmiSelect += ",DetectedCloudPlatform"
|
||||
}
|
||||
|
||||
nodeMIQuery, err := mi.NewQuery(fmt.Sprintf("SELECT %s FROM MSCluster_Node", wmiSelect))
|
||||
source, err := clusapi.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create WMI query: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
c.nodeMIQuery = nodeMIQuery
|
||||
c.nodeSource = source
|
||||
c.buildNodeDescriptors()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Collector) buildNodeDescriptors() {
|
||||
c.nodeBuildNumber = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(types.Namespace, nameNode, "build_number"),
|
||||
"Provides access to the node's BuildNumber property.",
|
||||
@@ -168,128 +171,51 @@ func (c *Collector) buildNode() error {
|
||||
[]string{"name"},
|
||||
nil,
|
||||
)
|
||||
|
||||
var dst []msClusterNode
|
||||
|
||||
if err := c.miSession.Query(&dst, mi.NamespaceRootMSCluster, c.nodeMIQuery, 0); err != nil {
|
||||
return fmt.Errorf("WMI query failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Collect sends the metric values for each metric
|
||||
// to the provided prometheus Metric channel.
|
||||
// to the provided prometheus Metric channel. It returns the node names that
|
||||
// the resource and resource group subcollectors use for owner_node.
|
||||
func (c *Collector) collectNode(ch chan<- prometheus.Metric, maxScrapeDuration time.Duration) ([]string, error) {
|
||||
var dst []msClusterNode
|
||||
|
||||
if err := c.miSession.Query(&dst, mi.NamespaceRootMSCluster, c.nodeMIQuery, maxScrapeDuration); err != nil {
|
||||
return nil, fmt.Errorf("WMI query failed: %w", err)
|
||||
if c.nodeSource == nil {
|
||||
return nil, errNotBuilt(subCollectorNode)
|
||||
}
|
||||
|
||||
nodeNames := make([]string, 0, len(dst))
|
||||
nodes, resultErr := callSource(maxScrapeDuration, c.nodeSource.Nodes)
|
||||
|
||||
for _, v := range dst {
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.nodeBuildNumber,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.BuildNumber),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.nodeCharacteristics,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.Characteristics),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.nodeDetectedCloudPlatform,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.DetectedCloudPlatform),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.nodeDynamicWeight,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.DynamicWeight),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.nodeFlags,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.Flags),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.nodeMajorVersion,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.MajorVersion),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.nodeMinorVersion,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.MinorVersion),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.nodeNeedsPreventQuorum,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.NeedsPreventQuorum),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.nodeNodeDrainStatus,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.NodeDrainStatus),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.nodeNodeHighestVersion,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.NodeHighestVersion),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.nodeNodeLowestVersion,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.NodeLowestVersion),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.nodeNodeWeight,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.NodeWeight),
|
||||
v.Name,
|
||||
)
|
||||
return c.publishNodes(ch, nodes, osversion.Build(), resultErr)
|
||||
}
|
||||
|
||||
func (c *Collector) publishNodes(ch chan<- prometheus.Metric, nodes []clusapi.Object, build uint16, resultErr error) ([]string, error) {
|
||||
fields := []objectField{
|
||||
{name: "BuildNumber", desc: c.nodeBuildNumber},
|
||||
{name: "Characteristics", desc: c.nodeCharacteristics},
|
||||
// The previous WMI query selected DetectedCloudPlatform only on Windows
|
||||
// Server 2022 and newer and published 0 on older builds.
|
||||
{name: "DetectedCloudPlatform", desc: c.nodeDetectedCloudPlatform, minBuild: osversion.LTSC2022, older: zeroIfMissing},
|
||||
{name: "DynamicWeight", desc: c.nodeDynamicWeight},
|
||||
{name: "Flags", desc: c.nodeFlags},
|
||||
{name: "MajorVersion", desc: c.nodeMajorVersion},
|
||||
{name: "MinorVersion", desc: c.nodeMinorVersion},
|
||||
{name: "NeedsPreventQuorum", desc: c.nodeNeedsPreventQuorum},
|
||||
{name: "NodeDrainStatus", desc: c.nodeNodeDrainStatus},
|
||||
{name: "NodeHighestVersion", desc: c.nodeNodeHighestVersion},
|
||||
{name: "NodeLowestVersion", desc: c.nodeNodeLowestVersion},
|
||||
{name: "NodeWeight", desc: c.nodeNodeWeight},
|
||||
{name: "State", desc: c.nodeState},
|
||||
{name: "StatusInformation", desc: c.nodeStatusInformation, minBuild: osversion.LTSC2016},
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.nodeState,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.State),
|
||||
v.Name,
|
||||
)
|
||||
nodeNames := make([]string, 0, len(nodes))
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.nodeStatusInformation,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.StatusInformation),
|
||||
v.Name,
|
||||
)
|
||||
for _, node := range nodes {
|
||||
if node.Name == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
nodeNames = append(nodeNames, v.Name)
|
||||
resultErr = publishObjectFields(ch, "node", node, fields, build, resultErr)
|
||||
nodeNames = append(nodeNames, node.Name)
|
||||
}
|
||||
|
||||
return nodeNames, nil
|
||||
return nodeNames, resultErr
|
||||
}
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
// 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 mscluster
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"maps"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/headers/clusapi"
|
||||
"github.com/prometheus-community/windows_exporter/internal/osversion"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
)
|
||||
|
||||
func nodeFixture(name string) clusapi.Object {
|
||||
return clusapi.Object{Name: name, Values: map[string]uint32{
|
||||
"BuildNumber": 20348, "Characteristics": 0, "DetectedCloudPlatform": 1, "DynamicWeight": 1, "Flags": 2, "MajorVersion": 10, "MinorVersion": 0, "NeedsPreventQuorum": 0, "NodeDrainStatus": 3, "NodeHighestVersion": 0x000b0000, "NodeLowestVersion": 0x00090000, "NodeWeight": 1, "State": ^uint32(0), "StatusInformation": 2,
|
||||
}}
|
||||
}
|
||||
|
||||
func gatherNodes(t *testing.T, nodes []clusapi.Object, inputErr error, build uint16) (map[string]*dto.MetricFamily, []string, error) {
|
||||
t.Helper()
|
||||
|
||||
c := New(&Config{CollectorsEnabled: []string{subCollectorNode}})
|
||||
c.buildNodeDescriptors()
|
||||
|
||||
var nodeNames []string
|
||||
|
||||
families, err := gatherPublished(t, func(ch chan<- prometheus.Metric) error {
|
||||
var err error
|
||||
|
||||
nodeNames, err = c.publishNodes(ch, nodes, build, inputErr)
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return families, nodeNames, err
|
||||
}
|
||||
|
||||
func TestNodeNativeMetricContract(t *testing.T) {
|
||||
families, nodeNames, err := gatherNodes(t, []clusapi.Object{nodeFixture("nodeA")}, nil, osversion.LTSC2022)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !slices.Equal(nodeNames, []string{"nodeA"}) {
|
||||
t.Fatalf("node names = %v", nodeNames)
|
||||
}
|
||||
|
||||
expected := map[string]float64{
|
||||
"build_number": 20348, "characteristics": 0, "detected_cloud_platform": 1, "dynamic_weight": 1, "flags": 2, "major_version": 10, "minor_version": 0, "needs_prevent_quorum": 0, "node_drain_status": 3, "node_highest_version": 0x000b0000, "node_lowest_version": 0x00090000, "node_weight": 1, "state": 4294967295, "status_information": 2,
|
||||
}
|
||||
|
||||
if len(families) != len(expected) {
|
||||
t.Fatalf("families = %d", len(families))
|
||||
}
|
||||
|
||||
for suffix, value := range expected {
|
||||
family := families["windows_mscluster_node_"+suffix]
|
||||
if family == nil || family.GetType() != dto.MetricType_GAUGE || len(family.GetMetric()) != 1 {
|
||||
t.Fatalf("invalid family %s: %v", suffix, family)
|
||||
}
|
||||
|
||||
metric := family.GetMetric()[0]
|
||||
if metric.GetGauge().GetValue() != value {
|
||||
t.Errorf("%s value = %v, want %v", suffix, metric.GetGauge().GetValue(), value)
|
||||
}
|
||||
|
||||
if !maps.Equal(metricLabels(metric), map[string]string{"name": "nodeA"}) {
|
||||
t.Fatalf("labels = %v", metricLabels(metric))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodePartialResultsAndJoinedErrors(t *testing.T) {
|
||||
first, second := errors.New("first cause"), errors.New("second cause")
|
||||
node := nodeFixture("nodeA")
|
||||
delete(node.Values, "State")
|
||||
delete(node.Values, "NodeWeight")
|
||||
|
||||
families, nodeNames, err := gatherNodes(t, []clusapi.Object{node, {}, nodeFixture("nodeB")}, errors.Join(first, second), osversion.LTSC2022)
|
||||
if !errors.Is(err, first) || !errors.Is(err, second) {
|
||||
t.Fatalf("lost joined cause: %v", err)
|
||||
}
|
||||
|
||||
// Nodes with unreadable properties still own resources and groups.
|
||||
if !slices.Equal(nodeNames, []string{"nodeA", "nodeB"}) {
|
||||
t.Fatalf("node names = %v", nodeNames)
|
||||
}
|
||||
|
||||
for name, family := range families {
|
||||
want := 2
|
||||
if name == "windows_mscluster_node_state" || name == "windows_mscluster_node_node_weight" {
|
||||
want = 1
|
||||
}
|
||||
|
||||
if len(family.GetMetric()) != want {
|
||||
t.Errorf("%s samples = %d, want %d", name, len(family.GetMetric()), want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeVersionDependentFields(t *testing.T) {
|
||||
node := nodeFixture("nodeA")
|
||||
delete(node.Values, "DetectedCloudPlatform")
|
||||
delete(node.Values, "StatusInformation")
|
||||
|
||||
// Windows Server 2012 R2: StatusInformation does not exist, and the previous
|
||||
// WMI query published DetectedCloudPlatform as 0.
|
||||
families, _, err := gatherNodes(t, []clusapi.Object{node}, nil, osversion.LTSC2016-1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, exists := families["windows_mscluster_node_status_information"]; exists {
|
||||
t.Fatal("published missing StatusInformation")
|
||||
}
|
||||
|
||||
if got := families["windows_mscluster_node_detected_cloud_platform"].GetMetric()[0].GetGauge().GetValue(); got != 0 {
|
||||
t.Fatalf("detected cloud platform = %v", got)
|
||||
}
|
||||
|
||||
// Windows Server 2019: StatusInformation is required.
|
||||
if _, _, err := gatherNodes(t, []clusapi.Object{node}, nil, osversion.LTSC2019); err == nil {
|
||||
t.Fatal("missing StatusInformation ignored on Windows Server 2019")
|
||||
}
|
||||
|
||||
// Windows Server 2022: both are required and never published as zero.
|
||||
families, _, err = gatherNodes(t, []clusapi.Object{node}, nil, osversion.LTSC2022)
|
||||
if err == nil {
|
||||
t.Fatal("missing DetectedCloudPlatform ignored on Windows Server 2022")
|
||||
}
|
||||
|
||||
if _, exists := families["windows_mscluster_node_detected_cloud_platform"]; exists {
|
||||
t.Fatal("published missing DetectedCloudPlatform")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeNamesFeedOwnerMetrics(t *testing.T) {
|
||||
source := &objectFixtureSource{objects: []clusapi.Object{nodeFixture("nodeA"), nodeFixture("nodeB")}}
|
||||
c := New(&Config{CollectorsEnabled: []string{subCollectorNode, subCollectorResourceGroup}})
|
||||
c.nodeSource, c.resourceGroupSource = source, &objectFixtureSource{objects: []clusapi.Object{resourceGroupFixture()}}
|
||||
c.buildNodeDescriptors()
|
||||
c.buildResourceGroupDescriptors()
|
||||
|
||||
families, err := gatherPublished(t, func(ch chan<- prometheus.Metric) error {
|
||||
return c.Collect(ch, time.Minute)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
owners := families["windows_mscluster_resourcegroup_owner_node"]
|
||||
if len(owners.GetMetric()) != 2 {
|
||||
t.Fatalf("owner samples = %d", len(owners.GetMetric()))
|
||||
}
|
||||
|
||||
for _, metric := range owners.GetMetric() {
|
||||
want := 0.0
|
||||
if metricLabels(metric)["node_name"] == "nodeB" {
|
||||
want = 1
|
||||
}
|
||||
|
||||
if metric.GetGauge().GetValue() != want {
|
||||
t.Fatalf("owner %v = %v", metricLabels(metric), metric.GetGauge().GetValue())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeCloseWaitsForCollection(t *testing.T) {
|
||||
source := &objectFixtureSource{objects: []clusapi.Object{nodeFixture("nodeA")}, started: make(chan struct{}), release: make(chan struct{})}
|
||||
c := New(&Config{CollectorsEnabled: []string{subCollectorNode}})
|
||||
c.nodeSource = source
|
||||
c.buildNodeDescriptors()
|
||||
|
||||
collectDone := make(chan error, 1)
|
||||
go func() { collectDone <- c.Collect(make(chan prometheus.Metric, 100), time.Second) }()
|
||||
|
||||
<-source.started
|
||||
|
||||
closeDone := make(chan error, 1)
|
||||
go func() { closeDone <- c.Close() }()
|
||||
|
||||
select {
|
||||
case err := <-closeDone:
|
||||
t.Fatalf("closed during collection: %v", err)
|
||||
case <-time.After(10 * time.Millisecond):
|
||||
}
|
||||
|
||||
close(source.release)
|
||||
|
||||
if err := <-collectDone; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := <-closeDone; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := c.Close(); err != nil || source.closeCount != 1 {
|
||||
t.Fatalf("close count = %d, err = %v", source.closeCount, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeRepeatedBuildClosesOldSource(t *testing.T) {
|
||||
source := &objectFixtureSource{}
|
||||
c := New(&Config{CollectorsEnabled: []string{"unsupported"}})
|
||||
c.nodeSource = source
|
||||
|
||||
if err := c.Build(slog.Default(), nil); err == nil {
|
||||
t.Fatal("accepted unsupported collector")
|
||||
}
|
||||
|
||||
if source.closeCount != 1 || c.nodeSource != nil {
|
||||
t.Fatal("old node source leaked")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeLargeClusterGather(t *testing.T) {
|
||||
nodes := make([]clusapi.Object, 64)
|
||||
for index := range nodes {
|
||||
nodes[index] = nodeFixture(fmt.Sprintf("node-%d", index))
|
||||
}
|
||||
|
||||
families, nodeNames, err := gatherNodes(t, nodes, nil, osversion.LTSC2022)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(nodeNames) != len(nodes) {
|
||||
t.Fatalf("node names = %d", len(nodeNames))
|
||||
}
|
||||
|
||||
for name, family := range families {
|
||||
if len(family.GetMetric()) != len(nodes) {
|
||||
t.Errorf("%s samples = %d", name, len(family.GetMetric()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNodePublication(b *testing.B) {
|
||||
c := New(&Config{CollectorsEnabled: []string{subCollectorNode}})
|
||||
c.buildNodeDescriptors()
|
||||
|
||||
nodes := []clusapi.Object{nodeFixture("nodeA")}
|
||||
ch := make(chan prometheus.Metric, 14)
|
||||
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
if _, err := c.publishNodes(ch, nodes, osversion.LTSC2022, nil); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
for range 14 {
|
||||
<-ch
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
// 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 mscluster
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/headers/clusapi"
|
||||
"github.com/prometheus-community/windows_exporter/internal/mi"
|
||||
"github.com/prometheus-community/windows_exporter/internal/osversion"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func nodeParityQuery() string {
|
||||
query := "SELECT Name,BuildNumber,Characteristics,DynamicWeight,Flags,MajorVersion,MinorVersion,NeedsPreventQuorum,NodeDrainStatus,NodeHighestVersion,NodeLowestVersion,NodeWeight,State,StatusInformation"
|
||||
if osversion.Build() >= osversion.LTSC2022 {
|
||||
query += ",DetectedCloudPlatform"
|
||||
}
|
||||
|
||||
return query + " FROM MSCluster_Node"
|
||||
}
|
||||
|
||||
// TestNodeNativeWMIParity compares gathered metric families and node names
|
||||
// from ClusAPI with the same publication fed by MSCluster_Node.
|
||||
func TestNodeNativeWMIParity(t *testing.T) {
|
||||
native, session, _ := resourceComparisonSources(t)
|
||||
|
||||
query, err := mi.NewQuery(nodeParityQuery())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var rows []msClusterNode
|
||||
|
||||
start := time.Now()
|
||||
|
||||
if err := session.Query(&rows, mi.NamespaceRootMSCluster, query, time.Minute); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Logf("WMI node query duration: %s", time.Since(start))
|
||||
|
||||
if len(rows) == 0 {
|
||||
t.Fatal("cluster fixture has no nodes")
|
||||
}
|
||||
|
||||
expected := make([]clusapi.Object, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
values := map[string]uint32{
|
||||
"BuildNumber": uint32(row.BuildNumber),
|
||||
"Characteristics": uint32(row.Characteristics),
|
||||
"DynamicWeight": uint32(row.DynamicWeight),
|
||||
"Flags": uint32(row.Flags),
|
||||
"MajorVersion": uint32(row.MajorVersion),
|
||||
"MinorVersion": uint32(row.MinorVersion),
|
||||
"NeedsPreventQuorum": uint32(row.NeedsPreventQuorum),
|
||||
"NodeDrainStatus": uint32(row.NodeDrainStatus),
|
||||
"NodeHighestVersion": uint32(row.NodeHighestVersion),
|
||||
"NodeLowestVersion": uint32(row.NodeLowestVersion),
|
||||
"NodeWeight": uint32(row.NodeWeight),
|
||||
"State": uint32(row.State),
|
||||
"StatusInformation": uint32(row.StatusInformation),
|
||||
}
|
||||
if osversion.Build() >= osversion.LTSC2022 {
|
||||
values["DetectedCloudPlatform"] = uint32(row.DetectedCloudPlatform)
|
||||
}
|
||||
|
||||
expected = append(expected, clusapi.Object{Name: row.Name, Values: values})
|
||||
}
|
||||
|
||||
start = time.Now()
|
||||
|
||||
nodes, err := native.Nodes(time.Now().Add(time.Minute))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
t.Logf("ClusAPI node query duration: %s, nodes: %d", time.Since(start), len(nodes))
|
||||
|
||||
gotFamilies, gotNames, err := gatherNodes(t, nodes, nil, osversion.Build())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
wantFamilies, wantNames, err := gatherNodes(t, expected, nil, osversion.Build())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Owner labels of resources and groups depend on identical node names.
|
||||
if !slices.Equal(slices.Sorted(slices.Values(gotNames)), slices.Sorted(slices.Values(wantNames))) {
|
||||
t.Errorf("node names = %v, want %v", gotNames, wantNames)
|
||||
}
|
||||
|
||||
if len(gotFamilies) != len(wantFamilies) {
|
||||
t.Errorf("families = %d, want %d", len(gotFamilies), len(wantFamilies))
|
||||
}
|
||||
|
||||
for name, want := range wantFamilies {
|
||||
if !proto.Equal(gotFamilies[name], want) {
|
||||
t.Errorf("native/WMI metric mismatch: %s\nnative: %v\nWMI: %v", name, gotFamilies[name], want)
|
||||
}
|
||||
}
|
||||
|
||||
if t.Failed() {
|
||||
for _, node := range nodes {
|
||||
t.Logf("native node %q DWORD properties: %v", node.Name, node.Values)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNodeSources(b *testing.B) {
|
||||
native, session, _ := resourceComparisonSources(b)
|
||||
|
||||
query, err := mi.NewQuery(nodeParityQuery())
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
b.Run("WMI", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
var rows []msClusterNode
|
||||
if err := session.Query(&rows, mi.NamespaceRootMSCluster, query, time.Minute); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("ClusAPI", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
if _, err := native.Nodes(time.Now().Add(time.Minute)); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
// 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 mscluster
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/headers/clusapi"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
// beforeMinBuild selects how a field is published on Windows builds older than
|
||||
// its objectField.minBuild.
|
||||
type beforeMinBuild int
|
||||
|
||||
const (
|
||||
// omitIfMissing publishes a value that exists and omits a missing one
|
||||
// without an error. The property did not exist on older builds.
|
||||
omitIfMissing beforeMinBuild = iota
|
||||
// zeroIfMissing keeps the previous behavior for properties that the WMI
|
||||
// query did not select on older builds and therefore published as 0.
|
||||
zeroIfMissing
|
||||
// neverPublish keeps the previous behavior for properties that were not
|
||||
// published at all on older builds.
|
||||
neverPublish
|
||||
)
|
||||
|
||||
// objectField maps a ClusAPI value of a cluster, group, node or network to the
|
||||
// metric that previously published the WMI property of the same name.
|
||||
type objectField struct {
|
||||
name string
|
||||
desc *prometheus.Desc
|
||||
// Signed fields are sint32 in WMI, so 0xFFFFFFFF is published as -1.
|
||||
signed bool
|
||||
// MinBuild is the first Windows build that has the property.
|
||||
minBuild uint16
|
||||
older beforeMinBuild
|
||||
// Absent, if set, is published when the cluster service does not report
|
||||
// the property on any build. It reproduces a value that the WMI provider
|
||||
// synthesized; see the cluster field list for each case.
|
||||
absent *uint32
|
||||
}
|
||||
|
||||
// publishObjectFields publishes the fields of one object with its name as the
|
||||
// only label. Missing values are omitted and reported, never published as 0.
|
||||
func publishObjectFields(ch chan<- prometheus.Metric, kind string, object clusapi.Object, fields []objectField, build uint16, resultErr error) error {
|
||||
for _, field := range fields {
|
||||
raw, exists := object.Values[field.name]
|
||||
older := build < field.minBuild
|
||||
|
||||
switch {
|
||||
case older && field.older == neverPublish:
|
||||
continue
|
||||
case exists:
|
||||
case field.absent != nil:
|
||||
raw = *field.absent
|
||||
case older && field.older == zeroIfMissing:
|
||||
raw = 0
|
||||
case older:
|
||||
continue
|
||||
default:
|
||||
resultErr = errors.Join(resultErr, fmt.Errorf("%s %q: missing property %s", kind, object.Name, field.name))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
value := float64(raw)
|
||||
if field.signed {
|
||||
value = float64(int32(raw))
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(field.desc, prometheus.GaugeValue, value, object.Name)
|
||||
}
|
||||
|
||||
return resultErr
|
||||
}
|
||||
@@ -18,18 +18,24 @@
|
||||
package mscluster
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/mi"
|
||||
"github.com/prometheus-community/windows_exporter/internal/headers/clusapi"
|
||||
"github.com/prometheus-community/windows_exporter/internal/types"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const nameResource = Name + "_resource"
|
||||
|
||||
type resourceSource interface {
|
||||
Resources(deadline time.Time) ([]clusapi.Resource, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
type collectorResource struct {
|
||||
resourceMIQuery mi.Query
|
||||
resourceSource resourceSource
|
||||
|
||||
resourceCharacteristics *prometheus.Desc
|
||||
resourceDeadlockTimeout *prometheus.Desc
|
||||
@@ -77,13 +83,18 @@ type msClusterResource struct {
|
||||
}
|
||||
|
||||
func (c *Collector) buildResource() error {
|
||||
resourceMIQuery, err := mi.NewQuery("SELECT Name,Type,OwnerGroup,OwnerNode,Characteristics,DeadlockTimeout,EmbeddedFailureAction,Flags,IsAlivePollInterval,LooksAlivePollInterval,MonitorProcessId,PendingTimeout,ResourceClass,RestartAction,RestartDelay,RestartPeriod,RestartThreshold,RetryPeriodOnFailure,State,Subclass FROM MSCluster_Resource")
|
||||
source, err := clusapi.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create WMI query: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
c.resourceMIQuery = resourceMIQuery
|
||||
c.resourceSource = source
|
||||
c.buildResourceDescriptors()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Collector) buildResourceDescriptors() {
|
||||
c.resourceCharacteristics = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(types.Namespace, nameResource, "characteristics"),
|
||||
"Provides the characteristics of the object.",
|
||||
@@ -192,152 +203,72 @@ func (c *Collector) buildResource() error {
|
||||
[]string{"type", "owner_group", "name"},
|
||||
nil,
|
||||
)
|
||||
|
||||
var dst []msClusterResource
|
||||
|
||||
if err := c.miSession.Query(&dst, mi.NamespaceRootMSCluster, c.resourceMIQuery, 0); err != nil {
|
||||
return fmt.Errorf("WMI query failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Collect sends the metric values for each metric
|
||||
// to the provided prometheus Metric channel.
|
||||
func (c *Collector) collectResource(ch chan<- prometheus.Metric, maxScrapeDuration time.Duration, nodeNames []string) error {
|
||||
var dst []msClusterResource
|
||||
|
||||
if err := c.miSession.Query(&dst, mi.NamespaceRootMSCluster, c.resourceMIQuery, maxScrapeDuration); err != nil {
|
||||
return fmt.Errorf("WMI query failed: %w", err)
|
||||
if c.resourceSource == nil {
|
||||
return errNotBuilt(subCollectorResource)
|
||||
}
|
||||
|
||||
for _, v := range dst {
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.resourceCharacteristics,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.Characteristics),
|
||||
v.Type, v.OwnerGroup, v.Name,
|
||||
)
|
||||
resources, resultErr := callSource(maxScrapeDuration, c.resourceSource.Resources)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.resourceDeadlockTimeout,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.DeadlockTimeout),
|
||||
v.Type, v.OwnerGroup, v.Name,
|
||||
)
|
||||
return c.publishResources(ch, resources, nodeNames, resultErr)
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.resourceEmbeddedFailureAction,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.EmbeddedFailureAction),
|
||||
v.Type, v.OwnerGroup, v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.resourceFlags,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.Flags),
|
||||
v.Type, v.OwnerGroup, v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.resourceIsAlivePollInterval,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.IsAlivePollInterval),
|
||||
v.Type, v.OwnerGroup, v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.resourceLooksAlivePollInterval,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.LooksAlivePollInterval),
|
||||
v.Type, v.OwnerGroup, v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.resourceMonitorProcessId,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.MonitorProcessId),
|
||||
v.Type, v.OwnerGroup, v.Name,
|
||||
)
|
||||
|
||||
for _, nodeName := range nodeNames {
|
||||
isCurrentState := 0.0
|
||||
if v.OwnerNode == nodeName {
|
||||
isCurrentState = 1.0
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.resourceOwnerNode,
|
||||
prometheus.GaugeValue,
|
||||
isCurrentState,
|
||||
v.Type, v.OwnerGroup, nodeName, v.Name,
|
||||
)
|
||||
func (c *Collector) publishResources(ch chan<- prometheus.Metric, resources []clusapi.Resource, nodeNames []string, resultErr error) error {
|
||||
for _, resource := range resources {
|
||||
if !resource.IdentityValid {
|
||||
continue
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.resourcePendingTimeout,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.PendingTimeout),
|
||||
v.Type, v.OwnerGroup, v.Name,
|
||||
)
|
||||
fields := []struct {
|
||||
name string
|
||||
desc *prometheus.Desc
|
||||
}{
|
||||
{"Characteristics", c.resourceCharacteristics},
|
||||
{"DeadlockTimeout", c.resourceDeadlockTimeout},
|
||||
{"EmbeddedFailureAction", c.resourceEmbeddedFailureAction},
|
||||
{"Flags", c.resourceFlags},
|
||||
{"IsAlivePollInterval", c.resourceIsAlivePollInterval},
|
||||
{"LooksAlivePollInterval", c.resourceLooksAlivePollInterval},
|
||||
{"MonitorProcessId", c.resourceMonitorProcessId},
|
||||
{"PendingTimeout", c.resourcePendingTimeout},
|
||||
{"ResourceClass", c.resourceResourceClass},
|
||||
{"RestartAction", c.resourceRestartAction},
|
||||
{"RestartDelay", c.resourceRestartDelay},
|
||||
{"RestartPeriod", c.resourceRestartPeriod},
|
||||
{"RestartThreshold", c.resourceRestartThreshold},
|
||||
{"RetryPeriodOnFailure", c.resourceRetryPeriodOnFailure},
|
||||
{"State", c.resourceState},
|
||||
}
|
||||
for _, field := range fields {
|
||||
value, exists := resource.Values[field.name]
|
||||
if !exists {
|
||||
resultErr = errors.Join(resultErr, fmt.Errorf("resource %q: missing DWORD property %s", resource.Name, field.name))
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.resourceResourceClass,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.ResourceClass),
|
||||
v.Type, v.OwnerGroup, v.Name,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.resourceRestartAction,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.RestartAction),
|
||||
v.Type, v.OwnerGroup, v.Name,
|
||||
)
|
||||
ch <- prometheus.MustNewConstMetric(field.desc, prometheus.GaugeValue, float64(value), resource.Type, resource.OwnerGroup, resource.Name)
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.resourceRestartDelay,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.RestartDelay),
|
||||
v.Type, v.OwnerGroup, v.Name,
|
||||
)
|
||||
// MSCluster_Resource.Subclass is NULL for every resource (seen on
|
||||
// Windows Server 2022, also for IP Address resources whose class
|
||||
// information carries CLUS_RESSUBCLASS_NETWORK_INTERNET_PROTOCOL), so
|
||||
// the WMI collector always published 0. Keep that series unchanged.
|
||||
ch <- prometheus.MustNewConstMetric(c.resourceSubClass, prometheus.GaugeValue, 0, resource.Type, resource.OwnerGroup, resource.Name)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.resourceRestartPeriod,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.RestartPeriod),
|
||||
v.Type, v.OwnerGroup, v.Name,
|
||||
)
|
||||
for _, nodeName := range nodeNames {
|
||||
value := 0.0
|
||||
if resource.OwnerNode == nodeName {
|
||||
value = 1
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.resourceRestartThreshold,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.RestartThreshold),
|
||||
v.Type, v.OwnerGroup, v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.resourceRetryPeriodOnFailure,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.RetryPeriodOnFailure),
|
||||
v.Type, v.OwnerGroup, v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.resourceState,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.State),
|
||||
v.Type, v.OwnerGroup, v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.resourceSubClass,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.Subclass),
|
||||
v.Type, v.OwnerGroup, v.Name,
|
||||
)
|
||||
ch <- prometheus.MustNewConstMetric(c.resourceOwnerNode, prometheus.GaugeValue, value, resource.Type, resource.OwnerGroup, nodeName, resource.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return resultErr
|
||||
}
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
// 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 mscluster
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"maps"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/headers/clusapi"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
)
|
||||
|
||||
type resourceFixtureSource struct {
|
||||
resources []clusapi.Resource
|
||||
err error
|
||||
started chan struct{}
|
||||
release chan struct{}
|
||||
closeCount int
|
||||
}
|
||||
|
||||
func (s *resourceFixtureSource) Resources(time.Time) ([]clusapi.Resource, error) {
|
||||
if s.started != nil {
|
||||
close(s.started)
|
||||
<-s.release
|
||||
}
|
||||
|
||||
return s.resources, s.err
|
||||
}
|
||||
|
||||
func (s *resourceFixtureSource) Close() error {
|
||||
s.closeCount++
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type gatheredSamples []prometheus.Metric
|
||||
|
||||
func (s gatheredSamples) Describe(ch chan<- *prometheus.Desc) {
|
||||
for _, metric := range s {
|
||||
ch <- metric.Desc()
|
||||
}
|
||||
}
|
||||
|
||||
func (s gatheredSamples) Collect(ch chan<- prometheus.Metric) {
|
||||
for _, metric := range s {
|
||||
ch <- metric
|
||||
}
|
||||
}
|
||||
|
||||
func resourceFixture() clusapi.Resource {
|
||||
return clusapi.Resource{Name: "Disk", Type: "Physical Disk", OwnerGroup: "Cluster Group", OwnerNode: "nodeB", IdentityValid: true, Values: map[string]uint32{
|
||||
"Characteristics": 1, "DeadlockTimeout": 2, "EmbeddedFailureAction": 3, "Flags": 4, "IsAlivePollInterval": ^uint32(0), "LooksAlivePollInterval": 6, "MonitorProcessId": 7, "PendingTimeout": 8, "ResourceClass": 1, "RestartAction": 10, "RestartDelay": 11, "RestartPeriod": 12, "RestartThreshold": 13, "RetryPeriodOnFailure": 14, "State": ^uint32(0), "Subclass": 0x80000000,
|
||||
}}
|
||||
}
|
||||
|
||||
func gatherResources(t *testing.T, resources []clusapi.Resource, inputErr error, nodeNames ...string) (map[string]*dto.MetricFamily, error) {
|
||||
t.Helper()
|
||||
|
||||
c := New(&Config{CollectorsEnabled: []string{subCollectorResource}})
|
||||
c.buildResourceDescriptors()
|
||||
|
||||
ch := make(chan prometheus.Metric, 100)
|
||||
|
||||
if len(nodeNames) == 0 {
|
||||
nodeNames = []string{"nodeA", "nodeB"}
|
||||
}
|
||||
|
||||
publicationDone := make(chan error, 1)
|
||||
go func() {
|
||||
publicationDone <- c.publishResources(ch, resources, nodeNames, inputErr)
|
||||
|
||||
close(ch)
|
||||
}()
|
||||
|
||||
var samples gatheredSamples
|
||||
for metric := range ch {
|
||||
samples = append(samples, metric)
|
||||
}
|
||||
|
||||
err := <-publicationDone
|
||||
|
||||
registry := prometheus.NewPedanticRegistry()
|
||||
if err := registry.Register(samples); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
families, gatherErr := registry.Gather()
|
||||
if gatherErr != nil {
|
||||
t.Fatal(gatherErr)
|
||||
}
|
||||
|
||||
result := make(map[string]*dto.MetricFamily, len(families))
|
||||
for _, family := range families {
|
||||
result[family.GetName()] = family
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func metricLabels(metric *dto.Metric) map[string]string {
|
||||
labels := make(map[string]string)
|
||||
for _, label := range metric.GetLabel() {
|
||||
labels[label.GetName()] = label.GetValue()
|
||||
}
|
||||
|
||||
return labels
|
||||
}
|
||||
|
||||
func TestResourceNativeMetricContract(t *testing.T) {
|
||||
families, err := gatherResources(t, []clusapi.Resource{resourceFixture()}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := map[string]float64{
|
||||
"characteristics": 1, "deadlock_timeout": 2, "embedded_failure_action": 3, "flags": 4, "is_alive_poll_interval": 4294967295, "looks_alive_poll_interval": 6, "monitor_process_id": 7, "pending_timeout": 8, "resource_class": 1, "restart_action": 10, "restart_delay": 11, "restart_period": 12, "restart_threshold": 13, "retry_period_on_failure": 14, "state": 4294967295, "subclass": 0,
|
||||
}
|
||||
|
||||
if len(families) != 17 {
|
||||
t.Fatalf("families = %d", len(families))
|
||||
}
|
||||
|
||||
for suffix, value := range expected {
|
||||
family := families["windows_mscluster_resource_"+suffix]
|
||||
if family == nil || family.GetType() != dto.MetricType_GAUGE || len(family.GetMetric()) != 1 {
|
||||
t.Fatalf("invalid family %s: %v", suffix, family)
|
||||
}
|
||||
|
||||
metric := family.GetMetric()[0]
|
||||
if metric.GetGauge().GetValue() != value {
|
||||
t.Errorf("%s value = %v, want %v", suffix, metric.GetGauge().GetValue(), value)
|
||||
}
|
||||
|
||||
if !maps.Equal(metricLabels(metric), map[string]string{"type": "Physical Disk", "owner_group": "Cluster Group", "name": "Disk"}) {
|
||||
t.Fatalf("labels = %v", metricLabels(metric))
|
||||
}
|
||||
}
|
||||
|
||||
owners := families["windows_mscluster_resource_owner_node"]
|
||||
if owners.GetType() != dto.MetricType_GAUGE || len(owners.GetMetric()) != 2 {
|
||||
t.Fatal("invalid owner family")
|
||||
}
|
||||
|
||||
for _, metric := range owners.GetMetric() {
|
||||
labels := metricLabels(metric)
|
||||
|
||||
value := 0.0
|
||||
if labels["node_name"] == "nodeB" {
|
||||
value = 1
|
||||
}
|
||||
|
||||
if metric.GetGauge().GetValue() != value {
|
||||
t.Fatal("invalid owner value")
|
||||
}
|
||||
|
||||
if !maps.Equal(labels, map[string]string{"type": "Physical Disk", "owner_group": "Cluster Group", "name": "Disk", "node_name": labels["node_name"]}) {
|
||||
t.Fatal("invalid owner labels")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourcePartialResultsAndJoinedErrors(t *testing.T) {
|
||||
first, second := errors.New("first cause"), errors.New("second cause")
|
||||
resource := resourceFixture()
|
||||
delete(resource.Values, "DeadlockTimeout")
|
||||
|
||||
families, err := gatherResources(t, []clusapi.Resource{resource, {Name: "unavailable"}}, errors.Join(first, second))
|
||||
if !errors.Is(err, first) || !errors.Is(err, second) {
|
||||
t.Fatalf("lost joined cause: %v", err)
|
||||
}
|
||||
|
||||
if _, exists := families["windows_mscluster_resource_deadlock_timeout"]; exists {
|
||||
t.Fatal("published missing field as zero")
|
||||
}
|
||||
|
||||
if len(families) != 16 {
|
||||
t.Fatalf("lost successful properties: %d", len(families))
|
||||
}
|
||||
|
||||
for _, family := range families {
|
||||
for _, metric := range family.GetMetric() {
|
||||
if metricLabels(metric)["name"] != "Disk" {
|
||||
t.Fatal("published invalid identity")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceOwnerNameCasePreserved(t *testing.T) {
|
||||
resource := resourceFixture()
|
||||
resource.OwnerNode = "NODEB"
|
||||
|
||||
families, err := gatherResources(t, []clusapi.Resource{resource}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, metric := range families["windows_mscluster_resource_owner_node"].GetMetric() {
|
||||
if metric.GetGauge().GetValue() != 0 {
|
||||
t.Fatal("changed case-sensitive owner matching")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceCloseWaitsForCollection(t *testing.T) {
|
||||
source := &resourceFixtureSource{resources: []clusapi.Resource{resourceFixture()}, started: make(chan struct{}), release: make(chan struct{})}
|
||||
c := New(&Config{CollectorsEnabled: []string{subCollectorResource}})
|
||||
c.resourceSource = source
|
||||
c.buildResourceDescriptors()
|
||||
|
||||
collectDone := make(chan error, 1)
|
||||
go func() { collectDone <- c.Collect(make(chan prometheus.Metric, 100), time.Second) }()
|
||||
|
||||
<-source.started
|
||||
|
||||
closeDone := make(chan error, 1)
|
||||
go func() { closeDone <- c.Close() }()
|
||||
|
||||
select {
|
||||
case err := <-closeDone:
|
||||
t.Fatalf("closed during collection: %v", err)
|
||||
case <-time.After(10 * time.Millisecond):
|
||||
}
|
||||
|
||||
close(source.release)
|
||||
|
||||
if err := <-collectDone; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := <-closeDone; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := c.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if source.closeCount != 1 {
|
||||
t.Fatalf("close count = %d", source.closeCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceRepeatedBuildClosesOldSource(t *testing.T) {
|
||||
source := &resourceFixtureSource{}
|
||||
c := New(&Config{CollectorsEnabled: []string{"unsupported"}})
|
||||
|
||||
c.resourceSource = source
|
||||
if err := c.Build(slog.Default(), nil); err == nil {
|
||||
t.Fatal("accepted unsupported collector")
|
||||
}
|
||||
|
||||
if source.closeCount != 1 || c.resourceSource != nil {
|
||||
t.Fatal("old resource source leaked")
|
||||
}
|
||||
|
||||
if err := c.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkResourcePublication(b *testing.B) {
|
||||
c := New(&Config{CollectorsEnabled: []string{subCollectorResource}})
|
||||
c.buildResourceDescriptors()
|
||||
|
||||
resources := []clusapi.Resource{resourceFixture()}
|
||||
ch := make(chan prometheus.Metric, 18)
|
||||
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
if err := c.publishResources(ch, resources, []string{"nodeA", "nodeB"}, nil); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
for range 18 {
|
||||
<-ch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceLargeClusterGather(t *testing.T) {
|
||||
resources := make([]clusapi.Resource, 81)
|
||||
for index := range resources {
|
||||
resources[index] = resourceFixture()
|
||||
resources[index].Name = fmt.Sprintf("resource-%d", index)
|
||||
}
|
||||
|
||||
families, err := gatherResources(t, resources, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(families) != 17 {
|
||||
t.Fatalf("families = %d, want 17", len(families))
|
||||
}
|
||||
|
||||
for name, family := range families {
|
||||
want := len(resources)
|
||||
if name == "windows_mscluster_resource_owner_node" {
|
||||
want *= 2
|
||||
}
|
||||
|
||||
if len(family.GetMetric()) != want {
|
||||
t.Errorf("%s samples = %d, want %d", name, len(family.GetMetric()), want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
// 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 mscluster
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/headers/clusapi"
|
||||
"github.com/prometheus-community/windows_exporter/internal/mi"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// This parity test runs against the CI failover-cluster fixture. Optional local
|
||||
// hosts may lack the role; fixture-required hosts must never skip native failures.
|
||||
func TestResourceNativeWMIParity(t *testing.T) {
|
||||
native, session, required := resourceComparisonSources(t)
|
||||
|
||||
query, err := mi.NewQuery("SELECT Name,Type,OwnerGroup,OwnerNode,Characteristics,DeadlockTimeout,EmbeddedFailureAction,Flags,IsAlivePollInterval,LooksAlivePollInterval,MonitorProcessId,PendingTimeout,ResourceClass,RestartAction,RestartDelay,RestartPeriod,RestartThreshold,RetryPeriodOnFailure,State,Subclass FROM MSCluster_Resource")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var rows []msClusterResource
|
||||
|
||||
start := time.Now()
|
||||
|
||||
if err := session.Query(&rows, mi.NamespaceRootMSCluster, query, time.Minute); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Logf("WMI resource query duration: %s", time.Since(start))
|
||||
|
||||
// The CI cluster provisions online, offline and failed resources. Comparing
|
||||
// two empty sets proves nothing, so the fixture must provide resources.
|
||||
if len(rows) == 0 {
|
||||
if required {
|
||||
t.Fatal("cluster resource fixture has no resources")
|
||||
}
|
||||
|
||||
t.Log("cluster has no resources; comparing empty sources")
|
||||
}
|
||||
|
||||
expected := make([]clusapi.Resource, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
resource := clusapi.Resource{Name: row.Name, Type: row.Type, OwnerGroup: row.OwnerGroup, OwnerNode: row.OwnerNode, IdentityValid: true, Values: make(map[string]uint32)}
|
||||
|
||||
value := reflect.ValueOf(row)
|
||||
for index := 4; index < value.NumField(); index++ {
|
||||
resource.Values[value.Type().Field(index).Name] = uint32(value.Field(index).Uint())
|
||||
}
|
||||
|
||||
expected = append(expected, resource)
|
||||
}
|
||||
|
||||
start = time.Now()
|
||||
|
||||
resources, err := native.Resources(time.Now().Add(time.Minute))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Logf("ClusAPI resource query duration: %s", time.Since(start))
|
||||
|
||||
nodeQuery, err := mi.NewQuery("SELECT Name FROM MSCluster_Node")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var nodes []struct {
|
||||
Name string `mi:"Name"`
|
||||
}
|
||||
if err := session.Query(&nodes, mi.NamespaceRootMSCluster, nodeQuery, time.Minute); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
nodeNames := make([]string, 0, len(nodes))
|
||||
for _, node := range nodes {
|
||||
nodeNames = append(nodeNames, node.Name)
|
||||
}
|
||||
|
||||
if len(nodeNames) == 0 {
|
||||
t.Fatal("cluster fixture has no nodes")
|
||||
}
|
||||
|
||||
gotFamilies, err := gatherResources(t, resources, nil, nodeNames...)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
wantFamilies, err := gatherResources(t, expected, nil, nodeNames...)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(gotFamilies) != len(wantFamilies) {
|
||||
t.Fatalf("families = %d, want %d", len(gotFamilies), len(wantFamilies))
|
||||
}
|
||||
|
||||
for name, want := range wantFamilies {
|
||||
if !proto.Equal(gotFamilies[name], want) {
|
||||
t.Errorf("native/WMI metric mismatch: %s\nnative: %v\nWMI: %v", name, gotFamilies[name], want)
|
||||
}
|
||||
}
|
||||
|
||||
if t.Failed() {
|
||||
logResourceWMINulls(t, session, query)
|
||||
}
|
||||
}
|
||||
|
||||
// logResourceWMINulls shows which WMI properties are NULL. The WMI collector
|
||||
// published NULL as 0, so a native value can only match if it is 0 as well.
|
||||
func logResourceWMINulls(t *testing.T, session *mi.Session, query mi.Query) {
|
||||
t.Helper()
|
||||
|
||||
err := session.QueryFunc(mi.NamespaceRootMSCluster, query, time.Minute, func(instance *mi.Instance) error {
|
||||
element, err := instance.GetElement("Name")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
name, err := element.String()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var nulls []string
|
||||
|
||||
for _, property := range []string{"Characteristics", "DeadlockTimeout", "EmbeddedFailureAction", "Flags", "IsAlivePollInterval", "LooksAlivePollInterval", "MonitorProcessId", "PendingTimeout", "ResourceClass", "RestartAction", "RestartDelay", "RestartPeriod", "RestartThreshold", "RetryPeriodOnFailure", "State", "Subclass", "Type", "OwnerGroup", "OwnerNode"} {
|
||||
element, err := instance.GetElement(property)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if element.IsNull() {
|
||||
nulls = append(nulls, property)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("WMI resource %q NULL properties: %v", name, nulls)
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Logf("WMI NULL diagnostics: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Resource source comparisons require a live cluster; missing CI fixtures fail.
|
||||
func resourceComparisonSources(tb testing.TB) (*clusapi.Cluster, *mi.Session, bool) {
|
||||
tb.Helper()
|
||||
|
||||
required := slices.Contains(strings.Split(os.Getenv("WINDOWS_EXPORTER_TEST_COLLECTORS"), ","), Name)
|
||||
|
||||
native, err := clusapi.Open()
|
||||
if err != nil {
|
||||
if !required && errors.Is(err, errors.ErrUnsupported) {
|
||||
tb.Skipf("failover cluster role unavailable: %v", err)
|
||||
}
|
||||
|
||||
tb.Fatal(err)
|
||||
}
|
||||
|
||||
tb.Cleanup(func() {
|
||||
if err := native.Close(); err != nil {
|
||||
tb.Error(err)
|
||||
}
|
||||
})
|
||||
|
||||
app, err := mi.ApplicationInitialize()
|
||||
if err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
|
||||
tb.Cleanup(func() {
|
||||
if err := app.Close(); err != nil {
|
||||
tb.Error(err)
|
||||
}
|
||||
})
|
||||
|
||||
session, err := app.NewSession(nil)
|
||||
if err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
|
||||
tb.Cleanup(func() {
|
||||
if err := session.Close(); err != nil {
|
||||
tb.Error(err)
|
||||
}
|
||||
})
|
||||
|
||||
return native, session, required
|
||||
}
|
||||
|
||||
func BenchmarkResourceSources(b *testing.B) {
|
||||
native, session, _ := resourceComparisonSources(b)
|
||||
|
||||
query, err := mi.NewQuery("SELECT Name,Type,OwnerGroup,OwnerNode,Characteristics,DeadlockTimeout,EmbeddedFailureAction,Flags,IsAlivePollInterval,LooksAlivePollInterval,MonitorProcessId,PendingTimeout,ResourceClass,RestartAction,RestartDelay,RestartPeriod,RestartThreshold,RetryPeriodOnFailure,State,Subclass FROM MSCluster_Resource")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
var rows []msClusterResource
|
||||
if err := session.Query(&rows, mi.NamespaceRootMSCluster, query, time.Minute); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
// Timing two empty enumerations says nothing about resource collection.
|
||||
if len(rows) == 0 {
|
||||
b.Skip("cluster has no resources")
|
||||
}
|
||||
|
||||
b.Run("WMI", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
var rows []msClusterResource
|
||||
if err := session.Query(&rows, mi.NamespaceRootMSCluster, query, time.Minute); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("ClusAPI", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
if _, err := native.Resources(time.Now().Add(time.Minute)); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -18,18 +18,23 @@
|
||||
package mscluster
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/mi"
|
||||
"github.com/prometheus-community/windows_exporter/internal/headers/clusapi"
|
||||
"github.com/prometheus-community/windows_exporter/internal/osversion"
|
||||
"github.com/prometheus-community/windows_exporter/internal/types"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const nameResourceGroup = Name + "_resourcegroup"
|
||||
|
||||
type resourceGroupSource interface {
|
||||
Groups(deadline time.Time) ([]clusapi.Object, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
type collectorResourceGroup struct {
|
||||
resourceGroupMIQuery mi.Query
|
||||
resourceGroupSource resourceGroupSource
|
||||
|
||||
resourceGroupAutoFailbackType *prometheus.Desc
|
||||
resourceGroupCharacteristics *prometheus.Desc
|
||||
@@ -47,7 +52,8 @@ type collectorResourceGroup struct {
|
||||
resourceGroupState *prometheus.Desc
|
||||
}
|
||||
|
||||
// msClusterResourceGroup represents the MSCluster_ResourceGroup WMI class
|
||||
// msClusterResourceGroup represents the MSCluster_ResourceGroup WMI class. The
|
||||
// collector reads ClusAPI; the parity test compares against this WMI model.
|
||||
// - https://docs.microsoft.com/en-us/previous-versions/windows/desktop/cluswmi/mscluster-resourcegroup
|
||||
type msClusterResourceGroup struct {
|
||||
Name string `mi:"Name"`
|
||||
@@ -69,13 +75,18 @@ type msClusterResourceGroup struct {
|
||||
}
|
||||
|
||||
func (c *Collector) buildResourceGroup() error {
|
||||
resourceGroupMIQuery, err := mi.NewQuery("SELECT AutoFailbackType,Characteristics,ColdStartSetting,DefaultOwner,FailbackWindowEnd,FailbackWindowStart,FailoverPeriod,FailoverThreshold,Flags,GroupType,OwnerNode,Priority,ResiliencyPeriod,State FROM MSCluster_ResourceGroup")
|
||||
source, err := clusapi.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create WMI query: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
c.resourceGroupMIQuery = resourceGroupMIQuery
|
||||
c.resourceGroupSource = source
|
||||
c.buildResourceGroupDescriptors()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Collector) buildResourceGroupDescriptors() {
|
||||
c.resourceGroupAutoFailbackType = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(types.Namespace, nameResourceGroup, "auto_failback_type"),
|
||||
"Provides access to the group's AutoFailbackType property.",
|
||||
@@ -142,12 +153,6 @@ func (c *Collector) buildResourceGroup() error {
|
||||
[]string{"node_name", "name"},
|
||||
nil,
|
||||
)
|
||||
c.resourceGroupOwnerNode = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(types.Namespace, nameResourceGroup, "owner_node"),
|
||||
"The node hosting the resource group. 0: Not hosted; 1: Hosted",
|
||||
[]string{"node_name", "name"},
|
||||
nil,
|
||||
)
|
||||
c.resourceGroupPriority = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(types.Namespace, nameResourceGroup, "priority"),
|
||||
"Priority value of the resource group",
|
||||
@@ -166,131 +171,57 @@ func (c *Collector) buildResourceGroup() error {
|
||||
[]string{"name"},
|
||||
nil,
|
||||
)
|
||||
|
||||
var dst []msClusterResourceGroup
|
||||
|
||||
if err := c.miSession.Query(&dst, mi.NamespaceRootMSCluster, c.resourceGroupMIQuery, 0); err != nil {
|
||||
return fmt.Errorf("WMI query failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Collect sends the metric values for each metric
|
||||
// to the provided prometheus Metric channel.
|
||||
func (c *Collector) collectResourceGroup(ch chan<- prometheus.Metric, maxScrapeDuration time.Duration, nodeNames []string) error {
|
||||
var dst []msClusterResourceGroup
|
||||
|
||||
if err := c.miSession.Query(&dst, mi.NamespaceRootMSCluster, c.resourceGroupMIQuery, maxScrapeDuration); err != nil {
|
||||
return fmt.Errorf("WMI query failed: %w", err)
|
||||
if c.resourceGroupSource == nil {
|
||||
return errNotBuilt(subCollectorResourceGroup)
|
||||
}
|
||||
|
||||
for _, v := range dst {
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.resourceGroupAutoFailbackType,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.AutoFailbackType),
|
||||
v.Name,
|
||||
)
|
||||
groups, resultErr := callSource(maxScrapeDuration, c.resourceGroupSource.Groups)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.resourceGroupCharacteristics,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.Characteristics),
|
||||
v.Name,
|
||||
)
|
||||
return c.publishResourceGroups(ch, groups, nodeNames, osversion.Build(), resultErr)
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.resourceGroupColdStartSetting,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.ColdStartSetting),
|
||||
v.Name,
|
||||
)
|
||||
func (c *Collector) publishResourceGroups(ch chan<- prometheus.Metric, groups []clusapi.Object, nodeNames []string, build uint16, resultErr error) error {
|
||||
fields := []objectField{
|
||||
{name: "AutoFailbackType", desc: c.resourceGroupAutoFailbackType},
|
||||
{name: "Characteristics", desc: c.resourceGroupCharacteristics},
|
||||
{name: "ColdStartSetting", desc: c.resourceGroupColdStartSetting, minBuild: osversion.LTSC2016, older: omitIfMissing},
|
||||
{name: "DefaultOwner", desc: c.resourceGroupDefaultOwner},
|
||||
{name: "FailbackWindowEnd", desc: c.resourceGroupFailbackWindowEnd, signed: true},
|
||||
{name: "FailbackWindowStart", desc: c.resourceGroupFailbackWindowStart, signed: true},
|
||||
{name: "FailoverPeriod", desc: c.resourceGroupFailOverPeriod},
|
||||
{name: "FailoverThreshold", desc: c.resourceGroupFailOverThreshold},
|
||||
{name: "Flags", desc: c.resourceGroupFlags},
|
||||
{name: "GroupType", desc: c.resourceGroupGroupType},
|
||||
{name: "Priority", desc: c.resourceGroupPriority},
|
||||
{name: "ResiliencyPeriod", desc: c.resourceGroupResiliencyPeriod, minBuild: osversion.LTSC2016},
|
||||
{name: "State", desc: c.resourceGroupState},
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.resourceGroupDefaultOwner,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.DefaultOwner),
|
||||
v.Name,
|
||||
)
|
||||
for _, group := range groups {
|
||||
if group.Name == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.resourceGroupFailbackWindowEnd,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.FailbackWindowEnd),
|
||||
v.Name,
|
||||
)
|
||||
resultErr = publishObjectFields(ch, "group", group, fields, build, resultErr)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.resourceGroupFailbackWindowStart,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.FailbackWindowStart),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.resourceGroupFailOverPeriod,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.FailoverPeriod),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.resourceGroupFailOverThreshold,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.FailoverThreshold),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.resourceGroupFlags,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.Flags),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.resourceGroupGroupType,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.GroupType),
|
||||
v.Name,
|
||||
)
|
||||
if !group.OwnerNodeValid {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, nodeName := range nodeNames {
|
||||
isCurrentState := 0.0
|
||||
if v.OwnerNode == nodeName {
|
||||
if group.OwnerNode == nodeName {
|
||||
isCurrentState = 1.0
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.resourceGroupOwnerNode,
|
||||
prometheus.GaugeValue,
|
||||
isCurrentState,
|
||||
nodeName, v.Name,
|
||||
)
|
||||
ch <- prometheus.MustNewConstMetric(c.resourceGroupOwnerNode, prometheus.GaugeValue, isCurrentState, nodeName, group.Name)
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.resourceGroupPriority,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.Priority),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.resourceGroupResiliencyPeriod,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.ResiliencyPeriod),
|
||||
v.Name,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.resourceGroupState,
|
||||
prometheus.GaugeValue,
|
||||
float64(v.State),
|
||||
v.Name,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
return resultErr
|
||||
}
|
||||
|
||||
@@ -0,0 +1,381 @@
|
||||
// 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 mscluster
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"maps"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/headers/clusapi"
|
||||
"github.com/prometheus-community/windows_exporter/internal/osversion"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
)
|
||||
|
||||
type objectFixtureSource struct {
|
||||
objects []clusapi.Object
|
||||
err error
|
||||
started chan struct{}
|
||||
release chan struct{}
|
||||
closeCount int
|
||||
closeErr error
|
||||
}
|
||||
|
||||
func (s *objectFixtureSource) objectsWithDeadline() ([]clusapi.Object, error) {
|
||||
if s.started != nil {
|
||||
close(s.started)
|
||||
<-s.release
|
||||
}
|
||||
|
||||
return s.objects, s.err
|
||||
}
|
||||
|
||||
func (s *objectFixtureSource) Groups(time.Time) ([]clusapi.Object, error) {
|
||||
return s.objectsWithDeadline()
|
||||
}
|
||||
|
||||
func (s *objectFixtureSource) Nodes(time.Time) ([]clusapi.Object, error) {
|
||||
return s.objectsWithDeadline()
|
||||
}
|
||||
|
||||
func (s *objectFixtureSource) Networks(time.Time) ([]clusapi.Object, error) {
|
||||
return s.objectsWithDeadline()
|
||||
}
|
||||
|
||||
func (s *objectFixtureSource) Close() error {
|
||||
s.closeCount++
|
||||
|
||||
return s.closeErr
|
||||
}
|
||||
|
||||
// gatherPublished registers the published samples in a pedantic registry, so
|
||||
// invalid descriptors, duplicate series and label mismatches fail the test.
|
||||
func gatherPublished(t *testing.T, publish func(chan<- prometheus.Metric) error) (map[string]*dto.MetricFamily, error) {
|
||||
t.Helper()
|
||||
|
||||
ch := make(chan prometheus.Metric, 100)
|
||||
publicationDone := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
publicationDone <- publish(ch)
|
||||
|
||||
close(ch)
|
||||
}()
|
||||
|
||||
var samples gatheredSamples
|
||||
for metric := range ch {
|
||||
samples = append(samples, metric)
|
||||
}
|
||||
|
||||
err := <-publicationDone
|
||||
|
||||
registry := prometheus.NewPedanticRegistry()
|
||||
if err := registry.Register(samples); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
families, gatherErr := registry.Gather()
|
||||
if gatherErr != nil {
|
||||
t.Fatal(gatherErr)
|
||||
}
|
||||
|
||||
result := make(map[string]*dto.MetricFamily, len(families))
|
||||
for _, family := range families {
|
||||
result[family.GetName()] = family
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func resourceGroupFixture() clusapi.Object {
|
||||
return clusapi.Object{Name: "Cluster Group", OwnerNode: "nodeB", OwnerNodeValid: true, Values: map[string]uint32{
|
||||
"AutoFailbackType": 1, "Characteristics": 2, "ColdStartSetting": 3, "DefaultOwner": ^uint32(0), "FailbackWindowEnd": ^uint32(0), "FailbackWindowStart": 5, "FailoverPeriod": 6, "FailoverThreshold": ^uint32(0) - 1, "Flags": 1, "GroupType": 9999, "Priority": 3000, "ResiliencyPeriod": 240, "State": ^uint32(0),
|
||||
}}
|
||||
}
|
||||
|
||||
func gatherResourceGroups(t *testing.T, groups []clusapi.Object, inputErr error, build uint16, nodeNames ...string) (map[string]*dto.MetricFamily, error) {
|
||||
t.Helper()
|
||||
|
||||
c := New(&Config{CollectorsEnabled: []string{subCollectorResourceGroup}})
|
||||
c.buildResourceGroupDescriptors()
|
||||
|
||||
if len(nodeNames) == 0 {
|
||||
nodeNames = []string{"nodeA", "nodeB"}
|
||||
}
|
||||
|
||||
return gatherPublished(t, func(ch chan<- prometheus.Metric) error {
|
||||
return c.publishResourceGroups(ch, groups, nodeNames, build, inputErr)
|
||||
})
|
||||
}
|
||||
|
||||
func TestResourceGroupNativeMetricContract(t *testing.T) {
|
||||
families, err := gatherResourceGroups(t, []clusapi.Object{resourceGroupFixture()}, nil, osversion.LTSC2022)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := map[string]float64{
|
||||
"auto_failback_type": 1, "characteristics": 2, "cold_start_setting": 3, "default_owner": 4294967295, "failback_window_end": -1, "failback_window_start": 5, "failover_period": 6, "failover_threshold": 4294967294, "flags": 1, "group_type": 9999, "priority": 3000, "resiliency_period": 240, "state": 4294967295,
|
||||
}
|
||||
|
||||
if len(families) != len(expected)+1 {
|
||||
t.Fatalf("families = %d", len(families))
|
||||
}
|
||||
|
||||
for suffix, value := range expected {
|
||||
family := families["windows_mscluster_resourcegroup_"+suffix]
|
||||
if family == nil || family.GetType() != dto.MetricType_GAUGE || len(family.GetMetric()) != 1 {
|
||||
t.Fatalf("invalid family %s: %v", suffix, family)
|
||||
}
|
||||
|
||||
metric := family.GetMetric()[0]
|
||||
if metric.GetGauge().GetValue() != value {
|
||||
t.Errorf("%s value = %v, want %v", suffix, metric.GetGauge().GetValue(), value)
|
||||
}
|
||||
|
||||
if !maps.Equal(metricLabels(metric), map[string]string{"name": "Cluster Group"}) {
|
||||
t.Fatalf("labels = %v", metricLabels(metric))
|
||||
}
|
||||
}
|
||||
|
||||
owners := families["windows_mscluster_resourcegroup_owner_node"]
|
||||
if owners.GetType() != dto.MetricType_GAUGE || len(owners.GetMetric()) != 2 {
|
||||
t.Fatal("invalid owner family")
|
||||
}
|
||||
|
||||
for _, metric := range owners.GetMetric() {
|
||||
labels := metricLabels(metric)
|
||||
|
||||
value := 0.0
|
||||
if labels["node_name"] == "nodeB" {
|
||||
value = 1
|
||||
}
|
||||
|
||||
if metric.GetGauge().GetValue() != value {
|
||||
t.Fatal("invalid owner value")
|
||||
}
|
||||
|
||||
if !maps.Equal(labels, map[string]string{"name": "Cluster Group", "node_name": labels["node_name"]}) {
|
||||
t.Fatalf("invalid owner labels: %v", labels)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceGroupPartialResultsAndJoinedErrors(t *testing.T) {
|
||||
first, second := errors.New("first cause"), errors.New("second cause")
|
||||
group := resourceGroupFixture()
|
||||
delete(group.Values, "Priority")
|
||||
delete(group.Values, "State")
|
||||
|
||||
group.OwnerNodeValid = false
|
||||
|
||||
families, err := gatherResourceGroups(t, []clusapi.Object{group, {}}, errors.Join(first, second), osversion.LTSC2022)
|
||||
if !errors.Is(err, first) || !errors.Is(err, second) {
|
||||
t.Fatalf("lost joined cause: %v", err)
|
||||
}
|
||||
|
||||
for _, name := range []string{"priority", "state", "owner_node"} {
|
||||
if _, exists := families["windows_mscluster_resourcegroup_"+name]; exists {
|
||||
t.Fatalf("published unreadable %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(families) != 11 {
|
||||
t.Fatalf("lost successful properties: %d", len(families))
|
||||
}
|
||||
|
||||
for _, family := range families {
|
||||
for _, metric := range family.GetMetric() {
|
||||
if metricLabels(metric)["name"] != "Cluster Group" {
|
||||
t.Fatal("published invalid identity")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceGroupServer2016FieldsOnOlderBuilds(t *testing.T) {
|
||||
group := resourceGroupFixture()
|
||||
delete(group.Values, "ColdStartSetting")
|
||||
delete(group.Values, "ResiliencyPeriod")
|
||||
|
||||
families, err := gatherResourceGroups(t, []clusapi.Object{group}, nil, osversion.LTSC2016-1)
|
||||
if err != nil {
|
||||
t.Fatalf("missing Windows Server 2016 fields failed an older build: %v", err)
|
||||
}
|
||||
|
||||
if len(families) != 12 {
|
||||
t.Fatalf("families = %d", len(families))
|
||||
}
|
||||
|
||||
if _, err := gatherResourceGroups(t, []clusapi.Object{group}, nil, osversion.LTSC2022); err == nil {
|
||||
t.Fatal("missing Windows Server 2016 fields were ignored on a newer build")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceGroupOwnerNameCasePreserved(t *testing.T) {
|
||||
group := resourceGroupFixture()
|
||||
group.OwnerNode = "NODEB"
|
||||
|
||||
families, err := gatherResourceGroups(t, []clusapi.Object{group}, nil, osversion.LTSC2022)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, metric := range families["windows_mscluster_resourcegroup_owner_node"].GetMetric() {
|
||||
if metric.GetGauge().GetValue() != 0 {
|
||||
t.Fatal("changed case-sensitive owner matching")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceGroupCloseWaitsForCollection(t *testing.T) {
|
||||
source := &objectFixtureSource{objects: []clusapi.Object{resourceGroupFixture()}, started: make(chan struct{}), release: make(chan struct{})}
|
||||
c := New(&Config{CollectorsEnabled: []string{subCollectorResourceGroup}})
|
||||
c.resourceGroupSource = source
|
||||
c.buildResourceGroupDescriptors()
|
||||
|
||||
collectDone := make(chan error, 1)
|
||||
go func() { collectDone <- c.Collect(make(chan prometheus.Metric, 100), time.Second) }()
|
||||
|
||||
<-source.started
|
||||
|
||||
closeDone := make(chan error, 1)
|
||||
go func() { closeDone <- c.Close() }()
|
||||
|
||||
select {
|
||||
case err := <-closeDone:
|
||||
t.Fatalf("closed during collection: %v", err)
|
||||
case <-time.After(10 * time.Millisecond):
|
||||
}
|
||||
|
||||
close(source.release)
|
||||
|
||||
if err := <-collectDone; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := <-closeDone; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := c.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if source.closeCount != 1 {
|
||||
t.Fatalf("close count = %d", source.closeCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceGroupRepeatedBuildClosesOldSources(t *testing.T) {
|
||||
resources, groups := &resourceFixtureSource{}, &objectFixtureSource{}
|
||||
c := New(&Config{CollectorsEnabled: []string{"unsupported"}})
|
||||
c.resourceSource, c.resourceGroupSource = resources, groups
|
||||
|
||||
if err := c.Build(slog.Default(), nil); err == nil {
|
||||
t.Fatal("accepted unsupported collector")
|
||||
}
|
||||
|
||||
if resources.closeCount != 1 || groups.closeCount != 1 || c.resourceSource != nil || c.resourceGroupSource != nil {
|
||||
t.Fatal("old sources leaked")
|
||||
}
|
||||
|
||||
if err := c.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// clusapi.Cluster releases its handle even when CloseCluster fails, so the
|
||||
// collector drops the source too and a later Build can create a new one.
|
||||
func TestCloseSourcesDropsFailedSource(t *testing.T) {
|
||||
closeErr := errors.New("close failed")
|
||||
resources, groups := &resourceFixtureSource{}, &objectFixtureSource{closeErr: closeErr}
|
||||
c := New(&Config{CollectorsEnabled: []string{subCollectorResourceGroup}})
|
||||
c.resourceSource, c.resourceGroupSource = resources, groups
|
||||
|
||||
if err := c.Close(); !errors.Is(err, closeErr) {
|
||||
t.Fatalf("lost close error: %v", err)
|
||||
}
|
||||
|
||||
if c.resourceSource != nil || c.resourceGroupSource != nil {
|
||||
t.Fatal("source retained after Close")
|
||||
}
|
||||
|
||||
if err := c.Close(); err != nil || groups.closeCount != 1 {
|
||||
t.Fatalf("second close: err=%v count=%d", err, groups.closeCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceGroupExpiredBudget(t *testing.T) {
|
||||
if _, err := remainingBudget(time.Millisecond, time.Now().Add(-time.Second)); err == nil {
|
||||
t.Fatal("expired budget accepted")
|
||||
}
|
||||
|
||||
budget, err := remainingBudget(0, time.Now().Add(-time.Second))
|
||||
if err != nil || budget != 0 {
|
||||
t.Fatalf("unlimited budget = %v, %v", budget, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceGroupLargeClusterGather(t *testing.T) {
|
||||
groups := make([]clusapi.Object, 64)
|
||||
for index := range groups {
|
||||
groups[index] = resourceGroupFixture()
|
||||
groups[index].Name = fmt.Sprintf("group-%d", index)
|
||||
}
|
||||
|
||||
families, err := gatherResourceGroups(t, groups, nil, osversion.LTSC2022)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for name, family := range families {
|
||||
want := len(groups)
|
||||
if name == "windows_mscluster_resourcegroup_owner_node" {
|
||||
want *= 2
|
||||
}
|
||||
|
||||
if len(family.GetMetric()) != want {
|
||||
t.Errorf("%s samples = %d, want %d", name, len(family.GetMetric()), want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkResourceGroupPublication(b *testing.B) {
|
||||
c := New(&Config{CollectorsEnabled: []string{subCollectorResourceGroup}})
|
||||
c.buildResourceGroupDescriptors()
|
||||
|
||||
groups := []clusapi.Object{resourceGroupFixture()}
|
||||
ch := make(chan prometheus.Metric, 15)
|
||||
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
if err := c.publishResourceGroups(ch, groups, []string{"nodeA", "nodeB"}, osversion.LTSC2022, nil); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
for range 15 {
|
||||
<-ch
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
// 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 mscluster
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/headers/clusapi"
|
||||
"github.com/prometheus-community/windows_exporter/internal/mi"
|
||||
"github.com/prometheus-community/windows_exporter/internal/osversion"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const resourceGroupParityQuery = "SELECT Name,AutoFailbackType,Characteristics,ColdStartSetting,DefaultOwner,FailbackWindowEnd,FailbackWindowStart,FailoverPeriod,FailoverThreshold,Flags,GroupType,OwnerNode,Priority,ResiliencyPeriod,State FROM MSCluster_ResourceGroup"
|
||||
|
||||
// TestResourceGroupNativeWMIParity compares gathered metric families from
|
||||
// ClusAPI with the same publication fed by MSCluster_ResourceGroup.
|
||||
func TestResourceGroupNativeWMIParity(t *testing.T) {
|
||||
native, session, _ := resourceComparisonSources(t)
|
||||
|
||||
query, err := mi.NewQuery(resourceGroupParityQuery)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var rows []msClusterResourceGroup
|
||||
|
||||
start := time.Now()
|
||||
|
||||
if err := session.Query(&rows, mi.NamespaceRootMSCluster, query, time.Minute); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Logf("WMI resource group query duration: %s", time.Since(start))
|
||||
|
||||
// A configured cluster always has the core cluster group.
|
||||
if len(rows) == 0 {
|
||||
t.Fatal("cluster fixture has no resource groups")
|
||||
}
|
||||
|
||||
expected := make([]clusapi.Object, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
expected = append(expected, clusapi.Object{Name: row.Name, OwnerNode: row.OwnerNode, OwnerNodeValid: true, Values: map[string]uint32{
|
||||
"AutoFailbackType": uint32(row.AutoFailbackType),
|
||||
"Characteristics": uint32(row.Characteristics),
|
||||
"ColdStartSetting": uint32(row.ColdStartSetting),
|
||||
"DefaultOwner": uint32(row.DefaultOwner),
|
||||
"FailbackWindowEnd": uint32(int32(row.FailbackWindowEnd)),
|
||||
"FailbackWindowStart": uint32(int32(row.FailbackWindowStart)),
|
||||
"FailoverPeriod": uint32(row.FailoverPeriod),
|
||||
"FailoverThreshold": uint32(row.FailoverThreshold),
|
||||
"Flags": uint32(row.Flags),
|
||||
"GroupType": uint32(row.GroupType),
|
||||
"Priority": uint32(row.Priority),
|
||||
"ResiliencyPeriod": uint32(row.ResiliencyPeriod),
|
||||
"State": uint32(row.State),
|
||||
}})
|
||||
}
|
||||
|
||||
start = time.Now()
|
||||
|
||||
groups, err := native.Groups(time.Now().Add(time.Minute))
|
||||
if err != nil {
|
||||
for _, group := range groups {
|
||||
t.Logf("native group %q DWORD properties: %v", group.Name, slices.Sorted(maps.Keys(group.Values)))
|
||||
}
|
||||
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Logf("ClusAPI resource group query duration: %s, groups: %d", time.Since(start), len(groups))
|
||||
|
||||
nodeNames := wmiNodeNames(t, session)
|
||||
|
||||
gotFamilies, err := gatherResourceGroups(t, groups, nil, osversion.Build(), nodeNames...)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
wantFamilies, err := gatherResourceGroups(t, expected, nil, osversion.Build(), nodeNames...)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(gotFamilies) != len(wantFamilies) {
|
||||
t.Errorf("families = %d, want %d", len(gotFamilies), len(wantFamilies))
|
||||
}
|
||||
|
||||
for name, want := range wantFamilies {
|
||||
if !proto.Equal(gotFamilies[name], want) {
|
||||
t.Errorf("native/WMI metric mismatch: %s\nnative: %v\nWMI: %v", name, gotFamilies[name], want)
|
||||
}
|
||||
}
|
||||
|
||||
if t.Failed() {
|
||||
for _, group := range groups {
|
||||
t.Logf("native group %q owner %q DWORD properties: %v", group.Name, group.OwnerNode, group.Values)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func wmiNodeNames(tb testing.TB, session *mi.Session) []string {
|
||||
tb.Helper()
|
||||
|
||||
nodeQuery, err := mi.NewQuery("SELECT Name FROM MSCluster_Node")
|
||||
if err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
|
||||
var nodes []struct {
|
||||
Name string `mi:"Name"`
|
||||
}
|
||||
if err := session.Query(&nodes, mi.NamespaceRootMSCluster, nodeQuery, time.Minute); err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
|
||||
nodeNames := make([]string, 0, len(nodes))
|
||||
for _, node := range nodes {
|
||||
nodeNames = append(nodeNames, node.Name)
|
||||
}
|
||||
|
||||
if len(nodeNames) == 0 {
|
||||
tb.Fatal("cluster fixture has no nodes")
|
||||
}
|
||||
|
||||
return nodeNames
|
||||
}
|
||||
|
||||
func BenchmarkResourceGroupSources(b *testing.B) {
|
||||
native, session, _ := resourceComparisonSources(b)
|
||||
|
||||
query, err := mi.NewQuery(resourceGroupParityQuery)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
b.Run("WMI", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
var rows []msClusterResourceGroup
|
||||
if err := session.Query(&rows, mi.NamespaceRootMSCluster, query, time.Minute); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("ClusAPI", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
if _, err := native.Groups(time.Now().Add(time.Minute)); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -22,22 +22,30 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/mi"
|
||||
"github.com/prometheus-community/windows_exporter/internal/headers/clusapi"
|
||||
"github.com/prometheus-community/windows_exporter/internal/types"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const nameSharedVolumes = Name + "_shared_volumes"
|
||||
|
||||
type sharedVolumesSource interface {
|
||||
DiskPartitions(deadline time.Time) ([]clusapi.Partition, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
type collectorSharedVolumes struct {
|
||||
sharedVolumesMIQuery mi.Query
|
||||
sharedVolumesSource sharedVolumesSource
|
||||
|
||||
sharedVolumesInfo *prometheus.Desc
|
||||
sharedVolumesTotalSize *prometheus.Desc
|
||||
sharedVolumesFreeSpace *prometheus.Desc
|
||||
}
|
||||
|
||||
// msClusterDiskPartition represents the MSCluster_DiskPartition WMI class
|
||||
// msClusterDiskPartition represents the MSCluster_DiskPartition WMI class. The
|
||||
// collector reads ClusAPI; the parity test compares against this WMI model.
|
||||
// TotalSize and FreeSpace are megabytes.
|
||||
type msClusterDiskPartition struct {
|
||||
Name string `mi:"Name"`
|
||||
Path string `mi:"Path"`
|
||||
@@ -48,13 +56,18 @@ type msClusterDiskPartition struct {
|
||||
}
|
||||
|
||||
func (c *Collector) buildSharedVolumes() error {
|
||||
sharedVolumesMIQuery, err := mi.NewQuery("SELECT Name, Path, TotalSize, FreeSpace, VolumeLabel, VolumeGuid FROM MSCluster_DiskPartition")
|
||||
source, err := clusapi.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create WMI query: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
c.sharedVolumesMIQuery = sharedVolumesMIQuery
|
||||
c.sharedVolumesSource = source
|
||||
c.buildSharedVolumesDescriptors()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Collector) buildSharedVolumesDescriptors() {
|
||||
c.sharedVolumesInfo = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(types.Namespace, nameSharedVolumes, "info"),
|
||||
"Cluster Shared Volumes information (value is always 1)",
|
||||
@@ -75,49 +88,56 @@ func (c *Collector) buildSharedVolumes() error {
|
||||
[]string{"name", "volume_guid"},
|
||||
nil,
|
||||
)
|
||||
|
||||
var dst []msClusterDiskPartition
|
||||
if err := c.miSession.Query(&dst, mi.NamespaceRootMSCluster, c.sharedVolumesMIQuery, 0); err != nil {
|
||||
return fmt.Errorf("WMI query failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Collector) collectSharedVolumes(ch chan<- prometheus.Metric, maxScrapeDuration time.Duration) error {
|
||||
var dst []msClusterDiskPartition
|
||||
if err := c.miSession.Query(&dst, mi.NamespaceRootMSCluster, c.sharedVolumesMIQuery, maxScrapeDuration); err != nil {
|
||||
return fmt.Errorf("WMI query failed: %w", err)
|
||||
if c.sharedVolumesSource == nil {
|
||||
return errNotBuilt(subCollectorSharedVolumes)
|
||||
}
|
||||
|
||||
for _, partition := range dst {
|
||||
volume := strings.TrimRight(partition.Volume, " ")
|
||||
partitions, resultErr := callSource(maxScrapeDuration, c.sharedVolumesSource.DiskPartitions)
|
||||
|
||||
return c.publishSharedVolumes(ch, partitions, resultErr)
|
||||
}
|
||||
|
||||
func (c *Collector) publishSharedVolumes(ch chan<- prometheus.Metric, partitions []clusapi.Partition, resultErr error) error {
|
||||
for _, partition := range partitions {
|
||||
volume := strings.TrimRight(partition.VolumeLabel, " ")
|
||||
volumeGUID := formatVolumeGUID(partition.VolumeGUID)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.sharedVolumesInfo,
|
||||
prometheus.GaugeValue,
|
||||
1.0,
|
||||
volume,
|
||||
partition.Path,
|
||||
partition.VolumeGuid,
|
||||
partition.DeviceName,
|
||||
volumeGUID,
|
||||
)
|
||||
|
||||
// MSCluster_DiskPartition reports whole megabytes; keep that resolution.
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.sharedVolumesTotalSize,
|
||||
prometheus.GaugeValue,
|
||||
float64(partition.TotalSize)*1024*1024, // Convert from KB to bytes
|
||||
float64(partition.TotalBytes>>20)*1024*1024,
|
||||
volume,
|
||||
partition.VolumeGuid,
|
||||
volumeGUID,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.sharedVolumesFreeSpace,
|
||||
prometheus.GaugeValue,
|
||||
float64(partition.FreeSpace)*1024*1024, // Convert from KB to bytes
|
||||
float64(partition.FreeBytes>>20)*1024*1024,
|
||||
volume,
|
||||
partition.VolumeGuid,
|
||||
volumeGUID,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
return resultErr
|
||||
}
|
||||
|
||||
// formatVolumeGUID matches MSCluster_DiskPartition.VolumeGuid: lower case,
|
||||
// without braces.
|
||||
func formatVolumeGUID(guid windows.GUID) string {
|
||||
return fmt.Sprintf("%08x-%04x-%04x-%x-%x",
|
||||
guid.Data1, guid.Data2, guid.Data3, guid.Data4[:2], guid.Data4[2:])
|
||||
}
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
// 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 mscluster
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"maps"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/headers/clusapi"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
type partitionFixtureSource struct {
|
||||
partitions []clusapi.Partition
|
||||
err error
|
||||
started chan struct{}
|
||||
release chan struct{}
|
||||
closeCount int
|
||||
}
|
||||
|
||||
func (s *partitionFixtureSource) DiskPartitions(time.Time) ([]clusapi.Partition, error) {
|
||||
if s.started != nil {
|
||||
close(s.started)
|
||||
<-s.release
|
||||
}
|
||||
|
||||
return s.partitions, s.err
|
||||
}
|
||||
|
||||
func (s *partitionFixtureSource) Close() error {
|
||||
s.closeCount++
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// partitionFixture matches the example output in #2301: 18128 MiB free.
|
||||
func partitionFixture() clusapi.Partition {
|
||||
return clusapi.Partition{
|
||||
Resource: "Cluster Virtual Disk (ClusterPerformanceHistory)",
|
||||
DeviceName: `C:\ClusterStorage\Volume1`,
|
||||
VolumeLabel: "ClusterPerformanceHistory ",
|
||||
VolumeGUID: windows.GUID{Data1: 0xd3dbada3, Data2: 0x448f, Data3: 0x4c4c, Data4: [8]byte{0xbe, 0x4d, 0x12, 0x2b, 0x7f, 0x29, 0x60, 0x07}},
|
||||
TotalBytes: 21474836480 + 4096,
|
||||
FreeBytes: 19008585728 + 1048575,
|
||||
}
|
||||
}
|
||||
|
||||
func gatherSharedVolumes(t *testing.T, partitions []clusapi.Partition, inputErr error) (map[string]*dto.MetricFamily, error) {
|
||||
t.Helper()
|
||||
|
||||
c := New(&Config{CollectorsEnabled: []string{subCollectorSharedVolumes}})
|
||||
c.buildSharedVolumesDescriptors()
|
||||
|
||||
return gatherPublished(t, func(ch chan<- prometheus.Metric) error {
|
||||
return c.publishSharedVolumes(ch, partitions, inputErr)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSharedVolumesNativeMetricContract(t *testing.T) {
|
||||
families, err := gatherSharedVolumes(t, []clusapi.Partition{partitionFixture()}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(families) != 3 {
|
||||
t.Fatalf("families = %d", len(families))
|
||||
}
|
||||
|
||||
labels := map[string]string{"name": "ClusterPerformanceHistory", "volume_guid": "d3dbada3-448f-4c4c-be4d-122b7f296007"}
|
||||
|
||||
for name, want := range map[string]float64{"total_bytes": 21474836480, "free_bytes": 1.9008585728e+10} {
|
||||
family := families["windows_mscluster_shared_volumes_"+name]
|
||||
if family.GetType() != dto.MetricType_GAUGE || len(family.GetMetric()) != 1 {
|
||||
t.Fatalf("invalid family %s", name)
|
||||
}
|
||||
|
||||
metric := family.GetMetric()[0]
|
||||
if metric.GetGauge().GetValue() != want {
|
||||
t.Errorf("%s = %v, want %v", name, metric.GetGauge().GetValue(), want)
|
||||
}
|
||||
|
||||
if !maps.Equal(metricLabels(metric), labels) {
|
||||
t.Errorf("%s labels = %v", name, metricLabels(metric))
|
||||
}
|
||||
}
|
||||
|
||||
info := families["windows_mscluster_shared_volumes_info"].GetMetric()[0]
|
||||
if info.GetGauge().GetValue() != 1 || !maps.Equal(metricLabels(info), map[string]string{"name": "ClusterPerformanceHistory", "path": `C:\ClusterStorage\Volume1`, "volume_guid": "d3dbada3-448f-4c4c-be4d-122b7f296007"}) {
|
||||
t.Fatalf("info = %v", metricLabels(info))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSharedVolumesPartialResults(t *testing.T) {
|
||||
cause := errors.New("resource failed")
|
||||
second := partitionFixture()
|
||||
second.VolumeLabel = "CSV02"
|
||||
second.VolumeGUID.Data1++
|
||||
|
||||
families, err := gatherSharedVolumes(t, []clusapi.Partition{partitionFixture(), second}, cause)
|
||||
if !errors.Is(err, cause) {
|
||||
t.Fatalf("lost cause: %v", err)
|
||||
}
|
||||
|
||||
for name, family := range families {
|
||||
if len(family.GetMetric()) != 2 {
|
||||
t.Errorf("%s samples = %d", name, len(family.GetMetric()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSharedVolumesCloseWaitsForCollection(t *testing.T) {
|
||||
source := &partitionFixtureSource{partitions: []clusapi.Partition{partitionFixture()}, started: make(chan struct{}), release: make(chan struct{})}
|
||||
c := New(&Config{CollectorsEnabled: []string{subCollectorSharedVolumes}})
|
||||
c.sharedVolumesSource = source
|
||||
c.buildSharedVolumesDescriptors()
|
||||
|
||||
collectDone := make(chan error, 1)
|
||||
go func() { collectDone <- c.Collect(make(chan prometheus.Metric, 100), time.Second) }()
|
||||
|
||||
<-source.started
|
||||
|
||||
closeDone := make(chan error, 1)
|
||||
go func() { closeDone <- c.Close() }()
|
||||
|
||||
select {
|
||||
case err := <-closeDone:
|
||||
t.Fatalf("closed during collection: %v", err)
|
||||
case <-time.After(10 * time.Millisecond):
|
||||
}
|
||||
|
||||
close(source.release)
|
||||
|
||||
if err := <-collectDone; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := <-closeDone; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := c.Close(); err != nil || source.closeCount != 1 {
|
||||
t.Fatalf("close count = %d, err = %v", source.closeCount, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSharedVolumesRepeatedBuildClosesOldSource(t *testing.T) {
|
||||
source := &partitionFixtureSource{}
|
||||
c := New(&Config{CollectorsEnabled: []string{"unsupported"}})
|
||||
c.sharedVolumesSource = source
|
||||
|
||||
if err := c.Build(slog.Default(), nil); err == nil {
|
||||
t.Fatal("accepted unsupported collector")
|
||||
}
|
||||
|
||||
if source.closeCount != 1 || c.sharedVolumesSource != nil {
|
||||
t.Fatal("old shared volumes source leaked")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSharedVolumesPublication(b *testing.B) {
|
||||
c := New(&Config{CollectorsEnabled: []string{subCollectorSharedVolumes}})
|
||||
c.buildSharedVolumesDescriptors()
|
||||
|
||||
partitions := []clusapi.Partition{partitionFixture()}
|
||||
ch := make(chan prometheus.Metric, 3)
|
||||
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
if err := c.publishSharedVolumes(ch, partitions, nil); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
for range 3 {
|
||||
<-ch
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
// 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 mscluster
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/headers/clusapi"
|
||||
"github.com/prometheus-community/windows_exporter/internal/mi"
|
||||
"golang.org/x/sys/windows"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const sharedVolumesParityQuery = "SELECT Name, Path, TotalSize, FreeSpace, VolumeLabel, VolumeGuid FROM MSCluster_DiskPartition"
|
||||
|
||||
// TestSharedVolumesNativeWMIParity compares gathered metric families from
|
||||
// CLUSCTL_RESOURCE_STORAGE_GET_DISK_INFO_EX with MSCluster_DiskPartition. The
|
||||
// CI cluster has no clustered disks, so there it compares empty sources.
|
||||
func TestSharedVolumesNativeWMIParity(t *testing.T) {
|
||||
native, session, _ := resourceComparisonSources(t)
|
||||
|
||||
query, err := mi.NewQuery(sharedVolumesParityQuery)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var rows []msClusterDiskPartition
|
||||
|
||||
start := time.Now()
|
||||
|
||||
if err := session.Query(&rows, mi.NamespaceRootMSCluster, query, time.Minute); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Logf("WMI disk partition query duration: %s, partitions: %d", time.Since(start), len(rows))
|
||||
|
||||
expected := make([]clusapi.Partition, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
guid, err := windows.GUIDFromString("{" + strings.Trim(row.VolumeGuid, "{}") + "}")
|
||||
if err != nil {
|
||||
t.Fatalf("WMI VolumeGuid %q: %v", row.VolumeGuid, err)
|
||||
}
|
||||
|
||||
expected = append(expected, clusapi.Partition{
|
||||
DeviceName: row.Path,
|
||||
VolumeLabel: row.Volume,
|
||||
VolumeGUID: guid,
|
||||
TotalBytes: row.TotalSize << 20,
|
||||
FreeBytes: row.FreeSpace << 20,
|
||||
})
|
||||
}
|
||||
|
||||
start = time.Now()
|
||||
|
||||
partitions, err := native.DiskPartitions(time.Now().Add(time.Minute))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
t.Logf("ClusAPI disk partition query duration: %s, partitions: %d", time.Since(start), len(partitions))
|
||||
|
||||
gotFamilies, err := gatherSharedVolumes(t, partitions, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
wantFamilies, err := gatherSharedVolumes(t, expected, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(gotFamilies) != len(wantFamilies) {
|
||||
t.Errorf("families = %d, want %d", len(gotFamilies), len(wantFamilies))
|
||||
}
|
||||
|
||||
for name, want := range wantFamilies {
|
||||
if !proto.Equal(gotFamilies[name], want) {
|
||||
t.Errorf("native/WMI metric mismatch: %s\nnative: %v\nWMI: %v", name, gotFamilies[name], want)
|
||||
}
|
||||
}
|
||||
|
||||
if t.Failed() {
|
||||
for _, row := range rows {
|
||||
t.Logf("WMI partition: name=%q path=%q label=%q guid=%q", row.Name, row.Path, row.Volume, row.VolumeGuid)
|
||||
}
|
||||
|
||||
for _, partition := range partitions {
|
||||
t.Logf("native partition: %+v", partition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSharedVolumesSources(b *testing.B) {
|
||||
native, session, _ := resourceComparisonSources(b)
|
||||
|
||||
query, err := mi.NewQuery(sharedVolumesParityQuery)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
b.Run("WMI", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
var rows []msClusterDiskPartition
|
||||
if err := session.Query(&rows, mi.NamespaceRootMSCluster, query, time.Minute); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("ClusAPI", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
if _, err := native.DiskPartitions(time.Now().Add(time.Minute)); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/mscluster"
|
||||
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
func BenchmarkCollector(b *testing.B) {
|
||||
@@ -29,5 +30,18 @@ func BenchmarkCollector(b *testing.B) {
|
||||
}
|
||||
|
||||
func TestCollector(t *testing.T) {
|
||||
testutils.TestCollector(t, mscluster.New, nil)
|
||||
metrics := testutils.TestCollector(t, mscluster.New, nil)
|
||||
|
||||
// Resource fixtures from the CI cluster setup cover each resource state.
|
||||
for name, state := range map[string]float64{
|
||||
"CI IP Address": 2, // Online
|
||||
"CI Generic Service": 2, // Online
|
||||
"CI Offline IP Address": 3, // Offline
|
||||
"CI Failed Service": 4, // Failed
|
||||
} {
|
||||
metric := testutils.RequireFixtureMetric(t, metrics, mscluster.Name, "windows_mscluster_resource_state", prometheus.Labels{"name": name})
|
||||
if metric != nil && metric.GetGauge().GetValue() != state {
|
||||
t.Errorf("resource %q state = %v, want %v", name, metric.GetGauge().GetValue(), state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,690 @@
|
||||
// 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 clusapi exposes read-only access to local failover cluster resources.
|
||||
package clusapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const (
|
||||
resourceGetCharacteristics = 0x01000005
|
||||
resourceGetFlags = 0x01000009
|
||||
resourceGetROCommonProperties = 0x01000055
|
||||
resourceGetCommonProperties = 0x01000059
|
||||
resourceGetType = 0x0100002d
|
||||
resourceGetClassInfo = 0x0100000d
|
||||
maxBufferSize = 64 << 20
|
||||
stateUnknown = ^uint32(0) // ClusterResourceStateUnknown
|
||||
propertyListBufferSize = 4 << 10
|
||||
typeBufferSize = 512
|
||||
)
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var (
|
||||
dll = windows.NewLazySystemDLL("clusapi.dll")
|
||||
getClusterState = dll.NewProc("GetNodeClusterState")
|
||||
openCluster = dll.NewProc("OpenClusterEx")
|
||||
closeCluster = dll.NewProc("CloseCluster")
|
||||
openEnum = dll.NewProc("ClusterOpenEnum")
|
||||
nextEnum = dll.NewProc("ClusterEnum")
|
||||
closeEnum = dll.NewProc("ClusterCloseEnum")
|
||||
openResource = dll.NewProc("OpenClusterResourceEx")
|
||||
closeResource = dll.NewProc("CloseClusterResource")
|
||||
resourceControl = dll.NewProc("ClusterResourceControl")
|
||||
resourceState = dll.NewProc("GetClusterResourceState")
|
||||
)
|
||||
|
||||
// ErrBusy reports that an earlier call on the same Cluster has not returned
|
||||
// yet. ClusAPI RPCs cannot be cancelled; a call that outlived its scrape keeps
|
||||
// the cluster handle until the RPC returns, and later calls fail fast instead
|
||||
// of queuing behind it.
|
||||
var ErrBusy = errors.New("previous ClusAPI collection is still running")
|
||||
|
||||
// nativeAPI holds the ClusAPI calls that drive the handle lifecycle and the
|
||||
// enumeration. Tests substitute it to exercise these paths without a cluster.
|
||||
type nativeAPI struct {
|
||||
openCluster func() (uintptr, error)
|
||||
closeCluster func(cluster uintptr) error
|
||||
openEnum func(cluster uintptr, enumType uint32) (uintptr, error)
|
||||
closeEnum func(enum uintptr) error
|
||||
enumName func(enum uintptr, index, enumType uint32, deadline time.Time) (objectName, error)
|
||||
openResource func(cluster uintptr, name objectName) (uintptr, error)
|
||||
closeResource func(resource uintptr) error
|
||||
readResource func(resource uintptr, out *Resource, deadline time.Time) error
|
||||
}
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var clusAPI = nativeAPI{
|
||||
openCluster: func() (uintptr, error) {
|
||||
handle, _, err := openCluster.Call(0, windows.GENERIC_READ, 0)
|
||||
if handle == 0 {
|
||||
return 0, callError(err)
|
||||
}
|
||||
|
||||
return handle, nil
|
||||
},
|
||||
closeCluster: func(cluster uintptr) error {
|
||||
if result, _, err := closeCluster.Call(cluster); result == 0 {
|
||||
return callError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
openEnum: func(cluster uintptr, enumType uint32) (uintptr, error) {
|
||||
enum, _, err := openEnum.Call(cluster, uintptr(enumType))
|
||||
if enum == 0 {
|
||||
return 0, callError(err)
|
||||
}
|
||||
|
||||
return enum, nil
|
||||
},
|
||||
closeEnum: func(enum uintptr) error {
|
||||
if status, _, _ := closeEnum.Call(enum); status != 0 {
|
||||
return windows.Errno(status)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
enumName: enumName,
|
||||
openResource: func(cluster uintptr, name objectName) (uintptr, error) {
|
||||
return openObject(openResource, cluster, name)
|
||||
},
|
||||
closeResource: func(resource uintptr) error {
|
||||
return closeObject(closeResource, resource)
|
||||
},
|
||||
readResource: readResource,
|
||||
}
|
||||
|
||||
// callError returns the last error of a failed handle or BOOL call. Go clears
|
||||
// the thread's last error before each call, so 0 means the API set none.
|
||||
func callError(err error) error {
|
||||
if errors.Is(err, windows.Errno(0)) {
|
||||
return errors.New("failed without setting a Windows error code")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// openObject calls one of the OpenCluster*Ex functions, which share a signature.
|
||||
func openObject(proc *windows.LazyProc, cluster uintptr, name objectName) (uintptr, error) {
|
||||
handle, _, err := proc.Call(cluster, uintptr(unsafe.Pointer(&name.units[0])), windows.GENERIC_READ, 0)
|
||||
if handle == 0 {
|
||||
return 0, callError(err)
|
||||
}
|
||||
|
||||
return handle, nil
|
||||
}
|
||||
|
||||
// closeObject calls one of the CloseCluster* functions, which return a BOOL.
|
||||
func closeObject(proc *windows.LazyProc, handle uintptr) error {
|
||||
if result, _, err := proc.Call(handle); result == 0 {
|
||||
return callError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cluster owns a cluster handle. Only one call runs at a time: while a call
|
||||
// runs it owns the handle, and Close leaves the handle to that call, which
|
||||
// closes it when its RPC returns. This keeps a hung RPC from blocking Close.
|
||||
type Cluster struct {
|
||||
api *nativeAPI
|
||||
|
||||
mu sync.Mutex // guards closed and running, and handle when not running
|
||||
handle uintptr
|
||||
closed bool
|
||||
running bool
|
||||
}
|
||||
|
||||
type Resource struct {
|
||||
Name string
|
||||
Type string
|
||||
OwnerGroup string
|
||||
OwnerNode string
|
||||
IdentityValid bool
|
||||
Values map[string]uint32
|
||||
}
|
||||
|
||||
// Open checks that the local node is a configured cluster member. A missing
|
||||
// clusapi.dll or export reports errors.ErrUnsupported, like a node that is not
|
||||
// part of a cluster.
|
||||
func Open() (*Cluster, error) {
|
||||
for _, proc := range []*windows.LazyProc{getClusterState, openCluster, closeCluster, openEnum, nextEnum, closeEnum, openResource, closeResource, resourceControl, resourceState} {
|
||||
if err := proc.Find(); err != nil {
|
||||
return nil, fmt.Errorf("load ClusAPI: %w: %w", errors.ErrUnsupported, err)
|
||||
}
|
||||
}
|
||||
|
||||
var state uint32
|
||||
|
||||
status, _, _ := getClusterState.Call(0, uintptr(unsafe.Pointer(&state)))
|
||||
if status != 0 {
|
||||
return nil, fmt.Errorf("GetNodeClusterState: %w", windows.Errno(status))
|
||||
}
|
||||
// Installed and configured are separate flags. A configured but stopped
|
||||
// service remains an operational failure rather than unsupported.
|
||||
if state&3 != 3 {
|
||||
return nil, fmt.Errorf("local failover cluster is not configured: %w", errors.ErrUnsupported)
|
||||
}
|
||||
|
||||
// Defer the potentially blocking RPC until collection. A transient service
|
||||
// outage must not turn resource initialization into an exporter startup failure.
|
||||
return &Cluster{api: &clusAPI}, nil
|
||||
}
|
||||
|
||||
func (c *Cluster) native() *nativeAPI {
|
||||
if c.api == nil {
|
||||
return &clusAPI
|
||||
}
|
||||
|
||||
return c.api
|
||||
}
|
||||
|
||||
// Close releases the cluster handle. A handle whose CloseCluster fails is
|
||||
// dropped anyway, so a later Build starts from a clean state. If a call is
|
||||
// still running, it closes the handle when it returns.
|
||||
func (c *Cluster) Close() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.closed = true
|
||||
|
||||
if c.running || c.handle == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
handle := c.handle
|
||||
c.handle = 0
|
||||
|
||||
if err := c.native().closeCluster(handle); err != nil {
|
||||
return fmt.Errorf("CloseCluster: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// begin marks a call as running. A call that is still running makes it fail
|
||||
// with ErrBusy. A successful begin must be paired with end.
|
||||
func (c *Cluster) begin(deadline time.Time) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
switch {
|
||||
case c.closed:
|
||||
return errors.New("cluster is closed")
|
||||
case c.running:
|
||||
return ErrBusy
|
||||
}
|
||||
|
||||
if err := checkDeadline(deadline); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.running = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// open opens the cluster handle on first use. The running call owns the
|
||||
// handle, so the RPC runs without the lock.
|
||||
func (c *Cluster) open() error {
|
||||
if c.handle != 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
handle, err := c.native().openCluster()
|
||||
if err != nil {
|
||||
return fmt.Errorf("OpenClusterEx: %w", err)
|
||||
}
|
||||
|
||||
c.handle = handle
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// end finishes a call started by begin. If Close ran meanwhile, the handle is
|
||||
// released here and the CloseCluster error is returned.
|
||||
func (c *Cluster) end() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.running = false
|
||||
|
||||
if !c.closed || c.handle == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
handle := c.handle
|
||||
c.handle = 0
|
||||
|
||||
if err := c.native().closeCluster(handle); err != nil {
|
||||
return fmt.Errorf("CloseCluster: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// resetHandle drops a handle whose RPC binding failed, for example after a
|
||||
// cluster service restart, so the next call reopens the cluster. The caller
|
||||
// owns the handle through begin. The close error is irrelevant: the call
|
||||
// reports the error that made the handle unusable.
|
||||
func (c *Cluster) resetHandle() {
|
||||
_ = c.native().closeCluster(c.handle)
|
||||
c.handle = 0
|
||||
}
|
||||
|
||||
// errObjectDeleted marks an object that was deleted after ClusterOpenEnum took
|
||||
// its snapshot. The enumeration skips it without an error.
|
||||
var errObjectDeleted = errors.New("object was deleted during the enumeration")
|
||||
|
||||
// enumerate calls visit for every object name of a CLUSTER_ENUM type. Visit
|
||||
// errors are joined per object; the walk stops at the deadline. The caller
|
||||
// owns the handle through begin.
|
||||
func (c *Cluster) enumerate(enumType uint32, kind string, deadline time.Time, visit func(name objectName) error) (resultErr error) {
|
||||
if err := checkDeadline(deadline); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
api := c.native()
|
||||
|
||||
enum, err := api.openEnum(c.handle, enumType)
|
||||
if err != nil {
|
||||
c.resetHandle()
|
||||
|
||||
return fmt.Errorf("ClusterOpenEnum: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := api.closeEnum(enum); err != nil {
|
||||
resultErr = errors.Join(resultErr, fmt.Errorf("ClusterCloseEnum: %w", err))
|
||||
}
|
||||
}()
|
||||
|
||||
for index := uint32(0); ; index++ {
|
||||
name, err := api.enumName(enum, index, enumType, deadline)
|
||||
if errors.Is(err, windows.ERROR_NO_MORE_ITEMS) {
|
||||
return resultErr
|
||||
}
|
||||
|
||||
if _, ok := errors.AsType[invalidEnumEntryError](err); ok {
|
||||
// One malformed entry must not hide the objects after it.
|
||||
resultErr = errors.Join(resultErr, fmt.Errorf("ClusterEnum index %d: %w", index, err))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return errors.Join(resultErr, fmt.Errorf("ClusterEnum: %w", err))
|
||||
}
|
||||
|
||||
err = visit(name)
|
||||
if errors.Is(err, errObjectDeleted) {
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
resultErr = errors.Join(resultErr, fmt.Errorf("%s %q: %w", kind, name.name, err))
|
||||
}
|
||||
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return resultErr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resources retains successful resource/property results alongside joined failures.
|
||||
// Windows ClusAPI RPCs cannot be cancelled. Deadline checks prevent starting
|
||||
// another RPC after the supplied budget; a call that still outlives it makes
|
||||
// later calls fail with ErrBusy until it returns.
|
||||
func (c *Cluster) Resources(deadline time.Time) (_ []Resource, resultErr error) {
|
||||
if err := c.begin(deadline); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { resultErr = errors.Join(resultErr, c.end()) }()
|
||||
|
||||
if err := c.open(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
api := c.native()
|
||||
|
||||
var resources []Resource
|
||||
|
||||
err := c.enumerate(enumResource, "resource", deadline, func(name objectName) error {
|
||||
if err := checkDeadline(deadline); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
handle, err := api.openResource(c.handle, name)
|
||||
if errors.Is(err, windows.ERROR_RESOURCE_NOT_FOUND) {
|
||||
return errObjectDeleted
|
||||
}
|
||||
|
||||
resource := Resource{Name: name.name, Values: make(map[string]uint32)}
|
||||
|
||||
if err != nil {
|
||||
resources = append(resources, resource)
|
||||
|
||||
return fmt.Errorf("OpenClusterResourceEx: %w", err)
|
||||
}
|
||||
|
||||
err = api.readResource(handle, &resource, deadline)
|
||||
if closeErr := api.closeResource(handle); closeErr != nil {
|
||||
err = errors.Join(err, fmt.Errorf("CloseClusterResource: %w", closeErr))
|
||||
}
|
||||
|
||||
resources = append(resources, resource)
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return resources, err
|
||||
}
|
||||
|
||||
// invalidEnumEntryError reports a ClusterEnum entry that violates the API
|
||||
// contract. Enumeration continues with the next index.
|
||||
type invalidEnumEntryError string
|
||||
|
||||
func (e invalidEnumEntryError) Error() string { return string(e) }
|
||||
|
||||
// objectName keeps the NUL-terminated native name for opening the resource
|
||||
// next to the decoded label value.
|
||||
type objectName struct {
|
||||
units []uint16
|
||||
name string
|
||||
}
|
||||
|
||||
func enumName(enum uintptr, index, enumType uint32, deadline time.Time) (objectName, error) {
|
||||
buffer := make([]uint16, 256)
|
||||
|
||||
for range 16 {
|
||||
if err := checkDeadline(deadline); err != nil {
|
||||
return objectName{}, err
|
||||
}
|
||||
|
||||
length := uint32(len(buffer))
|
||||
|
||||
var objectType uint32
|
||||
|
||||
status, _, _ := nextEnum.Call(enum, uintptr(index), uintptr(unsafe.Pointer(&objectType)), uintptr(unsafe.Pointer(&buffer[0])), uintptr(unsafe.Pointer(&length)))
|
||||
if windows.Errno(status) == windows.ERROR_MORE_DATA {
|
||||
if length >= maxBufferSize/2 || int(length)+1 <= len(buffer) {
|
||||
return objectName{}, errors.New("invalid ClusterEnum buffer size")
|
||||
}
|
||||
|
||||
buffer = make([]uint16, int(length)+1)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if status != 0 {
|
||||
return objectName{}, windows.Errno(status)
|
||||
}
|
||||
|
||||
if objectType != enumType || length >= uint32(len(buffer)) {
|
||||
return objectName{}, invalidEnumEntryError(fmt.Sprintf("invalid ClusterEnum entry: type %d, length %d", objectType, length))
|
||||
}
|
||||
|
||||
units := make([]uint16, length+1)
|
||||
copy(units, buffer[:length])
|
||||
|
||||
return objectName{units: units, name: decodeUnits(buffer[:length])}, nil
|
||||
}
|
||||
|
||||
return objectName{}, errors.New("ClusterEnum buffer did not stabilize")
|
||||
}
|
||||
|
||||
// readResource fills the properties of an open resource. Failed properties are
|
||||
// omitted; the identity is only valid once the state and owner are known.
|
||||
func readResource(handle uintptr, resource *Resource, deadline time.Time) error {
|
||||
var resultErr error
|
||||
|
||||
hasType := false
|
||||
|
||||
for _, code := range []uint32{resourceGetROCommonProperties, resourceGetCommonProperties} {
|
||||
data, err := resourceBuffer(handle, code, propertyListBufferSize, deadline)
|
||||
if err != nil {
|
||||
resultErr = errors.Join(resultErr, fmt.Errorf("ClusterResourceControl %#x: %w", code, err))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
properties, err := ParseProperties(data)
|
||||
if err != nil {
|
||||
resultErr = errors.Join(resultErr, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
for name, values := range properties {
|
||||
if len(values) != 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
if value, err := values[0].DWORD(); err == nil {
|
||||
resource.Values[name] = value
|
||||
}
|
||||
}
|
||||
|
||||
// The read-only common Type property is the same resource type name
|
||||
// that CLUSCTL_RESOURCE_GET_RESOURCE_TYPE returns.
|
||||
if values := properties["Type"]; len(values) == 1 {
|
||||
if value, err := values[0].String(); err == nil {
|
||||
resource.Type = value
|
||||
hasType = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, field := range []struct {
|
||||
name string
|
||||
code uint32
|
||||
}{{"Characteristics", resourceGetCharacteristics}, {"Flags", resourceGetFlags}} {
|
||||
data, err := resourceBuffer(handle, field.code, 4, deadline)
|
||||
switch {
|
||||
case err != nil:
|
||||
resultErr = errors.Join(resultErr, fmt.Errorf("%s: %w", field.name, err))
|
||||
case len(data) != 4:
|
||||
resultErr = errors.Join(resultErr, fmt.Errorf("%s: invalid DWORD size", field.name))
|
||||
default:
|
||||
resource.Values[field.name] = binary.LittleEndian.Uint32(data)
|
||||
}
|
||||
}
|
||||
|
||||
data, err := resourceBuffer(handle, resourceGetClassInfo, 8, deadline)
|
||||
switch {
|
||||
case err != nil:
|
||||
resultErr = errors.Join(resultErr, fmt.Errorf("class information: %w", err))
|
||||
case len(data) != 8:
|
||||
resultErr = errors.Join(resultErr, errors.New("invalid resource class information size"))
|
||||
default:
|
||||
resource.Values["ResourceClass"] = binary.LittleEndian.Uint32(data)
|
||||
resource.Values["Subclass"] = binary.LittleEndian.Uint32(data[4:])
|
||||
}
|
||||
|
||||
// The property lists normally carry the type; only ask separately without it.
|
||||
if !hasType {
|
||||
data, err = resourceBuffer(handle, resourceGetType, typeBufferSize, deadline)
|
||||
if err != nil {
|
||||
return errors.Join(resultErr, fmt.Errorf("resource type: %w", err))
|
||||
}
|
||||
|
||||
resource.Type = decodeString(data)
|
||||
}
|
||||
|
||||
state, owner, group, err := readResourceState(handle, deadline)
|
||||
if err != nil {
|
||||
return errors.Join(resultErr, fmt.Errorf("GetClusterResourceState: %w", err))
|
||||
}
|
||||
|
||||
resource.Values["State"] = state
|
||||
resource.OwnerNode = owner
|
||||
resource.OwnerGroup = group
|
||||
resource.IdentityValid = true
|
||||
|
||||
return resultErr
|
||||
}
|
||||
|
||||
func resourceBuffer(handle uintptr, code uint32, size int, deadline time.Time) ([]byte, error) {
|
||||
return controlBuffer(deadline, size, func(buffer []byte) (uint32, error) {
|
||||
var (
|
||||
size uint32
|
||||
pointer *byte
|
||||
)
|
||||
if len(buffer) != 0 {
|
||||
pointer = &buffer[0]
|
||||
}
|
||||
|
||||
status, _, _ := resourceControl.Call(handle, 0, uintptr(code), 0, 0, uintptr(unsafe.Pointer(pointer)), uintptr(len(buffer)), uintptr(unsafe.Pointer(&size)))
|
||||
if status != 0 {
|
||||
return size, windows.Errno(status)
|
||||
}
|
||||
|
||||
return size, nil
|
||||
})
|
||||
}
|
||||
|
||||
func readResourceState(handle uintptr, deadline time.Time) (uint32, string, string, error) {
|
||||
return stateBuffers(deadline, func(node, group []uint16) (uint32, uint32, uint32, error) {
|
||||
nodeLength, groupLength := uint32(len(node)), uint32(len(group))
|
||||
|
||||
state, _, err := resourceState.Call(handle, uintptr(unsafe.Pointer(&node[0])), uintptr(unsafe.Pointer(&nodeLength)), uintptr(unsafe.Pointer(&group[0])), uintptr(unsafe.Pointer(&groupLength)))
|
||||
// Only ERROR_MORE_DATA matters: the caller retries with larger buffers.
|
||||
if uint32(state) == stateUnknown && errors.Is(err, windows.ERROR_MORE_DATA) {
|
||||
return uint32(state), nodeLength, groupLength, err
|
||||
}
|
||||
|
||||
return uint32(state), nodeLength, groupLength, nil
|
||||
})
|
||||
}
|
||||
|
||||
func stateBuffers(deadline time.Time, call func([]uint16, []uint16) (uint32, uint32, uint32, error)) (uint32, string, string, error) {
|
||||
node, group := make([]uint16, 256), make([]uint16, 256)
|
||||
|
||||
for range 16 {
|
||||
if err := checkDeadline(deadline); err != nil {
|
||||
return 0, "", "", err
|
||||
}
|
||||
|
||||
state, nodeLength, groupLength, err := call(node, group)
|
||||
if errors.Is(err, windows.ERROR_MORE_DATA) {
|
||||
if nodeLength >= maxBufferSize/2 || groupLength >= maxBufferSize/2 {
|
||||
return 0, "", "", errors.New("invalid resource state name size")
|
||||
}
|
||||
|
||||
grew := false
|
||||
|
||||
if int(nodeLength)+1 > len(node) {
|
||||
node = make([]uint16, int(nodeLength)+1)
|
||||
grew = true
|
||||
}
|
||||
|
||||
if int(groupLength)+1 > len(group) {
|
||||
group = make([]uint16, int(groupLength)+1)
|
||||
grew = true
|
||||
}
|
||||
|
||||
if !grew {
|
||||
return 0, "", "", errors.New("resource state name buffer did not grow")
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// ClusterResourceStateUnknown is a state WMI published as 4294967295
|
||||
// together with the other properties. A failed query may leave the
|
||||
// names and lengths untouched; the buffers start zeroed, so decoding up
|
||||
// to the first NUL yields whatever names were returned.
|
||||
if state == stateUnknown {
|
||||
return state, decodeUnits(node), decodeUnits(group), nil
|
||||
}
|
||||
|
||||
if uint64(nodeLength) >= uint64(len(node)) || uint64(groupLength) >= uint64(len(group)) {
|
||||
return 0, "", "", errors.New("invalid resource state names")
|
||||
}
|
||||
|
||||
return state, decodeUnits(node[:nodeLength]), decodeUnits(group[:groupLength]), nil
|
||||
}
|
||||
|
||||
return 0, "", "", errors.New("resource state buffers did not stabilize")
|
||||
}
|
||||
|
||||
func checkDeadline(deadline time.Time) error {
|
||||
if !deadline.IsZero() && !time.Now().Before(deadline) {
|
||||
return context.DeadlineExceeded
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// controlBuffer starts with a buffer of the expected size, so most controls need
|
||||
// one RPC. ERROR_MORE_DATA still grows the buffer to the reported size; size 0
|
||||
// starts with a NULL size probe.
|
||||
func controlBuffer(deadline time.Time, size int, call func([]byte) (uint32, error)) ([]byte, error) {
|
||||
var buffer []byte
|
||||
if size > 0 {
|
||||
buffer = make([]byte, size)
|
||||
}
|
||||
|
||||
for range 16 {
|
||||
if err := checkDeadline(deadline); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
size, err := call(buffer)
|
||||
if errors.Is(err, windows.ERROR_MORE_DATA) {
|
||||
if size > maxBufferSize || int(size) <= len(buffer) {
|
||||
return nil, errors.New("invalid ClusterResourceControl buffer size")
|
||||
}
|
||||
|
||||
buffer = make([]byte, size)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// NULL/zero-size probes may succeed with the required buffer size.
|
||||
if len(buffer) == 0 && size != 0 {
|
||||
if size > maxBufferSize {
|
||||
return nil, errors.New("invalid ClusterResourceControl buffer size")
|
||||
}
|
||||
|
||||
buffer = make([]byte, size)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if uint64(size) > uint64(len(buffer)) {
|
||||
return nil, errors.New("ClusterResourceControl returned an oversized buffer")
|
||||
}
|
||||
|
||||
return buffer[:size], nil
|
||||
}
|
||||
|
||||
return nil, errors.New("ClusterResourceControl buffer did not stabilize")
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
// 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 clusapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func TestControlBuffer(t *testing.T) {
|
||||
calls := 0
|
||||
|
||||
data, err := controlBuffer(time.Time{}, 0, func(buffer []byte) (uint32, error) {
|
||||
calls++
|
||||
switch calls {
|
||||
case 1:
|
||||
if len(buffer) != 0 {
|
||||
t.Fatal("first query must not supply buffer")
|
||||
}
|
||||
|
||||
return 4, windows.ERROR_MORE_DATA
|
||||
case 2:
|
||||
if len(buffer) != 4 {
|
||||
t.Fatalf("buffer = %d", len(buffer))
|
||||
}
|
||||
|
||||
return 8, windows.ERROR_MORE_DATA
|
||||
default:
|
||||
if len(buffer) != 8 {
|
||||
t.Fatalf("buffer = %d", len(buffer))
|
||||
}
|
||||
|
||||
copy(buffer, []byte{1, 2, 3, 4})
|
||||
|
||||
return 4, nil
|
||||
}
|
||||
})
|
||||
if err != nil || len(data) != 4 || data[3] != 4 || calls != 3 {
|
||||
t.Fatalf("data = %v, calls = %d, err = %v", data, calls, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestControlBufferPresized(t *testing.T) {
|
||||
calls := 0
|
||||
|
||||
data, err := controlBuffer(time.Time{}, 8, func(buffer []byte) (uint32, error) {
|
||||
calls++
|
||||
|
||||
if len(buffer) != 8 {
|
||||
t.Fatalf("buffer = %d", len(buffer))
|
||||
}
|
||||
|
||||
copy(buffer, []byte{1, 2, 3, 4, 5, 6, 7, 8})
|
||||
|
||||
return 8, nil
|
||||
})
|
||||
if err != nil || calls != 1 || len(data) != 8 || data[7] != 8 {
|
||||
t.Fatalf("data = %v, calls = %d, err = %v", data, calls, err)
|
||||
}
|
||||
|
||||
calls = 0
|
||||
|
||||
data, err = controlBuffer(time.Time{}, 4, func(buffer []byte) (uint32, error) {
|
||||
calls++
|
||||
if calls == 1 {
|
||||
return 6, windows.ERROR_MORE_DATA
|
||||
}
|
||||
|
||||
if len(buffer) != 6 {
|
||||
t.Fatalf("buffer = %d", len(buffer))
|
||||
}
|
||||
|
||||
return 6, nil
|
||||
})
|
||||
if err != nil || calls != 2 || len(data) != 6 {
|
||||
t.Fatalf("growth: data = %v, calls = %d, err = %v", data, calls, err)
|
||||
}
|
||||
|
||||
// A pre-sized buffer that is already large enough must not be shrunk or retried.
|
||||
_, err = controlBuffer(time.Time{}, 8, func([]byte) (uint32, error) { return 4, windows.ERROR_MORE_DATA })
|
||||
if err == nil {
|
||||
t.Fatal("accepted ERROR_MORE_DATA without growth")
|
||||
}
|
||||
}
|
||||
|
||||
func TestControlBufferInvalidSizes(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
size uint32
|
||||
err error
|
||||
}{
|
||||
{"zero_more_data", 0, windows.ERROR_MORE_DATA},
|
||||
{"oversized_more_data", maxBufferSize + 1, windows.ERROR_MORE_DATA},
|
||||
{"oversized_success", maxBufferSize + 1, nil},
|
||||
{"unrelated_error", 0, windows.ERROR_ACCESS_DENIED},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := controlBuffer(time.Time{}, 0, func([]byte) (uint32, error) { return tc.size, tc.err })
|
||||
if err == nil {
|
||||
t.Fatal("accepted invalid native response")
|
||||
}
|
||||
|
||||
if errors.Is(tc.err, windows.ERROR_ACCESS_DENIED) && !errors.Is(err, tc.err) {
|
||||
t.Fatalf("lost native error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestControlBufferDeadline(t *testing.T) {
|
||||
calls := 0
|
||||
deadline := time.Now().Add(time.Millisecond)
|
||||
|
||||
_, err := controlBuffer(deadline, 0, func([]byte) (uint32, error) {
|
||||
calls++
|
||||
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
|
||||
return 4, windows.ERROR_MORE_DATA
|
||||
})
|
||||
if !errors.Is(err, context.DeadlineExceeded) || calls != 1 {
|
||||
t.Fatalf("calls = %d, err = %v", calls, err)
|
||||
}
|
||||
|
||||
_, err = controlBuffer(time.Now().Add(-time.Second), 0, func([]byte) (uint32, error) {
|
||||
t.Fatal("started native call after deadline")
|
||||
|
||||
return 0, nil
|
||||
})
|
||||
if !errors.Is(err, context.DeadlineExceeded) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterClosed(t *testing.T) {
|
||||
c := &Cluster{}
|
||||
if err := c.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := c.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := c.Resources(time.Time{}); err == nil {
|
||||
t.Fatal("read closed cluster")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStateBuffersGrowthAndBitPatterns(t *testing.T) {
|
||||
calls := 0
|
||||
|
||||
state, node, group, err := stateBuffers(time.Time{}, func(node, group []uint16) (uint32, uint32, uint32, error) {
|
||||
calls++
|
||||
if calls == 1 {
|
||||
return ^uint32(0), 300, 400, windows.ERROR_MORE_DATA
|
||||
}
|
||||
|
||||
if len(node) != 301 || len(group) != 401 {
|
||||
t.Fatalf("node/group lengths = %d/%d", len(node), len(group))
|
||||
}
|
||||
|
||||
copy(node, []uint16{'N', 0})
|
||||
copy(group, []uint16{'G', 0})
|
||||
|
||||
return 0x80000000, 1, 1, nil
|
||||
})
|
||||
if err != nil || state != 0x80000000 || node != "N" || group != "G" || calls != 2 {
|
||||
t.Fatalf("state=%d node=%s group=%s calls=%d err=%v", state, node, group, calls, err)
|
||||
}
|
||||
}
|
||||
|
||||
// WMI published ClusterResourceStateUnknown with the other resource properties,
|
||||
// so the state is a value, not a failure. The names may be left untouched.
|
||||
func TestStateBuffersUnknownState(t *testing.T) {
|
||||
state, node, group, err := stateBuffers(time.Time{}, func(node, group []uint16) (uint32, uint32, uint32, error) {
|
||||
return stateUnknown, uint32(len(node)), uint32(len(group)), nil
|
||||
})
|
||||
if err != nil || state != stateUnknown || node != "" || group != "" {
|
||||
t.Fatalf("untouched: state=%d node=%q group=%q err=%v", state, node, group, err)
|
||||
}
|
||||
|
||||
state, node, group, err = stateBuffers(time.Time{}, func(node, group []uint16) (uint32, uint32, uint32, error) {
|
||||
copy(node, []uint16{'N', 0})
|
||||
copy(group, []uint16{'G', 0})
|
||||
|
||||
return stateUnknown, 1, 1, nil
|
||||
})
|
||||
if err != nil || state != stateUnknown || node != "N" || group != "G" {
|
||||
t.Fatalf("filled: state=%d node=%q group=%q err=%v", state, node, group, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStateBuffersInvalidNativeResponses(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
state uint32
|
||||
nodeSize uint32
|
||||
groupSize uint32
|
||||
err error
|
||||
}{
|
||||
{"more_data_without_growth", ^uint32(0), 1, 1, windows.ERROR_MORE_DATA},
|
||||
{"oversized_more_data", ^uint32(0), maxBufferSize, 0, windows.ERROR_MORE_DATA},
|
||||
{"oversized_success", 2, 256, 0, nil},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, _, _, err := stateBuffers(time.Time{}, func([]uint16, []uint16) (uint32, uint32, uint32, error) {
|
||||
return tc.state, tc.nodeSize, tc.groupSize, tc.err
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("accepted invalid native response")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestControlBufferSuccessfulSizeProbe(t *testing.T) {
|
||||
calls := 0
|
||||
|
||||
data, err := controlBuffer(time.Time{}, 0, func(buffer []byte) (uint32, error) {
|
||||
calls++
|
||||
if calls == 1 {
|
||||
return 4, nil
|
||||
}
|
||||
|
||||
copy(buffer, []byte{1, 2, 3, 4})
|
||||
|
||||
return 4, nil
|
||||
})
|
||||
if err != nil || calls != 2 || len(data) != 4 || data[3] != 4 {
|
||||
t.Fatalf("successful probe: data=%v calls=%d err=%v", data, calls, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnopenedClusterExpiredBudget(t *testing.T) {
|
||||
c := &Cluster{}
|
||||
if _, err := c.Resources(time.Now().Add(-time.Second)); !errors.Is(err, context.DeadlineExceeded) {
|
||||
t.Fatalf("expired budget: %v", err)
|
||||
}
|
||||
|
||||
if c.handle != 0 || c.closed {
|
||||
t.Fatal("expired scrape changed unopened cluster lifecycle")
|
||||
}
|
||||
|
||||
if err := c.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !c.closed {
|
||||
t.Fatal("unopened cluster was not closed")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
// 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 clusapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const (
|
||||
// CLUS_OBJECT_CLUSTER, used for CLUSCTL_CLUSTER_* control codes.
|
||||
objectCluster = 7
|
||||
|
||||
// CLUS_RESCLASS_STORAGE.
|
||||
resourceClassStorage = 1
|
||||
)
|
||||
|
||||
// GetClusterInformation, ClusterControl and GetClusterQuorumResource are
|
||||
// available since Windows Server 2008.
|
||||
//
|
||||
//nolint:gochecknoglobals
|
||||
var (
|
||||
clusterInformation = dll.NewProc("GetClusterInformation")
|
||||
clusterControl = dll.NewProc("ClusterControl")
|
||||
quorumResource = dll.NewProc("GetClusterQuorumResource")
|
||||
)
|
||||
|
||||
// Properties reads the cluster name, the 32-bit cluster common properties and
|
||||
// the quorum values (see readQuorum). A failed name read returns no object.
|
||||
func (c *Cluster) Properties(deadline time.Time) (_ Object, resultErr error) {
|
||||
for _, proc := range []*windows.LazyProc{openCluster, clusterInformation, clusterControl, quorumResource, openResource, closeResource, resourceControl} {
|
||||
if err := proc.Find(); err != nil {
|
||||
return Object{}, fmt.Errorf("load ClusAPI: %w: %w", errors.ErrUnsupported, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.begin(deadline); err != nil {
|
||||
return Object{}, err
|
||||
}
|
||||
defer func() { resultErr = errors.Join(resultErr, c.end()) }()
|
||||
|
||||
if err := c.open(); err != nil {
|
||||
return Object{}, err
|
||||
}
|
||||
|
||||
name, err := readClusterName(c.handle, deadline)
|
||||
if err != nil {
|
||||
// The name is the first RPC on the handle; a broken binding fails here.
|
||||
c.resetHandle()
|
||||
|
||||
return Object{}, fmt.Errorf("GetClusterInformation: %w", err)
|
||||
}
|
||||
|
||||
object := Object{Name: name, Values: make(map[string]uint32)}
|
||||
|
||||
if err := readCommonProperties(clusterControl, c.handle, objectCluster, object.Values, deadline); err != nil {
|
||||
resultErr = errors.Join(resultErr, err)
|
||||
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return object, resultErr
|
||||
}
|
||||
}
|
||||
|
||||
// The common property is spelled ClusterEnforcedAntiaffinity; WMI and the
|
||||
// metric use ClusterEnforcedAntiAffinity.
|
||||
if value, exists := object.Values["ClusterEnforcedAntiaffinity"]; exists {
|
||||
object.Values["ClusterEnforcedAntiAffinity"] = value
|
||||
}
|
||||
|
||||
if err := c.readQuorum(object.Values, deadline); err != nil {
|
||||
resultErr = errors.Join(resultErr, err)
|
||||
}
|
||||
|
||||
return object, resultErr
|
||||
}
|
||||
|
||||
// Quorum type values of MSCluster_Cluster.QuorumTypeValue.
|
||||
const (
|
||||
quorumTypeNode = 1
|
||||
quorumTypeFileShareWitness = 2
|
||||
quorumTypeStorage = 3
|
||||
)
|
||||
|
||||
// readQuorum derives QuorumLogFileSize and QuorumTypeValue from
|
||||
// GetClusterQuorumResource. The log size parameter carries the quorum type
|
||||
// (CLUS_NODE_MAJORITY_QUORUM, CLUS_HYBRID_QUORUM or CLUS_LEGACY_QUORUM). A
|
||||
// cluster without a witness has no quorum resource; otherwise the witness
|
||||
// resource class decides between a disk and a file share or cloud witness.
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/clusapi/nf-clusapi-setclusterquorumresource
|
||||
func (c *Cluster) readQuorum(values map[string]uint32, deadline time.Time) error {
|
||||
resource, logSize, err := readQuorumResource(c.handle, deadline)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetClusterQuorumResource: %w", err)
|
||||
}
|
||||
|
||||
values["QuorumLogFileSize"] = logSize
|
||||
|
||||
if resource == "" {
|
||||
values["QuorumTypeValue"] = quorumTypeNode
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
class, err := c.resourceClass(resource, deadline)
|
||||
if err != nil {
|
||||
return fmt.Errorf("quorum resource %q: %w", resource, err)
|
||||
}
|
||||
|
||||
if class == resourceClassStorage {
|
||||
values["QuorumTypeValue"] = quorumTypeStorage
|
||||
} else {
|
||||
values["QuorumTypeValue"] = quorumTypeFileShareWitness
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// resourceClass returns the CLUS_RESOURCE_CLASS of a resource.
|
||||
func (c *Cluster) resourceClass(name string, deadline time.Time) (_ uint32, resultErr error) {
|
||||
if err := checkDeadline(deadline); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
namePtr, err := windows.UTF16PtrFromString(name)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
handle, _, err := openResource.Call(c.handle, uintptr(unsafe.Pointer(namePtr)), windows.GENERIC_READ, 0)
|
||||
if handle == 0 {
|
||||
return 0, fmt.Errorf("OpenClusterResourceEx: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
result, _, err := closeResource.Call(handle)
|
||||
if result == 0 {
|
||||
resultErr = errors.Join(resultErr, fmt.Errorf("CloseClusterResource: %w", err))
|
||||
}
|
||||
}()
|
||||
|
||||
data, err := resourceBuffer(handle, resourceGetClassInfo, 8, deadline)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("class information: %w", err)
|
||||
}
|
||||
|
||||
if len(data) != 8 {
|
||||
return 0, errors.New("invalid resource class information size")
|
||||
}
|
||||
|
||||
return binary.LittleEndian.Uint32(data), nil
|
||||
}
|
||||
|
||||
// readClusterName calls GetClusterInformation without CLUSTERVERSIONINFO.
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/clusapi/nf-clusapi-getclusterinformation
|
||||
func readClusterName(handle uintptr, deadline time.Time) (string, error) {
|
||||
_, name, _, err := stateBuffers(deadline, func(name, _ []uint16) (uint32, uint32, uint32, error) {
|
||||
length := uint32(len(name))
|
||||
|
||||
status, _, _ := clusterInformation.Call(handle, uintptr(unsafe.Pointer(&name[0])), uintptr(unsafe.Pointer(&length)), 0)
|
||||
if status != 0 {
|
||||
return 0, length, 0, windows.Errno(status)
|
||||
}
|
||||
|
||||
return 0, length, 0, nil
|
||||
})
|
||||
|
||||
return name, err
|
||||
}
|
||||
|
||||
// readQuorumResource returns the quorum resource name and the
|
||||
// lpdwMaxQuorumLogSize value of GetClusterQuorumResource.
|
||||
func readQuorumResource(handle uintptr, deadline time.Time) (string, uint32, error) {
|
||||
return quorumBuffers(deadline, func(resource, device []uint16) (uint32, uint32, uint32, error) {
|
||||
var maxLogSize uint32
|
||||
|
||||
resourceLength, deviceLength := uint32(len(resource)), uint32(len(device))
|
||||
|
||||
status, _, _ := quorumResource.Call(handle, uintptr(unsafe.Pointer(&resource[0])), uintptr(unsafe.Pointer(&resourceLength)), uintptr(unsafe.Pointer(&device[0])), uintptr(unsafe.Pointer(&deviceLength)), uintptr(unsafe.Pointer(&maxLogSize)))
|
||||
if status != 0 {
|
||||
return 0, resourceLength, deviceLength, windows.Errno(status)
|
||||
}
|
||||
|
||||
return maxLogSize, resourceLength, deviceLength, nil
|
||||
})
|
||||
}
|
||||
|
||||
// quorumBuffers grows the name buffers on ERROR_MORE_DATA. On a node majority
|
||||
// cluster the CI fixture showed returned lengths that cover NUL characters, so
|
||||
// the resource name ends at its first NUL within the reported length.
|
||||
func quorumBuffers(deadline time.Time, call func([]uint16, []uint16) (uint32, uint32, uint32, error)) (string, uint32, error) {
|
||||
resource, device := make([]uint16, 256), make([]uint16, 256)
|
||||
|
||||
for range 16 {
|
||||
if err := checkDeadline(deadline); err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
logSize, resourceLength, deviceLength, err := call(resource, device)
|
||||
if errors.Is(err, windows.ERROR_MORE_DATA) {
|
||||
if resourceLength >= maxBufferSize/2 || deviceLength >= maxBufferSize/2 {
|
||||
return "", 0, errors.New("invalid quorum resource name size")
|
||||
}
|
||||
|
||||
grew := false
|
||||
|
||||
if int(resourceLength)+1 > len(resource) {
|
||||
resource = make([]uint16, int(resourceLength)+1)
|
||||
grew = true
|
||||
}
|
||||
|
||||
if int(deviceLength)+1 > len(device) {
|
||||
device = make([]uint16, int(deviceLength)+1)
|
||||
grew = true
|
||||
}
|
||||
|
||||
if !grew {
|
||||
return "", 0, errors.New("quorum resource name buffer did not grow")
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
if uint64(resourceLength) >= uint64(len(resource)) {
|
||||
return "", 0, fmt.Errorf("quorum resource name length %d exceeds buffer", resourceLength)
|
||||
}
|
||||
|
||||
units := resource[:resourceLength]
|
||||
for index, unit := range units {
|
||||
if unit == 0 {
|
||||
units = units[:index]
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return decodeUnits(units), logSize, nil
|
||||
}
|
||||
|
||||
return "", 0, errors.New("quorum resource buffers did not stabilize")
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
// 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 clusapi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func TestQuorumBuffersNodeMajority(t *testing.T) {
|
||||
// No quorum resource: the reported length can cover NUL characters.
|
||||
name, logSize, err := quorumBuffers(time.Time{}, func(resource, _ []uint16) (uint32, uint32, uint32, error) {
|
||||
clear(resource)
|
||||
|
||||
return 0, 4, 4, nil
|
||||
})
|
||||
if err != nil || name != "" || logSize != 0 {
|
||||
t.Fatalf("name=%q logSize=%d err=%v", name, logSize, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuorumBuffersWitness(t *testing.T) {
|
||||
calls := 0
|
||||
|
||||
name, logSize, err := quorumBuffers(time.Time{}, func(resource, device []uint16) (uint32, uint32, uint32, error) {
|
||||
calls++
|
||||
if calls == 1 {
|
||||
return 0, 300, 10, windows.ERROR_MORE_DATA
|
||||
}
|
||||
|
||||
if len(resource) != 301 || len(device) != 256 {
|
||||
t.Fatalf("resource/device lengths = %d/%d", len(resource), len(device))
|
||||
}
|
||||
|
||||
copy(resource, []uint16{'F', 'S', 'W', 0})
|
||||
|
||||
return 0x400, 3, 0, nil
|
||||
})
|
||||
if err != nil || name != "FSW" || logSize != 0x400 || calls != 2 {
|
||||
t.Fatalf("name=%q logSize=%#x calls=%d err=%v", name, logSize, calls, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuorumBuffersInvalid(t *testing.T) {
|
||||
for name, call := range map[string]func([]uint16, []uint16) (uint32, uint32, uint32, error){
|
||||
"oversized_success": func([]uint16, []uint16) (uint32, uint32, uint32, error) { return 0, 256, 0, nil },
|
||||
"no_growth": func([]uint16, []uint16) (uint32, uint32, uint32, error) { return 0, 1, 1, windows.ERROR_MORE_DATA },
|
||||
"oversized_growth": func([]uint16, []uint16) (uint32, uint32, uint32, error) {
|
||||
return 0, maxBufferSize, 0, windows.ERROR_MORE_DATA
|
||||
},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if _, _, err := quorumBuffers(time.Time{}, call); err == nil {
|
||||
t.Fatal("accepted invalid native response")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Names decode like WMI strings: invalid UTF-16 becomes U+FFFD.
|
||||
name, _, err := quorumBuffers(time.Time{}, func(resource, _ []uint16) (uint32, uint32, uint32, error) {
|
||||
resource[0] = 0xd800
|
||||
|
||||
return 0, 1, 0, nil
|
||||
})
|
||||
if err != nil || name != "�" {
|
||||
t.Fatalf("surrogate: name=%q err=%v", name, err)
|
||||
}
|
||||
|
||||
_, _, err = quorumBuffers(time.Time{}, func([]uint16, []uint16) (uint32, uint32, uint32, error) {
|
||||
return 0, 0, 0, windows.ERROR_ACCESS_DENIED
|
||||
})
|
||||
if !errors.Is(err, windows.ERROR_ACCESS_DENIED) {
|
||||
t.Fatalf("lost native error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterClosedProperties(t *testing.T) {
|
||||
c := &Cluster{}
|
||||
if err := c.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := c.Properties(time.Time{}); err == nil {
|
||||
t.Fatal("read closed cluster")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
// 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 clusapi
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const (
|
||||
resourceGetROPrivateProperties = 0x0100007d
|
||||
resourceGetPrivateProperties = 0x01000081
|
||||
resourceTypeGetROCommonProperties = 0x02000055
|
||||
resourceTypeGetCommonProperties = 0x02000059
|
||||
resourceTypeGetPrivateProperties = 0x02000081
|
||||
)
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var resourceTypeControl = dll.NewProc("ClusterResourceTypeControl")
|
||||
|
||||
// TestDumpPropertyLists logs raw property lists from a live cluster as hex, so
|
||||
// parser fixtures can be built from real cluster service output. Set
|
||||
// WINDOWS_EXPORTER_TEST_CLUSAPI_DUMP=1 on a cluster node to enable it.
|
||||
func TestDumpPropertyLists(t *testing.T) {
|
||||
if os.Getenv("WINDOWS_EXPORTER_TEST_CLUSAPI_DUMP") == "" {
|
||||
t.Skip("WINDOWS_EXPORTER_TEST_CLUSAPI_DUMP is not set")
|
||||
}
|
||||
|
||||
cluster, _, err := openCluster.Call(0, windows.GENERIC_READ, 0)
|
||||
if cluster == 0 {
|
||||
t.Fatalf("OpenClusterEx: %v", err)
|
||||
}
|
||||
|
||||
defer closeCluster.Call(cluster) //nolint:errcheck
|
||||
|
||||
deadline := time.Now().Add(time.Minute)
|
||||
|
||||
for _, code := range []uint32{objectCluster<<24 | ctlGetROCommonProperties, objectCluster<<24 | ctlGetCommonProperties} {
|
||||
data, err := controlBuffer(deadline, 0, func(buffer []byte) (uint32, error) {
|
||||
var (
|
||||
size uint32
|
||||
pointer *byte
|
||||
)
|
||||
if len(buffer) != 0 {
|
||||
pointer = &buffer[0]
|
||||
}
|
||||
|
||||
status, _, _ := clusterControl.Call(cluster, 0, uintptr(code), 0, 0, uintptr(unsafe.Pointer(pointer)), uintptr(len(buffer)), uintptr(unsafe.Pointer(&size)))
|
||||
if status != 0 {
|
||||
return size, windows.Errno(status)
|
||||
}
|
||||
|
||||
return size, nil
|
||||
})
|
||||
logPropertyList(t, fmt.Sprintf("cluster %#x", code), data, err)
|
||||
}
|
||||
|
||||
// Resource type lists carry EXPAND_SZ values such as DllName, which the
|
||||
// cluster service returns as an EXPAND_SZ and EXPANDED_SZ value pair.
|
||||
for _, typeName := range []string{"Generic Service", "IP Address"} {
|
||||
typePtr, err := windows.UTF16PtrFromString(typeName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, code := range []uint32{resourceTypeGetROCommonProperties, resourceTypeGetCommonProperties, resourceTypeGetPrivateProperties} {
|
||||
data, err := controlBuffer(deadline, 0, func(buffer []byte) (uint32, error) {
|
||||
var (
|
||||
size uint32
|
||||
pointer *byte
|
||||
)
|
||||
if len(buffer) != 0 {
|
||||
pointer = &buffer[0]
|
||||
}
|
||||
|
||||
status, _, _ := resourceTypeControl.Call(cluster, uintptr(unsafe.Pointer(typePtr)), 0, uintptr(code), 0, 0, uintptr(unsafe.Pointer(pointer)), uintptr(len(buffer)), uintptr(unsafe.Pointer(&size)))
|
||||
if status != 0 {
|
||||
return size, windows.Errno(status)
|
||||
}
|
||||
|
||||
return size, nil
|
||||
})
|
||||
logPropertyList(t, fmt.Sprintf("resource type %q %#x", typeName, code), data, err)
|
||||
}
|
||||
}
|
||||
|
||||
enum, _, err := openEnum.Call(cluster, 4)
|
||||
if enum == 0 {
|
||||
t.Fatalf("ClusterOpenEnum: %v", err)
|
||||
}
|
||||
|
||||
defer closeEnum.Call(enum) //nolint:errcheck
|
||||
|
||||
for index := uint32(0); ; index++ {
|
||||
name, err := enumName(enum, index, enumResource, deadline)
|
||||
if errors.Is(err, windows.ERROR_NO_MORE_ITEMS) {
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("ClusterEnum: %v", err)
|
||||
}
|
||||
|
||||
handle, _, err := openResource.Call(cluster, uintptr(unsafe.Pointer(&name.units[0])), windows.GENERIC_READ, 0)
|
||||
if handle == 0 {
|
||||
t.Errorf("OpenClusterResourceEx %q: %v", name.name, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
for _, code := range []uint32{resourceGetROCommonProperties, resourceGetCommonProperties, resourceGetROPrivateProperties, resourceGetPrivateProperties} {
|
||||
data, err := resourceBuffer(handle, code, 0, deadline)
|
||||
logPropertyList(t, fmt.Sprintf("resource %q %#x", name.name, code), data, err)
|
||||
}
|
||||
|
||||
data, err := resourceBuffer(handle, resourceGetType, 0, deadline)
|
||||
t.Logf("resource %q GET_RESOURCE_TYPE: %s err=%v", name.name, hex.EncodeToString(data), err)
|
||||
|
||||
closeResource.Call(handle) //nolint:errcheck
|
||||
}
|
||||
}
|
||||
|
||||
func logPropertyList(t *testing.T, label string, data []byte, err error) {
|
||||
t.Helper()
|
||||
|
||||
if err != nil {
|
||||
t.Logf("%s: %v", label, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
properties, parseErr := ParseProperties(data)
|
||||
|
||||
formats := make([]string, 0, len(properties))
|
||||
for name, values := range properties {
|
||||
valueFormats := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
valueFormats = append(valueFormats, fmt.Sprintf("%d/%d", value.Format, len(value.Data)))
|
||||
}
|
||||
|
||||
formats = append(formats, name+"="+strings.Join(valueFormats, "+"))
|
||||
}
|
||||
|
||||
slices.Sort(formats)
|
||||
|
||||
t.Logf("%s: parse error=%v formats=[%s]\n%s", label, parseErr, strings.Join(formats, " "), hex.EncodeToString(data))
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
// 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 clusapi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const fakeClusterHandle = 0x10
|
||||
|
||||
// fakeAPI is a nativeAPI over an in-memory resource list. Names listed in
|
||||
// deleted fail to open like resources removed after the enumeration snapshot.
|
||||
type fakeAPI struct {
|
||||
mu sync.Mutex
|
||||
opens int
|
||||
closes []uintptr
|
||||
openEnumErr []error
|
||||
closeErr error
|
||||
names []string
|
||||
deleted map[string]bool
|
||||
// blockEnum, if set, is closed when the first enumeration starts and
|
||||
// waited on before it continues.
|
||||
started chan struct{}
|
||||
blockEnum chan struct{}
|
||||
}
|
||||
|
||||
func (f *fakeAPI) api() *nativeAPI {
|
||||
return &nativeAPI{
|
||||
openCluster: func() (uintptr, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
f.opens++
|
||||
|
||||
return fakeClusterHandle, nil
|
||||
},
|
||||
closeCluster: func(cluster uintptr) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
f.closes = append(f.closes, cluster)
|
||||
|
||||
return f.closeErr
|
||||
},
|
||||
openEnum: func(uintptr, uint32) (uintptr, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
if len(f.openEnumErr) != 0 {
|
||||
err := f.openEnumErr[0]
|
||||
f.openEnumErr = f.openEnumErr[1:]
|
||||
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return 1, nil
|
||||
},
|
||||
closeEnum: func(uintptr) error { return nil },
|
||||
enumName: func(_ uintptr, index, _ uint32, _ time.Time) (objectName, error) {
|
||||
if index == 0 && f.blockEnum != nil {
|
||||
close(f.started)
|
||||
<-f.blockEnum
|
||||
}
|
||||
|
||||
if int(index) >= len(f.names) {
|
||||
return objectName{}, windows.ERROR_NO_MORE_ITEMS
|
||||
}
|
||||
|
||||
name := f.names[index]
|
||||
|
||||
return objectName{units: append(windowsUnits(name), 0), name: name}, nil
|
||||
},
|
||||
openResource: func(_ uintptr, name objectName) (uintptr, error) {
|
||||
if f.deleted[name.name] {
|
||||
return 0, windows.ERROR_RESOURCE_NOT_FOUND
|
||||
}
|
||||
|
||||
return 2, nil
|
||||
},
|
||||
closeResource: func(uintptr) error { return nil },
|
||||
readResource: func(_ uintptr, resource *Resource, _ time.Time) error {
|
||||
resource.Values["State"] = 2
|
||||
resource.IdentityValid = true
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func windowsUnits(name string) []uint16 {
|
||||
units := make([]uint16, 0, len(name))
|
||||
for _, r := range name {
|
||||
units = append(units, uint16(r))
|
||||
}
|
||||
|
||||
return units
|
||||
}
|
||||
|
||||
// A ClusAPI RPC that outlives its scrape must not queue later calls behind it,
|
||||
// and Close must not wait for it: the running call releases the handle.
|
||||
func TestClusterBusyAndCloseWhileRunning(t *testing.T) {
|
||||
fake := &fakeAPI{names: []string{"a"}, started: make(chan struct{}), blockEnum: make(chan struct{})}
|
||||
c := &Cluster{api: fake.api()}
|
||||
|
||||
done := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
_, err := c.Resources(time.Time{})
|
||||
done <- err
|
||||
}()
|
||||
|
||||
<-fake.started
|
||||
|
||||
if _, err := c.Resources(time.Time{}); !errors.Is(err, ErrBusy) {
|
||||
t.Fatalf("second call: %v, want ErrBusy", err)
|
||||
}
|
||||
|
||||
if err := c.Close(); err != nil {
|
||||
t.Fatalf("Close while running: %v", err)
|
||||
}
|
||||
|
||||
fake.mu.Lock()
|
||||
closes := len(fake.closes)
|
||||
fake.mu.Unlock()
|
||||
|
||||
if closes != 0 {
|
||||
t.Fatal("Close released the handle of a running call")
|
||||
}
|
||||
|
||||
close(fake.blockEnum)
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(fake.closes) != 1 || fake.closes[0] != fakeClusterHandle || c.handle != 0 {
|
||||
t.Fatalf("running call did not release the handle: %v", fake.closes)
|
||||
}
|
||||
|
||||
if _, err := c.Resources(time.Time{}); err == nil || errors.Is(err, ErrBusy) {
|
||||
t.Fatalf("closed cluster: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// A failed ClusterOpenEnum usually means a broken RPC binding. The handle is
|
||||
// dropped so the next scrape reopens the cluster.
|
||||
func TestClusterReopensAfterOpenEnumFailure(t *testing.T) {
|
||||
fake := &fakeAPI{names: []string{"a"}, openEnumErr: []error{windows.RPC_S_SERVER_UNAVAILABLE}}
|
||||
c := &Cluster{api: fake.api()}
|
||||
|
||||
if _, err := c.Resources(time.Time{}); !errors.Is(err, windows.RPC_S_SERVER_UNAVAILABLE) {
|
||||
t.Fatalf("lost native error: %v", err)
|
||||
}
|
||||
|
||||
if c.handle != 0 || len(fake.closes) != 1 {
|
||||
t.Fatalf("handle = %#x, closes = %v", c.handle, fake.closes)
|
||||
}
|
||||
|
||||
resources, err := c.Resources(time.Time{})
|
||||
if err != nil || len(resources) != 1 || fake.opens != 2 {
|
||||
t.Fatalf("resources = %v, opens = %d, err = %v", resources, fake.opens, err)
|
||||
}
|
||||
|
||||
if err := c.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourcesSkipDeletedResources(t *testing.T) {
|
||||
fake := &fakeAPI{names: []string{"gone", "kept"}, deleted: map[string]bool{"gone": true}}
|
||||
c := &Cluster{api: fake.api()}
|
||||
|
||||
resources, err := c.Resources(time.Time{})
|
||||
if err != nil || len(resources) != 1 || resources[0].Name != "kept" || !resources[0].IdentityValid {
|
||||
t.Fatalf("resources = %v, err = %v", resources, err)
|
||||
}
|
||||
|
||||
if err := c.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// A handle whose CloseCluster fails is dropped, so Build can start over.
|
||||
func TestClusterCloseDropsHandleOnFailure(t *testing.T) {
|
||||
fake := &fakeAPI{closeErr: windows.ERROR_INVALID_HANDLE}
|
||||
c := &Cluster{api: fake.api(), handle: fakeClusterHandle}
|
||||
|
||||
if err := c.Close(); !errors.Is(err, windows.ERROR_INVALID_HANDLE) {
|
||||
t.Fatalf("lost native error: %v", err)
|
||||
}
|
||||
|
||||
if c.handle != 0 || !c.closed {
|
||||
t.Fatal("failed close kept the handle")
|
||||
}
|
||||
|
||||
if err := c.Close(); err != nil {
|
||||
t.Fatalf("second Close: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCallErrorWithoutLastError(t *testing.T) {
|
||||
if err := callError(windows.Errno(0)); err == nil || errors.Is(err, windows.Errno(0)) {
|
||||
t.Fatalf("callError(0) = %v", err)
|
||||
}
|
||||
|
||||
if err := callError(windows.ERROR_ACCESS_DENIED); !errors.Is(err, windows.ERROR_ACCESS_DENIED) {
|
||||
t.Fatalf("lost native error: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
// 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 clusapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// Errors of the OpenCluster*Ex functions for an object that no longer exists.
|
||||
const (
|
||||
errGroupNotFound windows.Errno = 5013 // ERROR_GROUP_NOT_FOUND
|
||||
errNodeNotFound windows.Errno = 5042 // ERROR_CLUSTER_NODE_NOT_FOUND
|
||||
errNetworkNotFound windows.Errno = 5045 // ERROR_CLUSTER_NETWORK_NOT_FOUND
|
||||
)
|
||||
|
||||
// CLUSTER_ENUM values for ClusterOpenEnum.
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/clusapi/ne-clusapi-cluster_enum
|
||||
const (
|
||||
enumNode = 0x01
|
||||
enumResource = 0x04
|
||||
enumGroup = 0x08
|
||||
enumNetwork = 0x10
|
||||
)
|
||||
|
||||
// CLUS_OBJECT_* identifiers and CLCTL_* operations. A CLUSCTL_* control code is
|
||||
// (object << 24) | operation, for example CLUSCTL_GROUP_GET_FLAGS = 0x03000009.
|
||||
// https://learn.microsoft.com/en-us/previous-versions/windows/desktop/mscs/control-code-architecture
|
||||
const (
|
||||
objectGroup = 3
|
||||
objectNode = 4
|
||||
objectNetwork = 5
|
||||
|
||||
ctlGetCharacteristics = 0x05
|
||||
ctlGetFlags = 0x09
|
||||
ctlGetROCommonProperties = 0x55
|
||||
ctlGetCommonProperties = 0x59
|
||||
)
|
||||
|
||||
// The Ex open functions and the control/state functions below are available
|
||||
// since Windows Server 2008 R2, so they cover Windows Server 2012 R2 and 2016.
|
||||
// ClusterOpenEnumEx and the per-type *OpenEnumEx functions (Windows Server 2016+)
|
||||
// are deliberately not used.
|
||||
//
|
||||
//nolint:gochecknoglobals
|
||||
var (
|
||||
openGroup = dll.NewProc("OpenClusterGroupEx")
|
||||
closeGroup = dll.NewProc("CloseClusterGroup")
|
||||
groupControl = dll.NewProc("ClusterGroupControl")
|
||||
groupState = dll.NewProc("GetClusterGroupState")
|
||||
|
||||
openNode = dll.NewProc("OpenClusterNodeEx")
|
||||
closeNode = dll.NewProc("CloseClusterNode")
|
||||
nodeControl = dll.NewProc("ClusterNodeControl")
|
||||
nodeState = dll.NewProc("GetClusterNodeState")
|
||||
|
||||
openNetwork = dll.NewProc("OpenClusterNetworkEx")
|
||||
closeNetwork = dll.NewProc("CloseClusterNetwork")
|
||||
networkControl = dll.NewProc("ClusterNetworkControl")
|
||||
networkState = dll.NewProc("GetClusterNetworkState")
|
||||
)
|
||||
|
||||
// Object is a cluster group, node or network. Values contains the 32-bit
|
||||
// common properties plus Characteristics, Flags and State. A missing key means
|
||||
// the value could not be read; it is never substituted with zero.
|
||||
type Object struct {
|
||||
Name string
|
||||
// OwnerNode is the hosting node of a group. OwnerNodeValid is false when
|
||||
// the group state could not be read or the object has no owner concept.
|
||||
OwnerNode string
|
||||
OwnerNodeValid bool
|
||||
Values map[string]uint32
|
||||
}
|
||||
|
||||
type objectAPI struct {
|
||||
kind string
|
||||
enumType uint32
|
||||
object uint32
|
||||
// notFound is the error of the open function for a deleted object.
|
||||
notFound windows.Errno
|
||||
open *windows.LazyProc
|
||||
close *windows.LazyProc
|
||||
control *windows.LazyProc
|
||||
// state reads the object state. It returns the owner node name for groups.
|
||||
state func(handle uintptr, deadline time.Time) (uint32, string, bool, error)
|
||||
}
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var groupAPI = objectAPI{
|
||||
kind: "group",
|
||||
enumType: enumGroup,
|
||||
object: objectGroup,
|
||||
notFound: errGroupNotFound,
|
||||
open: openGroup,
|
||||
close: closeGroup,
|
||||
control: groupControl,
|
||||
state: readGroupState,
|
||||
}
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var nodeAPI = objectAPI{
|
||||
kind: "node",
|
||||
enumType: enumNode,
|
||||
object: objectNode,
|
||||
notFound: errNodeNotFound,
|
||||
open: openNode,
|
||||
close: closeNode,
|
||||
control: nodeControl,
|
||||
state: simpleState(nodeState),
|
||||
}
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var networkAPI = objectAPI{
|
||||
kind: "network",
|
||||
enumType: enumNetwork,
|
||||
object: objectNetwork,
|
||||
notFound: errNetworkNotFound,
|
||||
open: openNetwork,
|
||||
close: closeNetwork,
|
||||
control: networkControl,
|
||||
state: simpleState(networkState),
|
||||
}
|
||||
|
||||
// Networks reads every cluster network; see Groups for the result contract.
|
||||
func (c *Cluster) Networks(deadline time.Time) ([]Object, error) {
|
||||
return c.objects(&networkAPI, deadline)
|
||||
}
|
||||
|
||||
// Nodes reads every cluster node; see Groups for the result contract.
|
||||
func (c *Cluster) Nodes(deadline time.Time) ([]Object, error) {
|
||||
return c.objects(&nodeAPI, deadline)
|
||||
}
|
||||
|
||||
// Groups reads every cluster group. Successful groups and properties are
|
||||
// retained alongside joined failures; see Resources for the deadline contract.
|
||||
func (c *Cluster) Groups(deadline time.Time) ([]Object, error) {
|
||||
return c.objects(&groupAPI, deadline)
|
||||
}
|
||||
|
||||
func (c *Cluster) objects(api *objectAPI, deadline time.Time) (_ []Object, resultErr error) {
|
||||
for _, proc := range []*windows.LazyProc{openCluster, openEnum, nextEnum, closeEnum, api.open, api.close, api.control} {
|
||||
if err := proc.Find(); err != nil {
|
||||
return nil, fmt.Errorf("load ClusAPI: %w: %w", errors.ErrUnsupported, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.begin(deadline); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { resultErr = errors.Join(resultErr, c.end()) }()
|
||||
|
||||
if err := c.open(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var objects []Object
|
||||
|
||||
err := c.enumerate(api.enumType, api.kind, deadline, func(name objectName) error {
|
||||
object, err := c.readObject(api, name, deadline)
|
||||
if errors.Is(err, errObjectDeleted) {
|
||||
return err
|
||||
}
|
||||
|
||||
objects = append(objects, object)
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return objects, err
|
||||
}
|
||||
|
||||
func (c *Cluster) readObject(api *objectAPI, name objectName, deadline time.Time) (_ Object, resultErr error) {
|
||||
object := Object{Name: name.name, Values: make(map[string]uint32)}
|
||||
if err := checkDeadline(deadline); err != nil {
|
||||
return object, err
|
||||
}
|
||||
|
||||
handle, err := openObject(api.open, c.handle, name)
|
||||
if errors.Is(err, api.notFound) {
|
||||
return object, errObjectDeleted
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return object, fmt.Errorf("%s: %w", api.open.Name, err)
|
||||
}
|
||||
defer func() {
|
||||
if err := closeObject(api.close, handle); err != nil {
|
||||
resultErr = errors.Join(resultErr, fmt.Errorf("%s: %w", api.close.Name, err))
|
||||
}
|
||||
}()
|
||||
|
||||
if err := readCommonProperties(api.control, handle, api.object, object.Values, deadline); err != nil {
|
||||
resultErr = errors.Join(resultErr, err)
|
||||
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return object, resultErr
|
||||
}
|
||||
}
|
||||
|
||||
for _, field := range []struct {
|
||||
name string
|
||||
operation uint32
|
||||
}{{"Characteristics", ctlGetCharacteristics}, {"Flags", ctlGetFlags}} {
|
||||
data, err := objectBuffer(api.control, handle, api.object<<24|field.operation, 4, deadline)
|
||||
|
||||
switch {
|
||||
case err != nil:
|
||||
resultErr = errors.Join(resultErr, fmt.Errorf("%s: %w", field.name, err))
|
||||
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return object, resultErr
|
||||
}
|
||||
case len(data) != 4:
|
||||
resultErr = errors.Join(resultErr, fmt.Errorf("%s: invalid DWORD size %d", field.name, len(data)))
|
||||
default:
|
||||
object.Values[field.name] = binary.LittleEndian.Uint32(data)
|
||||
}
|
||||
}
|
||||
|
||||
state, owner, ownerValid, err := api.state(handle, deadline)
|
||||
if err != nil {
|
||||
return object, errors.Join(resultErr, fmt.Errorf("state: %w", err))
|
||||
}
|
||||
|
||||
object.Values["State"] = state
|
||||
object.OwnerNode = owner
|
||||
object.OwnerNodeValid = ownerValid
|
||||
|
||||
return object, resultErr
|
||||
}
|
||||
|
||||
// readCommonProperties stores the single 32-bit values of the read-only and
|
||||
// read/write common property lists in values. Other formats are skipped.
|
||||
func readCommonProperties(control *windows.LazyProc, handle uintptr, object uint32, values map[string]uint32, deadline time.Time) error {
|
||||
var resultErr error
|
||||
|
||||
for _, operation := range []uint32{ctlGetROCommonProperties, ctlGetCommonProperties} {
|
||||
code := object<<24 | operation
|
||||
|
||||
data, err := objectBuffer(control, handle, code, propertyListBufferSize, deadline)
|
||||
if err != nil {
|
||||
resultErr = errors.Join(resultErr, fmt.Errorf("%s %#x: %w", control.Name, code, err))
|
||||
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return resultErr
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
properties, err := ParseProperties(data)
|
||||
if err != nil {
|
||||
resultErr = errors.Join(resultErr, fmt.Errorf("%s %#x: %w", control.Name, code, err))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
for name, list := range properties {
|
||||
if len(list) != 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
if value, err := list[0].Value32(); err == nil {
|
||||
values[name] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resultErr
|
||||
}
|
||||
|
||||
// objectBuffer calls a Cluster{Group,Node,Network}Control function. They share
|
||||
// the ClusterResourceControl signature and return a DWORD status code.
|
||||
func objectBuffer(proc *windows.LazyProc, handle uintptr, code uint32, size int, deadline time.Time) ([]byte, error) {
|
||||
return controlBuffer(deadline, size, func(buffer []byte) (uint32, error) {
|
||||
var (
|
||||
size uint32
|
||||
pointer *byte
|
||||
)
|
||||
if len(buffer) != 0 {
|
||||
pointer = &buffer[0]
|
||||
}
|
||||
|
||||
status, _, _ := proc.Call(handle, 0, uintptr(code), 0, 0, uintptr(unsafe.Pointer(pointer)), uintptr(len(buffer)), uintptr(unsafe.Pointer(&size)))
|
||||
if status != 0 {
|
||||
return size, windows.Errno(status)
|
||||
}
|
||||
|
||||
return size, nil
|
||||
})
|
||||
}
|
||||
|
||||
// readGroupState calls GetClusterGroupState. Like the resource state,
|
||||
// ClusterGroupStateUnknown (-1) is published as a value, as WMI did; the last
|
||||
// error only matters for ERROR_MORE_DATA.
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/clusapi/nf-clusapi-getclustergroupstate
|
||||
func readGroupState(handle uintptr, deadline time.Time) (uint32, string, bool, error) {
|
||||
if err := groupState.Find(); err != nil {
|
||||
return 0, "", false, err
|
||||
}
|
||||
|
||||
// stateBuffers handles a second name; groups have only an owner node, so
|
||||
// the unused buffer stays empty.
|
||||
state, node, _, err := stateBuffers(deadline, func(node, _ []uint16) (uint32, uint32, uint32, error) {
|
||||
nodeLength := uint32(len(node))
|
||||
|
||||
state, _, err := groupState.Call(handle, uintptr(unsafe.Pointer(&node[0])), uintptr(unsafe.Pointer(&nodeLength)))
|
||||
if uint32(state) == stateUnknown && errors.Is(err, windows.ERROR_MORE_DATA) {
|
||||
return uint32(state), nodeLength, 0, err
|
||||
}
|
||||
|
||||
return uint32(state), nodeLength, 0, nil
|
||||
})
|
||||
if err != nil {
|
||||
return 0, "", false, err
|
||||
}
|
||||
|
||||
return state, node, true, nil
|
||||
}
|
||||
|
||||
// simpleState wraps GetClusterNodeState and GetClusterNetworkState. Both take
|
||||
// only the object handle. Their unknown state (-1) is published as a value,
|
||||
// as WMI did.
|
||||
func simpleState(proc *windows.LazyProc) func(uintptr, time.Time) (uint32, string, bool, error) {
|
||||
return func(handle uintptr, deadline time.Time) (uint32, string, bool, error) {
|
||||
if err := proc.Find(); err != nil {
|
||||
return 0, "", false, err
|
||||
}
|
||||
|
||||
if err := checkDeadline(deadline); err != nil {
|
||||
return 0, "", false, err
|
||||
}
|
||||
|
||||
state, _, _ := proc.Call(handle)
|
||||
|
||||
return uint32(state), "", false, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// 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 clusapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func TestControlCodes(t *testing.T) {
|
||||
// Values from clusapi.h (CLUSCTL_GROUP_CODES).
|
||||
for code, want := range map[uint32]uint32{
|
||||
objectGroup<<24 | ctlGetCharacteristics: 0x03000005,
|
||||
objectGroup<<24 | ctlGetFlags: 0x03000009,
|
||||
objectGroup<<24 | ctlGetROCommonProperties: 0x03000055,
|
||||
objectGroup<<24 | ctlGetCommonProperties: 0x03000059,
|
||||
// CLUSCTL_NODE_CODES.
|
||||
objectNode<<24 | ctlGetCharacteristics: 0x04000005,
|
||||
objectNode<<24 | ctlGetFlags: 0x04000009,
|
||||
objectNode<<24 | ctlGetROCommonProperties: 0x04000055,
|
||||
objectNode<<24 | ctlGetCommonProperties: 0x04000059,
|
||||
// CLUSCTL_NETWORK_CODES.
|
||||
objectNetwork<<24 | ctlGetCharacteristics: 0x05000005,
|
||||
objectNetwork<<24 | ctlGetFlags: 0x05000009,
|
||||
objectNetwork<<24 | ctlGetROCommonProperties: 0x05000055,
|
||||
objectNetwork<<24 | ctlGetCommonProperties: 0x05000059,
|
||||
} {
|
||||
if code != want {
|
||||
t.Errorf("control code %#x, want %#x", code, want)
|
||||
}
|
||||
}
|
||||
|
||||
if resourceGetROCommonProperties != 1<<24|ctlGetROCommonProperties || resourceGetFlags != 1<<24|ctlGetFlags {
|
||||
t.Error("object control codes diverge from the resource control codes")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupStateBuffers(t *testing.T) {
|
||||
calls := 0
|
||||
|
||||
state, node, _, err := stateBuffers(time.Time{}, func(node, _ []uint16) (uint32, uint32, uint32, error) {
|
||||
calls++
|
||||
if calls == 1 {
|
||||
return ^uint32(0), 300, 0, windows.ERROR_MORE_DATA
|
||||
}
|
||||
|
||||
copy(node, []uint16{'N', 'B', 0})
|
||||
|
||||
return 0, 2, 0, nil
|
||||
})
|
||||
if err != nil || state != 0 || node != "NB" || calls != 2 {
|
||||
t.Fatalf("state=%d node=%q calls=%d err=%v", state, node, calls, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterClosedObjects(t *testing.T) {
|
||||
c := &Cluster{}
|
||||
if err := c.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := c.Groups(time.Time{}); err == nil {
|
||||
t.Fatal("read closed cluster")
|
||||
}
|
||||
|
||||
if _, err := c.Nodes(time.Time{}); err == nil {
|
||||
t.Fatal("read closed cluster")
|
||||
}
|
||||
|
||||
if _, err := c.Networks(time.Time{}); err == nil {
|
||||
t.Fatal("read closed cluster")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnopenedClusterObjectsExpiredBudget(t *testing.T) {
|
||||
c := &Cluster{}
|
||||
|
||||
_, err := c.Groups(time.Now().Add(-time.Second))
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
if c.handle != 0 || c.closed {
|
||||
t.Fatal("expired scrape changed unopened cluster lifecycle")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
// Hosts without the Failover Clustering feature lack clusapi.dll.
|
||||
if err == nil {
|
||||
t.Fatal("expired budget accepted")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
// 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 clusapi
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"unicode/utf16"
|
||||
)
|
||||
|
||||
const (
|
||||
propertyName = 0x00040003
|
||||
propertyValue = 0x00010000
|
||||
formatDWORD = 2
|
||||
formatString = 3
|
||||
formatExpandString = 4
|
||||
formatLong = 7
|
||||
formatExpandedString = 8
|
||||
)
|
||||
|
||||
// Property is a single CLUSPROP_LIST_VALUE. Values retain their native format;
|
||||
// unsupported formats can be skipped without confusing an absent DWORD with zero.
|
||||
type Property struct {
|
||||
Format uint16
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func (p Property) DWORD() (uint32, error) {
|
||||
if p.Format != formatDWORD || len(p.Data) != 4 {
|
||||
return 0, fmt.Errorf("expected DWORD, format %d, length %d", p.Format, len(p.Data))
|
||||
}
|
||||
|
||||
return binary.LittleEndian.Uint32(p.Data), nil
|
||||
}
|
||||
|
||||
// String decodes an SZ, EXPAND_SZ or EXPANDED_SZ value like WMI does: the value
|
||||
// ends at the first NUL and invalid UTF-16 is replaced with U+FFFD.
|
||||
func (p Property) String() (string, error) {
|
||||
switch p.Format {
|
||||
case formatString, formatExpandString, formatExpandedString:
|
||||
return decodeString(p.Data), nil
|
||||
default:
|
||||
return "", fmt.Errorf("expected string, format %d", p.Format)
|
||||
}
|
||||
}
|
||||
|
||||
// Value32 returns the bit pattern of a DWORD or LONG value. Callers decide the
|
||||
// signedness, matching the WMI type of the property they publish.
|
||||
func (p Property) Value32() (uint32, error) {
|
||||
if (p.Format != formatDWORD && p.Format != formatLong) || len(p.Data) != 4 {
|
||||
return 0, fmt.Errorf("expected DWORD or LONG, format %d, length %d", p.Format, len(p.Data))
|
||||
}
|
||||
|
||||
return binary.LittleEndian.Uint32(p.Data), nil
|
||||
}
|
||||
|
||||
// ParseProperties decodes CLUSPROP_LIST without casting untrusted bytes to native
|
||||
// structs. Headers and payloads are DWORD aligned; cbLength is measured in bytes.
|
||||
// Only the structure is validated: string contents are decoded when used, so one
|
||||
// unusual value does not discard the whole list.
|
||||
// https://learn.microsoft.com/en-us/previous-versions/windows/desktop/mscs/property-lists
|
||||
func ParseProperties(data []byte) (map[string][]Property, error) {
|
||||
if len(data) < 4 {
|
||||
return nil, errors.New("property list is missing its count")
|
||||
}
|
||||
|
||||
count := binary.LittleEndian.Uint32(data)
|
||||
|
||||
data = data[4:]
|
||||
if uint64(count) > uint64(len(data))/16 {
|
||||
return nil, errors.New("property count exceeds buffer")
|
||||
}
|
||||
|
||||
properties := make(map[string][]Property, count)
|
||||
|
||||
for range count {
|
||||
if len(data) < 8 || binary.LittleEndian.Uint32(data) != propertyName {
|
||||
return nil, errors.New("invalid property name header")
|
||||
}
|
||||
|
||||
nameBytes, rest, err := propertyPayload(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
name := decodeString(nameBytes)
|
||||
data = rest
|
||||
values := make([]Property, 0, 1)
|
||||
|
||||
for {
|
||||
if len(data) < 4 {
|
||||
return nil, fmt.Errorf("missing endmark for %q", name)
|
||||
}
|
||||
|
||||
syntax := binary.LittleEndian.Uint32(data)
|
||||
if syntax == 0 {
|
||||
data = data[4:]
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
if syntax&0xffff0000 != propertyValue {
|
||||
return nil, fmt.Errorf("invalid value syntax %#x for %q", syntax, name)
|
||||
}
|
||||
|
||||
payload, rest, err := propertyPayload(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("property %q: %w", name, err)
|
||||
}
|
||||
|
||||
format := uint16(syntax)
|
||||
if (format == formatDWORD || format == formatLong) && len(payload) != 4 {
|
||||
return nil, fmt.Errorf("invalid DWORD length for %q", name)
|
||||
}
|
||||
|
||||
values = append(values, Property{Format: format, Data: payload})
|
||||
data = rest
|
||||
}
|
||||
|
||||
// Keep the first occurrence; a repeated name does not affect the structure.
|
||||
if _, exists := properties[name]; !exists {
|
||||
properties[name] = values
|
||||
}
|
||||
}
|
||||
|
||||
// Lists returned by the cluster service end with a CLUSPROP_SYNTAX_ENDMARK
|
||||
// after the last value list. Accept zero padding, reject anything else.
|
||||
for _, b := range data {
|
||||
if b != 0 {
|
||||
return nil, fmt.Errorf("%d bytes of trailing property list data starting with % x", len(data), data[:min(len(data), 16)])
|
||||
}
|
||||
}
|
||||
|
||||
return properties, nil
|
||||
}
|
||||
|
||||
func propertyPayload(data []byte) ([]byte, []byte, error) {
|
||||
if len(data) < 8 {
|
||||
return nil, nil, errors.New("truncated property header")
|
||||
}
|
||||
|
||||
length := uint64(binary.LittleEndian.Uint32(data[4:]))
|
||||
|
||||
aligned := (length + 3) &^ uint64(3)
|
||||
if aligned > uint64(len(data)-8) {
|
||||
return nil, nil, errors.New("property payload exceeds buffer")
|
||||
}
|
||||
|
||||
return data[8 : 8+length], data[8+aligned:], nil
|
||||
}
|
||||
|
||||
// decodeString decodes little-endian UTF-16 bytes. A trailing odd byte is ignored.
|
||||
func decodeString(data []byte) string {
|
||||
units := make([]uint16, len(data)/2)
|
||||
for i := range units {
|
||||
units[i] = binary.LittleEndian.Uint16(data[i*2:])
|
||||
}
|
||||
|
||||
return decodeUnits(units)
|
||||
}
|
||||
|
||||
// decodeUnits ends the string at the first NUL and replaces invalid UTF-16 with
|
||||
// U+FFFD, matching the strings WMI returned for the same names.
|
||||
func decodeUnits(units []uint16) string {
|
||||
for i, unit := range units {
|
||||
if unit == 0 {
|
||||
units = units[:i]
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return string(utf16.Decode(units))
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
// 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 clusapi
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// CLUSPROP_FORMAT values that the collectors do not decode.
|
||||
const (
|
||||
formatBinary = 1
|
||||
formatMultiString = 5
|
||||
formatULargeInteger = 6
|
||||
formatFileTime = 12
|
||||
)
|
||||
|
||||
// Property lists in testdata were captured with TestDumpPropertyLists from the
|
||||
// CI cluster (Windows Server 2022). Unlike lists built from the documentation,
|
||||
// they end with the extra CLUSPROP_SYNTAX_ENDMARK of the cluster service.
|
||||
func readPropertyFixture(tb testing.TB, name string) []byte {
|
||||
tb.Helper()
|
||||
|
||||
text, err := os.ReadFile(filepath.Join("testdata", name))
|
||||
if err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
|
||||
data, err := hex.DecodeString(strings.Join(strings.Fields(string(text)), ""))
|
||||
if err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
type fixtureValue struct {
|
||||
format uint16
|
||||
length int
|
||||
dword *uint32
|
||||
text *string
|
||||
}
|
||||
|
||||
func dwordValue(value uint32) fixtureValue {
|
||||
return fixtureValue{format: formatDWORD, length: 4, dword: &value}
|
||||
}
|
||||
|
||||
func stringValue(value string, length int) fixtureValue {
|
||||
return fixtureValue{format: formatString, length: length, text: &value}
|
||||
}
|
||||
|
||||
func checkFixtureProperties(t *testing.T, properties map[string][]Property, count int, want map[string]fixtureValue) {
|
||||
t.Helper()
|
||||
|
||||
if len(properties) != count {
|
||||
t.Errorf("properties = %d, want %d", len(properties), count)
|
||||
}
|
||||
|
||||
for name, value := range want {
|
||||
values := properties[name]
|
||||
if len(values) != 1 || values[0].Format != value.format || len(values[0].Data) != value.length {
|
||||
t.Errorf("%s = %+v, want format %d with %d bytes", name, values, value.format, value.length)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if value.dword != nil {
|
||||
if got, err := values[0].DWORD(); err != nil || got != *value.dword {
|
||||
t.Errorf("%s = %d, %v, want %d", name, got, err, *value.dword)
|
||||
}
|
||||
}
|
||||
|
||||
if value.text != nil {
|
||||
if got, err := values[0].String(); err != nil || got != *value.text {
|
||||
t.Errorf("%s = %q, %v, want %q", name, got, err, *value.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePropertiesCapturedResourceLists(t *testing.T) {
|
||||
// Read-only common properties: an SZ whose 38 bytes need 2 bytes of
|
||||
// padding, ULARGE_INTEGER values and a DWORD.
|
||||
properties, err := ParseProperties(readPropertyFixture(t, "resource_ro_common_generic_service.hex"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
checkFixtureProperties(t, properties, 6, map[string]fixtureValue{
|
||||
"Name": stringValue("CI Generic Service", 38),
|
||||
"MonitorProcessId": dwordValue(9092),
|
||||
"StatusInformation": {format: formatULargeInteger, length: 8},
|
||||
"LastOperationStatusCode": {format: formatULargeInteger, length: 8},
|
||||
"ResourceSpecificData1": {format: formatULargeInteger, length: 8},
|
||||
"ResourceSpecificData2": {format: formatULargeInteger, length: 8},
|
||||
})
|
||||
|
||||
// Common properties: the resource type name the collector publishes, empty
|
||||
// strings (2 bytes, padded) and the DWORD values.
|
||||
properties, err = ParseProperties(readPropertyFixture(t, "resource_common_generic_service.hex"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
checkFixtureProperties(t, properties, 15, map[string]fixtureValue{
|
||||
"Type": stringValue("Generic Service", 32),
|
||||
"Description": stringValue("", 2),
|
||||
"ResourceSpecificStatus": stringValue("", 2),
|
||||
"DeadlockTimeout": dwordValue(300000),
|
||||
"EmbeddedFailureAction": dwordValue(2),
|
||||
"IsAlivePollInterval": dwordValue(^uint32(0)),
|
||||
"LooksAlivePollInterval": dwordValue(^uint32(0)),
|
||||
"PendingTimeout": dwordValue(180000),
|
||||
"PersistentState": dwordValue(1),
|
||||
"RestartAction": dwordValue(2),
|
||||
"RestartDelay": dwordValue(500),
|
||||
"RestartPeriod": dwordValue(600000),
|
||||
"RestartThreshold": dwordValue(1),
|
||||
"RetryPeriodOnFailure": dwordValue(600000),
|
||||
"SeparateMonitor": dwordValue(0),
|
||||
})
|
||||
|
||||
// Read-only private properties of an IP Address: FILETIME values.
|
||||
properties, err = ParseProperties(readPropertyFixture(t, "resource_ro_private_ip_address.hex"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
checkFixtureProperties(t, properties, 5, map[string]fixtureValue{
|
||||
"LeaseObtainedTime": {format: formatFileTime, length: 8},
|
||||
"LeaseExpiresTime": {format: formatFileTime, length: 8},
|
||||
"DhcpServer": stringValue("255.255.255.255", 32),
|
||||
"DhcpAddress": stringValue("0.0.0.0", 16),
|
||||
"DhcpSubnetMask": stringValue("255.0.0.0", 20),
|
||||
})
|
||||
}
|
||||
|
||||
func TestParsePropertiesCapturedClusterList(t *testing.T) {
|
||||
// The cluster common list is larger than the 4 KiB first buffer and holds
|
||||
// BINARY security descriptors, MULTI_SZ lists, ULARGE_INTEGER and FILETIME
|
||||
// values next to 64 DWORDs.
|
||||
data := readPropertyFixture(t, "cluster_common.hex")
|
||||
if len(data) <= propertyListBufferSize {
|
||||
t.Fatalf("fixture has %d bytes, want more than the first buffer", len(data))
|
||||
}
|
||||
|
||||
properties, err := ParseProperties(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
checkFixtureProperties(t, properties, 74, map[string]fixtureValue{
|
||||
"Description": stringValue("", 2),
|
||||
"PreferredSite": stringValue("", 2),
|
||||
"Security Descriptor": {format: formatBinary, length: 396},
|
||||
"SharedVolumeSecurityDescriptor": {format: formatBinary, length: 280},
|
||||
"SharedVolumeCompatibleFilters": {format: formatMultiString, length: 2},
|
||||
"SharedVolumeIncompatibleFilters": {format: formatMultiString, length: 2},
|
||||
"EnabledEventLogs": {format: formatMultiString, length: 354},
|
||||
"DumpPolicy": {format: formatULargeInteger, length: 8},
|
||||
"S2DCacheBehavior": {format: formatULargeInteger, length: 8},
|
||||
"RecentEventsResetTime": {format: formatFileTime, length: 8},
|
||||
"ClusterEnforcedAntiaffinity": {format: formatDWORD, length: 4},
|
||||
})
|
||||
|
||||
dwords := 0
|
||||
|
||||
for _, values := range properties {
|
||||
if _, err := values[0].Value32(); err == nil {
|
||||
dwords++
|
||||
}
|
||||
}
|
||||
|
||||
if dwords != 64 {
|
||||
t.Errorf("32-bit values = %d, want 64", dwords)
|
||||
}
|
||||
|
||||
// Every truncation of a real list must be rejected without a panic.
|
||||
for _, name := range []string{"resource_ro_common_generic_service.hex", "resource_common_generic_service.hex"} {
|
||||
data := readPropertyFixture(t, name)
|
||||
for index := range len(data) - 4 {
|
||||
if _, err := ParseProperties(data[:index]); err == nil {
|
||||
t.Errorf("%s: accepted truncation at %d", name, index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzParsePropertiesCaptured(f *testing.F) {
|
||||
for _, name := range []string{"resource_ro_common_generic_service.hex", "resource_common_generic_service.hex", "resource_ro_private_ip_address.hex", "cluster_common.hex"} {
|
||||
f.Add(readPropertyFixture(f, name))
|
||||
}
|
||||
|
||||
f.Fuzz(func(t *testing.T, data []byte) {
|
||||
properties, err := ParseProperties(data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, values := range properties {
|
||||
for _, value := range values {
|
||||
_, _ = value.String()
|
||||
_, _ = value.Value32()
|
||||
|
||||
if (value.Format == formatDWORD || value.Format == formatLong) && len(value.Data) != 4 {
|
||||
t.Fatalf("32-bit value with %d bytes", len(value.Data))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
// 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 clusapi
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
"unicode/utf16"
|
||||
)
|
||||
|
||||
func testPropertyList() []byte {
|
||||
data := binary.LittleEndian.AppendUint32(nil, 1)
|
||||
|
||||
name := make([]byte, 0)
|
||||
for _, unit := range utf16.Encode([]rune("NodeWeight\x00")) {
|
||||
name = binary.LittleEndian.AppendUint16(name, unit)
|
||||
}
|
||||
|
||||
data = binary.LittleEndian.AppendUint32(data, propertyName)
|
||||
data = binary.LittleEndian.AppendUint32(data, uint32(len(name)))
|
||||
|
||||
data = append(data, name...)
|
||||
for len(data)%4 != 0 {
|
||||
data = append(data, 0)
|
||||
}
|
||||
|
||||
data = binary.LittleEndian.AppendUint32(data, propertyValue|formatDWORD)
|
||||
data = binary.LittleEndian.AppendUint32(data, 4)
|
||||
data = binary.LittleEndian.AppendUint32(data, ^uint32(0))
|
||||
|
||||
return binary.LittleEndian.AppendUint32(data, 0)
|
||||
}
|
||||
|
||||
func TestParseProperties(t *testing.T) {
|
||||
data := testPropertyList()
|
||||
|
||||
properties, err := ParseProperties(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
values := properties["NodeWeight"]
|
||||
if len(values) != 1 {
|
||||
t.Fatalf("values = %v", values)
|
||||
}
|
||||
|
||||
value, err := values[0].DWORD()
|
||||
if err != nil || value != ^uint32(0) {
|
||||
t.Fatalf("value = %d, err = %v", value, err)
|
||||
}
|
||||
|
||||
for i := range data {
|
||||
if _, err := ParseProperties(data[:i]); err == nil {
|
||||
t.Errorf("accepted truncation at %d", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePropertiesInvalid(t *testing.T) {
|
||||
tests := map[string]func([]byte){
|
||||
"count": func(data []byte) { binary.LittleEndian.PutUint32(data, ^uint32(0)) },
|
||||
"syntax": func(data []byte) { binary.LittleEndian.PutUint32(data[4:], propertyValue|formatString) },
|
||||
"length": func(data []byte) { binary.LittleEndian.PutUint32(data[8:], ^uint32(0)) },
|
||||
"dword_length": func(data []byte) { binary.LittleEndian.PutUint32(data[40:], 3) },
|
||||
"endmark": func(data []byte) { binary.LittleEndian.PutUint32(data[len(data)-4:], propertyValue) },
|
||||
}
|
||||
for name, mutate := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
data := testPropertyList()
|
||||
mutate(data)
|
||||
|
||||
if _, err := ParseProperties(data); err == nil {
|
||||
t.Fatal("accepted invalid property list")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for _, trailing := range [][]byte{{1}, {1, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0x80}} {
|
||||
if _, err := ParseProperties(append(testPropertyList(), trailing...)); err == nil {
|
||||
t.Fatalf("accepted trailing data % x", trailing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bounds safety does not depend on the trailer, so any zero padding is accepted.
|
||||
func TestParsePropertiesZeroTrailer(t *testing.T) {
|
||||
for _, trailing := range [][]byte{{0}, {0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0}} {
|
||||
properties, err := ParseProperties(append(testPropertyList(), trailing...))
|
||||
if err != nil {
|
||||
t.Fatalf("rejected zero trailer % x: %v", trailing, err)
|
||||
}
|
||||
|
||||
if _, exists := properties["NodeWeight"]; !exists {
|
||||
t.Fatalf("lost property with zero trailer % x", trailing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// String contents are not validated while parsing. They decode like WMI strings:
|
||||
// up to the first NUL, with invalid UTF-16 replaced by U+FFFD.
|
||||
func TestParsePropertiesLenientStrings(t *testing.T) {
|
||||
data := testPropertyList()
|
||||
binary.LittleEndian.PutUint16(data[12:], 0xd800)
|
||||
|
||||
properties, err := ParseProperties(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, exists := properties["�odeWeight"]; !exists {
|
||||
t.Fatalf("unpaired surrogate was not replaced: %v", properties)
|
||||
}
|
||||
|
||||
data = testPropertyList()
|
||||
binary.LittleEndian.PutUint16(data[20:], 0)
|
||||
|
||||
properties, err = ParseProperties(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, exists := properties["Node"]; !exists {
|
||||
t.Fatalf("embedded NUL did not end the name: %v", properties)
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
value Property
|
||||
want string
|
||||
}{
|
||||
{Property{Format: formatString, Data: []byte{'a', 0, 0, 0}}, "a"},
|
||||
{Property{Format: formatExpandString, Data: []byte{'a', 0, 0, 0xd8, 'b', 0}}, "a�b"},
|
||||
{Property{Format: formatExpandedString, Data: []byte{'a', 0, 'b'}}, "a"},
|
||||
{Property{Format: formatString, Data: nil}, ""},
|
||||
} {
|
||||
got, err := tc.value.String()
|
||||
if err != nil || got != tc.want {
|
||||
t.Errorf("String(% x) = %q, %v, want %q", tc.value.Data, got, err, tc.want)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := (Property{Format: formatDWORD, Data: make([]byte, 4)}).String(); err == nil {
|
||||
t.Fatal("decoded DWORD as string")
|
||||
}
|
||||
}
|
||||
|
||||
// The cluster service terminates the whole list with CLUSPROP_SYNTAX_ENDMARK.
|
||||
func TestParsePropertiesListEndmark(t *testing.T) {
|
||||
data := binary.LittleEndian.AppendUint32(testPropertyList(), 0)
|
||||
|
||||
properties, err := ParseProperties(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if value, err := properties["NodeWeight"][0].DWORD(); err != nil || value != ^uint32(0) {
|
||||
t.Fatalf("value = %d, err = %v", value, err)
|
||||
}
|
||||
|
||||
if _, err := ParseProperties(binary.LittleEndian.AppendUint32(nil, 0)); err != nil {
|
||||
t.Fatalf("empty list: %v", err)
|
||||
}
|
||||
|
||||
if _, err := ParseProperties(binary.LittleEndian.AppendUint32(binary.LittleEndian.AppendUint32(nil, 0), 0)); err != nil {
|
||||
t.Fatalf("empty list with endmark: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func testLongPropertyList() []byte {
|
||||
data := testPropertyList()
|
||||
binary.LittleEndian.PutUint32(data[36:], propertyValue|formatLong)
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func TestPropertyValue32(t *testing.T) {
|
||||
properties, err := ParseProperties(testLongPropertyList())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
value := properties["NodeWeight"][0]
|
||||
if _, err := value.DWORD(); err == nil {
|
||||
t.Fatal("LONG accepted as DWORD")
|
||||
}
|
||||
|
||||
bits, err := value.Value32()
|
||||
if err != nil || int32(bits) != -1 {
|
||||
t.Fatalf("LONG bits = %#x, err = %v", bits, err)
|
||||
}
|
||||
|
||||
if _, err := (Property{Format: formatString, Data: []byte{0, 0, 0, 0}}).Value32(); err == nil {
|
||||
t.Fatal("string accepted as 32-bit value")
|
||||
}
|
||||
|
||||
if _, err := (Property{Format: formatLong, Data: []byte{0, 0}}).Value32(); err == nil {
|
||||
t.Fatal("short LONG accepted")
|
||||
}
|
||||
|
||||
invalid := testLongPropertyList()
|
||||
binary.LittleEndian.PutUint32(invalid[40:], 3)
|
||||
|
||||
if _, err := ParseProperties(invalid); err == nil {
|
||||
t.Fatal("accepted LONG with invalid length")
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzParseProperties(f *testing.F) {
|
||||
f.Add(testPropertyList())
|
||||
f.Add(testLongPropertyList())
|
||||
f.Add(binary.LittleEndian.AppendUint32(testPropertyList(), 0))
|
||||
f.Add([]byte{0, 0, 0, 0})
|
||||
f.Fuzz(func(t *testing.T, data []byte) { _, _ = ParseProperties(data) })
|
||||
}
|
||||
|
||||
func BenchmarkParseProperties(b *testing.B) {
|
||||
data := testPropertyList()
|
||||
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
if _, err := ParseProperties(data); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
// 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 clusapi
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const (
|
||||
// CLUSCTL_RESOURCE_STORAGE_GET_DISK_INFO_EX, available since Windows
|
||||
// Server 2008. The EX2 variant needs Windows Server 2016 and adds nothing
|
||||
// that the collector publishes.
|
||||
// https://learn.microsoft.com/en-us/previous-versions/windows/desktop/mscs/clusctl-resource-storage-get-disk-info-ex
|
||||
resourceStorageGetDiskInfoEx = 0x010001f1
|
||||
)
|
||||
|
||||
// Partition is one CLUS_PARTITION_INFO_EX entry of a storage class resource.
|
||||
type Partition struct {
|
||||
Resource string
|
||||
DeviceName string
|
||||
VolumeLabel string
|
||||
VolumeGUID windows.GUID
|
||||
TotalBytes uint64
|
||||
FreeBytes uint64
|
||||
}
|
||||
|
||||
// DiskPartitions reads the partitions of every storage class resource, the
|
||||
// source of the MSCluster_DiskPartition WMI class. Resources that do not
|
||||
// support the disk information control code are skipped. Successful
|
||||
// partitions are retained alongside joined failures.
|
||||
func (c *Cluster) DiskPartitions(deadline time.Time) (_ []Partition, resultErr error) {
|
||||
for _, proc := range []*windows.LazyProc{openCluster, openEnum, nextEnum, closeEnum, openResource, closeResource, resourceControl} {
|
||||
if err := proc.Find(); err != nil {
|
||||
return nil, fmt.Errorf("load ClusAPI: %w: %w", errors.ErrUnsupported, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.begin(deadline); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { resultErr = errors.Join(resultErr, c.end()) }()
|
||||
|
||||
if err := c.open(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var partitions []Partition
|
||||
|
||||
err := c.enumerate(enumResource, "resource", deadline, func(name objectName) error {
|
||||
found, err := c.readResourcePartitions(name, deadline)
|
||||
partitions = append(partitions, found...)
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return partitions, err
|
||||
}
|
||||
|
||||
func (c *Cluster) readResourcePartitions(name objectName, deadline time.Time) (_ []Partition, resultErr error) {
|
||||
if err := checkDeadline(deadline); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
handle, err := openObject(openResource, c.handle, name)
|
||||
if errors.Is(err, windows.ERROR_RESOURCE_NOT_FOUND) {
|
||||
return nil, errObjectDeleted
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("OpenClusterResourceEx: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := closeObject(closeResource, handle); err != nil {
|
||||
resultErr = errors.Join(resultErr, fmt.Errorf("CloseClusterResource: %w", err))
|
||||
}
|
||||
}()
|
||||
|
||||
data, err := resourceBuffer(handle, resourceGetClassInfo, 8, deadline)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("class information: %w", err)
|
||||
}
|
||||
|
||||
if len(data) != 8 {
|
||||
return nil, errors.New("invalid resource class information size")
|
||||
}
|
||||
|
||||
if binary.LittleEndian.Uint32(data) != resourceClassStorage {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
data, err = resourceBuffer(handle, resourceStorageGetDiskInfoEx, propertyListBufferSize, deadline)
|
||||
if errors.Is(err, windows.ERROR_INVALID_FUNCTION) || errors.Is(err, windows.ERROR_NOT_SUPPORTED) {
|
||||
// Storage class resources such as storage pools have no disk layout.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("CLUSCTL_RESOURCE_STORAGE_GET_DISK_INFO_EX: %w", err)
|
||||
}
|
||||
|
||||
partitions, err := ParseDiskPartitions(data)
|
||||
for index := range partitions {
|
||||
partitions[index].Resource = name.name
|
||||
}
|
||||
|
||||
return partitions, err
|
||||
}
|
||||
|
||||
// CLUSPROP_SYNTAX_PARTITION_INFO_EX: CLUSPROP_TYPE_PARTITION_INFO_EX (13) with
|
||||
// CLUSPROP_FORMAT_BINARY (1).
|
||||
const syntaxPartitionInfoEx = 13<<16 | 1
|
||||
|
||||
// Byte offsets in CLUS_PARTITION_INFO_EX. MAX_PATH is 260 WCHARs; the
|
||||
// structure is 1160 bytes with or without natural alignment of the
|
||||
// ULARGE_INTEGER members.
|
||||
// https://learn.microsoft.com/en-us/previous-versions/windows/desktop/api/clusapi/ns-clusapi-clus_partition_info_ex
|
||||
const (
|
||||
partitionDeviceNameOffset = 4
|
||||
partitionVolumeLabelOffset = partitionDeviceNameOffset + 260*2
|
||||
partitionTotalSizeOffset = partitionVolumeLabelOffset + 260*2 + 3*4 + 32*2
|
||||
partitionFreeSizeOffset = partitionTotalSizeOffset + 8
|
||||
partitionVolumeGUIDOffset = partitionFreeSizeOffset + 8 + 2*4
|
||||
partitionInfoExSize = partitionVolumeGUIDOffset + 16
|
||||
)
|
||||
|
||||
// ParseDiskPartitions decodes the CLUSPROP_PARTITION_INFO_EX entries of a
|
||||
// CLUSCTL_RESOURCE_STORAGE_GET_DISK_INFO_EX value list. Other entries, such as
|
||||
// the disk signature or size, are skipped.
|
||||
func ParseDiskPartitions(data []byte) ([]Partition, error) {
|
||||
values, err := ParseValueList(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var partitions []Partition
|
||||
|
||||
for _, value := range values {
|
||||
if value.Syntax != syntaxPartitionInfoEx {
|
||||
continue
|
||||
}
|
||||
|
||||
partition, err := parsePartitionInfoEx(value.Data)
|
||||
if err != nil {
|
||||
return partitions, err
|
||||
}
|
||||
|
||||
partitions = append(partitions, partition)
|
||||
}
|
||||
|
||||
return partitions, nil
|
||||
}
|
||||
|
||||
func parsePartitionInfoEx(data []byte) (Partition, error) {
|
||||
if len(data) < partitionInfoExSize {
|
||||
return Partition{}, fmt.Errorf("partition information has %d bytes, want at least %d", len(data), partitionInfoExSize)
|
||||
}
|
||||
|
||||
// Fixed-size WCHAR arrays decode up to their first NUL, like WMI strings.
|
||||
deviceName := decodeString(data[partitionDeviceNameOffset:partitionVolumeLabelOffset])
|
||||
volumeLabel := decodeString(data[partitionVolumeLabelOffset : partitionVolumeLabelOffset+260*2])
|
||||
|
||||
guid := data[partitionVolumeGUIDOffset:partitionInfoExSize]
|
||||
|
||||
return Partition{
|
||||
DeviceName: deviceName,
|
||||
VolumeLabel: volumeLabel,
|
||||
VolumeGUID: windows.GUID{
|
||||
Data1: binary.LittleEndian.Uint32(guid),
|
||||
Data2: binary.LittleEndian.Uint16(guid[4:]),
|
||||
Data3: binary.LittleEndian.Uint16(guid[6:]),
|
||||
Data4: [8]byte(guid[8:16]),
|
||||
},
|
||||
TotalBytes: binary.LittleEndian.Uint64(data[partitionTotalSizeOffset:]),
|
||||
FreeBytes: binary.LittleEndian.Uint64(data[partitionFreeSizeOffset:]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Value is one entry of a CLUSPROP value list.
|
||||
type Value struct {
|
||||
Syntax uint32
|
||||
Data []byte
|
||||
}
|
||||
|
||||
// ParseValueList decodes a value list: CLUSPROP_VALUE headers (syntax and
|
||||
// byte length) with DWORD-aligned payloads, terminated by
|
||||
// CLUSPROP_SYNTAX_ENDMARK. Zero padding after the endmark is accepted.
|
||||
// https://learn.microsoft.com/en-us/previous-versions/windows/desktop/mscs/value-lists
|
||||
func ParseValueList(data []byte) ([]Value, error) {
|
||||
var values []Value
|
||||
|
||||
for {
|
||||
if len(data) < 4 {
|
||||
return nil, errors.New("value list is missing its endmark")
|
||||
}
|
||||
|
||||
syntax := binary.LittleEndian.Uint32(data)
|
||||
if syntax == 0 {
|
||||
for _, b := range data[4:] {
|
||||
if b != 0 {
|
||||
return nil, fmt.Errorf("%d bytes of trailing value list data", len(data)-4)
|
||||
}
|
||||
}
|
||||
|
||||
return values, nil
|
||||
}
|
||||
|
||||
payload, rest, err := propertyPayload(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("value syntax %#x: %w", syntax, err)
|
||||
}
|
||||
|
||||
values = append(values, Value{Syntax: syntax, Data: payload})
|
||||
data = rest
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
// 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 clusapi
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
"time"
|
||||
"unicode/utf16"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func putFixedString(data []byte, value string) {
|
||||
for index, unit := range utf16.Encode([]rune(value)) {
|
||||
binary.LittleEndian.PutUint16(data[index*2:], unit)
|
||||
}
|
||||
}
|
||||
|
||||
func testPartitionInfoEx() []byte {
|
||||
data := make([]byte, partitionInfoExSize)
|
||||
putFixedString(data[partitionDeviceNameOffset:], `C:\ClusterStorage\Volume1`)
|
||||
putFixedString(data[partitionVolumeLabelOffset:], "CSV01 ")
|
||||
binary.LittleEndian.PutUint64(data[partitionTotalSizeOffset:], 2<<40)
|
||||
binary.LittleEndian.PutUint64(data[partitionFreeSizeOffset:], 19008585728+12345)
|
||||
|
||||
guid := data[partitionVolumeGUIDOffset:]
|
||||
binary.LittleEndian.PutUint32(guid, 0xd3dbada3)
|
||||
binary.LittleEndian.PutUint16(guid[4:], 0x448f)
|
||||
binary.LittleEndian.PutUint16(guid[6:], 0x4c4c)
|
||||
copy(guid[8:], []byte{0xbe, 0x4d, 0x12, 0x2b, 0x7f, 0x29, 0x60, 0x07})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func appendValue(list []byte, syntax uint32, payload []byte) []byte {
|
||||
list = binary.LittleEndian.AppendUint32(list, syntax)
|
||||
list = binary.LittleEndian.AppendUint32(list, uint32(len(payload)))
|
||||
|
||||
list = append(list, payload...)
|
||||
for len(list)%4 != 0 {
|
||||
list = append(list, 0)
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
// testDiskInfo builds the documented GET_DISK_INFO_EX value list: a disk
|
||||
// signature, a disk size, a partition and the endmark.
|
||||
func testDiskInfo() []byte {
|
||||
list := appendValue(nil, 5<<16|2, []byte{1, 2, 3, 4})
|
||||
list = appendValue(list, 12<<16|6, make([]byte, 8))
|
||||
list = appendValue(list, syntaxPartitionInfoEx, testPartitionInfoEx())
|
||||
|
||||
return binary.LittleEndian.AppendUint32(list, 0)
|
||||
}
|
||||
|
||||
func TestParseDiskPartitions(t *testing.T) {
|
||||
partitions, err := ParseDiskPartitions(testDiskInfo())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(partitions) != 1 {
|
||||
t.Fatalf("partitions = %d", len(partitions))
|
||||
}
|
||||
|
||||
partition := partitions[0]
|
||||
want := windows.GUID{Data1: 0xd3dbada3, Data2: 0x448f, Data3: 0x4c4c, Data4: [8]byte{0xbe, 0x4d, 0x12, 0x2b, 0x7f, 0x29, 0x60, 0x07}}
|
||||
|
||||
if partition.DeviceName != `C:\ClusterStorage\Volume1` || partition.VolumeLabel != "CSV01 " || partition.VolumeGUID != want ||
|
||||
partition.TotalBytes != 2<<40 || partition.FreeBytes != 19008585728+12345 {
|
||||
t.Fatalf("partition = %+v", partition)
|
||||
}
|
||||
|
||||
data := testDiskInfo()
|
||||
for i := range data {
|
||||
if _, err := ParseDiskPartitions(data[:i]); err == nil {
|
||||
t.Errorf("accepted truncation at %d", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDiskPartitionsInvalid(t *testing.T) {
|
||||
short := appendValue(nil, syntaxPartitionInfoEx, make([]byte, partitionInfoExSize-1))
|
||||
if _, err := ParseDiskPartitions(binary.LittleEndian.AppendUint32(short, 0)); err == nil {
|
||||
t.Fatal("accepted short partition information")
|
||||
}
|
||||
|
||||
unterminated := testPartitionInfoEx()
|
||||
for index := partitionVolumeLabelOffset; index < partitionVolumeLabelOffset+260*2; index += 2 {
|
||||
binary.LittleEndian.PutUint16(unterminated[index:], 'A')
|
||||
}
|
||||
|
||||
// Fixed-size names decode like WMI strings: the whole array when it is not
|
||||
// terminated, and invalid UTF-16 replaced with U+FFFD.
|
||||
list := binary.LittleEndian.AppendUint32(appendValue(nil, syntaxPartitionInfoEx, unterminated), 0)
|
||||
if partitions, err := ParseDiskPartitions(list); err != nil || len(partitions) != 1 || len(partitions[0].VolumeLabel) != 260 {
|
||||
t.Fatalf("unterminated volume label: %v, %v", partitions, err)
|
||||
}
|
||||
|
||||
surrogate := testPartitionInfoEx()
|
||||
binary.LittleEndian.PutUint16(surrogate[partitionDeviceNameOffset:], 0xdc00)
|
||||
|
||||
list = binary.LittleEndian.AppendUint32(appendValue(nil, syntaxPartitionInfoEx, surrogate), 0)
|
||||
if partitions, err := ParseDiskPartitions(list); err != nil || len(partitions) != 1 || partitions[0].DeviceName[:3] != "�" {
|
||||
t.Fatalf("unpaired surrogate: %v, %v", partitions, err)
|
||||
}
|
||||
|
||||
oversized := testDiskInfo()
|
||||
binary.LittleEndian.PutUint32(oversized[4:], ^uint32(0))
|
||||
|
||||
if _, err := ParseDiskPartitions(oversized); err == nil {
|
||||
t.Fatal("accepted oversized value length")
|
||||
}
|
||||
|
||||
if _, err := ParseDiskPartitions(append(testDiskInfo(), 1, 0, 0, 0)); err == nil {
|
||||
t.Fatal("accepted trailing data")
|
||||
}
|
||||
|
||||
if partitions, err := ParseDiskPartitions(append(testDiskInfo(), 0, 0, 0, 0)); err != nil || len(partitions) != 1 {
|
||||
t.Fatalf("zero padding: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseValueListEmpty(t *testing.T) {
|
||||
values, err := ParseValueList([]byte{0, 0, 0, 0})
|
||||
if err != nil || len(values) != 0 {
|
||||
t.Fatalf("values = %v, err = %v", values, err)
|
||||
}
|
||||
|
||||
if _, err := ParseValueList(nil); err == nil {
|
||||
t.Fatal("accepted missing endmark")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterClosedDiskPartitions(t *testing.T) {
|
||||
c := &Cluster{}
|
||||
if err := c.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := c.DiskPartitions(time.Time{}); err == nil {
|
||||
t.Fatal("read closed cluster")
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzParseDiskPartitions(f *testing.F) {
|
||||
f.Add(testDiskInfo())
|
||||
f.Add([]byte{0, 0, 0, 0})
|
||||
f.Fuzz(func(t *testing.T, data []byte) { _, _ = ParseDiskPartitions(data) })
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
4a000000030004001c0000004100640064004500760069006300740044006500
|
||||
6c0061007900000002000100040000003c000000000000000300040022000000
|
||||
4200610063006b007500700049006e00500072006f0067007200650073007300
|
||||
0000000002000100040000000000000000000000030004002600000043006c00
|
||||
75007300530076006300480061006e006700540069006d0065006f0075007400
|
||||
0000000002000100040000008700000000000000030004003600000043006c00
|
||||
7500730053007600630052006500670072006f00750070005300740061006700
|
||||
6500540069006d0065006f00750074000000000002000100040000000f000000
|
||||
00000000030004004200000043006c0075007300530076006300520065006700
|
||||
72006f00750070005400690063006b0049006e004d0069006c006c0069007300
|
||||
650063006f006e00640073000000000002000100040000002c01000000000000
|
||||
030004002c00000043006c0075007300740065007200470072006f0075007000
|
||||
5700610069007400440065006c00610079000000020001000400000078000000
|
||||
0000000003000400320000004d0069006e0069006d0075006d00500072006500
|
||||
65006d00700074006f0072005000720069006f00720069007400790000000000
|
||||
0200010004000000010000000000000003000400380000004d0069006e006900
|
||||
6d0075006d004e00650076006500720050007200650065006d00700074005000
|
||||
720069006f00720069007400790000000200010004000000b80b000000000000
|
||||
030004003800000043006c007500730074006500720045006e0066006f007200
|
||||
63006500640041006e007400690061006600660069006e006900740079000000
|
||||
02000100040000000000000000000000030004002000000043006c0075007300
|
||||
7400650072004c006f0067004c006500760065006c0000000200010004000000
|
||||
0300000000000000030004001e00000043006c00750073007400650072004c00
|
||||
6f006700530069007a0065000000000002000100040000000006000000000000
|
||||
0300040022000000430072006f00730073005300750062006e00650074004400
|
||||
65006c0061007900000000000200010004000000e80300000000000003000400
|
||||
2a000000430072006f00730073005300750062006e0065007400540068007200
|
||||
6500730068006f006c0064000000000002000100040000001e00000000000000
|
||||
030004001e000000430072006f00730073005300690074006500440065006c00
|
||||
61007900000000000200010004000000e8030000000000000300040026000000
|
||||
430072006f007300730053006900740065005400680072006500730068006f00
|
||||
6c00640000000000020001000400000014000000000000000300040026000000
|
||||
440065006600610075006c0074004e006500740077006f0072006b0052006f00
|
||||
6c00650000000000020001000400000003000000000000000300040018000000
|
||||
4400650073006300720069007000740069006f006e0000000300010002000000
|
||||
0000000000000000030004002800000045006e00610062006c00650053006800
|
||||
610072006500640056006f006c0075006d006500730000000200010004000000
|
||||
0100000000000000030004004c00000053006800610072006500640056006f00
|
||||
6c0075006d0065005600730073005700720069007400650072004f0070006500
|
||||
72006100740069006f006e00540069006d0065006f0075007400000002000100
|
||||
040000000807000000000000030004005000000055007300650043006c006900
|
||||
65006e0074004100630063006500730073004e006500740077006f0072006b00
|
||||
730046006f00720053006800610072006500640056006f006c0075006d006500
|
||||
7300000002000100040000000200000000000000030004002600000048006100
|
||||
6e0067005200650063006f00760065007200790041006300740069006f006e00
|
||||
000000000200010004000000060000000000000003000400280000004c006f00
|
||||
67005200650073006f00750072006300650043006f006e00740072006f006c00
|
||||
7300000002000100040000000000000000000000030004003400000050006c00
|
||||
75006d00620041006c006c00430072006f00730073005300750062006e006500
|
||||
740052006f007500740065007300000002000100040000000000000000000000
|
||||
0300040032000000510075006f00720075006d00410072006200690074007200
|
||||
6100740069006f006e00540069006d0065004d00610078000000000002000100
|
||||
0400000014000000000000000300040028000000520065007100750065007300
|
||||
74005200650070006c007900540069006d0065006f0075007400000002000100
|
||||
040000003c000000000000000300040020000000530061006d00650053007500
|
||||
62006e0065007400440065006c006100790000000200010004000000e8030000
|
||||
000000000300040028000000530061006d0065005300750062006e0065007400
|
||||
5400680072006500730068006f006c006400000002000100040000001e000000
|
||||
0000000003000400280000005300650063007500720069007400790020004400
|
||||
65007300630072006900700074006f0072000000010001008c01000001001480
|
||||
6c0100007c010000140000001c0000000200080000000000020050010a000000
|
||||
000328000300000001060000000000055000000050b9fca79985e2c9d62a71ee
|
||||
d4a97014fb0f3b080003280003000000010600000000000550000000428438f6
|
||||
a00cf3c69c706cb049cbf7d89a81446f00032800030000000106000000000005
|
||||
50000000dd2ce03f3162e2db4015aef4571a3e22e81bbe5a0003280003000000
|
||||
0106000000000005500000003805864282d831674ea6a91735fcf0b852c3fc27
|
||||
0003280003000000010600000000000550000000da87ba0d52ce1099d8a2ea28
|
||||
71d717cf58b8647f000318000300000001020000000000052000000046020000
|
||||
0003180003000000010200000000000520000000200200000003140001000000
|
||||
0101000000000005140000000003140003000000010100000000000512000000
|
||||
0000280003000000010600000000000550000000e5320fec28bea2922c2935f1
|
||||
6342dc6b5a0a2586010200000000000520000000200200000102000000000005
|
||||
200000002002000000000000030004001c000000530065006300750072006900
|
||||
740079004c006500760065006c00000002000100040000000100000000000000
|
||||
0300040030000000530065006300750072006900740079004c00650076006500
|
||||
6c0046006f007200530074006f00720061006700650000000200010004000000
|
||||
0000000000000000030004003c00000053006800610072006500640056006f00
|
||||
6c0075006d00650043006f006d00700061007400690062006c00650046006900
|
||||
6c00740065007200730000000500010002000000000000000000000003000400
|
||||
4000000053006800610072006500640056006f006c0075006d00650049006e00
|
||||
63006f006d00700061007400690062006c006500460069006c00740065007200
|
||||
7300000005000100020000000000000000000000030004003200000053006800
|
||||
7500740064006f0077006e00540069006d0065006f007500740049006e004d00
|
||||
69006e0075007400650073000000000002000100040000001800000000000000
|
||||
03000400380000005700690074006e0065007300730044006100740061006200
|
||||
61007300650057007200690074006500540069006d0065006f00750074000000
|
||||
02000100040000002c01000000000000030004002e0000005700690074006e00
|
||||
650073007300520065007300740061007200740049006e007400650072007600
|
||||
61006c000000000002000100040000000f00000000000000030004003e000000
|
||||
53006800610072006500640056006f006c0075006d0065005300650063007500
|
||||
7200690074007900440065007300630072006900700074006f00720000000000
|
||||
0100010018010000010004800c0100000000000000000000140000000200f800
|
||||
0600000000002800000000100106000000000005500000006084b9dbdd58ffa7
|
||||
f6a00e57c8f9dc9806ecf3140000280000000010010600000000000550000000
|
||||
072e3960a0dce3ce1fb141401771f6dc02293b63000028000000001001060000
|
||||
00000005500000004c58ea82b5845656286e250c7009c7e741f5d9e100002800
|
||||
000000100106000000000005570000003b3ba0f7f4649ae903ce7141e471f485
|
||||
91d1a94f0000280000000010010600000000000550000000c75b0a946ed5ffc5
|
||||
21b1fa2063a9672379b3d51a0000280000000010010600000000000557000000
|
||||
ac074fae90f9a2122adf50d8df37ff6b1b2b7009010100000000000512000000
|
||||
00000000030004001e00000042006c006f0063006b0043006100630068006500
|
||||
530069007a006500000000000200010004000000000400000000000003000400
|
||||
2600000052006f0075007400650048006900730074006f00720079004c006500
|
||||
6e00670074006800000000000200010004000000280000000000000003000400
|
||||
2a000000440079006e0061006d0069006300510075006f00720075006d004500
|
||||
6e00610062006c00650064000000000002000100040000000100000000000000
|
||||
030004002c00000052006500630065006e0074004500760065006e0074007300
|
||||
52006500730065007400540069006d00650000000c00010008000000f0a270a0
|
||||
c958dd0100000000030004002000000044007200610069006e004f006e005300
|
||||
68007500740064006f0077006e00000002000100040000000100000000000000
|
||||
030004002c000000440061007400610062006100730065005200650061006400
|
||||
570072006900740065004d006f00640065000000020001000400000000000000
|
||||
0000000003000400240000004e00650074006600740049005000530065006300
|
||||
45006e00610062006c0065006400000002000100040000000100000000000000
|
||||
0300040018000000430073007600420061006c0061006e006300650072000000
|
||||
0200010004000000010000000000000003000400280000004d00650073007300
|
||||
6100670065004200750066006600650072004c0065006e006700740068000000
|
||||
0200010004000000320000000000000003000400340000004c006f0077006500
|
||||
7200510075006f00720075006d005000720069006f0072006900740079004e00
|
||||
6f00640065004900640000000200010004000000000000000000000003000400
|
||||
16000000440075006d00700050006f006c006900630079000000000006000100
|
||||
0800000019110152000000000000000003000400180000005300320044004200
|
||||
7500730054007900700065007300000002000100040000000000000000000000
|
||||
03000400220000005300320044004f007000740069006d0069007a0061007400
|
||||
69006f006e007300000000000200010004000000000000000000000003000400
|
||||
2c00000053003200440049004f004c006100740065006e006300790054006800
|
||||
72006500730068006f006c006400000002000100040000001027000000000000
|
||||
030004002a000000530032004400430061006300680065004400650073006900
|
||||
7200650064005300740061007400650000000000020001000400000002000000
|
||||
0000000003000400380000005300320044004300610063006800650046006c00
|
||||
6100730068005200650073006500720076006500500065007200630065006e00
|
||||
7400000002000100040000000000000000000000030004002200000053003200
|
||||
4400430061006300680065004200650068006100760069006f00720000000000
|
||||
0600010008000000580000000000000000000000030004002e00000053003200
|
||||
4400430061006300680065005000610067006500530069007a0065004b004200
|
||||
7900740065007300000000000200010004000000100000000000000003000400
|
||||
2000000052006500730069006c00690065006e00630079004c00650076006500
|
||||
6c00000002000100040000000200000000000000030004003000000052006500
|
||||
730069006c00690065006e0063007900440065006600610075006c0074005000
|
||||
6500720069006f00640000000200010004000000f00000000000000003000400
|
||||
26000000510075006100720061006e00740069006e0065004400750072006100
|
||||
740069006f006e00000000000200010004000000201c00000000000003000400
|
||||
28000000510075006100720061006e00740069006e0065005400680072006500
|
||||
730068006f006c00640000000200010004000000030000000000000003000400
|
||||
1c00000050007200650066006500720072006500640053006900740065000000
|
||||
0300010002000000000000000000000003000400220000004100750074006f00
|
||||
420061006c0061006e006300650072004d006f00640065000000000002000100
|
||||
04000000020000000000000003000400240000004100750074006f0042006100
|
||||
6c0061006e006300650072004c006500760065006c0000000200010004000000
|
||||
010000000000000003000400260000004100750074006f004100730073006900
|
||||
67006e004e006f00640065005300690074006500000000000200010004000000
|
||||
0100000000000000030004002e000000470072006f0075007000440065007000
|
||||
65006e00640065006e0063007900540069006d0065006f007500740000000000
|
||||
02000100040000005802000000000000030004003e000000490067006e006f00
|
||||
72006500500065007200730069007300740065006e0074005300740061007400
|
||||
65004f006e005300740061007200740075007000000000000200010004000000
|
||||
0000000000000000030004002200000050006c006100630065006d0065006e00
|
||||
74004f007000740069006f006e00730000000000020001000400000000000000
|
||||
0000000003000400280000004400650074006500630074004d0061006e006100
|
||||
6700650064004500760065006e00740073000000020001000400000001000000
|
||||
00000000030004003a0000004400650074006500630074004d0061006e006100
|
||||
6700650064004500760065006e00740073005400680072006500730068006f00
|
||||
6c0064000000000002000100040000003c000000000000000300040022000000
|
||||
45006e00610062006c00650064004500760065006e0074004c006f0067007300
|
||||
0000000005000100620100004d006900630072006f0073006f00660074002d00
|
||||
570069006e0064006f00770073002d00480079007000650072002d0056002d00
|
||||
56006d005300770069007400630068002d0044006900610067006e006f007300
|
||||
7400690063002c0034002c003000780046004600460046004600460046004400
|
||||
00004d006900630072006f0073006f00660074002d00570069006e0064006f00
|
||||
770073002d0053004d0042004400690072006500630074002f00440065006200
|
||||
750067002c00340000004d006900630072006f0073006f00660074002d005700
|
||||
69006e0064006f00770073002d0053004d004200530065007200760065007200
|
||||
2f0041006e0061006c00790074006900630000004d006900630072006f007300
|
||||
6f00660074002d00570069006e0064006f00770073002d004b00650072006e00
|
||||
65006c002d004c00690076006500440075006d0070002f0041006e0061006c00
|
||||
7900740069006300000000000000000000000000030004002400000055007300
|
||||
6500520064006d00610046006f007200530074006f0072006100670065000000
|
||||
02000100040000000100000000000000030004002a0000005300650074005300
|
||||
4d004200420061006e006400770069006400740068004c0069006d0069007400
|
||||
0000000002000100040000000100000000000000030004003000000053004d00
|
||||
4200420061006e006400770069006400740068004c0069006d00690074004600
|
||||
6100630074006f00720000000200010004000000c40900000000000003000400
|
||||
340000004d006100780069006d0075006d0050006100720061006c006c006500
|
||||
6c004d006900670072006100740069006f006e00730000000200010004000000
|
||||
0100000000000000030004002000000057007000720053006500730073006900
|
||||
6f006e0043006f0075006e007400000002000100040000000200000000000000
|
||||
030004002c000000570070007200530065007300730069006f006e0043006f00
|
||||
6f006c004f0066006600540069006d0065000000020001000400000058020000
|
||||
0000000000000000
|
||||
@@ -0,0 +1,29 @@
|
||||
0f000000030004002000000044006500610064006c006f0063006b0054006900
|
||||
6d0065006f007500740000000200010004000000e09304000000000003000400
|
||||
180000004400650073006300720069007000740069006f006e00000003000100
|
||||
02000000000000000000000003000400280000004900730041006c0069007600
|
||||
650050006f006c006c0049006e00740065007200760061006c00000002000100
|
||||
04000000ffffffff00000000030004002e0000004c006f006f006b0073004100
|
||||
6c0069007600650050006f006c006c0049006e00740065007200760061006c00
|
||||
000000000200010004000000ffffffff00000000030004001e00000050006500
|
||||
6e00640069006e006700540069006d0065006f00750074000000000002000100
|
||||
0400000020bf0200000000000300040020000000500065007200730069007300
|
||||
740065006e007400530074006100740065000000020001000400000001000000
|
||||
00000000030004001c0000005200650073007400610072007400410063007400
|
||||
69006f006e00000002000100040000000200000000000000030004001a000000
|
||||
5200650073007400610072007400440065006c00610079000000000002000100
|
||||
04000000f401000000000000030004001c000000520065007300740061007200
|
||||
740050006500720069006f00640000000200010004000000c027090000000000
|
||||
0300040022000000520065007300740061007200740054006800720065007300
|
||||
68006f006c006400000000000200010004000000010000000000000003000400
|
||||
2c00000045006d006200650064006400650064004600610069006c0075007200
|
||||
650041006300740069006f006e00000002000100040000000200000000000000
|
||||
030004002a0000005200650074007200790050006500720069006f0064004f00
|
||||
6e004600610069006c00750072006500000000000200010004000000c0270900
|
||||
000000000300040020000000530065007000610072006100740065004d006f00
|
||||
6e00690074006f00720000000200010004000000000000000000000003000400
|
||||
0a0000005400790070006500000000000300010020000000470065006e006500
|
||||
7200690063002000530065007200760069006300650000000000000003000400
|
||||
2e0000005200650073006f007500720063006500530070006500630069006600
|
||||
6900630053007400610074007500730000000000030001000200000000000000
|
||||
0000000000000000
|
||||
@@ -0,0 +1,14 @@
|
||||
06000000030004000a0000004e0061006d006500000000000300010026000000
|
||||
430049002000470065006e006500720069006300200053006500720076006900
|
||||
63006500000000000000000003000400220000004d006f006e00690074006f00
|
||||
7200500072006f00630065007300730049006400000000000200010004000000
|
||||
8423000000000000030004002400000053007400610074007500730049006e00
|
||||
66006f0072006d006100740069006f006e000000060001000800000000000000
|
||||
000000000000000003000400300000004c006100730074004f00700065007200
|
||||
6100740069006f006e0053007400610074007500730043006f00640065000000
|
||||
0600010008000000000000000000000000000000030004002c00000052006500
|
||||
73006f0075007200630065005300700065006300690066006900630044006100
|
||||
7400610031000000060001000800000000000000000000000000000003000400
|
||||
2c0000005200650073006f007500720063006500530070006500630069006600
|
||||
6900630044006100740061003200000006000100080000000000000000000000
|
||||
0000000000000000
|
||||
@@ -0,0 +1,11 @@
|
||||
0500000003000400240000004c0065006100730065004f006200740061006900
|
||||
6e0065006400540069006d00650000000c000100080000000000000000000000
|
||||
0000000003000400220000004c00650061007300650045007800700069007200
|
||||
65007300540069006d006500000000000c000100080000000000000000000000
|
||||
0000000003000400160000004400680063007000530065007200760065007200
|
||||
0000000003000100200000003200350035002e003200350035002e0032003500
|
||||
35002e0032003500350000000000000003000400180000004400680063007000
|
||||
41006400640072006500730073000000030001001000000030002e0030002e00
|
||||
30002e003000000000000000030004001e000000440068006300700053007500
|
||||
62006e00650074004d00610073006b0000000000030001001400000032003500
|
||||
35002e0030002e0030002e00300000000000000000000000
|
||||
@@ -114,9 +114,10 @@ func TestCollector[C collector.Collector, V any](t *testing.T, fn func(*V) C, co
|
||||
// unsupportedBuild returns true for errors that indicate a collector's WMI
|
||||
// class or PDH counter is not present on this system. MI_RESULT_INVALID_CLASS
|
||||
// is only accepted here (Build path) so that a mistyped class in production
|
||||
// code still fails Collect.
|
||||
// code still fails Collect. Native collectors report a missing role, such as
|
||||
// an unconfigured failover cluster, with errors.ErrUnsupported.
|
||||
func unsupportedBuild(err error) bool {
|
||||
return errors.Is(err, mi.MI_RESULT_INVALID_CLASS) || unsupportedCollect(err)
|
||||
return errors.Is(err, mi.MI_RESULT_INVALID_CLASS) || errors.Is(err, errors.ErrUnsupported) || unsupportedCollect(err)
|
||||
}
|
||||
|
||||
// unsupportedCollect returns true for Collect-path errors that indicate a
|
||||
|
||||
Reference in New Issue
Block a user