Fehlende exponierte Dienste ergänzt (RaspberryMatic 80/443/2001/2010, iob-master 8081, z2m25 8080). Jeder Port per Socket-Probe belegt (nicht aus Doku geraten); nc -z lieferte falsch-negativ und wurde verworfen. iob-exposed/iob_slave bewusst ohne Service (kein Port gefunden). Nautobot: 20 -> 26 ipam.Services, idempotent. Compliance 140/140. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
179 lines
7.5 KiB
Python
179 lines
7.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
TP37 Punkt 2 (Dienste) — exponierte Ports als native ipam.Service-Objekte.
|
|
|
|
Idempotenter Seeder (get-or-create) fuer ipam.Service:
|
|
je VM-Name + Port + Protokoll ein Service-Eintrag.
|
|
|
|
Vorbedingung: TP37 P1 (tp37-substrat-seed.py --commit) muss vorher gelaufen sein,
|
|
da die VirtualMachine-Objekte existieren muessen.
|
|
|
|
Datenmodell-Entscheid (User 2026-09-06): native Modelle, KEINE eigene App.
|
|
Muster: nautobot/tp37-substrat-seed.py.
|
|
|
|
Umfang (User-Default 2026-09-07):
|
|
Caddy (:80/:443), Caddy-Backends (haos:8123, vaultwarden:80, gitea:3000, n8n:5678),
|
|
Nautobot:8080, AdGuard:53 (bf1+bf2, tcp+udp), platform-pg:5432, mariadb:3306,
|
|
influxdb2:8086, mqtt5:1883, portainer:9443,
|
|
MCP-Dienste (pve-mcp:3000, shelly-mcp:3030, n8n-mcp:3001, gitea-mcp:3000).
|
|
+ Smart-Home (vervollstaendigt 2026-09-08, Ports per Socket-Probe verifiziert):
|
|
RaspberryMatic/CCU (80/443/2001/2010), iob-master:8081, z2m25:8080.
|
|
iob-exposed/iob_slave BEWUSST ohne Service (kein exponierter Port gefunden).
|
|
|
|
Token-Quelle: docs/nas-vlan20/stacks/210-nautobot.yml (gitignored),
|
|
Schluessel NAUTOBOT_SUPERUSER_API_TOKEN.
|
|
|
|
Aufruf:
|
|
python3 nautobot/tp37-dienste-seed.py # Dry-Run (default)
|
|
python3 nautobot/tp37-dienste-seed.py --commit # legt an
|
|
"""
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
import urllib.request
|
|
import urllib.parse
|
|
import urllib.error
|
|
from pathlib import Path
|
|
|
|
NB = os.environ.get("NAUTOBOT_URL", "http://10.40.20.30:8080")
|
|
REPO = Path(__file__).resolve().parent.parent
|
|
YML = REPO / "docs" / "nas-vlan20" / "stacks" / "210-nautobot.yml"
|
|
COMMIT = "--commit" in sys.argv
|
|
|
|
|
|
def load_token() -> str:
|
|
for line in YML.read_text().splitlines():
|
|
m = re.search(r"NAUTOBOT_SUPERUSER_API_TOKEN[=:]\s*([^\s\"']+)", line)
|
|
if m:
|
|
return m.group(1)
|
|
sys.exit(f"NAUTOBOT_SUPERUSER_API_TOKEN nicht in {YML} gefunden")
|
|
|
|
|
|
TOKEN = load_token()
|
|
HDRS = {"Authorization": f"Token {TOKEN}", "Accept": "application/json",
|
|
"Content-Type": "application/json"}
|
|
|
|
|
|
def req(method: str, path: str, body=None):
|
|
url = f"{NB}{path}"
|
|
data = json.dumps(body).encode() if body is not None else None
|
|
r = urllib.request.Request(url, data=data, headers=HDRS, method=method)
|
|
try:
|
|
with urllib.request.urlopen(r, timeout=20) as resp:
|
|
return resp.status, json.loads(resp.read().decode() or "{}")
|
|
except urllib.error.HTTPError as e:
|
|
return e.code, json.loads(e.read().decode() or "{}")
|
|
|
|
|
|
def find(endpoint: str, params: dict, match_key: str, match_val: str):
|
|
q = urllib.parse.urlencode(dict(params, depth=0))
|
|
st, d = req("GET", f"/api/{endpoint}/?{q}")
|
|
for row in d.get("results", []):
|
|
if row.get(match_key) == match_val:
|
|
return row
|
|
return None
|
|
|
|
|
|
def get_or_create(endpoint: str, find_params: dict, match_key: str,
|
|
match_val: str, payload: dict, label: str):
|
|
existing = find(endpoint, find_params, match_key, match_val)
|
|
if existing:
|
|
print(f" = {label}: '{match_val}' existiert ({existing['id']})")
|
|
return existing
|
|
if not COMMIT:
|
|
print(f" + {label}: '{match_val}' [DRY-RUN]")
|
|
return {"id": f"<dry:{match_val}>"}
|
|
st, d = req("POST", f"/api/{endpoint}/", payload)
|
|
if st in (200, 201):
|
|
print(f" + {label}: '{match_val}' angelegt ({d.get('id')})")
|
|
return d
|
|
print(f" ! {label}: '{match_val}' FEHLER {st}: {json.dumps(d)[:300]}")
|
|
return None
|
|
|
|
|
|
# ---------------------------------------------------------------- Inventar
|
|
# (vm_name, svc_name, ports, protocol, description)
|
|
SERVICES = [
|
|
# Caddy Reverse-Proxy (TP34 critical)
|
|
("caddy-bf1", "caddy-http", [80], "tcp", "Caddy HTTP"),
|
|
("caddy-bf1", "caddy-https", [443], "tcp", "Caddy HTTPS Reverse Proxy"),
|
|
# Caddy-Backends via bf-public Tunnel
|
|
("haos", "home-assistant", [8123], "tcp", "Home Assistant Web"),
|
|
("vaultwarden", "vaultwarden-http", [80], "tcp", "Vaultwarden (HTTP, TLS via Caddy)"),
|
|
("gitea", "gitea-http", [3000], "tcp", "Gitea Web"),
|
|
("n8n", "n8n-web", [5678], "tcp", "n8n Workflow Engine"),
|
|
# Nautobot
|
|
("nautobot", "nautobot-web", [8080], "tcp", "Nautobot API/Web"),
|
|
# DNS (TCP + UDP je Resolver)
|
|
("agh-bf1", "adguard-dns-tcp", [53], "tcp", "AdGuard Home DNS/TCP"),
|
|
("agh-bf1", "adguard-dns-udp", [53], "udp", "AdGuard Home DNS/UDP"),
|
|
("agh-bf2", "adguard-dns-tcp", [53], "tcp", "AdGuard Home DNS/TCP"),
|
|
("agh-bf2", "adguard-dns-udp", [53], "udp", "AdGuard Home DNS/UDP"),
|
|
# Infrastruktur
|
|
("platform-pg", "postgresql", [5432], "tcp", "PostgreSQL 16 (Nautobot-DB)"),
|
|
("mariadb", "mariadb", [3306], "tcp", "MariaDB (vw/hass/gitea)"),
|
|
("influxdb2", "influxdb", [8086], "tcp", "InfluxDB 2"),
|
|
("mqtt5", "mqtt", [1883], "tcp", "MQTT Broker (Mosquitto)"),
|
|
("docker", "portainer", [9443], "tcp", "Portainer CE"),
|
|
# MCP-Dienste
|
|
("pve-mcp", "pve-mcp", [3000], "tcp", "Proxmox MCP (StreamableHTTP)"),
|
|
("shelly-mcp", "shelly-mcp", [3030], "tcp", "Shelly MCP"),
|
|
("n8n-mcp", "n8n-mcp", [3001], "tcp", "n8n MCP"),
|
|
("gitea-mcp", "gitea-mcp", [3000], "tcp", "Gitea MCP"),
|
|
# Smart-Home / IoT — Ports live per Socket-Probe verifiziert 2026-09-08 (NICHT raten!)
|
|
("RaspberryMatic","ccu-http", [80], "tcp", "Homematic CCU Web (HTTP)"),
|
|
("RaspberryMatic","ccu-https", [443], "tcp", "Homematic CCU Web (HTTPS)"),
|
|
("RaspberryMatic","ccu-rpc-bidcos", [2001], "tcp", "Homematic BidCos-RF XML-RPC"),
|
|
("RaspberryMatic","ccu-rpc-hmip", [2010], "tcp", "Homematic-IP XML-RPC"),
|
|
("iob-master", "iobroker-admin", [8081], "tcp", "ioBroker Admin"),
|
|
("z2m25", "zigbee2mqtt", [8080], "tcp", "Zigbee2MQTT Frontend"),
|
|
# iob-exposed (.19) + iob_slave (.33): kein exponierter Port gefunden (13 Ports, alle Timeout)
|
|
# -> BEWUSST kein Service, bis ein Port live belegt ist. Nicht "hilfreich" einen erfinden.
|
|
]
|
|
|
|
|
|
def main():
|
|
print(f"== TP37 Dienste-Seed ({'COMMIT' if COMMIT else 'DRY-RUN'}) @ {NB} ==")
|
|
|
|
# VM-IDs vorab holen (unique Namen, Batch)
|
|
vm_names = sorted({vm for vm, *_ in SERVICES})
|
|
print(f"\n[0] VM-IDs holen ({len(vm_names)} unique VMs)")
|
|
vm_ids = {}
|
|
for name in vm_names:
|
|
row = find("virtualization/virtual-machines", {"name": name}, "name", name)
|
|
if row:
|
|
vm_ids[name] = row["id"]
|
|
print(f" = {name}: {row['id']}")
|
|
else:
|
|
vm_ids[name] = None
|
|
print(f" ! {name}: NICHT gefunden — Service-Eintraege werden uebersprungen")
|
|
|
|
print(f"\n[1] ipam.Services ({len(SERVICES)} Eintraege)")
|
|
n_ok = n_skip = 0
|
|
for vm_name, svc_name, ports, protocol, desc in SERVICES:
|
|
vm_id = vm_ids.get(vm_name)
|
|
if not vm_id and COMMIT:
|
|
print(f" ~ {vm_name}/{svc_name}: VM fehlt, uebersprungen")
|
|
n_skip += 1
|
|
continue
|
|
svc = get_or_create(
|
|
"ipam/services",
|
|
{"name": svc_name, "virtual_machine": vm_name},
|
|
"name", svc_name,
|
|
{"virtual_machine": vm_id, "name": svc_name,
|
|
"ports": ports, "protocol": protocol, "description": desc},
|
|
f"{vm_name}/{protocol}/{ports[0]}",
|
|
)
|
|
if svc:
|
|
n_ok += 1
|
|
else:
|
|
n_skip += 1
|
|
|
|
print(f"\n== fertig ({'COMMIT' if COMMIT else 'DRY-RUN — nichts geschrieben'}) "
|
|
f"| Services={n_ok} uebersprungen={n_skip} ==")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|