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>
456 lines
17 KiB
Go
456 lines
17 KiB
Go
package proxmox
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"io"
|
|
"log/slog"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/syncova/syncova/packages/providers"
|
|
)
|
|
|
|
// TestConnectRejectsPlainHTTP prüft, dass unverschlüsselte Endpunkte abgelehnt
|
|
// werden.
|
|
//
|
|
// Über eine unverschlüsselte Verbindung ginge das API-Token im Klartext auf die
|
|
// Leitung — es ist dauerhaft gültig und öffnet den gesamten Verbund.
|
|
func TestConnectRejectsPlainHTTP(testInstance *testing.T) {
|
|
testLogger := slog.New(slog.NewJSONHandler(io.Discard, nil))
|
|
|
|
_, clientError := NewClient(ClientOptions{
|
|
BaseURL: "http://pve.example:8006",
|
|
APITokenID: "syncova@pve!backup",
|
|
APITokenSecret: "geheim",
|
|
}, testLogger)
|
|
|
|
if clientError == nil {
|
|
testInstance.Fatal("ein unverschlüsselter endpunkt muss abgelehnt werden")
|
|
}
|
|
|
|
if !strings.Contains(clientError.Error(), "https") {
|
|
testInstance.Errorf("die Fehlermeldung nennt die Ursache nicht: %v", clientError)
|
|
}
|
|
}
|
|
|
|
// TestConnectRequiresCredentials prüft die Pflichtangaben.
|
|
func TestConnectRequiresCredentials(testInstance *testing.T) {
|
|
testLogger := slog.New(slog.NewJSONHandler(io.Discard, nil))
|
|
|
|
_, clientError := NewClient(ClientOptions{
|
|
BaseURL: "https://pve.example:8006",
|
|
APITokenID: "syncova@pve!backup",
|
|
}, testLogger)
|
|
|
|
if !errors.Is(clientError, ErrMissingCredentials) {
|
|
testInstance.Fatalf("fehlende Anmeldedaten müssen als solche gemeldet werden: %v", clientError)
|
|
}
|
|
}
|
|
|
|
// TestTLSFingerprintPinningAccepts prüft die Bindung an ein Zertifikat.
|
|
//
|
|
// Proxmox liefert ab Werk ein selbstsigniertes Zertifikat. Der übliche Ausweg
|
|
// ist, jede Prüfung abzuschalten — der Fingerabdruck ist die Alternative, die
|
|
// keine Sicherheit aufgibt.
|
|
func TestTLSFingerprintPinningAccepts(testInstance *testing.T) {
|
|
fakeServer := newFakeProxmoxServer(testInstance)
|
|
testLogger := slog.New(slog.NewJSONHandler(io.Discard, nil))
|
|
|
|
serverCertificate := fakeServer.httpServer.Certificate()
|
|
certificateDigest := sha256.Sum256(serverCertificate.Raw)
|
|
|
|
pinnedProvider, providerError := NewProvider(ProviderOptions{
|
|
ClientOptions: ClientOptions{
|
|
BaseURL: fakeServer.httpServer.URL,
|
|
APITokenID: "syncova@pve!backup",
|
|
APITokenSecret: "geheim",
|
|
// Bewusst in der Schreibweise, die Proxmox anzeigt: Großbuchstaben
|
|
// mit Doppelpunkten.
|
|
TLSFingerprintSHA256: strings.ToUpper(insertColons(hex.EncodeToString(certificateDigest[:]))),
|
|
},
|
|
}, testLogger)
|
|
if providerError != nil {
|
|
testInstance.Fatalf("der Provider ließ sich nicht bauen: %v", providerError)
|
|
}
|
|
|
|
if connectError := pinnedProvider.Connect(context.Background()); connectError != nil {
|
|
testInstance.Fatalf("die Verbindung mit hinterlegtem Fingerabdruck schlug fehl: %v", connectError)
|
|
}
|
|
}
|
|
|
|
// TestTLSFingerprintPinningRejectsWrongCertificate prüft die Abweisung.
|
|
func TestTLSFingerprintPinningRejectsWrongCertificate(testInstance *testing.T) {
|
|
fakeServer := newFakeProxmoxServer(testInstance)
|
|
testLogger := slog.New(slog.NewJSONHandler(io.Discard, nil))
|
|
|
|
pinnedProvider, providerError := NewProvider(ProviderOptions{
|
|
ClientOptions: ClientOptions{
|
|
BaseURL: fakeServer.httpServer.URL,
|
|
APITokenID: "syncova@pve!backup",
|
|
APITokenSecret: "geheim",
|
|
TLSFingerprintSHA256: strings.Repeat("aa", 32),
|
|
MaximumRetries: 1,
|
|
},
|
|
}, testLogger)
|
|
if providerError != nil {
|
|
testInstance.Fatalf("der Provider ließ sich nicht bauen: %v", providerError)
|
|
}
|
|
|
|
connectError := pinnedProvider.Connect(context.Background())
|
|
if connectError == nil {
|
|
testInstance.Fatal("ein fremdes Zertifikat muss die Verbindung verhindern")
|
|
}
|
|
}
|
|
|
|
// insertColons formatiert einen Fingerabdruck in der Proxmox-Schreibweise.
|
|
func insertColons(hexadecimalText string) string {
|
|
var builder strings.Builder
|
|
|
|
for characterIndex := 0; characterIndex < len(hexadecimalText); characterIndex += 2 {
|
|
if characterIndex > 0 {
|
|
builder.WriteByte(':')
|
|
}
|
|
|
|
builder.WriteString(hexadecimalText[characterIndex : characterIndex+2])
|
|
}
|
|
|
|
return builder.String()
|
|
}
|
|
|
|
// TestTokenNeverAppearsInErrors prüft, dass das Geheimnis nicht durchsickert.
|
|
//
|
|
// Ein Token in einer Fehlermeldung landet im Protokoll, im Ticketsystem und in
|
|
// der Oberfläche (PROMPT.md §141).
|
|
func TestTokenNeverAppearsInErrors(testInstance *testing.T) {
|
|
testLogger := slog.New(slog.NewJSONHandler(io.Discard, nil))
|
|
|
|
const tokenSecret = "streng-geheimes-token-12345" // secretscan:erlaubt: erfundener Testwert
|
|
|
|
unreachableProvider, providerError := NewProvider(ProviderOptions{
|
|
ClientOptions: ClientOptions{
|
|
BaseURL: "https://127.0.0.1:1/",
|
|
APITokenID: "syncova@pve!backup",
|
|
APITokenSecret: tokenSecret,
|
|
RequestTimeout: time.Second,
|
|
MaximumRetries: 1,
|
|
},
|
|
}, testLogger)
|
|
if providerError != nil {
|
|
testInstance.Fatalf("der Provider ließ sich nicht bauen: %v", providerError)
|
|
}
|
|
|
|
connectError := unreachableProvider.Connect(context.Background())
|
|
if connectError == nil {
|
|
testInstance.Fatal("ein nicht erreichbarer Endpunkt muss einen Fehler ergeben")
|
|
}
|
|
|
|
if strings.Contains(connectError.Error(), tokenSecret) {
|
|
testInstance.Fatalf("das Token steht in der Fehlermeldung: %v", connectError)
|
|
}
|
|
}
|
|
|
|
// TestDiscoveryFindsClustersHostsAndGuests prüft die Erfassung.
|
|
func TestDiscoveryFindsClustersHostsAndGuests(testInstance *testing.T) {
|
|
fakeServer := newFakeProxmoxServer(testInstance)
|
|
testProvider := fakeServer.newConnectedProvider(testInstance)
|
|
|
|
discoveryContext := context.Background()
|
|
|
|
discoveredClusters, clusterError := testProvider.ListClusters(discoveryContext)
|
|
if clusterError != nil {
|
|
testInstance.Fatalf("die Verbunderfassung schlug fehl: %v", clusterError)
|
|
}
|
|
|
|
if len(discoveredClusters) != 1 || discoveredClusters[0].Name != "syncova-lab" {
|
|
testInstance.Fatalf("der Verbund wurde nicht erkannt: %+v", discoveredClusters)
|
|
}
|
|
|
|
if !discoveredClusters[0].Quorate {
|
|
testInstance.Error("der Verbund wurde fälschlich als nicht beschlussfähig gemeldet")
|
|
}
|
|
|
|
discoveredHosts, hostError := testProvider.ListHosts(discoveryContext, "syncova-lab")
|
|
if hostError != nil {
|
|
testInstance.Fatalf("die Knotenerfassung schlug fehl: %v", hostError)
|
|
}
|
|
|
|
if len(discoveredHosts) != 3 {
|
|
testInstance.Fatalf("es wurden 3 Knoten erwartet, gefunden wurden %d", len(discoveredHosts))
|
|
}
|
|
|
|
// Der abgeschaltete Knoten muss als solcher erscheinen und nicht fehlen:
|
|
// eine Wiederherstellung dorthin scheiterte sonst erst nach Stunden.
|
|
if discoveredHosts[2].Online {
|
|
testInstance.Error("der abgeschaltete Knoten wurde als erreichbar gemeldet")
|
|
}
|
|
|
|
discoveredGuests, guestError := testProvider.ListVMs(discoveryContext, "")
|
|
if guestError != nil {
|
|
testInstance.Fatalf("die Gasterfassung schlug fehl: %v", guestError)
|
|
}
|
|
|
|
// Drei Gäste — die Vorlage darf nicht dabei sein.
|
|
if len(discoveredGuests) != 3 {
|
|
testInstance.Fatalf("es wurden 3 Gäste erwartet, gefunden wurden %d: %+v", len(discoveredGuests), discoveredGuests)
|
|
}
|
|
|
|
for _, discoveredGuest := range discoveredGuests {
|
|
if strings.Contains(discoveredGuest.Name, "vorlage") {
|
|
testInstance.Errorf("eine Vorlage wurde als sicherbarer Gast gemeldet: %s", discoveredGuest.Name)
|
|
}
|
|
}
|
|
|
|
// Der Container muss als Container erkannt werden.
|
|
var foundContainer bool
|
|
for _, discoveredGuest := range discoveredGuests {
|
|
if discoveredGuest.Identifier == "lxc/200" && discoveredGuest.GuestType == providers.GuestTypeContainer {
|
|
foundContainer = true
|
|
}
|
|
}
|
|
|
|
if !foundContainer {
|
|
testInstance.Error("der LXC-Container wurde nicht als Container erkannt")
|
|
}
|
|
}
|
|
|
|
// TestListVMsFiltersByHost prüft die Einschränkung auf einen Knoten.
|
|
func TestListVMsFiltersByHost(testInstance *testing.T) {
|
|
fakeServer := newFakeProxmoxServer(testInstance)
|
|
testProvider := fakeServer.newConnectedProvider(testInstance)
|
|
|
|
guestsOnSecondNode, listError := testProvider.ListVMs(context.Background(), "pve-02")
|
|
if listError != nil {
|
|
testInstance.Fatalf("die Gasterfassung schlug fehl: %v", listError)
|
|
}
|
|
|
|
if len(guestsOnSecondNode) != 1 || guestsOnSecondNode[0].Identifier != "qemu/101" {
|
|
testInstance.Fatalf("die Einschränkung auf pve-02 wirkte nicht: %+v", guestsOnSecondNode)
|
|
}
|
|
}
|
|
|
|
// TestGetVMDisksSeparatesRealDisksFromMedia ist der wichtigste Erfassungstest.
|
|
//
|
|
// Zwei Fehler wären hier folgenschwer: ein eingelegtes ISO-Abbild als Platte zu
|
|
// sichern, und eine mit backup=0 ausgenommene Platte nicht als solche zu
|
|
// erkennen — im zweiten Fall hielte man eine unvollständige Maschine für
|
|
// vollständig.
|
|
func TestGetVMDisksSeparatesRealDisksFromMedia(testInstance *testing.T) {
|
|
fakeServer := newFakeProxmoxServer(testInstance)
|
|
testProvider := fakeServer.newConnectedProvider(testInstance)
|
|
|
|
discoveredDisks, diskError := testProvider.GetVMDisks(context.Background(), "qemu/100")
|
|
if diskError != nil {
|
|
testInstance.Fatalf("die Plattenerfassung schlug fehl: %v", diskError)
|
|
}
|
|
|
|
disksByIdentifier := make(map[string]providers.Disk, len(discoveredDisks))
|
|
for _, discoveredDisk := range discoveredDisks {
|
|
disksByIdentifier[discoveredDisk.Identifier] = discoveredDisk
|
|
}
|
|
|
|
// Das ISO-Abbild an ide2 ist keine Platte.
|
|
if _, hasCDROM := disksByIdentifier["ide2"]; hasCDROM {
|
|
testInstance.Error("ein eingelegtes ISO-Abbild wurde als Platte erfasst")
|
|
}
|
|
|
|
systemDisk, hasSystemDisk := disksByIdentifier["scsi0"]
|
|
if !hasSystemDisk {
|
|
testInstance.Fatal("die Systemplatte wurde nicht erfasst")
|
|
}
|
|
|
|
if systemDisk.SizeBytes != 32<<30 {
|
|
testInstance.Errorf("die Größe wurde als %d Byte gelesen, erwartet waren %d", systemDisk.SizeBytes, int64(32)<<30)
|
|
}
|
|
|
|
if systemDisk.StorageID != "local-lvm" {
|
|
testInstance.Errorf("der Speicher wurde als %q gelesen", systemDisk.StorageID)
|
|
}
|
|
|
|
if systemDisk.ExcludedFromBackup {
|
|
testInstance.Error("die Systemplatte wurde fälschlich als von der Sicherung ausgenommen gemeldet")
|
|
}
|
|
|
|
dataDisk, hasDataDisk := disksByIdentifier["scsi1"]
|
|
if !hasDataDisk {
|
|
testInstance.Fatal("die Datenplatte wurde nicht erfasst")
|
|
}
|
|
|
|
// Der entscheidende Punkt: backup=0 muss sichtbar werden.
|
|
if !dataDisk.ExcludedFromBackup {
|
|
testInstance.Fatal("eine mit backup=0 ausgenommene Platte wurde nicht als solche erkannt")
|
|
}
|
|
|
|
// Die abgehängte Platte trägt Daten und gehört ins Backup.
|
|
if _, hasUnused := disksByIdentifier["unused0"]; !hasUnused {
|
|
testInstance.Error("eine abgehängte Platte wurde übergangen, obwohl sie Daten enthält")
|
|
}
|
|
|
|
// Die EFI-Platte ist klein, aber ohne sie startet eine UEFI-Maschine nicht.
|
|
if _, hasEFI := disksByIdentifier["efidisk0"]; !hasEFI {
|
|
testInstance.Error("die EFI-Platte wurde übergangen; eine UEFI-Maschine startete ohne sie nicht")
|
|
}
|
|
}
|
|
|
|
// TestGetVMDisksParsesFractionalSizes prüft Größen wie "1.5T".
|
|
func TestGetVMDisksParsesFractionalSizes(testInstance *testing.T) {
|
|
fakeServer := newFakeProxmoxServer(testInstance)
|
|
testProvider := fakeServer.newConnectedProvider(testInstance)
|
|
|
|
discoveredDisks, diskError := testProvider.GetVMDisks(context.Background(), "qemu/101")
|
|
if diskError != nil {
|
|
testInstance.Fatalf("die Plattenerfassung schlug fehl: %v", diskError)
|
|
}
|
|
|
|
if len(discoveredDisks) != 1 {
|
|
testInstance.Fatalf("es wurde eine Platte erwartet, gefunden wurden %d", len(discoveredDisks))
|
|
}
|
|
|
|
expectedSize := int64(1.5 * float64(int64(1)<<40))
|
|
if discoveredDisks[0].SizeBytes != expectedSize {
|
|
testInstance.Errorf("die Größe 1.5T wurde als %d Byte gelesen, erwartet waren %d",
|
|
discoveredDisks[0].SizeBytes, expectedSize)
|
|
}
|
|
}
|
|
|
|
// TestGetVMDisksHandlesContainerMountpoints prüft LXC.
|
|
func TestGetVMDisksHandlesContainerMountpoints(testInstance *testing.T) {
|
|
fakeServer := newFakeProxmoxServer(testInstance)
|
|
testProvider := fakeServer.newConnectedProvider(testInstance)
|
|
|
|
discoveredDisks, diskError := testProvider.GetVMDisks(context.Background(), "lxc/200")
|
|
if diskError != nil {
|
|
testInstance.Fatalf("die Plattenerfassung schlug fehl: %v", diskError)
|
|
}
|
|
|
|
disksByIdentifier := make(map[string]providers.Disk, len(discoveredDisks))
|
|
for _, discoveredDisk := range discoveredDisks {
|
|
disksByIdentifier[discoveredDisk.Identifier] = discoveredDisk
|
|
}
|
|
|
|
if _, hasRootFS := disksByIdentifier["rootfs"]; !hasRootFS {
|
|
testInstance.Error("das Wurzeldateisystem des Containers wurde nicht erfasst")
|
|
}
|
|
|
|
if _, hasMountpoint := disksByIdentifier["mp0"]; !hasMountpoint {
|
|
testInstance.Error("ein Einhängepunkt des Containers wurde nicht erfasst")
|
|
}
|
|
}
|
|
|
|
// TestGetVMMetaDataPreservesConfiguration prüft die Konfigurationserfassung.
|
|
func TestGetVMMetaDataPreservesConfiguration(testInstance *testing.T) {
|
|
fakeServer := newFakeProxmoxServer(testInstance)
|
|
testProvider := fakeServer.newConnectedProvider(testInstance)
|
|
|
|
guestMetadata, metadataError := testProvider.GetVMMetaData(context.Background(), "qemu/100")
|
|
if metadataError != nil {
|
|
testInstance.Fatalf("die Konfigurationserfassung schlug fehl: %v", metadataError)
|
|
}
|
|
|
|
if guestMetadata.FirmwareType != "ovmf" {
|
|
testInstance.Errorf("die Firmware wurde als %q gelesen, erwartet war ovmf", guestMetadata.FirmwareType)
|
|
}
|
|
|
|
if !guestMetadata.GuestAgentEnabled {
|
|
testInstance.Error("der eingerichtete Gastdienst wurde nicht erkannt")
|
|
}
|
|
|
|
if len(guestMetadata.NetworkInterfaces) != 2 {
|
|
testInstance.Fatalf("es wurden 2 Netzwerkkarten erwartet, gefunden wurden %d", len(guestMetadata.NetworkInterfaces))
|
|
}
|
|
|
|
firstInterface := guestMetadata.NetworkInterfaces[0]
|
|
|
|
// Die MAC-Adresse muss erhalten bleiben: Lizenzbindungen und
|
|
// DHCP-Reservierungen hängen daran.
|
|
if firstInterface.MACAddress != "AA:BB:CC:DD:EE:01" {
|
|
testInstance.Errorf("die MAC-Adresse wurde als %q gelesen", firstInterface.MACAddress)
|
|
}
|
|
|
|
if firstInterface.Model != "virtio" {
|
|
testInstance.Errorf("das Kartenmodell wurde als %q gelesen", firstInterface.Model)
|
|
}
|
|
|
|
if firstInterface.VLANTag != 42 {
|
|
testInstance.Errorf("das VLAN-Etikett wurde als %d gelesen", firstInterface.VLANTag)
|
|
}
|
|
|
|
expectedBootOrder := []string{"scsi0", "net0"}
|
|
if len(guestMetadata.BootOrder) != len(expectedBootOrder) {
|
|
testInstance.Fatalf("die Startreihenfolge wurde als %v gelesen", guestMetadata.BootOrder)
|
|
}
|
|
|
|
// Die Rohkonfiguration muss wortgetreu erhalten bleiben — auch die Felder,
|
|
// die wir heute nicht deuten.
|
|
if guestMetadata.RawConfiguration["scsi0"] != "local-lvm:vm-100-disk-0,size=32G,ssd=1" {
|
|
testInstance.Errorf("die Rohkonfiguration wurde verändert: %q", guestMetadata.RawConfiguration["scsi0"])
|
|
}
|
|
}
|
|
|
|
// TestGetVMMetaDataDefaultsFirmwareForQEMU prüft den weggelassenen Standard.
|
|
//
|
|
// Proxmox lässt "bios" weg, wenn SeaBIOS gilt. Das Feld leer zu lassen wäre
|
|
// missverständlich: eine mit UEFI wiederhergestellte Maschine startet nicht.
|
|
func TestGetVMMetaDataDefaultsFirmwareForQEMU(testInstance *testing.T) {
|
|
fakeServer := newFakeProxmoxServer(testInstance)
|
|
testProvider := fakeServer.newConnectedProvider(testInstance)
|
|
|
|
guestMetadata, metadataError := testProvider.GetVMMetaData(context.Background(), "qemu/101")
|
|
if metadataError != nil {
|
|
testInstance.Fatalf("die Konfigurationserfassung schlug fehl: %v", metadataError)
|
|
}
|
|
|
|
if guestMetadata.FirmwareType != "seabios" {
|
|
testInstance.Errorf("ohne Angabe muss seabios gelten, gelesen wurde %q", guestMetadata.FirmwareType)
|
|
}
|
|
|
|
if guestMetadata.GuestAgentEnabled {
|
|
testInstance.Error("ohne agent=1 darf kein Gastdienst angenommen werden")
|
|
}
|
|
}
|
|
|
|
// TestGetVMMetaDataParsesLegacyBootOrder prüft die alte Schreibweise.
|
|
func TestGetVMMetaDataParsesLegacyBootOrder(testInstance *testing.T) {
|
|
fakeServer := newFakeProxmoxServer(testInstance)
|
|
testProvider := fakeServer.newConnectedProvider(testInstance)
|
|
|
|
guestMetadata, metadataError := testProvider.GetVMMetaData(context.Background(), "qemu/101")
|
|
if metadataError != nil {
|
|
testInstance.Fatalf("die Konfigurationserfassung schlug fehl: %v", metadataError)
|
|
}
|
|
|
|
expectedOrder := []string{"cdrom", "disk", "network"}
|
|
if len(guestMetadata.BootOrder) != len(expectedOrder) {
|
|
testInstance.Fatalf("die alte Startreihenfolge 'cdn' wurde als %v gelesen", guestMetadata.BootOrder)
|
|
}
|
|
}
|
|
|
|
// TestGuestNotFoundIsReportedClearly prüft die Meldung eines fehlenden Gasts.
|
|
func TestGuestNotFoundIsReportedClearly(testInstance *testing.T) {
|
|
fakeServer := newFakeProxmoxServer(testInstance)
|
|
testProvider := fakeServer.newConnectedProvider(testInstance)
|
|
|
|
_, infoError := testProvider.GetVMInfo(context.Background(), "qemu/999")
|
|
|
|
if !errors.Is(infoError, providers.ErrGuestNotFound) {
|
|
testInstance.Fatalf("ein fehlender Gast muss als solcher gemeldet werden: %v", infoError)
|
|
}
|
|
}
|
|
|
|
// TestInvalidGuestIdentifierIsRejected prüft die Kennungsprüfung.
|
|
func TestInvalidGuestIdentifierIsRejected(testInstance *testing.T) {
|
|
fakeServer := newFakeProxmoxServer(testInstance)
|
|
testProvider := fakeServer.newConnectedProvider(testInstance)
|
|
|
|
invalidIdentifiers := []string{"100", "vmware/100", "qemu/abc", "qemu/0", ""}
|
|
|
|
for _, invalidIdentifier := range invalidIdentifiers {
|
|
if _, diskError := testProvider.GetVMDisks(context.Background(), invalidIdentifier); diskError == nil {
|
|
testInstance.Errorf("die ungültige Kennung %q wurde angenommen", invalidIdentifier)
|
|
}
|
|
}
|
|
}
|