This commit is contained in:
2026-06-07 11:33:20 +02:00
parent 2bac6dc6be
commit c4c91ac45f
11 changed files with 380 additions and 9 deletions
@@ -0,0 +1,138 @@
from datetime import datetime, timezone
from fastapi import APIRouter
from fastapi.responses import PlainTextResponse
from app.core.config import get_parsed_servers
from app.models.schemas import (
AiContextResponse,
AiDuplicateSummary,
AiInventoryStats,
AiVmSummary,
ScanResponse,
)
from app.services.scanner import scan_servers
router = APIRouter(prefix="/ai", tags=["ai"])
@router.get("/context", response_model=AiContextResponse)
async def get_ai_context() -> AiContextResponse:
scan = await scan_servers(get_parsed_servers())
return build_ai_context(scan)
@router.get("/context.md", response_class=PlainTextResponse)
async def get_ai_context_markdown() -> str:
scan = await scan_servers(get_parsed_servers())
context = build_ai_context(scan)
return render_markdown(context)
def build_ai_context(scan: ScanResponse) -> AiContextResponse:
running = sum(1 for item in scan.items if item.status == "running")
stopped = sum(1 for item in scan.items if item.status == "stopped")
qemu = sum(1 for item in scan.items if item.type == "qemu")
lxc = sum(1 for item in scan.items if item.type == "lxc")
return AiContextResponse(
generated_at=datetime.now(timezone.utc).isoformat(),
purpose=(
"Contexte compact pour analyser l'inventaire Proxmox, les erreurs de scan "
"et les doublons de VM/LXC."
),
stats=AiInventoryStats(
total=len(scan.items),
running=running,
stopped=stopped,
qemu=qemu,
lxc=lxc,
duplicate_groups=len(scan.duplicates),
),
servers=scan.servers,
duplicates=[
AiDuplicateSummary(
label=group.label,
reason=group.reason,
count=group.count,
vmids=[
f"{item.server}/{item.node}/{item.type}/{item.vmid}"
for item in group.items
],
)
for group in scan.duplicates
],
inventory=[
AiVmSummary(
server=item.server,
node=item.node,
vmid=item.vmid,
type=item.type,
name=item.name,
status=item.status,
tags=item.tags,
duplicate_id=item.duplicate_id,
)
for item in scan.items
],
notes=[
"Les secrets API Proxmox ne sont jamais exposes par cette route.",
"Les champs duplicate_id dependent de la lecture des descriptions Proxmox.",
"Une erreur de serveur indique souvent un probleme reseau, TLS, token ou ACL.",
],
)
def render_markdown(context: AiContextResponse) -> str:
lines = [
"# Prox Visualizer AI Context",
"",
f"Generated at: `{context.generated_at}`",
"",
"## Stats",
"",
f"- Total VM/LXC: {context.stats.total}",
f"- Running: {context.stats.running}",
f"- Stopped: {context.stats.stopped}",
f"- QEMU VM: {context.stats.qemu}",
f"- LXC: {context.stats.lxc}",
f"- Duplicate groups: {context.stats.duplicate_groups}",
"",
"## Servers",
"",
]
for server in context.servers:
state = "OK" if server.ok else "ERROR"
lines.append(f"- {server.name}: {state}, {server.vm_count} VM/LXC, `{server.url}`")
for error in server.errors:
lines.append(f" - Error: {error}")
lines.extend(["", "## Duplicates", ""])
if context.duplicates:
for duplicate in context.duplicates:
vmids = ", ".join(duplicate.vmids)
lines.append(
f"- {duplicate.label} ({duplicate.reason}, x{duplicate.count}): {vmids}"
)
else:
lines.append("- None")
lines.extend(["", "## Inventory", ""])
for item in context.inventory:
tags = f", tags={item.tags}" if item.tags else ""
duplicate_id = (
f", duplicate_id={item.duplicate_id}"
if item.duplicate_id is not None
else ""
)
lines.append(
f"- {item.server}/{item.node}/{item.type}/{item.vmid}: "
f"{item.name}, status={item.status or 'unknown'}{tags}{duplicate_id}"
)
lines.extend(["", "## Notes", ""])
for note in context.notes:
lines.append(f"- {note}")
return "\n".join(lines) + "\n"
@@ -1,6 +1,6 @@
from fastapi import APIRouter
from app.core.config import settings
from app.core.config import get_parsed_servers
from app.models.schemas import ServerInfo
from app.services.scanner import parse_server_configs
@@ -9,7 +9,7 @@ router = APIRouter(prefix="/proxmox", tags=["proxmox"])
@router.get("/servers", response_model=list[ServerInfo])
async def list_servers() -> list[ServerInfo]:
servers, _ = parse_server_configs(settings.parsed_servers)
servers, _ = parse_server_configs(get_parsed_servers())
return [
ServerInfo(
name=server.name,
@@ -1,6 +1,6 @@
from fastapi import APIRouter
from app.core.config import settings
from app.core.config import get_parsed_servers
from app.models.schemas import ScanResponse
from app.services.scanner import scan_servers
@@ -9,4 +9,4 @@ router = APIRouter(prefix="/vms", tags=["vms"])
@router.get("/scan", response_model=ScanResponse)
async def scan_vms() -> ScanResponse:
return await scan_servers(settings.parsed_servers)
return await scan_servers(get_parsed_servers())
+2 -1
View File
@@ -1,7 +1,8 @@
from fastapi import APIRouter
from app.api.endpoints import proxmox, vms
from app.api.endpoints import ai, proxmox, vms
api_router = APIRouter(prefix="/api")
api_router.include_router(ai.router)
api_router.include_router(proxmox.router)
api_router.include_router(vms.router)
+10 -1
View File
@@ -4,6 +4,7 @@ from typing import List, Dict, Any
from pydantic_settings import BaseSettings
BACKEND_DIR = Path(__file__).resolve().parents[2]
ENV_FILE = BACKEND_DIR / ".env"
class Settings(BaseSettings):
API_ENV: str = "development"
@@ -35,6 +36,14 @@ class Settings(BaseSettings):
]
class Config:
env_file = str(BACKEND_DIR / ".env")
env_file = str(ENV_FILE)
settings = Settings()
def get_settings() -> Settings:
return Settings(_env_file=str(ENV_FILE))
def get_parsed_servers() -> List[Dict[str, Any]]:
return get_settings().parsed_servers
+2 -2
View File
@@ -1,7 +1,7 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.api.router import api_router
from app.core.config import settings
from app.core.config import get_parsed_servers, settings
app = FastAPI(
title="Proxmox Duplicate Visualizer API",
@@ -26,5 +26,5 @@ async def root():
return {
"status": "online",
"message": "Proxmox Visualizer API is running",
"configured_servers": len(settings.parsed_servers)
"configured_servers": len(get_parsed_servers())
}
@@ -56,3 +56,40 @@ class ScanResponse(BaseModel):
servers: List[ServerScanStatus]
items: List[VmRecord]
duplicates: List[DuplicateGroup]
class AiVmSummary(BaseModel):
server: str
node: str
vmid: int
type: Literal["qemu", "lxc"]
name: str
status: Optional[str] = None
tags: Optional[str] = None
duplicate_id: Optional[int] = None
class AiDuplicateSummary(BaseModel):
label: str
reason: Literal["name", "metadata"]
count: int
vmids: List[str]
class AiInventoryStats(BaseModel):
total: int
running: int
stopped: int
qemu: int
lxc: int
duplicate_groups: int
class AiContextResponse(BaseModel):
generated_at: str
purpose: str
stats: AiInventoryStats
servers: List[ServerScanStatus]
duplicates: List[AiDuplicateSummary]
inventory: List[AiVmSummary]
notes: List[str] = Field(default_factory=list)
@@ -2,7 +2,7 @@ from typing import Any, Dict, List, Optional
import httpx
from app.core.config import settings
from app.core.config import get_settings
from app.models.schemas import ProxmoxServerConfig, ServerScanStatus, VmRecord
from app.services.metadata import extract_duplicate_id
@@ -14,6 +14,7 @@ class ProxmoxClient:
self.errors: List[str] = []
async def __aenter__(self) -> "ProxmoxClient":
settings = get_settings()
self.client = httpx.AsyncClient(
base_url=f"{self.base_url}/api2/json",
headers={