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
+1 -18
View File
@@ -1,18 +1 @@
"""Pydantic schemas (request/response)."""
from app.schemas.audit import AuditLogRead
from app.schemas.auth import LoginRequest, TokenResponse
from app.schemas.server import ServerCreate, ServerRead, ServerUpdate
from app.schemas.update import JobTriggerRequest, UpdateJobRead, UpdateLogRead
__all__ = [
"AuditLogRead",
"LoginRequest",
"TokenResponse",
"ServerCreate",
"ServerRead",
"ServerUpdate",
"JobTriggerRequest",
"UpdateJobRead",
"UpdateLogRead",
]
"""Pydantic request/response schemas."""
+1
View File
@@ -10,6 +10,7 @@ class AuditLogRead(BaseModel):
id: int
timestamp: datetime
customer_id: int | None
username: str
action: str
target: str | None
+27
View File
@@ -0,0 +1,27 @@
"""Customer schemas."""
from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field
class CustomerCreate(BaseModel):
name: str = Field(min_length=1, max_length=255)
slug: str = Field(min_length=1, max_length=100, pattern=r"^[a-z0-9-]+$")
notes: str | None = None
class CustomerUpdate(BaseModel):
name: str | None = None
notes: str | None = None
class CustomerRead(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
name: str
slug: str
notes: str | None
created_at: datetime
updated_at: datetime
+30
View File
@@ -0,0 +1,30 @@
"""Satellite schemas."""
from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field
class SatelliteCreate(BaseModel):
customer_id: int
name: str = Field(min_length=1, max_length=255)
class SatelliteRead(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
customer_id: int
name: str
api_key_prefix: str
is_active: bool
last_seen_at: datetime | None
version: str | None
hostname: str | None
created_at: datetime
class SatelliteCreated(SatelliteRead):
"""Returned exactly once on creation - contains the plaintext API key."""
api_key: str
+71
View File
@@ -0,0 +1,71 @@
"""Satellite-facing schemas (agent API)."""
from datetime import datetime
from pydantic import BaseModel, Field
from app.models.update_job import JobType
class HeartbeatRequest(BaseModel):
version: str = Field(max_length=50)
hostname: str = Field(max_length=255)
class PollResponse(BaseModel):
"""Jobs handed to the satellite on poll. Empty list = nothing to do."""
jobs: list["SatelliteJob"]
class SatelliteJob(BaseModel):
job_id: int
type: JobType
# Target info (absent for network scans)
server_id: int | None = None
server_name: str | None = None
hostname: str | None = None
port: int | None = None
server_type: str | None = None
credential_ref: str | None = None
# Parameters
reboot_if_required: bool = False
scan_subnet: str | None = None
class LogLine(BaseModel):
timestamp: datetime
level: str = "info"
line: str
class LogBatchRequest(BaseModel):
job_id: int
lines: list[LogLine]
progress_percent: int | None = None
current_phase: str | None = None
class JobResultRequest(BaseModel):
job_id: int
status: str # "success" | "failed"
error: str | None = None
class ScanResultHost(BaseModel):
hostname: str
ip: str
os_guess: str | None = None # "windows" | "linux" | None
winrm_open: bool = False
ssh_open: bool = False
class ScanResultRequest(BaseModel):
job_id: int
hosts: list[ScanResultHost]
class HealthReportRequest(BaseModel):
server_id: int
ok: bool
message: str
+7 -11
View File
@@ -8,13 +8,14 @@ from app.models.server import ServerType
class ServerCreate(BaseModel):
customer_id: int
name: str = Field(min_length=1, max_length=255)
hostname: str = Field(min_length=1, max_length=255)
port: int = 5985
type: ServerType = ServerType.WINDOWS
description: str | None = None
tags: str | None = None
credential_id: int | None = None
credential_ref: str | None = None
class ServerUpdate(BaseModel):
@@ -24,29 +25,24 @@ class ServerUpdate(BaseModel):
type: ServerType | None = None
description: str | None = None
tags: str | None = None
credential_id: int | None = None
credential_ref: str | None = None
class ServerRead(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
customer_id: int
name: str
hostname: str
port: int
type: ServerType
description: str | None
tags: str | None
credential_id: int | None
credential_ref: str | None
last_health_at: datetime | None
last_health_ok: bool | None
last_health_message: str | None
discovered_by_scan: bool
created_at: datetime
updated_at: datetime
class HealthCheckResult(BaseModel):
server_id: int
ok: bool
latency_ms: float | None = None
message: str
checked_at: datetime
+24 -6
View File
@@ -8,24 +8,28 @@ from app.models.update_job import JobStatus, JobType
class JobTriggerRequest(BaseModel):
server_id: int
customer_id: int
type: JobType
# CAU-specific options
cluster_name: str | None = None
# Linux-specific options
# Target server; not required for NETWORK_SCAN
server_id: int | None = None
# Optional parameters
reboot_if_required: bool = False
scan_subnet: str | None = None # e.g. "192.168.1.0/24"
class UpdateJobRead(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
server_id: int
customer_id: int
server_id: int | None
satellite_id: int | None
type: JobType
status: JobStatus
progress_percent: int
current_phase: str | None
started_by: str
created_by: str
claimed_at: datetime | None
started_at: datetime | None
finished_at: datetime | None
error: str | None
@@ -40,3 +44,17 @@ class UpdateLogRead(BaseModel):
timestamp: datetime
level: str
line: str
class BatchJobTriggerRequest(BaseModel):
"""Trigger one job per server (or all servers of a customer)."""
customer_id: int
type: JobType
server_ids: list[int] | None = None # None = all servers of the customer
reboot_if_required: bool = False
class BatchJobTriggerResponse(BaseModel):
created: int
job_ids: list[int]