From d9ff0506769c68bc2daaa6396b017996735a52fa Mon Sep 17 00:00:00 2001 From: Meliox Date: Sun, 7 Jun 2026 12:55:03 +0200 Subject: [PATCH] first draft for testing --- .github/workflows/build-dep.yml | 45 --- .github/workflows/release.yml | 83 ++++++ .github/workflows/version-bump.yml | 117 ++++++++ debian/changelog | 5 + debian/compat | 1 + debian/control | 21 ++ debian/pve-mod.conffiles | 1 + debian/pve-mod.postinst | 36 +++ debian/pve-mod.postrm | 18 ++ debian/pve-mod.prerm | 18 ++ debian/rules | 46 +++ debian/source/format | 1 + install.sh | 61 ++++ src/PVENodeInfo/PVEMod_Config.pm | 67 ++++- src/PVENodeInfo/apply-patches.sh | 260 +++++++++++++++++ src/PVENodeInfo/revert-patches.sh | 69 +++++ src/Scripts/pve-mod-configure | 438 +++++++++++++++++++++++++++++ src/pve-mod-apt-hook.sh | 8 + src/pve-mod.conf | 38 +++ 19 files changed, 1283 insertions(+), 50 deletions(-) delete mode 100644 .github/workflows/build-dep.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/version-bump.yml create mode 100644 debian/changelog create mode 100644 debian/compat create mode 100644 debian/control create mode 100644 debian/pve-mod.conffiles create mode 100644 debian/pve-mod.postinst create mode 100644 debian/pve-mod.postrm create mode 100644 debian/pve-mod.prerm create mode 100644 debian/rules create mode 100644 debian/source/format create mode 100644 install.sh create mode 100644 src/PVENodeInfo/apply-patches.sh create mode 100644 src/PVENodeInfo/revert-patches.sh create mode 100644 src/Scripts/pve-mod-configure create mode 100644 src/pve-mod-apt-hook.sh create mode 100644 src/pve-mod.conf diff --git a/.github/workflows/build-dep.yml b/.github/workflows/build-dep.yml deleted file mode 100644 index 801822e..0000000 --- a/.github/workflows/build-dep.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Build DEB - -on: - push: - tags: - - 'v*' - workflow_dispatch: - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Create package structure - run: | - mkdir -p package/DEBIAN - mkdir -p package/usr/local/bin - - cp myscript.sh package/usr/local/bin/myapp - chmod +x package/usr/local/bin/myapp - - - name: Create control file - run: | - cat > package/DEBIAN/control < - Description: Example package built in GitHub Actions - EOF - - - name: Build .deb - run: | - dpkg-deb --build package - mv package.deb myapp_${GITHUB_REF_NAME#v}_amd64.deb - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: deb-package - path: "*.deb" \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..515b1b3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,83 @@ +name: Build and Release + +# Triggered when a "Release vX.Y.Z" PR is merged into main +# (detected by a push to main that changes debian/changelog). + +on: + push: + branches: + - main + paths: + - 'debian/changelog' + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract version from changelog + id: version + run: | + VERSION=$(grep -m1 '(' debian/changelog | sed 's/.*(\(.*\)).*/\1/') + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "tag=v$VERSION" >> "$GITHUB_OUTPUT" + + - name: Install build dependencies + run: | + sudo apt-get update -qq + sudo apt-get install -y debhelper dpkg-dev + + - name: Build deb package + run: | + dpkg-buildpackage -us -uc -b + ls -lh ../pve-mod_*.deb + + - name: Collect release assets + id: assets + run: | + DEB=$(ls ../pve-mod_${{ steps.version.outputs.version }}_all.deb) + echo "deb=$DEB" >> "$GITHUB_OUTPUT" + + - name: Extract release notes from changelog + id: notes + run: | + # Pull the top changelog entry (lines between first and second "^pve-mod (") + NOTES=$(awk ' + /^pve-mod \(/ { if (found) exit; found=1; next } + found && /^ --/ { exit } + found { print } + ' debian/changelog | grep '^\s*\*' | sed 's/^\s*\* /- /') + EOF=$(dd if=/dev/urandom bs=15 count=1 2>/dev/null | base64) + echo "notes<<$EOF" >> "$GITHUB_OUTPUT" + echo "$NOTES" >> "$GITHUB_OUTPUT" + echo "$EOF" >> "$GITHUB_OUTPUT" + + - name: Create Git tag + env: + TAG: ${{ steps.version.outputs.tag }} + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git tag "$TAG" + git push origin "$TAG" + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ steps.version.outputs.tag }} + NOTES: ${{ steps.notes.outputs.notes }} + DEB: ${{ steps.assets.outputs.deb }} + run: | + gh release create "$TAG" \ + --title "pve-mod $TAG" \ + --notes "$NOTES" \ + "$DEB" \ + "src/Scripts/install.sh" diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml new file mode 100644 index 0000000..ec6e0be --- /dev/null +++ b/.github/workflows/version-bump.yml @@ -0,0 +1,117 @@ +name: Version Bump + +# Triggered by any push to src/ on non-main branches. +# Increments the patch version in debian/changelog, commits it, +# and opens (or updates) a "Release vX.Y.Z" PR to main. +# The changelog commit touches only debian/changelog (not src/), +# so it does not re-trigger this workflow. + +on: + push: + branches-ignore: + - main + paths: + - 'src/**' + +permissions: + contents: write + pull-requests: write + +jobs: + bump: + runs-on: ubuntu-latest + # Skip commits made by this workflow itself + if: github.actor != 'github-actions[bot]' + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract and bump version + id: version + run: | + CURRENT=$(grep -m1 '(' debian/changelog | sed 's/.*(\(.*\)).*/\1/') + MAJOR=$(echo "$CURRENT" | cut -d. -f1) + MINOR=$(echo "$CURRENT" | cut -d. -f2) + PATCH=$(echo "$CURRENT" | cut -d. -f3) + NEW="${MAJOR}.${MINOR}.$((PATCH + 1))" + echo "current=$CURRENT" >> "$GITHUB_OUTPUT" + echo "new=$NEW" >> "$GITHUB_OUTPUT" + + - name: Build PR body from src/ commits since last release + id: commits + run: | + LAST_TAG=$(git tag -l 'v*' | sort -V | tail -n1) + if [[ -n "$LAST_TAG" ]]; then + LOG=$(git log "${LAST_TAG}..HEAD" --oneline -- src/ | head -50) + else + LOG=$(git log --oneline -- src/ | head -50) + fi + # Escape for GitHub multiline output + EOF=$(dd if=/dev/urandom bs=15 count=1 2>/dev/null | base64) + echo "log<<$EOF" >> "$GITHUB_OUTPUT" + echo "$LOG" >> "$GITHUB_OUTPUT" + echo "$EOF" >> "$GITHUB_OUTPUT" + + - name: Prepend new changelog entry + env: + NEW_VERSION: ${{ steps.version.outputs.new }} + COMMITS: ${{ steps.commits.outputs.log }} + run: | + DATE=$(date -R) + { + echo "pve-mod ($NEW_VERSION) stable; urgency=low" + echo "" + while IFS= read -r line; do + [[ -n "$line" ]] && echo " * $line" + done <<< "$COMMITS" + [[ -z "$COMMITS" ]] && echo " * Automated version bump." + echo "" + echo " -- github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> $DATE" + echo "" + cat debian/changelog + } > debian/changelog.new + mv debian/changelog.new debian/changelog + + - name: Commit changelog + env: + NEW_VERSION: ${{ steps.version.outputs.new }} + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add debian/changelog + git commit -m "Release v${NEW_VERSION}" + git push + + - name: Create or update release PR + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NEW_VERSION: ${{ steps.version.outputs.new }} + COMMITS: ${{ steps.commits.outputs.log }} + BRANCH: ${{ github.ref_name }} + run: | + TITLE="Release v${NEW_VERSION}" + BODY="## Changes since last release + +${COMMITS:-No src/ changes detected.} + +--- +*Merge this PR to build and publish the \`pve-mod_${NEW_VERSION}_all.deb\` release.*" + + # Check if a PR for this version already exists + EXISTING=$(gh pr list \ + --search "\"$TITLE\" in:title" \ + --json number --jq '.[0].number' 2>/dev/null || true) + + if [[ -n "$EXISTING" ]]; then + gh pr edit "$EXISTING" --body "$BODY" + echo "Updated existing PR #$EXISTING" + else + gh pr create \ + --title "$TITLE" \ + --body "$BODY" \ + --base main \ + --head "$BRANCH" + fi diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..1ceaf4a --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +pve-mod (1.0.0) stable; urgency=low + + * Initial release. + + -- Meliox Sat, 07 Jun 2026 00:00:00 +0000 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..b1bd38b --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +13 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..d2d26a3 --- /dev/null +++ b/debian/control @@ -0,0 +1,21 @@ +Source: pve-mod +Section: misc +Priority: optional +Maintainer: Meliox +Build-Depends: debhelper-compat (= 13) +Standards-Version: 4.6.2 +Homepage: https://github.com/Meliox/PVE-mods + +Package: pve-mod +Architecture: all +Depends: ${misc:Depends}, perl, librrds-perl +Recommends: lm-sensors, nut-client +Suggests: igt-gpu-tools +Description: Proxmox VE UI modifications and sensor monitoring + Extends the Proxmox VE web interface with sensor readings including + CPU, GPU, NVMe/HDD/SSD temperatures, fan speeds, and RAM temperatures + via lm-sensors. Optionally adds UPS monitoring via Network UPS Tools, + system/motherboard information via dmidecode, GPU historical data graphs, + and removes the subscription nag screen. + . + After installation, run: pve-mod-configure diff --git a/debian/pve-mod.conffiles b/debian/pve-mod.conffiles new file mode 100644 index 0000000..0c4d9d3 --- /dev/null +++ b/debian/pve-mod.conffiles @@ -0,0 +1 @@ +/etc/pve-mod/pve-mod.conf diff --git a/debian/pve-mod.postinst b/debian/pve-mod.postinst new file mode 100644 index 0000000..d238a32 --- /dev/null +++ b/debian/pve-mod.postinst @@ -0,0 +1,36 @@ +#!/bin/bash +set -e + +NODES_PM="/usr/share/perl5/PVE/API2/Nodes.pm" + +case "$1" in + configure) + # Check for legacy bash-script install markers and warn the user. + if grep -qF '$res->{sensorsJSONOutput}' "$NODES_PM" 2>/dev/null || \ + grep -qF '$res->{systemInfo}' "$NODES_PM" 2>/dev/null; then + echo "" + echo "WARNING: A legacy pve-mod bash-script installation was detected in Nodes.pm." + echo "The old installation should be removed first to avoid conflicts:" + echo " bash /path/to/pve-mod-gui-sensors.sh uninstall" + echo "" + echo "Skipping patch application. Run 'pve-mod-configure' after removing the old install." + echo "" + else + # Apply patches for any enabled modules (idempotent, safe on upgrades). + /usr/lib/pve-mod/apply-patches.sh 2>&1 || true + fi + + if [ -z "$2" ]; then + echo "" + echo "pve-mod installed successfully." + echo "Run 'pve-mod-configure' to enable and configure modules." + echo "" + fi + ;; + + abort-upgrade|abort-remove|abort-deconfigure) + ;; +esac + +#DEBHELPER# +exit 0 diff --git a/debian/pve-mod.postrm b/debian/pve-mod.postrm new file mode 100644 index 0000000..c2ac981 --- /dev/null +++ b/debian/pve-mod.postrm @@ -0,0 +1,18 @@ +#!/bin/bash +set -e + +case "$1" in + remove) + # Remove apt hook if it was installed via pve-mod-configure. + rm -f /etc/apt/apt.conf.d/99-pve-mod + ;; + + purge) + rm -f /etc/apt/apt.conf.d/99-pve-mod + rm -rf /etc/pve-mod + rm -rf /var/lib/pve-mod + ;; +esac + +#DEBHELPER# +exit 0 diff --git a/debian/pve-mod.prerm b/debian/pve-mod.prerm new file mode 100644 index 0000000..d241b23 --- /dev/null +++ b/debian/pve-mod.prerm @@ -0,0 +1,18 @@ +#!/bin/bash +set -e + +case "$1" in + remove|deconfigure) + # Revert PVE file patches before files are removed. + if [ -x /usr/lib/pve-mod/revert-patches.sh ]; then + /usr/lib/pve-mod/revert-patches.sh 2>&1 || true + fi + ;; + + upgrade) + # On upgrade, leave patches in place; postinst will re-apply them. + ;; +esac + +#DEBHELPER# +exit 0 diff --git a/debian/rules b/debian/rules new file mode 100644 index 0000000..f4ed5d8 --- /dev/null +++ b/debian/rules @@ -0,0 +1,46 @@ +#!/usr/bin/make -f +%: + dh $@ + +override_dh_install: + # PVE API2 facade + install -Dm644 src/PVENodeInfo/PveMod_SensorInfo.pm \ + debian/pve-mod/usr/share/perl5/PVE/API2/PVEMod_SensorInfo.pm + # PVEMod core modules (strip PVEMod_ prefix) + install -Dm644 src/PVENodeInfo/PVEMod_Config.pm \ + debian/pve-mod/usr/share/perl5/PVE/PVEMod/Config.pm + install -Dm644 src/PVENodeInfo/PVEMod_Utils.pm \ + debian/pve-mod/usr/share/perl5/PVE/PVEMod/Utils.pm + install -Dm644 src/PVENodeInfo/PVEMod_Store.pm \ + debian/pve-mod/usr/share/perl5/PVE/PVEMod/Store.pm + install -Dm644 src/PVENodeInfo/PVEMod_ProcessManager.pm \ + debian/pve-mod/usr/share/perl5/PVE/PVEMod/ProcessManager.pm + # Collector plugins + install -Dm644 src/PVENodeInfo/PVEMod_Collectors/Intel.pm \ + debian/pve-mod/usr/share/perl5/PVE/PVEMod/Collector/Intel.pm + install -Dm644 src/PVENodeInfo/PVEMod_Collectors/Nvidia.pm \ + debian/pve-mod/usr/share/perl5/PVE/PVEMod/Collector/Nvidia.pm + install -Dm644 src/PVENodeInfo/PVEMod_Collectors/Amd.pm \ + debian/pve-mod/usr/share/perl5/PVE/PVEMod/Collector/Amd.pm + install -Dm644 src/PVENodeInfo/PVEMod_Collectors/LmSensors.pm \ + debian/pve-mod/usr/share/perl5/PVE/PVEMod/Collector/LmSensors.pm + install -Dm644 src/PVENodeInfo/PVEMod_Collectors/Ups.pm \ + debian/pve-mod/usr/share/perl5/PVE/PVEMod/Collector/Ups.pm + install -Dm644 src/PVENodeInfo/PVEMod_Collectors/systemInformation.pm \ + debian/pve-mod/usr/share/perl5/PVE/PVEMod/Collector/SystemInformation.pm + # JS module (rename to match loader reference) + install -Dm644 src/PVENodeInfo/PveMod_pvemanagerlib.js \ + debian/pve-mod/usr/share/pve-manager/js/PveMod_PveNodeStatusView.js + # Patch helpers + install -Dm755 src/PVENodeInfo/apply-patches.sh \ + debian/pve-mod/usr/lib/pve-mod/apply-patches.sh + install -Dm755 src/PVENodeInfo/revert-patches.sh \ + debian/pve-mod/usr/lib/pve-mod/revert-patches.sh + install -Dm755 src/pve-mod-apt-hook.sh \ + debian/pve-mod/usr/lib/pve-mod/apt-hook.sh + # Configure tool + install -Dm755 src/Scripts/pve-mod-configure \ + debian/pve-mod/usr/sbin/pve-mod-configure + # Default config + install -Dm644 src/pve-mod.conf \ + debian/pve-mod/etc/pve-mod/pve-mod.conf diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..89ae9db --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..4694cad --- /dev/null +++ b/install.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# install.sh — Bootstrap installer for pve-mod +# Usage: curl -sL https://github.com/Meliox/PVE-mods/releases/latest/download/install.sh | bash + +set -euo pipefail + +REPO="Meliox/PVE-mods" +API_URL="https://api.github.com/repos/${REPO}/releases/latest" + +#region helpers +info() { echo -e "\e[0;32m[pve-mod] ${1}\e[0m"; } +err() { echo -e "\e[0;31m[pve-mod] ERROR: ${1}\e[0m" >&2; exit 1; } +#endregion helpers + +# ── Prerequisite checks ─────────────────────────────────────────────────────── +[[ $EUID -eq 0 ]] || err "This installer must be run as root." + +dpkg -l proxmox-ve &>/dev/null 2>&1 || \ + err "This system does not appear to be running Proxmox VE." + +for cmd in curl dpkg; do + command -v "$cmd" &>/dev/null || err "Required command not found: $cmd" +done + +# ── Fetch latest release metadata ───────────────────────────────────────────── +info "Fetching latest release information..." +RELEASE_JSON=$(curl -sL "$API_URL") || err "Failed to contact GitHub API." + +# Extract .deb download URL (no jq dependency) +DEB_URL=$(echo "$RELEASE_JSON" \ + | grep '"browser_download_url"' \ + | grep '\.deb"' \ + | sed 's/.*"browser_download_url": "\([^"]*\)".*/\1/' \ + | head -n1) + +[[ -n "$DEB_URL" ]] || err "No .deb package found in the latest release." + +VERSION=$(echo "$RELEASE_JSON" \ + | grep '"tag_name"' \ + | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' \ + | head -n1) + +info "Installing pve-mod ${VERSION}..." + +# ── Download and install ─────────────────────────────────────────────────────── +TMP=$(mktemp /tmp/pve-mod-XXXXXX.deb) +trap 'rm -f "$TMP"' EXIT + +curl -sL -o "$TMP" "$DEB_URL" || err "Failed to download package from $DEB_URL" + +dpkg -i "$TMP" || { + info "Resolving missing dependencies..." + apt-get install -f -y + dpkg -i "$TMP" +} + +# ── Done ────────────────────────────────────────────────────────────────────── +echo "" +info "pve-mod ${VERSION} installed successfully." +info "Run 'pve-mod-configure' to enable and configure modules." +echo "" diff --git a/src/PVENodeInfo/PVEMod_Config.pm b/src/PVENodeInfo/PVEMod_Config.pm index dbc215b..7627243 100644 --- a/src/PVENodeInfo/PVEMod_Config.pm +++ b/src/PVENodeInfo/PVEMod_Config.pm @@ -30,9 +30,10 @@ our $process_type = 'main'; # 'main', 'worker', or 'collector' our %config = ( gpu => { - intel_enabled => 1, + intel_enabled => 0, amd_enabled => 0, nvidia_enabled => 0, + gpu_history => 0, }, debug => { log_enabled => 0, @@ -54,14 +55,22 @@ our %config = ( collector_timeout => 10, # stop collectors after N seconds of inactivity }, lm_sensors => { - enabled => 1, + enabled => 0, + enable_cpu => 0, + cpu_temp_target => 'Core', + enable_ram_temp => 0, + enable_hdd_temp => 0, + enable_nvme_temp => 0, + enable_fan_speed => 0, + display_zero_speed_fans => 0, + temp_unit => 'C', }, ups => { - enabled => 1, - device_name => 'ups@192.168.3.2', + enabled => 0, + device_name => 'ups@localhost', }, system_info => { - enabled => 1, + enabled => 0, type => 1, # 1 = System (dmidecode -t 1), 2 = Baseboard/Motherboard (dmidecode -t 2) }, paths => { @@ -88,4 +97,52 @@ our $startup_lock = "$pve_mod_working_dir/startup.lock"; our $RRD_SOCKET = '/var/run/rrdcached.sock'; our $RRD_BASE = '/var/lib/rrdcached/db/pve-mod-gpu'; +# ============================================================================ +# Load configuration from /etc/pve-mod/pve-mod.conf (INI format). +# Merges file values into %config, overriding compiled-in defaults. +# Safe to call multiple times; silently skips missing file or unknown keys. +# ============================================================================ + +sub _load_ini_file { + my $path = '/etc/pve-mod/pve-mod.conf'; + return unless -f $path; + + open my $fh, '<', $path or return; + my $section = ''; + + while (my $line = <$fh>) { + chomp $line; + $line =~ s/#.*//; # strip inline comments + $line =~ s/^\s+|\s+$//g; # trim whitespace + next unless length $line; + + if ($line =~ /^\[([^\]]+)\]$/) { + $section = $1; + next; + } + + if ($line =~ /^([^=]+)=(.*)$/) { + my ($key, $val) = ($1, $2); + $key =~ s/^\s+|\s+$//g; + $val =~ s/^\s+|\s+$//g; + + if ($section eq 'gpu' && exists $config{gpu}{$key}) { + $config{gpu}{$key} = $val; + } + elsif ($section eq 'lm_sensors' && exists $config{lm_sensors}{$key}) { + $config{lm_sensors}{$key} = $val; + } + elsif ($section eq 'ups' && exists $config{ups}{$key}) { + $config{ups}{$key} = $val; + } + elsif ($section eq 'system_info' && exists $config{system_info}{$key}) { + $config{system_info}{$key} = $val; + } + } + } + close $fh; +} + +_load_ini_file(); + 1; diff --git a/src/PVENodeInfo/apply-patches.sh b/src/PVENodeInfo/apply-patches.sh new file mode 100644 index 0000000..6321ed3 --- /dev/null +++ b/src/PVENodeInfo/apply-patches.sh @@ -0,0 +1,260 @@ +#!/usr/bin/env bash +# /usr/lib/pve-mod/apply-patches.sh +# +# Applies pve-mod patches to Proxmox VE system files. +# Reads /etc/pve-mod/pve-mod.conf to determine which modules are enabled. +# Idempotent: safe to call multiple times (e.g. from apt hook after PVE upgrade). + +CONF_FILE="/etc/pve-mod/pve-mod.conf" +BACKUP_DIR="/var/lib/pve-mod/backup" +NODES_PM="/usr/share/perl5/PVE/API2/Nodes.pm" +PVE_MANAGER_JS="/usr/share/pve-manager/js/pvemanagerlib.js" +PVE_MOD_JS="/usr/share/pve-manager/js/PveMod_PveNodeStatusView.js" +PROXMOXLIB_JS="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js" +PROXMOXLIB_MIN_JS="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.min.js" +GPU_RRD_DIR="/var/lib/rrdcached/db/pve-mod-gpu" + +info() { echo "[pve-mod] $*"; } +warn() { echo "[pve-mod] WARNING: $*" >&2; } + +# Read one value from the INI config file; prints $default if not found. +read_conf() { + local section="$1" key="$2" default="${3:-0}" + if [[ ! -f "$CONF_FILE" ]]; then + echo "$default" + return + fi + local val + val=$(awk -F= -v sec="[$section]" -v k="$key" ' + /^\[/ { in_sec = ($0 == sec) } + in_sec && /^[^#=]+=/ { + gsub(/^[[:space:]]+|[[:space:]]+$/, "", $1) + if ($1 == k) { + gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2) + print $2; exit + } + } + ' "$CONF_FILE") + echo "${val:-$default}" +} + +backup_file() { + local src="$1" + [[ -f "$src" ]] || return 0 + local name ts + name=$(basename "$src") + ts=$(date +%Y%m%d_%H%M%S) + mkdir -p "$BACKUP_DIR" + cp "$src" "$BACKUP_DIR/${name}.${ts}" + info "Backed up $(basename "$src") → $BACKUP_DIR/${name}.${ts}" +} + +# ── Read enabled modules ────────────────────────────────────────────────────── +NODE_INFO=$(read_conf modules node_info 0) +NAG_SCREEN=$(read_conf modules nag_screen 0) +GPU_HISTORY=$(read_conf gpu gpu_history 0) + +CHANGED=false + +# ── node-info: Nodes.pm ─────────────────────────────────────────────────────── +if [[ "$NODE_INFO" == "1" ]]; then + if ! grep -qF "use PVE::API2::PVEMod_SensorInfo" "$NODES_PM" 2>/dev/null; then + backup_file "$NODES_PM" + python3 - "$NODES_PM" <<'PYEOF' +import sys, re + +path = sys.argv[1] +content = open(path).read() + +if 'use PVE::API2::PVEMod_SensorInfo' in content: + sys.exit(0) + +m = re.search(r'^([ \t]*)my \$dinfo = df\(\'\/\', 1\);', content, re.MULTILINE) +if not m: + print("ERROR: Anchor 'my $dinfo = df' not found in Nodes.pm", file=sys.stderr) + sys.exit(1) + +indent = m.group(1) +insertion = ( + f"{indent}# Collect sensor data from PveMod_SensorInfo\n" + f"{indent}use PVE::API2::PVEMod_SensorInfo;\n" + f"{indent}$res->{{PveMod_JsonSensorInfo}} = PVE::API2::PVEMod_SensorInfo::get_sensors_info();\n" + f"{indent}$res->{{PveMod_graphicsInfo}} = PVE::API2::PVEMod_SensorInfo::get_pve_mod_version();\n" + f"{indent}$res->{{PveMod_upsInfo}} = PVE::API2::PVEMod_SensorInfo::get_ups_info();\n" + f"{indent}$res->{{pveMod_sensorInfo_systemInfo}} = PVE::API2::PVEMod_SensorInfo::get_system_information();\n" +) +content = content[:m.start()] + insertion + content[m.start():] +open(path, 'w').write(content) +PYEOF + info "Patched Nodes.pm" + CHANGED=true + fi + + # ── node-info: pvemanagerlib.js ─────────────────────────────────────────── + if ! grep -qF "PveMod_PveNodeStatusView.js" "$PVE_MANAGER_JS" 2>/dev/null; then + backup_file "$PVE_MANAGER_JS" + python3 - "$PVE_MANAGER_JS" <<'PYEOF' +import sys, re + +path = sys.argv[1] +content = open(path).read() + +if 'PveMod_PveNodeStatusView.js' in content: + sys.exit(0) + +# Comment out original StatusView definition +content = re.sub( + r"(?m)^(Ext\.define\('PVE\.node\.StatusView',.*?^}\);)", + lambda m: '\n'.join('// ' + line for line in m.group(1).split('\n')), + content, flags=re.DOTALL +) + +# Comment out original Summary definition +content = re.sub( + r"(?m)^(Ext\.define\('PVE\.node\.Summary',.*?^}\);)", + lambda m: '\n'.join('// ' + line for line in m.group(1).split('\n')), + content, flags=re.DOTALL +) + +# Insert dynamic loader before the now-commented StatusView block +loader = ( + "// Load custom PVE.node.StatusView from external module\n" + "Ext.Loader.loadScript({\n" + " url: '/pve2/js/PveMod_PveNodeStatusView.js',\n" + " onLoad: function() { },\n" + " onError: function() { console.error('Failed to load PveMod_PveNodeStatusView.js'); }\n" + "});\n" +) +content = re.sub( + r"(// Ext\.define\('PVE\.node\.StatusView',)", + loader + r'\1', + content, count=1 +) +open(path, 'w').write(content) +PYEOF + info "Patched pvemanagerlib.js" + CHANGED=true + fi +fi + +# ── node-info: GPU RRD history ──────────────────────────────────────────────── +if [[ "$NODE_INFO" == "1" && "$GPU_HISTORY" == "1" ]]; then + if ! grep -qF "gpurrddata" "$NODES_PM" 2>/dev/null; then + # Register method in the node sub-path list + sed -i "s/{ name => 'rrddata' },/{ name => 'rrddata' },\n { name => 'gpurrddata' },/" "$NODES_PM" + + # Append gpurrddata method definition after the rrddata code block + python3 - "$NODES_PM" <<'PYEOF' +import sys, re + +path = sys.argv[1] +content = open(path).read() + +if 'gpurrddata' in content: + sys.exit(0) + +method = r""" +__PACKAGE__->register_method({ + name => 'gpurrddata', + path => 'gpurrddata', + method => 'GET', + protected => 1, + proxyto => 'node', + permissions => { + check => ['perm', '/nodes/{node}', ['Sys.Audit']], + }, + description => "Read GPU RRD statistics", + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + card => { + description => "The GPU card identifier (e.g. card0, nvidia0).", + type => 'string', + pattern => '[a-zA-Z0-9]+', + }, + timeframe => { + description => "Specify the time frame you are interested in.", + type => 'string', + enum => ['hour', 'day', 'week', 'month', 'year', 'decade'], + }, + cf => { + description => "The RRD consolidation function", + type => 'string', + enum => ['AVERAGE', 'MAX'], + optional => 1, + }, + }, + }, + returns => { + type => "array", + items => { + type => "object", + properties => {}, + }, + }, + code => sub { + my ($param) = @_; + my $nodename = PVE::INotify::nodename(); + my $card = $param->{card}; + die "invalid card name\n" unless $card =~ /^[a-zA-Z0-9]+$/; + return PVE::RRD::create_rrd_data( + "pve-mod-gpu/$nodename/$card", $param->{timeframe}, $param->{cf}, + ); + }, +}); +""" + +# Insert before the final 1; at end of file +content = re.sub(r'\n1;\s*$', method + '\n1;\n', content) +open(path, 'w').write(content) +PYEOF + + mkdir -p "$GPU_RRD_DIR" + chown www-data:www-data "$GPU_RRD_DIR" 2>/dev/null || true + info "Installed gpurrddata API endpoint" + CHANGED=true + fi +fi + +# ── nag-screen: proxmoxlib.js ───────────────────────────────────────────────── +if [[ "$NAG_SCREEN" == "1" ]]; then + if ! grep -qF "// disable subscription nag screen" "$PROXMOXLIB_JS" 2>/dev/null; then + backup_file "$PROXMOXLIB_JS" + python3 - "$PROXMOXLIB_JS" <<'PYEOF' +import sys, re + +path = sys.argv[1] +content = open(path).read() + +if '// disable subscription nag screen' in content: + sys.exit(0) + +m = re.search(r'(checked_command:\s*function\s*\(orig_cmd\)\s*\{)', content) +if not m: + print("ERROR: checked_command pattern not found in proxmoxlib.js", file=sys.stderr) + sys.exit(1) + +insert = "\n\t\t\t// disable subscription nag screen\n\t\t\torig_cmd();\n\t\t\treturn;" +pos = m.end() +content = content[:pos] + insert + content[pos:] +open(path, 'w').write(content) +PYEOF + info "Patched proxmoxlib.js (nag screen)" + CHANGED=true + fi + + if [[ ! -L "$PROXMOXLIB_MIN_JS" ]]; then + backup_file "$PROXMOXLIB_MIN_JS" + mv "$PROXMOXLIB_MIN_JS" "$BACKUP_DIR/proxmoxlib.min.js.$(date +%Y%m%d_%H%M%S)" 2>/dev/null || true + ln -sf "$PROXMOXLIB_JS" "$PROXMOXLIB_MIN_JS" + info "Symlinked proxmoxlib.min.js → proxmoxlib.js" + CHANGED=true + fi +fi + +# ── restart pveproxy if anything changed ────────────────────────────────────── +if [[ "$CHANGED" == "true" ]]; then + info "Restarting pveproxy..." + systemctl restart pveproxy 2>/dev/null || true +fi diff --git a/src/PVENodeInfo/revert-patches.sh b/src/PVENodeInfo/revert-patches.sh new file mode 100644 index 0000000..d86097a --- /dev/null +++ b/src/PVENodeInfo/revert-patches.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# /usr/lib/pve-mod/revert-patches.sh +# +# Reverts all patches applied by apply-patches.sh. +# Restores PVE system files from backups in /var/lib/pve-mod/backup/. +# Called by prerm before package files are removed. + +BACKUP_DIR="/var/lib/pve-mod/backup" +NODES_PM="/usr/share/perl5/PVE/API2/Nodes.pm" +PVE_MANAGER_JS="/usr/share/pve-manager/js/pvemanagerlib.js" +PVE_MOD_JS="/usr/share/pve-manager/js/PveMod_PveNodeStatusView.js" +PROXMOXLIB_JS="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js" +PROXMOXLIB_MIN_JS="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.min.js" + +info() { echo "[pve-mod] $*"; } +warn() { echo "[pve-mod] WARNING: $*" >&2; } + +restore_latest() { + local name="$1" target="$2" + local latest + latest=$(find "$BACKUP_DIR" -name "${name}.*" -type f -printf '%T+ %p\n' 2>/dev/null \ + | sort -r | head -n1 | awk '{print $2}') + if [[ -n "$latest" ]]; then + cp "$latest" "$target" + info "Restored $(basename "$target") from backup" + else + warn "No backup found for ${name}; $target not restored" + fi +} + +CHANGED=false + +# ── node-info: Nodes.pm ─────────────────────────────────────────────────────── +if grep -qF "use PVE::API2::PVEMod_SensorInfo" "$NODES_PM" 2>/dev/null; then + restore_latest "Nodes.pm" "$NODES_PM" + CHANGED=true +fi + +# ── node-info: pvemanagerlib.js ─────────────────────────────────────────────── +if grep -qF "PveMod_PveNodeStatusView.js" "$PVE_MANAGER_JS" 2>/dev/null; then + restore_latest "pvemanagerlib.js" "$PVE_MANAGER_JS" + CHANGED=true +fi + +# ── node-info: JS module file ───────────────────────────────────────────────── +if [[ -f "$PVE_MOD_JS" ]]; then + rm -f "$PVE_MOD_JS" + info "Removed PveMod_PveNodeStatusView.js" + CHANGED=true +fi + +# ── nag-screen: proxmoxlib.min.js symlink ──────────────────────────────────── +if [[ -L "$PROXMOXLIB_MIN_JS" ]]; then + rm -f "$PROXMOXLIB_MIN_JS" + restore_latest "proxmoxlib.min.js" "$PROXMOXLIB_MIN_JS" + CHANGED=true +fi + +# ── nag-screen: proxmoxlib.js ──────────────────────────────────────────────── +if grep -qF "// disable subscription nag screen" "$PROXMOXLIB_JS" 2>/dev/null; then + restore_latest "proxmoxlib.js" "$PROXMOXLIB_JS" + CHANGED=true +fi + +# ── restart pveproxy if anything changed ───────────────────────────────────── +if [[ "$CHANGED" == "true" ]]; then + info "Restarting pveproxy..." + systemctl restart pveproxy 2>/dev/null || true +fi diff --git a/src/Scripts/pve-mod-configure b/src/Scripts/pve-mod-configure new file mode 100644 index 0000000..9ccbfbd --- /dev/null +++ b/src/Scripts/pve-mod-configure @@ -0,0 +1,438 @@ +#!/usr/bin/env bash +# +# pve-mod-configure - Interactive configuration tool for pve-mod +# Detects hardware, asks the user which features to enable, writes +# /etc/pve-mod/pve-mod.conf, and applies patches to PVE system files. + +set -euo pipefail + +CONF_FILE="/etc/pve-mod/pve-mod.conf" +APT_HOOK_FILE="/etc/apt/apt.conf.d/99-pve-mod" +APPLY_PATCHES="/usr/lib/pve-mod/apply-patches.sh" +NODES_PM="/usr/share/perl5/PVE/API2/Nodes.pm" + +KNOWN_CPU_SENSORS=("coretemp-isa-" "k10temp-pci-") + +#region helpers +msgb() { echo -e "\e[1m${1}\e[0m"; } +info() { echo -e "\e[0;32m[info] ${1}\e[0m"; } +warn() { echo -e "\e[0;33m[warning] ${1}\e[0m"; } +err() { echo -e "\e[0;31m[error] ${1}\e[0m"; exit 1; } +ask() { + local prompt="$1" response + read -r -p $'\n\e[1;36m'"${prompt}:"$'\e[0m ' response + echo "$response" +} +bool() { [[ "$1" == true ]] && echo 1 || echo 0; } +#endregion helpers + +sanitize_sensors_output() { + local input="$1" + echo "$input" | perl -0777 -pe ' + s/ERROR:.+\s(\w+):\s(.+)/"$1": 0.000,/g; + s/ERROR:.+\s(\w+)!/"$1": 0.000,/g; + s/,\s*(\})/$1/g; + s/\bNaN\b/null/g; + s/"SODIMM"\s*:\s*\{\s*"temp(\d+)_input"/"SODIMM $1": {\n "temp$1_input"/g; + s/"([^"]*Fan[^"]*)"\s*:\s*\{\s*"fan(\d+)_input"/"$1 $2": {\n "fan$2_input"/g; + ' | python3 -m json.tool 2>/dev/null || echo "$input" +} + +_check_or_install_tool() { + local cmd="$1" pkg="$2" description="$3" + if command -v "$cmd" &>/dev/null; then + info "$description is installed." + return 0 + fi + local choice + choice=$(ask "$description is not installed. Install it now? (y/N)") + case "$choice" in + [yY]) + apt-get update -qq + apt-get install -y "$pkg" + command -v "$cmd" &>/dev/null && { info "$description installed."; return 0; } || \ + { warn "$description installation failed. Section will be skipped."; return 1; } + ;; + *) + info "Skipping $description." + return 1 + ;; + esac +} + +_check_nvidia_tool() { + if command -v nvidia-smi &>/dev/null; then + info "nvidia-smi is installed." + return 0 + fi + warn "nvidia-smi not found. NVIDIA monitoring requires NVIDIA drivers (not installable via apt)." + return 1 +} + +#region node-info wizard +configure_node_info() { + # Initialize all variables to off + LM_SENSORS_ENABLED=0 + ENABLE_CPU=0; CPU_TEMP_TARGET="Core" + ENABLE_RAM_TEMP=0; ENABLE_HDD_TEMP=0; ENABLE_NVME_TEMP=0 + ENABLE_FAN_SPEED=0; DISPLAY_ZERO_SPEED_FANS=0; TEMP_UNIT="C" + ENABLE_INTEL_GPU_INFO=0; ENABLE_NVIDIA_GPU_INFO=0; ENABLE_AMD_GPU_INFO=0 + ENABLE_GPU_HISTORY=0 + ENABLE_UPS=0; UPS_DEVICE_NAME="ups@localhost" + ENABLE_SYSTEM_INFO=0; SYSTEM_INFO_TYPE=1 + + local lm_sensors_ok=false + local sensors_detected=false + + _check_or_install_tool sensors lm-sensors "lm-sensors" && lm_sensors_ok=true && LM_SENSORS_ENABLED=1 + + if [[ "$lm_sensors_ok" == true ]]; then + local sensorsOutput sanitisedSensorsOutput + sensorsOutput=$(sensors -j 2>/dev/null) + sanitisedSensorsOutput=$(sanitize_sensors_output "$sensorsOutput") + + #region CPU + msgb "\n=== Detecting CPU temperature sensors ===" + local cpuList="" cpuCount=0 + for pattern in "${KNOWN_CPU_SENSORS[@]}"; do + local found_sensors + found_sensors=$(echo "$sanitisedSensorsOutput" | grep -o "\"${pattern}[^\"]*\"" | sed 's/"//g') + if [[ -n "$found_sensors" ]]; then + while read -r sensor; do + [[ -z "$sensor" ]] && continue + cpuCount=$((cpuCount + 1)) + cpuList="${cpuList:+$cpuList,}$sensor" + ENABLE_CPU=1 + done <<< "$found_sensors" + fi + done + if [[ "$ENABLE_CPU" -eq 1 ]]; then + info "Detected CPU sensors ($cpuCount): $cpuList" + sensors_detected=true + while true; do + local choice + choice=$(ask "Display temperatures for all cores [C] or average per CPU [a]? (C/a)") + case "$choice" in + [cC]|"") CPU_TEMP_TARGET="Core"; info "Showing per-core temperatures."; break ;; + [aA]) CPU_TEMP_TARGET="Package"; info "Showing average per-CPU temperature."; break ;; + *) warn "Invalid input, choose C or a." ;; + esac + done + else + warn "No CPU temperature sensors found." + fi + #endregion CPU + + #region RAM + msgb "\n=== Detecting RAM temperature sensors ===" + local ramCount + ramCount=$(grep -c '"SODIMM[^"]*"' <<<"$sanitisedSensorsOutput" || true) + if [[ "$ramCount" -gt 0 ]]; then + info "Detected $ramCount RAM sensor(s)." + ENABLE_RAM_TEMP=1; sensors_detected=true + else + warn "No RAM temperature sensors found." + fi + #endregion RAM + + #region HDD/SSD + msgb "\n=== Detecting HDD/SSD temperature sensors ===" + local hddList + hddList=$(echo "$sanitisedSensorsOutput" | grep -o '"drivetemp-scsi[^"]*"' | sed 's/"//g' | wc -l || true) + if [[ "$hddList" -gt 0 ]]; then + info "Detected $hddList HDD/SSD sensor(s)." + ENABLE_HDD_TEMP=1; sensors_detected=true + else + warn "No HDD/SSD temperature sensors found. (Requires kernel module 'drivetemp'.)" + fi + #endregion HDD/SSD + + #region NVMe + msgb "\n=== Detecting NVMe temperature sensors ===" + local nvmeCount + nvmeCount=$(echo "$sanitisedSensorsOutput" | grep -c '"nvme[^"]*"' || true) + if [[ "$nvmeCount" -gt 0 ]]; then + info "Detected $nvmeCount NVMe sensor(s)." + ENABLE_NVME_TEMP=1; sensors_detected=true + else + warn "No NVMe temperature sensors found." + fi + #endregion NVMe + + #region Fans + msgb "\n=== Detecting fan speed sensors ===" + local fanCount + fanCount=$(grep -c 'fan[0-9]\+_input' <<<"$sanitisedSensorsOutput" || true) + if [[ "$fanCount" -gt 0 ]]; then + info "Detected $fanCount fan speed reading(s)." + ENABLE_FAN_SPEED=1; sensors_detected=true + local choice + choice=$(ask "Display fans reporting zero speed? (Y/n)") + case "$choice" in + [nN]) DISPLAY_ZERO_SPEED_FANS=0; info "Zero-speed fans will be hidden." ;; + *) DISPLAY_ZERO_SPEED_FANS=1; info "Zero-speed fans will be shown." ;; + esac + else + warn "No fan speed sensors found." + fi + #endregion Fans + + #region Temperature unit + if [[ "$sensors_detected" == true ]]; then + msgb "\n=== Temperature unit ===" + local unit + unit=$(ask "Display temperatures in Celsius [C] or Fahrenheit [f]? (C/f)") + case "$unit" in + [fF]) TEMP_UNIT="F"; info "Using Fahrenheit." ;; + *) TEMP_UNIT="C"; info "Using Celsius." ;; + esac + fi + #endregion Temperature unit + fi + + #region Intel GPU + msgb "\n=== Detecting Intel GPU ===" + local intelCards="" + if _check_or_install_tool intel_gpu_top igt-gpu-tools "Intel GPU tools (igt-gpu-tools)"; then + intelCards=$(intel_gpu_top -L 2>/dev/null | grep -E '^card[0-9]+' || true) + if [[ -n "$intelCards" ]]; then + info "Intel GPU(s) detected:" + echo "$intelCards" | while IFS= read -r line; do echo " $line"; done + ENABLE_INTEL_GPU_INFO=1 + else + warn "No Intel GPUs detected by intel_gpu_top." + fi + fi + #endregion Intel GPU + + #region NVIDIA GPU + msgb "\n=== Detecting NVIDIA GPU ===" + if _check_nvidia_tool; then + local nvidiaCards + nvidiaCards=$(nvidia-smi -L 2>/dev/null || true) + if [[ -n "$nvidiaCards" ]]; then + info "NVIDIA GPU(s) detected:" + echo "$nvidiaCards" | while IFS= read -r line; do echo " $line"; done + ENABLE_NVIDIA_GPU_INFO=1 + else + warn "No NVIDIA GPUs detected by nvidia-smi." + fi + fi + #endregion NVIDIA GPU + + #region AMD GPU (placeholder) + ENABLE_AMD_GPU_INFO=0 + #endregion AMD GPU + + #region GPU history + if [[ "$ENABLE_INTEL_GPU_INFO" -eq 1 || "$ENABLE_NVIDIA_GPU_INFO" -eq 1 ]]; then + msgb "\n=== GPU Historical Data ===" + local choice + choice=$(ask "Store historical GPU data for graphs? (y/N)") + case "$choice" in + [yY]) ENABLE_GPU_HISTORY=1; info "Historical GPU data will be stored." ;; + *) info "Historical GPU data disabled." ;; + esac + fi + #endregion GPU history + + #region UPS + msgb "\n=== UPS Information ===" + local choiceUPS + choiceUPS=$(ask "Enable UPS information? (y/N)") + case "$choiceUPS" in + [yY]) + local upsConn modelName upsOutput + upsConn=$(ask "Enter UPS connection string (e.g. upsname@hostname[:port])") + if ! command -v upsc &>/dev/null; then + err "'upsc' is not available. Install 'nut-client' first." + fi + upsOutput=$(upsc "$upsConn" 2>&1) + if echo "$upsOutput" | grep -q "device.model:"; then + modelName=$(echo "$upsOutput" | grep "device.model:" | cut -d: -f2- | xargs) + ENABLE_UPS=1 + UPS_DEVICE_NAME="$upsConn" + info "Connected to UPS: $modelName at $upsConn" + else + warn "Could not connect to UPS at '$upsConn'. UPS info will be disabled." + ENABLE_UPS=0 + fi + ;; + *) info "UPS information disabled." ;; + esac + #endregion UPS + + #region System info + msgb "\n=== System Information ===" + echo " type 1) System information (manufacturer, product, serial)" + dmidecode -t 1 2>/dev/null | awk -F': ' '/Manufacturer|Product Name|Serial Number/ {print " "$0}' || true + echo " type 2) Baseboard/Motherboard information" + dmidecode -t 2 2>/dev/null | awk -F': ' '/Manufacturer|Product Name|Serial Number/ {print " "$0}' || true + + local choiceSys + choiceSys=$(ask "Enable system information? (1/2/n)") + case "$choiceSys" in + 1|"") ENABLE_SYSTEM_INFO=1; SYSTEM_INFO_TYPE=1; info "System information (type 1) will be shown." ;; + 2) ENABLE_SYSTEM_INFO=1; SYSTEM_INFO_TYPE=2; info "Baseboard information (type 2) will be shown." ;; + [nN]) info "System information disabled." ;; + *) warn "Invalid selection. Defaulting to type 1."; ENABLE_SYSTEM_INFO=1; SYSTEM_INFO_TYPE=1 ;; + esac + #endregion System info +} +#endregion node-info wizard + +#region write config +write_config() { + mkdir -p "$(dirname "$CONF_FILE")" + cat > "$CONF_FILE" < "$APT_HOOK_FILE" <<'HOOKEOF' +DPkg::Post-Invoke { "/usr/lib/pve-mod/apt-hook.sh || true"; }; +HOOKEOF + info "Apt hook installed: $APT_HOOK_FILE" + ;; + *) + APT_HOOK_ENABLED=0 + if [[ -f "$APT_HOOK_FILE" ]]; then + rm -f "$APT_HOOK_FILE" + info "Apt hook removed." + else + info "Apt hook not enabled." + fi + ;; + esac +} +#endregion apt hook + +main() { + # ── Root check ──────────────────────────────────────────────────────────── + [[ $EUID -eq 0 ]] || err "This script must be run as root." + + # ── Prerequisite check ──────────────────────────────────────────────────── + if [[ ! -x "$APPLY_PATCHES" ]]; then + err "pve-mod is not installed (cannot find $APPLY_PATCHES).\nInstall it first: dpkg -i pve-mod_*.deb" + fi + + # ── Legacy installation check ───────────────────────────────────────────── + if grep -qF '$res->{sensorsJSONOutput}' "$NODES_PM" 2>/dev/null || \ + grep -qF '$res->{systemInfo}' "$NODES_PM" 2>/dev/null; then + err "A legacy pve-mod bash-script installation was detected in Nodes.pm.\nRemove it first:\n bash /path/to/pve-mod-gui-sensors.sh uninstall" + fi + + # ── Existing config notice ──────────────────────────────────────────────── + if [[ -f "$CONF_FILE" ]]; then + warn "Existing configuration found at $CONF_FILE" + local choice + choice=$(ask "Reconfigure? (Y/n)") + case "$choice" in + [nN]) info "Keeping existing configuration."; exit 0 ;; + esac + fi + + # ── Initialize all config variables with safe defaults ──────────────────── + MOD_NODE_INFO=0; MOD_NAG_SCREEN=0 + LM_SENSORS_ENABLED=0 + ENABLE_CPU=0; CPU_TEMP_TARGET="Core" + ENABLE_RAM_TEMP=0; ENABLE_HDD_TEMP=0; ENABLE_NVME_TEMP=0 + ENABLE_FAN_SPEED=0; DISPLAY_ZERO_SPEED_FANS=0; TEMP_UNIT="C" + ENABLE_INTEL_GPU_INFO=0; ENABLE_NVIDIA_GPU_INFO=0; ENABLE_AMD_GPU_INFO=0 + ENABLE_GPU_HISTORY=0 + ENABLE_UPS=0; UPS_DEVICE_NAME="ups@localhost" + ENABLE_SYSTEM_INFO=0; SYSTEM_INFO_TYPE=1 + APT_HOOK_ENABLED=0 + + # ── Module selection ────────────────────────────────────────────────────── + msgb "\n=== pve-mod Module Selection ===" + echo "Available modules:" + echo " [1] Node Info — sensor readings, GPU stats, UPS, system information" + echo " [2] Nag Screen — remove Proxmox subscription nag screen" + echo " [3] Both" + echo " [n] None / cancel" + local modChoice + modChoice=$(ask "Select modules to enable (1/2/3/n)") + case "$modChoice" in + 1) MOD_NODE_INFO=1 ;; + 2) MOD_NAG_SCREEN=1 ;; + 3) MOD_NODE_INFO=1; MOD_NAG_SCREEN=1 ;; + [nN]) info "No modules selected. Exiting."; exit 0 ;; + *) warn "Invalid selection. Defaulting to Node Info only."; MOD_NODE_INFO=1 ;; + esac + + # ── Per-module wizards ──────────────────────────────────────────────────── + if [[ "$MOD_NODE_INFO" -eq 1 ]]; then + msgb "\n=== Node Info Configuration ===" + configure_node_info + fi + + if [[ "$MOD_NAG_SCREEN" -eq 1 ]]; then + msgb "\n=== Nag Screen ===" + info "Subscription nag screen removal will be applied." + fi + + # ── Apt hook ────────────────────────────────────────────────────────────── + configure_apt_hook + + # ── Write config and apply ──────────────────────────────────────────────── + write_config + + msgb "\n=== Applying patches ===" + "$APPLY_PATCHES" + + msgb "\n=== Done ===" + info "pve-mod is configured and active." + info "Clear your browser cache to see the changes." +} + +main diff --git a/src/pve-mod-apt-hook.sh b/src/pve-mod-apt-hook.sh new file mode 100644 index 0000000..b47cca7 --- /dev/null +++ b/src/pve-mod-apt-hook.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# /usr/lib/pve-mod/apt-hook.sh +# +# Installed to /etc/apt/apt.conf.d/99-pve-mod by pve-mod-configure when the +# user enables the apt hook. Re-applies PVE file patches after any dpkg run +# (e.g. after a pve-manager upgrade overwrites patched files). + +/usr/lib/pve-mod/apply-patches.sh || true diff --git a/src/pve-mod.conf b/src/pve-mod.conf new file mode 100644 index 0000000..96a8c0a --- /dev/null +++ b/src/pve-mod.conf @@ -0,0 +1,38 @@ +# pve-mod configuration file +# Run 'pve-mod-configure' to set values interactively. +# All flags are 0 (disabled) by default; pve-mod-configure enables them. + +[modules] +node_info=0 +nag_screen=0 + +[gpu] +intel_enabled=0 +nvidia_enabled=0 +amd_enabled=0 +gpu_history=0 + +[lm_sensors] +enabled=0 +enable_cpu=0 +cpu_temp_target=Core +enable_ram_temp=0 +enable_hdd_temp=0 +enable_nvme_temp=0 +enable_fan_speed=0 +display_zero_speed_fans=0 +temp_unit=C + +[ups] +enabled=0 +device_name=ups@localhost + +[system_info] +enabled=0 +type=1 + +[apt_hook] +enabled=0 + +[service] +mode=embedded