#!/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