mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-19 16:27:37 +00:00
Improve host agent onboarding flow
This commit is contained in:
parent
c933eb699c
commit
138d8facd2
10 changed files with 1015 additions and 163 deletions
|
|
@ -17,7 +17,53 @@ machine alongside the rest of your infrastructure.
|
|||
|
||||
> Replace `<api-token>` with a Pulse API token limited to the `host-agent:report` scope. Tokens generated from **Settings → Agents → Host agents** already apply this scope.
|
||||
|
||||
### Linux (systemd)
|
||||
### Linux
|
||||
|
||||
The hosted installer handles systemd, rc.local environments, and Unraid automatically.
|
||||
|
||||
```bash
|
||||
curl -fsSL http://pulse.example.local:7655/install-host-agent.sh | \
|
||||
bash -s -- --url http://pulse.example.local:7655 --token <api-token>
|
||||
```
|
||||
|
||||
- On systemd machines the script installs the binary, wires up `/etc/systemd/system/pulse-host-agent.service`, enables it, and tails the registration status.
|
||||
- On Unraid hosts it starts the agent under `nohup`, creates `/var/log/pulse`, and (optionally) inserts the auto-start line into `/boot/config/go`.
|
||||
- On minimalist distros without systemd (e.g. Alpine) it creates/updates `/etc/rc.local`, adds the background runner, and verifies it launches.
|
||||
|
||||
Use `--force` to skip interactive prompts or `--interval 1m` to change the polling cadence.
|
||||
|
||||
### macOS
|
||||
|
||||
```bash
|
||||
curl -fsSL http://pulse.example.local:7655/install-host-agent.sh | \
|
||||
bash -s -- --url http://pulse.example.local:7655 --token <api-token>
|
||||
```
|
||||
|
||||
On macOS the installer stores the token in the Keychain when possible, generates a launchd plist inside `~/Library/LaunchAgents`, and restarts the job so the agent survives logouts and reboots.
|
||||
|
||||
### Windows
|
||||
|
||||
Run the PowerShell bootstrapper as an administrator:
|
||||
|
||||
```powershell
|
||||
irm http://pulse.example.local:7655/install-host-agent.ps1 | iex
|
||||
```
|
||||
|
||||
Set `PULSE_URL` and `PULSE_TOKEN` in the environment first for a non-interactive flow:
|
||||
|
||||
```powershell
|
||||
$env:PULSE_URL = "http://pulse.example.local:7655"
|
||||
$env:PULSE_TOKEN = "<api-token>"
|
||||
irm http://pulse.example.local:7655/install-host-agent.ps1 | iex
|
||||
```
|
||||
|
||||
The script installs the service under `PulseHostAgent`, registers Windows Event Log messages, configures automatic recovery on failure, and waits for Pulse to acknowledge the new host.
|
||||
|
||||
### Manual installation (advanced)
|
||||
|
||||
Prefer to take full control or working in air-gapped environments? You can still download the static binaries and wire them up manually. The commands below mirror what the installer scripts perform for their respective platforms.
|
||||
|
||||
#### Linux (systemd)
|
||||
|
||||
```bash
|
||||
sudo curl -fsSL https://github.com/rcourtman/Pulse/releases/latest/download/pulse-host-agent-linux-amd64 \
|
||||
|
|
@ -31,7 +77,7 @@ sudo /usr/local/bin/pulse-host-agent \
|
|||
|
||||
For persistence, drop a systemd unit (e.g. `/etc/systemd/system/pulse-host-agent.service`) referencing the same command and enable it with `systemctl enable --now pulse-host-agent`.
|
||||
|
||||
### macOS (launchd)
|
||||
#### macOS (launchd)
|
||||
|
||||
```bash
|
||||
sudo curl -fsSL https://github.com/rcourtman/Pulse/releases/latest/download/pulse-host-agent-darwin-arm64 \
|
||||
|
|
@ -72,9 +118,18 @@ Create `~/Library/LaunchAgents/com.pulse.host-agent.plist` to keep the agent run
|
|||
|
||||
Load it with `launchctl load ~/Library/LaunchAgents/com.pulse.host-agent.plist`.
|
||||
|
||||
### Windows
|
||||
#### Windows (manual)
|
||||
|
||||
A Windows build will ship shortly. In the meantime run the Linux/WSL binary or compile from source (`GOOS=windows GOARCH=amd64`).
|
||||
Compile from source (`GOOS=windows GOARCH=amd64`) or download the latest release, then install the Windows service yourself:
|
||||
|
||||
```powershell
|
||||
New-Service -Name PulseHostAgent `
|
||||
-BinaryPathName '"C:\Program Files\Pulse\pulse-host-agent.exe" --url http://pulse.example.local:7655 --token <api-token> --interval 30s' `
|
||||
-DisplayName "Pulse Host Agent" `
|
||||
-Description "Monitors system metrics and reports to Pulse monitoring server" `
|
||||
-StartupType Automatic
|
||||
Start-Service -Name PulseHostAgent
|
||||
```
|
||||
|
||||
## Command Flags
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,63 @@ function Write-Error { param([string]$msg) Write-Color $Red "✗ $msg" }
|
|||
function Write-Info { param([string]$msg) Write-Color $Blue "ℹ $msg" }
|
||||
function Write-Warning { param([string]$msg) Write-Color $Yellow "⚠ $msg" }
|
||||
|
||||
function Write-InstallerEvent {
|
||||
param(
|
||||
[string]$SourceName,
|
||||
[string]$Message,
|
||||
[ValidateSet('Information', 'Warning', 'Error')] [string]$EntryType = 'Information',
|
||||
[int]$EventId = 1000
|
||||
)
|
||||
|
||||
if (-not $SourceName) { return }
|
||||
|
||||
try {
|
||||
Write-EventLog -LogName Application -Source $SourceName -EventId $EventId -EntryType $EntryType -Message $Message
|
||||
} catch {
|
||||
Write-Warning "Unable to write installer event log entry: $_"
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls13
|
||||
} catch {
|
||||
# Ignore if platform does not expose TLS 1.3
|
||||
}
|
||||
|
||||
function Get-RecentAgentEvents {
|
||||
param(
|
||||
[string]$ProviderName,
|
||||
[int]$Max = 5
|
||||
)
|
||||
try {
|
||||
return Get-WinEvent -FilterHashtable @{ LogName = 'Application'; ProviderName = $ProviderName } -MaxEvents $Max -ErrorAction Stop
|
||||
} catch {
|
||||
return Get-EventLog -LogName Application -Source $ProviderName -Newest $Max -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
function Test-AgentRegistration {
|
||||
param(
|
||||
[string]$PulseUrl,
|
||||
[string]$Hostname,
|
||||
[string]$Token
|
||||
)
|
||||
|
||||
if (-not $Token) {
|
||||
return $null
|
||||
}
|
||||
|
||||
try {
|
||||
$encodedHostname = [System.Uri]::EscapeDataString($Hostname)
|
||||
$lookupUri = "$PulseUrl/api/agents/host/lookup?hostname=$encodedHostname"
|
||||
$headers = @{ Authorization = "Bearer $Token" }
|
||||
$response = Invoke-RestMethod -Uri $lookupUri -Headers $headers -Method Get -ErrorAction Stop
|
||||
return $response.host
|
||||
} catch {
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Color $Blue "═══════════════════════════════════════════════════════════"
|
||||
Write-Color $Blue " Pulse Host Agent - Windows Installation"
|
||||
|
|
@ -89,6 +146,13 @@ try {
|
|||
# Download binary
|
||||
Invoke-WebRequest -Uri $downloadUrl -OutFile $agentPath -UseBasicParsing
|
||||
Write-Success "Downloaded agent to $agentPath"
|
||||
|
||||
$agentArgs = @("--url", "`"$PulseUrl`"", "--interval", $Interval)
|
||||
if ($Token) {
|
||||
$agentArgs += @("--token", "`"$Token`"")
|
||||
}
|
||||
$serviceBinaryPath = "`"$agentPath`" $($agentArgs -join ' ')"
|
||||
$manualCommand = "& `"$agentPath`" $($agentArgs -join ' ')"
|
||||
} catch {
|
||||
Write-Error "Failed to download agent: $_"
|
||||
exit 1
|
||||
|
|
@ -119,17 +183,6 @@ if ($existingService) {
|
|||
if (-not $NoService) {
|
||||
Write-Info "Installing native Windows service with built-in service support..."
|
||||
|
||||
# Build service arguments
|
||||
$serviceArgs = @(
|
||||
"--url", $PulseUrl,
|
||||
"--interval", $Interval
|
||||
)
|
||||
if ($Token) {
|
||||
$serviceArgs += "--token", $Token
|
||||
}
|
||||
|
||||
$serviceBinaryPath = "`"$agentPath`" $($serviceArgs -join ' ')"
|
||||
|
||||
try {
|
||||
if ($existingService) {
|
||||
Write-Info "Removing existing service..."
|
||||
|
|
@ -156,6 +209,8 @@ if (-not $NoService) {
|
|||
Write-Warning "Could not register Event Log source (not critical): $_"
|
||||
}
|
||||
|
||||
Write-InstallerEvent -SourceName $serviceName -Message "Pulse Host Agent installer registered service version $(Get-Item $agentPath).VersionInfo.FileVersion" -EventId 1000
|
||||
|
||||
# Configure service recovery options (restart on failure)
|
||||
sc.exe failure $serviceName reset= 86400 actions= restart/60000/restart/60000/restart/60000 | Out-Null
|
||||
Write-Success "Configured automatic restart on failure"
|
||||
|
|
@ -169,30 +224,62 @@ if (-not $NoService) {
|
|||
if ($status -eq 'Running') {
|
||||
Write-Success "Service started successfully!"
|
||||
|
||||
# Optional: Validate that agent is reporting
|
||||
Write-Info "Waiting 10 seconds to validate agent reporting..."
|
||||
Start-Sleep -Seconds 10
|
||||
|
||||
# Check Event Log for successful startup
|
||||
$recentLogs = Get-EventLog -LogName Application -Source $serviceName -Newest 5 -ErrorAction SilentlyContinue
|
||||
$hasStarted = $recentLogs | Where-Object { $_.Message -like "*started successfully*" }
|
||||
|
||||
if ($hasStarted) {
|
||||
Write-Success "Agent is reporting successfully!"
|
||||
$hostname = $env:COMPUTERNAME
|
||||
$lookupHost = Test-AgentRegistration -PulseUrl $PulseUrl -Hostname $hostname -Token $Token
|
||||
if ($lookupHost) {
|
||||
Write-Success "Agent successfully registered with Pulse (host '$hostname')."
|
||||
if ($lookupHost.status) {
|
||||
$lastSeen = $lookupHost.lastSeen
|
||||
if ($lastSeen -is [DateTime]) {
|
||||
$lastSeen = $lastSeen.ToString("u")
|
||||
}
|
||||
Write-Info ("Pulse reports status: {0} (last seen {1})" -f $lookupHost.status, $lastSeen)
|
||||
}
|
||||
Write-Info "Check your Pulse dashboard - this host should appear shortly."
|
||||
$statusForLog = if ($lookupHost.status) { $lookupHost.status } else { 'unknown' }
|
||||
Write-InstallerEvent -SourceName $serviceName -Message "Installer verified host '$hostname' reporting to Pulse (status: $statusForLog)." -EventId 1010
|
||||
} elseif ($Token) {
|
||||
Write-Warning "Agent is running but the lookup endpoint has not confirmed registration yet."
|
||||
Write-Info "It may take another moment for metrics to appear in the dashboard."
|
||||
Write-InstallerEvent -SourceName $serviceName -Message "Installer could not yet confirm host '$hostname' registration with Pulse." -EntryType Warning -EventId 1011
|
||||
} else {
|
||||
Write-Warning "Agent started but validation incomplete. Check Event Viewer if issues occur."
|
||||
Write-Info "Registration check skipped (no API token available)."
|
||||
Write-InstallerEvent -SourceName $serviceName -Message "Installer skipped registration lookup (no API token provided)." -EventId 1012
|
||||
}
|
||||
|
||||
$recentLogs = Get-RecentAgentEvents -ProviderName $serviceName -Max 5
|
||||
if ($recentLogs) {
|
||||
Write-Info "Recent service events:"
|
||||
$recentLogs | Select-Object -First 3 | ForEach-Object {
|
||||
$time = $_.TimeCreated
|
||||
if (-not $time) { $time = $_.TimeGenerated }
|
||||
Write-Host (" [{0}] {1}" -f $time.ToString("u"), $_.Message)
|
||||
}
|
||||
} else {
|
||||
Write-Warning "No recent Application log entries were found for $serviceName."
|
||||
}
|
||||
} else {
|
||||
Write-Warning "Service status: $status"
|
||||
Write-Info "Checking service logs..."
|
||||
Get-EventLog -LogName Application -Source $serviceName -Newest 5 -ErrorAction SilentlyContinue | Format-List TimeGenerated, Message
|
||||
$recentLogs = Get-RecentAgentEvents -ProviderName $serviceName -Max 5
|
||||
if ($recentLogs) {
|
||||
$recentLogs | ForEach-Object {
|
||||
$time = $_.TimeCreated
|
||||
if (-not $time) { $time = $_.TimeGenerated }
|
||||
Write-Host (" [{0}] {1}" -f $time.ToString("u"), $_.Message)
|
||||
}
|
||||
} else {
|
||||
Write-Warning "No Application log entries were found for $serviceName."
|
||||
}
|
||||
}
|
||||
|
||||
} catch {
|
||||
Write-Error "Failed to create/start service: $_"
|
||||
Write-Info "You can start the agent manually with:"
|
||||
Write-Host " & `"$agentPath`" --url $PulseUrl --interval $Interval $(if ($Token) { '--token ***' })"
|
||||
Write-Host " $manualCommand"
|
||||
Write-Host ""
|
||||
Write-Info "Or check Windows Event Viewer (Application log) for error details."
|
||||
exit 1
|
||||
|
|
@ -201,7 +288,7 @@ if (-not $NoService) {
|
|||
Write-Info "Skipping service installation (--NoService flag)"
|
||||
Write-Host ""
|
||||
Write-Info "To start the agent manually:"
|
||||
Write-Host " & `"$agentPath`" --url $PulseUrl --interval $Interval $(if ($Token) { '--token ***' })"
|
||||
Write-Host " $manualCommand"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
|
|
@ -216,7 +303,7 @@ Write-Host " Stop: Stop-Service -Name PulseHostAgent"
|
|||
Write-Host " Restart: Restart-Service -Name PulseHostAgent"
|
||||
Write-Host " Status: Get-Service -Name PulseHostAgent | Select Status, StartType"
|
||||
Write-Host " Remove: sc.exe delete PulseHostAgent"
|
||||
Write-Host " Logs: Get-EventLog -LogName Application -Source PulseHostAgent -Newest 50"
|
||||
Write-Host " Logs: Get-WinEvent -FilterHashtable @{LogName='Application'; ProviderName='PulseHostAgent'} -MaxEvents 50"
|
||||
Write-Host ""
|
||||
|
||||
Write-Info "Files installed:"
|
||||
|
|
|
|||
|
|
@ -112,8 +112,10 @@ LINUX_LOG_FILE="$LINUX_LOG_DIR/host-agent.log"
|
|||
SERVICE_MODE="manual"
|
||||
MANUAL_START_CMD=""
|
||||
MANUAL_START_WRAPPED=""
|
||||
LAUNCH_IDENTIFIER=""
|
||||
UNRAID=false
|
||||
if [[ -f /etc/unraid-version ]]; then
|
||||
UNRAID_GO_FILE="/boot/config/go"
|
||||
if [[ -f "$UNRAID_GO_FILE" ]] || [[ -f /etc/unraid-version ]]; then
|
||||
UNRAID=true
|
||||
fi
|
||||
|
||||
|
|
@ -365,6 +367,15 @@ sudo mv "$TEMP_BINARY" "$AGENT_PATH"
|
|||
sudo chmod +x "$AGENT_PATH"
|
||||
log_success "Agent binary installed to $AGENT_PATH"
|
||||
|
||||
# Build reusable agent command strings
|
||||
AGENT_CMD="$AGENT_PATH --url $PULSE_URL"
|
||||
if [[ -n "$PULSE_TOKEN" ]]; then
|
||||
AGENT_CMD="$AGENT_CMD --token $PULSE_TOKEN"
|
||||
fi
|
||||
AGENT_CMD="$AGENT_CMD --interval $INTERVAL"
|
||||
MANUAL_START_CMD="$AGENT_CMD"
|
||||
MANUAL_START_WRAPPED="nohup $MANUAL_START_CMD >$LINUX_LOG_FILE 2>&1 &"
|
||||
|
||||
# Set up service based on platform
|
||||
if [[ "$PLATFORM" == "linux" ]] && command -v systemctl &> /dev/null; then
|
||||
log_info "Setting up systemd service..."
|
||||
|
|
@ -379,7 +390,7 @@ After=network.target
|
|||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=$AGENT_PATH --url $PULSE_URL --token $PULSE_TOKEN --interval $INTERVAL
|
||||
ExecStart=$AGENT_CMD
|
||||
Restart=always
|
||||
RestartSec=5s
|
||||
User=root
|
||||
|
|
@ -558,6 +569,7 @@ EOF
|
|||
chmod 600 "$LAUNCHD_PLIST"
|
||||
|
||||
LAUNCH_TARGET="gui/$(id -u)"
|
||||
LAUNCH_IDENTIFIER="$LAUNCH_TARGET/com.pulse.host-agent"
|
||||
|
||||
# Attempt to unload any existing service instance
|
||||
if launchctl bootout "$LAUNCH_TARGET" "$LAUNCHD_PLIST" 2>/dev/null; then
|
||||
|
|
@ -576,23 +588,128 @@ EOF
|
|||
fi
|
||||
SERVICE_MODE="launchd"
|
||||
else
|
||||
log_warn "Automatic service setup not available for this platform"
|
||||
sudo mkdir -p "$LINUX_LOG_DIR"
|
||||
log_info "To run the agent manually:"
|
||||
MANUAL_START_CMD="$AGENT_PATH --url $PULSE_URL --token $PULSE_TOKEN --interval $INTERVAL"
|
||||
MANUAL_START_WRAPPED="nohup $MANUAL_START_CMD >$LINUX_LOG_FILE 2>&1 &"
|
||||
log_info " $MANUAL_START_CMD"
|
||||
log_info ""
|
||||
log_info "To keep the agent running persistently:"
|
||||
log_info " $MANUAL_START_WRAPPED"
|
||||
if [[ "$UNRAID" == true ]]; then
|
||||
log_info ""
|
||||
log_info "On Unraid, add the wrapped command to /boot/config/go so it starts on boot."
|
||||
log_info "Detected Unraid (no systemd). Configuring persistent background service..."
|
||||
|
||||
if pgrep -f "$AGENT_PATH" >/dev/null 2>&1; then
|
||||
log_warn "Existing pulse-host-agent process detected; restarting with new binary"
|
||||
sudo pkill -f "$AGENT_PATH" 2>/dev/null || true
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
log_info "Starting host agent with nohup (logs: $LINUX_LOG_FILE)"
|
||||
if sudo bash -c "$MANUAL_START_WRAPPED"; then
|
||||
log_success "Agent started in the background"
|
||||
else
|
||||
log_error "Failed to start agent automatically. Run manually:"
|
||||
log_info " $MANUAL_START_WRAPPED"
|
||||
fi
|
||||
|
||||
if [[ -f "$UNRAID_GO_FILE" ]]; then
|
||||
if sudo grep -qF -- "$MANUAL_START_WRAPPED" "$UNRAID_GO_FILE"; then
|
||||
log_info "Auto-start entry already present in $UNRAID_GO_FILE"
|
||||
else
|
||||
APPEND_STARTUP=true
|
||||
if [[ "$FORCE" == false ]]; then
|
||||
read -p "Add agent auto-start to $UNRAID_GO_FILE? (Y/n): " ADD_STARTUP_CHOICE
|
||||
if [[ "$ADD_STARTUP_CHOICE" == "n" || "$ADD_STARTUP_CHOICE" == "N" ]]; then
|
||||
APPEND_STARTUP=false
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$APPEND_STARTUP" == true ]]; then
|
||||
if sudo grep -qF "# Pulse Host Agent auto-start" "$UNRAID_GO_FILE"; then
|
||||
log_info "Updating existing auto-start entry in $UNRAID_GO_FILE"
|
||||
sudo sed -i '/# Pulse Host Agent auto-start/,+1d' "$UNRAID_GO_FILE" 2>/dev/null || true
|
||||
fi
|
||||
sudo tee -a "$UNRAID_GO_FILE" > /dev/null <<EOF
|
||||
# Pulse Host Agent auto-start
|
||||
$MANUAL_START_WRAPPED
|
||||
EOF
|
||||
log_success "Added auto-start command to $UNRAID_GO_FILE"
|
||||
else
|
||||
log_info "Skipped modifying $UNRAID_GO_FILE"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
log_warn "Could not find $UNRAID_GO_FILE; skipping persistence step."
|
||||
fi
|
||||
|
||||
log_info "To rerun manually: $MANUAL_START_CMD"
|
||||
SERVICE_MODE="unraid"
|
||||
else
|
||||
log_info ""
|
||||
log_info "On systems without systemd, add the wrapped command to /etc/rc.local (or similar) to start on boot."
|
||||
log_warn "Systemd not available; configuring rc.local-based startup"
|
||||
|
||||
RC_LOCAL_PATH=""
|
||||
for candidate in /etc/rc.local /etc/rc.d/rc.local; do
|
||||
if [[ -f "$candidate" ]]; then
|
||||
RC_LOCAL_PATH="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
CREATE_RC_LOCAL=false
|
||||
if [[ -z "$RC_LOCAL_PATH" ]]; then
|
||||
RC_LOCAL_PATH="/etc/rc.local"
|
||||
CREATE_RC_LOCAL=true
|
||||
fi
|
||||
|
||||
if [[ "$CREATE_RC_LOCAL" == true ]]; then
|
||||
log_info "Creating $RC_LOCAL_PATH"
|
||||
sudo tee "$RC_LOCAL_PATH" > /dev/null <<'EOF'
|
||||
#!/bin/sh
|
||||
# /etc/rc.local - generated by Pulse host agent installer
|
||||
# This script is executed at the end of each multi-user runlevel.
|
||||
exit 0
|
||||
EOF
|
||||
fi
|
||||
|
||||
if [[ -f "$RC_LOCAL_PATH" ]]; then
|
||||
RC_COMMENT="# Pulse Host Agent auto-start"
|
||||
if sudo grep -qF "$MANUAL_START_WRAPPED" "$RC_LOCAL_PATH"; then
|
||||
log_info "Auto-start entry already present in $RC_LOCAL_PATH"
|
||||
else
|
||||
APPEND_RC_LOCAL=true
|
||||
if [[ "$FORCE" == false ]]; then
|
||||
read -p "Add agent auto-start to $RC_LOCAL_PATH? (Y/n): " ADD_RC_CHOICE
|
||||
if [[ "$ADD_RC_CHOICE" == "n" || "$ADD_RC_CHOICE" == "N" ]]; then
|
||||
APPEND_RC_LOCAL=false
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$APPEND_RC_LOCAL" == true ]]; then
|
||||
sudo RC_APPEND_CMD="$MANUAL_START_WRAPPED" RC_COMMENT="$RC_COMMENT" RC_LOCAL_PATH="$RC_LOCAL_PATH" sh -c '
|
||||
tmpfile=$(mktemp)
|
||||
cp "$RC_LOCAL_PATH" "$tmpfile" 2>/dev/null || touch "$tmpfile"
|
||||
sed -i "/$RC_COMMENT/,+1d" "$tmpfile" 2>/dev/null || true
|
||||
sed -i "/^exit 0$/d" "$tmpfile" 2>/dev/null || true
|
||||
printf "\n%s\n%s\n" "$RC_COMMENT" "$RC_APPEND_CMD" >>"$tmpfile"
|
||||
echo "exit 0" >>"$tmpfile"
|
||||
mv "$tmpfile" "$RC_LOCAL_PATH"
|
||||
chmod +x "$RC_LOCAL_PATH"
|
||||
'
|
||||
log_success "Added auto-start command to $RC_LOCAL_PATH"
|
||||
else
|
||||
log_info "Skipped modifying $RC_LOCAL_PATH"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
log_warn "Could not access $RC_LOCAL_PATH; skipping persistence step."
|
||||
fi
|
||||
|
||||
log_info "Starting host agent with nohup (logs: $LINUX_LOG_FILE)"
|
||||
if sudo bash -c "$MANUAL_START_WRAPPED"; then
|
||||
log_success "Agent started in the background"
|
||||
else
|
||||
log_error "Failed to start agent automatically. Run manually:"
|
||||
log_info " $MANUAL_START_WRAPPED"
|
||||
fi
|
||||
|
||||
log_info "To manage manually, edit $RC_LOCAL_PATH or run:"
|
||||
log_info " $MANUAL_START_CMD"
|
||||
SERVICE_MODE="rc_local"
|
||||
fi
|
||||
SERVICE_MODE="manual"
|
||||
fi
|
||||
|
||||
# Validate installation
|
||||
|
|
@ -613,43 +730,72 @@ if [[ "$SERVICE_MODE" == "systemd" ]]; then
|
|||
log_info "Check logs with: sudo journalctl -u pulse-host-agent -n 50"
|
||||
fi
|
||||
elif [[ "$SERVICE_MODE" == "launchd" ]]; then
|
||||
if launchctl list | grep -q "com.pulse.host-agent"; then
|
||||
IDENTIFIER=${LAUNCH_IDENTIFIER:-"gui/$(id -u)/com.pulse.host-agent"}
|
||||
for _ in 1 2 3 4 5; do
|
||||
if launchctl print "$IDENTIFIER" >/dev/null 2>&1; then
|
||||
SERVICE_RUNNING=true
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
if [[ "$SERVICE_RUNNING" == true ]]; then
|
||||
log_success "Service is running successfully!"
|
||||
elif launchctl list | grep -q "com.pulse.host-agent"; then
|
||||
SERVICE_RUNNING=true
|
||||
log_success "Service is running successfully!"
|
||||
else
|
||||
log_warn "Service may not be running properly"
|
||||
log_info "Check logs with: tail -20 $MACOS_LOG_FILE"
|
||||
fi
|
||||
elif [[ "$SERVICE_MODE" == "unraid" ]]; then
|
||||
if pgrep -f "$AGENT_PATH" >/dev/null 2>&1; then
|
||||
SERVICE_RUNNING=true
|
||||
log_success "Agent process is running (nohup background task)"
|
||||
else
|
||||
log_warn "Agent process not detected; check $LINUX_LOG_FILE for errors"
|
||||
fi
|
||||
elif [[ "$SERVICE_MODE" == "rc_local" ]]; then
|
||||
if pgrep -f "$AGENT_PATH" >/dev/null 2>&1; then
|
||||
SERVICE_RUNNING=true
|
||||
log_success "Agent process is running (rc.local background task)"
|
||||
else
|
||||
log_warn "Agent process not detected; check $LINUX_LOG_FILE for errors"
|
||||
fi
|
||||
else
|
||||
log_info "Skipping automated service validation – start the agent manually using the commands above."
|
||||
fi
|
||||
|
||||
if [[ "$SERVICE_RUNNING" == true ]]; then
|
||||
VALIDATION_SUCCESS=true
|
||||
fi
|
||||
|
||||
# Try to verify with API endpoint that agent is reporting
|
||||
if [[ "$SERVICE_MODE" != "manual" && "$SERVICE_RUNNING" == true ]]; then
|
||||
log_info "Verifying agent registration with Pulse server..."
|
||||
|
||||
# Get hostname for verification
|
||||
HOSTNAME=$(hostname)
|
||||
|
||||
# Try to query the API for this host
|
||||
if command -v curl &> /dev/null; then
|
||||
API_CHECK=$(curl -fsSL "$PULSE_URL/api/hosts" 2>/dev/null || echo "")
|
||||
elif command -v wget &> /dev/null; then
|
||||
API_CHECK=$(wget -qO- "$PULSE_URL/api/hosts" 2>/dev/null || echo "")
|
||||
fi
|
||||
if [[ -z "$PULSE_TOKEN" ]]; then
|
||||
log_info "Registration check skipped (no API token available for lookup)."
|
||||
elif command -v curl &> /dev/null; then
|
||||
log_info "Verifying agent registration with Pulse server..."
|
||||
LOOKUP_RESPONSE=$(curl -fsSL \
|
||||
-H "Authorization: Bearer $PULSE_TOKEN" \
|
||||
--get \
|
||||
--data-urlencode "hostname=$HOSTNAME" \
|
||||
"$PULSE_URL/api/agents/host/lookup" 2>/dev/null || true)
|
||||
|
||||
if [[ -n "$API_CHECK" ]] && echo "$API_CHECK" | grep -q "$HOSTNAME"; then
|
||||
VALIDATION_SUCCESS=true
|
||||
log_success "Agent successfully registered with Pulse server!"
|
||||
log_success "Your host '$HOSTNAME' is now reporting metrics!"
|
||||
elif [[ -n "$API_CHECK" ]]; then
|
||||
log_warn "Agent is running but host not found in API yet (may take a few moments)"
|
||||
log_info "Service appears healthy, metrics should appear shortly."
|
||||
VALIDATION_SUCCESS=true # Service is running, so count as success
|
||||
if [[ "$LOOKUP_RESPONSE" == *'"success":true'* ]]; then
|
||||
host_status=$(printf '%s' "$LOOKUP_RESPONSE" | sed -n 's/.*"status":"\([^"]*\)".*/\1/p')
|
||||
last_seen=$(printf '%s' "$LOOKUP_RESPONSE" | sed -n 's/.*"lastSeen":"\([^"]*\)".*/\1/p')
|
||||
log_success "Agent successfully registered with Pulse server!"
|
||||
if [[ -n "$host_status" ]]; then
|
||||
log_info "Pulse reports status: $host_status (last seen $last_seen)"
|
||||
fi
|
||||
else
|
||||
log_warn "Agent lookup did not confirm registration yet (response: ${LOOKUP_RESPONSE:-no data})."
|
||||
log_info "Service is running; metrics should appear shortly."
|
||||
fi
|
||||
else
|
||||
log_warn "Could not verify registration via API (endpoint may not be accessible)"
|
||||
log_info "Service is running, check your Pulse dashboard manually."
|
||||
VALIDATION_SUCCESS=true # Service is running, so count as success
|
||||
log_info "Registration check skipped (curl is required for API validation)."
|
||||
fi
|
||||
fi
|
||||
|
||||
|
|
@ -657,11 +803,7 @@ if [[ "$SERVICE_MODE" == "manual" ]]; then
|
|||
log_warn "Service validation requires starting the agent manually."
|
||||
log_info "Run the following to launch the agent in the background:"
|
||||
log_info " $MANUAL_START_WRAPPED"
|
||||
if [[ "$UNRAID" == true ]]; then
|
||||
log_info "Add the same line to /boot/config/go to auto-start on boot."
|
||||
else
|
||||
log_info "Add the same line to /etc/rc.local (or equivalent) to auto-start on boot."
|
||||
fi
|
||||
log_info "Add the same line to /etc/rc.local (or equivalent) to auto-start on boot."
|
||||
elif [[ "$VALIDATION_SUCCESS" == true ]]; then
|
||||
log_info "Check your Pulse dashboard at: $PULSE_URL"
|
||||
else
|
||||
|
|
@ -677,13 +819,17 @@ else
|
|||
echo " View logs: tail -f $MACOS_LOG_FILE"
|
||||
echo " Check status: launchctl list | grep pulse"
|
||||
echo " Restart: launchctl unload $LAUNCHD_PLIST && launchctl load $LAUNCHD_PLIST"
|
||||
elif [[ "$SERVICE_MODE" == "unraid" ]]; then
|
||||
echo " Logs: tail -f $LINUX_LOG_FILE"
|
||||
echo " Restart: sudo pkill -f $AGENT_PATH && $MANUAL_START_WRAPPED"
|
||||
echo " Persist: Ensure the startup line exists in $UNRAID_GO_FILE"
|
||||
elif [[ "$SERVICE_MODE" == "rc_local" ]]; then
|
||||
echo " Logs: tail -f $LINUX_LOG_FILE"
|
||||
echo " Restart: sudo pkill -f $AGENT_PATH && $MANUAL_START_WRAPPED"
|
||||
echo " Persist: Ensure the startup block exists in $RC_LOCAL_PATH"
|
||||
else
|
||||
echo " Start agent: $MANUAL_START_WRAPPED"
|
||||
if [[ "$UNRAID" == true ]]; then
|
||||
echo " Persist: Add the wrapped command to /boot/config/go"
|
||||
else
|
||||
echo " Persist: Add the wrapped command to /etc/rc.local (or equivalent)"
|
||||
fi
|
||||
echo " Persist: Add the wrapped command to /etc/rc.local (or equivalent)"
|
||||
fi
|
||||
echo ""
|
||||
echo " Manual run: $MANUAL_START_CMD"
|
||||
|
|
@ -705,13 +851,21 @@ elif [[ "$SERVICE_MODE" == "launchd" ]]; then
|
|||
echo " Restart: launchctl unload $LAUNCHD_PLIST && launchctl load $LAUNCHD_PLIST"
|
||||
echo " Status: launchctl list | grep pulse"
|
||||
echo " Logs: tail -f $MACOS_LOG_FILE"
|
||||
elif [[ "$SERVICE_MODE" == "unraid" ]]; then
|
||||
echo " Start: $MANUAL_START_WRAPPED"
|
||||
echo " Stop: sudo pkill -f $AGENT_PATH"
|
||||
echo " Restart: sudo pkill -f $AGENT_PATH && $MANUAL_START_WRAPPED"
|
||||
echo " Logs: tail -f $LINUX_LOG_FILE"
|
||||
echo " Persist: Stored in $UNRAID_GO_FILE"
|
||||
elif [[ "$SERVICE_MODE" == "rc_local" ]]; then
|
||||
echo " Start: $MANUAL_START_WRAPPED"
|
||||
echo " Stop: sudo pkill -f $AGENT_PATH"
|
||||
echo " Restart: sudo pkill -f $AGENT_PATH && $MANUAL_START_WRAPPED"
|
||||
echo " Logs: tail -f $LINUX_LOG_FILE"
|
||||
echo " Persist: Stored in $RC_LOCAL_PATH"
|
||||
else
|
||||
echo " Start: $MANUAL_START_WRAPPED"
|
||||
if [[ "$UNRAID" == true ]]; then
|
||||
echo " Persist: Add the wrapped command to /boot/config/go so it starts on boot"
|
||||
else
|
||||
echo " Persist: Add the wrapped command to /etc/rc.local (or similar) to start on boot"
|
||||
fi
|
||||
echo " Persist: Add the wrapped command to /etc/rc.local (or similar) to start on boot"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { State, Performance, Stats, DockerHostCommand } from '@/types/api';
|
||||
import type { State, Performance, Stats, DockerHostCommand, HostLookupResponse } from '@/types/api';
|
||||
import { apiFetch, apiFetchJSON } from '@/utils/apiClient';
|
||||
|
||||
export class MonitoringAPI {
|
||||
|
|
@ -147,6 +147,56 @@ export class MonitoringAPI {
|
|||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
static async lookupHost(params: { id?: string; hostname?: string }): Promise<HostLookupResponse | null> {
|
||||
const search = new URLSearchParams();
|
||||
if (params.id) search.set('id', params.id);
|
||||
if (params.hostname) search.set('hostname', params.hostname);
|
||||
|
||||
if (!search.toString()) {
|
||||
throw new Error('Provide a host identifier or hostname to look up.');
|
||||
}
|
||||
|
||||
const url = `${this.baseUrl}/agents/host/lookup?${search.toString()}`;
|
||||
const response = await apiFetch(url);
|
||||
|
||||
if (response.status === 404) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
let message = text?.trim() || `Lookup failed with status ${response.status}`;
|
||||
try {
|
||||
const parsed = text ? JSON.parse(text) : null;
|
||||
if (parsed?.error) {
|
||||
message = parsed.error;
|
||||
}
|
||||
} catch (_err) {
|
||||
// ignore parse error
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
if (!text?.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = JSON.parse(text) as HostLookupResponse;
|
||||
const lastSeen = data?.host?.lastSeen as unknown;
|
||||
if (typeof lastSeen === 'string') {
|
||||
const parsed = Date.parse(lastSeen);
|
||||
data.host.lastSeen = Number.isFinite(parsed) ? parsed : Date.now();
|
||||
} else if (typeof lastSeen === 'number') {
|
||||
// assume already a timestamp
|
||||
data.host.lastSeen = lastSeen;
|
||||
} else {
|
||||
data.host.lastSeen = Date.now();
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
export interface DeleteDockerHostResponse {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { type Component, For, Show, createEffect, createMemo, createSignal, onMount } from 'solid-js';
|
||||
import { type Component, For, Show, createEffect, createMemo, createSignal, onCleanup, onMount } from 'solid-js';
|
||||
import type { JSX } from 'solid-js';
|
||||
import { useWebSocket } from '@/App';
|
||||
import type { Host } from '@/types/api';
|
||||
import type { Host, HostLookupResponse } from '@/types/api';
|
||||
import { Card } from '@/components/shared/Card';
|
||||
import { formatBytes, formatRelativeTime, formatUptime, formatAbsoluteTime } from '@/utils/format';
|
||||
import { notificationStore } from '@/stores/notifications';
|
||||
|
|
@ -9,6 +9,7 @@ import { HOST_AGENT_SCOPE } from '@/constants/apiScopes';
|
|||
import type { SecurityStatus } from '@/types/config';
|
||||
import type { APITokenRecord } from '@/api/security';
|
||||
import { SecurityAPI } from '@/api/security';
|
||||
import { MonitoringAPI } from '@/api/monitoring';
|
||||
|
||||
const TOKEN_PLACEHOLDER = '<api-token>';
|
||||
const pulseUrl = () => {
|
||||
|
|
@ -104,6 +105,12 @@ export const HostAgents: Component = () => {
|
|||
const [confirmedNoToken, setConfirmedNoToken] = createSignal(false);
|
||||
const [currentToken, setCurrentToken] = createSignal<string | null>(null);
|
||||
const [isGeneratingToken, setIsGeneratingToken] = createSignal(false);
|
||||
const [lookupValue, setLookupValue] = createSignal('');
|
||||
const [lookupResult, setLookupResult] = createSignal<HostLookupResponse | null>(null);
|
||||
const [lookupError, setLookupError] = createSignal<string | null>(null);
|
||||
const [lookupLoading, setLookupLoading] = createSignal(false);
|
||||
const [highlightedHostId, setHighlightedHostId] = createSignal<string | null>(null);
|
||||
let highlightTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
createEffect(() => {
|
||||
if (requiresToken()) {
|
||||
|
|
@ -133,6 +140,62 @@ export const HostAgents: Component = () => {
|
|||
})),
|
||||
);
|
||||
|
||||
const connectedFromStatus = (status: string | undefined | null) => {
|
||||
if (!status) return false;
|
||||
const value = status.toLowerCase();
|
||||
return value === 'online' || value === 'running' || value === 'healthy';
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
const current = lookupResult();
|
||||
if (!current) return;
|
||||
|
||||
const targetId = current.host.id;
|
||||
const targetHostname = current.host.hostname;
|
||||
const hosts = allHosts();
|
||||
const match = hosts.find((host) => host.id === targetId || host.hostname === targetHostname);
|
||||
if (!match) return;
|
||||
|
||||
if (highlightTimer) {
|
||||
clearTimeout(highlightTimer);
|
||||
highlightTimer = null;
|
||||
}
|
||||
|
||||
setHighlightedHostId(match.id);
|
||||
highlightTimer = setTimeout(() => {
|
||||
setHighlightedHostId(null);
|
||||
highlightTimer = null;
|
||||
}, 10_000);
|
||||
|
||||
const updated = {
|
||||
success: true,
|
||||
host: {
|
||||
id: match.id,
|
||||
hostname: match.hostname,
|
||||
displayName: match.displayName,
|
||||
status: match.status,
|
||||
connected: connectedFromStatus(match.status),
|
||||
lastSeen: match.lastSeen ?? Date.now(),
|
||||
agentVersion: match.agentVersion ?? current.host.agentVersion,
|
||||
},
|
||||
} satisfies HostLookupResponse;
|
||||
|
||||
const currentHost = current.host;
|
||||
if (
|
||||
currentHost.status === updated.host.status &&
|
||||
currentHost.connected === updated.host.connected &&
|
||||
currentHost.lastSeen === updated.host.lastSeen &&
|
||||
currentHost.agentVersion === updated.host.agentVersion &&
|
||||
(currentHost.displayName || '') === (updated.host.displayName || '') &&
|
||||
currentHost.hostname === updated.host.hostname
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLookupResult(updated);
|
||||
setLookupError(null);
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
|
|
@ -156,13 +219,20 @@ export const HostAgents: Component = () => {
|
|||
});
|
||||
|
||||
|
||||
const requiresToken = () => {
|
||||
const status = securityStatus();
|
||||
if (status) {
|
||||
return status.requiresAuth || status.apiTokenConfigured;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const requiresToken = () => {
|
||||
const status = securityStatus();
|
||||
if (status) {
|
||||
return status.requiresAuth || status.apiTokenConfigured;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
onCleanup(() => {
|
||||
if (highlightTimer) {
|
||||
clearTimeout(highlightTimer);
|
||||
highlightTimer = null;
|
||||
}
|
||||
});
|
||||
|
||||
const hasToken = () => Boolean(currentToken());
|
||||
const commandsUnlocked = () => (requiresToken() ? hasToken() : hasToken() || confirmedNoToken());
|
||||
|
|
@ -234,6 +304,35 @@ export const HostAgents: Component = () => {
|
|||
return currentToken() || 'disabled';
|
||||
};
|
||||
|
||||
const handleLookup = async () => {
|
||||
const query = lookupValue().trim();
|
||||
setLookupError(null);
|
||||
|
||||
if (!query) {
|
||||
setLookupResult(null);
|
||||
setLookupError('Enter a hostname or host ID to check.');
|
||||
return;
|
||||
}
|
||||
|
||||
setLookupLoading(true);
|
||||
try {
|
||||
const result = await MonitoringAPI.lookupHost({ id: query, hostname: query });
|
||||
if (!result) {
|
||||
setLookupResult(null);
|
||||
setLookupError(`No host has reported with "${query}" yet. Try again in a few seconds.`);
|
||||
} else {
|
||||
setLookupResult(result);
|
||||
setLookupError(null);
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Host lookup failed.';
|
||||
setLookupResult(null);
|
||||
setLookupError(message);
|
||||
} finally {
|
||||
setLookupLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getSystemdServiceUnit = () => `[Unit]
|
||||
Description=Pulse Host Agent
|
||||
After=network-online.target
|
||||
|
|
@ -387,6 +486,78 @@ sudo systemctl daemon-reload`;
|
|||
)}
|
||||
</For>
|
||||
</div>
|
||||
<div class="space-y-3 rounded-lg border border-blue-200 bg-blue-50 px-4 py-3 text-sm text-blue-900 dark:border-blue-800 dark:bg-blue-900/20 dark:text-blue-100">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<h5 class="text-sm font-semibold">Check installation status</h5>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLookup}
|
||||
disabled={lookupLoading()}
|
||||
class="rounded-lg bg-blue-600 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
{lookupLoading() ? 'Checking…' : 'Check status'}
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-blue-800 dark:text-blue-200">
|
||||
Enter the hostname (or host ID) from the machine you just installed. Pulse returns the latest status instantly.
|
||||
</p>
|
||||
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
|
||||
<input
|
||||
type="text"
|
||||
value={lookupValue()}
|
||||
onInput={(event) => {
|
||||
setLookupValue(event.currentTarget.value);
|
||||
setLookupError(null);
|
||||
setLookupResult(null);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
void handleLookup();
|
||||
}
|
||||
}}
|
||||
placeholder="Hostname or host ID"
|
||||
class="flex-1 rounded-lg border border-blue-200 bg-white px-3 py-2 text-sm text-blue-900 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:border-blue-700 dark:bg-blue-900 dark:text-blue-100 dark:focus:border-blue-300 dark:focus:ring-blue-800/60"
|
||||
/>
|
||||
</div>
|
||||
<Show when={lookupError()}>
|
||||
<p class="text-xs font-medium text-red-600 dark:text-red-300">{lookupError()}</p>
|
||||
</Show>
|
||||
<Show when={lookupResult()}>
|
||||
{(result) => {
|
||||
const host = () => result().host;
|
||||
const statusBadgeClasses = () =>
|
||||
host().connected
|
||||
? 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300'
|
||||
: 'bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-200';
|
||||
return (
|
||||
<div class="space-y-1 rounded-lg border border-blue-200 bg-white px-3 py-2 text-xs text-blue-900 dark:border-blue-700 dark:bg-blue-900/40 dark:text-blue-100">
|
||||
<div class="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="text-sm font-semibold">
|
||||
{host().displayName || host().hostname}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class={`inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-semibold ${statusBadgeClasses()}`}>
|
||||
{host().connected ? 'Connected' : 'Not reporting yet'}
|
||||
</span>
|
||||
<span class="inline-flex items-center rounded-full bg-blue-100 px-2 py-0.5 text-[11px] font-medium text-blue-700 dark:bg-blue-900/60 dark:text-blue-200">
|
||||
{host().status || 'unknown'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
Last seen {formatRelativeTime(host().lastSeen)} ({formatAbsoluteTime(host().lastSeen)})
|
||||
</div>
|
||||
<Show when={host().agentVersion}>
|
||||
<div class="text-xs text-blue-700 dark:text-blue-200">
|
||||
Agent version {host().agentVersion}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Show>
|
||||
</div>
|
||||
<details class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700 dark:border-gray-700 dark:bg-gray-800/50 dark:text-gray-300">
|
||||
<summary class="cursor-pointer text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
Advanced options (manual install & uninstall)
|
||||
|
|
@ -542,6 +713,7 @@ sudo systemctl daemon-reload`;
|
|||
status === 'online' ||
|
||||
status === 'running' ||
|
||||
status === 'healthy';
|
||||
const isHighlighted = highlightedHostId() === host.id;
|
||||
|
||||
const baseRowClass = isStale
|
||||
? 'bg-gray-50 dark:bg-gray-800/50 opacity-60'
|
||||
|
|
@ -549,6 +721,9 @@ sudo systemctl daemon-reload`;
|
|||
|
||||
const rowClass =
|
||||
tokenRevoked && !isStale ? `${baseRowClass} opacity-60` : baseRowClass;
|
||||
const highlightClass = isHighlighted
|
||||
? 'ring-2 ring-blue-500/70 dark:ring-blue-400/70 shadow-md'
|
||||
: '';
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!confirm(`Remove host "${host.displayName || host.hostname || host.id}"?\n\nThis will remove the host from Pulse monitoring. The host agent will re-register if it continues to report.`)) {
|
||||
|
|
@ -580,7 +755,7 @@ sudo systemctl daemon-reload`;
|
|||
};
|
||||
|
||||
return (
|
||||
<tr class={rowClass}>
|
||||
<tr class={`${rowClass} ${highlightClass}`}>
|
||||
<td class="py-3 px-4">
|
||||
<div class="font-medium text-gray-900 dark:text-gray-100">
|
||||
{host.displayName || host.hostname || host.id}
|
||||
|
|
|
|||
|
|
@ -241,6 +241,19 @@ export interface HostSensorSummary {
|
|||
additional?: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface HostLookupResponse {
|
||||
success: boolean;
|
||||
host: {
|
||||
id: string;
|
||||
hostname: string;
|
||||
displayName?: string;
|
||||
status: string;
|
||||
connected: boolean;
|
||||
lastSeen: number;
|
||||
agentVersion?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ReplicationJob {
|
||||
id: string;
|
||||
instance: string;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/utils"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/websocket"
|
||||
|
|
@ -78,6 +79,81 @@ func (h *HostAgentHandlers) HandleReport(w http.ResponseWriter, r *http.Request)
|
|||
}
|
||||
}
|
||||
|
||||
// HandleLookup returns host registration details for installer validation.
|
||||
func (h *HostAgentHandlers) HandleLookup(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeErrorResponse(w, http.StatusMethodNotAllowed, "method_not_allowed", "Only GET is allowed", nil)
|
||||
return
|
||||
}
|
||||
|
||||
id := strings.TrimSpace(r.URL.Query().Get("id"))
|
||||
hostname := strings.TrimSpace(r.URL.Query().Get("hostname"))
|
||||
|
||||
if id == "" && hostname == "" {
|
||||
writeErrorResponse(w, http.StatusBadRequest, "missing_lookup_param", "Provide either id or hostname to look up a host", nil)
|
||||
return
|
||||
}
|
||||
|
||||
state := h.monitor.GetState()
|
||||
|
||||
var (
|
||||
host models.Host
|
||||
found bool
|
||||
)
|
||||
|
||||
if id != "" {
|
||||
for _, candidate := range state.Hosts {
|
||||
if candidate.ID == id {
|
||||
host = candidate
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found && hostname != "" {
|
||||
for _, candidate := range state.Hosts {
|
||||
if strings.EqualFold(candidate.Hostname, hostname) || strings.EqualFold(candidate.DisplayName, hostname) {
|
||||
host = candidate
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
writeErrorResponse(w, http.StatusNotFound, "host_not_found", "Host has not registered with Pulse yet", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure the querying token matches the host (when applicable).
|
||||
if record := getAPITokenRecordFromRequest(r); record != nil && host.TokenID != "" && host.TokenID != record.ID {
|
||||
writeErrorResponse(w, http.StatusForbidden, "host_lookup_forbidden", "Host does not belong to this API token", nil)
|
||||
return
|
||||
}
|
||||
|
||||
connected := strings.EqualFold(host.Status, "online") ||
|
||||
strings.EqualFold(host.Status, "running") ||
|
||||
strings.EqualFold(host.Status, "healthy")
|
||||
|
||||
resp := map[string]any{
|
||||
"success": true,
|
||||
"host": map[string]any{
|
||||
"id": host.ID,
|
||||
"hostname": host.Hostname,
|
||||
"displayName": host.DisplayName,
|
||||
"status": host.Status,
|
||||
"connected": connected,
|
||||
"lastSeen": host.LastSeen,
|
||||
"agentVersion": host.AgentVersion,
|
||||
},
|
||||
}
|
||||
|
||||
if err := utils.WriteJSONResponse(w, resp); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to serialize host lookup response")
|
||||
}
|
||||
}
|
||||
|
||||
// HandleDeleteHost removes a host from the shared state.
|
||||
func (h *HostAgentHandlers) HandleDeleteHost(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete {
|
||||
|
|
|
|||
|
|
@ -132,6 +132,7 @@ func (r *Router) setupRoutes() {
|
|||
r.mux.HandleFunc("/api/state", r.handleState)
|
||||
r.mux.HandleFunc("/api/agents/docker/report", RequireAuth(r.config, RequireScope(config.ScopeDockerReport, r.dockerAgentHandlers.HandleReport)))
|
||||
r.mux.HandleFunc("/api/agents/host/report", RequireAuth(r.config, RequireScope(config.ScopeHostReport, r.hostAgentHandlers.HandleReport)))
|
||||
r.mux.HandleFunc("/api/agents/host/lookup", RequireAuth(r.config, RequireScope(config.ScopeHostReport, r.hostAgentHandlers.HandleLookup)))
|
||||
r.mux.HandleFunc("/api/agents/host/", RequireAdmin(r.config, RequireScope(config.ScopeHostManage, r.hostAgentHandlers.HandleDeleteHost)))
|
||||
r.mux.HandleFunc("/api/agents/docker/commands/", RequireAuth(r.config, RequireScope(config.ScopeDockerManage, r.dockerAgentHandlers.HandleCommandAck)))
|
||||
r.mux.HandleFunc("/api/agents/docker/hosts/", RequireAdmin(r.config, RequireScope(config.ScopeDockerManage, r.dockerAgentHandlers.HandleDockerHostActions)))
|
||||
|
|
|
|||
|
|
@ -34,6 +34,63 @@ function Write-Error { param([string]$msg) Write-Color $Red "✗ $msg" }
|
|||
function Write-Info { param([string]$msg) Write-Color $Blue "ℹ $msg" }
|
||||
function Write-Warning { param([string]$msg) Write-Color $Yellow "⚠ $msg" }
|
||||
|
||||
function Write-InstallerEvent {
|
||||
param(
|
||||
[string]$SourceName,
|
||||
[string]$Message,
|
||||
[ValidateSet('Information', 'Warning', 'Error')] [string]$EntryType = 'Information',
|
||||
[int]$EventId = 1000
|
||||
)
|
||||
|
||||
if (-not $SourceName) { return }
|
||||
|
||||
try {
|
||||
Write-EventLog -LogName Application -Source $SourceName -EventId $EventId -EntryType $EntryType -Message $Message
|
||||
} catch {
|
||||
Write-Warning "Unable to write installer event log entry: $_"
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls13
|
||||
} catch {
|
||||
# Ignore if platform does not expose TLS 1.3
|
||||
}
|
||||
|
||||
function Get-RecentAgentEvents {
|
||||
param(
|
||||
[string]$ProviderName,
|
||||
[int]$Max = 5
|
||||
)
|
||||
try {
|
||||
return Get-WinEvent -FilterHashtable @{ LogName = 'Application'; ProviderName = $ProviderName } -MaxEvents $Max -ErrorAction Stop
|
||||
} catch {
|
||||
return Get-EventLog -LogName Application -Source $ProviderName -Newest $Max -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
function Test-AgentRegistration {
|
||||
param(
|
||||
[string]$PulseUrl,
|
||||
[string]$Hostname,
|
||||
[string]$Token
|
||||
)
|
||||
|
||||
if (-not $Token) {
|
||||
return $null
|
||||
}
|
||||
|
||||
try {
|
||||
$encodedHostname = [System.Uri]::EscapeDataString($Hostname)
|
||||
$lookupUri = "$PulseUrl/api/agents/host/lookup?hostname=$encodedHostname"
|
||||
$headers = @{ Authorization = "Bearer $Token" }
|
||||
$response = Invoke-RestMethod -Uri $lookupUri -Headers $headers -Method Get -ErrorAction Stop
|
||||
return $response.host
|
||||
} catch {
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Color $Blue "═══════════════════════════════════════════════════════════"
|
||||
Write-Color $Blue " Pulse Host Agent - Windows Installation"
|
||||
|
|
@ -89,6 +146,13 @@ try {
|
|||
# Download binary
|
||||
Invoke-WebRequest -Uri $downloadUrl -OutFile $agentPath -UseBasicParsing
|
||||
Write-Success "Downloaded agent to $agentPath"
|
||||
|
||||
$agentArgs = @("--url", "`"$PulseUrl`"", "--interval", $Interval)
|
||||
if ($Token) {
|
||||
$agentArgs += @("--token", "`"$Token`"")
|
||||
}
|
||||
$serviceBinaryPath = "`"$agentPath`" $($agentArgs -join ' ')"
|
||||
$manualCommand = "& `"$agentPath`" $($agentArgs -join ' ')"
|
||||
} catch {
|
||||
Write-Error "Failed to download agent: $_"
|
||||
exit 1
|
||||
|
|
@ -119,17 +183,6 @@ if ($existingService) {
|
|||
if (-not $NoService) {
|
||||
Write-Info "Installing native Windows service with built-in service support..."
|
||||
|
||||
# Build service arguments
|
||||
$serviceArgs = @(
|
||||
"--url", $PulseUrl,
|
||||
"--interval", $Interval
|
||||
)
|
||||
if ($Token) {
|
||||
$serviceArgs += "--token", $Token
|
||||
}
|
||||
|
||||
$serviceBinaryPath = "`"$agentPath`" $($serviceArgs -join ' ')"
|
||||
|
||||
try {
|
||||
if ($existingService) {
|
||||
Write-Info "Removing existing service..."
|
||||
|
|
@ -156,6 +209,8 @@ if (-not $NoService) {
|
|||
Write-Warning "Could not register Event Log source (not critical): $_"
|
||||
}
|
||||
|
||||
Write-InstallerEvent -SourceName $serviceName -Message "Pulse Host Agent installer registered service version $(Get-Item $agentPath).VersionInfo.FileVersion" -EventId 1000
|
||||
|
||||
# Configure service recovery options (restart on failure)
|
||||
sc.exe failure $serviceName reset= 86400 actions= restart/60000/restart/60000/restart/60000 | Out-Null
|
||||
Write-Success "Configured automatic restart on failure"
|
||||
|
|
@ -169,30 +224,62 @@ if (-not $NoService) {
|
|||
if ($status -eq 'Running') {
|
||||
Write-Success "Service started successfully!"
|
||||
|
||||
# Optional: Validate that agent is reporting
|
||||
Write-Info "Waiting 10 seconds to validate agent reporting..."
|
||||
Start-Sleep -Seconds 10
|
||||
|
||||
# Check Event Log for successful startup
|
||||
$recentLogs = Get-EventLog -LogName Application -Source $serviceName -Newest 5 -ErrorAction SilentlyContinue
|
||||
$hasStarted = $recentLogs | Where-Object { $_.Message -like "*started successfully*" }
|
||||
|
||||
if ($hasStarted) {
|
||||
Write-Success "Agent is reporting successfully!"
|
||||
$hostname = $env:COMPUTERNAME
|
||||
$lookupHost = Test-AgentRegistration -PulseUrl $PulseUrl -Hostname $hostname -Token $Token
|
||||
if ($lookupHost) {
|
||||
Write-Success "Agent successfully registered with Pulse (host '$hostname')."
|
||||
if ($lookupHost.status) {
|
||||
$lastSeen = $lookupHost.lastSeen
|
||||
if ($lastSeen -is [DateTime]) {
|
||||
$lastSeen = $lastSeen.ToString("u")
|
||||
}
|
||||
Write-Info ("Pulse reports status: {0} (last seen {1})" -f $lookupHost.status, $lastSeen)
|
||||
}
|
||||
Write-Info "Check your Pulse dashboard - this host should appear shortly."
|
||||
$statusForLog = if ($lookupHost.status) { $lookupHost.status } else { 'unknown' }
|
||||
Write-InstallerEvent -SourceName $serviceName -Message "Installer verified host '$hostname' reporting to Pulse (status: $statusForLog)." -EventId 1010
|
||||
} elseif ($Token) {
|
||||
Write-Warning "Agent is running but the lookup endpoint has not confirmed registration yet."
|
||||
Write-Info "It may take another moment for metrics to appear in the dashboard."
|
||||
Write-InstallerEvent -SourceName $serviceName -Message "Installer could not yet confirm host '$hostname' registration with Pulse." -EntryType Warning -EventId 1011
|
||||
} else {
|
||||
Write-Warning "Agent started but validation incomplete. Check Event Viewer if issues occur."
|
||||
Write-Info "Registration check skipped (no API token available)."
|
||||
Write-InstallerEvent -SourceName $serviceName -Message "Installer skipped registration lookup (no API token provided)." -EventId 1012
|
||||
}
|
||||
|
||||
$recentLogs = Get-RecentAgentEvents -ProviderName $serviceName -Max 5
|
||||
if ($recentLogs) {
|
||||
Write-Info "Recent service events:"
|
||||
$recentLogs | Select-Object -First 3 | ForEach-Object {
|
||||
$time = $_.TimeCreated
|
||||
if (-not $time) { $time = $_.TimeGenerated }
|
||||
Write-Host (" [{0}] {1}" -f $time.ToString("u"), $_.Message)
|
||||
}
|
||||
} else {
|
||||
Write-Warning "No recent Application log entries were found for $serviceName."
|
||||
}
|
||||
} else {
|
||||
Write-Warning "Service status: $status"
|
||||
Write-Info "Checking service logs..."
|
||||
Get-EventLog -LogName Application -Source $serviceName -Newest 5 -ErrorAction SilentlyContinue | Format-List TimeGenerated, Message
|
||||
$recentLogs = Get-RecentAgentEvents -ProviderName $serviceName -Max 5
|
||||
if ($recentLogs) {
|
||||
$recentLogs | ForEach-Object {
|
||||
$time = $_.TimeCreated
|
||||
if (-not $time) { $time = $_.TimeGenerated }
|
||||
Write-Host (" [{0}] {1}" -f $time.ToString("u"), $_.Message)
|
||||
}
|
||||
} else {
|
||||
Write-Warning "No Application log entries were found for $serviceName."
|
||||
}
|
||||
}
|
||||
|
||||
} catch {
|
||||
Write-Error "Failed to create/start service: $_"
|
||||
Write-Info "You can start the agent manually with:"
|
||||
Write-Host " & `"$agentPath`" --url $PulseUrl --interval $Interval $(if ($Token) { '--token ***' })"
|
||||
Write-Host " $manualCommand"
|
||||
Write-Host ""
|
||||
Write-Info "Or check Windows Event Viewer (Application log) for error details."
|
||||
exit 1
|
||||
|
|
@ -201,7 +288,7 @@ if (-not $NoService) {
|
|||
Write-Info "Skipping service installation (--NoService flag)"
|
||||
Write-Host ""
|
||||
Write-Info "To start the agent manually:"
|
||||
Write-Host " & `"$agentPath`" --url $PulseUrl --interval $Interval $(if ($Token) { '--token ***' })"
|
||||
Write-Host " $manualCommand"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
|
|
@ -216,7 +303,7 @@ Write-Host " Stop: Stop-Service -Name PulseHostAgent"
|
|||
Write-Host " Restart: Restart-Service -Name PulseHostAgent"
|
||||
Write-Host " Status: Get-Service -Name PulseHostAgent | Select Status, StartType"
|
||||
Write-Host " Remove: sc.exe delete PulseHostAgent"
|
||||
Write-Host " Logs: Get-EventLog -LogName Application -Source PulseHostAgent -Newest 50"
|
||||
Write-Host " Logs: Get-WinEvent -FilterHashtable @{LogName='Application'; ProviderName='PulseHostAgent'} -MaxEvents 50"
|
||||
Write-Host ""
|
||||
|
||||
Write-Info "Files installed:"
|
||||
|
|
|
|||
|
|
@ -112,8 +112,10 @@ LINUX_LOG_FILE="$LINUX_LOG_DIR/host-agent.log"
|
|||
SERVICE_MODE="manual"
|
||||
MANUAL_START_CMD=""
|
||||
MANUAL_START_WRAPPED=""
|
||||
LAUNCH_IDENTIFIER=""
|
||||
UNRAID=false
|
||||
if [[ -f /etc/unraid-version ]]; then
|
||||
UNRAID_GO_FILE="/boot/config/go"
|
||||
if [[ -f "$UNRAID_GO_FILE" ]] || [[ -f /etc/unraid-version ]]; then
|
||||
UNRAID=true
|
||||
fi
|
||||
|
||||
|
|
@ -365,6 +367,15 @@ sudo mv "$TEMP_BINARY" "$AGENT_PATH"
|
|||
sudo chmod +x "$AGENT_PATH"
|
||||
log_success "Agent binary installed to $AGENT_PATH"
|
||||
|
||||
# Build reusable agent command strings
|
||||
AGENT_CMD="$AGENT_PATH --url $PULSE_URL"
|
||||
if [[ -n "$PULSE_TOKEN" ]]; then
|
||||
AGENT_CMD="$AGENT_CMD --token $PULSE_TOKEN"
|
||||
fi
|
||||
AGENT_CMD="$AGENT_CMD --interval $INTERVAL"
|
||||
MANUAL_START_CMD="$AGENT_CMD"
|
||||
MANUAL_START_WRAPPED="nohup $MANUAL_START_CMD >$LINUX_LOG_FILE 2>&1 &"
|
||||
|
||||
# Set up service based on platform
|
||||
if [[ "$PLATFORM" == "linux" ]] && command -v systemctl &> /dev/null; then
|
||||
log_info "Setting up systemd service..."
|
||||
|
|
@ -379,7 +390,7 @@ After=network.target
|
|||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=$AGENT_PATH --url $PULSE_URL --token $PULSE_TOKEN --interval $INTERVAL
|
||||
ExecStart=$AGENT_CMD
|
||||
Restart=always
|
||||
RestartSec=5s
|
||||
User=root
|
||||
|
|
@ -558,6 +569,7 @@ EOF
|
|||
chmod 600 "$LAUNCHD_PLIST"
|
||||
|
||||
LAUNCH_TARGET="gui/$(id -u)"
|
||||
LAUNCH_IDENTIFIER="$LAUNCH_TARGET/com.pulse.host-agent"
|
||||
|
||||
# Attempt to unload any existing service instance
|
||||
if launchctl bootout "$LAUNCH_TARGET" "$LAUNCHD_PLIST" 2>/dev/null; then
|
||||
|
|
@ -576,23 +588,128 @@ EOF
|
|||
fi
|
||||
SERVICE_MODE="launchd"
|
||||
else
|
||||
log_warn "Automatic service setup not available for this platform"
|
||||
sudo mkdir -p "$LINUX_LOG_DIR"
|
||||
log_info "To run the agent manually:"
|
||||
MANUAL_START_CMD="$AGENT_PATH --url $PULSE_URL --token $PULSE_TOKEN --interval $INTERVAL"
|
||||
MANUAL_START_WRAPPED="nohup $MANUAL_START_CMD >$LINUX_LOG_FILE 2>&1 &"
|
||||
log_info " $MANUAL_START_CMD"
|
||||
log_info ""
|
||||
log_info "To keep the agent running persistently:"
|
||||
log_info " $MANUAL_START_WRAPPED"
|
||||
if [[ "$UNRAID" == true ]]; then
|
||||
log_info ""
|
||||
log_info "On Unraid, add the wrapped command to /boot/config/go so it starts on boot."
|
||||
log_info "Detected Unraid (no systemd). Configuring persistent background service..."
|
||||
|
||||
if pgrep -f "$AGENT_PATH" >/dev/null 2>&1; then
|
||||
log_warn "Existing pulse-host-agent process detected; restarting with new binary"
|
||||
sudo pkill -f "$AGENT_PATH" 2>/dev/null || true
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
log_info "Starting host agent with nohup (logs: $LINUX_LOG_FILE)"
|
||||
if sudo bash -c "$MANUAL_START_WRAPPED"; then
|
||||
log_success "Agent started in the background"
|
||||
else
|
||||
log_error "Failed to start agent automatically. Run manually:"
|
||||
log_info " $MANUAL_START_WRAPPED"
|
||||
fi
|
||||
|
||||
if [[ -f "$UNRAID_GO_FILE" ]]; then
|
||||
if sudo grep -qF -- "$MANUAL_START_WRAPPED" "$UNRAID_GO_FILE"; then
|
||||
log_info "Auto-start entry already present in $UNRAID_GO_FILE"
|
||||
else
|
||||
APPEND_STARTUP=true
|
||||
if [[ "$FORCE" == false ]]; then
|
||||
read -p "Add agent auto-start to $UNRAID_GO_FILE? (Y/n): " ADD_STARTUP_CHOICE
|
||||
if [[ "$ADD_STARTUP_CHOICE" == "n" || "$ADD_STARTUP_CHOICE" == "N" ]]; then
|
||||
APPEND_STARTUP=false
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$APPEND_STARTUP" == true ]]; then
|
||||
if sudo grep -qF "# Pulse Host Agent auto-start" "$UNRAID_GO_FILE"; then
|
||||
log_info "Updating existing auto-start entry in $UNRAID_GO_FILE"
|
||||
sudo sed -i '/# Pulse Host Agent auto-start/,+1d' "$UNRAID_GO_FILE" 2>/dev/null || true
|
||||
fi
|
||||
sudo tee -a "$UNRAID_GO_FILE" > /dev/null <<EOF
|
||||
# Pulse Host Agent auto-start
|
||||
$MANUAL_START_WRAPPED
|
||||
EOF
|
||||
log_success "Added auto-start command to $UNRAID_GO_FILE"
|
||||
else
|
||||
log_info "Skipped modifying $UNRAID_GO_FILE"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
log_warn "Could not find $UNRAID_GO_FILE; skipping persistence step."
|
||||
fi
|
||||
|
||||
log_info "To rerun manually: $MANUAL_START_CMD"
|
||||
SERVICE_MODE="unraid"
|
||||
else
|
||||
log_info ""
|
||||
log_info "On systems without systemd, add the wrapped command to /etc/rc.local (or similar) to start on boot."
|
||||
log_warn "Systemd not available; configuring rc.local-based startup"
|
||||
|
||||
RC_LOCAL_PATH=""
|
||||
for candidate in /etc/rc.local /etc/rc.d/rc.local; do
|
||||
if [[ -f "$candidate" ]]; then
|
||||
RC_LOCAL_PATH="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
CREATE_RC_LOCAL=false
|
||||
if [[ -z "$RC_LOCAL_PATH" ]]; then
|
||||
RC_LOCAL_PATH="/etc/rc.local"
|
||||
CREATE_RC_LOCAL=true
|
||||
fi
|
||||
|
||||
if [[ "$CREATE_RC_LOCAL" == true ]]; then
|
||||
log_info "Creating $RC_LOCAL_PATH"
|
||||
sudo tee "$RC_LOCAL_PATH" > /dev/null <<'EOF'
|
||||
#!/bin/sh
|
||||
# /etc/rc.local - generated by Pulse host agent installer
|
||||
# This script is executed at the end of each multi-user runlevel.
|
||||
exit 0
|
||||
EOF
|
||||
fi
|
||||
|
||||
if [[ -f "$RC_LOCAL_PATH" ]]; then
|
||||
RC_COMMENT="# Pulse Host Agent auto-start"
|
||||
if sudo grep -qF "$MANUAL_START_WRAPPED" "$RC_LOCAL_PATH"; then
|
||||
log_info "Auto-start entry already present in $RC_LOCAL_PATH"
|
||||
else
|
||||
APPEND_RC_LOCAL=true
|
||||
if [[ "$FORCE" == false ]]; then
|
||||
read -p "Add agent auto-start to $RC_LOCAL_PATH? (Y/n): " ADD_RC_CHOICE
|
||||
if [[ "$ADD_RC_CHOICE" == "n" || "$ADD_RC_CHOICE" == "N" ]]; then
|
||||
APPEND_RC_LOCAL=false
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$APPEND_RC_LOCAL" == true ]]; then
|
||||
sudo RC_APPEND_CMD="$MANUAL_START_WRAPPED" RC_COMMENT="$RC_COMMENT" RC_LOCAL_PATH="$RC_LOCAL_PATH" sh -c '
|
||||
tmpfile=$(mktemp)
|
||||
cp "$RC_LOCAL_PATH" "$tmpfile" 2>/dev/null || touch "$tmpfile"
|
||||
sed -i "/$RC_COMMENT/,+1d" "$tmpfile" 2>/dev/null || true
|
||||
sed -i "/^exit 0$/d" "$tmpfile" 2>/dev/null || true
|
||||
printf "\n%s\n%s\n" "$RC_COMMENT" "$RC_APPEND_CMD" >>"$tmpfile"
|
||||
echo "exit 0" >>"$tmpfile"
|
||||
mv "$tmpfile" "$RC_LOCAL_PATH"
|
||||
chmod +x "$RC_LOCAL_PATH"
|
||||
'
|
||||
log_success "Added auto-start command to $RC_LOCAL_PATH"
|
||||
else
|
||||
log_info "Skipped modifying $RC_LOCAL_PATH"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
log_warn "Could not access $RC_LOCAL_PATH; skipping persistence step."
|
||||
fi
|
||||
|
||||
log_info "Starting host agent with nohup (logs: $LINUX_LOG_FILE)"
|
||||
if sudo bash -c "$MANUAL_START_WRAPPED"; then
|
||||
log_success "Agent started in the background"
|
||||
else
|
||||
log_error "Failed to start agent automatically. Run manually:"
|
||||
log_info " $MANUAL_START_WRAPPED"
|
||||
fi
|
||||
|
||||
log_info "To manage manually, edit $RC_LOCAL_PATH or run:"
|
||||
log_info " $MANUAL_START_CMD"
|
||||
SERVICE_MODE="rc_local"
|
||||
fi
|
||||
SERVICE_MODE="manual"
|
||||
fi
|
||||
|
||||
# Validate installation
|
||||
|
|
@ -613,43 +730,72 @@ if [[ "$SERVICE_MODE" == "systemd" ]]; then
|
|||
log_info "Check logs with: sudo journalctl -u pulse-host-agent -n 50"
|
||||
fi
|
||||
elif [[ "$SERVICE_MODE" == "launchd" ]]; then
|
||||
if launchctl list | grep -q "com.pulse.host-agent"; then
|
||||
IDENTIFIER=${LAUNCH_IDENTIFIER:-"gui/$(id -u)/com.pulse.host-agent"}
|
||||
for _ in 1 2 3 4 5; do
|
||||
if launchctl print "$IDENTIFIER" >/dev/null 2>&1; then
|
||||
SERVICE_RUNNING=true
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
if [[ "$SERVICE_RUNNING" == true ]]; then
|
||||
log_success "Service is running successfully!"
|
||||
elif launchctl list | grep -q "com.pulse.host-agent"; then
|
||||
SERVICE_RUNNING=true
|
||||
log_success "Service is running successfully!"
|
||||
else
|
||||
log_warn "Service may not be running properly"
|
||||
log_info "Check logs with: tail -20 $MACOS_LOG_FILE"
|
||||
fi
|
||||
elif [[ "$SERVICE_MODE" == "unraid" ]]; then
|
||||
if pgrep -f "$AGENT_PATH" >/dev/null 2>&1; then
|
||||
SERVICE_RUNNING=true
|
||||
log_success "Agent process is running (nohup background task)"
|
||||
else
|
||||
log_warn "Agent process not detected; check $LINUX_LOG_FILE for errors"
|
||||
fi
|
||||
elif [[ "$SERVICE_MODE" == "rc_local" ]]; then
|
||||
if pgrep -f "$AGENT_PATH" >/dev/null 2>&1; then
|
||||
SERVICE_RUNNING=true
|
||||
log_success "Agent process is running (rc.local background task)"
|
||||
else
|
||||
log_warn "Agent process not detected; check $LINUX_LOG_FILE for errors"
|
||||
fi
|
||||
else
|
||||
log_info "Skipping automated service validation – start the agent manually using the commands above."
|
||||
fi
|
||||
|
||||
if [[ "$SERVICE_RUNNING" == true ]]; then
|
||||
VALIDATION_SUCCESS=true
|
||||
fi
|
||||
|
||||
# Try to verify with API endpoint that agent is reporting
|
||||
if [[ "$SERVICE_MODE" != "manual" && "$SERVICE_RUNNING" == true ]]; then
|
||||
log_info "Verifying agent registration with Pulse server..."
|
||||
|
||||
# Get hostname for verification
|
||||
HOSTNAME=$(hostname)
|
||||
|
||||
# Try to query the API for this host
|
||||
if command -v curl &> /dev/null; then
|
||||
API_CHECK=$(curl -fsSL "$PULSE_URL/api/hosts" 2>/dev/null || echo "")
|
||||
elif command -v wget &> /dev/null; then
|
||||
API_CHECK=$(wget -qO- "$PULSE_URL/api/hosts" 2>/dev/null || echo "")
|
||||
fi
|
||||
if [[ -z "$PULSE_TOKEN" ]]; then
|
||||
log_info "Registration check skipped (no API token available for lookup)."
|
||||
elif command -v curl &> /dev/null; then
|
||||
log_info "Verifying agent registration with Pulse server..."
|
||||
LOOKUP_RESPONSE=$(curl -fsSL \
|
||||
-H "Authorization: Bearer $PULSE_TOKEN" \
|
||||
--get \
|
||||
--data-urlencode "hostname=$HOSTNAME" \
|
||||
"$PULSE_URL/api/agents/host/lookup" 2>/dev/null || true)
|
||||
|
||||
if [[ -n "$API_CHECK" ]] && echo "$API_CHECK" | grep -q "$HOSTNAME"; then
|
||||
VALIDATION_SUCCESS=true
|
||||
log_success "Agent successfully registered with Pulse server!"
|
||||
log_success "Your host '$HOSTNAME' is now reporting metrics!"
|
||||
elif [[ -n "$API_CHECK" ]]; then
|
||||
log_warn "Agent is running but host not found in API yet (may take a few moments)"
|
||||
log_info "Service appears healthy, metrics should appear shortly."
|
||||
VALIDATION_SUCCESS=true # Service is running, so count as success
|
||||
if [[ "$LOOKUP_RESPONSE" == *'"success":true'* ]]; then
|
||||
host_status=$(printf '%s' "$LOOKUP_RESPONSE" | sed -n 's/.*"status":"\([^"]*\)".*/\1/p')
|
||||
last_seen=$(printf '%s' "$LOOKUP_RESPONSE" | sed -n 's/.*"lastSeen":"\([^"]*\)".*/\1/p')
|
||||
log_success "Agent successfully registered with Pulse server!"
|
||||
if [[ -n "$host_status" ]]; then
|
||||
log_info "Pulse reports status: $host_status (last seen $last_seen)"
|
||||
fi
|
||||
else
|
||||
log_warn "Agent lookup did not confirm registration yet (response: ${LOOKUP_RESPONSE:-no data})."
|
||||
log_info "Service is running; metrics should appear shortly."
|
||||
fi
|
||||
else
|
||||
log_warn "Could not verify registration via API (endpoint may not be accessible)"
|
||||
log_info "Service is running, check your Pulse dashboard manually."
|
||||
VALIDATION_SUCCESS=true # Service is running, so count as success
|
||||
log_info "Registration check skipped (curl is required for API validation)."
|
||||
fi
|
||||
fi
|
||||
|
||||
|
|
@ -657,11 +803,7 @@ if [[ "$SERVICE_MODE" == "manual" ]]; then
|
|||
log_warn "Service validation requires starting the agent manually."
|
||||
log_info "Run the following to launch the agent in the background:"
|
||||
log_info " $MANUAL_START_WRAPPED"
|
||||
if [[ "$UNRAID" == true ]]; then
|
||||
log_info "Add the same line to /boot/config/go to auto-start on boot."
|
||||
else
|
||||
log_info "Add the same line to /etc/rc.local (or equivalent) to auto-start on boot."
|
||||
fi
|
||||
log_info "Add the same line to /etc/rc.local (or equivalent) to auto-start on boot."
|
||||
elif [[ "$VALIDATION_SUCCESS" == true ]]; then
|
||||
log_info "Check your Pulse dashboard at: $PULSE_URL"
|
||||
else
|
||||
|
|
@ -677,13 +819,17 @@ else
|
|||
echo " View logs: tail -f $MACOS_LOG_FILE"
|
||||
echo " Check status: launchctl list | grep pulse"
|
||||
echo " Restart: launchctl unload $LAUNCHD_PLIST && launchctl load $LAUNCHD_PLIST"
|
||||
elif [[ "$SERVICE_MODE" == "unraid" ]]; then
|
||||
echo " Logs: tail -f $LINUX_LOG_FILE"
|
||||
echo " Restart: sudo pkill -f $AGENT_PATH && $MANUAL_START_WRAPPED"
|
||||
echo " Persist: Ensure the startup line exists in $UNRAID_GO_FILE"
|
||||
elif [[ "$SERVICE_MODE" == "rc_local" ]]; then
|
||||
echo " Logs: tail -f $LINUX_LOG_FILE"
|
||||
echo " Restart: sudo pkill -f $AGENT_PATH && $MANUAL_START_WRAPPED"
|
||||
echo " Persist: Ensure the startup block exists in $RC_LOCAL_PATH"
|
||||
else
|
||||
echo " Start agent: $MANUAL_START_WRAPPED"
|
||||
if [[ "$UNRAID" == true ]]; then
|
||||
echo " Persist: Add the wrapped command to /boot/config/go"
|
||||
else
|
||||
echo " Persist: Add the wrapped command to /etc/rc.local (or equivalent)"
|
||||
fi
|
||||
echo " Persist: Add the wrapped command to /etc/rc.local (or equivalent)"
|
||||
fi
|
||||
echo ""
|
||||
echo " Manual run: $MANUAL_START_CMD"
|
||||
|
|
@ -705,13 +851,21 @@ elif [[ "$SERVICE_MODE" == "launchd" ]]; then
|
|||
echo " Restart: launchctl unload $LAUNCHD_PLIST && launchctl load $LAUNCHD_PLIST"
|
||||
echo " Status: launchctl list | grep pulse"
|
||||
echo " Logs: tail -f $MACOS_LOG_FILE"
|
||||
elif [[ "$SERVICE_MODE" == "unraid" ]]; then
|
||||
echo " Start: $MANUAL_START_WRAPPED"
|
||||
echo " Stop: sudo pkill -f $AGENT_PATH"
|
||||
echo " Restart: sudo pkill -f $AGENT_PATH && $MANUAL_START_WRAPPED"
|
||||
echo " Logs: tail -f $LINUX_LOG_FILE"
|
||||
echo " Persist: Stored in $UNRAID_GO_FILE"
|
||||
elif [[ "$SERVICE_MODE" == "rc_local" ]]; then
|
||||
echo " Start: $MANUAL_START_WRAPPED"
|
||||
echo " Stop: sudo pkill -f $AGENT_PATH"
|
||||
echo " Restart: sudo pkill -f $AGENT_PATH && $MANUAL_START_WRAPPED"
|
||||
echo " Logs: tail -f $LINUX_LOG_FILE"
|
||||
echo " Persist: Stored in $RC_LOCAL_PATH"
|
||||
else
|
||||
echo " Start: $MANUAL_START_WRAPPED"
|
||||
if [[ "$UNRAID" == true ]]; then
|
||||
echo " Persist: Add the wrapped command to /boot/config/go so it starts on boot"
|
||||
else
|
||||
echo " Persist: Add the wrapped command to /etc/rc.local (or similar) to start on boot"
|
||||
fi
|
||||
echo " Persist: Add the wrapped command to /etc/rc.local (or similar) to start on boot"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue