diff --git a/amelioration.md b/amelioration.md new file mode 100644 index 0000000..24fa781 --- /dev/null +++ b/amelioration.md @@ -0,0 +1,18 @@ +scan toute les vm et affiche un listing des machine qui sont en marche en meme temp: +( prevoir de tagger la machine qui doit etre en marche en priorité, les autres sont des vm de secours (ce tag appelle "first" et sur les autres doublon tag " secours" est mis uniquement dans la bdd de l'app)) + - alerte rouge: 2 ou plus vm identique sont en marche en meme temp + - niveau vert: 2 ou plus de vm identique existe mais une seule est en route ( et c'est celle qui a le tag first les autres on le tag secours) ou + 1 seule vm fonctionne et pas de doublon parmi les autres serveur et la vm a un 2eme tag "non critique" ( ce tag est mis uniquement dans la bdd de l'app est c'est la valeur par defaut tant que je ne vient pas le modiifer) + - niveau orange clair : 1 seule vm fonctionne et pas de doublon parmi les autres serveur et la vm a un 2eme tag "critique" ( ce tag est mis uniquement dans la bdd de l'app) + - niveau violet: 1 seule vm est en route et elle n'a pas le tag first + alerte vert= c'est ok + alerte rouge= je vais devoir arreter la machine en doublon qui n'a pas le tag "first" + alerte violet= prevoir d'arreter la machine doublon et de remettre en marche la machine first + alert orange clair= prevoir de créer un doublon de la vm avec le tag secours + + - ajouter la possibilite d'ajouter des tags (2 serie : "first" ou "secours" et "critique" ou "non critique") ces tag sont uniquement ajouté a la bdd et serve pour la visualisatioon + +- possibilite d'arreter ou demarrer une vm depuis l'insterface + +- un pve peut etre a l'arret => aucun message d'alerte si'il ne comporte aucune machine avec le tag "first", +- possibilite d'activer l'option autostart sur une vm d'un serveur \ No newline at end of file diff --git a/prox-visualizer/README.md b/prox-visualizer/README.md index b195834..cb4c9a8 100644 --- a/prox-visualizer/README.md +++ b/prox-visualizer/README.md @@ -51,3 +51,5 @@ Ouvre ensuite l'URL `Network` affichee par Vite, par exemple `http://10.0.3.x:51 - `GET /`: etat de l'API. - `GET /api/proxmox/servers`: serveurs configures, sans secrets. - `GET /api/vms/scan`: inventaire, etat des serveurs et groupes de doublons. +- `GET /api/ai/context`: contexte JSON compact pour une IA ou un outil externe. +- `GET /api/ai/context.md`: meme contexte au format Markdown. diff --git a/prox-visualizer/backend/app/api/endpoints/ai.py b/prox-visualizer/backend/app/api/endpoints/ai.py new file mode 100644 index 0000000..3d9acaa --- /dev/null +++ b/prox-visualizer/backend/app/api/endpoints/ai.py @@ -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" diff --git a/prox-visualizer/backend/app/api/endpoints/proxmox.py b/prox-visualizer/backend/app/api/endpoints/proxmox.py index acaa5c4..6d17f23 100644 --- a/prox-visualizer/backend/app/api/endpoints/proxmox.py +++ b/prox-visualizer/backend/app/api/endpoints/proxmox.py @@ -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, diff --git a/prox-visualizer/backend/app/api/endpoints/vms.py b/prox-visualizer/backend/app/api/endpoints/vms.py index c2984cf..68f99d3 100644 --- a/prox-visualizer/backend/app/api/endpoints/vms.py +++ b/prox-visualizer/backend/app/api/endpoints/vms.py @@ -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()) diff --git a/prox-visualizer/backend/app/api/router.py b/prox-visualizer/backend/app/api/router.py index d8fc536..7494ba0 100644 --- a/prox-visualizer/backend/app/api/router.py +++ b/prox-visualizer/backend/app/api/router.py @@ -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) diff --git a/prox-visualizer/backend/app/core/config.py b/prox-visualizer/backend/app/core/config.py index 320c47c..cdaa061 100644 --- a/prox-visualizer/backend/app/core/config.py +++ b/prox-visualizer/backend/app/core/config.py @@ -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 diff --git a/prox-visualizer/backend/app/main.py b/prox-visualizer/backend/app/main.py index c36fb83..8dc0577 100644 --- a/prox-visualizer/backend/app/main.py +++ b/prox-visualizer/backend/app/main.py @@ -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()) } diff --git a/prox-visualizer/backend/app/models/schemas.py b/prox-visualizer/backend/app/models/schemas.py index 8a6713f..cf83c1a 100644 --- a/prox-visualizer/backend/app/models/schemas.py +++ b/prox-visualizer/backend/app/models/schemas.py @@ -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) diff --git a/prox-visualizer/backend/app/services/proxmox_api.py b/prox-visualizer/backend/app/services/proxmox_api.py index dd6e954..fc61355 100644 --- a/prox-visualizer/backend/app/services/proxmox_api.py +++ b/prox-visualizer/backend/app/services/proxmox_api.py @@ -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={ diff --git a/tuto_user_cli_proxmox.md b/tuto_user_cli_proxmox.md new file mode 100644 index 0000000..1746a1c --- /dev/null +++ b/tuto_user_cli_proxmox.md @@ -0,0 +1,165 @@ +# Tuto CLI Proxmox : utilisateur API + +Commandes a lancer sur le shell Proxmox en `root`. + +## Creer l'utilisateur, le role et le token + +```bash +pveum user add api@pam --comment "Prox Visualizer API user" + +pveum role add ProxVisualizerFullAdmin \ + -privs "Sys.Audit Sys.Modify Sys.PowerMgmt VM.Audit VM.Allocate VM.PowerMgmt VM.Clone VM.Migrate VM.Backup VM.Snapshot VM.Config.Disk VM.Config.CDROM VM.Config.CPU VM.Config.Memory VM.Config.Network VM.Config.HWType VM.Config.Options VM.Config.Cloudinit Datastore.Audit Datastore.AllocateSpace Datastore.AllocateTemplate" + +pveum aclmod / \ + --users api@pam \ + --roles ProxVisualizerFullAdmin + +pveum user token add api@pam prox_visualizer_full \ + --privsep 1 \ + --comment "Prox Visualizer full admin token" + +pveum aclmod / \ + --tokens 'api@pam!prox_visualizer_full' \ + --roles ProxVisualizerFullAdmin +``` + +La commande `pveum user token add` affiche le secret une seule fois. A conserver dans le fichier `.env` de l'application : + +```json +{ + "name": "PVE-NODE", + "url": "https://10.0.3.203:8006", + "token_name": "api@pam!prox_visualizer_full", + "token_value": "SECRET_AFFICHE_PAR_PROXMOX" +} +``` + +## Lister et verifier + +Lister les ACL : + +```bash +pveum acl list +``` + +Lister les utilisateurs : + +```bash +pveum user list +``` + +Lister les roles Prox Visualizer : + +```bash +pveum role list | grep ProxVisualizer +``` + +Verifier les permissions effectives du token : + +```bash +pveum user permissions 'api@pam!prox_visualizer_full' --path / +``` + +Lister les tokens de l'utilisateur : + +```bash +pveum user token list api@pam +``` + +## Ajouter un serveur dans l'application + +Edite le fichier de configuration local de l'application : + +```bash +cd /home/gilles/Documents/projet/proxmox_list/prox-visualizer +nano backend/.env +``` + +Ajoute une entree dans la variable `PROXMOX_SERVERS`. Exemple avec deux serveurs : + +```bash +PROXMOX_SERVERS='[ + { + "name": "PVE-M710q", + "url": "https://10.0.3.203:8006", + "token_name": "api@pam!prox_visualizer_full", + "token_value": "SECRET_DU_TOKEN_M710Q" + }, + { + "name": "PVE-MSI", + "url": "https://10.0.3.202:8006", + "token_name": "api@pam!prox_visualizer_full", + "token_value": "SECRET_DU_TOKEN_MSI" + } +]' +``` + +Attention : le contenu de `PROXMOX_SERVERS` doit rester du JSON valide. Il ne faut pas mettre de virgule apres le dernier serveur. + +L'application relit automatiquement cette liste au prochain appel API. Apres modification de `backend/.env`, il suffit donc de rafraichir le dashboard ou de cliquer sur le bouton de rafraichissement. + +## Relancer l'application + +Depuis la racine du projet : + +```bash +cd /home/gilles/Documents/projet/proxmox_list/prox-visualizer +./start.sh +``` + +Le frontend affiche une URL locale et une URL reseau, par exemple : + +```text +http://127.0.0.1:5173/ +http://10.0.1.45:5173/ +``` + +Pour arreter backend et frontend : + +```text +Ctrl+C +``` + +Si le port `5173` est deja occupe, Vite utilise automatiquement le port suivant, par exemple `5174`. + +## Effacer et nettoyer + +Supprimer les ACL : + +```bash +pveum acl delete / \ + --tokens 'api@pam!prox_visualizer_full' \ + --roles ProxVisualizerFullAdmin + +pveum acl delete / \ + --users api@pam \ + --roles ProxVisualizerFullAdmin +``` + +Supprimer le token : + +```bash +pveum user token remove api@pam prox_visualizer_full +``` + +Supprimer l'utilisateur : + +```bash +pveum user delete api@pam +``` + +Supprimer le role : + +```bash +pveum role delete ProxVisualizerFullAdmin +``` + +Verifier que tout est nettoye : + +```bash +pveum acl list +pveum user list +pveum role list | grep ProxVisualizer +``` + +Si une commande indique que le token, le role ou l'utilisateur n'existe pas, ce n'est pas bloquant : l'element etait deja absent.