mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Attach ZFS pool status to dir storages on ZFS dataset paths
Back-port v5 fix ae6b663e9 to v6. Removes the 'only fetch ZFS pools when a
zfspool/zfs/local-zfs storage exists' gate (you cannot tell whether a dir
storage sits on a ZFS dataset without the pool list; fetch is gated by
PULSE_DISABLE_ZFS_MONITORING and empty-fast on non-ZFS nodes) and the
type guard before matchZFSPoolForStorage, so a dir storage whose path
resolves to a pool (e.g. /rpool/data) now gets ZFS health attached. The
guardrail-pinned 'if pool := matchZFSPoolForStorage(...)' call form is
preserved. Adds a poll integration test.
This commit is contained in:
@@ -293,36 +293,32 @@ func (m *Monitor) pollStorageWithNodes(ctx context.Context, instanceName string,
|
||||
var zfsPoolMap = make(map[string]*models.ZFSPool)
|
||||
|
||||
if enableZFSMonitoring {
|
||||
hasZFSStorage := false
|
||||
for _, storage := range nodeStorage {
|
||||
if storage.Type == "zfspool" || storage.Type == "zfs" || storage.Type == "local-zfs" {
|
||||
hasZFSStorage = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// Always fetch ZFS pool details when ZFS monitoring is enabled (it
|
||||
// is gated by PULSE_DISABLE_ZFS_MONITORING and returns empty quickly
|
||||
// on non-ZFS nodes). We cannot tell whether a dir-type storage is
|
||||
// backed by a ZFS dataset without the pool list, so a prior
|
||||
// "only fetch when a zfs-type storage exists" gate hid ZFS health
|
||||
// for dir storages on ZFS dataset paths.
|
||||
if poolInfos, err := client.GetZFSPoolsWithDetails(ctx, n.Node); err == nil {
|
||||
log.Debug().
|
||||
Str("node", n.Node).
|
||||
Int("pools", len(poolInfos)).
|
||||
Msg("Successfully fetched ZFS pool details")
|
||||
|
||||
if hasZFSStorage {
|
||||
if poolInfos, err := client.GetZFSPoolsWithDetails(ctx, n.Node); err == nil {
|
||||
log.Debug().
|
||||
Str("node", n.Node).
|
||||
Int("pools", len(poolInfos)).
|
||||
Msg("Successfully fetched ZFS pool details")
|
||||
|
||||
// Convert to our model format
|
||||
for _, poolInfo := range poolInfos {
|
||||
modelPool := convertPoolInfoToModel(&poolInfo)
|
||||
if modelPool != nil {
|
||||
zfsPoolMap[poolInfo.Name] = modelPool
|
||||
}
|
||||
// Convert to our model format
|
||||
for _, poolInfo := range poolInfos {
|
||||
modelPool := convertPoolInfoToModel(&poolInfo)
|
||||
if modelPool != nil {
|
||||
zfsPoolMap[poolInfo.Name] = modelPool
|
||||
}
|
||||
} else {
|
||||
// Log but don't fail - ZFS monitoring is optional
|
||||
log.Debug().
|
||||
Err(err).
|
||||
Str("node", n.Node).
|
||||
Str("instance", instanceName).
|
||||
Msg("Could not get ZFS pool status (may require additional permissions)")
|
||||
}
|
||||
} else {
|
||||
// Log but don't fail - ZFS monitoring is optional
|
||||
log.Debug().
|
||||
Err(err).
|
||||
Str("node", n.Node).
|
||||
Str("instance", instanceName).
|
||||
Msg("Could not get ZFS pool status (may require additional permissions)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,11 +389,11 @@ func (m *Monitor) pollStorageWithNodes(ctx context.Context, instanceName string,
|
||||
}
|
||||
}
|
||||
|
||||
// If this is ZFS storage, attach pool status information
|
||||
if storage.Type == "zfspool" || storage.Type == "zfs" || storage.Type == "local-zfs" {
|
||||
if pool := matchZFSPoolForStorage(modelStorage, zfsPoolMap); pool != nil {
|
||||
modelStorage.ZFSPool = pool
|
||||
}
|
||||
// Attach ZFS pool status whenever the storage name or dataset path
|
||||
// resolves to a known pool, including dir-type storages backed by a
|
||||
// ZFS dataset (not just zfspool/zfs/local-zfs types).
|
||||
if pool := matchZFSPoolForStorage(modelStorage, zfsPoolMap); pool != nil {
|
||||
modelStorage.ZFSPool = pool
|
||||
}
|
||||
|
||||
// Override with cluster config if available, but only when the
|
||||
|
||||
@@ -572,3 +572,54 @@ func TestPollStorageWithNodesUsesClusterStoragePoolFallback(t *testing.T) {
|
||||
t.Fatalf("expected cluster pool fallback to attach rpool, got %#v", monitor.state.Storage[0].ZFSPool)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPollStorageWithNodesOptimizedAttachesZFSPoolForDirStorageOnDatasetPath(t *testing.T) {
|
||||
t.Setenv("PULSE_DATA_DIR", t.TempDir())
|
||||
|
||||
monitor := &Monitor{
|
||||
state: &models.State{},
|
||||
metricsHistory: NewMetricsHistory(16, time.Hour),
|
||||
alertManager: alerts.NewManager(),
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
monitor.alertManager.Stop()
|
||||
})
|
||||
|
||||
storage := proxmox.Storage{
|
||||
Storage: "local",
|
||||
Type: "dir",
|
||||
Path: "/rpool/data",
|
||||
Content: "images",
|
||||
Active: 1,
|
||||
Enabled: 1,
|
||||
Shared: 0,
|
||||
Total: 1000,
|
||||
Used: 250,
|
||||
Available: 750,
|
||||
}
|
||||
|
||||
client := &fakeStorageClient{
|
||||
allStorage: []proxmox.Storage{storage},
|
||||
storageByNode: map[string][]proxmox.Storage{
|
||||
"node1": {storage},
|
||||
},
|
||||
zfsPoolsByNode: map[string][]proxmox.ZFSPoolInfo{
|
||||
"node1": {
|
||||
{Name: "rpool", Size: 1000, Alloc: 250, Free: 750, Frag: 1, Dedup: 1.0, Health: "ONLINE"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
nodes := []proxmox.Node{{Node: "node1", Status: "online"}}
|
||||
monitor.pollStorageWithNodes(context.Background(), "inst1", client, nodes)
|
||||
|
||||
if len(monitor.state.Storage) != 1 {
|
||||
t.Fatalf("expected 1 storage entry, got %d", len(monitor.state.Storage))
|
||||
}
|
||||
if monitor.state.Storage[0].ZFSPool == nil {
|
||||
t.Fatal("expected dir storage on ZFS dataset path to have ZFS pool attached")
|
||||
}
|
||||
if monitor.state.Storage[0].ZFSPool.Name != "rpool" {
|
||||
t.Fatalf("ZFS pool name = %q, want rpool", monitor.state.Storage[0].ZFSPool.Name)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user