diff --git a/modules/kea/module.json b/modules/kea/module.json
index 8ea25ba..f1d5072 100644
--- a/modules/kea/module.json
+++ b/modules/kea/module.json
@@ -17,13 +17,20 @@
},
"setup": {
"fields": [
- { "name": "db.driver", "label": "DB Driver", "type": "text", "required": true },
- { "name": "db.host", "label": "DB Host", "type": "text", "required": true },
- { "name": "db.port", "label": "DB Port", "type": "number", "required": true },
- { "name": "db.dbname", "label": "DB Name", "type": "text", "required": true },
- { "name": "db.schema", "label": "DB Schema", "type": "text", "required": false },
- { "name": "db.user", "label": "DB User", "type": "text", "required": true },
- { "name": "db.password", "label": "DB Passwort", "type": "password", "required": true },
+ { "name": "db.driver", "label": "KEA DB Driver", "type": "text", "required": true, "help": "Standard-KEA-Datenbank, die auch vom KEA-Dienst selbst genutzt wird." },
+ { "name": "db.host", "label": "KEA DB Host", "type": "text", "required": true },
+ { "name": "db.port", "label": "KEA DB Port", "type": "number", "required": true },
+ { "name": "db.dbname", "label": "KEA DB Name", "type": "text", "required": true },
+ { "name": "db.schema", "label": "KEA DB Schema", "type": "text", "required": false },
+ { "name": "db.user", "label": "KEA DB User", "type": "text", "required": true },
+ { "name": "db.password", "label": "KEA DB Passwort", "type": "password", "required": true },
+ { "name": "metadata_db.driver", "label": "Nexus DHCP DB Driver", "type": "text", "required": true, "help": "Separate Datenbank fuer Nexus-eigene DHCP-Zusatzinfos, nicht fuer KEA-Standardtabellen." },
+ { "name": "metadata_db.host", "label": "Nexus DHCP DB Host", "type": "text", "required": true },
+ { "name": "metadata_db.port", "label": "Nexus DHCP DB Port", "type": "number", "required": true },
+ { "name": "metadata_db.dbname", "label": "Nexus DHCP DB Name", "type": "text", "required": true },
+ { "name": "metadata_db.schema", "label": "Nexus DHCP DB Schema", "type": "text", "required": false },
+ { "name": "metadata_db.user", "label": "Nexus DHCP DB User", "type": "text", "required": true },
+ { "name": "metadata_db.password", "label": "Nexus DHCP DB Passwort", "type": "password", "required": true },
{ "name": "kea_db_version", "label": "KEA DB Version", "type": "text", "required": false },
{ "name": "kea_init_script", "label": "KEA Init Script", "type": "text", "required": false },
{ "name": "kea_init_cmd", "label": "KEA Init Command", "type": "text", "required": false },
@@ -38,5 +45,14 @@
"schema": "public",
"user": "",
"password": ""
+ },
+ "metadata_db_defaults": {
+ "driver": "pgsql",
+ "host": "192.168.178.10",
+ "port": 5432,
+ "dbname": "",
+ "schema": "public",
+ "user": "",
+ "password": ""
}
}
diff --git a/modules/kea/pages/index.php b/modules/kea/pages/index.php
index 3d27b02..01dc426 100644
--- a/modules/kea/pages/index.php
+++ b/modules/kea/pages/index.php
@@ -1,15 +1,28 @@
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)
+ ? array_replace($metadataFallback, $settings['metadata_db'])
+ : $metadataFallback;
+$metadataRepo = null;
$hosts = [];
$error = null;
try {
- $repo = new KeaHostRepository($pdo);
+ if (!empty($metadataConfig['driver']) && !empty($metadataConfig['dbname'])) {
+ $metadataRepo = new KeaHostMetadataRepository(Database::createFromArray($metadataConfig));
+ $metadataRepo->ensureSchema();
+ }
+
+ $repo = new KeaHostRepository($pdo, $metadataRepo);
$hosts = $repo->findAll(50);
} catch (\Exception $e) {
$error = "Datenbankfehler: " . $e->getMessage();
diff --git a/modules/kea/partials/dashboard.php b/modules/kea/partials/dashboard.php
index b3bffff..4f61022 100644
--- a/modules/kea/partials/dashboard.php
+++ b/modules/kea/partials/dashboard.php
@@ -35,7 +35,8 @@
Hostname |
IP Adresse |
MAC Adresse |
- Kontext |
+ Echter Name |
+ Standort |
Edit
|
@@ -44,7 +45,7 @@
- | Keine Hosts gefunden. |
+ Keine Hosts gefunden. |
@@ -59,7 +60,10 @@
= e($host['dhcp_identifier']) ?>
- = e($host['user_context'] ?? '-') ?>
+ = e((string)($host['metadata']['real_name'] ?? '-')) ?>
+ |
+
+ = e((string)($host['metadata']['location'] ?? '-')) ?>
|
Bearbeiten
@@ -71,4 +75,4 @@
-
\ No newline at end of file
+
diff --git a/partials/landingpages/modules/setup.php b/partials/landingpages/modules/setup.php
index d925a05..4380afe 100644
--- a/partials/landingpages/modules/setup.php
+++ b/partials/landingpages/modules/setup.php
@@ -28,10 +28,39 @@ $defaults = $module['db_defaults'] ?? [];
if (empty($current['db']) && is_array($defaults)) {
$current['db'] = $defaults;
}
+$metadataDefaults = $module['metadata_db_defaults'] ?? [];
+if (empty($current['metadata_db']) && is_array($metadataDefaults)) {
+ $current['metadata_db'] = $metadataDefaults;
+}
+
+$setNested = function (array &$target, string $path, mixed $value): void {
+ $parts = explode('.', $path);
+ $last = array_pop($parts);
+ $node = &$target;
+ foreach ($parts as $part) {
+ if (!isset($node[$part]) || !is_array($node[$part])) {
+ $node[$part] = [];
+ }
+ $node = &$node[$part];
+ }
+ if ($last !== null && $last !== '') {
+ $node[$last] = $value;
+ }
+};
+
+$getNested = function (array $source, string $path): mixed {
+ $node = $source;
+ foreach (explode('.', $path) as $part) {
+ if (!is_array($node) || !array_key_exists($part, $node)) {
+ return null;
+ }
+ $node = $node[$part];
+ }
+ return $node;
+};
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$payload = [];
- $db = $current['db'] ?? [];
foreach ($fields as $field) {
$name = (string)($field['name'] ?? '');
@@ -55,19 +84,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
continue;
}
- if (str_starts_with($name, 'db.')) {
- $key = substr($name, 3);
- $db[$key] = $value;
+ if (str_contains($name, '.')) {
+ $setNested($payload, $name, $value);
continue;
}
$payload[$name] = $value;
}
- if (!empty($db)) {
- $payload['db'] = $db;
- }
-
modules()->saveSettings($moduleName, $payload);
modules()->saveAuth($moduleName, [
'required' => isset($_POST['auth_required']),
@@ -107,9 +131,8 @@ $authConfig = is_array($module['auth'] ?? null) ? $module['auth'] : ['required'
$value = '';
if ($name === 'kea_auto_init') {
$value = !empty($current[$name]) ? '1' : '0';
- } elseif (str_starts_with($name, 'db.')) {
- $key = substr($name, 3);
- $value = (string)($current['db'][$key] ?? '');
+ } elseif (str_contains($name, '.')) {
+ $value = (string)($getNested($current, $name) ?? '');
} else {
$value = (string)($current[$name] ?? '');
}
diff --git a/src/App/Database.php b/src/App/Database.php
index 692711f..a798b16 100755
--- a/src/App/Database.php
+++ b/src/App/Database.php
@@ -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'])) {
diff --git a/src/App/ModuleManager.php b/src/App/ModuleManager.php
index c3004b2..0bf5b6d 100644
--- a/src/App/ModuleManager.php
+++ b/src/App/ModuleManager.php
@@ -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' => []],
diff --git a/src/Repository/KeaHostMetadataRepository.php b/src/Repository/KeaHostMetadataRepository.php
new file mode 100644
index 0000000..dee270f
--- /dev/null
+++ b/src/Repository/KeaHostMetadataRepository.php
@@ -0,0 +1,170 @@
+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;
+ }
+}
diff --git a/src/Repository/KeaHostRepository.php b/src/Repository/KeaHostRepository.php
index eab654a..8986978 100644
--- a/src/Repository/KeaHostRepository.php
+++ b/src/Repository/KeaHostRepository.php
@@ -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
|