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
+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"))
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")
# 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"}
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")
if "last_modified_at" not in switch_cols:
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
# (ä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."
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"])
@login_required
def switches():
@@ -1524,14 +1544,17 @@ def switches():
return redirect(url_for("switches"))
hostname = request.form["hostname"]
ip = request.form["ip"]
ssh_port, port_error = _parse_ssh_port(request.form)
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")
else:
try:
conn.execute(
"INSERT INTO switches (hostname, ip, credential_id) VALUES (?, ?, ?)",
(hostname, ip, credential_id),
"INSERT INTO switches (hostname, ip, ssh_port, credential_id) VALUES (?, ?, ?, ?)",
(hostname, ip, ssh_port, credential_id),
)
touch_record(conn, "switches", "hostname", hostname)
conn.commit()
@@ -1547,14 +1570,17 @@ def switches():
old_hostname = request.form["old_hostname"]
hostname = request.form["hostname"]
ip = request.form["ip"]
ssh_port, port_error = _parse_ssh_port(request.form)
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")
else:
try:
conn.execute(
"UPDATE switches SET hostname=?, ip=?, credential_id=? WHERE hostname=?",
(hostname, ip, credential_id, old_hostname),
"UPDATE switches SET hostname=?, ip=?, ssh_port=?, credential_id=? WHERE hostname=?",
(hostname, ip, ssh_port, credential_id, old_hostname),
)
if hostname != old_hostname:
conn.execute(
@@ -1569,7 +1595,7 @@ def switches():
flash("Hostname existiert bereits oder Eingabefehler!", "danger")
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
FROM switches
LEFT JOIN credentials ON credentials.id = switches.credential_id
+1
View File
@@ -26,6 +26,7 @@ c.execute("""
CREATE TABLE IF NOT EXISTS switches (
hostname TEXT PRIMARY KEY,
ip TEXT NOT NULL,
ssh_port INTEGER,
credential_id INTEGER,
last_modified_by TEXT,
last_modified_at TEXT,
+4 -1
View File
@@ -15,7 +15,7 @@ def generate_ips_list():
switches = {
row["hostname"]: row
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
FROM switches
LEFT JOIN credentials ON credentials.id = switches.credential_id
@@ -33,10 +33,12 @@ def generate_ips_list():
switch = switches.get(dev["switch_hostname"])
if switch and switch["password"]:
switch_ip = switch["ip"]
switch_ssh_port = switch["ssh_port"] or 22
switch_user = switch["username"]
switch_pass = decrypt_password(switch["password"])
else:
switch_ip = switch["ip"] if switch else ""
switch_ssh_port = (switch["ssh_port"] if switch else None) or 22
switch_user = ""
switch_pass = ""
@@ -45,6 +47,7 @@ def generate_ips_list():
f"{dev['rpi_ip']}|"
f"{dev['name']}|"
f"{switch_ip}|"
f"{switch_ssh_port}|"
f"{dev['switch_hostname'] or 'kein Switch'}|"
f"{port}|"
f"{switch_user}|"
+18 -4
View File
@@ -37,15 +37,20 @@
<tr>
<th data-sort-key="hostname">Hostname</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 style="width:1%;">Aktionen</th>
</tr>
</thead>
<tbody>
{% 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="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>
{% if s['credential_name'] %}
{{ 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">
<div class="invalid-feedback">Bitte eine gültige IP-Adresse eingeben.</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">
<label>Zugangsdaten</label>
<select name="credential_choice" class="credential-select" onchange="toggleNewCredentialFields(this)">
@@ -127,7 +136,7 @@
</div>
{% endif %}
{% 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 %}
</tbody>
</table>
@@ -152,6 +161,10 @@
<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>
<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">
<label>Zugangsdaten</label>
<select name="credential_choice" class="credential-select" onchange="toggleNewCredentialFields(this)">
@@ -300,6 +313,7 @@ function getCredentialInfo(form) {
function openTerminal(form) {
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);
activePasswordInput = passwordInput;
@@ -311,7 +325,7 @@ function openTerminal(form) {
PoeUI.openModal("terminalModal");
ensureTerminal();
term.reset();
document.getElementById("termTarget").innerText = `${username}@${host}`;
document.getElementById("termTarget").innerText = `${username}@${host}:${port}`;
setTermStatus("Verbinde…", "unknown");
setTimeout(() => fitAddon && fitAddon.fit(), 60);
@@ -321,7 +335,7 @@ function openTerminal(form) {
termSocket = new WebSocket(`${proto}//${location.host}/ws/ssh_terminal`);
termSocket.onopen = () => {
termSocket.send(JSON.stringify({ host, username, port: 22 }));
termSocket.send(JSON.stringify({ host, username, port }));
setTermStatus("Verbunden", "online");
};
termSocket.onmessage = (event) => term.write(event.data);