mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-24 04:33:17 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user