Merge pull request #2883 from pterodactyl/matthewpi/transfer-improvements
Add Transfer Logs
This commit is contained in:
commit
70afc51c9e
31 changed files with 579 additions and 246 deletions
17
app/Exceptions/Http/Server/ServerTransferringException.php
Normal file
17
app/Exceptions/Http/Server/ServerTransferringException.php
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Exceptions\Http\Server;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
class ServerTransferringException extends HttpException
|
||||
{
|
||||
/**
|
||||
* ServerTransferringException constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(Response::HTTP_CONFLICT, 'This server is currently being transferred to a new machine, please try again laster.');
|
||||
}
|
||||
}
|
|
@ -8,7 +8,6 @@ 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;
|
||||
|
@ -42,11 +41,6 @@ class ServerTransferController extends Controller
|
|||
*/
|
||||
private $nodeRepository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Servers\SuspensionService
|
||||
*/
|
||||
private $suspensionService;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Servers\TransferService
|
||||
*/
|
||||
|
@ -65,7 +59,6 @@ class ServerTransferController extends Controller
|
|||
* @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
|
||||
*/
|
||||
|
@ -75,7 +68,6 @@ class ServerTransferController extends Controller
|
|||
ServerRepository $repository,
|
||||
LocationRepository $locationRepository,
|
||||
NodeRepository $nodeRepository,
|
||||
SuspensionService $suspensionService,
|
||||
TransferService $transferService,
|
||||
DaemonConfigurationRepository $daemonConfigurationRepository
|
||||
) {
|
||||
|
@ -84,7 +76,6 @@ class ServerTransferController extends Controller
|
|||
$this->repository = $repository;
|
||||
$this->locationRepository = $locationRepository;
|
||||
$this->nodeRepository = $nodeRepository;
|
||||
$this->suspensionService = $suspensionService;
|
||||
$this->transferService = $transferService;
|
||||
$this->daemonConfigurationRepository = $daemonConfigurationRepository;
|
||||
}
|
||||
|
@ -98,8 +89,7 @@ class ServerTransferController extends Controller
|
|||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function transfer(Request $request, Server $server)
|
||||
{
|
||||
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',
|
||||
|
@ -116,9 +106,6 @@ class ServerTransferController extends Controller
|
|||
// 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;
|
||||
|
||||
|
@ -127,8 +114,8 @@ class ServerTransferController extends Controller
|
|||
$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->old_additional_allocations = $server->allocations->where('id', '!=', $server->allocation_id)->pluck('id');
|
||||
$transfer->new_additional_allocations = $additional_allocations;
|
||||
|
||||
$transfer->save();
|
||||
|
||||
|
|
|
@ -3,12 +3,11 @@
|
|||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Http\Response;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Pterodactyl\Models\Permission;
|
||||
use Pterodactyl\Services\Nodes\NodeJWTService;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Pterodactyl\Exceptions\Http\HttpForbiddenException;
|
||||
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
|
||||
use Pterodactyl\Services\Servers\GetUserPermissionsService;
|
||||
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
||||
|
@ -55,7 +54,22 @@ class WebsocketController extends ClientApiController
|
|||
{
|
||||
$user = $request->user();
|
||||
if ($user->cannot(Permission::ACTION_WEBSOCKET_CONNECT, $server)) {
|
||||
throw new HttpException(Response::HTTP_FORBIDDEN, 'You do not have permission to connect to this server\'s websocket.');
|
||||
throw new HttpForbiddenException('You do not have permission to connect to this server\'s websocket.');
|
||||
}
|
||||
|
||||
$permissions = $this->permissionsService->handle($server, $user);
|
||||
|
||||
$node = $server->node;
|
||||
if (! is_null($server->transfer)) {
|
||||
// Check if the user has permissions to receive transfer logs.
|
||||
if (! in_array('admin.websocket.transfer', $permissions)) {
|
||||
throw new HttpForbiddenException('You do not have permission to view server transfer logs.');
|
||||
}
|
||||
|
||||
// Redirect the websocket request to the new node if the server has been archived.
|
||||
if ($server->transfer->archived) {
|
||||
$node = $server->transfer->newNode;
|
||||
}
|
||||
}
|
||||
|
||||
$token = $this->jwtService
|
||||
|
@ -63,11 +77,11 @@ class WebsocketController extends ClientApiController
|
|||
->setClaims([
|
||||
'user_id' => $request->user()->id,
|
||||
'server_uuid' => $server->uuid,
|
||||
'permissions' => $this->permissionsService->handle($server, $user),
|
||||
'permissions' => $permissions,
|
||||
])
|
||||
->handle($server->node, $user->id . $server->uuid);
|
||||
->handle($node, $user->id . $server->uuid);
|
||||
|
||||
$socket = str_replace(['https://', 'http://'], ['wss://', 'ws://'], $server->node->getConnectionAddress());
|
||||
$socket = str_replace(['https://', 'http://'], ['wss://', 'ws://'], $node->getConnectionAddress());
|
||||
|
||||
return new JsonResponse([
|
||||
'data' => [
|
||||
|
|
|
@ -3,21 +3,19 @@
|
|||
namespace Pterodactyl\Http\Controllers\Api\Remote\Servers;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Lcobucci\JWT\Builder;
|
||||
use Illuminate\Support\Arr;
|
||||
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 Pterodactyl\Models\Allocation;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Pterodactyl\Models\ServerTransfer;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Pterodactyl\Http\Controllers\Controller;
|
||||
use Pterodactyl\Services\Servers\SuspensionService;
|
||||
use Pterodactyl\Repositories\Eloquent\NodeRepository;
|
||||
use Pterodactyl\Services\Nodes\NodeJWTService;
|
||||
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;
|
||||
|
||||
|
@ -33,16 +31,6 @@ class ServerTransferController extends Controller
|
|||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface
|
||||
*/
|
||||
private $allocationRepository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Repositories\Eloquent\NodeRepository
|
||||
*/
|
||||
private $nodeRepository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Repositories\Wings\DaemonServerRepository
|
||||
*/
|
||||
|
@ -59,48 +47,34 @@ class ServerTransferController extends Controller
|
|||
private $configurationStructureService;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Servers\SuspensionService
|
||||
* @var \Pterodactyl\Services\Nodes\NodeJWTService
|
||||
*/
|
||||
private $suspensionService;
|
||||
|
||||
/**
|
||||
* @var \Psr\Log\LoggerInterface
|
||||
*/
|
||||
private $writer;
|
||||
private $jwtService;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param \Pterodactyl\Services\Nodes\NodeJWTService $jwtService
|
||||
*/
|
||||
public function __construct(
|
||||
ConnectionInterface $connection,
|
||||
ServerRepository $repository,
|
||||
AllocationRepositoryInterface $allocationRepository,
|
||||
NodeRepository $nodeRepository,
|
||||
DaemonServerRepository $daemonServerRepository,
|
||||
DaemonTransferRepository $daemonTransferRepository,
|
||||
ServerConfigurationStructureService $configurationStructureService,
|
||||
SuspensionService $suspensionService,
|
||||
LoggerInterface $writer
|
||||
NodeJWTService $jwtService
|
||||
) {
|
||||
$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;
|
||||
$this->jwtService = $jwtService;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -110,7 +84,6 @@ class ServerTransferController extends Controller
|
|||
* @param string $uuid
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
* @throws \Throwable
|
||||
*/
|
||||
|
@ -120,52 +93,50 @@ class ServerTransferController extends Controller
|
|||
|
||||
// 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);
|
||||
return $this->processFailedTransfer($server->transfer);
|
||||
}
|
||||
|
||||
$server->node_id = $server->transfer->new_node;
|
||||
|
||||
$data = $this->configurationStructureService->handle($server);
|
||||
$data['suspended'] = false;
|
||||
$data['service']['skip_scripts'] = true;
|
||||
// We want to generate a new configuration using the new node_id value from the
|
||||
// transfer, and not the old node value.
|
||||
$data = $this->configurationStructureService->handle($server, [
|
||||
'node_id' => $server->transfer->new_node,
|
||||
]);
|
||||
|
||||
$allocations = $server->getAllocationMappings();
|
||||
$data['allocations']['default']['ip'] = array_key_first($allocations);
|
||||
$data['allocations']['default']['port'] = $allocations[$data['allocations']['default']['ip']][0];
|
||||
$primary = array_key_first($allocations);
|
||||
Arr::set($data, 'allocations.default.ip', $primary);
|
||||
Arr::set($data, 'allocations.default.port', $allocations[$primary][0]);
|
||||
Arr::set($data, 'service.skip_scripts', true);
|
||||
Arr::set($data, 'suspended', false);
|
||||
|
||||
$now = Chronos::now();
|
||||
$signer = new Sha256;
|
||||
$this->connection->transaction(function () use ($data, $server) {
|
||||
// This token is used by the new node the server is being transfered to. It allows
|
||||
// that node to communicate with the old node during the process to initiate the
|
||||
// actual file transfer.
|
||||
$token = $this->jwtService
|
||||
->setExpiresAt(Chronos::now()->addMinutes(15))
|
||||
->setSubject($server->uuid)
|
||||
->handle($server->node, $server->uuid, '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->getDecryptedKey()));
|
||||
// Update the archived field on the transfer to make clients connect to the websocket
|
||||
// on the new node to be able to receive transfer logs.
|
||||
$server->transfer->forceFill(['archived' => true])->saveOrFail();
|
||||
|
||||
// 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 {
|
||||
// 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.
|
||||
$this->daemonTransferRepository
|
||||
->setServer($server)
|
||||
->setNode($this->nodeRepository->find($server->transfer->new_node))
|
||||
->setNode($server->transfer->newNode)
|
||||
->notify($server, $data, $server->node, $token->__toString());
|
||||
} catch (DaemonConnectionException $exception) {
|
||||
throw $exception;
|
||||
}
|
||||
});
|
||||
|
||||
return JsonResponse::create([], Response::HTTP_NO_CONTENT);
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* The daemon notifies us about a transfer failure.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param string $uuid
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*
|
||||
|
@ -174,18 +145,8 @@ class ServerTransferController extends Controller
|
|||
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);
|
||||
return $this->processFailedTransfer($server->transfer);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -201,38 +162,63 @@ class ServerTransferController extends Controller
|
|||
$server = $this->repository->getByUuid($uuid);
|
||||
$transfer = $server->transfer;
|
||||
|
||||
$allocationIds = json_decode($transfer->old_additional_allocations);
|
||||
array_push($allocationIds, $transfer->old_allocation);
|
||||
/** @var \Pterodactyl\Models\Server $server */
|
||||
$server = $this->connection->transaction(function () use ($server, $transfer) {
|
||||
$allocations = [$transfer->old_allocation];
|
||||
if (! empty($transfer->old_additional_allocations)) {
|
||||
array_push($allocations, $transfer->old_additional_allocations);
|
||||
}
|
||||
|
||||
// Begin a transaction.
|
||||
$this->connection->beginTransaction();
|
||||
// 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([
|
||||
'allocation_id' => $transfer->new_allocation,
|
||||
'node_id' => $transfer->new_node,
|
||||
]);
|
||||
|
||||
// Remove the old allocations.
|
||||
$this->allocationRepository->updateWhereIn('id', $allocationIds, ['server_id' => null]);
|
||||
$server = $server->fresh();
|
||||
$server->transfer->update(['successful' => true]);
|
||||
|
||||
// Update the server's allocation_id and node_id.
|
||||
$server->allocation_id = $transfer->new_allocation;
|
||||
$server->node_id = $transfer->new_node;
|
||||
$server->save();
|
||||
return $server;
|
||||
});
|
||||
|
||||
// Mark the transfer as successful.
|
||||
$transfer->successful = true;
|
||||
$transfer->save();
|
||||
|
||||
// Commit the transaction.
|
||||
$this->connection->commit();
|
||||
|
||||
// Delete the server from the old node
|
||||
// Delete the server from the old node making sure to point it to the old node so
|
||||
// that we do not delete it from the new node the server was transfered to.
|
||||
try {
|
||||
$this->daemonServerRepository->setServer($server)->delete();
|
||||
$this->daemonServerRepository
|
||||
->setServer($server)
|
||||
->setNode($transfer->oldNode)
|
||||
->delete();
|
||||
} catch (DaemonConnectionException $exception) {
|
||||
$this->writer->warning($exception);
|
||||
Log::warning($exception, ['transfer_id' => $server->transfer->id]);
|
||||
}
|
||||
|
||||
// Unsuspend the server
|
||||
$server->load('node');
|
||||
$this->suspensionService->toggle($server, $this->suspensionService::ACTION_UNSUSPEND);
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
return JsonResponse::create([], Response::HTTP_NO_CONTENT);
|
||||
/**
|
||||
* Release all of the reserved allocations for this transfer and mark it as failed in
|
||||
* the database.
|
||||
*
|
||||
* @param \Pterodactyl\Models\ServerTransfer $transfer
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
protected function processFailedTransfer(ServerTransfer $transfer)
|
||||
{
|
||||
$this->connection->transaction(function () use (&$transfer) {
|
||||
$transfer->forceFill(['successful' => false])->saveOrFail();
|
||||
|
||||
$allocations = [$transfer->new_allocation];
|
||||
if (! empty($transfer->new_additional_allocations)) {
|
||||
array_push($allocations, $transfer->new_additional_allocations);
|
||||
}
|
||||
|
||||
Allocation::query()->whereIn('id', $allocations)->update(['server_id' => null]);
|
||||
});
|
||||
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ use Pterodactyl\Exceptions\Http\HttpForbiddenException;
|
|||
use Pterodactyl\Repositories\Eloquent\ServerRepository;
|
||||
use Pterodactyl\Services\Servers\GetUserPermissionsService;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Pterodactyl\Exceptions\Http\Server\ServerTransferringException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Pterodactyl\Http\Requests\Api\Remote\SftpAuthenticationFormRequest;
|
||||
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
|
||||
|
@ -110,7 +111,12 @@ class SftpAuthenticationController extends Controller
|
|||
}
|
||||
}
|
||||
|
||||
// Remeber, for security purposes, only reveal the existence of the server to people that
|
||||
// Prevent SFTP access to servers that are being transferred.
|
||||
if (! is_null($server->transfer)) {
|
||||
throw new ServerTransferringException;
|
||||
}
|
||||
|
||||
// Remember, for security purposes, only reveal the existence of the server to people that
|
||||
// have provided valid credentials, and have permissions to know about it.
|
||||
if ($server->installed !== 1 || $server->suspended) {
|
||||
throw new BadRequestHttpException(
|
||||
|
@ -118,7 +124,7 @@ class SftpAuthenticationController extends Controller
|
|||
);
|
||||
}
|
||||
|
||||
return JsonResponse::create([
|
||||
return new JsonResponse([
|
||||
'server' => $server->uuid,
|
||||
// Deprecated, but still needed at the moment for Wings.
|
||||
'token' => '',
|
||||
|
@ -132,7 +138,7 @@ class SftpAuthenticationController extends Controller
|
|||
* @param \Illuminate\Http\Request $request
|
||||
* @return string
|
||||
*/
|
||||
protected function throttleKey(Request $request)
|
||||
protected function throttleKey(Request $request): string
|
||||
{
|
||||
$username = explode('.', strrev($request->input('username', '')));
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
|
|||
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Pterodactyl\Exceptions\Http\Server\ServerTransferringException;
|
||||
|
||||
class AuthenticateServerAccess
|
||||
{
|
||||
|
@ -24,7 +24,6 @@ class AuthenticateServerAccess
|
|||
* @var string[]
|
||||
*/
|
||||
protected $except = [
|
||||
'api:client:server.view',
|
||||
'api:client:server.ws',
|
||||
];
|
||||
|
||||
|
@ -65,17 +64,26 @@ class AuthenticateServerAccess
|
|||
}
|
||||
}
|
||||
|
||||
if ($server->suspended && !$request->routeIs('api:client:server.resources')) {
|
||||
if ($server->suspended && ! $request->routeIs('api:client:server.resources')) {
|
||||
throw new BadRequestHttpException(
|
||||
'This server is currently suspended and the functionality requested is unavailable.'
|
||||
);
|
||||
}
|
||||
|
||||
if (! $server->isInstalled()) {
|
||||
// Throw an exception for all server routes; however if the user is an admin and requesting the
|
||||
// server details, don't throw the exception for them.
|
||||
if (! $user->root_admin || ($user->root_admin && ! $request->routeIs($this->except))) {
|
||||
throw new ConflictHttpException('Server has not completed the installation process.');
|
||||
// Still allow users to get information about their server if it is installing or being transferred.
|
||||
if (! $request->routeIs('api:client:server.view')) {
|
||||
if (! $server->isInstalled()) {
|
||||
// Throw an exception for all server routes; however if the user is an admin and requesting the
|
||||
// server details, don't throw the exception for them.
|
||||
if (! $user->root_admin || ($user->root_admin && ! $request->routeIs($this->except))) {
|
||||
throw new ConflictHttpException('Server has not completed the installation process.');
|
||||
}
|
||||
}
|
||||
|
||||
if (! is_null($server->transfer)) {
|
||||
if (! $user->root_admin || ($user->root_admin && ! $request->routeIs($this->except))) {
|
||||
throw new ServerTransferringException;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ use Illuminate\Contracts\Routing\ResponseFactory;
|
|||
use Illuminate\Contracts\Config\Repository as ConfigRepository;
|
||||
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
|
||||
use Pterodactyl\Exceptions\Http\Server\ServerTransferringException;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
|
||||
class AccessingValidServer
|
||||
|
@ -80,6 +81,14 @@ class AccessingValidServer
|
|||
return $this->response->view('errors.installing', [], 409);
|
||||
}
|
||||
|
||||
if (! is_null($server->transfer)) {
|
||||
if ($isApiRequest) {
|
||||
throw new ServerTransferringException;
|
||||
}
|
||||
|
||||
return $this->response->view('errors.transferring', [], 409);
|
||||
}
|
||||
|
||||
// Add server to the request attributes. This will replace sessions
|
||||
// as files are updated.
|
||||
$request->attributes->set('server', $server);
|
||||
|
|
|
@ -306,7 +306,7 @@ class Server extends Model
|
|||
*/
|
||||
public function transfer()
|
||||
{
|
||||
return $this->hasOne(ServerTransfer::class)->orderByDesc('id');
|
||||
return $this->hasOne(ServerTransfer::class)->whereNull('successful')->orderByDesc('id');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -9,13 +9,16 @@ namespace Pterodactyl\Models;
|
|||
* @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 array|null $old_additional_allocations
|
||||
* @property array|null $new_additional_allocations
|
||||
* @property bool|null $successful
|
||||
* @property bool $archived
|
||||
* @property \Carbon\Carbon $created_at
|
||||
* @property \Carbon\Carbon $updated_at
|
||||
*
|
||||
* @property \Pterodactyl\Models\Server $server
|
||||
* @property \Pterodactyl\Models\Node $oldNode
|
||||
* @property \Pterodactyl\Models\Node $newNode
|
||||
*/
|
||||
class ServerTransfer extends Model
|
||||
{
|
||||
|
@ -50,9 +53,10 @@ class ServerTransfer extends Model
|
|||
'new_node' => 'int',
|
||||
'old_allocation' => 'int',
|
||||
'new_allocation' => 'int',
|
||||
'old_additional_allocations' => 'string',
|
||||
'new_additional_allocations' => 'string',
|
||||
'old_additional_allocations' => 'array',
|
||||
'new_additional_allocations' => 'array',
|
||||
'successful' => 'bool',
|
||||
'archived' => 'bool',
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -64,9 +68,11 @@ class ServerTransfer extends Model
|
|||
'new_node' => 'required|numeric',
|
||||
'old_allocation' => 'required|numeric',
|
||||
'new_allocation' => 'required|numeric',
|
||||
'old_additional_allocations' => 'nullable',
|
||||
'new_additional_allocations' => 'nullable',
|
||||
'successful' => 'sometimes|boolean',
|
||||
'old_additional_allocations' => 'nullable|array',
|
||||
'old_additional_allocations.*' => 'numeric',
|
||||
'new_additional_allocations' => 'nullable|array',
|
||||
'new_additional_allocations.*' => 'numeric',
|
||||
'successful' => 'sometimes|nullable|boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -78,4 +84,24 @@ class ServerTransfer extends Model
|
|||
{
|
||||
return $this->belongsTo(Server::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the source node associated with a server transfer.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
*/
|
||||
public function oldNode()
|
||||
{
|
||||
return $this->hasOne(Node::class, 'id', 'old_node');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the target node associated with a server transfer.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
*/
|
||||
public function newNode()
|
||||
{
|
||||
return $this->hasOne(Node::class, 'id', 'new_node');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -102,7 +102,7 @@ class EggConfigurationService
|
|||
{
|
||||
// Get the legacy configuration structure for the server so that we
|
||||
// can property map the egg placeholders to values.
|
||||
$structure = $this->configurationStructureService->handle($server, true);
|
||||
$structure = $this->configurationStructureService->handle($server, [], true);
|
||||
|
||||
$response = [];
|
||||
// Normalize the output of the configuration for the new Wings Daemon to more
|
||||
|
|
|
@ -22,6 +22,11 @@ class NodeJWTService
|
|||
*/
|
||||
private $expiresAt;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $subject;
|
||||
|
||||
/**
|
||||
* Set the claims to include in this JWT.
|
||||
*
|
||||
|
@ -35,6 +40,10 @@ class NodeJWTService
|
|||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTimeInterface $date
|
||||
* @return $this
|
||||
*/
|
||||
public function setExpiresAt(DateTimeInterface $date)
|
||||
{
|
||||
$this->expiresAt = $date->getTimestamp();
|
||||
|
@ -42,20 +51,32 @@ class NodeJWTService
|
|||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $subject
|
||||
* @return $this
|
||||
*/
|
||||
public function setSubject(string $subject)
|
||||
{
|
||||
$this->subject = $subject;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new JWT for a given node.
|
||||
*
|
||||
* @param \Pterodactyl\Models\Node $node
|
||||
* @param string|null $identifiedBy
|
||||
* @param string $algo
|
||||
* @return \Lcobucci\JWT\Token
|
||||
*/
|
||||
public function handle(Node $node, string $identifiedBy)
|
||||
public function handle(Node $node, string $identifiedBy, string $algo = 'md5')
|
||||
{
|
||||
$signer = new Sha256;
|
||||
|
||||
$builder = (new Builder)->issuedBy(config('app.url'))
|
||||
->permittedFor($node->getConnectionAddress())
|
||||
->identifiedBy(md5($identifiedBy), true)
|
||||
->identifiedBy(hash($algo, $identifiedBy), true)
|
||||
->issuedAt(CarbonImmutable::now()->getTimestamp())
|
||||
->canOnlyBeUsedAfter(CarbonImmutable::now()->subMinutes(5)->getTimestamp());
|
||||
|
||||
|
@ -63,6 +84,10 @@ class NodeJWTService
|
|||
$builder = $builder->expiresAt($this->expiresAt);
|
||||
}
|
||||
|
||||
if (!empty($this->subject)) {
|
||||
$builder = $builder->relatedTo($this->subject, true);
|
||||
}
|
||||
|
||||
foreach ($this->claims as $key => $value) {
|
||||
$builder = $builder->withClaim($key, $value);
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ class GetUserPermissionsService
|
|||
if ($user->root_admin) {
|
||||
$permissions[] = 'admin.websocket.errors';
|
||||
$permissions[] = 'admin.websocket.install';
|
||||
$permissions[] = 'admin.websocket.transfer';
|
||||
}
|
||||
|
||||
return $permissions;
|
||||
|
|
|
@ -29,14 +29,25 @@ class ServerConfigurationStructureService
|
|||
* daemon, if you modify the structure eggs will break unexpectedly.
|
||||
*
|
||||
* @param \Pterodactyl\Models\Server $server
|
||||
* @param array $override
|
||||
* @param bool $legacy deprecated
|
||||
* @return array
|
||||
*/
|
||||
public function handle(Server $server, bool $legacy = false): array
|
||||
public function handle(Server $server, array $override = [], bool $legacy = false): array
|
||||
{
|
||||
return $legacy ?
|
||||
$this->returnLegacyFormat($server)
|
||||
: $this->returnCurrentFormat($server);
|
||||
$clone = $server;
|
||||
// If any overrides have been set on this call make sure to update them on the
|
||||
// cloned instance so that the configuration generated uses them.
|
||||
if (!empty($override)) {
|
||||
$clone = $server->fresh();
|
||||
foreach ($override as $key => $value) {
|
||||
$clone->setAttribute($key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
return $legacy
|
||||
? $this->returnLegacyFormat($clone)
|
||||
: $this->returnCurrentFormat($clone);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -105,12 +116,12 @@ class ServerConfigurationStructureService
|
|||
})->toArray(),
|
||||
'env' => $this->environment->handle($server),
|
||||
'oom_disabled' => $server->oom_disabled,
|
||||
'memory' => (int) $server->memory,
|
||||
'swap' => (int) $server->swap,
|
||||
'io' => (int) $server->io,
|
||||
'cpu' => (int) $server->cpu,
|
||||
'memory' => (int)$server->memory,
|
||||
'swap' => (int)$server->swap,
|
||||
'io' => (int)$server->io,
|
||||
'cpu' => (int)$server->cpu,
|
||||
'threads' => $server->threads,
|
||||
'disk' => (int) $server->disk,
|
||||
'disk' => (int)$server->disk,
|
||||
'image' => $server->image,
|
||||
],
|
||||
'service' => [
|
||||
|
@ -118,7 +129,7 @@ class ServerConfigurationStructureService
|
|||
'skip_scripts' => $server->skip_scripts,
|
||||
],
|
||||
'rebuild' => false,
|
||||
'suspended' => (int) $server->suspended,
|
||||
'suspended' => (int)$server->suspended,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@ use Webmozart\Assert\Assert;
|
|||
use Pterodactyl\Models\Server;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
|
||||
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
|
||||
use Pterodactyl\Exceptions\Http\Server\ServerTransferringException;
|
||||
|
||||
class SuspensionService
|
||||
{
|
||||
|
@ -56,12 +58,20 @@ class SuspensionService
|
|||
return;
|
||||
}
|
||||
|
||||
// Check if the server is currently being transferred.
|
||||
if (! is_null($server->transfer)) {
|
||||
throw new ServerTransferringException;
|
||||
}
|
||||
|
||||
$this->connection->transaction(function () use ($action, $server) {
|
||||
$server->update([
|
||||
'suspended' => $action === self::ACTION_SUSPEND,
|
||||
]);
|
||||
|
||||
$this->daemonServerRepository->setServer($server)->suspend($action === self::ACTION_UNSUSPEND);
|
||||
// Only send the suspension request to wings if the server is not currently being transferred.
|
||||
if (is_null($server->transfer)) {
|
||||
$this->daemonServerRepository->setServer($server)->suspend($action === self::ACTION_UNSUSPEND);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ class ServerTransformer extends BaseTransformer
|
|||
'location',
|
||||
'node',
|
||||
'databases',
|
||||
'transfer',
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -55,8 +56,6 @@ class ServerTransformer extends BaseTransformer
|
|||
*
|
||||
* @param \Pterodactyl\Models\Server $server
|
||||
* @return array
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
public function transform(Server $server): array
|
||||
{
|
||||
|
|
|
@ -72,6 +72,7 @@ class ServerTransformer extends BaseClientTransformer
|
|||
],
|
||||
'is_suspended' => $server->suspended,
|
||||
'is_installing' => $server->installed !== 1,
|
||||
'is_transferring' => ! is_null($server->transfer),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class MakeSuccessfulNullableInServerTransfers extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('server_transfers', function (Blueprint $table) {
|
||||
$table->boolean('successful')->nullable()->default(null)->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('server_transfers', function (Blueprint $table) {
|
||||
$table->boolean('successful')->default(0)->change();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddArchivedFieldToServerTransfersTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('server_transfers', function (Blueprint $table) {
|
||||
$table->boolean('archived')->default(0)->after('new_additional_allocations');
|
||||
});
|
||||
|
||||
// Update archived to all be true on existing transfers.
|
||||
Schema::table('server_transfers', function (Blueprint $table) {
|
||||
DB::statement('UPDATE `server_transfers` SET `archived` = 1 WHERE `successful` = 1');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('server_transfers', function (Blueprint $table) {
|
||||
$table->dropColumn('archived');
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class MakeAllocationFieldsJson extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('server_transfers', function (Blueprint $table) {
|
||||
$table->json('old_additional_allocations')->nullable()->change();
|
||||
$table->json('new_additional_allocations')->nullable()->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('server_transfers', function (Blueprint $table) {
|
||||
$table->string('old_additional_allocations')->nullable()->change();
|
||||
$table->string('new_additional_allocations')->nullable()->change();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -40,6 +40,7 @@ export interface Server {
|
|||
};
|
||||
isSuspended: boolean;
|
||||
isInstalling: boolean;
|
||||
isTransferring: boolean;
|
||||
variables: ServerEggVariable[];
|
||||
allocations: Allocation[];
|
||||
}
|
||||
|
@ -62,6 +63,7 @@ export const rawDataToServerObject = ({ attributes: data }: FractalResponseData)
|
|||
featureLimits: { ...data.feature_limits },
|
||||
isSuspended: data.is_suspended,
|
||||
isInstalling: data.is_installing,
|
||||
isTransferring: data.is_transferring,
|
||||
variables: ((data.relationships?.variables as FractalResponseList | undefined)?.data || []).map(rawDataToServerEggVariable),
|
||||
allocations: ((data.relationships?.allocations as FractalResponseList | undefined)?.data || []).map(rawDataToServerAllocation),
|
||||
});
|
||||
|
|
|
@ -26,14 +26,14 @@ const IconDescription = styled.p<{ $alarm: boolean }>`
|
|||
|
||||
const StatusIndicatorBox = styled(GreyRowBox)<{ $status: ServerPowerState | undefined }>`
|
||||
${tw`grid grid-cols-12 gap-4 relative`};
|
||||
|
||||
|
||||
& .status-bar {
|
||||
${tw`w-2 bg-red-500 absolute right-0 z-20 rounded-full m-1 opacity-50 transition-all duration-150`};
|
||||
height: calc(100% - 0.5rem);
|
||||
|
||||
|
||||
${({ $status }) => (!$status || $status === 'offline') ? tw`bg-red-500` : ($status === 'running' ? tw`bg-green-500` : tw`bg-yellow-500`)};
|
||||
}
|
||||
|
||||
|
||||
&:hover .status-bar {
|
||||
${tw`opacity-75`};
|
||||
}
|
||||
|
@ -74,8 +74,8 @@ export default ({ server, className }: { server: Server; className?: string }) =
|
|||
alarms.disk = server.limits.disk === 0 ? false : isAlarmState(stats.diskUsageInBytes, server.limits.disk);
|
||||
}
|
||||
|
||||
const disklimit = server.limits.disk !== 0 ? megabytesToHuman(server.limits.disk) : 'Unlimited';
|
||||
const memorylimit = server.limits.memory !== 0 ? megabytesToHuman(server.limits.memory) : 'Unlimited';
|
||||
const diskLimit = server.limits.disk !== 0 ? megabytesToHuman(server.limits.disk) : 'Unlimited';
|
||||
const memoryLimit = server.limits.memory !== 0 ? megabytesToHuman(server.limits.memory) : 'Unlimited';
|
||||
|
||||
return (
|
||||
<StatusIndicatorBox as={Link} to={`/server/${server.id}`} className={className} $status={stats?.status}>
|
||||
|
@ -118,7 +118,14 @@ export default ({ server, className }: { server: Server; className?: string }) =
|
|||
</span>
|
||||
</div>
|
||||
:
|
||||
<Spinner size={'small'}/>
|
||||
server.isTransferring ?
|
||||
<div css={tw`flex-1 text-center`}>
|
||||
<span css={tw`bg-neutral-500 rounded px-2 py-1 text-neutral-100 text-xs`}>
|
||||
Transferring
|
||||
</span>
|
||||
</div>
|
||||
:
|
||||
<Spinner size={'small'}/>
|
||||
:
|
||||
<React.Fragment>
|
||||
<div css={tw`flex-1 flex md:ml-4 sm:flex hidden justify-center`}>
|
||||
|
@ -134,7 +141,7 @@ export default ({ server, className }: { server: Server; className?: string }) =
|
|||
{bytesToHuman(stats.memoryUsageInBytes)}
|
||||
</IconDescription>
|
||||
</div>
|
||||
<p css={tw`text-xs text-neutral-600 text-center mt-1`}>of {memorylimit}</p>
|
||||
<p css={tw`text-xs text-neutral-600 text-center mt-1`}>of {memoryLimit}</p>
|
||||
</div>
|
||||
<div css={tw`flex-1 ml-4 sm:block hidden`}>
|
||||
<div css={tw`flex justify-center`}>
|
||||
|
@ -143,7 +150,7 @@ export default ({ server, className }: { server: Server; className?: string }) =
|
|||
{bytesToHuman(stats.diskUsageInBytes)}
|
||||
</IconDescription>
|
||||
</div>
|
||||
<p css={tw`text-xs text-neutral-600 text-center mt-1`}>of {disklimit}</p>
|
||||
<p css={tw`text-xs text-neutral-600 text-center mt-1`}>of {diskLimit}</p>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@ export default () => {
|
|||
}, [ progress, continuous ]);
|
||||
|
||||
return (
|
||||
<div className={'w-full fixed'} style={{ height: '2px' }}>
|
||||
<div css={tw`w-full fixed`} style={{ height: '2px' }}>
|
||||
<CSSTransition
|
||||
timeout={150}
|
||||
appear
|
||||
|
|
|
@ -67,6 +67,7 @@ export default () => {
|
|||
const { connected, instance } = ServerContext.useStoreState(state => state.socket);
|
||||
const [ canSendCommands ] = usePermissions([ 'control.console' ]);
|
||||
const serverId = ServerContext.useStoreState(state => state.server.data!.id);
|
||||
const isTransferring = ServerContext.useStoreState(state => state.server.data!.isTransferring);
|
||||
const [ history, setHistory ] = usePersistedState<string[]>(`${serverId}:command_history`, []);
|
||||
const [ historyIndex, setHistoryIndex ] = useState(-1);
|
||||
|
||||
|
@ -74,6 +75,19 @@ export default () => {
|
|||
(prelude ? TERMINAL_PRELUDE : '') + line.replace(/(?:\r\n|\r|\n)$/im, '') + '\u001b[0m',
|
||||
);
|
||||
|
||||
const handleTransferStatus = (status: string) => {
|
||||
switch (status) {
|
||||
// Sent by either the source or target node if a failure occurs.
|
||||
case 'failure':
|
||||
terminal.writeln(TERMINAL_PRELUDE + 'Transfer has failed.\u001b[0m');
|
||||
return;
|
||||
|
||||
// Sent by the source node whenever the server was archived successfully.
|
||||
case 'archive':
|
||||
terminal.writeln(TERMINAL_PRELUDE + 'Server has been archived successfully, attempting connection to target node..\u001b[0m');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDaemonErrorOutput = (line: string) => terminal.writeln(
|
||||
TERMINAL_PRELUDE + '\u001b[1m\u001b[41m' + line.replace(/(?:\r\n|\r|\n)$/im, '') + '\u001b[0m',
|
||||
);
|
||||
|
@ -122,20 +136,23 @@ export default () => {
|
|||
|
||||
// Add support for capturing keys
|
||||
terminal.attachCustomKeyEventHandler((e: KeyboardEvent) => {
|
||||
// Ctrl + C ( Copy )
|
||||
// Ctrl + C (Copy)
|
||||
if (e.ctrlKey && e.key === 'c') {
|
||||
document.execCommand('copy');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ctrl + F (Find)
|
||||
if (e.ctrlKey && e.key === 'f') {
|
||||
searchBar.show();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Escape
|
||||
if (e.key === 'Escape') {
|
||||
searchBar.hidden();
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
@ -149,22 +166,29 @@ export default () => {
|
|||
|
||||
useEffect(() => {
|
||||
if (connected && instance) {
|
||||
terminal.clear();
|
||||
// Do not clear the console if the server is being transferred.
|
||||
if (!isTransferring) {
|
||||
terminal.clear();
|
||||
}
|
||||
|
||||
instance.addListener('status', handlePowerChangeEvent);
|
||||
instance.addListener('console output', handleConsoleOutput);
|
||||
instance.addListener('install output', handleConsoleOutput);
|
||||
instance.addListener('transfer logs', handleConsoleOutput);
|
||||
instance.addListener('transfer status', handleTransferStatus);
|
||||
instance.addListener('daemon message', line => handleConsoleOutput(line, true));
|
||||
instance.addListener('daemon error', handleDaemonErrorOutput);
|
||||
instance.send('send logs');
|
||||
}
|
||||
|
||||
return () => {
|
||||
instance && instance.removeListener('console output', handleConsoleOutput)
|
||||
instance && instance.removeListener('status', handlePowerChangeEvent)
|
||||
.removeListener('console output', handleConsoleOutput)
|
||||
.removeListener('install output', handleConsoleOutput)
|
||||
.removeListener('transfer logs', handleConsoleOutput)
|
||||
.removeListener('transfer status', handleTransferStatus)
|
||||
.removeListener('daemon message', line => handleConsoleOutput(line, true))
|
||||
.removeListener('daemon error', handleDaemonErrorOutput)
|
||||
.removeListener('status', handlePowerChangeEvent);
|
||||
.removeListener('daemon error', handleDaemonErrorOutput);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ connected, instance ]);
|
||||
|
|
|
@ -17,6 +17,7 @@ const ChunkedStatGraphs = lazy(() => import(/* webpackChunkName: "graphs" */'@/c
|
|||
|
||||
const ServerConsole = () => {
|
||||
const isInstalling = ServerContext.useStoreState(state => state.server.data!.isInstalling);
|
||||
const isTransferring = ServerContext.useStoreState(state => state.server.data!.isTransferring);
|
||||
// @ts-ignore
|
||||
const eggFeatures: string[] = ServerContext.useStoreState(state => state.server.data!.eggFeatures, isEqual);
|
||||
|
||||
|
@ -24,11 +25,7 @@ const ServerConsole = () => {
|
|||
<ServerContentBlock title={'Console'} css={tw`flex flex-wrap`}>
|
||||
<div css={tw`w-full lg:w-1/4`}>
|
||||
<ServerDetailsBlock/>
|
||||
{!isInstalling ?
|
||||
<Can action={[ 'control.start', 'control.stop', 'control.restart' ]} matchAny>
|
||||
<PowerControls/>
|
||||
</Can>
|
||||
:
|
||||
{isInstalling ?
|
||||
<div css={tw`mt-4 rounded bg-yellow-500 p-3`}>
|
||||
<ContentContainer>
|
||||
<p css={tw`text-sm text-yellow-900`}>
|
||||
|
@ -37,6 +34,20 @@ const ServerConsole = () => {
|
|||
</p>
|
||||
</ContentContainer>
|
||||
</div>
|
||||
:
|
||||
isTransferring ?
|
||||
<div css={tw`mt-4 rounded bg-yellow-500 p-3`}>
|
||||
<ContentContainer>
|
||||
<p css={tw`text-sm text-yellow-900`}>
|
||||
This server is currently being transferred to another node and all actions
|
||||
are unavailable.
|
||||
</p>
|
||||
</ContentContainer>
|
||||
</div>
|
||||
:
|
||||
<Can action={[ 'control.start', 'control.stop', 'control.restart' ]} matchAny>
|
||||
<PowerControls/>
|
||||
</Can>
|
||||
}
|
||||
</div>
|
||||
<div css={tw`w-full lg:w-3/4 mt-4 lg:mt-0 lg:pl-4`}>
|
||||
|
|
|
@ -65,13 +65,14 @@ const ServerDetailsBlock = () => {
|
|||
|
||||
const name = ServerContext.useStoreState(state => state.server.data!.name);
|
||||
const isInstalling = ServerContext.useStoreState(state => state.server.data!.isInstalling);
|
||||
const isTransferring = ServerContext.useStoreState(state => state.server.data!.isTransferring);
|
||||
const limits = ServerContext.useStoreState(state => state.server.data!.limits);
|
||||
const primaryAllocation = ServerContext.useStoreState(state => state.server.data!.allocations.filter(alloc => alloc.isDefault).map(
|
||||
allocation => (allocation.alias || allocation.ip) + ':' + allocation.port
|
||||
)).toString();
|
||||
|
||||
const disklimit = limits.disk ? megabytesToHuman(limits.disk) : 'Unlimited';
|
||||
const memorylimit = limits.memory ? megabytesToHuman(limits.memory) : 'Unlimited';
|
||||
const diskLimit = limits.disk ? megabytesToHuman(limits.disk) : 'Unlimited';
|
||||
const memoryLimit = limits.memory ? megabytesToHuman(limits.memory) : 'Unlimited';
|
||||
|
||||
return (
|
||||
<TitledGreyBox css={tw`break-words`} title={name} icon={faServer}>
|
||||
|
@ -81,10 +82,10 @@ const ServerDetailsBlock = () => {
|
|||
fixedWidth
|
||||
css={[
|
||||
tw`mr-1`,
|
||||
statusToColor(status, isInstalling),
|
||||
statusToColor(status, isInstalling || isTransferring),
|
||||
]}
|
||||
/>
|
||||
{!status ? 'Connecting...' : (isInstalling ? 'Installing' : status)}
|
||||
{!status ? 'Connecting...' : (isInstalling ? 'Installing' : (isTransferring) ? 'Transferring' : status)}
|
||||
</p>
|
||||
<CopyOnClick text={primaryAllocation}>
|
||||
<p css={tw`text-xs mt-2`}>
|
||||
|
@ -97,11 +98,11 @@ const ServerDetailsBlock = () => {
|
|||
</p>
|
||||
<p css={tw`text-xs mt-2`}>
|
||||
<FontAwesomeIcon icon={faMemory} fixedWidth css={tw`mr-1`}/> {bytesToHuman(stats.memory)}
|
||||
<span css={tw`text-neutral-500`}> / {memorylimit}</span>
|
||||
<span css={tw`text-neutral-500`}> / {memoryLimit}</span>
|
||||
</p>
|
||||
<p css={tw`text-xs mt-2`}>
|
||||
<FontAwesomeIcon icon={faHdd} fixedWidth css={tw`mr-1`}/> {bytesToHuman(stats.disk)}
|
||||
<span css={tw`text-neutral-500`}> / {disklimit}</span>
|
||||
<span css={tw`text-neutral-500`}> / {diskLimit}</span>
|
||||
</p>
|
||||
</TitledGreyBox>
|
||||
);
|
||||
|
|
32
resources/scripts/components/server/TransferListener.tsx
Normal file
32
resources/scripts/components/server/TransferListener.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
import useWebsocketEvent from '@/plugins/useWebsocketEvent';
|
||||
import { ServerContext } from '@/state/server';
|
||||
|
||||
const TransferListener = () => {
|
||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||
const getServer = ServerContext.useStoreActions(actions => actions.server.getServer);
|
||||
const setServerFromState = ServerContext.useStoreActions(actions => actions.server.setServerFromState);
|
||||
|
||||
// Listen for the transfer status event so we can update the state of the server.
|
||||
useWebsocketEvent('transfer status', (status: string) => {
|
||||
if (status === 'starting') {
|
||||
setServerFromState(s => ({ ...s, isTransferring: true }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (status === 'failure') {
|
||||
setServerFromState(s => ({ ...s, isTransferring: false }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (status !== 'success') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Refresh the server's information as it's node and allocations were just updated.
|
||||
getServer(uuid).catch(error => console.error(error));
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default TransferListener;
|
|
@ -20,6 +20,59 @@ export default () => {
|
|||
const setServerStatus = ServerContext.useStoreActions(actions => actions.status.setServerStatus);
|
||||
const { setInstance, setConnectionState } = ServerContext.useStoreActions(actions => actions.socket);
|
||||
|
||||
const connect = (uuid: string) => {
|
||||
const socket = new Websocket();
|
||||
|
||||
socket.on('auth success', () => setConnectionState(true));
|
||||
socket.on('SOCKET_CLOSE', () => setConnectionState(false));
|
||||
socket.on('SOCKET_ERROR', () => {
|
||||
setError('connecting');
|
||||
setConnectionState(false);
|
||||
});
|
||||
socket.on('status', (status) => setServerStatus(status));
|
||||
|
||||
socket.on('daemon error', message => {
|
||||
console.warn('Got error message from daemon socket:', message);
|
||||
});
|
||||
|
||||
socket.on('token expiring', () => updateToken(uuid, socket));
|
||||
socket.on('token expired', () => updateToken(uuid, socket));
|
||||
socket.on('jwt error', (error: string) => {
|
||||
setConnectionState(false);
|
||||
console.warn('JWT validation error from wings:', error);
|
||||
|
||||
if (reconnectErrors.find(v => error.toLowerCase().indexOf(v) >= 0)) {
|
||||
updateToken(uuid, socket);
|
||||
} else {
|
||||
setError('There was an error validating the credentials provided for the websocket. Please refresh the page.');
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('transfer status', (status: string) => {
|
||||
if (status === 'starting' || status === 'success') {
|
||||
return;
|
||||
}
|
||||
|
||||
// This code forces a reconnection to the websocket which will connect us to the target node instead of the source node
|
||||
// in order to be able to receive transfer logs from the target node.
|
||||
socket.close();
|
||||
setError('connecting');
|
||||
setConnectionState(false);
|
||||
setInstance(null);
|
||||
connect(uuid);
|
||||
});
|
||||
|
||||
getWebsocketToken(uuid)
|
||||
.then(data => {
|
||||
// Connect and then set the authentication token.
|
||||
socket.setToken(data.token).connect(data.socket);
|
||||
|
||||
// Once that is done, set the instance.
|
||||
setInstance(socket);
|
||||
})
|
||||
.catch(error => console.error(error));
|
||||
};
|
||||
|
||||
const updateToken = (uuid: string, socket: Websocket) => {
|
||||
if (updatingToken) return;
|
||||
|
||||
|
@ -49,42 +102,7 @@ export default () => {
|
|||
return;
|
||||
}
|
||||
|
||||
const socket = new Websocket();
|
||||
|
||||
socket.on('auth success', () => setConnectionState(true));
|
||||
socket.on('SOCKET_CLOSE', () => setConnectionState(false));
|
||||
socket.on('SOCKET_ERROR', () => {
|
||||
setError('connecting');
|
||||
setConnectionState(false);
|
||||
});
|
||||
socket.on('status', (status) => setServerStatus(status));
|
||||
|
||||
socket.on('daemon error', message => {
|
||||
console.warn('Got error message from daemon socket:', message);
|
||||
});
|
||||
|
||||
socket.on('token expiring', () => updateToken(uuid, socket));
|
||||
socket.on('token expired', () => updateToken(uuid, socket));
|
||||
socket.on('jwt error', (error: string) => {
|
||||
setConnectionState(false);
|
||||
console.warn('JWT validation error from wings:', error);
|
||||
|
||||
if (reconnectErrors.find(v => error.toLowerCase().indexOf(v) >= 0)) {
|
||||
updateToken(uuid, socket);
|
||||
} else {
|
||||
setError('There was an error validating the credentials provided for the websocket. Please refresh the page.');
|
||||
}
|
||||
});
|
||||
|
||||
getWebsocketToken(uuid)
|
||||
.then(data => {
|
||||
// Connect and then set the authentication token.
|
||||
socket.setToken(data.token).connect(data.socket);
|
||||
|
||||
// Once that is done, set the instance.
|
||||
setInstance(socket);
|
||||
})
|
||||
.catch(error => console.error(error));
|
||||
connect(uuid);
|
||||
}, [ uuid ]);
|
||||
|
||||
return (
|
||||
|
|
|
@ -42,7 +42,6 @@ export default () => {
|
|||
setVisible(false);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
addError({ key: 'database:create', message: httpErrorToHuman(error) });
|
||||
setSubmitting(false);
|
||||
});
|
||||
|
|
|
@ -65,7 +65,7 @@ export default () => {
|
|||
<FileActionCheckbox
|
||||
type={'checkbox'}
|
||||
css={tw`mx-4`}
|
||||
checked={selectedFilesLength === (files ? files.length : -1)}
|
||||
checked={selectedFilesLength === (files?.length === 0 ? -1 : files?.length)}
|
||||
onChange={onSelectAllClick}
|
||||
/>
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import TransferListener from '@/components/server/TransferListener';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { NavLink, Route, RouteComponentProps, Switch } from 'react-router-dom';
|
||||
import NavigationBar from '@/components/NavigationBar';
|
||||
|
@ -35,10 +36,12 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
|
|||
const rootAdmin = useStoreState(state => state.user.data!.rootAdmin);
|
||||
const [ error, setError ] = useState('');
|
||||
const [ installing, setInstalling ] = useState(false);
|
||||
const [ transferring, setTransferring ] = useState(false);
|
||||
|
||||
const id = ServerContext.useStoreState(state => state.server.data?.id);
|
||||
const uuid = ServerContext.useStoreState(state => state.server.data?.uuid);
|
||||
const isInstalling = ServerContext.useStoreState(state => state.server.data?.isInstalling);
|
||||
const isTransferring = ServerContext.useStoreState(state => state.server.data?.isTransferring);
|
||||
const serverId = ServerContext.useStoreState(state => state.server.data?.internalId);
|
||||
const getServer = ServerContext.useStoreActions(actions => actions.server.getServer);
|
||||
const clearServerState = ServerContext.useStoreActions(actions => actions.clearServerState);
|
||||
|
@ -51,13 +54,23 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
|
|||
setInstalling(!!isInstalling);
|
||||
}, [ isInstalling ]);
|
||||
|
||||
useEffect(() => {
|
||||
setTransferring(!!isTransferring);
|
||||
}, [ isTransferring ]);
|
||||
|
||||
useEffect(() => {
|
||||
setError('');
|
||||
setInstalling(false);
|
||||
setTransferring(false);
|
||||
|
||||
getServer(match.params.id)
|
||||
.catch(error => {
|
||||
if (error.response?.status === 409) {
|
||||
setInstalling(true);
|
||||
if (error.response.data?.errors[0]?.code === 'ServerTransferringException') {
|
||||
setTransferring(true);
|
||||
} else {
|
||||
setInstalling(true);
|
||||
}
|
||||
} else {
|
||||
console.error(error);
|
||||
setError(httpErrorToHuman(error));
|
||||
|
@ -116,10 +129,11 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
|
|||
</SubNavigation>
|
||||
</CSSTransition>
|
||||
<InstallListener/>
|
||||
<TransferListener/>
|
||||
<WebsocketHandler/>
|
||||
{(installing && (!rootAdmin || (rootAdmin && !location.pathname.endsWith(`/server/${id}`)))) ?
|
||||
{((installing || transferring) && (!rootAdmin || (rootAdmin && !location.pathname.endsWith(`/server/${id}`)))) ?
|
||||
<ScreenBlock
|
||||
title={'Your server is installing.'}
|
||||
title={installing ? 'Your server is installing.' : 'Your server is currently being transferred.'}
|
||||
image={'/assets/svgs/server_installing.svg'}
|
||||
message={'Please check back in a few minutes.'}
|
||||
/>
|
||||
|
|
|
@ -58,6 +58,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(! $server->suspended)
|
||||
<div class="col-sm-4">
|
||||
<div class="box box-warning">
|
||||
|
@ -71,7 +72,7 @@
|
|||
<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>
|
||||
<button type="submit" class="btn btn-warning @if(! is_null($server->transfer)) disabled @endif">Suspend Server</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -96,28 +97,48 @@
|
|||
</div>
|
||||
@endif
|
||||
|
||||
<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>
|
||||
@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 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>
|
||||
</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">
|
||||
|
|
Loading…
Reference in a new issue