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