Hub-and-Spoke Umbau: Multi-Tenant Zentrale + Satellite-Agent

- Backend: Customer/Satellite Models, customer_id auf Server/Job/Audit
- Satellite-API: heartbeat, poll (atomares Claiming), logs, result,
  scan-result, health-report - Auth via X-Api-Key (SHA-256 gehasht)
- Job-Queue: pending/claimed/running/success/failed + Stale-Janitor
- Batch-Trigger: ein Job pro Server, Satellite arbeitet sequenziell ab
- Credentials bleiben lokal: nur symbolische credential_ref zentral
- Neues Paket satellite/: Pull-Loop, WinRM/SSH/CAU/Scanner, PyInstaller-tauglich
- Frontend: Kunden-Switcher, Satelliten-View, Polling statt WebSocket
- Entfernt: WebSocket/Socket.io, Redis, zentrale Credentials, JobRunner
- Docs: README/AGENTS/PROMPT auf neue Architektur aktualisiert
This commit is contained in:
B0rbor4d
2026-08-07 03:42:06 +00:00
parent b91dd66fee
commit cc4c3fcecb
72 changed files with 2759 additions and 1642 deletions
+52
View File
@@ -0,0 +1,52 @@
"""Satellite model - remote agent at a customer site.
The API key is stored as a SHA-256 hash; the plaintext key is shown
exactly once at creation time.
"""
import hashlib
import secrets
from datetime import UTC, datetime
from sqlalchemy import Boolean, DateTime, ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
def generate_api_key() -> str:
"""Generate a new satellite API key (plaintext, show once)."""
return f"ius_{secrets.token_urlsafe(32)}"
def hash_api_key(key: str) -> str:
return hashlib.sha256(key.encode()).hexdigest()
class Satellite(Base):
__tablename__ = "satellites"
id: Mapped[int] = mapped_column(primary_key=True)
customer_id: Mapped[int] = mapped_column(
ForeignKey("customers.id"), index=True
)
customer: Mapped["Customer"] = relationship(back_populates="satellites") # noqa: F821
name: Mapped[str] = mapped_column(String(255))
api_key_hash: Mapped[str] = mapped_column(String(64), unique=True, index=True)
api_key_prefix: Mapped[str] = mapped_column(String(12)) # for display in UI
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
# Filled by heartbeat
last_seen_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
version: Mapped[str | None] = mapped_column(String(50), nullable=True)
hostname: Mapped[str | None] = mapped_column(String(255), nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=lambda: datetime.now(UTC)
)
jobs: Mapped[list["UpdateJob"]] = relationship( # noqa: F821
back_populates="satellite"
)