129 lines
3.7 KiB
Python
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
|