mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-19 02:59:04 +02:00
mscluster: collect metrics through ClusAPI (#2634)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f292a6fbcf
commit
1302cf6899
@@ -0,0 +1,690 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Copyright The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build windows
|
||||
|
||||
// Package clusapi exposes read-only access to local failover cluster resources.
|
||||
package clusapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const (
|
||||
resourceGetCharacteristics = 0x01000005
|
||||
resourceGetFlags = 0x01000009
|
||||
resourceGetROCommonProperties = 0x01000055
|
||||
resourceGetCommonProperties = 0x01000059
|
||||
resourceGetType = 0x0100002d
|
||||
resourceGetClassInfo = 0x0100000d
|
||||
maxBufferSize = 64 << 20
|
||||
stateUnknown = ^uint32(0) // ClusterResourceStateUnknown
|
||||
propertyListBufferSize = 4 << 10
|
||||
typeBufferSize = 512
|
||||
)
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var (
|
||||
dll = windows.NewLazySystemDLL("clusapi.dll")
|
||||
getClusterState = dll.NewProc("GetNodeClusterState")
|
||||
openCluster = dll.NewProc("OpenClusterEx")
|
||||
closeCluster = dll.NewProc("CloseCluster")
|
||||
openEnum = dll.NewProc("ClusterOpenEnum")
|
||||
nextEnum = dll.NewProc("ClusterEnum")
|
||||
closeEnum = dll.NewProc("ClusterCloseEnum")
|
||||
openResource = dll.NewProc("OpenClusterResourceEx")
|
||||
closeResource = dll.NewProc("CloseClusterResource")
|
||||
resourceControl = dll.NewProc("ClusterResourceControl")
|
||||
resourceState = dll.NewProc("GetClusterResourceState")
|
||||
)
|
||||
|
||||
// ErrBusy reports that an earlier call on the same Cluster has not returned
|
||||
// yet. ClusAPI RPCs cannot be cancelled; a call that outlived its scrape keeps
|
||||
// the cluster handle until the RPC returns, and later calls fail fast instead
|
||||
// of queuing behind it.
|
||||
var ErrBusy = errors.New("previous ClusAPI collection is still running")
|
||||
|
||||
// nativeAPI holds the ClusAPI calls that drive the handle lifecycle and the
|
||||
// enumeration. Tests substitute it to exercise these paths without a cluster.
|
||||
type nativeAPI struct {
|
||||
openCluster func() (uintptr, error)
|
||||
closeCluster func(cluster uintptr) error
|
||||
openEnum func(cluster uintptr, enumType uint32) (uintptr, error)
|
||||
closeEnum func(enum uintptr) error
|
||||
enumName func(enum uintptr, index, enumType uint32, deadline time.Time) (objectName, error)
|
||||
openResource func(cluster uintptr, name objectName) (uintptr, error)
|
||||
closeResource func(resource uintptr) error
|
||||
readResource func(resource uintptr, out *Resource, deadline time.Time) error
|
||||
}
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var clusAPI = nativeAPI{
|
||||
openCluster: func() (uintptr, error) {
|
||||
handle, _, err := openCluster.Call(0, windows.GENERIC_READ, 0)
|
||||
if handle == 0 {
|
||||
return 0, callError(err)
|
||||
}
|
||||
|
||||
return handle, nil
|
||||
},
|
||||
closeCluster: func(cluster uintptr) error {
|
||||
if result, _, err := closeCluster.Call(cluster); result == 0 {
|
||||
return callError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
openEnum: func(cluster uintptr, enumType uint32) (uintptr, error) {
|
||||
enum, _, err := openEnum.Call(cluster, uintptr(enumType))
|
||||
if enum == 0 {
|
||||
return 0, callError(err)
|
||||
}
|
||||
|
||||
return enum, nil
|
||||
},
|
||||
closeEnum: func(enum uintptr) error {
|
||||
if status, _, _ := closeEnum.Call(enum); status != 0 {
|
||||
return windows.Errno(status)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
enumName: enumName,
|
||||
openResource: func(cluster uintptr, name objectName) (uintptr, error) {
|
||||
return openObject(openResource, cluster, name)
|
||||
},
|
||||
closeResource: func(resource uintptr) error {
|
||||
return closeObject(closeResource, resource)
|
||||
},
|
||||
readResource: readResource,
|
||||
}
|
||||
|
||||
// callError returns the last error of a failed handle or BOOL call. Go clears
|
||||
// the thread's last error before each call, so 0 means the API set none.
|
||||
func callError(err error) error {
|
||||
if errors.Is(err, windows.Errno(0)) {
|
||||
return errors.New("failed without setting a Windows error code")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// openObject calls one of the OpenCluster*Ex functions, which share a signature.
|
||||
func openObject(proc *windows.LazyProc, cluster uintptr, name objectName) (uintptr, error) {
|
||||
handle, _, err := proc.Call(cluster, uintptr(unsafe.Pointer(&name.units[0])), windows.GENERIC_READ, 0)
|
||||
if handle == 0 {
|
||||
return 0, callError(err)
|
||||
}
|
||||
|
||||
return handle, nil
|
||||
}
|
||||
|
||||
// closeObject calls one of the CloseCluster* functions, which return a BOOL.
|
||||
func closeObject(proc *windows.LazyProc, handle uintptr) error {
|
||||
if result, _, err := proc.Call(handle); result == 0 {
|
||||
return callError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cluster owns a cluster handle. Only one call runs at a time: while a call
|
||||
// runs it owns the handle, and Close leaves the handle to that call, which
|
||||
// closes it when its RPC returns. This keeps a hung RPC from blocking Close.
|
||||
type Cluster struct {
|
||||
api *nativeAPI
|
||||
|
||||
mu sync.Mutex // guards closed and running, and handle when not running
|
||||
handle uintptr
|
||||
closed bool
|
||||
running bool
|
||||
}
|
||||
|
||||
type Resource struct {
|
||||
Name string
|
||||
Type string
|
||||
OwnerGroup string
|
||||
OwnerNode string
|
||||
IdentityValid bool
|
||||
Values map[string]uint32
|
||||
}
|
||||
|
||||
// Open checks that the local node is a configured cluster member. A missing
|
||||
// clusapi.dll or export reports errors.ErrUnsupported, like a node that is not
|
||||
// part of a cluster.
|
||||
func Open() (*Cluster, error) {
|
||||
for _, proc := range []*windows.LazyProc{getClusterState, openCluster, closeCluster, openEnum, nextEnum, closeEnum, openResource, closeResource, resourceControl, resourceState} {
|
||||
if err := proc.Find(); err != nil {
|
||||
return nil, fmt.Errorf("load ClusAPI: %w: %w", errors.ErrUnsupported, err)
|
||||
}
|
||||
}
|
||||
|
||||
var state uint32
|
||||
|
||||
status, _, _ := getClusterState.Call(0, uintptr(unsafe.Pointer(&state)))
|
||||
if status != 0 {
|
||||
return nil, fmt.Errorf("GetNodeClusterState: %w", windows.Errno(status))
|
||||
}
|
||||
// Installed and configured are separate flags. A configured but stopped
|
||||
// service remains an operational failure rather than unsupported.
|
||||
if state&3 != 3 {
|
||||
return nil, fmt.Errorf("local failover cluster is not configured: %w", errors.ErrUnsupported)
|
||||
}
|
||||
|
||||
// Defer the potentially blocking RPC until collection. A transient service
|
||||
// outage must not turn resource initialization into an exporter startup failure.
|
||||
return &Cluster{api: &clusAPI}, nil
|
||||
}
|
||||
|
||||
func (c *Cluster) native() *nativeAPI {
|
||||
if c.api == nil {
|
||||
return &clusAPI
|
||||
}
|
||||
|
||||
return c.api
|
||||
}
|
||||
|
||||
// Close releases the cluster handle. A handle whose CloseCluster fails is
|
||||
// dropped anyway, so a later Build starts from a clean state. If a call is
|
||||
// still running, it closes the handle when it returns.
|
||||
func (c *Cluster) Close() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.closed = true
|
||||
|
||||
if c.running || c.handle == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
handle := c.handle
|
||||
c.handle = 0
|
||||
|
||||
if err := c.native().closeCluster(handle); err != nil {
|
||||
return fmt.Errorf("CloseCluster: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// begin marks a call as running. A call that is still running makes it fail
|
||||
// with ErrBusy. A successful begin must be paired with end.
|
||||
func (c *Cluster) begin(deadline time.Time) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
switch {
|
||||
case c.closed:
|
||||
return errors.New("cluster is closed")
|
||||
case c.running:
|
||||
return ErrBusy
|
||||
}
|
||||
|
||||
if err := checkDeadline(deadline); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.running = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// open opens the cluster handle on first use. The running call owns the
|
||||
// handle, so the RPC runs without the lock.
|
||||
func (c *Cluster) open() error {
|
||||
if c.handle != 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
handle, err := c.native().openCluster()
|
||||
if err != nil {
|
||||
return fmt.Errorf("OpenClusterEx: %w", err)
|
||||
}
|
||||
|
||||
c.handle = handle
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// end finishes a call started by begin. If Close ran meanwhile, the handle is
|
||||
// released here and the CloseCluster error is returned.
|
||||
func (c *Cluster) end() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.running = false
|
||||
|
||||
if !c.closed || c.handle == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
handle := c.handle
|
||||
c.handle = 0
|
||||
|
||||
if err := c.native().closeCluster(handle); err != nil {
|
||||
return fmt.Errorf("CloseCluster: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// resetHandle drops a handle whose RPC binding failed, for example after a
|
||||
// cluster service restart, so the next call reopens the cluster. The caller
|
||||
// owns the handle through begin. The close error is irrelevant: the call
|
||||
// reports the error that made the handle unusable.
|
||||
func (c *Cluster) resetHandle() {
|
||||
_ = c.native().closeCluster(c.handle)
|
||||
c.handle = 0
|
||||
}
|
||||
|
||||
// errObjectDeleted marks an object that was deleted after ClusterOpenEnum took
|
||||
// its snapshot. The enumeration skips it without an error.
|
||||
var errObjectDeleted = errors.New("object was deleted during the enumeration")
|
||||
|
||||
// enumerate calls visit for every object name of a CLUSTER_ENUM type. Visit
|
||||
// errors are joined per object; the walk stops at the deadline. The caller
|
||||
// owns the handle through begin.
|
||||
func (c *Cluster) enumerate(enumType uint32, kind string, deadline time.Time, visit func(name objectName) error) (resultErr error) {
|
||||
if err := checkDeadline(deadline); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
api := c.native()
|
||||
|
||||
enum, err := api.openEnum(c.handle, enumType)
|
||||
if err != nil {
|
||||
c.resetHandle()
|
||||
|
||||
return fmt.Errorf("ClusterOpenEnum: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := api.closeEnum(enum); err != nil {
|
||||
resultErr = errors.Join(resultErr, fmt.Errorf("ClusterCloseEnum: %w", err))
|
||||
}
|
||||
}()
|
||||
|
||||
for index := uint32(0); ; index++ {
|
||||
name, err := api.enumName(enum, index, enumType, deadline)
|
||||
if errors.Is(err, windows.ERROR_NO_MORE_ITEMS) {
|
||||
return resultErr
|
||||
}
|
||||
|
||||
if _, ok := errors.AsType[invalidEnumEntryError](err); ok {
|
||||
// One malformed entry must not hide the objects after it.
|
||||
resultErr = errors.Join(resultErr, fmt.Errorf("ClusterEnum index %d: %w", index, err))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return errors.Join(resultErr, fmt.Errorf("ClusterEnum: %w", err))
|
||||
}
|
||||
|
||||
err = visit(name)
|
||||
if errors.Is(err, errObjectDeleted) {
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
resultErr = errors.Join(resultErr, fmt.Errorf("%s %q: %w", kind, name.name, err))
|
||||
}
|
||||
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return resultErr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resources retains successful resource/property results alongside joined failures.
|
||||
// Windows ClusAPI RPCs cannot be cancelled. Deadline checks prevent starting
|
||||
// another RPC after the supplied budget; a call that still outlives it makes
|
||||
// later calls fail with ErrBusy until it returns.
|
||||
func (c *Cluster) Resources(deadline time.Time) (_ []Resource, resultErr error) {
|
||||
if err := c.begin(deadline); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { resultErr = errors.Join(resultErr, c.end()) }()
|
||||
|
||||
if err := c.open(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
api := c.native()
|
||||
|
||||
var resources []Resource
|
||||
|
||||
err := c.enumerate(enumResource, "resource", deadline, func(name objectName) error {
|
||||
if err := checkDeadline(deadline); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
handle, err := api.openResource(c.handle, name)
|
||||
if errors.Is(err, windows.ERROR_RESOURCE_NOT_FOUND) {
|
||||
return errObjectDeleted
|
||||
}
|
||||
|
||||
resource := Resource{Name: name.name, Values: make(map[string]uint32)}
|
||||
|
||||
if err != nil {
|
||||
resources = append(resources, resource)
|
||||
|
||||
return fmt.Errorf("OpenClusterResourceEx: %w", err)
|
||||
}
|
||||
|
||||
err = api.readResource(handle, &resource, deadline)
|
||||
if closeErr := api.closeResource(handle); closeErr != nil {
|
||||
err = errors.Join(err, fmt.Errorf("CloseClusterResource: %w", closeErr))
|
||||
}
|
||||
|
||||
resources = append(resources, resource)
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return resources, err
|
||||
}
|
||||
|
||||
// invalidEnumEntryError reports a ClusterEnum entry that violates the API
|
||||
// contract. Enumeration continues with the next index.
|
||||
type invalidEnumEntryError string
|
||||
|
||||
func (e invalidEnumEntryError) Error() string { return string(e) }
|
||||
|
||||
// objectName keeps the NUL-terminated native name for opening the resource
|
||||
// next to the decoded label value.
|
||||
type objectName struct {
|
||||
units []uint16
|
||||
name string
|
||||
}
|
||||
|
||||
func enumName(enum uintptr, index, enumType uint32, deadline time.Time) (objectName, error) {
|
||||
buffer := make([]uint16, 256)
|
||||
|
||||
for range 16 {
|
||||
if err := checkDeadline(deadline); err != nil {
|
||||
return objectName{}, err
|
||||
}
|
||||
|
||||
length := uint32(len(buffer))
|
||||
|
||||
var objectType uint32
|
||||
|
||||
status, _, _ := nextEnum.Call(enum, uintptr(index), uintptr(unsafe.Pointer(&objectType)), uintptr(unsafe.Pointer(&buffer[0])), uintptr(unsafe.Pointer(&length)))
|
||||
if windows.Errno(status) == windows.ERROR_MORE_DATA {
|
||||
if length >= maxBufferSize/2 || int(length)+1 <= len(buffer) {
|
||||
return objectName{}, errors.New("invalid ClusterEnum buffer size")
|
||||
}
|
||||
|
||||
buffer = make([]uint16, int(length)+1)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if status != 0 {
|
||||
return objectName{}, windows.Errno(status)
|
||||
}
|
||||
|
||||
if objectType != enumType || length >= uint32(len(buffer)) {
|
||||
return objectName{}, invalidEnumEntryError(fmt.Sprintf("invalid ClusterEnum entry: type %d, length %d", objectType, length))
|
||||
}
|
||||
|
||||
units := make([]uint16, length+1)
|
||||
copy(units, buffer[:length])
|
||||
|
||||
return objectName{units: units, name: decodeUnits(buffer[:length])}, nil
|
||||
}
|
||||
|
||||
return objectName{}, errors.New("ClusterEnum buffer did not stabilize")
|
||||
}
|
||||
|
||||
// readResource fills the properties of an open resource. Failed properties are
|
||||
// omitted; the identity is only valid once the state and owner are known.
|
||||
func readResource(handle uintptr, resource *Resource, deadline time.Time) error {
|
||||
var resultErr error
|
||||
|
||||
hasType := false
|
||||
|
||||
for _, code := range []uint32{resourceGetROCommonProperties, resourceGetCommonProperties} {
|
||||
data, err := resourceBuffer(handle, code, propertyListBufferSize, deadline)
|
||||
if err != nil {
|
||||
resultErr = errors.Join(resultErr, fmt.Errorf("ClusterResourceControl %#x: %w", code, err))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
properties, err := ParseProperties(data)
|
||||
if err != nil {
|
||||
resultErr = errors.Join(resultErr, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
for name, values := range properties {
|
||||
if len(values) != 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
if value, err := values[0].DWORD(); err == nil {
|
||||
resource.Values[name] = value
|
||||
}
|
||||
}
|
||||
|
||||
// The read-only common Type property is the same resource type name
|
||||
// that CLUSCTL_RESOURCE_GET_RESOURCE_TYPE returns.
|
||||
if values := properties["Type"]; len(values) == 1 {
|
||||
if value, err := values[0].String(); err == nil {
|
||||
resource.Type = value
|
||||
hasType = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, field := range []struct {
|
||||
name string
|
||||
code uint32
|
||||
}{{"Characteristics", resourceGetCharacteristics}, {"Flags", resourceGetFlags}} {
|
||||
data, err := resourceBuffer(handle, field.code, 4, deadline)
|
||||
switch {
|
||||
case err != nil:
|
||||
resultErr = errors.Join(resultErr, fmt.Errorf("%s: %w", field.name, err))
|
||||
case len(data) != 4:
|
||||
resultErr = errors.Join(resultErr, fmt.Errorf("%s: invalid DWORD size", field.name))
|
||||
default:
|
||||
resource.Values[field.name] = binary.LittleEndian.Uint32(data)
|
||||
}
|
||||
}
|
||||
|
||||
data, err := resourceBuffer(handle, resourceGetClassInfo, 8, deadline)
|
||||
switch {
|
||||
case err != nil:
|
||||
resultErr = errors.Join(resultErr, fmt.Errorf("class information: %w", err))
|
||||
case len(data) != 8:
|
||||
resultErr = errors.Join(resultErr, errors.New("invalid resource class information size"))
|
||||
default:
|
||||
resource.Values["ResourceClass"] = binary.LittleEndian.Uint32(data)
|
||||
resource.Values["Subclass"] = binary.LittleEndian.Uint32(data[4:])
|
||||
}
|
||||
|
||||
// The property lists normally carry the type; only ask separately without it.
|
||||
if !hasType {
|
||||
data, err = resourceBuffer(handle, resourceGetType, typeBufferSize, deadline)
|
||||
if err != nil {
|
||||
return errors.Join(resultErr, fmt.Errorf("resource type: %w", err))
|
||||
}
|
||||
|
||||
resource.Type = decodeString(data)
|
||||
}
|
||||
|
||||
state, owner, group, err := readResourceState(handle, deadline)
|
||||
if err != nil {
|
||||
return errors.Join(resultErr, fmt.Errorf("GetClusterResourceState: %w", err))
|
||||
}
|
||||
|
||||
resource.Values["State"] = state
|
||||
resource.OwnerNode = owner
|
||||
resource.OwnerGroup = group
|
||||
resource.IdentityValid = true
|
||||
|
||||
return resultErr
|
||||
}
|
||||
|
||||
func resourceBuffer(handle uintptr, code uint32, size int, deadline time.Time) ([]byte, error) {
|
||||
return controlBuffer(deadline, size, func(buffer []byte) (uint32, error) {
|
||||
var (
|
||||
size uint32
|
||||
pointer *byte
|
||||
)
|
||||
if len(buffer) != 0 {
|
||||
pointer = &buffer[0]
|
||||
}
|
||||
|
||||
status, _, _ := resourceControl.Call(handle, 0, uintptr(code), 0, 0, uintptr(unsafe.Pointer(pointer)), uintptr(len(buffer)), uintptr(unsafe.Pointer(&size)))
|
||||
if status != 0 {
|
||||
return size, windows.Errno(status)
|
||||
}
|
||||
|
||||
return size, nil
|
||||
})
|
||||
}
|
||||
|
||||
func readResourceState(handle uintptr, deadline time.Time) (uint32, string, string, error) {
|
||||
return stateBuffers(deadline, func(node, group []uint16) (uint32, uint32, uint32, error) {
|
||||
nodeLength, groupLength := uint32(len(node)), uint32(len(group))
|
||||
|
||||
state, _, err := resourceState.Call(handle, uintptr(unsafe.Pointer(&node[0])), uintptr(unsafe.Pointer(&nodeLength)), uintptr(unsafe.Pointer(&group[0])), uintptr(unsafe.Pointer(&groupLength)))
|
||||
// Only ERROR_MORE_DATA matters: the caller retries with larger buffers.
|
||||
if uint32(state) == stateUnknown && errors.Is(err, windows.ERROR_MORE_DATA) {
|
||||
return uint32(state), nodeLength, groupLength, err
|
||||
}
|
||||
|
||||
return uint32(state), nodeLength, groupLength, nil
|
||||
})
|
||||
}
|
||||
|
||||
func stateBuffers(deadline time.Time, call func([]uint16, []uint16) (uint32, uint32, uint32, error)) (uint32, string, string, error) {
|
||||
node, group := make([]uint16, 256), make([]uint16, 256)
|
||||
|
||||
for range 16 {
|
||||
if err := checkDeadline(deadline); err != nil {
|
||||
return 0, "", "", err
|
||||
}
|
||||
|
||||
state, nodeLength, groupLength, err := call(node, group)
|
||||
if errors.Is(err, windows.ERROR_MORE_DATA) {
|
||||
if nodeLength >= maxBufferSize/2 || groupLength >= maxBufferSize/2 {
|
||||
return 0, "", "", errors.New("invalid resource state name size")
|
||||
}
|
||||
|
||||
grew := false
|
||||
|
||||
if int(nodeLength)+1 > len(node) {
|
||||
node = make([]uint16, int(nodeLength)+1)
|
||||
grew = true
|
||||
}
|
||||
|
||||
if int(groupLength)+1 > len(group) {
|
||||
group = make([]uint16, int(groupLength)+1)
|
||||
grew = true
|
||||
}
|
||||
|
||||
if !grew {
|
||||
return 0, "", "", errors.New("resource state name buffer did not grow")
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// ClusterResourceStateUnknown is a state WMI published as 4294967295
|
||||
// together with the other properties. A failed query may leave the
|
||||
// names and lengths untouched; the buffers start zeroed, so decoding up
|
||||
// to the first NUL yields whatever names were returned.
|
||||
if state == stateUnknown {
|
||||
return state, decodeUnits(node), decodeUnits(group), nil
|
||||
}
|
||||
|
||||
if uint64(nodeLength) >= uint64(len(node)) || uint64(groupLength) >= uint64(len(group)) {
|
||||
return 0, "", "", errors.New("invalid resource state names")
|
||||
}
|
||||
|
||||
return state, decodeUnits(node[:nodeLength]), decodeUnits(group[:groupLength]), nil
|
||||
}
|
||||
|
||||
return 0, "", "", errors.New("resource state buffers did not stabilize")
|
||||
}
|
||||
|
||||
func checkDeadline(deadline time.Time) error {
|
||||
if !deadline.IsZero() && !time.Now().Before(deadline) {
|
||||
return context.DeadlineExceeded
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// controlBuffer starts with a buffer of the expected size, so most controls need
|
||||
// one RPC. ERROR_MORE_DATA still grows the buffer to the reported size; size 0
|
||||
// starts with a NULL size probe.
|
||||
func controlBuffer(deadline time.Time, size int, call func([]byte) (uint32, error)) ([]byte, error) {
|
||||
var buffer []byte
|
||||
if size > 0 {
|
||||
buffer = make([]byte, size)
|
||||
}
|
||||
|
||||
for range 16 {
|
||||
if err := checkDeadline(deadline); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
size, err := call(buffer)
|
||||
if errors.Is(err, windows.ERROR_MORE_DATA) {
|
||||
if size > maxBufferSize || int(size) <= len(buffer) {
|
||||
return nil, errors.New("invalid ClusterResourceControl buffer size")
|
||||
}
|
||||
|
||||
buffer = make([]byte, size)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// NULL/zero-size probes may succeed with the required buffer size.
|
||||
if len(buffer) == 0 && size != 0 {
|
||||
if size > maxBufferSize {
|
||||
return nil, errors.New("invalid ClusterResourceControl buffer size")
|
||||
}
|
||||
|
||||
buffer = make([]byte, size)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if uint64(size) > uint64(len(buffer)) {
|
||||
return nil, errors.New("ClusterResourceControl returned an oversized buffer")
|
||||
}
|
||||
|
||||
return buffer[:size], nil
|
||||
}
|
||||
|
||||
return nil, errors.New("ClusterResourceControl buffer did not stabilize")
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Copyright The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package clusapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func TestControlBuffer(t *testing.T) {
|
||||
calls := 0
|
||||
|
||||
data, err := controlBuffer(time.Time{}, 0, func(buffer []byte) (uint32, error) {
|
||||
calls++
|
||||
switch calls {
|
||||
case 1:
|
||||
if len(buffer) != 0 {
|
||||
t.Fatal("first query must not supply buffer")
|
||||
}
|
||||
|
||||
return 4, windows.ERROR_MORE_DATA
|
||||
case 2:
|
||||
if len(buffer) != 4 {
|
||||
t.Fatalf("buffer = %d", len(buffer))
|
||||
}
|
||||
|
||||
return 8, windows.ERROR_MORE_DATA
|
||||
default:
|
||||
if len(buffer) != 8 {
|
||||
t.Fatalf("buffer = %d", len(buffer))
|
||||
}
|
||||
|
||||
copy(buffer, []byte{1, 2, 3, 4})
|
||||
|
||||
return 4, nil
|
||||
}
|
||||
})
|
||||
if err != nil || len(data) != 4 || data[3] != 4 || calls != 3 {
|
||||
t.Fatalf("data = %v, calls = %d, err = %v", data, calls, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestControlBufferPresized(t *testing.T) {
|
||||
calls := 0
|
||||
|
||||
data, err := controlBuffer(time.Time{}, 8, func(buffer []byte) (uint32, error) {
|
||||
calls++
|
||||
|
||||
if len(buffer) != 8 {
|
||||
t.Fatalf("buffer = %d", len(buffer))
|
||||
}
|
||||
|
||||
copy(buffer, []byte{1, 2, 3, 4, 5, 6, 7, 8})
|
||||
|
||||
return 8, nil
|
||||
})
|
||||
if err != nil || calls != 1 || len(data) != 8 || data[7] != 8 {
|
||||
t.Fatalf("data = %v, calls = %d, err = %v", data, calls, err)
|
||||
}
|
||||
|
||||
calls = 0
|
||||
|
||||
data, err = controlBuffer(time.Time{}, 4, func(buffer []byte) (uint32, error) {
|
||||
calls++
|
||||
if calls == 1 {
|
||||
return 6, windows.ERROR_MORE_DATA
|
||||
}
|
||||
|
||||
if len(buffer) != 6 {
|
||||
t.Fatalf("buffer = %d", len(buffer))
|
||||
}
|
||||
|
||||
return 6, nil
|
||||
})
|
||||
if err != nil || calls != 2 || len(data) != 6 {
|
||||
t.Fatalf("growth: data = %v, calls = %d, err = %v", data, calls, err)
|
||||
}
|
||||
|
||||
// A pre-sized buffer that is already large enough must not be shrunk or retried.
|
||||
_, err = controlBuffer(time.Time{}, 8, func([]byte) (uint32, error) { return 4, windows.ERROR_MORE_DATA })
|
||||
if err == nil {
|
||||
t.Fatal("accepted ERROR_MORE_DATA without growth")
|
||||
}
|
||||
}
|
||||
|
||||
func TestControlBufferInvalidSizes(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
size uint32
|
||||
err error
|
||||
}{
|
||||
{"zero_more_data", 0, windows.ERROR_MORE_DATA},
|
||||
{"oversized_more_data", maxBufferSize + 1, windows.ERROR_MORE_DATA},
|
||||
{"oversized_success", maxBufferSize + 1, nil},
|
||||
{"unrelated_error", 0, windows.ERROR_ACCESS_DENIED},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := controlBuffer(time.Time{}, 0, func([]byte) (uint32, error) { return tc.size, tc.err })
|
||||
if err == nil {
|
||||
t.Fatal("accepted invalid native response")
|
||||
}
|
||||
|
||||
if errors.Is(tc.err, windows.ERROR_ACCESS_DENIED) && !errors.Is(err, tc.err) {
|
||||
t.Fatalf("lost native error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestControlBufferDeadline(t *testing.T) {
|
||||
calls := 0
|
||||
deadline := time.Now().Add(time.Millisecond)
|
||||
|
||||
_, err := controlBuffer(deadline, 0, func([]byte) (uint32, error) {
|
||||
calls++
|
||||
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
|
||||
return 4, windows.ERROR_MORE_DATA
|
||||
})
|
||||
if !errors.Is(err, context.DeadlineExceeded) || calls != 1 {
|
||||
t.Fatalf("calls = %d, err = %v", calls, err)
|
||||
}
|
||||
|
||||
_, err = controlBuffer(time.Now().Add(-time.Second), 0, func([]byte) (uint32, error) {
|
||||
t.Fatal("started native call after deadline")
|
||||
|
||||
return 0, nil
|
||||
})
|
||||
if !errors.Is(err, context.DeadlineExceeded) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterClosed(t *testing.T) {
|
||||
c := &Cluster{}
|
||||
if err := c.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := c.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := c.Resources(time.Time{}); err == nil {
|
||||
t.Fatal("read closed cluster")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStateBuffersGrowthAndBitPatterns(t *testing.T) {
|
||||
calls := 0
|
||||
|
||||
state, node, group, err := stateBuffers(time.Time{}, func(node, group []uint16) (uint32, uint32, uint32, error) {
|
||||
calls++
|
||||
if calls == 1 {
|
||||
return ^uint32(0), 300, 400, windows.ERROR_MORE_DATA
|
||||
}
|
||||
|
||||
if len(node) != 301 || len(group) != 401 {
|
||||
t.Fatalf("node/group lengths = %d/%d", len(node), len(group))
|
||||
}
|
||||
|
||||
copy(node, []uint16{'N', 0})
|
||||
copy(group, []uint16{'G', 0})
|
||||
|
||||
return 0x80000000, 1, 1, nil
|
||||
})
|
||||
if err != nil || state != 0x80000000 || node != "N" || group != "G" || calls != 2 {
|
||||
t.Fatalf("state=%d node=%s group=%s calls=%d err=%v", state, node, group, calls, err)
|
||||
}
|
||||
}
|
||||
|
||||
// WMI published ClusterResourceStateUnknown with the other resource properties,
|
||||
// so the state is a value, not a failure. The names may be left untouched.
|
||||
func TestStateBuffersUnknownState(t *testing.T) {
|
||||
state, node, group, err := stateBuffers(time.Time{}, func(node, group []uint16) (uint32, uint32, uint32, error) {
|
||||
return stateUnknown, uint32(len(node)), uint32(len(group)), nil
|
||||
})
|
||||
if err != nil || state != stateUnknown || node != "" || group != "" {
|
||||
t.Fatalf("untouched: state=%d node=%q group=%q err=%v", state, node, group, err)
|
||||
}
|
||||
|
||||
state, node, group, err = stateBuffers(time.Time{}, func(node, group []uint16) (uint32, uint32, uint32, error) {
|
||||
copy(node, []uint16{'N', 0})
|
||||
copy(group, []uint16{'G', 0})
|
||||
|
||||
return stateUnknown, 1, 1, nil
|
||||
})
|
||||
if err != nil || state != stateUnknown || node != "N" || group != "G" {
|
||||
t.Fatalf("filled: state=%d node=%q group=%q err=%v", state, node, group, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStateBuffersInvalidNativeResponses(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
state uint32
|
||||
nodeSize uint32
|
||||
groupSize uint32
|
||||
err error
|
||||
}{
|
||||
{"more_data_without_growth", ^uint32(0), 1, 1, windows.ERROR_MORE_DATA},
|
||||
{"oversized_more_data", ^uint32(0), maxBufferSize, 0, windows.ERROR_MORE_DATA},
|
||||
{"oversized_success", 2, 256, 0, nil},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, _, _, err := stateBuffers(time.Time{}, func([]uint16, []uint16) (uint32, uint32, uint32, error) {
|
||||
return tc.state, tc.nodeSize, tc.groupSize, tc.err
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("accepted invalid native response")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestControlBufferSuccessfulSizeProbe(t *testing.T) {
|
||||
calls := 0
|
||||
|
||||
data, err := controlBuffer(time.Time{}, 0, func(buffer []byte) (uint32, error) {
|
||||
calls++
|
||||
if calls == 1 {
|
||||
return 4, nil
|
||||
}
|
||||
|
||||
copy(buffer, []byte{1, 2, 3, 4})
|
||||
|
||||
return 4, nil
|
||||
})
|
||||
if err != nil || calls != 2 || len(data) != 4 || data[3] != 4 {
|
||||
t.Fatalf("successful probe: data=%v calls=%d err=%v", data, calls, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnopenedClusterExpiredBudget(t *testing.T) {
|
||||
c := &Cluster{}
|
||||
if _, err := c.Resources(time.Now().Add(-time.Second)); !errors.Is(err, context.DeadlineExceeded) {
|
||||
t.Fatalf("expired budget: %v", err)
|
||||
}
|
||||
|
||||
if c.handle != 0 || c.closed {
|
||||
t.Fatal("expired scrape changed unopened cluster lifecycle")
|
||||
}
|
||||
|
||||
if err := c.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !c.closed {
|
||||
t.Fatal("unopened cluster was not closed")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Copyright The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package clusapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const (
|
||||
// CLUS_OBJECT_CLUSTER, used for CLUSCTL_CLUSTER_* control codes.
|
||||
objectCluster = 7
|
||||
|
||||
// CLUS_RESCLASS_STORAGE.
|
||||
resourceClassStorage = 1
|
||||
)
|
||||
|
||||
// GetClusterInformation, ClusterControl and GetClusterQuorumResource are
|
||||
// available since Windows Server 2008.
|
||||
//
|
||||
//nolint:gochecknoglobals
|
||||
var (
|
||||
clusterInformation = dll.NewProc("GetClusterInformation")
|
||||
clusterControl = dll.NewProc("ClusterControl")
|
||||
quorumResource = dll.NewProc("GetClusterQuorumResource")
|
||||
)
|
||||
|
||||
// Properties reads the cluster name, the 32-bit cluster common properties and
|
||||
// the quorum values (see readQuorum). A failed name read returns no object.
|
||||
func (c *Cluster) Properties(deadline time.Time) (_ Object, resultErr error) {
|
||||
for _, proc := range []*windows.LazyProc{openCluster, clusterInformation, clusterControl, quorumResource, openResource, closeResource, resourceControl} {
|
||||
if err := proc.Find(); err != nil {
|
||||
return Object{}, fmt.Errorf("load ClusAPI: %w: %w", errors.ErrUnsupported, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.begin(deadline); err != nil {
|
||||
return Object{}, err
|
||||
}
|
||||
defer func() { resultErr = errors.Join(resultErr, c.end()) }()
|
||||
|
||||
if err := c.open(); err != nil {
|
||||
return Object{}, err
|
||||
}
|
||||
|
||||
name, err := readClusterName(c.handle, deadline)
|
||||
if err != nil {
|
||||
// The name is the first RPC on the handle; a broken binding fails here.
|
||||
c.resetHandle()
|
||||
|
||||
return Object{}, fmt.Errorf("GetClusterInformation: %w", err)
|
||||
}
|
||||
|
||||
object := Object{Name: name, Values: make(map[string]uint32)}
|
||||
|
||||
if err := readCommonProperties(clusterControl, c.handle, objectCluster, object.Values, deadline); err != nil {
|
||||
resultErr = errors.Join(resultErr, err)
|
||||
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return object, resultErr
|
||||
}
|
||||
}
|
||||
|
||||
// The common property is spelled ClusterEnforcedAntiaffinity; WMI and the
|
||||
// metric use ClusterEnforcedAntiAffinity.
|
||||
if value, exists := object.Values["ClusterEnforcedAntiaffinity"]; exists {
|
||||
object.Values["ClusterEnforcedAntiAffinity"] = value
|
||||
}
|
||||
|
||||
if err := c.readQuorum(object.Values, deadline); err != nil {
|
||||
resultErr = errors.Join(resultErr, err)
|
||||
}
|
||||
|
||||
return object, resultErr
|
||||
}
|
||||
|
||||
// Quorum type values of MSCluster_Cluster.QuorumTypeValue.
|
||||
const (
|
||||
quorumTypeNode = 1
|
||||
quorumTypeFileShareWitness = 2
|
||||
quorumTypeStorage = 3
|
||||
)
|
||||
|
||||
// readQuorum derives QuorumLogFileSize and QuorumTypeValue from
|
||||
// GetClusterQuorumResource. The log size parameter carries the quorum type
|
||||
// (CLUS_NODE_MAJORITY_QUORUM, CLUS_HYBRID_QUORUM or CLUS_LEGACY_QUORUM). A
|
||||
// cluster without a witness has no quorum resource; otherwise the witness
|
||||
// resource class decides between a disk and a file share or cloud witness.
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/clusapi/nf-clusapi-setclusterquorumresource
|
||||
func (c *Cluster) readQuorum(values map[string]uint32, deadline time.Time) error {
|
||||
resource, logSize, err := readQuorumResource(c.handle, deadline)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetClusterQuorumResource: %w", err)
|
||||
}
|
||||
|
||||
values["QuorumLogFileSize"] = logSize
|
||||
|
||||
if resource == "" {
|
||||
values["QuorumTypeValue"] = quorumTypeNode
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
class, err := c.resourceClass(resource, deadline)
|
||||
if err != nil {
|
||||
return fmt.Errorf("quorum resource %q: %w", resource, err)
|
||||
}
|
||||
|
||||
if class == resourceClassStorage {
|
||||
values["QuorumTypeValue"] = quorumTypeStorage
|
||||
} else {
|
||||
values["QuorumTypeValue"] = quorumTypeFileShareWitness
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// resourceClass returns the CLUS_RESOURCE_CLASS of a resource.
|
||||
func (c *Cluster) resourceClass(name string, deadline time.Time) (_ uint32, resultErr error) {
|
||||
if err := checkDeadline(deadline); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
namePtr, err := windows.UTF16PtrFromString(name)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
handle, _, err := openResource.Call(c.handle, uintptr(unsafe.Pointer(namePtr)), windows.GENERIC_READ, 0)
|
||||
if handle == 0 {
|
||||
return 0, fmt.Errorf("OpenClusterResourceEx: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
result, _, err := closeResource.Call(handle)
|
||||
if result == 0 {
|
||||
resultErr = errors.Join(resultErr, fmt.Errorf("CloseClusterResource: %w", err))
|
||||
}
|
||||
}()
|
||||
|
||||
data, err := resourceBuffer(handle, resourceGetClassInfo, 8, deadline)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("class information: %w", err)
|
||||
}
|
||||
|
||||
if len(data) != 8 {
|
||||
return 0, errors.New("invalid resource class information size")
|
||||
}
|
||||
|
||||
return binary.LittleEndian.Uint32(data), nil
|
||||
}
|
||||
|
||||
// readClusterName calls GetClusterInformation without CLUSTERVERSIONINFO.
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/clusapi/nf-clusapi-getclusterinformation
|
||||
func readClusterName(handle uintptr, deadline time.Time) (string, error) {
|
||||
_, name, _, err := stateBuffers(deadline, func(name, _ []uint16) (uint32, uint32, uint32, error) {
|
||||
length := uint32(len(name))
|
||||
|
||||
status, _, _ := clusterInformation.Call(handle, uintptr(unsafe.Pointer(&name[0])), uintptr(unsafe.Pointer(&length)), 0)
|
||||
if status != 0 {
|
||||
return 0, length, 0, windows.Errno(status)
|
||||
}
|
||||
|
||||
return 0, length, 0, nil
|
||||
})
|
||||
|
||||
return name, err
|
||||
}
|
||||
|
||||
// readQuorumResource returns the quorum resource name and the
|
||||
// lpdwMaxQuorumLogSize value of GetClusterQuorumResource.
|
||||
func readQuorumResource(handle uintptr, deadline time.Time) (string, uint32, error) {
|
||||
return quorumBuffers(deadline, func(resource, device []uint16) (uint32, uint32, uint32, error) {
|
||||
var maxLogSize uint32
|
||||
|
||||
resourceLength, deviceLength := uint32(len(resource)), uint32(len(device))
|
||||
|
||||
status, _, _ := quorumResource.Call(handle, uintptr(unsafe.Pointer(&resource[0])), uintptr(unsafe.Pointer(&resourceLength)), uintptr(unsafe.Pointer(&device[0])), uintptr(unsafe.Pointer(&deviceLength)), uintptr(unsafe.Pointer(&maxLogSize)))
|
||||
if status != 0 {
|
||||
return 0, resourceLength, deviceLength, windows.Errno(status)
|
||||
}
|
||||
|
||||
return maxLogSize, resourceLength, deviceLength, nil
|
||||
})
|
||||
}
|
||||
|
||||
// quorumBuffers grows the name buffers on ERROR_MORE_DATA. On a node majority
|
||||
// cluster the CI fixture showed returned lengths that cover NUL characters, so
|
||||
// the resource name ends at its first NUL within the reported length.
|
||||
func quorumBuffers(deadline time.Time, call func([]uint16, []uint16) (uint32, uint32, uint32, error)) (string, uint32, error) {
|
||||
resource, device := make([]uint16, 256), make([]uint16, 256)
|
||||
|
||||
for range 16 {
|
||||
if err := checkDeadline(deadline); err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
logSize, resourceLength, deviceLength, err := call(resource, device)
|
||||
if errors.Is(err, windows.ERROR_MORE_DATA) {
|
||||
if resourceLength >= maxBufferSize/2 || deviceLength >= maxBufferSize/2 {
|
||||
return "", 0, errors.New("invalid quorum resource name size")
|
||||
}
|
||||
|
||||
grew := false
|
||||
|
||||
if int(resourceLength)+1 > len(resource) {
|
||||
resource = make([]uint16, int(resourceLength)+1)
|
||||
grew = true
|
||||
}
|
||||
|
||||
if int(deviceLength)+1 > len(device) {
|
||||
device = make([]uint16, int(deviceLength)+1)
|
||||
grew = true
|
||||
}
|
||||
|
||||
if !grew {
|
||||
return "", 0, errors.New("quorum resource name buffer did not grow")
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
if uint64(resourceLength) >= uint64(len(resource)) {
|
||||
return "", 0, fmt.Errorf("quorum resource name length %d exceeds buffer", resourceLength)
|
||||
}
|
||||
|
||||
units := resource[:resourceLength]
|
||||
for index, unit := range units {
|
||||
if unit == 0 {
|
||||
units = units[:index]
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return decodeUnits(units), logSize, nil
|
||||
}
|
||||
|
||||
return "", 0, errors.New("quorum resource buffers did not stabilize")
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Copyright The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package clusapi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func TestQuorumBuffersNodeMajority(t *testing.T) {
|
||||
// No quorum resource: the reported length can cover NUL characters.
|
||||
name, logSize, err := quorumBuffers(time.Time{}, func(resource, _ []uint16) (uint32, uint32, uint32, error) {
|
||||
clear(resource)
|
||||
|
||||
return 0, 4, 4, nil
|
||||
})
|
||||
if err != nil || name != "" || logSize != 0 {
|
||||
t.Fatalf("name=%q logSize=%d err=%v", name, logSize, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuorumBuffersWitness(t *testing.T) {
|
||||
calls := 0
|
||||
|
||||
name, logSize, err := quorumBuffers(time.Time{}, func(resource, device []uint16) (uint32, uint32, uint32, error) {
|
||||
calls++
|
||||
if calls == 1 {
|
||||
return 0, 300, 10, windows.ERROR_MORE_DATA
|
||||
}
|
||||
|
||||
if len(resource) != 301 || len(device) != 256 {
|
||||
t.Fatalf("resource/device lengths = %d/%d", len(resource), len(device))
|
||||
}
|
||||
|
||||
copy(resource, []uint16{'F', 'S', 'W', 0})
|
||||
|
||||
return 0x400, 3, 0, nil
|
||||
})
|
||||
if err != nil || name != "FSW" || logSize != 0x400 || calls != 2 {
|
||||
t.Fatalf("name=%q logSize=%#x calls=%d err=%v", name, logSize, calls, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuorumBuffersInvalid(t *testing.T) {
|
||||
for name, call := range map[string]func([]uint16, []uint16) (uint32, uint32, uint32, error){
|
||||
"oversized_success": func([]uint16, []uint16) (uint32, uint32, uint32, error) { return 0, 256, 0, nil },
|
||||
"no_growth": func([]uint16, []uint16) (uint32, uint32, uint32, error) { return 0, 1, 1, windows.ERROR_MORE_DATA },
|
||||
"oversized_growth": func([]uint16, []uint16) (uint32, uint32, uint32, error) {
|
||||
return 0, maxBufferSize, 0, windows.ERROR_MORE_DATA
|
||||
},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if _, _, err := quorumBuffers(time.Time{}, call); err == nil {
|
||||
t.Fatal("accepted invalid native response")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Names decode like WMI strings: invalid UTF-16 becomes U+FFFD.
|
||||
name, _, err := quorumBuffers(time.Time{}, func(resource, _ []uint16) (uint32, uint32, uint32, error) {
|
||||
resource[0] = 0xd800
|
||||
|
||||
return 0, 1, 0, nil
|
||||
})
|
||||
if err != nil || name != "�" {
|
||||
t.Fatalf("surrogate: name=%q err=%v", name, err)
|
||||
}
|
||||
|
||||
_, _, err = quorumBuffers(time.Time{}, func([]uint16, []uint16) (uint32, uint32, uint32, error) {
|
||||
return 0, 0, 0, windows.ERROR_ACCESS_DENIED
|
||||
})
|
||||
if !errors.Is(err, windows.ERROR_ACCESS_DENIED) {
|
||||
t.Fatalf("lost native error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterClosedProperties(t *testing.T) {
|
||||
c := &Cluster{}
|
||||
if err := c.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := c.Properties(time.Time{}); err == nil {
|
||||
t.Fatal("read closed cluster")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Copyright The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package clusapi
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const (
|
||||
resourceGetROPrivateProperties = 0x0100007d
|
||||
resourceGetPrivateProperties = 0x01000081
|
||||
resourceTypeGetROCommonProperties = 0x02000055
|
||||
resourceTypeGetCommonProperties = 0x02000059
|
||||
resourceTypeGetPrivateProperties = 0x02000081
|
||||
)
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var resourceTypeControl = dll.NewProc("ClusterResourceTypeControl")
|
||||
|
||||
// TestDumpPropertyLists logs raw property lists from a live cluster as hex, so
|
||||
// parser fixtures can be built from real cluster service output. Set
|
||||
// WINDOWS_EXPORTER_TEST_CLUSAPI_DUMP=1 on a cluster node to enable it.
|
||||
func TestDumpPropertyLists(t *testing.T) {
|
||||
if os.Getenv("WINDOWS_EXPORTER_TEST_CLUSAPI_DUMP") == "" {
|
||||
t.Skip("WINDOWS_EXPORTER_TEST_CLUSAPI_DUMP is not set")
|
||||
}
|
||||
|
||||
cluster, _, err := openCluster.Call(0, windows.GENERIC_READ, 0)
|
||||
if cluster == 0 {
|
||||
t.Fatalf("OpenClusterEx: %v", err)
|
||||
}
|
||||
|
||||
defer closeCluster.Call(cluster) //nolint:errcheck
|
||||
|
||||
deadline := time.Now().Add(time.Minute)
|
||||
|
||||
for _, code := range []uint32{objectCluster<<24 | ctlGetROCommonProperties, objectCluster<<24 | ctlGetCommonProperties} {
|
||||
data, err := controlBuffer(deadline, 0, func(buffer []byte) (uint32, error) {
|
||||
var (
|
||||
size uint32
|
||||
pointer *byte
|
||||
)
|
||||
if len(buffer) != 0 {
|
||||
pointer = &buffer[0]
|
||||
}
|
||||
|
||||
status, _, _ := clusterControl.Call(cluster, 0, uintptr(code), 0, 0, uintptr(unsafe.Pointer(pointer)), uintptr(len(buffer)), uintptr(unsafe.Pointer(&size)))
|
||||
if status != 0 {
|
||||
return size, windows.Errno(status)
|
||||
}
|
||||
|
||||
return size, nil
|
||||
})
|
||||
logPropertyList(t, fmt.Sprintf("cluster %#x", code), data, err)
|
||||
}
|
||||
|
||||
// Resource type lists carry EXPAND_SZ values such as DllName, which the
|
||||
// cluster service returns as an EXPAND_SZ and EXPANDED_SZ value pair.
|
||||
for _, typeName := range []string{"Generic Service", "IP Address"} {
|
||||
typePtr, err := windows.UTF16PtrFromString(typeName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, code := range []uint32{resourceTypeGetROCommonProperties, resourceTypeGetCommonProperties, resourceTypeGetPrivateProperties} {
|
||||
data, err := controlBuffer(deadline, 0, func(buffer []byte) (uint32, error) {
|
||||
var (
|
||||
size uint32
|
||||
pointer *byte
|
||||
)
|
||||
if len(buffer) != 0 {
|
||||
pointer = &buffer[0]
|
||||
}
|
||||
|
||||
status, _, _ := resourceTypeControl.Call(cluster, uintptr(unsafe.Pointer(typePtr)), 0, uintptr(code), 0, 0, uintptr(unsafe.Pointer(pointer)), uintptr(len(buffer)), uintptr(unsafe.Pointer(&size)))
|
||||
if status != 0 {
|
||||
return size, windows.Errno(status)
|
||||
}
|
||||
|
||||
return size, nil
|
||||
})
|
||||
logPropertyList(t, fmt.Sprintf("resource type %q %#x", typeName, code), data, err)
|
||||
}
|
||||
}
|
||||
|
||||
enum, _, err := openEnum.Call(cluster, 4)
|
||||
if enum == 0 {
|
||||
t.Fatalf("ClusterOpenEnum: %v", err)
|
||||
}
|
||||
|
||||
defer closeEnum.Call(enum) //nolint:errcheck
|
||||
|
||||
for index := uint32(0); ; index++ {
|
||||
name, err := enumName(enum, index, enumResource, deadline)
|
||||
if errors.Is(err, windows.ERROR_NO_MORE_ITEMS) {
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("ClusterEnum: %v", err)
|
||||
}
|
||||
|
||||
handle, _, err := openResource.Call(cluster, uintptr(unsafe.Pointer(&name.units[0])), windows.GENERIC_READ, 0)
|
||||
if handle == 0 {
|
||||
t.Errorf("OpenClusterResourceEx %q: %v", name.name, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
for _, code := range []uint32{resourceGetROCommonProperties, resourceGetCommonProperties, resourceGetROPrivateProperties, resourceGetPrivateProperties} {
|
||||
data, err := resourceBuffer(handle, code, 0, deadline)
|
||||
logPropertyList(t, fmt.Sprintf("resource %q %#x", name.name, code), data, err)
|
||||
}
|
||||
|
||||
data, err := resourceBuffer(handle, resourceGetType, 0, deadline)
|
||||
t.Logf("resource %q GET_RESOURCE_TYPE: %s err=%v", name.name, hex.EncodeToString(data), err)
|
||||
|
||||
closeResource.Call(handle) //nolint:errcheck
|
||||
}
|
||||
}
|
||||
|
||||
func logPropertyList(t *testing.T, label string, data []byte, err error) {
|
||||
t.Helper()
|
||||
|
||||
if err != nil {
|
||||
t.Logf("%s: %v", label, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
properties, parseErr := ParseProperties(data)
|
||||
|
||||
formats := make([]string, 0, len(properties))
|
||||
for name, values := range properties {
|
||||
valueFormats := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
valueFormats = append(valueFormats, fmt.Sprintf("%d/%d", value.Format, len(value.Data)))
|
||||
}
|
||||
|
||||
formats = append(formats, name+"="+strings.Join(valueFormats, "+"))
|
||||
}
|
||||
|
||||
slices.Sort(formats)
|
||||
|
||||
t.Logf("%s: parse error=%v formats=[%s]\n%s", label, parseErr, strings.Join(formats, " "), hex.EncodeToString(data))
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Copyright The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package clusapi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const fakeClusterHandle = 0x10
|
||||
|
||||
// fakeAPI is a nativeAPI over an in-memory resource list. Names listed in
|
||||
// deleted fail to open like resources removed after the enumeration snapshot.
|
||||
type fakeAPI struct {
|
||||
mu sync.Mutex
|
||||
opens int
|
||||
closes []uintptr
|
||||
openEnumErr []error
|
||||
closeErr error
|
||||
names []string
|
||||
deleted map[string]bool
|
||||
// blockEnum, if set, is closed when the first enumeration starts and
|
||||
// waited on before it continues.
|
||||
started chan struct{}
|
||||
blockEnum chan struct{}
|
||||
}
|
||||
|
||||
func (f *fakeAPI) api() *nativeAPI {
|
||||
return &nativeAPI{
|
||||
openCluster: func() (uintptr, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
f.opens++
|
||||
|
||||
return fakeClusterHandle, nil
|
||||
},
|
||||
closeCluster: func(cluster uintptr) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
f.closes = append(f.closes, cluster)
|
||||
|
||||
return f.closeErr
|
||||
},
|
||||
openEnum: func(uintptr, uint32) (uintptr, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
if len(f.openEnumErr) != 0 {
|
||||
err := f.openEnumErr[0]
|
||||
f.openEnumErr = f.openEnumErr[1:]
|
||||
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return 1, nil
|
||||
},
|
||||
closeEnum: func(uintptr) error { return nil },
|
||||
enumName: func(_ uintptr, index, _ uint32, _ time.Time) (objectName, error) {
|
||||
if index == 0 && f.blockEnum != nil {
|
||||
close(f.started)
|
||||
<-f.blockEnum
|
||||
}
|
||||
|
||||
if int(index) >= len(f.names) {
|
||||
return objectName{}, windows.ERROR_NO_MORE_ITEMS
|
||||
}
|
||||
|
||||
name := f.names[index]
|
||||
|
||||
return objectName{units: append(windowsUnits(name), 0), name: name}, nil
|
||||
},
|
||||
openResource: func(_ uintptr, name objectName) (uintptr, error) {
|
||||
if f.deleted[name.name] {
|
||||
return 0, windows.ERROR_RESOURCE_NOT_FOUND
|
||||
}
|
||||
|
||||
return 2, nil
|
||||
},
|
||||
closeResource: func(uintptr) error { return nil },
|
||||
readResource: func(_ uintptr, resource *Resource, _ time.Time) error {
|
||||
resource.Values["State"] = 2
|
||||
resource.IdentityValid = true
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func windowsUnits(name string) []uint16 {
|
||||
units := make([]uint16, 0, len(name))
|
||||
for _, r := range name {
|
||||
units = append(units, uint16(r))
|
||||
}
|
||||
|
||||
return units
|
||||
}
|
||||
|
||||
// A ClusAPI RPC that outlives its scrape must not queue later calls behind it,
|
||||
// and Close must not wait for it: the running call releases the handle.
|
||||
func TestClusterBusyAndCloseWhileRunning(t *testing.T) {
|
||||
fake := &fakeAPI{names: []string{"a"}, started: make(chan struct{}), blockEnum: make(chan struct{})}
|
||||
c := &Cluster{api: fake.api()}
|
||||
|
||||
done := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
_, err := c.Resources(time.Time{})
|
||||
done <- err
|
||||
}()
|
||||
|
||||
<-fake.started
|
||||
|
||||
if _, err := c.Resources(time.Time{}); !errors.Is(err, ErrBusy) {
|
||||
t.Fatalf("second call: %v, want ErrBusy", err)
|
||||
}
|
||||
|
||||
if err := c.Close(); err != nil {
|
||||
t.Fatalf("Close while running: %v", err)
|
||||
}
|
||||
|
||||
fake.mu.Lock()
|
||||
closes := len(fake.closes)
|
||||
fake.mu.Unlock()
|
||||
|
||||
if closes != 0 {
|
||||
t.Fatal("Close released the handle of a running call")
|
||||
}
|
||||
|
||||
close(fake.blockEnum)
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(fake.closes) != 1 || fake.closes[0] != fakeClusterHandle || c.handle != 0 {
|
||||
t.Fatalf("running call did not release the handle: %v", fake.closes)
|
||||
}
|
||||
|
||||
if _, err := c.Resources(time.Time{}); err == nil || errors.Is(err, ErrBusy) {
|
||||
t.Fatalf("closed cluster: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// A failed ClusterOpenEnum usually means a broken RPC binding. The handle is
|
||||
// dropped so the next scrape reopens the cluster.
|
||||
func TestClusterReopensAfterOpenEnumFailure(t *testing.T) {
|
||||
fake := &fakeAPI{names: []string{"a"}, openEnumErr: []error{windows.RPC_S_SERVER_UNAVAILABLE}}
|
||||
c := &Cluster{api: fake.api()}
|
||||
|
||||
if _, err := c.Resources(time.Time{}); !errors.Is(err, windows.RPC_S_SERVER_UNAVAILABLE) {
|
||||
t.Fatalf("lost native error: %v", err)
|
||||
}
|
||||
|
||||
if c.handle != 0 || len(fake.closes) != 1 {
|
||||
t.Fatalf("handle = %#x, closes = %v", c.handle, fake.closes)
|
||||
}
|
||||
|
||||
resources, err := c.Resources(time.Time{})
|
||||
if err != nil || len(resources) != 1 || fake.opens != 2 {
|
||||
t.Fatalf("resources = %v, opens = %d, err = %v", resources, fake.opens, err)
|
||||
}
|
||||
|
||||
if err := c.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourcesSkipDeletedResources(t *testing.T) {
|
||||
fake := &fakeAPI{names: []string{"gone", "kept"}, deleted: map[string]bool{"gone": true}}
|
||||
c := &Cluster{api: fake.api()}
|
||||
|
||||
resources, err := c.Resources(time.Time{})
|
||||
if err != nil || len(resources) != 1 || resources[0].Name != "kept" || !resources[0].IdentityValid {
|
||||
t.Fatalf("resources = %v, err = %v", resources, err)
|
||||
}
|
||||
|
||||
if err := c.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// A handle whose CloseCluster fails is dropped, so Build can start over.
|
||||
func TestClusterCloseDropsHandleOnFailure(t *testing.T) {
|
||||
fake := &fakeAPI{closeErr: windows.ERROR_INVALID_HANDLE}
|
||||
c := &Cluster{api: fake.api(), handle: fakeClusterHandle}
|
||||
|
||||
if err := c.Close(); !errors.Is(err, windows.ERROR_INVALID_HANDLE) {
|
||||
t.Fatalf("lost native error: %v", err)
|
||||
}
|
||||
|
||||
if c.handle != 0 || !c.closed {
|
||||
t.Fatal("failed close kept the handle")
|
||||
}
|
||||
|
||||
if err := c.Close(); err != nil {
|
||||
t.Fatalf("second Close: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCallErrorWithoutLastError(t *testing.T) {
|
||||
if err := callError(windows.Errno(0)); err == nil || errors.Is(err, windows.Errno(0)) {
|
||||
t.Fatalf("callError(0) = %v", err)
|
||||
}
|
||||
|
||||
if err := callError(windows.ERROR_ACCESS_DENIED); !errors.Is(err, windows.ERROR_ACCESS_DENIED) {
|
||||
t.Fatalf("lost native error: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Copyright The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package clusapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// Errors of the OpenCluster*Ex functions for an object that no longer exists.
|
||||
const (
|
||||
errGroupNotFound windows.Errno = 5013 // ERROR_GROUP_NOT_FOUND
|
||||
errNodeNotFound windows.Errno = 5042 // ERROR_CLUSTER_NODE_NOT_FOUND
|
||||
errNetworkNotFound windows.Errno = 5045 // ERROR_CLUSTER_NETWORK_NOT_FOUND
|
||||
)
|
||||
|
||||
// CLUSTER_ENUM values for ClusterOpenEnum.
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/clusapi/ne-clusapi-cluster_enum
|
||||
const (
|
||||
enumNode = 0x01
|
||||
enumResource = 0x04
|
||||
enumGroup = 0x08
|
||||
enumNetwork = 0x10
|
||||
)
|
||||
|
||||
// CLUS_OBJECT_* identifiers and CLCTL_* operations. A CLUSCTL_* control code is
|
||||
// (object << 24) | operation, for example CLUSCTL_GROUP_GET_FLAGS = 0x03000009.
|
||||
// https://learn.microsoft.com/en-us/previous-versions/windows/desktop/mscs/control-code-architecture
|
||||
const (
|
||||
objectGroup = 3
|
||||
objectNode = 4
|
||||
objectNetwork = 5
|
||||
|
||||
ctlGetCharacteristics = 0x05
|
||||
ctlGetFlags = 0x09
|
||||
ctlGetROCommonProperties = 0x55
|
||||
ctlGetCommonProperties = 0x59
|
||||
)
|
||||
|
||||
// The Ex open functions and the control/state functions below are available
|
||||
// since Windows Server 2008 R2, so they cover Windows Server 2012 R2 and 2016.
|
||||
// ClusterOpenEnumEx and the per-type *OpenEnumEx functions (Windows Server 2016+)
|
||||
// are deliberately not used.
|
||||
//
|
||||
//nolint:gochecknoglobals
|
||||
var (
|
||||
openGroup = dll.NewProc("OpenClusterGroupEx")
|
||||
closeGroup = dll.NewProc("CloseClusterGroup")
|
||||
groupControl = dll.NewProc("ClusterGroupControl")
|
||||
groupState = dll.NewProc("GetClusterGroupState")
|
||||
|
||||
openNode = dll.NewProc("OpenClusterNodeEx")
|
||||
closeNode = dll.NewProc("CloseClusterNode")
|
||||
nodeControl = dll.NewProc("ClusterNodeControl")
|
||||
nodeState = dll.NewProc("GetClusterNodeState")
|
||||
|
||||
openNetwork = dll.NewProc("OpenClusterNetworkEx")
|
||||
closeNetwork = dll.NewProc("CloseClusterNetwork")
|
||||
networkControl = dll.NewProc("ClusterNetworkControl")
|
||||
networkState = dll.NewProc("GetClusterNetworkState")
|
||||
)
|
||||
|
||||
// Object is a cluster group, node or network. Values contains the 32-bit
|
||||
// common properties plus Characteristics, Flags and State. A missing key means
|
||||
// the value could not be read; it is never substituted with zero.
|
||||
type Object struct {
|
||||
Name string
|
||||
// OwnerNode is the hosting node of a group. OwnerNodeValid is false when
|
||||
// the group state could not be read or the object has no owner concept.
|
||||
OwnerNode string
|
||||
OwnerNodeValid bool
|
||||
Values map[string]uint32
|
||||
}
|
||||
|
||||
type objectAPI struct {
|
||||
kind string
|
||||
enumType uint32
|
||||
object uint32
|
||||
// notFound is the error of the open function for a deleted object.
|
||||
notFound windows.Errno
|
||||
open *windows.LazyProc
|
||||
close *windows.LazyProc
|
||||
control *windows.LazyProc
|
||||
// state reads the object state. It returns the owner node name for groups.
|
||||
state func(handle uintptr, deadline time.Time) (uint32, string, bool, error)
|
||||
}
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var groupAPI = objectAPI{
|
||||
kind: "group",
|
||||
enumType: enumGroup,
|
||||
object: objectGroup,
|
||||
notFound: errGroupNotFound,
|
||||
open: openGroup,
|
||||
close: closeGroup,
|
||||
control: groupControl,
|
||||
state: readGroupState,
|
||||
}
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var nodeAPI = objectAPI{
|
||||
kind: "node",
|
||||
enumType: enumNode,
|
||||
object: objectNode,
|
||||
notFound: errNodeNotFound,
|
||||
open: openNode,
|
||||
close: closeNode,
|
||||
control: nodeControl,
|
||||
state: simpleState(nodeState),
|
||||
}
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var networkAPI = objectAPI{
|
||||
kind: "network",
|
||||
enumType: enumNetwork,
|
||||
object: objectNetwork,
|
||||
notFound: errNetworkNotFound,
|
||||
open: openNetwork,
|
||||
close: closeNetwork,
|
||||
control: networkControl,
|
||||
state: simpleState(networkState),
|
||||
}
|
||||
|
||||
// Networks reads every cluster network; see Groups for the result contract.
|
||||
func (c *Cluster) Networks(deadline time.Time) ([]Object, error) {
|
||||
return c.objects(&networkAPI, deadline)
|
||||
}
|
||||
|
||||
// Nodes reads every cluster node; see Groups for the result contract.
|
||||
func (c *Cluster) Nodes(deadline time.Time) ([]Object, error) {
|
||||
return c.objects(&nodeAPI, deadline)
|
||||
}
|
||||
|
||||
// Groups reads every cluster group. Successful groups and properties are
|
||||
// retained alongside joined failures; see Resources for the deadline contract.
|
||||
func (c *Cluster) Groups(deadline time.Time) ([]Object, error) {
|
||||
return c.objects(&groupAPI, deadline)
|
||||
}
|
||||
|
||||
func (c *Cluster) objects(api *objectAPI, deadline time.Time) (_ []Object, resultErr error) {
|
||||
for _, proc := range []*windows.LazyProc{openCluster, openEnum, nextEnum, closeEnum, api.open, api.close, api.control} {
|
||||
if err := proc.Find(); err != nil {
|
||||
return nil, fmt.Errorf("load ClusAPI: %w: %w", errors.ErrUnsupported, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.begin(deadline); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { resultErr = errors.Join(resultErr, c.end()) }()
|
||||
|
||||
if err := c.open(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var objects []Object
|
||||
|
||||
err := c.enumerate(api.enumType, api.kind, deadline, func(name objectName) error {
|
||||
object, err := c.readObject(api, name, deadline)
|
||||
if errors.Is(err, errObjectDeleted) {
|
||||
return err
|
||||
}
|
||||
|
||||
objects = append(objects, object)
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return objects, err
|
||||
}
|
||||
|
||||
func (c *Cluster) readObject(api *objectAPI, name objectName, deadline time.Time) (_ Object, resultErr error) {
|
||||
object := Object{Name: name.name, Values: make(map[string]uint32)}
|
||||
if err := checkDeadline(deadline); err != nil {
|
||||
return object, err
|
||||
}
|
||||
|
||||
handle, err := openObject(api.open, c.handle, name)
|
||||
if errors.Is(err, api.notFound) {
|
||||
return object, errObjectDeleted
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return object, fmt.Errorf("%s: %w", api.open.Name, err)
|
||||
}
|
||||
defer func() {
|
||||
if err := closeObject(api.close, handle); err != nil {
|
||||
resultErr = errors.Join(resultErr, fmt.Errorf("%s: %w", api.close.Name, err))
|
||||
}
|
||||
}()
|
||||
|
||||
if err := readCommonProperties(api.control, handle, api.object, object.Values, deadline); err != nil {
|
||||
resultErr = errors.Join(resultErr, err)
|
||||
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return object, resultErr
|
||||
}
|
||||
}
|
||||
|
||||
for _, field := range []struct {
|
||||
name string
|
||||
operation uint32
|
||||
}{{"Characteristics", ctlGetCharacteristics}, {"Flags", ctlGetFlags}} {
|
||||
data, err := objectBuffer(api.control, handle, api.object<<24|field.operation, 4, deadline)
|
||||
|
||||
switch {
|
||||
case err != nil:
|
||||
resultErr = errors.Join(resultErr, fmt.Errorf("%s: %w", field.name, err))
|
||||
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return object, resultErr
|
||||
}
|
||||
case len(data) != 4:
|
||||
resultErr = errors.Join(resultErr, fmt.Errorf("%s: invalid DWORD size %d", field.name, len(data)))
|
||||
default:
|
||||
object.Values[field.name] = binary.LittleEndian.Uint32(data)
|
||||
}
|
||||
}
|
||||
|
||||
state, owner, ownerValid, err := api.state(handle, deadline)
|
||||
if err != nil {
|
||||
return object, errors.Join(resultErr, fmt.Errorf("state: %w", err))
|
||||
}
|
||||
|
||||
object.Values["State"] = state
|
||||
object.OwnerNode = owner
|
||||
object.OwnerNodeValid = ownerValid
|
||||
|
||||
return object, resultErr
|
||||
}
|
||||
|
||||
// readCommonProperties stores the single 32-bit values of the read-only and
|
||||
// read/write common property lists in values. Other formats are skipped.
|
||||
func readCommonProperties(control *windows.LazyProc, handle uintptr, object uint32, values map[string]uint32, deadline time.Time) error {
|
||||
var resultErr error
|
||||
|
||||
for _, operation := range []uint32{ctlGetROCommonProperties, ctlGetCommonProperties} {
|
||||
code := object<<24 | operation
|
||||
|
||||
data, err := objectBuffer(control, handle, code, propertyListBufferSize, deadline)
|
||||
if err != nil {
|
||||
resultErr = errors.Join(resultErr, fmt.Errorf("%s %#x: %w", control.Name, code, err))
|
||||
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return resultErr
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
properties, err := ParseProperties(data)
|
||||
if err != nil {
|
||||
resultErr = errors.Join(resultErr, fmt.Errorf("%s %#x: %w", control.Name, code, err))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
for name, list := range properties {
|
||||
if len(list) != 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
if value, err := list[0].Value32(); err == nil {
|
||||
values[name] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resultErr
|
||||
}
|
||||
|
||||
// objectBuffer calls a Cluster{Group,Node,Network}Control function. They share
|
||||
// the ClusterResourceControl signature and return a DWORD status code.
|
||||
func objectBuffer(proc *windows.LazyProc, handle uintptr, code uint32, size int, deadline time.Time) ([]byte, error) {
|
||||
return controlBuffer(deadline, size, func(buffer []byte) (uint32, error) {
|
||||
var (
|
||||
size uint32
|
||||
pointer *byte
|
||||
)
|
||||
if len(buffer) != 0 {
|
||||
pointer = &buffer[0]
|
||||
}
|
||||
|
||||
status, _, _ := proc.Call(handle, 0, uintptr(code), 0, 0, uintptr(unsafe.Pointer(pointer)), uintptr(len(buffer)), uintptr(unsafe.Pointer(&size)))
|
||||
if status != 0 {
|
||||
return size, windows.Errno(status)
|
||||
}
|
||||
|
||||
return size, nil
|
||||
})
|
||||
}
|
||||
|
||||
// readGroupState calls GetClusterGroupState. Like the resource state,
|
||||
// ClusterGroupStateUnknown (-1) is published as a value, as WMI did; the last
|
||||
// error only matters for ERROR_MORE_DATA.
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/clusapi/nf-clusapi-getclustergroupstate
|
||||
func readGroupState(handle uintptr, deadline time.Time) (uint32, string, bool, error) {
|
||||
if err := groupState.Find(); err != nil {
|
||||
return 0, "", false, err
|
||||
}
|
||||
|
||||
// stateBuffers handles a second name; groups have only an owner node, so
|
||||
// the unused buffer stays empty.
|
||||
state, node, _, err := stateBuffers(deadline, func(node, _ []uint16) (uint32, uint32, uint32, error) {
|
||||
nodeLength := uint32(len(node))
|
||||
|
||||
state, _, err := groupState.Call(handle, uintptr(unsafe.Pointer(&node[0])), uintptr(unsafe.Pointer(&nodeLength)))
|
||||
if uint32(state) == stateUnknown && errors.Is(err, windows.ERROR_MORE_DATA) {
|
||||
return uint32(state), nodeLength, 0, err
|
||||
}
|
||||
|
||||
return uint32(state), nodeLength, 0, nil
|
||||
})
|
||||
if err != nil {
|
||||
return 0, "", false, err
|
||||
}
|
||||
|
||||
return state, node, true, nil
|
||||
}
|
||||
|
||||
// simpleState wraps GetClusterNodeState and GetClusterNetworkState. Both take
|
||||
// only the object handle. Their unknown state (-1) is published as a value,
|
||||
// as WMI did.
|
||||
func simpleState(proc *windows.LazyProc) func(uintptr, time.Time) (uint32, string, bool, error) {
|
||||
return func(handle uintptr, deadline time.Time) (uint32, string, bool, error) {
|
||||
if err := proc.Find(); err != nil {
|
||||
return 0, "", false, err
|
||||
}
|
||||
|
||||
if err := checkDeadline(deadline); err != nil {
|
||||
return 0, "", false, err
|
||||
}
|
||||
|
||||
state, _, _ := proc.Call(handle)
|
||||
|
||||
return uint32(state), "", false, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Copyright The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package clusapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func TestControlCodes(t *testing.T) {
|
||||
// Values from clusapi.h (CLUSCTL_GROUP_CODES).
|
||||
for code, want := range map[uint32]uint32{
|
||||
objectGroup<<24 | ctlGetCharacteristics: 0x03000005,
|
||||
objectGroup<<24 | ctlGetFlags: 0x03000009,
|
||||
objectGroup<<24 | ctlGetROCommonProperties: 0x03000055,
|
||||
objectGroup<<24 | ctlGetCommonProperties: 0x03000059,
|
||||
// CLUSCTL_NODE_CODES.
|
||||
objectNode<<24 | ctlGetCharacteristics: 0x04000005,
|
||||
objectNode<<24 | ctlGetFlags: 0x04000009,
|
||||
objectNode<<24 | ctlGetROCommonProperties: 0x04000055,
|
||||
objectNode<<24 | ctlGetCommonProperties: 0x04000059,
|
||||
// CLUSCTL_NETWORK_CODES.
|
||||
objectNetwork<<24 | ctlGetCharacteristics: 0x05000005,
|
||||
objectNetwork<<24 | ctlGetFlags: 0x05000009,
|
||||
objectNetwork<<24 | ctlGetROCommonProperties: 0x05000055,
|
||||
objectNetwork<<24 | ctlGetCommonProperties: 0x05000059,
|
||||
} {
|
||||
if code != want {
|
||||
t.Errorf("control code %#x, want %#x", code, want)
|
||||
}
|
||||
}
|
||||
|
||||
if resourceGetROCommonProperties != 1<<24|ctlGetROCommonProperties || resourceGetFlags != 1<<24|ctlGetFlags {
|
||||
t.Error("object control codes diverge from the resource control codes")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupStateBuffers(t *testing.T) {
|
||||
calls := 0
|
||||
|
||||
state, node, _, err := stateBuffers(time.Time{}, func(node, _ []uint16) (uint32, uint32, uint32, error) {
|
||||
calls++
|
||||
if calls == 1 {
|
||||
return ^uint32(0), 300, 0, windows.ERROR_MORE_DATA
|
||||
}
|
||||
|
||||
copy(node, []uint16{'N', 'B', 0})
|
||||
|
||||
return 0, 2, 0, nil
|
||||
})
|
||||
if err != nil || state != 0 || node != "NB" || calls != 2 {
|
||||
t.Fatalf("state=%d node=%q calls=%d err=%v", state, node, calls, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterClosedObjects(t *testing.T) {
|
||||
c := &Cluster{}
|
||||
if err := c.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := c.Groups(time.Time{}); err == nil {
|
||||
t.Fatal("read closed cluster")
|
||||
}
|
||||
|
||||
if _, err := c.Nodes(time.Time{}); err == nil {
|
||||
t.Fatal("read closed cluster")
|
||||
}
|
||||
|
||||
if _, err := c.Networks(time.Time{}); err == nil {
|
||||
t.Fatal("read closed cluster")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnopenedClusterObjectsExpiredBudget(t *testing.T) {
|
||||
c := &Cluster{}
|
||||
|
||||
_, err := c.Groups(time.Now().Add(-time.Second))
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
if c.handle != 0 || c.closed {
|
||||
t.Fatal("expired scrape changed unopened cluster lifecycle")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
// Hosts without the Failover Clustering feature lack clusapi.dll.
|
||||
if err == nil {
|
||||
t.Fatal("expired budget accepted")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Copyright The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package clusapi
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"unicode/utf16"
|
||||
)
|
||||
|
||||
const (
|
||||
propertyName = 0x00040003
|
||||
propertyValue = 0x00010000
|
||||
formatDWORD = 2
|
||||
formatString = 3
|
||||
formatExpandString = 4
|
||||
formatLong = 7
|
||||
formatExpandedString = 8
|
||||
)
|
||||
|
||||
// Property is a single CLUSPROP_LIST_VALUE. Values retain their native format;
|
||||
// unsupported formats can be skipped without confusing an absent DWORD with zero.
|
||||
type Property struct {
|
||||
Format uint16
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func (p Property) DWORD() (uint32, error) {
|
||||
if p.Format != formatDWORD || len(p.Data) != 4 {
|
||||
return 0, fmt.Errorf("expected DWORD, format %d, length %d", p.Format, len(p.Data))
|
||||
}
|
||||
|
||||
return binary.LittleEndian.Uint32(p.Data), nil
|
||||
}
|
||||
|
||||
// String decodes an SZ, EXPAND_SZ or EXPANDED_SZ value like WMI does: the value
|
||||
// ends at the first NUL and invalid UTF-16 is replaced with U+FFFD.
|
||||
func (p Property) String() (string, error) {
|
||||
switch p.Format {
|
||||
case formatString, formatExpandString, formatExpandedString:
|
||||
return decodeString(p.Data), nil
|
||||
default:
|
||||
return "", fmt.Errorf("expected string, format %d", p.Format)
|
||||
}
|
||||
}
|
||||
|
||||
// Value32 returns the bit pattern of a DWORD or LONG value. Callers decide the
|
||||
// signedness, matching the WMI type of the property they publish.
|
||||
func (p Property) Value32() (uint32, error) {
|
||||
if (p.Format != formatDWORD && p.Format != formatLong) || len(p.Data) != 4 {
|
||||
return 0, fmt.Errorf("expected DWORD or LONG, format %d, length %d", p.Format, len(p.Data))
|
||||
}
|
||||
|
||||
return binary.LittleEndian.Uint32(p.Data), nil
|
||||
}
|
||||
|
||||
// ParseProperties decodes CLUSPROP_LIST without casting untrusted bytes to native
|
||||
// structs. Headers and payloads are DWORD aligned; cbLength is measured in bytes.
|
||||
// Only the structure is validated: string contents are decoded when used, so one
|
||||
// unusual value does not discard the whole list.
|
||||
// https://learn.microsoft.com/en-us/previous-versions/windows/desktop/mscs/property-lists
|
||||
func ParseProperties(data []byte) (map[string][]Property, error) {
|
||||
if len(data) < 4 {
|
||||
return nil, errors.New("property list is missing its count")
|
||||
}
|
||||
|
||||
count := binary.LittleEndian.Uint32(data)
|
||||
|
||||
data = data[4:]
|
||||
if uint64(count) > uint64(len(data))/16 {
|
||||
return nil, errors.New("property count exceeds buffer")
|
||||
}
|
||||
|
||||
properties := make(map[string][]Property, count)
|
||||
|
||||
for range count {
|
||||
if len(data) < 8 || binary.LittleEndian.Uint32(data) != propertyName {
|
||||
return nil, errors.New("invalid property name header")
|
||||
}
|
||||
|
||||
nameBytes, rest, err := propertyPayload(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
name := decodeString(nameBytes)
|
||||
data = rest
|
||||
values := make([]Property, 0, 1)
|
||||
|
||||
for {
|
||||
if len(data) < 4 {
|
||||
return nil, fmt.Errorf("missing endmark for %q", name)
|
||||
}
|
||||
|
||||
syntax := binary.LittleEndian.Uint32(data)
|
||||
if syntax == 0 {
|
||||
data = data[4:]
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
if syntax&0xffff0000 != propertyValue {
|
||||
return nil, fmt.Errorf("invalid value syntax %#x for %q", syntax, name)
|
||||
}
|
||||
|
||||
payload, rest, err := propertyPayload(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("property %q: %w", name, err)
|
||||
}
|
||||
|
||||
format := uint16(syntax)
|
||||
if (format == formatDWORD || format == formatLong) && len(payload) != 4 {
|
||||
return nil, fmt.Errorf("invalid DWORD length for %q", name)
|
||||
}
|
||||
|
||||
values = append(values, Property{Format: format, Data: payload})
|
||||
data = rest
|
||||
}
|
||||
|
||||
// Keep the first occurrence; a repeated name does not affect the structure.
|
||||
if _, exists := properties[name]; !exists {
|
||||
properties[name] = values
|
||||
}
|
||||
}
|
||||
|
||||
// Lists returned by the cluster service end with a CLUSPROP_SYNTAX_ENDMARK
|
||||
// after the last value list. Accept zero padding, reject anything else.
|
||||
for _, b := range data {
|
||||
if b != 0 {
|
||||
return nil, fmt.Errorf("%d bytes of trailing property list data starting with % x", len(data), data[:min(len(data), 16)])
|
||||
}
|
||||
}
|
||||
|
||||
return properties, nil
|
||||
}
|
||||
|
||||
func propertyPayload(data []byte) ([]byte, []byte, error) {
|
||||
if len(data) < 8 {
|
||||
return nil, nil, errors.New("truncated property header")
|
||||
}
|
||||
|
||||
length := uint64(binary.LittleEndian.Uint32(data[4:]))
|
||||
|
||||
aligned := (length + 3) &^ uint64(3)
|
||||
if aligned > uint64(len(data)-8) {
|
||||
return nil, nil, errors.New("property payload exceeds buffer")
|
||||
}
|
||||
|
||||
return data[8 : 8+length], data[8+aligned:], nil
|
||||
}
|
||||
|
||||
// decodeString decodes little-endian UTF-16 bytes. A trailing odd byte is ignored.
|
||||
func decodeString(data []byte) string {
|
||||
units := make([]uint16, len(data)/2)
|
||||
for i := range units {
|
||||
units[i] = binary.LittleEndian.Uint16(data[i*2:])
|
||||
}
|
||||
|
||||
return decodeUnits(units)
|
||||
}
|
||||
|
||||
// decodeUnits ends the string at the first NUL and replaces invalid UTF-16 with
|
||||
// U+FFFD, matching the strings WMI returned for the same names.
|
||||
func decodeUnits(units []uint16) string {
|
||||
for i, unit := range units {
|
||||
if unit == 0 {
|
||||
units = units[:i]
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return string(utf16.Decode(units))
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Copyright The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package clusapi
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// CLUSPROP_FORMAT values that the collectors do not decode.
|
||||
const (
|
||||
formatBinary = 1
|
||||
formatMultiString = 5
|
||||
formatULargeInteger = 6
|
||||
formatFileTime = 12
|
||||
)
|
||||
|
||||
// Property lists in testdata were captured with TestDumpPropertyLists from the
|
||||
// CI cluster (Windows Server 2022). Unlike lists built from the documentation,
|
||||
// they end with the extra CLUSPROP_SYNTAX_ENDMARK of the cluster service.
|
||||
func readPropertyFixture(tb testing.TB, name string) []byte {
|
||||
tb.Helper()
|
||||
|
||||
text, err := os.ReadFile(filepath.Join("testdata", name))
|
||||
if err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
|
||||
data, err := hex.DecodeString(strings.Join(strings.Fields(string(text)), ""))
|
||||
if err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
type fixtureValue struct {
|
||||
format uint16
|
||||
length int
|
||||
dword *uint32
|
||||
text *string
|
||||
}
|
||||
|
||||
func dwordValue(value uint32) fixtureValue {
|
||||
return fixtureValue{format: formatDWORD, length: 4, dword: &value}
|
||||
}
|
||||
|
||||
func stringValue(value string, length int) fixtureValue {
|
||||
return fixtureValue{format: formatString, length: length, text: &value}
|
||||
}
|
||||
|
||||
func checkFixtureProperties(t *testing.T, properties map[string][]Property, count int, want map[string]fixtureValue) {
|
||||
t.Helper()
|
||||
|
||||
if len(properties) != count {
|
||||
t.Errorf("properties = %d, want %d", len(properties), count)
|
||||
}
|
||||
|
||||
for name, value := range want {
|
||||
values := properties[name]
|
||||
if len(values) != 1 || values[0].Format != value.format || len(values[0].Data) != value.length {
|
||||
t.Errorf("%s = %+v, want format %d with %d bytes", name, values, value.format, value.length)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if value.dword != nil {
|
||||
if got, err := values[0].DWORD(); err != nil || got != *value.dword {
|
||||
t.Errorf("%s = %d, %v, want %d", name, got, err, *value.dword)
|
||||
}
|
||||
}
|
||||
|
||||
if value.text != nil {
|
||||
if got, err := values[0].String(); err != nil || got != *value.text {
|
||||
t.Errorf("%s = %q, %v, want %q", name, got, err, *value.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePropertiesCapturedResourceLists(t *testing.T) {
|
||||
// Read-only common properties: an SZ whose 38 bytes need 2 bytes of
|
||||
// padding, ULARGE_INTEGER values and a DWORD.
|
||||
properties, err := ParseProperties(readPropertyFixture(t, "resource_ro_common_generic_service.hex"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
checkFixtureProperties(t, properties, 6, map[string]fixtureValue{
|
||||
"Name": stringValue("CI Generic Service", 38),
|
||||
"MonitorProcessId": dwordValue(9092),
|
||||
"StatusInformation": {format: formatULargeInteger, length: 8},
|
||||
"LastOperationStatusCode": {format: formatULargeInteger, length: 8},
|
||||
"ResourceSpecificData1": {format: formatULargeInteger, length: 8},
|
||||
"ResourceSpecificData2": {format: formatULargeInteger, length: 8},
|
||||
})
|
||||
|
||||
// Common properties: the resource type name the collector publishes, empty
|
||||
// strings (2 bytes, padded) and the DWORD values.
|
||||
properties, err = ParseProperties(readPropertyFixture(t, "resource_common_generic_service.hex"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
checkFixtureProperties(t, properties, 15, map[string]fixtureValue{
|
||||
"Type": stringValue("Generic Service", 32),
|
||||
"Description": stringValue("", 2),
|
||||
"ResourceSpecificStatus": stringValue("", 2),
|
||||
"DeadlockTimeout": dwordValue(300000),
|
||||
"EmbeddedFailureAction": dwordValue(2),
|
||||
"IsAlivePollInterval": dwordValue(^uint32(0)),
|
||||
"LooksAlivePollInterval": dwordValue(^uint32(0)),
|
||||
"PendingTimeout": dwordValue(180000),
|
||||
"PersistentState": dwordValue(1),
|
||||
"RestartAction": dwordValue(2),
|
||||
"RestartDelay": dwordValue(500),
|
||||
"RestartPeriod": dwordValue(600000),
|
||||
"RestartThreshold": dwordValue(1),
|
||||
"RetryPeriodOnFailure": dwordValue(600000),
|
||||
"SeparateMonitor": dwordValue(0),
|
||||
})
|
||||
|
||||
// Read-only private properties of an IP Address: FILETIME values.
|
||||
properties, err = ParseProperties(readPropertyFixture(t, "resource_ro_private_ip_address.hex"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
checkFixtureProperties(t, properties, 5, map[string]fixtureValue{
|
||||
"LeaseObtainedTime": {format: formatFileTime, length: 8},
|
||||
"LeaseExpiresTime": {format: formatFileTime, length: 8},
|
||||
"DhcpServer": stringValue("255.255.255.255", 32),
|
||||
"DhcpAddress": stringValue("0.0.0.0", 16),
|
||||
"DhcpSubnetMask": stringValue("255.0.0.0", 20),
|
||||
})
|
||||
}
|
||||
|
||||
func TestParsePropertiesCapturedClusterList(t *testing.T) {
|
||||
// The cluster common list is larger than the 4 KiB first buffer and holds
|
||||
// BINARY security descriptors, MULTI_SZ lists, ULARGE_INTEGER and FILETIME
|
||||
// values next to 64 DWORDs.
|
||||
data := readPropertyFixture(t, "cluster_common.hex")
|
||||
if len(data) <= propertyListBufferSize {
|
||||
t.Fatalf("fixture has %d bytes, want more than the first buffer", len(data))
|
||||
}
|
||||
|
||||
properties, err := ParseProperties(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
checkFixtureProperties(t, properties, 74, map[string]fixtureValue{
|
||||
"Description": stringValue("", 2),
|
||||
"PreferredSite": stringValue("", 2),
|
||||
"Security Descriptor": {format: formatBinary, length: 396},
|
||||
"SharedVolumeSecurityDescriptor": {format: formatBinary, length: 280},
|
||||
"SharedVolumeCompatibleFilters": {format: formatMultiString, length: 2},
|
||||
"SharedVolumeIncompatibleFilters": {format: formatMultiString, length: 2},
|
||||
"EnabledEventLogs": {format: formatMultiString, length: 354},
|
||||
"DumpPolicy": {format: formatULargeInteger, length: 8},
|
||||
"S2DCacheBehavior": {format: formatULargeInteger, length: 8},
|
||||
"RecentEventsResetTime": {format: formatFileTime, length: 8},
|
||||
"ClusterEnforcedAntiaffinity": {format: formatDWORD, length: 4},
|
||||
})
|
||||
|
||||
dwords := 0
|
||||
|
||||
for _, values := range properties {
|
||||
if _, err := values[0].Value32(); err == nil {
|
||||
dwords++
|
||||
}
|
||||
}
|
||||
|
||||
if dwords != 64 {
|
||||
t.Errorf("32-bit values = %d, want 64", dwords)
|
||||
}
|
||||
|
||||
// Every truncation of a real list must be rejected without a panic.
|
||||
for _, name := range []string{"resource_ro_common_generic_service.hex", "resource_common_generic_service.hex"} {
|
||||
data := readPropertyFixture(t, name)
|
||||
for index := range len(data) - 4 {
|
||||
if _, err := ParseProperties(data[:index]); err == nil {
|
||||
t.Errorf("%s: accepted truncation at %d", name, index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzParsePropertiesCaptured(f *testing.F) {
|
||||
for _, name := range []string{"resource_ro_common_generic_service.hex", "resource_common_generic_service.hex", "resource_ro_private_ip_address.hex", "cluster_common.hex"} {
|
||||
f.Add(readPropertyFixture(f, name))
|
||||
}
|
||||
|
||||
f.Fuzz(func(t *testing.T, data []byte) {
|
||||
properties, err := ParseProperties(data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, values := range properties {
|
||||
for _, value := range values {
|
||||
_, _ = value.String()
|
||||
_, _ = value.Value32()
|
||||
|
||||
if (value.Format == formatDWORD || value.Format == formatLong) && len(value.Data) != 4 {
|
||||
t.Fatalf("32-bit value with %d bytes", len(value.Data))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Copyright The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package clusapi
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
"unicode/utf16"
|
||||
)
|
||||
|
||||
func testPropertyList() []byte {
|
||||
data := binary.LittleEndian.AppendUint32(nil, 1)
|
||||
|
||||
name := make([]byte, 0)
|
||||
for _, unit := range utf16.Encode([]rune("NodeWeight\x00")) {
|
||||
name = binary.LittleEndian.AppendUint16(name, unit)
|
||||
}
|
||||
|
||||
data = binary.LittleEndian.AppendUint32(data, propertyName)
|
||||
data = binary.LittleEndian.AppendUint32(data, uint32(len(name)))
|
||||
|
||||
data = append(data, name...)
|
||||
for len(data)%4 != 0 {
|
||||
data = append(data, 0)
|
||||
}
|
||||
|
||||
data = binary.LittleEndian.AppendUint32(data, propertyValue|formatDWORD)
|
||||
data = binary.LittleEndian.AppendUint32(data, 4)
|
||||
data = binary.LittleEndian.AppendUint32(data, ^uint32(0))
|
||||
|
||||
return binary.LittleEndian.AppendUint32(data, 0)
|
||||
}
|
||||
|
||||
func TestParseProperties(t *testing.T) {
|
||||
data := testPropertyList()
|
||||
|
||||
properties, err := ParseProperties(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
values := properties["NodeWeight"]
|
||||
if len(values) != 1 {
|
||||
t.Fatalf("values = %v", values)
|
||||
}
|
||||
|
||||
value, err := values[0].DWORD()
|
||||
if err != nil || value != ^uint32(0) {
|
||||
t.Fatalf("value = %d, err = %v", value, err)
|
||||
}
|
||||
|
||||
for i := range data {
|
||||
if _, err := ParseProperties(data[:i]); err == nil {
|
||||
t.Errorf("accepted truncation at %d", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePropertiesInvalid(t *testing.T) {
|
||||
tests := map[string]func([]byte){
|
||||
"count": func(data []byte) { binary.LittleEndian.PutUint32(data, ^uint32(0)) },
|
||||
"syntax": func(data []byte) { binary.LittleEndian.PutUint32(data[4:], propertyValue|formatString) },
|
||||
"length": func(data []byte) { binary.LittleEndian.PutUint32(data[8:], ^uint32(0)) },
|
||||
"dword_length": func(data []byte) { binary.LittleEndian.PutUint32(data[40:], 3) },
|
||||
"endmark": func(data []byte) { binary.LittleEndian.PutUint32(data[len(data)-4:], propertyValue) },
|
||||
}
|
||||
for name, mutate := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
data := testPropertyList()
|
||||
mutate(data)
|
||||
|
||||
if _, err := ParseProperties(data); err == nil {
|
||||
t.Fatal("accepted invalid property list")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for _, trailing := range [][]byte{{1}, {1, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0x80}} {
|
||||
if _, err := ParseProperties(append(testPropertyList(), trailing...)); err == nil {
|
||||
t.Fatalf("accepted trailing data % x", trailing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bounds safety does not depend on the trailer, so any zero padding is accepted.
|
||||
func TestParsePropertiesZeroTrailer(t *testing.T) {
|
||||
for _, trailing := range [][]byte{{0}, {0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0}} {
|
||||
properties, err := ParseProperties(append(testPropertyList(), trailing...))
|
||||
if err != nil {
|
||||
t.Fatalf("rejected zero trailer % x: %v", trailing, err)
|
||||
}
|
||||
|
||||
if _, exists := properties["NodeWeight"]; !exists {
|
||||
t.Fatalf("lost property with zero trailer % x", trailing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// String contents are not validated while parsing. They decode like WMI strings:
|
||||
// up to the first NUL, with invalid UTF-16 replaced by U+FFFD.
|
||||
func TestParsePropertiesLenientStrings(t *testing.T) {
|
||||
data := testPropertyList()
|
||||
binary.LittleEndian.PutUint16(data[12:], 0xd800)
|
||||
|
||||
properties, err := ParseProperties(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, exists := properties["�odeWeight"]; !exists {
|
||||
t.Fatalf("unpaired surrogate was not replaced: %v", properties)
|
||||
}
|
||||
|
||||
data = testPropertyList()
|
||||
binary.LittleEndian.PutUint16(data[20:], 0)
|
||||
|
||||
properties, err = ParseProperties(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, exists := properties["Node"]; !exists {
|
||||
t.Fatalf("embedded NUL did not end the name: %v", properties)
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
value Property
|
||||
want string
|
||||
}{
|
||||
{Property{Format: formatString, Data: []byte{'a', 0, 0, 0}}, "a"},
|
||||
{Property{Format: formatExpandString, Data: []byte{'a', 0, 0, 0xd8, 'b', 0}}, "a�b"},
|
||||
{Property{Format: formatExpandedString, Data: []byte{'a', 0, 'b'}}, "a"},
|
||||
{Property{Format: formatString, Data: nil}, ""},
|
||||
} {
|
||||
got, err := tc.value.String()
|
||||
if err != nil || got != tc.want {
|
||||
t.Errorf("String(% x) = %q, %v, want %q", tc.value.Data, got, err, tc.want)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := (Property{Format: formatDWORD, Data: make([]byte, 4)}).String(); err == nil {
|
||||
t.Fatal("decoded DWORD as string")
|
||||
}
|
||||
}
|
||||
|
||||
// The cluster service terminates the whole list with CLUSPROP_SYNTAX_ENDMARK.
|
||||
func TestParsePropertiesListEndmark(t *testing.T) {
|
||||
data := binary.LittleEndian.AppendUint32(testPropertyList(), 0)
|
||||
|
||||
properties, err := ParseProperties(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if value, err := properties["NodeWeight"][0].DWORD(); err != nil || value != ^uint32(0) {
|
||||
t.Fatalf("value = %d, err = %v", value, err)
|
||||
}
|
||||
|
||||
if _, err := ParseProperties(binary.LittleEndian.AppendUint32(nil, 0)); err != nil {
|
||||
t.Fatalf("empty list: %v", err)
|
||||
}
|
||||
|
||||
if _, err := ParseProperties(binary.LittleEndian.AppendUint32(binary.LittleEndian.AppendUint32(nil, 0), 0)); err != nil {
|
||||
t.Fatalf("empty list with endmark: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func testLongPropertyList() []byte {
|
||||
data := testPropertyList()
|
||||
binary.LittleEndian.PutUint32(data[36:], propertyValue|formatLong)
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func TestPropertyValue32(t *testing.T) {
|
||||
properties, err := ParseProperties(testLongPropertyList())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
value := properties["NodeWeight"][0]
|
||||
if _, err := value.DWORD(); err == nil {
|
||||
t.Fatal("LONG accepted as DWORD")
|
||||
}
|
||||
|
||||
bits, err := value.Value32()
|
||||
if err != nil || int32(bits) != -1 {
|
||||
t.Fatalf("LONG bits = %#x, err = %v", bits, err)
|
||||
}
|
||||
|
||||
if _, err := (Property{Format: formatString, Data: []byte{0, 0, 0, 0}}).Value32(); err == nil {
|
||||
t.Fatal("string accepted as 32-bit value")
|
||||
}
|
||||
|
||||
if _, err := (Property{Format: formatLong, Data: []byte{0, 0}}).Value32(); err == nil {
|
||||
t.Fatal("short LONG accepted")
|
||||
}
|
||||
|
||||
invalid := testLongPropertyList()
|
||||
binary.LittleEndian.PutUint32(invalid[40:], 3)
|
||||
|
||||
if _, err := ParseProperties(invalid); err == nil {
|
||||
t.Fatal("accepted LONG with invalid length")
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzParseProperties(f *testing.F) {
|
||||
f.Add(testPropertyList())
|
||||
f.Add(testLongPropertyList())
|
||||
f.Add(binary.LittleEndian.AppendUint32(testPropertyList(), 0))
|
||||
f.Add([]byte{0, 0, 0, 0})
|
||||
f.Fuzz(func(t *testing.T, data []byte) { _, _ = ParseProperties(data) })
|
||||
}
|
||||
|
||||
func BenchmarkParseProperties(b *testing.B) {
|
||||
data := testPropertyList()
|
||||
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
if _, err := ParseProperties(data); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Copyright The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package clusapi
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const (
|
||||
// CLUSCTL_RESOURCE_STORAGE_GET_DISK_INFO_EX, available since Windows
|
||||
// Server 2008. The EX2 variant needs Windows Server 2016 and adds nothing
|
||||
// that the collector publishes.
|
||||
// https://learn.microsoft.com/en-us/previous-versions/windows/desktop/mscs/clusctl-resource-storage-get-disk-info-ex
|
||||
resourceStorageGetDiskInfoEx = 0x010001f1
|
||||
)
|
||||
|
||||
// Partition is one CLUS_PARTITION_INFO_EX entry of a storage class resource.
|
||||
type Partition struct {
|
||||
Resource string
|
||||
DeviceName string
|
||||
VolumeLabel string
|
||||
VolumeGUID windows.GUID
|
||||
TotalBytes uint64
|
||||
FreeBytes uint64
|
||||
}
|
||||
|
||||
// DiskPartitions reads the partitions of every storage class resource, the
|
||||
// source of the MSCluster_DiskPartition WMI class. Resources that do not
|
||||
// support the disk information control code are skipped. Successful
|
||||
// partitions are retained alongside joined failures.
|
||||
func (c *Cluster) DiskPartitions(deadline time.Time) (_ []Partition, resultErr error) {
|
||||
for _, proc := range []*windows.LazyProc{openCluster, openEnum, nextEnum, closeEnum, openResource, closeResource, resourceControl} {
|
||||
if err := proc.Find(); err != nil {
|
||||
return nil, fmt.Errorf("load ClusAPI: %w: %w", errors.ErrUnsupported, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.begin(deadline); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { resultErr = errors.Join(resultErr, c.end()) }()
|
||||
|
||||
if err := c.open(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var partitions []Partition
|
||||
|
||||
err := c.enumerate(enumResource, "resource", deadline, func(name objectName) error {
|
||||
found, err := c.readResourcePartitions(name, deadline)
|
||||
partitions = append(partitions, found...)
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return partitions, err
|
||||
}
|
||||
|
||||
func (c *Cluster) readResourcePartitions(name objectName, deadline time.Time) (_ []Partition, resultErr error) {
|
||||
if err := checkDeadline(deadline); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
handle, err := openObject(openResource, c.handle, name)
|
||||
if errors.Is(err, windows.ERROR_RESOURCE_NOT_FOUND) {
|
||||
return nil, errObjectDeleted
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("OpenClusterResourceEx: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := closeObject(closeResource, handle); err != nil {
|
||||
resultErr = errors.Join(resultErr, fmt.Errorf("CloseClusterResource: %w", err))
|
||||
}
|
||||
}()
|
||||
|
||||
data, err := resourceBuffer(handle, resourceGetClassInfo, 8, deadline)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("class information: %w", err)
|
||||
}
|
||||
|
||||
if len(data) != 8 {
|
||||
return nil, errors.New("invalid resource class information size")
|
||||
}
|
||||
|
||||
if binary.LittleEndian.Uint32(data) != resourceClassStorage {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
data, err = resourceBuffer(handle, resourceStorageGetDiskInfoEx, propertyListBufferSize, deadline)
|
||||
if errors.Is(err, windows.ERROR_INVALID_FUNCTION) || errors.Is(err, windows.ERROR_NOT_SUPPORTED) {
|
||||
// Storage class resources such as storage pools have no disk layout.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("CLUSCTL_RESOURCE_STORAGE_GET_DISK_INFO_EX: %w", err)
|
||||
}
|
||||
|
||||
partitions, err := ParseDiskPartitions(data)
|
||||
for index := range partitions {
|
||||
partitions[index].Resource = name.name
|
||||
}
|
||||
|
||||
return partitions, err
|
||||
}
|
||||
|
||||
// CLUSPROP_SYNTAX_PARTITION_INFO_EX: CLUSPROP_TYPE_PARTITION_INFO_EX (13) with
|
||||
// CLUSPROP_FORMAT_BINARY (1).
|
||||
const syntaxPartitionInfoEx = 13<<16 | 1
|
||||
|
||||
// Byte offsets in CLUS_PARTITION_INFO_EX. MAX_PATH is 260 WCHARs; the
|
||||
// structure is 1160 bytes with or without natural alignment of the
|
||||
// ULARGE_INTEGER members.
|
||||
// https://learn.microsoft.com/en-us/previous-versions/windows/desktop/api/clusapi/ns-clusapi-clus_partition_info_ex
|
||||
const (
|
||||
partitionDeviceNameOffset = 4
|
||||
partitionVolumeLabelOffset = partitionDeviceNameOffset + 260*2
|
||||
partitionTotalSizeOffset = partitionVolumeLabelOffset + 260*2 + 3*4 + 32*2
|
||||
partitionFreeSizeOffset = partitionTotalSizeOffset + 8
|
||||
partitionVolumeGUIDOffset = partitionFreeSizeOffset + 8 + 2*4
|
||||
partitionInfoExSize = partitionVolumeGUIDOffset + 16
|
||||
)
|
||||
|
||||
// ParseDiskPartitions decodes the CLUSPROP_PARTITION_INFO_EX entries of a
|
||||
// CLUSCTL_RESOURCE_STORAGE_GET_DISK_INFO_EX value list. Other entries, such as
|
||||
// the disk signature or size, are skipped.
|
||||
func ParseDiskPartitions(data []byte) ([]Partition, error) {
|
||||
values, err := ParseValueList(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var partitions []Partition
|
||||
|
||||
for _, value := range values {
|
||||
if value.Syntax != syntaxPartitionInfoEx {
|
||||
continue
|
||||
}
|
||||
|
||||
partition, err := parsePartitionInfoEx(value.Data)
|
||||
if err != nil {
|
||||
return partitions, err
|
||||
}
|
||||
|
||||
partitions = append(partitions, partition)
|
||||
}
|
||||
|
||||
return partitions, nil
|
||||
}
|
||||
|
||||
func parsePartitionInfoEx(data []byte) (Partition, error) {
|
||||
if len(data) < partitionInfoExSize {
|
||||
return Partition{}, fmt.Errorf("partition information has %d bytes, want at least %d", len(data), partitionInfoExSize)
|
||||
}
|
||||
|
||||
// Fixed-size WCHAR arrays decode up to their first NUL, like WMI strings.
|
||||
deviceName := decodeString(data[partitionDeviceNameOffset:partitionVolumeLabelOffset])
|
||||
volumeLabel := decodeString(data[partitionVolumeLabelOffset : partitionVolumeLabelOffset+260*2])
|
||||
|
||||
guid := data[partitionVolumeGUIDOffset:partitionInfoExSize]
|
||||
|
||||
return Partition{
|
||||
DeviceName: deviceName,
|
||||
VolumeLabel: volumeLabel,
|
||||
VolumeGUID: windows.GUID{
|
||||
Data1: binary.LittleEndian.Uint32(guid),
|
||||
Data2: binary.LittleEndian.Uint16(guid[4:]),
|
||||
Data3: binary.LittleEndian.Uint16(guid[6:]),
|
||||
Data4: [8]byte(guid[8:16]),
|
||||
},
|
||||
TotalBytes: binary.LittleEndian.Uint64(data[partitionTotalSizeOffset:]),
|
||||
FreeBytes: binary.LittleEndian.Uint64(data[partitionFreeSizeOffset:]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Value is one entry of a CLUSPROP value list.
|
||||
type Value struct {
|
||||
Syntax uint32
|
||||
Data []byte
|
||||
}
|
||||
|
||||
// ParseValueList decodes a value list: CLUSPROP_VALUE headers (syntax and
|
||||
// byte length) with DWORD-aligned payloads, terminated by
|
||||
// CLUSPROP_SYNTAX_ENDMARK. Zero padding after the endmark is accepted.
|
||||
// https://learn.microsoft.com/en-us/previous-versions/windows/desktop/mscs/value-lists
|
||||
func ParseValueList(data []byte) ([]Value, error) {
|
||||
var values []Value
|
||||
|
||||
for {
|
||||
if len(data) < 4 {
|
||||
return nil, errors.New("value list is missing its endmark")
|
||||
}
|
||||
|
||||
syntax := binary.LittleEndian.Uint32(data)
|
||||
if syntax == 0 {
|
||||
for _, b := range data[4:] {
|
||||
if b != 0 {
|
||||
return nil, fmt.Errorf("%d bytes of trailing value list data", len(data)-4)
|
||||
}
|
||||
}
|
||||
|
||||
return values, nil
|
||||
}
|
||||
|
||||
payload, rest, err := propertyPayload(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("value syntax %#x: %w", syntax, err)
|
||||
}
|
||||
|
||||
values = append(values, Value{Syntax: syntax, Data: payload})
|
||||
data = rest
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Copyright The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package clusapi
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
"time"
|
||||
"unicode/utf16"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func putFixedString(data []byte, value string) {
|
||||
for index, unit := range utf16.Encode([]rune(value)) {
|
||||
binary.LittleEndian.PutUint16(data[index*2:], unit)
|
||||
}
|
||||
}
|
||||
|
||||
func testPartitionInfoEx() []byte {
|
||||
data := make([]byte, partitionInfoExSize)
|
||||
putFixedString(data[partitionDeviceNameOffset:], `C:\ClusterStorage\Volume1`)
|
||||
putFixedString(data[partitionVolumeLabelOffset:], "CSV01 ")
|
||||
binary.LittleEndian.PutUint64(data[partitionTotalSizeOffset:], 2<<40)
|
||||
binary.LittleEndian.PutUint64(data[partitionFreeSizeOffset:], 19008585728+12345)
|
||||
|
||||
guid := data[partitionVolumeGUIDOffset:]
|
||||
binary.LittleEndian.PutUint32(guid, 0xd3dbada3)
|
||||
binary.LittleEndian.PutUint16(guid[4:], 0x448f)
|
||||
binary.LittleEndian.PutUint16(guid[6:], 0x4c4c)
|
||||
copy(guid[8:], []byte{0xbe, 0x4d, 0x12, 0x2b, 0x7f, 0x29, 0x60, 0x07})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func appendValue(list []byte, syntax uint32, payload []byte) []byte {
|
||||
list = binary.LittleEndian.AppendUint32(list, syntax)
|
||||
list = binary.LittleEndian.AppendUint32(list, uint32(len(payload)))
|
||||
|
||||
list = append(list, payload...)
|
||||
for len(list)%4 != 0 {
|
||||
list = append(list, 0)
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
// testDiskInfo builds the documented GET_DISK_INFO_EX value list: a disk
|
||||
// signature, a disk size, a partition and the endmark.
|
||||
func testDiskInfo() []byte {
|
||||
list := appendValue(nil, 5<<16|2, []byte{1, 2, 3, 4})
|
||||
list = appendValue(list, 12<<16|6, make([]byte, 8))
|
||||
list = appendValue(list, syntaxPartitionInfoEx, testPartitionInfoEx())
|
||||
|
||||
return binary.LittleEndian.AppendUint32(list, 0)
|
||||
}
|
||||
|
||||
func TestParseDiskPartitions(t *testing.T) {
|
||||
partitions, err := ParseDiskPartitions(testDiskInfo())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(partitions) != 1 {
|
||||
t.Fatalf("partitions = %d", len(partitions))
|
||||
}
|
||||
|
||||
partition := partitions[0]
|
||||
want := windows.GUID{Data1: 0xd3dbada3, Data2: 0x448f, Data3: 0x4c4c, Data4: [8]byte{0xbe, 0x4d, 0x12, 0x2b, 0x7f, 0x29, 0x60, 0x07}}
|
||||
|
||||
if partition.DeviceName != `C:\ClusterStorage\Volume1` || partition.VolumeLabel != "CSV01 " || partition.VolumeGUID != want ||
|
||||
partition.TotalBytes != 2<<40 || partition.FreeBytes != 19008585728+12345 {
|
||||
t.Fatalf("partition = %+v", partition)
|
||||
}
|
||||
|
||||
data := testDiskInfo()
|
||||
for i := range data {
|
||||
if _, err := ParseDiskPartitions(data[:i]); err == nil {
|
||||
t.Errorf("accepted truncation at %d", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDiskPartitionsInvalid(t *testing.T) {
|
||||
short := appendValue(nil, syntaxPartitionInfoEx, make([]byte, partitionInfoExSize-1))
|
||||
if _, err := ParseDiskPartitions(binary.LittleEndian.AppendUint32(short, 0)); err == nil {
|
||||
t.Fatal("accepted short partition information")
|
||||
}
|
||||
|
||||
unterminated := testPartitionInfoEx()
|
||||
for index := partitionVolumeLabelOffset; index < partitionVolumeLabelOffset+260*2; index += 2 {
|
||||
binary.LittleEndian.PutUint16(unterminated[index:], 'A')
|
||||
}
|
||||
|
||||
// Fixed-size names decode like WMI strings: the whole array when it is not
|
||||
// terminated, and invalid UTF-16 replaced with U+FFFD.
|
||||
list := binary.LittleEndian.AppendUint32(appendValue(nil, syntaxPartitionInfoEx, unterminated), 0)
|
||||
if partitions, err := ParseDiskPartitions(list); err != nil || len(partitions) != 1 || len(partitions[0].VolumeLabel) != 260 {
|
||||
t.Fatalf("unterminated volume label: %v, %v", partitions, err)
|
||||
}
|
||||
|
||||
surrogate := testPartitionInfoEx()
|
||||
binary.LittleEndian.PutUint16(surrogate[partitionDeviceNameOffset:], 0xdc00)
|
||||
|
||||
list = binary.LittleEndian.AppendUint32(appendValue(nil, syntaxPartitionInfoEx, surrogate), 0)
|
||||
if partitions, err := ParseDiskPartitions(list); err != nil || len(partitions) != 1 || partitions[0].DeviceName[:3] != "�" {
|
||||
t.Fatalf("unpaired surrogate: %v, %v", partitions, err)
|
||||
}
|
||||
|
||||
oversized := testDiskInfo()
|
||||
binary.LittleEndian.PutUint32(oversized[4:], ^uint32(0))
|
||||
|
||||
if _, err := ParseDiskPartitions(oversized); err == nil {
|
||||
t.Fatal("accepted oversized value length")
|
||||
}
|
||||
|
||||
if _, err := ParseDiskPartitions(append(testDiskInfo(), 1, 0, 0, 0)); err == nil {
|
||||
t.Fatal("accepted trailing data")
|
||||
}
|
||||
|
||||
if partitions, err := ParseDiskPartitions(append(testDiskInfo(), 0, 0, 0, 0)); err != nil || len(partitions) != 1 {
|
||||
t.Fatalf("zero padding: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseValueListEmpty(t *testing.T) {
|
||||
values, err := ParseValueList([]byte{0, 0, 0, 0})
|
||||
if err != nil || len(values) != 0 {
|
||||
t.Fatalf("values = %v, err = %v", values, err)
|
||||
}
|
||||
|
||||
if _, err := ParseValueList(nil); err == nil {
|
||||
t.Fatal("accepted missing endmark")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterClosedDiskPartitions(t *testing.T) {
|
||||
c := &Cluster{}
|
||||
if err := c.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := c.DiskPartitions(time.Time{}); err == nil {
|
||||
t.Fatal("read closed cluster")
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzParseDiskPartitions(f *testing.F) {
|
||||
f.Add(testDiskInfo())
|
||||
f.Add([]byte{0, 0, 0, 0})
|
||||
f.Fuzz(func(t *testing.T, data []byte) { _, _ = ParseDiskPartitions(data) })
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
4a000000030004001c0000004100640064004500760069006300740044006500
|
||||
6c0061007900000002000100040000003c000000000000000300040022000000
|
||||
4200610063006b007500700049006e00500072006f0067007200650073007300
|
||||
0000000002000100040000000000000000000000030004002600000043006c00
|
||||
75007300530076006300480061006e006700540069006d0065006f0075007400
|
||||
0000000002000100040000008700000000000000030004003600000043006c00
|
||||
7500730053007600630052006500670072006f00750070005300740061006700
|
||||
6500540069006d0065006f00750074000000000002000100040000000f000000
|
||||
00000000030004004200000043006c0075007300530076006300520065006700
|
||||
72006f00750070005400690063006b0049006e004d0069006c006c0069007300
|
||||
650063006f006e00640073000000000002000100040000002c01000000000000
|
||||
030004002c00000043006c0075007300740065007200470072006f0075007000
|
||||
5700610069007400440065006c00610079000000020001000400000078000000
|
||||
0000000003000400320000004d0069006e0069006d0075006d00500072006500
|
||||
65006d00700074006f0072005000720069006f00720069007400790000000000
|
||||
0200010004000000010000000000000003000400380000004d0069006e006900
|
||||
6d0075006d004e00650076006500720050007200650065006d00700074005000
|
||||
720069006f00720069007400790000000200010004000000b80b000000000000
|
||||
030004003800000043006c007500730074006500720045006e0066006f007200
|
||||
63006500640041006e007400690061006600660069006e006900740079000000
|
||||
02000100040000000000000000000000030004002000000043006c0075007300
|
||||
7400650072004c006f0067004c006500760065006c0000000200010004000000
|
||||
0300000000000000030004001e00000043006c00750073007400650072004c00
|
||||
6f006700530069007a0065000000000002000100040000000006000000000000
|
||||
0300040022000000430072006f00730073005300750062006e00650074004400
|
||||
65006c0061007900000000000200010004000000e80300000000000003000400
|
||||
2a000000430072006f00730073005300750062006e0065007400540068007200
|
||||
6500730068006f006c0064000000000002000100040000001e00000000000000
|
||||
030004001e000000430072006f00730073005300690074006500440065006c00
|
||||
61007900000000000200010004000000e8030000000000000300040026000000
|
||||
430072006f007300730053006900740065005400680072006500730068006f00
|
||||
6c00640000000000020001000400000014000000000000000300040026000000
|
||||
440065006600610075006c0074004e006500740077006f0072006b0052006f00
|
||||
6c00650000000000020001000400000003000000000000000300040018000000
|
||||
4400650073006300720069007000740069006f006e0000000300010002000000
|
||||
0000000000000000030004002800000045006e00610062006c00650053006800
|
||||
610072006500640056006f006c0075006d006500730000000200010004000000
|
||||
0100000000000000030004004c00000053006800610072006500640056006f00
|
||||
6c0075006d0065005600730073005700720069007400650072004f0070006500
|
||||
72006100740069006f006e00540069006d0065006f0075007400000002000100
|
||||
040000000807000000000000030004005000000055007300650043006c006900
|
||||
65006e0074004100630063006500730073004e006500740077006f0072006b00
|
||||
730046006f00720053006800610072006500640056006f006c0075006d006500
|
||||
7300000002000100040000000200000000000000030004002600000048006100
|
||||
6e0067005200650063006f00760065007200790041006300740069006f006e00
|
||||
000000000200010004000000060000000000000003000400280000004c006f00
|
||||
67005200650073006f00750072006300650043006f006e00740072006f006c00
|
||||
7300000002000100040000000000000000000000030004003400000050006c00
|
||||
75006d00620041006c006c00430072006f00730073005300750062006e006500
|
||||
740052006f007500740065007300000002000100040000000000000000000000
|
||||
0300040032000000510075006f00720075006d00410072006200690074007200
|
||||
6100740069006f006e00540069006d0065004d00610078000000000002000100
|
||||
0400000014000000000000000300040028000000520065007100750065007300
|
||||
74005200650070006c007900540069006d0065006f0075007400000002000100
|
||||
040000003c000000000000000300040020000000530061006d00650053007500
|
||||
62006e0065007400440065006c006100790000000200010004000000e8030000
|
||||
000000000300040028000000530061006d0065005300750062006e0065007400
|
||||
5400680072006500730068006f006c006400000002000100040000001e000000
|
||||
0000000003000400280000005300650063007500720069007400790020004400
|
||||
65007300630072006900700074006f0072000000010001008c01000001001480
|
||||
6c0100007c010000140000001c0000000200080000000000020050010a000000
|
||||
000328000300000001060000000000055000000050b9fca79985e2c9d62a71ee
|
||||
d4a97014fb0f3b080003280003000000010600000000000550000000428438f6
|
||||
a00cf3c69c706cb049cbf7d89a81446f00032800030000000106000000000005
|
||||
50000000dd2ce03f3162e2db4015aef4571a3e22e81bbe5a0003280003000000
|
||||
0106000000000005500000003805864282d831674ea6a91735fcf0b852c3fc27
|
||||
0003280003000000010600000000000550000000da87ba0d52ce1099d8a2ea28
|
||||
71d717cf58b8647f000318000300000001020000000000052000000046020000
|
||||
0003180003000000010200000000000520000000200200000003140001000000
|
||||
0101000000000005140000000003140003000000010100000000000512000000
|
||||
0000280003000000010600000000000550000000e5320fec28bea2922c2935f1
|
||||
6342dc6b5a0a2586010200000000000520000000200200000102000000000005
|
||||
200000002002000000000000030004001c000000530065006300750072006900
|
||||
740079004c006500760065006c00000002000100040000000100000000000000
|
||||
0300040030000000530065006300750072006900740079004c00650076006500
|
||||
6c0046006f007200530074006f00720061006700650000000200010004000000
|
||||
0000000000000000030004003c00000053006800610072006500640056006f00
|
||||
6c0075006d00650043006f006d00700061007400690062006c00650046006900
|
||||
6c00740065007200730000000500010002000000000000000000000003000400
|
||||
4000000053006800610072006500640056006f006c0075006d00650049006e00
|
||||
63006f006d00700061007400690062006c006500460069006c00740065007200
|
||||
7300000005000100020000000000000000000000030004003200000053006800
|
||||
7500740064006f0077006e00540069006d0065006f007500740049006e004d00
|
||||
69006e0075007400650073000000000002000100040000001800000000000000
|
||||
03000400380000005700690074006e0065007300730044006100740061006200
|
||||
61007300650057007200690074006500540069006d0065006f00750074000000
|
||||
02000100040000002c01000000000000030004002e0000005700690074006e00
|
||||
650073007300520065007300740061007200740049006e007400650072007600
|
||||
61006c000000000002000100040000000f00000000000000030004003e000000
|
||||
53006800610072006500640056006f006c0075006d0065005300650063007500
|
||||
7200690074007900440065007300630072006900700074006f00720000000000
|
||||
0100010018010000010004800c0100000000000000000000140000000200f800
|
||||
0600000000002800000000100106000000000005500000006084b9dbdd58ffa7
|
||||
f6a00e57c8f9dc9806ecf3140000280000000010010600000000000550000000
|
||||
072e3960a0dce3ce1fb141401771f6dc02293b63000028000000001001060000
|
||||
00000005500000004c58ea82b5845656286e250c7009c7e741f5d9e100002800
|
||||
000000100106000000000005570000003b3ba0f7f4649ae903ce7141e471f485
|
||||
91d1a94f0000280000000010010600000000000550000000c75b0a946ed5ffc5
|
||||
21b1fa2063a9672379b3d51a0000280000000010010600000000000557000000
|
||||
ac074fae90f9a2122adf50d8df37ff6b1b2b7009010100000000000512000000
|
||||
00000000030004001e00000042006c006f0063006b0043006100630068006500
|
||||
530069007a006500000000000200010004000000000400000000000003000400
|
||||
2600000052006f0075007400650048006900730074006f00720079004c006500
|
||||
6e00670074006800000000000200010004000000280000000000000003000400
|
||||
2a000000440079006e0061006d0069006300510075006f00720075006d004500
|
||||
6e00610062006c00650064000000000002000100040000000100000000000000
|
||||
030004002c00000052006500630065006e0074004500760065006e0074007300
|
||||
52006500730065007400540069006d00650000000c00010008000000f0a270a0
|
||||
c958dd0100000000030004002000000044007200610069006e004f006e005300
|
||||
68007500740064006f0077006e00000002000100040000000100000000000000
|
||||
030004002c000000440061007400610062006100730065005200650061006400
|
||||
570072006900740065004d006f00640065000000020001000400000000000000
|
||||
0000000003000400240000004e00650074006600740049005000530065006300
|
||||
45006e00610062006c0065006400000002000100040000000100000000000000
|
||||
0300040018000000430073007600420061006c0061006e006300650072000000
|
||||
0200010004000000010000000000000003000400280000004d00650073007300
|
||||
6100670065004200750066006600650072004c0065006e006700740068000000
|
||||
0200010004000000320000000000000003000400340000004c006f0077006500
|
||||
7200510075006f00720075006d005000720069006f0072006900740079004e00
|
||||
6f00640065004900640000000200010004000000000000000000000003000400
|
||||
16000000440075006d00700050006f006c006900630079000000000006000100
|
||||
0800000019110152000000000000000003000400180000005300320044004200
|
||||
7500730054007900700065007300000002000100040000000000000000000000
|
||||
03000400220000005300320044004f007000740069006d0069007a0061007400
|
||||
69006f006e007300000000000200010004000000000000000000000003000400
|
||||
2c00000053003200440049004f004c006100740065006e006300790054006800
|
||||
72006500730068006f006c006400000002000100040000001027000000000000
|
||||
030004002a000000530032004400430061006300680065004400650073006900
|
||||
7200650064005300740061007400650000000000020001000400000002000000
|
||||
0000000003000400380000005300320044004300610063006800650046006c00
|
||||
6100730068005200650073006500720076006500500065007200630065006e00
|
||||
7400000002000100040000000000000000000000030004002200000053003200
|
||||
4400430061006300680065004200650068006100760069006f00720000000000
|
||||
0600010008000000580000000000000000000000030004002e00000053003200
|
||||
4400430061006300680065005000610067006500530069007a0065004b004200
|
||||
7900740065007300000000000200010004000000100000000000000003000400
|
||||
2000000052006500730069006c00690065006e00630079004c00650076006500
|
||||
6c00000002000100040000000200000000000000030004003000000052006500
|
||||
730069006c00690065006e0063007900440065006600610075006c0074005000
|
||||
6500720069006f00640000000200010004000000f00000000000000003000400
|
||||
26000000510075006100720061006e00740069006e0065004400750072006100
|
||||
740069006f006e00000000000200010004000000201c00000000000003000400
|
||||
28000000510075006100720061006e00740069006e0065005400680072006500
|
||||
730068006f006c00640000000200010004000000030000000000000003000400
|
||||
1c00000050007200650066006500720072006500640053006900740065000000
|
||||
0300010002000000000000000000000003000400220000004100750074006f00
|
||||
420061006c0061006e006300650072004d006f00640065000000000002000100
|
||||
04000000020000000000000003000400240000004100750074006f0042006100
|
||||
6c0061006e006300650072004c006500760065006c0000000200010004000000
|
||||
010000000000000003000400260000004100750074006f004100730073006900
|
||||
67006e004e006f00640065005300690074006500000000000200010004000000
|
||||
0100000000000000030004002e000000470072006f0075007000440065007000
|
||||
65006e00640065006e0063007900540069006d0065006f007500740000000000
|
||||
02000100040000005802000000000000030004003e000000490067006e006f00
|
||||
72006500500065007200730069007300740065006e0074005300740061007400
|
||||
65004f006e005300740061007200740075007000000000000200010004000000
|
||||
0000000000000000030004002200000050006c006100630065006d0065006e00
|
||||
74004f007000740069006f006e00730000000000020001000400000000000000
|
||||
0000000003000400280000004400650074006500630074004d0061006e006100
|
||||
6700650064004500760065006e00740073000000020001000400000001000000
|
||||
00000000030004003a0000004400650074006500630074004d0061006e006100
|
||||
6700650064004500760065006e00740073005400680072006500730068006f00
|
||||
6c0064000000000002000100040000003c000000000000000300040022000000
|
||||
45006e00610062006c00650064004500760065006e0074004c006f0067007300
|
||||
0000000005000100620100004d006900630072006f0073006f00660074002d00
|
||||
570069006e0064006f00770073002d00480079007000650072002d0056002d00
|
||||
56006d005300770069007400630068002d0044006900610067006e006f007300
|
||||
7400690063002c0034002c003000780046004600460046004600460046004400
|
||||
00004d006900630072006f0073006f00660074002d00570069006e0064006f00
|
||||
770073002d0053004d0042004400690072006500630074002f00440065006200
|
||||
750067002c00340000004d006900630072006f0073006f00660074002d005700
|
||||
69006e0064006f00770073002d0053004d004200530065007200760065007200
|
||||
2f0041006e0061006c00790074006900630000004d006900630072006f007300
|
||||
6f00660074002d00570069006e0064006f00770073002d004b00650072006e00
|
||||
65006c002d004c00690076006500440075006d0070002f0041006e0061006c00
|
||||
7900740069006300000000000000000000000000030004002400000055007300
|
||||
6500520064006d00610046006f007200530074006f0072006100670065000000
|
||||
02000100040000000100000000000000030004002a0000005300650074005300
|
||||
4d004200420061006e006400770069006400740068004c0069006d0069007400
|
||||
0000000002000100040000000100000000000000030004003000000053004d00
|
||||
4200420061006e006400770069006400740068004c0069006d00690074004600
|
||||
6100630074006f00720000000200010004000000c40900000000000003000400
|
||||
340000004d006100780069006d0075006d0050006100720061006c006c006500
|
||||
6c004d006900670072006100740069006f006e00730000000200010004000000
|
||||
0100000000000000030004002000000057007000720053006500730073006900
|
||||
6f006e0043006f0075006e007400000002000100040000000200000000000000
|
||||
030004002c000000570070007200530065007300730069006f006e0043006f00
|
||||
6f006c004f0066006600540069006d0065000000020001000400000058020000
|
||||
0000000000000000
|
||||
@@ -0,0 +1,29 @@
|
||||
0f000000030004002000000044006500610064006c006f0063006b0054006900
|
||||
6d0065006f007500740000000200010004000000e09304000000000003000400
|
||||
180000004400650073006300720069007000740069006f006e00000003000100
|
||||
02000000000000000000000003000400280000004900730041006c0069007600
|
||||
650050006f006c006c0049006e00740065007200760061006c00000002000100
|
||||
04000000ffffffff00000000030004002e0000004c006f006f006b0073004100
|
||||
6c0069007600650050006f006c006c0049006e00740065007200760061006c00
|
||||
000000000200010004000000ffffffff00000000030004001e00000050006500
|
||||
6e00640069006e006700540069006d0065006f00750074000000000002000100
|
||||
0400000020bf0200000000000300040020000000500065007200730069007300
|
||||
740065006e007400530074006100740065000000020001000400000001000000
|
||||
00000000030004001c0000005200650073007400610072007400410063007400
|
||||
69006f006e00000002000100040000000200000000000000030004001a000000
|
||||
5200650073007400610072007400440065006c00610079000000000002000100
|
||||
04000000f401000000000000030004001c000000520065007300740061007200
|
||||
740050006500720069006f00640000000200010004000000c027090000000000
|
||||
0300040022000000520065007300740061007200740054006800720065007300
|
||||
68006f006c006400000000000200010004000000010000000000000003000400
|
||||
2c00000045006d006200650064006400650064004600610069006c0075007200
|
||||
650041006300740069006f006e00000002000100040000000200000000000000
|
||||
030004002a0000005200650074007200790050006500720069006f0064004f00
|
||||
6e004600610069006c00750072006500000000000200010004000000c0270900
|
||||
000000000300040020000000530065007000610072006100740065004d006f00
|
||||
6e00690074006f00720000000200010004000000000000000000000003000400
|
||||
0a0000005400790070006500000000000300010020000000470065006e006500
|
||||
7200690063002000530065007200760069006300650000000000000003000400
|
||||
2e0000005200650073006f007500720063006500530070006500630069006600
|
||||
6900630053007400610074007500730000000000030001000200000000000000
|
||||
0000000000000000
|
||||
@@ -0,0 +1,14 @@
|
||||
06000000030004000a0000004e0061006d006500000000000300010026000000
|
||||
430049002000470065006e006500720069006300200053006500720076006900
|
||||
63006500000000000000000003000400220000004d006f006e00690074006f00
|
||||
7200500072006f00630065007300730049006400000000000200010004000000
|
||||
8423000000000000030004002400000053007400610074007500730049006e00
|
||||
66006f0072006d006100740069006f006e000000060001000800000000000000
|
||||
000000000000000003000400300000004c006100730074004f00700065007200
|
||||
6100740069006f006e0053007400610074007500730043006f00640065000000
|
||||
0600010008000000000000000000000000000000030004002c00000052006500
|
||||
73006f0075007200630065005300700065006300690066006900630044006100
|
||||
7400610031000000060001000800000000000000000000000000000003000400
|
||||
2c0000005200650073006f007500720063006500530070006500630069006600
|
||||
6900630044006100740061003200000006000100080000000000000000000000
|
||||
0000000000000000
|
||||
@@ -0,0 +1,11 @@
|
||||
0500000003000400240000004c0065006100730065004f006200740061006900
|
||||
6e0065006400540069006d00650000000c000100080000000000000000000000
|
||||
0000000003000400220000004c00650061007300650045007800700069007200
|
||||
65007300540069006d006500000000000c000100080000000000000000000000
|
||||
0000000003000400160000004400680063007000530065007200760065007200
|
||||
0000000003000100200000003200350035002e003200350035002e0032003500
|
||||
35002e0032003500350000000000000003000400180000004400680063007000
|
||||
41006400640072006500730073000000030001001000000030002e0030002e00
|
||||
30002e003000000000000000030004001e000000440068006300700053007500
|
||||
62006e00650074004d00610073006b0000000000030001001400000032003500
|
||||
35002e0030002e0030002e00300000000000000000000000
|
||||
Reference in New Issue
Block a user