Netpulse_SasS/server/migrations/0015_templates.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

206 lines
12 KiB
SQL
Raw 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 :: 0015_templates.sql
-- Шаблони опитування: набір метрик, який чіпляється до хоста одним
-- рухом, замість того щоб заводити кожен OID руками.
--
-- Навіщо окрема сутність, а не просто чеки: те, що знімається з
-- Mikrotik, однакове на всіх Mikrotik. Без шаблону цей факт живе в
-- голові інженера й повторюється стільки разів, скільки в мережі
-- пристроїв. Із шаблоном він живе в одному місці, і виправлення OID
-- доїжджає до всіх хостів само.
-- =====================================================================
CREATE SCHEMA IF NOT EXISTS tpl;
-- ---------------------------------------------------------------------
-- Шаблон
-- ---------------------------------------------------------------------
-- tenant_id IS NULL — вбудований шаблон, спільний для всіх (той самий
-- прийом, що в ncm.profiles). Тенант може завести власний; вбудовані
-- при цьому лишаються недоторканими.
CREATE TABLE tpl.templates (
id uuid PRIMARY KEY DEFAULT core.new_id(),
tenant_id uuid REFERENCES core.tenants(id) ON DELETE CASCADE,
key core.slug NOT NULL,
name text NOT NULL,
description text,
-- Підказка для UI: який шаблон запропонувати для цього виробника.
-- Саме підказка, а не автопризначення: помилка виробника в інвентарі
-- не має мовчки почати опитувати хост чужими OID.
vendor text,
is_builtin boolean NOT NULL DEFAULT false,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX tpl_templates_key_uniq
ON tpl.templates (COALESCE(tenant_id,'00000000-0000-0000-0000-000000000000'::uuid), key);
CREATE INDEX tpl_templates_vendor_idx ON tpl.templates (lower(vendor));
-- ---------------------------------------------------------------------
-- Елемент шаблону — одна метрика
-- ---------------------------------------------------------------------
-- Один рядок = один OID = одна метрика. Групування в реальні чеки
-- робить реконсиляція: тримати тут «чек» означало б змішати те, що
-- описує людина (метрику), з тим, що вигідно машині (пачку OID в
-- одному PDU).
CREATE TABLE tpl.items (
id uuid PRIMARY KEY DEFAULT core.new_id(),
template_id uuid NOT NULL REFERENCES tpl.templates(id) ON DELETE CASCADE,
key core.slug NOT NULL,
name text NOT NULL,
check_type text NOT NULL REFERENCES core.check_types(key) ON DELETE RESTRICT,
oid text NOT NULL,
metric_key text NOT NULL,
unit text NOT NULL DEFAULT '',
-- Множник: сенсори часто віддають десяті градуса цілим числом.
scale double precision NOT NULL DEFAULT 1 CHECK (scale <> 0),
interval_sec int NOT NULL DEFAULT 60 CHECK (interval_sec BETWEEN 5 AND 86400),
enabled boolean NOT NULL DEFAULT true,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX tpl_items_key_uniq ON tpl.items (template_id, key);
CREATE INDEX tpl_items_template_idx ON tpl.items (template_id);
-- ---------------------------------------------------------------------
-- Прив'язка шаблону до хоста
-- ---------------------------------------------------------------------
CREATE TABLE tpl.device_templates (
device_id uuid NOT NULL REFERENCES inv.devices(id) ON DELETE CASCADE,
template_id uuid NOT NULL REFERENCES tpl.templates(id) ON DELETE CASCADE,
tenant_id uuid NOT NULL REFERENCES core.tenants(id) ON DELETE CASCADE,
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (device_id, template_id)
);
CREATE INDEX tpl_device_templates_tpl_idx ON tpl.device_templates (template_id);
-- ---------------------------------------------------------------------
-- Слід шаблону в чеках
-- ---------------------------------------------------------------------
-- Чеки, породжені шаблоном, треба вміти впізнати: інакше відв'язування
-- шаблону не знало б, що прибирати, а зміна OID плодила б другий чек
-- замість правки першого.
--
-- ON DELETE CASCADE, а не SET NULL: чек без шаблону, який його створив,
-- нікому не належить і нікому не потрібен — він би просто тихо опитував
-- пристрій вічно.
ALTER TABLE core.checks
ADD COLUMN template_id uuid REFERENCES tpl.templates(id) ON DELETE CASCADE;
-- Один чек на (хост, шаблон, тип, інтервал). Саме інтервал у ключі, бо
-- елементи з різною частотою не можна класти в один PDU: пачка ходить
-- цілком і настільки часто, наскільки просить найшвидший її учасник.
--
-- Окремий індекс замість спільного checks_uniq: той містить md5(params),
-- тож будь-яка правка списку OID виглядала б як новий чек.
CREATE UNIQUE INDEX checks_template_uniq
ON core.checks (device_id, template_id, check_type, interval_sec)
WHERE template_id IS NOT NULL;
-- ---------------------------------------------------------------------
-- RLS
-- ---------------------------------------------------------------------
GRANT USAGE ON SCHEMA tpl TO netpulse_app, netpulse_worker;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA tpl
TO netpulse_app, netpulse_worker;
ALTER DEFAULT PRIVILEGES IN SCHEMA tpl
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO netpulse_app, netpulse_worker;
ALTER TABLE tpl.templates ENABLE ROW LEVEL SECURITY;
ALTER TABLE tpl.templates FORCE ROW LEVEL SECURITY;
ALTER TABLE tpl.device_templates ENABLE ROW LEVEL SECURITY;
ALTER TABLE tpl.device_templates FORCE ROW LEVEL SECURITY;
ALTER TABLE tpl.items ENABLE ROW LEVEL SECURITY;
ALTER TABLE tpl.items FORCE ROW LEVEL SECURITY;
-- Вбудовані шаблони видно всім, редагувати можна лише свої.
CREATE POLICY templates_visible ON tpl.templates
USING (tenant_id IS NULL OR tenant_id = core.current_tenant())
WITH CHECK (tenant_id = core.current_tenant());
CREATE POLICY device_templates_isolation ON tpl.device_templates
USING (tenant_id = core.current_tenant())
WITH CHECK (tenant_id = core.current_tenant());
-- У tpl.items немає власного tenant_id: він завжди дорівнював би
-- шаблоновому, а дублювання ключа ізоляції — це запрошення до розбіжності.
-- Видимість успадковується від шаблону через ту саму політику.
CREATE POLICY items_follow_template ON tpl.items
USING (EXISTS (
SELECT 1 FROM tpl.templates t
WHERE t.id = tpl.items.template_id
AND (t.tenant_id IS NULL OR t.tenant_id = core.current_tenant())
))
WITH CHECK (EXISTS (
SELECT 1 FROM tpl.templates t
WHERE t.id = tpl.items.template_id AND t.tenant_id = core.current_tenant()
));
-- ---------------------------------------------------------------------
-- Вбудовані шаблони
--
-- Свідомо небагато й свідомо базові. Шаблон, який ніхто не перевіряв на
-- живому залізі, гірший за його відсутність: він створює враження, що
-- хост під наглядом, поки метрики мовчки порожні.
-- ---------------------------------------------------------------------
INSERT INTO tpl.templates (id, tenant_id, key, name, description, vendor, is_builtin) VALUES
('00000000-0000-0000-0000-0000000000c1'::uuid, NULL, 'snmp-generic',
'SNMP: базовий хост',
'Час роботи системи — єдине число, яке віддає будь-який агент SNMP за RFC 1213. Падіння цього лічильника означає перезавантаження.',
NULL, true),
('00000000-0000-0000-0000-0000000000c2'::uuid, NULL, 'snmp-host-resources',
'SNMP: ресурси хоста (HOST-RESOURCES-MIB)',
'Кількість процесів і сесій. Працює на Linux, Windows і більшості мережевих ОС із HOST-RESOURCES-MIB.',
NULL, true),
('00000000-0000-0000-0000-0000000000c3'::uuid, NULL, 'snmp-ucd-linux',
'SNMP: Linux (UCD-SNMP-MIB)',
'Пам''ять, свопінг і середнє навантаження з net-snmp. Найточніше джерело для Linux-серверів.',
'Linux', true),
('00000000-0000-0000-0000-0000000000c4'::uuid, NULL, 'snmp-mikrotik',
'SNMP: Mikrotik RouterOS',
'Процесор, пам''ять і температура плати з MIKROTIK-MIB.',
'Mikrotik', true)
ON CONFLICT DO NOTHING;
INSERT INTO tpl.items (template_id, key, name, check_type, oid, metric_key, unit, scale, interval_sec) VALUES
-- Базовий хост (RFC 1213)
('00000000-0000-0000-0000-0000000000c1', 'uptime', 'Час роботи',
'snmp.get', '.1.3.6.1.2.1.1.3.0', 'sys.uptime_sec', 's', 0.01, 60),
-- HOST-RESOURCES-MIB
--
-- Без hrProcessorLoad: це таблиця, індексована процесором, і
-- «.1» — здогадка, а не адреса. Перевірено на net-snmp: віддає
-- «No Such Instance», тобто шаблон мовчки не збирав би нічого.
-- Для одноядерних платформ, де індекс фіксований (Mikrotik),
-- цей OID лишається у вендорному шаблоні.
('00000000-0000-0000-0000-0000000000c2', 'processes', 'Процесів',
'snmp.get', '.1.3.6.1.2.1.25.1.6.0', 'sys.processes', '', 1, 300),
('00000000-0000-0000-0000-0000000000c2', 'users', 'Сесій користувачів',
'snmp.get', '.1.3.6.1.2.1.25.1.5.0', 'sys.users', '', 1, 300),
-- UCD-SNMP-MIB
('00000000-0000-0000-0000-0000000000c3', 'mem-avail', 'Вільна пам''ять',
'snmp.get', '.1.3.6.1.4.1.2021.4.6.0', 'mem.available_bytes', 'B', 1024, 60),
('00000000-0000-0000-0000-0000000000c3', 'mem-total', 'Всього пам''яті',
'snmp.get', '.1.3.6.1.4.1.2021.4.5.0', 'mem.total_bytes', 'B', 1024, 300),
('00000000-0000-0000-0000-0000000000c3', 'swap-avail', 'Вільний своп',
'snmp.get', '.1.3.6.1.4.1.2021.4.4.0', 'swap.available_bytes', 'B', 1024, 300),
('00000000-0000-0000-0000-0000000000c3', 'load1', 'Навантаження, 1 хв',
'snmp.get', '.1.3.6.1.4.1.2021.10.1.5.1', 'cpu.load1', '', 0.01, 60),
('00000000-0000-0000-0000-0000000000c3', 'load5', 'Навантаження, 5 хв',
'snmp.get', '.1.3.6.1.4.1.2021.10.1.5.2', 'cpu.load5', '', 0.01, 60),
-- MIKROTIK-MIB
('00000000-0000-0000-0000-0000000000c4', 'cpu-load', 'Завантаження CPU',
'snmp.get', '.1.3.6.1.2.1.25.3.3.1.2.1', 'cpu.util_pct', '%', 1, 60),
('00000000-0000-0000-0000-0000000000c4', 'mem-free', 'Вільна пам''ять',
'snmp.get', '.1.3.6.1.2.1.25.2.3.1.6.65536', 'mem.available_bytes', 'B', 1024, 60),
('00000000-0000-0000-0000-0000000000c4', 'board-temp', 'Температура плати',
'snmp.get', '.1.3.6.1.4.1.14988.1.1.3.100.1.3.1', 'sensor.temp_c', '°C', 0.1, 300)
ON CONFLICT DO NOTHING;