1
1
mirror of https://github.com/pterodactyl/panel.git synced 2024-11-22 00:52:43 +01:00

Add ui elements for handling server transfers, add TransferJob.php and TransferService.php

This commit is contained in:
Matthew Penner 2020-04-03 21:45:37 -06:00
parent 49f0421e90
commit a2eab3ca43
11 changed files with 526 additions and 71 deletions

View File

@ -0,0 +1,97 @@
<?php
namespace Pterodactyl\Http\Controllers\Admin\Servers;
use Illuminate\Bus\Dispatcher;
use Illuminate\Http\Request;
use Prologue\Alerts\AlertsMessageBag;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Jobs\Server\TransferJob;
use Pterodactyl\Models\Server;
use Pterodactyl\Repositories\Eloquent\ServerRepository;
use Pterodactyl\Repositories\Eloquent\LocationRepository;
use Pterodactyl\Repositories\Eloquent\NodeRepository;
class ServerTransferController extends Controller
{
/**
* @var \Prologue\Alerts\AlertsMessageBag
*/
private $alert;
/**
* @var \Illuminate\Bus\Dispatcher
*/
private $dispatcher;
/**
* @var \Pterodactyl\Repositories\Eloquent\ServerRepository
*/
private $repository;
/**
* @var \Pterodactyl\Repositories\Eloquent\LocationRepository
*/
private $locationRepository;
/**
* @var \Pterodactyl\Repositories\Eloquent\NodeRepository
*/
private $nodeRepository;
/**
* ServerTransferController constructor.
*
* @param \Prologue\Alerts\AlertsMessageBag $alert
* @param \Illuminate\Bus\Dispatcher $dispatcher
* @param \Pterodactyl\Repositories\Eloquent\ServerRepository $repository
* @param \Pterodactyl\Repositories\Eloquent\LocationRepository $locationRepository
* @param \Pterodactyl\Repositories\Eloquent\NodeRepository $nodeRepository
*/
public function __construct(
AlertsMessageBag $alert,
Dispatcher $dispatcher,
ServerRepository $repository,
LocationRepository $locationRepository,
NodeRepository $nodeRepository
) {
$this->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);
}
}

View File

@ -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,
]);
}
/**

View File

@ -0,0 +1,91 @@
<?php
namespace Pterodactyl\Jobs\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Pterodactyl\Models\Node;
use Pterodactyl\Models\Server;
use Pterodactyl\Services\Servers\ServerCreationService;
use Pterodactyl\Services\Servers\ServerDeletionService;
use Pterodactyl\Services\Servers\SuspensionService;
use Pterodactyl\Services\Servers\TransferService;
class TransferJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private $server, $node, $allocation_id, $additional_allocations;
/**
* Create a new job instance.
*
* @param Server $serverToTransfer
* @param Node $newNode
*/
public function __construct(Server $serverToTransfer, Node $newNode, int $allocation_id, array $additional_allocations)
{
$this->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');
}
}

View File

@ -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;
}
}

View File

@ -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.

View File

@ -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;

View File

@ -0,0 +1,58 @@
<?php
namespace Pterodactyl\Services\Servers;
use Illuminate\Database\ConnectionInterface;
use Psr\Log\LoggerInterface;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Models\Server;
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
class TransferService
{
/**
* @var \Illuminate\Database\ConnectionInterface
*/
private $connection;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/
private $repository;
/**
* @var \Pterodactyl\Repositories\Wings\DaemonServerRepository
*/
private $daemonServerRepository;
/**
* @var \Psr\Log\LoggerInterface
*/
private $writer;
/**
* TransferService constructor.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
* @param \Psr\Log\LoggerInterface $writer
*/
public function __construct(
ConnectionInterface $connection,
DaemonServerRepository $daemonServerRepository,
ServerRepositoryInterface $repository,
LoggerInterface $writer
) {
$this->connection = $connection;
$this->repository = $repository;
$this->daemonServerRepository = $daemonServerRepository;
$this->writer = $writer;
}
public function handle(Server $server)
{
}
}

View File

@ -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',
});
}
});
}

View File

@ -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.',
],
];

View File

@ -20,80 +20,164 @@
@endsection
@section('content')
@include('admin.servers.partials.navigation')
<div class="row">
<div class="col-sm-4">
<div class="box box-primary">
<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 pack and service scripts. <strong>Danger!</strong> This could overwrite server data.</p>
</div>
<div class="box-footer">
@if($server->installed === 1)
<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
@include('admin.servers.partials.navigation')
<div class="row">
<div class="col-sm-4">
<div class="box box-primary">
<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 pack and service scripts. <strong>Danger!</strong> This could overwrite server data.</p>
</div>
<div class="box-footer">
@if($server->installed === 1)
<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->suspended)
<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">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($canTransfer)
<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>
Hopefully, you will soon be able to move servers around without needing to do a bunch of confusing
operations manually and it will work fluidly and with no problems.
</p>
</div>
<div class="box-footer">
<button class="btn btn-success" data-toggle="modal" data-target="#transferServerModal">Transfer Server</button>
</div>
</div>
</div>
@endif
</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>
<div class="modal fade" id="transferServerModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<!-- TODO: Change route -->
<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($locations as $location)
<optgroup label="{{ $location->long }} ({{ $location->short }})">
@foreach($location->nodes as $node)
@if($node->id != $server->node_id)
<option value="{{ $node->id }}"
@if($location->id === old('location_id')) selected @endif
>{{ $node->name }}</option>
@endif
@endforeach
</optgroup>
@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>
@if(! $server->suspended)
<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">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
</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

@ -125,6 +125,7 @@ Route::group(['prefix' => 'servers'], function () {
Route::post('/view/{server}/manage/toggle', 'ServersController@toggleInstall')->name('admin.servers.view.manage.toggle');
Route::post('/view/{server}/manage/suspension', 'ServersController@manageSuspension')->name('admin.servers.view.manage.suspension');
Route::post('/view/{server}/manage/reinstall', 'ServersController@reinstallServer')->name('admin.servers.view.manage.reinstall');
Route::post('/view/{server}/manage/transfer', 'Servers\ServerTransferController@transfer')->name('admin.servers.view.manage.transfer');
Route::post('/view/{server}/delete', 'ServersController@delete');
Route::patch('/view/{server}/details', 'ServersController@setDetails');