Merge pull request #1889 from matthewpi/feature/server-transfers-actually

Server Transfers (pterodactyl/panel#18)
This commit is contained in:
Dane Everitt 2020-04-05 11:07:07 -07:00 committed by GitHub
commit 536f056f2c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 972 additions and 73 deletions

View file

@ -0,0 +1,177 @@
<?php
namespace Pterodactyl\Http\Controllers\Admin\Servers;
use Illuminate\Http\Request;
use Pterodactyl\Models\Server;
use Prologue\Alerts\AlertsMessageBag;
use Pterodactyl\Models\ServerTransfer;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Services\Servers\TransferService;
use Pterodactyl\Services\Servers\SuspensionService;
use Pterodactyl\Repositories\Eloquent\NodeRepository;
use Pterodactyl\Repositories\Eloquent\ServerRepository;
use Pterodactyl\Repositories\Eloquent\LocationRepository;
use Pterodactyl\Repositories\Wings\DaemonConfigurationRepository;
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
class ServerTransferController extends Controller
{
/**
* @var \Prologue\Alerts\AlertsMessageBag
*/
private $alert;
/**
* @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface
*/
private $allocationRepository;
/**
* @var \Pterodactyl\Repositories\Eloquent\ServerRepository
*/
private $repository;
/**
* @var \Pterodactyl\Repositories\Eloquent\LocationRepository
*/
private $locationRepository;
/**
* @var \Pterodactyl\Repositories\Eloquent\NodeRepository
*/
private $nodeRepository;
/**
* @var \Pterodactyl\Services\Servers\SuspensionService
*/
private $suspensionService;
/**
* @var \Pterodactyl\Services\Servers\TransferService
*/
private $transferService;
/**
* @var \Pterodactyl\Repositories\Wings\DaemonConfigurationRepository
*/
private $daemonConfigurationRepository;
/**
* ServerTransferController constructor.
*
* @param \Prologue\Alerts\AlertsMessageBag $alert
* @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $allocationRepository
* @param \Pterodactyl\Repositories\Eloquent\ServerRepository $repository
* @param \Pterodactyl\Repositories\Eloquent\LocationRepository $locationRepository
* @param \Pterodactyl\Repositories\Eloquent\NodeRepository $nodeRepository
* @param \Pterodactyl\Services\Servers\SuspensionService $suspensionService
* @param \Pterodactyl\Services\Servers\TransferService $transferService
* @param \Pterodactyl\Repositories\Wings\DaemonConfigurationRepository $daemonConfigurationRepository
*/
public function __construct(
AlertsMessageBag $alert,
AllocationRepositoryInterface $allocationRepository,
ServerRepository $repository,
LocationRepository $locationRepository,
NodeRepository $nodeRepository,
SuspensionService $suspensionService,
TransferService $transferService,
DaemonConfigurationRepository $daemonConfigurationRepository
) {
$this->alert = $alert;
$this->allocationRepository = $allocationRepository;
$this->repository = $repository;
$this->locationRepository = $locationRepository;
$this->nodeRepository = $nodeRepository;
$this->suspensionService = $suspensionService;
$this->transferService = $transferService;
$this->daemonConfigurationRepository = $daemonConfigurationRepository;
}
/**
* Starts a transfer of a server to a new node.
*
* @param \Illuminate\Http\Request $request
* @param \Pterodactyl\Models\Server $server
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Throwable
*/
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 = intval($validatedData['allocation_id']);
$additional_allocations = array_map('intval', $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)) {
// Check if the selected daemon is online.
$this->daemonConfigurationRepository->setNode($node)->getSystemInformation();
// Suspend the server and request an archive to be created.
$this->suspensionService->toggle($server, 'suspend');
// Create a new ServerTransfer entry.
$transfer = new ServerTransfer;
$transfer->server_id = $server->id;
$transfer->old_node = $server->node_id;
$transfer->new_node = $node_id;
$transfer->old_allocation = $server->allocation_id;
$transfer->new_allocation = $allocation_id;
$transfer->old_additional_allocations = json_encode($server->allocations->where('id', '!=', $server->allocation_id)->pluck('id'));
$transfer->new_additional_allocations = json_encode($additional_allocations);
$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);
// Request an archive from the server's current daemon. (this also checks if the daemon is online)
$this->transferService->requestArchive($server);
$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);
}
/**
* Assigns the specified allocations to the specified server.
*
* @param Server $server
* @param int $node_id
* @param int $allocation_id
* @param array $additional_allocations
*/
private function assignAllocationsToServer(Server $server, int $node_id, int $allocation_id, array $additional_allocations)
{
$allocations = $additional_allocations;
array_push($allocations, $allocation_id);
$unassigned = $this->allocationRepository->getUnassignedAllocationIds($node_id);
$updateIds = [];
foreach ($allocations as $allocation) {
if (! in_array($allocation, $unassigned)) {
continue;
}
$updateIds[] = $allocation;
}
if (! empty($updateIds)) {
$this->allocationRepository->updateWhereIn('id', $updateIds, ['server_id' => $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,8 +10,10 @@ use Illuminate\Contracts\View\Factory;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Repositories\Eloquent\NestRepository;
use Pterodactyl\Repositories\Eloquent\NodeRepository;
use Pterodactyl\Repositories\Eloquent\ServerRepository;
use Pterodactyl\Traits\Controllers\JavascriptInjection;
use Pterodactyl\Repositories\Eloquent\LocationRepository;
use Pterodactyl\Repositories\Eloquent\DatabaseHostRepository;
class ServerViewController extends Controller
@ -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,238 @@
<?php
namespace Pterodactyl\Http\Controllers\Api\Remote\Servers;
use Cake\Chronos\Chronos;
use Lcobucci\JWT\Builder;
use Illuminate\Http\Request;
use Lcobucci\JWT\Signer\Key;
use Psr\Log\LoggerInterface;
use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Services\Servers\SuspensionService;
use Pterodactyl\Repositories\Eloquent\NodeRepository;
use Pterodactyl\Repositories\Eloquent\ServerRepository;
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
use Pterodactyl\Repositories\Wings\DaemonTransferRepository;
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
use Pterodactyl\Services\Servers\ServerConfigurationStructureService;
class ServerTransferController extends Controller
{
/**
* @var \Illuminate\Database\ConnectionInterface
*/
private $connection;
/**
* @var \Pterodactyl\Repositories\Eloquent\ServerRepository
*/
private $repository;
/**
* @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface
*/
private $allocationRepository;
/**
* @var \Pterodactyl\Repositories\Eloquent\NodeRepository
*/
private $nodeRepository;
/**
* @var \Pterodactyl\Repositories\Wings\DaemonServerRepository
*/
private $daemonServerRepository;
/**
* @var \Pterodactyl\Repositories\Wings\DaemonTransferRepository
*/
private $daemonTransferRepository;
/**
* @var \Pterodactyl\Services\Servers\ServerConfigurationStructureService
*/
private $configurationStructureService;
/**
* @var \Pterodactyl\Services\Servers\SuspensionService
*/
private $suspensionService;
/**
* @var \Psr\Log\LoggerInterface
*/
private $writer;
/**
* ServerTransferController constructor.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Pterodactyl\Repositories\Eloquent\ServerRepository $repository
* @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $allocationRepository
* @param \Pterodactyl\Repositories\Eloquent\NodeRepository $nodeRepository
* @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository
* @param \Pterodactyl\Repositories\Wings\DaemonTransferRepository $daemonTransferRepository
* @param \Pterodactyl\Services\Servers\ServerConfigurationStructureService $configurationStructureService
* @param \Pterodactyl\Services\Servers\SuspensionService $suspensionService
* @param \Psr\Log\LoggerInterface $writer
*/
public function __construct(
ConnectionInterface $connection,
ServerRepository $repository,
AllocationRepositoryInterface $allocationRepository,
NodeRepository $nodeRepository,
DaemonServerRepository $daemonServerRepository,
DaemonTransferRepository $daemonTransferRepository,
ServerConfigurationStructureService $configurationStructureService,
SuspensionService $suspensionService,
LoggerInterface $writer
) {
$this->connection = $connection;
$this->repository = $repository;
$this->allocationRepository = $allocationRepository;
$this->nodeRepository = $nodeRepository;
$this->daemonServerRepository = $daemonServerRepository;
$this->daemonTransferRepository = $daemonTransferRepository;
$this->configurationStructureService = $configurationStructureService;
$this->suspensionService = $suspensionService;
$this->writer = $writer;
}
/**
* The daemon notifies us about the archive status.
*
* @param \Illuminate\Http\Request $request
* @param string $uuid
* @return \Illuminate\Http\JsonResponse
*
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Throwable
*/
public function archive(Request $request, string $uuid)
{
$server = $this->repository->getByUuid($uuid);
// Unsuspend the server and don't continue the transfer.
if (! $request->input('successful')) {
$this->suspensionService->toggle($server, 'unsuspend');
return JsonResponse::create([], Response::HTTP_NO_CONTENT);
}
$server->node_id = $server->transfer->new_node;
$data = $this->configurationStructureService->handle($server);
$data['suspended'] = false;
$data['service']['skip_scripts'] = true;
$allocations = $server->getAllocationMappings();
$data['allocations']['default']['ip'] = array_key_first($allocations);
$data['allocations']['default']['port'] = $allocations[$data['allocations']['default']['ip']][0];
$now = Chronos::now();
$signer = new Sha256;
$token = (new Builder)->issuedBy(config('app.url'))
->permittedFor($server->node->getConnectionAddress())
->identifiedBy(hash('sha256', $server->uuid), true)
->issuedAt($now->getTimestamp())
->canOnlyBeUsedAfter($now->getTimestamp())
->expiresAt($now->addMinutes(15)->getTimestamp())
->relatedTo($server->uuid, true)
->getToken($signer, new Key($server->node->daemonSecret));
// On the daemon transfer repository, make sure to set the node after the server
// because setServer() tells the repository to use the server's node and not the one
// we want to specify.
try {
$this->daemonTransferRepository
->setServer($server)
->setNode($this->nodeRepository->find($server->transfer->new_node))
->notify($server, $data, $server->node, $token->__toString());
} catch (DaemonConnectionException $exception) {
throw $exception;
}
return JsonResponse::create([], Response::HTTP_NO_CONTENT);
}
/**
* The daemon notifies us about a transfer failure.
*
* @param \Illuminate\Http\Request $request
* @param string $uuid
* @return \Illuminate\Http\JsonResponse
*
* @throws \Throwable
*/
public function failure(string $uuid)
{
$server = $this->repository->getByUuid($uuid);
$transfer = $server->transfer;
$allocationIds = json_decode($transfer->new_additional_allocations);
array_push($allocationIds, $transfer->new_allocation);
// Remove the new allocations.
$this->allocationRepository->updateWhereIn('id', $allocationIds, ['server_id' => null]);
// Unsuspend the server.
$this->suspensionService->toggle($server, 'unsuspend');
return JsonResponse::create([], Response::HTTP_NO_CONTENT);
}
/**
* The daemon notifies us about a transfer success.
*
* @param string $uuid
* @return \Illuminate\Http\JsonResponse
*
* @throws \Throwable
*/
public function success(string $uuid)
{
$server = $this->repository->getByUuid($uuid);
$transfer = $server->transfer;
$allocationIds = json_decode($transfer->old_additional_allocations);
array_push($allocationIds, $transfer->old_allocation);
// Begin a transaction.
$this->connection->beginTransaction();
// Remove the old allocations.
$this->allocationRepository->updateWhereIn('id', $allocationIds, ['server_id' => null]);
// Update the server's allocation_id and node_id.
$server->allocation_id = $transfer->new_allocation;
$server->node_id = $transfer->new_node;
$server->save();
// Mark the transfer as successful.
$transfer->successful = true;
$transfer->save();
// Commit the transaction.
$this->connection->commit();
// Delete the server from the old node
try {
$this->daemonServerRepository->setServer($server)->delete();
} catch (DaemonConnectionException $exception) {
$this->writer->warning($exception);
}
// Unsuspend the server
$server->load('node');
$this->suspensionService->toggle($server, $this->suspensionService::ACTION_UNSUSPEND);
return JsonResponse::create([], Response::HTTP_NO_CONTENT);
}
}

View file

@ -171,6 +171,7 @@ class Node extends Model
],
'system' => [
'data' => $this->daemonBase,
'archive_directory' => $this->daemonBase . '/.archives',
'username' => 'pterodactyl',
'timezone_path' => '/etc/timezone',
'set_permissions_on_boot' => true,
@ -236,4 +237,19 @@ class Node extends Model
{
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

@ -51,6 +51,7 @@ use Znck\Eloquent\Traits\BelongsToThrough;
* @property \Pterodactyl\Models\Location $location
* @property \Pterodactyl\Models\DaemonKey $key
* @property \Pterodactyl\Models\DaemonKey[]|\Illuminate\Database\Eloquent\Collection $keys
* @property \Pterodactyl\Models\ServerTransfer $transfer
* @property \Pterodactyl\Models\Backup[]|\Illuminate\Database\Eloquent\Collection $backups
*/
class Server extends Model
@ -186,7 +187,7 @@ class Server extends Model
*/
public function getAllocationMappings(): array
{
return $this->allocations->groupBy('ip')->map(function ($item) {
return $this->allocations->where('node_id', $this->node_id)->groupBy('ip')->map(function ($item) {
return $item->pluck('port');
})->toArray();
}
@ -341,6 +342,16 @@ class Server extends Model
return $this->hasMany(DaemonKey::class);
}
/**
* Returns the associated server transfer.
*
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function transfer()
{
return $this->hasOne(ServerTransfer::class)->orderByDesc('id');
}
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/

View file

@ -0,0 +1,81 @@
<?php
namespace Pterodactyl\Models;
/**
* @property int $id
* @property int $server_id
* @property int $old_node
* @property int $new_node
* @property int $old_allocation
* @property int $new_allocation
* @property string $old_additional_allocations
* @property string $new_additional_allocations
* @property bool $successful
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
*
* @property \Pterodactyl\Models\Server $server
*/
class ServerTransfer extends Validable
{
/**
* The resource name for this model when it is transformed into an
* API representation using fractal.
*/
const RESOURCE_NAME = 'server_transfer';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'server_transfers';
/**
* Fields that are not mass assignable.
*
* @var array
*/
protected $guarded = ['id', 'created_at', 'updated_at'];
/**
* Cast values to correct type.
*
* @var array
*/
protected $casts = [
'server_id' => 'int',
'old_node' => 'int',
'new_node' => 'int',
'old_allocation' => 'int',
'new_allocation' => 'int',
'old_additional_allocations' => 'string',
'new_additional_allocations' => 'string',
'successful' => 'bool',
];
/**
* @var array
*/
public static $validationRules = [
'server_id' => 'required|numeric|exists:servers,id',
'old_node' => 'required|numeric',
'new_node' => 'required|numeric',
'old_allocation' => 'required|numeric',
'new_allocation' => 'required|numeric',
'old_additional_allocations' => 'nullable',
'new_additional_allocations' => 'nullable',
'successful' => 'sometimes|boolean',
];
/**
* Gets the server associated with a server transfer.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function server()
{
return $this->belongsTo(Server::class);
}
}

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.fqdn', 'nodes.scheme', 'nodes.daemonSecret', 'nodes.daemonListen', '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

@ -29,7 +29,7 @@ class UserRepository extends EloquentRepository implements UserRepositoryInterfa
*/
public function getAllUsersWithCounts(): LengthAwarePaginator
{
return $this->getBuilder()->withCount('servers', 'subuserOf')
return $this->getBuilder()->withCount('servers')
->search($this->getSearchTerm())
->paginate(50, $this->getColumns());
}

View file

@ -124,4 +124,24 @@ class DaemonServerRepository extends DaemonRepository
throw new DaemonConnectionException($exception);
}
}
/**
* Requests the daemon to create a full archive of the server.
* Once the daemon is finished they will send a POST request to
* "/api/remote/servers/{uuid}/archive" with a boolean.
*
* @throws DaemonConnectionException
*/
public function requestArchive(): void
{
Assert::isInstanceOf($this->server, Server::class);
try {
$this->getHttpClient()->post(sprintf(
'/api/servers/%s/archive', $this->server->uuid
));
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace Pterodactyl\Repositories\Wings;
use Pterodactyl\Models\Node;
use Pterodactyl\Models\Server;
use GuzzleHttp\Exception\TransferException;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
class DaemonTransferRepository extends DaemonRepository
{
/**
* @param Server $server
* @param array $data
* @param Node $node
* @param string $token
*
* @throws DaemonConnectionException
*/
public function notify(Server $server, array $data, Node $node, string $token): void
{
try {
$this->getHttpClient()->post('/api/transfer', [
'json' => [
'server_id' => $server->uuid,
'url' => $node->getConnectionAddress() . sprintf('/api/servers/%s/archive', $server->uuid),
'token' => 'Bearer ' . $token,
'server' => $data,
],
]);
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
}
}

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,46 @@
<?php
namespace Pterodactyl\Services\Servers;
use Pterodactyl\Models\Server;
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
class TransferService
{
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/
private $repository;
/**
* @var \Pterodactyl\Repositories\Wings\DaemonServerRepository
*/
private $daemonServerRepository;
/**
* TransferService constructor.
*
* @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
*/
public function __construct(
DaemonServerRepository $daemonServerRepository,
ServerRepositoryInterface $repository
) {
$this->repository = $repository;
$this->daemonServerRepository = $daemonServerRepository;
}
/**
* Requests an archive from the daemon.
*
* @param int|\Pterodactyl\Models\Server $server
*
* @throws \Throwable
*/
public function requestArchive(Server $server)
{
$this->daemonServerRepository->setServer($server)->requestArchive();
}
}

View file

@ -0,0 +1,44 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddTableServerTransfers extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::dropIfExists('server_transfers');
Schema::create('server_transfers', function (Blueprint $table) {
$table->increments('id');
$table->integer('server_id')->unsigned();
$table->integer('old_node')->unsigned();
$table->integer('new_node')->unsigned();
$table->integer('old_allocation')->unsigned();
$table->integer('new_allocation')->unsigned();
$table->string('old_additional_allocations')->nullable();
$table->string('new_additional_allocations')->nullable();
$table->timestamps();
});
Schema::table('server_transfers', function (Blueprint $table) {
$table->foreign('server_id')->references('id')->on('servers');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('server_transfers');
}
}

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddSuccessfulColumnToServerTransfers extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('server_transfers', function (Blueprint $table) {
$table->tinyInteger('successful')->unsigned()->default(0);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('server_transfers', function (Blueprint $table) {
$table->dropColumn('successful');
});
}
}

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-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 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');

View file

@ -12,5 +12,9 @@ Route::group(['prefix' => '/servers/{uuid}'], function () {
Route::get('/install', 'Servers\ServerInstallController@index');
Route::post('/install', 'Servers\ServerInstallController@store');
Route::post('/archive', 'Servers\ServerTransferController@archive');
Route::get('/transfer/failure', 'Servers\ServerTransferController@failure');
Route::get('/transfer/success', 'Servers\ServerTransferController@success');
Route::post('/backup/{backup}', 'Servers\ServerBackupController');
});