- Add storage_box role: generates SSH key pair, creates Hetzner Storage Box with known password, installs public key via install-ssh-key, writes storagebox.yml to stack config. Idempotent: skips key install if SSH key auth already works. - Add deploy.yml: one-shot playbook chaining provision → dns → storage_box → bootstrap → site for fresh deployments - Fix .envrc HCLOUD_TOKEN extraction stripping surrounding quotes from vault YAML values - Add restic_storagebox_password to vault template and setup.sh prompt - Add sshpass to README prerequisites Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
253 lines
9.6 KiB
Bash
Executable file
253 lines
9.6 KiB
Bash
Executable file
#!/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"
|
|
export stack_name
|
|
|
|
|
|
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
|
|
|
|
echo
|
|
info "configure restic backups (optional — leave blank to skip)"
|
|
prompt restic_storagebox_name "Storage box name" "${server_name}-backup"
|
|
prompt_secret restic_storagebox_password "Storage box password (leave blank to skip)"
|
|
|
|
export admin_user server_name server_ip domain hcloud_token restic_storagebox_name restic_storagebox_password
|
|
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 restic_storagebox_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" <<HOSTS_EOF
|
|
---
|
|
# ============================================================
|
|
# Linderhof Inventory — stack: $stack_name
|
|
# ============================================================
|
|
all:
|
|
hosts:
|
|
$server_name:
|
|
ansible_host: $server_ip
|
|
ansible_user: $admin_user
|
|
ansible_become: true
|
|
ansible_become_method: sudo
|
|
HOSTS_EOF
|
|
ok "hosts.yml created"
|
|
fi
|
|
|
|
# ── 9. write stack.env ───────────────────────────────────────
|
|
if [[ -f "$STACK_ENV" ]]; then
|
|
warn "stack.env already exists — skipping (not overwriting)"
|
|
else
|
|
info "writing stack.env..."
|
|
cat > "$STACK_ENV" <<ENV_EOF
|
|
# Per-stack environment variables — loaded by .envrc
|
|
export DOCKER_HOST="ssh://$admin_user@$server_name.$domain"
|
|
ENV_EOF
|
|
ok "stack.env created"
|
|
fi
|
|
|
|
# ── 10. write config.yml ──────────────────────────────────────
|
|
if [[ -f "$CONFIG" ]]; then
|
|
warn "config.yml already exists — skipping (not overwriting)"
|
|
else
|
|
info "writing config.yml..."
|
|
envsubst '$admin_user $server_name $server_ip $domain $ssh_key_pub $stack_name $restic_storagebox_name' \
|
|
< "$TEMPLATES/config.yml.setup" > "$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. Deploy: ansible-playbook playbooks/deploy.yml"
|
|
echo ""
|
|
echo " If mail is enabled, sync DKIM keys once the server is up:"
|
|
echo " ansible-playbook playbooks/dkim_sync.yml"
|