From 73b9cece168cc0eb6818888de8943b1ec0d2e24a Mon Sep 17 00:00:00 2001 From: Meliox <5264368+Meliox@users.noreply.github.com> Date: Sun, 7 Jun 2026 17:21:57 +0200 Subject: [PATCH] Make deb-installer for PVEMods (#168) * first deb-installer workflow for PVE-mods --------- Co-authored-by: Meliox Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .github/workflows/pr-create.yml | 84 +++++ .github/workflows/release.yml | 85 +++++ .github/workflows/test-release.yml | 52 +++ .github/workflows/version-bump.yml | 84 +++++ debian/changelog | 70 ++++ debian/control | 21 ++ debian/pve-mod.conffiles | 1 + debian/pve-mod.postinst | 86 +++++ debian/pve-mod.postrm | 18 + debian/pve-mod.prerm | 33 ++ debian/pve-mod.triggers | 1 + debian/rules | 47 +++ debian/source/format | 1 + install.sh | 61 +++ src/PVENodeInfo/PVEMod_Config.pm | 92 ++++- src/PVENodeInfo/apply-patches.sh | 260 +++++++++++++ src/PVENodeInfo/revert-patches.sh | 69 ++++ src/Scripts/pve-mod-configure | 584 +++++++++++++++++++++++++++++ src/pve-mod.conf | 55 +++ src/test.yml | 1 + 20 files changed, 1689 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/pr-create.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/test-release.yml create mode 100644 .github/workflows/version-bump.yml create mode 100644 debian/changelog 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/pve-mod.triggers 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.conf create mode 100644 src/test.yml diff --git a/.github/workflows/pr-create.yml b/.github/workflows/pr-create.yml new file mode 100644 index 0000000..de53944 --- /dev/null +++ b/.github/workflows/pr-create.yml @@ -0,0 +1,84 @@ +name: Open / Update Version Bump PR + +# Triggered by any push to main where src/ files changed. +# This happens when a feature PR is merged to main. +# Creates a dedicated chore/version-bump branch and opens a PR from it. +# When creating a new PR it assigns the default "bump:patch" label. +# When updating an existing PR the label is NOT changed, preserving any +# manual label change (e.g. bump:minor, bump:major) made by a reviewer. +# The bot guard prevents this from firing on changelog commits. + +on: + push: + branches: + - main + paths: + - 'src/**' + +permissions: + contents: write + pull-requests: write + +jobs: + pr: + runs-on: ubuntu-latest + if: github.actor != 'github-actions[bot]' + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Collect src/ commits from merge + id: commits + run: | + LOG=$(git log "HEAD~1..HEAD" --oneline -- src/ | head -50) + EOF=$(openssl rand -hex 16) + echo "log<<$EOF" >> "$GITHUB_OUTPUT" + echo "$LOG" >> "$GITHUB_OUTPUT" + echo "$EOF" >> "$GITHUB_OUTPUT" + + - name: Create or reset version bump branch + run: | + git checkout -B chore/version-bump + git push origin chore/version-bump --force + + - name: Create or update version bump PR + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COMMITS: ${{ steps.commits.outputs.log }} + run: | + BUMP_BRANCH="chore/version-bump" + VERSION=$(grep -m1 '(' debian/changelog | sed 's/.*(\(.*\)).*/\1/') + TITLE="chore: bump pve-mod to v${VERSION}" + + { + echo "## Changes merged to main" + echo "" + echo "${COMMITS:-No src/ changes detected.}" + echo "" + echo "---" + echo "*Apply a bump:minor or bump:major label before merging if needed.*" + echo "*Default label is bump:patch.*" + } > /tmp/pr-body.md + + EXISTING=$(gh pr list \ + --head "$BUMP_BRANCH" \ + --base main \ + --json number --jq '.[0].number' 2>/dev/null || true) + + if [[ -n "$EXISTING" ]]; then + gh pr edit "$EXISTING" \ + --title "$TITLE" \ + --body-file /tmp/pr-body.md + echo "Updated PR #${EXISTING} body and title (label unchanged)" + else + gh pr create \ + --title "$TITLE" \ + --body-file /tmp/pr-body.md \ + --base main \ + --head "$BUMP_BRANCH" \ + --label "bump:patch" + echo "Opened new version bump PR with label bump:patch" + fi \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..09176b5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,85 @@ +name: Build and Release + +# Triggered when debian/changelog changes on main — meaning version-bump.yml +# just committed a new version. Builds the .deb and publishes a GitHub Release. +# The actor guard ensures this only fires for the bot's commit, not manual edits. + +on: + push: + branches: + - main + paths: + - 'debian/changelog' + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + if: github.actor == 'github-actions[bot]' + + 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-mods_${{ steps.version.outputs.version }}.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" \ + "install.sh" diff --git a/.github/workflows/test-release.yml b/.github/workflows/test-release.yml new file mode 100644 index 0000000..407b044 --- /dev/null +++ b/.github/workflows/test-release.yml @@ -0,0 +1,52 @@ +name: Test Release Build + +# Runs on every push, manual dispatch, or when the 'make-test-build' label +# is assigned to a pull request. + +on: + push: + workflow_dispatch: + pull_request: + types: [labeled] + +jobs: + test-release-build: + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' || github.event.label.name == 'make-test-build' + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + + - name: Install build deps + run: sudo apt-get install -y devscripts debhelper build-essential + + - name: Build package + run: dpkg-buildpackage -us -uc -b + + - name: Verify package contents + run: | + echo "=== Built files ===" + ls -la ../*.deb + echo "" + echo "=== Package info ===" + dpkg-deb --info ../*.deb + echo "" + echo "=== Package contents ===" + dpkg-deb --contents ../*.deb + + - name: Collect deb artifacts + id: collect + run: | + mkdir -p artifacts + cp ../*.deb artifacts/ + SHORT_SHA=$(git rev-parse --short HEAD) + echo "short_sha=$SHORT_SHA" >> "$GITHUB_OUTPUT" + + - name: Upload deb as artifact + uses: actions/upload-artifact@v4 + with: + name: pve-mods-test-build-${{ steps.collect.outputs.short_sha }} + path: artifacts/*.deb + retention-days: 3 \ No newline at end of file diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml new file mode 100644 index 0000000..e97fff5 --- /dev/null +++ b/.github/workflows/version-bump.yml @@ -0,0 +1,84 @@ +name: Version Bump + +# Triggered when src/ changes land on main (i.e. a PR just merged). +# Increments the patch version in debian/changelog and commits it. +# The changelog commit only touches debian/changelog (not src/), +# so it does not re-trigger this workflow. +# release.yml picks up the changelog change and builds/publishes the release. + +on: + push: + branches: + - main + paths: + - 'src/**' + +permissions: + contents: 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: Collect src/ commits since last tag + 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 + 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 and push 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 diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..5bde0a1 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,70 @@ +pve-mod (0.1.5) stable; urgency=low + + * cfeccc2 align with main + * 463dd66 fix startup_message + * a6b2560 .. + * a7d8c3b add debug to pve-mod-configure + * 342a5b5 add debug mode to pve-mod-configure and config + * 54dd209 test again + * a7cef62 rework hook implementation to work on dpkg triggered + * d9ff050 first draft for testing + * 73567f1 Restructure git repo for installer + * bfb078a PVE Temp mod version 2.0 (#152) + + -- github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Sun, 07 Jun 2026 14:12:32 +0000 + +pve-mod (0.1.4) stable; urgency=low + + * 463dd66 fix startup_message + * a6b2560 .. + * a7d8c3b add debug to pve-mod-configure + * 342a5b5 add debug mode to pve-mod-configure and config + * 54dd209 test again + * a7cef62 rework hook implementation to work on dpkg triggered + * d9ff050 first draft for testing + * 73567f1 Restructure git repo for installer + * bfb078a PVE Temp mod version 2.0 (#152) + + -- github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Sun, 07 Jun 2026 14:05:47 +0000 + +pve-mod (0.1.3) stable; urgency=low + + * a6b2560 .. + * a7d8c3b add debug to pve-mod-configure + * 342a5b5 add debug mode to pve-mod-configure and config + * 54dd209 test again + * a7cef62 rework hook implementation to work on dpkg triggered + * d9ff050 first draft for testing + * 73567f1 Restructure git repo for installer + * bfb078a PVE Temp mod version 2.0 (#152) + + -- github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Sun, 07 Jun 2026 13:51:51 +0000 + +pve-mod (0.1.2) stable; urgency=low + + * a7d8c3b add debug to pve-mod-configure + * 342a5b5 add debug mode to pve-mod-configure and config + * 54dd209 test again + * a7cef62 rework hook implementation to work on dpkg triggered + * d9ff050 first draft for testing + * 73567f1 Restructure git repo for installer + * bfb078a PVE Temp mod version 2.0 (#152) + + -- github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Sun, 07 Jun 2026 13:48:24 +0000 + +pve-mod (0.1.1) stable; urgency=low + + * 342a5b5 add debug mode to pve-mod-configure and config + * 54dd209 test again + * a7cef62 rework hook implementation to work on dpkg triggered + * d9ff050 first draft for testing + * 73567f1 Restructure git repo for installer + * bfb078a PVE Temp mod version 2.0 (#152) + + -- github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Sun, 07 Jun 2026 13:04:56 +0000 + +pve-mod (0.1.0) beta; urgency=low + + * Initial release. + + -- Meliox Sat, 07 Jun 2026 00:00:00 +0000 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..7ca0c76 --- /dev/null +++ b/debian/pve-mod.postinst @@ -0,0 +1,86 @@ +#!/bin/bash +set -e + +NODES_PM="/usr/share/perl5/PVE/API2/Nodes.pm" +DEFAULT_CONF="/usr/share/pve-mod/pve-mod.conf.default" +USER_CONF="/etc/pve-mod/pve-mod.conf" + +# Extracts "section.key" pairs from an INI file, one per line. +_extract_conf_keys() { + local file="$1" section="" line key + while IFS= read -r line; do + case "$line" in + '#'*|'') continue ;; + '['*']') section="${line#[}"; section="${section%]}"; continue ;; + *'='*) key="${line%%=*}"; echo "${section}.${key}" ;; + esac + done < "$file" +} + +# Warns about keys present in the default config but missing from the user's config. +_check_new_config_keys() { + [ -f "$DEFAULT_CONF" ] || return 0 + [ -f "$USER_CONF" ] || return 0 + + local new_keys="" section="" key default_val + while IFS= read -r line; do + case "$line" in + '#'*|'') continue ;; + '['*']') section="${line#[}"; section="${section%]}"; continue ;; + *'='*) + key="${line%%=*}" + if ! _extract_conf_keys "$USER_CONF" | grep -qF "${section}.${key}"; then + new_keys="${new_keys} [${section}] ${line}\n" + fi + ;; + esac + done < "$DEFAULT_CONF" + + if [ -n "$new_keys" ]; then + echo "" + echo "pve-mod: New configuration options are available since your last install:" + printf "%b" "$new_keys" + echo "pve-mod: Run 'pve-mod-configure' to configure them, or add them manually to $USER_CONF" + echo "" + fi +} + +case "$1" in + configure) + # Apply patches for any enabled modules. Fails loudly on error. + /usr/lib/pve-mod/apply-patches.sh + + if [ -z "$2" ]; then + echo "" + echo "pve-mod installed successfully." + echo "Run 'pve-mod-configure' to enable and configure modules." + echo "" + else + # Upgrading from a previous version — warn about any new config keys. + _check_new_config_keys + fi + ;; + + triggered) + # Fired by dpkg when pve-manager is upgraded. Only re-apply patches + # if the user has opted in via 'pve-mod-configure'. + TRIGGER_ENABLED=$(awk -F= '/^\[pve_trigger\]/{s=1} s && /^enabled=/{print $2; exit}' \ + /etc/pve-mod/pve-mod.conf 2>/dev/null || echo 0) + if [ "${TRIGGER_ENABLED}" = "1" ]; then + echo "pve-mod: pve-manager upgrade detected — re-applying patches." + if ! /usr/lib/pve-mod/apply-patches.sh; then + echo "pve-mod: WARNING: Failed to re-apply patches. Run 'pve-mod-configure' to fix." >&2 + fi + else + echo "pve-mod: WARNING: pve-manager was upgraded but auto re-patching is disabled." >&2 + echo "pve-mod: The Proxmox UI modifications may no longer be active." >&2 + echo "pve-mod: Run 'pve-mod-configure' to re-apply patches." >&2 + 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..ec0b213 --- /dev/null +++ b/debian/pve-mod.prerm @@ -0,0 +1,33 @@ +#!/bin/bash +set -e + +_revert_patches() { + if [ -x /usr/lib/pve-mod/revert-patches.sh ]; then + /usr/lib/pve-mod/revert-patches.sh 2>&1 || true + fi +} + +case "$1" in + remove|deconfigure) + # 1. Revert patches before files are removed. + _revert_patches + + # 2. Ask whether to keep the config (only in interactive sessions). + if [ -t 0 ] && [ -f /etc/pve-mod/pve-mod.conf ]; then + printf '\npve-mod: Keep /etc/pve-mod/pve-mod.conf for re-use after reinstall? [Y/n] ' + read -r _keep + case "$_keep" in + [nN]*) rm -f /etc/pve-mod/pve-mod.conf + echo "pve-mod: Configuration removed." ;; + *) echo "pve-mod: Configuration kept at /etc/pve-mod/pve-mod.conf" ;; + esac + fi + ;; + + upgrade) + # On upgrade, leave patches in place; postinst will re-apply them. + ;; +esac + +#DEBHELPER# +exit 0 diff --git a/debian/pve-mod.triggers b/debian/pve-mod.triggers new file mode 100644 index 0000000..036d144 --- /dev/null +++ b/debian/pve-mod.triggers @@ -0,0 +1 @@ +interest pve-manager diff --git a/debian/rules b/debian/rules new file mode 100644 index 0000000..f816056 --- /dev/null +++ b/debian/rules @@ -0,0 +1,47 @@ +#!/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 + # Configure tool + install -Dm755 src/Scripts/pve-mod-configure \ + debian/pve-mod/usr/sbin/pve-mod-configure + # Default config (conffile for user edits) + install -Dm644 src/pve-mod.conf \ + debian/pve-mod/etc/pve-mod/pve-mod.conf + # Reference copy for upgrade key-diff (not a conffile — always updated) + install -Dm644 src/pve-mod.conf \ + debian/pve-mod/usr/share/pve-mod/pve-mod.conf.default 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..ad17b22 100644 --- a/src/PVENodeInfo/PVEMod_Config.pm +++ b/src/PVENodeInfo/PVEMod_Config.pm @@ -30,38 +30,47 @@ 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, - log_file => '/tmp/pve-mod-debug.log', - nvidia_mode => 1, - nvidia_devices_file => '/tmp/nvidia-smi-devices.csv', - nvidia_output_file => '/tmp/nvidia-smi-output.csv', - intel_mode => 0, - intel_devices_file => '/tmp/intel-gpu-devices.json', - amd_mode => 0, - amd_devices_file => '/tmp/amd-gpu-devices.json', - ups_mode => 0, - ups_output_file => '/tmp/ups-output.json', + log_enabled => 0, + log_file => '/tmp/pve-mod-debug.log', lm_sensors_mode => 0, lm_sensors_output_file => '/tmp/sensors-output.json', + intel_mode => 0, + intel_devices_file => '/tmp/intel-gpu-devices.json', + nvidia_mode => 0, + nvidia_devices_file => '/tmp/nvidia-smi-devices.csv', + nvidia_output_file => '/tmp/nvidia-smi-output.csv', + amd_mode => 0, + amd_devices_file => '/tmp/amd-gpu-devices.json', + ups_mode => 0, + ups_output_file => '/tmp/ups-output.json', }, intervals => { data_pull => 1, # seconds between data pulls 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,55 @@ 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; + } + elsif ($section eq 'debug' && exists $config{debug}{$key}) { + $config{debug}{$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..9911fb5 --- /dev/null +++ b/src/Scripts/pve-mod-configure @@ -0,0 +1,584 @@ +#!/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" +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; } + +_load_conf_debug() { + [[ -f "$CONF_FILE" ]] || return 0 + local in_debug=0 line key val + while IFS= read -r line; do + case "$line" in + '[debug]') in_debug=1; continue ;; + '['*']') in_debug=0; continue ;; + '#'*|'') continue ;; + esac + [[ "$in_debug" -eq 0 ]] && continue + key="${line%%=*}"; val="${line#*=}" + case "$key" in + lm_sensors_mode) DEBUG_LM_SENSORS="$val" ;; + lm_sensors_output_file) DEBUG_LM_SENSORS_FILE="$val" ;; + intel_mode) DEBUG_INTEL="$val" ;; + intel_devices_file) DEBUG_INTEL_FILE="$val" ;; + nvidia_mode) DEBUG_NVIDIA="$val" ;; + nvidia_devices_file) DEBUG_NVIDIA_DEVICES_FILE="$val" ;; + nvidia_output_file) DEBUG_NVIDIA_OUTPUT_FILE="$val" ;; + amd_mode) DEBUG_AMD="$val" ;; + amd_devices_file) DEBUG_AMD_FILE="$val" ;; + ups_mode) DEBUG_UPS="$val" ;; + ups_output_file) DEBUG_UPS_FILE="$val" ;; + log_enabled) DEBUG_LOG="$val" ;; + log_file) DEBUG_LOG_FILE="$val" ;; + esac + done < "$CONF_FILE" +} +#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 + + if [[ "$DEBUG_LM_SENSORS" -eq 1 && -f "$DEBUG_LM_SENSORS_FILE" ]]; then + info "[debug] Using sensor data from $DEBUG_LM_SENSORS_FILE" + lm_sensors_ok=true; LM_SENSORS_ENABLED=1 + else + _check_or_install_tool sensors lm-sensors "lm-sensors" && lm_sensors_ok=true && LM_SENSORS_ENABLED=1 + fi + + if [[ "$lm_sensors_ok" == true ]]; then + local sensorsOutput sanitisedSensorsOutput + if [[ "$DEBUG_LM_SENSORS" -eq 1 ]]; then + sensorsOutput=$(cat "$DEBUG_LM_SENSORS_FILE") + else + sensorsOutput=$(sensors -j 2>/dev/null) + fi + 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_cpus + found_cpus=$(echo "$sanitisedSensorsOutput" | grep -o "\"${pattern}[^\"]*\"" || true | sed 's/"//g') + if [[ -n "$found_cpus" ]]; then + while read -r sensor; do + [[ -z "$sensor" ]] && continue + cpuCount=$((cpuCount + 1)) + cpuList="${cpuList:+$cpuList,}$sensor" + ENABLE_CPU=1 + done <<< "$found_cpus" + 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 [[ "$DEBUG_INTEL" -eq 1 && -f "$DEBUG_INTEL_FILE" ]]; then + info "[debug] Using Intel GPU data from $DEBUG_INTEL_FILE" + intelCards=$(cat "$DEBUG_INTEL_FILE") + if [[ -n "$intelCards" ]]; then + info "Intel GPU(s) detected (debug):" + echo "$intelCards" | while IFS= read -r line; do echo " $line"; done + ENABLE_INTEL_GPU_INFO=1 + else + warn "No Intel GPUs in debug file." + fi + elif _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 [[ "$DEBUG_NVIDIA" -eq 1 && -f "$DEBUG_NVIDIA_DEVICES_FILE" ]]; then + info "[debug] Using NVIDIA GPU data from $DEBUG_NVIDIA_DEVICES_FILE" + local nvidiaCards + nvidiaCards=$(cat "$DEBUG_NVIDIA_DEVICES_FILE") + if [[ -n "$nvidiaCards" ]]; then + info "NVIDIA GPU(s) detected (debug):" + echo "$nvidiaCards" | while IFS= read -r line; do echo " $line"; done + ENABLE_NVIDIA_GPU_INFO=1 + else + warn "No NVIDIA GPUs in debug file." + fi + elif _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 [[ "$DEBUG_UPS" -eq 1 && -f "$DEBUG_UPS_FILE" ]]; then + info "[debug] Using UPS data from $DEBUG_UPS_FILE" + upsOutput=$(cat "$DEBUG_UPS_FILE") + else + if ! command -v upsc &>/dev/null; then + err "'upsc' is not available. Install 'nut-client' first." + fi + upsOutput=$(upsc "$upsConn" 2>&1) + fi + 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" </dev/null || echo "unknown") + echo -e "\e[1;34m" + echo " ██████╗ ██╗ ██╗███████╗ ███╗ ███╗ ██████╗ ██████╗ ███████╗" + echo " ██╔══██╗██║ ██║██╔════╝ ████╗ ████║██╔═══██╗██╔══██╗██╔════╝" + echo " ██████╔╝██║ ██║█████╗ ██╔████╔██║██║ ██║██║ ██║███████╗" + echo " ██╔═══╝ ╚██╗ ██╔╝██╔══╝ ██║╚██╔╝██║██║ ██║██║ ██║╚════██║" + echo " ██║ ╚████╔╝ ███████╗ ██║ ╚═╝ ██║╚██████╔╝██████╔╝███████║" + echo " ╚═╝ ╚═══╝ ╚══════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝" + echo -e "\e[0m" + echo -e "\e[1m PVE-mods Configurator \e[0;36mv${_version}\e[0m" + echo -e " Proxmox VE enhancement suite — hardware monitoring, GPU info & more\n" + + # ── 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 + local choice + choice=$(ask "Existing configuration found at $CONF_FILE — 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 + PVE_TRIGGER_ENABLED=0 + DEBUG_LM_SENSORS=0; DEBUG_LM_SENSORS_FILE="/tmp/sensors-output.json" + DEBUG_INTEL=0; DEBUG_INTEL_FILE="/tmp/intel-gpu-devices.json" + DEBUG_NVIDIA=0; DEBUG_NVIDIA_OUTPUT_FILE="/tmp/nvidia-smi-output.csv" + DEBUG_NVIDIA_DEVICES_FILE="/tmp/nvidia-smi-devices.csv" + DEBUG_AMD=0; DEBUG_AMD_FILE="/tmp/amd-gpu-devices.json" + DEBUG_UPS=0; DEBUG_UPS_FILE="/tmp/ups-output.json" + DEBUG_LOG=0; DEBUG_LOG_FILE="/tmp/pve-mod-debug.log" + _load_conf_debug + + # ── 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 + + # ── Debug mode ──────────────────────────────────────────────────────────── + configure_debug + + # ── Re-patch on PVE upgrade ─────────────────────────────────────────────── + msgb "\n=== Auto Re-patching on PVE Upgrade ===" + echo "pve-mod registers a dpkg trigger on pve-manager. When pve-manager is" + echo "upgraded, the trigger fires and can automatically re-apply patches." + local triggerChoice + triggerChoice=$(ask "Re-apply patches automatically when pve-manager upgrades? (y/N)") + case "$triggerChoice" in + [yY]) PVE_TRIGGER_ENABLED=1; info "Auto re-patching enabled." ;; + *) PVE_TRIGGER_ENABLED=0; info "Auto re-patching disabled. Run 'pve-mod-configure' after a PVE upgrade." ;; + esac + + # ── 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.conf b/src/pve-mod.conf new file mode 100644 index 0000000..40c3022 --- /dev/null +++ b/src/pve-mod.conf @@ -0,0 +1,55 @@ +# 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 + +[pve_trigger] +enabled=0 + +[service] +mode=embedded + +# Debug mode: when a mode is 1, the real tool is not required. +# Data is read from the file path instead. Useful for development/testing. +[debug] +lm_sensors_mode=0 +lm_sensors_output_file=/tmp/sensors-output.json +intel_mode=0 +intel_devices_file=/tmp/intel-gpu-devices.json +nvidia_mode=0 +nvidia_devices_file=/tmp/nvidia-smi-devices.csv +nvidia_output_file=/tmp/nvidia-smi-output.csv +amd_mode=0 +amd_devices_file=/tmp/amd-gpu-devices.json +ups_mode=0 +ups_output_file=/tmp/ups-output.json +log_enabled=0 +log_file=/tmp/pve-mod-debug.log diff --git a/src/test.yml b/src/test.yml new file mode 100644 index 0000000..d5f407e --- /dev/null +++ b/src/test.yml @@ -0,0 +1 @@ +trigger workflow test 1 \ No newline at end of file