Add deploy_hvac() with dual-homed admin-VLAN support

LXC 128, hvac_db, hvac app — plus generic dual-NIC support in create_lxc()
(admin VLAN bridge/tag/IP asked lazily via whiptail, persisted per-hotel in
the credentials file, since these vary per site). Used now for hvac's future
Modbus/Midea/Daikin direct-LAN drivers; the shared MQTT broker LXC will reuse
the same ensure_admin_vlan_config()/ensure_admin_vlan_ip() helpers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-26 20:19:24 +00:00
parent 2fd6305e4f
commit 20e0c52ea0

View file

@ -361,19 +361,30 @@ ensure_pool() {
# container skip AppArmor via `security_opt: apparmor=unconfined` in its compose. # container skip AppArmor via `security_opt: apparmor=unconfined` in its compose.
# (Overriding lxc.apparmor.profile would cancel nesting — don't do that.) # (Overriding lxc.apparmor.profile would cancel nesting — don't do that.)
create_lxc() { 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 if lxc_exists "$id"; then
msg_warn "LXC $id (hotel-manage-${name}) already exists — skipping creation" msg_warn "LXC $id (hotel-manage-${name}) already exists — skipping creation"
lxc_running "$id" || pct start "$id" lxc_running "$id" || pct start "$id"
return return
fi fi
local tmpl; tmpl=$(get_template) 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" \ pct create "$id" "$tmpl" \
--hostname "hotel-manage-${name}" \ --hostname "hotel-manage-${name}" \
--memory "$mem" \ --memory "$mem" \
--cores "$cores" \ --cores "$cores" \
--rootfs "${STORAGE}:8" \ --rootfs "${STORAGE}:8" \
--net0 "name=eth0,bridge=vmbr1,ip=${ip}/24,gw=10.10.10.1" \ --net0 "name=eth0,bridge=vmbr1,ip=${ip}/24,gw=10.10.10.1" \
"${net1_args[@]}" \
--features nesting=1 \ --features nesting=1 \
--unprivileged 0 \ --unprivileged 0 \
--onboot 1 \ --onboot 1 \
@ -383,6 +394,38 @@ create_lxc() {
sleep 5 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() { create_npm_lxc() {
local id=103 local id=103
if lxc_exists "$id"; then if lxc_exists "$id"; then
@ -1670,6 +1713,101 @@ ${build_out}"
npm_add_location "/utilities/" "10.10.10.127" 3080 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 <<EOF
NODE_ENV=production
APP_SLUG=hvac
DATABASE_URL=postgresql://hvac:${HVAC_DB_PASS}@10.10.10.100:5432/hvac_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}}
NEWBOOK_LOCATION_ID=${NEWBOOK_LOCATION_ID:-}
FRONTEND_PORT=3080
EOF
local build_out
if ! build_out=$(pct exec 128 -- bash -c "cd /opt/hvac && docker compose up -d --build 2>&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() { deploy_kitchen() {
msg_step "Kitchen (LXC 110 · 10.10.10.110)" msg_step "Kitchen (LXC 110 · 10.10.10.110)"
@ -2081,7 +2219,7 @@ SUMMARY
# Sources existing credentials and redeploys just the named service. # Sources existing credentials and redeploys just the named service.
if [[ "${1:-}" == "--only" ]]; then if [[ "${1:-}" == "--only" ]]; then
ONLY="${2:-}" 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 <service> (one of: ${VALID})" [[ -z "$ONLY" ]] && msg_error "Usage: install-stack.sh --only <service> (one of: ${VALID})"
grep -qw "$ONLY" <<< "$VALID" || msg_error "Unknown service '${ONLY}'. Valid: ${VALID}" grep -qw "$ONLY" <<< "$VALID" || msg_error "Unknown service '${ONLY}'. Valid: ${VALID}"
CREDS_FILE=/root/hotel-manage-credentials.txt 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)" "$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24)"
[[ -z "${UTILITIES_API_KEY:-}" ]] && _append_secret UTILITIES_API_KEY \ [[ -z "${UTILITIES_API_KEY:-}" ]] && _append_secret UTILITIES_API_KEY \
"$(openssl rand -hex 24)" "$(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 USE_FORGEJO=true
[[ -f /root/.ssh/hotel-manage_deploy.pub ]] \ [[ -f /root/.ssh/hotel-manage_deploy.pub ]] \
@ -2145,6 +2285,7 @@ if [[ "${1:-}" == "--only" ]]; then
kds) deploy_kds ;; kds) deploy_kds ;;
wages) deploy_wages ;; wages) deploy_wages ;;
utilities) deploy_utilities ;; utilities) deploy_utilities ;;
hvac) deploy_hvac ;;
esac esac
exit 0 exit 0
fi fi
@ -2174,5 +2315,6 @@ deploy_reports
deploy_kitchen deploy_kitchen
deploy_kds deploy_kds
deploy_wages deploy_wages
deploy_hvac
configure_npm_proxy_hosts configure_npm_proxy_hosts
print_summary print_summary