Netpulse_SasS/server/migrations/0003_agents_plugins.sql
byrsapty 205dd5e079 Пакування: runner міграцій і вшитий у бінарник фронтенд
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>
2026-08-25 01:01:55 +03:00

136 lines
6.9 KiB
SQL
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- =====================================================================
-- 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;