diff --git a/install-stack.sh b/install-stack.sh index 81aa3b8..9372de3 100755 --- a/install-stack.sh +++ b/install-stack.sh @@ -361,19 +361,30 @@ ensure_pool() { # 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} + local id=$1 ip=$2 name=$3 mem=${4:-512} cores=${5:-1} vlan_ip=${6:-} 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) + # Optional second NIC on the admin VLAN (dual-homed, same pattern as NPM/103) — + # used by containers that need direct LAN reach (e.g. hvac's Modbus/Midea/Daikin + # drivers, the shared MQTT broker). Bridge/tag are site-specific — see + # ensure_admin_vlan_config(). + local net1_args=() + if [[ -n "$vlan_ip" ]]; then + local tag_part="" + [[ -n "${ADMIN_VLAN_TAG:-}" ]] && tag_part=",tag=${ADMIN_VLAN_TAG}" + net1_args=(--net1 "name=eth1,bridge=${ADMIN_VLAN_BRIDGE:-vmbr0}${tag_part},ip=${vlan_ip}/24") + fi 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" \ + "${net1_args[@]}" \ --features nesting=1 \ --unprivileged 0 \ --onboot 1 \ @@ -383,6 +394,38 @@ create_lxc() { sleep 5 } +# ── Admin VLAN config (dual-homed containers) — asked once, lazily, per hotel ── +# Bridge name and VLAN tag vary per hotel install (e.g. a plain vmbr0 with no tag +# at one site, a tagged VLAN on a trunk port at another) — never hardcode either. +ensure_admin_vlan_config() { + [[ -n "${ADMIN_VLAN_BRIDGE:-}" ]] && return + ADMIN_VLAN_BRIDGE=$(whiptail --title "Hotel Manage — Admin VLAN" \ + --inputbox "Proxmox bridge carrying the admin/HVAC-device VLAN (varies per hotel):" \ + 8 66 "vmbr0" 3>&1 1>&2 2>&3) || exit 0 + ADMIN_VLAN_TAG=$(whiptail --title "Hotel Manage — Admin VLAN" \ + --inputbox "VLAN tag number for that bridge (leave blank if untagged/native VLAN — varies per hotel):" \ + 8 66 "" 3>&1 1>&2 2>&3) || exit 0 + { + echo "ADMIN_VLAN_BRIDGE=${ADMIN_VLAN_BRIDGE}" + echo "ADMIN_VLAN_TAG=${ADMIN_VLAN_TAG}" + } >> "$CREDS_FILE" + msg_ok "Admin VLAN config saved → ${CREDS_FILE}" +} + +# Ask for (and persist) a static IP on the admin VLAN for one dual-homed +# container. Kept per-consumer (not a single shared value) since each +# dual-homed LXC needs its own address on that VLAN. +ensure_admin_vlan_ip() { + local var=$1 label=$2 default=$3 + [[ -n "${!var:-}" ]] && return + local val + val=$(whiptail --title "Hotel Manage — Admin VLAN" \ + --inputbox "${label} (varies per hotel):" 8 66 "${default}" 3>&1 1>&2 2>&3) || exit 0 + export "$var"="$val" + echo "${var}=${val}" >> "$CREDS_FILE" + msg_ok "${var} saved → ${CREDS_FILE}" +} + create_npm_lxc() { local id=103 if lxc_exists "$id"; then @@ -1670,6 +1713,101 @@ ${build_out}" npm_add_location "/utilities/" "10.10.10.127" 3080 } +deploy_hvac() { + msg_step "HVAC (LXC 128 · 10.10.10.128)" + + if [[ -z "${HVAC_DB_PASS:-}" ]]; then + HVAC_DB_PASS=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24) + printf '\nHVAC_DB_PASS=%s\n' "$HVAC_DB_PASS" >> "$CREDS_FILE" + msg_ok "Generated HVAC_DB_PASS → ${CREDS_FILE}" + fi + + ensure_admin_vlan_config + ensure_admin_vlan_ip HVAC_ADMIN_VLAN_IP \ + "Static IP for hvac's admin-VLAN interface (net1 — direct Modbus/Midea/Daikin device access)" \ + "10.4.0.60" + + msg_info "Creating LXC 128" + create_lxc 128 "10.10.10.128" "hvac" 1024 1 "${HVAC_ADMIN_VLAN_IP}" + msg_ok "LXC 128 created (dual-homed: 10.10.10.128 + ${HVAC_ADMIN_VLAN_IP} on ${ADMIN_VLAN_BRIDGE}${ADMIN_VLAN_TAG:+ tag ${ADMIN_VLAN_TAG}})" + + msg_info "Installing Docker" + install_docker 128 + install_mgmt_key 128 + msg_ok "Docker + SSH ready" + + msg_info "Creating hvac database" + pct exec 100 -- bash -c " + docker exec hotel-manage-postgres psql -U postgres -c \ + \"CREATE USER hvac WITH PASSWORD '${HVAC_DB_PASS}';\" 2>/dev/null || true + docker exec hotel-manage-postgres psql -U postgres -c \ + \"CREATE DATABASE hvac_db OWNER hvac;\" 2>/dev/null || true + docker exec hotel-manage-postgres psql -U postgres -d hvac_db -c \ + \"GRANT ALL ON SCHEMA public TO hvac;\" 2>/dev/null || true + " &>/dev/null + msg_ok "Database hvac_db ready" + + msg_info "Deploying hvac" + deploy_service 128 "hvac" "${REPO_ROOT}/hvac" /opt/hvac + + # No MQTT broker LXC exists yet (separate infra piece, not part of this deploy) — + # hvac's mqtt.js connects out and retries with backoff, so this is safe to deploy + # before the broker exists. NEWBOOK_LOCATION_ID matches every other NewBook app. + push_file 128 /opt/hvac/.env <&1"); then + msg_error "docker compose build failed in LXC 128: +${build_out}" + fi + + msg_info "Waiting for hvac" + wait_healthy 128 "http://localhost:3080/hvac/health" \ + && msg_ok "HVAC running at 10.10.10.128:3080" \ + || msg_warn "HVAC may need extra time — check LXC 128" + + msg_info "Seeding hvac 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 ('hvac', 'HVAC', 'Room heating control — NewBook-driven TRV scheduling, aircon and boiler (phased)', '/hvac', 'Thermometer', '#c1440e', 'Operations', '10.10.10.128', 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','View zone dashboard, device status and activity',1), + ('control','Manual Override','Force a zone''s temperature and disable auto mode',2), + ('schedule_edit','Edit Schedules','Adjust per-zone temps, offsets and auto mode',3), + ('manage_devices','Manage Devices','Discover, map, photograph devices; sync zones from NewBook',4), + ('public_area_control','Public Area Control','Central control of public-area zones (Phase 3)',5), + ('boiler_view','Boiler — View','View boiler controller status (Phase 4)',6), + ('boiler_control','Boiler — Control','Adjust boiler weather-compensation / pump disable (Phase 4)',7), + ('settings','Settings','Configure hvac app settings',8) + ) AS c(slug, name, description, sort_order) + WHERE a.slug = 'hvac' + ON CONFLICT (app_id, slug) DO NOTHING; + " &>/dev/null \ + && msg_ok "hvac seeded into auth DB" \ + || msg_warn "Seed failed — run hvac/seed-app.js manually" + + npm_add_location "/hvac/" "10.10.10.128" 3080 + + msg_warn "hvac deployed but the shared MQTT broker (LXC 104) is separate infra and not provisioned by this installer — device control will retry/backoff until it exists. See the hvac plan doc's 'MQTT settings & auth' section." + msg_warn "hvac's admin-VLAN NIC (${HVAC_ADMIN_VLAN_IP}) is up but unused by Phase 1 (TRVs go via the MQTT broker) — it's provisioned now for Phase 2/3 Modbus/Midea/Daikin drivers, which aren't built yet." +} + deploy_kitchen() { msg_step "Kitchen (LXC 110 · 10.10.10.110)" @@ -2081,7 +2219,7 @@ SUMMARY # 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 rates reports kitchen kds wages utilities" + VALID="postgres auth portal npm management noticeboard settings cashup hk-planner twin-optimiser room-planner maintenance forecasting rates reports kitchen kds wages utilities hvac" [[ -z "$ONLY" ]] && msg_error "Usage: install-stack.sh --only (one of: ${VALID})" grep -qw "$ONLY" <<< "$VALID" || msg_error "Unknown service '${ONLY}'. Valid: ${VALID}" CREDS_FILE=/root/hotel-manage-credentials.txt @@ -2119,6 +2257,8 @@ if [[ "${1:-}" == "--only" ]]; then "$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)" [[ -z "${UTILITIES_API_KEY:-}" ]] && _append_secret UTILITIES_API_KEY \ "$(openssl rand -hex 24)" + [[ -z "${HVAC_DB_PASS:-}" ]] && _append_secret HVAC_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 ]] \ @@ -2145,6 +2285,7 @@ if [[ "${1:-}" == "--only" ]]; then kds) deploy_kds ;; wages) deploy_wages ;; utilities) deploy_utilities ;; + hvac) deploy_hvac ;; esac exit 0 fi @@ -2174,5 +2315,6 @@ deploy_reports deploy_kitchen deploy_kds deploy_wages +deploy_hvac configure_npm_proxy_hosts print_summary