mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +00:00
feat: drop proxmox host columns, use plugin settings instead
This commit is contained in:
1 parent
37c015a760
commit
bf147096eb
43 files changed
+835
-1999
No files matched your search
@@ -1,117 +0,0 @@
|
||||
import fs from "fs";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import Database from "better-sqlite3";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
/**
|
||||
* The Proxmox Stats feature adds `enable_proxmox_stats` and
|
||||
* `proxmox_stats_config` to `ssh_data`, backfilled via `addColumnIfNotExists`
|
||||
* next to the existing `enable_proxmox`/`proxmox_config` columns. Verify the
|
||||
* migration adds both columns, with the right default, on a database that
|
||||
* predates them.
|
||||
*/
|
||||
describe("proxmox stats columns migration", () => {
|
||||
let dataDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "termix-proxmox-stats-"));
|
||||
vi.resetModules();
|
||||
process.env.DATA_DIR = dataDir;
|
||||
process.env.DB_FILE_ENCRYPTION = "false";
|
||||
process.env.ALLOW_EMPTY_DATA_DIR = "true";
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.DATA_DIR;
|
||||
delete process.env.DB_FILE_ENCRYPTION;
|
||||
delete process.env.ALLOW_EMPTY_DATA_DIR;
|
||||
fs.rmSync(dataDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function writePreUpgradeDatabase(): void {
|
||||
const seed = new Database(":memory:");
|
||||
seed.exec(`
|
||||
CREATE TABLE users (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT NOT NULL,
|
||||
password_hash TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE ssh_data (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id TEXT NOT NULL,
|
||||
name TEXT,
|
||||
ip TEXT NOT NULL,
|
||||
port INTEGER NOT NULL,
|
||||
username TEXT NOT NULL,
|
||||
auth_type TEXT NOT NULL DEFAULT 'password',
|
||||
enable_proxmox INTEGER NOT NULL DEFAULT 0,
|
||||
proxmox_config TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
INSERT INTO users (id, username, password_hash)
|
||||
VALUES ('owner', 'alice', 'hash');
|
||||
|
||||
INSERT INTO ssh_data (id, user_id, name, ip, port, username, auth_type, enable_proxmox)
|
||||
VALUES (1, 'owner', 'pve node', '10.0.0.9', 22, 'root', 'password', 1);
|
||||
`);
|
||||
fs.writeFileSync(path.join(dataDir, "db.sqlite"), seed.serialize());
|
||||
seed.close();
|
||||
}
|
||||
|
||||
it("adds enable_proxmox_stats (default 0) and proxmox_stats_config (nullable) columns", async () => {
|
||||
writePreUpgradeDatabase();
|
||||
|
||||
const db = await import("../../../../src/backend/database/db/index.js");
|
||||
await db.initializeDatabase();
|
||||
const sqlite = db.getSqlite();
|
||||
|
||||
const columns = sqlite
|
||||
.prepare("PRAGMA table_info(ssh_data)")
|
||||
.all() as Array<{
|
||||
name: string;
|
||||
notnull: number;
|
||||
dflt_value: string | null;
|
||||
}>;
|
||||
|
||||
const enableCol = columns.find((c) => c.name === "enable_proxmox_stats");
|
||||
expect(enableCol).toBeDefined();
|
||||
expect(enableCol?.notnull).toBe(1);
|
||||
|
||||
const configCol = columns.find((c) => c.name === "proxmox_stats_config");
|
||||
expect(configCol).toBeDefined();
|
||||
expect(configCol?.notnull).toBe(0);
|
||||
|
||||
const row = sqlite
|
||||
.prepare(
|
||||
"SELECT enable_proxmox_stats, proxmox_stats_config FROM ssh_data WHERE id = 1",
|
||||
)
|
||||
.get() as {
|
||||
enable_proxmox_stats: number;
|
||||
proxmox_stats_config: string | null;
|
||||
};
|
||||
|
||||
// Pre-existing rows default to disabled, independent of enable_proxmox.
|
||||
expect(row.enable_proxmox_stats).toBe(0);
|
||||
expect(row.proxmox_stats_config).toBeNull();
|
||||
});
|
||||
|
||||
it("creates the proxmox_node_history and proxmox_stats_preferences tables", async () => {
|
||||
writePreUpgradeDatabase();
|
||||
|
||||
const db = await import("../../../../src/backend/database/db/index.js");
|
||||
await db.initializeDatabase();
|
||||
const sqlite = db.getSqlite();
|
||||
|
||||
const tables = sqlite
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type = 'table'")
|
||||
.pluck()
|
||||
.all() as string[];
|
||||
|
||||
expect(tables).toContain("proxmox_node_history");
|
||||
expect(tables).toContain("proxmox_stats_preferences");
|
||||
});
|
||||
});
|
||||
@@ -1,27 +1,20 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
|
||||
const historyCreate = vi.fn();
|
||||
const historyPrune = vi.fn();
|
||||
vi.mock("../../../../src/backend/database/repositories/factory.js", () => ({
|
||||
getCurrentSettingValue: () => null,
|
||||
createCurrentProxmoxNodeHistoryRepository: () => ({
|
||||
create: historyCreate,
|
||||
pruneOlderThan: historyPrune,
|
||||
vi.mock("../../src/backend/plugin-ctx.js", () => ({
|
||||
pluginCtx: () => ({
|
||||
log: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
|
||||
}),
|
||||
}));
|
||||
|
||||
const collectProxmoxStats = vi.fn();
|
||||
vi.mock(
|
||||
"../../../../src/backend/hosts/metrics/proxmox/collect-proxmox-stats.js",
|
||||
() => ({
|
||||
collectProxmoxStats: (...args: unknown[]) => collectProxmoxStats(...args),
|
||||
}),
|
||||
);
|
||||
vi.mock("../../src/backend/proxmox/collect-proxmox-stats.js", () => ({
|
||||
collectProxmoxStats: (...args: unknown[]) => collectProxmoxStats(...args),
|
||||
}));
|
||||
|
||||
import {
|
||||
ProxmoxPollingManager,
|
||||
parseProxmoxStatsConfig,
|
||||
} from "../../../../src/backend/hosts/metrics/proxmox-stats-polling.js";
|
||||
} from "../../src/backend/proxmox-stats-polling.js";
|
||||
import type { Client } from "ssh2";
|
||||
|
||||
interface TestHost {
|
||||
@@ -50,8 +43,6 @@ function snapshot(overrides: Partial<Record<string, unknown>> = {}) {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
historyCreate.mockReset();
|
||||
historyPrune.mockReset();
|
||||
collectProxmoxStats.mockReset();
|
||||
});
|
||||
|
||||
@@ -88,11 +79,17 @@ describe("ProxmoxPollingManager", () => {
|
||||
async (_host: TestHost, fn: (client: Client) => Promise<unknown>) =>
|
||||
fn({} as Client),
|
||||
);
|
||||
const historyRepository = {
|
||||
create: vi.fn(async () => {}),
|
||||
pruneOlderThan: vi.fn(async () => {}),
|
||||
listRange: vi.fn(async () => []),
|
||||
};
|
||||
const manager = new ProxmoxPollingManager<TestHost>({
|
||||
fetchHostById,
|
||||
withSshConnection,
|
||||
historyRepository,
|
||||
});
|
||||
return { manager, fetchHostById, withSshConnection };
|
||||
return { manager, fetchHostById, withSshConnection, historyRepository };
|
||||
}
|
||||
|
||||
it("starts polling and caches a snapshot when the first viewer registers", async () => {
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
const execCommand = vi.fn();
|
||||
vi.mock(
|
||||
"../../../../../src/backend/hosts/metrics-shared/common-utils.js",
|
||||
() => ({
|
||||
execCommand: (...args: unknown[]) => execCommand(...args),
|
||||
}),
|
||||
);
|
||||
vi.mock("../../../src/backend/proxmox/common-utils.js", () => ({
|
||||
execCommand: (...args: unknown[]) => execCommand(...args),
|
||||
}));
|
||||
|
||||
import { collectProxmoxClusterHealth } from "../../../../../src/backend/hosts/metrics/proxmox/cluster-health-collector.js";
|
||||
import { collectProxmoxClusterHealth } from "../../../src/backend/proxmox/cluster-health-collector.js";
|
||||
import type { Client } from "ssh2";
|
||||
|
||||
const fakeClient = {} as Client;
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
const execCommand = vi.fn();
|
||||
vi.mock(
|
||||
"../../../../../src/backend/hosts/metrics-shared/common-utils.js",
|
||||
() => ({
|
||||
execCommand: (...args: unknown[]) => execCommand(...args),
|
||||
toFixedNum: (n: number | null | undefined, digits = 2) => {
|
||||
if (typeof n !== "number" || !Number.isFinite(n)) return null;
|
||||
return Number(n.toFixed(digits));
|
||||
},
|
||||
}),
|
||||
);
|
||||
vi.mock("../../../src/backend/proxmox/common-utils.js", () => ({
|
||||
execCommand: (...args: unknown[]) => execCommand(...args),
|
||||
toFixedNum: (n: number | null | undefined, digits = 2) => {
|
||||
if (typeof n !== "number" || !Number.isFinite(n)) return null;
|
||||
return Number(n.toFixed(digits));
|
||||
},
|
||||
}));
|
||||
|
||||
import { collectProxmoxStats } from "../../../../../src/backend/hosts/metrics/proxmox/collect-proxmox-stats.js";
|
||||
import { collectProxmoxStats } from "../../../src/backend/proxmox/collect-proxmox-stats.js";
|
||||
import type { Client } from "ssh2";
|
||||
|
||||
const fakeClient = {} as Client;
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
const execCommand = vi.fn();
|
||||
vi.mock(
|
||||
"../../../../../src/backend/hosts/metrics-shared/common-utils.js",
|
||||
() => ({
|
||||
execCommand: (...args: unknown[]) => execCommand(...args),
|
||||
toFixedNum: (n: number | null | undefined, digits = 2) => {
|
||||
if (typeof n !== "number" || !Number.isFinite(n)) return null;
|
||||
return Number(n.toFixed(digits));
|
||||
},
|
||||
}),
|
||||
);
|
||||
vi.mock("../../../src/backend/proxmox/common-utils.js", () => ({
|
||||
execCommand: (...args: unknown[]) => execCommand(...args),
|
||||
toFixedNum: (n: number | null | undefined, digits = 2) => {
|
||||
if (typeof n !== "number" || !Number.isFinite(n)) return null;
|
||||
return Number(n.toFixed(digits));
|
||||
},
|
||||
}));
|
||||
|
||||
import { collectProxmoxGuestsSummary } from "../../../../../src/backend/hosts/metrics/proxmox/guests-collector.js";
|
||||
import { collectProxmoxGuestsSummary } from "../../../src/backend/proxmox/guests-collector.js";
|
||||
import type { Client } from "ssh2";
|
||||
|
||||
const fakeClient = {} as Client;
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
const execCommand = vi.fn();
|
||||
vi.mock(
|
||||
"../../../../../src/backend/hosts/metrics-shared/common-utils.js",
|
||||
() => ({
|
||||
execCommand: (...args: unknown[]) => execCommand(...args),
|
||||
}),
|
||||
);
|
||||
vi.mock("../../../src/backend/proxmox/common-utils.js", () => ({
|
||||
execCommand: (...args: unknown[]) => execCommand(...args),
|
||||
}));
|
||||
|
||||
import { collectProxmoxNodeNetwork } from "../../../../../src/backend/hosts/metrics/proxmox/node-network-collector.js";
|
||||
import { collectProxmoxNodeNetwork } from "../../../src/backend/proxmox/node-network-collector.js";
|
||||
import type { Client } from "ssh2";
|
||||
|
||||
const fakeClient = {} as Client;
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
const execCommand = vi.fn();
|
||||
vi.mock(
|
||||
"../../../../../src/backend/hosts/metrics-shared/common-utils.js",
|
||||
() => ({
|
||||
execCommand: (...args: unknown[]) => execCommand(...args),
|
||||
toFixedNum: (n: number | null | undefined, digits = 2) => {
|
||||
if (typeof n !== "number" || !Number.isFinite(n)) return null;
|
||||
return Number(n.toFixed(digits));
|
||||
},
|
||||
}),
|
||||
);
|
||||
vi.mock("../../../src/backend/proxmox/common-utils.js", () => ({
|
||||
execCommand: (...args: unknown[]) => execCommand(...args),
|
||||
toFixedNum: (n: number | null | undefined, digits = 2) => {
|
||||
if (typeof n !== "number" || !Number.isFinite(n)) return null;
|
||||
return Number(n.toFixed(digits));
|
||||
},
|
||||
}));
|
||||
|
||||
import { collectProxmoxNodeStatus } from "../../../../../src/backend/hosts/metrics/proxmox/node-status-collector.js";
|
||||
import { collectProxmoxNodeStatus } from "../../../src/backend/proxmox/node-status-collector.js";
|
||||
import type { Client } from "ssh2";
|
||||
|
||||
const fakeClient = {} as Client;
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
const execCommand = vi.fn();
|
||||
vi.mock(
|
||||
"../../../../../src/backend/hosts/metrics-shared/common-utils.js",
|
||||
() => ({
|
||||
execCommand: (...args: unknown[]) => execCommand(...args),
|
||||
toFixedNum: (n: number | null | undefined, digits = 2) => {
|
||||
if (typeof n !== "number" || !Number.isFinite(n)) return null;
|
||||
return Number(n.toFixed(digits));
|
||||
},
|
||||
}),
|
||||
);
|
||||
vi.mock("../../../src/backend/proxmox/common-utils.js", () => ({
|
||||
execCommand: (...args: unknown[]) => execCommand(...args),
|
||||
toFixedNum: (n: number | null | undefined, digits = 2) => {
|
||||
if (typeof n !== "number" || !Number.isFinite(n)) return null;
|
||||
return Number(n.toFixed(digits));
|
||||
},
|
||||
}));
|
||||
|
||||
import { collectProxmoxStorage } from "../../../../../src/backend/hosts/metrics/proxmox/storage-collector.js";
|
||||
import { collectProxmoxStorage } from "../../../src/backend/proxmox/storage-collector.js";
|
||||
import type { Client } from "ssh2";
|
||||
|
||||
const fakeClient = {} as Client;
|
||||
|
||||
+31
-27
@@ -1,30 +1,34 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { TestSqliteDatabase } from "../../../../../src/backend/tests/database/repositories/test-support.js";
|
||||
import { ProxmoxNodeHistoryRepository } from "../../../../../src/backend/database/repositories/proxmox-node-history-repository.js";
|
||||
import { createTestDb, type TestDb } from "@termix/plugin-sdk/testing";
|
||||
import { proxmoxNodeHistory } from "../../../src/backend/tables.js";
|
||||
import {
|
||||
createProxmoxNodeHistoryRepository,
|
||||
type ProxmoxNodeHistoryRepository,
|
||||
} from "../../../src/backend/proxmox-node-history-repository.js";
|
||||
import { pluginDir } from "../helpers.js";
|
||||
|
||||
describe("ProxmoxNodeHistoryRepository", () => {
|
||||
let adapter: TestSqliteDatabase | null = null;
|
||||
let db: TestDb | null = null;
|
||||
|
||||
afterEach(async () => {
|
||||
if (adapter) {
|
||||
await adapter.close();
|
||||
adapter = null;
|
||||
}
|
||||
afterEach(() => {
|
||||
db?.close();
|
||||
db = null;
|
||||
});
|
||||
|
||||
async function createRepository(
|
||||
onWrite?: () => void | Promise<void>,
|
||||
): Promise<ProxmoxNodeHistoryRepository> {
|
||||
adapter = new TestSqliteDatabase();
|
||||
const context = await adapter.connect();
|
||||
await adapter.exec(`
|
||||
INSERT INTO users (id, username, password_hash) VALUES
|
||||
('user-1', 'user-1', 'hash'),
|
||||
('user-2', 'user-2', 'hash');
|
||||
|
||||
INSERT INTO ssh_data (id, user_id, name, ip, port, username, auth_type)
|
||||
VALUES (1, 'user-1', 'pve1', '10.0.0.1', 22, 'root', 'password'), (2, 'user-2', 'pve2', '10.0.0.2', 22, 'root', 'password');
|
||||
INSERT INTO proxmox_node_history (
|
||||
async function createRepository(): Promise<ProxmoxNodeHistoryRepository> {
|
||||
db = await createTestDb(pluginDir, {
|
||||
before: (sqlite) => {
|
||||
sqlite.exec(
|
||||
"INSERT INTO users (id, username) VALUES ('user-1', 'user-1'), ('user-2', 'user-2')",
|
||||
);
|
||||
sqlite.exec("INSERT INTO ssh_data (id) VALUES (1), (2)");
|
||||
},
|
||||
skipMigrations: false,
|
||||
});
|
||||
// Migrations already ran (adopting proxmox_node_history), so seed rows on
|
||||
// the real table after the fact.
|
||||
db.sqlite.exec(`
|
||||
INSERT INTO p_proxmox_node_history (
|
||||
host_id, ts, cpu_percent, mem_percent, disk_percent, net_rx_bytes, net_tx_bytes
|
||||
)
|
||||
VALUES
|
||||
@@ -34,14 +38,14 @@ describe("ProxmoxNodeHistoryRepository", () => {
|
||||
(2, '2026-01-02 00:00:00', 99, 99, 99, 999, 999);
|
||||
`);
|
||||
|
||||
return new ProxmoxNodeHistoryRepository(context, onWrite);
|
||||
return createProxmoxNodeHistoryRepository(
|
||||
db.database,
|
||||
await db.database.define(proxmoxNodeHistory),
|
||||
);
|
||||
}
|
||||
|
||||
it("creates and lists node history rows by range", async () => {
|
||||
let writeCount = 0;
|
||||
const repo = await createRepository(() => {
|
||||
writeCount += 1;
|
||||
});
|
||||
const repo = await createRepository();
|
||||
|
||||
await repo.create({
|
||||
hostId: 1,
|
||||
@@ -59,7 +63,7 @@ describe("ProxmoxNodeHistoryRepository", () => {
|
||||
);
|
||||
|
||||
expect(rows.map((row) => row.cpuPercent)).toEqual([10, 11]);
|
||||
expect(writeCount).toBe(1);
|
||||
expect(db!.persisted).toBe(1);
|
||||
});
|
||||
|
||||
it("prunes old history for a host only", async () => {
|
||||
|
||||
Reference in new issue
Block a user