163 lines
5.3 KiB
PHP
163 lines
5.3 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Hosting\Provisioning;
|
|
|
|
use App\Enums\IpPoolType;
|
|
use App\Exceptions\Hosting\ProvisioningException;
|
|
use App\Models\Customer;
|
|
use App\Models\IpPool;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class IpAddressAllocator
|
|
{
|
|
public function allocateFromPool(?IpPool $pool = null, ?string $preferred = null): string
|
|
{
|
|
$pool ??= $this->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();
|
|
}
|
|
}
|