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.
|
> 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
|
```bash
|
||||||
sudo curl -fsSL https://github.com/rcourtman/Pulse/releases/latest/download/pulse-host-agent-linux-amd64 \
|
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`.
|
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
|
```bash
|
||||||
sudo curl -fsSL https://github.com/rcourtman/Pulse/releases/latest/download/pulse-host-agent-darwin-arm64 \
|
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`.
|
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
|
## 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-Info { param([string]$msg) Write-Color $Blue "ℹ $msg" }
|
||||||
function Write-Warning { param([string]$msg) Write-Color $Yellow "⚠ $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-Host ""
|
||||||
Write-Color $Blue "═══════════════════════════════════════════════════════════"
|
Write-Color $Blue "═══════════════════════════════════════════════════════════"
|
||||||
Write-Color $Blue " Pulse Host Agent - Windows Installation"
|
Write-Color $Blue " Pulse Host Agent - Windows Installation"
|
||||||
|
|
@ -89,6 +146,13 @@ try {
|
||||||
# Download binary
|
# Download binary
|
||||||
Invoke-WebRequest -Uri $downloadUrl -OutFile $agentPath -UseBasicParsing
|
Invoke-WebRequest -Uri $downloadUrl -OutFile $agentPath -UseBasicParsing
|
||||||
Write-Success "Downloaded agent to $agentPath"
|
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 {
|
} catch {
|
||||||
Write-Error "Failed to download agent: $_"
|
Write-Error "Failed to download agent: $_"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
@ -119,17 +183,6 @@ if ($existingService) {
|
||||||
if (-not $NoService) {
|
if (-not $NoService) {
|
||||||
Write-Info "Installing native Windows service with built-in service support..."
|
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 {
|
try {
|
||||||
if ($existingService) {
|
if ($existingService) {
|
||||||
Write-Info "Removing existing service..."
|
Write-Info "Removing existing service..."
|
||||||
|
|
@ -156,6 +209,8 @@ if (-not $NoService) {
|
||||||
Write-Warning "Could not register Event Log source (not critical): $_"
|
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)
|
# Configure service recovery options (restart on failure)
|
||||||
sc.exe failure $serviceName reset= 86400 actions= restart/60000/restart/60000/restart/60000 | Out-Null
|
sc.exe failure $serviceName reset= 86400 actions= restart/60000/restart/60000/restart/60000 | Out-Null
|
||||||
Write-Success "Configured automatic restart on failure"
|
Write-Success "Configured automatic restart on failure"
|
||||||
|
|
@ -169,30 +224,62 @@ if (-not $NoService) {
|
||||||
if ($status -eq 'Running') {
|
if ($status -eq 'Running') {
|
||||||
Write-Success "Service started successfully!"
|
Write-Success "Service started successfully!"
|
||||||
|
|
||||||
# Optional: Validate that agent is reporting
|
|
||||||
Write-Info "Waiting 10 seconds to validate agent reporting..."
|
Write-Info "Waiting 10 seconds to validate agent reporting..."
|
||||||
Start-Sleep -Seconds 10
|
Start-Sleep -Seconds 10
|
||||||
|
|
||||||
# Check Event Log for successful startup
|
$hostname = $env:COMPUTERNAME
|
||||||
$recentLogs = Get-EventLog -LogName Application -Source $serviceName -Newest 5 -ErrorAction SilentlyContinue
|
$lookupHost = Test-AgentRegistration -PulseUrl $PulseUrl -Hostname $hostname -Token $Token
|
||||||
$hasStarted = $recentLogs | Where-Object { $_.Message -like "*started successfully*" }
|
if ($lookupHost) {
|
||||||
|
Write-Success "Agent successfully registered with Pulse (host '$hostname')."
|
||||||
if ($hasStarted) {
|
if ($lookupHost.status) {
|
||||||
Write-Success "Agent is reporting successfully!"
|
$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."
|
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 {
|
} 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 {
|
} else {
|
||||||
Write-Warning "Service status: $status"
|
Write-Warning "Service status: $status"
|
||||||
Write-Info "Checking service logs..."
|
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 {
|
} catch {
|
||||||
Write-Error "Failed to create/start service: $_"
|
Write-Error "Failed to create/start service: $_"
|
||||||
Write-Info "You can start the agent manually with:"
|
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-Host ""
|
||||||
Write-Info "Or check Windows Event Viewer (Application log) for error details."
|
Write-Info "Or check Windows Event Viewer (Application log) for error details."
|
||||||
exit 1
|
exit 1
|
||||||
|
|
@ -201,7 +288,7 @@ if (-not $NoService) {
|
||||||
Write-Info "Skipping service installation (--NoService flag)"
|
Write-Info "Skipping service installation (--NoService flag)"
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Info "To start the agent manually:"
|
Write-Info "To start the agent manually:"
|
||||||
Write-Host " & `"$agentPath`" --url $PulseUrl --interval $Interval $(if ($Token) { '--token ***' })"
|
Write-Host " $manualCommand"
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
|
|
@ -216,7 +303,7 @@ Write-Host " Stop: Stop-Service -Name PulseHostAgent"
|
||||||
Write-Host " Restart: Restart-Service -Name PulseHostAgent"
|
Write-Host " Restart: Restart-Service -Name PulseHostAgent"
|
||||||
Write-Host " Status: Get-Service -Name PulseHostAgent | Select Status, StartType"
|
Write-Host " Status: Get-Service -Name PulseHostAgent | Select Status, StartType"
|
||||||
Write-Host " Remove: sc.exe delete PulseHostAgent"
|
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-Host ""
|
||||||
|
|
||||||
Write-Info "Files installed:"
|
Write-Info "Files installed:"
|
||||||
|
|
|
||||||
|
|
@ -112,8 +112,10 @@ LINUX_LOG_FILE="$LINUX_LOG_DIR/host-agent.log"
|
||||||
SERVICE_MODE="manual"
|
SERVICE_MODE="manual"
|
||||||
MANUAL_START_CMD=""
|
MANUAL_START_CMD=""
|
||||||
MANUAL_START_WRAPPED=""
|
MANUAL_START_WRAPPED=""
|
||||||
|
LAUNCH_IDENTIFIER=""
|
||||||
UNRAID=false
|
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
|
UNRAID=true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -365,6 +367,15 @@ sudo mv "$TEMP_BINARY" "$AGENT_PATH"
|
||||||
sudo chmod +x "$AGENT_PATH"
|
sudo chmod +x "$AGENT_PATH"
|
||||||
log_success "Agent binary installed to $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
|
# Set up service based on platform
|
||||||
if [[ "$PLATFORM" == "linux" ]] && command -v systemctl &> /dev/null; then
|
if [[ "$PLATFORM" == "linux" ]] && command -v systemctl &> /dev/null; then
|
||||||
log_info "Setting up systemd service..."
|
log_info "Setting up systemd service..."
|
||||||
|
|
@ -379,7 +390,7 @@ After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
ExecStart=$AGENT_PATH --url $PULSE_URL --token $PULSE_TOKEN --interval $INTERVAL
|
ExecStart=$AGENT_CMD
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=5s
|
RestartSec=5s
|
||||||
User=root
|
User=root
|
||||||
|
|
@ -558,6 +569,7 @@ EOF
|
||||||
chmod 600 "$LAUNCHD_PLIST"
|
chmod 600 "$LAUNCHD_PLIST"
|
||||||
|
|
||||||
LAUNCH_TARGET="gui/$(id -u)"
|
LAUNCH_TARGET="gui/$(id -u)"
|
||||||
|
LAUNCH_IDENTIFIER="$LAUNCH_TARGET/com.pulse.host-agent"
|
||||||
|
|
||||||
# Attempt to unload any existing service instance
|
# Attempt to unload any existing service instance
|
||||||
if launchctl bootout "$LAUNCH_TARGET" "$LAUNCHD_PLIST" 2>/dev/null; then
|
if launchctl bootout "$LAUNCH_TARGET" "$LAUNCHD_PLIST" 2>/dev/null; then
|
||||||
|
|
@ -576,23 +588,128 @@ EOF
|
||||||
fi
|
fi
|
||||||
SERVICE_MODE="launchd"
|
SERVICE_MODE="launchd"
|
||||||
else
|
else
|
||||||
log_warn "Automatic service setup not available for this platform"
|
|
||||||
sudo mkdir -p "$LINUX_LOG_DIR"
|
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
|
if [[ "$UNRAID" == true ]]; then
|
||||||
log_info ""
|
log_info "Detected Unraid (no systemd). Configuring persistent background service..."
|
||||||
log_info "On Unraid, add the wrapped command to /boot/config/go so it starts on boot."
|
|
||||||
|
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
|
else
|
||||||
log_info ""
|
log_warn "Systemd not available; configuring rc.local-based startup"
|
||||||
log_info "On systems without systemd, add the wrapped command to /etc/rc.local (or similar) to start on boot."
|
|
||||||
|
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
|
fi
|
||||||
SERVICE_MODE="manual"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Validate installation
|
# Validate installation
|
||||||
|
|
@ -613,43 +730,72 @@ if [[ "$SERVICE_MODE" == "systemd" ]]; then
|
||||||
log_info "Check logs with: sudo journalctl -u pulse-host-agent -n 50"
|
log_info "Check logs with: sudo journalctl -u pulse-host-agent -n 50"
|
||||||
fi
|
fi
|
||||||
elif [[ "$SERVICE_MODE" == "launchd" ]]; then
|
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
|
SERVICE_RUNNING=true
|
||||||
log_success "Service is running successfully!"
|
log_success "Service is running successfully!"
|
||||||
else
|
else
|
||||||
log_warn "Service may not be running properly"
|
log_warn "Service may not be running properly"
|
||||||
log_info "Check logs with: tail -20 $MACOS_LOG_FILE"
|
log_info "Check logs with: tail -20 $MACOS_LOG_FILE"
|
||||||
fi
|
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
|
else
|
||||||
log_info "Skipping automated service validation – start the agent manually using the commands above."
|
log_info "Skipping automated service validation – start the agent manually using the commands above."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "$SERVICE_RUNNING" == true ]]; then
|
||||||
|
VALIDATION_SUCCESS=true
|
||||||
|
fi
|
||||||
|
|
||||||
# Try to verify with API endpoint that agent is reporting
|
# Try to verify with API endpoint that agent is reporting
|
||||||
if [[ "$SERVICE_MODE" != "manual" && "$SERVICE_RUNNING" == true ]]; then
|
if [[ "$SERVICE_MODE" != "manual" && "$SERVICE_RUNNING" == true ]]; then
|
||||||
log_info "Verifying agent registration with Pulse server..."
|
|
||||||
|
|
||||||
# Get hostname for verification
|
|
||||||
HOSTNAME=$(hostname)
|
HOSTNAME=$(hostname)
|
||||||
|
|
||||||
# Try to query the API for this host
|
if [[ -z "$PULSE_TOKEN" ]]; then
|
||||||
if command -v curl &> /dev/null; then
|
log_info "Registration check skipped (no API token available for lookup)."
|
||||||
API_CHECK=$(curl -fsSL "$PULSE_URL/api/hosts" 2>/dev/null || echo "")
|
elif command -v curl &> /dev/null; then
|
||||||
elif command -v wget &> /dev/null; then
|
log_info "Verifying agent registration with Pulse server..."
|
||||||
API_CHECK=$(wget -qO- "$PULSE_URL/api/hosts" 2>/dev/null || echo "")
|
LOOKUP_RESPONSE=$(curl -fsSL \
|
||||||
fi
|
-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
|
if [[ "$LOOKUP_RESPONSE" == *'"success":true'* ]]; then
|
||||||
VALIDATION_SUCCESS=true
|
host_status=$(printf '%s' "$LOOKUP_RESPONSE" | sed -n 's/.*"status":"\([^"]*\)".*/\1/p')
|
||||||
log_success "Agent successfully registered with Pulse server!"
|
last_seen=$(printf '%s' "$LOOKUP_RESPONSE" | sed -n 's/.*"lastSeen":"\([^"]*\)".*/\1/p')
|
||||||
log_success "Your host '$HOSTNAME' is now reporting metrics!"
|
log_success "Agent successfully registered with Pulse server!"
|
||||||
elif [[ -n "$API_CHECK" ]]; then
|
if [[ -n "$host_status" ]]; then
|
||||||
log_warn "Agent is running but host not found in API yet (may take a few moments)"
|
log_info "Pulse reports status: $host_status (last seen $last_seen)"
|
||||||
log_info "Service appears healthy, metrics should appear shortly."
|
fi
|
||||||
VALIDATION_SUCCESS=true # Service is running, so count as success
|
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
|
else
|
||||||
log_warn "Could not verify registration via API (endpoint may not be accessible)"
|
log_info "Registration check skipped (curl is required for API validation)."
|
||||||
log_info "Service is running, check your Pulse dashboard manually."
|
|
||||||
VALIDATION_SUCCESS=true # Service is running, so count as success
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -657,11 +803,7 @@ if [[ "$SERVICE_MODE" == "manual" ]]; then
|
||||||
log_warn "Service validation requires starting the agent manually."
|
log_warn "Service validation requires starting the agent manually."
|
||||||
log_info "Run the following to launch the agent in the background:"
|
log_info "Run the following to launch the agent in the background:"
|
||||||
log_info " $MANUAL_START_WRAPPED"
|
log_info " $MANUAL_START_WRAPPED"
|
||||||
if [[ "$UNRAID" == true ]]; then
|
log_info "Add the same line to /etc/rc.local (or equivalent) to auto-start on boot."
|
||||||
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
|
|
||||||
elif [[ "$VALIDATION_SUCCESS" == true ]]; then
|
elif [[ "$VALIDATION_SUCCESS" == true ]]; then
|
||||||
log_info "Check your Pulse dashboard at: $PULSE_URL"
|
log_info "Check your Pulse dashboard at: $PULSE_URL"
|
||||||
else
|
else
|
||||||
|
|
@ -677,13 +819,17 @@ else
|
||||||
echo " View logs: tail -f $MACOS_LOG_FILE"
|
echo " View logs: tail -f $MACOS_LOG_FILE"
|
||||||
echo " Check status: launchctl list | grep pulse"
|
echo " Check status: launchctl list | grep pulse"
|
||||||
echo " Restart: launchctl unload $LAUNCHD_PLIST && launchctl load $LAUNCHD_PLIST"
|
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
|
else
|
||||||
echo " Start agent: $MANUAL_START_WRAPPED"
|
echo " Start agent: $MANUAL_START_WRAPPED"
|
||||||
if [[ "$UNRAID" == true ]]; then
|
echo " Persist: Add the wrapped command to /etc/rc.local (or equivalent)"
|
||||||
echo " Persist: Add the wrapped command to /boot/config/go"
|
|
||||||
else
|
|
||||||
echo " Persist: Add the wrapped command to /etc/rc.local (or equivalent)"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
echo " Manual run: $MANUAL_START_CMD"
|
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 " Restart: launchctl unload $LAUNCHD_PLIST && launchctl load $LAUNCHD_PLIST"
|
||||||
echo " Status: launchctl list | grep pulse"
|
echo " Status: launchctl list | grep pulse"
|
||||||
echo " Logs: tail -f $MACOS_LOG_FILE"
|
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
|
else
|
||||||
echo " Start: $MANUAL_START_WRAPPED"
|
echo " Start: $MANUAL_START_WRAPPED"
|
||||||
if [[ "$UNRAID" == true ]]; then
|
echo " Persist: Add the wrapped command to /etc/rc.local (or similar) to start on boot"
|
||||||
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
|
|
||||||
fi
|
fi
|
||||||
echo ""
|
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';
|
import { apiFetch, apiFetchJSON } from '@/utils/apiClient';
|
||||||
|
|
||||||
export class MonitoringAPI {
|
export class MonitoringAPI {
|
||||||
|
|
@ -147,6 +147,56 @@ export class MonitoringAPI {
|
||||||
throw new Error(message);
|
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 {
|
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 type { JSX } from 'solid-js';
|
||||||
import { useWebSocket } from '@/App';
|
import { useWebSocket } from '@/App';
|
||||||
import type { Host } from '@/types/api';
|
import type { Host, HostLookupResponse } from '@/types/api';
|
||||||
import { Card } from '@/components/shared/Card';
|
import { Card } from '@/components/shared/Card';
|
||||||
import { formatBytes, formatRelativeTime, formatUptime, formatAbsoluteTime } from '@/utils/format';
|
import { formatBytes, formatRelativeTime, formatUptime, formatAbsoluteTime } from '@/utils/format';
|
||||||
import { notificationStore } from '@/stores/notifications';
|
import { notificationStore } from '@/stores/notifications';
|
||||||
|
|
@ -9,6 +9,7 @@ import { HOST_AGENT_SCOPE } from '@/constants/apiScopes';
|
||||||
import type { SecurityStatus } from '@/types/config';
|
import type { SecurityStatus } from '@/types/config';
|
||||||
import type { APITokenRecord } from '@/api/security';
|
import type { APITokenRecord } from '@/api/security';
|
||||||
import { SecurityAPI } from '@/api/security';
|
import { SecurityAPI } from '@/api/security';
|
||||||
|
import { MonitoringAPI } from '@/api/monitoring';
|
||||||
|
|
||||||
const TOKEN_PLACEHOLDER = '<api-token>';
|
const TOKEN_PLACEHOLDER = '<api-token>';
|
||||||
const pulseUrl = () => {
|
const pulseUrl = () => {
|
||||||
|
|
@ -104,6 +105,12 @@ export const HostAgents: Component = () => {
|
||||||
const [confirmedNoToken, setConfirmedNoToken] = createSignal(false);
|
const [confirmedNoToken, setConfirmedNoToken] = createSignal(false);
|
||||||
const [currentToken, setCurrentToken] = createSignal<string | null>(null);
|
const [currentToken, setCurrentToken] = createSignal<string | null>(null);
|
||||||
const [isGeneratingToken, setIsGeneratingToken] = createSignal(false);
|
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(() => {
|
createEffect(() => {
|
||||||
if (requiresToken()) {
|
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(() => {
|
onMount(() => {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return;
|
return;
|
||||||
|
|
@ -156,13 +219,20 @@ export const HostAgents: Component = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const requiresToken = () => {
|
const requiresToken = () => {
|
||||||
const status = securityStatus();
|
const status = securityStatus();
|
||||||
if (status) {
|
if (status) {
|
||||||
return status.requiresAuth || status.apiTokenConfigured;
|
return status.requiresAuth || status.apiTokenConfigured;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
if (highlightTimer) {
|
||||||
|
clearTimeout(highlightTimer);
|
||||||
|
highlightTimer = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const hasToken = () => Boolean(currentToken());
|
const hasToken = () => Boolean(currentToken());
|
||||||
const commandsUnlocked = () => (requiresToken() ? hasToken() : hasToken() || confirmedNoToken());
|
const commandsUnlocked = () => (requiresToken() ? hasToken() : hasToken() || confirmedNoToken());
|
||||||
|
|
@ -234,6 +304,35 @@ export const HostAgents: Component = () => {
|
||||||
return currentToken() || 'disabled';
|
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]
|
const getSystemdServiceUnit = () => `[Unit]
|
||||||
Description=Pulse Host Agent
|
Description=Pulse Host Agent
|
||||||
After=network-online.target
|
After=network-online.target
|
||||||
|
|
@ -387,6 +486,78 @@ sudo systemctl daemon-reload`;
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</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">
|
<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">
|
<summary class="cursor-pointer text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
Advanced options (manual install & uninstall)
|
Advanced options (manual install & uninstall)
|
||||||
|
|
@ -542,6 +713,7 @@ sudo systemctl daemon-reload`;
|
||||||
status === 'online' ||
|
status === 'online' ||
|
||||||
status === 'running' ||
|
status === 'running' ||
|
||||||
status === 'healthy';
|
status === 'healthy';
|
||||||
|
const isHighlighted = highlightedHostId() === host.id;
|
||||||
|
|
||||||
const baseRowClass = isStale
|
const baseRowClass = isStale
|
||||||
? 'bg-gray-50 dark:bg-gray-800/50 opacity-60'
|
? 'bg-gray-50 dark:bg-gray-800/50 opacity-60'
|
||||||
|
|
@ -549,6 +721,9 @@ sudo systemctl daemon-reload`;
|
||||||
|
|
||||||
const rowClass =
|
const rowClass =
|
||||||
tokenRevoked && !isStale ? `${baseRowClass} opacity-60` : baseRowClass;
|
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 () => {
|
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.`)) {
|
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 (
|
return (
|
||||||
<tr class={rowClass}>
|
<tr class={`${rowClass} ${highlightClass}`}>
|
||||||
<td class="py-3 px-4">
|
<td class="py-3 px-4">
|
||||||
<div class="font-medium text-gray-900 dark:text-gray-100">
|
<div class="font-medium text-gray-900 dark:text-gray-100">
|
||||||
{host.displayName || host.hostname || host.id}
|
{host.displayName || host.hostname || host.id}
|
||||||
|
|
|
||||||
|
|
@ -241,6 +241,19 @@ export interface HostSensorSummary {
|
||||||
additional?: Record<string, number>;
|
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 {
|
export interface ReplicationJob {
|
||||||
id: string;
|
id: string;
|
||||||
instance: string;
|
instance: string;
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
|
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/utils"
|
"github.com/rcourtman/pulse-go-rewrite/internal/utils"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/websocket"
|
"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.
|
// HandleDeleteHost removes a host from the shared state.
|
||||||
func (h *HostAgentHandlers) HandleDeleteHost(w http.ResponseWriter, r *http.Request) {
|
func (h *HostAgentHandlers) HandleDeleteHost(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodDelete {
|
if r.Method != http.MethodDelete {
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,7 @@ func (r *Router) setupRoutes() {
|
||||||
r.mux.HandleFunc("/api/state", r.handleState)
|
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/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/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/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/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)))
|
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-Info { param([string]$msg) Write-Color $Blue "ℹ $msg" }
|
||||||
function Write-Warning { param([string]$msg) Write-Color $Yellow "⚠ $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-Host ""
|
||||||
Write-Color $Blue "═══════════════════════════════════════════════════════════"
|
Write-Color $Blue "═══════════════════════════════════════════════════════════"
|
||||||
Write-Color $Blue " Pulse Host Agent - Windows Installation"
|
Write-Color $Blue " Pulse Host Agent - Windows Installation"
|
||||||
|
|
@ -89,6 +146,13 @@ try {
|
||||||
# Download binary
|
# Download binary
|
||||||
Invoke-WebRequest -Uri $downloadUrl -OutFile $agentPath -UseBasicParsing
|
Invoke-WebRequest -Uri $downloadUrl -OutFile $agentPath -UseBasicParsing
|
||||||
Write-Success "Downloaded agent to $agentPath"
|
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 {
|
} catch {
|
||||||
Write-Error "Failed to download agent: $_"
|
Write-Error "Failed to download agent: $_"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
@ -119,17 +183,6 @@ if ($existingService) {
|
||||||
if (-not $NoService) {
|
if (-not $NoService) {
|
||||||
Write-Info "Installing native Windows service with built-in service support..."
|
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 {
|
try {
|
||||||
if ($existingService) {
|
if ($existingService) {
|
||||||
Write-Info "Removing existing service..."
|
Write-Info "Removing existing service..."
|
||||||
|
|
@ -156,6 +209,8 @@ if (-not $NoService) {
|
||||||
Write-Warning "Could not register Event Log source (not critical): $_"
|
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)
|
# Configure service recovery options (restart on failure)
|
||||||
sc.exe failure $serviceName reset= 86400 actions= restart/60000/restart/60000/restart/60000 | Out-Null
|
sc.exe failure $serviceName reset= 86400 actions= restart/60000/restart/60000/restart/60000 | Out-Null
|
||||||
Write-Success "Configured automatic restart on failure"
|
Write-Success "Configured automatic restart on failure"
|
||||||
|
|
@ -169,30 +224,62 @@ if (-not $NoService) {
|
||||||
if ($status -eq 'Running') {
|
if ($status -eq 'Running') {
|
||||||
Write-Success "Service started successfully!"
|
Write-Success "Service started successfully!"
|
||||||
|
|
||||||
# Optional: Validate that agent is reporting
|
|
||||||
Write-Info "Waiting 10 seconds to validate agent reporting..."
|
Write-Info "Waiting 10 seconds to validate agent reporting..."
|
||||||
Start-Sleep -Seconds 10
|
Start-Sleep -Seconds 10
|
||||||
|
|
||||||
# Check Event Log for successful startup
|
$hostname = $env:COMPUTERNAME
|
||||||
$recentLogs = Get-EventLog -LogName Application -Source $serviceName -Newest 5 -ErrorAction SilentlyContinue
|
$lookupHost = Test-AgentRegistration -PulseUrl $PulseUrl -Hostname $hostname -Token $Token
|
||||||
$hasStarted = $recentLogs | Where-Object { $_.Message -like "*started successfully*" }
|
if ($lookupHost) {
|
||||||
|
Write-Success "Agent successfully registered with Pulse (host '$hostname')."
|
||||||
if ($hasStarted) {
|
if ($lookupHost.status) {
|
||||||
Write-Success "Agent is reporting successfully!"
|
$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."
|
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 {
|
} 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 {
|
} else {
|
||||||
Write-Warning "Service status: $status"
|
Write-Warning "Service status: $status"
|
||||||
Write-Info "Checking service logs..."
|
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 {
|
} catch {
|
||||||
Write-Error "Failed to create/start service: $_"
|
Write-Error "Failed to create/start service: $_"
|
||||||
Write-Info "You can start the agent manually with:"
|
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-Host ""
|
||||||
Write-Info "Or check Windows Event Viewer (Application log) for error details."
|
Write-Info "Or check Windows Event Viewer (Application log) for error details."
|
||||||
exit 1
|
exit 1
|
||||||
|
|
@ -201,7 +288,7 @@ if (-not $NoService) {
|
||||||
Write-Info "Skipping service installation (--NoService flag)"
|
Write-Info "Skipping service installation (--NoService flag)"
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Info "To start the agent manually:"
|
Write-Info "To start the agent manually:"
|
||||||
Write-Host " & `"$agentPath`" --url $PulseUrl --interval $Interval $(if ($Token) { '--token ***' })"
|
Write-Host " $manualCommand"
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
|
|
@ -216,7 +303,7 @@ Write-Host " Stop: Stop-Service -Name PulseHostAgent"
|
||||||
Write-Host " Restart: Restart-Service -Name PulseHostAgent"
|
Write-Host " Restart: Restart-Service -Name PulseHostAgent"
|
||||||
Write-Host " Status: Get-Service -Name PulseHostAgent | Select Status, StartType"
|
Write-Host " Status: Get-Service -Name PulseHostAgent | Select Status, StartType"
|
||||||
Write-Host " Remove: sc.exe delete PulseHostAgent"
|
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-Host ""
|
||||||
|
|
||||||
Write-Info "Files installed:"
|
Write-Info "Files installed:"
|
||||||
|
|
|
||||||
|
|
@ -112,8 +112,10 @@ LINUX_LOG_FILE="$LINUX_LOG_DIR/host-agent.log"
|
||||||
SERVICE_MODE="manual"
|
SERVICE_MODE="manual"
|
||||||
MANUAL_START_CMD=""
|
MANUAL_START_CMD=""
|
||||||
MANUAL_START_WRAPPED=""
|
MANUAL_START_WRAPPED=""
|
||||||
|
LAUNCH_IDENTIFIER=""
|
||||||
UNRAID=false
|
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
|
UNRAID=true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -365,6 +367,15 @@ sudo mv "$TEMP_BINARY" "$AGENT_PATH"
|
||||||
sudo chmod +x "$AGENT_PATH"
|
sudo chmod +x "$AGENT_PATH"
|
||||||
log_success "Agent binary installed to $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
|
# Set up service based on platform
|
||||||
if [[ "$PLATFORM" == "linux" ]] && command -v systemctl &> /dev/null; then
|
if [[ "$PLATFORM" == "linux" ]] && command -v systemctl &> /dev/null; then
|
||||||
log_info "Setting up systemd service..."
|
log_info "Setting up systemd service..."
|
||||||
|
|
@ -379,7 +390,7 @@ After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
ExecStart=$AGENT_PATH --url $PULSE_URL --token $PULSE_TOKEN --interval $INTERVAL
|
ExecStart=$AGENT_CMD
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=5s
|
RestartSec=5s
|
||||||
User=root
|
User=root
|
||||||
|
|
@ -558,6 +569,7 @@ EOF
|
||||||
chmod 600 "$LAUNCHD_PLIST"
|
chmod 600 "$LAUNCHD_PLIST"
|
||||||
|
|
||||||
LAUNCH_TARGET="gui/$(id -u)"
|
LAUNCH_TARGET="gui/$(id -u)"
|
||||||
|
LAUNCH_IDENTIFIER="$LAUNCH_TARGET/com.pulse.host-agent"
|
||||||
|
|
||||||
# Attempt to unload any existing service instance
|
# Attempt to unload any existing service instance
|
||||||
if launchctl bootout "$LAUNCH_TARGET" "$LAUNCHD_PLIST" 2>/dev/null; then
|
if launchctl bootout "$LAUNCH_TARGET" "$LAUNCHD_PLIST" 2>/dev/null; then
|
||||||
|
|
@ -576,23 +588,128 @@ EOF
|
||||||
fi
|
fi
|
||||||
SERVICE_MODE="launchd"
|
SERVICE_MODE="launchd"
|
||||||
else
|
else
|
||||||
log_warn "Automatic service setup not available for this platform"
|
|
||||||
sudo mkdir -p "$LINUX_LOG_DIR"
|
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
|
if [[ "$UNRAID" == true ]]; then
|
||||||
log_info ""
|
log_info "Detected Unraid (no systemd). Configuring persistent background service..."
|
||||||
log_info "On Unraid, add the wrapped command to /boot/config/go so it starts on boot."
|
|
||||||
|
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
|
else
|
||||||
log_info ""
|
log_warn "Systemd not available; configuring rc.local-based startup"
|
||||||
log_info "On systems without systemd, add the wrapped command to /etc/rc.local (or similar) to start on boot."
|
|
||||||
|
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
|
fi
|
||||||
SERVICE_MODE="manual"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Validate installation
|
# Validate installation
|
||||||
|
|
@ -613,43 +730,72 @@ if [[ "$SERVICE_MODE" == "systemd" ]]; then
|
||||||
log_info "Check logs with: sudo journalctl -u pulse-host-agent -n 50"
|
log_info "Check logs with: sudo journalctl -u pulse-host-agent -n 50"
|
||||||
fi
|
fi
|
||||||
elif [[ "$SERVICE_MODE" == "launchd" ]]; then
|
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
|
SERVICE_RUNNING=true
|
||||||
log_success "Service is running successfully!"
|
log_success "Service is running successfully!"
|
||||||
else
|
else
|
||||||
log_warn "Service may not be running properly"
|
log_warn "Service may not be running properly"
|
||||||
log_info "Check logs with: tail -20 $MACOS_LOG_FILE"
|
log_info "Check logs with: tail -20 $MACOS_LOG_FILE"
|
||||||
fi
|
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
|
else
|
||||||
log_info "Skipping automated service validation – start the agent manually using the commands above."
|
log_info "Skipping automated service validation – start the agent manually using the commands above."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "$SERVICE_RUNNING" == true ]]; then
|
||||||
|
VALIDATION_SUCCESS=true
|
||||||
|
fi
|
||||||
|
|
||||||
# Try to verify with API endpoint that agent is reporting
|
# Try to verify with API endpoint that agent is reporting
|
||||||
if [[ "$SERVICE_MODE" != "manual" && "$SERVICE_RUNNING" == true ]]; then
|
if [[ "$SERVICE_MODE" != "manual" && "$SERVICE_RUNNING" == true ]]; then
|
||||||
log_info "Verifying agent registration with Pulse server..."
|
|
||||||
|
|
||||||
# Get hostname for verification
|
|
||||||
HOSTNAME=$(hostname)
|
HOSTNAME=$(hostname)
|
||||||
|
|
||||||
# Try to query the API for this host
|
if [[ -z "$PULSE_TOKEN" ]]; then
|
||||||
if command -v curl &> /dev/null; then
|
log_info "Registration check skipped (no API token available for lookup)."
|
||||||
API_CHECK=$(curl -fsSL "$PULSE_URL/api/hosts" 2>/dev/null || echo "")
|
elif command -v curl &> /dev/null; then
|
||||||
elif command -v wget &> /dev/null; then
|
log_info "Verifying agent registration with Pulse server..."
|
||||||
API_CHECK=$(wget -qO- "$PULSE_URL/api/hosts" 2>/dev/null || echo "")
|
LOOKUP_RESPONSE=$(curl -fsSL \
|
||||||
fi
|
-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
|
if [[ "$LOOKUP_RESPONSE" == *'"success":true'* ]]; then
|
||||||
VALIDATION_SUCCESS=true
|
host_status=$(printf '%s' "$LOOKUP_RESPONSE" | sed -n 's/.*"status":"\([^"]*\)".*/\1/p')
|
||||||
log_success "Agent successfully registered with Pulse server!"
|
last_seen=$(printf '%s' "$LOOKUP_RESPONSE" | sed -n 's/.*"lastSeen":"\([^"]*\)".*/\1/p')
|
||||||
log_success "Your host '$HOSTNAME' is now reporting metrics!"
|
log_success "Agent successfully registered with Pulse server!"
|
||||||
elif [[ -n "$API_CHECK" ]]; then
|
if [[ -n "$host_status" ]]; then
|
||||||
log_warn "Agent is running but host not found in API yet (may take a few moments)"
|
log_info "Pulse reports status: $host_status (last seen $last_seen)"
|
||||||
log_info "Service appears healthy, metrics should appear shortly."
|
fi
|
||||||
VALIDATION_SUCCESS=true # Service is running, so count as success
|
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
|
else
|
||||||
log_warn "Could not verify registration via API (endpoint may not be accessible)"
|
log_info "Registration check skipped (curl is required for API validation)."
|
||||||
log_info "Service is running, check your Pulse dashboard manually."
|
|
||||||
VALIDATION_SUCCESS=true # Service is running, so count as success
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -657,11 +803,7 @@ if [[ "$SERVICE_MODE" == "manual" ]]; then
|
||||||
log_warn "Service validation requires starting the agent manually."
|
log_warn "Service validation requires starting the agent manually."
|
||||||
log_info "Run the following to launch the agent in the background:"
|
log_info "Run the following to launch the agent in the background:"
|
||||||
log_info " $MANUAL_START_WRAPPED"
|
log_info " $MANUAL_START_WRAPPED"
|
||||||
if [[ "$UNRAID" == true ]]; then
|
log_info "Add the same line to /etc/rc.local (or equivalent) to auto-start on boot."
|
||||||
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
|
|
||||||
elif [[ "$VALIDATION_SUCCESS" == true ]]; then
|
elif [[ "$VALIDATION_SUCCESS" == true ]]; then
|
||||||
log_info "Check your Pulse dashboard at: $PULSE_URL"
|
log_info "Check your Pulse dashboard at: $PULSE_URL"
|
||||||
else
|
else
|
||||||
|
|
@ -677,13 +819,17 @@ else
|
||||||
echo " View logs: tail -f $MACOS_LOG_FILE"
|
echo " View logs: tail -f $MACOS_LOG_FILE"
|
||||||
echo " Check status: launchctl list | grep pulse"
|
echo " Check status: launchctl list | grep pulse"
|
||||||
echo " Restart: launchctl unload $LAUNCHD_PLIST && launchctl load $LAUNCHD_PLIST"
|
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
|
else
|
||||||
echo " Start agent: $MANUAL_START_WRAPPED"
|
echo " Start agent: $MANUAL_START_WRAPPED"
|
||||||
if [[ "$UNRAID" == true ]]; then
|
echo " Persist: Add the wrapped command to /etc/rc.local (or equivalent)"
|
||||||
echo " Persist: Add the wrapped command to /boot/config/go"
|
|
||||||
else
|
|
||||||
echo " Persist: Add the wrapped command to /etc/rc.local (or equivalent)"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
echo " Manual run: $MANUAL_START_CMD"
|
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 " Restart: launchctl unload $LAUNCHD_PLIST && launchctl load $LAUNCHD_PLIST"
|
||||||
echo " Status: launchctl list | grep pulse"
|
echo " Status: launchctl list | grep pulse"
|
||||||
echo " Logs: tail -f $MACOS_LOG_FILE"
|
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
|
else
|
||||||
echo " Start: $MANUAL_START_WRAPPED"
|
echo " Start: $MANUAL_START_WRAPPED"
|
||||||
if [[ "$UNRAID" == true ]]; then
|
echo " Persist: Add the wrapped command to /etc/rc.local (or similar) to start on boot"
|
||||||
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
|
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue