#!/usr/bin/env python3 r"""TP45 Schicht A — primaere IP eines Nautobot-Gastes umziehen. WARUM ES DIESES SKRIPT GIBT --------------------------- Nautobot verbietet das Aendern einer IP-Adresse nach dem Anlegen (`{"__all__": ["Host address cannot be changed once created"]}`). Ein Umzug ist deshalb kein PATCH, sondern ein Dreischritt je Gast: 1. neues ipam.IPAddress am Ziel anlegen (Status/Namespace vom Altobjekt) 2. altes Objekt an die VM-Schnittstelle haengen und primary_ip4 umhaengen 3. altes Objekt loeschen Von Hand ist das pro Gast drei API-Aufrufe in genau dieser Reihenfolge — bei ~15 Gaesten in Schicht A eine sichere Fehlerquelle. Reihenfolge ist zwingend: wer das Altobjekt vor dem Umhaengen loescht, reisst die primary_ip4-Referenz. ⚠️ Die IP allein ist NICHT der ganze Umzug. Ein Gast, den der Core ueber eine Firewall-Adressliste erreicht (z.B. `nautobot`), braucht die neue IP ZUERST in dieser Liste — sonst steht der Zugriff weg, bevor die Migration durch ist. python3 nautobot/tp45-ip-move.py # Dry-Run (Default) python3 nautobot/tp45-ip-move.py --apply """ import json import re import sys import urllib.error import urllib.request from pathlib import Path REPO = Path(__file__).resolve().parent.parent YML = REPO / "docs" / "nas-vlan20" / "stacks" / "210-nautobot.yml" BASE = "http://10.40.20.154:8080/api" APPLY = "--apply" in sys.argv # (VM-Name, alte IP, neue IP) — Oktett-Klassen nach TP32, s. docs/teilprojekte.md ### TP45 # Charge 1 (2026-09-21, angewendet). Die Altobjekte sind weg — ein erneuter Lauf # bricht darum sauber ab ("0 Treffer"), er zieht nichts doppelt um. MOVES = [ ("nautobot", "10.40.20.30/24", "10.40.20.154/24"), ("nautobot-worker", "10.40.20.31/24", "10.40.20.155/24"), ("nautobot-scheduler", "10.40.20.32/24", "10.40.20.156/24"), ("nautobot-redis", "10.40.20.34/24", "10.40.20.158/24"), ] 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) raise SystemExit(f"NAUTOBOT_SUPERUSER_API_TOKEN nicht in {YML}") TOK = load_token() def api(path, method="GET", payload=None): req = urllib.request.Request( BASE + path, method=method, data=json.dumps(payload).encode() if payload is not None else None, headers={"Authorization": f"Token {TOK}", "Content-Type": "application/json", "Accept": "application/json"}) try: with urllib.request.urlopen(req, timeout=30) as r: return json.load(r) if r.status != 204 else {} except urllib.error.HTTPError as e: raise SystemExit(f"{method} {path} -> {e.code}: {e.read().decode()[:300]}") def only(path, expect=1): res = api(path)["results"] if len(res) != expect: raise SystemExit(f"{path}: {len(res)} Treffer, erwartet {expect}") return res[0] if expect == 1 else res def main(): print(f"== TP45 IP-Move ({'APPLY' if APPLY else 'DRY-RUN'}) ==") for vm_name, old_ip, new_ip in MOVES: print(f"\n-- {vm_name}: {old_ip} -> {new_ip} --") old = only(f"/ipam/ip-addresses/?address={old_ip}") vm = only(f"/virtualization/virtual-machines/?name={vm_name}") ref = vm.get("primary_ip4") ref = ref.get("id") if isinstance(ref, dict) else ref if ref != old["id"]: raise SystemExit(f" primary_ip4 von {vm_name} zeigt nicht auf {old_ip} " f"(ist {ref}) — Abbruch, nichts geschrieben") if api(f"/ipam/ip-addresses/?address={new_ip}")["count"]: raise SystemExit(f" {new_ip} ist in Nautobot schon belegt — Abbruch") print(f" Altobjekt={old['id'][:8]} primary_ip4 bestaetigt Ziel frei") if not APPLY: print(" [DRY-RUN] wuerde: anlegen -> eth0 verknuepfen -> primary_ip4 " "umhaengen -> Altobjekt loeschen") continue # namespace ist am Altobjekt nicht serialisiert und beim Anlegen Pflicht ns = old.get("namespace") ns_id = (ns or {}).get("id") if isinstance(ns, dict) else ns if not ns_id: ns_id = only("/ipam/namespaces/?name=Global")["id"] st = old.get("status") new = api("/ipam/ip-addresses/", "POST", {"address": new_ip, "status": st["id"] if isinstance(st, dict) else st, "namespace": ns_id, "dns_name": ""}) print(f" + angelegt {new['id'][:8]} @ {new['address']}") vmif = api(f"/virtualization/interfaces/?virtual_machine={vm['id']}&name=eth0")["results"] if vmif: api("/ipam/ip-address-to-interface/", "POST", {"ip_address": new["id"], "vm_interface": vmif[0]["id"]}) print(f" + verknuepft mit {vm_name}/eth0 ({vmif[0]['id'][:8]})") else: print(" ~ keine eth0-Schnittstelle — Verknuepfung uebersprungen") api(f"/virtualization/virtual-machines/{vm['id']}/", "PATCH", {"primary_ip4": new["id"]}) print(f" + primary_ip4 umgehaengt") api(f"/ipam/ip-addresses/{old['id']}/", "DELETE") print(f" - Altobjekt {old_ip} geloescht") print("\nFertig." if APPLY else "\nDRY-RUN — nichts geschrieben. Mit --apply ausfuehren.") if __name__ == "__main__": main()