mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-23 11:46:28 +00:00
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:
@@ -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
|
||||
|
||||
+12
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user