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>
355 lines
12 KiB
Go
355 lines
12 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"errors"
|
|
"log/slog"
|
|
"net/http"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/syncova/syncova/packages/audit"
|
|
"github.com/syncova/syncova/packages/hypervisor"
|
|
"github.com/syncova/syncova/packages/platform/logging"
|
|
)
|
|
|
|
// hypervisorHandler bedient die Endpunkte der Virtualisierungsumgebungen.
|
|
type hypervisorHandler struct {
|
|
// clusterStore ist die Datenzugriffsschicht der Verbünde.
|
|
clusterStore *hypervisor.Store
|
|
// auditRecorder protokolliert die verändernden Zugriffe.
|
|
auditRecorder audit.Recorder
|
|
// logger protokolliert technische Fehler.
|
|
logger *slog.Logger
|
|
}
|
|
|
|
// handleListClusters bedient GET /proxmox/clusters.
|
|
func (handler *hypervisorHandler) handleListClusters(responseWriter http.ResponseWriter, request *http.Request) {
|
|
requestLogger := logging.WithContext(request.Context(), handler.logger)
|
|
|
|
clusterList, listError := handler.clusterStore.ListClusters(request.Context())
|
|
if listError != nil {
|
|
WriteError(responseWriter, request, requestLogger, NewInternalError(listError))
|
|
|
|
return
|
|
}
|
|
|
|
WriteSuccess(responseWriter, request, http.StatusOK, clusterList)
|
|
}
|
|
|
|
// handleCreateCluster bedient POST /proxmox/clusters.
|
|
func (handler *hypervisorHandler) handleCreateCluster(responseWriter http.ResponseWriter, request *http.Request) {
|
|
requestLogger := logging.WithContext(request.Context(), handler.logger)
|
|
actingUser, _ := AuthenticatedUserFromContext(request.Context())
|
|
|
|
var clusterInput hypervisor.ClusterInput
|
|
if decodeError := decodeJSONBody(request, &clusterInput); decodeError != nil {
|
|
WriteError(responseWriter, request, requestLogger, decodeError)
|
|
|
|
return
|
|
}
|
|
|
|
createdCluster, createError := handler.clusterStore.CreateCluster(request.Context(), clusterInput)
|
|
if createError != nil {
|
|
// Eine ungültige Eingabe ist kein Serverfehler. Die Meldung wird
|
|
// wörtlich weitergereicht, weil sie erklärt, was fehlt — eine
|
|
// allgemeine „Eingabe ungültig" ließe den Betreiber raten.
|
|
WriteError(responseWriter, request, requestLogger, NewValidationError(createError.Error()))
|
|
|
|
return
|
|
}
|
|
|
|
// Ein neuer Verbund bedeutet neue Zugangsdaten im System — das gehört ins
|
|
// Protokoll, unabhängig davon, ob es später jemanden interessiert.
|
|
handler.recordAudit(request, actingUser.ID, actingUser.Username, audit.ActionHypervisorClusterCreated,
|
|
createdCluster.ID, map[string]any{
|
|
"name": createdCluster.Name,
|
|
"api_endpoint": createdCluster.APIEndpoint,
|
|
"archive_transport": string(createdCluster.ArchiveTransport),
|
|
})
|
|
|
|
WriteSuccess(responseWriter, request, http.StatusCreated, createdCluster)
|
|
}
|
|
|
|
// handleGetCluster bedient GET /proxmox/clusters/{id}.
|
|
func (handler *hypervisorHandler) handleGetCluster(responseWriter http.ResponseWriter, request *http.Request) {
|
|
requestLogger := logging.WithContext(request.Context(), handler.logger)
|
|
|
|
clusterIdentifier, parseError := uuid.Parse(request.PathValue("id"))
|
|
if parseError != nil {
|
|
WriteError(responseWriter, request, requestLogger, NewBadRequestError("Die Kennung des Verbunds ist ungültig."))
|
|
|
|
return
|
|
}
|
|
|
|
foundCluster, readError := handler.clusterStore.GetCluster(request.Context(), clusterIdentifier)
|
|
if errors.Is(readError, hypervisor.ErrClusterNotFound) {
|
|
WriteError(responseWriter, request, requestLogger, NewNotFoundError("Der Verbund wurde nicht gefunden."))
|
|
|
|
return
|
|
}
|
|
|
|
if readError != nil {
|
|
WriteError(responseWriter, request, requestLogger, NewInternalError(readError))
|
|
|
|
return
|
|
}
|
|
|
|
WriteSuccess(responseWriter, request, http.StatusOK, foundCluster)
|
|
}
|
|
|
|
// handleTestConnection bedient POST /proxmox/clusters/{id}/test.
|
|
//
|
|
// Die Prüfung verändert nichts am Verbund und ist der erste Schritt nach dem
|
|
// Einrichten: Sie sagt, ob Anmeldung und Zertifikatsbindung stimmen, bevor um
|
|
// zwei Uhr nachts eine Sicherung daran scheitert.
|
|
func (handler *hypervisorHandler) handleTestConnection(responseWriter http.ResponseWriter, request *http.Request) {
|
|
requestLogger := logging.WithContext(request.Context(), handler.logger)
|
|
|
|
clusterIdentifier, parseError := uuid.Parse(request.PathValue("id"))
|
|
if parseError != nil {
|
|
WriteError(responseWriter, request, requestLogger, NewBadRequestError("Die Kennung des Verbunds ist ungültig."))
|
|
|
|
return
|
|
}
|
|
|
|
connectionError := handler.clusterStore.TestConnection(request.Context(), clusterIdentifier, handler.logger)
|
|
|
|
if errors.Is(connectionError, hypervisor.ErrClusterNotFound) {
|
|
WriteError(responseWriter, request, requestLogger, NewNotFoundError("Der Verbund wurde nicht gefunden."))
|
|
|
|
return
|
|
}
|
|
|
|
// Der Zustand nach der Prüfung ist die eigentliche Antwort — auch im
|
|
// Fehlerfall. Ein 500 verschwiege, dass die Prüfung ordnungsgemäß gelaufen
|
|
// ist und ein Ergebnis hat.
|
|
updatedCluster, readError := handler.clusterStore.GetCluster(request.Context(), clusterIdentifier)
|
|
if readError != nil {
|
|
WriteError(responseWriter, request, requestLogger, NewInternalError(readError))
|
|
|
|
return
|
|
}
|
|
|
|
responsePayload := map[string]any{
|
|
"cluster_id": clusterIdentifier,
|
|
"status": updatedCluster.Status,
|
|
"reachable": connectionError == nil,
|
|
}
|
|
|
|
if connectionError != nil {
|
|
responsePayload["error"] = connectionError.Error()
|
|
}
|
|
|
|
WriteSuccess(responseWriter, request, http.StatusOK, responsePayload)
|
|
}
|
|
|
|
// handleDiscover bedient POST /proxmox/clusters/{id}/discover.
|
|
func (handler *hypervisorHandler) handleDiscover(responseWriter http.ResponseWriter, request *http.Request) {
|
|
requestLogger := logging.WithContext(request.Context(), handler.logger)
|
|
|
|
clusterIdentifier, parseError := uuid.Parse(request.PathValue("id"))
|
|
if parseError != nil {
|
|
WriteError(responseWriter, request, requestLogger, NewBadRequestError("Die Kennung des Verbunds ist ungültig."))
|
|
|
|
return
|
|
}
|
|
|
|
discoveryResult, discoveryError := handler.clusterStore.Discover(request.Context(),
|
|
clusterIdentifier, handler.logger)
|
|
|
|
if errors.Is(discoveryError, hypervisor.ErrClusterNotFound) {
|
|
WriteError(responseWriter, request, requestLogger, NewNotFoundError("Der Verbund wurde nicht gefunden."))
|
|
|
|
return
|
|
}
|
|
|
|
if discoveryError != nil {
|
|
// Ein nicht erreichbarer Verbund ist kein Serverfehler, sondern eine
|
|
// Auskunft über die Anlage. 503 statt 500, damit die Oberfläche den
|
|
// Unterschied anzeigen kann.
|
|
WriteError(responseWriter, request, requestLogger,
|
|
NewServiceUnavailableError("Die Bestandsaufnahme schlug fehl: "+discoveryError.Error()))
|
|
|
|
return
|
|
}
|
|
|
|
WriteSuccess(responseWriter, request, http.StatusOK, discoveryResult)
|
|
}
|
|
|
|
// handleDeleteCluster bedient DELETE /proxmox/clusters/{id}.
|
|
func (handler *hypervisorHandler) handleDeleteCluster(responseWriter http.ResponseWriter, request *http.Request) {
|
|
requestLogger := logging.WithContext(request.Context(), handler.logger)
|
|
actingUser, _ := AuthenticatedUserFromContext(request.Context())
|
|
|
|
clusterIdentifier, parseError := uuid.Parse(request.PathValue("id"))
|
|
if parseError != nil {
|
|
WriteError(responseWriter, request, requestLogger, NewBadRequestError("Die Kennung des Verbunds ist ungültig."))
|
|
|
|
return
|
|
}
|
|
|
|
deleteError := handler.clusterStore.DeleteCluster(request.Context(), clusterIdentifier)
|
|
|
|
if errors.Is(deleteError, hypervisor.ErrClusterNotFound) {
|
|
WriteError(responseWriter, request, requestLogger, NewNotFoundError("Der Verbund wurde nicht gefunden."))
|
|
|
|
return
|
|
}
|
|
|
|
// Ein Verbund, auf den noch Sicherungsquellen verweisen, wird nicht
|
|
// entfernt: Die Aufträge verlören ihre Grundlage, und zwar stillschweigend
|
|
// bis zum nächsten Lauf.
|
|
if errors.Is(deleteError, hypervisor.ErrClusterInUse) {
|
|
WriteError(responseWriter, request, requestLogger, &APIError{
|
|
StatusCode: http.StatusConflict,
|
|
Code: ErrorCodeConflict,
|
|
Message: deleteError.Error(),
|
|
})
|
|
|
|
return
|
|
}
|
|
|
|
if deleteError != nil {
|
|
WriteError(responseWriter, request, requestLogger, NewInternalError(deleteError))
|
|
|
|
return
|
|
}
|
|
|
|
handler.recordAudit(request, actingUser.ID, actingUser.Username, audit.ActionHypervisorClusterDeleted,
|
|
clusterIdentifier, nil)
|
|
|
|
WriteSuccess(responseWriter, request, http.StatusOK, map[string]any{"deleted": true})
|
|
}
|
|
|
|
// handleListVirtualMachines bedient GET /virtual-machines.
|
|
func (handler *hypervisorHandler) handleListVirtualMachines(responseWriter http.ResponseWriter, request *http.Request) {
|
|
requestLogger := logging.WithContext(request.Context(), handler.logger)
|
|
|
|
var clusterFilter *uuid.UUID
|
|
|
|
if rawFilter := request.URL.Query().Get("cluster_id"); rawFilter != "" {
|
|
parsedFilter, parseError := uuid.Parse(rawFilter)
|
|
if parseError != nil {
|
|
WriteError(responseWriter, request, requestLogger,
|
|
NewBadRequestError("Die Kennung des Verbunds ist ungültig."))
|
|
|
|
return
|
|
}
|
|
|
|
clusterFilter = &parsedFilter
|
|
}
|
|
|
|
machineList, listError := handler.clusterStore.ListVirtualMachines(request.Context(), clusterFilter)
|
|
if listError != nil {
|
|
WriteError(responseWriter, request, requestLogger, NewInternalError(listError))
|
|
|
|
return
|
|
}
|
|
|
|
WriteSuccess(responseWriter, request, http.StatusOK, machineList)
|
|
}
|
|
|
|
// recordAudit schreibt einen Eintrag ins Auditprotokoll.
|
|
//
|
|
// Ein fehlgeschlagener Eintrag wird protokolliert, aber nicht geworfen: Die
|
|
// Handlung ist geschehen, und ein Fehler an dieser Stelle machte sie nicht
|
|
// ungeschehen — er verschwiege sie nur zusätzlich.
|
|
func (handler *hypervisorHandler) recordAudit(request *http.Request, actingUserID uuid.UUID,
|
|
actingUsername string, auditAction audit.Action, entityIdentifier uuid.UUID,
|
|
auditDetails map[string]any) {
|
|
if handler.auditRecorder == nil {
|
|
return
|
|
}
|
|
|
|
correlationIdentifier, _ := logging.CorrelationIDFromContext(request.Context())
|
|
|
|
recordError := handler.auditRecorder.Record(request.Context(), audit.Event{
|
|
UserID: &actingUserID,
|
|
ActorUsername: actingUsername,
|
|
Action: auditAction,
|
|
EntityType: "proxmox_cluster",
|
|
EntityID: &entityIdentifier,
|
|
Result: audit.ResultSuccess,
|
|
IPAddress: clientIPAddress(request),
|
|
UserAgent: request.UserAgent(),
|
|
CorrelationID: correlationIdentifier,
|
|
Details: auditDetails,
|
|
})
|
|
|
|
if recordError != nil {
|
|
logging.WithContext(request.Context(), handler.logger).Error(
|
|
"der audit-eintrag liess sich nicht schreiben",
|
|
slog.String("aktion", string(auditAction)),
|
|
slog.String("grund", recordError.Error()))
|
|
}
|
|
}
|
|
|
|
// handleListHosts bedient GET /proxmox/clusters/{id}/hosts.
|
|
func (handler *hypervisorHandler) handleListHosts(responseWriter http.ResponseWriter, request *http.Request) {
|
|
requestLogger := logging.WithContext(request.Context(), handler.logger)
|
|
|
|
clusterIdentifier, parseError := uuid.Parse(request.PathValue("id"))
|
|
if parseError != nil {
|
|
WriteError(responseWriter, request, requestLogger, NewBadRequestError("Die Kennung des Verbunds ist ungültig."))
|
|
|
|
return
|
|
}
|
|
|
|
hostList, listError := handler.clusterStore.ListHosts(request.Context(), clusterIdentifier)
|
|
if listError != nil {
|
|
WriteError(responseWriter, request, requestLogger, NewInternalError(listError))
|
|
|
|
return
|
|
}
|
|
|
|
WriteSuccess(responseWriter, request, http.StatusOK, hostList)
|
|
}
|
|
|
|
// handleListClusterMachines bedient GET /proxmox/clusters/{id}/vms.
|
|
func (handler *hypervisorHandler) handleListClusterMachines(responseWriter http.ResponseWriter, request *http.Request) {
|
|
requestLogger := logging.WithContext(request.Context(), handler.logger)
|
|
|
|
clusterIdentifier, parseError := uuid.Parse(request.PathValue("id"))
|
|
if parseError != nil {
|
|
WriteError(responseWriter, request, requestLogger, NewBadRequestError("Die Kennung des Verbunds ist ungültig."))
|
|
|
|
return
|
|
}
|
|
|
|
machineList, listError := handler.clusterStore.ListVirtualMachines(request.Context(), &clusterIdentifier)
|
|
if listError != nil {
|
|
WriteError(responseWriter, request, requestLogger, NewInternalError(listError))
|
|
|
|
return
|
|
}
|
|
|
|
WriteSuccess(responseWriter, request, http.StatusOK, machineList)
|
|
}
|
|
|
|
// handleGetVirtualMachine bedient GET /proxmox/vms/{id}.
|
|
func (handler *hypervisorHandler) handleGetVirtualMachine(responseWriter http.ResponseWriter, request *http.Request) {
|
|
requestLogger := logging.WithContext(request.Context(), handler.logger)
|
|
|
|
machineIdentifier, parseError := uuid.Parse(request.PathValue("id"))
|
|
if parseError != nil {
|
|
WriteError(responseWriter, request, requestLogger, NewBadRequestError("Die Kennung des Gasts ist ungültig."))
|
|
|
|
return
|
|
}
|
|
|
|
foundMachine, readError := handler.clusterStore.GetVirtualMachine(request.Context(), machineIdentifier)
|
|
if errors.Is(readError, hypervisor.ErrVirtualMachineNotFound) {
|
|
WriteError(responseWriter, request, requestLogger, NewNotFoundError("Der Gast wurde im Bestand nicht gefunden."))
|
|
|
|
return
|
|
}
|
|
|
|
if readError != nil {
|
|
WriteError(responseWriter, request, requestLogger, NewInternalError(readError))
|
|
|
|
return
|
|
}
|
|
|
|
WriteSuccess(responseWriter, request, http.StatusOK, foundMachine)
|
|
}
|