14 Commits
4 changed files with 310 additions and 8 deletions
+64 -3
View File
@@ -1,10 +1,10 @@
#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_bcrypt import Bcrypt
from cryptography.fernet import Fernet
from datetime import datetime
import sqlite3, glob, os, re
import sqlite3, glob, os, re, subprocess
app = Flask(__name__)
app.secret_key = "309cc4d5ce1fe7486ae25cbd232bbdfe6a72539c03f0127d372186dbdc0fc928"
@@ -15,6 +15,7 @@ login_manager.login_view = "login"
login_manager.init_app(app)
DB_PATH = "/srv/poe_manager/sqlite.db"
POE_SCRIPT = "/usr/local/bin/custom/poe.sh"
class User(UserMixin):
def __init__(self, id_, username, is_admin):
@@ -115,7 +116,7 @@ def index():
# Geräte aus DB laden
conn = sqlite3.connect("sqlite.db")
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()
# Intervall aus DB laden
@@ -458,6 +459,66 @@ def get_log():
except Exception as 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')
@login_required
def logs():
+10 -1
View File
@@ -29,7 +29,16 @@ def generate_ips_list():
switch_pass = ""
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__":
generate_ips_list()
+205 -3
View File
@@ -10,7 +10,14 @@
<div class="row g-3">
{% for d in devices %}
<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]) %}
title="{{ last_seen[d[0]] }}"
{% elif status[d[0]] == 'offline' %}
@@ -19,11 +26,11 @@
<div class="card-header">{{ d[1] }}</div>
<div class="card-body">
<span class="fw-bold" style="color:
{% if d[2] == 0 %}gray
{% if d[5] == 0 %}gray
{% elif status[d[0]] == 'online' %}green
{% else %}red
{% endif %};">
{% if d[2] == 0 %}
{% if d[5] == 0 %}
Deaktiviert
{% else %}
{% 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 intervalMilliseconds = intervalMinutes * 60 * 1000;
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) {
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));
}
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
setInterval(updateTimer, 1000);
@@ -90,4 +192,104 @@ document.addEventListener("DOMContentLoaded", () => {
});
</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 %}
+31 -1
View File
@@ -85,11 +85,41 @@ expect 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"
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; 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
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name ist erreichbar!" >> "$LOGFILE"
else