Version 2.8.28: dynamische Lease als Client uebernehmen
This commit is contained in:
1 parent
76a73017db
commit
bde2df2b44
4 files changed
+157
-4
No files matched your search
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm"
|
name = "tesm"
|
||||||
version = "2.8.27"
|
version = "2.8.28"
|
||||||
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
|
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from tesm_core.auth.service import client_ip
|
|||||||
from tesm_core.db import Database
|
from tesm_core.db import Database
|
||||||
from tesm_core.extension import core, db, transaction
|
from tesm_core.extension import core, db, transaction
|
||||||
|
|
||||||
from ..services import kea
|
from ..services import inventory, kea
|
||||||
from ..permissions import DHCP_PAGE
|
from ..permissions import DHCP_PAGE
|
||||||
|
|
||||||
bp = Blueprint("dhcp", __name__, url_prefix="/dhcp")
|
bp = Blueprint("dhcp", __name__, url_prefix="/dhcp")
|
||||||
@@ -73,6 +73,12 @@ def overview():
|
|||||||
# Zustand aus den laufenden Leases -- wie im Windows-DHCP.
|
# Zustand aus den laufenden Leases -- wie im Windows-DHCP.
|
||||||
reservations = kea.annotate_reservations(reservations, leases)
|
reservations = kea.annotate_reservations(reservations, leases)
|
||||||
reserved_ips = {str(entry["ip"]) for entry in reservations}
|
reserved_ips = {str(entry["ip"]) for entry in reservations}
|
||||||
|
# Wozu es schon einen Client gibt, ist nicht mehr zu uebernehmen -- auch
|
||||||
|
# wenn dessen Adresse (noch) keine Reservierung hat.
|
||||||
|
bekannte_macs = {
|
||||||
|
inventory.normalize_mac(str(zeile["mac"]))
|
||||||
|
for zeile in Database.all(conn, "SELECT mac FROM devices WHERE deleted_at IS NULL")
|
||||||
|
}
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"tesm/dhcp.html",
|
"tesm/dhcp.html",
|
||||||
@@ -90,6 +96,7 @@ def overview():
|
|||||||
manual_reservations=Database.all(conn, "SELECT * FROM dhcp_reservations ORDER BY ip"),
|
manual_reservations=Database.all(conn, "SELECT * FROM dhcp_reservations ORDER BY ip"),
|
||||||
leases=leases,
|
leases=leases,
|
||||||
reserved_ips=reserved_ips,
|
reserved_ips=reserved_ips,
|
||||||
|
bekannte_macs=bekannte_macs,
|
||||||
networks=networks,
|
networks=networks,
|
||||||
default_gateway=kea.default_gateway(sysops) if sysops else "",
|
default_gateway=kea.default_gateway(sysops) if sysops else "",
|
||||||
service=kea.service_state(sysops) if sysops else {},
|
service=kea.service_state(sysops) if sysops else {},
|
||||||
@@ -447,6 +454,91 @@ def add_reservation():
|
|||||||
return _write_and_redirect()
|
return _write_and_redirect()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.post("/lease/adopt")
|
||||||
|
# Zwei Rechte, mit Absicht: die Uebernahme ist eine eigene Funktion, das
|
||||||
|
# Ergebnis aber ein Client. Wer keine Clients anlegen darf, legt auch hier
|
||||||
|
# keine an -- sonst waere diese Schaltflaeche eine Hintertuer an der
|
||||||
|
# Geraeteverwaltung vorbei.
|
||||||
|
@permission_required("dhcp_lease_adopt.execute", "devices.create")
|
||||||
|
def adopt_lease():
|
||||||
|
"""Macht aus einer dynamisch vergebenen Adresse einen ueberwachten Client.
|
||||||
|
|
||||||
|
Die Angaben kommen **nicht** aus dem Formular, sondern aus der Lease-Datei:
|
||||||
|
geschickt wird nur die MAC, alles Weitere liest TESM frisch nach. Damit
|
||||||
|
heisst "Uebernahme" auch Uebernahme -- und eine Lease, die zwischen
|
||||||
|
Seitenaufbau und Klick abgelaufen oder umgezogen ist, faellt auf, statt
|
||||||
|
stillschweigend einen falschen Datensatz anzulegen.
|
||||||
|
"""
|
||||||
|
extension = core()
|
||||||
|
conn = db()
|
||||||
|
sysops = extension.sysops
|
||||||
|
gesucht = inventory.normalize_mac(request.form.get("mac") or "")
|
||||||
|
leases = kea.read_leases(sysops) if sysops else []
|
||||||
|
lease = next((eintrag for eintrag in leases if eintrag.mac == gesucht), None)
|
||||||
|
if lease is None:
|
||||||
|
flash(
|
||||||
|
"Zu dieser MAC liegt keine Lease mehr vor. Die Liste war nicht mehr aktuell.",
|
||||||
|
"warning",
|
||||||
|
)
|
||||||
|
return redirect(url_for("dhcp.overview"))
|
||||||
|
|
||||||
|
# Nur dynamische. Eine Adresse, die schon reserviert ist, gehoert bereits
|
||||||
|
# zu einem gepflegten Datensatz -- da gibt es nichts zu uebernehmen.
|
||||||
|
networks = kea.detect_networks(sysops) if sysops else []
|
||||||
|
reserviert = {
|
||||||
|
str(eintrag["ip"])
|
||||||
|
for eintrag in kea.collect_reservations(
|
||||||
|
conn, only_networks=kea.configured_networks(conn, networks)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if lease.address in reserviert:
|
||||||
|
flash(
|
||||||
|
f"{lease.address} ist bereits reserviert – diese Lease ist nicht dynamisch.",
|
||||||
|
"warning",
|
||||||
|
)
|
||||||
|
return redirect(url_for("dhcp.overview"))
|
||||||
|
|
||||||
|
# Der Hostname aus der Lease ist der Name, den das Geraet selbst nennt.
|
||||||
|
# Fehlt er, steht die Adresse da: ein Name wie "Gerät 7" sagt weniger.
|
||||||
|
name = (lease.hostname or "").strip() or f"Client {lease.address}"
|
||||||
|
try:
|
||||||
|
with transaction():
|
||||||
|
device_id = inventory.create_device(
|
||||||
|
conn,
|
||||||
|
name=name,
|
||||||
|
mac=lease.mac,
|
||||||
|
ip=lease.address,
|
||||||
|
switch_id=None,
|
||||||
|
port="",
|
||||||
|
credential_id=None,
|
||||||
|
ssh_port=None,
|
||||||
|
# Ohne Switch und Port gibt es keinen PoE-Neustart. Mit
|
||||||
|
# eingeschaltetem Automatismus schriebe die Ueberwachung bei
|
||||||
|
# jedem Ausfall einen Fehlschlag ins Protokoll, den niemand
|
||||||
|
# abstellen kann. Einschalten kann man ihn, sobald der Port
|
||||||
|
# eingetragen ist.
|
||||||
|
auto_restart=False,
|
||||||
|
actor=current_user.username,
|
||||||
|
)
|
||||||
|
except inventory.ValidationError as exc:
|
||||||
|
flash(str(exc), "danger")
|
||||||
|
return redirect(url_for("dhcp.overview"))
|
||||||
|
|
||||||
|
_audit(
|
||||||
|
"device.created",
|
||||||
|
name,
|
||||||
|
f"aus dynamischer Lease {lease.address} ({lease.mac}) übernommen",
|
||||||
|
severity="notice",
|
||||||
|
)
|
||||||
|
flash(
|
||||||
|
f"{name} wurde als Client übernommen. Switch, Port und Zugangsdaten fehlen noch – "
|
||||||
|
"ohne sie gibt es keinen PoE-Neustart und keine Wartung. Die feste Adresse entsteht "
|
||||||
|
"beim nächsten Schreiben der DHCP-Konfiguration.",
|
||||||
|
"success",
|
||||||
|
)
|
||||||
|
return redirect(url_for("devices.detail", device_id=device_id))
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/reservation/<int:reservation_id>/delete")
|
@bp.post("/reservation/<int:reservation_id>/delete")
|
||||||
@permission_required("dhcp_reservations.delete")
|
@permission_required("dhcp_reservations.delete")
|
||||||
def delete_reservation(reservation_id: int):
|
def delete_reservation(reservation_id: int):
|
||||||
|
|||||||
@@ -264,6 +264,31 @@ DHCP_SERVICE = Resource(
|
|||||||
action_hints={A.EXECUTE: "Dienstzustand ändern."},
|
action_hints={A.EXECUTE: "Dienstzustand ändern."},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
#: Eine dynamische Lease in einen Client verwandeln.
|
||||||
|
#:
|
||||||
|
#: Eigene Zeile, weil es eine eigene Entscheidung ist: hier wird aus einer
|
||||||
|
#: Beobachtung ("dieses Geraet hat sich eine Adresse geholt") ein gepflegter
|
||||||
|
#: Datensatz, den die Ueberwachung ab sofort anpingt und der beim naechsten
|
||||||
|
#: Schreiben der Konfiguration eine feste Adresse bekommt. Wer Leases sehen
|
||||||
|
#: darf, soll deshalb nicht automatisch welche uebernehmen duerfen.
|
||||||
|
#:
|
||||||
|
#: **Und es reicht allein nicht.** Die Uebernahme legt einen Client an, also
|
||||||
|
#: verlangt die Route zusaetzlich "Clients: Anlegen". Sonst waere dies eine
|
||||||
|
#: Hintertuer an der Geraeteverwaltung vorbei -- dieselbe Wirkung, anderes
|
||||||
|
#: Formular. Beide Haken zusammen, oder die Schaltflaeche erscheint nicht.
|
||||||
|
DHCP_LEASE_ADOPT = Resource(
|
||||||
|
key="dhcp_lease_adopt",
|
||||||
|
parent="dhcp_subnets",
|
||||||
|
label="DHCP: Lease als Client übernehmen",
|
||||||
|
actions=(A.READ, A.EXECUTE),
|
||||||
|
hint="Aus einer dynamisch vergebenen Adresse einen überwachten Client machen",
|
||||||
|
action_hints={
|
||||||
|
A.READ: "Die Schaltfläche an dynamischen Leases sehen.",
|
||||||
|
A.EXECUTE: "Client aus der Lease anlegen. Verlangt zusätzlich "
|
||||||
|
"'Clients: Anlegen' – es entsteht ein Gerätedatensatz.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
#: Die DHCP-Seite zeigt alle Abschnitte; jeder Abschnitt haengt an seiner
|
#: Die DHCP-Seite zeigt alle Abschnitte; jeder Abschnitt haengt an seiner
|
||||||
#: eigenen Zeile. Geoeffnet wird sie, wer einen davon sehen darf.
|
#: eigenen Zeile. Geoeffnet wird sie, wer einen davon sehen darf.
|
||||||
DHCP_PAGE: tuple[str, ...] = (
|
DHCP_PAGE: tuple[str, ...] = (
|
||||||
@@ -380,7 +405,14 @@ NETWORK_AREA = Area(
|
|||||||
key="network",
|
key="network",
|
||||||
label="Netzwerkdienste",
|
label="Netzwerkdienste",
|
||||||
icon="network",
|
icon="network",
|
||||||
resources=(DHCP_SUBNETS, DHCP_OPTIONS, DHCP_RESERVATIONS, DHCP_CONFIG, DHCP_SERVICE),
|
resources=(
|
||||||
|
DHCP_SUBNETS,
|
||||||
|
DHCP_OPTIONS,
|
||||||
|
DHCP_RESERVATIONS,
|
||||||
|
DHCP_LEASE_ADOPT,
|
||||||
|
DHCP_CONFIG,
|
||||||
|
DHCP_SERVICE,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
FILES_AREA = Area(
|
FILES_AREA = Area(
|
||||||
|
|||||||
@@ -377,6 +377,10 @@
|
|||||||
<div class="card__title">{{ icon('clock', size=17) }} Leases</div>
|
<div class="card__title">{{ icon('clock', size=17) }} Leases</div>
|
||||||
<div class="card__sub">
|
<div class="card__sub">
|
||||||
Direkt aus {{ lease_file }} gelesen – je Adresse gilt der letzte Eintrag.
|
Direkt aus {{ lease_file }} gelesen – je Adresse gilt der letzte Eintrag.
|
||||||
|
{% if current_user.has('dhcp_lease_adopt.read') and current_user.has('devices.create') %}
|
||||||
|
Eine <strong>dynamische</strong> Lease lässt sich als Client übernehmen; sie bekommt
|
||||||
|
damit eine feste Adresse und wird überwacht.
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -394,7 +398,7 @@
|
|||||||
<div class="table-wrap" style="max-height:420px;overflow:auto">
|
<div class="table-wrap" style="max-height:420px;overflow:auto">
|
||||||
<table class="table table--compact">
|
<table class="table table--compact">
|
||||||
<thead><tr><th>IP</th><th>MAC</th><th>Hostname</th><th>Art</th><th>Zustand</th>
|
<thead><tr><th>IP</th><th>MAC</th><th>Hostname</th><th>Art</th><th>Zustand</th>
|
||||||
<th>Läuft ab</th></tr></thead>
|
<th>Läuft ab</th><th></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for lease in leases %}
|
{% for lease in leases %}
|
||||||
<tr>
|
<tr>
|
||||||
@@ -413,6 +417,31 @@
|
|||||||
<td class="fs-sm">{{ lease.expires_text }}
|
<td class="fs-sm">{{ lease.expires_text }}
|
||||||
<div class="fs-xs faint">{{ lease.remaining_text }}</div>
|
<div class="fs-xs faint">{{ lease.remaining_text }}</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="nowrap">
|
||||||
|
{# Übernehmen gibt es nur für dynamische Leases: was reserviert
|
||||||
|
ist, gehört schon zu einem gepflegten Datensatz. Und nur,
|
||||||
|
wenn beide Rechte stehen -- hier entsteht ein Client. #}
|
||||||
|
{% if lease.address not in reserved_ips
|
||||||
|
and current_user.has('dhcp_lease_adopt.read')
|
||||||
|
and current_user.has('devices.create') %}
|
||||||
|
{% if lease.mac in bekannte_macs %}
|
||||||
|
<span class="faint fs-xs" title="Zu dieser MAC gibt es bereits einen Client">
|
||||||
|
bereits Client
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<form method="post" action="{{ url_for('dhcp.adopt_lease') }}">
|
||||||
|
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||||
|
<input type="hidden" name="mac" value="{{ lease.mac }}">
|
||||||
|
<button class="btn btn--sm btn--ghost" type="submit"
|
||||||
|
data-confirm="{{ lease.hostname or lease.address }} als überwachten Client anlegen? Switch, Port und Zugangsdaten tragen Sie danach nach."
|
||||||
|
data-confirm-title="Als Client übernehmen"
|
||||||
|
data-confirm-ok="Übernehmen">
|
||||||
|
{{ icon('plus', size=14) }} Als Client
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
Reference in new issue
Block a user