Compare commits
40 Commits
v1.0
...
df000eace9
| Author | SHA1 | Date | |
|---|---|---|---|
| df000eace9 | |||
| 74617a1e0f | |||
| bc80b5fcf1 | |||
| 0c82ce6100 | |||
| 4a093c1317 | |||
| ca389f488c | |||
| c39289c584 | |||
| a25462c78d | |||
| fe936ceca9 | |||
| ca7620abaf | |||
| 57005486cb | |||
| 80d596b94c | |||
| 2f50633eff | |||
| 84a24c811b | |||
| d3cdc5d3bd | |||
| 3a454780a2 | |||
| 0f090d594e | |||
| b602ebe089 | |||
| c0c3ed4dd7 | |||
| 85d7872934 | |||
| f057dc65c5 | |||
| d948096494 | |||
| 8b39425b38 | |||
| 12564647a2 | |||
| b7256868ee | |||
| dc9e42fadf | |||
| e7f6ff6ee1 | |||
| d38b8e4873 | |||
| 70d30e95fb | |||
| e99e17be3b | |||
| 26fb560aac | |||
| b95b1fbb9f | |||
| 02e7adf6b9 | |||
| 649d0598f9 | |||
| c067d26456 | |||
| 02efd0e40a | |||
| 58d1660073 | |||
| d60a525bfb | |||
| f45a346601 | |||
| 1e0df286db |
45
.gitignore
vendored
Normal file
45
.gitignore
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
# Alles ignorieren
|
||||
*
|
||||
|
||||
# .gitignore selbst tracken
|
||||
!.gitignore
|
||||
|
||||
# Virtuelle Umgebung ignorieren
|
||||
/srv/poe_manager/venv/
|
||||
__pycache__/
|
||||
|
||||
# Logfiles ignorieren
|
||||
*.log
|
||||
|
||||
# Systemd Services tracken
|
||||
!etc/
|
||||
!etc/systemd/
|
||||
!etc/systemd/system/
|
||||
!etc/systemd/system/rpi*.service
|
||||
!etc/systemd/system/rpi*.timer
|
||||
!etc/nginx/
|
||||
!etc/nginx/sites-enabled/
|
||||
!etc/nginx/sites-enabled/default
|
||||
|
||||
# Custom Scripts tracken
|
||||
!usr/
|
||||
!usr/local/
|
||||
!usr/local/bin/
|
||||
!usr/local/bin/custom/
|
||||
!usr/local/bin/custom/*
|
||||
|
||||
# Web-App Dateien im poe_manager tracken
|
||||
!/srv/
|
||||
!/srv/poe_manager/
|
||||
!/srv/poe_manager/*.*
|
||||
!/srv/poe_manager/templates/
|
||||
!/srv/poe_manager/templates/*.*
|
||||
!/srv/poe_manager/static/
|
||||
!/srv/poe_manager/static/css/
|
||||
!/srv/poe_manager/static/css/*
|
||||
!/srv/poe_manager/static/js/
|
||||
!/srv/poe_manager/static/js/*
|
||||
|
||||
|
||||
# Optional: SQLite DB ignorieren (falls du nicht willst, dass Passwörter im Repo landen)
|
||||
# /srv/poe_manager/sqlite.db
|
||||
45
README.md
45
README.md
@@ -1,15 +1,42 @@
|
||||
# Aruba PoE
|
||||
# PoE Manager Web-App
|
||||
|
||||
```
|
||||
Dieses Repo konfiguriert auf Ubuntu/Debian einen Service,
|
||||
dieser Service ist in der Lage IPs zu Pingen und entsprechend daran POE auf vorgegebenen Ports an einem Aruba Switch
|
||||
zu Aktivieren bzw. Deaktivieren.
|
||||
```
|
||||
Webbasierte Verwaltung und Monitoring von PoE-Devices und Switches.
|
||||
Die App ermöglicht:
|
||||
|
||||
Download:
|
||||
- Anzeige von Device-Status (Online/Offline)
|
||||
- Verwaltung von Devices und Switches
|
||||
- Einstellung des Prüfintervalls
|
||||
- Live-Log-Ansicht
|
||||
- Benutzerverwaltung mit Adminrechten
|
||||
|
||||
---
|
||||
|
||||
## **Installation (nach einem frischen Clone)**
|
||||
|
||||
```bash
|
||||
wget -qO- --header 'Authorization:token 9031f8d227dd83ba601680bf3a9f6c2d26c1a970' https://gitea.int.eertmoed.net/WiS/Aruba-PoE/archive/latest.tar.gz | tar xvz ; bash /root/aruba-poe/install.sh ;
|
||||
# Pakete installieren
|
||||
sudo apt update
|
||||
sudo apt install python3 python3-venv python3-pip nginx sqlite3 git nano -y
|
||||
|
||||
# Repo klonen
|
||||
git clone https://gitea.int.eertmoed.net/WiS/Aruba-PoE.git /srv/poe_manager
|
||||
cd /srv/poe_manager
|
||||
|
||||
# Virtuelle Umgebung erstellen
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
|
||||
# Abhängigkeiten installieren
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Datenbank initialisieren
|
||||
python create_db.py
|
||||
|
||||
# Admin-Benutzer erstellen
|
||||
python create_admin.py
|
||||
|
||||
# Web-App starten
|
||||
python app.py --host=0.0.0.0 --port=5000
|
||||
```
|
||||
|
||||
|
||||
|
||||
1
etc/nginx/sites-enabled/default
Symbolic link
1
etc/nginx/sites-enabled/default
Symbolic link
@@ -0,0 +1 @@
|
||||
/etc/nginx/sites-available/default
|
||||
@@ -3,7 +3,10 @@ Description=RPI Ping Check
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/local/bin/custom/poe.sh
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/srv/poe_manager
|
||||
ExecStart=/srv/poe_manager/venv/bin/python3 /srv/poe_manager/poe_wrapper.py
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
|
||||
416
srv/poe_manager/app.py
Normal file
416
srv/poe_manager/app.py
Normal file
@@ -0,0 +1,416 @@
|
||||
#u!/usr/bin/env python3
|
||||
from flask import Flask, render_template, request, redirect, url_for, flash
|
||||
from flask_login import LoginManager, login_user, login_required, logout_user, UserMixin, current_user
|
||||
from flask_bcrypt import Bcrypt
|
||||
from cryptography.fernet import Fernet
|
||||
import sqlite3
|
||||
import glob, os, re
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = "309cc4d5ce1fe7486ae25cbd232bbdfe6a72539c03f0127d372186dbdc0fc928"
|
||||
bcrypt = Bcrypt(app)
|
||||
|
||||
login_manager = LoginManager()
|
||||
login_manager.login_view = "login"
|
||||
login_manager.init_app(app)
|
||||
|
||||
DB_PATH = "/srv/poe_manager/sqlite.db"
|
||||
|
||||
class User(UserMixin):
|
||||
def __init__(self, id_, username, is_admin):
|
||||
self.id = id_
|
||||
self.username = username
|
||||
self.is_admin = is_admin
|
||||
|
||||
def get_interval_seconds():
|
||||
conn = get_db_connection()
|
||||
row = conn.execute("SELECT value FROM settings WHERE key='check_interval'").fetchone()
|
||||
conn.close()
|
||||
return int(row['value']) if row else 300
|
||||
|
||||
with open("/srv/poe_manager/fernet.key", "rb") as f:
|
||||
fernet = Fernet(f.read())
|
||||
|
||||
def encrypt_password(password: str) -> str:
|
||||
return fernet.encrypt(password.encode()).decode()
|
||||
|
||||
def decrypt_password(token: str) -> str:
|
||||
return fernet.decrypt(token.encode()).decode()
|
||||
|
||||
def get_db_connection():
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
def get_devices():
|
||||
"""
|
||||
Liefert eine Liste aller Devices aus der Datenbank als Dictionaries.
|
||||
"""
|
||||
conn = get_db_connection()
|
||||
devices = conn.execute("SELECT mac, rpi_ip, port, name, switch_hostname FROM devices").fetchall()
|
||||
conn.close()
|
||||
return devices
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
conn = get_db_connection()
|
||||
user = conn.execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchone()
|
||||
conn.close()
|
||||
if user:
|
||||
return User(user['id'], user['username'], user['is_admin'])
|
||||
return None
|
||||
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if request.method == 'POST':
|
||||
username = request.form['username']
|
||||
password = request.form['password']
|
||||
conn = get_db_connection()
|
||||
user = conn.execute("SELECT * FROM users WHERE username = ?", (username,)).fetchone()
|
||||
conn.close()
|
||||
if user and bcrypt.check_password_hash(user['password'], password):
|
||||
login_user(User(user['id'], user['username'], user['is_admin']))
|
||||
return redirect(url_for('index'))
|
||||
else:
|
||||
flash("Ungültiger Benutzername oder Passwort")
|
||||
return render_template('login.html')
|
||||
|
||||
@app.route('/logout')
|
||||
@login_required
|
||||
def logout():
|
||||
logout_user()
|
||||
return redirect(url_for('login'))
|
||||
|
||||
@app.route("/")
|
||||
@login_required
|
||||
def index():
|
||||
conn = sqlite3.connect("sqlite.db")
|
||||
c = conn.cursor()
|
||||
c.execute("SELECT mac, name FROM devices")
|
||||
devices = c.fetchall()
|
||||
conn.close()
|
||||
|
||||
# Status aus letztem Log ermitteln
|
||||
import glob, os
|
||||
log_files = glob.glob("/var/log/rpi-*.log")
|
||||
status_dict = {}
|
||||
if log_files:
|
||||
latest_log = max(log_files, key=os.path.getctime)
|
||||
with open(latest_log, "r") as f:
|
||||
for line in f:
|
||||
for dev in devices:
|
||||
if dev[1] in line:
|
||||
status_dict[dev[0]] = "online" if "erreichbar" in line else "offline"
|
||||
|
||||
return render_template("index.html", devices=devices, status=status_dict)
|
||||
|
||||
@app.route("/settings", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def settings():
|
||||
if not current_user.is_admin:
|
||||
flash("Nur Admins dürfen die Einstellungen ändern!")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
# Aktuellen Prüfintervall laden
|
||||
conn = sqlite3.connect("sqlite.db")
|
||||
c = conn.cursor()
|
||||
c.execute("SELECT value FROM settings WHERE key='interval'")
|
||||
row = c.fetchone()
|
||||
interval = int(row[0]) if row else 5 # Default 5 Minuten
|
||||
conn.close()
|
||||
|
||||
if request.method == "POST":
|
||||
new_interval = int(request.form["interval"])
|
||||
conn = sqlite3.connect("sqlite.db")
|
||||
c = conn.cursor()
|
||||
# upsert
|
||||
c.execute("INSERT INTO settings (key, value) VALUES (?, ?) "
|
||||
"ON CONFLICT(key) DO UPDATE SET value=excluded.value",
|
||||
("interval", new_interval))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# rpi-check.service neu starten
|
||||
import subprocess
|
||||
subprocess.run(["systemctl", "restart", "rpi-check.service"])
|
||||
|
||||
flash(f"Intervall auf {new_interval} Minuten gesetzt und Service neu gestartet!")
|
||||
return redirect(url_for("settings"))
|
||||
|
||||
return render_template("settings.html", interval=interval)
|
||||
|
||||
@app.route('/devices', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def devices():
|
||||
conn = get_db_connection()
|
||||
switches = conn.execute("SELECT hostname FROM switches").fetchall()
|
||||
|
||||
# Inline-Add
|
||||
if request.method == 'POST' and 'add_device' in request.form:
|
||||
if not current_user.is_admin:
|
||||
flash("Zugriff verweigert!")
|
||||
return redirect(url_for('devices'))
|
||||
mac = request.form['mac']
|
||||
rpi_ip = request.form['rpi_ip']
|
||||
port = request.form['port']
|
||||
name = request.form['name']
|
||||
switch_hostname = request.form['switch_hostname']
|
||||
try:
|
||||
conn.execute("INSERT INTO devices (mac, rpi_ip, port, name, switch_hostname) VALUES (?, ?, ?, ?, ?)",
|
||||
(mac, rpi_ip, port, name, switch_hostname))
|
||||
conn.commit()
|
||||
flash(f"Gerät {name} hinzugefügt.")
|
||||
except sqlite3.IntegrityError:
|
||||
flash("MAC existiert bereits oder Eingabefehler!")
|
||||
|
||||
# Inline-Edit
|
||||
if request.method == 'POST' and 'edit_device' in request.form:
|
||||
if not current_user.is_admin:
|
||||
flash("Zugriff verweigert!")
|
||||
return redirect(url_for('devices'))
|
||||
old_mac = request.form['old_mac']
|
||||
mac = request.form['mac']
|
||||
rpi_ip = request.form['rpi_ip']
|
||||
port = request.form['port']
|
||||
name = request.form['name']
|
||||
switch_hostname = request.form['switch_hostname']
|
||||
try:
|
||||
conn.execute("""
|
||||
UPDATE devices
|
||||
SET mac=?, rpi_ip=?, port=?, name=?, switch_hostname=?
|
||||
WHERE mac=?
|
||||
""", (mac, rpi_ip, port, name, switch_hostname, old_mac))
|
||||
conn.commit()
|
||||
flash(f"Gerät {name} aktualisiert.")
|
||||
except sqlite3.IntegrityError:
|
||||
flash("MAC existiert bereits oder Eingabefehler!")
|
||||
|
||||
# Inline-Delete
|
||||
if request.method == 'POST' and 'delete_device' in request.form:
|
||||
if not current_user.is_admin:
|
||||
flash("Zugriff verweigert!")
|
||||
return redirect(url_for('devices'))
|
||||
del_mac = request.form['delete_device']
|
||||
conn.execute("DELETE FROM devices WHERE mac=?", (del_mac,))
|
||||
conn.commit()
|
||||
flash(f"Gerät {del_mac} gelöscht.")
|
||||
|
||||
devices = conn.execute("""
|
||||
SELECT devices.mac, devices.rpi_ip, devices.port, devices.name, switches.hostname AS switch_hostname
|
||||
FROM devices
|
||||
JOIN switches ON devices.switch_hostname = switches.hostname
|
||||
""").fetchall()
|
||||
conn.close()
|
||||
interval_min = get_interval_seconds() // 60
|
||||
return render_template('devices.html', devices=devices, switches=switches)
|
||||
|
||||
@app.route('/switches', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def switches():
|
||||
conn = get_db_connection()
|
||||
|
||||
# Inline-Add
|
||||
if request.method == 'POST' and 'add_switch' in request.form:
|
||||
if not current_user.is_admin:
|
||||
flash("Zugriff verweigert!")
|
||||
return redirect(url_for('switches'))
|
||||
hostname = request.form['hostname']
|
||||
ip = request.form['ip']
|
||||
username = request.form['username']
|
||||
password = encrypt_password(request.form['password'])
|
||||
try:
|
||||
conn.execute("INSERT INTO switches (hostname, ip, username, password) VALUES (?, ?, ?, ?)",
|
||||
(hostname, ip, username, password))
|
||||
conn.commit()
|
||||
flash(f"Switch {hostname} hinzugefügt.")
|
||||
except sqlite3.IntegrityError:
|
||||
flash("Hostname existiert bereits oder Eingabefehler!")
|
||||
|
||||
# Inline-Edit
|
||||
if request.method == 'POST' and 'edit_switch' in request.form:
|
||||
if not current_user.is_admin:
|
||||
flash("Zugriff verweigert!")
|
||||
return redirect(url_for('switches'))
|
||||
old_hostname = request.form['old_hostname']
|
||||
hostname = request.form['hostname']
|
||||
ip = request.form['ip']
|
||||
username = request.form['username']
|
||||
password = encrypt_password(request.form['password'])
|
||||
try:
|
||||
conn.execute("""
|
||||
UPDATE switches
|
||||
SET hostname=?, ip=?, username=?, password=?
|
||||
WHERE hostname=?
|
||||
""", (hostname, ip, username, password, old_hostname))
|
||||
conn.commit()
|
||||
flash(f"Switch {hostname} aktualisiert.")
|
||||
except sqlite3.IntegrityError:
|
||||
flash("Hostname existiert bereits oder Eingabefehler!")
|
||||
|
||||
# Inline-Delete
|
||||
if request.method == 'POST' and 'delete_switch' in request.form:
|
||||
if not current_user.is_admin:
|
||||
flash("Zugriff verweigert!")
|
||||
return redirect(url_for('switches'))
|
||||
del_hostname = request.form['delete_switch']
|
||||
conn.execute("DELETE FROM switches WHERE hostname=?", (del_hostname,))
|
||||
conn.commit()
|
||||
flash(f"Switch {del_hostname} gelöscht.")
|
||||
|
||||
switches = conn.execute("SELECT hostname, ip, username FROM switches").fetchall()
|
||||
conn.close()
|
||||
return render_template('switche.html', switches=switches)
|
||||
|
||||
@app.route("/get_log")
|
||||
@login_required
|
||||
def get_log():
|
||||
log_files = glob.glob("/var/log/rpi-*.log")
|
||||
if not log_files:
|
||||
return "Keine Logfiles gefunden."
|
||||
|
||||
latest_log = max(log_files, key=os.path.getctime)
|
||||
|
||||
try:
|
||||
with open(latest_log, "r") as f:
|
||||
return f.read()
|
||||
except Exception as e:
|
||||
return f"Fehler beim Lesen des Logs: {e}"
|
||||
|
||||
@app.route('/logs')
|
||||
@login_required
|
||||
def logs():
|
||||
# Intervall aus DB laden
|
||||
conn = sqlite3.connect("sqlite.db")
|
||||
c = conn.cursor()
|
||||
c.execute("SELECT value FROM settings WHERE key='interval'")
|
||||
row = c.fetchone()
|
||||
interval = int(row[0]) if row else 5 # Default 5 Minuten
|
||||
conn.close()
|
||||
|
||||
# alle Logfiles mit Muster rpi-YYYYMMDDHHMMSS.log
|
||||
log_files = glob.glob("/var/log/rpi-*.log")
|
||||
if not log_files:
|
||||
return render_template('logs.html', log_content="Keine Logfiles gefunden.")
|
||||
|
||||
# das neuste Logfile auswählen
|
||||
latest_log = max(log_files, key=os.path.getctime)
|
||||
|
||||
try:
|
||||
with open(latest_log, "r") as f:
|
||||
log_content = f.read()
|
||||
except Exception as e:
|
||||
log_content = f"Fehler beim Lesen des Logs: {e}"
|
||||
|
||||
return render_template('logs.html', log_content=log_content, log_name=os.path.basename(latest_log), interval=interval)
|
||||
|
||||
def load_device_status():
|
||||
"""
|
||||
Liest das aktuellste rpi-Logfile und extrahiert den letzten Status jedes Devices.
|
||||
Gibt ein Dictionary zurück: {Device-Name: 'online'/'offline'}
|
||||
"""
|
||||
status = {}
|
||||
log_files = glob.glob("/var/log/rpi-*.log")
|
||||
if not log_files:
|
||||
return status
|
||||
|
||||
latest_log = max(log_files, key=os.path.getctime)
|
||||
|
||||
# Jede Zeile des Logs lesen
|
||||
with open(latest_log, "r") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
# Regex für Ping-Ergebnisse
|
||||
online_re = re.compile(r"(\S+) ist erreichbar!")
|
||||
offline_re = re.compile(r"(\S+) ist nicht erreichbar!")
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
m_online = online_re.search(line)
|
||||
m_offline = offline_re.search(line)
|
||||
if m_online:
|
||||
status[m_online.group(1)] = 'online'
|
||||
elif m_offline:
|
||||
status[m_offline.group(1)] = 'offline'
|
||||
|
||||
return status
|
||||
|
||||
@app.route("/users", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def users():
|
||||
if not current_user.is_admin:
|
||||
flash("Nur Admins dürfen Benutzer verwalten!")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
conn = sqlite3.connect("sqlite.db")
|
||||
conn.row_factory = sqlite3.Row
|
||||
c = conn.cursor()
|
||||
|
||||
if request.method == "POST":
|
||||
|
||||
# Neuen Benutzer hinzufügen
|
||||
if "add_user" in request.form:
|
||||
username = request.form["username"].strip()
|
||||
password = request.form["password"].strip()
|
||||
is_admin = int(request.form.get("is_admin", 0))
|
||||
|
||||
if username and password:
|
||||
pw_hash = bcrypt.generate_password_hash(password).decode("utf-8")
|
||||
try:
|
||||
c.execute(
|
||||
"INSERT INTO users (username, password, is_admin) VALUES (?, ?, ?)",
|
||||
(username, pw_hash, is_admin)
|
||||
)
|
||||
conn.commit()
|
||||
flash(f"Benutzer '{username}' erfolgreich angelegt!")
|
||||
except sqlite3.IntegrityError:
|
||||
flash("Benutzername existiert bereits!")
|
||||
else:
|
||||
flash("Username und Passwort dürfen nicht leer sein!")
|
||||
|
||||
# Rolle ändern
|
||||
elif "change_role" in request.form:
|
||||
user_id = request.form["user_id"]
|
||||
username = request.form.get("username", "").strip()
|
||||
is_admin = int(request.form.get("is_admin", 0))
|
||||
if username:
|
||||
c.execute(
|
||||
"UPDATE users SET username=?, is_admin=? WHERE id=?",
|
||||
(username, is_admin, user_id)
|
||||
)
|
||||
conn.commit()
|
||||
flash("Rolle und Username geändert!")
|
||||
else:
|
||||
flash("Username darf nicht leer sein!")
|
||||
|
||||
# Passwort ändern
|
||||
elif "change_password" in request.form:
|
||||
user_id = request.form["user_id"]
|
||||
new_password = request.form.get("new_password", "").strip()
|
||||
if new_password:
|
||||
pw_hash = bcrypt.generate_password_hash(new_password).decode("utf-8")
|
||||
c.execute(
|
||||
"UPDATE users SET password=? WHERE id=?",
|
||||
(pw_hash, user_id)
|
||||
)
|
||||
conn.commit()
|
||||
flash("Passwort erfolgreich geändert!")
|
||||
else:
|
||||
flash("Passwort darf nicht leer sein!")
|
||||
|
||||
# Benutzer löschen
|
||||
elif "delete_user" in request.form:
|
||||
user_id = request.form["delete_user"]
|
||||
c.execute("DELETE FROM users WHERE id=?", (user_id,))
|
||||
conn.commit()
|
||||
flash("Benutzer gelöscht!")
|
||||
|
||||
# Alle Benutzer laden (GET oder nach POST)
|
||||
c.execute("SELECT id, username, is_admin FROM users")
|
||||
users_list = c.fetchall()
|
||||
conn.close()
|
||||
|
||||
# Direkt rendern, Flash-Messages werden angezeigt
|
||||
return render_template("users.html", users=users_list)
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||
33
srv/poe_manager/create_admin.py
Executable file
33
srv/poe_manager/create_admin.py
Executable file
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env python3
|
||||
import sqlite3
|
||||
from getpass import getpass
|
||||
from flask_bcrypt import Bcrypt
|
||||
|
||||
DB_PATH = "/srv/poe_manager/sqlite.db"
|
||||
bcrypt = Bcrypt()
|
||||
|
||||
def main():
|
||||
username = input("Admin-Benutzername: ")
|
||||
password = getpass("Passwort: ")
|
||||
password_confirm = getpass("Passwort bestätigen: ")
|
||||
|
||||
if password != password_confirm:
|
||||
print("Passwörter stimmen nicht überein!")
|
||||
return
|
||||
|
||||
pw_hash = bcrypt.generate_password_hash(password).decode('utf-8')
|
||||
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
cur.execute("INSERT INTO users (username, password, is_admin) VALUES (?, ?, ?)",
|
||||
(username, pw_hash, 1))
|
||||
conn.commit()
|
||||
print(f"Admin-Benutzer '{username}' erfolgreich angelegt.")
|
||||
except sqlite3.IntegrityError:
|
||||
print("Benutzername existiert bereits!")
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
52
srv/poe_manager/create_db.py
Normal file
52
srv/poe_manager/create_db.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import sqlite3
|
||||
|
||||
conn = sqlite3.connect("sqlite.db")
|
||||
c = conn.cursor()
|
||||
|
||||
# Switches
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS switches (
|
||||
hostname TEXT PRIMARY KEY,
|
||||
ip TEXT NOT NULL,
|
||||
username TEXT NOT NULL,
|
||||
password TEXT NOT NULL
|
||||
);
|
||||
""")
|
||||
|
||||
# Devices
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS devices (
|
||||
mac TEXT PRIMARY KEY,
|
||||
rpi_ip TEXT NOT NULL,
|
||||
switch_hostname TEXT NOT NULL,
|
||||
port TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
is_active INTEGER DEFAULT 0,
|
||||
FOREIGN KEY (switch_hostname) REFERENCES switches(hostname)
|
||||
);
|
||||
""")
|
||||
|
||||
# Benutzer
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
is_admin INTEGER DEFAULT 0
|
||||
);
|
||||
""")
|
||||
|
||||
# Settings (z.B. Prüfintervall)
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
""")
|
||||
|
||||
# Standard-Setting: Prüfintervall 5 Minuten
|
||||
c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("interval_minutes", "5"))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print("Datenbank sqlite.db wurde initialisiert inklusive Settings.")
|
||||
1
srv/poe_manager/fernet.key
Normal file
1
srv/poe_manager/fernet.key
Normal file
@@ -0,0 +1 @@
|
||||
lKahy0lgo1McpoZtUDBwqddKkkRg7EnRnK3zdCL4_dw=
|
||||
36
srv/poe_manager/generate_ips.py
Normal file
36
srv/poe_manager/generate_ips.py
Normal file
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
import sqlite3
|
||||
import tempfile
|
||||
import os
|
||||
from app import decrypt_password, DB_PATH
|
||||
|
||||
def generate_ips_list():
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
|
||||
# Alle Switches laden
|
||||
switches = {row['hostname']: row for row in conn.execute("SELECT hostname, ip, username, password FROM switches")}
|
||||
|
||||
# Alle Geräte laden
|
||||
devices = conn.execute("SELECT mac, rpi_ip, port, name, switch_hostname FROM devices").fetchall()
|
||||
conn.close()
|
||||
|
||||
tmp = tempfile.NamedTemporaryFile(delete=False, mode='w', prefix='ips_', suffix='.list')
|
||||
tmp_path = tmp.name
|
||||
|
||||
for dev in devices:
|
||||
switch = switches.get(dev['switch_hostname'])
|
||||
if not switch:
|
||||
continue # Switch existiert nicht, überspringen
|
||||
|
||||
password = decrypt_password(switch['password'])
|
||||
# Format: IP-Device:Hostname-Device:IP-Switch:Hostname-Switch:Port-Switch:Username-Switch:Password-Switch
|
||||
line = f"{dev['rpi_ip']}:{dev['name']}:{switch['ip']}:{switch['hostname']}:{dev['port']}:{switch['username']}:{password}\n"
|
||||
tmp.write(line)
|
||||
|
||||
tmp.close()
|
||||
return tmp_path
|
||||
|
||||
if __name__ == "__main__":
|
||||
path = generate_ips_list()
|
||||
print(path) # optional, gibt die Tempdatei zurück
|
||||
9
srv/poe_manager/poe_wrapper.py
Normal file
9
srv/poe_manager/poe_wrapper.py
Normal file
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
# Virtuelle Umgebung aktivieren (Pfad zu deinem venv)
|
||||
venv_python = "/srv/poe_manager/venv/bin/python3"
|
||||
|
||||
# Poe.sh über bash ausführen, innerhalb der venv
|
||||
subprocess.run(["/bin/bash", "/usr/local/bin/custom/poe.sh"], env={"PATH": f"/srv/poe_manager/venv/bin:" + os.environ["PATH"]})
|
||||
13
srv/poe_manager/requirements.txt
Normal file
13
srv/poe_manager/requirements.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
bcrypt==5.0.0
|
||||
blinker==1.9.0
|
||||
cffi==2.0.0
|
||||
click==8.3.0
|
||||
cryptography==46.0.1
|
||||
Flask==3.1.2
|
||||
Flask-Bcrypt==1.0.1
|
||||
Flask-Login==0.6.3
|
||||
itsdangerous==2.2.0
|
||||
Jinja2==3.1.6
|
||||
MarkupSafe==3.0.2
|
||||
pycparser==2.23
|
||||
Werkzeug==3.1.3
|
||||
BIN
srv/poe_manager/sqlite.db
Normal file
BIN
srv/poe_manager/sqlite.db
Normal file
Binary file not shown.
6
srv/poe_manager/static/css/bootstrap.min.css
vendored
Normal file
6
srv/poe_manager/static/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
104
srv/poe_manager/static/css/style.css
Normal file
104
srv/poe_manager/static/css/style.css
Normal file
@@ -0,0 +1,104 @@
|
||||
body {
|
||||
padding: 20px;
|
||||
background: #3c4346;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.table-dark {
|
||||
background-color: #343a40 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: #f8f9fa;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
max-height: 600px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.mb-3 a.btn {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.device-card {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
text-align: center;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.device-name {
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-online {
|
||||
color: green;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-offline {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
max-width: 1024px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.navbar-container {
|
||||
display: flex;
|
||||
justify-content: space-between; /* Logo links, Navbar rechts */
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.navbar-buttons .btn {
|
||||
margin-right: 0.25rem; /* kleine Lücke zwischen Buttons */
|
||||
}
|
||||
|
||||
.navbar-logo img {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
height: 60px; /* Logo Höhe, kann angepasst werden */
|
||||
}
|
||||
|
||||
#log-container {
|
||||
position: relative;
|
||||
height: calc(100vh - 150px); /* Füllt die Seite minus Header */
|
||||
}
|
||||
|
||||
#log-box {
|
||||
height: 97%;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
font-family: monospace;
|
||||
border: 1px solid #dee2e6; /* Bootstrap-like border */
|
||||
padding: 1rem;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
#refresh-timer {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
font-size: 0.9em;
|
||||
color: gray;
|
||||
}
|
||||
7
srv/poe_manager/static/js/bootstrap.bundle.min.js
vendored
Normal file
7
srv/poe_manager/static/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
66
srv/poe_manager/templates/base.html
Normal file
66
srv/poe_manager/templates/base.html
Normal file
@@ -0,0 +1,66 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ title or "PoE Manager" }}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||
</head>
|
||||
<body class="p-4">
|
||||
|
||||
<div class="content-wrapper">
|
||||
|
||||
<!-- Logo + Navbar -->
|
||||
<div class="navbar-container">
|
||||
<div class="navbar-logo">
|
||||
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo">
|
||||
</div>
|
||||
<div class="navbar">
|
||||
<div class="navbar-buttons btn-group" role="group">
|
||||
<a href="{{ url_for('index') }}" class="btn btn-secondary">Dashboard</a>
|
||||
<a href="{{ url_for('devices') }}" class="btn btn-secondary">Devices</a>
|
||||
{% if current_user.is_admin %}
|
||||
<a href="{{ url_for('switches') }}" class="btn btn-secondary">Switches</a>
|
||||
<a href="{{ url_for('users') }}" class="btn btn-secondary">Users</a>
|
||||
<a href="{{ url_for('logs') }}" class="btn btn-secondary">Live-Log</a>
|
||||
<a href="{{ url_for('settings') }}" class="btn btn-secondary">Settings</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('logout') }}" class="btn btn-danger">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Flash Messages -->
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div id="flash-messages" class="mt-2">
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-info alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<!-- Content -->
|
||||
<div>
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
|
||||
<script>
|
||||
// Automatisch alle Alerts nach 5 Sekunden ausblenden
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
setTimeout(() => {
|
||||
const alerts = document.querySelectorAll('#flash-messages .alert');
|
||||
alerts.forEach(alert => {
|
||||
bootstrap.Alert.getOrCreateInstance(alert).close();
|
||||
});
|
||||
}, 5000);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
83
srv/poe_manager/templates/devices.html
Normal file
83
srv/poe_manager/templates/devices.html
Normal file
@@ -0,0 +1,83 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<h2>Devices</h2>
|
||||
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div class="mt-2 alert alert-info">
|
||||
{% for message in messages %}{{ message }}<br>{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% if current_user.is_admin %}
|
||||
<h4>Neues Gerät hinzufügen</h4>
|
||||
<form method="post" class="row g-2 mb-4">
|
||||
<input type="hidden" name="add_device" value="1">
|
||||
<div class="col"><input type="text" name="name" placeholder="Hostname" class="form-control" required></div>
|
||||
<div class="col"><input type="text" name="rpi_ip" placeholder="IP-Adresse" class="form-control" required></div>
|
||||
<div class="col"><input type="text" name="mac" placeholder="MAC-Adresse" class="form-control" required></div>
|
||||
<div class="col"><input type="text" name="port" placeholder="Port" class="form-control" required></div>
|
||||
<div class="col">
|
||||
<select name="switch_hostname" class="form-control" required>
|
||||
<option value="">Switch wählen</option>
|
||||
{% for sw in switches %}
|
||||
<option value="{{ sw['hostname'] }}">{{ sw['hostname'] }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col"><button class="btn btn-success w-100">Hinzufügen</button></div>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Hostname</th>
|
||||
<th>IP-Adresse</th>
|
||||
<th>MAC-Adresse</th>
|
||||
<th>Switch</th>
|
||||
<th>Port</th>
|
||||
{% if current_user.is_admin %}<th>Aktionen</th>{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for d in devices %}
|
||||
<tr>
|
||||
{% if current_user.is_admin %}
|
||||
<form method="post">
|
||||
<input type="hidden" name="edit_device" value="1">
|
||||
<input type="hidden" name="old_mac" value="{{ d['mac'] }}">
|
||||
<td><input name="name" value="{{ d['name'] }}" class="form-control" required></td>
|
||||
<td><input name="rpi_ip" value="{{ d['rpi_ip'] }}" class="form-control" required></td>
|
||||
<td><input name="mac" value="{{ d['mac'] }}" class="form-control" required></td>
|
||||
<td>
|
||||
<select name="switch_hostname" class="form-control" required>
|
||||
{% for sw in switches %}
|
||||
<option value="{{ sw['hostname'] }}" {% if sw['hostname']==d['switch_hostname'] %}selected{% endif %}>
|
||||
{{ sw['hostname'] }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td><input name="port" value="{{ d['port'] }}" class="form-control" required></td>
|
||||
<td class="d-flex gap-1">
|
||||
<button class="btn btn-primary btn-sm">Speichern</button>
|
||||
<button name="delete_device" value="{{ d['mac'] }}" class="btn btn-danger btn-sm"
|
||||
onclick="return confirm('Willst du das Gerät wirklich löschen?');">Löschen</button>
|
||||
</td>
|
||||
</form>
|
||||
{% else %}
|
||||
<td>{{ d['name'] }}</td>
|
||||
<td>{{ d['rpi_ip'] }}</td>
|
||||
<td>{{ d['mac'] }}</td>
|
||||
<td>{{ d['switch_hostname'] }}</td>
|
||||
<td>{{ d['port'] }}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
||||
20
srv/poe_manager/templates/index.html
Normal file
20
srv/poe_manager/templates/index.html
Normal file
@@ -0,0 +1,20 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<h2>Dashboard</h2>
|
||||
<div class="row row-cols-1 row-cols-md-6 g-3">
|
||||
{% for d in devices %}
|
||||
<div class="col">
|
||||
<div class="card text-center p-2">
|
||||
<div class="card-header">{{ d[1] }}</div>
|
||||
<div class="card-body">
|
||||
<span class="fw-bold" style="color: {% if status[d[0]]=='online' %}green{% else %}red{% endif %};">
|
||||
{% if status[d[0]] %}{{ status[d[0]]|capitalize }}{% else %}unbekannt{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
33
srv/poe_manager/templates/login.html
Normal file
33
srv/poe_manager/templates/login.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Login</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||
</head>
|
||||
<body class="p-5">
|
||||
|
||||
<h2>Login</h2>
|
||||
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div class="mt-2 alert alert-danger">
|
||||
{% for message in messages %}{{ message }}<br>{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="post" class="w-25">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Username</label>
|
||||
<input type="text" name="username" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Password</label>
|
||||
<input type="password" name="password" class="form-control" required>
|
||||
</div>
|
||||
<button class="btn btn-primary">Login</button>
|
||||
</form>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
46
srv/poe_manager/templates/logs.html
Normal file
46
srv/poe_manager/templates/logs.html
Normal file
@@ -0,0 +1,46 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<h2>Live Log</h2>
|
||||
|
||||
<div id="log-container">
|
||||
<div id="log-box"></div>
|
||||
<div id="refresh-timer">
|
||||
Nächstes Update in <span id="timer"></span> Sekunden
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const intervalMinutes = {{ interval | int }};
|
||||
const intervalMilliseconds = intervalMinutes * 60 * 1000;
|
||||
let lastUpdateTime = Date.now();
|
||||
|
||||
function fetchLog() {
|
||||
fetch("{{ url_for('get_log') }}")
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
const box = document.getElementById("log-box");
|
||||
const filteredLines = data
|
||||
.split("\n")
|
||||
.filter(line => !line.includes("ist erreichbar!"))
|
||||
.join("\n");
|
||||
box.innerText = filteredLines;
|
||||
box.scrollTop = box.scrollHeight;
|
||||
lastUpdateTime = Date.now();
|
||||
})
|
||||
.catch(err => console.error("Fehler beim Laden der Logs:", err));
|
||||
}
|
||||
|
||||
function updateTimer() {
|
||||
const now = Date.now();
|
||||
const remainingMs = intervalMilliseconds - (now - lastUpdateTime);
|
||||
const remainingSec = Math.max(Math.ceil(remainingMs / 1000), 0);
|
||||
document.getElementById("timer").innerText = remainingSec;
|
||||
}
|
||||
|
||||
setInterval(updateTimer, 1000);
|
||||
fetchLog();
|
||||
setInterval(fetchLog, intervalMilliseconds);
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
24
srv/poe_manager/templates/settings.html
Normal file
24
srv/poe_manager/templates/settings.html
Normal file
@@ -0,0 +1,24 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<h2>Settings</h2>
|
||||
|
||||
<form method="post" class="row g-2">
|
||||
<div class="col-auto">
|
||||
<label for="interval" class="form-label">Prüfintervall (Minuten):</label>
|
||||
<input type="number" name="interval" id="interval" class="form-control" value="{{ interval }}" min="1" required>
|
||||
</div>
|
||||
<div class="col-auto align-self-end">
|
||||
<button type="submit" class="btn btn-success">Speichern & Service neustarten</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div class="mt-2 alert alert-info">
|
||||
{% for message in messages %}{{ message }}<br>{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% endblock %}
|
||||
53
srv/poe_manager/templates/switche.html
Normal file
53
srv/poe_manager/templates/switche.html
Normal file
@@ -0,0 +1,53 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<h2>Switche</h2>
|
||||
|
||||
{% if current_user.is_admin %}
|
||||
<h4>Neuen Switch hinzufügen</h4>
|
||||
<form method="post" class="row g-2 mb-4">
|
||||
<input type="hidden" name="add_switch" value="1">
|
||||
<div class="col"><input type="text" name="hostname" placeholder="Hostname" class="form-control" required></div>
|
||||
<div class="col"><input type="text" name="ip" placeholder="IP-Adresse" class="form-control" required></div>
|
||||
<div class="col"><input type="text" name="username" placeholder="Username" class="form-control" required></div>
|
||||
<div class="col"><input type="password" name="password" placeholder="Password" class="form-control" required></div>
|
||||
<div class="col"><button class="btn btn-success w-100">Hinzufügen</button></div>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Hostname</th>
|
||||
<th>IP-Adresse</th>
|
||||
<th>Username</th>
|
||||
{% if current_user.is_admin %}<th>Aktionen</th>{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for s in switches %}
|
||||
<tr>
|
||||
{% if current_user.is_admin %}
|
||||
<form method="post">
|
||||
<input type="hidden" name="edit_switch" value="1">
|
||||
<input type="hidden" name="old_hostname" value="{{ s['hostname'] }}">
|
||||
<td><input name="hostname" value="{{ s['hostname'] }}" class="form-control" required></td>
|
||||
<td><input name="ip" value="{{ s['ip'] }}" class="form-control" required></td>
|
||||
<td><input name="username" value="{{ s['username'] }}" class="form-control" required></td>
|
||||
<td class="d-flex gap-1">
|
||||
<button class="btn btn-primary btn-sm">Speichern</button>
|
||||
<button name="delete_switch" value="{{ s['hostname'] }}" class="btn btn-danger btn-sm"
|
||||
onclick="return confirm('Willst du den Switch wirklich löschen?');">Löschen</button>
|
||||
</td>
|
||||
</form>
|
||||
{% else %}
|
||||
<td>{{ s['hostname'] }}</td>
|
||||
<td>{{ s['ip'] }}</td>
|
||||
<td>{{ s['username'] }}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
||||
167
srv/poe_manager/templates/users.html
Normal file
167
srv/poe_manager/templates/users.html
Normal file
@@ -0,0 +1,167 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<h2>Users</h2>
|
||||
|
||||
{% if current_user.is_admin %}
|
||||
<!-- Button Neues User-Popup -->
|
||||
<button class="btn btn-success mb-3" data-bs-toggle="modal" data-bs-target="#userModal" onclick="openUserModal()">Neuen Benutzer</button>
|
||||
{% endif %}
|
||||
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Rolle</th>
|
||||
{% if current_user.is_admin %}<th>Aktionen</th>{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for u in users %}
|
||||
<tr>
|
||||
<td>{{ u['username'] }}</td>
|
||||
<td>{% if u['is_admin'] %}Admin{% else %}User{% endif %}</td>
|
||||
{% if current_user.is_admin %}
|
||||
<td>
|
||||
<!-- Rolle ändern -->
|
||||
<button class="btn btn-primary btn-sm" data-bs-toggle="modal"
|
||||
data-bs-target="#roleModal"
|
||||
onclick="openRoleModal({{ u['id'] }}, '{{ u['username'] }}', {{ u['is_admin'] }})">
|
||||
Rolle ändern
|
||||
</button>
|
||||
|
||||
<!-- Passwort ändern -->
|
||||
<button class="btn btn-warning btn-sm" data-bs-toggle="modal"
|
||||
data-bs-target="#passwordModal"
|
||||
onclick="openPasswordModal({{ u['id'] }}, '{{ u['username'] }}')">
|
||||
Passwort ändern
|
||||
</button>
|
||||
|
||||
<!-- Benutzer löschen -->
|
||||
<form method="post" style="display:inline;">
|
||||
<button name="delete_user" value="{{ u['id'] }}" class="btn btn-danger btn-sm"
|
||||
onclick="return confirm('Willst du den Benutzer wirklich löschen?');">Löschen</button>
|
||||
</form>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Modal für neuen Benutzer -->
|
||||
<div class="modal fade" id="userModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="post" id="userForm">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Neuen Benutzer anlegen</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label>Username</label>
|
||||
<input type="text" name="username" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label>Passwort</label>
|
||||
<input type="password" name="password" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label>Rolle</label>
|
||||
<select name="is_admin" class="form-control">
|
||||
<option value="0">User</option>
|
||||
<option value="1">Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="add_user" value="1" class="btn btn-success">Benutzer anlegen</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal für Rolle ändern -->
|
||||
<div class="modal fade" id="roleModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="post" id="roleForm">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Rolle ändern</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="user_id" id="role_user_id">
|
||||
<div class="mb-3">
|
||||
<label>Username</label>
|
||||
<input type="text" name="username" id="role_username" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label>Rolle</label>
|
||||
<select name="is_admin" id="role_is_admin" class="form-control">
|
||||
<option value="0">User</option>
|
||||
<option value="1">Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="change_role" value="1" class="btn btn-primary">Rolle ändern</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal für Passwort ändern -->
|
||||
<div class="modal fade" id="passwordModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="post" id="passwordForm">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Passwort ändern</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="user_id" id="password_user_id">
|
||||
<div class="mb-3">
|
||||
<label>Username</label>
|
||||
<input type="text" name="username" id ="password_username" class="form-control" readonly>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label>Neues Passwort</label>
|
||||
<input type="password" name="new_password" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="change_password" value="1" class="btn btn-warning">Passwort ändern</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openUserModal() {
|
||||
document.getElementById('userForm').reset();
|
||||
}
|
||||
|
||||
function openRoleModal(user_id, username, is_admin) {
|
||||
document.getElementById('role_user_id').value = user_id;
|
||||
document.getElementById('role_username').value = username;
|
||||
document.getElementById('role_is_admin').value = is_admin;
|
||||
}
|
||||
|
||||
function openPasswordModal(user_id, username) {
|
||||
document.getElementById('password_user_id').value = user_id;
|
||||
document.getElementById('password_username').value = username;
|
||||
// Nur das Passwort-Feld zurücksetzen
|
||||
document.querySelector("#passwordForm input[name='new_password']").value = "";
|
||||
}
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,28 +1,40 @@
|
||||
#!/bin/bash
|
||||
|
||||
USER="admin"
|
||||
PASS="Expl0rer#2022"
|
||||
IP_FILE="/usr/local/bin/custom/ips.list"
|
||||
LOGFILE="/var/log/rpi-$(date '+%Y%m%d%H%M%S').log"
|
||||
|
||||
# Intervall aus DB (Sekunden) abrufen
|
||||
SLEEP=$(python3 - <<END
|
||||
import sqlite3
|
||||
conn = sqlite3.connect("/srv/poe_manager/sqlite.db")
|
||||
row = conn.execute("SELECT value FROM settings WHERE key='check_interval'").fetchone()
|
||||
conn.close()
|
||||
print(row[0] if row else 300)
|
||||
END
|
||||
)
|
||||
|
||||
# Umrechnung falls nötig
|
||||
SLEEP=${SLEEP:-300} # default 300 Sekunden
|
||||
|
||||
function disable_poe() {
|
||||
local switch_ip=$1
|
||||
local port=$2
|
||||
local switch_port=$2
|
||||
local username=$3
|
||||
local password=$4
|
||||
expect <<EOF
|
||||
set timeout 5
|
||||
spawn ssh $USER@$switch_ip
|
||||
spawn ssh $username@$switch_ip
|
||||
|
||||
expect {
|
||||
"assword:" { send "$PASS\r"; exp_continue }
|
||||
"assword:" { send "$password\r"; exp_continue }
|
||||
"Press any key" { send "\r"; exp_continue }
|
||||
-re ".*> $" { }
|
||||
}
|
||||
send "configure terminal\r"
|
||||
expect "(config)#"
|
||||
send "interface $port\r"
|
||||
expect "(eth-$port)#"
|
||||
send "interface $switch_port\r"
|
||||
expect "(eth-$switch_port)#"
|
||||
send "no power-over-ethernet\r"
|
||||
expect "(eth-$port)#"
|
||||
expect "(eth-$switch_port)#"
|
||||
send "exit\r"
|
||||
expect "(config)#"
|
||||
send "exit\r"
|
||||
@@ -37,22 +49,24 @@ EOF
|
||||
|
||||
function enable_poe() {
|
||||
local switch_ip=$1
|
||||
local port=$2
|
||||
local switch_port=$2
|
||||
local username=$3
|
||||
local password=$4
|
||||
expect <<EOF
|
||||
set timeout 5
|
||||
spawn ssh $USER@$switch_ip
|
||||
spawn ssh $username@$switch_ip
|
||||
|
||||
expect {
|
||||
"assword:" { send "$PASS\r"; exp_continue }
|
||||
"assword:" { send "$password\r"; exp_continue }
|
||||
"Press any key" { send "\r"; exp_continue }
|
||||
-re ".*> $" { }
|
||||
}
|
||||
send "configure terminal\r"
|
||||
expect "(config)#"
|
||||
send "interface $port\r"
|
||||
expect "(eth-$port)#"
|
||||
send "interface $switch_port\r"
|
||||
expect "(eth-$switch_port)#"
|
||||
send "power-over-ethernet\r"
|
||||
expect "(eth-$port)#"
|
||||
expect "(eth-$switch_port)#"
|
||||
send "exit\r"
|
||||
expect "(config)#"
|
||||
send "exit\r"
|
||||
@@ -68,18 +82,20 @@ EOF
|
||||
echo "" > $LOGFILE
|
||||
while true; do
|
||||
echo "--------------------------------------------------------------------" >> $LOGFILE
|
||||
while IFS=: read -r ip switch port hap; do
|
||||
ping -c 1 -W 2 $ip &> /dev/null
|
||||
IP_FILE=$(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
|
||||
ping -c 1 -W 2 "$rpi_ip" &> /dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $hap ist nicht erreichbar!" >> $LOGFILE
|
||||
disable_poe $switch $port
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $hap PoE auf Port $port für IP $ip am Switch $switch deaktiviert." >> $LOGFILE
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name ist nicht erreichbar!" >> $LOGFILE
|
||||
disable_poe "$switch_ip" "$switch_port" "$switch_user" "$switch_pass"
|
||||
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 $port
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $hap PoE auf Port $port für IP $ip am Switch $switch aktiviert." >> $LOGFILE
|
||||
enable_poe "$switch_ip" "$switch_port" "$switch_user" "$switch_pass"
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name PoE auf Port $switch_port am Switch $switch_hostname aktiviert." >> $LOGFILE
|
||||
else
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $hap ist erreichbar!" >> $LOGFILE
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name ist erreichbar!" >> $LOGFILE
|
||||
fi
|
||||
done < "$IP_FILE"
|
||||
sleep 300
|
||||
rm -f "$IP_FILE"
|
||||
sleep $SLEEP
|
||||
done
|
||||
Reference in New Issue
Block a user