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:
@@ -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
|
||||
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
|
||||
|
||||
Jede Anlage, Bearbeitung, Löschung sowie jedes Aktivieren/Deaktivieren von
|
||||
|
||||
+33
-7
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}|"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -30,9 +30,10 @@ function disable_poe() {
|
||||
local switch_port=$2
|
||||
local username=$3
|
||||
local password=$4
|
||||
local ssh_port=${5:-22}
|
||||
expect <<EOF
|
||||
set timeout 5
|
||||
spawn ssh $username@$switch_ip
|
||||
spawn ssh -p $ssh_port $username@$switch_ip
|
||||
expect {
|
||||
"assword:" { send "$password\r"; exp_continue }
|
||||
"Press any key" { send "\r"; exp_continue }
|
||||
@@ -60,9 +61,10 @@ function enable_poe() {
|
||||
local switch_port=$2
|
||||
local username=$3
|
||||
local password=$4
|
||||
local ssh_port=${5:-22}
|
||||
expect <<EOF
|
||||
set timeout 5
|
||||
spawn ssh $username@$switch_ip
|
||||
spawn ssh -p $ssh_port $username@$switch_ip
|
||||
expect {
|
||||
"assword:" { send "$password\r"; exp_continue }
|
||||
"Press any key" { send "\r"; exp_continue }
|
||||
@@ -87,16 +89,16 @@ EOF
|
||||
|
||||
function manual_restart() {
|
||||
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
|
||||
continue
|
||||
fi
|
||||
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
|
||||
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"
|
||||
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') Manueller Neustart von $dev_name abgeschlossen." >> "$LOGFILE"
|
||||
else
|
||||
@@ -119,7 +121,7 @@ echo "" > "$LOGFILE"
|
||||
|
||||
while true; do
|
||||
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
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name ist erreichbar!" >> "$LOGFILE"
|
||||
else
|
||||
@@ -130,10 +132,10 @@ while true; do
|
||||
# (switch_ip leer) darf keinen SSH-Versuch mit leeren
|
||||
# Zugangsdaten auslösen.
|
||||
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"
|
||||
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"
|
||||
fi
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user