Files
windows_exporter/internal/headers/clusapi/storage.go
T
2026-10-11 13:52:55 +02:00

232 lines
7.1 KiB
Go

// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package clusapi
import (
"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 := c.scratch.resourceControl(handle, resourceGetClassInfo, 8, deadline)
if err != nil {
return nil, fmt.Errorf("class information: %w", err)
}
if class, _ := parseResourceClassInfo(data); class != resourceClassStorage {
return nil, nil
}
data, err = c.scratch.resourceControl(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
}
}