14 Commits
4 changed files with 310 additions and 8 deletions
+64 -3
View File
@@ -1,10 +1,10 @@
#u!/usr/bin/env python3 #u!/usr/bin/env python3
from flask import Flask, render_template, request, redirect, url_for, flash from flask import Flask, render_template, request, redirect, url_for, flash, jsonify
from flask_login import LoginManager, login_user, login_required, logout_user, UserMixin, current_user from flask_login import LoginManager, login_user, login_required, logout_user, UserMixin, current_user
from flask_bcrypt import Bcrypt from flask_bcrypt import Bcrypt
from cryptography.fernet import Fernet from cryptography.fernet import Fernet
from datetime import datetime from datetime import datetime
import sqlite3, glob, os, re import sqlite3, glob, os, re, subprocess
app = Flask(__name__) app = Flask(__name__)
app.secret_key = "309cc4d5ce1fe7486ae25cbd232bbdfe6a72539c03f0127d372186dbdc0fc928" app.secret_key = "309cc4d5ce1fe7486ae25cbd232bbdfe6a72539c03f0127d372186dbdc0fc928"
@@ -15,6 +15,7 @@ login_manager.login_view = "login"
login_manager.init_app(app) login_manager.init_app(app)
DB_PATH = "/srv/poe_manager/sqlite.db" DB_PATH = "/srv/poe_manager/sqlite.db"
POE_SCRIPT = "/usr/local/bin/custom/poe.sh"
class User(UserMixin): class User(UserMixin):
def __init__(self, id_, username, is_admin): def __init__(self, id_, username, is_admin):
@@ -115,7 +116,7 @@ def index():
# Geräte aus DB laden # Geräte aus DB laden
conn = sqlite3.connect("sqlite.db") conn = sqlite3.connect("sqlite.db")
c = conn.cursor() c = conn.cursor()
c.execute("SELECT mac, name, is_active FROM devices ORDER BY name ASC") c.execute("SELECT mac, name, rpi_ip, switch_hostname, port, is_active FROM devices ORDER BY name ASC")
devices = c.fetchall() devices = c.fetchall()
# Intervall aus DB laden # Intervall aus DB laden
@@ -458,6 +459,66 @@ def get_log():
except Exception as e: except Exception as e:
return f"Fehler beim Lesen des Logs: {e}" return f"Fehler beim Lesen des Logs: {e}"
@app.route("/restart/<mac>", methods=["POST"])
@login_required
def restart_device(mac):
conn = sqlite3.connect("sqlite.db")
conn.row_factory = sqlite3.Row
c = conn.cursor()
c.execute("""
SELECT
mac,
name,
is_active
FROM devices
WHERE mac=?
""", (mac,))
device = c.fetchone()
conn.close()
if not device:
return jsonify({
"success": False,
"message": "Gerät wurde nicht gefunden."
}), 404
if device["is_active"] == 0:
return jsonify({
"success": False,
"message": f"{device['name']} ist deaktiviert."
}), 400
try:
subprocess.Popen(
[
"/bin/bash",
POE_SCRIPT,
"restart",
mac
],
env={
"PATH": "/srv/poe_manager/venv/bin:" + os.environ["PATH"]
},
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
return jsonify({
"success": True,
"device": device["name"]
})
except Exception as e:
return jsonify({
"success": False,
"message": str(e)
}), 500
@app.route('/logs') @app.route('/logs')
@login_required @login_required
def logs(): def logs():
+10 -1
View File
@@ -29,7 +29,16 @@ def generate_ips_list():
switch_pass = "" switch_pass = ""
port = dev['port'] or "" port = dev['port'] or ""
print(f"{dev['rpi_ip']}:{dev['name']}:{switch_ip}:{dev['switch_hostname'] or 'kein Switch'}:{port}:{switch_user}:{switch_pass}") print(
f"{dev['rpi_ip']}|"
f"{dev['name']}|"
f"{switch_ip}|"
f"{dev['switch_hostname'] or 'kein Switch'}|"
f"{port}|"
f"{switch_user}|"
f"{switch_pass}|"
f"{dev['mac']}"
)
if __name__ == "__main__": if __name__ == "__main__":
generate_ips_list() generate_ips_list()
+205 -3
View File
@@ -10,7 +10,14 @@
<div class="row g-3"> <div class="row g-3">
{% for d in devices %} {% for d in devices %}
<div class="col-6 col-md-4 col-lg-3 col-xl-2"> <div class="col-6 col-md-4 col-lg-3 col-xl-2">
<div class="card text-center p-2" <div class="card text-center p-2 device-card"
style="cursor:pointer;"
data-mac="{{ d[0] }}"
data-name="{{ d[1] }}"
data-ip="{{ d[2] }}"
data-switch="{{ d[3] }}"
data-port="{{ d[4] }}"
data-active="{{ d[5] }}"
{% if last_seen.get(d[0]) %} {% if last_seen.get(d[0]) %}
title="{{ last_seen[d[0]] }}" title="{{ last_seen[d[0]] }}"
{% elif status[d[0]] == 'offline' %} {% elif status[d[0]] == 'offline' %}
@@ -19,11 +26,11 @@
<div class="card-header">{{ d[1] }}</div> <div class="card-header">{{ d[1] }}</div>
<div class="card-body"> <div class="card-body">
<span class="fw-bold" style="color: <span class="fw-bold" style="color:
{% if d[2] == 0 %}gray {% if d[5] == 0 %}gray
{% elif status[d[0]] == 'online' %}green {% elif status[d[0]] == 'online' %}green
{% else %}red {% else %}red
{% endif %};"> {% endif %};">
{% if d[2] == 0 %} {% if d[5] == 0 %}
Deaktiviert Deaktiviert
{% else %} {% else %}
{% if status[d[0]] %}{{ status[d[0]]|capitalize }}{% else %}Unbekannt{% endif %} {% if status[d[0]] %}{{ status[d[0]]|capitalize }}{% else %}Unbekannt{% endif %}
@@ -39,6 +46,17 @@ document.addEventListener("DOMContentLoaded", () => {
const intervalMinutes = {{ interval | int }}; // aus DB const intervalMinutes = {{ interval | int }}; // aus DB
const intervalMilliseconds = intervalMinutes * 60 * 1000; const intervalMilliseconds = intervalMinutes * 60 * 1000;
let lastUpdateTime = Date.now(); let lastUpdateTime = Date.now();
let selectedCard = null;
let selectedMac = null;
let selectedName = null;
const deviceModal = new bootstrap.Modal(
document.getElementById("deviceModal")
);
const restartModal = new bootstrap.Modal(
document.getElementById("restartModal")
);
function parseLogTimestamp(ts) { function parseLogTimestamp(ts) {
const parts = ts.match(/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/); const parts = ts.match(/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/);
@@ -82,6 +100,90 @@ document.addEventListener("DOMContentLoaded", () => {
.catch(err => console.error("Fehler beim Laden der Logs:", err)); .catch(err => console.error("Fehler beim Laden der Logs:", err));
} }
document.querySelectorAll(".device-card").forEach(card => {
const mac = card.dataset.mac;
const restartKey = "restart_" + mac;
if (sessionStorage.getItem(restartKey)) {
const span = card.querySelector(".fw-bold");
if (span.innerText.trim().toLowerCase() === "online") {
sessionStorage.removeItem(restartKey);
}
else {
span.innerHTML =
'<span class="spinner-border spinner-border-sm me-2"></span>Restarting...';
span.style.color = "#ffc107";
document.getElementById("deviceStatus").innerHTML =
'<span class="spinner-border spinner-border-sm me-2"></span>Restarting...';
}
}
card.addEventListener("click", function () {
if (this.dataset.active == "0") {
return;
}
if (sessionStorage.getItem("restart_" + this.dataset.mac)) {
return;
}
selectedCard = this;
selectedMac = this.dataset.mac;
selectedName = this.dataset.name;
document.getElementById("deviceModalTitle").innerText =
this.dataset.name;
document.getElementById("deviceIp").innerText =
this.dataset.ip || "-";
document.getElementById("deviceSwitch").innerText =
this.dataset.switch || "-";
document.getElementById("devicePort").innerText =
this.dataset.port || "-";
document.getElementById("deviceStatus").innerText =
this.querySelector(".fw-bold").innerText;
deviceModal.show();
});
});
document
.getElementById("restartButton")
.addEventListener("click", function () {
deviceModal.hide();
document.getElementById("restartTitle").innerText =
selectedName + " neu starten?";
restartModal.show();
});
document
.getElementById("confirmRestart")
.addEventListener("click", function () {
const button = this;
button.disabled = true;
fetch("/restart/" + encodeURIComponent(selectedMac), {
method: "POST"
})
.then(response => response.json())
.then(data => {
button.disabled = false;
if (!data.success) {
alert(data.message || "Neustart konnte nicht gestartet werden.");
return;
}
restartModal.hide();
deviceModal.hide();
sessionStorage.setItem(
"restart_" + selectedMac,
"1"
);
const span = selectedCard.querySelector(".fw-bold");
span.innerHTML =
'<span class="spinner-border spinner-border-sm me-2"></span>Restarting...';
span.style.color = "#ffc107";
})
.catch(error => {
button.disabled = false;
alert(
"Fehler beim Starten des Neustarts.\n\n" +
error
);
});
});
// Timer alle 1 Sekunde aktualisieren // Timer alle 1 Sekunde aktualisieren
setInterval(updateTimer, 1000); setInterval(updateTimer, 1000);
@@ -90,4 +192,104 @@ document.addEventListener("DOMContentLoaded", () => {
}); });
</script> </script>
<!-- ========================================================= -->
<!-- Geräteinformationen -->
<!-- ========================================================= -->
<div class="modal fade"
id="deviceModal"
tabindex="-1"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5
id="deviceModalTitle"
class="modal-title">
</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Schließen">
</button>
</div>
<div class="modal-body">
<table class="table table-sm align-middle mb-0">
<tr>
<th style="width:120px;">IP-Adresse</th>
<td id="deviceIp"></td>
</tr>
<tr>
<th>Switch</th>
<td id="deviceSwitch"></td>
</tr>
<tr>
<th>Port</th>
<td id="devicePort"></td>
</tr>
<tr>
<th>Status</th>
<td id="deviceStatus"></td>
</tr>
</table>
</div>
<div class="modal-footer">
<button
id="restartButton"
class="btn btn-warning">
🔄 Neustarten
</button>
<button
class="btn btn-secondary"
data-bs-dismiss="modal">
Schließen
</button>
</div>
</div>
</div>
</div>
<!-- ========================================================= -->
<!-- Neustart bestätigen -->
<!-- ========================================================= -->
<div class="modal fade"
id="restartModal"
tabindex="-1"
aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5
id="restartTitle"
class="modal-title">
</h5>
</div>
<div class="modal-body">
<p class="mb-2">
Der Neustart erfolgt durch einen kurzen
PoE-Reset.
</p>
<p class="mb-0">
Der Pi wird für wenige Sekunden
vom Netzwerk getrennt.
</p>
</div>
<div class="modal-footer">
<button
class="btn btn-secondary"
data-bs-dismiss="modal">
Abbrechen
</button>
<button
id="confirmRestart"
class="btn btn-danger">
Neustarten
</button>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}
+31 -1
View File
@@ -85,11 +85,41 @@ expect eof
EOF 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
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_port" ] && [ "$switch_port" != "None" ]; then
disable_poe "$switch_ip" "$switch_port" "$switch_user" "$switch_pass"
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"
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
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name besitzt keinen PoE-Port." >> "$LOGFILE"
fi
exit 0
done
}
if [[ "$1" == "restart" ]]; then
if [[ -z "$2" ]]; then
echo "MAC-Adresse fehlt."
exit 1
fi
manual_restart "$2"
exit 0
fi
echo "" > "$LOGFILE" 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; do 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; 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