mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-26 21:49:03 +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,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")
|
||||
}
|
||||
Reference in New Issue
Block a user