[GH-ISSUE #527] Bug: Additional Attribute in Snapshot Creation leading to a failed Creation for LXC Containers #145

Open
opened 2026-02-28 00:40:44 +03:00 by kerem · 0 comments
Owner

Originally created by @Jobonabas on GitHub (Dec 2, 2025).
Original GitHub issue: https://github.com/Telmate/proxmox-api-go/issues/527

Originally assigned to: @Tinyblargon on GitHub.

📄 Description

When creating a snapshot the API does not differ between LXC Container and Qemu VMs. Qemu VMs need the attribute vmstate, while it is not needed for LXC VMs . This leads to an error on the API side when creating a snapshot of an LXC Container.

👣 Steps to reproduce

For this to work you need to have at least one LXC VM

  1. Try to create a Snapshot using the Create function in the snapshot.go file with the ConfigSnapshot struct
package main

import (
	"context"
	"crypto/tls"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
	"time"

	proxmox "github.com/Telmate/proxmox-api-go/proxmox"
	"github.com/joho/godotenv"
)

func main() {
	fmt.Println("Backend running")
	http.HandleFunc("/snapshot/create", CreateSnapshot)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

type CreateSnapshotRequest struct {
	VMID        uint32 `json:"vmid"`
	Name        string `json:"name"`
	Description string `json:"description"`
}

// loadEnv ensures .env is loaded and returns Proxmox API credentials
func loadEnv() (string, string, string, error) {
	if err := godotenv.Load("./.env"); err != nil {
		return "", "", "", fmt.Errorf("failed to load .env: %w", err)
	}
	apiURL := os.Getenv("API_URL")
	tokenID := os.Getenv("TOKEN_ID")
	tokenSecret := os.Getenv("TOKEN_SECRET")
	if apiURL == "" || tokenID == "" || tokenSecret == "" {
		return "", "", "", fmt.Errorf("missing required environment variables")
	}
	return apiURL, tokenID, tokenSecret, nil
}

func enableCors(w http.ResponseWriter) {
	w.Header().Set("Access-Control-Allow-Origin", "*")
	w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
	w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
}

func newProxmoxClient(ctx context.Context) (*proxmox.Client, error) {
	apiURL, tokenID, tokenSecret, err := loadEnv()
	if err != nil {
		return nil, err
	}

	tlsConfig := &tls.Config{InsecureSkipVerify: true}
	hclient := &http.Client{
		Transport: &http.Transport{TLSClientConfig: tlsConfig},
	}

	client, err := proxmox.NewClient(apiURL, hclient, "", tlsConfig, "", 300)
	if err != nil {
		return nil, fmt.Errorf("failed to create client: %w", err)
	}

	client.SetAPIToken(tokenID, tokenSecret)
	return client, nil
}


func CreateSnapshot(w http.ResponseWriter, r *http.Request) {
	enableCors(w)

	// Preflight-Request behandeln
	if r.Method == http.MethodOptions {
		w.WriteHeader(http.StatusOK)
		return
	}

	// Nur POST zulassen
	if r.Method != http.MethodPost {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	var req CreateSnapshotRequest

	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "Invalid JSON body: "+err.Error(), http.StatusBadRequest)
		return
	}

	fmt.Println("VMID:", req.VMID)
	fmt.Println("Name:", req.Name)
	fmt.Println("Description:", req.Description)

	ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second)
	defer cancel()
	client, err := newProxmoxClient(ctx)

	if err != nil {
		http.Error(w, "Failed to connect to Proxmox", http.StatusInternalServerError)
		return
	}

	vmr, err := client.GetVmRefById(ctx, proxmox.GuestID(req.VMID))
	if err != nil {
		log.Printf("failed to get VM ref: %v", err)
		http.Error(w, "Failed to get VM ref: "+err.Error(), http.StatusInternalServerError)
		return
	}
	snapshot := proxmox.ConfigSnapshot{
		Name:        proxmox.SnapshotName(req.Name),
		Description: req.Description,
	}

       // Those methods were copied from the Create-method only the print statements were inserted for debugging purposes

	if err = client.CheckVmRef(ctx, vmr); err != nil {
		fmt.Printf("VMRef %v\n", err)
	}
	if err = snapshot.Validate(); err != nil {
		fmt.Printf("Snapshot %v\n", err)
	}
	

	if err := snapshot.CreateNoCheck(ctx, client, vmr); err != nil {
		log.Printf("failed to create snapshot: %v\n", err)
		http.Error(w, "Failed to create snapshot: ", http.StatusInternalServerError)
		return
	}

}

Current behaviour

This leads to the following error:
2025/12/02 15:05:58 failed to create snapshot: error creating Snapshot: 400 Parameter verification failed., (params: {"description":"Test3","snapname":"Test3","vmstate":false})
Leaving out the vmstate false in the struct leads to the same error, as the vmstate is automatically replaced.

Image

Using the Proxmox UI I am able to create snapshots as usual.

✔ Expected behaviour

Snapshot creation should differ between Qemu VMs and LXC Containers and send the attributes according to the Proxmox API:
LXC: https://pve.proxmox.com/pve-docs/api-viewer/#/nodes/{node}/lxc/{vmid}/snapshot
Qemu: https://pve.proxmox.com/pve-docs/api-viewer/#/nodes/{node}/qemu/{vmid}/snapshot

Originally created by @Jobonabas on GitHub (Dec 2, 2025). Original GitHub issue: https://github.com/Telmate/proxmox-api-go/issues/527 Originally assigned to: @Tinyblargon on GitHub. ## 📄 Description When creating a snapshot the API does not differ between LXC Container and Qemu VMs. Qemu VMs need the attribute vmstate, while it is not needed for LXC VMs . This leads to an error on the API side when creating a snapshot of an LXC Container. ## 👣 Steps to reproduce For this to work you need to have at least one LXC VM 1. Try to create a Snapshot using the Create function in the [snapshot.go](https://github.com/Telmate/proxmox-api-go/blob/master/proxmox/snapshot.go) file with the ConfigSnapshot struct ``` go package main import ( "context" "crypto/tls" "encoding/json" "fmt" "log" "net/http" "os" "time" proxmox "github.com/Telmate/proxmox-api-go/proxmox" "github.com/joho/godotenv" ) func main() { fmt.Println("Backend running") http.HandleFunc("/snapshot/create", CreateSnapshot) log.Fatal(http.ListenAndServe(":8080", nil)) } type CreateSnapshotRequest struct { VMID uint32 `json:"vmid"` Name string `json:"name"` Description string `json:"description"` } // loadEnv ensures .env is loaded and returns Proxmox API credentials func loadEnv() (string, string, string, error) { if err := godotenv.Load("./.env"); err != nil { return "", "", "", fmt.Errorf("failed to load .env: %w", err) } apiURL := os.Getenv("API_URL") tokenID := os.Getenv("TOKEN_ID") tokenSecret := os.Getenv("TOKEN_SECRET") if apiURL == "" || tokenID == "" || tokenSecret == "" { return "", "", "", fmt.Errorf("missing required environment variables") } return apiURL, tokenID, tokenSecret, nil } func enableCors(w http.ResponseWriter) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") } func newProxmoxClient(ctx context.Context) (*proxmox.Client, error) { apiURL, tokenID, tokenSecret, err := loadEnv() if err != nil { return nil, err } tlsConfig := &tls.Config{InsecureSkipVerify: true} hclient := &http.Client{ Transport: &http.Transport{TLSClientConfig: tlsConfig}, } client, err := proxmox.NewClient(apiURL, hclient, "", tlsConfig, "", 300) if err != nil { return nil, fmt.Errorf("failed to create client: %w", err) } client.SetAPIToken(tokenID, tokenSecret) return client, nil } func CreateSnapshot(w http.ResponseWriter, r *http.Request) { enableCors(w) // Preflight-Request behandeln if r.Method == http.MethodOptions { w.WriteHeader(http.StatusOK) return } // Nur POST zulassen if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var req CreateSnapshotRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid JSON body: "+err.Error(), http.StatusBadRequest) return } fmt.Println("VMID:", req.VMID) fmt.Println("Name:", req.Name) fmt.Println("Description:", req.Description) ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second) defer cancel() client, err := newProxmoxClient(ctx) if err != nil { http.Error(w, "Failed to connect to Proxmox", http.StatusInternalServerError) return } vmr, err := client.GetVmRefById(ctx, proxmox.GuestID(req.VMID)) if err != nil { log.Printf("failed to get VM ref: %v", err) http.Error(w, "Failed to get VM ref: "+err.Error(), http.StatusInternalServerError) return } snapshot := proxmox.ConfigSnapshot{ Name: proxmox.SnapshotName(req.Name), Description: req.Description, } // Those methods were copied from the Create-method only the print statements were inserted for debugging purposes if err = client.CheckVmRef(ctx, vmr); err != nil { fmt.Printf("VMRef %v\n", err) } if err = snapshot.Validate(); err != nil { fmt.Printf("Snapshot %v\n", err) } if err := snapshot.CreateNoCheck(ctx, client, vmr); err != nil { log.Printf("failed to create snapshot: %v\n", err) http.Error(w, "Failed to create snapshot: ", http.StatusInternalServerError) return } } ``` ## ❌ Current behaviour This leads to the following error: 2025/12/02 15:05:58 failed to create snapshot: error creating Snapshot: 400 Parameter verification failed., (params: {"description":"Test3","snapname":"Test3","vmstate":false}) Leaving out the vmstate false in the struct leads to the same error, as the vmstate is automatically replaced. <img width="1920" height="1080" alt="Image" src="https://github.com/user-attachments/assets/46c8a862-0c45-42f5-bf98-3ce56bc7454a" /> Using the Proxmox UI I am able to create snapshots as usual. ## ✔ Expected behaviour Snapshot creation should differ between Qemu VMs and LXC Containers and send the attributes according to the Proxmox API: LXC: https://pve.proxmox.com/pve-docs/api-viewer/#/nodes/{node}/lxc/{vmid}/snapshot Qemu: https://pve.proxmox.com/pve-docs/api-viewer/#/nodes/{node}/qemu/{vmid}/snapshot
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/proxmox-api-go#145
No description provided.