#!/usr/bin/env bash # lib/install.func — shared in-container helpers for install/*-install.sh # # Sourced via: # source <(curl -fsSL https://gitea.luki-net.org/luki-net/proxmox-scripts/raw/branch/main/lib/install.func) # ── colors / logging ───────────────────────────────────────────────────────── if [[ -z "${_COLORS_LOADED:-}" ]]; then RED="\033[0;31m"; GREEN="\033[0;32m"; YELLOW="\033[1;33m"; BLUE="\033[0;34m"; NC="\033[0m" _COLORS_LOADED=1 fi msg_info() { echo -e "${BLUE}[i]${NC} $*"; } msg_ok() { echo -e "${GREEN}[✓]${NC} $*"; } msg_warn() { echo -e "${YELLOW}[!]${NC} $*"; } msg_err() { echo -e "${RED}[✗]${NC} $*" >&2; } # ── apt ────────────────────────────────────────────────────────────────────── # Always installs: ca-certificates curl openssl tzdata gnupg locales # Additional packages can be passed as args. setup_base_apt() { export DEBIAN_FRONTEND=noninteractive # C.UTF-8 ist in glibc eingebaut und damit schon VOR dem locales-Paket # verfügbar — deckt den ersten apt/dpkg-Lauf ab (keine perl-Warnungen # "Setting locale failed" mehr, LXC-Templates kommen ohne Locale). export LANG=C.UTF-8 LC_ALL=C.UTF-8 msg_info "Updating apt index..." apt-get update -qq if [[ $# -gt 0 ]]; then msg_info "Installing base packages + $*..." else msg_info "Installing base packages..." fi apt-get install -y -qq \ ca-certificates curl openssl tzdata gnupg locales \ "$@" \ >/dev/null setup_locales msg_ok "apt setup complete" } # en_US.UTF-8 generieren und systemweit als Default setzen; C.UTF-8 braucht # keine Generierung (glibc-built-in). Idempotent: sed greift nur auf die # auskommentierte Zeile, locale-gen/update-locale sind re-run-sicher. setup_locales() { msg_info "Generating locales (en_US.UTF-8; C.UTF-8 built-in)..." sed -i 's/^# *en_US\.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen locale-gen >/dev/null update-locale LANG=en_US.UTF-8 msg_ok "Default locale: en_US.UTF-8" } apt_cleanup() { apt-get autoremove -y -qq >/dev/null || true apt-get autoclean -qq >/dev/null || true } # ── database installers: UTF-8 locale before initdb ────────────────────────── # Pattern for every DB installer. A PostgreSQL cluster/database freezes its # encoding at initdb / CREATE DATABASE time and it cannot be changed later — # a C (non-UTF-8) locale yields a SQL_ASCII cluster. psycopg3 then returns # text as bytes and SQLAlchemy crashes on server-version detection; the app # reports "db: unreachable". So: GENERATE the UTF-8 locale AND make it active # for THIS process before the server package runs its automatic initdb, then # fail loudly if it is not actually available (generating alone is not enough # — the locale must be active when initdb runs). # Generates+activates a UTF-8 locale (default en_US.UTF-8); the argument # honours other UTF-8 locales consistently (match, locale.gen line and the # exported value all derive from it). Matching normalises case and dashes so # the canonical `en_US.UTF-8` matches `locale -a`'s `en_US.utf8`. ensure_utf8_locale_active() { local loc="${1:-en_US.UTF-8}" local norm; norm="$(printf '%s' "$loc" | tr 'A-Z' 'a-z' | tr -d '-')" _locale_present() { locale -a 2>/dev/null | tr 'A-Z' 'a-z' | tr -d '-' | grep -qx "$norm"; } msg_info "Ensuring $loc is generated and active (DB encoding is frozen at initdb)..." if ! _locale_present; then # Uncomment the matching `# UTF-8` line, then generate. sed -i "s/^# *${loc} UTF-8/${loc} UTF-8/" /etc/locale.gen locale-gen >/dev/null fi if ! _locale_present; then msg_err "Locale $loc not available after locale-gen — refusing to continue (initdb would create a SQL_ASCII cluster)" return 1 fi # Activate for the current process so any automatic initdb during the # server package install inherits a UTF-8 locale, not the bare-template C. export LANG="$loc" LC_ALL="$loc" msg_ok "Locale active for initdb: LANG=$LANG" } # Post-install guard: a database MUST be UTF8. Encoding is irreversible, so a # wrong value is database damage — abort with a clear, actionable message # instead of shipping a broken cluster. Uses argv-clean `runuser ... psql` # with a quoted :'db' literal binding (robust regardless of caller); the # credentials/README docs use `su - postgres -c` for hand maintenance (these # minimal LXCs have no sudo). assert_db_encoding_utf8() { local db="$1" enc enc="$(runuser -u postgres -- psql -X -qAt -v db="$db" \ -c "SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = :'db'")" if [[ "$enc" != "UTF8" ]]; then msg_err "Database '$db' has encoding '${enc:-}', expected UTF8." msg_err "Encoding is frozen at creation time — this is DB damage, not cosmetic." msg_err "Fix: regenerate the locale (locale-gen en_US.UTF-8) and recreate the DB" msg_err " with: CREATE DATABASE $db ... TEMPLATE template0 ENCODING 'UTF8'" msg_err " LC_COLLATE 'en_US.UTF-8' LC_CTYPE 'en_US.UTF-8';" return 1 fi msg_ok "Encoding check: database '$db' is UTF8" } # ── ssh ────────────────────────────────────────────────────────────────────── # SSH-Root-Login gemäß Host-Prompt (prompt_lxc_config setzt SSH_ROOT_LOGIN, # bootstrap_install_script reicht es als Env durch; Default: yes). # yes → PermitRootLogin yes (Passwort-Login mit dem generierten Root-Passwort) # no → PermitRootLogin prohibit-password (Debian-Default, nur SSH-Key) # Umsetzung als Drop-in, damit Paket-Updates von sshd_config nicht kollidieren. configure_ssh_root_login() { local choice="${SSH_ROOT_LOGIN:-yes}" value case "$choice" in yes) value="yes" ;; no) value="prohibit-password" ;; *) msg_err "SSH_ROOT_LOGIN must be yes|no, got: '$choice'"; return 1 ;; esac if [[ ! -d /etc/ssh/sshd_config.d ]]; then if [[ "$choice" == "no" ]]; then msg_warn "openssh-server not installed — nothing to configure (root login stays off)" return 0 fi msg_info "Installing openssh-server..." apt-get install -y -qq openssh-server >/dev/null fi msg_info "Configuring SSH root login: PermitRootLogin $value" printf 'PermitRootLogin %s\n' "$value" >/etc/ssh/sshd_config.d/zz-root-login.conf systemctl reload ssh 2>/dev/null || systemctl restart ssh 2>/dev/null \ || msg_warn "ssh.service not active yet — config applies on first start" msg_ok "SSH root login: $choice" } # ── users / dirs ───────────────────────────────────────────────────────────── create_system_user() { local user="$1" home="$2" if id -u "$user" >/dev/null 2>&1; then msg_warn "User $user already exists, skipping" return 0 fi msg_info "Creating system user $user (home: $home)" useradd --system --create-home --home-dir "$home" --shell /usr/sbin/nologin "$user" } # ── systemd ────────────────────────────────────────────────────────────────── # write_systemd_unit NAME CONTENT # Writes /etc/systemd/system/NAME.service, daemon-reloads, enables + starts. write_systemd_unit() { local name="$1" content="$2" msg_info "Writing systemd unit: $name.service" printf '%s\n' "$content" >"/etc/systemd/system/$name.service" systemctl daemon-reload systemctl enable --now "$name.service" msg_ok "$name.service enabled and started" } # ── http wait ──────────────────────────────────────────────────────────────── # wait_for_http URL [TIMEOUT_SECONDS] wait_for_http() { local url="$1" timeout="${2:-30}" msg_info "Waiting for $url..." local i for ((i=0; i/dev/null 2>&1; then msg_ok "$url responding" return 0 fi sleep 1 done msg_warn "$url did not respond within ${timeout}s" return 1 }