Neu: DHCP-Reservierungen aus Client-Stammdaten (Systemeinstellungen -> DHCP)
- Neue Unterseite unter Einstellungen (eigenes Rechtepaar
settings_dhcp.view/settings_dhcp.edit, in PERMISSIONS/NAV_ITEMS/
_nav_key_visible integriert, Kill-Switch über settings_group.view
greift wie bei den anderen Einstellungen-Unterpunkten).
- Rein lesende Installations-/Status-Erkennung (shutil.which("dhcpd"),
systemctl is-active isc-dhcp-server) — die App installiert/startet nie
selbst einen DHCP-Dienst, sondern zeigt bei fehlender Installation den
passenden manuellen Befehl an.
- Konfigurierbare Netzwerkparameter (Interface/Subnet/Netzmaske/Range/
DNS/Domain/Lease-Zeiten/Ausgabepfad), gespeichert als dhcp_*-Schlüssel
in der bestehenden settings-Tabelle.
- Reservierungen werden aus aktiven Geräten mit gültiger MAC+IP generiert
(Hostname aus Gerätename abgeleitet, Kollisionen automatisch
durchnummeriert, Geräte ohne MAC/IP werden übersprungen).
- "In Datei schreiben" (nur mit settings_dhcp.edit) schreibt eine
separate Include-Datei statt der aktiven dhcpd.conf; kein automatischer
Dienst-Reload/-Restart durch die App.
- Live getestet: Status-Erkennung, Config speichern, Reservierungs-
Generierung inkl. Namenskollisionen, Datei-Schreiben in sicheren
Testpfad, View-only-Gating (Formular ausgeblendet, POST blockiert),
Kill-Switch über settings_group.view.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -212,6 +212,37 @@ sich per SSH mit einem Switch verbindet: dem Web-Terminal (Verbindungstest),
|
|||||||
`expect`-Aufrufen in `poe.sh` selbst (`ssh -p <port> ...`) für automatische
|
`expect`-Aufrufen in `poe.sh` selbst (`ssh -p <port> ...`) für automatische
|
||||||
wie manuelle PoE-Neustarts.
|
wie manuelle PoE-Neustarts.
|
||||||
|
|
||||||
|
## DHCP-Reservierungen (Systemeinstellungen → DHCP)
|
||||||
|
|
||||||
|
Erzeugt aus den vorhandenen Client-Stammdaten (MAC + IP + Name) automatisch
|
||||||
|
feste DHCP-Reservierungen, ohne die Daten ein zweites Mal pflegen zu müssen.
|
||||||
|
Eigenes Rechtepaar `settings_dhcp.view`/`settings_dhcp.edit`, gespiegelt in
|
||||||
|
der Navbar unter Einstellungen.
|
||||||
|
|
||||||
|
- **Status-Erkennung** (rein lesend): prüft per `shutil.which("dhcpd")` und
|
||||||
|
`systemctl is-active isc-dhcp-server`, ob ein ISC-DHCP-Server installiert
|
||||||
|
bzw. aktiv ist. Die App installiert und startet **nie selbst** einen
|
||||||
|
DHCP-Dienst — ist keiner gefunden, zeigt die Seite lediglich den passenden
|
||||||
|
manuellen Befehl (`sudo apt-get install isc-dhcp-server`) an. Grund: ein
|
||||||
|
unerwartet aktiver oder fehlkonfigurierter DHCP-Server kann ein ganzes
|
||||||
|
Netzwerksegment stören, das soll ausschließlich eine bewusste,
|
||||||
|
eigenständige Admin-Entscheidung außerhalb der App bleiben.
|
||||||
|
- **Konfiguration**: Interface, Subnet, Netzmaske, Range, DNS-Server,
|
||||||
|
Domain, Lease-Zeiten sowie der Ausgabepfad der generierten Datei — als
|
||||||
|
`dhcp_*`-Schlüssel in der bestehenden `settings`-Tabelle gespeichert.
|
||||||
|
- **Reservierungen**: für jedes aktive Gerät mit gültiger MAC + IP wird ein
|
||||||
|
`host { hardware ethernet ...; fixed-address ...; }`-Block erzeugt; der
|
||||||
|
Hostname wird aus dem Gerätenamen abgeleitet (nur `a-z0-9-`, Kollisionen
|
||||||
|
bekommen automatisch ein `-2`/`-3`-Suffix). Geräte ohne MAC/IP werden mit
|
||||||
|
Hinweis übersprungen statt eine kaputte Reservierung zu erzeugen.
|
||||||
|
- **Schreiben**: „In Datei schreiben“ (nur mit `settings_dhcp.edit`)
|
||||||
|
schreibt die generierte Subnet-/Reservierungs-Deklaration in eine
|
||||||
|
**separate Include-Datei** (Standard-Vorschlag
|
||||||
|
`/etc/dhcp/dhcpd.conf.d/aruba-poe-reservations.conf`) — **nicht** in die
|
||||||
|
eigentliche `dhcpd.conf`. Einmalig manuell per `include "<Pfad>";`
|
||||||
|
einbinden. Die App reloadet/restartet den DHCP-Dienst dabei bewusst
|
||||||
|
**nicht automatisch** — dieselbe Begründung wie bei der Status-Erkennung.
|
||||||
|
|
||||||
## Änderungslog
|
## Änderungslog
|
||||||
|
|
||||||
Jede Anlage, Bearbeitung, Löschung sowie jedes Aktivieren/Deaktivieren von
|
Jede Anlage, Bearbeitung, Löschung sowie jedes Aktivieren/Deaktivieren von
|
||||||
|
|||||||
+200
-1
@@ -17,7 +17,7 @@ from cryptography.fernet import Fernet
|
|||||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||||
from cryptography.hazmat.primitives import hashes
|
from cryptography.hazmat.primitives import hashes
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import base64, sqlite3, glob, json, os, re, secrets, subprocess, threading, time, traceback
|
import base64, shutil, sqlite3, glob, json, os, re, secrets, subprocess, threading, time, traceback
|
||||||
import paramiko
|
import paramiko
|
||||||
|
|
||||||
|
|
||||||
@@ -218,6 +218,12 @@ PERMISSIONS = {
|
|||||||
# L = Export (nur lesend), Ä = Import (verändert Bestand).
|
# L = Export (nur lesend), Ä = Import (verändert Bestand).
|
||||||
"rows": {"view": "settings_importexport.view", "edit": "settings_importexport.edit"},
|
"rows": {"view": "settings_importexport.view", "edit": "settings_importexport.edit"},
|
||||||
},
|
},
|
||||||
|
"settings_dhcp": {
|
||||||
|
"label": "DHCP",
|
||||||
|
# L = Konfiguration/Status/Vorschau ansehen, Ä = Konfiguration
|
||||||
|
# speichern und die generierte Reservierungsdatei schreiben.
|
||||||
|
"rows": {"view": "settings_dhcp.view", "edit": "settings_dhcp.edit"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -242,6 +248,8 @@ PERMISSION_LABELS = {
|
|||||||
"groups.edit": "Gruppen ändern (inkl. Löschen)",
|
"groups.edit": "Gruppen ändern (inkl. Löschen)",
|
||||||
"settings_system.view": "Systemeinstellungen lesen", "settings_system.edit": "Systemeinstellungen ändern",
|
"settings_system.view": "Systemeinstellungen lesen", "settings_system.edit": "Systemeinstellungen ändern",
|
||||||
"settings_importexport.view": "Daten exportieren", "settings_importexport.edit": "Daten importieren",
|
"settings_importexport.view": "Daten exportieren", "settings_importexport.edit": "Daten importieren",
|
||||||
|
"settings_dhcp.view": "DHCP-Status und -Konfiguration lesen",
|
||||||
|
"settings_dhcp.edit": "DHCP-Konfiguration speichern und Reservierungsdatei schreiben",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Aus der verschachtelten Struktur abgeleitet: alle vergebbaren Keys, sowie
|
# Aus der verschachtelten Struktur abgeleitet: alle vergebbaren Keys, sowie
|
||||||
@@ -286,6 +294,7 @@ NAV_ITEMS = [
|
|||||||
{"key": "groups", "label": "Gruppen", "icon": "groups", "endpoint": "groups"},
|
{"key": "groups", "label": "Gruppen", "icon": "groups", "endpoint": "groups"},
|
||||||
{"key": "settings_system", "label": "Systemeinstellungen", "icon": "sliders", "endpoint": "settings"},
|
{"key": "settings_system", "label": "Systemeinstellungen", "icon": "sliders", "endpoint": "settings"},
|
||||||
{"key": "settings_importexport", "label": "Im-/Export", "icon": "transfer", "endpoint": "settings_import_export"},
|
{"key": "settings_importexport", "label": "Im-/Export", "icon": "transfer", "endpoint": "settings_import_export"},
|
||||||
|
{"key": "settings_dhcp", "label": "DHCP", "icon": "network", "endpoint": "settings_dhcp"},
|
||||||
]},
|
]},
|
||||||
{"key": "logs_group", "label": "Logs", "icon": "terminal", "children": [
|
{"key": "logs_group", "label": "Logs", "icon": "terminal", "children": [
|
||||||
{"key": "logs_live", "label": "Live", "icon": "terminal", "endpoint": "logs"},
|
{"key": "logs_live", "label": "Live", "icon": "terminal", "endpoint": "logs"},
|
||||||
@@ -313,6 +322,8 @@ def _nav_key_visible(key, user):
|
|||||||
return user.can_view_settings_system
|
return user.can_view_settings_system
|
||||||
if key == "settings_importexport":
|
if key == "settings_importexport":
|
||||||
return user.can_view_settings_importexport
|
return user.can_view_settings_importexport
|
||||||
|
if key == "settings_dhcp":
|
||||||
|
return user.can_view_settings_dhcp
|
||||||
if key == "logs_live":
|
if key == "logs_live":
|
||||||
return user.can_view_live_log
|
return user.can_view_live_log
|
||||||
if key == "logs_activity":
|
if key == "logs_activity":
|
||||||
@@ -415,6 +426,10 @@ class User(UserMixin):
|
|||||||
def can_view_settings_importexport(self):
|
def can_view_settings_importexport(self):
|
||||||
return self.has_permission("settings_importexport.view")
|
return self.has_permission("settings_importexport.view")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def can_view_settings_dhcp(self):
|
||||||
|
return self.has_permission("settings_dhcp.view")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def full_name(self):
|
def full_name(self):
|
||||||
parts = [p for p in (self.first_name, self.last_name) if p]
|
parts = [p for p in (self.first_name, self.last_name) if p]
|
||||||
@@ -1168,6 +1183,190 @@ def settings_import_export():
|
|||||||
return render_template("settings_import_export.html")
|
return render_template("settings_import_export.html")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# DHCP-Reservierungen
|
||||||
|
#
|
||||||
|
# Bewusste Beschränkung: die App erkennt nur, ob ein DHCP-Server installiert
|
||||||
|
# ist und generiert eine reine Reservierungs-Include-Datei aus den
|
||||||
|
# Client-Daten — sie installiert NIE selbst ein Paket und startet/reloadet
|
||||||
|
# NIE den DHCP-Dienst. Ein fehlkonfigurierter/unerwartet aktiver DHCP-Server
|
||||||
|
# kann ein ganzes Netz stören; Installation und Dienst-Reload bleiben daher
|
||||||
|
# bewusst manuelle, außerhalb der App liegende Admin-Schritte (Anleitung
|
||||||
|
# dazu wird auf der Seite angezeigt).
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
DHCP_DEFAULTS = {
|
||||||
|
"dhcp_interface": "eth0",
|
||||||
|
"dhcp_subnet": "192.168.1.0",
|
||||||
|
"dhcp_netmask": "255.255.255.0",
|
||||||
|
"dhcp_range_start": "192.168.1.100",
|
||||||
|
"dhcp_range_end": "192.168.1.200",
|
||||||
|
"dhcp_dns": "192.168.1.1",
|
||||||
|
"dhcp_domain": "local",
|
||||||
|
"dhcp_lease_default": "600",
|
||||||
|
"dhcp_lease_max": "7200",
|
||||||
|
"dhcp_output_path": "/etc/dhcp/dhcpd.conf.d/aruba-poe-reservations.conf",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _load_dhcp_settings():
|
||||||
|
return {key: get_setting(key, default) for key, default in DHCP_DEFAULTS.items()}
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_dhcp_status():
|
||||||
|
"""Rein lesende Erkennung, ob ein ISC-DHCP-Server auf diesem Host
|
||||||
|
installiert ist und läuft — führt selbst NIE eine Installation, einen
|
||||||
|
Start oder einen Reload aus."""
|
||||||
|
dhcpd_path = shutil.which("dhcpd")
|
||||||
|
installed = dhcpd_path is not None
|
||||||
|
active = None
|
||||||
|
version = None
|
||||||
|
if installed:
|
||||||
|
try:
|
||||||
|
version_out = subprocess.run(
|
||||||
|
["dhcpd", "--version"], capture_output=True, text=True, timeout=5
|
||||||
|
)
|
||||||
|
version = (version_out.stdout or version_out.stderr or "").strip().splitlines()[0] if (version_out.stdout or version_out.stderr) else None
|
||||||
|
except Exception:
|
||||||
|
version = None
|
||||||
|
try:
|
||||||
|
status = subprocess.run(
|
||||||
|
["systemctl", "is-active", "isc-dhcp-server"],
|
||||||
|
capture_output=True, text=True, timeout=5,
|
||||||
|
)
|
||||||
|
active = status.stdout.strip() == "active"
|
||||||
|
except Exception:
|
||||||
|
active = None
|
||||||
|
return {"installed": installed, "path": dhcpd_path, "active": active, "version": version}
|
||||||
|
|
||||||
|
|
||||||
|
def _dhcp_safe_hostname(name, mac):
|
||||||
|
"""Wandelt einen Client-Namen in einen für DHCP-Host-Deklarationen und
|
||||||
|
option host-name gültigen Bezeichner um (nur a-z0-9-, keine
|
||||||
|
führenden/abschließenden/doppelten Bindestriche). Fällt bei leerem
|
||||||
|
Ergebnis auf die letzten 4 Byte der MAC zurück, damit nie ein leerer
|
||||||
|
oder doppelter Bezeichner entsteht."""
|
||||||
|
slug = re.sub(r"[^a-z0-9-]+", "-", (name or "").strip().lower()).strip("-")
|
||||||
|
slug = re.sub(r"-{2,}", "-", slug)
|
||||||
|
if slug:
|
||||||
|
return slug
|
||||||
|
return "geraet-" + re.sub(r"[^0-9a-f]", "", mac.lower())[-8:]
|
||||||
|
|
||||||
|
|
||||||
|
def _dhcp_reservation_candidates(conn):
|
||||||
|
"""Aktive Geräte mit gültiger MAC + IP, aus denen sich eine feste
|
||||||
|
DHCP-Reservierung erzeugen lässt. Geräte ohne rpi_ip/mac werden
|
||||||
|
übersprungen statt eine kaputte Reservierung zu erzeugen."""
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT mac, rpi_ip, name FROM devices WHERE is_active=1 AND mac IS NOT NULL AND rpi_ip IS NOT NULL ORDER BY name ASC"
|
||||||
|
).fetchall()
|
||||||
|
seen_hostnames = set()
|
||||||
|
reservations = []
|
||||||
|
for row in rows:
|
||||||
|
hostname = _dhcp_safe_hostname(row["name"], row["mac"])
|
||||||
|
# Kollisionen bei generierten Namen (z.B. zwei Geräte mit gleichem
|
||||||
|
# Namen) auflösen, statt zwei Host-Blöcke mit demselben Bezeichner
|
||||||
|
# zu erzeugen (das würde dhcpd -t als Fehler ablehnen).
|
||||||
|
base_hostname, suffix = hostname, 2
|
||||||
|
while hostname in seen_hostnames:
|
||||||
|
hostname = f"{base_hostname}-{suffix}"
|
||||||
|
suffix += 1
|
||||||
|
seen_hostnames.add(hostname)
|
||||||
|
reservations.append({"mac": row["mac"], "ip": row["rpi_ip"], "hostname": hostname, "name": row["name"]})
|
||||||
|
return reservations
|
||||||
|
|
||||||
|
|
||||||
|
def _render_dhcp_config(cfg, reservations):
|
||||||
|
lines = [
|
||||||
|
"# Automatisch generiert vom PoE Manager — Änderungen werden beim",
|
||||||
|
"# nächsten Schreiben aus den Geräte-Stammdaten überschrieben.",
|
||||||
|
"# Diese Datei enthält NUR die Subnet-/Reservierungs-Deklaration —",
|
||||||
|
"# per \"include\" aus der eigentlichen dhcpd.conf einbinden.",
|
||||||
|
"",
|
||||||
|
f"subnet {cfg['dhcp_subnet']} netmask {cfg['dhcp_netmask']} {{",
|
||||||
|
f" option routers {cfg['dhcp_dns'].split(',')[0].strip() if cfg['dhcp_dns'] else cfg['dhcp_subnet']};",
|
||||||
|
f" option domain-name-servers {cfg['dhcp_dns']};",
|
||||||
|
f" option domain-name \"{cfg['dhcp_domain']}\";",
|
||||||
|
f" range {cfg['dhcp_range_start']} {cfg['dhcp_range_end']};",
|
||||||
|
f" default-lease-time {cfg['dhcp_lease_default']};",
|
||||||
|
f" max-lease-time {cfg['dhcp_lease_max']};",
|
||||||
|
"}",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
for r in reservations:
|
||||||
|
lines += [
|
||||||
|
f"host {r['hostname']} {{",
|
||||||
|
f" hardware ethernet {r['mac']};",
|
||||||
|
f" fixed-address {r['ip']};",
|
||||||
|
f" option host-name \"{r['hostname']}\";",
|
||||||
|
"}",
|
||||||
|
]
|
||||||
|
return "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/settings/dhcp", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
|
def settings_dhcp():
|
||||||
|
if request.method == "GET" and not current_user.can_view_settings_dhcp:
|
||||||
|
flash("Keine Berechtigung, die DHCP-Einstellungen anzusehen.", "danger")
|
||||||
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
if not current_user.has_permission("settings_dhcp.edit"):
|
||||||
|
flash("Keine Berechtigung, die DHCP-Konfiguration zu ändern.", "danger")
|
||||||
|
conn.close()
|
||||||
|
return redirect(url_for("settings_dhcp"))
|
||||||
|
|
||||||
|
if "save_dhcp_config" in request.form:
|
||||||
|
for key in DHCP_DEFAULTS:
|
||||||
|
value = request.form.get(key, "").strip()
|
||||||
|
if value:
|
||||||
|
set_setting(key, value)
|
||||||
|
log_action("settings.update", "DHCP-Konfiguration")
|
||||||
|
flash("DHCP-Konfiguration gespeichert.", "success")
|
||||||
|
|
||||||
|
elif "write_dhcp_file" in request.form:
|
||||||
|
cfg = _load_dhcp_settings()
|
||||||
|
reservations = _dhcp_reservation_candidates(conn)
|
||||||
|
content = _render_dhcp_config(cfg, reservations)
|
||||||
|
output_path = cfg["dhcp_output_path"]
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||||
|
with open(output_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(content)
|
||||||
|
log_action("settings.dhcp_write", output_path, f"{len(reservations)} Reservierungen")
|
||||||
|
flash(
|
||||||
|
f"{len(reservations)} Reservierungen nach {output_path} geschrieben. "
|
||||||
|
f"Der DHCP-Dienst übernimmt die Änderung erst nach einem manuellen "
|
||||||
|
f"„sudo systemctl reload isc-dhcp-server“ — das führt die App bewusst nicht automatisch aus.",
|
||||||
|
"success",
|
||||||
|
)
|
||||||
|
except OSError as e:
|
||||||
|
flash(f"Konnte Datei nicht schreiben: {e}", "danger")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
return redirect(url_for("settings_dhcp"))
|
||||||
|
|
||||||
|
cfg = _load_dhcp_settings()
|
||||||
|
reservations = _dhcp_reservation_candidates(conn)
|
||||||
|
skipped_count = conn.execute(
|
||||||
|
"SELECT COUNT(*) AS n FROM devices WHERE is_active=1 AND (mac IS NULL OR rpi_ip IS NULL)"
|
||||||
|
).fetchone()["n"]
|
||||||
|
conn.close()
|
||||||
|
status = _detect_dhcp_status()
|
||||||
|
preview = _render_dhcp_config(cfg, reservations)
|
||||||
|
return render_template(
|
||||||
|
"settings_dhcp.html",
|
||||||
|
cfg=cfg,
|
||||||
|
status=status,
|
||||||
|
reservations=reservations,
|
||||||
|
skipped_count=skipped_count,
|
||||||
|
preview=preview,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/settings/nav-order", methods=["POST"])
|
@app.route("/settings/nav-order", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def save_nav_order():
|
def save_nav_order():
|
||||||
|
|||||||
@@ -1024,6 +1024,20 @@ select {
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
color: #c7ccd6;
|
color: #c7ccd6;
|
||||||
}
|
}
|
||||||
|
.code-preview {
|
||||||
|
background: #0a0c10;
|
||||||
|
border: 1px solid var(--border-soft);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 14px 18px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12.5px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #c7ccd6;
|
||||||
|
max-height: 360px;
|
||||||
|
overflow: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
.log-line { display: block; }
|
.log-line { display: block; }
|
||||||
.log-line.online { color: #4ade80; }
|
.log-line.online { color: #4ade80; }
|
||||||
.log-line.offline { color: #f87171; }
|
.log-line.offline { color: #f87171; }
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"logout": '<path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4"/><path d="M16 17l5-5-5-5"/><path d="M21 12H9"/>',
|
"logout": '<path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4"/><path d="M16 17l5-5-5-5"/><path d="M21 12H9"/>',
|
||||||
"gear": '<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 11-2.83 2.83l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 11-2.83-2.83l.06-.06A1.65 1.65 0 004.6 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 112.83-2.83l.06.06A1.65 1.65 0 009 4.6a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 112.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/>',
|
"gear": '<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 11-2.83 2.83l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 11-2.83-2.83l.06-.06A1.65 1.65 0 004.6 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 112.83-2.83l.06.06A1.65 1.65 0 009 4.6a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 112.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/>',
|
||||||
"transfer": '<path d="M17 3l4 4-4 4"/><path d="M3 7h18"/><path d="M7 21l-4-4 4-4"/><path d="M21 17H3"/>',
|
"transfer": '<path d="M17 3l4 4-4 4"/><path d="M3 7h18"/><path d="M7 21l-4-4 4-4"/><path d="M21 17H3"/>',
|
||||||
|
"network": '<rect x="9" y="2" width="6" height="6" rx="1.2"/><rect x="2" y="16" width="6" height="6" rx="1.2"/><rect x="16" y="16" width="6" height="6" rx="1.2"/><path d="M12 8v4M12 12H5v4M12 12h7v4"/>',
|
||||||
} %}
|
} %}
|
||||||
|
|
||||||
<div class="app-shell">
|
<div class="app-shell">
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% set active_page = "settings_dhcp" %}
|
||||||
|
{% set can_edit = current_user.has_permission('settings_dhcp.edit') %}
|
||||||
|
{% block page_title %}DHCP{% endblock %}
|
||||||
|
{% block page_sub %}<div class="topbar-sub">Reservierungen aus den Client-Stammdaten generieren</div>{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="settings-grid">
|
||||||
|
|
||||||
|
<div class="card card-pad">
|
||||||
|
<div class="section-head" style="margin-bottom:16px;">
|
||||||
|
<div>
|
||||||
|
<h2 style="font-size:16px;">DHCP-Server-Status</h2>
|
||||||
|
<div class="hint">Rein lesende Erkennung — die App installiert oder startet nie selbst einen DHCP-Dienst.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if status.installed %}
|
||||||
|
<div class="flex gap-2" style="align-items:center; margin-bottom:10px;">
|
||||||
|
<span class="pill online">Installiert</span>
|
||||||
|
{% if status.active is sameas true %}
|
||||||
|
<span class="pill online">Dienst aktiv</span>
|
||||||
|
{% elif status.active is sameas false %}
|
||||||
|
<span class="pill offline">Dienst inaktiv</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="pill unknown">Dienst-Status unbekannt</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if status.version %}<div class="text-faint mono" style="font-size:12px;">{{ status.version }}</div>{% endif %}
|
||||||
|
{% if status.active is not sameas true %}
|
||||||
|
<p class="text-faint" style="font-size:11.5px; margin-top:10px;">
|
||||||
|
Dienst manuell starten: <code>sudo systemctl enable --now isc-dhcp-server</code>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<span class="pill disabled">Nicht installiert</span>
|
||||||
|
<p class="text-faint" style="font-size:11.5px; margin-top:10px;">
|
||||||
|
Kein <code>dhcpd</code> auf diesem Host gefunden. Installation bewusst manuell, außerhalb der App:<br>
|
||||||
|
<code>sudo apt-get install isc-dhcp-server</code>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card card-pad">
|
||||||
|
<div class="section-head" style="margin-bottom:16px;">
|
||||||
|
<div>
|
||||||
|
<h2 style="font-size:16px;">Netzwerk-Konfiguration</h2>
|
||||||
|
<div class="hint">Grundlage für die generierte Subnet- und Reservierungs-Deklaration.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if can_edit %}
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" name="save_dhcp_config" value="1">
|
||||||
|
<div class="field"><label>Interface</label>
|
||||||
|
<input type="text" name="dhcp_interface" value="{{ cfg.dhcp_interface }}">
|
||||||
|
</div>
|
||||||
|
<div class="field"><label>Subnet</label>
|
||||||
|
<input type="text" name="dhcp_subnet" value="{{ cfg.dhcp_subnet }}">
|
||||||
|
</div>
|
||||||
|
<div class="field"><label>Netzmaske</label>
|
||||||
|
<input type="text" name="dhcp_netmask" value="{{ cfg.dhcp_netmask }}">
|
||||||
|
</div>
|
||||||
|
<div class="field"><label>Range Start</label>
|
||||||
|
<input type="text" name="dhcp_range_start" value="{{ cfg.dhcp_range_start }}">
|
||||||
|
</div>
|
||||||
|
<div class="field"><label>Range Ende</label>
|
||||||
|
<input type="text" name="dhcp_range_end" value="{{ cfg.dhcp_range_end }}">
|
||||||
|
</div>
|
||||||
|
<div class="field"><label>DNS-Server</label>
|
||||||
|
<input type="text" name="dhcp_dns" value="{{ cfg.dhcp_dns }}">
|
||||||
|
<div class="field-hint">Kommagetrennt, falls mehrere.</div>
|
||||||
|
</div>
|
||||||
|
<div class="field"><label>Domain</label>
|
||||||
|
<input type="text" name="dhcp_domain" value="{{ cfg.dhcp_domain }}">
|
||||||
|
</div>
|
||||||
|
<div class="field"><label>Lease-Zeit Standard (Sek.)</label>
|
||||||
|
<input type="number" name="dhcp_lease_default" value="{{ cfg.dhcp_lease_default }}">
|
||||||
|
</div>
|
||||||
|
<div class="field"><label>Lease-Zeit Maximum (Sek.)</label>
|
||||||
|
<input type="number" name="dhcp_lease_max" value="{{ cfg.dhcp_lease_max }}">
|
||||||
|
</div>
|
||||||
|
<div class="field"><label>Ausgabepfad (Reservierungsdatei)</label>
|
||||||
|
<input type="text" name="dhcp_output_path" value="{{ cfg.dhcp_output_path }}" class="mono">
|
||||||
|
<div class="field-hint">Separate Include-Datei — nicht die aktive dhcpd.conf. Per <code>include "{{ cfg.dhcp_output_path }}";</code> einmalig manuell einbinden.</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary btn-block">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>
|
||||||
|
Konfiguration speichern
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-faint" style="font-size:12.5px;">Für Änderungen fehlt das Recht „DHCP ändern“.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card card-pad" style="grid-column:1 / -1;">
|
||||||
|
<div class="section-head" style="margin-bottom:16px;">
|
||||||
|
<div>
|
||||||
|
<h2 style="font-size:16px;">Reservierungen aus den Clients</h2>
|
||||||
|
<div class="hint">
|
||||||
|
{{ reservations|length }} aktive{{ ' Clients' if reservations|length != 1 else 'r Client' }} mit gültiger MAC + IP.
|
||||||
|
{% if skipped_count %}{{ skipped_count }} aktive{{ ' Clients' if skipped_count != 1 else 'r Client' }} ohne MAC/IP übersprungen.{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if can_edit %}
|
||||||
|
<form method="post" data-confirm="Reservierungsdatei nach „{{ cfg.dhcp_output_path }}“ schreiben? Der DHCP-Dienst übernimmt die Änderung erst nach einem manuellen Reload.">
|
||||||
|
<input type="hidden" name="write_dhcp_file" value="1">
|
||||||
|
<button type="submit" class="btn btn-secondary">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><path d="M14 2v6h6"/></svg>
|
||||||
|
In Datei schreiben
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if reservations %}
|
||||||
|
<div class="table-wrap" style="margin-bottom:18px;">
|
||||||
|
<div style="overflow-x:auto;">
|
||||||
|
<table class="data-table">
|
||||||
|
<thead><tr><th>Hostname</th><th>MAC</th><th>IP-Adresse</th><th>Client-Name</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for r in reservations %}
|
||||||
|
<tr>
|
||||||
|
<td class="mono">{{ r.hostname }}</td>
|
||||||
|
<td class="mono">{{ r.mac }}</td>
|
||||||
|
<td class="mono">{{ r.ip }}</td>
|
||||||
|
<td>{{ r.name }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-faint" style="font-size:12.5px; margin-bottom:18px;">Keine aktiven Clients mit MAC + IP vorhanden.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="permission-group-title">Generierte Konfiguration (Vorschau)</div>
|
||||||
|
<pre class="code-preview">{{ preview }}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user