"""Sync-Logik: liest Subnetze + Adressen aus phpipam und schreibt sie als ein Outline-Dokument pro Subnetz.""" from __future__ import annotations from .config import settings from .outline_client import OutlineClient from .phpipam_client import PhpIpamClient def _subnet_title(subnet: dict) -> str: cidr = f"{subnet.get('subnet')}/{subnet.get('mask')}" desc = subnet.get("description") or "" return f"{cidr} – {desc}".rstrip(" –") if desc else cidr def _subnet_markdown(subnet: dict, addresses: list[dict], section_name: str | None) -> str: cidr = f"{subnet.get('subnet')}/{subnet.get('mask')}" lines: list[str] = [] lines.append(f"# {cidr}") lines.append("") lines.append(f"- **Beschreibung:** {subnet.get('description') or '–'}") if section_name: lines.append(f"- **Section:** {section_name}") if subnet.get("vlanId") and subnet.get("vlanId") != "0": lines.append(f"- **VLAN-ID:** {subnet.get('vlanId')}") if subnet.get("location"): lines.append(f"- **Standort:** {subnet.get('location')}") lines.append("") if not addresses: lines.append("_Keine belegten Adressen in phpipam hinterlegt._") return "\n".join(lines) lines.append("| IP | Hostname | Beschreibung | MAC | Owner |") lines.append("|---|---|---|---|---|") for addr in sorted(addresses, key=lambda a: tuple(int(o) for o in a["ip"].split("."))): lines.append( "| {ip} | {hostname} | {desc} | {mac} | {owner} |".format( ip=addr.get("ip", ""), hostname=addr.get("hostname") or "", desc=(addr.get("description") or "").replace("\n", " "), mac=addr.get("mac") or "", owner=addr.get("owner") or "", ) ) return "\n".join(lines) def run_sync(dry_run: bool | None = None) -> list[dict]: """Führt den Sync durch. Gibt eine Liste von Ergebnis-Infos je Subnetz zurück.""" dry_run = settings.sync_dry_run if dry_run is None else dry_run results: list[dict] = [] section_cache: dict[str, str | None] = {} with PhpIpamClient() as ipam: subnets = ipam.list_subnets() outline = None if dry_run else OutlineClient() collection_id = settings.outline_collection_id parent_document_id = settings.outline_parent_document_id try: if outline is not None: collection_id = outline.resolve_collection_id(settings.outline_collection_id) if parent_document_id: parent_document_id = outline.resolve_document_id(parent_document_id) for subnet in subnets: section_id = subnet.get("sectionId") if section_id not in section_cache: section_cache[section_id] = ipam.get_section_name(section_id) if section_id else None section_name = section_cache.get(section_id) addresses = ipam.list_addresses(subnet["id"]) title = _subnet_title(subnet) text = _subnet_markdown(subnet, addresses, section_name) if dry_run: results.append( { "subnet_id": subnet["id"], "title": title, "address_count": len(addresses), "action": "dry-run", } ) continue doc, created = outline.upsert_document( # type: ignore[union-attr] title, text, collection_id, parent_document_id, ) results.append( { "subnet_id": subnet["id"], "title": title, "address_count": len(addresses), "action": "created" if created else "updated", "outline_document_id": doc.get("id"), "outline_url": doc.get("url"), } ) finally: if outline is not None: outline.close() return results