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>
206 lines
12 KiB
SQL
206 lines
12 KiB
SQL
-- =====================================================================
|
||
-- 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;
|