Один коміт, а не десяток тематичних, свідомо: теми переплетені в
спільних файлах (store.go, docker-compose.yml, deploy/README.md), і
розділити їх можна було б лише індексуванням шматків. Коміти, які не
збираються, гірші за один великий — тим паче що це рівно той стан, який
перевірявся разом.
ЩО ПРАЦЮЄ НА СТЕНДІ Й ПЕРЕВІРЕНО ТАМ
0058 подієві алерти: syslog, ncm, compliance спрацьовують у мить
події; правило з нереалізованим джерелом більше не зберігається
мовчки
0059 snmp.walk і прототипи шаблонів — таблиці з динамічним індексом
описуються шаблоном, а не Go
0060 відкат конфігу: план як різниця, маскування паролів із підписом
плану, обов'язковий контрольний збір, verifying при обриві
0061 кнопки Telegram: довге опитування, авторизація не з callback_data
0062 аудит і архів хостів; тест на AST, що падає на ключі без назви
0063 RLS: три ролі, окремий пул для фонових тактів
0064 строки зберігання даних і сторінка сховища
0065 приймач SNMP-трапів; перевірено справжніми пакетами по дроту,
переклад v1→v2 за RFC 3584 дає правильний OID
0066 ескалації сповіщень
0067 алерт про вичерпання диска
0068 поля заливки конфігу переїхали в каталог профілів
Плюс: 137 тестів вебу з нуля (їх не було взагалі), одинадцять справжніх
вад, знайдених ними й виправлених, і виправлення двох інтеграційних
тестів grpcapi, які мовчки пропускались півтора року.
ЩО ЩЕ НЕ ЗАПУСКАЛОСЬ
netpulse установник: одна команда замість 18 змінних і
593 рядків інструкції
RLS з першого запуску нова інсталяція під політиками одразу;
RLS-EXISTING-INSTALL.md лишається тільки для
старих інсталяцій
.forgejo + CI раннер не зареєстрований
Ці три перевірені компіляцією й міркуванням, але не виконанням.
ГОЛОВНИЙ ВИСНОВОК ДВОХ СЕСІЙ
Зелена перевірка доводить рівно те, що вона перевіряє. Тест ізоляції RLS
був правильний і зелений — і пропустив зламаний вхід, бо перевіряв «чи
не видно чужого», коли зламалось «чи видно своє». Інтеграційні тести
grpcapi були зелені, бо не виконувались. Схема, довідник і протокол
описували те, чого в коді не існувало, і виглядало це як готове.
Тому в кожному завданні цих сесій стояла вимога назвати НЕПОКРИТЕ, а
чотири задачі закінчились не можливістю, а відмовою: правило з
нереалізованим джерелом не зберігається, профіль без команд заливки
каже про це замість мовчазної кнопки, міграція RLS валить сама себе на
таблиці без політики, тест словника аудиту падає на ключі без назви.
Подробиці — HISTORY.md, розділи за 26 і 27 серпня.
119 lines
8.4 KiB
SQL
119 lines
8.4 KiB
SQL
-- =====================================================================
|
||
-- NetPulse :: 0057_device_purge.sql
|
||
-- Повне видалення хоста: черга видалень посилань у Git і індекс, без
|
||
-- якого прибирання історії алертів читало б усю гіпертаблицю.
|
||
--
|
||
-- Що взагалі змінюється в поведінці. Досі «видалити хост» означало
|
||
-- deleted_at = now(): хост зникав з інтерфейсу, а все зібране лишалось
|
||
-- у базі назавжди й недосяжним — переліку видалених хостів немає, і
|
||
-- відновлення теж немає. На цьому стенді від такого видалення вже
|
||
-- лишилось 7 рядів метрик і 3 перевірки, які не належать жодному
|
||
-- видимому хосту. Тепер поруч із архівним видаленням є повне, і саме
|
||
-- воно вимагає цієї міграції.
|
||
--
|
||
-- Самі каскади вже є: на inv.devices стоїть 21 зовнішній ключ, і всі,
|
||
-- крім topo.neighbors.resolved_device_id, — ON DELETE CASCADE. Тобто
|
||
-- один DELETE прибирає перевірки, алерти, конфіги, порти, вузли мап,
|
||
-- ряди метрик. Ця міграція про те, чого каскад НЕ дістає.
|
||
-- =====================================================================
|
||
|
||
-- ---------------------------------------------------------------------
|
||
-- Черга видалень гілок у Git
|
||
-- ---------------------------------------------------------------------
|
||
|
||
-- Навіщо черга, коли гілку можна прибрати прямо під час видалення.
|
||
--
|
||
-- Локальну — можна, і вона прибирається одразу. Але архів конфігів
|
||
-- дзеркалиться на зовнішній Git (0054), а туди push іде окремим
|
||
-- фоновим тактом саме тому, що чужий сервер не має права впливати на
|
||
-- нашу роботу. Видалення хоста підпадає під те саме правило: Forgejo
|
||
-- лежить — хост усе одно видаляється, а прибирання гілки на дзеркалі
|
||
-- чекає своєї черги.
|
||
--
|
||
-- Загального «prune» тут навмисно немає. Дзеркало заводять на випадок
|
||
-- втрати локального диска; prune, який зносить на тому кінці все, чого
|
||
-- немає тут, у день пошкодження локального репозиторію знищив би саме
|
||
-- ту копію, заради якої дзеркало й існує. Тому видалення адресне:
|
||
-- система знає, яку саме гілку прибрала, і шле видалення рівно цієї.
|
||
CREATE TABLE ncm.ref_deletions (
|
||
id uuid PRIMARY KEY DEFAULT core.new_id(),
|
||
tenant_id uuid NOT NULL REFERENCES core.tenants(id) ON DELETE CASCADE,
|
||
-- Репозиторій, у якому лежить гілка. NULL можливий: рядок ncm.repos
|
||
-- заводиться при першому бекапі або при налаштуванні дзеркала, і
|
||
-- хост могли видалити раніше за обидві події.
|
||
repo_id uuid REFERENCES ncm.repos(id) ON DELETE CASCADE,
|
||
|
||
-- Ім'я гілки БЕЗ refs/heads/: device/Леніна.21-10.0.0.1. Саме в
|
||
-- такому вигляді його складає store.DeviceBranch і приймає
|
||
-- gitstore — зберігати тут повне посилання означало б розібрати його
|
||
-- назад у двох місцях.
|
||
branch text NOT NULL CHECK (branch <> ''),
|
||
|
||
-- Хост, якого вже немає. Зовнішнього ключа немає НАВМИСНО: рядок
|
||
-- заводиться в тій самій транзакції, що й видалення хоста, і ключ
|
||
-- знищив би його разом із хостом каскадом — тобто рівно те, що ця
|
||
-- таблиця має пережити. Ім'я поруч із id з тієї ж причини, що й в
|
||
-- аудиті: через тиждень id нічого не скаже.
|
||
device_id uuid,
|
||
device_name text NOT NULL DEFAULT '',
|
||
|
||
created_at timestamptz NOT NULL DEFAULT now(),
|
||
attempts int NOT NULL DEFAULT 0,
|
||
last_error text,
|
||
-- Коли пробувати знову. NULL — «на найближчому такті».
|
||
next_attempt_at timestamptz,
|
||
-- Локальну гілку вже прибрано. Окремо від віддаленої, бо локальна
|
||
-- зникає одразу при видаленні хоста, а віддалена може чекати добу
|
||
-- недоступного Forgejo — і повторювати за цей час локальне видалення
|
||
-- сто разів немає сенсу.
|
||
local_done boolean NOT NULL DEFAULT false
|
||
);
|
||
|
||
-- Одна гілка — один рядок черги.
|
||
--
|
||
-- Хост можуть видалити, завести знову з тим самим іменем і адресою й
|
||
-- видалити ще раз; другий рядок означав би дві спроби видалити те, чого
|
||
-- вже немає, і другу помилку в журналі про це.
|
||
CREATE UNIQUE INDEX ref_deletions_uniq ON ncm.ref_deletions (tenant_id, branch);
|
||
|
||
-- Вибірка такту: що вже час пробувати.
|
||
CREATE INDEX ref_deletions_due_idx
|
||
ON ncm.ref_deletions (next_attempt_at NULLS FIRST);
|
||
|
||
ALTER TABLE ncm.ref_deletions ENABLE ROW LEVEL SECURITY;
|
||
ALTER TABLE ncm.ref_deletions FORCE ROW LEVEL SECURITY;
|
||
CREATE POLICY tenant_isolation ON ncm.ref_deletions
|
||
USING (tenant_id = core.current_tenant())
|
||
WITH CHECK (tenant_id = core.current_tenant());
|
||
|
||
GRANT SELECT, INSERT, UPDATE, DELETE ON ncm.ref_deletions
|
||
TO netpulse_app, netpulse_worker;
|
||
|
||
COMMENT ON TABLE ncm.ref_deletions IS
|
||
'Гілки видалених хостів, які треба прибрати локально й на дзеркалі; переживає недоступність дзеркала';
|
||
COMMENT ON COLUMN ncm.ref_deletions.branch IS
|
||
'Ім''я гілки без refs/heads/ — у тому вигляді, у якому його складає store.DeviceBranch';
|
||
|
||
-- ---------------------------------------------------------------------
|
||
-- Індекс під прибирання історії алертів
|
||
-- ---------------------------------------------------------------------
|
||
|
||
-- alr.alerts_history — гіпертаблиця, і зовнішнього ключа на
|
||
-- inv.devices у неї немає й бути не може: TimescaleDB не дозволяє
|
||
-- посилатись на гіпертаблицю й не тягне на неї каскади. Тобто історію
|
||
-- алертів видаленого хоста прибирає не база, а код — DELETE ... WHERE
|
||
-- device_id = $1. Наявні індекси йдуть по (tenant_id, ts) і по (ts),
|
||
-- тож такий DELETE читав би ВСЮ історію тенанта заради одного хоста.
|
||
--
|
||
-- Той самий індекс уже є в ts.icmp_samples, ts.if_counters, ts.syslog,
|
||
-- ts.snmp_traps і ts.device_status_history (icmp_device_ts_idx і далі);
|
||
-- тут його просто забули, і побачити це можна було лише тоді, коли
|
||
-- знадобилось видаляти по хосту.
|
||
CREATE INDEX IF NOT EXISTS alerts_hist_device_ts_idx
|
||
ON alr.alerts_history (device_id, ts DESC);
|
||
|
||
-- Решті гіпертаблиць, які прибирає код, індекс уже є й додавати нічого
|
||
-- не треба: ts.icmp_samples, ts.if_counters, ts.syslog, ts.snmp_traps і
|
||
-- ts.device_status_history мають (device_id, ts), ts.link_status —
|
||
-- (link_id, ts), alr.notifications — (alert_id, ts). Перевірено на
|
||
-- живій схемі, а не з опису таблиць.
|