Improvements to pulse-sensor-proxy: - Fix cluster discovery to use pvecm status for IP addresses instead of node names - Add standalone node support for non-clustered Proxmox hosts - Enhanced SSH key push with detailed logging, success/failure tracking, and error reporting - Add --pulse-server flag to installer for custom Pulse URLs - Configure www-data group membership for Proxmox IPC access UI and API cleanup: - Remove unused "Ensure cluster keys" button from Settings - Remove /api/diagnostics/temperature-proxy/ensure-cluster-keys endpoint - Remove EnsureClusterKeys method from tempproxy client The setup script already handles SSH key distribution during initial configuration, making the manual refresh button redundant.
172 lines
4.1 KiB
Go
172 lines
4.1 KiB
Go
package tempproxy
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
const (
|
|
defaultSocketPath = "/run/pulse-sensor-proxy/pulse-sensor-proxy.sock"
|
|
containerSocketPath = "/mnt/pulse-proxy/pulse-sensor-proxy.sock"
|
|
defaultTimeout = 30 * time.Second // Increased to accommodate SSH operations
|
|
)
|
|
|
|
// Client communicates with pulse-sensor-proxy via unix socket
|
|
type Client struct {
|
|
socketPath string
|
|
timeout time.Duration
|
|
}
|
|
|
|
// NewClient creates a new proxy client
|
|
func NewClient() *Client {
|
|
socketPath := os.Getenv("PULSE_SENSOR_PROXY_SOCKET")
|
|
if socketPath == "" {
|
|
if _, err := os.Stat(defaultSocketPath); err == nil {
|
|
socketPath = defaultSocketPath
|
|
} else if _, err := os.Stat(containerSocketPath); err == nil {
|
|
socketPath = containerSocketPath
|
|
} else {
|
|
socketPath = defaultSocketPath
|
|
}
|
|
}
|
|
|
|
return &Client{
|
|
socketPath: socketPath,
|
|
timeout: defaultTimeout,
|
|
}
|
|
}
|
|
|
|
// IsAvailable checks if the proxy is running and accessible
|
|
func (c *Client) IsAvailable() bool {
|
|
_, err := os.Stat(c.socketPath)
|
|
return err == nil
|
|
}
|
|
|
|
// RPCRequest represents a request to the proxy
|
|
type RPCRequest struct {
|
|
Method string `json:"method"`
|
|
Params map[string]interface{} `json:"params"`
|
|
}
|
|
|
|
// RPCResponse represents a response from the proxy
|
|
type RPCResponse struct {
|
|
Success bool `json:"success"`
|
|
Data map[string]interface{} `json:"data,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// call sends an RPC request and returns the response
|
|
func (c *Client) call(method string, params map[string]interface{}) (*RPCResponse, error) {
|
|
// Connect to unix socket
|
|
conn, err := net.DialTimeout("unix", c.socketPath, c.timeout)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to connect to proxy: %w", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
// Set deadline
|
|
conn.SetDeadline(time.Now().Add(c.timeout))
|
|
|
|
// Send request
|
|
req := RPCRequest{
|
|
Method: method,
|
|
Params: params,
|
|
}
|
|
|
|
encoder := json.NewEncoder(conn)
|
|
if err := encoder.Encode(req); err != nil {
|
|
return nil, fmt.Errorf("failed to encode request: %w", err)
|
|
}
|
|
|
|
// Read response (server uses newline-delimited framing, no CloseWrite needed)
|
|
var resp RPCResponse
|
|
decoder := json.NewDecoder(conn)
|
|
if err := decoder.Decode(&resp); err != nil {
|
|
return nil, fmt.Errorf("failed to decode response: %w", err)
|
|
}
|
|
|
|
return &resp, nil
|
|
}
|
|
|
|
// GetStatus returns proxy status
|
|
func (c *Client) GetStatus() (map[string]interface{}, error) {
|
|
resp, err := c.call("get_status", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !resp.Success {
|
|
return nil, fmt.Errorf("proxy error: %s", resp.Error)
|
|
}
|
|
|
|
return resp.Data, nil
|
|
}
|
|
|
|
// RegisterNodes returns list of discovered nodes with SSH status
|
|
func (c *Client) RegisterNodes() ([]map[string]interface{}, error) {
|
|
resp, err := c.call("register_nodes", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !resp.Success {
|
|
return nil, fmt.Errorf("proxy error: %s", resp.Error)
|
|
}
|
|
|
|
// Extract nodes array from data
|
|
nodesRaw, ok := resp.Data["nodes"]
|
|
if !ok {
|
|
return nil, fmt.Errorf("no nodes in response")
|
|
}
|
|
|
|
// Type assertion to []interface{} first, then convert
|
|
nodesArray, ok := nodesRaw.([]interface{})
|
|
if !ok {
|
|
return nil, fmt.Errorf("nodes is not an array")
|
|
}
|
|
|
|
nodes := make([]map[string]interface{}, len(nodesArray))
|
|
for i, nodeRaw := range nodesArray {
|
|
node, ok := nodeRaw.(map[string]interface{})
|
|
if !ok {
|
|
return nil, fmt.Errorf("node %d is not a map", i)
|
|
}
|
|
nodes[i] = node
|
|
}
|
|
|
|
return nodes, nil
|
|
}
|
|
|
|
// GetTemperature fetches temperature data from a specific node
|
|
func (c *Client) GetTemperature(nodeHost string) (string, error) {
|
|
params := map[string]interface{}{
|
|
"node": nodeHost,
|
|
}
|
|
|
|
resp, err := c.call("get_temperature", params)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if !resp.Success {
|
|
return "", fmt.Errorf("proxy error: %s", resp.Error)
|
|
}
|
|
|
|
// Extract temperature JSON string
|
|
tempRaw, ok := resp.Data["temperature"]
|
|
if !ok {
|
|
return "", fmt.Errorf("no temperature data in response")
|
|
}
|
|
|
|
tempStr, ok := tempRaw.(string)
|
|
if !ok {
|
|
return "", fmt.Errorf("temperature is not a string")
|
|
}
|
|
|
|
return tempStr, nil
|
|
}
|