feat: auto-detect platforms during agent install and allow multi-host tokens
- Install script now auto-detects Docker, Kubernetes, and Proxmox - Platform monitoring is enabled automatically when detected - Users can override with --disable-* or --enable-* flags - Allow same token to register multiple hosts (one per hostname) - Update tests to reflect new multi-host token behavior - Improve CompleteStep and UnifiedAgents UI components - Update UNIFIED_AGENT.md documentation
This commit is contained in:
parent
eb59faaa4a
commit
6795f83e63
7 changed files with 335 additions and 160 deletions
|
|
@ -41,8 +41,12 @@ curl -fsSL http://<pulse-ip>:7655/install.sh | \
|
|||
| `--token` | `PULSE_TOKEN` | API token | *(required)* |
|
||||
| `--interval` | `PULSE_INTERVAL` | Reporting interval | `30s` |
|
||||
| `--enable-host` | `PULSE_ENABLE_HOST` | Enable host metrics | `true` |
|
||||
| `--enable-docker` | `PULSE_ENABLE_DOCKER` | Enable Docker metrics | `false` |
|
||||
| `--enable-kubernetes` | `PULSE_ENABLE_KUBERNETES` | Enable Kubernetes metrics | `false` |
|
||||
| `--enable-docker` | `PULSE_ENABLE_DOCKER` | Force enable Docker metrics | **auto-detect** |
|
||||
| `--disable-docker` | - | Disable Docker even if detected | - |
|
||||
| `--enable-kubernetes` | `PULSE_ENABLE_KUBERNETES` | Force enable Kubernetes metrics | **auto-detect** |
|
||||
| `--disable-kubernetes` | - | Disable Kubernetes even if detected | - |
|
||||
| `--enable-proxmox` | `PULSE_ENABLE_PROXMOX` | Force enable Proxmox integration | **auto-detect** |
|
||||
| `--disable-proxmox` | - | Disable Proxmox even if detected | - |
|
||||
| `--kubeconfig` | `PULSE_KUBECONFIG` | Kubeconfig path (optional) | *(auto)* |
|
||||
| `--kube-context` | `PULSE_KUBE_CONTEXT` | Kubeconfig context (optional) | *(auto)* |
|
||||
| `--kube-include-namespace` | `PULSE_KUBE_INCLUDE_NAMESPACES` | Limit namespaces (repeatable or CSV) | *(all)* |
|
||||
|
|
@ -55,20 +59,36 @@ curl -fsSL http://<pulse-ip>:7655/install.sh | \
|
|||
| `--agent-id` | `PULSE_AGENT_ID` | Unique agent identifier | *(machine-id)* |
|
||||
| `--health-addr` | `PULSE_HEALTH_ADDR` | Health/metrics server address | `:9191` |
|
||||
|
||||
## Auto-Detection
|
||||
|
||||
The installer automatically detects available platforms on the target machine:
|
||||
|
||||
- **Docker/Podman**: Enabled if `docker info` or `podman info` succeeds
|
||||
- **Kubernetes**: Enabled if `kubectl cluster-info` succeeds or kubeconfig exists
|
||||
- **Proxmox**: Enabled if `/etc/pve` or `/etc/proxmox-backup` exists
|
||||
|
||||
Use `--disable-*` flags to skip auto-detected platforms, or `--enable-*` to force enable.
|
||||
|
||||
## Installation Options
|
||||
|
||||
### Host Monitoring Only (default)
|
||||
### Simple Install (auto-detects everything)
|
||||
```bash
|
||||
curl -fsSL http://<pulse-ip>:7655/install.sh | \
|
||||
bash -s -- --url http://<pulse-ip>:7655 --token <token>
|
||||
```
|
||||
|
||||
### Host + Docker Monitoring
|
||||
### Force Enable Docker (if auto-detection fails)
|
||||
```bash
|
||||
curl -fsSL http://<pulse-ip>:7655/install.sh | \
|
||||
bash -s -- --url http://<pulse-ip>:7655 --token <token> --enable-docker
|
||||
```
|
||||
|
||||
### Disable Docker (even if detected)
|
||||
```bash
|
||||
curl -fsSL http://<pulse-ip>:7655/install.sh | \
|
||||
bash -s -- --url http://<pulse-ip>:7655 --token <token> --disable-docker
|
||||
```
|
||||
|
||||
### Host + Kubernetes Monitoring
|
||||
```bash
|
||||
curl -fsSL http://<pulse-ip>:7655/install.sh | \
|
||||
|
|
|
|||
|
|
@ -486,49 +486,52 @@ export const UnifiedAgents: Component = () => {
|
|||
<div class="space-y-3">
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-gray-100">Installation commands</h4>
|
||||
<div class="flex items-center gap-4">
|
||||
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={enableDocker()}
|
||||
onChange={(e) => setEnableDocker(e.currentTarget.checked)}
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
/>
|
||||
Docker monitoring
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={enableKubernetes()}
|
||||
onChange={(e) => setEnableKubernetes(e.currentTarget.checked)}
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
/>
|
||||
Kubernetes monitoring
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 cursor-pointer" title="For Proxmox VE/PBS nodes - auto-creates API token and registers the node">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={enableProxmox()}
|
||||
onChange={(e) => setEnableProxmox(e.currentTarget.checked)}
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
/>
|
||||
Proxmox setup
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 cursor-pointer" title="Skip TLS certificate verification (for self-signed certificates)">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={insecureMode()}
|
||||
onChange={(e) => setInsecureMode(e.currentTarget.checked)}
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
/>
|
||||
Skip TLS verify
|
||||
</label>
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-gray-100">Installation commands</h4>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">The installer auto-detects Docker, Kubernetes, and Proxmox. Use these to force enable/disable:</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 flex-wrap">
|
||||
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 cursor-pointer" title="Force enable Docker monitoring (auto-detected if Docker/Podman is available)">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={enableDocker()}
|
||||
onChange={(e) => setEnableDocker(e.currentTarget.checked)}
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
/>
|
||||
Force Docker
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 cursor-pointer" title="Force enable Kubernetes monitoring (auto-detected if kubectl/kubeconfig is available)">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={enableKubernetes()}
|
||||
onChange={(e) => setEnableKubernetes(e.currentTarget.checked)}
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
/>
|
||||
Force Kubernetes
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 cursor-pointer" title="Force enable Proxmox integration (auto-detected on PVE/PBS hosts)">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={enableProxmox()}
|
||||
onChange={(e) => setEnableProxmox(e.currentTarget.checked)}
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
/>
|
||||
Force Proxmox
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 cursor-pointer" title="Skip TLS certificate verification (for self-signed certificates)">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={insecureMode()}
|
||||
onChange={(e) => setInsecureMode(e.currentTarget.checked)}
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
/>
|
||||
Skip TLS verify
|
||||
</label>
|
||||
</div>
|
||||
<Show when={enableProxmox()}>
|
||||
<div class="rounded-lg border border-blue-200 bg-blue-50 px-4 py-3 text-sm text-blue-800 dark:border-blue-800 dark:bg-blue-900/20 dark:text-blue-200">
|
||||
<p class="font-medium">Proxmox auto-setup enabled</p>
|
||||
<p class="font-medium">Proxmox force-enable</p>
|
||||
<p class="text-xs mt-1 text-blue-700 dark:text-blue-300">
|
||||
The agent will create a <code class="bg-blue-100 dark:bg-blue-900/40 px-1 rounded">pulse-monitor</code> user and API token on the Proxmox node,
|
||||
then register it with Pulse automatically. Includes temperature monitoring.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { Component, createSignal, createEffect, onCleanup, Show, For } from 'solid-js';
|
||||
// Note: For is still used for connectedAgents list
|
||||
import { copyToClipboard } from '@/utils/clipboard';
|
||||
import { getPulseBaseUrl } from '@/utils/url';
|
||||
import { SecurityAPI } from '@/api/security';
|
||||
import { ProxmoxIcon } from '@/components/icons/ProxmoxIcon';
|
||||
import type { WizardState } from '../SetupWizard';
|
||||
|
||||
interface CompleteStepProps {
|
||||
|
|
@ -9,7 +11,7 @@ interface CompleteStepProps {
|
|||
onComplete: () => void;
|
||||
}
|
||||
|
||||
type Platform = 'proxmox' | 'docker' | 'kubernetes' | 'host';
|
||||
// Platform auto-detection is now handled by the install script
|
||||
|
||||
interface ConnectedAgent {
|
||||
id: string;
|
||||
|
|
@ -22,18 +24,10 @@ interface ConnectedAgent {
|
|||
export const CompleteStep: Component<CompleteStepProps> = (props) => {
|
||||
const [copied, setCopied] = createSignal<'password' | 'token' | 'install' | null>(null);
|
||||
const [showCredentials, setShowCredentials] = createSignal(false);
|
||||
const [selectedPlatforms, setSelectedPlatforms] = createSignal<Platform[]>([]);
|
||||
const [connectedAgents, setConnectedAgents] = createSignal<ConnectedAgent[]>([]);
|
||||
const [currentInstallToken, setCurrentInstallToken] = createSignal(props.state.apiToken);
|
||||
const [generatingToken, setGeneratingToken] = createSignal(false);
|
||||
|
||||
// Available optional platforms (host monitoring is always enabled)
|
||||
const platforms = [
|
||||
{ id: 'proxmox' as Platform, name: 'Proxmox VE', desc: 'VMs & containers via API', icon: '🖥️' },
|
||||
{ id: 'docker' as Platform, name: 'Docker', desc: 'Container monitoring', icon: '🐳' },
|
||||
{ id: 'kubernetes' as Platform, name: 'Kubernetes', desc: 'Cluster monitoring', icon: '☸️' },
|
||||
];
|
||||
|
||||
// Poll for agent connections since WebSocket isn't available during setup
|
||||
createEffect(() => {
|
||||
let pollInterval: number | undefined;
|
||||
|
|
@ -137,14 +131,7 @@ export const CompleteStep: Component<CompleteStepProps> = (props) => {
|
|||
});
|
||||
});
|
||||
|
||||
const togglePlatform = (platform: Platform) => {
|
||||
const current = selectedPlatforms();
|
||||
if (current.includes(platform)) {
|
||||
setSelectedPlatforms(current.filter(p => p !== platform));
|
||||
} else {
|
||||
setSelectedPlatforms([...current, platform]);
|
||||
}
|
||||
};
|
||||
// Platform selection removed - installer now auto-detects Docker, Kubernetes, Proxmox
|
||||
|
||||
const generateNewToken = async () => {
|
||||
if (generatingToken()) return;
|
||||
|
|
@ -219,13 +206,8 @@ Keep these credentials secure!
|
|||
|
||||
const getInstallCommand = () => {
|
||||
const baseUrl = getPulseBaseUrl();
|
||||
// Host monitoring is always enabled by default, only add flags for optional integrations
|
||||
const platformFlags = selectedPlatforms()
|
||||
.filter(p => p !== 'host') // host is default, don't need flag
|
||||
.map(p => `--enable-${p}`)
|
||||
.join(' ');
|
||||
const flagsPart = platformFlags ? ` ${platformFlags}` : '';
|
||||
return `curl -sSL ${baseUrl}/install.sh | sudo bash -s -- --url "${baseUrl}" --token "${currentInstallToken()}"${flagsPart}`;
|
||||
// Simple command - the install script auto-detects Docker, Kubernetes, and Proxmox
|
||||
return `curl -sSL ${baseUrl}/install.sh | sudo bash -s -- --url "${baseUrl}" --token "${currentInstallToken()}"`;
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -275,12 +257,44 @@ Keep these credentials secure!
|
|||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Platform selection */}
|
||||
{/* Auto-detection info */}
|
||||
<div class="bg-white/10 backdrop-blur-xl rounded-xl border border-white/20 p-4 text-left mb-4">
|
||||
<h3 class="text-sm font-semibold text-white mb-3">What does this host have?</h3>
|
||||
<h3 class="text-sm font-semibold text-white mb-3 flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||
</svg>
|
||||
Smart Auto-Detection
|
||||
</h3>
|
||||
|
||||
{/* Always included - Host monitoring */}
|
||||
<div class="bg-green-500/10 border border-green-400/30 rounded-lg p-2.5 mb-3">
|
||||
<p class="text-white/70 text-xs mb-3">
|
||||
The installer automatically detects what's running on each host:
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-3 gap-2 mb-3">
|
||||
<div class="bg-white/5 border border-white/10 rounded-lg p-2 text-center">
|
||||
<div class="h-6 flex items-center justify-center mb-1">
|
||||
<span class="text-lg">🐳</span>
|
||||
</div>
|
||||
<div class="text-white font-medium text-xs">Docker</div>
|
||||
<p class="text-[9px] text-white/40">Container monitoring</p>
|
||||
</div>
|
||||
<div class="bg-white/5 border border-white/10 rounded-lg p-2 text-center">
|
||||
<div class="h-6 flex items-center justify-center mb-1">
|
||||
<span class="text-lg">☸️</span>
|
||||
</div>
|
||||
<div class="text-white font-medium text-xs">Kubernetes</div>
|
||||
<p class="text-[9px] text-white/40">Cluster monitoring</p>
|
||||
</div>
|
||||
<div class="bg-white/5 border border-white/10 rounded-lg p-2 text-center">
|
||||
<div class="h-6 flex items-center justify-center mb-1">
|
||||
<ProxmoxIcon class="w-5 h-5 text-orange-400" />
|
||||
</div>
|
||||
<div class="text-white font-medium text-xs">Proxmox</div>
|
||||
<p class="text-[9px] text-white/40">VM & container API</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-green-500/10 border border-green-400/30 rounded-lg p-2.5">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-4 h-4 rounded-full bg-green-500 flex items-center justify-center">
|
||||
<svg class="w-2.5 h-2.5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3">
|
||||
|
|
@ -289,44 +303,13 @@ Keep these credentials secure!
|
|||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-white font-medium text-xs">Host Monitoring</span>
|
||||
<span class="text-white font-medium text-xs">Host Metrics</span>
|
||||
<span class="text-[9px] text-green-300 bg-green-500/20 px-1.5 py-0.5 rounded">Always included</span>
|
||||
</div>
|
||||
<p class="text-[10px] text-white/50">CPU, memory, disk, network on any Linux/macOS/Windows server</p>
|
||||
<p class="text-[10px] text-white/50">CPU, memory, disk, network on any Linux/macOS/Windows</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Optional integrations */}
|
||||
<p class="text-[10px] text-white/50 mb-2">Enable if this host runs:</p>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<For each={platforms}>
|
||||
{(platform) => (
|
||||
<button
|
||||
onClick={() => togglePlatform(platform.id)}
|
||||
class={`p-2 rounded-lg text-left transition-all border ${selectedPlatforms().includes(platform.id)
|
||||
? 'bg-blue-500/20 border-blue-400/50'
|
||||
: 'bg-white/5 border-white/10 hover:bg-white/10'
|
||||
}`}
|
||||
>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div class={`w-3.5 h-3.5 rounded border-2 flex items-center justify-center ${selectedPlatforms().includes(platform.id)
|
||||
? 'bg-blue-500 border-blue-500'
|
||||
: 'border-white/40'
|
||||
}`}>
|
||||
<Show when={selectedPlatforms().includes(platform.id)}>
|
||||
<svg class="w-2.5 h-2.5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</Show>
|
||||
</div>
|
||||
<span class="text-white font-medium text-xs">{platform.name}</span>
|
||||
</div>
|
||||
<p class="text-[9px] text-white/40 ml-5 mt-0.5">{platform.desc}</p>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Agent installation */}
|
||||
|
|
|
|||
|
|
@ -12,8 +12,7 @@ const (
|
|||
BcryptCost = 12
|
||||
|
||||
// MinPasswordLength is the minimum required password length
|
||||
// Set to 1 to allow users to choose their own password security
|
||||
MinPasswordLength = 1
|
||||
MinPasswordLength = 12
|
||||
)
|
||||
|
||||
// HashPassword generates a bcrypt hash from a plain text password
|
||||
|
|
@ -37,7 +36,7 @@ func ValidatePasswordComplexity(password string) error {
|
|||
return fmt.Errorf("password must be at least %d characters long", MinPasswordLength)
|
||||
}
|
||||
|
||||
// That's it - let users choose their own passwords
|
||||
// No annoying character type requirements
|
||||
// Let users choose their own passwords beyond length.
|
||||
// No character type requirements.
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -628,7 +628,7 @@ type Monitor struct {
|
|||
dockerTokenBindings map[string]string // Track token ID -> agent ID bindings to enforce uniqueness
|
||||
removedKubernetesClusters map[string]time.Time // Track deliberately removed Kubernetes clusters (ID -> removal time)
|
||||
kubernetesTokenBindings map[string]string // Track token ID -> agent ID bindings to enforce uniqueness
|
||||
hostTokenBindings map[string]string // Track token ID -> agent ID bindings to enforce uniqueness
|
||||
hostTokenBindings map[string]string // Track tokenID:hostname -> host identity bindings
|
||||
dockerCommands map[string]*dockerHostCommand
|
||||
dockerCommandIndex map[string]string
|
||||
guestMetadataMu sync.RWMutex
|
||||
|
|
@ -1129,33 +1129,81 @@ func (m *Monitor) RemoveHostAgent(hostID string) (models.Host, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// Revoke the API token associated with this host agent
|
||||
if host.TokenID != "" {
|
||||
tokenRemoved := m.config.RemoveAPIToken(host.TokenID)
|
||||
if tokenRemoved {
|
||||
m.config.SortAPITokens()
|
||||
m.config.APITokenEnabled = m.config.HasAPITokens()
|
||||
tokenID := strings.TrimSpace(host.TokenID)
|
||||
hostname := strings.TrimSpace(host.Hostname)
|
||||
|
||||
if m.persistence != nil {
|
||||
if err := m.persistence.SaveAPITokens(m.config.APITokens); err != nil {
|
||||
log.Warn().Err(err).Str("tokenID", host.TokenID).Msg("Failed to persist API token revocation after host agent removal")
|
||||
} else {
|
||||
log.Info().Str("tokenID", host.TokenID).Str("tokenName", host.TokenName).Msg("API token revoked for removed host agent")
|
||||
tokenStillUsed := false
|
||||
if tokenID != "" && m.state != nil {
|
||||
for _, other := range m.state.GetHosts() {
|
||||
if strings.TrimSpace(other.TokenID) == tokenID {
|
||||
tokenStillUsed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !tokenStillUsed {
|
||||
for _, other := range m.state.GetDockerHosts() {
|
||||
if strings.TrimSpace(other.TokenID) == tokenID {
|
||||
tokenStillUsed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if host.TokenID != "" {
|
||||
tokenRevoked := false
|
||||
if tokenID != "" && !tokenStillUsed {
|
||||
tokenRevoked = m.config.RemoveAPIToken(tokenID)
|
||||
if tokenRevoked {
|
||||
m.config.SortAPITokens()
|
||||
m.config.APITokenEnabled = m.config.HasAPITokens()
|
||||
|
||||
if m.persistence != nil {
|
||||
if err := m.persistence.SaveAPITokens(m.config.APITokens); err != nil {
|
||||
log.Warn().Err(err).Str("tokenID", tokenID).Msg("Failed to persist API token revocation after host agent removal")
|
||||
} else {
|
||||
log.Info().Str("tokenID", tokenID).Str("tokenName", host.TokenName).Msg("API token revoked for removed host agent")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if tokenID != "" && tokenStillUsed {
|
||||
log.Info().
|
||||
Str("tokenID", tokenID).
|
||||
Str("hostID", hostID).
|
||||
Msg("API token still used by other agents; skipping revocation during host removal")
|
||||
}
|
||||
|
||||
if tokenID != "" {
|
||||
m.mu.Lock()
|
||||
if _, exists := m.hostTokenBindings[host.TokenID]; exists {
|
||||
delete(m.hostTokenBindings, host.TokenID)
|
||||
log.Debug().
|
||||
Str("tokenID", host.TokenID).
|
||||
Str("hostID", hostID).
|
||||
Msg("Unbound host agent token from removed host")
|
||||
if m.hostTokenBindings == nil {
|
||||
m.hostTokenBindings = make(map[string]string)
|
||||
}
|
||||
|
||||
if _, exists := m.hostTokenBindings[tokenID]; exists {
|
||||
delete(m.hostTokenBindings, tokenID)
|
||||
}
|
||||
|
||||
if hostname != "" {
|
||||
key := fmt.Sprintf("%s:%s", tokenID, hostname)
|
||||
if _, exists := m.hostTokenBindings[key]; exists {
|
||||
delete(m.hostTokenBindings, key)
|
||||
}
|
||||
}
|
||||
|
||||
if tokenRevoked {
|
||||
prefix := tokenID + ":"
|
||||
for key := range m.hostTokenBindings {
|
||||
if strings.HasPrefix(key, prefix) {
|
||||
delete(m.hostTokenBindings, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
log.Debug().
|
||||
Str("tokenID", tokenID).
|
||||
Str("hostID", hostID).
|
||||
Bool("revoked", tokenRevoked).
|
||||
Msg("Unbound host agent token bindings after host removal")
|
||||
}
|
||||
|
||||
m.state.RemoveConnectionHealth(hostConnectionPrefix + hostID)
|
||||
|
|
@ -1390,10 +1438,11 @@ func (m *Monitor) RebuildTokenBindings() {
|
|||
if _, valid := validTokens[tokenID]; !valid {
|
||||
continue
|
||||
}
|
||||
// Use host ID as the binding identifier
|
||||
if host.ID != "" {
|
||||
newHostBindings[tokenID] = host.ID
|
||||
hostname := strings.TrimSpace(host.Hostname)
|
||||
if hostname == "" || host.ID == "" {
|
||||
continue
|
||||
}
|
||||
newHostBindings[fmt.Sprintf("%s:%s", tokenID, hostname)] = host.ID
|
||||
}
|
||||
|
||||
// Log what changed
|
||||
|
|
@ -1925,17 +1974,9 @@ func (m *Monitor) ApplyHostReport(report agentshost.Report, tokenRecord *config.
|
|||
|
||||
existingHosts := m.state.GetHosts()
|
||||
|
||||
agentID := strings.TrimSpace(report.Agent.ID)
|
||||
if agentID == "" {
|
||||
agentID = identifier
|
||||
}
|
||||
|
||||
if tokenRecord != nil && tokenRecord.ID != "" {
|
||||
tokenID := strings.TrimSpace(tokenRecord.ID)
|
||||
bindingID := agentID
|
||||
if bindingID == "" {
|
||||
bindingID = identifier
|
||||
}
|
||||
bindingID := identifier
|
||||
|
||||
m.mu.Lock()
|
||||
if m.hostTokenBindings == nil {
|
||||
|
|
@ -1944,22 +1985,20 @@ func (m *Monitor) ApplyHostReport(report agentshost.Report, tokenRecord *config.
|
|||
// Bind tokens by hostname rather than agent ID. This allows:
|
||||
// - Same host to reconnect after agent reinstall (agent ID changes but hostname doesn't)
|
||||
// - Multiple hosts to use the same token (each hostname gets its own binding entry)
|
||||
// - Prevents a stolen token from being used on a different hostname
|
||||
bindingKey := fmt.Sprintf("%s:%s", tokenID, hostname)
|
||||
if boundID, exists := m.hostTokenBindings[bindingKey]; exists && boundID != bindingID {
|
||||
// Same token+hostname but different agent ID - this is a reinstall, allow it
|
||||
log.Info().
|
||||
Str("tokenID", tokenID).
|
||||
Str("hostname", hostname).
|
||||
Str("oldAgentID", boundID).
|
||||
Str("newAgentID", bindingID).
|
||||
Msg("Host agent reinstalled - updating token binding")
|
||||
Str("previousHostID", boundID).
|
||||
Str("newHostID", bindingID).
|
||||
Msg("Host agent identity changed for token binding; updating binding")
|
||||
m.hostTokenBindings[bindingKey] = bindingID
|
||||
} else if !exists {
|
||||
m.hostTokenBindings[bindingKey] = bindingID
|
||||
log.Debug().
|
||||
Str("tokenID", tokenID).
|
||||
Str("agentID", bindingID).
|
||||
Str("hostID", bindingID).
|
||||
Str("hostname", hostname).
|
||||
Msg("Bound host agent token to hostname")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ func TestEvaluateHostAgentsClearsAlertWhenHostReturns(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestApplyHostReportRejectsTokenReuseAcrossAgents(t *testing.T) {
|
||||
func TestApplyHostReportAllowsTokenReuseAcrossHosts(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
monitor := &Monitor{
|
||||
|
|
@ -164,8 +164,20 @@ func TestApplyHostReportRejectsTokenReuseAcrossAgents(t *testing.T) {
|
|||
secondReport.Host.Hostname = "host-two"
|
||||
secondReport.Timestamp = now.Add(30 * time.Second)
|
||||
|
||||
if _, err := monitor.ApplyHostReport(secondReport, token); err == nil {
|
||||
t.Fatalf("expected token reuse across agents to be rejected")
|
||||
hostTwo, err := monitor.ApplyHostReport(secondReport, token)
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyHostReport hostTwo: %v", err)
|
||||
}
|
||||
if hostTwo.ID == "" {
|
||||
t.Fatalf("expected hostTwo to have an identifier")
|
||||
}
|
||||
if hostTwo.ID == hostOne.ID {
|
||||
t.Fatalf("expected different host IDs for different machines, got %q", hostTwo.ID)
|
||||
}
|
||||
|
||||
snapshot := monitor.state.GetSnapshot()
|
||||
if got := len(snapshot.Hosts); got != 2 {
|
||||
t.Fatalf("expected 2 hosts in state, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -187,15 +199,19 @@ func TestRemoveHostAgentUnbindsToken(t *testing.T) {
|
|||
Hostname: "remove.me",
|
||||
TokenID: tokenID,
|
||||
})
|
||||
monitor.hostTokenBindings[tokenID] = "agent-remove"
|
||||
monitor.hostTokenBindings[tokenID+":remove.me"] = hostID
|
||||
monitor.hostTokenBindings[tokenID] = hostID
|
||||
|
||||
if _, err := monitor.RemoveHostAgent(hostID); err != nil {
|
||||
t.Fatalf("RemoveHostAgent: %v", err)
|
||||
}
|
||||
|
||||
if _, exists := monitor.hostTokenBindings[tokenID]; exists {
|
||||
if _, exists := monitor.hostTokenBindings[tokenID+":remove.me"]; exists {
|
||||
t.Fatalf("expected token binding to be cleared after host removal")
|
||||
}
|
||||
if _, exists := monitor.hostTokenBindings[tokenID]; exists {
|
||||
t.Fatalf("expected legacy token binding to be cleared after host removal")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateHostAgentsEmptyHostsList(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -8,11 +8,22 @@
|
|||
#
|
||||
# Options:
|
||||
# --enable-host Enable host metrics (default: true)
|
||||
# --enable-docker Enable docker metrics (default: false)
|
||||
# --enable-kubernetes Enable Kubernetes metrics (default: false)
|
||||
# --enable-docker Force enable Docker monitoring (default: auto-detect)
|
||||
# --disable-docker Disable Docker monitoring even if detected
|
||||
# --enable-kubernetes Force enable Kubernetes monitoring (default: auto-detect)
|
||||
# --disable-kubernetes Disable Kubernetes monitoring even if detected
|
||||
# --enable-proxmox Force enable Proxmox integration (default: auto-detect)
|
||||
# --disable-proxmox Disable Proxmox integration even if detected
|
||||
# --interval <dur> Reporting interval (default: 30s)
|
||||
# --agent-id <id> Custom agent identifier (default: auto-generated)
|
||||
# --insecure Skip TLS certificate verification
|
||||
# --uninstall Remove the agent
|
||||
#
|
||||
# Auto-Detection:
|
||||
# The installer automatically detects Docker, Kubernetes, and Proxmox on the
|
||||
# target machine and enables monitoring for detected platforms. Use --disable-*
|
||||
# flags to skip specific platforms, or --enable-* to force enable even if not
|
||||
# detected.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
|
|
@ -54,14 +65,19 @@ PULSE_URL=""
|
|||
PULSE_TOKEN=""
|
||||
INTERVAL="30s"
|
||||
ENABLE_HOST="true"
|
||||
ENABLE_DOCKER="false"
|
||||
ENABLE_KUBERNETES="false"
|
||||
ENABLE_PROXMOX="false"
|
||||
ENABLE_DOCKER="" # Empty means "auto-detect"
|
||||
ENABLE_KUBERNETES="" # Empty means "auto-detect"
|
||||
ENABLE_PROXMOX="" # Empty means "auto-detect"
|
||||
PROXMOX_TYPE=""
|
||||
UNINSTALL="false"
|
||||
INSECURE="false"
|
||||
AGENT_ID=""
|
||||
|
||||
# Track if flags were explicitly set (to override auto-detection)
|
||||
DOCKER_EXPLICIT="false"
|
||||
KUBERNETES_EXPLICIT="false"
|
||||
PROXMOX_EXPLICIT="false"
|
||||
|
||||
# --- Helper Functions ---
|
||||
log_info() { printf "[INFO] %s\n" "$1"; }
|
||||
log_warn() { printf "[WARN] %s\n" "$1"; }
|
||||
|
|
@ -76,6 +92,63 @@ fail() {
|
|||
exit 1
|
||||
}
|
||||
|
||||
# --- Auto-Detection Functions ---
|
||||
detect_docker() {
|
||||
# Check if Docker is available and accessible
|
||||
if command -v docker &>/dev/null; then
|
||||
# Try to connect to Docker daemon
|
||||
if docker info &>/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
# Also check for Podman (Docker-compatible)
|
||||
if command -v podman &>/dev/null; then
|
||||
if podman info &>/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
detect_kubernetes() {
|
||||
# Check for kubectl and cluster access
|
||||
if command -v kubectl &>/dev/null; then
|
||||
# Try to connect to cluster (quick timeout)
|
||||
if timeout 3 kubectl cluster-info &>/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
# Check for kubeconfig file
|
||||
if [[ -f "${HOME}/.kube/config" ]] || [[ -f "/etc/kubernetes/admin.conf" ]]; then
|
||||
return 0
|
||||
fi
|
||||
# Check if running inside a Kubernetes pod
|
||||
if [[ -f "/var/run/secrets/kubernetes.io/serviceaccount/token" ]]; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
detect_proxmox() {
|
||||
# Check for Proxmox VE
|
||||
if [[ -d "/etc/pve" ]]; then
|
||||
return 0
|
||||
fi
|
||||
# Check for Proxmox Backup Server
|
||||
if [[ -d "/etc/proxmox-backup" ]]; then
|
||||
return 0
|
||||
fi
|
||||
# Check for pveversion command
|
||||
if command -v pveversion &>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
# Check for proxmox-backup-manager command
|
||||
if command -v proxmox-backup-manager &>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# Build exec args string for use in service files
|
||||
# Returns via EXEC_ARGS variable
|
||||
build_exec_args() {
|
||||
|
|
@ -120,11 +193,12 @@ while [[ $# -gt 0 ]]; do
|
|||
--interval) INTERVAL="$2"; shift 2 ;;
|
||||
--enable-host) ENABLE_HOST="true"; shift ;;
|
||||
--disable-host) ENABLE_HOST="false"; shift ;;
|
||||
--enable-docker) ENABLE_DOCKER="true"; shift ;;
|
||||
--disable-docker) ENABLE_DOCKER="false"; shift ;;
|
||||
--enable-kubernetes) ENABLE_KUBERNETES="true"; shift ;;
|
||||
--disable-kubernetes) ENABLE_KUBERNETES="false"; shift ;;
|
||||
--enable-proxmox) ENABLE_PROXMOX="true"; shift ;;
|
||||
--enable-docker) ENABLE_DOCKER="true"; DOCKER_EXPLICIT="true"; shift ;;
|
||||
--disable-docker) ENABLE_DOCKER="false"; DOCKER_EXPLICIT="true"; shift ;;
|
||||
--enable-kubernetes) ENABLE_KUBERNETES="true"; KUBERNETES_EXPLICIT="true"; shift ;;
|
||||
--disable-kubernetes) ENABLE_KUBERNETES="false"; KUBERNETES_EXPLICIT="true"; shift ;;
|
||||
--enable-proxmox) ENABLE_PROXMOX="true"; PROXMOX_EXPLICIT="true"; shift ;;
|
||||
--disable-proxmox) ENABLE_PROXMOX="false"; PROXMOX_EXPLICIT="true"; shift ;;
|
||||
--proxmox-type) PROXMOX_TYPE="$2"; shift 2 ;;
|
||||
--insecure) INSECURE="true"; shift ;;
|
||||
--uninstall) UNINSTALL="true"; shift ;;
|
||||
|
|
@ -133,6 +207,47 @@ while [[ $# -gt 0 ]]; do
|
|||
esac
|
||||
done
|
||||
|
||||
# --- Platform Auto-Detection ---
|
||||
# Only auto-detect if flags weren't explicitly set
|
||||
log_info "Detecting available platforms..."
|
||||
|
||||
if [[ "$DOCKER_EXPLICIT" != "true" ]]; then
|
||||
if detect_docker; then
|
||||
log_info "Docker/Podman detected - enabling container monitoring"
|
||||
log_info " (use --disable-docker to skip)"
|
||||
ENABLE_DOCKER="true"
|
||||
else
|
||||
ENABLE_DOCKER="false"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$KUBERNETES_EXPLICIT" != "true" ]]; then
|
||||
if detect_kubernetes; then
|
||||
log_info "Kubernetes detected - enabling cluster monitoring"
|
||||
log_info " (use --disable-kubernetes to skip)"
|
||||
ENABLE_KUBERNETES="true"
|
||||
else
|
||||
ENABLE_KUBERNETES="false"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$PROXMOX_EXPLICIT" != "true" ]]; then
|
||||
if detect_proxmox; then
|
||||
log_info "Proxmox detected - enabling Proxmox integration"
|
||||
log_info " (use --disable-proxmox to skip)"
|
||||
ENABLE_PROXMOX="true"
|
||||
else
|
||||
ENABLE_PROXMOX="false"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Summary of what will be monitored
|
||||
log_info "Monitoring configuration:"
|
||||
log_info " Host metrics: $ENABLE_HOST"
|
||||
log_info " Docker/Podman: $ENABLE_DOCKER"
|
||||
log_info " Kubernetes: $ENABLE_KUBERNETES"
|
||||
log_info " Proxmox: $ENABLE_PROXMOX"
|
||||
|
||||
# --- Uninstall Logic ---
|
||||
if [[ "$UNINSTALL" == "true" ]]; then
|
||||
log_info "Uninstalling ${AGENT_NAME} and cleaning up legacy agents..."
|
||||
|
|
|
|||
Loading…
Reference in a new issue