Один коміт, а не десяток тематичних, свідомо: теми переплетені в
спільних файлах (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 серпня.
163 lines
10 KiB
SQL
163 lines
10 KiB
SQL
-- =====================================================================
|
||
-- NetPulse :: 0036_command_runs.sql
|
||
-- Масове виконання команд по фільтру хостів.
|
||
--
|
||
-- Навіщо окремі таблиці, а не ще один trigger у ncm.jobs.
|
||
--
|
||
-- Збір конфігу і виконання команд справді ходять одним шляхом (сервер →
|
||
-- черга → жива сесія зонда → CLI), і механіку ми переюзовуємо цілком.
|
||
-- Але сам рядок відповідає на різні питання. У ncm.jobs один рядок — це
|
||
-- «зняти поточний конфіг цього хоста»: він самодостатній, ним ніхто не
|
||
-- керує ззовні, його результат — версія конфігу. Тут рядок — це «частина
|
||
-- прогону»: у нього є спільні з іншими рядками команди, спільне
|
||
-- обмеження паралельності, спільна зупинка на льоту й спільний
|
||
-- підтверджений перелік хостів.
|
||
--
|
||
-- Спроба вкласти це в ncm.jobs дала б колонки, порожні для дев'яноста
|
||
-- дев'яти відсотків рядків, і — головне — зламала б наявний вибірник
|
||
-- ClaimConfigJobs: він забирає ВСЕ, що стоїть у 'queued', і команда
|
||
-- поїхала б на пристрій як завдання зняти конфіг.
|
||
--
|
||
-- Тому рядок прогону тут одночасно і завдання, і результат: окрема
|
||
-- таблиця «результатів» додала б JOIN там, де зв'язок один-до-одного.
|
||
-- =====================================================================
|
||
|
||
-- ---------------------------------------------------------------------
|
||
-- Право
|
||
-- ---------------------------------------------------------------------
|
||
|
||
-- Окреме право, і навмисно не ncm:write.
|
||
--
|
||
-- Це найнебезпечніша дія в системі: одна команда на двісті пристроїв,
|
||
-- без попереднього перегляду того, що вона зробить. «Може міняти
|
||
-- розклад бекапів» і «може виконати довільну команду на всій мережі» —
|
||
-- різні за наслідками речі, і зводити їх до одного прапорця означало б
|
||
-- роздати друге разом із першим.
|
||
INSERT INTO core.permissions (key, description) VALUES
|
||
('ncm:exec', 'Масове виконання команд на обладнанні')
|
||
ON CONFLICT (key) DO NOTHING;
|
||
|
||
-- Видається лише власнику й адміну. Інженер (a3) свідомо НЕ отримує:
|
||
-- він і так має ncm:write, і роздати разом із ним право виконувати
|
||
-- команди означало б увімкнути найнебезпечнішу функцію системи всім,
|
||
-- хто вже працює, — без жодного рішення людини. Кому треба, той видасть
|
||
-- явно.
|
||
INSERT INTO core.role_permissions (role_id, permission_key) VALUES
|
||
('00000000-0000-0000-0000-0000000000a1', 'ncm:exec'),
|
||
('00000000-0000-0000-0000-0000000000a2', 'ncm:exec')
|
||
ON CONFLICT DO NOTHING;
|
||
|
||
-- ---------------------------------------------------------------------
|
||
-- Прогін
|
||
-- ---------------------------------------------------------------------
|
||
|
||
CREATE TYPE ncm.command_run_status AS ENUM
|
||
('running','done','canceled');
|
||
|
||
CREATE TABLE ncm.command_runs (
|
||
id uuid PRIMARY KEY DEFAULT core.new_id(),
|
||
tenant_id uuid NOT NULL REFERENCES core.tenants(id) ON DELETE CASCADE,
|
||
created_by uuid REFERENCES core.users(id) ON DELETE SET NULL,
|
||
|
||
-- Команди в тому вигляді, у якому їх набрала людина. Підготовчі
|
||
-- команди профілю (screen-length, terminal length) сюди не входять:
|
||
-- вони належать моделі заліза, а не наміру оператора, і в кожного
|
||
-- хоста прогону можуть бути свої.
|
||
commands jsonb NOT NULL,
|
||
|
||
-- Знімок фільтра, яким обрано хости. Зберігається як текст наміру
|
||
-- («усі Huawei на майданчику Миронівка»), а не як єдине джерело
|
||
-- переліку: перелік уже зафіксовано в targets. Через тиждень, коли
|
||
-- питатимуть «а чому ця команда пішла на цей хост», відповідь має
|
||
-- бути в рядку, а не в реконструкції стану інвентарю на той момент.
|
||
filter jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||
|
||
status ncm.command_run_status NOT NULL DEFAULT 'running',
|
||
-- Скільки хостів беремо одночасно. Стеля не косметична: та сама
|
||
-- команда на всій дільниці — це стільки ж одночасних SSH-сесій із
|
||
-- зонда, а зонд стоїть на тому самому каналі, що й моніторинг.
|
||
concurrency int NOT NULL DEFAULT 5 CHECK (concurrency BETWEEN 1 AND 50),
|
||
timeout_sec int NOT NULL DEFAULT 120 CHECK (timeout_sec BETWEEN 10 AND 600),
|
||
|
||
total int NOT NULL DEFAULT 0,
|
||
canceled_by uuid REFERENCES core.users(id) ON DELETE SET NULL,
|
||
canceled_at timestamptz,
|
||
created_at timestamptz NOT NULL DEFAULT now(),
|
||
finished_at timestamptz
|
||
);
|
||
CREATE INDEX command_runs_tenant_idx ON ncm.command_runs (tenant_id, created_at DESC);
|
||
CREATE INDEX command_runs_active_idx ON ncm.command_runs (status) WHERE status = 'running';
|
||
|
||
-- ---------------------------------------------------------------------
|
||
-- Хост у прогоні: він же завдання, він же результат
|
||
-- ---------------------------------------------------------------------
|
||
|
||
-- 'pending' — чекає своєї черги (діє обмеження паралельності);
|
||
-- 'queued' — віддано диспетчеру, чекає живої сесії зонда;
|
||
-- 'running' — надіслано зонду;
|
||
-- 'success' — усі команди виконались;
|
||
-- 'failed' — не під'єднались, або команда впала;
|
||
-- 'canceled' — прогін зупинили до того, як дійшло до цього хоста.
|
||
CREATE TYPE ncm.command_target_status AS ENUM
|
||
('pending','queued','running','success','failed','canceled');
|
||
|
||
CREATE TABLE ncm.command_targets (
|
||
id uuid PRIMARY KEY DEFAULT core.new_id(),
|
||
run_id uuid NOT NULL REFERENCES ncm.command_runs(id) ON DELETE CASCADE,
|
||
tenant_id uuid NOT NULL REFERENCES core.tenants(id) ON DELETE CASCADE,
|
||
device_id uuid NOT NULL REFERENCES inv.devices(id) ON DELETE CASCADE,
|
||
-- Зонд фіксується в момент створення прогону, а не в момент відправки:
|
||
-- перелік хостів людина вже підтвердила, і мовчазна зміна виконавця
|
||
-- між підтвердженням і виконанням була б рівно тим, чого підтвердження
|
||
-- має не допускати.
|
||
agent_id uuid REFERENCES core.agents(id) ON DELETE SET NULL,
|
||
|
||
status ncm.command_target_status NOT NULL DEFAULT 'pending',
|
||
-- Вивід кожної команди окремо: [{"command":…,"output":…,"error":…}].
|
||
-- Окремо, а не суцільним текстом, бо перше питання до результату —
|
||
-- «що відповіла ОЦЯ команда», і різати спільний потік за відлунням
|
||
-- команди означало б удруге робити роботу, яку зонд уже зробив.
|
||
outcomes jsonb NOT NULL DEFAULT '[]'::jsonb,
|
||
error text,
|
||
-- Стенограма сесії — лише байти від пристрою; пароль у неї не
|
||
-- потрапляє (див. agent/internal/ncmx/transport.go).
|
||
transcript text,
|
||
|
||
started_at timestamptz,
|
||
finished_at timestamptz,
|
||
duration_ms int,
|
||
created_at timestamptz NOT NULL DEFAULT now(),
|
||
|
||
-- Один хост у прогоні рівно раз: перелік підтверджений людиною, і
|
||
-- дубль означав би дві сесії до одного пристрою замість однієї.
|
||
UNIQUE (run_id, device_id)
|
||
);
|
||
CREATE INDEX command_targets_run_idx ON ncm.command_targets (run_id, created_at);
|
||
-- Вибірка диспетчера: усе, що чекає відправки, за зондом.
|
||
CREATE INDEX command_targets_pending_idx ON ncm.command_targets (agent_id, status)
|
||
WHERE status IN ('pending','queued','running');
|
||
|
||
-- ---------------------------------------------------------------------
|
||
-- RLS: як на решті таблиць із tenant_id (0011 накотився раніше й нових
|
||
-- таблиць не бачив).
|
||
-- ---------------------------------------------------------------------
|
||
|
||
ALTER TABLE ncm.command_runs ENABLE ROW LEVEL SECURITY;
|
||
ALTER TABLE ncm.command_runs FORCE ROW LEVEL SECURITY;
|
||
CREATE POLICY tenant_isolation ON ncm.command_runs
|
||
USING (tenant_id = core.current_tenant())
|
||
WITH CHECK (tenant_id = core.current_tenant());
|
||
|
||
ALTER TABLE ncm.command_targets ENABLE ROW LEVEL SECURITY;
|
||
ALTER TABLE ncm.command_targets FORCE ROW LEVEL SECURITY;
|
||
CREATE POLICY tenant_isolation ON ncm.command_targets
|
||
USING (tenant_id = core.current_tenant())
|
||
WITH CHECK (tenant_id = core.current_tenant());
|
||
|
||
GRANT SELECT, INSERT, UPDATE, DELETE ON ncm.command_runs, ncm.command_targets
|
||
TO netpulse_app, netpulse_worker;
|
||
|
||
COMMENT ON TABLE ncm.command_runs IS
|
||
'Масове виконання команд: намір, підтверджений перелік хостів, обмеження';
|
||
COMMENT ON TABLE ncm.command_targets IS
|
||
'Хост у прогоні команд: одночасно завдання для зонда й результат по ньому';
|