Files
thomas 1f110866f5
CI / backend (pull_request) Canceled after 0s
CI / shell (pull_request) Canceled after 0s
CI / frontend (pull_request) Canceled after 0s
CI / arm64-smoke (pull_request) Canceled after 0s
CI / backend (push) Canceled after 0s
CI / shell (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / arm64-smoke (push) Canceled after 0s
Update docs and screenshots
2026-07-25 10:26:29 +02:00

774 lines
22 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SOURCE_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
APP_DIR="/opt/tuxdmx"
STAGE_DIR="/opt/tuxdmx.new"
DATA_DIR="/var/lib/tuxdmx"
BACKUP_DIR="/var/lib/tuxdmx/backups"
DIAGNOSTICS_DIR="/var/lib/tuxdmx/diagnostics"
UPLOADS_DIR="/var/lib/tuxdmx/uploads"
FIXTURE_CACHE_DIR="/var/lib/tuxdmx/fixtures"
LOG_DIR="/var/log/tuxdmx"
CONFIG_DIR="/etc/tuxdmx"
ENV_FILE="$CONFIG_DIR/tuxdmx.env"
SERVICE_USER="tuxdmx"
SERVICE_GROUP="tuxdmx"
SYSTEMD_UNIT_NAME="tuxdmx.service"
SYSTEMD_UNIT_PATH="/etc/systemd/system/$SYSTEMD_UNIT_NAME"
SYSTEM_CONTROL_SUDOERS_PATH="/etc/sudoers.d/tuxdmx-control"
SYSTEMD_WORKDIR="/opt/tuxdmx"
PYTHON_BIN="${PYTHON_BIN:-python3}"
NODE_REQUIRED_MAJOR="20"
PNPM_REQUIRED_VERSION="10.28.2"
SKIP_FRONTEND_BUILD="false"
NON_INTERACTIVE="false"
FORCE_OVERWRITE="false"
APT_UPDATED="false"
PNPM_BIN=""
ROLLBACK_APP_ARCHIVE=""
ROLLBACK_DATA_ARCHIVE=""
ROLLBACK_CONFIG_ARCHIVE=""
ROLLBACK_UNIT_FILE=""
HAD_EXISTING_APP="false"
HAD_EXISTING_DATA="false"
HAD_EXISTING_CONFIG="false"
HAD_EXISTING_UNIT="false"
OLA_SERVICE_NAME="olad"
OLA_SYSTEMD_UNIT="olad.service"
readonly COMMON_PACKAGES=(
python3
python3-venv
python3-pip
python3-dev
sqlite3
sudo
curl
git
jq
zip
unzip
rsync
ca-certificates
nodejs
npm
ola
alsa-utils
usbutils
)
readonly OLA_PYTHON_PACKAGES=(
python3-ola
ola-python
)
readonly RUNTIME_ROOT_FILES=(
".env.example"
"LICENSE"
"NOTICE"
"package.json"
"pnpm-lock.yaml"
"pnpm-workspace.yaml"
"pyproject.toml"
"README.md"
"requirements.lock"
"THIRD_PARTY_NOTICES.md"
"tsconfig.json"
"tsconfig.node.json"
"vite.config.ts"
)
readonly RUNTIME_DIRS=(
"backend"
"frontend"
"scripts"
"systemd"
"test-data"
)
log() {
echo "[TuxDMX] $*"
}
fail() {
echo "[TuxDMX] FEJL: $*" >&2
exit 1
}
parse_common_args() {
for arg in "$@"; do
case "$arg" in
--non-interactive) NON_INTERACTIVE="true" ;;
--force) FORCE_OVERWRITE="true" ;;
--skip-frontend-build) SKIP_FRONTEND_BUILD="true" ;;
*) echo "Ukendt argument: $arg" >&2; exit 2 ;;
esac
done
}
require_root() {
if [[ "${EUID}" -ne 0 ]]; then
fail "Koer scriptet som root eller via sudo."
fi
}
apt_update_once() {
if [[ "$APT_UPDATED" == "true" ]]; then
return
fi
apt-get update
APT_UPDATED="true"
}
install_packages() {
apt_update_once
DEBIAN_FRONTEND=noninteractive apt-get install -y "$@"
}
package_available() {
apt-cache show "$1" >/dev/null 2>&1
}
check_network_connectivity() {
curl -fsS https://open-fixture-library.org >/dev/null
}
verify_debian_13_amd64() {
source /etc/os-release
local arch
arch="$(dpkg --print-architecture)"
[[ "${ID:-}" == "debian" ]] || fail "Debian 13 amd64 er paakraevet. Fundet ID=${ID:-ukendt}."
[[ "${VERSION_ID:-}" == "13" ]] || fail "Debian 13 er paakraevet. Fundet VERSION_ID=${VERSION_ID:-ukendt}."
[[ "$arch" == "amd64" ]] || fail "amd64 er paakraevet. Fundet arkitektur=$arch."
}
verify_pi_platform() {
source /etc/os-release
local arch
arch="$(uname -m)"
[[ "${ID:-}" =~ ^(debian|raspbian)$ ]] || fail "Pi-installation forventer Debian eller Raspberry Pi OS."
[[ "$arch" =~ ^(aarch64|arm64|armv7l)$ ]] || log "Advarsel: forventet ARM-arkitektur, men fandt $arch."
}
verify_source_tree() {
[[ -f "$SOURCE_ROOT/backend/app/main.py" ]] || fail "Source tree mangler backend/app/main.py."
[[ -f "$SOURCE_ROOT/package.json" ]] || fail "Source tree mangler package.json."
[[ -f "$SOURCE_ROOT/requirements.lock" ]] || fail "Source tree mangler requirements.lock."
}
confirm_install_overwrite() {
if [[ -d "$APP_DIR" ]] && find "$APP_DIR" -mindepth 1 -print -quit >/dev/null 2>&1; then
if [[ "$FORCE_OVERWRITE" == "true" ]]; then
return
fi
if [[ "$NON_INTERACTIVE" == "true" ]]; then
fail "Eksisterende installation fundet. Brug --force sammen med --non-interactive."
fi
read -r -p "Eksisterende installation fundet i $APP_DIR. Fortsaet? [y/N] " answer
[[ "$answer" =~ ^[Yy]$ ]] || exit 1
fi
}
ensure_service_user() {
if ! id "$SERVICE_USER" >/dev/null 2>&1; then
useradd --system --home "$DATA_DIR" --create-home --groups dialout,plugdev,audio "$SERVICE_USER"
fi
usermod -a -G dialout,plugdev,audio "$SERVICE_USER"
}
ensure_directories() {
install -d -o root -g "$SERVICE_GROUP" -m 0755 "$APP_DIR"
install -d -o root -g "$SERVICE_GROUP" -m 0755 "$STAGE_DIR"
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0755 "$DATA_DIR"
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0755 "$BACKUP_DIR"
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0755 "$DIAGNOSTICS_DIR"
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0755 "$UPLOADS_DIR"
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0755 "$FIXTURE_CACHE_DIR"
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0755 "$LOG_DIR"
install -d -o root -g "$SERVICE_GROUP" -m 0750 "$CONFIG_DIR"
}
ensure_backup_dir() {
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0755 "$BACKUP_DIR"
}
install_base_packages() {
install_packages "${COMMON_PACKAGES[@]}"
}
install_ola_python_binding() {
local installed="false"
for candidate in "${OLA_PYTHON_PACKAGES[@]}"; do
if package_available "$candidate"; then
install_packages "$candidate"
installed="true"
break
fi
done
if [[ "$installed" != "true" ]]; then
fail "Kunne ikke finde Debian-pakke til OLA Python-binding (proevede: ${OLA_PYTHON_PACKAGES[*]})."
fi
if ! "$PYTHON_BIN" - <<'PY'
from ola.ClientWrapper import ClientWrapper
print(ClientWrapper.__name__)
PY
then
fail "OLA Python-binding kunne ikke importeres med system-python."
fi
command -v olad >/dev/null 2>&1 || fail "olad blev ikke fundet efter installation af OLA."
}
ensure_node_toolchain() {
local node_version
local node_major
local global_prefix
node_version="$(node --version | sed 's/^v//')"
node_major="${node_version%%.*}"
[[ "$node_major" == "$NODE_REQUIRED_MAJOR" ]] || fail "Node 20 er paakraevet. Fundet $node_version."
npm --version >/dev/null
global_prefix="$(npm config get prefix)"
npm install -g --force "pnpm@$PNPM_REQUIRED_VERSION"
export PATH="$global_prefix/bin:$PATH"
hash -r
PNPM_BIN="$(command -v pnpm || true)"
[[ -n "$PNPM_BIN" ]] || fail "pnpm blev ikke fundet efter installation."
[[ "$("$PNPM_BIN" --version)" == "$PNPM_REQUIRED_VERSION" ]] \
|| fail "Aktiv pnpm-version er ikke $PNPM_REQUIRED_VERSION."
}
require_source_frontend_artifacts() {
[[ -f "$SOURCE_ROOT/frontend/dist/index.html" ]] \
|| fail "--skip-frontend-build kraever eksisterende frontend/dist/index.html."
}
build_frontend() {
if [[ "$SKIP_FRONTEND_BUILD" == "true" ]]; then
require_source_frontend_artifacts
return
fi
ensure_node_toolchain
(
cd "$SOURCE_ROOT"
"$PNPM_BIN" install --frozen-lockfile
"$PNPM_BIN" build
)
[[ -f "$SOURCE_ROOT/frontend/dist/index.html" ]] \
|| fail "Frontend-build afsluttede uden frontend/dist/index.html."
}
cleanup_stage_dir() {
rm -rf "$STAGE_DIR"
install -d -o root -g "$SERVICE_GROUP" -m 0755 "$STAGE_DIR"
}
sync_directory() {
local relative_path="$1"
rsync -a --delete --chown=root:"$SERVICE_GROUP" \
--chmod=Du=rwx,Dg=rx,Do=rx,Fu=rw,Fg=r,Fo=r \
"$SOURCE_ROOT/$relative_path/" "$STAGE_DIR/$relative_path/"
}
sync_file() {
local relative_path="$1"
install -d -o root -g "$SERVICE_GROUP" -m 0755 "$(dirname "$STAGE_DIR/$relative_path")"
rsync -a --chown=root:"$SERVICE_GROUP" \
--chmod=Fu=rw,Fg=r,Fo=r \
"$SOURCE_ROOT/$relative_path" "$STAGE_DIR/$relative_path"
}
sync_runtime_tree_to_stage() {
local dir_path
local file_path
cleanup_stage_dir
for dir_path in "${RUNTIME_DIRS[@]}"; do
sync_directory "$dir_path"
done
for file_path in "${RUNTIME_ROOT_FILES[@]}"; do
if [[ -f "$SOURCE_ROOT/$file_path" ]]; then
sync_file "$file_path"
fi
done
ln -sfn "$DATA_DIR" "$STAGE_DIR/data"
find "$STAGE_DIR/scripts" -type f -name "*.sh" -exec chmod 0755 {} +
[[ -f "$STAGE_DIR/backend/app/main.py" ]] || fail "Backend-layout er forkert: $STAGE_DIR/backend/app/main.py mangler."
[[ -f "$STAGE_DIR/frontend/dist/index.html" ]] || fail "Kopieret frontend/dist/index.html mangler i stage."
}
create_or_refresh_venv() {
log "Opretter virtualenv i stage"
"$PYTHON_BIN" -m venv --system-site-packages "$STAGE_DIR/.venv"
"$STAGE_DIR/.venv/bin/pip" install --upgrade pip
"$STAGE_DIR/.venv/bin/pip" install -r "$STAGE_DIR/requirements.lock"
run_runtime_pip_check
}
run_runtime_pip_check() {
local output=""
local status=0
local filtered=""
output="$("$STAGE_DIR/.venv/bin/pip" check 2>&1)" || status=$?
if [[ "$status" -eq 0 ]]; then
return
fi
filtered="$(printf '%s\n' "$output" | grep -Ev '^apt-listchanges .+ requires debconf, which is not installed\.$' || true)"
if [[ -z "${filtered//[[:space:]]/}" ]]; then
log "Ignorerer kendt Debian pip check-advarsel fra apt-listchanges/debconf."
return
fi
printf '%s\n' "$filtered" >&2
fail "pip check fejlede i runtime-venv."
}
write_default_env_file() {
local secret
secret="$("$STAGE_DIR/.venv/bin/python" - <<'PY'
import secrets
print(secrets.token_hex(32))
PY
)"
cat >"$ENV_FILE" <<EOF
TUXDMX_ENV=production
TUXDMX_SECRET_KEY=$secret
TUXDMX_DATABASE_URL=sqlite+aiosqlite:////var/lib/tuxdmx/tuxdmx.db
TUXDMX_LOG_LEVEL=INFO
TUXDMX_SIMULATOR_ENABLED=false
TUXDMX_OLA_UNIVERSE=1
TUXDMX_OLA_OUTPUT_PORT=
TUXDMX_OLA_SEND_TIMEOUT_MS=1000
TUXDMX_DATA_DIR=/var/lib/tuxdmx
TUXDMX_BACKUP_DIR=/var/lib/tuxdmx/backups
TUXDMX_FIXTURE_CACHE_DIR=/var/lib/tuxdmx/fixtures
TUXDMX_DIAGNOSTICS_DIR=/var/lib/tuxdmx/diagnostics
TUXDMX_UPLOADS_DIR=/var/lib/tuxdmx/uploads
TUXDMX_LOG_DIR=/var/log/tuxdmx
TUXDMX_FRONTEND_DIST=/opt/tuxdmx/frontend/dist
EOF
chown root:"$SERVICE_GROUP" "$ENV_FILE"
chmod 0640 "$ENV_FILE"
}
ensure_env_file() {
if [[ ! -f "$ENV_FILE" ]]; then
write_default_env_file
else
chown root:"$SERVICE_GROUP" "$ENV_FILE"
chmod 0640 "$ENV_FILE"
fi
ensure_env_setting "TUXDMX_DATABASE_URL" "sqlite+aiosqlite:////var/lib/tuxdmx/tuxdmx.db"
ensure_env_setting "TUXDMX_LOG_LEVEL" "INFO"
ensure_env_setting "TUXDMX_SIMULATOR_ENABLED" "false"
ensure_env_setting "TUXDMX_OLA_UNIVERSE" "1"
ensure_env_setting "TUXDMX_OLA_OUTPUT_PORT" ""
ensure_env_setting "TUXDMX_OLA_SEND_TIMEOUT_MS" "1000"
ensure_env_setting "TUXDMX_DATA_DIR" "/var/lib/tuxdmx"
ensure_env_setting "TUXDMX_BACKUP_DIR" "/var/lib/tuxdmx/backups"
ensure_env_setting "TUXDMX_FIXTURE_CACHE_DIR" "/var/lib/tuxdmx/fixtures"
ensure_env_setting "TUXDMX_DIAGNOSTICS_DIR" "/var/lib/tuxdmx/diagnostics"
ensure_env_setting "TUXDMX_UPLOADS_DIR" "/var/lib/tuxdmx/uploads"
ensure_env_setting "TUXDMX_LOG_DIR" "/var/log/tuxdmx"
ensure_env_setting "TUXDMX_FRONTEND_DIST" "/opt/tuxdmx/frontend/dist"
}
ensure_env_setting() {
local key="$1"
local value="$2"
if grep -q "^${key}=" "$ENV_FILE"; then
return
fi
printf '%s=%s\n' "$key" "$value" >>"$ENV_FILE"
}
load_env_lines() {
local line
while IFS= read -r line; do
[[ -z "$line" ]] && continue
[[ "$line" =~ ^[[:space:]]*# ]] && continue
printf '%s\0' "$line"
done < "$ENV_FILE"
}
run_as_service_user() {
local app_root="$1"
shift
local env_lines=()
while IFS= read -r -d '' line; do
env_lines+=("$line")
done < <(load_env_lines)
runuser -u "$SERVICE_USER" -- env \
"PYTHONPATH=$app_root/backend" \
"${env_lines[@]}" \
"$@"
}
verify_python_runtime() {
run_as_service_user "$STAGE_DIR" "$STAGE_DIR/.venv/bin/python" -c "import greenlet; print(greenlet.__version__)"
run_as_service_user "$STAGE_DIR" "$STAGE_DIR/.venv/bin/python" -c "import sqlalchemy; print(sqlalchemy.__version__)"
run_as_service_user "$STAGE_DIR" "$STAGE_DIR/.venv/bin/python" -c "import ola; print(ola.__file__)"
}
verify_stage_permissions_and_layout() {
test -f "$STAGE_DIR/backend/app/main.py" || fail "Stage mangler backend/app/main.py."
runuser -u "$SERVICE_USER" -- test -r "$STAGE_DIR/frontend/dist/index.html"
runuser -u "$SERVICE_USER" -- "$PYTHON_BIN" -c \
"from pathlib import Path; print(Path('$STAGE_DIR/frontend/dist/index.html').read_text()[:20])"
}
verify_app_import() {
run_as_service_user "$STAGE_DIR" "$STAGE_DIR/.venv/bin/python" -c \
"import sys; sys.path.insert(0, '$STAGE_DIR/backend'); import app.main"
}
run_migrations() {
log "Koerer database migration som $SERVICE_USER"
run_as_service_user "$STAGE_DIR" \
"$STAGE_DIR/.venv/bin/python" -m alembic -c "$STAGE_DIR/backend/alembic.ini" upgrade head
}
verify_database_writable() {
run_as_service_user "$STAGE_DIR" "$STAGE_DIR/.venv/bin/python" -c \
"from pathlib import Path; db=Path('/var/lib/tuxdmx/tuxdmx.db'); print(db.exists()); wal=Path('/var/lib/tuxdmx/tuxdmx.db-wal'); wal.touch(exist_ok=True); print(wal.exists())"
chown "$SERVICE_USER":"$SERVICE_GROUP" /var/lib/tuxdmx/tuxdmx.db* 2>/dev/null || true
}
install_systemd_unit() {
install -m 0644 "$STAGE_DIR/systemd/tuxdmx.service" "$SYSTEMD_UNIT_PATH"
systemctl daemon-reload
enable_ola_service
install_system_control_sudoers
systemctl enable "$SYSTEMD_UNIT_NAME"
}
install_system_control_sudoers() {
cat >"$SYSTEM_CONTROL_SUDOERS_PATH" <<'EOF'
Defaults:tuxdmx !requiretty
tuxdmx ALL=(root) NOPASSWD: /opt/tuxdmx/scripts/control-system.sh restart-service, /opt/tuxdmx/scripts/control-system.sh reboot-host
EOF
chmod 0440 "$SYSTEM_CONTROL_SUDOERS_PATH"
visudo -cf "$SYSTEM_CONTROL_SUDOERS_PATH" >/dev/null
}
show_service_diagnostics() {
systemctl status "$SYSTEMD_UNIT_NAME" --no-pager --full || true
journalctl -u "$SYSTEMD_UNIT_NAME" -n 200 --no-pager || true
}
resolve_ola_service_name() {
local unit_files
unit_files="$(systemctl list-unit-files --type=service --no-legend 2>/dev/null | awk '{print $1}')"
if grep -Fxq "olad.service" <<<"$unit_files"; then
OLA_SERVICE_NAME="olad"
OLA_SYSTEMD_UNIT="olad.service"
return
fi
if grep -Fxq "ola.service" <<<"$unit_files"; then
OLA_SERVICE_NAME="ola"
OLA_SYSTEMD_UNIT="ola.service"
return
fi
if [[ -x /etc/init.d/olad ]]; then
OLA_SERVICE_NAME="olad"
OLA_SYSTEMD_UNIT="olad.service"
return
fi
if [[ -x /etc/init.d/ola ]]; then
OLA_SERVICE_NAME="ola"
OLA_SYSTEMD_UNIT="ola.service"
fi
}
has_sysv_boot_link() {
local service_name="$1"
compgen -G "/etc/rc?.d/S??$service_name" >/dev/null
}
enable_ola_service() {
resolve_ola_service_name
systemctl enable "$OLA_SYSTEMD_UNIT" >/dev/null 2>&1 || true
if [[ -x "/etc/init.d/$OLA_SERVICE_NAME" ]]; then
update-rc.d "$OLA_SERVICE_NAME" defaults >/dev/null 2>&1 || true
fi
if [[ "$OLA_SERVICE_NAME" != "olad" && -x /etc/init.d/olad ]]; then
update-rc.d olad defaults >/dev/null 2>&1 || true
fi
if [[ "$OLA_SERVICE_NAME" != "ola" && -x /etc/init.d/ola ]]; then
update-rc.d ola defaults >/dev/null 2>&1 || true
fi
if systemctl is-enabled "$OLA_SYSTEMD_UNIT" >/dev/null 2>&1; then
return
fi
if has_sysv_boot_link "$OLA_SERVICE_NAME"; then
return
fi
fail "Kunne ikke registrere $OLA_SYSTEMD_UNIT til autostart."
}
show_ola_diagnostics() {
systemctl status "$OLA_SYSTEMD_UNIT" --no-pager --full || true
journalctl -u "$OLA_SYSTEMD_UNIT" -n 200 --no-pager || true
}
stop_existing_service_for_snapshot() {
if systemctl list-unit-files "$SYSTEMD_UNIT_NAME" >/dev/null 2>&1; then
systemctl stop "$SYSTEMD_UNIT_NAME" || true
for _ in $(seq 1 15); do
local state
state="$(systemctl is-active "$SYSTEMD_UNIT_NAME" || true)"
if [[ "$state" != "active" && "$state" != "activating" && "$state" != "deactivating" ]]; then
break
fi
sleep 1
done
fi
}
restart_and_healthcheck() {
local max_checks=30
local check
resolve_ola_service_name
if ! systemctl restart "$OLA_SYSTEMD_UNIT"; then
if ! service "$OLA_SERVICE_NAME" restart; then
show_ola_diagnostics
fail "Kunne ikke restarte $OLA_SYSTEMD_UNIT."
fi
fi
if ! systemctl restart "$SYSTEMD_UNIT_NAME"; then
show_service_diagnostics
fail "Kunne ikke starte $SYSTEMD_UNIT_NAME."
fi
for check in $(seq 1 "$max_checks"); do
local service_state
service_state="$(systemctl is-active "$SYSTEMD_UNIT_NAME" || true)"
if [[ "$service_state" == "failed" || "$service_state" == "deactivating" ]]; then
show_service_diagnostics
fail "tuxdmx.service gik i state '$service_state' under health check."
fi
if curl -fsS http://127.0.0.1:8000/api/v1/health >/dev/null; then
systemctl is-active "$SYSTEMD_UNIT_NAME" >/dev/null
return
fi
sleep 2
done
show_service_diagnostics
fail "Health check mod http://127.0.0.1:8000/api/v1/health fejlede."
}
make_archive() {
local source_dir="$1"
local archive_path="$2"
if [[ ! -d "$source_dir" ]]; then
return 1
fi
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0755 "$BACKUP_DIR"
if [[ "$source_dir" == "$DATA_DIR" ]]; then
tar \
--warning=no-file-changed \
--exclude="./backups" \
--exclude="./tuxdmx.json.log" \
-czf "$archive_path" -C "$source_dir" .
return
fi
tar -czf "$archive_path" -C "$source_dir" .
}
restore_archive() {
local archive_path="$1"
local target_dir="$2"
[[ -f "$archive_path" ]] || return
rm -rf "$target_dir"
install -d -o root -g "$SERVICE_GROUP" -m 0755 "$target_dir"
tar -xzf "$archive_path" -C "$target_dir"
}
prepare_install_rollback() {
local stamp
stamp="$(date +%Y%m%d-%H%M%S)"
ensure_backup_dir
if [[ -d "$APP_DIR" ]] && find "$APP_DIR" -mindepth 1 -print -quit >/dev/null 2>&1; then
HAD_EXISTING_APP="true"
ROLLBACK_APP_ARCHIVE="$BACKUP_DIR/install-app-$stamp.tar.gz"
make_archive "$APP_DIR" "$ROLLBACK_APP_ARCHIVE"
fi
if [[ -d "$DATA_DIR" ]] && find "$DATA_DIR" -mindepth 1 -print -quit >/dev/null 2>&1; then
HAD_EXISTING_DATA="true"
ROLLBACK_DATA_ARCHIVE="$BACKUP_DIR/install-data-$stamp.tar.gz"
make_archive "$DATA_DIR" "$ROLLBACK_DATA_ARCHIVE"
fi
if [[ -d "$CONFIG_DIR" ]] && find "$CONFIG_DIR" -mindepth 1 -print -quit >/dev/null 2>&1; then
HAD_EXISTING_CONFIG="true"
ROLLBACK_CONFIG_ARCHIVE="$BACKUP_DIR/install-config-$stamp.tar.gz"
make_archive "$CONFIG_DIR" "$ROLLBACK_CONFIG_ARCHIVE"
fi
if [[ -f "$SYSTEMD_UNIT_PATH" ]]; then
HAD_EXISTING_UNIT="true"
ROLLBACK_UNIT_FILE="$BACKUP_DIR/install-$SYSTEMD_UNIT_NAME.$stamp.bak"
cp "$SYSTEMD_UNIT_PATH" "$ROLLBACK_UNIT_FILE"
fi
}
prepare_update_rollback() {
local stamp
stamp="$(date +%Y%m%d-%H%M%S)"
ensure_backup_dir
ROLLBACK_APP_ARCHIVE="$BACKUP_DIR/update-app-$stamp.tar.gz"
ROLLBACK_DATA_ARCHIVE="$BACKUP_DIR/update-data-$stamp.tar.gz"
ROLLBACK_CONFIG_ARCHIVE="$BACKUP_DIR/update-config-$stamp.tar.gz"
ROLLBACK_UNIT_FILE="$BACKUP_DIR/update-$SYSTEMD_UNIT_NAME.$stamp.bak"
make_archive "$APP_DIR" "$ROLLBACK_APP_ARCHIVE" || fail "Kunne ikke oprette backup af eksisterende appmappe."
make_archive "$DATA_DIR" "$ROLLBACK_DATA_ARCHIVE" || fail "Kunne ikke oprette backup af datamappe."
make_archive "$CONFIG_DIR" "$ROLLBACK_CONFIG_ARCHIVE" || true
if [[ -f "$SYSTEMD_UNIT_PATH" ]]; then
cp "$SYSTEMD_UNIT_PATH" "$ROLLBACK_UNIT_FILE"
fi
}
restore_rollbacks() {
log "Fejl opdaget. Forsoger rollback."
if [[ -n "$ROLLBACK_APP_ARCHIVE" && -f "$ROLLBACK_APP_ARCHIVE" ]]; then
restore_archive "$ROLLBACK_APP_ARCHIVE" "$APP_DIR"
fi
if [[ -n "$ROLLBACK_DATA_ARCHIVE" && -f "$ROLLBACK_DATA_ARCHIVE" ]]; then
restore_archive "$ROLLBACK_DATA_ARCHIVE" "$DATA_DIR"
chown -R "$SERVICE_USER":"$SERVICE_GROUP" "$DATA_DIR"
fi
if [[ -n "$ROLLBACK_CONFIG_ARCHIVE" && -f "$ROLLBACK_CONFIG_ARCHIVE" ]]; then
restore_archive "$ROLLBACK_CONFIG_ARCHIVE" "$CONFIG_DIR"
chown -R root:"$SERVICE_GROUP" "$CONFIG_DIR"
chmod 0750 "$CONFIG_DIR"
[[ -f "$ENV_FILE" ]] && chmod 0640 "$ENV_FILE"
fi
if [[ -f "$ROLLBACK_UNIT_FILE" ]]; then
install -m 0644 "$ROLLBACK_UNIT_FILE" "$SYSTEMD_UNIT_PATH"
fi
rm -rf "$STAGE_DIR"
systemctl daemon-reload || true
resolve_ola_service_name
systemctl restart "$OLA_SYSTEMD_UNIT" || true
systemctl restart "$SYSTEMD_UNIT_NAME" || true
}
promote_stage() {
systemctl stop "$SYSTEMD_UNIT_NAME" || true
rm -rf "$APP_DIR"
mv "$STAGE_DIR" "$APP_DIR"
chown -R root:"$SERVICE_GROUP" "$APP_DIR"
}
finalize_runtime_layout() {
ln -sfn "$DATA_DIR" "$APP_DIR/data"
}
run_install_flow() {
local platform_label="$1"
require_root
confirm_install_overwrite
check_network_connectivity
log "Starter installation for $platform_label"
verify_source_tree
install_base_packages
install_ola_python_binding
ensure_node_toolchain
build_frontend
stop_existing_service_for_snapshot
prepare_install_rollback
trap 'restore_rollbacks' ERR
ensure_service_user
ensure_directories
sync_runtime_tree_to_stage
create_or_refresh_venv
ensure_env_file
verify_python_runtime
verify_stage_permissions_and_layout
verify_app_import
run_migrations
verify_database_writable
install_systemd_unit
promote_stage
finalize_runtime_layout
restart_and_healthcheck
trap - ERR
log "Installation fuldfoert"
}
run_update_flow() {
local platform_label="$1"
require_root
check_network_connectivity
log "Starter update for $platform_label"
verify_source_tree
install_base_packages
install_ola_python_binding
ensure_node_toolchain
build_frontend
stop_existing_service_for_snapshot
prepare_update_rollback
trap 'restore_rollbacks' ERR
ensure_service_user
ensure_directories
sync_runtime_tree_to_stage
create_or_refresh_venv
ensure_env_file
verify_python_runtime
verify_stage_permissions_and_layout
verify_app_import
run_migrations
verify_database_writable
install_systemd_unit
promote_stage
finalize_runtime_layout
restart_and_healthcheck
trap - ERR
log "Update gennemfoert"
}