feat: complete MVP epics E7-E10 (provisioning, console proxy, audit, frontend)

This commit is contained in:
Philipp
2026-06-12 10:45:13 +02:00
parent 137c13fa98
commit ac30a19960
33 changed files with 3625 additions and 263 deletions
+164
View File
@@ -228,6 +228,170 @@ func (c *Client) GetVMStatus(ctx context.Context, node string, vmid int) (VMStat
return VMStatus{Status: body.Data.Status}, nil
}
func (c *Client) CloneVM(ctx context.Context, node string, templateVMID int, newVMID int, name string) (string, error) {
requestBody := map[string]any{
"newid": newVMID,
"name": name,
}
body, err := json.Marshal(requestBody)
if err != nil {
return "", err
}
response, err := c.Post(ctx, fmt.Sprintf(
"/nodes/%s/qemu/%d/clone",
url.PathEscape(node),
templateVMID,
), body)
if err != nil {
return "", err
}
defer response.Body.Close()
if response.StatusCode >= http.StatusBadRequest {
_, _ = io.Copy(io.Discard, response.Body)
return "", fmt.Errorf("proxmox returned %s", response.Status)
}
return decodeUPID(response.Body)
}
func (c *Client) ConfigureCloudInit(ctx context.Context, node string, vmid int, cfg CloudInitConfig) (string, error) {
requestBody := map[string]any{
"ciuser": cfg.CIUser,
}
if cfg.SSHKeys != "" {
requestBody["sshkeys"] = cfg.SSHKeys
}
if cfg.IPConfig0 != "" {
requestBody["ipconfig0"] = cfg.IPConfig0
}
if cfg.Hostname != "" {
requestBody["searchdomain"] = cfg.Hostname
}
body, err := json.Marshal(requestBody)
if err != nil {
return "", err
}
response, err := c.Post(ctx, fmt.Sprintf(
"/nodes/%s/qemu/%d/config",
url.PathEscape(node),
vmid,
), body)
if err != nil {
return "", err
}
defer response.Body.Close()
if response.StatusCode >= http.StatusBadRequest {
_, _ = io.Copy(io.Discard, response.Body)
return "", fmt.Errorf("proxmox returned %s", response.Status)
}
return decodeUPID(response.Body)
}
func (c *Client) StartVM(ctx context.Context, node string, vmid int) (string, error) {
return c.PowerVM(ctx, node, vmid, "start")
}
func (c *Client) StopVM(ctx context.Context, node string, vmid int) (string, error) {
return c.PowerVM(ctx, node, vmid, "stop")
}
func (c *Client) DeleteVM(ctx context.Context, node string, vmid int) (string, error) {
response, err := c.Delete(ctx, fmt.Sprintf(
"/nodes/%s/qemu/%d",
url.PathEscape(node),
vmid,
))
if err != nil {
return "", err
}
defer response.Body.Close()
if response.StatusCode >= http.StatusBadRequest {
_, _ = io.Copy(io.Discard, response.Body)
return "", fmt.Errorf("proxmox returned %s", response.Status)
}
return decodeUPID(response.Body)
}
type CloudInitConfig struct {
CIUser string `json:"ciuser"`
SSHKeys string `json:"sshkeys,omitempty"`
IPConfig0 string `json:"ipconfig0,omitempty"`
Hostname string `json:"hostname,omitempty"`
}
func decodeUPID(body io.Reader) (string, error) {
var response struct {
Data string `json:"data"`
}
if err := json.NewDecoder(body).Decode(&response); err != nil {
return "", err
}
if response.Data == "" {
return "", fmt.Errorf("proxmox response missing UPID")
}
return response.Data, nil
}
func (c *Client) GetVNCTicket(ctx context.Context, node string, vmid int) (VNCInfo, error) {
requestBody := map[string]any{}
body, err := json.Marshal(requestBody)
if err != nil {
return VNCInfo{}, err
}
response, err := c.Post(ctx, fmt.Sprintf(
"/nodes/%s/qemu/%d/vncproxy",
url.PathEscape(node),
vmid,
), body)
if err != nil {
return VNCInfo{}, err
}
defer response.Body.Close()
if response.StatusCode >= http.StatusBadRequest {
_, _ = io.Copy(io.Discard, response.Body)
return VNCInfo{}, fmt.Errorf("proxmox returned %s", response.Status)
}
var result struct {
Data struct {
Port int `json:"port"`
Ticket string `json:"ticket"`
User string `json:"user"`
Cert string `json:"cert"`
UPID string `json:"upid"`
} `json:"data"`
}
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
return VNCInfo{}, err
}
return VNCInfo{
Port: result.Data.Port,
Ticket: result.Data.Ticket,
User: result.Data.User,
Cert: result.Data.Cert,
UPID: result.Data.UPID,
}, nil
}
type VNCInfo struct {
Port int
Ticket string
User string
Cert string
UPID string
}
func (c *Client) Do(ctx context.Context, method string, path string, body []byte) (*http.Response, error) {
var lastErr error
attempts := c.retries + 1