Update logic for tracking a server's transfer state
This commit is contained in:
parent
5d03c0d2e5
commit
e6c4a68e4a
20 changed files with 206 additions and 74 deletions
|
@ -117,7 +117,7 @@ class ServerTransferController extends Controller
|
||||||
$this->daemonConfigurationRepository->setNode($node)->getSystemInformation();
|
$this->daemonConfigurationRepository->setNode($node)->getSystemInformation();
|
||||||
|
|
||||||
// Suspend the server and request an archive to be created.
|
// Suspend the server and request an archive to be created.
|
||||||
$this->suspensionService->toggle($server, 'suspend');
|
//$this->suspensionService->toggle($server, 'suspend');
|
||||||
|
|
||||||
// Create a new ServerTransfer entry.
|
// Create a new ServerTransfer entry.
|
||||||
$transfer = new ServerTransfer;
|
$transfer = new ServerTransfer;
|
||||||
|
|
|
@ -120,9 +120,12 @@ class ServerTransferController extends Controller
|
||||||
|
|
||||||
// Unsuspend the server and don't continue the transfer.
|
// Unsuspend the server and don't continue the transfer.
|
||||||
if (! $request->input('successful')) {
|
if (! $request->input('successful')) {
|
||||||
$this->suspensionService->toggle($server, 'unsuspend');
|
//$this->suspensionService->toggle($server, 'unsuspend');
|
||||||
|
$server->transfer->forceFill([
|
||||||
|
'successful' => false,
|
||||||
|
])->saveOrFail();
|
||||||
|
|
||||||
return JsonResponse::create([], Response::HTTP_NO_CONTENT);
|
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
$server->node_id = $server->transfer->new_node;
|
$server->node_id = $server->transfer->new_node;
|
||||||
|
@ -151,21 +154,23 @@ class ServerTransferController extends Controller
|
||||||
// because setServer() tells the repository to use the server's node and not the one
|
// because setServer() tells the repository to use the server's node and not the one
|
||||||
// we want to specify.
|
// we want to specify.
|
||||||
try {
|
try {
|
||||||
|
/** @var \Pterodactyl\Models\Node $newNode */
|
||||||
|
$newNode = $this->nodeRepository->find($server->transfer->new_node);
|
||||||
|
|
||||||
$this->daemonTransferRepository
|
$this->daemonTransferRepository
|
||||||
->setServer($server)
|
->setServer($server)
|
||||||
->setNode($this->nodeRepository->find($server->transfer->new_node))
|
->setNode($newNode)
|
||||||
->notify($server, $data, $server->node, $token->__toString());
|
->notify($server, $data, $server->node, $token->__toString());
|
||||||
} catch (DaemonConnectionException $exception) {
|
} catch (DaemonConnectionException $exception) {
|
||||||
throw $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.
|
* The daemon notifies us about a transfer failure.
|
||||||
*
|
*
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
* @param string $uuid
|
* @param string $uuid
|
||||||
* @return \Illuminate\Http\JsonResponse
|
* @return \Illuminate\Http\JsonResponse
|
||||||
*
|
*
|
||||||
|
@ -183,9 +188,9 @@ class ServerTransferController extends Controller
|
||||||
$this->allocationRepository->updateWhereIn('id', $allocationIds, ['server_id' => null]);
|
$this->allocationRepository->updateWhereIn('id', $allocationIds, ['server_id' => null]);
|
||||||
|
|
||||||
// Unsuspend the server.
|
// Unsuspend the server.
|
||||||
$this->suspensionService->toggle($server, 'unsuspend');
|
//$this->suspensionService->toggle($server, 'unsuspend');
|
||||||
|
|
||||||
return JsonResponse::create([], Response::HTTP_NO_CONTENT);
|
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -213,11 +218,11 @@ class ServerTransferController extends Controller
|
||||||
// Update the server's allocation_id and node_id.
|
// Update the server's allocation_id and node_id.
|
||||||
$server->allocation_id = $transfer->new_allocation;
|
$server->allocation_id = $transfer->new_allocation;
|
||||||
$server->node_id = $transfer->new_node;
|
$server->node_id = $transfer->new_node;
|
||||||
$server->save();
|
$server->saveOrFail();
|
||||||
|
|
||||||
// Mark the transfer as successful.
|
// Mark the transfer as successful.
|
||||||
$transfer->successful = true;
|
$transfer->successful = true;
|
||||||
$transfer->save();
|
$transfer->saveOrFail();
|
||||||
|
|
||||||
// Commit the transaction.
|
// Commit the transaction.
|
||||||
$this->connection->commit();
|
$this->connection->commit();
|
||||||
|
@ -231,8 +236,8 @@ class ServerTransferController extends Controller
|
||||||
|
|
||||||
// Unsuspend the server
|
// Unsuspend the server
|
||||||
$server->load('node');
|
$server->load('node');
|
||||||
$this->suspensionService->toggle($server, $this->suspensionService::ACTION_UNSUSPEND);
|
//$this->suspensionService->toggle($server, $this->suspensionService::ACTION_UNSUSPEND);
|
||||||
|
|
||||||
return JsonResponse::create([], Response::HTTP_NO_CONTENT);
|
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,13 +112,13 @@ class SftpAuthenticationController extends Controller
|
||||||
|
|
||||||
// Remeber, for security purposes, only reveal the existence of the server to people that
|
// Remeber, for security purposes, only reveal the existence of the server to people that
|
||||||
// have provided valid credentials, and have permissions to know about it.
|
// have provided valid credentials, and have permissions to know about it.
|
||||||
if ($server->installed !== 1 || $server->suspended) {
|
if ($server->installed !== 1 || $server->suspended || $server->transfer !== null) {
|
||||||
throw new BadRequestHttpException(
|
throw new BadRequestHttpException(
|
||||||
'Server is not installed or is currently suspended.'
|
'Server is not installed or is currently suspended.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return JsonResponse::create([
|
return new JsonResponse([
|
||||||
'server' => $server->uuid,
|
'server' => $server->uuid,
|
||||||
// Deprecated, but still needed at the moment for Wings.
|
// Deprecated, but still needed at the moment for Wings.
|
||||||
'token' => '',
|
'token' => '',
|
||||||
|
|
|
@ -9,7 +9,6 @@ use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
|
||||||
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
|
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
|
||||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
|
||||||
|
|
||||||
class AuthenticateServerAccess
|
class AuthenticateServerAccess
|
||||||
{
|
{
|
||||||
|
@ -24,7 +23,6 @@ class AuthenticateServerAccess
|
||||||
* @var string[]
|
* @var string[]
|
||||||
*/
|
*/
|
||||||
protected $except = [
|
protected $except = [
|
||||||
'api:client:server.view',
|
|
||||||
'api:client:server.ws',
|
'api:client:server.ws',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -65,17 +63,26 @@ class AuthenticateServerAccess
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($server->suspended && !$request->routeIs('api:client:server.resources')) {
|
if ($server->suspended && ! $request->routeIs('api:client:server.resources')) {
|
||||||
throw new BadRequestHttpException(
|
throw new BadRequestHttpException(
|
||||||
'This server is currently suspended and the functionality requested is unavailable.'
|
'This server is currently suspended and the functionality requested is unavailable.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $server->isInstalled()) {
|
// Still allow users to get information about there server if it is installing or being transferred.
|
||||||
// Throw an exception for all server routes; however if the user is an admin and requesting the
|
if (! $request->routeIs('api:client:server.view')) {
|
||||||
// server details, don't throw the exception for them.
|
if (! $server->isInstalled()) {
|
||||||
if (! $user->root_admin || ($user->root_admin && ! $request->routeIs($this->except))) {
|
// Throw an exception for all server routes; however if the user is an admin and requesting the
|
||||||
throw new ConflictHttpException('Server has not completed the installation process.');
|
// 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 ($server->transfer !== null) {
|
||||||
|
if (! $user->root_admin || ($user->root_admin && ! $request->routeIs($this->except))) {
|
||||||
|
throw new ConflictHttpException('Server is currently being transferred.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -80,6 +80,14 @@ class AccessingValidServer
|
||||||
return $this->response->view('errors.installing', [], 409);
|
return $this->response->view('errors.installing', [], 409);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($server->transfer !== null) {
|
||||||
|
if ($isApiRequest) {
|
||||||
|
throw new ConflictHttpException('Server is currently being transferred.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->response->view('errors.transferring', [], 409);
|
||||||
|
}
|
||||||
|
|
||||||
// Add server to the request attributes. This will replace sessions
|
// Add server to the request attributes. This will replace sessions
|
||||||
// as files are updated.
|
// as files are updated.
|
||||||
$request->attributes->set('server', $server);
|
$request->attributes->set('server', $server);
|
||||||
|
|
|
@ -306,7 +306,7 @@ class Server extends Model
|
||||||
*/
|
*/
|
||||||
public function transfer()
|
public function transfer()
|
||||||
{
|
{
|
||||||
return $this->hasOne(ServerTransfer::class)->orderByDesc('id');
|
return $this->hasOne(ServerTransfer::class)->whereNull('successful')->orderByDesc('id');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -11,7 +11,7 @@ namespace Pterodactyl\Models;
|
||||||
* @property int $new_allocation
|
* @property int $new_allocation
|
||||||
* @property string $old_additional_allocations
|
* @property string $old_additional_allocations
|
||||||
* @property string $new_additional_allocations
|
* @property string $new_additional_allocations
|
||||||
* @property bool $successful
|
* @property bool|null $successful
|
||||||
* @property \Carbon\Carbon $created_at
|
* @property \Carbon\Carbon $created_at
|
||||||
* @property \Carbon\Carbon $updated_at
|
* @property \Carbon\Carbon $updated_at
|
||||||
*
|
*
|
||||||
|
@ -66,7 +66,7 @@ class ServerTransfer extends Model
|
||||||
'new_allocation' => 'required|numeric',
|
'new_allocation' => 'required|numeric',
|
||||||
'old_additional_allocations' => 'nullable',
|
'old_additional_allocations' => 'nullable',
|
||||||
'new_additional_allocations' => 'nullable',
|
'new_additional_allocations' => 'nullable',
|
||||||
'successful' => 'sometimes|boolean',
|
'successful' => 'sometimes|nullable|boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -24,6 +24,7 @@ class GetUserPermissionsService
|
||||||
if ($user->root_admin) {
|
if ($user->root_admin) {
|
||||||
$permissions[] = 'admin.websocket.errors';
|
$permissions[] = 'admin.websocket.errors';
|
||||||
$permissions[] = 'admin.websocket.install';
|
$permissions[] = 'admin.websocket.install';
|
||||||
|
$permissions[] = 'admin.websocket.transfer';
|
||||||
}
|
}
|
||||||
|
|
||||||
return $permissions;
|
return $permissions;
|
||||||
|
|
|
@ -61,7 +61,10 @@ class SuspensionService
|
||||||
'suspended' => $action === self::ACTION_SUSPEND,
|
'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 ($server->transfer === null) {
|
||||||
|
$this->daemonServerRepository->setServer($server)->suspend($action === self::ACTION_UNSUSPEND);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ class ServerTransformer extends BaseTransformer
|
||||||
'location',
|
'location',
|
||||||
'node',
|
'node',
|
||||||
'databases',
|
'databases',
|
||||||
|
'transfer',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -55,8 +56,6 @@ class ServerTransformer extends BaseTransformer
|
||||||
*
|
*
|
||||||
* @param \Pterodactyl\Models\Server $server
|
* @param \Pterodactyl\Models\Server $server
|
||||||
* @return array
|
* @return array
|
||||||
*
|
|
||||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
|
||||||
*/
|
*/
|
||||||
public function transform(Server $server): array
|
public function transform(Server $server): array
|
||||||
{
|
{
|
||||||
|
|
|
@ -72,6 +72,7 @@ class ServerTransformer extends BaseClientTransformer
|
||||||
],
|
],
|
||||||
'is_suspended' => $server->suspended,
|
'is_suspended' => $server->suspended,
|
||||||
'is_installing' => $server->installed !== 1,
|
'is_installing' => $server->installed !== 1,
|
||||||
|
'is_transferring' => $server->transfer !== null,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -40,6 +40,7 @@ export interface Server {
|
||||||
};
|
};
|
||||||
isSuspended: boolean;
|
isSuspended: boolean;
|
||||||
isInstalling: boolean;
|
isInstalling: boolean;
|
||||||
|
isTransferring: boolean;
|
||||||
variables: ServerEggVariable[];
|
variables: ServerEggVariable[];
|
||||||
allocations: Allocation[];
|
allocations: Allocation[];
|
||||||
}
|
}
|
||||||
|
@ -62,6 +63,7 @@ export const rawDataToServerObject = ({ attributes: data }: FractalResponseData)
|
||||||
featureLimits: { ...data.feature_limits },
|
featureLimits: { ...data.feature_limits },
|
||||||
isSuspended: data.is_suspended,
|
isSuspended: data.is_suspended,
|
||||||
isInstalling: data.is_installing,
|
isInstalling: data.is_installing,
|
||||||
|
isTransferring: data.is_transferring,
|
||||||
variables: ((data.relationships?.variables as FractalResponseList | undefined)?.data || []).map(rawDataToServerEggVariable),
|
variables: ((data.relationships?.variables as FractalResponseList | undefined)?.data || []).map(rawDataToServerEggVariable),
|
||||||
allocations: ((data.relationships?.allocations as FractalResponseList | undefined)?.data || []).map(rawDataToServerAllocation),
|
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 }>`
|
const StatusIndicatorBox = styled(GreyRowBox)<{ $status: ServerPowerState | undefined }>`
|
||||||
${tw`grid grid-cols-12 gap-4 relative`};
|
${tw`grid grid-cols-12 gap-4 relative`};
|
||||||
|
|
||||||
& .status-bar {
|
& .status-bar {
|
||||||
${tw`w-2 bg-red-500 absolute right-0 z-20 rounded-full m-1 opacity-50 transition-all duration-150`};
|
${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);
|
height: calc(100% - 0.5rem);
|
||||||
|
|
||||||
${({ $status }) => (!$status || $status === 'offline') ? tw`bg-red-500` : ($status === 'running' ? tw`bg-green-500` : tw`bg-yellow-500`)};
|
${({ $status }) => (!$status || $status === 'offline') ? tw`bg-red-500` : ($status === 'running' ? tw`bg-green-500` : tw`bg-yellow-500`)};
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover .status-bar {
|
&:hover .status-bar {
|
||||||
${tw`opacity-75`};
|
${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);
|
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 diskLimit = server.limits.disk !== 0 ? megabytesToHuman(server.limits.disk) : 'Unlimited';
|
||||||
const memorylimit = server.limits.memory !== 0 ? megabytesToHuman(server.limits.memory) : 'Unlimited';
|
const memoryLimit = server.limits.memory !== 0 ? megabytesToHuman(server.limits.memory) : 'Unlimited';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StatusIndicatorBox as={Link} to={`/server/${server.id}`} className={className} $status={stats?.status}>
|
<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>
|
</span>
|
||||||
</div>
|
</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>
|
<React.Fragment>
|
||||||
<div css={tw`flex-1 flex md:ml-4 sm:flex hidden justify-center`}>
|
<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)}
|
{bytesToHuman(stats.memoryUsageInBytes)}
|
||||||
</IconDescription>
|
</IconDescription>
|
||||||
</div>
|
</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>
|
||||||
<div css={tw`flex-1 ml-4 sm:block hidden`}>
|
<div css={tw`flex-1 ml-4 sm:block hidden`}>
|
||||||
<div css={tw`flex justify-center`}>
|
<div css={tw`flex justify-center`}>
|
||||||
|
@ -143,7 +150,7 @@ export default ({ server, className }: { server: Server; className?: string }) =
|
||||||
{bytesToHuman(stats.diskUsageInBytes)}
|
{bytesToHuman(stats.diskUsageInBytes)}
|
||||||
</IconDescription>
|
</IconDescription>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,7 +59,7 @@ export default () => {
|
||||||
}, [ progress, continuous ]);
|
}, [ progress, continuous ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'w-full fixed'} style={{ height: '2px' }}>
|
<div css={tw`w-full fixed`} style={{ height: '2px' }}>
|
||||||
<CSSTransition
|
<CSSTransition
|
||||||
timeout={150}
|
timeout={150}
|
||||||
appear
|
appear
|
||||||
|
|
|
@ -74,6 +74,21 @@ export default () => {
|
||||||
(prelude ? TERMINAL_PRELUDE : '') + line.replace(/(?:\r\n|\r|\n)$/im, '') + '\u001b[0m',
|
(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');
|
||||||
|
// TODO: Get WebSocket information for the target node.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleDaemonErrorOutput = (line: string) => terminal.writeln(
|
const handleDaemonErrorOutput = (line: string) => terminal.writeln(
|
||||||
TERMINAL_PRELUDE + '\u001b[1m\u001b[41m' + line.replace(/(?:\r\n|\r|\n)$/im, '') + '\u001b[0m',
|
TERMINAL_PRELUDE + '\u001b[1m\u001b[41m' + line.replace(/(?:\r\n|\r|\n)$/im, '') + '\u001b[0m',
|
||||||
);
|
);
|
||||||
|
@ -122,20 +137,23 @@ export default () => {
|
||||||
|
|
||||||
// Add support for capturing keys
|
// Add support for capturing keys
|
||||||
terminal.attachCustomKeyEventHandler((e: KeyboardEvent) => {
|
terminal.attachCustomKeyEventHandler((e: KeyboardEvent) => {
|
||||||
// Ctrl + C ( Copy )
|
// Ctrl + C (Copy)
|
||||||
if (e.ctrlKey && e.key === 'c') {
|
if (e.ctrlKey && e.key === 'c') {
|
||||||
document.execCommand('copy');
|
document.execCommand('copy');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ctrl + F (Find)
|
||||||
if (e.ctrlKey && e.key === 'f') {
|
if (e.ctrlKey && e.key === 'f') {
|
||||||
searchBar.show();
|
searchBar.show();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Escape
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
searchBar.hidden();
|
searchBar.hidden();
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -154,17 +172,21 @@ export default () => {
|
||||||
instance.addListener('status', handlePowerChangeEvent);
|
instance.addListener('status', handlePowerChangeEvent);
|
||||||
instance.addListener('console output', handleConsoleOutput);
|
instance.addListener('console output', handleConsoleOutput);
|
||||||
instance.addListener('install 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 message', line => handleConsoleOutput(line, true));
|
||||||
instance.addListener('daemon error', handleDaemonErrorOutput);
|
instance.addListener('daemon error', handleDaemonErrorOutput);
|
||||||
instance.send('send logs');
|
instance.send('send logs');
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
instance && instance.removeListener('console output', handleConsoleOutput)
|
instance && instance.removeListener('status', handlePowerChangeEvent)
|
||||||
|
.removeListener('console output', handleConsoleOutput)
|
||||||
.removeListener('install output', handleConsoleOutput)
|
.removeListener('install output', handleConsoleOutput)
|
||||||
|
.removeListener('transfer logs', handleConsoleOutput)
|
||||||
|
.removeListener('transfer status', handleTransferStatus)
|
||||||
.removeListener('daemon message', line => handleConsoleOutput(line, true))
|
.removeListener('daemon message', line => handleConsoleOutput(line, true))
|
||||||
.removeListener('daemon error', handleDaemonErrorOutput)
|
.removeListener('daemon error', handleDaemonErrorOutput);
|
||||||
.removeListener('status', handlePowerChangeEvent);
|
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [ connected, instance ]);
|
}, [ connected, instance ]);
|
||||||
|
|
|
@ -17,6 +17,7 @@ const ChunkedStatGraphs = lazy(() => import(/* webpackChunkName: "graphs" */'@/c
|
||||||
|
|
||||||
const ServerConsole = () => {
|
const ServerConsole = () => {
|
||||||
const isInstalling = ServerContext.useStoreState(state => state.server.data!.isInstalling);
|
const isInstalling = ServerContext.useStoreState(state => state.server.data!.isInstalling);
|
||||||
|
const isTransferring = ServerContext.useStoreState(state => state.server.data!.isTransferring);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const eggFeatures: string[] = ServerContext.useStoreState(state => state.server.data!.eggFeatures, isEqual);
|
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`}>
|
<ServerContentBlock title={'Console'} css={tw`flex flex-wrap`}>
|
||||||
<div css={tw`w-full lg:w-1/4`}>
|
<div css={tw`w-full lg:w-1/4`}>
|
||||||
<ServerDetailsBlock/>
|
<ServerDetailsBlock/>
|
||||||
{!isInstalling ?
|
{isInstalling ?
|
||||||
<Can action={[ 'control.start', 'control.stop', 'control.restart' ]} matchAny>
|
|
||||||
<PowerControls/>
|
|
||||||
</Can>
|
|
||||||
:
|
|
||||||
<div css={tw`mt-4 rounded bg-yellow-500 p-3`}>
|
<div css={tw`mt-4 rounded bg-yellow-500 p-3`}>
|
||||||
<ContentContainer>
|
<ContentContainer>
|
||||||
<p css={tw`text-sm text-yellow-900`}>
|
<p css={tw`text-sm text-yellow-900`}>
|
||||||
|
@ -37,6 +34,20 @@ const ServerConsole = () => {
|
||||||
</p>
|
</p>
|
||||||
</ContentContainer>
|
</ContentContainer>
|
||||||
</div>
|
</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>
|
||||||
<div css={tw`w-full lg:w-3/4 mt-4 lg:mt-0 lg:pl-4`}>
|
<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 name = ServerContext.useStoreState(state => state.server.data!.name);
|
||||||
const isInstalling = ServerContext.useStoreState(state => state.server.data!.isInstalling);
|
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 limits = ServerContext.useStoreState(state => state.server.data!.limits);
|
||||||
const primaryAllocation = ServerContext.useStoreState(state => state.server.data!.allocations.filter(alloc => alloc.isDefault).map(
|
const primaryAllocation = ServerContext.useStoreState(state => state.server.data!.allocations.filter(alloc => alloc.isDefault).map(
|
||||||
allocation => (allocation.alias || allocation.ip) + ':' + allocation.port
|
allocation => (allocation.alias || allocation.ip) + ':' + allocation.port
|
||||||
)).toString();
|
)).toString();
|
||||||
|
|
||||||
const disklimit = limits.disk ? megabytesToHuman(limits.disk) : 'Unlimited';
|
const diskLimit = limits.disk ? megabytesToHuman(limits.disk) : 'Unlimited';
|
||||||
const memorylimit = limits.memory ? megabytesToHuman(limits.memory) : 'Unlimited';
|
const memoryLimit = limits.memory ? megabytesToHuman(limits.memory) : 'Unlimited';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TitledGreyBox css={tw`break-words`} title={name} icon={faServer}>
|
<TitledGreyBox css={tw`break-words`} title={name} icon={faServer}>
|
||||||
|
@ -81,10 +82,10 @@ const ServerDetailsBlock = () => {
|
||||||
fixedWidth
|
fixedWidth
|
||||||
css={[
|
css={[
|
||||||
tw`mr-1`,
|
tw`mr-1`,
|
||||||
statusToColor(status, isInstalling),
|
statusToColor(status, isInstalling || isTransferring),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
{!status ? 'Connecting...' : (isInstalling ? 'Installing' : status)}
|
{!status ? 'Connecting...' : (isInstalling ? 'Installing' : (isTransferring) ? 'Transferring' : status)}
|
||||||
</p>
|
</p>
|
||||||
<CopyOnClick text={primaryAllocation}>
|
<CopyOnClick text={primaryAllocation}>
|
||||||
<p css={tw`text-xs mt-2`}>
|
<p css={tw`text-xs mt-2`}>
|
||||||
|
@ -97,11 +98,11 @@ const ServerDetailsBlock = () => {
|
||||||
</p>
|
</p>
|
||||||
<p css={tw`text-xs mt-2`}>
|
<p css={tw`text-xs mt-2`}>
|
||||||
<FontAwesomeIcon icon={faMemory} fixedWidth css={tw`mr-1`}/> {bytesToHuman(stats.memory)}
|
<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>
|
||||||
<p css={tw`text-xs mt-2`}>
|
<p css={tw`text-xs mt-2`}>
|
||||||
<FontAwesomeIcon icon={faHdd} fixedWidth css={tw`mr-1`}/> {bytesToHuman(stats.disk)}
|
<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>
|
</p>
|
||||||
</TitledGreyBox>
|
</TitledGreyBox>
|
||||||
);
|
);
|
||||||
|
|
|
@ -35,10 +35,12 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
|
||||||
const rootAdmin = useStoreState(state => state.user.data!.rootAdmin);
|
const rootAdmin = useStoreState(state => state.user.data!.rootAdmin);
|
||||||
const [ error, setError ] = useState('');
|
const [ error, setError ] = useState('');
|
||||||
const [ installing, setInstalling ] = useState(false);
|
const [ installing, setInstalling ] = useState(false);
|
||||||
|
const [ transferring, setTransferring ] = useState(false);
|
||||||
|
|
||||||
const id = ServerContext.useStoreState(state => state.server.data?.id);
|
const id = ServerContext.useStoreState(state => state.server.data?.id);
|
||||||
const uuid = ServerContext.useStoreState(state => state.server.data?.uuid);
|
const uuid = ServerContext.useStoreState(state => state.server.data?.uuid);
|
||||||
const isInstalling = ServerContext.useStoreState(state => state.server.data?.isInstalling);
|
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 serverId = ServerContext.useStoreState(state => state.server.data?.internalId);
|
||||||
const getServer = ServerContext.useStoreActions(actions => actions.server.getServer);
|
const getServer = ServerContext.useStoreActions(actions => actions.server.getServer);
|
||||||
const clearServerState = ServerContext.useStoreActions(actions => actions.clearServerState);
|
const clearServerState = ServerContext.useStoreActions(actions => actions.clearServerState);
|
||||||
|
@ -51,13 +53,23 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
|
||||||
setInstalling(!!isInstalling);
|
setInstalling(!!isInstalling);
|
||||||
}, [ isInstalling ]);
|
}, [ isInstalling ]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTransferring(!!isTransferring);
|
||||||
|
}, [ isTransferring ]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setError('');
|
setError('');
|
||||||
setInstalling(false);
|
setInstalling(false);
|
||||||
|
setTransferring(false);
|
||||||
|
|
||||||
getServer(match.params.id)
|
getServer(match.params.id)
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
if (error.response?.status === 409) {
|
if (error.response?.status === 409) {
|
||||||
setInstalling(true);
|
if (error.response.data?.errors[0]?.detail?.includes('transfer')) {
|
||||||
|
setTransferring(true);
|
||||||
|
} else {
|
||||||
|
setInstalling(true);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
setError(httpErrorToHuman(error));
|
setError(httpErrorToHuman(error));
|
||||||
|
@ -117,9 +129,9 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
|
||||||
</CSSTransition>
|
</CSSTransition>
|
||||||
<InstallListener/>
|
<InstallListener/>
|
||||||
<WebsocketHandler/>
|
<WebsocketHandler/>
|
||||||
{(installing && (!rootAdmin || (rootAdmin && !location.pathname.endsWith(`/server/${id}`)))) ?
|
{((installing || transferring) && (!rootAdmin || (rootAdmin && !location.pathname.endsWith(`/server/${id}`)))) ?
|
||||||
<ScreenBlock
|
<ScreenBlock
|
||||||
title={'Your server is installing.'}
|
title={installing ? 'Your server is installing.' : 'Your server is currently being transferred.'}
|
||||||
image={'/assets/svgs/server_installing.svg'}
|
image={'/assets/svgs/server_installing.svg'}
|
||||||
message={'Please check back in a few minutes.'}
|
message={'Please check back in a few minutes.'}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -58,6 +58,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if(! $server->suspended)
|
@if(! $server->suspended)
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4">
|
||||||
<div class="box box-warning">
|
<div class="box box-warning">
|
||||||
|
@ -96,28 +97,48 @@
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<div class="col-sm-4">
|
@if($server->transfer === null)
|
||||||
<div class="box box-success">
|
<div class="col-sm-4">
|
||||||
<div class="box-header with-border">
|
<div class="box box-success">
|
||||||
<h3 class="box-title">Transfer Server</h3>
|
<div class="box-header with-border">
|
||||||
</div>
|
<h3 class="box-title">Transfer Server</h3>
|
||||||
<div class="box-body">
|
</div>
|
||||||
<p>
|
<div class="box-body">
|
||||||
Transfer this server to another node connected to this panel.
|
<p>
|
||||||
<strong>Warning!</strong> This feature has not been fully tested and may have bugs.
|
Transfer this server to another node connected to this panel.
|
||||||
</p>
|
<strong>Warning!</strong> This feature has not been fully tested and may have bugs.
|
||||||
</div>
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="box-footer">
|
<div class="box-footer">
|
||||||
@if($canTransfer)
|
@if($canTransfer)
|
||||||
<button class="btn btn-success" data-toggle="modal" data-target="#transferServerModal">Transfer Server</button>
|
<button class="btn btn-success" data-toggle="modal" data-target="#transferServerModal">Transfer Server</button>
|
||||||
@else
|
@else
|
||||||
<button class="btn btn-success disabled">Transfer Server</button>
|
<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>
|
<p style="padding-top: 1rem;">Transferring a server requires more than one node to be configured on your panel.</p>
|
||||||
@endif
|
@endif
|
||||||
|
</div>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div class="modal fade" id="transferServerModal" tabindex="-1" role="dialog">
|
<div class="modal fade" id="transferServerModal" tabindex="-1" role="dialog">
|
||||||
|
|
Loading…
Reference in a new issue