Files
windows_exporter/internal/headers/clusapi/cluster.go
T
2026-10-10 17:52:05 +02:00

265 lines
8.0 KiB
Go

// 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")
}