From 60238814f456888d0c55fdfce3aa18c7ec6590e5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 16 Aug 2026 00:46:09 +0000 Subject: [PATCH 1/4] dr_bringup: support restoring from the offsite mirror on a brand-new box MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is what's actually needed for a DigitalOcean -> IONOS Asterisk migration (item #1 of the user's original 4-item list): move the whole stack to different hardware entirely, not restore onto the box the offsite mirror already targets. dr_bringup_kopia.sh only ever scanned DEST_NAMES for restorable snapshots — those are always local filesystem Kopia repos (services/backup.sh creates them with `repository create filesystem --path=...`), meaning they only exist on whichever box originally ran the backup. On a genuinely new box, every one of them fails to connect and there's nothing left to restore from — the script's own header comment only covered the case where "the spare box IS the box the primary's mirror targets" (i.e. already holds a copy of the repo data), not a fresh, unrelated box. Fix: also try REMOTE_TYPE/REMOTE_ARGS (the offsite Backblaze/S3 mirror, if configured) as a same-shaped destination named "offsite", reusing DEST_default_PASSWORD since sync-to always mirrors that exact same encrypted repo. Connects once into a fresh local config file scoped to this DR run, then folds into the existing per-destination scan/restore loop unchanged — "offsite" just becomes another entry in _DEST_ARR. Documented the actual migration workflow in the header comment, including the BACKUP_CONF override so copying the old box's backup.conf over doesn't clobber the new box's own freshly-configured one. Verified against the real script (not a reimplementation) with mocked kopia/docker binaries and a crafted backup.conf, covering: local dest unreachable + offsite connects successfully (the actual migration shape) with correct service/path discovery; a real (non---list) restore run confirmed it selects the latest of multiple snapshots by startTime and issues the correct `kopia restore ` call; offsite connect failing (bad REMOTE_ARGS) warns and degrades to "no restorable sources found" instead of crashing; REMOTE_TYPE=none skips the new code path entirely with no behavior change (regression check against the pre-existing local-only case). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01H4k6J1qXXyYxhGEgnJaMvn --- extras/dr_bringup_kopia.sh | 52 +++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/extras/dr_bringup_kopia.sh b/extras/dr_bringup_kopia.sh index 48ca048..45f781b 100644 --- a/extras/dr_bringup_kopia.sh +++ b/extras/dr_bringup_kopia.sh @@ -15,13 +15,29 @@ # sudo ./dr_bringup.sh --dry-run show what would happen, touch nothing # sudo ./dr_bringup.sh --no-start restore only, skip docker compose up -d # -# Reads backup.conf from the same directory. On the spare box this file -# won't exist yet on its own — copy it over from the primary box first (it -# holds the repository paths/passwords needed to connect): +# Reads backup.conf from the same directory (or wherever $BACKUP_CONF +# points — see below). On the spare box this file won't exist yet on its +# own — copy it over from the primary box first (it holds the repository +# paths/passwords needed to connect): # scp primary:~/docker/backup/backup.conf ~/docker/backup/backup.conf # If the destination repo is a local path shared with the primary (e.g. the # spare box IS the box the primary's REMOTE_TYPE=sftp mirror targets), -# nothing else is needed — the repo data is already there. +# nothing else is needed — the repo data is already there. Otherwise this +# also tries REMOTE_TYPE/REMOTE_ARGS (the offsite Backblaze/S3 mirror, if +# configured) as a restore source — the only one reachable from a genuinely +# new box (e.g. migrating to different hardware/a different provider). +# +# Migrating to a brand-new box that will keep running this repo's own +# backup.sh going forward: don't just scp the old box's backup.conf over +# the new box's own (that would replace its freshly-configured local repo +# with the old box's, which doesn't exist on this box). Instead: +# sudo ./setup.sh backup # sets up this box's own backups, +# # and deploys this script +# scp old-box:~/docker/backup/backup.conf ~/docker/backup/backup-source.conf +# sudo BACKUP_CONF=~/docker/backup/backup-source.conf \ +# ~/docker/backup/dr_bringup.sh --list # preview first +# sudo BACKUP_CONF=~/docker/backup/backup-source.conf \ +# ~/docker/backup/dr_bringup.sh # then actually restore set -uo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -87,6 +103,34 @@ k_for() { read -ra _DEST_ARR <<< "$DEST_NAMES" declare -a SRC_PATH_LIST=() SRC_DEST_LIST=() SRC_SNAP_LIST=() +# DEST_NAMES entries are always LOCAL filesystem repos (services/backup.sh +# creates them with `repository create filesystem --path=...`) — meaning +# they only exist on whichever box originally ran the backup. On a genuinely +# new box (migrating to different hardware/provider, not restoring onto the +# same DR-spare the offsite mirror already targets), none of them will +# connect, and without this block there would be nothing left to restore +# from at all. REMOTE_TYPE/REMOTE_ARGS (the offsite Backblaze/S3 mirror) is +# reachable from anywhere, so try it too, as a same-shaped destination named +# "offsite" — reusing DEST_default_PASSWORD since sync-to always mirrors +# that exact same encrypted repo, so there's no separate password to ask +# for. Connect once into a fresh local config file scoped to this DR run +# (no pre-existing config for it the way the local DEST_NAMES entries have). +if [ -n "${REMOTE_TYPE:-}" ] && [ "$REMOTE_TYPE" != "none" ] && [ -n "${DEST_default_PASSWORD:-}" ]; then + OFFSITE_CFG="/etc/kopia-backup/dr-offsite.config" + mkdir -p "$(dirname "$OFFSITE_CFG")" /var/cache/kopia-backup + # shellcheck disable=SC2086 + if env KOPIA_PASSWORD="$DEST_default_PASSWORD" "$KOPIA" --config-file="$OFFSITE_CFG" repository status >/dev/null 2>&1 \ + || env KOPIA_PASSWORD="$DEST_default_PASSWORD" "$KOPIA" --config-file="$OFFSITE_CFG" \ + --cache-directory=/var/cache/kopia-backup repository connect $REMOTE_TYPE $REMOTE_ARGS >/dev/null 2>&1; then + DEST_offsite_CONFIG="$OFFSITE_CFG" + DEST_offsite_PASSWORD="$DEST_default_PASSWORD" + _DEST_ARR+=("offsite") + info "Connected to the offsite mirror ($REMOTE_TYPE) as an additional restore source ('offsite')." + else + warn "Could not connect to the offsite mirror ($REMOTE_TYPE) as a restore source — check REMOTE_TYPE/REMOTE_ARGS in $CONF." + fi +fi + for dest in "${_DEST_ARR[@]}"; do if ! k_for "$dest" repository status >/dev/null 2>&1; then warn "Cannot connect to destination '$dest' — skipping." From 6dc4f0d6c6dc45c18311877f079bfac7a3479b29 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 16 Aug 2026 01:02:28 +0000 Subject: [PATCH 2/4] asterisk-standalone-backup.sh: fix external IP on cross-box restore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User confirmed via a live pjsip.conf on their actual DO box: both external_media_address and external_signaling_address are literal IPs, written by easy-asterisk at first container start (not something this repo's install script controls directly). A straight restore of a backup archive from a DIFFERENT box onto the new IONOS box would leave the OLD DigitalOcean IP baked into pjsip.conf — dialplan and PJSIP device credentials would come back fine, but RTP media (and likely SIP signaling/registration) would stay broken, silently, since nothing in the restore path previously touched these values. Fix: after extracting the archive, `restore` reads the archive's own external_signaling_address as "old IP", detects this host's actual current public IP (same DO-metadata -> ifconfig.me -> hostname -I fallback chain services/asterisk.sh's own install already uses), and if they differ, rewrites every occurrence across config/ and .env (fixed-string match, not a regex, so the IP's dots can't be misinterpreted). Deliberately does NOT touch spool/, logs/, or lib/ — those hold voicemail messages and call recordings, and a blind text substitution across binary audio would corrupt it. A restore onto the same host (e.g. rolling back a bad config change, no IP change) leaves every file untouched — the check only fires on an actual mismatch. Verified against the real generated script (extracted verbatim from the heredoc, not a reimplementation) with a full mock backup/restore cycle: built a fixture archive with pjsip.conf's three transport blocks (udp/tcp/tls) and .env's TURN_SERVER all hardcoded to a fake "old box" IP, plus a fake binary voicemail file; restored it onto a mocked "new box" with a different detected IP via a stubbed curl. Confirmed every occurrence in both pjsip.conf and .env was correctly rewritten to the new IP, and confirmed via byte-for-byte comparison that the binary voicemail file was completely untouched. Separately verified the same-IP case (mocked curl returning the archive's own IP) makes no changes at all, matching a same-host config rollback. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01H4k6J1qXXyYxhGEgnJaMvn --- services/asterisk.sh | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/services/asterisk.sh b/services/asterisk.sh index 701f83f..50248be 100644 --- a/services/asterisk.sh +++ b/services/asterisk.sh @@ -591,6 +591,37 @@ case "$cmd" in exit 1 fi + # pjsip.conf bakes external_media_address/external_signaling_address + # in as literal IPs (easy-asterisk writes them at first container + # start, not this repo) — restoring an archive from a DIFFERENT box + # verbatim leaves the OLD box's IP in place, breaking RTP media (and + # likely SIP signaling) on THIS box even though the dialplan itself + # comes back fine. Detect the mismatch and patch it automatically — + # this is the whole reason "restore" onto a new host exists, so a + # stale IP left behind would defeat the point every single time. + PJSIP_CONF="$HERE/config/asterisk/pjsip.conf" + if [ -f "$PJSIP_CONF" ]; then + OLD_EXT_IP="$(grep -m1 '^external_signaling_address=' "$PJSIP_CONF" | cut -d= -f2)" + NEW_EXT_IP="$(curl -fsS --max-time 2 http://169.254.169.254/metadata/v1/interfaces/public/0/ipv4/address 2>/dev/null || true)" + [ -z "$NEW_EXT_IP" ] && NEW_EXT_IP="$(curl -fsS --max-time 3 https://ifconfig.me 2>/dev/null || true)" + [ -z "$NEW_EXT_IP" ] && NEW_EXT_IP="$(hostname -I 2>/dev/null | awk '{print $1}')" + + if [ -n "$OLD_EXT_IP" ] && [ -n "$NEW_EXT_IP" ] && [ "$OLD_EXT_IP" != "$NEW_EXT_IP" ]; then + echo "This archive's SIP config was for a different box's public IP" + echo "($OLD_EXT_IP) — this box's is $NEW_EXT_IP. Updating" + echo "external_media_address/external_signaling_address so RTP/SIP work here." + # Fixed-string match/replace, restricted to config + .env — + # never spool/logs/lib, which hold voicemail/recording audio + # that a text substitution would corrupt. + ESC_OLD_IP="${OLD_EXT_IP//./\\.}" + grep -rlF "$OLD_EXT_IP" "$HERE/config" "$HERE/.env" 2>/dev/null | while read -r _f; do + sed -i "s/$ESC_OLD_IP/$NEW_EXT_IP/g" "$_f" + done + echo "Updated. If this PBX uses VLANs/extra subnets, re-check them:" + echo " docker exec -it $CONTAINER easy-asterisk # Server Settings -> Configure VLAN/VPN Subnets" + fi + fi + chown -R "$ACTUAL_USER:$ACTUAL_USER" "$HERE" if [ "$WAS_RUNNING" = true ]; then @@ -1705,6 +1736,19 @@ To migrate to a new host: run \`backup\` on the old one, copy the archive over, then run \`restore\` after a fresh \`sudo ./setup.sh asterisk\` install (or directly into an empty \`${EA_DIR}\`) on the new host. +\`restore\` also detects and fixes the one thing that doesn't travel between +boxes on its own: \`pjsip.conf\`'s \`external_media_address\`/ +\`external_signaling_address\` are literal IPs, baked in by easy-asterisk at +first container start — restoring an archive from a different box verbatim +would otherwise leave the OLD box's IP in place, breaking RTP media (and +likely SIP registration) even though the dialplan itself comes back fine. +\`restore\` compares the archive's IP against this host's own (same +DO-metadata → ifconfig.me → \`hostname -I\` detection this script's own +install uses) and, if they differ, rewrites every occurrence across +\`config/\` and \`.env\` — never \`spool/\`/\`logs/\`/\`lib/\`, which hold voicemail +and recording audio a text substitution would corrupt. A same-host restore +(rolling back a config mistake, no IP change) leaves everything untouched. + ## VLANs / other subnets \`.env\` → \`HAS_VLANS\`/\`VLAN_SUBNETS\` lists extra networks (space-separated From be684e114dd29e2003062537df3c40ab1d2c122f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 16 Aug 2026 02:34:30 +0000 Subject: [PATCH 3/4] asterisk-standalone-backup.sh: fix cross-layout restore (DO -> non-DO) Live blocker: user's actual migration is DigitalOcean droplet (asterisk-digital-ocean/, container easy-asterisk-do) -> IONOS (plain asterisk/, container easy-asterisk) -- exactly the case this script didn't handle. The archive's own top-level directory name and docker-compose.yml reflect whichever layout produced it (_asterisk_resolve_layout's two known layouts). The previous restore extracted straight into $PARENT_DIR, which recreates whatever name is baked into the archive -- restoring a droplet archive onto a fresh non-droplet install would land the data at a *second*, wrongly-named directory (asterisk-digital-ocean) alongside the freshly-installed one it was meant to replace, with docker-compose.yml still naming the old project/container(s). Every service that resolves Asterisk's layout by directory/container name (security-dashboard.sh, pstn-trunk.sh, CrowdSec's Asterisk acquisition, Caddy) would get confused by having two candidate layouts on disk, one of them stale and half-wired. Fix: extract into a scratch staging directory first. If the archived docker-compose.yml's container_name differs from this run's own $CONTAINER (baked in at generation time, so always correct for whichever layout THIS box's install actually uses), rewrite the project name, container name, and coturn container name in place (coturn's is always "$CONTAINER-coturn" on both known layouts, so no lookup table needed) before the data ever lands at $HERE -- never lets the archive's own naming leak through. A same-layout restore (most common case, or two droplet boxes, or two plain boxes) detects no mismatch and skips the rewrite entirely, unchanged from before. Verified against the real generated script (extracted from the heredoc, not reimplemented): a droplet-flavored archive restored onto a fresh plain-layout box lands at the correct single directory with no stray second directory, and docker-compose.yml's name/container_name/ coturn container_name all correctly rewritten to the plain layout (confirmed by diffing the actual restored file, not just checking for absence of errors); a same-layout restore (droplet archive onto a droplet box) confirmed to skip the rewrite entirely; the pre-existing external-IP patch (previous commit) still fires correctly stacked on top of the layout fix; and the extraction-failure rollback path (a corrupt/unreadable archive) still restores the pre-restore install untouched, verified via a marker file surviving the rollback. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01H4k6J1qXXyYxhGEgnJaMvn --- services/asterisk.sh | 58 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/services/asterisk.sh b/services/asterisk.sh index 50248be..d06c9a4 100644 --- a/services/asterisk.sh +++ b/services/asterisk.sh @@ -582,11 +582,54 @@ case "$cmd" in mv "$HERE" "$ASIDE" echo "Extracting $ARCHIVE -> $PARENT_DIR ..." - if tar -xzf "$ARCHIVE" -C "$PARENT_DIR"; then + # Extract into a scratch staging dir first rather than straight into + # $PARENT_DIR — an archive's own top-level directory name reflects + # whichever layout produced it (see _asterisk_resolve_layout: plain + # "asterisk"/"easy-asterisk" vs droplet "asterisk-digital-ocean"/ + # "easy-asterisk-do"), which can differ from THIS box's layout (e.g. + # restoring a droplet backup onto a fresh non-droplet IONOS install). + # Landing it under the archive's own name instead of $HERE would + # leave two Asterisk directories on disk and confuse every service + # that resolves the layout by directory/container name (security- + # dashboard, pstn-trunk, CrowdSec's Asterisk acquisition, Caddy). + STAGING="$(mktemp -d)" + if tar -xzf "$ARCHIVE" -C "$STAGING"; then + EXTRACTED_DIR="$(find "$STAGING" -mindepth 1 -maxdepth 1 -type d | head -1)" + if [ -z "$EXTRACTED_DIR" ]; then + echo "Archive didn't contain a top-level directory — rolling back." + rm -rf "$STAGING" + mv "$ASIDE" "$HERE" + exit 1 + fi + + # If the archive came from the other layout, its docker- + # compose.yml still names the OLD project/container(s) — fix + # those to match THIS box's own layout before it lands at $HERE. + # $CONTAINER is this run's own correct value (baked in at + # generation time); everything else derives from it the same + # way _asterisk_resolve_layout's two known layouts do. + ARCHIVED_COMPOSE="$EXTRACTED_DIR/docker-compose.yml" + if [ -f "$ARCHIVED_COMPOSE" ]; then + ARCHIVED_CONTAINER="$(grep -m1 'container_name:' "$ARCHIVED_COMPOSE" | awk '{print $2}')" + if [ -n "$ARCHIVED_CONTAINER" ] && [ "$ARCHIVED_CONTAINER" != "$CONTAINER" ]; then + echo "Archive is from a different Asterisk layout ($ARCHIVED_CONTAINER) than" + echo "this box ($CONTAINER) — updating docker-compose.yml to match this box." + ARCHIVED_COTURN="${ARCHIVED_CONTAINER}-coturn" + NEW_COTURN="${CONTAINER}-coturn" + sed -i "s/name: ${ARCHIVED_CONTAINER#easy-}\$/name: ${CONTAINER#easy-}/; \ + s/container_name: ${ARCHIVED_CONTAINER}\$/container_name: ${CONTAINER}/; \ + s/container_name: ${ARCHIVED_COTURN}\$/container_name: ${NEW_COTURN}/" \ + "$ARCHIVED_COMPOSE" + fi + fi + + rm -rf "${HERE:?}" + mv "$EXTRACTED_DIR" "$HERE" + rm -rf "$STAGING" echo "Extracted." else echo "Extraction failed — rolling back to the pre-restore install." - rm -rf "${PARENT_DIR:?}/$BASE_NAME" + rm -rf "$STAGING" mv "$ASIDE" "$HERE" exit 1 fi @@ -1749,6 +1792,17 @@ install uses) and, if they differ, rewrites every occurrence across and recording audio a text substitution would corrupt. A same-host restore (rolling back a config mistake, no IP change) leaves everything untouched. +\`restore\` also handles moving between the two layouts this repo supports +(see \`_asterisk_resolve_layout\` — plain \`asterisk\`/\`easy-asterisk\` vs. +DigitalOcean-droplet \`asterisk-digital-ocean\`/\`easy-asterisk-do\`). An +archive's own directory name and \`docker-compose.yml\` reflect whichever +layout produced it; restoring a droplet backup onto a fresh non-droplet +install (or vice versa) rewrites \`docker-compose.yml\`'s project/container/ +coturn-container names to match THIS box's layout and lands the data at +this box's own directory — never leaving a second, wrongly-named directory +behind that would confuse every service that resolves Asterisk's layout +(Security Dashboard, PSTN trunk, CrowdSec's Asterisk acquisition, Caddy). + ## VLANs / other subnets \`.env\` → \`HAS_VLANS\`/\`VLAN_SUBNETS\` lists extra networks (space-separated From 930233cc7fcc433f79a3b199bfa46b872e93d7ec Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 16 Aug 2026 19:50:23 +0000 Subject: [PATCH 4/4] Fix app_voicemail module-load collision via modules.conf Live-discovered bug, present on every install using this script, not specific to any one box or extension: the easy-asterisk image ships app_voicemail.so, app_voicemail_imap.so, and app_voicemail_odbc.so all autoloading by default -- three alternative storage backends for the SAME application (VoiceMail, VoiceMailMain, VMAuthenticate, VoiceMailPlayMsg, VMSayName, the VM_INFO function, several AMI actions), which collide registering those names against each other on every single Asterisk start. This box's own container log showed the exact signature on every restart: "Already have an application 'VoiceMail'" (and every sibling) followed by "app_voicemail.c:15897 load_module: Failure registering applications, functions or tests" -- app_voicemail never actually finished loading. Confirmed against Asterisk's own documentation this session rather than assumed: this is a known multi-backend conflict ("administrators should enable only one module at a time"), not something specific to this repo's config. Fix: new _asterisk_write_modules_conf, called from both the fresh- install and update paths (matching voicemail-dialplan.conf's own call-site pattern) alongside the other config/asterisk files, all sharing the already-bind-mounted ./config/asterisk:/etc/asterisk volume -- no new mount needed. noloads the two backends this repo never configures (no IMAP/ODBC settings are ever written anywhere in this script), leaving only the plain file-based app_voicemail.so (the one voicemail.conf's [default] mailboxes actually target) to load cleanly. Regenerated on every install/update, unlike voicemail.conf, since modules.conf carries no per-install state of its own -- consistent with how messaging-dialplan.conf/voicemail- dialplan.conf are already handled, and added to their same chmod 644 line. Requires a container restart to take effect on an existing install (re-run `sudo ./setup.sh asterisk` -> Update, which regenerates this file, then restart the container once). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01H4k6J1qXXyYxhGEgnJaMvn --- services/asterisk.sh | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/services/asterisk.sh b/services/asterisk.sh index d06c9a4..29cf2ba 100644 --- a/services/asterisk.sh +++ b/services/asterisk.sh @@ -1063,6 +1063,41 @@ operator=no EOF } +# The easy-asterisk image ships app_voicemail.so, app_voicemail_imap.so, and +# app_voicemail_odbc.so all autoloading by default — three alternative +# storage backends for the SAME application (VoiceMail, VoiceMailMain, +# VM_INFO, etc.), which collide registering those names against each other +# on every single Asterisk start. Confirmed live: this box's own log on +# every restart shows "Already have an application 'VoiceMail'" (and every +# sibling app/function) followed by "app_voicemail.c:15897 load_module: +# Failure registering applications, functions or tests" — app_voicemail +# never actually finishes loading, on every install using this script, not +# something specific to one box or one extension. This repo only ever uses +# the plain file-based backend (voicemail.conf's [default] mailboxes, +# written above), so noload the other two — modules.conf lands in the same +# already-bind-mounted config/asterisk directory, no new volume needed. +# Regenerated on every install/update (unlike voicemail.conf) since it +# carries no per-install state of its own. +_asterisk_write_modules_conf() { + local FILE="$1" + cat > "$FILE" << 'EOF' +; services/asterisk.sh — regenerated on every install/update; edit there, +; not here directly. +; +; app_voicemail_imap/_odbc are alternative voicemail storage backends this +; repo never configures (no IMAP/ODBC settings are ever written) — loading +; them alongside the plain file-based app_voicemail.so makes all three +; collide registering the same application/function names, and +; app_voicemail.so is the one that ends up losing that race and failing to +; load at all. noload the two unused backends so the one actually +; configured (file-based, via voicemail.conf) loads cleanly. +[modules] +autoload=yes +noload => app_voicemail_imap.so +noload => app_voicemail_odbc.so +EOF +} + # Same anchor-position reasoning as _asterisk_patch_messaging_vendor_files: # Same anchor as messaging's own patch (see the comment above # _asterisk_patch_messaging_vendor_files for the live-confirmed reasoning): @@ -2021,9 +2056,10 @@ install_asterisk() { _asterisk_patch_voicemail_vendor_files "$EA_DIR" _asterisk_write_voicemail_dialplan "$EA_DIR/config/asterisk/voicemail-dialplan.conf" _asterisk_write_voicemail_conf "$EA_DIR/config/asterisk/voicemail.conf" + _asterisk_write_modules_conf "$EA_DIR/config/asterisk/modules.conf" _asterisk_ensure_live_voicemail_include "$EA_DIR" "$CONTAINER" ensure_docker_dir_ownership "$EA_DIR/config/asterisk" - chmod 644 "$EA_DIR/config/asterisk/messaging-dialplan.conf" "$EA_DIR/config/asterisk/voicemail-dialplan.conf" + chmod 644 "$EA_DIR/config/asterisk/messaging-dialplan.conf" "$EA_DIR/config/asterisk/voicemail-dialplan.conf" "$EA_DIR/config/asterisk/modules.conf" log_info "Rebuilding and restarting containers..." if docker compose up -d --build --force-recreate; then @@ -2116,9 +2152,10 @@ install_asterisk() { _asterisk_patch_voicemail_vendor_files "$EA_DIR" _asterisk_write_voicemail_dialplan "$EA_DIR/config/asterisk/voicemail-dialplan.conf" _asterisk_write_voicemail_conf "$EA_DIR/config/asterisk/voicemail.conf" + _asterisk_write_modules_conf "$EA_DIR/config/asterisk/modules.conf" _asterisk_ensure_live_voicemail_include "$EA_DIR" "$CONTAINER" ensure_docker_dir_ownership "$EA_DIR/config/asterisk" - chmod 644 "$EA_DIR/config/asterisk/messaging-dialplan.conf" "$EA_DIR/config/asterisk/voicemail-dialplan.conf" + chmod 644 "$EA_DIR/config/asterisk/messaging-dialplan.conf" "$EA_DIR/config/asterisk/voicemail-dialplan.conf" "$EA_DIR/config/asterisk/modules.conf" # ── Domain / networking mode ────────────────────────────────────────────── # A public cloud box is always reachable from anywhere, so there's no