From 1834a703d5d5b9ed42f121bb258087e8d4db7b52 Mon Sep 17 00:00:00 2001 From: Pulse Monitor Date: Sat, 23 Aug 2025 22:16:01 +0000 Subject: [PATCH] refactor: simplify setup flow by removing setup code prompts Replaced the two-step setup code process with a simpler token-in-URL approach: - Auth token is now embedded directly in the setup URL - No more prompting users for setup codes - Same security level with better UX - Backwards compatible with old setupCode field The new flow generates a command like: curl -sSL "http://pulse/api/setup-script?...&auth_token=TOKEN" | bash This makes it much easier for users, especially in Proxmox shell where interactive prompts can be problematic. --- README.md | 1 + VERSION | 2 +- docs/FAQ.md | 13 ++- docs/VM_DISK_MONITORING.md | 158 ++++++++++++++++++++++++++++++++ internal/api/config_handlers.go | 134 +++++++++------------------ internal/updates/version.go | 2 +- scripts/hot-dev.sh | 6 ++ 7 files changed, 220 insertions(+), 96 deletions(-) create mode 100644 docs/VM_DISK_MONITORING.md diff --git a/README.md b/README.md index 531d3e8bc..7dc6d7aae 100644 --- a/README.md +++ b/README.md @@ -450,6 +450,7 @@ journalctl -u pulse -f - [Docker Guide](docs/DOCKER.md) - Complete Docker deployment guide - [Configuration Guide](docs/CONFIGURATION.md) - Complete setup and configuration +- [VM Disk Monitoring](docs/VM_DISK_MONITORING.md) - Set up QEMU Guest Agent for accurate VM disk usage - [Port Configuration](docs/PORT_CONFIGURATION.md) - How to change the default port - [Troubleshooting](docs/TROUBLESHOOTING.md) - Common issues and solutions - [API Reference](docs/API.md) - REST API endpoints and examples diff --git a/VERSION b/VERSION index 5ca7df98c..93d7d00ec 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.7.4 \ No newline at end of file +4.7.5 \ No newline at end of file diff --git a/docs/FAQ.md b/docs/FAQ.md index dfc5c608e..3041f6d9b 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -31,7 +31,8 @@ If a setting is disabled with an amber warning, it's being overridden by an envi Remove the env var (check `sudo systemctl show pulse | grep Environment`) and restart to enable UI configuration. ### What permissions needed? -- PVE: `PVEAuditor` minimum +- PVE: `PVEAuditor` minimum (includes VM.GuestAgent.Audit for disk usage in PVE 9+) +- PVE 8: Also needs `VM.Monitor` permission for VM disk usage via QEMU agent - PBS: `DatastoreReader` minimum ### API tokens vs passwords? @@ -87,6 +88,16 @@ Reduce `metricsRetentionDays` in settings and restart ## Features +### Why do VMs show allocated size instead of actual usage? +VMs need the QEMU Guest Agent installed to report actual disk usage. Without it, Pulse can only show the allocated disk size. See [VM Disk Monitoring Guide](VM_DISK_MONITORING.md) for setup instructions. + +### How do I see real disk usage for VMs? +Install QEMU Guest Agent in your VMs: +- Linux: `apt install qemu-guest-agent` or `yum install qemu-guest-agent` +- Windows: Install virtio-win guest tools +- Enable in VM Options → QEMU Guest Agent +See [VM Disk Monitoring Guide](VM_DISK_MONITORING.md) for details. + ### Multiple clusters? Yes, add multiple nodes in Settings diff --git a/docs/VM_DISK_MONITORING.md b/docs/VM_DISK_MONITORING.md new file mode 100644 index 000000000..667899dd0 --- /dev/null +++ b/docs/VM_DISK_MONITORING.md @@ -0,0 +1,158 @@ +# VM Disk Usage Monitoring + +Pulse can show actual disk usage for VMs (just like containers) when the QEMU Guest Agent is installed and configured properly. + +## What You See + +**Without QEMU Guest Agent:** +- VMs show allocated disk size only (e.g., 32GB allocated) +- No visibility into actual disk usage inside the VM + +**With QEMU Guest Agent:** +- VMs show real disk usage like containers do (e.g., 5.2GB used of 32GB) +- Accurate threshold alerts based on actual usage +- Better capacity planning with real data + +## Requirements + +### 1. Install QEMU Guest Agent in Your VMs + +**Linux VMs:** +```bash +# Debian/Ubuntu +apt-get install qemu-guest-agent +systemctl enable --now qemu-guest-agent + +# RHEL/Rocky/AlmaLinux +yum install qemu-guest-agent +systemctl enable --now qemu-guest-agent + +# Alpine +apk add qemu-guest-agent +rc-update add qemu-guest-agent +rc-service qemu-guest-agent start +``` + +**Windows VMs:** +- Download virtio-win guest tools from: https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/ +- Install the guest tools package which includes the QEMU Guest Agent +- The service starts automatically after installation + +### 2. Enable Guest Agent in VM Options + +In Proxmox web UI: +1. Select your VM +2. Go to **Options** → **QEMU Guest Agent** +3. Check **Enabled** +4. Start/restart the VM + +Or via CLI: +```bash +qm set --agent enabled=1 +``` + +### 3. Verify Guest Agent is Working + +Check if the agent is responding: +```bash +qm agent ping +``` + +Get filesystem info (what Pulse uses): +```bash +qm agent get-fsinfo +``` + +### 4. Pulse Permissions + +Pulse needs the right permissions to query the guest agent: + +**Proxmox 8 and below:** Requires `VM.Monitor` permission +**Proxmox 9+:** Requires `VM.GuestAgent.Audit` permission + +When you run the Pulse setup script, it automatically detects your Proxmox version and sets the correct permissions. If setting up manually: + +```bash +# Proxmox 9+ +pveum aclmod / -user pulse-monitor@pam -role PVEAuditor +# PVEAuditor includes VM.GuestAgent.Audit in PVE 9+ + +# Proxmox 8 and below +pveum role add PulseMonitor -privs VM.Monitor +pveum aclmod / -user pulse-monitor@pam -role PulseMonitor +``` + +## Troubleshooting + +### Guest Agent Not Responding + +**Check if agent is running inside VM:** +```bash +# Linux +systemctl status qemu-guest-agent + +# Windows +Get-Service QEMU-GA +``` + +**Check VM configuration:** +```bash +# Should show "agent: 1" +qm config | grep agent +``` + +**Check agent communication:** +```bash +# Should return without error +qm agent ping +``` + +### Disk Usage Not Showing + +If the agent is working but Pulse still shows allocated size: + +1. **Check Pulse permissions** - Ensure the Pulse user has VM.Monitor (PVE 8) or VM.GuestAgent.Audit (PVE 9+) +2. **Check agent version** - Older agents might not support filesystem info +3. **Windows VMs** - Ensure virtio-win drivers are up to date +4. **Check Pulse logs** - Look for "GetVMFSInfo" errors + +### Network Filesystems + +The agent reports all mounted filesystems. Pulse automatically filters out: +- Network mounts (NFS, CIFS, SMB) +- Special filesystems (proc, sys, tmpfs, etc.) +- Bind mounts and overlays + +Only local disk usage is counted toward the VM's total. + +## Best Practices + +1. **Install guest agent in VM templates** - New VMs will have it ready +2. **Monitor agent status** - Set up alerts if critical VMs lose agent connectivity +3. **Keep agents updated** - Update guest agents when updating VM operating systems +4. **Test after VM migrations** - Verify agent still works after moving VMs between nodes + +## Platform-Specific Notes + +### Cloud-Init Images +Most cloud images include qemu-guest-agent pre-installed but may need to be enabled: +```bash +systemctl enable --now qemu-guest-agent +``` + +### Docker/Kubernetes VMs +Container workloads can show high disk usage due to container layers. Consider: +- Using separate disks for container storage +- Monitoring container disk usage separately +- Setting appropriate thresholds for container hosts + +### Database VMs +Databases often pre-allocate space. The guest agent shows actual usage, which might be less than what the database reports internally. + +## Benefits + +With QEMU Guest Agent disk monitoring: +- **Accurate alerts** - Alert on real usage, not allocated space +- **Better planning** - See actual growth trends +- **Prevent surprises** - Know when VMs are actually running out of space +- **Optimize storage** - Identify over-provisioned VMs \ No newline at end of file diff --git a/internal/api/config_handlers.go b/internal/api/config_handlers.go index e2bec169d..82bf36e99 100644 --- a/internal/api/config_handlers.go +++ b/internal/api/config_handlers.go @@ -1806,6 +1806,7 @@ func (h *ConfigHandlers) HandleSetupScript(w http.ResponseWriter, r *http.Reques serverHost := query.Get("host") pulseURL := query.Get("pulse_url") // URL of the Pulse server for auto-registration backupPerms := query.Get("backup_perms") == "true" // Whether to add backup management permissions + authToken := query.Get("auth_token") // Temporary auth token for auto-registration // Validate required parameters if serverType == "" { @@ -2025,41 +2026,11 @@ else echo "🔄 Attempting auto-registration with Pulse..." echo "" - # Prompt for setup code - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo "🔐 SETUP CODE REQUIRED" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo "Enter your 6-character setup code from Pulse UI: " + # Use auth token from URL parameter (much simpler!) + AUTH_TOKEN="%s" - # Check if code was provided via environment variable first - if [ -n "$PULSE_SETUP_CODE" ]; then - SETUP_CODE="$PULSE_SETUP_CODE" - echo "Using setup code from environment variable: $SETUP_CODE" - else - # Read setup code from user - if [ -t 0 ]; then - # Running interactively - read -p "> " SETUP_CODE - else - # Being piped - try to read from terminal - if read -p "> " SETUP_CODE /dev/null; then - : - else - echo "" - echo "❌ Cannot read setup code in non-interactive mode" - echo " Please run this script interactively or provide the code via environment variable:" - echo " PULSE_SETUP_CODE=XXXXXX curl -sSL ... | bash" - echo "" - AUTO_REG_SUCCESS=false - SETUP_CODE="" - fi - fi - fi - - echo "" - - # Only proceed with auto-registration if we have a setup code - if [ -n "$SETUP_CODE" ]; then + # Only proceed with auto-registration if we have an auth token + if [ -n "$AUTH_TOKEN" ]; then # Get the server's hostname SERVER_HOSTNAME=$(hostname -f 2>/dev/null || hostname) SERVER_IP=$(hostname -I | awk '{print $1}') @@ -2095,7 +2066,7 @@ else "serverName": "$SERVER_HOSTNAME", "tokenId": "pulse-monitor@pam!%s", "tokenValue": "$TOKEN_VALUE", - "setupCode": "$SETUP_CODE" + "authToken": "$AUTH_TOKEN" } EOF ) @@ -2183,7 +2154,7 @@ if [ "$AUTO_REG_SUCCESS" != true ]; then fi `, serverName, time.Now().Format("2006-01-02 15:04:05"), pulseIP, tokenName, tokenName, tokenName, tokenName, tokenName, tokenName, - pulseURL, serverHost, tokenName, tokenName, storagePerms, tokenName, serverHost) + authToken, pulseURL, serverHost, tokenName, tokenName, storagePerms, tokenName, serverHost) } else { // PBS script = fmt.Sprintf(`#!/bin/bash @@ -2292,41 +2263,11 @@ else echo "🔄 Attempting auto-registration with Pulse..." echo "" - # Prompt for setup code - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo "🔐 SETUP CODE REQUIRED" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo "Enter your 6-character setup code from Pulse UI: " + # Use auth token from URL parameter (much simpler!) + AUTH_TOKEN="%s" - # Check if code was provided via environment variable first - if [ -n "$PULSE_SETUP_CODE" ]; then - SETUP_CODE="$PULSE_SETUP_CODE" - echo "Using setup code from environment variable: $SETUP_CODE" - else - # Read setup code from user - if [ -t 0 ]; then - # Running interactively - read -p "> " SETUP_CODE - else - # Being piped - try to read from terminal - if read -p "> " SETUP_CODE /dev/null; then - : - else - echo "" - echo "❌ Cannot read setup code in non-interactive mode" - echo " Please run this script interactively or provide the code via environment variable:" - echo " PULSE_SETUP_CODE=XXXXXX curl -sSL ... | bash" - echo "" - AUTO_REG_SUCCESS=false - SETUP_CODE="" - fi - fi - fi - - echo "" - - # Only proceed with auto-registration if we have a setup code - if [ -n "$SETUP_CODE" ]; then + # Only proceed with auto-registration if we have an auth token + if [ -n "$AUTH_TOKEN" ]; then # Get the server's hostname SERVER_HOSTNAME=$(hostname -f 2>/dev/null || hostname) SERVER_IP=$(hostname -I | awk '{print $1}') @@ -2362,7 +2303,7 @@ else "serverName": "$SERVER_HOSTNAME", "tokenId": "pulse-monitor@pbs!%s", "tokenValue": "$TOKEN_VALUE", - "setupCode": "$SETUP_CODE" + "authToken": "$AUTH_TOKEN" } EOF ) @@ -2432,7 +2373,7 @@ if [ "$AUTO_REG_SUCCESS" != true ]; then fi `, serverName, time.Now().Format("2006-01-02 15:04:05"), pulseIP, tokenName, tokenName, tokenName, tokenName, tokenName, - pulseURL, serverHost, tokenName, tokenName, tokenName, tokenName) + authToken, pulseURL, serverHost, tokenName, tokenName, tokenName, tokenName) } // Set headers for script download @@ -2472,14 +2413,14 @@ func (h *ConfigHandlers) HandleSetupScriptURL(w http.ResponseWriter, r *http.Req return } - // Generate a 6-character setup code - code := h.generateSetupCode() - codeHash := internalauth.HashAPIToken(code) // Reuse the hash function for consistency + // Generate a temporary auth token (simpler than setup codes) + token := h.generateSetupCode() // Reuse the generation function + tokenHash := internalauth.HashAPIToken(token) - // Store the code with expiry (5 minutes) + // Store the token with expiry (5 minutes) expiry := time.Now().Add(5 * time.Minute) h.codeMutex.Lock() - h.setupCodes[codeHash] = &SetupCode{ + h.setupCodes[tokenHash] = &SetupCode{ ExpiresAt: expiry, Used: false, NodeType: req.Type, @@ -2488,12 +2429,12 @@ func (h *ConfigHandlers) HandleSetupScriptURL(w http.ResponseWriter, r *http.Req h.codeMutex.Unlock() log.Info(). - Str("code_hash", codeHash[:8]+"..."). + Str("token_hash", tokenHash[:8]+"..."). Time("expiry", expiry). Str("type", req.Type). - Msg("Generated setup code") + Msg("Generated temporary auth token") - // Build the URL without any authentication tokens + // Build the URL with the token included pulseURL := fmt.Sprintf("%s://%s", "http", r.Host) if r.TLS != nil { pulseURL = fmt.Sprintf("%s://%s", "https", r.Host) @@ -2509,16 +2450,15 @@ func (h *ConfigHandlers) HandleSetupScriptURL(w http.ResponseWriter, r *http.Req backupPerms = "&backup_perms=true" } - // URL doesn't contain any secrets - the code is entered interactively - scriptURL := fmt.Sprintf("%s/api/setup-script?type=%s%s&pulse_url=%s%s", - pulseURL, req.Type, encodedHost, pulseURL, backupPerms) + // Include the token directly in the URL - much simpler! + scriptURL := fmt.Sprintf("%s/api/setup-script?type=%s%s&pulse_url=%s%s&auth_token=%s", + pulseURL, req.Type, encodedHost, pulseURL, backupPerms, token) - // Return the URL, command, and setup code - // Include the setup code in the command for easy copy-paste in Proxmox shell + // Return a simple curl command - no environment variables needed response := map[string]interface{}{ "url": scriptURL, - "command": fmt.Sprintf(`PULSE_SETUP_CODE=%s curl -sSL "%s" | bash`, code, scriptURL), - "setupCode": code, // The user needs to see this + "command": fmt.Sprintf(`curl -sSL "%s" | bash`, scriptURL), + "setupCode": token, // Keep for backwards compatibility but it's really just a token now "expires": expiry.Unix(), } @@ -2533,7 +2473,8 @@ type AutoRegisterRequest struct { TokenID string `json:"tokenId"` // Full token ID like pulse-monitor@pam!pulse-token TokenValue string `json:"tokenValue,omitempty"` // The token value for the node ServerName string `json:"serverName"` // Hostname or IP - SetupCode string `json:"setupCode,omitempty"` // One-time setup code for authentication + SetupCode string `json:"setupCode,omitempty"` // One-time setup code for authentication (deprecated) + AuthToken string `json:"authToken,omitempty"` // Direct auth token from URL (new approach) // New secure fields RequestToken bool `json:"requestToken,omitempty"` // If true, Pulse will generate and return a token Username string `json:"username,omitempty"` // Username for creating token (e.g., "root@pam") @@ -2565,9 +2506,15 @@ func (h *ConfigHandlers) HandleAutoRegister(w http.ResponseWriter, r *http.Reque // Check authentication - require either setup code or API token if auth is enabled authenticated := false - // First check for setup code in the request - if req.SetupCode != "" { - codeHash := internalauth.HashAPIToken(req.SetupCode) + // Support both setupCode (old) and authToken (new) fields + authCode := req.SetupCode + if req.AuthToken != "" { + authCode = req.AuthToken + } + + // First check for setup code/auth token in the request + if authCode != "" { + codeHash := internalauth.HashAPIToken(authCode) h.codeMutex.Lock() setupCode, exists := h.setupCodes[codeHash] if exists && !setupCode.Used && time.Now().Before(setupCode.ExpiresAt) { @@ -2580,7 +2527,8 @@ func (h *ConfigHandlers) HandleAutoRegister(w http.ResponseWriter, r *http.Reque log.Info(). Str("type", req.Type). Str("host", req.Host). - Msg("Auto-register authenticated via setup code") + Bool("via_authToken", req.AuthToken != ""). + Msg("Auto-register authenticated via setup code/token") } else { log.Warn(). Str("expected_type", setupCode.NodeType). @@ -2592,7 +2540,7 @@ func (h *ConfigHandlers) HandleAutoRegister(w http.ResponseWriter, r *http.Reque } else if exists { log.Warn().Msg("Setup code expired") } else { - log.Warn().Msg("Invalid setup code") + log.Warn().Msg("Invalid setup code/token") } h.codeMutex.Unlock() } diff --git a/internal/updates/version.go b/internal/updates/version.go index 275d39504..8b7dbd768 100644 --- a/internal/updates/version.go +++ b/internal/updates/version.go @@ -164,7 +164,7 @@ func GetCurrentVersion() (*VersionInfo, error) { } // Final fallback - version := "4.7.4" + version := "4.7.5" channel := "stable" if strings.Contains(strings.ToLower(version), "rc") { channel = "rc" diff --git a/scripts/hot-dev.sh b/scripts/hot-dev.sh index 00547531e..8fefbc6ff 100755 --- a/scripts/hot-dev.sh +++ b/scripts/hot-dev.sh @@ -18,8 +18,12 @@ echo "=========================================" # Kill any existing Pulse processes (but NOT ttyd/tmux which run Claude Code!) sudo systemctl stop pulse-backend 2>/dev/null +# Kill the backend-watch script to free up port 7655 +pkill -f "backend-watch.sh" 2>/dev/null # Use exact match to only kill the "pulse" binary, not processes running FROM /opt/pulse pkill -x "pulse" 2>/dev/null +# Give services time to fully stop +sleep 1 # Start backend on port 7656 (one port up from normal) echo "Starting backend on port 7656..." @@ -75,6 +79,8 @@ cleanup() { echo "Stopping services..." kill $BACKEND_PID 2>/dev/null rm -f vite.config.dev.ts + echo "Restarting backend-watch service..." + sudo systemctl start pulse-backend 2>/dev/null exit } trap cleanup INT TERM