#!/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: , 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: , 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 , 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()