netpulse-migrate замість PowerShell-скрипта: у контейнері немає ані psql, ані PowerShell, а тягнути клієнт Postgres в образ заради одного запуску — це половина дистрибутива на порожньому місці. Міграції вшиті через embed і переїхали в server/migrations: embed не бачить нічого за межами кореня свого модуля, а міграції поруч із бінарником, який їх накочує, не можуть розійтися версіями. Накочування під advisory-блокуванням: два інстанси при rolling update інакше застосували б ту саму міграцію двічі. Кожен файл в одній транзакції разом із записом у schema_migrations; виняток — continuous aggregates, які TimescaleDB забороняє в транзакції. Змінена вже застосована міграція зупиняє запуск: у різних інсталяціях інакше опиниться різна схема під одним номером. Перевірено на чистій базі: 23 міграції, 101 таблиця, повторний запуск каже «схема актуальна». Веб віддає сам API через embed: на self-hosted це прибирає з інструкції встановлення цілий компонент. Три політики кешування — назавжди для assets із хешем у імені, ніколи для index.html, коротко для решти. Знайдено живим прогоном: невідомий шлях під /api/ віддавав 200 з index.html, і клієнт падав на розборі HTML як JSON замість чесного 404. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
136 lines
6.9 KiB
SQL
136 lines
6.9 KiB
SQL
-- =====================================================================
|
||
-- NetPulse :: 0003_agents_plugins.sql
|
||
-- Зонди (Go-агенти), реєстр плагінів, завдання опитування, черги.
|
||
-- =====================================================================
|
||
|
||
-- ---------------------------------------------------------------------
|
||
-- Реєстр плагінів (Plugin-First Architecture)
|
||
-- Ядро знає лише про Auth/RBAC/EventBus/TSDB/API — решта тут.
|
||
-- ---------------------------------------------------------------------
|
||
|
||
CREATE TYPE core.plugin_scope AS ENUM ('agent','server','ui','both');
|
||
|
||
CREATE TABLE core.plugins (
|
||
key core.slug PRIMARY KEY, -- icmp, snmp, ncm, topology, netflow, modbus
|
||
name text NOT NULL,
|
||
version text NOT NULL,
|
||
scope core.plugin_scope NOT NULL,
|
||
publisher text NOT NULL DEFAULT 'netpulse',
|
||
description text,
|
||
-- Оголошення можливостей: метрики, типи чеків, UI-панелі, права доступу
|
||
manifest jsonb NOT NULL,
|
||
-- Мінімальний тариф, з якого плагін доступний (NULL = усі)
|
||
min_plan_key core.slug,
|
||
checksum bytea, -- sha256 бінарника/бандла
|
||
is_core boolean NOT NULL DEFAULT false,
|
||
created_at timestamptz NOT NULL DEFAULT now()
|
||
);
|
||
|
||
CREATE TABLE core.plugin_installs (
|
||
id uuid PRIMARY KEY DEFAULT core.new_id(),
|
||
tenant_id uuid NOT NULL REFERENCES core.tenants(id) ON DELETE CASCADE,
|
||
plugin_key core.slug NOT NULL REFERENCES core.plugins(key) ON DELETE CASCADE,
|
||
enabled boolean NOT NULL DEFAULT true,
|
||
config jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||
installed_at timestamptz NOT NULL DEFAULT now(),
|
||
UNIQUE (tenant_id, plugin_key)
|
||
);
|
||
|
||
-- ---------------------------------------------------------------------
|
||
-- Агенти / зонди
|
||
-- ---------------------------------------------------------------------
|
||
|
||
CREATE TYPE core.agent_status AS ENUM ('pending','online','degraded','offline','disabled');
|
||
|
||
CREATE TABLE core.agents (
|
||
id uuid PRIMARY KEY DEFAULT core.new_id(),
|
||
tenant_id uuid NOT NULL REFERENCES core.tenants(id) ON DELETE CASCADE,
|
||
site_id uuid REFERENCES inv.sites(id) ON DELETE SET NULL,
|
||
name text NOT NULL,
|
||
-- Автентифікація агента: sha256(enrollment/agent token). Тільки вихідний gRPC/TLS.
|
||
token_hash bytea NOT NULL UNIQUE,
|
||
fingerprint text, -- TLS client cert SPKI pin
|
||
version text,
|
||
os text,
|
||
arch text,
|
||
hostname text,
|
||
public_ip inet,
|
||
status core.agent_status NOT NULL DEFAULT 'pending',
|
||
last_heartbeat_at timestamptz,
|
||
-- Які модулі активовані сервером на цьому агенті
|
||
enabled_modules core.slug[] NOT NULL DEFAULT '{icmp}',
|
||
-- Ліміти опитування: {"max_concurrency":256,"icmp_rate_pps":500}
|
||
limits jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||
-- Останні самометрики агента (RAM/CPU/черга) для швидкого показу в UI
|
||
health jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||
created_at timestamptz NOT NULL DEFAULT now(),
|
||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||
UNIQUE (tenant_id, name)
|
||
);
|
||
CREATE INDEX agents_tenant_status_idx ON core.agents (tenant_id, status);
|
||
CREATE TRIGGER trg_agents_touch BEFORE UPDATE ON core.agents
|
||
FOR EACH ROW EXECUTE FUNCTION core.touch_updated_at();
|
||
|
||
-- Відкладений FK з 0002 (devices.agent_id)
|
||
ALTER TABLE inv.devices
|
||
ADD CONSTRAINT devices_agent_fk
|
||
FOREIGN KEY (agent_id) REFERENCES core.agents(id) ON DELETE SET NULL;
|
||
|
||
-- ---------------------------------------------------------------------
|
||
-- Завдання опитування (те, що агент реально виконує)
|
||
-- ---------------------------------------------------------------------
|
||
|
||
CREATE TABLE core.check_types (
|
||
-- Ключ у формі "<plugin>.<check>" — крапка не проходить core.slug, тому власний CHECK
|
||
key text PRIMARY KEY CHECK (key ~ '^[a-z0-9]+(\.[a-z0-9_]+)+$'),
|
||
-- icmp.ping, snmp.get, snmp.walk, http.status,
|
||
plugin_key core.slug NOT NULL REFERENCES core.plugins(key) ON DELETE CASCADE,
|
||
name text NOT NULL,
|
||
-- JSON Schema параметрів + перелік метрик, які повертає чек
|
||
params_schema jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||
metrics jsonb NOT NULL DEFAULT '[]'::jsonb,
|
||
|
||
-- Префікс типу чека ЗОБОВ'ЯЗАНИЙ дорівнювати ключу плагіна: саме за
|
||
-- ним агент обирає модуль-виконавця ("snmp.if" -> модуль snmp).
|
||
-- Без цього обмеження неузгодженість помічається аж у полі, коли
|
||
-- зонд відхиляє задачу як адресовану неіснуючому модулю.
|
||
CONSTRAINT check_types_prefix_matches_plugin
|
||
CHECK (key LIKE plugin_key || '.%')
|
||
);
|
||
|
||
CREATE TABLE core.checks (
|
||
id uuid PRIMARY KEY DEFAULT core.new_id(),
|
||
tenant_id uuid NOT NULL REFERENCES core.tenants(id) ON DELETE CASCADE,
|
||
device_id uuid NOT NULL REFERENCES inv.devices(id) ON DELETE CASCADE,
|
||
interface_id uuid REFERENCES inv.interfaces(id) ON DELETE CASCADE,
|
||
check_type text NOT NULL REFERENCES core.check_types(key) ON DELETE RESTRICT,
|
||
params jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||
interval_sec int NOT NULL DEFAULT 60 CHECK (interval_sec BETWEEN 5 AND 86400),
|
||
timeout_ms int NOT NULL DEFAULT 3000,
|
||
retries int NOT NULL DEFAULT 2,
|
||
enabled boolean NOT NULL DEFAULT true,
|
||
-- Стан планувальника
|
||
last_run_at timestamptz,
|
||
next_run_at timestamptz,
|
||
last_error text,
|
||
created_at timestamptz NOT NULL DEFAULT now(),
|
||
updated_at timestamptz NOT NULL DEFAULT now()
|
||
);
|
||
CREATE INDEX checks_device_idx ON core.checks (device_id);
|
||
CREATE INDEX checks_schedule_idx ON core.checks (next_run_at) WHERE enabled;
|
||
CREATE UNIQUE INDEX checks_uniq
|
||
ON core.checks (device_id, check_type, COALESCE(interface_id, '00000000-0000-0000-0000-000000000000'::uuid), md5(params::text));
|
||
|
||
-- ---------------------------------------------------------------------
|
||
-- Event Bus (outbox): усе, що йде у Redis/WebSocket, спершу лягає сюди
|
||
-- ---------------------------------------------------------------------
|
||
|
||
CREATE TABLE core.event_outbox (
|
||
id bigserial PRIMARY KEY,
|
||
tenant_id uuid NOT NULL,
|
||
topic text NOT NULL, -- device.status, link.status, alert.fired, ncm.diff
|
||
payload jsonb NOT NULL,
|
||
created_at timestamptz NOT NULL DEFAULT now(),
|
||
published_at timestamptz
|
||
);
|
||
CREATE INDEX event_outbox_pending_idx ON core.event_outbox (id) WHERE published_at IS NULL;
|