Files
automations/scripts/setup-host.sh
57_Wolve 7faa9098de feat: unified launcher, multi-OS hardening, login alerts & auto-updates
Restructure around a single entry point (automations.sh) with a Gum wizard and
a self-extracting bundle for repo-less installs. Add scripts/oslib.sh so the
provisioning scripts (setup-host, harden-ssh, harden-jumphost, sshuser) run on
Alpine/Debian/Alma; seed root keys from globals/.

- ntfy SSH-login alerts (user, source IP, key, region, jump target) via pam_exec
- daily auto-updates with AUTO_REBOOT=idle (reboots only when no SSH active) and
  opt-in Alpine stable-branch upgrades
- generic + per-deployment cloud-init; Gitea release workflow on tag
- README/LICENSE/.gitignore/.gitattributes (force LF); repo URLs -> Gitea
2026-06-12 14:56:02 -05:00

167 lines
6.3 KiB
Bash

#!/usr/bin/env bash
#
# setup-host.sh -- set a host's name (per the Network Domain Name Schema) and
# install the shared SSH MOTD banner. Works on Alpine, Debian, and Alma via
# oslib.sh.
#
# Hostnames follow globals/Network Domain Name Schema.md. Our VMs skip the
# region code and use srvno.de as the base, so the form is:
# <service>-<n>.srvno.de e.g. sto-1.srvno.de
# from which we derive:
# Node ID = <SERVICE>-<N> e.g. STO-1 (short name, uppercased)
#
# The MOTD is rendered from globals/motd.txt. You edit the *content* there;
# this script draws the borders and computes every bit of padding, so the box
# stays aligned regardless of value length (the spacing problem, solved).
#
# Usage:
# bash setup-host.sh sto-1 # -> sto-1.srvno.de, Node ID STO-1
# HOST=dns-1 bash setup-host.sh
# HOST=web-2.srvno.de bash setup-host.sh # full FQDN accepted too
# BASE_DOMAIN=example.net HOST=app-1 bash setup-host.sh
# DATACENTER="Stockholm SE" HOST=sto-1 bash setup-host.sh
#
# Env:
# HOST required short name (sto-1) or FQDN (sto-1.srvno.de)
# BASE_DOMAIN srvno.de appended when HOST has no dot
# DATACENTER "Globally Everywhere" shown in the MOTD
# MOTD_TEMPLATE <repo>/globals/motd.txt
# MOTD_WIDTH 60 inner width of the MOTD box
# SET_HOSTNAME 1 set to 0 to render MOTD only (skip hostname)
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck source=scripts/oslib.sh
. "$REPO_ROOT/scripts/oslib.sh"
: "${HOST:=${1:-}}"
: "${BASE_DOMAIN:=srvno.de}"
: "${DATACENTER:=Globally Everywhere}"
: "${MOTD_TEMPLATE:=$REPO_ROOT/globals/motd.txt}"
: "${MOTD_WIDTH:=60}"
: "${MOTD_OUT:=/etc/motd}"
: "${SET_HOSTNAME:=1}"
[[ $EUID -eq 0 ]] || _die "Run as root."
[[ -n "$HOST" ]] || _die "Set HOST=<service>-<n> (e.g. sto-1) or a full FQDN."
os_detect
# ----------------------------------------------------------------------------
# Derive FQDN / short name / Node ID from HOST
# ----------------------------------------------------------------------------
if [[ "$HOST" == *.* ]]; then
FQDN="$HOST"; SHORT="${HOST%%.*}"
else
SHORT="$HOST"; FQDN="$HOST.$BASE_DOMAIN"
fi
NODE_ID="$(printf '%s' "$SHORT" | tr '[:lower:]' '[:upper:]')"
# Light sanity check against the schema (svc code + instance number). Warn
# only -- we don't want to block an intentional exception.
[[ "$SHORT" =~ ^[a-z]{2,4}-[0-9]+$ ]] \
|| _warn "Short name '$SHORT' doesn't match <svc>-<n> (see globals/Network Domain Name Schema.md)."
_log "Host: $FQDN"
_log "Node ID: $NODE_ID"
_log "Data Cntr: $DATACENTER"
# ----------------------------------------------------------------------------
# Set hostname (OS-gated inside oslib)
# ----------------------------------------------------------------------------
if [[ "$SET_HOSTNAME" == "1" ]]; then
_log "Setting hostname to $FQDN ..."
set_hostname "$FQDN"
fi
# ============================================================================
# MOTD renderer
# ============================================================================
# All padding is computed in ASCII (spaces, labels, values are ASCII so
# ${#str} is the column count). Box-drawing glyphs are only ever emitted by
# repetition, never measured -- so this is correct under any shell/locale.
W="$MOTD_WIDTH"
_spaces() { printf '%*s' "$1" ''; } # N ASCII spaces
_repeat() { local i; for ((i=0;i<$2;i++)); do printf '%s' "$1"; done; } # glyph xN
# Bordered content line: ┃ + <inner W cols> + ┃
_line() { printf '┃%s┃\n' "$1"; }
# Center an ASCII string within W.
_center() {
local s="$1" len=${#1} pad left
(( len > W )) && { s="${s:0:W}"; len=$W; }
pad=$(( W - len )); left=$(( pad / 2 ))
_line "$(_spaces "$left")$s$(_spaces $(( pad - left )))"
}
# Center a pre-built string of KNOWN visible width vis (may contain glyphs).
_center_known() {
local s="$1" vis="$2" pad left
pad=$(( W - vis )); left=$(( pad / 2 ))
_line "$(_spaces "$left")$s$(_spaces $(( pad - left )))"
}
# Field line: left margin, right-aligned label, ": ", value, pad to W.
LM=8 # left margin before the label column
_field() {
local label="$1" value="$2" content
content="$(printf '%*s%*s: %s' "$LM" '' "$LABELW" "$label" "$value")"
local len=${#content}
(( len > W )) && { content="${content:0:W}"; len=$W; }
_line "$content$(_spaces $(( W - len )))"
}
# Mini double-line box around centered ASCII text, itself centered in W.
_inner_box() {
local text="$1" tw=$(( ${#1} + 2 )) vis
vis=$(( tw + 2 )) # ║ + text(+2 spaces) + ║
_center_known "╔$(_repeat '═' "$tw")╗" "$vis"
_center_known "║ $text ║" "$vis"
_center_known "╚$(_repeat '═' "$tw")╝" "$vis"
}
render_motd() {
# First pass: compute LABELW (max @F@ label width) so colons align.
LABELW=0
local line tok rest label
while IFS= read -r line || [[ -n "$line" ]]; do
[[ "$line" =~ ^[[:space:]]*# ]] && continue
[[ "$line" == "@F@ "* ]] || continue
rest="${line#@F@ }"; label="${rest%%|*}"
(( ${#label} > LABELW )) && LABELW=${#label}
done < "$MOTD_TEMPLATE"
# Second pass: emit. Borders come from the @TOP@/@HR@/@BOT@ tokens.
while IFS= read -r line || [[ -n "$line" ]]; do
[[ "$line" =~ ^[[:space:]]*# ]] && continue
[[ -z "$line" ]] && continue
tok="${line%% *}"; rest="${line#"$tok"}"; rest="${rest# }"
# substitute placeholders
rest="${rest//\{\{HOSTNAME\}\}/$FQDN}"
rest="${rest//\{\{NODE_ID\}\}/$NODE_ID}"
rest="${rest//\{\{DATACENTER\}\}/$DATACENTER}"
case "$tok" in
@TOP@) printf '┏%s┓\n' "$(_repeat '━' "$W")" ;;
@BOT@) printf '┗%s┛\n' "$(_repeat '━' "$W")" ;;
@HR@) printf '┣%s┫\n' "$(_repeat '━' "$W")" ;;
@BLANK@) _line "$(_spaces "$W")" ;;
@C@) _center "$rest" ;;
@BOX@) _inner_box "$rest" ;;
@F@) _field "${rest%%|*}" "${rest#*|}" ;;
*) _warn "Unknown MOTD token: $tok (skipped)" ;;
esac
done < "$MOTD_TEMPLATE"
}
[[ -f "$MOTD_TEMPLATE" ]] || _die "MOTD template not found: $MOTD_TEMPLATE"
_log "Rendering MOTD -> $MOTD_OUT"
render_motd > "$MOTD_OUT"
chmod 644 "$MOTD_OUT" 2>/dev/null || true
_log "Done."
echo
cat "$MOTD_OUT"