Commit a5ae454
restore: route through pod fastboot + pod TFTP when power=rack (#94)
## Summary
Brings `defib restore` to parity with `defib install` (#88 + #93) for
rack-controlled cameras. Three pieces:
### Phase 1 — fastboot when `power=rack`
The previous host-side frame-blast race (power-off → open serial → start
session → power-on) is RouterOS-only. Rack pods don't expose independent
`power_off`/`power_on` and don't need to — the pod's `/fastboot`
endpoint does the whole sequence locally with microsecond ACK latency.
Drop the hard-coded *"restore needs RouterOSController only"* reject —
`RackController` is now an accepted alternative. Vectis stays rejected.
### Phase 5 — `--tftp-via=auto|pod|host` (default auto)
Same flag as `install`. Auto → pod when `power=rack`, host otherwise.
Pod path stages every partition via `RackController.tftp_put`, sets
`serverip=192.168.1.1` (the pod), and unifies the UBI rootfs file-swap
through `_replace_in_tftp(name, data)`.
Two robustness improvements:
- **`tftp_clear` BEFORE staging.** A prior aborted run leaves PSRAM
occupied; if the next run can't allocate, the 4 MB rootfs OOMs at 256 KB
largest-free. Wipe first.
- **`try/finally` around Phase 5 + 6.** A mid-loop write failure skipped
`__aexit__` and leaked ~7 MB of pod PSRAM until the next install. The
`try/finally` (with the cleanup hooks pre-registered on the
`AsyncExitStack`) makes cleanup unconditional.
### Live verification on rack pod `10.216.128.69` (hi3516ev300)
Synthetic dump dir at `/tmp/cam_dump/` (mtd0..3 sized to match the 16 MB
NOR layout):
```
$ DEFIB_POWER_TYPE=rack DEFIB_RACK_HOST=10.216.128.69 \
defib restore -c hi3516ev300 -i /tmp/cam_dump/ \
-p rack://10.216.128.69 --power-cycle --flash-type nor
Power: rack pod HTTP API
Phase 1: Loading U-Boot to RAM
Pod-side fastboot in progress…
Phase 4: Network setup — Network OK (attempt 1)
Phase 5: Writing flash
Staging 7664 KB in pod PSRAM via POST /tftp/<name>...
Pod TFTP ready on 192.168.1.1:69
mtd1: 64KB → 0x40000 Written (7.5 s)
mtd2: 3072KB → 0x50000 Written (11.7 s)
mtd3: 4272KB → 0x350000 Written (15.7 s)
mtd0: 256KB → 0x0 Written (8.3 s)
Restore complete!
```
Camera reaches `openipc-hi3516ev300 login:` cleanly. `exit=0`.
### Companion rack-firmware change (local-only)
`UART_IDLE_TIMEOUT_S` **60 → 600**. The 60-second idle timer was killing
the bridge socket mid-staging — ~50 s of HTTP `/tftp` uploads counts as
"idle" to the bridge (no host→pod UART traffic during that window). 600
s comfortably covers full installs and restores.
## Test plan
- [ ] `uv run pytest tests/ -x -v --ignore=tests/fuzz` — 486 passed / 2
skipped (no new unit tests; `_restore_async` is integration-only)
- [ ] `uv run ruff check src/defib/cli/app.py` — clean
- [ ] `uv run mypy src/defib/cli/app.py --ignore-missing-imports` —
clean
- [ ] Regression: `defib restore --tftp-via host …` still works on
existing RouterOS+host-TFTP setups — host branch is byte-identical
except for being inside the shared `AsyncExitStack`.
- [ ] `--tftp-via pod` without `DEFIB_POWER_TYPE=rack` → clean error
message.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Dmitry Ilyin <widgetii@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent ed4bc98 commit a5ae454
1 file changed
Lines changed: 376 additions & 262 deletions
0 commit comments