Skip to content

Add migration deadman switch and Australia migration docs#46

Open
pbitzer wants to merge 10 commits into
0.3.xfrom
feature/migration-tooling
Open

Add migration deadman switch and Australia migration docs#46
pbitzer wants to merge 10 commits into
0.3.xfrom
feature/migration-tooling

Conversation

@pbitzer

@pbitzer pbitzer commented Mar 31, 2026

Copy link
Copy Markdown
Contributor

Summary

  • scripts/migration-deadman.sh — Safety net for remote sensor migrations: arms a systemd timer that auto-restores from backup and reboots if not defused within a configurable timeout
  • docs/superpowers/specs/ — Migration design spec
  • docs/superpowers/plans/ — Migration execution plan
  • Hardened through 11 rounds of review

Used in production

Successfully used for mj41, mj42, mj43 migration (2026-03-27/30). Armed, verified, and defused on all three sensors with zero rollbacks needed.

Test plan

  • 28 shell tests pass (shellcheck, syntax)
  • Used in 3 production migrations

🤖 Generated with Claude Code

pbitzer and others added 10 commits March 31, 2026 10:26
Migration plan for mj41/42/43 from wwan_install to feature/daily-compression.
Includes dead man's switch script (systemd timer-based rollback) for safety
during remote cellular network changes on sensors accessible only via SSH tunnels.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Deadman script fixes:
- Validate minutes argument (prevent 0-second timer on typo)
- Check systemctl enable/start exit codes, verify timer is running
- Detect in-progress rollback in defuse() with warning
- Signal trap in arm() cleans up partial state on Ctrl-C/SSH drop
- Force git checkout (-f) in rollback for dirty working trees
- Use runuser instead of sudo in rollback (avoids PAM issues)
- Use --absolute-names in tar to suppress warnings
- Capture tar exit code explicitly in rollback
- Trap SIGTERM in rollback after file restore (point of no return)
- Remove unreliable $0 from user-facing messages

Plan fixes:
- Reorder git fetch before stopping wwan-check timer (shrink
  no-recovery window)
- Add ip route check after install.sh (verify wwan0 default route)
- Use ; instead of && in verification checklist (one failure
  doesn't abort remaining checks)
- Use precise grep patterns (^DNS=, ^APN\s*=)
- Add 30-eth1.network to verification checklist
- Add fallback defuse instructions for post-reboot scenario
- Fix heredoc-over-SSH for power_delim (use printf instead)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comprehensive hardening of migration-deadman.sh based on iterative
expert review (bash, systemd, signal handling, adversarial testing):

- Fix git checkout: remove `--` separator that treated branch as pathspec
- Move trap '' TERM INT HUP to first line of rollback (prevent mid-restore kill)
- Add ConditionPathExists + backup dir deletion to break infinite reboot loops
- Add flock concurrency control to prevent arm/defuse races
- Add timeout on systemctl stop in defuse (TOCTOU race with timer firing)
- Add post-stop re-check of service state to prevent false "DEFUSED" message
- Add timeout 30 on systemd-networkd restart in rollback (prevent hang)
- Add timeout 60 on git checkout in rollback (prevent hang on hooks/locks)
- Add tar integrity verification before extraction
- Add sync before reboot, reboot fallback cascade (reboot -f, sysrq)
- Add input validation (positive int, max 1440 min), cleanup on all error paths
- Fix detached HEAD handling (guard against literal "HEAD" branch name)
- Use sudo -H -u pi per project rules (not runuser)
- Use systemctl show ActiveState for oneshot service detection (not is-active)
- Add logger calls for syslog audit trail

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fixes 2 P0, 4 P1, 4 P2, and 2 P3 findings from independent review:

P0: Defuse re-check now matches both activating AND deactivating states
    (timeout kills systemctl client, not the rollback process)
P0: Read-only filesystem detection with rollback attempt counter (max 3)
    to break infinite reboot loops when SD card goes read-only
P1: Add SIGPIPE to rollback trap (dead syslog socket can't kill us)
P1: Constrain minutes regex to ^[0-9]{1,4}$ (prevent bash arithmetic overflow)
P1: Add sync immediately after tar extraction (not just before reboot)
P1: Add --unlink-first to tar extract (handle file-type changes)
P2: Add daemon-reload after unit deletion in rollback
P2: Persistent rollback log at /var/log/deadman-rollback.log
P2: Verify chmod +x and unit file writes before enabling timer
P3: Stop timer before service in defuse (close re-trigger window)
P3: Eliminate double tar read on degraded SD cards

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Round 7 review findings:

- Verify rollback script heredoc was fully written (grep sentinel)
  to catch truncation from full disk — without this, a truncated
  rollback script gets armed but never completes on fire
- Reject leading zeros in minutes arg (^[1-9][0-9]{0,3}$) to avoid
  bash octal interpretation errors (e.g. "09" is invalid octal)
- Sanitize attempt counter from disk before arithmetic to handle
  corrupted file contents gracefully

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Test both /var/lib and /etc writability (selective sector wear can
  make one RO while other is writable)
- Atomic attempt counter write via tmp+mv (prevents corruption on
  power loss mid-write that would reset counter and enable infinite loop)
- Delete rollback script before rm -rf backup dir (ConditionPathExists
  target removed first, so partial rm -rf still breaks the loop)
- Add exit 1 after reboot cascade failure (systemd marks service failed)
- Document --unlink-first tradeoff (file-type fix vs power-loss risk)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- arm() now checks if rollback service is running before proceeding
  (flock only guards this script, not the systemd-launched rollback)
- arm() checks for unit file existence as secondary guard against
  early-boot false negatives from systemctl is-active
- logger wrapped in timeout 5 to prevent hangs when journald is stuck
- RO filesystem now attempts remount rw before giving up; if truly
  read-only, sleeps 5min before reboot to throttle the loop instead
  of falling through to cleanup code that silently fails
- Trap handlers now pass correct exit codes per signal

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address 5 findings from Round 10 (4 independent reviewers):
- P1: Add --kill-after=10 to all timeout invocations in rollback heredoc
  (trap '' TERM sets SIG_IGN, inherited across fork+exec per POSIX;
  SIGTERM ignored by children, SIGKILL cannot be)
- P1: Wrap logger in cleanup_partial_arm with timeout 5
- P2: Wrap all bare logger calls in arm/defuse with timeout 5
- P2: Add timeout to systemctl daemon-reload in rollback
- P2: Clean up orphaned wwan-check files not in backup tar after rollback

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- P1: Guard orphan cleanup with tar readability check — if backup tar
  is corrupt, skip file deletion entirely (conservative: leaving extra
  files is safer than deleting needed wwan-check mechanism)
- P3: Add || true to defuse() daemon-reload for consistency

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implementation plans and design specs are working documents,
not permanent repo artifacts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant