adasd
This commit is contained in:
@@ -40,6 +40,17 @@ final class BaseSchema
|
||||
)"
|
||||
);
|
||||
|
||||
$pdo->exec(
|
||||
"CREATE TABLE IF NOT EXISTS nexus_module_migrations (
|
||||
module_name TEXT NOT NULL,
|
||||
migration TEXT NOT NULL,
|
||||
version TEXT,
|
||||
checksum TEXT NOT NULL,
|
||||
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (module_name, migration)
|
||||
)"
|
||||
);
|
||||
|
||||
$pdo->exec(
|
||||
"CREATE TABLE IF NOT EXISTS nexus_module_auth (
|
||||
name TEXT PRIMARY KEY,
|
||||
@@ -117,6 +128,17 @@ final class BaseSchema
|
||||
)"
|
||||
);
|
||||
|
||||
$pdo->exec(
|
||||
"CREATE TABLE IF NOT EXISTS nexus_module_migrations (
|
||||
module_name TEXT NOT NULL,
|
||||
migration TEXT NOT NULL,
|
||||
version TEXT,
|
||||
checksum TEXT NOT NULL,
|
||||
applied_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
PRIMARY KEY (module_name, migration)
|
||||
)"
|
||||
);
|
||||
|
||||
$pdo->exec(
|
||||
"CREATE TABLE IF NOT EXISTS nexus_module_auth (
|
||||
name TEXT PRIMARY KEY,
|
||||
@@ -194,6 +216,17 @@ final class BaseSchema
|
||||
)"
|
||||
);
|
||||
|
||||
$pdo->exec(
|
||||
"CREATE TABLE IF NOT EXISTS nexus_module_migrations (
|
||||
module_name VARCHAR(190) NOT NULL,
|
||||
migration VARCHAR(190) NOT NULL,
|
||||
version VARCHAR(64),
|
||||
checksum VARCHAR(64) NOT NULL,
|
||||
applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (module_name, migration)
|
||||
)"
|
||||
);
|
||||
|
||||
$pdo->exec(
|
||||
"CREATE TABLE IF NOT EXISTS nexus_module_auth (
|
||||
name VARCHAR(190) PRIMARY KEY,
|
||||
|
||||
@@ -65,6 +65,24 @@ final class ModuleManager
|
||||
$this->modules[$name]['enabled'] = $enabled;
|
||||
}
|
||||
|
||||
public function migrationStatus(string $name): array
|
||||
{
|
||||
if (!$this->basePdo || !isset($this->modules[$name])) {
|
||||
return ['available' => 0, 'applied' => 0, 'pending' => [], 'changed' => []];
|
||||
}
|
||||
|
||||
return (new ModuleMigrationService($this->basePdo, $this))->status($this->modules[$name]);
|
||||
}
|
||||
|
||||
public function applyPendingMigrations(string $name): array
|
||||
{
|
||||
if (!$this->basePdo || !isset($this->modules[$name])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (new ModuleMigrationService($this->basePdo, $this))->applyPending($this->modules[$name]);
|
||||
}
|
||||
|
||||
public function settings(string $name): array
|
||||
{
|
||||
if (!$this->basePdo) {
|
||||
@@ -260,6 +278,7 @@ final class ModuleManager
|
||||
'sidebar' => $data['sidebar'] ?? [],
|
||||
'db_defaults' => $data['db_defaults'] ?? [],
|
||||
'metadata_db_defaults' => $data['metadata_db_defaults'] ?? [],
|
||||
'schema_version' => (int)($data['schema_version'] ?? 0),
|
||||
'path' => $dir,
|
||||
'entry' => '/module/' . rawurlencode($name),
|
||||
'auth' => $this->loadAuth($name, $manifestAuth),
|
||||
|
||||
18
src/App/ModuleMigrationContext.php
Normal file
18
src/App/ModuleMigrationContext.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App;
|
||||
|
||||
final class ModuleMigrationContext
|
||||
{
|
||||
public function __construct(
|
||||
public readonly \PDO $basePdo,
|
||||
public readonly ModuleManager $modules,
|
||||
public readonly array $module
|
||||
) {}
|
||||
|
||||
public function settings(): array
|
||||
{
|
||||
return $this->modules->settings((string)$this->module['name']);
|
||||
}
|
||||
}
|
||||
172
src/App/ModuleMigrationService.php
Normal file
172
src/App/ModuleMigrationService.php
Normal file
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App;
|
||||
|
||||
final class ModuleMigrationService
|
||||
{
|
||||
public function __construct(
|
||||
private \PDO $basePdo,
|
||||
private ModuleManager $modules
|
||||
) {}
|
||||
|
||||
public function status(array $module): array
|
||||
{
|
||||
$migrations = $this->discover($module);
|
||||
$applied = $this->applied((string)$module['name']);
|
||||
$pending = [];
|
||||
$changed = [];
|
||||
|
||||
foreach ($migrations as $migration) {
|
||||
$id = $migration['id'];
|
||||
if (!isset($applied[$id])) {
|
||||
$pending[] = $migration;
|
||||
continue;
|
||||
}
|
||||
if (($applied[$id]['checksum'] ?? '') !== $migration['checksum']) {
|
||||
$changed[] = $migration;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'available' => count($migrations),
|
||||
'applied' => count($applied),
|
||||
'pending' => $pending,
|
||||
'changed' => $changed,
|
||||
];
|
||||
}
|
||||
|
||||
public function applyPending(array $module): array
|
||||
{
|
||||
$status = $this->status($module);
|
||||
$applied = [];
|
||||
$moduleName = (string)$module['name'];
|
||||
$context = new ModuleMigrationContext($this->basePdo, $this->modules, $module);
|
||||
|
||||
foreach ($status['pending'] as $migration) {
|
||||
$this->basePdo->beginTransaction();
|
||||
try {
|
||||
$runner = require $migration['path'];
|
||||
if (is_object($runner) && method_exists($runner, 'up')) {
|
||||
$runner->up($context);
|
||||
} elseif (is_callable($runner)) {
|
||||
$runner($context);
|
||||
} else {
|
||||
throw new \RuntimeException('Migration does not return a callable or object with up().');
|
||||
}
|
||||
|
||||
$this->record($moduleName, $migration);
|
||||
$this->basePdo->commit();
|
||||
$applied[] = $migration;
|
||||
} catch (\Throwable $e) {
|
||||
if ($this->basePdo->inTransaction()) {
|
||||
$this->basePdo->rollBack();
|
||||
}
|
||||
throw new \RuntimeException(
|
||||
'Migration fehlgeschlagen: ' . $moduleName . '/' . $migration['id'] . ' - ' . $e->getMessage(),
|
||||
0,
|
||||
$e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($applied !== []) {
|
||||
$this->recordModuleVersion($moduleName, (string)($module['version'] ?? ''));
|
||||
}
|
||||
|
||||
return $applied;
|
||||
}
|
||||
|
||||
private function discover(array $module): array
|
||||
{
|
||||
$path = (string)($module['path'] ?? '') . '/migrations';
|
||||
if (!is_dir($path)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$items = [];
|
||||
foreach (glob($path . '/*.php') ?: [] as $file) {
|
||||
$id = basename($file, '.php');
|
||||
$items[] = [
|
||||
'id' => $id,
|
||||
'version' => $this->versionFromId($id),
|
||||
'path' => $file,
|
||||
'checksum' => hash_file('sha256', $file) ?: '',
|
||||
];
|
||||
}
|
||||
|
||||
usort($items, static fn(array $a, array $b): int => strnatcmp($a['id'], $b['id']));
|
||||
return $items;
|
||||
}
|
||||
|
||||
private function applied(string $moduleName): array
|
||||
{
|
||||
$stmt = $this->basePdo->prepare(
|
||||
"SELECT migration, version, checksum, applied_at
|
||||
FROM nexus_module_migrations
|
||||
WHERE module_name = :module
|
||||
ORDER BY migration ASC"
|
||||
);
|
||||
$stmt->execute(['module' => $moduleName]);
|
||||
|
||||
$items = [];
|
||||
foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
|
||||
$items[(string)$row['migration']] = $row;
|
||||
}
|
||||
return $items;
|
||||
}
|
||||
|
||||
private function record(string $moduleName, array $migration): void
|
||||
{
|
||||
$driver = (string)$this->basePdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
|
||||
if ($driver === 'pgsql' || $driver === 'sqlite') {
|
||||
$sql = "INSERT INTO nexus_module_migrations (module_name, migration, version, checksum, applied_at)
|
||||
VALUES (:module, :migration, :version, :checksum, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(module_name, migration) DO UPDATE SET
|
||||
version = excluded.version,
|
||||
checksum = excluded.checksum,
|
||||
applied_at = CURRENT_TIMESTAMP";
|
||||
} else {
|
||||
$sql = "INSERT INTO nexus_module_migrations (module_name, migration, version, checksum, applied_at)
|
||||
VALUES (:module, :migration, :version, :checksum, CURRENT_TIMESTAMP)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
version = VALUES(version),
|
||||
checksum = VALUES(checksum),
|
||||
applied_at = CURRENT_TIMESTAMP";
|
||||
}
|
||||
|
||||
$stmt = $this->basePdo->prepare($sql);
|
||||
$stmt->execute([
|
||||
'module' => $moduleName,
|
||||
'migration' => $migration['id'],
|
||||
'version' => $migration['version'],
|
||||
'checksum' => $migration['checksum'],
|
||||
]);
|
||||
}
|
||||
|
||||
private function recordModuleVersion(string $moduleName, string $version): void
|
||||
{
|
||||
if ($version === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$stmt = $this->basePdo->prepare(
|
||||
"UPDATE nexus_modules
|
||||
SET version = :version,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE name = :module"
|
||||
);
|
||||
$stmt->execute([
|
||||
'module' => $moduleName,
|
||||
'version' => $version,
|
||||
]);
|
||||
}
|
||||
|
||||
private function versionFromId(string $id): string
|
||||
{
|
||||
if (preg_match('/(?:^|_)(\d+\.\d+\.\d+)(?:_|$)/', $id, $m)) {
|
||||
return $m[1];
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -24,11 +24,15 @@ final class KeaHostMetadataRepository
|
||||
owner TEXT,
|
||||
location TEXT,
|
||||
device_type TEXT,
|
||||
group_name TEXT,
|
||||
desired_ip TEXT,
|
||||
notes TEXT,
|
||||
tags_json JSONB,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)"
|
||||
);
|
||||
$this->ensureColumn('nexus_dhcp_host_meta', 'group_name', 'TEXT');
|
||||
$this->ensureColumn('nexus_dhcp_host_meta', 'desired_ip', 'TEXT');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -43,11 +47,15 @@ final class KeaHostMetadataRepository
|
||||
owner TEXT,
|
||||
location TEXT,
|
||||
device_type TEXT,
|
||||
group_name TEXT,
|
||||
desired_ip TEXT,
|
||||
notes TEXT,
|
||||
tags_json TEXT,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)"
|
||||
);
|
||||
$this->ensureColumn('nexus_dhcp_host_meta', 'group_name', 'TEXT');
|
||||
$this->ensureColumn('nexus_dhcp_host_meta', 'desired_ip', 'TEXT');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -61,11 +69,16 @@ final class KeaHostMetadataRepository
|
||||
owner TEXT,
|
||||
location TEXT,
|
||||
device_type TEXT,
|
||||
group_name TEXT,
|
||||
desired_ip TEXT,
|
||||
notes TEXT,
|
||||
tags_json TEXT,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)"
|
||||
);
|
||||
|
||||
$this->ensureColumn('nexus_dhcp_host_meta', 'group_name', 'TEXT');
|
||||
$this->ensureColumn('nexus_dhcp_host_meta', 'desired_ip', 'TEXT');
|
||||
}
|
||||
|
||||
public function findByHostIds(array $hostIds): array
|
||||
@@ -84,7 +97,7 @@ final class KeaHostMetadataRepository
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare(
|
||||
'SELECT host_id, hardware_address, ip_address, real_name, device_name, owner, location, device_type, notes, tags_json, updated_at
|
||||
'SELECT host_id, hardware_address, ip_address, real_name, device_name, owner, location, device_type, group_name, desired_ip, notes, tags_json, updated_at
|
||||
FROM nexus_dhcp_host_meta
|
||||
WHERE host_id IN (' . implode(', ', $placeholders) . ')'
|
||||
);
|
||||
@@ -109,6 +122,8 @@ final class KeaHostMetadataRepository
|
||||
'owner' => null,
|
||||
'location' => null,
|
||||
'device_type' => null,
|
||||
'group_name' => null,
|
||||
'desired_ip' => null,
|
||||
'notes' => null,
|
||||
'tags' => [],
|
||||
], $metadata);
|
||||
@@ -122,9 +137,9 @@ final class KeaHostMetadataRepository
|
||||
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
|
||||
host_id, hardware_address, ip_address, real_name, device_name, owner, location, device_type, group_name, desired_ip, 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()
|
||||
:host_id, :hardware_address, :ip_address, :real_name, :device_name, :owner, :location, :device_type, :group_name, :desired_ip, :notes, CAST(:tags_json AS jsonb), NOW()
|
||||
)
|
||||
ON CONFLICT (host_id) DO UPDATE SET
|
||||
hardware_address = EXCLUDED.hardware_address,
|
||||
@@ -134,6 +149,8 @@ final class KeaHostMetadataRepository
|
||||
owner = EXCLUDED.owner,
|
||||
location = EXCLUDED.location,
|
||||
device_type = EXCLUDED.device_type,
|
||||
group_name = EXCLUDED.group_name,
|
||||
desired_ip = EXCLUDED.desired_ip,
|
||||
notes = EXCLUDED.notes,
|
||||
tags_json = EXCLUDED.tags_json,
|
||||
updated_at = NOW()"
|
||||
@@ -141,9 +158,9 @@ final class KeaHostMetadataRepository
|
||||
} 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
|
||||
host_id, hardware_address, ip_address, real_name, device_name, owner, location, device_type, group_name, desired_ip, 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
|
||||
:host_id, :hardware_address, :ip_address, :real_name, :device_name, :owner, :location, :device_type, :group_name, :desired_ip, :notes, :tags_json, CURRENT_TIMESTAMP
|
||||
)"
|
||||
);
|
||||
}
|
||||
@@ -157,11 +174,85 @@ final class KeaHostMetadataRepository
|
||||
'owner' => $this->nullableString($metadata['owner']),
|
||||
'location' => $this->nullableString($metadata['location']),
|
||||
'device_type' => $this->nullableString($metadata['device_type']),
|
||||
'group_name' => $this->nullableString($metadata['group_name']),
|
||||
'desired_ip' => $this->nullableString($metadata['desired_ip']),
|
||||
'notes' => $this->nullableString($metadata['notes']),
|
||||
'tags_json' => $tagsJson,
|
||||
]);
|
||||
}
|
||||
|
||||
public function listGroups(): array
|
||||
{
|
||||
$stmt = $this->pdo->query(
|
||||
"SELECT DISTINCT group_name
|
||||
FROM nexus_dhcp_host_meta
|
||||
WHERE group_name IS NOT NULL AND group_name <> ''
|
||||
ORDER BY group_name ASC"
|
||||
);
|
||||
|
||||
return array_values(array_filter(array_map(
|
||||
static fn(array $row): string => (string)($row['group_name'] ?? ''),
|
||||
$stmt->fetchAll(PDO::FETCH_ASSOC)
|
||||
)));
|
||||
}
|
||||
|
||||
private function ensureColumn(string $table, string $column, string $definition): void
|
||||
{
|
||||
if ($this->columnExists($table, $column)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$driver = (string)$this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
|
||||
$quote = $driver === 'mysql' ? '`' : '"';
|
||||
try {
|
||||
$this->pdo->exec(
|
||||
'ALTER TABLE ' . $quote . $table . $quote
|
||||
. ' ADD COLUMN ' . $quote . $column . $quote . ' ' . $definition
|
||||
);
|
||||
} catch (\PDOException $e) {
|
||||
if (!$this->columnExists($table, $column)) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function columnExists(string $table, string $column): bool
|
||||
{
|
||||
$driver = (string)$this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
|
||||
|
||||
if ($driver === 'sqlite') {
|
||||
$stmt = $this->pdo->query('PRAGMA table_info(' . $table . ')');
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
if (($row['name'] ?? '') === $column) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($driver === 'pgsql') {
|
||||
$stmt = $this->pdo->prepare(
|
||||
"SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = current_schema()
|
||||
AND table_name = :table
|
||||
AND column_name = :column
|
||||
LIMIT 1"
|
||||
);
|
||||
} else {
|
||||
$stmt = $this->pdo->prepare(
|
||||
"SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = :table
|
||||
AND column_name = :column
|
||||
LIMIT 1"
|
||||
);
|
||||
}
|
||||
$stmt->execute(['table' => $table, 'column' => $column]);
|
||||
return (bool)$stmt->fetchColumn();
|
||||
}
|
||||
|
||||
private function nullableString(mixed $value): ?string
|
||||
{
|
||||
$value = trim((string)$value);
|
||||
|
||||
@@ -84,6 +84,7 @@ final class KeaHostRepository
|
||||
{$ipExpr} AS ipv4_address_text,
|
||||
hostname,
|
||||
user_context,
|
||||
dhcp4_subnet_id AS subnet_id,
|
||||
'reservation' AS source,
|
||||
host_id AS sort_id,
|
||||
{$sortExpr} AS sort_time
|
||||
@@ -114,6 +115,7 @@ final class KeaHostRepository
|
||||
{$ipExpr} AS ipv4_address_text,
|
||||
hostname,
|
||||
user_context,
|
||||
subnet_id,
|
||||
'lease' AS source,
|
||||
address AS sort_id,
|
||||
{$expireExpr} AS sort_time
|
||||
@@ -185,6 +187,7 @@ final class KeaHostRepository
|
||||
{$ipExpr} AS ipv4_address_text,
|
||||
hostname,
|
||||
user_context,
|
||||
dhcp4_subnet_id AS subnet_id,
|
||||
'reservation' AS source,
|
||||
host_id AS sort_id,
|
||||
{$sortExpr} AS sort_time
|
||||
@@ -220,16 +223,19 @@ final class KeaHostRepository
|
||||
*/
|
||||
public function create(string $mac, string $ip, int $subnetId, ?string $hostname = null, array $metadata = []): int
|
||||
{
|
||||
// dhcp_identifier_type 1 = HW_ADDRESS (Ethernet)
|
||||
$macHex = $this->macToHex($mac);
|
||||
$ipNumber = $this->ipv4ToNumber($ip);
|
||||
$identifierExpr = $this->binaryFromHexExpression(':mac_hex');
|
||||
|
||||
$stmt = $this->pdo->prepare(
|
||||
'INSERT INTO hosts (dhcp_identifier, dhcp_identifier_type, dhcp4_subnet_id, ipv4_address, hostname)
|
||||
VALUES (:mac, 1, :subnetId, :ip, :hostname)'
|
||||
"INSERT INTO hosts (dhcp_identifier, dhcp_identifier_type, dhcp4_subnet_id, ipv4_address, hostname)
|
||||
VALUES ({$identifierExpr}, 1, :subnetId, :ip, :hostname)"
|
||||
);
|
||||
|
||||
$stmt->execute([
|
||||
'mac' => $mac,
|
||||
'mac_hex' => $macHex,
|
||||
'subnetId' => $subnetId,
|
||||
'ip' => $ip,
|
||||
'ip' => $ipNumber,
|
||||
'hostname' => $hostname,
|
||||
]);
|
||||
|
||||
@@ -241,6 +247,49 @@ final class KeaHostRepository
|
||||
return $hostId;
|
||||
}
|
||||
|
||||
public function reserveDisplayEntry(array $host, string $ip, array $metadata = []): int
|
||||
{
|
||||
$source = (string)($host['source'] ?? 'reservation');
|
||||
$subnetId = (int)($host['subnet_id'] ?? 0);
|
||||
if ($subnetId <= 0) {
|
||||
throw new \RuntimeException('Subnet-ID fehlt. Ohne Subnet kann keine KEA-Reservierung angelegt werden.');
|
||||
}
|
||||
|
||||
$hostname = (string)($host['hostname'] ?? '');
|
||||
if ($source === 'reservation') {
|
||||
$hostId = (int)($host['host_id'] ?? 0);
|
||||
if ($hostId <= 0) {
|
||||
throw new \RuntimeException('Reservierung hat keine gueltige Host-ID.');
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare(
|
||||
'UPDATE hosts
|
||||
SET ipv4_address = :ip, hostname = :hostname, dhcp4_subnet_id = :subnet_id
|
||||
WHERE host_id = :host_id'
|
||||
);
|
||||
$stmt->execute([
|
||||
'ip' => $this->ipv4ToNumber($ip),
|
||||
'hostname' => $hostname !== '' ? $hostname : null,
|
||||
'subnet_id' => $subnetId,
|
||||
'host_id' => $hostId,
|
||||
]);
|
||||
|
||||
if ($this->metadata !== null) {
|
||||
$this->metadata->saveForHost($hostId, (string)($host['dhcp_identifier'] ?? ''), $ip, $metadata);
|
||||
}
|
||||
|
||||
return $hostId;
|
||||
}
|
||||
|
||||
return $this->create(
|
||||
(string)($host['dhcp_identifier'] ?? ''),
|
||||
$ip,
|
||||
$subnetId,
|
||||
$hostname !== '' ? $hostname : null,
|
||||
$metadata
|
||||
);
|
||||
}
|
||||
|
||||
private function withMetadata(array $hosts): array
|
||||
{
|
||||
if ($hosts === [] || $this->metadata === null) {
|
||||
@@ -323,11 +372,50 @@ final class KeaHostRepository
|
||||
$row['dhcp_identifier'] = $this->formatMac((string)($row['dhcp_identifier_hex'] ?? ''));
|
||||
$row['ipv4_address'] = (string)($row['ipv4_address_text'] ?? '');
|
||||
$row['hostname'] = (string)($row['hostname'] ?? '');
|
||||
$contextName = $this->nameFromUserContext((string)($row['user_context'] ?? ''));
|
||||
$row['display_name'] = $row['hostname'] !== '' ? $row['hostname'] : ($contextName !== '' ? $contextName : 'Unbekannt');
|
||||
$row['metadata'] = [];
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
private function nameFromUserContext(string $userContext): string
|
||||
{
|
||||
if (trim($userContext) === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$decoded = json_decode($userContext, true);
|
||||
if (!is_array($decoded)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->findContextValue($decoded, ['hostname', 'host-name', 'client-hostname', 'device_name', 'name', 'label']);
|
||||
}
|
||||
|
||||
private function findContextValue(array $data, array $keys): string
|
||||
{
|
||||
foreach ($keys as $key) {
|
||||
if (isset($data[$key]) && is_scalar($data[$key])) {
|
||||
$value = trim((string)$data[$key]);
|
||||
if ($value !== '') {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($data as $value) {
|
||||
if (is_array($value)) {
|
||||
$found = $this->findContextValue($value, $keys);
|
||||
if ($found !== '') {
|
||||
return $found;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private function formatMac(string $hex): string
|
||||
{
|
||||
$hex = strtolower(preg_replace('/[^a-fA-F0-9]/', '', $hex) ?? '');
|
||||
@@ -338,6 +426,35 @@ final class KeaHostRepository
|
||||
return implode(':', str_split($hex, 2));
|
||||
}
|
||||
|
||||
private function macToHex(string $mac): string
|
||||
{
|
||||
$hex = strtolower(preg_replace('/[^a-fA-F0-9]/', '', $mac) ?? '');
|
||||
if ($hex === '') {
|
||||
throw new \RuntimeException('MAC-Adresse fehlt.');
|
||||
}
|
||||
|
||||
return $hex;
|
||||
}
|
||||
|
||||
private function binaryFromHexExpression(string $placeholder): string
|
||||
{
|
||||
return match ($this->driver()) {
|
||||
'pgsql' => "decode({$placeholder}, 'hex')",
|
||||
'mysql' => "UNHEX({$placeholder})",
|
||||
default => $placeholder,
|
||||
};
|
||||
}
|
||||
|
||||
private function ipv4ToNumber(string $ip): string
|
||||
{
|
||||
$long = ip2long($ip);
|
||||
if ($long === false) {
|
||||
throw new \RuntimeException('Ungueltige IPv4-Adresse.');
|
||||
}
|
||||
|
||||
return sprintf('%u', $long);
|
||||
}
|
||||
|
||||
private function lastInsertIdSafe(): int
|
||||
{
|
||||
$driver = $this->driver();
|
||||
|
||||
Reference in New Issue
Block a user