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>
221 lines
8.5 KiB
Go
221 lines
8.5 KiB
Go
// Package agentregistry verwaltet die am Control Server angemeldeten Agents.
|
|
//
|
|
// Leitgedanke aus PROMPT.md §59: Ein kompromittierter Agent darf nicht
|
|
// automatisch vollständigen Zugriff auf die Backup-Infrastruktur erhalten.
|
|
// Daraus folgt die Trennung in zwei Tokenarten:
|
|
//
|
|
// - Ein Aufnahme-Token gilt einmalig und nur zur Registrierung.
|
|
// - Ein Betriebs-Token gilt dauerhaft, trägt aber ausschließlich
|
|
// agentspezifische Rechte und ist einzeln widerrufbar.
|
|
package agentregistry
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// AgentStatus ist der Betriebszustand eines Agents.
|
|
type AgentStatus string
|
|
|
|
const (
|
|
// AgentStatusActive bedeutet: der Agent darf arbeiten.
|
|
AgentStatusActive AgentStatus = "active"
|
|
// AgentStatusDisabled bedeutet: der Agent ist vorübergehend abgeschaltet.
|
|
AgentStatusDisabled AgentStatus = "disabled"
|
|
// AgentStatusRevoked bedeutet: der Agent ist dauerhaft gesperrt.
|
|
AgentStatusRevoked AgentStatus = "revoked"
|
|
)
|
|
|
|
// Platform benennt das Betriebssystem eines Agents.
|
|
type Platform string
|
|
|
|
const (
|
|
// PlatformWindows ist ein Windows-System.
|
|
PlatformWindows Platform = "windows"
|
|
// PlatformLinux ist ein Linux-System.
|
|
PlatformLinux Platform = "linux"
|
|
// PlatformDarwin ist ein macOS-System.
|
|
PlatformDarwin Platform = "darwin"
|
|
)
|
|
|
|
// IsSupported meldet, ob eine Plattform bekannt ist.
|
|
func (platform Platform) IsSupported() bool {
|
|
switch platform {
|
|
case PlatformWindows, PlatformLinux, PlatformDarwin:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Agent beschreibt einen registrierten Agent.
|
|
type Agent struct {
|
|
// ID ist der öffentliche Bezeichner.
|
|
ID uuid.UUID `json:"id"`
|
|
// Name ist die sprechende Bezeichnung.
|
|
Name string `json:"name"`
|
|
// Hostname ist der gemeldete Rechnername.
|
|
Hostname string `json:"hostname,omitempty"`
|
|
// Platform ist das Betriebssystem.
|
|
Platform Platform `json:"platform"`
|
|
// Architecture ist die Rechnerarchitektur.
|
|
Architecture string `json:"architecture,omitempty"`
|
|
// Version ist die Programmversion des Agents.
|
|
Version string `json:"version,omitempty"`
|
|
// Status ist der Betriebszustand.
|
|
Status AgentStatus `json:"status"`
|
|
// LastHeartbeatAt ist der Zeitpunkt der letzten Lebendmeldung in UTC.
|
|
LastHeartbeatAt *time.Time `json:"last_heartbeat_at,omitempty"`
|
|
// LastIPAddress ist die zuletzt gesehene Absenderadresse.
|
|
LastIPAddress string `json:"last_ip_address,omitempty"`
|
|
// RegisteredAt ist der Zeitpunkt der Aufnahme in UTC.
|
|
RegisteredAt time.Time `json:"registered_at"`
|
|
}
|
|
|
|
// HeartbeatAge liefert die Zeit seit der letzten Lebendmeldung.
|
|
//
|
|
// Der zweite Rückgabewert meldet, ob überhaupt je eine einging. Ein Agent ohne
|
|
// Lebendmeldung ist nicht dasselbe wie einer mit alter Meldung.
|
|
func (agent Agent) HeartbeatAge(referenceTime time.Time) (time.Duration, bool) {
|
|
if agent.LastHeartbeatAt == nil {
|
|
return 0, false
|
|
}
|
|
|
|
return referenceTime.Sub(*agent.LastHeartbeatAt), true
|
|
}
|
|
|
|
// IsOffline meldet, ob ein Agent länger als die erlaubte Frist stumm ist.
|
|
//
|
|
// Ein stummer Agent bedeutet: von diesem System kommen keine Backups mehr.
|
|
// Das ist eine Warnung wert (PROMPT.md §38).
|
|
func (agent Agent) IsOffline(referenceTime time.Time, allowedSilence time.Duration) bool {
|
|
heartbeatAge, hasHeartbeat := agent.HeartbeatAge(referenceTime)
|
|
if !hasHeartbeat {
|
|
// Ein Agent, der sich nie gemeldet hat, gilt ab der Frist nach seiner
|
|
// Aufnahme als offline.
|
|
return referenceTime.Sub(agent.RegisteredAt) > allowedSilence
|
|
}
|
|
|
|
return heartbeatAge > allowedSilence
|
|
}
|
|
|
|
// EnrollmentToken ist ein einmaliges Aufnahme-Token.
|
|
type EnrollmentToken struct {
|
|
// ID ist der Bezeichner des Tokens.
|
|
ID uuid.UUID `json:"id"`
|
|
// Token ist der Klartext.
|
|
//
|
|
// Er verlässt den Server genau einmal bei der Ausstellung; danach liegt
|
|
// nur noch sein Hash vor.
|
|
Token string `json:"token"`
|
|
// AgentName ist der vorgesehene Name des aufzunehmenden Agents.
|
|
AgentName string `json:"agent_name"`
|
|
// ExpiresAt ist die Ablaufzeit in UTC.
|
|
ExpiresAt time.Time `json:"expires_at"`
|
|
}
|
|
|
|
// RegistrationResult ist das Ergebnis einer Agent-Registrierung.
|
|
type RegistrationResult struct {
|
|
// Agent ist der angelegte Agent.
|
|
Agent Agent `json:"agent"`
|
|
// AgentToken ist das Betriebstoken im Klartext.
|
|
//
|
|
// Es verlässt den Server genau einmal. Geht es verloren, muss der Agent
|
|
// neu aufgenommen oder sein Token gewechselt werden.
|
|
AgentToken string `json:"agent_token"`
|
|
}
|
|
|
|
// RegistrationRequest beschreibt einen sich anmeldenden Agent.
|
|
type RegistrationRequest struct {
|
|
// EnrollmentToken ist das Aufnahme-Token.
|
|
EnrollmentToken string
|
|
// Hostname ist der Rechnername des Systems.
|
|
Hostname string
|
|
// Platform ist das Betriebssystem.
|
|
Platform Platform
|
|
// Architecture ist die Rechnerarchitektur.
|
|
Architecture string
|
|
// Version ist die Programmversion des Agents.
|
|
Version string
|
|
// IPAddress ist die Absenderadresse.
|
|
IPAddress string
|
|
}
|
|
|
|
// HeartbeatRequest ist eine Lebendmeldung eines Agents.
|
|
type HeartbeatRequest struct {
|
|
// Version ist die aktuelle Programmversion des Agents.
|
|
//
|
|
// Sie wird bei jeder Meldung übernommen, damit ein Update sofort sichtbar
|
|
// ist und veraltete Agents erkennbar bleiben (PROMPT.md §90).
|
|
Version string
|
|
// IPAddress ist die Absenderadresse.
|
|
IPAddress string
|
|
}
|
|
|
|
// Fehler der Agent-Verwaltung.
|
|
var (
|
|
// ErrEnrollmentTokenInvalid meldet ein unbrauchbares Aufnahme-Token.
|
|
//
|
|
// Der Fehler unterscheidet bewusst nicht zwischen unbekannt, abgelaufen und
|
|
// bereits eingelöst: eine Unterscheidung erlaubte es, gültige Tokens zu erraten.
|
|
ErrEnrollmentTokenInvalid = errors.New("das aufnahme-token ist ungültig, abgelaufen oder bereits verwendet")
|
|
// ErrAgentTokenInvalid meldet ein unbrauchbares Betriebstoken.
|
|
ErrAgentTokenInvalid = errors.New("das agent-token ist ungültig oder widerrufen")
|
|
// ErrAgentNotFound meldet einen nicht vorhandenen Agent.
|
|
ErrAgentNotFound = errors.New("der agent existiert nicht")
|
|
// ErrAgentRevoked meldet einen gesperrten Agent.
|
|
ErrAgentRevoked = errors.New("der agent ist gesperrt")
|
|
// ErrUnsupportedPlatform meldet eine unbekannte Plattform.
|
|
ErrUnsupportedPlatform = errors.New("die plattform wird nicht unterstützt")
|
|
)
|
|
|
|
// defaultEnrollmentValidity ist die Gültigkeitsdauer eines Aufnahme-Tokens.
|
|
//
|
|
// Eine Stunde reicht für eine geplante Installation und begrenzt zugleich das
|
|
// Zeitfenster, in dem ein abgefangenes Token nützt.
|
|
const defaultEnrollmentValidity = time.Hour
|
|
|
|
// AgentFilter schränkt eine Agent-Abfrage ein.
|
|
type AgentFilter struct {
|
|
// Status beschränkt auf einen Betriebszustand.
|
|
Status string
|
|
// Platform beschränkt auf ein Betriebssystem.
|
|
Platform string
|
|
// Page ist die gewünschte Seite, beginnend bei 1.
|
|
Page int
|
|
// PageSize ist die Anzahl der Einträge je Seite.
|
|
PageSize int
|
|
}
|
|
|
|
// Store kapselt die Ablage der Agent-Daten.
|
|
//
|
|
// Die Schnittstelle trennt die Domänenlogik von der Datenbank und macht sie
|
|
// einzeln prüfbar.
|
|
type Store interface {
|
|
// CreateEnrollmentToken legt ein Aufnahme-Token an.
|
|
CreateEnrollmentToken(createContext context.Context, tokenHash string, agentName string, expiresAt time.Time, createdBy *uuid.UUID) (uuid.UUID, error)
|
|
// ConsumeEnrollmentToken löst ein Aufnahme-Token ein und liefert den vorgesehenen Namen.
|
|
ConsumeEnrollmentToken(consumeContext context.Context, tokenHash string, agentID uuid.UUID) (agentName string, consumeError error)
|
|
// PeekEnrollmentToken liest den vorgesehenen Namen, ohne einzulösen.
|
|
PeekEnrollmentToken(peekContext context.Context, tokenHash string) (agentName string, peekError error)
|
|
// CreateAgent legt einen Agent an.
|
|
CreateAgent(createContext context.Context, agentToCreate Agent) (uuid.UUID, error)
|
|
// CreateAgentToken legt ein Betriebstoken an.
|
|
CreateAgentToken(createContext context.Context, agentID uuid.UUID, tokenHash string) (uuid.UUID, error)
|
|
// FindAgentByTokenHash sucht einen Agent anhand seines Betriebstokens.
|
|
FindAgentByTokenHash(queryContext context.Context, tokenHash string) (Agent, error)
|
|
// FindAgentByID sucht einen Agent anhand seiner Kennung.
|
|
FindAgentByID(queryContext context.Context, agentID uuid.UUID) (Agent, error)
|
|
// ListAgents liefert eine Seite von Agents.
|
|
ListAgents(queryContext context.Context, agentFilter AgentFilter) ([]Agent, int64, error)
|
|
// RecordHeartbeat vermerkt eine Lebendmeldung.
|
|
RecordHeartbeat(updateContext context.Context, agentID uuid.UUID, agentVersion string, ipAddress string) error
|
|
// RevokeAgent sperrt einen Agent und alle seine Tokens.
|
|
RevokeAgent(updateContext context.Context, agentID uuid.UUID) error
|
|
// RotateAgentToken widerruft alle Tokens eines Agents und legt ein neues an.
|
|
RotateAgentToken(updateContext context.Context, agentID uuid.UUID, newTokenHash string) error
|
|
}
|