Files
kerio-connect-rename-user/kerio_trial_setup.py
T
duffyduckandClaude Opus 5 b12041fc81 Script fuer den Erstkonfigurationsassistenten, gemeinsames Modul
kerio_trial_setup.py bringt eine frische Installation am Assistenten
vorbei, wenn dessen Lizenzschritt nicht durchlaufbar ist: der Link
"Im unregistrierten Modus aktivieren" fehlt je nach Version und die
verlinkte Registrierungsseite ist seit der GFI-Uebernahme teils tot.
Gesetzt wird ConfigWizardDone in mailserver.cfg; Kerio laeuft danach
30 Tage unregistriert, ohne Einschraenkung bei Benutzerverwaltung und
Admin-API.

Die Datei darf nur bei gestopptem Dienst bearbeitet werden, weil Kerio
seine Konfiguration beim Beenden aus dem Speicher zurueckschreibt. Das
Script stoppt den Dienst, liest danach neu ein, schreibt atomar ueber
eine temporaere Datei und liest den Wert nach dem Start zur Kontrolle
erneut. Backup vor jeder Aenderung, --restore spielt es zurueck.

API-Client und Dienststeuerung wandern nach kerio_common.py, damit
beide Scripts sie teilen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 13:26:09 +02:00

361 lines
13 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Bringt eine frische Kerio-Connect-Installation am Erstkonfigurationsassistenten
vorbei, wenn dessen Lizenzierungsschritt nicht durchlaufbar ist.
Hintergrund: Der Assistent verlangt im Schritt "Registrierte Testaktivierung"
eine Testlizenznummer samt CAPTCHA. Der Link "Im unregistrierten Modus
aktivieren" fehlt je nach Version oder ist im Fenster abgeschnitten, und die
verlinkte Registrierungsseite ist seit der Uebernahme durch GFI teilweise tot.
Kerio kennt dafuer den Schalter ConfigWizardDone in mailserver.cfg. Steht er
auf 1, oeffnet die Administrationskonsole den Assistenten nicht mehr und der
Server laeuft 30 Tage ab Installation im unregistrierten Modus.
Was im unregistrierten Modus fehlt: Antivirus-Updates, ActiveSync, Greylisting,
Hersteller-Support. Nach 30 Tagen stoppt die Engine. Benutzerverwaltung,
Administrationskonsole und Admin-API sind nicht eingeschraenkt.
WICHTIG: mailserver.cfg darf nur bei gestopptem Dienst bearbeitet werden.
Kerio schreibt seine Konfiguration beim Beenden aus dem Speicher zurueck und
wuerde eine Aenderung am laufenden Dienst wieder ueberschreiben. Genau das
erledigt dieses Script in der richtigen Reihenfolge.
Das Script muss LOKAL AUF DEM KERIO-SERVER ALS ROOT laufen.
Beispiele:
./kerio_trial_setup.py --status # nur anzeigen
sudo ./kerio_trial_setup.py --skip-wizard # Assistenten ueberspringen
sudo ./kerio_trial_setup.py --restore # letztes Backup zuruecksetzen
"""
from __future__ import annotations
import argparse
import os
import re
import shutil
import sys
import time
from datetime import datetime, timedelta
from pathlib import Path
from kerio_common import (
DEFAULT_INSTALL_DIR,
DEFAULT_SERVICE,
AbortedError,
ServiceController,
)
# Kerio deaktiviert die Engine 30 Tage nach der Installation.
TRIAL_DAYS = 30
BACKUP_SUFFIX = ".bak-trial-setup"
# <variable name="ConfigWizardDone">0</variable>
WIZARD_PATTERN = re.compile(
r'(<variable\s+name="ConfigWizardDone"\s*>)([^<]*)(</variable>)'
)
class ConfigFile:
"""Liest und schreibt mailserver.cfg mit Backup."""
def __init__(self, path: Path) -> None:
self.path = path
if not path.is_file():
raise AbortedError(
f"'{path}' existiert nicht. Installationsverzeichnis mit "
"--install-dir angeben."
)
self.text = path.read_text(encoding="utf-8")
# -- Lesen -------------------------------------------------------------
def wizard_done(self) -> str | None:
match = WIZARD_PATTERN.search(self.text)
return match.group(2).strip() if match else None
def trial_id(self) -> str | None:
match = re.search(
r'<variable\s+name="TrialID"\s*>([^<]*)</variable>', self.text)
return match.group(1).strip() if match else None
def store_dir(self) -> str | None:
match = re.search(
r'<variable\s+name="StoreDir"\s*>([^<]*)</variable>', self.text)
return match.group(1).strip() if match else None
# -- Schreiben ---------------------------------------------------------
def set_wizard_done(self, value: str = "1") -> bool:
"""Setzt ConfigWizardDone. Gibt False zurueck, wenn schon so."""
match = WIZARD_PATTERN.search(self.text)
if match is None:
raise AbortedError(
"ConfigWizardDone steht nicht in der Konfiguration. "
"Andere Kerio-Version? Bitte die Datei von Hand pruefen."
)
if match.group(2).strip() == value:
return False
self.text = WIZARD_PATTERN.sub(rf"\g<1>{value}\g<3>", self.text, count=1)
return True
def backup(self) -> Path:
target = self.path.with_name(self.path.name + BACKUP_SUFFIX)
shutil.copy2(self.path, target)
return target
def save(self) -> None:
# Ueber eine temporaere Datei im selben Verzeichnis, damit die
# Konfiguration nie halb geschrieben auf der Platte liegt.
temp = self.path.with_name(self.path.name + ".tmp-trial-setup")
temp.write_text(self.text, encoding="utf-8")
shutil.copystat(self.path, temp)
os.replace(temp, self.path)
def estimate_install_date(install_dir: Path, config: ConfigFile) -> datetime | None:
"""Schaetzt den Installationszeitpunkt.
Kerio fuehrt kein Installationsdatum in der Konfiguration, deshalb bleibt
nur der Umweg ueber Zeitstempel:
1. der Eintrag der Paketverwaltung - am zuverlaessigsten
2. sonst der aelteste Zeitstempel unter den Store-Unterverzeichnissen,
die Kerio einmalig beim ersten Start anlegt
Verzeichnisse wie store/ selbst taugen nicht: ihr Zeitstempel wandert
mit jeder Aenderung darin. Der Wert ist eine Schaetzung, kein Beweis.
"""
for package_file in (
Path("/var/lib/dpkg/info/kerio-connect.list"),
Path("/var/lib/dpkg/info/kerio-connect:amd64.list"),
):
if package_file.is_file():
try:
return datetime.fromtimestamp(package_file.stat().st_mtime)
except OSError:
pass
store = Path(config.store_dir() or (install_dir / "store"))
stamps = []
for name in ("queue", "saas", "spamassassin", "logs"):
path = store / name
if path.is_dir():
try:
stamps.append(path.stat().st_mtime)
except OSError:
continue
if stamps:
return datetime.fromtimestamp(min(stamps))
return None
def print_status(config: ConfigFile, install_dir: Path,
service: ServiceController) -> None:
print("Status")
print("------")
print(f" Konfiguration : {config.path}")
running = service.is_running()
state = {True: "laeuft", False: "gestoppt", None: "unbekannt"}[running]
print(f" Dienst : {service.describe()} -> {state}")
wizard = config.wizard_done()
if wizard == "1":
wizard_text = "1 (Assistent wird uebersprungen)"
elif wizard == "0":
wizard_text = "0 (Assistent erscheint beim Oeffnen der Konsole)"
else:
wizard_text = f"{wizard!r} (unerwarteter Wert)"
print(f" ConfigWizard : {wizard_text}")
trial = config.trial_id()
print(f" TrialID : {trial or '(leer - unregistrierter Modus)'}")
license_dir = install_dir / "license"
if license_dir.is_dir():
entries = [e for e in license_dir.iterdir() if not e.name.startswith(".")]
print(f" Lizenzdateien : {len(entries) or 'keine'}")
installed = estimate_install_date(install_dir, config)
if installed:
expires = installed + timedelta(days=TRIAL_DAYS)
remaining = (expires - datetime.now()).days
print(f" Installiert : ~{installed:%Y-%m-%d %H:%M} (geschaetzt)")
if remaining >= 0:
print(f" Testzeitraum : laeuft ~{expires:%Y-%m-%d} ab "
f"(noch ~{remaining} Tage)")
else:
print(f" Testzeitraum : seit ~{expires:%Y-%m-%d} abgelaufen "
"- die Engine wird deaktiviert sein")
def skip_wizard(config: ConfigFile, service: ServiceController,
args: argparse.Namespace) -> int:
if config.wizard_done() == "1":
print("\nConfigWizardDone steht bereits auf 1 - nichts zu tun.")
return 0
service.preflight()
print("\nGeplante Aenderung")
print("------------------")
print(f" {config.path}")
print(" ConfigWizardDone: 0 -> 1")
print(f" Backup nach : {config.path.name}{BACKUP_SUFFIX}")
print(f" Dienst : {service.describe()} (stoppen, aendern, starten)")
if args.dry_run:
print("\n--dry-run: es wurde nichts geaendert.")
return 0
if not args.yes:
print("\nAchtung: der Kerio-Dienst wird dabei kurz gestoppt.")
if input("Jetzt durchfuehren? [j/N] ").strip().lower() not in ("j", "ja", "y", "yes"):
print("Abgebrochen.")
return 1
print()
was_running = service.is_running()
service.stop()
try:
# Erst nach dem Stoppen neu einlesen: Kerio hat die Datei beim
# Beenden zurueckgeschrieben, unsere Kopie von vorhin ist veraltet.
fresh = ConfigFile(config.path)
backup = fresh.backup()
print(f" Backup: {backup}")
if not fresh.set_wizard_done("1"):
print(" ConfigWizardDone stand nach dem Stoppen schon auf 1.")
else:
fresh.save()
print(" ConfigWizardDone auf 1 gesetzt.")
except (AbortedError, OSError) as exc:
print(f"\nFEHLER: {exc}", file=sys.stderr)
if was_running:
service.start()
return 2
finally:
if was_running:
service.start()
# Gegenlesen: haelt der Wert, nachdem Kerio wieder laeuft?
time.sleep(2)
check = ConfigFile(config.path)
if check.wizard_done() != "1":
print("\n WARNUNG: Der Wert steht nach dem Start nicht mehr auf 1. "
"Lief der Dienst beim Schreiben doch noch?", file=sys.stderr)
return 2
print("\nFertig. Die Administrationskonsole oeffnet den Assistenten nicht mehr.")
print("Kerio laeuft im unregistrierten Modus - 30 Tage ab Installation.")
return 0
def restore(config_path: Path, service: ServiceController,
args: argparse.Namespace) -> int:
backup = config_path.with_name(config_path.name + BACKUP_SUFFIX)
if not backup.is_file():
raise AbortedError(f"Kein Backup gefunden: {backup}")
print(f"\nStellt '{backup}' nach '{config_path}' wieder her.")
if args.dry_run:
print("--dry-run: es wurde nichts geaendert.")
return 0
if not args.yes:
if input("Jetzt zuruecksetzen? [j/N] ").strip().lower() not in ("j", "ja", "y", "yes"):
print("Abgebrochen.")
return 1
print()
was_running = service.is_running()
service.stop()
try:
shutil.copy2(backup, config_path)
print(f" Wiederhergestellt aus {backup}")
finally:
if was_running:
service.start()
return 0
def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Kerio Connect am Erstkonfigurationsassistenten vorbeibringen.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument("--install-dir", default=DEFAULT_INSTALL_DIR,
help=f"Kerio-Installationsverzeichnis "
f"(Standard: {DEFAULT_INSTALL_DIR})")
parser.add_argument("--service-name", default=DEFAULT_SERVICE,
help=f"Name des Kerio-Dienstes (Standard: {DEFAULT_SERVICE})")
parser.add_argument("--service-manager", default="auto",
choices=("auto", "systemd", "initd", "none"),
help="Wie der Dienst gestoppt wird")
parser.add_argument("--service-wait", type=int, default=90,
help="Sekunden Wartezeit aufs Stoppen (Standard: 90)")
action = parser.add_mutually_exclusive_group()
action.add_argument("--status", action="store_true",
help="Nur den Zustand anzeigen (Standard)")
action.add_argument("--skip-wizard", action="store_true",
help="ConfigWizardDone auf 1 setzen")
action.add_argument("--restore", action="store_true",
help="Konfiguration aus dem Backup zuruecksetzen")
parser.add_argument("--dry-run", action="store_true",
help="Nur anzeigen, was passieren wuerde")
parser.add_argument("--yes", action="store_true",
help="Rueckfrage ueberspringen")
args = parser.parse_args(argv)
if (args.skip_wizard or args.restore) and not args.dry_run and os.geteuid() != 0:
parser.error(
"Zum Aendern der Konfiguration werden root-Rechte benoetigt. "
"Mit sudo starten (oder --dry-run zum Testen)."
)
return args
def main(argv: list[str]) -> int:
args = parse_args(argv)
install_dir = Path(args.install_dir)
config_path = install_dir / "mailserver.cfg"
service = ServiceController(
name=args.service_name,
mode=args.service_manager,
wait_seconds=args.service_wait,
dry_run=args.dry_run,
)
try:
if args.restore:
return restore(config_path, service, args)
config = ConfigFile(config_path)
print_status(config, install_dir, service)
if args.skip_wizard:
return skip_wizard(config, service, args)
return 0
except AbortedError as exc:
print(f"\nFEHLER: {exc}", file=sys.stderr)
return 2
except PermissionError as exc:
print(f"\nFEHLER: Keine Berechtigung: {exc}\nMit sudo starten.", file=sys.stderr)
return 2
except KeyboardInterrupt:
print("\nAbgebrochen.", file=sys.stderr)
return 130
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))