TUI: Dienst per Taste 'r' neu laden
Nach dem Speichern musste man bisher in die Konsole wechseln, um die Konfiguration mit "systemctl reload pvesnap" scharf zu schalten. Die Gruppenliste kann das jetzt selbst: * 'r' laedt den Dienst neu; ungespeicherte Aenderungen werden vorher zum Speichern angeboten, ein gestoppter Dienst zum Starten, und wenn das Neuladen scheitert, wird ein Neustart angeboten. * Nach 's' fragt der Editor direkt, ob neu geladen werden soll. * Die Kopfzeile zeigt laufend den Zustand des Dienstes. * Die Tastenleiste bricht auf schmalen Terminals auf zwei Zeilen um, statt hinten abgeschnitten zu werden. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4e7ff59911
commit
8f1bf037cd
+168
-8
@@ -7,6 +7,7 @@ from __future__ import annotations
|
||||
|
||||
import curses
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from .config import (WEEKDAY_NAMES, Config, ConfigError, Group, clone_group,
|
||||
load_config, parse_time_of_day, save_config)
|
||||
@@ -33,6 +34,92 @@ TYPE_CHOICES = [
|
||||
|
||||
C_HEADER, C_FOOTER, C_SEL, C_WARN, C_OK, C_DIM = 1, 2, 3, 4, 5, 6
|
||||
|
||||
SERVICE = "pvesnap.service"
|
||||
|
||||
# Tastenleiste der Gruppenliste: (Taste, lange Beschriftung, kurze Beschriftung)
|
||||
GROUP_KEYS = [
|
||||
("Enter", "Bearbeiten", "Bearb."),
|
||||
("n", "Neu", "Neu"),
|
||||
("c", "Kopieren", "Kopie"),
|
||||
("d", "Loeschen", "Loesch"),
|
||||
("Leer", "An/Aus", "An/Aus"),
|
||||
("g", "Global", "Global"),
|
||||
("v", "VMs", "VMs"),
|
||||
("s", "Speichern", "Speich."),
|
||||
("r", "Dienst neu laden", "Reload"),
|
||||
("q", "Ende", "Ende"),
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# systemd
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _systemctl(*args, **kwargs):
|
||||
"""Ruft systemctl auf und gibt (Rueckgabewert, Ausgabe) zurueck."""
|
||||
try:
|
||||
proc = subprocess.run(["systemctl"] + list(args), stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT, timeout=kwargs.get("timeout", 30))
|
||||
return proc.returncode, proc.stdout.decode("utf-8", "replace").strip()
|
||||
except FileNotFoundError:
|
||||
return 127, "systemctl nicht gefunden"
|
||||
except subprocess.TimeoutExpired:
|
||||
return 124, "Zeitueberschreitung beim Aufruf von systemctl"
|
||||
|
||||
|
||||
def service_state():
|
||||
"""('geladen?', 'Zustand') des pvesnap-Dienstes, z.B. (True, 'active')."""
|
||||
code, output = _systemctl("show", "-p", "LoadState", "-p", "ActiveState", SERVICE)
|
||||
if code not in (0, 3):
|
||||
return None, output.splitlines()[0] if output else "unbekannt"
|
||||
values = {}
|
||||
for line in output.splitlines():
|
||||
if "=" in line:
|
||||
key, _, value = line.partition("=")
|
||||
values[key.strip()] = value.strip()
|
||||
loaded = values.get("LoadState") == "loaded"
|
||||
return loaded, values.get("ActiveState", "unbekannt")
|
||||
|
||||
|
||||
def service_text():
|
||||
loaded, state = service_state()
|
||||
if loaded is None:
|
||||
return "Dienst: %s" % state
|
||||
if not loaded:
|
||||
return "Dienst: nicht installiert"
|
||||
return {"active": "Dienst: laeuft",
|
||||
"inactive": "Dienst: gestoppt",
|
||||
"failed": "Dienst: FEHLER",
|
||||
"activating": "Dienst: startet"}.get(state, "Dienst: %s" % state)
|
||||
|
||||
|
||||
def _keybar(width, items):
|
||||
"""Tastenleiste als Liste von Zeilen, passend zur Breite des Terminals.
|
||||
|
||||
Reicht der Platz nicht fuer eine Zeile, wird auf zwei Zeilen umgebrochen;
|
||||
bei ganz schmalen Terminals bleiben nur noch die Tasten uebrig.
|
||||
"""
|
||||
def join(entries, index):
|
||||
return " | ".join("%s %s" % (entry[0], entry[index]) for entry in entries)
|
||||
|
||||
long_text = join(items, 1)
|
||||
if len(long_text) + 2 <= width:
|
||||
return [long_text]
|
||||
|
||||
half = (len(items) + 1) // 2
|
||||
first, second = join(items[:half], 1), join(items[half:], 1)
|
||||
if max(len(first), len(second)) + 2 <= width:
|
||||
return [first, second]
|
||||
|
||||
short_text = join(items, 2)
|
||||
if len(short_text) + 2 <= width:
|
||||
return [short_text]
|
||||
|
||||
first, second = join(items[:half], 2), join(items[half:], 2)
|
||||
if max(len(first), len(second)) + 2 <= width:
|
||||
return [first, second]
|
||||
return [" ".join(entry[0] for entry in items)]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Zeichen-Helfer
|
||||
@@ -87,6 +174,7 @@ class Editor:
|
||||
self.dirty = False
|
||||
self._guests = None
|
||||
self._inventory_error = ""
|
||||
self._service_cache = None
|
||||
|
||||
# -- Daten ------------------------------------------------------------
|
||||
|
||||
@@ -121,7 +209,9 @@ class Editor:
|
||||
_fill(win, 0, " pvesnap - %s" % title, curses.color_pair(C_HEADER) | curses.A_BOLD)
|
||||
marker = "*" if self.dirty else " "
|
||||
_fill(win, 1, " %s%s" % (marker, subtitle), curses.color_pair(C_DIM))
|
||||
_fill(win, height - 1, " " + keys, curses.color_pair(C_FOOTER))
|
||||
lines = list(keys) if isinstance(keys, (list, tuple)) else [keys]
|
||||
for offset, line in enumerate(reversed(lines)):
|
||||
_fill(win, height - 1 - offset, " " + line, curses.color_pair(C_FOOTER))
|
||||
return height, width
|
||||
|
||||
def _message(self, win, text, error=False):
|
||||
@@ -237,11 +327,16 @@ class Editor:
|
||||
while True:
|
||||
groups = self.config.groups
|
||||
index = max(0, min(index, max(0, len(groups) - 1)))
|
||||
_, width_now = win.getmaxyx()
|
||||
status = self._service_text()
|
||||
# Der Dienststatus soll immer sichtbar bleiben - notfalls wird der
|
||||
# Pfad vorne gekuerzt.
|
||||
room = max(12, width_now - len(status) - 15)
|
||||
path_text = (self.path if len(self.path) <= room
|
||||
else "…" + self.path[-(room - 1):])
|
||||
height, width = self._frame(
|
||||
win, "Gruppen",
|
||||
"Datei: %s" % self.path,
|
||||
"Enter Bearbeiten | n Neu | c Kopieren | d Loeschen | Leer An/Aus | "
|
||||
"g Global | v VMs | s Speichern | q Ende")
|
||||
win, "Gruppen", "Datei: %s [%s]" % (path_text, status),
|
||||
_keybar(width_now, GROUP_KEYS))
|
||||
|
||||
_put(win, 3, 2, "Gruppe".ljust(18) + "Aktiv " + "Zeitplan".ljust(30)
|
||||
+ "Behalte".ljust(20) + "Auswahl", curses.A_BOLD)
|
||||
@@ -308,6 +403,8 @@ class Editor:
|
||||
self._show_problems(win)
|
||||
elif key == "s":
|
||||
self._save(win)
|
||||
elif key in ("r", "R"):
|
||||
self._reload_service(win)
|
||||
elif key in ("q", "Q") or _is_escape(key):
|
||||
if self.dirty and not self._confirm(win, "Ungespeicherte Aenderungen verwerfen?"):
|
||||
continue
|
||||
@@ -350,7 +447,7 @@ class Editor:
|
||||
win.refresh()
|
||||
_read_key(win)
|
||||
|
||||
def _save(self, win):
|
||||
def _save(self, win, offer_reload=True):
|
||||
problems = self.config.validate()
|
||||
if problems:
|
||||
if not self._confirm(win, "%d Hinweis(e) - trotzdem speichern?" % len(problems)):
|
||||
@@ -358,10 +455,73 @@ class Editor:
|
||||
try:
|
||||
target = save_config(self.config, self.path)
|
||||
self.dirty = False
|
||||
self._message(win, "Gespeichert: %s (systemctl reload pvesnap nicht vergessen)"
|
||||
% target)
|
||||
except OSError as exc:
|
||||
self._message(win, "Speichern fehlgeschlagen: %s" % exc, error=True)
|
||||
return
|
||||
|
||||
# Direkt anbieten, die Aenderung auch scharf zu schalten.
|
||||
loaded, state = service_state()
|
||||
if offer_reload and loaded and state == "active":
|
||||
if self._confirm(win, "Gespeichert. Dienst jetzt neu laden?"):
|
||||
self._reload_service(win)
|
||||
return
|
||||
self._message(win, "Gespeichert: %s (wirksam nach 'r' oder "
|
||||
"'systemctl reload pvesnap')" % target)
|
||||
else:
|
||||
self._message(win, "Gespeichert: %s" % target)
|
||||
|
||||
# -- Dienst steuern ---------------------------------------------------
|
||||
|
||||
def _service_text(self, refresh=False):
|
||||
if self._service_cache is None or refresh:
|
||||
self._service_cache = service_text()
|
||||
return self._service_cache
|
||||
|
||||
def _reload_service(self, win):
|
||||
"""Taste 'r': Konfiguration speichern und den Dienst neu laden."""
|
||||
if self.dirty:
|
||||
if self._confirm(win, "Es gibt ungespeicherte Aenderungen - erst speichern?"):
|
||||
self._save(win, offer_reload=False)
|
||||
if self.dirty:
|
||||
return # Speichern abgebrochen oder fehlgeschlagen
|
||||
|
||||
loaded, state = service_state()
|
||||
if loaded is None:
|
||||
self._message(win, "systemd nicht erreichbar: %s" % state, error=True)
|
||||
return
|
||||
if not loaded:
|
||||
self._message(win, "Der Dienst %s ist nicht installiert - bitte install.sh "
|
||||
"ausfuehren." % SERVICE, error=True)
|
||||
return
|
||||
if os.geteuid() != 0:
|
||||
self._message(win, "Dazu werden root-Rechte benoetigt - bitte 'sudo pvesnap "
|
||||
"config' verwenden.", error=True)
|
||||
return
|
||||
|
||||
if state == "active":
|
||||
code, output = self._run_service_action(win, "reload", "neu geladen")
|
||||
if code != 0:
|
||||
first = (output.splitlines() or ["Fehler %d" % code])[0]
|
||||
if self._confirm(win, "Fehlgeschlagen: %s - stattdessen neu starten?"
|
||||
% truncate(first, 50)):
|
||||
self._run_service_action(win, "restart", "neu gestartet")
|
||||
else:
|
||||
if self._confirm(win, "Der Dienst laeuft nicht - jetzt starten?"):
|
||||
self._run_service_action(win, "start", "gestartet")
|
||||
|
||||
def _run_service_action(self, win, action, past_tense):
|
||||
height, _ = win.getmaxyx()
|
||||
_fill(win, height - 2, " systemctl %s %s ..." % (action, SERVICE),
|
||||
curses.color_pair(C_SEL))
|
||||
win.refresh()
|
||||
code, output = _systemctl(action, SERVICE)
|
||||
self._service_cache = None
|
||||
if code == 0:
|
||||
self._message(win, "Dienst %s - %s" % (past_tense, self._service_text(refresh=True)))
|
||||
else:
|
||||
first = (output.splitlines() or ["Rueckgabewert %d" % code])[0]
|
||||
self._message(win, "systemctl %s fehlgeschlagen: %s" % (action, first), error=True)
|
||||
return code, output
|
||||
|
||||
# -- Bildschirm: eine Gruppe -----------------------------------------
|
||||
|
||||
|
||||
Reference in New Issue
Block a user