From 18dd4f720d94cc20be74cdc3c9c652a244323856 Mon Sep 17 00:00:00 2001 From: Florian Schmidt Date: Fri, 17 Jul 2026 11:18:27 +0200 Subject: [PATCH] Deploy: Proxmox VE community-scripts LXC installer (Gitea, build-from-source) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds deploy/ct/metervault.sh (host) + deploy/install/metervault-install.sh (in-LXC), modeled on MQTTower's community-scripts installer but adapted for MeterVault: it lives on public Gitea (not GitHub) and is a single Blazor app + TimescaleDB (not multi-mode). The host script sources the community-scripts framework, creates a Debian 12 LXC and runs the install script via pct exec. The install script sets up PostgreSQL 16 + TimescaleDB, installs the .NET SDK, builds the app from the public Gitea repo (no prebuilt tarball exists — releases ship as a container image), writes /etc/metervault/environment + a systemd unit on port 8760, and installs an 'update' command (git pull + rebuild). scripts/run-metervault-ct-install.sh is the maintainer helper. All three pass shellcheck (warning-level clean) and bash -n. README documents the one-liner. Claude-Session: https://claude.ai/code/session_01Kib2MniVFbD95fkgLgBBnB --- README.md | 15 ++ deploy/ct/metervault.sh | 251 +++++++++++++++++++++++++++ deploy/install/metervault-install.sh | 248 ++++++++++++++++++++++++++ scripts/run-metervault-ct-install.sh | 39 +++++ 4 files changed, 553 insertions(+) create mode 100755 deploy/ct/metervault.sh create mode 100755 deploy/install/metervault-install.sh create mode 100755 scripts/run-metervault-ct-install.sh diff --git a/README.md b/README.md index 9d4557b..c8ca3bc 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,21 @@ docker compose -f deploy/docker-compose.yml up -d # API docs at http://localhost:8760/swagger ``` +## Quick start (Proxmox VE LXC) + +A community-scripts–style installer builds a self-contained LXC (Debian + PostgreSQL/TimescaleDB + +the app as a systemd service). Run **on the Proxmox host**: + +```bash +bash -c "$(curl -fsSL https://git.finalfactory.de/FinalFactory/MeterVault/raw/branch/master/deploy/ct/metervault.sh)" +``` + +It asks the standard container questions, optionally loads the demo dataset, and prints the URL +(`http://:8760`) plus the generated DB password. Re-run `update` inside the container to pull +the latest source and rebuild. It builds from this public Gitea repo (there is no prebuilt tarball — +releases ship as a container image). See [`deploy/ct/metervault.sh`](deploy/ct/metervault.sh) and +[`deploy/install/metervault-install.sh`](deploy/install/metervault-install.sh). + Configuration is via environment variables (`Section__Key` double-underscore mapping), e.g.: | Variable | Purpose | diff --git a/deploy/ct/metervault.sh b/deploy/ct/metervault.sh new file mode 100755 index 0000000..fa29f18 --- /dev/null +++ b/deploy/ct/metervault.sh @@ -0,0 +1,251 @@ +#!/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}" diff --git a/deploy/install/metervault-install.sh b/deploy/install/metervault-install.sh new file mode 100755 index 0000000..de6a3a5 --- /dev/null +++ b/deploy/install/metervault-install.sh @@ -0,0 +1,248 @@ +#!/usr/bin/env bash +# 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 +# +# Runs inside the LXC: installs PostgreSQL + TimescaleDB and the .NET SDK, builds the MeterVault +# Blazor app from the public Gitea repo, and runs it as a systemd service. MeterVault publishes a +# container image (not a prebuilt tarball), so — unlike MQTTower — this builds from source. + +: "${GITEA_BASE:=https://git.finalfactory.de}" +: "${GITEA_REPO:=FinalFactory/MeterVault}" +: "${METERVAULT_REPO_URL:=${GITEA_BASE}/${GITEA_REPO}.git}" +: "${METERVAULT_BRANCH:=master}" + +: "${METERVAULT_PORT:=8760}" +: "${METERVAULT_TIMEZONE:=Europe/Berlin}" +: "${METERVAULT_SEED:=false}" +: "${METERVAULT_DB_PASSWORD:=}" + +: "${PG_VERSION:=16}" +: "${DOTNET_CHANNEL:=10.0}" +: "${INSTALL_DIR:=/opt/metervault}" +: "${SOURCE_DIR:=/opt/metervault-src}" +: "${ENV_FILE:=/etc/metervault/environment}" +: "${DB_NAME:=metervault}" +: "${DB_USER:=metervault}" + +INSTALL_FUNC_URL="${INSTALL_FUNC_URL:-https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/install.func}" +FUNCTIONS_FILE_PATH="${FUNCTIONS_FILE_PATH:-$(curl -fsSL "$INSTALL_FUNC_URL")}" + +# shellcheck disable=SC1091 # community-scripts install.func is fetched at runtime +source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" +color +verb_ip6 +catch_errors +setting_up_container +network_check +update_os + +export APPLICATION="${APPLICATION:-MeterVault}" +export app="${app:-metervault}" + +need_cmd() { + command -v "$1" >/dev/null 2>&1 || { msg_error "Missing command: $1"; exit 127; } +} + +get_ipv4() { + hostname -I 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -n1 || true +} + +rand_hex() { + openssl rand -hex 16 2>/dev/null || head -c 16 /dev/urandom | xxd -p +} + +# Unprivileged LXC: systemctl enable may fail creating unit symlinks; start often works anyway. +systemctl_enable_now_best_effort() { + local svc="$1" + if systemctl enable -q --now "$svc" 2>/dev/null; then + return 0 + fi + msg_info "systemctl enable --now failed for ${svc}; trying start only (common in unprivileged LXC)." + systemctl daemon-reload 2>/dev/null || true + if systemctl start "$svc" 2>/dev/null && systemctl is-active --quiet "$svc" 2>/dev/null; then + return 0 + fi + msg_error "Could not start ${svc}. See: journalctl -xeu ${svc}" + return 1 +} + +# Append KEY=VALUE only if KEY is absent (safe re-runs / new keys across versions). +ensure_env_key() { + local file="$1" key="$2" value="$3" + if ! grep -q "^${key}=" "$file" 2>/dev/null; then + printf '%s=%s\n' "$key" "$value" >>"$file" + fi +} + +install_postgres_timescaledb() { + export DEBIAN_FRONTEND=noninteractive + msg_info "Adding PostgreSQL (PGDG) and TimescaleDB apt repositories" + $STD apt-get install -y gnupg postgresql-common apt-transport-https lsb-release wget ca-certificates + # PGDG: the official PostgreSQL apt repo (provides a consistent postgresql-${PG_VERSION}). + $STD /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y + local codename + codename="$(lsb_release -cs)" + echo "deb https://packagecloud.io/timescale/timescaledb/debian/ ${codename} main" >/etc/apt/sources.list.d/timescaledb.list + wget --quiet -O - https://packagecloud.io/timescale/timescaledb/gpgkey | gpg --dearmor -o /etc/apt/trusted.gpg.d/timescaledb.gpg + $STD apt-get update + msg_ok "Repositories added" + + msg_info "Installing PostgreSQL ${PG_VERSION} + TimescaleDB" + $STD apt-get install -y "postgresql-${PG_VERSION}" "postgresql-client-${PG_VERSION}" "timescaledb-2-postgresql-${PG_VERSION}" + msg_ok "Installed PostgreSQL + TimescaleDB" + + msg_info "Enabling the timescaledb preload" + local pgconf="/etc/postgresql/${PG_VERSION}/main/postgresql.conf" + if command -v timescaledb-tune >/dev/null 2>&1; then + timescaledb-tune --quiet --yes --pg-config "/usr/lib/postgresql/${PG_VERSION}/bin/pg_config" >/dev/null 2>&1 || true + fi + if ! grep -qE "^\s*shared_preload_libraries\s*=.*timescaledb" "$pgconf" 2>/dev/null; then + echo "shared_preload_libraries = 'timescaledb'" >>"$pgconf" + fi + systemctl restart postgresql + msg_ok "PostgreSQL restarted with timescaledb preloaded" +} + +provision_database() { + msg_info "Creating the ${DB_NAME} database and role" + [[ -n "${METERVAULT_DB_PASSWORD}" ]] || METERVAULT_DB_PASSWORD="$(rand_hex)" + # Role: create or reset the password (idempotent re-runs). + if sudo -u postgres psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='${DB_USER}'" | grep -q 1; then + sudo -u postgres psql -c "ALTER ROLE ${DB_USER} WITH LOGIN PASSWORD '${METERVAULT_DB_PASSWORD}';" >/dev/null + else + sudo -u postgres psql -c "CREATE ROLE ${DB_USER} WITH LOGIN PASSWORD '${METERVAULT_DB_PASSWORD}';" >/dev/null + fi + if ! sudo -u postgres psql -tAc "SELECT 1 FROM pg_database WHERE datname='${DB_NAME}'" | grep -q 1; then + sudo -u postgres psql -c "CREATE DATABASE ${DB_NAME} OWNER ${DB_USER};" >/dev/null + fi + # Pre-create the extension as superuser (CREATE EXTENSION timescaledb needs superuser); the app's + # migration then finds it present and its own CREATE EXTENSION IF NOT EXISTS is a no-op. + sudo -u postgres psql -d "${DB_NAME}" -c "CREATE EXTENSION IF NOT EXISTS timescaledb;" >/dev/null + msg_ok "Database ready" +} + +install_dotnet_sdk() { + export DEBIAN_FRONTEND=noninteractive + msg_info "Installing the .NET SDK (${DOTNET_CHANNEL}) to build from source" + local deb_ver + deb_ver="$(. /etc/os-release 2>/dev/null && echo "${VERSION_ID%%.*}" || echo "12")" + if [[ "$deb_ver" != "11" && "$deb_ver" != "12" && "$deb_ver" != "13" ]]; then + deb_ver="12" + fi + if [[ ! -f /etc/apt/sources.list.d/microsoft-prod.list && ! -f /etc/apt/sources.list.d/microsoft-prod.sources ]]; then + $STD curl -fsSL "https://packages.microsoft.com/config/debian/${deb_ver}/packages-microsoft-prod.deb" -o /tmp/packages-microsoft-prod.deb + $STD dpkg -i /tmp/packages-microsoft-prod.deb + rm -f /tmp/packages-microsoft-prod.deb + fi + $STD apt-get update -y + $STD apt-get install -y "dotnet-sdk-${DOTNET_CHANNEL}" + msg_ok "Installed .NET SDK" +} + +build_metervault() { + export DOTNET_CLI_TELEMETRY_OPTOUT=1 DOTNET_NOLOGO=1 + msg_info "Fetching MeterVault source (${METERVAULT_REPO_URL})" + if [[ -d "${SOURCE_DIR}/.git" ]]; then + git -C "${SOURCE_DIR}" fetch --depth 1 origin "${METERVAULT_BRANCH}" + git -C "${SOURCE_DIR}" reset --hard "origin/${METERVAULT_BRANCH}" + else + rm -rf "${SOURCE_DIR}" + $STD git clone --depth 1 --branch "${METERVAULT_BRANCH}" "${METERVAULT_REPO_URL}" "${SOURCE_DIR}" + fi + msg_ok "Source ready at ${SOURCE_DIR}" + + msg_info "Building MeterVault (dotnet publish — this can take a few minutes)" + # sampledata/ is at the repo root; the App csproj links it as Content so publish ships the demo CSVs. + $STD dotnet publish "${SOURCE_DIR}/src/App/MeterVault.App.csproj" -c Release -o "${INSTALL_DIR}" /p:UseAppHost=false + msg_ok "Published to ${INSTALL_DIR}" +} + +write_env() { + mkdir -p "$(dirname "${ENV_FILE}")" + umask 077 + local conn="Host=127.0.0.1;Port=5432;Database=${DB_NAME};Username=${DB_USER};Password=${METERVAULT_DB_PASSWORD}" + if [[ -f "${ENV_FILE}" ]]; then + msg_info "Environment exists; merging new keys and refreshing the connection string" + ensure_env_key "${ENV_FILE}" "ASPNETCORE_ENVIRONMENT" "Production" + ensure_env_key "${ENV_FILE}" "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT" "false" + ensure_env_key "${ENV_FILE}" "ASPNETCORE_URLS" "http://+:${METERVAULT_PORT}" + ensure_env_key "${ENV_FILE}" "MeterVault__TimeZone" "${METERVAULT_TIMEZONE}" + ensure_env_key "${ENV_FILE}" "MeterVault__Currency" "EUR" + ensure_env_key "${ENV_FILE}" "MeterVault__Locale" "en" + ensure_env_key "${ENV_FILE}" "MeterVault__SeedReferenceData" "${METERVAULT_SEED}" + sed -i "s|^ConnectionStrings__Default=.*|ConnectionStrings__Default=${conn}|" "${ENV_FILE}" + grep -q '^ConnectionStrings__Default=' "${ENV_FILE}" || printf 'ConnectionStrings__Default=%s\n' "${conn}" >>"${ENV_FILE}" + msg_ok "Updated ${ENV_FILE}" + else + cat <"${ENV_FILE}" +ASPNETCORE_ENVIRONMENT=Production +DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false +ASPNETCORE_URLS=http://+:${METERVAULT_PORT} +ConnectionStrings__Default=${conn} +MeterVault__TimeZone=${METERVAULT_TIMEZONE} +MeterVault__Currency=EUR +MeterVault__Locale=en +MeterVault__SeedReferenceData=${METERVAULT_SEED} +EOF + msg_ok "Wrote ${ENV_FILE}" + fi + chmod 600 "${ENV_FILE}" +} + +write_systemd() { + cat <<'EOF' >/etc/systemd/system/metervault.service +[Unit] +Description=MeterVault (self-hosted energy & utility metering) +After=network-online.target postgresql.service +Wants=network-online.target +Requires=postgresql.service + +[Service] +Type=simple +EnvironmentFile=/etc/metervault/environment +WorkingDirectory=/opt/metervault +ExecStart=/usr/bin/dotnet /opt/metervault/MeterVault.App.dll +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target +EOF +} + +write_update_command() { + local url="${METERVAULT_CT_URL:-https://git.finalfactory.de/FinalFactory/MeterVault/raw/branch/master/deploy/ct/metervault.sh}" + cat </usr/bin/update +#!/usr/bin/env bash +exec bash -c "\$(curl -fsSL '${url}')" +EOF + chmod +x /usr/bin/update +} + +main() { + need_cmd curl + ensure_dependencies git jq openssl sudo + + install_postgres_timescaledb + provision_database + install_dotnet_sdk + build_metervault + write_env + write_systemd + + systemctl daemon-reload 2>/dev/null || true + systemctl_enable_now_best_effort metervault + + motd_ssh + customize 2>/dev/null || true + write_update_command + cleanup_lxc 2>/dev/null || true + + local ip + ip="$(get_ipv4)" + msg_ok "MeterVault is up at http://${ip:-127.0.0.1}:${METERVAULT_PORT}" +} + +main "$@" diff --git a/scripts/run-metervault-ct-install.sh b/scripts/run-metervault-ct-install.sh new file mode 100755 index 0000000..8f61da8 --- /dev/null +++ b/scripts/run-metervault-ct-install.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Maintainer helper. Not part of the community-scripts tree under deploy/. +# +# If bash reports "$'\r': command not found" or "set: pipefail: invalid option", the file has +# CRLF line endings. Fix (use the real path to this file — not $0 from an interactive shell): +# sed -i 's/\r$//' /tmp/run-metervault-ct-install.sh && bash /tmp/run-metervault-ct-install.sh +# +# Run this **inside** an existing Debian LXC (not on the Proxmox host). It exports the same +# METERVAULT_* variables that deploy/ct/metervault.sh passes via pct exec, then runs the official +# install script from the public Gitea repo. +# +# Optional overrides file: export METERVAULT_INSTALL_ENV=/root/metervault-install.env + +set -euo pipefail + +[[ "${1:-}" == "-h" || "${1:-}" == "--help" ]] && { echo "Usage: $0 (run as root inside the LXC)"; exit 0; } +[[ "$(id -u)" -eq 0 ]] || { echo "error: run as root inside the container" >&2; exit 1; } + +rand_hex() { openssl rand -hex 16 2>/dev/null || head -c 16 /dev/urandom | xxd -p; } + +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}" + +[[ -n "${METERVAULT_INSTALL_ENV:-}" && -f "${METERVAULT_INSTALL_ENV}" ]] && { set -a; # shellcheck source=/dev/null + source "${METERVAULT_INSTALL_ENV}"; set +a; } + +export CONTAINER_INSTALLING="${CONTAINER_INSTALLING:-true}" +export METERVAULT_CT_URL="${METERVAULT_CT_URL:-${METERVAULT_DEPLOY_BASE}/ct/metervault.sh}" +export METERVAULT_PORT="${METERVAULT_PORT:-8760}" +export METERVAULT_TIMEZONE="${METERVAULT_TIMEZONE:-Europe/Berlin}" +export METERVAULT_SEED="${METERVAULT_SEED:-false}" +if [[ -z "${METERVAULT_DB_PASSWORD:-}" ]]; then + METERVAULT_DB_PASSWORD="$(rand_hex)" + echo "Generated METERVAULT_DB_PASSWORD: ${METERVAULT_DB_PASSWORD}" +fi +export METERVAULT_DB_PASSWORD + +echo "METERVAULT_INSTALL_URL=${METERVAULT_INSTALL_URL} PORT=${METERVAULT_PORT} SEED=${METERVAULT_SEED}" +exec bash -c "$(curl -fsSL "$METERVAULT_INSTALL_URL")"