Session-Caching, Domain-/Gruppen-Skripte; KeePass-DB aus Repo entfernen
- 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>
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user