#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" TEMPLATES="$SCRIPT_DIR/inventory/group_vars/all" # ── helpers ────────────────────────────────────────────────── info() { printf '\033[1;34m::\033[0m %s\n' "$*"; } ok() { printf '\033[1;32m::\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m::\033[0m %s\n' "$*"; } die() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; } prompt() { local var="$1" msg="$2" default="$3" printf '%s [%s]: ' "$msg" "$default" read -r input eval "$var=\"\${input:-$default}\"" } prompt_secret() { local var="$1" msg="$2" printf '%s: ' "$msg" read -rs input echo eval "$var=\"\$input\"" } # ── 1. check prerequisites ────────────────────────────────── info "checking prerequisites..." missing=() for cmd in ansible ansible-galaxy ssh-keygen openssl envsubst; do command -v "$cmd" &>/dev/null || missing+=("$cmd") done if (( ${#missing[@]} )); then die "missing required commands: ${missing[*]}" fi ok "all prerequisites found" # ── 2. install ansible collections ────────────────────────── info "installing ansible collections..." ansible-galaxy collection install -r "$SCRIPT_DIR/requirements.yml" ok "collections installed" # ── 3. stack name ──────────────────────────────────────────── echo info "stack setup" prompt stack_name "Stack name" "home" LINDERHOF_CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/linderhof" STACK_DIR="$LINDERHOF_CONFIG_DIR/$stack_name" GROUP_VARS="$STACK_DIR/group_vars/all" CONFIG="$GROUP_VARS/config.yml" VAULT="$GROUP_VARS/vault.yml" HOSTS="$STACK_DIR/hosts.yml" STACK_ENV="$STACK_DIR/stack.env" VAULT_PASS_FILE="$STACK_DIR/vault-pass" info "stack directory: $STACK_DIR" mkdir -p "$GROUP_VARS" # ── 4. SSH key ─────────────────────────────────────────────── prompt ssh_key_path "SSH key path" "$HOME/.ssh/id_ed25519" if [[ -f "$ssh_key_path" ]]; then ok "SSH key already exists at $ssh_key_path" else printf 'No key at %s. Generate one? [Y/n]: ' "$ssh_key_path" read -r yn if [[ "${yn,,}" != "n" ]]; then ssh-keygen -t ed25519 -f "$ssh_key_path" ok "SSH key generated" else warn "skipping SSH key generation" fi fi # ── 5. vault password ─────────────────────────────────────── if [[ -f "$VAULT_PASS_FILE" ]]; then ok "vault password file already exists at $VAULT_PASS_FILE" else info "generating vault password..." openssl rand -base64 32 > "$VAULT_PASS_FILE" chmod 600 "$VAULT_PASS_FILE" ok "vault password saved to $VAULT_PASS_FILE" fi export ANSIBLE_VAULT_PASSWORD_FILE="$VAULT_PASS_FILE" # ── 6. server settings ─────────────────────────────────────── echo info "configure your server" prompt admin_user "Admin username" "$USER" prompt server_name "Server hostname" "$stack_name" prompt server_ip "Server IP (leave TBD if provisioning via Hetzner)" "TBD" prompt domain "Domain" "example.com" prompt_secret hcloud_token "Hetzner API token (leave blank to skip)" if [[ -z "$hcloud_token" ]]; then warn "no Hetzner token provided — add it to vault.yml manually if needed" fi export admin_user server_name server_ip domain hcloud_token export ssh_key_pub="${ssh_key_path}.pub" echo info "using domain: $domain" info " mail: mail.$domain" info " forgejo: code.$domain" info " grafana: watch.$domain" info " tuwunel: chat.$domain" info " webmail: webmail.$domain" info " rspamd: rspamd.$domain" # ── 7. generate secrets ───────────────────────────────────── info "generating secrets..." export root_password admin_password export admin_mail_password notifications_mail_password git_mail_password export grafana_admin_password rspamd_web_password goaccess_password rainloop_admin_password radicale_password export tuwunel_registration_token restic_password export forgejo_secret_key forgejo_internal_token forgejo_jwt_secret root_password=$(openssl rand -base64 32) admin_password=$(openssl rand -base64 32) admin_mail_password=$(openssl rand -base64 32) notifications_mail_password=$(openssl rand -base64 32) git_mail_password=$(openssl rand -base64 32) grafana_admin_password=$(openssl rand -base64 32) rspamd_web_password=$(openssl rand -base64 32) goaccess_password=$(openssl rand -base64 32) radicale_password=$(openssl rand -base64 32) rainloop_admin_password=$(openssl rand -base64 32) tuwunel_registration_token=$(openssl rand -base64 32) restic_password=$(openssl rand -base64 32) forgejo_secret_key=$(openssl rand -hex 32) forgejo_internal_token=$(openssl rand -hex 32) forgejo_jwt_secret=$(openssl rand -hex 32) ok "secrets generated" # ── 8. write hosts.yml ─────────────────────────────────────── if [[ -f "$HOSTS" ]]; then warn "hosts.yml already exists — skipping (not overwriting)" else info "writing hosts.yml..." cat > "$HOSTS" < "$STACK_ENV" < "$CONFIG" ok "config.yml created" fi # ── 10b. write dns.yml ──────────────────────────────────────── DNS_CONFIG="$GROUP_VARS/dns.yml" if [[ -f "$DNS_CONFIG" ]]; then warn "dns.yml already exists — skipping (not overwriting)" else info "writing dns.yml..." envsubst '$domain $server_name' \ < "$TEMPLATES/dns.yml.setup" > "$DNS_CONFIG" ok "dns.yml created (uncomment DKIM records after first mail deployment)" fi # ── 11. write vault.yml ─────────────────────────────────────── if [[ -f "$VAULT" ]]; then warn "vault.yml already exists — skipping (not overwriting)" else info "writing vault.yml..." envsubst < "$TEMPLATES/vault.yml.setup" > "$VAULT" ansible-vault encrypt "$VAULT" ok "vault.yml created and encrypted" fi # ── 12. write .stack file ───────────────────────────────────── if [[ -f "$SCRIPT_DIR/.stack" ]]; then warn ".stack already exists — skipping (not overwriting)" else printf '%s\n' "$stack_name" > "$SCRIPT_DIR/.stack" ok ".stack file written ($stack_name)" fi # ── 13. summary ─────────────────────────────────────────────── echo echo "============================================================" ok "linderhof setup complete! (stack: $stack_name)" echo "============================================================" echo echo " stack dir: $STACK_DIR" echo " vault password: $VAULT_PASS_FILE" echo " SSH key: $ssh_key_path" echo " inventory: $HOSTS" echo " config: $CONFIG" echo " dns zones: $DNS_CONFIG" echo " vault: $VAULT" echo echo "to override any variable (e.g. mail_hostname during migration):" echo " vi $GROUP_VARS/overrides.yml" echo echo "activate the stack (if not already done):" echo " direnv allow # reads .stack file automatically" echo " # or: export LINDERHOF_STACK=$stack_name" echo echo "config.yml is plain text — edit it directly:" echo " vi $CONFIG" echo echo "vault.yml is encrypted. to view or edit it:" echo " ansible-vault view $VAULT" echo " ansible-vault edit $VAULT" echo echo "Next steps:" echo " 1. Review $CONFIG" echo " 2. Review $VAULT (ansible-vault edit)" echo " 3. Review $DNS_CONFIG" echo " 4. Provision a server: ansible-playbook playbooks/provision.yml" echo " 5. Update DNS: ansible-playbook playbooks/dns.yml" echo " 6. Deploy: ansible-playbook playbooks/site.yml" echo " 7. After mail deploys, retrieve DKIM keys and add to vault.yml:" echo " docker exec mailserver cat /tmp/docker-mailserver/rspamd/dkim/$domain/mail.pub"