mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 10:35:51 +00:00
Restore the TrueNAS pool topology discriminator
599c8e634 changed the TrueNAS pool projection to publish the ZFS data vdev
layout ("mirror", "raidz2", "stripe") as Storage.Topology. Topology is the
closed discriminator vocabulary every storage consumer keys on, and TrueNAS
pools travel as ResourceTypeStorage, so the frontend identifies them solely
by storage.topology === 'pool'. On any appliance whose pools report data
vdevs, every pool therefore fell out of the TrueNAS page, its datasets
orphaned, and per-system pool counts read zero.
Nothing caught it because internal/truenas/fixtures.go carries no VDevs, so
the projection still returned "pool" for every fixture while the producer
side pinned the new "mirror" value. Both suites stayed green and disagreed
with each other.
Pools now publish Topology "pool" again, with the layout in the new
Storage.VDevLayout field. poolVDevLayout returns an empty string when the
native report carries no data vdevs so callers fall back rather than
inventing a layout, and the presentation surfaces that previously read the
layout out of topology now prefer vdevLayout and fall back to topology.
Alert typing was unaffected (unified_eval.go already routes non-dataset
storage to truenas-pool through its default arm); incident metadata carried
the layout string and now carries the discriminator again.
Regression coverage pins the boundary from both sides, driving real vdev
layouts rather than the vdev-less fixtures:
- TestPoolTopologyStaysDiscriminatorAcrossVDevLayouts (producer, five layouts)
- TestStorageTopologyAndVDevLayoutAreDistinctIdentityFields (clone and wire)
- pool identity boundary cases in truenasPageModel.test.ts (consumer)
- topology label cases in storageAdapters.test.ts and the TrueNAS drawer model
This commit is contained in:
@@ -2415,6 +2415,17 @@ verification.
|
||||
rather than verified results. Safe auto-fix and Autopilot remain visibly
|
||||
`not_assessed` until their governed canaries exist.
|
||||
|
||||
42. Frontend consumers identify a storage resource by
|
||||
`storage.topology`, never by a ZFS vdev layout. TrueNAS pools arrive
|
||||
as `type: 'storage'`, so `topology === 'pool'` is the only pool
|
||||
discriminator available to the page model, and layout strings belong
|
||||
in `storage.vdevLayout`. Regression coverage: the `pool identity
|
||||
boundary` cases in
|
||||
`frontend-modern/src/features/truenas/__tests__/truenasPageModel.test.ts`
|
||||
and `shows the vdev layout as the storage kind while topology stays
|
||||
the pool discriminator` in
|
||||
`frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerTrueNASModel.test.ts`.
|
||||
|
||||
## Current State
|
||||
|
||||
### System member rows are source-type aware
|
||||
|
||||
@@ -712,6 +712,16 @@ remains.
|
||||
paths added to the adapter must keep that persistence step; ephemeral
|
||||
snapshot-bridge adapters stay read-only.
|
||||
|
||||
11. The TrueNAS provider projects pools with `Storage.Topology` fixed to
|
||||
`pool` and the ZFS data vdev layout in `Storage.VDevLayout`. The
|
||||
layout summary (`poolVDevLayout`) returns an empty string when the
|
||||
native report carries no data vdevs so callers fall back instead of
|
||||
inventing a layout. Publishing the layout as the topology drops every
|
||||
pool out of the TrueNAS page, and package fixtures carry no vdevs, so
|
||||
layout-bearing pools must be exercised explicitly. Regression
|
||||
coverage: `TestPoolTopologyStaysDiscriminatorAcrossVDevLayouts` in
|
||||
`internal/truenas/provider_pool_health_contract_test.go`.
|
||||
|
||||
## Current State
|
||||
|
||||
### PBS health is one completed-poll outcome
|
||||
|
||||
@@ -1958,6 +1958,15 @@ pressure to assess context selection, but it must not read live backup records,
|
||||
invoke recovery APIs, mutate storage, or treat a synthetic pass as evidence
|
||||
that Safe auto-fix or Autopilot remediation is verified.
|
||||
|
||||
26. Storage row presentation resolves its topology label from
|
||||
`storage.vdevLayout` first and falls back to `storage.topology`, so a
|
||||
pool reads as its concrete layout ("Mirror", "Raidz2") without the
|
||||
discriminator ever carrying presentation detail. Regression coverage:
|
||||
`labels a TrueNAS pool by its vdev layout while keeping the pool
|
||||
discriminator` and `falls back to the topology discriminator when no
|
||||
vdev layout is reported` in
|
||||
`frontend-modern/src/features/storageBackups/__tests__/storageAdapters.test.ts`.
|
||||
|
||||
## Current State
|
||||
|
||||
### Proxmox runtime continuity is not protection evidence
|
||||
|
||||
@@ -1633,6 +1633,16 @@ AI-only summary payloads, or page-local heuristics.
|
||||
`TestIngestRecordsSkipRecordDeclaredSuccessionForLiveOldID` in
|
||||
`internal/unifiedresources/canonical_id_succession_test.go`.
|
||||
|
||||
28. `StorageMeta.Topology` is a closed discriminator vocabulary shared by
|
||||
every storage provider (`pool`, `dataset`, `array`, `datastore`).
|
||||
Consumers key resource identity off it, so a provider must never
|
||||
publish presentation detail there. ZFS data vdev layout is
|
||||
presentation and belongs in `StorageMeta.VDevLayout`, which is absent
|
||||
rather than invented when the native report carries no data vdevs.
|
||||
Regression coverage:
|
||||
`TestStorageTopologyAndVDevLayoutAreDistinctIdentityFields` in
|
||||
`internal/unifiedresources/canonical_identity_test.go`.
|
||||
|
||||
## Current State
|
||||
|
||||
### Host views expose the integration-source discriminator
|
||||
|
||||
+33
-1
@@ -206,7 +206,8 @@ describe('resourceDetailDrawerTrueNASModel', () => {
|
||||
platformScopes: ['truenas'],
|
||||
storage: {
|
||||
type: 'zfs-pool',
|
||||
topology: 'mirror',
|
||||
topology: 'pool',
|
||||
vdevLayout: 'mirror',
|
||||
platform: 'truenas',
|
||||
zfsPoolState: 'DEGRADED',
|
||||
poolHealth: {
|
||||
@@ -313,4 +314,35 @@ describe('resourceDetailDrawerTrueNASModel', () => {
|
||||
'SMART',
|
||||
]);
|
||||
});
|
||||
|
||||
it('shows the vdev layout as the storage kind while topology stays the pool discriminator', () => {
|
||||
// Regression guard for 599c8e634. `topology` is the cross-provider
|
||||
// discriminator ('pool' | 'dataset' | ...); the ZFS layout belongs in
|
||||
// `vdevLayout`. The drawer prefers the layout for display but must not
|
||||
// depend on it being in `topology`.
|
||||
const withLayout = baseResource({
|
||||
type: 'storage',
|
||||
displayName: 'tank',
|
||||
storage: {
|
||||
type: 'zfs-pool',
|
||||
topology: 'pool',
|
||||
vdevLayout: 'raidz2',
|
||||
platform: 'truenas',
|
||||
},
|
||||
} as Partial<Resource>);
|
||||
|
||||
expect(buildTrueNASDetailsSummary(withLayout)).toContain('Raidz2');
|
||||
|
||||
const withoutLayout = baseResource({
|
||||
type: 'storage',
|
||||
displayName: 'tank',
|
||||
storage: {
|
||||
type: 'zfs-pool',
|
||||
topology: 'pool',
|
||||
platform: 'truenas',
|
||||
},
|
||||
} as Partial<Resource>);
|
||||
|
||||
expect(buildTrueNASDetailsSummary(withoutLayout)).toContain('Pool');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -205,6 +205,8 @@ const buildTrueNASSystemSections = (
|
||||
};
|
||||
|
||||
const storageKindLabel = (storage: ResourceStorageMeta): string | null => {
|
||||
const layout = asString(storage.vdevLayout);
|
||||
if (layout) return normalizeDelimitedLabel(layout);
|
||||
const topology = asString(storage.topology);
|
||||
if (topology) return normalizeDelimitedLabel(topology);
|
||||
return normalizeDelimitedLabel(storage.type);
|
||||
|
||||
@@ -662,4 +662,48 @@ describe('storageAdapters', () => {
|
||||
expect(records).toHaveLength(1);
|
||||
expect(records[0].details?.zfsPool).toEqual(zfsPool);
|
||||
});
|
||||
|
||||
it('labels a TrueNAS pool by its vdev layout while keeping the pool discriminator', () => {
|
||||
// Regression guard for 599c8e634: the layout must ride in `vdevLayout`, not
|
||||
// in `topology`. `topology` identifies the resource as a pool; publishing a
|
||||
// layout there dropped every pool off the TrueNAS page on real hardware.
|
||||
const state = baseState();
|
||||
const records = buildStorageRecords({
|
||||
state,
|
||||
resources: [
|
||||
makeResourceStorage({
|
||||
storage: {
|
||||
type: 'zfs-pool',
|
||||
isZfs: true,
|
||||
platform: 'truenas',
|
||||
topology: 'pool',
|
||||
vdevLayout: 'mirror',
|
||||
} as Resource['storage'],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
expect(records).toHaveLength(1);
|
||||
expect(records[0].topologyLabel).toBe('Mirror');
|
||||
});
|
||||
|
||||
it('falls back to the topology discriminator when no vdev layout is reported', () => {
|
||||
const state = baseState();
|
||||
const records = buildStorageRecords({
|
||||
state,
|
||||
resources: [
|
||||
makeResourceStorage({
|
||||
storage: {
|
||||
type: 'zfs-pool',
|
||||
isZfs: true,
|
||||
platform: 'truenas',
|
||||
topology: 'pool',
|
||||
} as Resource['storage'],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
expect(records).toHaveLength(1);
|
||||
expect(records[0].topologyLabel).toBe('Pool');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -152,7 +152,9 @@ const mapResourceStorageRecord = (resource: Resource, adapterId: string): Storag
|
||||
const topologyLabel = getResourceStorageTopologyLabel(
|
||||
resource,
|
||||
storageType,
|
||||
resource.storage?.topology,
|
||||
// Prefer the concrete vdev layout ("Mirror", "Raidz2") over the generic
|
||||
// discriminator ("Pool") when the provider reports one.
|
||||
resource.storage?.vdevLayout || resource.storage?.topology,
|
||||
);
|
||||
const metricsTarget = metricsTargetForStorageResource(resource);
|
||||
|
||||
|
||||
@@ -990,3 +990,54 @@ describe('truenasPageModel', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pool identity boundary', () => {
|
||||
// Regression guard for 599c8e634: the producer briefly published the ZFS vdev
|
||||
// layout ("mirror", "raidz2") as storage.topology. TrueNAS pools arrive as
|
||||
// type 'storage', so `topology === 'pool'` is the only thing that identifies
|
||||
// them here — publishing a layout there dropped every pool off this page on
|
||||
// real hardware while every fixture (which omits vdevs) stayed green.
|
||||
it('recognises a pool that also reports a vdev layout', () => {
|
||||
const system = makeResource({ id: 'nas-a', type: 'agent', name: 'nas-a' });
|
||||
const pool = makeResource({
|
||||
id: 'pool-tank',
|
||||
type: 'storage',
|
||||
name: 'tank',
|
||||
parentId: system.id,
|
||||
storage: { topology: 'pool', vdevLayout: 'mirror', platform: 'truenas' },
|
||||
});
|
||||
const dataset = makeResource({
|
||||
id: 'dataset-media',
|
||||
type: 'storage',
|
||||
name: 'tank/media',
|
||||
parentId: pool.id,
|
||||
storage: { topology: 'dataset', platform: 'truenas' },
|
||||
});
|
||||
|
||||
const rows = buildTrueNASStorageTopologyRows([system, pool, dataset]);
|
||||
const poolRow = rows.find((row) => row.resource.id === pool.id);
|
||||
|
||||
expect(poolRow?.kind).toBe('pool');
|
||||
// The dataset must still hang off the pool rather than orphaning.
|
||||
const datasetRow = rows.find((row) => row.resource.id === dataset.id);
|
||||
expect(datasetRow?.parentRowId).toBe(poolRow?.id);
|
||||
});
|
||||
|
||||
it('does not identify a pool by its vdev layout alone', () => {
|
||||
const system = makeResource({ id: 'nas-a', type: 'agent', name: 'nas-a' });
|
||||
// A pool whose topology carries a layout instead of the discriminator is
|
||||
// exactly the broken shape; assert it is NOT silently accepted, so the
|
||||
// producer contract stays the single place this is fixed.
|
||||
const malformed = makeResource({
|
||||
id: 'pool-tank',
|
||||
type: 'storage',
|
||||
name: 'tank',
|
||||
parentId: system.id,
|
||||
storage: { topology: 'mirror', platform: 'truenas' },
|
||||
});
|
||||
|
||||
const rows = buildTrueNASStorageTopologyRows([system, malformed]);
|
||||
|
||||
expect(rows.find((row) => row.resource.id === malformed.id)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -389,7 +389,14 @@ export interface ResourceStorageMeta {
|
||||
isCeph?: boolean;
|
||||
isZfs?: boolean;
|
||||
platform?: string;
|
||||
/**
|
||||
* Closed discriminator vocabulary shared by every storage provider
|
||||
* ('pool' | 'dataset' | 'array' | 'datastore'). Resource identity keys off
|
||||
* it, so it never carries presentation detail — see `vdevLayout`.
|
||||
*/
|
||||
topology?: string;
|
||||
/** ZFS data vdev layout for display ('mirror', 'raidz2', 'mirror+special'). */
|
||||
vdevLayout?: string;
|
||||
protection?: string;
|
||||
risk?: ResourceStorageRisk;
|
||||
riskSummary?: string;
|
||||
|
||||
@@ -605,7 +605,8 @@ func truenasRecordsFromSnapshot(snapshot *FixtureSnapshot, connectionID string,
|
||||
Type: "zfs-pool",
|
||||
IsZFS: true,
|
||||
Platform: "truenas",
|
||||
Topology: poolTopologyLabel(pool),
|
||||
Topology: "pool",
|
||||
VDevLayout: poolVDevLayout(pool),
|
||||
Protection: "zfs",
|
||||
Risk: risk,
|
||||
RiskSummary: unifiedresources.StorageRiskSummary(risk),
|
||||
@@ -1652,7 +1653,12 @@ func poolScanIsActive(state string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func poolTopologyLabel(pool Pool) string {
|
||||
// poolVDevLayout summarizes the pool's data vdev layout ("mirror", "raidz2",
|
||||
// "stripe", "mirror+special") for presentation. It returns an empty string when
|
||||
// the native report carries no data vdevs, so callers fall back rather than
|
||||
// inventing a layout. This is deliberately NOT the pool's topology
|
||||
// discriminator: see StorageMeta.Topology.
|
||||
func poolVDevLayout(pool Pool) string {
|
||||
var dataTypes []string
|
||||
for _, vdev := range pool.VDevs {
|
||||
if vdev.ParentID != "" || !strings.EqualFold(strings.TrimSpace(vdev.Role), "data") {
|
||||
@@ -1665,7 +1671,7 @@ func poolTopologyLabel(pool Pool) string {
|
||||
dataTypes = append(dataTypes, vdevType)
|
||||
}
|
||||
if len(dataTypes) == 0 {
|
||||
return "pool"
|
||||
return ""
|
||||
}
|
||||
if len(dataTypes) == 1 && dataTypes[0] == "disk" && len(pool.DiskMembers) > 1 {
|
||||
return "stripe"
|
||||
|
||||
@@ -59,8 +59,14 @@ func TestProviderProjectsFullZFSHealthAndActionableDatasetAppIncidents(t *testin
|
||||
if pool.Storage == nil || pool.Storage.ZFSPool == nil || pool.Storage.PoolHealth == nil {
|
||||
t.Fatalf("full pool health contract missing: %+v", pool.Storage)
|
||||
}
|
||||
if pool.Storage.Topology != "mirror" {
|
||||
t.Fatalf("topology = %q", pool.Storage.Topology)
|
||||
// Topology is the cross-provider discriminator every pool consumer keys on
|
||||
// and must stay "pool" even for a pool with a rich vdev layout; the layout
|
||||
// itself rides in VDevLayout.
|
||||
if pool.Storage.Topology != "pool" {
|
||||
t.Fatalf("topology = %q, want \"pool\"", pool.Storage.Topology)
|
||||
}
|
||||
if pool.Storage.VDevLayout != "mirror" {
|
||||
t.Fatalf("vdev layout = %q, want \"mirror\"", pool.Storage.VDevLayout)
|
||||
}
|
||||
if pool.Storage.ZFSReadErrors != 1 || pool.Storage.ZFSChecksumErrors != 2 {
|
||||
t.Fatalf("flattened errors = %+v", pool.Storage)
|
||||
@@ -152,3 +158,80 @@ func requireRecordByNameAndType(t *testing.T, records []unifiedresources.IngestR
|
||||
t.Fatalf("missing %s %q in %+v", resourceType, name, records)
|
||||
return unifiedresources.IngestRecord{}
|
||||
}
|
||||
|
||||
// TestPoolTopologyStaysDiscriminatorAcrossVDevLayouts pins the boundary that
|
||||
// broke in 599c8e634: the frontend identifies a TrueNAS pool solely by
|
||||
// storage.topology == "pool" (TrueNAS pools are transported as
|
||||
// ResourceTypeStorage, so resource.type is never "pool"), and the producer
|
||||
// briefly published the vdev layout there instead. Every fixture in this
|
||||
// package omits VDevs, so the projection kept returning "pool" and both sides
|
||||
// stayed green while disagreeing on real hardware. Drive real layouts here.
|
||||
func TestPoolTopologyStaysDiscriminatorAcrossVDevLayouts(t *testing.T) {
|
||||
observedAt := time.Date(2026, 7, 24, 10, 0, 0, 0, time.UTC)
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
vdevs []PoolVDev
|
||||
members []PoolDiskMember
|
||||
wantLayout string
|
||||
}{
|
||||
{name: "no vdevs reported", wantLayout: ""},
|
||||
{
|
||||
name: "mirror",
|
||||
vdevs: []PoolVDev{{ID: "m0", Name: "mirror-0", Type: "MIRROR", Role: "data", Status: "ONLINE"}},
|
||||
wantLayout: "mirror",
|
||||
},
|
||||
{
|
||||
name: "raidz2",
|
||||
vdevs: []PoolVDev{{ID: "z0", Name: "raidz2-0", Type: "RAIDZ2", Role: "data", Status: "ONLINE"}},
|
||||
wantLayout: "raidz2",
|
||||
},
|
||||
{
|
||||
name: "striped single disks",
|
||||
vdevs: []PoolVDev{
|
||||
{ID: "d0", Name: "sda", Type: "DISK", Role: "data", Status: "ONLINE"},
|
||||
{ID: "d1", Name: "sdb", Type: "DISK", Role: "data", Status: "ONLINE"},
|
||||
},
|
||||
members: []PoolDiskMember{
|
||||
{Disk: "sda", Status: "ONLINE", Role: "data"},
|
||||
{Disk: "sdb", Status: "ONLINE", Role: "data"},
|
||||
},
|
||||
wantLayout: "stripe",
|
||||
},
|
||||
{
|
||||
name: "mixed data vdev types",
|
||||
vdevs: []PoolVDev{
|
||||
{ID: "m0", Name: "mirror-0", Type: "MIRROR", Role: "data", Status: "ONLINE"},
|
||||
{ID: "s0", Name: "special-0", Type: "SPECIAL", Role: "data", Status: "ONLINE"},
|
||||
},
|
||||
wantLayout: "mirror+special",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
records := FixtureRecords(FixtureSnapshot{
|
||||
CollectedAt: observedAt,
|
||||
System: SystemInfo{Hostname: "nas-a", Healthy: true},
|
||||
Pools: []Pool{{
|
||||
ID: "1",
|
||||
GUID: "pool-guid",
|
||||
Name: "tank",
|
||||
Status: "ONLINE",
|
||||
VDevs: tc.vdevs,
|
||||
DiskMembers: tc.members,
|
||||
}},
|
||||
})
|
||||
|
||||
pool := requirePoolRecord(t, records, "tank").Resource
|
||||
if pool.Storage == nil {
|
||||
t.Fatalf("pool record carries no storage meta: %+v", pool)
|
||||
}
|
||||
if pool.Storage.Topology != "pool" {
|
||||
t.Fatalf("topology = %q, want %q: a non-%q value drops the pool out of the TrueNAS page entirely",
|
||||
pool.Storage.Topology, "pool", "pool")
|
||||
}
|
||||
if pool.Storage.VDevLayout != tc.wantLayout {
|
||||
t.Fatalf("vdev layout = %q, want %q", pool.Storage.VDevLayout, tc.wantLayout)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package unifiedresources
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
@@ -915,3 +916,51 @@ func TestRefreshCanonicalIdentityPreservesTrueNASVMIdentity(t *testing.T) {
|
||||
t.Fatalf("primaryId = %q, want vm:42", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStorageTopologyAndVDevLayoutAreDistinctIdentityFields pins the separation
|
||||
// restored after 599c8e634. StorageMeta.Topology is the closed discriminator
|
||||
// vocabulary consumers use to identify a resource ("pool", "dataset", "array",
|
||||
// "datastore"); the ZFS vdev layout is presentation and lives in VDevLayout.
|
||||
// Collapsing the two, or dropping either through a clone, silently changes
|
||||
// resource identity for every storage consumer.
|
||||
func TestStorageTopologyAndVDevLayoutAreDistinctIdentityFields(t *testing.T) {
|
||||
meta := &StorageMeta{
|
||||
Type: "zfs-pool",
|
||||
Platform: "truenas",
|
||||
Topology: "pool",
|
||||
VDevLayout: "mirror+special",
|
||||
}
|
||||
|
||||
cloned := cloneStorageMeta(meta)
|
||||
if cloned == nil {
|
||||
t.Fatal("cloneStorageMeta returned nil")
|
||||
}
|
||||
if cloned.Topology != "pool" {
|
||||
t.Fatalf("cloned topology = %q, want \"pool\"", cloned.Topology)
|
||||
}
|
||||
if cloned.VDevLayout != "mirror+special" {
|
||||
t.Fatalf("cloned vdev layout = %q, want \"mirror+special\"", cloned.VDevLayout)
|
||||
}
|
||||
|
||||
// A pool with no reported vdevs still carries the discriminator, and the
|
||||
// layout stays absent rather than being backfilled from it.
|
||||
bare := cloneStorageMeta(&StorageMeta{Topology: "pool"})
|
||||
if bare.Topology != "pool" || bare.VDevLayout != "" {
|
||||
t.Fatalf("bare pool meta = %+v, want topology \"pool\" and empty layout", bare)
|
||||
}
|
||||
|
||||
encoded, err := json.Marshal(meta)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal storage meta: %v", err)
|
||||
}
|
||||
var decoded map[string]any
|
||||
if err := json.Unmarshal(encoded, &decoded); err != nil {
|
||||
t.Fatalf("unmarshal storage meta: %v", err)
|
||||
}
|
||||
if decoded["topology"] != "pool" {
|
||||
t.Fatalf("wire topology = %v, want \"pool\"", decoded["topology"])
|
||||
}
|
||||
if decoded["vdevLayout"] != "mirror+special" {
|
||||
t.Fatalf("wire vdevLayout = %v, want \"mirror+special\"", decoded["vdevLayout"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,16 +384,21 @@ type ProxmoxData struct {
|
||||
|
||||
// StorageMeta contains storage-specific metadata for storage resources.
|
||||
type StorageMeta struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
ContentTypes []string `json:"contentTypes,omitempty"`
|
||||
Shared bool `json:"shared"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Active bool `json:"active"`
|
||||
IsCeph bool `json:"isCeph"`
|
||||
IsZFS bool `json:"isZfs"`
|
||||
Platform string `json:"platform,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
ContentTypes []string `json:"contentTypes,omitempty"`
|
||||
Shared bool `json:"shared"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Active bool `json:"active"`
|
||||
IsCeph bool `json:"isCeph"`
|
||||
IsZFS bool `json:"isZfs"`
|
||||
Platform string `json:"platform,omitempty"`
|
||||
// Topology is a closed discriminator vocabulary shared by every storage
|
||||
// provider ("pool", "dataset", "array", "datastore"). Consumers key
|
||||
// resource identity off it, so it must never carry presentation detail.
|
||||
// Per-pool vdev layout belongs in VDevLayout.
|
||||
Topology string `json:"topology,omitempty"`
|
||||
VDevLayout string `json:"vdevLayout,omitempty"`
|
||||
Protection string `json:"protection,omitempty"`
|
||||
Risk *StorageRisk `json:"risk,omitempty"`
|
||||
RiskSummary string `json:"riskSummary,omitempty"`
|
||||
|
||||
Reference in New Issue
Block a user