Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e3bb0ff7b | ||
|
|
462eb4f80c | ||
|
|
ba88e5997a | ||
|
|
f8f9e0c852 |
@@ -4,7 +4,7 @@ Proxmox Virtual Environment Prometheus metrics exporter.
|
||||
|
||||
## Overview
|
||||
|
||||
PVE Exporter is a tool that collects metrics from a Proxmox Virtual Environment cluster and exposes them for Prometheus to scrape. This exporter supports gathering metrics for cluster state, LXC containers, QEMU virtual machines, physical disks, node storage, node status, node subscription details, and software-defined networking (SDN).
|
||||
PVE Exporter is a tool that collects metrics from a Proxmox Virtual Environment cluster and exposes them for Prometheus to scrape. This exporter supports gathering metrics for cluster state, LXC containers, QEMU virtual machines, physical disks, ZFS pools, node storage, node status, node subscription details, and software-defined networking (SDN).
|
||||
|
||||
## Features
|
||||
|
||||
@@ -52,6 +52,7 @@ metrics:
|
||||
ltc: true # Enable collection of LXC container metrics.
|
||||
qemu: true # Enable collection of QEMU virtual machine metrics.
|
||||
disk: true # Enable collection of physical disk metrics.
|
||||
zfs: true # Enable collection of ZFS pool metrics.
|
||||
storage: true # Enable collection of node storage metrics.
|
||||
nodeStatus: true # Enable collection of node status metrics.
|
||||
subscription: true # Enable collection of node subscription details.
|
||||
@@ -61,6 +62,19 @@ metrics:
|
||||
The cluster state collector exposes `pve_cluster_mode`, where `1` means that
|
||||
Proxmox VE is configured as a cluster and `0` means that it is a standalone node.
|
||||
|
||||
The `pve_node_subscription_status` metric uses `0` when no subscription is
|
||||
configured, `1` for an active subscription, and `2` for an expired or otherwise
|
||||
unusable subscription.
|
||||
|
||||
The ZFS collector discovers all pools on every node and exports their recursive
|
||||
topology with `cluster`, `node`, `pool`, `component`, `path`, and `leaf` labels:
|
||||
|
||||
- `pve_node_zfs_state` (`0=UNKNOWN`, `1=ONLINE`, `2=DEGRADED`, `3=FAULTED`,
|
||||
`4=OFFLINE`, `5=REMOVED`, `6=UNAVAIL`)
|
||||
- `pve_node_zfs_read_errors`
|
||||
- `pve_node_zfs_write_errors`
|
||||
- `pve_node_zfs_checksum_errors`
|
||||
|
||||
## Build
|
||||
|
||||
To build the Docker image for PVE Exporter, use the following command:
|
||||
|
||||
+3
-1
@@ -35,6 +35,8 @@ proxmox:
|
||||
qemu: true
|
||||
# Enable collection of physical disk metrics.
|
||||
disk: true
|
||||
# Enable collection of ZFS pool metrics.
|
||||
zfs: true
|
||||
# Enable collection of node storage metrics.
|
||||
storage: true
|
||||
# Enable collection of node status metrics.
|
||||
@@ -42,4 +44,4 @@ proxmox:
|
||||
# Enable collection of node subscription details.
|
||||
subscription: true
|
||||
# Enable collection of software-defined network (SDN) metrics.
|
||||
sdn: true
|
||||
sdn: true
|
||||
|
||||
@@ -27,6 +27,7 @@ type PveMetricsConfiguration struct {
|
||||
LXC bool `yaml:"lxc"` // Enables LXC container metrics collection.
|
||||
QEMU bool `yaml:"qemu"` // Enable QEMU virtual machine metrics collection.
|
||||
Disk bool `yaml:"disk"` // Enable physical disk metrics collection.
|
||||
ZFS bool `yaml:"zfs"` // Enable node ZFS pool metrics collection.
|
||||
Storage bool `yaml:"storage"` // Enable node storage metrics collection.
|
||||
NodeStatus bool `yaml:"nodeStatus"` // Enable node status metrics collection.
|
||||
Subscription bool `yaml:"subscription"` // Enable node subscription detail collection.
|
||||
@@ -62,6 +63,10 @@ func (c *Configuration) Validate() error {
|
||||
if !(u.Scheme == "http" || u.Scheme == "https") {
|
||||
return fmt.Errorf("PVE host '%s' must be protocol type of HTTP or HTTPS.", host)
|
||||
}
|
||||
|
||||
if u.Hostname() == "" {
|
||||
return fmt.Errorf("PVE host '%s' must contain an IP address or DNS name.", host)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate PVE token
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package configuration
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestConfigurationValidateAcceptsIPAndDNSPveHosts(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
host string
|
||||
}{
|
||||
{name: "IP address", host: "https://192.168.0.10:8006"},
|
||||
{name: "DNS name", host: "https://pve.example.com:8006"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
configuration := Configuration{
|
||||
Host: "0.0.0.0",
|
||||
PVE: PveConfiguration{
|
||||
Hosts: []string{tt.host},
|
||||
Token: PveTokenConfiguration{
|
||||
TokenId: "token",
|
||||
Secret: "secret",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := configuration.Validate(); err != nil {
|
||||
t.Fatalf("Validate() rejected %s host %q: %v", tt.name, tt.host, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,11 @@ func NewPveMetricsManager(apiClient *proxmox.PveApiClient, conf *configuration.P
|
||||
c.RegisterCollector(NewPveNodeDiskCollector(apiClient, c.registry))
|
||||
}
|
||||
|
||||
// Node ZFS pool collector.
|
||||
if metricsCf.ZFS {
|
||||
c.RegisterCollector(NewPveNodeZfsCollector(apiClient, c.registry))
|
||||
}
|
||||
|
||||
// Node SDN collector.
|
||||
if metricsCf.SDN {
|
||||
c.RegisterCollector(NewPveSdnCollector(apiClient, c.registry))
|
||||
|
||||
@@ -39,7 +39,7 @@ func NewPveSubscriptionCollector(apiClient *proxmox.PveApiClient, registry *TTLR
|
||||
c.status = NewTTLGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_node_subscription_status",
|
||||
Help: "Node subscription status.",
|
||||
Help: "Node subscription status (0 = not found, 1 = active, 2 = expired or otherwise unusable).",
|
||||
},
|
||||
[]string{"cluster", "node"},
|
||||
5*time.Minute,
|
||||
@@ -111,7 +111,7 @@ func (c *PveSubscriptionCollector) CollectMetrics() error {
|
||||
c.info.With(subsLabels).Set(1)
|
||||
|
||||
// Subscription state.
|
||||
c.status.With(labels).Set(subscription.GetActiveNumeric())
|
||||
c.status.With(labels).Set(subscription.GetStatusNumeric())
|
||||
|
||||
// Subscription sockets count.
|
||||
c.sockets.With(labels).Set(float64(subscription.Sockets))
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"lostak.dev/pve-exporter/proxmox"
|
||||
)
|
||||
|
||||
// PveNodeZfsCollector collects ZFS pool and component health metrics.
|
||||
type PveNodeZfsCollector struct {
|
||||
apiClient *proxmox.PveApiClient
|
||||
registry *TTLRegistry
|
||||
|
||||
state *TTLGaugeVec
|
||||
readErrors *TTLGaugeVec
|
||||
writeErrors *TTLGaugeVec
|
||||
checksumErrors *TTLGaugeVec
|
||||
}
|
||||
|
||||
// zfsMetricComponent is a flattened entry from the recursive ZFS topology.
|
||||
type zfsMetricComponent struct {
|
||||
proxmox.PveZfsComponent
|
||||
Path string
|
||||
}
|
||||
|
||||
var zfsStateValues = map[string]float64{
|
||||
"ONLINE": 1,
|
||||
"DEGRADED": 2,
|
||||
"FAULTED": 3,
|
||||
"OFFLINE": 4,
|
||||
"REMOVED": 5,
|
||||
"UNAVAIL": 6,
|
||||
}
|
||||
|
||||
// NewPveNodeZfsCollector creates a ZFS metrics collector.
|
||||
func NewPveNodeZfsCollector(apiClient *proxmox.PveApiClient, registry *TTLRegistry) *PveNodeZfsCollector {
|
||||
c := PveNodeZfsCollector{apiClient: apiClient, registry: registry}
|
||||
componentLabelNames := []string{"cluster", "node", "pool", "component", "path", "leaf"}
|
||||
|
||||
c.state = NewTTLGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_node_zfs_state",
|
||||
Help: "ZFS pool component state (0 = UNKNOWN, 1 = ONLINE, 2 = DEGRADED, 3 = FAULTED, 4 = OFFLINE, 5 = REMOVED, 6 = UNAVAIL).",
|
||||
},
|
||||
componentLabelNames,
|
||||
5*time.Minute,
|
||||
)
|
||||
c.registry.Register(c.state)
|
||||
|
||||
c.readErrors = NewTTLGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_node_zfs_read_errors",
|
||||
Help: "ZFS pool component read error count.",
|
||||
},
|
||||
componentLabelNames,
|
||||
5*time.Minute,
|
||||
)
|
||||
c.registry.Register(c.readErrors)
|
||||
|
||||
c.writeErrors = NewTTLGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_node_zfs_write_errors",
|
||||
Help: "ZFS pool component write error count.",
|
||||
},
|
||||
componentLabelNames,
|
||||
5*time.Minute,
|
||||
)
|
||||
c.registry.Register(c.writeErrors)
|
||||
|
||||
c.checksumErrors = NewTTLGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "pve_node_zfs_checksum_errors",
|
||||
Help: "ZFS pool component checksum error count.",
|
||||
},
|
||||
componentLabelNames,
|
||||
5*time.Minute,
|
||||
)
|
||||
c.registry.Register(c.checksumErrors)
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
// CollectMetrics implements PveMetricsCollector.
|
||||
func (c *PveNodeZfsCollector) CollectMetrics() error {
|
||||
cluster, err := c.apiClient.GetClusterStatus()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, node := range cluster.NodeStatuses {
|
||||
pools, err := c.apiClient.GetNodeZfsPools(node.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, pool := range *pools {
|
||||
status, err := c.apiClient.GetNodeZfsPoolStatus(node.Name, pool.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, component := range flattenZfsComponents(status) {
|
||||
labels := prometheus.Labels{
|
||||
"cluster": cluster.GetClusterName(),
|
||||
"node": node.Name,
|
||||
"pool": pool.Name,
|
||||
"component": component.Name,
|
||||
"path": component.Path,
|
||||
"leaf": strconv.FormatBool(component.Leaf == 1),
|
||||
}
|
||||
|
||||
if component.State != "" {
|
||||
c.state.With(labels).Set(zfsStateNumeric(component.State))
|
||||
}
|
||||
if component.Read != nil {
|
||||
c.readErrors.With(labels).Set(float64(*component.Read))
|
||||
}
|
||||
if component.Write != nil {
|
||||
c.writeErrors.With(labels).Set(float64(*component.Write))
|
||||
}
|
||||
if component.Checksum != nil {
|
||||
c.checksumErrors.With(labels).Set(float64(*component.Checksum))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetName implements PveMetricsCollector.
|
||||
func (c *PveNodeZfsCollector) GetName() string {
|
||||
return "Node ZFS"
|
||||
}
|
||||
|
||||
func flattenZfsComponents(status *proxmox.PveZfsPoolStatus) []zfsMetricComponent {
|
||||
components := make([]zfsMetricComponent, 0)
|
||||
appendZfsComponent(&components, status.PveZfsComponent, nil)
|
||||
return components
|
||||
}
|
||||
|
||||
func appendZfsComponent(components *[]zfsMetricComponent, component proxmox.PveZfsComponent, parents []string) {
|
||||
pathParts := append(append([]string{}, parents...), component.Name)
|
||||
*components = append(*components, zfsMetricComponent{
|
||||
PveZfsComponent: component,
|
||||
Path: strings.Join(pathParts, " > "),
|
||||
})
|
||||
|
||||
for _, child := range component.Children {
|
||||
appendZfsComponent(components, child, pathParts)
|
||||
}
|
||||
}
|
||||
|
||||
func zfsStateNumeric(state string) float64 {
|
||||
if value, ok := zfsStateValues[strings.ToUpper(state)]; ok {
|
||||
return value
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"lostak.dev/pve-exporter/proxmox"
|
||||
)
|
||||
|
||||
func TestFlattenZfsComponents(t *testing.T) {
|
||||
zero := uint64(0)
|
||||
status := &proxmox.PveZfsPoolStatus{
|
||||
PveZfsComponent: proxmox.PveZfsComponent{
|
||||
Name: "hdd",
|
||||
State: "ONLINE",
|
||||
Children: []proxmox.PveZfsComponent{
|
||||
{
|
||||
Name: "hdd",
|
||||
Children: []proxmox.PveZfsComponent{
|
||||
{
|
||||
Name: "raidz1-0",
|
||||
State: "ONLINE",
|
||||
Read: &zero,
|
||||
Write: &zero,
|
||||
Checksum: &zero,
|
||||
Children: []proxmox.PveZfsComponent{
|
||||
{
|
||||
Name: "/dev/disk/by-id/disk-1-part1",
|
||||
State: "ONLINE",
|
||||
Leaf: 1,
|
||||
Read: &zero,
|
||||
Write: &zero,
|
||||
Checksum: &zero,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
components := flattenZfsComponents(status)
|
||||
if len(components) != 4 {
|
||||
t.Fatalf("flattenZfsComponents() returned %d components, want 4", len(components))
|
||||
}
|
||||
|
||||
leaf := components[3]
|
||||
wantPath := "hdd > hdd > raidz1-0 > /dev/disk/by-id/disk-1-part1"
|
||||
if leaf.Path != wantPath {
|
||||
t.Fatalf("leaf path = %q, want %q", leaf.Path, wantPath)
|
||||
}
|
||||
if leaf.Leaf != 1 || leaf.Read == nil || leaf.Write == nil || leaf.Checksum == nil {
|
||||
t.Fatal("leaf component lost its type or error counters")
|
||||
}
|
||||
}
|
||||
|
||||
func TestZfsStateNumeric(t *testing.T) {
|
||||
tests := []struct {
|
||||
state string
|
||||
want float64
|
||||
}{
|
||||
{state: "UNKNOWN", want: 0},
|
||||
{state: "ONLINE", want: 1},
|
||||
{state: "DEGRADED", want: 2},
|
||||
{state: "FAULTED", want: 3},
|
||||
{state: "OFFLINE", want: 4},
|
||||
{state: "REMOVED", want: 5},
|
||||
{state: "UNAVAIL", want: 6},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.state, func(t *testing.T) {
|
||||
if got := zfsStateNumeric(tt.state); got != tt.want {
|
||||
t.Fatalf("zfsStateNumeric(%q) = %v, want %v", tt.state, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -55,7 +56,7 @@ func NewApiClient(endpoints []string, tokenId string, secret string, checkInterv
|
||||
// Prepare API endpoints.
|
||||
for _, endpoint := range endpoints {
|
||||
apiEndpoint := ApiEndpoint{
|
||||
host: endpoint,
|
||||
host: normalizeApiHost(endpoint),
|
||||
alive: false,
|
||||
}
|
||||
instance.endpoints = append(instance.endpoints, &apiEndpoint)
|
||||
@@ -86,6 +87,11 @@ func NewApiClient(endpoints []string, tokenId string, secret string, checkInterv
|
||||
return &instance
|
||||
}
|
||||
|
||||
// normalizeApiHost ensures API paths can be appended to either IP- or DNS-based hosts.
|
||||
func normalizeApiHost(host string) string {
|
||||
return strings.TrimRight(strings.TrimSpace(host), "/") + "/"
|
||||
}
|
||||
|
||||
// Check endpoint liveness state.
|
||||
func (instance *ApiClient) checkEndpointsLiveness() {
|
||||
// We want to make sure other routines won't make any requests until we have checked for alive connections.
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package proxmox
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewApiClientNormalizesEndpointHosts(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
host string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "IP without trailing slash",
|
||||
host: "https://192.168.0.10:8006",
|
||||
want: "https://192.168.0.10:8006/",
|
||||
},
|
||||
{
|
||||
name: "IP with trailing slash",
|
||||
host: "https://192.168.0.10:8006/",
|
||||
want: "https://192.168.0.10:8006/",
|
||||
},
|
||||
{
|
||||
name: "DNS without trailing slash",
|
||||
host: "https://pve.example.com:8006",
|
||||
want: "https://pve.example.com:8006/",
|
||||
},
|
||||
{
|
||||
name: "DNS with trailing slash",
|
||||
host: "https://pve.example.com:8006/",
|
||||
want: "https://pve.example.com:8006/",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client := NewApiClient([]string{tt.host}, "token", "secret", time.Second)
|
||||
defer client.httpClient.CloseIdleConnections()
|
||||
|
||||
if got := client.endpoints[0].host; got != tt.want {
|
||||
t.Fatalf("normalized endpoint host = %q, want %q", got, tt.want)
|
||||
}
|
||||
if got := client.endpoints[0].host + "api2/json/"; got != tt.want+"api2/json/" {
|
||||
t.Fatalf("liveness URL = %q, want %q", got, tt.want+"api2/json/")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+44
-5
@@ -223,6 +223,40 @@ type PveDisk struct {
|
||||
Used string `json:"used,omitempty"` // How the drive is used (optional field).
|
||||
}
|
||||
|
||||
// PveZfsPool represents summary information about a ZFS pool.
|
||||
type PveZfsPool struct {
|
||||
Name string `json:"name"` // Pool name.
|
||||
Size uint64 `json:"size"` // Total pool size in bytes.
|
||||
Alloc uint64 `json:"alloc"` // Allocated pool space in bytes.
|
||||
Free uint64 `json:"free"` // Free pool space in bytes.
|
||||
Frag uint64 `json:"frag"` // Pool fragmentation percentage.
|
||||
Dedup float64 `json:"dedup"` // Pool deduplication ratio.
|
||||
Health string `json:"health"` // Pool health state.
|
||||
}
|
||||
|
||||
// PveZfsComponent represents a component in the recursive ZFS pool topology.
|
||||
// Error counters are pointers because section nodes such as "cache" do not
|
||||
// always contain read, write, or checksum values in the PVE API response.
|
||||
type PveZfsComponent struct {
|
||||
Name string `json:"name"` // Component, vdev, or device name.
|
||||
State string `json:"state"` // Component state, for example ONLINE.
|
||||
Leaf uint8 `json:"leaf"` // Whether this component is a leaf device.
|
||||
Read *uint64 `json:"read"` // Read error count when available.
|
||||
Write *uint64 `json:"write"` // Write error count when available.
|
||||
Checksum *uint64 `json:"cksum"` // Checksum error count when available.
|
||||
Message string `json:"msg"` // Optional component status message.
|
||||
Children []PveZfsComponent `json:"children"` // Nested ZFS components.
|
||||
}
|
||||
|
||||
// PveZfsPoolStatus represents the detailed status and topology of a ZFS pool.
|
||||
type PveZfsPoolStatus struct {
|
||||
PveZfsComponent
|
||||
Status string `json:"status"` // Human-readable pool status.
|
||||
Action string `json:"action"` // Recommended recovery action.
|
||||
Scan string `json:"scan"` // Last or current scrub information.
|
||||
Errors string `json:"errors"` // Human-readable pool errors.
|
||||
}
|
||||
|
||||
// PVE node time.
|
||||
type PveNodeTime struct {
|
||||
Time uint64 `json:"time"` // Unix timestamp in UTC.
|
||||
@@ -430,13 +464,18 @@ func (r *PveSdnResource) GetStatusNumeric() float64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// GetActiveNumeric returns the numeric state of a subscription.
|
||||
// Returns 1 if the subscription status is "active", otherwise returns 0.
|
||||
func (r *PveSubscription) GetActiveNumeric() float64 {
|
||||
if r.Status == "active" {
|
||||
// GetStatusNumeric returns the numeric state of a subscription.
|
||||
// A missing subscription is 0, an active subscription is 1, and an expired
|
||||
// or otherwise unusable subscription is 2.
|
||||
func (r *PveSubscription) GetStatusNumeric() float64 {
|
||||
switch r.Status {
|
||||
case "notfound":
|
||||
return 0
|
||||
case "active":
|
||||
return 1
|
||||
default:
|
||||
return 2
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// GetSmartPassedState returns the numeric health state of a disk.
|
||||
|
||||
+85
-1
@@ -1,6 +1,9 @@
|
||||
package proxmox
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPveClusterStatusGetClusterModeNumeric(t *testing.T) {
|
||||
tests := []struct {
|
||||
@@ -28,3 +31,84 @@ func TestPveClusterStatusGetClusterModeNumeric(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPveSubscriptionGetStatusNumeric(t *testing.T) {
|
||||
tests := []struct {
|
||||
status string
|
||||
want float64
|
||||
}{
|
||||
{status: "notfound", want: 0},
|
||||
{status: "active", want: 1},
|
||||
{status: "expired", want: 2},
|
||||
{status: "invalid", want: 2},
|
||||
{status: "suspended", want: 2},
|
||||
{status: "new", want: 2},
|
||||
{status: "unknown", want: 2},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.status, func(t *testing.T) {
|
||||
subscription := PveSubscription{Status: tt.status}
|
||||
if got := subscription.GetStatusNumeric(); got != tt.want {
|
||||
t.Fatalf("GetStatusNumeric() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPveZfsPoolStatusUnmarshal(t *testing.T) {
|
||||
payload := []byte(`{
|
||||
"errors": "No known data errors",
|
||||
"state": "ONLINE",
|
||||
"leaf": 0,
|
||||
"name": "hdd",
|
||||
"children": [
|
||||
{
|
||||
"name": "hdd",
|
||||
"state": "ONLINE",
|
||||
"read": 0,
|
||||
"write": 0,
|
||||
"cksum": 0,
|
||||
"children": [
|
||||
{
|
||||
"name": "/dev/disk/by-id/disk-1-part1",
|
||||
"state": "ONLINE",
|
||||
"leaf": 1,
|
||||
"read": 0,
|
||||
"write": 0,
|
||||
"cksum": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "cache",
|
||||
"leaf": 0
|
||||
}
|
||||
]
|
||||
}`)
|
||||
|
||||
var status PveZfsPoolStatus
|
||||
if err := json.Unmarshal(payload, &status); err != nil {
|
||||
t.Fatalf("unable to unmarshal ZFS pool status: %v", err)
|
||||
}
|
||||
|
||||
if status.Name != "hdd" || status.State != "ONLINE" || status.Errors != "No known data errors" {
|
||||
t.Fatalf("unexpected pool status: %+v", status)
|
||||
}
|
||||
if len(status.Children) != 2 || len(status.Children[0].Children) != 1 {
|
||||
t.Fatalf("unexpected ZFS topology: %+v", status.Children)
|
||||
}
|
||||
|
||||
leaf := status.Children[0].Children[0]
|
||||
if leaf.Leaf != 1 || leaf.Read == nil || leaf.Write == nil || leaf.Checksum == nil {
|
||||
t.Fatal("leaf component is missing its type or error counters")
|
||||
}
|
||||
if *leaf.Read != 0 || *leaf.Write != 0 || *leaf.Checksum != 0 {
|
||||
t.Fatalf("unexpected leaf error counters: read=%d write=%d checksum=%d", *leaf.Read, *leaf.Write, *leaf.Checksum)
|
||||
}
|
||||
|
||||
cache := status.Children[1]
|
||||
if cache.Read != nil || cache.Write != nil || cache.Checksum != nil {
|
||||
t.Fatal("missing cache counters must stay absent instead of becoming zero metrics")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
@@ -212,6 +213,37 @@ func (instance *PveApiClient) GetNodeDisksList(node string) (*[]PveDisk, error)
|
||||
return &disks, nil
|
||||
}
|
||||
|
||||
// Get ZFS pools configured on a PVE node.
|
||||
func (instance *PveApiClient) GetNodeZfsPools(node string) (*[]PveZfsPool, error) {
|
||||
res, err := instance.apiClient.PerformRequest("GET", "/nodes/"+node+"/disks/zfs", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var pools []PveZfsPool
|
||||
if err := json.Unmarshal(res.Data, &pools); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &pools, nil
|
||||
}
|
||||
|
||||
// Get detailed status and topology of a ZFS pool on a PVE node.
|
||||
func (instance *PveApiClient) GetNodeZfsPoolStatus(node string, pool string) (*PveZfsPoolStatus, error) {
|
||||
path := "/nodes/" + node + "/disks/zfs/" + url.PathEscape(pool)
|
||||
res, err := instance.apiClient.PerformRequest("GET", path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var status PveZfsPoolStatus
|
||||
if err := json.Unmarshal(res.Data, &status); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &status, nil
|
||||
}
|
||||
|
||||
// Get PVE node time info.
|
||||
func (instance *PveApiClient) GetNodeTime(node string) (*PveNodeTime, error) {
|
||||
res, err := instance.apiClient.PerformRequest("GET", "/nodes/"+node+"/time", nil)
|
||||
|
||||
Reference in New Issue
Block a user