From cfb7415e2aea6a1377a5ef5a0700d30b619581ce Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 10 Feb 2018 14:01:49 -0600 Subject: [PATCH] Fix data integrity exception, closes #922 --- CHANGELOG.md | 1 + .../Servers/StoreServerRequest.php | 4 + .../Servers/ServerCreationService.php | 25 ++++- .../Servers/ServerCreationServiceTest.php | 91 ++++++++++++++++++- 4 files changed, 116 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 900c06e13..b2f3c2ae6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. * `[rc.2]` — Fixes bad API behavior on `/user` routes. * `[rc.2]` — Fixes Admin CP user editing resetting a password on users unintentionally. * `[rc.2]` — Fixes bug with server creation API endpoint that would fail to validate `allocation.default` correctly. +* `[rc.2]` — Fix data integrity exception occuring due to invalid data being passed to server creation service on the API. ### Added * Added ability to search the following API endpoints: list users, list servers, and list locations. diff --git a/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php b/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php index 6b4473ab3..ff389e05e 100644 --- a/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php +++ b/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php @@ -49,6 +49,10 @@ class StoreServerRequest extends ApplicationApiRequest 'limits.io' => $rules['io'], 'limits.cpu' => $rules['cpu'], + // Placeholders for rules added in withValidator() function. + 'allocation.default' => '', + 'allocation.additional.*' => '', + // Automatic deployment rules 'deploy' => 'sometimes|required|array', 'deploy.locations' => 'array', diff --git a/app/Services/Servers/ServerCreationService.php b/app/Services/Servers/ServerCreationService.php index 4b2f01bd1..a57e7712b 100644 --- a/app/Services/Servers/ServerCreationService.php +++ b/app/Services/Servers/ServerCreationService.php @@ -112,7 +112,9 @@ class ServerCreationService /** * Create a server on the Panel and trigger a request to the Daemon to begin the server - * creation process. + * creation process. This function will attempt to set as many additional values + * as possible given the input data. For example, if an allocation_id is passed with + * no node_id the node_is will be picked from the allocation. * * @param array $data * @param \Pterodactyl\Models\Objects\DeploymentObject|null $deployment @@ -138,6 +140,12 @@ class ServerCreationService $data['node_id'] = $allocation->node_id; } + // Auto-configure the node based on the selected allocation + // if no node was defined. + if (is_null(array_get($data, 'node_id'))) { + $data['node_id'] = $this->getNodeFromAllocation($data['allocation_id']); + } + if (is_null(array_get($data, 'nest_id'))) { $egg = $this->eggRepository->setColumns(['id', 'nest_id'])->find(array_get($data, 'egg_id')); $data['nest_id'] = $egg->nest_id; @@ -263,4 +271,19 @@ class ServerCreationService $this->serverVariableRepository->insert($records); } } + + /** + * Get the node that an allocation belongs to. + * + * @param int $allocation + * @return int + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + private function getNodeFromAllocation(int $allocation): int + { + $allocation = $this->allocationRepository->setColumns(['id', 'node_id'])->find($allocation); + + return $allocation->node_id; + } } diff --git a/tests/Unit/Services/Servers/ServerCreationServiceTest.php b/tests/Unit/Services/Servers/ServerCreationServiceTest.php index 5f6c61d1a..309508dbf 100644 --- a/tests/Unit/Services/Servers/ServerCreationServiceTest.php +++ b/tests/Unit/Services/Servers/ServerCreationServiceTest.php @@ -4,12 +4,14 @@ namespace Tests\Unit\Services\Servers; use Mockery as m; use Tests\TestCase; -use Pterodactyl\Models\Node; +use Pterodactyl\Models\Egg; use Pterodactyl\Models\User; use Tests\Traits\MocksUuids; use Pterodactyl\Models\Server; +use Pterodactyl\Models\Allocation; use Tests\Traits\MocksRequestException; use Illuminate\Database\ConnectionInterface; +use Pterodactyl\Models\Objects\DeploymentObject; use Pterodactyl\Services\Servers\ServerCreationService; use Pterodactyl\Services\Servers\VariableValidatorService; use Pterodactyl\Services\Deployment\FindViableNodesService; @@ -35,7 +37,7 @@ class ServerCreationServiceTest extends TestCase private $allocationRepository; /** - * @var \Pterodactyl\Services\Deployment\AllocationSelectionService + * @var \Pterodactyl\Services\Deployment\AllocationSelectionService|\Mockery\Mock */ private $allocationSelectionService; @@ -55,12 +57,12 @@ class ServerCreationServiceTest extends TestCase private $daemonServerRepository; /** - * @var \Pterodactyl\Contracts\Repository\EggRepositoryInterface + * @var \Pterodactyl\Contracts\Repository\EggRepositoryInterface|\Mockery\Mock */ private $eggRepository; /** - * @var \Pterodactyl\Services\Deployment\FindViableNodesService + * @var \Pterodactyl\Services\Deployment\FindViableNodesService|\Mockery\Mock */ private $findViableNodesService; @@ -117,6 +119,7 @@ class ServerCreationServiceTest extends TestCase $this->repository->shouldReceive('create')->with(m::subset([ 'uuid' => $this->getKnownUuid(), 'node_id' => $model->node_id, + 'allocation_id' => $model->allocation_id, 'owner_id' => $model->owner_id, 'nest_id' => $model->nest_id, 'egg_id' => $model->egg_id, @@ -147,6 +150,86 @@ class ServerCreationServiceTest extends TestCase $this->assertSame($model, $response); } + /** + * Test that optional parameters get auto-filled correctly on the model. + */ + public function testDataIsAutoFilled() + { + $model = factory(Server::class)->make(['uuid' => $this->getKnownUuid()]); + $allocationModel = factory(Allocation::class)->make(['node_id' => $model->node_id]); + $eggModel = factory(Egg::class)->make(['nest_id' => $model->nest_id]); + + $this->connection->shouldReceive('beginTransaction')->once()->withNoArgs(); + $this->allocationRepository->shouldReceive('setColumns->find')->once()->with($model->allocation_id)->andReturn($allocationModel); + $this->eggRepository->shouldReceive('setColumns->find')->once()->with($model->egg_id)->andReturn($eggModel); + + $this->validatorService->shouldReceive('setUserLevel->handle')->once()->andReturn(collect([])); + $this->repository->shouldReceive('create')->once()->with(m::subset([ + 'uuid' => $this->getKnownUuid(), + 'node_id' => $model->node_id, + 'allocation_id' => $model->allocation_id, + 'nest_id' => $model->nest_id, + 'egg_id' => $model->egg_id, + ]))->andReturn($model); + + $this->allocationRepository->shouldReceive('assignAllocationsToServer')->once()->with($model->id, [$model->allocation_id]); + $this->configurationStructureService->shouldReceive('handle')->once()->with($model)->andReturn([]); + + $this->daemonServerRepository->shouldReceive('setServer->create')->once(); + $this->connection->shouldReceive('commit')->once()->withNoArgs()->andReturnNull(); + + $this->getService()->handle( + collect($model->toArray())->except(['node_id', 'nest_id'])->toArray() + ); + } + + /** + * Test that an auto-deployment object is used correctly if passed. + */ + public function testAutoDeploymentObject() + { + $model = factory(Server::class)->make(['uuid' => $this->getKnownUuid()]); + $deploymentObject = new DeploymentObject(); + $deploymentObject->setPorts(['25565']); + $deploymentObject->setDedicated(false); + $deploymentObject->setLocations([1]); + + $this->connection->shouldReceive('beginTransaction')->once()->withNoArgs(); + + $this->findViableNodesService->shouldReceive('setLocations')->once()->with($deploymentObject->getLocations())->andReturnSelf(); + $this->findViableNodesService->shouldReceive('setDisk')->once()->with($model->disk)->andReturnSelf(); + $this->findViableNodesService->shouldReceive('setMemory')->once()->with($model->memory)->andReturnSelf(); + $this->findViableNodesService->shouldReceive('handle')->once()->withNoArgs()->andReturn([1, 2]); + + $allocationModel = factory(Allocation::class)->make([ + 'id' => $model->allocation_id, + 'node_id' => $model->node_id, + ]); + $this->allocationSelectionService->shouldReceive('setDedicated')->once()->with($deploymentObject->isDedicated())->andReturnSelf(); + $this->allocationSelectionService->shouldReceive('setNodes')->once()->with([1, 2])->andReturnSelf(); + $this->allocationSelectionService->shouldReceive('setPorts')->once()->with($deploymentObject->getPorts())->andReturnSelf(); + $this->allocationSelectionService->shouldReceive('handle')->once()->withNoArgs()->andReturn($allocationModel); + + $this->validatorService->shouldReceive('setUserLevel->handle')->once()->andReturn(collect([])); + $this->repository->shouldReceive('create')->once()->with(m::subset([ + 'uuid' => $this->getKnownUuid(), + 'node_id' => $model->node_id, + 'allocation_id' => $model->allocation_id, + 'nest_id' => $model->nest_id, + 'egg_id' => $model->egg_id, + ]))->andReturn($model); + + $this->allocationRepository->shouldReceive('assignAllocationsToServer')->once()->with($model->id, [$model->allocation_id]); + $this->configurationStructureService->shouldReceive('handle')->once()->with($model)->andReturn([]); + + $this->daemonServerRepository->shouldReceive('setServer->create')->once(); + $this->connection->shouldReceive('commit')->once()->withNoArgs()->andReturnNull(); + + $this->getService()->handle( + collect($model->toArray())->except(['allocation_id', 'node_id'])->toArray(), $deploymentObject + ); + } + /** * Test handling of node timeout or other daemon error. *