Files
B0rbor4d cc4c3fcecb 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
2026-08-07 03:42:06 +00:00

93 lines
3.4 KiB
Python

"""Update job + log line models.
Job lifecycle (pull model):
pending -> claimed (satellite picked it up) -> running -> success | failed | cancelled
A claimed/running job whose satellite goes silent past the stale timeout
is marked failed by the janitor.
"""
import enum
from datetime import UTC, datetime
from sqlalchemy import DateTime, Enum, ForeignKey, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
class JobStatus(enum.StrEnum):
PENDING = "pending"
CLAIMED = "claimed"
RUNNING = "running"
SUCCESS = "success"
FAILED = "failed"
CANCELLED = "cancelled"
class JobType(enum.StrEnum):
WINDOWS_UPDATE = "windows_update"
LINUX_UPDATE = "linux_update"
CAU_RUN = "cau_run"
HEALTH_CHECK = "health_check"
NETWORK_SCAN = "network_scan"
class UpdateJob(Base):
__tablename__ = "update_jobs"
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="jobs", lazy="selectin") # noqa: F821
# Null for NETWORK_SCAN jobs (target = whole local network)
server_id: Mapped[int | None] = mapped_column(
ForeignKey("servers.id"), nullable=True, index=True
)
server: Mapped["Server | None"] = relationship(back_populates="jobs", lazy="selectin") # noqa: F821
# Set when a satellite claims the job
satellite_id: Mapped[int | None] = mapped_column(
ForeignKey("satellites.id"), nullable=True, index=True
)
satellite: Mapped["Satellite | None"] = relationship(back_populates="jobs", lazy="selectin") # noqa: F821
type: Mapped[JobType] = mapped_column(Enum(JobType))
status: Mapped[JobStatus] = mapped_column(Enum(JobStatus), default=JobStatus.PENDING, index=True)
progress_percent: Mapped[int] = mapped_column(Integer, default=0)
current_phase: Mapped[str | None] = mapped_column(String(255), nullable=True)
# Optional job parameters (e.g. reboot_if_required, scan_subnet)
params: Mapped[str | None] = mapped_column(Text, nullable=True) # JSON blob
created_by: Mapped[str] = mapped_column(String(255)) # dashboard username
claimed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
last_report_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
error: Mapped[str | None] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=lambda: datetime.now(UTC)
)
logs: Mapped[list["UpdateLog"]] = relationship(
back_populates="job", cascade="all, delete-orphan", order_by="UpdateLog.id"
)
class UpdateLog(Base):
__tablename__ = "update_logs"
id: Mapped[int] = mapped_column(primary_key=True)
job_id: Mapped[int] = mapped_column(ForeignKey("update_jobs.id"), index=True)
job: Mapped[UpdateJob] = relationship(back_populates="logs")
timestamp: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=lambda: datetime.now(UTC)
)
level: Mapped[str] = mapped_column(String(20), default="info")
line: Mapped[str] = mapped_column(Text)