syncova-backup/packages/providers/proxmox/fakeserver_test.go
Jerrit Fritzsche 610719c316
Some checks failed
CI / Backend (Go) (push) Failing after 3m7s
CI / Frontend (React/TypeScript) (push) Successful in 37s
CI / Sicherheitsprüfungen (push) Successful in 44s
Syncova Backups V1
Enterprise-Backup-, Recovery-, Verification-, Security- und
Monitoring-Plattform fuer Proxmox VE, Windows, Linux und Dateisysteme.

Der Leitsatz, der fast jede Entscheidung erklaert: Ein Backup gilt erst als
vertrauenswuerdig, wenn Integritaet geprueft und Wiederherstellbarkeit
nachgewiesen wurde. Deshalb steigt ein Wiederherstellungspunkt erst nach einem
tatsaechlich durchgefuehrten Restore-Test auf "recoverable", und Unbekanntes
geht in keine Bewertung als "gut" ein.

Umfang (Phasen 0-23):

- Repository Engine: inhaltsadressierte Bloecke, atomares Commit-Protokoll,
  Katalogaufbau allein aus den Manifesten — ohne Datenbank
- Backup Engine: inhaltsabhaengiges Chunking, Deduplizierung trotz
  Verschluesselung, zstd, AES-256-GCM, Streaming mit Gegendruck
- Agenten fuer Windows und Linux mit Auftragsabholung (Pull-Modell)
- Proxmox-Provider mit beiden Zugriffswegen auf die Sicherungsarchive
- Scheduler, Recovery Engine mit Pruefpunkt, Verification, Unveraenderlichkeit
- Weboberflaeche, Kennzahlen, Meldungen, Berichte, Security Center,
  Ransomware-Heuristik (meldet, handelt nie)
- Disaster Recovery, Haertung, Leistungsmessung, Chaos Testing
- Eingefrorene Vertraege fuer API, Migrationen, Backup-Format und Repository
- Auslieferungspaket fuer linux/amd64, linux/arm64 und windows/amd64

Nicht enthalten und als solches gekennzeichnet: Kapazitaetsprognose, Backup
Copy, Changed Block Tracking bei Proxmox, erweiterte Attribute und ACLs.

Gebaut, aber nie auf echter Hardware gefahren: der Windows-Dienst, die
systemd-Einheit und der verpflichtende Proxmox-Meilenstein — ob eine
wiederhergestellte VM startet, ist ungeprueft. Einzelheiten in CHANGELOG.md
und docs/release-candidate.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 09:10:54 +02:00

592 lines
20 KiB
Go

package proxmox
import (
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"strings"
"sync"
"testing"
"time"
)
// fakeProxmoxServer bildet die Proxmox-API für Tests nach.
//
// Der Nachbau ist kein Ersatz für eine echte Umgebung — die Grenzen stehen in
// docs/proxmox.md. Er prüft aber genau das, was ohne Cluster prüfbar ist: dass
// wir die Antwortformate richtig deuten, den Anmeldekopf richtig setzen, den
// asynchronen Ablauf über UPIDs korrekt verfolgen und Fehlerfälle nicht
// beschönigen.
//
// Die Antworten sind den echten nachgebildet: die "data"-Hülle, das
// UPID-Format, die Konfigurationswerte als Zeichenkette-mit-Kommas.
type fakeProxmoxServer struct {
// httpServer ist der laufende Testserver.
httpServer *httptest.Server
// mutex schützt den veränderlichen Zustand.
mutex sync.Mutex
// tasksByIdentifier hält die angelegten Aufgaben.
tasksByIdentifier map[string]*fakeTask
// guestConfigurations sind die Konfigurationen je Gast.
guestConfigurations map[int]map[string]any
// guestStatuses sind die Betriebszustände je Gast.
guestStatuses map[int]string
// snapshotsByGuest hält die angelegten Abbilder.
snapshotsByGuest map[int][]string
// requestLog hält die eingegangenen Aufrufe für Zusicherungen.
requestLog []string
// receivedAuthorizationHeaders hält die gesehenen Anmeldeköpfe.
receivedAuthorizationHeaders []string
// taskCounter vergibt fortlaufende Aufgabennummern.
taskCounter int
// createdGuests hält die durch Wiederherstellung angelegten Gäste.
createdGuests map[int]url.Values
// appliedConfigurations hält die gesetzten Konfigurationen.
appliedConfigurations map[int]url.Values
// failNextTask lässt die nächste Aufgabe scheitern.
failNextTask string
// transientFailuresRemaining lässt Aufrufe vorübergehend scheitern.
transientFailuresRemaining int
// archiveVolumes sind die vorhandenen Sicherungsarchive je Gast.
archiveVolumes map[int][]fakeArchive
// deletedArchives sammelt die entfernten Sicherungsarchive.
deletedArchives []string
}
// fakeArchive ist ein nachgebildetes Sicherungsarchiv.
type fakeArchive struct {
// Volume ist die Proxmox-Bezeichnung.
Volume string
// CreatedAt ist der Erstellungszeitpunkt als Unix-Zeit.
CreatedAt int64
// Storage ist der Speicher, auf dem das Archiv liegt.
Storage string
}
// fakeTask ist eine nachgebildete Proxmox-Aufgabe.
type fakeTask struct {
// Identifier ist die UPID.
Identifier string
// TaskType benennt die Art.
TaskType string
// PollsRemaining zählt, wie oft die Aufgabe noch als laufend gilt.
//
// Damit prüft der Test den asynchronen Ablauf wirklich: Eine Aufgabe, die
// sofort fertig ist, würde eine fehlende Verfolgungsschleife nicht auffallen
// lassen.
PollsRemaining int
// ExitStatus ist das Ergebnis nach Ablauf.
ExitStatus string
// NodeName ist der ausführende Knoten.
NodeName string
}
// newFakeProxmoxServer erzeugt einen vorbereiteten Nachbau.
func newFakeProxmoxServer(testInstance *testing.T) *fakeProxmoxServer {
testInstance.Helper()
fakeServer := &fakeProxmoxServer{
tasksByIdentifier: make(map[string]*fakeTask),
guestConfigurations: map[int]map[string]any{
100: {
"name": "web-01",
"cores": 4,
"memory": 8192,
"bios": "ovmf",
"agent": "1,fstrim_cloned_disks=1",
"boot": "order=scsi0;net0",
"scsi0": "local-lvm:vm-100-disk-0,size=32G,ssd=1",
"scsi1": "local-lvm:vm-100-disk-1,size=500G,backup=0",
"efidisk0": "local-lvm:vm-100-disk-2,efitype=4m,size=1M",
"ide2": "local:iso/debian-12.iso,media=cdrom",
"net0": "virtio=AA:BB:CC:DD:EE:01,bridge=vmbr0,tag=42",
"net1": "e1000=AA:BB:CC:DD:EE:02,bridge=vmbr1",
"unused0": "local-lvm:vm-100-disk-9,size=10G",
"digest": "abc123",
},
101: {
"name": "db-01",
"cores": 8,
"memory": 32768,
"scsi0": "ceph-pool:vm-101-disk-0,size=1.5T",
"net0": "virtio=AA:BB:CC:DD:EE:03,bridge=vmbr0",
"boot": "cdn",
},
200: {
"hostname": "lxc-web",
"rootfs": "local-lvm:vm-200-disk-0,size=8G",
"mp0": "local-lvm:vm-200-disk-1,mp=/daten,size=100G",
},
},
guestStatuses: map[int]string{
100: "running",
101: "stopped",
200: "running",
},
snapshotsByGuest: make(map[int][]string),
createdGuests: make(map[int]url.Values),
appliedConfigurations: make(map[int]url.Values),
archiveVolumes: make(map[int][]fakeArchive),
requestLog: make([]string, 0, 32),
}
fakeServer.httpServer = httptest.NewTLSServer(http.HandlerFunc(fakeServer.handleRequest))
testInstance.Cleanup(fakeServer.httpServer.Close)
return fakeServer
}
// newConnectedProvider baut einen Provider gegen den Nachbau.
func (fakeServer *fakeProxmoxServer) newConnectedProvider(testInstance *testing.T) *Provider {
testInstance.Helper()
testLogger := slog.New(slog.NewJSONHandler(io.Discard, nil))
builtProvider, providerError := NewProvider(ProviderOptions{
ClientOptions: ClientOptions{
BaseURL: fakeServer.httpServer.URL,
APITokenID: "syncova@pve!backup",
APITokenSecret: "geheim-nicht-in-logs", // secretscan:erlaubt: erfundener Testwert
// Der Testserver trägt ein selbstsigniertes Zertifikat. Der Weg
// über den Fingerabdruck wird gesondert geprüft.
InsecureSkipTLSVerify: true,
RequestTimeout: 5 * time.Second,
MaximumRetries: 2,
},
BackupStorageID: "local",
// Der Nachbau antwortet sofort; ein Sekundentakt liesse die Tests nur
// warten, ohne mehr zu prüfen.
TaskPollInterval: 5 * time.Millisecond,
}, testLogger)
if providerError != nil {
testInstance.Fatalf("der Provider ließ sich nicht bauen: %v", providerError)
}
return builtProvider
}
// handleRequest verteilt die Aufrufe.
func (fakeServer *fakeProxmoxServer) handleRequest(responseWriter http.ResponseWriter, request *http.Request) {
fakeServer.mutex.Lock()
fakeServer.requestLog = append(fakeServer.requestLog, request.Method+" "+request.URL.Path)
fakeServer.receivedAuthorizationHeaders = append(fakeServer.receivedAuthorizationHeaders, request.Header.Get("Authorization"))
if fakeServer.transientFailuresRemaining > 0 {
fakeServer.transientFailuresRemaining--
fakeServer.mutex.Unlock()
// 503 ist ein vorübergehender Fehler; der Client muss ihn wiederholen.
http.Error(responseWriter, `{"message":"service unavailable"}`, http.StatusServiceUnavailable)
return
}
fakeServer.mutex.Unlock()
// Der Anmeldekopf muss auf jedem Aufruf stehen, nicht nur auf dem ersten.
if !strings.HasPrefix(request.Header.Get("Authorization"), "PVEAPIToken=") {
http.Error(responseWriter, `{"message":"authentication failure"}`, http.StatusUnauthorized)
return
}
apiPath := strings.TrimPrefix(request.URL.Path, apiBasePath)
_ = request.ParseForm()
switch {
case apiPath == "/version":
writeData(responseWriter, map[string]string{"version": "8.2.4", "release": "8.2"})
case apiPath == "/cluster/status":
fakeServer.handleClusterStatus(responseWriter)
case apiPath == "/nodes":
writeData(responseWriter, []map[string]any{
{"node": "pve-01", "status": "online", "maxcpu": 32, "maxmem": int64(137438953472)},
{"node": "pve-02", "status": "online", "maxcpu": 32, "maxmem": int64(137438953472)},
{"node": "pve-03", "status": "offline", "maxcpu": 16, "maxmem": int64(68719476736)},
})
case apiPath == "/cluster/resources":
fakeServer.handleClusterResources(responseWriter)
case strings.HasSuffix(apiPath, "/config"):
fakeServer.handleConfiguration(responseWriter, request, apiPath)
case strings.HasSuffix(apiPath, "/status/current"):
fakeServer.handleGuestStatus(responseWriter, apiPath)
case strings.HasSuffix(apiPath, "/status/start"):
fakeServer.handleStart(responseWriter, apiPath)
case strings.Contains(apiPath, "/tasks/") && strings.HasSuffix(apiPath, "/status"):
fakeServer.handleTaskStatus(responseWriter, apiPath)
case strings.Contains(apiPath, "/tasks/") && strings.HasSuffix(apiPath, "/log"):
writeData(responseWriter, []map[string]any{
{"n": 1, "t": "starting task"},
{"n": 2, "t": "ERROR: no space left on device"},
})
case strings.Contains(apiPath, "/snapshot"):
fakeServer.handleSnapshot(responseWriter, request, apiPath)
case strings.HasSuffix(apiPath, "/vzdump"):
fakeServer.handleVzdump(responseWriter, request, apiPath)
case strings.Contains(apiPath, "/storage/") && strings.HasSuffix(apiPath, "/content"):
fakeServer.handleStorageContent(responseWriter, request, apiPath)
case strings.Contains(apiPath, "/storage/") && strings.HasSuffix(apiPath, "/status"):
fakeServer.handleStorageStatus(responseWriter, apiPath)
case request.Method == http.MethodDelete && strings.Contains(apiPath, "/storage/") &&
strings.Contains(apiPath, "/content/"):
fakeServer.handleArchiveDeletion(responseWriter, apiPath)
case request.Method == http.MethodPost && (strings.HasSuffix(apiPath, "/qemu") || strings.HasSuffix(apiPath, "/lxc")):
fakeServer.handleRestore(responseWriter, request, apiPath)
default:
http.Error(responseWriter, `{"message":"not found"}`, http.StatusNotFound)
}
}
// handleClusterStatus liefert den Verbundstatus.
func (fakeServer *fakeProxmoxServer) handleClusterStatus(responseWriter http.ResponseWriter) {
writeData(responseWriter, []map[string]any{
{"type": "cluster", "id": "cluster", "name": "syncova-lab", "nodes": 3, "quorate": 1, "version": 5},
{"type": "node", "id": "node/pve-01", "name": "pve-01", "online": 1},
{"type": "node", "id": "node/pve-02", "name": "pve-02", "online": 1},
{"type": "node", "id": "node/pve-03", "name": "pve-03", "online": 0},
})
}
// handleClusterResources liefert die Gäste des Verbunds.
func (fakeServer *fakeProxmoxServer) handleClusterResources(responseWriter http.ResponseWriter) {
fakeServer.mutex.Lock()
defer fakeServer.mutex.Unlock()
resourceEntries := []map[string]any{
{"type": "qemu", "id": "qemu/100", "node": "pve-01", "vmid": 100, "name": "web-01",
"status": fakeServer.guestStatuses[100], "maxcpu": 4.0, "maxmem": int64(8589934592), "tags": "produktiv;web"},
{"type": "qemu", "id": "qemu/101", "node": "pve-02", "vmid": 101, "name": "db-01",
"status": fakeServer.guestStatuses[101], "maxcpu": 8.0, "maxmem": int64(34359738368), "tags": "produktiv;datenbank"},
{"type": "lxc", "id": "lxc/200", "node": "pve-01", "vmid": 200, "name": "lxc-web",
"status": fakeServer.guestStatuses[200], "maxcpu": 2.0, "maxmem": int64(2147483648)},
// Eine Vorlage: sie darf in der Erfassung nicht erscheinen.
{"type": "qemu", "id": "qemu/900", "node": "pve-01", "vmid": 900, "name": "vorlage-debian",
"status": "stopped", "template": 1},
}
for guestID := range fakeServer.createdGuests {
resourceEntries = append(resourceEntries, map[string]any{
"type": "qemu", "id": fmt.Sprintf("qemu/%d", guestID), "node": "pve-01",
"vmid": guestID, "name": fmt.Sprintf("restore-%d", guestID), "status": "stopped",
})
}
writeData(responseWriter, resourceEntries)
}
// handleConfiguration liefert oder setzt eine Gastkonfiguration.
func (fakeServer *fakeProxmoxServer) handleConfiguration(responseWriter http.ResponseWriter, request *http.Request, apiPath string) {
guestID := extractGuestIDFromPath(apiPath)
fakeServer.mutex.Lock()
defer fakeServer.mutex.Unlock()
if request.Method == http.MethodPut {
fakeServer.appliedConfigurations[guestID] = request.PostForm
writeData(responseWriter, nil)
return
}
guestConfiguration, isKnown := fakeServer.guestConfigurations[guestID]
if !isKnown {
http.Error(responseWriter, `{"message":"Configuration file does not exist"}`, http.StatusNotFound)
return
}
writeData(responseWriter, guestConfiguration)
}
// handleGuestStatus liefert den Betriebszustand.
func (fakeServer *fakeProxmoxServer) handleGuestStatus(responseWriter http.ResponseWriter, apiPath string) {
guestID := extractGuestIDFromPath(apiPath)
fakeServer.mutex.Lock()
defer fakeServer.mutex.Unlock()
currentStatus, isKnown := fakeServer.guestStatuses[guestID]
if !isKnown {
currentStatus = "stopped"
}
writeData(responseWriter, map[string]any{"status": currentStatus, "qmpstatus": currentStatus})
}
// handleStart startet einen Gast.
func (fakeServer *fakeProxmoxServer) handleStart(responseWriter http.ResponseWriter, apiPath string) {
guestID := extractGuestIDFromPath(apiPath)
fakeServer.mutex.Lock()
fakeServer.guestStatuses[guestID] = "running"
fakeServer.mutex.Unlock()
writeData(responseWriter, fakeServer.createTask("qmstart", "pve-01", 1))
}
// handleSnapshot legt Abbilder an oder entfernt sie.
func (fakeServer *fakeProxmoxServer) handleSnapshot(responseWriter http.ResponseWriter, request *http.Request, apiPath string) {
guestID := extractGuestIDFromPath(apiPath)
switch request.Method {
case http.MethodPost:
snapshotName := request.PostForm.Get("snapname")
fakeServer.mutex.Lock()
fakeServer.snapshotsByGuest[guestID] = append(fakeServer.snapshotsByGuest[guestID], snapshotName)
fakeServer.mutex.Unlock()
// Zwei Abfragen als laufend: der Test soll die Verfolgungsschleife
// wirklich durchlaufen.
writeData(responseWriter, fakeServer.createTask("qmsnapshot", "pve-01", 2))
case http.MethodDelete:
snapshotName := apiPath[strings.LastIndex(apiPath, "/")+1:]
fakeServer.mutex.Lock()
remainingSnapshots := make([]string, 0)
for _, existingSnapshot := range fakeServer.snapshotsByGuest[guestID] {
if existingSnapshot != snapshotName {
remainingSnapshots = append(remainingSnapshots, existingSnapshot)
}
}
fakeServer.snapshotsByGuest[guestID] = remainingSnapshots
fakeServer.mutex.Unlock()
writeData(responseWriter, fakeServer.createTask("qmdelsnapshot", "pve-01", 1))
default:
http.Error(responseWriter, `{"message":"method not allowed"}`, http.StatusMethodNotAllowed)
}
}
// knownBackupStorages sind die Speicher, die im Nachbau Sicherungen aufnehmen.
//
// Ein unbekannter Speicher nimmt nichts auf. Damit lässt sich der Fall prüfen,
// dass eine Aufgabe Erfolg meldet und trotzdem kein Archiv entsteht.
var knownBackupStorages = map[string]bool{"local": true, "nfs-backup": true}
// handleVzdump stößt eine Proxmox-Sicherung an.
func (fakeServer *fakeProxmoxServer) handleVzdump(responseWriter http.ResponseWriter, request *http.Request, _ string) {
guestID, _ := strconv.Atoi(request.PostForm.Get("vmid"))
storageName := request.PostForm.Get("storage")
if knownBackupStorages[storageName] {
fakeServer.mutex.Lock()
fakeServer.archiveVolumes[guestID] = append(fakeServer.archiveVolumes[guestID], fakeArchive{
Volume: fmt.Sprintf("%s:backup/vzdump-qemu-%d-2026_08_11-02_00_00.vma", storageName, guestID),
CreatedAt: time.Now().Unix(),
Storage: storageName,
})
fakeServer.mutex.Unlock()
}
writeData(responseWriter, fakeServer.createTask("vzdump", "pve-01", 2))
}
// handleStorageContent liefert die Archivliste eines Speichers.
func (fakeServer *fakeProxmoxServer) handleStorageContent(responseWriter http.ResponseWriter, request *http.Request, apiPath string) {
guestID, _ := strconv.Atoi(request.URL.Query().Get("vmid"))
storageName := extractStorageFromPath(apiPath)
fakeServer.mutex.Lock()
defer fakeServer.mutex.Unlock()
contentEntries := make([]map[string]any, 0)
for _, archiveEntry := range fakeServer.archiveVolumes[guestID] {
// Ein Archiv liegt auf genau einem Speicher; ein anderer zeigt es nicht.
if archiveEntry.Storage != storageName {
continue
}
contentEntries = append(contentEntries, map[string]any{
"volid": archiveEntry.Volume,
"ctime": archiveEntry.CreatedAt,
"size": int64(1024),
})
}
writeData(responseWriter, contentEntries)
}
// extractStorageFromPath liest den Speichernamen aus einem API-Pfad.
func extractStorageFromPath(apiPath string) string {
pathSegments := strings.Split(apiPath, "/")
for segmentIndex, pathSegment := range pathSegments {
if pathSegment == "storage" && segmentIndex+1 < len(pathSegments) {
return pathSegments[segmentIndex+1]
}
}
return ""
}
// handleRestore legt einen Gast aus einem Archiv an.
func (fakeServer *fakeProxmoxServer) handleRestore(responseWriter http.ResponseWriter, request *http.Request, _ string) {
guestID, _ := strconv.Atoi(request.PostForm.Get("vmid"))
fakeServer.mutex.Lock()
fakeServer.createdGuests[guestID] = request.PostForm
fakeServer.guestStatuses[guestID] = "stopped"
fakeServer.mutex.Unlock()
writeData(responseWriter, fakeServer.createTask("qmrestore", "pve-01", 2))
}
// handleTaskStatus liefert den Stand einer Aufgabe.
func (fakeServer *fakeProxmoxServer) handleTaskStatus(responseWriter http.ResponseWriter, apiPath string) {
pathSegments := strings.Split(strings.TrimPrefix(apiPath, "/"), "/")
// Aufbau: nodes/{knoten}/tasks/{upid}/status
const taskIdentifierSegment = 3
if len(pathSegments) <= taskIdentifierSegment {
http.Error(responseWriter, `{"message":"not found"}`, http.StatusNotFound)
return
}
taskIdentifier := pathSegments[taskIdentifierSegment]
fakeServer.mutex.Lock()
defer fakeServer.mutex.Unlock()
currentTask, isKnown := fakeServer.tasksByIdentifier[taskIdentifier]
if !isKnown {
http.Error(responseWriter, `{"message":"no such task"}`, http.StatusNotFound)
return
}
if currentTask.PollsRemaining > 0 {
currentTask.PollsRemaining--
writeData(responseWriter, map[string]any{
"status": "running", "type": currentTask.TaskType, "node": currentTask.NodeName,
"starttime": time.Now().Unix(),
})
return
}
writeData(responseWriter, map[string]any{
"status": "stopped", "exitstatus": currentTask.ExitStatus,
"type": currentTask.TaskType, "node": currentTask.NodeName,
"starttime": time.Now().Unix(),
})
}
// createTask legt eine nachgebildete Aufgabe an und liefert ihre UPID.
func (fakeServer *fakeProxmoxServer) createTask(taskType string, nodeName string, pollsRemaining int) string {
fakeServer.mutex.Lock()
defer fakeServer.mutex.Unlock()
fakeServer.taskCounter++
// Echtes UPID-Format: UPID:node:pid:pstart:starttime:type:id:user:
taskIdentifier := fmt.Sprintf("UPID:%s:%08X:%08X:%08X:%s:100:syncova@pve:",
nodeName, fakeServer.taskCounter, fakeServer.taskCounter, time.Now().Unix(), taskType)
exitStatus := "OK"
if fakeServer.failNextTask != "" {
exitStatus = fakeServer.failNextTask
fakeServer.failNextTask = ""
}
fakeServer.tasksByIdentifier[taskIdentifier] = &fakeTask{
Identifier: taskIdentifier,
TaskType: taskType,
PollsRemaining: pollsRemaining,
ExitStatus: exitStatus,
NodeName: nodeName,
}
return taskIdentifier
}
// extractGuestIDFromPath liest die Gastkennung aus einem API-Pfad.
func extractGuestIDFromPath(apiPath string) int {
pathSegments := strings.Split(apiPath, "/")
for segmentIndex, pathSegment := range pathSegments {
if pathSegment != "qemu" && pathSegment != "lxc" {
continue
}
if segmentIndex+1 >= len(pathSegments) {
return 0
}
guestID, conversionError := strconv.Atoi(pathSegments[segmentIndex+1])
if conversionError != nil {
return 0
}
return guestID
}
return 0
}
// writeData schreibt eine Antwort in der Proxmox-Hülle.
func writeData(responseWriter http.ResponseWriter, payload any) {
responseWriter.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(responseWriter).Encode(map[string]any{"data": payload})
}
// handleStorageStatus liefert die Angaben eines Speichers.
//
// Ein verzeichnisbasierter Speicher traegt einen Pfad; ZFS, LVM und Ceph nicht.
// Der Unterschied entscheidet, ob sich ein Archiv als Datei lesen laesst.
func (fakeServer *fakeProxmoxServer) handleStorageStatus(responseWriter http.ResponseWriter, apiPath string) {
if strings.Contains(apiPath, "/zfs-pool/") {
writeData(responseWriter, map[string]any{"type": "zfspool", "path": ""})
return
}
writeData(responseWriter, map[string]any{
"type": "dir",
"path": "/var/lib/vz",
"total": 500 * 1024 * 1024 * 1024,
"avail": 200 * 1024 * 1024 * 1024,
})
}
// handleArchiveDeletion nimmt das Entfernen eines Archivs entgegen.
func (fakeServer *fakeProxmoxServer) handleArchiveDeletion(responseWriter http.ResponseWriter, apiPath string) {
fakeServer.mutex.Lock()
fakeServer.deletedArchives = append(fakeServer.deletedArchives, apiPath)
fakeServer.mutex.Unlock()
writeData(responseWriter, "UPID:pve-01:00001234:00ABCDEF:66B00000:imgdel:local:root@pam:")
}
// deletedArchiveList liefert die entfernten Archive.
//
// Ueber die Sperre, weil das Aufraeumen aus dem Close() eines Datenstroms
// heraus geschieht — also nicht zwingend aus dem Ablauf des Tests.
func (fakeServer *fakeProxmoxServer) deletedArchiveList() []string {
fakeServer.mutex.Lock()
defer fakeServer.mutex.Unlock()
return append([]string(nil), fakeServer.deletedArchives...)
}