#!/usr/bin/env bash # shellcheck disable=SC1090 # community-scripts build.func is fetched at runtime source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func) # Copyright (c) 2021-2026 community-scripts ORG # Author: FinalFactory # License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE # Source: https://git.finalfactory.de/FinalFactory/MeterVault # # Creates a Debian LXC and installs MeterVault (self-hosted energy & utility metering) natively: # PostgreSQL + TimescaleDB, the .NET runtime, and the Blazor app built from the public Gitea repo, # fronted by a systemd service. Unlike MQTTower (GitHub + prebuilt release tarballs), MeterVault # lives on a public Gitea and ships only a container image, so this builds the app from source. APP="MeterVault" var_tags="${var_tags:-energy;metering;iot}" var_cpu="${var_cpu:-2}" var_ram="${var_ram:-3072}" var_disk="${var_disk:-15}" var_os="${var_os:-debian}" # Debian 12 (bookworm): TimescaleDB's apt packages + PGDG PostgreSQL 16 are reliably built for it. var_version="${var_version:-12}" var_unprivileged="${var_unprivileged:-1}" header_info "$APP" variables color catch_errors # Gitea (public) instead of GitHub: raw file URLs use /raw/branch//. METERVAULT_DEPLOY_BASE="${METERVAULT_DEPLOY_BASE:-https://git.finalfactory.de/FinalFactory/MeterVault/raw/branch/master/deploy}" METERVAULT_INSTALL_URL="${METERVAULT_INSTALL_URL:-${METERVAULT_DEPLOY_BASE}/install/metervault-install.sh}" function update_script() { header_info check_container_storage check_container_resources if [[ ! -d /opt/metervault ]]; then msg_error "No ${APP} Installation Found!" exit fi if [[ ! -d /opt/metervault-src ]]; then msg_error "No source checkout at /opt/metervault-src — cannot rebuild. Reinstall to restore it." exit fi msg_info "Stopping ${APP}" systemctl stop metervault msg_ok "Stopped ${APP}" msg_info "Pulling latest source and rebuilding (this can take a few minutes)" cd /opt/metervault-src || exit 1 git fetch --depth 1 origin master git reset --hard origin/master dotnet publish src/App/MeterVault.App.csproj -c Release -o /opt/metervault /p:UseAppHost=false msg_ok "Rebuilt ${APP}" msg_info "Starting ${APP}" systemctl start metervault msg_ok "Started ${APP}" msg_ok "Updated successfully!" exit } start # ============================================================================= # Custom questions (after the standard wizard, before build) # ============================================================================= rand_hex() { openssl rand -hex 16 2>/dev/null || head -c 16 /dev/urandom | xxd -p } # msg_info() runs a background spinner on stderr; whiptail also draws on stderr. Stop the spinner # and clear the line first so dialogs don't render corrupted. metervault_whiptail() { stop_spinner clear_line whiptail "$@" 3>&1 1>&2 2>&3 } collect_metervault_env() { export METERVAULT_PORT="${METERVAULT_PORT:-8760}" export METERVAULT_TIMEZONE="${METERVAULT_TIMEZONE:-Europe/Berlin}" if [[ -z "${METERVAULT_DB_PASSWORD:-}" ]]; then METERVAULT_DB_PASSWORD="$(rand_hex)" msg_info "Generated PostgreSQL password for the metervault user: ${METERVAULT_DB_PASSWORD}" fi export METERVAULT_DB_PASSWORD # Demo dataset: honour an explicit env value, otherwise ask. local seed="${METERVAULT_SEED:-}" case "${seed,,}" in true | 1 | yes) export METERVAULT_SEED="true" ;; false | 0 | no) export METERVAULT_SEED="false" ;; *) if metervault_whiptail --title "${APP}" --yesno \ "Load the bundled Energiebilanz demo dataset (meters, tariffs, ~2000 readings) on first start?" 10 70; then export METERVAULT_SEED="true" else export METERVAULT_SEED="false" fi ;; esac } # ============================================================================= # Custom build_container — identical to the framework except the install script URL. # The framework build_container() hardcodes the install URL to community-scripts/ProxmoxVE; # once MeterVault is accepted upstream this whole function can be replaced by build_container. # ============================================================================= metervault_build_container() { # --- Network string (same logic as build.func) --- NET_STRING="-net0 name=eth0,bridge=${BRG:-vmbr0}" [[ -n "${MAC:-}" ]] && case "$MAC" in ,hwaddr=*) NET_STRING+="$MAC" ;; *) NET_STRING+=",hwaddr=$MAC" ;; esac NET_STRING+=",ip=${NET:-dhcp}" [[ -n "${GATE:-}" ]] && case "$GATE" in ,gw=) ;; ,gw=*) NET_STRING+="$GATE" ;; *) NET_STRING+=",gw=$GATE" ;; esac [[ -n "${VLAN:-}" ]] && case "$VLAN" in ,tag=*) NET_STRING+="$VLAN" ;; *) NET_STRING+=",tag=$VLAN" ;; esac [[ -n "${MTU:-}" ]] && case "$MTU" in ,mtu=*) NET_STRING+="$MTU" ;; *) NET_STRING+=",mtu=$MTU" ;; esac case "${IPV6_METHOD:-none}" in auto) NET_STRING+=",ip6=auto" ;; dhcp) NET_STRING+=",ip6=dhcp" ;; static) [[ -n "${IPV6_ADDR:-}" ]] && { NET_STRING+=",ip6=$IPV6_ADDR"; [[ -n "${IPV6_GATE:-}" ]] && NET_STRING+=",gw6=$IPV6_GATE"; } ;; esac # --- Features --- FEATURES="nesting=1" [[ "$CT_TYPE" == "1" ]] && FEATURES="${FEATURES},keyctl=1" # --- Download install.func for the container --- export FUNCTIONS_FILE_PATH FUNCTIONS_FILE_PATH="$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/install.func)" if [[ -z "$FUNCTIONS_FILE_PATH" || ${#FUNCTIONS_FILE_PATH} -lt 100 ]]; then msg_error "Failed to download install.func" exit 115 fi # --- Standard exports (same as build.func build_container) --- export DIAGNOSTICS="${DIAGNOSTICS:-no}" export RANDOM_UUID="${RANDOM_UUID:-$(cat /proc/sys/kernel/random/uuid)}" export EXECUTION_ID="${EXECUTION_ID:-$RANDOM_UUID}" export SESSION_ID="${SESSION_ID:-${RANDOM_UUID:0:8}}" export CACHER="${APT_CACHER:-}" export CACHER_IP="${APT_CACHER_IP:-}" export tz="${timezone:-$(timedatectl show --value --property=Timezone 2>/dev/null || echo UTC)}" export APPLICATION="$APP" export app="$NSAPP" export PASSWORD="${PW:-}" export VERBOSE="${VERBOSE:-no}" export SSH_ROOT="${SSH:-no}" export SSH_AUTHORIZED_KEY="${SSH_AUTHORIZED_KEY:-}" export CTID="${CT_ID:-}" export CTTYPE="${CT_TYPE:-1}" export PCT_OSTYPE="$var_os" export PCT_OSVERSION="$var_version" export PCT_DISK_SIZE="${DISK_SIZE:-$var_disk}" export IPV6_METHOD="${IPV6_METHOD:-none}" BUILD_LOG="${BUILD_LOG:-/tmp/create-lxc-${SESSION_ID}.log}" export BUILD_LOG export INSTALL_LOG="/root/.install-${SESSION_ID}.log" # --- MeterVault-specific exports --- export METERVAULT_CT_URL="${METERVAULT_CT_URL:-${METERVAULT_DEPLOY_BASE}/ct/metervault.sh}" # --- Build PCT_OPTIONS string --- PCT_OPTIONS_STRING=" -hostname ${HN:-metervault}" [[ -n "${TAGS:-}" ]] && PCT_OPTIONS_STRING+=$'\n'" -tags $TAGS" [[ -n "$FEATURES" ]] && PCT_OPTIONS_STRING=" -features $FEATURES"$'\n'"$PCT_OPTIONS_STRING" [[ -n "${SD:-}" ]] && PCT_OPTIONS_STRING+=$'\n'" $SD" [[ -n "${NS:-}" ]] && PCT_OPTIONS_STRING+=$'\n'" $NS" PCT_OPTIONS_STRING+=$'\n'" $NET_STRING" PCT_OPTIONS_STRING+=$'\n'" -onboot 1" PCT_OPTIONS_STRING+=$'\n'" -cores ${CORE_COUNT:-$var_cpu}" PCT_OPTIONS_STRING+=$'\n'" -memory ${RAM_SIZE:-$var_ram}" PCT_OPTIONS_STRING+=$'\n'" -unprivileged ${CT_TYPE:-1}" [[ -n "${PW:-}" ]] && PCT_OPTIONS_STRING+=$'\n'" $PW" export PCT_OPTIONS="$PCT_OPTIONS_STRING" export TEMPLATE_STORAGE="${var_template_storage:-}" export CONTAINER_STORAGE="${var_container_storage:-}" # --- Create LXC (framework function) --- create_lxc_container || exit $? # --- Start container and wait for network --- msg_info "Starting LXC Container" pct start "$CTID" for i in {1..10}; do if pct status "$CTID" | grep -q "status: running"; then msg_ok "Started LXC Container" break fi sleep 1 [[ "$i" -eq 10 ]] && { msg_error "LXC Container did not start"; exit 117; } done msg_info "Waiting for network in LXC container" local ip_in_lxc="" wait_secs=0 while true; do ip_in_lxc=$(pct exec "$CTID" -- ip -4 addr show dev eth0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1) [[ -z "$ip_in_lxc" ]] && ip_in_lxc=$(pct exec "$CTID" -- ip -6 addr show dev eth0 scope global 2>/dev/null | awk '/inet6 / {print $2}' | cut -d/ -f1 | head -n1) [[ -n "$ip_in_lxc" ]] && break sleep 1 wait_secs=$((wait_secs + 1)) if ((wait_secs % 20 == 0)); then msg_warn "No IP on eth0 after ${wait_secs}s — still waiting (Ctrl+C to abort)" fi done msg_ok "Network in LXC is reachable (${ip_in_lxc})" # --- Base packages --- msg_info "Customizing LXC Container" sleep 2 pct exec "$CTID" -- bash -c "apt-get update 2>&1 && apt-get install -y sudo curl mc gnupg2 jq 2>&1" >>"$BUILD_LOG" 2>&1 || { msg_error "Failed to install base packages" exit 116 } msg_ok "Customized LXC Container" # --- Run MeterVault install script (the ONLY difference vs the framework build_container) --- # pct exec (not lxc-attach): unprivileged LXC often returns EPERM from lxc-attach, and pct exec # does not inherit the host env — pass METERVAULT_* explicitly (collect_metervault_env). Append to # BUILD_LOG so failures show the real error. msg_info "Running MeterVault install script (full output: ${BUILD_LOG})" pct exec "$CTID" -- env \ CONTAINER_INSTALLING=true \ "METERVAULT_CT_URL=${METERVAULT_CT_URL:-${METERVAULT_DEPLOY_BASE}/ct/metervault.sh}" \ "METERVAULT_PORT=${METERVAULT_PORT:-8760}" \ "METERVAULT_TIMEZONE=${METERVAULT_TIMEZONE:-Europe/Berlin}" \ "METERVAULT_DB_PASSWORD=${METERVAULT_DB_PASSWORD:-}" \ "METERVAULT_SEED=${METERVAULT_SEED:-false}" \ bash -c "$(curl -fsSL "$METERVAULT_INSTALL_URL")" >>"$BUILD_LOG" 2>&1 || { msg_error "MeterVault install script failed — see tail of ${BUILD_LOG}" exit 1 } } collect_metervault_env metervault_build_container description msg_ok "Completed Successfully!\n" echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" echo -e "${INFO}${YW} Access it using the following URL:${CL}" echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:${METERVAULT_PORT:-8760}${CL}" echo -e "${TAB}${INFO}First run: open Import → \"Load reference data\" for a demo (unless you enabled seeding).${CL}" echo -e "${TAB}${INFO}PostgreSQL user 'metervault' password: ${METERVAULT_DB_PASSWORD:-} (also in /etc/metervault/environment)${CL}"