diff --git a/.markdownlint.yaml b/.markdownlint.yaml index 65f2e28..7632e6c 100644 --- a/.markdownlint.yaml +++ b/.markdownlint.yaml @@ -1,11 +1,16 @@ # markdownlint configuration # See: https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md +# Repeated section names per release (Keep a Changelog) +MD024: + siblings_only: true + # Line length MD013: line_length: 80 heading_line_length: 80 code_blocks: false + tables: false # Inline HTML allowed MD033: false diff --git a/CHANGELOG.md b/CHANGELOG.md index ae9910a..1c78c80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,137 +11,188 @@ Versions follow [Semantic Versioning](https://semver.org/). ## [2.13.0] - 2026-09-19 ### Added -- `--health` (boxed), `--health --list` and `--health --json`: CPU, memory and disk usage, uptime - and a health level per local VM/CT, read from `pvesh get /cluster/resources`; templates and guests - on other cluster nodes are skipped. `--filter` and `--name` apply. -- Main menu key `h` opens the health overview; the status action prints an extra `Health:` line. -- `--check` for cron: alerts only on changes (new, escalated, improved, resolved) for resource - thresholds (CPU after `HEALTH_CPU_RUNS` consecutive runs; resolved when the guest stops), guests - stopped with `onboot=1`, guests whose latest lifecycle task does not explain a stop (stop, shutdown, - suspend, migrate, destroy, backup), and failed node tasks (read with `--source all`). - Nagios-style summary and exit codes 0 OK, 1 WARN, 2 CRIT, 3 UNKNOWN (also for usage errors). -- `--dry-run` (with `--check`) prints the result and the composed message without sending or - writing state; `--test-notify` sends a test message. -- Notifications via ntfy (`NTFY_URL`, optional `NTFY_TOKEN_FILE`) and e-mail through the local - `sendmail` (`HEALTH_MAIL_TO`, `HEALTH_MAIL_FROM`); one combined message per run. -- New config keys `HEALTH_{CPU,MEM,DISK}_{WARN,CRIT}` (`0` disables a level), `HEALTH_CPU_RUNS`, - `HEALTH_IGNORE_IDS`, `HEALTH_STATE_DIR` (default `/var/lib/pman`) and the notification keys above. -- CI runs the test suite (`tests/run.sh`); shell completions gained `--name`, `--health`, - `--check`, `--dry-run` and `--test-notify`. + +- `--health` (boxed), `--health --list` and `--health --json`: CPU, memory and + disk usage, uptime and a health level per local VM/CT, read from `pvesh get + /cluster/resources`; templates and guests on other cluster nodes are skipped. + `--filter` and `--name` apply. +- Main menu key `h` opens the health overview; the status action prints an extra + `Health:` line. +- `--check` for cron: alerts only on changes (new, escalated, improved, + resolved) for resource thresholds (CPU after `HEALTH_CPU_RUNS` consecutive + runs; resolved when the guest stops), guests stopped with `onboot=1`, guests + whose latest lifecycle task does not explain a stop (stop, shutdown, suspend, + migrate, destroy, backup), and failed node tasks (read with `--source all`). + Nagios-style summary and exit codes 0 OK, 1 WARN, 2 CRIT, 3 UNKNOWN (also for + usage errors). +- `--dry-run` (with `--check`) prints the result and the composed message + without sending or writing state; `--test-notify` sends a test message. +- Notifications via ntfy (`NTFY_URL`, optional `NTFY_TOKEN_FILE`) and e-mail + through the local `sendmail` (`HEALTH_MAIL_TO`, `HEALTH_MAIL_FROM`); one + combined message per run. +- New config keys `HEALTH_{CPU,MEM,DISK}_{WARN,CRIT}` (`0` disables a level), + `HEALTH_CPU_RUNS`, `HEALTH_IGNORE_IDS`, `HEALTH_STATE_DIR` (default + `/var/lib/pman`) and the notification keys above. +- CI runs the test suite (`tests/run.sh`); shell completions gained `--name`, + `--health`, `--check`, `--dry-run` and `--test-notify`. ### Changed -- `STOP_TIMEOUT` and `PROXMOX_MANAGER_SPICE_ADDR` are validated after the command line is parsed. -- With `--json` the `--force` warning goes to stderr, so the JSON output stays valid. -- Outbound traffic is now possible, but only when notifications are configured (opt-in). + +- `STOP_TIMEOUT` and `PROXMOX_MANAGER_SPICE_ADDR` are validated after the + command line is parsed. +- With `--json` the `--force` warning goes to stderr, so the JSON output stays + valid. +- Outbound traffic is now possible, but only when notifications are configured + (opt-in). ### Security -- The ntfy access token is read from a `0600` file owned by the caller in a directory nobody else can - write; the open descriptor is verified before reading (no swap between check and use). An inline - `NTFY_TOKEN` in a config file and a token with an `http://` URL are configuration errors. -- The token, URL and headers reach `curl` only via its stdin config, never argv; `curl -q` ignores - `~/.curlrc`, redirects are not followed, a token forces `https` only, and requests are not retried. + +- The ntfy access token is read from a `0600` file owned by the caller in a + directory nobody else can write; the open descriptor is verified before + reading (no swap between check and use). An inline `NTFY_TOKEN` in a config + file and a token with an `http://` URL are configuration errors. +- The token, URL and headers reach `curl` only via its stdin config, never argv; + `curl -q` ignores `~/.curlrc`, redirects are not followed, a token forces + `https` only, and requests are not retried. - Logs name only the ntfy host, never the topic path or the token. -- Mail headers are built from validated single-line values (no header injection). -- JSON output escapes all control characters; `_truncate` never cuts inside a UTF-8 character. -- `qm`/`pct` config fallbacks run with a timeout; the `--check` lock descriptor is not inherited by - child processes and the lock file is never truncated; the state directory's parent must not be - group/world-writable. -- The `--check` state lives in a `0700` directory, is written atomically with mode `0600`, is parsed - as validated data (never sourced), and parallel runs are prevented with `flock`. +- Mail headers are built from validated single-line values (no header + injection). +- JSON output escapes all control characters; `_truncate` never cuts inside a + UTF-8 character. +- `qm`/`pct` config fallbacks run with a timeout; the `--check` lock descriptor + is not inherited by child processes and the lock file is never truncated; the + state directory's parent must not be group/world-writable. +- The `--check` state lives in a `0700` directory, is written atomically with + mode `0600`, is parsed as validated data (never sourced), and parallel runs + are prevented with `flock`. ## [2.12.1] - 2026-09-19 ### Fixed -- Action and snapshot menus now draw a closed frame: every row is padded to the box width and gets a right - border; long guest names in the menu titles are truncated instead of overflowing. + +- Action and snapshot menus now draw a closed frame: every row is padded to the + box width and gets a right border; long guest names in the menu titles are + truncated instead of overflowing. ### Changed -- README screenshots regenerated from v2.12.1 on a real PVE 9.2 host; the obsolete, unreferenced - `docs/screenshots/Screenshot.png` (pre-2.8 UI) was removed. + +- README screenshots regenerated from v2.12.1 on a real PVE 9.2 host; the + obsolete, unreferenced `docs/screenshots/Screenshot.png` (pre-2.8 UI) was + removed. ## [2.12.0] - 2026-09-19 ### Security -- Configuration files are now parsed as allowlisted data instead of being executed with `source`. -- Log files must be private, owner-controlled regular files; unsafe paths and symlinks are rejected. -- The system-wide `pman` command is installed as an atomic root-owned copy instead of a symlink to a potentially user-writable checkout. -- GitHub Actions are commit-SHA pinned, remote tool downloads are checksum-verified, workflow permissions are minimized, and checkout credentials are not persisted. -- User-controlled status and message text is no longer interpreted as `printf %b` escape sequences. + +- Configuration files are now parsed as allowlisted data instead of being + executed with `source`. +- Log files must be private, owner-controlled regular files; unsafe paths and + symlinks are rejected. +- The system-wide `pman` command is installed as an atomic root-owned copy + instead of a symlink to a potentially user-writable checkout. +- GitHub Actions are commit-SHA pinned, remote tool downloads are + checksum-verified, workflow permissions are minimized, and checkout + credentials are not persisted. +- User-controlled status and message text is no longer interpreted as `printf + %b` escape sequences. ### Fixed -- Broken TUI frames (#33): header and table rows are now padded by visible width, so right borders line up; - the table gained a closing border, the NAME column grows with long guest names (capped at the terminal - width, then truncated), and header/data columns are aligned in `--list` output too. -- The header showed `PVE: (running` instead of the version; it now parses `pveversion` correctly and shows a - compact uptime (e.g. `12d 1h 25m`). -- VM IP lookup now accepts the native QEMU Guest Agent list payload from `qm agent ... network-get-interfaces` - as well as the earlier wrapped shape, preventing parser errors on hosts that return a bare JSON array. -- Snapshot name validation now matches Proxmox `pve-configid` rules before calling `pct` / `qm`; - numeric-only and single-letter names are rejected locally with a clearer message. -- SPICE `.vv` files now prefer the actual SPICE bind address/port from Proxmox instead of always - using the host LAN IP, preventing mismatched connection targets on loopback-bound SPICE setups. -- `spice_info()` no longer claims success launching `virt-viewer` from a non-graphical shell; it - falls back to the `.vv` file with a clear desktop-session hint. -- VM console guidance now tells the user to follow the escape hint printed by Proxmox instead of - hardcoding a potentially wrong key sequence. + +- Broken TUI frames (#33): header and table rows are now padded by visible + width, so right borders line up; the table gained a closing border, the NAME + column grows with long guest names (capped at the terminal width, then + truncated), and header/data columns are aligned in `--list` output too. +- The header showed `PVE: (running` instead of the version; it now parses + `pveversion` correctly and shows a compact uptime (e.g. `12d 1h 25m`). +- VM IP lookup now accepts the native QEMU Guest Agent list payload from `qm + agent ... network-get-interfaces` as well as the earlier wrapped shape, + preventing parser errors on hosts that return a bare JSON array. +- Snapshot name validation now matches Proxmox `pve-configid` rules before + calling `pct` / `qm`; numeric-only and single-letter names are rejected + locally with a clearer message. +- SPICE `.vv` files now prefer the actual SPICE bind address/port from Proxmox + instead of always using the host LAN IP, preventing mismatched connection + targets on loopback-bound SPICE setups. +- `spice_info()` no longer claims success launching `virt-viewer` from a + non-graphical shell; it falls back to the `.vv` file with a clear + desktop-session hint. +- VM console guidance now tells the user to follow the escape hint printed by + Proxmox instead of hardcoding a potentially wrong key sequence. ### Added -- New **IP info** menu item: shows current IPv4 addresses for running VMs and CTs. VMs use the - QEMU Guest Agent, CTs use `pct exec ... ip -j addr show`, with clear fallback messages when - no address is available. + +- New **IP info** menu item: shows current IPv4 addresses for running VMs and + CTs. VMs use the QEMU Guest Agent, CTs use `pct exec ... ip -j addr show`, + with clear fallback messages when no address is available. ## [2.9.0 – 2.11.1] - 2026-04-09 – 2026-05-04 -Consolidated notes for the releases between 2.8.4 and 2.11.1 (tagged `v2.9.0` and `v2026.05.04`). +Consolidated notes for the releases between 2.8.4 and 2.11.1 (tagged `v2.9.0` +and `v2026.05.04`). ### Added -- `--filter STATUS` flag: filter `--list` / `--json` output to `running`, `stopped`, or `paused` - instances; invalid values exit 1 with a clear error message. -- `--timeout SECS` flag: set a timeout (default 60 s) for stop operations; on exit code 124 - (timeout fired) automatically retries with `--overrule-shutdown 1` as a force-stop fallback. -- `--force` flag: skip all `confirm()` prompts automatically; prints a warning at startup and - logs each bypassed prompt in yellow so the action is always visible. -- `filtered_instances()` wrapper around `collect_instances()`; used by `print_table` and - `print_json` so filter logic is in one place. -- Bash completion script: `completions/pman.bash` — tab-completes all flags; suggests - `running|stopped|paused` after `--filter` and common timeout values after `--timeout`. -- Zsh completion script: `completions/pman.zsh` — `_arguments`-based with value specs for - `--filter` and `--timeout`. -- GitHub issue templates (YAML form format): `bug_report.yml`, `feature_request.yml`. -- GitHub `ISSUE_TEMPLATE/config.yml`: blank issues disabled, link to Discussions. -- GitHub `PULL_REQUEST_TEMPLATE.md` with ShellCheck / test / changelog checklist. -- GitHub `workflows/release.yml`: auto-creates a GitHub Release with `proxmox-manager.sh` - as an asset on every `v*` tag push. + +- `--filter STATUS` flag: filter `--list` / `--json` output to `running`, + `stopped`, or `paused` instances; invalid values exit 1 with a clear error + message. +- `--timeout SECS` flag: set a timeout (default 60 s) for stop operations; on + exit code 124 (timeout fired) automatically retries with `--overrule-shutdown + 1` as a force-stop fallback. +- `--force` flag: skip all `confirm()` prompts automatically; prints a warning + at startup and logs each bypassed prompt in yellow so the action is always + visible. +- `filtered_instances()` wrapper around `collect_instances()`; used by + `print_table` and `print_json` so filter logic is in one place. +- Bash completion script: `completions/pman.bash` — tab-completes all flags; + suggests `running|stopped|paused` after `--filter` and common timeout values + after `--timeout`. +- Zsh completion script: `completions/pman.zsh` — `_arguments`-based with value + specs for `--filter` and `--timeout`. +- GitHub issue templates (YAML form format): `bug_report.yml`, + `feature_request.yml`. +- GitHub `ISSUE_TEMPLATE/config.yml`: blank issues disabled, link to + Discussions. +- GitHub `PULL_REQUEST_TEMPLATE.md` with ShellCheck / test / changelog + checklist. +- GitHub `workflows/release.yml`: auto-creates a GitHub Release with + `proxmox-manager.sh` as an asset on every `v*` tag push. - GitHub `FUNDING.yml`: GitHub Sponsors link for TimInTech. ### Fixed -- `do_action restart`: replaced manual `stop → sleep 1 → start` sequence with native - `pct reboot` / `qm reboot` — atomic, no unnecessary 1-second delay, cleaner error output. + +- `do_action restart`: replaced manual `stop → sleep 1 → start` sequence with + native `pct reboot` / `qm reboot` — atomic, no unnecessary 1-second delay, + cleaner error output. ### Changed -- `type_of_id()` now checks a global `_type_cache` associative array before querying - `pct list` / `qm list`; cache is populated as a zero-cost side-effect of the existing - `main_menu` instance loop (no extra Proxmox call required). -- `collect_instances()`: replaced three `awk '{print $N}' <<<` subshells per VM/CT with a - single parameter-expansion trim + `IFS=' ' read -r` — eliminates 6 subshells per iteration. -- `print_table` "no results" message is context-aware: when `--filter` is active it shows - "No \ VMs or containers found." rather than the generic root-hint message. + +- `type_of_id()` now checks a global `_type_cache` associative array before + querying `pct list` / `qm list`; cache is populated as a zero-cost side-effect + of the existing `main_menu` instance loop (no extra Proxmox call required). +- `collect_instances()`: replaced three `awk '{print $N}' <<<` subshells per + VM/CT with a single parameter-expansion trim + `IFS=' ' read -r` — eliminates + 6 subshells per iteration. +- `print_table` "no results" message is context-aware: when `--filter` is active + it shows "No \ VMs or containers found." rather than the generic + root-hint message. --- ## [2.8.4] - 2026-03-06 ### Fixed -- `validate_snapshot_name()` now applied to rollback and delete operations (previously only - enforced on create). -- `snapshots_menu`: rollback and delete show Proxmox error details via `_rb_out` / `_del_out` - variables, consistent with create. -- `open_console`: guard added for VM terminal — checks `qm status` before calling - `qm terminal`; prints `Ctrl+]` exit hint before entering. -- `_pve_out` correctly declared `local` in stop and restart branches of `do_action()`; - fixes potential variable bleed between branches. + +- `validate_snapshot_name()` now applied to rollback and delete operations + (previously only enforced on create). +- `snapshots_menu`: rollback and delete show Proxmox error details via `_rb_out` + / `_del_out` variables, consistent with create. +- `open_console`: guard added for VM terminal — checks `qm status` before + calling `qm terminal`; prints `Ctrl+]` exit hint before entering. +- `_pve_out` correctly declared `local` in stop and restart branches of + `do_action()`; fixes potential variable bleed between branches. ### Added + - Keyboard legend in the main menu (``, `r`, `q`). --- @@ -149,32 +200,40 @@ Consolidated notes for the releases between 2.8.4 and 2.11.1 (tagged `v2.9.0` an ## [2.8.3] - 2026-03-06 ### Added + - `pman` global command: `install_dependencies.sh` now installs a symlink at `/usr/local/bin/pman`; idempotent. ### Fixed + - `_pve_out` declared `local` in all do_action() branches. - Rollback and delete snapshot errors now captured and displayed. -- VM console guard: `open_console` checks VM running status before `qm terminal`. +- VM console guard: `open_console` checks VM running status before `qm + terminal`. --- ## [2.8.2] - 2026-03-06 ### Fixed -- Proxmox error details surfaced on start / stop / restart failure via stderr capture. + +- Proxmox error details surfaced on start / stop / restart failure via stderr + capture. - Snapshot name validated before any Proxmox call (create path). -- UX polish: VMID-not-found message hints to press `r` to refresh; empty Enter in - action menu treated as "Back". +- UX polish: VMID-not-found message hints to press `r` to refresh; empty Enter + in action menu treated as "Back". --- ## [2.8.1] - 2026-03-06 ### Fixed -- `open_console`: CT status checked before `pct enter`; clear error if not running. + +- `open_console`: CT status checked before `pct enter`; clear error if not + running. ### Added + - `print_table` appends colour-coded count line: `Count: N running M stopped`. - `header()` shows system uptime via `uptime -p`. @@ -183,12 +242,16 @@ Consolidated notes for the releases between 2.8.4 and 2.11.1 (tagged `v2.9.0` an ## [2.8.0] - 2026-03-06 ### Changed -- Full modular rewrite: `collect_instances`, `print_table`, `print_json`, `do_action`, - `snapshots_menu`, `spice_info`, `spice_enable` separated into distinct functions. -- Status symbols changed to ASCII: `[+]` running, `[-]` stopped, `[~]` paused, `[?]` unknown. + +- Full modular rewrite: `collect_instances`, `print_table`, `print_json`, + `do_action`, `snapshots_menu`, `spice_info`, `spice_enable` separated into + distinct functions. +- Status symbols changed to ASCII: `[+]` running, `[-]` stopped, `[~]` paused, + `[?]` unknown. - Output normalised to English throughout. - `set -Eeuo pipefail` + `IFS=$'\n\t'` + `LC_ALL=C` hardened at the top. -- `confirm()` added before all destructive actions (stop, restart, rollback, delete). +- `confirm()` added before all destructive actions (stop, restart, rollback, + delete). - `log()` added for optional structured file logging via `$LOG_FILE`. - `validate_vmid()` added — rejects non-numeric or out-of-range VMIDs. - `header()` shows node hostname and PVE version when available. @@ -200,7 +263,8 @@ Consolidated notes for the releases between 2.8.4 and 2.11.1 (tagged `v2.9.0` an ## [1.5.0] - (pre-rewrite) -Early version with basic console and snapshot management. Replaced by the v2.8.0 rewrite. +Early version with basic console and snapshot management. Replaced by the v2.8.0 +rewrite. --- diff --git a/README.md b/README.md index 91f241d..dfd98af 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@
-``` +```text ╔══════════════════════════════════════════════════════════╗ ║ ██████╗ ███╗ ███╗ █████╗ ███╗ ██╗ ║ ║ ██╔══██╗ ████╗ ████║ ██╔══██╗ ████╗ ██║ ║ @@ -17,35 +17,50 @@ **Single-file Bash tool for managing Proxmox VMs and containers.** No daemons. No agents. No dependencies beyond what ships with Proxmox VE. +**New in v2.13.0:** 🩺 health view per guest and 🔔 ntfy / e-mail alerts from a +cron check — [set up alerts in 3 steps](#-quick-start-alerts-in-3-steps). + [![CI](https://img.shields.io/github/actions/workflow/status/TimInTech/proxmox-manager/ci.yml?branch=main&style=for-the-badge&logo=github&label=CI)](https://github.com/TimInTech/proxmox-manager/actions) [![Gitleaks](https://img.shields.io/github/actions/workflow/status/TimInTech/proxmox-manager/gitleaks.yml?branch=main&style=for-the-badge&logo=security&label=Gitleaks)](https://github.com/TimInTech/proxmox-manager/actions) [![License](https://img.shields.io/github/license/TimInTech/proxmox-manager?style=for-the-badge&color=blue)](LICENSE) [![Shell](https://img.shields.io/badge/Shell-Bash-4EAA25?style=for-the-badge&logo=gnu-bash)](https://www.gnu.org/software/bash/) -[![Proxmox VE](https://img.shields.io/badge/Proxmox-VE%207%2F8%2F9-orange?style=for-the-badge)](https://www.proxmox.com/) +[![Proxmox +VE](https://img.shields.io/badge/Proxmox-VE%207%2F8%2F9-orange?style=for-the-badge)](https://www.proxmox.com/) ![Tech Stack](https://skillicons.dev/icons?i=linux,bash,debian) - Buy Me A Coffee + Buy Me A Coffee
--- + ## 📖 Community & Tutorials Check out how others are using `proxmox-manager`: -* 🇵🇱 **[Proxmox Manager – Co to jest, jak działa, instalacja i obsługa](https://blog.askomputer.pl/proxmox-manager-timintech-co-to-jest-jak-dziala-instalacja-i-obsluga/)** *A detailed walkthrough and review by askomputer.pl featuring usage examples and screenshots (in Polish).* +- 🇵🇱 **[Proxmox Manager – Co to jest, jak działa, instalacja i + obsługa](https://blog.askomputer.pl/proxmox-manager-timintech-co-to-jest-jak-dziala-instalacja-i-obsluga/)** + *A detailed walkthrough and review by askomputer.pl featuring usage examples + and screenshots (in Polish).* ## 📸 Screenshots
-| Main Menu — VM/CT Table | Action Menu | +| Main menu — VM/CT table | Action menu — status with health line | |:---:|:---:| | ![Main menu](docs/screenshots/screenshot-tui.png) | ![Action menu](docs/screenshots/screenshot-action-menu.png) | -| Live status for all VMs & containers | Per-instance controls: start, stop, console, snapshots | +| Live status for all VMs & containers | Start, stop, console, snapshots; `Status` adds CPU / MEM / DISK | +| **Health view — `pman --health`** | **Cron check — `pman --check`** | +| ![Health view](docs/screenshots/screenshot-health.png) | ![Health check](docs/screenshots/screenshot-check.png) | +| CPU / MEM / DISK %, uptime and level per guest | WARN → RESOLVED with Nagios exit codes | +| **ntfy push — web app** | **E-mail alert** | +| ![ntfy alert](docs/screenshots/screenshot-ntfy.png) | ![E-mail alert](docs/screenshots/screenshot-mail.png) | +| Same message on phone, desktop or browser | Plain-text mail via the local `sendmail` |
@@ -55,57 +70,112 @@ Check out how others are using `proxmox-manager`: | | Feature | Details | |---|---|---| +| 🩺 | **Health Monitoring** | CPU / RAM / disk per guest, uptime and OK / WARN / CRIT — `pman --health`, key `h` in the TUI, `Health:` line in the status action | +| 🔔 | **Alerts** | `pman --check` for cron: thresholds, stopped `onboot` guests, unexpected stops and failed tasks; ntfy / e-mail only when something changes (opt-in) | | 📋 | **List & Status** | All VMs and containers with live status — `[+]` running · `[-]` stopped · `[~]` paused · `[?]` unknown | | ⚡ | **Start / Stop / Restart** | Confirmation prompt for destructive actions. Proxmox error details on failure. Configurable timeout with force-stop fallback | | 🖥️ | **Console Access** | LXC shell via `pct enter` or QEMU terminal via `qm terminal`. Verifies running state before entering | | 🌐 | **IP Address Lookup** | Shows current IPv4 addresses for running VMs and CTs — VM via QEMU Guest Agent, CT via `pct exec` | | 📦 | **Snapshot Management** | List, create, rollback, delete — with name validation and snapshot preview before destructive actions | | 🖱️ | **SPICE Integration** | Enable SPICE for VMs and retrieve `.vv` connection files. Auto-launches `virt-viewer` when installed | -| 🩺 | **Health & Alerts** | CPU / RAM / disk per guest, stopped-with-onboot, unexpected stops and failed tasks — `--health` view, `--check` for cron with ntfy / e-mail alerts (opt-in) | | 🤖 | **Automation-Ready** | `--json` output, `--filter` by status, `--name` ERE filter, `--force` mode, structured logging via `LOG_FILE` | | ⚙️ | **Config File** | Persistent allowlisted defaults via `/etc/pmanrc` or `~/.pmanrc` — CLI flags always win | --- -## 🏗️ How It Works +## 🔔 Quick start: alerts in 3 steps + +Run as root on the Proxmox node after [installing `pman`](#-installation). + +### 1 — Pick a channel in `/etc/pmanrc` + +```bash +touch /etc/pmanrc && chmod 600 /etc/pmanrc +cat >>/etc/pmanrc <<'EOF' +NTFY_URL="https://ntfy.sh/pman-alerts-CHANGE-ME" # hard-to-guess topic name +HEALTH_MAIL_TO="admin@example.com" # optional, needs sendmail +EOF +``` -A single `proxmox-manager.sh` script — no build step, no service, no config files. Runs on-demand as root directly on the Proxmox VE node. +Anyone who knows a public ntfy.sh topic can read it — treat the name like a +password, or use a [protected topic](#health-monitoring--alerts) with a token. +### 2 — Send a test message + +```bash +pman --test-notify +pman --check --dry-run; echo "exit $?" # what would be sent, nothing saved +``` + +Subscribe to the same topic in the [ntfy +app](https://ntfy.sh/docs/subscribe/phone/) or at `https://ntfy.sh/` +in the browser; the test message shows up there. + +### 3 — Let cron check every 5 minutes + +```bash +cat >/etc/cron.d/pman-health <<'EOF' +PATH=/usr/sbin:/usr/bin:/sbin:/bin +*/5 * * * * root /usr/local/bin/pman --check >/dev/null 2>&1 +EOF ``` - User / Automation - │ - ▼ - ┌─────────────────────────────────┐ - │ pman (proxmox-manager.sh) │ - │ │ - │ ┌──────────┐ ┌─────────────┐ │ - │ │ --list │ │ interactive │ │ - │ │ --json │ │ TUI │ │ - │ │ --filter │ │ │ │ - │ └────┬─────┘ └──────┬──────┘ │ - └───────┼───────────────┼─────────┘ - │ │ - ▼ ▼ - ┌───────────────────────────────┐ - │ qm (VMs) · pct (CTs) │ ← Proxmox CLI (bundled with PVE) - └───────────────────────────────┘ + +The first run only records a baseline. From then on, you get one message per +change — new or escalated problems, improvements and `RESOLVED`: + +

+ ntfy web app with WARN and RESOLVED alerts from pman +

+ +Details, thresholds and limitations: [Health monitoring & +alerts](#health-monitoring--alerts). + +--- + +## 🏗️ How It Works + +A single `proxmox-manager.sh` script — no build step, no service, config +optional. Runs on-demand as root directly on the Proxmox VE node; alerts run +from cron. + +```text + User / Automation cron (*/5) + │ │ + ▼ ▼ + ┌───────────────────────────────────────────────────────┐ + │ pman (proxmox-manager.sh) │ + │ │ + │ ┌──────────┐ ┌─────────────┐ ┌──────────────────┐ │ + │ │ --list │ │ interactive │ │ --health │ │ + │ │ --json │ │ TUI │ │ --check ─────────┼──┼──▶ ntfy / e-mail + │ │ --filter │ │ │ │ state: /var/lib │ │ (on change) + │ └────┬─────┘ └──────┬──────┘ └────────┬─────────┘ │ + └───────┼───────────────┼──────────────────┼────────────┘ + │ │ │ + ▼ ▼ ▼ + ┌───────────────────────────────────────────────────────┐ + │ qm (VMs) · pct (CTs) · pvesh (usage, tasks) │ ← Proxmox CLI + └───────────────────────────────────────────────────────┘ │ ▼ - ┌───────────────────────────────┐ - │ Proxmox VE Host (local) │ - │ QEMU Virtual Machines │ - │ LXC Containers │ - └───────────────────────────────┘ + ┌───────────────────────────────────────────────────────┐ + │ Proxmox VE Host (local) │ + │ QEMU Virtual Machines · LXC Containers │ + └───────────────────────────────────────────────────────┘ ``` -> ✅ No network calls (unless alerts are configured) · ✅ No background process · ✅ Bash ≥ 4.0 · ✅ PVE 7.x / 8.x / 9.x +> ✅ No network calls (unless alerts are configured) · ✅ No background process · +> ✅ Bash ≥ 4.0 · ✅ PVE 7.x / 8.x / 9.x --- ## 🚀 Installation -**Requirements:** Proxmox VE host · Bash ≥ 4.0 · Root privileges · `qm` / `pct` / `pvesh` and -`python3` (all bundled with PVE) · optional: `sendmail` (e.g. postfix) for e-mail alerts +**Requirements:** Proxmox VE host · Bash ≥ 4.0 · Root privileges · `qm` / `pct` +/ `pvesh` and `python3` (all bundled with PVE) · optional: `sendmail` (e.g. +postfix) for e-mail alerts ### Step 1 — Clone @@ -121,13 +191,14 @@ chmod +x proxmox-manager.sh ./proxmox-manager.sh ``` -### Step 3 — Or register as `pman` system-wide _(optional, requires root)_ +### Step 3 — Or register as `pman` system-wide *(optional, requires root)* ```bash ./install_dependencies.sh ``` -Installs an atomic, root-owned copy at `/usr/local/bin/pman` so later edits to the checkout cannot change the privileged executable. +Installs an atomic, root-owned copy at `/usr/local/bin/pman` so later edits to +the checkout cannot change the privileged executable. --- @@ -153,13 +224,13 @@ Installs an atomic, root-owned copy at `/usr/local/bin/pman` so later edits to t ### Interactive mode -``` +```text pman ``` Displays a table of all VMs/containers. Enter a VMID to open the action menu. -``` +```text [+] running [-] stopped [~] paused [?] unknown ``` @@ -180,21 +251,33 @@ pman --json | jq '.[] | select(.status == "running")' ### SPICE remote desktop -The SPICE integration generates a `.vv` connection file for any VM. If [`virt-viewer`](https://virt-manager.org/) is installed, it is launched automatically: +The SPICE integration generates a `.vv` connection file for any VM. If +[`virt-viewer`](https://virt-manager.org/) is installed, it is launched +automatically: ```bash # Install virt-viewer (Debian / Proxmox host) apt install virt-viewer ``` -When `virt-viewer` is **not** installed, the path to the generated `.vv` file is printed together with the install hint. The SPICE bind address defaults to `127.0.0.1` and can be overridden via `PROXMOX_MANAGER_SPICE_ADDR` (env var or `~/.pmanrc`). +When `virt-viewer` is **not** installed, the path to the generated `.vv` file is +printed together with the install hint. The SPICE bind address defaults to +`127.0.0.1` and can be overridden via `PROXMOX_MANAGER_SPICE_ADDR` (env var or +`~/.pmanrc`). ### Health monitoring & alerts -`pman --health` shows every guest on the **local node** (templates skipped) with CPU, memory and -disk usage, uptime and a health level; `--list` prints plain text, `--json` machine-readable -data (`null` where a value is not available). The same data backs the `h` key in the TUI and -the `Health:` line of the status action. +`pman --health` shows every guest on the **local node** (templates skipped) with +CPU, memory and disk usage, uptime and a health level; `--list` prints plain +text, `--json` machine-readable data (`null` where a value is not available). +The same data backs the `h` key in the TUI and the `Health:` line of the status +action. + +

+ pman --health: CPU, memory, disk, uptime and level per guest +

`pman --check` is meant for cron. It evaluates: @@ -202,7 +285,7 @@ the `Health:` line of the status action. |---|---|---| | CPU | WARN / CRIT | Must stay above the threshold for `HEALTH_CPU_RUNS` runs in a row | | Memory, disk | WARN / CRIT | Immediately. VM disk usage needs the QEMU guest agent, otherwise n/a | -| _(guest stops)_ | — | CPU / memory / disk alerts of a stopped guest are resolved | +| *(guest stops)* | — | CPU / memory / disk alerts of a stopped guest are resolved | | Stopped with `onboot=1` | CRIT | Configured to start at boot but not running (WARN if stopped by a task) | | Stopped unexpectedly | WARN | Was running at the last check; its latest task is no stop/shutdown/suspend/migrate/destroy/backup | | Failed task | WARN / CRIT | Any node task that ended not `OK` since the last run (`WARNINGS` → WARN) | @@ -214,16 +297,25 @@ Thresholds (percent; `0` disables that level): | `HEALTH_CPU_WARN` | 85 | `HEALTH_CPU_CRIT` | 95 | | `HEALTH_MEM_WARN` | 90 | `HEALTH_MEM_CRIT` | 95 | | `HEALTH_DISK_WARN` | 85 | `HEALTH_DISK_CRIT` | 95 | -| `HEALTH_CPU_RUNS` | 3 | `HEALTH_IGNORE_IDS` | _(empty)_ e.g. `105,210` | - -A notification is sent only when something changes: new or escalated problems, improvements and -`RESOLVED` (with duration). All changes of one run go into one message. The first run only -records a baseline, so old failed tasks are not reported. State is kept in `HEALTH_STATE_DIR` -(default `/var/lib/pman`, mode `0700`); if every channel fails, the unsent changes stay pending and -are sent by the next run. Exit codes: `0` OK · `1` WARN · `2` CRIT · `3` UNKNOWN (usage, config, -lock or `pvesh` error) — the output is a Nagios-style summary line plus one line per problem. - -**ntfy** — push to phone/desktop via [ntfy](https://ntfy.sh) (public or self-hosted): +| `HEALTH_CPU_RUNS` | 3 | `HEALTH_IGNORE_IDS` | *(empty)* e.g. `105,210` | + +A notification is sent only when something changes: new or escalated problems, +improvements and `RESOLVED` (with duration). All changes of one run go into one +message. The first run only records a baseline, so old failed tasks are not +reported. State is kept in `HEALTH_STATE_DIR` (default `/var/lib/pman`, mode +`0700`); if every channel fails, the unsent changes stay pending and are sent by +the next run. Exit codes: `0` OK · `1` WARN · `2` CRIT · `3` UNKNOWN (usage, +config, lock or `pvesh` error) — the output is a Nagios-style summary line plus +one line per problem. + +

+ pman --check: three memory warnings, RESOLVED minutes later +

+ +**ntfy** — push to phone/desktop via [ntfy](https://ntfy.sh) (public or +self-hosted): ```bash # /etc/pmanrc @@ -235,9 +327,10 @@ install -d -m 700 /etc/pman install -m 600 /dev/null /etc/pman/ntfy.token && nano /etc/pman/ntfy.token ``` -The token file and its directory must not be writable by others; a token requires an `https://` -URL (`http://` plus a token is a configuration error). **E-mail** uses the local `sendmail` (postfix, which PVE -installs for its own notifications, works out of the box when it can relay): +The token file and its directory must not be writable by others; a token +requires an `https://` URL (`http://` plus a token is a configuration error). +**E-mail** uses the local `sendmail` (postfix, which PVE installs for its own +notifications, works out of the box when it can relay): ```bash # /etc/pmanrc @@ -245,22 +338,36 @@ HEALTH_MAIL_TO="admin@example.com" # comma separated, no spaces HEALTH_MAIL_FROM="pman@pve.example.com" # optional ``` -Test the channels, then add the cron job (set `PATH`, cron's default lacks `/usr/sbin`): +Subject and body match the ntfy message (plain text, `Auto-Submitted: +auto-generated`): + +

+ pman WARN alert e-mail in a web mail client +

+ +If postfix cannot deliver directly, point it at a smarthost (`relayhost` in +`/etc/postfix/main.cf`); `mailq` lists mail that is stuck in the queue. + +Test the channels, then add the cron job (set `PATH`, cron's default lacks +`/usr/sbin`): ```bash pman --test-notify pman --check --dry-run; echo "exit $?" ``` -``` +```text # /etc/cron.d/pman-health PATH=/usr/sbin:/usr/bin:/sbin:/bin */5 * * * * root /usr/local/bin/pman --check >/dev/null 2>&1 ``` -Limitations: a shutdown from **inside** a guest has no Proxmox task and is reported as -"stopped unexpectedly"; in a cluster, run the cron job on **every node** (each checks only its own -guests); failed tasks are read from the last 200 node tasks. +Limitations: a shutdown from **inside** a guest has no Proxmox task and is +reported as "stopped unexpectedly"; in a cluster, run the cron job on **every +node** (each checks only its own guests); failed tasks are read from the last +200 node tasks. ### Configuration @@ -282,13 +389,17 @@ HEALTH_MEM_WARN=80 # health thresholds, see "Health monit NTFY_URL="https://ntfy.sh/pman-alerts-CHANGE-ME" ``` -Configuration files are parsed as data, not executed as shell code. Accepted keys: -`STOP_TIMEOUT`, `LOG_FILE`, `PROXMOX_MANAGER_SPICE_ADDR`, `HEALTH_CPU_WARN`, `HEALTH_CPU_CRIT`, -`HEALTH_MEM_WARN`, `HEALTH_MEM_CRIT`, `HEALTH_DISK_WARN`, `HEALTH_DISK_CRIT`, `HEALTH_CPU_RUNS`, -`HEALTH_IGNORE_IDS`, `HEALTH_STATE_DIR`, `NTFY_URL`, `NTFY_TOKEN_FILE`, `HEALTH_MAIL_TO` and -`HEALTH_MAIL_FROM`; anything else is ignored with a warning. Health settings are validated only -by `--health`, `--check` and `--test-notify`, so a typo never blocks the TUI. Because the file -may name alert targets, keep it private: `chmod 600 /etc/pmanrc`. When enabled, `LOG_FILE` must be an absolute, non-symlinked regular file owned by the current user with mode `0600`; its parent directory must also be owner-controlled and not group/world-writable. +Configuration files are parsed as data, not executed as shell code. Accepted +keys: `STOP_TIMEOUT`, `LOG_FILE`, `PROXMOX_MANAGER_SPICE_ADDR`, +`HEALTH_CPU_WARN`, `HEALTH_CPU_CRIT`, `HEALTH_MEM_WARN`, `HEALTH_MEM_CRIT`, +`HEALTH_DISK_WARN`, `HEALTH_DISK_CRIT`, `HEALTH_CPU_RUNS`, `HEALTH_IGNORE_IDS`, +`HEALTH_STATE_DIR`, `NTFY_URL`, `NTFY_TOKEN_FILE`, `HEALTH_MAIL_TO` and +`HEALTH_MAIL_FROM`; anything else is ignored with a warning. Health settings are +validated only by `--health`, `--check` and `--test-notify`, so a typo never +blocks the TUI. Because the file may name alert targets, keep it private: `chmod +600 /etc/pmanrc`. When enabled, `LOG_FILE` must be an absolute, non-symlinked +regular file owned by the current user with mode `0600`; its parent directory +must also be owner-controlled and not group/world-writable. ### Shell Completions @@ -305,11 +416,14 @@ cp completions/pman.zsh ~/.zsh/completions/_pman ## 🔐 Security -- **Root required:** `qm` and `pct` need elevated privileges — there's no workaround. +- **Root required:** `qm` and `pct` need elevated privileges — there's no + workaround. - **No credentials stored:** Relies entirely on Proxmox host authentication. -- **No outbound traffic unless notifications are configured:** alerts via ntfy / e-mail are opt-in; - the ntfy token is read from a `0600` file, passed to `curl` via stdin and never sent over plain http. -- **CI hardening:** ShellCheck on every push · Gitleaks scan for accidental secrets. +- **No outbound traffic unless notifications are configured:** alerts via ntfy / + e-mail are opt-in; the ntfy token is read from a `0600` file, passed to `curl` + via stdin and never sent over plain http. +- **CI hardening:** ShellCheck on every push · Gitleaks scan for accidental + secrets. --- @@ -317,8 +431,8 @@ cp completions/pman.zsh ~/.zsh/completions/_pman ### 🆕 [v2.13.0](CHANGELOG.md) — 2026-09-19 -> Health view (`--health`, key `h`) · `--check` for cron with ntfy / e-mail alerts on changes · -> exit codes 0/1/2/3 · `--test-notify` +> Health view (`--health`, key `h`) · `--check` for cron with ntfy / e-mail +> alerts on changes · exit codes 0/1/2/3 · `--test-notify` ### [v2.12.1](CHANGELOG.md) — 2026-09-19 @@ -347,7 +461,8 @@ cp completions/pman.zsh ~/.zsh/completions/_pman ### [v2.9.0](CHANGELOG.md) — 2026-04-09 -> `--filter STATUS` · `--timeout SECS` with force-stop fallback · `--force` mode · 29 unit tests · Bash & Zsh shell completions +> `--filter STATUS` · `--timeout SECS` with force-stop fallback · `--force` mode +> · 29 unit tests · Bash & Zsh shell completions **→ [View full CHANGELOG](CHANGELOG.md)** @@ -363,9 +478,9 @@ shellcheck proxmox-manager.sh tests/run.sh ``` -131 tests covering `validate_vmid`, `validate_snapshot_name`, `ip_info`, `--filter`, CLI flags, config -parsing, the health view, the `--check` engine and notifications (mocked `pvesh`, `curl`, `sendmail`; -no network, no root). +131 tests covering `validate_vmid`, `validate_snapshot_name`, `ip_info`, +`--filter`, CLI flags, config parsing, the health view, the `--check` engine and +notifications (mocked `pvesh`, `curl`, `sendmail`; no network, no root). --- @@ -378,7 +493,8 @@ Contributions welcome — keep it simple, keep it Bash. 3. Commit with conventional format: `feat(vm): add suspend action` 4. Open a Pull Request. -**Do not commit:** generated files, scan outputs, binary files, or large test data. +**Do not commit:** generated files, scan outputs, binary files, or large test +data. --- diff --git a/docs/screenshots/screenshot-action-menu.png b/docs/screenshots/screenshot-action-menu.png index ff30d3b..8b14df7 100644 Binary files a/docs/screenshots/screenshot-action-menu.png and b/docs/screenshots/screenshot-action-menu.png differ diff --git a/docs/screenshots/screenshot-check.png b/docs/screenshots/screenshot-check.png new file mode 100644 index 0000000..df512f5 Binary files /dev/null and b/docs/screenshots/screenshot-check.png differ diff --git a/docs/screenshots/screenshot-health.png b/docs/screenshots/screenshot-health.png new file mode 100644 index 0000000..48ef021 Binary files /dev/null and b/docs/screenshots/screenshot-health.png differ diff --git a/docs/screenshots/screenshot-mail.png b/docs/screenshots/screenshot-mail.png new file mode 100644 index 0000000..8b5f792 Binary files /dev/null and b/docs/screenshots/screenshot-mail.png differ diff --git a/docs/screenshots/screenshot-ntfy.png b/docs/screenshots/screenshot-ntfy.png new file mode 100644 index 0000000..caceacc Binary files /dev/null and b/docs/screenshots/screenshot-ntfy.png differ diff --git a/docs/screenshots/screenshot-tui.png b/docs/screenshots/screenshot-tui.png index 62861a6..79f8745 100644 Binary files a/docs/screenshots/screenshot-tui.png and b/docs/screenshots/screenshot-tui.png differ