#!/bin/sh
# Bring this machine up to date: the operating system and the phone system,
# both through the package manager, both from the repositories the first
# boot wrote down. Nothing is fetched by hand and nothing is trusted that
# the package manager would not trust: the Alpine repository is signed with
# the key the package installed into /etc/apk/keys, the Debian one with the
# key in /usr/share/keyrings.
set -eu

if [ "$(id -u)" != 0 ]; then
	echo "run this as root" >&2
	exit 1
fi

before=$(nuxpbx version 2>/dev/null || echo "?")

# Which Asterisk is installed, asked the way this machine's package manager
# answers. Empty where nothing can say, and the restart below reads that as
# "cannot tell" and goes ahead: the old behaviour, which is the safe one.
#
# Without the -v. `apk info -v asterisk` never prints a version at all: -v is
# apk's global verbose flag, and `apk info` with no field flag answers with
# the description, the URL and the rounded installed size, so verbose only
# takes the version off the prefix. What it compared was therefore three
# constants and a megabyte rounded to the nearest whole one, and whether the
# restart fired came down to whether that rounding happened to move. Upgraded
# in place from Alpine 3.22 to edge, Asterisk 20.11.1-r6 becoming 22.10.1-r0,
# the three lines come back identical: a whole major version landed on disk,
# the restart did not fire, the old process went on serving calls, and both
# `nuxpbx version` and apk reported the new release. Plain `apk info` prints
# `asterisk-20.11.1-r0 description:`, which carries it.
engine_version() {
	if command -v apk >/dev/null 2>&1; then
		apk info asterisk 2>/dev/null | head -1 || true
	elif command -v dpkg-query >/dev/null 2>&1; then
		dpkg-query -W -f='${Version}' asterisk 2>/dev/null || true
	fi
}
engine_before=$(engine_version)

# A refresh that fails is not the end of the night. One source that cannot be
# read (a withdrawn PPA, an expired key, a repository unreachable for apk) makes
# the refresh exit non-zero, and under set -e that stopped every update from
# then on, the security fixes whose indexes did refresh included. The upgrade
# goes ahead from what refreshed, and the run fails at the end so the unit
# still says a source is broken.
refreshed=0
say_refresh() { echo "$1 reported failed sources; upgrading from the indexes that did refresh" >&2; }

if command -v apk >/dev/null 2>&1; then
	apk update -q || { refreshed=$?; say_refresh "apk update"; }
	apk upgrade -q
	rc-service nuxpbx restart >/dev/null
	# And Asterisk, where the upgrade replaced it on disk. Alpine's asterisk
	# package carries a .pre-install and a .pre-upgrade and nothing that runs
	# afterwards, so apk lands the new binary and leaves the old process
	# serving calls; Debian's postinst restarts the service itself, which is
	# why the branch below says nothing about it. Without this an Alpine box
	# runs the version it was installed with until somebody reboots it, and
	# `nuxpbx version` reports the new one all the while.
	#
	# Only where it replaced it. Restarting the engine ends every call that
	# is up, and most releases here are of nuxpbx alone: done every time, an
	# ordinary nightly update cut the phones on Alpine and left them alone on
	# Debian, which is the asymmetry this was written to take out.
	if [ -z "$engine_before" ] || [ "$engine_before" != "$(engine_version)" ]; then
		rc-service asterisk restart >/dev/null 2>&1 \
			|| echo "the old Asterisk is still running: restart it by hand to see why it refused" >&2
	fi
elif command -v apt-get >/dev/null 2>&1; then
	apt-get update -q || { refreshed=$?; say_refresh "apt-get update"; }
	# A run that was cut off between unpack and configure (a reboot, an OOM
	# kill, the unit stopped) leaves dpkg marked as interrupted, and every
	# dist-upgrade after it exits 100 before doing anything. Finished here
	# with the same conffile policy as below; on a clean system both do
	# nothing.
	#
	# dpkg's refusal is not the end of it. Cut off between asterisk and
	# asterisk-modules, which need each other at one version, dpkg leaves
	# the new asterisk unconfigured and exits 1 on the dependency; apt-get
	# -f install is what fetches the missing version, and apt will not run
	# at all until dpkg has cleared its journal, so dpkg goes first.
	#
	# Neither has the last word. A package whose postinst fails stays half
	# configured, and apt-get -f install only runs that postinst again and
	# exits 100; the dist-upgrade below is what unpacks the fixed release over
	# it. Ended here, the machine never took the release that repairs it.
	# Where the damage is real, the dist-upgrade fails too and says so.
	DEBIAN_FRONTEND=noninteractive dpkg --configure -a --force-confold --force-confdef || true
	DEBIAN_FRONTEND=noninteractive apt-get -f install -y -q \
		-o Dpkg::Options::=--force-confold \
		-o Dpkg::Options::=--force-confdef \
		|| echo "apt-get -f install failed; the upgrade goes ahead and has the last word" >&2
	# dist-upgrade, not upgrade. `upgrade` holds back any package whose new
	# version needs a package that is not installed yet, and nuxpbx has taken
	# on nftables, fail2ban, python3-systemd and cron since the first release:
	# on those upgrades `upgrade` left the old binary in place, changed
	# nothing, and this script then said the machine was already the newest.
	# Nothing new is fetched that the package manager would not trust either
	# way; what changes is that a new dependency may be installed.
	#
	# And the conffile policy, which is not a preference here. eleven of the
	# files nuxpbx-render writes are dpkg conffiles of asterisk-config, the
	# package asterisk depends on at an exact version: extensions.conf,
	# pjsip.conf, sorcery.conf, extconfig.conf, res_odbc.conf, manager.conf,
	# voicemail.conf, features.conf, logger.conf, rtp.conf, and the
	# modules.conf nuxpbx-realtime-on edits. Every one of them differs from
	# the packaged version on every machine here, so the first upstream
	# Asterisk release that changes any of them makes dpkg ask. Asked with no
	# terminal, dpkg fails with "end of file on stdin at conffile prompt",
	# apt exits 100, set -e ends this script before the restart below, and
	# asterisk-config is left half configured so every later apt call refuses
	# until somebody runs dpkg --configure -a. Answered by hand, Y on
	# extensions.conf installs Asterisk's sample dial plan while asterisk's
	# own postinst restarts the engine onto it, and the machine stops being a
	# telephone. Ours is the copy that must survive: whatever these files
	# hold, nuxpbx wrote it and can write it again.
	#
	# Failed or not, nuxpbx is restarted onto whatever did land, and the run
	# still fails afterwards so the unit says so.
	upgraded=0
	DEBIAN_FRONTEND=noninteractive apt-get dist-upgrade -y -q \
		-o Dpkg::Options::=--force-confold \
		-o Dpkg::Options::=--force-confdef \
		|| upgraded=$?
	systemctl restart nuxpbx
	[ "$upgraded" = 0 ] || exit "$upgraded"
else
	echo "neither apk nor apt: fetch the release yourself" >&2
	exit 1
fi

after=$(nuxpbx version 2>/dev/null || echo "?")
# Said where the engine was actually replaced, which is where both branches
# restart it: apk above, and Debian in asterisk's own postinst, which has
# always done it and said nothing. A bare "done" then reads as an update that
# cost nobody anything, and the calls that were up are gone.
if [ -z "$engine_before" ] || [ "$engine_before" != "$(engine_version)" ]; then
	echo "the phone engine was replaced and restarted, so calls that were up are gone."
fi
if [ "$before" = "$after" ]; then
	echo "done. nuxpbx $after was already the newest."
else
	echo "done. nuxpbx $before is now $after."
fi
[ "$refreshed" = 0 ] || exit "$refreshed"
