Merge pull request #2434 from pressstartearly/develop
Added Autoallocation Button
This commit is contained in:
commit
fda50bb6e1
24 changed files with 842 additions and 140 deletions
|
@ -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.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,7 +10,9 @@ 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\SetPrimaryAllocationRequest;
|
use Pterodactyl\Http\Requests\Api\Client\Servers\Network\SetPrimaryAllocationRequest;
|
||||||
|
@ -27,20 +29,28 @@ class NetworkAllocationController extends ClientApiController
|
||||||
*/
|
*/
|
||||||
private $serverRepository;
|
private $serverRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \Pterodactyl\Services\Allocations\FindAssignableAllocationService
|
||||||
|
*/
|
||||||
|
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\FindAssignableAllocationService $assignableAllocationService
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
AllocationRepository $repository,
|
AllocationRepository $repository,
|
||||||
ServerRepository $serverRepository
|
ServerRepository $serverRepository,
|
||||||
|
FindAssignableAllocationService $assignableAllocationService
|
||||||
) {
|
) {
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
|
|
||||||
$this->repository = $repository;
|
$this->repository = $repository;
|
||||||
$this->serverRepository = $serverRepository;
|
$this->serverRepository = $serverRepository;
|
||||||
|
$this->assignableAllocationService = $assignableAllocationService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -100,6 +110,31 @@ class NetworkAllocationController extends ClientApiController
|
||||||
->toArray();
|
->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the notes for the allocation for a server.
|
||||||
|
*s
|
||||||
|
*
|
||||||
|
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Network\NewAllocationRequest $request
|
||||||
|
* @param \Pterodactyl\Models\Server $server
|
||||||
|
* @return array
|
||||||
|
*
|
||||||
|
* @throws \Pterodactyl\Exceptions\DisplayException
|
||||||
|
*/
|
||||||
|
public function store(NewAllocationRequest $request, Server $server): array
|
||||||
|
{
|
||||||
|
if ($server->allocations()->count() >= $server->allocation_limit) {
|
||||||
|
throw new DisplayException(
|
||||||
|
'Cannot assign additional allocations to this server: limit has been reached.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$allocation = $this->assignableAllocationService->handle($server);
|
||||||
|
|
||||||
|
return $this->fractal->item($allocation)
|
||||||
|
->transformWith($this->getTransformer(AllocationTransformer::class))
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete an allocation from a server.
|
* Delete an allocation from a server.
|
||||||
*
|
*
|
||||||
|
@ -109,18 +144,19 @@ class NetworkAllocationController extends ClientApiController
|
||||||
* @return \Illuminate\Http\JsonResponse
|
* @return \Illuminate\Http\JsonResponse
|
||||||
*
|
*
|
||||||
* @throws \Pterodactyl\Exceptions\DisplayException
|
* @throws \Pterodactyl\Exceptions\DisplayException
|
||||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
|
||||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
|
||||||
*/
|
*/
|
||||||
public function delete(DeleteAllocationRequest $request, Server $server, Allocation $allocation)
|
public function delete(DeleteAllocationRequest $request, Server $server, Allocation $allocation)
|
||||||
{
|
{
|
||||||
if ($allocation->id === $server->allocation_id) {
|
if ($allocation->id === $server->allocation_id) {
|
||||||
throw new DisplayException(
|
throw new DisplayException(
|
||||||
'Cannot delete the primary allocation for a server.'
|
'You cannot delete the primary allocation for this server.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->repository->update($allocation->id, ['server_id' => null, 'notes' => null]);
|
Allocation::query()->where('id', $allocation->id)->update([
|
||||||
|
'notes' => null,
|
||||||
|
'server_id' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,11 @@ class AdvancedSettingsFormRequest extends AdminFormRequest
|
||||||
'recaptcha:website_key' => 'required|string|max:191',
|
'recaptcha:website_key' => 'required|string|max:191',
|
||||||
'pterodactyl:guzzle:timeout' => 'required|integer|between:1,60',
|
'pterodactyl:guzzle:timeout' => 'required|integer|between:1,60',
|
||||||
'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:frequency' => 'required|integer|min:10',
|
||||||
|
'pterodactyl:client_features:allocations:enabled' => 'required|in:true,false',
|
||||||
|
'pterodactyl:client_features:allocations:range_start' => 'required|integer|between:1024,65535',
|
||||||
|
'pterodactyl:client_features:allocations:range_end' => 'required|integer|between:1024,65535',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,6 +38,11 @@ class AdvancedSettingsFormRequest extends AdminFormRequest
|
||||||
'recaptcha:website_key' => 'reCAPTCHA Website Key',
|
'recaptcha:website_key' => 'reCAPTCHA Website Key',
|
||||||
'pterodactyl:guzzle:timeout' => 'HTTP Request Timeout',
|
'pterodactyl:guzzle:timeout' => 'HTTP Request Timeout',
|
||||||
'pterodactyl:guzzle:connect_timeout' => 'HTTP Connection Timeout',
|
'pterodactyl:guzzle:connect_timeout' => 'HTTP Connection Timeout',
|
||||||
|
'pterodactyl:console:count' => 'Console Message Count',
|
||||||
|
'pterodactyl:console:frequency' => 'Console Frequency Tick',
|
||||||
|
'pterodactyl:client_features:allocations:enabled' => 'Auto Create Allocations Enabled',
|
||||||
|
'pterodactyl:client_features:allocations:range_start' => 'Starting Port',
|
||||||
|
'pterodactyl:client_features:allocations:range_end' => 'Ending Port',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Network;
|
||||||
|
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Pterodactyl\Models\Allocation;
|
||||||
|
use Pterodactyl\Models\Permission;
|
||||||
|
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
|
||||||
|
|
||||||
|
class NewAllocationRequest extends ClientApiRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function permission(): string
|
||||||
|
{
|
||||||
|
return Permission::ACTION_ALLOCATION_CREATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -30,6 +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',
|
||||||
|
'pterodactyl:client_features:allocations:enabled',
|
||||||
|
'pterodactyl:client_features:allocations:range_start',
|
||||||
|
'pterodactyl:client_features:allocations:range_end',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
125
app/Services/Allocations/FindAssignableAllocationService.php
Normal file
125
app/Services/Allocations/FindAssignableAllocationService.php
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pterodactyl\Services\Allocations;
|
||||||
|
|
||||||
|
use Webmozart\Assert\Assert;
|
||||||
|
use Pterodactyl\Models\Server;
|
||||||
|
use Pterodactyl\Models\Allocation;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new allocation on the server's node with a random port from the defined range
|
||||||
|
* in the settings. If there are no matches in that range, or something is wrong with the
|
||||||
|
* range information provided an exception will be raised.
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick a random port out of the remaining available ports.
|
||||||
|
/** @var int $port */
|
||||||
|
$port = $available[array_rand($available)];
|
||||||
|
|
||||||
|
$this->service->handle($server->node, [
|
||||||
|
'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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ return [
|
||||||
| change this value if you are not maintaining your own internal versions.
|
| change this value if you are not maintaining your own internal versions.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'version' => 'canary',
|
'version' => '1.0.1',
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
@ -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'),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Allocation } from '@/api/server/getServer';
|
||||||
|
import http from '@/api/http';
|
||||||
|
import { rawDataToServerAllocation } from '@/api/transformers';
|
||||||
|
|
||||||
|
export default async (uuid: string): Promise<Allocation> => {
|
||||||
|
const { data } = await http.post(`/api/client/servers/${uuid}/network/allocations`);
|
||||||
|
|
||||||
|
return rawDataToServerAllocation(data);
|
||||||
|
};
|
|
@ -1,9 +0,0 @@
|
||||||
import http from '@/api/http';
|
|
||||||
import { rawDataToServerAllocation } from '@/api/transformers';
|
|
||||||
import { Allocation } from '@/api/server/getServer';
|
|
||||||
|
|
||||||
export default async (uuid: string): Promise<Allocation[]> => {
|
|
||||||
const { data } = await http.get(`/api/client/servers/${uuid}/network/allocations`);
|
|
||||||
|
|
||||||
return (data.data || []).map(rawDataToServerAllocation);
|
|
||||||
};
|
|
15
resources/scripts/api/swr/getServerAllocations.ts
Normal file
15
resources/scripts/api/swr/getServerAllocations.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { ServerContext } from '@/state/server';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import http from '@/api/http';
|
||||||
|
import { rawDataToServerAllocation } from '@/api/transformers';
|
||||||
|
import { Allocation } from '@/api/server/getServer';
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||||
|
|
||||||
|
return useSWR<Allocation[]>([ 'server:allocations', uuid ], async () => {
|
||||||
|
const { data } = await http.get(`/api/client/servers/${uuid}/network/allocations`);
|
||||||
|
|
||||||
|
return (data.data || []).map(rawDataToServerAllocation);
|
||||||
|
}, { revalidateOnFocus: false, revalidateOnMount: false });
|
||||||
|
};
|
|
@ -20,7 +20,7 @@ const ConfirmationModal = ({ title, children, buttonText, onConfirmed }: Props)
|
||||||
<h2 css={tw`text-2xl mb-6`}>{title}</h2>
|
<h2 css={tw`text-2xl mb-6`}>{title}</h2>
|
||||||
<p css={tw`text-sm`}>{children}</p>
|
<p css={tw`text-sm`}>{children}</p>
|
||||||
<div css={tw`flex flex-wrap items-center justify-end mt-8`}>
|
<div css={tw`flex flex-wrap items-center justify-end mt-8`}>
|
||||||
<Button isSecondary onClick={() => dismiss()} css={tw`w-full sm:w-auto`}>
|
<Button isSecondary onClick={() => dismiss()} css={tw`w-full sm:w-auto border-transparent`}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button color={'red'} css={tw`w-full sm:w-auto mt-4 sm:mt-0 sm:ml-4`} onClick={() => onConfirmed()}>
|
<Button color={'red'} css={tw`w-full sm:w-auto mt-4 sm:mt-0 sm:ml-4`} onClick={() => onConfirmed()}>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { memo, useState } from 'react';
|
import React, { memo, useCallback, useState } from 'react';
|
||||||
import isEqual from 'react-fast-compare';
|
import isEqual from 'react-fast-compare';
|
||||||
import tw from 'twin.macro';
|
import tw from 'twin.macro';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
@ -15,20 +15,26 @@ import setServerAllocationNotes from '@/api/server/network/setServerAllocationNo
|
||||||
import useFlash from '@/plugins/useFlash';
|
import useFlash from '@/plugins/useFlash';
|
||||||
import { ServerContext } from '@/state/server';
|
import { ServerContext } from '@/state/server';
|
||||||
import CopyOnClick from '@/components/elements/CopyOnClick';
|
import CopyOnClick from '@/components/elements/CopyOnClick';
|
||||||
|
import DeleteAllocationButton from '@/components/server/network/DeleteAllocationButton';
|
||||||
|
import setPrimaryServerAllocation from '@/api/server/network/setPrimaryServerAllocation';
|
||||||
|
import getServerAllocations from '@/api/swr/getServerAllocations';
|
||||||
|
|
||||||
const Code = styled.code`${tw`font-mono py-1 px-2 bg-neutral-900 rounded text-sm inline-block`}`;
|
const Code = styled.code`${tw`font-mono py-1 px-2 bg-neutral-900 rounded text-sm inline-block`}`;
|
||||||
const Label = styled.label`${tw`uppercase text-xs mt-1 text-neutral-400 block px-1 select-none transition-colors duration-150`}`;
|
const Label = styled.label`${tw`uppercase text-xs mt-1 text-neutral-400 block px-1 select-none transition-colors duration-150`}`;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
allocation: Allocation;
|
allocation: Allocation;
|
||||||
onSetPrimary: (id: number) => void;
|
|
||||||
onNotesChanged: (id: number, notes: string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const AllocationRow = ({ allocation, onSetPrimary, onNotesChanged }: Props) => {
|
const AllocationRow = ({ allocation }: Props) => {
|
||||||
const [ loading, setLoading ] = useState(false);
|
const [ loading, setLoading ] = useState(false);
|
||||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||||
|
const { mutate } = getServerAllocations();
|
||||||
|
|
||||||
|
const onNotesChanged = useCallback((id: number, notes: string) => {
|
||||||
|
mutate(data => data?.map(a => a.id === id ? { ...a, notes } : a), false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const setAllocationNotes = debounce((notes: string) => {
|
const setAllocationNotes = debounce((notes: string) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
@ -40,14 +46,26 @@ const AllocationRow = ({ allocation, onSetPrimary, onNotesChanged }: Props) => {
|
||||||
.then(() => setLoading(false));
|
.then(() => setLoading(false));
|
||||||
}, 750);
|
}, 750);
|
||||||
|
|
||||||
|
const setPrimaryAllocation = () => {
|
||||||
|
clearFlashes('server:network');
|
||||||
|
mutate(data => data?.map(a => ({ ...a, isDefault: a.id === allocation.id })), false);
|
||||||
|
|
||||||
|
setPrimaryServerAllocation(uuid, allocation.id)
|
||||||
|
.catch(error => {
|
||||||
|
clearAndAddHttpError({ key: 'server:network', error });
|
||||||
|
mutate();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GreyRowBox $hoverable={false} css={tw`flex-wrap md:flex-no-wrap mt-2`}>
|
<GreyRowBox $hoverable={false} css={tw`flex-wrap md:flex-no-wrap mt-2`}>
|
||||||
<div css={tw`flex items-center w-full md:w-auto`}>
|
<div css={tw`flex items-center w-full md:w-auto`}>
|
||||||
<div css={tw`pl-4 pr-6 text-neutral-400`}>
|
<div css={tw`pl-4 pr-6 text-neutral-400`}>
|
||||||
<FontAwesomeIcon icon={faNetworkWired} />
|
<FontAwesomeIcon icon={faNetworkWired}/>
|
||||||
</div>
|
</div>
|
||||||
<div css={tw`mr-4 flex-1 md:w-40`}>
|
<div css={tw`mr-4 flex-1 md:w-40`}>
|
||||||
{allocation.alias ? <CopyOnClick text={allocation.alias}><Code css={tw`w-40 truncate`}>{allocation.alias}</Code></CopyOnClick> :
|
{allocation.alias ?
|
||||||
|
<CopyOnClick text={allocation.alias}><Code css={tw`w-40 truncate`}>{allocation.alias}</Code></CopyOnClick> :
|
||||||
<CopyOnClick text={allocation.ip}><Code>{allocation.ip}</Code></CopyOnClick>}
|
<CopyOnClick text={allocation.ip}><Code>{allocation.ip}</Code></CopyOnClick>}
|
||||||
<Label>{allocation.alias ? 'Hostname' : 'IP Address'}</Label>
|
<Label>{allocation.alias ? 'Hostname' : 'IP Address'}</Label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -66,20 +84,25 @@ const AllocationRow = ({ allocation, onSetPrimary, onNotesChanged }: Props) => {
|
||||||
/>
|
/>
|
||||||
</InputSpinner>
|
</InputSpinner>
|
||||||
</div>
|
</div>
|
||||||
<div css={tw`w-full md:flex-none md:w-32 md:text-center mt-4 md:mt-0 text-right ml-4`}>
|
<div css={tw`w-full md:flex-none md:w-40 md:text-center mt-4 md:mt-0 ml-4 flex items-center justify-end`}>
|
||||||
{allocation.isDefault ?
|
{allocation.isDefault ?
|
||||||
<span css={tw`bg-green-500 py-1 px-2 rounded text-green-50 text-xs`}>Primary</span>
|
<span css={tw`bg-green-500 py-1 px-2 rounded text-green-50 text-xs`}>Primary</span>
|
||||||
:
|
:
|
||||||
<Can action={'allocations.update'}>
|
<>
|
||||||
<Button
|
<Can action={'allocations.delete'}>
|
||||||
isSecondary
|
<DeleteAllocationButton allocation={allocation.id}/>
|
||||||
size={'xsmall'}
|
</Can>
|
||||||
color={'primary'}
|
<Can action={'allocations.update'}>
|
||||||
onClick={() => onSetPrimary(allocation.id)}
|
<Button
|
||||||
>
|
isSecondary
|
||||||
Make Primary
|
size={'xsmall'}
|
||||||
</Button>
|
color={'primary'}
|
||||||
</Can>
|
onClick={setPrimaryAllocation}
|
||||||
|
>
|
||||||
|
Make Primary
|
||||||
|
</Button>
|
||||||
|
</Can>
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</GreyRowBox>
|
</GreyRowBox>
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { faTrashAlt } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import tw from 'twin.macro';
|
||||||
|
import Icon from '@/components/elements/Icon';
|
||||||
|
import ConfirmationModal from '@/components/elements/ConfirmationModal';
|
||||||
|
import { ServerContext } from '@/state/server';
|
||||||
|
import deleteServerAllocation from '@/api/server/network/deleteServerAllocation';
|
||||||
|
import getServerAllocations from '@/api/swr/getServerAllocations';
|
||||||
|
import useFlash from '@/plugins/useFlash';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
allocation: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeleteAllocationButton = ({ allocation }: Props) => {
|
||||||
|
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||||
|
const [ confirm, setConfirm ] = useState(false);
|
||||||
|
const { mutate } = getServerAllocations();
|
||||||
|
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||||
|
|
||||||
|
const deleteAllocation = () => {
|
||||||
|
clearFlashes('server:network');
|
||||||
|
|
||||||
|
mutate(data => data?.filter(a => a.id !== allocation), false);
|
||||||
|
deleteServerAllocation(uuid, allocation)
|
||||||
|
.catch(error => clearAndAddHttpError({ key: 'server:network', error }));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ConfirmationModal
|
||||||
|
visible={confirm}
|
||||||
|
title={'Remove this allocation?'}
|
||||||
|
buttonText={'Delete'}
|
||||||
|
onConfirmed={deleteAllocation}
|
||||||
|
onModalDismissed={() => setConfirm(false)}
|
||||||
|
>
|
||||||
|
This allocation will be immediately removed from your server. Are you sure you want to continue?
|
||||||
|
</ConfirmationModal>
|
||||||
|
<button
|
||||||
|
css={tw`text-neutral-400 px-2 py-1 mr-2 transition-colors duration-150 hover:text-red-400`}
|
||||||
|
type={'button'}
|
||||||
|
onClick={() => setConfirm(true)}
|
||||||
|
>
|
||||||
|
<Icon icon={faTrashAlt} css={tw`w-3 h-auto`}/>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeleteAllocationButton;
|
|
@ -1,24 +1,31 @@
|
||||||
import React, { useCallback, useEffect } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import useSWR from 'swr';
|
|
||||||
import getServerAllocations from '@/api/server/network/getServerAllocations';
|
|
||||||
import { Allocation } from '@/api/server/getServer';
|
|
||||||
import Spinner from '@/components/elements/Spinner';
|
import Spinner from '@/components/elements/Spinner';
|
||||||
import useFlash from '@/plugins/useFlash';
|
import useFlash from '@/plugins/useFlash';
|
||||||
import ServerContentBlock from '@/components/elements/ServerContentBlock';
|
import ServerContentBlock from '@/components/elements/ServerContentBlock';
|
||||||
import { ServerContext } from '@/state/server';
|
import { ServerContext } from '@/state/server';
|
||||||
import { useDeepMemoize } from '@/plugins/useDeepMemoize';
|
|
||||||
import AllocationRow from '@/components/server/network/AllocationRow';
|
import AllocationRow from '@/components/server/network/AllocationRow';
|
||||||
import setPrimaryServerAllocation from '@/api/server/network/setPrimaryServerAllocation';
|
import Button from '@/components/elements/Button';
|
||||||
|
import createServerAllocation from '@/api/server/network/createServerAllocation';
|
||||||
|
import tw from 'twin.macro';
|
||||||
|
import Can from '@/components/elements/Can';
|
||||||
|
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||||
|
import getServerAllocations from '@/api/swr/getServerAllocations';
|
||||||
|
import isEqual from 'react-fast-compare';
|
||||||
|
import { Allocation } from '@/api/server/getServer';
|
||||||
|
|
||||||
const NetworkContainer = () => {
|
const NetworkContainer = () => {
|
||||||
|
const [ loading, setLoading ] = useState(false);
|
||||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||||
const allocations = useDeepMemoize(ServerContext.useStoreState(state => state.server.data!.allocations));
|
const allocationLimit = ServerContext.useStoreState(state => state.server.data!.featureLimits.allocations);
|
||||||
|
// @ts-ignore
|
||||||
|
const allocations: Allocation[] = ServerContext.useStoreState(state => state.server.data!.allocations, isEqual);
|
||||||
|
|
||||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||||
const { data, error, mutate } = useSWR<Allocation[]>(uuid, key => getServerAllocations(key), {
|
const { data, error, mutate } = getServerAllocations();
|
||||||
initialData: allocations,
|
|
||||||
revalidateOnFocus: false,
|
useEffect(() => {
|
||||||
});
|
mutate(allocations, false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@ -26,36 +33,45 @@ const NetworkContainer = () => {
|
||||||
}
|
}
|
||||||
}, [ error ]);
|
}, [ error ]);
|
||||||
|
|
||||||
const setPrimaryAllocation = useCallback((id: number) => {
|
const onCreateAllocation = () => {
|
||||||
clearFlashes('server:network');
|
clearFlashes('server:network');
|
||||||
|
|
||||||
const initial = data;
|
setLoading(true);
|
||||||
mutate(data?.map(a => a.id === id ? { ...a, isDefault: true } : { ...a, isDefault: false }), false);
|
createServerAllocation(uuid)
|
||||||
|
.then(allocation => mutate(data?.concat(allocation), false))
|
||||||
setPrimaryServerAllocation(uuid, id)
|
.catch(error => clearAndAddHttpError({ key: 'server:network', error }))
|
||||||
.catch(error => {
|
.then(() => setLoading(false));
|
||||||
clearAndAddHttpError({ key: 'server:network', error });
|
};
|
||||||
mutate(initial, false);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onNotesAdded = useCallback((id: number, notes: string) => {
|
|
||||||
mutate(data?.map(a => a.id === id ? { ...a, notes } : a), false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ServerContentBlock showFlashKey={'server:network'} title={'Network'}>
|
<ServerContentBlock showFlashKey={'server:network'} title={'Network'}>
|
||||||
{!data ?
|
{!data ?
|
||||||
<Spinner size={'large'} centered/>
|
<Spinner size={'large'} centered/>
|
||||||
:
|
:
|
||||||
data.map(allocation => (
|
<>
|
||||||
<AllocationRow
|
{
|
||||||
key={`${allocation.ip}:${allocation.port}`}
|
data.map(allocation => (
|
||||||
allocation={allocation}
|
<AllocationRow
|
||||||
onSetPrimary={setPrimaryAllocation}
|
key={`${allocation.ip}:${allocation.port}`}
|
||||||
onNotesChanged={onNotesAdded}
|
allocation={allocation}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
}
|
||||||
|
<Can action={'allocation.create'}>
|
||||||
|
<SpinnerOverlay visible={loading}/>
|
||||||
|
<div css={tw`mt-6 sm:flex items-center justify-end`}>
|
||||||
|
<p css={tw`text-sm text-neutral-300 mb-4 sm:mr-6 sm:mb-0`}>
|
||||||
|
You are currently using {data.length} of {allocationLimit} allowed allocations for this
|
||||||
|
server.
|
||||||
|
</p>
|
||||||
|
{allocationLimit > data.length &&
|
||||||
|
<Button css={tw`w-full sm:w-auto`} color={'primary'} onClick={onCreateAllocation}>
|
||||||
|
Create Allocation
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</Can>
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
</ServerContentBlock>
|
</ServerContentBlock>
|
||||||
);
|
);
|
||||||
|
|
|
@ -115,7 +115,7 @@
|
||||||
<div>
|
<div>
|
||||||
<input type="text" name="allocation_limit" class="form-control" value="{{ old('allocation_limit', $server->allocation_limit) }}"/>
|
<input type="text" name="allocation_limit" class="form-control" value="{{ old('allocation_limit', $server->allocation_limit) }}"/>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-muted small"><strong>This feature is not currently implemented.</strong> The total number of allocations a user is allowed to create for this server.</p>
|
<p class="text-muted small">The total number of allocations a user is allowed to create for this server.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group col-xs-6">
|
<div class="form-group col-xs-6">
|
||||||
<label for="backup_limit" class="control-label">Backup Limit</label>
|
<label for="backup_limit" class="control-label">Backup Limit</label>
|
||||||
|
|
|
@ -82,6 +82,62 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="box">
|
||||||
|
<div class="box-header with-border">
|
||||||
|
<h3 class="box-title">Console</h3>
|
||||||
|
</div>
|
||||||
|
<div class="box-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-6">
|
||||||
|
<label class="control-label">Message Count</label>
|
||||||
|
<div>
|
||||||
|
<input type="number" required class="form-control" name="pterodactyl:console:count" value="{{ old('pterodactyl:console:count', config('pterodactyl.console.count')) }}">
|
||||||
|
<p class="text-muted small">The number of messages to be pushed to the console per frequency tick.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-md-6">
|
||||||
|
<label class="control-label">Frequency Tick</label>
|
||||||
|
<div>
|
||||||
|
<input type="number" required class="form-control" name="pterodactyl:console:frequency" value="{{ old('pterodactyl:console:frequency', config('pterodactyl.console.frequency')) }}">
|
||||||
|
<p class="text-muted small">The amount of time in milliseconds between each console message sending tick.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="box">
|
||||||
|
<div class="box-header with-border">
|
||||||
|
<h3 class="box-title">Automatic Allocation Creation</h3>
|
||||||
|
</div>
|
||||||
|
<div class="box-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-4">
|
||||||
|
<label class="control-label">Status</label>
|
||||||
|
<div>
|
||||||
|
<select class="form-control" name="pterodactyl:client_features:allocations:enabled">
|
||||||
|
<option value="false">Disabled</option>
|
||||||
|
<option value="true" @if(old('pterodactyl:client_features:allocations:enabled', config('pterodactyl.client_features.allocations.enabled'))) selected @endif>Enabled</option>
|
||||||
|
</select>
|
||||||
|
<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 class="form-group col-md-4">
|
||||||
|
<label class="control-label">Starting Port</label>
|
||||||
|
<div>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-md-4">
|
||||||
|
<label class="control-label">Ending Port</label>
|
||||||
|
<div>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="box box-primary">
|
<div class="box box-primary">
|
||||||
<div class="box-footer">
|
<div class="box-footer">
|
||||||
{{ csrf_field() }}
|
{{ csrf_field() }}
|
||||||
|
|
|
@ -82,6 +82,7 @@ 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::delete('/allocations/{allocation}', 'Servers\NetworkAllocationController@delete');
|
Route::delete('/allocations/{allocation}', 'Servers\NetworkAllocationController@delete');
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pterodactyl\Tests\Integration\Api\Client\Server\Allocation;
|
||||||
|
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
use Pterodactyl\Models\Permission;
|
||||||
|
use Pterodactyl\Models\Allocation;
|
||||||
|
use Pterodactyl\Tests\Integration\Api\Client\ClientApiIntegrationTestCase;
|
||||||
|
|
||||||
|
class CreateNewAllocationTest extends ClientApiIntegrationTestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Setup tests.
|
||||||
|
*/
|
||||||
|
public function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
config()->set('pterodactyl.client_features.allocations.enabled', true);
|
||||||
|
config()->set('pterodactyl.client_features.allocations.range_start', 5000);
|
||||||
|
config()->set('pterodactyl.client_features.allocations.range_end', 5050);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests that a new allocation can be properly assigned to a server.
|
||||||
|
*
|
||||||
|
* @param array $permission
|
||||||
|
* @dataProvider permissionDataProvider
|
||||||
|
*/
|
||||||
|
public function testNewAllocationCanBeAssignedToServer(array $permission)
|
||||||
|
{
|
||||||
|
/** @var \Pterodactyl\Models\Server $server */
|
||||||
|
[$user, $server] = $this->generateTestAccount($permission);
|
||||||
|
$server->update(['allocation_limit' => 2]);
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)->postJson($this->link($server, "/network/allocations"));
|
||||||
|
$response->assertJsonPath('object', Allocation::RESOURCE_NAME);
|
||||||
|
|
||||||
|
$matched = Allocation::query()->findOrFail($response->json('attributes.id'));
|
||||||
|
|
||||||
|
$this->assertSame($server->id, $matched->server_id);
|
||||||
|
$this->assertJsonTransformedWith($response->json('attributes'), $matched);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that a user without the required permissions cannot create an allocation for
|
||||||
|
* the server instance.
|
||||||
|
*/
|
||||||
|
public function testAllocationCannotBeCreatedIfUserDoesNotHavePermission()
|
||||||
|
{
|
||||||
|
/** @var \Pterodactyl\Models\Server $server */
|
||||||
|
[$user, $server] = $this->generateTestAccount([Permission::ACTION_ALLOCATION_UPDATE]);
|
||||||
|
$server->update(['allocation_limit' => 2]);
|
||||||
|
|
||||||
|
$this->actingAs($user)->postJson($this->link($server, "/network/allocations"))->assertForbidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that an error is returned to the user if this feature is not enabled on the system.
|
||||||
|
*/
|
||||||
|
public function testAllocationCannotBeCreatedIfNotEnabled()
|
||||||
|
{
|
||||||
|
config()->set('pterodactyl.client_features.allocations.enabled', false);
|
||||||
|
|
||||||
|
/** @var \Pterodactyl\Models\Server $server */
|
||||||
|
[$user, $server] = $this->generateTestAccount();
|
||||||
|
$server->update(['allocation_limit' => 2]);
|
||||||
|
|
||||||
|
$this->actingAs($user)->postJson($this->link($server, "/network/allocations"))
|
||||||
|
->assertStatus(Response::HTTP_BAD_REQUEST)
|
||||||
|
->assertJsonPath('errors.0.code', 'AutoAllocationNotEnabledException')
|
||||||
|
->assertJsonPath('errors.0.detail', 'Server auto-allocation is not enabled for this instance.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that an allocation cannot be created if the server has reached it's allocation limit.
|
||||||
|
*/
|
||||||
|
public function testAllocationCannotBeCreatedIfServerIsAtLimit()
|
||||||
|
{
|
||||||
|
/** @var \Pterodactyl\Models\Server $server */
|
||||||
|
[$user, $server] = $this->generateTestAccount();
|
||||||
|
$server->update(['allocation_limit' => 1]);
|
||||||
|
|
||||||
|
$this->actingAs($user)->postJson($this->link($server, "/network/allocations"))
|
||||||
|
->assertStatus(Response::HTTP_BAD_REQUEST)
|
||||||
|
->assertJsonPath('errors.0.code', 'DisplayException')
|
||||||
|
->assertJsonPath('errors.0.detail', 'Cannot assign additional allocations to this server: limit has been reached.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function permissionDataProvider()
|
||||||
|
{
|
||||||
|
return [[[Permission::ACTION_ALLOCATION_CREATE]], [[]]];
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pterodactyl\Tests\Integration\Api\Client\Server\Allocation;
|
||||||
|
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
use Pterodactyl\Models\Permission;
|
||||||
|
use Pterodactyl\Models\Allocation;
|
||||||
|
use Pterodactyl\Tests\Integration\Api\Client\ClientApiIntegrationTestCase;
|
||||||
|
|
||||||
|
class DeleteAllocationTest extends ClientApiIntegrationTestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Test that an allocation is deleted from the server and the notes are properly reset
|
||||||
|
* to an empty value on assignment.
|
||||||
|
*
|
||||||
|
* @param array $permission
|
||||||
|
* @dataProvider permissionDataProvider
|
||||||
|
*/
|
||||||
|
public function testAllocationCanBeDeletedFromServer(array $permission)
|
||||||
|
{
|
||||||
|
/** @var \Pterodactyl\Models\Server $server */
|
||||||
|
[$user, $server] = $this->generateTestAccount($permission);
|
||||||
|
|
||||||
|
/** @var \Pterodactyl\Models\Allocation $allocation */
|
||||||
|
$allocation = factory(Allocation::class)->create([
|
||||||
|
'server_id' => $server->id,
|
||||||
|
'node_id' => $server->node_id,
|
||||||
|
'notes' => 'hodor',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs($user)->deleteJson($this->link($allocation))->assertStatus(Response::HTTP_NO_CONTENT);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('allocations', ['id' => $allocation->id, 'server_id' => null, 'notes' => null]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that an error is returned if the user does not have permissiont to delete an allocation.
|
||||||
|
*/
|
||||||
|
public function testErrorIsReturnedIfUserDoesNotHavePermission()
|
||||||
|
{
|
||||||
|
/** @var \Pterodactyl\Models\Server $server */
|
||||||
|
[$user, $server] = $this->generateTestAccount([Permission::ACTION_ALLOCATION_CREATE]);
|
||||||
|
|
||||||
|
/** @var \Pterodactyl\Models\Allocation $allocation */
|
||||||
|
$allocation = factory(Allocation::class)->create([
|
||||||
|
'server_id' => $server->id,
|
||||||
|
'node_id' => $server->node_id,
|
||||||
|
'notes' => 'hodor',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs($user)->deleteJson($this->link($allocation))->assertForbidden();
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('allocations', ['id' => $allocation->id, 'server_id' => $server->id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that an allocation is not deleted if it is currently marked as the primary allocation
|
||||||
|
* for the server.
|
||||||
|
*/
|
||||||
|
public function testErrorIsReturnedIfAllocationIsPrimary()
|
||||||
|
{
|
||||||
|
/** @var \Pterodactyl\Models\Server $server */
|
||||||
|
[$user, $server] = $this->generateTestAccount();
|
||||||
|
|
||||||
|
$this->actingAs($user)->deleteJson($this->link($server->allocation))
|
||||||
|
->assertStatus(Response::HTTP_BAD_REQUEST)
|
||||||
|
->assertJsonPath('errors.0.code', 'DisplayException')
|
||||||
|
->assertJsonPath('errors.0.detail', 'You cannot delete the primary allocation for this server.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that an allocation cannot be deleted if it does not belong to the server instance.
|
||||||
|
*/
|
||||||
|
public function testErrorIsReturnedIfAllocationDoesNotBelongToServer()
|
||||||
|
{
|
||||||
|
/** @var \Pterodactyl\Models\Server $server */
|
||||||
|
[$user, $server] = $this->generateTestAccount();
|
||||||
|
[, $server2] = $this->generateTestAccount();
|
||||||
|
|
||||||
|
$this->actingAs($user)->deleteJson($this->link($server2->allocation))->assertNotFound();
|
||||||
|
$this->actingAs($user)->deleteJson($this->link($server, "/network/allocations/{$server2->allocation_id}"))->assertNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function permissionDataProvider()
|
||||||
|
{
|
||||||
|
return [[[Permission::ACTION_ALLOCATION_DELETE]], [[]]];
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,6 @@ namespace Pterodactyl\Tests\Integration\Api\Client\Server;
|
||||||
|
|
||||||
use Pterodactyl\Models\User;
|
use Pterodactyl\Models\User;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
use Pterodactyl\Models\Server;
|
|
||||||
use Pterodactyl\Models\Allocation;
|
use Pterodactyl\Models\Allocation;
|
||||||
use Pterodactyl\Models\Permission;
|
use Pterodactyl\Models\Permission;
|
||||||
use Pterodactyl\Tests\Integration\Api\Client\ClientApiIntegrationTestCase;
|
use Pterodactyl\Tests\Integration\Api\Client\ClientApiIntegrationTestCase;
|
||||||
|
@ -17,7 +16,6 @@ class NetworkAllocationControllerTest extends ClientApiIntegrationTestCase
|
||||||
public function testServerAllocationsAreReturned()
|
public function testServerAllocationsAreReturned()
|
||||||
{
|
{
|
||||||
[$user, $server] = $this->generateTestAccount();
|
[$user, $server] = $this->generateTestAccount();
|
||||||
$allocation = $this->getAllocation($server);
|
|
||||||
|
|
||||||
$response = $this->actingAs($user)->getJson($this->link($server, '/network/allocations'));
|
$response = $this->actingAs($user)->getJson($this->link($server, '/network/allocations'));
|
||||||
|
|
||||||
|
@ -25,7 +23,7 @@ class NetworkAllocationControllerTest extends ClientApiIntegrationTestCase
|
||||||
$response->assertJsonPath('object', 'list');
|
$response->assertJsonPath('object', 'list');
|
||||||
$response->assertJsonCount(1, 'data');
|
$response->assertJsonCount(1, 'data');
|
||||||
|
|
||||||
$this->assertJsonTransformedWith($response->json('data.0.attributes'), $allocation);
|
$this->assertJsonTransformedWith($response->json('data.0.attributes'), $server->allocation);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -57,7 +55,7 @@ class NetworkAllocationControllerTest extends ClientApiIntegrationTestCase
|
||||||
public function testAllocationNotesCanBeUpdated(array $permissions)
|
public function testAllocationNotesCanBeUpdated(array $permissions)
|
||||||
{
|
{
|
||||||
[$user, $server] = $this->generateTestAccount($permissions);
|
[$user, $server] = $this->generateTestAccount($permissions);
|
||||||
$allocation = $this->getAllocation($server);
|
$allocation = $server->allocation;
|
||||||
|
|
||||||
$this->assertNull($allocation->notes);
|
$this->assertNull($allocation->notes);
|
||||||
|
|
||||||
|
@ -92,13 +90,11 @@ class NetworkAllocationControllerTest extends ClientApiIntegrationTestCase
|
||||||
$server->owner_id = $user2->id;
|
$server->owner_id = $user2->id;
|
||||||
$server->save();
|
$server->save();
|
||||||
|
|
||||||
$this->actingAs($user)->postJson($this->link($this->getAllocation($server)))
|
$this->actingAs($user)->postJson($this->link($server->allocation))->assertNotFound();
|
||||||
->assertNotFound();
|
|
||||||
|
|
||||||
[$user, $server] = $this->generateTestAccount([Permission::ACTION_ALLOCATION_CREATE]);
|
[$user, $server] = $this->generateTestAccount([Permission::ACTION_ALLOCATION_CREATE]);
|
||||||
|
|
||||||
$this->actingAs($user)->postJson($this->link($this->getAllocation($server)))
|
$this->actingAs($user)->postJson($this->link($server->allocation))->assertForbidden();
|
||||||
->assertForbidden();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -108,8 +104,8 @@ class NetworkAllocationControllerTest extends ClientApiIntegrationTestCase
|
||||||
public function testPrimaryAllocationCanBeModified(array $permissions)
|
public function testPrimaryAllocationCanBeModified(array $permissions)
|
||||||
{
|
{
|
||||||
[$user, $server] = $this->generateTestAccount($permissions);
|
[$user, $server] = $this->generateTestAccount($permissions);
|
||||||
$allocation = $this->getAllocation($server);
|
$allocation = $server->allocation;
|
||||||
$allocation2 = $this->getAllocation($server);
|
$allocation2 = factory(Allocation::class)->create(['node_id' => $server->node_id, 'server_id' => $server->id]);
|
||||||
|
|
||||||
$server->allocation_id = $allocation->id;
|
$server->allocation_id = $allocation->id;
|
||||||
$server->save();
|
$server->save();
|
||||||
|
@ -130,61 +126,12 @@ class NetworkAllocationControllerTest extends ClientApiIntegrationTestCase
|
||||||
$server->owner_id = $user2->id;
|
$server->owner_id = $user2->id;
|
||||||
$server->save();
|
$server->save();
|
||||||
|
|
||||||
$this->actingAs($user)->postJson($this->link($this->getAllocation($server), '/primary'))
|
$this->actingAs($user)->postJson($this->link($server->allocation, '/primary'))
|
||||||
->assertNotFound();
|
->assertNotFound();
|
||||||
|
|
||||||
[$user, $server] = $this->generateTestAccount([Permission::ACTION_ALLOCATION_CREATE]);
|
[$user, $server] = $this->generateTestAccount([Permission::ACTION_ALLOCATION_CREATE]);
|
||||||
|
|
||||||
$this->actingAs($user)->postJson($this->link($this->getAllocation($server), '/primary'))
|
$this->actingAs($user)->postJson($this->link($server->allocation, '/primary'))
|
||||||
->assertForbidden();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array $permissions
|
|
||||||
* @dataProvider deletePermissionsDataProvider
|
|
||||||
*/
|
|
||||||
public function testAllocationCanBeDeleted(array $permissions)
|
|
||||||
{
|
|
||||||
[$user, $server] = $this->generateTestAccount($permissions);
|
|
||||||
$allocation = $this->getAllocation($server);
|
|
||||||
$allocation2 = $this->getAllocation($server);
|
|
||||||
|
|
||||||
$allocation2->notes = 'Filled notes';
|
|
||||||
$allocation2->save();
|
|
||||||
|
|
||||||
$server->allocation_id = $allocation->id;
|
|
||||||
$server->save();
|
|
||||||
|
|
||||||
$this->actingAs($user)->deleteJson($this->link($allocation))
|
|
||||||
->assertStatus(Response::HTTP_BAD_REQUEST)
|
|
||||||
->assertJsonPath('errors.0.code', 'DisplayException')
|
|
||||||
->assertJsonPath('errors.0.detail', 'Cannot delete the primary allocation for a server.');
|
|
||||||
|
|
||||||
$this->actingAs($user)->deleteJson($this->link($allocation2))
|
|
||||||
->assertStatus(Response::HTTP_NO_CONTENT);
|
|
||||||
|
|
||||||
$server = $server->refresh();
|
|
||||||
$allocation2 = $allocation2->refresh();
|
|
||||||
|
|
||||||
$this->assertSame($allocation->id, $server->allocation_id);
|
|
||||||
$this->assertNull($allocation2->server_id);
|
|
||||||
$this->assertNull($allocation2->notes);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testAllocationCannotBeDeletedByInvalidUser()
|
|
||||||
{
|
|
||||||
[$user, $server] = $this->generateTestAccount();
|
|
||||||
$user2 = factory(User::class)->create();
|
|
||||||
|
|
||||||
$server->owner_id = $user2->id;
|
|
||||||
$server->save();
|
|
||||||
|
|
||||||
$this->actingAs($user)->deleteJson($this->link($this->getAllocation($server)))
|
|
||||||
->assertNotFound();
|
|
||||||
|
|
||||||
[$user, $server] = $this->generateTestAccount([Permission::ACTION_ALLOCATION_CREATE]);
|
|
||||||
|
|
||||||
$this->actingAs($user)->deleteJson($this->link($this->getAllocation($server)))
|
|
||||||
->assertForbidden();
|
->assertForbidden();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,16 +144,4 @@ class NetworkAllocationControllerTest extends ClientApiIntegrationTestCase
|
||||||
{
|
{
|
||||||
return [[[]], [[Permission::ACTION_ALLOCATION_DELETE]]];
|
return [[[]], [[Permission::ACTION_ALLOCATION_DELETE]]];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param \Pterodactyl\Models\Server $server
|
|
||||||
* @return \Pterodactyl\Models\Allocation
|
|
||||||
*/
|
|
||||||
protected function getAllocation(Server $server): Allocation
|
|
||||||
{
|
|
||||||
return factory(Allocation::class)->create([
|
|
||||||
'server_id' => $server->id,
|
|
||||||
'node_id' => $server->node_id,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,179 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pterodactyl\Tests\Integration\Services\Allocations;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use Pterodactyl\Models\Allocation;
|
||||||
|
use Pterodactyl\Tests\Integration\IntegrationTestCase;
|
||||||
|
use Pterodactyl\Services\Allocations\FindAssignableAllocationService;
|
||||||
|
use Pterodactyl\Exceptions\Service\Allocation\AutoAllocationNotEnabledException;
|
||||||
|
use Pterodactyl\Exceptions\Service\Allocation\NoAutoAllocationSpaceAvailableException;
|
||||||
|
|
||||||
|
class FindAssignableAllocationServiceTest extends IntegrationTestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Setup tests.
|
||||||
|
*/
|
||||||
|
public function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
config()->set('pterodactyl.client_features.allocations.enabled', true);
|
||||||
|
config()->set('pterodactyl.client_features.allocations.range_start', 0);
|
||||||
|
config()->set('pterodactyl.client_features.allocations.range_end', 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that an unassigned allocation is prefered rather than creating an entirely new
|
||||||
|
* allocation for the server.
|
||||||
|
*/
|
||||||
|
public function testExistingAllocationIsPreferred()
|
||||||
|
{
|
||||||
|
$server = $this->createServerModel();
|
||||||
|
|
||||||
|
$created = factory(Allocation::class)->create([
|
||||||
|
'node_id' => $server->node_id,
|
||||||
|
'ip' => $server->allocation->ip,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->getService()->handle($server);
|
||||||
|
|
||||||
|
$this->assertSame($created->id, $response->id);
|
||||||
|
$this->assertSame($server->allocation->ip, $response->ip);
|
||||||
|
$this->assertSame($server->node_id, $response->node_id);
|
||||||
|
$this->assertSame($server->id, $response->server_id);
|
||||||
|
$this->assertNotSame($server->allocation_id, $response->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that a new allocation is created if there is not a free one available.
|
||||||
|
*/
|
||||||
|
public function testNewAllocationIsCreatedIfOneIsNotFound()
|
||||||
|
{
|
||||||
|
$server = $this->createServerModel();
|
||||||
|
config()->set('pterodactyl.client_features.allocations.range_start', 5000);
|
||||||
|
config()->set('pterodactyl.client_features.allocations.range_end', 5005);
|
||||||
|
|
||||||
|
$response = $this->getService()->handle($server);
|
||||||
|
$this->assertSame($server->id, $response->server_id);
|
||||||
|
$this->assertSame($server->allocation->ip, $response->ip);
|
||||||
|
$this->assertSame($server->node_id, $response->node_id);
|
||||||
|
$this->assertNotSame($server->allocation_id, $response->id);
|
||||||
|
$this->assertTrue($response->port >= 5000 && $response->port <= 5005);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that a currently assigned port is never assigned to a server.
|
||||||
|
*/
|
||||||
|
public function testOnlyPortNotInUseIsCreated()
|
||||||
|
{
|
||||||
|
$server = $this->createServerModel();
|
||||||
|
$server2 = $this->createServerModel(['node_id' => $server->node_id]);
|
||||||
|
|
||||||
|
config()->set('pterodactyl.client_features.allocations.range_start', 5000);
|
||||||
|
config()->set('pterodactyl.client_features.allocations.range_end', 5001);
|
||||||
|
|
||||||
|
factory(Allocation::class)->create([
|
||||||
|
'server_id' => $server2->id,
|
||||||
|
'node_id' => $server->node_id,
|
||||||
|
'ip' => $server->allocation->ip,
|
||||||
|
'port' => 5000,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->getService()->handle($server);
|
||||||
|
$this->assertSame(5001, $response->port);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testExceptionIsThrownIfNoMoreAllocationsCanBeCreatedInRange()
|
||||||
|
{
|
||||||
|
$server = $this->createServerModel();
|
||||||
|
$server2 = $this->createServerModel(['node_id' => $server->node_id]);
|
||||||
|
config()->set('pterodactyl.client_features.allocations.range_start', 5000);
|
||||||
|
config()->set('pterodactyl.client_features.allocations.range_end', 5005);
|
||||||
|
|
||||||
|
for ($i = 5000; $i <= 5005; $i++) {
|
||||||
|
factory(Allocation::class)->create([
|
||||||
|
'ip' => $server->allocation->ip,
|
||||||
|
'port' => $i,
|
||||||
|
'node_id' => $server->node_id,
|
||||||
|
'server_id' => $server2->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->expectException(NoAutoAllocationSpaceAvailableException::class);
|
||||||
|
$this->expectExceptionMessage('Cannot assign additional allocation: no more space available on node.');
|
||||||
|
|
||||||
|
$this->getService()->handle($server);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that we only auto-allocate from the current server's IP address space, and not a random
|
||||||
|
* IP address available on that node.
|
||||||
|
*/
|
||||||
|
public function testExceptionIsThrownIfOnlyFreePortIsOnADifferentIp()
|
||||||
|
{
|
||||||
|
$server = $this->createServerModel();
|
||||||
|
|
||||||
|
factory(Allocation::class)->times(5)->create(['node_id' => $server->node_id]);
|
||||||
|
|
||||||
|
$this->expectException(NoAutoAllocationSpaceAvailableException::class);
|
||||||
|
$this->expectExceptionMessage('Cannot assign additional allocation: no more space available on node.');
|
||||||
|
|
||||||
|
$this->getService()->handle($server);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testExceptionIsThrownIfStartOrEndRangeIsNotDefined()
|
||||||
|
{
|
||||||
|
$server = $this->createServerModel();
|
||||||
|
|
||||||
|
$this->expectException(NoAutoAllocationSpaceAvailableException::class);
|
||||||
|
$this->expectExceptionMessage('Cannot assign additional allocation: no more space available on node.');
|
||||||
|
|
||||||
|
$this->getService()->handle($server);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testExceptionIsThrownIfStartOrEndRangeIsNotNumeric()
|
||||||
|
{
|
||||||
|
$server = $this->createServerModel();
|
||||||
|
config()->set('pterodactyl.client_features.allocations.range_start', 'hodor');
|
||||||
|
config()->set('pterodactyl.client_features.allocations.range_end', 10);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->getService()->handle($server);
|
||||||
|
$this->assertTrue(false, 'This assertion should not be reached.');
|
||||||
|
} catch (Exception $exception) {
|
||||||
|
$this->assertInstanceOf(InvalidArgumentException::class, $exception);
|
||||||
|
$this->assertSame('Expected an integerish value. Got: string', $exception->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
config()->set('pterodactyl.client_features.allocations.range_start', 10);
|
||||||
|
config()->set('pterodactyl.client_features.allocations.range_end', 'hodor');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->getService()->handle($server);
|
||||||
|
$this->assertTrue(false, 'This assertion should not be reached.');
|
||||||
|
} catch (Exception $exception) {
|
||||||
|
$this->assertInstanceOf(InvalidArgumentException::class, $exception);
|
||||||
|
$this->assertSame('Expected an integerish value. Got: string', $exception->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testExceptionIsThrownIfFeatureIsNotEnabled()
|
||||||
|
{
|
||||||
|
config()->set('pterodactyl.client_features.allocations.enabled', false);
|
||||||
|
$server = $this->createServerModel();
|
||||||
|
|
||||||
|
$this->expectException(AutoAllocationNotEnabledException::class);
|
||||||
|
|
||||||
|
$this->getService()->handle($server);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return \Pterodactyl\Services\Allocations\FindAssignableAllocationService
|
||||||
|
*/
|
||||||
|
private function getService()
|
||||||
|
{
|
||||||
|
return $this->app->make(FindAssignableAllocationService::class);
|
||||||
|
}
|
||||||
|
}
|
|
@ -71,6 +71,8 @@ trait CreatesTestModels
|
||||||
|
|
||||||
$server = $factory->of(Server::class)->create($attributes);
|
$server = $factory->of(Server::class)->create($attributes);
|
||||||
|
|
||||||
|
Allocation::query()->where('id', $server->allocation_id)->update(['server_id' => $server->id]);
|
||||||
|
|
||||||
return Server::with([
|
return Server::with([
|
||||||
'location', 'user', 'node', 'allocation', 'nest', 'egg',
|
'location', 'user', 'node', 'allocation', 'nest', 'egg',
|
||||||
])->findOrFail($server->id);
|
])->findOrFail($server->id);
|
||||||
|
|
Loading…
Reference in a new issue