// Package proxmox fournit un client pour l'API REST Proxmox VE. // Les credentials (token API ou user/password) sont stockés chiffrés en SQLite. package proxmox import ( "crypto/tls" "encoding/json" "fmt" "io" "net/http" "strings" "time" ) // Client est le client HTTP vers l'API Proxmox VE. type Client struct { baseURL string httpClient *http.Client token string // Format: "PVEAPIToken=user@realm!tokenid=secret" } // NodeStatus représente l'état d'un nœud Proxmox. type NodeStatus struct { Node string `json:"node"` Status string `json:"status"` CPU float64 `json:"cpu"` MaxCPU int `json:"maxcpu"` Mem int64 `json:"mem"` MaxMem int64 `json:"maxmem"` Uptime int64 `json:"uptime"` } // Resource représente un LXC, une VM ou un autre objet Proxmox. type Resource struct { VMID int `json:"vmid"` Name string `json:"name"` Node string `json:"node"` Type string `json:"type"` // "lxc" | "qemu" | "storage" | "node" Status string `json:"status"` // "running" | "stopped" CPU float64 `json:"cpu"` MaxCPU int `json:"maxcpu"` Mem int64 `json:"mem"` MaxMem int64 `json:"maxmem"` Disk int64 `json:"disk"` MaxDisk int64 `json:"maxdisk"` Uptime int64 `json:"uptime"` NetIn int64 `json:"netin"` NetOut int64 `json:"netout"` } // proxmoxResponse est l'enveloppe générique des réponses API Proxmox. type proxmoxResponse struct { Data json.RawMessage `json:"data"` Error string `json:"errors"` } // NewClient crée un client Proxmox avec le token API fourni. // baseURL : ex "https://10.0.0.1:8006" // token : ex "PVEAPIToken=enzo@pam!panel=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" func NewClient(baseURL, token string) *Client { return &Client{ baseURL: strings.TrimRight(baseURL, "/"), token: token, httpClient: &http.Client{ Timeout: 15 * time.Second, Transport: &http.Transport{ // Proxmox utilise des certificats auto-signés TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }, }, } } // GetNodes retourne la liste des nœuds Proxmox. func (c *Client) GetNodes() ([]NodeStatus, error) { var nodes []NodeStatus if err := c.get("/api2/json/nodes", &nodes); err != nil { return nil, err } return nodes, nil } // GetResources retourne tous les LXC et VM de l'ensemble du cluster. // Le paramètre type filtre les résultats ("lxc", "vm", ou "" pour tout). func (c *Client) GetResources(resourceType string) ([]Resource, error) { path := "/api2/json/cluster/resources" if resourceType != "" { path += "?type=" + resourceType } var resources []Resource if err := c.get(path, &resources); err != nil { return nil, err } return resources, nil } // GetLXCList retourne uniquement les conteneurs LXC. func (c *Client) GetLXCList() ([]Resource, error) { return c.GetResources("lxc") } // GetVMList retourne uniquement les machines virtuelles QEMU. func (c *Client) GetVMList() ([]Resource, error) { return c.GetResources("vm") } // StartLXC démarre un conteneur LXC. func (c *Client) StartLXC(node string, vmid int) error { _, err := c.post(fmt.Sprintf("/api2/json/nodes/%s/lxc/%d/status/start", node, vmid), nil) return err } // StopLXC arrête un conteneur LXC. func (c *Client) StopLXC(node string, vmid int) error { _, err := c.post(fmt.Sprintf("/api2/json/nodes/%s/lxc/%d/status/stop", node, vmid), nil) return err } // StartVM démarre une machine virtuelle. func (c *Client) StartVM(node string, vmid int) error { _, err := c.post(fmt.Sprintf("/api2/json/nodes/%s/qemu/%d/status/start", node, vmid), nil) return err } // StopVM arrête une machine virtuelle. func (c *Client) StopVM(node string, vmid int) error { _, err := c.post(fmt.Sprintf("/api2/json/nodes/%s/qemu/%d/status/stop", node, vmid), nil) return err } // TestConnection vérifie que le token API est valide en récupérant la liste des nœuds. func (c *Client) TestConnection() error { _, err := c.GetNodes() return err } // get effectue une requête GET et décode la réponse dans dest. func (c *Client) get(path string, dest any) error { req, err := http.NewRequest("GET", c.baseURL+path, nil) if err != nil { return err } req.Header.Set("Authorization", c.token) resp, err := c.httpClient.Do(req) if err != nil { return fmt.Errorf("requête Proxmox : %w", err) } defer resp.Body.Close() if resp.StatusCode == 401 { return fmt.Errorf("token Proxmox invalide ou expiré") } if resp.StatusCode >= 400 { return fmt.Errorf("erreur Proxmox API : HTTP %d", resp.StatusCode) } return c.decodeResponse(resp.Body, dest) } // post effectue une requête POST. func (c *Client) post(path string, body any) (json.RawMessage, error) { var reader io.Reader if body != nil { data, err := json.Marshal(body) if err != nil { return nil, err } reader = strings.NewReader(string(data)) } req, err := http.NewRequest("POST", c.baseURL+path, reader) if err != nil { return nil, err } req.Header.Set("Authorization", c.token) if body != nil { req.Header.Set("Content-Type", "application/json") } resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("requête Proxmox : %w", err) } defer resp.Body.Close() if resp.StatusCode == 401 { return nil, fmt.Errorf("token Proxmox invalide") } if resp.StatusCode >= 400 { return nil, fmt.Errorf("erreur Proxmox API : HTTP %d", resp.StatusCode) } var result json.RawMessage c.decodeResponse(resp.Body, &result) return result, nil } // decodeResponse décode l'enveloppe JSON Proxmox et extrait le champ "data". func (c *Client) decodeResponse(body io.Reader, dest any) error { var wrapper proxmoxResponse if err := json.NewDecoder(body).Decode(&wrapper); err != nil { return fmt.Errorf("décodage réponse Proxmox : %w", err) } if wrapper.Error != "" { return fmt.Errorf("erreur Proxmox : %s", wrapper.Error) } if dest == nil || wrapper.Data == nil { return nil } return json.Unmarshal(wrapper.Data, dest) }