#!/bin/bash # ══════════════════════════════════════════════════════════════════════════════ # enconf-sip — One-Liner Installer # # curl -fsSL https://get.sip.enconf.com | bash # # Supported: Debian 13 (Trixie) — amd64 + arm64 # # On a minimal Debian install curl is not pre-installed: # apt update && apt install -y curl # ══════════════════════════════════════════════════════════════════════════════ set -euo pipefail # Force a C locale so command output we parse (apt-cache policy "Candidate:", # apt messages, …) is always English. Without this, a German/other-locale # Debian prints "Installationskandidat:" and `grep Candidate` fails — which, # under `pipefail`+`set -e`, aborted the whole installer silently right after # the checklist. C.UTF-8 keeps UTF-8 so the spinner/✓/✗ glyphs still render. export LC_ALL=C.UTF-8 LANG=C.UTF-8 # ─── Colors + helpers ───────────────────────────────────────────────────────── GRN="\033[0;32m"; RED="\033[0;31m"; YLW="\033[0;33m"; CYN="\033[0;36m" BLD="\033[1m"; DIM="\033[2m"; NC="\033[0m" CHECK="${GRN}✓${NC}" CROSS="${RED}✗${NC}" step() { local label="$1"; shift local spin_chars='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏' local pid logf logf="$(mktemp /tmp/enconf-sip-step.XXXXXX 2>/dev/null || echo /tmp/enconf-sip-step.log)" # Start command in background, capturing output so a failure can be shown. "$@" >"$logf" 2>&1 & pid=$! # Animate spinner with elapsed time local i=0 local start=$SECONDS while kill -0 "$pid" 2>/dev/null; do local c="${spin_chars:i%${#spin_chars}:1}" local elapsed=$(( SECONDS - start )) printf "\r ${CYN}${c}${NC} ${label}... ${DIM}${elapsed}s${NC} " sleep 0.1 i=$((i + 1)) done local total=$(( SECONDS - start )) if wait "$pid"; then printf "\r ${CHECK} ${label} ${DIM}(${total}s)${NC} \n" rm -f "$logf" return 0 fi # Failure — surface the captured output instead of aborting silently. A # bare ✗ with no explanation is the #1 install-support issue on the other # enconf installers; printing the real error here avoids repeating it. printf "\r ${CROSS} ${label} ${DIM}(${total}s)${NC} \n" echo -e " ${RED}Failed at: ${label}${NC}" echo -e " ${DIM}────────────────── Output ──────────────────${NC}" tail -n 25 "$logf" | sed 's/^/ /' echo -e " ${DIM}──────────────────────────────────────────────${NC}" rm -f "$logf" return 1 } step_log() { printf " ${CHECK} $1\n"; } fail() { echo -e "\n ${CROSS} ${RED}$*${NC}\n"; exit 1; } # On a freshly booted server, unattended-upgrades/apt-daily holds the dpkg/apt # lock — apt-get then aborts with "Could not get lock". Wait until it's free # (max 5 min) instead of failing immediately on first boot. wait_for_apt() { local max=300 waited=0 while fuser /var/lib/dpkg/lock-frontend /var/lib/apt/lists/lock /var/lib/dpkg/lock >/dev/null 2>&1; do [ "$waited" -ge "$max" ] && fail "apt/dpkg lock still held after ${max}s. Stop running apt processes and retry." sleep 3 waited=$((waited + 3)) done } apt_update() { wait_for_apt # PDiffs=false avoids a corrupted incremental-index state that can leave # Debian's own packages unfindable ("Unable to locate package ..."). apt-get update -qq -o Acquire::PDiffs=false 2>/dev/null || true # Some cloud images arrive with a broken apt list state (empty main index # from failed PDiffs) — adduser is a good canary: always in Debian main, # and postinst needs it. If it's unresolvable, nuke the lists and rebuild. if ! apt-cache policy adduser 2>/dev/null | grep -Eq 'Candidate: [0-9]'; then rm -rf /var/lib/apt/lists/* 2>/dev/null apt-get clean >/dev/null 2>&1 || true apt-get update -qq -o Acquire::PDiffs=false fi } install_prereqs() { wait_for_apt; DEBIAN_FRONTEND=noninteractive apt-get install -y -qq -o Dpkg::Options::=--force-confold curl gnupg ca-certificates apt-transport-https; } # ─── Banner ─────────────────────────────────────────────────────────────────── clear 2>/dev/null || true echo "" echo -e " ${CYN}${BLD}enconf-sip${NC} — Installer" echo -e " ${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" # ─── Root check ─────────────────────────────────────────────────────────────── [ "$(id -u)" -ne 0 ] && fail "Please run as root: curl -fsSL https://get.sip.enconf.com | bash" # ─── OS check ───────────────────────────────────────────────────────────────── if [ -f /etc/os-release ]; then . /etc/os-release OS_ID="$ID" OS_VERSION="$VERSION_ID" OS_CODENAME="${VERSION_CODENAME:-}" else fail "Operating system not detected." fi case "$OS_ID" in debian) [ "$OS_VERSION" = "13" ] || fail "Debian $OS_VERSION not supported (only 13)." ;; *) fail "$OS_ID not supported. enconf-sip only runs on Debian 13 (Trixie)." ;; esac ARCH=$(dpkg --print-architecture 2>/dev/null || uname -m) case "$ARCH" in amd64|x86_64) ARCH="amd64" ;; arm64|aarch64) ARCH="arm64" ;; *) fail "Architecture $ARCH not supported (only amd64/arm64)." ;; esac step_log "${OS_ID^} ${OS_VERSION} (${OS_CODENAME}) · ${ARCH}" # ─── Safety: refuse to re-provision an already-provisioned box ──────────────── # This installer is for FRESH boxes; updates go via apt. Override for a # deliberate wipe-and-rebuild with ENCONF_SIP_FORCE_REINSTALL=1. if [ "${ENCONF_SIP_FORCE_REINSTALL:-}" != "1" ]; then if [ -e /etc/enconf-sip/enconf-sip.env ] || dpkg-query -W -f='${Status}' enconf-sip 2>/dev/null | grep -q "install ok installed"; then fail "Existing enconf-sip install detected (/etc/enconf-sip or installed package) — this box is already provisioned. Refusing to reinstall over it (updates go via 'apt upgrade'). Set ENCONF_SIP_FORCE_REINSTALL=1 to override." fi fi # ─── 1. Update system ──────────────────────────────────────────────────────── # netcell.list/netcell-gitea.asc is a legacy key name from an earlier repo # layout — leaving it alongside enconf.list makes apt fail hard with # "Conflicting values set for option Signed-By". rm -f /etc/apt/sources.list.d/netcell.list /etc/apt/keyrings/netcell-gitea.asc 2>/dev/null || true step "Update system" apt_update step "Install prerequisites" install_prereqs # ─── 2. Set up repository ──────────────────────────────────────────────────── setup_repo() { wait_for_apt rm -f /etc/apt/sources.list.d/netcell.list /etc/apt/keyrings/netcell-gitea.asc 2>/dev/null || true mkdir -p /etc/apt/keyrings curl -fsSL "https://git.netcell-it.de/api/packages/projekte/debian/repository.key" \ -o /etc/apt/keyrings/enconf-gitea.asc # Kanal-Modell: Suite = OS-Codename, Komponente = Kanal (Default stable). local suite="$OS_CODENAME" case "$suite" in trixie) ;; *) suite="trixie" ;; esac local channel="${ENCONF_CHANNEL:-stable}" echo "deb [signed-by=/etc/apt/keyrings/enconf-gitea.asc] https://git.netcell-it.de/api/packages/projekte/debian $suite $channel" \ > /etc/apt/sources.list.d/enconf.list apt-get update -qq -o Acquire::PDiffs=false } step "Set up enconf apt repository" setup_repo # ─── 3. Check package availability ─────────────────────────────────────────── # awk-Pattern statt grep|awk: findet die Candidate-Zeile ohne dass ein # Nicht-Treffer (pipefail) den Installer killt. LC_ALL=C erzwingt Englisch. AVAILABLE=$(LC_ALL=C apt-cache policy enconf-sip 2>/dev/null | awk '/Candidate:/{print $2}') if [ -z "$AVAILABLE" ] || [ "$AVAILABLE" = "(none)" ]; then fail "Package 'enconf-sip' not found in the apt repository (channel: ${ENCONF_CHANNEL:-stable}, suite: trixie). The repository itself is reachable, but no enconf-sip build has been published to it yet. Ask NetCell IT to publish a build, or set ENCONF_CHANNEL=testing if a build only exists there." fi step_log "Version ${BLD}${AVAILABLE}${NC} available" # ─── 4. Install enconf-sip ─────────────────────────────────────────────────── install_sip() { wait_for_apt local apt_opts=(-y -qq -o Dpkg::Options::=--force-confold -o Dpkg::Options::=--force-confdef) if ! DEBIAN_FRONTEND=noninteractive apt-get install "${apt_opts[@]}" enconf-sip; then # Safety net for any postinst that misbehaves on a dirty apt state. # DEBIAN_FRONTEND=noninteractive matters on dpkg --configure too. DEBIAN_FRONTEND=noninteractive dpkg --configure -a >/dev/null 2>&1 || true DEBIAN_FRONTEND=noninteractive apt-get install "${apt_opts[@]}" --fix-broken >/dev/null 2>&1 || true DEBIAN_FRONTEND=noninteractive apt-get install "${apt_opts[@]}" enconf-sip fi } step "Install enconf-sip" install_sip # ─── 5. Dienste prüfen ─────────────────────────────────────────────────────── if systemctl is-active --quiet enconf-sip 2>/dev/null; then step_log "enconf-sip running" else printf " ${CROSS} enconf-sip not active — check: journalctl -u enconf-sip\n" fi # ─── Result ────────────────────────────────────────────────────────────────── SERVER_IP=$(hostname -I | awk '{print $1}') echo "" echo -e " ${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" echo -e " ${GRN}${BLD}Installation complete!${NC}" echo "" echo -e " ${BLD}▸ Admin-UI:${NC} ${CYN}http://${SERVER_IP}:3200${NC}" echo -e " ${BLD}▸ SIP (UDP):${NC} ${SERVER_IP}:5060" echo "" echo -e " ${DIM}Open the Admin-UI URL in your browser to configure extensions and trunks.${NC}" echo -e " ${DIM}Config/secrets: /etc/enconf-sip/enconf-sip.env · Daten: /opt/enconf-sip/data${NC}" echo "" echo -e " ${YLW}!${NC} ${BLD}Firewall:${NC} open these ports for calls to carry audio, not just signaling:" echo -e " ${DIM}5060/udp (SIP) · 5061/tcp (SIP-TLS) · 8089/tcp (WSS Softphone) · 10000-20000/udp (RTP)${NC}" echo "" echo -e " ${BLD}▸ Optional — real TLS for the Admin-UI (Let's Encrypt):${NC}" echo -e " ${DIM}Set ADMIN_DOMAIN=admin.example.com in /etc/enconf-sip/enconf-sip.env, open 80/tcp${NC}" echo -e " ${DIM}+ 3443/tcp, then restart the service — Admin-UI becomes reachable on${NC}" echo -e " ${DIM}https://:3443 (http://...:3200 keeps working unchanged).${NC}" echo "" # Anonymous install-success beacon — only fires after the full script completed. # host-hash is SHA-truncated machine-id so reinstalls on the same host don't double-count. { HOST_HASH=$(printf '%s' "$(cat /etc/machine-id 2>/dev/null || hostname)" | sha256sum 2>/dev/null | cut -c1-16) OS_ID=$(. /etc/os-release 2>/dev/null && echo "${ID:-?}-${VERSION_ID:-?}") curl -fsS --max-time 5 "https://get.sip.enconf.com/installed?h=${HOST_HASH}&os=${OS_ID}" >/dev/null 2>&1 || true } & disown 2>/dev/null || true