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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
485a681a0a
commit
b12041fc81
@@ -1,7 +1,19 @@
|
||||
# kerio-connect-rename-user
|
||||
|
||||
Benennt einen Benutzer in Kerio Connect vollständig um — Login-Name in der
|
||||
Serverkonfiguration **und** das zugehörige Mailbox-Verzeichnis im Store.
|
||||
Werkzeuge für Kerio Connect auf der Kommandozeile.
|
||||
|
||||
| Datei | Zweck |
|
||||
| --- | --- |
|
||||
| [`kerio_rename_user.py`](kerio_rename_user.py) | Benutzer umbenennen — Login-Name **und** Mailbox-Verzeichnis im Store |
|
||||
| [`kerio_trial_setup.py`](kerio_trial_setup.py) | Am Erstkonfigurationsassistenten vorbei, wenn dessen Lizenzschritt klemmt |
|
||||
| [`kerio_common.py`](kerio_common.py) | Gemeinsame Bausteine: API-Client, Dienststeuerung |
|
||||
|
||||
Beide Scripts laufen **lokal auf dem Kerio-Server als root**, brauchen nur die
|
||||
Python-Standardbibliothek und bieten `--dry-run`.
|
||||
|
||||
---
|
||||
|
||||
# Benutzer umbenennen
|
||||
|
||||
## Warum
|
||||
|
||||
@@ -110,8 +122,62 @@ selbst gestoppt sein, bevor das Script läuft.
|
||||
- Weitere Adressen am Benutzer (`emailAddresses`) bleiben unverändert und werden
|
||||
im Plan zur Kontrolle mit angezeigt.
|
||||
|
||||
## API-Referenz
|
||||
---
|
||||
|
||||
# Erstkonfigurationsassistent überspringen
|
||||
|
||||
## Warum
|
||||
|
||||
Der Assistent verlangt im Schritt „Registrierte Testaktivierung" eine
|
||||
Testlizenznummer samt CAPTCHA. Der dokumentierte Link „Im unregistrierten Modus
|
||||
aktivieren" fehlt je nach Version oder ist im Fenster abgeschnitten, und die
|
||||
verlinkte Registrierungsseite ist seit der Übernahme durch GFI teilweise tot —
|
||||
man kommt im Assistenten nicht weiter.
|
||||
|
||||
Kerio kennt dafür den Schalter `ConfigWizardDone` in `mailserver.cfg`. Steht er
|
||||
auf `1`, öffnet die Administrationskonsole den Assistenten nicht mehr und der
|
||||
Server läuft 30 Tage ab Installation im unregistrierten Modus.
|
||||
|
||||
Im unregistrierten Modus fehlen Antivirus-Updates, ActiveSync, Greylisting und
|
||||
Hersteller-Support; nach 30 Tagen stoppt die Engine. **Benutzerverwaltung,
|
||||
Administrationskonsole und Admin-API sind nicht eingeschränkt** — für einen
|
||||
Testserver also unproblematisch.
|
||||
|
||||
## Benutzung
|
||||
|
||||
```bash
|
||||
./kerio_trial_setup.py --status # Zustand anzeigen (kein root nötig)
|
||||
sudo ./kerio_trial_setup.py --skip-wizard # Assistenten überspringen
|
||||
sudo ./kerio_trial_setup.py --restore # Backup zurückspielen
|
||||
```
|
||||
|
||||
`--status` zeigt Dienstzustand, `ConfigWizardDone`, `TrialID`, vorhandene
|
||||
Lizenzdateien und eine Schätzung, wann der Testzeitraum abläuft.
|
||||
|
||||
## Der springende Punkt
|
||||
|
||||
`mailserver.cfg` darf **nur bei gestopptem Dienst** bearbeitet werden. Kerio
|
||||
schreibt seine Konfiguration beim Beenden aus dem Speicher zurück und würde eine
|
||||
Änderung am laufenden Dienst wieder überschreiben. Das Script stoppt deshalb den
|
||||
Dienst, liest die Datei **danach neu ein** (die Version von vorher ist zu diesem
|
||||
Zeitpunkt bereits veraltet), schreibt atomar über eine temporäre Datei im selben
|
||||
Verzeichnis, startet den Dienst und liest den Wert zur Kontrolle noch einmal.
|
||||
|
||||
Vor jeder Änderung wird ein Backup als `mailserver.cfg.bak-trial-setup` angelegt.
|
||||
|
||||
## Falls du doch eine registrierte Trial willst
|
||||
|
||||
Geht aus der laufenden Instanz heraus, ohne Installer: **Dashboard → „Become a
|
||||
registered trial user"**. Braucht ausgehendes HTTPS auf Port 443 zum
|
||||
GFI-Registrierungsserver.
|
||||
|
||||
---
|
||||
|
||||
## API- und Doku-Referenz
|
||||
|
||||
- [Administration API for Kerio Connect — Users](https://manuals.gfi.com/en/kerio/api/connect/admin/reference/interfacekerio_1_1jsonapi_1_1admin_1_1_users.html)
|
||||
- [Sample communication (Session.login, X-Token)](https://manuals.gfi.com/en/kerio/api/connect/admin/reference/sample_communication.html)
|
||||
- [User struct](https://manuals.gfi.com/en/kerio/api/connect/admin/reference/structkerio_1_1jsonapi_1_1admin_1_1_user.html)
|
||||
- [Registering Kerio Connect (unregistered mode)](https://manuals.gfi.com/en/kerio/connect/content/registration-and-licenses/registering-kerio-connect-1134.html)
|
||||
- [Initial Wizard Settings Are Not Saving (ConfigWizardDone)](https://support.kerioconnect.gfi.com/article/114858-initial-wizard-settings-are-not-saving-in-kerio-connect)
|
||||
- [Modifying the mailserver.cfg](https://support.kerioconnect.gfi.com/en-us/article/114788-modifying-the-mailserver-cfg)
|
||||
|
||||
+301
@@ -0,0 +1,301 @@
|
||||
"""
|
||||
Gemeinsame Bausteine fuer die Kerio-Connect-Scripts:
|
||||
|
||||
* KerioAdminApi - JSON-RPC-Client fuer die Administration API
|
||||
* ServiceController - startet und stoppt den Kerio-Dienst
|
||||
* Hilfsfunktionen zum Auffinden von Domains und Benutzern
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import http.cookiejar
|
||||
import json
|
||||
import shutil
|
||||
import ssl
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
APP_INFO = {
|
||||
"name": "kerio-connect-user-rename",
|
||||
"vendor": "inhouse",
|
||||
"version": "2.0",
|
||||
}
|
||||
|
||||
# Seitengroesse fuer die Paginierung von Users.get / Domains.get.
|
||||
PAGE_SIZE = 500
|
||||
|
||||
DEFAULT_INSTALL_DIR = "/opt/kerio/mailserver"
|
||||
DEFAULT_STORE_DIR = "/opt/kerio/mailserver/store"
|
||||
DEFAULT_SERVICE = "kerio-connect"
|
||||
|
||||
|
||||
class KerioError(Exception):
|
||||
"""Fehler, den der Kerio-Server gemeldet hat (oder ein Transportfehler)."""
|
||||
|
||||
|
||||
class AbortedError(Exception):
|
||||
"""Der Benutzer hat abgebrochen oder eine Vorpruefung ist fehlgeschlagen."""
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# Administration API (JSON-RPC)
|
||||
# ==========================================================================
|
||||
|
||||
|
||||
class KerioAdminApi:
|
||||
"""Duenner JSON-RPC-Client fuer die Kerio Connect Admin-API.
|
||||
|
||||
Die API will nach dem Login drei Dinge sehen: das Session-Cookie, den
|
||||
X-Token-Header und denselben Token nochmal im JSON-Payload.
|
||||
"""
|
||||
|
||||
def __init__(self, host: str, port: int = 4040, verify_tls: bool = True,
|
||||
timeout: int = 30) -> None:
|
||||
self.url = f"https://{host}:{port}/admin/api/jsonrpc/"
|
||||
self.timeout = timeout
|
||||
self.token: str | None = None
|
||||
self._request_id = 0
|
||||
|
||||
context = ssl.create_default_context()
|
||||
if not verify_tls:
|
||||
context.check_hostname = False
|
||||
context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
self._opener = urllib.request.build_opener(
|
||||
urllib.request.HTTPSHandler(context=context),
|
||||
urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()),
|
||||
)
|
||||
|
||||
# -- Transport ---------------------------------------------------------
|
||||
|
||||
def call(self, method: str, params: dict | None = None) -> dict:
|
||||
self._request_id += 1
|
||||
payload: dict = {"jsonrpc": "2.0", "id": self._request_id, "method": method}
|
||||
if params is not None:
|
||||
payload["params"] = params
|
||||
if self.token:
|
||||
payload["token"] = self.token
|
||||
|
||||
headers = {"Content-Type": "application/json-rpc; charset=UTF-8"}
|
||||
if self.token:
|
||||
headers["X-Token"] = self.token
|
||||
|
||||
request = urllib.request.Request(
|
||||
self.url,
|
||||
data=json.dumps(payload).encode("utf-8"),
|
||||
headers=headers,
|
||||
method="POST",
|
||||
)
|
||||
|
||||
try:
|
||||
with self._opener.open(request, timeout=self.timeout) as response:
|
||||
body = json.loads(response.read().decode("utf-8"))
|
||||
except urllib.error.HTTPError as exc:
|
||||
raise KerioError(f"HTTP {exc.code} bei {method}: {exc.read()[:500]!r}") from exc
|
||||
except urllib.error.URLError as exc:
|
||||
raise KerioError(f"Verbindung zu {self.url} fehlgeschlagen: {exc.reason}") from exc
|
||||
except ssl.SSLError as exc:
|
||||
raise KerioError(
|
||||
f"TLS-Fehler: {exc}. Bei selbstsigniertem Zertifikat --insecure verwenden."
|
||||
) from exc
|
||||
|
||||
if "error" in body:
|
||||
error = body["error"]
|
||||
raise KerioError(
|
||||
f"{method}: {error.get('message', 'unbekannter Fehler')} "
|
||||
f"(Code {error.get('code')})"
|
||||
)
|
||||
|
||||
result = body.get("result", {})
|
||||
_raise_on_error_list(method, result)
|
||||
return result
|
||||
|
||||
def call_paged(self, method: str, params: dict, list_key: str = "list") -> list[dict]:
|
||||
"""Ruft eine get-Methode so oft auf, bis alle totalItems eingesammelt sind."""
|
||||
items: list[dict] = []
|
||||
while True:
|
||||
query = dict(params.get("query") or {})
|
||||
query.update({"start": len(items), "limit": PAGE_SIZE})
|
||||
query.setdefault("fields", [])
|
||||
query.setdefault("conditions", [])
|
||||
query.setdefault("combining", "Or")
|
||||
|
||||
result = self.call(method, {**params, "query": query})
|
||||
page = result.get(list_key) or []
|
||||
items.extend(page)
|
||||
|
||||
total = result.get("totalItems", len(items))
|
||||
if not page or len(items) >= total:
|
||||
return items
|
||||
|
||||
# -- Session -----------------------------------------------------------
|
||||
|
||||
def login(self, username: str, password: str) -> None:
|
||||
result = self.call("Session.login", {
|
||||
"userName": username,
|
||||
"password": password,
|
||||
"application": APP_INFO,
|
||||
})
|
||||
self.token = result.get("token")
|
||||
if not self.token:
|
||||
raise KerioError("Login lieferte keinen Token zurueck.")
|
||||
|
||||
def logout(self) -> None:
|
||||
if self.token:
|
||||
try:
|
||||
self.call("Session.logout")
|
||||
except KerioError:
|
||||
pass # Ausloggen ist best effort
|
||||
finally:
|
||||
self.token = None
|
||||
|
||||
def __enter__(self) -> "KerioAdminApi":
|
||||
return self
|
||||
|
||||
def __exit__(self, *_exc_info) -> None:
|
||||
self.logout()
|
||||
|
||||
|
||||
def _raise_on_error_list(method: str, result: dict) -> None:
|
||||
"""Kerio liefert Teilfehler nicht als JSON-RPC-error, sondern als 'errors'-Liste."""
|
||||
errors = result.get("errors")
|
||||
if not errors:
|
||||
return
|
||||
details = "; ".join(
|
||||
f"{e.get('message', '?')} (Code {e.get('code')}, Feld {e.get('inputIndex')})"
|
||||
for e in errors
|
||||
)
|
||||
raise KerioError(f"{method} meldet Fehler: {details}")
|
||||
|
||||
|
||||
def find_domain(api: KerioAdminApi, domain_name: str) -> dict:
|
||||
domains = api.call_paged("Domains.get", {})
|
||||
wanted = domain_name.strip().lower()
|
||||
for domain in domains:
|
||||
if domain.get("name", "").lower() == wanted:
|
||||
return domain
|
||||
known = ", ".join(sorted(d.get("name", "?") for d in domains)) or "(keine)"
|
||||
raise KerioError(f"Domain '{domain_name}' nicht gefunden. Vorhanden: {known}")
|
||||
|
||||
|
||||
def find_user(api: KerioAdminApi, domain_id: str, login_name: str) -> dict | None:
|
||||
users = api.call_paged("Users.get", {"domainId": domain_id})
|
||||
wanted = login_name.strip().lower()
|
||||
for user in users:
|
||||
if user.get("loginName", "").lower() == wanted:
|
||||
return user
|
||||
return None
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# Dienststeuerung
|
||||
# ==========================================================================
|
||||
|
||||
|
||||
class ServiceController:
|
||||
"""Startet und stoppt den Kerio-Connect-Dienst.
|
||||
|
||||
Modus 'none' fasst den Dienst nicht an - dann muss man selbst dafuer
|
||||
sorgen, dass Kerio waehrend der Aenderung steht.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str = DEFAULT_SERVICE, mode: str = "auto",
|
||||
wait_seconds: int = 90, dry_run: bool = False) -> None:
|
||||
self.name = name
|
||||
self.wait_seconds = wait_seconds
|
||||
self.dry_run = dry_run
|
||||
self.mode = self._resolve_mode(mode)
|
||||
|
||||
@staticmethod
|
||||
def _resolve_mode(mode: str) -> str:
|
||||
if mode != "auto":
|
||||
return mode
|
||||
if shutil.which("systemctl"):
|
||||
return "systemd"
|
||||
if Path("/etc/init.d", DEFAULT_SERVICE).exists():
|
||||
return "initd"
|
||||
return "none"
|
||||
|
||||
def _run(self, *command: str) -> subprocess.CompletedProcess:
|
||||
return subprocess.run(command, capture_output=True, text=True, timeout=120)
|
||||
|
||||
def describe(self) -> str:
|
||||
if self.mode == "none":
|
||||
return "Dienst wird nicht angefasst (--service-manager none)"
|
||||
return f"{self.mode}: {self.name}"
|
||||
|
||||
def is_running(self) -> bool | None:
|
||||
"""True/False, oder None wenn der Status nicht ermittelbar ist."""
|
||||
if self.mode == "systemd":
|
||||
return self._run("systemctl", "is-active", "--quiet", self.name).returncode == 0
|
||||
if self.mode == "initd":
|
||||
return self._run(f"/etc/init.d/{self.name}", "status").returncode == 0
|
||||
return None
|
||||
|
||||
def preflight(self) -> None:
|
||||
"""Prueft vor allen Aenderungen, ob der Dienst ueberhaupt steuerbar ist."""
|
||||
if self.mode == "none":
|
||||
return
|
||||
if self.mode == "systemd":
|
||||
if self._run("systemctl", "cat", self.name).returncode != 0:
|
||||
raise AbortedError(
|
||||
f"systemd kennt die Unit '{self.name}' nicht. "
|
||||
"Richtigen Namen mit --service-name angeben oder "
|
||||
"--service-manager none verwenden."
|
||||
)
|
||||
elif self.mode == "initd":
|
||||
if not Path(f"/etc/init.d/{self.name}").exists():
|
||||
raise AbortedError(f"/etc/init.d/{self.name} existiert nicht.")
|
||||
|
||||
def stop(self) -> None:
|
||||
if self.mode == "none":
|
||||
print(" Dienst: uebersprungen (--service-manager none)")
|
||||
return
|
||||
if self.dry_run:
|
||||
print(f" [dry-run] wuerde Dienst '{self.name}' stoppen")
|
||||
return
|
||||
|
||||
print(f" Stoppe Dienst '{self.name}' ...")
|
||||
if self.mode == "systemd":
|
||||
result = self._run("systemctl", "stop", self.name)
|
||||
else:
|
||||
result = self._run(f"/etc/init.d/{self.name}", "stop")
|
||||
if result.returncode != 0:
|
||||
raise AbortedError(
|
||||
f"Dienst konnte nicht gestoppt werden: "
|
||||
f"{result.stderr.strip() or result.stdout.strip()}"
|
||||
)
|
||||
|
||||
# Warten, bis der Dienst wirklich unten ist - sonst schreibt Kerio
|
||||
# noch in Dateien, die wir gerade anfassen.
|
||||
deadline = time.monotonic() + self.wait_seconds
|
||||
while time.monotonic() < deadline:
|
||||
if self.is_running() is False:
|
||||
print(" Dienst gestoppt.")
|
||||
return
|
||||
time.sleep(1)
|
||||
raise AbortedError(
|
||||
f"Dienst '{self.name}' laeuft nach {self.wait_seconds}s immer noch. Abbruch."
|
||||
)
|
||||
|
||||
def start(self) -> None:
|
||||
if self.mode == "none":
|
||||
return
|
||||
if self.dry_run:
|
||||
print(f" [dry-run] wuerde Dienst '{self.name}' starten")
|
||||
return
|
||||
|
||||
print(f" Starte Dienst '{self.name}' ...")
|
||||
if self.mode == "systemd":
|
||||
result = self._run("systemctl", "start", self.name)
|
||||
else:
|
||||
result = self._run(f"/etc/init.d/{self.name}", "start")
|
||||
if result.returncode != 0:
|
||||
print(f" WARNUNG: Start fehlgeschlagen: "
|
||||
f"{result.stderr.strip() or result.stdout.strip()}", file=sys.stderr)
|
||||
else:
|
||||
print(" Dienst gestartet.")
|
||||
+12
-288
@@ -31,306 +31,30 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import getpass
|
||||
import http.cookiejar
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import ssl
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
APP_INFO = {
|
||||
"name": "kerio-connect-user-rename",
|
||||
"vendor": "inhouse",
|
||||
"version": "2.0",
|
||||
}
|
||||
|
||||
# Seitengroesse fuer die Paginierung von Users.get / Domains.get.
|
||||
PAGE_SIZE = 500
|
||||
|
||||
DEFAULT_STORE_DIR = "/opt/kerio/mailserver/store"
|
||||
DEFAULT_SERVICE = "kerio-connect"
|
||||
from kerio_common import (
|
||||
DEFAULT_SERVICE,
|
||||
DEFAULT_STORE_DIR,
|
||||
AbortedError,
|
||||
KerioAdminApi,
|
||||
KerioError,
|
||||
ServiceController,
|
||||
find_domain,
|
||||
find_user,
|
||||
)
|
||||
|
||||
# Store-Unterbaeume, in denen pro Benutzer ein Verzeichnis liegen kann.
|
||||
# Alle folgen dem Schema <store>/<subtree>/<domain>/<loginname>/
|
||||
STORE_SUBTREES = ("mail", "archive")
|
||||
|
||||
|
||||
class KerioError(Exception):
|
||||
"""Fehler, den der Kerio-Server gemeldet hat (oder ein Transportfehler)."""
|
||||
|
||||
|
||||
class AbortedError(Exception):
|
||||
"""Der Benutzer hat abgebrochen oder eine Vorpruefung ist fehlgeschlagen."""
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# Teil 1: Administration API (JSON-RPC)
|
||||
# ==========================================================================
|
||||
|
||||
|
||||
class KerioAdminApi:
|
||||
"""Duenner JSON-RPC-Client fuer die Kerio Connect Admin-API.
|
||||
|
||||
Die API will nach dem Login drei Dinge sehen: das Session-Cookie, den
|
||||
X-Token-Header und denselben Token nochmal im JSON-Payload.
|
||||
"""
|
||||
|
||||
def __init__(self, host: str, port: int = 4040, verify_tls: bool = True,
|
||||
timeout: int = 30) -> None:
|
||||
self.url = f"https://{host}:{port}/admin/api/jsonrpc/"
|
||||
self.timeout = timeout
|
||||
self.token: str | None = None
|
||||
self._request_id = 0
|
||||
|
||||
context = ssl.create_default_context()
|
||||
if not verify_tls:
|
||||
context.check_hostname = False
|
||||
context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
self._opener = urllib.request.build_opener(
|
||||
urllib.request.HTTPSHandler(context=context),
|
||||
urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()),
|
||||
)
|
||||
|
||||
# -- Transport ---------------------------------------------------------
|
||||
|
||||
def call(self, method: str, params: dict | None = None) -> dict:
|
||||
self._request_id += 1
|
||||
payload: dict = {"jsonrpc": "2.0", "id": self._request_id, "method": method}
|
||||
if params is not None:
|
||||
payload["params"] = params
|
||||
if self.token:
|
||||
payload["token"] = self.token
|
||||
|
||||
headers = {"Content-Type": "application/json-rpc; charset=UTF-8"}
|
||||
if self.token:
|
||||
headers["X-Token"] = self.token
|
||||
|
||||
request = urllib.request.Request(
|
||||
self.url,
|
||||
data=json.dumps(payload).encode("utf-8"),
|
||||
headers=headers,
|
||||
method="POST",
|
||||
)
|
||||
|
||||
try:
|
||||
with self._opener.open(request, timeout=self.timeout) as response:
|
||||
body = json.loads(response.read().decode("utf-8"))
|
||||
except urllib.error.HTTPError as exc:
|
||||
raise KerioError(f"HTTP {exc.code} bei {method}: {exc.read()[:500]!r}") from exc
|
||||
except urllib.error.URLError as exc:
|
||||
raise KerioError(f"Verbindung zu {self.url} fehlgeschlagen: {exc.reason}") from exc
|
||||
except ssl.SSLError as exc:
|
||||
raise KerioError(
|
||||
f"TLS-Fehler: {exc}. Bei selbstsigniertem Zertifikat --insecure verwenden."
|
||||
) from exc
|
||||
|
||||
if "error" in body:
|
||||
error = body["error"]
|
||||
raise KerioError(
|
||||
f"{method}: {error.get('message', 'unbekannter Fehler')} "
|
||||
f"(Code {error.get('code')})"
|
||||
)
|
||||
|
||||
result = body.get("result", {})
|
||||
_raise_on_error_list(method, result)
|
||||
return result
|
||||
|
||||
def call_paged(self, method: str, params: dict, list_key: str = "list") -> list[dict]:
|
||||
"""Ruft eine get-Methode so oft auf, bis alle totalItems eingesammelt sind."""
|
||||
items: list[dict] = []
|
||||
while True:
|
||||
query = dict(params.get("query") or {})
|
||||
query.update({"start": len(items), "limit": PAGE_SIZE})
|
||||
query.setdefault("fields", [])
|
||||
query.setdefault("conditions", [])
|
||||
query.setdefault("combining", "Or")
|
||||
|
||||
result = self.call(method, {**params, "query": query})
|
||||
page = result.get(list_key) or []
|
||||
items.extend(page)
|
||||
|
||||
total = result.get("totalItems", len(items))
|
||||
if not page or len(items) >= total:
|
||||
return items
|
||||
|
||||
# -- Session -----------------------------------------------------------
|
||||
|
||||
def login(self, username: str, password: str) -> None:
|
||||
result = self.call("Session.login", {
|
||||
"userName": username,
|
||||
"password": password,
|
||||
"application": APP_INFO,
|
||||
})
|
||||
self.token = result.get("token")
|
||||
if not self.token:
|
||||
raise KerioError("Login lieferte keinen Token zurueck.")
|
||||
|
||||
def logout(self) -> None:
|
||||
if self.token:
|
||||
try:
|
||||
self.call("Session.logout")
|
||||
except KerioError:
|
||||
pass # Ausloggen ist best effort
|
||||
finally:
|
||||
self.token = None
|
||||
|
||||
def __enter__(self) -> "KerioAdminApi":
|
||||
return self
|
||||
|
||||
def __exit__(self, *_exc_info) -> None:
|
||||
self.logout()
|
||||
|
||||
|
||||
def _raise_on_error_list(method: str, result: dict) -> None:
|
||||
"""Kerio liefert Teilfehler nicht als JSON-RPC-error, sondern als 'errors'-Liste."""
|
||||
errors = result.get("errors")
|
||||
if not errors:
|
||||
return
|
||||
details = "; ".join(
|
||||
f"{e.get('message', '?')} (Code {e.get('code')}, Feld {e.get('inputIndex')})"
|
||||
for e in errors
|
||||
)
|
||||
raise KerioError(f"{method} meldet Fehler: {details}")
|
||||
|
||||
|
||||
def find_domain(api: KerioAdminApi, domain_name: str) -> dict:
|
||||
domains = api.call_paged("Domains.get", {})
|
||||
wanted = domain_name.strip().lower()
|
||||
for domain in domains:
|
||||
if domain.get("name", "").lower() == wanted:
|
||||
return domain
|
||||
known = ", ".join(sorted(d.get("name", "?") for d in domains)) or "(keine)"
|
||||
raise KerioError(f"Domain '{domain_name}' nicht gefunden. Vorhanden: {known}")
|
||||
|
||||
|
||||
def find_user(api: KerioAdminApi, domain_id: str, login_name: str) -> dict | None:
|
||||
users = api.call_paged("Users.get", {"domainId": domain_id})
|
||||
wanted = login_name.strip().lower()
|
||||
for user in users:
|
||||
if user.get("loginName", "").lower() == wanted:
|
||||
return user
|
||||
return None
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# Teil 2: Kerio-Dienst steuern
|
||||
# ==========================================================================
|
||||
|
||||
|
||||
class ServiceController:
|
||||
"""Startet und stoppt den Kerio-Connect-Dienst.
|
||||
|
||||
Modus 'none' fasst den Dienst nicht an - dann muss man selbst dafuer
|
||||
sorgen, dass Kerio waehrend des Verschiebens steht.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, mode: str, wait_seconds: int = 90,
|
||||
dry_run: bool = False) -> None:
|
||||
self.name = name
|
||||
self.wait_seconds = wait_seconds
|
||||
self.dry_run = dry_run
|
||||
self.mode = self._resolve_mode(mode)
|
||||
|
||||
@staticmethod
|
||||
def _resolve_mode(mode: str) -> str:
|
||||
if mode != "auto":
|
||||
return mode
|
||||
if shutil.which("systemctl"):
|
||||
return "systemd"
|
||||
if Path("/etc/init.d", DEFAULT_SERVICE).exists():
|
||||
return "initd"
|
||||
return "none"
|
||||
|
||||
def _run(self, *command: str) -> subprocess.CompletedProcess:
|
||||
return subprocess.run(command, capture_output=True, text=True, timeout=120)
|
||||
|
||||
def describe(self) -> str:
|
||||
if self.mode == "none":
|
||||
return "Dienst wird nicht angefasst (--service-manager none)"
|
||||
return f"{self.mode}: {self.name}"
|
||||
|
||||
def is_running(self) -> bool | None:
|
||||
"""True/False, oder None wenn der Status nicht ermittelbar ist."""
|
||||
if self.mode == "systemd":
|
||||
return self._run("systemctl", "is-active", "--quiet", self.name).returncode == 0
|
||||
if self.mode == "initd":
|
||||
result = self._run(f"/etc/init.d/{self.name}", "status")
|
||||
return result.returncode == 0
|
||||
return None
|
||||
|
||||
def preflight(self) -> None:
|
||||
"""Prueft vor allen Aenderungen, ob der Dienst ueberhaupt steuerbar ist."""
|
||||
if self.mode == "none":
|
||||
return
|
||||
if self.mode == "systemd":
|
||||
result = self._run("systemctl", "cat", self.name)
|
||||
if result.returncode != 0:
|
||||
raise AbortedError(
|
||||
f"systemd kennt die Unit '{self.name}' nicht. "
|
||||
"Richtigen Namen mit --service-name angeben oder "
|
||||
"--service-manager none verwenden."
|
||||
)
|
||||
elif self.mode == "initd":
|
||||
if not Path(f"/etc/init.d/{self.name}").exists():
|
||||
raise AbortedError(f"/etc/init.d/{self.name} existiert nicht.")
|
||||
|
||||
def stop(self) -> None:
|
||||
if self.mode == "none":
|
||||
print(" Dienst: uebersprungen (--service-manager none)")
|
||||
return
|
||||
if self.dry_run:
|
||||
print(f" [dry-run] wuerde Dienst '{self.name}' stoppen")
|
||||
return
|
||||
|
||||
print(f" Stoppe Dienst '{self.name}' ...")
|
||||
if self.mode == "systemd":
|
||||
result = self._run("systemctl", "stop", self.name)
|
||||
else:
|
||||
result = self._run(f"/etc/init.d/{self.name}", "stop")
|
||||
if result.returncode != 0:
|
||||
raise AbortedError(
|
||||
f"Dienst konnte nicht gestoppt werden: {result.stderr.strip() or result.stdout.strip()}"
|
||||
)
|
||||
|
||||
# Warten, bis der Dienst wirklich unten ist - sonst schreibt Kerio
|
||||
# noch in Dateien, die wir gerade verschieben.
|
||||
deadline = time.monotonic() + self.wait_seconds
|
||||
while time.monotonic() < deadline:
|
||||
if self.is_running() is False:
|
||||
print(" Dienst gestoppt.")
|
||||
return
|
||||
time.sleep(1)
|
||||
raise AbortedError(
|
||||
f"Dienst '{self.name}' laeuft nach {self.wait_seconds}s immer noch. Abbruch."
|
||||
)
|
||||
|
||||
def start(self) -> None:
|
||||
if self.mode == "none":
|
||||
return
|
||||
if self.dry_run:
|
||||
print(f" [dry-run] wuerde Dienst '{self.name}' starten")
|
||||
return
|
||||
|
||||
print(f" Starte Dienst '{self.name}' ...")
|
||||
if self.mode == "systemd":
|
||||
result = self._run("systemctl", "start", self.name)
|
||||
else:
|
||||
result = self._run(f"/etc/init.d/{self.name}", "start")
|
||||
if result.returncode != 0:
|
||||
print(f" WARNUNG: Start fehlgeschlagen: "
|
||||
f"{result.stderr.strip() or result.stdout.strip()}", file=sys.stderr)
|
||||
else:
|
||||
print(" Dienst gestartet.")
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# Teil 3: Store-Verzeichnisse
|
||||
# Store-Verzeichnisse
|
||||
# ==========================================================================
|
||||
|
||||
|
||||
@@ -434,7 +158,7 @@ def _revert_store_renames(done: list[tuple[Path, Path]]) -> None:
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# Teil 4: Ablauf
|
||||
# Ablauf
|
||||
# ==========================================================================
|
||||
|
||||
|
||||
|
||||
Executable
+360
@@ -0,0 +1,360 @@
|
||||
#!/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:]))
|
||||
Reference in New Issue
Block a user