Files
proxmox_list/prox-visualizer/backend/app/services/scanner.py
T
2026-06-07 09:30:11 +02:00

129 lines
3.7 KiB
Python

import asyncio
import re
from collections import defaultdict
from typing import Dict, Iterable, List, Tuple
from pydantic import ValidationError
from app.models.schemas import (
DuplicateGroup,
ProxmoxServerConfig,
ScanResponse,
ServerScanStatus,
VmRecord,
)
from app.services.proxmox_api import ProxmoxClient
def parse_server_configs(raw_servers: Iterable[dict]) -> Tuple[List[ProxmoxServerConfig], List[str]]:
servers: List[ProxmoxServerConfig] = []
errors: List[str] = []
for index, raw_server in enumerate(raw_servers, start=1):
try:
servers.append(ProxmoxServerConfig.model_validate(raw_server))
except ValidationError as exc:
errors.append(f"serveur #{index}: {exc.errors()[0]['msg']}")
return servers, errors
async def scan_servers(raw_servers: Iterable[dict]) -> ScanResponse:
servers, config_errors = parse_server_configs(raw_servers)
if not servers:
return ScanResponse(
servers=[
ServerScanStatus(
name="configuration",
url="",
ok=False,
errors=config_errors or ["aucun serveur Proxmox configure"],
)
],
items=[],
duplicates=[],
)
results = await asyncio.gather(*[_scan_one(server) for server in servers])
statuses: List[ServerScanStatus] = []
items: List[VmRecord] = []
if config_errors:
statuses.append(
ServerScanStatus(
name="configuration",
url="",
ok=False,
errors=config_errors,
)
)
for status, server_items in results:
statuses.append(status)
items.extend(server_items)
return ScanResponse(
servers=statuses,
items=sorted(items, key=lambda vm: (vm.server, vm.node, vm.vmid)),
duplicates=find_duplicates(items),
)
async def _scan_one(server: ProxmoxServerConfig) -> tuple[ServerScanStatus, List[VmRecord]]:
async with ProxmoxClient(server) as client:
return await client.scan()
def find_duplicates(items: List[VmRecord]) -> List[DuplicateGroup]:
groups: List[DuplicateGroup] = []
seen_item_sets = set()
by_metadata: Dict[int, List[VmRecord]] = defaultdict(list)
for item in items:
if item.duplicate_id is not None:
by_metadata[item.duplicate_id].append(item)
for duplicate_id, members in sorted(by_metadata.items()):
if len(members) < 2:
continue
groups.append(
DuplicateGroup(
id=f"metadata:{duplicate_id}",
label=f"duplicate_id={duplicate_id}",
reason="metadata",
count=len(members),
items=members,
)
)
seen_item_sets.add(frozenset(item.id for item in members))
by_name: Dict[str, List[VmRecord]] = defaultdict(list)
for item in items:
normalized = normalize_name(item.name)
if normalized:
by_name[normalized].append(item)
for normalized, members in sorted(by_name.items()):
if len(members) < 2:
continue
item_set = frozenset(item.id for item in members)
if item_set in seen_item_sets:
continue
groups.append(
DuplicateGroup(
id=f"name:{normalized}",
label=members[0].name,
reason="name",
count=len(members),
items=members,
)
)
return groups
def normalize_name(name: str) -> str:
normalized = name.strip().lower()
normalized = re.sub(r"\s+", " ", normalized)
return normalized