first release
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
#!/usr/bin/env python3
|
||||
"""CLI / Orchestrierung: Plesk-Mail → Kerio-User+POP3-Sammler → Nextcloud-User → PDF."""
|
||||
import argparse
|
||||
import csv
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Callable, List, Tuple
|
||||
|
||||
from config import Config
|
||||
from models import Account, AccountResult, StepResult
|
||||
from pdf import write_combined_pdf, write_user_pdf
|
||||
|
||||
|
||||
def _detect_delimiter(path: Path) -> str:
|
||||
with open(path, "r", encoding="utf-8-sig") as f:
|
||||
sample = f.read(4096)
|
||||
if sample.count(";") > sample.count(","):
|
||||
return ";"
|
||||
return ","
|
||||
|
||||
|
||||
def parse_csv(path) -> List[Account]:
|
||||
path = Path(path)
|
||||
delim = _detect_delimiter(path)
|
||||
accounts: List[Account] = []
|
||||
with open(path, "r", encoding="utf-8-sig", newline="") as f:
|
||||
reader = csv.DictReader(f, delimiter=delim)
|
||||
for i, raw in enumerate(reader, 2): # row 1 = header
|
||||
row = {(k or "").strip().lower(): (v or "").strip() for k, v in raw.items()}
|
||||
try:
|
||||
quota_raw = row.get("nextcloudspeicher", "")
|
||||
quota = int(quota_raw) if quota_raw else None
|
||||
accounts.append(Account(
|
||||
name=row["name"],
|
||||
vorname=row["vorname"],
|
||||
emailadresse=row["emailadresse"],
|
||||
pleskhost=row["pleskhost"],
|
||||
keriohost=row["keriohost"],
|
||||
nextcloudhost=row["nextcloudhost"],
|
||||
kerioemailkennwort=row["kerioemailkennwort"],
|
||||
pleskemailkennwort=row["pleskemailkennwort"],
|
||||
nextcloudgruppe=row.get("nextcloudgruppe", ""),
|
||||
nextcloudspeicher=quota,
|
||||
nextcloudkennwort=row["nextcloudkennwort"],
|
||||
))
|
||||
except KeyError as e:
|
||||
raise ValueError(f"CSV Zeile {i}: Spalte fehlt → {e}") from e
|
||||
except ValueError as e:
|
||||
raise ValueError(f"CSV Zeile {i}: ungültiger Wert → {e}") from e
|
||||
return accounts
|
||||
|
||||
|
||||
def _make_plesk_client(account: Account, cfg: Config):
|
||||
"""Liefert (client, error_class). client=None → Manual-Modus."""
|
||||
backend = cfg.PLESK_BACKEND
|
||||
if backend == "manual":
|
||||
return None, None
|
||||
if backend == "ssh":
|
||||
from clients.plesk_ssh import PleskSshClient, PleskSshError
|
||||
return PleskSshClient(
|
||||
host=account.pleskhost,
|
||||
ssh_port=cfg.PLESK_SSH_PORT,
|
||||
ssh_user=cfg.PLESK_SSH_USER,
|
||||
ssh_password=cfg.PLESK_SSH_PASSWORD or None,
|
||||
ssh_key=cfg.PLESK_SSH_KEY or None,
|
||||
ssh_key_passphrase=cfg.PLESK_SSH_KEY_PASSPHRASE or None,
|
||||
use_sudo=cfg.PLESK_SSH_USE_SUDO,
|
||||
), PleskSshError
|
||||
if backend == "api":
|
||||
from clients.plesk import PleskClient, PleskError
|
||||
return PleskClient(
|
||||
host=account.pleskhost,
|
||||
api_key=cfg.PLESK_API_KEY or None,
|
||||
user=cfg.PLESK_USER or None,
|
||||
password=cfg.PLESK_PASSWORD or None,
|
||||
port=cfg.PLESK_PORT,
|
||||
verify=cfg.VERIFY_TLS,
|
||||
), PleskError
|
||||
raise ValueError(f"Unbekanntes PLESK_BACKEND={backend!r} (erlaubt: manual|api|ssh)")
|
||||
|
||||
|
||||
def _deploy_one(account: Account, cfg: Config, log: Callable[[str], None]) -> AccountResult:
|
||||
# Importe lokal, damit die GUI ohne Netzwerk-Modulladen startet.
|
||||
from clients.kerio import KerioClient, KerioError
|
||||
from clients.nextcloud import NextcloudClient, NextcloudError
|
||||
|
||||
result = AccountResult(account=account)
|
||||
|
||||
# 1) Plesk
|
||||
if cfg.PLESK_BACKEND == "manual":
|
||||
log(f" → Plesk: MANUELL anzulegen ({account.emailadresse} @ {account.pleskhost})")
|
||||
result.steps.append(StepResult(
|
||||
"Plesk", "manuell",
|
||||
f"Bitte im Plesk-Webinterface anlegen: {account.emailadresse}, "
|
||||
f"Passwort wie unten – Tool fährt mit Kerio/Nextcloud fort.",
|
||||
))
|
||||
else:
|
||||
log(f" → Plesk Mail anlegen ({cfg.PLESK_BACKEND}): "
|
||||
f"{account.emailadresse} @ {account.pleskhost}")
|
||||
try:
|
||||
plesk, plesk_error_cls = _make_plesk_client(account, cfg)
|
||||
try:
|
||||
if plesk.mail_exists(account.emailadresse):
|
||||
result.steps.append(StepResult(
|
||||
"Plesk", "übersprungen", "Mail existiert bereits"))
|
||||
log(" · existiert bereits, übersprungen")
|
||||
else:
|
||||
plesk.create_mail(account.emailadresse, account.pleskemailkennwort)
|
||||
result.steps.append(StepResult("Plesk", "angelegt"))
|
||||
log(" ✓ angelegt")
|
||||
finally:
|
||||
close = getattr(plesk, "close", None)
|
||||
if callable(close):
|
||||
close()
|
||||
except Exception as e:
|
||||
result.steps.append(StepResult("Plesk", "Fehler", str(e)))
|
||||
log(f" ✗ FEHLER: {e}")
|
||||
# Wir machen mit Kerio/Nextcloud trotzdem weiter – die sind unabhängig.
|
||||
|
||||
# 2) Kerio
|
||||
log(f" → Kerio User+POP3-Sammler: {account.emailadresse} @ {account.keriohost}")
|
||||
try:
|
||||
kerio = KerioClient(
|
||||
host=account.keriohost,
|
||||
user=cfg.KERIO_USER, password=cfg.KERIO_PASSWORD,
|
||||
port=cfg.KERIO_PORT, verify=cfg.VERIFY_TLS,
|
||||
)
|
||||
try:
|
||||
if kerio.user_exists(account.emailadresse):
|
||||
result.steps.append(StepResult("Kerio", "übersprungen", "User existiert bereits"))
|
||||
log(" · existiert bereits, POP3-Sammler nicht neu angelegt")
|
||||
else:
|
||||
uid = kerio.create_user(
|
||||
account.emailadresse,
|
||||
account.kerioemailkennwort,
|
||||
account.vollname,
|
||||
)
|
||||
kerio.add_pop3_collection(
|
||||
kerio_user_id=uid,
|
||||
server=account.pleskhost,
|
||||
login_name=account.emailadresse,
|
||||
password=account.pleskemailkennwort,
|
||||
port=cfg.POP3_PORT, ssl=True,
|
||||
leave_days=cfg.POP3_KEEP_DAYS,
|
||||
)
|
||||
result.steps.append(StepResult("Kerio", "angelegt", "inkl. POP3-Sammler"))
|
||||
log(" ✓ angelegt + POP3-Sammler")
|
||||
finally:
|
||||
kerio.logout()
|
||||
except Exception as e:
|
||||
result.steps.append(StepResult("Kerio", "Fehler", str(e)))
|
||||
log(f" ✗ FEHLER: {e}")
|
||||
|
||||
# 3) Nextcloud
|
||||
log(f" → Nextcloud User: {account.nextcloud_username} @ {account.nextcloudhost}")
|
||||
try:
|
||||
nc = NextcloudClient(
|
||||
host=account.nextcloudhost,
|
||||
admin_user=cfg.NEXTCLOUD_USER,
|
||||
admin_password=cfg.NEXTCLOUD_PASSWORD,
|
||||
verify=cfg.VERIFY_TLS,
|
||||
)
|
||||
if nc.user_exists(account.nextcloud_username):
|
||||
result.steps.append(StepResult("Nextcloud", "übersprungen", "User existiert bereits"))
|
||||
log(" · existiert bereits, übersprungen")
|
||||
else:
|
||||
if account.nextcloudgruppe:
|
||||
nc.ensure_group(account.nextcloudgruppe)
|
||||
nc.create_user(
|
||||
userid=account.nextcloud_username,
|
||||
password=account.nextcloudkennwort,
|
||||
email=account.emailadresse,
|
||||
display_name=account.vollname,
|
||||
group=account.nextcloudgruppe or None,
|
||||
quota_gb=account.nextcloudspeicher,
|
||||
)
|
||||
result.steps.append(StepResult("Nextcloud", "angelegt"))
|
||||
log(" ✓ angelegt")
|
||||
except Exception as e:
|
||||
result.steps.append(StepResult("Nextcloud", "Fehler", str(e)))
|
||||
log(f" ✗ FEHLER: {e}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _write_admin_report(path: Path, results: List[AccountResult],
|
||||
cfg: Config, timestamp: str) -> None:
|
||||
lines = [
|
||||
f"Deploy-Report {timestamp}",
|
||||
f"Plesk-Backend: {cfg.PLESK_BACKEND}",
|
||||
"=" * 70,
|
||||
"",
|
||||
]
|
||||
for r in results:
|
||||
a = r.account
|
||||
lines.append(f"{a.vollname} <{a.emailadresse}>")
|
||||
for s in r.steps:
|
||||
marker = {"angelegt": "✓", "übersprungen": "·",
|
||||
"manuell": "⚠", "Fehler": "✗"}.get(s.status, "?")
|
||||
detail = f" – {s.detail}" if s.detail else ""
|
||||
lines.append(f" {marker} {s.service:10s} {s.status}{detail}")
|
||||
lines.append("")
|
||||
if cfg.PLESK_BACKEND == "manual":
|
||||
lines += [
|
||||
"=" * 70,
|
||||
"PLESK MANUELL ANZULEGEN (Shared-Host – kein API/SSH):",
|
||||
" → im Plesk-Webinterface jede Mailadresse oben mit dem",
|
||||
" in der CSV vergebenen Plesk-Mail-Passwort einrichten,",
|
||||
" sonst kann der Kerio-POP3-Sammler die Mails nicht abholen.",
|
||||
]
|
||||
path.write_text("\n".join(lines), encoding="utf-8")
|
||||
|
||||
|
||||
def run_deploy(accounts: List[Account], cfg: Config,
|
||||
output_dir, log: Callable[[str], None] = print
|
||||
) -> Tuple[List[AccountResult], Path]:
|
||||
output_dir = Path(output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
results: List[AccountResult] = []
|
||||
for i, acc in enumerate(accounts, 1):
|
||||
log(f"[{i}/{len(accounts)}] {acc.vollname} <{acc.emailadresse}>")
|
||||
result = _deploy_one(acc, cfg, log)
|
||||
results.append(result)
|
||||
safe = "".join(c if c.isalnum() or c in "-_." else "_" for c in acc.emailadresse)
|
||||
per_pdf = output_dir / f"zugangsdaten_{safe}.pdf"
|
||||
write_user_pdf(per_pdf, acc, cfg.SMTP_PORT, cfg.POP3_PORT)
|
||||
log(f" ⤷ PDF: {per_pdf}")
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
combined = output_dir / f"zugangsdaten_gesamt_{timestamp}.pdf"
|
||||
write_combined_pdf(combined, accounts, cfg.SMTP_PORT, cfg.POP3_PORT)
|
||||
log(f"✓ Gesamt-PDF: {combined}")
|
||||
|
||||
report = output_dir / f"_admin_report_{timestamp}.txt"
|
||||
_write_admin_report(report, results, cfg, timestamp)
|
||||
log(f"✓ Admin-Report (nicht für Kunden!): {report}")
|
||||
return results, combined
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(
|
||||
description="Email/Cloud-Accounts auf Plesk + Kerio + Nextcloud deployen.",
|
||||
)
|
||||
p.add_argument("--csv", help="Pfad zur CSV-Datei (CLI-Modus)")
|
||||
p.add_argument("--output", default="./output", help="Verzeichnis für PDFs (default ./output)")
|
||||
p.add_argument("--gui", action="store_true", help="GUI starten")
|
||||
args = p.parse_args()
|
||||
|
||||
if args.gui or not args.csv:
|
||||
from gui import launch
|
||||
launch()
|
||||
return
|
||||
|
||||
cfg = Config()
|
||||
try:
|
||||
accounts = parse_csv(args.csv)
|
||||
except (FileNotFoundError, ValueError) as e:
|
||||
print(f"CSV-Fehler: {e}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
print(f"{len(accounts)} Account(s) eingelesen.\n")
|
||||
results, combined = run_deploy(accounts, cfg, args.output)
|
||||
failed = sum(1 for r in results if r.has_errors)
|
||||
print(f"\nFertig. {len(results) - failed}/{len(results)} ohne Fehler. Gesamt-PDF: {combined}")
|
||||
sys.exit(1 if failed else 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user