Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b3051c984 | ||
|
|
6dd412c6e1 |
+1
-1
@@ -1 +1 @@
|
|||||||
1.0.0
|
1.0.1
|
||||||
|
|||||||
+96
-21
@@ -4448,9 +4448,22 @@ def _import_groups(conn, items):
|
|||||||
|
|
||||||
|
|
||||||
def _import_ldap(conn, data):
|
def _import_ldap(conn, data):
|
||||||
|
# set_setting() bewusst NICHT verwendet: das öffnet eine EIGENE
|
||||||
|
# sqlite3-Verbindung -- import_apply() ruft mehrere IMPORT_APPLIERS
|
||||||
|
# nacheinander auf DERSELBEN, bereits offenen "conn" auf und committet
|
||||||
|
# erst ganz am Ende. Ist zu diesem Zeitpunkt (z.B. durch eine vorher im
|
||||||
|
# selben Import verarbeitete Kategorie) bereits eine Schreib-Transaktion
|
||||||
|
# auf "conn" offen, blockiert set_settings()' eigene Verbindung darauf,
|
||||||
|
# bis der SQLite-Standard-Timeout zuschlägt -- "sqlite3.OperationalError:
|
||||||
|
# database is locked", live in Produktion reproduziert. Direkt auf der
|
||||||
|
# übergebenen "conn" schreiben vermeidet die zweite Verbindung komplett.
|
||||||
for key, value in data.get("settings", {}).items():
|
for key, value in data.get("settings", {}).items():
|
||||||
if key in LDAP_SETTING_KEYS:
|
if key in LDAP_SETTING_KEYS:
|
||||||
set_setting(key, value)
|
conn.execute(
|
||||||
|
"INSERT INTO settings (key, value) VALUES (?, ?) "
|
||||||
|
"ON CONFLICT(key) DO UPDATE SET value=excluded.value",
|
||||||
|
(key, value),
|
||||||
|
)
|
||||||
if data.get("bind_username"):
|
if data.get("bind_username"):
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO service_accounts (purpose, username, password) VALUES (?, ?, ?) "
|
"INSERT INTO service_accounts (purpose, username, password) VALUES (?, ?, ?) "
|
||||||
@@ -4469,9 +4482,16 @@ def _import_ldap(conn, data):
|
|||||||
|
|
||||||
|
|
||||||
def _import_dhcp(conn, data):
|
def _import_dhcp(conn, data):
|
||||||
|
# set_setting() bewusst NICHT verwendet -- siehe ausführlicher Kommentar
|
||||||
|
# in _import_ldap() (dieselbe "database is locked"-Falle bei mehreren
|
||||||
|
# IMPORT_APPLIERS auf derselben offenen Transaktion).
|
||||||
for key, value in data.get("settings", {}).items():
|
for key, value in data.get("settings", {}).items():
|
||||||
if key in DHCP_DEFAULTS:
|
if key in DHCP_DEFAULTS:
|
||||||
set_setting(key, value)
|
conn.execute(
|
||||||
|
"INSERT INTO settings (key, value) VALUES (?, ?) "
|
||||||
|
"ON CONFLICT(key) DO UPDATE SET value=excluded.value",
|
||||||
|
(key, value),
|
||||||
|
)
|
||||||
n = 0
|
n = 0
|
||||||
# Subnetze additiv (nie löschen/ersetzen) -- ein bestehendes, unabhängig
|
# Subnetze additiv (nie löschen/ersetzen) -- ein bestehendes, unabhängig
|
||||||
# vom Ziel-Admin konfiguriertes Subnetz soll ein Import nie entfernen.
|
# vom Ziel-Admin konfiguriertes Subnetz soll ein Import nie entfernen.
|
||||||
@@ -5596,6 +5616,27 @@ def maintenance_status():
|
|||||||
|
|
||||||
|
|
||||||
_MAINTENANCE_OUTPUT_MAX_LINES = 400
|
_MAINTENANCE_OUTPUT_MAX_LINES = 400
|
||||||
|
TESM_CLIENT_UPDATE_LOG_DIR = os.path.join(TESM_LOG_DIR, "clientupdate")
|
||||||
|
|
||||||
|
|
||||||
|
def _write_client_update_error_log(name, exit_code, lines):
|
||||||
|
"""Schreibt bei einem mit Exit-Code fehlgeschlagenen Wartungs-Update
|
||||||
|
eine eigene Fehler-Logdatei pro Client (überschreibt eine vorherige aus
|
||||||
|
einem früheren Fehlschlag desselben Clients) -- zusätzlich zur ohnehin
|
||||||
|
im Job-Status/Live-Fenster sichtbaren Ausgabe, damit ein Fehlschlag
|
||||||
|
auch ohne offenes Browser-Fenster im Nachhinein auffindbar bleibt."""
|
||||||
|
try:
|
||||||
|
os.makedirs(TESM_CLIENT_UPDATE_LOG_DIR, exist_ok=True)
|
||||||
|
os.chmod(TESM_CLIENT_UPDATE_LOG_DIR, 0o777)
|
||||||
|
safe_name = secure_filename(name) or "unbenannt"
|
||||||
|
path = os.path.join(TESM_CLIENT_UPDATE_LOG_DIR, f"{safe_name}.error")
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(f"Update fehlgeschlagen: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
||||||
|
f.write(f"Exit-Code: {exit_code}\n\n")
|
||||||
|
f.write("\n".join(lines))
|
||||||
|
f.write("\n")
|
||||||
|
except OSError:
|
||||||
|
app.logger.error("_write_client_update_error_log failed:\n%s", traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
def _maintenance_run_update(mac, name, host, port, username, password):
|
def _maintenance_run_update(mac, name, host, port, username, password):
|
||||||
@@ -5647,6 +5688,7 @@ def _maintenance_run_update(mac, name, host, port, username, password):
|
|||||||
message=f"Update fehlgeschlagen (Exit-Code {result['exit_code']}).",
|
message=f"Update fehlgeschlagen (Exit-Code {result['exit_code']}).",
|
||||||
output="\n".join(lines), finished=finished,
|
output="\n".join(lines), finished=finished,
|
||||||
)
|
)
|
||||||
|
_write_client_update_error_log(name, result["exit_code"], lines)
|
||||||
log_action_system("maintenance.update", name, f"fehlgeschlagen: Exit-Code {result['exit_code']}")
|
log_action_system("maintenance.update", name, f"fehlgeschlagen: Exit-Code {result['exit_code']}")
|
||||||
|
|
||||||
|
|
||||||
@@ -6108,31 +6150,64 @@ def _ssh_terminal_inner(ws):
|
|||||||
# ihn möglich sind -- beide Wege teilen sich dieselbe SSH_KNOWN_HOSTS_PATH.
|
# ihn möglich sind -- beide Wege teilen sich dieselbe SSH_KNOWN_HOSTS_PATH.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Jeder Schritt einzeln benannt (statt nur mit "&&" verkettet), damit nach
|
||||||
|
# jedem Schritt ein eigener "Schritt N -> erfolgreich/fehlgeschlagen"-Marker
|
||||||
|
# ins Live-Log geschrieben werden kann (siehe _apt_upgrade_command()) --
|
||||||
|
# apt/apt-get statt der literal vorgegebenen "apt"-Aufrufe, weil apt-get
|
||||||
|
# eine stabile, für Skripte gedachte Schnittstelle hat (die apt(8)-Manpage
|
||||||
|
# warnt ausdrücklich: "the apt command is intended to be pleasant for
|
||||||
|
# end users and does not need to be backward compatible" -- also nichts
|
||||||
|
# für Automatisierung).
|
||||||
|
_LINUX_UPDATE_STEPS = [
|
||||||
|
("apt update", "apt-get update"),
|
||||||
|
("initramfs MODULES=most setzen", "sed -i 's/^MODULES=.*/MODULES=most/' /etc/initramfs-tools/initramfs.conf"),
|
||||||
|
(
|
||||||
|
"apt upgrade",
|
||||||
|
'apt-get -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" upgrade',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"apt full-upgrade",
|
||||||
|
'apt-get -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" full-upgrade',
|
||||||
|
),
|
||||||
|
("apt clean", "apt-get clean"),
|
||||||
|
("apt autoremove", "apt-get -y autoremove"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def _apt_upgrade_command():
|
def _apt_upgrade_command():
|
||||||
"""Sicheres, nicht-interaktives 'apt update && apt upgrade -y':
|
"""Baut die Wartungs-Update-Kette aus _LINUX_UPDATE_STEPS zusammen --
|
||||||
|
jeder Schritt einzeln mit eigenem "=== Schritt N/M: ... ===" und
|
||||||
|
danach "Schritt N (...) -> erfolgreich"/"-> fehlgeschlagen (Exit-Code
|
||||||
|
...)" im Live-Log, statt nur einer einzigen Erfolgs-/Fehlermeldung für
|
||||||
|
die ganze Kette. Bricht beim ersten fehlgeschlagenen Schritt sofort ab
|
||||||
|
(dessen Exit-Code wird zum Exit-Code des Gesamtkommandos), statt
|
||||||
|
blind mit dem nächsten Schritt weiterzumachen.
|
||||||
|
|
||||||
DEBIAN_FRONTEND=noninteractive plus --force-confdef/--force-confold
|
DEBIAN_FRONTEND=noninteractive plus --force-confdef/--force-confold
|
||||||
verhindern, dass ein Paket-Postinst-Skript auf eine Konfig-Datei-
|
verhindern, dass ein Paket-Postinst-Skript auf eine Konfig-Datei-
|
||||||
Rückfrage wartet, die nie kommt (das Kommando würde sonst ohne PTY
|
Rückfrage wartet, die nie kommt (das Kommando würde sonst ohne PTY
|
||||||
unbeaufsichtigt hängen bleiben, bis der Timeout zuschlägt).
|
unbeaufsichtigt hängen bleiben, bis der Timeout zuschlägt).
|
||||||
|
|
||||||
MODULES=most (initramfs-tools) wird nur für diesen einen Aufruf per
|
MODULES=most wird -- anders als zuvor -- dauerhaft in
|
||||||
Umgebungsvariable gesetzt, nicht dauerhaft in /etc/initramfs-tools/
|
/etc/initramfs-tools/initramfs.conf gesetzt (nicht nur für diesen einen
|
||||||
initramfs.conf geändert -- falls das Update einen neuen Kernel mitbringt
|
Aufruf per Umgebungsvariable): falls apt upgrade/full-upgrade einen
|
||||||
und dabei ein initramfs neu gebaut wird, landen so vorsorglich ALLE
|
neuen Kernel installiert und dabei ein initramfs neu gebaut wird,
|
||||||
verfügbaren Treiber darin (statt nur die per "dep" erkannten), damit das
|
landen so ALLE verfügbaren Treiber darin (statt nur die per "dep"
|
||||||
Gerät nach einem Kernel-Wechsel zuverlässig bootet, auch wenn sich z.B.
|
erkannten), damit das Gerät nach einem Kernel-Wechsel zuverlässig
|
||||||
Storage-/Netzwerk-Hardware nicht sauber per Abhängigkeitserkennung
|
bootet -- und das bleibt auch für jeden künftigen, unabhängig von
|
||||||
ermitteln lässt. Am Ende noch "apt-get autoremove"/"apt-get clean", damit
|
dieser App ausgelösten Kernel-Update/initramfs-Rebuild so bestehen."""
|
||||||
nicht mehr benötigte Kernel/Pakete sowie der heruntergeladene .deb-Cache
|
script_lines = ["export DEBIAN_FRONTEND=noninteractive"]
|
||||||
nicht auf jedem Client unbegrenzt Platz belegen."""
|
total = len(_LINUX_UPDATE_STEPS)
|
||||||
inner = (
|
for i, (label, cmd) in enumerate(_LINUX_UPDATE_STEPS, start=1):
|
||||||
"env DEBIAN_FRONTEND=noninteractive MODULES=most apt-get update && "
|
script_lines.append(f"echo '=== Schritt {i}/{total}: {label} ==='")
|
||||||
'env DEBIAN_FRONTEND=noninteractive MODULES=most apt-get -y '
|
script_lines.append(cmd)
|
||||||
'-o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" upgrade && '
|
script_lines.append("_rc=$?")
|
||||||
"env DEBIAN_FRONTEND=noninteractive apt-get -y autoremove && "
|
script_lines.append(
|
||||||
"apt-get clean"
|
f'if [ "$_rc" -eq 0 ]; then echo "Schritt {i} ({label}) -> erfolgreich"; '
|
||||||
)
|
f'else echo "Schritt {i} ({label}) -> fehlgeschlagen (Exit-Code $_rc)"; exit "$_rc"; fi'
|
||||||
return f"sudo -S -p '' bash -c {shlex.quote(inner)}"
|
)
|
||||||
|
script = "\n".join(script_lines)
|
||||||
|
return f"sudo -S -p '' bash -c {shlex.quote(script)}"
|
||||||
|
|
||||||
|
|
||||||
def _resolve_device_ssh_target(conn, device):
|
def _resolve_device_ssh_target(conn, device):
|
||||||
|
|||||||
Reference in New Issue
Block a user