initial commit
This commit is contained in:
162
app/Services/Hosting/Provisioning/IpAddressAllocator.php
Normal file
162
app/Services/Hosting/Provisioning/IpAddressAllocator.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user