fix(proxmox): DestroyLXC DELETE takes no body + force=1 (live restore-test teardown)

The slice-6 restore-test teardown failed live: DELETE /lxc/{vmid} with a form body
returns HTTP 501 'Unexpected content for method DELETE', and a booted scratch needs
force=1. Move purge/destroy-unreferenced-disks/force to the query string, send no body.
Regression test locks the contract.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 13:53:36 +02:00
parent b527430ec7
commit 280a7f80e5
2 changed files with 69 additions and 5 deletions
+63
View File
@@ -0,0 +1,63 @@
package proxmox
import (
"context"
"io"
"net/http"
"strings"
"testing"
)
// TestDestroyLXC_DeleteContract locks the DELETE wire contract: no request body (a form
// body → PVE HTTP 501 "Unexpected content for method 'DELETE'"), flags in the query string,
// force=1 so a still-running scratch can be torn down. Regression guard for the slice-6
// teardown bug.
func TestDestroyLXC_DeleteContract(t *testing.T) {
var gotMethod, gotURL string
var gotBody string
m := &mockDoer{fn: func(r *http.Request) (*http.Response, error) {
gotMethod = r.Method
gotURL = r.URL.String()
if r.Body != nil {
b, _ := io.ReadAll(r.Body)
gotBody = string(b)
}
return jsonResp(200, `{"data":"UPID:demo:0000:vzdestroy::root@pam:"}`), nil
}}
c := newTestClient(m)
upid, err := c.DestroyLXC(context.Background(), 990000)
if err != nil {
t.Fatalf("DestroyLXC: %v", err)
}
if upid == "" {
t.Error("expected a UPID")
}
if gotMethod != http.MethodDelete {
t.Errorf("method = %s, want DELETE", gotMethod)
}
if gotBody != "" {
t.Errorf("DELETE must send NO body, got %q", gotBody)
}
for _, want := range []string{"/nodes/demo-felhom/lxc/990000", "purge=1", "destroy-unreferenced-disks=1", "force=1"} {
if !strings.Contains(gotURL, want) {
t.Errorf("URL %q missing %q", gotURL, want)
}
}
}
// TestVzdump_NotesTemplate confirms the notes-template param is sent when set.
func TestVzdump_NotesTemplate(t *testing.T) {
var gotBody string
m := &mockDoer{fn: func(r *http.Request) (*http.Response, error) {
b, _ := io.ReadAll(r.Body)
gotBody = string(b)
return jsonResp(200, `{"data":"UPID:demo:0:vzdump::root@pam:"}`), nil
}}
c := newTestClient(m)
if _, err := c.Vzdump(context.Background(), VzdumpOptions{VMID: 9001, Storage: "local", Mode: ModeSnapshot, Notes: "felhom {{guestname}}"}); err != nil {
t.Fatal(err)
}
if !strings.Contains(gotBody, "notes-template=") {
t.Errorf("vzdump body missing notes-template: %q", gotBody)
}
}
+6 -5
View File
@@ -96,11 +96,12 @@ func (c *Client) DestroyLXC(ctx context.Context, vmid int) (string, error) {
if vmid == 0 {
return "", fmt.Errorf("proxmox: DestroyLXC needs a vmid")
}
v := url.Values{}
v.Set("purge", "1")
v.Set("destroy-unreferenced-disks", "1")
path := fmt.Sprintf("/nodes/%s/lxc/%d", c.node, vmid)
return c.dataString(ctx, http.MethodDelete, path, v)
// DELETE takes NO request body on PVE (a form body → HTTP 501 "Unexpected content for
// method 'DELETE'"); the flags go in the query string. `force=1` destroys even a running
// guest (the scratch may still be booted at teardown), `purge=1` drops it from jobs/HA,
// `destroy-unreferenced-disks=1` reaps orphaned volumes.
path := fmt.Sprintf("/nodes/%s/lxc/%d?purge=1&destroy-unreferenced-disks=1&force=1", c.node, vmid)
return c.dataString(ctx, http.MethodDelete, path, nil)
}
// Snapshot creates an LXC snapshot via POST /nodes/{node}/lxc/{vmid}/snapshot.