Skip to content

Commit d22c448

Browse files
committed
fix(claim): stop airplanes-claim.timer after a successful claim write
Once the secret lands on disk the timer's job is done. Without an active stop, its OnUnitActiveSec=5min keeps firing and systemd logs a condition-skip line against airplanes-claim.service on every fire — which the webconfig Claim activity panel surfaces to feeder owners as endless 'unmet condition' noise. Hook into both writers (claim register success, claim set new-write + idempotent re-normalize). The helper is silent on hosts without systemctl or the timer unit, mirroring restart_feeder_services's guards, so legacy non-image installs are unaffected. WantedBy=timers.target stays intact so a future factory reset or reclaim re-arms the timer on the next reboot.
1 parent be88d95 commit d22c448

5 files changed

Lines changed: 460 additions & 18 deletions

File tree

scripts/apl-feed/claim.sh

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ claim_register() {
118118
chmod 640 "$pending"
119119
mv "$pending" "$final"
120120
fi
121+
# The secret is on disk; stop the timer before any later
122+
# failure (write_version_file errno, echo EPIPE, etc.)
123+
# could abort under `set -e` and leave the retry timer
124+
# firing indefinitely against a now-claimed feeder.
125+
stop_claim_timer_if_present
121126
write_version_file "$version"
122127
echo "SUCCESS ($status, version $version)"
123128
echo "Secret persisted to $final"
@@ -394,10 +399,16 @@ claim_rotate() {
394399
claim_set() {
395400
# Save a claim secret minted by the website (e.g. the same-IP claim
396401
# legacy bootstrap, the owner-side Reset secret flow, or a support
397-
# reset) and restart feeder services so the new value takes effect
398-
# immediately. The secret is read from stdin (TTY prompts; pipes work
402+
# reset). The secret is read from stdin (TTY prompts; pipes work
399403
# too) so the value never lands in argv or shell history.
400404
#
405+
# No feeder-daemon restart: neither airplanes-feed nor airplanes-mlat
406+
# reads the claim secret — only apl-feed itself does, and the next
407+
# CLI invocation picks up the file change immediately. The only
408+
# systemd touch is stopping the image-side airplanes-claim.timer
409+
# post-write so the now-claimed feeder stops accumulating condition-
410+
# skip noise in the service journal.
411+
#
401412
# Refuses to overwrite an existing different secret unless --force is
402413
# passed; a no-op when the supplied secret already matches the local
403414
# one.
@@ -468,7 +479,7 @@ claim_set() {
468479

469480
echo "Feeder ID: $uuid"
470481
if (( DRY_RUN )); then
471-
echo "(dry-run; would save secret + restart feeder services)"
482+
echo "(dry-run; would save secret to $final)"
472483
return 0
473484
fi
474485

@@ -481,14 +492,17 @@ claim_set() {
481492
# Same canonical value already on disk. Re-write to normalize byte
482493
# contents (lowercase / hyphenated raw input gets canonicalized)
483494
# and the file mode (0600). Don't drop the version file — the
484-
# local secret bytes haven't functionally changed. Don't restart
485-
# services either: nothing observable changed.
495+
# local secret bytes haven't functionally changed.
486496
write_secret_file "$final" "$secret"
497+
stop_claim_timer_if_present
487498
echo "Local claim secret already matches — no change."
488499
return 0
489500
fi
490501

491502
write_secret_file "$final" "$secret"
503+
# Secret is on disk; stop the timer before any later step (rm,
504+
# echo EPIPE) could abort under `set -e`.
505+
stop_claim_timer_if_present
492506

493507
# Local secret no longer matches whatever the version file claimed.
494508
# Drop the version file so `claim show` / `backup` can't pair a
@@ -497,12 +511,10 @@ claim_set() {
497511
rm -f "$version_path"
498512

499513
echo "Claim secret saved."
500-
# No daemon restart: neither airplanes-feed nor airplanes-mlat reads
501-
# the claim secret. Only this CLI does, and the next CLI invocation
502-
# picks up the file change immediately. The website's hash is already
503-
# the new value (this command is run AFTER the website mints the
504-
# secret), so the next outbound `status` or `rotate` from the feeder
505-
# authenticates fine.
514+
# The website's hash is already the new value (this command is run
515+
# AFTER the website mints the secret), so the next outbound `status`
516+
# or `rotate` from the feeder authenticates fine. No daemon restart
517+
# — neither airplanes-feed nor airplanes-mlat reads this file.
506518
echo "Done. The feeder will use the new secret on its next contact with the website."
507519
}
508520

scripts/apl-feed/common.sh

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,34 @@ restart_feeder_services() {
510510
return $rc
511511
}
512512

513+
# Stop the image-side airplanes-claim.timer after a successful claim write
514+
# (claim register / claim set). The timer drives retry of unclaimed feeders;
515+
# once the secret is on disk it has nothing to do, and every subsequent
516+
# 5-min fire logs `Condition check resulted in ... skipped` against the
517+
# service unit — which the webconfig "Claim activity" panel surfaces as
518+
# noise. The timer's WantedBy=timers.target keeps it re-armable on next
519+
# reboot if the secret is ever deleted (factory reset, manual reclaim).
520+
#
521+
# Defensive on every leg: silently no-ops on hosts without systemctl,
522+
# hosts without the timer unit (legacy non-image installs), and non-root
523+
# --root invocations (mirrors restart_feeder_services — stopping the
524+
# host's timer when operating on a different rootfs is wrong).
525+
#
526+
# APL_FEED_TEST_TIMER_STOP_FORCE is a test-only override so bats
527+
# integration tests can exercise the helper while using --root to scope
528+
# filesystem fixtures. The TEST prefix is deliberate so a stray export
529+
# in a production shell is visible at a glance; production callers must
530+
# never set this.
531+
stop_claim_timer_if_present() {
532+
if [[ "$ROOT" != "/" && -z "${APL_FEED_TEST_TIMER_STOP_FORCE:-}" ]]; then
533+
return 0
534+
fi
535+
if ! command -v systemctl >/dev/null 2>&1; then
536+
return 0
537+
fi
538+
systemctl --no-block stop airplanes-claim.timer 2>/dev/null || true
539+
}
540+
513541
parse_common_option() {
514542
case "${1:-}" in
515543
--root)

test/test_apl_feed_cli.bats

Lines changed: 102 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -570,22 +570,32 @@ EOF
570570
[ "$(cat "$ROOT_DIR/etc/airplanes/feeder-claim-secret.version")" = "7" ]
571571
}
572572

573-
@test "claim set never invokes systemctl (no daemon consumes the secret)" {
573+
@test "claim set never restarts feeder daemons (no daemon consumes the secret)" {
574574
# The claim secret is consumed only by apl-feed itself, not by
575575
# airplanes-feed or airplanes-mlat. Saving it must not bounce a
576-
# working feeder. Use a sentinel stub that fails the test if invoked.
576+
# working feeder. The only permitted systemctl call is the post-write
577+
# stop of airplanes-claim.timer (see test_claim_timer_stop.bats);
578+
# this assertion pins that NOTHING ELSE is touched.
579+
COMMAND_LOG="$(mktemp)"
577580
cat > "$STUB_BIN_DIR/systemctl" <<'STUB'
578581
#!/usr/bin/env bash
579-
echo "systemctl invoked with: $*" >&2
580-
exit 99
582+
{ printf 'systemctl'; for a in "$@"; do printf ' %s' "$a"; done; printf '\n'; } >> "$COMMAND_LOG"
583+
exit 0
581584
STUB
582585
chmod +x "$STUB_BIN_DIR/systemctl"
586+
export COMMAND_LOG APL_FEED_TEST_TIMER_STOP_FORCE=1
583587

584588
run env "$SCRIPT" claim set --root "$ROOT_DIR" <<<"ABCDEFGHIJKLMNOP"
585589

586590
[ "$status" -eq 0 ]
587-
[[ ! "$output" =~ "systemctl invoked" ]]
588591
[[ ! "$output" =~ "Restarted" ]]
592+
# Every recorded systemctl invocation must be the timer-stop. No
593+
# restart, no reload, no apl-feed daemon touched.
594+
while IFS= read -r line; do
595+
[[ "$line" == "systemctl --no-block stop airplanes-claim.timer" ]] \
596+
|| { echo "unexpected systemctl call: $line" >&2; false; }
597+
done < "$COMMAND_LOG"
598+
rm -f "$COMMAND_LOG"
589599
}
590600

591601
@test "claim set is idempotent when supplied secret already matches local" {
@@ -657,6 +667,93 @@ STUB
657667
[ ! -f "$restart_log" ]
658668
}
659669

670+
671+
# --- claim set: post-write timer stop -------------------------------------
672+
#
673+
# Coordinated with the image-side airplanes-claim.timer. Once claim set
674+
# lands the secret on disk, the timer has nothing left to do, and every
675+
# subsequent fire pollutes the service journal with condition-skip lines
676+
# that the webconfig Claim activity panel surfaces.
677+
678+
@test "claim set new-secret write stops airplanes-claim.timer" {
679+
COMMAND_LOG="$(mktemp)"
680+
cat > "$STUB_BIN_DIR/systemctl" <<'STUB'
681+
#!/usr/bin/env bash
682+
{ printf 'systemctl'; for a in "$@"; do printf ' %s' "$a"; done; printf '\n'; } >> "$COMMAND_LOG"
683+
exit 0
684+
STUB
685+
chmod +x "$STUB_BIN_DIR/systemctl"
686+
export COMMAND_LOG APL_FEED_TEST_TIMER_STOP_FORCE=1
687+
688+
run env "$SCRIPT" claim set --root "$ROOT_DIR" <<<"ABCDEFGHIJKLMNOP"
689+
690+
[ "$status" -eq 0 ]
691+
grep -F -- '--no-block stop airplanes-claim.timer' "$COMMAND_LOG"
692+
rm -f "$COMMAND_LOG"
693+
}
694+
695+
@test "claim set same-canonical-value (idempotent) still stops the timer" {
696+
# The re-normalize path also writes the file (to fix mode / casing), so
697+
# we want the timer-stop here too — keeps the helper invariant simple:
698+
# any successful secret write triggers the stop.
699+
echo "abcd-efgh-ijkl-mnop" > "$ROOT_DIR/etc/airplanes/feeder-claim-secret"
700+
chmod 644 "$ROOT_DIR/etc/airplanes/feeder-claim-secret"
701+
COMMAND_LOG="$(mktemp)"
702+
cat > "$STUB_BIN_DIR/systemctl" <<'STUB'
703+
#!/usr/bin/env bash
704+
{ printf 'systemctl'; for a in "$@"; do printf ' %s' "$a"; done; printf '\n'; } >> "$COMMAND_LOG"
705+
exit 0
706+
STUB
707+
chmod +x "$STUB_BIN_DIR/systemctl"
708+
export COMMAND_LOG APL_FEED_TEST_TIMER_STOP_FORCE=1
709+
710+
run env "$SCRIPT" claim set --root "$ROOT_DIR" <<<"ABCD-EFGH-IJKL-MNOP"
711+
712+
[ "$status" -eq 0 ]
713+
[[ "$output" =~ "already matches" ]]
714+
grep -F -- '--no-block stop airplanes-claim.timer' "$COMMAND_LOG"
715+
rm -f "$COMMAND_LOG"
716+
}
717+
718+
@test "claim set --dry-run does NOT stop the timer (no secret was written)" {
719+
COMMAND_LOG="$(mktemp)"
720+
cat > "$STUB_BIN_DIR/systemctl" <<'STUB'
721+
#!/usr/bin/env bash
722+
{ printf 'systemctl'; for a in "$@"; do printf ' %s' "$a"; done; printf '\n'; } >> "$COMMAND_LOG"
723+
exit 0
724+
STUB
725+
chmod +x "$STUB_BIN_DIR/systemctl"
726+
export COMMAND_LOG APL_FEED_TEST_TIMER_STOP_FORCE=1
727+
728+
run env "$SCRIPT" claim set --root "$ROOT_DIR" --dry-run <<<"ABCDEFGHIJKLMNOP"
729+
730+
[ "$status" -eq 0 ]
731+
[[ "$output" =~ "dry-run" ]]
732+
[ ! -f "$ROOT_DIR/etc/airplanes/feeder-claim-secret" ]
733+
! grep -F -- 'stop airplanes-claim.timer' "$COMMAND_LOG"
734+
rm -f "$COMMAND_LOG"
735+
}
736+
737+
@test "claim set refuse-without-force does NOT stop the timer (nothing written)" {
738+
echo "OLDSECRETXYZ1234" > "$ROOT_DIR/etc/airplanes/feeder-claim-secret"
739+
chmod 600 "$ROOT_DIR/etc/airplanes/feeder-claim-secret"
740+
COMMAND_LOG="$(mktemp)"
741+
cat > "$STUB_BIN_DIR/systemctl" <<'STUB'
742+
#!/usr/bin/env bash
743+
{ printf 'systemctl'; for a in "$@"; do printf ' %s' "$a"; done; printf '\n'; } >> "$COMMAND_LOG"
744+
exit 0
745+
STUB
746+
chmod +x "$STUB_BIN_DIR/systemctl"
747+
export COMMAND_LOG APL_FEED_TEST_TIMER_STOP_FORCE=1
748+
749+
run env "$SCRIPT" claim set --root "$ROOT_DIR" <<<"NEWSECRETXYZ5678"
750+
751+
[ "$status" -ne 0 ]
752+
[[ "$output" =~ "different claim secret" ]]
753+
! grep -F -- 'stop airplanes-claim.timer' "$COMMAND_LOG"
754+
rm -f "$COMMAND_LOG"
755+
}
756+
660757
@test "id set writes a new UUID and restarts both daemons feed-first" {
661758
rm -f "$ROOT_DIR/etc/airplanes/feeder-id"
662759
local restart_log="$ROOT_DIR/restart.log"

0 commit comments

Comments
 (0)