Oktett-Klasse .150-.199 (App-Services). Nautobot gibt damit .30/.31 an AGH frei. .157 ist gitea, deshalb nimmt redis .158. - Datenmigration ueber neues nautobot/tp45-ip-move.py: Nautobot verbietet das Aendern einer IP nach dem Anlegen -> anlegen/primary_ip4 umhaengen/ Altobjekt loeschen, in dieser Reihenfolge - Stack-Redeploy (PUT /api/stacks/203), Caddy-Backend, Core-Literal *24 ueber GC deployt (Plan 51a90e25) mit Wirksamkeitspruefung am Geraet - caddy/deploy.sh: push_verified() ersetzt den push||push-Fallback. `pct push` meldet Exit 0 AUCH wenn es die Quelle nicht lesen kann, der Fallback griff deshalb nie und `caddy validate` pruefte die ALTE Datei -> gruene Abnahme bei unveraendertem Zustand (Symptom: 502 bei gesundem Backend) - 39 Referenzen in 28 Dateien mitgezogen; lebende Doku nachgezogen (CLAUDE.md, ip-plan.md, cf-tunnel-mapping §0-Kopf, TP16.3/TP31-Prosa) - GC-Compliance 140/140, 0 Drifts Neue Funde (TP45-Prosa + CLAUDE.md): - Die Core-Adressliste `nautobot` wird von GC nirgends gerendert -> ein Recovery von null erzeugt einen Core, in den Nautobot nicht mehr per SSH kommt. Gleiche Klasse wie die fehlenden DHCP-Reservierungen. - Regel *24 (Literal) und *28 (Liste) sind Dubletten; der Literal steht bei 0 Paketen. Zusammenlegung bleibt offen. - gc-manual-plan.py: /api/extras/job-log-entries/ liefert 404 (Nautobot 3.x) -> bei gescheitertem Deploy fehlt die Fehlermeldung. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
132 lines
5.3 KiB
Python
132 lines
5.3 KiB
Python
#!/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()
|