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.
This commit is contained in:
Pulse Monitor
2025-08-23 22:16:01 +00:00
parent 8e9e6ea0aa
commit 1834a703d5
7 changed files with 220 additions and 96 deletions
+1
View File
@@ -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
+1 -1
View File
@@ -1 +1 @@
4.7.4
4.7.5
+12 -1
View File
@@ -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
+158
View File
@@ -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 <vmid> --agent enabled=1
```
### 3. Verify Guest Agent is Working
Check if the agent is responding:
```bash
qm agent <vmid> ping
```
Get filesystem info (what Pulse uses):
```bash
qm agent <vmid> 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 <vmid> | grep agent
```
**Check agent communication:**
```bash
# Should return without error
qm agent <vmid> 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
+41 -93
View File
@@ -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/tty 2>/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/tty 2>/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()
}
+1 -1
View File
@@ -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"
+6
View File
@@ -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