cf7c29639c
Backend: config/db/security/logging core, SQLAlchemy models (Server, Credential, UpdateJob, UpdateLog, AuditLog, User), services (winrm, ssh, cau, audit, job_runner), REST API (auth, servers, updates, audit), Socket.io WebSocket layer. Frontend: Vue 3 + TS + Pinia + Tailwind, Views (Dashboard, Servers, Updates, Audit, Login), axios + socket.io-client, nginx prod config.
161 lines
4.8 KiB
Python
161 lines
4.8 KiB
Python
"""Server inventory routes."""
|
|
|
|
from datetime import UTC, datetime
|
|
|
|
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 NotFoundError
|
|
from app.models.credential import Credential
|
|
from app.models.server import Server, ServerType
|
|
from app.models.user import User
|
|
from app.schemas.server import HealthCheckResult, ServerCreate, ServerRead, ServerUpdate
|
|
from app.services.audit import AuditService
|
|
from app.services.cau import CAUService
|
|
from app.services.job_runner import JobRunner
|
|
from app.services.ssh import SSHService
|
|
from app.services.winrm import WinRMService
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("", response_model=list[ServerRead])
|
|
async def list_servers(
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: User = Depends(get_current_user),
|
|
) -> list[Server]:
|
|
result = await db.execute(select(Server).order_by(Server.name))
|
|
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:
|
|
server = Server(**payload.model_dump())
|
|
db.add(server)
|
|
await db.flush()
|
|
await AuditService(db).log(
|
|
username=user.username,
|
|
action="server.create",
|
|
target=server.name,
|
|
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,
|
|
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,
|
|
ip_address=client_ip(request),
|
|
)
|
|
await db.delete(server)
|
|
|
|
|
|
@router.get("/{server_id}/health", response_model=HealthCheckResult)
|
|
async def check_server_health(
|
|
server_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: User = Depends(get_current_user),
|
|
) -> HealthCheckResult:
|
|
server = await db.get(Server, server_id)
|
|
if not server:
|
|
raise NotFoundError("Server nicht gefunden")
|
|
|
|
credential = await db.get(Credential, server.credential_id) if server.credential_id else None
|
|
|
|
if server.type == ServerType.LINUX:
|
|
service = SSHService(
|
|
server.hostname,
|
|
port=server.port,
|
|
credentials=JobRunner._ssh_creds(credential),
|
|
)
|
|
elif server.type == ServerType.CAU_CLUSTER:
|
|
cau = CAUService(
|
|
server.hostname,
|
|
access_node=server.hostname,
|
|
port=server.port,
|
|
credentials=JobRunner._winrm_creds(credential),
|
|
)
|
|
ok, message = await cau.test_cluster()
|
|
server.last_health_at = datetime.now(UTC)
|
|
server.last_health_ok = ok
|
|
return HealthCheckResult(
|
|
server_id=server.id,
|
|
ok=ok,
|
|
message=message,
|
|
checked_at=server.last_health_at,
|
|
)
|
|
else:
|
|
service = WinRMService(
|
|
server.hostname,
|
|
port=server.port,
|
|
credentials=JobRunner._winrm_creds(credential),
|
|
)
|
|
|
|
started = datetime.now(UTC)
|
|
ok, message = await service.test_connection()
|
|
latency_ms = (datetime.now(UTC) - started).total_seconds() * 1000
|
|
|
|
server.last_health_at = datetime.now(UTC)
|
|
server.last_health_ok = ok
|
|
|
|
return HealthCheckResult(
|
|
server_id=server.id,
|
|
ok=ok,
|
|
latency_ms=round(latency_ms, 1),
|
|
message=message,
|
|
checked_at=server.last_health_at,
|
|
)
|