Import: 'database is locked' bei LDAP-/DHCP-Import behoben; Wartungs-Update mit Einzelschritten + Fehler-Log
_import_ldap()/_import_dhcp() riefen bislang set_setting() auf, das eine EIGENE sqlite3-Verbindung oeffnet. import_apply() ruft mehrere IMPORT_APPLIERS nacheinander auf DERSELBEN, noch offenen Verbindung auf und committet erst am Ende -- war zu diesem Zeitpunkt (z.B. durch eine vorher im selben Import verarbeitete Kategorie) bereits eine Schreib- Transaktion offen, blockierte set_settings()' eigene Verbindung darauf, bis 'sqlite3.OperationalError: database is locked' auftrat (live in Produktion reproduziert). Beide Funktionen schreiben jetzt direkt auf die uebergebene Verbindung. Isoliert reproduziert und verifiziert (alter Code: Lock-Fehler: neuer Code: fehlerfrei). Wartungs-Update fuer Linux-Clients (_apt_upgrade_command) außerdem komplett neu aufgebaut: statt einer einzigen, mit '&&' verketteten Befehlskette laufen jetzt einzeln benannte Schritte (apt update -> initramfs MODULES=most dauerhaft in initramfs.conf setzen -> apt upgrade -> apt full-upgrade -> apt clean -> apt autoremove), jeder mit eigenem 'Schritt N (...) -> erfolgreich/fehlgeschlagen (Exit-Code ...)'-Marker im Live-Fenster (mit Zeitstempel je Zeile, wie gehabt). Bricht beim ersten fehlgeschlagenen Schritt sofort ab. Bei einem mit Exit-Code fehlgeschlagenen Update wird zusaetzlich eine Fehler-Logdatei unter /var/log/tesm/clientupdate/<clientname>.error geschrieben (ueberschreibt eine vorherige aus einem frueheren Fehlschlag desselben Clients).
This commit is contained in:
+95
-20
@@ -4448,9 +4448,22 @@ def _import_groups(conn, items):
|
||||
|
||||
|
||||
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():
|
||||
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"):
|
||||
conn.execute(
|
||||
"INSERT INTO service_accounts (purpose, username, password) VALUES (?, ?, ?) "
|
||||
@@ -4469,9 +4482,16 @@ def _import_ldap(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():
|
||||
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
|
||||
# Subnetze additiv (nie löschen/ersetzen) -- ein bestehendes, unabhängig
|
||||
# vom Ziel-Admin konfiguriertes Subnetz soll ein Import nie entfernen.
|
||||
@@ -5596,6 +5616,27 @@ def maintenance_status():
|
||||
|
||||
|
||||
_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):
|
||||
@@ -5647,6 +5688,7 @@ def _maintenance_run_update(mac, name, host, port, username, password):
|
||||
message=f"Update fehlgeschlagen (Exit-Code {result['exit_code']}).",
|
||||
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']}")
|
||||
|
||||
|
||||
@@ -6108,31 +6150,64 @@ def _ssh_terminal_inner(ws):
|
||||
# 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():
|
||||
"""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
|
||||
verhindern, dass ein Paket-Postinst-Skript auf eine Konfig-Datei-
|
||||
Rückfrage wartet, die nie kommt (das Kommando würde sonst ohne PTY
|
||||
unbeaufsichtigt hängen bleiben, bis der Timeout zuschlägt).
|
||||
|
||||
MODULES=most (initramfs-tools) wird nur für diesen einen Aufruf per
|
||||
Umgebungsvariable gesetzt, nicht dauerhaft in /etc/initramfs-tools/
|
||||
initramfs.conf geändert -- falls das Update einen neuen Kernel mitbringt
|
||||
und dabei ein initramfs neu gebaut wird, landen so vorsorglich ALLE
|
||||
verfügbaren Treiber darin (statt nur die per "dep" erkannten), damit das
|
||||
Gerät nach einem Kernel-Wechsel zuverlässig bootet, auch wenn sich z.B.
|
||||
Storage-/Netzwerk-Hardware nicht sauber per Abhängigkeitserkennung
|
||||
ermitteln lässt. Am Ende noch "apt-get autoremove"/"apt-get clean", damit
|
||||
nicht mehr benötigte Kernel/Pakete sowie der heruntergeladene .deb-Cache
|
||||
nicht auf jedem Client unbegrenzt Platz belegen."""
|
||||
inner = (
|
||||
"env DEBIAN_FRONTEND=noninteractive MODULES=most apt-get update && "
|
||||
'env DEBIAN_FRONTEND=noninteractive MODULES=most apt-get -y '
|
||||
'-o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" upgrade && '
|
||||
"env DEBIAN_FRONTEND=noninteractive apt-get -y autoremove && "
|
||||
"apt-get clean"
|
||||
MODULES=most wird -- anders als zuvor -- dauerhaft in
|
||||
/etc/initramfs-tools/initramfs.conf gesetzt (nicht nur für diesen einen
|
||||
Aufruf per Umgebungsvariable): falls apt upgrade/full-upgrade einen
|
||||
neuen Kernel installiert und dabei ein initramfs neu gebaut wird,
|
||||
landen so ALLE verfügbaren Treiber darin (statt nur die per "dep"
|
||||
erkannten), damit das Gerät nach einem Kernel-Wechsel zuverlässig
|
||||
bootet -- und das bleibt auch für jeden künftigen, unabhängig von
|
||||
dieser App ausgelösten Kernel-Update/initramfs-Rebuild so bestehen."""
|
||||
script_lines = ["export DEBIAN_FRONTEND=noninteractive"]
|
||||
total = len(_LINUX_UPDATE_STEPS)
|
||||
for i, (label, cmd) in enumerate(_LINUX_UPDATE_STEPS, start=1):
|
||||
script_lines.append(f"echo '=== Schritt {i}/{total}: {label} ==='")
|
||||
script_lines.append(cmd)
|
||||
script_lines.append("_rc=$?")
|
||||
script_lines.append(
|
||||
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):
|
||||
|
||||
Reference in New Issue
Block a user