KEA update
This commit is contained in:
26
modules/kea/migrations/003_1.2.0_device_checks.php
Normal file
26
modules/kea/migrations/003_1.2.0_device_checks.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Database;
|
||||||
|
use App\ModuleMigrationContext;
|
||||||
|
use App\Repository\KeaHostMetadataRepository;
|
||||||
|
|
||||||
|
return new class {
|
||||||
|
public function up(ModuleMigrationContext $context): void
|
||||||
|
{
|
||||||
|
$settings = $context->settings();
|
||||||
|
$fallback = is_array($context->module['metadata_db_defaults'] ?? null)
|
||||||
|
? $context->module['metadata_db_defaults']
|
||||||
|
: [];
|
||||||
|
$config = is_array($settings['metadata_db'] ?? null)
|
||||||
|
? array_replace($fallback, $settings['metadata_db'])
|
||||||
|
: $fallback;
|
||||||
|
|
||||||
|
if (empty($config['driver']) || empty($config['dbname'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$repo = new KeaHostMetadataRepository(Database::createFromArray($config));
|
||||||
|
$repo->ensureSchema();
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"title": "KEA DHCP",
|
"title": "KEA DHCP",
|
||||||
"version": "1.1.0",
|
"version": "1.2.0",
|
||||||
"schema_version": 2,
|
"schema_version": 3,
|
||||||
"description": "Verwaltung von KEA DHCP Hosts und Reservierungen.",
|
"description": "Verwaltung von KEA DHCP Hosts und Reservierungen.",
|
||||||
"menu": [
|
"menu": [
|
||||||
{ "label": "Hosts", "href": "/module/kea" },
|
{ "label": "Hosts", "href": "/module/kea" },
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ $host = null;
|
|||||||
$metadataRepo = null;
|
$metadataRepo = null;
|
||||||
$groups = [];
|
$groups = [];
|
||||||
$availableIpsByGroup = [];
|
$availableIpsByGroup = [];
|
||||||
|
$checks = [];
|
||||||
|
$hostKey = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$pdo = modules()->modulePdo('kea', $fallback);
|
$pdo = modules()->modulePdo('kea', $fallback);
|
||||||
@@ -35,8 +37,25 @@ try {
|
|||||||
if (!$host) {
|
if (!$host) {
|
||||||
throw new RuntimeException('KEA Eintrag wurde nicht gefunden.');
|
throw new RuntimeException('KEA Eintrag wurde nicht gefunden.');
|
||||||
}
|
}
|
||||||
|
$hostKey = $source . ':' . (string)($host['host_id'] ?? $id);
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$action = (string)($_POST['action'] ?? 'save_metadata');
|
||||||
|
if ($action === 'dns_lookup') {
|
||||||
|
$ip = (string)($host['ipv4_address'] ?? '');
|
||||||
|
$hostname = trim((string)($host['metadata']['device_name'] ?? $host['hostname'] ?? ''));
|
||||||
|
$reverse = $ip !== '' ? gethostbyaddr($ip) : '';
|
||||||
|
$forward = $hostname !== '' ? gethostbyname($hostname) : '';
|
||||||
|
$success = ($reverse !== '' && $reverse !== $ip) || ($forward !== '' && $forward !== $hostname);
|
||||||
|
|
||||||
|
$metadataRepo->saveCheck($hostKey, 'dns', $success ? 'success' : 'warning', [
|
||||||
|
'ip' => $ip,
|
||||||
|
'hostname' => $hostname,
|
||||||
|
'reverse' => $reverse,
|
||||||
|
'forward' => $forward,
|
||||||
|
]);
|
||||||
|
$notice = $success ? 'DNS-Pruefung gespeichert.' : 'DNS-Pruefung gespeichert, aber kein eindeutiger Name gefunden.';
|
||||||
|
} else {
|
||||||
$metadata = [
|
$metadata = [
|
||||||
'real_name' => $_POST['real_name'] ?? '',
|
'real_name' => $_POST['real_name'] ?? '',
|
||||||
'device_name' => $_POST['device_name'] ?? '',
|
'device_name' => $_POST['device_name'] ?? '',
|
||||||
@@ -63,7 +82,9 @@ try {
|
|||||||
);
|
);
|
||||||
$notice = 'Zusatzdaten gespeichert.';
|
$notice = 'Zusatzdaten gespeichert.';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
$host = $repo->findDisplayByKey($source, $id) ?: $host;
|
$host = $repo->findDisplayByKey($source, $id) ?: $host;
|
||||||
|
$hostKey = $source . ':' . (string)($host['host_id'] ?? $id);
|
||||||
}
|
}
|
||||||
|
|
||||||
$usedIps = array_diff(
|
$usedIps = array_diff(
|
||||||
@@ -71,6 +92,7 @@ try {
|
|||||||
[(string)($host['ipv4_address'] ?? ''), (string)($host['metadata']['desired_ip'] ?? '')]
|
[(string)($host['ipv4_address'] ?? ''), (string)($host['metadata']['desired_ip'] ?? '')]
|
||||||
);
|
);
|
||||||
$availableIpsByGroup = $metadataRepo->availableIpsByGroup($usedIps);
|
$availableIpsByGroup = $metadataRepo->availableIpsByGroup($usedIps);
|
||||||
|
$checks = $metadataRepo->latestChecks([$hostKey])[$hostKey] ?? [];
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
$error = $e->getMessage();
|
$error = $e->getMessage();
|
||||||
}
|
}
|
||||||
@@ -115,6 +137,7 @@ $selectedIp = (string)($metadata['desired_ip'] ?? '');
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="post" class="kea-edit-form">
|
<form method="post" class="kea-edit-form">
|
||||||
|
<input type="hidden" name="action" value="save_metadata">
|
||||||
<input type="hidden" name="source" value="<?= e($source) ?>">
|
<input type="hidden" name="source" value="<?= e($source) ?>">
|
||||||
<input type="hidden" name="id" value="<?= e((string)$id) ?>">
|
<input type="hidden" name="id" value="<?= e((string)$id) ?>">
|
||||||
|
|
||||||
@@ -168,6 +191,40 @@ $selectedIp = (string)($metadata['desired_ip'] ?? '');
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="kea-panel">
|
||||||
|
<div class="kea-panel__head">
|
||||||
|
<div>
|
||||||
|
<span class="pill">Pruefungen</span>
|
||||||
|
<h3>Gerätechecks</h3>
|
||||||
|
<p class="muted">Pruefergebnisse werden in der Nexus-DHCP-Datenbank gespeichert und koennen spaeter fuer Reports genutzt werden.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="kea-check-grid">
|
||||||
|
<div class="kea-check-card">
|
||||||
|
<h4>DNS / Hostname</h4>
|
||||||
|
<?php $dnsCheck = $checks['dns'] ?? null; ?>
|
||||||
|
<?php if ($dnsCheck): ?>
|
||||||
|
<?php $dnsResult = json_decode((string)($dnsCheck['result_json'] ?? '{}'), true) ?: []; ?>
|
||||||
|
<p class="muted">Status: <?= e((string)($dnsCheck['status'] ?? '')) ?> · <?= e((string)($dnsCheck['checked_at'] ?? '')) ?></p>
|
||||||
|
<p class="mono">Reverse: <?= e((string)($dnsResult['reverse'] ?? '-')) ?></p>
|
||||||
|
<p class="mono">Forward: <?= e((string)($dnsResult['forward'] ?? '-')) ?></p>
|
||||||
|
<?php else: ?>
|
||||||
|
<p class="muted">Noch keine DNS-Pruefung gespeichert.</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
<form method="post" class="setup-actions">
|
||||||
|
<input type="hidden" name="action" value="dns_lookup">
|
||||||
|
<input type="hidden" name="source" value="<?= e($source) ?>">
|
||||||
|
<input type="hidden" name="id" value="<?= e((string)$id) ?>">
|
||||||
|
<button class="nav-link" type="submit">DNS jetzt pruefen</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="kea-check-card">
|
||||||
|
<h4>Login-Erkennung</h4>
|
||||||
|
<p class="muted">Vorbereitet fuer spaetere HTTP/Port-Erkennung. Noch nicht automatisch aktiv, damit keine ungewollten Scans laufen.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<script>
|
<script>
|
||||||
(() => {
|
(() => {
|
||||||
const ipsByGroup = <?= json_encode($availableIpsByGroup, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>;
|
const ipsByGroup = <?= json_encode($availableIpsByGroup, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ $error = null;
|
|||||||
$notice = null;
|
$notice = null;
|
||||||
$groups = [];
|
$groups = [];
|
||||||
$availableIpsByGroup = [];
|
$availableIpsByGroup = [];
|
||||||
|
$usedIps = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (empty($metadataConfig['driver']) || empty($metadataConfig['dbname'])) {
|
if (empty($metadataConfig['driver']) || empty($metadataConfig['dbname'])) {
|
||||||
@@ -26,7 +27,11 @@ try {
|
|||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
$action = (string)($_POST['action'] ?? '');
|
$action = (string)($_POST['action'] ?? '');
|
||||||
if ($action === 'save_group') {
|
if ($action === 'save_group') {
|
||||||
$metadataRepo->saveGroup((string)($_POST['name'] ?? ''), (string)($_POST['description'] ?? ''));
|
$metadataRepo->saveGroup(
|
||||||
|
(string)($_POST['name'] ?? ''),
|
||||||
|
(string)($_POST['description'] ?? ''),
|
||||||
|
(string)($_POST['parent_name'] ?? '')
|
||||||
|
);
|
||||||
$notice = 'Gruppe gespeichert.';
|
$notice = 'Gruppe gespeichert.';
|
||||||
} elseif ($action === 'add_range') {
|
} elseif ($action === 'add_range') {
|
||||||
$metadataRepo->addRange(
|
$metadataRepo->addRange(
|
||||||
@@ -40,13 +45,35 @@ try {
|
|||||||
|
|
||||||
$groups = $metadataRepo->listGroupsWithRanges();
|
$groups = $metadataRepo->listGroupsWithRanges();
|
||||||
$keaRepo = new KeaHostRepository(modules()->modulePdo('kea', $fallback), $metadataRepo);
|
$keaRepo = new KeaHostRepository(modules()->modulePdo('kea', $fallback), $metadataRepo);
|
||||||
$availableIpsByGroup = $metadataRepo->availableIpsByGroup(
|
$usedIps = array_merge($keaRepo->usedIpAddresses(), $metadataRepo->desiredIps());
|
||||||
array_merge($keaRepo->usedIpAddresses(), $metadataRepo->desiredIps()),
|
$availableIpsByGroup = $metadataRepo->availableIpsByGroup($usedIps, 4096);
|
||||||
4096
|
|
||||||
);
|
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
$error = $e->getMessage();
|
$error = $e->getMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$usedIpLookup = array_flip(array_filter(array_map('strval', $usedIps)));
|
||||||
|
$matrixForGroup = static function (array $group) use ($usedIpLookup): array {
|
||||||
|
$dots = [];
|
||||||
|
foreach (($group['ranges'] ?? []) as $range) {
|
||||||
|
$start = ip2long((string)($range['start_ip'] ?? ''));
|
||||||
|
$end = ip2long((string)($range['end_ip'] ?? ''));
|
||||||
|
if ($start === false || $end === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for ($ip = $start; $ip <= $end && count($dots) < 512; $ip++) {
|
||||||
|
$address = long2ip($ip);
|
||||||
|
if ($address === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$dots[] = [
|
||||||
|
'ip' => $address,
|
||||||
|
'used' => isset($usedIpLookup[$address]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $dots;
|
||||||
|
};
|
||||||
?>
|
?>
|
||||||
<section class="kea-page">
|
<section class="kea-page">
|
||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
@@ -83,6 +110,15 @@ try {
|
|||||||
<span>Beschreibung</span>
|
<span>Beschreibung</span>
|
||||||
<input type="text" name="description">
|
<input type="text" name="description">
|
||||||
</label>
|
</label>
|
||||||
|
<label class="setup-field">
|
||||||
|
<span>Uebergeordnete Gruppe</span>
|
||||||
|
<select name="parent_name">
|
||||||
|
<option value="">Keine</option>
|
||||||
|
<?php foreach ($groups as $group): ?>
|
||||||
|
<option value="<?= e((string)$group['name']) ?>"><?= e((string)$group['name']) ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
<div class="setup-actions kea-edit-form__wide">
|
<div class="setup-actions kea-edit-form__wide">
|
||||||
<button class="cta-button" type="submit">Gruppe speichern</button>
|
<button class="cta-button" type="submit">Gruppe speichern</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -133,19 +169,22 @@ try {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Gruppe</th>
|
<th>Gruppe</th>
|
||||||
|
<th>Untergruppe von</th>
|
||||||
<th>Beschreibung</th>
|
<th>Beschreibung</th>
|
||||||
<th>Bereiche</th>
|
<th>Bereiche</th>
|
||||||
<th>Freie IPs</th>
|
<th>Freie IPs</th>
|
||||||
|
<th>Matrix</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php if ($groups === []): ?>
|
<?php if ($groups === []): ?>
|
||||||
<tr><td colspan="4" class="kea-empty">Noch keine Gruppen definiert.</td></tr>
|
<tr><td colspan="6" class="kea-empty">Noch keine Gruppen definiert.</td></tr>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?php foreach ($groups as $group): ?>
|
<?php foreach ($groups as $group): ?>
|
||||||
<?php $available = $availableIpsByGroup[(string)$group['name']] ?? []; ?>
|
<?php $available = $availableIpsByGroup[(string)$group['name']] ?? []; ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td><?= e((string)$group['name']) ?></td>
|
<td><?= e((string)$group['name']) ?></td>
|
||||||
|
<td><?= e((string)($group['parent_name'] ?: '-')) ?></td>
|
||||||
<td><?= e((string)($group['description'] ?? '-')) ?></td>
|
<td><?= e((string)($group['description'] ?? '-')) ?></td>
|
||||||
<td>
|
<td>
|
||||||
<?php if (($group['ranges'] ?? []) === []): ?>
|
<?php if (($group['ranges'] ?? []) === []): ?>
|
||||||
@@ -157,6 +196,21 @@ try {
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
<td><?= e((string)count($available)) ?></td>
|
<td><?= e((string)count($available)) ?></td>
|
||||||
|
<td>
|
||||||
|
<?php $matrix = $matrixForGroup($group); ?>
|
||||||
|
<?php if ($matrix === []): ?>
|
||||||
|
<span class="muted">Kein Bereich</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="ip-matrix" aria-label="IP Matrix fuer <?= e((string)$group['name']) ?>">
|
||||||
|
<?php foreach ($matrix as $dot): ?>
|
||||||
|
<span
|
||||||
|
class="ip-dot <?= $dot['used'] ? 'is-used' : 'is-free' ?>"
|
||||||
|
title="<?= e((string)$dot['ip']) ?> · <?= $dot['used'] ? 'belegt' : 'frei' ?>"
|
||||||
|
></span>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|||||||
@@ -70,6 +70,8 @@
|
|||||||
<th>Hostname</th>
|
<th>Hostname</th>
|
||||||
<th>IP Adresse</th>
|
<th>IP Adresse</th>
|
||||||
<th>MAC Adresse</th>
|
<th>MAC Adresse</th>
|
||||||
|
<th>Zuletzt gesehen</th>
|
||||||
|
<th>Lease bis</th>
|
||||||
<th>Echter Name</th>
|
<th>Echter Name</th>
|
||||||
<th>Standort</th>
|
<th>Standort</th>
|
||||||
<th>Gruppe</th>
|
<th>Gruppe</th>
|
||||||
@@ -79,7 +81,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<?php if (empty($hosts)): ?>
|
<?php if (empty($hosts)): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="8" class="kea-empty">
|
<td colspan="10" class="kea-empty">
|
||||||
Keine Reservierungen oder aktiven Leases gefunden.
|
Keine Reservierungen oder aktiven Leases gefunden.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -98,6 +100,12 @@
|
|||||||
<td class="mono">
|
<td class="mono">
|
||||||
<?= e($host['dhcp_identifier']) ?>
|
<?= e($host['dhcp_identifier']) ?>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<?= e((string)($host['last_seen_at'] ?? '-')) ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?= e((string)($host['lease_expires_at'] ?? '-')) ?>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<?= e((string)($host['metadata']['real_name'] ?? '-')) ?>
|
<?= e((string)($host['metadata']['real_name'] ?? '-')) ?>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -853,3 +853,47 @@ a {
|
|||||||
.kea-edit-form__wide {
|
.kea-edit-form__wide {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.kea-check-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||||
|
gap: 14px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kea-check-card {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--surface);
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kea-check-card h4 {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kea-check-card p {
|
||||||
|
margin: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ip-matrix {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(10px, 1fr));
|
||||||
|
gap: 4px;
|
||||||
|
max-width: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ip-dot {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ip-dot.is-free {
|
||||||
|
background: var(--accent-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ip-dot.is-used {
|
||||||
|
background: color-mix(in srgb, var(--muted) 55%, var(--surface));
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ final class KeaHostMetadataRepository
|
|||||||
$this->ensureColumn('nexus_dhcp_host_meta', 'group_name', 'TEXT');
|
$this->ensureColumn('nexus_dhcp_host_meta', 'group_name', 'TEXT');
|
||||||
$this->ensureColumn('nexus_dhcp_host_meta', 'desired_ip', 'TEXT');
|
$this->ensureColumn('nexus_dhcp_host_meta', 'desired_ip', 'TEXT');
|
||||||
$this->ensureGroupSchema($driver);
|
$this->ensureGroupSchema($driver);
|
||||||
|
$this->ensureCheckSchema($driver);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +59,7 @@ final class KeaHostMetadataRepository
|
|||||||
$this->ensureColumn('nexus_dhcp_host_meta', 'group_name', 'TEXT');
|
$this->ensureColumn('nexus_dhcp_host_meta', 'group_name', 'TEXT');
|
||||||
$this->ensureColumn('nexus_dhcp_host_meta', 'desired_ip', 'TEXT');
|
$this->ensureColumn('nexus_dhcp_host_meta', 'desired_ip', 'TEXT');
|
||||||
$this->ensureGroupSchema($driver);
|
$this->ensureGroupSchema($driver);
|
||||||
|
$this->ensureCheckSchema($driver);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +84,7 @@ final class KeaHostMetadataRepository
|
|||||||
$this->ensureColumn('nexus_dhcp_host_meta', 'group_name', 'TEXT');
|
$this->ensureColumn('nexus_dhcp_host_meta', 'group_name', 'TEXT');
|
||||||
$this->ensureColumn('nexus_dhcp_host_meta', 'desired_ip', 'TEXT');
|
$this->ensureColumn('nexus_dhcp_host_meta', 'desired_ip', 'TEXT');
|
||||||
$this->ensureGroupSchema($driver);
|
$this->ensureGroupSchema($driver);
|
||||||
|
$this->ensureCheckSchema($driver);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function findByHostIds(array $hostIds): array
|
public function findByHostIds(array $hostIds): array
|
||||||
@@ -222,9 +225,10 @@ final class KeaHostMetadataRepository
|
|||||||
|
|
||||||
$groups = [];
|
$groups = [];
|
||||||
$stmt = $this->pdo->query(
|
$stmt = $this->pdo->query(
|
||||||
"SELECT id, name, description
|
"SELECT child.id, child.name, child.description, child.parent_id, parent.name AS parent_name
|
||||||
FROM nexus_dhcp_groups
|
FROM nexus_dhcp_groups child
|
||||||
ORDER BY name ASC"
|
LEFT JOIN nexus_dhcp_groups parent ON parent.id = child.parent_id
|
||||||
|
ORDER BY parent.name ASC, child.name ASC"
|
||||||
);
|
);
|
||||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||||
$id = (int)$row['id'];
|
$id = (int)$row['id'];
|
||||||
@@ -232,6 +236,8 @@ final class KeaHostMetadataRepository
|
|||||||
'id' => $id,
|
'id' => $id,
|
||||||
'name' => (string)$row['name'],
|
'name' => (string)$row['name'],
|
||||||
'description' => (string)($row['description'] ?? ''),
|
'description' => (string)($row['description'] ?? ''),
|
||||||
|
'parent_id' => $row['parent_id'] !== null ? (int)$row['parent_id'] : null,
|
||||||
|
'parent_name' => (string)($row['parent_name'] ?? ''),
|
||||||
'ranges' => [],
|
'ranges' => [],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -257,33 +263,47 @@ final class KeaHostMetadataRepository
|
|||||||
return array_values($groups);
|
return array_values($groups);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function saveGroup(string $name, string $description = ''): void
|
public function saveGroup(string $name, string $description = '', string $parentName = ''): void
|
||||||
{
|
{
|
||||||
$name = trim($name);
|
$name = trim($name);
|
||||||
if ($name === '') {
|
if ($name === '') {
|
||||||
throw new \RuntimeException('Gruppenname fehlt.');
|
throw new \RuntimeException('Gruppenname fehlt.');
|
||||||
}
|
}
|
||||||
|
$parentName = trim($parentName);
|
||||||
|
if ($parentName !== '' && strcasecmp($parentName, $name) === 0) {
|
||||||
|
throw new \RuntimeException('Eine Gruppe kann nicht ihre eigene Untergruppe sein.');
|
||||||
|
}
|
||||||
|
$parentId = $parentName !== '' ? $this->groupIdByName($parentName) : null;
|
||||||
|
if ($parentName !== '' && !$parentId) {
|
||||||
|
throw new \RuntimeException('Uebergeordnete Gruppe wurde nicht gefunden.');
|
||||||
|
}
|
||||||
|
|
||||||
$driver = (string)$this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
|
$driver = (string)$this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
|
||||||
if ($driver === 'pgsql' || $driver === 'sqlite') {
|
if ($driver === 'pgsql' || $driver === 'sqlite') {
|
||||||
$sql = "INSERT INTO nexus_dhcp_groups (name, description, updated_at)
|
$sql = "INSERT INTO nexus_dhcp_groups (name, description, parent_id, updated_at)
|
||||||
VALUES (:name, :description, CURRENT_TIMESTAMP)
|
VALUES (:name, :description, :parent_id, CURRENT_TIMESTAMP)
|
||||||
ON CONFLICT(name) DO UPDATE SET
|
ON CONFLICT(name) DO UPDATE SET
|
||||||
description = excluded.description,
|
description = excluded.description,
|
||||||
|
parent_id = excluded.parent_id,
|
||||||
updated_at = CURRENT_TIMESTAMP";
|
updated_at = CURRENT_TIMESTAMP";
|
||||||
} else {
|
} else {
|
||||||
$sql = "INSERT INTO nexus_dhcp_groups (name, description, updated_at)
|
$sql = "INSERT INTO nexus_dhcp_groups (name, description, parent_id, updated_at)
|
||||||
VALUES (:name, :description, CURRENT_TIMESTAMP)
|
VALUES (:name, :description, :parent_id, CURRENT_TIMESTAMP)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
description = VALUES(description),
|
description = VALUES(description),
|
||||||
|
parent_id = VALUES(parent_id),
|
||||||
updated_at = CURRENT_TIMESTAMP";
|
updated_at = CURRENT_TIMESTAMP";
|
||||||
}
|
}
|
||||||
|
|
||||||
$stmt = $this->pdo->prepare($sql);
|
$stmt = $this->pdo->prepare($sql);
|
||||||
$stmt->execute([
|
$stmt->bindValue('name', $name);
|
||||||
'name' => $name,
|
$stmt->bindValue('description', $this->nullableString($description));
|
||||||
'description' => $this->nullableString($description),
|
if ($parentId === null) {
|
||||||
]);
|
$stmt->bindValue('parent_id', null, PDO::PARAM_NULL);
|
||||||
|
} else {
|
||||||
|
$stmt->bindValue('parent_id', $parentId, PDO::PARAM_INT);
|
||||||
|
}
|
||||||
|
$stmt->execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function addRange(string $groupName, string $startIp, string $endIp): void
|
public function addRange(string $groupName, string $startIp, string $endIp): void
|
||||||
@@ -354,6 +374,89 @@ final class KeaHostMetadataRepository
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function saveCheck(string $hostKey, string $checkType, string $status, array $result = [], ?string $nextCheckAt = null): void
|
||||||
|
{
|
||||||
|
$hostKey = trim($hostKey);
|
||||||
|
$checkType = trim($checkType);
|
||||||
|
$status = trim($status);
|
||||||
|
if ($hostKey === '' || $checkType === '' || $status === '') {
|
||||||
|
throw new \RuntimeException('Pruefdaten sind unvollstaendig.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$resultJson = json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
if ($resultJson === false) {
|
||||||
|
$resultJson = '{}';
|
||||||
|
}
|
||||||
|
|
||||||
|
$driver = (string)$this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
|
||||||
|
if ($driver === 'pgsql') {
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
"INSERT INTO nexus_dhcp_device_checks (
|
||||||
|
host_key, check_type, status, result_json, checked_at, next_check_at
|
||||||
|
) VALUES (
|
||||||
|
:host_key, :check_type, :status, CAST(:result_json AS jsonb), NOW(), :next_check_at
|
||||||
|
)"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
"INSERT INTO nexus_dhcp_device_checks (
|
||||||
|
host_key, check_type, status, result_json, checked_at, next_check_at
|
||||||
|
) VALUES (
|
||||||
|
:host_key, :check_type, :status, :result_json, CURRENT_TIMESTAMP, :next_check_at
|
||||||
|
)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
'host_key' => $hostKey,
|
||||||
|
'check_type' => $checkType,
|
||||||
|
'status' => $status,
|
||||||
|
'result_json' => $resultJson,
|
||||||
|
'next_check_at' => $this->nullableString($nextCheckAt),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function latestChecks(array $hostKeys): array
|
||||||
|
{
|
||||||
|
$hostKeys = array_values(array_unique(array_filter(array_map(
|
||||||
|
static fn(mixed $value): string => trim((string)$value),
|
||||||
|
$hostKeys
|
||||||
|
))));
|
||||||
|
if ($hostKeys === [] || !$this->tableExists('nexus_dhcp_device_checks')) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$params = [];
|
||||||
|
$placeholders = [];
|
||||||
|
foreach ($hostKeys as $idx => $hostKey) {
|
||||||
|
$key = ':host_key_' . $idx;
|
||||||
|
$placeholders[] = $key;
|
||||||
|
$params[$key] = $hostKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
'SELECT host_key, check_type, status, result_json, checked_at
|
||||||
|
FROM nexus_dhcp_device_checks
|
||||||
|
WHERE host_key IN (' . implode(', ', $placeholders) . ')
|
||||||
|
ORDER BY checked_at DESC, id DESC'
|
||||||
|
);
|
||||||
|
foreach ($params as $key => $value) {
|
||||||
|
$stmt->bindValue($key, $value);
|
||||||
|
}
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
$items = [];
|
||||||
|
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||||
|
$hostKey = (string)$row['host_key'];
|
||||||
|
$type = (string)$row['check_type'];
|
||||||
|
if (!isset($items[$hostKey][$type])) {
|
||||||
|
$items[$hostKey][$type] = $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $items;
|
||||||
|
}
|
||||||
|
|
||||||
private function ensureGroupSchema(string $driver): void
|
private function ensureGroupSchema(string $driver): void
|
||||||
{
|
{
|
||||||
if ($driver === 'pgsql') {
|
if ($driver === 'pgsql') {
|
||||||
@@ -362,9 +465,11 @@ final class KeaHostMetadataRepository
|
|||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name TEXT NOT NULL UNIQUE,
|
name TEXT NOT NULL UNIQUE,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
|
parent_id BIGINT REFERENCES nexus_dhcp_groups(id) ON DELETE SET NULL,
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
)"
|
)"
|
||||||
);
|
);
|
||||||
|
$this->ensureColumn('nexus_dhcp_groups', 'parent_id', 'BIGINT');
|
||||||
$this->pdo->exec(
|
$this->pdo->exec(
|
||||||
"CREATE TABLE IF NOT EXISTS nexus_dhcp_group_ranges (
|
"CREATE TABLE IF NOT EXISTS nexus_dhcp_group_ranges (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
@@ -383,9 +488,11 @@ final class KeaHostMetadataRepository
|
|||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
name TEXT NOT NULL UNIQUE,
|
name TEXT NOT NULL UNIQUE,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
|
parent_id INTEGER,
|
||||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
)"
|
)"
|
||||||
);
|
);
|
||||||
|
$this->ensureColumn('nexus_dhcp_groups', 'parent_id', 'INTEGER');
|
||||||
$this->pdo->exec(
|
$this->pdo->exec(
|
||||||
"CREATE TABLE IF NOT EXISTS nexus_dhcp_group_ranges (
|
"CREATE TABLE IF NOT EXISTS nexus_dhcp_group_ranges (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -403,9 +510,11 @@ final class KeaHostMetadataRepository
|
|||||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
name VARCHAR(190) NOT NULL UNIQUE,
|
name VARCHAR(190) NOT NULL UNIQUE,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
|
parent_id BIGINT NULL,
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
)"
|
)"
|
||||||
);
|
);
|
||||||
|
$this->ensureColumn('nexus_dhcp_groups', 'parent_id', 'BIGINT NULL');
|
||||||
$this->pdo->exec(
|
$this->pdo->exec(
|
||||||
"CREATE TABLE IF NOT EXISTS nexus_dhcp_group_ranges (
|
"CREATE TABLE IF NOT EXISTS nexus_dhcp_group_ranges (
|
||||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
@@ -418,6 +527,60 @@ final class KeaHostMetadataRepository
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function ensureCheckSchema(string $driver): void
|
||||||
|
{
|
||||||
|
if ($driver === 'pgsql') {
|
||||||
|
$this->pdo->exec(
|
||||||
|
"CREATE TABLE IF NOT EXISTS nexus_dhcp_device_checks (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
host_key TEXT NOT NULL,
|
||||||
|
check_type TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL,
|
||||||
|
result_json JSONB,
|
||||||
|
checked_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
next_check_at TIMESTAMPTZ
|
||||||
|
)"
|
||||||
|
);
|
||||||
|
$this->pdo->exec(
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_nexus_dhcp_device_checks_host_type
|
||||||
|
ON nexus_dhcp_device_checks (host_key, check_type, checked_at)'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($driver === 'sqlite') {
|
||||||
|
$this->pdo->exec(
|
||||||
|
"CREATE TABLE IF NOT EXISTS nexus_dhcp_device_checks (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
host_key TEXT NOT NULL,
|
||||||
|
check_type TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL,
|
||||||
|
result_json TEXT,
|
||||||
|
checked_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
next_check_at TEXT
|
||||||
|
)"
|
||||||
|
);
|
||||||
|
$this->pdo->exec(
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_nexus_dhcp_device_checks_host_type
|
||||||
|
ON nexus_dhcp_device_checks (host_key, check_type, checked_at)'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->pdo->exec(
|
||||||
|
"CREATE TABLE IF NOT EXISTS nexus_dhcp_device_checks (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
host_key VARCHAR(190) NOT NULL,
|
||||||
|
check_type VARCHAR(80) NOT NULL,
|
||||||
|
status VARCHAR(40) NOT NULL,
|
||||||
|
result_json JSON,
|
||||||
|
checked_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
next_check_at DATETIME NULL,
|
||||||
|
INDEX idx_nexus_dhcp_device_checks_host_type (host_key, check_type, checked_at)
|
||||||
|
)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private function groupIdByName(string $name): int
|
private function groupIdByName(string $name): int
|
||||||
{
|
{
|
||||||
$stmt = $this->pdo->prepare('SELECT id FROM nexus_dhcp_groups WHERE name = :name LIMIT 1');
|
$stmt = $this->pdo->prepare('SELECT id FROM nexus_dhcp_groups WHERE name = :name LIMIT 1');
|
||||||
|
|||||||
@@ -94,6 +94,10 @@ final class KeaHostRepository
|
|||||||
user_context,
|
user_context,
|
||||||
dhcp4_subnet_id AS subnet_id,
|
dhcp4_subnet_id AS subnet_id,
|
||||||
'reservation' AS source,
|
'reservation' AS source,
|
||||||
|
NULL AS lease_state,
|
||||||
|
NULL AS valid_lifetime,
|
||||||
|
NULL AS lease_expires_at,
|
||||||
|
NULL AS last_seen_at,
|
||||||
host_id AS sort_id,
|
host_id AS sort_id,
|
||||||
{$sortExpr} AS sort_time
|
{$sortExpr} AS sort_time
|
||||||
FROM hosts
|
FROM hosts
|
||||||
@@ -114,7 +118,13 @@ final class KeaHostRepository
|
|||||||
|
|
||||||
$macExpr = $this->hexExpression('hwaddr');
|
$macExpr = $this->hexExpression('hwaddr');
|
||||||
$ipExpr = $this->ipv4Expression('address');
|
$ipExpr = $this->ipv4Expression('address');
|
||||||
$expireExpr = $this->driver() === 'pgsql' ? 'expire::text' : 'CAST(expire AS CHAR)';
|
$driver = $this->driver();
|
||||||
|
$expireExpr = $driver === 'pgsql' ? 'expire::text' : 'CAST(expire AS CHAR)';
|
||||||
|
$lastSeenExpr = match ($driver) {
|
||||||
|
'pgsql' => '(expire - make_interval(secs => valid_lifetime))::text',
|
||||||
|
'mysql' => 'CAST(DATE_SUB(expire, INTERVAL valid_lifetime SECOND) AS CHAR)',
|
||||||
|
default => 'NULL',
|
||||||
|
};
|
||||||
|
|
||||||
$stmt = $this->pdo->prepare(
|
$stmt = $this->pdo->prepare(
|
||||||
"SELECT
|
"SELECT
|
||||||
@@ -125,6 +135,10 @@ final class KeaHostRepository
|
|||||||
user_context,
|
user_context,
|
||||||
subnet_id,
|
subnet_id,
|
||||||
'lease' AS source,
|
'lease' AS source,
|
||||||
|
state AS lease_state,
|
||||||
|
valid_lifetime,
|
||||||
|
{$expireExpr} AS lease_expires_at,
|
||||||
|
{$lastSeenExpr} AS last_seen_at,
|
||||||
address AS sort_id,
|
address AS sort_id,
|
||||||
{$expireExpr} AS sort_time
|
{$expireExpr} AS sort_time
|
||||||
FROM lease4
|
FROM lease4
|
||||||
@@ -197,6 +211,10 @@ final class KeaHostRepository
|
|||||||
user_context,
|
user_context,
|
||||||
dhcp4_subnet_id AS subnet_id,
|
dhcp4_subnet_id AS subnet_id,
|
||||||
'reservation' AS source,
|
'reservation' AS source,
|
||||||
|
NULL AS lease_state,
|
||||||
|
NULL AS valid_lifetime,
|
||||||
|
NULL AS lease_expires_at,
|
||||||
|
NULL AS last_seen_at,
|
||||||
host_id AS sort_id,
|
host_id AS sort_id,
|
||||||
{$sortExpr} AS sort_time
|
{$sortExpr} AS sort_time
|
||||||
FROM hosts
|
FROM hosts
|
||||||
|
|||||||
Reference in New Issue
Block a user