Files
Anthias/bin/install.sh
Viktor Petersson 10c68b26cc feat(viewer,build,balena): add arm64/Qt6 pi3-64 board and the Rock Pi 4 fleet; keep 32-bit pi3 as legacy (#2985)
* feat(viewer,build): add arm64/Qt6 pi3-64 board; keep 32-bit pi3 as legacy

Revises issue #2906 Phase 2. The original plan (delete the Qt 5 toolchain,
force Pi 2/Pi 3 onto Qt 6) is abandoned: Qt 5 was fixed up on master and
stays. Instead, add a NEW board target `pi3-64` — a 64-bit (arm64) Qt 6
viewer image for Raspberry Pi 3 hardware on a 64-bit OS — as its own image
stream, disk image, and balena fleet. The legacy 32-bit armhf/Qt5 `pi3`
board is left untouched and flagged as legacy/maintenance.

pi3-64 mirrors the existing `pi4-64` path (Qt 6, eglfs_kms; video played
in-process by AnthiasViewer's QtMultimedia pipeline — QMediaPlayer + the
ffmpeg/libavcodec backend with V4L2 HW decode, no external player).
VideoCore IV is H.264-only HW decode. Board selection is by `uname -m`: a
Pi 3 on a 64-bit OS gets `pi3-64`, a 32-bit OS keeps `pi3` (the model
string is identical on both arches).

- image_builder: pi3-64 build params (arm64) + is_qt6; constants.
- Dockerfile.viewer.j2 + start_viewer.sh: pi3-64 shares the pi4-64 eglfs
  KMS path; renamed board-agnostic eglfs-kms-pi4.json -> eglfs-kms.json.
- Detection: install.sh / upgrade_containers.sh (aarch64 Pi 3 -> pi3-64).
- Runtime: media_player force_mpv set (selects MPVMediaPlayer, the
  QtMultimedia D-Bus shim); processing codec grid {'h264'}.
- CI: docker-build matrix + mirror-latest-tags.
- Balena (fleet screenly_ose/anthias-pi3-64, device type raspberrypi3-64):
  disk-image + manual-deploy workflows, balena_ota_deploy.sh,
  balena_fleet_maintenance.py, balena_unpin_devices.py, deploy_to_balena.sh,
  balena-host-config.json.
- Pi Imager: SUPPORTED_BOARDS += pi3-64 (non-maintenance); pi3 stays legacy.
- Docs + tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(website): link the Pi 3 (64-bit) bullet like its siblings

Copilot review: the list is introduced as 'links to the images', so the
new pi3-64 entry should be navigable like the surrounding bullets. Link
the label to the release-images section.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(balena): add the Rock Pi 4 fleet (screenly_ose/anthias-rockpi4)

Wires the anthias-rockpi4 balena fleet (device type rockpi-4b-rk3399)
into the OTA deploy + disk-image pipeline. The fleet has no
board-specific image build: it runs the generic arm64 containers, so
bin/balena_ota_deploy.sh / bin/deploy_to_balena.sh map the rockpi4
board to the <short-hash>-arm64 image tags (and strip the /dev/vchiq
mount — no VideoCore on RK3399), and the disk-image preflight verifies
the arm64 images exist.

Root-cause fix for the fleet's codec gate: balena ships no
anthias_host_agent service, so host:board_subtype was never published
and resolve_device_key() stayed 'arm64' — whose HW-decode set is empty,
rejecting every video upload. The model-string → subtype table moves to
the dependency-free anthias_common.device_helper.detect_board_subtype
(single source, imported by host_agent), and
anthias_common.board.get_board_subtype now falls back to reading
/proc/device-tree/model in-container when Redis has no value. The
device tree is kernel-global — the same mechanism get_device_type has
always used for Pi detection — so the rockpi4 fleet resolves its
{h264, hevc} envelope without a host-side daemon, and compose installs
whose host_agent died self-heal too.

- build-balena-disk-image.yaml: rockpi4 in both matrices, fleet +
  rockpi-4b-rk3399 image cases, arm64 images in the preflight check.
- deploy-balena-manual.yaml: rockpi4 board option.
- balena-host-config.json: rockpi4 declared {} (config.txt is
  RPi-only; the reconcile hard-fails on a missing key).
- balena_fleet_maintenance.py / balena_unpin_devices.py: fleet added.
- tests: get_board_subtype Redis-first + device-tree-fallback order;
  detect_board_subtype patch targets follow the move.
- docs: board-enablement, balena-fleet-host-config,
  installation-options.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 07:49:12 +02:00

709 lines
25 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# Bash-interpreter guard. `set -o pipefail` below is a bashism that
# would abort with a cryptic message under dash/ash; surface a clear
# error before we reach it so users running `sh install.sh` know what
# went wrong.
if [ -z "${BASH_VERSION:-}" ]; then
echo "error: install.sh must be run with bash, not sh/dash." >&2
exit 1
fi
set -euo pipefail
BRANCH="master"
ANSIBLE_PLAYBOOK_ARGS=()
REPOSITORY="https://github.com/Screenly/Anthias.git"
ANTHIAS_REPO_DIR="/home/${USER}/anthias"
GITHUB_API_REPO_URL="https://api.github.com/repos/Screenly/Anthias"
GITHUB_RELEASES_URL="https://github.com/Screenly/Anthias/releases"
GITHUB_RAW_URL="https://raw.githubusercontent.com/Screenly/Anthias"
DOCKER_TAG="latest"
UPGRADE_SCRIPT_PATH="${ANTHIAS_REPO_DIR}/bin/upgrade_containers.sh"
ARCHITECTURE=$(uname -m)
# Pin uv to match docker/uv-builder.j2 so host and image use the same binary.
UV_PIN_VERSION="0.9.17"
# Ephemeral install-time venv for ansible-core. Created in
# install_ansible(), torn down by the EXIT trap below. A separate
# *persistent* venv at HOST_AGENT_VENV (see provision_host_agent_venv
# below) is what the anthias-host-agent.service systemd unit
# ExecStart actually executes — keeping the two venvs distinct lets
# the installer-only Python state (~100 MB) come and go with each
# install without taking the host-agent down.
INSTALLER_VENV=""
HOST_AGENT_VENV="/home/${USER}/installer_venv"
cleanup_installer_venv() {
if [ -n "${INSTALLER_VENV}" ] && [ -d "${INSTALLER_VENV}" ]; then
# Ansible's `become: true` tasks run python from this venv as
# root and end up writing __pycache__/*.pyc entries owned by
# root. A plain user-level rm therefore can't clean the tree.
# By the time the EXIT trap fires, modify_permissions() has
# already installed the NOPASSWD sudoers rule for the install
# user, so `sudo -n` succeeds non-interactively. Fall back to
# a best-effort user-space rm if the trap fires earlier in
# the script (i.e. before modify_permissions ran).
sudo -n rm -rf "${INSTALLER_VENV}" 2>/dev/null \
|| rm -rf "${INSTALLER_VENV}" 2>/dev/null \
|| true
fi
}
trap cleanup_installer_venv EXIT
INTRO_MESSAGE=(
"Anthias runs on a dedicated Raspberry Pi (2/3/4-64-bit/5), x86 device,"
"or generic 64-bit ARM SBC (Armbian on Rock Pi / Orange Pi / Banana Pi"
"and similar — best-effort, software video decode). The host will be"
"repurposed for digital signage — on a Pi you lose the regular desktop"
"environment, and the machine should not be used for anything else."
""
"When prompted for the version, you can choose between the following:"
" - latest: Installs the latest version from the master branch."
" - tag: Installs a pinned version based on the tag name."
""
"Take note that 'latest' is a rolling release."
)
MANAGE_NETWORK_PROMPT=(
"Would you like Anthias to manage the network for you?"
)
VERSION_PROMPT=(
"Which version of Anthias would you like to install?"
)
SYSTEM_UPGRADE_PROMPT=(
"Would you like to perform a full system upgrade as well?"
)
TITLE_TEXT=$(cat <<EOF
@@@@@@@@@
@@@@@@@@@@@@ d8888 888 888 d8b
@@@@@@@ @@@ @@ d88888 888 888 Y8P
@@@@@@@@@@@@@ @@@ d88P888 888 888
@@@@@@@@@@ @@ @@@@ d88P 888 88888b. 888888 88888b. 888 8888b. .d8888b
@@@@@ @@@@@@@@ d88P 888 888 "88b 888 888 "88b 888 "88b 88K
@@@%: :@@@@@@@@ d88P 888 888 888 888 888 888 888 .d888888 "Y8888b.
@@-:::::::%@@@@@@@ d8888888888 888 888 Y88b. 888 888 888 888 888 X88
@=::::=%@@@@@@@@ d88P 888 888 888 "Y888 888 888 888 "Y888888 88888P'
@@@@@@@@@@
EOF
)
# Anthias brand styling. The CSS palette (sass/_variables.scss) is built
# from purples (#270035#8819C7) and yellows (#FFD800#FFF963); whiptail's
# NEWT_COLORS only accepts the 16 named ANSI colors, so we map purple to
# magenta/brightmagenta and the accent to yellow. The ANSI escapes below
# are used for the plain-text banner/section headers that print between
# long-running install steps, kept off when stdout is not a TTY.
export NEWT_COLORS='
root=,magenta
border=brightmagenta,white
window=black,white
shadow=black,gray
title=white,magenta
button=black,yellow
actbutton=white,brightmagenta
compactbutton=black,white
checkbox=black,white
actcheckbox=yellow,brightmagenta
entry=black,white
disentry=gray,white
label=black,white
listbox=black,white
actlistbox=white,brightmagenta
sellistbox=black,yellow
actsellistbox=white,brightmagenta
textbox=black,white
acttextbox=white,brightmagenta
emptyscale=,white
fullscale=,brightmagenta
helpline=yellow,magenta
roottext=yellow,magenta
'
if [ -t 1 ]; then
ANSI_PURPLE=$'\033[1;35m'
ANSI_YELLOW=$'\033[1;33m'
ANSI_RESET=$'\033[0m'
else
ANSI_PURPLE=''
ANSI_YELLOW=''
ANSI_RESET=''
fi
# whiptail/jq/curl/ca-certificates ship with Debian/Raspbian by default;
# the apt install is a safety net for minimal images. curl runs before
# install_packages now (release menu fetch + connectivity probe), so it
# must be present here rather than later in the pipeline.
function install_prerequisites() {
if [ -f /usr/bin/whiptail ] \
&& [ -f /usr/bin/jq ] \
&& [ -f /usr/bin/curl ] \
&& [ -f /etc/ssl/certs/ca-certificates.crt ]; then
return
fi
sudo apt -y update && sudo apt -y install \
whiptail jq curl ca-certificates
}
function display_banner() {
local TITLE="${1:-Anthias Installer}"
echo
echo "${ANSI_PURPLE}${TITLE}${ANSI_RESET}"
echo
}
function display_section() {
local TITLE="${1:-Section}"
local LINE="======================================================================"
echo
echo "${ANSI_YELLOW}${LINE}${ANSI_RESET}"
echo "${ANSI_PURPLE} ${TITLE}${ANSI_RESET}"
echo "${ANSI_YELLOW}${LINE}${ANSI_RESET}"
echo
}
# Preflight: refuse to run as root, require ${USER} to be set. The
# script elevates with sudo where needed and writes user-owned state
# under /home/${USER}; running the whole script as root would put
# venvs and sudoers entries in the wrong place.
function require_supported_environment() {
if [ "$(id -u)" -eq 0 ] || [ "${USER:-}" = "root" ]; then
echo "error: install.sh must not be run as root." >&2
echo " Run it as the user that will own the Anthias install" >&2
echo " (e.g. 'pi' or 'anthias'); the script elevates with sudo" >&2
echo " only where required." >&2
exit 1
fi
if [ -z "${USER:-}" ]; then
echo "error: \$USER is unset; run install.sh from a login shell." >&2
exit 1
fi
}
# Preflight: confirm github.com is reachable before we burn an apt-get
# update + start prompting the user. Saves a long, confusing failure
# when the device has no network configured yet.
function require_network() {
if ! curl -fsSL --max-time 10 -o /dev/null "${GITHUB_API_REPO_URL}"; then
echo "error: cannot reach ${GITHUB_API_REPO_URL}" >&2
echo " install.sh needs network access to fetch releases and" >&2
echo " clone the repo. Verify connectivity and retry." >&2
exit 1
fi
}
# Emit TAG<TAB>DESCRIPTION lines for the version menu. Filters to
# non-draft, non-prerelease releases that have a `docker-tag` asset
# attached (the only ones the installer can actually use); capped at
# the 10 most recent so the menu fits on an 80×24 console.
function fetch_release_menu_options() {
curl -fsSL --max-time 15 \
"${GITHUB_API_REPO_URL}/releases?per_page=30" 2>/dev/null \
| jq -r '
.[]
| select(.draft == false)
| select(.prerelease == false)
| select(.assets[]?.name == "docker-tag")
| "\(.tag_name)\tReleased \(.published_at[0:10])"
' 2>/dev/null \
| head -n 10 \
|| true
}
function fetch_docker_tag_for_release() {
local TAG="$1"
curl -fsSL --max-time 15 \
"${GITHUB_RELEASES_URL}/download/${TAG}/docker-tag" 2>/dev/null \
|| true
}
function initialize_ansible() {
sudo mkdir -p /etc/ansible
echo -e "[local]\nlocalhost ansible_connection=local" | \
sudo tee /etc/ansible/hosts > /dev/null
}
function initialize_locales() {
display_section "Initialize Locales"
if [ ! -f /etc/locale.gen ]; then
# No locales found. Creating locales with default UK/US setup.
echo -e "en_GB.UTF-8 UTF-8\nen_US.UTF-8 UTF-8" | \
sudo tee /etc/locale.gen > /dev/null
sudo locale-gen
fi
}
function install_packages() {
display_section "Install Packages via APT"
local APT_INSTALL_ARGS=(
"ca-certificates"
"curl"
"gettext-base"
"git"
"libffi-dev"
"libssl-dev"
"whois"
)
if [ "$MANAGE_NETWORK" = "Yes" ]; then
APT_INSTALL_ARGS+=("network-manager")
fi
# Rewrite the legacy `apt.screenlyapp.com` mirror reference (carried
# over from older Screenly OSE installs) to the upstream Raspbian
# archive on Pi hardware. Guarded on file existence: Armbian and most
# non-Pi ARM distros only populate /etc/apt/sources.list.d/*.list and
# leave /etc/apt/sources.list absent, where the unconditional sed
# would exit non-zero and trip `set -e`.
if [ "$ARCHITECTURE" != "x86_64" ] && [ -f /etc/apt/sources.list ]; then
sudo sed -i 's/apt.screenlyapp.com/archive.raspbian.org/g' \
/etc/apt/sources.list
fi
sudo apt-get update
sudo apt-get install -y "${APT_INSTALL_ARGS[@]}"
}
function migrate_repo_dir() {
# Rename ~/screenly -> ~/anthias before clone_repo runs so the user's
# existing checkout state, hooks, and any local changes are preserved
# rather than starting fresh in a new directory. Config and asset
# dirs are migrated separately by bin/migrate_legacy_paths.sh inside
# the cloned repo (see post_clone_migrate_legacy_paths).
local OLD_REPO_DIR="/home/${USER}/screenly"
if [ -d "${OLD_REPO_DIR}/.git" ] && [ ! -e "${ANTHIAS_REPO_DIR}" ]; then
display_section "Rename ${OLD_REPO_DIR} -> ${ANTHIAS_REPO_DIR}"
mv "${OLD_REPO_DIR}" "${ANTHIAS_REPO_DIR}"
# Back-compat symlink for one release.
ln -s "${ANTHIAS_REPO_DIR}" "${OLD_REPO_DIR}"
fi
}
function post_clone_migrate_legacy_paths() {
# Run the in-repo migration helper once the repo is on disk. Handles
# ~/.screenly -> ~/.anthias, ~/screenly_assets -> ~/anthias_assets,
# the screenly.{db,conf} -> anthias.{db,conf} renames, and the
# back-compat symlinks. Idempotent / no-op on fresh installs.
display_section "Migrate Legacy 'screenly' Data Paths"
"${ANTHIAS_REPO_DIR}/bin/migrate_legacy_paths.sh"
}
function clone_repo() {
display_section "Clone Anthias Repository"
if [ ! -d "${ANTHIAS_REPO_DIR}/.git" ]; then
git clone "${REPOSITORY}" "${ANTHIAS_REPO_DIR}"
fi
git -C "${ANTHIAS_REPO_DIR}" fetch --tags origin
git -C "${ANTHIAS_REPO_DIR}" checkout "${BRANCH}"
# Releases are pinned via tags (e.g. v0.20.5), which fetch into
# refs/tags/ — `origin/v0.20.5` is not a valid ref. Resolve tags
# explicitly via refs/tags/${BRANCH} and fall through to
# origin/${BRANCH} only for actual branches.
local RESET_REF
if git -C "${ANTHIAS_REPO_DIR}" show-ref --verify --quiet \
"refs/tags/${BRANCH}"; then
RESET_REF="refs/tags/${BRANCH}"
elif git -C "${ANTHIAS_REPO_DIR}" show-ref --verify --quiet \
"refs/remotes/origin/${BRANCH}"; then
RESET_REF="origin/${BRANCH}"
else
echo "error: '${BRANCH}' is neither a tag nor a remote branch" >&2
exit 1
fi
git -C "${ANTHIAS_REPO_DIR}" reset --hard "${RESET_REF}"
}
function install_ansible() {
display_section "Install uv and host Python dependencies"
# uv manages its own Python and packages — no system pip/venv needed.
# Pinned via UV_PIN_VERSION so the host and the docker uv-builder
# stage stay in lockstep (see docker/uv-builder.j2).
if ! command -v uv &> /dev/null || \
[ "$(uv --version 2>/dev/null | awk '{print $2}')" != "${UV_PIN_VERSION}" ]; then
curl -LsSf "https://astral.sh/uv/${UV_PIN_VERSION}/install.sh" | sh
fi
export PATH="$HOME/.local/bin:$PATH"
# Provision the venv in a fresh tmpdir each run so a previous
# install's interpreter/layout can never collide with this one.
# Earlier releases pinned a constraint here (`--python ">=3.13"`)
# which uv resolves to the latest matching managed interpreter — on
# Bookworm that picked 3.14, while pyproject's `.python-version`
# asks for 3.13, and the disagreement triggered a tear-down on
# every subsequent `uv sync` (Fixes #2842). Letting `.python-version`
# be the single source of truth keeps both invocations aligned.
INSTALLER_VENV=$(mktemp -d -t anthias-installer-venv.XXXXXX)
# UV_LINK_MODE=copy: ~/.cache/uv and /tmp are on different
# filesystems on most Pi/Debian installs (tmpfs vs the SD card),
# so uv's default hardlink fails and falls back to copy with a
# warning. Force copy mode to match what we actually want.
UV_PROJECT_ENVIRONMENT="${INSTALLER_VENV}" \
UV_LINK_MODE=copy \
uv sync \
--project "${ANTHIAS_REPO_DIR}" \
--no-default-groups \
--group host \
--no-install-project
}
function provision_host_agent_venv() {
display_section "Provision Host Agent Python Environment"
# Permanent venv consumed by anthias-host-agent.service. The unit
# template (ansible/roles/anthias/templates/anthias-host-agent.service)
# hardcodes this path so a Trixie/Python-3.13 cutover that retires
# the system interpreter the host-agent was launched with leaves a
# 203/EXEC loop until reinstall — recreating the venv unconditionally
# on every install + upgrade is what keeps the service self-healing.
if [ -d "${HOST_AGENT_VENV}" ]; then
# Same sudo-with-fallback pattern as cleanup_installer_venv:
# earlier runs may have left root-owned __pycache__ entries.
sudo -n rm -rf "${HOST_AGENT_VENV}" 2>/dev/null \
|| rm -rf "${HOST_AGENT_VENV}" 2>/dev/null
fi
UV_PROJECT_ENVIRONMENT="${HOST_AGENT_VENV}" \
uv sync \
--project "${ANTHIAS_REPO_DIR}" \
--no-default-groups \
--group host \
--no-install-project
# On upgrade the unit is already loaded and running with the
# *previous* venv's interpreter (Python keeps its image even after
# the venv directory is unlinked), so the ansible task's
# `state: started` is a no-op and the new deps never get picked
# up. Restart explicitly when the unit is present. On a fresh
# install the unit doesn't exist yet, so the check is a no-op and
# ansible's `state: started` does the first start.
if systemctl list-unit-files anthias-host-agent.service \
>/dev/null 2>&1 \
&& systemctl is-active --quiet anthias-host-agent.service; then
sudo systemctl restart anthias-host-agent.service
fi
}
function set_device_type() {
if [ ! -f /proc/device-tree/model ] && [ "$(uname -m)" = "x86_64" ]; then
export DEVICE_TYPE="x86"
elif grep -qF "Raspberry Pi 5" /proc/device-tree/model || grep -qF "Compute Module 5" /proc/device-tree/model; then
export DEVICE_TYPE="pi5"
elif grep -qF "Raspberry Pi 4" /proc/device-tree/model || grep -qF "Compute Module 4" /proc/device-tree/model; then
export DEVICE_TYPE="pi4-64"
elif grep -qF "Raspberry Pi 3" /proc/device-tree/model || grep -qF "Compute Module 3" /proc/device-tree/model; then
# A Pi 3 reports the same model string on both a 32-bit and a
# 64-bit OS — the split is the running kernel/userland arch. On
# a 64-bit OS use the arm64 Qt 6 viewer (`pi3-64`, the
# recommended stream); a 32-bit OS gets the legacy armhf/Qt5
# `pi3` image. There's no in-place arch switch: a device only
# changes streams by reflashing to a different-arch OS.
if [ "$(uname -m)" = "aarch64" ]; then
export DEVICE_TYPE="pi3-64"
else
export DEVICE_TYPE="pi3"
fi
elif grep -qF "Raspberry Pi 2" /proc/device-tree/model; then
export DEVICE_TYPE="pi2"
elif [ "$(uname -m)" = "aarch64" ]; then
# Generic 64-bit ARM SBC fallback (Orange Pi, Rock Pi, Banana Pi, …).
# Best-effort: stack runs on any board with mainline Mesa DRM/KMS;
# video decode falls back to software since hwdec varies per SoC.
# Intentional catch-all — a future Pi model whose model string
# drifts past the regexes above will land here too. The cost is
# software decode + no Pi-specific boot tweaks; the alternative
# would be loud-failing every new SBC variant until someone
# extends the device_tree dispatch above, which we'd rather
# avoid for "best-effort" boards. Loud failure stays reserved
# for non-aarch64 unknown hosts (the else branch below).
export DEVICE_TYPE="arm64"
else
echo "Unsupported device. Anthias supports Pi 2/3/4 (64-bit)/5, x86, and 64-bit ARM SBCs (best-effort)." >&2
exit 1
fi
}
function run_ansible_playbook() {
display_section "Run the Anthias Ansible Playbook"
# DEVICE_TYPE is already exported by main() via set_device_type as
# a preflight, so unsupported hardware fails before the long apt
# pipeline starts.
# Forwarded to the playbook so the screenly role can pin
# /usr/local/sbin/upgrade_anthias.sh to the same ref the user picked.
export ANTHIAS_BRANCH="${BRANCH}"
cd "${ANTHIAS_REPO_DIR}/ansible"
# If the user doesn't have NOPASSWD sudo yet (first install), Ansible
# needs --ask-become-pass to elevate. The blanket NOPASSWD rule is
# written by modify_permissions later in this script.
if [ ! -f "/etc/sudoers.d/010_${USER}-nopasswd" ]; then
ANSIBLE_PLAYBOOK_ARGS+=("--ask-become-pass")
echo "Note: Ansible may prompt for your sudo password below."
echo
fi
# Pi-specific boot tweaks (config.txt/cmdline.txt, gpu_mem, vc4 dtoverlays)
# only apply on actual Raspberry Pi hardware. Skip on x86 and on the
# arm64 fallback (non-Pi 64-bit ARM SBCs).
if [ "$ARCHITECTURE" == "x86_64" ] || [ "$DEVICE_TYPE" == "arm64" ]; then
ANSIBLE_PLAYBOOK_ARGS+=("--skip-tags" "raspberry-pi")
fi
# Point Ansible at the venv's Python — we no longer install
# python3 system-wide in the bootstrap step. Both this and the
# ansible-playbook path interpolate INSTALLER_VENV at parse time, so
# the literal tmpdir path crosses the sudo boundary as the argv —
# no env-forwarding gymnastics needed beyond -E preserving
# ANSIBLE_PYTHON_INTERPRETER itself.
export ANSIBLE_PYTHON_INTERPRETER="${INSTALLER_VENV}/bin/python"
sudo -E -u "${USER}" \
"${INSTALLER_VENV}/bin/ansible-playbook" \
site.yml "${ANSIBLE_PLAYBOOK_ARGS[@]}"
}
function upgrade_docker_containers() {
display_section "Initialize/Upgrade Docker Containers"
# Pull upgrade_containers.sh from the same ref the user picked,
# not master, so a tagged install gets the matching upgrade script.
curl -fsSL \
"${GITHUB_RAW_URL}/${BRANCH}/bin/upgrade_containers.sh" \
-o "${UPGRADE_SCRIPT_PATH}"
sudo -u "${USER}" \
DOCKER_TAG="${DOCKER_TAG}" \
GIT_BRANCH="${BRANCH}" \
"${UPGRADE_SCRIPT_PATH}"
}
function cleanup() {
display_section "Clean Up Unused Packages and Files"
sudo apt-get autoclean
sudo apt-get clean
sudo docker system prune -f
sudo apt autoremove -y
sudo apt-get install plymouth --reinstall -y
sudo find /usr/share/doc \
-depth \
-type f \
! -name copyright \
-delete
sudo find /usr/share/doc \
-empty \
-delete
sudo rm -rf \
/usr/share/man \
/usr/share/groff \
/usr/share/info/* \
/usr/share/lintian \
/usr/share/linda /var/cache/man
sudo find /usr/share/locale \
-type f \
! -name 'en' \
! -name 'de*' \
! -name 'es*' \
! -name 'ja*' \
! -name 'fr*' \
! -name 'zh*' \
-delete
sudo find /usr/share/locale \
-mindepth 1 \
-maxdepth 1 \
! -name 'en*' \
! -name 'de*' \
! -name 'es*' \
! -name 'ja*' \
! -name 'fr*' \
! -name 'zh*' \
! -name 'locale.alias' \
-exec rm -r {} \;
}
function modify_permissions() {
sudo chown -R "${USER}:${USER}" "/home/${USER}"
# Run `sudo` without entering a password.
local SUDOERS_FILE="/etc/sudoers.d/010_${USER}-nopasswd"
if [ ! -f "${SUDOERS_FILE}" ]; then
echo "${USER} ALL=(ALL) NOPASSWD: ALL" | \
sudo tee "${SUDOERS_FILE}" > /dev/null
sudo chmod 0440 "${SUDOERS_FILE}"
fi
}
function write_anthias_version() {
local GIT_BRANCH GIT_SHORT_HASH ANTHIAS_VERSION
GIT_BRANCH=$(git -C "${ANTHIAS_REPO_DIR}" rev-parse --abbrev-ref HEAD)
GIT_SHORT_HASH=$(git -C "${ANTHIAS_REPO_DIR}" rev-parse --short HEAD)
ANTHIAS_VERSION="Anthias Version: ${GIT_BRANCH}@${GIT_SHORT_HASH}"
{
echo "${ANTHIAS_VERSION}"
lsb_release -a 2> /dev/null
} > ~/version.md
}
function post_installation() {
display_section "Installation Complete"
local PROMPT="A reboot is required to complete the installation."
if [ -n "${SSH_CONNECTION:-}" ]; then
PROMPT+=$'\n\nHeads up: you appear to be connected over SSH; rebooting will drop your session.'
fi
PROMPT+=$'\n\nDo you want to reboot now?'
if whiptail \
--title "Anthias Installer" \
--yesno "${PROMPT}" 14 70; then
echo "Rebooting..."
sudo reboot
fi
}
function set_custom_version() {
BRANCH=$(
whiptail \
--title "Anthias Installer" \
--inputbox "Enter the tag name you want to install:" \
10 70 \
3>&1 1>&2 2>&3
)
local STATUS_CODE
STATUS_CODE=$(curl -fsS -o /dev/null -w "%{http_code}" \
"${GITHUB_API_REPO_URL}/git/refs/tags/${BRANCH}" || echo "000")
if [ "$STATUS_CODE" -ne 200 ]; then
whiptail \
--title "Anthias Installer" \
--msgbox "Invalid tag name." 8 60
exit 1
fi
DOCKER_TAG=$(fetch_docker_tag_for_release "${BRANCH}")
if [ -z "${DOCKER_TAG}" ]; then
whiptail \
--title "Anthias Installer" \
--msgbox "This version doesn't have a docker-tag file." 8 60
exit 1
fi
}
function main() {
require_supported_environment
set_device_type
install_prerequisites && clear
require_network
display_banner "${TITLE_TEXT}"
local INTRO
INTRO=$(printf '%s\n' "${INTRO_MESSAGE[@]}")
whiptail \
--title "Anthias Installer" \
--yesno "${INTRO}"$'\n\nDo you still want to continue?' \
20 76 \
|| exit 0
if whiptail \
--title "Anthias Installer" \
--yesno "${MANAGE_NETWORK_PROMPT[*]}" 10 70; then
export MANAGE_NETWORK="Yes"
else
export MANAGE_NETWORK="No"
fi
# Build the version menu from the live GitHub release list, with
# "latest" pinned at the top and "other" as an escape hatch for
# tags not in the recent window (or if the API call is empty).
local -a MENU_OPTS=(
"latest" "Tip of master branch (rolling release)"
)
local TAG DESC
while IFS=$'\t' read -r TAG DESC; do
[ -n "${TAG}" ] && MENU_OPTS+=("${TAG}" "${DESC}")
done < <(fetch_release_menu_options)
MENU_OPTS+=("other" "Enter a specific tag name manually")
VERSION=$(
whiptail \
--title "Anthias Installer" \
--menu "${VERSION_PROMPT[*]}" 22 76 12 \
"${MENU_OPTS[@]}" \
3>&1 1>&2 2>&3
)
case "${VERSION}" in
latest)
BRANCH="master"
;;
other)
set_custom_version
;;
*)
BRANCH="${VERSION}"
DOCKER_TAG=$(fetch_docker_tag_for_release "${BRANCH}")
if [ -z "${DOCKER_TAG}" ]; then
whiptail \
--title "Anthias Installer" \
--msgbox "Could not fetch docker-tag for ${BRANCH}." \
8 60
exit 1
fi
;;
esac
if whiptail \
--title "Anthias Installer" \
--yesno "${SYSTEM_UPGRADE_PROMPT[*]}" 10 70; then
SYSTEM_UPGRADE="Yes"
else
SYSTEM_UPGRADE="No"
ANSIBLE_PLAYBOOK_ARGS+=("--skip-tags" "system-upgrade")
fi
display_section "User Input Summary"
echo "Manage Network: ${MANAGE_NETWORK}"
echo "Branch/Tag: ${BRANCH}"
echo "System Upgrade: ${SYSTEM_UPGRADE}"
echo "Docker Tag Prefix: ${DOCKER_TAG}"
initialize_ansible
initialize_locales
install_packages
migrate_repo_dir
clone_repo
post_clone_migrate_legacy_paths
install_ansible
provision_host_agent_venv
run_ansible_playbook
upgrade_docker_containers
cleanup
modify_permissions
write_anthias_version
post_installation
}
# Only run the interactive installer when this file is executed directly.
# Sourcing it (e.g. from a non-interactive bootstrap that wants to call
# specific functions like install_packages without going through the gum
# UI) leaves main untouched.
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main
fi