Merge pull request #343 from outis1one/claude/ionos-script-integration-x32ofw

Claude/ionos script integration x32ofw
This commit is contained in:
Outis
2026-08-16 15:51:34 -04:00
committed by GitHub
2 changed files with 187 additions and 8 deletions
+48 -4
View File
@@ -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."
+139 -4
View File
@@ -582,15 +582,89 @@ 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
# 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
@@ -989,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):
@@ -1705,6 +1814,30 @@ 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.
\`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
@@ -1923,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
@@ -2018,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