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\Transformers\Api\Client\AllocationTransformer;
|
||||
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\NewAllocationRequest;
|
||||
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\SetPrimaryAllocationRequest;
|
||||
|
@ -27,20 +29,28 @@ class NetworkAllocationController extends ClientApiController
|
|||
*/
|
||||
private $serverRepository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Allocations\FindAssignableAllocationService
|
||||
*/
|
||||
private $assignableAllocationService;
|
||||
|
||||
/**
|
||||
* NetworkController constructor.
|
||||
*
|
||||
* @param \Pterodactyl\Repositories\Eloquent\AllocationRepository $repository
|
||||
* @param \Pterodactyl\Repositories\Eloquent\ServerRepository $serverRepository
|
||||
* @param \Pterodactyl\Services\Allocations\FindAssignableAllocationService $assignableAllocationService
|
||||
*/
|
||||
public function __construct(
|
||||
AllocationRepository $repository,
|
||||
ServerRepository $serverRepository
|
||||
ServerRepository $serverRepository,
|
||||
FindAssignableAllocationService $assignableAllocationService
|
||||
) {
|
||||
parent::__construct();
|
||||
|
||||
$this->repository = $repository;
|
||||
$this->serverRepository = $serverRepository;
|
||||
$this->assignableAllocationService = $assignableAllocationService;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -100,6 +110,31 @@ class NetworkAllocationController extends ClientApiController
|
|||
->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.
|
||||
*
|
||||
|
@ -109,18 +144,19 @@ class NetworkAllocationController extends ClientApiController
|
|||
* @return \Illuminate\Http\JsonResponse
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\DisplayException
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
public function delete(DeleteAllocationRequest $request, Server $server, Allocation $allocation)
|
||||
{
|
||||
if ($allocation->id === $server->allocation_id) {
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -19,6 +19,11 @@ class AdvancedSettingsFormRequest extends AdminFormRequest
|
|||
'recaptcha:website_key' => 'required|string|max:191',
|
||||
'pterodactyl:guzzle: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',
|
||||
'pterodactyl:guzzle:timeout' => 'HTTP Request 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:frequency',
|
||||
'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.
|
||||
*/
|
||||
|
||||
'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.
|
||||
'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>
|
||||
<p css={tw`text-sm`}>{children}</p>
|
||||
<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
|
||||
</Button>
|
||||
<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 tw from 'twin.macro';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
@ -15,20 +15,26 @@ import setServerAllocationNotes from '@/api/server/network/setServerAllocationNo
|
|||
import useFlash from '@/plugins/useFlash';
|
||||
import { ServerContext } from '@/state/server';
|
||||
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 Label = styled.label`${tw`uppercase text-xs mt-1 text-neutral-400 block px-1 select-none transition-colors duration-150`}`;
|
||||
|
||||
interface Props {
|
||||
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 { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||
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) => {
|
||||
setLoading(true);
|
||||
|
@ -40,14 +46,26 @@ const AllocationRow = ({ allocation, onSetPrimary, onNotesChanged }: Props) => {
|
|||
.then(() => setLoading(false));
|
||||
}, 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 (
|
||||
<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`pl-4 pr-6 text-neutral-400`}>
|
||||
<FontAwesomeIcon icon={faNetworkWired} />
|
||||
<FontAwesomeIcon icon={faNetworkWired}/>
|
||||
</div>
|
||||
<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>}
|
||||
<Label>{allocation.alias ? 'Hostname' : 'IP Address'}</Label>
|
||||
</div>
|
||||
|
@ -66,20 +84,25 @@ const AllocationRow = ({ allocation, onSetPrimary, onNotesChanged }: Props) => {
|
|||
/>
|
||||
</InputSpinner>
|
||||
</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 ?
|
||||
<span css={tw`bg-green-500 py-1 px-2 rounded text-green-50 text-xs`}>Primary</span>
|
||||
:
|
||||
<Can action={'allocations.update'}>
|
||||
<Button
|
||||
isSecondary
|
||||
size={'xsmall'}
|
||||
color={'primary'}
|
||||
onClick={() => onSetPrimary(allocation.id)}
|
||||
>
|
||||
Make Primary
|
||||
</Button>
|
||||
</Can>
|
||||
<>
|
||||
<Can action={'allocations.delete'}>
|
||||
<DeleteAllocationButton allocation={allocation.id}/>
|
||||
</Can>
|
||||
<Can action={'allocations.update'}>
|
||||
<Button
|
||||
isSecondary
|
||||
size={'xsmall'}
|
||||
color={'primary'}
|
||||
onClick={setPrimaryAllocation}
|
||||
>
|
||||
Make Primary
|
||||
</Button>
|
||||
</Can>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</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 useSWR from 'swr';
|
||||
import getServerAllocations from '@/api/server/network/getServerAllocations';
|
||||
import { Allocation } from '@/api/server/getServer';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
import useFlash from '@/plugins/useFlash';
|
||||
import ServerContentBlock from '@/components/elements/ServerContentBlock';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import { useDeepMemoize } from '@/plugins/useDeepMemoize';
|
||||
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 [ loading, setLoading ] = useState(false);
|
||||
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 { data, error, mutate } = useSWR<Allocation[]>(uuid, key => getServerAllocations(key), {
|
||||
initialData: allocations,
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
const { data, error, mutate } = getServerAllocations();
|
||||
|
||||
useEffect(() => {
|
||||
mutate(allocations, false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
|
@ -26,36 +33,45 @@ const NetworkContainer = () => {
|
|||
}
|
||||
}, [ error ]);
|
||||
|
||||
const setPrimaryAllocation = useCallback((id: number) => {
|
||||
const onCreateAllocation = () => {
|
||||
clearFlashes('server:network');
|
||||
|
||||
const initial = data;
|
||||
mutate(data?.map(a => a.id === id ? { ...a, isDefault: true } : { ...a, isDefault: false }), false);
|
||||
|
||||
setPrimaryServerAllocation(uuid, id)
|
||||
.catch(error => {
|
||||
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);
|
||||
}, []);
|
||||
setLoading(true);
|
||||
createServerAllocation(uuid)
|
||||
.then(allocation => mutate(data?.concat(allocation), false))
|
||||
.catch(error => clearAndAddHttpError({ key: 'server:network', error }))
|
||||
.then(() => setLoading(false));
|
||||
};
|
||||
|
||||
return (
|
||||
<ServerContentBlock showFlashKey={'server:network'} title={'Network'}>
|
||||
{!data ?
|
||||
<Spinner size={'large'} centered/>
|
||||
:
|
||||
data.map(allocation => (
|
||||
<AllocationRow
|
||||
key={`${allocation.ip}:${allocation.port}`}
|
||||
allocation={allocation}
|
||||
onSetPrimary={setPrimaryAllocation}
|
||||
onNotesChanged={onNotesAdded}
|
||||
/>
|
||||
))
|
||||
<>
|
||||
{
|
||||
data.map(allocation => (
|
||||
<AllocationRow
|
||||
key={`${allocation.ip}:${allocation.port}`}
|
||||
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>
|
||||
);
|
||||
|
|
|
@ -115,7 +115,7 @@
|
|||
<div>
|
||||
<input type="text" name="allocation_limit" class="form-control" value="{{ old('allocation_limit', $server->allocation_limit) }}"/>
|
||||
</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 class="form-group col-xs-6">
|
||||
<label for="backup_limit" class="control-label">Backup Limit</label>
|
||||
|
|
|
@ -82,6 +82,62 @@
|
|||
</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-footer">
|
||||
{{ csrf_field() }}
|
||||
|
|
|
@ -82,6 +82,7 @@ Route::group(['prefix' => '/servers/{server}', 'middleware' => [AuthenticateServ
|
|||
|
||||
Route::group(['prefix' => '/network', 'middleware' => [AllocationBelongsToServer::class]], function () {
|
||||
Route::get('/allocations', 'Servers\NetworkAllocationController@index');
|
||||
Route::post('/allocations', 'Servers\NetworkAllocationController@store');
|
||||
Route::post('/allocations/{allocation}', 'Servers\NetworkAllocationController@update');
|
||||
Route::post('/allocations/{allocation}/primary', 'Servers\NetworkAllocationController@setPrimary');
|
||||
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 Illuminate\Http\Response;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Models\Allocation;
|
||||
use Pterodactyl\Models\Permission;
|
||||
use Pterodactyl\Tests\Integration\Api\Client\ClientApiIntegrationTestCase;
|
||||
|
@ -17,7 +16,6 @@ class NetworkAllocationControllerTest extends ClientApiIntegrationTestCase
|
|||
public function testServerAllocationsAreReturned()
|
||||
{
|
||||
[$user, $server] = $this->generateTestAccount();
|
||||
$allocation = $this->getAllocation($server);
|
||||
|
||||
$response = $this->actingAs($user)->getJson($this->link($server, '/network/allocations'));
|
||||
|
||||
|
@ -25,7 +23,7 @@ class NetworkAllocationControllerTest extends ClientApiIntegrationTestCase
|
|||
$response->assertJsonPath('object', 'list');
|
||||
$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)
|
||||
{
|
||||
[$user, $server] = $this->generateTestAccount($permissions);
|
||||
$allocation = $this->getAllocation($server);
|
||||
$allocation = $server->allocation;
|
||||
|
||||
$this->assertNull($allocation->notes);
|
||||
|
||||
|
@ -92,13 +90,11 @@ class NetworkAllocationControllerTest extends ClientApiIntegrationTestCase
|
|||
$server->owner_id = $user2->id;
|
||||
$server->save();
|
||||
|
||||
$this->actingAs($user)->postJson($this->link($this->getAllocation($server)))
|
||||
->assertNotFound();
|
||||
$this->actingAs($user)->postJson($this->link($server->allocation))->assertNotFound();
|
||||
|
||||
[$user, $server] = $this->generateTestAccount([Permission::ACTION_ALLOCATION_CREATE]);
|
||||
|
||||
$this->actingAs($user)->postJson($this->link($this->getAllocation($server)))
|
||||
->assertForbidden();
|
||||
$this->actingAs($user)->postJson($this->link($server->allocation))->assertForbidden();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -108,8 +104,8 @@ class NetworkAllocationControllerTest extends ClientApiIntegrationTestCase
|
|||
public function testPrimaryAllocationCanBeModified(array $permissions)
|
||||
{
|
||||
[$user, $server] = $this->generateTestAccount($permissions);
|
||||
$allocation = $this->getAllocation($server);
|
||||
$allocation2 = $this->getAllocation($server);
|
||||
$allocation = $server->allocation;
|
||||
$allocation2 = factory(Allocation::class)->create(['node_id' => $server->node_id, 'server_id' => $server->id]);
|
||||
|
||||
$server->allocation_id = $allocation->id;
|
||||
$server->save();
|
||||
|
@ -130,61 +126,12 @@ class NetworkAllocationControllerTest extends ClientApiIntegrationTestCase
|
|||
$server->owner_id = $user2->id;
|
||||
$server->save();
|
||||
|
||||
$this->actingAs($user)->postJson($this->link($this->getAllocation($server), '/primary'))
|
||||
$this->actingAs($user)->postJson($this->link($server->allocation, '/primary'))
|
||||
->assertNotFound();
|
||||
|
||||
[$user, $server] = $this->generateTestAccount([Permission::ACTION_ALLOCATION_CREATE]);
|
||||
|
||||
$this->actingAs($user)->postJson($this->link($this->getAllocation($server), '/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)))
|
||||
$this->actingAs($user)->postJson($this->link($server->allocation, '/primary'))
|
||||
->assertForbidden();
|
||||
}
|
||||
|
||||
|
@ -197,16 +144,4 @@ class NetworkAllocationControllerTest extends ClientApiIntegrationTestCase
|
|||
{
|
||||
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);
|
||||
|
||||
Allocation::query()->where('id', $server->allocation_id)->update(['server_id' => $server->id]);
|
||||
|
||||
return Server::with([
|
||||
'location', 'user', 'node', 'allocation', 'nest', 'egg',
|
||||
])->findOrFail($server->id);
|
||||
|
|
Loading…
Reference in a new issue