diff --git a/app/Http/Controllers/Admin/Servers/ServerTransferController.php b/app/Http/Controllers/Admin/Servers/ServerTransferController.php new file mode 100644 index 000000000..fc1b04e33 --- /dev/null +++ b/app/Http/Controllers/Admin/Servers/ServerTransferController.php @@ -0,0 +1,97 @@ +alert = $alert; + $this->dispatcher = $dispatcher; + $this->repository = $repository; + $this->locationRepository = $locationRepository; + $this->nodeRepository = $nodeRepository; + } + + /** + * Starts a transfer of a server to a new node. + * + * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Models\Server $server + * @return \Illuminate\Http\RedirectResponse + */ + public function transfer(Request $request, Server $server) + { + $validatedData = $request->validate([ + 'node_id' => 'required|exists:nodes,id', + 'allocation_id' => 'required|bail|unique:servers|exists:allocations,id', + 'allocation_additional' => 'nullable', + ]); + + $node_id = $validatedData['node_id']; + $allocation_id = $validatedData['allocation_id']; + $additional_allocations = $validatedData['allocation_additional'] ?? []; + + // Check if the node is viable for the transfer. + $node = $this->nodeRepository->getNodeWithResourceUsage($node_id); + if ($node->isViable($server->memory, $server->disk)) { + // TODO: Run TransferJob. + $this->dispatcher->dispatch(new TransferJob($server, $node, $allocation_id, $additional_allocations)); + + $this->alert->success(trans('admin/server.alerts.transfer_started'))->flash(); + } else { + $this->alert->danger(trans('admin/server.alerts.transfer_not_viable'))->flash(); + } + + return redirect()->route('admin.servers.view.manage', $server->id); + } +} diff --git a/app/Http/Controllers/Admin/Servers/ServerViewController.php b/app/Http/Controllers/Admin/Servers/ServerViewController.php index de78ca17f..1a4a931fa 100644 --- a/app/Http/Controllers/Admin/Servers/ServerViewController.php +++ b/app/Http/Controllers/Admin/Servers/ServerViewController.php @@ -2,6 +2,7 @@ namespace Pterodactyl\Http\Controllers\Admin\Servers; +use JavaScript; use Illuminate\Http\Request; use Pterodactyl\Models\Nest; use Pterodactyl\Models\Server; @@ -9,6 +10,8 @@ use Illuminate\Contracts\View\Factory; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Repositories\Eloquent\NestRepository; +use Pterodactyl\Repositories\Eloquent\LocationRepository; +use Pterodactyl\Repositories\Eloquent\NodeRepository; use Pterodactyl\Repositories\Eloquent\ServerRepository; use Pterodactyl\Traits\Controllers\JavascriptInjection; use Pterodactyl\Repositories\Eloquent\DatabaseHostRepository; @@ -37,17 +40,31 @@ class ServerViewController extends Controller */ private $nestRepository; + /** + * @var \Pterodactyl\Repositories\Eloquent\LocationRepository + */ + private $locationRepository; + + /** + * @var \Pterodactyl\Repositories\Eloquent\NodeRepository + */ + private $nodeRepository; + /** * ServerViewController constructor. * * @param \Pterodactyl\Repositories\Eloquent\DatabaseHostRepository $databaseHostRepository * @param \Pterodactyl\Repositories\Eloquent\NestRepository $nestRepository + * @param \Pterodactyl\Repositories\Eloquent\LocationRepository $locationRepository + * @param \Pterodactyl\Repositories\Eloquent\NodeRepository $nodeRepository * @param \Pterodactyl\Repositories\Eloquent\ServerRepository $repository * @param \Illuminate\Contracts\View\Factory $view */ public function __construct( DatabaseHostRepository $databaseHostRepository, NestRepository $nestRepository, + LocationRepository $locationRepository, + NodeRepository $nodeRepository, ServerRepository $repository, Factory $view ) { @@ -55,6 +72,8 @@ class ServerViewController extends Controller $this->databaseHostRepository = $databaseHostRepository; $this->repository = $repository; $this->nestRepository = $nestRepository; + $this->nodeRepository = $nodeRepository; + $this->locationRepository = $locationRepository; } /** @@ -150,6 +169,7 @@ class ServerViewController extends Controller * @return \Illuminate\Contracts\View\View * * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function manage(Request $request, Server $server) { @@ -159,7 +179,22 @@ class ServerViewController extends Controller ); } - return $this->view->make('admin.servers.view.manage', compact('server')); + // Check if the panel doesn't have at least 2 nodes configured. + $nodes = $this->nodeRepository->all(); + $canTransfer = false; + if (count($nodes) >= 2) { + $canTransfer = true; + } + + Javascript::put([ + 'nodeData' => $this->nodeRepository->getNodesForServerCreation(), + ]); + + return $this->view->make('admin.servers.view.manage', [ + 'server' => $server, + 'locations' => $this->locationRepository->all(), + 'canTransfer' => $canTransfer, + ]); } /** diff --git a/app/Jobs/Server/TransferJob.php b/app/Jobs/Server/TransferJob.php new file mode 100644 index 000000000..24b38b3bd --- /dev/null +++ b/app/Jobs/Server/TransferJob.php @@ -0,0 +1,91 @@ +server = $serverToTransfer; + $this->node = $newNode; + $this->allocation_id = $allocation_id; + $this->additional_allocations = $additional_allocations; + } + + /** + * Execute the job. + * + * @param ServerCreationService $creationService + * @param ServerDeletionService $deletionService + * @param SuspensionService $suspensionService + * @param TransferService $transferService + * @return void + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Throwable + */ + public function handle( + ServerCreationService $creationService, + ServerDeletionService $deletionService, + SuspensionService $suspensionService, + TransferService $transferService + ) { + //$server = $this->server; + //$newNode = $this->node; + + // 1. Suspend Old Server + //$suspensionService->toggle($server, 'suspend'); + + // 2. Zip Folder + //$backup = $server->generateBackup(); + + // 3. Transfer Zip File + //$archive = $newNode->transfer($backup); + + // 4. Verify File Hash + /*if ($backup->hash !== $archive->hash) { + $archive->delete(); + abort(500, 'File transfer corrupted, please try again.'); + }*/ + + // 5. Unzip File + //$archive->extract(); + + // 6. Update Settings on New Node + //$newServerDetails = $server->toArray(); + //$newServerDetails['node_id'] = $newNode->id; + //$newServer = $creationService->create($newServerDetails); + + // 7. Verify Server Status + /*if (!$newServer->isWorking()) { + $deletionService->withForce()->handle($newServer); + abort(500, 'Server failed to startup, please try again.'); + }*/ + + // 8. Unsuspend Old Server + //$deletionService->withForce()->handle($server); + //$suspensionService->toggle($server, 'unsuspend'); + } +} diff --git a/app/Models/Node.php b/app/Models/Node.php index 81cd99383..dc789e64d 100644 --- a/app/Models/Node.php +++ b/app/Models/Node.php @@ -235,4 +235,18 @@ class Node extends Validable { return $this->hasMany(Allocation::class); } + + /** + * Returns a boolean if the node is viable for an additional server to be placed on it. + * + * @param int $memory + * @param int $disk + * @return bool + */ + public function isViable(int $memory, int $disk): bool { + $memoryLimit = $this->memory * (1 + ($this->memory_overallocate / 100)); + $diskLimit = $this->disk * (1 + ($this->disk_overallocate / 100)); + + return ($this->sum_memory + $memory) <= $memoryLimit && ($this->sum_disk + $disk) <= $diskLimit; + } } diff --git a/app/Repositories/Eloquent/NodeRepository.php b/app/Repositories/Eloquent/NodeRepository.php index 89abbeeb3..ef6e3b2ca 100644 --- a/app/Repositories/Eloquent/NodeRepository.php +++ b/app/Repositories/Eloquent/NodeRepository.php @@ -174,6 +174,23 @@ class NodeRepository extends EloquentRepository implements NodeRepositoryInterfa })->values(); } + /** + * Returns a node with the given id with the Node's resource usage. + * + * @param int $node_id + * @return Node + */ + public function getNodeWithResourceUsage(int $node_id): Node + { + $instance = $this->getBuilder() + ->select(['nodes.id', 'nodes.memory', 'nodes.disk', 'nodes.memory_overallocate', 'nodes.disk_overallocate']) + ->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory, IFNULL(SUM(servers.disk), 0) as sum_disk') + ->leftJoin('servers', 'servers.node_id', '=', 'nodes.id') + ->where('nodes.id', $node_id); + + return $instance->first(); + } + /** * Return the IDs of all nodes that exist in the provided locations and have the space * available to support the additional disk and memory provided. diff --git a/app/Services/Servers/ServerCreationService.php b/app/Services/Servers/ServerCreationService.php index b7b6fdb22..cf0900282 100644 --- a/app/Services/Servers/ServerCreationService.php +++ b/app/Services/Servers/ServerCreationService.php @@ -4,7 +4,6 @@ namespace Pterodactyl\Services\Servers; use Ramsey\Uuid\Uuid; use Illuminate\Support\Arr; -use Pterodactyl\Models\Node; use Pterodactyl\Models\User; use Pterodactyl\Models\Server; use Illuminate\Support\Collection; diff --git a/app/Services/Servers/TransferService.php b/app/Services/Servers/TransferService.php new file mode 100644 index 000000000..762487e4f --- /dev/null +++ b/app/Services/Servers/TransferService.php @@ -0,0 +1,58 @@ +connection = $connection; + $this->repository = $repository; + $this->daemonServerRepository = $daemonServerRepository; + $this->writer = $writer; + } + + public function handle(Server $server) + { + + } +} diff --git a/public/themes/pterodactyl/js/admin/server/transfer.js b/public/themes/pterodactyl/js/admin/server/transfer.js new file mode 100644 index 000000000..5c2664a86 --- /dev/null +++ b/public/themes/pterodactyl/js/admin/server/transfer.js @@ -0,0 +1,56 @@ +$(document).ready(function () { + $('#pNodeId').select2({ + placeholder: 'Select a Node', + }).change(); + + $('#pAllocation').select2({ + placeholder: 'Select a Default Allocation', + }); + + $('#pAllocationAdditional').select2({ + placeholder: 'Select Additional Allocations', + }); +}); + +$('#pNodeId').on('change', function () { + let currentNode = $(this).val(); + + $.each(Pterodactyl.nodeData, function (i, v) { + if (v.id == currentNode) { + $('#pAllocation').html('').select2({ + data: v.allocations, + placeholder: 'Select a Default Allocation', + }); + + updateAdditionalAllocations(); + } + }); +}); + +$('#pAllocation').on('change', function () { + updateAdditionalAllocations(); +}); + +function updateAdditionalAllocations() { + let currentAllocation = $('#pAllocation').val(); + let currentNode = $('#pNodeId').val(); + + $.each(Pterodactyl.nodeData, function (i, v) { + if (v.id == currentNode) { + let allocations = []; + + for (let i = 0; i < v.allocations.length; i++) { + const allocation = v.allocations[i]; + + if (allocation.id != currentAllocation) { + allocations.push(allocation); + } + } + + $('#pAllocationAdditional').html('').select2({ + data: allocations, + placeholder: 'Select Additional Allocations', + }); + } + }); +} diff --git a/resources/lang/en/admin/server.php b/resources/lang/en/admin/server.php index fa254c8d9..a697d4e9e 100644 --- a/resources/lang/en/admin/server.php +++ b/resources/lang/en/admin/server.php @@ -27,5 +27,8 @@ return [ 'details_updated' => 'Server details have been successfully updated.', 'docker_image_updated' => 'Successfully changed the default Docker image to use for this server. A reboot is required to apply this change.', 'node_required' => 'You must have at least one node configured before you can add a server to this panel.', + 'transfer_nodes_required' => 'You must have at least two nodes configured before you can transfer servers.', + 'transfer_started' => 'Server transfer has been started.', + 'transfer_not_viable' => 'The node you selected is not viable for this transfer.', ], ]; diff --git a/resources/views/admin/servers/view/manage.blade.php b/resources/views/admin/servers/view/manage.blade.php index d84555acb..8ebb84b63 100644 --- a/resources/views/admin/servers/view/manage.blade.php +++ b/resources/views/admin/servers/view/manage.blade.php @@ -20,80 +20,164 @@ @endsection @section('content') -@include('admin.servers.partials.navigation') -
This will reinstall the server with the assigned pack and service scripts. Danger! This could overwrite server data.
-