Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56e6b23af6 | ||
|
|
5488bf4d18 | ||
|
|
f6137b2dcc | ||
|
|
425dd90871 |
No files matched your search
@@ -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
|
### Adding the integration
|
||||||
<AddingIntegration />
|
<AddingIntegration />
|
||||||
|
|
||||||
|
|||||||
@@ -7,42 +7,6 @@ import { createHomarrContainer } from "./shared/create-homarr-container";
|
|||||||
import { createSqliteDbFileAsync } from "./shared/e2e-db";
|
import { createSqliteDbFileAsync } from "./shared/e2e-db";
|
||||||
|
|
||||||
describe("Onboarding", () => {
|
describe("Onboarding", () => {
|
||||||
test("Credentials onboarding should be successful", async () => {
|
|
||||||
// Arrange
|
|
||||||
const { db, localMountPath } = await createSqliteDbFileAsync();
|
|
||||||
const homarrContainer = await createHomarrContainer({
|
|
||||||
mounts: {
|
|
||||||
"/appdata": localMountPath,
|
|
||||||
},
|
|
||||||
}).start();
|
|
||||||
|
|
||||||
const browser = await chromium.launch();
|
|
||||||
const context = await browser.newContext();
|
|
||||||
const page = await context.newPage();
|
|
||||||
const actions = new OnboardingActions(page, db);
|
|
||||||
const assertions = new OnboardingAssertions(page, db);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Act
|
|
||||||
await page.goto(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}`);
|
|
||||||
await actions.startOnboardingAsync("scratch");
|
|
||||||
await actions.processUserStepAsync({
|
|
||||||
username: "admin",
|
|
||||||
password: "Comp(exP4sswOrd",
|
|
||||||
confirmPassword: "Comp(exP4sswOrd",
|
|
||||||
});
|
|
||||||
await actions.processSettingsStepAsync();
|
|
||||||
await actions.processIntegrationsStepAsync();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
await assertions.assertFinishStepVisibleAsync();
|
|
||||||
await assertions.assertUserAndAdminGroupInsertedAsync("admin");
|
|
||||||
await assertions.assertDbOnboardingStepAsync("finish");
|
|
||||||
} finally {
|
|
||||||
await Promise.all([browser.close(), homarrContainer.stop()]);
|
|
||||||
}
|
|
||||||
}, 60_000);
|
|
||||||
|
|
||||||
test("External provider onboarding setup should be successful", async () => {
|
test("External provider onboarding setup should be successful", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const { db, localMountPath } = await createSqliteDbFileAsync();
|
const { db, localMountPath } = await createSqliteDbFileAsync();
|
||||||
|
|||||||
@@ -148,6 +148,14 @@ const happyResponder = (method: string) => {
|
|||||||
];
|
];
|
||||||
case "pool.query":
|
case "pool.query":
|
||||||
return [{ name: "tank", status: "ONLINE", healthy: true, free: 500, size: 1000, allocated: 500 }];
|
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":
|
case "interface.query":
|
||||||
return [{ id: "eth0", name: "eth0" }];
|
return [{ id: "eth0", name: "eth0" }];
|
||||||
case "reporting.netdata_get_data":
|
case "reporting.netdata_get_data":
|
||||||
@@ -191,6 +199,18 @@ describe("TrueNasIntegration", () => {
|
|||||||
// JSON-RPC does not use the legacy "connect" session handshake.
|
// JSON-RPC does not use the legacy "connect" session handshake.
|
||||||
expect(ws.sentPayloads.some((payload) => payload.msg === "connect")).toBe(false);
|
expect(ws.sentPayloads.some((payload) => payload.msg === "connect")).toBe(false);
|
||||||
|
|
||||||
|
const datasetPayload = ws.sentPayloads.find((payload) => payload.method === "pool.dataset.query");
|
||||||
|
expect(datasetPayload).toMatchObject({
|
||||||
|
params: [
|
||||||
|
[["id", "in", ["tank"]]],
|
||||||
|
{
|
||||||
|
extra: {
|
||||||
|
properties: ["used", "available"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
expect(result).toMatchObject({
|
expect(result).toMatchObject({
|
||||||
version: "TrueNAS-SCALE-24.10",
|
version: "TrueNAS-SCALE-24.10",
|
||||||
cpuModelName: "Intel Xeon",
|
cpuModelName: "Intel Xeon",
|
||||||
@@ -204,12 +224,25 @@ describe("TrueNasIntegration", () => {
|
|||||||
loadAverage: null,
|
loadAverage: null,
|
||||||
gpu: [],
|
gpu: [],
|
||||||
network: { up: 20_000, down: 10_000 },
|
network: { up: 20_000, down: 10_000 },
|
||||||
// `available` is the free space left on the pool (free = 500), not its total size.
|
// `available` and `used` come from the root dataset, not the physical pool.
|
||||||
fileSystem: [{ deviceName: "tank", available: "500", used: "500", percentage: 50 }],
|
fileSystem: [{ deviceName: "tank", available: "300", used: "200", percentage: 40 }],
|
||||||
smart: [{ deviceName: "tank", healthy: true, overallStatus: "ONLINE", temperature: null }],
|
smart: [{ deviceName: "tank", healthy: true, overallStatus: "ONLINE", temperature: null }],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("reports zero usage for an empty root dataset", async () => {
|
||||||
|
ws.setResponder((method) =>
|
||||||
|
method === "pool.dataset.query"
|
||||||
|
? [{ id: "tank", used: { parsed: 0 }, available: { parsed: 0 } }]
|
||||||
|
: happyResponder(method),
|
||||||
|
);
|
||||||
|
const integration = createIntegration(credentials);
|
||||||
|
|
||||||
|
const result = await integration.getSystemInfoAsync();
|
||||||
|
|
||||||
|
expect(result.fileSystem[0]?.percentage).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
test("authenticates with an API key when one is configured", async () => {
|
test("authenticates with an API key when one is configured", async () => {
|
||||||
const integration = createIntegration([{ kind: "apiKey", value: "api-key-123" }]);
|
const integration = createIntegration([{ kind: "apiKey", value: "api-key-123" }]);
|
||||||
|
|
||||||
|
|||||||
@@ -49,9 +49,10 @@ export class TrueNasIntegration extends Integration implements ISystemHealthMoni
|
|||||||
memUsedInBytes,
|
memUsedInBytes,
|
||||||
fileSystem: datasets.map((dataset) => ({
|
fileSystem: datasets.map((dataset) => ({
|
||||||
deviceName: dataset.name,
|
deviceName: dataset.name,
|
||||||
available: `${dataset.free}`, // free space left on the pool
|
available: `${dataset.available}`,
|
||||||
used: `${dataset.allocated}`,
|
used: `${dataset.used}`,
|
||||||
percentage: (dataset.allocated / dataset.size) * 100,
|
percentage:
|
||||||
|
dataset.used + dataset.available === 0 ? 0 : (dataset.used / (dataset.used + dataset.available)) * 100,
|
||||||
})),
|
})),
|
||||||
availablePkgUpdates: 0,
|
availablePkgUpdates: 0,
|
||||||
network: {
|
network: {
|
||||||
@@ -117,12 +118,40 @@ export class TrueNasIntegration extends Integration implements ISystemHealthMoni
|
|||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
.filter((pool) => pool !== 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", {
|
logger.debug("Retrieved pools", {
|
||||||
url: this.integration.url,
|
url: this.integration.url,
|
||||||
totalCount: result.length,
|
totalCount: result.length,
|
||||||
activeCount: activePools.length,
|
activeCount: poolsWithUsableSpace.length,
|
||||||
});
|
});
|
||||||
return activePools;
|
return poolsWithUsableSpace;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -263,6 +292,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(
|
const reportingNetDataSchema = z.array(
|
||||||
z.object({
|
z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
|
|||||||
Reference in new issue
Block a user