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.
135 lines
4.0 KiB
Python
135 lines
4.0 KiB
Python
"""Update job routes: trigger, list, logs, cancel."""
|
|
|
|
from fastapi import APIRouter, Depends, Query, Request
|
|
from sqlalchemy import func, 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 JobNotCancellableError, NotFoundError
|
|
from app.models.server import Server
|
|
from app.models.update_job import JobStatus, UpdateJob, UpdateLog
|
|
from app.models.user import User
|
|
from app.schemas.update import JobTriggerRequest, UpdateJobRead, UpdateLogRead
|
|
from app.services.audit import AuditService
|
|
from app.services.job_runner import job_runner
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.post("/trigger", response_model=UpdateJobRead, status_code=201)
|
|
async def trigger_update(
|
|
payload: JobTriggerRequest,
|
|
request: Request,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user),
|
|
) -> UpdateJob:
|
|
server = await db.get(Server, payload.server_id)
|
|
if not server:
|
|
raise NotFoundError("Server nicht gefunden")
|
|
|
|
job = UpdateJob(
|
|
server_id=server.id,
|
|
type=payload.type,
|
|
started_by=user.username,
|
|
)
|
|
db.add(job)
|
|
await db.flush()
|
|
|
|
await AuditService(db).log(
|
|
username=user.username,
|
|
action="update.trigger",
|
|
target=server.name,
|
|
details={"job_id": job.id, "type": payload.type.value},
|
|
ip_address=client_ip(request),
|
|
)
|
|
await db.commit()
|
|
|
|
await job_runner.start(job.id)
|
|
return job
|
|
|
|
|
|
@router.get("", response_model=list[UpdateJobRead])
|
|
async def list_jobs(
|
|
status: JobStatus | None = None,
|
|
limit: int = Query(default=50, le=200),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: User = Depends(get_current_user),
|
|
) -> list[UpdateJob]:
|
|
stmt = select(UpdateJob).order_by(UpdateJob.id.desc()).limit(limit)
|
|
if status:
|
|
stmt = stmt.where(UpdateJob.status == status)
|
|
result = await db.execute(stmt)
|
|
return list(result.scalars().all())
|
|
|
|
|
|
@router.get("/{job_id}", response_model=UpdateJobRead)
|
|
async def get_job(
|
|
job_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: User = Depends(get_current_user),
|
|
) -> UpdateJob:
|
|
job = await db.get(UpdateJob, job_id)
|
|
if not job:
|
|
raise NotFoundError("Job nicht gefunden")
|
|
return job
|
|
|
|
|
|
@router.get("/{job_id}/logs", response_model=list[UpdateLogRead])
|
|
async def get_job_logs(
|
|
job_id: int,
|
|
after_id: int = 0,
|
|
limit: int = Query(default=500, le=2000),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: User = Depends(get_current_user),
|
|
) -> list[UpdateLog]:
|
|
stmt = (
|
|
select(UpdateLog)
|
|
.where(UpdateLog.job_id == job_id, UpdateLog.id > after_id)
|
|
.order_by(UpdateLog.id)
|
|
.limit(limit)
|
|
)
|
|
result = await db.execute(stmt)
|
|
return list(result.scalars().all())
|
|
|
|
|
|
@router.post("/{job_id}/cancel", response_model=UpdateJobRead)
|
|
async def cancel_job(
|
|
job_id: int,
|
|
request: Request,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user),
|
|
) -> UpdateJob:
|
|
job = await db.get(UpdateJob, job_id)
|
|
if not job:
|
|
raise NotFoundError("Job nicht gefunden")
|
|
if job.status not in (JobStatus.PENDING, JobStatus.RUNNING):
|
|
raise JobNotCancellableError()
|
|
|
|
cancelled = await job_runner.cancel(job_id)
|
|
if not cancelled:
|
|
job.status = JobStatus.CANCELLED
|
|
|
|
await AuditService(db).log(
|
|
username=user.username,
|
|
action="update.cancel",
|
|
target=f"job:{job_id}",
|
|
ip_address=client_ip(request),
|
|
)
|
|
return job
|
|
|
|
|
|
@router.get("/stats/summary")
|
|
async def job_stats(
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: User = Depends(get_current_user),
|
|
) -> dict:
|
|
total = await db.scalar(select(func.count(UpdateJob.id)))
|
|
running = await db.scalar(
|
|
select(func.count(UpdateJob.id)).where(UpdateJob.status == JobStatus.RUNNING)
|
|
)
|
|
failed = await db.scalar(
|
|
select(func.count(UpdateJob.id)).where(UpdateJob.status == JobStatus.FAILED)
|
|
)
|
|
return {"total": total or 0, "running": running or 0, "failed": failed or 0}
|