fix: add cluster endpoint IP override and Windows agent download support

1. Add IPOverride field to ClusterEndpoint struct
   - Allows users to specify a custom IP that takes precedence over auto-discovered IPs
   - Fixes #929 and #1066 where Pulse used internal cluster IPs instead of management IPs
   - Added EffectiveIP() method to cleanly handle the override logic

2. Update connection code to use EffectiveIP()
   - monitor.go: Use override when building endpoint URLs
   - temperature_proxy.go: Use override for proxy connections

3. Add bare Windows EXE files to GitHub releases
   - Fixes #1064 where LXC/barebone installs couldn't download Windows agents
   - Modified build-release.sh to copy EXEs alongside ZIPs
   - Added EXEs to checksum generation
This commit is contained in:
rcourtman
2026-01-08 23:04:25 +00:00
parent 568aac6bd0
commit d5c93fd226
8 changed files with 4318 additions and 4224 deletions
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,5 @@
import { createSignal, Show, For, onMount, createMemo, onCleanup, createEffect } from 'solid-js';
import { Shield, CheckCircle, XCircle, AlertTriangle, RefreshCw, Filter, Info, Play, X } from 'lucide-solid';
import { Shield, CheckCircle, XCircle, RefreshCw, Filter, Info, Play, X } from 'lucide-solid';
import { showTooltip, hideTooltip } from '@/components/shared/Tooltip';
import Toggle from '@/components/shared/Toggle';
import {
File diff suppressed because it is too large Load Diff
+66
View File
@@ -0,0 +1,66 @@
import { createSignal, createMemo } from 'solid-js';
import { LicenseAPI, type LicenseStatus } from '@/api/license';
import { logger } from '@/utils/logger';
// Reactive signals for license status
const [licenseStatus, setLicenseStatus] = createSignal<LicenseStatus | null>(null);
const [loading, setLoading] = createSignal(false);
const [loaded, setLoaded] = createSignal(false);
/**
* Load the license status from the server.
*/
export async function loadLicenseStatus(force = false): Promise<void> {
if (loaded() && !force) return;
setLoading(true);
try {
const status = await LicenseAPI.getStatus();
setLicenseStatus(status);
setLoaded(true);
logger.debug('[licenseStore] License status loaded', { tier: status.tier, valid: status.valid });
} catch (err) {
logger.error('[licenseStore] Failed to load license status', err);
// Fallback to free tier on error to avoid breaking UI
setLicenseStatus({
valid: false,
tier: 'free',
is_lifetime: false,
days_remaining: 0,
features: [],
});
setLoaded(true);
} finally {
setLoading(false);
}
}
/**
* Helper to check if the current license is Pulse Pro or Enterprise.
*/
export const isPro = createMemo(() => {
const current = licenseStatus();
return Boolean(current?.valid && current.tier !== 'free');
});
/**
* Helper to check if the current license is Enterprise.
*/
export const isEnterprise = createMemo(() => {
const current = licenseStatus();
return Boolean(current?.valid && (current.tier === 'enterprise' || current.tier === 'msp'));
});
/**
* Check if a specific feature is enabled by the current license.
*/
export function hasFeature(feature: string): boolean {
const current = licenseStatus();
if (!current?.valid) return false;
return current.features.includes(feature);
}
/**
* Get the full license status.
*/
export { licenseStatus, loading as licenseLoading, loaded as licenseLoaded };
+2 -1
View File
@@ -150,7 +150,8 @@ func buildAuthorizedNodeList(instances []config.PVEInstance) []authorizedNode {
if instance.ClusterEndpoints != nil {
for _, ep := range instance.ClusterEndpoints {
name := ep.NodeName
ip := ep.IP
// Use EffectiveIP() which prefers IPOverride over auto-discovered IP
ip := ep.EffectiveIP()
if ip == "" {
ip = extractHostPart(ep.Host)
}
+10 -1
View File
@@ -458,7 +458,8 @@ type ClusterEndpoint struct {
NodeName string // Node name
Host string // Full URL (e.g., https://node1.lan:8006)
GuestURL string // Optional guest-accessible URL (for navigation)
IP string // IP address
IP string // IP address (auto-discovered from cluster)
IPOverride string // User-specified IP override (takes precedence over IP if set)
Fingerprint string // TLS certificate fingerprint (SHA256, auto-captured via TOFU)
Online bool // Current online status from Proxmox
LastSeen time.Time // Last successful connection
@@ -470,6 +471,14 @@ type ClusterEndpoint struct {
TemperatureProxyControlToken string // Control-plane token for this specific node
}
// EffectiveIP returns the IP to use for this endpoint, preferring IPOverride if set
func (e ClusterEndpoint) EffectiveIP() string {
if e.IPOverride != "" {
return e.IPOverride
}
return e.IP
}
// PBSInstance represents a Proxmox Backup Server connection
type PBSInstance struct {
Name string
+13 -6
View File
@@ -537,18 +537,21 @@ func clusterEndpointEffectiveURL(endpoint config.ClusterEndpoint, verifySSL bool
// bypasses hostname checks), prefer IP to reduce DNS lookups (refs #620).
requiresHostnameForTLS := verifySSL && !hasFingerprint
// Use EffectiveIP() which prefers user-specified IPOverride over auto-discovered IP
effectiveIP := endpoint.EffectiveIP()
if requiresHostnameForTLS {
// Prefer hostname for proper TLS certificate validation
if endpoint.Host != "" {
return ensureClusterEndpointURL(endpoint.Host)
}
if endpoint.IP != "" {
return ensureClusterEndpointURL(endpoint.IP)
if effectiveIP != "" {
return ensureClusterEndpointURL(effectiveIP)
}
} else {
// Prefer IP address to avoid excessive DNS lookups
if endpoint.IP != "" {
return ensureClusterEndpointURL(endpoint.IP)
if effectiveIP != "" {
return ensureClusterEndpointURL(effectiveIP)
}
if endpoint.Host != "" {
return ensureClusterEndpointURL(endpoint.Host)
@@ -3879,10 +3882,13 @@ func (m *Monitor) getConfiguredHostIPs() []string {
// Add PVE hosts
for _, pve := range m.config.PVEInstances {
addHost(pve.Host)
// Also add cluster endpoints
// Also add cluster endpoints (include both auto-discovered IP and override if set)
for _, ep := range pve.ClusterEndpoints {
addHost(ep.Host)
addHost(ep.IP)
if ep.IPOverride != "" && ep.IPOverride != ep.IP {
addHost(ep.IPOverride)
}
}
}
@@ -4265,7 +4271,8 @@ func (m *Monitor) retryFailedConnections(ctx context.Context) {
endpointFingerprints := make(map[string]string)
for _, ep := range pve.ClusterEndpoints {
host := ep.IP
// Use EffectiveIP() which prefers IPOverride over auto-discovered IP
host := ep.EffectiveIP()
if host == "" {
host = ep.Host
}
+13 -3
View File
@@ -344,11 +344,21 @@ tar -czf "$RELEASE_DIR/pulse-agent-v${VERSION}-darwin-arm64.tar.gz" -C "$BUILD_D
# FreeBSD
tar -czf "$RELEASE_DIR/pulse-agent-v${VERSION}-freebsd-amd64.tar.gz" -C "$BUILD_DIR" pulse-agent-freebsd-amd64
tar -czf "$RELEASE_DIR/pulse-agent-v${VERSION}-freebsd-arm64.tar.gz" -C "$BUILD_DIR" pulse-agent-freebsd-arm64
# Windows
# Windows (zip archives with version in filename)
zip -j "$RELEASE_DIR/pulse-agent-v${VERSION}-windows-amd64.zip" "$BUILD_DIR/pulse-agent-windows-amd64.exe"
zip -j "$RELEASE_DIR/pulse-agent-v${VERSION}-windows-arm64.zip" "$BUILD_DIR/pulse-agent-windows-arm64.exe"
zip -j "$RELEASE_DIR/pulse-agent-v${VERSION}-windows-386.zip" "$BUILD_DIR/pulse-agent-windows-386.exe"
# Also copy bare Windows EXEs for /releases/latest/download/ redirect compatibility
# These allow LXC/barebone installs to redirect to GitHub without needing versioned URLs
echo "Copying bare Windows EXEs to release directory for redirect compatibility..."
cp "$BUILD_DIR/pulse-agent-windows-amd64.exe" "$RELEASE_DIR/"
cp "$BUILD_DIR/pulse-agent-windows-arm64.exe" "$RELEASE_DIR/"
cp "$BUILD_DIR/pulse-agent-windows-386.exe" "$RELEASE_DIR/"
cp "$BUILD_DIR/pulse-host-agent-windows-amd64.exe" "$RELEASE_DIR/"
cp "$BUILD_DIR/pulse-host-agent-windows-arm64.exe" "$RELEASE_DIR/"
cp "$BUILD_DIR/pulse-host-agent-windows-386.exe" "$RELEASE_DIR/"
# Copy Windows, macOS, and FreeBSD binaries into universal tarball for /download/ endpoint
echo "Adding Windows, macOS, and FreeBSD binaries to universal tarball..."
cp "$BUILD_DIR/pulse-host-agent-darwin-amd64" "$universal_dir/bin/"
@@ -410,8 +420,8 @@ cp scripts/pulse-auto-update.sh "$RELEASE_DIR/"
# Generate checksums (include tarballs, zip files, helm chart, and install.sh)
cd "$RELEASE_DIR"
shopt -s nullglob extglob
# Match all tarballs, zip files, and install scripts
checksum_files=( *.tar.gz *.zip install.sh install-sensor-proxy.sh install-docker.sh pulse-auto-update.sh )
# Match all tarballs, zip files, exe files, and install scripts
checksum_files=( *.tar.gz *.zip *.exe install.sh install-sensor-proxy.sh install-docker.sh pulse-auto-update.sh )
if compgen -G "pulse-*.tgz" > /dev/null; then
checksum_files+=( pulse-*.tgz )
fi