package hypervisor import ( "context" "encoding/json" "fmt" "github.com/google/uuid" "github.com/syncova/syncova/packages/providers" ) // saveHosts schreibt die Knoten fort und liefert ihre Kennungen. // // Der Rückgabewert ordnet die Providerkennung des Wirts der Datenbankkennung // zu; ohne sie ließe sich ein Gast nicht an seinen Knoten binden. func (store *Store) saveHosts(saveContext context.Context, clusterIdentifier uuid.UUID, foundHosts []providers.Host) (map[string]uuid.UUID, error) { const upsertStatement = ` INSERT INTO proxmox_hosts (cluster_id, node_name, status, cpu_count, memory_bytes, last_seen_at) VALUES ($1, $2, $3, $4, $5, NOW()) ON CONFLICT (cluster_id, node_name) DO UPDATE SET status = EXCLUDED.status, cpu_count = EXCLUDED.cpu_count, memory_bytes = EXCLUDED.memory_bytes, last_seen_at = NOW(), updated_at = NOW() RETURNING id` hostIdentifiers := make(map[string]uuid.UUID, len(foundHosts)) for _, foundHost := range foundHosts { hostStatus := "offline" if foundHost.Online { hostStatus = "online" } var storedIdentifier uuid.UUID scanError := store.connectionPool.QueryRow(saveContext, upsertStatement, clusterIdentifier, foundHost.Name, hostStatus, zeroToNil(foundHost.CPUCount), zeroToNilInt64(foundHost.MemoryBytes)).Scan(&storedIdentifier) if scanError != nil { return nil, fmt.Errorf("der knoten %q liess sich nicht speichern: %w", foundHost.Name, scanError) } // Beide Schreibweisen eintragen: Ein Gast nennt als Wirt mal die // Kennung, mal den Knotennamen — je nachdem, woher er stammt. hostIdentifiers[foundHost.Identifier] = storedIdentifier hostIdentifiers[foundHost.Name] = storedIdentifier } return hostIdentifiers, nil } // saveGuest schreibt einen Gast fort. func (store *Store) saveGuest(saveContext context.Context, clusterIdentifier uuid.UUID, hostIdentifier uuid.UUID, foundGuest providers.Guest, guestDisks []providers.Disk, guestMetadata *providers.GuestMetadata) error { encodedDisks, encodeError := json.Marshal(guestDisks) if encodeError != nil { return fmt.Errorf("die plattenliste von %q liess sich nicht kodieren: %w", foundGuest.Name, encodeError) } var ( encodedConfiguration []byte guestAgentRunning *bool ) if guestMetadata != nil { configuration, configurationError := json.Marshal(guestMetadata.RawConfiguration) if configurationError != nil { return fmt.Errorf("die konfiguration von %q liess sich nicht kodieren: %w", foundGuest.Name, configurationError) } encodedConfiguration = configuration // Nur wenn die Konfiguration gelesen wurde, gibt es überhaupt eine // Aussage. Ohne sie bleibt das Feld NULL — „nicht geprüft" ist etwas // anderes als „läuft nicht". agentState := guestMetadata.GuestAgentEnabled guestAgentRunning = &agentState } guestKind := "qemu" if foundGuest.GuestType == providers.GuestTypeContainer { guestKind = "lxc" } const upsertStatement = ` INSERT INTO virtual_machines ( cluster_id, host_id, provider_vm_id, name, guest_kind, status, cpu_count, memory_bytes, config_json, disks_json, guest_agent_running, last_discovered_at, missing_since) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, NOW(), NULL) ON CONFLICT (cluster_id, provider_vm_id) DO UPDATE SET host_id = EXCLUDED.host_id, name = EXCLUDED.name, guest_kind = EXCLUDED.guest_kind, status = EXCLUDED.status, cpu_count = EXCLUDED.cpu_count, memory_bytes = EXCLUDED.memory_bytes, -- Eine fehlgeschlagene Konfigurationsabfrage darf die zuletzt -- bekannte nicht loeschen: Sie ist das, womit eine -- Wiederherstellung die Maschine in ihrer Gestalt aufbaut. config_json = COALESCE(EXCLUDED.config_json, virtual_machines.config_json), disks_json = COALESCE(EXCLUDED.disks_json, virtual_machines.disks_json), guest_agent_running = COALESCE(EXCLUDED.guest_agent_running, virtual_machines.guest_agent_running), last_discovered_at = NOW(), missing_since = NULL, updated_at = NOW()` var hostReference *uuid.UUID if hostIdentifier != uuid.Nil { hostReference = &hostIdentifier } _, executeError := store.connectionPool.Exec(saveContext, upsertStatement, clusterIdentifier, hostReference, foundGuest.Identifier, foundGuest.Name, guestKind, emptyToNil(string(foundGuest.PowerState)), zeroToNil(foundGuest.CPUCount), zeroToNilInt64(foundGuest.MemoryBytes), encodedConfiguration, encodedDisks, guestAgentRunning) if executeError != nil { return fmt.Errorf("der gast %q liess sich nicht speichern: %w", foundGuest.Name, executeError) } return nil } // markMissingGuests vermerkt Gäste, die bei dieser Aufnahme fehlten. // // Gelöscht wird nichts. Ein Gast kann abgeschaltet, verschoben oder tatsächlich // entfernt worden sein — und eine gelöschte Zeile nähme die Zuordnung zu // vorhandenen Backups mit. Wer sie braucht, braucht sie genau dann, wenn die // Maschine weg ist. func (store *Store) markMissingGuests(markContext context.Context, clusterIdentifier uuid.UUID, seenIdentifiers []string) (int, error) { // Eine leere Liste muss als leeres Array übergeben werden: `x = ANY(NULL)` // ist niemals wahr, und `NOT (x = ANY(NULL))` ist niemals falsch — es ist // NULL. Der Fund aus Phase 14, an anderer Stelle. if seenIdentifiers == nil { seenIdentifiers = []string{} } const updateStatement = ` UPDATE virtual_machines SET missing_since = COALESCE(missing_since, NOW()), updated_at = NOW() WHERE cluster_id = $1 AND NOT (provider_vm_id = ANY($2::text[])) AND missing_since IS NULL` commandTag, executeError := store.connectionPool.Exec(markContext, updateStatement, clusterIdentifier, seenIdentifiers) if executeError != nil { return 0, fmt.Errorf("die fehlenden gaeste liessen sich nicht vermerken: %w", executeError) } return int(commandTag.RowsAffected()), nil } // zeroToNilInt64 macht aus einer Null ein NULL. func zeroToNilInt64(rawValue int64) *int64 { if rawValue == 0 { return nil } return &rawValue }