Files
kerio-connect-rename-user/kerio_rename_user.py
T
duffyduckandClaude Opus 5 b9cbf65fac Klarere Meldung, wenn das Zielpostfach abgeloest wird
Die alte Formulierung sprach vom Zusammenfuehren, das gar nicht zur
Debatte steht, und liess offen, was mit dem bestehenden Postfach
passiert. Sie sagt jetzt direkt, wohin es umbenannt wird und dass nichts
geloescht wird.

In der README steht dazu, warum nicht zusammengefuehrt wird: Kerio fuehrt
eigene Indexdateien, die beim Hineinkopieren fremder Mails nicht mehr
passen wuerden, waehrend das Verschieben eines vollstaendigen
Verzeichnisses sie mitnimmt.

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

693 lines
27 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Benennt einen Benutzer in Kerio Connect vollstaendig um:
1. den Login-Namen in users.cfg (und optional den angezeigten Namen)
2. das zugehoerige Mailbox-Verzeichnis im Store auf der Platte
Warum nicht ueber die Administration API: Kerio nimmt loginName in Users.set
zwar entgegen, ignoriert das Feld aber stillschweigend - kein Fehler, keine
Wirkung (geprueft gegen Kerio Connect 10.0.9). Der Login-Name laesst sich nur
in users.cfg aendern. Deshalb kommt dieses Script ohne Admin-Zugangsdaten aus.
Kerio legt die Mailbox unter <store>/mail/<domain>/<loginname>/ ab. Ohne
Schritt 2 findet der Benutzer nach der Umbenennung eine leere Mailbox vor,
waehrend die alten Mails unter dem alten Verzeichnisnamen liegenbleiben.
Beide Aenderungen passieren in EINEM Stopp-Fenster des Dienstes. Das ist keine
Bequemlichkeit: laeuft Kerio zwischendurch mit nur einer der beiden Haelften,
legt es die fehlende Mailbox sofort neu an und man hat ein verwaistes
Verzeichnis mehr.
Das Script muss LOKAL AUF DEM KERIO-SERVER ALS ROOT laufen.
Beispiele:
./kerio_rename_user.py --list # Benutzer anzeigen
sudo ./kerio_rename_user.py \\
--domain firma.de \\
--old-login anna.mueller \\
--new-login anna.schmidt \\
--full-name "Anna Schmidt" \\
--dry-run
Mit --adopt laesst sich ein verwaistes Mailbox-Verzeichnis einem bereits
angelegten Benutzer zuordnen - fuer den Fall, dass der alte Benutzer schon
geloescht und der neue von Hand neu angelegt wurde. users.cfg bleibt dabei
unangetastet, es wandert nur das Verzeichnis:
sudo ./kerio_rename_user.py --adopt \\
--domain firma.de \\
--old-login anna.mueller \\
--new-login anna.schmidt
Ohne --dry-run wird vor dem Schreiben nachgefragt (ausser mit --yes).
"""
from __future__ import annotations
import argparse
import os
import re
import shutil
import sys
import time
from pathlib import Path
from kerio_common import (
DEFAULT_INSTALL_DIR,
DEFAULT_SERVICE,
DEFAULT_STORE_DIR,
AbortedError,
ServiceController,
)
from kerio_users_config import UsersConfig, validate_login
# Store-Unterbaeume, in denen pro Benutzer ein Verzeichnis liegen kann.
# Alle folgen dem Schema <store>/<subtree>/<domain>/<loginname>/
STORE_SUBTREES = ("mail", "archive")
BACKUP_SUFFIX = ".bak-rename-user"
# ==========================================================================
# Store-Verzeichnisse
# ==========================================================================
def find_dir_ci(parent: Path, name: str) -> Path | None:
"""Sucht ein Unterverzeichnis - erst exakt, dann ohne Ruecksicht auf Gross/Klein.
Kerio legt die Verzeichnisse meist klein geschrieben an, auch wenn der
Login-Name gemischt geschrieben ist.
"""
direct = parent / name
if direct.is_dir():
return direct
if not parent.is_dir():
return None
wanted = name.lower()
for child in parent.iterdir():
if child.is_dir() and child.name.lower() == wanted:
return child
return None
def plan_store_renames(store_dir: Path, domain_name: str, old_login: str,
new_login: str) -> list[tuple[Path, Path]]:
"""Ermittelt alle zu verschiebenden Verzeichnisse als (quelle, ziel)-Paare."""
if not store_dir.is_dir():
raise AbortedError(
f"Store-Verzeichnis '{store_dir}' existiert nicht. "
"Richtigen Pfad mit --store-dir angeben."
)
renames: list[tuple[Path, Path]] = []
for subtree in STORE_SUBTREES:
domain_dir = find_dir_ci(store_dir / subtree, domain_name)
if domain_dir is None:
continue
source = find_dir_ci(domain_dir, old_login)
if source is None:
continue
# Schreibweise des vorhandenen Verzeichnisses uebernehmen.
target_name = new_login.lower() if source.name.islower() else new_login
renames.append((source, domain_dir / target_name))
return renames
def rewrite_personal_vcard(mailbox: Path, old_address: str, new_address: str,
new_fullname: str | None) -> bool:
"""Schreibt die persoenliche Kontaktkarte auf den neuen Benutzer um.
<mailbox>/.personal ist eine vCard im Mail-Format und traegt Adresse und
Namen des Benutzers, dem die Mailbox vorher gehoerte. Sie einfach zu
entfernen geht nicht: Kerio legt sie nicht neu an - weder beim Start noch
bei einer Aenderung am Benutzer (geprueft gegen 10.0.9). Der Benutzer
stuende dann ohne eigene Kontaktkarte da.
Gearbeitet wird auf Bytes, damit die CRLF-Zeilenenden erhalten bleiben.
"""
path = mailbox / ".personal"
if not path.is_file():
return False
try:
data = path.read_bytes()
except OSError:
return False
updated = data.replace(old_address.encode("utf-8"), new_address.encode("utf-8"))
if new_fullname:
name = new_fullname.encode("utf-8")
# FN: und Subject: tragen den angezeigten Namen. [^\r\n]* laesst die
# Zeilenenden in Ruhe.
updated = re.sub(rb"(?m)^(FN:)[^\r\n]*", lambda m: m.group(1) + name, updated)
updated = re.sub(rb"(?mi)^(Subject:[ \t]*)[^\r\n]*",
lambda m: m.group(1) + name, updated)
if updated == data:
return False
temp = mailbox / ".personal.tmp-rename"
temp.write_bytes(updated)
shutil.copystat(path, temp)
os.replace(temp, path)
return True
def count_mails(path: Path) -> int:
"""Zaehlt die Mail-Dateien unterhalb eines Verzeichnisses."""
try:
return sum(1 for _ in path.rglob("*.eml"))
except OSError:
return 0
def plan_store_adoption(store_dir: Path, domain_name: str, source_name: str,
target_login: str, stamp: str,
force: bool) -> list[tuple[Path, Path]]:
"""Plant die Uebernahme eines verwaisten Verzeichnisses fuer einen Benutzer.
Liegt am Ziel schon ein Verzeichnis, wird es beiseitegeschoben statt
geloescht - erst danach rueckt die Quelle nach. Beides sind Umbenennungen,
deshalb reicht dieselbe (quelle, ziel)-Liste wie beim normalen Rename.
"""
if not store_dir.is_dir():
raise AbortedError(
f"Store-Verzeichnis '{store_dir}' existiert nicht. "
"Richtigen Pfad mit --store-dir angeben."
)
moves: list[tuple[Path, Path]] = []
found_any = False
for subtree in STORE_SUBTREES:
domain_dir = find_dir_ci(store_dir / subtree, domain_name)
if domain_dir is None:
continue
source = find_dir_ci(domain_dir, source_name)
if source is None:
continue
found_any = True
target_name = target_login.lower() if source.name.islower() else target_login
target = find_dir_ci(domain_dir, target_login)
if target is not None and target.resolve() != source.resolve():
existing_mails = count_mails(target)
if existing_mails and not force:
raise AbortedError(
f"Das Postfach von '{target_login}' enthaelt bereits "
f"{existing_mails} Mail(s) und wuerde abgeloest. Mit "
"--force durchfuehren - es wird dann nach "
f"'{target.name}.abgeloest-{stamp}' umbenannt und bleibt "
"vollstaendig erhalten, geloescht wird nichts."
)
moves.append((target, domain_dir / f"{target.name}.abgeloest-{stamp}"))
moves.append((source, domain_dir / target_name))
if not found_any:
raise AbortedError(
f"Kein Verzeichnis '{source_name}' unter {store_dir} gefunden "
f"(gesucht in: {', '.join(STORE_SUBTREES)}/{domain_name}/)."
)
return moves
def check_rename_targets(renames: list[tuple[Path, Path]]) -> None:
"""Prueft die Verschiebungen als Folge, nicht einzeln.
Bei der Uebernahme eines verwaisten Verzeichnisses raeumt der erste
Schritt das Ziel, das der zweite dann belegt. Eine Pruefung, die jedes
Paar isoliert betrachtet, wuerde das faelschlich als Konflikt melden -
deshalb wird mitgefuehrt, welche Pfade unterwegs frei bzw. belegt werden.
"""
freed: set[str] = set()
used: set[str] = set()
for source, target in renames:
if str(target) in used:
raise AbortedError(
f"Zwei Verschiebungen zielen auf '{target}' - das kann nicht "
"aufgehen."
)
if target.exists() and target.resolve() != source.resolve() \
and str(target) not in freed:
raise AbortedError(
f"Zielverzeichnis '{target}' existiert bereits. "
"Bitte manuell pruefen und wegraeumen."
)
try:
same_fs = source.stat().st_dev == target.parent.stat().st_dev
except OSError as exc:
raise AbortedError(f"'{source}' ist nicht mehr lesbar: {exc}") from exc
if not same_fs:
raise AbortedError(
f"'{source}' und '{target.parent}' liegen auf verschiedenen "
"Dateisystemen - das wuerde ein Kopieren statt Verschieben bedeuten."
)
freed.add(str(source))
used.add(str(target))
def apply_store_renames(renames: list[tuple[Path, Path]]) -> list[tuple[Path, Path]]:
"""Fuehrt die Verschiebungen aus. Gibt zurueck, was tatsaechlich getan wurde."""
done: list[tuple[Path, Path]] = []
for source, target in renames:
try:
os.rename(source, target)
except OSError as exc:
revert_store_renames(done)
raise AbortedError(
f"Verschieben von '{source}' nach '{target}' fehlgeschlagen: {exc}"
) from exc
done.append((source, target))
print(f" Verschoben: {source} -> {target}")
return done
def revert_store_renames(done: list[tuple[Path, Path]]) -> None:
for source, target in reversed(done):
try:
os.rename(target, source)
print(f" Zurueckgenommen: {target} -> {source}", file=sys.stderr)
except OSError as exc:
print(f" WARNUNG: Rueckgaengigmachen von '{target}' fehlgeschlagen: {exc}",
file=sys.stderr)
# ==========================================================================
# Ablauf
# ==========================================================================
def list_users(config: UsersConfig, domain: str | None) -> int:
domains = config.domains()
if not domains:
print("Keine Benutzer in users.cfg gefunden.")
return 1
for name in domains:
if domain and name.lower() != domain.lower():
continue
print(f"\nDomain: {name}")
for user in config.users(name):
flags = [] if user["enabled"] else ["deaktiviert"]
if not config.is_internal(user["loginName"], name):
flags.append("Verzeichnisdienst")
suffix = f" [{', '.join(flags)}]" if flags else ""
print(f" {user['loginName']:<28} {user['fullName']}{suffix}")
return 0
def adopt_store(args: argparse.Namespace, config: UsersConfig,
service: ServiceController) -> int:
"""Haengt ein verwaistes Mailbox-Verzeichnis an einen bestehenden Benutzer.
Fuer den Fall, dass der alte Benutzer schon geloescht und der neue bereits
angelegt wurde - dann ist nur noch das Verzeichnis uebrig. users.cfg wird
dabei nicht angefasst, der Zielbenutzer existiert ja bereits.
"""
store_dir = Path(args.store_dir)
stamp = time.strftime("%Y%m%d-%H%M%S")
domains = config.domains()
domain = next((d for d in domains if d.lower() == args.domain.lower()), None)
if domain is None:
raise AbortedError(
f"Domain '{args.domain}' kommt in users.cfg nicht vor. "
f"Vorhanden: {', '.join(domains) or '(keine)'}"
)
# Der Zielbenutzer muss es geben - sonst laeuft die Mailbox ins Leere.
target_user = config.find_user(args.new_login, domain)
if target_user is None:
raise AbortedError(
f"Zielbenutzer '{args.new_login}' existiert nicht in Domain "
f"'{domain}'. Erst in der Administrationskonsole anlegen."
)
# Gibt es den alten Benutzer noch, ist die normale Umbenennung richtig.
if config.find_user(args.old_login, domain) is not None:
raise AbortedError(
f"Benutzer '{args.old_login}' existiert noch. Fuer diesen Fall "
"ist die normale Umbenennung gedacht (ohne --adopt), die auch "
"users.cfg mitzieht."
)
moves = plan_store_adoption(store_dir, domain, args.old_login,
args.new_login, stamp, args.force)
check_rename_targets(moves)
service.preflight()
source_mails = sum(count_mails(s) for s, _ in moves
if s.name.lower() == args.old_login.lower())
print("\nGeplante Uebernahme")
print("-------------------")
print(f" Domain : {domain}")
print(f" Zielbenutzer : {target_user['loginName']} "
f"({target_user['fullName']})")
print(f" Guid : {target_user['guid']}")
print(f" Mails in der Quelle: {source_mails}")
print(f" users.cfg : wird NICHT geaendert")
print(f" Dienst : {service.describe()}")
for source, target in moves:
marker = "beiseite" if ".abgeloest-" in target.name else "uebernehmen"
print(f" {marker:<12}: {source}")
print(f" -> {target}")
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("Uebernahme jetzt durchfuehren? [j/N] ").strip().lower() \
not in ("j", "ja", "y", "yes"):
print("Abgebrochen.")
return 1
print()
was_running = service.is_running()
service.stop()
done: list[tuple[Path, Path]] = []
try:
check_rename_targets(moves)
done = apply_store_renames(moves)
# Die uebernommene Mailbox traegt noch die Identitaet des alten
# Benutzers. stats.usr legt Kerio selbst neu an, die Kontaktkarte
# nicht - die muss umgeschrieben werden.
for _, target in done:
if ".abgeloest-" in target.name:
continue
if rewrite_personal_vcard(
target,
f"{args.old_login}@{domain}",
f"{args.new_login}@{domain}",
target_user["fullName"]):
print(f" Kontaktkarte auf {target_user['fullName']} umgeschrieben")
stats = target / "stats.usr"
if stats.is_file():
stats.rename(target / f"stats.usr.alt-{stamp}")
print(" Beiseite: stats.usr (Kerio legt sie neu an)")
except (AbortedError, OSError) as exc:
print(f"\nFEHLER: {exc}", file=sys.stderr)
revert_store_renames(done)
if was_running:
service.start()
return 2
if was_running:
service.start()
adopted = [t for _, t in done if ".abgeloest-" not in t.name]
print(f"\nFertig. '{args.new_login}' hat jetzt {source_mails} Mail(s) "
f"unter '{adopted[0] if adopted else '?'}'.")
print("Der Benutzer meldet sich unverändert mit seinem eigenen Kennwort an - "
"das Kennwort des alten Benutzers wird nicht uebernommen.")
beiseite = [t for _, t in done if ".abgeloest-" in t.name]
if beiseite:
print(f"Das vorherige Verzeichnis liegt als '{beiseite[0].name}' daneben.")
return 0
def rename_user(args: argparse.Namespace, config: UsersConfig,
service: ServiceController) -> int:
store_dir = Path(args.store_dir)
# -- Vorpruefungen: nichts wird geschrieben ---------------------------
domains = config.domains()
domain = next((d for d in domains if d.lower() == args.domain.lower()), None)
if domain is None:
raise AbortedError(
f"Domain '{args.domain}' kommt in users.cfg nicht vor. "
f"Vorhanden: {', '.join(domains) or '(keine)'}"
)
user = config.find_user(args.old_login, domain)
if user is None:
raise AbortedError(
f"Benutzer '{args.old_login}' existiert nicht in Domain '{domain}'."
)
old_login = user["loginName"]
new_login = args.new_login or old_login
if new_login != old_login:
validate_login(new_login)
if not config.is_internal(old_login, domain) and not args.force:
raise AbortedError(
"Benutzer stammt aus einem Verzeichnisdienst (LDAP/AD). Der "
"Login-Name muss dort geaendert werden, nicht in Kerio. "
"Mit --force trotzdem versuchen."
)
if new_login.lower() != old_login.lower() and \
config.find_user(new_login, domain) is not None:
raise AbortedError(f"Login-Name '{new_login}' ist bereits vergeben.")
renames: list[tuple[Path, Path]] = []
if new_login != old_login:
renames = plan_store_renames(store_dir, domain, old_login, new_login)
if not renames:
print(f" Hinweis: kein Mailbox-Verzeichnis fuer '{old_login}' gefunden. "
"Es wird nur users.cfg geaendert.")
check_rename_targets(renames)
service.preflight()
needs_name = new_login != old_login
needs_fullname = bool(args.full_name) and args.full_name != user["fullName"]
if not needs_name and not needs_fullname:
print("\nNichts zu tun.")
return 0
# -- Plan ausgeben ----------------------------------------------------
print("\nGeplante Aenderungen")
print("--------------------")
print(f" Domain : {domain}")
print(f" Guid : {user['guid']}")
if needs_name:
print(f" Login-Name : {old_login} -> {new_login}")
print(f" Adresse : {old_login}@{domain} -> {new_login}@{domain}")
else:
print(f" Login-Name : {old_login} (unveraendert)")
if needs_fullname:
print(f" Voller Name : {user['fullName']} -> {args.full_name}")
else:
print(f" Voller Name : {user['fullName']} (unveraendert)")
print(f" users.cfg : {config.path}")
if renames:
print(f" Dienst : {service.describe()}")
for source, target in renames:
print(f" Verzeichnis : {source}")
print(f" -> {target}")
extra = config.other_occurrences(old_login, domain) if needs_name else []
if extra:
print("\n ACHTUNG: Der alte Name kommt auch hier vor und wird NICHT "
"automatisch geaendert:")
for hit in extra:
print(f" - {hit}")
print(" Bitte nach der Umbenennung in der Administrationskonsole pruefen.")
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("Aenderungen jetzt durchfuehren? [j/N] ").strip().lower() \
not in ("j", "ja", "y", "yes"):
print("Abgebrochen.")
return 1
# -- Schreiben: users.cfg und Verzeichnisse in einem Stopp-Fenster ----
print()
was_running = service.is_running()
service.stop()
done: list[tuple[Path, Path]] = []
backup: Path | None = None
try:
# Nach dem Stoppen neu einlesen: Kerio hat users.cfg beim Beenden
# aus dem Speicher zurueckgeschrieben, unsere Kopie ist veraltet.
fresh = UsersConfig(config.path)
backup = fresh.backup(BACKUP_SUFFIX)
print(f" Backup: {backup}")
changed = fresh.rename_user(
old_login, new_login, domain,
args.full_name if needs_fullname else None)
if changed == 0:
raise AbortedError(
"Kein passender Eintrag in users.cfg gefunden - nichts geaendert."
)
fresh.save()
print(f" users.cfg: {changed} Eintraege angepasst "
f"(Listen: User, UserAdditionalData)")
if renames:
# Zielpruefung wiederholen: der Zustand kann sich seit der
# Vorpruefung geaendert haben.
check_rename_targets(renames)
done = apply_store_renames(renames)
# Die Kontaktkarte traegt noch die alte Adresse - Kerio zieht dort
# nur den Namen nach, nicht die Adresse.
display_name = args.full_name if needs_fullname else user["fullName"]
for _, target in done:
if rewrite_personal_vcard(target, f"{old_login}@{domain}",
f"{new_login}@{domain}", display_name):
print(" Kontaktkarte auf die neue Adresse umgeschrieben")
except (AbortedError, OSError) as exc:
print(f"\nFEHLER: {exc}", file=sys.stderr)
revert_store_renames(done)
if backup and backup.is_file():
try:
shutil.copy2(backup, config.path)
print(f" users.cfg aus {backup} zurueckgesetzt.", file=sys.stderr)
except OSError as restore_exc:
print(f" WARNUNG: Zuruecksetzen von users.cfg fehlgeschlagen: "
f"{restore_exc}", file=sys.stderr)
if was_running:
service.start()
return 2
if was_running:
service.start()
# -- Gegenlesen -------------------------------------------------------
check = UsersConfig(config.path)
result = check.find_user(new_login, domain)
if result is None:
print(f"\n WARNUNG: '{new_login}' ist in users.cfg nach dem Start nicht "
"auffindbar. Bitte pruefen.", file=sys.stderr)
return 2
print(f"\nFertig. Benutzer heisst jetzt '{result['loginName']}' "
f"({result['fullName']}).")
if done:
print(f"Mailbox liegt unter '{done[0][1]}'.")
print("Hinweis: Der Benutzer muss sich in allen Mail-Clients mit dem "
"neuen Login neu anmelden.")
if needs_name:
print("Die alte Adresse nimmt keine Mail mehr an. Wer das braucht, legt "
"in der Administrationskonsole einen Alias an.")
return 0
# ==========================================================================
# CLI
# ==========================================================================
def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Benennt einen Kerio-Connect-Benutzer inklusive "
"Store-Verzeichnis um.",
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("--store-dir", default=DEFAULT_STORE_DIR,
help=f"Kerio-Store-Verzeichnis (Standard: {DEFAULT_STORE_DIR})")
parser.add_argument("--domain",
help="Mail-Domain des Benutzers, z. B. firma.de")
parser.add_argument("--old-login",
help="Bisheriger Login-Name (ohne @domain)")
parser.add_argument("--new-login",
help="Neuer Login-Name (ohne @domain)")
parser.add_argument("--full-name",
help='Neuer angezeigter Name, z. B. "Anna Schmidt"')
parser.add_argument("--list", action="store_true",
help="Benutzer aus users.cfg auflisten und beenden")
parser.add_argument("--adopt", action="store_true",
help="Verwaistes Mailbox-Verzeichnis (--old-login) einem "
"bereits angelegten Benutzer (--new-login) zuordnen. "
"users.cfg bleibt unangetastet.")
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. 'none' fasst ihn nicht "
"an - dann muss Kerio selbst gestoppt sein.")
parser.add_argument("--service-wait", type=int, default=90,
help="Sekunden Wartezeit aufs Stoppen (Standard: 90)")
parser.add_argument("--dry-run", action="store_true",
help="Nur anzeigen, was passieren wuerde")
parser.add_argument("--yes", action="store_true",
help="Rueckfrage ueberspringen")
parser.add_argument("--force", action="store_true",
help="Auch bei Benutzern aus einem Verzeichnisdienst")
args = parser.parse_args(argv)
if not args.list:
needed = ("domain", "old_login", "new_login") if args.adopt \
else ("domain", "old_login")
missing = [n for n in needed if not getattr(args, n)]
if missing:
parser.error("Benoetigt: " + ", ".join("--" + m.replace("_", "-")
for m in missing))
if args.adopt:
if args.full_name:
parser.error("--full-name ergibt mit --adopt keinen Sinn: der "
"Zielbenutzer existiert bereits und behaelt seine "
"Angaben.")
elif not args.new_login and not args.full_name:
parser.error("Mindestens --new-login oder --full-name angeben.")
if not args.dry_run and os.geteuid() != 0:
parser.error(
"Zum Aendern von users.cfg und zum Verschieben des "
"Store-Verzeichnisses werden root-Rechte benoetigt. "
"Mit sudo starten (oder --dry-run zum Testen)."
)
return args
def main(argv: list[str]) -> int:
args = parse_args(argv)
config_path = Path(args.install_dir) / "users.cfg"
service = ServiceController(
name=args.service_name,
mode=args.service_manager,
wait_seconds=args.service_wait,
dry_run=args.dry_run,
)
try:
config = UsersConfig(config_path)
if args.list:
return list_users(config, args.domain)
if args.adopt:
return adopt_store(args, config, service)
return rename_user(args, config, service)
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:]))