aboutsummaryrefslogtreecommitdiff
path: root/straper/install-base.sh
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukasz.kasprzak@pm.me>2026-03-19 15:05:32 +0100
committerLukasz Kasprzak <lukasz.kasprzak@pm.me>2026-03-19 15:05:32 +0100
commitb1844d805a8f190af00faf3f6e5bed7d997ecaae (patch)
treed6296ba5e85034836e203a542d2ad34a9e5c5b32 /straper/install-base.sh
parentc74163f1ecbd809c83bb465ee89fa04369a89972 (diff)
downloadbin-b1844d805a8f190af00faf3f6e5bed7d997ecaae.tar.gz
bin-b1844d805a8f190af00faf3f6e5bed7d997ecaae.zip
created sorter for sorting downloads with custom rules; created straper for recreation of server
Diffstat (limited to 'straper/install-base.sh')
-rw-r--r--straper/install-base.sh342
1 files changed, 342 insertions, 0 deletions
diff --git a/straper/install-base.sh b/straper/install-base.sh
new file mode 100644
index 0000000..e2a2d42
--- /dev/null
+++ b/straper/install-base.sh
@@ -0,0 +1,342 @@
+#!/usr/bin/env bash
+set -Eeuo pipefail
+
+SCRIPT_DIR="$(cd -- "$(dirname -- "$0")" && pwd)"
+# shellcheck disable=SC1091
+source "${SCRIPT_DIR}/lib/common.sh"
+
+SCRIPT_NAME="install-base.sh"
+USE_DB_APT_SOURCES=false
+FROM_DB_MANUAL=false
+HOSTNAME_WANTED=""
+TIMEZONE_WANTED="Europe/Warsaw"
+LOCALE_WANTED="en_US.UTF-8"
+
+usage() {
+ cat <<USAGE
+Usage: sudo ./install-base.sh [options]
+
+Purpose:
+ Install a reproducible Debian base for sanctum/labunix recovery without
+ importing host-specific configs or identities.
+
+Options:
+ --db-dir PATH Capture DB root (default: ${DB_DIR})
+ --role lab|hardware|replacement
+ --profile minimal|core|full
+ --hostname NAME Set hostname
+ --timezone TZ Set timezone (default: ${TIMEZONE_WANTED})
+ --locale LOCALE Set locale (default: ${LOCALE_WANTED})
+ --use-db-apt-sources Restore apt sources from db/public/packages/apt
+ --from-db-manual Install packages listed in apt-mark-manual.txt
+ --start-safe-services Enable/start low-risk services after install
+ --yes Non-interactive yes to prompts
+ --dry-run Print actions only
+ --verbose More logging
+ --help
+USAGE
+}
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --db-dir) DB_DIR="$2"; PUBLIC_DIR="${DB_DIR}/db/public"; SECRET_DIR="${DB_DIR}/db/secret"; shift ;;
+ --role) ROLE="$2"; shift ;;
+ --profile) PROFILE="$2"; shift ;;
+ --hostname) HOSTNAME_WANTED="$2"; shift ;;
+ --timezone) TIMEZONE_WANTED="$2"; shift ;;
+ --locale) LOCALE_WANTED="$2"; shift ;;
+ --use-db-apt-sources) USE_DB_APT_SOURCES=true ;;
+ --from-db-manual) FROM_DB_MANUAL=true ;;
+ --start-safe-services) START_SERVICES=true ;;
+ --yes) ASSUME_YES=true ;;
+ --dry-run) DRY_RUN=true ;;
+ --verbose) VERBOSE=true ;;
+ --help) usage; exit 0 ;;
+ *) die "unknown option: $1" ;;
+ esac
+ shift
+done
+
+ensure_root
+load_optional_config
+ensure_runtime_dirs
+require_cmd apt-get apt-cache dpkg hostnamectl rsync
+
+mkdir -p -- /etc/labunix
+
+log "${SCRIPT_NAME}: role=${ROLE} profile=${PROFILE} db=${DB_DIR}"
+report "${SCRIPT_NAME}" "run" "start" "init" "ok" "role=${ROLE} profile=${PROFILE}" ""
+
+case "${ROLE}" in
+ lab|hardware|replacement) ;;
+ *) die "invalid role: ${ROLE}" ;;
+esac
+case "${PROFILE}" in
+ minimal|core|full) ;;
+ *) die "invalid profile: ${PROFILE}" ;;
+esac
+
+BASE_PACKAGES=(
+ ca-certificates curl wget gnupg2 jq rsync vim zsh sudo
+ locales tzdata lsb-release apt-transport-https
+ net-tools iproute2 iputils-ping dnsutils ethtool
+ openssh-server systemd-resolved nftables fail2ban
+)
+
+CORE_PACKAGES=(
+ wireguard wireguard-tools dnsmasq unbound nginx mariadb-server postfix prosody
+ tor i2pd pygopherd mumble-server prometheus prometheus-node-exporter loki
+ apparmor apparmor-utils
+)
+
+FULL_EXTRA_PACKAGES=(
+ git tmux htop build-essential pkg-config
+ docker.io
+)
+
+bootstrap_dns_prepare() {
+ local test_host="${BOOTSTRAP_TEST_HOST:-deb.debian.org}"
+ local backup="/etc/resolv.conf.labunix-preinstall.bak"
+
+ if getent ahostsv4 "${test_host}" >/dev/null 2>&1; then
+ report "${SCRIPT_NAME}" "dns" "bootstrap" "check" "ok" "resolver already working" ""
+ return 0
+ fi
+
+ if ! ping -c 1 -W 3 1.1.1.1 >/dev/null 2>&1; then
+ note_failure "${SCRIPT_NAME}" "dns" "bootstrap" "check" "no outbound IP connectivity"
+ die "bootstrap DNS check failed: no outbound IP connectivity"
+ fi
+
+ if command -v dig >/dev/null 2>&1 && dig +time=2 +tries=1 +short @1.1.1.1 "${test_host}" >/dev/null 2>&1; then
+ [[ -e /etc/resolv.conf && ! -e "${backup}" ]] && cp -a /etc/resolv.conf "${backup}" || true
+ printf 'nameserver 1.1.1.1
+nameserver 9.9.9.9
+' > /etc/resolv.conf
+ chmod 644 /etc/resolv.conf || true
+
+ if getent ahostsv4 "${test_host}" >/dev/null 2>&1; then
+ report "${SCRIPT_NAME}" "dns" "bootstrap" "fallback" "changed" "static resolv.conf applied" "${backup}"
+ return 0
+ fi
+ fi
+
+ note_failure "${SCRIPT_NAME}" "dns" "bootstrap" "check" "DNS still broken after fallback attempt"
+ die "bootstrap DNS check failed"
+}
+
+install_pkg_group() {
+ local group_name="$1"; shift
+ local pkgs=("$@")
+ log "installing package group: ${group_name}"
+ if apt_install_if_missing "${pkgs[@]}"; then
+ report "${SCRIPT_NAME}" "packages" "${group_name}" "install" "ok" "group processed" ""
+ else
+ note_failure "${SCRIPT_NAME}" "packages" "${group_name}" "install" "apt install failed"
+ die "package group failed: ${group_name}"
+ fi
+}
+
+apply_hostname_locale_timezone() {
+ if [[ -n ${HOSTNAME_WANTED} ]]; then
+ if [[ ${DRY_RUN} == true ]]; then
+ printf '[dry] hostnamectl set-hostname %s\n' "${HOSTNAME_WANTED}"
+ else
+ hostnamectl set-hostname "${HOSTNAME_WANTED}"
+ printf '%s\n' "${HOSTNAME_WANTED}" > /etc/hostname
+ fi
+ report "${SCRIPT_NAME}" "system" "hostname" "set" "changed" "${HOSTNAME_WANTED}" ""
+ fi
+
+ if [[ -f /etc/locale.gen ]] && grep -Eq "^#?\s*${LOCALE_WANTED}\b" /etc/locale.gen 2>/dev/null; then
+ if [[ ${DRY_RUN} == true ]]; then
+ printf '[dry] enable locale %s in /etc/locale.gen\n' "${LOCALE_WANTED}"
+ else
+ if have_cmd locale-gen && have_cmd update-locale; then
+ sed -i "s/^# *\(${LOCALE_WANTED}\b.*\)/\1/" /etc/locale.gen
+ locale-gen "${LOCALE_WANTED}" >/dev/null
+ update-locale LANG="${LOCALE_WANTED}" >/dev/null
+ else
+ report "${SCRIPT_NAME}" "system" "locale" "manual" "manual" "locale tools missing; install locales first or rerun after base packages" ""
+ return 0
+ fi
+ fi
+ report "${SCRIPT_NAME}" "system" "locale" "set" "changed" "${LOCALE_WANTED}" ""
+ else
+ report "${SCRIPT_NAME}" "system" "locale" "manual" "manual" "locale missing in /etc/locale.gen: ${LOCALE_WANTED}" ""
+ fi
+
+ if [[ ${DRY_RUN} == true ]]; then
+ printf '[dry] set timezone %s\n' "${TIMEZONE_WANTED}"
+ else
+ ln -sf "/usr/share/zoneinfo/${TIMEZONE_WANTED}" /etc/localtime
+ dpkg-reconfigure -f noninteractive tzdata >/dev/null
+ fi
+ report "${SCRIPT_NAME}" "system" "timezone" "set" "changed" "${TIMEZONE_WANTED}" ""
+}
+
+restore_apt_sources_from_db() {
+ local src="${PUBLIC_DIR}/packages/apt"
+ [[ -d ${src} ]] || { mark_manual "${SCRIPT_NAME}" "apt" "sources" "db/public/packages/apt missing"; return 0; }
+
+ if ! prompt_yes_no "Restore APT sources from DB now?" no; then
+ report "${SCRIPT_NAME}" "apt" "sources" "restore" "skipped" "operator skipped" ""
+ return 0
+ fi
+
+ restore_path "${SCRIPT_NAME}" "apt" "sources.list" "${src}/sources.list" "/etc/apt/sources.list"
+ restore_path "${SCRIPT_NAME}" "apt" "sources.list.d" "${src}/sources.list.d" "/etc/apt/sources.list.d"
+ restore_path "${SCRIPT_NAME}" "apt" "preferences" "${src}/preferences" "/etc/apt/preferences"
+ restore_path "${SCRIPT_NAME}" "apt" "preferences.d" "${src}/preferences.d" "/etc/apt/preferences.d"
+ restore_path "${SCRIPT_NAME}" "apt" "apt.conf.d" "${src}/apt.conf.d" "/etc/apt/apt.conf.d"
+
+ if [[ ${DRY_RUN} == true ]]; then
+ printf '[dry] apt-get update\n'
+ else
+ if apt-get update; then
+ report "${SCRIPT_NAME}" "apt" "update-after-restore" "update" "ok" "apt metadata refreshed" ""
+ else
+ note_failure "${SCRIPT_NAME}" "apt" "update-after-restore" "update" "apt-get update failed after restoring sources"
+ fi
+ fi
+}
+
+package_available() {
+ local pkg="$1"
+ apt-cache show -- "${pkg}" >/dev/null 2>&1
+}
+
+install_from_db_manual() {
+ local f="${PUBLIC_DIR}/packages/apt-mark-manual.txt" pkg installed=0 skipped=0 failed=0
+ [[ -f ${f} ]] || { mark_manual "${SCRIPT_NAME}" "packages" "db-manual" "apt-mark-manual.txt missing"; return 0; }
+
+ while IFS= read -r pkg; do
+ [[ -n ${pkg} ]] || continue
+ [[ ${pkg} =~ ^# ]] && continue
+ if dpkg -s -- "${pkg}" >/dev/null 2>&1; then
+ ((installed+=1))
+ continue
+ fi
+ if ! package_available "${pkg}"; then
+ ((skipped+=1))
+ report "${SCRIPT_NAME}" "packages" "${pkg}" "install" "skipped" "package not available in current apt sources" ""
+ continue
+ fi
+ if [[ ${DRY_RUN} == true ]]; then
+ printf '[dry] apt-get install -y --no-install-recommends %s\n' "${pkg}"
+ ((installed+=1))
+ continue
+ fi
+ if DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "${pkg}"; then
+ ((installed+=1))
+ report "${SCRIPT_NAME}" "packages" "${pkg}" "install" "changed" "installed from apt-mark-manual" ""
+ else
+ ((failed+=1))
+ note_failure "${SCRIPT_NAME}" "packages" "${pkg}" "install" "apt install failed"
+ fi
+ done < <(list_from_file "${f}")
+
+ report "${SCRIPT_NAME}" "packages" "db-manual-summary" "install" "ok" "installed=${installed} skipped=${skipped} failed=${failed}" ""
+}
+normalize_service_baseline() {
+ local keep_active=(
+ ssh.service
+ systemd-resolved.service
+ fail2ban.service
+ )
+
+ local stop_disable=(
+ dnsmasq.service
+ unbound.service
+ unbound-resolvconf.service
+ nginx.service
+ mariadb.service
+ postfix.service
+ prosody.service
+ prometheus.service
+ tor.service
+ i2pd.service
+ mumble-server.service
+ pygopherd.service
+ )
+
+ local unit
+
+ for unit in "${stop_disable[@]}"; do
+ if systemctl list-unit-files --type=service --no-legend --no-pager 2>/dev/null | awk '{print $1}' | grep -qx -- "${unit}"; then
+ systemctl disable --now "${unit}" >/dev/null 2>&1 || true
+ fi
+ done
+
+ systemctl reset-failed "${stop_disable[@]}" >/dev/null 2>&1 || true
+
+ for unit in "${keep_active[@]}"; do
+ if systemctl list-unit-files --type=service --no-legend --no-pager 2>/dev/null | awk '{print $1}' | grep -qx -- "${unit}"; then
+ systemctl enable "${unit}" >/dev/null 2>&1 || true
+ systemctl restart "${unit}" >/dev/null 2>&1 || true
+ fi
+ done
+
+ report "${SCRIPT_NAME}" "services" "baseline" "normalize" "changed" "safe services kept; app/conflicting services stopped" ""
+}
+start_safe_services() {
+ local svc
+ for svc in ssh systemd-resolved fail2ban; do
+ systemctl_safe_enable "${svc}" || true
+ systemctl_safe_start "${svc}" || true
+ done
+
+ case "${DNS_MODE}" in
+ resolved)
+ systemctl_safe_enable systemd-resolved || true
+ systemctl_safe_start systemd-resolved || true
+ ;;
+ esac
+ report "${SCRIPT_NAME}" "services" "safe-start" "start" "ok" "safe services processed" ""
+}
+
+if [[ ${DRY_RUN} == false ]]; then
+ bootstrap_dns_prepare
+ if apt-get update; then
+ report "${SCRIPT_NAME}" "apt" "update" "update" "ok" "initial apt update" ""
+ else
+ note_failure "${SCRIPT_NAME}" "apt" "update" "update" "initial apt-get update failed"
+ die "initial apt-get update failed"
+ fi
+else
+ report "${SCRIPT_NAME}" "apt" "update" "update" "ok" "initial apt update (dry-run)" ""
+fi
+
+install_pkg_group base "${BASE_PACKAGES[@]}"
+case "${PROFILE}" in
+ minimal) ;;
+ core) install_pkg_group core "${CORE_PACKAGES[@]}" ;;
+ full)
+ install_pkg_group core "${CORE_PACKAGES[@]}"
+ install_pkg_group full-extra "${FULL_EXTRA_PACKAGES[@]}"
+ ;;
+esac
+
+normalize_service_baseline
+apply_hostname_locale_timezone
+
+if [[ ${USE_DB_APT_SOURCES} == true ]]; then
+ restore_apt_sources_from_db
+fi
+
+if [[ ${FROM_DB_MANUAL} == true ]]; then
+ install_from_db_manual
+fi
+
+install -d -m 0755 /srv /srv/www /srv/docs /etc/wireguard /etc/labunix /var/lib/labunix
+report "${SCRIPT_NAME}" "filesystem" "base-dirs" "create" "ok" "/srv /etc/wireguard /etc/labunix prepared" ""
+
+if [[ ${START_SERVICES} == true ]]; then
+ start_safe_services
+else
+ report "${SCRIPT_NAME}" "services" "safe-start" "start" "skipped" "--start-safe-services not requested" ""
+fi
+
+set_state install_base done
+report "${SCRIPT_NAME}" "run" "finish" "exit" "ok" "completed" ""
+log "${SCRIPT_NAME}: done; report=${REPORT_FILE}"