diff --git a/internal/monitoring/monitor_polling.go b/internal/monitoring/monitor_polling.go index 2938d31de..4f6017062 100644 --- a/internal/monitoring/monitor_polling.go +++ b/internal/monitoring/monitor_polling.go @@ -1531,36 +1531,26 @@ func (m *Monitor) pollStorageWithNodes(ctx context.Context, instanceName string, enableZFSMonitoring := os.Getenv("PULSE_DISABLE_ZFS_MONITORING") != "true" // Enabled by default if enableZFSMonitoring { - hasZFSStorage := false - for _, storage := range nodeStorage { - if storage.Type == "zfspool" || storage.Type == "zfs" || storage.Type == "local-zfs" { - hasZFSStorage = true - break - } - } + 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)") } } @@ -1627,10 +1617,10 @@ 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" { - modelStorage.ZFSPool = matchZFSPoolForStorage(modelStorage, zfsPoolMap) - } + // Attach ZFS pool status information whenever the storage name or + // dataset path resolves to a known pool, even for dir storages + // backed by ZFS datasets. + modelStorage.ZFSPool = matchZFSPoolForStorage(modelStorage, zfsPoolMap) // Override with cluster config if available, but only when the // cluster metadata explicitly carries those flags. Some storage diff --git a/internal/monitoring/monitor_storage_test.go b/internal/monitoring/monitor_storage_test.go index 2e901407e..47915d893 100644 --- a/internal/monitoring/monitor_storage_test.go +++ b/internal/monitoring/monitor_storage_test.go @@ -258,6 +258,12 @@ func TestMatchZFSPoolForStorage(t *testing.T) { pools: map[string]*models.ZFSPool{"rpool": rpool}, want: "rpool", }, + { + name: "matches dir storage from dataset path", + storage: models.Storage{Name: "local", Type: "dir", Path: "/rpool/data"}, + pools: map[string]*models.ZFSPool{"rpool": rpool}, + want: "rpool", + }, { name: "single pool fallback for local zfs", storage: models.Storage{Name: "local-zfs", Type: "local-zfs"}, @@ -342,3 +348,54 @@ func TestPollStorageWithNodesOptimizedAttachesZFSPoolFromDatasetPath(t *testing. t.Fatalf("ZFS pool name = %q, want rpool", monitor.state.Storage[0].ZFSPool.Name) } } + +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) + } +}