diff --git a/modules/kea/pages/index.php b/modules/kea/pages/index.php index 01dc426..7289860 100644 --- a/modules/kea/pages/index.php +++ b/modules/kea/pages/index.php @@ -6,7 +6,6 @@ use App\Repository\KeaHostMetadataRepository; $module = modules()->get('kea'); $fallback = $module['db_defaults'] ?? []; -$pdo = modules()->modulePdo('kea', $fallback); $settings = modules()->settings('kea'); $metadataFallback = is_array($module['metadata_db_defaults'] ?? null) ? $module['metadata_db_defaults'] : []; $metadataConfig = is_array($settings['metadata_db'] ?? null) @@ -15,11 +14,18 @@ $metadataConfig = is_array($settings['metadata_db'] ?? null) $metadataRepo = null; $hosts = []; $error = null; +$warnings = []; try { + $pdo = modules()->modulePdo('kea', $fallback); if (!empty($metadataConfig['driver']) && !empty($metadataConfig['dbname'])) { - $metadataRepo = new KeaHostMetadataRepository(Database::createFromArray($metadataConfig)); - $metadataRepo->ensureSchema(); + try { + $metadataRepo = new KeaHostMetadataRepository(Database::createFromArray($metadataConfig)); + $metadataRepo->ensureSchema(); + } catch (\Throwable $e) { + $warnings[] = 'Nexus DHCP Zusatzdatenbank nicht verfuegbar: ' . $e->getMessage(); + $metadataRepo = null; + } } $repo = new KeaHostRepository($pdo, $metadataRepo); @@ -28,4 +34,4 @@ try { $error = "Datenbankfehler: " . $e->getMessage(); } -module_tpl('kea', 'dashboard', compact('hosts', 'error')); +module_tpl('kea', 'dashboard', compact('hosts', 'error', 'warnings')); diff --git a/modules/kea/partials/dashboard.php b/modules/kea/partials/dashboard.php index 4f61022..a1fda56 100644 --- a/modules/kea/partials/dashboard.php +++ b/modules/kea/partials/dashboard.php @@ -2,6 +2,7 @@ /** * @var array $hosts Die Liste der KEA-Hosts. * @var string|null $error Eine Fehlermeldung, falls vorhanden. + * @var array $warnings Hinweise, falls Zusatzdaten nicht geladen werden konnten. */ ?>
@@ -19,6 +20,12 @@
+ + + +

@@ -32,6 +39,7 @@ + @@ -45,11 +53,16 @@ - + + diff --git a/src/Repository/KeaHostRepository.php b/src/Repository/KeaHostRepository.php index 8986978..416cea8 100644 --- a/src/Repository/KeaHostRepository.php +++ b/src/Repository/KeaHostRepository.php @@ -22,21 +22,42 @@ final class KeaHostRepository public function findAll(int $limit = 50): array { try { - // 'dhcp_identifier' ist in KEA i.d.R. die MAC-Adresse (bei type=1) - $stmt = $this->pdo->prepare( - 'SELECT host_id, dhcp_identifier, ipv4_address, hostname, user_context - FROM hosts - ORDER BY host_id DESC - LIMIT :limit' - ); - $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); - $stmt->execute(); + $hasReservations = $this->tableExists('hosts'); + $hasLeases = $this->tableExists('lease4'); + if (!$hasReservations && !$hasLeases) { + throw new \RuntimeException( + 'Im aktuellen KEA-DB-Schema wurden weder hosts noch lease4 gefunden. Bitte Datenbank, Schema/Search-Path und Benutzerrechte pruefen.' + ); + } - return $this->withMetadata($stmt->fetchAll(PDO::FETCH_ASSOC)); + $hosts = []; + if ($hasReservations) { + foreach ($this->findReservations($limit) as $host) { + $hosts[] = $host; + } + } + + if ($hasLeases) { + foreach ($this->findLeases($limit) as $lease) { + $hosts[] = $lease; + } + } + + usort($hosts, static function (array $a, array $b): int { + $aTime = (string)($a['sort_time'] ?? ''); + $bTime = (string)($b['sort_time'] ?? ''); + if ($aTime !== '' || $bTime !== '') { + return strcmp($bTime, $aTime); + } + + return (int)($b['sort_id'] ?? 0) <=> (int)($a['sort_id'] ?? 0); + }); + + return array_slice($this->withMetadata($hosts), 0, $limit); } catch (\PDOException $e) { if ($this->isMissingTable($e)) { throw new \RuntimeException( - 'KEA schema not initialized. Enable APP_DB_AUTO_INIT or run kea-admin db-init pgsql.', + 'KEA schema not initialized or expected tables are missing. Expected hosts and/or lease4.', 0, $e ); @@ -45,6 +66,67 @@ final class KeaHostRepository } } + private function findReservations(int $limit): array + { + if (!$this->tableExists('hosts')) { + return []; + } + + $driver = $this->driver(); + $macExpr = $this->hexExpression('dhcp_identifier'); + $ipExpr = $this->ipv4Expression('ipv4_address'); + $sortExpr = $driver === 'pgsql' ? 'host_id::text' : 'CAST(host_id AS CHAR)'; + + $stmt = $this->pdo->prepare( + "SELECT + host_id, + {$macExpr} AS dhcp_identifier_hex, + {$ipExpr} AS ipv4_address_text, + hostname, + user_context, + 'reservation' AS source, + host_id AS sort_id, + {$sortExpr} AS sort_time + FROM hosts + ORDER BY host_id DESC + LIMIT :limit" + ); + $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); + $stmt->execute(); + + return array_map(fn(array $row): array => $this->normalizeRow($row), $stmt->fetchAll(PDO::FETCH_ASSOC)); + } + + private function findLeases(int $limit): array + { + if (!$this->tableExists('lease4')) { + return []; + } + + $macExpr = $this->hexExpression('hwaddr'); + $ipExpr = $this->ipv4Expression('address'); + $expireExpr = $this->driver() === 'pgsql' ? 'expire::text' : 'CAST(expire AS CHAR)'; + + $stmt = $this->pdo->prepare( + "SELECT + address AS host_id, + {$macExpr} AS dhcp_identifier_hex, + {$ipExpr} AS ipv4_address_text, + hostname, + user_context, + 'lease' AS source, + address AS sort_id, + {$expireExpr} AS sort_time + FROM lease4 + ORDER BY expire DESC + LIMIT :limit" + ); + $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); + $stmt->execute(); + + return array_map(fn(array $row): array => $this->normalizeRow($row), $stmt->fetchAll(PDO::FETCH_ASSOC)); + } + /** * Sucht einen Host anhand der MAC-Adresse (dhcp_identifier). */ @@ -78,7 +160,7 @@ final class KeaHostRepository private function isMissingTable(\PDOException $e): bool { - return $e->getCode() === '42P01'; + return in_array((string)$e->getCode(), ['42P01', '42S02'], true); } /** @@ -119,10 +201,15 @@ final class KeaHostRepository return $hosts; } - $metadataByHost = $this->metadata->findByHostIds(array_column($hosts, 'host_id')); + $metadataByHost = $this->metadata->findByHostIds( + array_column( + array_filter($hosts, static fn(array $host): bool => ($host['source'] ?? '') === 'reservation'), + 'host_id' + ) + ); foreach ($hosts as &$host) { $hostId = (int)($host['host_id'] ?? 0); - $host['metadata'] = $metadataByHost[$hostId] ?? []; + $host['metadata'] = ($host['source'] ?? '') === 'reservation' ? ($metadataByHost[$hostId] ?? []) : []; } unset($host); @@ -134,6 +221,82 @@ final class KeaHostRepository return (string)$this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME); } + private function tableExists(string $table): bool + { + $driver = $this->driver(); + + if ($driver === 'pgsql') { + $stmt = $this->pdo->prepare( + "SELECT 1 + FROM information_schema.tables + WHERE table_schema = current_schema() + AND table_name = :table + LIMIT 1" + ); + $stmt->execute(['table' => $table]); + return (bool)$stmt->fetchColumn(); + } + + if ($driver === 'sqlite') { + $stmt = $this->pdo->prepare( + "SELECT 1 + FROM sqlite_master + WHERE type = 'table' + AND name = :table + LIMIT 1" + ); + $stmt->execute(['table' => $table]); + return (bool)$stmt->fetchColumn(); + } + + $stmt = $this->pdo->prepare( + "SELECT 1 + FROM information_schema.tables + WHERE table_schema = DATABASE() + AND table_name = :table + LIMIT 1" + ); + $stmt->execute(['table' => $table]); + return (bool)$stmt->fetchColumn(); + } + + private function hexExpression(string $column): string + { + return match ($this->driver()) { + 'pgsql' => "encode({$column}, 'hex')", + default => "HEX({$column})", + }; + } + + private function ipv4Expression(string $column): string + { + return match ($this->driver()) { + 'pgsql' => "host('0.0.0.0'::inet + ({$column})::bigint)", + 'mysql' => "INET_NTOA({$column})", + default => "CAST({$column} AS TEXT)", + }; + } + + private function normalizeRow(array $row): array + { + $row['dhcp_identifier'] = $this->formatMac((string)($row['dhcp_identifier_hex'] ?? '')); + $row['ipv4_address'] = (string)($row['ipv4_address_text'] ?? ''); + $row['hostname'] = (string)($row['hostname'] ?? ''); + $row['metadata'] = []; + + return $row; + } + + private function formatMac(string $hex): string + { + $hex = strtolower(preg_replace('/[^a-fA-F0-9]/', '', $hex) ?? ''); + if ($hex === '') { + return ''; + } + + return implode(':', str_split($hex, 2)); + } + private function lastInsertIdSafe(): int { $driver = $this->driver();
Quelle Hostname IP Adresse MAC Adresse
Keine Hosts gefunden. + Keine Reservierungen oder aktiven Leases gefunden. +
+ +