asterisk.sh: proactive stack health check in update mode

"update" mode's whole promise is leaving already-configured things
alone — but that assumption silently breaks when something was
configured but never fully wired up, and update never re-asks the
questions that would reveal it. This session hit three separate
instances of exactly that on one droplet revert: a domain set with no
Caddy block, a Caddy block with no synced TLS cert (transport-tls fails
to bind — "Unable to retrieve PJSIP transport 'transport-tls'", breaking
every call), and a baked-in external IP left over from before the box
moved. sms-inbound and (potentially) Security Dashboard/ntfy can have
the identical "domain set, nothing serving it" gap with no way to
discover it either, since their own update modes don't re-ask.

_asterisk_run_stack_health_check(), called every "update", replaces the
narrower Caddy-only check added last time:
- Compares pjsip.conf's baked external_signaling_address against this
  box's actual current public IP; offers to rewrite it and restart.
- Checks Asterisk's own DOMAIN_NAME has both a Caddy site block and a
  matching TLS cert in the container; offers to fix each independently.
- Checks Security Dashboard / sms-inbound / ntfy (whichever are
  installed) for a matching Caddy site block, via a new lib/common.sh
  helper (caddy_domain_for_upstream) that finds the block without
  needing to already know the domain — none of these three services
  persist it anywhere. Points at that service's own "Full reinstall"
  (the only mode that re-asks) since fixing their config isn't this
  file's to script.

The cert-sync fix needed a non-interactive hook into the vendored
easy-asterisk CLI, which only exposed it as an interactive menu item
(Server Settings -> Force re-sync Caddy certs). Added a --sync-caddy-cert
flag via _asterisk_patch_cert_sync_cli(), patching the deployed vendor
copy the same way _asterisk_patch_voicemail_vendor_files and friends
already do — never vendor/ in git.

Every check runs unconditionally (never opt-in, so a gap is never missed
by nobody thinking to ask); every fix is individually opt-in and named
as a real config change, unlike the rest of "update"'s no-side-effects
default.

Also factored the DO-metadata/ifconfig.me/hostname-I public-IP detection
chain (previously duplicated 3 times) into _asterisk_current_public_ip().

Verified: caddy_domain_for_upstream against a multi-block Caddyfile
(distinguishes same-prefix upstreams correctly); the full health check
against fake docker/curl across every combination (all wired, IP
mismatch declined/accepted, cert mismatch declined/accepted, dashboard
unwired, sms-inbound wired vs. placeholder-domain, multi-instance ntfy
with one wired and one not); and _asterisk_patch_cert_sync_cli's
idempotency + resulting syntax against a real copy of the vendor script.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpKTLpwAgZNooTacWeQLuc
This commit is contained in:
Claude
2026-08-21 22:45:36 +00:00
parent b7691e6c1b
commit d1a3c4b0be
2 changed files with 271 additions and 40 deletions
+36
View File
@@ -416,6 +416,42 @@ _remove_caddy_site_block() {
' "$caddy_file"
}
# Read-only counterpart to _remove_caddy_site_block: returns (on stdout) the
# domain of the local Caddy site block whose body contains
# "reverse_proxy <upstream>" (same substring-match convention), or nothing
# if there's no local Caddy, no Caddyfile, or no matching block. Never
# modifies the Caddyfile — for services/asterisk.sh's stack health check
# (and any future caller) to answer "is X actually wired into Caddy?"
# without needing to already know the domain, since several services here
# (security-dashboard, sms-inbound) never persist the domain they were
# configured with anywhere — the Caddyfile is the only record of it.
caddy_domain_for_upstream() {
local upstream="$1"
local caddyfile="$DOCKER_DIR/caddy/Caddyfile"
[ -f "$caddyfile" ] || return 0
awk -v upstream="$upstream" '
BEGIN { depth = 0; candidate = ""; domain = ""; found = 0 }
{
line = $0
opens = gsub(/\{/, "{", line)
closes = gsub(/\}/, "}", line)
if (depth == 0 && opens > 0) {
header = $0
sub(/[[:space:]]*\{.*$/, "", header)
candidate = header
depth += opens - closes
next
}
if (depth > 0) {
if (index($0, "reverse_proxy " upstream) > 0) { found = 1; domain = candidate }
depth += opens - closes
next
}
}
END { if (found) print domain }
' "$caddyfile"
}
# Generic per-service removal: stops/removes its containers, its Caddy site
# block (if any), any UFW rule tagged with its name, and optionally its
# ~/docker/<name> directory. Scoped to the common case (a Docker service