2 GB RAM, 2 CPU, PostgreSQL rates_db. Mirrors deploy_forecasting pattern. Seeds rates app + 6 caps. Adds npm proxy route /rates/. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1748 lines
74 KiB
Bash
Executable file
1748 lines
74 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# ┌─────────────────────────────────────────────────────────────────────────┐
|
|
# │ Hotel Manage — Proxmox Stack Installer │
|
|
# │ Provisions: postgres · auth · portal · npm · management · noticeboard │
|
|
# │ │
|
|
# │ Run on the Proxmox host shell: │
|
|
# │ bash install-stack.sh │
|
|
# │ │
|
|
# │ Or from Forgejo once repos are pushed: │
|
|
# │ bash <(curl -fsSL https://git.pterois.co.uk/hotel-manage-stack/stack-init/raw/branch/main/install-stack.sh)
|
|
# └─────────────────────────────────────────────────────────────────────────┘
|
|
set -euo pipefail
|
|
|
|
# ── Colour helpers ────────────────────────────────────────────────────────────
|
|
YW="\033[33m"; BL="\033[36m"; RD="\033[01;31m"
|
|
GN="\033[1;92m"; DGN="\033[32m"; 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 — PROXMOX STACK INSTALLER ║
|
|
║ postgres · auth · portal · npm · mgmt · noticeboard · settings ║
|
|
╚══════════════════════════════════════════════════════════════╝
|
|
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 pvesm &>/dev/null || msg_error "pvesm 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; }
|
|
|
|
# Defensive — when run via `bash <(curl ...)` BASH_SOURCE is a pipe, not a file
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd || echo /tmp)"
|
|
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." 2>/dev/null && pwd || echo /tmp)"
|
|
|
|
header_info
|
|
|
|
# ── Detect storage pool ───────────────────────────────────────────────────────
|
|
detect_storage() {
|
|
if pvesm status 2>/dev/null | awk '{print $1}' | grep -q "^local-lvm$"; then
|
|
echo "local-lvm"
|
|
elif pvesm status 2>/dev/null | awk '{print $1}' | grep -q "^local-zfs$"; then
|
|
echo "local-zfs"
|
|
else
|
|
echo "local"
|
|
fi
|
|
}
|
|
STORAGE=$(detect_storage)
|
|
|
|
# ── Internal network: bridge + NAT ────────────────────────────────────────────
|
|
# The isolated vmbr1 needs outbound NAT so LXCs can apt-install Docker, git
|
|
# clone, and pull images. Persisted via an if-up.d hook so it survives reboot.
|
|
ensure_nat() {
|
|
local wan; wan=$(ip route show default 2>/dev/null | awk '{print $5; exit}'); wan=${wan:-vmbr0}
|
|
msg_info "Enabling internal NAT (10.10.10.0/24 → ${wan})"
|
|
sysctl -wq net.ipv4.ip_forward=1 2>/dev/null || echo 1 > /proc/sys/net/ipv4/ip_forward
|
|
echo 'net.ipv4.ip_forward=1' > /etc/sysctl.d/99-hotel-manage.conf
|
|
iptables -t nat -C POSTROUTING -s 10.10.10.0/24 ! -d 10.10.10.0/24 -o "$wan" -j MASQUERADE 2>/dev/null \
|
|
|| iptables -t nat -A POSTROUTING -s 10.10.10.0/24 ! -d 10.10.10.0/24 -o "$wan" -j MASQUERADE
|
|
cat > /etc/network/if-up.d/hotel-manage-nat <<'HOOK'
|
|
#!/bin/sh
|
|
[ "$IFACE" = "vmbr1" ] || exit 0
|
|
WAN=$(ip route show default | awk '{print $5; exit}'); WAN=${WAN:-vmbr0}
|
|
sysctl -w net.ipv4.ip_forward=1 >/dev/null 2>&1
|
|
iptables -t nat -C POSTROUTING -s 10.10.10.0/24 ! -d 10.10.10.0/24 -o "$WAN" -j MASQUERADE 2>/dev/null \
|
|
|| iptables -t nat -A POSTROUTING -s 10.10.10.0/24 ! -d 10.10.10.0/24 -o "$WAN" -j MASQUERADE
|
|
HOOK
|
|
chmod +x /etc/network/if-up.d/hotel-manage-nat
|
|
msg_ok "Internal NAT enabled (via ${wan})"
|
|
}
|
|
|
|
check_vmbr1() {
|
|
if ip link show vmbr1 &>/dev/null; then
|
|
ensure_nat # bridge exists — make sure NAT/forwarding is in place
|
|
return
|
|
fi
|
|
|
|
if whiptail --title "Create internal bridge vmbr1?" --yesno \
|
|
"The internal container bridge vmbr1 (10.10.10.1/24) doesn't exist yet.
|
|
|
|
Create it now? This adds an ISOLATED bridge with no physical ports,
|
|
so it cannot affect your LAN or vmbr0. It appends a stanza to
|
|
/etc/network/interfaces, applies it with ifreload -a, and enables
|
|
outbound NAT so the containers can reach the internet." 15 68; then
|
|
|
|
if grep -qE '^\s*iface\s+vmbr1' /etc/network/interfaces 2>/dev/null; then
|
|
msg_warn "vmbr1 already defined in /etc/network/interfaces — applying it"
|
|
else
|
|
cat >> /etc/network/interfaces <<'EOF'
|
|
|
|
auto vmbr1
|
|
iface vmbr1 inet static
|
|
address 10.10.10.1/24
|
|
bridge-ports none
|
|
bridge-stp off
|
|
bridge-fd 0
|
|
EOF
|
|
fi
|
|
msg_info "Bringing up vmbr1"
|
|
ifreload -a &>/dev/null || systemctl restart networking &>/dev/null || true
|
|
sleep 2
|
|
if ip link show vmbr1 &>/dev/null; then
|
|
msg_ok "vmbr1 up (10.10.10.1/24)"
|
|
else
|
|
msg_error "vmbr1 still not up — check /etc/network/interfaces and re-run"
|
|
fi
|
|
ensure_nat
|
|
else
|
|
whiptail --title "vmbr1 Missing" --msgbox \
|
|
"Cancelled. Add this to /etc/network/interfaces, run 'ifreload -a',
|
|
then re-run the installer:
|
|
|
|
auto vmbr1
|
|
iface vmbr1 inet static
|
|
address 10.10.10.1/24
|
|
bridge-ports none
|
|
bridge-stp off
|
|
bridge-fd 0" 16 66
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# ── Ensure Ubuntu 22.04 template ─────────────────────────────────────────────
|
|
ensure_template() {
|
|
# NOTE: progress goes to stderr — stdout is captured as the template volid.
|
|
local tmpl
|
|
tmpl=$(pveam list local 2>/dev/null | awk '/ubuntu-22\.04-standard/{print $1; exit}')
|
|
if [[ -z "$tmpl" ]]; then
|
|
msg_info "Downloading Ubuntu 22.04 LXC template" >&2
|
|
pveam update &>/dev/null || true
|
|
# Resolve the exact filename currently offered (pinned names go stale)
|
|
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" >&2; exit 1; }
|
|
pveam download local "$avail" &>/dev/null || { msg_error "Template download failed" >&2; exit 1; }
|
|
msg_ok "Template downloaded" >&2
|
|
tmpl="$avail"
|
|
fi
|
|
# Return full path for pct create
|
|
echo "local:vztmpl/${tmpl##*/}"
|
|
}
|
|
|
|
# ── Collect site config ───────────────────────────────────────────────────────
|
|
collect_config() {
|
|
SITE_NAME=$(whiptail --title "Hotel Manage — Site Config" \
|
|
--inputbox "Site name:" 8 52 "Hotel Number Four" 3>&1 1>&2 2>&3) || exit 0
|
|
|
|
DOMAIN=$(whiptail --title "Hotel Manage — Site Config" \
|
|
--inputbox "Public domain (e.g. manage.hotelnumberfour.com):" 8 64 "manage.hotelnumberfour.com" \
|
|
3>&1 1>&2 2>&3) || exit 0
|
|
|
|
NPM_LAN_IP=$(whiptail --title "Hotel Manage — Site Config" \
|
|
--inputbox "NPM LXC static LAN IP (from your hotel LAN pool):" 8 64 "10.4.0.50" \
|
|
3>&1 1>&2 2>&3) || exit 0
|
|
|
|
LAN_GW=$(whiptail --title "Hotel Manage — Site Config" \
|
|
--inputbox "LAN gateway IP:" 8 52 "10.4.0.1" 3>&1 1>&2 2>&3) || exit 0
|
|
|
|
OFFICE_IP=$(whiptail --title "Hotel Manage — Site Config" \
|
|
--inputbox \
|
|
"Onsite matcher(s) for offsite restriction — comma-separated, any match = onsite.
|
|
Each can be: an IP, a CIDR, a DDNS hostname, or 'auto' (self-detect the site's
|
|
public IP — no DDNS service needed). 'disabled' allows access from anywhere.
|
|
|
|
Recommended for a dynamic public IP: 10.4.0.0/22,auto
|
|
(LAN clients match the CIDR; public-IP/hairpin clients match auto)" \
|
|
13 72 "10.4.0.0/22,auto" 3>&1 1>&2 2>&3) || exit 0
|
|
|
|
ADMIN_EMAIL=$(whiptail --title "Hotel Manage — Admin Account" \
|
|
--inputbox "Admin user email:" 8 52 "" 3>&1 1>&2 2>&3) || exit 0
|
|
|
|
ADMIN_PASS=$(whiptail --title "Hotel Manage — Admin Account" \
|
|
--passwordbox "Admin user password:" 8 52 3>&1 1>&2 2>&3) || exit 0
|
|
|
|
# Deploy source — per-service Forgejo repos (default) or a local copy on this host
|
|
FORGEJO_BASE=$(whiptail --title "Hotel Manage — Forgejo" \
|
|
--inputbox \
|
|
"Forgejo org/base URL hosting the per-service repos.
|
|
Each service is cloned from <base>/<service>.git
|
|
→ auth portal management noticeboard
|
|
|
|
Example: https://git.pterois.co.uk/hotel-manage-stack" \
|
|
13 66 "https://git.pterois.co.uk/hotel-manage-stack" 3>&1 1>&2 2>&3) || exit 0
|
|
FORGEJO_BASE="${FORGEJO_BASE%/}"
|
|
|
|
FORGEJO_TOKEN=$(whiptail --title "Hotel Manage — Forgejo Token" \
|
|
--passwordbox \
|
|
"The repos are PUBLIC — leave this blank.
|
|
|
|
(Only needed if you make them private again: an access token
|
|
from Forgejo → Settings → Applications, scope read:repository,
|
|
which gets embedded in each LXC's git remote for the updater.)" \
|
|
12 66 3>&1 1>&2 2>&3) || exit 0
|
|
|
|
if whiptail --title "Hotel Manage — Deploy Source" --yesno \
|
|
"Deploy services from Forgejo? (recommended)\n\nNo = copy from a local repo at ${REPO_ROOT}\n(only works if you already copied the repo to this host)" \
|
|
11 62; then
|
|
USE_FORGEJO=true
|
|
else
|
|
USE_FORGEJO=false
|
|
fi
|
|
|
|
BACKUP_REMOTE=$(whiptail --title "Hotel Manage — Backup" \
|
|
--inputbox \
|
|
"Backup rsync target (leave blank to skip backup config).
|
|
Example: backup@192.168.1.10:/backups/hotel-manage" \
|
|
10 64 "" 3>&1 1>&2 2>&3) || exit 0
|
|
|
|
# Confirm LXC allocation
|
|
whiptail --title "Hotel Manage — Confirm" --yesno \
|
|
"LXCs to be created (storage: ${STORAGE}):
|
|
|
|
ID Hostname IP
|
|
──────────────────────────────────────────
|
|
100 hotel-manage-postgres 10.10.10.100
|
|
101 hotel-manage-auth 10.10.10.101
|
|
102 hotel-manage-portal 10.10.10.102
|
|
103 hotel-manage-npm 10.10.10.103 / ${NPM_LAN_IP} (dual-homed)
|
|
105 hotel-manage-management 10.10.10.105
|
|
112 hotel-manage-noticeboard 10.10.10.112
|
|
116 hotel-manage-settings 10.10.10.116
|
|
|
|
Domain: ${DOMAIN}
|
|
Admin: ${ADMIN_EMAIL}
|
|
|
|
Proceed?" 26 58 || exit 0
|
|
}
|
|
|
|
# ── Generate secrets ──────────────────────────────────────────────────────────
|
|
CREDS_FILE=/root/hotel-manage-credentials.txt
|
|
gen_secrets() {
|
|
# Resume-safe: if secrets already exist, reuse them so a re-run doesn't
|
|
# mismatch an already-initialised Postgres / already-deployed services.
|
|
if [[ -f "$CREDS_FILE" ]] && grep -q '^PG_SUPERPASS=' "$CREDS_FILE"; then
|
|
msg_info "Reusing existing secrets from $CREDS_FILE"
|
|
PG_SUPERPASS=$(sed -n 's/^PG_SUPERPASS=//p' "$CREDS_FILE" | head -1)
|
|
AUTH_DB_PASS=$(sed -n 's/^AUTH_DB_PASS=//p' "$CREDS_FILE" | head -1)
|
|
NOTICES_DB_PASS=$(sed -n 's/^NOTICES_DB_PASS=//p' "$CREDS_FILE" | head -1)
|
|
SETTINGS_DB_PASS=$(sed -n 's/^SETTINGS_DB_PASS=//p' "$CREDS_FILE" | head -1)
|
|
CASHUP_DB_PASS=$(sed -n 's/^CASHUP_DB_PASS=//p' "$CREDS_FILE" | head -1)
|
|
HK_PLANNER_DB_PASS=$(sed -n 's/^HK_PLANNER_DB_PASS=//p' "$CREDS_FILE" | head -1)
|
|
TWIN_OPT_DB_PASS=$(sed -n 's/^TWIN_OPT_DB_PASS=//p' "$CREDS_FILE" | head -1)
|
|
ROOM_PLANNER_DB_PASS=$(sed -n 's/^ROOM_PLANNER_DB_PASS=//p' "$CREDS_FILE" | head -1)
|
|
CENTRAL_AUTH_SECRET=$(sed -n 's/^CENTRAL_AUTH_SECRET=//p' "$CREDS_FILE" | head -1)
|
|
SETTINGS_SECRET=$(sed -n 's/^SETTINGS_SECRET=//p' "$CREDS_FILE" | head -1)
|
|
WEBHOOK_SECRET=$(sed -n 's/^WEBHOOK_SECRET=//p' "$CREDS_FILE" | head -1)
|
|
NPM_LAN_IP=$(sed -n 's/^NPM_LAN_IP=//p' "$CREDS_FILE" | head -1)
|
|
NPM_ADMIN_EMAIL=$(sed -n 's/^NPM_ADMIN_EMAIL=//p' "$CREDS_FILE" | head -1)
|
|
NPM_ADMIN_PASS=$(sed -n 's/^NPM_ADMIN_PASS=//p' "$CREDS_FILE" | head -1)
|
|
msg_ok "Reusing existing secrets"
|
|
return
|
|
fi
|
|
|
|
msg_info "Generating secrets"
|
|
PG_SUPERPASS=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)
|
|
AUTH_DB_PASS=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)
|
|
NOTICES_DB_PASS=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)
|
|
SETTINGS_DB_PASS=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)
|
|
CASHUP_DB_PASS=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)
|
|
HK_PLANNER_DB_PASS=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)
|
|
TWIN_OPT_DB_PASS=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)
|
|
ROOM_PLANNER_DB_PASS=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)
|
|
CENTRAL_AUTH_SECRET=$(openssl rand -hex 32)
|
|
SETTINGS_SECRET=$(openssl rand -hex 32)
|
|
WEBHOOK_SECRET=$(openssl rand -hex 24)
|
|
NPM_ADMIN_PASS=$(openssl rand -base64 12 | tr -dc 'a-zA-Z0-9' | head -c 12)
|
|
|
|
cat > "$CREDS_FILE" <<EOF
|
|
# Hotel Manage credentials — generated $(date '+%Y-%m-%d %H:%M')
|
|
# !! KEEP THIS FILE SAFE — store a copy offsite !!
|
|
|
|
SITE_NAME="${SITE_NAME}"
|
|
DOMAIN=${DOMAIN}
|
|
ADMIN_EMAIL=${ADMIN_EMAIL}
|
|
ADMIN_PASS="${ADMIN_PASS}"
|
|
OFFICE_IP_CHECK=${OFFICE_IP}
|
|
|
|
PG_SUPERPASS=${PG_SUPERPASS}
|
|
AUTH_DB_PASS=${AUTH_DB_PASS}
|
|
NOTICES_DB_PASS=${NOTICES_DB_PASS}
|
|
SETTINGS_DB_PASS=${SETTINGS_DB_PASS}
|
|
CASHUP_DB_PASS=${CASHUP_DB_PASS}
|
|
HK_PLANNER_DB_PASS=${HK_PLANNER_DB_PASS}
|
|
TWIN_OPT_DB_PASS=${TWIN_OPT_DB_PASS}
|
|
ROOM_PLANNER_DB_PASS=${ROOM_PLANNER_DB_PASS}
|
|
CENTRAL_AUTH_SECRET=${CENTRAL_AUTH_SECRET}
|
|
SETTINGS_SECRET=${SETTINGS_SECRET}
|
|
WEBHOOK_SECRET=${WEBHOOK_SECRET}
|
|
|
|
NPM_LAN_IP=${NPM_LAN_IP}
|
|
NPM_ADMIN_EMAIL=manage@hotel.com
|
|
NPM_ADMIN_PASS=${NPM_ADMIN_PASS}
|
|
|
|
FORGEJO_BASE=${FORGEJO_BASE}
|
|
FORGEJO_TOKEN=${FORGEJO_TOKEN}
|
|
|
|
BACKUP_REMOTE=${BACKUP_REMOTE}
|
|
EOF
|
|
chmod 600 "$CREDS_FILE"
|
|
msg_ok "Secrets generated → $CREDS_FILE"
|
|
}
|
|
|
|
# ── SSH keypair for management → app LXCs ────────────────────────────────────
|
|
gen_mgmt_ssh_key() {
|
|
if [[ ! -f /root/.ssh/hotel-manage_deploy ]]; then
|
|
msg_info "Generating management SSH keypair"
|
|
mkdir -p /root/.ssh
|
|
ssh-keygen -t ed25519 -f /root/.ssh/hotel-manage_deploy -N "" -C "hotel-manage-management-deploy" &>/dev/null
|
|
msg_ok "SSH keypair generated → /root/.ssh/hotel-manage_deploy"
|
|
else
|
|
msg_ok "Using existing SSH keypair at /root/.ssh/hotel-manage_deploy"
|
|
fi
|
|
MGMT_PUBKEY=$(cat /root/.ssh/hotel-manage_deploy.pub)
|
|
# record the pubkey once (don't duplicate on re-run)
|
|
grep -qF "${MGMT_PUBKEY}" "$CREDS_FILE" 2>/dev/null || echo "${MGMT_PUBKEY}" >> "$CREDS_FILE"
|
|
}
|
|
|
|
# ── LXC lifecycle helpers ─────────────────────────────────────────────────────
|
|
TEMPLATE_PATH=""
|
|
|
|
get_template() {
|
|
[[ -n "$TEMPLATE_PATH" ]] && { echo "$TEMPLATE_PATH"; return; }
|
|
TEMPLATE_PATH=$(ensure_template)
|
|
echo "$TEMPLATE_PATH"
|
|
}
|
|
|
|
lxc_exists() { pct status "$1" &>/dev/null; }
|
|
|
|
lxc_running() {
|
|
pct status "$1" 2>/dev/null | grep -q "running"
|
|
}
|
|
|
|
# Group the stack in a Proxmox resource pool + tag for tidy UI organisation.
|
|
POOL="hotel-manage"
|
|
POOL_OPT=""
|
|
ensure_pool() {
|
|
if ! pvesh get "/pools/${POOL}" &>/dev/null; then
|
|
msg_info "Creating Proxmox resource pool '${POOL}'"
|
|
pvesh create /pools --poolid "${POOL}" --comment "Hotel Manage stack" &>/dev/null || true
|
|
pvesh get "/pools/${POOL}" &>/dev/null && msg_ok "Pool '${POOL}' ready" \
|
|
|| msg_warn "Could not create pool '${POOL}' — continuing without it"
|
|
fi
|
|
# Only pass --pool if it actually exists (else pct create would fail)
|
|
pvesh get "/pools/${POOL}" &>/dev/null && POOL_OPT="--pool ${POOL}"
|
|
}
|
|
|
|
# Docker-in-LXC: keep the nesting=1 feature (needed by Docker) and let each
|
|
# container skip AppArmor via `security_opt: apparmor=unconfined` in its compose.
|
|
# (Overriding lxc.apparmor.profile would cancel nesting — don't do that.)
|
|
create_lxc() {
|
|
local id=$1 ip=$2 name=$3 mem=${4:-512} cores=${5:-1}
|
|
if lxc_exists "$id"; then
|
|
msg_warn "LXC $id (hotel-manage-${name}) already exists — skipping creation"
|
|
lxc_running "$id" || pct start "$id"
|
|
return
|
|
fi
|
|
local tmpl; tmpl=$(get_template)
|
|
pct create "$id" "$tmpl" \
|
|
--hostname "hotel-manage-${name}" \
|
|
--memory "$mem" \
|
|
--cores "$cores" \
|
|
--rootfs "${STORAGE}:8" \
|
|
--net0 "name=eth0,bridge=vmbr1,ip=${ip}/24,gw=10.10.10.1" \
|
|
--features nesting=1 \
|
|
--unprivileged 0 \
|
|
--onboot 1 \
|
|
--tags "${POOL}" \
|
|
${POOL_OPT} \
|
|
--start 1 &>/dev/null
|
|
sleep 5
|
|
}
|
|
|
|
create_npm_lxc() {
|
|
local id=103
|
|
if lxc_exists "$id"; then
|
|
msg_warn "LXC $id (hotel-manage-npm) already exists — skipping creation"
|
|
lxc_running "$id" || pct start "$id"
|
|
return
|
|
fi
|
|
local tmpl; tmpl=$(get_template)
|
|
pct create "$id" "$tmpl" \
|
|
--hostname "hotel-manage-npm" \
|
|
--memory 512 \
|
|
--cores 1 \
|
|
--rootfs "${STORAGE}:8" \
|
|
--net0 "name=eth0,bridge=vmbr0,ip=${NPM_LAN_IP}/22,gw=${LAN_GW}" \
|
|
--net1 "name=eth1,bridge=vmbr1,ip=10.10.10.3/24" \
|
|
--features nesting=1 \
|
|
--unprivileged 0 \
|
|
--onboot 1 \
|
|
--tags "${POOL}" \
|
|
${POOL_OPT} \
|
|
--start 1 &>/dev/null
|
|
sleep 5
|
|
}
|
|
|
|
install_docker() {
|
|
local id=$1
|
|
pct exec "$id" -- bash -s &>/dev/null <<'DOCKER_INSTALL'
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
# Docker-in-LXC: neutralise AppArmor so Docker never tries to load a profile.
|
|
# In a confined LXC that fails ("docker-default ... while confined") for BOTH
|
|
# image builds and container runtime. Removing apparmor_parser makes Docker
|
|
# run everything unconfined — the container itself is the isolation boundary.
|
|
if [ -e /usr/sbin/apparmor_parser ]; then
|
|
mv -f /usr/sbin/apparmor_parser /usr/sbin/apparmor_parser.disabled
|
|
systemctl is-active --quiet docker && systemctl restart docker
|
|
fi
|
|
# Idempotent: if Docker's already here, just make sure it's running and bail.
|
|
if command -v docker >/dev/null 2>&1; then
|
|
systemctl enable --now docker ssh 2>/dev/null
|
|
exit 0
|
|
fi
|
|
apt-get update -qq 2>/dev/null
|
|
apt-get install -y -qq ca-certificates curl gnupg git openssh-server 2>/dev/null
|
|
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 2>/dev/null
|
|
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 2>/dev/null
|
|
apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-compose-plugin 2>/dev/null
|
|
systemctl enable --now docker 2>/dev/null
|
|
systemctl enable --now ssh 2>/dev/null
|
|
DOCKER_INSTALL
|
|
}
|
|
|
|
install_mgmt_key() {
|
|
local id=$1
|
|
pct exec "$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
|
|
}
|
|
|
|
push_file() {
|
|
# Write content to a temp file, push into LXC, remove temp
|
|
local id=$1 dest=$2; shift 2
|
|
local tmp; tmp=$(mktemp /tmp/hotel-manage-push-XXXX)
|
|
cat > "$tmp" # reads stdin
|
|
pct push "$id" "$tmp" "$dest" 2>/dev/null
|
|
rm -f "$tmp"
|
|
}
|
|
|
|
push_dir() {
|
|
# tar local dir → push tarball → extract in LXC at parent of dest
|
|
local id=$1 src=$2 dest=$3
|
|
local tmp; tmp=$(mktemp /tmp/hotel-manage-dir-XXXX.tar.gz)
|
|
tar czf "$tmp" -C "$(dirname "$src")" "$(basename "$src")" 2>/dev/null
|
|
pct push "$id" "$tmp" /tmp/hotel-manage-deploy.tar.gz 2>/dev/null
|
|
pct exec "$id" -- bash -c "
|
|
mkdir -p '$(dirname "$dest")'
|
|
tar xzf /tmp/hotel-manage-deploy.tar.gz -C '$(dirname "$dest")'
|
|
mv '$(dirname "$dest")/$(basename "$src")' '${dest}' 2>/dev/null || true
|
|
rm -f /tmp/hotel-manage-deploy.tar.gz
|
|
" &>/dev/null
|
|
rm -f "$tmp"
|
|
}
|
|
|
|
build_clone_url() {
|
|
# Inject the Forgejo token into the clone URL so private repos work and the
|
|
# updater can pull later without extra credentials.
|
|
local repo=$1
|
|
local url="${FORGEJO_BASE}/${repo}.git"
|
|
if [[ -n "${FORGEJO_TOKEN:-}" ]]; then
|
|
url="${url/https:\/\//https://oauth2:${FORGEJO_TOKEN}@}"
|
|
url="${url/http:\/\//http://oauth2:${FORGEJO_TOKEN}@}"
|
|
fi
|
|
echo "$url"
|
|
}
|
|
|
|
deploy_service() {
|
|
# Either clone from Forgejo or push from a local repo copy on this host
|
|
local id=$1 repo_name=$2 local_src=$3 dest=$4
|
|
if [[ "$USE_FORGEJO" == "true" ]]; then
|
|
local url; url=$(build_clone_url "$repo_name")
|
|
local tmp; tmp=$(mktemp /tmp/hotel-manage-deploy-out-XXXX)
|
|
if ! pct exec "$id" -- bash -c "
|
|
if [ -d '${dest}/.git' ]; then
|
|
cd '${dest}' && git pull
|
|
else
|
|
git clone '${url}' '${dest}'
|
|
fi
|
|
" > "$tmp" 2>&1; then
|
|
local out; out=$(cat "$tmp"); rm -f "$tmp"
|
|
msg_error "Deploy of ${repo_name} to LXC ${id} failed:
|
|
${out}
|
|
|
|
Debug: pct enter ${id} && git clone ${FORGEJO_BASE}/${repo_name}.git /tmp/test-clone"
|
|
fi
|
|
rm -f "$tmp"
|
|
else
|
|
push_dir "$id" "$local_src" "$dest"
|
|
fi
|
|
}
|
|
|
|
wait_healthy() {
|
|
local id=$1 url=$2 max=${3:-40}
|
|
local i=0
|
|
while ! pct exec "$id" -- curl -sf --max-time 2 "$url" &>/dev/null; do
|
|
sleep 3; i=$((i+1))
|
|
[[ $i -ge $max ]] && return 1
|
|
done
|
|
return 0
|
|
}
|
|
|
|
wait_pg() {
|
|
local max=30 i=0
|
|
while ! pct exec 100 -- bash -c \
|
|
"docker exec hotel-manage-postgres pg_isready -U postgres" &>/dev/null; do
|
|
sleep 3; i=$((i+1))
|
|
[[ $i -ge $max ]] && { msg_warn "Postgres not ready after 90s"; return 1; }
|
|
done
|
|
}
|
|
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
# PHASE 1 — POSTGRES LXC 100
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
deploy_postgres() {
|
|
msg_step "1/7 Postgres (LXC 100 · 10.10.10.100)"
|
|
|
|
msg_info "Creating LXC 100"
|
|
create_lxc 100 "10.10.10.100" "postgres" 512 1
|
|
msg_ok "LXC 100 created"
|
|
|
|
msg_info "Installing Docker"
|
|
install_docker 100
|
|
msg_ok "Docker installed"
|
|
|
|
msg_info "Deploying postgres"
|
|
pct exec 100 -- mkdir -p /opt/postgres/init
|
|
|
|
# docker-compose.yml
|
|
push_file 100 /opt/postgres/docker-compose.yml <<'EOF'
|
|
services:
|
|
postgres:
|
|
container_name: hotel-manage-postgres
|
|
image: postgres:16-alpine
|
|
security_opt:
|
|
- apparmor=unconfined
|
|
environment:
|
|
- POSTGRES_USER=postgres
|
|
- POSTGRES_PASSWORD=${PG_SUPERPASS}
|
|
volumes:
|
|
- pg_data:/var/lib/postgresql/data
|
|
- ./init:/docker-entrypoint-initdb.d:ro
|
|
ports:
|
|
- "5432:5432"
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
interval: 5s
|
|
retries: 15
|
|
restart: unless-stopped
|
|
volumes:
|
|
pg_data:
|
|
EOF
|
|
|
|
# .env
|
|
push_file 100 /opt/postgres/.env <<EOF
|
|
PG_SUPERPASS=${PG_SUPERPASS}
|
|
EOF
|
|
|
|
# Init SQL — auth DB
|
|
push_file 100 /opt/postgres/init/01-auth.sql <<EOF
|
|
CREATE USER auth WITH PASSWORD '${AUTH_DB_PASS}';
|
|
CREATE DATABASE auth_db OWNER auth;
|
|
\c auth_db
|
|
GRANT ALL ON SCHEMA public TO auth;
|
|
EOF
|
|
|
|
# Init SQL — noticeboard DB
|
|
push_file 100 /opt/postgres/init/02-noticeboard.sql <<EOF
|
|
CREATE USER noticeboard WITH PASSWORD '${NOTICES_DB_PASS}';
|
|
CREATE DATABASE noticeboard_db OWNER noticeboard;
|
|
\c noticeboard_db
|
|
GRANT ALL ON SCHEMA public TO noticeboard;
|
|
EOF
|
|
|
|
# Init SQL — settings DB
|
|
push_file 100 /opt/postgres/init/03-settings.sql <<EOF
|
|
CREATE USER settings WITH PASSWORD '${SETTINGS_DB_PASS}';
|
|
CREATE DATABASE settings_db OWNER settings;
|
|
\c settings_db
|
|
GRANT ALL ON SCHEMA public TO settings;
|
|
EOF
|
|
|
|
# Init SQL — cashup DB
|
|
push_file 100 /opt/postgres/init/04-cashup.sql <<EOF
|
|
CREATE USER cashup WITH PASSWORD '${CASHUP_DB_PASS}';
|
|
CREATE DATABASE cashup_db OWNER cashup;
|
|
\c cashup_db
|
|
GRANT ALL ON SCHEMA public TO cashup;
|
|
EOF
|
|
|
|
# Init SQL — hk-planner DB
|
|
push_file 100 /opt/postgres/init/05-hk-planner.sql <<EOF
|
|
CREATE USER hk_planner WITH PASSWORD '${HK_PLANNER_DB_PASS}';
|
|
CREATE DATABASE hk_planner_db OWNER hk_planner;
|
|
\c hk_planner_db
|
|
GRANT ALL ON SCHEMA public TO hk_planner;
|
|
EOF
|
|
|
|
# Init SQL — twin-optimiser DB
|
|
push_file 100 /opt/postgres/init/06-twin-optimiser.sql <<EOF
|
|
CREATE USER twin_optimiser WITH PASSWORD '${TWIN_OPT_DB_PASS}';
|
|
CREATE DATABASE twin_optimiser_db OWNER twin_optimiser;
|
|
\c twin_optimiser_db
|
|
GRANT ALL ON SCHEMA public TO twin_optimiser;
|
|
EOF
|
|
|
|
# Init SQL — room-planner DB
|
|
push_file 100 /opt/postgres/init/07-room-planner.sql <<EOF
|
|
CREATE USER room_planner WITH PASSWORD '${ROOM_PLANNER_DB_PASS}';
|
|
CREATE DATABASE room_planner_db OWNER room_planner;
|
|
\c room_planner_db
|
|
GRANT ALL ON SCHEMA public TO room_planner;
|
|
EOF
|
|
|
|
pct exec 100 -- bash -c "cd /opt/postgres && docker compose up -d" &>/dev/null
|
|
|
|
msg_info "Waiting for postgres to be ready"
|
|
wait_pg && msg_ok "Postgres running at 10.10.10.100:5432" || msg_warn "Postgres may need extra time — check LXC 100"
|
|
|
|
install_mgmt_key 100
|
|
}
|
|
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
# PHASE 2 — AUTH SERVICE LXC 101
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
deploy_auth() {
|
|
msg_step "2/7 Auth service (LXC 101 · 10.10.10.101)"
|
|
|
|
msg_info "Creating LXC 101"
|
|
create_lxc 101 "10.10.10.101" "auth" 1024 1
|
|
msg_ok "LXC 101 created"
|
|
|
|
msg_info "Installing Docker"
|
|
install_docker 101
|
|
install_mgmt_key 101
|
|
msg_ok "Docker + SSH ready"
|
|
|
|
msg_info "Deploying auth service"
|
|
deploy_service 101 "auth" "${REPO_ROOT}/auth" /opt/auth
|
|
|
|
push_file 101 /opt/auth/.env <<EOF
|
|
NODE_ENV=production
|
|
PORT=3001
|
|
DATABASE_URL=postgresql://auth:${AUTH_DB_PASS}@10.10.10.100:5432/auth_db
|
|
CENTRAL_AUTH_SECRET=${CENTRAL_AUTH_SECRET}
|
|
DOMAIN=${DOMAIN}
|
|
ADMIN_EMAIL=${ADMIN_EMAIL}
|
|
ADMIN_PASSWORD=${ADMIN_PASS}
|
|
OFFICE_IP_CHECK=${OFFICE_IP_CHECK:-${OFFICE_IP:-disabled}}
|
|
CORS_ORIGIN=https://${DOMAIN}
|
|
SESSION_DAYS=30
|
|
SETTINGS_URL=http://10.10.10.116:3080
|
|
SETTINGS_SECRET=${SETTINGS_SECRET}
|
|
EOF
|
|
|
|
pct exec 101 -- bash -c "cd /opt/auth && docker compose up -d --build" &>/dev/null
|
|
|
|
msg_info "Waiting for auth service"
|
|
wait_healthy 101 "http://localhost:3001/health" \
|
|
&& msg_ok "Auth service running at 10.10.10.101:3001" \
|
|
|| msg_warn "Auth service may need extra time — check LXC 101"
|
|
}
|
|
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
# PHASE 3 — PORTAL LXC 102
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
deploy_portal() {
|
|
msg_step "3/7 Portal (LXC 102 · 10.10.10.102)"
|
|
|
|
msg_info "Creating LXC 102"
|
|
create_lxc 102 "10.10.10.102" "portal" 1024 2
|
|
msg_ok "LXC 102 created"
|
|
|
|
msg_info "Installing Docker"
|
|
install_docker 102
|
|
install_mgmt_key 102
|
|
msg_ok "Docker + SSH ready"
|
|
|
|
msg_info "Deploying portal"
|
|
deploy_service 102 "portal" "${REPO_ROOT}/portal" /opt/portal
|
|
|
|
push_file 102 /opt/portal/.env <<EOF
|
|
NODE_ENV=production
|
|
VITE_API_BASE=
|
|
EOF
|
|
|
|
# Patch nginx.conf with real auth LXC IP (already correct in template but be explicit)
|
|
pct exec 102 -- bash -c "
|
|
cd /opt/portal && docker compose up -d --build
|
|
" &>/dev/null
|
|
|
|
msg_info "Waiting for portal"
|
|
wait_healthy 102 "http://localhost:3000/health" \
|
|
&& msg_ok "Portal running at 10.10.10.102:3000" \
|
|
|| msg_warn "Portal may need extra time — check LXC 102"
|
|
}
|
|
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
# PHASE 4 — NPM LXC 103 (dual-homed)
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
deploy_npm() {
|
|
msg_step "4/7 Nginx Proxy Manager (LXC 103 · ${NPM_LAN_IP} / 10.10.10.3)"
|
|
|
|
msg_info "Creating NPM LXC 103 (dual-homed)"
|
|
create_npm_lxc
|
|
msg_ok "LXC 103 created"
|
|
|
|
msg_info "Installing Docker"
|
|
install_docker 103
|
|
msg_ok "Docker installed"
|
|
|
|
msg_info "Deploying NPM"
|
|
pct exec 103 -- mkdir -p /opt/npm
|
|
|
|
push_file 103 /opt/npm/docker-compose.yml <<'EOF'
|
|
services:
|
|
npm:
|
|
container_name: hotel-manage-npm
|
|
image: jc21/nginx-proxy-manager:latest
|
|
security_opt:
|
|
- apparmor=unconfined
|
|
ports:
|
|
- "80:80"
|
|
- "443:443"
|
|
- "81:81"
|
|
volumes:
|
|
- npm_data:/data
|
|
- npm_letsencrypt:/etc/letsencrypt
|
|
restart: unless-stopped
|
|
volumes:
|
|
npm_data:
|
|
npm_letsencrypt:
|
|
EOF
|
|
|
|
pct exec 103 -- bash -c "cd /opt/npm && docker compose up -d" &>/dev/null
|
|
|
|
msg_info "Waiting for NPM admin UI"
|
|
# NPM admin API on port 81 — wait up to 60s
|
|
local i=0
|
|
while ! pct exec 103 -- curl -sf --max-time 3 "http://localhost:81/api/" &>/dev/null; do
|
|
sleep 3; i=$((i+1)); [[ $i -ge 20 ]] && break
|
|
done
|
|
msg_ok "NPM running — admin UI at http://${NPM_LAN_IP}:81"
|
|
msg_warn "NPM default login: admin@example.com / changeme (change immediately!)"
|
|
}
|
|
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
# PHASE 5 — MANAGEMENT LXC 105
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
deploy_management() {
|
|
msg_step "5/7 Management (LXC 105 · 10.10.10.105)"
|
|
|
|
msg_info "Creating LXC 105"
|
|
create_lxc 105 "10.10.10.105" "management" 1024 1
|
|
msg_ok "LXC 105 created"
|
|
|
|
msg_info "Installing Docker"
|
|
install_docker 105
|
|
msg_ok "Docker installed"
|
|
|
|
# Copy the management SSH private key into management container
|
|
pct exec 105 -- mkdir -p /root/.ssh
|
|
pct push 105 /root/.ssh/hotel-manage_deploy /root/.ssh/hotel-manage_deploy &>/dev/null
|
|
pct exec 105 -- chmod 600 /root/.ssh/hotel-manage_deploy
|
|
|
|
msg_info "Deploying management stack"
|
|
deploy_service 105 "management" "${REPO_ROOT}/management" /opt/management
|
|
|
|
push_file 105 /opt/management/.env <<EOF
|
|
FORGEJO_WEBHOOK_SECRET=${WEBHOOK_SECRET}
|
|
WEBHOOK_SECRET=${WEBHOOK_SECRET}
|
|
CENTRAL_AUTH_SECRET=${CENTRAL_AUTH_SECRET}
|
|
AUTH_URL=http://10.10.10.101:3001
|
|
BACKUP_REMOTE=${BACKUP_REMOTE}
|
|
BACKUP_PG_HOST=10.10.10.100
|
|
BACKUP_PG_USER=postgres
|
|
BACKUP_PG_PASS=${PG_SUPERPASS}
|
|
UPTIME_KUMA_PORT=3002
|
|
UPDATER_PORT=9000
|
|
EOF
|
|
|
|
pct exec 105 -- bash -c "cd /opt/management && docker compose up -d --build" &>/dev/null
|
|
|
|
msg_info "Waiting for Uptime Kuma"
|
|
wait_healthy 105 "http://localhost:3002" \
|
|
&& msg_ok "Management running — Kuma at 10.10.10.105:3002" \
|
|
|| msg_warn "Management may need extra time — check LXC 105"
|
|
}
|
|
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
# PHASE 6 — NOTICEBOARD LXC 112
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
deploy_noticeboard() {
|
|
msg_step "6/7 Noticeboard (LXC 112 · 10.10.10.112)"
|
|
|
|
msg_info "Creating LXC 112"
|
|
create_lxc 112 "10.10.10.112" "noticeboard" 1024 1
|
|
msg_ok "LXC 112 created"
|
|
|
|
msg_info "Installing Docker"
|
|
install_docker 112
|
|
install_mgmt_key 112
|
|
msg_ok "Docker + SSH ready"
|
|
|
|
msg_info "Deploying noticeboard"
|
|
deploy_service 112 "noticeboard" "${REPO_ROOT}/noticeboard" /opt/noticeboard
|
|
|
|
push_file 112 /opt/noticeboard/.env <<EOF
|
|
DATABASE_URL=postgresql://noticeboard:${NOTICES_DB_PASS}@10.10.10.100:5432/noticeboard_db
|
|
CENTRAL_AUTH_SECRET=${CENTRAL_AUTH_SECRET}
|
|
APP_SLUG=noticeboard
|
|
OFFICE_IP_CHECK=${OFFICE_IP_CHECK:-${OFFICE_IP:-disabled}}
|
|
NODE_ENV=production
|
|
AUTH_URL=http://10.10.10.101:3001
|
|
EOF
|
|
|
|
pct exec 112 -- bash -c "cd /opt/noticeboard && docker compose up -d --build" &>/dev/null
|
|
|
|
msg_info "Waiting for noticeboard"
|
|
wait_healthy 112 "http://localhost:3080/notices/health" \
|
|
&& msg_ok "Noticeboard running at 10.10.10.112:3080" \
|
|
|| msg_warn "Noticeboard may need extra time — check LXC 112"
|
|
}
|
|
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
# PHASE 7 — SETTINGS SERVICE LXC 116
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
deploy_settings() {
|
|
msg_step "7/7 Settings service (LXC 116 · 10.10.10.116)"
|
|
|
|
if [[ -z "${SETTINGS_DB_PASS:-}" ]]; then
|
|
SETTINGS_DB_PASS=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)
|
|
printf '\nSETTINGS_DB_PASS=%s\n' "$SETTINGS_DB_PASS" >> "$CREDS_FILE"
|
|
msg_ok "Generated SETTINGS_DB_PASS → ${CREDS_FILE}"
|
|
fi
|
|
if [[ -z "${SETTINGS_SECRET:-}" ]]; then
|
|
SETTINGS_SECRET=$(openssl rand -hex 32)
|
|
printf 'SETTINGS_SECRET=%s\n' "$SETTINGS_SECRET" >> "$CREDS_FILE"
|
|
msg_ok "Generated SETTINGS_SECRET → ${CREDS_FILE}"
|
|
fi
|
|
|
|
msg_info "Creating LXC 116"
|
|
create_lxc 116 "10.10.10.116" "settings" 512 1
|
|
msg_ok "LXC 116 created"
|
|
|
|
msg_info "Installing Docker"
|
|
install_docker 116
|
|
install_mgmt_key 116
|
|
msg_ok "Docker + SSH ready"
|
|
|
|
msg_info "Deploying settings service"
|
|
deploy_service 116 "settings" "${REPO_ROOT}/settings" /opt/settings
|
|
|
|
push_file 116 /opt/settings/.env <<EOF
|
|
NODE_ENV=production
|
|
DATABASE_URL=postgresql://settings:${SETTINGS_DB_PASS}@10.10.10.100:5432/settings_db
|
|
SETTINGS_SECRET=${SETTINGS_SECRET}
|
|
AUTH_URL=http://10.10.10.101:3001
|
|
EOF
|
|
|
|
pct exec 116 -- bash -c "cd /opt/settings && docker compose up -d --build" &>/dev/null
|
|
|
|
msg_info "Waiting for settings service"
|
|
wait_healthy 116 "http://localhost:3080/health" \
|
|
&& msg_ok "Settings service running at 10.10.10.116:3080" \
|
|
|| msg_warn "Settings may need extra time — check LXC 116"
|
|
}
|
|
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
# PHASE 8 — CASHUP LXC 117
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
deploy_cashup() {
|
|
msg_step "8/8 Cashup (LXC 117 · 10.10.10.117)"
|
|
|
|
# Generate DB password if not already in creds file (--only cashup on existing stack)
|
|
if [[ -z "${CASHUP_DB_PASS:-}" ]]; then
|
|
CASHUP_DB_PASS=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)
|
|
printf '\nCASHUP_DB_PASS=%s\n' "$CASHUP_DB_PASS" >> "$CREDS_FILE"
|
|
msg_ok "Generated CASHUP_DB_PASS → ${CREDS_FILE}"
|
|
fi
|
|
|
|
msg_info "Creating LXC 117"
|
|
create_lxc 117 "10.10.10.117" "cashup" 1024 1
|
|
msg_ok "LXC 117 created"
|
|
|
|
msg_info "Installing Docker"
|
|
install_docker 117
|
|
install_mgmt_key 117
|
|
msg_ok "Docker + SSH ready"
|
|
|
|
# Create DB — idempotent via || true, works on fresh installs and re-runs
|
|
msg_info "Creating cashup database"
|
|
pct exec 100 -- bash -c "
|
|
docker exec hotel-manage-postgres psql -U postgres -c \
|
|
\"CREATE USER cashup WITH PASSWORD '${CASHUP_DB_PASS}';\" 2>/dev/null || true
|
|
docker exec hotel-manage-postgres psql -U postgres -c \
|
|
\"CREATE DATABASE cashup_db OWNER cashup;\" 2>/dev/null || true
|
|
docker exec hotel-manage-postgres psql -U postgres -d cashup_db -c \
|
|
\"GRANT ALL ON SCHEMA public TO cashup;\" 2>/dev/null || true
|
|
" &>/dev/null
|
|
msg_ok "Database cashup_db ready"
|
|
|
|
msg_info "Deploying cashup"
|
|
deploy_service 117 "cashup" "${REPO_ROOT}/cashup" /opt/cashup
|
|
|
|
push_file 117 /opt/cashup/.env <<EOF
|
|
NODE_ENV=production
|
|
APP_SLUG=cashup
|
|
DATABASE_URL=postgresql://cashup:${CASHUP_DB_PASS}@10.10.10.100:5432/cashup_db
|
|
CENTRAL_AUTH_SECRET=${CENTRAL_AUTH_SECRET}
|
|
SETTINGS_URL=http://10.10.10.116:3080
|
|
SETTINGS_SECRET=${SETTINGS_SECRET}
|
|
OFFICE_IP_CHECK=${OFFICE_IP_CHECK:-${OFFICE_IP:-disabled}}
|
|
FRONTEND_PORT=3083
|
|
EOF
|
|
|
|
local build_out
|
|
if ! build_out=$(pct exec 117 -- bash -c "cd /opt/cashup && docker compose up -d --build 2>&1"); then
|
|
msg_error "docker compose build failed in LXC 117:
|
|
${build_out}"
|
|
fi
|
|
|
|
msg_info "Waiting for cashup"
|
|
wait_healthy 117 "http://localhost:3083/cashup/health" \
|
|
&& msg_ok "Cashup running at 10.10.10.117:3083" \
|
|
|| msg_warn "Cashup may need extra time — check LXC 117"
|
|
|
|
# Add /cashup/ location to the existing NPM proxy host (idempotent)
|
|
npm_add_location "/cashup/" "10.10.10.117" 3083
|
|
}
|
|
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
# PHASE 9 — HK PLANNER LXC 118
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
deploy_hk_planner() {
|
|
msg_step "9/10 HK Planner (LXC 118 · 10.10.10.118)"
|
|
|
|
if [[ -z "${HK_PLANNER_DB_PASS:-}" ]]; then
|
|
HK_PLANNER_DB_PASS=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)
|
|
printf '\nHK_PLANNER_DB_PASS=%s\n' "$HK_PLANNER_DB_PASS" >> "$CREDS_FILE"
|
|
msg_ok "Generated HK_PLANNER_DB_PASS → ${CREDS_FILE}"
|
|
fi
|
|
|
|
msg_info "Creating LXC 118"
|
|
create_lxc 118 "10.10.10.118" "hk-planner" 1024 1
|
|
msg_ok "LXC 118 created"
|
|
|
|
msg_info "Installing Docker"
|
|
install_docker 118
|
|
install_mgmt_key 118
|
|
msg_ok "Docker + SSH ready"
|
|
|
|
msg_info "Creating hk_planner database"
|
|
pct exec 100 -- bash -c "
|
|
docker exec hotel-manage-postgres psql -U postgres -c \
|
|
\"CREATE USER hk_planner WITH PASSWORD '${HK_PLANNER_DB_PASS}';\" 2>/dev/null || true
|
|
docker exec hotel-manage-postgres psql -U postgres -c \
|
|
\"CREATE DATABASE hk_planner_db OWNER hk_planner;\" 2>/dev/null || true
|
|
docker exec hotel-manage-postgres psql -U postgres -d hk_planner_db -c \
|
|
\"GRANT ALL ON SCHEMA public TO hk_planner;\" 2>/dev/null || true
|
|
" &>/dev/null
|
|
msg_ok "Database hk_planner_db ready"
|
|
|
|
msg_info "Deploying hk-planner"
|
|
deploy_service 118 "hk-planner" "${REPO_ROOT}/hk-planner" /opt/hk-planner
|
|
|
|
push_file 118 /opt/hk-planner/.env <<EOF
|
|
NODE_ENV=production
|
|
APP_SLUG=hk-planner
|
|
DATABASE_URL=postgresql://hk_planner:${HK_PLANNER_DB_PASS}@10.10.10.100:5432/hk_planner_db
|
|
CENTRAL_AUTH_SECRET=${CENTRAL_AUTH_SECRET}
|
|
SETTINGS_URL=http://10.10.10.116:3080
|
|
SETTINGS_SECRET=${SETTINGS_SECRET}
|
|
OFFICE_IP_CHECK=${OFFICE_IP_CHECK:-${OFFICE_IP:-disabled}}
|
|
VITE_HOTEL_NAME=${SITE_NAME}
|
|
FRONTEND_PORT=3080
|
|
EOF
|
|
|
|
local build_out
|
|
if ! build_out=$(pct exec 118 -- bash -c "cd /opt/hk-planner && docker compose up -d --build 2>&1"); then
|
|
msg_error "docker compose build failed in LXC 118:
|
|
${build_out}"
|
|
fi
|
|
|
|
msg_info "Waiting for hk-planner"
|
|
wait_healthy 118 "http://localhost:3080/hk-planner/health" \
|
|
&& msg_ok "HK Planner running at 10.10.10.118:3080" \
|
|
|| msg_warn "HK Planner may need extra time — check LXC 118"
|
|
|
|
# Add /hk-planner/ location to NPM proxy host
|
|
npm_add_location "/hk-planner/" "10.10.10.118" 3080
|
|
}
|
|
|
|
deploy_twin_optimiser() {
|
|
msg_step "10/10 Twin Optimiser (LXC 119 · 10.10.10.119)"
|
|
|
|
if [[ -z "${TWIN_OPT_DB_PASS:-}" ]]; then
|
|
TWIN_OPT_DB_PASS=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)
|
|
printf '\nTWIN_OPT_DB_PASS=%s\n' "$TWIN_OPT_DB_PASS" >> "$CREDS_FILE"
|
|
msg_ok "Generated TWIN_OPT_DB_PASS → ${CREDS_FILE}"
|
|
fi
|
|
|
|
msg_info "Creating LXC 119"
|
|
create_lxc 119 "10.10.10.119" "twin-optimiser" 1024 1
|
|
msg_ok "LXC 119 created"
|
|
|
|
msg_info "Installing Docker"
|
|
install_docker 119
|
|
install_mgmt_key 119
|
|
msg_ok "Docker + SSH ready"
|
|
|
|
msg_info "Creating twin_optimiser database"
|
|
pct exec 100 -- bash -c "
|
|
docker exec hotel-manage-postgres psql -U postgres -c \
|
|
\"CREATE USER twin_optimiser WITH PASSWORD '${TWIN_OPT_DB_PASS}';\" 2>/dev/null || true
|
|
docker exec hotel-manage-postgres psql -U postgres -c \
|
|
\"CREATE DATABASE twin_optimiser_db OWNER twin_optimiser;\" 2>/dev/null || true
|
|
docker exec hotel-manage-postgres psql -U postgres -d twin_optimiser_db -c \
|
|
\"GRANT ALL ON SCHEMA public TO twin_optimiser;\" 2>/dev/null || true
|
|
" &>/dev/null
|
|
msg_ok "Database twin_optimiser_db ready"
|
|
|
|
msg_info "Deploying twin-optimiser"
|
|
deploy_service 119 "twin-optimiser" "${REPO_ROOT}/twin-optimiser" /opt/twin-optimiser
|
|
|
|
push_file 119 /opt/twin-optimiser/.env <<EOF
|
|
NODE_ENV=production
|
|
APP_SLUG=twin-optimiser
|
|
DATABASE_URL=postgresql://twin_optimiser:${TWIN_OPT_DB_PASS}@10.10.10.100:5432/twin_optimiser_db
|
|
CENTRAL_AUTH_SECRET=${CENTRAL_AUTH_SECRET}
|
|
SETTINGS_URL=http://10.10.10.116:3080
|
|
SETTINGS_SECRET=${SETTINGS_SECRET}
|
|
OFFICE_IP_CHECK=${OFFICE_IP_CHECK:-${OFFICE_IP:-disabled}}
|
|
VITE_HOTEL_NAME=${SITE_NAME}
|
|
FRONTEND_PORT=3080
|
|
EOF
|
|
|
|
local build_out
|
|
if ! build_out=$(pct exec 119 -- bash -c "cd /opt/twin-optimiser && docker compose up -d --build 2>&1"); then
|
|
msg_error "docker compose build failed in LXC 119:
|
|
${build_out}"
|
|
fi
|
|
|
|
msg_info "Waiting for twin-optimiser"
|
|
wait_healthy 119 "http://localhost:3080/twin-optimiser/health" \
|
|
&& msg_ok "Twin Optimiser running at 10.10.10.119:3080" \
|
|
|| msg_warn "Twin Optimiser may need extra time — check LXC 119"
|
|
|
|
# Add /twin-optimiser/ location to NPM proxy host
|
|
npm_add_location "/twin-optimiser/" "10.10.10.119" 3080
|
|
}
|
|
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
# PHASE 11 — ROOM PLANNER LXC 120
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
deploy_room_planner() {
|
|
msg_step "11/11 Room Planner (LXC 120 · 10.10.10.120)"
|
|
|
|
if [[ -z "${ROOM_PLANNER_DB_PASS:-}" ]]; then
|
|
ROOM_PLANNER_DB_PASS=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)
|
|
printf '\nROOM_PLANNER_DB_PASS=%s\n' "$ROOM_PLANNER_DB_PASS" >> "$CREDS_FILE"
|
|
msg_ok "Generated ROOM_PLANNER_DB_PASS → ${CREDS_FILE}"
|
|
fi
|
|
|
|
msg_info "Creating LXC 120"
|
|
create_lxc 120 "10.10.10.120" "room-planner" 1024 1
|
|
msg_ok "LXC 120 created"
|
|
|
|
msg_info "Installing Docker"
|
|
install_docker 120
|
|
install_mgmt_key 120
|
|
msg_ok "Docker + SSH ready"
|
|
|
|
msg_info "Creating room_planner database"
|
|
pct exec 100 -- bash -c "
|
|
docker exec hotel-manage-postgres psql -U postgres -c \
|
|
\"CREATE USER room_planner WITH PASSWORD '${ROOM_PLANNER_DB_PASS}';\" 2>/dev/null || true
|
|
docker exec hotel-manage-postgres psql -U postgres -c \
|
|
\"CREATE DATABASE room_planner_db OWNER room_planner;\" 2>/dev/null || true
|
|
docker exec hotel-manage-postgres psql -U postgres -d room_planner_db -c \
|
|
\"GRANT ALL ON SCHEMA public TO room_planner;\" 2>/dev/null || true
|
|
" &>/dev/null
|
|
msg_ok "Database room_planner_db ready"
|
|
|
|
msg_info "Deploying room-planner"
|
|
deploy_service 120 "room-planner" "${REPO_ROOT}/room-planner" /opt/room-planner
|
|
|
|
push_file 120 /opt/room-planner/.env <<EOF
|
|
NODE_ENV=production
|
|
APP_SLUG=room-planner
|
|
DATABASE_URL=postgresql://room_planner:${ROOM_PLANNER_DB_PASS}@10.10.10.100:5432/room_planner_db
|
|
CENTRAL_AUTH_SECRET=${CENTRAL_AUTH_SECRET}
|
|
SETTINGS_URL=http://10.10.10.116:3080
|
|
SETTINGS_SECRET=${SETTINGS_SECRET}
|
|
OFFICE_IP_CHECK=${OFFICE_IP_CHECK:-${OFFICE_IP:-disabled}}
|
|
DEFAULT_CHECKOUT_TIME=11:00
|
|
FRONTEND_PORT=3080
|
|
EOF
|
|
|
|
local build_out
|
|
if ! build_out=$(pct exec 120 -- bash -c "cd /opt/room-planner && docker compose up -d --build 2>&1"); then
|
|
msg_error "docker compose build failed in LXC 120:
|
|
${build_out}"
|
|
fi
|
|
|
|
msg_info "Waiting for room-planner"
|
|
wait_healthy 120 "http://localhost:3080/room-planner/health" \
|
|
&& msg_ok "Room Planner running at 10.10.10.120:3080" \
|
|
|| msg_warn "Room Planner may need extra time — check LXC 120"
|
|
|
|
# Seed app + capabilities into auth DB (psql on LXC 100 — same pattern as add-app.sh)
|
|
msg_info "Seeding room-planner into auth DB"
|
|
pct exec 100 -- docker exec hotel-manage-postgres psql -U postgres -d auth_db -c "
|
|
INSERT INTO apps (slug, name, description, base_path, icon, theme_color, category, internal_host, internal_port)
|
|
VALUES ('room-planner', 'Room Planner', 'Daily housekeeping room view with NewBook task management', '/room-planner', 'BedDouble', '#2d6a4f', 'Housekeeping', '10.10.10.120', 3080)
|
|
ON CONFLICT (slug) DO UPDATE SET
|
|
name=EXCLUDED.name, description=EXCLUDED.description, base_path=EXCLUDED.base_path,
|
|
icon=EXCLUDED.icon, theme_color=EXCLUDED.theme_color, category=EXCLUDED.category,
|
|
internal_host=EXCLUDED.internal_host, internal_port=EXCLUDED.internal_port;
|
|
INSERT INTO app_capabilities (app_id, slug, name, description, sort_order)
|
|
SELECT a.id, c.slug, c.name, c.description, c.sort_order
|
|
FROM apps a, (VALUES
|
|
('view','View Planner','Access the room planner view',1),
|
|
('guest_details','Guest Details','View guest names and personal information',2),
|
|
('rate_details','Rate Details','View pricing and rate plan information',3),
|
|
('view_all_notes','View All Notes','View all booking note types',4),
|
|
('complete_tasks','Complete Tasks','Mark NewBook tasks as complete',5),
|
|
('update_status','Update Room Status','Mark rooms clean, dirty or inspected',6),
|
|
('settings','Settings','Configure room planner settings',7)
|
|
) AS c(slug, name, description, sort_order)
|
|
WHERE a.slug = 'room-planner'
|
|
ON CONFLICT (app_id, slug) DO NOTHING;
|
|
" &>/dev/null \
|
|
&& msg_ok "room-planner seeded into auth DB" \
|
|
|| msg_warn "Seed failed — run the SQL in room-planner/seed-app.js manually via psql on LXC 100"
|
|
|
|
# Add /room-planner/ location to NPM proxy host
|
|
npm_add_location "/room-planner/" "10.10.10.120" 3080
|
|
}
|
|
|
|
deploy_maintenance() {
|
|
msg_step "12/12 Maintenance (LXC 121 · 10.10.10.121)"
|
|
|
|
if [[ -z "${MAINTENANCE_DB_PASS:-}" ]]; then
|
|
MAINTENANCE_DB_PASS=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)
|
|
printf '\nMAINTENANCE_DB_PASS=%s\n' "$MAINTENANCE_DB_PASS" >> "$CREDS_FILE"
|
|
msg_ok "Generated MAINTENANCE_DB_PASS → ${CREDS_FILE}"
|
|
fi
|
|
|
|
msg_info "Creating LXC 121"
|
|
create_lxc 121 "10.10.10.121" "maintenance" 1024 1
|
|
msg_ok "LXC 121 created"
|
|
|
|
msg_info "Installing Docker"
|
|
install_docker 121
|
|
install_mgmt_key 121
|
|
msg_ok "Docker + SSH ready"
|
|
|
|
msg_info "Creating maintenance database"
|
|
pct exec 100 -- bash -c "
|
|
docker exec hotel-manage-postgres psql -U postgres -c \
|
|
\"CREATE USER maintenance WITH PASSWORD '${MAINTENANCE_DB_PASS}';\" 2>/dev/null || true
|
|
docker exec hotel-manage-postgres psql -U postgres -c \
|
|
\"CREATE DATABASE maintenance_db OWNER maintenance;\" 2>/dev/null || true
|
|
docker exec hotel-manage-postgres psql -U postgres -d maintenance_db -c \
|
|
\"GRANT ALL ON SCHEMA public TO maintenance;\" 2>/dev/null || true
|
|
" &>/dev/null
|
|
msg_ok "Database maintenance_db ready"
|
|
|
|
msg_info "Deploying maintenance"
|
|
deploy_service 121 "maintenance" "${REPO_ROOT}/maintenance" /opt/maintenance
|
|
|
|
push_file 121 /opt/maintenance/.env <<EOF
|
|
NODE_ENV=production
|
|
APP_SLUG=maintenance
|
|
DATABASE_URL=postgresql://maintenance:${MAINTENANCE_DB_PASS}@10.10.10.100:5432/maintenance_db
|
|
CENTRAL_AUTH_SECRET=${CENTRAL_AUTH_SECRET}
|
|
SETTINGS_URL=http://10.10.10.116:3080
|
|
SETTINGS_SECRET=${SETTINGS_SECRET}
|
|
OFFICE_IP_CHECK=${OFFICE_IP_CHECK:-${OFFICE_IP:-disabled}}
|
|
FRONTEND_PORT=3080
|
|
EOF
|
|
|
|
local build_out
|
|
if ! build_out=$(pct exec 121 -- bash -c "cd /opt/maintenance && docker compose up -d --build 2>&1"); then
|
|
msg_error "docker compose build failed in LXC 121:
|
|
${build_out}"
|
|
fi
|
|
|
|
msg_info "Waiting for maintenance"
|
|
wait_healthy 121 "http://localhost:3080/maintenance/health" \
|
|
&& msg_ok "Maintenance running at 10.10.10.121:3080" \
|
|
|| msg_warn "Maintenance may need extra time — check LXC 121"
|
|
|
|
# Seed app + capabilities into auth DB (psql on LXC 100 — same pattern as add-app.sh)
|
|
msg_info "Seeding maintenance into auth DB"
|
|
pct exec 100 -- docker exec hotel-manage-postgres psql -U postgres -d auth_db -c "
|
|
INSERT INTO apps (slug, name, description, base_path, icon, theme_color, category, internal_host, internal_port)
|
|
VALUES ('maintenance', 'Maintenance', 'Maintenance log book — faults, recurring tasks, assets and contractors', '/maintenance', 'Wrench', '#b45309', 'Operations', '10.10.10.121', 3080)
|
|
ON CONFLICT (slug) DO UPDATE SET
|
|
name=EXCLUDED.name, description=EXCLUDED.description, base_path=EXCLUDED.base_path,
|
|
icon=EXCLUDED.icon, theme_color=EXCLUDED.theme_color, category=EXCLUDED.category,
|
|
internal_host=EXCLUDED.internal_host, internal_port=EXCLUDED.internal_port;
|
|
INSERT INTO app_capabilities (app_id, slug, name, description, sort_order)
|
|
SELECT a.id, c.slug, c.name, c.description, c.sort_order
|
|
FROM apps a, (VALUES
|
|
('view','View Tasks','View the maintenance log and history',1),
|
|
('report','Report Faults','Create tasks, add photos and comments',2),
|
|
('update','Update Tasks','Change task state, reassign, edit details',3),
|
|
('resolve','Resolve Tasks','Mark tasks temporary fixed or fixed',4),
|
|
('costs','View Costs','See and enter repair cost values',5),
|
|
('manage_locations','Manage Locations','Manage locations, categories and NewBook sync',6),
|
|
('manage_assets','Manage Assets','Create and edit the asset register',7),
|
|
('manage_contractors','Manage Contractors','Manage contractors and their documents',8),
|
|
('manage_templates','Manage Recurring','Create and edit recurring task templates',9),
|
|
('settings','Settings','Configure maintenance app settings',10)
|
|
) AS c(slug, name, description, sort_order)
|
|
WHERE a.slug = 'maintenance'
|
|
ON CONFLICT (app_id, slug) DO NOTHING;
|
|
" &>/dev/null \
|
|
&& msg_ok "maintenance seeded into auth DB" \
|
|
|| msg_warn "Seed failed — run maintenance/seed-app.js manually via psql on LXC 100"
|
|
|
|
# Add /maintenance/ location to NPM proxy host
|
|
npm_add_location "/maintenance/" "10.10.10.121" 3080
|
|
}
|
|
|
|
deploy_forecasting() {
|
|
msg_step "Forecasting (LXC 113 · 10.10.10.113)"
|
|
|
|
if [[ -z "${FORECASTING_DB_PASS:-}" ]]; then
|
|
FORECASTING_DB_PASS=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)
|
|
printf '\nFORECASTING_DB_PASS=%s\n' "$FORECASTING_DB_PASS" >> "$CREDS_FILE"
|
|
msg_ok "Generated FORECASTING_DB_PASS → ${CREDS_FILE}"
|
|
fi
|
|
|
|
# Forecasting needs more RAM (ML models) and more disk (Chromium, Prophet/CmdStan)
|
|
msg_info "Creating LXC 113 (4 GB RAM, 2 cores)"
|
|
create_lxc 113 "10.10.10.113" "forecasting" 4096 2
|
|
pct resize 113 rootfs 20G &>/dev/null || true
|
|
msg_ok "LXC 113 created"
|
|
|
|
msg_info "Installing Docker"
|
|
install_docker 113
|
|
install_mgmt_key 113
|
|
msg_ok "Docker + SSH ready"
|
|
|
|
msg_info "Creating forecasting database"
|
|
pct exec 100 -- bash -c "
|
|
docker exec hotel-manage-postgres psql -U postgres -c \
|
|
\"CREATE USER forecasting WITH PASSWORD '${FORECASTING_DB_PASS}';\" 2>/dev/null || true
|
|
docker exec hotel-manage-postgres psql -U postgres -c \
|
|
\"CREATE DATABASE forecasting_db OWNER forecasting;\" 2>/dev/null || true
|
|
docker exec hotel-manage-postgres psql -U postgres -d forecasting_db -c \
|
|
\"GRANT ALL ON SCHEMA public TO forecasting;\" 2>/dev/null || true
|
|
" &>/dev/null
|
|
|
|
msg_ok "Database forecasting_db ready (schema applied by Python backend on first start)"
|
|
|
|
msg_info "Deploying forecasting"
|
|
deploy_service 113 "forecasting" "${REPO_ROOT}/forecasting" /opt/forecasting
|
|
|
|
# NewBook credentials come from the central Settings service (SETTINGS_URL).
|
|
# RESOS_API_KEY is configured via the app's Settings page after first deploy.
|
|
# ANTHROPIC_API_KEY is optional — enables the AI Dashboard feature.
|
|
push_file 113 /opt/forecasting/.env <<EOF
|
|
APP_SLUG=forecasting
|
|
DATABASE_URL=postgresql://forecasting:${FORECASTING_DB_PASS}@10.10.10.100:5432/forecasting_db
|
|
CENTRAL_AUTH_SECRET=${CENTRAL_AUTH_SECRET}
|
|
SETTINGS_URL=http://10.10.10.116:3080
|
|
SETTINGS_SECRET=${SETTINGS_SECRET}
|
|
RESOS_API_KEY=${RESOS_API_KEY:-}
|
|
ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
|
|
VITE_HOTEL_NAME=${SITE_NAME:-Hotel}
|
|
EOF
|
|
|
|
# Python ML build takes 20-30 min for Prophet/CmdStan compilation + Playwright install
|
|
msg_info "Building forecasting (ML deps — this takes 20-30 min)"
|
|
local build_out
|
|
if ! build_out=$(pct exec 113 -- bash -c "cd /opt/forecasting && docker compose up -d --build 2>&1"); then
|
|
msg_error "docker compose build failed in LXC 113:
|
|
${build_out}"
|
|
fi
|
|
|
|
msg_info "Waiting for forecasting (up to 10 min)"
|
|
wait_healthy 113 "http://localhost:3080/forecasting/health" 200 \
|
|
&& msg_ok "Forecasting running at 10.10.10.113:3080" \
|
|
|| msg_warn "Forecasting may need extra time — check LXC 113 logs"
|
|
|
|
# Seed app + capabilities into auth DB
|
|
msg_info "Seeding forecasting into auth DB"
|
|
pct exec 100 -- docker exec hotel-manage-postgres psql -U postgres -d auth_db -c "
|
|
INSERT INTO apps (slug, name, description, base_path, icon, theme_color, category, internal_host, internal_port)
|
|
VALUES ('forecasting', 'Forecasting', 'Revenue forecasting and reporting', '/forecasting', 'TrendingUp', '#0077b6', 'Finance', '10.10.10.113', 3080)
|
|
ON CONFLICT (slug) DO UPDATE SET
|
|
name=EXCLUDED.name, description=EXCLUDED.description, base_path=EXCLUDED.base_path,
|
|
icon=EXCLUDED.icon, theme_color=EXCLUDED.theme_color, category=EXCLUDED.category,
|
|
internal_host=EXCLUDED.internal_host, internal_port=EXCLUDED.internal_port;
|
|
INSERT INTO app_capabilities (app_id, slug, name, description, sort_order)
|
|
SELECT a.id, c.slug, c.name, c.description, c.sort_order
|
|
FROM apps a, (VALUES
|
|
('view','View Forecasts','View dashboard, forecasts, and history',1),
|
|
('view_bookability','View Bookability','View the rate / bookability matrix',2),
|
|
('view_competitor_rates','View Competitors','View competitor rate scraping and matrix',3),
|
|
('view_accuracy','View Accuracy','View model accuracy metrics and backtesting',4),
|
|
('manage_sync','Manage Data Sync','Trigger Newbook / Resos data syncs manually',5),
|
|
('settings','Manage Settings','Configure Newbook credentials and sync schedules',6)
|
|
) AS c(slug, name, description, sort_order)
|
|
WHERE a.slug = 'forecasting'
|
|
ON CONFLICT (app_id, slug) DO NOTHING;
|
|
" &>/dev/null \
|
|
&& msg_ok "forecasting seeded into auth DB" \
|
|
|| msg_warn "Seed failed — run forecasting/seed-app.js manually"
|
|
|
|
npm_add_location "/forecasting/" "10.10.10.113" 3080
|
|
}
|
|
|
|
deploy_rates() {
|
|
msg_step "Rate Monitor (LXC 115 · 10.10.10.115)"
|
|
|
|
if [[ -z "${RATES_DB_PASS:-}" ]]; then
|
|
RATES_DB_PASS=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)
|
|
printf '\nRATES_DB_PASS=%s\n' "$RATES_DB_PASS" >> "$CREDS_FILE"
|
|
msg_ok "Generated RATES_DB_PASS → ${CREDS_FILE}"
|
|
fi
|
|
if [[ -z "${RATES_SECRET:-}" ]]; then
|
|
RATES_SECRET=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 32)
|
|
printf '\nRATES_SECRET=%s\n' "$RATES_SECRET" >> "$CREDS_FILE"
|
|
msg_ok "Generated RATES_SECRET → ${CREDS_FILE}"
|
|
fi
|
|
|
|
msg_info "Creating LXC 115 (2 GB RAM, 2 cores)"
|
|
create_lxc 115 "10.10.10.115" "rates" 2048 2
|
|
msg_ok "LXC 115 created"
|
|
|
|
msg_info "Installing Docker"
|
|
install_docker 115
|
|
install_mgmt_key 115
|
|
msg_ok "Docker + SSH ready"
|
|
|
|
msg_info "Creating rates database"
|
|
pct exec 100 -- bash -c "
|
|
docker exec hotel-manage-postgres psql -U postgres -c \
|
|
\"CREATE USER rates WITH PASSWORD '${RATES_DB_PASS}';\" 2>/dev/null || true
|
|
docker exec hotel-manage-postgres psql -U postgres -c \
|
|
\"CREATE DATABASE rates_db OWNER rates;\" 2>/dev/null || true
|
|
docker exec hotel-manage-postgres psql -U postgres -d rates_db -c \
|
|
\"GRANT ALL ON SCHEMA public TO rates;\" 2>/dev/null || true
|
|
" &>/dev/null
|
|
|
|
msg_ok "Database rates_db ready (schema applied by Python backend on first start)"
|
|
|
|
msg_info "Deploying rates"
|
|
deploy_service 115 "rates" "${REPO_ROOT}/rates" /opt/rates
|
|
|
|
push_file 115 /opt/rates/.env <<EOF
|
|
APP_SLUG=rates
|
|
DATABASE_URL=postgresql://rates:${RATES_DB_PASS}@10.10.10.100:5432/rates_db
|
|
CENTRAL_AUTH_SECRET=${CENTRAL_AUTH_SECRET}
|
|
SETTINGS_URL=http://10.10.10.116:3080
|
|
SETTINGS_SECRET=${SETTINGS_SECRET}
|
|
EOF
|
|
|
|
msg_info "Building rates (Playwright install — takes 5-10 min)"
|
|
local build_out
|
|
if ! build_out=$(pct exec 115 -- bash -c "cd /opt/rates && docker compose up -d --build 2>&1"); then
|
|
msg_error "docker compose build failed in LXC 115:
|
|
${build_out}"
|
|
fi
|
|
|
|
msg_info "Waiting for rates"
|
|
wait_healthy 115 "http://localhost:3080/rates/health" 200 \
|
|
&& msg_ok "Rate Monitor running at 10.10.10.115:3080" \
|
|
|| msg_warn "Rate Monitor may need extra time — check LXC 115 logs"
|
|
|
|
# Seed app + capabilities into auth DB
|
|
msg_info "Seeding rates into auth DB"
|
|
pct exec 100 -- docker exec hotel-manage-postgres psql -U postgres -d auth_db -c "
|
|
INSERT INTO apps (slug, name, description, base_path, icon, theme_color, category, internal_host, internal_port)
|
|
VALUES ('rates', 'Rate Monitor', 'Competitor rate monitoring and direct booking engine rates', '/rates', 'Tag', '#7b4f00', 'Finance', '10.10.10.115', 3080)
|
|
ON CONFLICT (slug) DO UPDATE SET
|
|
name=EXCLUDED.name, description=EXCLUDED.description, base_path=EXCLUDED.base_path,
|
|
icon=EXCLUDED.icon, theme_color=EXCLUDED.theme_color, category=EXCLUDED.category,
|
|
internal_host=EXCLUDED.internal_host, internal_port=EXCLUDED.internal_port;
|
|
INSERT INTO app_capabilities (app_id, slug, name, description, sort_order)
|
|
SELECT a.id, c.slug, c.name, c.description, c.sort_order
|
|
FROM apps a, (VALUES
|
|
('view_own_rates','View Bookability','View own hotel rate and tariff availability',1),
|
|
('view_competitors','View Market View','View competitor Booking.com rates',2),
|
|
('view_direct_rates','View Direct Rates','View competitor direct booking engine rates',3),
|
|
('rate_analysis','Rate Analysis','Drill into competitor pricing structure',4),
|
|
('manage_scraper','Manage Scraper','Configure scraper, trigger manual scrapes',5),
|
|
('manage_hotels','Manage Competitors','Classify and configure competitor hotels',6)
|
|
) AS c(slug, name, description, sort_order)
|
|
WHERE a.slug = 'rates'
|
|
ON CONFLICT (app_id, slug) DO NOTHING;
|
|
" &>/dev/null \
|
|
&& msg_ok "rates seeded into auth DB" \
|
|
|| msg_warn "Seed failed — run auth db.js manually"
|
|
|
|
npm_add_location "/rates/" "10.10.10.115" 3080
|
|
}
|
|
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
# NPM PROXY HOSTS (via API)
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
|
|
# Returns a Bearer token for the NPM API, or empty string on failure.
|
|
# Robust against a messy creds file: tries the currently-sourced NPM_ADMIN_*
|
|
# pair, then EVERY email/pass pair found in the creds file (handles duplicates,
|
|
# ordering, and stale placeholders), then the NPM factory default.
|
|
npm_get_token() {
|
|
local npm_ip="10.10.10.3" # stable internal NPM IP (vmbr1) — API only needs internal reach
|
|
|
|
_try_token() {
|
|
local email=$1 pass=$2 tok
|
|
[[ -z "$email" || -z "$pass" ]] && return 1
|
|
tok=$(curl -sf -X POST "http://${npm_ip}:81/api/tokens" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"identity\":\"${email}\",\"secret\":\"${pass}\"}" \
|
|
2>/dev/null | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
|
|
[[ -n "$tok" ]] && { echo "$tok"; return 0; }
|
|
return 1
|
|
}
|
|
|
|
# 1. Currently-sourced pair
|
|
_try_token "${NPM_ADMIN_EMAIL:-}" "${NPM_ADMIN_PASS:-}" && return
|
|
|
|
# 2. Every email→pass pair in the creds file (pair each EMAIL with the
|
|
# NPM_ADMIN_PASS that follows it)
|
|
if [[ -f "$CREDS_FILE" ]]; then
|
|
local email="" line key val
|
|
while IFS= read -r line; do
|
|
case "$line" in
|
|
NPM_ADMIN_EMAIL=*) email="${line#*=}" ;;
|
|
NPM_ADMIN_PASS=*) _try_token "$email" "${line#*=}" && return; email="" ;;
|
|
esac
|
|
done < "$CREDS_FILE"
|
|
fi
|
|
|
|
# 3. NPM factory default (fresh, unconfigured install)
|
|
_try_token "admin@example.com" "changeme" && return
|
|
|
|
echo ""
|
|
}
|
|
|
|
# Idempotently add a custom location (path → forward_host:port) to the NPM
|
|
# proxy host that serves $DOMAIN. Robust JSON handling via python3 — the proxy
|
|
# host is located by matching $DOMAIN inside its domain_names array (field
|
|
# order in NPM's JSON is not guaranteed, so grep-based matching is unreliable).
|
|
npm_add_location() {
|
|
local path=$1 fwd_host=$2 fwd_port=$3
|
|
local npm_ip="10.10.10.3"
|
|
msg_info "Adding ${path} to NPM proxy"
|
|
|
|
local token; token=$(npm_get_token)
|
|
if [[ -z "$token" ]]; then
|
|
msg_warn "NPM API unavailable — add ${path} → ${fwd_host}:${fwd_port} manually in NPM admin"
|
|
return
|
|
fi
|
|
|
|
local hosts_json host_id
|
|
hosts_json=$(curl -sf "http://${npm_ip}:81/api/nginx/proxy-hosts" \
|
|
-H "Authorization: Bearer ${token}" 2>/dev/null) || true
|
|
host_id=$(echo "$hosts_json" | python3 -c "
|
|
import sys, json
|
|
try: hosts = json.load(sys.stdin)
|
|
except Exception: sys.exit(0)
|
|
dom = '${DOMAIN}'
|
|
for h in hosts:
|
|
if dom in (h.get('domain_names') or []):
|
|
print(h['id']); break
|
|
" 2>/dev/null) || true
|
|
|
|
if [[ -z "$host_id" ]]; then
|
|
msg_warn "NPM proxy host for ${DOMAIN} not found — add ${path} → ${fwd_host}:${fwd_port} manually"
|
|
return
|
|
fi
|
|
|
|
local existing merged
|
|
existing=$(curl -sf "http://${npm_ip}:81/api/nginx/proxy-hosts/${host_id}" \
|
|
-H "Authorization: Bearer ${token}" 2>/dev/null) || true
|
|
merged=$(echo "$existing" | python3 -c "
|
|
import sys, json
|
|
try: h = json.load(sys.stdin)
|
|
except Exception: sys.exit(0)
|
|
path, fh, fp = '${path}', '${fwd_host}', ${fwd_port}
|
|
locs = h.get('locations') or []
|
|
if any(l.get('path') == path for l in locs):
|
|
print('EXISTS'); sys.exit(0)
|
|
locs.append({'path':path,'forward_scheme':'http','forward_host':fh,'forward_port':fp,'advanced_config':''})
|
|
print(json.dumps(locs))
|
|
" 2>/dev/null) || true
|
|
|
|
if [[ "$merged" == "EXISTS" ]]; then
|
|
msg_ok "NPM location ${path} already present"
|
|
return
|
|
fi
|
|
if [[ -z "$merged" ]]; then
|
|
msg_warn "NPM merge failed — add ${path} → ${fwd_host}:${fwd_port} manually"
|
|
return
|
|
fi
|
|
|
|
if curl -sf -X PUT "http://${npm_ip}:81/api/nginx/proxy-hosts/${host_id}" \
|
|
-H "Authorization: Bearer ${token}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"locations\":${merged}}" &>/dev/null; then
|
|
msg_ok "NPM location ${path} added"
|
|
else
|
|
msg_warn "NPM update failed — add ${path} → ${fwd_host}:${fwd_port} manually"
|
|
fi
|
|
}
|
|
|
|
configure_npm_proxy_hosts() {
|
|
msg_step "Configuring NPM proxy hosts"
|
|
msg_info "Waiting for NPM API to be ready"
|
|
|
|
local npm_ip="10.10.10.3" # stable internal NPM IP (vmbr1)
|
|
local i=0
|
|
while ! curl -sf --max-time 3 "http://${npm_ip}:81/api/" &>/dev/null; do
|
|
sleep 3; i=$((i+1))
|
|
[[ $i -ge 30 ]] && { msg_warn "NPM API not responding — configure proxy hosts manually"; return; }
|
|
done
|
|
|
|
local token
|
|
token=$(npm_get_token)
|
|
|
|
if [[ -z "$token" ]]; then
|
|
msg_warn "Could not get NPM token — proxy hosts must be created manually (see summary)"
|
|
return
|
|
fi
|
|
|
|
msg_ok "NPM API authenticated"
|
|
|
|
# NPM's proxy-host endpoint is /api/nginx/proxy-hosts (NOT /api/proxy-hosts).
|
|
# NPM already sets X-Real-IP / X-Forwarded-For by default, so no advanced_config.
|
|
create_proxy_host() {
|
|
local forward_host=$1 forward_port=$2 locations_json=${3:-'[]'}
|
|
curl -sf -X POST "http://${npm_ip}:81/api/nginx/proxy-hosts" \
|
|
-H "Authorization: Bearer ${token}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{
|
|
\"domain_names\": [\"${DOMAIN}\"],
|
|
\"forward_scheme\": \"http\",
|
|
\"forward_host\": \"${forward_host}\",
|
|
\"forward_port\": ${forward_port},
|
|
\"access_list_id\": 0,
|
|
\"certificate_id\": 0,
|
|
\"ssl_forced\": false,
|
|
\"caching_enabled\": false,
|
|
\"block_exploits\": true,
|
|
\"allow_websocket_upgrade\": true,
|
|
\"http2_support\": false,
|
|
\"hsts_enabled\": false,
|
|
\"hsts_subdomains\": false,
|
|
\"advanced_config\": \"\",
|
|
\"meta\": {\"letsencrypt_agree\": false, \"dns_challenge\": false},
|
|
\"locations\": ${locations_json}
|
|
}" &>/dev/null && echo "created" || echo "failed"
|
|
}
|
|
|
|
# Single proxy host for the domain routed to the portal, with custom
|
|
# locations per app path (NPM's "custom locations" feature).
|
|
local locations='[
|
|
{"path":"/api/auth/","forward_scheme":"http","forward_host":"10.10.10.101","forward_port":3001,"advanced_config":""},
|
|
{"path":"/notices/","forward_scheme":"http","forward_host":"10.10.10.112","forward_port":3080,"advanced_config":""},
|
|
{"path":"/monitor/","forward_scheme":"http","forward_host":"10.10.10.105","forward_port":3002,"advanced_config":""},
|
|
{"path":"/cashup/","forward_scheme":"http","forward_host":"10.10.10.117","forward_port":3083,"advanced_config":""},
|
|
{"path":"/hk-planner/","forward_scheme":"http","forward_host":"10.10.10.118","forward_port":3080,"advanced_config":""},
|
|
{"path":"/twin-optimiser/","forward_scheme":"http","forward_host":"10.10.10.119","forward_port":3080,"advanced_config":""}
|
|
]'
|
|
|
|
local result; result=$(create_proxy_host "10.10.10.102" 3000 "$locations")
|
|
if [[ "$result" == "created" ]]; then
|
|
msg_ok "NPM proxy host created for ${DOMAIN}"
|
|
msg_warn "SSL certificate: configure in NPM admin UI after DNS is pointed at ${NPM_LAN_IP}"
|
|
else
|
|
msg_warn "NPM proxy host creation failed — create manually (see summary)"
|
|
fi
|
|
}
|
|
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
# SUMMARY
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
print_summary() {
|
|
printf "\n${GN}"
|
|
cat <<SUMMARY
|
|
╔══════════════════════════════════════════════════════════════╗
|
|
║ Hotel Manage — Stack Deployed ║
|
|
╚══════════════════════════════════════════════════════════════╝
|
|
SUMMARY
|
|
printf "${CL}"
|
|
|
|
cat <<SUMMARY
|
|
|
|
Site: ${SITE_NAME}
|
|
Domain: https://${DOMAIN}
|
|
|
|
── Services ──────────────────────────────────────────────────
|
|
LXC Hostname IP Port Status
|
|
100 hotel-manage-postgres 10.10.10.100 5432 (internal only)
|
|
101 hotel-manage-auth 10.10.10.101 3001 /api/auth/*
|
|
102 hotel-manage-portal 10.10.10.102 3000 /
|
|
103 hotel-manage-npm 10.10.10.3 80/443 entry point
|
|
(LAN) ${NPM_LAN_IP} 81 NPM admin
|
|
105 hotel-manage-management 10.10.10.105 3002 Uptime Kuma
|
|
9000 Forgejo webhooks
|
|
112 hotel-manage-noticeboard 10.10.10.112 3080 /notices/
|
|
116 hotel-manage-settings 10.10.10.116 3080 /settings/api/
|
|
117 hotel-manage-cashup 10.10.10.117 3083 /cashup/
|
|
118 hotel-manage-hk-planner 10.10.10.118 3080 /hk-planner/
|
|
119 hotel-manage-twin-optimiser 10.10.10.119 3080 /twin-optimiser/
|
|
|
|
── Credentials ───────────────────────────────────────────────
|
|
Admin login: ${ADMIN_EMAIL}
|
|
Credentials: /root/hotel-manage-credentials.txt (chmod 600)
|
|
|
|
── Next steps ────────────────────────────────────────────────
|
|
1. Point DNS: ${DOMAIN} → ${NPM_LAN_IP}
|
|
|
|
2. NPM admin UI: http://${NPM_LAN_IP}:81
|
|
Default: admin@example.com / changeme
|
|
→ Change password → Add SSL cert for ${DOMAIN}
|
|
→ Verify proxy host paths are routing correctly
|
|
|
|
3. Set up Uptime Kuma monitors:
|
|
http://10.10.10.105:3002
|
|
Health endpoints to monitor:
|
|
http://10.10.10.101:3001/health (auth)
|
|
http://10.10.10.102:3000/health (portal)
|
|
http://10.10.10.112:3080/notices/health (noticeboard)
|
|
|
|
4. Forgejo webhooks (when repos are pushed):
|
|
URL: http://10.10.10.105:9000/webhook
|
|
Secret: ${WEBHOOK_SECRET}
|
|
Events: Push
|
|
|
|
5. To add an app LXC later, on this host run:
|
|
bash <(curl -fsSL https://git.pterois.co.uk/hotel-manage-stack/stack-init/raw/branch/main/add-app.sh)
|
|
|
|
SUMMARY
|
|
}
|
|
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
# ENTRY POINT
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
|
|
# Single-service mode: install-stack.sh --only <service>
|
|
# Sources existing credentials and redeploys just the named service.
|
|
if [[ "${1:-}" == "--only" ]]; then
|
|
ONLY="${2:-}"
|
|
VALID="postgres auth portal npm management noticeboard settings cashup hk-planner twin-optimiser room-planner maintenance forecasting"
|
|
[[ -z "$ONLY" ]] && msg_error "Usage: install-stack.sh --only <service> (one of: ${VALID})"
|
|
grep -qw "$ONLY" <<< "$VALID" || msg_error "Unknown service '${ONLY}'. Valid: ${VALID}"
|
|
CREDS_FILE=/root/hotel-manage-credentials.txt
|
|
[[ -f "$CREDS_FILE" ]] || msg_error "No credentials file at ${CREDS_FILE} — run the full installer first"
|
|
# Read key=value pairs safely — handles values that contain spaces (e.g. SITE_NAME)
|
|
while IFS= read -r line; do
|
|
[[ "$line" =~ ^[A-Z_][A-Z0-9_]*= ]] || continue
|
|
key="${line%%=*}"; value="${line#*=}"
|
|
export "$key"="$value"
|
|
done < "$CREDS_FILE"
|
|
# Generate and append any secrets that didn't exist when the stack was first installed
|
|
_append_secret() {
|
|
local var=$1 val=$2
|
|
export "$var"="$val"
|
|
echo "${var}=${val}" >> "$CREDS_FILE"
|
|
msg_ok "Generated missing secret: ${var}"
|
|
}
|
|
[[ -z "${SETTINGS_DB_PASS:-}" ]] && _append_secret SETTINGS_DB_PASS \
|
|
"$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)"
|
|
[[ -z "${SETTINGS_SECRET:-}" ]] && _append_secret SETTINGS_SECRET \
|
|
"$(openssl rand -hex 32)"
|
|
[[ -z "${HK_PLANNER_DB_PASS:-}" ]] && _append_secret HK_PLANNER_DB_PASS \
|
|
"$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)"
|
|
[[ -z "${TWIN_OPT_DB_PASS:-}" ]] && _append_secret TWIN_OPT_DB_PASS \
|
|
"$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)"
|
|
[[ -z "${FORECASTING_DB_PASS:-}" ]] && _append_secret FORECASTING_DB_PASS \
|
|
"$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)"
|
|
|
|
USE_FORGEJO=true
|
|
[[ -f /root/.ssh/hotel-manage_deploy.pub ]] \
|
|
&& MGMT_PUBKEY=$(cat /root/.ssh/hotel-manage_deploy.pub) || MGMT_PUBKEY=""
|
|
check_vmbr1
|
|
ensure_pool
|
|
case "$ONLY" in
|
|
postgres) deploy_postgres ;;
|
|
auth) deploy_auth ;;
|
|
portal) deploy_portal ;;
|
|
npm) deploy_npm ;;
|
|
management) deploy_management ;;
|
|
noticeboard) deploy_noticeboard ;;
|
|
settings) deploy_settings ;;
|
|
cashup) deploy_cashup ;;
|
|
hk-planner) deploy_hk_planner ;;
|
|
twin-optimiser) deploy_twin_optimiser ;;
|
|
room-planner) deploy_room_planner ;;
|
|
maintenance) deploy_maintenance ;;
|
|
forecasting) deploy_forecasting ;;
|
|
rates) deploy_rates ;;
|
|
esac
|
|
exit 0
|
|
fi
|
|
|
|
check_vmbr1
|
|
collect_config
|
|
gen_secrets
|
|
gen_mgmt_ssh_key
|
|
ensure_pool
|
|
|
|
deploy_postgres
|
|
deploy_auth
|
|
deploy_portal
|
|
deploy_npm
|
|
deploy_management
|
|
deploy_noticeboard
|
|
deploy_settings
|
|
deploy_cashup
|
|
deploy_hk_planner
|
|
deploy_twin_optimiser
|
|
deploy_room_planner
|
|
deploy_maintenance
|
|
deploy_forecasting
|
|
deploy_rates
|
|
configure_npm_proxy_hosts
|
|
print_summary
|