Switche: optionaler SSH-Port statt fest 22

- Neue Spalte switches.ssh_port (nullable INTEGER), Migration für
  bestehende DBs in _ensure_schema(), Feld in create_db.py ergänzt.
- Formular (Anlegen/Bearbeiten): optionales SSH-Port-Feld, Validierung
  1-65535, leer -> NULL (= Standard 22 überall, SWITCH_DEFAULT_SSH_PORT).
  Switch-Liste zeigt den effektiven Port inkl. "(Standard)"-Hinweis.
- generate_ips.py liefert den effektiven Port (Fallback 22) als eigenes
  Pipe-Feld an poe.sh; poe.sh übernimmt es in disable_poe/enable_poe und
  reicht es als "ssh -p <port>" an die expect-Skripte durch (Default
  weiterhin 22 falls Parameter fehlt).
- Web-Terminal (Verbindungstest) sendet den im Formular eingetragenen
  Port statt hartkodiert 22.
- Live getestet: gültiger/leerer/ungültiger Port beim Anlegen, korrekte
  Weitergabe durch generate_ips.py inkl. Feldreihenfolge, die poe.sh
'read' erwartet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-10 19:56:23 +02:00
co-authored by Claude Sonnet 5
parent 813b17d002
commit 06db4c1382
6 changed files with 75 additions and 20 deletions
+9
View File
@@ -203,6 +203,15 @@ Löschen ist nur möglich, wenn kein Switch mehr auf den Eintrag verweist.
Bestehende Datenbanken aus einer älteren Version (Username/Passwort direkt Bestehende Datenbanken aus einer älteren Version (Username/Passwort direkt
am Switch) werden beim ersten Start automatisch migriert. am Switch) werden beim ersten Start automatisch migriert.
Jeder Switch kann außerdem einen individuellen **SSH-Port** hinterlegen
(Feld „SSH-Port“ beim Anlegen/Bearbeiten) — bleibt er leer, wird überall
automatisch **Port 22** angenommen (`SWITCH_DEFAULT_SSH_PORT` in `app.py`).
Der Port wird konsistent an allen drei Stellen verwendet, an denen die App
sich per SSH mit einem Switch verbindet: dem Web-Terminal (Verbindungstest),
`generate_ips.py` (liefert ihn als eigenes Feld an `poe.sh`) und den
`expect`-Aufrufen in `poe.sh` selbst (`ssh -p <port> ...`) für automatische
wie manuelle PoE-Neustarts.
## Änderungslog ## Änderungslog
Jede Anlage, Bearbeitung, Löschung sowie jedes Aktivieren/Deaktivieren von Jede Anlage, Bearbeitung, Löschung sowie jedes Aktivieren/Deaktivieren von
+33 -7
View File
@@ -77,6 +77,10 @@ FERNET_KEY_PATH = os.environ.get("POE_FERNET_KEY", os.path.join(BASE_DIR, "ferne
SSH_KNOWN_HOSTS_PATH = os.environ.get("POE_KNOWN_HOSTS", os.path.join(BASE_DIR, "known_hosts")) SSH_KNOWN_HOSTS_PATH = os.environ.get("POE_KNOWN_HOSTS", os.path.join(BASE_DIR, "known_hosts"))
SECRET_KEY_PATH = os.environ.get("POE_SECRET_KEY_FILE", os.path.join(BASE_DIR, "secret.key")) SECRET_KEY_PATH = os.environ.get("POE_SECRET_KEY_FILE", os.path.join(BASE_DIR, "secret.key"))
AVATAR_DIR = os.path.join(BASE_DIR, "static", "uploads", "avatars") AVATAR_DIR = os.path.join(BASE_DIR, "static", "uploads", "avatars")
# Wird verwendet, wenn ein Switch keinen eigenen ssh_port hinterlegt hat
# (Feld leer gelassen) — deckt den Standardfall ab, ohne dass er überall
# explizit eingetragen werden muss.
SWITCH_DEFAULT_SSH_PORT = 22
ALLOWED_AVATAR_EXT = {"png", "jpg", "jpeg", "gif", "webp"} ALLOWED_AVATAR_EXT = {"png", "jpg", "jpeg", "gif", "webp"}
os.makedirs(AVATAR_DIR, exist_ok=True) os.makedirs(AVATAR_DIR, exist_ok=True)
@@ -519,6 +523,10 @@ def _ensure_schema():
conn.execute("ALTER TABLE switches ADD COLUMN last_modified_by TEXT") conn.execute("ALTER TABLE switches ADD COLUMN last_modified_by TEXT")
if "last_modified_at" not in switch_cols: if "last_modified_at" not in switch_cols:
conn.execute("ALTER TABLE switches ADD COLUMN last_modified_at TEXT") conn.execute("ALTER TABLE switches ADD COLUMN last_modified_at TEXT")
# SSH-Port je Switch (falls nicht Standard 22) — NULL/leer bedeutet
# überall "22 verwenden" (siehe SWITCH_DEFAULT_SSH_PORT-Fallback).
if "ssh_port" not in switch_cols:
conn.execute("ALTER TABLE switches ADD COLUMN ssh_port INTEGER")
# Migration: bestehende, direkt am Switch hinterlegte Zugangsdaten # Migration: bestehende, direkt am Switch hinterlegte Zugangsdaten
# (ältere DB-Version) in eigene Credentials-Datensätze überführen. # (ältere DB-Version) in eigene Credentials-Datensätze überführen.
@@ -1509,6 +1517,18 @@ def _resolve_credential_choice(conn):
return None, "Bitte Zugangsdaten auswählen oder neue anlegen." return None, "Bitte Zugangsdaten auswählen oder neue anlegen."
def _parse_ssh_port(form):
"""Liest das optionale SSH-Port-Feld aus dem Formular. Leer -> None (=
Fallback auf SWITCH_DEFAULT_SSH_PORT überall, wo der Port gebraucht
wird). Gibt (port_or_none, error_message) zurück."""
raw = (form.get("ssh_port") or "").strip()
if not raw:
return None, None
if not raw.isdigit() or not (1 <= int(raw) <= 65535):
return None, "SSH-Port muss eine Zahl zwischen 1 und 65535 sein (oder leer für Standard 22)!"
return int(raw), None
@app.route("/switches", methods=["GET", "POST"]) @app.route("/switches", methods=["GET", "POST"])
@login_required @login_required
def switches(): def switches():
@@ -1524,14 +1544,17 @@ def switches():
return redirect(url_for("switches")) return redirect(url_for("switches"))
hostname = request.form["hostname"] hostname = request.form["hostname"]
ip = request.form["ip"] ip = request.form["ip"]
ssh_port, port_error = _parse_ssh_port(request.form)
credential_id, cred_error = _resolve_credential_choice(conn) credential_id, cred_error = _resolve_credential_choice(conn)
if cred_error: if port_error:
flash(port_error, "danger")
elif cred_error:
flash(cred_error, "danger") flash(cred_error, "danger")
else: else:
try: try:
conn.execute( conn.execute(
"INSERT INTO switches (hostname, ip, credential_id) VALUES (?, ?, ?)", "INSERT INTO switches (hostname, ip, ssh_port, credential_id) VALUES (?, ?, ?, ?)",
(hostname, ip, credential_id), (hostname, ip, ssh_port, credential_id),
) )
touch_record(conn, "switches", "hostname", hostname) touch_record(conn, "switches", "hostname", hostname)
conn.commit() conn.commit()
@@ -1547,14 +1570,17 @@ def switches():
old_hostname = request.form["old_hostname"] old_hostname = request.form["old_hostname"]
hostname = request.form["hostname"] hostname = request.form["hostname"]
ip = request.form["ip"] ip = request.form["ip"]
ssh_port, port_error = _parse_ssh_port(request.form)
credential_id, cred_error = _resolve_credential_choice(conn) credential_id, cred_error = _resolve_credential_choice(conn)
if cred_error: if port_error:
flash(port_error, "danger")
elif cred_error:
flash(cred_error, "danger") flash(cred_error, "danger")
else: else:
try: try:
conn.execute( conn.execute(
"UPDATE switches SET hostname=?, ip=?, credential_id=? WHERE hostname=?", "UPDATE switches SET hostname=?, ip=?, ssh_port=?, credential_id=? WHERE hostname=?",
(hostname, ip, credential_id, old_hostname), (hostname, ip, ssh_port, credential_id, old_hostname),
) )
if hostname != old_hostname: if hostname != old_hostname:
conn.execute( conn.execute(
@@ -1569,7 +1595,7 @@ def switches():
flash("Hostname existiert bereits oder Eingabefehler!", "danger") flash("Hostname existiert bereits oder Eingabefehler!", "danger")
switch_rows = conn.execute(""" switch_rows = conn.execute("""
SELECT switches.hostname, switches.ip, switches.credential_id, SELECT switches.hostname, switches.ip, switches.ssh_port, switches.credential_id,
credentials.name AS credential_name, credentials.username AS credential_username credentials.name AS credential_name, credentials.username AS credential_username
FROM switches FROM switches
LEFT JOIN credentials ON credentials.id = switches.credential_id LEFT JOIN credentials ON credentials.id = switches.credential_id
+1
View File
@@ -26,6 +26,7 @@ c.execute("""
CREATE TABLE IF NOT EXISTS switches ( CREATE TABLE IF NOT EXISTS switches (
hostname TEXT PRIMARY KEY, hostname TEXT PRIMARY KEY,
ip TEXT NOT NULL, ip TEXT NOT NULL,
ssh_port INTEGER,
credential_id INTEGER, credential_id INTEGER,
last_modified_by TEXT, last_modified_by TEXT,
last_modified_at TEXT, last_modified_at TEXT,
+4 -1
View File
@@ -15,7 +15,7 @@ def generate_ips_list():
switches = { switches = {
row["hostname"]: row row["hostname"]: row
for row in conn.execute(""" for row in conn.execute("""
SELECT switches.hostname, switches.ip, SELECT switches.hostname, switches.ip, switches.ssh_port,
credentials.username AS username, credentials.password AS password credentials.username AS username, credentials.password AS password
FROM switches FROM switches
LEFT JOIN credentials ON credentials.id = switches.credential_id LEFT JOIN credentials ON credentials.id = switches.credential_id
@@ -33,10 +33,12 @@ def generate_ips_list():
switch = switches.get(dev["switch_hostname"]) switch = switches.get(dev["switch_hostname"])
if switch and switch["password"]: if switch and switch["password"]:
switch_ip = switch["ip"] switch_ip = switch["ip"]
switch_ssh_port = switch["ssh_port"] or 22
switch_user = switch["username"] switch_user = switch["username"]
switch_pass = decrypt_password(switch["password"]) switch_pass = decrypt_password(switch["password"])
else: else:
switch_ip = switch["ip"] if switch else "" switch_ip = switch["ip"] if switch else ""
switch_ssh_port = (switch["ssh_port"] if switch else None) or 22
switch_user = "" switch_user = ""
switch_pass = "" switch_pass = ""
@@ -45,6 +47,7 @@ def generate_ips_list():
f"{dev['rpi_ip']}|" f"{dev['rpi_ip']}|"
f"{dev['name']}|" f"{dev['name']}|"
f"{switch_ip}|" f"{switch_ip}|"
f"{switch_ssh_port}|"
f"{dev['switch_hostname'] or 'kein Switch'}|" f"{dev['switch_hostname'] or 'kein Switch'}|"
f"{port}|" f"{port}|"
f"{switch_user}|" f"{switch_user}|"
+18 -4
View File
@@ -37,15 +37,20 @@
<tr> <tr>
<th data-sort-key="hostname">Hostname</th> <th data-sort-key="hostname">Hostname</th>
<th data-sort-key="ip">IP-Adresse</th> <th data-sort-key="ip">IP-Adresse</th>
<th data-sort-key="ssh_port">SSH-Port</th>
<th data-sort-key="credential">Zugangsdaten</th> <th data-sort-key="credential">Zugangsdaten</th>
<th style="width:1%;">Aktionen</th> <th style="width:1%;">Aktionen</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for s in switches %} {% for s in switches %}
<tr data-sort-hostname="{{ s['hostname']|lower }}" data-sort-ip="{{ s['ip']|lower }}" data-sort-credential="{{ (s['credential_name'] or '')|lower }}"> <tr data-sort-hostname="{{ s['hostname']|lower }}" data-sort-ip="{{ s['ip']|lower }}" data-sort-ssh_port="{{ s['ssh_port'] or 22 }}" data-sort-credential="{{ (s['credential_name'] or '')|lower }}">
<td class="cell-name">{{ s['hostname'] }}</td> <td class="cell-name">{{ s['hostname'] }}</td>
<td class="mono">{{ s['ip'] }}</td> <td class="mono">{{ s['ip'] }}</td>
<td class="mono">
{{ s['ssh_port'] or 22 }}
{% if not s['ssh_port'] %}<span class="text-faint" style="font-size:11px;">(Standard)</span>{% endif %}
</td>
<td> <td>
{% if s['credential_name'] %} {% if s['credential_name'] %}
{{ s['credential_name'] }} <span class="text-faint mono" style="font-size:11.5px;">({{ s['credential_username'] }})</span> {{ s['credential_name'] }} <span class="text-faint mono" style="font-size:11.5px;">({{ s['credential_username'] }})</span>
@@ -89,6 +94,10 @@
<input type="text" name="ip" value="{{ s['ip'] }}" required placeholder="z.B. 192.168.1.100"> <input type="text" name="ip" value="{{ s['ip'] }}" required placeholder="z.B. 192.168.1.100">
<div class="invalid-feedback">Bitte eine gültige IP-Adresse eingeben.</div> <div class="invalid-feedback">Bitte eine gültige IP-Adresse eingeben.</div>
</div> </div>
<div class="field"><label>SSH-Port</label>
<input type="number" name="ssh_port" value="{{ s['ssh_port'] or '' }}" min="1" max="65535" placeholder="22 (Standard)">
<div class="field-hint">Leer lassen, wenn der Switch den Standard-Port 22 verwendet.</div>
</div>
<div class="field"> <div class="field">
<label>Zugangsdaten</label> <label>Zugangsdaten</label>
<select name="credential_choice" class="credential-select" onchange="toggleNewCredentialFields(this)"> <select name="credential_choice" class="credential-select" onchange="toggleNewCredentialFields(this)">
@@ -127,7 +136,7 @@
</div> </div>
{% endif %} {% endif %}
{% else %} {% else %}
<tr class="empty-row"><td colspan="4">Noch keine Switche vorhanden.</td></tr> <tr class="empty-row"><td colspan="5">Noch keine Switche vorhanden.</td></tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
@@ -152,6 +161,10 @@
<input type="text" name="ip" required placeholder="z.B. 192.168.1.100"> <input type="text" name="ip" required placeholder="z.B. 192.168.1.100">
<div class="invalid-feedback">Bitte eine gültige IP-Adresse eingeben.</div> <div class="invalid-feedback">Bitte eine gültige IP-Adresse eingeben.</div>
</div> </div>
<div class="field"><label>SSH-Port</label>
<input type="number" name="ssh_port" min="1" max="65535" placeholder="22 (Standard)">
<div class="field-hint">Leer lassen, wenn der Switch den Standard-Port 22 verwendet.</div>
</div>
<div class="field"> <div class="field">
<label>Zugangsdaten</label> <label>Zugangsdaten</label>
<select name="credential_choice" class="credential-select" onchange="toggleNewCredentialFields(this)"> <select name="credential_choice" class="credential-select" onchange="toggleNewCredentialFields(this)">
@@ -300,6 +313,7 @@ function getCredentialInfo(form) {
function openTerminal(form) { function openTerminal(form) {
const host = (form.querySelector("input[name='ip']") || {}).value?.trim(); const host = (form.querySelector("input[name='ip']") || {}).value?.trim();
const port = parseInt((form.querySelector("input[name='ssh_port']") || {}).value, 10) || 22;
const { username, passwordInput } = getCredentialInfo(form); const { username, passwordInput } = getCredentialInfo(form);
activePasswordInput = passwordInput; activePasswordInput = passwordInput;
@@ -311,7 +325,7 @@ function openTerminal(form) {
PoeUI.openModal("terminalModal"); PoeUI.openModal("terminalModal");
ensureTerminal(); ensureTerminal();
term.reset(); term.reset();
document.getElementById("termTarget").innerText = `${username}@${host}`; document.getElementById("termTarget").innerText = `${username}@${host}:${port}`;
setTermStatus("Verbinde…", "unknown"); setTermStatus("Verbinde…", "unknown");
setTimeout(() => fitAddon && fitAddon.fit(), 60); setTimeout(() => fitAddon && fitAddon.fit(), 60);
@@ -321,7 +335,7 @@ function openTerminal(form) {
termSocket = new WebSocket(`${proto}//${location.host}/ws/ssh_terminal`); termSocket = new WebSocket(`${proto}//${location.host}/ws/ssh_terminal`);
termSocket.onopen = () => { termSocket.onopen = () => {
termSocket.send(JSON.stringify({ host, username, port: 22 })); termSocket.send(JSON.stringify({ host, username, port }));
setTermStatus("Verbunden", "online"); setTermStatus("Verbunden", "online");
}; };
termSocket.onmessage = (event) => term.write(event.data); termSocket.onmessage = (event) => term.write(event.data);
+10 -8
View File
@@ -30,9 +30,10 @@ function disable_poe() {
local switch_port=$2 local switch_port=$2
local username=$3 local username=$3
local password=$4 local password=$4
local ssh_port=${5:-22}
expect <<EOF expect <<EOF
set timeout 5 set timeout 5
spawn ssh $username@$switch_ip spawn ssh -p $ssh_port $username@$switch_ip
expect { expect {
"assword:" { send "$password\r"; exp_continue } "assword:" { send "$password\r"; exp_continue }
"Press any key" { send "\r"; exp_continue } "Press any key" { send "\r"; exp_continue }
@@ -60,9 +61,10 @@ function enable_poe() {
local switch_port=$2 local switch_port=$2
local username=$3 local username=$3
local password=$4 local password=$4
local ssh_port=${5:-22}
expect <<EOF expect <<EOF
set timeout 5 set timeout 5
spawn ssh $username@$switch_ip spawn ssh -p $ssh_port $username@$switch_ip
expect { expect {
"assword:" { send "$password\r"; exp_continue } "assword:" { send "$password\r"; exp_continue }
"Press any key" { send "\r"; exp_continue } "Press any key" { send "\r"; exp_continue }
@@ -87,16 +89,16 @@ EOF
function manual_restart() { function manual_restart() {
local target_mac="$1" local target_mac="$1"
python3 /srv/poe_manager/generate_ips.py | while IFS='|' read -r rpi_ip dev_name switch_ip switch_hostname switch_port switch_user switch_pass mac; do python3 /srv/poe_manager/generate_ips.py | while IFS='|' read -r rpi_ip dev_name switch_ip switch_ssh_port switch_hostname switch_port switch_user switch_pass mac; do
if [[ "$mac" != "$target_mac" ]]; then if [[ "$mac" != "$target_mac" ]]; then
continue continue
fi fi
echo "$(date '+%Y-%m-%d %H:%M:%S') Manueller Neustart von $dev_name gestartet." >> "$LOGFILE" echo "$(date '+%Y-%m-%d %H:%M:%S') Manueller Neustart von $dev_name gestartet." >> "$LOGFILE"
if [ -n "$switch_ip" ] && [ -n "$switch_port" ] && [ "$switch_port" != "None" ]; then if [ -n "$switch_ip" ] && [ -n "$switch_port" ] && [ "$switch_port" != "None" ]; then
disable_poe "$switch_ip" "$switch_port" "$switch_user" "$switch_pass" disable_poe "$switch_ip" "$switch_port" "$switch_user" "$switch_pass" "$switch_ssh_port"
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name PoE deaktiviert." >> "$LOGFILE" echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name PoE deaktiviert." >> "$LOGFILE"
sleep 2 sleep 2
enable_poe "$switch_ip" "$switch_port" "$switch_user" "$switch_pass" enable_poe "$switch_ip" "$switch_port" "$switch_user" "$switch_pass" "$switch_ssh_port"
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name PoE aktiviert." >> "$LOGFILE" echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name PoE aktiviert." >> "$LOGFILE"
echo "$(date '+%Y-%m-%d %H:%M:%S') Manueller Neustart von $dev_name abgeschlossen." >> "$LOGFILE" echo "$(date '+%Y-%m-%d %H:%M:%S') Manueller Neustart von $dev_name abgeschlossen." >> "$LOGFILE"
else else
@@ -119,7 +121,7 @@ echo "" > "$LOGFILE"
while true; do while true; do
echo "--------------------------------------------------------------------" >> "$LOGFILE" echo "--------------------------------------------------------------------" >> "$LOGFILE"
python3 /srv/poe_manager/generate_ips.py | while IFS='|' read -r rpi_ip dev_name switch_ip switch_hostname switch_port switch_user switch_pass mac; do python3 /srv/poe_manager/generate_ips.py | while IFS='|' read -r rpi_ip dev_name switch_ip switch_ssh_port switch_hostname switch_port switch_user switch_pass mac; do
if ping -c 1 -W 2 "$rpi_ip" &> /dev/null; then if ping -c 1 -W 2 "$rpi_ip" &> /dev/null; then
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name ist erreichbar!" >> "$LOGFILE" echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name ist erreichbar!" >> "$LOGFILE"
else else
@@ -130,10 +132,10 @@ while true; do
# (switch_ip leer) darf keinen SSH-Versuch mit leeren # (switch_ip leer) darf keinen SSH-Versuch mit leeren
# Zugangsdaten auslösen. # Zugangsdaten auslösen.
if [ -n "$switch_ip" ] && [ -n "$switch_port" ] && [ "$switch_port" != "None" ]; then if [ -n "$switch_ip" ] && [ -n "$switch_port" ] && [ "$switch_port" != "None" ]; then
disable_poe "$switch_ip" "$switch_port" "$switch_user" "$switch_pass" disable_poe "$switch_ip" "$switch_port" "$switch_user" "$switch_pass" "$switch_ssh_port"
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name PoE auf Port $switch_port am Switch $switch_hostname deaktiviert." >> "$LOGFILE" echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name PoE auf Port $switch_port am Switch $switch_hostname deaktiviert." >> "$LOGFILE"
sleep 2 sleep 2
enable_poe "$switch_ip" "$switch_port" "$switch_user" "$switch_pass" enable_poe "$switch_ip" "$switch_port" "$switch_user" "$switch_pass" "$switch_ssh_port"
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name PoE auf Port $switch_port am Switch $switch_hostname aktiviert." >> "$LOGFILE" echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name PoE auf Port $switch_port am Switch $switch_hostname aktiviert." >> "$LOGFILE"
fi fi
fi fi