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:
Jan-Otto Kröpke
2026-10-10 17:52:05 +02:00
committed by GitHub
co-authored by Claude Opus 5.5
parent f292a6fbcf
commit 1302cf6899
44 changed files with 7050 additions and 1036 deletions
+690
View File
@@ -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")
}
+271
View File
@@ -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")
}
}
+264
View File
@@ -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")
}
+104
View File
@@ -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")
}
}
+171
View File
@@ -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))
}
+229
View File
@@ -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)
}
}
+359
View File
@@ -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
}
}
+109
View File
@@ -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")
}
}
+188
View File
@@ -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))
}
}
}
})
}
+238
View File
@@ -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)
}
}
}
+235
View File
@@ -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
}
}
+167
View File
@@ -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) })
}
+189
View File
@@ -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