3
This commit is contained in:
@@ -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
|
||||||
@@ -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 /`: etat de l'API.
|
||||||
- `GET /api/proxmox/servers`: serveurs configures, sans secrets.
|
- `GET /api/proxmox/servers`: serveurs configures, sans secrets.
|
||||||
- `GET /api/vms/scan`: inventaire, etat des serveurs et groupes de doublons.
|
- `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.
|
||||||
|
|||||||
@@ -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 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.models.schemas import ServerInfo
|
||||||
from app.services.scanner import parse_server_configs
|
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])
|
@router.get("/servers", response_model=list[ServerInfo])
|
||||||
async def list_servers() -> list[ServerInfo]:
|
async def list_servers() -> list[ServerInfo]:
|
||||||
servers, _ = parse_server_configs(settings.parsed_servers)
|
servers, _ = parse_server_configs(get_parsed_servers())
|
||||||
return [
|
return [
|
||||||
ServerInfo(
|
ServerInfo(
|
||||||
name=server.name,
|
name=server.name,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from fastapi import APIRouter
|
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.models.schemas import ScanResponse
|
||||||
from app.services.scanner import scan_servers
|
from app.services.scanner import scan_servers
|
||||||
|
|
||||||
@@ -9,4 +9,4 @@ router = APIRouter(prefix="/vms", tags=["vms"])
|
|||||||
|
|
||||||
@router.get("/scan", response_model=ScanResponse)
|
@router.get("/scan", response_model=ScanResponse)
|
||||||
async def scan_vms() -> ScanResponse:
|
async def scan_vms() -> ScanResponse:
|
||||||
return await scan_servers(settings.parsed_servers)
|
return await scan_servers(get_parsed_servers())
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
from fastapi import APIRouter
|
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 = APIRouter(prefix="/api")
|
||||||
|
api_router.include_router(ai.router)
|
||||||
api_router.include_router(proxmox.router)
|
api_router.include_router(proxmox.router)
|
||||||
api_router.include_router(vms.router)
|
api_router.include_router(vms.router)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from typing import List, Dict, Any
|
|||||||
from pydantic_settings import BaseSettings
|
from pydantic_settings import BaseSettings
|
||||||
|
|
||||||
BACKEND_DIR = Path(__file__).resolve().parents[2]
|
BACKEND_DIR = Path(__file__).resolve().parents[2]
|
||||||
|
ENV_FILE = BACKEND_DIR / ".env"
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
API_ENV: str = "development"
|
API_ENV: str = "development"
|
||||||
@@ -35,6 +36,14 @@ class Settings(BaseSettings):
|
|||||||
]
|
]
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
env_file = str(BACKEND_DIR / ".env")
|
env_file = str(ENV_FILE)
|
||||||
|
|
||||||
settings = Settings()
|
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
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from app.api.router import api_router
|
from app.api.router import api_router
|
||||||
from app.core.config import settings
|
from app.core.config import get_parsed_servers, settings
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Proxmox Duplicate Visualizer API",
|
title="Proxmox Duplicate Visualizer API",
|
||||||
@@ -26,5 +26,5 @@ async def root():
|
|||||||
return {
|
return {
|
||||||
"status": "online",
|
"status": "online",
|
||||||
"message": "Proxmox Visualizer API is running",
|
"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]
|
servers: List[ServerScanStatus]
|
||||||
items: List[VmRecord]
|
items: List[VmRecord]
|
||||||
duplicates: List[DuplicateGroup]
|
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
|
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.models.schemas import ProxmoxServerConfig, ServerScanStatus, VmRecord
|
||||||
from app.services.metadata import extract_duplicate_id
|
from app.services.metadata import extract_duplicate_id
|
||||||
|
|
||||||
@@ -14,6 +14,7 @@ class ProxmoxClient:
|
|||||||
self.errors: List[str] = []
|
self.errors: List[str] = []
|
||||||
|
|
||||||
async def __aenter__(self) -> "ProxmoxClient":
|
async def __aenter__(self) -> "ProxmoxClient":
|
||||||
|
settings = get_settings()
|
||||||
self.client = httpx.AsyncClient(
|
self.client = httpx.AsyncClient(
|
||||||
base_url=f"{self.base_url}/api2/json",
|
base_url=f"{self.base_url}/api2/json",
|
||||||
headers={
|
headers={
|
||||||
|
|||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user