This commit is contained in:
Lance Pioch 2024-06-11 16:58:50 -04:00
parent 510ae3c0df
commit 36e2fa8e2b
67 changed files with 144 additions and 2917 deletions

View File

@ -1,9 +0,0 @@
<?php
namespace App\Exceptions\Service\Allocation;
use App\Exceptions\DisplayException;
class ServerUsingAllocationException extends DisplayException
{
}

View File

@ -52,7 +52,7 @@ class CreateEgg extends CreateRecord
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]), ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
Forms\Components\Toggle::make('force_outgoing_ip') Forms\Components\Toggle::make('force_outgoing_ip')
->hintIcon('tabler-question-mark') ->hintIcon('tabler-question-mark')
->hintIconTooltip("Forces all outgoing network traffic to have its Source IP NATed to the IP of the server's primary allocation IP. ->hintIconTooltip("Forces all outgoing network traffic to have its Source IP NATed to the IP of the server's primary endpoint.
Required for certain games to work properly when the Node has multiple public IP addresses. Required for certain games to work properly when the Node has multiple public IP addresses.
Enabling this option will disable internal networking for any servers using this egg, causing them to be unable to internally access other servers on the same node."), Enabling this option will disable internal networking for any servers using this egg, causing them to be unable to internally access other servers on the same node."),
Forms\Components\Hidden::make('script_is_privileged') Forms\Components\Hidden::make('script_is_privileged')

View File

@ -62,7 +62,7 @@ class EditEgg extends EditRecord
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]), ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
Forms\Components\Toggle::make('force_outgoing_ip') Forms\Components\Toggle::make('force_outgoing_ip')
->hintIcon('tabler-question-mark') ->hintIcon('tabler-question-mark')
->hintIconTooltip("Forces all outgoing network traffic to have its Source IP NATed to the IP of the server's primary allocation IP. ->hintIconTooltip("Forces all outgoing network traffic to have its Source IP NATed to the IP of the server's endpoint.
Required for certain games to work properly when the Node has multiple public IP addresses. Required for certain games to work properly when the Node has multiple public IP addresses.
Enabling this option will disable internal networking for any servers using this egg, causing them to be unable to internally access other servers on the same node."), Enabling this option will disable internal networking for any servers using this egg, causing them to be unable to internally access other servers on the same node."),
Forms\Components\Hidden::make('script_is_privileged') Forms\Components\Hidden::make('script_is_privileged')

View File

@ -32,11 +32,6 @@ class ServersRelationManager extends RelationManager
->url(fn (Server $server): string => route('filament.admin.resources.nodes.edit', ['record' => $server->node])), ->url(fn (Server $server): string => route('filament.admin.resources.nodes.edit', ['record' => $server->node])),
Tables\Columns\TextColumn::make('image') Tables\Columns\TextColumn::make('image')
->label('Docker Image'), ->label('Docker Image'),
Tables\Columns\SelectColumn::make('allocation.id')
->label('Primary Allocation')
->options(fn ($state, Server $server) => [$server->allocation->id => $server->allocation->address])
->selectablePlaceholder(false)
->sortable(),
]); ]);
} }
} }

View File

@ -32,11 +32,6 @@ class NodesRelationManager extends RelationManager
->icon('tabler-egg') ->icon('tabler-egg')
->url(fn (Server $server): string => route('filament.admin.resources.eggs.edit', ['record' => $server->user])) ->url(fn (Server $server): string => route('filament.admin.resources.eggs.edit', ['record' => $server->user]))
->sortable(), ->sortable(),
Tables\Columns\SelectColumn::make('allocation.id')
->label('Primary Allocation')
->options(fn ($state, Server $server) => [$server->allocation->id => $server->allocation->address])
->selectablePlaceholder(false)
->sortable(),
Tables\Columns\TextColumn::make('memory')->icon('tabler-device-desktop-analytics'), Tables\Columns\TextColumn::make('memory')->icon('tabler-device-desktop-analytics'),
Tables\Columns\TextColumn::make('cpu')->icon('tabler-cpu'), Tables\Columns\TextColumn::make('cpu')->icon('tabler-cpu'),
Tables\Columns\TextColumn::make('databases_count') Tables\Columns\TextColumn::make('databases_count')

View File

@ -66,11 +66,6 @@ class ServersRelationManager extends RelationManager
->icon('tabler-egg') ->icon('tabler-egg')
->url(fn (Server $server): string => route('filament.admin.resources.eggs.edit', ['record' => $server->egg])) ->url(fn (Server $server): string => route('filament.admin.resources.eggs.edit', ['record' => $server->egg]))
->sortable(), ->sortable(),
Tables\Columns\SelectColumn::make('allocation.id')
->label('Primary Allocation')
->options(fn ($state, Server $server) => [$server->allocation->id => $server->allocation->address])
->selectablePlaceholder(false)
->sortable(),
Tables\Columns\TextColumn::make('image')->hidden(), Tables\Columns\TextColumn::make('image')->hidden(),
Tables\Columns\TextColumn::make('databases_count') Tables\Columns\TextColumn::make('databases_count')
->counts('databases') ->counts('databases')

View File

@ -5,7 +5,6 @@ namespace App\Http\Controllers\Admin\Nodes;
use Illuminate\View\View; use Illuminate\View\View;
use App\Models\Node; use App\Models\Node;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use App\Models\Allocation;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Traits\Controllers\JavascriptInjection; use App\Traits\Controllers\JavascriptInjection;
use App\Services\Helpers\SoftwareVersionService; use App\Services\Helpers\SoftwareVersionService;
@ -56,32 +55,6 @@ class NodeViewController extends Controller
return view('admin.nodes.view.configuration', compact('node')); return view('admin.nodes.view.configuration', compact('node'));
} }
/**
* Return the node allocation management page.
*/
public function allocations(Node $node): View
{
$node->setRelation(
'allocations',
$node->allocations()
->orderByRaw('server_id IS NOT NULL DESC, server_id IS NULL')
->orderByRaw('INET_ATON(ip) ASC')
->orderBy('port')
->with('server:id,name')
->paginate(50)
);
$this->plainInject(['node' => Collection::wrap($node)->only(['id'])]);
return view('admin.nodes.view.allocation', [
'node' => $node,
'allocations' => Allocation::query()->where('node_id', $node->id)
->groupBy('ip')
->orderByRaw('INET_ATON(ip) ASC')
->get(['ip']),
]);
}
/** /**
* Return a listing of servers that exist for this specific node. * Return a listing of servers that exist for this specific node.
*/ */

View File

@ -3,10 +3,7 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use Illuminate\View\View; use Illuminate\View\View;
use Illuminate\Http\Request;
use App\Models\Node; use App\Models\Node;
use Illuminate\Http\Response;
use App\Models\Allocation;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag; use Prologue\Alerts\AlertsMessageBag;
use Illuminate\View\Factory as ViewFactory; use Illuminate\View\Factory as ViewFactory;
@ -15,11 +12,8 @@ use App\Services\Nodes\NodeUpdateService;
use Illuminate\Cache\Repository as CacheRepository; use Illuminate\Cache\Repository as CacheRepository;
use App\Services\Nodes\NodeCreationService; use App\Services\Nodes\NodeCreationService;
use App\Services\Nodes\NodeDeletionService; use App\Services\Nodes\NodeDeletionService;
use App\Services\Allocations\AssignmentService;
use App\Services\Helpers\SoftwareVersionService; use App\Services\Helpers\SoftwareVersionService;
use App\Http\Requests\Admin\Node\NodeFormRequest; use App\Http\Requests\Admin\Node\NodeFormRequest;
use App\Http\Requests\Admin\Node\AllocationFormRequest;
use App\Http\Requests\Admin\Node\AllocationAliasFormRequest;
class NodesController extends Controller class NodesController extends Controller
{ {
@ -28,7 +22,6 @@ class NodesController extends Controller
*/ */
public function __construct( public function __construct(
protected AlertsMessageBag $alert, protected AlertsMessageBag $alert,
protected AssignmentService $assignmentService,
protected CacheRepository $cache, protected CacheRepository $cache,
protected NodeCreationService $creationService, protected NodeCreationService $creationService,
protected NodeDeletionService $deletionService, protected NodeDeletionService $deletionService,
@ -46,19 +39,6 @@ class NodesController extends Controller
return view('admin.nodes.new'); return view('admin.nodes.new');
} }
/**
* Post controller to create a new node on the system.
*
* @throws \App\Exceptions\Model\DataValidationException
*/
public function store(NodeFormRequest $request): RedirectResponse
{
$node = $this->creationService->handle($request->normalize());
$this->alert->info(trans('admin/node.notices.node_created'))->flash();
return redirect()->route('admin.nodes.view.allocation', $node->id);
}
/** /**
* Updates settings for a node. * Updates settings for a node.
* *
@ -73,83 +53,6 @@ class NodesController extends Controller
return redirect()->route('admin.nodes.view.settings', $node->id)->withInput(); return redirect()->route('admin.nodes.view.settings', $node->id)->withInput();
} }
/**
* Removes a single allocation from a node.
*
* @throws \App\Exceptions\Service\Allocation\ServerUsingAllocationException
*/
public function allocationRemoveSingle(int $node, Allocation $allocation): Response
{
$allocation->delete();
return response('', 204);
}
/**
* Removes multiple individual allocations from a node.
*
* @throws \App\Exceptions\Service\Allocation\ServerUsingAllocationException
*/
public function allocationRemoveMultiple(Request $request, int $node): Response
{
$allocations = $request->input('allocations');
foreach ($allocations as $rawAllocation) {
$allocation = new Allocation();
$allocation->id = $rawAllocation['id'];
$this->allocationRemoveSingle($node, $allocation);
}
return response('', 204);
}
/**
* Remove all allocations for a specific IP at once on a node.
*/
public function allocationRemoveBlock(Request $request, int $node): RedirectResponse
{
/** @var Node $node */
$node = Node::query()->findOrFail($node);
$node->allocations()
->where('ip', $request->input('ip'))
->whereNull('server_id')
->delete();
$this->alert->success(trans('admin/node.notices.unallocated_deleted', ['ip' => $request->input('ip')]))
->flash();
return redirect()->route('admin.nodes.view.allocation', $node);
}
/**
* Sets an alias for a specific allocation on a node.
*
* @throws \App\Exceptions\Model\DataValidationException
*/
public function allocationSetAlias(AllocationAliasFormRequest $request): \Symfony\Component\HttpFoundation\Response
{
$allocation = Allocation::query()->findOrFail($request->input('allocation_id'));
$alias = (empty($request->input('alias'))) ? null : $request->input('alias');
$allocation->update(['ip_alias' => $alias]);
return response('', 204);
}
/**
* Creates new allocations on a node.
*
* @throws \App\Exceptions\Service\Allocation\CidrOutOfRangeException
* @throws \App\Exceptions\Service\Allocation\InvalidPortMappingException
* @throws \App\Exceptions\Service\Allocation\PortOutOfRangeException
* @throws \App\Exceptions\Service\Allocation\TooManyPortsInRangeException
*/
public function createAllocation(AllocationFormRequest $request, Node $node): RedirectResponse
{
$this->assignmentService->handle($node, $request->normalize());
$this->alert->success(trans('admin/node.notices.allocations_added'))->flash();
return redirect()->route('admin.nodes.view.allocation', $node->id);
}
/** /**
* Deletes a node from the system. * Deletes a node from the system.
* *

View File

@ -36,11 +36,6 @@ class CreateServerController extends Controller
$eggs = Egg::with('variables')->get(); $eggs = Egg::with('variables')->get();
\JavaScript::put([
'nodeData' => Node::getForServerCreation(),
'eggs' => $eggs->keyBy('id'),
]);
return view('admin.servers.new', [ return view('admin.servers.new', [
'eggs' => $eggs, 'eggs' => $eggs,
'nodes' => Node::all(), 'nodes' => Node::all(),

View File

@ -29,8 +29,6 @@ class ServerTransferController extends Controller
{ {
$validatedData = $request->validate([ $validatedData = $request->validate([
'node_id' => 'required|exists:nodes,id', 'node_id' => 'required|exists:nodes,id',
'allocation_id' => 'required|bail|unique:servers|exists:allocations,id',
'allocation_additional' => 'nullable',
]); ]);
if ($this->transferServerService->handle($server, $validatedData)) { if ($this->transferServerService->handle($server, $validatedData)) {

View File

@ -121,10 +121,6 @@ class ServerViewController extends Controller
$canTransfer = true; $canTransfer = true;
} }
\JavaScript::put([
'nodeData' => Node::getForServerCreation(),
]);
return view('admin.servers.view.manage', [ return view('admin.servers.view.manage', [
'nodes' => Node::all(), 'nodes' => Node::all(),
'server' => $server, 'server' => $server,

View File

@ -1,79 +0,0 @@
<?php
namespace App\Http\Controllers\Api\Application\Nodes;
use App\Models\Node;
use Illuminate\Http\JsonResponse;
use App\Models\Allocation;
use Spatie\QueryBuilder\QueryBuilder;
use Spatie\QueryBuilder\AllowedFilter;
use Illuminate\Database\Eloquent\Builder;
use App\Services\Allocations\AssignmentService;
use App\Transformers\Api\Application\AllocationTransformer;
use App\Http\Controllers\Api\Application\ApplicationApiController;
use App\Http\Requests\Api\Application\Allocations\GetAllocationsRequest;
use App\Http\Requests\Api\Application\Allocations\StoreAllocationRequest;
use App\Http\Requests\Api\Application\Allocations\DeleteAllocationRequest;
class AllocationController extends ApplicationApiController
{
/**
* AllocationController constructor.
*/
public function __construct(
private AssignmentService $assignmentService,
) {
parent::__construct();
}
/**
* Return all the allocations that exist for a given node.
*/
public function index(GetAllocationsRequest $request, Node $node): array
{
$allocations = QueryBuilder::for($node->allocations())
->allowedFilters([
AllowedFilter::exact('ip'),
AllowedFilter::exact('port'),
'ip_alias',
AllowedFilter::callback('server_id', function (Builder $builder, $value) {
if (empty($value) || is_bool($value) || !ctype_digit((string) $value)) {
return $builder->whereNull('server_id');
}
return $builder->where('server_id', $value);
}),
])
->paginate($request->query('per_page') ?? 50);
return $this->fractal->collection($allocations)
->transformWith($this->getTransformer(AllocationTransformer::class))
->toArray();
}
/**
* Store new allocations for a given node.
*
* @throws \App\Exceptions\DisplayException
* @throws \App\Exceptions\Service\Allocation\CidrOutOfRangeException
* @throws \App\Exceptions\Service\Allocation\InvalidPortMappingException
* @throws \App\Exceptions\Service\Allocation\PortOutOfRangeException
* @throws \App\Exceptions\Service\Allocation\TooManyPortsInRangeException
*/
public function store(StoreAllocationRequest $request, Node $node): JsonResponse
{
$this->assignmentService->handle($node, $request->validated());
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
}
/**
* Delete a specific allocation from the Panel.
*/
public function delete(DeleteAllocationRequest $request, Node $node, Allocation $allocation): JsonResponse
{
$allocation->delete();
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
}
}

View File

@ -69,8 +69,6 @@ class ServerManagementController extends ApplicationApiController
{ {
$validatedData = $request->validate([ $validatedData = $request->validate([
'node_id' => 'required|exists:nodes,id', 'node_id' => 'required|exists:nodes,id',
'allocation_id' => 'required|bail|unique:servers|exists:allocations,id',
'allocation_additional' => 'nullable',
]); ]);
if ($this->transferServerService->handle($server, $validatedData)) { if ($this->transferServerService->handle($server, $validatedData)) {

View File

@ -1,137 +0,0 @@
<?php
namespace App\Http\Controllers\Api\Client\Servers;
use App\Models\Server;
use Illuminate\Http\JsonResponse;
use App\Facades\Activity;
use App\Models\Allocation;
use App\Exceptions\DisplayException;
use App\Transformers\Api\Client\AllocationTransformer;
use App\Http\Controllers\Api\Client\ClientApiController;
use App\Services\Allocations\FindAssignableAllocationService;
use App\Http\Requests\Api\Client\Servers\Network\GetNetworkRequest;
use App\Http\Requests\Api\Client\Servers\Network\NewAllocationRequest;
use App\Http\Requests\Api\Client\Servers\Network\DeleteAllocationRequest;
use App\Http\Requests\Api\Client\Servers\Network\UpdateAllocationRequest;
use App\Http\Requests\Api\Client\Servers\Network\SetPrimaryAllocationRequest;
class NetworkAllocationController extends ClientApiController
{
/**
* NetworkAllocationController constructor.
*/
public function __construct(
private FindAssignableAllocationService $assignableAllocationService,
) {
parent::__construct();
}
/**
* Lists all the allocations available to a server and whether
* they are currently assigned as the primary for this server.
*/
public function index(GetNetworkRequest $request, Server $server): array
{
return $this->fractal->collection($server->allocations)
->transformWith($this->getTransformer(AllocationTransformer::class))
->toArray();
}
/**
* Set the primary allocation for a server.
*
* @throws \App\Exceptions\Model\DataValidationException
*/
public function update(UpdateAllocationRequest $request, Server $server, Allocation $allocation): array
{
$original = $allocation->notes;
$allocation->forceFill(['notes' => $request->input('notes')])->save();
if ($original !== $allocation->notes) {
Activity::event('server:allocation.notes')
->subject($allocation)
->property(['allocation' => $allocation->toString(), 'old' => $original, 'new' => $allocation->notes])
->log();
}
return $this->fractal->item($allocation)
->transformWith($this->getTransformer(AllocationTransformer::class))
->toArray();
}
/**
* Set the primary allocation for a server.
*
* @throws \App\Exceptions\Model\DataValidationException
*/
public function setPrimary(SetPrimaryAllocationRequest $request, Server $server, Allocation $allocation): array
{
$server->allocation()->associate($allocation);
$server->save();
Activity::event('server:allocation.primary')
->subject($allocation)
->property('allocation', $allocation->toString())
->log();
return $this->fractal->item($allocation)
->transformWith($this->getTransformer(AllocationTransformer::class))
->toArray();
}
/**
* Set the notes for the allocation for a server.
*s.
*
* @throws \App\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);
Activity::event('server:allocation.create')
->subject($allocation)
->property('allocation', $allocation->toString())
->log();
return $this->fractal->item($allocation)
->transformWith($this->getTransformer(AllocationTransformer::class))
->toArray();
}
/**
* Delete an allocation from a server.
*
* @throws \App\Exceptions\DisplayException
*/
public function delete(DeleteAllocationRequest $request, Server $server, Allocation $allocation): JsonResponse
{
// Don't allow the deletion of allocations if the server does not have an
// allocation limit set.
if (empty($server->allocation_limit)) {
throw new DisplayException('You cannot delete allocations for this server: no allocation limit is set.');
}
if ($allocation->id === $server->allocation_id) {
throw new DisplayException('You cannot delete the primary allocation for this server.');
}
Allocation::query()->where('id', $allocation->id)->update([
'notes' => null,
'server_id' => null,
]);
Activity::event('server:allocation.delete')
->subject($allocation)
->property('allocation', $allocation->toString())
->log();
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
}
}

View File

@ -48,7 +48,7 @@ class ServerDetailsController extends Controller
// Avoid run-away N+1 SQL queries by preloading the relationships that are used // Avoid run-away N+1 SQL queries by preloading the relationships that are used
// within each of the services called below. // within each of the services called below.
$servers = Server::query()->with('allocations', 'egg', 'mounts', 'variables') $servers = Server::query()->with('egg', 'mounts', 'variables')
->where('node_id', $node->id) ->where('node_id', $node->id)
// If you don't cast this to a string you'll end up with a stringified per_page returned in // If you don't cast this to a string you'll end up with a stringified per_page returned in
// the metadata, and then daemon will panic crash as a result. // the metadata, and then daemon will panic crash as a result.

View File

@ -6,7 +6,6 @@ use App\Models\Server;
use App\Repositories\Daemon\DaemonServerRepository; use App\Repositories\Daemon\DaemonServerRepository;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use App\Models\Allocation;
use App\Models\ServerTransfer; use App\Models\ServerTransfer;
use Illuminate\Database\ConnectionInterface; use Illuminate\Database\ConnectionInterface;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
@ -53,13 +52,7 @@ class ServerTransferController extends Controller
/** @var \App\Models\Server $server */ /** @var \App\Models\Server $server */
$server = $this->connection->transaction(function () use ($server, $transfer) { $server = $this->connection->transaction(function () use ($server, $transfer) {
$allocations = array_merge([$transfer->old_allocation], $transfer->old_additional_allocations);
// Remove the old allocations for the server and re-assign the server to the new
// primary allocation and node.
Allocation::query()->whereIn('id', $allocations)->update(['server_id' => null]);
$server->update([ $server->update([
'allocation_id' => $transfer->new_allocation,
'node_id' => $transfer->new_node, 'node_id' => $transfer->new_node,
]); ]);
@ -93,9 +86,6 @@ class ServerTransferController extends Controller
{ {
$this->connection->transaction(function () use (&$transfer) { $this->connection->transaction(function () use (&$transfer) {
$transfer->forceFill(['successful' => false])->saveOrFail(); $transfer->forceFill(['successful' => false])->saveOrFail();
$allocations = array_merge([$transfer->new_allocation], $transfer->new_additional_allocations);
Allocation::query()->whereIn('id', $allocations)->update(['server_id' => null]);
}); });
return new JsonResponse([], Response::HTTP_NO_CONTENT); return new JsonResponse([], Response::HTTP_NO_CONTENT);

View File

@ -10,7 +10,6 @@ use App\Models\Server;
use App\Models\Subuser; use App\Models\Subuser;
use App\Models\Database; use App\Models\Database;
use App\Models\Schedule; use App\Models\Schedule;
use App\Models\Allocation;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@ -48,7 +47,6 @@ class ResourceBelongsToServer
switch (get_class($model)) { switch (get_class($model)) {
// All of these models use "server_id" as the field key for the server // All of these models use "server_id" as the field key for the server
// they are assigned to, so the logic is identical for them all. // they are assigned to, so the logic is identical for them all.
case Allocation::class:
case Backup::class: case Backup::class:
case Database::class: case Database::class:
case Schedule::class: case Schedule::class:

View File

@ -1,16 +0,0 @@
<?php
namespace App\Http\Requests\Admin\Node;
use App\Http\Requests\Admin\AdminFormRequest;
class AllocationAliasFormRequest extends AdminFormRequest
{
public function rules(): array
{
return [
'alias' => 'present|nullable|string',
'allocation_id' => 'required|numeric|exists:allocations,id',
];
}
}

View File

@ -1,17 +0,0 @@
<?php
namespace App\Http\Requests\Admin\Node;
use App\Http\Requests\Admin\AdminFormRequest;
class AllocationFormRequest extends AdminFormRequest
{
public function rules(): array
{
return [
'allocation_ip' => 'required|string',
'allocation_alias' => 'sometimes|nullable|string|max:191',
'allocation_ports' => 'required|array',
];
}
}

View File

@ -3,7 +3,6 @@
namespace App\Http\Requests\Admin; namespace App\Http\Requests\Admin;
use App\Models\Server; use App\Models\Server;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Validator; use Illuminate\Validation\Validator;
class ServerFormRequest extends AdminFormRequest class ServerFormRequest extends AdminFormRequest
@ -25,34 +24,10 @@ class ServerFormRequest extends AdminFormRequest
*/ */
public function withValidator(Validator $validator): void public function withValidator(Validator $validator): void
{ {
$validator->after(function ($validator) { $validator->after(function (Validator $validator) {
$validator->sometimes('node_id', 'required|numeric|bail|exists:nodes,id', function ($input) { $validator->sometimes('node_id', 'required|numeric|bail|exists:nodes,id', function ($input) {
return !$input->auto_deploy; return !$input->auto_deploy;
}); });
$validator->sometimes('allocation_id', [
'required',
'numeric',
'bail',
Rule::exists('allocations', 'id')->where(function ($query) {
$query->where('node_id', $this->input('node_id'));
$query->whereNull('server_id');
}),
], function ($input) {
return !$input->auto_deploy;
});
$validator->sometimes('allocation_additional.*', [
'sometimes',
'required',
'numeric',
Rule::exists('allocations', 'id')->where(function ($query) {
$query->where('node_id', $this->input('node_id'));
$query->whereNull('server_id');
}),
], function ($input) {
return !$input->auto_deploy;
});
}); });
} }
} }

View File

@ -1,13 +0,0 @@
<?php
namespace App\Http\Requests\Api\Application\Allocations;
use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest;
class DeleteAllocationRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_ALLOCATIONS;
protected int $permission = AdminAcl::WRITE;
}

View File

@ -1,13 +0,0 @@
<?php
namespace App\Http\Requests\Api\Application\Allocations;
use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest;
class GetAllocationsRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_ALLOCATIONS;
protected int $permission = AdminAcl::READ;
}

View File

@ -1,34 +0,0 @@
<?php
namespace App\Http\Requests\Api\Application\Allocations;
use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest;
class StoreAllocationRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_ALLOCATIONS;
protected int $permission = AdminAcl::WRITE;
public function rules(): array
{
return [
'ip' => 'required|string',
'alias' => 'sometimes|nullable|string|max:191',
'ports' => 'required|array',
'ports.*' => 'string',
];
}
public function validated($key = null, $default = null): array
{
$data = parent::validated();
return [
'allocation_ip' => $data['ip'],
'allocation_ports' => $data['ports'],
'allocation_alias' => $data['alias'] ?? null,
];
}
}

View File

@ -3,7 +3,6 @@
namespace App\Http\Requests\Api\Application\Servers; namespace App\Http\Requests\Api\Application\Servers;
use App\Models\Server; use App\Models\Server;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Validator; use Illuminate\Validation\Validator;
use App\Services\Acl\Api\AdminAcl; use App\Services\Acl\Api\AdminAcl;
use App\Models\Objects\DeploymentObject; use App\Models\Objects\DeploymentObject;
@ -49,10 +48,6 @@ class StoreServerRequest extends ApplicationApiRequest
'feature_limits.allocations' => $rules['allocation_limit'], 'feature_limits.allocations' => $rules['allocation_limit'],
'feature_limits.backups' => $rules['backup_limit'], 'feature_limits.backups' => $rules['backup_limit'],
// Placeholders for rules added in withValidator() function.
'allocation.default' => '',
'allocation.additional.*' => '',
// Automatic deployment rules // Automatic deployment rules
'deploy' => 'sometimes|required|array', 'deploy' => 'sometimes|required|array',
'deploy.locations' => 'array', 'deploy.locations' => 'array',
@ -87,8 +82,7 @@ class StoreServerRequest extends ApplicationApiRequest
'cpu' => array_get($data, 'limits.cpu'), 'cpu' => array_get($data, 'limits.cpu'),
'threads' => array_get($data, 'limits.threads'), 'threads' => array_get($data, 'limits.threads'),
'skip_scripts' => array_get($data, 'skip_scripts', false), 'skip_scripts' => array_get($data, 'skip_scripts', false),
'allocation_id' => array_get($data, 'allocation.default'), 'ports' => array_get($data, 'ports'),
'allocation_additional' => array_get($data, 'allocation.additional'),
'start_on_completion' => array_get($data, 'start_on_completion', false), 'start_on_completion' => array_get($data, 'start_on_completion', false),
'database_limit' => array_get($data, 'feature_limits.databases'), 'database_limit' => array_get($data, 'feature_limits.databases'),
'allocation_limit' => array_get($data, 'feature_limits.allocations'), 'allocation_limit' => array_get($data, 'feature_limits.allocations'),
@ -104,24 +98,6 @@ class StoreServerRequest extends ApplicationApiRequest
*/ */
public function withValidator(Validator $validator): void public function withValidator(Validator $validator): void
{ {
$validator->sometimes('allocation.default', [
'required', 'integer', 'bail',
Rule::exists('allocations', 'id')->where(function ($query) {
$query->whereNull('server_id');
}),
], function ($input) {
return !$input->deploy;
});
$validator->sometimes('allocation.additional.*', [
'integer',
Rule::exists('allocations', 'id')->where(function ($query) {
$query->whereNull('server_id');
}),
], function ($input) {
return !$input->deploy;
});
/** @deprecated use tags instead */ /** @deprecated use tags instead */
$validator->sometimes('deploy.locations', 'present', function ($input) { $validator->sometimes('deploy.locations', 'present', function ($input) {
return $input->deploy; return $input->deploy;

View File

@ -15,7 +15,6 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest
$rules = Server::getRulesForUpdate($this->parameter('server', Server::class)); $rules = Server::getRulesForUpdate($this->parameter('server', Server::class));
return [ return [
'allocation' => $rules['allocation_id'],
'oom_killer' => $rules['oom_killer'], 'oom_killer' => $rules['oom_killer'],
'limits' => 'sometimes|array', 'limits' => 'sometimes|array',
@ -54,7 +53,6 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest
{ {
$data = parent::validated(); $data = parent::validated();
$data['allocation_id'] = $data['allocation'];
$data['database_limit'] = $data['feature_limits']['databases'] ?? null; $data['database_limit'] = $data['feature_limits']['databases'] ?? null;
$data['allocation_limit'] = $data['feature_limits']['allocations'] ?? null; $data['allocation_limit'] = $data['feature_limits']['allocations'] ?? null;
$data['backup_limit'] = $data['feature_limits']['backups'] ?? null; $data['backup_limit'] = $data['feature_limits']['backups'] ?? null;

View File

@ -1,14 +0,0 @@
<?php
namespace App\Http\Requests\Api\Client\Servers\Network;
use App\Models\Permission;
use App\Http\Requests\Api\Client\ClientApiRequest;
class DeleteAllocationRequest extends ClientApiRequest
{
public function permission(): string
{
return Permission::ACTION_ALLOCATION_DELETE;
}
}

View File

@ -1,18 +0,0 @@
<?php
namespace App\Http\Requests\Api\Client\Servers\Network;
use App\Models\Permission;
use App\Http\Requests\Api\Client\ClientApiRequest;
class GetNetworkRequest extends ClientApiRequest
{
/**
* Check that the user has permission to view the allocations for
* this server.
*/
public function permission(): string
{
return Permission::ACTION_ALLOCATION_READ;
}
}

View File

@ -1,14 +0,0 @@
<?php
namespace App\Http\Requests\Api\Client\Servers\Network;
use App\Models\Permission;
use App\Http\Requests\Api\Client\ClientApiRequest;
class NewAllocationRequest extends ClientApiRequest
{
public function permission(): string
{
return Permission::ACTION_ALLOCATION_CREATE;
}
}

View File

@ -1,11 +0,0 @@
<?php
namespace App\Http\Requests\Api\Client\Servers\Network;
class SetPrimaryAllocationRequest extends UpdateAllocationRequest
{
public function rules(): array
{
return [];
}
}

View File

@ -1,24 +0,0 @@
<?php
namespace App\Http\Requests\Api\Client\Servers\Network;
use App\Models\Allocation;
use App\Models\Permission;
use App\Http\Requests\Api\Client\ClientApiRequest;
class UpdateAllocationRequest extends ClientApiRequest
{
public function permission(): string
{
return Permission::ACTION_ALLOCATION_UPDATE;
}
public function rules(): array
{
$rules = Allocation::getRules();
return [
'notes' => array_merge($rules['notes'], ['present']),
];
}
}

View File

@ -1,100 +0,0 @@
<?php
namespace App\Models;
use App\Exceptions\Service\Allocation\ServerUsingAllocationException;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Allocation extends Model
{
/**
* The resource name for this model when it is transformed into an
* API representation using fractal.
*/
public const RESOURCE_NAME = 'allocation';
/**
* The table associated with the model.
*/
protected $table = 'allocations';
/**
* Fields that are not mass assignable.
*/
protected $guarded = ['id', 'created_at', 'updated_at'];
public static array $validationRules = [
'node_id' => 'required|exists:nodes,id',
'ip' => 'required|ip',
'port' => 'required|numeric|between:1024,65535',
'ip_alias' => 'nullable|string',
'server_id' => 'nullable|exists:servers,id',
'notes' => 'nullable|string|max:256',
];
protected static function booted(): void
{
static::deleting(function (self $allocation) {
throw_if($allocation->server_id, new ServerUsingAllocationException(trans('exceptions.allocations.server_using')));
});
}
protected function casts(): array
{
return [
'node_id' => 'integer',
'port' => 'integer',
'server_id' => 'integer',
];
}
public function getRouteKeyName(): string
{
return $this->getKeyName();
}
/**
* Accessor to automatically provide the IP alias if defined.
*/
public function getAliasAttribute(?string $value): string
{
return (is_null($this->ip_alias)) ? $this->ip : $this->ip_alias;
}
/**
* Accessor to quickly determine if this allocation has an alias.
*/
public function getHasAliasAttribute(?string $value): bool
{
return !is_null($this->ip_alias);
}
public function address(): Attribute
{
return Attribute::make(
get: fn () => "$this->ip:$this->port",
);
}
public function toString(): string
{
return $this->address;
}
/**
* Gets information for the server associated with this allocation.
*/
public function server(): BelongsTo
{
return $this->belongsTo(Server::class);
}
/**
* Return the Node model associated with this allocation.
*/
public function node(): BelongsTo
{
return $this->belongsTo(Node::class);
}
}

View File

@ -2,7 +2,6 @@
namespace App\Models\Filters; namespace App\Models\Filters;
use Illuminate\Support\Str;
use Spatie\QueryBuilder\Filters\Filter; use Spatie\QueryBuilder\Filters\Filter;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
@ -32,26 +31,6 @@ class MultiFieldServerFilter implements Filter
// Only select the server values, otherwise you'll end up merging the allocation and // Only select the server values, otherwise you'll end up merging the allocation and
// server objects together, resulting in incorrect behavior and returned values. // server objects together, resulting in incorrect behavior and returned values.
->select('servers.*') ->select('servers.*')
->join('allocations', 'allocations.server_id', '=', 'servers.id')
->where(function (Builder $builder) use ($value) {
$parts = explode(':', $value);
$builder->when(
!Str::startsWith($value, ':'),
// When the string does not start with a ":" it means we're looking for an IP or IP:Port
// combo, so use a query to handle that.
function (Builder $builder) use ($parts) {
$builder->orWhere('allocations.ip', $parts[0]);
if (!is_null($parts[1] ?? null)) {
$builder->where('allocations.port', 'LIKE', "{$parts[1]}%");
}
},
// Otherwise, just try to search for that specific port in the allocations.
function (Builder $builder) use ($value) {
$builder->orWhere('allocations.port', 'LIKE', substr($value, 1) . '%');
}
);
})
->groupBy('servers.id'); ->groupBy('servers.id');
return; return;

View File

@ -201,14 +201,6 @@ class Node extends Model
return $this->hasMany(Server::class); return $this->hasMany(Server::class);
} }
/**
* Gets the allocations associated with a node.
*/
public function allocations(): HasMany
{
return $this->hasMany(Allocation::class);
}
/** /**
* Returns a boolean if the node is viable for an additional server to be placed on it. * Returns a boolean if the node is viable for an additional server to be placed on it.
*/ */
@ -238,28 +230,6 @@ class Node extends Model
return true; return true;
} }
public static function getForServerCreation()
{
return self::with('allocations')->get()->map(function (Node $item) {
$filtered = $item->getRelation('allocations')->where('server_id', null)->map(function ($map) {
return collect($map)->only(['id', 'ip', 'port']);
});
$ports = $filtered->map(function ($map) {
return [
'id' => $map['id'],
'text' => sprintf('%s:%s', $map['ip'], $map['port']),
];
})->values();
return [
'id' => $item->id,
'text' => $item->name,
'allocations' => $ports,
];
})->values();
}
public function systemInformation(): array public function systemInformation(): array
{ {
return once(function () { return once(function () {

View File

@ -0,0 +1,39 @@
<?php
namespace App\Models\Objects;
use InvalidArgumentException;
class Endpoint
{
public const CIDR_MAX_BITS = 27;
public const CIDR_MIN_BITS = 32;
public const PORT_FLOOR = 1024;
public const PORT_CEIL = 65535;
public const PORT_RANGE_LIMIT = 1000;
public const PORT_RANGE_REGEX = '/^(\d{4,5})-(\d{4,5})$/';
public int $port;
public string $ip;
public function __construct(string $address = null, int $port = null) {
if ($address === null) {
$address = '0.0.0.0';
}
$ip = $address;
if (str_contains($address, ':')) {
[$ip, $port] = explode(':', $address);
throw_unless(is_numeric($port), new InvalidArgumentException("Port ($port) must be a number"));
$port = (int) $port;
}
throw_unless(is_int($port), new InvalidArgumentException("Port ($port) must be an integer"));
throw_unless(filter_var($ip, FILTER_VALIDATE_IP) !== false, new InvalidArgumentException("$ip is an invalid IP address"));
$this->ip = $ip;
$this->port = $port;
}
}

View File

@ -41,11 +41,6 @@ class Server extends Model
'installed_at' => null, 'installed_at' => null,
]; ];
/**
* The default relationships to load for all server models.
*/
protected $with = ['allocation'];
/** /**
* Fields that are not mass assignable. * Fields that are not mass assignable.
*/ */
@ -65,7 +60,6 @@ class Server extends Model
'threads' => 'nullable|regex:/^[0-9-,]+$/', 'threads' => 'nullable|regex:/^[0-9-,]+$/',
'oom_killer' => 'sometimes|boolean', 'oom_killer' => 'sometimes|boolean',
'disk' => 'required|numeric|min:0', 'disk' => 'required|numeric|min:0',
'allocation_id' => 'required|bail|unique:servers|exists:allocations,id',
'egg_id' => 'required|exists:eggs,id', 'egg_id' => 'required|exists:eggs,id',
'startup' => 'required|string', 'startup' => 'required|string',
'skip_scripts' => 'sometimes|boolean', 'skip_scripts' => 'sometimes|boolean',
@ -73,6 +67,7 @@ class Server extends Model
'database_limit' => 'present|nullable|integer|min:0', 'database_limit' => 'present|nullable|integer|min:0',
'allocation_limit' => 'sometimes|nullable|integer|min:0', 'allocation_limit' => 'sometimes|nullable|integer|min:0',
'backup_limit' => 'present|nullable|integer|min:0', 'backup_limit' => 'present|nullable|integer|min:0',
'ports' => 'array',
]; ];
protected function casts(): array protected function casts(): array
@ -88,27 +83,33 @@ class Server extends Model
'io' => 'integer', 'io' => 'integer',
'cpu' => 'integer', 'cpu' => 'integer',
'oom_killer' => 'boolean', 'oom_killer' => 'boolean',
'allocation_id' => 'integer',
'egg_id' => 'integer', 'egg_id' => 'integer',
'database_limit' => 'integer', 'database_limit' => 'integer',
'allocation_limit' => 'integer', 'allocation_limit' => 'integer',
'backup_limit' => 'integer', 'backup_limit' => 'integer',
self::CREATED_AT => 'datetime',
self::UPDATED_AT => 'datetime',
'deleted_at' => 'datetime', 'deleted_at' => 'datetime',
'installed_at' => 'datetime', 'installed_at' => 'datetime',
'docker_labels' => 'array', 'docker_labels' => 'array',
'ports' => 'array',
]; ];
} }
/** /**
* Returns the format for server allocations when communicating with the Daemon. * Returns the format for server allocations when communicating with the Daemon.
*/ */
public function getAllocationMappings(): array public function getPortMappings(): array
{ {
return $this->allocations->where('node_id', $this->node_id)->groupBy('ip')->map(function ($item) { $defaultIp = '0.0.0.0';
return $item->pluck('port');
})->toArray(); $ports = collect($this->ports)
->map(fn ($port) => str_contains($port, ':') ? $port : "$defaultIp:$port")
->mapToGroups(function ($port) {
[$ip, $port] = explode(':', $port);
return [$ip => (int) $port];
});
return $ports->all();
} }
public function isInstalled(): bool public function isInstalled(): bool
@ -137,22 +138,6 @@ class Server extends Model
return $this->hasMany(Subuser::class, 'server_id', 'id'); return $this->hasMany(Subuser::class, 'server_id', 'id');
} }
/**
* Gets the default allocation for a server.
*/
public function allocation(): BelongsTo
{
return $this->belongsTo(Allocation::class);
}
/**
* Gets all allocations associated with this server.
*/
public function allocations(): HasMany
{
return $this->hasMany(Allocation::class);
}
/** /**
* Gets information for the egg associated with this server. * Gets information for the egg associated with this server.
*/ */

View File

@ -45,7 +45,6 @@ class AppServiceProvider extends ServiceProvider
} }
Relation::enforceMorphMap([ Relation::enforceMorphMap([
'allocation' => Models\Allocation::class,
'api_key' => Models\ApiKey::class, 'api_key' => Models\ApiKey::class,
'backup' => Models\Backup::class, 'backup' => Models\Backup::class,
'database' => Models\Database::class, 'database' => Models\Database::class,

View File

@ -23,12 +23,12 @@ class Port implements ValidationRule
$fail('The :attribute must be an integer.'); $fail('The :attribute must be an integer.');
} }
if ($value < 0) { if ($value <= 1024) {
$fail('The :attribute must be greater or equal to 0.'); $fail('The :attribute must be greater than 1024.');
} }
if ($value > 65535) { if ($value > 65535) {
$fail('The :attribute must be less or equal to 65535.'); $fail('The :attribute must be less than 65535.');
} }
} }
} }

View File

@ -1,115 +0,0 @@
<?php
namespace App\Services\Allocations;
use App\Models\Allocation;
use IPTools\Network;
use App\Models\Node;
use Illuminate\Database\ConnectionInterface;
use App\Exceptions\DisplayException;
use App\Exceptions\Service\Allocation\CidrOutOfRangeException;
use App\Exceptions\Service\Allocation\PortOutOfRangeException;
use App\Exceptions\Service\Allocation\InvalidPortMappingException;
use App\Exceptions\Service\Allocation\TooManyPortsInRangeException;
class AssignmentService
{
public const CIDR_MAX_BITS = 27;
public const CIDR_MIN_BITS = 32;
public const PORT_FLOOR = 1024;
public const PORT_CEIL = 65535;
public const PORT_RANGE_LIMIT = 1000;
public const PORT_RANGE_REGEX = '/^(\d{4,5})-(\d{4,5})$/';
/**
* AssignmentService constructor.
*/
public function __construct(protected ConnectionInterface $connection)
{
}
/**
* Insert allocations into the database and link them to a specific node.
*
* @throws \App\Exceptions\DisplayException
* @throws \App\Exceptions\Service\Allocation\CidrOutOfRangeException
* @throws \App\Exceptions\Service\Allocation\InvalidPortMappingException
* @throws \App\Exceptions\Service\Allocation\PortOutOfRangeException
* @throws \App\Exceptions\Service\Allocation\TooManyPortsInRangeException
*/
public function handle(Node $node, array $data): array
{
$explode = explode('/', $data['allocation_ip']);
if (count($explode) !== 1) {
if (!ctype_digit($explode[1]) || ($explode[1] > self::CIDR_MIN_BITS || $explode[1] < self::CIDR_MAX_BITS)) {
throw new CidrOutOfRangeException();
}
}
try {
// TODO: how should we approach supporting IPv6 with this?
// gethostbyname only supports IPv4, but the alternative (dns_get_record) returns
// an array of records, which is not ideal for this use case, we need a SINGLE
// IP to use, not multiple.
$underlying = gethostbyname($data['allocation_ip']);
$parsed = Network::parse($underlying);
} catch (\Exception $exception) {
throw new DisplayException("Could not parse provided allocation IP address ({$data['allocation_ip']}): {$exception->getMessage()}", $exception);
}
$this->connection->beginTransaction();
$ids = [];
foreach ($parsed as $ip) {
foreach ($data['allocation_ports'] as $port) {
if (!is_digit($port) && !preg_match(self::PORT_RANGE_REGEX, $port)) {
throw new InvalidPortMappingException($port);
}
$insertData = [];
if (preg_match(self::PORT_RANGE_REGEX, $port, $matches)) {
$block = range($matches[1], $matches[2]);
if (count($block) > self::PORT_RANGE_LIMIT) {
throw new TooManyPortsInRangeException();
}
if ((int) $matches[1] <= self::PORT_FLOOR || (int) $matches[2] > self::PORT_CEIL) {
throw new PortOutOfRangeException();
}
foreach ($block as $unit) {
$insertData[] = [
'node_id' => $node->id,
'ip' => $ip->__toString(),
'port' => (int) $unit,
'ip_alias' => array_get($data, 'allocation_alias'),
'server_id' => null,
];
}
} else {
if ((int) $port <= self::PORT_FLOOR || (int) $port > self::PORT_CEIL) {
throw new PortOutOfRangeException();
}
$insertData[] = [
'node_id' => $node->id,
'ip' => $ip->__toString(),
'port' => (int) $port,
'ip_alias' => array_get($data, 'allocation_alias'),
'server_id' => null,
];
}
foreach ($insertData as $insert) {
$allocation = Allocation::query()->create($insert);
$ids[] = $allocation->id;
}
}
}
$this->connection->commit();
return $ids;
}
}

View File

@ -4,51 +4,26 @@ namespace App\Services\Allocations;
use Webmozart\Assert\Assert; use Webmozart\Assert\Assert;
use App\Models\Server; use App\Models\Server;
use App\Models\Allocation;
use App\Exceptions\Service\Allocation\AutoAllocationNotEnabledException; use App\Exceptions\Service\Allocation\AutoAllocationNotEnabledException;
use App\Exceptions\Service\Allocation\NoAutoAllocationSpaceAvailableException; use App\Exceptions\Service\Allocation\NoAutoAllocationSpaceAvailableException;
class FindAssignableAllocationService class FindAssignableAllocationService
{ {
/** public function __construct()
* FindAssignableAllocationService constructor.
*/
public function __construct(private AssignmentService $service)
{ {
} }
/** /**
* Finds an existing unassigned allocation and attempts to assign it to the given server. If * @throws AutoAllocationNotEnabledException
* no allocation can be found, a new one will be created with a random port between the defined * @throws NoAutoAllocationSpaceAvailableException
* range from the configuration.
*
* @throws \App\Exceptions\DisplayException
* @throws \App\Exceptions\Service\Allocation\CidrOutOfRangeException
* @throws \App\Exceptions\Service\Allocation\InvalidPortMappingException
* @throws \App\Exceptions\Service\Allocation\PortOutOfRangeException
* @throws \App\Exceptions\Service\Allocation\TooManyPortsInRangeException
*/ */
public function handle(Server $server): Allocation public function handle(Server $server): int
{ {
if (!config('panel.client_features.allocations.enabled')) { if (!config('panel.client_features.allocations.enabled')) {
throw new AutoAllocationNotEnabledException(); throw new AutoAllocationNotEnabledException();
} }
// Attempt to find a given available allocation for a server. If one cannot be found return $this->createNewAllocation($server);
// we will fall back to attempting to create a new allocation that can be used for the
// server.
/** @var \App\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();
} }
/** /**
@ -56,16 +31,12 @@ class FindAssignableAllocationService
* in the settings. If there are no matches in that range, or something is wrong with the * 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. * range information provided an exception will be raised.
* *
* @throws \App\Exceptions\DisplayException * @throws NoAutoAllocationSpaceAvailableException
* @throws \App\Exceptions\Service\Allocation\CidrOutOfRangeException
* @throws \App\Exceptions\Service\Allocation\InvalidPortMappingException
* @throws \App\Exceptions\Service\Allocation\PortOutOfRangeException
* @throws \App\Exceptions\Service\Allocation\TooManyPortsInRangeException
*/ */
protected function createNewAllocation(Server $server): Allocation protected function createNewAllocation(Server $server): int
{ {
$start = config('panel.client_features.allocations.range_start', null); $start = config('panel.client_features.allocations.range_start');
$end = config('panel.client_features.allocations.range_end', null); $end = config('panel.client_features.allocations.range_end');
if (!$start || !$end) { if (!$start || !$end) {
throw new NoAutoAllocationSpaceAvailableException(); throw new NoAutoAllocationSpaceAvailableException();
@ -74,8 +45,7 @@ class FindAssignableAllocationService
Assert::integerish($start); Assert::integerish($start);
Assert::integerish($end); Assert::integerish($end);
// Get all of the currently allocated ports for the node so that we can figure out // Get all the currently allocated ports for the node so that we can figure out which port might be available.
// which port might be available.
$ports = $server->node->allocations() $ports = $server->node->allocations()
->where('ip', $server->allocation->ip) ->where('ip', $server->allocation->ip)
->whereBetween('port', [$start, $end]) ->whereBetween('port', [$start, $end])
@ -86,26 +56,8 @@ class FindAssignableAllocationService
// array of ports to create a new allocation to assign to the server. // array of ports to create a new allocation to assign to the server.
$available = array_diff(range($start, $end), $ports->toArray()); $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. // Pick a random port out of the remaining available ports.
/** @var int $port */ /** @var int $port */
$port = $available[array_rand($available)]; return $available[array_rand($available)];
$this->service->handle($server->node, [
'allocation_ip' => $server->allocation->ip,
'allocation_ports' => [$port],
]);
/** @var \App\Models\Allocation $allocation */
$allocation = $server->node->allocations()
->where('ip', $server->allocation->ip)
->where('port', $port)
->firstOrFail();
return $allocation;
} }
} }

View File

@ -1,150 +0,0 @@
<?php
namespace App\Services\Deployment;
use App\Models\Allocation;
use App\Exceptions\DisplayException;
use App\Services\Allocations\AssignmentService;
use App\Exceptions\Service\Deployment\NoViableAllocationException;
class AllocationSelectionService
{
protected bool $dedicated = false;
protected array $nodes = [];
protected array $ports = [];
/**
* Toggle if the selected allocation should be the only allocation belonging
* to the given IP address. If true an allocation will not be selected if an IP
* already has another server set to use on if its allocations.
*/
public function setDedicated(bool $dedicated): self
{
$this->dedicated = $dedicated;
return $this;
}
/**
* A list of node IDs that should be used when selecting an allocation. If empty, all
* nodes will be used to filter with.
*/
public function setNodes(array $nodes): self
{
$this->nodes = $nodes;
return $this;
}
/**
* An array of individual ports or port ranges to use when selecting an allocation. If
* empty, all ports will be considered when finding an allocation. If set, only ports appearing
* in the array or range will be used.
*
* @throws \App\Exceptions\DisplayException
*/
public function setPorts(array $ports): self
{
$stored = [];
foreach ($ports as $port) {
if (is_digit($port)) {
$stored[] = $port;
}
// Ranges are stored in the ports array as an array which can be
// better processed in the repository.
if (preg_match(AssignmentService::PORT_RANGE_REGEX, $port, $matches)) {
if (abs((int) $matches[2] - (int) $matches[1]) > AssignmentService::PORT_RANGE_LIMIT) {
throw new DisplayException(trans('exceptions.allocations.too_many_ports'));
}
$stored[] = [$matches[1], $matches[2]];
}
}
$this->ports = $stored;
return $this;
}
/**
* Return a single allocation that should be used as the default allocation for a server.
*
* @throws \App\Exceptions\Service\Deployment\NoViableAllocationException
*/
public function handle(): Allocation
{
$allocation = $this->getRandomAllocation($this->nodes, $this->ports, $this->dedicated);
if (is_null($allocation)) {
throw new NoViableAllocationException(trans('exceptions.deployment.no_viable_allocations'));
}
return $allocation;
}
/**
* Return a single allocation from those meeting the requirements.
*/
private function getRandomAllocation(array $nodes = [], array $ports = [], bool $dedicated = false): ?Allocation
{
$query = Allocation::query()
->whereNull('server_id')
->whereIn('node_id', $nodes);
if (!empty($ports)) {
$query->where(function ($inner) use ($ports) {
$whereIn = [];
foreach ($ports as $port) {
if (is_array($port)) {
$inner->orWhereBetween('port', $port);
continue;
}
$whereIn[] = $port;
}
if (!empty($whereIn)) {
$inner->orWhereIn('port', $whereIn);
}
});
}
// If this allocation should not be shared with any other servers get
// the data and modify the query as necessary,
if ($dedicated) {
$discard = $this->getDiscardableDedicatedAllocations($nodes);
if (!empty($discard)) {
$query->whereNotIn('ip', $discard);
}
}
return $query->inRandomOrder()->first();
}
/**
* Return a result set of node ips that already have at least one
* server assigned to that IP. This allows for filtering out sets for
* dedicated allocation IPs.
*
* If an array of nodes is passed the results will be limited to allocations
* in those nodes.
*/
private function getDiscardableDedicatedAllocations(array $nodes = []): array
{
$query = Allocation::query()->whereNotNull('server_id');
if (!empty($nodes)) {
$query->whereIn('node_id', $nodes);
}
return $query->groupBy('ip')
->get()
->pluck('ip')
->toArray();
}
}

View File

@ -4,9 +4,7 @@ namespace App\Services\Servers;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use App\Models\Server; use App\Models\Server;
use App\Models\Allocation;
use Illuminate\Database\ConnectionInterface; use Illuminate\Database\ConnectionInterface;
use App\Exceptions\DisplayException;
use App\Repositories\Daemon\DaemonServerRepository; use App\Repositories\Daemon\DaemonServerRepository;
use App\Exceptions\Http\Connection\DaemonConnectionException; use App\Exceptions\Http\Connection\DaemonConnectionException;
@ -32,20 +30,12 @@ class BuildModificationService
{ {
/** @var \App\Models\Server $server */ /** @var \App\Models\Server $server */
$server = $this->connection->transaction(function () use ($server, $data) { $server = $this->connection->transaction(function () use ($server, $data) {
$this->processAllocations($server, $data);
if (isset($data['allocation_id']) && $data['allocation_id'] != $server->allocation_id) {
$existingAllocation = $server->allocations()->findOrFail($data['allocation_id']);
throw_unless($existingAllocation, new DisplayException('The requested default allocation is not currently assigned to this server.'));
}
if (!isset($data['oom_killer']) && isset($data['oom_disabled'])) { if (!isset($data['oom_killer']) && isset($data['oom_disabled'])) {
$data['oom_killer'] = !$data['oom_disabled']; $data['oom_killer'] = !$data['oom_disabled'];
} }
// If any of these values are passed through in the data array go ahead and set them correctly on the server model. // If any of these values are passed through in the data array go ahead and set them correctly on the server model.
$merge = Arr::only($data, ['oom_killer', 'memory', 'swap', 'io', 'cpu', 'threads', 'disk', 'allocation_id']); $merge = Arr::only($data, ['oom_killer', 'memory', 'swap', 'io', 'cpu', 'threads', 'disk', 'ports']);
$server->forceFill(array_merge($merge, [ $server->forceFill(array_merge($merge, [
'database_limit' => Arr::get($data, 'database_limit', 0) ?? null, 'database_limit' => Arr::get($data, 'database_limit', 0) ?? null,
@ -72,59 +62,4 @@ class BuildModificationService
return $server; return $server;
} }
/**
* Process the allocations being assigned in the data and ensure they are available for a server.
*
* @throws \App\Exceptions\DisplayException
*/
private function processAllocations(Server $server, array &$data): void
{
if (empty($data['add_allocations']) && empty($data['remove_allocations'])) {
return;
}
// Handle the addition of allocations to this server. Only assign allocations that are not currently
// assigned to a different server, and only allocations on the same node as the server.
if (!empty($data['add_allocations'])) {
$query = Allocation::query()
->where('node_id', $server->node_id)
->whereIn('id', $data['add_allocations'])
->whereNull('server_id');
// Keep track of all the allocations we're just now adding so that we can use the first
// one to reset the default allocation to.
$freshlyAllocated = $query->first()?->id;
$query->update(['server_id' => $server->id, 'notes' => null]);
}
if (!empty($data['remove_allocations'])) {
foreach ($data['remove_allocations'] as $allocation) {
// If we are attempting to remove the default allocation for the server, see if we can reassign
// to the first provided value in add_allocations. If there is no new first allocation then we
// will throw an exception back.
if ($allocation === ($data['allocation_id'] ?? $server->allocation_id)) {
if (empty($freshlyAllocated)) {
throw new DisplayException('You are attempting to delete the default allocation for this server but there is no fallback allocation to use.');
}
// Update the default allocation to be the first allocation that we are creating.
$data['allocation_id'] = $freshlyAllocated;
}
}
// Remove any of the allocations we got that are currently assigned to this server on
// this node. Also set the notes to null, otherwise when re-allocated to a new server those
// notes will be carried over.
Allocation::query()->where('node_id', $server->node_id)
->where('server_id', $server->id)
// Only remove the allocations that we didn't also attempt to add to the server...
->whereIn('id', array_diff($data['remove_allocations'], $data['add_allocations'] ?? []))
->update([
'notes' => null,
'server_id' => null,
]);
}
}
} }

View File

@ -69,7 +69,7 @@ class ServerConfigurationStructureService
'ip' => $server->allocation->ip, 'ip' => $server->allocation->ip,
'port' => $server->allocation->port, 'port' => $server->allocation->port,
], ],
'mappings' => $server->getAllocationMappings(), 'mappings' => $server->getPortMappings(),
], ],
'egg' => [ 'egg' => [
'id' => $server->egg->uuid, 'id' => $server->egg->uuid,

View File

@ -10,12 +10,9 @@ use App\Models\User;
use Webmozart\Assert\Assert; use Webmozart\Assert\Assert;
use App\Models\Server; use App\Models\Server;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use App\Models\Allocation;
use Illuminate\Database\ConnectionInterface; use Illuminate\Database\ConnectionInterface;
use App\Models\Objects\DeploymentObject; use App\Models\Objects\DeploymentObject;
use App\Repositories\Daemon\DaemonServerRepository; use App\Repositories\Daemon\DaemonServerRepository;
use App\Services\Deployment\FindViableNodesService;
use App\Services\Deployment\AllocationSelectionService;
use App\Exceptions\Http\Connection\DaemonConnectionException; use App\Exceptions\Http\Connection\DaemonConnectionException;
class ServerCreationService class ServerCreationService
@ -24,27 +21,23 @@ class ServerCreationService
* ServerCreationService constructor. * ServerCreationService constructor.
*/ */
public function __construct( public function __construct(
private AllocationSelectionService $allocationSelectionService,
private ConnectionInterface $connection, private ConnectionInterface $connection,
private DaemonServerRepository $daemonServerRepository, private DaemonServerRepository $daemonServerRepository,
private FindViableNodesService $findViableNodesService,
private ServerDeletionService $serverDeletionService, private ServerDeletionService $serverDeletionService,
private VariableValidatorService $validatorService private VariableValidatorService $validatorService
) { ) {
} }
/** /**
* Create a server on the Panel and trigger a request to the Daemon to begin the server * Create a server on the Panel and trigger a request to the Daemon to begin the server creation process.
* creation process. This function will attempt to set as many additional values * This function will attempt to set as many additional values as possible given the input data.
* as possible given the input data. For example, if an allocation_id is passed with
* no node_id the node_is will be picked from the allocation.
* *
* @throws \Throwable * @throws \Throwable
* @throws \App\Exceptions\DisplayException * @throws \App\Exceptions\DisplayException
* @throws \Illuminate\Validation\ValidationException * @throws \Illuminate\Validation\ValidationException
* @throws \App\Exceptions\Service\Deployment\NoViableAllocationException * @throws \App\Exceptions\Service\Deployment\NoViableAllocationException
*/ */
public function handle(array $data, DeploymentObject $deployment = null): Server public function handle(array $data, DeploymentObject $deployment = null, $validateVariables = true): Server
{ {
if (!isset($data['oom_killer']) && isset($data['oom_disabled'])) { if (!isset($data['oom_killer']) && isset($data['oom_disabled'])) {
$data['oom_killer'] = !$data['oom_disabled']; $data['oom_killer'] = !$data['oom_disabled'];
@ -53,22 +46,15 @@ class ServerCreationService
// If a deployment object has been passed we need to get the allocation // If a deployment object has been passed we need to get the allocation
// that the server should use, and assign the node from that allocation. // that the server should use, and assign the node from that allocation.
if ($deployment instanceof DeploymentObject) { if ($deployment instanceof DeploymentObject) {
$allocation = $this->configureDeployment($data, $deployment); // Todo: Refactor
$data['allocation_id'] = $allocation->id; // $allocation = $this->configureDeployment($data, $deployment);
$data['node_id'] = $allocation->node_id;
} }
// Auto-configure the node based on the selected allocation Assert::false(empty($data['node_id']));
// if no node was defined.
if (empty($data['node_id'])) {
Assert::false(empty($data['allocation_id']), 'Expected a non-empty allocation_id in server creation data.');
$data['node_id'] = Allocation::query()->findOrFail($data['allocation_id'])->node_id;
}
$eggVariableData = $this->validatorService $eggVariableData = $this->validatorService
->setUserLevel(User::USER_LEVEL_ADMIN) ->setUserLevel(User::USER_LEVEL_ADMIN)
->handle(Arr::get($data, 'egg_id'), Arr::get($data, 'environment', [])); ->handle(Arr::get($data, 'egg_id'), Arr::get($data, 'environment', []), $validateVariables);
// Due to the design of the Daemon, we need to persist this server to the disk // Due to the design of the Daemon, we need to persist this server to the disk
// before we can actually create it on the Daemon. // before we can actually create it on the Daemon.
@ -80,7 +66,6 @@ class ServerCreationService
// Create the server and assign any additional allocations to it. // Create the server and assign any additional allocations to it.
$server = $this->createModel($data); $server = $this->createModel($data);
$this->storeAssignedAllocations($server, $data);
$this->storeEggVariables($server, $eggVariableData); $this->storeEggVariables($server, $eggVariableData);
return $server; return $server;
@ -99,28 +84,6 @@ class ServerCreationService
return $server; return $server;
} }
/**
* Gets an allocation to use for automatic deployment.
*
* @throws \App\Exceptions\DisplayException
* @throws \App\Exceptions\Service\Deployment\NoViableAllocationException
*/
private function configureDeployment(array $data, DeploymentObject $deployment): Allocation
{
/** @var Collection<\App\Models\Node> $nodes */
$nodes = $this->findViableNodesService->handle(
Arr::get($data, 'memory', 0),
Arr::get($data, 'disk', 0),
Arr::get($data, 'cpu', 0),
Arr::get($data, 'tags', []),
);
return $this->allocationSelectionService->setDedicated($deployment->isDedicated())
->setNodes($nodes->pluck('id')->toArray())
->setPorts($deployment->getPorts())
->handle();
}
/** /**
* Store the server in the database and return the model. * Store the server in the database and return the model.
* *
@ -158,21 +121,6 @@ class ServerCreationService
]); ]);
} }
/**
* Configure the allocations assigned to this server.
*/
private function storeAssignedAllocations(Server $server, array $data): void
{
$records = [$data['allocation_id']];
if (isset($data['allocation_additional']) && is_array($data['allocation_additional'])) {
$records = array_merge($records, $data['allocation_additional']);
}
Allocation::query()->whereIn('id', $records)->update([
'server_id' => $server->id,
]);
}
/** /**
* Process environment variables passed for this server and store them in the database. * Process environment variables passed for this server and store them in the database.
*/ */

View File

@ -3,7 +3,6 @@
namespace App\Services\Servers; namespace App\Services\Servers;
use App\Exceptions\Http\Connection\DaemonConnectionException; use App\Exceptions\Http\Connection\DaemonConnectionException;
use App\Models\Allocation;
use App\Models\Node; use App\Models\Node;
use App\Models\Server; use App\Models\Server;
use App\Models\ServerTransfer; use App\Models\ServerTransfer;
@ -52,8 +51,6 @@ class TransferServerService
public function handle(Server $server, array $data): bool public function handle(Server $server, array $data): bool
{ {
$node_id = $data['node_id']; $node_id = $data['node_id'];
$allocation_id = intval($data['allocation_id']);
$additional_allocations = array_map(intval(...), $data['allocation_additional'] ?? []);
// Check if the node is viable for the transfer. // Check if the node is viable for the transfer.
$node = Node::query() $node = Node::query()
@ -71,23 +68,15 @@ class TransferServerService
$server->validateTransferState(); $server->validateTransferState();
$this->connection->transaction(function () use ($server, $node_id, $allocation_id, $additional_allocations) { $this->connection->transaction(function () use ($server, $node_id) {
// Create a new ServerTransfer entry.
$transfer = new ServerTransfer(); $transfer = new ServerTransfer();
$transfer->server_id = $server->id; $transfer->server_id = $server->id;
$transfer->old_node = $server->node_id; $transfer->old_node = $server->node_id;
$transfer->new_node = $node_id; $transfer->new_node = $node_id;
$transfer->old_allocation = $server->allocation_id;
$transfer->new_allocation = $allocation_id;
$transfer->old_additional_allocations = $server->allocations->where('id', '!=', $server->allocation_id)->pluck('id')->all();
$transfer->new_additional_allocations = $additional_allocations;
$transfer->save(); $transfer->save();
// Add the allocations to the server, so they cannot be automatically assigned while the transfer is in progress.
$this->assignAllocationsToServer($server, $node_id, $allocation_id, $additional_allocations);
// Generate a token for the destination node that the source node can use to authenticate with. // Generate a token for the destination node that the source node can use to authenticate with.
$token = $this->nodeJWTService $token = $this->nodeJWTService
->setExpiresAt(CarbonImmutable::now()->addMinutes(15)) ->setExpiresAt(CarbonImmutable::now()->addMinutes(15))
@ -102,32 +91,4 @@ class TransferServerService
return true; return true;
} }
/**
* Assigns the specified allocations to the specified server.
*/
private function assignAllocationsToServer(Server $server, int $node_id, int $allocation_id, array $additional_allocations)
{
$allocations = $additional_allocations;
$allocations[] = $allocation_id;
$node = Node::query()->findOrFail($node_id);
$unassigned = $node->allocations()
->whereNull('server_id')
->pluck('id')
->toArray();
$updateIds = [];
foreach ($allocations as $allocation) {
if (!in_array($allocation, $unassigned)) {
continue;
}
$updateIds[] = $allocation;
}
if (!empty($updateIds)) {
Allocation::query()->whereIn('id', $updateIds)->update(['server_id' => $server->id]);
}
}
} }

View File

@ -25,7 +25,7 @@ class VariableValidatorService
* *
* @throws \Illuminate\Validation\ValidationException * @throws \Illuminate\Validation\ValidationException
*/ */
public function handle(int $egg, array $fields = []): Collection public function handle(int $egg, array $fields = [], $validate = false): Collection
{ {
$query = EggVariable::query()->where('egg_id', $egg); $query = EggVariable::query()->where('egg_id', $egg);
if (!$this->isUserLevel(User::USER_LEVEL_ADMIN)) { if (!$this->isUserLevel(User::USER_LEVEL_ADMIN)) {
@ -44,9 +44,11 @@ class VariableValidatorService
$customAttributes['environment.' . $variable->env_variable] = trans('validation.internal.variable_value', ['env' => $variable->name]); $customAttributes['environment.' . $variable->env_variable] = trans('validation.internal.variable_value', ['env' => $variable->name]);
} }
$validator = $this->validator->make($data, $rules, [], $customAttributes); if ($validate) {
if ($validator->fails()) { $validator = $this->validator->make($data, $rules, [], $customAttributes);
throw new ValidationException($validator); if ($validator->fails()) {
throw new ValidationException($validator);
}
} }
return Collection::make($variables)->map(function ($item) use ($fields) { return Collection::make($variables)->map(function ($item) use ($fields) {

View File

@ -1,77 +0,0 @@
<?php
namespace App\Transformers\Api\Application;
use App\Models\Node;
use App\Models\Server;
use League\Fractal\Resource\Item;
use App\Models\Allocation;
use League\Fractal\Resource\NullResource;
use App\Services\Acl\Api\AdminAcl;
class AllocationTransformer extends BaseTransformer
{
/**
* Relationships that can be loaded onto allocation transformations.
*/
protected array $availableIncludes = ['node', 'server'];
/**
* Return the resource name for the JSONAPI output.
*/
public function getResourceName(): string
{
return Allocation::RESOURCE_NAME;
}
/**
* Return a generic transformed allocation array.
*/
public function transform(Allocation $allocation): array
{
return [
'id' => $allocation->id,
'ip' => $allocation->ip,
'alias' => $allocation->ip_alias,
'port' => $allocation->port,
'notes' => $allocation->notes,
'assigned' => !is_null($allocation->server_id),
];
}
/**
* Load the node relationship onto a given transformation.
*
* @throws \App\Exceptions\Transformer\InvalidTransformerLevelException
*/
public function includeNode(Allocation $allocation): Item|NullResource
{
if (!$this->authorize(AdminAcl::RESOURCE_NODES)) {
return $this->null();
}
return $this->item(
$allocation->node,
$this->makeTransformer(NodeTransformer::class),
Node::RESOURCE_NAME
);
}
/**
* Load the server relationship onto a given transformation.
*
* @throws \App\Exceptions\Transformer\InvalidTransformerLevelException
*/
public function includeServer(Allocation $allocation): Item|NullResource
{
if (!$this->authorize(AdminAcl::RESOURCE_SERVERS) || !$allocation->server) {
return $this->null();
}
return $this->item(
$allocation->server,
$this->makeTransformer(ServerTransformer::class),
Server::RESOURCE_NAME
);
}
}

View File

@ -12,7 +12,7 @@ class NodeTransformer extends BaseTransformer
/** /**
* List of resources that can be included. * List of resources that can be included.
*/ */
protected array $availableIncludes = ['allocations', 'servers']; protected array $availableIncludes = ['servers'];
/** /**
* Return the resource name for the JSONAPI output. * Return the resource name for the JSONAPI output.
@ -45,26 +45,6 @@ class NodeTransformer extends BaseTransformer
return $response; return $response;
} }
/**
* Return the nodes associated with this location.
*
* @throws \App\Exceptions\Transformer\InvalidTransformerLevelException
*/
public function includeAllocations(Node $node): Collection|NullResource
{
if (!$this->authorize(AdminAcl::RESOURCE_ALLOCATIONS)) {
return $this->null();
}
$node->loadMissing('allocations');
return $this->collection(
$node->getRelation('allocations'),
$this->makeTransformer(AllocationTransformer::class),
'allocation'
);
}
/** /**
* Return the nodes associated with this location. * Return the nodes associated with this location.
* *

View File

@ -17,7 +17,6 @@ class ServerTransformer extends BaseTransformer
* List of resources that can be included. * List of resources that can be included.
*/ */
protected array $availableIncludes = [ protected array $availableIncludes = [
'allocations',
'user', 'user',
'subusers', 'subusers',
'egg', 'egg',
@ -76,7 +75,6 @@ class ServerTransformer extends BaseTransformer
], ],
'user' => $server->owner_id, 'user' => $server->owner_id,
'node' => $server->node_id, 'node' => $server->node_id,
'allocation' => $server->allocation_id,
'egg' => $server->egg_id, 'egg' => $server->egg_id,
'container' => [ 'container' => [
'startup_command' => $server->startup, 'startup_command' => $server->startup,
@ -87,25 +85,25 @@ class ServerTransformer extends BaseTransformer
], ],
$server->getUpdatedAtColumn() => $this->formatTimestamp($server->updated_at), $server->getUpdatedAtColumn() => $this->formatTimestamp($server->updated_at),
$server->getCreatedAtColumn() => $this->formatTimestamp($server->created_at), $server->getCreatedAtColumn() => $this->formatTimestamp($server->created_at),
'allocations' => collect($server->ports)->map(function ($port) {
$ip = '0.0.0.0';
if (str_contains($port, ':')) {
[$ip, $port] = explode(':', $port);
}
return [
'id' => random_int(1, PHP_INT_MAX),
'ip' => $ip,
'alias' => null,
'port' => (int) $port,
'notes' => null,
'assigned' => false,
];
})->all(),
]; ];
} }
/**
* Return a generic array of allocations for this server.
*
* @throws \App\Exceptions\Transformer\InvalidTransformerLevelException
*/
public function includeAllocations(Server $server): Collection|NullResource
{
if (!$this->authorize(AdminAcl::RESOURCE_ALLOCATIONS)) {
return $this->null();
}
$server->loadMissing('allocations');
return $this->collection($server->getRelation('allocations'), $this->makeTransformer(AllocationTransformer::class), 'allocation');
}
/** /**
* Return a generic array of data about subusers for this server. * Return a generic array of data about subusers for this server.
* *

View File

@ -1,28 +0,0 @@
<?php
namespace App\Transformers\Api\Client;
use App\Models\Allocation;
class AllocationTransformer extends BaseClientTransformer
{
/**
* Return the resource name for the JSONAPI output.
*/
public function getResourceName(): string
{
return 'allocation';
}
public function transform(Allocation $model): array
{
return [
'id' => $model->id,
'ip' => $model->ip,
'ip_alias' => $model->ip_alias,
'port' => $model->port,
'notes' => $model->notes,
'is_default' => $model->server->allocation_id === $model->id,
];
}
}

View File

@ -6,7 +6,6 @@ use App\Models\Egg;
use App\Models\Server; use App\Models\Server;
use App\Models\Subuser; use App\Models\Subuser;
use League\Fractal\Resource\Item; use League\Fractal\Resource\Item;
use App\Models\Allocation;
use App\Models\Permission; use App\Models\Permission;
use Illuminate\Container\Container; use Illuminate\Container\Container;
use App\Models\EggVariable; use App\Models\EggVariable;
@ -16,7 +15,7 @@ use App\Services\Servers\StartupCommandService;
class ServerTransformer extends BaseClientTransformer class ServerTransformer extends BaseClientTransformer
{ {
protected array $defaultIncludes = ['allocations', 'variables']; protected array $defaultIncludes = ['variables'];
protected array $availableIncludes = ['egg', 'subusers']; protected array $availableIncludes = ['egg', 'subusers'];
@ -78,33 +77,6 @@ class ServerTransformer extends BaseClientTransformer
]; ];
} }
/**
* Returns the allocations associated with this server.
*
* @throws \App\Exceptions\Transformer\InvalidTransformerLevelException
*/
public function includeAllocations(Server $server): Collection
{
$transformer = $this->makeTransformer(AllocationTransformer::class);
$user = $this->request->user();
// While we include this permission, we do need to actually handle it slightly different here
// for the purpose of keeping things functionally working. If the user doesn't have read permissions
// for the allocations we'll only return the primary server allocation, and any notes associated
// with it will be hidden.
//
// This allows us to avoid too much permission regression, without also hiding information that
// is generally needed for the frontend to make sense when browsing or searching results.
if (!$user->can(Permission::ACTION_ALLOCATION_READ, $server)) {
$primary = clone $server->allocation;
$primary->notes = null;
return $this->collection([$primary], $transformer, Allocation::RESOURCE_NAME);
}
return $this->collection($server->allocations, $transformer, Allocation::RESOURCE_NAME);
}
/** /**
* @throws \App\Exceptions\Transformer\InvalidTransformerLevelException * @throws \App\Exceptions\Transformer\InvalidTransformerLevelException
*/ */

View File

@ -1,36 +0,0 @@
<?php
namespace Database\Factories;
use App\Models\Server;
use App\Models\Allocation;
use Illuminate\Database\Eloquent\Factories\Factory;
class AllocationFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = Allocation::class;
/**
* Define the model's default state.
*/
public function definition(): array
{
return [
'ip' => $this->faker->unique()->ipv4(),
'port' => $this->faker->unique()->numberBetween(1024, 65535),
];
}
/**
* Attaches the allocation to a specific server model.
*/
public function forServer(Server $server): self
{
return $this->for($server)->for($server->node);
}
}

View File

@ -11,14 +11,37 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
Schema::table('servers', function (Blueprint $table) { Schema::table('server_transfers', function (Blueprint $table) {
$table->json('ports'); $table->dropColumn(['old_allocation', 'new_allocation', 'old_additional_allocations', 'new_additional_allocations']);
}); });
Schema::table('servers', function (Blueprint $table) {
$table->json('ports')->nullable();
});
DB::table('servers')->update(['ports' => '[]']);
Schema::table('servers', function (Blueprint $table) {
$table->json('ports')->change();
});
dd('works?');
$portMappings = [];
foreach (DB::table('allocations')->get() as $allocation) {
$portMappings[$allocation->server_id][] = "$allocation->ip:$allocation->port";
}
foreach ($portMappings as $serverId => $ports) {
DB::table('servers')
->where('id', $serverId)
->update(['ports' => json_encode($ports)]);
}
Schema::dropIfExists('allocations'); Schema::dropIfExists('allocations');
Schema::table('servers', function (Blueprint $table) { Schema::table('servers', function (Blueprint $table) {
$table->dropColumn(['allocation_id', 'allocation_limit']); $table->dropColumn(['allocation_id']);
}); });
Schema::table('nodes', function (Blueprint $table) { Schema::table('nodes', function (Blueprint $table) {
@ -47,5 +70,12 @@ return new class extends Migration
$table->unique(['node_id', 'ip', 'port']); $table->unique(['node_id', 'ip', 'port']);
}); });
Schema::table('server_transfers', function (Blueprint $table) {
$table->integer('old_node');
$table->integer('new_node');
$table->json('old_additional_allocations')->nullable();
$table->json('new_additional_allocations')->nullable();
});
} }
}; };

View File

@ -1,375 +0,0 @@
@extends('layouts.admin')
@section('title')
New Server
@endsection
@section('content-header')
<h1>Create Server<small>Add a new server to the panel.</small></h1>
<ol class="breadcrumb">
<li><a href="{{ route('admin.index') }}">Admin</a></li>
<li><a href="{{ route('admin.servers') }}">Servers</a></li>
<li class="active">Create Server</li>
</ol>
@endsection
@section('content')
<form action="{{ route('admin.servers.new') }}" method="POST">
<div class="row">
<div class="col-xs-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">Core Details</h3>
</div>
<div class="box-body row">
<div class="col-md-6">
<div class="form-group">
<label for="pName">Server Name</label>
<input type="text" class="form-control" id="pName" name="name" value="{{ old('name') }}" placeholder="Server Name">
<p class="small text-muted no-margin">Character limits: <code>a-z A-Z 0-9 _ - .</code> and <code>[Space]</code>.</p>
</div>
<div class="form-group">
<label for="pUserId">Server Owner</label>
<select id="pUserId" name="owner_id" class="form-control" style="padding-left:0;"></select>
<p class="small text-muted no-margin">Email address of the Server Owner.</p>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="pDescription" class="control-label">Server Description</label>
<textarea id="pDescription" name="description" rows="3" class="form-control">{{ old('description') }}</textarea>
<p class="text-muted small">A brief description of this server.</p>
</div>
<div class="form-group">
<div class="checkbox checkbox-primary no-margin-bottom">
<input id="pStartOnCreation" name="start_on_completion" type="checkbox" {{ \App\Helpers\Utilities::checked('start_on_completion', 1) }} />
<label for="pStartOnCreation" class="strong">Start Server when Installed</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div class="box">
<div class="overlay" id="allocationLoader" style="display:none;"><i class="fa fa-refresh fa-spin"></i></div>
<div class="box-header with-border">
<h3 class="box-title">Allocation Management</h3>
</div>
<div class="box-body row">
<div class="form-group col-sm-4">
<label for="pNodeId">Node</label>
<select name="node_id" id="pNodeId" class="form-control">
@foreach($nodes as $node)
<option value="{{ $node->id }}">{{ $node->name }}</option>
@endforeach
</select>
<p class="small text-muted no-margin">The node which this server will be deployed to.</p>
</div>
<div class="form-group col-sm-4">
<label for="pAllocation">Default Allocation</label>
<select id="pAllocation" name="allocation_id" class="form-control"></select>
<p class="small text-muted no-margin">The main allocation that will be assigned to this server.</p>
</div>
<div class="form-group col-sm-4">
<label for="pAllocationAdditional">Additional Allocation(s)</label>
<select id="pAllocationAdditional" name="allocation_additional[]" class="form-control" multiple></select>
<p class="small text-muted no-margin">Additional allocations to assign to this server on creation.</p>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div class="box">
<div class="overlay" id="allocationLoader" style="display:none;"><i class="fa fa-refresh fa-spin"></i></div>
<div class="box-header with-border">
<h3 class="box-title">Application Feature Limits</h3>
</div>
<div class="box-body row">
<div class="form-group col-xs-6">
<label for="pDatabaseLimit" class="control-label">Database Limit</label>
<div>
<input type="text" id="pDatabaseLimit" name="database_limit" class="form-control" value="{{ old('database_limit', 0) }}"/>
</div>
<p class="text-muted small">The total number of databases a user is allowed to create for this server.</p>
</div>
<div class="form-group col-xs-6">
<label for="pAllocationLimit" class="control-label">Allocation Limit</label>
<div>
<input type="text" id="pAllocationLimit" name="allocation_limit" class="form-control" value="{{ old('allocation_limit', 0) }}"/>
</div>
<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="pBackupLimit" class="control-label">Backup Limit</label>
<div>
<input type="text" id="pBackupLimit" name="backup_limit" class="form-control" value="{{ old('backup_limit', 0) }}"/>
</div>
<p class="text-muted small">The total number of backups that can be created for this server.</p>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">Resource Management</h3>
</div>
<div class="box-body row">
<div class="form-group col-xs-6">
<label for="pCPU">CPU Limit</label>
<div class="input-group">
<input type="text" id="pCPU" name="cpu" class="form-control" value="{{ old('cpu', 0) }}" />
<span class="input-group-addon">%</span>
</div>
<p class="text-muted small">If you do not want to limit CPU usage, set the value to <code>0</code>. To determine a value, take the number of threads and multiply it by 100. For example, on a quad core system without hyperthreading <code>(4 * 100 = 400)</code> there is <code>400%</code> available. To limit a server to using half of a single thread, you would set the value to <code>50</code>. To allow a server to use up to two threads, set the value to <code>200</code>.<p>
</div>
<div class="form-group col-xs-6">
<label for="pThreads">CPU Pinning</label>
<div>
<input type="text" id="pThreads" name="threads" class="form-control" value="{{ old('threads') }}" />
</div>
<p class="text-muted small"><strong>Advanced:</strong> Enter the specific CPU threads that this process can run on, or leave blank to allow all threads. This can be a single number, or a comma separated list. Example: <code>0</code>, <code>0-1,3</code>, or <code>0,1,3,4</code>.</p>
</div>
</div>
<div class="box-body row">
<div class="form-group col-xs-6">
<label for="pMemory">Memory</label>
<div class="input-group">
<input type="text" id="pMemory" name="memory" class="form-control" value="{{ old('memory') }}" />
<span class="input-group-addon">MiB</span>
</div>
<p class="text-muted small">The maximum amount of memory allowed for this container. Setting this to <code>0</code> will allow unlimited memory in a container.</p>
</div>
<div class="form-group col-xs-6">
<label for="pSwap">Swap</label>
<div class="input-group">
<input type="text" id="pSwap" name="swap" class="form-control" value="{{ old('swap', 0) }}" />
<span class="input-group-addon">MiB</span>
</div>
<p class="text-muted small">Setting this to <code>0</code> will disable swap space on this server. Setting to <code>-1</code> will allow unlimited swap.</p>
</div>
</div>
<div class="box-body row">
<div class="form-group col-xs-6">
<label for="pDisk">Disk Space</label>
<div class="input-group">
<input type="text" id="pDisk" name="disk" class="form-control" value="{{ old('disk') }}" />
<span class="input-group-addon">MiB</span>
</div>
<p class="text-muted small">This server will not be allowed to boot if it is using more than this amount of space. If a server goes over this limit while running it will be safely stopped and locked until enough space is available. Set to <code>0</code> to allow unlimited disk usage.</p>
</div>
<div class="form-group col-xs-6">
<label for="pIO">Block IO Weight</label>
<div>
<input type="text" id="pIO" name="io" class="form-control" value="{{ old('io', 500) }}" />
</div>
<p class="text-muted small"><strong>Advanced</strong>: The IO performance of this server relative to other <em>running</em> containers on the system. Value should be between <code>10</code> and <code>1000</code>. Please see <a href="https://docs.docker.com/engine/reference/run/#block-io-bandwidth-blkio-constraint" target="_blank">this documentation</a> for more information about it.</p>
</div>
<div class="form-group col-xs-12">
<div class="checkbox checkbox-primary no-margin-bottom">
<input type="checkbox" id="pOomKiller" name="oom_killer" value="0" {{ \App\Helpers\Utilities::checked('oom_killer', 0) }} />
<label for="pOomKiller" class="strong">Enable OOM Killer</label>
</div>
<p class="small text-muted no-margin">Terminates the server if it breaches the memory limits. Enabling OOM killer may cause server processes to exit unexpectedly.</p>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">Egg Configuration</h3>
</div>
<div class="box-body row">
<div class="form-group col-xs-12">
<label for="pEggId">Egg</label>
<select id="pEggId" name="egg_id" class="form-control">
@foreach($eggs as $egg)
<option value="{{ $egg->id }}"
@if($egg->id === old('egg_id'))
selected="selected"
@endif
>{{ $egg->name }}</option>
@endforeach
</select>
<p class="small text-muted no-margin">Select the Egg that will define how this server should operate.</p>
</div>
<div class="form-group col-xs-12">
<div class="checkbox checkbox-primary no-margin-bottom">
<input type="checkbox" id="pSkipScripting" name="skip_scripts" value="1" {{ \App\Helpers\Utilities::checked('skip_scripts', 0) }} />
<label for="pSkipScripting" class="strong">Skip Egg Install Script</label>
</div>
<p class="small text-muted no-margin">If the selected Egg has an install script attached to it, the script will run during the install. If you would like to skip this step, check this box.</p>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">Docker Configuration</h3>
</div>
<div class="box-body row">
<div class="form-group col-xs-12">
<label for="pDefaultContainer">Docker Image</label>
<select id="pDefaultContainer" name="image" class="form-control"></select>
<input id="pDefaultContainerCustom" name="custom_image" value="{{ old('custom_image') }}" class="form-control" placeholder="Or enter a custom image..." style="margin-top:1rem"/>
<p class="small text-muted no-margin">This is the default Docker image that will be used to run this server. Select an image from the dropdown above, or enter a custom image in the text field above.</p>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">Startup Configuration</h3>
</div>
<div class="box-body row">
<div class="form-group col-xs-12">
<label for="pStartup">Startup Command</label>
<input type="text" id="pStartup" name="startup" value="{{ old('startup') }}" class="form-control" />
<p class="small text-muted no-margin">The following data substitutes are available for the startup command: <code>@{{SERVER_MEMORY}}</code>, <code>@{{SERVER_IP}}</code>, and <code>@{{SERVER_PORT}}</code>. They will be replaced with the allocated memory, server IP, and server port respectively.</p>
</div>
</div>
<div class="box-header with-border" style="margin-top:-10px;">
<h3 class="box-title">Egg Variables</h3>
</div>
<div class="box-body row" id="appendVariablesTo"></div>
<div class="box-footer">
{!! csrf_field() !!}
<input type="submit" class="btn btn-success pull-right" value="Create Server" />
</div>
</div>
</div>
</div>
</form>
@endsection
@section('footer-scripts')
@parent
{!! Theme::js('vendor/lodash/lodash.js') !!}
<script type="application/javascript">
// Persist 'Egg Variables'
function eggVariablesUpdated(eggId, ids) {
@if (old('egg_id'))
// Check if the egg id matches.
if (eggId != '{{ old('egg_id') }}') {
return;
}
@if (old('environment'))
@foreach (old('environment') as $key => $value)
$('#' + ids['{{ $key }}']).val('{{ $value }}');
@endforeach
@endif
@endif
@if(old('image'))
$('#pDefaultContainer').val('{{ old('image') }}');
@endif
}
// END Persist 'Egg Variables'
</script>
{!! Theme::js('js/admin/new-server.js?v=20220530') !!}
<script type="application/javascript">
$(document).ready(function() {
// Persist 'Server Owner' select2
@if (old('owner_id'))
$.ajax({
url: '/admin/users/accounts.json?user_id={{ old('owner_id') }}',
dataType: 'json',
}).then(function (data) {
initUserIdSelect([ data ]);
});
@else
initUserIdSelect();
@endif
// END Persist 'Server Owner' select2
// Persist 'Node' select2
@if (old('node_id'))
$('#pNodeId').val('{{ old('node_id') }}').change();
// Persist 'Default Allocation' select2
@if (old('allocation_id'))
$('#pAllocation').val('{{ old('allocation_id') }}').change();
@endif
// END Persist 'Default Allocation' select2
// Persist 'Additional Allocations' select2
@if (old('allocation_additional'))
const additional_allocations = [];
@for ($i = 0; $i < count(old('allocation_additional')); $i++)
additional_allocations.push('{{ old('allocation_additional.'.$i)}}');
@endfor
$('#pAllocationAdditional').val(additional_allocations).change();
@endif
// END Persist 'Additional Allocations' select2
@endif
// END Persist 'Node' select2
// Persist 'Egg' select2
@if (old('egg_id'))
$('#pEggId').val('{{ old('egg_id') }}').change();
@endif
// END Persist 'Egg' select2
});
</script>
@endsection

View File

@ -1,194 +0,0 @@
@extends('layouts.admin')
@section('title')
Server {{ $server->name }}: Manage
@endsection
@section('content-header')
<h1>{{ $server->name }}<small>Additional actions to control this server.</small></h1>
<ol class="breadcrumb">
<li><a href="{{ route('admin.index') }}">Admin</a></li>
<li><a href="{{ route('admin.servers') }}">Servers</a></li>
<li><a href="{{ route('admin.servers.view', $server->id) }}">{{ $server->name }}</a></li>
<li class="active">Manage</li>
</ol>
@endsection
@section('content')
@include('admin.servers.partials.navigation')
<div class="row">
<div class="col-sm-4">
<div class="box box-danger">
<div class="box-header with-border">
<h3 class="box-title">Reinstall Server</h3>
</div>
<div class="box-body">
<p>This will reinstall the server with the assigned egg scripts. <strong>Danger!</strong> This could overwrite server data.</p>
</div>
<div class="box-footer">
@if($server->isInstalled())
<form action="{{ route('admin.servers.view.manage.reinstall', $server->id) }}" method="POST">
{!! csrf_field() !!}
<button type="submit" class="btn btn-danger">Reinstall Server</button>
</form>
@else
<button class="btn btn-danger disabled">Server Must Install Properly to Reinstall</button>
@endif
</div>
</div>
</div>
<div class="col-sm-4">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">Install Status</h3>
</div>
<div class="box-body">
<p>If you need to change the install status from uninstalled to installed, or vice versa, you may do so with the button below.</p>
</div>
<div class="box-footer">
<form action="{{ route('admin.servers.view.manage.toggle', $server->id) }}" method="POST">
{!! csrf_field() !!}
<button type="submit" class="btn btn-primary">Toggle Install Status</button>
</form>
</div>
</div>
</div>
@if(! $server->isSuspended())
<div class="col-sm-4">
<div class="box box-warning">
<div class="box-header with-border">
<h3 class="box-title">Suspend Server</h3>
</div>
<div class="box-body">
<p>This will suspend the server, stop any running processes, and immediately block the user from being able to access their files or otherwise manage the server through the panel or API.</p>
</div>
<div class="box-footer">
<form action="{{ route('admin.servers.view.manage.suspension', $server->id) }}" method="POST">
{!! csrf_field() !!}
<input type="hidden" name="action" value="suspend" />
<button type="submit" class="btn btn-warning @if(! is_null($server->transfer)) disabled @endif">Suspend Server</button>
</form>
</div>
</div>
</div>
@else
<div class="col-sm-4">
<div class="box box-success">
<div class="box-header with-border">
<h3 class="box-title">Unsuspend Server</h3>
</div>
<div class="box-body">
<p>This will unsuspend the server and restore normal user access.</p>
</div>
<div class="box-footer">
<form action="{{ route('admin.servers.view.manage.suspension', $server->id) }}" method="POST">
{!! csrf_field() !!}
<input type="hidden" name="action" value="unsuspend" />
<button type="submit" class="btn btn-success">Unsuspend Server</button>
</form>
</div>
</div>
</div>
@endif
@if(is_null($server->transfer))
<div class="col-sm-4">
<div class="box box-success">
<div class="box-header with-border">
<h3 class="box-title">Transfer Server</h3>
</div>
<div class="box-body">
<p>
Transfer this server to another node connected to this panel.
<strong>Warning!</strong> This feature has not been fully tested and may have bugs.
</p>
</div>
<div class="box-footer">
@if(!$canTransfer)
<button class="btn btn-success" data-toggle="modal" data-target="#transferServerModal">Transfer Server</button>
@else
<button class="btn btn-success disabled">Transfer Server</button>
<p style="padding-top: 1rem;">Transferring a server requires more than one node to be configured on your panel.</p>
@endif
</div>
</div>
</div>
@else
<div class="col-sm-4">
<div class="box box-success">
<div class="box-header with-border">
<h3 class="box-title">Transfer Server</h3>
</div>
<div class="box-body">
<p>
This server is currently being transferred to another node.
Transfer was initiated at <strong>{{ $server->transfer->created_at }}</strong>
</p>
</div>
<div class="box-footer">
<button class="btn btn-success disabled">Transfer Server</button>
</div>
</div>
</div>
@endif
</div>
<div class="modal fade" id="transferServerModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<form action="{{ route('admin.servers.view.manage.transfer', $server->id) }}" method="POST">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title">Transfer Server</h4>
</div>
<div class="modal-body">
<div class="row">
<div class="form-group col-md-12">
<label for="pNodeId">Node</label>
<select name="node_id" id="pNodeId" class="form-control">
@foreach($nodes as $node)
@if($node->id != $server->node_id)
<option value="{{ $node->id }}">{{ $node->name }}</option>
@endif
@endforeach
</select>
<p class="small text-muted no-margin">The node which this server will be transferred to.</p>
</div>
<div class="form-group col-md-12">
<label for="pAllocation">Default Allocation</label>
<select name="allocation_id" id="pAllocation" class="form-control"></select>
<p class="small text-muted no-margin">The main allocation that will be assigned to this server.</p>
</div>
<div class="form-group col-md-12">
<label for="pAllocationAdditional">Additional Allocation(s)</label>
<select name="allocation_additional[]" id="pAllocationAdditional" class="form-control" multiple></select>
<p class="small text-muted no-margin">Additional allocations to assign to this server on creation.</p>
</div>
</div>
</div>
<div class="modal-footer">
{!! csrf_field() !!}
<button type="button" class="btn btn-default btn-sm pull-left" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-success btn-sm">Confirm</button>
</div>
</form>
</div>
</div>
</div>
@endsection
@section('footer-scripts')
@parent
{!! Theme::js('vendor/lodash/lodash.js') !!}
@if($canTransfer)
{!! Theme::js('js/admin/server/transfer.js') !!}
@endif
@endsection

View File

@ -137,21 +137,14 @@ Route::prefix('nodes')->group(function () {
Route::get('/view/{node:id}', [Admin\Nodes\NodeViewController::class, 'index'])->name('admin.nodes.view'); Route::get('/view/{node:id}', [Admin\Nodes\NodeViewController::class, 'index'])->name('admin.nodes.view');
Route::get('/view/{node:id}/settings', [Admin\Nodes\NodeViewController::class, 'settings'])->name('admin.nodes.view.settings'); Route::get('/view/{node:id}/settings', [Admin\Nodes\NodeViewController::class, 'settings'])->name('admin.nodes.view.settings');
Route::get('/view/{node:id}/configuration', [Admin\Nodes\NodeViewController::class, 'configuration'])->name('admin.nodes.view.configuration'); Route::get('/view/{node:id}/configuration', [Admin\Nodes\NodeViewController::class, 'configuration'])->name('admin.nodes.view.configuration');
Route::get('/view/{node:id}/allocation', [Admin\Nodes\NodeViewController::class, 'allocations'])->name('admin.nodes.view.allocation');
Route::get('/view/{node:id}/servers', [Admin\Nodes\NodeViewController::class, 'servers'])->name('admin.nodes.view.servers'); Route::get('/view/{node:id}/servers', [Admin\Nodes\NodeViewController::class, 'servers'])->name('admin.nodes.view.servers');
Route::get('/view/{node:id}/system-information', Admin\Nodes\SystemInformationController::class); Route::get('/view/{node:id}/system-information', Admin\Nodes\SystemInformationController::class);
Route::post('/new', [Admin\NodesController::class, 'store']);
Route::post('/view/{node:id}/allocation', [Admin\NodesController::class, 'createAllocation']);
Route::post('/view/{node:id}/allocation/remove', [Admin\NodesController::class, 'allocationRemoveBlock'])->name('admin.nodes.view.allocation.removeBlock');
Route::post('/view/{node:id}/allocation/alias', [Admin\NodesController::class, 'allocationSetAlias'])->name('admin.nodes.view.allocation.setAlias');
Route::post('/view/{node:id}/settings/token', Admin\NodeAutoDeployController::class)->name('admin.nodes.view.configuration.token'); Route::post('/view/{node:id}/settings/token', Admin\NodeAutoDeployController::class)->name('admin.nodes.view.configuration.token');
Route::patch('/view/{node:id}/settings', [Admin\NodesController::class, 'updateSettings']); Route::patch('/view/{node:id}/settings', [Admin\NodesController::class, 'updateSettings']);
Route::delete('/view/{node:id}/delete', [Admin\NodesController::class, 'delete'])->name('admin.nodes.view.delete'); Route::delete('/view/{node:id}/delete', [Admin\NodesController::class, 'delete'])->name('admin.nodes.view.delete');
Route::delete('/view/{node:id}/allocation/remove/{allocation:id}', [Admin\NodesController::class, 'allocationRemoveSingle'])->name('admin.nodes.view.allocation.removeSingle');
Route::delete('/view/{node:id}/allocations', [Admin\NodesController::class, 'allocationRemoveMultiple'])->name('admin.nodes.view.allocation.removeMultiple');
}); });
/* /*

View File

@ -40,12 +40,6 @@ Route::prefix('/nodes')->group(function () {
Route::patch('/{node:id}', [Application\Nodes\NodeController::class, 'update']); Route::patch('/{node:id}', [Application\Nodes\NodeController::class, 'update']);
Route::delete('/{node:id}', [Application\Nodes\NodeController::class, 'delete']); Route::delete('/{node:id}', [Application\Nodes\NodeController::class, 'delete']);
Route::prefix('/{node:id}/allocations')->group(function () {
Route::get('/', [Application\Nodes\AllocationController::class, 'index'])->name('api.application.allocations');
Route::post('/', [Application\Nodes\AllocationController::class, 'store']);
Route::delete('/{allocation:id}', [Application\Nodes\AllocationController::class, 'delete'])->name('api.application.allocations.view');
});
}); });
/* /*
@ -72,7 +66,6 @@ Route::prefix('/servers')->group(function () {
Route::post('/{server:id}/transfer', [Application\Servers\ServerManagementController::class, 'startTransfer'])->name('api.application.servers.transfer'); Route::post('/{server:id}/transfer', [Application\Servers\ServerManagementController::class, 'startTransfer'])->name('api.application.servers.transfer');
Route::post('/{server:id}/transfer/cancel', [Application\Servers\ServerManagementController::class, 'cancelTransfer'])->name('api.application.servers.transfer.cancel'); Route::post('/{server:id}/transfer/cancel', [Application\Servers\ServerManagementController::class, 'cancelTransfer'])->name('api.application.servers.transfer.cancel');
Route::delete('/{server:id}', [Application\Servers\ServerController::class, 'delete']);
Route::delete('/{server:id}/{force?}', [Application\Servers\ServerController::class, 'delete']); Route::delete('/{server:id}/{force?}', [Application\Servers\ServerController::class, 'delete']);
// Database Management Endpoint // Database Management Endpoint

View File

@ -96,14 +96,6 @@ Route::prefix('/servers/{server:uuid}')->middleware([ServerSubject::class, Authe
Route::delete('/{schedule}/tasks/{task}', [Client\Servers\ScheduleTaskController::class, 'delete']); Route::delete('/{schedule}/tasks/{task}', [Client\Servers\ScheduleTaskController::class, 'delete']);
}); });
Route::prefix('/network')->group(function () {
Route::get('/allocations', [Client\Servers\NetworkAllocationController::class, 'index']);
Route::post('/allocations', [Client\Servers\NetworkAllocationController::class, 'store']);
Route::post('/allocations/{allocation}', [Client\Servers\NetworkAllocationController::class, 'update']);
Route::post('/allocations/{allocation}/primary', [Client\Servers\NetworkAllocationController::class, 'setPrimary']);
Route::delete('/allocations/{allocation}', [Client\Servers\NetworkAllocationController::class, 'delete']);
});
Route::prefix('/users')->group(function () { Route::prefix('/users')->group(function () {
Route::get('/', [Client\Servers\SubuserController::class, 'index']); Route::get('/', [Client\Servers\SubuserController::class, 'index']);
Route::post('/', [Client\Servers\SubuserController::class, 'store']); Route::post('/', [Client\Servers\SubuserController::class, 'store']);

View File

@ -11,9 +11,7 @@ use App\Models\Server;
use App\Models\Database; use App\Models\Database;
use App\Models\Schedule; use App\Models\Schedule;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use App\Models\Allocation;
use App\Models\DatabaseHost; use App\Models\DatabaseHost;
use App\Tests\Integration\TestResponse;
use App\Tests\Integration\IntegrationTestCase; use App\Tests\Integration\IntegrationTestCase;
use Illuminate\Database\Eloquent\Model as EloquentModel; use Illuminate\Database\Eloquent\Model as EloquentModel;
use App\Transformers\Api\Client\BaseClientTransformer; use App\Transformers\Api\Client\BaseClientTransformer;
@ -59,9 +57,6 @@ abstract class ClientApiIntegrationTestCase extends IntegrationTestCase
case Task::class: case Task::class:
$link = "/api/client/servers/{$model->schedule->server->uuid}/schedules/{$model->schedule->id}/tasks/$model->id"; $link = "/api/client/servers/{$model->schedule->server->uuid}/schedules/{$model->schedule->id}/tasks/$model->id";
break; break;
case Allocation::class:
$link = "/api/client/servers/{$model->server->uuid}/network/allocations/$model->id";
break;
case Backup::class: case Backup::class:
$link = "/api/client/servers/{$model->server->uuid}/backups/$model->uuid"; $link = "/api/client/servers/{$model->server->uuid}/backups/$model->uuid";
break; break;

View File

@ -5,7 +5,6 @@ namespace App\Tests\Integration\Api\Client;
use App\Models\User; use App\Models\User;
use App\Models\Server; use App\Models\Server;
use App\Models\Subuser; use App\Models\Subuser;
use App\Models\Allocation;
use App\Models\Permission; use App\Models\Permission;
class ClientControllerTest extends ClientApiIntegrationTestCase class ClientControllerTest extends ClientApiIntegrationTestCase
@ -95,49 +94,6 @@ class ClientControllerTest extends ClientApiIntegrationTestCase
->assertJsonPath('data.1.attributes.identifier', $servers[2]->uuid_short); ->assertJsonPath('data.1.attributes.identifier', $servers[2]->uuid_short);
} }
/**
* Test that using ?filter[*]=:25565 or ?filter[*]=192.168.1.1:25565 returns only those servers
* with the same allocation for the given user.
*/
public function testServersAreFilteredUsingAllocationInformation(): void
{
/** @var \App\Models\User $user */
/** @var \App\Models\Server $server */
[$user, $server] = $this->generateTestAccount();
$server2 = $this->createServerModel(['user_id' => $user->id, 'node_id' => $server->node_id]);
$allocation = Allocation::factory()->create(['node_id' => $server->node_id, 'server_id' => $server->id, 'ip' => '192.168.1.1', 'port' => 25565]);
$allocation2 = Allocation::factory()->create(['node_id' => $server->node_id, 'server_id' => $server2->id, 'ip' => '192.168.1.1', 'port' => 25570]);
$server->update(['allocation_id' => $allocation->id]);
$server2->update(['allocation_id' => $allocation2->id]);
$server->refresh();
$server2->refresh();
$this->actingAs($user)->getJson('/api/client?filter[*]=192.168.1.1')
->assertOk()
->assertJsonCount(2, 'data')
->assertJsonPath('data.0.attributes.identifier', $server->uuid_short)
->assertJsonPath('data.1.attributes.identifier', $server2->uuid_short);
$this->actingAs($user)->getJson('/api/client?filter[*]=192.168.1.1:25565')
->assertOk()
->assertJsonCount(1, 'data')
->assertJsonPath('data.0.attributes.identifier', $server->uuid_short);
$this->actingAs($user)->getJson('/api/client?filter[*]=:25570')
->assertOk()
->assertJsonCount(1, 'data')
->assertJsonPath('data.0.attributes.identifier', $server2->uuid_short);
$this->actingAs($user)->getJson('/api/client?filter[*]=:255')
->assertOk()
->assertJsonCount(2, 'data')
->assertJsonPath('data.0.attributes.identifier', $server->uuid_short)
->assertJsonPath('data.1.attributes.identifier', $server2->uuid_short);
}
/** /**
* Test that servers where the user is a subuser are returned by default in the API call. * Test that servers where the user is a subuser are returned by default in the API call.
*/ */
@ -311,13 +267,6 @@ class ClientControllerTest extends ClientApiIntegrationTestCase
{ {
/** @var \App\Models\Server $server */ /** @var \App\Models\Server $server */
[$user, $server] = $this->generateTestAccount([Permission::ACTION_WEBSOCKET_CONNECT]); [$user, $server] = $this->generateTestAccount([Permission::ACTION_WEBSOCKET_CONNECT]);
$server->allocation->notes = 'Test notes';
$server->allocation->save();
Allocation::factory()->times(2)->create([
'node_id' => $server->node_id,
'server_id' => $server->id,
]);
$server->refresh(); $server->refresh();
$response = $this->actingAs($user)->getJson('/api/client'); $response = $this->actingAs($user)->getJson('/api/client');
@ -327,8 +276,6 @@ class ClientControllerTest extends ClientApiIntegrationTestCase
$response->assertJsonPath('data.0.attributes.server_owner', false); $response->assertJsonPath('data.0.attributes.server_owner', false);
$response->assertJsonPath('data.0.attributes.uuid', $server->uuid); $response->assertJsonPath('data.0.attributes.uuid', $server->uuid);
$response->assertJsonCount(1, 'data.0.attributes.relationships.allocations.data'); $response->assertJsonCount(1, 'data.0.attributes.relationships.allocations.data');
$response->assertJsonPath('data.0.attributes.relationships.allocations.data.0.attributes.id', $server->allocation->id);
$response->assertJsonPath('data.0.attributes.relationships.allocations.data.0.attributes.notes', null);
} }
public static function filterTypeDataProvider(): array public static function filterTypeDataProvider(): array

View File

@ -1,57 +0,0 @@
<?php
namespace App\Tests\Integration\Api\Client\Server\Allocation;
use App\Models\Subuser;
use App\Models\Allocation;
use App\Tests\Integration\Api\Client\ClientApiIntegrationTestCase;
class AllocationAuthorizationTest extends ClientApiIntegrationTestCase
{
/**
* @dataProvider methodDataProvider
*/
public function testAccessToAServersAllocationsIsRestrictedProperly(string $method, string $endpoint): void
{
// The API $user is the owner of $server1.
[$user, $server1] = $this->generateTestAccount();
// Will be a subuser of $server2.
$server2 = $this->createServerModel();
// And as no access to $server3.
$server3 = $this->createServerModel();
// Set the API $user as a subuser of server 2, but with no permissions
// to do anything with the allocations for that server.
Subuser::factory()->create(['server_id' => $server2->id, 'user_id' => $user->id]);
$allocation1 = Allocation::factory()->create(['server_id' => $server1->id, 'node_id' => $server1->node_id]);
$allocation2 = Allocation::factory()->create(['server_id' => $server2->id, 'node_id' => $server2->node_id]);
$allocation3 = Allocation::factory()->create(['server_id' => $server3->id, 'node_id' => $server3->node_id]);
// This is the only valid call for this test, accessing the allocation for the same
// server that the API user is the owner of.
$response = $this->actingAs($user)->json($method, $this->link($server1, '/network/allocations/' . $allocation1->id . $endpoint));
$this->assertTrue($response->status() <= 204 || $response->status() === 400 || $response->status() === 422);
// This request fails because the allocation is valid for that server but the user
// making the request is not authorized to perform that action.
$this->actingAs($user)->json($method, $this->link($server2, '/network/allocations/' . $allocation2->id . $endpoint))->assertForbidden();
// Both of these should report a 404 error due to the allocations being linked to
// servers that are not the same as the server in the request, or are assigned
// to a server for which the user making the request has no access to.
$this->actingAs($user)->json($method, $this->link($server1, '/network/allocations/' . $allocation2->id . $endpoint))->assertNotFound();
$this->actingAs($user)->json($method, $this->link($server1, '/network/allocations/' . $allocation3->id . $endpoint))->assertNotFound();
$this->actingAs($user)->json($method, $this->link($server2, '/network/allocations/' . $allocation3->id . $endpoint))->assertNotFound();
$this->actingAs($user)->json($method, $this->link($server3, '/network/allocations/' . $allocation3->id . $endpoint))->assertNotFound();
}
public static function methodDataProvider(): array
{
return [
['POST', ''],
['DELETE', ''],
['POST', '/primary'],
];
}
}

View File

@ -1,93 +0,0 @@
<?php
namespace App\Tests\Integration\Api\Client\Server\Allocation;
use Illuminate\Http\Response;
use App\Models\Allocation;
use App\Models\Permission;
use App\Tests\Integration\Api\Client\ClientApiIntegrationTestCase;
class CreateNewAllocationTest extends ClientApiIntegrationTestCase
{
/**
* Setup tests.
*/
protected function setUp(): void
{
parent::setUp();
config()->set('panel.client_features.allocations.enabled', true);
config()->set('panel.client_features.allocations.range_start', 5000);
config()->set('panel.client_features.allocations.range_end', 5050);
}
/**
* Tests that a new allocation can be properly assigned to a server.
*
* @dataProvider permissionDataProvider
*/
public function testNewAllocationCanBeAssignedToServer(array $permission): void
{
/** @var \App\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(): void
{
/** @var \App\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(): void
{
config()->set('panel.client_features.allocations.enabled', false);
/** @var \App\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 its allocation limit.
*/
public function testAllocationCannotBeCreatedIfServerIsAtLimit(): void
{
/** @var \App\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.');
}
public static function permissionDataProvider(): array
{
return [[[Permission::ACTION_ALLOCATION_CREATE]], [[]]];
}
}

View File

@ -1,105 +0,0 @@
<?php
namespace App\Tests\Integration\Api\Client\Server\Allocation;
use Illuminate\Http\Response;
use App\Models\Allocation;
use App\Models\Permission;
use App\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.
*
* @dataProvider permissionDataProvider
*/
public function testAllocationCanBeDeletedFromServer(array $permission): void
{
/** @var \App\Models\Server $server */
[$user, $server] = $this->generateTestAccount($permission);
$server->update(['allocation_limit' => 2]);
/** @var \App\Models\Allocation $allocation */
$allocation = Allocation::factory()->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(): void
{
/** @var \App\Models\Server $server */
[$user, $server] = $this->generateTestAccount([Permission::ACTION_ALLOCATION_CREATE]);
/** @var \App\Models\Allocation $allocation */
$allocation = Allocation::factory()->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(): void
{
/** @var \App\Models\Server $server */
[$user, $server] = $this->generateTestAccount();
$server->update(['allocation_limit' => 2]);
$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.');
}
public function testAllocationCannotBeDeletedIfServerLimitIsNotDefined(): void
{
[$user, $server] = $this->generateTestAccount();
/** @var \App\Models\Allocation $allocation */
$allocation = Allocation::factory()->forServer($server)->create(['notes' => 'Test notes']);
$this->actingAs($user)->deleteJson($this->link($allocation))
->assertStatus(400)
->assertJsonPath('errors.0.detail', 'You cannot delete allocations for this server: no allocation limit is set.');
$allocation->refresh();
$this->assertNotNull($allocation->notes);
$this->assertEquals($server->id, $allocation->server_id);
}
/**
* Test that an allocation cannot be deleted if it does not belong to the server instance.
*/
public function testErrorIsReturnedIfAllocationDoesNotBelongToServer(): void
{
/** @var \App\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();
}
public static function permissionDataProvider(): array
{
return [[[Permission::ACTION_ALLOCATION_DELETE]], [[]]];
}
}

View File

@ -1,140 +0,0 @@
<?php
namespace App\Tests\Integration\Api\Client\Server;
use App\Models\User;
use Illuminate\Http\Response;
use App\Models\Allocation;
use App\Models\Permission;
use App\Tests\Integration\Api\Client\ClientApiIntegrationTestCase;
class NetworkAllocationControllerTest extends ClientApiIntegrationTestCase
{
/**
* Test that a servers allocations are returned in the expected format.
*/
public function testServerAllocationsAreReturned(): void
{
[$user, $server] = $this->generateTestAccount();
$response = $this->actingAs($user)->getJson($this->link($server, '/network/allocations'));
$response->assertOk();
$response->assertJsonPath('object', 'list');
$response->assertJsonCount(1, 'data');
$this->assertJsonTransformedWith($response->json('data.0.attributes'), $server->allocation);
}
/**
* Test that allocations cannot be returned without the required user permissions.
*/
public function testServerAllocationsAreNotReturnedWithoutPermission(): void
{
[$user, $server] = $this->generateTestAccount();
$user2 = User::factory()->create();
$server->owner_id = $user2->id;
$server->save();
$this->actingAs($user)->getJson($this->link($server, '/network/allocations'))
->assertNotFound();
[$user, $server] = $this->generateTestAccount([Permission::ACTION_ALLOCATION_CREATE]);
$this->actingAs($user)->getJson($this->link($server, '/network/allocations'))
->assertForbidden();
}
/**
* Tests that notes on an allocation can be set correctly.
*
* @dataProvider updatePermissionsDataProvider
*/
public function testAllocationNotesCanBeUpdated(array $permissions): void
{
[$user, $server] = $this->generateTestAccount($permissions);
$allocation = $server->allocation;
$this->assertNull($allocation->notes);
$this->actingAs($user)->postJson($this->link($allocation), [])
->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY)
->assertJsonPath('errors.0.meta.rule', 'present');
$this->actingAs($user)->postJson($this->link($allocation), ['notes' => 'Test notes'])
->assertOk()
->assertJsonPath('object', Allocation::RESOURCE_NAME)
->assertJsonPath('attributes.notes', 'Test notes');
$allocation = $allocation->refresh();
$this->assertSame('Test notes', $allocation->notes);
$this->actingAs($user)->postJson($this->link($allocation), ['notes' => null])
->assertOk()
->assertJsonPath('object', Allocation::RESOURCE_NAME)
->assertJsonPath('attributes.notes', null);
$allocation = $allocation->refresh();
$this->assertNull($allocation->notes);
}
public function testAllocationNotesCannotBeUpdatedByInvalidUsers(): void
{
[$user, $server] = $this->generateTestAccount();
$user2 = User::factory()->create();
$server->owner_id = $user2->id;
$server->save();
$this->actingAs($user)->postJson($this->link($server->allocation))->assertNotFound();
[$user, $server] = $this->generateTestAccount([Permission::ACTION_ALLOCATION_CREATE]);
$this->actingAs($user)->postJson($this->link($server->allocation))->assertForbidden();
}
/**
* @dataProvider updatePermissionsDataProvider
*/
public function testPrimaryAllocationCanBeModified(array $permissions): void
{
[$user, $server] = $this->generateTestAccount($permissions);
$allocation = $server->allocation;
$allocation2 = Allocation::factory()->create(['node_id' => $server->node_id, 'server_id' => $server->id]);
$server->allocation_id = $allocation->id;
$server->save();
$this->actingAs($user)->postJson($this->link($allocation2, '/primary'))
->assertOk();
$server = $server->refresh();
$this->assertSame($allocation2->id, $server->allocation_id);
}
public function testPrimaryAllocationCannotBeModifiedByInvalidUser(): void
{
[$user, $server] = $this->generateTestAccount();
$user2 = User::factory()->create();
$server->owner_id = $user2->id;
$server->save();
$this->actingAs($user)->postJson($this->link($server->allocation, '/primary'))
->assertNotFound();
[$user, $server] = $this->generateTestAccount([Permission::ACTION_ALLOCATION_CREATE]);
$this->actingAs($user)->postJson($this->link($server->allocation, '/primary'))
->assertForbidden();
}
public static function updatePermissionsDataProvider(): array
{
return [[[]], [[Permission::ACTION_ALLOCATION_UPDATE]]];
}
}

View File

@ -1,174 +0,0 @@
<?php
namespace App\Tests\Integration\Services\Allocations;
use App\Models\Allocation;
use App\Tests\Integration\IntegrationTestCase;
use App\Services\Allocations\FindAssignableAllocationService;
use App\Exceptions\Service\Allocation\AutoAllocationNotEnabledException;
use App\Exceptions\Service\Allocation\NoAutoAllocationSpaceAvailableException;
class FindAssignableAllocationServiceTest extends IntegrationTestCase
{
/**
* Setup tests.
*/
protected function setUp(): void
{
parent::setUp();
config()->set('panel.client_features.allocations.enabled', true);
config()->set('panel.client_features.allocations.range_start', 0);
config()->set('panel.client_features.allocations.range_end', 0);
}
/**
* Test that an unassigned allocation is preferred rather than creating an entirely new
* allocation for the server.
*/
public function testExistingAllocationIsPreferred(): void
{
$server = $this->createServerModel();
$created = Allocation::factory()->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(): void
{
$server = $this->createServerModel();
config()->set('panel.client_features.allocations.range_start', 5000);
config()->set('panel.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(): void
{
$server = $this->createServerModel();
$server2 = $this->createServerModel(['node_id' => $server->node_id]);
config()->set('panel.client_features.allocations.range_start', 5000);
config()->set('panel.client_features.allocations.range_end', 5001);
Allocation::factory()->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(): void
{
$server = $this->createServerModel();
$server2 = $this->createServerModel(['node_id' => $server->node_id]);
config()->set('panel.client_features.allocations.range_start', 5000);
config()->set('panel.client_features.allocations.range_end', 5005);
for ($i = 5000; $i <= 5005; $i++) {
Allocation::factory()->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(): void
{
$server = $this->createServerModel();
Allocation::factory()->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(): void
{
$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(): void
{
$server = $this->createServerModel();
config()->set('panel.client_features.allocations.range_start', 'hodor');
config()->set('panel.client_features.allocations.range_end', 10);
try {
$this->getService()->handle($server);
$this->fail('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('panel.client_features.allocations.range_start', 10);
config()->set('panel.client_features.allocations.range_end', 'hodor');
try {
$this->getService()->handle($server);
$this->fail('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(): void
{
config()->set('panel.client_features.allocations.enabled', false);
$server = $this->createServerModel();
$this->expectException(AutoAllocationNotEnabledException::class);
$this->getService()->handle($server);
}
private function getService(): FindAssignableAllocationService
{
return $this->app->make(FindAssignableAllocationService::class);
}
}

View File

@ -6,9 +6,7 @@ use Mockery\MockInterface;
use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Response;
use App\Models\Server; use App\Models\Server;
use App\Models\Allocation;
use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Exception\RequestException;
use App\Exceptions\DisplayException;
use App\Tests\Integration\IntegrationTestCase; use App\Tests\Integration\IntegrationTestCase;
use App\Repositories\Daemon\DaemonServerRepository; use App\Repositories\Daemon\DaemonServerRepository;
use App\Services\Servers\BuildModificationService; use App\Services\Servers\BuildModificationService;
@ -28,76 +26,6 @@ class BuildModificationServiceTest extends IntegrationTestCase
$this->daemonServerRepository = $this->mock(DaemonServerRepository::class); $this->daemonServerRepository = $this->mock(DaemonServerRepository::class);
} }
/**
* Test that allocations can be added and removed from a server. Only the allocations on the
* current node and belonging to this server should be modified.
*/
public function testAllocationsCanBeModifiedForTheServer(): void
{
$server = $this->createServerModel();
$server2 = $this->createServerModel();
/** @var \App\Models\Allocation[] $allocations */
$allocations = Allocation::factory()->times(4)->create(['node_id' => $server->node_id, 'notes' => 'Random notes']);
$initialAllocationId = $server->allocation_id;
$allocations[0]->update(['server_id' => $server->id, 'notes' => 'Test notes']);
// Some additional test allocations for the other server, not the server we are attempting
// to modify.
$allocations[2]->update(['server_id' => $server2->id]);
$allocations[3]->update(['server_id' => $server2->id]);
$this->daemonServerRepository->expects('setServer->sync')->andReturnUndefined();
$response = $this->getService()->handle($server, [
// Attempt to add one new allocation, and an allocation assigned to another server. The
// other server allocation should be ignored, and only the allocation for this server should
// be used.
'add_allocations' => [$allocations[2]->id, $allocations[1]->id],
// Remove the default server allocation, ensuring that the new allocation passed through
// in the data becomes the default allocation.
'remove_allocations' => [$server->allocation_id, $allocations[0]->id, $allocations[3]->id],
]);
$this->assertInstanceOf(Server::class, $response);
// Only one allocation should exist for this server now.
$this->assertCount(1, $response->allocations);
$this->assertSame($allocations[1]->id, $response->allocation_id);
$this->assertNull($response->allocation->notes);
// These two allocations should not have been touched.
$this->assertDatabaseHas('allocations', ['id' => $allocations[2]->id, 'server_id' => $server2->id]);
$this->assertDatabaseHas('allocations', ['id' => $allocations[3]->id, 'server_id' => $server2->id]);
// Both of these allocations should have been removed from the server, and have had their
// notes properly reset.
$this->assertDatabaseHas('allocations', ['id' => $initialAllocationId, 'server_id' => null, 'notes' => null]);
$this->assertDatabaseHas('allocations', ['id' => $allocations[0]->id, 'server_id' => null, 'notes' => null]);
}
/**
* Test that an exception is thrown if removing the default allocation without also assigning
* new allocations to the server.
*/
public function testExceptionIsThrownIfRemovingTheDefaultAllocation(): void
{
$server = $this->createServerModel();
/** @var \App\Models\Allocation[] $allocations */
$allocations = Allocation::factory()->times(4)->create(['node_id' => $server->node_id]);
$allocations[0]->update(['server_id' => $server->id]);
$this->expectException(DisplayException::class);
$this->expectExceptionMessage('You are attempting to delete the default allocation for this server but there is no fallback allocation to use.');
$this->getService()->handle($server, [
'add_allocations' => [],
'remove_allocations' => [$server->allocation_id, $allocations[0]->id],
]);
}
/** /**
* Test that the build data for the server is properly passed along to the daemon instance so that * Test that the build data for the server is properly passed along to the daemon instance so that
* the server data is updated in realtime. This test also ensures that only certain fields get updated * the server data is updated in realtime. This test also ensures that only certain fields get updated
@ -164,91 +92,6 @@ class BuildModificationServiceTest extends IntegrationTestCase
$this->assertDatabaseHas('servers', ['id' => $response->id, 'memory' => 256, 'disk' => 10240]); $this->assertDatabaseHas('servers', ['id' => $response->id, 'memory' => 256, 'disk' => 10240]);
} }
/**
* Test that no exception is thrown if we are only removing an allocation.
*/
public function testNoExceptionIsThrownIfOnlyRemovingAllocation(): void
{
$server = $this->createServerModel();
/** @var \App\Models\Allocation $allocation */
$allocation = Allocation::factory()->create(['node_id' => $server->node_id, 'server_id' => $server->id]);
$this->daemonServerRepository->expects('setServer->sync')->andReturnUndefined();
$this->getService()->handle($server, [
'remove_allocations' => [$allocation->id],
]);
$this->assertDatabaseHas('allocations', ['id' => $allocation->id, 'server_id' => null]);
}
/**
* Test that allocations in both the add and remove arrays are only added, and not removed.
* This scenario wouldn't really happen in the UI, but it is possible to perform via the API,
* so we want to make sure that the logic being used doesn't break if the allocation exists
* in both arrays.
*
* We'll default to adding the allocation in this case.
*/
public function testAllocationInBothAddAndRemoveIsAdded(): void
{
$server = $this->createServerModel();
/** @var \App\Models\Allocation $allocation */
$allocation = Allocation::factory()->create(['node_id' => $server->node_id]);
$this->daemonServerRepository->expects('setServer->sync')->andReturnUndefined();
$this->getService()->handle($server, [
'add_allocations' => [$allocation->id],
'remove_allocations' => [$allocation->id],
]);
$this->assertDatabaseHas('allocations', ['id' => $allocation->id, 'server_id' => $server->id]);
}
/**
* Test that using the same allocation ID multiple times in the array does not cause an error.
*/
public function testUsingSameAllocationIdMultipleTimesDoesNotError(): void
{
$server = $this->createServerModel();
/** @var \App\Models\Allocation $allocation */
$allocation = Allocation::factory()->create(['node_id' => $server->node_id, 'server_id' => $server->id]);
/** @var \App\Models\Allocation $allocation2 */
$allocation2 = Allocation::factory()->create(['node_id' => $server->node_id]);
$this->daemonServerRepository->expects('setServer->sync')->andReturnUndefined();
$this->getService()->handle($server, [
'add_allocations' => [$allocation2->id, $allocation2->id],
'remove_allocations' => [$allocation->id, $allocation->id],
]);
$this->assertDatabaseHas('allocations', ['id' => $allocation->id, 'server_id' => null]);
$this->assertDatabaseHas('allocations', ['id' => $allocation2->id, 'server_id' => $server->id]);
}
/**
* Test that any changes we made to the server or allocations are rolled back if there is an
* exception while performing any action. This is different from the connection exception
* test which should properly ignore connection issues. We want any other type of exception
* to properly be thrown back to the caller.
*/
public function testThatUpdatesAreRolledBackIfExceptionIsEncountered(): void
{
$server = $this->createServerModel();
/** @var \App\Models\Allocation $allocation */
$allocation = Allocation::factory()->create(['node_id' => $server->node_id]);
$this->daemonServerRepository->expects('setServer->sync')->andThrows(new DisplayException('Test'));
$this->expectException(DisplayException::class);
$this->getService()->handle($server, ['add_allocations' => [$allocation->id]]);
$this->assertDatabaseHas('allocations', ['id' => $allocation->id, 'server_id' => null]);
}
private function getService(): BuildModificationService private function getService(): BuildModificationService
{ {
return $this->app->make(BuildModificationService::class); return $this->app->make(BuildModificationService::class);

View File

@ -9,7 +9,6 @@ use App\Models\Node;
use App\Models\User; use App\Models\User;
use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Response;
use App\Models\Server; use App\Models\Server;
use App\Models\Allocation;
use Illuminate\Foundation\Testing\WithFaker; use Illuminate\Foundation\Testing\WithFaker;
use GuzzleHttp\Exception\BadResponseException; use GuzzleHttp\Exception\BadResponseException;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
@ -59,13 +58,8 @@ class ServerCreationServiceTest extends IntegrationTestCase
/** @var \App\Models\Node $node */ /** @var \App\Models\Node $node */
$node = Node::factory()->create(); $node = Node::factory()->create();
/** @var \App\Models\Allocation[]|\Illuminate\Database\Eloquent\Collection $allocations */
$allocations = Allocation::factory()->times(5)->create([
'node_id' => $node->id,
]);
$deployment = (new DeploymentObject())->setDedicated(true)->setPorts([ $deployment = (new DeploymentObject())->setDedicated(true)->setPorts([
$allocations[0]->port, 1234,
]); ]);
$egg = $this->cloneEggAndVariables($this->bungeecord); $egg = $this->cloneEggAndVariables($this->bungeecord);
@ -87,9 +81,7 @@ class ServerCreationServiceTest extends IntegrationTestCase
'startup' => 'java server2.jar', 'startup' => 'java server2.jar',
'image' => 'java:8', 'image' => 'java:8',
'egg_id' => $egg->id, 'egg_id' => $egg->id,
'allocation_additional' => [ 'ports' => [1234, 2345, 3456],
$allocations[4]->id,
],
'environment' => [ 'environment' => [
'BUNGEE_VERSION' => '123', 'BUNGEE_VERSION' => '123',
'SERVER_JARFILE' => 'server2.jar', 'SERVER_JARFILE' => 'server2.jar',
@ -125,18 +117,13 @@ class ServerCreationServiceTest extends IntegrationTestCase
$this->assertSame('server2.jar', $response->variables[1]->server_value); $this->assertSame('server2.jar', $response->variables[1]->server_value);
foreach ($data as $key => $value) { foreach ($data as $key => $value) {
if (in_array($key, ['allocation_additional', 'environment', 'start_on_completion'])) { if (in_array($key, ['environment', 'start_on_completion'])) {
continue; continue;
} }
$this->assertSame($value, $response->{$key}, "Failed asserting equality of '$key' in server response. Got: [{$response->{$key}}] Expected: [$value]"); $this->assertSame($value, $response->{$key}, "Failed asserting equality of '$key' in server response. Got: [{$response->{$key}}] Expected: [$value]");
} }
$this->assertCount(2, $response->allocations);
$this->assertSame($response->allocation_id, $response->allocations[0]->id);
$this->assertSame($allocations[0]->id, $response->allocations[0]->id);
$this->assertSame($allocations[4]->id, $response->allocations[1]->id);
$this->assertFalse($response->isSuspended()); $this->assertFalse($response->isSuspended());
$this->assertFalse($response->oom_killer); $this->assertFalse($response->oom_killer);
$this->assertSame(0, $response->database_limit); $this->assertSame(0, $response->database_limit);
@ -156,17 +143,11 @@ class ServerCreationServiceTest extends IntegrationTestCase
/** @var \App\Models\Node $node */ /** @var \App\Models\Node $node */
$node = Node::factory()->create(); $node = Node::factory()->create();
/** @var \App\Models\Allocation $allocation */
$allocation = Allocation::factory()->create([
'node_id' => $node->id,
]);
$data = [ $data = [
'name' => $this->faker->name(), 'name' => $this->faker->name(),
'description' => $this->faker->sentence(), 'description' => $this->faker->sentence(),
'owner_id' => $user->id, 'owner_id' => $user->id,
'allocation_id' => $allocation->id, 'node_id' => $node->id,
'node_id' => $allocation->node_id,
'memory' => 256, 'memory' => 256,
'swap' => 128, 'swap' => 128,
'disk' => 100, 'disk' => 100,

View File

@ -8,7 +8,6 @@ use App\Models\Node;
use App\Models\User; use App\Models\User;
use App\Models\Server; use App\Models\Server;
use App\Models\Subuser; use App\Models\Subuser;
use App\Models\Allocation;
trait CreatesTestModels trait CreatesTestModels
{ {
@ -37,12 +36,6 @@ trait CreatesTestModels
$attributes['node_id'] = $node->id; $attributes['node_id'] = $node->id;
} }
if (!isset($attributes['allocation_id'])) {
/** @var \App\Models\Allocation $allocation */
$allocation = Allocation::factory()->create(['node_id' => $attributes['node_id']]);
$attributes['allocation_id'] = $allocation->id;
}
if (empty($attributes['egg_id'])) { if (empty($attributes['egg_id'])) {
$egg = $this->getBungeecordEgg(); $egg = $this->getBungeecordEgg();
@ -54,10 +47,8 @@ trait CreatesTestModels
/** @var \App\Models\Server $server */ /** @var \App\Models\Server $server */
$server = Server::factory()->create($attributes); $server = Server::factory()->create($attributes);
Allocation::query()->where('id', $server->allocation_id)->update(['server_id' => $server->id]);
return $server->fresh([ return $server->fresh([
'user', 'node', 'allocation', 'egg', 'user', 'node', 'egg',
]); ]);
} }