Files
inwx-scripts/inwx_domains_owner.py
2026-06-19 17:00:37 +02:00

243 lines
8.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 collections import Counter
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")
PRICE_FILE = os.path.join(BASE_DIR, "domain_price")
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 tld_of(domain):
"""Liefert die Top-Level-Domain (Teil hinter dem letzten Punkt)."""
return domain.rsplit('.', 1)[-1].lower() if '.' in domain else domain.lower()
def load_prices():
"""Liest die Preise je Top-Level-Domain aus der Datei domain_price.
Format pro Zeile: <tld><Whitespace><preis>, z.B. 'de 5,00'. Als Dezimal-
trennzeichen wird Komma akzeptiert. Leere Zeilen und Kommentare (#) werden
ignoriert. Liefert ein Dict {tld: preis_als_float}.
"""
prices = {}
if not os.path.exists(PRICE_FILE):
print(f"⚠️ Datei {PRICE_FILE} nicht gefunden keine Preise verfügbar.")
return prices
try:
with open(PRICE_FILE, "r") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
parts = line.split(None, 1)
if len(parts) == 2:
tld, preis = parts
try:
prices[tld.strip().lower()] = float(preis.strip().replace(",", "."))
except ValueError:
print(f"⚠️ Ungültiger Preis in domain_price: {line!r}")
except Exception as e:
print(f"⚠️ Konnte {PRICE_FILE} nicht lesen: {e}")
return prices
def fmt_price(value):
"""Formatiert einen Betrag im deutschen Format, z.B. 10,00 €."""
return f"{value:.2f}".replace(".", ",") + ""
def print_group_sums(recs, ziffer, prices):
"""Gibt für eine Gruppe die Anzahl Domains pro Top-Level-Domain aus.
Eine Zeile je TLD im Format <ziffer> <tld> <anzahl> <preis>, z.B.
'10 de 1 5,00 €'. Der Preis ergibt sich aus Anzahl × Einzelpreis laut
domain_price; fehlt ein Preis, wird ein Hinweis ausgegeben.
"""
counts = Counter(tld_of(r['name']) for r in recs)
for tld, anzahl in sorted(counts.items()):
einzel = prices.get(tld)
if einzel is None:
print(f" {ziffer} {tld} {anzahl} (kein Preis)")
else:
print(f" {ziffer} {tld} {anzahl} {fmt_price(anzahl * einzel)}")
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 und Preise je TLD laden
owners = load_owners()
prices = load_prices()
# 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))
print_group_sums(matching, ziffer, prices) # Summe direkt unter den Domains
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_group_sums(rest, "-", prices) # Summe direkt unter den Domains
print()
gesamt_preis = sum(prices.get(tld_of(r['name']), 0.0) for r in records)
print(f"Gesamt: {len(records)} Domain(s), {fmt_price(gesamt_preis)}")
# Kein logout() -> die Session bleibt gültig und kann wiederverwendet werden.
if __name__ == "__main__":
main()