Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public function __construct(
public function index(GetServersRequest $request): array
{
$servers = QueryBuilder::for(Server::query())
->allowedFilters(['uuid', 'uuidShort', 'name', 'description', 'image', 'external_id'])
->allowedFilters(['uuid', 'uuidShort', 'name', 'description', 'image', 'external_id', "node_id"])
->allowedSorts(['id', 'uuid'])
->paginate($request->query('per_page') ?? 50);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?php

namespace Pterodactyl\Http\Controllers\Api\Application\Servers;

use Carbon\CarbonImmutable;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Http\JsonResponse;
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController;
use Pterodactyl\Http\Requests\Api\Application\Servers\Transfers\GetServerTransferRequest;
use Pterodactyl\Http\Requests\Api\Application\Servers\Transfers\StoreServerTransferRequest;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\ServerTransfer;
use Pterodactyl\Repositories\Eloquent\NodeRepository;
use Pterodactyl\Repositories\Wings\DaemonTransferRepository;
use Pterodactyl\Services\Nodes\NodeJWTService;
use Pterodactyl\Transformers\Api\Application\ServerTransferTransformer;
use Spatie\QueryBuilder\QueryBuilder;

class ServerTransferController extends ApplicationApiController
{
/**
* ServerTransferController constructor.
*/
public function __construct(
private AllocationRepositoryInterface $allocationRepository,
private ConnectionInterface $connection,
private DaemonTransferRepository $repository,
private NodeJWTService $nodeJWTService,
private NodeRepository $nodeRepository,
) {
parent::__construct();
}

public function index(GetServerTransferRequest $request, Server $server)
{
$transfers = QueryBuilder::for(ServerTransfer::query())
->where('server_id', $server->id)
->allowedFilters(['id', 'server_id', 'successful'])
->allowedSorts(['id', 'created_at'])
->defaultSort('-created_at')
->paginate($request->query('per_page') ?? 50);

return $this->fractal->collection($transfers)->transformWith($this->getTransformer(ServerTransferTransformer::class))->toArray();
}

/**
* @throws DisplayException
*/
public function store(StoreServerTransferRequest $request, Server $server)
{
$node_id = $request->input('node_id');
$allocation_id = $request->input('allocation_id', $this->getAvailableIp($node_id));
$additional_allocations = $request->input('additional_allocations', []);

$node = $this->nodeRepository->getNodeWithResourceUsage($node_id);
if(! $node->isViable($server->memory, $server->disk)) {
throw new DisplayException('Node is not viable for this server');
}

$server->validateTransferState();

$transfer = new ServerTransfer();
$transfer->server_id = $server->id;
$transfer->old_node = $server->node_id;
$transfer->new_node = $node_id;
$transfer->old_allocation = $server->allocation_id;
$transfer->new_allocation = $allocation_id;
$transfer->old_additional_allocations = $server->allocations->where('id', '!=', $server->allocation_id)->pluck('id');
$transfer->new_additional_allocations = $additional_allocations;

$this->connection->transaction(function () use ($server, $node_id, $transfer, $allocation_id, $additional_allocations) {
// Create a new ServerTransfer entry.
$transfer->save();

// Add the allocations to the server, so they cannot be automatically assigned while the transfer is in progress.
$this->assignAllocationsToServer($server, $node_id, $allocation_id, $additional_allocations);

// Generate a token for the destination node that the source node can use to authenticate with.
$token = $this->nodeJWTService
->setExpiresAt(CarbonImmutable::now()->addMinutes(15))
->setSubject($server->uuid)
->handle($transfer->newNode, $server->uuid, 'sha256');

// Notify the source node of the pending outgoing transfer.
$this->repository->setServer($server)->notify($transfer->newNode, $token);

return $transfer;
});

return $this->fractal->item($transfer)->transformWith($this->getTransformer(ServerTransferTransformer::class))->respond(201);
}

private function getAvailableIp(int $node_id): int
{
$unassigned = $this->allocationRepository->getUnassignedAllocationIds($node_id);

if(empty($unassigned)) {
throw new DisplayException('No unassigned IPs found on the node');
}

return $unassigned[0];
}

private function assignAllocationsToServer(Server $server, int $node_id, int $allocation_id, array $additional_allocations): void
{
$allocations = $additional_allocations;
$allocations[] = $allocation_id;

$unassigned = $this->allocationRepository->getUnassignedAllocationIds($node_id);

$updateIds = [];
foreach ($allocations as $allocation) {
if (! in_array($allocation, $unassigned)) {
continue;
}

$updateIds[] = $allocation;
}

if (! empty($updateIds)) {
$this->allocationRepository->updateWhereIn('id', $updateIds, ['server_id' => $server->id]);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Pterodactyl\Http\Requests\Api\Application\Servers\Transfers;

use Pterodactyl\Services\Acl\Api\AdminAcl;
use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest;

class GetServerTransferRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_SERVERS;

protected int $permission = AdminAcl::READ;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace Pterodactyl\Http\Requests\Api\Application\Servers\Transfers;

use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest;
use Pterodactyl\Services\Acl\Api\AdminAcl;

class StoreServerTransferRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_SERVERS;

protected int $permission = AdminAcl::WRITE;

public function rules(): array
{
return [
'node_id' => 'required|exists:nodes,id',
'allocation_id' => 'nullable|bail|unique:servers|exists:allocations,id',
'allocation_additional' => 'nullable',
];
}
}
35 changes: 35 additions & 0 deletions app/Transformers/Api/Application/ServerTransferTransformer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace Pterodactyl\Transformers\Api\Application;

use Pterodactyl\Models\ServerTransfer;

class ServerTransferTransformer extends BaseTransformer
{
public function getResourceName(): string
{
return ServerTransfer::RESOURCE_NAME;
}

public function transform(ServerTransfer $model): array
{
return [
'id' => $model->id,
'server_id' => $model->server_id,
'successful' => $model->successful,
'archived' => $model->archived,
'node' => [
'old' => $model->old_node,
'new' => $model->new_node,
],
'allocations' => [
'old' => $model->new_allocation,
'new' => $model->old_allocation,
],
'additional_allocations' => [
'old' => $model->old_additional_allocations,
'new' => $model->new_additional_allocations,
]
];
}
}
5 changes: 5 additions & 0 deletions routes/api-application.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@
Route::delete('/{server:id}', [Application\Servers\ServerController::class, 'delete']);
Route::delete('/{server:id}/{force?}', [Application\Servers\ServerController::class, 'delete']);

Route::group(['prefix' => '/{server:id}/transfer'], function () {
Route::get('/', [Application\Servers\ServerTransferController::class, 'index'])->name('api.application.servers.transfer');
Route::post('/', [Application\Servers\ServerTransferController::class, 'store']);
});

// Database Management Endpoint
Route::group(['prefix' => '/{server:id}/databases'], function () {
Route::get('/', [Application\Servers\DatabaseController::class, 'index'])->name('api.application.servers.databases');
Expand Down