#!/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: # -.srvno.de e.g. sto-1.srvno.de # from which we derive: # Node ID = - 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 /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=- (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 - (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: ┃ + + ┃ _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"