Stop reporting unavailable storage as disabled to AI tools

The storage pool rows the AI tools build (pulse_storage pools and
pulse_query storage pools) derived both enabled and active from "status
is not offline". Storage that reported itself unavailable, such as
Proxmox storage with active=0, reached Patrol and Assistant as
administratively disabled even when it is enabled in Proxmox.

Enabled now comes from the storage facet's own flag. Active needs the
facet's flag and a row that is not offline, because PBS datastores
report Active even while unavailable. Rows without a storage facet keep
the status-derived value.

TrueNAS pools and datasets were the only storage producers that left
both flags at their zero value, so reading the facet alone would have
turned every TrueNAS pool into a disabled one. They now report Enabled,
and Active from the pool state or the dataset's mount and lock state.
This commit is contained in:
rcourtman
2026-10-07 10:33:22 +01:00
parent 41cea2ffab
commit 6fb9752397
6 changed files with 145 additions and 2 deletions
@@ -6474,6 +6474,18 @@ That availability contract also applies when recovery points are the only storag
`internal/ai/tools/executor.go` must keep `pulse_storage` exposed whenever a
`RecoveryPointsProvider` is configured, so tenant and self-hosted Chat surfaces do not lose
recovery-backed snapshot and backup-task reads just because backup/read-state adapters are absent.
Storage pool rows built by `storagePoolSummaryFromResource` in
`internal/ai/tools/tools_storage.go` (`pulse_storage` pools and `pulse_query`
storage pools) take `enabled` from the storage facet's own
`StorageMeta.Enabled`, so storage that reported itself unavailable (Proxmox
`active=0`, unified status `offline`) never reads as administratively disabled
to Patrol or Assistant. `active` is the facet's `Active` flag and also requires
the row not to be `offline`, because some sources (PBS datastores) report
`Active` even while unavailable. Only a row without a storage facet falls back
to the status-derived value.
`TestStoragePoolSummaryReadsStorageFacetFlags` in
`internal/ai/tools/tools_storage_test.go` pins unavailable, disabled, online,
offline-but-flagged-active and facet-less rows.
Tenant-scoped AI services must now also follow canonical runtime ownership:
Patrol may initialize and operate from tenant `ReadState` and unified-resource
providers without requiring a tenant snapshot-provider bridge, and
@@ -4904,6 +4904,15 @@ remain separate through refresh, cache rebuild, restart, and registry ingest.
Replication-target readonly classification remains a separate native-evidence
step and cannot hide locked or unmounted dataset state.
TrueNAS pool and dataset storage facets carry `StorageMeta.Enabled` and
`Active` like every other storage producer, because the AI storage tools and
storage platform payloads read those flags directly. TrueNAS has no disabled
state for an imported pool or a listed dataset, so both are always `Enabled`. A
pool is `Active` unless its native state is FAULTED, OFFLINE, REMOVED or
UNAVAIL, and a dataset is `Active` only while it is mounted and unlocked.
`TestProviderRecordsPopulateStorageEnabledAndActive` in
`internal/truenas/provider_test.go` pins both.
Ceph monitoring may enter the provider-neutral pool-health envelope only from
the native cluster health state and native health-check map. It preserves check
codes, severity, and summaries in deterministic order. A cluster-level
+9 -1
View File
@@ -338,8 +338,16 @@ func storagePoolSummaryFromResource(r unifiedresources.Resource) StoragePoolSumm
freeBytes = totalBytes - usedBytes
}
// Enabled is the source's configuration flag, so an unavailable storage
// (Proxmox active=0) must not read as administratively disabled. Active
// also needs the row itself to be up: PBS datastores report Active even
// while unavailable, and an offline row is not usable whatever its flag.
active := r.Status != unifiedresources.StatusOffline
enabled := r.Status != unifiedresources.StatusOffline
enabled := active
if r.Storage != nil {
enabled = r.Storage.Enabled
active = active && r.Storage.Active
}
id := r.ID
if id == "" {
+59
View File
@@ -279,6 +279,65 @@ func TestStorageUsagePercentNotDoubled(t *testing.T) {
}
}
func TestStoragePoolSummaryReadsStorageFacetFlags(t *testing.T) {
storage := func(enabled, active bool) *unifiedresources.StorageMeta {
return &unifiedresources.StorageMeta{Type: "dir", Enabled: enabled, Active: active}
}
cases := []struct {
name string
status unifiedresources.ResourceStatus
storage *unifiedresources.StorageMeta
wantEnabled bool
wantActive bool
listSkipsRow bool
}{
// Proxmox storage that reports active=0 is unavailable, not disabled.
{"unavailable storage stays enabled", unifiedresources.StatusOffline, storage(true, false), true, false, false},
{"disabled storage", unifiedresources.StatusWarning, storage(false, false), false, false, false},
{"online storage", unifiedresources.StatusOnline, storage(true, true), true, true, false},
// PBS datastores report Active even while unavailable.
{"offline row whose source reports active", unifiedresources.StatusOffline, storage(true, true), true, false, false},
{"no facet online", unifiedresources.StatusOnline, nil, true, true, true},
{"no facet offline", unifiedresources.StatusOffline, nil, false, false, true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
resource := unifiedresources.Resource{
ID: "local-lvm",
Name: "local-lvm",
Type: unifiedresources.ResourceTypeStorage,
Status: tc.status,
Storage: tc.storage,
}
got := storagePoolSummaryFromResource(resource)
if got.Enabled != tc.wantEnabled || got.Active != tc.wantActive {
t.Fatalf("summary enabled/active = %v/%v, want %v/%v", got.Enabled, got.Active, tc.wantEnabled, tc.wantActive)
}
if tc.listSkipsRow {
return
}
executor := NewPulseToolExecutor(ExecutorConfig{StateProvider: &mockStateProvider{}})
executor.unifiedResourceProvider = &stubUnifiedResourceProvider{resources: []unifiedresources.Resource{resource}}
result, err := executor.executeListStorage(context.Background(), map[string]interface{}{})
if err != nil {
t.Fatalf("executeListStorage: %v", err)
}
var resp StorageResponse
if err := json.Unmarshal([]byte(result.Content[0].Text), &resp); err != nil {
t.Fatalf("decode storage response: %v", err)
}
if len(resp.Pools) != 1 {
t.Fatalf("pools = %+v, want one", resp.Pools)
}
if resp.Pools[0].Enabled != tc.wantEnabled || resp.Pools[0].Active != tc.wantActive {
t.Fatalf("listed enabled/active = %v/%v, want %v/%v", resp.Pools[0].Enabled, resp.Pools[0].Active, tc.wantEnabled, tc.wantActive)
}
})
}
}
func TestStorageResponsesUseCanonicalEmptyCollections(t *testing.T) {
backupsPayload, err := json.Marshal(EmptyBackupsResponse())
if err != nil {
+9 -1
View File
@@ -590,6 +590,7 @@ func truenasRecordsFromSnapshot(snapshot *FixtureSnapshot, connectionID string,
risk := unifiedresources.StorageRiskFromAssessment(assessment)
incidents := incidentAssignments.Pools[strings.TrimSpace(pool.Name)]
zfsPool := zfsPoolFromPool(pool)
poolStatus := statusFromPool(pool)
poolSourceID := scopedPoolSourceID(systemSourceID, pool.Name)
records = append(records, unifiedresources.IngestRecord{
SourceID: poolSourceID,
@@ -598,14 +599,19 @@ func truenasRecordsFromSnapshot(snapshot *FixtureSnapshot, connectionID string,
Resource: unifiedresources.Resource{
Type: unifiedresources.ResourceTypeStorage,
Name: pool.Name,
Status: unifiedresources.IncidentsStatus(statusFromPool(pool), incidents),
Status: unifiedresources.IncidentsStatus(poolStatus, incidents),
LastSeen: collectedAt,
UpdatedAt: collectedAt,
Metrics: &unifiedresources.ResourceMetrics{
Disk: diskMetric(pool.TotalBytes, pool.UsedBytes),
},
// TrueNAS has no disabled state for an imported pool, so
// Enabled is always true; Active follows whether the pool is
// usable now.
Storage: &unifiedresources.StorageMeta{
Type: "zfs-pool",
Enabled: true,
Active: poolStatus != unifiedresources.StatusOffline,
IsZFS: true,
Platform: "truenas",
Topology: "pool",
@@ -654,6 +660,8 @@ func truenasRecordsFromSnapshot(snapshot *FixtureSnapshot, connectionID string,
},
Storage: &unifiedresources.StorageMeta{
Type: "zfs-dataset",
Enabled: true,
Active: dataset.Mounted && !dataset.Locked,
IsZFS: true,
Platform: "truenas",
Topology: "dataset",
+47
View File
@@ -261,6 +261,53 @@ func TestProviderRecordsSynthesizesIncidentFromUnhealthyPoolStatus(t *testing.T)
}
}
func TestProviderRecordsPopulateStorageEnabledAndActive(t *testing.T) {
records := FixtureRecords(FixtureSnapshot{
CollectedAt: time.Date(2026, 10, 7, 9, 0, 0, 0, time.UTC),
System: SystemInfo{Hostname: "truenas-main", Healthy: true},
Pools: []Pool{
{ID: "pool-tank", Name: "tank", Status: "ONLINE", TotalBytes: 1000, UsedBytes: 400},
{ID: "pool-cold", Name: "cold", Status: "DEGRADED", TotalBytes: 1000, UsedBytes: 400},
{ID: "pool-gone", Name: "gone", Status: "UNAVAIL"},
{ID: "pool-odd", Name: "odd", Status: ""},
},
Datasets: []Dataset{
{ID: "tank/apps", Name: "tank/apps", Pool: "tank", Mounted: true},
{ID: "tank/spare", Name: "tank/spare", Pool: "tank", Mounted: false},
{ID: "tank/vault", Name: "tank/vault", Pool: "tank", Mounted: false, Locked: true},
},
})
cases := []struct {
name string
record unifiedresources.IngestRecord
active bool
}{
{"online pool", requirePoolRecord(t, records, "tank"), true},
{"degraded pool", requirePoolRecord(t, records, "cold"), true},
{"unavailable pool", requirePoolRecord(t, records, "gone"), false},
// An unreported pool state is not known offline, matching its status.
{"unknown-state pool", requirePoolRecord(t, records, "odd"), true},
{"mounted dataset", requireRecordByNameAndType(t, records, "tank/apps", unifiedresources.ResourceTypeStorage), true},
{"unmounted dataset", requireRecordByNameAndType(t, records, "tank/spare", unifiedresources.ResourceTypeStorage), false},
{"locked dataset", requireRecordByNameAndType(t, records, "tank/vault", unifiedresources.ResourceTypeStorage), false},
}
for _, tc := range cases {
storage := tc.record.Resource.Storage
if storage == nil {
t.Fatalf("%s: missing storage facet", tc.name)
}
// Consumers read these flags directly, so the zero value would mark
// every TrueNAS pool and dataset as disabled and inactive.
if !storage.Enabled {
t.Errorf("%s: Enabled = false, want true", tc.name)
}
if storage.Active != tc.active {
t.Errorf("%s: Active = %v, want %v", tc.name, storage.Active, tc.active)
}
}
}
func TestProviderRecordsDoesNotDuplicateNativePoolAlert(t *testing.T) {
alertTime := time.Date(2026, 6, 29, 11, 30, 0, 0, time.UTC)
records := FixtureRecords(FixtureSnapshot{