fix(orchestrator): сделать staged firewall guard транзакционным
Снятие автоматического отката firewall было утверждением, а не фактом:
systemctl stop <unit>.timer <unit>.service || true
-> "firewall rollback timer disarmed"
-> phase=installed
Отказ остановки стирался через `|| true`, и взведённый таймер мог вернуть
прежний firewall уже ПОСЛЕ долговечной записи успеха. Просто убрать `|| true`
нельзя: для транзиентного юнита, уже убранного systemd, `systemctl stop`
возвращает 5 — законный исход, неотличимый от успеха.
Соседний дефект того же корня: guard мог сработать ВО ВРЕМЯ успешного smoke.
Окно 45 секунд короче худшего случая smoke, а единственной проверкой firewall
был `nft -c` — разбор текущего файла, каким бы он ни был. Откатившийся прежний
ruleset проходил её зелёным, и сервер объявлялся настроенным с firewall,
который операция же и заменила.
Оба закрываются маркером /run/hy2xs/rollback/<op>/auto-rollback-fired, который
rollback-скрипт создаёт первым действием. Инвариант стал детерминированным:
маркер отсутствует И timer/service inactive => можно фиксировать успех
Остальное в том же проходе:
- auto-rollback переехал из однострочного `sh -c` в сгенерированный скрипт.
Прежний держался на склейке соседних кавычек и на том, что op-id не содержит
пробелов; теперь ключ проверяется, а скрипт покрыт тестом и shell-парсером;
- скрипт накапливает rc и уходит в failed вместо молчаливого 0 при частичном
восстановлении. Состояние nftables.service он сознательно не трогает:
ExecStop у него делает `nft flush ruleset`;
- smoke сверяет ЭФФЕКТИВНЫЙ firewall: фрагмент на диске против отрендеренного,
принадлежность entrypoint и загруженность таблицы inet hy2xs;
- откат восстанавливает enabled/active nftables.service — стадиями, идущими до
применения ruleset;
- остановка guard'а в rollbackFirewallNow стала стадией с отчётом, а не вызовом
с `|| true` внутри;
- `*.candidate` больше не остаются на диске навсегда;
- стадии восстановления reconfigure независимы по ОТДЕЛЬНОМУ ФАЙЛУ, а не по
группе;
- ключ операции считается одной функцией: install писал в маркер сырой
ISO-timestamp, и путь /run/hy2xs/rollback/<op_id> из runbook не существовал.
This commit is contained in:
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { preflight } from "../src/steps/preflight";
|
||||
import { classifyReconfigureFailure } from "../src/commands/reconfigure";
|
||||
import { FirewallGuardFiredError } from "../src/steps/firewall";
|
||||
import { baselineConfig, testContext } from "./fixtures";
|
||||
|
||||
/**
|
||||
@@ -134,7 +135,26 @@ describe("классификация отказа reconfigure/repair", () => {
|
||||
// классификация по тексту ошибки убрана, должен быть разрешён.
|
||||
test("текст ошибки на классификацию не влияет", () => {
|
||||
expect(reconfigureSource).not.toContain(".test(message)");
|
||||
expect(reconfigureSource).toContain("classifyReconfigureFailure(ownership)");
|
||||
expect(reconfigureSource).toContain("classifyReconfigureFailure(ownership, error)");
|
||||
});
|
||||
|
||||
/**
|
||||
* Сработавший guard классифицируется по ТИПУ ошибки.
|
||||
*
|
||||
* Разница с прежним разбором сообщения принципиальна, и проверяется она
|
||||
* именно так: ошибка, в тексте которой есть все нужные слова, но у которой
|
||||
* другой тип, обязана классифицироваться по владению, как и раньше.
|
||||
*/
|
||||
test("сработавший guard опознаётся по типу ошибки, а не по её тексту", () => {
|
||||
const late = ownership({ firewallTouched: true, servicesRestarted: true });
|
||||
|
||||
expect(classifyReconfigureFailure(late, new FirewallGuardFiredError("guard"))).toBe(
|
||||
"firewall_guard_fired"
|
||||
);
|
||||
expect(
|
||||
classifyReconfigureFailure(late, new Error("automatic firewall rollback has already fired"))
|
||||
).toBe("smoke_failed");
|
||||
expect(classifyReconfigureFailure(late)).toBe("smoke_failed");
|
||||
});
|
||||
|
||||
test("отказ диагностики не отменяет откат firewall и конфигов", () => {
|
||||
|
||||
Reference in New Issue
Block a user