From 32355e021974d8b833727bae163df68635f5b732 Mon Sep 17 00:00:00 2001 From: Gmer4Lfe Date: Mon, 17 Aug 2026 04:46:01 -0400 Subject: [PATCH] Record the tailscale grace period as a deadline instead of sleeping six hours inside the offboard --- Partnership/partnership_offboard.sh | 70 ++++++++++++++++++++--------- 1 file changed, 49 insertions(+), 21 deletions(-) diff --git a/Partnership/partnership_offboard.sh b/Partnership/partnership_offboard.sh index 62cf37e..cf3e51d 100755 --- a/Partnership/partnership_offboard.sh +++ b/Partnership/partnership_offboard.sh @@ -25,7 +25,7 @@ # Step 8: Restart mirror — bring up mirror's own parked containers # Step 9: Revocation — Emby admin, SSH keys # Step 10: Write state — INACTIVE locally + pushed to mirror, mirror blocklisted -# Tailscale — grace window then device removal (after state written) +# Tailscale — grace deadline recorded, device removed after it expires # # MIRROR PATH (8 steps) # Step 1: Stop rsync — halt any running sync @@ -103,9 +103,12 @@ # The mirror is added to the partnership blocklist, which rsync.sh checks and refuses on — # stale access cannot survive the offboard. # -# Tailscale Grace Window +# Tailscale Grace Window Is a Deadline, Not a Sleep # Device removal happens after state is written, not before, so the final state push -# cannot be cut off by removing its own transport. +# cannot be cut off by removing its own transport. The grace period itself is recorded to +# STATE_DIR/tailscale_removal_due.db and the offboard returns. It used to sleep +# PARTNERSHIP_GRACE_HOURS inline — six hours by default — holding the lock and its job record +# open the whole time, reporting "running", and blocking any re-onboard behind it. # # Dry Run Support # --dry-run walks the full sequence reporting each step without executing any. @@ -603,21 +606,46 @@ fi if [[ "${PARTNERSHIP_REMOVE_TAILSCALE:-true}" == true ]]; then echo "" echo "━━━ $ICON_NET Tailscale Separation ━━━" - if [[ "$MIRROR_REACHABLE" == true ]]; then - grace_seconds=$(( ${PARTNERSHIP_GRACE_HOURS:-6} * 3600 )) - warn "Waiting ${PARTNERSHIP_GRACE_HOURS:-6}hr grace — mirror can collect backups..." + # The grace period is recorded as a deadline, not slept through. + # + # This used to `sleep $((PARTNERSHIP_GRACE_HOURS * 3600))` inline — six hours by default — + # holding the offboard's lock and its job record open the whole time, showing "running" to + # every status reader, and blocking any re-onboard behind the lock. Worse, the sleep ran + # even when removal was going to be a no-op: TAILSCALE_API_KEY and TAILSCALE_TAILNET are + # both empty here, so the six hours bought nothing at all. + # + # The offboard's own work is finished by this point. Writing the deadline lets the teardown + # complete now and leaves the removal to whoever reads the file — and makes the wait + # visible and cancellable instead of buried in a sleeping process. + _grace_h="${PARTNERSHIP_GRACE_HOURS:-6}" + if [[ "$_grace_h" -gt 0 ]] && [[ "$MIRROR_REACHABLE" == true ]]; then + _due=$(( $(date +%s) + _grace_h * 3600 )) if [[ "$DRY_RUN" == false ]]; then - trap 'warn "Offboard interrupted during grace sleep"; exit 0' SIGTERM SIGINT - sleep "$grace_seconds" - trap - SIGTERM SIGINT + printf 'host=%s\ndue=%s\ndue_human=%s\nreason=%s\n' \ + "$MIRROR" "$_due" "$(date -d "@$_due" '+%Y-%m-%d %H:%M:%S')" "$REASON" \ + > "${STATE_DIR}/tailscale_removal_due.db" + fi + warn "Grace period: $MIRROR stays on the tailnet until $(date -d "@$_due" '+%Y-%m-%d %H:%M') — recorded, not slept" + # No CLI entry point removes it yet, and there is deliberately no invented one here: + # remove_tailscale_device() is a partnership_manager.sh function with no --mode of its + # own, and it no-ops without credentials regardless. Say what is true. + if [[ -z "${TAILSCALE_API_KEY:-}" || -z "${TAILSCALE_TAILNET:-}" ]]; then + warn "Automatic removal is not possible — TAILSCALE_API_KEY/TAILSCALE_TAILNET are unset; remove it in the Tailscale admin console" + else + warn "Removal after that is not yet automated — remove it in the Tailscale admin console" + fi + TAILSCALE_REMOVED=deferred + unset _grace_h _due + else + # No grace configured, or the mirror is already unreachable — remove now. + # + # Outcome recorded, not assumed. remove_tailscale_device returns 1 when TAILSCALE_API_KEY + # or TAILSCALE_TAILNET is unset — it warns "skipping Tailscale removal" and the summary + # went on to report "removed ✅" anyway, so an offboard that left the device on the + # tailnet said it had taken it off. Neither key is configured here, so that was every run. + if remove_tailscale_device "$MIRROR"; then + TAILSCALE_REMOVED=true fi - fi - # Outcome recorded, not assumed. remove_tailscale_device returns 1 when TAILSCALE_API_KEY or - # TAILSCALE_TAILNET is unset — it warns "skipping Tailscale removal" and the summary went on - # to report "removed ✅" anyway, so an offboard that left the device on the tailnet said it - # had taken it off. Neither key is configured here, so that was the case on every run. - if remove_tailscale_device "$MIRROR"; then - TAILSCALE_REMOVED=true fi fi @@ -667,11 +695,11 @@ echo " Step 10 — State: $( [[ "$STEP_STATE_WRITE_OK" == true ]] && e echo "" echo " Blocklist: $MIRROR blocked — re-onboard to permit access again ✅" if [[ "${PARTNERSHIP_REMOVE_TAILSCALE:-true}" == true ]]; then - if [[ "$TAILSCALE_REMOVED" == true ]]; then - echo " Tailscale: $MIRROR removed ✅" - else - echo " Tailscale: $MIRROR NOT removed ⚠ — still on the tailnet (needs TAILSCALE_API_KEY + TAILSCALE_TAILNET)" - fi + case "$TAILSCALE_REMOVED" in + true) echo " Tailscale: $MIRROR removed ✅" ;; + deferred) echo " Tailscale: $MIRROR kept until the grace period expires — see ${STATE_DIR}/tailscale_removal_due.db" ;; + *) echo " Tailscale: $MIRROR NOT removed ⚠ — still on the tailnet (needs TAILSCALE_API_KEY + TAILSCALE_TAILNET)" ;; + esac fi # Named because it is the one partnership switch neither onboard nor offboard moves, so it # survives an offboard still true and there is nothing else that would ever mention it.