kea
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-15 02:48:23 +02:00
parent 7157c98dcb
commit 598348a4b6
8 changed files with 532 additions and 11 deletions

View File

@@ -33,6 +33,7 @@ final class KeaHostMetadataRepository
);
$this->ensureColumn('nexus_dhcp_host_meta', 'group_name', 'TEXT');
$this->ensureColumn('nexus_dhcp_host_meta', 'desired_ip', 'TEXT');
$this->ensureGroupSchema($driver);
return;
}
@@ -56,6 +57,7 @@ final class KeaHostMetadataRepository
);
$this->ensureColumn('nexus_dhcp_host_meta', 'group_name', 'TEXT');
$this->ensureColumn('nexus_dhcp_host_meta', 'desired_ip', 'TEXT');
$this->ensureGroupSchema($driver);
return;
}
@@ -79,6 +81,7 @@ final class KeaHostMetadataRepository
$this->ensureColumn('nexus_dhcp_host_meta', 'group_name', 'TEXT');
$this->ensureColumn('nexus_dhcp_host_meta', 'desired_ip', 'TEXT');
$this->ensureGroupSchema($driver);
}
public function findByHostIds(array $hostIds): array
@@ -183,19 +186,278 @@ final class KeaHostMetadataRepository
public function listGroups(): array
{
$groups = [];
if ($this->tableExists('nexus_dhcp_groups')) {
$stmt = $this->pdo->query("SELECT name FROM nexus_dhcp_groups ORDER BY name ASC");
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$name = trim((string)($row['name'] ?? ''));
if ($name !== '') {
$groups[] = $name;
}
}
}
$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"
WHERE group_name IS NOT NULL AND group_name <> ''"
);
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$name = trim((string)($row['group_name'] ?? ''));
if ($name !== '') {
$groups[] = $name;
}
}
sort($groups, SORT_NATURAL | SORT_FLAG_CASE);
return array_values(array_unique($groups));
}
public function listGroupsWithRanges(): array
{
if (!$this->tableExists('nexus_dhcp_groups')) {
return [];
}
$groups = [];
$stmt = $this->pdo->query(
"SELECT id, name, description
FROM nexus_dhcp_groups
ORDER BY name ASC"
);
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$id = (int)$row['id'];
$groups[$id] = [
'id' => $id,
'name' => (string)$row['name'],
'description' => (string)($row['description'] ?? ''),
'ranges' => [],
];
}
if ($groups !== [] && $this->tableExists('nexus_dhcp_group_ranges')) {
$stmt = $this->pdo->query(
"SELECT id, group_id, start_ip, end_ip
FROM nexus_dhcp_group_ranges
ORDER BY start_ip ASC"
);
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$groupId = (int)$row['group_id'];
if (isset($groups[$groupId])) {
$groups[$groupId]['ranges'][] = [
'id' => (int)$row['id'],
'start_ip' => (string)$row['start_ip'],
'end_ip' => (string)$row['end_ip'],
];
}
}
}
return array_values($groups);
}
public function saveGroup(string $name, string $description = ''): void
{
$name = trim($name);
if ($name === '') {
throw new \RuntimeException('Gruppenname fehlt.');
}
$driver = (string)$this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
if ($driver === 'pgsql' || $driver === 'sqlite') {
$sql = "INSERT INTO nexus_dhcp_groups (name, description, updated_at)
VALUES (:name, :description, CURRENT_TIMESTAMP)
ON CONFLICT(name) DO UPDATE SET
description = excluded.description,
updated_at = CURRENT_TIMESTAMP";
} else {
$sql = "INSERT INTO nexus_dhcp_groups (name, description, updated_at)
VALUES (:name, :description, CURRENT_TIMESTAMP)
ON DUPLICATE KEY UPDATE
description = VALUES(description),
updated_at = CURRENT_TIMESTAMP";
}
$stmt = $this->pdo->prepare($sql);
$stmt->execute([
'name' => $name,
'description' => $this->nullableString($description),
]);
}
public function addRange(string $groupName, string $startIp, string $endIp): void
{
$groupName = trim($groupName);
$startIp = trim($startIp);
$endIp = trim($endIp);
if ($groupName === '' || !filter_var($startIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) || !filter_var($endIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
throw new \RuntimeException('Gruppe oder IP-Bereich ist ungueltig.');
}
if (ip2long($startIp) > ip2long($endIp)) {
throw new \RuntimeException('Start-IP muss vor der End-IP liegen.');
}
$groupId = $this->groupIdByName($groupName);
if ($groupId <= 0) {
throw new \RuntimeException('Gruppe wurde nicht gefunden.');
}
$stmt = $this->pdo->prepare(
"INSERT INTO nexus_dhcp_group_ranges (group_id, start_ip, end_ip, updated_at)
VALUES (:group_id, :start_ip, :end_ip, CURRENT_TIMESTAMP)"
);
$stmt->execute([
'group_id' => $groupId,
'start_ip' => $startIp,
'end_ip' => $endIp,
]);
}
public function availableIpsByGroup(array $usedIps, int $limitPerGroup = 512): array
{
$used = array_flip(array_filter(array_map('strval', $usedIps)));
$items = [];
foreach ($this->listGroupsWithRanges() as $group) {
$available = [];
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($available) < $limitPerGroup; $ip++) {
$address = long2ip($ip);
if ($address !== false && !isset($used[$address])) {
$available[] = $address;
}
}
}
$items[(string)$group['name']] = $available;
}
return $items;
}
public function desiredIps(): array
{
$stmt = $this->pdo->query(
"SELECT desired_ip
FROM nexus_dhcp_host_meta
WHERE desired_ip IS NOT NULL AND desired_ip <> ''"
);
return array_values(array_filter(array_map(
static fn(array $row): string => (string)($row['group_name'] ?? ''),
static fn(array $row): string => (string)($row['desired_ip'] ?? ''),
$stmt->fetchAll(PDO::FETCH_ASSOC)
)));
}
private function ensureGroupSchema(string $driver): void
{
if ($driver === 'pgsql') {
$this->pdo->exec(
"CREATE TABLE IF NOT EXISTS nexus_dhcp_groups (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
description TEXT,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)"
);
$this->pdo->exec(
"CREATE TABLE IF NOT EXISTS nexus_dhcp_group_ranges (
id BIGSERIAL PRIMARY KEY,
group_id BIGINT NOT NULL REFERENCES nexus_dhcp_groups(id) ON DELETE CASCADE,
start_ip TEXT NOT NULL,
end_ip TEXT NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)"
);
return;
}
if ($driver === 'sqlite') {
$this->pdo->exec(
"CREATE TABLE IF NOT EXISTS nexus_dhcp_groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
description TEXT,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
)"
);
$this->pdo->exec(
"CREATE TABLE IF NOT EXISTS nexus_dhcp_group_ranges (
id INTEGER PRIMARY KEY AUTOINCREMENT,
group_id INTEGER NOT NULL,
start_ip TEXT NOT NULL,
end_ip TEXT NOT NULL,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
)"
);
return;
}
$this->pdo->exec(
"CREATE TABLE IF NOT EXISTS nexus_dhcp_groups (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(190) NOT NULL UNIQUE,
description TEXT,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
)"
);
$this->pdo->exec(
"CREATE TABLE IF NOT EXISTS nexus_dhcp_group_ranges (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
group_id BIGINT NOT NULL,
start_ip VARCHAR(45) NOT NULL,
end_ip VARCHAR(45) NOT NULL,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_nexus_dhcp_group_ranges_group_id (group_id)
)"
);
}
private function groupIdByName(string $name): int
{
$stmt = $this->pdo->prepare('SELECT id FROM nexus_dhcp_groups WHERE name = :name LIMIT 1');
$stmt->execute(['name' => $name]);
return (int)$stmt->fetchColumn();
}
private function tableExists(string $table): bool
{
$driver = (string)$this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
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();
}
if ($driver === 'pgsql') {
$stmt = $this->pdo->prepare(
"SELECT 1
FROM information_schema.tables
WHERE table_schema = current_schema()
AND table_name = :table
LIMIT 1"
);
} else {
$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 ensureColumn(string $table, string $column, string $definition): void
{
if ($this->columnExists($table, $column)) {

View File

@@ -66,6 +66,14 @@ final class KeaHostRepository
}
}
public function usedIpAddresses(int $limit = 10000): array
{
return array_values(array_unique(array_filter(array_map(
static fn(array $host): string => (string)($host['ipv4_address'] ?? ''),
$this->findAll($limit)
))));
}
private function findReservations(int $limit): array
{
if (!$this->tableExists('hosts')) {