firewall guard: барьер покоя fail-closed и явный контракт транзиентного таймера
Барьер, обязанный ДОКАЗАТЬ отсутствие асинхронного исполнителя, в трёх местах принимал за доказательство отсутствие наблюдения. - отказ `systemctl` больше не выдаётся за отсутствие guard: вместо `return []` введён единый наблюдатель inspectRollbackGuard с исходами quiescent/pending/ unknown и отдельным типом отказа GuardStateUnknownError; - покой перечисляется белым списком (inactive, failed): maintenance, refreshing и любое незнакомое состояние systemd блокируют операцию; - у транзиентного таймера явно заданы AccuracySec=1s (умолчание 1min превращало обещанные 45 секунд в 45-105) и RemainAfterElapse=no; барьер дополнительно опознаёт SubState=elapsed у *.timer как покой; - команда взведения строится чистой buildArmGuardArgv и выполняется новым runMutatingArgv без shell, поэтому её контракт проверяется значением, а не грепом по исходнику; - status перестал листить guard-юниты своей копией кода: без --plain, с `|| true` и с трактовкой failed как «вооружён» отчёт вечно противоречил барьеру. Добавлены rollback_guard_state и firewall_state=guard_unknown; - purge-v0.sh пропускал failed-юниты из-за маркера в первой колонке. Барьер покрыт поведенческими тестами через подставляемый SystemdUnitProbe: прежние проверки грепом по тексту функции пережили инверсию смысла - строка `return [];` была на месте, а решение стало неверным. Документация (README, docs/07, 11, 12, 13, 14, CHANGELOG) приведена к реальному окну 45-46 секунд и к новому тексту отказа. Отдельно исправлен комментарий PNPM_AUDIT_LEVEL в versions.env: гейт давно проверяет весь lock-граф.
This commit is contained in:
@@ -288,7 +288,33 @@ HYSTERIA_BIN=/usr/local/bin/hysteria ./tools/test/e2e-hysteria.sh
|
||||
стадиями, идущими **до** применения ruleset;
|
||||
- candidate-файлы убираются после успеха и best-effort при откате;
|
||||
- ключ операции считается одной функцией: install писал в маркер сырой
|
||||
ISO-timestamp, и путь `/run/hy2xs/rollback/<op_id>` из runbook не существовал.
|
||||
ISO-timestamp, и путь `/run/hy2xs/rollback/<op_id>` из runbook не существовал;
|
||||
- команда взведения guard проверяется как **значение**, а не грепом по
|
||||
исходнику: `buildArmGuardArgv` возвращает готовый argv, и тест сверяет его
|
||||
целиком — имя юнита с явным суффиксом, `--on-active=45s`,
|
||||
`--timer-property=AccuracySec=1s`, `--timer-property=RemainAfterElapse=no`.
|
||||
Небезопасный ключ операции отвергается до запуска: shell в этой команде не
|
||||
участвует, поэтому единственная защита — отказ;
|
||||
- барьер покоя проверяется **поведенчески**, с подставляемым наблюдателем
|
||||
systemd (`SystemdUnitProbe`), без systemd и без Linux:
|
||||
- взведённый таймер (`active/waiting`) и идущий прямо сейчас откат
|
||||
(`activating`) запрещают операцию с `PendingRecoveryError`;
|
||||
- отказ `list-units` **и** отказ `show` на любом отдельном юните дают
|
||||
`GuardStateUnknownError`: отсутствие ответа systemd — отсутствие
|
||||
наблюдения, а не наблюдение покоя. Прежний тест закреплял обратное
|
||||
(`return [];` в тексте функции) и потому пережил инверсию смысла: строка
|
||||
была на месте, а решение стало неверным;
|
||||
- оба отказа имеют общего предка `OperationBarrierError`;
|
||||
- покой — ровно `inactive` и `failed`; `maintenance`, `refreshing` и любое
|
||||
незнакомое состояние блокируют операцию, потому что политика перечисляет
|
||||
безопасные состояния, а не опасные;
|
||||
- `*.timer` в `SubState=elapsed` считается покоем: `TIMER_ELAPSED` в systemd
|
||||
отображается в `UNIT_ACTIVE`, и без этой ветки отработавший таймер
|
||||
блокировал бы `repair` навсегда. У сервиса тот же `SubState` ничего не
|
||||
значит;
|
||||
- разбор вывода `list-units` находит имя юнита и когда первой колонкой идёт
|
||||
маркер `●` (состояние `failed`) — иначе терялся бы именно аварийно
|
||||
сработавший guard.
|
||||
|
||||
## A5f. Взаимное исключение операций (unit)
|
||||
|
||||
@@ -1040,7 +1066,19 @@ firewall и успешным smoke.
|
||||
Сценарий:
|
||||
|
||||
1. установка доходит до шага `firewall`, в журнале появляется
|
||||
`firewall rollback guard armed`;
|
||||
`firewall rollback guard armed: … fires in 45s (timer accuracy 1s)`.
|
||||
Пока guard ждёт, свойства таймера проверяются напрямую — обещанное окно
|
||||
обязано быть контрактом systemd, а не намерением:
|
||||
|
||||
```bash
|
||||
systemctl show hy2xs-fw-rollback-<op-id>.timer \
|
||||
-p ActiveState -p SubState -p AccuracyUSec -p RemainAfterElapse
|
||||
```
|
||||
|
||||
Ожидается `ActiveState=active`, `SubState=waiting`, `AccuracyUSec=1s`,
|
||||
`RemainAfterElapse=no`. Без явной точности systemd вправе сработать в окне
|
||||
`[45s; 45s + AccuracySec]`, а умолчание `AccuracySec=` — одна минута, то
|
||||
есть реальное окно было бы 45–105 секунд;
|
||||
2. smoke искусственно замедляется дольше 45 секунд. Проще всего задержать один
|
||||
из сервисов — например, добавить в `hy2xs-admin.service` временный
|
||||
`ExecStartPre=/bin/sleep 60` и выполнить `systemctl daemon-reload` до запуска
|
||||
@@ -1142,18 +1180,56 @@ guard A срабатывает и возвращает firewall, который
|
||||
|
||||
```text
|
||||
previous HY2XS operation is no longer running, but its firewall rollback guard
|
||||
is still armed: hy2xs-fw-rollback-<op-id>.timer (active)
|
||||
is still armed: hy2xs-fw-rollback-<op-id>.timer (active/waiting)
|
||||
```
|
||||
|
||||
5. отказ происходит **до** снятия резервной копии и до первой мутации;
|
||||
6. `install.sh` в том же окне отказывает на PHASE 0 по той же причине;
|
||||
7. после срабатывания guard (`hy2xs-fw-rollback-*` больше не `active`)
|
||||
`repair` проходит.
|
||||
7. после срабатывания guard транзиентный таймер выгружается
|
||||
(`RemainAfterElapse=no`), и `repair` проходит. Проверяется наблюдением, а не
|
||||
ожиданием на глаз:
|
||||
|
||||
```bash
|
||||
systemctl show hy2xs-fw-rollback-<op-id>.timer -p LoadState -p ActiveState
|
||||
systemctl list-units --all --plain 'hy2xs-fw-rollback-*'
|
||||
```
|
||||
|
||||
Ожидается, что таймера в списке больше нет; оставшийся `.service` в
|
||||
состоянии `failed` (частичное восстановление) операцию не блокирует.
|
||||
|
||||
Обратная проверка: на сервере без вооружённого guard барьер молчит и ни одну
|
||||
операцию не задерживает, а `failed` от уже отработавшего guard **не** считается
|
||||
непокоем — иначе он заблокировал бы `repair`, которым и чинят последствия.
|
||||
|
||||
Отдельная проверка того же барьера — недоказуемое состояние. Барьер обязан
|
||||
различать «guard вооружён» и «спросить не удалось»: это разные утверждения, и
|
||||
оператору по ним нужны разные действия.
|
||||
|
||||
1. на рабочей установке без вооружённого guard делается недоступным запрос к
|
||||
systemd — проще всего временно подложить в `PATH` оркестратора `systemctl`,
|
||||
завершающийся ненулевым кодом;
|
||||
2. любая операция жизненного цикла (`repair`, `reconfigure --apply`, `doctor`,
|
||||
`install.sh` на PHASE 0) обязана отказать:
|
||||
|
||||
```text
|
||||
unable to verify firewall rollback guard state; systemd query failed,
|
||||
refusing to start a lifecycle operation
|
||||
```
|
||||
|
||||
3. отказ происходит **до** первой мутации, и тип ошибки —
|
||||
`GuardStateUnknownError`, а не `PendingRecoveryError`: ждать окна отката
|
||||
здесь бессмысленно;
|
||||
4. `hy2xs-orchestrator status` при этом **не** падает: он замок не берёт и
|
||||
существует в том числе для сломанного хоста, поэтому сообщает
|
||||
`rollback_guard_state: "unknown"` и `firewall_state: "guard_unknown"`;
|
||||
5. после возврата рабочего `systemctl` операция проходит без дополнительных
|
||||
действий.
|
||||
|
||||
Смысл проверки — в том, что прежнее поведение было противоположным: отказ
|
||||
запроса давал пустой список guard'ов, барьер считал систему спокойной и
|
||||
пропускал операцию, а взведённый таймер предыдущей операции срабатывал уже
|
||||
посреди неё.
|
||||
|
||||
## D1g. Успешная установка не оставляет следов транзакции
|
||||
|
||||
Проверяется на чистом хосте, обычной успешной установкой. Это обратная проверка
|
||||
@@ -1163,7 +1239,7 @@ is still armed: hy2xs-fw-rollback-<op-id>.timer (active)
|
||||
После `installed`:
|
||||
|
||||
```text
|
||||
systemctl list-units --all 'hy2xs-fw-rollback-*' → пусто
|
||||
systemctl list-units --all --plain 'hy2xs-fw-rollback-*' → пусто
|
||||
ls /run/hy2xs/rollback/ → пусто
|
||||
ls /run/lock/hy2xs-orchestrator.lock → отсутствует
|
||||
ls /etc/nftables.conf.candidate → отсутствует
|
||||
@@ -1366,3 +1442,8 @@ hy2xs-orchestrator doctor
|
||||
68. новая операция не начинается, пока у предыдущей остаётся вооружённый rollback guard: условие старта — «у предыдущей нет исполнителей, способных изменить систему», а не «её PID мёртв»
|
||||
69. отказ записи маркера `auto-rollback-fired` не может привести к фиксации успеха: он переводит юнит guard в `failed`, а `failed` фиксацию запрещает
|
||||
70. восстановление `UnitFileState` у `nftables.service` не обещает точности, которой не даёт: восстанавливаются `enabled`/`disabled`, остальные состояния называются оператору и не трогаются
|
||||
71. отказ запроса к systemd не выдаётся за покой: барьер обязан **доказать** отсутствие исполнителей предыдущей операции, а при невозможности получить доказательство отказывает с `GuardStateUnknownError`, а не разрешает операцию
|
||||
72. покой guard перечисляется белым списком (`inactive`, `failed`): незнакомое состояние systemd блокирует операцию, а не проходит молча по принципу «его нет в списке опасных»
|
||||
73. отработавший таймер не блокирует операцию навсегда: `RemainAfterElapse=no` выгружает его, а барьер дополнительно опознаёт `SubState=elapsed` у `*.timer` как покой
|
||||
74. обещанное окно отката — контракт systemd, а не намерение: у транзиентного таймера явно задан `AccuracySec=1s`, иначе умолчание `AccuracySec=1min` превращало «45 секунд» в 45–105
|
||||
75. состояние guard читает один наблюдатель: `status` берёт его у того же кода, что и барьер, и сообщает `unknown` вместо тихого «guard'ов нет» при отказе systemd
|
||||
|
||||
Reference in New Issue
Block a user