KEA Setup
All checks were successful
Deploy / deploy-staging (push) Successful in 6s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-04-13 02:13:43 +02:00
parent 4d73eec687
commit 677f9314f5
8 changed files with 285 additions and 73 deletions

View File

@@ -98,10 +98,6 @@ final class Database
}
}
// After init, ensure our metadata table exists (non-invasive)
if (self::tableExists($pdo, 'hosts')) {
self::ensureNexusTables($pdo);
}
}
private static function tableExists(\PDO $pdo, string $table): bool
@@ -179,37 +175,6 @@ final class Database
$pdo->exec($sql);
}
private static function ensureNexusTables(\PDO $pdo): void
{
$pdo->exec(
"CREATE TABLE IF NOT EXISTS nexus_host_meta (
host_id BIGINT PRIMARY KEY,
location TEXT,
device_type TEXT,
owner TEXT,
tags JSONB,
notes TEXT,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)"
);
$pdo->exec(
"DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE constraint_name = 'fk_nexus_host_meta_host'
) THEN
ALTER TABLE nexus_host_meta
ADD CONSTRAINT fk_nexus_host_meta_host
FOREIGN KEY (host_id)
REFERENCES hosts(host_id)
ON DELETE CASCADE;
END IF;
END $$;"
);
}
private static function buildMysqlDsn(array $db): string
{
if (empty($db['dbname'])) {

View File

@@ -258,6 +258,7 @@ final class ModuleManager
'menu' => $data['menu'] ?? [],
'sidebar' => $data['sidebar'] ?? [],
'db_defaults' => $data['db_defaults'] ?? [],
'metadata_db_defaults' => $data['metadata_db_defaults'] ?? [],
'path' => $dir,
'entry' => '/module/' . rawurlencode($name),
'auth' => is_array($data['auth'] ?? null) ? $data['auth'] : ['required' => false, 'users' => [], 'groups' => []],

View File

@@ -0,0 +1,170 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use PDO;
final class KeaHostMetadataRepository
{
public function __construct(private PDO $pdo) {}
public function ensureSchema(): void
{
$driver = (string)$this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
if ($driver === 'pgsql') {
$this->pdo->exec(
"CREATE TABLE IF NOT EXISTS nexus_dhcp_host_meta (
host_id BIGINT PRIMARY KEY,
hardware_address TEXT,
ip_address TEXT,
real_name TEXT,
device_name TEXT,
owner TEXT,
location TEXT,
device_type TEXT,
notes TEXT,
tags_json JSONB,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)"
);
return;
}
if ($driver === 'sqlite') {
$this->pdo->exec(
"CREATE TABLE IF NOT EXISTS nexus_dhcp_host_meta (
host_id INTEGER PRIMARY KEY,
hardware_address TEXT,
ip_address TEXT,
real_name TEXT,
device_name TEXT,
owner TEXT,
location TEXT,
device_type TEXT,
notes TEXT,
tags_json TEXT,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
)"
);
return;
}
$this->pdo->exec(
"CREATE TABLE IF NOT EXISTS nexus_dhcp_host_meta (
host_id BIGINT PRIMARY KEY,
hardware_address TEXT,
ip_address TEXT,
real_name TEXT,
device_name TEXT,
owner TEXT,
location TEXT,
device_type TEXT,
notes TEXT,
tags_json TEXT,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
)"
);
}
public function findByHostIds(array $hostIds): array
{
$hostIds = array_values(array_unique(array_filter(array_map('intval', $hostIds))));
if ($hostIds === []) {
return [];
}
$params = [];
$placeholders = [];
foreach ($hostIds as $idx => $hostId) {
$key = ':host_id_' . $idx;
$placeholders[] = $key;
$params[$key] = $hostId;
}
$stmt = $this->pdo->prepare(
'SELECT host_id, hardware_address, ip_address, real_name, device_name, owner, location, device_type, notes, tags_json, updated_at
FROM nexus_dhcp_host_meta
WHERE host_id IN (' . implode(', ', $placeholders) . ')'
);
foreach ($params as $key => $value) {
$stmt->bindValue($key, $value, PDO::PARAM_INT);
}
$stmt->execute();
$items = [];
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$items[(int)$row['host_id']] = $row;
}
return $items;
}
public function saveForHost(int $hostId, string $hardwareAddress, string $ipAddress, array $metadata): void
{
$metadata = array_merge([
'real_name' => null,
'device_name' => null,
'owner' => null,
'location' => null,
'device_type' => null,
'notes' => null,
'tags' => [],
], $metadata);
$tagsJson = json_encode($metadata['tags'], JSON_UNESCAPED_UNICODE);
if ($tagsJson === false) {
$tagsJson = '[]';
}
$driver = (string)$this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
if ($driver === 'pgsql') {
$stmt = $this->pdo->prepare(
"INSERT INTO nexus_dhcp_host_meta (
host_id, hardware_address, ip_address, real_name, device_name, owner, location, device_type, notes, tags_json, updated_at
) VALUES (
:host_id, :hardware_address, :ip_address, :real_name, :device_name, :owner, :location, :device_type, :notes, CAST(:tags_json AS jsonb), NOW()
)
ON CONFLICT (host_id) DO UPDATE SET
hardware_address = EXCLUDED.hardware_address,
ip_address = EXCLUDED.ip_address,
real_name = EXCLUDED.real_name,
device_name = EXCLUDED.device_name,
owner = EXCLUDED.owner,
location = EXCLUDED.location,
device_type = EXCLUDED.device_type,
notes = EXCLUDED.notes,
tags_json = EXCLUDED.tags_json,
updated_at = NOW()"
);
} else {
$stmt = $this->pdo->prepare(
"REPLACE INTO nexus_dhcp_host_meta (
host_id, hardware_address, ip_address, real_name, device_name, owner, location, device_type, notes, tags_json, updated_at
) VALUES (
:host_id, :hardware_address, :ip_address, :real_name, :device_name, :owner, :location, :device_type, :notes, :tags_json, CURRENT_TIMESTAMP
)"
);
}
$stmt->execute([
'host_id' => $hostId,
'hardware_address' => $hardwareAddress,
'ip_address' => $ipAddress,
'real_name' => $this->nullableString($metadata['real_name']),
'device_name' => $this->nullableString($metadata['device_name']),
'owner' => $this->nullableString($metadata['owner']),
'location' => $this->nullableString($metadata['location']),
'device_type' => $this->nullableString($metadata['device_type']),
'notes' => $this->nullableString($metadata['notes']),
'tags_json' => $tagsJson,
]);
}
private function nullableString(mixed $value): ?string
{
$value = trim((string)$value);
return $value !== '' ? $value : null;
}
}

View File

@@ -7,11 +7,14 @@ use PDO;
/**
* Repository für das KEA DHCP Host-Management.
* Interagiert direkt mit der 'hosts' Tabelle in der PostgreSQL-Datenbank.
* Interagiert direkt mit der 'hosts' Tabelle in der KEA-Datenbank.
*/
final class KeaHostRepository
{
public function __construct(private PDO $pdo) {}
public function __construct(
private PDO $pdo,
private ?KeaHostMetadataRepository $metadata = null
) {}
/**
* Ruft eine Liste aller Host-Reservierungen ab (Geräte-Inventar).
@@ -29,7 +32,7 @@ final class KeaHostRepository
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
return $this->withMetadata($stmt->fetchAll(PDO::FETCH_ASSOC));
} catch (\PDOException $e) {
if ($this->isMissingTable($e)) {
throw new \RuntimeException(
@@ -85,28 +88,45 @@ final class KeaHostRepository
* @param string $ip IPv4-Adresse
* @param int $subnetId Die ID des Subnets (dhcp4_subnet_id), in dem die IP liegt
* @param string|null $hostname Optionaler Hostname
* @param array $metadata Zusätzliche Infos (Standort, Verantwortlicher etc.) für 'user_context'
* @param array $metadata Zusätzliche Infos fuer die separate Nexus-DHCP-Metadatenbank.
*/
public function create(string $mac, string $ip, int $subnetId, ?string $hostname = null, array $metadata = []): int
{
// Metadaten werden im KEA-Standardfeld 'user_context' als JSON gespeichert
$userContextJson = json_encode($metadata, JSON_THROW_ON_ERROR);
// dhcp_identifier_type 1 = HW_ADDRESS (Ethernet)
$stmt = $this->pdo->prepare(
'INSERT INTO hosts (dhcp_identifier, dhcp_identifier_type, dhcp4_subnet_id, ipv4_address, hostname, user_context)
VALUES (:mac, 1, :subnetId, :ip, :hostname, :user_context)'
'INSERT INTO hosts (dhcp_identifier, dhcp_identifier_type, dhcp4_subnet_id, ipv4_address, hostname)
VALUES (:mac, 1, :subnetId, :ip, :hostname)'
);
$stmt->execute([
'mac' => $mac,
'subnetId' => $subnetId,
'ip' => $ip,
'hostname' => $hostname,
'user_context' => $userContextJson,
'mac' => $mac,
'subnetId' => $subnetId,
'ip' => $ip,
'hostname' => $hostname,
]);
return $this->lastInsertIdSafe();
$hostId = $this->lastInsertIdSafe();
if ($this->metadata !== null && $metadata !== []) {
$this->metadata->saveForHost($hostId, $mac, $ip, $metadata);
}
return $hostId;
}
private function withMetadata(array $hosts): array
{
if ($hosts === [] || $this->metadata === null) {
return $hosts;
}
$metadataByHost = $this->metadata->findByHostIds(array_column($hosts, 'host_id'));
foreach ($hosts as &$host) {
$hostId = (int)($host['host_id'] ?? 0);
$host['metadata'] = $metadataByHost[$hostId] ?? [];
}
unset($host);
return $hosts;
}
private function driver(): string