Fix HTTP mode for pulse-sensor-proxy and improve installer safety
## HTTP Server Fixes - Add source IP middleware to enforce allowed_source_subnets - Fix missing source subnet validation for external HTTP requests - HTTP health endpoint now respects subnet restrictions ## Installer Improvements - Auto-configure allowed_source_subnets with Pulse server IP - Add cluster node hostnames to allowed_nodes (not just IPs) - Fix node validation to accept both hostnames and IPs - Add Pulse server reachability check before installation - Add port availability check for HTTP mode - Add automatic rollback on service startup failure - Add HTTP endpoint health check after installation - Fix config backup and deduplication (prevent duplicate keys) - Fix IPv4 validation with loopback rejection - Improve registration retry logic with detailed errors - Add automatic LXC bind mount cleanup on uninstall ## Temperature Collection Fixes - Add local temperature collection for self-monitoring nodes - Fix node identifier matching (use hostname not SSH host) - Fix JSON double-encoding in HTTP client response Related to #XXX (temperature monitoring fixes)
This commit is contained in:
parent
71217dfae4
commit
e2bd514899
7 changed files with 410 additions and 40 deletions
|
|
@ -61,7 +61,7 @@ func (h *HTTPServer) Start() error {
|
|||
|
||||
h.server = &http.Server{
|
||||
Addr: h.config.HTTPListenAddr,
|
||||
Handler: h.rateLimitMiddleware(h.authMiddleware(mux)),
|
||||
Handler: h.sourceIPMiddleware(h.rateLimitMiddleware(h.authMiddleware(mux))),
|
||||
TLSConfig: tlsConfig,
|
||||
ReadTimeout: h.config.ReadTimeout,
|
||||
WriteTimeout: h.config.WriteTimeout,
|
||||
|
|
@ -130,6 +130,56 @@ func (h *HTTPServer) authMiddleware(next http.Handler) http.Handler {
|
|||
})
|
||||
}
|
||||
|
||||
// sourceIPMiddleware enforces allowed_source_subnets restrictions
|
||||
func (h *HTTPServer) sourceIPMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Extract client IP
|
||||
clientIP, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
clientIP = r.RemoteAddr
|
||||
}
|
||||
|
||||
// Parse client IP
|
||||
ip := net.ParseIP(clientIP)
|
||||
if ip == nil {
|
||||
log.Warn().Str("remote_addr", r.RemoteAddr).Msg("Failed to parse client IP")
|
||||
h.sendJSONError(w, http.StatusForbidden, "invalid source IP")
|
||||
if h.proxy.audit != nil {
|
||||
h.proxy.audit.LogHTTPRequest(r.RemoteAddr, r.Method, r.URL.Path, http.StatusForbidden, "invalid_source_ip")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Check if IP is in allowed subnets
|
||||
allowed := false
|
||||
for _, subnetStr := range h.config.AllowedSourceSubnets {
|
||||
_, subnet, err := net.ParseCIDR(subnetStr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if subnet.Contains(ip) {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !allowed {
|
||||
log.Warn().
|
||||
Str("client_ip", clientIP).
|
||||
Str("path", r.URL.Path).
|
||||
Msg("HTTP request from unauthorized source IP")
|
||||
h.sendJSONError(w, http.StatusForbidden, "source IP not allowed")
|
||||
if h.proxy.audit != nil {
|
||||
h.proxy.audit.LogHTTPRequest(r.RemoteAddr, r.Method, r.URL.Path, http.StatusForbidden, "source_ip_not_allowed")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// IP is allowed, proceed to next handler
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// rateLimitMiddleware applies rate limiting per client IP
|
||||
func (h *HTTPServer) rateLimitMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
|||
|
|
@ -508,6 +508,13 @@ func (p *Proxy) getTemperatureViaSSH(nodeHost string) (string, error) {
|
|||
startTime := time.Now()
|
||||
nodeLabel := sanitizeNodeLabel(nodeHost)
|
||||
|
||||
// Check if we're requesting the local node (self-monitoring)
|
||||
// For standalone nodes, avoid SSH and run sensors directly
|
||||
if isLocalNode(nodeHost) {
|
||||
log.Debug().Str("node", nodeHost).Msg("Self-monitoring detected, collecting temperatures locally")
|
||||
return p.getTemperatureLocal()
|
||||
}
|
||||
|
||||
privKeyPath := filepath.Join(p.sshKeyPath, "id_ed25519")
|
||||
if err := p.ensureHostKey(nodeHost); err != nil {
|
||||
p.metrics.sshRequests.WithLabelValues(nodeLabel, "error").Inc()
|
||||
|
|
@ -871,3 +878,81 @@ func isProxmoxHost() bool {
|
|||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isLocalNode checks if the requested node is the local machine
|
||||
func isLocalNode(nodeHost string) bool {
|
||||
// Get local hostname (short)
|
||||
hostname, err := os.Hostname()
|
||||
if err == nil {
|
||||
// Match short hostname
|
||||
if strings.EqualFold(nodeHost, hostname) {
|
||||
return true
|
||||
}
|
||||
// Match FQDN if nodeHost contains dots
|
||||
if strings.Contains(nodeHost, ".") {
|
||||
cmd := exec.Command("hostname", "-f")
|
||||
if output, err := cmd.Output(); err == nil {
|
||||
fqdn := strings.TrimSpace(string(output))
|
||||
if strings.EqualFold(nodeHost, fqdn) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if nodeHost is a local IP address
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, iface := range ifaces {
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
var ip net.IP
|
||||
switch v := addr.(type) {
|
||||
case *net.IPNet:
|
||||
ip = v.IP
|
||||
case *net.IPAddr:
|
||||
ip = v.IP
|
||||
}
|
||||
if ip != nil && ip.String() == nodeHost {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check special cases
|
||||
if nodeHost == "localhost" || nodeHost == "127.0.0.1" || nodeHost == "::1" {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// getTemperatureLocal collects temperature data from the local machine
|
||||
func (p *Proxy) getTemperatureLocal() (string, error) {
|
||||
// Run the same command that the wrapper script runs
|
||||
cmd := exec.Command("sensors", "-j")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
// Try without -j flag as fallback
|
||||
cmd = exec.Command("sensors")
|
||||
output, err = cmd.Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to run sensors: %w", err)
|
||||
}
|
||||
// Return empty JSON object for non-JSON output
|
||||
return "{}", nil
|
||||
}
|
||||
|
||||
result := strings.TrimSpace(string(output))
|
||||
if result == "" {
|
||||
return "{}", nil
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1298,6 +1298,7 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||
"/api/install/pulse-sensor-proxy", // Temperature proxy binary fallback
|
||||
"/api/install/install-docker.sh", // Docker turnkey installer
|
||||
"/api/system/proxy-public-key", // Temperature proxy public key for setup script
|
||||
"/api/temperature-proxy/register", // Temperature proxy registration (called by installer)
|
||||
}
|
||||
|
||||
// Also allow static assets without auth (JS, CSS, etc)
|
||||
|
|
|
|||
|
|
@ -80,6 +80,13 @@ func (h *TemperatureProxyHandlers) HandleRegister(w http.ResponseWriter, r *http
|
|||
for i := range nodesConfig.PVEInstances {
|
||||
instance := &nodesConfig.PVEInstances[i]
|
||||
|
||||
// Try to match by instance name
|
||||
if strings.EqualFold(instance.Name, hostname) {
|
||||
matchedInstance = instance
|
||||
matchedIndex = i
|
||||
break
|
||||
}
|
||||
|
||||
// Try to match by hostname in the Host field or cluster endpoints
|
||||
if strings.Contains(strings.ToLower(instance.Host), strings.ToLower(hostname)) {
|
||||
matchedInstance = instance
|
||||
|
|
|
|||
|
|
@ -106,7 +106,8 @@ func (tc *TemperatureCollector) CollectTemperatureWithProxy(ctx context.Context,
|
|||
if proxyURL != "" && proxyToken != "" {
|
||||
httpClient := tempproxy.NewHTTPClient(proxyURL, proxyToken)
|
||||
if httpClient.IsAvailable() {
|
||||
output, err = httpClient.GetTemperature(host)
|
||||
// Use nodeName for HTTP proxy (sensor-proxy needs short hostname, not IP)
|
||||
output, err = httpClient.GetTemperature(nodeName)
|
||||
if err != nil {
|
||||
log.Debug().
|
||||
Str("node", nodeName).
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ func NewHTTPClient(baseURL, authToken string) *HTTPClient {
|
|||
Timeout: defaultTimeout,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
InsecureSkipVerify: true, // Accept self-signed certificates from sensor-proxy
|
||||
},
|
||||
MaxIdleConns: 10,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
|
|
@ -147,8 +148,8 @@ func (c *HTTPClient) GetTemperature(nodeHost string) (string, error) {
|
|||
|
||||
// Parse JSON response
|
||||
var jsonResp struct {
|
||||
Node string `json:"node"`
|
||||
Temperature interface{} `json:"temperature"`
|
||||
Node string `json:"node"`
|
||||
Temperature string `json:"temperature"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &jsonResp); err != nil {
|
||||
|
|
@ -160,18 +161,8 @@ func (c *HTTPClient) GetTemperature(nodeHost string) (string, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// Convert temperature data back to JSON string (matching socket interface)
|
||||
tempJSON, err := json.Marshal(jsonResp.Temperature)
|
||||
if err != nil {
|
||||
return "", &ProxyError{
|
||||
Type: ErrorTypeTransport,
|
||||
Message: "failed to marshal temperature data",
|
||||
Retryable: false,
|
||||
Wrapped: err,
|
||||
}
|
||||
}
|
||||
|
||||
return string(tempJSON), nil
|
||||
// Temperature field is already a JSON string, return as-is
|
||||
return jsonResp.Temperature, nil
|
||||
}
|
||||
|
||||
// All retries exhausted
|
||||
|
|
|
|||
|
|
@ -242,6 +242,34 @@ EOF
|
|||
rm -f "$SOCKET_PATH" 2>/dev/null || true
|
||||
rm -rf "$RUNTIME_DIR" 2>/dev/null || true
|
||||
|
||||
# Always remove HTTP secrets and TLS material (security best practice)
|
||||
if [[ -f "/etc/pulse-sensor-proxy/.http-auth-token" ]]; then
|
||||
rm -f "/etc/pulse-sensor-proxy/.http-auth-token"
|
||||
print_success "Removed HTTP auth token"
|
||||
fi
|
||||
if [[ -d "/etc/pulse-sensor-proxy/tls" ]]; then
|
||||
rm -rf "/etc/pulse-sensor-proxy/tls"
|
||||
print_success "Removed TLS certificates"
|
||||
fi
|
||||
|
||||
# Check for and remove LXC bind mounts on any containers
|
||||
if command -v pct >/dev/null 2>&1; then
|
||||
print_info "Checking for LXC bind mounts..."
|
||||
# Find all containers with pulse-sensor-proxy bind mounts
|
||||
for ctid in $(pct list | awk 'NR>1 {print $1}'); do
|
||||
if grep -q "pulse-sensor-proxy" /etc/pve/lxc/${ctid}.conf 2>/dev/null; then
|
||||
CONTAINER_NAME=$(pct list | awk -v id="$ctid" '$1==id {print $3}')
|
||||
print_info "Found bind mount in container $ctid ($CONTAINER_NAME)"
|
||||
if sed -i '/pulse-sensor-proxy/d' /etc/pve/lxc/${ctid}.conf 2>/dev/null; then
|
||||
print_success "Removed bind mount from container $ctid ($CONTAINER_NAME)"
|
||||
print_warn "Container restart required for change to take effect"
|
||||
else
|
||||
print_warn "Failed to remove bind mount from container $ctid"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ "$PURGE" == true ]]; then
|
||||
print_info "Purging Pulse sensor proxy state"
|
||||
rm -rf "$WORK_DIR" "$CONFIG_DIR" 2>/dev/null || true
|
||||
|
|
@ -364,6 +392,30 @@ if [[ -n "$PULSE_SERVER" ]]; then
|
|||
FALLBACK_BASE="${PULSE_SERVER}/api/install/pulse-sensor-proxy"
|
||||
fi
|
||||
|
||||
# Preflight checks
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
print_error "This script must be run as root"
|
||||
print_error "Use: sudo $0 $*"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check required commands
|
||||
REQUIRED_CMDS="curl openssl systemctl useradd groupadd install chmod chown mkdir"
|
||||
if [[ "$HTTP_MODE" == true ]]; then
|
||||
REQUIRED_CMDS="$REQUIRED_CMDS hostname awk"
|
||||
fi
|
||||
if [[ "$STANDALONE" == false ]]; then
|
||||
REQUIRED_CMDS="$REQUIRED_CMDS pvecm"
|
||||
fi
|
||||
|
||||
for cmd in $REQUIRED_CMDS; do
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
print_error "Required command not found: $cmd"
|
||||
print_error "Please install it and try again"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Check if running on Proxmox host (only required for LXC mode)
|
||||
if [[ "$STANDALONE" == false ]]; then
|
||||
if ! command -v pvecm >/dev/null 2>&1; then
|
||||
|
|
@ -726,26 +778,59 @@ register_with_pulse() {
|
|||
local hostname="$2"
|
||||
local proxy_url="$3"
|
||||
|
||||
print_info "Registering temperature proxy with Pulse at $pulse_url..."
|
||||
# Output to stderr so it doesn't interfere with command substitution
|
||||
print_info "Registering temperature proxy with Pulse at $pulse_url..." >&2
|
||||
|
||||
# Build registration request
|
||||
# Build registration request with retry logic
|
||||
local response
|
||||
response=$(curl -f -s -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"hostname\":\"${hostname}\",\"proxy_url\":\"${proxy_url}\"}" \
|
||||
"${pulse_url}/api/temperature-proxy/register" 2>&1)
|
||||
local http_code
|
||||
local attempt
|
||||
local max_attempts=3
|
||||
local register_url="${pulse_url}/api/temperature-proxy/register"
|
||||
|
||||
if [[ $? -ne 0 ]]; then
|
||||
print_error "Failed to register with Pulse API"
|
||||
print_error "Response: $response"
|
||||
print_error ""
|
||||
print_error "Troubleshooting:"
|
||||
print_error " 1. Ensure Pulse server is running at $pulse_url"
|
||||
print_error " 2. Ensure this PVE instance is already added to Pulse"
|
||||
print_error " 3. Check hostname matches: $hostname"
|
||||
print_error " 4. Verify firewall allows access to Pulse"
|
||||
return 1
|
||||
fi
|
||||
for attempt in $(seq 1 $max_attempts); do
|
||||
if [[ $attempt -gt 1 ]]; then
|
||||
print_info "Retry attempt $attempt/$max_attempts..." >&2
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
# Capture both HTTP code and response body
|
||||
response=$(curl -w "\n%{http_code}" -f -s -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"hostname\":\"${hostname}\",\"proxy_url\":\"${proxy_url}\"}" \
|
||||
"$register_url" 2>&1)
|
||||
|
||||
local curl_exit=$?
|
||||
http_code=$(echo "$response" | tail -1)
|
||||
response=$(echo "$response" | head -n -1)
|
||||
|
||||
if [[ $curl_exit -eq 0 ]]; then
|
||||
break
|
||||
fi
|
||||
|
||||
if [[ $attempt -eq $max_attempts ]]; then
|
||||
print_error "Failed to register with Pulse API after $max_attempts attempts" >&2
|
||||
print_error "" >&2
|
||||
print_error "═══════════════════════════════════════════════════════" >&2
|
||||
print_error "Registration Details:" >&2
|
||||
print_error "═══════════════════════════════════════════════════════" >&2
|
||||
print_error "URL: $register_url" >&2
|
||||
print_error "HTTP Code: $http_code" >&2
|
||||
print_error "Hostname: $hostname" >&2
|
||||
print_error "Proxy URL: $proxy_url" >&2
|
||||
print_error "Response: $response" >&2
|
||||
print_error "" >&2
|
||||
print_error "Troubleshooting:" >&2
|
||||
print_error " 1. Ensure this PVE instance is added to Pulse first" >&2
|
||||
print_error " 2. Verify hostname matches instance name: $hostname" >&2
|
||||
print_error " 3. Check Pulse logs: docker logs pulse | tail -50" >&2
|
||||
print_error " 4. Test registration manually:" >&2
|
||||
print_error " curl -X POST -H 'Content-Type: application/json' \\" >&2
|
||||
print_error " -d '{\"hostname\":\"${hostname}\",\"proxy_url\":\"${proxy_url}\"}' \\" >&2
|
||||
print_error " $register_url" >&2
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Parse token from response
|
||||
local token
|
||||
|
|
@ -762,15 +847,17 @@ register_with_pulse() {
|
|||
chmod 600 /etc/pulse-sensor-proxy/.http-auth-token
|
||||
chown pulse-sensor-proxy:pulse-sensor-proxy /etc/pulse-sensor-proxy/.http-auth-token
|
||||
|
||||
print_success "Registered successfully - token received"
|
||||
# Output to stderr so it doesn't interfere with command substitution
|
||||
print_success "Registered successfully - token received" >&2
|
||||
|
||||
# Parse instance name from response for logging
|
||||
local instance_name
|
||||
instance_name=$(echo "$response" | grep -o '"pve_instance":"[^"]*"' | cut -d'"' -f4)
|
||||
if [[ -n "$instance_name" ]]; then
|
||||
print_info "Linked to PVE instance: $instance_name"
|
||||
print_info "Linked to PVE instance: $instance_name" >&2
|
||||
fi
|
||||
|
||||
# Return token for caller to use (stdout only)
|
||||
echo "$token"
|
||||
}
|
||||
|
||||
|
|
@ -806,22 +893,110 @@ if [[ "$HTTP_MODE" == true ]]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
# Test Pulse server reachability before proceeding
|
||||
print_info "Testing connection to Pulse server..."
|
||||
if ! curl -f -s -m 5 "${PULSE_SERVER}/api/health" >/dev/null 2>&1; then
|
||||
print_error "Cannot reach Pulse server at: $PULSE_SERVER"
|
||||
print_error ""
|
||||
print_error "Troubleshooting:"
|
||||
print_error " 1. Verify Pulse is running: docker ps | grep pulse"
|
||||
print_error " 2. Check URL is correct (include protocol and port)"
|
||||
print_error " 3. Test connectivity: curl -v ${PULSE_SERVER}/api/health"
|
||||
print_error " 4. Check firewall allows access from this host"
|
||||
print_error ""
|
||||
print_error "Installation aborted to prevent incomplete setup"
|
||||
exit 1
|
||||
fi
|
||||
print_success "Pulse server is reachable"
|
||||
|
||||
# Check if port is already in use
|
||||
PORT_NUMBER="${HTTP_ADDR#:}"
|
||||
if ss -ltn | grep -q ":${PORT_NUMBER} "; then
|
||||
print_error "Port ${PORT_NUMBER} is already in use"
|
||||
print_error ""
|
||||
print_error "Currently using port ${PORT_NUMBER}:"
|
||||
ss -ltnp | grep ":${PORT_NUMBER} " || true
|
||||
print_error ""
|
||||
print_error "Options:"
|
||||
print_error " 1. Stop the conflicting service"
|
||||
print_error " 2. Use a different port: --http-addr :PORT"
|
||||
print_error " 3. If this is a previous sensor-proxy, uninstall first:"
|
||||
print_error " $0 --uninstall"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Setup TLS certificates
|
||||
setup_tls_certificates "" "" # Empty params = auto-generate
|
||||
|
||||
# Determine proxy URL
|
||||
PROXY_HOSTNAME=$(hostname -f 2>/dev/null || hostname)
|
||||
PROXY_URL="https://${PROXY_HOSTNAME}${HTTP_ADDR}"
|
||||
# Determine proxy URL - use IP address for reliable network access
|
||||
PRIMARY_IP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||
if [[ -z "$PRIMARY_IP" ]]; then
|
||||
print_error "Failed to determine primary IP address"
|
||||
print_error "Use --proxy-url to specify manually"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate it's an IPv4 address (not IPv6)
|
||||
if [[ ! "$PRIMARY_IP" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
print_warn "Primary IP appears to be IPv6 or invalid: $PRIMARY_IP"
|
||||
print_warn "Attempting to find first IPv4 address..."
|
||||
PRIMARY_IP=$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -1)
|
||||
if [[ -z "$PRIMARY_IP" ]]; then
|
||||
print_error "No IPv4 address found"
|
||||
print_error "Use --proxy-url https://YOUR_IP${HTTP_ADDR} to specify manually"
|
||||
exit 1
|
||||
fi
|
||||
print_info "Using IPv4 address: $PRIMARY_IP"
|
||||
fi
|
||||
|
||||
# Warn if using loopback
|
||||
if [[ "$PRIMARY_IP" == "127.0.0.1" ]]; then
|
||||
print_warn "Primary IP is loopback (127.0.0.1)"
|
||||
print_warn "Pulse will not be able to reach this proxy!"
|
||||
print_error "Use --proxy-url https://YOUR_REAL_IP${HTTP_ADDR} to specify a reachable address"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PROXY_URL="https://${PRIMARY_IP}${HTTP_ADDR}"
|
||||
print_info "Proxy will be accessible at: $PROXY_URL"
|
||||
|
||||
# Register with Pulse and get auth token
|
||||
HTTP_AUTH_TOKEN=$(register_with_pulse "$PULSE_SERVER" "$PROXY_HOSTNAME" "$PROXY_URL")
|
||||
# Use short hostname for registration matching (PVE cluster endpoints use short names)
|
||||
SHORT_HOSTNAME=$(hostname -s 2>/dev/null || hostname | cut -d'.' -f1)
|
||||
HTTP_AUTH_TOKEN=$(register_with_pulse "$PULSE_SERVER" "$SHORT_HOSTNAME" "$PROXY_URL")
|
||||
if [[ $? -ne 0 || -z "$HTTP_AUTH_TOKEN" ]]; then
|
||||
print_error "Failed to register with Pulse - aborting installation"
|
||||
print_error "Fix the issue and re-run the installer"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Backup config before modifying
|
||||
if [[ -f /etc/pulse-sensor-proxy/config.yaml ]]; then
|
||||
BACKUP_CONFIG="/etc/pulse-sensor-proxy/config.yaml.backup.$(date +%s)"
|
||||
cp /etc/pulse-sensor-proxy/config.yaml "$BACKUP_CONFIG"
|
||||
print_info "Config backed up to: $BACKUP_CONFIG"
|
||||
|
||||
# Remove any existing HTTP configuration to prevent duplicates
|
||||
if grep -q "^# HTTP Mode Configuration" /etc/pulse-sensor-proxy/config.yaml; then
|
||||
print_info "Removing existing HTTP configuration..."
|
||||
# Remove from "# HTTP Mode Configuration" to end of file
|
||||
sed -i '/^# HTTP Mode Configuration/,$ d' /etc/pulse-sensor-proxy/config.yaml
|
||||
fi
|
||||
fi
|
||||
|
||||
# Extract Pulse server IP/hostname for allowed_source_subnets
|
||||
# Remove protocol and port to get just the host
|
||||
PULSE_HOST=$(echo "$PULSE_SERVER" | sed -E 's#^https?://##' | sed -E 's#:[0-9]+$##')
|
||||
|
||||
# Try to resolve to IP if it's a hostname
|
||||
PULSE_IP=$(getent hosts "$PULSE_HOST" 2>/dev/null | awk '{print $1; exit}')
|
||||
if [[ -z "$PULSE_IP" ]]; then
|
||||
# Fallback: assume PULSE_HOST is already an IP or use it as-is
|
||||
PULSE_IP="$PULSE_HOST"
|
||||
fi
|
||||
|
||||
print_info "Pulse server detected at: $PULSE_IP"
|
||||
|
||||
# Append HTTP configuration to config.yaml
|
||||
print_info "Configuring HTTP mode..."
|
||||
cat >> /etc/pulse-sensor-proxy/config.yaml << EOF
|
||||
|
|
@ -832,6 +1007,10 @@ http_listen_addr: "$HTTP_ADDR"
|
|||
http_tls_cert: /etc/pulse-sensor-proxy/tls/server.crt
|
||||
http_tls_key: /etc/pulse-sensor-proxy/tls/server.key
|
||||
http_auth_token: "$HTTP_AUTH_TOKEN"
|
||||
|
||||
# Allow HTTP connections from Pulse server only
|
||||
allowed_source_subnets:
|
||||
- $PULSE_IP/32
|
||||
EOF
|
||||
|
||||
print_success "HTTP mode configured successfully"
|
||||
|
|
@ -999,6 +1178,27 @@ fi
|
|||
if ! systemctl restart pulse-sensor-proxy.service; then
|
||||
print_error "Failed to start pulse-sensor-proxy service"
|
||||
print_error ""
|
||||
|
||||
# Attempt rollback if HTTP mode and we have a backup
|
||||
if [[ "$HTTP_MODE" == true && -n "$BACKUP_CONFIG" && -f "$BACKUP_CONFIG" ]]; then
|
||||
print_warn "Attempting to rollback to previous configuration..."
|
||||
if cp "$BACKUP_CONFIG" /etc/pulse-sensor-proxy/config.yaml; then
|
||||
print_info "Config restored from backup"
|
||||
if systemctl restart pulse-sensor-proxy.service; then
|
||||
print_success "Service restarted with previous configuration"
|
||||
print_error ""
|
||||
print_error "HTTP mode installation failed but previous config restored"
|
||||
print_error "Temperature monitoring should still work via Unix socket"
|
||||
print_error "Review the error above and fix before retrying"
|
||||
exit 1
|
||||
else
|
||||
print_error "Rollback failed - service won't start even with old config"
|
||||
fi
|
||||
else
|
||||
print_error "Failed to restore config from backup"
|
||||
fi
|
||||
fi
|
||||
|
||||
print_error "═══════════════════════════════════════════════════════"
|
||||
print_error "Service Status:"
|
||||
print_error "═══════════════════════════════════════════════════════"
|
||||
|
|
@ -1016,6 +1216,7 @@ if ! systemctl restart pulse-sensor-proxy.service; then
|
|||
print_error "2. Permission errors: Check ownership of /var/lib/pulse-sensor-proxy"
|
||||
print_error "3. lm-sensors not installed: Run 'apt-get install lm-sensors && sensors-detect --auto'"
|
||||
print_error "4. Standalone node detection: If you see 'pvecm' errors, this is expected for non-clustered hosts"
|
||||
print_error "5. Port already in use: Check 'ss -tlnp | grep ${HTTP_ADDR#:}'"
|
||||
print_error ""
|
||||
print_error "For more help: https://github.com/rcourtman/Pulse/blob/main/docs/TROUBLESHOOTING.md"
|
||||
exit 1
|
||||
|
|
@ -1056,6 +1257,33 @@ fi
|
|||
|
||||
print_info "Socket ready at $SOCKET_PATH"
|
||||
|
||||
# Validate HTTP endpoint if HTTP mode is enabled
|
||||
if [[ "$HTTP_MODE" == true ]]; then
|
||||
print_info "Validating HTTP endpoint..."
|
||||
|
||||
# Wait a moment for HTTP server to fully start
|
||||
sleep 2
|
||||
|
||||
# Test HTTP endpoint
|
||||
HTTP_CHECK_URL="https://${PRIMARY_IP}${HTTP_ADDR}/health"
|
||||
if curl -f -s -k -m 5 \
|
||||
-H "Authorization: Bearer ${HTTP_AUTH_TOKEN}" \
|
||||
"$HTTP_CHECK_URL" >/dev/null 2>&1; then
|
||||
print_success "HTTP endpoint validated successfully"
|
||||
else
|
||||
print_error "HTTP endpoint validation failed"
|
||||
print_error "URL: $HTTP_CHECK_URL"
|
||||
print_error ""
|
||||
print_error "Troubleshooting:"
|
||||
print_error " 1. Check if port ${HTTP_ADDR#:} is listening: ss -tlnp | grep ${HTTP_ADDR#:}"
|
||||
print_error " 2. Check sensor-proxy logs: journalctl -u pulse-sensor-proxy -n 50"
|
||||
print_error " 3. Test manually: curl -k -H 'Authorization: Bearer \$TOKEN' $HTTP_CHECK_URL"
|
||||
print_error ""
|
||||
print_warn "Service is running but HTTP endpoint may not be accessible"
|
||||
print_warn "Temperature monitoring may not work properly"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Install sensor wrapper script for combined sensor and SMART data collection
|
||||
print_info "Installing sensor wrapper script..."
|
||||
cat > "$WRAPPER_SCRIPT" << 'WRAPPER_EOF'
|
||||
|
|
@ -1454,6 +1682,9 @@ if command -v pvecm >/dev/null 2>&1; then
|
|||
# Extract node IPs from pvecm status
|
||||
CLUSTER_NODES=$(pvecm status 2>/dev/null | awk '/0x[0-9a-f]+.*[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/ {print $3}' || true)
|
||||
|
||||
# Extract node names from pvecm nodes (for allowlist)
|
||||
CLUSTER_NODE_NAMES=$(pvecm nodes 2>/dev/null | awk 'NR>2 {print $3}' || true)
|
||||
|
||||
if [[ -n "$CLUSTER_NODES" ]]; then
|
||||
print_info "Discovered cluster nodes: $(echo $CLUSTER_NODES | tr '\n' ' ')"
|
||||
|
||||
|
|
@ -1539,9 +1770,13 @@ if command -v pvecm >/dev/null 2>&1; then
|
|||
# These nodes are allowed to request temperature data when cluster IPC validation is unavailable
|
||||
allowed_nodes:
|
||||
EOF
|
||||
# Add both IPs and hostnames to allow-list
|
||||
for node_ip in $CLUSTER_NODES; do
|
||||
echo " - $node_ip" >> /etc/pulse-sensor-proxy/config.yaml
|
||||
done
|
||||
for node_name in $CLUSTER_NODE_NAMES; do
|
||||
echo " - $node_name" >> /etc/pulse-sensor-proxy/config.yaml
|
||||
done
|
||||
else
|
||||
# No cluster found - configure standalone node
|
||||
print_info "No cluster detected, configuring standalone node..."
|
||||
|
|
|
|||
Loading…
Reference in a new issue