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

121 lines
3.6 KiB
Python

"""Server inventory routes (customer-scoped).
No direct connectivity from the central server - health checks are
HEALTH_CHECK jobs executed by the customer's satellite.
"""
from fastapi import APIRouter, Depends, Request
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import client_ip, get_current_user
from app.core.database import get_db
from app.core.exceptions import ConflictError, NotFoundError
from app.models.customer import Customer
from app.models.server import Server
from app.models.user import User
from app.schemas.server import ServerCreate, ServerRead, ServerUpdate
from app.services.audit import AuditService
router = APIRouter()
@router.get("", response_model=list[ServerRead])
async def list_servers(
customer_id: int | None = None,
db: AsyncSession = Depends(get_db),
_user: User = Depends(get_current_user),
) -> list[Server]:
stmt = select(Server).order_by(Server.name)
if customer_id is not None:
stmt = stmt.where(Server.customer_id == customer_id)
result = await db.execute(stmt)
return list(result.scalars().all())
@router.post("", response_model=ServerRead, status_code=201)
async def create_server(
payload: ServerCreate,
request: Request,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
) -> Server:
customer = await db.get(Customer, payload.customer_id)
if not customer:
raise NotFoundError("Kunde nicht gefunden")
existing = await db.execute(
select(Server).where(
Server.customer_id == payload.customer_id, Server.name == payload.name
)
)
if existing.scalar_one_or_none():
raise ConflictError("Server mit diesem Namen existiert beim Kunden bereits")
server = Server(**payload.model_dump())
db.add(server)
await db.flush()
await AuditService(db).log(
username=user.username,
action="server.create",
target=server.name,
customer_id=customer.id,
ip_address=client_ip(request),
)
return server
@router.get("/{server_id}", response_model=ServerRead)
async def get_server(
server_id: int,
db: AsyncSession = Depends(get_db),
_user: User = Depends(get_current_user),
) -> Server:
server = await db.get(Server, server_id)
if not server:
raise NotFoundError("Server nicht gefunden")
return server
@router.patch("/{server_id}", response_model=ServerRead)
async def update_server(
server_id: int,
payload: ServerUpdate,
request: Request,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
) -> Server:
server = await db.get(Server, server_id)
if not server:
raise NotFoundError("Server nicht gefunden")
for field, value in payload.model_dump(exclude_unset=True).items():
setattr(server, field, value)
await AuditService(db).log(
username=user.username,
action="server.update",
target=server.name,
customer_id=server.customer_id,
ip_address=client_ip(request),
)
return server
@router.delete("/{server_id}", status_code=204)
async def delete_server(
server_id: int,
request: Request,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
) -> None:
server = await db.get(Server, server_id)
if not server:
raise NotFoundError("Server nicht gefunden")
await AuditService(db).log(
username=user.username,
action="server.delete",
target=server.name,
customer_id=server.customer_id,
ip_address=client_ip(request),
)
await db.delete(server)