Improve logic handle auto-allocation of ports for a server

This commit is contained in:
Dane Everitt 2020-10-31 14:58:15 -07:00
parent 7638ffccde
commit c6bd7ff661
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
10 changed files with 201 additions and 79 deletions

View file

@ -0,0 +1,18 @@
<?php
namespace Pterodactyl\Exceptions\Service\Allocation;
use Pterodactyl\Exceptions\DisplayException;
class AutoAllocationNotEnabledException extends DisplayException
{
/**
* AutoAllocationNotEnabledException constructor.
*/
public function __construct()
{
parent::__construct(
'Server auto-allocation is not enabled for this instance.'
);
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace Pterodactyl\Exceptions\Service\Allocation;
use Pterodactyl\Exceptions\DisplayException;
class NoAutoAllocationSpaceAvailableException extends DisplayException
{
/**
* NoAutoAllocationSpaceAvailableException constructor.
*/
public function __construct()
{
parent::__construct(
'Cannot assign additional allocation: no more space available on node.'
);
}
}

View file

@ -2,7 +2,6 @@
namespace Pterodactyl\Http\Controllers\Api\Client\Servers; namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
use Illuminate\Contracts\Config\Repository;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Pterodactyl\Models\Allocation; use Pterodactyl\Models\Allocation;
@ -11,13 +10,12 @@ use Pterodactyl\Repositories\Eloquent\ServerRepository;
use Pterodactyl\Repositories\Eloquent\AllocationRepository; use Pterodactyl\Repositories\Eloquent\AllocationRepository;
use Pterodactyl\Transformers\Api\Client\AllocationTransformer; use Pterodactyl\Transformers\Api\Client\AllocationTransformer;
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController; use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
use Pterodactyl\Services\Allocations\FindAssignableAllocationService;
use Pterodactyl\Http\Requests\Api\Client\Servers\Network\GetNetworkRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Network\GetNetworkRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Network\NewAllocationRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Network\DeleteAllocationRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Network\DeleteAllocationRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Network\UpdateAllocationRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Network\UpdateAllocationRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Network\NewAllocationRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Network\SetPrimaryAllocationRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Network\SetPrimaryAllocationRequest;
use Pterodactyl\Services\Allocations\AssignmentService;
use Illuminate\Support\Facades\Log;
class NetworkAllocationController extends ClientApiController class NetworkAllocationController extends ClientApiController
{ {
@ -32,37 +30,27 @@ class NetworkAllocationController extends ClientApiController
private $serverRepository; private $serverRepository;
/** /**
* @var \Pterodactyl\Services\Allocations\AssignmentService * @var \Pterodactyl\Services\Allocations\FindAssignableAllocationService
*/ */
protected $assignmentService; private $assignableAllocationService;
/** /**
* NetworkController constructor. * NetworkController constructor.
* *
* @param \Pterodactyl\Repositories\Eloquent\AllocationRepository $repository * @param \Pterodactyl\Repositories\Eloquent\AllocationRepository $repository
* @param \Pterodactyl\Repositories\Eloquent\ServerRepository $serverRepository * @param \Pterodactyl\Repositories\Eloquent\ServerRepository $serverRepository
* @param \Pterodactyl\Services\Allocations\AssignmentService $assignmentService * @param \Pterodactyl\Services\Allocations\FindAssignableAllocationService $assignableAllocationService
* @param \Illuminate\Contracts\Config\Repository $config
*/ */
/**
* @var \Illuminate\Contracts\Config\Repository
*/
private $config;
public function __construct( public function __construct(
AllocationRepository $repository, AllocationRepository $repository,
ServerRepository $serverRepository, ServerRepository $serverRepository,
AssignmentService $assignmentService, FindAssignableAllocationService $assignableAllocationService
Repository $config
) { ) {
parent::__construct(); parent::__construct();
$this->repository = $repository; $this->repository = $repository;
$this->serverRepository = $serverRepository; $this->serverRepository = $serverRepository;
$this->assignmentService = $assignmentService; $this->assignableAllocationService = $assignableAllocationService;
$this->config = $config;
} }
/** /**
@ -125,52 +113,16 @@ class NetworkAllocationController extends ClientApiController
/** /**
* Set the notes for the allocation for a server. * Set the notes for the allocation for a server.
*s *s
*
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Network\NewAllocationRequest $request * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Network\NewAllocationRequest $request
* @param \Pterodactyl\Models\Server $server * @param \Pterodactyl\Models\Server $server
* @param \Pterodactyl\Models\Allocation $allocation
* @return array * @return array
* *
* @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/ */
public function addNew(NewAllocationRequest $request, Server $server): array public function store(NewAllocationRequest $request, Server $server): array
{ {
$topRange = config('pterodactyl.allocation.stop',0); $allocation = $this->assignableAllocationService->handle($server);
$bottomRange = config('pterodactyl.allocation.start',0);
if($server->allocation_limit <= $server->allocations->count()) {
throw new DisplayException(
'You have created the maximum number of allocations!'
);
}
$allocation = $server->node->allocations()->where('ip',$server->allocation->ip)->whereNull('server_id')->first();
if(!$allocation) {
if($server->node->allocations()->where('ip',$server->allocation->ip)->where([['port', '>=', $bottomRange ], ['port', '<=', $topRange],])->count() >= $topRange-$bottomRange+1 || !config('allocation.enabled', false)) {
throw new DisplayException(
'No more allocations available!'
);
}
$allPorts = $server->node->allocations()->select(['port'])->where('ip',$server->allocation->ip)->get()->pluck('port')->toArray();
do {
$port = rand($bottomRange, $topRange);
} while(array_search($port, $allPorts));
$this->assignmentService->handle($server->node,[
'allocation_ip'=>$server->allocation->ip,
'allocation_ports'=>[$port],
'server_id'=>$server->id
]);
$allocation = $server->node->allocations()->where('ip',$server->allocation->ip)->where('port', $port)->first();
}
$allocation->update(['server_id' => $server->id]);
return $this->fractal->item($allocation) return $this->fractal->item($allocation)
->transformWith($this->getTransformer(AllocationTransformer::class)) ->transformWith($this->getTransformer(AllocationTransformer::class))

View file

@ -21,9 +21,9 @@ class AdvancedSettingsFormRequest extends AdminFormRequest
'pterodactyl:guzzle:connect_timeout' => 'required|integer|between:1,60', 'pterodactyl:guzzle:connect_timeout' => 'required|integer|between:1,60',
'pterodactyl:console:count' => 'required|integer|min:1', 'pterodactyl:console:count' => 'required|integer|min:1',
'pterodactyl:console:frequency' => 'required|integer|min:10', 'pterodactyl:console:frequency' => 'required|integer|min:10',
'allocation:enabled' => 'required|in:true,false', 'pterodactyl:client_features:allocations:enabled' => 'required|in:true,false',
'pterodactyl:allocation:start' => 'required|integer|between:2000,65535', 'pterodactyl:client_features:allocations:range_start' => 'required|integer|between:1024,65535',
'pterodactyl:allocation:stop' => 'required|integer|between:2000,65535', 'pterodactyl:client_features:allocations:range_end' => 'required|integer|between:1024,65535',
]; ];
} }
@ -40,9 +40,9 @@ class AdvancedSettingsFormRequest extends AdminFormRequest
'pterodactyl:guzzle:connect_timeout' => 'HTTP Connection Timeout', 'pterodactyl:guzzle:connect_timeout' => 'HTTP Connection Timeout',
'pterodactyl:console:count' => 'Console Message Count', 'pterodactyl:console:count' => 'Console Message Count',
'pterodactyl:console:frequency' => 'Console Frequency Tick', 'pterodactyl:console:frequency' => 'Console Frequency Tick',
'allocation:enabled' => 'Auto Create Allocations Enabled', 'pterodactyl:client_features:allocations:enabled' => 'Auto Create Allocations Enabled',
'pterodactyl:allocation:start' => 'Starting Port', 'pterodactyl:client_features:allocations:range_start' => 'Starting Port',
'pterodactyl:allocation:stop' => 'Ending Port', 'pterodactyl:client_features:allocations:range_end' => 'Ending Port',
]; ];
} }
} }

View file

@ -30,9 +30,9 @@ class SettingsServiceProvider extends ServiceProvider
'pterodactyl:console:count', 'pterodactyl:console:count',
'pterodactyl:console:frequency', 'pterodactyl:console:frequency',
'pterodactyl:auth:2fa_required', 'pterodactyl:auth:2fa_required',
'allocation:enabled', 'pterodactyl:client_features:allocations:enabled',
'pterodactyl:allocation:stop', 'pterodactyl:client_features:allocations:range_start',
'pterodactyl:allocation:start', 'pterodactyl:client_features:allocations:range_end',
]; ];
/** /**

View file

@ -0,0 +1,130 @@
<?php
namespace Pterodactyl\Services\Allocations;
use Webmozart\Assert\Assert;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Allocation;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Exceptions\Service\Allocation\AutoAllocationNotEnabledException;
use Pterodactyl\Exceptions\Service\Allocation\NoAutoAllocationSpaceAvailableException;
class FindAssignableAllocationService
{
/**
* @var \Pterodactyl\Services\Allocations\AssignmentService
*/
private $service;
/**
* FindAssignableAllocationService constructor.
*
* @param \Pterodactyl\Services\Allocations\AssignmentService $service
*/
public function __construct(AssignmentService $service)
{
$this->service = $service;
}
/**
* Finds an existing unassigned allocation and attempts to assign it to the given server. If
* no allocation can be found, a new one will be created with a random port between the defined
* range from the configuration.
*
* @param \Pterodactyl\Models\Server $server
* @return \Pterodactyl\Models\Allocation
*
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Service\Allocation\CidrOutOfRangeException
* @throws \Pterodactyl\Exceptions\Service\Allocation\InvalidPortMappingException
* @throws \Pterodactyl\Exceptions\Service\Allocation\PortOutOfRangeException
* @throws \Pterodactyl\Exceptions\Service\Allocation\TooManyPortsInRangeException
*/
public function handle(Server $server)
{
if (!config('pterodactyl.client_features.allocations.enabled')) {
throw new AutoAllocationNotEnabledException;
}
if ($server->allocations()->count() >= $server->allocation_limit) {
throw new DisplayException(
'Cannot assign additional allocations to this server: limit has been reached.'
);
}
// Attempt to find a given available allocation for a server. If one cannot be found
// we will fall back to attempting to create a new allocation that can be used for the
// server.
/** @var \Pterodactyl\Models\Allocation|null $allocation */
$allocation = $server->node->allocations()
->where('ip', $server->allocation->ip)
->whereNull('server_id')
->inRandomOrder()
->first();
$allocation = $allocation ?? $this->createNewAllocation($server);
$allocation->update(['server_id' => $server->id]);
return $allocation->refresh();
}
/**
* @param \Pterodactyl\Models\Server $server
* @return \Pterodactyl\Models\Allocation
*
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Service\Allocation\CidrOutOfRangeException
* @throws \Pterodactyl\Exceptions\Service\Allocation\InvalidPortMappingException
* @throws \Pterodactyl\Exceptions\Service\Allocation\PortOutOfRangeException
* @throws \Pterodactyl\Exceptions\Service\Allocation\TooManyPortsInRangeException
*/
protected function createNewAllocation(Server $server): Allocation
{
$start = config('pterodactyl.client_features.allocations.range_start', null);
$end = config('pterodactyl.client_features.allocations.range_end', null);
if (!$start || !$end) {
throw new NoAutoAllocationSpaceAvailableException;
}
Assert::integerish($start);
Assert::integerish($end);
// Get all of the currently allocated ports for the node so that we can figure out
// which port might be available.
$ports = $server->node->allocations()
->where('ip', $server->allocation->ip)
->whereBetween('port', [$start, $end])
->pluck('port');
// Compute the difference of the range and the currently created ports, finding
// any port that does not already exist in the database. We will then use this
// array of ports to create a new allocation to assign to the server.
$available = array_diff(range($start, $end), $ports->toArray());
// If we've already allocated all of the ports, just abort.
if (empty($available)) {
throw new NoAutoAllocationSpaceAvailableException;
}
// dd($available, array_rand($available));
// Pick a random port out of the remaining available ports.
/** @var int $port */
$port = $available[array_rand($available)];
$this->service->handle($server->node, [
'server_id' => $server->id,
'allocation_ip' => $server->allocation->ip,
'allocation_ports' => [$port],
]);
/** @var \Pterodactyl\Models\Allocation $allocation */
$allocation = $server->node->allocations()
->where('ip', $server->allocation->ip)
->where('port', $port)
->firstOrFail();
return $allocation;
}
}

View file

@ -143,6 +143,12 @@ return [
// The total number of tasks that can exist for any given schedule at once. // The total number of tasks that can exist for any given schedule at once.
'per_schedule_task_limit' => 10, 'per_schedule_task_limit' => 10,
], ],
'allocations' => [
'enabled' => env('PTERODACTYL_CLIENT_ALLOCATIONS_ENABLED', false),
'range_start' => env('PTERODACTYL_CLIENT_ALLOCATIONS_RANGE_START'),
'range_end' => env('PTERODACTYL_CLIENT_ALLOCATIONS_RANGE_END'),
],
], ],
/* /*

View file

@ -2,10 +2,8 @@ import { Allocation } from '@/api/server/getServer';
import http from '@/api/http'; import http from '@/api/http';
import { rawDataToServerAllocation } from '@/api/transformers'; import { rawDataToServerAllocation } from '@/api/transformers';
export default (uuid: string): Promise<Allocation> => { export default async (uuid: string): Promise<Allocation> => {
return new Promise((resolve, reject) => { const { data } = await http.post(`/api/client/servers/${uuid}/network/allocations`);
http.get(`/api/client/servers/${uuid}/network/allocations/new`)
.then(({ data }) => resolve(rawDataToServerAllocation(data))) return rawDataToServerAllocation(data);
.catch(reject);
});
}; };

View file

@ -114,24 +114,24 @@
<div class="form-group col-md-4"> <div class="form-group col-md-4">
<label class="control-label">Status</label> <label class="control-label">Status</label>
<div> <div>
<select class="form-control" name="allocation:enabled"> <select class="form-control" name="pterodactyl:client_features:allocations:enabled">
<option value="false">Disabled</option> <option value="false">Disabled</option>
<option value="true" @if(old('allocation:enabled', config('allocation.enabled'))) selected @endif>Enabled</option> <option value="true" @if(old('pterodactyl:client_features:allocations:enabled', config('pterodactyl.client_features.allocations.enabled'))) selected @endif>Enabled</option>
</select> </select>
<p class="text-muted small">If enabled, the panel will attempt to auto create a new allocation in the range specified if there are no more allocations already created on the node.</p> <p class="text-muted small">If enabled users will have the option to automatically create new allocations for their server via the frontend.</p>
</div> </div>
</div> </div>
<div class="form-group col-md-4"> <div class="form-group col-md-4">
<label class="control-label">Starting Port</label> <label class="control-label">Starting Port</label>
<div> <div>
<input type="number" required class="form-control" name="pterodactyl:allocation:start" value="{{ old('pterodactyl:allocation:start', config('pterodactyl.allocation.start')) }}"> <input type="number" required class="form-control" name="pterodactyl:client_features:allocations:range_start" value="{{ old('pterodactyl:client_features:allocations:range_start', config('pterodactyl.client_features.allocations.range_start')) }}">
<p class="text-muted small">The starting port in the range that can be automatically allocated.</p> <p class="text-muted small">The starting port in the range that can be automatically allocated.</p>
</div> </div>
</div> </div>
<div class="form-group col-md-4"> <div class="form-group col-md-4">
<label class="control-label">Ending Port</label> <label class="control-label">Ending Port</label>
<div> <div>
<input type="number" required class="form-control" name="pterodactyl:allocation:stop" value="{{ old('pterodactyl:allocation:stop', config('pterodactyl.allocation.stop')) }}"> <input type="number" required class="form-control" name="pterodactyl:client_features:allocations:range_end" value="{{ old('pterodactyl:client_features:allocations:range_end', config('pterodactyl.client_features.allocations.range_end')) }}">
<p class="text-muted small">The ending port in the range that can be automatically allocated.</p> <p class="text-muted small">The ending port in the range that can be automatically allocated.</p>
</div> </div>
</div> </div>

View file

@ -82,9 +82,9 @@ Route::group(['prefix' => '/servers/{server}', 'middleware' => [AuthenticateServ
Route::group(['prefix' => '/network', 'middleware' => [AllocationBelongsToServer::class]], function () { Route::group(['prefix' => '/network', 'middleware' => [AllocationBelongsToServer::class]], function () {
Route::get('/allocations', 'Servers\NetworkAllocationController@index'); Route::get('/allocations', 'Servers\NetworkAllocationController@index');
Route::post('/allocations', 'Servers\NetworkAllocationController@store');
Route::post('/allocations/{allocation}', 'Servers\NetworkAllocationController@update'); Route::post('/allocations/{allocation}', 'Servers\NetworkAllocationController@update');
Route::post('/allocations/{allocation}/primary', 'Servers\NetworkAllocationController@setPrimary'); Route::post('/allocations/{allocation}/primary', 'Servers\NetworkAllocationController@setPrimary');
Route::get('/allocations/new', 'Servers\NetworkAllocationController@addNew');
Route::delete('/allocations/{allocation}', 'Servers\NetworkAllocationController@delete'); Route::delete('/allocations/{allocation}', 'Servers\NetworkAllocationController@delete');
}); });