defaultPrivatePool(); return $this->allocate($pool, $preferred); } public function allocatePublicIp(?IpPool $pool = null, ?string $preferred = null): string { $pool ??= IpPool::query() ->where('type', IpPoolType::Public) ->where('is_active', true) ->first() ?? $this->bootstrapPublicPool(); if ($pool->type !== IpPoolType::Public) { throw new ProvisioningException('Selected pool is not a public IP pool.', step: 'ip_allocation'); } return $this->allocate($pool, $preferred, 'public_ip'); } public function allocate(IpPool $pool, ?string $preferred = null, string $column = 'ip_address'): string { return DB::transaction(function () use ($pool, $preferred, $column) { IpPool::query()->whereKey($pool->id)->lockForUpdate()->first(); if ($preferred !== null) { if (! $pool->containsIp($preferred)) { throw new ProvisioningException( "IP {$preferred} is outside pool {$pool->name}.", step: 'ip_allocation', ); } if ($this->isIpUsed($preferred, $pool, $column)) { throw new ProvisioningException( "IP address {$preferred} is already in use.", step: 'ip_allocation', ); } return $preferred; } $start = ip2long($pool->start_ip); $end = ip2long($pool->end_ip); if ($start === false || $end === false || $start > $end) { throw new ProvisioningException('Invalid IP pool range.', step: 'ip_allocation'); } $used = $this->usedIpsInPool($pool, $column); for ($long = $start; $long <= $end; $long++) { if (! isset($used[$long])) { return long2ip($long); } } throw new ProvisioningException("No free IPs in pool {$pool->name}.", step: 'ip_allocation'); }, 3); } private function defaultPrivatePool(): IpPool { $pool = IpPool::query() ->where('type', IpPoolType::Private) ->where('is_active', true) ->orderBy('id') ->first(); if ($pool) { return $pool; } return $this->bootstrapPoolFromConfig(IpPoolType::Private); } private function bootstrapPublicPool(): IpPool { return IpPool::query()->firstOrCreate( ['name' => 'Öffentlich 185.45.149.x', 'type' => IpPoolType::Public], [ 'start_ip' => config('hosting.public_network.ip_pool_start'), 'end_ip' => config('hosting.public_network.ip_pool_end'), 'gateway' => config('hosting.public_network.gateway'), 'cidr' => config('hosting.public_network.cidr'), 'is_active' => true, ], ); } private function bootstrapPoolFromConfig(IpPoolType $type): IpPool { return IpPool::query()->firstOrCreate( ['name' => 'Privat 10.32.0.0/24', 'type' => IpPoolType::Private], [ 'start_ip' => config('hosting.network.ip_pool_start'), 'end_ip' => config('hosting.network.ip_pool_end'), 'gateway' => config('hosting.network.gateway'), 'cidr' => config('hosting.network.cidr'), 'is_active' => true, ], ); } private function usedIpsInPool(IpPool $pool, string $column): array { $query = Customer::query() ->where('ip_pool_id', $pool->id) ->whereIn('status', ['pending', 'active']); if ($column === 'public_ip') { return $query->whereNotNull('public_ip') ->pluck('public_ip') ->merge( Customer::query() ->whereIn('status', ['pending', 'active']) ->whereNotNull('public_ip') ->pluck('public_ip') ) ->unique() ->map(fn (string $ip) => ip2long($ip)) ->filter() ->flip() ->all(); } return $query->whereNotNull('ip_address') ->pluck('ip_address') ->map(fn (string $ip) => ip2long($ip)) ->filter() ->flip() ->all(); } private function isIpUsed(string $ip, IpPool $pool, string $column): bool { $check = Customer::query() ->whereIn('status', ['pending', 'active']); if ($column === 'public_ip') { return (clone $check)->where('public_ip', $ip)->exists() || (clone $check)->where('ip_address', $ip)->exists(); } return (clone $check)->where('ip_address', $ip)->exists() || (clone $check)->where('public_ip', $ip)->exists(); } }