Backend Go 1.23+ : - API REST + WebSocket (chi, gorilla/websocket) - Authentification PAM via SSH + JWT RS256 - Chiffrement AES-256-GCM pour secrets SQLite - Pool SSH, client Proxmox REST, hub WebSocket pub/sub - Système de modules compilés à initialisation conditionnelle - Audit log, migrations SQLite versionnées Frontend Vue 3 + Vite + TypeScript : - Thème Neumorphism sombre/clair (CSS custom properties) - Wizard d'installation, Dashboard drag-drop, Terminal xterm.js - Toutes les vues CORE + stubs modules optionnels - i18n EN/FR (vue-i18n v11) Infrastructure : - Docker multi-stage (Go → alpine, Node → nginx) - docker-compose.yml, .gitattributes, LICENSE MIT, README Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
212 lines
5.9 KiB
Go
212 lines
5.9 KiB
Go
// 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)
|
|
}
|