maidn-cli/internal/proxmox/client.go

166 lines
4 KiB
Go

package proxmox
import (
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"net/url"
"sort"
"strings"
"time"
)
type Client struct {
baseURL string
tokenID string
tokenSecret string
httpClient *http.Client
}
type clusterNodesResponse struct {
Data []struct {
Node string `json:"node"`
Status string `json:"status"`
} `json:"data"`
}
type nodeStatusResponse struct {
Data struct {
CPU float64 `json:"cpu"`
Memory struct {
Used uint64 `json:"used"`
Total uint64 `json:"total"`
} `json:"memory"`
RootFS struct {
Avail uint64 `json:"avail"`
Total uint64 `json:"total"`
} `json:"rootfs"`
} `json:"data"`
}
type networkResponse struct {
Data []struct {
Iface string `json:"iface"`
Type string `json:"type"`
Active int `json:"active"`
} `json:"data"`
}
type storageResponse struct {
Data []struct {
Storage string `json:"storage"`
Type string `json:"type"`
Avail uint64 `json:"avail"`
Total uint64 `json:"total"`
} `json:"data"`
}
type NodeInfo struct {
Name string
Networks []string
Storages []StorageInfo
FreeMemoryMB int
TotalMemoryMB int
FreeDiskGB int
}
type StorageInfo struct {
Name string
Type string
FreeGB int
SupportsISO bool
}
func New(baseURL, tokenID, tokenSecret string, insecure bool) *Client {
transport := &http.Transport{}
if insecure {
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
return &Client{
baseURL: strings.TrimRight(baseURL, "/"),
tokenID: tokenID,
tokenSecret: tokenSecret,
httpClient: &http.Client{Timeout: 15 * time.Second, Transport: transport},
}
}
func (c *Client) Discover() ([]NodeInfo, error) {
var nodesResp clusterNodesResponse
if err := c.getJSON("/cluster/resources?type=node", &nodesResp); err != nil {
return nil, err
}
result := make([]NodeInfo, 0, len(nodesResp.Data))
for _, node := range nodesResp.Data {
if node.Node == "" || node.Status != "online" {
continue
}
info, err := c.describeNode(node.Node)
if err != nil {
return nil, err
}
result = append(result, info)
}
sort.Slice(result, func(i, j int) bool { return result[i].Name < result[j].Name })
return result, nil
}
func (c *Client) describeNode(node string) (NodeInfo, error) {
var status nodeStatusResponse
if err := c.getJSON("/nodes/"+node+"/status", &status); err != nil {
return NodeInfo{}, err
}
var networks networkResponse
if err := c.getJSON("/nodes/"+node+"/network", &networks); err != nil {
return NodeInfo{}, err
}
var storages storageResponse
if err := c.getJSON("/nodes/"+node+"/storage", &storages); err != nil {
return NodeInfo{}, err
}
info := NodeInfo{
Name: node,
FreeMemoryMB: int((status.Data.Memory.Total - status.Data.Memory.Used) / 1024 / 1024),
TotalMemoryMB: int(status.Data.Memory.Total / 1024 / 1024),
FreeDiskGB: int(status.Data.RootFS.Avail / 1024 / 1024 / 1024),
}
for _, net := range networks.Data {
if net.Active == 1 && (net.Type == "bridge" || net.Type == "eth") {
info.Networks = append(info.Networks, net.Iface)
}
}
for _, storage := range storages.Data {
info.Storages = append(info.Storages, StorageInfo{
Name: storage.Storage,
Type: storage.Type,
FreeGB: int(storage.Avail / 1024 / 1024 / 1024),
SupportsISO: storage.Type == "dir" || storage.Type == "nfs",
})
}
return info, nil
}
func (c *Client) getJSON(path string, target any) error {
request, err := http.NewRequest(http.MethodGet, c.baseURL+"/api2/json"+path, nil)
if err != nil {
return err
}
request.Header.Set("Authorization", fmt.Sprintf("PVEAPIToken=%s=%s", c.tokenID, c.tokenSecret))
response, err := c.httpClient.Do(request)
if err != nil {
return err
}
defer response.Body.Close()
if response.StatusCode >= 300 {
return fmt.Errorf("proxmox returned %s", response.Status)
}
return json.NewDecoder(response.Body).Decode(target)
}
func BuildAPIURL(host string) string {
if strings.HasPrefix(host, "http") {
return strings.TrimRight(host, "/")
}
return (&url.URL{Scheme: "https", Host: host, Path: "/api2/json"}).String()
}