diff --git a/apps/cashup.sh b/apps/cashup.sh new file mode 100755 index 0000000..c02db29 --- /dev/null +++ b/apps/cashup.sh @@ -0,0 +1,318 @@ +#!/usr/bin/env bash +# ┌─────────────────────────────────────────────────────────────────────────┐ +# │ Hotel Manage — Cashup App Installer │ +# │ Provisions a new LXC for the cashup app and wires it into the stack. │ +# │ │ +# │ Run on the Proxmox host shell: │ +# │ bash <(curl -fsSL https://git.pterois.co.uk/hotel-manage-stack/stack-init/raw/branch/main/apps/cashup.sh) +# └─────────────────────────────────────────────────────────────────────────┘ +set -euo pipefail + +YW="\033[33m"; RD="\033[01;31m" +GN="\033[1;92m"; DGN="\033[32m"; BL="\033[36m"; CL="\033[m" +BFR="\\r\\033[K"; CM="${GN}✓${CL}"; CROSS="${RD}✗${CL}" + +msg_info() { printf " ◌ ${YW}%-55s${CL}" "$*"; } +msg_ok() { printf "${BFR} ${CM} ${DGN}%s${CL}\n" "$*"; } +msg_error() { printf "${BFR} ${CROSS} ${RD}%s${CL}\n" "$*"; exit 1; } +msg_warn() { printf "\n ${CROSS} ${YW}%s${CL}\n" "$*"; } +msg_step() { printf "\n${BL}── %s ────────────────────────────────────${CL}\n" "$*"; } + +header_info() { + clear + printf "${BL}" + cat <<'BANNER' + ╔══════════════════════════════════════════════════════════════╗ + ║ HOTEL MANAGE — CASHUP APP INSTALLER ║ + ║ Daily cash reconciliation & Newbook PMS integration ║ + ╚══════════════════════════════════════════════════════════════╝ +BANNER + printf "${CL}\n" +} + +# ── Pre-flight ──────────────────────────────────────────────────────────────── +[[ $EUID -ne 0 ]] && msg_error "Must run as root on the Proxmox VE host" +command -v pct &>/dev/null || msg_error "pct not found — run this on a Proxmox VE host" +command -v whiptail &>/dev/null || { apt-get install -y -qq whiptail &>/dev/null; } +command -v openssl &>/dev/null || { apt-get install -y -qq openssl &>/dev/null; } + +# When run via `bash <(curl ...)` BASH_SOURCE is a pipe +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd || echo /tmp)" + +# ── Load existing stack credentials ────────────────────────────────────────── +CREDS_FILE=/root/hotel-manage-credentials.txt +[[ -f "$CREDS_FILE" ]] || msg_error "Stack credentials not found at ${CREDS_FILE} — run install-stack.sh first" + +# shellcheck disable=SC1090 +set -a; source <(grep -v '^#' "$CREDS_FILE" | grep '='); set +a + +[[ -z "${CENTRAL_AUTH_SECRET:-}" ]] && msg_error "CENTRAL_AUTH_SECRET missing from ${CREDS_FILE}" + +# ── App constants ───────────────────────────────────────────────────────────── +APP_NAME=cashup +APP_PORT=3083 +APP_PATH=/cashup/ +APP_DB_NAME=cashup_db +APP_DB_USER=cashup + +# ── Storage pool ────────────────────────────────────────────────────────────── +STORAGE=$(pvesm status 2>/dev/null | awk '{print $1}' | grep -E "^local-lvm$|^local-zfs$" | head -1) +STORAGE="${STORAGE:-local}" + +# ── Collect config ──────────────────────────────────────────────────────────── +header_info + +LXC_ID=$(whiptail --title "Cashup — LXC Config" \ + --inputbox "LXC ID to allocate (check Proxmox for free IDs):" 8 52 "116" \ + 3>&1 1>&2 2>&3) || exit 0 + +LAST_OCTET=$(whiptail --title "Cashup — LXC Config" \ + --inputbox "Internal IP last octet (10.10.10.X):" 8 52 "${LXC_ID}" \ + 3>&1 1>&2 2>&3) || exit 0 + +FORGEJO_BASE="${FORGEJO_BASE:-https://git.pterois.co.uk/hotel-manage-stack}" + +if whiptail --title "Cashup — Deploy Source" --yesno \ + "Deploy from Forgejo? (recommended)\n\n ${FORGEJO_BASE}/cashup.git\n\nNo = copy from local repo on this host" \ + 10 66; then + USE_FORGEJO=true +else + USE_FORGEJO=false + # When run locally the cashup dir sits two levels up from stack-init/apps/ + LOCAL_SRC="$(cd "${SCRIPT_DIR}/../../cashup" 2>/dev/null && pwd || echo '')" + LOCAL_SRC=$(whiptail --title "Cashup — Local Source" \ + --inputbox "Path to cashup app directory on this host:" 8 70 "${LOCAL_SRC}" \ + 3>&1 1>&2 2>&3) || exit 0 + [[ -d "$LOCAL_SRC" ]] || msg_error "Directory not found: ${LOCAL_SRC}" +fi + +whiptail --title "Cashup — Confirm" --yesno \ +"Create cashup LXC: + + LXC ID: ${LXC_ID} + IP: 10.10.10.${LAST_OCTET} + Port: ${APP_PORT} + Path: ${APP_PATH} + Database: ${APP_DB_NAME} + Source: $( [[ "$USE_FORGEJO" == "true" ]] && echo "Forgejo" || echo "$LOCAL_SRC" ) + +Proceed?" 18 52 || exit 0 + +# ── Generate DB password ────────────────────────────────────────────────────── +# Reuse if already in creds file (idempotent re-runs) +if grep -q "^CASHUP_DB_PASS=" "$CREDS_FILE" 2>/dev/null; then + APP_DB_PASS=$(grep "^CASHUP_DB_PASS=" "$CREDS_FILE" | cut -d= -f2) + msg_ok "Reusing existing cashup DB password" +else + APP_DB_PASS=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24) + cat >> "$CREDS_FILE" </dev/null | awk '/ubuntu-22\.04-standard/{print $1; exit}') + if [[ -z "$tmpl" ]]; then + msg_info "Downloading Ubuntu 22.04 LXC template" + pveam update &>/dev/null || true + local avail + avail=$(pveam available --section system 2>/dev/null \ + | awk '/ubuntu-22\.04-standard/{print $2}' | sort -V | tail -1) + [[ -n "$avail" ]] || msg_error "No ubuntu-22.04 template available via pveam" + pveam download local "$avail" &>/dev/null + msg_ok "Template downloaded" + tmpl="$avail" + fi + echo "local:vztmpl/${tmpl##*/}" +} + +# ── Helper: push file from stdin ────────────────────────────────────────────── +push_file() { + local id=$1 dest=$2; shift 2 + local tmp; tmp=$(mktemp /tmp/hotel-manage-push-XXXX) + cat > "$tmp" + pct push "$id" "$tmp" "$dest" 2>/dev/null + rm -f "$tmp" +} + +# ── PHASE 1 — Create LXC ────────────────────────────────────────────────────── +msg_step "1/5 LXC ${LXC_ID} (10.10.10.${LAST_OCTET})" + +TMPL=$(get_template) +POOL="hotel-manage" +POOL_OPT="" +pvesh get "/pools/${POOL}" &>/dev/null && POOL_OPT="--pool ${POOL}" + +if pct status "$LXC_ID" &>/dev/null; then + msg_warn "LXC ${LXC_ID} already exists — skipping creation" + pct status "$LXC_ID" | grep -q running || pct start "$LXC_ID" +else + msg_info "Creating LXC ${LXC_ID}" + pct create "$LXC_ID" "$TMPL" \ + --hostname "hotel-manage-${APP_NAME}" \ + --memory 1024 \ + --cores 1 \ + --rootfs "${STORAGE}:8" \ + --net0 "name=eth0,bridge=vmbr1,ip=10.10.10.${LAST_OCTET}/24,gw=10.10.10.1" \ + --features nesting=1 \ + --unprivileged 0 \ + --onboot 1 \ + --tags "${POOL}" \ + ${POOL_OPT} \ + --start 1 &>/dev/null + sleep 5 + msg_ok "LXC ${LXC_ID} created" +fi + +# ── PHASE 2 — Install Docker ────────────────────────────────────────────────── +msg_step "2/5 Docker" +msg_info "Installing Docker in LXC ${LXC_ID}" +pct exec "$LXC_ID" -- bash -s &>/dev/null <<'DOCKER' +export DEBIAN_FRONTEND=noninteractive +if [ -e /usr/sbin/apparmor_parser ]; then + mv -f /usr/sbin/apparmor_parser /usr/sbin/apparmor_parser.disabled +fi +if command -v docker >/dev/null 2>&1; then + systemctl enable --now docker ssh 2>/dev/null; exit 0 +fi +apt-get update -qq +apt-get install -y -qq ca-certificates curl gnupg git openssh-server +install -m 0755 -d /etc/apt/keyrings +rm -f /etc/apt/keyrings/docker.gpg +curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ + | gpg --batch --yes --dearmor -o /etc/apt/keyrings/docker.gpg +echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] \ +https://download.docker.com/linux/ubuntu jammy stable" \ + > /etc/apt/sources.list.d/docker.list +apt-get update -qq +apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-compose-plugin +systemctl enable --now docker ssh +DOCKER +msg_ok "Docker installed" + +# Install management SSH key so the updater can pull later +if [[ -f /root/.ssh/hotel-manage_deploy.pub ]]; then + MGMT_PUBKEY=$(cat /root/.ssh/hotel-manage_deploy.pub) + pct exec "$LXC_ID" -- bash -c " + mkdir -p /root/.ssh && chmod 700 /root/.ssh + grep -qF '${MGMT_PUBKEY}' /root/.ssh/authorized_keys 2>/dev/null || \ + echo '${MGMT_PUBKEY}' >> /root/.ssh/authorized_keys + chmod 600 /root/.ssh/authorized_keys + " &>/dev/null + msg_ok "Management SSH key installed" +fi + +# ── PHASE 3 — Create database ───────────────────────────────────────────────── +msg_step "3/5 Database" +msg_info "Creating ${APP_DB_NAME} on postgres LXC 100" +pct exec 100 -- bash -c " + docker exec hotel-manage-postgres psql -U postgres -c \ + \"CREATE USER ${APP_DB_USER} WITH PASSWORD '${APP_DB_PASS}';\" 2>/dev/null || true + docker exec hotel-manage-postgres psql -U postgres -c \ + \"CREATE DATABASE ${APP_DB_NAME} OWNER ${APP_DB_USER};\" 2>/dev/null || true + docker exec hotel-manage-postgres psql -U postgres -d ${APP_DB_NAME} -c \ + \"GRANT ALL ON SCHEMA public TO ${APP_DB_USER};\" 2>/dev/null || true +" &>/dev/null +msg_ok "Database ${APP_DB_NAME} ready" + +# ── PHASE 4 — Deploy app ────────────────────────────────────────────────────── +msg_step "4/5 Deploy" +msg_info "Deploying cashup to LXC ${LXC_ID}" + +if [[ "$USE_FORGEJO" == "true" ]]; then + CLONE_URL="${FORGEJO_BASE}/cashup.git" + if [[ -n "${FORGEJO_TOKEN:-}" ]]; then + CLONE_URL="${CLONE_URL/https:\/\//https://oauth2:${FORGEJO_TOKEN}@}" + fi + pct exec "$LXC_ID" -- bash -c " + if [ -d /opt/cashup/.git ]; then + cd /opt/cashup && git pull -q + else + git clone -q '${CLONE_URL}' /opt/cashup + fi + " &>/dev/null +else + tmp=$(mktemp /tmp/hotel-manage-cashup-XXXX.tar.gz) + tar czf "$tmp" -C "$(dirname "$LOCAL_SRC")" "$(basename "$LOCAL_SRC")" 2>/dev/null + pct push "$LXC_ID" "$tmp" /tmp/hotel-manage-cashup.tar.gz 2>/dev/null + pct exec "$LXC_ID" -- bash -c " + mkdir -p /opt + tar xzf /tmp/hotel-manage-cashup.tar.gz -C /opt + mv /opt/$(basename "$LOCAL_SRC") /opt/cashup 2>/dev/null || true + rm -f /tmp/hotel-manage-cashup.tar.gz + " &>/dev/null + rm -f "$tmp" +fi + +# Write .env +push_file "$LXC_ID" /opt/cashup/.env </dev/null +msg_ok "Containers started" + +msg_info "Waiting for cashup to be healthy" +ATTEMPTS=0 +until pct exec "$LXC_ID" -- curl -sf --max-time 2 "http://localhost:${APP_PORT}/cashup/api/health" &>/dev/null; do + ATTEMPTS=$((ATTEMPTS + 1)) + if [[ $ATTEMPTS -ge 40 ]]; then + msg_warn "Health check timed out — app may still be starting, check: pct enter ${LXC_ID} && docker compose logs" + break + fi + sleep 3 +done +[[ $ATTEMPTS -lt 40 ]] && msg_ok "Cashup healthy at 10.10.10.${LAST_OCTET}:${APP_PORT}" + +# ── Summary ─────────────────────────────────────────────────────────────────── +printf "\n${GN}" +cat <<'BANNER' + ╔══════════════════════════════════════════════════════════════╗ + ║ Cashup — Deployed ║ + ╚══════════════════════════════════════════════════════════════╝ +BANNER +printf "${CL}" + +cat <} + Events: Push + + Credentials saved to: ${CREDS_FILE} + +SUMMARY