#!/usr/bin/env bash set -Eeuo pipefail umask 077 # Transient systemd units may omit HOME; Go needs explicit cache locations. export HOME=${HOME:-/root} export GOPATH=${GOPATH:-/var/cache/debtik/go} export GOCACHE=${GOCACHE:-/var/cache/debtik/go-build} mkdir -p "$GOPATH" "$GOCACHE" base=/var/lib/debtik/update mkdir -p "$base" exec 9>"$base/lock" flock -n 9 || { echo 'An update is already running' >&2; exit 1; } exec >>"$base/update.log" 2>&1 state() { printf '%s\n' "$1" >"$base/status"; printf '[%s] %s\n' "$(date -u +%FT%TZ)" "$1"; } cleanup_veth() { python3 - <<'PY' import json,subprocess,pathlib p=pathlib.Path('/etc/proxmox-router/vpn/aerotun_instances.json') if p.exists(): for tunnel in json.loads(p.read_text()): name=tunnel['name'] result=subprocess.run(['ip','-j','-d','link','show','dev',name],capture_output=True,text=True) if result.returncode: continue links=json.loads(result.stdout) if len(links)!=1:raise RuntimeError('Ambiguous AeroTUN interface') link=links[0] if link.get('linkinfo',{}).get('info_kind')=='veth': if link.get('link')!=name+'_ep':raise RuntimeError('Unexpected veth peer') subprocess.run(['ip','link','delete',name],check=True) PY } restore() { state rolling_back systemctl stop proxmox-router.service || true cleanup_veth || true for name in proxmox-router debtik-cli; do test -f "$base/previous/$name" cp -p "$base/previous/$name" "/usr/local/bin/$name.rollback" mv -f "/usr/local/bin/$name.rollback" "/usr/local/bin/$name" done for name in debtik-update debtik-release-version; do if [[ -f "$base/previous/$name" ]]; then install -m 0750 "$base/previous/$name" "/usr/local/libexec/$name.rollback"; mv -f "/usr/local/libexec/$name.rollback" "/usr/local/libexec/$name"; fi done if [[ -f "$base/previous_commit" ]]; then cp -p "$base/previous_commit" "$base/installed_commit"; fi if [[ -f "$base/previous_version" ]]; then cp -p "$base/previous_version" "$base/installed_version"; fi systemctl start proxmox-router.service systemctl is-active --quiet proxmox-router.service if [[ -f "$base/previous/iptables.save" ]]; then iptables-restore -c < "$base/previous/iptables.save"; fi if [[ -s "$base/previous/ip6tables.save" ]]; then ip6tables-restore -c < "$base/previous/ip6tables.save"; fi state rolled_back } if [[ ${1:-} == rollback ]]; then restore; exit; fi [[ $# == 4 && $1 == install ]] || { echo 'Usage: debtik-update install URL BRANCH | rollback' >&2; exit 2; } repo=$2 branch=$3 pin=$4 git_opts=() if [[ -n $pin ]]; then [[ $pin =~ ^sha256//[A-Za-z0-9+/]{43}=$ ]] || exit 2 git_opts=(-c http.sslVerify=false -c "http.pinnedpubkey=$pin") fi [[ $repo == http://* || $repo == https://* || $repo == ssh://* ]] || exit 2 [[ $branch =~ ^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$ && $branch != *..* ]] || exit 2 work=$(mktemp -d "$base/work.XXXXXXXX") trap 'rm -rf "$work"' EXIT trap 'state failed' ERR state fetching version_tool=/usr/local/libexec/debtik-release-version version=$(GIT_TERMINAL_PROMPT=0 git "${git_opts[@]}" ls-remote --tags "$repo" 'refs/tags/v*' | "$version_tool" latest) installed_version=$(cat "$base/installed_version") if ! "$version_tool" greater "$version" "$installed_version"; then state failed echo "Update refused: available version $version must be greater than installed version $installed_version" exit 1 fi GIT_TERMINAL_PROMPT=0 git -c protocol.file.allow=never "${git_opts[@]}" clone --single-branch --branch "v$version" -- "$repo" "$work/source" cd "$work/source" commit=$(git rev-parse HEAD) [[ $(cat VERSION) == "$version" ]] || { state failed; echo 'Release tag and VERSION do not match'; exit 1; } bash -n scripts/debtik-update python3 -c 'import pathlib; compile(pathlib.Path("scripts/release-version.py").read_text(),"release-version.py","exec")' state building go_cmd=$(command -v go || true) if [[ -x /opt/debtik/go/bin/go ]]; then go_cmd=/opt/debtik/go/bin/go; fi [[ -n $go_cmd ]] || { state failed; echo 'Go is not installed'; exit 1; } CGO_ENABLED=0 "$go_cmd" build -trimpath -ldflags "-X proxmox-router/internal/api.BuildCommit=$commit" -o "$work/proxmox-router" ./cmd/server CGO_ENABLED=0 "$go_cmd" build -trimpath -o "$work/debtik-cli" ./cmd/debtik-cli install -d -m 0700 "$base/previous" for name in proxmox-router debtik-cli; do cp -p "/usr/local/bin/$name" "$base/previous/$name"; done for name in debtik-update debtik-release-version; do cp -p "/usr/local/libexec/$name" "$base/previous/$name"; done cp -p "$base/installed_version" "$base/previous_version" if [[ -f "$base/installed_commit" ]]; then cp -p "$base/installed_commit" "$base/previous_commit"; fi printf '%s\n' "$commit" > "$base/candidate_commit" iptables-save -c > "$base/previous/iptables.save" ip6tables-save -c > "$base/previous/ip6tables.save" state installing rollback_on_error() { trap - ERR; restore || state failed; exit 1; } trap rollback_on_error ERR systemctl stop proxmox-router.service cleanup_veth for name in proxmox-router debtik-cli; do install -m 0755 "$work/$name" "/usr/local/bin/$name.next" mv -f "/usr/local/bin/$name.next" "/usr/local/bin/$name" done install -m 0750 scripts/debtik-update /usr/local/libexec/debtik-update.next install -m 0750 scripts/release-version.py /usr/local/libexec/debtik-release-version.next mv -f /usr/local/libexec/debtik-update.next /usr/local/libexec/debtik-update mv -f /usr/local/libexec/debtik-release-version.next /usr/local/libexec/debtik-release-version systemctl start proxmox-router.service sleep 3 systemctl is-active --quiet proxmox-router.service printf '%s\n' "$commit" > "$base/installed_commit" printf '%s\n' "$version" > "$base/installed_version" state complete