From 2483365141bc1873ad0f5068a42725fe8782533c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 16:51:50 +0000 Subject: [PATCH 1/6] Fix web admin/CrowdSec port collision: default WEB_ADMIN_PORT to 8081 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Real-world failure: CrowdSec's Local API listens on 127.0.0.1:8080 by default (confirmed against its actual upstream config.yaml), and Easy Asterisk's web admin also defaults to 8080. Both services in this repo run with network_mode: host / directly on the host, so whichever one starts second gets "OSError: [Errno 98] Address already in use" — in this case CrowdSec (started earlier via the auto-install chain) had already claimed the port before the web admin tried to start. Moved the web admin's default to 8081 in both asterisk-do.sh and asterisk.sh — WEB_ADMIN_PORT in .env, the UFW rule, the DO Cloud Firewall rule, the Caddy reverse_proxy target, and every doc/summary reference. 8081 doesn't collide with anything else in either stack (5060/5061/8088/8089/3478/10000-20000/49152-49252) or with CrowdSec's LAPI (8080) or Prometheus metrics (6060, localhost-only either way). Left the vendor files' own internal fallback (WEB_ADMIN_PORT:-8080) untouched — .env's explicit value overrides it at runtime regardless, and vendor/ stays pristine per this repo's convention. Verified: no stray 8080 in any generated .env/docker-compose.yml for either service after a full install run; the vendor files' own internal 8080 fallback (never applies here, since .env always sets it explicitly) is the only remaining occurrence anywhere. --- services/asterisk-do.sh | 22 +++++++++++----------- services/asterisk.sh | 14 +++++++------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/services/asterisk-do.sh b/services/asterisk-do.sh index d4e2af8..04e1c08 100755 --- a/services/asterisk-do.sh +++ b/services/asterisk-do.sh @@ -210,7 +210,7 @@ install_asterisk-do() { echo "[DRY-RUN] Would create $EA_DIR with Dockerfile, docker-compose.yml, .env" echo "[DRY-RUN] Would copy/download vendor files from easy-asterisk" echo "[DRY-RUN] Would detect droplet public IP via DO metadata service" - echo "[DRY-RUN] Would open UFW ports: 5060, 5061, 8080, 8088, 8089, 3478, 10000-20000, 49152-49252" + echo "[DRY-RUN] Would open UFW ports: 5060, 5061, 8081, 8088, 8089, 3478, 10000-20000, 49152-49252" echo "[DRY-RUN] Would open 51820/udp (not 51821) if wg-easy was selected" echo "[DRY-RUN] Would offer to create a DigitalOcean Cloud Firewall via doctl" echo "[DRY-RUN] Would reverse-proxy the web admin on the SAME FQDN used for SIP (needed for cert sync)" @@ -512,7 +512,7 @@ HAS_VLANS=n VLAN_SUBNETS= # ── Web admin ───────────────────────────────────────────────── -WEB_ADMIN_PORT=8080 +WEB_ADMIN_PORT=8081 WEB_ADMIN_AUTH_DISABLED=false ENV chmod 600 .env @@ -523,7 +523,7 @@ ENV ufw allow 5060/udp ufw allow 5060/tcp ufw allow 5061/tcp - ufw allow 8080/tcp + ufw allow 8081/tcp ufw allow 8088/tcp ufw allow 8089/tcp ufw allow 3478/udp @@ -555,7 +555,7 @@ ENV "protocol:tcp,ports:5060,address:0.0.0.0/0,address:::/0" "protocol:udp,ports:5060,address:0.0.0.0/0,address:::/0" "protocol:tcp,ports:5061,address:0.0.0.0/0,address:::/0" - "protocol:tcp,ports:8080,address:0.0.0.0/0,address:::/0" + "protocol:tcp,ports:8081,address:0.0.0.0/0,address:::/0" "protocol:tcp,ports:8088-8089,address:0.0.0.0/0,address:::/0" "protocol:tcp,ports:3478,address:0.0.0.0/0,address:::/0" "protocol:udp,ports:3478,address:0.0.0.0/0,address:::/0" @@ -604,9 +604,9 @@ ENV # matching $DOMAIN_NAME, and SIP TLS would silently stay self-signed. So # there's no separate domain prompt: this always targets $DOMAIN_NAME. if [[ -z "$DOMAIN_NAME" ]]; then - log_info "No FQDN set — web admin stays on http://${PUBLIC_IP:-localhost}:8080 (nothing for Caddy to do)." + log_info "No FQDN set — web admin stays on http://${PUBLIC_IP:-localhost}:8081 (nothing for Caddy to do)." elif [[ ! -d "$DOCKER_DIR/caddy" ]] && [[ -z "${CADDY_REMOTE_HOST:-}" ]]; then - log_info "Caddy not installed — web admin stays on http://${PUBLIC_IP:-localhost}:8080, SIP TLS stays self-signed." + log_info "Caddy not installed — web admin stays on http://${PUBLIC_IP:-localhost}:8081, SIP TLS stays self-signed." else local EXTRA_BLOCK="" if [ -d "$DOCKER_DIR/authelia" ]; then @@ -665,7 +665,7 @@ ENV # Asterisk Web Admin ${DOMAIN_NAME} { - reverse_proxy localhost:8080 + reverse_proxy localhost:8081 header { Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" @@ -847,7 +847,7 @@ plan for the admin panel. | 22 | TCP | SSH (keep this open or you're locked out) | | 5060 | UDP/TCP | SIP signalling (unencrypted) | | 5061 | TCP | SIP over TLS | -| 8080 | TCP | Easy Asterisk web admin | +| 8081 | TCP | Easy Asterisk web admin | | 8088/8089 | TCP | Asterisk HTTP/WS (ARI/AMI) | | 3478 | UDP/TCP | TURN/STUN (coturn) | | 10000–20000 | UDP | RTP media streams | @@ -881,7 +881,7 @@ be added later by running \`sudo ./setup.sh \` from the repo. the public firewall; the web UI (51821) is deliberately **not** exposed — reach it via SSH tunnel: \`ssh -L 51821:localhost:51821 user@\`, then browse \`http://localhost:51821\`. A natural next step once it's - installed: restrict the web admin (8080) to the VPN subnet only, on both + installed: restrict the web admin (8081) to the VPN subnet only, on both firewall layers, so reconfiguring the PBX requires being on the VPN — done manually, not automatically, since a firewall mistake there can lock you out. @@ -965,7 +965,7 @@ accept it manually). ## Web admin -Access the Easy Asterisk web interface at http://:8080 +Access the Easy Asterisk web interface at http://:8081 or via your configured reverse-proxy domain. ## Data directories (all inside ~/docker/asterisk-do/, included in backup) @@ -1001,7 +1001,7 @@ MD fi echo " Public IP: ${PUBLIC_IP:-unknown}" echo " SIP port: 5061 (TLS) / 5060 (UDP)" - echo " Web admin: http://${PUBLIC_IP:-localhost}:8080" + echo " Web admin: http://${PUBLIC_IP:-localhost}:8081" echo " Manage: docker compose -f $EA_DIR/docker-compose.yml " echo " Script: docker exec -it easy-asterisk-do easy-asterisk --help" if [[ -n "$DOMAIN_NAME" ]] && [[ -d "$DOCKER_DIR/caddy" ]]; then diff --git a/services/asterisk.sh b/services/asterisk.sh index d3cc4fb..3e06463 100644 --- a/services/asterisk.sh +++ b/services/asterisk.sh @@ -204,7 +204,7 @@ install_asterisk() { if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would create $EA_DIR with Dockerfile, docker-compose.yml, .env" echo "[DRY-RUN] Would copy/download vendor files from easy-asterisk" - echo "[DRY-RUN] Would open UFW ports: 5060, 5061, 8080, 8088, 8089, 3478, 10000-20000, 49152-49252" + echo "[DRY-RUN] Would open UFW ports: 5060, 5061, 8081, 8088, 8089, 3478, 10000-20000, 49152-49252" return 0 fi @@ -383,7 +383,7 @@ HAS_VLANS=${HAS_VLANS_VAL} VLAN_SUBNETS=${VLAN_SUBNETS_VAL} # ── Web admin ───────────────────────────────────────────────── -WEB_ADMIN_PORT=8080 +WEB_ADMIN_PORT=8081 WEB_ADMIN_AUTH_DISABLED=false ENV chmod 600 .env @@ -394,7 +394,7 @@ ENV ufw allow 5060/udp ufw allow 5060/tcp ufw allow 5061/tcp - ufw allow 8080/tcp + ufw allow 8081/tcp ufw allow 8088/tcp ufw allow 8089/tcp ufw allow 3478/udp @@ -415,7 +415,7 @@ ENV sed -i "s/^WEB_ADMIN_AUTH_DISABLED=.*/WEB_ADMIN_AUTH_DISABLED=true/" .env fi fi - configure_caddy_for_service "Asterisk Web Admin" "8080" "asterisk" "$EXTRA_BLOCK" + configure_caddy_for_service "Asterisk Web Admin" "8081" "asterisk" "$EXTRA_BLOCK" # ── README ──────────────────────────────────────────────────────────────── write_readme "$EA_DIR" << 'MD' @@ -483,7 +483,7 @@ to accept it). ## Web admin -Access the Easy Asterisk web interface at http://:8080 +Access the Easy Asterisk web interface at http://:8081 or via your configured reverse-proxy domain. ## Data directories (all inside ~/docker/asterisk/, included in backup) @@ -502,7 +502,7 @@ or via your configured reverse-proxy domain. |---------------|----------|----------------------------------| | 5060 | UDP/TCP | SIP signalling (unencrypted) | | 5061 | TCP | SIP over TLS | -| 8080 | TCP | Easy Asterisk web admin | +| 8081 | TCP | Easy Asterisk web admin | | 8088/8089 | TCP | Asterisk HTTP/WS (ARI/AMI) | | 3478 | UDP/TCP | TURN/STUN (coturn) | | 10000–20000 | UDP | RTP media streams | @@ -530,7 +530,7 @@ MD echo " TURN server: (none — LAN/VPN only)" fi echo " SIP port: 5061 (TLS) / 5060 (UDP)" - echo " Web admin: http://$(hostname -I 2>/dev/null | awk '{print $1}' || echo localhost):8080" + echo " Web admin: http://$(hostname -I 2>/dev/null | awk '{print $1}' || echo localhost):8081" echo " Manage: docker compose -f $EA_DIR/docker-compose.yml " echo " Script: docker exec -it easy-asterisk easy-asterisk --help" echo "" From 4af9cfeae3b6eec51357629b6e3266cf5d501de7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 17:20:56 +0000 Subject: [PATCH 2/6] Auto-scan for a free web admin port instead of hardcoding one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 8080->8081 fix from the last commit just moved the collision risk, not removed it — any hardcoded port can eventually collide with something else on a box running several services. Both services now scan for the first genuinely free port starting at 8081 (ss -tlnH "sport = :$PORT", capped at 100 ports checked) and use whatever they find — .env, UFW, the DO Cloud Firewall rule, and the Caddy proxy target all follow the actual chosen port, not a fixed number. asterisk-do.sh's self-contained Caddy block (unquoted heredoc) reads the port live. asterisk.sh's README heredoc is quoted (no expansion), so its generated docs keep the static "8081" default with an added note to check .env for the real value if it differed — the summary echo outside that heredoc still reports the live value correctly. Verified: normal case still lands on 8081; with 8081 deliberately occupied by another process, both services correctly detect the collision and fall through to 8082 instead, confirmed via the actual generated .env in each case. --- services/asterisk-do.sh | 46 +++++++++++++++++++++++++++++++---------- services/asterisk.sh | 41 +++++++++++++++++++++++++++++------- 2 files changed, 69 insertions(+), 18 deletions(-) diff --git a/services/asterisk-do.sh b/services/asterisk-do.sh index 04e1c08..ff0e6d4 100755 --- a/services/asterisk-do.sh +++ b/services/asterisk-do.sh @@ -210,7 +210,8 @@ install_asterisk-do() { echo "[DRY-RUN] Would create $EA_DIR with Dockerfile, docker-compose.yml, .env" echo "[DRY-RUN] Would copy/download vendor files from easy-asterisk" echo "[DRY-RUN] Would detect droplet public IP via DO metadata service" - echo "[DRY-RUN] Would open UFW ports: 5060, 5061, 8081, 8088, 8089, 3478, 10000-20000, 49152-49252" + echo "[DRY-RUN] Would scan for a free web admin port starting at 8081 (avoids e.g. CrowdSec's 8080)" + echo "[DRY-RUN] Would open UFW ports: 5060, 5061, , 8088, 8089, 3478, 10000-20000, 49152-49252" echo "[DRY-RUN] Would open 51820/udp (not 51821) if wg-easy was selected" echo "[DRY-RUN] Would offer to create a DigitalOcean Cloud Firewall via doctl" echo "[DRY-RUN] Would reverse-proxy the web admin on the SAME FQDN used for SIP (needed for cert sync)" @@ -487,6 +488,25 @@ EOF sed -i "/CADDY_VOLUME_PLACEHOLDER/d" docker-compose.yml fi + # ── Pick a free port for the web admin ───────────────────────────────────── + # Hardcoding a single number gets fragile fast once several services share + # a host — CrowdSec's own LAPI already collides with 8080 by default (its + # own upstream default, confirmed against its real config.yaml). Scan + # instead: start at 8081 and take the first port nothing is listening on, + # capped so a pathological box can't spin this forever. + local WEB_ADMIN_PORT_VAL=8081 + local _port_scan_limit=$((WEB_ADMIN_PORT_VAL + 100)) + while ss -tlnH "sport = :${WEB_ADMIN_PORT_VAL}" 2>/dev/null | grep -q . \ + && [[ "$WEB_ADMIN_PORT_VAL" -lt "$_port_scan_limit" ]]; do + WEB_ADMIN_PORT_VAL=$((WEB_ADMIN_PORT_VAL + 1)) + done + if [[ "$WEB_ADMIN_PORT_VAL" -ge "$_port_scan_limit" ]]; then + log_warning "No free port found in 8081-${_port_scan_limit} — falling back to 8081 anyway." + WEB_ADMIN_PORT_VAL=8081 + elif [[ "$WEB_ADMIN_PORT_VAL" != 8081 ]]; then + log_info "Port 8081 was already taken — web admin will use ${WEB_ADMIN_PORT_VAL} instead." + fi + # ── .env ────────────────────────────────────────────────────────────────── cat > .env << ENV # ── Domain ──────────────────────────────────────────────────── @@ -512,7 +532,11 @@ HAS_VLANS=n VLAN_SUBNETS= # ── Web admin ───────────────────────────────────────────────── -WEB_ADMIN_PORT=8081 +# Picked automatically at install time (first free port starting at 8081) — +# see WEB_ADMIN_PORT_VAL in services/asterisk-do.sh if this ever needs to +# change again; don't hand-edit without also updating Caddy's Caddyfile and +# both firewall layers to match. +WEB_ADMIN_PORT=${WEB_ADMIN_PORT_VAL} WEB_ADMIN_AUTH_DISABLED=false ENV chmod 600 .env @@ -523,7 +547,7 @@ ENV ufw allow 5060/udp ufw allow 5060/tcp ufw allow 5061/tcp - ufw allow 8081/tcp + ufw allow "${WEB_ADMIN_PORT_VAL}/tcp" ufw allow 8088/tcp ufw allow 8089/tcp ufw allow 3478/udp @@ -555,7 +579,7 @@ ENV "protocol:tcp,ports:5060,address:0.0.0.0/0,address:::/0" "protocol:udp,ports:5060,address:0.0.0.0/0,address:::/0" "protocol:tcp,ports:5061,address:0.0.0.0/0,address:::/0" - "protocol:tcp,ports:8081,address:0.0.0.0/0,address:::/0" + "protocol:tcp,ports:${WEB_ADMIN_PORT_VAL},address:0.0.0.0/0,address:::/0" "protocol:tcp,ports:8088-8089,address:0.0.0.0/0,address:::/0" "protocol:tcp,ports:3478,address:0.0.0.0/0,address:::/0" "protocol:udp,ports:3478,address:0.0.0.0/0,address:::/0" @@ -604,9 +628,9 @@ ENV # matching $DOMAIN_NAME, and SIP TLS would silently stay self-signed. So # there's no separate domain prompt: this always targets $DOMAIN_NAME. if [[ -z "$DOMAIN_NAME" ]]; then - log_info "No FQDN set — web admin stays on http://${PUBLIC_IP:-localhost}:8081 (nothing for Caddy to do)." + log_info "No FQDN set — web admin stays on http://${PUBLIC_IP:-localhost}:${WEB_ADMIN_PORT_VAL} (nothing for Caddy to do)." elif [[ ! -d "$DOCKER_DIR/caddy" ]] && [[ -z "${CADDY_REMOTE_HOST:-}" ]]; then - log_info "Caddy not installed — web admin stays on http://${PUBLIC_IP:-localhost}:8081, SIP TLS stays self-signed." + log_info "Caddy not installed — web admin stays on http://${PUBLIC_IP:-localhost}:${WEB_ADMIN_PORT_VAL}, SIP TLS stays self-signed." else local EXTRA_BLOCK="" if [ -d "$DOCKER_DIR/authelia" ]; then @@ -665,7 +689,7 @@ ENV # Asterisk Web Admin ${DOMAIN_NAME} { - reverse_proxy localhost:8081 + reverse_proxy localhost:${WEB_ADMIN_PORT_VAL} header { Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" @@ -847,7 +871,7 @@ plan for the admin panel. | 22 | TCP | SSH (keep this open or you're locked out) | | 5060 | UDP/TCP | SIP signalling (unencrypted) | | 5061 | TCP | SIP over TLS | -| 8081 | TCP | Easy Asterisk web admin | +| ${WEB_ADMIN_PORT_VAL} | TCP | Easy Asterisk web admin (auto-picked — see \`.env\`) | | 8088/8089 | TCP | Asterisk HTTP/WS (ARI/AMI) | | 3478 | UDP/TCP | TURN/STUN (coturn) | | 10000–20000 | UDP | RTP media streams | @@ -881,7 +905,7 @@ be added later by running \`sudo ./setup.sh \` from the repo. the public firewall; the web UI (51821) is deliberately **not** exposed — reach it via SSH tunnel: \`ssh -L 51821:localhost:51821 user@\`, then browse \`http://localhost:51821\`. A natural next step once it's - installed: restrict the web admin (8081) to the VPN subnet only, on both + installed: restrict the web admin (${WEB_ADMIN_PORT_VAL}) to the VPN subnet only, on both firewall layers, so reconfiguring the PBX requires being on the VPN — done manually, not automatically, since a firewall mistake there can lock you out. @@ -965,7 +989,7 @@ accept it manually). ## Web admin -Access the Easy Asterisk web interface at http://:8081 +Access the Easy Asterisk web interface at http://:${WEB_ADMIN_PORT_VAL} or via your configured reverse-proxy domain. ## Data directories (all inside ~/docker/asterisk-do/, included in backup) @@ -1001,7 +1025,7 @@ MD fi echo " Public IP: ${PUBLIC_IP:-unknown}" echo " SIP port: 5061 (TLS) / 5060 (UDP)" - echo " Web admin: http://${PUBLIC_IP:-localhost}:8081" + echo " Web admin: http://${PUBLIC_IP:-localhost}:${WEB_ADMIN_PORT_VAL}" echo " Manage: docker compose -f $EA_DIR/docker-compose.yml " echo " Script: docker exec -it easy-asterisk-do easy-asterisk --help" if [[ -n "$DOMAIN_NAME" ]] && [[ -d "$DOCKER_DIR/caddy" ]]; then diff --git a/services/asterisk.sh b/services/asterisk.sh index 3e06463..4e6514f 100644 --- a/services/asterisk.sh +++ b/services/asterisk.sh @@ -204,7 +204,8 @@ install_asterisk() { if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would create $EA_DIR with Dockerfile, docker-compose.yml, .env" echo "[DRY-RUN] Would copy/download vendor files from easy-asterisk" - echo "[DRY-RUN] Would open UFW ports: 5060, 5061, 8081, 8088, 8089, 3478, 10000-20000, 49152-49252" + echo "[DRY-RUN] Would scan for a free web admin port starting at 8081 (avoids e.g. CrowdSec's 8080)" + echo "[DRY-RUN] Would open UFW ports: 5060, 5061, , 8088, 8089, 3478, 10000-20000, 49152-49252" return 0 fi @@ -359,6 +360,25 @@ EOF sed -i "/CADDY_VOLUME_PLACEHOLDER/d" docker-compose.yml fi + # ── Pick a free port for the web admin ───────────────────────────────────── + # Hardcoding a single number gets fragile fast once several services share + # a host — CrowdSec's own LAPI already collides with 8080 by default (its + # own upstream default, confirmed against its real config.yaml). Scan + # instead: start at 8081 and take the first port nothing is listening on, + # capped so a pathological box can't spin this forever. + local WEB_ADMIN_PORT_VAL=8081 + local _port_scan_limit=$((WEB_ADMIN_PORT_VAL + 100)) + while ss -tlnH "sport = :${WEB_ADMIN_PORT_VAL}" 2>/dev/null | grep -q . \ + && [[ "$WEB_ADMIN_PORT_VAL" -lt "$_port_scan_limit" ]]; do + WEB_ADMIN_PORT_VAL=$((WEB_ADMIN_PORT_VAL + 1)) + done + if [[ "$WEB_ADMIN_PORT_VAL" -ge "$_port_scan_limit" ]]; then + log_warning "No free port found in 8081-${_port_scan_limit} — falling back to 8081 anyway." + WEB_ADMIN_PORT_VAL=8081 + elif [[ "$WEB_ADMIN_PORT_VAL" != 8081 ]]; then + log_info "Port 8081 was already taken — web admin will use ${WEB_ADMIN_PORT_VAL} instead." + fi + # ── .env ────────────────────────────────────────────────────────────────── cat > .env << ENV # ── Domain ──────────────────────────────────────────────────── @@ -383,7 +403,11 @@ HAS_VLANS=${HAS_VLANS_VAL} VLAN_SUBNETS=${VLAN_SUBNETS_VAL} # ── Web admin ───────────────────────────────────────────────── -WEB_ADMIN_PORT=8081 +# Picked automatically at install time (first free port starting at 8081) — +# see WEB_ADMIN_PORT_VAL in services/asterisk.sh if this ever needs to +# change again; don't hand-edit without also updating Caddy's Caddyfile and +# any firewall rules to match. +WEB_ADMIN_PORT=${WEB_ADMIN_PORT_VAL} WEB_ADMIN_AUTH_DISABLED=false ENV chmod 600 .env @@ -394,7 +418,7 @@ ENV ufw allow 5060/udp ufw allow 5060/tcp ufw allow 5061/tcp - ufw allow 8081/tcp + ufw allow "${WEB_ADMIN_PORT_VAL}/tcp" ufw allow 8088/tcp ufw allow 8089/tcp ufw allow 3478/udp @@ -415,7 +439,7 @@ ENV sed -i "s/^WEB_ADMIN_AUTH_DISABLED=.*/WEB_ADMIN_AUTH_DISABLED=true/" .env fi fi - configure_caddy_for_service "Asterisk Web Admin" "8081" "asterisk" "$EXTRA_BLOCK" + configure_caddy_for_service "Asterisk Web Admin" "${WEB_ADMIN_PORT_VAL}" "asterisk" "$EXTRA_BLOCK" # ── README ──────────────────────────────────────────────────────────────── write_readme "$EA_DIR" << 'MD' @@ -484,7 +508,10 @@ to accept it). ## Web admin Access the Easy Asterisk web interface at http://:8081 -or via your configured reverse-proxy domain. +or via your configured reverse-proxy domain. (8081 is the default; if that +port was already taken by something else on this box, the installer picked +the next free one instead — check WEB_ADMIN_PORT in .env for the actual +value.) ## Data directories (all inside ~/docker/asterisk/, included in backup) @@ -502,7 +529,7 @@ or via your configured reverse-proxy domain. |---------------|----------|----------------------------------| | 5060 | UDP/TCP | SIP signalling (unencrypted) | | 5061 | TCP | SIP over TLS | -| 8081 | TCP | Easy Asterisk web admin | +| 8081 | TCP | Easy Asterisk web admin (default — see .env) | | 8088/8089 | TCP | Asterisk HTTP/WS (ARI/AMI) | | 3478 | UDP/TCP | TURN/STUN (coturn) | | 10000–20000 | UDP | RTP media streams | @@ -530,7 +557,7 @@ MD echo " TURN server: (none — LAN/VPN only)" fi echo " SIP port: 5061 (TLS) / 5060 (UDP)" - echo " Web admin: http://$(hostname -I 2>/dev/null | awk '{print $1}' || echo localhost):8081" + echo " Web admin: http://$(hostname -I 2>/dev/null | awk '{print $1}' || echo localhost):${WEB_ADMIN_PORT_VAL}" echo " Manage: docker compose -f $EA_DIR/docker-compose.yml " echo " Script: docker exec -it easy-asterisk easy-asterisk --help" echo "" From cb8ff3f4f0e5ff0cc22b712cd27a96b779cf9e91 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 17:43:52 +0000 Subject: [PATCH 3/6] Fall back to a Caddy restart when reload fails (admin API is off) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Confirmed on a real deployment: the template Caddyfile ships with "admin off" (deliberate — no local API attack surface), which means `caddy reload` can never work, since it depends on that same admin endpoint. Every Caddyfile-editing code path was silently failing to apply changes as a result — `docker logs caddy` showed "admin endpoint disabled" and the reload command errored, but the Caddyfile edit itself (which doesn't need the admin API) had already succeeded, leaving the running config stale until something else happened to restart the container. Fixed in the two places that actually matter here: lib/common.sh's configure_caddy_for_service (used by asterisk.sh and most other Caddy-fronted services in the full repo) and asterisk-do.sh's own self-contained Caddy block (both the standalone-bootstrap stub and the main path). Each now tries the lightweight reload first — harmless, and still works if a box ever has the admin API enabled — then falls back to `docker restart caddy` if that fails, rather than leaving an edited-but-unapplied Caddyfile. Not fixed: the same duplicated pattern in ~35 other service files that carry their own standalone-bootstrap copy of this logic. Those only matter for the rare single-file standalone execution path for each of those specific services and are unrelated to tonight's actual issue — out of scope here. Verified: full regression run on both asterisk.sh and asterisk-do.sh still completes cleanly end to end. --- lib/common.sh | 14 ++++++++++++-- services/asterisk-do.sh | 21 +++++++++++++++++---- services/asterisk.sh | 10 ++++++++-- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/lib/common.sh b/lib/common.sh index 9a2777b..67265d7 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -512,10 +512,20 @@ CADDY_BLOCK echo " ✓ Configuration added to Caddyfile" echo " Reloading Caddy configuration..." docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile 2>/dev/null || true + # The template Caddyfile ships with "admin off" (security hardening — + # no local API attack surface), so `caddy reload` never works here; + # it depends on that same admin endpoint. Try it anyway in case a + # box has admin enabled, but fall back to a full container restart + # (brief availability gap for everything Caddy fronts, but reliable + # regardless of the admin setting) rather than leaving the change + # sitting unapplied on disk. if docker exec caddy caddy reload --config /etc/caddy/Caddyfile 2>/dev/null; then echo " ✓ $SERVICE_NAME is now accessible at: https://$SERVICE_DOMAIN" + elif docker restart caddy &>/dev/null; then + echo " ✓ Caddy restarted to apply changes (reload API is disabled by default)" + echo " ✓ $SERVICE_NAME should be accessible at: https://$SERVICE_DOMAIN" else - echo " ⚠ Failed to reload Caddy. Check: docker logs caddy" + echo " ⚠ Failed to reload or restart Caddy. Check: docker logs caddy" echo " You can restore from backup: $BACKUP_FILE" fi @@ -533,7 +543,7 @@ CADDY_BLOCK echo " scp $SNIPPET_FILE caddy-host:~/caddy-snippets/" echo " # then on the Caddy machine:" echo " cat ~/caddy-snippets/${DEFAULT_SUBDOMAIN}.caddy >> /path/to/Caddyfile" - echo " docker exec caddy caddy reload --config /etc/caddy/Caddyfile" + echo " docker restart caddy # reload API is disabled by default; a restart is what applies it" echo "" echo " Or rsync all snippets at once:" echo " rsync -av $SNIPPET_DIR/ caddy-host:~/caddy-snippets/" diff --git a/services/asterisk-do.sh b/services/asterisk-do.sh index ff0e6d4..c0c0837 100755 --- a/services/asterisk-do.sh +++ b/services/asterisk-do.sh @@ -145,11 +145,17 @@ CBLOCK printf '%s\n' "$_site_block" >> "$_caddyfile" log_success "Added $_domain to Caddyfile" docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile 2>/dev/null || true + # The template Caddyfile ships with "admin off", so `caddy + # reload` (which needs that same admin API) never actually + # works here. Try it anyway, fall back to a restart. if docker exec caddy caddy reload --config /etc/caddy/Caddyfile 2>/dev/null; then log_success "$_name accessible at: https://$_domain" + elif docker restart caddy &>/dev/null; then + log_success "Caddy restarted to apply changes (reload API is disabled by default)" + log_success "$_name should be accessible at: https://$_domain" else - log_warning "Reload failed — check: docker logs caddy" - log_info "Manual reload: docker exec caddy caddy reload --config /etc/caddy/Caddyfile" + log_warning "Reload/restart failed — check: docker logs caddy" + log_info "Manual fix: docker restart caddy" fi else local _snippet_dir="$DOCKER_DIR/caddy-snippets" @@ -721,11 +727,18 @@ CADDY_BLOCK printf '%s\n' "$_SITE_BLOCK" >> "$_CADDYFILE" log_success "Added ${DOMAIN_NAME} to Caddyfile (backup: $(basename "$_CADDY_BACKUP"))" docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile 2>/dev/null || true + # The template Caddyfile ships with "admin off", so + # `caddy reload` (which needs that same admin API) never + # actually works here. Try it anyway, fall back to a + # restart — confirmed necessary on a real deployment. if docker exec caddy caddy reload --config /etc/caddy/Caddyfile 2>/dev/null; then log_success "Web admin accessible at: https://${DOMAIN_NAME}" + elif docker restart caddy &>/dev/null; then + log_success "Caddy restarted to apply changes (reload API is disabled by default)" + log_success "Web admin should be accessible at: https://${DOMAIN_NAME}" else - log_warning "Reload failed — check: docker logs caddy" - log_info "Manual reload: docker exec caddy caddy reload --config /etc/caddy/Caddyfile" + log_warning "Reload/restart failed — check: docker logs caddy" + log_info "Manual fix: docker restart caddy" fi fi else diff --git a/services/asterisk.sh b/services/asterisk.sh index 4e6514f..d683b98 100644 --- a/services/asterisk.sh +++ b/services/asterisk.sh @@ -143,11 +143,17 @@ CBLOCK printf '%s\n' "$_site_block" >> "$_caddyfile" log_success "Added $_domain to Caddyfile" docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile 2>/dev/null || true + # The template Caddyfile ships with "admin off", so `caddy + # reload` (which needs that same admin API) never actually + # works here. Try it anyway, fall back to a restart. if docker exec caddy caddy reload --config /etc/caddy/Caddyfile 2>/dev/null; then log_success "$_name accessible at: https://$_domain" + elif docker restart caddy &>/dev/null; then + log_success "Caddy restarted to apply changes (reload API is disabled by default)" + log_success "$_name should be accessible at: https://$_domain" else - log_warning "Reload failed — check: docker logs caddy" - log_info "Manual reload: docker exec caddy caddy reload --config /etc/caddy/Caddyfile" + log_warning "Reload/restart failed — check: docker logs caddy" + log_info "Manual fix: docker restart caddy" fi else local _snippet_dir="$DOCKER_DIR/caddy-snippets" From 21f8e0e0960507958f5bbdbef985d836c135dfd2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 01:02:37 +0000 Subject: [PATCH 4/6] Persist Easy Asterisk client exports to the docker directory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The vendor easy-asterisk script hardcodes /root for both export output and its import file listing, but nothing was mounted there — exports were being written to the container's ephemeral filesystem and lost on recreate. Bind-mount ./exports to /root in both asterisk.sh and asterisk-do.sh so exports/imports land under ~/docker//exports on the host. --- services/asterisk-do.sh | 3 ++- services/asterisk.sh | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/services/asterisk-do.sh b/services/asterisk-do.sh index c0c0837..7c6d71f 100755 --- a/services/asterisk-do.sh +++ b/services/asterisk-do.sh @@ -337,7 +337,7 @@ install_asterisk-do() { mkdir -p "$EA_DIR" mkdir -p "$EA_DIR/config/asterisk" "$EA_DIR/config/easy-asterisk" \ - "$EA_DIR/logs" "$EA_DIR/spool" "$EA_DIR/lib" + "$EA_DIR/logs" "$EA_DIR/spool" "$EA_DIR/lib" "$EA_DIR/exports" ensure_docker_dir_ownership "$EA_DIR" cd "$EA_DIR" || return 1 @@ -448,6 +448,7 @@ services: - ./spool:/var/spool/asterisk - ./lib:/var/lib/asterisk - ./easy-asterisk.sh:/usr/local/bin/easy-asterisk:ro + - ./exports:/root CADDY_VOLUME_PLACEHOLDER env_file: .env restart: unless-stopped diff --git a/services/asterisk.sh b/services/asterisk.sh index d683b98..74e7f7a 100644 --- a/services/asterisk.sh +++ b/services/asterisk.sh @@ -217,7 +217,7 @@ install_asterisk() { mkdir -p "$EA_DIR" mkdir -p "$EA_DIR/config/asterisk" "$EA_DIR/config/easy-asterisk" \ - "$EA_DIR/logs" "$EA_DIR/spool" "$EA_DIR/lib" + "$EA_DIR/logs" "$EA_DIR/spool" "$EA_DIR/lib" "$EA_DIR/exports" ensure_docker_dir_ownership "$EA_DIR" cd "$EA_DIR" || return 1 @@ -320,6 +320,7 @@ services: - ./spool:/var/spool/asterisk - ./lib:/var/lib/asterisk - ./easy-asterisk.sh:/usr/local/bin/easy-asterisk:ro + - ./exports:/root CADDY_VOLUME_PLACEHOLDER env_file: .env restart: unless-stopped From 0d719a5cad443200750cd7f5d3d1b24105daed94 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 01:13:51 +0000 Subject: [PATCH 5/6] Add update-in-place mode to asterisk and asterisk-do installers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-running either installer on an existing install used to re-ask every prompt (domain, extras, firewall, Authelia) just to pick up a script fix like the exports mount. Both now detect an existing docker-compose.yml + .env and offer to update in place instead: only vendor files and docker-compose.yml are refreshed and the stack is rebuilt, leaving .env, firewall rules, and Caddy/Authelia config untouched. The vendor-copy and docker-compose.yml generation blocks (previously inline and duplicated between what would have been two near-identical code paths) are factored into per-file helper functions (_asterisk_do_refresh_vendor_files/_asterisk_do_write_compose and _asterisk_refresh_vendor_files/_asterisk_write_compose) so fresh installs and updates share one copy of the logic instead of drifting apart — the same problem that caused the /root export path and the vpn-diagnostics.sh COPY bug to slip through unevenly between the two services in the first place. Names are per-file since setup.sh sources every services/*.sh into one process. --- services/asterisk-do.sh | 275 ++++++++++++++++++++++++---------------- services/asterisk.sh | 199 ++++++++++++++++++----------- 2 files changed, 291 insertions(+), 183 deletions(-) diff --git a/services/asterisk-do.sh b/services/asterisk-do.sh index 7c6d71f..b5b8029 100755 --- a/services/asterisk-do.sh +++ b/services/asterisk-do.sh @@ -203,6 +203,123 @@ fi register_service asterisk-do homelab "Easy Asterisk PBX + coturn, tuned for a public DigitalOcean droplet" 5061 +# ── Shared: vendor file refresh ──────────────────────────────────────────── +# Called from both a fresh install and an "update in place" run, so a single +# copy of this logic stays current for both instead of drifting apart. Must +# be called with $PWD already at $EA_DIR. +_asterisk_do_refresh_vendor_files() { + mkdir -p docker scripts + + local _SELF_DIR_LOCAL + _SELF_DIR_LOCAL="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + local VENDOR_DIR="$_SELF_DIR_LOCAL/../vendor/easy-asterisk" + + if [[ -d "$VENDOR_DIR" ]]; then + log_info "Copying vendor files from $VENDOR_DIR ..." + cp "$VENDOR_DIR/Dockerfile" ./Dockerfile + cp "$VENDOR_DIR/docker/entrypoint.sh" ./docker/entrypoint.sh + cp "$VENDOR_DIR/docker/coturn-entrypoint.sh" ./docker/coturn-entrypoint.sh + cp "$VENDOR_DIR/easy-asterisk-v0.10.0.sh" ./easy-asterisk.sh + cp "$VENDOR_DIR/easy-asterisk-v0.10.0.sh" ./easy-asterisk-v0.10.0.sh + cp "$VENDOR_DIR/scripts/vpn-diagnostics.sh" ./scripts/vpn-diagnostics.sh + cp "$VENDOR_DIR/scripts/dns-whitelist.sh" ./scripts/dns-whitelist.sh + else + log_info "Vendor directory not found — downloading from GitHub ..." + local GH_RAW="https://raw.githubusercontent.com/DeadDork/easy-asterisk/main" + curl -fsSL "$GH_RAW/Dockerfile" -o ./Dockerfile + curl -fsSL "$GH_RAW/docker/entrypoint.sh" -o ./docker/entrypoint.sh + curl -fsSL "$GH_RAW/docker/coturn-entrypoint.sh" -o ./docker/coturn-entrypoint.sh + curl -fsSL "$GH_RAW/easy-asterisk-v0.10.0.sh" -o ./easy-asterisk.sh + curl -fsSL "$GH_RAW/scripts/vpn-diagnostics.sh" -o ./scripts/vpn-diagnostics.sh + curl -fsSL "$GH_RAW/scripts/dns-whitelist.sh" -o ./scripts/dns-whitelist.sh + cp ./easy-asterisk.sh ./easy-asterisk-v0.10.0.sh + fi + + chmod 755 ./easy-asterisk.sh ./easy-asterisk-v0.10.0.sh \ + ./docker/entrypoint.sh ./docker/coturn-entrypoint.sh \ + ./scripts/vpn-diagnostics.sh ./scripts/dns-whitelist.sh + + # Persist security-level logging to a file — vendor's logger.conf only + # sends the "security" level (auth failures, SIP brute-force attempts) to + # the console (Docker stdout), not a file CrowdSec/fail2ban can tail. + if grep -q '^console => notice,warning,error,security$' ./docker/entrypoint.sh; then + sed -i '/^console => notice,warning,error,security$/a full => notice,warning,error,security' \ + ./docker/entrypoint.sh + else + log_warning "entrypoint.sh logger.conf template changed upstream — security events won't be logged to a file. Update the sed patch in this installer." + fi +} + +# ── Shared: docker-compose.yml ───────────────────────────────────────────── +# Same reasoning as above — one copy of the template used by both fresh +# installs and updates. Must be called with $PWD already at $EA_DIR. +_asterisk_do_write_compose() { + cat > docker-compose.yml << 'EOF' +name: asterisk-do + +services: + asterisk: + build: . + container_name: easy-asterisk-do + network_mode: host + depends_on: + coturn: + condition: service_started + volumes: + - ./config/asterisk:/etc/asterisk + - ./config/easy-asterisk:/etc/easy-asterisk + - ./logs:/var/log/asterisk + - ./spool:/var/spool/asterisk + - ./lib:/var/lib/asterisk + - ./easy-asterisk.sh:/usr/local/bin/easy-asterisk:ro + - ./exports:/root +CADDY_VOLUME_PLACEHOLDER + env_file: .env + restart: unless-stopped + healthcheck: + test: ["CMD", "asterisk", "-rx", "core show version"] + interval: 30s + timeout: 5s + retries: 3 + + coturn: + image: coturn/coturn:latest + container_name: easy-asterisk-do-coturn + network_mode: host + user: root + entrypoint: ["/coturn-entrypoint.sh"] + volumes: + - ./docker/coturn-entrypoint.sh:/coturn-entrypoint.sh:ro + env_file: .env + command: + - -n + - --listening-port=${TURN_PORT:-3478} + - --listening-ip=0.0.0.0 + - --fingerprint + - --lt-cred-mech + - --user=${TURN_USERNAME:-easyasterisk}:${TURN_PASSWORD} + - --realm=${DOMAIN_NAME:-localhost} + - --min-port=49152 + - --max-port=49252 + - --no-tls + - --no-dtls + - --no-cli + - --no-multicast-peers + - --log-file=stdout + restart: unless-stopped + +EOF + + # Share Caddy's cert store (read-only) so the entrypoint can auto-sync a + # real Let's Encrypt cert for DOMAIN_NAME instead of falling back to + # self-signed. No-op if Caddy isn't installed on this box. + if [[ -d "$DOCKER_DIR/caddy/data" ]]; then + sed -i "s#CADDY_VOLUME_PLACEHOLDER# - ${DOCKER_DIR}/caddy/data:/caddy-data:ro#" docker-compose.yml + else + sed -i "/CADDY_VOLUME_PLACEHOLDER/d" docker-compose.yml + fi +} + install_asterisk-do() { require_docker || return 1 log_info "Installing Easy Asterisk PBX + coturn (DigitalOcean droplet edition)..." @@ -224,9 +341,53 @@ install_asterisk-do() { echo "[DRY-RUN] Would offer local OR remote Authelia to protect the web admin" echo "[DRY-RUN] Would offer to install CrowdSec if not already present (full repo only)" echo "[DRY-RUN] Would offer to run base setup first if not already done (full repo only)" + echo "[DRY-RUN] Would offer 'update in place' instead of a fresh install if $EA_DIR already exists" return 0 fi + # ── Existing install? Offer update-in-place instead of a full reinstall ─── + # A fresh install re-runs every prompt (domain, extras, DO firewall, + # Authelia). An update only refreshes vendor files + docker-compose.yml — + # picking up fixes like this one — and rebuilds, without touching .env, + # UFW, the Cloud Firewall, or the Caddy/Authelia config already in place. + if [[ -f "$EA_DIR/docker-compose.yml" && -f "$EA_DIR/.env" ]]; then + echo "" + log_info "Existing install found at $EA_DIR." + local UPDATE_INPLACE="" + prompt_yn "Update in place (refresh vendor files + docker-compose.yml, keep your existing domain/firewall/Authelia settings) instead of a full fresh reinstall? (y/n):" "y" UPDATE_INPLACE + if [[ "$UPDATE_INPLACE" =~ ^[Yy]$ ]]; then + mkdir -p "$EA_DIR/config/asterisk" "$EA_DIR/config/easy-asterisk" \ + "$EA_DIR/logs" "$EA_DIR/spool" "$EA_DIR/lib" "$EA_DIR/exports" + ensure_docker_dir_ownership "$EA_DIR" + cd "$EA_DIR" || return 1 + + _asterisk_do_refresh_vendor_files + _asterisk_do_write_compose + + log_info "Rebuilding and restarting containers..." + if docker compose up -d --build --force-recreate; then + log_success "Update complete — vendor files and docker-compose.yml refreshed." + else + log_warning "docker compose up failed — check: docker compose -f $EA_DIR/docker-compose.yml logs" + fi + + local _EXISTING_DOMAIN _EXISTING_PORT + _EXISTING_DOMAIN="$(grep -E '^DOMAIN_NAME=' .env | cut -d= -f2-)" + _EXISTING_PORT="$(grep -E '^WEB_ADMIN_PORT=' .env | cut -d= -f2-)" + echo "" + log_success "Existing .env, UFW rules, Cloud Firewall, and Caddy/Authelia config were left untouched." + if [[ -n "$_EXISTING_DOMAIN" ]]; then + echo " Web admin: https://${_EXISTING_DOMAIN}/" + else + echo " Web admin: http://:${_EXISTING_PORT:-8081}" + fi + echo " Logs: docker compose -f $EA_DIR/docker-compose.yml logs -f" + echo "" + return 0 + fi + log_info "Proceeding with a full fresh reinstall — existing config will be reused where prompts match, everything else re-asked." + fi + # ── Bring in base first, if this is a genuinely fresh box ───────────────── # Naming a service directly (sudo ./setup.sh asterisk-do) skips setup.sh's # own first-run base step — essential packages, SSH key import, disabling @@ -341,53 +502,7 @@ install_asterisk-do() { ensure_docker_dir_ownership "$EA_DIR" cd "$EA_DIR" || return 1 - mkdir -p docker - - # ── Vendor files (shared with services/asterisk.sh — no duplication) ────── - local _SELF_DIR_LOCAL - _SELF_DIR_LOCAL="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - local VENDOR_DIR="$_SELF_DIR_LOCAL/../vendor/easy-asterisk" - - mkdir -p scripts - - if [[ -d "$VENDOR_DIR" ]]; then - log_info "Copying vendor files from $VENDOR_DIR ..." - cp "$VENDOR_DIR/Dockerfile" ./Dockerfile - cp "$VENDOR_DIR/docker/entrypoint.sh" ./docker/entrypoint.sh - cp "$VENDOR_DIR/docker/coturn-entrypoint.sh" ./docker/coturn-entrypoint.sh - cp "$VENDOR_DIR/easy-asterisk-v0.10.0.sh" ./easy-asterisk.sh - cp "$VENDOR_DIR/easy-asterisk-v0.10.0.sh" ./easy-asterisk-v0.10.0.sh - cp "$VENDOR_DIR/scripts/vpn-diagnostics.sh" ./scripts/vpn-diagnostics.sh - cp "$VENDOR_DIR/scripts/dns-whitelist.sh" ./scripts/dns-whitelist.sh - else - log_info "Vendor directory not found — downloading from GitHub ..." - local GH_RAW="https://raw.githubusercontent.com/DeadDork/easy-asterisk/main" - curl -fsSL "$GH_RAW/Dockerfile" -o ./Dockerfile - curl -fsSL "$GH_RAW/docker/entrypoint.sh" -o ./docker/entrypoint.sh - curl -fsSL "$GH_RAW/docker/coturn-entrypoint.sh" -o ./docker/coturn-entrypoint.sh - curl -fsSL "$GH_RAW/easy-asterisk-v0.10.0.sh" -o ./easy-asterisk.sh - curl -fsSL "$GH_RAW/scripts/vpn-diagnostics.sh" -o ./scripts/vpn-diagnostics.sh - curl -fsSL "$GH_RAW/scripts/dns-whitelist.sh" -o ./scripts/dns-whitelist.sh - cp ./easy-asterisk.sh ./easy-asterisk-v0.10.0.sh - fi - - chmod 755 ./easy-asterisk.sh ./easy-asterisk-v0.10.0.sh \ - ./docker/entrypoint.sh ./docker/coturn-entrypoint.sh \ - ./scripts/vpn-diagnostics.sh ./scripts/dns-whitelist.sh - - # ── Persist security-level logging to a file ────────────────────────────── - # Vendor's logger.conf only sends the "security" level (auth failures, SIP - # brute-force attempts) to the console — that's Docker's stdout, not a file - # CrowdSec/fail2ban can tail. Patch our copy of entrypoint.sh (not the - # shared vendor/ source) so it also writes those events to - # /var/log/asterisk/full, which is bind-mounted to $EA_DIR/logs/full — a - # host path services/crowdsec.sh can point its Asterisk acquisition at. - if grep -q '^console => notice,warning,error,security$' ./docker/entrypoint.sh; then - sed -i '/^console => notice,warning,error,security$/a full => notice,warning,error,security' \ - ./docker/entrypoint.sh - else - log_warning "entrypoint.sh logger.conf template changed upstream — security events won't be logged to a file. Update the sed patch in this installer." - fi + _asterisk_do_refresh_vendor_files # ── DigitalOcean droplet detection ──────────────────────────────────────── # A droplet's own public IP/ID are readable, unauthenticated, from the @@ -429,71 +544,7 @@ install_asterisk-do() { # a usable address (the FQDN if set, otherwise the droplet's public IP). local TURN_SERVER_VAL="${DOMAIN_NAME:-$PUBLIC_IP}:3478" - # ── docker-compose.yml ──────────────────────────────────────────────────── - cat > docker-compose.yml << 'EOF' -name: asterisk-do - -services: - asterisk: - build: . - container_name: easy-asterisk-do - network_mode: host - depends_on: - coturn: - condition: service_started - volumes: - - ./config/asterisk:/etc/asterisk - - ./config/easy-asterisk:/etc/easy-asterisk - - ./logs:/var/log/asterisk - - ./spool:/var/spool/asterisk - - ./lib:/var/lib/asterisk - - ./easy-asterisk.sh:/usr/local/bin/easy-asterisk:ro - - ./exports:/root -CADDY_VOLUME_PLACEHOLDER - env_file: .env - restart: unless-stopped - healthcheck: - test: ["CMD", "asterisk", "-rx", "core show version"] - interval: 30s - timeout: 5s - retries: 3 - - coturn: - image: coturn/coturn:latest - container_name: easy-asterisk-do-coturn - network_mode: host - user: root - entrypoint: ["/coturn-entrypoint.sh"] - volumes: - - ./docker/coturn-entrypoint.sh:/coturn-entrypoint.sh:ro - env_file: .env - command: - - -n - - --listening-port=${TURN_PORT:-3478} - - --listening-ip=0.0.0.0 - - --fingerprint - - --lt-cred-mech - - --user=${TURN_USERNAME:-easyasterisk}:${TURN_PASSWORD} - - --realm=${DOMAIN_NAME:-localhost} - - --min-port=49152 - - --max-port=49252 - - --no-tls - - --no-dtls - - --no-cli - - --no-multicast-peers - - --log-file=stdout - restart: unless-stopped - -EOF - - # Share Caddy's cert store (read-only) so the entrypoint can auto-sync a - # real Let's Encrypt cert for DOMAIN_NAME instead of falling back to - # self-signed. No-op if Caddy isn't installed on this box. - if [[ -d "$DOCKER_DIR/caddy/data" ]]; then - sed -i "s#CADDY_VOLUME_PLACEHOLDER# - ${DOCKER_DIR}/caddy/data:/caddy-data:ro#" docker-compose.yml - else - sed -i "/CADDY_VOLUME_PLACEHOLDER/d" docker-compose.yml - fi + _asterisk_do_write_compose # ── Pick a free port for the web admin ───────────────────────────────────── # Hardcoding a single number gets fragile fast once several services share diff --git a/services/asterisk.sh b/services/asterisk.sh index 74e7f7a..66da5b7 100644 --- a/services/asterisk.sh +++ b/services/asterisk.sh @@ -201,35 +201,17 @@ fi register_service asterisk homelab "Easy Asterisk PBX + coturn TURN server (home intercom/VoIP)" 5061 -install_asterisk() { - require_docker || return 1 - log_info "Installing Easy Asterisk PBX + coturn..." +# ── Shared: vendor file refresh ──────────────────────────────────────────── +# Called from both a fresh install and an "update in place" run, so a single +# copy of this logic stays current for both instead of drifting apart. Must +# be called with $PWD already at $EA_DIR. +_asterisk_refresh_vendor_files() { + mkdir -p docker scripts - local EA_DIR="$DOCKER_DIR/asterisk" - - if [ "$DRY_RUN" = true ]; then - echo "[DRY-RUN] Would create $EA_DIR with Dockerfile, docker-compose.yml, .env" - echo "[DRY-RUN] Would copy/download vendor files from easy-asterisk" - echo "[DRY-RUN] Would scan for a free web admin port starting at 8081 (avoids e.g. CrowdSec's 8080)" - echo "[DRY-RUN] Would open UFW ports: 5060, 5061, , 8088, 8089, 3478, 10000-20000, 49152-49252" - return 0 - fi - - mkdir -p "$EA_DIR" - mkdir -p "$EA_DIR/config/asterisk" "$EA_DIR/config/easy-asterisk" \ - "$EA_DIR/logs" "$EA_DIR/spool" "$EA_DIR/lib" "$EA_DIR/exports" - ensure_docker_dir_ownership "$EA_DIR" - cd "$EA_DIR" || return 1 - - mkdir -p docker - - # ── Vendor files ────────────────────────────────────────────────────────── local _SELF_DIR_LOCAL _SELF_DIR_LOCAL="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" local VENDOR_DIR="$_SELF_DIR_LOCAL/../vendor/easy-asterisk" - mkdir -p scripts - if [[ -d "$VENDOR_DIR" ]]; then log_info "Copying vendor files from $VENDOR_DIR ..." cp "$VENDOR_DIR/Dockerfile" ./Dockerfile @@ -254,54 +236,14 @@ install_asterisk() { chmod 755 ./easy-asterisk.sh ./easy-asterisk-v0.10.0.sh \ ./docker/entrypoint.sh ./docker/coturn-entrypoint.sh \ ./scripts/vpn-diagnostics.sh ./scripts/dns-whitelist.sh +} - # ── Networking mode ─────────────────────────────────────────────────────── - echo "" - echo " Networking mode:" - echo " 1) FQDN (recommended) — TLS + TURN relay, every phone connects the" - echo " same way regardless of LAN/VLAN/remote" - echo " 2) LAN-only — no domain, self-signed cert, local network/VPN only" - local HA_NETMODE="" - prompt_text "Choose [1]:" "1" HA_NETMODE - - local DOMAIN_NAME="" - if [[ "$HA_NETMODE" != "2" ]]; then - prompt_text "FQDN (e.g. asterisk.${SITE_DOMAIN:-example.com}) [blank=fall back to LAN-only]:" "" DOMAIN_NAME - [[ -z "$DOMAIN_NAME" ]] && log_warning "No FQDN entered — proceeding in LAN-only mode." - fi - - # ── Local networks / VLANs ──────────────────────────────────────────────── - # Feeds HAS_VLANS/VLAN_SUBNETS into .env, which the entrypoint reads to add - # extra local_net= entries in pjsip.conf so phones on those subnets get - # correct NAT/SDP handling (this is what fixes the "no sound" symptom for - # devices on a VLAN the server isn't itself attached to). - echo "" - echo " Detecting networks this host can see..." - local DETECTED_NETS="" - DETECTED_NETS="$(ip -o -f inet addr show scope global 2>/dev/null \ - | awk '{print $2, $4}' \ - | grep -Ev '^(docker|br-|veth|tun|tap|wg)' \ - | awk '{ split($2,a,"/"); split(a[1],o,"."); print o[1]"."o[2]"."o[3]".0/"a[2] }' \ - | sort -u)" - if [[ -n "$DETECTED_NETS" ]]; then - echo " This host is directly attached to:" - echo "$DETECTED_NETS" | sed 's/^/ /' - fi - echo " Phones on OTHER VLANs (this server usually can't see those directly)" - echo " still need to be listed here so their media is treated as local/trusted." - local VLAN_SUBNETS_VAL="" - prompt_text "VLAN/VPN subnets, space-separated CIDRs [blank=none]:" "" VLAN_SUBNETS_VAL - local HAS_VLANS_VAL="n" - [[ -n "$VLAN_SUBNETS_VAL" ]] && HAS_VLANS_VAL="y" - - # ── Secrets ─────────────────────────────────────────────────────────────── - local TURN_PASSWORD - TURN_PASSWORD="$(generate_password 24)" - - local TURN_SERVER_VAL="" - [[ -n "$DOMAIN_NAME" ]] && TURN_SERVER_VAL="${DOMAIN_NAME}:3478" - - # ── docker-compose.yml ──────────────────────────────────────────────────── +# ── Shared: docker-compose.yml ───────────────────────────────────────────── +# Same reasoning as above — one copy of the template used by both fresh +# installs and updates. Must be called with $PWD already at $EA_DIR. +# HAS_VLANS_VAL/VLAN_SUBNETS_VAL aren't referenced here — they live only in +# .env, which the entrypoint reads at container start. +_asterisk_write_compose() { cat > docker-compose.yml << 'EOF' name: asterisk @@ -366,6 +308,121 @@ EOF else sed -i "/CADDY_VOLUME_PLACEHOLDER/d" docker-compose.yml fi +} + +install_asterisk() { + require_docker || return 1 + log_info "Installing Easy Asterisk PBX + coturn..." + + local EA_DIR="$DOCKER_DIR/asterisk" + + if [ "$DRY_RUN" = true ]; then + echo "[DRY-RUN] Would create $EA_DIR with Dockerfile, docker-compose.yml, .env" + echo "[DRY-RUN] Would copy/download vendor files from easy-asterisk" + echo "[DRY-RUN] Would scan for a free web admin port starting at 8081 (avoids e.g. CrowdSec's 8080)" + echo "[DRY-RUN] Would open UFW ports: 5060, 5061, , 8088, 8089, 3478, 10000-20000, 49152-49252" + echo "[DRY-RUN] Would offer 'update in place' instead of a fresh install if $EA_DIR already exists" + return 0 + fi + + # ── Existing install? Offer update-in-place instead of a full reinstall ─── + # A fresh install re-runs every prompt (networking mode, domain, VLANs, + # Authelia). An update only refreshes vendor files + docker-compose.yml — + # picking up fixes like this one — and rebuilds, without touching .env, + # UFW, or the Caddy/Authelia config already in place. + if [[ -f "$EA_DIR/docker-compose.yml" && -f "$EA_DIR/.env" ]]; then + echo "" + log_info "Existing install found at $EA_DIR." + local UPDATE_INPLACE="" + prompt_yn "Update in place (refresh vendor files + docker-compose.yml, keep your existing domain/VLAN/Authelia settings) instead of a full fresh reinstall? (y/n):" "y" UPDATE_INPLACE + if [[ "$UPDATE_INPLACE" =~ ^[Yy]$ ]]; then + mkdir -p "$EA_DIR/config/asterisk" "$EA_DIR/config/easy-asterisk" \ + "$EA_DIR/logs" "$EA_DIR/spool" "$EA_DIR/lib" "$EA_DIR/exports" + ensure_docker_dir_ownership "$EA_DIR" + cd "$EA_DIR" || return 1 + + _asterisk_refresh_vendor_files + _asterisk_write_compose + + log_info "Rebuilding and restarting containers..." + if docker compose up -d --build --force-recreate; then + log_success "Update complete — vendor files and docker-compose.yml refreshed." + else + log_warning "docker compose up failed — check: docker compose -f $EA_DIR/docker-compose.yml logs" + fi + + local _EXISTING_DOMAIN _EXISTING_PORT + _EXISTING_DOMAIN="$(grep -E '^DOMAIN_NAME=' .env | cut -d= -f2-)" + _EXISTING_PORT="$(grep -E '^WEB_ADMIN_PORT=' .env | cut -d= -f2-)" + echo "" + log_success "Existing .env, UFW rules, and Caddy/Authelia config were left untouched." + if [[ -n "$_EXISTING_DOMAIN" ]]; then + echo " Web admin: https://${_EXISTING_DOMAIN}/" + else + echo " Web admin: http://$(hostname -I 2>/dev/null | awk '{print $1}' || echo localhost):${_EXISTING_PORT:-8081}" + fi + echo " Logs: docker compose -f $EA_DIR/docker-compose.yml logs -f" + echo "" + return 0 + fi + log_info "Proceeding with a full fresh reinstall — existing config will be reused where prompts match, everything else re-asked." + fi + + mkdir -p "$EA_DIR" + mkdir -p "$EA_DIR/config/asterisk" "$EA_DIR/config/easy-asterisk" \ + "$EA_DIR/logs" "$EA_DIR/spool" "$EA_DIR/lib" "$EA_DIR/exports" + ensure_docker_dir_ownership "$EA_DIR" + cd "$EA_DIR" || return 1 + + _asterisk_refresh_vendor_files + + # ── Networking mode ─────────────────────────────────────────────────────── + echo "" + echo " Networking mode:" + echo " 1) FQDN (recommended) — TLS + TURN relay, every phone connects the" + echo " same way regardless of LAN/VLAN/remote" + echo " 2) LAN-only — no domain, self-signed cert, local network/VPN only" + local HA_NETMODE="" + prompt_text "Choose [1]:" "1" HA_NETMODE + + local DOMAIN_NAME="" + if [[ "$HA_NETMODE" != "2" ]]; then + prompt_text "FQDN (e.g. asterisk.${SITE_DOMAIN:-example.com}) [blank=fall back to LAN-only]:" "" DOMAIN_NAME + [[ -z "$DOMAIN_NAME" ]] && log_warning "No FQDN entered — proceeding in LAN-only mode." + fi + + # ── Local networks / VLANs ──────────────────────────────────────────────── + # Feeds HAS_VLANS/VLAN_SUBNETS into .env, which the entrypoint reads to add + # extra local_net= entries in pjsip.conf so phones on those subnets get + # correct NAT/SDP handling (this is what fixes the "no sound" symptom for + # devices on a VLAN the server isn't itself attached to). + echo "" + echo " Detecting networks this host can see..." + local DETECTED_NETS="" + DETECTED_NETS="$(ip -o -f inet addr show scope global 2>/dev/null \ + | awk '{print $2, $4}' \ + | grep -Ev '^(docker|br-|veth|tun|tap|wg)' \ + | awk '{ split($2,a,"/"); split(a[1],o,"."); print o[1]"."o[2]"."o[3]".0/"a[2] }' \ + | sort -u)" + if [[ -n "$DETECTED_NETS" ]]; then + echo " This host is directly attached to:" + echo "$DETECTED_NETS" | sed 's/^/ /' + fi + echo " Phones on OTHER VLANs (this server usually can't see those directly)" + echo " still need to be listed here so their media is treated as local/trusted." + local VLAN_SUBNETS_VAL="" + prompt_text "VLAN/VPN subnets, space-separated CIDRs [blank=none]:" "" VLAN_SUBNETS_VAL + local HAS_VLANS_VAL="n" + [[ -n "$VLAN_SUBNETS_VAL" ]] && HAS_VLANS_VAL="y" + + # ── Secrets ─────────────────────────────────────────────────────────────── + local TURN_PASSWORD + TURN_PASSWORD="$(generate_password 24)" + + local TURN_SERVER_VAL="" + [[ -n "$DOMAIN_NAME" ]] && TURN_SERVER_VAL="${DOMAIN_NAME}:3478" + + _asterisk_write_compose # ── Pick a free port for the web admin ───────────────────────────────────── # Hardcoding a single number gets fragile fast once several services share From 8ee35e1995fa0add8c66ed3d3f1aac1c192745d5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 01:23:22 +0000 Subject: [PATCH 6/6] Add prompt_reinstall_mode helper and document the update/fresh/cancel convention MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the y/n "update in place?" prompt in asterisk.sh/asterisk-do.sh with an explicit r/f/c choice — (r)einstall in place, (f)ull install, (c)ancel — defaulting to cancel on a bare Enter (or Ctrl-D) instead of falling through to a destructive full reinstall. Adds prompt_reinstall_mode to lib/common.sh (plus matching standalone stubs in both asterisk scripts for when they run without the full repo) and documents the convention in CLAUDE.md: any service with a persistent install directory should offer this choice on rerun instead of re-asking every prompt just to pick up a script fix. --- CLAUDE.md | 50 ++++++++++++++++++++++- lib/common.sh | 26 ++++++++++++ services/asterisk-do.sh | 87 +++++++++++++++++++++++++++-------------- services/asterisk.sh | 87 +++++++++++++++++++++++++++-------------- 4 files changed, 189 insertions(+), 61 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 93ea525..e0db519 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -125,9 +125,12 @@ log_error "message" # red [ERROR] ```bash prompt_yn "Question? (y/n):" "default_y_or_n" VARNAME prompt_text "Question? [default]:" "default" VARNAME +prompt_reinstall_mode VARNAME # sets VARNAME to: update | fresh | cancel ``` -When `UNATTENDED=true` both functions skip the prompt and use the default. +When `UNATTENDED=true` all three skip the prompt; `prompt_yn`/`prompt_text` use +their given default, `prompt_reinstall_mode` always resolves to `cancel`. See +**Update vs. fresh reinstall on rerun** below for how to use the latter. ### Pre-flight @@ -262,6 +265,51 @@ fi Put the check early — after any pure-display output (banners, info text) but before the first write. +## Update vs. fresh reinstall on rerun + +Every service should detect an existing install at the top of its +`install_()` — after the `DRY_RUN` check, before any prompts — and +offer `prompt_reinstall_mode` instead of silently re-running every prompt +(domain, secrets, firewall, Authelia, extras...) from scratch. What counts +as "already installed" is service-specific: usually `docker-compose.yml` and +`.env` both existing in the service's `$DOCKER_DIR/` directory. + +```bash +if [[ -f "$DIR/docker-compose.yml" && -f "$DIR/.env" ]]; then + local MODE="" + prompt_reinstall_mode MODE + case "$MODE" in + update) + # Refresh vendor files / config templates, rebuild, done. + # Do NOT touch .env, firewall rules, or Caddy/Authelia config. + ... + return 0 + ;; + cancel) + log_info "Leaving the existing install as-is." + return 0 + ;; + fresh) ;; # fall through to the full install flow below + esac +fi +``` + +`update` should be genuinely non-destructive: refresh whatever the service +vendors or templates (Docker image sources, config templates, +`docker-compose.yml`) and rebuild/restart, but never touch `.env`, firewall +rules, or reverse-proxy/SSO config that's already in place. If the +vendor-copy or `docker-compose.yml`-generation logic is more than a few +lines, factor it into a helper function so the fresh-install path and the +update path share one copy instead of drifting apart — see +`_asterisk_do_refresh_vendor_files`/`_asterisk_do_write_compose` in +`services/asterisk-do.sh` (and their `_asterisk_*` counterparts in +`services/asterisk.sh`) for the reference pattern. + +`cancel` must leave the install completely untouched — it's the default for +a reason (a stray Enter on a service you're just checking on shouldn't +trigger anything). `fresh` runs the exact same flow a first-time install +would, prompts included. + ## .env files and secrets Generate passwords with `generate_password` (never hardcode them). diff --git a/lib/common.sh b/lib/common.sh index 67265d7..91c00df 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -362,6 +362,32 @@ prompt_text() { eval "$varname='${response:-$default}'" } +# Prompt for how to handle a service that's already installed, honoring +# unattended. prompt_reinstall_mode VARNAME +# Sets VARNAME to one of: update | fresh | cancel +# Enter (no input) and any unrecognized input both resolve to "cancel" — this +# guards a destructive full reinstall behind a deliberate keypress instead of +# a stray Enter. Unattended mode always resolves to "cancel" too: never +# silently touch an existing install when nobody's watching the prompt. +prompt_reinstall_mode() { + local varname="$1" response + if [ "$UNATTENDED" = true ]; then + eval "$varname='cancel'" + echo "Existing install detected — leaving it as-is [auto: cancel, unattended mode]" + return + fi + echo " Existing install detected. Choose:" + echo " r) Reinstall in place — refresh vendor files/config, keep existing settings" + echo " f) Full install — re-run every prompt from scratch" + echo " c) Cancel — leave everything as-is [default]" + read -p " Choice [r/f/c, Enter=cancel]: " response + case "${response,,}" in + r) eval "$varname='update'" ;; + f) eval "$varname='fresh'" ;; + *) eval "$varname='cancel'" ;; + esac +} + # ── Per-service README generation ──────────────────────────────────────────── # Write /README.md from stdin (markdown). Every module is encouraged to # call this so each ~/docker// folder is self-documenting. diff --git a/services/asterisk-do.sh b/services/asterisk-do.sh index b5b8029..9379bbf 100755 --- a/services/asterisk-do.sh +++ b/services/asterisk-do.sh @@ -61,6 +61,25 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then eval "$_var='${_r:-$_def}'" } + prompt_reinstall_mode() { + local _var="$1" _r + if [[ "${UNATTENDED:-false}" == "true" ]]; then + eval "$_var='cancel'" + echo "Existing install detected — leaving it as-is [auto: cancel, unattended mode]" + return + fi + echo " Existing install detected. Choose:" + echo " r) Reinstall in place — refresh vendor files/config, keep existing settings" + echo " f) Full install — re-run every prompt from scratch" + echo " c) Cancel — leave everything as-is [default]" + read -r -p " Choice [r/f/c, Enter=cancel]: " _r + case "${_r,,}" in + r) eval "$_var='update'" ;; + f) eval "$_var='fresh'" ;; + *) eval "$_var='cancel'" ;; + esac + } + configure_caddy_for_service() { local _name="$1" _upstream="$2" _subdomain="$3" _extra="${4:-}" local _caddy_dir="$DOCKER_DIR/caddy" @@ -353,39 +372,47 @@ install_asterisk-do() { if [[ -f "$EA_DIR/docker-compose.yml" && -f "$EA_DIR/.env" ]]; then echo "" log_info "Existing install found at $EA_DIR." - local UPDATE_INPLACE="" - prompt_yn "Update in place (refresh vendor files + docker-compose.yml, keep your existing domain/firewall/Authelia settings) instead of a full fresh reinstall? (y/n):" "y" UPDATE_INPLACE - if [[ "$UPDATE_INPLACE" =~ ^[Yy]$ ]]; then - mkdir -p "$EA_DIR/config/asterisk" "$EA_DIR/config/easy-asterisk" \ - "$EA_DIR/logs" "$EA_DIR/spool" "$EA_DIR/lib" "$EA_DIR/exports" - ensure_docker_dir_ownership "$EA_DIR" - cd "$EA_DIR" || return 1 + local REINSTALL_MODE="" + prompt_reinstall_mode REINSTALL_MODE + case "$REINSTALL_MODE" in + update) + mkdir -p "$EA_DIR/config/asterisk" "$EA_DIR/config/easy-asterisk" \ + "$EA_DIR/logs" "$EA_DIR/spool" "$EA_DIR/lib" "$EA_DIR/exports" + ensure_docker_dir_ownership "$EA_DIR" + cd "$EA_DIR" || return 1 - _asterisk_do_refresh_vendor_files - _asterisk_do_write_compose + _asterisk_do_refresh_vendor_files + _asterisk_do_write_compose - log_info "Rebuilding and restarting containers..." - if docker compose up -d --build --force-recreate; then - log_success "Update complete — vendor files and docker-compose.yml refreshed." - else - log_warning "docker compose up failed — check: docker compose -f $EA_DIR/docker-compose.yml logs" - fi + log_info "Rebuilding and restarting containers..." + if docker compose up -d --build --force-recreate; then + log_success "Update complete — vendor files and docker-compose.yml refreshed." + else + log_warning "docker compose up failed — check: docker compose -f $EA_DIR/docker-compose.yml logs" + fi - local _EXISTING_DOMAIN _EXISTING_PORT - _EXISTING_DOMAIN="$(grep -E '^DOMAIN_NAME=' .env | cut -d= -f2-)" - _EXISTING_PORT="$(grep -E '^WEB_ADMIN_PORT=' .env | cut -d= -f2-)" - echo "" - log_success "Existing .env, UFW rules, Cloud Firewall, and Caddy/Authelia config were left untouched." - if [[ -n "$_EXISTING_DOMAIN" ]]; then - echo " Web admin: https://${_EXISTING_DOMAIN}/" - else - echo " Web admin: http://:${_EXISTING_PORT:-8081}" - fi - echo " Logs: docker compose -f $EA_DIR/docker-compose.yml logs -f" - echo "" - return 0 - fi - log_info "Proceeding with a full fresh reinstall — existing config will be reused where prompts match, everything else re-asked." + local _EXISTING_DOMAIN _EXISTING_PORT + _EXISTING_DOMAIN="$(grep -E '^DOMAIN_NAME=' .env | cut -d= -f2-)" + _EXISTING_PORT="$(grep -E '^WEB_ADMIN_PORT=' .env | cut -d= -f2-)" + echo "" + log_success "Existing .env, UFW rules, Cloud Firewall, and Caddy/Authelia config were left untouched." + if [[ -n "$_EXISTING_DOMAIN" ]]; then + echo " Web admin: https://${_EXISTING_DOMAIN}/" + else + echo " Web admin: http://:${_EXISTING_PORT:-8081}" + fi + echo " Logs: docker compose -f $EA_DIR/docker-compose.yml logs -f" + echo "" + return 0 + ;; + cancel) + log_info "Leaving the existing install as-is — nothing changed." + return 0 + ;; + fresh) + log_info "Proceeding with a full fresh reinstall — every prompt below runs from scratch." + ;; + esac fi # ── Bring in base first, if this is a genuinely fresh box ───────────────── diff --git a/services/asterisk.sh b/services/asterisk.sh index 66da5b7..418bd66 100644 --- a/services/asterisk.sh +++ b/services/asterisk.sh @@ -59,6 +59,25 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then eval "$_var='${_r:-$_def}'" } + prompt_reinstall_mode() { + local _var="$1" _r + if [[ "${UNATTENDED:-false}" == "true" ]]; then + eval "$_var='cancel'" + echo "Existing install detected — leaving it as-is [auto: cancel, unattended mode]" + return + fi + echo " Existing install detected. Choose:" + echo " r) Reinstall in place — refresh vendor files/config, keep existing settings" + echo " f) Full install — re-run every prompt from scratch" + echo " c) Cancel — leave everything as-is [default]" + read -r -p " Choice [r/f/c, Enter=cancel]: " _r + case "${_r,,}" in + r) eval "$_var='update'" ;; + f) eval "$_var='fresh'" ;; + *) eval "$_var='cancel'" ;; + esac + } + configure_caddy_for_service() { local _name="$1" _upstream="$2" _subdomain="$3" _extra="${4:-}" local _caddy_dir="$DOCKER_DIR/caddy" @@ -333,39 +352,47 @@ install_asterisk() { if [[ -f "$EA_DIR/docker-compose.yml" && -f "$EA_DIR/.env" ]]; then echo "" log_info "Existing install found at $EA_DIR." - local UPDATE_INPLACE="" - prompt_yn "Update in place (refresh vendor files + docker-compose.yml, keep your existing domain/VLAN/Authelia settings) instead of a full fresh reinstall? (y/n):" "y" UPDATE_INPLACE - if [[ "$UPDATE_INPLACE" =~ ^[Yy]$ ]]; then - mkdir -p "$EA_DIR/config/asterisk" "$EA_DIR/config/easy-asterisk" \ - "$EA_DIR/logs" "$EA_DIR/spool" "$EA_DIR/lib" "$EA_DIR/exports" - ensure_docker_dir_ownership "$EA_DIR" - cd "$EA_DIR" || return 1 + local REINSTALL_MODE="" + prompt_reinstall_mode REINSTALL_MODE + case "$REINSTALL_MODE" in + update) + mkdir -p "$EA_DIR/config/asterisk" "$EA_DIR/config/easy-asterisk" \ + "$EA_DIR/logs" "$EA_DIR/spool" "$EA_DIR/lib" "$EA_DIR/exports" + ensure_docker_dir_ownership "$EA_DIR" + cd "$EA_DIR" || return 1 - _asterisk_refresh_vendor_files - _asterisk_write_compose + _asterisk_refresh_vendor_files + _asterisk_write_compose - log_info "Rebuilding and restarting containers..." - if docker compose up -d --build --force-recreate; then - log_success "Update complete — vendor files and docker-compose.yml refreshed." - else - log_warning "docker compose up failed — check: docker compose -f $EA_DIR/docker-compose.yml logs" - fi + log_info "Rebuilding and restarting containers..." + if docker compose up -d --build --force-recreate; then + log_success "Update complete — vendor files and docker-compose.yml refreshed." + else + log_warning "docker compose up failed — check: docker compose -f $EA_DIR/docker-compose.yml logs" + fi - local _EXISTING_DOMAIN _EXISTING_PORT - _EXISTING_DOMAIN="$(grep -E '^DOMAIN_NAME=' .env | cut -d= -f2-)" - _EXISTING_PORT="$(grep -E '^WEB_ADMIN_PORT=' .env | cut -d= -f2-)" - echo "" - log_success "Existing .env, UFW rules, and Caddy/Authelia config were left untouched." - if [[ -n "$_EXISTING_DOMAIN" ]]; then - echo " Web admin: https://${_EXISTING_DOMAIN}/" - else - echo " Web admin: http://$(hostname -I 2>/dev/null | awk '{print $1}' || echo localhost):${_EXISTING_PORT:-8081}" - fi - echo " Logs: docker compose -f $EA_DIR/docker-compose.yml logs -f" - echo "" - return 0 - fi - log_info "Proceeding with a full fresh reinstall — existing config will be reused where prompts match, everything else re-asked." + local _EXISTING_DOMAIN _EXISTING_PORT + _EXISTING_DOMAIN="$(grep -E '^DOMAIN_NAME=' .env | cut -d= -f2-)" + _EXISTING_PORT="$(grep -E '^WEB_ADMIN_PORT=' .env | cut -d= -f2-)" + echo "" + log_success "Existing .env, UFW rules, and Caddy/Authelia config were left untouched." + if [[ -n "$_EXISTING_DOMAIN" ]]; then + echo " Web admin: https://${_EXISTING_DOMAIN}/" + else + echo " Web admin: http://$(hostname -I 2>/dev/null | awk '{print $1}' || echo localhost):${_EXISTING_PORT:-8081}" + fi + echo " Logs: docker compose -f $EA_DIR/docker-compose.yml logs -f" + echo "" + return 0 + ;; + cancel) + log_info "Leaving the existing install as-is — nothing changed." + return 0 + ;; + fresh) + log_info "Proceeding with a full fresh reinstall — every prompt below runs from scratch." + ;; + esac fi mkdir -p "$EA_DIR"