diff --git a/apps/docs/docs/integrations/truenas/index.mdx b/apps/docs/docs/integrations/truenas/index.mdx index 5d29a7019..5e57f3a89 100644 --- a/apps/docs/docs/integrations/truenas/index.mdx +++ b/apps/docs/docs/integrations/truenas/index.mdx @@ -27,6 +27,8 @@ import Alert from '@theme/Admonition'; ]} /> +The storage values shown by these widgets use the root ZFS dataset's usable `used` and `available` space, including RAIDZ parity overhead and dataset limits. + ### Adding the integration diff --git a/packages/integrations/src/truenas/test/truenas-integration.spec.ts b/packages/integrations/src/truenas/test/truenas-integration.spec.ts index 91ab0662c..7f1a2ad4e 100644 --- a/packages/integrations/src/truenas/test/truenas-integration.spec.ts +++ b/packages/integrations/src/truenas/test/truenas-integration.spec.ts @@ -148,6 +148,14 @@ const happyResponder = (method: string) => { ]; case "pool.query": return [{ name: "tank", status: "ONLINE", healthy: true, free: 500, size: 1000, allocated: 500 }]; + case "pool.dataset.query": + return [ + { + id: "tank", + used: { parsed: 200 }, + available: { parsed: 300 }, + }, + ]; case "interface.query": return [{ id: "eth0", name: "eth0" }]; case "reporting.netdata_get_data": @@ -204,8 +212,8 @@ describe("TrueNasIntegration", () => { loadAverage: null, gpu: [], network: { up: 20_000, down: 10_000 }, - // `available` is the free space left on the pool (free = 500), not its total size. - fileSystem: [{ deviceName: "tank", available: "500", used: "500", percentage: 50 }], + // `available` and `used` come from the root dataset, not the physical pool. + fileSystem: [{ deviceName: "tank", available: "300", used: "200", percentage: 40 }], smart: [{ deviceName: "tank", healthy: true, overallStatus: "ONLINE", temperature: null }], }); }); diff --git a/packages/integrations/src/truenas/truenas-integration.ts b/packages/integrations/src/truenas/truenas-integration.ts index 21f526201..6a5249a2e 100644 --- a/packages/integrations/src/truenas/truenas-integration.ts +++ b/packages/integrations/src/truenas/truenas-integration.ts @@ -49,9 +49,9 @@ export class TrueNasIntegration extends Integration implements ISystemHealthMoni memUsedInBytes, fileSystem: datasets.map((dataset) => ({ deviceName: dataset.name, - available: `${dataset.free}`, // free space left on the pool - used: `${dataset.allocated}`, - percentage: (dataset.allocated / dataset.size) * 100, + available: `${dataset.available}`, + used: `${dataset.used}`, + percentage: (dataset.used / (dataset.used + dataset.available)) * 100, })), availablePkgUpdates: 0, network: { @@ -117,12 +117,40 @@ export class TrueNasIntegration extends Integration implements ISystemHealthMoni return null; }) .filter((pool) => pool !== null); + + if (activePools.length === 0) return []; + + const datasets = await this.requestAsync("pool.dataset.query", [ + [["id", "IN", activePools.map((pool) => pool.name)]], + { + extra: { + properties: ["used", "available"], + }, + }, + ]); + const parsedDatasets = await poolDatasetSchema.parseAsync(datasets); + + const poolsWithUsableSpace = activePools + .map((pool) => { + const dataset = parsedDatasets.find((candidate) => candidate.id === pool.name); + if (!dataset || dataset.used.parsed === null || dataset.available.parsed === null) return null; + + return { + name: pool.name, + status: pool.status, + healthy: pool.healthy, + used: dataset.used.parsed, + available: dataset.available.parsed, + }; + }) + .filter((pool) => pool !== null); + logger.debug("Retrieved pools", { url: this.integration.url, totalCount: result.length, - activeCount: activePools.length, + activeCount: poolsWithUsableSpace.length, }); - return activePools; + return poolsWithUsableSpace; } /** @@ -263,6 +291,14 @@ const poolSchema = z.array( }), ); +const poolDatasetSchema = z.array( + z.object({ + id: z.string(), + used: z.object({ parsed: z.number().min(0).nullable() }), + available: z.object({ parsed: z.number().min(0).nullable() }), + }), +); + const reportingNetDataSchema = z.array( z.object({ name: z.string(),