e31bdeb59d
- inwx_common.py: Login mit Session-Cache, KP_PW-Option - inwx_list/add: Domain als Argument, gemeinsamer Helfer - inwx_domains(_owner).py: Domains alphabetisch, NS, Gruppen via citeq-TXT (DNS) - hosting.kdbx aus Tracking genommen und in .gitignore Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
182 lines
6.0 KiB
Python
182 lines
6.0 KiB
Python
#!/usr/bin/env python3
|
||
"""Listet die Domains gruppiert nach den Einträgen in domain_owner auf.
|
||
|
||
Reihenfolge richtet sich nach der Datei domain_owner: zuerst der 1. Eintrag
|
||
(Ziffer + Klarname), darunter alle Domains mit dieser Ziffer, dann eine
|
||
Leerzeile, dann der 2. Eintrag usw. Domains ohne passende Zuordnung werden
|
||
am Ende separat aufgeführt.
|
||
"""
|
||
import os
|
||
import subprocess
|
||
import sys
|
||
|
||
from inwx_common import get_client, call_api
|
||
|
||
PAGE_LIMIT = 100 # Domains pro API-Seite
|
||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||
OWNER_FILE = os.path.join(BASE_DIR, "domain_owner")
|
||
TXT_PREFIX = "citeq:" # Präfix der Gruppen-TXT-Einträge
|
||
|
||
|
||
def fetch_all_domains(api):
|
||
"""Holt alle Domains über alle Seiten hinweg (domain.list ist paginiert)."""
|
||
domains = []
|
||
page = 1
|
||
while True:
|
||
res = call_api(api, 'domain.list', {'page': page, 'pageLimit': PAGE_LIMIT})
|
||
if res['code'] != 1000:
|
||
print(f"❌ API Fehler: {res['msg']} (Code: {res['code']})")
|
||
sys.exit(1)
|
||
|
||
data = res['resData']
|
||
domains.extend(data.get('domain', []))
|
||
|
||
# Solange wir noch nicht alle laut 'count' geladen haben, weiterblättern.
|
||
if len(domains) >= data.get('count', 0) or not data.get('domain'):
|
||
break
|
||
page += 1
|
||
|
||
return domains
|
||
|
||
|
||
def get_nameservers(api, domain):
|
||
"""Ermittelt die zuständigen Nameserver (Registry-Delegation) einer Domain.
|
||
|
||
Nutzt domain.info -> Feld 'ns', das die delegierten Nameserver enthält und
|
||
auch dann funktioniert, wenn die DNS-Zone nicht bei INWX gehostet wird.
|
||
Liefert eine sortierte Liste der NS-Hostnamen oder None bei Fehler.
|
||
"""
|
||
res = call_api(api, 'domain.info', {'domain': domain})
|
||
if res['code'] != 1000:
|
||
return None
|
||
|
||
ns = res['resData'].get('ns') or []
|
||
return sorted(n.rstrip('.') for n in ns)
|
||
|
||
|
||
def load_owners():
|
||
"""Liest die Zuordnung Ziffer -> Klarname aus der Datei domain_owner.
|
||
|
||
Die Reihenfolge der Einträge aus der Datei bleibt erhalten (dict behält
|
||
seit Python 3.7 die Einfügereihenfolge). Format pro Zeile:
|
||
<ziffer><Whitespace><klarname>, Trenner ist beliebiger Whitespace (ein
|
||
oder mehrere Tabs/Leerzeichen); der Klarname darf Leerzeichen enthalten.
|
||
Leere Zeilen und Kommentarzeilen (#) werden ignoriert.
|
||
"""
|
||
owners = {}
|
||
if not os.path.exists(OWNER_FILE):
|
||
print(f"⚠️ Datei {OWNER_FILE} nicht gefunden – keine Gruppierung möglich.")
|
||
return owners
|
||
try:
|
||
with open(OWNER_FILE, "r") as f:
|
||
for line in f:
|
||
line = line.strip()
|
||
if not line or line.startswith("#"):
|
||
continue
|
||
parts = line.split(None, 1) # an erstem Whitespace trennen
|
||
if len(parts) == 2:
|
||
ziffer, klarname = parts
|
||
owners[ziffer.strip()] = klarname.strip()
|
||
except Exception as e:
|
||
print(f"⚠️ Konnte {OWNER_FILE} nicht lesen: {e}")
|
||
return owners
|
||
|
||
|
||
def get_group(domain):
|
||
"""Ermittelt die Gruppen-Ziffer aus dem citeq:-TXT-Eintrag einer Domain.
|
||
|
||
Nutzt eine echte DNS-Abfrage (dig +short TXT) und funktioniert daher
|
||
unabhängig davon, wo die DNS-Zone gehostet wird. Liefert die Ziffer als
|
||
String oder None, wenn kein passender TXT-Eintrag gefunden wird.
|
||
"""
|
||
try:
|
||
out = subprocess.run(
|
||
["dig", "+short", "TXT", domain],
|
||
capture_output=True, text=True, timeout=10,
|
||
)
|
||
except FileNotFoundError:
|
||
print("⚠️ 'dig' nicht gefunden – Gruppen können nicht ermittelt werden.")
|
||
return None
|
||
except subprocess.TimeoutExpired:
|
||
return None
|
||
|
||
if out.returncode != 0:
|
||
return None
|
||
|
||
for line in out.stdout.splitlines():
|
||
# dig liefert TXT-Werte in Anführungszeichen, lange Werte ggf. in
|
||
# mehreren Teilen ("teil1" "teil2") -> zusammenfügen und entkleiden.
|
||
content = line.replace('" "', '').strip().strip('"')
|
||
if content.startswith(TXT_PREFIX):
|
||
return content[len(TXT_PREFIX):].strip()
|
||
return None
|
||
|
||
|
||
def format_domain_line(rec):
|
||
"""Einzeilige Darstellung einer Domain (eingerückt unter der Gruppe)."""
|
||
if rec['ns'] is None:
|
||
ns = "(nicht abrufbar)"
|
||
elif not rec['ns']:
|
||
ns = "(keine NS-Einträge gefunden)"
|
||
else:
|
||
ns = ", ".join(rec['ns'])
|
||
return f" {rec['name']:<35} {rec['status']:<8} {ns}"
|
||
|
||
|
||
def main():
|
||
api = get_client()
|
||
|
||
print("Rufe Domain-Liste ab...")
|
||
domains = fetch_all_domains(api)
|
||
|
||
if not domains:
|
||
print("ℹ️ Keine Domains gefunden.")
|
||
return
|
||
|
||
# Zuordnung Ziffer -> Klarname laden (in Dateireihenfolge)
|
||
owners = load_owners()
|
||
|
||
# Pro Domain einmal Ziffer (DNS) und Nameserver (API) ermitteln
|
||
records = []
|
||
for d in domains:
|
||
name = d.get('domain', '')
|
||
records.append({
|
||
'name': name,
|
||
'status': d.get('status', ''),
|
||
'ziffer': get_group(name),
|
||
'ns': get_nameservers(api, name),
|
||
})
|
||
|
||
# Alphabetisch sortieren (Groß-/Kleinschreibung ignorieren)
|
||
records.sort(key=lambda r: r['name'].lower())
|
||
|
||
# Ausgabe in der Reihenfolge der Einträge aus domain_owner
|
||
print()
|
||
for ziffer, klarname in owners.items():
|
||
print(f"{ziffer} ({klarname})")
|
||
matching = [r for r in records if r['ziffer'] == ziffer]
|
||
if matching:
|
||
for rec in matching:
|
||
print(format_domain_line(rec))
|
||
else:
|
||
print(" (keine Domains)")
|
||
print() # Leerzeile zwischen den Gruppen
|
||
|
||
# Domains, die keiner Ziffer aus domain_owner zugeordnet sind
|
||
rest = [r for r in records if r['ziffer'] not in owners]
|
||
if rest:
|
||
print("Ohne Zuordnung")
|
||
for rec in rest:
|
||
ziffer = rec['ziffer']
|
||
hinweis = "" if ziffer is None else f" [citeq:{ziffer} unbekannt]"
|
||
print(format_domain_line(rec) + hinweis)
|
||
print()
|
||
|
||
print(f"Gesamt: {len(records)} Domain(s)")
|
||
|
||
# Kein logout() -> die Session bleibt gültig und kann wiederverwendet werden.
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|