Handle allocation assignment using services

Function is significantly quicker and uses 1 SQL query per IP rather than 1 query per port.
This commit is contained in:
Dane Everitt 2017-08-05 21:10:32 -05:00
parent 396b5c22d9
commit 669119c8f8
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
17 changed files with 754 additions and 5915 deletions

View file

@ -186,4 +186,12 @@ interface RepositoryInterface
* @return bool
*/
public function insert(array $data);
/**
* Insert multiple records into the database and ignore duplicates.
*
* @param array $values
* @return bool
*/
public function insertIgnore(array $values);
}

View file

@ -29,7 +29,7 @@ use Prologue\Alerts\AlertsMessageBag;
use Pterodactyl\Services\LocationService;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Http\Requests\Admin\LocationRequest;
use Pterodactyl\Http\Requests\Admin\LocationFormRequest;
use Pterodactyl\Contracts\Repository\LocationRepositoryInterface;
class LocationController extends Controller
@ -94,13 +94,13 @@ class LocationController extends Controller
/**
* Handle request to create new location.
*
* @param \Pterodactyl\Http\Requests\Admin\LocationRequest $request
* @param \Pterodactyl\Http\Requests\Admin\LocationFormRequest $request
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Throwable
* @throws \Watson\Validating\ValidationException
*/
public function create(LocationRequest $request)
public function create(LocationFormRequest $request)
{
$location = $this->service->create($request->normalize());
$this->alert->success('Location was created successfully.')->flash();
@ -111,14 +111,14 @@ class LocationController extends Controller
/**
* Handle request to update or delete location.
*
* @param \Pterodactyl\Http\Requests\Admin\LocationRequest $request
* @param \Pterodactyl\Models\Location $location
* @param \Pterodactyl\Http\Requests\Admin\LocationFormRequest $request
* @param \Pterodactyl\Models\Location $location
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Throwable
* @throws \Watson\Validating\ValidationException
*/
public function update(LocationRequest $request, Location $location)
public function update(LocationFormRequest $request, Location $location)
{
if ($request->input('action') === 'delete') {
return $this->delete($location);

View file

@ -24,25 +24,25 @@
namespace Pterodactyl\Http\Controllers\Admin;
use Log;
use Alert;
use Javascript;
use Illuminate\Http\Request;
use Pterodactyl\Models\Node;
use Pterodactyl\Models\Allocation;
use Prologue\Alerts\AlertsMessageBag;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Repositories\NodeRepository;
use Pterodactyl\Services\Nodes\UpdateService;
use Pterodactyl\Services\Nodes\CreationService;
use Pterodactyl\Services\Nodes\DeletionService;
use Illuminate\Contracts\Translation\Translator;
use Illuminate\Cache\Repository as CacheRepository;
use Pterodactyl\Http\Requests\Admin\NodeFormRequest;
use Pterodactyl\Exceptions\DisplayValidationException;
use Pterodactyl\Services\Allocations\AssignmentService;
use Pterodactyl\Http\Requests\Admin\Node\NodeFormRequest;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
use Pterodactyl\Http\Requests\Admin\Node\AllocationFormRequest;
use Pterodactyl\Contracts\Repository\LocationRepositoryInterface;
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
use Pterodactyl\Http\Requests\Admin\Node\AllocationAliasFormRequest;
class NodesController extends Controller
{
@ -51,6 +51,16 @@ class NodesController extends Controller
*/
protected $alert;
/**
* @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface
*/
protected $allocationRepository;
/**
* @var \Pterodactyl\Services\Allocations\AssignmentService
*/
protected $assignmentService;
/**
* @var \Illuminate\Cache\Repository
*/
@ -86,8 +96,24 @@ class NodesController extends Controller
*/
protected $updateService;
/**
* NodesController constructor.
*
* @param \Prologue\Alerts\AlertsMessageBag $alert
* @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $allocationRepository
* @param \Pterodactyl\Services\Allocations\AssignmentService $assignmentService
* @param \Illuminate\Cache\Repository $cache
* @param \Pterodactyl\Services\Nodes\CreationService $creationService
* @param \Pterodactyl\Services\Nodes\DeletionService $deletionService
* @param \Pterodactyl\Contracts\Repository\LocationRepositoryInterface $locationRepository
* @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository
* @param \Illuminate\Contracts\Translation\Translator $translator
* @param \Pterodactyl\Services\Nodes\UpdateService $updateService
*/
public function __construct(
AlertsMessageBag $alert,
AllocationRepositoryInterface $allocationRepository,
AssignmentService $assignmentService,
CacheRepository $cache,
CreationService $creationService,
DeletionService $deletionService,
@ -97,6 +123,8 @@ class NodesController extends Controller
UpdateService $updateService
) {
$this->alert = $alert;
$this->allocationRepository = $allocationRepository;
$this->assignmentService = $assignmentService;
$this->cache = $cache;
$this->creationService = $creationService;
$this->deletionService = $deletionService;
@ -139,7 +167,7 @@ class NodesController extends Controller
/**
* Post controller to create a new node on the system.
*
* @param \Pterodactyl\Http\Requests\Admin\NodeFormRequest $request
* @param \Pterodactyl\Http\Requests\Admin\Node\NodeFormRequest $request
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
@ -224,8 +252,8 @@ class NodesController extends Controller
/**
* Updates settings for a node.
*
* @param \Pterodactyl\Http\Requests\Admin\NodeFormRequest $request
* @param \Pterodactyl\Models\Node $node
* @param \Pterodactyl\Http\Requests\Admin\Node\NodeFormRequest $request
* @param \Pterodactyl\Models\Node $node
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Pterodactyl\Exceptions\DisplayException
@ -242,12 +270,11 @@ class NodesController extends Controller
/**
* Removes a single allocation from a node.
*
* @param \Illuminate\Http\Request $request
* @param int $node
* @param int $allocation
* @param int $node
* @param int $allocation
* @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse
*/
public function allocationRemoveSingle(Request $request, $node, $allocation)
public function allocationRemoveSingle($node, $allocation)
{
$query = Allocation::where('node_id', $node)->whereNull('server_id')->where('id', $allocation)->delete();
if ($query < 1) {
@ -284,55 +311,35 @@ class NodesController extends Controller
/**
* Sets an alias for a specific allocation on a node.
*
* @param \Illuminate\Http\Request $request
* @param int $node
* @return \Illuminate\Http\Response
* @param \Pterodactyl\Http\Requests\Admin\Node\AllocationAliasFormRequest $request
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
*/
public function allocationSetAlias(Request $request, $node)
public function allocationSetAlias(AllocationAliasFormRequest $request)
{
if (! $request->input('allocation_id')) {
return response('Missing required parameters.', 422);
}
$this->allocationRepository->update($request->input('allocation_id'), [
'ip_alias' => (empty($request->input('alias'))) ? null : $request->input('alias'),
]);
try {
$update = Allocation::findOrFail($request->input('allocation_id'));
$update->ip_alias = (empty($request->input('alias'))) ? null : $request->input('alias');
$update->save();
return response('', 204);
} catch (\Exception $ex) {
throw $ex;
}
return response('', 204);
}
/**
* Creates new allocations on a node.
*
* @param \Illuminate\Http\Request $request
* @param int $node
* @param \Pterodactyl\Http\Requests\Admin\Node\AllocationFormRequest $request
* @param int|\Pterodactyl\Models\Node $node
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Pterodactyl\Exceptions\DisplayException
*/
public function createAllocation(Request $request, $node)
public function createAllocation(AllocationFormRequest $request, Node $node)
{
$repo = new NodeRepository;
$this->assignmentService->handle($node, $request->normalize());
$this->alert->success($this->translator->trans('admin/node.notices.allocations_added'))->flash();
try {
$repo->addAllocations($node, $request->intersect(['allocation_ip', 'allocation_alias', 'allocation_ports']));
Alert::success('Successfully added new allocations!')->flash();
} catch (DisplayValidationException $ex) {
return redirect()
->route('admin.nodes.view.allocation', $node)
->withErrors(json_decode($ex->getMessage()))
->withInput();
} catch (DisplayException $ex) {
Alert::danger($ex->getMessage())->flash();
} catch (\Exception $ex) {
Log::error($ex);
Alert::danger('An unhandled exception occured while attempting to add allocations this node. This error has been logged.')
->flash();
}
return redirect()->route('admin.nodes.view.allocation', $node);
return redirect()->route('admin.nodes.view.allocation', $node->id);
}
/**

View file

@ -26,7 +26,7 @@ namespace Pterodactyl\Http\Requests\Admin;
use Pterodactyl\Models\Location;
class LocationRequest extends AdminFormRequest
class LocationFormRequest extends AdminFormRequest
{
/**
* Setup the validation rules to use for these requests.
@ -36,7 +36,7 @@ class LocationRequest extends AdminFormRequest
public function rules()
{
if ($this->method() === 'PATCH') {
return Location::getUpdateRulesForId($this->location->id);
return Location::getUpdateRulesForId($this->route()->parameter('location')->id);
}
return Location::getCreateRules();

View file

@ -0,0 +1,41 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
namespace Pterodactyl\Http\Requests\Admin\Node;
use Pterodactyl\Http\Requests\Admin\AdminFormRequest;
class AllocationAliasFormRequest extends AdminFormRequest
{
/**
* @return array
*/
public function rules()
{
return [
'alias' => 'required|nullable|string',
'allocation_id' => 'required|numeric|exists:allocations,id',
];
}
}

View file

@ -0,0 +1,42 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
namespace Pterodactyl\Http\Requests\Admin\Node;
use Pterodactyl\Http\Requests\Admin\AdminFormRequest;
class AllocationFormRequest extends AdminFormRequest
{
/**
* @return array
*/
public function rules()
{
return [
'allocation_ip' => 'required|string',
'allocation_alias' => 'sometimes|string|max:255',
'allocation_ports' => 'required|array',
];
}
}

View file

@ -22,9 +22,10 @@
* SOFTWARE.
*/
namespace Pterodactyl\Http\Requests\Admin;
namespace Pterodactyl\Http\Requests\Admin\Node;
use Pterodactyl\Models\Node;
use Pterodactyl\Http\Requests\Admin\AdminFormRequest;
class NodeFormRequest extends AdminFormRequest
{

View file

@ -34,7 +34,7 @@ class UserFormRequest extends AdminFormRequest
public function rules()
{
if ($this->method() === 'PATCH') {
return User::getUpdateRulesForId($this->user->id);
return User::getUpdateRulesForId($this->route()->parameter('user')->id);
}
return User::getCreateRules();

View file

@ -24,10 +24,15 @@
namespace Pterodactyl\Models;
use Sofa\Eloquence\Eloquence;
use Sofa\Eloquence\Validable;
use Illuminate\Database\Eloquent\Model;
use Sofa\Eloquence\Contracts\Validable as ValidableContract;
class Allocation extends Model
class Allocation extends Model implements ValidableContract
{
use Eloquence, Validable;
/**
* The table associated with the model.
*
@ -42,46 +47,66 @@ class Allocation extends Model
*/
protected $guarded = ['id', 'created_at', 'updated_at'];
/**
* Cast values to correct type.
*
* @var array
*/
protected $casts = [
'node_id' => 'integer',
'port' => 'integer',
'server_id' => 'integer',
];
/**
* Cast values to correct type.
*
* @var array
*/
protected $casts = [
'node_id' => 'integer',
'port' => 'integer',
'server_id' => 'integer',
];
/**
* Accessor to automatically provide the IP alias if defined.
*
* @param null|string $value
* @return string
*/
public function getAliasAttribute($value)
{
return (is_null($this->ip_alias)) ? $this->ip : $this->ip_alias;
}
/**
* @var array
*/
protected static $applicationRules = [
'node_id' => 'required',
'ip' => 'required',
'port' => 'required',
];
/**
* Accessor to quickly determine if this allocation has an alias.
*
* @param null|string $value
* @return bool
*/
public function getHasAliasAttribute($value)
{
return ! is_null($this->ip_alias);
}
/**
* @var array
*/
protected static $dataIntegrityRules = [
'node_id' => 'exists:nodes,id',
'ip' => 'ip',
'port' => 'numeric|between:1024,65553',
'alias' => 'string',
'server_id' => 'nullable|exists:servers,id',
];
/**
* Gets information for the server associated with this allocation.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function server()
{
return $this->belongsTo(Server::class);
}
/**
* Accessor to automatically provide the IP alias if defined.
*
* @param null|string $value
* @return string
*/
public function getAliasAttribute($value)
{
return (is_null($this->ip_alias)) ? $this->ip : $this->ip_alias;
}
/**
* Accessor to quickly determine if this allocation has an alias.
*
* @param null|string $value
* @return bool
*/
public function getHasAliasAttribute($value)
{
return ! is_null($this->ip_alias);
}
/**
* Gets information for the server associated with this allocation.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function server()
{
return $this->belongsTo(Server::class);
}
}

View file

@ -24,6 +24,7 @@
namespace Pterodactyl\Repositories\Eloquent;
use Illuminate\Database\Query\Expression;
use Pterodactyl\Repository\Repository;
use Pterodactyl\Contracts\Repository\RepositoryInterface;
use Pterodactyl\Exceptions\Model\DataValidationException;
@ -185,6 +186,44 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf
return $this->getBuilder()->insert($data);
}
/**
* Insert multiple records into the database and ignore duplicates.
*
* @param array $values
* @return bool
*/
public function insertIgnore(array $values)
{
if (empty($values)) {
return true;
}
if (! is_array(reset($values))) {
$values = [$values];
} else {
foreach ($values as $key => $value) {
ksort($value);
$values[$key] = $value;
}
}
$bindings = array_values(array_filter(array_flatten($values, 1), function ($binding) {
return ! $binding instanceof Expression;
}));
$grammar = $this->getBuilder()->toBase()->getGrammar();
$table = $grammar->wrapTable($this->getModel()->getTable());
$columns = $grammar->columnize(array_keys(reset($values)));
$parameters = collect($values)->map(function ($record) use ($grammar) {
return sprintf('(%s)', $grammar->parameterize($record));
})->implode(', ');
$statement = "insert ignore into $table ($columns) values $parameters";
return $this->getBuilder()->getConnection()->statement($statement, $bindings);
}
/**
* {@inheritdoc}
* @return bool|\Illuminate\Database\Eloquent\Model

View file

@ -113,10 +113,8 @@ class NodeRepository extends SearchableRepository implements NodeRepositoryInter
$instance->setRelation(
'allocations',
$this->getModel()->allocations()->orderBy('ip', 'asc')
->orderBy('port', 'asc')
->with('server')
->paginate(50)
$instance->allocations()->orderBy('ip', 'asc')->orderBy('port', 'asc')
->with('server')->paginate(50)
);
return $instance;

View file

@ -0,0 +1,125 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
namespace Pterodactyl\Services\Allocations;
use IPTools\Network;
use Pterodactyl\Models\Node;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
class AssignmentService
{
const CIDR_MAX_BITS = 27;
const CIDR_MIN_BITS = 32;
const PORT_RANGE_LIMIT = 1000;
const PORT_RANGE_REGEX = '/^(\d{1,5})-(\d{1,5})$/';
/**
* @var \Illuminate\Database\ConnectionInterface
*/
protected $connection;
/**
* @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface
*/
protected $repository;
/**
* AssignmentService constructor.
*
* @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $repository
* @param \Illuminate\Database\ConnectionInterface $connection
*/
public function __construct(
AllocationRepositoryInterface $repository,
ConnectionInterface $connection
) {
$this->connection = $connection;
$this->repository = $repository;
}
/**
* Insert allocations into the database and link them to a specific node.
*
* @param int|\Pterodactyl\Models\Node $node
* @param array $data
*
* @throws \Pterodactyl\Exceptions\DisplayException
*/
public function handle($node, array $data)
{
if ($node instanceof Node) {
$node = $node->id;
}
$explode = explode('/', $data['allocation_ip']);
if (count($explode) !== 1) {
if (! ctype_digit($explode[1]) || ($explode[1] > self::CIDR_MIN_BITS || $explode[1] < self::CIDR_MAX_BITS)) {
throw new DisplayException(trans('admin/exceptions.allocations.cidr_out_of_range'));
}
}
$this->connection->beginTransaction();
foreach (Network::parse(gethostbyname($data['allocation_ip'])) as $ip) {
foreach ($data['allocation_ports'] as $port) {
if (! ctype_digit($port) && ! preg_match(self::PORT_RANGE_REGEX, $port)) {
throw new DisplayException(trans('admin/exceptions.allocations.invalid_mapping', ['port' => $port]));
}
$insertData = [];
if (preg_match(self::PORT_RANGE_REGEX, $port, $matches)) {
$block = range($matches[1], $matches[2]);
if (count($block) > self::PORT_RANGE_LIMIT) {
throw new DisplayException(trans('admin/exceptions.allocations.too_many_ports'));
}
foreach ($block as $unit) {
$insertData[] = [
'node_id' => $node,
'ip' => $ip->__toString(),
'port' => (int) $unit,
'ip_alias' => array_get($data, 'allocation_alias'),
'server_id' => null,
];
}
} else {
$insertData[] = [
'node_id' => $node,
'ip' => $ip->__toString(),
'port' => (int) $port,
'ip_alias' => array_get($data, 'allocation_alias'),
'server_id' => null,
];
}
$this->repository->insertIgnore($insertData);
}
}
$this->connection->commit();
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class SetAllocationUnqiueUsingMultipleFields extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('allocations', function (Blueprint $table) {
$table->unique(['node_id', 'ip', 'port']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('allocations', function (Blueprint $table) {
$table->dropUnique(['node_id', 'ip', 'port']);
});
}
}

View file

@ -28,4 +28,9 @@ return [
'servers_attached' => 'A node must have no servers linked to it in order to be deleted.',
'daemon_off_config_updated' => 'The daemon configuration <strong>has been updated</strong>, however there was an error encountered while attempting to automatically update the configuration file on the Daemon. You will need to manually update the configuration file (core.json) for the daemon to apply these changes. The daemon responded with a HTTP/:code response code and the error has been logged.',
],
'allocations' => [
'too_many_ports' => 'Adding more than 1000 ports at a single time is not supported. Please use a smaller range.',
'invalid_mapping' => 'The mapping provided for :port was invalid and could not be processed.',
'cidr_out_of_range' => 'CIDR notation only allows masks between /25 and /32.',
],
];

View file

@ -28,6 +28,7 @@ return [
'fqdn_required_for_ssl' => 'A fully qualified domain name that resolves to a public IP address is required in order to use SSL for this node.',
],
'notices' => [
'allocations_added' => 'Allocations have successfully been added to this node.',
'node_deleted' => 'Node has been successfully removed from the panel.',
'location_required' => 'You must have at least one location configured before you can add a node to this panel.',
'node_created' => 'Successfully created new node. You can automatically configure the daemon on this machine by visiting the \'Configuration\' tab. <strong>Before you can add any servers you must first allocate at least one IP address and port.</strong>',

View file

@ -0,0 +1,327 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
namespace Tests\Unit\Services\Allocations;
use Exception;
use Mockery as m;
use Tests\TestCase;
use phpmock\phpunit\PHPMock;
use Pterodactyl\Models\Node;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Services\Allocations\AssignmentService;
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
class AssignmentServiceTest extends TestCase
{
use PHPMock;
/**
* @var \Illuminate\Database\ConnectionInterface
*/
protected $connection;
/**
* @var \Pterodactyl\Models\Node
*/
protected $node;
/**
* @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface
*/
protected $repository;
/**
* @var \Pterodactyl\Services\Allocations\AssignmentService
*/
protected $service;
/**
* Setup tests.
*/
public function setUp()
{
parent::setUp();
// Due to a bug in PHP, this is necessary since we only have a single test
// that relies on this mock. If this does not exist the test will fail to register
// correctly.
//
// This can also be avoided if tests were run in isolated processes, or if that test
// came first, but neither of those are good solutions, so this is the next best option.
PHPMock::defineFunctionMock('\\Pterodactyl\\Services\\Allocations', 'gethostbyname');
$this->node = factory(Node::class)->make();
$this->connection = m::mock(ConnectionInterface::class);
$this->repository = m::mock(AllocationRepositoryInterface::class);
$this->service = new AssignmentService($this->repository, $this->connection);
}
/**
* Test a non-CIDR notated IP address without a port range.
*/
public function testIndividualIpAddressWithoutRange()
{
$data = [
'allocation_ip' => '192.168.1.1',
'allocation_ports' => ['1024']
];
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->repository->shouldReceive('insertIgnore')->with([
[
'node_id' => $this->node->id,
'ip' => '192.168.1.1',
'port' => 1024,
'ip_alias' => null,
'server_id' => null,
],
])->once()->andReturnNull();
$this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$this->service->handle($this->node->id, $data);
}
/**
* Test a non-CIDR IP address with a port range provided.
*/
public function testIndividualIpAddressWithRange()
{
$data = [
'allocation_ip' => '192.168.1.1',
'allocation_ports' => ['1024-1026']
];
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->repository->shouldReceive('insertIgnore')->with([
[
'node_id' => $this->node->id,
'ip' => '192.168.1.1',
'port' => 1024,
'ip_alias' => null,
'server_id' => null,
],
[
'node_id' => $this->node->id,
'ip' => '192.168.1.1',
'port' => 1025,
'ip_alias' => null,
'server_id' => null,
],
[
'node_id' => $this->node->id,
'ip' => '192.168.1.1',
'port' => 1026,
'ip_alias' => null,
'server_id' => null,
],
])->once()->andReturnNull();
$this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$this->service->handle($this->node->id, $data);
}
/**
* Test a non-CIRD IP address with a single port and an alias.
*/
public function testIndividualIPAddressWithAlias()
{
$data = [
'allocation_ip' => '192.168.1.1',
'allocation_ports' => ['1024'],
'allocation_alias' => 'my.alias.net',
];
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->repository->shouldReceive('insertIgnore')->with([
[
'node_id' => $this->node->id,
'ip' => '192.168.1.1',
'port' => 1024,
'ip_alias' => 'my.alias.net',
'server_id' => null,
],
])->once()->andReturnNull();
$this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$this->service->handle($this->node->id, $data);
}
/**
* Test that a domain name can be passed in place of an IP address.
*/
public function testDomainNamePassedInPlaceOfIPAddress()
{
$data = [
'allocation_ip' => 'test-domain.com',
'allocation_ports' => ['1024'],
];
$this->getFunctionMock('\\Pterodactyl\\Services\\Allocations', 'gethostbyname')
->expects($this->once())->willReturn('192.168.1.1');
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->repository->shouldReceive('insertIgnore')->with([
[
'node_id' => $this->node->id,
'ip' => '192.168.1.1',
'port' => 1024,
'ip_alias' => null,
'server_id' => null,
],
])->once()->andReturnNull();
$this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$this->service->handle($this->node->id, $data);
}
/**
* Test that a CIDR IP address without a range works properly.
*/
public function testCIDRNotatedIPAddressWithoutRange()
{
$data = [
'allocation_ip' => '192.168.1.100/31',
'allocation_ports' => ['1024'],
];
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->repository->shouldReceive('insertIgnore')->with([
[
'node_id' => $this->node->id,
'ip' => '192.168.1.100',
'port' => 1024,
'ip_alias' => null,
'server_id' => null,
],
])->once()->andReturnNull();
$this->repository->shouldReceive('insertIgnore')->with([
[
'node_id' => $this->node->id,
'ip' => '192.168.1.101',
'port' => 1024,
'ip_alias' => null,
'server_id' => null,
],
])->once()->andReturnNull();
$this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$this->service->handle($this->node->id, $data);
}
/**
* Test that a CIDR IP address with a range works properly.
*/
public function testCIDRNotatedIPAddressOutsideRangeLimit()
{
$data = [
'allocation_ip' => '192.168.1.100/20',
'allocation_ports' => ['1024'],
];
try {
$this->service->handle($this->node->id, $data);
} catch (Exception $exception) {
$this->assertInstanceOf(DisplayException::class, $exception);
$this->assertEquals(trans('admin/exceptions.allocations.cidr_out_of_range'), $exception->getMessage());
}
}
/**
* Test that an exception is thrown if there are too many ports.
*/
public function testAllocationWithPortsExceedingLimit()
{
$data = [
'allocation_ip' => '192.168.1.1',
'allocation_ports' => ['5000-7000'],
];
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
try {
$this->service->handle($this->node->id, $data);
} catch (Exception $exception) {
if (! $exception instanceof DisplayException) {
throw $exception;
}
$this->assertInstanceOf(DisplayException::class, $exception);
$this->assertEquals(trans('admin/exceptions.allocations.too_many_ports'), $exception->getMessage());
}
}
/**
* Test that an exception is thrown if an invalid port is provided.
*/
public function testInvalidPortProvided()
{
$data = [
'allocation_ip' => '192.168.1.1',
'allocation_ports' => ['test123'],
];
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
try {
$this->service->handle($this->node->id, $data);
} catch (Exception $exception) {
if (! $exception instanceof DisplayException) {
throw $exception;
}
$this->assertInstanceOf(DisplayException::class, $exception);
$this->assertEquals(trans('admin/exceptions.allocations.invalid_mapping', ['port' => 'test123']), $exception->getMessage());
}
}
/**
* Test that a model can be passed in place of an ID.
*/
public function testModelCanBePassedInPlaceOfNodeModel()
{
$data = [
'allocation_ip' => '192.168.1.1',
'allocation_ports' => ['1024']
];
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->repository->shouldReceive('insertIgnore')->with([
[
'node_id' => $this->node->id,
'ip' => '192.168.1.1',
'port' => 1024,
'ip_alias' => null,
'server_id' => null,
],
])->once()->andReturnNull();
$this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$this->service->handle($this->node, $data);
}
}