package collector import ( "crypto/tls" "fmt" "io" "net" "net/http" "strings" "sync" "time" ) // Client is an HTTP client for the Proxmox VE API. // It supports multi-host failover: hosts are tried in order, and the last // successful host is remembered for subsequent requests. type Client struct { hosts []string token string maxConc int httpClient *http.Client lastGoodHost int mu sync.Mutex } // NewClient creates a new PVE API client. // hosts is a list of base URLs (e.g. "https://pve1.example.com:8006"). // token is the PVE API token in the form "USER@REALM!TOKENID=SECRET". // tlsInsecure disables TLS certificate verification when true. // maxConcurrent controls the maximum number of concurrent API requests. func NewClient(hosts []string, token string, tlsInsecure bool, maxConcurrent int) *Client { transport := &http.Transport{ DialContext: (&net.Dialer{ Timeout: 1 * time.Second, }).DialContext, TLSClientConfig: &tls.Config{ InsecureSkipVerify: tlsInsecure, }, MaxIdleConns: 10, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 5 * time.Second, } // Normalize hosts: strip trailing slashes for i, h := range hosts { hosts[i] = strings.TrimRight(h, "/") } // Normalize token: ensure it has the PVEAPIToken= prefix if !strings.HasPrefix(token, "PVEAPIToken=") { token = "PVEAPIToken=" + token } return &Client{ hosts: hosts, token: token, maxConc: maxConcurrent, httpClient: &http.Client{ Transport: transport, Timeout: 30 * time.Second, }, } } // MaxConcurrent returns the maximum number of concurrent API requests. func (c *Client) MaxConcurrent() int { return c.maxConc } // Get performs an HTTP GET against the PVE API. The path is prepended with // /api2/json (e.g. "/cluster/resources" becomes "/api2/json/cluster/resources"). // Hosts are tried in order starting from the last known good host. If a request // fails or returns a non-2xx status, the next host is tried. func (c *Client) Get(path string) ([]byte, error) { c.mu.Lock() startIdx := c.lastGoodHost c.mu.Unlock() fullPath := "/api2/json" + path var lastErr error for i := 0; i < len(c.hosts); i++ { idx := (startIdx + i) % len(c.hosts) host := c.hosts[idx] url := host + fullPath req, err := http.NewRequest("GET", url, nil) if err != nil { lastErr = fmt.Errorf("creating request for %s: %w", url, err) continue } req.Header.Set("Authorization", c.token) resp, err := c.httpClient.Do(req) if err != nil { lastErr = fmt.Errorf("request to %s: %w", host, err) continue } body, err := io.ReadAll(resp.Body) resp.Body.Close() if err != nil { lastErr = fmt.Errorf("reading response from %s: %w", host, err) continue } if resp.StatusCode < 200 || resp.StatusCode >= 300 { lastErr = fmt.Errorf("request to %s returned status %d", host, resp.StatusCode) continue } c.mu.Lock() c.lastGoodHost = idx c.mu.Unlock() return body, nil } return nil, fmt.Errorf("all PVE hosts failed: %w", lastErr) }