fix(swap): make the /etc/fstab rewrite transactional [P0]
Two release blockers, both in the most destructive operation the package performs. 1. An unreadable /etc/fstab was treated as an empty one. let content = fs::read_to_string(fstab_path).unwrap_or_default(); PermissionDenied, an I/O error, a damaged filesystem and "file absent" all collapsed into an empty string. The rewrite then emitted a fstab containing only the managed swap line and installed it atomically and successfully, dropping the root and boot mounts. Consequence: a host that does not boot, guarded by one unwrap_or_default. The same masking was present in read_fstab_swap_entries, so an unreadable fstab was reported by status as "no swap entries", which reads as healthy drift-free state. 2. The backup was best-effort. let _ = fs::write(bak_path, &content); If it failed, the rewrite proceeded anyway. IMPLEMENTATION_SPEC and the acceptance checklist both asserted the backup exists. The scenario where the backup fails is exactly the scenario where it is needed. New module fstab.rs owns the operation: - read_required distinguishes NotFound from every other error and fails on both. A missing /etc/fstab on Debian is an anomaly, not a blank slate, so it is never created; - a blank candidate is refused outright; - the backup is mandatory and flushed before the temp file is created, so there is no window where the target is about to be replaced and no recovery copy exists; - two copies are kept: .awg2-harness.bak is the state before the current rewrite, .awg2-harness.original is written once with create_new and holds the host as it was before this package first touched it; - the temp file is a sibling of the target, created with create_new, sync_all'd, and inherits the original permissions; a stale temp from a killed run is removed explicitly rather than reused; - the candidate is validated with findmnt --verify --tab-file before the swap. The step is skipped when findmnt cannot be spawned; a rejection is fatal; - commit is an atomic rename followed by a directory fsync; - after the rename the target is re-read and compared to what was written. A mismatch restores the backup and fails. Also fixed: render_fstab_line interpolated the path raw. fstab fields are whitespace-separated, so a space or tab in swap.path would shift every following column. Paths now go through octal escaping (\040, \011, \012, \134). Ten tests, all portable - the "unreadable but present" case is exercised by pointing the reader at a directory, which fails as a non-NotFound error on every platform. Covered: unreadable target is an error rather than an empty file, missing target is an error, backup failure leaves the target byte-identical, blank candidate refused, unchanged candidate is a no-op that writes no backup, the pristine copy survives repeated rewrites, a stale temp does not block commit, and field escaping. Docs updated: IMPLEMENTATION_SPEC section 17 gained the full rewrite contract, the acceptance checklist gained seven checks including the two failure modes above. cargo test --workspace: 124 + 4 passed, ten new. clippy -D warnings clean.
This commit is contained in:
@@ -100,6 +100,12 @@
|
||||
- [ ] `ensure-swap` создаёт файл по пути `swap.path` (в примере `/swapfile`) размером `swap.size_mb` (в примере 2048 MB).
|
||||
- [ ] Права на swap-файл — `0600`.
|
||||
- [ ] Перед перезаписью `/etc/fstab` создана резервная копия `/etc/fstab.awg2-harness.bak`.
|
||||
- [ ] После первой правки существует `/etc/fstab.awg2-harness.original` и он **не** меняется при последующих запусках.
|
||||
- [ ] Нечитаемый `/etc/fstab` (например, `chmod 000`) приводит к явной ошибке, а не к перезаписи файла одной managed-строкой.
|
||||
- [ ] Отсутствующий `/etc/fstab` приводит к ошибке, а не к созданию нового файла.
|
||||
- [ ] Если резервную копию записать не удалось, `/etc/fstab` остаётся неизменным.
|
||||
- [ ] `status` при нечитаемом `/etc/fstab` показывает ошибку, а не «нет swap-записей».
|
||||
- [ ] Путь swap с пробелом в имени попадает в `/etc/fstab` экранированным (`\040`) и корректно монтируется.
|
||||
- [ ] Если до запуска был активен другой swap, после `ensure-swap` он выключен.
|
||||
- [ ] После `ensure-swap` активен ровно один swap: `/swapfile`.
|
||||
- [ ] Все старые swap entries удалены из `/etc/fstab`.
|
||||
|
||||
Reference in New Issue
Block a user