refactor: one ownership model for every file written under /etc [P1]

Two modules wrote into /etc/sysctl.d and did it differently. sysctl_tuning refused to
touch a file whose first line lacked its marker and cleaned up on disable. swap wrote
/etc/sysctl.d/99-awg2-harness.conf with a different marker, no ownership check at all -
an administrator's file at that path was silently clobbered - and never removed it when
the module was turned off, so vm.swappiness stayed pinned forever.

New module managed_file.rs holds the rules once:

- a read that fails is never an absent file;
- ownership is Absent / Managed / Foreign, decided by the marker on the first line;
- write refuses Foreign, skips when the content already matches, and is otherwise atomic:
  temp sibling created with create_new, sync_all, mode 0644, rename, directory flush;
- remove refuses Foreign and treats Absent as success.

Both modules now go through it. sysctl_tuning loses its private write_atomic,
ensure_managed_or_absent and remove_managed_sysctl_file_at.

The swappiness file moves to /etc/sysctl.d/90-awg2-harness-swap.conf with the marker
"# Managed by awg2-harness swap", matching the 90- prefix every other managed file uses.
The old path is removed on first run of the new version, but only when it still carries
the previous "# Managed by awg2-harness" marker; a foreign file there is left alone and
does not fail the migration, since the current file lives elsewhere.

Disable semantics are now explicit: swap.enabled = false removes the managed file and
does nothing else. Runtime vm.swappiness is deliberately not reset - picking a value
would be inventing a policy - and active swap is not deactivated, because merely turning
a feature off should not trigger a destructive action. Previously ensure_swap returned
early and left the persisted file behind.

Also replaced a vacuous test: swappiness_conf_render_is_stable asserted that a string
literal equals a format! of the same literal, touching no production code at all. It now
exercises render_swappiness_conf, and a companion test proves the rendered file is
recognised as managed by the same marker check that guards overwriting it - otherwise
every reconcile would refuse to touch its own output.

Twelve tests: the three ownership states, refusal to overwrite and refusal to remove a
foreign file with content asserted intact, idempotent write, stale temp handling, an
unreadable path reported as an error, plus legacy migration in all three cases - ours,
foreign, absent.

cargo test --workspace: 148 + 4 passed. clippy -D warnings clean.
This commit is contained in:
2026-08-06 22:50:19 +05:00
parent a895295701
commit e9f0e29f75
7 changed files with 435 additions and 101 deletions
+5
View File
@@ -129,6 +129,11 @@
- [ ] Файл без marker по managed-пути не перезаписывается и не удаляется: команда завершается ошибкой.
- [ ] В managed-файле отсутствует `udp_wmem_min`.
- [ ] `net.ipv4.tcp_congestion_control` и `net.core.default_qdisc` присутствуют в managed-файле только при активном BBR.
- [ ] `swap` пишет `vm.swappiness` в `/etc/sysctl.d/90-awg2-harness-swap.conf`, а не в `99-awg2-harness.conf`.
- [ ] При обновлении с прежней версии `/etc/sysctl.d/99-awg2-harness.conf` удаляется, если несёт маркер `# Managed by awg2-harness`.
- [ ] Файл по пути `99-awg2-harness.conf` без этого маркера остаётся нетронутым и не ломает `ensure-swap`.
- [ ] При `swap.enabled = false` файл `90-awg2-harness-swap.conf` удаляется, активный swap **не** выключается, runtime `vm.swappiness` не переопределяется.
- [ ] Любой managed-файл под `/etc`, у которого первая строка не содержит маркер владельца, не перезаписывается и не удаляется.
- [ ] Runtime apply меняет только drifted keys (`sysctl -w`), без `sysctl --system`.
- [ ] При `tcp_bbr = auto` и недоступном BBR сервис не падает, в статусе есть warning.
- [ ] При `tcp_bbr = require` и недоступном BBR команда/старт завершается с ошибкой.
+1
View File
@@ -119,6 +119,7 @@ swappiness = 1
`sysctl_tuning` — отдельный host-level модуль управляемого сетевого baseline через `sysctl`:
- persistent-файл: `/etc/sysctl.d/90-awg2-harness-vpn.conf`;
- модуль `swap` отдельно управляет `/etc/sysctl.d/90-awg2-harness-swap.conf` (только `vm.swappiness`); прежний путь `/etc/sysctl.d/99-awg2-harness.conf` удаляется при первом запуске новой версии, если несёт старый маркер;
- дополнительный managed-файл `/etc/modules-load.d/90-awg2-harness-bbr.conf` создаётся только при фактически активном BBR и удаляется, когда BBR неактивен;
- оба managed-файла несут в первой строке маркер `# Managed by awg2-harness sysctl_tuning`; файл без маркера не перезаписывается и не удаляется, вместо этого возвращается ошибка;
- `net.ipv4.tcp_congestion_control` и `net.core.default_qdisc` управляются только при активном BBR;
+18 -1
View File
@@ -76,7 +76,24 @@ awg2-harness validate-runtime
- не использует `/etc/sysctl.conf` и `sysctl --system`;
- не управляет `udp_wmem_min`.
Оба управляемых файла несут в первой строке маркер `# Managed by awg2-harness sysctl_tuning`. Файл без маркера не перезаписывается и не удаляется — вместо этого возвращается ошибка. Это защищает чужие настройки, случайно оказавшиеся по тому же пути.
### Управляемые файлы и владение
Все файлы, которые пакет пишет под `/etc`, проходят через общий модуль `managed_file.rs`. Правила одни для всех и не дублируются по модулям:
- ошибка чтения никогда не трактуется как отсутствие файла;
- файл, у которого первая строка не содержит маркер владельца, не перезаписывается и не удаляется — возвращается ошибка;
- запись атомарная: временный файл-сосед, `sync_all`, `rename`, синхронизация каталога, права `0644`;
- повторная запись того же содержимого не трогает файл.
| Файл | Владелец | Маркер |
| --- | --- | --- |
| `/etc/sysctl.d/90-awg2-harness-vpn.conf` | `sysctl_tuning` | `# Managed by awg2-harness sysctl_tuning` |
| `/etc/modules-load.d/90-awg2-harness-bbr.conf` | `sysctl_tuning` | `# Managed by awg2-harness sysctl_tuning` |
| `/etc/sysctl.d/90-awg2-harness-swap.conf` | `swap` | `# Managed by awg2-harness swap` |
Модуль `swap` раньше писал `vm.swappiness` в `/etc/sysctl.d/99-awg2-harness.conf` с другим маркером, без проверки владения перед перезаписью и без удаления при отключении. Файл переехал на `90-`, как и остальные. Старый путь удаляется автоматически, но **только** если он всё ещё несёт прежний маркер `# Managed by awg2-harness`; файл администратора по этому пути не трогается, и это не считается ошибкой миграции.
При `swap.enabled = false` управляемый файл удаляется. Runtime-значение `vm.swappiness` при этом намеренно не меняется: назначать произвольное значение означало бы выдумать политику, о которой оператор не просил. После перезагрузки действует kernel default. Активный swap при отключении модуля тоже не выключается — деструктивное действие не должно запускаться самим фактом отключения функции.
`net.ipv4.tcp_congestion_control` и `net.core.default_qdisc` попадают в целевое состояние только при активном BBR.