diff --git a/app/Http/Controllers/Api/Application/Servers/ServerController.php b/app/Http/Controllers/Api/Application/Servers/ServerController.php index b1e80c4719..edd250070d 100644 --- a/app/Http/Controllers/Api/Application/Servers/ServerController.php +++ b/app/Http/Controllers/Api/Application/Servers/ServerController.php @@ -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); diff --git a/app/Http/Controllers/Api/Application/Servers/ServerTransferController.php b/app/Http/Controllers/Api/Application/Servers/ServerTransferController.php new file mode 100644 index 0000000000..6636333cd9 --- /dev/null +++ b/app/Http/Controllers/Api/Application/Servers/ServerTransferController.php @@ -0,0 +1,126 @@ +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]); + } + } +} diff --git a/app/Http/Requests/Api/Application/Servers/Transfers/GetServerTransferRequest.php b/app/Http/Requests/Api/Application/Servers/Transfers/GetServerTransferRequest.php new file mode 100644 index 0000000000..d94e8968fc --- /dev/null +++ b/app/Http/Requests/Api/Application/Servers/Transfers/GetServerTransferRequest.php @@ -0,0 +1,13 @@ + 'required|exists:nodes,id', + 'allocation_id' => 'nullable|bail|unique:servers|exists:allocations,id', + 'allocation_additional' => 'nullable', + ]; + } +} diff --git a/app/Transformers/Api/Application/ServerTransferTransformer.php b/app/Transformers/Api/Application/ServerTransferTransformer.php new file mode 100644 index 0000000000..0ce94d335f --- /dev/null +++ b/app/Transformers/Api/Application/ServerTransferTransformer.php @@ -0,0 +1,35 @@ + $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, + ] + ]; + } +} diff --git a/routes/api-application.php b/routes/api-application.php index dc6b0e5bb6..240d949c9b 100644 --- a/routes/api-application.php +++ b/routes/api-application.php @@ -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');